Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ yarn-error.log*
# OS
.DS_Store
Thumbs.db

# Python
__pycache__/
*.pyc
.venv/
*.egg-info/
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ project directories that live alongside it on the same machine.

- **`docs/`** — system-setup and integration documentation (rclone, Google APIs, etc.)
- **`scripts/`** — workspace bootstrapping, environment checks, and example scraping/data-prep utilities
- **`gig-scraper/`** — Playwright + xlsx scrapers for JoshMariaMusic gig booking
- **`gemma-cli/`** — local Gemma 4 Coordinator (Python package; editable pip install with its own venv)
- **`CLAUDE.md` / `GEMINI.md`** — orientation and rules for AI assistants working in the workspace

## Getting started
Expand All @@ -27,6 +29,56 @@ Then read:
- [docs/rclone-setup.md](docs/rclone-setup.md) — mounting Google Drive locally via rclone + systemd
- [docs/api-integrations.md](docs/api-integrations.md) — reference snapshot of one working setup (machine-specific paths; use the generic guide above for your own setup)

## VSCode multi-root workspace

`WebJamApps.code-workspace` is a multi-root VSCode workspace that opens this repo alongside its sibling repos (JaMmusic, CollegeLutheran, AppersonAuto, web-jam-back, WebJamSocketCluster, WebJamPg). It uses **relative paths**, so for it to work the sibling repos need to be cloned next to `web-jam-tools/`:

```text
~/WebJamApps/
├── web-jam-tools/ ← this repo
│ └── WebJamApps.code-workspace
├── JaMmusic/
├── CollegeLutheran/
├── AppersonAuto/
├── web-jam-back/
├── WebJamSocketCluster/
└── WebJamPg/
```

The committed copy of the workspace file lives in this repo; on the maintainer's machine a symlink at `~/WebJamApps/WebJamApps.code-workspace` points to it so the relative paths inside resolve correctly. To set up the same on a fresh checkout:

```bash
ln -s ~/WebJamApps/web-jam-tools/WebJamApps.code-workspace ~/WebJamApps/WebJamApps.code-workspace
```

Then `File → Open Workspace from File...` → that symlink (or the file directly).

## gemma-cli setup notes

