Compare commits

...

173 Commits

Author SHA1 Message Date
gavrielc 60fab764e3 style: apply prettier formatting
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:07:46 +03:00
gavrielc e38e63234e feat(v2): per-group provider config (WIP)
Per-group provider blocks in groups/<folder>/container.json::providers.<name>
take precedence over host .env for model selection. Includes claude.ts
(new, emits ANTHROPIC_MODEL) and opencode.ts per-group overrides, plus
related plumbing and skill doc updates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:07:32 +03:00
gavrielc 2529c3e387 Merge pull request #1776 from talmosko-code/feat/add-opencode-v2
feat(v2): OpenCode agent provider
2026-04-17 12:22:48 +03:00
gavrielc f18d0561a6 Merge pull request #1814 from qwibitai/refactor/provider-barrel-v2
refactor(v2/providers): self-registration barrel + host container-config registry
2026-04-17 12:22:12 +03:00
Tal Moskovich c9dd6e050e docs: update OpenCode Zen integration details in SKILL.md
- Clarified the use of `x-api-key` for Zen's HTTP API, addressing common issues with Bearer tokens.
- Added configuration examples for `.env` and OneCLI registration for Zen keys.
- Provided guidance on naming conventions for OpenCode agent and provider settings.
- Included a note on the difference in authentication methods between OpenCode and OpenRouter.
2026-04-17 12:20:22 +03:00
Tal Moskovich 22150261c5 feat(v2): OpenCode agent provider
- Add OpenCodeProvider (SSE, session resume, MCP via mcp-to-opencode)
- Register opencode in factory; AGENT_PROVIDER passthrough from DB
- Host: XDG mount, NO_PROXY merge, OPENCODE_* env for opencode sessions
- Dockerfile: opencode-ai CLI; docs checklist + architecture diagram
- Skill add-opencode for v2; AgentProviderName in src/types.ts

Made-with: Cursor
2026-04-17 12:20:22 +03:00
gavrielc 7639f7b1bb style(v2/providers): prettier reflow of provider-container-registry signatures
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:17:09 +03:00
gavrielc 1f3b023a5a refactor(v2/providers): self-registration barrel + host container-config registry
Providers now mirror the channels pattern: each module calls
registerProvider() at top level, and providers/index.ts is a barrel of
side-effect imports. createProvider() becomes a thin registry lookup;
the closed ProviderName union is gone (now a string alias, since the
env var is a runtime string anyway).

Also adds a host-side provider-container-registry so providers can
declare their own mounts and env passthrough in src/providers/<name>.ts
instead of the container-runner having to know about each one. The
resolver runs once per spawn and threads provider + contribution
through buildMounts and buildContainerArgs so side effects (mkdir,
etc.) fire exactly once.

Both barrels are append-only — adding a new provider is a new file
+ one import line per barrel, no edits to existing files. The built-in
providers (claude, mock) don't need host-side config, so src/providers/
ships with an empty barrel; the container-side barrel imports both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:17:09 +03:00
gavrielc a38c6a962f Merge pull request #1813 from qwibitai/feat/v2-bun-container
feat(v2): Bun container runtime + build-surface improvements
2026-04-17 12:14:48 +03:00
gavrielc f04921deee docs(v2): runtime-split guide, CLAUDE.md gotchas, setup CJK autodetect
- docs/v2-build-and-runtime.md: new — runtime split rationale (Node host,
  Bun container), lockfile topology, supply-chain trade-offs, image build
  surface, two session-wake paths, CI shape, key invariants. Indexed from
  CLAUDE.md v2 Docs Index.
- CLAUDE.md: Container Runtime (Bun) section with trigger/action gotchas
  a contributor editing the container must know (named-param prefix rule,
  bun:test vs vitest, bun.lock regeneration, no minimumReleaseAge for the
  Bun tree, no tsc build step, DELETE pragma invariant). CJK font support
  section for Claude sessions outside of /setup to proactively offer when
  they detect CJK signals. Development section updated with Bun commands.
- .claude/skills/setup/SKILL.md: step 3b — auto-enable CJK fonts without
  asking if the user is already writing in CJK; otherwise ask only on clear
  signals (CJK timezone from step 2a). 3c renumbered from old 3b.
2026-04-17 11:38:20 +03:00
gavrielc c5d0ef8b4f feat(v2): migrate container runtime to Bun, improve image build surface
Container side:
- agent-runner switches to Bun. Drops better-sqlite3 (native compile gone),
  drops tsc build step in-image AND the tsc-on-every-session-wake in the
  entrypoint — bun runs src/index.ts directly. bun:sqlite replaces
  better-sqlite3; cross-mount DB invariants (journal_mode=DELETE, busy_timeout)
  preserved. Named params converted from @name to $name because bun:sqlite
  does not auto-strip the prefix the way better-sqlite3 does.
- Tests ported from vitest to bun:test (only describe/it/expect/before/afterEach
  used, API-compatible). vitest.config.ts excludes container/agent-runner/.
- bun.lock replaces pnpm-lock.yaml + pnpm-workspace.yaml under
  container/agent-runner/. Host pnpm workspace does NOT include this tree.

Dockerfile improvements (independent of Bun but bundled while touching the file):
- tini as PID 1 for correct SIGTERM propagation (prevents half-written
  outbound.db on shutdown).
- Extracted entrypoint.sh — readable and diffable vs the old inline printf.
- BuildKit cache mounts for apt + bun install + pnpm install.
- --no-install-recommends on apt, pinned CLAUDE_CODE_VERSION, AGENT_BROWSER,
  VERCEL, BUN_VERSION.
- CJK fonts (~200MB) behind ARG INSTALL_CJK_FONTS=false; build.sh reads from
  .env; setup/container.ts reads the same .env so /setup and manual rebuild
  stay in sync.
- PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 in case any postinstall tries to pull a
  redundant Chromium.
- /home/node 755 (was 777).

Host side:
- src/container-runner.ts dynamic spawn command collapses from
  `pnpm exec tsc --outDir /tmp/dist … && node /tmp/dist/index.js` to
  `exec bun run /app/src/index.ts` — cold start ~200-500ms faster per wake.

CI:
- oven-sh/setup-bun@v2 alongside Node/pnpm. Adds explicit container
  typecheck (was documented in CLAUDE.md, not enforced) and `bun test` for
  agent-runner tests.
2026-04-17 11:38:01 +03:00
gavrielc 45c35a08f0 docs(v2/db): add database architecture reference
Split into three files linked from CLAUDE.md's v2 docs index:

- v2-db.md — overview: three-DB model, cross-mount rules, central-vs-session
  decision, design patterns, readers/writers map.
- v2-db-central.md — every table in data/v2.db plus migration history.
- v2-db-session.md — per-session inbound.db / outbound.db schemas, session
  folder layout, seq parity invariant, lazy schema evolution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 10:37:26 +03:00
gavrielc 31063a1b4c docs(v2/checklist): reflect progress on package install, vercel, and scope adjustments
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 09:55:24 +03:00
gavrielc f2f76ab4ff Merge pull request #1771 from meeech/feat/v2-pnpm-migration
chore: migrate v2 from npm to pnpm
2026-04-17 09:54:40 +03:00
gavrielc 22498ae69c fix: remaining npm→pnpm gaps + dockerignore for pnpm symlinks
- container/.dockerignore (new): exclude agent-runner/node_modules and
  agent-runner/dist so COPY agent-runner/ ./ doesn't clobber the
  pnpm-installed node_modules with host directories. Under npm's flat
  layout this was forgiving; under pnpm's symlink layout it's a hard
  conflict (overlay2 cannot copy onto a symlink target).
- setup/{groups,service}.ts: execSync('pnpm run build') not npm.
- setup/index.ts: usage string.
- scripts/*.ts: usage comments + seed-discord final log.
- .claude/settings.json: permission allowlist entries.
- .claude/skills/{add-whatsapp-v2,add-dashboard}/SKILL.md: docs.
- container/skills/{frontend-engineer,vercel-cli,self-customize}/SKILL.md:
  agent-facing docs still told the container agent to run npm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 09:53:00 +03:00
gavrielc 504e5296c9 fix: pnpm-lock sync, per-group build-script allowlist, ppnpm typos
- Regenerate pnpm-lock.yaml to match v2 package.json (Baileys 6.17.16,
  @chat-adapter/linear 4.26.0)
- src/container-runner.ts: when install_packages rebuilds a per-group
  image, append each installed package to /root/.npmrc's
  only-built-dependencies before pnpm install -g, so packages with
  postinstall scripts (playwright, puppeteer, native addons) don't
  install silently broken
- Fix stray 'ppnpm uninstall' in 13 skill files (REMOVE.md + SKILL.md)
  left over from the npm→pnpm sed pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 09:25:59 +03:00
meeech 44e2361293 fix: migrate container agent-runner to pnpm with proper build scripts
- Remove package-lock.json, add pnpm-lock.yaml for frozen installs
- Add pnpm-workspace.yaml with onlyBuiltDependencies (better-sqlite3)
  and minimumReleaseAge matching root project policy
- Dockerfile: allow agent-browser build scripts via global .npmrc
  (global installs don't read project-level workspace config)
- Dockerfile: make lockfile and workspace COPY mandatory (drop glob)
2026-04-17 09:23:35 +03:00
meeech 994b323cfa docs: add supply chain security rules for pnpm
Add agent-facing rules to CLAUDE.md covering minimumReleaseAgeExclude,
onlyBuiltDependencies, and frozen lockfile requirements — all require
human sign-off. Add comprehensive human-facing section to
docs/SECURITY.md with rationale, exclusion procedure (exact version
pin, approval, expiry), and build script allowlist documentation.
2026-04-17 09:23:26 +03:00
meeech 163f5700a5 chore: add .pnpm-store to gitignore and commit formatting fix
Add .pnpm-store/ to .gitignore — pnpm creates this when running in
sandbox mode with restricted network/filesystem access. Also commit
whatsapp.ts formatting change from prettier pre-commit hook.
2026-04-17 09:23:12 +03:00
meeech 211d2b5877 docs: convert all skill instructions from npm to pnpm
Batch update 62 files across .claude/skills/ — SKILL.md, REMOVE.md,
and script files. Conversions: npm run -> pnpm run, npm install ->
pnpm install, npx -> pnpm exec/dlx, npm uninstall -> pnpm uninstall,
package-lock.json -> pnpm-lock.yaml, shebangs updated.
2026-04-17 09:22:45 +03:00
meeech 8fbf9861f1 docs: update documentation for pnpm migration
Convert npm run/install/ci -> pnpm equivalents, npx -> pnpm exec,
package-lock.json -> pnpm-lock.yaml across CLAUDE.md, groups/global/,
docs/SPEC.md, docs/DEBUG_CHECKLIST.md, docs/BRANCH-FORK-MAINTENANCE.md,
and docs/docker-sandboxes.md. Kept .npmrc and npm config references
where they document real files.
2026-04-17 09:18:58 +03:00
meeech 2b7fef628d chore: migrate setup.sh bootstrap to pnpm
Replace npm ci with corepack enable + pnpm install --frozen-lockfile.
Remove --unsafe-perm logic (not needed with pnpm).
2026-04-17 09:18:29 +03:00
meeech 0b06343323 chore: use pnpm in dynamic container commands
Replace npx tsc with pnpm exec tsc in container entrypoint command,
and npm install -g with pnpm install -g in dynamic Dockerfile
generation. Variable names (npm, npmPackages) intentionally kept as
they refer to npm-registry packages, not the CLI.
2026-04-17 09:18:29 +03:00
meeech 58420e16eb chore: migrate container Dockerfile to pnpm with corepack
Enable corepack and PNPM_HOME in container, switch all npm/npx
invocations to pnpm/pnpm exec. Use wildcard COPY for optional
pnpm-lock.yaml in agent-runner.
2026-04-17 09:18:29 +03:00
meeech d1ddfa0657 chore: migrate CI workflows and git hooks to pnpm
- ci.yml: add pnpm/action-setup, cache pnpm, use pnpm exec
- bump-version.yml: add pnpm/action-setup, pnpm version, drop
  package-lock.json from git add
- husky pre-commit: npm run -> pnpm run
2026-04-17 09:18:07 +03:00
meeech 113caa97e4 chore: replace package-lock.json with pnpm-lock.yaml
Generated via pnpm install with pnpm@10.33.0. Verified:
- pnpm install --frozen-lockfile passes
- better-sqlite3 native module loads correctly
2026-04-17 09:18:07 +03:00
meeech 3e1f226281 chore: add pnpm packageManager field and workspace config
Add pnpm@10.33.0 as packageManager, create pnpm-workspace.yaml with
minimumReleaseAge (3 days) and onlyBuiltDependencies allowlist
(better-sqlite3, esbuild, protobufjs, sharp), update .npmrc as
safety-net fallback.
2026-04-17 09:17:45 +03:00
Gabi Simons 1aeb8fb4ca refactor(add-vercel): drop Frontend Engineer delegation, add pre-deploy checks
The specialist-subagent pattern forced every /add-vercel user through the
OneCLI credential-assignment plumbing (dynamically created Frontend Engineer
agents had no Vercel secret on first deploy). For a personal assistant the
isolation wasn't worth the complexity — the host agent can deploy to Vercel
directly using the same CLI.

- Remove the Phase 5 CLAUDE.md patch that forbade writing frontend code
- Drop the bundled frontend-engineer container skill
- Strip the HARD RULE + "Building Websites" delegation from vercel-cli
- Add a concise "Pre-Send Checks" section: local build, deployment READY,
  live URL returns 2xx, optional agent-browser visual check

Net: -194 lines. /add-vercel now installs the CLI, registers the secret,
assigns it to existing agents, and teaches the agent to verify before
sharing the URL. No subagent plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 19:59:30 +00:00
gavrielc cc784ff94b refactor(v2): remove trigger_credential_collection MCP tool
Drops the in-chat credential-collection flow introduced in e92b245. Agents
can no longer collect API keys via a secure modal — users must add secrets
through OneCLI directly. Keeps the OneCLI manual-approval handler and
threaded-routing work from the same commit intact.

Removed:
* container/agent-runner/src/mcp-tools/credentials.ts (MCP tool)
* src/credentials.ts (host-side modal/OneCLI pipeline)
* src/db/credentials.ts + migration 005 (pending_credentials table)
* src/onecli-secrets.ts (createSecret CLI facade, only caller was credentials.ts)
* findCredentialResponse from agent-runner DB layer
* PendingCredential types
* Four credential hooks from ChannelSetup (getCredentialForModal,
  onCredentialReject, onCredentialSubmit, onCredentialChannelUnsupported)
* Credential card/modal handling in chat-sdk-bridge (nccr/nccm prefixes,
  Modal/TextInput imports)
* credential_request text fallback in WhatsApp adapter
* request_credential system-action case in delivery.ts

Added:
* Migration 009 drops pending_credentials on existing installs.

Vercel skill now tells the agent to ask the user to register the token via
OneCLI instead of invoking the removed tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 21:41:41 +03:00
gavrielc e55ed0f4e8 fix(whatsapp): upgrade Baileys 6.7→6.17, fix proto import and 515 restart
Baileys 6.7.21 silently failed the pairing handshake. Upgrade to 6.17.16
which fixes this. Three related issues:

1. proto is no longer a named ESM export in 6.17.x — use createRequire
   to import via CJS (matching the proven v1 pattern).
2. Setup auth script didn't handle the 515 stream restart that WhatsApp
   sends after successful pairing. Refactored to reconnect (matching v1's
   connectSocket(isReconnect) pattern) instead of hanging until timeout.
3. Added succeeded guard and process.exit(0) to prevent timeout race
   after successful auth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:01:55 +03:00
exe.dev user cdf18e608f feat(v2): add update_task MCP tool, dedup list_tasks by series
update_task lets the agent adjust prompt/recurrence/processAfter/script
on a live scheduled task without losing the series id the user already
knows. Empty string clears recurrence/script.

list_tasks now groups by series_id so recurring tasks show as one row
(the live pending/paused occurrence) instead of one per firing — the
id displayed is the stable series handle that update/cancel/pause/resume
all match against.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 16:31:29 +00:00
exe.dev user 8ef30ad289 fix(v2): cancel/pause/resume recurring tasks via series_id
Recurring tasks spawn a new messages_in row per occurrence. Cancel
only matched the completed row the agent remembered, leaving the
live next occurrence running. Tag every row in a recurrence chain
with the originating task's id (series_id) so cancel/pause/resume
can reach any live row in the series. Cancel also clears recurrence
to prevent the sweep from cloning a cancelled task. Kind-aware id
prefix on recurrences (task- instead of msg-) keeps list_tasks output
consistent across occurrences.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 16:31:29 +00:00
exe.dev user 419d04fee0 chore(agent-runner): sync package-lock for existing deps
better-sqlite3 and @types/better-sqlite3 were declared in package.json
but missing from the lockfile. Ran `npm install` (needed to get tsc
working locally) and it patched the entries in. No code or behaviour
changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:31:29 +00:00
exe.dev user 4c46dd3b39 fix(v2): await handleRecurrence so the DB isn't closed mid-flight
sweepSession called handleRecurrence without await, then synchronously
closed inDb in its finally block. handleRecurrence is async because it
does a dynamic `import('cron-parser')` before the first DB write; that
import resolved after the finally had already run, so insertRecurrence
hit a closed handle and threw "The database connection is not open".

Net effect: every recurring task was correctly marked completed by
syncProcessingAcks, but its next occurrence never got scheduled.
Single-word fix — `await`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:31:29 +00:00
exe.dev user 9617087c16 fix(v2): compare process_after as datetime, not raw string
Scheduled tasks stored process_after as ISO-8601 with `T` and `Z`
(e.g. `2026-04-16T14:37:00Z`) but the due-check queries compared it
via raw `<=` against `datetime('now')`, which returns space-separated
format (`2026-04-16 14:37:00`). Since `'T' (0x54) > ' ' (0x20)`,
every ISO-formatted process_after sorted greater than any SQLite-format
`now`, so tasks were never picked up by either the host sweep's
countDueMessages or the container's getPendingMessages.

Wrapping process_after in datetime() normalises both sides before
comparison. Recurrence rows (written by retryWithBackoff using
datetime('now', ...)) already had SQLite format and were unaffected,
which is why the bug only surfaced for agent-scheduled tasks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:31:29 +00:00
exe.dev user b9f95df340 feat(v2): add pre-task script hook for scheduled tasks
Scheduled tasks can now carry a bash script that runs inside the container
before the agent is invoked. The script prints `{wakeAgent, data?}` on its
last stdout line; if `wakeAgent: false` (or the script errors) the task
row is marked completed and the agent is never queried, saving API calls
on no-op checks. On wake, the script's `data` is injected into the task
prompt. Semantics mirror V1: 30s bash timeout, 1MB buffer, last-line JSON,
error == skip.

Also blocks the Claude SDK's built-in scheduling tools (CronCreate,
CronDelete, CronList, ScheduleWakeup) via `disallowedTools` so tasks
actually flow through `mcp__nanoclaw__schedule_task` and get the script
gate. CLAUDE.md gains a soft pointer explaining why `schedule_task` is
the right path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:31:29 +00:00
gabi-simons 82422d2077 fix(vercel): complete add-vercel flow for fresh installs
- Add Phase 5: patch agent CLAUDE.md with frontend delegation rule
  so agents treat it as a hard constraint, not a suggestion
- Add Phase 6: sync container skills to existing agent sessions
  (skills are copied once at group creation, not auto-updated)
- Add OneCLI secret assignment step in Phase 3 (selective mode
  requires explicit assignment per agent)
- Add hard rule to vercel-cli container skill header
- Clean up Phase 4 (check Dockerfile before rebuilding)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:36:06 +00:00
gabi-simons 056d308868 feat(welcome): progressive discovery onboarding
Welcome skill now uses drip-feed approach instead of listing all
capabilities upfront. Agent asks user to explore or jump into building.
Init script delegates to /welcome skill instead of hardcoded prompt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:15:12 +00:00
gavrielc 4ae9785a61 fix(setup): move OpenClaw detection into environment step
Avoids running `ls -d ~/.openclaw` as a separate Bash command which
triggers permission prompts for reading outside the project directory.
The environment step now reports OPENCLAW_PATH in its status block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:14:25 +03:00
Koshkoshinsk fdece8047e fix: reply in the Slack DM thread the user wrote in
- chat-sdk-bridge: forward thread.id to the router for DMs so sub-thread
  context survives into delivery. Previously hardcoded to null, which
  collapsed every reply to the DM top level.
- router: when a DM (is_group=0) is wired as `shared`, don't auto-escalate
  to per-thread — keep one session for the whole DM and let thread_id
  flow through to the adapter.
- agent-runner poll-loop: defer follow-up messages whose thread_id
  differs from the active turn's routing. Mixing threads into one
  streaming turn sent every reply to the first thread because routing
  is captured at turn start.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:14:05 +00:00
gavrielc 79fd142be4 improve setup UX: welcome message + AskUserQuestion for pre-approval
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:06:37 +03:00
gavrielc ce80e4ec3e feat(setup): add Linear channel, fix setup permissions
Enable Linear channel adapter. Fix setup permission rules: use specific
npm install entries per adapter package, replace cp -r with rsync -a to
avoid built-in cp safety prompt, add head to allow list for chained
commands. Update Linear API key URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:00:27 +03:00
gabi-simons a1a324097e refactor(telegram-pairing): remove TTL expiry from pairing codes
Pairing codes no longer expire on a timer. They are consumed on match
or invalidated by wrong guesses. Removes ttlMs/expiresAt/deadline from
the pairing primitive, setup CLI, and tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:54:48 +00:00
gavrielc 39d2af9981 feat(v2): track unregistered senders + setup improvements
- Add unregistered_senders table to capture dropped message origins
  (one row per sender, upserted with message_count and last_seen)
- Add inbound DM logging to chat-sdk-bridge for debugging
- Add vercel CLI to base container image
- Install vercel-cli and frontend-engineer container skills
- Default requiresTrigger to false in register step

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:58:40 +03:00
gavrielc 57c9bfc670 improve setup flow: streamline steps, add pre-approval, new manage-mounts skill
- Disable sandbox by default in project settings
- Setup: remove Apple Container option (Docker only), single channel selection
  with plain text list, move fork to end, auto-set empty mounts, add command
  pre-approval step, add UTC timezone confirmation, add wait-on-user guidance,
  add 5m timeouts for long steps
- iMessage: improve Full Disk Access UX with Finder open + drag instructions
- Add /manage-mounts skill for post-setup mount configuration
- Enable iMessage channel import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:26:20 +03:00
gavrielc 03684d33e2 style: apply prettier formatting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:15:13 +03:00
gavrielc 81d45b5be9 refactor(v2): remove builder-agent dev-agent/worktree/swap flow
The dev-agent-in-worktree approach for source self-modification is abandoned
in favor of a direct draft/activate flow with OS-level RO enforcement
(planned, not yet implemented). Strip the whole subgraph:
src/builder-agent/, pending-swaps DB module + migration 006, builder-agent
MCP tools, and all host wiring (startup sweep, approval routing, deadman,
worktree mount, freeze gate). Tool descriptions in self-mod.ts / agents.ts
no longer cross-reference create_dev_agent. CLAUDE.md + v2-checklist updated
to describe the new direction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:15:13 +03:00
gavrielc 20a24dfd13 style: apply prettier formatting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:15:13 +03:00
gavrielc 75c2fde2b5 feat(v2): builder-agent self-modification WIP + container-config as per-group file
Checkpoints the builder-agent dev-agent/worktree/swap flow (create_dev_agent,
request_swap, classifier, deadman, promote) before pivoting to a unified
draft-activate approach with OS-level RO enforcement. Lifts container_config
out of the agent_groups row into groups/<folder>/container.json so install_packages,
add_mcp_server, and rebuild flows can eventually route through the same draft
path as source edits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:15:13 +03:00
Gabi Simons c54c779834 feat(v2/add-vercel): add frontend-engineer skill with agent delegation
When /add-vercel is applied, agents that need to build websites spin up
a dedicated Frontend Engineer agent instead of building inline. The
frontend agent enforces build-test-verify discipline with visual browser
verification before deploying to Vercel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:44:27 +00:00
Gabi Simons e9a427ad1a docs(v2): add /add-dashboard skill with resource-based pusher
Self-contained skill: SKILL.md has instructions, resources/ holds
the dashboard-pusher.ts that gets copied to src/ at install time.
No src/ changes until the skill is applied.

npm package: @nanoco/nanoclaw-dashboard
Repo: https://github.com/qwibitai/nanoclaw-dashboard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:21:19 +00:00
exe.dev user 8ef26d323f fix(v2/telegram): await pairing interceptor work to serialize DB commits
The Telegram pairing interceptor fired DB writes (createMessagingGroup,
upsertUser, grantRole) and the pairing-success confirmation inside an
unawaited `void (async () => {...})()`. Recent changes (0d3326a user
privilege model, c483860 pairing confirmation) widened the work done
inside this closure to include an extra two DB writes and a Telegram
API round-trip, making the race between match and commit reproducible
— a paired message could appear "lost" until a second send.

Change onInbound to optionally return a Promise, await it in the
chat-sdk-bridge dispatch callbacks, and make the pairing interceptor
async so its DB writes + confirmation send complete before the handler
resolves.

Note: the upstream @chat-adapter/telegram SDK itself does not await
processUpdate in its polling loop, so the adapter's getUpdates offset
still advances before our handler resolves. A true restart-safe fix
needs a corresponding change in chat-adapter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:16:49 +00:00
Koshkoshinsk 77321984ba feat(pair-telegram): emit REMINDER_TO_ASSISTANT in status block
Move the "print the pairing code as plain text" directive from three
skill docs into the CLI output itself. Every caller of pair-telegram
(init-first-agent, manage-channels, add-telegram-v2, future callers)
now sees the reminder directly in the PAIR_TELEGRAM_ISSUED and
PAIR_TELEGRAM_NEW_CODE blocks. Skill docs shortened to point at it.

Also add a short pre-tool-call sentence in init-first-agent step 3b
instructing the assistant to extract the code and ask the user to send
it in Telegram.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:50:25 +00:00
Koshkoshinsk 1eaf4f88d9 Revert "docs(skills): print pairing code as final message; drop log-tail verify"
This reverts commit bd6a26a002.
2026-04-15 10:48:17 +00:00
Koshkoshinsk bd6a26a002 docs(skills): print pairing code as final message; drop log-tail verify
- Reword pair-code instruction across add-telegram-v2, manage-channels,
  and init-first-agent so the very last user-visible message after
  generating the code MUST be a plain-text print of it.
- Replace init-first-agent's tail -f based verify step with a plain-text
  prompt asking the user to confirm receipt of the welcome DM, falling
  back to DB-based diagnostics only on non-arrival. Avoids harness
  blocks on long leading sleeps and fragile log-string greps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:31:46 +00:00
Koshkoshinsk 6801a14736 docs(skills): require pairing code be printed as final user-visible message
Claude Code's UI collapses bash tool output, so the user never sees the
pairing code emitted by pair-telegram. Reframe the skill instructions
to require the last user-visible message at this step to be a plain-text
print of the code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:06:58 +00:00
Koshkoshinsk f60b42666f docs(skills): surface telegram pairing code outside bash output
Claude Code's UI folds bash tool results by default, hiding the 4-digit
pairing code from the user. Instruct the skill to echo the CODE as plain
text in the reply so it's always visible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:42:39 +00:00
Koshkoshinsk c483860cd9 feat(v2/telegram): send pairing-success confirmation to paired chat
After a Telegram pair-code is successfully consumed, send a one-shot
"Pairing success! I'm spinning up the agent now, you'll get a message
from them shortly." reply to the same chat so the user knows the code
was accepted before the agent's own welcome DM arrives.

Best-effort: any sendMessage failure is logged but not rethrown, so a
Telegram outage can't undo a successful pairing or trigger the
interceptor's fail-open path.

Also includes a no-op prettier reformat in chat-sdk-bridge.ts that the
husky hook missed in the previous commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:59:17 +00:00
Koshkoshinsk ed5dc5ea51 fix(v2/chat-sdk): project author into flat sender fields for router gate
The chat-sdk bridge was emitting inbound messages with a nested
author.{userId,fullName,userName} shape, but router.ts:extractAndUpsertUser
reads flat content.senderId / sender / senderName. Result: every chat-sdk
adapter (telegram, discord, slack, teams, gchat, webex, matrix, resend,
imessage, whatsapp-cloud) hit the strict access gate with userId=null and
got dropped, even for the registered owner.

Project author into the flat fields inside messageToInbound so the bridge
matches the contract documented at router.ts:14-17. Native adapters
already set these directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:38:02 +00:00
gavrielc db3aa0bf1f docs(v2/checklist): reflect post-refactor MCP gating, cold-DM infra, and delivery ACL
- create_agent is not admin-gated (host has no role check on the system
  action; agentTools unconditionally in the container MCP tool list).
- install_packages / add_mcp_server approval is owner/admin via
  pickApprover, not "admin-only".
- Chat-first setup bootstrap + post-handoff welcome are partially done
  via /setup + /init-first-agent (still TODO: single top-level entrypoint,
  welcome prompt expansion).
- Add entries for cold-DM infrastructure (ChannelAdapter.openDM,
  ensureUserDm, user_dms cache) and /init-first-agent skill under
  Channel Adapters.
- Add entry for delivery ACL throw-on-unauthorized + implicit-origin
  allow + auto-create agent_destinations on wire (the silent-drop bug
  fix from the welcome-DM end-to-end test).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:09:55 +03:00
gavrielc 4d562524cd style: apply prettier formatting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:04:11 +03:00
gavrielc c60a9bef2d style: apply prettier formatting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:03:51 +03:00
gavrielc 0d3326aae5 feat(v2): user-level privilege model + cold DM infra + init-first-agent skill
Replaces the agent-group-centric "main group" concept with user-level
privileges and adds the cold-DM infrastructure needed for proactive
outbound messaging (pairing, approvals, welcome flows).

Privilege model
- New tables: users, user_roles (owner global-only; admin global or
  scoped to an agent_group), agent_group_members (explicit non-
  privileged access; admin/owner imply membership), user_dms (cold-DM
  resolution cache).
- Removed agent_groups.is_admin, messaging_groups.admin_user_id. Replaced
  with messaging_groups.unknown_sender_policy (strict | request_approval
  | public) for per-chat unknown-sender gating.
- src/access.ts: canAccessAgentGroup, pickApprover, pickApprovalDelivery.
- src/router.ts: access gate on every inbound, honoring
  unknown_sender_policy for unknown senders.
- src/channels/telegram.ts: pairing interceptor upserts the paired user
  and promotes them to owner if hasAnyOwner() is false (first-pair-wins).

Cold DM infrastructure
- ChannelAdapter.openDM?(handle) — optional method. Chat-SDK-bridge wires
  it to chat.openDM() for resolution-required channels (Discord, Slack,
  Teams, Webex, gChat); direct-addressable channels (Telegram, WhatsApp,
  iMessage, Matrix, Resend) fall through to the handle directly.
- src/user-dm.ts: ensureUserDm(userId) — resolves + caches via user_dms.

Approval routing
- onecli-approvals + delivery use pickApprover + pickApprovalDelivery:
  scoped admins → global admins → owners (dedup), first reachable via
  ensureUserDm, same-channel-kind tie-break. Approvals land in the
  approver's DM, not the origin chat.

Delivery fixes
- delivery.ts ACL rejection now throws instead of returning undefined —
  the outer loop previously marked rejected messages as delivered.
- Implicit-origin allow: session.messaging_group_id === target skips the
  destination check.
- createMessagingGroupAgent auto-creates the companion agent_destinations
  row (normalized local_name from the messaging group's name, collision-
  broken within the agent's namespace).

Container
- container-runner.ts: /workspace/global always read-only; drops
  NANOCLAW_IS_ADMIN; adds NANOCLAW_ADMIN_USER_IDS (owners + global admins
  + scoped admins for this agent group). Agent-runner poll-loop gates
  slash commands against that set.

New skill: /init-first-agent
- Walks the operator through standing up the first agent for a channel:
  channel pick → identity lookup (reads each channel SKILL.md's
  ## Channel Info > how-to-find-id) → DM platform_id resolution (direct-
  addressable, cold-DM via "user DMs bot first + sqlite lookup", or
  Telegram pair-code fallback) → run scripts/init-first-agent.ts →
  verify via tail of nanoclaw.log.
- scripts/init-first-agent.ts: parameterized helper that upserts the
  user + grants owner (if none), creates dm-with-<display-name> agent
  group + initGroupFilesystem, reuses/creates the DM messaging_group,
  wires it (auto-creates destination), resolves the session, and writes
  a kind:'chat' / sender:'system' welcome message into inbound.db. Host
  sweep wakes the container and the agent DMs the operator via the
  normal delivery path.

/manage-channels rewrite
- Drops --is-main / --jid / main-vs-non-main isolation references.
- First-channel flow delegates to /init-first-agent.
- Explains createMessagingGroupAgent auto-creates destinations.
- Adds a privileged-users show section.

setup/
- register.ts: drop --is-main, --jid, --local-name, --trigger
  requiresTrigger defaults; call initGroupFilesystem; normalize to
  v2 schema (no is_admin, no admin_user_id, sets unknown_sender_policy
  'strict'); let createMessagingGroupAgent handle the destination row.
- pair-telegram.ts: emit PAIRED_USER_ID (namespaced "telegram:<id>")
  instead of ADMIN_USER_ID; update header comment.
- register.test.ts deleted — was v1-only, tested a registered_groups
  table that no longer exists.

Docs
- v2-architecture-diagram.{md,html}: ER diagram updated to drop
  is_admin/admin_user_id, add unknown_sender_policy, and include
  users/user_roles/agent_group_members/user_dms.
- v2-architecture-draft.md: approval-routing paragraph rewritten for
  pickApprover/pickApprovalDelivery/ensureUserDm; SQL schema block
  updated; admin-verification paragraph references
  NANOCLAW_ADMIN_USER_IDS.
- v2-setup-wiring.md: entity-model sketch rewritten.
- v2-checklist.md: marked privilege refactor / container filtering /
  approval routing / unknown-sender gating done; removed obsolete
  admin_user_id and main-vs-non-main items.

Scripts
- scripts/init-first-agent.ts (new) replaces scripts/welcome-owner-dm.ts
  (removed; welcome-owner was a Discord-specific one-off).
- test-v2-host.ts, test-v2-channel-e2e.ts, seed-discord.ts: drop
  is_admin + admin_user_id, use unknown_sender_policy.

Tests
- src/access.test.ts (new): 14 tests for canAccessAgentGroup, role
  helpers, pickApprover, ensureUserDm, pickApprovalDelivery.
- src/db/db-v2.test.ts: adds 3 tests for the auto-created
  agent_destinations row (normalized name, no duplicates, collision
  break within an agent group).
- host-core.test.ts, channel-registry.test.ts: updated fixtures to
  use unknown_sender_policy: 'public' where the test exercises routing
  rather than the access gate.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:03:51 +03:00
Koshkoshinsk 8430e543c1 style: apply prettier formatting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:31:48 +00:00
Koshkoshinsk 63746dfeb3 fix(v2/delivery): allow agent self-messages without a destination row
Approval follow-up prompts (e.g. the post-rebuild "Packages installed,
verify they work" note) are written with channel_type='agent' and
platform_id=<self agent_group_id>, and were dropped by the
agent-to-agent authorization check because no self-destination row
exists. Agents are always authorized to message themselves; skip the
hasDestination check when source == target.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:31:44 +00:00
Koshkoshinsk 2df81e0b32 fix(v2/approvals): render correct title + selected label after click
Approval cards bypass the deliverMessage path that populates
pending_questions, so the post-click lookup found nothing and the
card edit fell back to " Question" + the raw option value
("approve"/"reject"). Store title and normalized options on
pending_approvals as well, and look up either table via a shared
getAskQuestionRender helper so the chat-sdk post-click edit and the
Discord interaction callback render the per-card title and the
selectedLabel (e.g. " Approved").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:31:44 +00:00
Koshkoshinsk 42467d796d style: apply prettier formatting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:31:44 +00:00
Koshkoshinsk d92d75e173 feat(v2/approvals): per-card titles and structured options
Approval cards now carry a required title (Add MCP Request, Install
Packages Request, Rebuild Request, Credentials Request) and structured
options with distinct pre-click label, post-click selectedLabel (e.g.
" Approved" / " Rejected"), and value used for click routing. The
title and normalized options are persisted in pending_questions so the
post-click card edit can render the correct per-type title and selected
label on both chat-sdk channels and Discord interactions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:31:44 +00:00
Gabi Simons 8d60af71d3 feat(v2): add /add-vercel skill for agent Vercel deployments
Setup skill that installs Vercel CLI in agent containers and configures
OneCLI credential injection for api.vercel.com. Container skill bundled
in .claude/skills/add-vercel/container-skills/ and copied to
container/skills/ during setup. Also adds dashboard & web apps prompt
to /setup flow (step 5b).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:24:31 +00:00
Koshkoshinsk 1903fab5e8 feat(v2/approvals): bundle install_packages + rebuild into one approval
Install approval now auto-rebuilds the image and kills the container,
replacing the prior two-card flow where the agent had to call
request_rebuild separately after install_packages was approved.

Queues a processAfter=+5s synthetic prompt so the respawned container
verifies the new packages and reports back to the user.

Adds two v2-checklist gaps found along the way:
- /remote-control and /remote-control-end are v1 host-level commands
  not ported to v2
- messaging_groups.admin_user_id is hardcoded null at registration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:54:13 +00:00
Gabi Simons 192a5a7569 docs(v2): add /add-whatsapp-v2 setup skill
Separate from the v1 /add-whatsapp skill — v1 remains untouched.
Follows the v2 skill pattern (flat sections, defers to /manage-channels
for wiring). Covers Baileys auth, pairing code, QR code, and
documents the native adapter's features and limitations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:02:27 +00:00
Gabi Simons c36541ba6c feat(v2/whatsapp): add file attachments, reactions, and inbound media
- Outbound files: images, videos, audio as native media messages;
  other types as documents. First file gets text as caption.
- Reactions: send emoji reactions via Baileys react message type
- Inbound media: download images, video, audio, documents from
  incoming messages and pass as attachments to the agent
- Edit operations silently skipped (WhatsApp linked device limitation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:30:06 +00:00
Gabi Simons c02ac06258 feat(v2): add formatting, approvals, and echo filter to WhatsApp adapter
- Markdown→WhatsApp formatting: **bold**→*bold*, *italic*→_italic_,
  headings→bold, links→plaintext, code blocks preserved
- ask_question support: renders as text with /approve, /reject slash
  commands; matches replies and routes through onAction pipeline
- credential_request: text fallback (WhatsApp has no modal support)
- Bot echo filter: skip fromMe messages to prevent loops
- Formatting applied to all outbound text messages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:02:42 +00:00
Koshkoshinsk f304c67318 fix(telegram): sanitize outbound markdown for legacy parse mode
The @chat-adapter/telegram adapter hardcodes parse_mode=Markdown (legacy)
but its converter emits CommonMark. Messages containing **bold** or list
bullets that round-trip to `*` produce "can't parse entities" errors and
get dropped after retries.

Add an opt-in transformOutboundText hook on the chat-sdk bridge and wire
a Telegram-specific sanitizer that downgrades **bold** to *bold*, rewrites
dash/plus list bullets to a Unicode bullet so the adapter's re-stringify
doesn't inject stray `*`, and strips unbalanced delimiters or brackets.
Only Telegram opts in; other channels are unaffected.

Workaround until upstream (vercel/chat) ships mode-aware conversion —
PR #367 adds a parseMode knob but not the converter fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:30:32 +00:00
Gabi Simons c303b6eb14 feat(v2): add native WhatsApp adapter using Baileys v6
Direct ChannelAdapter implementation — no Chat SDK bridge.
Ports v1 infrastructure: getMessage fallback, outgoing queue,
group metadata cache, LID-to-phone mapping, auto-reconnect.
Auth via pairing code (WHATSAPP_PHONE_NUMBER) or QR code.

Text messaging only (MVP). Not yet implemented:
- File/image attachments (send and receive)
- Edit message, delete message
- Reactions
- Bot echo filtering (own messages loop back as inbound)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:04:24 +00:00
Koshkoshinsk d16755eabc docs(v2-checklist): note self-approval UX gap 2026-04-13 14:08:51 +00:00
gavrielc 871bfa1809 fix(v2): use in-tree symlink for global CLAUDE.md @import
Claude Code's @-import directive only follows paths inside the project
memory tree (cwd + ancestors). Both `@/workspace/global/CLAUDE.md` and
`@../global/CLAUDE.md` are silently ignored because `/workspace/global`
is outside `/workspace/agent` (the cwd). The import line is parsed but
the content is never loaded — validated with a sentinel passphrase test
against a live container.

Fix: drop a `.claude-global.md` symlink into each group's dir pointing
at `/workspace/global/CLAUDE.md`. The link path is absolute on container
terms (dangling on host, valid via the /workspace/global mount) and the
symlink file itself is inside cwd, so Claude's @-import is happy. The
group's CLAUDE.md imports via `@./.claude-global.md`.

- src/group-init.ts: initGroupFilesystem now drops the symlink (idempotent,
  uses lstat so existsSync doesn't trip on the dangling target on the
  host). Default CLAUDE.md body uses `@./.claude-global.md`.
- scripts/migrate-group-claude-md.ts: creates the symlink for existing
  groups and rewrites any broken `@/workspace/global/CLAUDE.md` or
  `@../global/CLAUDE.md` import line to `@./.claude-global.md`.
- groups/main/CLAUDE.md: migration rewrote the import.

Validated: live container with the symlinked import correctly surfaces
global CLAUDE.md content (passphrase `quinoa-submarine-42` added to
global, retrieved via claude -p, removed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:46:50 +03:00
Koshkoshinsk 9a955b9b01 docs(v2-checklist): plan main/non-main -> owner/admin refactor
Pairing-code registration applies to every Telegram group once the privileged
"main chat" identity goes away.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:49:22 +00:00
Koshkoshinsk ae88d2b7c2 fix(telegram): retry adapter setup on transient network errors
Cold-start DNS/network hiccups can fail the adapter's first deleteWebhook or
getMe call, leaving the channel silently dead while the service stays up.
Wrap bridge.setup in an exponential-backoff retry (5 attempts) — if the
network is truly down we surface it instead of hanging forever.

Lives in telegram.ts so the chat-sdk bridge stays generic; other channels
can opt in by copying the small helper if they hit the same issue.
2026-04-13 12:27:45 +00:00
Koshkoshinsk 65afcdc946 feat(telegram-pairing): surface wrong-code attempts + auto-regen with retry cap
- createPairing now replaces any existing pending pairing for the same intent
  (replace-by-default; no "two pending codes for one intent" state)
- tryConsume records each attempt on pending records (capped at 10); a
  wrong code invalidates the pairing immediately (one attempt per code)
- waitForPairing gains onAttempt callback for misses and rejects with a
  distinct "invalidated by wrong code" message so callers can distinguish
  TTL expiry from user-error
- pair-telegram emits PAIR_TELEGRAM_ATTEMPT on misses and auto-regenerates
  the pairing up to 5 times, emitting PAIR_TELEGRAM_NEW_CODE for each
- Skill docs updated so the host Claude knows to show new codes and
  offer another batch on max-regenerations-exceeded

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:27:09 +00:00
Koshkoshinsk 2454444f2e feat(telegram-pairing): accept bare 4-digit codes
Require the message to be exactly the 4 digits (optionally prefixed by
@botname). Loose matches like "my pin is 0349" are rejected to avoid false
positives from chat traffic that happens to contain a 4-digit number.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:27:06 +00:00
Koshkoshinsk 2017589683 feat(telegram): self-contained pairing for chat ownership verification
BotFather issues bot tokens with no user binding, so anyone who guesses the
bot's username can DM it and get registered as a channel. Pairing closes that
gap: setup issues a one-time 4-digit code, the operator echoes it back from
the chat they want to register, and the inbound interceptor binds
admin_user_id before the message reaches the router.

- src/channels/telegram-pairing.ts: JSON-backed store with createPairing,
  tryConsume, getStatus, waitForPairing (fs.watch + poll fallback)
- src/channels/telegram.ts: wraps bridge.setup with an onInbound interceptor
  that consumes pairing codes and upserts messaging_groups
- setup/pair-telegram.ts: CLI step issues a code and waits up to 5 min for
  the operator to echo it back, emitting PLATFORM_ID/IS_GROUP/ADMIN_USER_ID
- Skill docs: /setup reorders mounts -> service -> wire (pairing needs a
  live polling adapter); /manage-channels and /add-telegram-v2 use pairing
  instead of asking the user to discover chat IDs

All other channels still bind admin via install-time identity (OAuth/QR/token);
pairing is Telegram-only. The bridge, router, and other adapters are untouched.
2026-04-13 12:27:02 +00:00
gavrielc af13c23a5a style: format group-init.ts signature
Prettier reformat applied by the format hook after the previous commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:17:55 +03:00
gavrielc 2e6dc21748 refactor(v2): per-group filesystem init, persistent across spawns
Each group's on-disk state (CLAUDE.md, .claude-shared/, agent-runner-src/)
is now initialized exactly once at group creation and owned by the group
forever after. Spawn does only mounts — no copies, no settings.json
overwrites, no skill clobbers, no source resyncs.

Global memory composition switches from "host reads /workspace/global/CLAUDE.md
at bootstrap and stuffs it into systemPrompt.append" to "group CLAUDE.md
imports it via @/workspace/global/CLAUDE.md at the top." Edits to global
propagate instantly through the existing read-only mount; no copy, no
restart.

- src/group-init.ts: new initGroupFilesystem(group, opts?) — idempotent,
  populates groups/<folder>/, .claude-shared/, agent-runner-src/ only when
  paths don't already exist.
- src/container-runner.ts: buildMounts() calls init defensively at the
  top (catches existing groups on first spawn after this change), drops
  the inline settings.json write, skills cpSync loop, and agent-runner-src
  rm-then-copy. Just mounts now.
- src/delivery.ts: create_agent flow uses initGroupFilesystem with
  optional instructions, replacing the inline mkdirSync + writeFileSync.
- container/agent-runner/src/index.ts: drops GLOBAL_CLAUDE_MD reading.
  systemContext.instructions is now only the runtime-generated
  destinations addendum.
- scripts/migrate-group-claude-md.ts: one-shot migration that prepends
  the @-import to existing groups' CLAUDE.md. Skips if global doesn't
  exist or if the @-import is already present (regex match on the @ form
  to avoid false positives from prose mentions of the path).
- groups/main/CLAUDE.md: prepended by the migration.

Existing groups need a one-time wipe of their agent-runner-src/ dir so
init re-populates from current host source — done locally before this
commit. Future host-side updates to container/skills/ or
container/agent-runner/src/ won't auto-propagate; that's the trade-off
for unconditional persistence and will be covered by host-mediated
refresh tools in a follow-up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:17:50 +03:00
Gabi Simons 8676c07448 feat(v2): support async channel adapter factories
Channel adapter factories can now return a Promise, enabling adapters
that need async initialization like loading auth state from disk
(e.g. WhatsApp reading credentials via useMultiFileAuthState).
Existing sync factories are unaffected — await on a sync return is
a no-op. All current adapters remain synchronous.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:11:06 +00:00
Gabi Simons 3db0dceb1b docs(teams-v2): full setup guide with Azure CLI, manifest, and sideloading
Rewrites the add-teams-v2 skill with step-by-step instructions
covering App Registration, client secret, Azure Bot creation (portal
and CLI), messaging endpoint, Teams channel, manifest template,
sideloading, and RSC permissions for receiving all messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:45:28 +00:00
gavrielc d4aacfe416 fix(v2): clear per-group agent-runner src before copy
fs.cpSync never removes files that disappeared from the source, so
renamed or deleted files linger in data/v2-sessions/<group>/agent-runner-src/.
The container's entrypoint runs tsc over the whole mounted src via
tsconfig's `include: ["src/**/*"]`, so a single stale file fails the
compile and the container exits 2.

