{"items":[{"id":"cmugucu8l00iwqu06jgdj4yr2","slug":"nanocoai-nanoclaw-add-dashboard","name":"add-dashboard","description":"Add a monitoring dashboard to NanoClaw. Installs @nanoco/nanoclaw-dashboard and a pusher that sends periodic JSON snapshots.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-dashboard","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add a monitoring dashboard to NanoClaw. Installs @nanoco/nanoclaw-dashboard and a pusher that sends periodic JSON snapshots.","permissions":[],"systemPrompt":"# /add-dashboard — NanoClaw Dashboard\n\nAdds a local monitoring dashboard showing agent groups, sessions, channels, users, token usage, context windows, message activity, and real-time logs.\n\n## Architecture\n\n```\nNanoClaw (pusher)              Dashboard (npm package)\n┌──────────┐    POST JSON      ┌──────────────┐\n│ collects │ ────────────────→ │ /api/ingest  │\n│ DB data  │   every 60s       │ in-memory    │\n│ tails    │ ────────────────→ │ /api/logs/   │\n│ log file │   every 2s        │   push       │\n└──────────┘                   │ serves UI    │\n                               └──────────────┘\n```\n\n## Steps\n\n### 1. Install the npm package\n\n```bash\npnpm install @nanoco/nanoclaw-dashboard\n```\n\n### 2. Copy the pusher module and its tests\n\nCopy all three resource files into `src/`. The tests ship with the skill and run against the composed project — they're how you confirm the skill works and is wired in correctly.\n\n```\n.claude/skills/add-dashboard/resources/dashboard-pusher.ts       → src/dashboard-pusher.ts\n.claude/skills/add-dashboard/resources/dashboard-pusher.test.ts  → src/dashboard-pusher.test.ts\n.claude/skills/add-dashboard/resources/dashboard-wiring.test.ts  → src/dashboard-wiring.test.ts\n```\n\n- `dashboard-pusher.test.ts` — behavior: starts the pusher, posts a real snapshot to a fake dashboard.\n- `dashboard-wiring.test.ts` — the code edit in step 3: asserts (via the TS AST) that `index.ts` dynamically imports `./dashboard-pusher.js` and `await`s `startDashboard()` as colocated statements of `main()`, after DB init and before the boot-complete log. Delete or misplace the edit and this goes red.\n\n### 3. Wire into src/index.ts\n\nThis is the skill's one integration point, and it's deliberately minimal and self-contained: all the startup logic lives in `dashboard-pusher.ts`, and the import is **colocated** with the call so the whole edit is a single block in one place — there's no separate top-of-file import to add (or to remember to remove).\n\nAdd this block inside `main()`, just before the `log.info('NanoClaw running')` line:\n\n```typescript\n  // Dashboard (optional; no-ops without DASHBOARD_SECRET)\n  const { startDashboard } = await import('./dashboard-pusher.js');\n  await startDashboard();\n```\n\n`startDashboard()` reads `DASHBOARD_SECRET`/`DASHBOARD_PORT` itself and no-ops if the secret is unset, so nothing else in core needs to change.\n\n### 4. Add environment variables to .env\n\n```\nDASHBOARD_SECRET=<generate-a-random-secret>\nDASHBOARD_PORT=3100\n```\n\nGenerate the secret: `node -e \"console.log('nc-' + require('crypto').randomBytes(16).toString('hex'))\"`\n\n### 5. Build, test, and restart\n\nRun from your NanoClaw project root:\n\n```bash\npnpm run build\npnpm exec vitest run src/dashboard-pusher.test.ts src/dashboard-wiring.test.ts   # behavior + wiring\nsource setup/lib/install-slug.sh\nsystemctl --user restart $(systemd_unit)              # Linux\n# or: launchctl kickstart -k gui/$(id -u)/$(launchd_label)  # macOS\n```\n\nRun `build` **before** the tests: it's what guards the `@nanoco/nanoclaw-dashboard` dependency. `dashboard-pusher.ts` reaches the package through `await import('@nanoco/nanoclaw-dashboard')`, so if step 4 was skipped, `pnpm run build` fails with `TS2307: Cannot find module`. The behavior test deliberately *mocks* that package — its `startDashboard` binds a real dashboard port, a side effect we don't want in a test — so the test alone would pass with the dependency missing. Build is therefore the leg that verifies the dependency is installed; keep it ahead of the tests in the validate step.\n\n### 6. Verify (runtime smoke check)\n\nOnce the service is restarted, confirm the dashboard is live:\n\n```bash\ncurl -s http://localhost:3100/api/status\ncurl -s -H \"Authorization: Bearer <secret>\" http://localhost:3100/api/overview\n```\n\nOpen `http://localhost:3100/dashboard` in a browser.\n\n## Dashboard Pages\n\n| Page | Shows |\n|------|-------|\n| Overview | Stats, token usage + cache hit rate, context windows, activity chart |\n| Agent Groups | Sessions, wirings, destinations, members, admins |\n| Sessions | Status, container state, context window usage bars |\n| Channels | Live/offline status, messaging groups, sender policies |\n| Messages | Per-session inbound/outbound messages |\n| Users | Privilege hierarchy: owner > admin > member |\n| Logs | Real-time log streaming with level filter |\n\n## Troubleshooting\n\n- **\"No data yet\"**: Wait 60s for first push, or check logs for push errors\n- **401 errors**: Verify `DASHBOARD_SECRET` matches in `.env`\n- **Port conflict**: Change `DASHBOARD_PORT` in `.env`\n- **No logs**: Check `logs/nanoclaw.log` exists\n\n## Removal\n\nReverse the apply steps. Safe to re-run even if some pieces are already gone.\n\n```bash\nrm -f src/dashboard-pusher.ts src/dashboard-pusher.test.ts src/dashboard-wiring.test.ts\npnpm uninstall @nanoco/nanoclaw-dashboard 2>/dev/null || true\n```\n\nThen, by hand, remove the single dashboard block the skill added to `main()` in `src/index.ts` (the `// Dashboard (optional…)` comment, the `await import('./dashboard-pusher.js')` line, and the `await startDashboard();` call), and remove `DASHBOARD_SECRET` and `DASHBOARD_PORT` from `.env`.\n\n```bash\npnpm run build\n```","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-dashboard","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-dashboard/SKILL.md","defaultBranch":"main"},"readme":"# /add-dashboard — NanoClaw Dashboard\n\nAdds a local monitoring dashboard showing agent groups, sessions, channels, users, token usage, context windows, message activity, and real-time logs.\n\n## Architecture\n\n```\nNanoClaw (pusher)              Dashboard (npm package)\n┌──────────┐    POST JSON      ┌──────────────┐\n│ collects │ ────────────────→ │ /api/ingest  │\n│ DB data  │   every 60s       │ in-memory    │\n│ tails    │ ────────────────→ │ /api/logs/   │\n│ log file │   every 2s        │   push       │\n└──────────┘                   │ serves UI    │\n                               └──────────────┘\n```\n\n## Steps\n\n### 1. Install the npm package\n\n```bash\npnpm install @nanoco/nanoclaw-dashboard\n```\n\n### 2. Copy the pusher module and its tests\n\nCopy all three resource files into `src/`. The tests ship with the skill and run against the composed project — they're how you confirm the skill works and is wired in correctly.\n\n```\n.claude/skills/add-dashboard/resources/dashboard-pusher.ts       → src/dashboard-pusher.ts\n.claude/skills/add-dashboard/resources/dashboard-pusher.test.ts  → src/dashboard-pusher.test.ts\n.claude/skills/add-dashboard/resources/dashboard-wiring.test.ts  → src/dashboard-wiring.test.ts\n```\n\n- `dashboard-pusher.test.ts` — behavior: starts the pusher, posts a real snapshot to a fake dashboard.\n- `dashboard-wiring.test.ts` — the code edit in step 3: asserts (via the TS AST) that `index.ts` dynamically imports `./dashboard-pusher.js` and `await`s `startDashboard()` as colocated statements of `main()`, after DB init and before the boot-complete log. Delete or misplace the edit and this goes red.\n\n### 3. Wire into src/index.ts\n\nThis is the skill's one integration point, and it's deliberately minimal and self-contained: all the startup logic lives in `dashboard-pusher.ts`, and the import is **colocated** with the call so the whole edit is a single block in one place — there's no separate top-of-file import to add (or to remember to remove).\n\nAdd this block inside `main()`, just before the `log.info('NanoClaw running')` line:\n\n```typescript\n  // Dashboard (optional; no-ops without DASHBOARD_SECRET)\n  const { startDashboard } = await import('./dashboard-pusher.js');\n  await startDashboard();\n```\n\n`startDashboard()` reads `DASHBOARD_SECRET`/`DASHBOARD_PORT` itself and no-ops if the secret is unset, so nothing else in core needs to change.\n\n### 4. Add environment variables to .env\n\n```\nDASHBOARD_SECRET=<generate-a-random-secret>\nDASHBOARD_PORT=3100\n```\n\nGenerate the secret: `node -e \"console.log('nc-' + require('crypto').randomBytes(16).toString('hex'))\"`\n\n### 5. Build, test, and restart\n\nRun from your NanoClaw project root:\n\n```bash\npnpm run build\npnpm exec vitest run src/dashboard-pusher.test.ts src/dashboard-wiring.test.ts   # behavior + wiring\nsource setup/lib/install-slug.sh\nsystemctl --user restart $(systemd_unit)              # Linux\n# or: launchctl kickstart -k gui/$(id -u)/$(launchd_label)  # macOS\n```\n\nRun `build` **before** the tests: it's what guards the `@nanoco/nanoclaw-dashboard` dependency. `dashboard-pusher.ts` reaches the package through `await import('@nanoco/nanoclaw-dashboard')`, so if step 4 was skipped, `pnpm run build` fails with `TS2307: Cannot find module`. The behavior test deliberately *mocks* that package — its `startDashboard` binds a real dashboard port, a side effect we don't want in a test — so the test alone would pass with the dependency missing. Build is therefore the leg that verifies the dependency is installed; keep it ahead of the tests in the validate step.\n\n### 6. Verify (runtime smoke check)\n\nOnce the service is restarted, confirm the dashboard is live:\n\n```bash\ncurl -s http://localhost:3100/api/status\ncurl -s -H \"Authorization: Bearer <secret>\" http://localhost:3100/api/overview\n```\n\nOpen `http://localhost:3100/dashboard` in a browser.\n\n## Dashboard Pages\n\n| Page | Shows |\n|------|-------|\n| Overview | Stats, token usage + cache hit rate, context windows, activity chart |\n| Agent Grou","createdAt":"2026-09-25T10:52:06.550Z","updatedAt":"2026-09-25T10:52:06.550Z"},{"id":"cmugucu6g00ihqu06xov1p5y5","slug":"nanocoai-nanoclaw","name":"nanoclaw","description":"A lightweight alternative to OpenClaw that runs in containers for security. Connects to WhatsApp, Telegram, Slack, Discord, Gmail and other messaging apps,, has memory, scheduled jobs, and runs directly on Anthropic's Agents SDK","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"MCP","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"nanoclaw","tools":[],"category":"MCP","entrypoint":{"args":[],"type":"mcp-stdio","command":"npx"},"description":"","permissions":["shell","network"],"requiredEnv":[],"schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","mcp"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":".mcp.json","manifestPath":".mcp.json","defaultBranch":"main"},"readme":"<p align=\"center\">\n  <img src=\"assets/nanoclaw-logo.png\" alt=\"NanoClaw\" width=\"400\">\n</p>\n\n<p align=\"center\">\n  An AI assistant that runs agents securely in their own containers. Lightweight, built to be easily understood and completely customized for your needs.\n</p>\n\n<p align=\"center\">\n  <a href=\"https://nanoclaw.dev\">nanoclaw.dev</a>&nbsp; • &nbsp;\n  <a href=\"https://docs.nanoclaw.dev\">docs</a>&nbsp; • &nbsp;\n  <a href=\"README_zh.md\">中文</a>&nbsp; • &nbsp;\n  <a href=\"README_ja.md\">日本語</a>&nbsp; • &nbsp;\n  <a href=\"README_ko.md\">한국어</a>&nbsp; • &nbsp;\n  <a href=\"https://discord.gg/VDdww8qS42\"><img src=\"https://img.shields.io/discord/1470188214710046894?label=Discord&logo=discord&v=2\" alt=\"Discord\" valign=\"middle\"></a>&nbsp; • &nbsp;\n  <a href=\"repo-tokens\"><img src=\"repo-tokens/badge.svg\" alt=\"repo tokens\" valign=\"middle\"></a>\n</p>\n\n---\n\n<div align=\"center\">\n\n### <img src=\"https://img.shields.io/badge/NEW!-2EB67D?style=for-the-badge\" alt=\"NEW!\" valign=\"middle\"> Agents in Slack: one app per agent <img src=\"assets/slack-icon.svg\" alt=\"\" width=\"22\" valign=\"middle\">\n\nSetup provisions each agent its own Slack app: manifest, avatar, and workspace install, no tokens to paste.\nSpawn teammates from chat: every one gets its own bot identity, container, and memory, with shared rooms and canvases.\n\n[![Quick Start](https://img.shields.io/badge/Quick%20Start%20%E2%86%92-4A154B?style=for-the-badge)](#quick-start)\n\n</div>\n\n---\n\n## Why I Built NanoClaw\n\n[OpenClaw](https://github.com/openclaw/openclaw) is an impressive project, but I wouldn't have been able to sleep if I had given complex software I didn't understand full access to my life. OpenClaw has nearly half a million lines of code, 53 config files, and 70+ dependencies. Its security is at the application level (allowlists, pairing codes) rather than true OS-level isolation. Everything runs in one Node process with shared memory.\n\nNanoClaw provides that same core functionality, but in a codebase small enough to understand: one process and a handful of files. Agents run in their own Linux containers with filesystem isolation, not merely behind permission checks.\n\n## Quick Start\n\n```bash\ngit clone https://github.com/nanocoai/nanoclaw.git nanoclaw-v2\ncd nanoclaw-v2\nbash nanoclaw.sh\n```\n\n`nanoclaw.sh` walks you from a fresh machine to a named agent you can message. It installs Node, pnpm, and Docker if missing, installs a credential gateway and registers your Anthropic credential with it, builds the agent container, and pairs your first channel (Slack, Telegram, Discord, WhatsApp, iMessage, or a local CLI). If a step fails, Claude Code is invoked automatically to diagnose and resume from where it broke.\n\n<details>\n<summary><strong>Migrating from NanoClaw v1?</strong></summary>\n\nRun from a fresh v2 checkout next to your v1 install:\n\n```bash\ngit clone https://github.com/nanocoai/nanoclaw.git nanoclaw-v2\ncd nanoclaw-v2\nbash migrate-v2.sh\n```\n\n`migrate-v2.sh` finds your v1 install (sibling directory, or `NANOCLAW_V1_PATH=/path/to/nanoclaw`), migrates state into the v2 checkout, then `exec`s into Claude Code to finish the parts that need judgment (owner seeding, shared-memory migration, fork-customisation replay).\n\nRun the script directly, not from inside a Claude session — the deterministic side needs interactive prompts and real shell I/O for Node/pnpm bootstrap, Docker, the credential gateway, and the container build.\n\n**What it does:** merges `.env`, seeds the v2 DB from `registered_groups`, copies group folders + session data + scheduled tasks, installs the channel adapters you select, copies channel auth state (including the Baileys keystore for WhatsApp — LID mapping is now resolved per-message by the Baileys v7 adapter, not migrated), builds the agent container.\n\n**What it doesn't:** flip the system service. Pick *\"switch to v2\"* at the prompt, or do it manually after testing — your v1 install is left untouched.\n\nSee [docs/v1-to-v2-changes.md](docs/v1-to-v2-changes.md) for what's differe","createdAt":"2026-09-25T10:52:06.472Z","updatedAt":"2026-09-25T10:52:06.472Z"},{"id":"cmugucu6v00ikqu06g52ln9do","slug":"nanocoai-nanoclaw-add-anydoc","name":"add-anydoc","description":"Add local office-document-to-Markdown conversion to NanoClaw agent containers with the pinned Firecrawl AnyDoc CLI. Use when agents need to read attached Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, or text-based PDF files without uploading them to a hosted parser.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-anydoc","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add local office-document-to-Markdown conversion to NanoClaw agent containers with the pinned Firecrawl AnyDoc CLI. Use when agents need to read attached Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, or text-based PDF files without uploading them to a hosted parser.","permissions":[],"systemPrompt":"# Add AnyDoc\n\nInstall one pinned CLI and one focused container skill. Keep document conversion inside the agent container; do not change NanoClaw's attachment pipeline or add credentials, an MCP server, or a hosted parser.\n\n## Preflight\n\n1. Read `CONTRIBUTING.md`, `docs/skill-guidelines.md`, and the supply-chain section of `docs/SECURITY.md`.\n2. Run this check against the official npm registry before changing files:\n\n   ```bash\n   curl -fsSL \"https://registry.npmjs.org/@firecrawl%2Fanydoc\" | node -e '\n     let body = \"\";\n     process.stdin.setEncoding(\"utf8\");\n     process.stdin.on(\"data\", (chunk) => (body += chunk));\n     process.stdin.on(\"end\", () => {\n       const metadata = JSON.parse(body);\n       const version = \"0.1.6\";\n       const release = metadata.versions?.[version];\n       const publishedAt = Date.parse(metadata.time?.[version] ?? \"\");\n       const eligibleAt = publishedAt + 72 * 60 * 60 * 1000;\n       if (!release) throw new Error(`${version} is missing from the registry`);\n       if (release.deprecated) throw new Error(`${version} is deprecated: ${release.deprecated}`);\n       if (!Number.isFinite(publishedAt)) throw new Error(`missing publish time for ${version}`);\n       if (Date.now() < eligibleAt) throw new Error(`${version} is gated until ${new Date(eligibleAt).toISOString()}`);\n       console.log(`${version} passed the 72-hour release gate`);\n     });\n   '\n   ```\n\n   Stop on any failure. Do not install a PR commit, add a `minimumReleaseAgeExclude`, enable lifecycle scripts, or silently substitute another version, unless the user explicitly approves it.\n\n3. Inspect `container/cli-tools.json` for `@firecrawl/anydoc` before changing files:\n   - No entry: continue.\n   - Exactly one entry at `0.1.6`: leave it unchanged.\n   - A duplicate or any other version: stop and report the conflict.\n4. Check whether `container/skills/convert-documents-to-markdown/SKILL.md` and `src/anydoc-manifest.test.ts` already exist. Reapplying this skill overwrites only those dedicated files.\n5. If `data/v2.db` exists, inspect per-group image pins before changing files. Standard derived images can be rebuilt from the updated shared image. Stop and report any other pin because its owner must decide how to rebuild it:\n\n   ```bash\n   if [ -f data/v2.db ]; then\n     source setup/lib/install-slug.sh\n     image_base=\"$(container_image_base)\"\n     foreign=0\n     while IFS='|' read -r group_id image_tag package_count; do\n       [ -z \"$group_id\" ] && continue\n       if [ \"$image_tag\" != \"${image_base}:${group_id}\" ]; then\n         echo \"Foreign image pin: $group_id -> $image_tag\" >&2\n         foreign=1\n       elif [ \"$package_count\" -eq 0 ]; then\n         echo \"Derived image cannot be rebuilt: $group_id has no configured packages\" >&2\n         foreign=1\n       elif ! ncl groups get --id \"$group_id\" >/dev/null; then\n         echo \"Cannot reach NanoClaw through ncl for derived image: $group_id\" >&2\n         foreign=1\n       fi\n     done < <(pnpm exec tsx scripts/q.ts data/v2.db \\\n       \"SELECT agent_group_id, image_tag, COALESCE(json_array_length(packages_apt), 0) + COALESCE(json_array_length(packages_npm), 0) FROM container_configs WHERE image_tag IS NOT NULL ORDER BY agent_group_id\")\n     [ \"$foreign\" -eq 0 ]\n   fi\n   ```\n\n## Install\n\nResolve this skill's bundled files from either Claude Code's skill variable or the project skill directory, then copy both files:\n\n```bash\nproject_root=\"$(git rev-parse --show-toplevel)\"\nskill_dir=\"${CLAUDE_SKILL_DIR:-$project_root/.claude/skills/add-anydoc}\"\ntest -f \"$skill_dir/container-skills/convert-documents-to-markdown/SKILL.md\"\ntest -f \"$skill_dir/anydoc-manifest.test.ts\"\nmkdir -p container/skills/convert-documents-to-markdown\ncp \"$skill_dir/container-skills/convert-documents-to-markdown/SKILL.md\" \\\n  container/skills/convert-documents-to-markdown/SKILL.md\ncp \"$skill_dir/anydoc-manifest.test.ts\" src/anydoc-manifest.test.ts\n```\n\nIf the manifest has no AnyDoc entry, append this exact object to its JSON array. Do not add `onlyBuilt`; the package and its prebuilt Linux bindings have no install lifecycle script.\n\n```json\n{ \"name\": \"@firecrawl/anydoc\", \"version\": \"0.1.6\" }\n```\n\n## Validate and build\n\nRun validation before building the image:\n\n```bash\npnpm exec vitest run src/anydoc-manifest.test.ts container/cli-tools.test.ts\npnpm run build\n./container/build.sh\n```\n\nIf pnpm rejects the package as too new, stop. Do not bypass the release-age policy.\n\nRebuild standard per-group images so groups with custom packages inherit the updated shared image:\n\n```bash\nif [ -f data/v2.db ]; then\n  source setup/lib/install-slug.sh\n  image_base=\"$(container_image_base)\"\n  while IFS='|' read -r group_id image_tag; do\n    [ -z \"$group_id\" ] && continue\n    if [ \"$image_tag\" != \"${image_base}:${group_id}\" ]; then\n      echo \"Foreign image pin appeared during install: $group_id -> $image_tag\" >&2\n      exit 1\n    fi\n    ncl groups restart --id \"$group_id\" --rebuild\n  done < <(pnpm exec tsx scripts/q.ts data/v2.db \\\n    \"SELECT agent_group_id, image_tag FROM container_configs WHERE image_tag IS NOT NULL ORDER BY agent_group_id\")\nfi\n```\n\nResolve this install's image name and exercise the native binding, not only the help path:\n\n```bash\nsource setup/lib/install-slug.sh\nimage=\"$(container_image_base):latest\"\ndocker run --rm --entrypoint anydoc \"$image\" --version\nprintf 'name,count\\nalpha,2\\n' | \\\n  docker run --rm -i --entrypoint anydoc \"$image\" - --format csv | grep -q alpha\ndocker run --rm --entrypoint sh \"$image\" -c 'command -v timeout'\n```\n\nIf `timeout` is absent, remove its wrapper from the installed container skill; do not add another dependency. Convert local DOCX, PPTX, and XLSX fixtures when available. Do not add private or large binary fixtures to the repository.\n\n## Restart\n\nRestart this NanoClaw service only, so its running containers stop and default `skills: \"all\"` groups receive the new shared skill on their next spawn:\n\n```bash\nsource setup/lib/install-slug.sh\n# macOS\nlaunchctl kickstart -k \"gui/$(id -u)/$(launchd_label)\"\n# Linux\nsystemctl --user restart \"$(systemd_unit)\"\n```\n\nRun only the command for the current platform. If NanoClaw is not service-managed, stop this install's running agent containers by their `nanoclaw-install=<install-slug>` label instead of matching every `nanoclaw-v2` container on the host.\n\n## Smoke test\n\nUse one real channel attachment and verify the complete path:\n\n1. Confirm the message supplies an absolute local path such as `/workspace/inbox/<message-id>/<file>`, and use that exact path.\n2. Convert it to `/workspace/agent/converted/` and summarize only the relevant Markdown sections.\n3. Confirm an image-only PDF fails clearly and is not uploaded anywhere.\n4. For agents open to unknown senders, recommend an operator-set `CONTAINER_MEMORY_LIMIT`; AnyDoc's parser caps decompression, but NanoClaw containers have no memory limit by default.\n\nReport that office documents now convert locally. Call out that scanned PDFs need OCR, embedded visuals may be incomplete, and spreadsheet Markdown is not authoritative for calculations.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-anydoc","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-anydoc/SKILL.md","defaultBranch":"main"},"readme":"# Add AnyDoc\n\nInstall one pinned CLI and one focused container skill. Keep document conversion inside the agent container; do not change NanoClaw's attachment pipeline or add credentials, an MCP server, or a hosted parser.\n\n## Preflight\n\n1. Read `CONTRIBUTING.md`, `docs/skill-guidelines.md`, and the supply-chain section of `docs/SECURITY.md`.\n2. Run this check against the official npm registry before changing files:\n\n   ```bash\n   curl -fsSL \"https://registry.npmjs.org/@firecrawl%2Fanydoc\" | node -e '\n     let body = \"\";\n     process.stdin.setEncoding(\"utf8\");\n     process.stdin.on(\"data\", (chunk) => (body += chunk));\n     process.stdin.on(\"end\", () => {\n       const metadata = JSON.parse(body);\n       const version = \"0.1.6\";\n       const release = metadata.versions?.[version];\n       const publishedAt = Date.parse(metadata.time?.[version] ?? \"\");\n       const eligibleAt = publishedAt + 72 * 60 * 60 * 1000;\n       if (!release) throw new Error(`${version} is missing from the registry`);\n       if (release.deprecated) throw new Error(`${version} is deprecated: ${release.deprecated}`);\n       if (!Number.isFinite(publishedAt)) throw new Error(`missing publish time for ${version}`);\n       if (Date.now() < eligibleAt) throw new Error(`${version} is gated until ${new Date(eligibleAt).toISOString()}`);\n       console.log(`${version} passed the 72-hour release gate`);\n     });\n   '\n   ```\n\n   Stop on any failure. Do not install a PR commit, add a `minimumReleaseAgeExclude`, enable lifecycle scripts, or silently substitute another version, unless the user explicitly approves it.\n\n3. Inspect `container/cli-tools.json` for `@firecrawl/anydoc` before changing files:\n   - No entry: continue.\n   - Exactly one entry at `0.1.6`: leave it unchanged.\n   - A duplicate or any other version: stop and report the conflict.\n4. Check whether `container/skills/convert-documents-to-markdown/SKILL.md` and `src/anydoc-manifest.test.ts` already exist. Reapplying this skill overwrites only those dedicated files.\n5. If `data/v2.db` exists, inspect per-group image pins before changing files. Standard derived images can be rebuilt from the updated shared image. Stop and report any other pin because its owner must decide how to rebuild it:\n\n   ```bash\n   if [ -f data/v2.db ]; then\n     source setup/lib/install-slug.sh\n     image_base=\"$(container_image_base)\"\n     foreign=0\n     while IFS='|' read -r group_id image_tag package_count; do\n       [ -z \"$group_id\" ] && continue\n       if [ \"$image_tag\" != \"${image_base}:${group_id}\" ]; then\n         echo \"Foreign image pin: $group_id -> $image_tag\" >&2\n         foreign=1\n       elif [ \"$package_count\" -eq 0 ]; then\n         echo \"Derived image cannot be rebuilt: $group_id has no configured packages\" >&2\n         foreign=1\n       elif ! ncl groups get --id \"$group_id\" >/dev/null; then\n         echo \"Cannot reach NanoClaw through ncl for derived image: $group_id\" >&2\n         foreign=1\n       fi\n     done < <(pnpm exec tsx scripts/q.ts data/v2.db \\\n       \"SELECT agent_group_id, image_tag, COALESCE(json_array_length(packages_apt), 0) + COALESCE(json_array_length(packages_npm), 0) FROM container_configs WHERE image_tag IS NOT NULL ORDER BY agent_group_id\")\n     [ \"$foreign\" -eq 0 ]\n   fi\n   ```\n\n## Install\n\nResolve this skill's bundled files from either Claude Code's skill variable or the project skill directory, then copy both files:\n\n```bash\nproject_root=\"$(git rev-parse --show-toplevel)\"\nskill_dir=\"${CLAUDE_SKILL_DIR:-$project_root/.claude/skills/add-anydoc}\"\ntest -f \"$skill_dir/container-skills/convert-documents-to-markdown/SKILL.md\"\ntest -f \"$skill_dir/anydoc-manifest.test.ts\"\nmkdir -p container/skills/convert-documents-to-markdown\ncp \"$skill_dir/container-skills/convert-documents-to-markdown/SKILL.md\" \\\n  container/skills/convert-documents-to-markdown/SKILL.md\ncp \"$skill_dir/anydoc-manifest.test.ts\" src/anydoc-manifest.test.ts\n```\n\nIf the manifest has no AnyDoc entry, append this exact object to its JSON arra","createdAt":"2026-09-25T10:52:06.488Z","updatedAt":"2026-09-25T10:52:06.488Z"},{"id":"cmugucu7900inqu06o9bzchn1","slug":"nanocoai-nanoclaw-add-atomic-chat-tool","name":"add-atomic-chat-tool","description":"Add Atomic Chat MCP server so the container agent can call local models served by the Atomic Chat desktop app via its OpenAI-compatible API.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-atomic-chat-tool","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add Atomic Chat MCP server so the container agent can call local models served by the Atomic Chat desktop app via its OpenAI-compatible API.","permissions":[],"systemPrompt":"# Add Atomic Chat Integration\n\nThis skill adds a stdio-based MCP server that exposes models running in the local [Atomic Chat](https://github.com/AtomicBot-ai/Atomic-Chat) desktop app as tools for the container agent. Claude remains the orchestrator but can offload work to local models served by Atomic Chat on `http://127.0.0.1:1337/v1` (OpenAI-compatible).\n\nTools exposed:\n- `atomic_chat_list_models` — list models currently available in Atomic Chat (`GET /v1/models`)\n- `atomic_chat_generate` — send a prompt to a specified model and return the response (`POST /v1/chat/completions`)\n\nModel management (download, delete) is done through the **Atomic Chat desktop UI** — the app is a fork of Jan and manages its own model library.\n\nThe skill ships the MCP server source (and its test) in this folder and copies them into the agent-runner tree at install time, then registers the server in `index.ts` and forwards host env vars in `container-runner.ts`. Registering the server is enough to expose its tools — the agent's allow-pattern (`mcp__atomic_chat__*`) is derived from the registered server name.\n\n## Phase 1: Pre-flight\n\n### Check if already applied\n\nCheck if `container/agent-runner/src/atomic-chat-mcp-stdio.ts` exists. If it does, skip to Phase 3 (Configure).\n\n### Check prerequisites\n\nVerify Atomic Chat is installed and its local API server is running. On the host:\n\n```bash\ncurl -s http://127.0.0.1:1337/v1/models | head\n```\n\nIf the request fails:\n\n1. Install Atomic Chat from the [latest release](https://github.com/AtomicBot-ai/Atomic-Chat/releases) (macOS only for now — `atomic-chat.dmg`).\n2. Open the app.\n3. Open **Settings → Local API Server** and make sure it's enabled on port `1337`.\n4. Go to the **Hub** (or **Models**) tab and download at least one model (e.g. Llama 3.2 3B, Qwen 2.5 Coder 7B).\n5. Load the model once by sending any message in Atomic Chat's UI to warm it up.\n\n## Phase 2: Apply Code Changes\n\n### Copy the skill's source and tests into both trees\n\nThis skill reaches into both the container (Bun) tree and the host (Node) tree, so its\nfiles go into both, alongside the integration points they cover.\n\n```bash\nS=.claude/skills/add-atomic-chat-tool\n# Container (Bun) tree — the MCP server and the registration wiring test\ncp $S/atomic-chat-mcp-stdio.ts        container/agent-runner/src/atomic-chat-mcp-stdio.ts\ncp $S/atomic-chat-registration.test.ts container/agent-runner/src/atomic-chat-registration.test.ts\n# Host (Node) tree — the env-forwarding helper and the wiring test\ncp $S/atomic-chat-env.ts              src/atomic-chat-env.ts\ncp $S/atomic-chat-wiring.test.ts      src/atomic-chat-wiring.test.ts\n```\n\n### Register the MCP server in the agent-runner\n\nEdit `container/agent-runner/src/index.ts`. Find the `mcpServers` object that currently looks like this:\n\n```ts\n  const mcpServers: Record<string, { command: string; args: string[]; env: Record<string, string> }> = {\n    nanoclaw: {\n      command: 'bun',\n      args: ['run', mcpServerPath],\n      env: {},\n    },\n  };\n```\n\nAdd an `atomic_chat` entry alongside `nanoclaw`:\n\n```ts\n  const mcpServers: Record<string, { command: string; args: string[]; env: Record<string, string> }> = {\n    nanoclaw: {\n      command: 'bun',\n      args: ['run', mcpServerPath],\n      env: {},\n    },\n    atomic_chat: {\n      command: 'bun',\n      args: ['run', path.join(__dirname, 'atomic-chat-mcp-stdio.ts')],\n      env: {\n        ...(process.env.ATOMIC_CHAT_HOST ? { ATOMIC_CHAT_HOST: process.env.ATOMIC_CHAT_HOST } : {}),\n        ...(process.env.ATOMIC_CHAT_API_KEY ? { ATOMIC_CHAT_API_KEY: process.env.ATOMIC_CHAT_API_KEY } : {}),\n      },\n    },\n  };\n```\n\n`atomic-chat-registration.test.ts` asserts this entry is present and points at the server module — the tool only appears to the agent if it is registered here.\n\n### Forward host env vars into the container\n\nThe env-forwarding logic lives in the copied `src/atomic-chat-env.ts` (`atomicChatEnv()`), so the reach-in into `composeSessionSpec` is a single spread.\n\nImport it in `src/container-runner.ts` (alongside the other local imports):\n\n```ts\nimport { atomicChatEnv } from './atomic-chat-env.js';\n```\n\nThen, in `composeSessionSpec`, find the `contributedEnv` literal and spread the helper at the end. The contributed lane — not the composed `env` literal — because `ATOMIC_CHAT_API_KEY` is credential-NAMED and the composed lane's key-name check would refuse the spawn; the contributed lane exempts the name and still refuses credential-shaped values:\n\n```ts\n  const contributedEnv: Record<string, string> = {\n    ...(contribution.env ?? {}),\n    ...(gateway.env ?? {}),\n    ...atomicChatEnv(),\n  };\n```\n\n`atomic-chat-wiring.test.ts` asserts this `...atomicChatEnv()` spread exists inside `composeSessionSpec`.\n\n### Surface `[ATOMIC]` log lines at info level\n\n> **Shared block.** This rewrites the driver's container-stderr logger, which other local-model tools (e.g. `add-ollama-tool` for `[OLLAMA]`) also edit to surface their own prefix. Touch only the `[ATOMIC]` branch and leave the rest of the block intact, so the edits coexist and removal restores it cleanly.\n\nContainer stderr now lands in the Docker driver: in `src/drivers/docker-driver.ts`, inside `DockerHandle.start()`, find the stderr handler:\n\n```ts\n    proc.onStderr((line) => {\n      log.debug(line, { container: this.name });\n      this.#stderrTail.push(line);\n      if (this.#stderrTail.length > 10) this.#stderrTail.shift();\n    });\n```\n\nReplace the `log.debug` line with a prefix branch (leave the stderr-tail lines intact — they feed the non-zero-exit warning):\n\n```ts\n    proc.onStderr((line) => {\n      if (line.includes('[ATOMIC]')) {\n        log.info(line, { container: this.name });\n      } else {\n        log.debug(line, { container: this.name });\n      }\n      this.#stderrTail.push(line);\n      if (this.#stderrTail.length > 10) this.#stderrTail.shift();\n    });\n```\n\n### Add env-var stubs to `.env.example`\n\nAppend to `.env.example`:\n\n```bash\n# Atomic Chat MCP tool (.claude/skills/add-atomic-chat-tool)\n# Override the host where Atomic Chat exposes its OpenAI-compatible API.\n# Default: http://host.docker.internal:1337 (with fallback to localhost)\n# ATOMIC_CHAT_HOST=http://host.docker.internal:1337\n\n# Optional API key. Leave unset for a local Atomic Chat install — it does not require auth.\n# ATOMIC_CHAT_API_KEY=\n```\n\n### Validate code changes\n\n```bash\npnpm run build\npnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit\n# Host tree: composeSessionSpec wiring\npnpm exec vitest run src/atomic-chat-wiring.test.ts\n# Container tree: index.ts registration\n(cd container/agent-runner && bun test src/atomic-chat-registration.test.ts)\n./container/build.sh\n```\n\nAll must be clean before proceeding. The wiring and registration tests confirm the two\nintegration points — the `composeSessionSpec` spread and the `index.ts` registration — are\nactually in place; a failure means one drifted. (The MCP server's own request/response\nbehavior against Atomic Chat is the author's build-time concern, not part of these tests —\nverify it manually in Phase 4.)\n\n## Phase 3: Configure\n\n### Set Atomic Chat host (optional)\n\nBy default, the MCP server connects to `http://host.docker.internal:1337` (Docker Desktop) with a fallback to `localhost`. To use a custom host, add to `.env`:\n\n```bash\nATOMIC_CHAT_HOST=http://your-atomic-chat-host:1337\n```\n\n### Set API key (optional)\n\nAtomic Chat does **not require authentication** when running locally — leave this unset. Only set it if you've put Atomic Chat behind a reverse proxy that enforces auth:\n\n```bash\nATOMIC_CHAT_API_KEY=sk-...\n```\n\n### Restart the service\n\nRun from your NanoClaw project root:\n\n```bash\nsource setup/lib/install-slug.sh\nlaunchctl kickstart -k gui/$(id -u)/$(launchd_label)  # macOS\n# Linux: systemctl --user restart $(systemd_unit)\n```\n\n## Phase 4: Verify\n\n### Test inference\n\nTell the user:\n\n> Send a message like: \"use atomic chat to tell me the capital of France\"\n>\n> The agent should use `atomic_chat_list_models` to find available models, then `atomic_chat_generate` to get a response.\n\n### Check logs if needed\n\n```bash\ntail -f logs/nanoclaw.log | grep -i atomic\n```\n\nLook for:\n- `[ATOMIC] Listing models...` — list request started\n- `[ATOMIC] Found N models` — models discovered\n- `[ATOMIC] >>> Generating with <model>` — generation started\n- `[ATOMIC] <<< Done: <model> | Xs | N tokens | M chars` — generation completed\n\n## Troubleshooting\n\n### Agent says \"Atomic Chat is not installed\" or tries to run a CLI\n\nThe agent is looking for a CLI that doesn't exist instead of using the MCP tools. This means:\n1. The MCP server wasn't copied — check `container/agent-runner/src/atomic-chat-mcp-stdio.ts` exists\n2. The MCP server wasn't registered — check `container/agent-runner/src/index.ts` has the `atomic_chat` entry in `mcpServers` (the allow-pattern is derived from this, so registration is the only thing to check)\n3. The container wasn't rebuilt — run `./container/build.sh`\n\n### \"Failed to connect to Atomic Chat\"\n\n1. Verify the host API is reachable: `curl http://127.0.0.1:1337/v1/models`\n2. Confirm the Local API Server is enabled in Atomic Chat's settings\n3. Check Docker can reach the host: `docker run --rm curlimages/curl curl -s http://host.docker.internal:1337/v1/models`\n4. If using a custom host, check `ATOMIC_CHAT_HOST` in `.env`\n\n### `model not found` / 404 on generate\n\nThe model ID passed to `atomic_chat_generate` must exactly match one of the IDs returned by `atomic_chat_list_models`. Ask the agent to list models first, then pick one from that list.\n\n### Slow first response\n\nAtomic Chat lazy-loads models into memory on first use. The initial call may take longer while the model warms up. Subsequent calls against the same model are fast.\n\n### Agent doesn't use Atomic Chat tools\n\nThe agent may not know about the tools. Try being explicit: \"use the atomic_chat_generate tool with llama3.2-3b-instruct to answer: ...\"\n\n### Context window or output size issues\n\nAtomic Chat respects each model's native context length. If you hit limits, pass `max_tokens` explicitly when calling `atomic_chat_generate`, or switch to a model with a larger context window in the Atomic Chat UI.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-atomic-chat-tool","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-atomic-chat-tool/SKILL.md","defaultBranch":"main"},"readme":"# Add Atomic Chat Integration\n\nThis skill adds a stdio-based MCP server that exposes models running in the local [Atomic Chat](https://github.com/AtomicBot-ai/Atomic-Chat) desktop app as tools for the container agent. Claude remains the orchestrator but can offload work to local models served by Atomic Chat on `http://127.0.0.1:1337/v1` (OpenAI-compatible).\n\nTools exposed:\n- `atomic_chat_list_models` — list models currently available in Atomic Chat (`GET /v1/models`)\n- `atomic_chat_generate` — send a prompt to a specified model and return the response (`POST /v1/chat/completions`)\n\nModel management (download, delete) is done through the **Atomic Chat desktop UI** — the app is a fork of Jan and manages its own model library.\n\nThe skill ships the MCP server source (and its test) in this folder and copies them into the agent-runner tree at install time, then registers the server in `index.ts` and forwards host env vars in `container-runner.ts`. Registering the server is enough to expose its tools — the agent's allow-pattern (`mcp__atomic_chat__*`) is derived from the registered server name.\n\n## Phase 1: Pre-flight\n\n### Check if already applied\n\nCheck if `container/agent-runner/src/atomic-chat-mcp-stdio.ts` exists. If it does, skip to Phase 3 (Configure).\n\n### Check prerequisites\n\nVerify Atomic Chat is installed and its local API server is running. On the host:\n\n```bash\ncurl -s http://127.0.0.1:1337/v1/models | head\n```\n\nIf the request fails:\n\n1. Install Atomic Chat from the [latest release](https://github.com/AtomicBot-ai/Atomic-Chat/releases) (macOS only for now — `atomic-chat.dmg`).\n2. Open the app.\n3. Open **Settings → Local API Server** and make sure it's enabled on port `1337`.\n4. Go to the **Hub** (or **Models**) tab and download at least one model (e.g. Llama 3.2 3B, Qwen 2.5 Coder 7B).\n5. Load the model once by sending any message in Atomic Chat's UI to warm it up.\n\n## Phase 2: Apply Code Changes\n\n### Copy the skill's source and tests into both trees\n\nThis skill reaches into both the container (Bun) tree and the host (Node) tree, so its\nfiles go into both, alongside the integration points they cover.\n\n```bash\nS=.claude/skills/add-atomic-chat-tool\n# Container (Bun) tree — the MCP server and the registration wiring test\ncp $S/atomic-chat-mcp-stdio.ts        container/agent-runner/src/atomic-chat-mcp-stdio.ts\ncp $S/atomic-chat-registration.test.ts container/agent-runner/src/atomic-chat-registration.test.ts\n# Host (Node) tree — the env-forwarding helper and the wiring test\ncp $S/atomic-chat-env.ts              src/atomic-chat-env.ts\ncp $S/atomic-chat-wiring.test.ts      src/atomic-chat-wiring.test.ts\n```\n\n### Register the MCP server in the agent-runner\n\nEdit `container/agent-runner/src/index.ts`. Find the `mcpServers` object that currently looks like this:\n\n```ts\n  const mcpServers: Record<string, { command: string; args: string[]; env: Record<string, string> }> = {\n    nanoclaw: {\n      command: 'bun',\n      args: ['run', mcpServerPath],\n      env: {},\n    },\n  };\n```\n\nAdd an `atomic_chat` entry alongside `nanoclaw`:\n\n```ts\n  const mcpServers: Record<string, { command: string; args: string[]; env: Record<string, string> }> = {\n    nanoclaw: {\n      command: 'bun',\n      args: ['run', mcpServerPath],\n      env: {},\n    },\n    atomic_chat: {\n      command: 'bun',\n      args: ['run', path.join(__dirname, 'atomic-chat-mcp-stdio.ts')],\n      env: {\n        ...(process.env.ATOMIC_CHAT_HOST ? { ATOMIC_CHAT_HOST: process.env.ATOMIC_CHAT_HOST } : {}),\n        ...(process.env.ATOMIC_CHAT_API_KEY ? { ATOMIC_CHAT_API_KEY: process.env.ATOMIC_CHAT_API_KEY } : {}),\n      },\n    },\n  };\n```\n\n`atomic-chat-registration.test.ts` asserts this entry is present and points at the server module — the tool only appears to the agent if it is registered here.\n\n### Forward host env vars into the container\n\nThe env-forwarding logic lives in the copied `src/atomic-chat-env.ts` (`atomicChatEnv()`), so the reach-in into `composeSessionSpec` is a single spr","createdAt":"2026-09-25T10:52:06.502Z","updatedAt":"2026-09-25T10:52:06.502Z"},{"id":"cmugucu7u00iqqu06tnvev4rv","slug":"nanocoai-nanoclaw-add-clidash","name":"add-clidash","description":"Add clidash — a zero-dependency, read-only web dashboard that derives its tabs and tables at runtime from any CLI that lists resources as JSON. Ships pre-wired for NanoClaw's ncl CLI (agent groups, sessions, channels, users, roles), plus message-activity charts, a log tail, and a read-only file viewer for group skills/CLAUDE.md/profiles.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-clidash","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add clidash — a zero-dependency, read-only web dashboard that derives its tabs and tables at runtime from any CLI that lists resources as JSON. Ships pre-wired for NanoClaw's ncl CLI (agent groups, sessions, channels, users, roles), plus message-activity charts, a log tail, and a read-only file viewer for group skills/CLAUDE.md/profiles.","permissions":[],"systemPrompt":"# /add-clidash — CLI-derived read-only dashboard\n\nclidash is a small, read-only web dashboard. You point it at any CLI that can\nlist resources as JSON (NanoClaw's `ncl`, `docker`, `kubectl`, …) and it builds\nthe dashboard at runtime: one tab per resource, a generic table over whatever\ncolumns the rows have. A new `ncl` resource becomes a new tab and a new column\nbecomes a new table column with **zero code changes**.\n\nIt ships pre-wired for NanoClaw's `ncl` CLI and adds three NanoClaw-aware\npanels driven entirely by config:\n\n- **Agents overview** — status cards joining groups + sessions + messaging\n  groups + wirings (green <15m / amber <2h / red older).\n- **Activity** — per-session inbound/outbound message totals and a daily series,\n  read directly from the session DBs (`ncl` has no messages resource).\n- **Logs** — last N lines of allowlisted host log files.\n- **Files** — a read-only viewer for group skills, `CLAUDE.md`, and profiles.\n\n## Why it's safe\n\nclidash is **read-only by construction**: the server can only `execFile` the\nargv templates in its config. `{resource}` is the sole substitution and is\nallowlist-validated against the discovered/static resource set before exec —\nnever a shell, no free-form input reaches argv. There is no auth; **the network\nis the auth boundary** — it binds `127.0.0.1` by default. Only ever bind a\nprivate interface (e.g. a tailnet IP), never a public one.\n\nIt's distinct from `/add-dashboard` (which pushes JSON snapshots to a separate\n`@nanoco/nanoclaw-dashboard` npm package): clidash has **zero dependencies**, no\nbuild step, no push pipeline, and no edits to NanoClaw source — it just reads\n`ncl` and the session DBs.\n\n## Steps\n\n### 1. Copy the tool into place\n\nclidash is fully self-contained — copy the whole directory in:\n\n`tools/` is not a standard NanoClaw directory and `cp -R` won't create it, so\nmake it first:\n\n```bash\nmkdir -p tools\ncp -R .claude/skills/add-clidash/add/tools/clidash tools/clidash\n```\n\nThat is the only file change this skill makes. Nothing in NanoClaw `src/` is\ntouched, no dependency is added.\n\n### 2. Create the config\n\nThe example config is pre-wired for NanoClaw with paths relative to the repo\nroot, so it works as-is when you run clidash from `tools/clidash/`:\n\n```bash\ncd tools/clidash\ncp clidash.config.example.json clidash.config.json\n```\n\n`clidash.config.json` is your local config — add it to `.gitignore` if you\ndon't want to commit install-specific paths:\n\n```bash\necho 'tools/clidash/clidash.config.json' >> ../../.gitignore\n```\n\nThe example assumes `ncl` is built at `bin/ncl`. If `bin/ncl` doesn't exist,\nbuild it first (`pnpm run build`) or point `clis.ncl.bin` at the right path.\n\n### 3. Test\n\nTests use a stub CLI — no real `ncl` or `docker` needed:\n\n```bash\nnpm test\n```\n\nAll tests should pass (Node ≥ 22.5, `node:test`, zero dependencies).\n\n### 4. Run and verify\n\n```bash\nnode server.js          # serves http://127.0.0.1:4690\n```\n\nIn another shell, confirm it's live and that `ncl` discovery worked:\n\n```bash\ncurl -s http://127.0.0.1:4690/api/clis | head -c 400      # CLIs + discovered resources\ncurl -s http://127.0.0.1:4690/api/r/ncl/groups | head -c 400   # a real resource table\n```\n\nThen open `http://127.0.0.1:4690/` in a browser. You should see the Agents\noverview plus a tab per `ncl` resource.\n\n### 5. (Optional) Run as a service\n\nclidash binds `127.0.0.1` by default. To reach it from other devices, bind a\nprivate (e.g. tailnet) IP via the `BIND` env var or `bind` in config — never a\npublic interface.\n\n```ini\n# ~/.config/systemd/user/clidash.service   (Linux)\n[Unit]\nDescription=clidash read-only CLI dashboard\n\n[Service]\nWorkingDirectory=%h/nanoclaw/tools/clidash\nExecStart=/usr/bin/node %h/nanoclaw/tools/clidash/server.js\nEnvironment=BIND=127.0.0.1\nRestart=on-failure\n\n[Install]\nWantedBy=default.target\n```\n\n```bash\nsystemctl --user enable --now clidash\n```\n\nOn macOS, wrap `node server.js` (with `WorkingDirectory` = `tools/clidash`) in a\nlaunchd plist the same way the main NanoClaw service is configured.\n\n## Configuration reference\n\n`clidash.config.json` keys (see `tools/clidash/README.md` and\n`clidash.config.example.json` for the full shape):\n\n| Key | Purpose |\n|-----|---------|\n| `port`, `bind`, `refreshSeconds` | server bind + UI auto-refresh cadence |\n| `clis.<name>.bin` / `cwd` / `env` | how to invoke the CLI (`bin` is relative to `cwd`) |\n| `clis.<name>.discover` or `resources` | runtime discovery (`ncl help`) vs a static resource list |\n| `clis.<name>.list` | argv template; `{resource}` is the only substitution |\n| `clis.<name>.output` | `json` or `jsonlines` (docker/kubectl style) |\n| `clis.<name>.unwrap` | dot-path into a response envelope (e.g. `data`) |\n| `clis.<name>.enrich`/`badges`/`summary` | table decorations (ID→name joins, status colors, summary cards) |\n| `activity` | `sessionsRoot` + `days` for the message-activity charts |\n| `logs` | `dir`, `tailLines`, and an allowlist of `files` to tail |\n| `docs` | file viewer: `root`, a `deny` glob list, and `collections` of glob patterns |\n\nAdding a second CLI is config-only — e.g. `docker` is included as a `jsonlines`\nexample. View plugins (`views/<cli>-<view>.js`) are the only per-CLI code and\nare optional.\n\n## Troubleshooting\n\n- **`ENOENT` / config not found** — run from `tools/clidash/` and make sure you\n  copied `clidash.config.example.json` to `clidash.config.json` (step 2), or set\n  `CLIDASH_CONFIG=/abs/path.json`.\n- **No `ncl` resources / discovery empty** — `bin/ncl` isn't built or the path\n  is wrong. Build it (`pnpm run build`) or fix `clis.ncl.bin`.\n- **docker tab errors** — the docker daemon isn't running, or remove the\n  `docker` CLI from config if you don't need it.\n- **Can't reach it from another device** — it binds `127.0.0.1`; set\n  `BIND=<private-ip>` (tailnet), never a public interface.\n- **Empty Activity/Logs/Files** — check that `activity.sessionsRoot`,\n  `logs.dir`, and `docs.root` resolve to your NanoClaw root (relative to where\n  you launch `node server.js`).\n\n## Removal\n\nSee [REMOVE.md](REMOVE.md).","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-clidash","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-clidash/SKILL.md","defaultBranch":"main"},"readme":"# /add-clidash — CLI-derived read-only dashboard\n\nclidash is a small, read-only web dashboard. You point it at any CLI that can\nlist resources as JSON (NanoClaw's `ncl`, `docker`, `kubectl`, …) and it builds\nthe dashboard at runtime: one tab per resource, a generic table over whatever\ncolumns the rows have. A new `ncl` resource becomes a new tab and a new column\nbecomes a new table column with **zero code changes**.\n\nIt ships pre-wired for NanoClaw's `ncl` CLI and adds three NanoClaw-aware\npanels driven entirely by config:\n\n- **Agents overview** — status cards joining groups + sessions + messaging\n  groups + wirings (green <15m / amber <2h / red older).\n- **Activity** — per-session inbound/outbound message totals and a daily series,\n  read directly from the session DBs (`ncl` has no messages resource).\n- **Logs** — last N lines of allowlisted host log files.\n- **Files** — a read-only viewer for group skills, `CLAUDE.md`, and profiles.\n\n## Why it's safe\n\nclidash is **read-only by construction**: the server can only `execFile` the\nargv templates in its config. `{resource}` is the sole substitution and is\nallowlist-validated against the discovered/static resource set before exec —\nnever a shell, no free-form input reaches argv. There is no auth; **the network\nis the auth boundary** — it binds `127.0.0.1` by default. Only ever bind a\nprivate interface (e.g. a tailnet IP), never a public one.\n\nIt's distinct from `/add-dashboard` (which pushes JSON snapshots to a separate\n`@nanoco/nanoclaw-dashboard` npm package): clidash has **zero dependencies**, no\nbuild step, no push pipeline, and no edits to NanoClaw source — it just reads\n`ncl` and the session DBs.\n\n## Steps\n\n### 1. Copy the tool into place\n\nclidash is fully self-contained — copy the whole directory in:\n\n`tools/` is not a standard NanoClaw directory and `cp -R` won't create it, so\nmake it first:\n\n```bash\nmkdir -p tools\ncp -R .claude/skills/add-clidash/add/tools/clidash tools/clidash\n```\n\nThat is the only file change this skill makes. Nothing in NanoClaw `src/` is\ntouched, no dependency is added.\n\n### 2. Create the config\n\nThe example config is pre-wired for NanoClaw with paths relative to the repo\nroot, so it works as-is when you run clidash from `tools/clidash/`:\n\n```bash\ncd tools/clidash\ncp clidash.config.example.json clidash.config.json\n```\n\n`clidash.config.json` is your local config — add it to `.gitignore` if you\ndon't want to commit install-specific paths:\n\n```bash\necho 'tools/clidash/clidash.config.json' >> ../../.gitignore\n```\n\nThe example assumes `ncl` is built at `bin/ncl`. If `bin/ncl` doesn't exist,\nbuild it first (`pnpm run build`) or point `clis.ncl.bin` at the right path.\n\n### 3. Test\n\nTests use a stub CLI — no real `ncl` or `docker` needed:\n\n```bash\nnpm test\n```\n\nAll tests should pass (Node ≥ 22.5, `node:test`, zero dependencies).\n\n### 4. Run and verify\n\n```bash\nnode server.js          # serves http://127.0.0.1:4690\n```\n\nIn another shell, confirm it's live and that `ncl` discovery worked:\n\n```bash\ncurl -s http://127.0.0.1:4690/api/clis | head -c 400      # CLIs + discovered resources\ncurl -s http://127.0.0.1:4690/api/r/ncl/groups | head -c 400   # a real resource table\n```\n\nThen open `http://127.0.0.1:4690/` in a browser. You should see the Agents\noverview plus a tab per `ncl` resource.\n\n### 5. (Optional) Run as a service\n\nclidash binds `127.0.0.1` by default. To reach it from other devices, bind a\nprivate (e.g. tailnet) IP via the `BIND` env var or `bind` in config — never a\npublic interface.\n\n```ini\n# ~/.config/systemd/user/clidash.service   (Linux)\n[Unit]\nDescription=clidash read-only CLI dashboard\n\n[Service]\nWorkingDirectory=%h/nanoclaw/tools/clidash\nExecStart=/usr/bin/node %h/nanoclaw/tools/clidash/server.js\nEnvironment=BIND=127.0.0.1\nRestart=on-failure\n\n[Install]\nWantedBy=default.target\n```\n\n```bash\nsystemctl --user enable --now clidash\n```\n\nOn macOS, wrap `node server.js` (with `WorkingDirectory` = `tools/clidash`) in a\nlaunchd plist the same way the mai","createdAt":"2026-09-25T10:52:06.522Z","updatedAt":"2026-09-25T10:52:06.522Z"},{"id":"cmugucu8800itqu06m2pk59ew","slug":"nanocoai-nanoclaw-add-codex","name":"add-codex","description":"Use Codex (OpenAI's codex app-server) as a full agent provider — planning, tool orchestration, MCP tools, server-side history, session resume — alongside or instead of Claude. ChatGPT subscription or OpenAI API key, vault-only via the selected gateway. Per-group via `ncl groups config update --provider codex`. Distinct from using OpenAI as an MCP tool (where Claude remains the planner).","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-codex","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Use Codex (OpenAI's codex app-server) as a full agent provider — planning, tool orchestration, MCP tools, server-side history, session resume — alongside or instead of Claude. ChatGPT subscription or OpenAI API key, vault-only via the selected gateway. Per-group via `ncl groups config update --provider codex`. Distinct from using OpenAI as an MCP tool (where Claude remains the planner).","permissions":[],"systemPrompt":"# Codex agent provider\n\n> Shortcut: `pnpm exec tsx setup/index.ts --step provider-auth codex` performs this whole install (manifest-driven from the providers branch: files, barrels, CLI manifest entry, image rebuild) plus auth in one command. The steps below are the same operations, for agent-driven or manual application.\n\nNanoClaw selects each group's agent backend from `container_configs.provider` (default `claude`). This skill installs the Codex provider: copy the payload from the `providers` branch, append one import to each of the three provider barrels, add the pinned Codex CLI to the container manifest (`container/cli-tools.json`), rebuild, then run the vault auth walk-through.\n\nThe provider runs `codex app-server` as a child process speaking JSON-RPC over stdio: native streaming, MCP tools, server-side conversation history (the continuation is a thread id, no on-disk transcript). Credentials are **vault-only**: The selected gateway serves a sentinel `auth.json` stub into the container and swaps the real ChatGPT token or API key on the wire — no key in `.env`, nothing readable in the container.\n\nThe mechanical steps under **Install** carry `nc:` directive fences: an agent reads the prose and applies them, and a parser can apply them deterministically from the same document. Every directive is idempotent, so the whole skill is safe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Install\n\n### Pre-flight\n\nRequires `src/project-doc-compose.ts` on trunk. If it is missing, stop and tell\nthe operator to run `/update-nanoclaw` first.\n\nCheck whether the payload is already wired (a prior apply, or a trunk that still carries it). All of these present means installed — skip to **Authenticate**:\n\n- `src/providers/codex.ts` and `src/providers/codex-agents-md.ts`\n- `container/agent-runner/src/providers/codex.ts` and `codex-app-server.ts`\n- `setup/providers/codex.ts` and both `provider-contracts/codex.ts` declarations (host and container)\n- `import './codex.js';` in the three provider barrels and both contract barrels\n- an `@openai/codex` entry in `container/cli-tools.json`\n\n### 1. Fetch and copy the payload\n\nFetch the `providers` branch and copy the Codex payload into all three trees (additive — overwrite each file, never merge the branch). The host files are the provider contribution + the AGENTS.md spec (composition itself lives in trunk's `src/project-doc-compose.ts`) + their guards; the container files are the provider runtime (turn loop, JSON-RPC wrapper, native memory SessionStart hook, per-exchange archiver) + their guards; the setup file is the picker entry + vault auth walk-through; `container/AGENTS.md` is the runtime-contract base the composed AGENTS.md embeds.\n\n```nc:copy from-branch:providers\nsrc/providers/codex.ts\nsrc/providers/codex-agents-md.ts\nsrc/providers/codex-registration.test.ts\nsrc/providers/codex-host-contribution.test.ts\nsrc/providers/codex-agents-md.test.ts\ncontainer/agent-runner/src/providers/codex.ts\ncontainer/agent-runner/src/providers/codex-app-server.ts\ncontainer/agent-runner/src/providers/exchange-archive.ts\ncontainer/agent-runner/src/providers/exchange-archive.test.ts\ncontainer/agent-runner/src/providers/codex-registration.test.ts\ncontainer/agent-runner/src/providers/codex.factory.test.ts\ncontainer/agent-runner/src/providers/codex.turns.test.ts\ncontainer/agent-runner/src/providers/codex-app-server.test.ts\ncontainer/agent-runner/src/providers/codex-contract-parity.test.ts\ncontainer/agent-runner/src/providers/codex.conformance.test.ts\ncontainer/agent-runner/src/providers/codex-cli-tools.test.ts\ncontainer/agent-runner/src/provider-contracts/codex.ts\nsetup/providers/codex-registration.test.ts\ncontainer/AGENTS.md\n```\n\n### Use the selected gateway for authentication\n\nInstall the bundled Codex authentication hook alongside the registry payload. This\nkeeps the same login choices while delegating custody to the selected gateway,\nand preserves the hook when a provider refresh copies registry files again.\nThese two files are omitted from the registry copy so refresh stays idempotent.\nThe setup screens and step sequence do not change.\n\n```nc:copy\npayload/src/provider-contracts/codex.ts -> src/provider-contracts/codex.ts\npayload/setup/providers/codex.ts -> setup/providers/codex.ts\npayload/setup/providers/codex.test.ts -> setup/providers/codex.test.ts\n```\n\n### 2. Wire the barrels\n\nAppend the self-registration import to each provider and contract barrel (skipped if already present).\n\n```nc:append to:src/providers/index.ts\nimport './codex.js';\n```\n\n```nc:append to:src/provider-contracts/index.ts\nimport './codex.js';\n```\n\n```nc:append to:container/agent-runner/src/provider-contracts/index.ts\nimport './codex.js';\n```\n\n```nc:append to:container/agent-runner/src/providers/index.ts\nimport './codex.js';\n```\n\n```nc:append to:setup/providers/index.ts\nimport './codex.js';\n```\n\n### 3. CLI manifest\n\nThe agent's global Node CLIs install from `container/cli-tools.json` (a json-merge seam), not hand-edited Dockerfile layers. Add Codex by appending one entry — idempotent on `name`, so a re-run is a no-op. `@openai/codex` has no native postinstall, so no `onlyBuilt`. The Dockerfile already installs every manifest entry via pinned `pnpm install -g`; no Dockerfile edit is needed.\n\n```nc:json-merge into:container/cli-tools.json key:name\n{ \"name\": \"@openai/codex\", \"version\": \"0.155.1\" }\n```\n\nThe version (`0.155.1`) is the canonical pin — this SKILL.md is the source of truth.\n\n### 4. Build\n\n```nc:run effect:build\npnpm run build\npnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit\n./container/build.sh\n```\n\n### 5. Validate\n\n```nc:run effect:test\npnpm exec tsx scripts/provider-contract-verifier.ts --required-declared codex\n```\n\nThe registration tests import only the real barrels — they go red if a barrel line is missing, a barrel fails to evaluate, or the payload is broken.\n\n## Authenticate\n\n```nc:run effect:external\npnpm exec tsx setup/index.ts --step provider-auth codex\n```\n\nThe same walk-through fresh installs get from the setup picker: ChatGPT subscription (browser login or device pairing) or an OpenAI API key, landed in the selected gateway’s vault. Idempotent — it short-circuits when a matching secret already exists. It finishes with the install check.\n\n## Use it\n\nPer group:\n\n```bash\nncl groups config update --id <group-id> --provider codex\nncl groups restart --id <group-id>\n```\n\nSwitching is an operator action — run it from the host. Every provider uses the\nsame `memory/` tree, so memory carries across automatically. Run\n`/migrate-memory` only when upgrading a group that still has legacy `.seed.md`,\n`CLAUDE.local.md`, or unindexed imported memory. See\n[docs/provider-migration.md](../../docs/provider-migration.md).\n\n### Default new groups to codex (optional)\n\nNew groups are created on the **instance default** (`DEFAULT_AGENT_PROVIDER` in `.env`, or `claude` when unset). Installing this skill wires codex in but does NOT change that default — \"installed\" is not \"authenticated\", so the default stays claude until you opt in explicitly.\n\nAfter install, ask the operator before flipping it:\n\n> \"Codex is installed. Default new agent groups to codex? Existing groups keep their current provider.\"\n\nOn yes — set it, then restart the host so it takes effect:\n\n```bash\npnpm exec tsx setup/index.ts --step set-env -- --key DEFAULT_AGENT_PROVIDER --value codex\nlaunchctl kickstart -k gui/$(id -u)/com.nanoclaw   # macOS; Linux: systemctl --user restart nanoclaw\n```\n\nThis affects only groups created afterward. Per-group `ncl groups config update --provider` still overrides the default in either direction. Creation itself stays provider-agnostic (no `--provider` flag — provider is a DB property stamped from the instance default at creation).\n\n## Troubleshooting\n\n- **Container dies at boot, channel silent:** `grep 'Container exited non-zero' logs/nanoclaw.error.log` — the `stderrTail` carries the reason (e.g. `Unknown provider: codex. Registered: claude` means the barrels aren't wired in the running build).\n- **In-channel `Error: spawn codex ENOENT` on every message:** the image predates the manifest entry — re-run `./container/build.sh`.\n- **Auth errors mid-conversation:** the vault secret is missing or stale — re-run `pnpm exec tsx setup/index.ts --step provider-auth codex` (subscription re-login updates the vault copy).","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-codex","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-codex/SKILL.md","defaultBranch":"main"},"readme":"# Codex agent provider\n\n> Shortcut: `pnpm exec tsx setup/index.ts --step provider-auth codex` performs this whole install (manifest-driven from the providers branch: files, barrels, CLI manifest entry, image rebuild) plus auth in one command. The steps below are the same operations, for agent-driven or manual application.\n\nNanoClaw selects each group's agent backend from `container_configs.provider` (default `claude`). This skill installs the Codex provider: copy the payload from the `providers` branch, append one import to each of the three provider barrels, add the pinned Codex CLI to the container manifest (`container/cli-tools.json`), rebuild, then run the vault auth walk-through.\n\nThe provider runs `codex app-server` as a child process speaking JSON-RPC over stdio: native streaming, MCP tools, server-side conversation history (the continuation is a thread id, no on-disk transcript). Credentials are **vault-only**: The selected gateway serves a sentinel `auth.json` stub into the container and swaps the real ChatGPT token or API key on the wire — no key in `.env`, nothing readable in the container.\n\nThe mechanical steps under **Install** carry `nc:` directive fences: an agent reads the prose and applies them, and a parser can apply them deterministically from the same document. Every directive is idempotent, so the whole skill is safe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Install\n\n### Pre-flight\n\nRequires `src/project-doc-compose.ts` on trunk. If it is missing, stop and tell\nthe operator to run `/update-nanoclaw` first.\n\nCheck whether the payload is already wired (a prior apply, or a trunk that still carries it). All of these present means installed — skip to **Authenticate**:\n\n- `src/providers/codex.ts` and `src/providers/codex-agents-md.ts`\n- `container/agent-runner/src/providers/codex.ts` and `codex-app-server.ts`\n- `setup/providers/codex.ts` and both `provider-contracts/codex.ts` declarations (host and container)\n- `import './codex.js';` in the three provider barrels and both contract barrels\n- an `@openai/codex` entry in `container/cli-tools.json`\n\n### 1. Fetch and copy the payload\n\nFetch the `providers` branch and copy the Codex payload into all three trees (additive — overwrite each file, never merge the branch). The host files are the provider contribution + the AGENTS.md spec (composition itself lives in trunk's `src/project-doc-compose.ts`) + their guards; the container files are the provider runtime (turn loop, JSON-RPC wrapper, native memory SessionStart hook, per-exchange archiver) + their guards; the setup file is the picker entry + vault auth walk-through; `container/AGENTS.md` is the runtime-contract base the composed AGENTS.md embeds.\n\n```nc:copy from-branch:providers\nsrc/providers/codex.ts\nsrc/providers/codex-agents-md.ts\nsrc/providers/codex-registration.test.ts\nsrc/providers/codex-host-contribution.test.ts\nsrc/providers/codex-agents-md.test.ts\ncontainer/agent-runner/src/providers/codex.ts\ncontainer/agent-runner/src/providers/codex-app-server.ts\ncontainer/agent-runner/src/providers/exchange-archive.ts\ncontainer/agent-runner/src/providers/exchange-archive.test.ts\ncontainer/agent-runner/src/providers/codex-registration.test.ts\ncontainer/agent-runner/src/providers/codex.factory.test.ts\ncontainer/agent-runner/src/providers/codex.turns.test.ts\ncontainer/agent-runner/src/providers/codex-app-server.test.ts\ncontainer/agent-runner/src/providers/codex-contract-parity.test.ts\ncontainer/agent-runner/src/providers/codex.conformance.test.ts\ncontainer/agent-runner/src/providers/codex-cli-tools.test.ts\ncontainer/agent-runner/src/provider-contracts/codex.ts\nsetup/providers/codex-registration.test.ts\ncontainer/AGENTS.md\n```\n\n### Use the selected gateway for authentication\n\nInstall the bundled Codex authentication hook alongside the registry payload. This\nkeeps the same login choices while delegating custody to the selected gateway,\nand preserves the hook when a provider refresh copi","createdAt":"2026-09-25T10:52:06.537Z","updatedAt":"2026-09-25T10:52:06.537Z"},{"id":"cmugucu8x00izqu06e6y1pena","slug":"nanocoai-nanoclaw-add-deltachat","name":"add-deltachat","description":"Add DeltaChat channel integration via @deltachat/stdio-rpc-server. Native adapter — no Chat SDK bridge. Email-based messaging with end-to-end encryption.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-deltachat","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add DeltaChat channel integration via @deltachat/stdio-rpc-server. Native adapter — no Chat SDK bridge. Email-based messaging with end-to-end encryption.","permissions":[],"systemPrompt":"# Add DeltaChat Channel\n\nThe adapter drives the `@deltachat/stdio-rpc-server` JSON-RPC subprocess directly — pure Node.js against the DeltaChat core library. Messages are delivered over email with Autocrypt/OpenPGP encryption.\n\n## Install\n\n### 1. Copy the adapter and its registration test\n\nFetch the `channels` branch from the configured remote that carries it, then\noverwrite the skill-owned files with the canonical registry copies:\n\n```nc:copy from-branch:channels\nsrc/channels/deltachat.ts\nsrc/channels/deltachat-registration.test.ts\n```\n\n### 2. Append the self-registration import\n\nAppend to `src/channels/index.ts` (skip if already present):\n\n```nc:append to:src/channels/index.ts\nimport './deltachat.js';\n```\n\n### 3. Install the adapter package (pinned)\n\n```nc:dep\n@deltachat/stdio-rpc-server@2.49.0\n```\n\n### 4. Build and validate\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/deltachat-registration.test.ts\n```\n\nBoth must be clean before proceeding. `deltachat-registration.test.ts` is the one integration test: it imports the real channel barrel and asserts the registry contains `deltachat`. It goes red if the `import './deltachat.js';` line is deleted or drifts, if the barrel fails to evaluate (so the channel genuinely would not register), or if `@deltachat/stdio-rpc-server` isn't installed (the import throws) — so it also implicitly verifies the dependency from step 4. Importing is safe: deltachat instantiates the rpc client only in `setup()` (at host startup), never at import.\n\nEnd-to-end message delivery against a real email account is verified manually once the service is running — see Wiring and Troubleshooting.\n\n## Account Setup\n\nA dedicated email account is strongly recommended — it will accumulate DeltaChat-formatted messages and store encryption keys. Not all providers work well with DeltaChat; check https://providers.delta.chat/ before picking one.\n\n**Default security modes:** IMAP uses SSL/TLS (port 993), SMTP uses STARTTLS (port 587). Both are configurable via `.env` — see Credentials below.\n\nTo find the correct hostnames for a domain:\n\n```bash\nnode -e \"require('dns').resolveMx('example.com', (e,r) => console.log(r))\"\n```\n\nMost providers publish their IMAP/SMTP hostnames in their help docs under \"manual setup\" or \"IMAP access.\"\n\n## Credentials\n\nAdd to `.env`:\n\n```bash\nDC_EMAIL=bot@example.com\nDC_PASSWORD=your-app-password\nDC_IMAP_HOST=imap.example.com\nDC_IMAP_PORT=993\nDC_IMAP_SECURITY=1        # 1=SSL/TLS (default), 2=STARTTLS, 3=plain\nDC_SMTP_HOST=smtp.example.com\nDC_SMTP_PORT=587\nDC_SMTP_SECURITY=2        # 2=STARTTLS (default), 1=SSL/TLS, 3=plain\n```\n\nSecurity settings are applied on every startup, so changing them in `.env` and restarting takes effect without wiping the account.\n\n\n### Optional settings\n\nThe following are read from the process environment (not `.env`). To override them, add `Environment=` lines to the systemd service unit or your launchd plist:\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `DC_ACCOUNT_DIR` | `dc-account` | Directory for DeltaChat account data (IMAP state, keys, blobs) |\n| `DC_DISPLAY_NAME` | `NanoClaw` | Bot display name shown in DeltaChat |\n| `DC_AVATAR_PATH` | _(none)_ | Absolute path to avatar image; set at startup only |\n\nThe `/set-avatar` command (send an image with that caption) is the easiest way to set the avatar at runtime without modifying the service file. Only users with `owner` or global `admin` role can use it.\n\n### Restart\n\nRun from your NanoClaw project root:\n\n```bash\nsource setup/lib/install-slug.sh\n\n# Linux\nsystemctl --user restart $(systemd_unit)\n\n# macOS\nlaunchctl kickstart -k gui/$(id -u)/$(launchd_label)\n```\n\nOn first start the adapter configures the email account (IMAP/SMTP credentials, calls `configure()`). Subsequent starts skip straight to `startIo()`. Account data is stored in `dc-account/` in the project root (or your `DC_ACCOUNT_DIR`).\n\n## Wiring\n\n### DMs\n\n**DeltaChat contacts cannot be added by email alone** — to start a chat, the user must open the bot's invite link in their DeltaChat app or scan its QR code. This triggers the SecureJoin handshake.\n\n#### Step 1 — Get the invite link\n\nAfter the service starts, the adapter logs the invite URL and writes a QR SVG:\n\n```bash\ngrep \"invite link\" logs/nanoclaw.log | tail -1\n# url field contains the https://i.delta.chat/... invite link\n# also written to dc-account/invite-qr.svg (or $DC_ACCOUNT_DIR/invite-qr.svg)\n```\n\nThe invite URL is stable (tied to the bot's email and encryption keys) so it stays valid across restarts.\n\n#### Step 2 — Add the bot in DeltaChat\n\nTwo options for the user to connect:\n\n- **Link**: Copy the `https://i.delta.chat/...` URL and open it on the device running DeltaChat. The app recognises it and shows a \"Start chat\" prompt.\n- **QR code**: Open `dc-account/invite-qr.svg` in a browser or image viewer, display it on screen, and scan it from the DeltaChat app using the QR-scan button on the new-chat screen.\n\nAfter accepting, DeltaChat exchanges keys and creates the chat automatically.\n\n#### Step 3 — Wire the chat to an agent\n\nOnce the first message arrives the router auto-creates a `messaging_groups` row. Look up the chat ID:\n\n```bash\npnpm exec tsx scripts/q.ts data/v2.db \\\n  \"SELECT platform_id, name FROM messaging_groups WHERE channel_type='deltachat' AND is_group=0 ORDER BY created_at DESC LIMIT 5\"\n```\n\nThen run `/init-first-agent` — it creates the agent group, grants the user owner access, and wires the messaging group in one step:\n\n```bash\npnpm exec tsx scripts/init-first-agent.ts \\\n  --channel deltachat \\\n  --user-id deltachat:user@example.com \\\n  --platform-id <platform_id from above> \\\n  --display-name \"Your Name\"\n```\n\n### Groups\n\nAdd the bot email to a DeltaChat group. When any member sends a message, the router creates a `messaging_groups` row with `is_group = 1`. Run `/manage-channels` to wire it to an agent group, or wire it directly with `ncl` — **the host service must be running** (`ncl` connects to it over a Unix socket):\n\n```bash\n# Engage mode/pattern default to the DeltaChat adapter's declared channel\n# defaults — for DeltaChat groups that's a name pattern (the platform has no\n# mention metadata), so the agent responds when addressed by name.\nncl wirings create --messaging-group-id <mg-id> --agent-group-id <ag-id>\n```\n\n## Next Steps\n\nIf you're in the middle of `/setup`, return to the setup flow now.\n\nOtherwise, run `/init-first-agent` to create an agent and wire it to your DeltaChat DM (see Wiring above), or `/manage-channels` to wire this channel to an existing agent group.\n\n## Channel Info\n\n- **type**: `deltachat`\n- **terminology**: DeltaChat calls them \"chats\" (1:1 DMs) and \"groups\"\n- **supports-threads**: no — DeltaChat has no thread model\n- **platform-id-format**: numeric chat ID as a string (e.g. `\"12\"`) — the DeltaChat core's internal chat identifier\n- **user-id-format**: `deltachat:{email}` — the contact's email address\n- **how-to-find-id**: Send a message from DeltaChat to the bot email, then query `messaging_groups` as shown above\n- **typical-use**: Personal assistant over DeltaChat DMs; small groups where participants use DeltaChat\n- **default-isolation**: One agent per bot identity. Multiple chats with the same operator can share an agent group; groups with other people should typically get their own agent group (the default `shared` session mode already gives each messaging group its own session)\n\n### Features\n\n- File attachments — inbound and outbound; inbound waits up to 30 seconds for large-message download to complete\n- Invite link logged on every startup — URL + QR SVG written to `dc-account/invite-qr.svg`; see Wiring for the bootstrap flow\n- `/set-avatar` — send an image with this caption to change the bot's DeltaChat avatar (admin/owner only)\n- Connectivity watchdog — restarts IO if IMAP goes quiet for 20 minutes or connectivity drops below threshold for two consecutive 5-minute checks\n- Network nudge — `maybeNetwork()` called every 10 minutes to recover from prolonged idle\n\nNot supported: DeltaChat reactions, message editing/deletion, read receipts.\n\n### Connectivity model\n\n`isConnected()` returns `true` when the internal connectivity value is ≥ 3000:\n\n| Range | Meaning |\n|-------|---------|\n| 1000–1999 | Not connected |\n| 2000–2999 | Connecting |\n| 3000–3999 | Working (IMAP fetching) |\n| ≥ 4000 | Fully connected (IMAP IDLE) |\n\n## Troubleshooting\n\n### Adapter not starting — credentials missing\n\n```bash\ngrep \"Channel credentials missing\" logs/nanoclaw.log | grep deltachat\n```\n\nAll six required vars (`DC_EMAIL`, `DC_PASSWORD`, `DC_IMAP_HOST`, `DC_IMAP_PORT`, `DC_SMTP_HOST`, `DC_SMTP_PORT`) must be present in `.env`.\n\n### Account configure fails\n\n```bash\ngrep \"DeltaChat\" logs/nanoclaw.log | tail -20\n```\n\nCommon causes:\n- Wrong IMAP/SMTP hostnames — double-check provider docs\n- App password not generated — Gmail and some others require this when 2FA is enabled\n- Port/security mismatch — defaults are port 993 + SSL/TLS for IMAP and port 587 + STARTTLS for SMTP; override with `DC_IMAP_PORT`/`DC_IMAP_SECURITY` or `DC_SMTP_PORT`/`DC_SMTP_SECURITY` in `.env`\n\n### Provider uses SMTP port 465 (SSL/TLS) instead of 587\n\nSet `DC_SMTP_SECURITY=1` and `DC_SMTP_PORT=465` in `.env`, then restart.\n\n### Messages not arriving\n\n1. Check the service is running and the adapter started: `grep \"Channel adapter started.*deltachat\" logs/nanoclaw.log`\n2. Check connectivity: `grep \"DeltaChat: IO started\" logs/nanoclaw.log`\n3. Check the sender has been granted access — run `/init-first-agent` to create their user record and wire the chat\n4. Verify the messaging group is wired: `pnpm exec tsx scripts/q.ts data/v2.db \"SELECT mg.platform_id, mga.agent_group_id FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id = mga.messaging_group_id WHERE mg.channel_type='deltachat'\"`\n\n### Stale lock file after crash\n\n```bash\nrm -f dc-account/accounts.lock\nsystemctl --user restart \"$(. setup/lib/install-slug.sh && systemd_unit)\"\n```\n\n### Bot not responding after restart\n\nThe account is already configured — IO restarts automatically on service start. If the RPC subprocess is stuck, restart the service. Check for errors:\n\n```bash\ngrep \"DeltaChat\" logs/nanoclaw.error.log | tail -20\n```\n\n### Messages received but agent not responding\n\nThe messaging group exists but may not be wired to an agent group. Run:\n\n```bash\npnpm exec tsx scripts/q.ts data/v2.db \"SELECT id, platform_id, name FROM messaging_groups WHERE channel_type='deltachat'\"\n```\n\nIf the group has no entry in `messaging_group_agents`, wire it with `/manage-channels`.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-deltachat","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-deltachat/SKILL.md","defaultBranch":"main"},"readme":"# Add DeltaChat Channel\n\nThe adapter drives the `@deltachat/stdio-rpc-server` JSON-RPC subprocess directly — pure Node.js against the DeltaChat core library. Messages are delivered over email with Autocrypt/OpenPGP encryption.\n\n## Install\n\n### 1. Copy the adapter and its registration test\n\nFetch the `channels` branch from the configured remote that carries it, then\noverwrite the skill-owned files with the canonical registry copies:\n\n```nc:copy from-branch:channels\nsrc/channels/deltachat.ts\nsrc/channels/deltachat-registration.test.ts\n```\n\n### 2. Append the self-registration import\n\nAppend to `src/channels/index.ts` (skip if already present):\n\n```nc:append to:src/channels/index.ts\nimport './deltachat.js';\n```\n\n### 3. Install the adapter package (pinned)\n\n```nc:dep\n@deltachat/stdio-rpc-server@2.49.0\n```\n\n### 4. Build and validate\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/deltachat-registration.test.ts\n```\n\nBoth must be clean before proceeding. `deltachat-registration.test.ts` is the one integration test: it imports the real channel barrel and asserts the registry contains `deltachat`. It goes red if the `import './deltachat.js';` line is deleted or drifts, if the barrel fails to evaluate (so the channel genuinely would not register), or if `@deltachat/stdio-rpc-server` isn't installed (the import throws) — so it also implicitly verifies the dependency from step 4. Importing is safe: deltachat instantiates the rpc client only in `setup()` (at host startup), never at import.\n\nEnd-to-end message delivery against a real email account is verified manually once the service is running — see Wiring and Troubleshooting.\n\n## Account Setup\n\nA dedicated email account is strongly recommended — it will accumulate DeltaChat-formatted messages and store encryption keys. Not all providers work well with DeltaChat; check https://providers.delta.chat/ before picking one.\n\n**Default security modes:** IMAP uses SSL/TLS (port 993), SMTP uses STARTTLS (port 587). Both are configurable via `.env` — see Credentials below.\n\nTo find the correct hostnames for a domain:\n\n```bash\nnode -e \"require('dns').resolveMx('example.com', (e,r) => console.log(r))\"\n```\n\nMost providers publish their IMAP/SMTP hostnames in their help docs under \"manual setup\" or \"IMAP access.\"\n\n## Credentials\n\nAdd to `.env`:\n\n```bash\nDC_EMAIL=bot@example.com\nDC_PASSWORD=your-app-password\nDC_IMAP_HOST=imap.example.com\nDC_IMAP_PORT=993\nDC_IMAP_SECURITY=1        # 1=SSL/TLS (default), 2=STARTTLS, 3=plain\nDC_SMTP_HOST=smtp.example.com\nDC_SMTP_PORT=587\nDC_SMTP_SECURITY=2        # 2=STARTTLS (default), 1=SSL/TLS, 3=plain\n```\n\nSecurity settings are applied on every startup, so changing them in `.env` and restarting takes effect without wiping the account.\n\n\n### Optional settings\n\nThe following are read from the process environment (not `.env`). To override them, add `Environment=` lines to the systemd service unit or your launchd plist:\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `DC_ACCOUNT_DIR` | `dc-account` | Directory for DeltaChat account data (IMAP state, keys, blobs) |\n| `DC_DISPLAY_NAME` | `NanoClaw` | Bot display name shown in DeltaChat |\n| `DC_AVATAR_PATH` | _(none)_ | Absolute path to avatar image; set at startup only |\n\nThe `/set-avatar` command (send an image with that caption) is the easiest way to set the avatar at runtime without modifying the service file. Only users with `owner` or global `admin` role can use it.\n\n### Restart\n\nRun from your NanoClaw project root:\n\n```bash\nsource setup/lib/install-slug.sh\n\n# Linux\nsystemctl --user restart $(systemd_unit)\n\n# macOS\nlaunchctl kickstart -k gui/$(id -u)/$(launchd_label)\n```\n\nOn first start the adapter configures the email account (IMAP/SMTP credentials, calls `configure()`). Subsequent starts skip straight to `startIo()`. Account data is stored in `dc-account/` in the project root (or your `DC_ACCOUNT_DIR`).\n\n## Wiring\n\n### DMs\n\n**DeltaChat cont","createdAt":"2026-09-25T10:52:06.562Z","updatedAt":"2026-09-25T10:52:06.562Z"},{"id":"cmugucu9800j2qu06cegz2tp5","slug":"nanocoai-nanoclaw-add-dial-number","name":"add-dial-number","description":"Add another phone number to an existing Dial channel — a second (or third) public line for the agent, so one NanoClaw install answers SMS and AI voice calls on multiple numbers. Use when Dial is already installed and the operator wants an additional number (e.g. a personal line plus a support line). Requires the Dial channel to already be installed (see /add-dial).","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-dial-number","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add another phone number to an existing Dial channel — a second (or third) public line for the agent, so one NanoClaw install answers SMS and AI voice calls on multiple numbers. Use when Dial is already installed and the operator wants an additional number (e.g. a personal line plus a support line). Requires the Dial channel to already be installed (see /add-dial).","permissions":[],"systemPrompt":"# Add another Dial number\n\nOne NanoClaw install can serve **multiple Dial numbers** at once. Each number is\nits own **public, threaded line** — its own messaging group (`platform_id` = the\nDial number), each remote correspondent a thread inside it — and the agent\nreplies from whichever number a person texted.\n\nThis skill is for adding a number to an **already-installed** Dial channel. Its\nmechanical steps use `nc:` directives so an agent and the deterministic skill\nengine perform the same validated, idempotent workflow.\n\n## Pre-flight\n\nConfirm Dial is installed and registered. If this fails, run `/add-dial` first,\nthen retry:\n\n```nc:run effect:check\ntest -f src/channels/dial.ts && grep -q \"import './dial.js';\" src/channels/index.ts\n```\n\nAdding another number requires the multi-number adapter, which routes on the\nline each event arrived on (`data.to`). If this fails, run `/update-skills` to\nrefresh the installed adapter, rebuild, and retry:\n\n```nc:run effect:check\ngrep -q \"eventLine\" src/channels/dial.ts\n```\n\nResolve the Dial CLI and the install's user-agent token once for every account\nrequest below:\n\n```nc:run capture:dial_path validate:^/.+ effect:fetch\ncommand -v dial\n```\n\n```nc:run capture:dial_ua validate:^nanoclaw/\\S+$ effect:fetch\nnode -p \"'nanoclaw/'+(require('./package.json').version||'unknown')\" 2>/dev/null || echo nanoclaw/unknown\n```\n\n## Choose the number\n\nList the account's current numbers:\n\n```nc:run capture:dial_numbers effect:fetch\nDIAL_USER_AGENT={{dial_ua}} \"{{dial_path}}\" number list --json | jq -r 'if (.numbers|length)==0 then \"none\" else [.numbers[].number] | join(\", \") end'\n```\n\nTell the operator what is available. Buying a number spends account funds, so\nleave that explicit: if they need a new one, they should purchase it with\n`dial number purchase --inbound-instruction \"…\" --explicit-programmatic-consent\n\"<account-holder consent attestation>\"`, then enter the returned number below.\n\n```nc:operator\nDial numbers on this account: {{dial_numbers}}. Choose one that is not already wired to NanoClaw. If you need a new number, purchase it first; this may charge the Dial account.\n```\n\n```nc:prompt platform_id validate:^[+][1-9]\\d{6,14}$ normalize:trim\nWhich Dial number should be added? Enter its E.164 value, for example +14155550123.\n```\n\nVerify that the chosen number belongs to the signed-in account:\n\n```nc:run effect:check\nDIAL_USER_AGENT={{dial_ua}} \"{{dial_path}}\" number list --json | jq -e --arg number '{{platform_id}}' '.numbers[] | select(.number==$number)' >/dev/null\n```\n\n## Choose the agent\n\nList the agent groups:\n\n```nc:run capture:agent_groups effect:fetch\nncl groups list --json | jq -r 'if (.data|length)==0 then \"no agent groups yet\" else [.data[] | \"\\(.id) (\\(.name))\"] | join(\", \") end'\n```\n\n```nc:operator\nAgent groups on this install: {{agent_groups}}.\n```\n\n```nc:prompt agent_group_id validate:^ag-[A-Za-z0-9-]+$ normalize:trim\nWhich agent group should answer this number? Enter its ag-… id.\n```\n\nReject a typo before creating anything:\n\n```nc:run effect:check\nncl groups list --json | jq -e --arg id '{{agent_group_id}}' '.data[] | select(.id==$id)' >/dev/null\n```\n\nChoose a safe display name and who may start conversations on this line.\n`strict` admits only known users; `public` lets anyone who knows the number reach\nthe agent. The choice belongs to this number and does not change existing lines:\n\n```nc:prompt line_name validate:^[A-Za-z0-9][A-Za-z0-9_.-]*(\\x20[A-Za-z0-9][A-Za-z0-9_.-]*)*$ normalize:trim\nWhat should this line be called? Use letters, numbers, spaces, dots, dashes, or underscores.\n```\n\n```nc:prompt inbound_access validate:^(strict|public)$ normalize:trim\nWho may text this line: strict or public?\n```\n\n## Wire the line\n\nCreate the threaded Dial messaging group. This is idempotent on the number, so\na re-run returns an existing row without resetting later policy changes:\n\n```nc:run effect:wire\nncl messaging-groups create --channel-type dial --platform-id {{platform_id}} --is-group 1 --name \"{{line_name}}\" --unknown-sender-policy {{inbound_access}}\n```\n\nWire it to the selected agent group. The adapter's declaration supplies the\nthread and engagement defaults:\n\n```nc:run effect:wire\nncl wirings create --channel-type dial --platform-id {{platform_id}} --agent-group-id {{agent_group_id}}\n```\n\n## Restart and verify\n\nRestart the service so the new line is picked up consistently:\n\n```nc:run effect:restart\nbash setup/lib/restart.sh\n```\n\nVerify the exact messaging-group and wiring pair exists:\n\n```nc:run effect:check\nmg=$(ncl messaging-groups list --json | jq -er --arg number '{{platform_id}}' '.data[] | select(.channel_type==\"dial\" and .platform_id==$number) | .id') && ncl wirings list --json | jq -e --arg mg \"$mg\" --arg ag '{{agent_group_id}}' '.data[] | select(.messaging_group_id==$mg and .agent_group_id==$ag)' >/dev/null\n```\n\nThe new number now reaches the selected agent as a separate threaded line, and\nreplies leave from the number that received the message. Existing lines are\nunchanged.\n\n## Troubleshooting\n\n- **The multi-number check fails** → run `/update-skills`, rebuild, and retry.\n- **The selected number is rejected** → sign in to the correct Dial account or\n  purchase the number first, then copy its exact E.164 value.\n- **New texts land in an old line or replies use the wrong number** → the running\n  service still has the old adapter; refresh it and restart again.\n- **New inbound events never arrive** → one `dial listen` daemon covers the\n  whole account; confirm `dial doctor --json` reports `listen.running: true` and\n  `dial local-target list --json` contains NanoClaw's command target.\n- **`ncl` errors** → the host service must be running; `ncl` connects over a\n  Unix socket.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-dial-number","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-dial-number/SKILL.md","defaultBranch":"main"},"readme":"# Add another Dial number\n\nOne NanoClaw install can serve **multiple Dial numbers** at once. Each number is\nits own **public, threaded line** — its own messaging group (`platform_id` = the\nDial number), each remote correspondent a thread inside it — and the agent\nreplies from whichever number a person texted.\n\nThis skill is for adding a number to an **already-installed** Dial channel. Its\nmechanical steps use `nc:` directives so an agent and the deterministic skill\nengine perform the same validated, idempotent workflow.\n\n## Pre-flight\n\nConfirm Dial is installed and registered. If this fails, run `/add-dial` first,\nthen retry:\n\n```nc:run effect:check\ntest -f src/channels/dial.ts && grep -q \"import './dial.js';\" src/channels/index.ts\n```\n\nAdding another number requires the multi-number adapter, which routes on the\nline each event arrived on (`data.to`). If this fails, run `/update-skills` to\nrefresh the installed adapter, rebuild, and retry:\n\n```nc:run effect:check\ngrep -q \"eventLine\" src/channels/dial.ts\n```\n\nResolve the Dial CLI and the install's user-agent token once for every account\nrequest below:\n\n```nc:run capture:dial_path validate:^/.+ effect:fetch\ncommand -v dial\n```\n\n```nc:run capture:dial_ua validate:^nanoclaw/\\S+$ effect:fetch\nnode -p \"'nanoclaw/'+(require('./package.json').version||'unknown')\" 2>/dev/null || echo nanoclaw/unknown\n```\n\n## Choose the number\n\nList the account's current numbers:\n\n```nc:run capture:dial_numbers effect:fetch\nDIAL_USER_AGENT={{dial_ua}} \"{{dial_path}}\" number list --json | jq -r 'if (.numbers|length)==0 then \"none\" else [.numbers[].number] | join(\", \") end'\n```\n\nTell the operator what is available. Buying a number spends account funds, so\nleave that explicit: if they need a new one, they should purchase it with\n`dial number purchase --inbound-instruction \"…\" --explicit-programmatic-consent\n\"<account-holder consent attestation>\"`, then enter the returned number below.\n\n```nc:operator\nDial numbers on this account: {{dial_numbers}}. Choose one that is not already wired to NanoClaw. If you need a new number, purchase it first; this may charge the Dial account.\n```\n\n```nc:prompt platform_id validate:^[+][1-9]\\d{6,14}$ normalize:trim\nWhich Dial number should be added? Enter its E.164 value, for example +14155550123.\n```\n\nVerify that the chosen number belongs to the signed-in account:\n\n```nc:run effect:check\nDIAL_USER_AGENT={{dial_ua}} \"{{dial_path}}\" number list --json | jq -e --arg number '{{platform_id}}' '.numbers[] | select(.number==$number)' >/dev/null\n```\n\n## Choose the agent\n\nList the agent groups:\n\n```nc:run capture:agent_groups effect:fetch\nncl groups list --json | jq -r 'if (.data|length)==0 then \"no agent groups yet\" else [.data[] | \"\\(.id) (\\(.name))\"] | join(\", \") end'\n```\n\n```nc:operator\nAgent groups on this install: {{agent_groups}}.\n```\n\n```nc:prompt agent_group_id validate:^ag-[A-Za-z0-9-]+$ normalize:trim\nWhich agent group should answer this number? Enter its ag-… id.\n```\n\nReject a typo before creating anything:\n\n```nc:run effect:check\nncl groups list --json | jq -e --arg id '{{agent_group_id}}' '.data[] | select(.id==$id)' >/dev/null\n```\n\nChoose a safe display name and who may start conversations on this line.\n`strict` admits only known users; `public` lets anyone who knows the number reach\nthe agent. The choice belongs to this number and does not change existing lines:\n\n```nc:prompt line_name validate:^[A-Za-z0-9][A-Za-z0-9_.-]*(\\x20[A-Za-z0-9][A-Za-z0-9_.-]*)*$ normalize:trim\nWhat should this line be called? Use letters, numbers, spaces, dots, dashes, or underscores.\n```\n\n```nc:prompt inbound_access validate:^(strict|public)$ normalize:trim\nWho may text this line: strict or public?\n```\n\n## Wire the line\n\nCreate the threaded Dial messaging group. This is idempotent on the number, so\na re-run returns an existing row without resetting later policy changes:\n\n```nc:run effect:wire\nncl messaging-groups create --channel-type dial --platform-id {{platform_id}} --is-group 1 --name \"","createdAt":"2026-09-25T10:52:06.573Z","updatedAt":"2026-09-25T10:52:06.573Z"},{"id":"cmugucu9j00j5qu06bvq4u0df","slug":"nanocoai-nanoclaw-add-dial-tool","name":"add-dial-tool","description":"Give chosen NanoClaw agents a real phone number as a container tool — the `dial` CLI baked into the agent image plus OneCLI credential injection for api.getdial.ai, scoped per agent, so the agents you pick can send SMS, place AI voice calls, and receive verification codes from inside the sandbox. Independent of the Dial channel; idempotent; re-run to change which agents may use it. Use when the user wants agents to text, call, or run `dial …` from a chat, without wiring Dial as a messaging channel.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-dial-tool","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Give chosen NanoClaw agents a real phone number as a container tool — the `dial` CLI baked into the agent image plus OneCLI credential injection for api.getdial.ai, scoped per agent, so the agents you pick can send SMS, place AI voice calls, and receive verification codes from inside the sandbox. Independent of the Dial channel; idempotent; re-run to change which agents may use it. Use when the user wants agents to text, call, or run `dial …` from a chat, without wiring Dial as a messaging channel.","permissions":[],"systemPrompt":"# Add Dial Tool\n\nInstalls Dial as a **container tool**: the `dial` CLI on the agent's `PATH`, the\n`dial-cli` skill so the agent knows how to drive it, and an OneCLI credential so\nin-container calls are injected keyless. Independent of the Dial **channel**\n(`/add-dial`) — install this alone. Idempotent: re-run it to change which agents\nmay use Dial.\n\n**This tool spends money and reaches real people.** An agent with Dial access can\ntext and call any number and buy more numbers, billed to the Dial account. The\nCLI and the skill file land in every agent's container, but the **key** is\ninjected per agent by OneCLI, so the operator chooses which agents get it. Every\nother agent gets an OneCLI block rule and sees `403 blocked_by_policy` if it\ntries.\n\nRun this from the NanoClaw repo on the host (not from a chat with an agent — the\ncontainer can't install itself). The mechanical steps carry `nc:` directive\nfences: an agent reads the prose and applies them, and a parser can apply them\ndeterministically from the same document. Every directive is idempotent, so the\nwhole skill is safe to re-run; anything a parser can't apply falls back to the\nprose beside it.\n\n## Pre-flight\n\nOneCLI is required for credential injection — without it there is no way to hand\nthe key to a container without putting it in an env var. This must succeed before\nanything else runs:\n\n```nc:run effect:check\ncommand -v onecli >/dev/null\n```\n\nIf it fails, tell the user to run `/init-onecli` first, then retry. Stop here.\n\nCalls this setup makes to Dial identify the install. The `dial` CLI prepends\n`DIAL_USER_AGENT` to its own token, so the account's requests stay attributable\nto this NanoClaw install in Dial's server-side logs. Resolve the token once\n(`nanoclaw/<version>`; an unreadable `package.json` degrades to\n`nanoclaw/unknown` rather than blocking the install):\n\n```nc:run capture:dial_ua validate:^nanoclaw/\\S+$ effect:fetch\nnode -p \"'nanoclaw/'+(require('./package.json').version||'unknown')\" 2>/dev/null || echo nanoclaw/unknown\n```\n\nPrefix every `dial` command below with `DIAL_USER_AGENT={{dial_ua}}`.\n\n## Choose which agents may use Dial\n\nList the agent groups (the NanoClaw service must be running — `ncl` talks to it\nover its socket):\n\n```nc:run capture:agent_groups effect:fetch\nncl groups list --json | jq -r 'if (.data|length)==0 then \"no agent groups yet\" else [.data[] | \"\\(.id) (\\(.name))\"] | join(\", \") end'\n```\n\nAsk the operator which of them may use Dial. Say plainly what they are granting,\nand ask even when there is a single agent:\n\n```nc:operator\nAgents on this install: {{agent_groups}}. Giving an agent Dial lets it text and call any number and buy numbers, billed to your Dial account. Agents you leave out are blocked at the gateway (reversible by running /add-dial-tool again). Agents created after this run have Dial until the next run.\n```\n```nc:prompt dial_agents validate:^(all|none|ag-[A-Za-z0-9-]+(,ag-[A-Za-z0-9-]+)*)$ normalize:trim\nWhich agents may use Dial? Enter agent ids separated by commas with no spaces (the `ag-…` column), `all` for every agent, or `none` to install the tool with every agent blocked for now.\n```\n\n`all` and `none` cannot be mixed with ids, and an empty answer is never\n\"everyone\". A typo must not silently open or close anything, so every id named\nmust be a real agent group:\n\n```nc:run effect:check\nfor w in $(printf '%s' '{{dial_agents}}' | tr ',' ' '); do case \"$w\" in all|none) ;; *) ncl groups list --json | jq -e --arg id \"$w\" '.data[] | select(.id==$id)' >/dev/null || { echo \"unknown agent group '$w' — see: ncl groups list\" >&2; exit 1; }; esac; done\n```\n\n## Install the Dial CLI on the host\n\nThe host needs the `dial` CLI to sign in: `dial auth login` / `dial auth\nverify-otp` write the host auth file that the credential step below reads. Pinned\nto the same version the agent image gets, so host and sandbox agree:\n\n```nc:run effect:external\ncommand -v dial >/dev/null || npm install -g @getdial/cli@0.37.0\n```\n\n## Sign in to Dial\n\nDial's CLI owns the account credential (an auth file it writes on sign-in).\n\n### Check the host sign-in\n\nIs this host already signed in?\n\n```nc:run capture:signed_in=.auth.signedIn validate:^(true|false)$ effect:fetch\nDIAL_USER_AGENT={{dial_ua}} dial doctor --json\n```\n\n### Read the account\n\nIf it **is**, read which account — that account's key is what the chosen agents\nwill use:\n\n```nc:run capture:connected_email=.auth.email when:signed_in=true effect:fetch\nDIAL_USER_AGENT={{dial_ua}} dial doctor --json\n```\n```nc:operator when:signed_in=true\nThis host is signed in to Dial as {{connected_email}}; the agents you chose will use that account. To give them a different account, run `dial auth login <email> --force` and `dial auth verify-otp --code <code>` on the host first, then run /add-dial-tool again.\n```\n\n### Send the code\n\nIf it is **not**, verify an email with a one-time code. Collect the email:\n\n```nc:prompt owner_email validate:^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$ when:signed_in=false\nWhat's your email? Dial sends a one-time code to verify it. By continuing you create a Dial account and agree to Dial's Terms of Service (https://getdial.ai/terms) and Privacy Policy (https://getdial.ai/privacy).\n```\n\nSend the code (`--force` re-sends even if a prior code is pending):\n\n```nc:run effect:external when:signed_in=false\nDIAL_USER_AGENT={{dial_ua}} dial auth login {{owner_email}} --force\n```\n\n### Verify the code\n\nCollect the code:\n\n```nc:prompt otp validate:^\\d{6}$ when:signed_in=false\nEnter the 6-digit code from your email\n```\n\nVerify it. Do **not** pass `--agent nanoclaw` here: this skill owns the container\n`dial-cli` skill, and `--agent` would drop a second, unmanaged copy next to it:\n\n```nc:run effect:external when:signed_in=false\nDIAL_USER_AGENT={{dial_ua}} dial auth verify-otp --code {{otp}}\n```\n\n## Put the CLI and its skill in the agent image\n\nThe agent's global Node CLIs install from `container/cli-tools.json`, not from\nhand-edited Dockerfile layers. Add the pinned Dial CLI — idempotent on `name`, so\na re-run is a no-op. `@getdial/cli` has no native postinstall, so no `onlyBuilt`:\n\n```nc:json-merge into:container/cli-tools.json key:name\n{ \"name\": \"@getdial/cli\", \"version\": \"0.37.0\" }\n```\n\nThe version (`0.37.0`) is the canonical pin — this document is the source of\ntruth; the host install above uses the same one.\n\nMount the sandbox-aware `dial-cli` skill so the agent knows the CLI runs keyless\nin there and never asks for credentials. `container/skills/` is mounted read-only\ninto every agent container (at `/app/skills`) — which is why the key, not the\nskill file, is what gets scoped per agent:\n\n```nc:copy\ncontainer-skills/dial-cli/SKILL.md -> container/skills/dial-cli/SKILL.md\n```\n\nRebuild the image so the CLI lands. On an install that fetches a published image\nthis adds Dial as a layer on top of it; on one that builds its own it rebuilds:\n\n```nc:run effect:build\n./container/build.sh\n```\n\n## Register the credential with OneCLI\n\nRead the API key from the host auth file — the single source of truth, written\nby `dial auth login` / `dial auth verify-otp` — and put it in the OneCLI vault\nfor `api.getdial.ai`. Always **replace**: the vault is keyed by name, so an\nexisting \"Dial API\" secret is not necessarily this account's (re-onboarding,\nswitching accounts, or rotating the key all leave a secret whose value points at\nthe previous account, and a sandboxed agent then lists *that* account's numbers).\nA stale secret is deleted and a fresh one created rather than updated in place:\n`onecli secrets update` accepts a new value only on the command line, and the key\nmust never sit on one. It travels through a `0600` temp file that is removed right\nafter (`--file`), so it is never on argv or in a captured variable. Selective-mode\nagents pick the new id up in the merge step below:\n\n```nc:run effect:external\nT=$(mktemp) && chmod 600 \"$T\" && jq -r '.apiKey // empty' \"${XDG_DATA_HOME:-$HOME/.local/share}/dial/auth.v1.json\" > \"$T\" 2>/dev/null; [ -s \"$T\" ] || { rm -f \"$T\"; echo \"no Dial API key in the host auth file — sign in with dial auth login / verify-otp, then re-run\" >&2; exit 1; }; S=$(onecli secrets list | jq -r 'first(.data[] | select(.name | test(\"(?i)dial\"))) | .id // empty'); if [ -n \"$S\" ]; then onecli secrets delete --id \"$S\" >/dev/null || { rm -f \"$T\"; echo \"could not remove the previous Dial secret $S\" >&2; exit 1; }; fi; onecli secrets create --name \"Dial API\" --type generic --file \"$T\" --host-pattern api.getdial.ai --header-name Authorization --value-format \"Bearer {value}\" >/dev/null; rc=$?; rm -f \"$T\"; exit $rc\n```\n\n## Scope it to the chosen agents\n\n### Create the OneCLI agents\n\nNanoClaw gives every agent group its own OneCLI agent whose `identifier` is the\ngroup id, created on the group's first spawn. A group that has never spawned has\nno OneCLI agent yet, and a block rule needs one to attach to — so create the\nmissing ones now, exactly as the runtime would (secret mode `all`, nothing else\ntouched):\n\n```nc:run effect:wire\nG=$(ncl groups list --json) || { echo \"could not list agent groups — is the NanoClaw host running?\" >&2; exit 1; }; AG=$(onecli agents list) || { echo \"could not list OneCLI agents\" >&2; exit 1; }; printf '%s' \"$G\" | jq -r '.data[] | \"\\(.id)\\t\\(.name)\"' | while IFS=\"$(printf '\\t')\" read -r gid gname; do printf '%s' \"$AG\" | jq -e --arg g \"$gid\" '.data[] | select(.identifier==$g)' >/dev/null || onecli agents create --name \"$gname\" --identifier \"$gid\" >/dev/null || { echo \"could not create an OneCLI agent for $gname ($gid)\" >&2; exit 1; }; done\n```\n\n### Set the block rules\n\nThe one switch is a per-agent **block rule** on `api.getdial.ai`, named\n`Dial: blocked for <group>` so only this skill's rules are ever read or written\n(an operator's own rules on the host are left alone). A chosen agent has its\nrule removed; every other agent has one present and enabled. A `403\nblocked_by_policy` in a container means \"not chosen\", not \"broken\":\n\n```nc:run effect:wire\nA=$(printf '%s' '{{dial_agents}}' | tr -d ' '); G=$(ncl groups list --json) || { echo \"could not list agent groups — is the NanoClaw host running?\" >&2; exit 1; }; case \",$A,\" in *,all,*) A=$(printf '%s' \"$G\" | jq -r '[.data[].id] | join(\",\")');; esac; AG=$(onecli agents list) || { echo \"could not list OneCLI agents\" >&2; exit 1; }; RL=$(onecli rules list) || { echo \"could not list OneCLI rules\" >&2; exit 1; }; printf '%s' \"$G\" | jq -r '.data[] | \"\\(.id)\\t\\(.name)\"' | while IFS=\"$(printf '\\t')\" read -r gid gname; do aid=$(printf '%s' \"$AG\" | jq -r --arg g \"$gid\" 'first(.data[] | select(.identifier==$g)) | .id // empty'); [ -n \"$aid\" ] || { echo \"no OneCLI agent for $gname ($gid)\" >&2; exit 1; }; rid=$(printf '%s' \"$RL\" | jq -r --arg a \"$aid\" 'first(.data[] | select(.hostPattern==\"api.getdial.ai\" and .action==\"block\" and .agentId==$a and (.name | startswith(\"Dial: blocked for \")) and ((.pathPattern // \"\")==\"\") and ((.method // \"\")==\"\"))) | .id // empty'); case \",$A,\" in *,\"$gid\",*) if [ -n \"$rid\" ]; then onecli rules delete --id \"$rid\" >/dev/null || { echo \"could not remove the Dial block for $gname ($gid)\" >&2; exit 1; }; fi; echo \"allowed: $gname ($gid)\";; *) if [ -z \"$rid\" ]; then onecli rules create --name \"Dial: blocked for $gname\" --host-pattern api.getdial.ai --action block --agent-id \"$aid\" --enabled >/dev/null || { echo \"could not create the Dial block for $gname ($gid)\" >&2; exit 1; }; else onecli rules update --id \"$rid\" --enabled true >/dev/null || { echo \"could not re-enable the Dial block for $gname ($gid)\" >&2; exit 1; }; fi; echo \"blocked: $gname ($gid)\";; esac; done\n```\n\n### Merge secrets for selective agents\n\nSecret lists are left alone, with one exception. An agent in `selective` mode only\ngets the secrets on its list, so a **chosen** selective agent has the Dial secret\nmerged into it. `onecli agents set-secrets` switches an agent to selective mode,\nso it is never called on an `all`-mode agent — that would silently cut the agent\noff from every credential not on its list. Blocked agents keep their lists\nuntouched in either mode; the rule alone blocks:\n\n```nc:run effect:wire\nA=$(printf '%s' '{{dial_agents}}' | tr -d ' '); case \",$A,\" in *,all,*) A=$(ncl groups list --json | jq -r '[.data[].id] | join(\",\")');; esac; S=$(onecli secrets list | jq -r 'first(.data[] | select(.name | test(\"(?i)dial\"))) | .id // empty'); [ -n \"$S\" ] || { echo \"no Dial secret in the OneCLI vault — the credential step above did not complete\" >&2; exit 1; }; onecli agents list | jq -r '.data[] | select(.secretMode==\"selective\") | \"\\(.id)\\t\\(.identifier)\"' | while IFS=\"$(printf '\\t')\" read -r aid gid; do case \",$A,\" in *,\"$gid\",*) onecli agents set-secrets --id \"$aid\" --secret-ids \"$(onecli agents secrets --id \"$aid\" | jq -r --arg s \"$S\" '[.data[], $s] | unique | join(\",\")')\" >/dev/null || { echo \"could not add the Dial secret to $gid\" >&2; exit 1; }; echo \"Dial secret added to the list of $gid\";; esac; done\n```\n\n## Hand the tool to running agents\n\n`container/skills/` is mounted read-only into every agent container, and each\ngroup's `.claude-shared/skills/` holds symlinks into that mount that are synced\nwhen the container spawns — so nothing is copied per session. A running agent\nkeeps its old image until it respawns, so restart every group; without a\n`--message` each one comes back on its next message, on the new image, with the\nCLI on `PATH` and the skill in place. This is a restart effect, so it does not\nfire after an earlier step bounced — agents keep the image they have until the\ngap above is fixed and the skill is re-applied:\n\n```nc:run effect:restart\nncl groups list --json | jq -r '.data[].id' | while read -r gid; do ncl groups restart --id \"$gid\" >/dev/null || { echo \"could not restart $gid\" >&2; exit 1; }; done\n```\n\n## Done\n\nThe chosen agents can now use Dial from inside their containers; the others are\nblocked at the gateway. Auth is injected by OneCLI; a `403 blocked_by_policy`\nmeans the agent was not chosen (run `/add-dial-tool` again to change that); a\n`401` means the Dial secret needs (re)connecting — not a login. Verify from a\nchat with a chosen agent: \"run dial doctor\" or \"text +1… hi\".\n\nTo uninstall: see [REMOVE.md](REMOVE.md). To wire Dial as a **messaging\nchannel** too, run `/add-dial`.\n\n## Troubleshooting\n\n**`command -v onecli` fails.** OneCLI is not installed or not on `PATH`. Run\n`/init-onecli`, then re-run this skill.\n\n**`ncl` can't reach the host.** The agent list and the scoping steps talk to the\nrunning NanoClaw service. Start it (`pnpm run dev`, or restart the service) and\nre-run.\n\n**`unknown agent group`.** An id in your answer is not in `ncl groups list`. Copy\nthe `ag-…` id exactly; names are not accepted.\n\n**`no Dial API key in the host auth file`.** The sign-in did not complete. Run\n`dial auth login <email> --force`, then `dial auth verify-otp --code <code>`, and\nre-run.\n\n**A chosen agent gets `401`.** The vault secret is stale (a different account's\nkey, or a rotated one). Re-run this skill — it always rewrites the secret with the\nkey the host is signed in with.\n\n**An agent you left out can still use Dial.** It was created after the last run\n(a new OneCLI agent starts in `all` mode with no rule). Re-run this skill; it\nonly touches the per-agent rules.\n\n**`dial: command not found` inside a container.** The image predates the manifest\nentry. Run `./container/build.sh`, then `ncl groups restart --id <group-id>` so the\nagent respawns on it.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-dial-tool","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-dial-tool/SKILL.md","defaultBranch":"main"},"readme":"# Add Dial Tool\n\nInstalls Dial as a **container tool**: the `dial` CLI on the agent's `PATH`, the\n`dial-cli` skill so the agent knows how to drive it, and an OneCLI credential so\nin-container calls are injected keyless. Independent of the Dial **channel**\n(`/add-dial`) — install this alone. Idempotent: re-run it to change which agents\nmay use Dial.\n\n**This tool spends money and reaches real people.** An agent with Dial access can\ntext and call any number and buy more numbers, billed to the Dial account. The\nCLI and the skill file land in every agent's container, but the **key** is\ninjected per agent by OneCLI, so the operator chooses which agents get it. Every\nother agent gets an OneCLI block rule and sees `403 blocked_by_policy` if it\ntries.\n\nRun this from the NanoClaw repo on the host (not from a chat with an agent — the\ncontainer can't install itself). The mechanical steps carry `nc:` directive\nfences: an agent reads the prose and applies them, and a parser can apply them\ndeterministically from the same document. Every directive is idempotent, so the\nwhole skill is safe to re-run; anything a parser can't apply falls back to the\nprose beside it.\n\n## Pre-flight\n\nOneCLI is required for credential injection — without it there is no way to hand\nthe key to a container without putting it in an env var. This must succeed before\nanything else runs:\n\n```nc:run effect:check\ncommand -v onecli >/dev/null\n```\n\nIf it fails, tell the user to run `/init-onecli` first, then retry. Stop here.\n\nCalls this setup makes to Dial identify the install. The `dial` CLI prepends\n`DIAL_USER_AGENT` to its own token, so the account's requests stay attributable\nto this NanoClaw install in Dial's server-side logs. Resolve the token once\n(`nanoclaw/<version>`; an unreadable `package.json` degrades to\n`nanoclaw/unknown` rather than blocking the install):\n\n```nc:run capture:dial_ua validate:^nanoclaw/\\S+$ effect:fetch\nnode -p \"'nanoclaw/'+(require('./package.json').version||'unknown')\" 2>/dev/null || echo nanoclaw/unknown\n```\n\nPrefix every `dial` command below with `DIAL_USER_AGENT={{dial_ua}}`.\n\n## Choose which agents may use Dial\n\nList the agent groups (the NanoClaw service must be running — `ncl` talks to it\nover its socket):\n\n```nc:run capture:agent_groups effect:fetch\nncl groups list --json | jq -r 'if (.data|length)==0 then \"no agent groups yet\" else [.data[] | \"\\(.id) (\\(.name))\"] | join(\", \") end'\n```\n\nAsk the operator which of them may use Dial. Say plainly what they are granting,\nand ask even when there is a single agent:\n\n```nc:operator\nAgents on this install: {{agent_groups}}. Giving an agent Dial lets it text and call any number and buy numbers, billed to your Dial account. Agents you leave out are blocked at the gateway (reversible by running /add-dial-tool again). Agents created after this run have Dial until the next run.\n```\n```nc:prompt dial_agents validate:^(all|none|ag-[A-Za-z0-9-]+(,ag-[A-Za-z0-9-]+)*)$ normalize:trim\nWhich agents may use Dial? Enter agent ids separated by commas with no spaces (the `ag-…` column), `all` for every agent, or `none` to install the tool with every agent blocked for now.\n```\n\n`all` and `none` cannot be mixed with ids, and an empty answer is never\n\"everyone\". A typo must not silently open or close anything, so every id named\nmust be a real agent group:\n\n```nc:run effect:check\nfor w in $(printf '%s' '{{dial_agents}}' | tr ',' ' '); do case \"$w\" in all|none) ;; *) ncl groups list --json | jq -e --arg id \"$w\" '.data[] | select(.id==$id)' >/dev/null || { echo \"unknown agent group '$w' — see: ncl groups list\" >&2; exit 1; }; esac; done\n```\n\n## Install the Dial CLI on the host\n\nThe host needs the `dial` CLI to sign in: `dial auth login` / `dial auth\nverify-otp` write the host auth file that the credential step below reads. Pinned\nto the same version the agent image gets, so host and sandbox agree:\n\n```nc:run effect:external\ncommand -v dial >/dev/null || npm install -g @getdial/cli@0.37.0\n```\n\n## Sign in to Dial\n\nDial","createdAt":"2026-09-25T10:52:06.583Z","updatedAt":"2026-09-25T10:52:06.583Z"},{"id":"cmugucu9z00j8qu06gvb583b2","slug":"nanocoai-nanoclaw-add-dial","name":"add-dial","description":"Add Dial channel integration — a real phone number for SMS and AI voice calls via the Dial platform (getdial.ai). Native adapter — no Chat SDK bridge.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-dial","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add Dial channel integration — a real phone number for SMS and AI voice calls via the Dial platform (getdial.ai). Native adapter — no Chat SDK bridge.","permissions":[],"systemPrompt":"# Add Dial Channel\n\nAdds [Dial](https://getdial.ai) — a real phone number for **SMS and AI voice\ncalls**. Native adapter (no Chat SDK bridge): both directions go through the\n`dial` CLI — outbound via `dial message`, inbound via its command-target daemon. NanoClaw doesn't ship\nchannels in trunk — this skill copies the Dial adapter, its pairing helper, and\ntheir tests in from the `channels` branch. The `pair-dial` setup step is\nmaintained in trunk, so it is not copied here.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent\nreads the prose and applies them, and a parser can apply them deterministically\nfrom the same document. Every directive is idempotent, so the whole skill is\nsafe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Apply\n\n### 1. Copy the adapter, pairing helper, and tests\n\nFetch the `channels` branch and copy the Dial adapter, its pairing store and\nuser-agent helper (each with its test), and the registration test into place\n(overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/dial.ts\nsrc/channels/dial-pairing.ts\nsrc/channels/dial-pairing.test.ts\nsrc/channels/dial-user-agent.ts\nsrc/channels/dial-user-agent.test.ts\nsrc/channels/dial-registration.test.ts\nsrc/channels/dial-grant.test.ts\nsrc/channels/dial-status.test.ts\n```\n\nThe `dial-cli` container skill is deliberately **not** copied here.\n`container/skills/` is mounted read-only into *every* agent container\n(`src/container-runner.ts`), and a group with `skills:'all'` picks up whatever\nit finds there — so shipping the skill with the adapter would hand it to agents\non installs that never configured Dial. It is installed only by\n`/add-dial-tool`, offered under *Add phone superpowers* below, which is the\nskill that actually provisions the CLI the skill documents.\n\n`dial.ts` imports `dial-user-agent.js` at module scope, so omitting that helper\nbreaks the build and every test that loads the channel barrel.\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if present).\nThis one line is the skill's only reach-in into the channel core:\n\n```nc:append to:src/channels/index.ts\nimport './dial.js';\n```\n\n### 3. Register the pairing setup step\n\nAdd the `pair-dial` loader to the `STEPS` map in `setup/index.ts`, inside the\ndormant marker region (skipped if already present — `pair-dial` ships in core, so\nthis idempotent-skips on a normal install, but is expressed for a clean-upstream\nrebuild). The pairing handshake below spawns this step:\n\n```nc:append to:setup/index.ts at:nanoclaw:setup-steps\n'pair-dial': () => import('./pair-dial.js'),\n```\n\n### 4. Install the packages\n\nPinned to exact versions — the supply-chain policy rejects ranges and `latest`.\n`qrcode` renders the scannable pairing card:\n\n```nc:dep\nqrcode@1.5.4\n```\n\nThe adapter needs no Dial client library: it shells out to the `dial` CLI, which\nthis skill already requires for inbound. `@getdial/sdk` was dropped because it\ndepends on `pubnub`, which pulls react-native, Metro and Hermes into the\nlockfile for what is a single send — and the CLI ships in lockstep with the Dial\nAPI, so a contract change arrives as a CLI release rather than breaking a\nrequest pinned in the adapter.\n\n### 5. Build\n\nBuild first: it guards the adapter's typed core calls and proves the dependency\nis installed.\n\n```nc:run effect:build\npnpm run build\n```\n\n### 6. Validate\n\nThen run the one integration test.\n\n```nc:run effect:test\npnpm exec vitest run src/channels/dial-registration.test.ts\n```\n\n`dial-registration.test.ts` imports the real channel barrel and asserts the\nregistry contains `dial` — it goes red if the import line drifts. End-to-end\nSMS/voice is verified manually once the service runs.\n\n## Sign in to Dial\n\n### Install the CLI\n\nDial's CLI owns the account credential (an auth file it writes on sign-in), so\nthe setup uses the `dial` CLI here. Ensure it's installed — this installs it if\nit's missing (for the full onboarding/auth reference, see the `dial-cli` skill or\n`curl -fsSL https://getdial.ai/skills.md`):\n\n```nc:run effect:external\ncommand -v dial || curl -fsSL https://getdial.ai/install | bash\n```\n\n### Identify this install\n\nCalls this setup makes to Dial identify the install. The `dial` CLI prepends\n`DIAL_USER_AGENT` to its own token, so the account's requests stay attributable\nto this NanoClaw install in Dial's server-side logs. Resolve the token once\n(`nanoclaw/<version>`; an unreadable `package.json` degrades to\n`nanoclaw/unknown` rather than blocking the install) and prefix every `dial`\ncommand below with it:\n\n```nc:run capture:dial_ua validate:^nanoclaw/\\S+$ effect:fetch\nnode -p \"'nanoclaw/'+(require('./package.json').version||'unknown')\" 2>/dev/null || echo nanoclaw/unknown\n```\n\n### Pin the CLI path\n\nNow pin the CLI's **absolute** path into `.env`. The adapter shells out to `dial`\nto register its inbound command target, and it runs inside the NanoClaw service,\nwhich does not inherit your interactive shell's `PATH`. The CLI usually lands in\na version-manager bin directory (`~/.nvm/versions/node/*/bin`, `~/node/bin`, …)\nthat the service cannot see, so a bare `dial` fails with `ENOENT`, the command\ntarget is never registered, and the channel comes up connected but deaf — no\ninbound SMS or calls, with only a line in `logs/nanoclaw.error.log` to show for\nit. `DIAL_CLI_PATH` removes the guesswork; `dial.ts` already prefers it:\n\n```nc:run capture:dial_cli_path validate:^/.+ effect:fetch\ncommand -v dial\n```\n```nc:env-set\nDIAL_CLI_PATH={{dial_cli_path}}\n```\n\n### Check the sign-in\n\nCheck whether you're already signed in:\n\n```nc:run capture:signed_in=.auth.signedIn validate:^(true|false)$ effect:fetch\nDIAL_USER_AGENT={{dial_ua}} dial doctor --json\n```\n\n### Skip the reuse question when signed out\n\nIf you're **not** signed in, go straight to email verification — default the\nchoice so the branch guard below stays single-valued:\n\n```nc:run capture:reuse_choice when:signed_in=false effect:external\necho switch\n```\n\n### Read the account\n\nIf you **are** signed in, read which account (for the prompt below) and ask\nwhether to reuse it or sign in as a different one (matches the old wizard's\n\"Reuse this account?\" prompt, with an explicit way to switch):\n\n```nc:run capture:connected_email=.auth.email when:signed_in=true effect:fetch\nDIAL_USER_AGENT={{dial_ua}} dial doctor --json\n```\n```nc:operator when:signed_in=true\nYou're already signed in to Dial as {{connected_email}}.\n```\n```nc:prompt reuse_choice validate:^(reuse|switch)$ when:signed_in=true\nReuse this Dial account, or sign in as a different one? (reuse/switch)\n```\n\n### Reuse the account\n\n**Reuse** — no verification needed; with no `--code` the command just (re)installs\nthe NanoClaw agent skill:\n\n```nc:run effect:external when:reuse_choice=reuse\nDIAL_USER_AGENT={{dial_ua}} dial auth verify-otp --agent nanoclaw\n```\n\n### Send the code\n\n**Switch (or not signed in)** — verify an email with a one-time code. Collect the email:\n\n```nc:prompt owner_email validate:^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$ when:reuse_choice=switch\nWhat's your email? Dial sends a one-time code to verify it. By continuing you create a Dial account and agree to Dial's Terms of Service (https://getdial.ai/terms) and Privacy Policy (https://getdial.ai/privacy).\n```\n\nSend the code (`--force` re-sends even if a prior code is pending):\n\n```nc:run effect:external when:reuse_choice=switch\nDIAL_USER_AGENT={{dial_ua}} dial auth login {{owner_email}} --force\n```\n\n### Verify the code\n\nCollect the code (resolves inline, right after the send above):\n\n```nc:prompt otp validate:^\\d{6}$ when:reuse_choice=switch\nEnter the 6-digit code from your email\n```\n\nVerify it and provision your number (this also installs the NanoClaw agent skill):\n\n```nc:run effect:external when:reuse_choice=switch\nDIAL_USER_AGENT={{dial_ua}} dial auth verify-otp --code {{otp}} --agent nanoclaw\n```\n\n### Confirm the line\n\nConfirm the account's number — this becomes the agent's public line (its\n`platform_id`):\n\n```nc:run capture:platform_id validate:^\\+[1-9]\\d{6,14}$ effect:fetch\nDIAL_USER_AGENT={{dial_ua}} dial number list --json | jq -er '.numbers[0].number'\n```\n```nc:operator\nYour agent's Dial line is {{platform_id}}.\n```\n\n### Set the inbound greeting\n\nSet the line's inbound behavior — the system prompt the AI uses on calls *into*\nthis number. Verification no longer takes an instruction, so a fresh number\nstarts on Dial's default greeting until this runs:\n\n```nc:run effect:external\nDIAL_USER_AGENT={{dial_ua}} dial number set {{platform_id}} --inbound-instruction \"You are a friendly AI receptionist answering calls to this number. Greet the caller, ask how you can help, and take a clear message — their name, number, and reason for calling — if you cannot help directly.\"\n```\n\n### Pin the default sender\n\nMake that line the CLI's default sender. Verification saves whichever number\nthe account considers primary — the **oldest** one — while the line picked above\nis the **newest** (`numbers[0]`). On a single-number account those coincide, so\nnothing looks wrong; with two or more they diverge permanently, and every\n`dial call` / `dial message` that omits `--from-number` goes out from a number\nthis install isn't listening on. Replies to it are dropped as `no_agent_wired`.\n\nRewriting `phoneNumber`/`phoneNumberId` in the auth file makes the no-flag path\nland on the wired line, so an agent that forgets the selector is still correct:\n\n```nc:run effect:external\nf=\"${XDG_DATA_HOME:-$HOME/.local/share}/dial/auth.v1.json\"; i=$(DIAL_USER_AGENT={{dial_ua}} dial number list --json | jq -er --arg n '{{platform_id}}' '.numbers[]|select(.number==$n)|.id') && jq --arg n '{{platform_id}}' --arg i \"$i\" '.phoneNumber=$n|.phoneNumberId=$i' \"$f\" > \"$f.new\" && mv -f \"$f.new\" \"$f\" && chmod 600 \"$f\" && echo \"default sender pinned to {{platform_id}}\"\n```\n\n## Choose who may text the line\n\nA phone number is guessable, and whoever reaches the agent gets a turn with it —\nincluding its `dial` CLI, which is authenticated for the whole Dial account. An\nadmitted stranger can ask the agent to list every SMS and call on the account,\nread call transcripts, or buy another number. Session isolation doesn't prevent\nthis: the credential is the exposure, not the conversation.\n\nSo decide who gets in. `owner` is the safe default; pick `public` only if you\nwant a line strangers can start conversations on (an inbound receptionist, or\noutbound sales where prospects text back):\n\n```nc:prompt inbound_access validate:^(owner|public)$\nWho may text this line — `owner` (only the phone you pair next; everyone else is refused) or `public` (anyone who knows the number reaches the agent)?\n```\n\nYour answer is written to the line's own `unknown_sender_policy` when the line is\nregistered below, after the restart (`ncl` is socket-only, so it needs the\nservice up). It lives in the database from then on — per line, so a second number\nadded later carries its own answer — and the adapter never rewrites it.\n\n```nc:operator when:inbound_access=owner\nLocked to you: only the phone you pair in a moment can reach the agent on {{platform_id}}. Anyone else who texts it is refused — including people your agent calls, so they can't reply by text. To open it later: `ncl messaging-groups update --id <id> --unknown-sender-policy public` (find the id with `ncl messaging-groups list`).\n```\n```nc:operator when:inbound_access=public\nOpen line: anyone who knows {{platform_id}} can text the agent and will get a reply. Each person gets their own conversation, but they all reach an agent holding your Dial account credentials — so don't hand out this number casually. To lock it to just you later: `ncl messaging-groups update --id <id> --unknown-sender-policy strict` (find the id with `ncl messaging-groups list`).\n```\n\n## Restart\n\n### Restart the service\n\nRestart the service so it loads the Dial adapter, and wait for its CLI socket.\nThe adapter must be live and polling before pairing — it's the thing that\nobserves the code you text:\n\n```nc:run effect:restart\nbash setup/lib/restart.sh\n```\n\n### Start inbound delivery\n\nWire inbound event delivery and the command target. Both are best-effort: a\nsandbox/CI without a user-service supervisor can't run the `listen` daemon, but\noutbound still works and inbound can be started manually later (see\nTroubleshooting), so these never fail the run:\n\n```nc:run effect:external\nDIAL_USER_AGENT={{dial_ua}} dial listen install || true\n```\n\n### Register the command target\n\nPoint the daemon at the adapter's event handler (same best-effort rule):\n\n```nc:run effect:external\nDIAL_USER_AGENT={{dial_ua}} dial local-target add cmd \"$PWD/data/dial/handle-dial-event.sh\" || true\n```\n\n### Register the line (owner-only)\n\nRegister the line, carrying the access choice from above onto its own row. One\n`platform_id` serves many correspondents, so it's a group (`--is-group 1`) and\neach texter becomes a thread inside it. Idempotent — a re-run returns the\nexisting row, and does NOT reset a policy you have since changed with `ncl`:\n\n```nc:run effect:wire when:inbound_access=owner\nncl messaging-groups create --channel-type dial --platform-id {{platform_id}} --is-group 1 --name \"Dial {{platform_id}}\" --unknown-sender-policy strict\n```\n\n### Register the line (public)\n\nThe same row, open to anyone who texts it:\n\n```nc:run effect:wire when:inbound_access=public\nncl messaging-groups create --channel-type dial --platform-id {{platform_id}} --is-group 1 --name \"Dial {{platform_id}}\" --unknown-sender-policy public\n```\n\n## Pair your phone\n\nDial account auth carries no per-sender binding, so the agent proves you own the\nphone you'll text from with a one-time pairing handshake: it issues a 6-digit\ncode, you text those exact 6 digits to the Dial line, and the live adapter\nmatches them. Tell the user:\n\n```nc:operator\nA 6-digit pairing code (and a scannable QR) is about to appear in this terminal. From the phone you want to use, text just those 6 digits to your Dial line {{platform_id}} — or scan the QR, which opens Messages pre-filled so you just press Send.\n```\n\nRun the pairing handshake. It prints the code/QR, streams \"waiting…\" while it\nwatches for your text, and resolves the sender's number once the code matches:\n\n```nc:run effect:step capture:owner_handle=PAIRED_NUMBER\npnpm exec tsx setup/index.ts --step pair-dial -- --line {{platform_id}}\n```\n\n`owner_handle` (the phone you paired from) and `platform_id` (your Dial line) are\nwhat the owner-wiring step needs. The greeting goes out over your Dial line as\nsoon as pairing completes.\n\nNow set expectations about outbound SMS, before the operator hits it as a silent\ncarrier rejection. Calls both ways and inbound texts work from this moment; US\ncarriers drop outbound SMS from a number that has no 10DLC registration, and the\nadapter reports that as a delivery-failure notice rather than anything the\noperator can fix in NanoClaw:\n\n```nc:operator\nYour number {{platform_id}} can receive texts and calls now. To text US numbers it needs 10DLC registration: $25, usually 3-5 business days, here's the link: https://getdial.ai/dashboard/numbers\n```\n\n## Add phone superpowers (optional)\n\nShow the pitch as a boxed note, then ask — mirrors the old wizard's `p.note`\n+ confirm:\n\n```nc:operator\nAdd phone superpowers to your assistant? Say yes so your assistant can send SMS and make AI calls for you from every channel you use it on — Telegram, WhatsApp, and more.\n```\n```nc:prompt install_tool validate:^(yes|no)$\nInstall the Dial tool now?\n```\n\nIf yes, apply the `/add-dial-tool` skill now, as its own document describes. It\nasks which agents may use Dial (and blocks the rest at the gateway), puts the\nCLI and its skill in the agent image, and registers the account's key with\nOneCLI. It needs OneCLI; if that isn't set up it says so, and the channel\nstill works without the tool:\n\nThe tool's own document asks which agents may use Dial. Ask it here instead: a\nnested step's stdout is a pipe, so clack cannot echo what is typed into it, and\nthis skill owns the operator's terminal. List the groups, then collect the answer\nand hand it down:\n\n```nc:run capture:has_agents validate:^(yes|no)$ when:install_tool=yes effect:fetch\nncl groups list --json | jq -r 'if (.data|length)==0 then \"no\" else \"yes\" end'\n```\n```nc:operator when:has_agents=no\nNo agents exist yet — this install creates its first one in a moment, so there is nobody to choose between. Installing the tool for every agent; re-run `/add-dial-tool` any time to narrow that down.\n```\n```nc:run capture:agent_groups when:has_agents=yes effect:fetch\nncl groups list --json | jq -r '[.data[] | \"\\(.id) (\\(.name))\"] | join(\", \")'\n```\n```nc:operator when:has_agents=yes\nAgents on this install: {{agent_groups}}. Giving an agent Dial lets it text and call any number and buy numbers, billed to your Dial account. Agents you leave out are blocked at the gateway (reversible by running /add-dial-tool again). Agents created after this run have Dial until the next run.\n```\n```nc:prompt dial_agents validate:^(all|none|ag-[A-Za-z0-9-]+(,ag-[A-Za-z0-9-]+)*)$ normalize:trim when:has_agents=yes\nWhich agents may use Dial? Enter agent ids separated by commas with no spaces (the `ag-…` column), `all` for every agent, or `none` to install the tool with every agent blocked for now.\n```\n```nc:run effect:step when:has_agents=yes\npnpm exec tsx setup/lib/skill-driver.ts .claude/skills/add-dial-tool --input 'dial_agents={{dial_agents}}'\n```\n```nc:run effect:step when:has_agents=no\npnpm exec tsx setup/lib/skill-driver.ts .claude/skills/add-dial-tool --input 'dial_agents=all'\n```\n\nThen tell the sandboxed agent which line is its own. The container authenticates\nthrough the OneCLI proxy and has **no** auth file, so `defaultNumberId` is null\nin there — an agent that omits `--from-number` gets an error, and one that picks\nfrom `dial number list` gets whichever number sorts first, which is unrelated to\nwhat's wired. Only this skill knows the answer, so it has to write it down —\ninto the mounted skill file, which every container reads on its next spawn:\n\n```nc:run effect:external when:install_tool=yes\nif [ ! -f container/skills/dial-cli/SKILL.md ]; then echo \"dial-cli skill not installed (the tool installer did not complete) — skipping the wired-line note\"; else printf '\\n## This install'\"'\"'s line\\n\\nAlways pass `--from-number {{platform_id}}` on every `dial call` and `dial message`. That is the line this NanoClaw install is wired to; any other number on the account reaches nobody and replies to it are dropped.\\n' >> container/skills/dial-cli/SKILL.md && echo \"wired line recorded for the sandbox: {{platform_id}}\"; fi\n```\n\n## Next Steps\n\nIf you're in the middle of `/setup`, return to the setup flow now. Otherwise wire\nthis channel with `/init-first-agent` (or `/manage-channels`). To add a second\nDial number later, see the `/add-dial-number` skill.\n\n## Channel Info\n\n- **type**: `dial`\n- **terminology**: Dial calls it a \"number\" or \"line.\" One number is a single threaded line — each texter/caller gets their own thread.\n- **platform-id-format**: the bare E.164 number (e.g. `+14155550123`) — unlike prefixed channels, the number itself is the id.\n- **how-to-find-id**: Do NOT ask the user for an id. Dial registration uses pairing — run `pnpm exec tsx setup/index.ts --step pair-dial -- --line <E.164>`. The step prints a 6-digit code + QR; tell the user to text just those 6 digits to the Dial line. Success emits a `PAIR_DIAL` block with `STATUS=success`, `PLATFORM_ID` (the bare line), and `PAIRED_NUMBER` (the bare sender E.164). The service must be running — the adapter is what observes the code.\n- **supports-threads**: yes (each correspondent is a thread on the line, with its own session)\n- **typical-use**: A real phone number for SMS and AI-handled voice calls — receptionist, notifications, 2FA relay.\n- **default-isolation**: One line → one agent group. Who may reach it is the operator's choice at setup (`inbound_access`): `owner` admits only the paired phone, `public` admits everyone. Defaults to owner-only.\n\n## Troubleshooting\n\n**`dial: command not found` / the CLI gate fails.** The Dial CLI isn't on PATH. Run `curl -fsSL https://getdial.ai/skills.md` and follow its install steps, then re-run this step.\n\n**The email code never arrives.** Check spam, confirm the address is one you can read, and re-run — `dial auth login <email> --force` re-sends. The code is sent by Dial's servers, not NanoClaw.\n\n**Inbound texts/calls don't reach the agent.** `dial listen install` needs a user-service supervisor (launchd/systemd `--user`); sandboxes/CI don't have one. Outbound still works. Start it manually with `dial listen install` once a supervisor is available, and confirm the command target with `dial local-target list`.\n\n**Pairing never completes.** The live adapter observes the code, so the service must be running — the restart step comes before pairing for exactly this reason. Text *just* the 6 digits to the Dial line; a wrong message is ignored. Codes expire after 10 minutes, so if it times out (5 min) or the code goes stale, re-run this step for a fresh one.\n\n**\"Pairing is paused for about N min.\"** Five wrong codes texted to the line inside 10 minutes locks that line for 15 — a brute-force guard, and while it holds even the correct code is refused. The wizard prints this warning when it happens; nothing is texted back to the sender, by design. Wait it out and re-run this step for a fresh code, or override the thresholds with `DIAL_PAIRING_MAX_ATTEMPTS` / `DIAL_PAIRING_ATTEMPT_WINDOW_MS` / `DIAL_PAIRING_COOLDOWN_MS` / `DIAL_PAIRING_TTL_MS`.\n\n**Everything green but no replies.** Run `pnpm exec vitest run src/channels/dial-registration.test.ts` — red means the barrel import drifted, so re-run the Apply steps. If green, restart again (`bash setup/lib/restart.sh`) and check `logs/nanoclaw.error.log`.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-dial","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-dial/SKILL.md","defaultBranch":"main"},"readme":"# Add Dial Channel\n\nAdds [Dial](https://getdial.ai) — a real phone number for **SMS and AI voice\ncalls**. Native adapter (no Chat SDK bridge): both directions go through the\n`dial` CLI — outbound via `dial message`, inbound via its command-target daemon. NanoClaw doesn't ship\nchannels in trunk — this skill copies the Dial adapter, its pairing helper, and\ntheir tests in from the `channels` branch. The `pair-dial` setup step is\nmaintained in trunk, so it is not copied here.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent\nreads the prose and applies them, and a parser can apply them deterministically\nfrom the same document. Every directive is idempotent, so the whole skill is\nsafe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Apply\n\n### 1. Copy the adapter, pairing helper, and tests\n\nFetch the `channels` branch and copy the Dial adapter, its pairing store and\nuser-agent helper (each with its test), and the registration test into place\n(overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/dial.ts\nsrc/channels/dial-pairing.ts\nsrc/channels/dial-pairing.test.ts\nsrc/channels/dial-user-agent.ts\nsrc/channels/dial-user-agent.test.ts\nsrc/channels/dial-registration.test.ts\nsrc/channels/dial-grant.test.ts\nsrc/channels/dial-status.test.ts\n```\n\nThe `dial-cli` container skill is deliberately **not** copied here.\n`container/skills/` is mounted read-only into *every* agent container\n(`src/container-runner.ts`), and a group with `skills:'all'` picks up whatever\nit finds there — so shipping the skill with the adapter would hand it to agents\non installs that never configured Dial. It is installed only by\n`/add-dial-tool`, offered under *Add phone superpowers* below, which is the\nskill that actually provisions the CLI the skill documents.\n\n`dial.ts` imports `dial-user-agent.js` at module scope, so omitting that helper\nbreaks the build and every test that loads the channel barrel.\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if present).\nThis one line is the skill's only reach-in into the channel core:\n\n```nc:append to:src/channels/index.ts\nimport './dial.js';\n```\n\n### 3. Register the pairing setup step\n\nAdd the `pair-dial` loader to the `STEPS` map in `setup/index.ts`, inside the\ndormant marker region (skipped if already present — `pair-dial` ships in core, so\nthis idempotent-skips on a normal install, but is expressed for a clean-upstream\nrebuild). The pairing handshake below spawns this step:\n\n```nc:append to:setup/index.ts at:nanoclaw:setup-steps\n'pair-dial': () => import('./pair-dial.js'),\n```\n\n### 4. Install the packages\n\nPinned to exact versions — the supply-chain policy rejects ranges and `latest`.\n`qrcode` renders the scannable pairing card:\n\n```nc:dep\nqrcode@1.5.4\n```\n\nThe adapter needs no Dial client library: it shells out to the `dial` CLI, which\nthis skill already requires for inbound. `@getdial/sdk` was dropped because it\ndepends on `pubnub`, which pulls react-native, Metro and Hermes into the\nlockfile for what is a single send — and the CLI ships in lockstep with the Dial\nAPI, so a contract change arrives as a CLI release rather than breaking a\nrequest pinned in the adapter.\n\n### 5. Build\n\nBuild first: it guards the adapter's typed core calls and proves the dependency\nis installed.\n\n```nc:run effect:build\npnpm run build\n```\n\n### 6. Validate\n\nThen run the one integration test.\n\n```nc:run effect:test\npnpm exec vitest run src/channels/dial-registration.test.ts\n```\n\n`dial-registration.test.ts` imports the real channel barrel and asserts the\nregistry contains `dial` — it goes red if the import line drifts. End-to-end\nSMS/voice is verified manually once the service runs.\n\n## Sign in to Dial\n\n### Install the CLI\n\nDial's CLI owns the account credential (an auth file it writes on sign-in), so\nthe setup uses the `dial` CLI here. Ensure it's installed — this installs it if\nit's missing (for the full onb","createdAt":"2026-09-25T10:52:06.599Z","updatedAt":"2026-09-25T10:52:06.599Z"},{"id":"cmugucuaa00jbqu069dbjbtbn","slug":"nanocoai-nanoclaw-add-discord","name":"add-discord","description":"Add Discord bot channel integration via Chat SDK.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-discord","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add Discord bot channel integration via Chat SDK.","permissions":[],"systemPrompt":"# Add Discord Channel\n\nAdds Discord bot support via the Chat SDK bridge. NanoClaw doesn't ship channels\nin trunk — this skill copies the Discord adapter in from the `channels` branch.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent\nreads the prose and applies them, and a parser can apply them deterministically\nfrom the same document. Every directive is idempotent, so the whole skill is\nsafe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Apply\n\n### 1. Copy the adapter and its registration test\n\nFetch the `channels` branch and copy the Discord adapter and its registration\ntest into `src/channels/` (overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/discord.ts\nsrc/channels/discord-registration.test.ts\n```\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './discord.js';\n```\n\n### 3. Install the adapter package\n\nPinned to an exact version — the supply-chain policy rejects ranges and `latest`:\n\n```nc:dep\n@chat-adapter/discord@4.29.0\n```\n\n### 4. Build and validate\n\nBuild first: it guards the typed `createChatSdkBridge(...)` core call and proves\nthe dependency is installed. Then run the one integration test.\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/discord-registration.test.ts\n```\n\n`discord-registration.test.ts` imports the real channel barrel and asserts the\nregistry contains `discord`. It goes red if the import line is deleted or drifts,\nif the barrel fails to evaluate, or if `@chat-adapter/discord` isn't installed\n(the import throws) — so it also covers the dependency from step 3. End-to-end\ndelivery against a real server is verified manually once the service runs.\n\n## Credentials\n\nDiscord app setup is human and interactive — no parser can click through the\nDiscord Developer Portal. The adapter is installed and registered, but it can't\nreceive a message until the bot exists, has Message Content Intent, and shares a\nserver with you. Tell the user:\n\n```nc:operator\nCreate the Discord bot:\n1. Go to https://discord.com/developers/applications → New Application. Name it (e.g. \"NanoClaw Assistant\").\n2. Bot tab → Add Bot if needed → Reset Token, then copy the Bot Token (it's shown only once).\n3. Bot tab → Privileged Gateway Intents → enable Message Content Intent.\n4. OAuth2 → URL Generator → Scopes: bot; Bot Permissions: Send Messages, Read Message History, Add Reactions, Attach Files, Use Slash Commands.\n5. Open the generated URL and invite the bot to a server you're also in (a personal server is fine) — the bot can only DM you once you share a server.\n```\n\nPaste the Bot Token (it's shown only once). You don't paste the Application ID or\nthe Public Key by hand — the bot's own application record carries both, so a\nsingle call derives them from the token:\n\n```nc:prompt bot_token secret validate:^[A-Za-z0-9._-]{50,}$\nPaste the Bot Token — Bot tab. Click `Reset Token` if you need a new one.\n```\n\nRead the application's own record. `GET /oauth2/applications/@me` returns the\nApplication ID (`id`), the Public Key (`verify_key`), and your own account as the\napp's owner (`owner.id`) — so the App ID, the Public Key, and your Discord user ID\nall come from this one call instead of being copied by hand. A bad token fails\nhere, before the restart, rather than silently later:\n\n```nc:run capture:application_id=.id,public_key=.verify_key,owner_handle=.owner.id effect:fetch\ncurl -sf https://discord.com/api/v10/oauth2/applications/@me -H \"Authorization: Bot {{bot_token}}\"\n```\n\nStore the token and the two derived credentials — the adapter reads them from\n`.env` and fails to start without `DISCORD_PUBLIC_KEY` and `DISCORD_APPLICATION_ID`\n(set-if-absent, so a value you've already filled in is never overwritten):\n\n```nc:env-set\nDISCORD_BOT_TOKEN={{bot_token}}\nDISCORD_APPLICATION_ID={{application_id}}\nDISCORD_PUBLIC_KEY={{public_key}}\n```\n## Restart\n\nRestart the service so it loads the Discord adapter and the credentials you just\nstored, and wait for its CLI socket before resolving:\n\n```nc:run effect:restart\nbash setup/lib/restart.sh\n```\n\n## Invite the bot to a shared server\n\nThe bot can only DM you once it shares a server with you. If you didn't already\ninvite it via the OAuth2 URL Generator while setting up the app, do it now: add\nthe bot to a server you're also in (a personal server is fine). Tell the user:\n\n```nc:operator\nOpen the invite link — https://discord.com/oauth2/authorize?client_id={{application_id}}&scope=bot&permissions=2147584064 — and add the bot to a server you're also in (a personal server works fine); the bot can only DM you once you share a server. If you already invited it while setting up the app, you can skip this.\n```\n\n## Resolve your DM channel\n\nThe agent talks to you in your direct-message channel with the bot. Your Discord\nuser ID was already derived as the application's owner (`owner_handle`), so all\nthat's left is to open the DM and read back its channel id.\n\nOpen the DM with `POST /users/@me/channels` and take the channel id it returns as\nthe conversation address `discord:@me:<channelId>` (if Discord refuses, the bot\ndoesn't share a server with you yet — invite it, then retry):\n\n```nc:run capture:platform_id effect:fetch\ncurl -s -X POST https://discord.com/api/v10/users/@me/channels -H \"Authorization: Bot {{bot_token}}\" -H \"Content-Type: application/json\" -d '{\"recipient_id\":\"{{owner_handle}}\"}' | jq -er '\"discord:@me:\" + .id'\n```\n\n`owner_handle` and `platform_id` are what the owner-wiring step needs. The\ngreeting goes out over the DM channel, which works as soon as the bot shares a\nserver with you.\n\n## Next Steps\n\nIf you're in the middle of `/setup`, return to the setup flow now. Otherwise wire\nthis channel with `/init-first-agent` (or `/manage-channels`).\n\n## Channel Info\n\n- **type**: `discord`\n- **terminology**: Discord has \"servers\" (also called \"guilds\") containing \"channels.\" Text channels start with #. The bot can also receive direct messages.\n- **platform-id-format**: `discord:@me:{dmChannelId}` for the owner DM (e.g. `discord:@me:1399...`), `discord:{guildId}:{channelId}` for server channels — both IDs required for channels.\n- **how-to-find-id**: Enable Developer Mode in Discord (Settings > App Settings > Advanced > Developer Mode). Then right-click a server and select \"Copy Server ID\" for the guild ID, and right-click the text channel and select \"Copy Channel ID.\" The platform ID format used in registration is `discord:{guildId}:{channelId}` — both IDs are required.\n- **supports-threads**: yes\n- **typical-use**: Interactive chat — server channels or direct messages\n- **default-isolation**: Same agent group for your personal server. Separate agent group for servers with different communities or where different members have different information boundaries.\n\n## Troubleshooting\n\n**The Bot Token paste is rejected.** The token must be at least 50 characters of letters, digits, dots, underscores, and hyphens — a real Bot Token has two `.` separators. It lives under **Bot → Reset Token** in the Developer Portal and is shown only once; reset to get a fresh one. The short numeric **Application ID** and the **OAuth2 Client Secret** are different values and won't pass.\n\n**`applications/@me` returns 401.** The token was reset since you copied it, or a stray space/newline came along with the paste. Reset the token in the Bot tab and re-run the check — it fails here on purpose, before the restart, while the credential is still cheap to fix.\n\n**The bot is online but never sees your messages.** Two usual causes: Message Content Intent is off (Bot tab → Privileged Gateway Intents), so message bodies arrive empty and nothing triggers; or the bot doesn't share a server with you — in which case `POST /users/@me/channels` also refuses. Open the invite URL and add the bot to a server you're in, then retry.\n\n**Adapter looks installed but Discord never connects.** Run `pnpm exec vitest run src/channels/discord-registration.test.ts` — red means the barrel import or the `@chat-adapter/discord` install drifted, so re-run the Apply steps. If it's green, the service probably hasn't restarted since the credentials were stored: `bash setup/lib/restart.sh`, then check `logs/nanoclaw.error.log` for missing `DISCORD_PUBLIC_KEY` / `DISCORD_APPLICATION_ID` complaints.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-discord","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-discord/SKILL.md","defaultBranch":"main"},"readme":"# Add Discord Channel\n\nAdds Discord bot support via the Chat SDK bridge. NanoClaw doesn't ship channels\nin trunk — this skill copies the Discord adapter in from the `channels` branch.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent\nreads the prose and applies them, and a parser can apply them deterministically\nfrom the same document. Every directive is idempotent, so the whole skill is\nsafe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Apply\n\n### 1. Copy the adapter and its registration test\n\nFetch the `channels` branch and copy the Discord adapter and its registration\ntest into `src/channels/` (overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/discord.ts\nsrc/channels/discord-registration.test.ts\n```\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './discord.js';\n```\n\n### 3. Install the adapter package\n\nPinned to an exact version — the supply-chain policy rejects ranges and `latest`:\n\n```nc:dep\n@chat-adapter/discord@4.29.0\n```\n\n### 4. Build and validate\n\nBuild first: it guards the typed `createChatSdkBridge(...)` core call and proves\nthe dependency is installed. Then run the one integration test.\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/discord-registration.test.ts\n```\n\n`discord-registration.test.ts` imports the real channel barrel and asserts the\nregistry contains `discord`. It goes red if the import line is deleted or drifts,\nif the barrel fails to evaluate, or if `@chat-adapter/discord` isn't installed\n(the import throws) — so it also covers the dependency from step 3. End-to-end\ndelivery against a real server is verified manually once the service runs.\n\n## Credentials\n\nDiscord app setup is human and interactive — no parser can click through the\nDiscord Developer Portal. The adapter is installed and registered, but it can't\nreceive a message until the bot exists, has Message Content Intent, and shares a\nserver with you. Tell the user:\n\n```nc:operator\nCreate the Discord bot:\n1. Go to https://discord.com/developers/applications → New Application. Name it (e.g. \"NanoClaw Assistant\").\n2. Bot tab → Add Bot if needed → Reset Token, then copy the Bot Token (it's shown only once).\n3. Bot tab → Privileged Gateway Intents → enable Message Content Intent.\n4. OAuth2 → URL Generator → Scopes: bot; Bot Permissions: Send Messages, Read Message History, Add Reactions, Attach Files, Use Slash Commands.\n5. Open the generated URL and invite the bot to a server you're also in (a personal server is fine) — the bot can only DM you once you share a server.\n```\n\nPaste the Bot Token (it's shown only once). You don't paste the Application ID or\nthe Public Key by hand — the bot's own application record carries both, so a\nsingle call derives them from the token:\n\n```nc:prompt bot_token secret validate:^[A-Za-z0-9._-]{50,}$\nPaste the Bot Token — Bot tab. Click `Reset Token` if you need a new one.\n```\n\nRead the application's own record. `GET /oauth2/applications/@me` returns the\nApplication ID (`id`), the Public Key (`verify_key`), and your own account as the\napp's owner (`owner.id`) — so the App ID, the Public Key, and your Discord user ID\nall come from this one call instead of being copied by hand. A bad token fails\nhere, before the restart, rather than silently later:\n\n```nc:run capture:application_id=.id,public_key=.verify_key,owner_handle=.owner.id effect:fetch\ncurl -sf https://discord.com/api/v10/oauth2/applications/@me -H \"Authorization: Bot {{bot_token}}\"\n```\n\nStore the token and the two derived credentials — the adapter reads them from\n`.env` and fails to start without `DISCORD_PUBLIC_KEY` and `DISCORD_APPLICATION_ID`\n(set-if-absent, so a value you've already filled in is never overwritten):\n\n```nc:env-set\nDISCORD","createdAt":"2026-09-25T10:52:06.611Z","updatedAt":"2026-09-25T10:52:06.611Z"},{"id":"cmugucuam00jequ068ckcn8vx","slug":"nanocoai-nanoclaw-add-emacs","name":"add-emacs","description":"Add Emacs as a channel. Opens an interactive chat buffer and org-mode integration so you can talk to NanoClaw from within Emacs (Doom, Spacemacs, or vanilla). Local HTTP bridge — no bot token or external service needed.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-emacs","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add Emacs as a channel. Opens an interactive chat buffer and org-mode integration so you can talk to NanoClaw from within Emacs (Doom, Spacemacs, or vanilla). Local HTTP bridge — no bot token or external service needed.","permissions":[],"systemPrompt":"# Add Emacs Channel\n\nAdds Emacs support via a local HTTP bridge. Works with Doom Emacs, Spacemacs, and vanilla Emacs 27.1+.\n\n## What you can do with this\n\n- **Ask while coding** — open the chat buffer (`C-c n c` / `SPC N c`), ask about a function or error without leaving Emacs\n- **Code review** — select a region and send it with `nanoclaw-org-send`; the response appears as a child heading inline in your org file\n- **Meeting notes** — send an org agenda entry; get a summary or action item list back as a child node\n- **Draft writing** — send org prose; receive revisions or continuations in place\n- **Research capture** — ask a question directly in your org notes; the answer lands exactly where you need it\n\n## Install\n\nNanoClaw doesn't ship channels in trunk. This skill copies the Emacs adapter and the Lisp client in from the `channels` branch. Native HTTP bridge — no Chat SDK, no adapter package.\n\n### 1. Copy the adapter and Lisp client\n\nFetch the `channels` branch from the configured remote that carries it, then\noverwrite the skill-owned files with the canonical registry copies:\n\n```nc:copy from-branch:channels\nsrc/channels/emacs.ts\nsrc/channels/emacs.test.ts\nsrc/channels/emacs-registration.test.ts\nemacs/nanoclaw.el\n```\n\n### 2. Append the self-registration import\n\nAppend to `src/channels/index.ts` (skip if the line is already present):\n\n```nc:append to:src/channels/index.ts\nimport './emacs.js';\n```\n\n### 3. Build and validate\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/emacs-registration.test.ts\n```\n\nBoth must be clean before proceeding. `emacs-registration.test.ts` is the one integration test: it imports the real channel barrel and asserts the registry contains `emacs`. It goes red if the `import './emacs.js';` line is deleted or drifts, or if the barrel fails to evaluate (so the channel genuinely would not register). The adapter uses only Node builtins (`http`), so there is no npm dependency to guard for this channel.\n\nEnd-to-end message delivery from a real Emacs buffer is verified manually once the service is running — see Verify and Troubleshooting.\n\n## Enable\n\nThe adapter is gated by `EMACS_ENABLED` so the HTTP port isn't opened on hosts that aren't running Emacs. Add to `.env`:\n\n```bash\nEMACS_ENABLED=true\nEMACS_CHANNEL_PORT=8766       # optional — change only if 8766 is taken\nEMACS_AUTH_TOKEN=             # optional — set to a random string to lock the endpoint\nEMACS_PLATFORM_ID=default     # optional — only change if you want a non-default chat id\n```\n\nGenerate an auth token (recommended even on single-user machines — prevents other local processes from poking the endpoint):\n\n```bash\nnode -e \"console.log(require('crypto').randomBytes(16).toString('hex'))\"\n```\n\n## Wire the channel\n\nEmacs is a single-user, single-chat channel. One host = one messaging group with `platform_id = \"default\"`.\n\n### If this is your first agent group\n\nRun `/init-first-agent` — pick **Emacs** as the channel, use any short handle as the \"user id\" (e.g. your OS username), and the skill will create the agent group, wire the channel, and write a welcome message that the agent delivers back to your Emacs buffer.\n\n### Otherwise — wire to an existing agent group\n\nRun the `register` step directly. The `EMACS_PLATFORM_ID` (default `default`) becomes the messaging group's platform id:\n\n```bash\npnpm exec tsx setup/index.ts --step register -- \\\n  --platform-id \"default\" --name \"Emacs\" \\\n  --folder \"<existing-folder>\" --channel \"emacs\" \\\n  --session-mode \"agent-shared\" \\\n  --assistant-name \"<existing-assistant-name>\"\n```\n\n`agent-shared` puts Emacs messages in the same session as any other channel wired to the same agent group — so a conversation you started in Telegram continues in Emacs. Use `shared` to keep an independent Emacs thread with the same workspace, or a new `--folder` for a dedicated Emacs-only agent.\n\nAlternatively create the rows with `ncl` — **the host service must be running** (`ncl` connects to it over a Unix socket). Engage mode/pattern and `unknown_sender_policy` default to the Emacs adapter's declared channel defaults:\n\n```bash\nncl messaging-groups create --channel-type emacs --platform-id \"default\" --name \"Emacs\"\nncl wirings create --messaging-group-id <mg-id-from-above> --agent-group-id <ag-id> \\\n  --session-mode agent-shared\n```\n\n## Configure Emacs\n\n`nanoclaw.el` needs only Emacs 27.1+ builtins (`url`, `json`, `org`) — no package manager.\n\nAskUserQuestion: Which Emacs distribution are you using?\n- **Doom Emacs** — `config.el` with `map!` keybindings\n- **Spacemacs** — `dotspacemacs/user-config` in `~/.spacemacs`\n- **Vanilla Emacs / other** — `init.el` with `global-set-key`\n\n**Doom Emacs** — add to `~/.config/doom/config.el` (or `~/.doom.d/config.el`):\n\n```elisp\n;; NanoClaw — personal AI assistant channel\n(load (expand-file-name \"~/src/nanoclaw/emacs/nanoclaw.el\"))\n\n(map! :leader\n      :prefix (\"N\" . \"NanoClaw\")\n      :desc \"Chat buffer\"  \"c\" #'nanoclaw-chat\n      :desc \"Send org\"     \"o\" #'nanoclaw-org-send)\n```\n\nReload: `M-x doom/reload`\n\n**Spacemacs** — add to `dotspacemacs/user-config` in `~/.spacemacs`:\n\n```elisp\n;; NanoClaw — personal AI assistant channel\n(load-file \"~/src/nanoclaw/emacs/nanoclaw.el\")\n\n(spacemacs/set-leader-keys \"aNc\" #'nanoclaw-chat)\n(spacemacs/set-leader-keys \"aNo\" #'nanoclaw-org-send)\n```\n\nReload: `M-x dotspacemacs/sync-configuration-layers` or restart Emacs.\n\n**Vanilla Emacs** — add to `~/.emacs.d/init.el`:\n\n```elisp\n;; NanoClaw — personal AI assistant channel\n(load-file \"~/src/nanoclaw/emacs/nanoclaw.el\")\n\n(global-set-key (kbd \"C-c n c\") #'nanoclaw-chat)\n(global-set-key (kbd \"C-c n o\") #'nanoclaw-org-send)\n```\n\nReload: `M-x eval-buffer` or restart Emacs.\n\nReplace `~/src/nanoclaw/emacs/nanoclaw.el` with your actual NanoClaw checkout path.\n\nIf `EMACS_AUTH_TOKEN` is set, also add (any distribution):\n\n```elisp\n(setq nanoclaw-auth-token \"<your-token>\")\n```\n\nIf you changed `EMACS_CHANNEL_PORT` from the default:\n\n```elisp\n(setq nanoclaw-port <your-port>)\n```\n\n## Restart NanoClaw\n\nRun from your NanoClaw project root:\n\n```bash\npnpm run build\nsource setup/lib/install-slug.sh\nlaunchctl kickstart -k gui/$(id -u)/$(launchd_label)   # macOS\n# systemctl --user restart $(systemd_unit)             # Linux\n```\n\n## Verify\n\n### HTTP endpoint\n\n```bash\ncurl -s http://localhost:8766/api/messages?since=0\n```\n\nExpected: `{\"messages\":[]}`. With an auth token:\n\n```bash\ncurl -s -H \"Authorization: Bearer <token>\" http://localhost:8766/api/messages?since=0\n```\n\n### From Emacs\n\nTell the user:\n\n> 1. Open the chat buffer with your keybinding (`SPC N c`, `SPC a N c`, or `C-c n c`)\n> 2. Type a message and press `C-c C-c` to send (RET inserts newlines)\n> 3. A response should appear within a few seconds\n>\n> For org-mode: open any `.org` file, position the cursor on a heading, and use `SPC N o` / `SPC a N o` / `C-c n o`\n\n### Log line\n\n`tail -f logs/nanoclaw.log` should show `Emacs channel listening` at startup.\n\n## Channel Info\n\n- **type**: `emacs`\n- **terminology**: Single local buffer. There are no \"groups\" or separate chats — one host = one chat, addressed by a `platform_id` string (default `default`).\n- **how-to-find-id**: The platform id is whatever you set in `EMACS_PLATFORM_ID` (default `default`). User handles are arbitrary; your OS username or first name is fine (e.g. `emacs:<username>`).\n- **supports-threads**: no\n- **typical-use**: Single developer talking to the assistant from within Emacs, alongside whatever other channel they use (Slack, Telegram, Discord).\n- **default-isolation**: Same agent group as the primary DM, with `session-mode = agent-shared` so a conversation started elsewhere continues in Emacs. Pick a separate folder only if you specifically want an Emacs-only persona.\n\n### Features\n\n- Interactive chat buffer (`nanoclaw-chat`) with markdown → org-mode rendering\n- Org integration (`nanoclaw-org-send`) — sends the current subtree or region; reply lands as a child heading\n- Optional bearer-token auth for the local endpoint\n- Single-user: the adapter exposes exactly one messaging group per host\n\nNot applicable (design): multi-user channels, threads, cold DM initiation, typing indicators, attachments.\n\n## Troubleshooting\n\n### Port already in use\n\n```\nError: listen EADDRINUSE: address already in use :::8766\n```\n\nEither a stale NanoClaw is running or another app has the port. Kill stale process or change port:\n\n```bash\nlsof -ti :8766 | xargs kill -9\n# or set EMACS_CHANNEL_PORT in .env and mirror in Emacs config (nanoclaw-port)\n```\n\n### Adapter not starting\n\nIf `grep \"Emacs channel listening\" logs/nanoclaw.log` returns nothing, check that `EMACS_ENABLED=true` is in `.env` and that the adapter import is present:\n\n```bash\ngrep -q '^EMACS_ENABLED=true' .env && echo \"enabled\" || echo \"not enabled\"\ngrep -q \"import './emacs.js'\" src/channels/index.ts && echo \"imported\" || echo \"not imported\"\n```\n\n### No response from agent\n\n1. NanoClaw running: `launchctl list | grep \"$(. setup/lib/install-slug.sh && launchd_label)\"` (macOS) / `systemctl --user status \"$(. setup/lib/install-slug.sh && systemd_unit)\"` (Linux)\n2. Messaging group wired: `pnpm exec tsx scripts/q.ts data/v2.db \"SELECT mg.platform_id, ag.folder FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id = mga.messaging_group_id JOIN agent_groups ag ON ag.id = mga.agent_group_id WHERE mg.channel_type = 'emacs'\"`\n3. Logs show inbound: `grep 'channel_type=emacs\\|Emacs' logs/nanoclaw.log | tail -20`\n\nIf no messaging group row exists, run the `register` command above.\n\n### Auth token mismatch (401 Unauthorized)\n\n```elisp\nM-x describe-variable RET nanoclaw-auth-token RET\n```\n\nMust match `EMACS_AUTH_TOKEN` in `.env`. If you didn't set one server-side, clear it in Emacs too:\n\n```elisp\n(setq nanoclaw-auth-token nil)\n```\n\n### nanoclaw.el not loading\n\n```bash\nls ~/src/nanoclaw/emacs/nanoclaw.el\n```\n\nIf NanoClaw is cloned elsewhere, update the `load`/`load-file` path in your Emacs config.\n\n## Agent Formatting\n\nThe Emacs bridge converts markdown → org-mode automatically. Agents should output standard markdown, **not** org-mode syntax:\n\n| Markdown | Org-mode |\n|----------|----------|\n| `**bold**` | `*bold*` |\n| `*italic*` | `/italic/` |\n| `~~text~~` | `+text+` |\n| `` `code` `` | `~code~` |\n| ` ```lang ` | `#+begin_src lang` |\n\nIf an agent outputs org-mode directly, markers get double-converted and render incorrectly.\n\n## Removal\n\nSee [REMOVE.md](REMOVE.md) to uninstall this channel.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-emacs","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-emacs/SKILL.md","defaultBranch":"main"},"readme":"# Add Emacs Channel\n\nAdds Emacs support via a local HTTP bridge. Works with Doom Emacs, Spacemacs, and vanilla Emacs 27.1+.\n\n## What you can do with this\n\n- **Ask while coding** — open the chat buffer (`C-c n c` / `SPC N c`), ask about a function or error without leaving Emacs\n- **Code review** — select a region and send it with `nanoclaw-org-send`; the response appears as a child heading inline in your org file\n- **Meeting notes** — send an org agenda entry; get a summary or action item list back as a child node\n- **Draft writing** — send org prose; receive revisions or continuations in place\n- **Research capture** — ask a question directly in your org notes; the answer lands exactly where you need it\n\n## Install\n\nNanoClaw doesn't ship channels in trunk. This skill copies the Emacs adapter and the Lisp client in from the `channels` branch. Native HTTP bridge — no Chat SDK, no adapter package.\n\n### 1. Copy the adapter and Lisp client\n\nFetch the `channels` branch from the configured remote that carries it, then\noverwrite the skill-owned files with the canonical registry copies:\n\n```nc:copy from-branch:channels\nsrc/channels/emacs.ts\nsrc/channels/emacs.test.ts\nsrc/channels/emacs-registration.test.ts\nemacs/nanoclaw.el\n```\n\n### 2. Append the self-registration import\n\nAppend to `src/channels/index.ts` (skip if the line is already present):\n\n```nc:append to:src/channels/index.ts\nimport './emacs.js';\n```\n\n### 3. Build and validate\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/emacs-registration.test.ts\n```\n\nBoth must be clean before proceeding. `emacs-registration.test.ts` is the one integration test: it imports the real channel barrel and asserts the registry contains `emacs`. It goes red if the `import './emacs.js';` line is deleted or drifts, or if the barrel fails to evaluate (so the channel genuinely would not register). The adapter uses only Node builtins (`http`), so there is no npm dependency to guard for this channel.\n\nEnd-to-end message delivery from a real Emacs buffer is verified manually once the service is running — see Verify and Troubleshooting.\n\n## Enable\n\nThe adapter is gated by `EMACS_ENABLED` so the HTTP port isn't opened on hosts that aren't running Emacs. Add to `.env`:\n\n```bash\nEMACS_ENABLED=true\nEMACS_CHANNEL_PORT=8766       # optional — change only if 8766 is taken\nEMACS_AUTH_TOKEN=             # optional — set to a random string to lock the endpoint\nEMACS_PLATFORM_ID=default     # optional — only change if you want a non-default chat id\n```\n\nGenerate an auth token (recommended even on single-user machines — prevents other local processes from poking the endpoint):\n\n```bash\nnode -e \"console.log(require('crypto').randomBytes(16).toString('hex'))\"\n```\n\n## Wire the channel\n\nEmacs is a single-user, single-chat channel. One host = one messaging group with `platform_id = \"default\"`.\n\n### If this is your first agent group\n\nRun `/init-first-agent` — pick **Emacs** as the channel, use any short handle as the \"user id\" (e.g. your OS username), and the skill will create the agent group, wire the channel, and write a welcome message that the agent delivers back to your Emacs buffer.\n\n### Otherwise — wire to an existing agent group\n\nRun the `register` step directly. The `EMACS_PLATFORM_ID` (default `default`) becomes the messaging group's platform id:\n\n```bash\npnpm exec tsx setup/index.ts --step register -- \\\n  --platform-id \"default\" --name \"Emacs\" \\\n  --folder \"<existing-folder>\" --channel \"emacs\" \\\n  --session-mode \"agent-shared\" \\\n  --assistant-name \"<existing-assistant-name>\"\n```\n\n`agent-shared` puts Emacs messages in the same session as any other channel wired to the same agent group — so a conversation you started in Telegram continues in Emacs. Use `shared` to keep an independent Emacs thread with the same workspace, or a new `--folder` for a dedicated Emacs-only agent.\n\nAlternatively create the rows with `ncl` — **the host service must be running** (`ncl` connec","createdAt":"2026-09-25T10:52:06.622Z","updatedAt":"2026-09-25T10:52:06.622Z"},{"id":"cmugucuaw00jhqu0642zw8mg0","slug":"nanocoai-nanoclaw-add-gchat","name":"add-gchat","description":"Add Google Chat channel integration via Chat SDK.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-gchat","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add Google Chat channel integration via Chat SDK.","permissions":[],"systemPrompt":"# Add Google Chat Channel\n\nAdds Google Chat support via the Chat SDK bridge. NanoClaw doesn't ship channels\nin trunk — this skill copies the Google Chat adapter in from the `channels`\nbranch.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent\nreads the prose and applies them, and a parser can apply them deterministically\nfrom the same document. Every directive is idempotent, so the whole skill is\nsafe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Apply\n\n### 1. Copy the adapter and its registration test\n\nFetch the `channels` branch and copy the Google Chat adapter and its\nregistration test into `src/channels/` (overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/gchat.ts\nsrc/channels/gchat-registration.test.ts\n```\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './gchat.js';\n```\n\n### 3. Install the adapter package\n\nPinned to an exact version — the supply-chain policy rejects ranges and `latest`:\n\n```nc:dep\n@chat-adapter/gchat@4.29.0\n```\n\n### 4. Build and validate\n\nBuild first: it guards the typed `createChatSdkBridge(...)` core call and proves\nthe dependency is installed. Then run the one integration test.\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/gchat-registration.test.ts\n```\n\n`gchat-registration.test.ts` imports the real channel barrel and asserts the\nregistry contains `gchat`. It goes red if the import line is deleted or drifts,\nif the barrel fails to evaluate, or if `@chat-adapter/gchat` isn't installed (the\nimport throws) — so it also covers the dependency from step 3. End-to-end\ndelivery against a real Google Chat space is verified manually once the service\nruns — see Credentials and Next Steps.\n\n## Credentials\n\nGoogle Cloud setup is human and interactive — these steps are prose, not\ndirectives (no parser can click through the Google Cloud Console). A recipe\nrebuild produces a compiling, registered adapter that cannot receive a message\nuntil they're done.\n\n> 1. Go to [Google Cloud Console](https://console.cloud.google.com)\n> 2. Create or select a project\n> 3. Enable the **Google Chat API**\n> 4. Go to **Google Chat API** > **Configuration**:\n>    - App name and description\n>    - Connection settings: select **HTTP endpoint URL** and set to `https://your-domain/webhook/gchat`\n> 5. Create a **Service Account**:\n>    - Go to **IAM & Admin** > **Service Accounts** > **Create Service Account**\n>    - Grant the Chat Bot role\n>    - Create a JSON key and download it\n\n### Store the credentials\n\nCapture the service account JSON, then write it. `prompt` only *asks* and binds\nthe answer to a name; a separate directive consumes it — so the same prompt\ncould feed `ncl` or the OneCLI vault instead of `.env` by swapping only the\nconsumer. Here it goes to `.env` (set-if-absent — a value you've already filled\nin is never overwritten) as a single-line string:\n\n```nc:prompt gchat_credentials secret\nPaste the service account JSON as a single line — the key file you downloaded, e.g. `{\"type\":\"service_account\",\"project_id\":\"...\",\"private_key\":\"...\",\"client_email\":\"...\"}`.\n```\n```nc:env-set\nGCHAT_CREDENTIALS={{gchat_credentials}}\n```\n### Webhook server\n\nThe Chat SDK bridge automatically starts a shared webhook server on port 3000\n(`WEBHOOK_PORT` to change it), handling `/webhook/gchat`. This port must be\npublicly reachable for Google Chat to deliver events — it's the HTTP endpoint\nURL you set in the Connection settings above. Running locally, expose it with\nngrok (`ngrok http 3000`), a Cloudflare Tunnel, or a reverse proxy on a VPS.\n\n## Next Steps\n\nIf you're in the middle of `/setup`, return to the setup flow now. Otherwise run\n`/manage-channels` to wire this channel to an agent group.\n\n## Channel Info\n\n- **type**: `gchat`\n- **terminology**: Google Chat has \"spaces.\" A space can be a group conversation or a direct message with the bot.\n- **how-to-find-id**: Open the space in Google Chat, look at the URL — the space ID is the segment after `/space/` (e.g. `spaces/AAAA...`). Or use the Google Chat API to list spaces.\n- **supports-threads**: yes\n- **typical-use**: Interactive chat — team spaces or direct messages\n- **default-isolation**: Same agent group for spaces where you're the primary user. Separate agent group for spaces with different teams or sensitive contexts.\n\n## Troubleshooting\n\n**The adapter starts, then errors about credentials.** `GCHAT_CREDENTIALS` must be the *entire* service account JSON collapsed to one line — inspect `.env` and confirm it still contains `\"type\":\"service_account\"`, `\"private_key\"`, and `\"client_email\"`. A truncated paste (shells often mangle the multi-line private key) is the usual cause; download a fresh JSON key under **IAM & Admin → Service Accounts → Keys** and re-paste it as a single line.\n\n**Messages sent in the space never reach the agent.** Google Chat delivers only to the HTTP endpoint URL set under **Google Chat API → Configuration**, and that URL must be publicly reachable at `/webhook/gchat` (shared webhook server, port 3000). Tunnel hostnames (ngrok free tier) change on restart — make sure the Configuration URL matches the tunnel that's actually up.\n\n**The app doesn't appear when adding it to a space.** Check the Chat API Configuration page: the app status must be live and its visibility must include your domain or user, and you must be adding it from the same Google Workspace the Cloud project belongs to.\n\n**Everything configured but still silent.** Run `pnpm exec vitest run src/channels/gchat-registration.test.ts` — red means the barrel import or the `@chat-adapter/gchat` install drifted, so re-run the Apply steps. If green, restart the service so it picks up the adapter and `.env`, then watch `logs/nanoclaw.log` for the inbound webhook hit.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-gchat","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-gchat/SKILL.md","defaultBranch":"main"},"readme":"# Add Google Chat Channel\n\nAdds Google Chat support via the Chat SDK bridge. NanoClaw doesn't ship channels\nin trunk — this skill copies the Google Chat adapter in from the `channels`\nbranch.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent\nreads the prose and applies them, and a parser can apply them deterministically\nfrom the same document. Every directive is idempotent, so the whole skill is\nsafe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Apply\n\n### 1. Copy the adapter and its registration test\n\nFetch the `channels` branch and copy the Google Chat adapter and its\nregistration test into `src/channels/` (overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/gchat.ts\nsrc/channels/gchat-registration.test.ts\n```\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './gchat.js';\n```\n\n### 3. Install the adapter package\n\nPinned to an exact version — the supply-chain policy rejects ranges and `latest`:\n\n```nc:dep\n@chat-adapter/gchat@4.29.0\n```\n\n### 4. Build and validate\n\nBuild first: it guards the typed `createChatSdkBridge(...)` core call and proves\nthe dependency is installed. Then run the one integration test.\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/gchat-registration.test.ts\n```\n\n`gchat-registration.test.ts` imports the real channel barrel and asserts the\nregistry contains `gchat`. It goes red if the import line is deleted or drifts,\nif the barrel fails to evaluate, or if `@chat-adapter/gchat` isn't installed (the\nimport throws) — so it also covers the dependency from step 3. End-to-end\ndelivery against a real Google Chat space is verified manually once the service\nruns — see Credentials and Next Steps.\n\n## Credentials\n\nGoogle Cloud setup is human and interactive — these steps are prose, not\ndirectives (no parser can click through the Google Cloud Console). A recipe\nrebuild produces a compiling, registered adapter that cannot receive a message\nuntil they're done.\n\n> 1. Go to [Google Cloud Console](https://console.cloud.google.com)\n> 2. Create or select a project\n> 3. Enable the **Google Chat API**\n> 4. Go to **Google Chat API** > **Configuration**:\n>    - App name and description\n>    - Connection settings: select **HTTP endpoint URL** and set to `https://your-domain/webhook/gchat`\n> 5. Create a **Service Account**:\n>    - Go to **IAM & Admin** > **Service Accounts** > **Create Service Account**\n>    - Grant the Chat Bot role\n>    - Create a JSON key and download it\n\n### Store the credentials\n\nCapture the service account JSON, then write it. `prompt` only *asks* and binds\nthe answer to a name; a separate directive consumes it — so the same prompt\ncould feed `ncl` or the OneCLI vault instead of `.env` by swapping only the\nconsumer. Here it goes to `.env` (set-if-absent — a value you've already filled\nin is never overwritten) as a single-line string:\n\n```nc:prompt gchat_credentials secret\nPaste the service account JSON as a single line — the key file you downloaded, e.g. `{\"type\":\"service_account\",\"project_id\":\"...\",\"private_key\":\"...\",\"client_email\":\"...\"}`.\n```\n```nc:env-set\nGCHAT_CREDENTIALS={{gchat_credentials}}\n```\n### Webhook server\n\nThe Chat SDK bridge automatically starts a shared webhook server on port 3000\n(`WEBHOOK_PORT` to change it), handling `/webhook/gchat`. This port must be\npublicly reachable for Google Chat to deliver events — it's the HTTP endpoint\nURL you set in the Connection settings above. Running locally, expose it with\nngrok (`ngrok http 3000`), a Cloudflare Tunnel, or a reverse proxy on a VPS.\n\n## Next Steps\n\nIf you're in the middle of `/setup`, return to the setup flow now. Otherwise run\n`/manage-channels` to wire this channel to an agent group.\n\n## Channel Info\n\n- **type**: `gch","createdAt":"2026-09-25T10:52:06.633Z","updatedAt":"2026-09-25T10:52:06.633Z"},{"id":"cmugucub400jkqu06z7td0hnh","slug":"nanocoai-nanoclaw-add-github","name":"add-github","description":"Add GitHub channel integration via Chat SDK. PR and issue comment threads as conversations.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-github","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add GitHub channel integration via Chat SDK. PR and issue comment threads as conversations.","permissions":[],"systemPrompt":"# Add GitHub Channel\n\nAdds GitHub support via the Chat SDK bridge. The agent participates in PR and\nissue comment threads. NanoClaw doesn't ship channels in trunk — this skill\ncopies the GitHub adapter in from the `channels` branch.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent\nreads the prose and applies them, and a parser can apply them deterministically\nfrom the same document. Every directive is idempotent, so the whole skill is\nsafe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Prerequisites\n\nYou need a **dedicated GitHub bot account** (not your personal account). The adapter uses this account to post replies and filters out its own messages to avoid loops. Create a free GitHub account for your bot (e.g. `my-org-bot`), then invite it as a collaborator with write access to the repos you want monitored.\n\n## Apply\n\n### 1. Copy the adapter\n\nFetch the `channels` branch and copy the GitHub adapter into `src/channels/`\n(overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/github.ts\nsrc/channels/github-registration.test.ts\n```\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './github.js';\n```\n\n### 3. Install the adapter package\n\nPinned to an exact version — the supply-chain policy rejects ranges and `latest`:\n\n```nc:dep\n@chat-adapter/github@4.29.0\n```\n\n### 4. Build and validate\n\nThe build guards the typed `createChatSdkBridge(...)` core call and proves the\ndependency is installed (the adapter import throws if `@chat-adapter/github`\nisn't present):\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/github-registration.test.ts\n```\n\n`github-registration.test.ts` imports the real channel barrel and asserts the\nregistry contains `github`. It goes red if the import line is deleted or drifts,\nif the barrel fails to evaluate, or if `@chat-adapter/github` isn't installed\n(the import throws) — so it also covers the dependency from step 3.\n\nEnd-to-end message delivery against a real GitHub repo is verified manually once\nthe service is running — see Next Steps and the webhook setup below.\n\n## Credentials\n\n### 1. Create a Personal Access Token for the bot account\n\nLog in as your **bot account**, then:\n\n1. Go to [Settings > Developer Settings > Personal Access Tokens](https://github.com/settings/tokens)\n2. Create a **Fine-grained token** with:\n   - Repository access: select the repos you want the bot to monitor\n   - Permissions: **Pull requests** (Read & Write), **Issues** (Read & Write)\n3. Copy the token\n\n### 2. Set up a webhook on each repo\n\nOn each repo (logged in as the repo owner/admin):\n\n1. Go to **Settings** > **Webhooks** > **Add webhook**\n2. Payload URL: `https://your-domain/webhook/github` (the shared webhook server, default port 3000)\n3. Content type: `application/json`\n4. Secret: generate a random string (e.g. `openssl rand -hex 20`)\n5. Events: select **Issue comments** and **Pull request review comments**\n\n### 3. Configure environment\n\nCapture the three values, then write them. `prompt` only *asks* and binds the\nanswer to a name; a separate directive consumes it — so the same prompts could\nfeed `ncl` or the OneCLI vault instead of `.env` by swapping only the consumer.\nHere they go to `.env` (set-if-absent — a value you've already filled in is\nnever overwritten):\n\n```nc:prompt github_token secret\nPaste the Fine-grained Personal Access Token for the bot account — starts with `github_pat_`.\n```\n```nc:prompt webhook_secret secret\nPaste the webhook secret you generated for the repo webhook(s).\n```\n```nc:prompt bot_username\nEnter the bot account's GitHub username exactly (used for @-mention detection).\n```\n```nc:env-set\nGITHUB_TOKEN={{github_token}}\nGITHUB_WEBHOOK_SECRET={{webhook_secret}}\nGITHUB_BOT_USERNAME={{bot_username}}\n```\n`GITHUB_BOT_USERNAME` must match the bot account's GitHub username exactly. This is used for @-mention detection — the agent responds when someone writes `@your-bot-username` in a PR or issue comment.\n\n## Wiring\n\nAsk the user: **Is this a private or public repo?**\n\n- **Private repo** — use `unknown_sender_policy: 'public'`. Only collaborators can comment anyway, so it's safe to let all comments through.\n- **Public repo** — use `unknown_sender_policy: 'strict'`. Only registered members can trigger the agent, preventing strangers from consuming agent resources. Add trusted collaborators as members (see below).\n\nRun `/manage-channels` to wire the GitHub channel to an agent group, or create the rows directly with `ncl`. **The host service must be running** — `ncl` connects to it over a Unix socket:\n\n```bash\n# Create messaging group (one per repo)\nncl messaging-groups create --channel-type github --platform-id \"github:owner/repo\" \\\n  --name \"owner/repo\" --is-group 1 --unknown-sender-policy <policy>\n\n# Wire to agent group (engage mode/pattern default to the GitHub adapter's\n# declared channel defaults; grab the mg id from the create output above)\nncl wirings create --messaging-group-id <mg-id> --agent-group-id <your-agent-group-id> \\\n  --session-mode per-thread\n```\n\nReplace `<policy>` with `public` or `strict` based on the user's choice above.\n\n### Adding members (for strict mode)\n\nWhen using `strict`, add each GitHub user who should be able to trigger the agent:\n\n```bash\n# Add user (kind = 'github', id = 'github:<numeric-user-id>')\nncl users create --id \"github:<user-id>\" --kind github --display-name \"<username>\"\n\n# Grant membership to the agent group\nncl members add --user \"github:<user-id>\" --group \"<agent-group-id>\"\n```\n\nTo find a GitHub user's numeric ID: `gh api users/<username> --jq .id`\n\nUse `per-thread` session mode so each PR/issue gets its own agent session.\n\n## Next Steps\n\nIf you're in the middle of `/setup`, return to the setup flow now.\n\nOtherwise, restart the service to pick up the new channel.\n\nRun from your NanoClaw project root:\n\n```bash\nsource setup/lib/install-slug.sh\nlaunchctl kickstart -k gui/$(id -u)/$(launchd_label)  # macOS\nsystemctl --user restart $(systemd_unit)              # Linux\n```\n\n## Channel Info\n\n- **type**: `github`\n- **terminology**: GitHub has \"repositories\" containing \"pull requests\" and \"issues.\" Each PR or issue comment thread is a separate conversation.\n- **how-to-find-id**: The platform ID is `github:owner/repo` (e.g. `github:acme/backend`). Each PR/issue becomes its own thread automatically.\n- **supports-threads**: yes (PR and issue comment threads are native conversations)\n- **typical-use**: Webhook-driven — the agent receives PR and issue comment events and responds in comment threads when @-mentioned. After the first mention, the thread is subscribed and the agent responds to all follow-up comments.\n- **default-isolation**: Use `per-thread` session mode. Each PR or issue gets its own isolated agent session. Typically wire to a dedicated agent group if the repo contains sensitive code.\n\n## Troubleshooting\n\n**API calls return 401/403 with the token.** The token must be a **fine-grained** PAT starting `github_pat_`, created while logged in as the *bot* account (Settings → Developer Settings → Personal Access Tokens → Fine-grained tokens), with the monitored repos selected under Repository access and both **Pull requests** and **Issues** set to Read & Write. A classic `ghp_` token, or one minted on your personal account, is the usual miss.\n\n**Webhook deliveries show red in the repo settings.** Open **Settings → Webhooks → Recent Deliveries** on the repo: a 401 response means the secret in the webhook form doesn't match `GITHUB_WEBHOOK_SECRET`; a timeout means `https://your-domain/webhook/github` isn't publicly reachable on the shared webhook port (3000). Fix, then use **Redeliver** to retest without writing a new comment.\n\n**Comments never trigger the agent.** The @-mention must match `GITHUB_BOT_USERNAME` exactly, and the webhook must subscribe to **Issue comments** and **Pull request review comments** (not just pushes). Comments authored by the bot account itself are filtered by design — test from a different account than the bot.\n\n**Adapter installed but the channel is dead.** Run `pnpm exec vitest run src/channels/github-registration.test.ts` — red means the barrel import or the `@chat-adapter/github` install drifted, so re-run the Apply steps. If green, restart the service (see Next Steps) so it loads the adapter and the new `.env` values.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-github","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-github/SKILL.md","defaultBranch":"main"},"readme":"# Add GitHub Channel\n\nAdds GitHub support via the Chat SDK bridge. The agent participates in PR and\nissue comment threads. NanoClaw doesn't ship channels in trunk — this skill\ncopies the GitHub adapter in from the `channels` branch.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent\nreads the prose and applies them, and a parser can apply them deterministically\nfrom the same document. Every directive is idempotent, so the whole skill is\nsafe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Prerequisites\n\nYou need a **dedicated GitHub bot account** (not your personal account). The adapter uses this account to post replies and filters out its own messages to avoid loops. Create a free GitHub account for your bot (e.g. `my-org-bot`), then invite it as a collaborator with write access to the repos you want monitored.\n\n## Apply\n\n### 1. Copy the adapter\n\nFetch the `channels` branch and copy the GitHub adapter into `src/channels/`\n(overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/github.ts\nsrc/channels/github-registration.test.ts\n```\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './github.js';\n```\n\n### 3. Install the adapter package\n\nPinned to an exact version — the supply-chain policy rejects ranges and `latest`:\n\n```nc:dep\n@chat-adapter/github@4.29.0\n```\n\n### 4. Build and validate\n\nThe build guards the typed `createChatSdkBridge(...)` core call and proves the\ndependency is installed (the adapter import throws if `@chat-adapter/github`\nisn't present):\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/github-registration.test.ts\n```\n\n`github-registration.test.ts` imports the real channel barrel and asserts the\nregistry contains `github`. It goes red if the import line is deleted or drifts,\nif the barrel fails to evaluate, or if `@chat-adapter/github` isn't installed\n(the import throws) — so it also covers the dependency from step 3.\n\nEnd-to-end message delivery against a real GitHub repo is verified manually once\nthe service is running — see Next Steps and the webhook setup below.\n\n## Credentials\n\n### 1. Create a Personal Access Token for the bot account\n\nLog in as your **bot account**, then:\n\n1. Go to [Settings > Developer Settings > Personal Access Tokens](https://github.com/settings/tokens)\n2. Create a **Fine-grained token** with:\n   - Repository access: select the repos you want the bot to monitor\n   - Permissions: **Pull requests** (Read & Write), **Issues** (Read & Write)\n3. Copy the token\n\n### 2. Set up a webhook on each repo\n\nOn each repo (logged in as the repo owner/admin):\n\n1. Go to **Settings** > **Webhooks** > **Add webhook**\n2. Payload URL: `https://your-domain/webhook/github` (the shared webhook server, default port 3000)\n3. Content type: `application/json`\n4. Secret: generate a random string (e.g. `openssl rand -hex 20`)\n5. Events: select **Issue comments** and **Pull request review comments**\n\n### 3. Configure environment\n\nCapture the three values, then write them. `prompt` only *asks* and binds the\nanswer to a name; a separate directive consumes it — so the same prompts could\nfeed `ncl` or the OneCLI vault instead of `.env` by swapping only the consumer.\nHere they go to `.env` (set-if-absent — a value you've already filled in is\nnever overwritten):\n\n```nc:prompt github_token secret\nPaste the Fine-grained Personal Access Token for the bot account — starts with `github_pat_`.\n```\n```nc:prompt webhook_secret secret\nPaste the webhook secret you generated for the repo webhook(s).\n```\n```nc:prompt bot_username\nEnter the bot account's GitHub username exactly (used for @-mention detection).\n```\n```nc:env-set\nGITHUB_TOKEN={{github_token}}\nGITHUB_WEBHOOK_SECRET={{webhook_secret}}\nGITHUB_BOT_USERNAME={{bot_us","createdAt":"2026-09-25T10:52:06.640Z","updatedAt":"2026-09-25T10:52:06.640Z"},{"id":"cmugucucr00k5qu06lzfs5bvj","slug":"nanocoai-nanoclaw-add-mattermost","name":"add-mattermost","description":"Add a self-hosted or cloud Mattermost bot channel through the Chat SDK bridge, reusing a healthy server when available and linking to official server setup guidance when needed.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-mattermost","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add a self-hosted or cloud Mattermost bot channel through the Chat SDK bridge, reusing a healthy server when available and linking to official server setup guidance when needed.","permissions":[],"systemPrompt":"# Add Mattermost Channel\n\nAdds Mattermost DMs, channels, threads, files, reactions, and interactive\napproval cards. Messages arrive over Mattermost's WebSocket; card clicks return\nto NanoClaw over an authenticated HTTP callback. Every step is safe to re-run.\n\n## Discover the server first\n\nDo this before installing the adapter or asking for a URL. The goal is to\nreuse a healthy Mattermost the user already has and establish one canonical\nbase URL.\n\n1. Check an existing `MATTERMOST_BASE_URL` in the current environment and\n   NanoClaw env/config files. Do not print tokens or dump whole env files.\n2. Probe likely local URLs, at least `http://localhost:8065` and\n   `http://127.0.0.1:8065`, using `GET /api/v4/system/ping`. A listening port\n   alone is not evidence that the service is Mattermost.\n3. Inspect Docker/Compose for Mattermost containers. If a matching container\n   exists but is stopped, offer to start it with its original mechanism; do not\n   start or recreate it without the user's approval.\n4. If you find a healthy server, show its URL and ask the user to use it or\n   enter a different URL. Do not select a server automatically. Treat the\n   localhost and 127.0.0.1 endpoints for the same container as one server.\n5. If nothing healthy is found, offer Mattermost's maintained evaluation and\n   deployment guidance from [SERVER_SETUP.md](SERVER_SETUP.md), then ask for\n   the server URL after the operator has one running.\n\nSet `MATTERMOST_BASE_URL` to the chosen canonical URL (scheme included, no\ntrailing slash), then use that exact hostname in browser/Desktop setup.\nNanoClaw connects to Mattermost; it does not install or manage the server.\n\n## Apply\n\n### 1. Detect or select the server\n\nTest the configured URL and the standard local URLs. A detected server is only\na suggestion. The user must select it.\n\n```nc:run capture:discovery=.discovery,detected_url=.base_url,detected_config_access=.config_access,detected_container=.mattermost_container effect:fetch\nnode .claude/skills/add-mattermost/scripts/discover-server.mjs\n```\n\n```nc:operator when:discovery=found\nNanoClaw found a healthy Mattermost server at {{detected_url}}. You can use this server or enter a different Mattermost URL.\n```\n\n```nc:prompt server_choice when:discovery=found normalize:lower validate:^(use|enter)$\nEnter `use` to use {{detected_url}}. Enter `enter` to specify a different Mattermost URL.\n```\n\n```nc:run capture:base_url=.base_url,config_access=.config_access,mattermost_container=.mattermost_container effect:fetch when:server_choice=use\nnode .claude/skills/add-mattermost/scripts/select-server.mjs use \"{{detected_url}}\" \"{{detected_config_access}}\" \"{{detected_container}}\"\n```\n\n```nc:prompt entered_url when:server_choice=enter normalize:rstrip-slash validate:^https?://(?:[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?|\\[[0-9A-Fa-f:.]+\\])(?::[0-9]{1,5})?(?:/[A-Za-z0-9._~%+-]+)*$\nEnter the Mattermost base URL. Include the scheme, for example `https://mattermost.example.com`.\n```\n\n```nc:run capture:base_url=.base_url,config_access=.config_access,mattermost_container=.mattermost_container effect:fetch when:server_choice=enter\nnode .claude/skills/add-mattermost/scripts/select-server.mjs enter \"{{entered_url}}\"\n```\n\n```nc:operator when:discovery=none\nNanoClaw did not find a healthy Mattermost server. NanoClaw connects to a server but does not install or operate one. For a temporary local trial, follow Mattermost's official Quick Start Evaluation: https://docs.mattermost.com/deployment-guide/quick-start-evaluation. For a persistent or production installation, choose a supported path in Mattermost's deployment guide: https://docs.mattermost.com/deployment-guide/server/deploy-server. Return here when the server is running.\n```\n\n```nc:prompt entered_url_new when:discovery=none normalize:rstrip-slash validate:^https?://(?:[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?|\\[[0-9A-Fa-f:.]+\\])(?::[0-9]{1,5})?(?:/[A-Za-z0-9._~%+-]+)*$\nMattermost base URL. Include the scheme, for example `https://mattermost.example.com`.\n```\n\n```nc:run capture:base_url=.base_url,config_access=.config_access,mattermost_container=.mattermost_container effect:fetch when:discovery=none\nnode .claude/skills/add-mattermost/scripts/select-server.mjs enter \"{{entered_url_new}}\"\n```\n\n### 2. Set the server SiteURL\n\nMattermost Desktop sends its configured server URL as the WebSocket Origin.\nBefore you install the adapter, set `ServiceSettings.SiteURL` to the same URL:\n`{{base_url}}`. Keep\n`ServiceSettings.WebsocketURL` blank. Do not change\n`ServiceSettings.AllowCorsFrom` to correct an Origin error.\n\nWhen discovery found host-local `mmctl`, ask before changing the server:\n\n```nc:prompt site_url_action normalize:lower validate:^(set|already)$ when:config_access=host\nEnter `set` to set SiteURL to {{base_url}} and clear WebsocketURL. Enter `already` if these values are already correct.\n```\n\n```nc:run effect:external when:site_url_action=set\nmmctl config set ServiceSettings.SiteURL \"{{base_url}}\" --local\nmmctl config set ServiceSettings.WebsocketURL \"\" --local\n```\n\nWhen discovery found `mmctl` inside a local Mattermost container, ask before\nchanging it there:\n\n```nc:prompt site_url_action_docker normalize:lower validate:^(set|already)$ when:config_access=docker\nEnter `set` to set SiteURL to {{base_url}} in {{mattermost_container}} and clear WebsocketURL. Enter `already` if these values are already correct.\n```\n\n```nc:run effect:external when:site_url_action_docker=set\ndocker exec \"{{mattermost_container}}\" mmctl config set ServiceSettings.SiteURL \"{{base_url}}\" --local\ndocker exec \"{{mattermost_container}}\" mmctl config set ServiceSettings.WebsocketURL \"\" --local\n```\n\nIf local configuration access is unavailable, tell the operator:\n\n```nc:operator when:config_access=unavailable\nSet Mattermost ServiceSettings.SiteURL to {{base_url}}. Leave ServiceSettings.WebsocketURL blank. As a System Admin, run `mmctl config set ServiceSettings.SiteURL \"{{base_url}}\"`. Run `mmctl config set ServiceSettings.WebsocketURL \"\"`. You can also use System Console → Environment → Web Server. Do not change ServiceSettings.AllowCorsFrom to correct an Origin error.\n```\n\n```nc:prompt site_url_ready normalize:lower validate:^ready$ when:config_access=unavailable\nEnter `ready` after you save these Mattermost settings.\n```\n\nUse the public client configuration endpoint to verify the settings. The\ncommand must print `{{base_url}}` and then a blank line.\n\n```nc:run effect:fetch\ncurl -fsS \"{{base_url}}/api/v4/config/client?format=old\" | node .claude/skills/add-mattermost/scripts/read-response.mjs config \"{{base_url}}\"\n```\n\n### 3. Copy and register the channel\n\nCopy the canonical adapter and registration test from the `channels` branch.\nThe payload must include authenticated transport liveness and setup callback\nprobes. Existing files are preserved by installation; on an older installation,\nrun `/update-skills` to refresh Mattermost before rerunning this skill.\n\n```nc:copy from-branch:channels\nsrc/channels/mattermost.ts\nsrc/channels/mattermost-registration.test.ts\nsrc/channels/mattermost-adapter/adapter.ts\nsrc/channels/mattermost-adapter/adapter.test.ts\nsrc/channels/mattermost-adapter/format.ts\nsrc/channels/mattermost-adapter/index.ts\nsrc/channels/mattermost-adapter/rest.ts\nsrc/channels/mattermost-adapter/thread-id.ts\nsrc/channels/mattermost-adapter/types.ts\nsrc/channels/mattermost-adapter/websocket.ts\nsrc/channels/mattermost-adapter/websocket.test.ts\n```\n\nAppend the channel's single reach-in to the barrel, skipping it if present.\n\n```nc:append to:src/channels/index.ts\nimport './mattermost.js';\n```\n\nRemove the unscoped `chat-adapter-mattermost` package when it is installed.\nNothing in this repository imports it: it is typosquat-shaped against the\nscoped `@chat-adapter` family, so any copy in `package.json` is stale or\nmistaken and would sit beside the audited implementation copied from the\n`channels` branch.\n\n```nc:run\nif node -e \"const p=require('./package.json'); process.exit(p.dependencies?.['chat-adapter-mattermost'] ? 0 : 1)\"; then pnpm remove chat-adapter-mattermost; fi\n```\n\nInstall the vendored adapter's direct WebSocket dependencies at the exact\nsupported versions.\n\n```nc:dep\nws@8.21.3\n@types/ws@8.18.1\n```\n\n### 4. Create and authenticate the bot\n\nTell the operator:\n\n```nc:operator\nNow create a Mattermost bot for NanoClaw:\n1. As a System Admin, open System Console → Integrations → Bot Accounts. Turn on Enable Bot Account Creation. This setting permits bot creation. You do not create the bot on this page.\n2. Return to the Mattermost workspace. Open Product menu → Integrations → Bot Accounts. Select Add Bot Account. Create a bot, for example `nanoclaw`.\n3. Copy the access token.\n4. Add the bot to each required team and channel. Mattermost does not add bots to teams or channels automatically.\n5. Keep the token secret. If you lose the token, create a replacement. Deactivate the old token after the replacement works.\n```\n\n```nc:prompt bot_token secret reuse:MATTERMOST_BOT_TOKEN normalize:trim validate:^[A-Za-z0-9_-]{20,}$\nMattermost bot access token (20 or more letters, digits, underscores, or hyphens).\n```\n\nThe configuration helper below authenticates this token and captures the bot\nidentity before it saves any settings.\n\n### 5. Configure authenticated card callbacks\n\nApprovals require Mattermost itself—not the browser—to reach NanoClaw. Ask for\na URL routable from the Mattermost server. It may be NanoClaw's base URL or the\nfull `/webhook/mattermost` route; the adapter normalizes either form.\n\n```nc:prompt callback_url reuse:MATTERMOST_CALLBACK_URL normalize:rstrip-slash validate:^https?://(?:[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?|\\[[0-9A-Fa-f:.]+\\])(?::[0-9]{1,5})?(?:/[A-Za-z0-9._~%+-]+)*$\nCallback URL reachable from Mattermost, such as `https://nanoclaw.example.com` or `http://host.docker.internal:3000/webhook/mattermost`.\n```\n\nSave the selected server, authenticated bot token, and callback URL. On a rerun,\noffer to reuse each existing setting; save replacements when the operator\nchooses them. Mattermost does not sign action callbacks, so the helper creates\na random shared secret locally on first setup. It preserves that secret on\nreruns so existing approval cards keep working. Never print the secret.\n\n```nc:run capture:bot_user_id=.id,bot_username=.username effect:external remove:.claude/skills/add-mattermost/scripts/remove-config.mjs\npnpm exec tsx .claude/skills/add-mattermost/scripts/configure.ts \"{{base_url}}\" \"{{bot_token}}\" \"{{callback_url}}\"\n```\n\nTell the operator:\n\n```nc:operator\nMake sure that the Mattermost server can reach the callback host. For a private host or Docker bridge name, add the host name or IP address in System Console → Environment → Developer → Allow untrusted internal connections. Use a publicly trusted HTTPS certificate in production.\n```\n\n### 6. Resolve the owner's DM\n\nAsk for the Mattermost username that will own this NanoClaw installation.\n\n```nc:prompt owner_username normalize:lower validate:^[a-z0-9][a-z0-9._-]{0,63}$\nYour Mattermost username, without `@`.\n```\n\nResolve that user and open the DM shared with the bot.\n\n```nc:run capture:owner_user_id=.id,owner_handle=.id effect:fetch\ncurl -sf \"{{base_url}}/api/v4/users/username/{{owner_username}}\" -H \"Authorization: Bearer {{bot_token}}\"\n```\n\n```nc:run capture:platform_id effect:fetch validate:^mattermost:[a-z0-9]{26}$\ncurl -sf -X POST \"{{base_url}}/api/v4/channels/direct\" -H \"Authorization: Bearer {{bot_token}}\" -H \"Content-Type: application/json\" -d '[\"{{owner_user_id}}\",\"{{bot_user_id}}\"]' | node .claude/skills/add-mattermost/scripts/read-response.mjs dm\n```\n\nThe resolved `platform_id`, `owner_handle`, and `owner_username` are used by\n`/init-first-agent`. If an owner exists, use `/manage-channels` instead.\n\n### 7. Build, test, and restart\n\nBuild the composed host to guard the typed Chat SDK bridge call and dependency.\n\n```nc:run effect:build\npnpm run build\n```\n\nRun the registration test through the channel barrel. Also run the installed\nadapter regression tests.\n\n```nc:run effect:test\npnpm exec vitest run src/channels/mattermost-registration.test.ts src/channels/mattermost-adapter/adapter.test.ts src/channels/mattermost-adapter/websocket.test.ts\n```\n\nRestart NanoClaw so the channel and credentials load.\n\n```nc:run effect:restart\nbash setup/lib/restart.sh --channel mattermost\n```\n\nVerify the new host loaded the selected bot and settings, and that the selected\nbot and owner belong to the DM. The helper checks the host's actual listener,\nthen creates a temporary diagnostic card in that DM and invokes its action\nthrough Mattermost. It requires receipt by this host and deletes the diagnostic\ncard afterward. This exercises Mattermost's outbound routing and TLS policy.\nIt reads the saved settings without printing credentials.\n\n```nc:run effect:wire\npnpm exec tsx .claude/skills/add-mattermost/scripts/verify-runtime.ts \"{{bot_user_id}}\" \"{{owner_user_id}}\" \"{{platform_id}}\"\n```\n\n## Next steps\n\nFor a first channel, continue with `/init-first-agent` using `mattermost`,\n`{{platform_id}}`, and `{{owner_username}}`. Otherwise run `/manage-channels`.\n\nSend the bot a DM and mention it in a joined channel. The first mention in an\nunwired channel sends an approval card to the owner's bot DM. Approve it there;\nNanoClaw replays the held message after creating the wiring.\n\nClick a real approval card to verify the approval workflow beyond the automated\ncallback transport check. Success replaces the buttons with the chosen result.\nAn unsigned probe must return `401` (alone this does not identify the host):\n\n```bash\ncurl -sS -o /dev/null -w '%{http_code}\\n' -X POST \\\n  -H 'content-type: application/json' -d '{}' \\\n  http://<nanoclaw-host>:3000/webhook/mattermost\n```\n\n## Channel information\n\n- **type:** `mattermost`\n- **platform ID:** `mattermost:<channel-id>` for channels and DMs\n- **threads:** channel posts use optional Mattermost reply roots\n- **group trigger:** mention-sticky, scoped per thread\n- **DM trigger:** every message\n- **unknown channels:** request owner approval\n- **transport:** WebSocket inbound, REST outbound, HTTP action callbacks\n\n## Troubleshooting\n\n**The token check returns 401.** The token is stale, belongs to a deactivated\nbot, or was pasted incorrectly. Create a replacement token and deactivate the\nold token after the replacement works.\n\n**The bot ignores a channel.** Add it to that team and channel. Membership\nchanges are observed, but restarting NanoClaw forces a fresh subscription.\n\n**A new channel gets no immediate reply.** Check the owner's DM with the bot.\nNanoClaw holds the first message behind a channel-approval card and deduplicates\nlater mentions until that card is resolved.\n\n**Desktop messages appear only after a manual refresh.** The server can reject\nthe WebSocket Origin. Use the same host name in the Desktop server URL,\n`MATTERMOST_BASE_URL`, and `ServiceSettings.SiteURL`. Keep\n`ServiceSettings.WebsocketURL` blank. Verify the values through\n`/api/v4/config/client?format=old`. Check the server logs for `request origin\nnot allowed`. Do not change `ServiceSettings.AllowCorsFrom` to correct this\nerror. For a container installation, set SiteURL in the server configuration.\n\n**Cards render but clicks do nothing.** Rerun the runtime helper above. It binds\nthe listener and callback receipt to this host; a `401` alone cannot do that.\nMattermost logs report blocked hosts and TLS errors. If a verification request\ntimes out after creating a card, remove that diagnostic card from the owner DM.\n\n**Runtime verification requests a payload refresh.** Run `/update-skills` for\nMattermost, rebuild, and rerun this skill. The normal install path preserves\nexisting adapter files; old payloads cannot prove the running configuration.\n\n**The adapter repeatedly reconnects.** Confirm `/api/v4/websocket` supports\nWebSocket upgrades through every reverse proxy and that idle connections live\nlonger than the adapter heartbeat.\n\n**Messages arrive but no agent runs.** Inspect `ncl dropped-messages list` and\n`ncl wirings list`. `no_agent_wired` means approval is pending or no wiring was\ncreated; it is not an adapter failure.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-mattermost","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-mattermost/SKILL.md","defaultBranch":"main"},"readme":"# Add Mattermost Channel\n\nAdds Mattermost DMs, channels, threads, files, reactions, and interactive\napproval cards. Messages arrive over Mattermost's WebSocket; card clicks return\nto NanoClaw over an authenticated HTTP callback. Every step is safe to re-run.\n\n## Discover the server first\n\nDo this before installing the adapter or asking for a URL. The goal is to\nreuse a healthy Mattermost the user already has and establish one canonical\nbase URL.\n\n1. Check an existing `MATTERMOST_BASE_URL` in the current environment and\n   NanoClaw env/config files. Do not print tokens or dump whole env files.\n2. Probe likely local URLs, at least `http://localhost:8065` and\n   `http://127.0.0.1:8065`, using `GET /api/v4/system/ping`. A listening port\n   alone is not evidence that the service is Mattermost.\n3. Inspect Docker/Compose for Mattermost containers. If a matching container\n   exists but is stopped, offer to start it with its original mechanism; do not\n   start or recreate it without the user's approval.\n4. If you find a healthy server, show its URL and ask the user to use it or\n   enter a different URL. Do not select a server automatically. Treat the\n   localhost and 127.0.0.1 endpoints for the same container as one server.\n5. If nothing healthy is found, offer Mattermost's maintained evaluation and\n   deployment guidance from [SERVER_SETUP.md](SERVER_SETUP.md), then ask for\n   the server URL after the operator has one running.\n\nSet `MATTERMOST_BASE_URL` to the chosen canonical URL (scheme included, no\ntrailing slash), then use that exact hostname in browser/Desktop setup.\nNanoClaw connects to Mattermost; it does not install or manage the server.\n\n## Apply\n\n### 1. Detect or select the server\n\nTest the configured URL and the standard local URLs. A detected server is only\na suggestion. The user must select it.\n\n```nc:run capture:discovery=.discovery,detected_url=.base_url,detected_config_access=.config_access,detected_container=.mattermost_container effect:fetch\nnode .claude/skills/add-mattermost/scripts/discover-server.mjs\n```\n\n```nc:operator when:discovery=found\nNanoClaw found a healthy Mattermost server at {{detected_url}}. You can use this server or enter a different Mattermost URL.\n```\n\n```nc:prompt server_choice when:discovery=found normalize:lower validate:^(use|enter)$\nEnter `use` to use {{detected_url}}. Enter `enter` to specify a different Mattermost URL.\n```\n\n```nc:run capture:base_url=.base_url,config_access=.config_access,mattermost_container=.mattermost_container effect:fetch when:server_choice=use\nnode .claude/skills/add-mattermost/scripts/select-server.mjs use \"{{detected_url}}\" \"{{detected_config_access}}\" \"{{detected_container}}\"\n```\n\n```nc:prompt entered_url when:server_choice=enter normalize:rstrip-slash validate:^https?://(?:[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?|\\[[0-9A-Fa-f:.]+\\])(?::[0-9]{1,5})?(?:/[A-Za-z0-9._~%+-]+)*$\nEnter the Mattermost base URL. Include the scheme, for example `https://mattermost.example.com`.\n```\n\n```nc:run capture:base_url=.base_url,config_access=.config_access,mattermost_container=.mattermost_container effect:fetch when:server_choice=enter\nnode .claude/skills/add-mattermost/scripts/select-server.mjs enter \"{{entered_url}}\"\n```\n\n```nc:operator when:discovery=none\nNanoClaw did not find a healthy Mattermost server. NanoClaw connects to a server but does not install or operate one. For a temporary local trial, follow Mattermost's official Quick Start Evaluation: https://docs.mattermost.com/deployment-guide/quick-start-evaluation. For a persistent or production installation, choose a supported path in Mattermost's deployment guide: https://docs.mattermost.com/deployment-guide/server/deploy-server. Return here when the server is running.\n```\n\n```nc:prompt entered_url_new when:discovery=none normalize:rstrip-slash validate:^https?://(?:[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?|\\[[0-9A-Fa-f:.]+\\])(?::[0-9]{1,5})?(?:/[A-Za-z0-9._~%+-]+)*$\nMattermost base URL. Include the scheme, for example `htt","createdAt":"2026-09-25T10:52:06.699Z","updatedAt":"2026-09-25T10:52:06.699Z"},{"id":"cmugucud100k8qu06wyp7f0eo","slug":"nanocoai-nanoclaw-add-mnemon","name":"add-mnemon","description":"Add persistent graph-based memory via mnemon. Agents recall past context before responding and remember insights after each turn.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-mnemon","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add persistent graph-based memory via mnemon. Agents recall past context before responding and remember insights after each turn.","permissions":[],"systemPrompt":"# Add Mnemon — Persistent Memory\n\nInstalls [mnemon](https://github.com/mnemon-dev/mnemon) in the agent container image. On each container start, `mnemon setup` registers Claude Code hooks that surface relevant memory before the agent responds and store new insights after each turn. Memory is written to the per-agent-group `.claude/` mount and survives container restarts.\n\n## Provider Compatibility\n\nmnemon hooks fire only under `--target claude-code`. Use this skill on agent groups that run the default Claude provider. The provider is the materialized `provider` key in each group's `container.json` (absent or `claude` = default Claude provider). Confirm it before applying:\n\n```bash\ngrep -H '\"provider\"' groups/*/container.json 2>/dev/null   # no match, or \"provider\": \"claude\" = Claude\n```\n\nIf a group sets a different provider (e.g. `\"provider\": \"opencode\"`), it spawns its own process and never invokes the `claude` CLI, so the hooks registered by `mnemon setup` do not run for that group.\n\n## Phase 1: Pre-flight\n\n### Check if already applied\n\n```bash\ngrep -q 'MNEMON_VERSION' container/Dockerfile && echo \"Already applied\" || echo \"Not applied\"\n```\n\nIf already applied, re-run Phase 2 anyway — every step is idempotent and skips work that is already in place — then continue to Phase 3 (Verify).\n\n### Check latest mnemon version\n\n```bash\ncurl -fsSL https://api.github.com/repos/mnemon-dev/mnemon/releases/latest | grep '\"tag_name\"'\n```\n\nNote the version (e.g. `v0.1.1`) — use it as `MNEMON_VERSION` in the next step.\n\n## Phase 2: Apply Changes\n\n### 1. Dockerfile — install mnemon binary\n\nInsert the mnemon block immediately above the `# ---- Bun runtime` section of `container/Dockerfile` (skip if `grep -q 'MNEMON_VERSION' container/Dockerfile` already matches):\n\n```dockerfile\n# ---- mnemon — persistent agent memory ----------------------------------------\nARG MNEMON_VERSION=0.1.1\nRUN ARCH=$(dpkg --print-architecture) && \\\n    curl -fsSL \"https://github.com/mnemon-dev/mnemon/releases/download/v${MNEMON_VERSION}/mnemon_${MNEMON_VERSION}_linux_${ARCH}.tar.gz\" \\\n    | tar -xz -C /usr/local/bin mnemon && \\\n    chmod +x /usr/local/bin/mnemon\n\nENV MNEMON_DATA_DIR=/home/node/.claude/mnemon\n```\n\n`MNEMON_DATA_DIR` points into the per-agent-group `.claude/` mount, so memory persists across container restarts.\n\n### 2. Entrypoint — run mnemon setup on each container start\n\n`mnemon setup` is idempotent. Run it once per `container/entrypoint.sh`. First check whether the line is already present:\n\n```bash\ngrep -q 'mnemon setup' container/entrypoint.sh && echo \"Already wired\" || echo \"Wire it\"\n```\n\nIf it prints `Wire it`, add the setup call right after `set -e`, before the `cat` that captures stdin, so the result looks like:\n\n```bash\n#!/bin/bash\n# NanoClaw agent container entrypoint.\n#\n# ...existing header comment...\n\nset -e\n\nmnemon setup --target claude-code --yes --global >/dev/stderr 2>&1\n\ncat > /tmp/input.json\n\nexec bun run /app/src/index.ts < /tmp/input.json\n```\n\n`>/dev/stderr 2>&1` routes all mnemon output to stderr (docker logs) so it doesn't interfere with the JSON stdin handshake between host and agent-runner.\n\n### 3. Copy the integration tests\n\nBoth reach-ins are into container build/runtime files that aren't importable or typed (a GitHub-release binary in the Dockerfile, a shell line in the entrypoint), so structural tests guard them. Copy them into the host test tree:\n\n```bash\ncp .claude/skills/add-mnemon/mnemon-dockerfile.test.ts src/mnemon-dockerfile.test.ts\ncp .claude/skills/add-mnemon/mnemon-entrypoint.test.ts src/mnemon-entrypoint.test.ts\npnpm exec vitest run src/mnemon-dockerfile.test.ts src/mnemon-entrypoint.test.ts\n```\n\n`mnemon-dockerfile.test.ts` asserts the `MNEMON_VERSION` ARG and `MNEMON_DATA_DIR` ENV are present (red if the install layer is dropped on an upgrade). `mnemon-entrypoint.test.ts` asserts the entrypoint invokes `mnemon setup --target claude-code` (red if the wiring is removed).\n\n### 4. Rebuild and smoke-test the image\n\n```bash\n./container/build.sh\ndocker run --rm --entrypoint mnemon nanoclaw-agent:latest --version\n```\n\n## Phase 3: Restart and Verify\n\n### Restart the service\n\nRun from your NanoClaw project root:\n\n```bash\nsource setup/lib/install-slug.sh\nsystemctl --user restart $(systemd_unit)              # Linux\n# launchctl kickstart -k gui/$(id -u)/$(launchd_label)   # macOS\n```\n\n### Confirm mnemon hooks are registered\n\nAfter the next container starts, check that setup ran:\n\n```bash\ndocker logs $(docker ps --filter label=nanoclaw-session --format \"{{.Names}}\" | head -1) 2>&1 | grep -i mnemon\n```\n\nThen inspect the hooks inside the running container:\n\n```bash\ndocker exec $(docker ps --filter label=nanoclaw-session --format \"{{.Names}}\" | head -1) \\\n  cat /home/node/.claude/settings.json | grep -A5 mnemon\n```\n\n### Test memory recall\n\nHave a conversation with the agent, then start a new session and reference something from the earlier one. Mnemon should surface the relevant context automatically without you restating it.\n\n## Memory Storage\n\nMnemon writes to `/home/node/.claude/mnemon/` inside the container, which maps to the per-agent-group `.claude/` directory on the host. To find the exact host path:\n\n```bash\ndocker inspect $(docker ps --filter label=nanoclaw-session --format \"{{.Names}}\" | head -1) \\\n  --format '{{range .Mounts}}{{if eq .Destination \"/home/node/.claude\"}}{{.Source}}{{end}}{{end}}'\n```\n\nTo reset all memory for an agent, stop the container and delete the `mnemon/` subdirectory from that host path.\n\n## Troubleshooting\n\n### `mnemon: command not found` in container\n\nThe image wasn't rebuilt after adding the Dockerfile layer. Run `./container/build.sh` and restart.\n\n### Memory not persisting across restarts\n\nVerify `MNEMON_DATA_DIR` resolves to a mounted path (not an in-container ephemeral directory):\n\n```bash\ndocker exec <container> sh -c 'ls -la $MNEMON_DATA_DIR'\n```\n\nIf the directory is empty after conversations, the mount is missing or the path is wrong. Check the host mount with the `docker inspect` command above.\n\n### Agent not using past memory\n\n`mnemon setup` writes hooks into `/home/node/.claude/settings.json`. Verify:\n\n```bash\ndocker exec <container> cat /home/node/.claude/settings.json\n```\n\nIf the hooks are absent, `mnemon setup` may have failed silently. Check container startup logs for errors from mnemon.\n\n### Setup fails at container start\n\nRun setup manually inside a running container to see the full error:\n\n```bash\ndocker exec -it <container> mnemon setup --target claude-code --yes --global\n```","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-mnemon","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-mnemon/SKILL.md","defaultBranch":"main"},"readme":"# Add Mnemon — Persistent Memory\n\nInstalls [mnemon](https://github.com/mnemon-dev/mnemon) in the agent container image. On each container start, `mnemon setup` registers Claude Code hooks that surface relevant memory before the agent responds and store new insights after each turn. Memory is written to the per-agent-group `.claude/` mount and survives container restarts.\n\n## Provider Compatibility\n\nmnemon hooks fire only under `--target claude-code`. Use this skill on agent groups that run the default Claude provider. The provider is the materialized `provider` key in each group's `container.json` (absent or `claude` = default Claude provider). Confirm it before applying:\n\n```bash\ngrep -H '\"provider\"' groups/*/container.json 2>/dev/null   # no match, or \"provider\": \"claude\" = Claude\n```\n\nIf a group sets a different provider (e.g. `\"provider\": \"opencode\"`), it spawns its own process and never invokes the `claude` CLI, so the hooks registered by `mnemon setup` do not run for that group.\n\n## Phase 1: Pre-flight\n\n### Check if already applied\n\n```bash\ngrep -q 'MNEMON_VERSION' container/Dockerfile && echo \"Already applied\" || echo \"Not applied\"\n```\n\nIf already applied, re-run Phase 2 anyway — every step is idempotent and skips work that is already in place — then continue to Phase 3 (Verify).\n\n### Check latest mnemon version\n\n```bash\ncurl -fsSL https://api.github.com/repos/mnemon-dev/mnemon/releases/latest | grep '\"tag_name\"'\n```\n\nNote the version (e.g. `v0.1.1`) — use it as `MNEMON_VERSION` in the next step.\n\n## Phase 2: Apply Changes\n\n### 1. Dockerfile — install mnemon binary\n\nInsert the mnemon block immediately above the `# ---- Bun runtime` section of `container/Dockerfile` (skip if `grep -q 'MNEMON_VERSION' container/Dockerfile` already matches):\n\n```dockerfile\n# ---- mnemon — persistent agent memory ----------------------------------------\nARG MNEMON_VERSION=0.1.1\nRUN ARCH=$(dpkg --print-architecture) && \\\n    curl -fsSL \"https://github.com/mnemon-dev/mnemon/releases/download/v${MNEMON_VERSION}/mnemon_${MNEMON_VERSION}_linux_${ARCH}.tar.gz\" \\\n    | tar -xz -C /usr/local/bin mnemon && \\\n    chmod +x /usr/local/bin/mnemon\n\nENV MNEMON_DATA_DIR=/home/node/.claude/mnemon\n```\n\n`MNEMON_DATA_DIR` points into the per-agent-group `.claude/` mount, so memory persists across container restarts.\n\n### 2. Entrypoint — run mnemon setup on each container start\n\n`mnemon setup` is idempotent. Run it once per `container/entrypoint.sh`. First check whether the line is already present:\n\n```bash\ngrep -q 'mnemon setup' container/entrypoint.sh && echo \"Already wired\" || echo \"Wire it\"\n```\n\nIf it prints `Wire it`, add the setup call right after `set -e`, before the `cat` that captures stdin, so the result looks like:\n\n```bash\n#!/bin/bash\n# NanoClaw agent container entrypoint.\n#\n# ...existing header comment...\n\nset -e\n\nmnemon setup --target claude-code --yes --global >/dev/stderr 2>&1\n\ncat > /tmp/input.json\n\nexec bun run /app/src/index.ts < /tmp/input.json\n```\n\n`>/dev/stderr 2>&1` routes all mnemon output to stderr (docker logs) so it doesn't interfere with the JSON stdin handshake between host and agent-runner.\n\n### 3. Copy the integration tests\n\nBoth reach-ins are into container build/runtime files that aren't importable or typed (a GitHub-release binary in the Dockerfile, a shell line in the entrypoint), so structural tests guard them. Copy them into the host test tree:\n\n```bash\ncp .claude/skills/add-mnemon/mnemon-dockerfile.test.ts src/mnemon-dockerfile.test.ts\ncp .claude/skills/add-mnemon/mnemon-entrypoint.test.ts src/mnemon-entrypoint.test.ts\npnpm exec vitest run src/mnemon-dockerfile.test.ts src/mnemon-entrypoint.test.ts\n```\n\n`mnemon-dockerfile.test.ts` asserts the `MNEMON_VERSION` ARG and `MNEMON_DATA_DIR` ENV are present (red if the install layer is dropped on an upgrade). `mnemon-entrypoint.test.ts` asserts the entrypoint invokes `mnemon setup --target claude-code` (red if the wiring is removed).\n\n### 4. Rebuild and smoke-test the image\n\n```ba","createdAt":"2026-09-25T10:52:06.709Z","updatedAt":"2026-09-25T10:52:06.709Z"},{"id":"cmugucuer00kqqu06zxz2wxjh","slug":"nanocoai-nanoclaw-add-rtk","name":"add-rtk","description":"Install rtk token-compression proxy into agent containers. Routes Bash tool calls through rtk for 60–90% token savings on dev commands (git, cargo, pytest, docker, kubectl, etc.).","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-rtk","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Install rtk token-compression proxy into agent containers. Routes Bash tool calls through rtk for 60–90% token savings on dev commands (git, cargo, pytest, docker, kubectl, etc.).","permissions":[],"systemPrompt":"# Add rtk\n\nInstall [rtk](https://github.com/rtk-ai/rtk) — a CLI proxy delivering 60–90% token savings on common dev commands (git, cargo, pytest, docker, kubectl, etc.) — and wire it transparently into agent containers via the Claude Code `PreToolUse` hook.\n\n## What this sets up\n\n- `rtk` binary at `~/.local/bin/rtk` on the host\n- `~/.local/bin/rtk` mounted read-only at `/usr/local/bin/rtk` inside the target agent group's containers\n- `PreToolUse` hook in the agent group's `settings.json` so every Bash call is automatically filtered through rtk — no CLAUDE.md instructions needed\n\n## Integration tests\n\nThis skill has **no in-tree integration test** by design. Its only functional reach-ins are runtime operator actions — the host-only `ncl groups config add-mount` (Step 3) and the `settings.json` `PreToolUse` hook write (Step 4) — neither of which leaves a line in the source tree whose deletion a test could catch. There are no package dependencies or Dockerfile edits to guard either. Conformance is idempotent apply + `REMOVE.md`; the mount and hook are verified at runtime (see Verify).\n\n## Step 1 — Install rtk on the host\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh\n```\n\nIf the script put the binary elsewhere, move it:\n\n```bash\nfind ~/.local ~/.cargo/bin ~/bin -name rtk 2>/dev/null\nmv \"$(which rtk 2>/dev/null)\" ~/.local/bin/rtk\n```\n\nVerify:\n\n```bash\n~/.local/bin/rtk --version\nchmod +x ~/.local/bin/rtk   # if needed\n```\n\n## Step 2 — Identify the target agent group\n\n```bash\nncl groups list\n```\n\nNote the group ID (e.g. `ag-1776342942165-ptgddd`). Repeat Steps 3–5 for each group.\n\n## Step 3 — Mount rtk into the container config\n\nMount the host rtk binary read-only into the container with the host-only `add-mount` verb. It is idempotent — re-running skips the entry if it is already present:\n\n```bash\nncl groups config add-mount --id <group-id> \\\n  --host ~/.local/bin/rtk \\\n  --container /usr/local/bin/rtk \\\n  --ro\n```\n\nThis verb is operator-only and runs host-side (via `/setup`, `/customize`, or `/manage-mounts`); it is rejected from inside a container.\n\nThe host root (`~/.local/bin`) must also be in the external mount allowlist at `~/.config/nanoclaw/mount-allowlist.json` for the mount to take effect at spawn. Add it there if it isn't already.\n\nVerify:\n\n```bash\nncl groups config get --id <group-id>\n# Look for the /usr/local/bin/rtk mount\n```\n\n## Step 4 — Add the PreToolUse hook to settings.json\n\nEach agent group has a `settings.json` at:\n\n```\ndata/v2-sessions/<group-id>/.claude-shared/settings.json\n```\n\nThis file is mounted at `/home/node/.claude/settings.json` inside the container and is read by Claude Code for hooks, env, and model config.\n\nAdd the `PreToolUse` entry with `jq`. This drops any existing rtk Bash hook first, then appends a fresh one, so it is safe to re-run without creating duplicates:\n\n```bash\nSETTINGS=\"data/v2-sessions/<group-id>/.claude-shared/settings.json\"\n\njq '.hooks.PreToolUse = ((.hooks.PreToolUse // [])\n      | map(select((.hooks // []) | any(.command == \"rtk hook claude\") | not)))\n    + [{\"matcher\":\"Bash\",\"hooks\":[{\"type\":\"command\",\"command\":\"rtk hook claude\"}]}]' \\\n  \"$SETTINGS\" > /tmp/rtk-settings.json && mv /tmp/rtk-settings.json \"$SETTINGS\"\n```\n\n## Step 5 — Restart the container\n\n```bash\nncl groups restart --id <group-id>\n```\n\n## Verify\n\nConfirm the binary is executable inside the container so a missing or non-executable mount surfaces immediately rather than as a silent hook failure:\n\n```bash\ndocker exec \"$(docker ps --filter \"name=<group-id>\" --format '{{.Names}}' | head -1)\" rtk --version\n```\n\nThen ask the agent to run `git status` or any other supported command. rtk intercepts it silently. Check savings with:\n\n```bash\n~/.local/bin/rtk gain\n```\n\n## Troubleshooting\n\n### `rtk: command not found` inside the container\n\nMount wasn't applied or container wasn't restarted:\n\n```bash\nncl groups config get --id <group-id>\n# Look for the /usr/local/bin/rtk mount\nncl groups restart --id <group-id>\n```\n\n### Hook not firing\n\nVerify the hook is in `settings.json`:\n\n```bash\njq '.hooks.PreToolUse' data/v2-sessions/<group-id>/.claude-shared/settings.json\n```\n\nIf missing, re-run Step 4.\n\n### Binary won't execute — permission denied\n\n```bash\nchmod +x ~/.local/bin/rtk\n```","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-rtk","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-rtk/SKILL.md","defaultBranch":"main"},"readme":"# Add rtk\n\nInstall [rtk](https://github.com/rtk-ai/rtk) — a CLI proxy delivering 60–90% token savings on common dev commands (git, cargo, pytest, docker, kubectl, etc.) — and wire it transparently into agent containers via the Claude Code `PreToolUse` hook.\n\n## What this sets up\n\n- `rtk` binary at `~/.local/bin/rtk` on the host\n- `~/.local/bin/rtk` mounted read-only at `/usr/local/bin/rtk` inside the target agent group's containers\n- `PreToolUse` hook in the agent group's `settings.json` so every Bash call is automatically filtered through rtk — no CLAUDE.md instructions needed\n\n## Integration tests\n\nThis skill has **no in-tree integration test** by design. Its only functional reach-ins are runtime operator actions — the host-only `ncl groups config add-mount` (Step 3) and the `settings.json` `PreToolUse` hook write (Step 4) — neither of which leaves a line in the source tree whose deletion a test could catch. There are no package dependencies or Dockerfile edits to guard either. Conformance is idempotent apply + `REMOVE.md`; the mount and hook are verified at runtime (see Verify).\n\n## Step 1 — Install rtk on the host\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh\n```\n\nIf the script put the binary elsewhere, move it:\n\n```bash\nfind ~/.local ~/.cargo/bin ~/bin -name rtk 2>/dev/null\nmv \"$(which rtk 2>/dev/null)\" ~/.local/bin/rtk\n```\n\nVerify:\n\n```bash\n~/.local/bin/rtk --version\nchmod +x ~/.local/bin/rtk   # if needed\n```\n\n## Step 2 — Identify the target agent group\n\n```bash\nncl groups list\n```\n\nNote the group ID (e.g. `ag-1776342942165-ptgddd`). Repeat Steps 3–5 for each group.\n\n## Step 3 — Mount rtk into the container config\n\nMount the host rtk binary read-only into the container with the host-only `add-mount` verb. It is idempotent — re-running skips the entry if it is already present:\n\n```bash\nncl groups config add-mount --id <group-id> \\\n  --host ~/.local/bin/rtk \\\n  --container /usr/local/bin/rtk \\\n  --ro\n```\n\nThis verb is operator-only and runs host-side (via `/setup`, `/customize`, or `/manage-mounts`); it is rejected from inside a container.\n\nThe host root (`~/.local/bin`) must also be in the external mount allowlist at `~/.config/nanoclaw/mount-allowlist.json` for the mount to take effect at spawn. Add it there if it isn't already.\n\nVerify:\n\n```bash\nncl groups config get --id <group-id>\n# Look for the /usr/local/bin/rtk mount\n```\n\n## Step 4 — Add the PreToolUse hook to settings.json\n\nEach agent group has a `settings.json` at:\n\n```\ndata/v2-sessions/<group-id>/.claude-shared/settings.json\n```\n\nThis file is mounted at `/home/node/.claude/settings.json` inside the container and is read by Claude Code for hooks, env, and model config.\n\nAdd the `PreToolUse` entry with `jq`. This drops any existing rtk Bash hook first, then appends a fresh one, so it is safe to re-run without creating duplicates:\n\n```bash\nSETTINGS=\"data/v2-sessions/<group-id>/.claude-shared/settings.json\"\n\njq '.hooks.PreToolUse = ((.hooks.PreToolUse // [])\n      | map(select((.hooks // []) | any(.command == \"rtk hook claude\") | not)))\n    + [{\"matcher\":\"Bash\",\"hooks\":[{\"type\":\"command\",\"command\":\"rtk hook claude\"}]}]' \\\n  \"$SETTINGS\" > /tmp/rtk-settings.json && mv /tmp/rtk-settings.json \"$SETTINGS\"\n```\n\n## Step 5 — Restart the container\n\n```bash\nncl groups restart --id <group-id>\n```\n\n## Verify\n\nConfirm the binary is executable inside the container so a missing or non-executable mount surfaces immediately rather than as a silent hook failure:\n\n```bash\ndocker exec \"$(docker ps --filter \"name=<group-id>\" --format '{{.Names}}' | head -1)\" rtk --version\n```\n\nThen ask the agent to run `git status` or any other supported command. rtk intercepts it silently. Check savings with:\n\n```bash\n~/.local/bin/rtk gain\n```\n\n## Troubleshooting\n\n### `rtk: command not found` inside the container\n\nMount wasn't applied or container wasn't restarted:\n\n```bash\nncl groups config get --id <group-id>\n# Look for the /usr/local/bin/rtk moun","createdAt":"2026-09-25T10:52:06.771Z","updatedAt":"2026-09-25T10:52:06.771Z"},{"id":"cmugucubc00jnqu06soy0ct8h","slug":"nanocoai-nanoclaw-add-imessage","name":"add-imessage","description":"Add iMessage to NanoClaw — one channel, two backends. Local (this Mac's chat.db via the Chat SDK bridge; macOS + Full Disk Access) or Hosted iMessage (via photon.codes — native spectrum-ts with a device-login wizard; any OS, no Mac relay). Triggers on \"add imessage\", \"connect imessage\", \"add photon\", \"imessage via photon\", \"native imessage\".","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-imessage","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add iMessage to NanoClaw — one channel, two backends. Local (this Mac's chat.db via the Chat SDK bridge; macOS + Full Disk Access) or Hosted iMessage (via photon.codes — native spectrum-ts with a device-login wizard; any OS, no Mac relay). Triggers on \"add imessage\", \"connect imessage\", \"add photon\", \"imessage via photon\", \"native imessage\".","permissions":[],"systemPrompt":"# Add iMessage\n\nNanoClaw talks to iMessage through a single **`imessage`** channel with two\npluggable backends:\n\n- **Local (this Mac)** — the Chat SDK bridge over `chat-adapter-imessage`,\n  reading this Mac's signed-in iMessage account (`chat.db`). macOS only; the\n  Node binary needs Full Disk Access.\n- **Hosted iMessage (via photon.codes)** — a native adapter over Photon's\n  `spectrum-ts` gRPC stream. The hosted service owns the iMessage line, so\n  there's no Mac relay, webhook, or public URL. Works on any OS, and a\n  device-login flow provisions everything for you.\n\nBoth register the same `imessage` channel type; only one runs per install.\nNanoClaw doesn't ship channels in trunk — this skill copies the unified\n`imessage` adapter in from the `channels` branch. Full reference:\n[docs/imessage.md](docs.md).\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent reads\nthe prose and applies them, and a parser can apply them deterministically from\nthe same document. Every directive is idempotent, so the whole skill is safe to\nre-run; anything a parser can't apply falls back to the prose beside it.\n\n## Apply\n\n### 1. Choose a backend\n\nPick the backend first — it decides which package gets installed and which\nwalkthrough runs below (the other backend's steps are skipped):\n\n```nc:prompt backend validate:^(local|hosted)$\nHow should iMessage run — `local` (this Mac's signed-in iMessage account; macOS only, needs Full Disk Access) or `hosted` (a managed line via photon.codes; works on any OS)?\n```\n\nThe local backend only works on a Mac — it reads this machine's iMessage\n`chat.db` directly, and there is no such database off macOS. On any other OS,\nstop here and choose `hosted` instead; otherwise you'd write a local config\nthat can never receive a message:\n\n```nc:run effect:check when:backend=local\n[ \"$(uname)\" = Darwin ]\n```\n\n### 2. Copy the adapter\n\nFetch the `channels` branch and copy the unified iMessage adapter and its tests\ninto `src/channels/`:\n\n```nc:copy from-branch:channels\nsrc/channels/imessage.ts\nsrc/channels/imessage.test.ts\nsrc/channels/imessage-registration.test.ts\n```\n\n### 3. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './imessage.js';\n```\n\n### 4. Install the chosen backend's package\n\nPinned to an exact version — the supply-chain policy rejects ranges and\n`latest`. Install only the chosen backend's package.\n\n**Local** — the Chat SDK iMessage adapter:\n\n```nc:dep when:backend=local\nchat-adapter-imessage@0.1.1\n```\n\n**Hosted** — Photon's Spectrum SDK:\n\n```nc:dep when:backend=hosted\nspectrum-ts@11.0.0\n```\n\n> Pin exactly. `spectrum-ts` ships breaking majors (v11 is what the adapter\n> targets); don't `@latest`. NanoClaw's pnpm gate (`minimumReleaseAge`) requires\n> a version ≥3 days old — both pins clear it. A fresher pin needs human sign-off\n> before a `minimumReleaseAgeExclude` entry (CLAUDE.md → Supply Chain Security).\n\n### 5. Build and validate\n\nBuild guards the typed `createChatSdkBridge(...)` core call used by the local\nbackend, and the registration test proves the channel is wired:\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/imessage-registration.test.ts\n```\n\nBoth must be clean. `imessage-registration.test.ts` imports the real channel\nbarrel and asserts the registry contains `imessage` — it goes red if the\n`import './imessage.js';` line is missing or the barrel fails to evaluate. The\nadapter loads neither backend's SDK at import (hosted `spectrum-ts` only in\n`setup()`, local `chat-adapter-imessage` only in the factory), so the test\nneeds no package.\n\nFor the hosted backend, also run the full adapter suite — it includes an\nintegration block that exercises the real installed `spectrum-ts` (version,\nexports, builders) and auto-skips when the package is absent:\n\n```nc:run effect:test when:backend=hosted\npnpm exec vitest run src/channels/imessage.test.ts\n```\n\n## Local backend: Full Disk Access (macOS)\n\nThe adapter reads this Mac's `chat.db`, which requires Full Disk Access granted\nto the Node binary the host runs under. The Node path is buried deep (e.g.\n`~/.nvm/versions/node/v22.x.x/bin/node`), so open its folder in Finder to make\nthe drag-and-drop target obvious. Harmless off a desktop (SSH/headless) — it\njust no-ops:\n\n```nc:run effect:external when:backend=local\nopen \"$(dirname \"$(which node)\")\" 2>/dev/null || true\n```\n\nThen tell the user:\n\n```nc:operator when:backend=local\nGrant Full Disk Access to Node so iMessage can read your chat history:\n1. Open System Settings > Privacy & Security > Full Disk Access.\n2. Click +, then drag the \"node\" file from the Finder window that just opened.\n3. Toggle it on, then come back here.\n```\n\nStop and wait for the user to confirm Full Disk Access is granted before\ncontinuing.\n\nNow select the local backend in `.env`. The configure script owns this\nupsert-and-remove (a plain set-if-absent env write can neither replace a stale\nvalue nor delete a key, and a lingering hosted selector would shadow the\nchoice):\n\n```nc:run effect:external when:backend=local\nbash setup/channels/imessage-configure.sh local\n```\n\n## Hosted backend: device login (via photon.codes)\n\nThe provisioning flow needs the phone number you send iMessages from — it\nregisters that number with your project so the hosted line recognises you:\n\n```nc:prompt owner_handle normalize:trim validate:^\\+\\d{8,15}$ when:backend=hosted\nThe phone number you iMessage from, in E.164 format — + followed by country code and number, no spaces or dashes (e.g. +14155551234).\n```\n\nTell the user what's about to happen:\n\n```nc:operator when:backend=hosted\nConnect your hosted iMessage line (photon.codes):\n1. A login URL and a short code will print below.\n2. Open the URL in a browser, approve the device, and enter the code.\n3. Setup then registers your number and prints the iMessage line Photon assigned to it. Send one message from your phone to that line — a number only enters routing after it has texted its line once.\n4. Once the opt-in lands, setup finishes on its own and confirms your agent's iMessage number.\n```\n\nRun the device-login flow. It provisions the project, reuses its current secret\n(regenerating only when the API returns none), registers your number, prints\nthe line to text and waits until that message opts the number in, and surfaces\nthe iMessage number you'll use — writing\n`PHOTON_PROJECT_ID` + `PHOTON_PROJECT_SECRET` to `.env` and the assigned number\nto `data/photon-auth.json`:\n\n```nc:run effect:step when:backend=hosted\npnpm exec tsx scripts/photon-setup.ts setup --phone {{owner_handle}} --embedded\n```\n\nIf the login times out, the code expired (~30 min) — re-run the step; a stored\ntoken is reused. Check state any time with\n`pnpm exec tsx scripts/photon-setup.ts status`.\n\nThen select the hosted backend in `.env` — the Photon credentials already imply\nhosted, but the explicit selector avoids ambiguity if local keys linger:\n\n```nc:run effect:external when:backend=hosted\nbash setup/channels/imessage-configure.sh hosted\n```\n\n## Restart\n\nRestart the service so it loads the iMessage adapter and the backend config you\njust wrote, and wait for its CLI socket before wiring:\n\n```nc:run effect:restart\nbash setup/lib/restart.sh\n```\n\nFor the hosted backend, confirm the connection came up:\n`grep \"Photon channel connected\" logs/nanoclaw.log | tail -1`.\n\n## Resolve your iMessage handle\n\nThe agent greets you in the iMessage conversation tied to the handle you\nmessage from — that handle is both your identity and the conversation address.\nThe hosted flow already collected it above; for the local backend, resolve it\nnow (email works too — whatever iMessage recognises):\n\n```nc:prompt owner_handle validate:^(\\+\\d{8,15}|[^\\s@]+@[^\\s@]+\\.[^\\s@]+)$ when:backend=local\nThe phone number or email you iMessage from — a +E.164 number (e.g. +14155551234) or an email / Apple ID (e.g. you@icloud.com).\n```\n\n**Hosted first contact:** text your agent's iMessage number once (it was\nprinted above; also stored in `data/photon-auth.json`) before expecting any\nmessage from it. This first text is required, not just convenient — the hosted\nline can only message numbers that have already texted it (cold outbound is\nrejected with `Target not allowed for this project`). Tell the user:\n\n```nc:operator when:backend=hosted\nSend one text — anything — from your phone to your agent's iMessage number (printed above). The hosted line can only reply to numbers that have texted it first, so its welcome message needs yours to arrive first.\n```\n\niMessage is a native channel: it sends the raw handle as the conversation\naddress, with no channel prefix — so the messaging-group platform id is that\nhandle as-is:\n\n```nc:run capture:platform_id\necho \"{{owner_handle}}\"\n```\n\n`owner_handle` and `platform_id` are what the owner-wiring step needs. The\nwelcome iMessage goes out through the adapter once the service is running — on\nthe local backend that needs Full Disk Access granted (above); on the hosted\nbackend it goes out via your photon.codes line after your first text.\n\n## Next Steps\n\nIf you're in the middle of `/setup`, return to the setup flow now. Otherwise\n`/init-first-agent` stands up an agent on your iMessage DM, or `/manage-channels`\nwires it to an existing agent group.\n\n## Channel Info\n\n- **type**: `imessage` (one channel; the backend is local or hosted)\n- **terminology**: iMessage has 1:1 \"chats\" (DMs) and group chats. Photon\n  (hosted) calls each conversation a \"space\".\n- **platform-id-format**: DM = your bare handle (E.164 phone, or email for\n  local) — direct-addressable; the user id is `imessage:<handle>`. Group\n  (hosted) = the opaque Spectrum space id.\n- **how-to-find-id**: DMs use the counterpart's phone/email. Groups (hosted) are\n  discovered on first message —\n  `pnpm exec tsx scripts/q.ts data/v2.db \"SELECT platform_id, name FROM messaging_groups WHERE channel_type='imessage'\"`\n- **supports-threads**: no\n- **typical-use**: Interactive 1:1 chat — personal messaging\n- **default-isolation**: One agent per install. Multiple DMs with the same\n  operator can share an agent group; groups with other people should typically\n  use `isolated` session mode.\n\n### Hosted features\n\nMarkdown (native; `PHOTON_MARKDOWN=false` for plain text), file attachments in\nand out (inbound staged into the session inbox, capped by\n`PHOTON_MAX_INLINE_ATTACHMENT_BYTES`, default 20 MB), tapback reactions, read\nreceipts, typing indicators, and `ask_user_question` via `/approve` / `/reject`\nslash replies. Optional `.env`: `PHOTON_MARKDOWN`, `PHOTON_TELEMETRY`,\n`PHOTON_MAX_INLINE_ATTACHMENT_BYTES`, `PHOTON_DASHBOARD_HOST`,\n`PHOTON_SPECTRUM_HOST`. Full table in [docs/imessage.md](docs.md).\n\n## Troubleshooting\n\n**The backend answer is rejected.** It must be exactly `local` or `hosted`,\nlowercase. Local only exists on macOS — it reads this Mac's `chat.db` directly —\nso on any other OS the platform check stops you and hosted is the only path.\n\n**Local: outgoing works but nothing ever arrives.** Full Disk Access wasn't\ngranted to the *actual* Node binary the service runs under — with nvm the path\nchanges per Node version (`~/.nvm/versions/node/v22.x.x/bin/node`), so an old\ngrant silently stops covering a new binary. Re-open System Settings → Privacy &\nSecurity → Full Disk Access, add the binary at `$(which node)`, then restart\nthe service.\n\n**`spectrum-ts` not installed** (hosted) — re-run step 4\n(`pnpm install spectrum-ts@11.0.0`) and restart.\n\n**Device login times out** (hosted) — the code expires in ~30 min; re-run the\nlogin step (a stored token is reused).\n\n**`Target not allowed for this project`** (hosted) — intended: the line only\nmessages numbers that have texted it first. Text the agent's number once, then\nretry (a welcome DM queued before that first text simply fails delivery).\n\n**Your handle is rejected at the resolve step.** It must be a bare +E.164\nnumber (`+14155551234` — no spaces, dashes, or parentheses) or, on the local\nbackend, an email/Apple ID. Use the exact handle you actually send iMessages\nfrom — a number-vs-email mismatch means your messages never map to the wired\nconversation.\n\n**Adapter installed but silent.** Run\n`pnpm exec vitest run src/channels/imessage-registration.test.ts` — red means\nthe barrel import or the package install drifted, so re-run the Apply steps.\nIf green, confirm the backend connected (hosted:\n`grep \"Photon channel connected\" logs/nanoclaw.log`), restart the service\n(`bash setup/lib/restart.sh`), then check `logs/nanoclaw.error.log`.\n\nMore in [docs/imessage.md](docs.md).\n\n## Upgrading spectrum-ts (hosted)\n\n`spectrum-ts` is pinned exactly because it ships breaking majors. To upgrade,\nread the [release notes](https://github.com/photon-hq/spectrum-ts/releases) for\nevery version between the pins, bump the pin, reconcile\n`src/channels/imessage.ts` against the new typings, then `pnpm run build` and\n`pnpm exec vitest run src/channels/imessage.test.ts`. See\n[docs/imessage.md](docs.md).","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-imessage","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-imessage/SKILL.md","defaultBranch":"main"},"readme":"# Add iMessage\n\nNanoClaw talks to iMessage through a single **`imessage`** channel with two\npluggable backends:\n\n- **Local (this Mac)** — the Chat SDK bridge over `chat-adapter-imessage`,\n  reading this Mac's signed-in iMessage account (`chat.db`). macOS only; the\n  Node binary needs Full Disk Access.\n- **Hosted iMessage (via photon.codes)** — a native adapter over Photon's\n  `spectrum-ts` gRPC stream. The hosted service owns the iMessage line, so\n  there's no Mac relay, webhook, or public URL. Works on any OS, and a\n  device-login flow provisions everything for you.\n\nBoth register the same `imessage` channel type; only one runs per install.\nNanoClaw doesn't ship channels in trunk — this skill copies the unified\n`imessage` adapter in from the `channels` branch. Full reference:\n[docs/imessage.md](docs.md).\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent reads\nthe prose and applies them, and a parser can apply them deterministically from\nthe same document. Every directive is idempotent, so the whole skill is safe to\nre-run; anything a parser can't apply falls back to the prose beside it.\n\n## Apply\n\n### 1. Choose a backend\n\nPick the backend first — it decides which package gets installed and which\nwalkthrough runs below (the other backend's steps are skipped):\n\n```nc:prompt backend validate:^(local|hosted)$\nHow should iMessage run — `local` (this Mac's signed-in iMessage account; macOS only, needs Full Disk Access) or `hosted` (a managed line via photon.codes; works on any OS)?\n```\n\nThe local backend only works on a Mac — it reads this machine's iMessage\n`chat.db` directly, and there is no such database off macOS. On any other OS,\nstop here and choose `hosted` instead; otherwise you'd write a local config\nthat can never receive a message:\n\n```nc:run effect:check when:backend=local\n[ \"$(uname)\" = Darwin ]\n```\n\n### 2. Copy the adapter\n\nFetch the `channels` branch and copy the unified iMessage adapter and its tests\ninto `src/channels/`:\n\n```nc:copy from-branch:channels\nsrc/channels/imessage.ts\nsrc/channels/imessage.test.ts\nsrc/channels/imessage-registration.test.ts\n```\n\n### 3. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './imessage.js';\n```\n\n### 4. Install the chosen backend's package\n\nPinned to an exact version — the supply-chain policy rejects ranges and\n`latest`. Install only the chosen backend's package.\n\n**Local** — the Chat SDK iMessage adapter:\n\n```nc:dep when:backend=local\nchat-adapter-imessage@0.1.1\n```\n\n**Hosted** — Photon's Spectrum SDK:\n\n```nc:dep when:backend=hosted\nspectrum-ts@11.0.0\n```\n\n> Pin exactly. `spectrum-ts` ships breaking majors (v11 is what the adapter\n> targets); don't `@latest`. NanoClaw's pnpm gate (`minimumReleaseAge`) requires\n> a version ≥3 days old — both pins clear it. A fresher pin needs human sign-off\n> before a `minimumReleaseAgeExclude` entry (CLAUDE.md → Supply Chain Security).\n\n### 5. Build and validate\n\nBuild guards the typed `createChatSdkBridge(...)` core call used by the local\nbackend, and the registration test proves the channel is wired:\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/imessage-registration.test.ts\n```\n\nBoth must be clean. `imessage-registration.test.ts` imports the real channel\nbarrel and asserts the registry contains `imessage` — it goes red if the\n`import './imessage.js';` line is missing or the barrel fails to evaluate. The\nadapter loads neither backend's SDK at import (hosted `spectrum-ts` only in\n`setup()`, local `chat-adapter-imessage` only in the factory), so the test\nneeds no package.\n\nFor the hosted backend, also run the full adapter suite — it includes an\nintegration block that exercises the real installed `spectrum-ts` (version,\nexports, builders) and auto-skips when the package is absent:\n\n```nc:run effect:","createdAt":"2026-09-25T10:52:06.648Z","updatedAt":"2026-09-25T10:52:06.648Z"},{"id":"cmugucubl00jqqu0684zwmmou","slug":"nanocoai-nanoclaw-add-iron-proxy","name":"add-iron-proxy","description":"Install or refresh Iron Proxy and its official Iron Control web console for NanoClaw. Use when selecting the Iron gateway, adding its management UI, configuring credentials and grants, or restoring the proxy, approval bridge, and agent guidance.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-iron-proxy","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Install or refresh Iron Proxy and its official Iron Control web console for NanoClaw. Use when selecting the Iron gateway, adding its management UI, configuring credentials and grants, or restoring the proxy, approval bridge, and agent guidance.","permissions":[],"systemPrompt":"# Add Iron Proxy gateway\n\nInstall the official `iron-control` console and PostgreSQL alongside one central Iron Proxy. NanoClaw core supplies the generic gateway seam and human approval flow. Read `docs/gateway-seam.md` before changing the integration.\n\nUse the bundled setup scripts and the source revisions in `versions.json`. The console is the upstream Rails application; its UI is not generated or copied into NanoClaw. This is a local Docker installation. Reuse this copy's recorded services and keys when refreshing it.\n\n## Install the provider payload\n\nCopy the package's provider, approval middleware, tests, and agent guidance into their normal NanoClaw paths.\n\n```nc:copy\npayload/src/gateway-providers/iron-proxy.ts -> src/gateway-providers/iron-proxy.ts\npayload/src/gateway-providers/iron-proxy.test.ts -> src/gateway-providers/iron-proxy.test.ts\npayload/src/gateway-providers/iron-proxy-approval.ts -> src/gateway-providers/iron-proxy-approval.ts\npayload/src/gateway-providers/iron-proxy-approval.test.ts -> src/gateway-providers/iron-proxy-approval.test.ts\npayload/src/gateway-providers/iron-proxy-transform.proto -> src/gateway-providers/iron-proxy-transform.proto\npayload/container/skills/iron-proxy-gateway/SKILL.md -> container/skills/iron-proxy-gateway/SKILL.md\npayload/container/skills/iron-proxy-gateway/instructions.md -> container/skills/iron-proxy-gateway/instructions.md\n```\n\n## Register once\n\nThe provider file makes the only product registration call. It declares idempotent sessions, typed runtime contributions, owned-resource cleanup, normalized approvals, network access, and agent guidance. NanoClaw core owns approval persistence, cards, clicks, authorization, and timeouts.\n\n```nc:append to:src/gateway-providers/installed.ts\nimport './iron-proxy.js';\n```\n\n## Install the bridge dependencies\n\n```nc:dep manager:pnpm\n@grpc/grpc-js@1.14.4\n@grpc/proto-loader@0.8.1\n```\n\n## Install the console and pinned proxy\n\nSetup pulls the pinned official Iron Control image and database image, starts them on a dedicated Docker network, creates a local operator account, and registers this copy's proxy and principal through Iron's API. It stores credentials and encryption keys in owner-only files under `data/session-materials/iron-control/`. The database has its own persistent volume and no published port. Neither the console credentials nor its database are mounted into agents.\n\nThe installer streams stage names and elapsed-time updates. Source downloads stop after two minutes, the image build after twenty minutes, and console startup after six minutes. It never opens a Git credential prompt. The proxy is built locally from the pinned public upstream source, so installation does not require a GitHub account or access to a private proxy image. The console and build dependencies use their pinned public images. On failure, fix the reported access or service issue and rerun setup; keep existing database volumes and encryption keys together. Raw subprocess output is not streamed because it can contain credentials.\n\nSetup builds unmodified upstream Iron Proxy and a separate NanoClaw approval front in the same image. No Iron fork or source patch is used. The front is the only network-facing listener. It authenticates session identities, inspects each HTTP request inside HTTPS tunnels, checks the allowlist, and waits for an explicit approval before forwarding to Iron on `127.0.0.1:18080`. Empty, malformed, rejected or timed-out decisions fail closed. Iron's own dial-time loopback and link-local deny rules prevent DNS aliases from reaching the internal backend. Managed control-plane updates only change Iron's credential transforms; they cannot remove the front's checks.\n\nThe front builds and runs the pinned OneCLI helper in `gateway-compat/onecli-summary`; do not add app-specific rules. Only method, host, path, response status and the resulting OneCLI summary reach the approval bridge. Raw bodies, authorization headers, query strings and Iron transform traces do not. The helper sees a bounded pre-injection body prefix; the full original stream is preserved. Read the approval-presentation contract in `docs/gateway-seam.md`. The build tests stock Iron, the front, and the summary helper, then records an immutable image ID and source hash.\nNanoClaw's approval service uses a private Unix socket on Linux. On macOS it uses loopback with mutual TLS and a proxy-only client certificate. Credentials pass directly from Iron Control to Iron Proxy.\n\n`NANOCLAW_IRON_PROXY_PORT` in `.env` sets the internal proxy port (default `8080`). Setup uses the same value for the front listener and the agent proxy URL. This does not publish a host port. Re-run setup and restart this NanoClaw copy after changing it.\n\n`NANOCLAW_IRON_CONTROL_PORT` sets the console port (default `10257`). Only `127.0.0.1` is published. Set it before setup if another install uses that port; use the URL printed by setup. The official image currently targets `linux/amd64`; Docker on Apple Silicon runs it with emulation.\n\n```nc:run effect:step\npnpm exec tsx .claude/skills/add-iron-proxy/scripts/setup.ts --with-control\n```\n\n## Troubleshooting\n\n- **Source access fails:** verify the pinned source is readable from this machine.\n  The installer uses the public source in `versions.json` and disables interactive\n  Git prompts. Check connectivity and the pinned revision, then retry.\n- **A command times out:** use the last printed stage to identify whether source\n  download, image build, or console startup failed. Check connectivity and Docker\n  health before retrying. The installer terminates the timed-out process group.\n- **The database exists but keys are missing:** restore its matching `control.env`.\n  Keep the database volume and encryption keys together; do not generate replacement\n  keys for an existing database.\n\n## Validate\n\n```nc:run effect:build\npnpm run build\n```\n\n```nc:run effect:test\npnpm exec vitest run src/gateway-providers/iron-proxy.test.ts src/gateway-providers/iron-proxy-approval.test.ts src/gateway-providers/gateway-provider-registry.test.ts src/gateway-approval-coordinator.test.ts .claude/skills/add-iron-proxy/scripts/control.test.ts .claude/skills/add-iron-proxy/scripts/provider-credentials.test.ts .claude/skills/add-iron-proxy/scripts/credential-isolation.test.ts .claude/skills/add-iron-proxy/scripts/install-command.test.ts\n```\n\nThe setup consumer writes `NANOCLAW_GATEWAY_PROVIDER=iron-proxy` only after every directive succeeds. Restart only this copy's NanoClaw service after an upgrade so its session contribution and approval bridge match the new installation. Check the proxy has synced its assigned principal before reporting the gateway ready.\n\nThe request order is front identity and allowlist → human approval → stock Iron credentials → upstream. The front also requires an explicit response decision before returning upstream data. HTTPS tunnels pin the target authority; each inner HTTP request is checked again. Streaming responses and WebSocket upgrades use this same request/response gate. Credentialed application requests use HTTPS; the approval bridge rejects plaintext HTTP destinations. Do not rewrite an HTTP request’s approval metadata as HTTPS to bypass that restriction. The bridge forwards every allowed HTTP request to core as a default approval request. Core uses the active agent provider’s model-domain declaration to permit model traffic without a card; other destinations retain human approval. CONNECT verifies identity; the inner HTTP request is the approval point. Existing standalone model credentials are moved into Iron Control during setup and removed from the old secret file after successful storage and grant.\n\n## Open and use the official console\n\nOpen the URL printed by setup. Give the operator the path to `login.txt` in the same private directory; keep passwords and API tokens out of chat and command logs. `control.ts status` prints only the URL and login-file location.\n\nThe pinned console provides **Secrets**, **Credentials**, **OAuth Apps**, and **Principals**. Use Secrets to create a credential, choose its source, and set its host, method, and path rules. The current console shows principals and grants but creates grants through its API. Apply a secret to this NanoClaw copy with the bundled helper:\n\n```bash\npnpm exec tsx .claude/skills/add-iron-proxy/scripts/control.ts grant static <secret-id>\n```\n\nOther supported kinds are `gcp`, `aws`, `oauth`, `postgres`, and `hmac`. The command only grants an existing credential to this install's recorded principal. Read [references/control-plane.md](references/control-plane.md) for the exact supported UI, source links, policy example, and verification steps.\n\nThe pinned source has no general egress Policies screen or audit search. Do not promise the screens shown in newer or hosted documentation. A grant's request rules govern credential use; the local egress allowlist remains separate.\n\nTo allow another destination explicitly, the operator runs:\n\n```bash\npnpm exec tsx .claude/skills/add-iron-proxy/scripts/setup.ts --allow-host <hostname-or-*.domain>\n```\n\nFor a standalone proxy without the console, omit `--with-control` on a fresh install. A recorded console is preserved on refresh. Both modes build the pinned public source. To reuse an independently built image, build the exact commit from `versions.json`\nand label the image `org.opencontainers.image.revision` with that commit. Then run\n`pnpm exec tsx .claude/skills/add-iron-proxy/scripts/setup.ts --local-image <image>`.\nSetup checks the revision and records the immutable local image ID. All installs require the exact bundled approval-front label; a stock Iron image alone is not the full NanoClaw integration.\n\n## Codex authentication\n\nUse the existing provider-auth entry point after `/add-codex` installs the\nprovider. Browser sign-in, device pairing, and API-key entry are the same for\nall gateways; `scripts/credential-store.ts` supplies Iron's custody adapter.\nIron Control stores API keys and manages subscription refresh-token rotation\nthrough its native broker. Agents receive only a synthetic `auth.json` file.\nA subscription login uses a new, dedicated Codex session; never copy personal\nCodex credentials. The setup flow and provider picker stay unchanged.\n\n```bash\npnpm exec tsx setup/index.ts --step provider-auth codex\n```\n\n## OpenCode authentication\n\nAfter installing `/add-opencode`, use the same provider-auth entry point:\n\n```bash\npnpm exec tsx setup/index.ts --step provider-auth opencode\n```\n\nThe OpenCode setup flow supports ChatGPT sign-in and API keys through Iron\nControl. It installs no OneCLI service and needs no OneCLI management settings.\nChatGPT arrives as the seam's `chatgpt` OAuth profile: Iron creates a native\nbroker from OpenCode's public OAuth client and refresh token, and a separate\ngranted secret carries the `ChatGPT-Account-Id` header; any other OAuth profile\nis rejected. The agent sees only placeholders. Initial sign-in and reauthentication wait for the\nnative broker to refresh successfully (up to two minutes) before setup continues. API keys use each backend's declared header\nscheme. Setup grants the secrets to this install's principal and permits the\nmodel hostname. Rotation and reauthentication keep IDs and grants. Moving a key\nto another host requires confirmation and re-entering its value.\n\nNative backends and custom/keyless HTTPS endpoints on port 443 are supported.\nUse a DNS name and TLS for local models; plaintext HTTP endpoints fail during\nsetup. Follow the OpenCode skill to restart the host and test a real reply.\n\n## Remove\n\nFollow [REMOVE.md](REMOVE.md). Stop only this copy's proxy and console services. Keep the database volume and encryption keys together when preserving data.\n\n### Provider credentials on shared model hosts\n\nOpenCode and Codex use distinct non-secret markers. Iron replaces only the matching\nheader marker, so each runtime retains its own account on a shared HTTPS host.\nClaude's managed model marker remains distinct. These replacements use\n`require: false`: a request from another provider must pass without substitution.\nThe upstream API still rejects an unavailable or unmatched placeholder.\n\nRefresh the installed provider payloads and rebuild the agent image before using\nthis version. Reconnect older Codex credentials to replace their host-wide injection\nrules. If setup identifies a conflicting legacy or manual grant, reconnect that\nprovider with this version (including Claude's model credential), or remove the\nconflicting grant in Iron Control. Stored secrets are write-only and are not\nsilently rewritten. Setup checks direct and role grants, including inactive OAuth\nbrokers. The installation's Iron principal remains the authorization boundary;\nthese markers select credentials, not permissions for separate untrusted tenants.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-iron-proxy","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-iron-proxy/SKILL.md","defaultBranch":"main"},"readme":"# Add Iron Proxy gateway\n\nInstall the official `iron-control` console and PostgreSQL alongside one central Iron Proxy. NanoClaw core supplies the generic gateway seam and human approval flow. Read `docs/gateway-seam.md` before changing the integration.\n\nUse the bundled setup scripts and the source revisions in `versions.json`. The console is the upstream Rails application; its UI is not generated or copied into NanoClaw. This is a local Docker installation. Reuse this copy's recorded services and keys when refreshing it.\n\n## Install the provider payload\n\nCopy the package's provider, approval middleware, tests, and agent guidance into their normal NanoClaw paths.\n\n```nc:copy\npayload/src/gateway-providers/iron-proxy.ts -> src/gateway-providers/iron-proxy.ts\npayload/src/gateway-providers/iron-proxy.test.ts -> src/gateway-providers/iron-proxy.test.ts\npayload/src/gateway-providers/iron-proxy-approval.ts -> src/gateway-providers/iron-proxy-approval.ts\npayload/src/gateway-providers/iron-proxy-approval.test.ts -> src/gateway-providers/iron-proxy-approval.test.ts\npayload/src/gateway-providers/iron-proxy-transform.proto -> src/gateway-providers/iron-proxy-transform.proto\npayload/container/skills/iron-proxy-gateway/SKILL.md -> container/skills/iron-proxy-gateway/SKILL.md\npayload/container/skills/iron-proxy-gateway/instructions.md -> container/skills/iron-proxy-gateway/instructions.md\n```\n\n## Register once\n\nThe provider file makes the only product registration call. It declares idempotent sessions, typed runtime contributions, owned-resource cleanup, normalized approvals, network access, and agent guidance. NanoClaw core owns approval persistence, cards, clicks, authorization, and timeouts.\n\n```nc:append to:src/gateway-providers/installed.ts\nimport './iron-proxy.js';\n```\n\n## Install the bridge dependencies\n\n```nc:dep manager:pnpm\n@grpc/grpc-js@1.14.4\n@grpc/proto-loader@0.8.1\n```\n\n## Install the console and pinned proxy\n\nSetup pulls the pinned official Iron Control image and database image, starts them on a dedicated Docker network, creates a local operator account, and registers this copy's proxy and principal through Iron's API. It stores credentials and encryption keys in owner-only files under `data/session-materials/iron-control/`. The database has its own persistent volume and no published port. Neither the console credentials nor its database are mounted into agents.\n\nThe installer streams stage names and elapsed-time updates. Source downloads stop after two minutes, the image build after twenty minutes, and console startup after six minutes. It never opens a Git credential prompt. The proxy is built locally from the pinned public upstream source, so installation does not require a GitHub account or access to a private proxy image. The console and build dependencies use their pinned public images. On failure, fix the reported access or service issue and rerun setup; keep existing database volumes and encryption keys together. Raw subprocess output is not streamed because it can contain credentials.\n\nSetup builds unmodified upstream Iron Proxy and a separate NanoClaw approval front in the same image. No Iron fork or source patch is used. The front is the only network-facing listener. It authenticates session identities, inspects each HTTP request inside HTTPS tunnels, checks the allowlist, and waits for an explicit approval before forwarding to Iron on `127.0.0.1:18080`. Empty, malformed, rejected or timed-out decisions fail closed. Iron's own dial-time loopback and link-local deny rules prevent DNS aliases from reaching the internal backend. Managed control-plane updates only change Iron's credential transforms; they cannot remove the front's checks.\n\nThe front builds and runs the pinned OneCLI helper in `gateway-compat/onecli-summary`; do not add app-specific rules. Only method, host, path, response status and the resulting OneCLI summary reach the approval bridge. Raw bodies, authorization headers, query strings and Iron transform","createdAt":"2026-09-25T10:52:06.657Z","updatedAt":"2026-09-25T10:52:06.657Z"},{"id":"cmugucubv00jtqu06rzc3ep1x","slug":"nanocoai-nanoclaw-add-karpathy-llm-wiki","name":"add-karpathy-llm-wiki","description":"Add a persistent wiki knowledge base to a NanoClaw group. Based on Karpathy's LLM Wiki pattern. Triggers on \"add wiki\", \"wiki\", \"knowledge base\", \"llm wiki\", \"karpathy wiki\".","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-karpathy-llm-wiki","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add a persistent wiki knowledge base to a NanoClaw group. Based on Karpathy's LLM Wiki pattern. Triggers on \"add wiki\", \"wiki\", \"knowledge base\", \"llm wiki\", \"karpathy wiki\".","permissions":[],"systemPrompt":"# Add Karpathy LLM Wiki\n\nSet up a persistent wiki knowledge base on NanoClaw, based on Karpathy's LLM Wiki pattern.\n\nEach step is safe to re-run: directory creation uses `mkdir -p`, initial wiki files are created only if absent, the container skill is preserved unless the user opts to update it, and the group CLAUDE.md section is replaced in place via marker comments rather than duplicated.\n\n## Step 1: Read the pattern\n\nRead `${CLAUDE_SKILL_DIR}/llm-wiki.md` — this is the full LLM Wiki idea as written by Karpathy. Understand it thoroughly before proceeding. Summarize the core idea to the user briefly, then discuss what they want to build.\n\n## Step 2: Choose a group\n\nAskUserQuestion: \"Which group should have the wiki?\"\n\n1. **Main group** — add to your existing main chat\n2. **Dedicated group** — create a new group just for the wiki\n3. **Other** — pick an existing group\n\nIf dedicated: ask which channel and chat, then register with `pnpm exec tsx setup/index.ts --step register`.\n\n## Step 3: Design collaboratively\n\nDiscuss with the user based on the pattern:\n- What's the wiki's domain or topic?\n- What kinds of sources will they add? (URLs, PDFs, images, voice notes, books, transcripts)\n- Do they want the full three-layer architecture or a lighter version?\n- Any specific conventions they care about? (The pattern intentionally leaves this open.)\n\nBased on this discussion, create three things:\n\n### 3a. Directory structure\n\nCreate `wiki/` and `sources/` directories in the group folder (`mkdir -p` — safe if they already exist). Create initial `index.md` and `log.md` per the pattern's Indexing and Logging section, adapted to the user's domain. Skip any of these files that already exist so a populated wiki is never clobbered on re-run.\n\n### 3b. Container skill\n\nCreate `container/skills/wiki/SKILL.md` tailored to this user's wiki. This is the schema layer from the pattern — it tells the agent how to maintain the wiki. Base it on the pattern's Operations section (ingest, query, lint) and the conventions you agreed on with the user. Don't over-prescribe — the pattern says \"your LLM figures out the rest.\"\n\nIf `container/skills/wiki/SKILL.md` already exists, ask the user whether to update it before overwriting, so an existing tailored schema is preserved on re-run.\n\n### 3c. Group CLAUDE.md\n\nEdit the group's CLAUDE.md to add a wiki section, wrapped in marker comments so it can be located and replaced on re-run:\n\n```markdown\n<!-- BEGIN karpathy-llm-wiki -->\n## Wiki\n...section body...\n<!-- END karpathy-llm-wiki -->\n```\n\nIf a `<!-- BEGIN karpathy-llm-wiki -->` block already exists, replace it in place rather than appending a second copy. This is critical — it's what turns the agent into a wiki maintainer. The section should:\n\n- Explain the wiki system concisely: what it is, the three layers (sources, wiki, schema), the three operations (ingest, query, lint)\n- Index the key files and folders (`wiki/`, `sources/`, `wiki/index.md`, `wiki/log.md`)\n- Point to the container skill for detailed workflow\n- **Ingest discipline:** Be very explicit that when the user provides multiple files or points at a folder with many files, the agent MUST process them one at a time. For each file: read it, discuss takeaways, create/update all wiki pages (summary, entities, concepts, cross-references, index, log), and completely finish with that file before moving to the next. Never batch-read all files and then process them together — this produces shallow, generic pages instead of the deep integration the pattern requires.\n\n## Step 4: Source handling capabilities\n\nBased on the source types the user plans to ingest (discussed in Step 3), check whether the agent can already handle those formats — some are supported natively, others need a skill (e.g. `/add-image-vision`, `/add-pdf-reader`, `/add-voice-transcription`). If a needed capability isn't installed, check if there's an available skill for it and help the user get it set up.\n\n### URL handling note\n\nclaude has built-in `WebFetch`, but it returns a summary, not the full document. For wiki ingestion of a URL where the full text matters, the container skill and CLAUDE.md should instruct claude to use bash commands to download full files instead. For example:\n\n```bash\ncurl -sLo sources/filename.pdf \"<url>\"\n```\n\nIf the document is a webpage, then claude can use fetch or `agent-browser` to open the page and extract full text if available. The container skill and CLAUDE.md should note this so claude gets full content for sources rather than summaries.\n\n\n## Step 5: Optional lint schedule\n\nAskUserQuestion: \"Want periodic wiki health checks?\"\n\n1. **Weekly**\n2. **Monthly**\n3. **Skip** — lint manually\n\nIf yes, ask the agent to schedule the lint task using the `schedule_task` MCP tool in conversation.\n\n## Step 6: Restart\n\nRun from your NanoClaw project root:\n\n```bash\nsource setup/lib/install-slug.sh\nlaunchctl kickstart -k gui/$(id -u)/$(launchd_label)  # macOS\nsystemctl --user restart $(systemd_unit)              # Linux\n```\n\nTell the user to test by sending a source to the wiki group.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-karpathy-llm-wiki","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-karpathy-llm-wiki/SKILL.md","defaultBranch":"main"},"readme":"# Add Karpathy LLM Wiki\n\nSet up a persistent wiki knowledge base on NanoClaw, based on Karpathy's LLM Wiki pattern.\n\nEach step is safe to re-run: directory creation uses `mkdir -p`, initial wiki files are created only if absent, the container skill is preserved unless the user opts to update it, and the group CLAUDE.md section is replaced in place via marker comments rather than duplicated.\n\n## Step 1: Read the pattern\n\nRead `${CLAUDE_SKILL_DIR}/llm-wiki.md` — this is the full LLM Wiki idea as written by Karpathy. Understand it thoroughly before proceeding. Summarize the core idea to the user briefly, then discuss what they want to build.\n\n## Step 2: Choose a group\n\nAskUserQuestion: \"Which group should have the wiki?\"\n\n1. **Main group** — add to your existing main chat\n2. **Dedicated group** — create a new group just for the wiki\n3. **Other** — pick an existing group\n\nIf dedicated: ask which channel and chat, then register with `pnpm exec tsx setup/index.ts --step register`.\n\n## Step 3: Design collaboratively\n\nDiscuss with the user based on the pattern:\n- What's the wiki's domain or topic?\n- What kinds of sources will they add? (URLs, PDFs, images, voice notes, books, transcripts)\n- Do they want the full three-layer architecture or a lighter version?\n- Any specific conventions they care about? (The pattern intentionally leaves this open.)\n\nBased on this discussion, create three things:\n\n### 3a. Directory structure\n\nCreate `wiki/` and `sources/` directories in the group folder (`mkdir -p` — safe if they already exist). Create initial `index.md` and `log.md` per the pattern's Indexing and Logging section, adapted to the user's domain. Skip any of these files that already exist so a populated wiki is never clobbered on re-run.\n\n### 3b. Container skill\n\nCreate `container/skills/wiki/SKILL.md` tailored to this user's wiki. This is the schema layer from the pattern — it tells the agent how to maintain the wiki. Base it on the pattern's Operations section (ingest, query, lint) and the conventions you agreed on with the user. Don't over-prescribe — the pattern says \"your LLM figures out the rest.\"\n\nIf `container/skills/wiki/SKILL.md` already exists, ask the user whether to update it before overwriting, so an existing tailored schema is preserved on re-run.\n\n### 3c. Group CLAUDE.md\n\nEdit the group's CLAUDE.md to add a wiki section, wrapped in marker comments so it can be located and replaced on re-run:\n\n```markdown\n<!-- BEGIN karpathy-llm-wiki -->\n## Wiki\n...section body...\n<!-- END karpathy-llm-wiki -->\n```\n\nIf a `<!-- BEGIN karpathy-llm-wiki -->` block already exists, replace it in place rather than appending a second copy. This is critical — it's what turns the agent into a wiki maintainer. The section should:\n\n- Explain the wiki system concisely: what it is, the three layers (sources, wiki, schema), the three operations (ingest, query, lint)\n- Index the key files and folders (`wiki/`, `sources/`, `wiki/index.md`, `wiki/log.md`)\n- Point to the container skill for detailed workflow\n- **Ingest discipline:** Be very explicit that when the user provides multiple files or points at a folder with many files, the agent MUST process them one at a time. For each file: read it, discuss takeaways, create/update all wiki pages (summary, entities, concepts, cross-references, index, log), and completely finish with that file before moving to the next. Never batch-read all files and then process them together — this produces shallow, generic pages instead of the deep integration the pattern requires.\n\n## Step 4: Source handling capabilities\n\nBased on the source types the user plans to ingest (discussed in Step 3), check whether the agent can already handle those formats — some are supported natively, others need a skill (e.g. `/add-image-vision`, `/add-pdf-reader`, `/add-voice-transcription`). If a needed capability isn't installed, check if there's an available skill for it and help the user get it set up.\n\n### URL handling note\n\nclaude has built","createdAt":"2026-09-25T10:52:06.667Z","updatedAt":"2026-09-25T10:52:06.667Z"},{"id":"cmugucuc200jwqu06r9zr617h","slug":"nanocoai-nanoclaw-add-linear","name":"add-linear","description":"Add Linear channel integration via Chat SDK. Issue comment threads as conversations.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-linear","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add Linear channel integration via Chat SDK. Issue comment threads as conversations.","permissions":[],"systemPrompt":"# Add Linear Channel\n\nAdds Linear support via the Chat SDK bridge. The agent participates in issue\ncomment threads. Every comment on a Linear issue triggers the agent — no\n@-mention needed. NanoClaw doesn't ship channels in trunk — this skill copies the\nLinear adapter in from the `channels` branch.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent reads\nthe prose and applies them, and a parser can apply them deterministically from\nthe same document. Every directive is idempotent, so the whole skill is safe to\nre-run; anything a parser can't apply falls back to the prose beside it.\n\n## Prerequisites\n\n**Recommended:** Create a Linear **OAuth application** so the agent posts as an app identity, not as you. This prevents the adapter from filtering your own comments as self-messages.\n\n1. Go to [Linear Settings > API > OAuth Applications](https://linear.app/settings/api/applications/new)\n2. Create an app (e.g. \"NanoClaw Bot\")\n   - Developer URL: your repo URL (e.g. `https://github.com/your-org/nanoclaw`)\n   - Callback URL: `http://localhost`\n3. After creating, click the app and enable **Client credentials** under grant types\n4. Copy the **Client ID** and **Client Secret**\n\n**Alternative:** Use a Personal API Key (`LINEAR_API_KEY`) for simpler setup. The agent will post as you, and your own comments will be filtered (other team members' comments still work).\n\n## Apply\n\nLinear OAuth apps post and read comments under an app identity that can't be\n@-mentioned; the adapter's declared channel defaults therefore respond to plain\ncomments rather than mention-only, and the wiring below sets that same pattern\nmode explicitly.\n\n### 1. Copy the adapter and its registration test\n\nFetch the `channels` branch and copy the Linear adapter and its registration\ntest into `src/channels/` (overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/linear.ts\nsrc/channels/linear-registration.test.ts\n```\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into the channel\nregistry:\n\n```nc:append to:src/channels/index.ts\nimport './linear.js';\n```\n\n### 3. Install the adapter package\n\nPinned to an exact version — the supply-chain policy rejects ranges and `latest`:\n\n```nc:dep\n@chat-adapter/linear@4.29.0\n```\n\n### 4. Build and validate\n\nBuild first: it guards the typed `createChatSdkBridge(...)` core call and proves\nthe dependency is installed. Then run the one integration test.\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/linear-registration.test.ts\n```\n\nBoth must be clean before proceeding. `linear-registration.test.ts` imports the\nreal channel barrel and asserts the registry contains `linear`. It goes red if\nthe `import './linear.js';` line is deleted or drifts, if the barrel fails to\nevaluate, or if `@chat-adapter/linear` isn't installed (the import throws) — so\nit also covers the dependency from step 3. End-to-end message delivery against a\nreal Linear workspace is verified manually once the service is running — see\nWiring and Next Steps.\n\n## Credentials\n\nLinear app and webhook setup is human and interactive — these steps are prose\n(no parser can click through the Linear UI), except the final env write.\n\n### 1. Set up a webhook\n\n1. Go to **Linear Settings** > **API** > **Webhooks** > **New webhook**\n2. Label: `NanoClaw`\n3. URL: `https://your-domain/webhook/linear` (the shared webhook server, default port 3000)\n4. Team: select the team you want to monitor\n5. Events: check **Comment**\n6. Save — copy the **signing secret**\n\nNote: Linear webhook delivery may be delayed 1-5 minutes for new webhooks. This is normal.\n\n### 2. Store the credentials\n\nCapture the values, then write them. `prompt` only *asks* and binds the answer\nto a name; a separate directive consumes it. Here they go to `.env`\n(set-if-absent — a value you've already filled in is never overwritten) and sync\nto the container.\n\nUse **either** the OAuth app credentials (recommended) **or** a Personal API key.\nFor the API-key path, paste `none` at the OAuth prompts and set `LINEAR_API_KEY`\nin `.env` by hand (commented in the template below). `LINEAR_BOT_USERNAME` is the\ndisplay name for the bot, used for self-message detection when using a Personal\nAPI Key. `LINEAR_TEAM_KEY` is the Linear team key (e.g. `ENG`, `NAN`) — find it\nin Linear under Settings > Teams; all issues in this team route to one messaging\ngroup.\n\n```nc:prompt linear_client_id secret\nPaste the OAuth Client ID — Linear Settings > API > OAuth Applications. Paste `none` if using a Personal API key instead.\n```\n```nc:prompt linear_client_secret secret\nPaste the OAuth Client Secret. Paste `none` if using a Personal API key instead.\n```\n```nc:prompt linear_webhook_secret secret\nPaste the webhook signing secret from the webhook you just created.\n```\n```nc:prompt linear_team_key\nEnter the Linear team key (e.g. `ENG`, `NAN`) — Settings > Teams.\n```\n```nc:prompt linear_bot_username\nEnter the bot display name (e.g. `NanoClaw Bot`).\n```\n```nc:env-set\nLINEAR_CLIENT_ID={{linear_client_id}}\nLINEAR_CLIENT_SECRET={{linear_client_secret}}\nLINEAR_WEBHOOK_SECRET={{linear_webhook_secret}}\nLINEAR_TEAM_KEY={{linear_team_key}}\nLINEAR_BOT_USERNAME={{linear_bot_username}}\n```\nIf you went the Personal API key route, add this line to `.env` instead of the\nOAuth pair (agent posts as you, your own comments are filtered):\n\n```bash\nLINEAR_API_KEY=lin_api_...\n```\n\n## Wiring\n\nLinear is team-routed: the assistant watches one team and answers *every* comment\non its issues (it can't be @-mentioned). Wire the team you set up to an agent —\npick which one should answer (`ncl groups list` shows their folders). The host\nservice must be running — `ncl` connects to it over a Unix socket.\n\nThe sender policy depends on the workspace: a private workspace can use `public`\n(only workspace members can comment anyway); a public workspace should use\n`strict` so only registered members may talk to the agent.\n\n```nc:prompt agent_folder\nWhich agent should answer Linear comments? Enter its folder (run `ncl groups list`).\n```\n```nc:prompt linear_sender_policy normalize:lower validate:^(public|strict)$\nIs this a private or public Linear workspace? Enter `public` for a private workspace (only members can comment) or `strict` for a public workspace (only registered members may talk to the agent).\n```\n```nc:run effect:wire\nncl messaging-groups create --channel-type linear --platform-id linear:{{linear_team_key}} --is-group 1 --unknown-sender-policy {{linear_sender_policy}} --name {{linear_team_key}}\nncl wirings create --channel-type linear --platform-id linear:{{linear_team_key}} --agent-group {{agent_folder}} --engage-mode pattern --engage-pattern . --session-mode per-thread\n```\n\nThe explicit `pattern` engage mode with pattern `.` matches the Linear adapter's\ndeclared channel defaults — Linear can't be @-mentioned, so the agent answers\nevery comment. Each issue thread becomes its own conversation. There's no\nwelcome — Linear has no direct message, so the assistant greets people when it\nfirst answers a comment. If you chose `strict`, register the people who may talk\nto the agent (see the GitHub skill for adding members).\n\n## Next Steps\n\nIf you're in the middle of `/setup`, return to the setup flow now.\n\nOtherwise, restart the service to pick up the new channel.\n\nRun from your NanoClaw project root:\n\n```bash\nsource setup/lib/install-slug.sh\nlaunchctl kickstart -k gui/$(id -u)/$(launchd_label)  # macOS\nsystemctl --user restart $(systemd_unit)              # Linux\n```\n\n## Channel Info\n\n- **type**: `linear`\n- **terminology**: Linear has \"teams\" containing \"issues.\" Each issue's comment thread is a separate conversation.\n- **how-to-find-id**: The platform ID is `linear:<TEAM_KEY>` (e.g. `linear:ENG`). Find your team key in Linear under Settings > Teams. Each issue becomes its own thread automatically.\n- **supports-threads**: yes (issue comment threads are native conversations)\n- **typical-use**: Webhook-driven — the agent receives all issue comment events and responds automatically. No @-mention needed (Linear OAuth apps can't be @-mentioned).\n- **default-isolation**: Use `per-thread` session mode. Each issue comment thread gets its own isolated agent session.\n\n## Troubleshooting\n\n**Comments never reach the agent.** New Linear webhooks can lag 1–5 minutes, so wait before digging. Then check the webhook in Linear Settings → API → Webhooks: the URL must be your public host at `/webhook/linear` (shared webhook server, port 3000), the right team selected, and the **Comment** event checked. A mismatch between the webhook's signing secret and `LINEAR_WEBHOOK_SECRET` makes deliveries fail signature verification silently — re-copy the secret from the webhook page.\n\n**OAuth credentials rejected.** The Client ID and Secret come from Linear Settings → API → OAuth Applications, and the app must have **Client credentials** enabled under grant types after creation — without that toggle the token exchange 401s. If you meant to use a Personal API key instead, answer `none` at both OAuth prompts and set `LINEAR_API_KEY` in `.env` by hand.\n\n**The agent ignores your own comments.** That's Personal-API-key mode working as designed: comments from the key's account are filtered as self-messages so the bot doesn't answer itself. Other members' comments still trigger it; if it must answer you too, switch to the OAuth app identity.\n\n**Sender-policy answer rejected, or issues route nowhere.** The policy must be exactly `public` or `strict` (lowercase), and `LINEAR_TEAM_KEY` must be the short team key (e.g. `ENG`) from Settings → Teams — all issues in that one team route to the messaging group.\n\n**Wired but dead.** Run `pnpm exec vitest run src/channels/linear-registration.test.ts` — red means the barrel import or the `@chat-adapter/linear` install drifted, so re-run the Apply steps. If green, restart the service (see Next Steps) so the adapter and `.env` values are live.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-linear","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-linear/SKILL.md","defaultBranch":"main"},"readme":"# Add Linear Channel\n\nAdds Linear support via the Chat SDK bridge. The agent participates in issue\ncomment threads. Every comment on a Linear issue triggers the agent — no\n@-mention needed. NanoClaw doesn't ship channels in trunk — this skill copies the\nLinear adapter in from the `channels` branch.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent reads\nthe prose and applies them, and a parser can apply them deterministically from\nthe same document. Every directive is idempotent, so the whole skill is safe to\nre-run; anything a parser can't apply falls back to the prose beside it.\n\n## Prerequisites\n\n**Recommended:** Create a Linear **OAuth application** so the agent posts as an app identity, not as you. This prevents the adapter from filtering your own comments as self-messages.\n\n1. Go to [Linear Settings > API > OAuth Applications](https://linear.app/settings/api/applications/new)\n2. Create an app (e.g. \"NanoClaw Bot\")\n   - Developer URL: your repo URL (e.g. `https://github.com/your-org/nanoclaw`)\n   - Callback URL: `http://localhost`\n3. After creating, click the app and enable **Client credentials** under grant types\n4. Copy the **Client ID** and **Client Secret**\n\n**Alternative:** Use a Personal API Key (`LINEAR_API_KEY`) for simpler setup. The agent will post as you, and your own comments will be filtered (other team members' comments still work).\n\n## Apply\n\nLinear OAuth apps post and read comments under an app identity that can't be\n@-mentioned; the adapter's declared channel defaults therefore respond to plain\ncomments rather than mention-only, and the wiring below sets that same pattern\nmode explicitly.\n\n### 1. Copy the adapter and its registration test\n\nFetch the `channels` branch and copy the Linear adapter and its registration\ntest into `src/channels/` (overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/linear.ts\nsrc/channels/linear-registration.test.ts\n```\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into the channel\nregistry:\n\n```nc:append to:src/channels/index.ts\nimport './linear.js';\n```\n\n### 3. Install the adapter package\n\nPinned to an exact version — the supply-chain policy rejects ranges and `latest`:\n\n```nc:dep\n@chat-adapter/linear@4.29.0\n```\n\n### 4. Build and validate\n\nBuild first: it guards the typed `createChatSdkBridge(...)` core call and proves\nthe dependency is installed. Then run the one integration test.\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/linear-registration.test.ts\n```\n\nBoth must be clean before proceeding. `linear-registration.test.ts` imports the\nreal channel barrel and asserts the registry contains `linear`. It goes red if\nthe `import './linear.js';` line is deleted or drifts, if the barrel fails to\nevaluate, or if `@chat-adapter/linear` isn't installed (the import throws) — so\nit also covers the dependency from step 3. End-to-end message delivery against a\nreal Linear workspace is verified manually once the service is running — see\nWiring and Next Steps.\n\n## Credentials\n\nLinear app and webhook setup is human and interactive — these steps are prose\n(no parser can click through the Linear UI), except the final env write.\n\n### 1. Set up a webhook\n\n1. Go to **Linear Settings** > **API** > **Webhooks** > **New webhook**\n2. Label: `NanoClaw`\n3. URL: `https://your-domain/webhook/linear` (the shared webhook server, default port 3000)\n4. Team: select the team you want to monitor\n5. Events: check **Comment**\n6. Save — copy the **signing secret**\n\nNote: Linear webhook delivery may be delayed 1-5 minutes for new webhooks. This is normal.\n\n### 2. Store the credentials\n\nCapture the values, then write them. `prompt` only *asks* and binds the answer\nto a name; a separate directive consumes it. Here they go to `.env`\n(set-if-absent — a value you've already filled in","createdAt":"2026-09-25T10:52:06.674Z","updatedAt":"2026-09-25T10:52:06.674Z"},{"id":"cmugucuc900jzqu06atev3rdy","slug":"nanocoai-nanoclaw-add-macos-statusbar","name":"add-macos-statusbar","description":"Add a macOS menu bar status indicator for NanoClaw. Shows a bolt icon with a green/red dot indicating whether NanoClaw is running, with Start, Stop, and Restart controls. macOS only.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-macos-statusbar","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add a macOS menu bar status indicator for NanoClaw. Shows a bolt icon with a green/red dot indicating whether NanoClaw is running, with Start, Stop, and Restart controls. macOS only.","permissions":[],"systemPrompt":"# Add macOS Menu Bar Status Indicator\n\nAdds a persistent menu bar icon that shows NanoClaw's running status and lets the user\nstart, stop, or restart the service — similar to how Docker Desktop appears in the menu bar.\n\n**macOS only.** Requires Xcode Command Line Tools (`swiftc`).\n\n## Phase 1: Pre-flight\n\n### Check platform\n\nIf not on macOS, stop and tell the user:\n\n> This skill is macOS only. The menu bar status indicator uses AppKit and requires `swiftc` (Xcode Command Line Tools).\n\n### Check for swiftc\n\n```bash\nwhich swiftc\n```\n\nIf not found, tell the user:\n\n> Xcode Command Line Tools are required. Install them by running:\n>\n> ```bash\n> xcode-select --install\n> ```\n>\n> Then re-run `/add-macos-statusbar`.\n\n### Check if already installed\n\n```bash\nlaunchctl list | grep com.nanoclaw.statusbar\n```\n\nIf it returns a PID (not `-`), tell the user it's already installed and skip to Phase 3 (Verify).\n\n## Phase 2: Compile and Install\n\n### Compile the Swift binary\n\nThe source lives in the skill directory. Compile it into `dist/`:\n\n```bash\nmkdir -p dist\nswiftc -O -o dist/statusbar \"${CLAUDE_SKILL_DIR}/add/src/statusbar.swift\"\n```\n\nThis produces a small native binary at `dist/statusbar`.\n\nOn macOS Sequoia or later, clear the quarantine attribute so the binary can run:\n\n```bash\nxattr -cr dist/statusbar\n```\n\n### Create the launchd plist\n\nDetermine the absolute project root and home directory:\n\n```bash\npwd\necho $HOME\n```\n\nCreate `~/Library/LaunchAgents/com.nanoclaw.statusbar.plist`, substituting the actual values\nfor `{PROJECT_ROOT}` and `{HOME}`:\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>Label</key>\n    <string>com.nanoclaw.statusbar</string>\n    <key>ProgramArguments</key>\n    <array>\n        <string>{PROJECT_ROOT}/dist/statusbar</string>\n    </array>\n    <key>RunAtLoad</key>\n    <true/>\n    <key>KeepAlive</key>\n    <true/>\n    <key>EnvironmentVariables</key>\n    <dict>\n        <key>HOME</key>\n        <string>{HOME}</string>\n    </dict>\n    <key>StandardOutPath</key>\n    <string>{PROJECT_ROOT}/logs/statusbar.log</string>\n    <key>StandardErrorPath</key>\n    <string>{PROJECT_ROOT}/logs/statusbar.error.log</string>\n</dict>\n</plist>\n```\n\n### Load the service\n\n```bash\nlaunchctl load ~/Library/LaunchAgents/com.nanoclaw.statusbar.plist\n```\n\n## Phase 3: Verify\n\n```bash\nlaunchctl list | grep com.nanoclaw.statusbar\n```\n\nThe first column should show a PID (not `-`).\n\nTell the user:\n\n> The bolt icon should now appear in your macOS menu bar. Click it to see NanoClaw's status and control the service.\n>\n> - **Green dot** — NanoClaw is running\n> - **Red dot** — NanoClaw is stopped\n>\n> Use **Restart** after making code changes, and **View Logs** to open the log file directly.\n\nTo uninstall, follow [REMOVE.md](REMOVE.md).","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-macos-statusbar","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-macos-statusbar/SKILL.md","defaultBranch":"main"},"readme":"# Add macOS Menu Bar Status Indicator\n\nAdds a persistent menu bar icon that shows NanoClaw's running status and lets the user\nstart, stop, or restart the service — similar to how Docker Desktop appears in the menu bar.\n\n**macOS only.** Requires Xcode Command Line Tools (`swiftc`).\n\n## Phase 1: Pre-flight\n\n### Check platform\n\nIf not on macOS, stop and tell the user:\n\n> This skill is macOS only. The menu bar status indicator uses AppKit and requires `swiftc` (Xcode Command Line Tools).\n\n### Check for swiftc\n\n```bash\nwhich swiftc\n```\n\nIf not found, tell the user:\n\n> Xcode Command Line Tools are required. Install them by running:\n>\n> ```bash\n> xcode-select --install\n> ```\n>\n> Then re-run `/add-macos-statusbar`.\n\n### Check if already installed\n\n```bash\nlaunchctl list | grep com.nanoclaw.statusbar\n```\n\nIf it returns a PID (not `-`), tell the user it's already installed and skip to Phase 3 (Verify).\n\n## Phase 2: Compile and Install\n\n### Compile the Swift binary\n\nThe source lives in the skill directory. Compile it into `dist/`:\n\n```bash\nmkdir -p dist\nswiftc -O -o dist/statusbar \"${CLAUDE_SKILL_DIR}/add/src/statusbar.swift\"\n```\n\nThis produces a small native binary at `dist/statusbar`.\n\nOn macOS Sequoia or later, clear the quarantine attribute so the binary can run:\n\n```bash\nxattr -cr dist/statusbar\n```\n\n### Create the launchd plist\n\nDetermine the absolute project root and home directory:\n\n```bash\npwd\necho $HOME\n```\n\nCreate `~/Library/LaunchAgents/com.nanoclaw.statusbar.plist`, substituting the actual values\nfor `{PROJECT_ROOT}` and `{HOME}`:\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>Label</key>\n    <string>com.nanoclaw.statusbar</string>\n    <key>ProgramArguments</key>\n    <array>\n        <string>{PROJECT_ROOT}/dist/statusbar</string>\n    </array>\n    <key>RunAtLoad</key>\n    <true/>\n    <key>KeepAlive</key>\n    <true/>\n    <key>EnvironmentVariables</key>\n    <dict>\n        <key>HOME</key>\n        <string>{HOME}</string>\n    </dict>\n    <key>StandardOutPath</key>\n    <string>{PROJECT_ROOT}/logs/statusbar.log</string>\n    <key>StandardErrorPath</key>\n    <string>{PROJECT_ROOT}/logs/statusbar.error.log</string>\n</dict>\n</plist>\n```\n\n### Load the service\n\n```bash\nlaunchctl load ~/Library/LaunchAgents/com.nanoclaw.statusbar.plist\n```\n\n## Phase 3: Verify\n\n```bash\nlaunchctl list | grep com.nanoclaw.statusbar\n```\n\nThe first column should show a PID (not `-`).\n\nTell the user:\n\n> The bolt icon should now appear in your macOS menu bar. Click it to see NanoClaw's status and control the service.\n>\n> - **Green dot** — NanoClaw is running\n> - **Red dot** — NanoClaw is stopped\n>\n> Use **Restart** after making code changes, and **View Logs** to open the log file directly.\n\nTo uninstall, follow [REMOVE.md](REMOVE.md).","createdAt":"2026-09-25T10:52:06.681Z","updatedAt":"2026-09-25T10:52:06.681Z"},{"id":"cmugucucj00k2qu06mv78uvq2","slug":"nanocoai-nanoclaw-add-matrix","name":"add-matrix","description":"Add Matrix channel integration via Chat SDK. Works with any Matrix homeserver.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Community","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-matrix","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Add Matrix channel integration via Chat SDK. Works with any Matrix homeserver.","permissions":[],"systemPrompt":"# Add Matrix Channel\n\nAdds Matrix support via the Chat SDK bridge. NanoClaw doesn't ship channels in\ntrunk — this skill copies the Matrix adapter in from the `channels` branch.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent\nreads the prose and applies them, and a parser can apply them deterministically\nfrom the same document. Every directive is idempotent, so the whole skill is\nsafe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Apply\n\n### 1. Copy the adapter\n\nFetch the `channels` branch and copy the Matrix adapter into `src/channels/`\n(overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/matrix.ts\nsrc/channels/matrix-registration.test.ts\n```\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './matrix.js';\n```\n\n### 3. Configure the ESM patch\n\nThe published adapter references `matrix-js-sdk/lib/...` without `.js`\nextensions, which fails under Node 22 strict ESM resolution. Register the\ncommitted pnpm patch before installing the package. pnpm reapplies it after\nevery install and fails if the pinned package drifts away from the patch:\n\n```nc:copy\npatches/@beeper__chat-adapter-matrix@0.2.0.patch\n```\n\n```nc:run effect:refresh\npnpm pkg set 'pnpm.patchedDependencies[@beeper/chat-adapter-matrix@0.2.0]=patches/@beeper__chat-adapter-matrix@0.2.0.patch'\n```\n\n### 4. Install the adapter package\n\nPinned to an exact version — the supply-chain policy rejects ranges and `latest`.\nThe Matrix adapter lives in the `@beeper/` namespace and versions on its own\ntrack (not the `@chat-adapter/*` family), so it carries its own pin:\n\n```nc:dep\n@beeper/chat-adapter-matrix@0.2.0\n```\n\n### 5. Verify and build\n\nBuild guards the typed `createChatSdkBridge(...)` core call the adapter makes\nand fails if the `import './matrix.js';` line is missing. The direct Node import\nchecks the published ESM entrypoint using the real runtime resolver:\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\nnode --input-type=module -e 'await import(\"@beeper/chat-adapter-matrix\")'\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/matrix-registration.test.ts\n```\n\n`matrix-registration.test.ts` imports the real channel barrel and asserts the\nregistry contains `matrix`. It goes red if the import line is deleted or drifts,\nif the barrel fails to evaluate, or if `@beeper/chat-adapter-matrix` isn't\ninstalled (the import throws) — so it also covers the dependency from step 3.\n\nEnd-to-end message delivery against a real Matrix homeserver is verified\nmanually once the service is running — see Next Steps.\n\n## Credentials\n\nThe bot needs its own Matrix account — separate from the user's account. This is\nrequired because Matrix cannot send DMs to yourself. These steps are human and\ninteractive (no parser can click through Element), so they stay prose.\n\n### Create a bot account\n\n1. Open [app.element.io](https://app.element.io) in a private/incognito window (or sign out first)\n2. Register a new account for the bot (e.g. `andybot` on matrix.org)\n3. Note the bot's user ID (e.g. `@andybot:matrix.org`)\n\n### Choose an auth method\n\n**Option A: Username + Password (simpler)**\n\nNo extra steps — just use the bot account's credentials directly. The adapter logs in automatically.\n\n```bash\nMATRIX_BASE_URL=https://matrix.org\nMATRIX_USERNAME=andybot\nMATRIX_PASSWORD=your-bot-password\nMATRIX_USER_ID=@andybot:matrix.org\nMATRIX_BOT_USERNAME=Andy\n```\n\n**Option B: Access Token (recommended for production)**\n\nGet an access token from Element: sign into the bot account → **Settings** > **Help & About** > **Access Token** (under Advanced). Or via API:\n\n```bash\ncurl -XPOST 'https://matrix.org/_matrix/client/r0/login' \\\n  -d '{\"type\":\"m.login.password\",\"user\":\"andybot\",\"password\":\"...\"}'\n```\n\n```bash\nMATRIX_BASE_URL=https://matrix.org\nMATRIX_ACCESS_TOKEN=your-access-token\nMATRIX_USER_ID=@andybot:matrix.org\nMATRIX_BOT_USERNAME=Andy\n```\n\n### Optional settings\n\n```bash\nMATRIX_INVITE_AUTOJOIN=true                    # Auto-accept room invites (default: true)\nMATRIX_INVITE_AUTOJOIN_ALLOWLIST=@you:matrix.org  # Only accept invites from these users\nMATRIX_RECOVERY_KEY=your-recovery-key          # Enable E2EE cross-signing\nMATRIX_DEVICE_ID=NANOCLAW01                    # Stable device ID across restarts\n```\n\n### Store the credentials\n\nCapture the values for the auth method you chose, then write them. `prompt` only\n*asks* and binds the answer to a name; a separate directive consumes it — so the\nsame prompts could feed `ncl` or the OneCLI vault instead of `.env` by swapping\nonly the consumer. The homeserver URL, the bot's user ID, and a display name are\nshared across both auth methods:\n\n```nc:prompt base_url\nPaste the homeserver base URL, e.g. `https://matrix.org`.\n```\n```nc:prompt user_id\nPaste the bot's full Matrix user ID, e.g. `@andybot:matrix.org`.\n```\n```nc:prompt bot_username\nPaste a display name for the bot, e.g. `Andy`.\n```\n```nc:env-set\nMATRIX_BASE_URL={{base_url}}\nMATRIX_USER_ID={{user_id}}\nMATRIX_BOT_USERNAME={{bot_username}}\n```\n\nFor **Option A** capture the bot login, for **Option B** capture the access\ntoken — set only the block matching your chosen method:\n\n```nc:prompt username\nOption A only — the bot's login username (the localpart, e.g. `andybot`).\n```\n```nc:prompt password secret\nOption A only — the bot account's password.\n```\n```nc:env-set\nMATRIX_USERNAME={{username}}\nMATRIX_PASSWORD={{password}}\n```\n```nc:prompt access_token secret\nOption B only — the access token from Element Settings > Help & About, or from the login API.\n```\n```nc:env-set\nMATRIX_ACCESS_TOKEN={{access_token}}\n```\n\n## Next Steps\n\nIf you're in the middle of `/setup`, return to the setup flow now.\n\nOtherwise, run `/manage-channels` to wire this channel to an agent group.\n\n## Channel Info\n\n- **type**: `matrix`\n- **terminology**: Matrix has \"rooms.\" A room can be a group chat or a direct message. Rooms have internal IDs (like `!abc123:matrix.org`) and optional aliases (like `#general:matrix.org`).\n- **how-to-find-id**: For DMs, use the bot's `openDM` to resolve the room automatically. For group rooms, in Element click the room name > Settings > Advanced — the \"Internal room ID\" is the platform ID (starts with `!`). Or use a room alias like `#general:matrix.org`.\n- **supports-threads**: partial (some clients support threads, but not all — treat as no for reliability)\n- **typical-use**: Interactive chat — rooms or direct messages. Requires a separate bot account (the agent cannot DM users from their own account).\n- **default-isolation**: Same agent group for rooms where you're the primary user. Separate agent group for rooms with different communities or sensitive contexts.\n\n## Troubleshooting\n\n**Build fails with `ERR_MODULE_NOT_FOUND` for `matrix-js-sdk/lib/...`.** The ESM extension patch (step 4) hasn't been applied — or a later `pnpm install` reinstalled the adapter and wiped it. Re-run the patch, then `pnpm run build`; the patch is idempotent, so re-running is always safe.\n\n**Login fails with `M_FORBIDDEN`.** The username/user-ID split is the usual trip: `MATRIX_USERNAME` is the bare localpart (`andybot`), while `MATRIX_USER_ID` is the full ID (`@andybot:matrix.org`) — swapping them fails auth. With Option B, an access token dies the moment that Element session signs out; grab a fresh one from Settings → Help & About → Access Token, or via the login API.\n\n**The bot never joins your room.** Auto-join is on by default (`MATRIX_INVITE_AUTOJOIN=true`), but an allowlist (`MATRIX_INVITE_AUTOJOIN_ALLOWLIST`) that doesn't include your user ID makes it ignore your invites. Invite the bot from your own account and watch the service log for the join.\n\n**Messages to yourself never arrive.** Matrix cannot DM your own account — the bot must be its own account, separate from yours. If you configured the adapter with your personal credentials, register a dedicated bot account and redo the credential steps.\n\n**Registered but silent.** Run `pnpm exec vitest run src/channels/matrix-registration.test.ts` — red means the barrel import or the `@beeper/chat-adapter-matrix` install drifted, so re-run the Apply steps. If green, restart the service (see Next Steps) and check `logs/nanoclaw.error.log` for login errors.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-matrix","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-matrix/SKILL.md","defaultBranch":"main"},"readme":"# Add Matrix Channel\n\nAdds Matrix support via the Chat SDK bridge. NanoClaw doesn't ship channels in\ntrunk — this skill copies the Matrix adapter in from the `channels` branch.\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent\nreads the prose and applies them, and a parser can apply them deterministically\nfrom the same document. Every directive is idempotent, so the whole skill is\nsafe to re-run; anything a parser can't apply falls back to the prose beside it.\n\n## Apply\n\n### 1. Copy the adapter\n\nFetch the `channels` branch and copy the Matrix adapter into `src/channels/`\n(overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/matrix.ts\nsrc/channels/matrix-registration.test.ts\n```\n\n### 2. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './matrix.js';\n```\n\n### 3. Configure the ESM patch\n\nThe published adapter references `matrix-js-sdk/lib/...` without `.js`\nextensions, which fails under Node 22 strict ESM resolution. Register the\ncommitted pnpm patch before installing the package. pnpm reapplies it after\nevery install and fails if the pinned package drifts away from the patch:\n\n```nc:copy\npatches/@beeper__chat-adapter-matrix@0.2.0.patch\n```\n\n```nc:run effect:refresh\npnpm pkg set 'pnpm.patchedDependencies[@beeper/chat-adapter-matrix@0.2.0]=patches/@beeper__chat-adapter-matrix@0.2.0.patch'\n```\n\n### 4. Install the adapter package\n\nPinned to an exact version — the supply-chain policy rejects ranges and `latest`.\nThe Matrix adapter lives in the `@beeper/` namespace and versions on its own\ntrack (not the `@chat-adapter/*` family), so it carries its own pin:\n\n```nc:dep\n@beeper/chat-adapter-matrix@0.2.0\n```\n\n### 5. Verify and build\n\nBuild guards the typed `createChatSdkBridge(...)` core call the adapter makes\nand fails if the `import './matrix.js';` line is missing. The direct Node import\nchecks the published ESM entrypoint using the real runtime resolver:\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\nnode --input-type=module -e 'await import(\"@beeper/chat-adapter-matrix\")'\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/matrix-registration.test.ts\n```\n\n`matrix-registration.test.ts` imports the real channel barrel and asserts the\nregistry contains `matrix`. It goes red if the import line is deleted or drifts,\nif the barrel fails to evaluate, or if `@beeper/chat-adapter-matrix` isn't\ninstalled (the import throws) — so it also covers the dependency from step 3.\n\nEnd-to-end message delivery against a real Matrix homeserver is verified\nmanually once the service is running — see Next Steps.\n\n## Credentials\n\nThe bot needs its own Matrix account — separate from the user's account. This is\nrequired because Matrix cannot send DMs to yourself. These steps are human and\ninteractive (no parser can click through Element), so they stay prose.\n\n### Create a bot account\n\n1. Open [app.element.io](https://app.element.io) in a private/incognito window (or sign out first)\n2. Register a new account for the bot (e.g. `andybot` on matrix.org)\n3. Note the bot's user ID (e.g. `@andybot:matrix.org`)\n\n### Choose an auth method\n\n**Option A: Username + Password (simpler)**\n\nNo extra steps — just use the bot account's credentials directly. The adapter logs in automatically.\n\n```bash\nMATRIX_BASE_URL=https://matrix.org\nMATRIX_USERNAME=andybot\nMATRIX_PASSWORD=your-bot-password\nMATRIX_USER_ID=@andybot:matrix.org\nMATRIX_BOT_USERNAME=Andy\n```\n\n**Option B: Access Token (recommended for production)**\n\nGet an access token from Element: sign into the bot account → **Settings** > **Help & About** > **Access Token** (under Advanced). Or via API:\n\n```bash\ncurl -XPOST 'https://matrix.org/_matrix/client/r0/login' \\\n  -d '{\"type\":\"m.login.password\",\"user\":\"andybot\",\"password\":\"...\"}'\n```\n\n```bash\nMATRIX_BASE_URL=https://matri","createdAt":"2026-09-25T10:52:06.691Z","updatedAt":"2026-09-25T10:52:06.691Z"},{"id":"cmugucudb00kbqu06f0lj4tl3","slug":"nanocoai-nanoclaw-add-ollama-provider","name":"add-ollama-provider","description":"Route a NanoClaw agent group to a local Ollama model instead of the Anthropic API. Ollama speaks the Anthropic API natively (v1/messages), so no provider code changes are needed — just env var overrides and a model setting. Use when the user wants to run their agent locally, cut API costs, or experiment with open-weight models. See docs/ollama.md for background.","authorId":"gh:nanocoai","authorName":"nanocoai","version":"0.1.0","category":"Prompt","securityLevel":"Sandbox","downloadsCount":0,"githubStars":30846,"pricePerCall":0,"manifest":{"name":"add-ollama-provider","tools":[],"category":"Prompt","entrypoint":{"type":"prompt"},"description":"Route a NanoClaw agent group to a local Ollama model instead of the Anthropic API. Ollama speaks the Anthropic API natively (v1/messages), so no provider code changes are needed — just env var overrides and a model setting. Use when the user wants to run their agent locally, cut API costs, or experiment with open-weight models. See docs/ollama.md for background.","permissions":[],"systemPrompt":"# Add Ollama Provider\n\nRoutes an agent group to a local Ollama instance instead of the Anthropic API.\nSee `docs/ollama.md` for how this works and the tradeoffs involved.\n\n## Prerequisites\n\n1. **Ollama is installed and running** on the host — verify: `curl -s http://localhost:11434/api/tags`\n2. **A model is pulled** — e.g. `ollama pull gemma4` or `ollama pull qwen3-coder`\n3. **The agent group already exists** — run `/init-first-agent` first if needed\n\n## 1. Check source support\n\nThe feature requires two fields in `ContainerConfig` (`env` and `blockedHosts`) and their\ncorresponding wiring in `container-runner.ts`. Check if already present:\n\n```bash\ngrep -c 'blockedHosts' src/container-config.ts src/container-runner.ts\n```\n\nIf either count is 0, apply the changes in steps 1a and 1b. Otherwise skip to step 2.\n\n### 1a. Extend ContainerConfig\n\nIn `src/container-config.ts`, add to the `ContainerConfig` interface:\n\n```typescript\nenv?: Record<string, string>;\nblockedHosts?: string[];\n```\n\nAnd in `readContainerConfig`, add inside the returned object:\n\n```typescript\nenv: raw.env,\nblockedHosts: raw.blockedHosts,\n```\n\n### 1b. Wire into container-runner\n\nIn `src/container-runner.ts`, after the `NANOCLAW_MCP_SERVERS` block, add:\n\n```typescript\n// Per-agent-group env overrides — applied last to win over OneCLI values.\nif (containerConfig.env) {\n  for (const [key, value] of Object.entries(containerConfig.env)) {\n    args.push('-e', `${key}=${value}`);\n  }\n}\n\n// Blocked hosts: resolve to 0.0.0.0 so they are unreachable inside the container.\nif (containerConfig.blockedHosts) {\n  for (const host of containerConfig.blockedHosts) {\n    args.push('--add-host', `${host}:0.0.0.0`);\n  }\n}\n```\n\n### 1c. Fix home directory permissions (if not already done)\n\nThe container may run as your host uid (not uid 1000). Check the Dockerfile:\n\n```bash\ngrep 'chmod.*home/node' container/Dockerfile\n```\n\nIf it shows `chmod 755`, change it to `chmod 777` so any uid can write there.\nThen rebuild the container image: `./container/build.sh`\n\n## 2. Identify the setup\n\nAsk the user (plain text, not AskUserQuestion):\n\n1. **Which agent group?** List available groups: `pnpm exec tsx scripts/q.ts data/v2.db \"SELECT folder, name FROM agent_groups;\"`\n2. **Which Ollama model?** List available: `curl -s http://localhost:11434/api/tags | grep '\"name\"'`\n3. **Block Anthropic API?** Recommended yes — prevents accidental spend if config drifts.\n\nRecord as `FOLDER`, `MODEL`, and `BLOCK_ANTHROPIC`.\n\n## 3. Configure container.json\n\nRead `groups/<FOLDER>/container.json`. Add (or merge into) an `env` block and optionally `blockedHosts`:\n\n```json\n{\n  \"env\": {\n    \"ANTHROPIC_BASE_URL\": \"http://host.docker.internal:11434\",\n    \"ANTHROPIC_API_KEY\": \"ollama\",\n    \"NO_PROXY\": \"host.docker.internal\",\n    \"no_proxy\": \"host.docker.internal\"\n  },\n  \"blockedHosts\": [\"api.anthropic.com\"]\n}\n```\n\nOmit `blockedHosts` if the user declined step 2.\n\n**Why these vars:** `ANTHROPIC_BASE_URL` redirects the Anthropic SDK to Ollama.\n`ANTHROPIC_API_KEY=ollama` satisfies the SDK's key requirement (Ollama ignores it).\n`NO_PROXY` bypasses the OneCLI HTTPS proxy for requests to `host.docker.internal`\nso they reach Ollama directly instead of going through the credential gateway.\n\n## 4. Set the model\n\nRead the agent group's shared Claude settings:\n\n```bash\n# Find the agent group ID\nAG_ID=$(pnpm exec tsx scripts/q.ts data/v2.db \"SELECT id FROM agent_groups WHERE folder='<FOLDER>';\")\nSETTINGS=data/v2-sessions/$AG_ID/.claude-shared/settings.json\n```\n\nAdd `\"model\": \"<MODEL>\"` to that settings file. Create the file if it doesn't exist:\n\n```json\n{\n  \"model\": \"gemma4:latest\"\n}\n```\n\nIf the file already has content, merge the `model` key in — don't overwrite existing keys.\n\n**Why here and not container.json:** Claude Code reads its model from its own settings\nfile, not from env vars. This file is bind-mounted into the container as `~/.claude/settings.json`.\n\n## 5. Build and restart\n\nRun from your NanoClaw project root:\n\n```bash\nexport PATH=\"/opt/homebrew/bin:$PATH\"\npnpm run build\nsource setup/lib/install-slug.sh\nlaunchctl unload ~/Library/LaunchAgents/$(launchd_label).plist\nlaunchctl load   ~/Library/LaunchAgents/$(launchd_label).plist\n# Linux: systemctl --user restart $(systemd_unit)\n```\n\n## 6. Verify\n\nSend a message to the agent. Then confirm:\n\n```bash\n# Ollama shows the model as active\ncurl -s http://localhost:11434/api/ps | grep '\"name\"'\n\n# Container has the right env vars\nCTR=$(docker ps --filter \"label=nanoclaw-group-folder=<FOLDER>\" --format \"{{.Names}}\" | head -1)\ndocker inspect \"$CTR\" --format '{{json .HostConfig.ExtraHosts}}'\ndocker exec \"$CTR\" env | grep ANTHROPIC\n```\n\nExpected: `api.anthropic.com:0.0.0.0` in ExtraHosts, `ANTHROPIC_BASE_URL=http://host.docker.internal:11434`.\n\n## Reverting to Claude\n\nTo switch back to the Anthropic API:\n\n1. Remove the `env` and `blockedHosts` keys from `groups/<FOLDER>/container.json`\n2. Remove `\"model\"` from the shared settings file\n3. Restart the service\n\nNo rebuild needed — both files are read at container spawn time.\n\n## Troubleshooting\n\n**Agent hangs, no response:** Ollama may be loading the model cold (large models take 10–30s).\nWatch `curl -s http://localhost:11434/api/ps` — the model appears once loaded.\n\n**\"model not found\" error in container logs:** The model name in settings.json doesn't match\nwhat Ollama has. Run `ollama list` on the host and use the exact name shown.\n\n**Responses claim to be Claude:** The model was trained on data that includes Claude conversations.\nAdd a line to `groups/<FOLDER>/CLAUDE.md` telling it what model it runs on.\n\n**Agent responds but Ollama shows no activity:** `NO_PROXY` may not have taken effect for\n`http_proxy` (lowercase). Add both `NO_PROXY` and `no_proxy` to the env block.","schemaVersion":1},"repoUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-ollama-provider","tags":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw","prompt"],"stats":{"installVelocity7d":0,"retentionRate":0,"executions":0,"rating":null},"origin":"github","source":{"repo":"nanoclaw","audit":{"files":["package.json","pnpm-lock.yaml"],"binaries":[],"findings":[],"packages":7,"auditedAt":"2026-09-25T10:52:06.410Z","lockfiles":["pnpm-lock.yaml"]},"forks":12819,"owner":"nanocoai","stars":30846,"topics":["ai-agents","ai-assistant","claude-code","claude-skills","openclaw"],"license":"MIT","fullName":"nanocoai/nanoclaw","homepage":"https://nanoclaw.dev","language":"TypeScript","pushedAt":"2026-09-24T14:19:25Z","avatarUrl":"https://avatars.githubusercontent.com/u/255066954?v=4","crawledAt":"2026-09-25T10:51:53.205Z","openIssues":1074,"manifestFile":"SKILL.md","manifestPath":".claude/skills/add-ollama-provider/SKILL.md","defaultBranch":"main"},"readme":"# Add Ollama Provider\n\nRoutes an agent group to a local Ollama instance instead of the Anthropic API.\nSee `docs/ollama.md` for how this works and the tradeoffs involved.\n\n## Prerequisites\n\n1. **Ollama is installed and running** on the host — verify: `curl -s http://localhost:11434/api/tags`\n2. **A model is pulled** — e.g. `ollama pull gemma4` or `ollama pull qwen3-coder`\n3. **The agent group already exists** — run `/init-first-agent` first if needed\n\n## 1. Check source support\n\nThe feature requires two fields in `ContainerConfig` (`env` and `blockedHosts`) and their\ncorresponding wiring in `container-runner.ts`. Check if already present:\n\n```bash\ngrep -c 'blockedHosts' src/container-config.ts src/container-runner.ts\n```\n\nIf either count is 0, apply the changes in steps 1a and 1b. Otherwise skip to step 2.\n\n### 1a. Extend ContainerConfig\n\nIn `src/container-config.ts`, add to the `ContainerConfig` interface:\n\n```typescript\nenv?: Record<string, string>;\nblockedHosts?: string[];\n```\n\nAnd in `readContainerConfig`, add inside the returned object:\n\n```typescript\nenv: raw.env,\nblockedHosts: raw.blockedHosts,\n```\n\n### 1b. Wire into container-runner\n\nIn `src/container-runner.ts`, after the `NANOCLAW_MCP_SERVERS` block, add:\n\n```typescript\n// Per-agent-group env overrides — applied last to win over OneCLI values.\nif (containerConfig.env) {\n  for (const [key, value] of Object.entries(containerConfig.env)) {\n    args.push('-e', `${key}=${value}`);\n  }\n}\n\n// Blocked hosts: resolve to 0.0.0.0 so they are unreachable inside the container.\nif (containerConfig.blockedHosts) {\n  for (const host of containerConfig.blockedHosts) {\n    args.push('--add-host', `${host}:0.0.0.0`);\n  }\n}\n```\n\n### 1c. Fix home directory permissions (if not already done)\n\nThe container may run as your host uid (not uid 1000). Check the Dockerfile:\n\n```bash\ngrep 'chmod.*home/node' container/Dockerfile\n```\n\nIf it shows `chmod 755`, change it to `chmod 777` so any uid can write there.\nThen rebuild the container image: `./container/build.sh`\n\n## 2. Identify the setup\n\nAsk the user (plain text, not AskUserQuestion):\n\n1. **Which agent group?** List available groups: `pnpm exec tsx scripts/q.ts data/v2.db \"SELECT folder, name FROM agent_groups;\"`\n2. **Which Ollama model?** List available: `curl -s http://localhost:11434/api/tags | grep '\"name\"'`\n3. **Block Anthropic API?** Recommended yes — prevents accidental spend if config drifts.\n\nRecord as `FOLDER`, `MODEL`, and `BLOCK_ANTHROPIC`.\n\n## 3. Configure container.json\n\nRead `groups/<FOLDER>/container.json`. Add (or merge into) an `env` block and optionally `blockedHosts`:\n\n```json\n{\n  \"env\": {\n    \"ANTHROPIC_BASE_URL\": \"http://host.docker.internal:11434\",\n    \"ANTHROPIC_API_KEY\": \"ollama\",\n    \"NO_PROXY\": \"host.docker.internal\",\n    \"no_proxy\": \"host.docker.internal\"\n  },\n  \"blockedHosts\": [\"api.anthropic.com\"]\n}\n```\n\nOmit `blockedHosts` if the user declined step 2.\n\n**Why these vars:** `ANTHROPIC_BASE_URL` redirects the Anthropic SDK to Ollama.\n`ANTHROPIC_API_KEY=ollama` satisfies the SDK's key requirement (Ollama ignores it).\n`NO_PROXY` bypasses the OneCLI HTTPS proxy for requests to `host.docker.internal`\nso they reach Ollama directly instead of going through the credential gateway.\n\n## 4. Set the model\n\nRead the agent group's shared Claude settings:\n\n```bash\n# Find the agent group ID\nAG_ID=$(pnpm exec tsx scripts/q.ts data/v2.db \"SELECT id FROM agent_groups WHERE folder='<FOLDER>';\")\nSETTINGS=data/v2-sessions/$AG_ID/.claude-shared/settings.json\n```\n\nAdd `\"model\": \"<MODEL>\"` to that settings file. Create the file if it doesn't exist:\n\n```json\n{\n  \"model\": \"gemma4:latest\"\n}\n```\n\nIf the file already has content, merge the `model` key in — don't overwrite existing keys.\n\n**Why here and not container.json:** Claude Code reads its model from its own settings\nfile, not from env vars. This file is bind-mounted into the container as `~/.claude/settings.json`.\n\n## 5. Build and restart\n\nRun from your NanoClaw project root:\n\n```bash\nex","createdAt":"2026-09-25T10:52:06.719Z","updatedAt":"2026-09-25T10:52:06.719Z"}],"total":83,"limit":24,"offset":0}