`gemma-cli/` is a Python package designed for editable install in its own venv. From `web-jam-tools/gemma-cli/`:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
```

Optionally put it on your PATH so `gemma` works from anywhere:

```bash
ln -s "$(pwd)/.venv/bin/gemma" ~/.local/bin/gemma
```

### Compatibility symlink (if migrating from a previous location)

`gemma-cli` lived at `~/WebJamApps/gemma-cli/` (a sibling of `web-jam-tools/`) before being moved inside this repo on 2026-05-13. Existing wrapper scripts, cron entries, or shell aliases that reference the old absolute path keep working if you leave a symlink at the old location pointing to the new one:

```bash
ln -s ~/WebJamApps/web-jam-tools/gemma-cli ~/WebJamApps/gemma-cli
```

Why a symlink instead of rebuilding: a Python venv bakes absolute paths into its activate script, shebang lines, and `.pth` files. The symlink lets every existing reference resolve transparently without needing to recreate the venv or grep your dotfiles for the old path.

## Contributing

- Branch from `dev`, open a PR against `dev`. Do not merge to `dev` or `main` from an AI assistant — a human reviewer is required.
Expand Down
119 changes: 119 additions & 0 deletions WebJamApps.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{
"folders": [
{ "name": "JaMmusic", "path": "JaMmusic" },
{ "name": "CollegeLutheran", "path": "CollegeLutheran" },
{ "name": "AppersonAuto", "path": "AppersonAuto" },
{ "name": "web-jam-back", "path": "web-jam-back" },
{ "name": "WebJamSocketCluster", "path": "WebJamSocketCluster" },
{ "name": "WebJamPg", "path": "WebJamPg" },
{ "name": "web-jam-tools", "path": "web-jam-tools" }
],
"settings": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
"eslint.workingDirectories": [
{ "pattern": "./*" }
],
"vitest.disableWorkspaceWarning": true,
"files.exclude": {
"**/node_modules": true,
"**/.git": true,
"**/coverage": true,
"**/dist": true
},
"search.exclude": {
"**/node_modules": true,
"**/coverage": true,
"**/dist": true,
"**/package-lock.json": true
},
"typescript.tsdk": "JaMmusic/node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"git.ignoredRepositories": [
"/home/joshua/WebJamApps/web-jam-back/JaMmusic",
"/home/joshua/WebJamApps/WebJamSocketCluster/JaMmusic"
]
},
"extensions": {
"recommendations": [
"vitest.explorer",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"stylelint.vscode-stylelint",
"eamodio.gitlens",
"google.gemini-cli-vscode-ide-companion",
"google.geminicodeassist"
],
"unwantedRecommendations": [
"orta.vscode-jest",
"hbenl.vscode-test-explorer",
"ms-vscode.test-adapter-converter"
]
},
"tasks": {
"version": "2.0.0",
"tasks": [
{
"label": "web-jam-back: dev (install + run)",
"type": "shell",
"command": "git checkout dev && npm install --ignore-scripts && npm run dev",
"options": {
"cwd": "${workspaceFolder:web-jam-back}",
"shell": { "executable": "/bin/bash", "args": ["-l", "-c"] }
},
"presentation": {
"panel": "new",
"reveal": "always",
"focus": false,
"showReuseMessage": false,
"clear": true
},
"isBackground": true,
"problemMatcher": []
},
{
"label": "WebJamSocketCluster: dev (install + run)",
"type": "shell",
"command": "git checkout dev && npm install --ignore-scripts && npm run dev",
"options": {
"cwd": "${workspaceFolder:WebJamSocketCluster}",
"shell": { "executable": "/bin/bash", "args": ["-l", "-c"] }
},
"presentation": {
"panel": "new",
"reveal": "always",
"focus": false,
"showReuseMessage": false,
"clear": true
},
"isBackground": true,
"problemMatcher": []
},
{
"label": "Start backend + socket cluster (dev)",
"dependsOn": [
"web-jam-back: dev (install + run)",
"WebJamSocketCluster: dev (install + run)"
],
"dependsOrder": "parallel",
"problemMatcher": []
},
{
"label": "Gemma: Coordinator REPL (gemma)",
"type": "shell",
"command": "gemma",
"presentation": {
"panel": "dedicated",
"reveal": "always",
"focus": true,
"showReuseMessage": false,
"clear": false
},
"problemMatcher": []
}
]
}
}
5 changes: 5 additions & 0 deletions gemma-cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.venv/
__pycache__/
*.egg-info/
*.pyc
.env
1 change: 1 addition & 0 deletions gemma-cli/gemma_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
38 changes: 38 additions & 0 deletions gemma-cli/gemma_cli/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Load Google OAuth credentials by reusing the existing google-drive-mcp token.

The Drive MCP token already has the scopes we need (drive, calendar, calendar.events,
documents, spreadsheets, drive.file). We piggy-back on it rather than running our own
OAuth dance.
"""

from __future__ import annotations

import json
from pathlib import Path

from google.oauth2.credentials import Credentials

DRIVE_MCP_DIR = Path.home() / ".config" / "google-drive-mcp"
GMAIL_MCP_DIR = Path.home() / ".gmail-mcp"


def _load(token_path: Path, keys_path: Path) -> Credentials:
token_data = json.loads(token_path.read_text())
keys_data = json.loads(keys_path.read_text())
installed = keys_data.get("installed") or keys_data.get("web") or {}
return Credentials(
token=token_data["access_token"],
refresh_token=token_data.get("refresh_token"),
token_uri="https://oauth2.googleapis.com/token",
client_id=installed.get("client_id"),
client_secret=installed.get("client_secret"),
scopes=token_data.get("scope", "").split(),
)


def load_credentials() -> Credentials:
return _load(DRIVE_MCP_DIR / "tokens.json", DRIVE_MCP_DIR / "gcp-oauth.keys.json")


def load_gmail_credentials() -> Credentials:
return _load(GMAIL_MCP_DIR / "credentials.json", GMAIL_MCP_DIR / "gcp-oauth.keys.json")
Loading