Latent since the dir was introduced — surfaced when the provider
interface refactor made a leftover index-v2.ts stop typechecking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:25:40 +03:00
gavrielc b63dd186df refactor(agent-runner): decouple provider interface from Claude specifics
Reshape AgentProvider so provider-specific assumptions stop leaking into
the generic layer. No change to what reaches sdkQuery() — same values,
different plumbing.

- QueryInput: opaque `continuation` replaces `sessionId` + `resumeAt`;
  `systemContext.instructions` replaces ambiguous `systemPrompt`;
  `mcpServers`, `env`, `additionalDirectories` move to `ProviderOptions`
  at construction time.
- AgentProvider gains `isSessionInvalid(err)` and
  `supportsNativeSlashCommands` so the poll-loop stops regex-matching
  Claude error strings and gates passthrough slash commands per provider.
- ClaudeProvider owns `CLAUDE_CODE_AUTO_COMPACT_WINDOW` and the
  stale-session regex internally.
- ProviderEvent.activity kept and documented as the liveness signal
  (fires on every SDK message so the idle timer stays honest during
  long tool runs); init carries `continuation` instead of `sessionId`.
- poll-loop drops mcpServers/env/systemPrompt from its config; admin
  user id now passed explicitly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:25:29 +03:00
gavrielc e07158e194 fix(agent-runner): preserve thread_id when sending to current channel
send_file and send_message with an explicit `to` parameter were always
setting thread_id to null, causing files and messages to land in the
Discord channel root instead of the thread the session is bound to.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:13:42 +03:00
gavrielc f0e4f07ac2 refactor(v2): extract webhook server into standalone module
Aligns with upstream feat/chat-sdk-integration pattern: regex-based
routing (/webhook/{adapterName}), response streaming, cleanup function.
Updates Slack and Teams skill docs to match /webhook/{name} convention
used by all other v2 channel skills.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:36:16 +03:00
gavrielc 5a606a83d4 refactor(v2): use Chat SDK webhooks proxy and clean up webhook server
Route webhook requests through chat.webhooks[name]() instead of calling
adapter.handleWebhook() directly, getting proper auto-initialization and
signature verification. Extract Node↔Web Request/Response conversion
into reusable helpers, parse URL pathname properly for query string
safety, and support all HTTP methods (not just POST).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:36:16 +03:00
gavrielc 669a8444ef refactor(v2): extract session DB operations into src/db/session-db.ts
Move all raw SQL out of session-manager, delivery, and host-sweep into
a dedicated DB module. Make session schemas idempotent (IF NOT EXISTS)
so initSessionFolder always applies them. Revert the markdown
plain-text retry from 4c477ac.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:36:16 +03:00
Koshkoshinsk 2376c88aaf docs(v2): add delivery-failure-feedback to system actions checklist 2026-04-12 13:31:29 +00:00
Gabi Simons b140b3655b fix(agent-runner): reply to originating channel in single-destination shortcut
When an agent has one configured destination (e.g. Discord) but
receives a message from a different channel (e.g. Slack), the
single-destination shortcut was routing replies to the destination
instead of the originating channel. Now uses the inbound message's
routing context (channel_type, platform_id) when available, falling
back to the destination table only when routing context is absent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:34:21 +00:00
Gabi Simons 7e74bfd330 feat(v2): Teams adapter env-driven app type and updated skill docs
Teams adapter now reads TEAMS_APP_TYPE and TEAMS_APP_TENANT_ID from
env, supporting both MultiTenant (default) and SingleTenant configs.
Updated add-teams-v2 skill docs with full Azure Bot setup flow,
webhook endpoint format, and app package sideloading instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:34:09 +00:00
Gabi Simons a9f9eda9f8 docs(slack-v2): update skill with DM setup, webhook URL, and reinstall step
Corrects webhook URL to /api/webhooks/slack, adds Enable DMs step
(App Home > Messages Tab), documents reinstall requirement after
adding event subscriptions, and adds webhook server section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:33:56 +00:00
Gabi Simons 9476a80ab0 feat(v2): shared webhook server for webhook-based channel adapters
Adds a shared HTTP server (port 3000, configurable via WEBHOOK_PORT)
that routes incoming webhooks to the correct Chat SDK adapter by path
(e.g. /api/webhooks/slack, /api/webhooks/teams). Required by Slack,
Teams, GitHub, Linear, and other non-gateway adapters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:33:45 +00:00
Koshkoshinsk 53e12a627f chore(v2): drop session-DB schema band-aid in writeSessionRouting
The forward-compat CREATE TABLE IF NOT EXISTS papered over a stale-DB
problem we don't need to support — the canonical INBOUND_SCHEMA in
src/db/schema.ts already creates session_routing for every fresh
session DB. Pre-existing local DBs that predate the schema entry are
treated as garbage and recreated, not migrated.

Schema is the single source of truth; write paths shouldn't carry
defensive table-creation logic.
2026-04-12 10:43:42 +00:00
Koshkoshinsk 7bd8c6ad41 fix(v2): retry channel adapter setup on transient network errors
A NetworkError during adapter.setup() (e.g. Telegram deleteWebhook hitting
a DNS hiccup at boot) would log the failure and immediately give up,
leaving the channel permanently dead until the host process was manually
restarted — even though the host kept running and other channels worked.

Wrap the setup call in a small retry loop with backoff (2s, 5s, 10s) that
fires only on NetworkError. Misconfigs (bad tokens, invalid options) still
fail fast since they don't surface as NetworkError.

Universal across channels — applies to any adapter that throws
NetworkError from setup(), not just Telegram.
2026-04-12 09:32:15 +00:00
Koshkoshinsk 4c477acca3 fix(v2): retry as plain text when adapter rejects markdown
A single message with markdown the adapter couldn't parse (e.g. Telegram
MarkdownV2 entity errors) would fail in deliverSessionMessages and be
retried forever, blocking every subsequent reply on that session.

Catch ValidationError from postMessage and retry once with the markdown
stripped to plain text via markdownToPlainText. Files re-attach in a
follow-up post since the plain-text retry drops the files payload shape.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 09:32:12 +00:00
gavrielc 9dda75bb21 docs(v2): cross-mount invariants + diagrams; inline a2a routing
- session-manager.ts: shrink the cross-mount invariant header from 31
  lines to 12, keeping each invariant's cause and consequence inline.
- agent-runner/db/connection.ts: parallel cross-mount comment for the
  container-side reader (inbound.db must be journal_mode=DELETE).
- agent-runner/db/messages-out.ts: document that even/odd seq parity
  is load-bearing — seq is the agent-facing message ID returned by
  send_message and consumed by edit_message / add_reaction, looked
  up across both tables.
- v2-checklist.md: record the cross-mount invariants and seq parity
  under Core Architecture so future "simplifications" don't regress
  them.
- scripts/sanity-live-poll.ts: empirical validation harness for the
  three cross-mount invariants — flips each one and observes silent
  message loss / corruption.
- delivery.ts: inline routeAgentMessage at its single callsite (-17
  net lines). The wrapper added more boilerplate than it factored.
- docs/v2-architecture-diagram.{md,html}: rendered Mermaid diagrams
  of the v2 system, message flow, named destinations, entity model,
  and the two-DB split.
- channels/adapter.ts, chat-sdk-bridge.ts, credentials.ts,
  db/sessions.ts, db/db-v2.test.ts: prettier format pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:21:12 +03:00
gavrielc c9fa5cdbed docs(v2): expand checklist — credential collection, approvals, Chat SDK input
Mark OneCLI manual-approval integration and credential collection from chat
as partial with concrete sub-TODOs. Add upstream asks:

* Chat SDK input support beyond Slack — platforms support it natively
  (Discord modals, Teams/GChat/Webex Adaptive Cards, WhatsApp Flows), Chat
  SDK just doesn't expose the surfaces yet. Concrete per-platform mapping
  captured.
* Built-in OneCLI apps shadow generic secrets on the same host; the
  collection tool should check apps-list first and surface the connect URL
  when an app exists.
* Tunneled OneCLI dashboard fallback for channels with no native form input.
* Per-agent-group secret scoping via OneCLI agentId.
* SDK-native secret management to replace the shell facade in onecli-secrets.

Also:

* Admin model refactor — instance-level default admin + per-group override
  + DM delivery when supported.
* Discord-specific Chat SDK quirks (first-message @mention requirement,
  sub-thread materialization on subscribe).
* OneCLI migration check under Migration — flag whether existing installs
  need OneCLI re-init (new SDK version, credentials re-scoped).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:18:51 +03:00
gavrielc 062b0cb6bf fix(agent-runner): add updated_at column to session_state on older DBs
session_state was added after the initial v2 schema with a lazy
`CREATE TABLE IF NOT EXISTS` in getOutboundDb(), so older session
outbound.db files have a session_state table from before updated_at
existed. The lazy create is a no-op when the table already exists,
leaving the column missing and causing:

    Error: table session_state has no column named updated_at

on every `INSERT OR REPLACE INTO session_state` call.

Follow up the CREATE IF NOT EXISTS with a PRAGMA table_info check and
ALTER TABLE ADD COLUMN when updated_at is missing. Cheap on every open,
only runs DDL once per DB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:18:34 +03:00
gavrielc e92b245399 feat(v2): OneCLI 0.3.1 — approvals, credential collection, threaded routing
Three features built on top of @onecli-sh/sdk 0.3.1, landed together because
they share wiring surfaces (session DB schema, delivery dispatcher, Chat SDK
bridge, channel adapter contract).

## OneCLI manual-approval handler

* `src/onecli-approvals.ts` — long-polls OneCLI via the SDK's
  `configureManualApproval`; on each request, delivers an `ask_question` card
  to the admin agent group's first messaging group, persists a
  `pending_approvals` row, and waits on an in-memory Promise resolved by the
  admin's button click or an expiry timer. Expired cards are edited to
  "Expired (...)" and a startup sweep flushes any rows left over from a
  previous process.
* Short 11-byte approval id (`oa-<8 base36>`) instead of the SDK's UUID so the
  Telegram 64-byte `callback_data` limit is respected; the OneCLI UUID stays
  in the persisted payload for audit.
* Migration 003 consolidated: `pending_approvals` now has the OneCLI-aware
  columns from the start (`agent_group_id`, `channel_type`, `platform_id`,
  `platform_message_id`, `expires_at`, `status`), `session_id` relaxed to
  nullable so cross-session approvals fit.
* `handleQuestionResponse` in `src/index.ts` now routes OneCLI approvals
  through `resolveOneCLIApproval` before falling back to the
  session-bound approval path.

## Credential collection from chat

New `trigger_credential_collection` MCP tool — the agent researches a
third-party API, calls the tool with `{name, hostPattern, headerName,
valueFormat, description}`, and blocks until the host reports saved, rejected,
or failed. The credential value never enters the agent's context: the user
submits it into a Chat SDK Modal on the host side, the host writes it to
OneCLI via a thin facade (`src/onecli-secrets.ts` — shells out to
`onecli secrets create`, shape mirrors the SDK we expect upstream), and only
the status string flows back to the container via a system message.

* `src/credentials.ts` — host-side handler: delivers the card to the
  conversation's own channel (not the admin channel — credential collection
  is a user-facing flow, distinct from admin approval), persists a
  `pending_credentials` row, drives the submit → `createSecret` → notify
  pipeline. Falls back gracefully when the channel doesn't support modals.
* `src/db/credentials.ts` + migration 005: `pending_credentials` table.
* `src/channels/chat-sdk-bridge.ts`: renders a `credential_request` card,
  handles the `nccr:` action prefix by opening a Modal with a TextInput,
  registers an `onModalSubmit` handler for the `nccm:` callback prefix.
* `container/agent-runner/src/mcp-tools/credentials.ts`: the blocking MCP
  tool, mirroring the `ask_user_question` polling pattern.
* `container/agent-runner/src/db/messages-in.ts`: `findCredentialResponse`
  helper to pick up the system message the host writes back.

## Threaded adapter routing

The destination layer previously didn't carry thread context, so agent replies
to Discord always landed in the root channel regardless of which thread the
inbound came from.

* `ChannelAdapter.supportsThreads: boolean` — declared by every channel skill
  at `createChatSdkBridge`. Threaded: Discord, Slack, Teams, Google Chat,
  Linear, GitHub, Webex. Non-threaded: Telegram, WhatsApp Cloud, Matrix,
  Resend, iMessage.
* `src/router.ts`: non-threaded adapters strip `threadId` at ingest (threads
  collapse to channel-level sessions). Threaded adapters override the
  wiring's `session_mode` to `'per-thread'` so each thread = a session
  (except `agent-shared`, which is preserved as a cross-channel intent the
  adapter can't know about).
* `session_routing` table in `inbound.db` — single-row default reply routing
  written by the host on every container wake from
  `session.messaging_group_id` + `session.thread_id`. Forward-compat
  `CREATE TABLE IF NOT EXISTS` handles older session DBs lazily.
* `container/agent-runner/src/db/session-routing.ts` — container-side reader.
* `send_message` / `send_file` / `ask_user_question` / `send_card` /
  scheduling tools all default their routing (channel, platform, **and**
  thread) from the session when no explicit `to` is given. Explicit `to`
  uses the destination's channel with `thread_id = null` (cross-destination
  sends start a new conversation elsewhere).
* `poll-loop.ts::sendToDestination` (the final-text single-destination
  shortcut) now inherits `thread_id` from `RoutingContext` too — this was
  the root cause of Discord replies landing in the root channel even after
  `send_message` was wired correctly.

## Related cleanups

* `src/container-runner.ts`: OneCLI agent identifier switched from the lossy
  folder-derived string to `agent_group.id`, making `getAgentGroup(externalId)`
  a trivial reverse lookup for per-agent scoping.
* `wakeContainer` race fix via an in-flight promise map — concurrent wakes
  during the async buildContainerArgs / OneCLI `applyContainerConfig` window
  no longer double-spawn containers against the same session directory.
* `src/db/db-v2.test.ts`: dropped the brittle `expect(row.v).toBe(N)` schema
  version assertion — it had to be bumped on every migration addition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:18:21 +03:00
