diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index fb44898..7759435 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -51,7 +51,7 @@ body: - librarian - transcriber - postman - - Routing / dispatcher (CLAUDE.md) + - Routing / dispatcher (e.g. CLAUDE.md) - Unknown / not sure validations: required: true @@ -80,11 +80,11 @@ body: id: logs attributes: label: Logs / error output - description: Paste any relevant error messages or Claude Code output. + description: Paste any relevant error messages or agent output. render: text - type: textarea id: context attributes: label: Additional context - description: Screenshots, OS, Claude model used, anything else that might help. + description: Screenshots, OS, agentic platform and LLM model used, anything else that might help. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index c955177..78df07c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -33,7 +33,7 @@ body: description: Which part of the project does this affect? options: - Specific agent (specify below) - - Routing / CLAUDE.md + - Routing / Dispatcher / CLAUDE.md - Installer / scripts - Documentation - References / shared docs diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md old mode 100644 new mode 100755 index 0637b3a..962a463 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -29,9 +29,9 @@ - [ ] I have read the [Contributing Guide](CONTRIBUTING.md) - [ ] Agent files are written in English - [ ] Trigger phrases include at least English and Italian -- [ ] New/modified agents follow the frontmatter format (`name`, `description`, `tools`, `model`) +- [ ] New/modified agents follow the source frontmatter format (`name`, `description`, `capabilities`, `model`) - [ ] Inter-agent messaging protocol is respected (if applicable) -- [ ] I have tested this with `claude --plugin-dir ./` +- [ ] I have tested this with at least one platform (`bash scripts/build.sh --platform claude-code`) --- diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index efb0bfd..02597a3 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Build artifacts dist/ -scripts/build.sh logs/ # macOS .DS_Store @@ -9,3 +8,4 @@ Test-SecondBrain/ *.zip # Dev config .claude/settings.local.json +.mcp.json \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100644 new mode 100755 index 5a79acb..0acee55 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,16 +13,17 @@ Found that an agent behaves weirdly, gives poor results, or misses edge cases? 1. Open an issue describing the problem with a concrete example 2. Or submit a PR with the improvement -Agent files live in `agents/.md`. The plugin manifest is at `.claude-plugin/plugin.json`. All agents are written in English, and they automatically respond in the user's language. +Agent source files live in `agents/.md`. They use a platform-neutral format with `capabilities:` (not tool names) and `model`: `low`/`mid`/`high` (not platform-specific model names). The build system translates these into each platform's native format. All agents are written in English, and they automatically respond in the user's language. -To test your changes locally: +To test your changes locally, build and install into a test vault: ```bash -claude --plugin-dir ./ +bash scripts/build.sh --platform claude-code # or gemini-cli, opencode, etc. +bash scripts/launchme.sh --platform claude-code --target /tmp/test-vault ``` ### Propose a new core crew member -> **Note**: Users can create custom agents directly within their vault by saying "create a new agent" in Claude Code. The Architect handles the entire process. The section below is for proposing new *core* agents that ship with the project. +> **Note**: Users can create custom agents directly within their vault by saying "create a new agent". The Architect handles the entire process. The section below is for proposing new *core* agents that ship with the project. Have an idea for a new core agent? Open an issue with: @@ -50,7 +51,7 @@ Open an issue with: ## Agent file structure -Each agent is a Claude Code **subagent**, a standalone `.md` file with YAML frontmatter: +Each agent is a standalone `.md` file with YAML frontmatter in the **source format** (platform-neutral): ```yaml --- @@ -59,8 +60,8 @@ description: > One paragraph description used for auto-triggering. Include trigger phrases in multiple languages (English, Italian, French, Spanish, German, Portuguese) for maximum discoverability. -tools: Read, Write, Edit, Glob, Grep -model: sonnet +capabilities: [read, write, edit] +model: mid --- # @@ -68,15 +69,17 @@ model: sonnet [Agent instructions in English] ``` -### Frontmatter fields +The build system translates `capabilities` into platform-specific tool lists or permission blocks, and `model` into platform-specific model names (e.g., `mid` → `sonnet` for Claude Code, `gemini-2.5-flash` for Gemini CLI). + +### Frontmatter fields (source format) | Field | Required | Description | |-------|----------|-------------| | `name` | Yes | Lowercase, hyphens only (e.g., `my-agent`) | -| `description` | Yes | When Claude should auto-invoke this agent. Include multilingual triggers | -| `tools` | Yes | Comma-separated list of allowed tools | -| `disallowedTools` | No | Tools to explicitly deny (e.g., `Write, Edit` for read-only agents) | -| `model` | No | `sonnet`, `opus`, or `haiku` (default: inherits from parent) | +| `description` | Yes | When the platform should auto-invoke this agent. Include multilingual triggers | +| `capabilities` | Yes | List from: `read`, `write`, `edit`, `bash`, `webfetch`, `websearch`, `task`, `todo` | +| `model` | No | `low`, `mid`, or `high` (default: inherits from parent) | +| `exclude` | No | List of platforms to exclude this agent from (e.g., `[opencode]`) | ### Key rules for agent files @@ -97,7 +100,7 @@ Agents coordinate through a dispatcher-driven orchestration system. When an agen ## Custom agents vs. core agents -**Custom agents** are created by users within their own vault using the Architect agent. They live in the user's `.claude/agents/` directory and are personal to that vault. Custom agents: +**Custom agents** are created by users within their own vault using the Architect agent. They live in the user's platform agents directory (e.g., `.claude/agents/`) and are personal to that vault. Custom agents: - Are created through a conversational flow with the Architect - Follow the same file structure and conventions as core agents - Participate in the dispatcher's routing and orchestration system @@ -125,6 +128,122 @@ If your custom agent solves a problem that many users would benefit from, consid --- +## Hooks + +Three hooks ship with the crew, protecting vault integrity across all platforms: + +| Hook | Event | What it does | +|------|-------|-------------| +| `protect-system-files` | `before-tool-use` | Blocks edits to core agents, skills, references, and the dispatcher file. Custom agents are allowed through. | +| `validate-frontmatter` | `after-tool-use` | Warns if a written `.md` file has broken YAML frontmatter (missing delimiters, tabs, unquoted colons). | +| `notify` | `on-notification` | Sends a desktop notification (macOS/Linux) when the platform needs attention during long agent chains. | + +Hook source files live in `hooks/`. Each hook has a `.hook.yaml` (metadata: name, script, triggers, match-tool filters) and a `.sh` (implementation). Hooks are **platform-agnostic** — they read `platform_dir` and `dispatcher_name` from the neutral JSON input to determine which paths to protect. The adapter layer handles translating platform-native events into the neutral schema before calling the hooks. + +If you add a new hook: +1. Create `hooks/.hook.yaml` with `name`, `script`, `triggers` (using the neutral event vocabulary: `before-tool-use`, `after-tool-use`, `on-notification`, `on-session-start`, `on-prompt-submit`) +2. Create `hooks/.sh` reading neutral JSON from stdin +3. Use `$PLATFORM_DIR` and `$DISPATCHER_NAME` (extracted from JSON input) instead of hardcoded paths + +--- + +## Adding a new platform adapter + +The build system uses a **source-of-truth + per-platform adapters** architecture. Source files (`agents/`, `skills/`, `references/`, `hooks/`, `DISPATCHER.md`) are platform-neutral. Each adapter translates them into a platform's native format. + +### Adapter contract + +Every adapter is a single file at `adapters//adapter.sh` that implements these functions: + +| Function | Responsibility | +|----------|---------------| +| `adapter_translate_dispatcher(src, dst)` | Copy `DISPATCHER.md` to the platform's dispatcher filename | +| `adapter_translate_references(src, dst)` | Copy reference `.md` files to the platform's references directory | +| `adapter_translate_skills(src, dst)` | Copy skill `SKILL.md` files to the platform's skills directory | +| `adapter_translate_agents(src, dst)` | Translate agent frontmatter (capabilities → tools/permissions, model → native name) and write to agents directory | +| `adapter_translate_hooks(src, dst)` | Copy hook scripts and generate platform-native hook configuration (settings.json, JS plugin, etc.) | +| `adapter_translate_mcp(src, dst)` | Read `mcp/servers.yaml` and write platform-native MCP config | +| `adapter_finalize(src, dst)` | Any final assembly (e.g., merging multiple config files into one) | + +The entry point is `adapter_build(src, dst)` which calls all seven functions in order. + +### How to add a new platform + +1. **Create the adapter directory**: `mkdir -p adapters//templates/` + +2. **Create `adapters//adapter.sh`** with: + - Platform constants (e.g., `MY_PLATFORM="my-platform"`, `MY_FW_DIR="myplatform"`, `MY_DISPATCHER="MY_DISPATCH.md"`) + - Vocabulary mapping functions (capabilities → native tools, events → native events, model tiers → native model names) + - All 7 `adapter_translate_*` functions + `adapter_build` + - Call `rewrite_platform_paths "$file" "$MY_FW_DIR" "$MY_DISPATCHER"` on every output text file + +3. **Add the platform to install scripts**: add a case to the `case "$PLATFORM"` block in `scripts/launchme.sh` and `scripts/updateme.sh`, setting `DIST_COMPONENTS_DIR`, `VAULT_COMPONENTS_DIR`, `DISPATCHER_SRC`, `DISPATCHER_DST`, `MCP_SRC`, `MCP_DST`, and `HAS_PLUGINS`. + +4. **Write tests**: create `tests/adapters//adapter.test.sh` with tests for each translation function. + +5. **Verify**: `bash scripts/build.sh --platform ` should produce a complete `dist//` tree. Check that no `.platform/` or `DISPATCHER.md` placeholders leak into the output. + +The shared library `adapters/lib.sh` provides parsing helpers (`parse_frontmatter`, `parse_capabilities`, `should_include`, `parse_hook_yaml`, `agent_body`, `enumerate_agents`, `enumerate_hooks`) and the `rewrite_platform_paths` function. Your adapter sources this automatically via `scripts/build.sh`. + +Look at `adapters/claude-code/adapter.sh` or `adapters/gemini-cli/adapter.sh` as reference implementations. + +--- + +## Testing + +The project has two levels of tests: + +### Unit tests + +Per-adapter unit tests live in `tests/adapters/`: + +``` +tests/adapters/ +├── lib.test.sh Shared library tests (18 tests) +├── claude-code/adapter.test.sh CC adapter tests (10 tests) +├── opencode/adapter.test.sh OC adapter tests (17 tests) +├── opencode/config-merge.test.sh OC config merge tests (6 tests) +└── gemini-cli/adapter.test.sh Gemini adapter tests (13 tests) +``` + +Run them in isolation (each adapter must be tested in its own shell since they share function names): + +```bash +# All lib tests +bash -c 'source adapters/lib.sh; source tests/adapters/lib.test.sh; P=0; F=0; for fn in $(declare -F | awk "{print \$3}" | grep "^test_"); do $fn >/dev/null 2>&1 && P=$((P+1)) || { echo "FAIL: $fn"; F=$((F+1)); }; done; echo "$P pass, $F fail"' + +# CC adapter tests +bash -c 'source adapters/lib.sh; source adapters/claude-code/adapter.sh; source tests/adapters/claude-code/adapter.test.sh; P=0; F=0; for fn in $(declare -F | awk "{print \$3}" | grep "^test_"); do $fn >/dev/null 2>&1 && P=$((P+1)) || { echo "FAIL: $fn"; F=$((F+1)); }; done; echo "$P pass, $F fail"' + +# Same pattern for opencode (grep "^test_oc_") and gemini-cli (grep "^test_gemini_") +``` + +### Regression test + +`tests/regression/run.sh` builds the Claude Code adapter and compares the output byte-for-byte against a pre-captured snapshot. This catches accidental changes to the CC build output. + +```bash +bash tests/regression/run.sh +``` + +If you change source files or the CC adapter, you may need to update the snapshot: + +```bash +bash scripts/build.sh --platform claude-code +cp -r dist/claude-code/.claude/* tests/regression/snapshot/.claude/ +cp dist/claude-code/CLAUDE.md tests/regression/snapshot/CLAUDE.md +bash tests/regression/run.sh # should now pass +``` + +### When to run tests + +- After modifying any adapter: run that adapter's tests +- After modifying `adapters/lib.sh`: run all adapter tests +- After modifying source files (agents, skills, references, hooks, DISPATCHER.md): run the regression test +- Before submitting a PR: run everything + +--- + ## Philosophy This project is built for people who are already overwhelmed. Contributions should make things **simpler**, not more complex. diff --git a/DISPATCHER.md b/DISPATCHER.md new file mode 100755 index 0000000..175e5a8 --- /dev/null +++ b/DISPATCHER.md @@ -0,0 +1,324 @@ +# ROUTING RULES — MANDATORY — READ BEFORE ANYTHING ELSE + +**NEVER RESPOND DIRECTLY TO THE USER IF AN AGENT EXISTS FOR THE TASK.** You are the dispatcher. The user talks to you, but the crew does the work. Your only job is to recognize intent and delegate to the right agent. + +## ABSOLUTE CONSTRAINT: ONLY skills and agents from THIS project + +Your crew consists of **14 skills** (in `.platform/skills/`) and **8 core agents** (in `.platform/agents/`). Your agent platform auto-loads both at session start. + +The 8 core agents are: + +`architect`, `scribe`, `sorter`, `seeker`, `connector`, `librarian`, `transcriber`, `postman` + +Custom agents created by the Architect are also valid. Check `.platform/references/agents-registry.md` for the full list of active agents (core + custom). + +**NEVER USE:** +- External plugins, third-party tools, or MCP servers not defined here +- Any agent, plugin, skill, or system that is not defined in this project's files +- If something is not defined in this project's files, **IT DOES NOT EXIST** + +## How to delegate + +**Skills FIRST, agents SECOND.** Check the skill routing table before the agent routing table. + +- **Skills** handle complex, multi-step, or conversational flows. Invoke them via the **Skill tool**. They run in the main conversation context (multi-turn state is preserved). +- **Agents** handle reactive, single-shot operations. Invoke them via the **Agent tool**. They run as subprocesses. + +**CRITICAL RULES:** +1. **Do NOT answer yourself** — you are ONLY the dispatcher. Don't say "I'm sorry", don't give advice, don't add empathy. DELEGATE. Period. +2. **Check skill routing FIRST** — if the user's message matches a skill trigger, invoke the skill using the **Skill tool**. Do NOT use the Agent tool for skill-routed triggers. +3. **Fall through to agent routing** — if NO skill matches, use the agent routing table and invoke via the **Agent tool**. +4. **When in doubt, DELEGATE** — better to activate a skill/agent one time too many than to miss an important delegation. +5. **Pass the user's message** — in the Agent/Skill prompt, include the user's original message as-is. + +--- + +## Skill routing (check FIRST — highest priority) + +Skills handle complex, multi-step flows. **Check this table BEFORE the agent table.** If a match is found, invoke the skill via the `Skill` tool and STOP — do not also invoke an agent. + +| # | Skill | Description | Triggers | +|---|-------|-------------|----------| +| 1 | `/onboarding` | First-time vault setup. Multi-phase conversation to collect preferences, life areas, integrations, then creates vault structure. | EN: "initialize the vault", "set up the vault", "onboarding", "vault setup" · IT: "inizializza il vault", "configura il vault", "setup del vault" · FR: "initialiser le vault", "configurer le vault" · ES: "inicializar el vault", "configurar el vault" · DE: "Vault initialisieren", "Vault einrichten" · PT: "inicializar o vault", "configurar o vault" · JA: "Vaultを初期化", "Vaultをセットアップ" | +| 2 | `/create-agent` | Create a new custom agent. 6-phase interview to define purpose, capabilities, triggers, output, then generates the agent file. | EN: "create a new agent", "custom agent", "I need a new agent", "build an agent", "new crew member" · IT: "crea un nuovo agente", "agente personalizzato", "nuovo membro del crew" · FR: "créer un nouvel agent", "agent personnalisé" · ES: "crear un nuevo agente", "agente personalizado" · DE: "neuen Agenten erstellen" · PT: "criar um novo agente" | +| 3 | `/manage-agent` | Edit, update, remove, or list custom agents. | EN: "edit my agent", "update agent", "remove agent", "delete agent", "list agents", "show my agents" · IT: "modifica il mio agente", "aggiorna agente", "rimuovi agente", "lista agenti", "mostra i miei agenti" · FR: "modifier mon agent", "supprimer agent", "lister les agents" · ES: "editar mi agente", "eliminar agente", "listar agentes" · DE: "Agenten bearbeiten", "Agenten löschen", "Agenten auflisten" · PT: "editar meu agente", "remover agente", "listar agentes" | +| 4 | `/defrag` | Weekly vault defragmentation. 5-phase structural audit: inbox hygiene, area completeness, MOC refresh, tag consistency, and report. | EN: "defragment the vault", "reorganize the vault", "structural maintenance", "vault defrag", "weekly defrag" · IT: "deframmenta il vault", "riorganizza il vault", "manutenzione strutturale", "defrag settimanale" · FR: "défragmenter le vault", "réorganiser le vault" · ES: "desfragmentar el vault", "reorganizar el vault" · DE: "Vault defragmentieren", "Vault reorganisieren" · PT: "desfragmentar o vault", "reorganizar o vault" | +| 5 | `/email-triage` | Scan and process unread emails. Priority scoring, classification, saves relevant emails as vault notes, triage report. | EN: "check my email", "what's in my inbox", "process emails", "email triage", "anything urgent in email?", "save important emails" · IT: "controlla le email", "cosa c'è nella mia inbox", "triage email", "processa le email", "email urgenti" · FR: "vérifier mes emails", "trier mes emails" · ES: "revisar mi correo", "triaje de emails" · DE: "E-Mails prüfen", "Posteingang sichten" · PT: "verificar meus emails", "triagem de emails" | +| 6 | `/meeting-prep` | Comprehensive meeting brief. Gathers participant context, related emails, past notes, vault references. | EN: "prepare for meeting", "meeting prep", "brief me for the meeting", "get ready for the call" · IT: "prepara la riunione", "brief per il meeting", "preparami per la call" · FR: "préparer la réunion", "brief pour le meeting" · ES: "preparar la reunión", "brief para la reunión" · DE: "Meeting vorbereiten", "Besprechung vorbereiten" · PT: "preparar a reunião", "brief para o meeting" | +| 7 | `/weekly-agenda` | Day-by-day week overview combining calendar, email deadlines, and vault tasks. | EN: "weekly agenda", "what's this week", "week overview", "plan my week" · IT: "agenda settimanale", "cosa c'è questa settimana", "panoramica della settimana" · FR: "agenda de la semaine", "programme de la semaine" · ES: "agenda semanal", "qué hay esta semana" · DE: "Wochenagenda", "Wochenübersicht" · PT: "agenda semanal", "o que tem esta semana" | +| 8 | `/deadline-radar` | Unified deadline timeline from emails, calendar, and vault. Groups by urgency with alert levels. | EN: "deadline radar", "what are my deadlines", "this week's deadlines", "upcoming deadlines" · IT: "scadenze", "radar scadenze", "le mie scadenze", "scadenze della settimana" · FR: "échéances", "radar des échéances" · ES: "fechas límite", "radar de plazos" · DE: "Fristen-Radar", "meine Fristen" · PT: "radar de prazos", "meus prazos" | +| 9 | `/transcribe` | Process audio recordings, transcripts, podcasts, lectures. Intake interview then structured notes with action items and decisions. | EN: "transcribe", "I have a recording", "process this audio", "meeting notes from recording", "summarize the call", "lecture notes", "podcast summary" · IT: "trascrivi", "ho una registrazione", "processa questo audio", "note della riunione", "riassumi la call" · FR: "transcrire", "j'ai un enregistrement", "résumer l'appel" · ES: "transcribir", "tengo una grabación", "resumir la llamada" · DE: "transkribieren", "Aufnahme verarbeiten" · PT: "transcrever", "tenho uma gravação" | +| 10 | `/vault-audit` | Full 7-phase vault audit: structural scan, duplicates, links, frontmatter, MOCs, cross-agent, health report. | EN: "weekly review", "check the vault", "vault audit", "full audit", "vault health" · IT: "revisione settimanale", "controlla il vault", "audit del vault", "salute del vault" · FR: "audit du vault", "vérifier le vault" · ES: "auditoría del vault", "revisar el vault" · DE: "Vault-Audit", "Vault überprüfen" · PT: "auditoria do vault", "verificar o vault" | +| 11 | `/deep-clean` | Extended vault cleanup: full audit plus stale content, outdated refs, redundant tags, template compliance. | EN: "deep clean", "deep cleanup", "thorough cleanup", "the vault is a mess" · IT: "pulizia profonda", "pulizia completa", "il vault è un disastro" · FR: "nettoyage en profondeur", "le vault est un désordre" · ES: "limpieza profunda", "el vault es un desastre" · DE: "Tiefenreinigung", "das Vault ist ein Chaos" · PT: "limpeza profunda", "o vault está uma bagunça" | +| 12 | `/tag-garden` | Analyze all vault tags: unused, orphan, near-duplicates, over/under-used. Suggest merges. | EN: "tag garden", "clean up tags", "tag cleanup", "tag audit" · IT: "tag garden", "pulizia tag", "revisione tag" · FR: "jardinage des tags", "nettoyer les tags" · ES: "jardín de tags", "limpiar tags" · DE: "Tag-Garten", "Tags aufräumen" · PT: "jardim de tags", "limpar tags" | +| 13 | `/inbox-triage` | Process all notes in 00-Inbox/: classify, route, update MOCs, extract actions, daily digest. | EN: "triage the inbox", "clean up the inbox", "sort my notes", "empty inbox", "file my notes", "process the inbox" · IT: "smista l'inbox", "svuota l'inbox", "ordina le note", "triage dell'inbox", "processa l'inbox" · FR: "trier la boîte de réception", "vider l'inbox", "classer mes notes" · ES: "clasificar la bandeja de entrada", "vaciar el inbox", "ordenar mis notas" · DE: "Inbox sortieren", "Inbox leeren", "Notizen einordnen" · PT: "triagem da inbox", "esvaziar a inbox", "organizar minhas notas" | +| 14 | `/contact-sync` | Sync a person to Apple Contacts: search, create if missing, update if incomplete. Requires `apple-contacts` MCP. | EN: "sync contact", "add to contacts", "save contact", "update contact", "is this person in my contacts" · IT: "sincronizza contatto", "aggiungi ai contatti", "salva contatto", "aggiorna contatto" · FR: "synchroniser le contact", "ajouter aux contacts" · ES: "sincronizar contacto", "agregar a contactos" · DE: "Kontakt synchronisieren", "zu Kontakten hinzufuegen" · PT: "sincronizar contato", "adicionar aos contatos" | + +--- + +## Agent routing (fallback — only if NO skill matched above) + +When a message does NOT match any skill trigger above, use this table. Activate the agent with the highest priority. + +| # | Agent/Skill | When to activate | +|---|-------------|-----------------| +| 1 | **postman** | Calendar import, create event, targeted email/calendar search, VIP filter, email draft | +| 2 | **transcriber** | (most triggers now go to `/transcribe` skill — agent handles only edge cases) | +| 3 | **scribe** | Text capture, notes, ideas, thoughts, to-dos, brainstorming, gratitude | +| 4 | **seeker** | Vault search, questions about notes, "find", "where did I put" | +| 5 | **architect** | Vault structure, areas, templates, MOCs, tags (NOT onboarding, defrag, or agent creation — those are skills) | +| 6 | **sorter** | Smart batch, priority triage, project pulse (NOT standard inbox triage — that's a skill) | +| 7 | **connector** | Links between notes, graph, MOCs, relationships, cross-linking | +| 8 | **librarian** | Quick health check, consistency report, growth analytics, stale content (NOT full audit, deep clean, or tag garden — those are skills) | +| 9+ | **custom agents** | Any agent created via the Architect. Check `.platform/references/agents-registry.md` for triggers and capabilities. Custom agents always have lower priority than core 8. | + +--- + +## 1. POSTMAN (agent) + +Activate for calendar operations and simple email interactions NOT covered by skills. + +Triggers: "import events", "what's on my calendar", "create event", "postman", "VIP emails", "draft reply", "travel plan", "invoice tracker", "targeted email search", "calendar search" + +> **Note**: email triage → `/email-triage` skill. Meeting prep → `/meeting-prep` skill. Weekly agenda → `/weekly-agenda` skill. Deadlines → `/deadline-radar` skill. + +--- + +## 2. TRANSCRIBER (agent) + +Activate only for edge cases not covered by the `/transcribe` skill. + +> **Note**: most transcription triggers ("transcribe", "recording", "meeting notes", "podcast") go to the `/transcribe` skill. The agent handles only direct follow-up or edge cases. + +--- + +## 3. SCRIBE (agent) + +Activate when the user wants to capture/save information to the vault. + +Triggers: "save this", "jot this down", "quick note", "write this", "remind me that", "note this", "capture this", "voice note", "brainstorm", "reading notes", "quote", "take note", "mark this down", "quick idea", "I have a thought", "write a note about", "gratitude journal", "gratitude", "what am I grateful for today", "evening gratitude" + +Also activate when the user pastes unstructured text, does speech-to-text, or dumps a list of thoughts. + +--- + +## 4. SEEKER (agent) + +Activate for any search or question about vault content. + +Triggers: "search the vault", "find", "where did I put", "what notes do I have on", "what do we know about", "show me", "edit the note on", "update the note", "find and edit", "answer from my notes", "timeline", "compare", "what am I missing", "what should I revisit", "search", "show me", "what info do I have on" + +--- + +## 5. ARCHITECT (agent) + +Activate for reactive vault structure operations NOT covered by skills. + +Triggers: "create a new area", "new project", "add template", "modify the structure", "new folder", "tag taxonomy", "naming convention", "create a MOC", "restructure the vault", "add an area", "fix the structure" + +Also activate: when another agent reports missing structure; when a new topic/project/area emerges. + +> **Note**: onboarding → `/onboarding` skill. Agent creation → `/create-agent` skill. Agent management → `/manage-agent` skill. Defrag → `/defrag` skill. + +--- + +## 6. SORTER (agent) + +Activate for sorting modes NOT covered by the `/inbox-triage` skill. + +Triggers: "batch sort", "priority triage", "project pulse", "evening triage" + +> **Note**: standard inbox triage ("triage the inbox", "empty inbox", "sort my notes") → `/inbox-triage` skill. + +--- + +## 7. CONNECTOR (agent) + +Activate for link analysis and knowledge graph work. + +Triggers: "connect the notes", "find connections", "improve the graph", "what connections are missing", "strengthen links", "analyze relationships", "network analysis", "serendipity", "constellation", "bridge notes", "people network", "graph health", "missing links" + +--- + +## 8. LIBRARIAN (agent) + +Activate for quick checks and analytics NOT covered by skills. + +Triggers: "quick check", "consistency report", "growth analytics", "stale content", "are there duplicates?", "maintenance" + +> **Note**: full audit → `/vault-audit` skill. Deep clean → `/deep-clean` skill. Tag garden → `/tag-garden` skill. + +--- + +## 9. CUSTOM AGENTS + +Custom agents are created via the `/create-agent` skill and stored in `.platform/agents/`. They are auto-discovered like core agents. When a user message does not match any skill or core agent, check `.platform/references/agents-registry.md` for custom agents whose Input column matches the message. If a match is found, delegate to that agent. + +--- + +## Multi-agent routing + +The dispatcher is a **reactive multi-router**. After invoking an agent, analyze its output before responding to the user: + +1. Did the agent create content that needs filing? → Consider **Sorter** +2. Did the agent report missing structure? → Consider **Architect** +3. Did the agent find notes that need linking? → Consider **Connector** +4. Did the agent produce notes that need cleanup? → Consider **Librarian** +5. Did the agent include a `### Suggested next agent` section? → Validate and consider it +6. Did the agent include a `### Suggested new agent` section? → Ask the user if they want the **Architect** to create a custom agent for the detected need + +Consult `.platform/references/agents-registry.md` to validate suggestions and match output to agent capabilities. + +### Call chain tracking + +Maintain a call chain for each user request: + +1. Start with an empty chain: `[]` +2. After each agent returns, append its name to the chain (the chain always lists agents already invoked, in order) +3. When invoking the next agent, pass the chain and position, e.g.: `"Call chain so far: [scribe, architect]. You are step 3 of max 3."` +4. After the agent returns, read its output and decide if another agent is needed + +### Anti-recursion rules + +- **No duplicates**: never invoke the same agent twice in one user request +- **No circular chains**: if Agent A's output suggests Agent B, and B is already in the chain, skip it +- **Max depth 3**: no more than 3 agents per user request +- **On overflow**: return results to the user and suggest what they can do next (e.g., _"The Connector also detected 5 orphan notes — say 'connect the notes' to handle that."_) + +### Decision flow + +``` +USER MESSAGE → check SKILL routing table first + ↓ + Skill match found? → INVOKE skill (Skill tool) → RESPOND to user + ↓ (no skill match) + Check AGENT routing table → INVOKE agent (Agent tool) + ↓ + READ OUTPUT → check agents-registry.md + ↓ + Does output match another agent's capabilities? + YES + not in chain + depth < 3 → INVOKE next + NO or limit reached → RESPOND to user +``` + +--- + +## Inter-agent coordination + +Agents do NOT communicate directly with each other. The dispatcher orchestrates all agent calls. + +When an agent detects work for another agent (e.g., missing structure, orphan notes, broken links), it reports this in its output via a `### Suggested next agent` section. The dispatcher reads this and decides whether to chain the next agent. + +See `.platform/references/agent-orchestration.md` for the full protocol and `.platform/references/agents-registry.md` for the agent registry. + +--- + +# Project Info + +## My Brain Is Full - Crew + +A crew of 8 AI subagents that manage an Obsidian vault through natural conversation. + +## Installation + +### Step 1: Create your Obsidian vault + +If you don't have one yet, open [Obsidian](https://obsidian.md) and create a new vault. + +### Step 2: Clone the repo inside your vault + +```bash +cd /path/to/your-vault +git clone https://github.com/gnekt/My-Brain-Is-Full-Crew.git +``` + +### Step 3: Run the installer + +```bash +cd My-Brain-Is-Full-Crew +bash scripts/launchme.sh +``` + +The script asks a couple of questions and copies everything into `.platform/` inside your vault: + +``` +your-vault/ +├── .platform/ +│ ├── agents/ ← 8 crew agents (auto-loaded at session start) +│ └── references/ ← shared docs the agents read +├── .mcp.json ← Gmail + Calendar (optional, if you chose yes) +├── My-Brain-Is-Full-Crew/ ← the repo (for updates) +└── ... your notes +``` + +### Step 4: Initialize + +1. Open your agent platform **inside your vault folder** +2. Say: **"Initialize my vault"** +3. The Architect agent runs onboarding — creates your folder structure, templates, and preferences + +### Updating + +```bash +cd /path/to/your-vault/My-Brain-Is-Full-Crew +git pull +bash scripts/updateme.sh +``` + +Only changed files are overwritten. Your vault notes are never touched. + +## Requirements + +- A supported **agent platform** (see the README for details) +- **Obsidian** (free) — [obsidian.md](https://obsidian.md) +- **Gmail / Google Calendar** (optional) — only for the Postman agent + +## Project Structure + +``` +My-Brain-Is-Full-Crew/ +├── agents/ The 8 subagents +│ ├── architect.md Vault setup & onboarding +│ ├── scribe.md Text capture & note creation +│ ├── sorter.md Inbox triage & filing +│ ├── seeker.md Search & knowledge retrieval +│ ├── connector.md Knowledge graph & link analysis +│ ├── librarian.md Vault health & maintenance +│ ├── transcriber.md Audio & meeting transcription +│ └── postman.md Email & calendar integration +├── references/ Shared agent documentation +├── docs/ User-facing documentation +├── scripts/ +│ ├── launchme.sh First-time installer +│ └── updateme.sh Post-pull updater +├── mcp/servers.yaml MCP server definitions (source of truth) +├── README.md +├── CONTRIBUTING.md +└── LICENSE +``` + +## Language + +All agent files are written in English. Agents automatically respond in whatever language the user writes in — no configuration needed. + +## Architecture + +Each agent is defined in `.platform/agents/{name}.md` (in the destination vault) with YAML frontmatter and a full system prompt body. The platform auto-discovers these agents at session start, reads their `description` field, and delegates automatically when the user's message matches. + +The dispatcher routing rules reinforce this auto-delegation — they provide explicit priority ordering and trigger lists to ensure correct delegation. + +Key design decisions: + +- **Seeker** is search-only (`tools: Read, Glob, Grep`) — it finds information but doesn't modify notes +- **Architect** and **Librarian** have full access including Bash for structural operations +- **Postman** uses email (Gmail via `gws`, Hey.com via `hey` CLI) and Google Calendar for full read/write access, with MCP servers (`.mcp.json`) as a read-only fallback. See `docs/gws-setup-guide.md` for GWS setup +- All agents auto-activate based on their `description` field — just talk naturally +- Agents reference shared docs at `.platform/references/` + +## Installation + +```bash +bash scripts/launchme.sh --platform +``` + +This builds the source files for your platform and installs them into your vault. See the README for platform-specific details. diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 4d2a09d..476ae42 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ The crew ships with 8 agents. But your life isn't generic, and your system shoul | *"I keep starting side projects and abandoning them"* | **project-pulse**: weekly check-in on all active projects, flags stale ones | | *"I have three freelance clients and I mix up their deadlines"* | **client-tracker**: aggregates deadlines per client from notes and calendar | -Custom agents coordinate with the core crew, get discovered automatically by Claude Code, and respond in your language. They just solve the problems that are specific to **your** life. +Custom agents coordinate with the core crew, get discovered automatically by your agent platform, and respond in your language. They just solve the problems that are specific to **your** life. > **Your custom agents, your responsibility.** Custom agents are created by you and run on your data. The project provides no warranty on their behavior. See [Terms of Use](TERMS_OF_USE.md). @@ -140,7 +140,7 @@ The dispatcher automatically routes your message to the right skill or agent. Yo ## How it works ``` -You talk to Claude → Dispatcher checks skills first → If match: invokes skill +You talk naturally → Dispatcher checks skills first → If match: invokes skill → If no match: invokes agent → Your vault gets updated ``` @@ -153,12 +153,12 @@ Each crew member is an isolated AI with its own system prompt, tool restrictions ```mermaid graph TB User((You)) - Claude["Claude Code\nDispatcher"] + Dispatcher["Dispatcher"] - User -->|"talk naturally"| Claude - Claude -->|"skill match?\ninvoke skill"| Skills - Claude -->|"no skill match?\ninvoke agent"| Agents - Claude -->|"chains agents when needed"| Agents + User -->|"talk naturally"| Dispatcher + Dispatcher -->|"skill match?\ninvoke skill"| Skills + Dispatcher -->|"no skill match?\ninvoke agent"| Agents + Dispatcher -->|"chains agents when needed"| Agents subgraph Skills["Specialized Skills (14)"] direction TB @@ -202,7 +202,7 @@ graph TB end style User fill:#7c3aed,stroke:#5b21b6,color:#fff - style Claude fill:#3b82f6,stroke:#2563eb,color:#fff + style Dispatcher fill:#3b82f6,stroke:#2563eb,color:#fff style Skills fill:#fef3c7,stroke:#f59e0b style Core fill:#e0e7ff,stroke:#818cf8 style External fill:#dbeafe,stroke:#60a5fa @@ -234,16 +234,17 @@ sequenceDiagram S->>S: files notes to correct locations ``` -### Works on both Claude Code CLI and Claude Code Desktop (Cowork) +### Multi-platform support -The installer sets up **two parallel layers** so the Crew works everywhere: +The Crew works on multiple agent platforms. The installer builds from a single source and deploys to your platform of choice: -| Layer | Location | Purpose | -|-------|----------|---------| -| **Agents** | `.claude/agents/` | Lightweight reactive agents for single-shot tasks (capture, search, create) | -| **Skills** | `.claude/skills/` | Specialized multi-step flows for complex tasks (onboarding, triage, audits) | +| Platform | Install command | Config dir | Dispatcher | +|----------|----------------|------------|------------| +| **Claude Code** (CLI & Desktop) | `bash scripts/launchme.sh --platform claude-code` | `.claude/` | `CLAUDE.md` | +| **Gemini CLI** | `bash scripts/launchme.sh --platform gemini-cli` | `.gemini/` | `GEMINI.md` | +| **OpenCode** | `bash scripts/launchme.sh --platform opencode` | `.opencode/` | `AGENTS.md` | -Both layers work on CLI and Desktop. `launchme.sh` installs both automatically. The dispatcher decides whether to invoke a skill or an agent based on your message. +If you omit `--platform`, the installer asks you to choose. Each platform gets agents, skills, references, hooks, and MCP servers translated to its native format. `launchme.sh` installs everything automatically. Your vault follows a hybrid **PARA + Zettelkasten** structure: @@ -265,7 +266,7 @@ Meta/ Vault config, agent logs, health reports ## Quick start -> **Prerequisite**: You need [Claude Code](https://claude.ai/code) with a Claude Pro, Max, or Team subscription, and [Obsidian](https://obsidian.md) (free). +> **Prerequisites**: [Obsidian](https://obsidian.md) (free) and one of the supported agentic platforms. ### 1. Create your Obsidian vault @@ -285,13 +286,13 @@ cd My-Brain-Is-Full-Crew bash scripts/launchme.sh ``` -The script asks a couple of questions and copies the agents and skills into your vault's `.claude/` directory. That's it. When Claude Code is open in your vault folder, the agents activate automatically. When you're in any other project, they don't. +The script asks you to pick a platform, then builds and installs the agents and skills into your vault. When your agent platform is open in your vault folder, the agents activate automatically. When you're in any other project, they don't. > **Never used a terminal before?** See the [step-by-step guide for beginners](docs/getting-started.md). It walks you through everything, or just show this page to a tech-savvy friend. It takes 60 seconds. ### 4. Initialize -Open Claude Code **inside your vault folder** and say: +Open your agent platform **inside your vault folder** and say: > **"Initialize my vault"** @@ -333,7 +334,7 @@ No translations to install. No language packs. It just works. ## Works from your phone too -You can control the Crew from your phone using Claude Code's **Remote Control** feature. Your computer runs Claude Code locally (with full vault and agent access), and your phone acts as a remote interface through the browser or the Claude mobile app. +If you use Claude Code, you can control the Crew from your phone using its **Remote Control** feature. Your computer runs Claude Code locally (with full vault and agent access), and your phone acts as a remote interface through the browser or the Claude mobile app. Capture a quick thought on a walk. Check your email from the couch. Search your vault from the supermarket. Everything runs on your computer; your phone is just the remote. @@ -359,7 +360,7 @@ No agent works in isolation. The crew is greater than the sum of its parts. The **Postman** agent (and its related skills: `/email-triage`, `/meeting-prep`, `/weekly-agenda`, `/deadline-radar`) requires one of: - **Google Workspace CLI** (`gws`) — full read/write access to Gmail and Google Calendar: search, read, archive, delete, label, send emails; create/update/delete calendar events. See [`docs/gws-setup-guide.md`](docs/gws-setup-guide.md) for setup. - **Hey CLI** (`hey`) — for Hey.com accounts. Read/reply/compose emails, leverages Hey's pre-sorted mailboxes (Imbox, Feed, Paper Trail, Reply Later, Set Aside, Bubble Up). Calendar operations still use `gws`. See [Hey CLI](https://github.com/basecamp/hey-cli) for installation. -- **MCP connectors** (read-only fallback) — `launchme.sh` offers to set up `.mcp.json` automatically. Limited to reading emails and calendar events, plus draft creation. +- **MCP connectors** (read-only fallback) — `launchme.sh` sets up MCP servers automatically (format varies by platform). Limited to reading emails and calendar events, plus draft creation. You can use `gws` and `hey` simultaneously if you have both Gmail and Hey.com accounts. @@ -396,6 +397,12 @@ git pull bash scripts/updateme.sh ``` +The updater **automatically detects** which platform is installed in your vault (by checking for a platform-specific folder). If you have multiple platforms installed, it asks you to choose which one to update. You can also specify explicitly with `--platform`: + +```bash +bash scripts/updateme.sh --platform opencode +``` + Only changed files are updated. Your vault notes are never touched. --- @@ -449,26 +456,30 @@ My-Brain-Is-Full-Crew/ ← cloned inside your vault │ ├── getting-started.md Step-by-step setup guide │ ├── examples.md Real-world usage examples │ └── agents/ Deep-dive into each agent -├── .mcp.json MCP servers — read-only fallback (see docs/gws-setup-guide.md for full access) -├── .claude-plugin/plugin.json Plugin manifest (for --plugin-dir) +├── adapters/ Platform adapters (build system) +│ ├── lib.sh Shared parsing and rewrite helpers +│ ├── claude-code/ Claude Code adapter +│ ├── gemini-cli/ Gemini CLI adapter +│ └── opencode/ OpenCode adapter +├── mcp/servers.yaml MCP server definitions (source of truth) ├── LICENSE ├── README.md You are here └── CONTRIBUTING.md ``` -After running `launchme.sh`, your vault looks like: +After running `launchme.sh`, your vault looks like (paths vary by platform): ``` your-vault/ -├── .claude/ -│ ├── agents/ ← lightweight reactive agents -│ ├── skills/ ← specialized multi-step skills -│ └── references/ ← shared docs +├── ./ ← .claude/, .gemini/, .opencode/, etc. +│ ├── agents/ ← lightweight reactive agents +│ ├── skills/ ← specialized multi-step skills +│ ├── hooks/ ← file protection and validation hooks +│ └── references/ ← shared docs ├── Meta/ -│ └── scripts/ ← orchestra scripts (permission-free agent commands) -├── CLAUDE.md ← project instructions (dispatcher routing) -├── .mcp.json ← Gmail + Calendar read-only fallback (if enabled) -├── My-Brain-Is-Full-Crew/ ← the repo (for updates) +│ └── scripts/ ← orchestra scripts (permission-free agent commands) +├── CLAUDE.md / GEMINI.md / AGENTS.md / ... ← dispatcher (platform-specific name) +├── My-Brain-Is-Full-Crew/ ← the repo (for updates) └── ... your Obsidian notes ``` @@ -476,7 +487,7 @@ your-vault/ ## Contributing (seriously, please help) -This started as one person's survival tool. I'm sharing it because I think it can help others, but **I know it can be much better**, and I need help from people who know Claude Code, prompt engineering, and Obsidian better than I do. +This started as one person's survival tool. I'm sharing it because I think it can help others, but **I know it can be much better**, and I need help from people who know prompt engineering, agentic platforms, and Obsidian better than I do. **Every single PR is welcome.** I mean it. If you see something that could be improved (a better prompt structure, a smarter agent behavior, a more elegant architecture) please submit it. I won't be precious about my code. The goal is to help people, not to protect my ego. @@ -490,6 +501,10 @@ If you want to: ...PRs, issues, and honest feedback are all welcome. See [CONTRIBUTING.md](CONTRIBUTING.md). +### TO DO +- [ ] Update the installer to auto-install the orchestra scripts (currently a manual copy-paste step) +- [ ] Completely decouple the installer from the platform (currently has some platform-specific logic that could be moved to the adapters or to some kind of config file) + --- ## Philosophy diff --git a/TERMS_OF_USE.md b/TERMS_OF_USE.md old mode 100644 new mode 100755 index fbec200..16961b8 --- a/TERMS_OF_USE.md +++ b/TERMS_OF_USE.md @@ -9,7 +9,7 @@ By using this software, you agree to the following terms. If you do not agree, d ## 1. Nature of the Software -This software is a collection of AI agent prompts ("the Crew") designed to help individuals organize personal notes, tasks, and information inside an Obsidian vault using Claude Code. It is an open-source tool provided free of charge under the MIT License. +This software is a collection of AI agent prompts ("the Crew") designed to help individuals organize personal notes, tasks, and information inside an Obsidian vault using supported agent platforms. It is an open-source tool provided free of charge under the MIT License. The software does not collect, transmit, or store any data outside your local device. All data remains in your Obsidian vault on your filesystem. @@ -160,9 +160,9 @@ b) LLMs can and do **hallucinate**: they generate text that appears factual but c) The author provides **prompt engineering only**. The behavior, accuracy, safety, and reliability of the output depend entirely on the underlying model, its training data, its alignment methods (RLHF, DPO, Constitutional AI, or others), its safety filters, and its runtime configuration. The author has no control over any of these factors. -d) This software is designed and tested with **Anthropic's Claude models**. If you use a different LLM (whether through a fork, a plugin modification, a different MCP configuration, or any other means), the quality, safety, and reliability of the output are **entirely unpredictable and entirely your responsibility**. Models without adequate alignment, safety training, or content filtering may produce harmful, misleading, dangerous, or offensive output. +d) This software is primarily designed and tested with **Anthropic's Claude models**. If you use an unsupported LLM or platform (whether through a fork, a plugin modification, a different MCP configuration, or any other means), the quality, safety, and reliability of the output are **entirely unpredictable and entirely your responsibility**. Models without adequate alignment, safety training, or content filtering may produce harmful, misleading, dangerous, or offensive output. -e) Even when using Claude, **no output should be treated as authoritative, factual, or reliable without independent verification.** This applies to all agents — core and custom — including but not limited to: factual claims made by the Seeker, organizational suggestions by the Architect, and any advice-like output generated by custom agents in domains such as health, legal, financial, or any other regulated field. +e) Regardless of which platform or model you use, **no output should be treated as authoritative, factual, or reliable without independent verification.** This applies to all agents — core and custom — including but not limited to: factual claims made by the Seeker, organizational suggestions by the Architect, and any advice-like output generated by custom agents in domains such as health, legal, financial, or any other regulated field. f) Even though this software has been tested by the author using Anthropic's Claude, **no quality standard can be guaranteed.** Due to the inherent stochastic nature of large language models, the same prompt can produce different output across different sessions, model versions, context windows, and runtime conditions. Testing validates that the prompts are well-formed and produce reasonable results under observed conditions, but it **cannot and does not guarantee** consistent, accurate, or safe output in all circumstances or for all users. @@ -173,7 +173,7 @@ The author: - **Does not guarantee** the accuracy, completeness, safety, or appropriateness of any output generated by any agent under any circumstances - **Does not guarantee** that safety instructions in the prompts will be followed by the model in all cases. Prompt-based safety is best-effort, not a guarantee - **Does not guarantee** that the software will behave identically across different model versions, providers, or configurations -- **Is not responsible** for any output generated by models other than Anthropic's Claude, regardless of the reason the user chose to use a different model +- **Is not responsible** for any output generated by any model or platform, including but not limited to Anthropic's Claude, Google's Gemini, and any model used through OpenCode - **Is not responsible** for any action you take based on AI-generated output --- @@ -197,7 +197,7 @@ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILIT This includes, without limitation, liability for: - Any advice-like output generated by core or custom agents, including but not limited to health, legal, financial, dietary, and wellness domains - Hallucinated, fabricated, inaccurate, or misleading content generated by the underlying AI model -- Any consequence of using this software with a model other than Anthropic's Claude +- Any consequence of using this software with any model or platform, whether supported or unsupported - Loss or corruption of data in your vault - Violations of data protection law arising from your use of the software - Any action taken by custom agents created by the user (Section 9) diff --git a/adapters/claude-code/adapter.sh b/adapters/claude-code/adapter.sh new file mode 100755 index 0000000..89b243b --- /dev/null +++ b/adapters/claude-code/adapter.sh @@ -0,0 +1,317 @@ +#!/usr/bin/env bash +# ============================================================================= +# adapters/claude-code/adapter.sh — Claude Code framework adapter +# ============================================================================= +# Sourced by scripts/build.sh AFTER adapters/lib.sh. +# Translates source files into a dist/claude-code/ tree that mirrors what +# Claude Code expects in the user's vault. +# ============================================================================= + +CC_PLATFORM="claude-code" +CC_FW_DIR="claude" +CC_DISPATCHER="CLAUDE.md" + +# Capability → CC tools mapping. Each capability expands into one or more +# Claude Code tool names. The expansion is order-preserving. +cc_capability_to_tools() { + local cap="$1" + case "$cap" in + read) echo "Read" ;; + write) echo "Write" ;; + edit) echo "Edit" ;; + bash) echo "Bash" ;; + webfetch) echo "WebFetch" ;; + websearch) echo "WebSearch" ;; + notebook) echo "NotebookEdit" ;; + task) echo "Task" ;; + todo) echo "TodoWrite" ;; + *) echo "" ;; + esac +} + +# Event vocabulary → CC native event mapping. +cc_event_to_native() { + local event="$1" + case "$event" in + before-tool-use) echo "PreToolUse" ;; + after-tool-use) echo "PostToolUse" ;; + on-notification) echo "Notification" ;; + on-session-start) echo "SessionStart" ;; + on-prompt-submit) echo "UserPromptSubmit" ;; + *) echo "" ;; + esac +} + +# adapter_finalize +# Writes any framework-specific top-level files (plugin manifest, etc.). +adapter_finalize() { + local src="$1" dst="$2" + if [[ -f "$src/.claude-plugin/plugin.json" ]]; then + mkdir -p "$dst/.claude-plugin" + cp "$src/.claude-plugin/plugin.json" "$dst/.claude-plugin/plugin.json" + fi +} + +# adapter_translate_mcp +# Reads mcp/servers.yaml and writes dst/.mcp.json with mcpServers key. +adapter_translate_mcp() { + local src="$1" dst="$2" + local yaml="$src/servers.yaml" + [[ -f "$yaml" ]] || return 0 + + local out="$dst/.mcp.json" + mkdir -p "$dst" + + # Parse the YAML into a JSON object {server_name: {command, args, env}} + local json='{}' + local current_name="" current_cmd="" current_url="" current_type="" + + while IFS= read -r line; do + case "$line" in + *"- name:"*) + # Flush previous server + if [[ -n "$current_name" ]]; then + if [[ -n "$current_cmd" ]]; then + local first_arg="${current_cmd%% *}" + local rest="${current_cmd#* }" + local args_json='[]' + if [[ "$rest" != "$current_cmd" ]]; then + args_json="$(echo "$rest" | jq -R 'split(" ")')" + fi + json="$(echo "$json" | jq --arg n "$current_name" --arg c "$first_arg" --argjson a "$args_json" '.[$n] = {command: $c, args: $a, env: {}}')" + elif [[ -n "$current_url" ]]; then + json="$(echo "$json" | jq --arg n "$current_name" --arg u "$current_url" --arg t "$current_type" '.[$n] = {type: $t, url: $u}')" + fi + fi + current_name="$(echo "$line" | sed 's/.*- name:[[:space:]]*//' | tr -d '"')" + current_cmd="" + current_url="" + current_type="http" + ;; + *"command:"*"["*) + current_cmd="$(echo "$line" | sed 's/.*command:[[:space:]]*\[//' | sed 's/\][[:space:]]*$//' | tr -d '"' | sed 's/,[[:space:]]*/\ /g')" + ;; + *"url:"*) + current_url="$(echo "$line" | sed 's/.*url:[[:space:]]*//' | tr -d '"')" + ;; + *"type:"*) + current_type="$(echo "$line" | sed 's/.*type:[[:space:]]*//' | tr -d '"')" + ;; + esac + done < "$yaml" + + # Flush the last server + if [[ -n "$current_name" ]]; then + if [[ -n "$current_cmd" ]]; then + local first_arg="${current_cmd%% *}" + local rest="${current_cmd#* }" + local args_json='[]' + if [[ "$rest" != "$current_cmd" ]]; then + args_json="$(echo "$rest" | jq -R 'split(" ")')" + fi + json="$(echo "$json" | jq --arg n "$current_name" --arg c "$first_arg" --argjson a "$args_json" '.[$n] = {command: $c, args: $a, env: {}}')" + elif [[ -n "$current_url" ]]; then + json="$(echo "$json" | jq --arg n "$current_name" --arg u "$current_url" --arg t "$current_type" '.[$n] = {type: $t, url: $u}')" + fi + fi + + echo "$json" | jq '{mcpServers: .}' > "$out" +} + +# Helper: convert match-tool tokens to CC matcher syntax (e.g., "edit write" → "Edit|Write") +cc_match_tool_to_matcher() { + local tokens="$1" + local matcher="" + for t in $tokens; do + local cap_t + cap_t="$(echo "$t" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')" + if [[ -z "$matcher" ]]; then + matcher="$cap_t" + else + matcher="$matcher|$cap_t" + fi + done + echo "$matcher" +} + +# Neutral model tier → Claude Code model name. +cc_model_to_native() { + local model="$1" + case "$model" in + */*) echo "$model" ;; # already qualified — passthrough + low) echo "haiku" ;; + mid) echo "sonnet" ;; + high) echo "opus" ;; + *) echo "$model" ;; # unknown — passthrough + esac +} + +# adapter_translate_hooks +adapter_translate_hooks() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + local out_dir="$dst/.claude/hooks" + mkdir -p "$out_dir" + + local template; template="$(dirname "${BASH_SOURCE[0]}")/templates/cc-hook-wrapper.sh.tmpl" + + # Accumulate hook entries here, by event type + local pre_entries="" post_entries="" notif_entries="" sess_entries="" prompt_entries="" + + while IFS= read -r yaml; do + [[ -z "$yaml" ]] && continue + should_include "$yaml" "$CC_PLATFORM" || continue + local meta; meta="$(parse_hook_yaml "$yaml")" + local name; name="$(echo "$meta" | grep '^name=' | head -1 | cut -d= -f2- || true)" + local script; script="$(echo "$meta" | grep '^script=' | head -1 | cut -d= -f2- || true)" + local event; event="$(echo "$meta" | grep '^event=' | head -1 | cut -d= -f2- || true)" + local match_tool; match_tool="$(echo "$meta" | grep '^match-tool=' | head -1 | cut -d= -f2- || true)" + + # Copy the bash script + cp "$src/$script" "$out_dir/$script" + chmod +x "$out_dir/$script" + rewrite_platform_paths "$out_dir/$script" "$CC_FW_DIR" "$CC_DISPATCHER" + + # Generate the wrapper + local wrapper_file="$out_dir/${name}-wrapper.sh" + local cc_event; cc_event="$(cc_event_to_native "$event")" + sed -e "s/__HOOK_NAME__/$name/g" -e "s/__EVENT_NAME__/$event/g" "$template" > "$wrapper_file" + chmod +x "$wrapper_file" + + # Build the settings.json entry for this hook + local matcher="" + [[ -n "$match_tool" ]] && matcher="$(cc_match_tool_to_matcher "$match_tool")" + local entry; entry="$(jq -cn \ + --arg cmd ".claude/hooks/${name}-wrapper.sh" \ + --arg matcher "$matcher" \ + '{matcher: $matcher, hooks: [{type: "command", command: ("bash " + $cmd)}]}')" + + case "$cc_event" in + PreToolUse) pre_entries="$pre_entries$entry"$'\n' ;; + PostToolUse) post_entries="$post_entries$entry"$'\n' ;; + Notification) notif_entries="$notif_entries$entry"$'\n' ;; + SessionStart) sess_entries="$sess_entries$entry"$'\n' ;; + UserPromptSubmit) prompt_entries="$prompt_entries$entry"$'\n' ;; + esac + done < <(enumerate_hooks "$src") + + # Compose the final settings.json + local settings; settings='{"hooks":{}}' + for ev_pair in "PreToolUse:$pre_entries" "PostToolUse:$post_entries" "Notification:$notif_entries" "SessionStart:$sess_entries" "UserPromptSubmit:$prompt_entries"; do + local ev_name="${ev_pair%%:*}" + local ev_data="${ev_pair#*:}" + [[ -z "$ev_data" ]] && continue + local arr; arr="$(echo "$ev_data" | jq -cs '.')" + settings="$(echo "$settings" | jq --arg ev "$ev_name" --argjson arr "$arr" '.hooks[$ev] = $arr')" + done + + mkdir -p "$dst/.claude" + echo "$settings" | jq '.' > "$dst/.claude/settings.json" +} + +# adapter_translate_agents +# For each *.md in source_agents_dir, translate the capabilities frontmatter +# into a CC tools: allowlist and write to dest_root/.claude/agents/.md. +adapter_translate_agents() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + local out_dir="$dst/.claude/agents" + mkdir -p "$out_dir" + + while IFS= read -r agent; do + [[ -z "$agent" ]] && continue + should_include "$agent" "$CC_PLATFORM" || continue + local name; name="$(parse_frontmatter "$agent" name)" + local model; model="$(parse_frontmatter "$agent" model)" + model="$(cc_model_to_native "$model")" + local caps; caps="$(parse_capabilities "$agent")" + + # Build tools allowlist by expanding each capability. + # read → Read, Glob, Grep; other capabilities follow. + local tools="" + for cap in $caps; do + local expansion; expansion="$(cc_capability_to_tools "$cap")" + [[ -n "$expansion" ]] || continue + for tool in $expansion; do + if [[ -z "$tools" ]]; then + tools="$tool" + else + tools="$tools, $tool" + fi + done + # If this was "read", immediately append Glob and Grep + if [[ "$cap" == "read" ]]; then + tools="$tools, Glob, Grep" + fi + done + + local out_file="$out_dir/$(basename "$agent")" + { + echo "---" + echo "name: $name" + # Copy description block (may be folded YAML with continuation lines) + awk '/^---$/{n++; next} n==1 && /^description:/{print; in_desc=1; next} n==1 && in_desc && /^[[:space:]]/{print; next} n==1 && in_desc && !/^[[:space:]]/{in_desc=0} n>=2{exit}' "$agent" + echo "tools: $tools" + echo "model: $model" + echo "---" + agent_body "$agent" + } > "$out_file" + rewrite_platform_paths "$out_file" "$CC_FW_DIR" "$CC_DISPATCHER" + done < <(enumerate_agents "$src") +} + +# adapter_translate_skills +# Copies each skill directory's SKILL.md into dest_root/.claude/skills//. +adapter_translate_skills() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + for skill_dir in "$src"/*/; do + [[ -f "${skill_dir}SKILL.md" ]] || continue + should_include "${skill_dir}SKILL.md" "$CC_PLATFORM" || continue + local name; name="$(basename "$skill_dir")" + local out="$dst/.claude/skills/$name" + mkdir -p "$out" + cp "${skill_dir}SKILL.md" "$out/SKILL.md" + rewrite_platform_paths "$out/SKILL.md" "$CC_FW_DIR" "$CC_DISPATCHER" + done +} + +# adapter_translate_references +# Verbatim copy of *.md into dest_root/.claude/references/. +adapter_translate_references() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + local out="$dst/.claude/references" + mkdir -p "$out" + for f in "$src"/*.md; do + [[ -f "$f" ]] || continue + should_include "$f" "$CC_PLATFORM" || continue + cp "$f" "$out/" + rewrite_platform_paths "$out/$(basename "$f")" "$CC_FW_DIR" "$CC_DISPATCHER" + done +} + +# adapter_translate_dispatcher +# Copies the source DISPATCHER.md to dest_dir/CLAUDE.md (no content change). +adapter_translate_dispatcher() { + local src="$1" dst="$2" + [[ -f "$src" ]] || return 0 + mkdir -p "$dst" + cp "$src" "$dst/CLAUDE.md" + rewrite_platform_paths "$dst/CLAUDE.md" "$CC_FW_DIR" "$CC_DISPATCHER" +} + +# adapter_build +# The single entry point invoked by scripts/build.sh. +adapter_build() { + local src="$1" dst="$2" + rm -rf "$dst" + mkdir -p "$dst" + adapter_translate_dispatcher "$src/DISPATCHER.md" "$dst" + adapter_translate_references "$src/references" "$dst" + adapter_translate_skills "$src/skills" "$dst" + adapter_translate_agents "$src/agents" "$dst" + adapter_translate_hooks "$src/hooks" "$dst" + adapter_translate_mcp "$src/mcp" "$dst" + adapter_finalize "$src" "$dst" +} diff --git a/adapters/claude-code/templates/cc-hook-wrapper.sh.tmpl b/adapters/claude-code/templates/cc-hook-wrapper.sh.tmpl new file mode 100755 index 0000000..7003047 --- /dev/null +++ b/adapters/claude-code/templates/cc-hook-wrapper.sh.tmpl @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# ============================================================================= +# Generated by adapters/claude-code/adapter.sh — do not edit. +# Wrapper for hook: __HOOK_NAME__ +# Reads Claude Code native PreToolUse/PostToolUse/Notification JSON from stdin, +# transforms it into the neutral schema, and pipes the result to __HOOK_NAME__.sh. +# ============================================================================= +set -eo pipefail + +INPUT=$(cat) +HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Translate CC native fields to the neutral schema +NEUTRAL=$(echo "$INPUT" | jq -c '{ + event: "__EVENT_NAME__", + tool: (.tool_name // ""), + args: (.tool_input // {title: .title, message: .message}), + session_id: (.session_id // ""), + cwd: (.cwd // ""), + framework: "claude-code", + platform_dir: ".claude", + dispatcher_name: "CLAUDE.md" +}') + +echo "$NEUTRAL" | bash "$HOOK_DIR/__HOOK_NAME__.sh" diff --git a/adapters/gemini-cli/adapter.sh b/adapters/gemini-cli/adapter.sh new file mode 100755 index 0000000..ff3052e --- /dev/null +++ b/adapters/gemini-cli/adapter.sh @@ -0,0 +1,318 @@ +#!/usr/bin/env bash +# ============================================================================= +# adapters/gemini-cli/adapter.sh — Gemini CLI framework adapter +# ============================================================================= +# Sourced by scripts/build.sh AFTER adapters/lib.sh. +# Translates source files into a dist/gemini-cli/ tree that mirrors what +# Gemini CLI expects in the user's project. +# ============================================================================= + +GEMINI_PLATFORM="gemini-cli" +GEMINI_FW_DIR="gemini" +GEMINI_DISPATCHER="GEMINI.md" + +# Capability → Gemini CLI tool names. Returns space-separated tool names. +gemini_capability_to_tools() { + local cap="$1" + case "$cap" in + read) echo "read_file list_directory grep_search" ;; + write) echo "write_file" ;; + edit) echo "replace" ;; + bash) echo "run_shell_command" ;; + webfetch) echo "web_fetch" ;; + websearch) echo "web_search" ;; + notebook) echo "" ;; + task) echo "activate_skill" ;; + todo) echo "" ;; + *) echo "" ;; + esac +} + +# Event vocabulary → Gemini CLI native event name. +gemini_event_to_native() { + local event="$1" + case "$event" in + before-tool-use) echo "BeforeTool" ;; + after-tool-use) echo "AfterTool" ;; + on-notification) echo "Notification" ;; + on-session-start) echo "SessionStart" ;; + on-prompt-submit) echo "BeforeAgent" ;; + *) echo "" ;; + esac +} + +# Neutral model tier → Gemini model name. +gemini_model_to_native() { + local model="$1" + case "$model" in + */*) echo "$model" ;; + low) echo "gemini-2.5-flash" ;; + mid) echo "gemini-2.5-flash" ;; + high) echo "gemini-2.5-pro" ;; + *) echo "$model" ;; + esac +} + +# Match-tool token → Gemini tool name for hook matchers. +gemini_match_tool_to_native() { + local token="$1" + case "$token" in + read) echo "read_file" ;; + write) echo "write_file" ;; + edit) echo "replace" ;; + bash) echo "run_shell_command" ;; + *) echo "$token" ;; + esac +} + +# adapter_translate_dispatcher +# Copies the source DISPATCHER.md to dest_dir/GEMINI.md (no content change). +adapter_translate_dispatcher() { + local src="$1" dst="$2" + [[ -f "$src" ]] || return 0 + mkdir -p "$dst" + cp "$src" "$dst/$GEMINI_DISPATCHER" + rewrite_platform_paths "$dst/$GEMINI_DISPATCHER" "$GEMINI_FW_DIR" "$GEMINI_DISPATCHER" +} + +# adapter_translate_references +adapter_translate_references() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + local out="$dst/.$GEMINI_FW_DIR/references" + mkdir -p "$out" + for f in "$src"/*.md; do + [[ -f "$f" ]] || continue + should_include "$f" "$GEMINI_PLATFORM" || continue + cp "$f" "$out/" + rewrite_platform_paths "$out/$(basename "$f")" "$GEMINI_FW_DIR" "$GEMINI_DISPATCHER" + done +} + +# adapter_translate_skills +adapter_translate_skills() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + for skill_dir in "$src"/*/; do + [[ -f "${skill_dir}SKILL.md" ]] || continue + should_include "${skill_dir}SKILL.md" "$GEMINI_PLATFORM" || continue + local name; name="$(basename "$skill_dir")" + local out="$dst/.$GEMINI_FW_DIR/skills/$name" + mkdir -p "$out" + cp "${skill_dir}SKILL.md" "$out/SKILL.md" + rewrite_platform_paths "$out/SKILL.md" "$GEMINI_FW_DIR" "$GEMINI_DISPATCHER" + done +} + +# adapter_translate_agents +adapter_translate_agents() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + local out_dir="$dst/.$GEMINI_FW_DIR/agents" + mkdir -p "$out_dir" + + while IFS= read -r agent; do + [[ -f "$agent" ]] || continue + should_include "$agent" "$GEMINI_PLATFORM" || continue + + local name; name="$(parse_frontmatter "$agent" name)" + local model_raw; model_raw="$(parse_frontmatter "$agent" model)" + local caps; caps="$(parse_capabilities "$agent")" + + local model_out; model_out="$(gemini_model_to_native "$model_raw")" + + # Build deduplicated tools list + local tools_seen="" tools_yaml="" + for cap in $caps; do + local expansion; expansion="$(gemini_capability_to_tools "$cap")" + for tool in $expansion; do + [[ -z "$tool" ]] && continue + case " $tools_seen " in + *" $tool "*) ;; + *) + tools_seen="$tools_seen $tool" + tools_yaml="${tools_yaml} - ${tool} +" + ;; + esac + done + done + + local out_file="$out_dir/$(basename "$agent")" + { + echo "---" + echo "name: $name" + # Copy description block (may be folded YAML) + awk '/^---$/{n++; next} n==1 && /^description:/{print; in_desc=1; next} n==1 && in_desc && /^[[:space:]]/{print; next} n==1 && in_desc && !/^[[:space:]]/{in_desc=0} n>=2{exit}' "$agent" + echo "tools:" + printf '%s' "$tools_yaml" + echo "model: $model_out" + echo "---" + agent_body "$agent" + } > "$out_file" + rewrite_platform_paths "$out_file" "$GEMINI_FW_DIR" "$GEMINI_DISPATCHER" + done < <(enumerate_agents "$src") +} + +# adapter_translate_hooks +adapter_translate_hooks() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + + local hooks_out="$dst/.$GEMINI_FW_DIR/hooks" + mkdir -p "$hooks_out" + + local tpl_dir; tpl_dir="$(dirname "${BASH_SOURCE[0]}")/templates" + local hooks_json='{}' + local have_any=0 + + while IFS= read -r yaml; do + [[ -f "$yaml" ]] || continue + should_include "$yaml" "$GEMINI_PLATFORM" || continue + + local meta; meta="$(parse_hook_yaml "$yaml")" + local hook_name; hook_name="$(echo "$meta" | grep '^name=' | head -1 | cut -d= -f2-)" + local script; script="$(echo "$meta" | grep '^script=' | head -1 | cut -d= -f2-)" + local event; event="$(echo "$meta" | grep '^event=' | head -1 | cut -d= -f2-)" + local match_tool; match_tool="$(echo "$meta" | grep '^match-tool=' | head -1 | cut -d= -f2- || true)" + + [[ -f "$src/$script" ]] || continue + + # Copy the hook script + cp "$src/$script" "$hooks_out/$script" + chmod +x "$hooks_out/$script" + rewrite_platform_paths "$hooks_out/$script" "$GEMINI_FW_DIR" "$GEMINI_DISPATCHER" + + # Generate wrapper script from template + local wrapper_name="${hook_name}-wrapper.sh" + sed -e "s/__HOOK_NAME__/$hook_name/g" -e "s/__EVENT_NAME__/$event/g" \ + "$tpl_dir/gemini-hook-wrapper.sh.tmpl" > "$hooks_out/$wrapper_name" + chmod +x "$hooks_out/$wrapper_name" + + # Build matcher: map each match-tool token to Gemini tool name + local gemini_event; gemini_event="$(gemini_event_to_native "$event")" + local matcher="" + if [[ -n "$match_tool" ]]; then + for t in $match_tool; do + local native; native="$(gemini_match_tool_to_native "$t")" + if [[ -z "$matcher" ]]; then + matcher="$native" + else + matcher="$matcher|$native" + fi + done + fi + + # Add to hooks JSON + local hook_cmd="bash .$GEMINI_FW_DIR/hooks/$wrapper_name" + if [[ -n "$matcher" ]]; then + hooks_json="$(echo "$hooks_json" | jq \ + --arg ev "$gemini_event" \ + --arg matcher "$matcher" \ + --arg cmd "$hook_cmd" \ + '.hooks[$ev] += [{ matcher: $matcher, hooks: [{ type: "command", command: $cmd, timeout: 5000 }] }]')" + else + hooks_json="$(echo "$hooks_json" | jq \ + --arg ev "$gemini_event" \ + --arg cmd "$hook_cmd" \ + '.hooks[$ev] += [{ hooks: [{ type: "command", command: $cmd, timeout: 5000 }] }]')" + fi + have_any=1 + done < <(enumerate_hooks "$src") + + if [[ $have_any -eq 0 ]]; then + rmdir "$hooks_out" 2>/dev/null || true + return 0 + fi + + echo "$hooks_json" | jq '.' > "$dst/.$GEMINI_FW_DIR/_hooks.json" +} + +# adapter_translate_mcp +adapter_translate_mcp() { + local src="$1" dst="$2" + local yaml="$src/servers.yaml" + [[ -f "$yaml" ]] || return 0 + + mkdir -p "$dst/.$GEMINI_FW_DIR" + + local json='{}' + local current_name="" current_cmd="" current_url="" current_type="" + + _gemini_flush_mcp() { + [[ -z "$current_name" ]] && return 0 + if [[ "$current_type" == "local" || -n "$current_cmd" ]]; then + local cmd_first; cmd_first="$(echo "$current_cmd" | awk '{print $1}')" + local cmd_rest; cmd_rest="$(echo "$current_cmd" | awk '{$1=""; print}' | sed 's/^ *//')" + local args_json; args_json="$(echo "$cmd_rest" | jq -R 'split(" ") | map(select(length > 0))')" + json="$(echo "$json" | jq --arg n "$current_name" --arg c "$cmd_first" --argjson a "$args_json" \ + '.mcpServers[$n] = {command: $c, args: $a}')" + else + json="$(echo "$json" | jq --arg n "$current_name" --arg u "$current_url" \ + '.mcpServers[$n] = {url: $u}')" + fi + } + + while IFS= read -r line; do + case "$line" in + *"- name:"*) + _gemini_flush_mcp + current_name="$(echo "$line" | sed 's/.*- name:[[:space:]]*//' | tr -d '"')" + current_cmd="" ; current_url="" ; current_type="" + ;; + *"type:"*) + current_type="$(echo "$line" | sed 's/.*type:[[:space:]]*//' | tr -d '"')" + ;; + *"command:"*"["*) + current_cmd="$(echo "$line" | sed 's/.*command:[[:space:]]*\[//' | sed 's/\][[:space:]]*$//' | tr -d '"' | sed 's/,[[:space:]]*/ /g')" + ;; + *"url:"*) + current_url="$(echo "$line" | sed 's/.*url:[[:space:]]*//' | tr -d '"')" + ;; + esac + done < "$yaml" + _gemini_flush_mcp + + echo "$json" | jq '.' > "$dst/.$GEMINI_FW_DIR/_mcp.json" + unset -f _gemini_flush_mcp +} + +# adapter_finalize +adapter_finalize() { + local src="$1" dst="$2" + local gemini_dir="$dst/.$GEMINI_FW_DIR" + local hooks_tmp="$gemini_dir/_hooks.json" + local mcp_tmp="$gemini_dir/_mcp.json" + local settings="$gemini_dir/settings.json" + + local result='{}' + + if [[ -f "$hooks_tmp" ]]; then + result="$(echo "$result" | jq --slurpfile h "$hooks_tmp" '. + $h[0]')" + rm "$hooks_tmp" + fi + + if [[ -f "$mcp_tmp" ]]; then + result="$(echo "$result" | jq --slurpfile m "$mcp_tmp" '. + $m[0]')" + rm "$mcp_tmp" + fi + + if [[ "$result" != "{}" ]]; then + echo "$result" | jq '.' > "$settings" + fi +} + +# adapter_build +# The single entry point invoked by scripts/build.sh. +adapter_build() { + local src="$1" dst="$2" + rm -rf "$dst" + mkdir -p "$dst" + adapter_translate_dispatcher "$src/DISPATCHER.md" "$dst" + adapter_translate_references "$src/references" "$dst" + adapter_translate_skills "$src/skills" "$dst" + adapter_translate_agents "$src/agents" "$dst" + adapter_translate_hooks "$src/hooks" "$dst" + adapter_translate_mcp "$src/mcp" "$dst" + adapter_finalize "$src" "$dst" +} diff --git a/adapters/gemini-cli/templates/gemini-hook-wrapper.sh.tmpl b/adapters/gemini-cli/templates/gemini-hook-wrapper.sh.tmpl new file mode 100755 index 0000000..d256434 --- /dev/null +++ b/adapters/gemini-cli/templates/gemini-hook-wrapper.sh.tmpl @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# ============================================================================= +# Generated by adapters/gemini-cli/adapter.sh — do not edit. +# Wrapper for hook: __HOOK_NAME__ +# Reads Gemini CLI native JSON from stdin, translates it into the neutral +# schema, and pipes the result to __HOOK_NAME__.sh. +# ============================================================================= +set -eo pipefail + +INPUT=$(cat) +HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +NEUTRAL=$(echo "$INPUT" | jq -c '{ + event: "__EVENT_NAME__", + tool: (.tool_name // ""), + args: (.tool_input // {}), + session_id: (env.GEMINI_SESSION_ID // ""), + cwd: (env.GEMINI_CWD // env.GEMINI_PROJECT_DIR // ""), + framework: "gemini-cli", + platform_dir: ".gemini", + dispatcher_name: "GEMINI.md" +}') + +echo "$NEUTRAL" | bash "$HOOK_DIR/__HOOK_NAME__.sh" diff --git a/adapters/lib.sh b/adapters/lib.sh new file mode 100755 index 0000000..1dab156 --- /dev/null +++ b/adapters/lib.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# ============================================================================= +# adapters/lib.sh — Shared helpers for framework adapters +# ============================================================================= +# Sourced by scripts/build.sh BEFORE the framework-specific adapter.sh. +# Provides vocabulary tables, parsing helpers, filtering, and JSON utilities. +# Do NOT execute directly. +# ============================================================================= + +# ── Vocabulary constants ───────────────────────────────────────────────────── +# The closed set of capabilities the source frontmatter may declare. +# Adapters consult this list to validate frontmatter before translating. +CAPABILITY_VOCAB="read write edit bash webfetch websearch notebook task todo" + +# The closed set of hook event names the source .hook.yaml may declare. +EVENT_VOCAB="before-tool-use after-tool-use on-notification on-session-start on-prompt-submit" + +# The closed set of model names the source agents may declare. +MODEL_VOCAB="low mid high" + +# ── Path rewriting ─────────────────────────────────────────────────────────── + +# rewrite_platform_paths +# Rewrites platform-neutral path references in a text file. +# Source files use .platform/ and DISPATCHER.md as neutral placeholders. +# Each adapter calls this after copying any text file from source to dist, +# passing its platform-specific directory name and dispatcher filename. +rewrite_platform_paths() { + local file="$1" platform_dir="$2" dispatcher="$3" + local tmp; tmp="$(mktemp)" + sed "s|\.platform/|.${platform_dir}/|g; s|DISPATCHER\.md|${dispatcher}|g" "$file" > "$tmp" + mv "$tmp" "$file" +} + +# ── Parsing helpers ────────────────────────────────────────────────────────── + +# parse_frontmatter +# Echoes the value of a top-level YAML key from the file's --- ... --- block. +# Returns nothing (empty) if the key is not found. +# Supports scalar values and flat list values; preserves the raw value as-written. +parse_frontmatter() { + local file="$1" key="$2" + awk -v key="$key" ' + /^---$/ { fm++; next } + fm == 1 { + # Match "key: value" — strip leading whitespace, capture value after first ":" + sub(/^[[:space:]]+/, "") + if (match($0, "^" key ":[[:space:]]*")) { + value = substr($0, RLENGTH + 1) + sub(/[[:space:]]+$/, "", value) + print value + exit + } + } + fm >= 2 { exit } + ' "$file" +} + +# parse_capabilities +# Echoes the agent's capabilities as space-separated tokens. +# Empty output if capabilities is missing or []. +parse_capabilities() { + local file="$1" + local raw; raw="$(parse_frontmatter "$file" capabilities)" + # Strip [ and ] and commas, leaving space-separated tokens + echo "$raw" | tr -d '[]' | tr ',' ' ' | xargs +} + +# should_include +# Exit 0 if the component should be included in the given framework's build. +# Exit 1 if the component's exclude: list contains the framework. +# Supports both frontmatter-delimited files (agents, skills) and plain YAML +# files (hook .yaml) — falls back to a direct key read if no frontmatter found. +should_include() { + local file="$1" framework="$2" + local raw; raw="$(parse_frontmatter "$file" exclude)" + # If parse_frontmatter returned nothing, try reading exclude: as a plain YAML key + # (hook .yaml files don't have --- delimiters) + if [[ -z "$raw" ]]; then + raw="$(awk '/^exclude:/ { sub(/^exclude:[[:space:]]*/, ""); print; exit }' "$file")" + fi + # Treat missing or empty exclude as "include" + [[ -z "$raw" || "$raw" == "[]" ]] && return 0 + # Tokenize the list and check membership + local tokens; tokens="$(echo "$raw" | tr -d '[]' | tr ',' ' ' | xargs)" + for t in $tokens; do + [[ "$t" == "$framework" ]] && return 1 + done + return 0 +} + +# parse_hook_yaml +# Emits key=value lines for each top-level scalar AND for trigger fields. +# Output keys: name, script, event, match-tool (space-separated tokens), exclude. +# +# NOTE: This flattens all triggers into a single stream. When a hook has +# multiple triggers, the association between event and match-tool is lost. +# Currently all hooks use a single trigger, so this is not an issue in +# practice. If multi-trigger hooks are needed, this output format must be +# changed to emit delimited records (one per trigger). +parse_hook_yaml() { + local file="$1" + awk ' + /^name:/ { sub(/^name:[[:space:]]*/, ""); print "name=" $0 } + /^script:/ { sub(/^script:[[:space:]]*/, ""); print "script=" $0 } + /^[[:space:]]*-[[:space:]]*event:/ { + sub(/^[[:space:]]*-[[:space:]]*event:[[:space:]]*/, "") + print "event=" $0 + } + /^[[:space:]]*match-tool:/ { + sub(/^[[:space:]]*match-tool:[[:space:]]*/, "") + gsub(/[\[\],]/, " ") + gsub(/^[[:space:]]+|[[:space:]]+$/, "") + gsub(/[[:space:]]+/, " ") + print "match-tool=" $0 + } + /^exclude:/ { sub(/^exclude:[[:space:]]*/, ""); print "exclude=" $0 } + ' "$file" +} + +# agent_body +# Echoes everything after the closing --- of the frontmatter block. +agent_body() { + local file="$1" + awk ' + fm < 2 && /^---$/ { fm++; next } + fm >= 2 { print } + ' "$file" +} + +# enumerate_agents +# Echoes one agent file path per line. +enumerate_agents() { + local dir="$1" + [[ -d "$dir" ]] || return 0 + for f in "$dir"/*.md; do + [[ -f "$f" ]] && echo "$f" + done +} + +# enumerate_hooks +# Echoes one .hook.yaml file path per line. +enumerate_hooks() { + local dir="$1" + [[ -d "$dir" ]] || return 0 + for f in "$dir"/*.hook.yaml; do + [[ -f "$f" ]] && echo "$f" + done +} diff --git a/adapters/opencode/adapter.sh b/adapters/opencode/adapter.sh new file mode 100755 index 0000000..72259e1 --- /dev/null +++ b/adapters/opencode/adapter.sh @@ -0,0 +1,353 @@ +#!/usr/bin/env bash +# ============================================================================= +# adapters/opencode/adapter.sh — Opencode framework adapter +# ============================================================================= +# Sourced by scripts/build.sh AFTER adapters/lib.sh. +# Translates source files into a dist/opencode/ tree that mirrors what +# opencode expects in the user's vault. +# ============================================================================= + +# shellcheck source=adapters/opencode/config-merge.sh +source "$(dirname "${BASH_SOURCE[0]}")/config-merge.sh" + +OC_PLATFORM="opencode" +OC_FW_DIR="opencode" +OC_DISPATCHER="AGENTS.md" + +# Capability → opencode permission key. Returns the permission key to set to +# "allow" for each capability, or empty string for capabilities that have no +# opencode equivalent (they are dropped). +# +# Reference (spec §"Capability vocabulary"): +# read → implicit, no permission needed +# write → edit: allow +# edit → edit: allow +# bash → bash: allow +# webfetch → webfetch: allow +# websearch → drop (no equivalent) +# notebook → drop +# task → drop (subagent invocation, not a permission) +# todo → drop +oc_capability_to_permission() { + local cap="$1" + case "$cap" in + read) echo "" ;; # implicit + write) echo "edit" ;; + edit) echo "edit" ;; + bash) echo "bash" ;; + webfetch) echo "webfetch" ;; + websearch) echo "" ;; # drop + notebook) echo "" ;; # drop + task) echo "" ;; # drop + todo) echo "" ;; # drop + *) echo "" ;; + esac +} + +# Event vocabulary → opencode native event name. +oc_event_to_native() { + local event="$1" + case "$event" in + before-tool-use) echo "tool.execute.before" ;; + after-tool-use) echo "tool.execute.after" ;; + on-notification) echo "session.idle" ;; + on-session-start) echo "session.created" ;; + on-prompt-submit) echo "tui.prompt.append" ;; + *) echo "" ;; + esac +} + +# Neutral model tier → opencode provider/model id. Conservative mapping: +# if the source model is already provider-prefixed (contains "/"), pass through +# unchanged. Otherwise look up in the table and fall back to the raw value. +oc_model_to_provider() { + local model="$1" + case "$model" in + */*) echo "$model" ;; # already qualified + low) echo "anthropic/claude-haiku-4-5" ;; + mid) echo "anthropic/claude-sonnet-4-5" ;; + high) echo "anthropic/claude-opus-4-5" ;; + *) echo "$model" ;; # unknown — passthrough + esac +} + +# adapter_translate_dispatcher +# Copies the source DISPATCHER.md to dest_dir/AGENTS.md (opencode's vault-root +# dispatcher filename). Rewrites .platform/ and DISPATCHER.md to opencode paths. +adapter_translate_dispatcher() { + local src="$1" dst="$2" + [[ -f "$src" ]] || return 0 + mkdir -p "$dst" + cp "$src" "$dst/AGENTS.md" + rewrite_platform_paths "$dst/AGENTS.md" "$OC_FW_DIR" "$OC_DISPATCHER" +} + +# adapter_translate_references +# Copies *.md into dest_root/.opencode/references/, rewriting framework paths. +adapter_translate_references() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + local out="$dst/.opencode/references" + mkdir -p "$out" + for f in "$src"/*.md; do + [[ -f "$f" ]] || continue + should_include "$f" "$OC_PLATFORM" || continue + cp "$f" "$out/" + rewrite_platform_paths "$out/$(basename "$f")" "$OC_FW_DIR" "$OC_DISPATCHER" + done +} + +# adapter_translate_skills +# Copies each skill directory's SKILL.md into dest_root/.opencode/skills//, +# rewriting framework paths in the body. +adapter_translate_skills() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + for skill_dir in "$src"/*/; do + [[ -f "${skill_dir}SKILL.md" ]] || continue + should_include "${skill_dir}SKILL.md" "$OC_PLATFORM" || continue + local name; name="$(basename "$skill_dir")" + local out="$dst/.opencode/skills/$name" + mkdir -p "$out" + cp "${skill_dir}SKILL.md" "$out/SKILL.md" + rewrite_platform_paths "$out/SKILL.md" "$OC_FW_DIR" "$OC_DISPATCHER" + done +} + +# adapter_translate_agents +# For each *.md in source_agents_dir, translate the capabilities frontmatter +# into an opencode permission block, map the model, and write to +# dest_root/.opencode/agents/.md. +adapter_translate_agents() { + local src="$1" dst="$2" + [[ -d "$src" ]] || return 0 + local out_dir="$dst/.opencode/agents" + mkdir -p "$out_dir" + + while IFS= read -r agent; do + [[ -f "$agent" ]] || continue + should_include "$agent" "$OC_PLATFORM" || continue + + local model_raw; model_raw="$(parse_frontmatter "$agent" model)" + local mode_raw; mode_raw="$(parse_frontmatter "$agent" mode)" + local caps; caps="$(parse_capabilities "$agent")" + + local model_out; model_out="$(oc_model_to_provider "$model_raw")" + local mode_out="${mode_raw:-subagent}" + + # Build a unique permission list + local perms="" + for cap in $caps; do + local p; p="$(oc_capability_to_permission "$cap")" + [[ -z "$p" ]] && continue + # Dedupe: skip if already in $perms (space-delimited) + case " $perms " in + *" $p "*) ;; + *) perms="$perms $p" ;; + esac + done + perms="${perms# }" + + local out_file="$out_dir/$(basename "$agent")" + { + echo "---" + # Copy description block verbatim (may be folded YAML with continuation lines) + awk '/^---$/{n++; next} n==1 && /^description:/{print; in_desc=1; next} n==1 && in_desc && /^[[:space:]]/{print; next} n==1 && in_desc && !/^[[:space:]]/{in_desc=0} n>=2{exit}' "$agent" + echo "mode: $mode_out" + echo "model: $model_out" + if [[ -z "$perms" ]]; then + echo "permission: {}" + else + echo "permission:" + for p in $perms; do + echo " $p: allow" + done + fi + echo "---" + echo "" + agent_body "$agent" + } > "$out_file" + rewrite_platform_paths "$out_file" "$OC_FW_DIR" "$OC_DISPATCHER" + done < <(enumerate_agents "$src") +} + +# _oc_hook_registry_json +# Emits a JSON array literal representing the hook registry, suitable for +# substituting into the plugin template. Each entry: {name, script, triggers: [{event, matchTool}]}. +# Script paths are stored as "../hooks/" so the plugin (at .opencode/plugins/) +# can reach .opencode/hooks/ at runtime via __dirname + path join. +_oc_hook_registry_json() { + local src="$1" + local entries='[]' + while IFS= read -r yaml; do + [[ -f "$yaml" ]] || continue + should_include "$yaml" "$OC_PLATFORM" || continue + local meta; meta="$(parse_hook_yaml "$yaml")" + local name; name="$(echo "$meta" | grep '^name=' | head -1 | cut -d= -f2-)" + local script; script="$(echo "$meta" | grep '^script=' | head -1 | cut -d= -f2-)" + local event; event="$(echo "$meta" | grep '^event=' | head -1 | cut -d= -f2-)" + local match_tool; match_tool="$(echo "$meta" | grep '^match-tool=' | head -1 | cut -d= -f2- || true)" + local oc_event; oc_event="$(oc_event_to_native "$event")" + + # Build the matchTool JSON array (empty array if no filter) + local match_json='[]' + if [[ -n "$match_tool" ]]; then + match_json="$(echo "$match_tool" | jq -R 'split(" ") | map(select(length > 0))')" + fi + + # Use ../hooks/