gavrielc 9dc8bc5d99 docs(v2): expand checklist — chat-first setup, product focus, skills marketplace
Capture the product direction that's been landing in recent work:
everything configurable from chat once bootstrap is done, skills as
the primary extension mechanism, and mark named destinations / agent
self-modification / agent-to-agent comms as complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 01:18:09 +03:00
gavrielc 630dd54ea9 chore(container): drop v1 IPC dirs and update entrypoint comment
The /workspace/ipc/* tree is a v1 leftover — v2 routes everything
through inbound.db / outbound.db. Refresh the surrounding comment to
describe what the entrypoint actually does.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 01:18:01 +03:00
gavrielc b59216c299 fix(v2): persist SDK session ID across container restarts
The v2 poll loop held the session ID in a local variable, so every
container restart started a fresh SDK session even though the .jsonl
transcript was still sitting in the shared .claude mount. Store it in
outbound.db (container-owned, already per channel/thread), seed the
loop on startup, clear on /clear, and recover from stale-session
errors the same way v1 did.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 01:17:42 +03:00
gavrielc b591d7ce96 refactor: move destinations from JSON file into inbound.db
The per-session destination map was being written as a sidecar JSON file
(/workspace/.nanoclaw-destinations.json) — inconsistent with the rest of
v2, where all host↔container IO goes through inbound.db / outbound.db.

Move it into a `destinations` table in INBOUND_SCHEMA. The host writes
it before every container wake AND on demand (e.g. after create_agent)
so the creator sees the new child destination mid-session without a
restart. The container queries the table live on every lookup — no
cache, no staleness window.

- src/db/schema.ts: add `destinations` table to INBOUND_SCHEMA.
- src/session-manager.ts: writeDestinationsFile → writeDestinations,
  writes via DELETE + INSERT inside a transaction.
- src/delivery.ts: create_agent handler calls writeDestinations on the
  creator's session after inserting the new destination rows.
- container/agent-runner/src/destinations.ts: queries inbound.db
  directly in every findByName/getAllDestinations/findByRouting call.
  No more cache. No setDestinationsForTest (obsolete). No fs import.
- container/agent-runner/src/index.ts and mcp-tools/index.ts: remove
  loadDestinations() calls — no longer needed.
- Test helper initTestSessionDb creates the destinations table.
  Integration test inserts a row directly instead of mocking the cache.

No backwards compatibility: sessions predating the schema update must
be recreated. This is fine on the v2 branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:45:53 +03:00
gavrielc 09e1861a22 feat: single-destination shortcut — no wrapping needed when there's only one
When an agent has exactly one configured destination, wrapping output in
<message to="..."> blocks is unnecessary. Plain text goes to the sole
destination automatically. This preserves the simple "just reply" flow
for the common case of one user on one channel.

Applies in three places:

- System prompt addendum: single-destination case gets a simplified
  explanation ("your messages are delivered to X, just write directly").
  Multi-destination case keeps the <message to="..."> syntax docs.

- Main output parser: if zero <message> blocks are found and there is
  exactly one destination, the entire cleaned text (with <internal>
  stripped) is sent to that destination.

- send_message / send_file MCP tools: `to` parameter is now optional.
  With one destination, omitted defaults to it. With multiple, omitting
  returns an error listing the options.

Multi-destination behavior is unchanged — explicit <message to="..."> is
still required, and untagged text is still scratchpad.

groups/global/CLAUDE.md updated to describe both cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:36:09 +03:00
gavrielc 67f081671d style: prettier formatting fixes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:31:45 +03:00
gavrielc e83ffbc103 feat: named destinations + permission enforcement + fire-and-forget self-mod
Replaces implicit routing context (NANOCLAW_PLATFORM_ID env vars) with
per-agent named destination maps. Agents reference channels and peer
agents by local names; the host re-validates every outbound route against
a new agent_destinations table that is both the routing map and the ACL.

Model changes:
- New migration 004 adds agent_destinations (agent_group_id, local_name,
  target_type, target_id). Backfills from existing messaging_group_agents.
- Host writes /workspace/.nanoclaw-destinations.json before every container
  wake so admin changes take effect on next start.
- Container loads map at startup, appends system-prompt addendum listing
  available destinations and the <message to="name">…</message> syntax.
- Agent main output is parsed for <message to="..."> blocks; each block
  becomes a messages_out row with routing resolved via the local map.
  Untagged text and <internal>…</internal> are scratchpad (logged only).
- send_message MCP tool now takes `to` (destination name) instead of raw
  routing fields. send_to_agent deleted (redundant — agents are just
  destinations). send_file/edit_message/add_reaction route via map too.
- Inbound formatter adds from="name" attribute via reverse-lookup so the
  agent sees a consistent namespace in both directions.

Permission enforcement:
- Host checks hasDestination() before every channel delivery AND every
  agent-to-agent route. Unauthorized messages dropped and logged.
- routeAgentMessage simplified: ~15 lines, no JSON parse, content copied
  verbatim (target formatter resolves the sender via its own local map).
- create_agent is admin-only, checked at both the container (tool not
  registered for non-admins) and the host (re-check on receive). Inserts
  bidirectional destination rows so parent↔child comms work immediately.
  Includes path-traversal guard on folder name.

Self-modification cleanup:
- add_mcp_server now requires admin approval (previously had none).
- install_packages validates package names on BOTH sides (container tool
  + host receiver) with strict regex. Max 20 packages per request.
- All three self-mod tools are fire-and-forget: write request, return
  immediately with "submitted" message. Admin approval triggers a chat
  notification to the requesting agent — no tool-call polling, no 5-min
  holds. On rebuild/mcp_server approval, the container is killed so the
  next wake picks up new config/image.
- Approval delivery extracted into requestApproval() helper (the one
  place where three call sites were literally identical).

Also folded in the phase-1 dynamic import cleanup (create_agent no longer
does `await import('./db/agent-groups.js')`) and removes NANOCLAW_PLATFORM_ID
/ CHANNEL_TYPE / THREAD_ID env-var routing entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:31:37 +03:00
gavrielc 4004a6b284 docs: add self-customize skill and refine communication guidance
Self-customize skill lives in container/skills/ so it's loaded into the
agent container at runtime. Documents the builder-agent pattern with
diff size limits for safer self-modification.

CLAUDE.md communication section now has three tiers (short / longer /
long-running) instead of a single blanket rule — agents should
acknowledge upfront on longer work and update before slow operations,
but stay silent on quick tasks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:02:32 +03:00
gavrielc 6eb81b5737 style: prettier formatting fixes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:11:06 +03:00
gavrielc d8fbd3b239 feat: agent-to-agent communication, dynamic agent creation, self-modification tools
Agent-to-agent: host routes messages with channel_type='agent' to target
agent's inbound.db, enriches with sender info, wakes target container.
Bidirectional routing works via inherited routing context.

Dynamic agents: create_agent MCP tool + system action handler creates
agent groups, folders, and optional CLAUDE.md on the fly.

Self-modification: install_packages (apt/npm, requires admin approval),
add_mcp_server (no approval), request_rebuild (builds per-agent-group
Docker image with approved packages). Approval flow reuses interactive
card infrastructure with pending_approvals table.

Also includes fixes from prior session: attachment download, reply context
extraction, message editing (platform message ID tracking), delivery retry
limits, and card update on button click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:11:06 +03:00
Gabi Simons 9af9bc947a fix(discord-v2): document required DISCORD_PUBLIC_KEY and APPLICATION_ID
The Discord adapter fails to start without all three env vars. Also
fix platform ID format docs to show discord:{guildId}:{channelId}.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:57:28 +00:00
gavrielc 69939b7774 docs: fix v2 checklist accuracy — pre-agent scripts, typing, stubs
- Pre-agent scripts: [~] → [ ] (formatter references scriptOutput but
  no execution logic exists)
- Add typing indicator as completed (triggerTyping in router)
- Remove "stub exists" from register_group/reset_session (no stubs found)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:54:54 +03:00
gavrielc e2dbc35a15 docs: add auto-onboarding to v2 checklist
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:52:35 +03:00
gavrielc 5a309a0e25 fix: only send onboarding message on first wiring, not re-registration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:49:58 +03:00
gavrielc 4a999ec973 feat: auto-onboarding when a channel is registered
After wiring a channel to an agent group, register.ts writes a task
message to the session that triggers the /welcome container skill.
The agent introduces itself immediately — the user sees typing and
then a greeting without having to send a message first.

Uses kind 'task' (not 'system') so the poll loop picks it up normally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:48:56 +03:00
gavrielc a2badbd525 fix: normalize platform ID at registration, not router lookup
Channel adapters prefix platform IDs with their channel type
(e.g. "telegram:123"). Normalize in register.ts so the DB always
stores the canonical format. Removes fallback lookup from router.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:41:07 +03:00
gavrielc 9f5c37fc4c fix: handle platform ID prefix mismatch in router, not register
Move prefix handling from register.ts to router.ts. Users register with
raw platform IDs (what they naturally have), adapters send prefixed IDs
(their internal format). Router now tries stripping the channel type
prefix when the exact lookup fails, matching either format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:39:40 +03:00
gavrielc 6941e37366 fix: auto-prefix platform IDs in register.ts to match Chat SDK format
Chat SDK adapters use prefixed platform IDs (e.g. "telegram:6037840640",
"discord:guildId:channelId") but users provide raw IDs during setup.
Without the prefix, the router can't match the registered messaging group
to incoming messages and silently drops them.

register.ts now auto-prefixes with the channel type if not already present.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:38:23 +03:00
gavrielc d656b5ccc1 fix: Chat SDK bridge delivery and typing for non-Discord adapters
- Use platformId directly as thread ID in deliver() and setTyping()
  instead of calling encodeThreadId with Discord-shaped args — platformId
  is already in the adapter's encoded format (e.g. "telegram:6037840640")
- Add triggerTyping() in delivery.ts, call from router on message route
- Enable Telegram channel in barrel
- Verified E2E: Telegram message in → agent → typing indicator → response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:36:45 +03:00
gavrielc 57a6491c7e v2: channel isolation model, manage-channels skill, refactored channel skills
- Add three-level isolation model (shared session, same agent, separate agent)
  with agent-shared session mode for cross-channel shared sessions
- Create /manage-channels skill for wiring channels to agent groups
- Refactor all 12 v2 channel skills: lean SKILL.md + VERIFY.md + REMOVE.md
  with structured Channel Info section for platform-specific metadata
- Create /add-discord-v2 skill (was missing)
- Add step 5a to setup SKILL.md invoking /manage-channels after channel install
- Update setup/verify.ts to check all 12 channel token types
- Add docs/v2-isolation-model.md explaining the isolation model
- Update v2-checklist.md and v2-setup-wiring.md to reflect completed work

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:19:19 +03:00
gavrielc ed76d51e0b docs: add v2 setup wiring status and remaining work
Detailed status document for next session: what's done (two-DB split,
OneCLI, barrel, register.ts), what's not (channel skills don't call
register, no group creation step in setup, v1 add-discord incompatible).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:28:46 +03:00
gavrielc 1dc5750ca3 fix: uncomment Discord import in channel barrel
Discord was directly imported in src/index.ts before the barrel wiring.
Moving to the barrel without uncommenting it broke Discord.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:24:06 +03:00
gavrielc e7514edd35 fix: wire v2 setup flow — barrel import, registration, verification
- Import channel barrel from src/index.ts so channel skills that
  uncomment lines in src/channels/index.ts actually execute
- Rewrite setup/register.ts to create v2 entities (agent_groups,
  messaging_groups, messaging_group_agents) in data/v2.db instead
  of v1's store/messages.db
- Fix setup/verify.ts to check v2 central DB for registered groups
- Add prominent "MESSAGE DROPPED" warnings in router when no agent
  groups are wired, with actionable guidance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:23:23 +03:00
gavrielc b76fd425c8 style: prettier formatting fixes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:18:31 +03:00
gavrielc 82cb363f84 v2: split session DB into inbound/outbound for write isolation
Eliminates SQLite write contention across the host-container mount
boundary by splitting the single session.db into two files, each with
exactly one writer:

  inbound.db  — host writes (messages_in, delivered tracking)
  outbound.db — container writes (messages_out, processing_ack)

Key changes:
- Host uses even seq numbers, container uses odd (collision-free)
- Container heartbeat via file touch instead of DB UPDATE
- Scheduling MCP tools now emit system actions via messages_out
  (host applies them to inbound.db during delivery)
- Host sweep reads processing_ack + heartbeat file for stale detection
- OneCLI ensureAgent() call added (was missing from v2, caused
  applyContainerConfig to reject unknown agent identifiers)

Verified: tsc clean, 327 tests pass, real e2e through Docker works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:17:31 +03:00
gavrielc 320176e7e8 fix: remaining -v2 references in scripts, add v1 channels barrel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:44:06 +03:00
gavrielc 2b64fec0e6 fix: clean up iMessage adapter type compatibility
Replace `as never` cast with proper polyfill for channelIdFromThreadId.
Narrow GatewayAdapter cast to only the gateway code path in bridge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:42:49 +03:00
gavrielc 9486d56b01 v2: make v2 the main entry point, move v1 to src/v1/
- Move all v1 files (index, router, container-runner, db, ipc, types,
  logger, channels/registry, and all utilities) to src/v1/ as a
  fully self-contained archive with no shared dependencies
- Rename v2 files to remove -v2 suffix (index-v2.ts → index.ts, etc.)
- Update all imports across v2 source, tests, and setup files
- Migrate shared utilities (config, env, container-runtime, mount-security,
  timezone, group-folder) from pino logger to v2 log module
- Migrate setup/ files from logger to log with argument order swap
- Container agent-runner: move v1 entry to v1/, rename v2 to index.ts
- Update setup skill to offer all 13 v2 channels
- Install all Chat SDK adapter packages
- dist/index.js now runs v2; dist/v1/index.js runs v1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:40:36 +03:00
gavrielc 12af451069 v2: add Chat SDK channel adapters and skills for 11 platforms
Thin wrapper adapters + SKILL.md for Slack, Telegram, GitHub, Linear,
Google Chat, Teams, WhatsApp Cloud API, Resend, Matrix, Webex, iMessage.
All follow the same pattern as discord-v2.ts: readEnvFile → create*Adapter
→ createChatSdkBridge → registerChannelAdapter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:26:33 +03:00
gavrielc 8a06b01646 v2: SQLite state adapter, admin commands, compact feedback
- Replace in-memory Chat SDK state with SqliteStateAdapter — thread
  subscriptions now persist across restarts
- Add migration 002 for chat_sdk_kv, subscriptions, locks, lists tables
- Handle /clear in agent-runner (reset sessionId) — SDK has
  supportsNonInteractive:false for this command
- Pass /compact, /context, /cost, /files through to SDK as admin commands
- Skip admin commands in follow-up poll so they start fresh queries
- Emit compact_boundary events as user-visible feedback messages
- Pass NANOCLAW_ADMIN_USER_ID and NANOCLAW_ASSISTANT_NAME to containers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 03:58:35 +03:00
gavrielc c31bb02c06 v2 phase 5: pending questions with interactive cards
End-to-end ask_user_question flow:
- Agent MCP tool writes question card to messages_out
- Host delivery creates pending_questions row, delivers as Discord Card with buttons
- Local webhook server receives Gateway INTERACTION_CREATE events
- Acknowledges interaction + updates card to show selected answer
- Routes response back to session DB as system message
- MCP tool poll picks up response and returns to agent

Key fixes:
- Poll loop now skips system messages (reserved for MCP tool responses)
- Gateway listener uses webhookUrl forwarding mode for interaction support
- Button custom_id encodes questionId + option text for self-contained routing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 03:26:16 +03:00
gavrielc c348fabf22 v2 phase 5: scheduling fixes, media handling, command processing
- Host sweep: fix DELETE journal mode, busy_timeout, seq in recurrence INSERT
- Outbound files: delivery reads from outbox dir, passes buffers to adapter,
  cleans up after delivery. Chat SDK bridge sends files via postMessage.
- Inbound attachments: formatter includes attachment info in prompts
- Commands: categorize /commands as admin, filtered, or passthrough.
  Admin commands check sender against NANOCLAW_ADMIN_USER_ID.
  Filtered commands silently dropped. Passthrough sent raw to agent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:59:33 +03:00
gavrielc afbc20a6c4 v2 phase 4+5: Discord via Chat SDK, expanded MCP tools, message seq IDs
- Chat SDK bridge + Discord adapter (gateway listener, message routing)
- MCP tools refactored into modular structure: core (send_message, send_file,
  edit_message, add_reaction), scheduling (schedule/list/cancel/pause/resume
  tasks), interactive (ask_user_question, send_card), agents (send_to_agent)
- Message seq IDs: shared integer sequence across messages_in/out so agents
  see small numeric IDs instead of platform snowflakes
- busy_timeout=5000 for session DB (poll loop + MCP server concurrent access)
- Always copy agent-runner source to fix stale cache when non-index files change
- Seed script for Discord testing, e2e test script

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:53:39 +03:00
gavrielc b36f127acc style: prettier formatting fixes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:40:52 +03:00
gavrielc 6f2a7314d0 v2: fix agent-runner lifecycle and session DB reliability
- Use DELETE journal mode for session DBs instead of WAL. WAL doesn't
  sync reliably across Docker volume mounts (VirtioFS), causing dropped
  writes and duplicate deliveries.
- Add 20s idle detection to end the query stream. The concurrent poll
  tracks SDK activity via a new 'activity' provider event. When no SDK
  events arrive for 20s and no messages are pending, the stream ends
  and the poll loop continues.
- Add touchProcessing heartbeat so the host can distinguish active
  agents from idle ones by checking status_changed recency.
- Catch query errors in the poll loop and write error responses to
  messages_out instead of crashing the process.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:34:59 +03:00
gavrielc 7201fe5032 v2 phase 4: channel adapter interface, registry, and host wiring
ChannelAdapter interface with setup/deliver/teardown/setTyping lifecycle.
Self-registration pattern via channel-registry. Host wiring in index-v2
bridges inbound messages to routeInbound and outbound delivery to adapters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 00:10:46 +03:00
gavrielc d35386a46e style: apply prettier formatting to v2 source files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:59:08 +03:00
gavrielc 03c4e3b672 v2: fix container launch for v2 agent-runner
- Override entrypoint to compile and run index-v2.js (no stdin)
- Add better-sqlite3 + @types to agent-runner dependencies
- Exclude test files from agent-runner tsconfig (Docker build)
- Add real e2e test script (host → container → Claude → session DB)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:49:30 +03:00
gavrielc 8535875d0c v2: add host core integration tests
Tests for session manager (folder/DB creation, shared vs per-thread
resolution, message writing), router (end-to-end routing, auto-create
messaging groups), and delivery (undelivered message detection).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:44:26 +03:00
gavrielc d7c68e04b1 v2 phase 3: host core — router, session manager, delivery, sweep
Host orchestrator connecting channel events to session DBs and
delivering responses back through channel adapters.

- session-manager.ts: session folder/DB lifecycle, message writing
- container-runner-v2.ts: Docker spawn with session + agent group
  mounts, OneCLI, idle timeout, agent-runner recompilation
- router-v2.ts: inbound routing (channel → messaging group → agent
  group → session → messages_in → wake container)
- delivery.ts: two-tier polling (1s active, 60s sweep) for
  messages_out, channel adapter delivery
- host-sweep.ts: stale detection with backoff, recurrence, wake
  containers for due messages
- index-v2.ts: thin entry point wiring everything together
- scripts/test-v2-agent.ts: real Claude provider integration test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:43:13 +03:00
gavrielc 18d0b6e53f v2: add agent-runner integration tests
Poll loop end-to-end with mock provider: message pickup, batch
processing, concurrent polling for late arrivals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:40:00 +03:00
gavrielc 5a0098edc9 v2 phase 2: agent-runner — provider interface, poll loop, formatter
AgentProvider abstraction with Claude and Mock implementations.
Poll loop reads messages_in, formats by kind, queries provider,
writes results to messages_out. Concurrent polling pushes follow-up
messages into active queries.

- providers/types.ts: AgentProvider, AgentQuery, ProviderEvent
- providers/claude.ts: wraps Agent SDK with MessageStream, hooks,
  transcript archiving
- providers/mock.ts: canned responses with push() support
- providers/factory.ts: createProvider()
- formatter.ts: format by kind (chat/task/webhook/system), XML
  escaping, routing extraction
- poll-loop.ts: poll → format → query → write, concurrent polling
- mcp-tools.ts: MCP server with send_message tool
- index-v2.ts: new entry point (config from env, enters poll loop)
- 11 new tests, all 288 tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:36:55 +03:00
gavrielc 3f0451b7b0 v2 phase 1: foundation — types, DB layer, logging
Add the v2 data layer: typed interfaces, central DB with migration
runner, per-entity CRUD, and agent-runner session DB operations.

- src/log.ts: concise message-first logging API
- src/types-v2.ts: AgentGroup, MessagingGroup, Session, MessageIn/Out
- src/db/: connection (WAL), migration runner, 001-initial schema,
  CRUD for agent_groups, messaging_groups, sessions, pending_questions
- container/agent-runner/src/db/: session DB connection, messages_in
  reads + status transitions, messages_out writes
- 31 new tests, all 277 tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:34:09 +03:00
gavrielc 90acff28ad chore: set printWidth to 120 and reformat
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:34:03 +03:00
gavrielc e540df46e6 docs: add code style (120 char lines, concise logging) and config pattern
Skills document env vars in SKILL.md instead of patching config.ts.
Prettier printWidth 120 to keep log calls and signatures on one line.
Thin logging wrapper for one-line structured log calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:24:09 +03:00
gavrielc a03f832dbb docs: add v1 conflict hotspot analysis and isolation strategies
Based on analysis of 33 skill branches. Maps each conflict hotspot
(index.ts, config.ts, container-runner.ts, db.ts) to its v2 solution.
Adds mount registration pattern so channel skills don't edit
container-runner. Config stays in the module that uses it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:19:35 +03:00
gavrielc 820c5067b7 docs: add DB file structure and migration strategy
Split DB by entity (agent-groups.ts, messaging-groups.ts, sessions.ts)
instead of one monolith. Numbered migration files replace inline
ALTER TABLE blocks. Channels use barrel pattern for self-registration.
Session DB split into messages-in.ts and messages-out.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:16:17 +03:00
gavrielc 1b652e1dc0 docs: add code structure principles for skill customization
Channels, MCP tools, and providers use registration patterns so
skill branches can add capabilities without conflicting. Index
stays thin. File map updated with channels/ and mcp-tools/
directories.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:10:15 +03:00
gavrielc b539fddbcb docs: v2 architecture design — session DB, channel adapters, agent provider
Three documents covering the complete v2 architecture:

- v2-architecture-draft.md: Core design (per-session SQLite as sole IO,
  two-level DB, entity model, channel adapters with Chat SDK bridge,
  container lifecycle, message flow, interactive operations, routing,
  flexibility model with PR Factory example)

- v2-api-details.md: Channel adapter interface definitions, Chat SDK
  bridge implementation, native channel example, message content
  format examples, host delivery logic

- v2-agent-runner-details.md: AgentProvider interface (stream-in/out),
  provider implementations (Claude, Codex, OpenCode), poll loop, MCP
  tool definitions, message formatting, media handling, container
  startup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:07:33 +03:00
gavrielc 934f063aff update deps 2026-04-07 08:35:25 +03:00
gavrielc 32a487b96b Merge pull request #1660 from johnnyfish/fix/gmail-onecli-credential-mode
fix(gmail): add OneCLI credential mode detection
2026-04-07 01:12:05 +03:00
johnnyfish 751a9ed2d1 fix(gmail): add OneCLI credential mode detection 2026-04-06 20:34:24 +03:00
gavrielc 22d7856ce0 reduce setup friction 2026-04-06 01:19:22 +03:00
gavrielc ca9333d48d improve diagnostics 2026-04-06 00:37:34 +03:00
gavrielc 6c289c3a80 chore: add .npmrc with 7-day minimum release age
Supply chain protection — npm will not install package versions
published less than 7 days ago.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:37:52 +03:00
github-actions[bot] b8cf30830b chore: bump version to 1.2.52 2026-04-05 19:33:34 +00:00
gavrielc 5702760206 Merge pull request #1644 from sargunv/fix/global-memory-path
Fix global memory for main agent: correct path and add writable mount
2026-04-05 22:33:23 +03:00
github-actions[bot] 653390d9aa chore: bump version to 1.2.51 2026-04-05 19:29:01 +00:00
gavrielc 3381509e69 Merge pull request #1658 from guyb1/patch-1
Update SKILL.md to use ONECLI_URL variable
2026-04-05 22:28:50 +03:00
Guy Ben Aharon 19ce90c663 fix 2026-04-05 21:36:42 +03:00
Guy Ben-Aharon 0918f78a0c fix 2026-04-05 20:01:46 +03:00
Guy Ben-Aharon 4fd75860cd update init-onecli 2026-04-05 19:46:29 +03:00
Guy Ben-Aharon 5adc9497b3 Update SKILL.md to use ONECLI_URL variable 2026-04-05 19:40:52 +03:00
gavrielc 1d5c38d15a fix: three issues in karpathy wiki skill
1. Lint schedule now uses NanoClaw scheduled_tasks table instead of
   Claude Code cron — runs in the group's agent container
2. CLAUDE.md must enforce one-at-a-time file ingestion — never batch
3. Expanded CLAUDE.md guidance: explain system, index files, point to
   container skill, enforce ingest discipline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:35:40 +03:00
github-actions[bot] 75c2e1868f chore: bump version to 1.2.50 2026-04-05 13:16:10 +00:00
gavrielc f77f9ce2c4 feat: set auto-compact threshold to 165k tokens
Compact earlier to preserve more context fidelity before the window fills.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:15:56 +03:00
gavrielc 27f9f0ca32 Merge pull request #1649 from qwibitai/skill/wiki
feat: add /add-karpathy-llm-wiki skill
2026-04-05 11:29:28 +03:00
Sargun Vohra 1488c5b251 fix: add writable global memory mount for main agent
Main group had no mount for the global memory directory
(/workspace/global), so it could only reach it through the read-only
project root. This meant the main agent couldn't write to global
memory despite groups/main/CLAUDE.md instructing it to do so.

Add a writable mount at /workspace/global for the isMain branch,
matching the read-only mount that non-main groups already have.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:11:48 -07:00
Sargun Vohra 22ab96ccac fix: correct global memory path in container CLAUDE.md
The documented path /workspace/project/groups/global/CLAUDE.md doesn't
match the actual mount point /workspace/global. This caused agents to
look for global memory at a nonexistent path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:01:09 -07:00
292 changed files with 34313 additions and 9701 deletions
+1
View File
@@ -0,0 +1 @@
{"sessionId":"bedd47ed-bfa0-41da-9a03-93d41159b4cd","pid":24606,"acquiredAt":1776194767342}
+41 -1
View File
@@ -1 +1,41 @@
{}
{
"sandbox": {
"enabled": false
},
"permissions": {
"allow": [
"Bash(bash setup.sh*)",
"Bash(git remote *)",
"Bash(pnpm exec tsx setup/index.ts*)",
"Bash(pnpm exec tsx scripts/init-first-agent.ts*)",
"Bash(pnpm install @chat-adapter/*)",
"Bash(pnpm install chat-adapter-imessage*)",
"Bash(pnpm install @bitbasti/chat-adapter-webex*)",
"Bash(pnpm install @resend/chat-sdk-adapter*)",
"Bash(pnpm install @whiskeysockets/baileys*)",
"Bash(pnpm install @beeper/chat-adapter-matrix*)",
"Bash(pnpm install @nanoco/nanoclaw-dashboard*)",
"Bash(pnpm install --frozen-lockfile*)",
"Bash(pnpm run build*)",
"Bash(curl -fsSL onecli.sh*)",
"Bash(onecli *)",
"Bash(grep -q *)",
"Bash(echo *>> .env)",
"Bash(ls *)",
"Bash(cat ~/.config/nanoclaw/*)",
"Bash(tail *logs/*)",
"Bash(launchctl *nanoclaw*)",
"Bash(sqlite3 data/*)",
"Bash(docker info*)",
"Bash(docker logs *)",
"Bash(mkdir -p *)",
"Bash(cp .env *)",
"Bash(rsync -a .claude/skills/*)",
"Bash(head *)",
"Bash(xattr *)",
"Bash(find ~/.npm *)",
"Bash(which onecli*)",
"Bash(./container/build.sh*)"
]
}
}
+5 -5
View File
@@ -39,8 +39,8 @@ This adds:
### Validate
```bash
npm test
npm run build
pnpm test
pnpm run build
```
### Rebuild container
@@ -60,7 +60,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
### Integration Test
1. Start NanoClaw in dev mode: `npm run dev`
1. Start NanoClaw in dev mode: `pnpm run dev`
2. From the **main group** (self-chat), send exactly: `/compact`
3. Verify:
- The agent acknowledges compaction (e.g., "Conversation compacted.")
@@ -104,8 +104,8 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
git clone <your-fork> /tmp/nanoclaw-test
cd /tmp/nanoclaw-test
claude # then run /add-compact
npm run build
npm test
pnpm run build
pnpm test
./container/build.sh
# Manual: send /compact from main group, verify compaction + continuation
# Manual: send @<assistant> /compact from non-main as non-admin, verify denial
+138
View File
@@ -0,0 +1,138 @@
---
name: add-dashboard
description: Add a monitoring dashboard to NanoClaw v2. Installs @nanoco/nanoclaw-dashboard and a pusher that sends periodic JSON snapshots.
---
# /add-dashboard — NanoClaw Dashboard
Adds a local monitoring dashboard showing agent groups, sessions, channels, users, token usage, context windows, message activity, and real-time logs.
## Architecture
```
NanoClaw (pusher) Dashboard (npm package)
┌──────────┐ POST JSON ┌──────────────┐
│ collects │ ────────────────→ │ /api/ingest │
│ DB data │ every 60s │ in-memory │
│ tails │ ────────────────→ │ /api/logs/ │
│ log file │ every 2s │ push │
└──────────┘ │ serves UI │
└──────────────┘
```
## Steps
### 1. Install the npm package
```bash
pnpm install @nanoco/nanoclaw-dashboard
```
### 2. Copy the pusher module
Copy the resource file into src:
```
.claude/skills/add-dashboard/resources/dashboard-pusher.ts → src/dashboard-pusher.ts
```
### 3. Add exports to src/db/index.ts
Add these two export blocks if not already present:
```typescript
// After the messaging-groups exports, add:
export {
getMessagingGroupsByAgentGroup,
} from './messaging-groups.js';
// Before the credentials exports, add:
export {
createDestination,
getDestinations,
getDestinationByName,
getDestinationByTarget,
hasDestination,
deleteDestination,
} from './agent-destinations.js';
```
### 4. Wire into src/index.ts
Add the `readEnvFile` import at the top if not already present:
```typescript
import { readEnvFile } from './env.js';
```
Add after step 7 (OneCLI approval handler), before the `log.info('NanoClaw v2 running')` line:
```typescript
// 8. Dashboard (optional)
const dashboardEnv = readEnvFile(['DASHBOARD_SECRET', 'DASHBOARD_PORT']);
const dashboardSecret = process.env.DASHBOARD_SECRET || dashboardEnv.DASHBOARD_SECRET;
const dashboardPort = parseInt(process.env.DASHBOARD_PORT || dashboardEnv.DASHBOARD_PORT || '3100', 10);
if (dashboardSecret) {
const { startDashboard } = await import('@nanoco/nanoclaw-dashboard');
const { startDashboardPusher } = await import('./dashboard-pusher.js');
startDashboard({ port: dashboardPort, secret: dashboardSecret });
startDashboardPusher({ port: dashboardPort, secret: dashboardSecret, intervalMs: 60000 });
} else {
log.info('Dashboard disabled (no DASHBOARD_SECRET)');
}
```
### 5. Add environment variables to .env
```
DASHBOARD_SECRET=<generate-a-random-secret>
DASHBOARD_PORT=3100
```
Generate the secret: `node -e "console.log('nc-' + require('crypto').randomBytes(16).toString('hex'))"`
### 6. Build and restart
```bash
pnpm run build
systemctl --user restart nanoclaw # Linux
# or: launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
```
### 7. Verify
```bash
curl -s http://localhost:3100/api/status
curl -s -H "Authorization: Bearer <secret>" http://localhost:3100/api/overview
```
Open `http://localhost:3100/dashboard` in a browser.
## Dashboard Pages
| Page | Shows |
|------|-------|
| Overview | Stats, token usage + cache hit rate, context windows, activity chart |
| Agent Groups | Sessions, wirings, destinations, members, admins |
| Sessions | Status, container state, context window usage bars |
| Channels | Live/offline status, messaging groups, sender policies |
| Messages | Per-session inbound/outbound messages |
| Users | Privilege hierarchy: owner > admin > member |
| Logs | Real-time log streaming with level filter |
## Troubleshooting
- **"No data yet"**: Wait 60s for first push, or check logs for push errors
- **401 errors**: Verify `DASHBOARD_SECRET` matches in `.env`
- **Port conflict**: Change `DASHBOARD_PORT` in `.env`
- **No logs**: Check `logs/nanoclaw.log` exists
## Removal
```bash
pnpm uninstall @nanoco/nanoclaw-dashboard
rm src/dashboard-pusher.ts
# Remove the dashboard block from src/index.ts
# Remove DASHBOARD_SECRET and DASHBOARD_PORT from .env
pnpm run build
```
@@ -0,0 +1,495 @@
/**
* Dashboard pusher — collects NanoClaw state and POSTs a JSON
* snapshot to the dashboard's /api/ingest endpoint every interval.
*/
import fs from 'fs';
import path from 'path';
import http from 'http';
import Database from 'better-sqlite3';
import { getAllAgentGroups, getAgentGroup } from './db/agent-groups.js';
import { getSessionsByAgentGroup } from './db/sessions.js';
import { getAllMessagingGroups, getMessagingGroupAgents } from './db/messaging-groups.js';
import { getDestinations } from './db/agent-destinations.js';
import { getMembers } from './db/agent-group-members.js';
import { getAllUsers, getUser } from './db/users.js';
import { getUserRoles, getAdminsOfAgentGroup } from './db/user-roles.js';
import { getUserDmsForUser } from './db/user-dms.js';
import { getActiveAdapters, getRegisteredChannelNames } from './channels/channel-registry.js';
import { DATA_DIR, ASSISTANT_NAME } from './config.js';
import { getDb } from './db/connection.js';
import { log } from './log.js';
interface PusherConfig {
port: number;
secret: string;
intervalMs?: number;
}
let timer: ReturnType<typeof setInterval> | null = null;
let logTimer: ReturnType<typeof setInterval> | null = null;
let logOffset = 0;
export function startDashboardPusher(config: PusherConfig): void {
const interval = config.intervalMs || 60000;
// Push immediately on start, then on interval
push(config).catch((err) => log.error('Dashboard push failed', { err }));
timer = setInterval(() => {
push(config).catch((err) => log.error('Dashboard push failed', { err }));
}, interval);
// Start log file tailing
startLogTail(config);
log.info('Dashboard pusher started', { intervalMs: interval });
}
export function stopDashboardPusher(): void {
if (timer) {
clearInterval(timer);
timer = null;
}
if (logTimer) {
clearInterval(logTimer);
logTimer = null;
}
}
/** Fire-and-forget POST to the dashboard. */
function postJson(config: PusherConfig, urlPath: string, data: unknown): void {
const body = JSON.stringify(data);
const req = http.request({
hostname: '127.0.0.1',
port: config.port,
path: urlPath,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
Authorization: `Bearer ${config.secret}`,
},
});
req.on('error', () => {});
req.write(body);
req.end();
}
const ANSI_RE = /\x1b\[[0-9;]*m/g;
function startLogTail(config: PusherConfig): void {
const logFile = path.resolve(process.cwd(), 'logs', 'nanoclaw.log');
if (!fs.existsSync(logFile)) return;
// Send last 200 lines as backfill
try {
const allLines = fs.readFileSync(logFile, 'utf-8').split('\n').filter((l) => l.trim());
logOffset = fs.statSync(logFile).size;
const tail = allLines.slice(-200).map((l) => l.replace(ANSI_RE, ''));
if (tail.length > 0) postJson(config, '/api/logs/push', { lines: tail });
} catch { return; }
// Poll every 2s for new lines
logTimer = setInterval(() => {
try {
const stat = fs.statSync(logFile);
if (stat.size <= logOffset) { logOffset = stat.size; return; }
const buf = Buffer.alloc(stat.size - logOffset);
const fd = fs.openSync(logFile, 'r');
fs.readSync(fd, buf, 0, buf.length, logOffset);
fs.closeSync(fd);
logOffset = stat.size;
const lines = buf.toString().split('\n').filter((l) => l.trim()).map((l) => l.replace(ANSI_RE, ''));
if (lines.length > 0) postJson(config, '/api/logs/push', { lines });
} catch { /* ignore */ }
}, 2000);
}
async function push(config: PusherConfig): Promise<void> {
const snapshot = collectSnapshot();
postJson(config, '/api/ingest', snapshot);
log.debug('Dashboard snapshot pushed');
}
function collectSnapshot(): Record<string, unknown> {
return {
timestamp: new Date().toISOString(),
assistant_name: ASSISTANT_NAME,
uptime: Math.floor(process.uptime()),
agent_groups: collectAgentGroups(),
sessions: collectSessions(),
channels: collectChannels(),
users: collectUsers(),
tokens: collectTokens(),
context_windows: collectContextWindows(),
activity: collectActivity(),
messages: collectMessages(),
};
}
function collectAgentGroups() {
return getAllAgentGroups().map((g) => {
const sessions = getSessionsByAgentGroup(g.id);
const running = sessions.filter((s) => s.container_status === 'running' || s.container_status === 'idle');
const destinations = getDestinations(g.id);
const members = getMembers(g.id).map((m) => {
const user = getUser(m.user_id);
return { ...m, display_name: user?.display_name ?? null };
});
const admins = getAdminsOfAgentGroup(g.id).map((a) => {
const user = getUser(a.user_id);
return { ...a, display_name: user?.display_name ?? null };
});
// Wirings
const db = getDb();
const wirings = db
.prepare(
`SELECT mga.*, mg.channel_type, mg.platform_id, mg.name as mg_name, mg.is_group, mg.unknown_sender_policy
FROM messaging_group_agents mga
JOIN messaging_groups mg ON mg.id = mga.messaging_group_id
WHERE mga.agent_group_id = ?`,
)
.all(g.id) as Array<Record<string, unknown>>;
return {
id: g.id,
name: g.name,
folder: g.folder,
agent_provider: g.agent_provider,
container_config: g.container_config ? JSON.parse(g.container_config) : null,
sessionCount: sessions.length,
runningSessions: running.length,
wirings,
destinations,
members,
admins,
created_at: g.created_at,
};
});
}
function collectSessions() {
const db = getDb();
return db
.prepare(
`SELECT s.*, ag.name as agent_group_name, ag.folder as agent_group_folder,
mg.channel_type, mg.platform_id, mg.name as messaging_group_name
FROM sessions s
LEFT JOIN agent_groups ag ON ag.id = s.agent_group_id
LEFT JOIN messaging_groups mg ON mg.id = s.messaging_group_id
ORDER BY s.last_active DESC NULLS LAST`,
)
.all() as Array<Record<string, unknown>>;
}
function collectChannels() {
const messagingGroups = getAllMessagingGroups();
const liveAdapters = getActiveAdapters().map((a) => a.channelType);
const registeredChannels = getRegisteredChannelNames();
const byType: Record<string, { channelType: string; isLive: boolean; isRegistered: boolean; groups: unknown[] }> = {};
for (const mg of messagingGroups) {
if (!byType[mg.channel_type]) {
byType[mg.channel_type] = {
channelType: mg.channel_type,
isLive: liveAdapters.includes(mg.channel_type),
isRegistered: registeredChannels.includes(mg.channel_type),
groups: [],
};
}
const agents = getMessagingGroupAgents(mg.id).map((a) => {
const group = getAgentGroup(a.agent_group_id);
return { agent_group_id: a.agent_group_id, agent_group_name: group?.name ?? null, priority: a.priority };
});
byType[mg.channel_type].groups.push({
messagingGroup: {
id: mg.id,
platform_id: mg.platform_id,
name: mg.name,
is_group: mg.is_group,
unknown_sender_policy: (mg as unknown as Record<string, unknown>).unknown_sender_policy ?? 'strict',
},
agents,
});
}
// Include live adapters with no messaging groups
for (const ct of liveAdapters) {
if (!byType[ct]) {
byType[ct] = { channelType: ct, isLive: true, isRegistered: true, groups: [] };
}
}
return Object.values(byType).sort((a, b) => a.channelType.localeCompare(b.channelType));
}
function collectUsers() {
return getAllUsers().map((u) => {
const roles = getUserRoles(u.id);
const dms = getUserDmsForUser(u.id);
const db = getDb();
const memberships = db
.prepare(
`SELECT agm.agent_group_id, ag.name as agent_group_name
FROM agent_group_members agm
JOIN agent_groups ag ON ag.id = agm.agent_group_id
WHERE agm.user_id = ?`,
)
.all(u.id) as Array<Record<string, unknown>>;
let privilege = 'none';
if (roles.some((r) => r.role === 'owner')) privilege = 'owner';
else if (roles.some((r) => r.role === 'admin' && !r.agent_group_id)) privilege = 'global_admin';
else if (roles.some((r) => r.role === 'admin')) privilege = 'admin';
else if (memberships.length > 0) privilege = 'member';
return {
id: u.id,
kind: u.kind,
display_name: u.display_name,
privilege,
roles,
memberships,
dmChannels: dms.map((d) => ({ channel_type: d.channel_type })),
created_at: u.created_at,
};
});
}
function collectTokens() {
const sessionsDir = path.join(DATA_DIR, 'v2-sessions');
const allEntries: Array<{ model: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreationTokens: number; agentGroupId: string }> = [];
const agentGroups = getAllAgentGroups();
const nameMap = new Map(agentGroups.map((g) => [g.id, g.name]));
if (fs.existsSync(sessionsDir)) {
for (const agDir of fs.readdirSync(sessionsDir).filter((d) => d.startsWith('ag-'))) {
const entries = scanJsonlTokens(path.join(sessionsDir, agDir));
allEntries.push(...entries.map((e) => ({ ...e, agentGroupId: agDir })));
}
}
const byModel: Record<string, { requests: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreationTokens: number }> = {};
const byGroup: Record<string, { requests: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreationTokens: number; name: string }> = {};
const totals = { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 };
for (const e of allEntries) {
if (!byModel[e.model]) byModel[e.model] = { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 };
byModel[e.model].requests++;
byModel[e.model].inputTokens += e.inputTokens;
byModel[e.model].outputTokens += e.outputTokens;
byModel[e.model].cacheReadTokens += e.cacheReadTokens;
byModel[e.model].cacheCreationTokens += e.cacheCreationTokens;
if (!byGroup[e.agentGroupId]) byGroup[e.agentGroupId] = { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, name: nameMap.get(e.agentGroupId) || e.agentGroupId };
byGroup[e.agentGroupId].requests++;
byGroup[e.agentGroupId].inputTokens += e.inputTokens;
byGroup[e.agentGroupId].outputTokens += e.outputTokens;
byGroup[e.agentGroupId].cacheReadTokens += e.cacheReadTokens;
byGroup[e.agentGroupId].cacheCreationTokens += e.cacheCreationTokens;
totals.requests++;
totals.inputTokens += e.inputTokens;
totals.outputTokens += e.outputTokens;
totals.cacheReadTokens += e.cacheReadTokens;
totals.cacheCreationTokens += e.cacheCreationTokens;
}
return { totals, byModel, byGroup };
}
function scanJsonlTokens(agentDir: string) {
const claudeDir = path.join(agentDir, '.claude-shared', 'projects');
if (!fs.existsSync(claudeDir)) return [];
const entries: Array<{ model: string; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreationTokens: number }> = [];
const walk = (dir: string): void => {
try {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (entry.name.endsWith('.jsonl')) {
try {
for (const line of fs.readFileSync(full, 'utf-8').split('\n')) {
if (!line.trim()) continue;
try {
const r = JSON.parse(line);
if (r.type === 'assistant' && r.message?.usage) {
const u = r.message.usage;
entries.push({
model: r.message.model || 'unknown',
inputTokens: u.input_tokens || 0,
outputTokens: u.output_tokens || 0,
cacheReadTokens: u.cache_read_input_tokens || 0,
cacheCreationTokens: u.cache_creation_input_tokens || 0,
});
}
} catch { /* skip line */ }
}
} catch { /* skip file */ }
}
}
} catch { /* skip dir */ }
};
walk(claudeDir);
return entries;
}
function collectContextWindows() {
const sessionsDir = path.join(DATA_DIR, 'v2-sessions');
if (!fs.existsSync(sessionsDir)) return [];
const results: unknown[] = [];
const agentGroups = getAllAgentGroups();
const nameMap = new Map(agentGroups.map((g) => [g.id, g.name]));
for (const agDir of fs.readdirSync(sessionsDir).filter((d) => d.startsWith('ag-'))) {
const claudeDir = path.join(sessionsDir, agDir, '.claude-shared', 'projects');
if (!fs.existsSync(claudeDir)) continue;
// Find most recent JSONL
const jsonlFiles: string[] = [];
const walk = (dir: string): void => {
try {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (entry.name.endsWith('.jsonl')) jsonlFiles.push(full);
}
} catch { /* skip */ }
};
walk(claudeDir);
if (jsonlFiles.length === 0) continue;
jsonlFiles.sort((a, b) => {
try { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; } catch { return 0; }
});
// Read last assistant turn from newest file
const content = fs.readFileSync(jsonlFiles[0], 'utf-8');
const lines = content.split('\n');
for (let i = lines.length - 1; i >= 0; i--) {
if (!lines[i].trim()) continue;
try {
const r = JSON.parse(lines[i]);
if (r.type === 'assistant' && r.message?.usage) {
const u = r.message.usage;
const model = r.message.model || 'unknown';
const ctx = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
const max = 200000;
results.push({
agentGroupId: agDir,
agentGroupName: nameMap.get(agDir),
sessionId: path.basename(jsonlFiles[0], '.jsonl'),
model,
contextTokens: ctx,
outputTokens: u.output_tokens || 0,
cacheReadTokens: u.cache_read_input_tokens || 0,
cacheCreationTokens: u.cache_creation_input_tokens || 0,
maxContext: max,
usagePercent: max > 0 ? Math.round((ctx / max) * 100) : 0,
timestamp: r.timestamp || '',
});
break;
}
} catch { /* skip */ }
}
}
return results;
}
function collectActivity() {
const now = Date.now();
const buckets: Record<string, { inbound: number; outbound: number }> = {};
for (let i = 0; i < 24; i++) {
const key = new Date(now - i * 3600000).toISOString().slice(0, 13);
buckets[key] = { inbound: 0, outbound: 0 };
}
const sessionsDir = path.join(DATA_DIR, 'v2-sessions');
if (!fs.existsSync(sessionsDir)) return toBucketArray(buckets);
const cutoff = new Date(now - 86400000).toISOString();
try {
for (const agDir of fs.readdirSync(sessionsDir).filter((d) => d.startsWith('ag-'))) {
const agPath = path.join(sessionsDir, agDir);
for (const sessDir of fs.readdirSync(agPath).filter((d) => d.startsWith('sess-'))) {
for (const [dbName, direction] of [['outbound.db', 'outbound'], ['inbound.db', 'inbound']] as const) {
const dbPath = path.join(agPath, sessDir, dbName);
if (!fs.existsSync(dbPath)) continue;
try {
const db = new Database(dbPath, { readonly: true });
const table = direction === 'outbound' ? 'messages_out' : 'messages_in';
const rows = db.prepare(`SELECT timestamp FROM ${table} WHERE timestamp > ?`).all(cutoff) as { timestamp: string }[];
for (const row of rows) {
const key = row.timestamp.slice(0, 13);
if (buckets[key]) buckets[key][direction]++;
}
db.close();
} catch { /* skip */ }
}
}
}
} catch { /* skip */ }
return toBucketArray(buckets);
}
function toBucketArray(buckets: Record<string, { inbound: number; outbound: number }>) {
return Object.entries(buckets)
.map(([hour, counts]) => ({ hour, ...counts }))
.sort((a, b) => a.hour.localeCompare(b.hour));
}
function collectMessages() {
const sessionsDir = path.join(DATA_DIR, 'v2-sessions');
if (!fs.existsSync(sessionsDir)) return [];
const results: Array<{ agentGroupId: string; sessionId: string; inbound: unknown[]; outbound: unknown[] }> = [];
const limit = 50;
try {
for (const agDir of fs.readdirSync(sessionsDir).filter((d) => d.startsWith('ag-'))) {
const agPath = path.join(sessionsDir, agDir);
for (const sessDir of fs.readdirSync(agPath).filter((d) => d.startsWith('sess-'))) {
const inbound: unknown[] = [];
const outbound: unknown[] = [];
const inDbPath = path.join(agPath, sessDir, 'inbound.db');
if (fs.existsSync(inDbPath)) {
try {
const db = new Database(inDbPath, { readonly: true });
const rows = db.prepare('SELECT * FROM messages_in ORDER BY seq DESC LIMIT ?').all(limit);
inbound.push(...(rows as unknown[]).reverse());
db.close();
} catch { /* skip */ }
}
const outDbPath = path.join(agPath, sessDir, 'outbound.db');
if (fs.existsSync(outDbPath)) {
try {
const db = new Database(outDbPath, { readonly: true });
const rows = db.prepare('SELECT * FROM messages_out ORDER BY seq DESC LIMIT ?').all(limit);
outbound.push(...(rows as unknown[]).reverse());
db.close();
} catch { /* skip */ }
}
if (inbound.length > 0 || outbound.length > 0) {
results.push({ agentGroupId: agDir, sessionId: sessDir, inbound, outbound });
}
}
}
} catch { /* skip */ }
return results;
}
+7
View File
@@ -0,0 +1,7 @@
# Remove Discord
1. Comment out `import './discord.js'` in `src/channels/index.ts`
2. Remove `DISCORD_BOT_TOKEN` from `.env`
3. Rebuild and restart
No package to uninstall — Discord is built in.
+74
View File
@@ -0,0 +1,74 @@
---
name: add-discord-v2
description: Add Discord bot channel integration to NanoClaw v2 via Chat SDK.
---
# Add Discord Channel
Adds Discord bot support to NanoClaw v2. Discord is built in — no adapter package to install.
## Pre-flight
Check if `src/channels/discord.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
Discord support is bundled with NanoClaw — there is no separate package to install.
### Enable the channel
Uncomment the Discord import in `src/channels/index.ts`:
```typescript
import './discord.js';
```
### Build
```bash
pnpm run build
```
## Credentials
### Create Discord Bot
1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
2. Click **New Application** and give it a name (e.g., "NanoClaw Assistant")
3. From the **General Information** tab, copy the **Application ID** and **Public Key**
4. Go to the **Bot** tab and click **Add Bot** if needed
5. Copy the Bot Token (click **Reset Token** if you need a new one — you can only see it once)
6. Under **Privileged Gateway Intents**, enable **Message Content Intent**
7. Go to **OAuth2** > **URL Generator**:
- Scopes: select `bot`
- Bot Permissions: select `Send Messages`, `Read Message History`, `Add Reactions`, `Attach Files`, `Use Slash Commands`
8. Copy the generated URL and open it in your browser to invite the bot to your server
### Configure environment
All three values are required — the adapter will fail to start without `DISCORD_PUBLIC_KEY` and `DISCORD_APPLICATION_ID`.
Add to `.env`:
```bash
DISCORD_BOT_TOKEN=your-bot-token
DISCORD_APPLICATION_ID=your-application-id
DISCORD_PUBLIC_KEY=your-public-key
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `discord`
- **terminology**: Discord has "servers" (also called "guilds") containing "channels." Text channels start with #. The bot can also receive direct messages.
- **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.
- **supports-threads**: yes
- **typical-use**: Interactive chat — server channels or direct messages
- **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.
+3
View File
@@ -0,0 +1,3 @@
# Verify Discord
Send a message in a channel where the bot has access, or DM the bot directly. The bot should respond within a few seconds.
+9 -9
View File
@@ -40,8 +40,8 @@ git remote add discord https://github.com/qwibitai/nanoclaw-discord.git
```bash
git fetch discord main
git merge discord/main || {
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
}
```
@@ -58,9 +58,9 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
### Validate code changes
```bash
npm install
npm run build
npx vitest run src/channels/discord.test.ts
pnpm install
pnpm run build
pnpm exec vitest run src/channels/discord.test.ts
```
All tests must pass (including the new Discord tests) and build must be clean before proceeding.
@@ -108,7 +108,7 @@ The container reads environment from `data/env/env`, not `.env` directly.
### Build and restart
```bash
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
```
@@ -130,18 +130,18 @@ Wait for the user to provide the channel ID (format: `dc:1234567890123456`).
### Register the channel
The channel ID, name, and folder name are needed. Use `npx tsx setup/index.ts --step register` with the appropriate flags.
The channel ID, name, and folder name are needed. Use `pnpm exec tsx setup/index.ts --step register` with the appropriate flags.
For a main channel (responds to all messages):
```bash
npx tsx setup/index.ts --step register -- --jid "dc:<channel-id>" --name "<server-name> #<channel-name>" --folder "discord_main" --trigger "@${ASSISTANT_NAME}" --channel discord --no-trigger-required --is-main
pnpm exec tsx setup/index.ts --step register -- --jid "dc:<channel-id>" --name "<server-name> #<channel-name>" --folder "discord_main" --trigger "@${ASSISTANT_NAME}" --channel discord --no-trigger-required --is-main
```
For additional channels (trigger-only):
```bash
npx tsx setup/index.ts --step register -- --jid "dc:<channel-id>" --name "<server-name> #<channel-name>" --folder "discord_<channel-name>" --trigger "@${ASSISTANT_NAME}" --channel discord
pnpm exec tsx setup/index.ts --step register -- --jid "dc:<channel-id>" --name "<server-name> #<channel-name>" --folder "discord_<channel-name>" --trigger "@${ASSISTANT_NAME}" --channel discord
```
## Phase 5: Verify
+10 -10
View File
@@ -51,12 +51,12 @@ git fetch upstream skill/emacs
git merge upstream/skill/emacs
```
If there are merge conflicts on `package-lock.json`, resolve them by accepting the incoming
If there are merge conflicts on `pnpm-lock.yaml`, resolve them by accepting the incoming
version and continuing:
```bash
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
```
@@ -73,8 +73,8 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
### Validate code changes
```bash
npm run build
npx vitest run src/channels/emacs.test.ts
pnpm run build
pnpm exec vitest run src/channels/emacs.test.ts
```
Build must be clean and tests must pass before proceeding.
@@ -158,7 +158,7 @@ If `EMACS_CHANNEL_PORT` was changed from the default, also add:
### Restart NanoClaw
```bash
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
```
@@ -246,18 +246,18 @@ If NanoClaw is cloned elsewhere, update the `load`/`load-file` path in your Emac
## After Setup
If running `npm run dev` while the service is active:
If running `pnpm run dev` while the service is active:
```bash
# macOS:
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
npm run dev
pnpm run dev
# When done testing:
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
# Linux:
# systemctl --user stop nanoclaw
# npm run dev
# pnpm run dev
# systemctl --user start nanoclaw
```
@@ -286,4 +286,4 @@ To remove the Emacs channel:
3. Remove the NanoClaw block from your Emacs config file
4. Remove Emacs registration from SQLite: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid = 'emacs:default'"`
5. Remove `EMACS_CHANNEL_PORT` and `EMACS_AUTH_TOKEN` from `.env` if set
6. Rebuild: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux)
6. Rebuild: `pnpm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `pnpm run build && systemctl --user restart nanoclaw` (Linux)
+6
View File
@@ -0,0 +1,6 @@
# Remove Google Chat Channel
1. Comment out `import './gchat.js'` in `src/channels/index.ts`
2. Remove `GCHAT_CREDENTIALS` from `.env`
3. `pnpm uninstall @chat-adapter/gchat`
4. Rebuild and restart
+66
View File
@@ -0,0 +1,66 @@
---
name: add-gchat-v2
description: Add Google Chat channel integration to NanoClaw v2 via Chat SDK.
---
# Add Google Chat Channel
Adds Google Chat support to NanoClaw v2 using the Chat SDK bridge.
## Pre-flight
Check if `src/channels/gchat.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
```bash
pnpm install @chat-adapter/gchat
```
Uncomment the Google Chat import in `src/channels/index.ts`:
```typescript
import './gchat.js';
```
```bash
pnpm run build
```
## Credentials
> 1. Go to [Google Cloud Console](https://console.cloud.google.com)
> 2. Create or select a project
> 3. Enable the **Google Chat API**
> 4. Go to **Google Chat API** > **Configuration**:
> - App name and description
> - Connection settings: select **HTTP endpoint URL** and set to `https://your-domain/webhook/gchat`
> 5. Create a **Service Account**:
> - Go to **IAM & Admin** > **Service Accounts** > **Create Service Account**
> - Grant the Chat Bot role
> - Create a JSON key and download it
### Configure environment
Add the service account JSON as a single-line string to `.env`:
```bash
GCHAT_CREDENTIALS={"type":"service_account","project_id":"...","private_key":"...","client_email":"..."}
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `gchat`
- **terminology**: Google Chat has "spaces." A space can be a group conversation or a direct message with the bot.
- **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.
- **supports-threads**: yes
- **typical-use**: Interactive chat — team spaces or direct messages
- **default-isolation**: Same agent group for spaces where you're the primary user. Separate agent group for spaces with different teams or sensitive contexts.
+3
View File
@@ -0,0 +1,3 @@
# Verify Google Chat Channel
Add the bot to a Google Chat space, then send a message or @mention the bot. The bot should respond within a few seconds.
+6
View File
@@ -0,0 +1,6 @@
# Remove GitHub Channel
1. Comment out `import './github.js'` in `src/channels/index.ts`
2. Remove `GITHUB_TOKEN` and `GITHUB_WEBHOOK_SECRET` from `.env`
3. `pnpm uninstall @chat-adapter/github`
4. Rebuild and restart
+68
View File
@@ -0,0 +1,68 @@
---
name: add-github-v2
description: Add GitHub channel integration to NanoClaw v2 via Chat SDK. PR and issue comment threads as conversations.
---
# Add GitHub Channel
Adds GitHub support to NanoClaw v2 using the Chat SDK bridge. The agent participates in PR and issue comment threads.
## Pre-flight
Check if `src/channels/github.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
```bash
pnpm install @chat-adapter/github
```
Uncomment the GitHub import in `src/channels/index.ts`:
```typescript
import './github.js';
```
```bash
pnpm run build
```
## Credentials
> 1. Go to [GitHub Settings > Developer Settings > Personal Access Tokens](https://github.com/settings/tokens)
> 2. Create a **Fine-grained token** with:
> - Repository access: select the repos you want the bot to monitor
> - Permissions: **Pull requests** (Read & Write), **Issues** (Read & Write)
> 3. Copy the token
> 4. Set up a webhook on your repo(s):
> - Go to **Settings** > **Webhooks** > **Add webhook**
> - Payload URL: `https://your-domain/webhook/github`
> - Content type: `application/json`
> - Secret: generate a random string
> - Events: select **Issue comments**, **Pull request review comments**
### Configure environment
Add to `.env`:
```bash
GITHUB_TOKEN=github_pat_...
GITHUB_WEBHOOK_SECRET=your-webhook-secret
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `github`
- **terminology**: GitHub has "repositories" containing "pull requests" and "issues." Each PR or issue comment thread is a separate conversation.
- **how-to-find-id**: The platform ID is `owner/repo` (e.g. `acme/backend`). Each PR/issue becomes its own thread automatically.
- **supports-threads**: yes (PR and issue comment threads are native conversations)
- **typical-use**: Webhook/notification — the agent receives PR and issue events and responds in comment threads
- **default-isolation**: Typically shares a session with a chat channel (e.g. Slack) so the agent can summarize PRs and respond to reviews in the same context. Use a separate agent group if the repo contains sensitive code that other channels shouldn't access.
+3
View File
@@ -0,0 +1,3 @@
# Verify GitHub Channel
@mention the bot in a PR comment or issue comment. The bot should respond within a few seconds.
+31 -15
View File
@@ -41,8 +41,8 @@ git remote add gmail https://github.com/qwibitai/nanoclaw-gmail.git
```bash
git fetch gmail main
git merge gmail/main || {
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
}
```
@@ -70,9 +70,9 @@ When you receive an email notification (messages starting with `[Email from ...`
### Validate code changes
```bash
npm install
npm run build
npx vitest run src/channels/gmail.test.ts
pnpm install
pnpm run build
pnpm exec vitest run src/channels/gmail.test.ts
```
All tests must pass (including the new Gmail tests) and build must be clean before proceeding.
@@ -85,11 +85,27 @@ All tests must pass (including the new Gmail tests) and build must be clean befo
ls -la ~/.gmail-mcp/ 2>/dev/null || echo "No Gmail config found"
```
If `credentials.json` already exists, skip to "Build and restart" below.
If `credentials.json` already exists with real tokens (not `onecli-managed` values), skip to "Build and restart" below.
### GCP Project Setup
Tell the user:
Check if OneCLI is configured:
```bash
grep -q 'ONECLI_URL=.' .env 2>/dev/null && echo "onecli" || echo "manual"
```
**If OneCLI:** Tell the user to open `${ONECLI_URL}/connections?connect=gmail` to set up their Gmail connection. The dashboard walks them through creating a Google Cloud OAuth app and authorizing it. Ask them to let you know when done.
Once the user confirms, run:
```bash
onecli apps get --provider gmail
```
Check that `config.hasCredentials` is `true` or `connection` is not null. The response `hint` field has instructions and a docs URL for what stub credential files to create under `~/.gmail-mcp/`. Follow the hint — never overwrite existing files that don't contain `onecli-managed` values.
**If manual:** Tell the user:
> I need you to set up Google Cloud OAuth credentials:
>
@@ -120,10 +136,10 @@ Tell the user:
Run the authorization:
```bash
npx -y @gongrzhe/server-gmail-autoauth-mcp auth
pnpm dlx @gongrzhe/server-gmail-autoauth-mcp auth
```
If that fails (some versions don't have an auth subcommand), try `timeout 60 npx -y @gongrzhe/server-gmail-autoauth-mcp || true`. Verify with `ls ~/.gmail-mcp/credentials.json`.
If that fails (some versions don't have an auth subcommand), try `timeout 60 pnpm dlx @gongrzhe/server-gmail-autoauth-mcp || true`. Verify with `ls ~/.gmail-mcp/credentials.json`.
### Build and restart
@@ -142,7 +158,7 @@ cd container && ./build.sh
Then compile and restart:
```bash
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
```
@@ -176,7 +192,7 @@ tail -f logs/nanoclaw.log
Test directly:
```bash
npx -y @gongrzhe/server-gmail-autoauth-mcp
pnpm dlx @gongrzhe/server-gmail-autoauth-mcp
```
### OAuth token expired
@@ -185,7 +201,7 @@ Re-authorize:
```bash
rm ~/.gmail-mcp/credentials.json
npx -y @gongrzhe/server-gmail-autoauth-mcp
pnpm dlx @gongrzhe/server-gmail-autoauth-mcp
```
### Container can't access Gmail
@@ -206,7 +222,7 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp
2. Remove `gmail` MCP server and `mcp__gmail__*` from `container/agent-runner/src/index.ts`
3. Rebuild and restart
4. Clear stale agent-runner copies: `rm -r data/sessions/*/agent-runner-src 2>/dev/null || true`
5. Rebuild: `cd container && ./build.sh && cd .. && npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
5. Rebuild: `cd container && ./build.sh && cd .. && pnpm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
### Channel mode
@@ -214,7 +230,7 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp
2. Remove `import './gmail.js'` from `src/channels/index.ts`
3. Remove `~/.gmail-mcp` mount from `src/container-runner.ts`
4. Remove `gmail` MCP server and `mcp__gmail__*` from `container/agent-runner/src/index.ts`
5. Uninstall: `npm uninstall googleapis`
5. Uninstall: `pnpm uninstall googleapis`
6. Rebuild and restart
7. Clear stale agent-runner copies: `rm -r data/sessions/*/agent-runner-src 2>/dev/null || true`
8. Rebuild: `cd container && ./build.sh && cd .. && npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
8. Rebuild: `cd container && ./build.sh && cd .. && pnpm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
+6 -6
View File
@@ -33,8 +33,8 @@ git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
```bash
git fetch whatsapp skill/image-vision
git merge whatsapp/skill/image-vision || {
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
}
```
@@ -52,9 +52,9 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
### Validate code changes
```bash
npm install
npm run build
npx vitest run src/image.test.ts
pnpm install
pnpm run build
pnpm exec vitest run src/image.test.ts
```
All tests must pass and build must be clean before proceeding.
@@ -90,5 +90,5 @@ All tests must pass and build must be clean before proceeding.
## Troubleshooting
- **"Image - download failed"**: Check WhatsApp connection stability. The download may timeout on slow connections.
- **"Image - processing failed"**: Sharp may not be installed correctly. Run `npm ls sharp` to verify.
- **"Image - processing failed"**: Sharp may not be installed correctly. Run `pnpm ls sharp` to verify.
- **Agent doesn't mention image content**: Check container logs for "Loaded image" messages. If missing, ensure agent-runner source was synced to group caches.
+6
View File
@@ -0,0 +1,6 @@
# Remove iMessage Channel
1. Comment out `import './imessage.js'` in `src/channels/index.ts`
2. Remove iMessage env vars (`IMESSAGE_ENABLED`, `IMESSAGE_LOCAL`, `IMESSAGE_SERVER_URL`, `IMESSAGE_API_KEY`) from `.env`
3. `pnpm uninstall chat-adapter-imessage`
4. Rebuild and restart
+87
View File
@@ -0,0 +1,87 @@
---
name: add-imessage-v2
description: Add iMessage channel integration to NanoClaw v2 via Chat SDK. Local (macOS) or remote (Photon API) mode.
---
# Add iMessage Channel
Adds iMessage support to NanoClaw v2 using the Chat SDK bridge. Two modes: local (macOS with Full Disk Access) or remote (Photon API).
## Pre-flight
Check if `src/channels/imessage.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
```bash
pnpm install chat-adapter-imessage
```
Uncomment the iMessage import in `src/channels/index.ts`:
```typescript
import './imessage.js';
```
```bash
pnpm run build
```
## Credentials
### Local Mode (macOS)
Requirements: macOS with Full Disk Access granted to the Node.js binary.
The Node binary path is buried deep (e.g. `~/.nvm/versions/node/v22.x.x/bin/node`). To make it easy, open the folder in Finder so the user can drag the file into System Settings:
```bash
open "$(dirname "$(which node)")"
```
Then tell the user:
1. Open **System Settings** > **Privacy & Security** > **Full Disk Access**
2. Click **+**, then drag the `node` file from the Finder window that just opened
3. Toggle it on
Stop and wait for the user to confirm before continuing.
### Remote Mode (Photon API)
1. Set up a [Photon](https://photon.im) account
2. Get your server URL and API key
### Configure environment
**Local mode** -- add to `.env`:
```bash
IMESSAGE_ENABLED=true
IMESSAGE_LOCAL=true
```
**Remote mode** -- add to `.env`:
```bash
IMESSAGE_LOCAL=false
IMESSAGE_SERVER_URL=https://your-photon-server.com
IMESSAGE_API_KEY=your-api-key
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `imessage`
- **terminology**: iMessage has "conversations." Each conversation is with a contact identified by phone number or email address. Group chats are also supported.
- **how-to-find-id**: The platform ID is the contact's phone number (e.g. `+15551234567`) or email address. For group chats, the ID is assigned by iMessage internally.
- **supports-threads**: no
- **typical-use**: Interactive 1:1 chat — personal messaging
- **default-isolation**: Same agent group if you're the only person messaging the bot across iMessage and other channels. Separate agent group if different contacts should have information isolation.
+3
View File
@@ -0,0 +1,3 @@
# Verify iMessage Channel
Send an iMessage to the account running NanoClaw. The bot should respond within a few seconds.
+34 -4
View File
@@ -19,7 +19,7 @@ AskUserQuestion: "Which group should have the wiki?"
2. **Dedicated group** — create a new group just for the wiki
3. **Other** — pick an existing group
If dedicated: ask which channel and chat, then register with `npx tsx setup/index.ts --step register`.
If dedicated: ask which channel and chat, then register with `pnpm exec tsx setup/index.ts --step register`.
## Step 3: Design collaboratively
@@ -41,7 +41,12 @@ Create a `container/skills/wiki/SKILL.md` tailored to this user's wiki. This is
### 3c. Group CLAUDE.md
Add a wiki section to the group's CLAUDE.md that activates the wiki behavior and points to the container skill. It should concisely explain the system and have an index of the key files and folders.
Edit the group's CLAUDE.md to add a wiki section. This is critical — it's what turns the agent into a wiki maintainer. It should:
- Explain the wiki system concisely: what it is, the three layers (sources, wiki, schema), the three operations (ingest, query, lint)
- Index the key files and folders (`wiki/`, `sources/`, `wiki/index.md`, `wiki/log.md`)
- Point to the container skill for detailed workflow
- **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.
## Step 4: Source handling capabilities
@@ -66,12 +71,37 @@ AskUserQuestion: "Want periodic wiki health checks?"
2. **Monthly**
3. **Skip** — lint manually
If yes, schedule via `mcp__nanoclaw__schedule_task` with a prompt based on the pattern's Lint operation.
If yes, create a NanoClaw scheduled task that runs in the wiki group. This is NOT a Claude Code cron job — it's a NanoClaw group task that runs in the agent container. Insert it into the SQLite database:
```bash
pnpm exec tsx -e "
const Database = require('better-sqlite3');
const { CronExpressionParser } = require('cron-parser');
const db = new Database('store/messages.db');
const interval = CronExpressionParser.parse('<cron-expr>', { tz: process.env.TZ || 'UTC' });
const nextRun = interval.next().toISOString();
db.prepare('INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, schedule_type, schedule_value, context_mode, next_run, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(
'wiki-lint',
'<group_folder>',
'<chat_jid>',
'Run a wiki lint pass per the wiki container skill. Check for contradictions, orphan pages, stale content, missing cross-references, and gaps. Report findings and offer to fix issues.',
'cron',
'<cron-expr>',
'group',
nextRun,
'active',
new Date().toISOString()
);
db.close();
"
```
Use the group's `folder` and `chat_jid` from the registered groups table. Cron expressions: `0 10 * * 0` (weekly Sunday 10am) or `0 10 1 * *` (monthly 1st at 10am).
## Step 6: Build and restart
```bash
npm run build
pnpm run build
./container/build.sh
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
+6
View File
@@ -0,0 +1,6 @@
# Remove Linear Channel
1. Comment out `import './linear.js'` in `src/channels/index.ts`
2. Remove `LINEAR_API_KEY` and `LINEAR_WEBHOOK_SECRET` from `.env`
3. `pnpm uninstall @chat-adapter/linear`
4. Rebuild and restart
+65
View File
@@ -0,0 +1,65 @@
---
name: add-linear-v2
description: Add Linear channel integration to NanoClaw v2 via Chat SDK. Issue comment threads as conversations.
---
# Add Linear Channel
Adds Linear support to NanoClaw v2 using the Chat SDK bridge. The agent participates in issue comment threads.
## Pre-flight
Check if `src/channels/linear.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
```bash
pnpm install @chat-adapter/linear
```
Uncomment the Linear import in `src/channels/index.ts`:
```typescript
import './linear.js';
```
```bash
pnpm run build
```
## Credentials
> 1. Go to [Linear Settings > API Keys](https://linear.app/settings/account/security/api-keys/new)
> 2. Create a **Personal API Key** (or use an OAuth application for team-wide access)
> 3. Copy the API key
> 4. Set up a webhook:
> - Go to **Settings** > **API** > **Webhooks** > **New webhook**
> - URL: `https://your-domain/webhook/linear`
> - Select events: **Comment** (created, updated)
> - Copy the signing secret
### Configure environment
Add to `.env`:
```bash
LINEAR_API_KEY=lin_api_...
LINEAR_WEBHOOK_SECRET=your-webhook-secret
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `linear`
- **terminology**: Linear has "teams" containing "issues." Each issue's comment thread is a separate conversation.
- **how-to-find-id**: The platform ID is your team key (e.g. `ENG`). Find it in Linear under Settings > Teams. Each issue becomes its own thread automatically.
- **supports-threads**: yes (issue comment threads are native conversations)
- **typical-use**: Webhook/notification — the agent receives issue comment events and responds in threads
- **default-isolation**: Typically shares a session with a chat channel (e.g. Slack) so the agent can discuss issues in the same context as team chat. Use a separate agent group if the Linear team tracks sensitive work.
+3
View File
@@ -0,0 +1,3 @@
# Verify Linear Channel
@mention the bot in a Linear issue comment. The bot should respond within a few seconds.
+6
View File
@@ -0,0 +1,6 @@
# Remove Matrix Channel
1. Comment out `import './matrix.js'` in `src/channels/index.ts`
2. Remove `MATRIX_BASE_URL`, `MATRIX_ACCESS_TOKEN`, `MATRIX_USER_ID`, `MATRIX_BOT_USERNAME` from `.env`
3. `pnpm uninstall @beeper/chat-adapter-matrix`
4. Rebuild and restart
+65
View File
@@ -0,0 +1,65 @@
---
name: add-matrix-v2
description: Add Matrix channel integration to NanoClaw v2 via Chat SDK. Works with any Matrix homeserver.
---
# Add Matrix Channel
Adds Matrix support to NanoClaw v2 using the Chat SDK bridge.
## Pre-flight
Check if `src/channels/matrix.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
```bash
pnpm install @beeper/chat-adapter-matrix
```
Uncomment the Matrix import in `src/channels/index.ts`:
```typescript
import './matrix.js';
```
```bash
pnpm run build
```
## Credentials
1. Register a bot account on your Matrix homeserver (e.g., via Element)
2. Get the homeserver URL (e.g., `https://matrix.org` or your self-hosted URL)
3. Get an access token:
- In Element: **Settings** > **Help & About** > **Access Token** (advanced)
- Or via API: `curl -XPOST 'https://matrix.org/_matrix/client/r0/login' -d '{"type":"m.login.password","user":"botuser","password":"..."}'`
4. Note the bot's user ID (e.g., `@botuser:matrix.org`)
### Configure environment
Add to `.env`:
```bash
MATRIX_BASE_URL=https://matrix.org
MATRIX_ACCESS_TOKEN=your-access-token
MATRIX_USER_ID=@botuser:matrix.org
MATRIX_BOT_USERNAME=botuser
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `matrix`
- **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`).
- **how-to-find-id**: 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`.
- **supports-threads**: partial (some clients support threads, but not all — treat as no for reliability)
- **typical-use**: Interactive chat — rooms or direct messages
- **default-isolation**: Same agent group for rooms where you're the primary user. Separate agent group for rooms with different communities or sensitive contexts.
+3
View File
@@ -0,0 +1,3 @@
# Verify Matrix Channel
Invite the bot to a Matrix room and send a message. The bot should respond within a few seconds.
+1 -1
View File
@@ -87,7 +87,7 @@ done
### Validate code changes
```bash
npm run build
pnpm run build
./container/build.sh
```
+114
View File
@@ -0,0 +1,114 @@
---
name: add-opencode
description: Use OpenCode as an agent provider on NanoClaw v2 (AGENT_PROVIDER=opencode). OpenRouter, OpenAI, Google, DeepSeek, etc. via OpenCode config — not the Anthropic Agent SDK. Per-session and per-group via agent_provider; host passes OPENCODE_* and XDG mount when spawning containers.
---
# OpenCode agent provider (v2)
NanoClaw **v2** runs agents in a long-lived **poll loop** inside the container. The backend is selected with **`AGENT_PROVIDER`** (`claude` | `opencode` | `mock`), not the v1 `AGENT_RUNNER` env var.
## What it does (upstream v2)
- **`container/agent-runner/src/providers/opencode.ts`** — `OpenCodeProvider` implementing `AgentProvider` (SSE via OpenCode server, session resume, MCP from merged `ProviderOptions.mcpServers` only — no `settings.json` MCP bridge).
- **`container/agent-runner/src/providers/mcp-to-opencode.ts`** — maps v2 `McpServerConfig` to OpenCode `mcp` entries.
- **`container/agent-runner/src/providers/factory.ts`** — registers `opencode`.
- **`container/agent-runner/package.json`** — dependency `@opencode-ai/sdk`.
- **`container/Dockerfile`** — global **`opencode-ai`** CLI for `opencode serve`.
- **`src/container-runner.ts`** — when effective provider is `opencode`: `XDG_DATA_HOME=/opencode-xdg`, session-scoped host mount, `NO_PROXY`/`no_proxy` merge for `127.0.0.1,localhost`, passes through `OPENCODE_PROVIDER`, `OPENCODE_MODEL`, `OPENCODE_SMALL_MODEL` from the host environment.
## Configuration
### Host `.env` (typical)
Set model/provider strings in the form OpenCode expects (often `provider/model-id`). **Put comments on their own lines** — a `#` inside a value is kept verbatim and breaks model IDs.
These variables are read **on the host** and passed into the container only when the effective provider is `opencode` (see `src/container-runner.ts`). They do not switch the provider by themselves; the DB still needs `agent_provider` set (below).
- `OPENCODE_PROVIDER` — OpenCode provider id, e.g. `openrouter`, `anthropic` (if unset, the runner defaults to `anthropic`).
- `OPENCODE_MODEL` — full model id, e.g. `openrouter/anthropic/claude-sonnet-4`.
- `OPENCODE_SMALL_MODEL` — optional second model for “small” tasks.
Credentials: OneCLI / credential proxy patterns are unchanged. For non-`anthropic` OpenCode providers, the runner registers a placeholder API key and **`ANTHROPIC_BASE_URL`** (the credential proxy) as `baseURL` so the real key never lives in the container.
#### Example: OpenRouter
Use ids that match OpenCodes registry / your custom registrations. Adjust names to what you actually run.
```env
# OpenCode — host passes these into the container when agent_provider is opencode
OPENCODE_PROVIDER=openrouter
OPENCODE_MODEL=openrouter/anthropic/claude-sonnet-4
OPENCODE_SMALL_MODEL=openrouter/anthropic/claude-haiku-4.5
```
#### Example: Anthropic via existing proxy env
When `OPENCODE_PROVIDER` is `anthropic`, OpenCode uses normal Anthropic env inside the container (proxy + placeholder key pattern unchanged).
```env
OPENCODE_PROVIDER=anthropic
OPENCODE_MODEL=anthropic/claude-sonnet-4-20250514
```
#### Example: only a main model
```env
OPENCODE_PROVIDER=openrouter
OPENCODE_MODEL=openrouter/google/gemini-2.5-pro-preview
```
#### OpenCode Zen (`x-api-key`, not Bearer)
Zens HTTP API (e.g. `POST …/zen/v1/messages`) expects the key in the **`x-api-key`** header. If OneCLI injects **`Authorization: Bearer …`** only, Zen often returns **401 / “Missing API key”** even though the gateway is working.
**Naming:** NanoClaw **`AGENT_PROVIDER=opencode`** (v2 DB `agent_provider`) means “run the **OpenCode agent provider**.” Separately, **`OPENCODE_PROVIDER=opencode`** in `.env` is OpenCodes **Zen provider id** inside the OpenCode config (see [Zen docs](https://opencode.ai/docs/zen/)).
**Host `.env` (typical Zen shape):**
```env
# NanoClaw still resolves AGENT_PROVIDER from agent_groups / sessions; set agent_provider to opencode there.
# OpenCode SDK: Zen as the upstream provider + models under opencode/…
OPENCODE_PROVIDER=opencode
OPENCODE_MODEL=opencode/big-pickle
OPENCODE_SMALL_MODEL=opencode/big-pickle
# Point the credential proxy at Zens Anthropic-compatible base URL (host + OneCLI must forward this host).
ANTHROPIC_BASE_URL=https://opencode.ai/zen/v1
```
Use a real Zen model id from the docs; `big-pickle` is one example.
**OneCLI:** register the Zen key with **`x-api-key`**, not Bearer:
```bash
onecli secrets create --name "OpenCode Zen" --type generic \
--value YOUR_ZEN_KEY --host-pattern opencode.ai \
--header-name "x-api-key" --value-format "{value}"
```
For comparison, OpenRouter uses `Authorization` + `Bearer {value}`. Zen is different by design.
### Per group / per session
v2 schema: **`agent_groups.agent_provider`** and **`sessions.agent_provider`**. Set to `opencode` for groups or sessions that should use OpenCode. The container receives `AGENT_PROVIDER` from the resolved value (session overrides group).
Extra MCP servers still come from **`NANOCLAW_MCP_SERVERS`** / `container_config.mcpServers` on the host; the runner merges them into the same `mcpServers` object passed to **both** Claude and OpenCode providers.
## Operational notes
- OpenCode keeps a local **`opencode serve`** process and SSE subscription; the provider tears down with **`stream.return`** and **SIGKILL** on the server process on **`abort()`** / shared runtime reset to avoid MCP/zombie hangs.
- Session continuation is opaque (`ses_*` ids); stale sessions are cleared using **`isSessionInvalid`** on OpenCode-specific errors (timeouts, connection resets, not-found patterns) in addition to the poll-loops existing recovery.
- **`NO_PROXY`** for localhost matters when the OpenCode client talks to `127.0.0.1` inside the container while HTTP(S)_PROXY is set (e.g. OneCLI).
## Verify
```bash
grep -q opencode container/agent-runner/src/providers/factory.ts && echo "OpenCode registered" || echo "Missing"
npm run build --prefix container/agent-runner
```
Rebuild the agent image after Dockerfile changes: `./container/build.sh` (or your usual image build).
## Migrate from v1 wording
If documentation or habits still say **`AGENT_RUNNER=opencode`**, update to **`AGENT_PROVIDER=opencode`** and store **`agent_provider`** in v2 tables, not v1 runner columns.
+2 -2
View File
@@ -232,7 +232,7 @@ echo '{}' | docker run -i --entrypoint /bin/echo nanoclaw-agent:latest "Containe
Rebuild the main app and restart:
```bash
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
```
@@ -286,5 +286,5 @@ To remove Parallel AI integration:
1. Remove from .env: `sed -i.bak '/PARALLEL_API_KEY/d' .env`
2. Revert changes to container-runner.ts and agent-runner/src/index.ts
3. Remove Web Research Tools section from groups/main/CLAUDE.md
4. Rebuild: `./container/build.sh && npm run build`
4. Rebuild: `./container/build.sh && pnpm run build`
5. Restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
+4 -4
View File
@@ -31,8 +31,8 @@ git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
```bash
git fetch whatsapp skill/pdf-reader
git merge whatsapp/skill/pdf-reader || {
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
}
```
@@ -49,8 +49,8 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
### Validate
```bash
npm run build
npx vitest run src/channels/whatsapp.test.ts
pnpm run build
pnpm exec vitest run src/channels/whatsapp.test.ts
```
### Rebuild container
+6 -6
View File
@@ -38,8 +38,8 @@ git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
```bash
git fetch whatsapp skill/reactions
git merge whatsapp/skill/reactions || {
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
}
```
@@ -54,14 +54,14 @@ This adds:
### Run database migration
```bash
npx tsx scripts/migrate-reactions.ts
pnpm exec tsx scripts/migrate-reactions.ts
```
### Validate code changes
```bash
npm test
npm run build
pnpm test
pnpm run build
```
All tests must pass and build must be clean before proceeding.
@@ -71,7 +71,7 @@ All tests must pass and build must be clean before proceeding.
### Build and restart
```bash
npm run build
pnpm run build
```
Linux:
+6
View File
@@ -0,0 +1,6 @@
# Remove Resend Email Channel
1. Comment out `import './resend.js'` in `src/channels/index.ts`
2. Remove `RESEND_API_KEY`, `RESEND_FROM_ADDRESS`, `RESEND_FROM_NAME`, `RESEND_WEBHOOK_SECRET` from `.env`
3. `pnpm uninstall @resend/chat-sdk-adapter`
4. Rebuild and restart
+69
View File
@@ -0,0 +1,69 @@
---
name: add-resend-v2
description: Add Resend (email) channel integration to NanoClaw v2 via Chat SDK.
---
# Add Resend Email Channel
Connect NanoClaw to email via Resend for async email conversations.
## Pre-flight
Check if `src/channels/resend.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
```bash
pnpm install @resend/chat-sdk-adapter
```
Uncomment the Resend import in `src/channels/index.ts`:
```typescript
import './resend.js';
```
Build:
```bash
pnpm run build
```
## Credentials
1. Go to [resend.com](https://resend.com) and create an account.
2. Add and verify your sending domain.
3. Go to **API Keys** and create a new key.
4. Set up a webhook:
- Go to **Webhooks** > **Add webhook**.
- URL: `https://your-domain/webhook/resend`.
- Events: select **email.received**.
- Copy the signing secret.
### Configure environment
Add to `.env`:
```bash
RESEND_API_KEY=re_...
RESEND_FROM_ADDRESS=bot@yourdomain.com
RESEND_FROM_NAME=NanoClaw
RESEND_WEBHOOK_SECRET=your-webhook-secret
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `resend`
- **terminology**: Resend handles email. Each email thread (identified by subject/In-Reply-To headers) is a separate conversation. The "from address" is the bot's identity.
- **how-to-find-id**: The platform ID is the from email address (e.g. `bot@yourdomain.com`). Each sender's email thread becomes its own conversation.
- **supports-threads**: yes (via email threading headers -- replies to the same thread stay together)
- **typical-use**: Async communication -- email conversations with longer response expectations
- **default-isolation**: Same agent group if you want your agent to handle email alongside other channels. Separate agent group if email contains sensitive correspondence that shouldn't be accessible from other channels.
+3
View File
@@ -0,0 +1,3 @@
# Verify Resend Email Channel
Send an email to the configured from address. The bot should respond via email within a few seconds.
+6
View File
@@ -0,0 +1,6 @@
# Remove Slack
1. Comment out `import './slack.js'` in `src/channels/index.ts`
2. Remove `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` from `.env`
3. `pnpm uninstall @chat-adapter/slack`
4. Rebuild and restart
+92
View File
@@ -0,0 +1,92 @@
---
name: add-slack-v2
description: Add Slack channel integration to NanoClaw v2 via Chat SDK.
---
# Add Slack Channel
Adds Slack support to NanoClaw v2 using the Chat SDK bridge.
## Pre-flight
Check if `src/channels/slack.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
### Install the adapter package
```bash
pnpm install @chat-adapter/slack
```
### Enable the channel
Uncomment the Slack import in `src/channels/index.ts`:
```typescript
import './slack.js';
```
### Build
```bash
pnpm run build
```
## Credentials
### Create Slack App
1. Go to [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App** > **From scratch**
2. Name it (e.g., "NanoClaw") and select your workspace
3. Go to **OAuth & Permissions** and add Bot Token Scopes:
- `chat:write`, `channels:history`, `groups:history`, `im:history`, `channels:read`, `groups:read`, `users:read`, `reactions:write`
4. Click **Install to Workspace** and copy the **Bot User OAuth Token** (`xoxb-...`)
5. Go to **Basic Information** and copy the **Signing Secret**
### Enable DMs
6. Go to **App Home** and enable the **Messages Tab**
7. Check **"Allow users to send Slash commands and messages from the messages tab"**
### Event Subscriptions
8. Go to **Event Subscriptions** and toggle **Enable Events**
9. Set the **Request URL** to `https://your-domain/webhook/slack` — Slack will send a verification challenge; it must pass before you can save
10. Under **Subscribe to bot events**, add:
- `message.channels`, `message.groups`, `message.im`, `app_mention`
11. Click **Save Changes**
12. Slack will show a banner asking you to **reinstall the app** — click it to apply the new event subscriptions
### Configure environment
Add to `.env`:
```bash
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_SIGNING_SECRET=your-signing-secret
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
### Webhook server
The Chat SDK bridge automatically starts a shared webhook server on port 3000 (configurable via `WEBHOOK_PORT` env var). The server handles `/webhook/slack` for Slack and other webhook-based adapters. This port must be publicly reachable from the internet for Slack to deliver events.
If running locally, discuss options for exposing the server — e.g. ngrok (`ngrok http 3000`), Cloudflare Tunnel, or a reverse proxy on a VPS. The resulting public URL becomes the base for `https://your-domain/webhook/slack`.
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `slack`
- **terminology**: Slack has "workspaces" containing "channels." Channels can be public (#general) or private. The bot can also receive direct messages.
- **platform-id-format**: `slack:{channelId}` for channels (e.g., `slack:C0123ABC`), `slack:{dmId}` for DMs (e.g., `slack:D0ARWEBLV63`)
- **how-to-find-id**: Right-click a channel name > "View channel details" — the Channel ID is at the bottom (starts with C). For DMs, the ID starts with D. Or copy the channel link — the ID is the last segment of the URL.
- **supports-threads**: yes
- **typical-use**: Interactive chat — team channels or direct messages
- **default-isolation**: Same agent group for channels where you're the primary user. Separate agent group for channels with different teams or sensitive contexts.
+3
View File
@@ -0,0 +1,3 @@
# Verify Slack
Add the bot to a Slack channel, then send a message or @mention the bot. The bot should respond within a few seconds.
+9 -9
View File
@@ -36,8 +36,8 @@ git remote add slack https://github.com/qwibitai/nanoclaw-slack.git
```bash
git fetch slack main
git merge slack/main || {
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
}
```
@@ -54,9 +54,9 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
### Validate code changes
```bash
npm install
npm run build
npx vitest run src/channels/slack.test.ts
pnpm install
pnpm run build
pnpm exec vitest run src/channels/slack.test.ts
```
All tests must pass (including the new Slack tests) and build must be clean before proceeding.
@@ -98,7 +98,7 @@ The container reads environment from `data/env/env`, not `.env` directly.
### Build and restart
```bash
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
```
@@ -118,18 +118,18 @@ Wait for the user to provide the channel ID.
### Register the channel
The channel ID, name, and folder name are needed. Use `npx tsx setup/index.ts --step register` with the appropriate flags.
The channel ID, name, and folder name are needed. Use `pnpm exec tsx setup/index.ts --step register` with the appropriate flags.
For a main channel (responds to all messages):
```bash
npx tsx setup/index.ts --step register -- --jid "slack:<channel-id>" --name "<channel-name>" --folder "slack_main" --trigger "@${ASSISTANT_NAME}" --channel slack --no-trigger-required --is-main
pnpm exec tsx setup/index.ts --step register -- --jid "slack:<channel-id>" --name "<channel-name>" --folder "slack_main" --trigger "@${ASSISTANT_NAME}" --channel slack --no-trigger-required --is-main
```
For additional channels (trigger-only):
```bash
npx tsx setup/index.ts --step register -- --jid "slack:<channel-id>" --name "<channel-name>" --folder "slack_<channel-name>" --trigger "@${ASSISTANT_NAME}" --channel slack
pnpm exec tsx setup/index.ts --step register -- --jid "slack:<channel-id>" --name "<channel-name>" --folder "slack_<channel-name>" --trigger "@${ASSISTANT_NAME}" --channel slack
```
## Phase 5: Verify
+6
View File
@@ -0,0 +1,6 @@
# Remove Microsoft Teams Channel
1. Comment out `import './teams.js'` in `src/channels/index.ts`
2. Remove `TEAMS_APP_ID` and `TEAMS_APP_PASSWORD` from `.env`
3. `pnpm uninstall @chat-adapter/teams`
4. Rebuild and restart
+183
View File
@@ -0,0 +1,183 @@
---
name: add-teams-v2
description: Add Microsoft Teams channel integration to NanoClaw v2 via Chat SDK.
---
# Add Microsoft Teams Channel
Connect NanoClaw to Microsoft Teams for interactive chat in team channels, group chats, and direct messages.
## Pre-flight
Check if `src/channels/teams.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
```bash
pnpm install @chat-adapter/teams
```
Uncomment the Teams import in `src/channels/index.ts`:
```typescript
import './teams.js';
```
Build:
```bash
pnpm run build
```
## Credentials
### Step 1: Create an Azure AD App Registration
1. Go to [Azure Portal](https://portal.azure.com) > **App registrations** > **New registration**
2. Name it (e.g., "NanoClaw")
3. Supported account types: **Single tenant** (your org only) or **Multi tenant** (any org)
4. Click **Register**
5. Copy the **Application (client) ID** and **Directory (tenant) ID** from the Overview page
### Step 2: Create a Client Secret
1. In the App Registration, go to **Certificates & secrets**
2. Click **New client secret**, description "nanoclaw", expiry 180 days
3. Click **Add** and **copy the Value immediately** (shown only once)
### Step 3: Create an Azure Bot
1. Go to Azure Portal > search **Azure Bot** > **Create**
2. Fill in:
- **Bot handle**: unique name (e.g., "nanoclaw-bot")
- **Type of App**: match your app registration (Single or Multi Tenant)
- **Creation type**: **Use existing app registration**
- **App ID**: paste from Step 1
- **App tenant ID**: paste from Step 1 (Single Tenant only)
3. Click **Review + create** > **Create**
Or use Azure CLI:
```bash
az group create --name nanoclaw-rg --location eastus
az bot create \
--resource-group nanoclaw-rg \
--name nanoclaw-bot \
--app-type SingleTenant \
--appid YOUR_APP_ID \
--tenant-id YOUR_TENANT_ID \
--endpoint "https://your-domain/api/webhooks/teams"
```
### Step 4: Configure Messaging Endpoint
1. Go to your Azure Bot resource > **Configuration**
2. Set **Messaging endpoint** to `https://your-domain/api/webhooks/teams`
3. Click **Apply**
### Step 5: Enable Teams Channel
1. In the Azure Bot resource, go to **Channels**
2. Click **Microsoft Teams** > Accept terms > **Apply**
Or via CLI:
```bash
az bot msteams create --resource-group nanoclaw-rg --name nanoclaw-bot
```
### Step 6: Create and Sideload Teams App
Create a `manifest.json`:
```json
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json",
"manifestVersion": "1.16",
"version": "1.0.0",
"id": "YOUR_APP_ID",
"packageName": "com.nanoclaw.bot",
"developer": {
"name": "NanoClaw",
"websiteUrl": "https://your-domain",
"privacyUrl": "https://your-domain",
"termsOfUseUrl": "https://your-domain"
},
"name": { "short": "NanoClaw", "full": "NanoClaw Assistant" },
"description": {
"short": "NanoClaw assistant bot",
"full": "NanoClaw personal assistant powered by Claude."
},
"icons": { "outline": "outline.png", "color": "color.png" },
"accentColor": "#4A90D9",
"bots": [{
"botId": "YOUR_APP_ID",
"scopes": ["personal", "team", "groupchat"],
"supportsFiles": false,
"isNotificationOnly": false
}],
"permissions": ["identity", "messageTeamMembers"],
"validDomains": ["your-domain"]
}
```
Create two icon PNGs (32x32 `outline.png`, 192x192 `color.png`), zip all three files together.
**Sideload in Teams:**
1. Open Teams > **Apps** > **Manage your apps**
2. Click **Upload an app** > **Upload a custom app**
3. Select the zip file
Sideloading requires Teams admin access. Free personal Teams does NOT support sideloading. Use a Microsoft 365 Business account or developer tenant.
### Step 7: Receive All Messages (Optional)
By default, the bot only receives messages when @-mentioned. To receive all messages in a channel without @-mention, add RSC permissions to `manifest.json`:
```json
{
"authorization": {
"permissions": {
"resourceSpecific": [
{ "name": "ChannelMessage.Read.Group", "type": "Application" }
]
}
}
}
```
### Configure environment
Add to `.env`:
```bash
TEAMS_APP_ID=your-app-id
TEAMS_APP_PASSWORD=your-client-secret
# For Single Tenant only:
TEAMS_APP_TENANT_ID=your-tenant-id
TEAMS_APP_TYPE=SingleTenant
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
### Webhook server
The Chat SDK bridge automatically starts a shared webhook server on port 3000 (configurable via `WEBHOOK_PORT` env var). The server handles `/api/webhooks/teams` for Teams and other webhook-based adapters. This port must be publicly reachable from the internet for Azure Bot Service to deliver activities.
For local development without a public URL, use a tunnel (e.g., `ngrok http 3000`) and update the messaging endpoint in Azure Bot Configuration.
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `teams`
- **terminology**: Teams has "teams" containing "channels." The bot can also receive DMs (personal scope) and group chat messages. Channels support threaded replies.
- **platform-id-format**: `teams:{base64-encoded-conversation-id}:{base64-encoded-service-url}` — auto-generated by the adapter, not human-readable. Use the auto-created messaging group ID for wiring.
- **how-to-find-id**: Send a message to the bot in the channel. NanoClaw auto-creates a messaging group and logs the platform ID. Use that messaging group ID for wiring.
- **supports-threads**: yes (channels only; DMs and group chats are flat)
- **typical-use**: Team collaboration with the bot in channels; personal assistant via DMs
- **default-isolation**: Separate agent group per team. DMs can share an agent group with your main channel for unified personal memory.
+3
View File
@@ -0,0 +1,3 @@
# Verify Microsoft Teams Channel
Add the bot to a Teams channel or send it a direct message. The bot should respond within a few seconds.
+2 -2
View File
@@ -319,7 +319,7 @@ Also add `TELEGRAM_BOT_POOL` to the launchd plist (`~/Library/LaunchAgents/com.n
### Step 7: Rebuild and Restart
```bash
npm run build
pnpm run build
./container/build.sh # Required — MCP tool changed
# macOS:
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
@@ -381,4 +381,4 @@ To remove Agent Swarm support while keeping basic Telegram:
5. Remove `sender` param from MCP tool in `container/agent-runner/src/ipc-mcp-stdio.ts`
6. Remove Agent Teams section from group CLAUDE.md files
7. Remove `TELEGRAM_BOT_POOL` from `.env`, `data/env/env`, and launchd plist/systemd unit
8. Rebuild: `npm run build && ./container/build.sh && launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist` (macOS) or `npm run build && ./container/build.sh && systemctl --user restart nanoclaw` (Linux)
8. Rebuild: `pnpm run build && ./container/build.sh && launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist` (macOS) or `pnpm run build && ./container/build.sh && systemctl --user restart nanoclaw` (Linux)
+6
View File
@@ -0,0 +1,6 @@
# Remove Telegram
1. Comment out `import './telegram.js'` in `src/channels/index.ts`
2. Remove `TELEGRAM_BOT_TOKEN` from `.env`
3. `pnpm uninstall @chat-adapter/telegram`
4. Rebuild and restart
+74
View File
@@ -0,0 +1,74 @@
---
name: add-telegram-v2
description: Add Telegram channel integration to NanoClaw v2 via Chat SDK.
---
# Add Telegram Channel
Adds Telegram bot support to NanoClaw v2 using the Chat SDK bridge.
## Pre-flight
Check if `src/channels/telegram.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
### Install the adapter package
```bash
pnpm install @chat-adapter/telegram
```
### Enable the channel
Uncomment the Telegram import in `src/channels/index.ts`:
```typescript
import './telegram.js';
```
### Build
```bash
pnpm run build
```
## Credentials
### Create Telegram Bot
1. Open Telegram and search for `@BotFather`
2. Send `/newbot` and follow the prompts:
- Bot name: Something friendly (e.g., "NanoClaw Assistant")
- Bot username: Must end with "bot" (e.g., "nanoclaw_bot")
3. Copy the bot token (looks like `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`)
**Important for group chats**: By default, Telegram bots only see @mentions and commands in groups. To let the bot see all messages:
1. Open `@BotFather` > `/mybots` > select your bot
2. **Bot Settings** > **Group Privacy** > **Turn off**
### Configure environment
Add to `.env`:
```bash
TELEGRAM_BOT_TOKEN=your-bot-token
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `telegram`
- **terminology**: Telegram calls them "groups" and "chats." A "group" has multiple members; a "chat" is a 1:1 conversation with the bot.
- **how-to-find-id**: Do NOT ask the user for a chat ID. Telegram registration uses pairing — run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent <main|wire-to:folder|new-agent:folder>`, show the user the 4-digit `CODE` from the `PAIR_TELEGRAM_ISSUED` block (follow the `REMINDER_TO_ASSISTANT` line in that block), and tell them to send just the 4 digits as a message from the chat they want to register (DM the bot for `main`, post in the group otherwise). In groups with Group Privacy ON, prefix with the bot handle: `@<botname> CODE`. Wrong guesses invalidate the code — if a `PAIR_TELEGRAM_ATTEMPT` block arrives with a mismatched `RECEIVED_CODE`, a `PAIR_TELEGRAM_NEW_CODE` block will follow automatically (up to 5 regenerations); show the new code. On `PAIR_TELEGRAM STATUS=failed ERROR=max-regenerations-exceeded`, ask the user if they want to try again and re-invoke the step — each invocation starts a fresh 5-attempt batch. Success emits `PAIR_TELEGRAM STATUS=success` with `PLATFORM_ID`, `IS_GROUP`, and `ADMIN_USER_ID`. The service must be running for this to work (the polling adapter is what observes the code).
- **supports-threads**: no
- **typical-use**: Interactive chat — direct messages or small groups
- **default-isolation**: Same agent group if you're the only participant across multiple chats. Separate agent group if different people are in different groups.
+3
View File
@@ -0,0 +1,3 @@
# Verify Telegram
Send a message to your bot in Telegram (search for its username), or add the bot to a group and send a message there. The bot should respond within a few seconds.
+14 -14
View File
@@ -40,8 +40,8 @@ git remote add telegram https://github.com/qwibitai/nanoclaw-telegram.git
```bash
git fetch telegram main
git merge telegram/main || {
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
}
```
@@ -58,9 +58,9 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
### Validate code changes
```bash
npm install
npm run build
npx vitest run src/channels/telegram.test.ts
pnpm install
pnpm run build
pnpm exec vitest run src/channels/telegram.test.ts
```
All tests must pass (including the new Telegram tests) and build must be clean before proceeding.
@@ -114,7 +114,7 @@ Tell the user:
### Build and restart
```bash
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
```
@@ -133,18 +133,18 @@ Wait for the user to provide the chat ID (format: `tg:123456789` or `tg:-1001234
### Register the chat
The chat ID, name, and folder name are needed. Use `npx tsx setup/index.ts --step register` with the appropriate flags.
The chat ID, name, and folder name are needed. Use `pnpm exec tsx setup/index.ts --step register` with the appropriate flags.
For a main chat (responds to all messages):
```bash
npx tsx setup/index.ts --step register -- --jid "tg:<chat-id>" --name "<chat-name>" --folder "telegram_main" --trigger "@${ASSISTANT_NAME}" --channel telegram --no-trigger-required --is-main
pnpm exec tsx setup/index.ts --step register -- --jid "tg:<chat-id>" --name "<chat-name>" --folder "telegram_main" --trigger "@${ASSISTANT_NAME}" --channel telegram --no-trigger-required --is-main
```
For additional chats (trigger-only):
```bash
npx tsx setup/index.ts --step register -- --jid "tg:<chat-id>" --name "<chat-name>" --folder "telegram_<group-name>" --trigger "@${ASSISTANT_NAME}" --channel telegram
pnpm exec tsx setup/index.ts --step register -- --jid "tg:<chat-id>" --name "<chat-name>" --folder "telegram_<group-name>" --trigger "@${ASSISTANT_NAME}" --channel telegram
```
## Phase 5: Verify
@@ -189,16 +189,16 @@ If `/chatid` doesn't work:
## After Setup
If running `npm run dev` while the service is active:
If running `pnpm run dev` while the service is active:
```bash
# macOS:
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
npm run dev
pnpm run dev
# When done testing:
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
# Linux:
# systemctl --user stop nanoclaw
# npm run dev
# pnpm run dev
# systemctl --user start nanoclaw
```
@@ -210,5 +210,5 @@ To remove Telegram integration:
2. Remove `import './telegram.js'` from `src/channels/index.ts`
3. Remove `TELEGRAM_BOT_TOKEN` from `.env`
4. Remove Telegram registrations from SQLite: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid LIKE 'tg:%'"`
5. Uninstall: `npm uninstall grammy`
6. Rebuild: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux)
5. Uninstall: `pnpm uninstall grammy`
6. Rebuild: `pnpm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `pnpm run build && systemctl --user restart nanoclaw` (Linux)
+147
View File
@@ -0,0 +1,147 @@
---
name: add-vercel
description: Add Vercel deployment capability to NanoClaw agents. Installs the Vercel CLI in agent containers and sets up OneCLI credential injection for api.vercel.com. Use when the user wants agents to deploy web applications to Vercel.
---
# Add Vercel
This skill gives NanoClaw agents the ability to deploy web applications to Vercel. It installs the Vercel CLI in agent containers and configures OneCLI to inject Vercel credentials automatically.
**Principle:** Do the work — don't tell the user to do it. Only ask for their input when it genuinely requires manual action (pasting a token).
## Phase 1: Pre-flight
### Check if already applied
Check if the container skill exists:
```bash
test -d container/skills/vercel-cli && echo "INSTALLED" || echo "NOT_INSTALLED"
```
If `INSTALLED`, skip to Phase 3 (Configure Credentials).
### Check prerequisites
Verify OneCLI is working (required for credential injection):
```bash
onecli version 2>/dev/null && echo "ONECLI_OK" || echo "ONECLI_MISSING"
```
If `ONECLI_MISSING`, tell the user to run `/init-onecli` first, then retry `/add-vercel`. Stop here.
## Phase 2: Install Container Skill
Copy the bundled container skill into the container skills directory:
```bash
rsync -a .claude/skills/add-vercel/container-skills/ container/skills/
```
Verify:
```bash
head -5 container/skills/vercel-cli/SKILL.md
```
## Phase 3: Configure Credentials
### Check if Vercel credential already exists
```bash
onecli secrets list 2>/dev/null | grep -i vercel
```
If a Vercel credential already exists, skip to Phase 4.
### Set up Vercel API credential
The agent needs a Vercel personal access token. Tell the user:
> I need your Vercel personal access token. Go to https://vercel.com/account/tokens and create one with these settings:
>
> - **Token name:** `nanoclaw` (or any name you'll recognize)
> - **Scope:** "Full Account" — the agent needs to create projects, deploy, and manage domains
> - **Expiration:** "No expiration" recommended (avoids credential rotation), or pick a date if your security policy requires it
>
> After creating the token, copy it — you'll only see it once.
Once the user provides the token, add it to OneCLI:
```bash
onecli secrets create \
--name "Vercel API Token" \
--type generic \
--value "<TOKEN>" \
--host-pattern "api.vercel.com" \
--header-name "Authorization" \
--value-format "Bearer {value}"
```
Verify:
```bash
onecli secrets list | grep -i vercel
```
### Assign the secret to all agents
OneCLI uses selective secret mode — secrets must be explicitly assigned to each agent. Get the Vercel secret ID from the output above, then assign it to every agent:
```bash
# For each agent, add the Vercel secret to its assigned secrets list.
# First get current assignments, then set them with the new secret appended.
VERCEL_SECRET_ID=$(onecli secrets list 2>/dev/null | grep -B2 "Vercel" | grep '"id"' | head -1 | sed 's/.*"id": "//;s/".*//')
for agent in $(onecli agents list 2>/dev/null | grep '"id"' | sed 's/.*"id": "//;s/".*//'); do
CURRENT=$(onecli agents secrets --id "$agent" 2>/dev/null | grep '"' | grep -v hint | grep -v data | sed 's/.*"//;s/".*//' | tr '\n' ',' | sed 's/,$//')
onecli agents set-secrets --id "$agent" --secret-ids "${CURRENT:+$CURRENT,}$VERCEL_SECRET_ID"
done
```
## Phase 4: Ensure Vercel CLI in Container Image
Check if `vercel` is already in the Dockerfile:
```bash
grep -q 'vercel' container/Dockerfile && echo "PRESENT" || echo "MISSING"
```
If `MISSING`, add `vercel` to the global npm install line in `container/Dockerfile`, then rebuild:
```bash
./container/build.sh
```
If `PRESENT`, skip — no rebuild needed.
## Phase 5: Sync Skills to Running Agent Groups
Container skills are copied once at group creation and not auto-synced. After installing or updating a container skill, sync it to all existing agent groups:
```bash
for session_dir in data/v2-sessions/ag-*; do
if [ -d "$session_dir/.claude-shared/skills" ]; then
rsync -a container/skills/ "$session_dir/.claude-shared/skills/"
echo "Synced skills to: $session_dir"
fi
done
```
## Phase 6: Restart Running Containers
Stop all running agent containers so they pick up the new skills on next wake:
```bash
docker ps --format "{{.ID}} {{.Names}}" | grep nanoclaw-v2 | awk '{print $1}' | xargs -r docker stop
```
## Done
The agent can now deploy web applications to Vercel. Key commands:
- `vercel deploy --yes --prod --token placeholder` — deploy to production
- `vercel ls --token placeholder` — list deployments
- `vercel whoami --token placeholder` — check auth
For the full command reference, the agent has the `vercel-cli` container skill loaded automatically.
@@ -0,0 +1,103 @@
---
name: vercel-cli
description: Deploy apps to Vercel. Use when asked to deploy, ship, or publish a web application, or manage Vercel projects, domains, and environment variables.
---
# Vercel CLI
You can deploy web applications to Vercel using the `vercel` CLI.
## Auth
Auth is handled by OneCLI — the HTTPS_PROXY injects the real token into API requests automatically. The Vercel CLI requires a token to be present to skip its local credential check, so **always pass `--token placeholder`** on every command. OneCLI replaces this with the real token at the proxy level.
Before any Vercel operation, verify auth:
```bash
vercel whoami --token placeholder
```
If this fails with an auth error, ask the user to add a Vercel token to OneCLI. They can create one at https://vercel.com/account/tokens and register it via `onecli secrets create` on the host. Once added, retry `vercel whoami`.
## Deploying
Always use `--yes` to skip interactive prompts and `--token placeholder` for auth (OneCLI replaces with real token).
```bash
# Deploy to production
vercel deploy --yes --prod --token placeholder
# Deploy from a specific directory
vercel deploy --yes --prod --token placeholder --cwd /path/to/project
# Preview deployment (not production)
vercel deploy --yes --token placeholder
```
After deploying, verify the live URL:
```bash
# Check deployment status
vercel inspect <deployment-url> --token placeholder
```
## Pre-Send Checks (do this before sharing the URL)
Don't send the deployment URL to the user until you've confirmed it's actually working. At minimum:
1. **Local build passes** — run `npm run build` (or the project's build command) before `vercel deploy`. If the build fails locally, fix it first; don't deploy broken code.
2. **Deployment succeeded** — the `vercel deploy` output shows a "Production: https://..." URL and the status is READY (confirm with `vercel inspect`).
3. **Live URL responds**`curl -sI <url> | head -1` should return `HTTP/2 200` (or another 2xx/3xx). A 404/500 means something's broken even though Vercel reported success.
4. **Optional visual check** — if `agent-browser` is loaded, open the URL and eyeball it. Helpful for catching broken layouts that a 200 response wouldn't reveal.
If any check fails, fix the issue and redeploy before reporting to the user.
## Project Management
```bash
# Link to an existing Vercel project (non-interactive)
vercel link --yes --token placeholder
# List recent deployments
vercel ls --token placeholder
# List all projects
vercel project ls --token placeholder
```
## Domains
```bash
# List domains
vercel domains ls --token placeholder
# Add a domain to the current project
vercel domains add example.com --token placeholder
```
## Environment Variables
```bash
# Pull env vars from Vercel to local .env
vercel env pull --token placeholder
# Add an env var (use echo to pipe the value — avoids interactive prompt)
echo "value" | vercel env add VAR_NAME production --token placeholder
```
## Common Errors
| Error | Fix |
|-------|-----|
| `Error: No framework detected` | Ensure the project has a `package.json` with a `build` script, or set the framework in `vercel.json` |
| `Error: Rate limited` | Wait and retry. Don't loop — report to user |
| `Error: You have reached your project limit` | User needs to upgrade Vercel plan or delete unused projects |
| `ENOTFOUND api.vercel.com` | Network issue. Check proxy connectivity |
| Auth error after `vercel whoami` | Credential may be expired. Ask the user to refresh the Vercel token in OneCLI |
## Best Practices
- Run `npm run build` locally before deploying to catch build errors early
- Use `--cwd` instead of `cd` to keep your working directory stable
- For Next.js projects, `vercel deploy` auto-detects the framework — no extra config needed
- Use `vercel.json` only when you need custom build settings, rewrites, or headers
@@ -42,8 +42,8 @@ git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
```bash
git fetch whatsapp skill/voice-transcription
git merge whatsapp/skill/voice-transcription || {
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
}
```
@@ -60,9 +60,9 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
### Validate code changes
```bash
npm install --legacy-peer-deps
npm run build
npx vitest run src/channels/whatsapp.test.ts
pnpm install
pnpm run build
pnpm exec vitest run src/channels/whatsapp.test.ts
```
All tests must pass and build must be clean before proceeding.
@@ -103,7 +103,7 @@ The container reads environment from `data/env/env`, not `.env` directly.
### Build and restart
```bash
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
```
+6
View File
@@ -0,0 +1,6 @@
# Remove Webex Channel
1. Comment out `import './webex.js'` in `src/channels/index.ts`
2. Remove `WEBEX_BOT_TOKEN` and `WEBEX_WEBHOOK_SECRET` from `.env`
3. `pnpm uninstall @bitbasti/chat-adapter-webex`
4. Rebuild and restart
+62
View File
@@ -0,0 +1,62 @@
---
name: add-webex-v2
description: Add Webex channel integration to NanoClaw v2 via Chat SDK.
---
# Add Webex Channel
Adds Cisco Webex support to NanoClaw v2 using the Chat SDK bridge.
## Pre-flight
Check if `src/channels/webex.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
```bash
pnpm install @bitbasti/chat-adapter-webex
```
Uncomment the Webex import in `src/channels/index.ts`:
```typescript
import './webex.js';
```
```bash
pnpm run build
```
## Credentials
1. Go to [developer.webex.com](https://developer.webex.com/my-apps/new/bot) and create a new bot
2. Copy the **Bot Access Token**
3. Set up a webhook:
- Use the Webex API or Developer Portal to create a webhook pointing to `https://your-domain/webhook/webex`
- Set a webhook secret for signature verification
### Configure environment
Add to `.env`:
```bash
WEBEX_BOT_TOKEN=your-bot-token
WEBEX_WEBHOOK_SECRET=your-webhook-secret
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `webex`
- **terminology**: Webex has "spaces." A space can be a group conversation or a 1:1 direct message with the bot.
- **how-to-find-id**: Open the space in Webex, click the space name > Settings — the Space ID is listed there. Or use the Webex API (`GET /rooms`) to list spaces and their IDs.
- **supports-threads**: yes
- **typical-use**: Interactive chat — team spaces or direct messages
- **default-isolation**: Same agent group for spaces where you're the primary user. Separate agent group for spaces with different teams or sensitive information.
+3
View File
@@ -0,0 +1,3 @@
# Verify Webex Channel
Add the bot to a Webex space or send it a direct message. The bot should respond within a few seconds.
@@ -0,0 +1,6 @@
# Remove WhatsApp Cloud API Channel
1. Comment out `import './whatsapp-cloud.js'` in `src/channels/index.ts`
2. Remove `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_APP_SECRET`, `WHATSAPP_VERIFY_TOKEN` from `.env`
3. `pnpm uninstall @chat-adapter/whatsapp`
4. Rebuild and restart
@@ -0,0 +1,71 @@
---
name: add-whatsapp-cloud-v2
description: Add WhatsApp Business Cloud API channel to NanoClaw v2 via Chat SDK. Official Meta API.
---
# Add WhatsApp Cloud API Channel
Connect NanoClaw to WhatsApp via the official Meta WhatsApp Business Cloud API.
## Pre-flight
Check if `src/channels/whatsapp-cloud.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
```bash
pnpm install @chat-adapter/whatsapp
```
Uncomment the WhatsApp Cloud API import in `src/channels/index.ts`:
```typescript
import './whatsapp-cloud.js';
```
Build:
```bash
pnpm run build
```
## Credentials
1. Go to [Meta for Developers](https://developers.facebook.com/apps/) and create an app (type: Business).
2. Add the **WhatsApp** product.
3. Go to **WhatsApp** > **API Setup**:
- Note the **Phone Number ID** (not the phone number itself).
- Generate a **permanent System User access token** with `whatsapp_business_messaging` permission.
4. Go to **WhatsApp** > **Configuration**:
- Set webhook URL: `https://your-domain/webhook/whatsapp`.
- Set a **Verify Token** (any random string you choose).
- Subscribe to webhook fields: `messages`.
5. Copy the **App Secret** from **Settings** > **Basic**.
### Configure environment
Add to `.env`:
```bash
WHATSAPP_ACCESS_TOKEN=your-system-user-access-token
WHATSAPP_PHONE_NUMBER_ID=your-phone-number-id
WHATSAPP_APP_SECRET=your-app-secret
WHATSAPP_VERIFY_TOKEN=your-verify-token
```
Sync to container: `mkdir -p data/env && cp .env data/env/env`
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `whatsapp-cloud`
- **terminology**: WhatsApp Cloud API supports 1:1 conversations only (no group chats). Each conversation is with a phone number.
- **how-to-find-id**: The platform ID is the Phone Number ID from the Meta Business dashboard (not the phone number itself). Find it under WhatsApp > API Setup.
- **supports-threads**: no
- **typical-use**: Interactive 1:1 chat -- direct messages only
- **default-isolation**: Same agent group if you're the only person messaging the bot. Each additional person who messages gets their own conversation automatically, but they share the agent's workspace and memory -- use a separate agent group if you need information isolation between different contacts.
@@ -0,0 +1,3 @@
# Verify WhatsApp Cloud API Channel
Send a message to your WhatsApp Business number. The bot should respond within a few seconds. Note: WhatsApp Cloud API only supports 1:1 DMs, not group chats.
+240
View File
@@ -0,0 +1,240 @@
---
name: add-whatsapp-v2
description: Add WhatsApp channel to NanoClaw v2 using native Baileys adapter. Direct connection — no Chat SDK bridge. Uses QR code or pairing code for authentication.
---
# Add WhatsApp Channel
Adds WhatsApp support to NanoClaw v2 using the native Baileys adapter (no Chat SDK bridge).
## Pre-flight
Check if `src/channels/whatsapp.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Credentials.
## Install
### Install the adapter packages
```bash
pnpm install @whiskeysockets/baileys@^6.7.21 pino@^9.6.0 qrcode@^1.5.4 @types/qrcode@^1.5.6
```
### Enable the channel
If `src/channels/whatsapp.ts` is missing, fetch it from upstream:
```bash
git remote -v | grep -q upstream || git remote add upstream https://github.com/qwibitai/nanoclaw.git
git fetch upstream v2
git checkout upstream/v2 -- src/channels/whatsapp.ts
```
Uncomment or add the WhatsApp import in `src/channels/index.ts`:
```typescript
// whatsapp (native, no Chat SDK)
import './whatsapp.js';
```
### Build
```bash
pnpm run build
```
## Credentials
WhatsApp uses linked-device authentication — no API key, just a one-time pairing from your phone.
### Check current state
Check if WhatsApp is already authenticated. If `store/auth/creds.json` exists, skip to "Shared vs dedicated number".
```bash
test -f store/auth/creds.json && echo "WhatsApp auth exists" || echo "No WhatsApp auth"
```
### Detect environment
Check whether the environment is headless (no display server):
```bash
[[ -z "$DISPLAY" && -z "$WAYLAND_DISPLAY" && "$OSTYPE" != darwin* ]] && echo "IS_HEADLESS=true" || echo "IS_HEADLESS=false"
```
### Ask the user
Use `AskUserQuestion` to collect configuration. **Adapt auth options based on environment:**
If IS_HEADLESS=true AND not WSL → AskUserQuestion: How do you want to authenticate WhatsApp?
- **Pairing code** (Recommended) - Enter a numeric code on your phone (no camera needed, requires phone number)
- **QR code in terminal** - Displays QR code in the terminal (can be too small on some displays)
Otherwise (macOS, desktop Linux, or WSL) → AskUserQuestion: How do you want to authenticate WhatsApp?
- **QR code in browser** (Recommended) - Opens a browser window with a large, scannable QR code
- **Pairing code** - Enter a numeric code on your phone (no camera needed, requires phone number)
- **QR code in terminal** - Displays QR code in the terminal (can be too small on some displays)
If they chose pairing code:
AskUserQuestion: What is your phone number? (Digits only — country code followed by your 10-digit number, no + prefix, spaces, or dashes. Example: 14155551234 where 1 is the US country code and 4155551234 is the phone number.)
### Clean previous auth state (if re-authenticating)
```bash
rm -rf store/auth/
```
### Run WhatsApp authentication
For QR code in browser (recommended):
```bash
pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
```
(Bash timeout: 150000ms)
Tell the user:
> A browser window will open with a QR code.
>
> 1. Open WhatsApp > **Settings** > **Linked Devices** > **Link a Device**
> 2. Scan the QR code in the browser
> 3. The page will show "Authenticated!" when done
For QR code in terminal:
```bash
pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method qr-terminal
```
(Bash timeout: 150000ms)
Tell the user:
> 1. Open WhatsApp > **Settings** > **Linked Devices** > **Link a Device**
> 2. Scan the QR code displayed in the terminal
For pairing code:
Tell the user to have WhatsApp open on **Settings > Linked Devices > Link a Device**, ready to tap **"Link with phone number instead"** — the code expires in ~60 seconds and must be entered immediately.
Run the auth process in the background and poll `store/pairing-code.txt` for the code:
```bash
rm -f store/pairing-code.txt && pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method pairing-code --phone <their-phone-number> > /tmp/wa-auth.log 2>&1 &
```
Then immediately poll for the code (do NOT wait for the background command to finish):
```bash
for i in $(seq 1 20); do [ -f store/pairing-code.txt ] && cat store/pairing-code.txt && break; sleep 1; done
```
Display the code to the user the moment it appears. Tell them:
> **Enter this code now** — it expires in ~60 seconds.
>
> 1. Open WhatsApp > **Settings** > **Linked Devices** > **Link a Device**
> 2. Tap **Link with phone number instead**
> 3. Enter the code immediately
After the user enters the code, poll for authentication to complete:
```bash
for i in $(seq 1 60); do grep -q 'STATUS: authenticated' /tmp/wa-auth.log 2>/dev/null && echo "authenticated" && break; grep -q 'STATUS: failed' /tmp/wa-auth.log 2>/dev/null && echo "failed" && break; sleep 2; done
```
**If failed:** logged_out → delete `store/auth/` and re-run. timeout → ask user, offer retry.
### Verify authentication succeeded
```bash
test -f store/auth/creds.json && echo "Authentication successful" || echo "Authentication failed"
```
### Shared vs dedicated number
AskUserQuestion: Is this a shared phone number (personal WhatsApp) or a dedicated number?
- **Shared number** — your personal WhatsApp (bot prefixes messages with its name)
- **Dedicated number** — a separate phone/SIM for the assistant
If dedicated, add to `.env`:
```bash
ASSISTANT_HAS_OWN_NUMBER=true
```
## Next Steps
If you're in the middle of `/setup`, return to the setup flow now.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
## Channel Info
- **type**: `whatsapp`
- **terminology**: WhatsApp calls them "groups" and "chats." A "chat" is a 1:1 DM; a "group" has multiple members.
- **how-to-find-id**: DMs use `<phone>@s.whatsapp.net` (e.g. `14155551234@s.whatsapp.net`). Groups use `<id>@g.us`. To find your number: `node -e "const c=JSON.parse(require('fs').readFileSync('store/auth/creds.json','utf-8'));console.log(c.me?.id?.split(':')[0]+'@s.whatsapp.net')"`. Groups are auto-discovered — check `sqlite3 data/v2.db "SELECT platform_id, name FROM messaging_groups WHERE channel_type='whatsapp' AND is_group=1"`.
- **supports-threads**: no
- **typical-use**: Interactive chat — direct messages or small groups
- **default-isolation**: Same agent group if you're the only participant across multiple chats. Separate agent group if different people are in different groups.
### Features
- Markdown formatting — `**bold**``*bold*`, `*italic*``_italic_`, headings→bold, code blocks preserved
- Approval questions — `ask_user_question` renders with `/approve`, `/reject` slash commands
- File attachments — send and receive images, video, audio, documents
- Reactions — send emoji reactions on messages
- Typing indicators — composing presence updates
- Credential requests — text fallback (WhatsApp has no modal support)
Not supported (WhatsApp linked device limitation): edit messages, delete messages.
## Troubleshooting
### QR code expired
QR codes expire after ~60 seconds. Re-run the auth command:
```bash
rm -rf store/auth/ && pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
```
### Pairing code not working
Codes expire in ~60 seconds. Delete auth and retry:
```bash
rm -rf store/auth/ && pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method pairing-code --phone <phone>
```
Ensure: digits only (no `+`), phone has internet, WhatsApp is updated.
If pairing code keeps failing, switch to QR-browser auth instead:
```bash
rm -rf store/auth/ && pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
```
### "waiting for this message" on reactions
Signal sessions corrupted from rapid restarts. Clear sessions:
```bash
systemctl --user stop nanoclaw
rm store/auth/session-*.json
systemctl --user start nanoclaw
```
### Bot not responding
1. Auth exists: `test -f store/auth/creds.json`
2. Connected: `grep "Connected to WhatsApp" logs/nanoclaw.log | tail -1`
3. Channel wired: `sqlite3 data/v2.db "SELECT mg.platform_id, mg.name FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id=mga.messaging_group_id WHERE mg.channel_type='whatsapp'"`
4. Service running: `systemctl --user status nanoclaw`
### "conflict" disconnection
Two instances connected with same credentials. Ensure only one NanoClaw process is running.
+22 -22
View File
@@ -63,8 +63,8 @@ git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
```bash
git fetch whatsapp main
git merge whatsapp/main || {
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
}
```
@@ -84,9 +84,9 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
### Validate code changes
```bash
npm install
npm run build
npx vitest run src/channels/whatsapp.test.ts
pnpm install
pnpm run build
pnpm exec vitest run src/channels/whatsapp.test.ts
```
All tests must pass and build must be clean before proceeding.
@@ -104,7 +104,7 @@ rm -rf store/auth/
For QR code in browser (recommended):
```bash
npx tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
```
(Bash timeout: 150000ms)
@@ -120,10 +120,10 @@ Tell the user:
For QR code in terminal:
```bash
npx tsx setup/index.ts --step whatsapp-auth -- --method qr-terminal
pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method qr-terminal
```
Tell the user to run `npm run auth` in another terminal, then:
Tell the user to run `pnpm run auth` in another terminal, then:
> 1. Open WhatsApp > **Settings** > **Linked Devices** > **Link a Device**
> 2. Scan the QR code displayed in the terminal
@@ -135,7 +135,7 @@ Tell the user to have WhatsApp open on **Settings > Linked Devices > Link a Devi
Run the auth process in the background and poll `store/pairing-code.txt` for the code:
```bash
rm -f store/pairing-code.txt && npx tsx setup/index.ts --step whatsapp-auth -- --method pairing-code --phone <their-phone-number> > /tmp/wa-auth.log 2>&1 &
rm -f store/pairing-code.txt && pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method pairing-code --phone <their-phone-number> > /tmp/wa-auth.log 2>&1 &
```
Then immediately poll for the code (do NOT wait for the background command to finish):
@@ -221,8 +221,8 @@ node -e "const c=JSON.parse(require('fs').readFileSync('store/auth/creds.json','
**Group (solo, existing):** Run group sync and list available groups:
```bash
npx tsx setup/index.ts --step groups
npx tsx setup/index.ts --step groups --list
pnpm exec tsx setup/index.ts --step groups
pnpm exec tsx setup/index.ts --step groups --list
```
The output shows `JID|GroupName` pairs. Present candidates as AskUserQuestion (names only, not JIDs).
@@ -230,7 +230,7 @@ The output shows `JID|GroupName` pairs. Present candidates as AskUserQuestion (n
### Register the chat
```bash
npx tsx setup/index.ts --step register \
pnpm exec tsx setup/index.ts --step register \
--jid "<jid>" \
--name "<chat-name>" \
--trigger "@<trigger>" \
@@ -244,7 +244,7 @@ npx tsx setup/index.ts --step register \
For additional groups (trigger-required):
```bash
npx tsx setup/index.ts --step register \
pnpm exec tsx setup/index.ts --step register \
--jid "<group-jid>" \
--name "<group-name>" \
--trigger "@<trigger>" \
@@ -257,7 +257,7 @@ npx tsx setup/index.ts --step register \
### Build and restart
```bash
npm run build
pnpm run build
```
Restart the service:
@@ -296,7 +296,7 @@ tail -f logs/nanoclaw.log
QR codes expire after ~60 seconds. Re-run the auth command:
```bash
rm -rf store/auth/ && npx tsx src/whatsapp-auth.ts
rm -rf store/auth/ && pnpm exec tsx src/whatsapp-auth.ts
```
### Pairing code not working
@@ -304,7 +304,7 @@ rm -rf store/auth/ && npx tsx src/whatsapp-auth.ts
Codes expire in ~60 seconds. To retry:
```bash
rm -rf store/auth/ && npx tsx src/whatsapp-auth.ts --pairing-code --phone <phone>
rm -rf store/auth/ && pnpm exec tsx src/whatsapp-auth.ts --pairing-code --phone <phone>
```
Enter the code **immediately** when it appears. Also ensure:
@@ -315,7 +315,7 @@ Enter the code **immediately** when it appears. Also ensure:
If pairing code keeps failing, switch to QR-browser auth instead:
```bash
rm -rf store/auth/ && npx tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
rm -rf store/auth/ && pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method qr-browser
```
### "conflict" disconnection
@@ -340,25 +340,25 @@ Check:
Run group metadata sync:
```bash
npx tsx setup/index.ts --step groups
pnpm exec tsx setup/index.ts --step groups
```
This fetches all group names from WhatsApp. Runs automatically every 24 hours.
## After Setup
If running `npm run dev` while the service is active:
If running `pnpm run dev` while the service is active:
```bash
# macOS:
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
npm run dev
pnpm run dev
# When done testing:
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
# Linux:
# systemctl --user stop nanoclaw
# npm run dev
# pnpm run dev
# systemctl --user start nanoclaw
```
@@ -369,4 +369,4 @@ To remove WhatsApp integration:
1. Delete auth credentials: `rm -rf store/auth/`
2. Remove WhatsApp registrations: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid LIKE '%@g.us' OR jid LIKE '%@s.whatsapp.net'"`
3. Sync env: `mkdir -p data/env && cp .env data/env/env`
4. Rebuild and restart: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux)
4. Rebuild and restart: `pnpm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `pnpm run build && systemctl --user restart nanoclaw` (Linux)
+8 -8
View File
@@ -51,12 +51,12 @@ git fetch upstream skill/channel-formatting
git merge upstream/skill/channel-formatting
```
If there are merge conflicts on `package-lock.json`, resolve them by accepting the incoming
If there are merge conflicts on `pnpm-lock.yaml`, resolve them by accepting the incoming
version and continuing:
```bash
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
```
@@ -74,9 +74,9 @@ This merge adds:
### Validate
```bash
npm install
npm run build
npx vitest run src/formatting.test.ts
pnpm install
pnpm run build
pnpm exec vitest run src/formatting.test.ts
```
All 73 tests should pass and the build should be clean before continuing.
@@ -86,7 +86,7 @@ All 73 tests should pass and the build should be clean before continuing.
### Rebuild and restart
```bash
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
```
@@ -133,5 +133,5 @@ git checkout upstream/main -- src/router.ts
# Revert the index.ts sendMessage call sites to plain formatOutbound(rawText)
# (edit manually or: git checkout upstream/main -- src/index.ts)
npm run build
pnpm run build
```
@@ -80,8 +80,8 @@ If the merge reports conflicts, resolve them by reading the conflicted files and
### Validate code changes
```bash
npm test
npm run build
pnpm test
pnpm run build
```
All tests must pass and build must be clean before proceeding.
@@ -172,7 +172,7 @@ Expected: Both operations succeed.
### Full integration test
```bash
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
```
+41 -1
View File
@@ -54,6 +54,46 @@ Implementation:
1. Add MCP server config to the container settings (see `src/container-runner.ts` for how MCP servers are mounted)
2. Document available tools in `groups/CLAUDE.md`
### Changing Provider or Model (per agent group)
Each agent group picks its own provider (`claude` | `opencode` | `mock`) and its own model via `providers.<name>.model` in `groups/<folder>/container.json`. There are two paths depending on whether the operator is at the keyboard or wants the agent to change itself from chat.
Questions to ask:
- Which group? (look up by name or folder)
- Switch provider, change model, or both?
- New model id (provider-specific — e.g. `claude-sonnet-4-5`, `openrouter/anthropic/claude-sonnet-4`, `opencode/big-pickle`)
**From Claude Code (operator editing files):**
1. For provider switch: `sqlite3 data/v2.db "UPDATE agent_groups SET agent_provider = '<name>' WHERE folder = '<folder>'"`.
2. For model: edit `groups/<folder>/container.json` and merge into `providers.<provider>`:
```jsonc
{ "providers": { "claude": { "model": "claude-sonnet-4-5" } } }
```
3. Bounce the session's container (kill it or wait for idle) so the next wake picks up the new env.
**From chat (agent reconfigures itself):**
Tell the agent (or let it propose it): "use the `set_provider_config` tool to switch my model to X." The tool writes a system action that triggers an approval card to an admin; after approval, the host writes `container.json` and kills the container so the next message spawns with the new env. For provider switching, the DB column still needs a direct edit (no self-mod tool for `agent_groups.agent_provider` yet).
Shape of the per-group config (what the tool merges into):
```jsonc
// groups/<folder>/container.json
{
"providers": {
"claude": { "model": "claude-sonnet-4-5" },
"opencode": {
"innerProvider": "openrouter",
"model": "openrouter/anthropic/claude-sonnet-4",
"smallModel": "openrouter/anthropic/claude-haiku-4.5"
}
}
}
```
Fallback chain: per-group config → host `.env` (`ANTHROPIC_MODEL` / `OPENCODE_*`) → provider default.
### Changing Assistant Behavior
Questions to ask:
@@ -91,7 +131,7 @@ Implementation:
Always tell the user:
```bash
# Rebuild and restart
npm run build
pnpm run build
# macOS:
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
+2 -2
View File
@@ -41,7 +41,7 @@ Set `LOG_LEVEL=debug` for verbose output:
```bash
# For development
LOG_LEVEL=debug npm run dev
LOG_LEVEL=debug pnpm run dev
# For launchd service (macOS), add to plist EnvironmentVariables:
<key>LOG_LEVEL</key>
@@ -231,7 +231,7 @@ query({
```bash
# Rebuild main app
npm run build
pnpm run build
# Rebuild container (use --no-cache for clean rebuild)
./container/build.sh
+131
View File
@@ -0,0 +1,131 @@
---
name: init-first-agent
description: Walk the operator through creating the first NanoClaw v2 agent for a DM channel — resolve the operator's channel identity, wire the DM messaging group to a new agent, and trigger a welcome DM via the normal delivery path. Use after channel credentials are configured and the service is running.
---
# Init First Agent
Stand up the first NanoClaw v2 agent for a channel and verify end-to-end delivery by having the agent DM the operator. Everything the skill does is idempotent — rerunning is safe.
## Prerequisites
- **Service running.** Check: `launchctl list | grep nanoclaw` (macOS) or `systemctl --user status nanoclaw` (Linux). If stopped, tell the user to run `/setup` first.
- **Target channel installed.** At least one `/add-<channel>-v2` skill has run, credentials are in `.env`, and the adapter is uncommented in `src/channels/index.ts`.
- **Adapter connected.** Tail `logs/nanoclaw.log` — look for a recent `channel setup` / `adapter connected` line for the target channel.
## 1. Pick the channel
Read `src/channels/index.ts` to find enabled channels (uncommented imports). Cross-check `.env` for the relevant credentials.
AskUserQuestion: "Which channel should host the welcome DM?" with one option per enabled channel (Discord, Slack, Telegram, WhatsApp, Webex, Teams, Google Chat, Matrix, iMessage, Resend, …).
Record the choice as `CHANNEL` (lowercase, e.g. `discord`).
## 2. Ask for the operator's identity
Read the channel's own skill for its `## Channel Info > how-to-find-id` section (e.g. `.claude/skills/add-discord-v2/SKILL.md`, `.claude/skills/add-telegram-v2/SKILL.md`). Show those instructions to the user in plain text.
Then ask in plain text (NOT `AskUserQuestion` — these are free-form):
1. **Your user id on this channel** — e.g. a Discord user ID, Telegram user ID, Slack user ID. Record as `USER_HANDLE`.
2. **Your display name** — human name, used to name the agent group (`dm-with-<normalized>`) and as the welcome-message addressee. Record as `DISPLAY_NAME`.
3. **Agent persona name** — the assistant's display name. Default: `DISPLAY_NAME`. Record as `AGENT_NAME`.
Then use `AskUserQuestion` for the agent backend:
4. **Provider** — "Which agent provider should this agent use?" with options `claude` (default, Anthropic Agent SDK) and `opencode` (OpenRouter / Anthropic / etc. via OpenCode — assumes `/add-opencode` has been run). Record as `PROVIDER`.
Then ask in plain text (free-form — model ids are provider-specific strings):
5. **Model** — "Which model id? (Leave blank for the provider default.)" For claude: values like `claude-sonnet-4-5`, `claude-opus-4-5`. For opencode: values like `openrouter/anthropic/claude-sonnet-4`, `opencode/big-pickle`. Record as `MODEL` (may be empty).
## 3. Resolve the DM platform id
This depends on whether the channel supports cold DM via `adapter.openDM`.
**Channels without cold DM (direct-addressable): telegram, whatsapp, imessage, matrix, resend.** The user handle doubles as the DM chat id. Set:
```
PLATFORM_ID=${CHANNEL}:${USER_HANDLE}
```
Skip to step 4.
**Channels with cold DM (resolution-required): discord, slack, teams, webex, gchat.** The bot can DM cold at runtime via Chat SDK, but this skill runs standalone — it can't call the adapter. Two resolutions:
### 3a. User DMs the bot once (Discord / Slack / Teams / Webex / gChat)
Tell the user:
> Send any single message to the bot as a DM from your account on `${CHANNEL}`. The router will record the DM as a messaging group. Reply `done` here when you've sent the message.
Wait for the user's confirmation. Then look up the most recent DM messaging groups:
```bash
sqlite3 data/v2.db "SELECT id, platform_id, name, created_at FROM messaging_groups WHERE channel_type='${CHANNEL}' AND is_group=0 ORDER BY created_at DESC LIMIT 5"
```
Show the top rows to the user and confirm which `platform_id` is theirs (usually the most recent). Record as `PLATFORM_ID`. If none appeared, check `logs/nanoclaw.log` for `unknown_sender` drops — the adapter might be rejecting inbound due to connection or permission issues.
### 3b. Telegram pair-code path (if the user prefers not to DM first)
For Telegram only, there's an existing pair-code primitive. When you run this tool, take the output and extract the pairing code. Then show it to the user in plain text and ask the user to send the code in the Telegram chat to complete the pairing.
```bash
npx tsx setup/index.ts --step pair-telegram -- --intent new-agent:dm-with-<folder>
```
Parse the `PAIR_TELEGRAM_ISSUED` status block for `CODE` and follow the `REMINDER_TO_ASSISTANT` line in that block. Then wait for the `PAIR_TELEGRAM` block — read `PLATFORM_ID` and `PAIRED_USER_ID` from it. telegram.ts's interceptor has already upserted the user and granted owner if none existed yet. Use `PLATFORM_ID` and `PAIRED_USER_ID` directly in step 4.
## 4. Run the init script
```bash
npx tsx scripts/init-first-agent.ts \
--channel "${CHANNEL}" \
--user-id "${CHANNEL}:${USER_HANDLE}" \
--platform-id "${PLATFORM_ID}" \
--display-name "${DISPLAY_NAME}" \
--agent-name "${AGENT_NAME}" \
--provider "${PROVIDER}" \
${MODEL:+--model "${MODEL}"}
```
Pass `--provider` even when the user picked the default `claude`, so the DB column is set explicitly rather than left null. Omit `--model` entirely if the user left it blank — the provider will fall back to the host env / SDK default. Add `--welcome "System instruction: ..."` to override the default welcome prompt.
The script:
1. Upserts the `users` row and grants `owner` role if no owner exists.
2. Creates the `agent_groups` row and calls `initGroupFilesystem` at `groups/dm-with-<name>/`.
3. Reuses or creates the DM `messaging_groups` row.
4. Wires them via `messaging_group_agents` (which auto-creates the companion `agent_destinations` row).
5. Resolves the session (creates `inbound.db` / `outbound.db`).
6. Writes a `kind: 'chat'`, `sender: 'system'` welcome message into `inbound.db`.
Show the script's output to the user.
## 5. Verify
Host sweep runs every ~60s. Within one sweep window the container wakes, the agent processes the system message, and the reply flows through `outbound.db` to the channel.
Do not tail the log or poll in a sleep loop. Ask the user in plain text:
> The welcome DM should arrive within ~60 seconds. Let me know when you've received it (or if it doesn't arrive within two minutes).
Wait for the user's reply. If they confirm receipt, the skill is done.
If they say it didn't arrive, then diagnose using the DB directly (no waiting loops required — the message either delivered or it didn't):
- `sqlite3 data/v2-sessions/<agent-group-id>/sessions/<session-id>/outbound.db "SELECT id, status, created_at FROM messages_out ORDER BY created_at DESC LIMIT 5"` — check for stuck `pending` rows. Replace `<agent-group-id>` and `<session-id>` with the values from the script's output.
- `grep -E 'Unauthorized channel destination|container.*exited|error' logs/nanoclaw.log | tail -20` — look for ACL rejections or container crashes.
- `ls data/v2-sessions/<agent-group-id>/sessions/*/outbound.db` — confirm the session exists.
## Troubleshooting
**"Missing required args"** — the script wants `--channel`, `--user-id`, `--platform-id`, `--display-name` at minimum. Re-check the command you assembled.
**No `messaging_groups` row appears after the user DMs (step 3a)** — the router silently drops messages from unknown senders under `strict` policy but still creates the `messaging_groups` row. If the row is missing entirely, the adapter isn't receiving the inbound message. Check `logs/nanoclaw.log` for adapter errors (auth, gateway disconnect, rate limit).
**Owner already exists** — `hasAnyOwner()` returned true, so the grant is skipped silently. That's fine; the script still creates the agent and wiring. Reassigning ownership needs a separate flow (not this skill).
**Wrong person got the welcome DM** — the `--platform-id` you passed is someone else's DM channel. Rerun with the correct one; the script is idempotent on user/messaging-group/agent-group but writes a new session welcome each run.
**Agent group name collision** — if `dm-with-<display-name>` already exists (e.g. rerunning with the same display name), the script reuses it. Pass a different `--display-name` to get a distinct folder.
+11 -17
View File
@@ -17,13 +17,7 @@ This skill installs OneCLI, configures the Agent Vault gateway, and migrates any
onecli version 2>/dev/null
```
If the command succeeds, OneCLI is installed. Check if the gateway is reachable:
```bash
curl -sf http://127.0.0.1:10254/health
```
If both succeed, check for an Anthropic secret:
If the command succeeds, OneCLI is installed, check for an Anthropic secret:
```bash
onecli secrets list
@@ -81,16 +75,16 @@ Re-verify with `onecli version`.
### Configure the CLI
Point the CLI at the local OneCLI instance:
Point the CLI at the local OneCLI instance, the ONECLI_URL was output from the install script above:
```bash
onecli config set api-host http://127.0.0.1:10254
onecli config set api-host ${ONECLI_URL}
```
### Set ONECLI_URL in .env
```bash
grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=http://127.0.0.1:10254' >> .env
grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=${ONECLI_URL}' >> .env
```
### Wait for gateway readiness
@@ -99,7 +93,7 @@ The gateway may take a moment to start after installation. Poll for up to 15 sec
```bash
for i in $(seq 1 15); do
curl -sf http://127.0.0.1:10254/health && break
curl -sf ${ONECLI_URL}/health && break
sleep 1
done
```
@@ -214,7 +208,7 @@ Tell the user to run `claude setup-token` in another terminal and copy the token
Once they have the token, AskUserQuestion with two options:
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI. Use type 'anthropic' and paste your token as the value."
1. **Dashboard** — description: "Best if you have a browser on this machine. Open ${ONECLI_URL} and add the secret in the UI. Use type 'anthropic' and paste your token as the value."
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_TOKEN --host-pattern api.anthropic.com`"
#### API key path
@@ -223,7 +217,7 @@ Tell the user to get an API key from https://console.anthropic.com/settings/keys
AskUserQuestion with two options:
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI."
1. **Dashboard** — description: "Best if you have a browser on this machine. Open ${ONECLI_URL} and add the secret in the UI."
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_KEY --host-pattern api.anthropic.com`"
#### After either path
@@ -237,10 +231,10 @@ Ask them to let you know when done.
## Phase 4: Build and restart
```bash
npm run build
pnpm run build
```
If build fails, diagnose and fix. Common issue: `@onecli-sh/sdk` not installed — run `npm install` first.
If build fails, diagnose and fix. Common issue: `@onecli-sh/sdk` not installed — run `pnpm install` first.
Restart the service:
- macOS (launchd): `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`
@@ -262,12 +256,12 @@ If the service is running and a channel is configured, tell the user to send a t
Tell the user:
- OneCLI Agent Vault is now managing credentials
- Agents never see raw API keys — credentials are injected at the gateway level
- To manage secrets: `onecli secrets list`, or open http://127.0.0.1:10254
- To manage secrets: `onecli secrets list`, or open ${ONECLI_URL}
- To add rate limits or policies: `onecli rules create --help`
## Troubleshooting
**"OneCLI gateway not reachable" in logs:** The gateway isn't running. Check with `curl -sf http://127.0.0.1:10254/health`. Start it with `onecli start` if needed.
**"OneCLI gateway not reachable" in logs:** The gateway isn't running. Check with `curl -sf ${ONECLI_URL}/health`. Start it with `onecli start` if needed.
**Container gets no credentials:** Verify `ONECLI_URL` is set in `.env` and the gateway has an Anthropic secret (`onecli secrets list`).
+87
View File
@@ -0,0 +1,87 @@
---
name: manage-channels
description: Wire channels to agent groups, manage isolation levels, add new channel groups. Use after adding a channel, during setup, or standalone to reconfigure.
---
# Manage Channels
Wire messaging channels to agent groups. See `docs/v2-isolation-model.md` for the full isolation model.
Privilege is a **user-level** concept, not a channel-level one (see `src/db/user-roles.ts`, `src/access.ts`). There is no "main channel" / "main group" — any user can be granted `owner` or `admin` (global or scoped to an agent group) via `grantRole()`, and messages from unknown senders are gated per-messaging-group by `unknown_sender_policy` (`strict` | `request_approval` | `public`).
## Assess Current State
Read the v2 central DB (`data/v2.db`) — query `agent_groups`, `messaging_groups`, `messaging_group_agents`, `users`, and `user_roles` tables. Also check `.env` for channel tokens and `src/channels/index.ts` for uncommented imports.
Categorize channels as: **wired** (has DB entities + messaging_group_agents row), **configured but unwired** (has credentials + barrel import, no DB entities), or **not configured**.
If the instance has no owner yet (`SELECT COUNT(*) FROM user_roles WHERE role='owner' AND agent_group_id IS NULL` returns 0), tell the user they should run `/init-first-agent` first — it stands up the first agent group, promotes the operator to owner, and verifies delivery end-to-end by having the agent DM them. Then return here for any additional channels/groups.
## First Channel (No Agent Groups Exist)
**Delegate to `/init-first-agent`.** It handles: channel choice, operator identity lookup, DM platform id resolution (with cold-DM or pair-code fallback), agent group creation, wiring, and the welcome DM. Return here afterward for any additional channels.
## Wire New Channel
For each unwired channel:
1. Read its SKILL.md `## Channel Info` for terminology, how-to-find-id, typical-use, and default-isolation
2. Ask for the platform ID using the platform's terminology
3. Ask the isolation question (see below)
4. Register with the appropriate flags
### Isolation Question
Present a multiple-choice with a contextual recommendation. The three options:
- **Same conversation** (`--session-mode "agent-shared"` + existing folder) — all messages land in one session. Recommend for webhook + chat combos (GitHub + Slack).
- **Same agent, separate conversations** (`--session-mode "shared"` + existing folder) — shared workspace/memory, independent threads. Recommend for same user across platforms.
- **Separate agent** (new `--folder`) — full isolation. Recommend when different people are involved.
Use the channel's `typical-use` and `default-isolation` fields to pick the recommendation. Offer to explain more if the user is unsure — reference `docs/v2-isolation-model.md` for the detailed explanation.
### Register Command
```bash
pnpm exec tsx setup/index.ts --step register -- \
--platform-id "<id>" --name "<name>" \
--folder "<folder>" --channel "<type>" \
--session-mode "<shared|agent-shared|per-thread>" \
--assistant-name "<name>"
```
The `register` step creates the agent group (reusing it if the folder already exists), the messaging group, and the wiring row. `createMessagingGroupAgent` auto-creates the companion `agent_destinations` row so the agent can address the channel by name — no separate destination step needed.
For separate agents, also ask for a folder name and optionally a different assistant name.
## Add Channel Group
When adding another group/chat on an already-configured platform (e.g. a second Telegram group):
1. **Telegram:** ask the isolation question first to determine intent (`wire-to:<folder>` for an existing agent, `new-agent:<folder>` for a fresh one). Run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent <intent>`, show the CODE (follow the `REMINDER_TO_ASSISTANT` line in the `PAIR_TELEGRAM_ISSUED` block) and tell the user to post `@<botname> CODE` in the target group (or DM the bot for a private chat). Wait for the `PAIR_TELEGRAM` block. The inbound interceptor has already created the `messaging_groups` row with `unknown_sender_policy = 'strict'` and upserted the paired user — `register` only needs to add the wiring:
```bash
pnpm exec tsx setup/index.ts --step register -- \
--platform-id "<PLATFORM_ID>" --name "<group-name>" \
--folder "<folder>" --channel "telegram" \
--session-mode "<shared|agent-shared|per-thread>" \
--assistant-name "<name>"
```
2. **Other channels:** read the channel's SKILL.md `## Channel Info` for terminology and how-to-find-id. Ask for the new group/chat ID, ask the isolation question, then register. No package or credential changes needed.
## Change Wiring
1. Show current wiring (agent_groups × messaging_group_agents)
2. Ask which channel to move and to which agent group
3. Delete the old `messaging_group_agents` entry, create a new one
4. Note: existing sessions stay with the old agent group; new messages route to the new one. The `agent_destinations` row created for the old wiring is NOT automatically removed — if you want the old agent to stop seeing the channel as a named target, delete it from `agent_destinations` manually.
## Show Configuration
Display a readable summary showing:
- **Agent groups** with their wired channels (from `messaging_group_agents`)
- **Configured-but-unwired** channels (credentials present, no DB entities)
- **Unconfigured** channels
- **Privileged users**: `SELECT user_id, role, agent_group_id FROM user_roles ORDER BY role='owner' DESC`
+47
View File
@@ -0,0 +1,47 @@
---
name: manage-mounts
description: Configure which host directories agent containers can access. View, add, or remove mount allowlist entries. Triggers on "mounts", "mount allowlist", "agent access to directories", "container mounts".
---
# Manage Mounts
Configure which host directories NanoClaw agent containers can access. The mount allowlist lives at `~/.config/nanoclaw/mount-allowlist.json`.
## Show Current Config
```bash
cat ~/.config/nanoclaw/mount-allowlist.json 2>/dev/null || echo "No mount allowlist configured"
```
Show the current config to the user in a readable format: which directories are allowed, whether non-main agents are read-only.
## Add Directories
Ask which directories the user wants agents to access. For each path:
- Validate the path exists
- Ask if it should be read-only for non-main agents (default: yes)
Build the JSON config and write it:
```bash
npx tsx setup/index.ts --step mounts --force -- --json '{"allowedRoots":[{"path":"/path/to/dir","readOnly":false}],"blockedPatterns":[],"nonMainReadOnly":true}'
```
Use `--force` to overwrite the existing config.
## Remove Directories
Read the current config, show it, ask which entry to remove, write the updated config.
## Reset to Empty
```bash
npx tsx setup/index.ts --step mounts --force -- --empty
```
## After Changes
Restart the service so containers pick up the new config:
- macOS: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`
- Linux: `systemctl --user restart nanoclaw`
@@ -68,7 +68,7 @@ Source: `src/db.ts`
Insert directly into the SQLite database. This requires groups to be registered first (Phase 1). Use the registered group's `folder` and `chat_jid`:
```bash
npx tsx -e "
pnpm exec tsx -e "
const Database = require('better-sqlite3');
const { CronExpressionParser } = require('cron-parser');
const db = new Database('store/messages.db');
@@ -36,7 +36,7 @@ Keep it factual and terse — this is for machine recovery after compaction, not
Run the discovery script to find and summarize the OpenClaw installation:
```bash
npx tsx ${CLAUDE_SKILL_DIR}/scripts/discover-openclaw.ts
pnpm exec tsx ${CLAUDE_SKILL_DIR}/scripts/discover-openclaw.ts
```
If the user specifies a custom path, pass it: `--state-dir <path>`
@@ -106,7 +106,7 @@ The discovery script provides detected groups in the GROUPS field (format: `chan
For each group the user wants to bring over, pre-register it:
```bash
npx tsx setup/index.ts --step register -- --jid "<nanoclaw_jid>" --name "<group_name>" --folder "<channel>_<slug>" --trigger "@<confirmed_name>" --channel <channel> --assistant-name "<confirmed_name>"
pnpm exec tsx setup/index.ts --step register -- --jid "<nanoclaw_jid>" --name "<group_name>" --folder "<channel>_<slug>" --trigger "@<confirmed_name>" --channel <channel> --assistant-name "<confirmed_name>"
```
Only pass `--assistant-name` on the first registration (it updates all CLAUDE.md templates globally).
@@ -115,7 +115,7 @@ Folder naming: `<channel>_<name-slug>` (e.g. `whatsapp_family-chat`, `telegram_d
For the first/primary group, add `--is-main --no-trigger-required`. Other groups default to requiring a trigger prefix.
**Important:** Registration requires the database to exist. If the environment step hasn't been run yet, run it first: `npx tsx setup/index.ts --step environment`. Registration also creates the group folder under `groups/` and copies the CLAUDE.md template.
**Important:** Registration requires the database to exist. If the environment step hasn't been run yet, run it first: `pnpm exec tsx setup/index.ts --step environment`. Registration also creates the group folder under `groups/` and copies the CLAUDE.md template.
Register groups from all channels — including channels NanoClaw doesn't yet support (signal, matrix, etc.). The registration stores the JID and metadata in the database, ready for when that channel is added later. Groups won't receive messages until their channel code is installed, but the registration, group folder, and CLAUDE.md will be ready.
@@ -295,7 +295,7 @@ For each detected plugin, present the name to the user and discuss whether to se
1. **If NanoClaw has a matching skill** — check the available NanoClaw skills list for an equivalent (e.g. `/add-voice-transcription` for whisper). If found, save the API key to `.env` and invoke that skill.
2. **If the OpenClaw plugin was an MCP server** — read its config to find the exact package name and command. Install the same MCP server (e.g. `npx -y <exact-package-from-config>`). Don't search for or guess at MCP packages — only install what was explicitly configured.
2. **If the OpenClaw plugin was an MCP server** — read its config to find the exact package name and command. Install the same MCP server (e.g. `pnpm dlx <exact-package-from-config>`). Don't search for or guess at MCP packages — only install what was explicitly configured.
3. **If the OpenClaw plugin was a CLI tool** — read the config to identify the exact tool. If it's an npm package, add it to the container's Dockerfile. Add a note to the group's CLAUDE.md that the tool is available and how to invoke it.
@@ -324,7 +324,7 @@ Run the credential extraction script with `--write-env .env` so it writes creden
First, run without `--write-env` to preview:
```bash
npx tsx ${CLAUDE_SKILL_DIR}/scripts/extract-channel-credentials.ts --state-dir <STATE_DIR> --channel <name>
pnpm exec tsx ${CLAUDE_SKILL_DIR}/scripts/extract-channel-credentials.ts --state-dir <STATE_DIR> --channel <name>
```
Parse the status block. Key fields: HAS_CREDENTIAL, CREDENTIAL_MASKED, NANOCLAW_ENV_VAR.
@@ -339,7 +339,7 @@ If HAS_CREDENTIAL=true: Show the masked credential (`CREDENTIAL_MASKED`). AskUse
If using the credential:
```bash
npx tsx ${CLAUDE_SKILL_DIR}/scripts/extract-channel-credentials.ts --state-dir <STATE_DIR> --channel <name> --write-env .env
pnpm exec tsx ${CLAUDE_SKILL_DIR}/scripts/extract-channel-credentials.ts --state-dir <STATE_DIR> --channel <name> --write-env .env
```
The script writes the credential directly to `.env` using the correct NanoClaw variable name (e.g. `TELEGRAM_BOT_TOKEN`). Check the status block for `WRITTEN_TO` and `WRITTEN_COUNT` to confirm.
@@ -1,7 +1,7 @@
/**
* Discover an existing OpenClaw installation and emit a structured summary.
*
* Usage: npx tsx .claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts [--state-dir <path>]
* Usage: pnpm exec tsx .claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts [--state-dir <path>]
*
* Checks (in order): --state-dir arg, $OPENCLAW_STATE_DIR, ~/.openclaw, ~/.clawdbot
* Parses openclaw.json (JSON5-tolerant), scans workspace for identity/memory files,
@@ -2,7 +2,7 @@
* Extract a channel credential from an OpenClaw configuration and write it
* directly to the NanoClaw .env file.
*
* Usage: npx tsx .claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts \
* Usage: pnpm exec tsx .claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts \
* --channel telegram --state-dir ~/.openclaw --write-env .env
*
* Handles OpenClaw SecretRef formats:
+3 -3
View File
@@ -391,7 +391,7 @@ For behavior customizations (CLAUDE.md files): copy from the main tree. These ar
## 2.6 Validate in worktree
```bash
cd "$WORKTREE" && npm install && npm run build && npm test
cd "$WORKTREE" && pnpm install && pnpm run build && pnpm test
```
If build fails, show the error. Fix only issues caused by the migration. If unclear, ask the user.
@@ -417,7 +417,7 @@ If testing live:
ln -s "$PROJECT_ROOT/.env" "$WORKTREE/.env"
```
3. Start from worktree: `cd "$WORKTREE" && npm run dev`
3. Start from worktree: `cd "$WORKTREE" && pnpm run dev`
4. Ask the user to send a test message from their phone. Wait for them to confirm it works.
@@ -461,7 +461,7 @@ Do NOT use `git checkout -B` to create an intermediate branch — this caused is
## 2.9 Post-upgrade
Run `npm install && npm run build` in the main tree to confirm.
Run `npm install && pnpm run build` in the main tree to confirm.
Restart the service:
```bash
+143 -135
View File
@@ -5,50 +5,41 @@ description: Run initial NanoClaw setup. Use when user wants to install dependen
# NanoClaw Setup
Run setup steps automatically. Only pause when user action is required (channel authentication, configuration choices). Setup uses `bash setup.sh` for bootstrap, then `npx tsx setup/index.ts --step <name>` for all other steps. Steps emit structured status blocks to stdout. Verbose logs go to `logs/setup.log`.
Welcome the user to NanoClaw. Introduce yourself — you'll be walking them through the entire setup process step by step, from installing dependencies to getting their first message through. Keep it warm and brief (2-3 sentences).
**Principle:** When something is broken or missing, fix it. Don't tell the user to go fix it themselves unless it genuinely requires their manual action (e.g. authenticating a channel, pasting a secret token). If a dependency is missing, install it. If a service won't start, diagnose and repair. Ask the user for permission when needed, then do the work.
Then explain that setup involves running many shell commands (installing packages, building containers, starting services), and recommend pre-approving the standard setup commands so they don't have to confirm each one individually.
**UX Note:** Use `AskUserQuestion` for multiple-choice questions only (e.g. "Docker or Apple Container?", "which channels?"). Do NOT use it when free-text input is needed (e.g. phone numbers, tokens, paths) — just ask the question in plain text and wait for the user's reply.
Use `AskUserQuestion` with these options:
## 0. Git & Fork Setup
1. **Pre-approve (recommended)** — description: "Pre-approve standard setup commands so you don't have to confirm each one. You can review the list first if you'd like."
2. **No thanks** — description: "I'll approve each command individually as it comes up."
3. **Show me the list first** — description: "Show me exactly which commands will be pre-approved before I decide."
Check the git remote configuration to ensure the user has a fork and upstream is configured.
If they pick option 1: read `.claude/skills/setup/setup-permissions.json`, then read the project settings file at `.claude/settings.json` (create it if it doesn't exist with `{}`), and directly edit it to add/merge the permissions into the `permissions.allow` array. Do NOT use the `update-config` skill.
Run:
- `git remote -v`
If they pick option 3: read and display `.claude/skills/setup/setup-permissions.json`, then re-ask with just options 1 and 2.
**Case A — `origin` points to `qwibitai/nanoclaw` (user cloned directly):**
If they decline, continue — they'll approve commands individually.
The user cloned instead of forking. AskUserQuestion: "You cloned NanoClaw directly. We recommend forking so you can push your customizations. Would you like to set up a fork?"
- Fork now (recommended) — walk them through it
- Continue without fork — they'll only have local changes
---
**Internal guidance (do not show to user):**
- Run setup steps automatically. Only pause when user action is required (channel authentication, configuration choices).
- Setup uses `bash setup.sh` for bootstrap, then `npx tsx setup/index.ts --step <name>` for all other steps. Steps emit structured status blocks to stdout. Verbose logs go to `logs/setup.log`.
- **Principle:** When something is broken or missing, fix it. Don't tell the user to go fix it themselves unless it genuinely requires their manual action (e.g. authenticating a channel, pasting a secret token). If a dependency is missing, install it. If a service won't start, diagnose and repair.
- **UX Note:** Use `AskUserQuestion` for multiple-choice questions only (e.g. "which credential method?"). Do NOT use it when free-text input is needed (e.g. phone numbers, tokens, paths) — just ask the question in plain text and wait for the user's reply.
- **Timeouts:** Use 5m timeouts for install and build steps.
- **Waiting on user:** When the user needs to do something (change a setting, get a token, open a browser, etc.), stop and wait. Give clear instructions, then say "Let me know when done or if you need help." Do NOT continue to the next step. If they ask for help, give more detail, ask where they got stuck, and try to assist.
## 0. Git Upstream
Ensure `upstream` remote points to `qwibitai/nanoclaw`. If missing, add it silently:
If fork: instruct the user to fork `qwibitai/nanoclaw` on GitHub (they need to do this in their browser), then ask them for their GitHub username. Run:
```bash
git remote rename origin upstream
git remote add origin https://github.com/<their-username>/nanoclaw.git
git push --force origin main
git remote -v
git remote add upstream https://github.com/qwibitai/nanoclaw.git 2>/dev/null || true
```
Verify with `git remote -v`.
If continue without fork: add upstream so they can still pull updates:
```bash
git remote add upstream https://github.com/qwibitai/nanoclaw.git
```
**Case B — `origin` points to user's fork, no `upstream` remote:**
Add upstream:
```bash
git remote add upstream https://github.com/qwibitai/nanoclaw.git
```
**Case C — both `origin` (user's fork) and `upstream` (qwibitai) exist:**
Already configured. Continue.
**Verify:** `git remote -v` should show `origin` → user's repo, `upstream``qwibitai/nanoclaw.git`.
## 1. Bootstrap (Node.js + Dependencies)
@@ -64,21 +55,15 @@ Run `bash setup.sh` and parse the status block.
## 2. Check Environment
Run `npx tsx setup/index.ts --step environment` and parse the status block.
Run `pnpm exec tsx setup/index.ts --step environment` and parse the status block.
- If HAS_AUTH=true → WhatsApp is already configured, note for step 5
- If HAS_REGISTERED_GROUPS=true → note existing config, offer to skip or reconfigure
- Record APPLE_CONTAINER and DOCKER values for step 3
- Record DOCKER value for step 3
### OpenClaw Migration Detection
Check for an existing OpenClaw installation:
```bash
ls -d ~/.openclaw 2>/dev/null || ls -d ~/.clawdbot 2>/dev/null
```
If a directory is found, AskUserQuestion:
If OPENCLAW_PATH is not `none` from the environment check above, AskUserQuestion:
1. **Migrate now** — "Import identity, credentials, and settings from OpenClaw before continuing setup."
2. **Fresh start** — "Skip migration and set up NanoClaw from scratch."
@@ -88,61 +73,51 @@ If "Migrate now": invoke `/migrate-from-openclaw`, then return here and continue
## 2a. Timezone
Run `npx tsx setup/index.ts --step timezone` and parse the status block.
Run `pnpm exec tsx setup/index.ts --step timezone` and parse the status block.
- If NEEDS_USER_INPUT=true → The system timezone could not be autodetected (e.g. POSIX-style TZ like `IST-2`). AskUserQuestion: "What is your timezone?" with common options (America/New_York, Europe/London, Asia/Jerusalem, Asia/Tokyo) and an "Other" escape. Then re-run: `npx tsx setup/index.ts --step timezone -- --tz <their-answer>`.
- If NEEDS_USER_INPUT=true → The system timezone could not be autodetected (e.g. POSIX-style TZ like `IST-2`). AskUserQuestion: "What is your timezone?" with common options (America/New_York, Europe/London, Asia/Jerusalem, Asia/Tokyo) and an "Other" escape. Then re-run: `pnpm exec tsx setup/index.ts --step timezone -- --tz <their-answer>`.
- If STATUS=success and RESOLVED_TZ is `UTC` or `Etc/UTC` → confirm with the user: "Your system timezone is UTC — is that correct, or are you on a remote server?" If wrong, ask for their actual timezone and re-run with `--tz`.
- If STATUS=success → Timezone is configured. Note RESOLVED_TZ for reference.
## 3. Container Runtime
## 3. Container Runtime (Docker)
### 3a. Choose runtime
### 3a. Install Docker
Check the preflight results for `APPLE_CONTAINER` and `DOCKER`, and the PLATFORM from step 1.
- PLATFORM=linux → Docker (only option)
- PLATFORM=macos + APPLE_CONTAINER=installed → AskUserQuestion with two options:
1. **Docker (recommended)** — description: "Cross-platform, better credential management, well-tested."
2. **Apple Container (experimental)** — description: "Native macOS runtime. Requires advanced setup."
If Apple Container, run `/convert-to-apple-container` now, then skip to 3c.
- PLATFORM=macos + APPLE_CONTAINER=not_found → Docker
### 3a-docker. Install Docker
- DOCKER=running → continue to 4b
- DOCKER=running → continue to step 4
- DOCKER=installed_not_running → start Docker: `open -a Docker` (macOS) or `sudo systemctl start docker` (Linux). Wait 15s, re-check with `docker info`.
- DOCKER=not_found → Use `AskUserQuestion: Docker is required for running agents. Would you like me to install it?` If confirmed:
- macOS: install via `brew install --cask docker`, then `open -a Docker` and wait for it to start. If brew not available, direct to Docker Desktop download at https://docker.com/products/docker-desktop
- Linux: install with `curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $USER`. Note: user may need to log out/in for group membership.
### 3b. Apple Container conversion gate (if needed)
### 3b. CJK fonts
**If the chosen runtime is Apple Container**, you MUST check whether the source code has already been converted from Docker to Apple Container. Do NOT skip this step. Run:
Agent containers skip CJK fonts by default (~200MB saved). Without them, Chromium-rendered screenshots and PDFs show tofu for Chinese/Japanese/Korean.
- **User writing to you in Chinese, Japanese, or Korean** → enable without asking. Mention it briefly.
- **Resolved timezone from step 2a is a CJK region** (`Asia/Tokyo`, `Asia/Shanghai`, `Asia/Hong_Kong`, `Asia/Taipei`, `Asia/Seoul`) or other signal short of active CJK use → ask: "Enable CJK fonts? Adds ~200MB, lets the agent render CJK in screenshots and PDFs."
- **Otherwise** → skip.
To enable, write `INSTALL_CJK_FONTS=true` to `.env`:
```bash
grep -q "CONTAINER_RUNTIME_BIN = 'container'" src/container-runtime.ts && echo "ALREADY_CONVERTED" || echo "NEEDS_CONVERSION"
grep -q '^INSTALL_CJK_FONTS=' .env && sed -i.bak 's/^INSTALL_CJK_FONTS=.*/INSTALL_CJK_FONTS=true/' .env && rm -f .env.bak || echo 'INSTALL_CJK_FONTS=true' >> .env
```
**If NEEDS_CONVERSION**, the source code still uses Docker as the runtime. You MUST run the `/convert-to-apple-container` skill NOW, before proceeding to the build step.
**If ALREADY_CONVERTED**, the code already uses Apple Container. Continue to 3c.
**If the chosen runtime is Docker**, no conversion is needed. Continue to 3c.
The next step's build picks it up automatically.
### 3c. Build and test
Run `npx tsx setup/index.ts --step container -- --runtime <chosen>` and parse the status block.
Run `pnpm exec tsx setup/index.ts --step container -- --runtime docker` and parse the status block.
**If BUILD_OK=false:** Read `logs/setup.log` tail for the build error.
- Cache issue (stale layers): `docker builder prune -f` (Docker) or `container builder stop && container builder rm && container builder start` (Apple Container). Retry.
- Cache issue (stale layers): `docker builder prune -f`. Retry.
- Dockerfile syntax or missing files: diagnose from the log and fix, then retry.
**If TEST_OK=false but BUILD_OK=true:** The image built but won't run. Check logs — common cause is runtime not fully started. Wait a moment and retry the test.
## 4. Credential System
The credential system depends on the container runtime chosen in step 3.
### 4a. Docker → OneCLI
### 4a. OneCLI
Install OneCLI and its CLI tool:
@@ -162,14 +137,14 @@ grep -q '.local/bin' ~/.zshrc 2>/dev/null || echo 'export PATH="$HOME/.local/bin
Then re-verify with `onecli version`.
Point the CLI at the local OneCLI instance (it defaults to the cloud service otherwise):
Point the CLI at the local OneCLI instance, the ONECLI_URL was output from the install script above:
```bash
onecli config set api-host http://127.0.0.1:10254
onecli config set api-host ${ONECLI_URL}
```
Ensure `.env` has the OneCLI URL (create the file if it doesn't exist):
```bash
grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=http://127.0.0.1:10254' >> .env
grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=${ONECLI_URL}' >> .env
```
Check if a secret already exists:
@@ -194,7 +169,7 @@ Then stop and wait for the user to confirm they have the token. Do NOT proceed u
Once they confirm, they register it with OneCLI. AskUserQuestion with two options:
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI. Use type 'anthropic' and paste your token as the value."
1. **Dashboard** — description: "Best if you have a browser on this machine. Open ${ONECLI_URL} and add the secret in the UI. Use type 'anthropic' and paste your token as the value."
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_TOKEN --host-pattern api.anthropic.com`"
#### API key path
@@ -203,7 +178,7 @@ Tell the user to get an API key from https://console.anthropic.com/settings/keys
Then AskUserQuestion with two options:
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI."
1. **Dashboard** — description: "Best if you have a browser on this machine. Open ${ONECLI_URL} and add the secret in the UI."
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_KEY --host-pattern api.anthropic.com`"
#### After either path
@@ -214,69 +189,65 @@ Ask them to let you know when done.
**After user confirms:** verify with `onecli secrets list` that an Anthropic secret exists. If not, ask again.
### 4b. Apple Container → Native Credential Proxy
Apple Container is not compatible with OneCLI. The credential proxy code is already included in the apple-container branch — do NOT invoke `/use-native-credential-proxy` (it would conflict with already-applied code).
Instead, just configure the credentials in `.env`:
AskUserQuestion: Do you want to use your **Claude subscription** (Pro/Max) or an **Anthropic API key**?
1. **Claude subscription (Pro/Max)** — description: "Uses your existing Claude Pro or Max subscription. Run `claude setup-token` in another terminal to get your token."
2. **Anthropic API key** — description: "Pay-per-use API key from console.anthropic.com."
For subscription: tell the user to run `claude setup-token` in another terminal. Stop and wait for the user to confirm they have completed this step successfully before proceeding.
Once confirmed, add the token to `.env`:
```bash
echo 'CLAUDE_CODE_OAUTH_TOKEN=<their-token>' >> .env
```
For API key: add to `.env`:
```bash
echo 'ANTHROPIC_API_KEY=<their-key>' >> .env
```
Verify the proxy starts: `npm run dev` should show "Credential proxy listening" in the logs.
## 5. Set Up Channels
AskUserQuestion (multiSelect): Which messaging channels do you want to enable?
- WhatsApp (authenticates via QR code or pairing code)
- Telegram (authenticates via bot token from @BotFather)
- Slack (authenticates via Slack app with Socket Mode)
- Discord (authenticates via Discord bot token)
Show the full list of available channels in plain text (do NOT use AskUserQuestion — it limits to 4 options). Ask which one they want to start with. They can add more later with `/customize`.
**Delegate to each selected channel's own skill.** Each channel skill handles its own code installation, authentication, registration, and JID resolution. This avoids duplicating channel-specific logic and ensures JIDs are always correct.
Channels where the agent gets its own identity (name and avatar) are marked as recommended.
For each selected channel, invoke its skill:
1. Discord *(recommended — agent gets own identity)*
2. Slack *(recommended — agent gets own identity)*
3. Telegram *(recommended — agent gets own identity)*
4. Microsoft Teams *(recommended — agent gets own identity)*
5. Webex *(recommended — agent gets own identity)*
6. WhatsApp
7. WhatsApp Cloud API
8. iMessage
9. GitHub
10. Linear
11. Google Chat
12. Resend (email)
13. Matrix
- **WhatsApp:** Invoke `/add-whatsapp`
- **Telegram:** Invoke `/add-telegram`
- **Slack:** Invoke `/add-slack`
- **Discord:** Invoke `/add-discord`
**Delegate to the selected channel's skill.** Each channel skill handles its own package installation, authentication, registration, and configuration.
Each skill will:
1. Install the channel code (via `git merge` of the skill branch)
2. Collect credentials/tokens and write to `.env`
3. Authenticate (WhatsApp QR/pairing, or verify token-based connection)
4. Register the chat with the correct JID format
5. Build and verify
Invoke the matching skill:
**After all channel skills complete**, install dependencies and rebuild — channel merges may introduce new packages:
- **Discord:** Invoke `/add-discord-v2`
- **Slack:** Invoke `/add-slack-v2`
- **Telegram:** Invoke `/add-telegram-v2`
- **GitHub:** Invoke `/add-github-v2`
- **Linear:** Invoke `/add-linear-v2`
- **Microsoft Teams:** Invoke `/add-teams-v2`
- **Google Chat:** Invoke `/add-gchat-v2`
- **WhatsApp Cloud API:** Invoke `/add-whatsapp-cloud-v2`
- **WhatsApp Baileys:** Invoke `/add-whatsapp`
- **Resend:** Invoke `/add-resend-v2`
- **Matrix:** Invoke `/add-matrix-v2`
- **Webex:** Invoke `/add-webex-v2`
- **iMessage:** Invoke `/add-imessage-v2`
The skill will:
1. Install the Chat SDK adapter package
2. Uncomment the channel import in `src/channels/index.ts`
3. Collect credentials/tokens and write to `.env`
4. Build and verify
**After the channel skill completes**, install dependencies and rebuild — channel merges may introduce new packages:
```bash
npm install && npm run build
pnpm install && pnpm run build
```
If the build fails, read the error output and fix it (usually a missing dependency). Then continue to step 6.
If the build fails, read the error output and fix it (usually a missing dependency). Then continue to step 5a.
## 6. Mount Allowlist
AskUserQuestion: Agent access to external directories?
Set empty mount allowlist (agents only access their own workspace). Users can configure mounts later with `/manage-mounts`.
**No:** `npx tsx setup/index.ts --step mounts -- --empty`
**Yes:** Collect paths/permissions. `npx tsx setup/index.ts --step mounts -- --json '{"allowedRoots":[...],"blockedPatterns":[],"nonMainReadOnly":true}'`
```bash
pnpm exec tsx setup/index.ts --step mounts -- --empty
```
## 7. Start Service
@@ -284,7 +255,7 @@ If service already running: unload first.
- macOS: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist`
- Linux: `systemctl --user stop nanoclaw` (or `systemctl stop nanoclaw` if root)
Run `npx tsx setup/index.ts --step service` and parse the status block.
Run `pnpm exec tsx setup/index.ts --step service` and parse the status block.
**If FALLBACK=wsl_no_systemd:** WSL without systemd detected. Tell user they can either enable systemd in WSL (`echo -e "[boot]\nsystemd=true" | sudo tee /etc/wsl.conf` then restart WSL) or use the generated `start-nanoclaw.sh` wrapper.
@@ -308,27 +279,47 @@ Replace `USERNAME` with the actual username (from `whoami`). Run the two `sudo`
- Linux: check `systemctl --user status nanoclaw`.
- Re-run the service step after fixing.
## 7a. Wire Channels to Agent Groups
The service is now running, so polling-based adapters (Telegram) can observe inbound messages — required for pairing.
Invoke `/manage-channels` to wire the installed channels to agent groups. This step:
1. Creates the agent group(s) and assigns a name to the assistant
2. Resolves each channel's platform-specific ID (Telegram via pairing code; other channels via the platform's own ID lookup)
3. Decides the isolation level — whether channels share an agent, session, or are fully separate
The `/manage-channels` skill reads each channel's `## Channel Info` section from its SKILL.md for platform-specific guidance (terminology, how to find IDs, recommended isolation).
**This step is required.** Without it, channels are installed but not wired — messages will be silently dropped because the router has no agent group to route to.
## 7b. Dashboard & Web Applications
AskUserQuestion: Do you want to create a dashboard and build web applications?
1. **Yes (recommended)** — description: "Get a NanoClaw dashboard to monitor your agents and build custom websites however you want. Deploys to Vercel."
2. **Not now** — description: "You can add this later with `/add-vercel`."
If yes: invoke `/add-vercel`.
## 8. Verify
Run `npx tsx setup/index.ts --step verify` and parse the status block.
Run `pnpm exec tsx setup/index.ts --step verify` and parse the status block.
**If STATUS=failed, fix each:**
- SERVICE=stopped → `npm run build`, then restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) or `bash start-nanoclaw.sh` (WSL nohup)
- SERVICE=stopped → `pnpm run build`, then restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) or `bash start-nanoclaw.sh` (WSL nohup)
- SERVICE=not_found → re-run step 7
- CREDENTIALS=missing → re-run step 4 (Docker: check `onecli secrets list`; Apple Container: check `.env` for credentials)
- CREDENTIALS=missing → re-run step 4 (check `onecli secrets list`)
- CHANNEL_AUTH shows `not_found` for any channel → re-invoke that channel's skill (e.g. `/add-telegram`)
- REGISTERED_GROUPS=0 → re-invoke the channel skills from step 5
- MOUNT_ALLOWLIST=missing → `npx tsx setup/index.ts --step mounts -- --empty`
- REGISTERED_GROUPS=0 → re-invoke `/manage-channels` from step 7a
Tell user to test: send a message in their registered chat. Show: `tail -f logs/nanoclaw.log`
## Troubleshooting
**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 7), credential system not running (Docker: check `curl http://127.0.0.1:10254/api/health`; Apple Container: check `.env` credentials), missing channel credentials (re-invoke channel skill).
**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 7), credential system not running (check `curl ${ONECLI_URL}/api/health`), missing channel credentials (re-invoke channel skill).
**Container agent fails ("Claude Code process exited with code 1"):** Ensure the container runtime is running — `open -a Docker` (macOS Docker), `container system start` (Apple Container), or `sudo systemctl start docker` (Linux). Check container logs in `groups/main/logs/container-*.log`.
**Container agent fails ("Claude Code process exited with code 1"):** Ensure Docker is running — `open -a Docker` (macOS) or `sudo systemctl start docker` (Linux). Check container logs in `groups/main/logs/container-*.log`.
**No response to messages:** Check trigger pattern. Main channel doesn't need prefix. Check DB: `npx tsx setup/index.ts --step verify`. Check `logs/nanoclaw.log`.
**No response to messages:** Check trigger pattern. Main channel doesn't need prefix. Check DB: `pnpm exec tsx setup/index.ts --step verify`. Check `logs/nanoclaw.log`.
**Channel not connecting:** Verify the channel's credentials are set in `.env`. Channels auto-enable when their credentials are present. For WhatsApp: check `store/auth/creds.json` exists. For token-based channels: check token values in `.env`. Restart the service after any `.env` change.
@@ -339,3 +330,20 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/
1. Use the Read tool to read `.claude/skills/setup/diagnostics.md`.
2. Follow every step in that file before completing setup.
## 10. Fork Setup
Only run this after the user has confirmed 2-way messaging works.
Check `git remote -v`. If `origin` points to `qwibitai/nanoclaw` (not a fork), ask in plain text:
> We recommend forking NanoClaw so you can push your customizations and pull updates easily. Would you like to set up a fork now?
If yes: instruct the user to fork `qwibitai/nanoclaw` on GitHub (they need to do this in their browser), then ask for their GitHub username. Run:
```bash
git remote rename origin upstream
git remote add origin https://github.com/<their-username>/nanoclaw.git
git push --force origin main
```
If no: skip — upstream is already configured from step 0.
@@ -0,0 +1,34 @@
[
"Bash(bash setup.sh*)",
"Bash(git remote *)",
"Bash(npx tsx setup/index.ts*)",
"Bash(npx tsx scripts/init-first-agent.ts*)",
"Bash(npm install @chat-adapter/*)",
"Bash(npm install chat-adapter-imessage*)",
"Bash(npm install @bitbasti/chat-adapter-webex*)",
"Bash(npm install @resend/chat-sdk-adapter*)",
"Bash(npm install @whiskeysockets/baileys*)",
"Bash(npm install @beeper/chat-adapter-matrix*)",
"Bash(npm install @nanoco/nanoclaw-dashboard*)",
"Bash(npm ci*)",
"Bash(npm run build*)",
"Bash(curl -fsSL onecli.sh*)",
"Bash(onecli *)",
"Bash(grep -q *)",
"Bash(echo *>> .env)",
"Bash(ls *)",
"Bash(cat ~/.config/nanoclaw/*)",
"Bash(tail *logs/*)",
"Bash(launchctl *nanoclaw*)",
"Bash(sqlite3 data/*)",
"Bash(docker info*)",
"Bash(docker logs *)",
"Bash(mkdir -p *)",
"Bash(cp .env *)",
"Bash(rsync -a .claude/skills/*)",
"Bash(head *)",
"Bash(xattr *)",
"Bash(find ~/.npm *)",
"Bash(which onecli*)",
"Bash(./container/build.sh*)"
]
+5 -5
View File
@@ -30,7 +30,7 @@ Run `/update-nanoclaw` in Claude Code.
**Conflict resolution**: opens only conflicted files, resolves the conflict markers, keeps your local customizations intact.
**Validation**: runs `npm run build` and `npm test`.
**Validation**: runs `pnpm run build` and `pnpm test`.
**Breaking changes check**: after validation, reads CHANGELOG.md for any `[BREAKING]` entries introduced by the update. If found, shows each breaking change and offers to run the recommended skill to migrate.
@@ -109,7 +109,7 @@ Show file-level impact from upstream:
Bucket the upstream changed files:
- **Skills** (`.claude/skills/`): unlikely to conflict unless the user edited an upstream skill
- **Source** (`src/`): may conflict if user modified the same files
- **Build/config** (`package.json`, `package-lock.json`, `tsconfig*.json`, `container/`, `launchd/`): review needed
- **Build/config** (`package.json`, `pnpm-lock.yaml`, `tsconfig*.json`, `container/`, `launchd/`): review needed
- **Other**: docs, tests, misc
**Large drift check:** If the upstream commit count and age suggest the user has a lot of catching up to do, mention that `/migrate-nanoclaw` might be a better fit — it extracts customizations and reapplies them on clean upstream instead of merging. Offer it as an option but don't push.
@@ -175,8 +175,8 @@ If it gets messy (more than 3 rounds of conflicts):
# Step 5: Validation
Run:
- `npm run build`
- `npm test` (do not fail the flow if tests are not configured)
- `pnpm run build`
- `pnpm test` (do not fail the flow if tests are not configured)
If build fails:
- Show the error.
@@ -234,7 +234,7 @@ Tell the user:
- Backup branch also exists: `backup/pre-update-<HASH>-<TIMESTAMP>`
- Restart the service to apply changes:
- If using launchd: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist`
- If running manually: restart `npm run dev`
- If running manually: restart `pnpm run dev`
## Diagnostics
+2 -2
View File
@@ -110,8 +110,8 @@ If a merge fails badly (e.g., cannot resolve conflicts):
# Step 4: Validation
After all selected skills are merged:
- `npm run build`
- `npm test` (do not fail the flow if tests are not configured)
- `pnpm run build`
- `pnpm test` (do not fail the flow if tests are not configured)
If build fails:
- Show the error.
+4 -4
View File
@@ -76,8 +76,8 @@ git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
```bash
git fetch whatsapp skill/local-whisper
git merge whatsapp/skill/local-whisper || {
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
}
```
@@ -87,7 +87,7 @@ This modifies `src/transcription.ts` to use the `whisper-cli` binary instead of
### Validate
```bash
npm run build
pnpm run build
```
## Phase 3: Verify
@@ -110,7 +110,7 @@ launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
### Build and restart
```bash
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
```
@@ -48,8 +48,8 @@ git remote add upstream https://github.com/qwibitai/nanoclaw.git
```bash
git fetch upstream skill/native-credential-proxy
git merge upstream/skill/native-credential-proxy || {
git checkout --theirs package-lock.json
git add package-lock.json
git checkout --theirs pnpm-lock.yaml
git add pnpm-lock.yaml
git merge --continue
}
```
@@ -62,7 +62,7 @@ This merges in:
- Restored platform-aware proxy bind address detection
- Reverted setup skill to `.env`-based credential instructions
If the merge reports conflicts beyond `package-lock.json`, resolve them by reading the conflicted files and understanding the intent of both sides.
If the merge reports conflicts beyond `pnpm-lock.yaml`, resolve them by reading the conflicted files and understanding the intent of both sides.
### Update main group CLAUDE.md
@@ -77,9 +77,9 @@ with:
### Validate code changes
```bash
npm install
npm run build
npx vitest run src/credential-proxy.test.ts src/container-runner.test.ts
pnpm install
pnpm run build
pnpm exec vitest run src/credential-proxy.test.ts src/container-runner.test.ts
```
All tests must pass and build must be clean before proceeding.
@@ -125,7 +125,7 @@ echo 'ANTHROPIC_API_KEY=<key>' >> .env
1. Rebuild and restart:
```bash
npm run build
pnpm run build
```
Then restart the service:
@@ -161,7 +161,7 @@ To revert to OneCLI gateway:
1. Find the merge commit: `git log --oneline --merges -5`
2. Revert it: `git revert <merge-commit> -m 1` (undoes the skill branch merge, keeps your other changes)
3. `npm install` (re-adds `@onecli-sh/sdk`)
4. `npm run build`
3. `pnpm install` (re-adds `@onecli-sh/sdk`)
4. `pnpm run build`
5. Follow `/setup` step 4 to configure OneCLI credentials
6. Remove `ANTHROPIC_API_KEY` / `CLAUDE_CODE_OAUTH_TOKEN` from `.env`
+11 -11
View File
@@ -26,7 +26,7 @@ Before using this skill, ensure:
1. **NanoClaw is installed and running** - WhatsApp connected, service active
2. **Dependencies installed**:
```bash
npm ls playwright dotenv-cli || npm install playwright dotenv-cli
pnpm ls playwright dotenv-cli || pnpm install playwright dotenv-cli
```
3. **CHROME_PATH configured** in `.env` (if Chrome is not at default location):
```bash
@@ -40,7 +40,7 @@ Before using this skill, ensure:
```bash
# 1. Setup authentication (interactive)
npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
pnpm exec dotenv -e .env -- pnpm exec tsx .claude/skills/x-integration/scripts/setup.ts
# Verify: data/x-auth.json should exist after successful login
# 2. Rebuild container to include skill
@@ -48,7 +48,7 @@ npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
# Verify: Output shows "COPY .claude/skills/x-integration/agent.ts"
# 3. Rebuild host and restart service
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
# Verify: launchctl list | grep nanoclaw (macOS) or systemctl --user status nanoclaw (Linux)
@@ -225,7 +225,7 @@ COPY container/agent-runner/package*.json ./
COPY container/agent-runner/ ./
```
Then add COPY line after `COPY container/agent-runner/ ./` and before `RUN npm run build`:
Then add COPY line after `COPY container/agent-runner/ ./` and before `RUN pnpm run build`:
```dockerfile
# Copy skill MCP tools
COPY .claude/skills/x-integration/agent.ts ./src/skills/x-integration/
@@ -247,7 +247,7 @@ echo "Chrome not found - update CHROME_PATH in .env"
### 2. Run Authentication
```bash
npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
pnpm exec dotenv -e .env -- pnpm exec tsx .claude/skills/x-integration/scripts/setup.ts
```
This opens Chrome for manual X login. Session saved to `data/x-browser-profile/`.
@@ -271,7 +271,7 @@ cat data/x-auth.json # Should show {"authenticated": true, ...}
### 4. Restart Service
```bash
npm run build
pnpm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
```
@@ -317,26 +317,26 @@ ls -la data/x-browser-profile/ 2>/dev/null | head -5
### Re-authenticate (if expired)
```bash
npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
pnpm exec dotenv -e .env -- pnpm exec tsx .claude/skills/x-integration/scripts/setup.ts
```
### Test Post (will actually post)
```bash
echo '{"content":"Test tweet - please ignore"}' | npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/post.ts
echo '{"content":"Test tweet - please ignore"}' | pnpm exec dotenv -e .env -- pnpm exec tsx .claude/skills/x-integration/scripts/post.ts
```
### Test Like
```bash
echo '{"tweetUrl":"https://x.com/user/status/123"}' | npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/like.ts
echo '{"tweetUrl":"https://x.com/user/status/123"}' | pnpm exec dotenv -e .env -- pnpm exec tsx .claude/skills/x-integration/scripts/like.ts
```
Or export `CHROME_PATH` manually before running:
```bash
export CHROME_PATH="/path/to/chrome"
echo '{"content":"Test"}' | npx tsx .claude/skills/x-integration/scripts/post.ts
echo '{"content":"Test"}' | pnpm exec tsx .claude/skills/x-integration/scripts/post.ts
```
## Troubleshooting
@@ -344,7 +344,7 @@ echo '{"content":"Test"}' | npx tsx .claude/skills/x-integration/scripts/post.ts
### Authentication Expired
```bash
npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
pnpm exec dotenv -e .env -- pnpm exec tsx .claude/skills/x-integration/scripts/setup.ts
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
```
+1 -1
View File
@@ -22,7 +22,7 @@ async function runScript(script: string, args: object): Promise<SkillResult> {
const scriptPath = path.join(process.cwd(), '.claude', 'skills', 'x-integration', 'scripts', `${script}.ts`);
return new Promise((resolve) => {
const proc = spawn('npx', ['tsx', scriptPath], {
const proc = spawn('pnpm', ['exec', 'tsx', scriptPath], {
cwd: process.cwd(),
env: { ...process.env, NANOCLAW_ROOT: process.cwd() },
stdio: ['pipe', 'pipe', 'pipe']
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env npx tsx
#!/usr/bin/env pnpm exec tsx
/**
* X Integration - Like Tweet
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123"}' | npx tsx like.ts
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123"}' | pnpm exec tsx like.ts
*/
import { getBrowserContext, navigateToTweet, runScript, config, ScriptResult } from '../lib/browser.js';
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env npx tsx
#!/usr/bin/env pnpm exec tsx
/**
* X Integration - Post Tweet
* Usage: echo '{"content":"Hello world"}' | npx tsx post.ts
* Usage: echo '{"content":"Hello world"}' | pnpm exec tsx post.ts
*/
import { getBrowserContext, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
@@ -1,7 +1,7 @@
#!/usr/bin/env npx tsx
#!/usr/bin/env pnpm exec tsx
/**
* X Integration - Quote Tweet
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123","comment":"My thoughts"}' | npx tsx quote.ts
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123","comment":"My thoughts"}' | pnpm exec tsx quote.ts
*/
import { getBrowserContext, navigateToTweet, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
@@ -1,7 +1,7 @@
#!/usr/bin/env npx tsx
#!/usr/bin/env pnpm exec tsx
/**
* X Integration - Reply to Tweet
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123","content":"Great post!"}' | npx tsx reply.ts
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123","content":"Great post!"}' | pnpm exec tsx reply.ts
*/
import { getBrowserContext, navigateToTweet, runScript, validateContent, config, ScriptResult } from '../lib/browser.js';
@@ -1,7 +1,7 @@
#!/usr/bin/env npx tsx
#!/usr/bin/env pnpm exec tsx
/**
* X Integration - Retweet
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123"}' | npx tsx retweet.ts
* Usage: echo '{"tweetUrl":"https://x.com/user/status/123"}' | pnpm exec tsx retweet.ts
*/
import { getBrowserContext, navigateToTweet, runScript, config, ScriptResult } from '../lib/browser.js';
@@ -1,7 +1,7 @@
#!/usr/bin/env npx tsx
#!/usr/bin/env pnpm exec tsx
/**
* X Integration - Authentication Setup
* Usage: npx tsx setup.ts
* Usage: pnpm exec tsx setup.ts
*
* Interactive script - opens browser for manual login
*/
+4 -2
View File
@@ -20,10 +20,12 @@ jobs:
with:
token: ${{ steps.app-token.outputs.token }}
- uses: pnpm/action-setup@v4
- name: Bump patch version
run: |
npm version patch --no-git-tag-version
git add package.json package-lock.json
pnpm version patch --no-git-tag-version
git add package.json
git diff --cached --quiet && exit 0
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
+21 -7
View File
@@ -9,17 +9,31 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
cache: pnpm
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- run: pnpm install --frozen-lockfile
- name: Install agent-runner deps (Bun)
working-directory: container/agent-runner
run: bun install --frozen-lockfile
- name: Format check
run: npm run format:check
run: pnpm run format:check
- name: Typecheck
run: npx tsc --noEmit
- name: Typecheck host
run: pnpm exec tsc --noEmit
- name: Tests
run: npx vitest run
- name: Typecheck container
run: pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit
- name: Host tests
run: pnpm exec vitest run
- name: Container tests
working-directory: container/agent-runner
run: bun test
+2
View File
@@ -1,6 +1,8 @@
# Dependencies
node_modules/
.npm-cache/
# pnpm content-addressable store (created when running in sandbox mode)
.pnpm-store/
# Build output
dist/
+1 -1
View File
@@ -1 +1 @@
npm run format:fix
pnpm run format:fix
+3
View File
@@ -0,0 +1,3 @@
# Safety net — pnpm-workspace.yaml has the authoritative minimumReleaseAge (4320 min = 3 days)
# This .npmrc value is a fallback if npm is ever invoked directly
minReleaseAge=3d
+2 -1
View File
@@ -1,3 +1,4 @@
{
"singleQuote": true
"singleQuote": true,
"printWidth": 120
}
+145 -37
View File
@@ -1,80 +1,188 @@
# NanoClaw
Personal Claude assistant. See [README.md](README.md) for philosophy and setup. See [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) for architecture decisions.
Personal Claude assistant. See [README.md](README.md) for philosophy and setup. Architecture lives in `docs/v2-*.md`.
## Quick Context
## Quick Context (v2)
Single Node.js process with skill-based channel system. Channels (WhatsApp, Telegram, Slack, Discord, Gmail) are skills that self-register at startup. Messages route to Claude Agent SDK running in containers (Linux VMs). Each group has isolated filesystem and memory.
v2 is the current branch and codebase. v1 still exists under `src/v1/` and `container/agent-runner/src/v1/` for reference but is no longer the runtime. If a file mentions v1 in its comments, it is probably stale.
The host is a single Node process that orchestrates per-session agent containers. Platform messages land via channel adapters, route through an entity model (users → messaging groups → agent groups → sessions), get written into the session's inbound DB, and wake a container. The agent-runner inside the container polls the DB, calls Claude, and writes back to the outbound DB. The host polls the outbound DB and delivers through the same adapter.
**Everything is a message.** There is no IPC, no file watcher, no stdin piping between host and container. The two session DBs are the sole IO surface.
## Entity Model
```
users (id "<channel>:<handle>", kind, display_name)
user_roles (user_id, role, agent_group_id) — owner | admin (global or scoped)
agent_group_members (user_id, agent_group_id) — unprivileged access gate
user_dms (user_id, channel_type, messaging_group_id) — cold-DM cache
agent_groups (workspace, memory, CLAUDE.md, personality, container config)
↕ many-to-many via messaging_group_agents (session_mode, trigger_rules, priority)
messaging_groups (one chat/channel on one platform; unknown_sender_policy)
sessions (agent_group_id + messaging_group_id + thread_id → per-session container)
```
Privilege is user-level (owner/admin), not agent-group-level. See [docs/v2-isolation-model.md](docs/v2-isolation-model.md) for the three isolation levels (`agent-shared`, `shared`, separate agents).
## Two-DB Session Split
Each session has **two** SQLite files under `data/v2-sessions/<session_id>/`:
- `inbound.db` — host writes, container reads. `messages_in`, routing, destinations, pending_questions, processing_ack.
- `outbound.db` — container writes, host reads. `messages_out`, session_state.
Exactly one writer per file — no cross-mount lock contention. Heartbeat is a file touch at `/workspace/.heartbeat`, not a DB update. Host uses even `seq` numbers, container uses odd.
## Central DB
`data/v2.db` holds everything that isn't per-session: users, user_roles, agent_groups, messaging_groups, wiring, pending_approvals, user_dms, chat_sdk_* (for the Chat SDK bridge), schema_version. Migrations live at `src/db/migrations/`.
## Key Files
| File | Purpose |
|------|---------|
| `src/index.ts` | Orchestrator: state, message loop, agent invocation |
| `src/channels/registry.ts` | Channel registry (self-registration at startup) |
| `src/ipc.ts` | IPC watcher and task processing |
| `src/router.ts` | Message formatting and outbound routing |
| `src/config.ts` | Trigger pattern, paths, intervals |
| `src/container-runner.ts` | Spawns agent containers with mounts |
| `src/task-scheduler.ts` | Runs scheduled tasks |
| `src/db.ts` | SQLite operations |
| `groups/{name}/CLAUDE.md` | Per-group memory (isolated) |
| `container/skills/` | Skills loaded inside agent containers (browser, status, formatting) |
| `src/index.ts` | Entry point: init DB, migrations, channel adapters, delivery polls, sweep, shutdown |
| `src/router.ts` | Inbound routing: messaging group → agent group → session → `inbound.db` → wake |
| `src/delivery.ts` | Polls `outbound.db`, delivers via adapter, handles system actions (schedule, approvals, etc.) |
| `src/host-sweep.ts` | 60s sweep: `processing_ack` sync, stale detection, due-message wake, recurrence |
| `src/session-manager.ts` | Resolves sessions; opens `inbound.db` / `outbound.db`; manages heartbeat path |
| `src/container-runner.ts` | Spawns per-agent-group Docker containers with session DB + outbox mounts, OneCLI `ensureAgent` |
| `src/container-runtime.ts` | Runtime selection (Docker vs Apple containers), orphan cleanup |
| `src/access.ts` | `pickApprover`, `pickApprovalDelivery`, admin resolution for `NANOCLAW_ADMIN_USER_IDS` |
| `src/onecli-approvals.ts` | OneCLI credentialed-action approval bridge |
| `src/user-dm.ts` | Cold-DM resolution + `user_dms` cache |
| `src/group-init.ts` | Per-agent-group filesystem scaffold (CLAUDE.md, skills, agent-runner-src overlay) |
| `src/db/` | DB layer — agent_groups, messaging_groups, sessions, user_roles, user_dms, pending_*, migrations |
| `src/channels/` | Channel adapters + Chat SDK bridge |
| `container/agent-runner/src/` | Agent-runner: poll loop, formatter, provider abstraction, MCP tools, destinations |
| `container/skills/` | Container skills mounted into every agent session |
| `groups/<folder>/` | Per-agent-group filesystem (CLAUDE.md, skills, per-group `agent-runner-src/` overlay) |
| `scripts/init-first-agent.ts` | Bootstrap the first DM-wired agent (used by `/init-first-agent` skill) |
## Secrets / Credentials / Proxy (OneCLI)
## Self-Modification
API keys, secret keys, OAuth tokens, and auth credentials are managed by the OneCLI gateway — which handles secret injection into containers at request time, so no keys or tokens are ever passed to containers directly. Run `onecli --help`.
One tier of agent self-modification today:
1. **`install_packages` / `add_mcp_server` / `request_rebuild`** — changes to the per-agent-group container config only (apt/npm deps, wire an existing MCP server). Admin approval, rebuild, container restart. `container/agent-runner/src/mcp-tools/self-mod.ts`.
A second tier (direct source-level self-edits via a draft/activate flow) is planned but not yet implemented.
## Secrets / Credentials / OneCLI
API keys, OAuth tokens, and auth credentials are managed by the OneCLI gateway. Secrets are injected into per-agent containers at request time — none are passed in env vars or through chat context. `src/onecli-approvals.ts`, `ensureAgent()` in `container-runner.ts`. Run `onecli --help`.
## Skills
Four types of skills exist in NanoClaw. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full taxonomy and guidelines.
Four types of skills. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full taxonomy.
- **Feature skills** merge a `skill/*` branch to add capabilities (e.g. `/add-telegram`, `/add-slack`)
- **Utility skills** — ship code files alongside SKILL.md (e.g. `/claw`)
- **Operational skills** — instruction-only workflows, always on `main` (e.g. `/setup`, `/debug`)
- **Container skills** — loaded inside agent containers at runtime (`container/skills/`)
- **Feature skills**`skill/*` branches merged via `scripts/apply-skill.ts` (e.g. `/add-discord-v2`, `/add-slack-v2`, `/add-whatsapp-v2`)
- **Utility skills** — ship code files alongside `SKILL.md` (e.g. `/claw`)
- **Operational skills** — instruction-only workflows (`/setup`, `/debug`, `/customize`, `/init-first-agent`, `/manage-channels`, `/init-onecli`, `/update-nanoclaw`)
- **Container skills** — loaded inside agent containers at runtime (`container/skills/`: `welcome`, `self-customize`, `agent-browser`, `slack-formatting`)
| Skill | When to Use |
|-------|-------------|
| `/setup` | First-time installation, authentication, service configuration |
| `/customize` | Adding channels, integrations, changing behavior |
| `/setup` | First-time install, auth, service config |
| `/init-first-agent` | Bootstrap the first DM-wired agent (channel pick → identity → wire → welcome DM) |
| `/manage-channels` | Wire channels to agent groups with isolation level decisions |
| `/customize` | Adding channels, integrations, behavior changes |
| `/debug` | Container issues, logs, troubleshooting |
| `/update-nanoclaw` | Bring upstream NanoClaw updates into a customized install |
| `/init-onecli` | Install OneCLI Agent Vault and migrate `.env` credentials to it |
| `/qodo-pr-resolver` | Fetch and fix Qodo PR review issues interactively or in batch |
| `/get-qodo-rules` | Load org- and repo-level coding rules from Qodo before code tasks |
| `/update-nanoclaw` | Bring upstream updates into a customized install |
| `/init-onecli` | Install OneCLI Agent Vault and migrate `.env` credentials |
## Contributing
Before creating a PR, adding a skill, or preparing any contribution, you MUST read [CONTRIBUTING.md](CONTRIBUTING.md). It covers accepted change types, the four skill types and their guidelines, SKILL.md format rules, PR requirements, and the pre-submission checklist (searching for existing PRs/issues, testing, description format).
Before creating a PR, adding a skill, or preparing any contribution, you MUST read [CONTRIBUTING.md](CONTRIBUTING.md). It covers accepted change types, the four skill types and their guidelines, `SKILL.md` format rules, and the pre-submission checklist.
## Development
Run commands directlydon't tell the user to run them.
Run commands directlydon't tell the user to run them.
```bash
npm run dev # Run with hot reload
npm run build # Compile TypeScript
./container/build.sh # Rebuild agent container
# Host (Node + pnpm)
pnpm run dev # Host with hot reload
pnpm run build # Compile host TypeScript (src/)
./container/build.sh # Rebuild agent container image (nanoclaw-agent:latest)
pnpm test # Host tests (vitest)
# Agent-runner (Bun — separate package tree under container/agent-runner/)
cd container/agent-runner && bun install # After editing agent-runner deps
cd container/agent-runner && bun test # Container tests (bun:test)
```
Container typecheck is a separate tsconfig — if you edit `container/agent-runner/src/`, run `pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit` from root (or `bun run typecheck` from `container/agent-runner/`).
Service management:
```bash
# macOS (launchd)
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # restart
# Linux (systemd)
systemctl --user start nanoclaw
systemctl --user stop nanoclaw
systemctl --user restart nanoclaw
systemctl --user start|stop|restart nanoclaw
```
## Troubleshooting
Host logs: `logs/nanoclaw.log` (normal) and `logs/nanoclaw.error.log` (errors only — some delivery/approval failures only show up here).
**WhatsApp not connecting after upgrade:** WhatsApp is now a separate skill, not bundled in core. Run `/add-whatsapp` (or `npx tsx scripts/apply-skill.ts .claude/skills/add-whatsapp && npm run build`) to install it. Existing auth credentials and groups are preserved.
## Supply Chain Security (pnpm)
This project uses pnpm with `minimumReleaseAge: 4320` (3 days) in `pnpm-workspace.yaml`. New package versions must exist on the npm registry for 3 days before pnpm will resolve them.
**Rules — do not bypass without explicit human approval:**
- **`minimumReleaseAgeExclude`**: Never add entries without human sign-off. If a package must bypass the release age gate, the human must approve and the entry must pin the exact version being excluded (e.g. `package@1.2.3`), never a range.
- **`onlyBuiltDependencies`**: Never add packages to this list without human approval — build scripts execute arbitrary code during install.
- **`pnpm install --frozen-lockfile`** should be used in CI, automation, and container builds. Never run bare `pnpm install` in those contexts.
## v2 Docs Index
| Doc | Purpose |
|-----|---------|
| [docs/v2-architecture-draft.md](docs/v2-architecture-draft.md) | Full architecture writeup |
| [docs/v2-api-details.md](docs/v2-api-details.md) | Host API + DB schema details |
| [docs/v2-db.md](docs/v2-db.md) | DB architecture overview: three-DB model, cross-mount rules, readers/writers map |
| [docs/v2-db-central.md](docs/v2-db-central.md) | Central DB (`data/v2.db`) — every table + migration system |
| [docs/v2-db-session.md](docs/v2-db-session.md) | Per-session `inbound.db` + `outbound.db` schemas + seq parity |
| [docs/v2-agent-runner-details.md](docs/v2-agent-runner-details.md) | Agent-runner internals + MCP tool interface |
| [docs/v2-isolation-model.md](docs/v2-isolation-model.md) | Three-level channel isolation model |
| [docs/v2-setup-wiring.md](docs/v2-setup-wiring.md) | What's wired, what's open in the setup flow |
| [docs/v2-checklist.md](docs/v2-checklist.md) | Rolling status checklist across all subsystems |
| [docs/v2-architecture-diagram.md](docs/v2-architecture-diagram.md) | Diagram version of the architecture |
| [docs/v2-build-and-runtime.md](docs/v2-build-and-runtime.md) | Runtime split (Node host + Bun container), lockfiles, image build surface, CI, key invariants |
## Container Build Cache
The container buildkit caches the build context aggressively. `--no-cache` alone does NOT invalidate COPY steps — the builder's volume retains stale files. To force a truly clean rebuild, prune the builder then re-run `./container/build.sh`.
## Container Runtime (Bun)
The agent container runs on **Bun**; the host runs on **Node** (pnpm). They communicate only via session DBs — no shared modules. Details and rationale: [docs/v2-build-and-runtime.md](docs/v2-build-and-runtime.md).
**Gotchas — trigger + action:**
- **Adding or bumping a runtime dep in `container/agent-runner/`** → edit `package.json`, then `cd container/agent-runner && bun install` and commit the updated `bun.lock`. Do not run `pnpm install` there — agent-runner is not a pnpm workspace.
- **Bumping `@anthropic-ai/claude-agent-sdk`, `@modelcontextprotocol/sdk`, or any agent-runner runtime dep** → no `minimumReleaseAge` policy applies to this tree. Check the release date on npm, pin deliberately, never `bun update` blindly.
- **Writing a new named-param SQL insert/update in the container** → use `$name` in both SQL and JS keys: `.run({ $id: msg.id })`. `bun:sqlite` does not auto-strip the prefix the way `better-sqlite3` does on the host. Positional `?` params work normally.
- **Adding a test in `container/agent-runner/src/`** → import from `bun:test`, not `vitest`. Vitest runs on Node and can't load `bun:sqlite`. `vitest.config.ts` excludes this tree.
- **Adding a Node CLI the agent invokes at runtime** (like `agent-browser`, `claude-code`, `vercel`) → put it in the Dockerfile's pnpm global-install block, pinned to an exact version via a new `ARG`. Don't use `bun install -g` — that bypasses the pnpm supply-chain policy.
- **Changing the Dockerfile entrypoint or the dynamic-spawn command** (`src/container-runner.ts` line ~301) → keep `exec bun ...` so signals forward cleanly. The image has no `/app/dist`; don't reintroduce a tsc build step.
- **Changing session-DB pragmas** (`container/agent-runner/src/db/connection.ts`) → `journal_mode=DELETE` is load-bearing for cross-mount visibility. Read the comment block at the top of the file first.
## CJK font support
Agent containers ship without CJK fonts by default (~200MB saved). If you notice signals the user works with Chinese/Japanese/Korean content — conversing in CJK, CJK timezone (e.g., `Asia/Tokyo`, `Asia/Shanghai`, `Asia/Seoul`, `Asia/Taipei`, `Asia/Hong_Kong`), system locale hint, or mentions of needing to render CJK in screenshots/PDFs/scraped pages — offer to enable it:
```bash
# Ensure .env has INSTALL_CJK_FONTS=true (overwrite or append)
grep -q '^INSTALL_CJK_FONTS=' .env && sed -i.bak 's/^INSTALL_CJK_FONTS=.*/INSTALL_CJK_FONTS=true/' .env && rm -f .env.bak || echo 'INSTALL_CJK_FONTS=true' >> .env
# Rebuild and restart so new sessions pick up the new image
./container/build.sh
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# systemctl --user restart nanoclaw # Linux
```
`container/build.sh` reads `INSTALL_CJK_FONTS` from `.env` and passes it through as a Docker build-arg. Without CJK fonts, Chromium-rendered screenshots and PDFs containing CJK text show tofu (empty rectangles) instead of characters.
+2
View File
@@ -0,0 +1,2 @@
agent-runner/node_modules
agent-runner/dist
+94 -49
View File
@@ -1,70 +1,115 @@
# syntax=docker/dockerfile:1.7
# NanoClaw Agent Container
# Runs Claude Agent SDK in isolated Linux VM with browser automation
# Runs Claude Agent SDK in isolated Linux VM with browser automation.
#
# Runtime split:
# - agent-runner (our TypeScript code): Bun
# - globally-installed Node CLIs (claude-code, agent-browser, vercel): pnpm + Node
FROM node:22-slim
# Install system dependencies for Chromium
RUN apt-get update && apt-get install -y \
chromium \
fonts-liberation \
fonts-noto-cjk \
fonts-noto-color-emoji \
libgbm1 \
libnss3 \
libatk-bridge2.0-0 \
libgtk-3-0 \
libx11-xcb1 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
libasound2 \
libpangocairo-1.0-0 \
libcups2 \
libdrm2 \
libxshmfence1 \
curl \
git \
# ---- Build-time arguments ----------------------------------------------------
# CJK fonts add ~200MB. Opt in only if you render Chinese/Japanese/Korean text.
ARG INSTALL_CJK_FONTS=false
# Pin CLI versions for reproducibility. Bump deliberately — unpinned installs
# mean every rebuild silently picks up the latest and can break in lockstep
# across all users.
ARG CLAUDE_CODE_VERSION=2.1.112
ARG AGENT_BROWSER_VERSION=latest
ARG VERCEL_VERSION=latest
ARG OPENCODE_VERSION=latest
ARG BUN_VERSION=1.3.12
# ---- System dependencies -----------------------------------------------------
# tini: correct PID 1 / signal forwarding so outbound.db writes finalize on
# SIGTERM instead of being orphaned by the shell entrypoint.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
chromium \
fonts-liberation \
fonts-noto-color-emoji \
libgbm1 \
libnss3 \
libatk-bridge2.0-0 \
libgtk-3-0 \
libx11-xcb1 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
libasound2 \
libpangocairo-1.0-0 \
libcups2 \
libdrm2 \
libxshmfence1 \
ca-certificates \
curl \
git \
tini \
unzip \
&& if [ "$INSTALL_CJK_FONTS" = "true" ]; then \
apt-get install -y --no-install-recommends fonts-noto-cjk; \
fi \
&& rm -rf /var/lib/apt/lists/*
# Set Chromium path for agent-browser
# Chromium path for agent-browser / Playwright consumers
ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
# Belt-and-braces: prevent Playwright's postinstall from downloading its own
# ~300MB Chromium. We've already installed the system one above.
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
# Install agent-browser and claude-code globally
RUN npm install -g agent-browser @anthropic-ai/claude-code
# ---- Bun runtime -------------------------------------------------------------
# Install via the official script (handles multi-arch detection), then move
# the binary to /usr/local/bin so the non-root `node` user can execute it.
RUN curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" && \
install -m 0755 /root/.bun/bin/bun /usr/local/bin/bun && \
rm -rf /root/.bun
# Create app directory
# ---- pnpm + global Node CLIs -------------------------------------------------
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
# agent-browser has a postinstall build script — pnpm skips these by default.
# Allowlist it via .npmrc so the install doesn't silently produce a broken
# package. Pinned versions so every rebuild is reproducible.
RUN --mount=type=cache,target=/root/.cache/pnpm \
echo "only-built-dependencies[]=agent-browser" > /root/.npmrc && \
pnpm install -g \
"@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
"agent-browser@${AGENT_BROWSER_VERSION}" \
"vercel@${VERCEL_VERSION}" \
"opencode-ai@${OPENCODE_VERSION}"
# ---- agent-runner ------------------------------------------------------------
WORKDIR /app
# Copy package files first for better caching
COPY agent-runner/package*.json ./
# Copy manifest + lockfile first so the install layer caches independently of
# source edits.
COPY agent-runner/package.json agent-runner/bun.lock ./
# Install dependencies
RUN npm install
RUN --mount=type=cache,target=/root/.bun/install/cache \
bun install --frozen-lockfile
# Copy source code
# Source. Bun runs TS directly — no tsc build step. The host remounts this
# path at runtime via `src/container-runner.ts` so source edits on the host
# take effect without rebuilding the image; the baked copy is the fallback.
COPY agent-runner/ ./
# Build TypeScript
RUN npm run build
# ---- Entrypoint --------------------------------------------------------------
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
# Create workspace directories
RUN mkdir -p /workspace/group /workspace/global /workspace/extra /workspace/ipc/messages /workspace/ipc/tasks /workspace/ipc/input
# ---- Workspace + permissions -------------------------------------------------
RUN mkdir -p /workspace/group /workspace/global /workspace/extra && \
chown -R node:node /workspace && \
chmod 755 /home/node
# Create entrypoint script
# Container input (prompt, group info) is passed via stdin JSON.
# Credentials are injected by the host's credential proxy — never passed here.
# Follow-up messages arrive via IPC files in /workspace/ipc/input/
RUN printf '#!/bin/bash\nset -e\ncd /app && npx tsc --outDir /tmp/dist 2>&1 >&2\nln -s /app/node_modules /tmp/dist/node_modules\nchmod -R a-w /tmp/dist\ncat > /tmp/input.json\nnode /tmp/dist/index.js < /tmp/input.json\n' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh
# Set ownership to node user (non-root) for writable directories
RUN chown -R node:node /workspace && chmod 777 /home/node
# Switch to non-root user (required for --dangerously-skip-permissions)
USER node
# Set working directory to group workspace
WORKDIR /workspace/group
# Entry point reads JSON from stdin, outputs JSON to stdout
ENTRYPOINT ["/app/entrypoint.sh"]
# tini is PID 1, reaps zombies, forwards signals cleanly. entrypoint.sh does
# `exec bun ...` so bun runs as tini's direct child.
ENTRYPOINT ["/usr/bin/tini", "--", "/app/entrypoint.sh"]
+262
View File
@@ -0,0 +1,262 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "nanoclaw-agent-runner",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
"@modelcontextprotocol/sdk": "^1.12.1",
"@opencode-ai/sdk": "^1.4.3",
"cron-parser": "^5.0.0",
"zod": "^4.0.0",
},
"devDependencies": {
"@types/bun": "^1.1.0",
"@types/node": "^22.10.7",
"typescript": "^5.7.3",
},
},
},
"packages": {
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.112", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-vMFoiDKlOive8p3tphpV1gQaaytOipwGJ+uw9mvvaLQUODSC2+fCdRDAY25i2Tsv+lOtxzXBKctmaDuWqZY7ig=="],
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="],
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.4.7", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-onEtaooQyoDP5gTShQeQSf0Sd8V7949G9pPNyIyRXnVtFqyDIhUDLGtL/a/+EIW9x5s+Y6lDy/3oVoGMvQ0rQQ=="],
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
"@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"cron-parser": ["cron-parser@5.5.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
}
}
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -3,18 +3,20 @@
"version": "1.0.0",
"type": "module",
"description": "Container-side agent runner for NanoClaw",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
"start": "bun src/index.ts",
"typecheck": "tsc --noEmit",
"test": "bun test"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
"@modelcontextprotocol/sdk": "^1.12.1",
"@opencode-ai/sdk": "^1.4.3",
"cron-parser": "^5.0.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@types/bun": "^1.1.0",
"@types/node": "^22.10.7",
"typescript": "^5.7.3"
}
+178
View File
@@ -0,0 +1,178 @@
/**
* Two-DB connection layer.
*
* The session uses two SQLite files to eliminate write contention across
* the host-container mount boundary:
*
* inbound.db host writes new messages here; container opens READ-ONLY
* outbound.db container writes responses + acks here; host opens read-only
*
* Each file has exactly one writer, so no cross-process lock contention.
*
* Cross-mount visibility: inbound.db MUST be journal_mode=DELETE (set by
* the host when the file is created). WAL's `-shm` is memory-mapped and
* VirtioFS does not propagate mmap coherency from host to guest, so a
* WAL-mode inbound.db would leave this reader frozen on an early snapshot
* and it would silently never see new host messages. See
* src/session-manager.ts for the full set of cross-mount invariants and
* scripts/sanity-live-poll.ts for the empirical validation.
*/
import { Database } from 'bun:sqlite';
import fs from 'fs';
const DEFAULT_INBOUND_PATH = '/workspace/inbound.db';
const DEFAULT_OUTBOUND_PATH = '/workspace/outbound.db';
const DEFAULT_HEARTBEAT_PATH = '/workspace/.heartbeat';
let _inbound: Database | null = null;
let _outbound: Database | null = null;
let _heartbeatPath: string = DEFAULT_HEARTBEAT_PATH;
/** Inbound DB — container opens read-only (host is the sole writer). */
export function getInboundDb(): Database {
if (!_inbound) {
const dbPath = process.env.SESSION_INBOUND_DB_PATH || DEFAULT_INBOUND_PATH;
_inbound = new Database(dbPath, { readonly: true });
_inbound.exec('PRAGMA busy_timeout = 5000');
}
return _inbound;
}
/** Outbound DB — container owns this file (sole writer). */
export function getOutboundDb(): Database {
if (!_outbound) {
const dbPath = process.env.SESSION_OUTBOUND_DB_PATH || DEFAULT_OUTBOUND_PATH;
_outbound = new Database(dbPath);
_outbound.exec('PRAGMA journal_mode = DELETE');
_outbound.exec('PRAGMA busy_timeout = 5000');
_outbound.exec('PRAGMA foreign_keys = ON');
// Lightweight forward-compat: session_state was added after the initial
// v2 schema, so older session DBs don't have it. Create it on demand
// instead of requiring a formal migration pass. Also handle the case
// where an earlier revision of this table existed without updated_at —
// ALTER TABLE to add any missing columns.
_outbound.exec(`
CREATE TABLE IF NOT EXISTS session_state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
`);
const cols = new Set(
(_outbound.prepare("PRAGMA table_info('session_state')").all() as Array<{ name: string }>).map((c) => c.name),
);
if (!cols.has('updated_at')) {
_outbound.exec(`ALTER TABLE session_state ADD COLUMN updated_at TEXT NOT NULL DEFAULT ''`);
}
}
return _outbound;
}
/**
* Touch the heartbeat file replaces the old touchProcessing() DB writes.
* The host checks this file's mtime for stale container detection.
* A file touch is cheaper and avoids cross-boundary DB write contention.
*/
export function touchHeartbeat(): void {
const p = process.env.SESSION_HEARTBEAT_PATH || _heartbeatPath;
const now = new Date();
try {
fs.utimesSync(p, now, now);
} catch {
try {
fs.writeFileSync(p, '');
} catch {
// Silently ignore — parent dir may not exist (e.g., in-memory test DBs)
}
}
}
/**
* Clear stale processing_ack entries on container startup.
* If the previous container crashed, 'processing' entries are leftover.
* Clearing them lets the new container re-process those messages.
*/
export function clearStaleProcessingAcks(): void {
getOutboundDb().prepare("DELETE FROM processing_ack WHERE status = 'processing'").run();
}
/** For tests — creates in-memory DBs with the session schemas. */
export function initTestSessionDb(): { inbound: Database; outbound: Database } {
_inbound = new Database(':memory:');
_inbound.exec('PRAGMA foreign_keys = ON');
_inbound.exec(`
CREATE TABLE messages_in (
id TEXT PRIMARY KEY,
seq INTEGER UNIQUE,
kind TEXT NOT NULL,
timestamp TEXT NOT NULL,
status TEXT DEFAULT 'pending',
process_after TEXT,
recurrence TEXT,
tries INTEGER DEFAULT 0,
platform_id TEXT,
channel_type TEXT,
thread_id TEXT,
content TEXT NOT NULL
);
CREATE TABLE delivered (
message_out_id TEXT PRIMARY KEY,
platform_message_id TEXT,
status TEXT NOT NULL DEFAULT 'delivered',
delivered_at TEXT NOT NULL
);
CREATE TABLE destinations (
name TEXT PRIMARY KEY,
display_name TEXT,
type TEXT NOT NULL,
channel_type TEXT,
platform_id TEXT,
agent_group_id TEXT
);
`);
_outbound = new Database(':memory:');
_outbound.exec('PRAGMA foreign_keys = ON');
_outbound.exec(`
CREATE TABLE messages_out (
id TEXT PRIMARY KEY,
seq INTEGER UNIQUE,
in_reply_to TEXT,
timestamp TEXT NOT NULL,
deliver_after TEXT,
recurrence TEXT,
kind TEXT NOT NULL,
platform_id TEXT,
channel_type TEXT,
thread_id TEXT,
content TEXT NOT NULL
);
CREATE TABLE processing_ack (
message_id TEXT PRIMARY KEY,
status TEXT NOT NULL,
status_changed TEXT NOT NULL
);
CREATE TABLE session_state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
`);
return { inbound: _inbound, outbound: _outbound };
}
export function closeSessionDb(): void {
_inbound?.close();
_inbound = null;
_outbound?.close();
_outbound = null;
}
/**
* @deprecated Use getInboundDb() / getOutboundDb() instead.
* Kept for backward compatibility during migration.
*/
export function getSessionDb(): Database {
return getInboundDb();
}
+20
View File
@@ -0,0 +1,20 @@
export {
getInboundDb,
getOutboundDb,
getSessionDb,
initTestSessionDb,
closeSessionDb,
touchHeartbeat,
clearStaleProcessingAcks,
} from './connection.js';
export {
getPendingMessages,
markProcessing,
markCompleted,
markFailed,
getMessageIn,
findQuestionResponse,
} from './messages-in.js';
export type { MessageInRow } from './messages-in.js';
export { writeMessageOut, getUndeliveredMessages } from './messages-out.js';
export type { MessageOutRow, WriteMessageOut } from './messages-out.js';

Some files were not shown because too many files have changed in this diff Show More