Compare commits

...

301 Commits

Author SHA1 Message Date
gavrielc 420fe5ecff setup skill: inject top-of-flow state via dynamic context
Replace the four auto-run commands that execute before the first user
decision (git upstream check, bash setup.sh, environment probe, timezone
probe) with `!`cmd`` dynamic-context placeholders so their output is
pre-rendered into the skill instead of Claude running them interactively.

Adds matching allowed-tools frontmatter so preprocessing runs without
per-command approval prompts. Later auto-run steps (container build,
mounts, service, verify) stay as body instructions — they depend on
mid-flow user choices and earlier state changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:30:16 +03:00
gavrielc 5ed5b72f10 docs: consolidate refactor follow-ups into a single REFACTOR.md
Single forward-looking reference that replaces the two untracked planning
docs (REFACTOR_PLAN.md + REFACTOR_EXECUTION.md) which had become a mix of
historical PR timeline and still-relevant decisions.

Keeps only what's actionable going forward:
- Module tiers, the four registries, and the module distribution model
  (architecture summary).
- Remaining work: Phase 5 (v2 → main) and the modules-branch decision.
- Operational patterns worth preserving (standing checks, TDZ rule,
  branch-sync file-presence diff procedure, prettier drift pattern).
- 17 curated open questions across design, distribution, core slotting,
  and documentation.

Canonical references (docs/module-contract.md, docs/architecture.md, etc.)
are linked but not duplicated. This doc is transient — retire when the
refactor is fully behind us.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:14:09 +03:00
gavrielc 60ffe34396 Merge pull request #1853 from qwibitai/refactor/pr9-cli-channel
feat(channels): add CLI channel — talk to your agent from the terminal
2026-04-18 22:02:02 +03:00
gavrielc 131fc99700 feat(channels): add CLI channel — talk to your agent from the terminal
First default channel that ships with main. Unix-socket adapter + thin
client; plugs into the running daemon rather than spawning its own host.

## src/channels/cli.ts

- ChannelAdapter with channelType='cli', platformId='local'.
- setup() unlinks any stale socket, listens on $DATA_DIR/cli.sock (mode 0600
  so only the local user can connect).
- On client connect: reads newline-delimited JSON ({"text": "..."}) and
  calls config.onInbound('local', null, {id, kind:'chat', content, ts}).
- deliver() writes {"text": <body>} back to the connected socket; silently
  no-ops when no client is attached (outbound row still persists).
- Single-client policy: a second connection supersedes the first with a
  [superseded] notice.
- teardown() closes the client, closes the server, removes the socket file.

## scripts/chat.ts + pnpm run chat

One-shot client:
- pnpm run chat <message...>
- Connects to the socket, writes one JSON line with the message.
- Reads replies; exits 2s after the first reply lands (hard timeout 120s).
- ENOENT/ECONNREFUSED prints a hint to start the daemon.

## scripts/init-first-agent.ts

- Fix stale imports after earlier module extractions (permissions +
  agent-to-agent moved their DB helpers into modules/).
- After wiring the DM channel, also create cli/local messaging_group
  (unknown_sender_policy='public' — local socket perms handle auth) and
  wire it to the same agent. User can `pnpm run chat` immediately.

## package.json

- Add "chat": "tsx scripts/chat.ts" script.

## Validation

- pnpm run build clean.
- pnpm test — 137 host tests pass.
- bun test in container/agent-runner — 17 pass.
- Service boot logs: "CLI channel listening" + "Channel adapter started
  channel=cli type=cli". Clean SIGTERM shutdown; socket file removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:51:04 +03:00
gavrielc e5319b3e1e Merge pull request #1851 from qwibitai/refactor/pr8-slim-and-cli
refactor: relocate outbox I/O, dead-code sweep, add CLI harness
2026-04-18 21:34:37 +03:00
gavrielc 7169c25e70 refactor: relocate outbox I/O to session-manager + dead-code sweep
## Outbox extraction (delivery.ts → session-manager.ts)

File I/O for outbound attachments now lives in session-manager.ts alongside
the symmetric inbound extractAttachmentFiles. delivery.ts no longer touches
the filesystem — it hands buffers to the adapter and calls clearOutbox on
success.

- New `readOutboxFiles(agentGroupId, sessionId, messageId, filenames)` and
  `clearOutbox(agentGroupId, sessionId, messageId)` in session-manager.ts.
- deliverMessage in delivery.ts loses ~35 lines of fs/path code and its
  `fs`/`path` imports.

## Dead-code sweep

TypeScript's --noUnusedLocals surfaced several cruft imports. Fixed:

- src/container-runner.ts: drop unused `markContainerIdle` import; drop
  unused `session` parameter from `buildContainerArgs` signature.
- src/delivery.ts: drop unused `getSession`, `writeSessionMessage`,
  `wakeContainer` imports.
- src/host-sweep.ts: drop unused `updateSession`, `outboundDbPath` imports.
- container/agent-runner/src/poll-loop.ts: drop unused `config`,
  `processingIds` params from `processQuery`.
- Test files: drop unused imports in channel-registry.test, db-v2.test,
  host-core.test.

Skipped: `conversations` state in chat-sdk-bridge.ts (never read but
tangled with public `updateConversations` method; cleaning it risks a
merge conflict with the channels branch at the next sync).

## Validation

- `pnpm run build` clean
- `pnpm test` — 137 host tests pass
- `bun test` in container/agent-runner — 17 tests pass
- Service boots (`NanoClaw running`, `OneCLI approval handler started`)
  and shuts down cleanly on SIGTERM

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:34:08 +03:00
gavrielc 3d945db6eb Merge pull request #1849 from qwibitai/refactor/pr7-retier-approvals
refactor(modules): re-tier approvals as default, extract self-mod as optional
2026-04-18 19:54:49 +03:00
gavrielc 95fdec335a refactor(modules): re-tier approvals as default; extract self-mod as optional
Promotes approvals to the default tier with a public API (requestApproval +
registerApprovalHandler) that other modules consume. Self-modification
(install_packages / request_rebuild / add_mcp_server) moves into a new
optional module that registers delivery actions + matching approval handlers
via the new API.

## Approvals (default tier)

- Adds `src/modules/approvals/primitive.ts` exporting `requestApproval`,
  `registerApprovalHandler`, `notifyAgent`. Absorbs `pickApprover` /
  `pickApprovalDelivery` / `channelTypeOf` from the deleted `src/access.ts`.
- Rewrites `response-handler.ts` to dispatch to registered approval handlers
  on approve (action-keyed Map). Reject path is centralized.
- Drops the three self-mod-specific delivery-action registrations from
  `approvals/index.ts`; they belong to self-mod now.
- `onecli-approvals.ts` now imports picks from the primitive instead of
  `src/access.ts`.

## Self-mod (optional tier)

- New `src/modules/self-mod/` with request handlers (validate input + call
  requestApproval) and apply handlers (orchestration on approve).
- `apply.ts` owns updateContainerConfig + buildAgentGroupImage + killContainer
  calls. Self-mod depends on approvals (via registerApprovalHandler +
  requestApproval + notifyAgent) and on core (container-runner, container-config).
- Registers 3 delivery actions + 3 approval handlers at import time.

## Other changes

- `src/access.ts` and `src/access.test.ts` deleted. Tests split across
  `src/modules/approvals/picks.test.ts` (approver selection) and
  `src/modules/permissions/permissions.test.ts` (access + roles + DM).
- `src/modules/index.ts` barrel: approvals loads before self-mod so
  registerApprovalHandler is bound when self-mod registers at import time.

## Validation

- `pnpm run build` clean
- `pnpm test` — 137 host tests pass
- `bun test` in container/agent-runner — 17 tests pass
- Service starts; boot log shows `OneCLI approval handler started`,
  `NanoClaw running`; clean SIGTERM shutdown

Resolves the transitional tier violation flagged in PR #5 where core
imported from the permissions optional module via `src/access.ts`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:41:26 +03:00
gavrielc 1f179e07f2 Merge pull request #1848 from qwibitai/refactor/pr6-agent-to-agent
refactor(modules): extract agent-to-agent as registry-based module
2026-04-18 19:12:17 +03:00
gavrielc 46b19dcf9c refactor(modules): extract agent-to-agent as registry-based module
Last extraction of Phase 3. Moves inter-agent messaging + create_agent +
destination projection into src/modules/agent-to-agent/. Core retains:

- `channel_type === 'agent'` dispatch in delivery.ts, guarded by
  hasTable('agent_destinations') + dynamic import into module.
- Channel-permission ACL in delivery.ts, guarded by hasTable, with
  inlined SQL (no module import from core).
- writeDestinations call in container-runner.ts, guarded by hasTable +
  dynamic import into module.
- createMessagingGroupAgent's destination side effect in db/messaging-groups.ts,
  guarded by hasTable. This is a documented transitional tier violation
  (core imports from optional module), analogous to src/access.ts.

Migration `004-agent-destinations.ts` renamed to `module-agent-to-agent-
destinations.ts` preserving `name: 'agent-destinations'` so existing DBs
don't re-run it.

delivery.ts: 600 → 449 lines. handleSystemAction's last switch case gone
(just registry + default log-and-drop). notifyAgent helper removed (only
create_agent used it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:00:10 +03:00
gavrielc c80a23e24f Merge pull request #1847 from qwibitai/refactor/pr5-permissions
refactor(modules): extract permissions as optional module
2026-04-18 18:41:52 +03:00
gavrielc 32bcc2c5ae refactor(permissions): preserve pre-PR behavior in three spots
PR #5 review flagged three behavior changes that shouldn't have slipped
in. This commit reverts each to match the pre-refactor behavior exactly.

1. User upsert ordering. Split the router hook into two setters:
   setSenderResolver (runs before agent resolution) and setAccessGate
   (runs after). Restores the pre-PR sequence where the users row is
   upserted even if the message is dropped by wiring or trigger rules.

2. dropped_messages audit. Moved src/modules/permissions/db/dropped-messages.ts
   back to src/db/dropped-messages.ts. The table is core audit infra, not
   permissions-specific. Router re-writes rows for no_agent_wired and
   no_trigger_match; the access gate writes rows for policy refusals.

3. Permissionless container fallback. Dropped. poll-loop restores the
   original deny-all check when NANOCLAW_ADMIN_USER_IDS is empty.

Module contract doc updated with the two-hook shape.

Validation: host build clean, 137/137 host tests, 17/17 container
tests, typecheck clean, service boots to "NanoClaw running" with
permissions module registering both hooks and clean SIGTERM shutdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:00:10 +03:00
gavrielc 7cc4ecc3be refactor(modules): extract permissions as optional module
Moves user-roles / users / agent-group-members / user-dms /
dropped-messages / user-dm / canAccessAgentGroup into
src/modules/permissions/. Module registers a single inbound-gate that
owns sender resolution, access decision, unknown-sender policy, and
drop-audit recording.

Router slimmed from 357 → 179 lines; the inline fallback chain
(extractAndUpsertUser / enforceAccess / handleUnknownSender /
recordDroppedMessage) is gone — without the permissions module core
defaults to allow-all with userId=null.

container-runner's admin-ID query is now inline SQL guarded by
sqlite_master on user_roles, keeping core free of any import from the
permissions module. The container-side formatter falls back to
permissionless mode when NANOCLAW_ADMIN_USER_IDS is empty: every sender
with an identifiable senderId is treated as admin.

Module contract doc formalizes the tier model and the dependency rule
(core ← default modules ← optional modules). One transitional violation
flagged: src/access.ts (core) imports from the permissions module for
its remaining approver-picking helpers; resolves in the planned PR #7
re-tier.

Validation: host build clean, 137/137 host tests, 17/17 container
tests, typecheck clean, service boots to "NanoClaw running" with
permissions module registering its gate and clean SIGTERM shutdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:42:14 +03:00
gavrielc e75af5e44d Merge pull request #1842 from qwibitai/refactor/pr4-scheduling
refactor: extract scheduling as registry-based module (PR #4)
2026-04-18 17:23:27 +03:00
gavrielc 473f766585 refactor(modules): extract scheduling as registry-based module
Moves the scheduling surface — 5 delivery actions (schedule_task,
cancel_task, pause_task, resume_task, update_task), handleRecurrence,
applyPreTaskScripts, and task DB helpers — out of core and into
src/modules/scheduling/ (host) and container/agent-runner/src/scheduling/
(container).

First PR to fill the MODULE-HOOK markers introduced in PR #2:
  - src/host-sweep.ts MODULE-HOOK:scheduling-recurrence now dynamically
    imports handleRecurrence from the module each sweep tick.
  - container/agent-runner/src/poll-loop.ts MODULE-HOOK:scheduling-pre-task
    dynamically imports applyPreTaskScripts before the provider call.
    When the marker block is empty (scheduling uninstalled), `keep`
    falls back to `normalMessages` so non-task messages still flow.

The 5 task cases are removed from delivery.ts's handleSystemAction
switch — the registry now routes them. Task DB helpers moved out of
src/db/session-db.ts (which kept `nextEvenSeq` as a named export so
the module can uphold the host-writes-even-seq invariant). Test suite
split to match: scheduling-specific tests live in the module.

No migration — tasks are messages_in rows with kind='task'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:17:47 +03:00
gavrielc 71aab8c316 Merge pull request #1840 from qwibitai/refactor/pr3-approvals-interactive
refactor(modules): extract approvals + interactive as registry-based modules
2026-04-18 15:57:29 +03:00
gavrielc 626c565a70 fix(modules): break circular import TDZ between index.ts and modules
PR #3 introduced a circular-import temporal-dead-zone bug that didn't
surface in unit tests but crashed the service at startup:

  src/index.ts imports './modules/index.js' for side effects
  → src/modules/interactive/index.ts calls registerResponseHandler()
  → that function is declared in src/index.ts
  → but src/index.ts's const responseHandlers = [] hasn't been
    initialized yet (we're in the middle of its module-init)
  → ReferenceError: Cannot access 'responseHandlers' before initialization

Same issue for registerResponseHandler itself (the function reference
resolves to undefined) and for onShutdown in the approvals module.

Caught when the operator started the service and systemd flagged the
process as crashing in auto-restart loop.

Fix: extract responseHandlers + registerResponseHandler + shutdownCallbacks
+ onShutdown into src/response-registry.ts, which has no dependencies on
src/index.ts or on modules. index.ts re-exports the same surface for any
existing consumers; modules import directly from response-registry.js.

The bug was latent because:
- Unit tests import pieces, never src/index.ts's main() flow.
- Host builds clean because TypeScript doesn't catch runtime circular
  init order.
- Only surfaces when the ES module loader actually executes src/index.ts
  as the entry point.

Verified: service boots on Linux host with approvals + interactive
loaded; OneCLI handler starts via onDeliveryAdapterReady callback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:48:43 +03:00
gavrielc a4573395d9 refactor(modules): extract approvals + interactive as registry-based modules
Phase 2 / PR #3 of the module refactor. Moves the approval and interactive-
question flows out of core and into src/modules/, wired through the response
dispatcher and delivery action registries.

New modules:
- src/modules/interactive/ — registers a response handler that claims
  pending_questions rows, writes question_response to the session DB, wakes
  the container. createPendingQuestion call stays inline in delivery.ts
  (guarded by hasTable) per plan.
- src/modules/approvals/ — registers 3 delivery actions (install_packages,
  request_rebuild, add_mcp_server), a response handler for pending_approvals
  (including OneCLI action fall-through), an adapter-ready hook that boots
  the OneCLI manual-approval handler, and a shutdown hook that stops it.
  OneCLI implementation (src/onecli-approvals.ts) moves into the module.

Core lifecycle hooks added (narrow, not registries):
- onDeliveryAdapterReady(cb) in delivery.ts — fires when setDeliveryAdapter
  runs (or immediately if already set). Used by approvals for OneCLI boot.
- onShutdown(cb) in index.ts — fires on SIGTERM/SIGINT. Used by approvals
  for OneCLI teardown.
- getDeliveryAdapter() getter in delivery.ts — for live-flow adapter access
  in registered delivery actions.

Core shrinks: delivery.ts 911 → 665 lines, index.ts 405 → 224 lines.
dispatchResponse now logs "Unclaimed response" instead of falling through
to an inline handler — the inline fallback moved into the two modules.

Migration files renamed to the module-<name>-<short>.ts convention:
- 003-pending-approvals.ts → module-approvals-pending-approvals.ts
- 007-pending-approvals-title-options.ts → module-approvals-title-options.ts
Migration.name fields unchanged so existing DBs treat them as already-applied.

Degradation verified: emptying the modules barrel builds clean and 137/137
tests pass. Actions would log "Unknown system action"; button clicks would
log "Unclaimed response".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:16:53 +03:00
gavrielc a612c2ca24 Merge pull request #1839 from qwibitai/refactor/pr2-scaffolding
refactor: scaffold module registries + default-module layout (PR #2)
2026-04-18 14:48:50 +03:00
gavrielc 4202041d0b refactor: scaffold module registries and default-module layout
Additive change — existing code paths still run via inline fallbacks.
Prepares core for per-module extractions in PR #3 onward.

Four registries added with empty defaults:
  - delivery action handlers (delivery.ts)
  - router inbound gate (router.ts)
  - response dispatcher (index.ts)
  - MCP tool self-registration (container/agent-runner/src/mcp-tools/server.ts)

Default modules moved to src/modules/ for signaling:
  - src/modules/typing/       (extracted from delivery.ts)
  - src/modules/mount-security/ (moved from src/mount-security.ts)

Both are imported directly by core — no hook, no registry. Removal
requires editing core imports.

Migrator now keys applied rows by name (uniqueness) so module
migrations can pick arbitrary version numbers. Stored version column
is auto-assigned as an applied-order sequence.

sqlite_master guards added around core calls into module-owned tables
(user_roles, agent_destinations, pending_questions). No-ops today;
load-bearing after the owning modules are extracted.

MODULE-HOOK markers placed at scheduling's two skill-edit sites
(host-sweep.ts recurrence call, poll-loop.ts pre-task gate). PR #4
replaces the marked blocks when scheduling moves to its module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:46:19 +03:00
gavrielc 1888ecc1e9 Merge pull request #1838 from qwibitai/refactor/pr1-prep
refactor: PR #1 prep — v1 removal, module contract, sdk bump
2026-04-18 14:26:30 +03:00
gavrielc a1b227269e chore(deps): bump @onecli-sh/sdk to 0.3.1
Lockfile was pinned to 0.2.0 while package.json already declared
^0.3.1. The code depends on types added in 0.3.x (ApprovalRequest,
ManualApprovalHandle, configureManualApproval), so the host build
was failing on v2. Refreshing the lockfile resolves it.

0.3.1 was published 2026-04-10, well clear of minimumReleaseAge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:24:04 +03:00
gavrielc 53e8135102 docs: add module contract for refactor
Codifies the interface between core and modules: the four registries
(delivery actions, inbound gate, response dispatcher, MCP tool
self-registration), default modules (typing, mount-security),
guarded-inline fallbacks, MODULE-HOOK skill-edit markers, and module
migration naming.

Authoritative reference for downstream extraction PRs and install
skills. See REFACTOR_PLAN.md for broader context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:23:56 +03:00
gavrielc 86becf8dea chore: delete v1 reference code
Removes src/v1/ (37 files) and container/agent-runner/src/v1/ (3 files)
along with the v1 reference note in CLAUDE.md and the now-obsolete
tsconfig exclude. v1 was already out of the runtime path; this just
removes the dead weight.

~8,800 LOC removed, zero runtime change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:23:47 +03:00
gavrielc 27c52205f9 fix(channels): bridge openDM delegates to adapter directly
chat.openDM dispatches via inferAdapterFromUserId, which only recognizes
Discord/Slack/Teams/gChat formats and throws for everything else —
breaking approval delivery on Telegram (numeric IDs) and the other
direct-addressable channels the bridge now wraps. Delegate straight to
adapter.openDM + channelIdFromThreadId, and only expose openDM when the
underlying adapter implements it. Preserves the adapter's native
platform_id encoding (e.g. "telegram:<chatId>") so user_dms caches align
with the messaging_groups rows onInbound wrote.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 18:30:38 +03:00
gavrielc e93292de2a fix(agent-runner): spawn built-in MCP server with bun, not node
The Bun migration (c5d0ef8) dropped the in-image tsc build step, so
/app/src/mcp-tools/index.js never exists — only index.ts. The spawn
config in container/agent-runner/src/index.ts still pointed at
index.js and invoked it with `node`, which can't execute TypeScript
anyway. Net effect: every session failed to start the `nanoclaw`
MCP server, so scheduling, send_to_agent, interactive questions,
and self-mod tools were silently absent from the agent's toolset.

Matches entrypoint.sh and src/container-runner.ts, which already
use `exec bun run /app/src/index.ts` for the same reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:17:45 +03:00
gavrielc bd659fd7d6 style(v2/delivery): prettier reflow of test import
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:17:07 +03:00
gavrielc ac9535baa8 fix(v2/delivery): prevent double-send from overlapping delivery polls
The active poll (1s, running sessions) and sweep poll (60s, all active
sessions) both call deliverSessionMessages, and a running session is in
both result sets. Without locking they race on the same outbound row:
both read it as undelivered, both call the channel adapter, both
markDelivered. INSERT OR IGNORE hides the DB collision but the user has
already received the message twice.

Adds a per-session inflight guard; the second concurrent caller skips
and picks up any leftover work on the next poll tick.

Also makes outbox cleanup in deliverMessage best-effort: the message is
already on the user's screen, so a cleanup throw must not propagate to
the retry path (which would resend).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 15:17:06 +03:00
gavrielc bfc626be82 docs: drop v2 framing across CLAUDE.md and 12 docs
Renamed 12 docs/v2-*.md → docs/*.md (already in index from earlier git mv).
Rewrote CLAUDE.md to describe the codebase as just "the codebase" rather
than "v2"; added a "Channels and Providers (skill-installed)" section
reflecting the new model and updated the docs index links.

Agent (general-purpose) cleaned the 12 doc bodies:
- Dropped "NanoClaw v2" / "v2 schema" / "(v2)" prose throughout
- Rewrote inter-doc cross-references docs/v2-X.md → docs/X.md
- Architecture, agent-runner-details: collapsed v1↔v2 comparison tables
  into present-tense facts; added notes that trunk only ships `claude`
  and that channel adapters are skill-installed from the `channels` branch
- Setup-wiring, checklist: dropped v1→v2 migration items that no longer
  apply
- Frozen runtime paths preserved: data/v2.db, data/v2-sessions/,
  container name nanoclaw-v2

git grep confirms remaining `\bv2\b` matches in docs/ are only those
runtime paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:53:21 +03:00
gavrielc c37609ffc8 docs(skills): drop "v2" from skill content + src/index.ts log lines
Cleans up the prose-level v2 references that the rename commit didn't
touch. Skills now describe themselves and the codebase without "v2"
versioning language. /add-X-v2 cross-references in setup, init-first-agent,
and manage-channels updated to /add-X.

Runtime path identifiers (data/v2.db, data/v2-sessions/, container name
nanoclaw-v2) deliberately left as-is — renaming them breaks live installs
without commensurate benefit.

Verified: pnpm run build clean, 326 host tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:42:55 +03:00
gavrielc 00fb1bee4a chore(skills): rename add-*-v2 → add-* and drop dead v1 channel skills
Renamed 13 skill folders to drop the -v2 suffix (the v2/v1 distinction
isn't load-bearing anymore — there is no v1 runtime). Deleted the four
v1 channel skills that occupied the rename target paths (add-discord,
add-slack, add-telegram, add-whatsapp); they targeted src/v1 which is
reference-only per CLAUDE.md.

Skill content still says "v2" in places — that's a follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:38:19 +03:00
gavrielc 4857512267 refactor(v2): move setup/groups.ts off trunk + drop pino dep
setup/groups.ts is whatsapp-only — its inline syncScript imports baileys
and pino to fetch group metadata via Baileys.groupFetchAllParticipating.
On trunk it was a no-op for non-whatsapp users (returned early without
auth) and the only thing keeping pino alive.

Removed:
- setup/groups.ts (lives on `channels` branch; restored by /add-whatsapp-v2)
- `groups` STEPS entry from setup/index.ts
- pino from package.json (no longer used outside the moved file)

/add-whatsapp-v2 skill updated to copy setup/groups.ts and register both
groups + whatsapp-auth in setup/index.ts STEPS, install pino@9.6.0 along
with baileys + qrcode.

Verified: build clean, 326 host tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:34:16 +03:00
gavrielc 712720eef4 refactor(v2): drop @chat-adapter/shared dep — duck-type NetworkError
The only use was channel-registry.ts checking `err instanceof NetworkError`
to retry transient setup failures. Switched to a duck-type predicate
(`err.name === 'NetworkError'`) so the dep is no longer needed at trunk
level. Channel skills bring it in transitively when they install their
Chat SDK adapter package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:33:22 +03:00
gavrielc a3376c25df docs(channel-skills): rewrite all 13 /add-*-v2 skills for copy-from-channels-branch pattern
Each skill now:
- Pre-flight: checks for file present + import line + dep listed (idempotent)
- git fetch origin channels
- git show origin/channels:<paths> > <paths> to copy adapter (and helpers/tests/setup-step where applicable)
- Append `import './<chan>.js';` to src/channels/index.ts
- pnpm install <pkg>@<pinned-version>
- pnpm run build

Telegram additionally copies 4 helper/test files + setup/pair-telegram.ts
and registers `'pair-telegram':` in setup/index.ts STEPS.

WhatsApp (native) additionally copies setup/whatsapp-auth.ts and
registers `'whatsapp-auth':` in setup/index.ts STEPS, installs
@whiskeysockets/baileys + qrcode + @types/qrcode pinned.

All credential / next-steps / channel-info / troubleshooting sections
preserved verbatim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:24:20 +03:00
gavrielc 437ba63a2f refactor(v2): move channel adapters off v2 trunk
v2 ships with no channels baked in. All channel adapters (discord, slack,
telegram + helpers, whatsapp, whatsapp-cloud, gchat, github, imessage,
linear, matrix, resend, teams, webex) and their channel-specific setup
steps (pair-telegram, whatsapp-auth) now live on the `channels` branch
and get copied in via /add-*-v2 skills.

Removed:
- src/channels/{discord,slack,telegram*,whatsapp*,gchat,github,imessage,linear,matrix,resend,teams,webex}.ts
- setup/{pair-telegram,whatsapp-auth}.ts
- 14 channel-specific deps from package.json (@chat-adapter/*, @beeper/*,
  @bitbasti/*, @resend/chat-sdk-adapter, @whiskeysockets/baileys,
  chat-adapter-imessage, qrcode, @chat-adapter/state-memory unused)
- Their corresponding STEPS entries from setup/index.ts
- Channel imports from src/channels/index.ts

Kept:
- Channel infra: adapter.ts, channel-registry.ts (+ test), chat-sdk-bridge.ts,
  ask-question.ts, an empty-imports index.ts
- Chat SDK runtime (`chat`) for channels that copy in via Chat SDK bridge
- @chat-adapter/shared promoted from transitive to direct dep
  (channel-registry.ts uses NetworkError from it)

Verified: pnpm run build clean, 326 host tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:20:28 +03:00
gavrielc 32a973f1cd docs(add-opencode): rewrite skill for copy-from-providers-branch pattern
v2 no longer ships opencode on trunk. The skill now:
- Fetches origin/providers
- Copies opencode source files to their target paths
- Appends self-registration imports to both provider barrels
- Adds @opencode-ai/sdk@1.4.3 as a pinned agent-runner dep
- Adds OPENCODE_VERSION ARG + opencode-ai pnpm global install to Dockerfile
- Rebuilds host + container

All steps idempotent. Credential/env/Zen docs unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:14:10 +03:00
gavrielc 03dd559786 style: trim trailing whitespace in providers barrel
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:12:35 +03:00
gavrielc e0258e8c1b refactor(v2): move opencode provider off v2 trunk
v2 ships with only claude baked in. opencode now lives on the `providers`
branch and gets copied in via the /add-opencode skill.

Removed:
- src/providers/opencode.ts
- container/agent-runner/src/providers/{opencode,mcp-to-opencode}.ts + test
- @opencode-ai/sdk from agent-runner package.json + bun.lock
- opencode-ai global install + OPENCODE_VERSION ARG from Dockerfile
- opencode self-registration imports from both provider barrels
- opencode test case from factory.test.ts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:10:56 +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
gavrielc 0c67fbf456 Merge branch 'main' into skill/wiki 2026-04-05 11:29:01 +03:00
gavrielc 15e356a572 chore: revert unrelated db.ts formatting change
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 11:27:01 +03:00
gavrielc 33b5627f42 chore: rename skill to add-karpathy-llm-wiki
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 11:25:57 +03:00
gavrielc f69979fb9e fix: simplify source handling step and fix typo in wiki skill
Remove hardcoded file path checks. Step 4 now discusses source types
with the user and helps install needed skills dynamically. Fix "use use"
typo and change curl example to file download.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 11:24:51 +03:00
gavrielc 54bf4543f2 refactor: rework wiki skill to use Karpathy's original text as reference
Remove pre-written container skill. Instead, include llm-wiki.md
(Karpathy's gist) as the reference material and have the setup skill
guide the user through collaboratively building their own wiki schema,
container skill, and directory structure based on the pattern.

Add NanoClaw-specific notes: image vision, PDF reader, voice
transcription, curl for full document fetch, file attachment handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 10:07:48 +03:00
gavrielc 36943fbcfd feat: add /add-wiki skill for persistent LLM Wiki knowledge bases
Container skill teaches the agent to maintain a structured, interlinked
wiki from ingested sources. Feature skill bootstraps the setup — directory
structure, group CLAUDE.md, optional scheduled lint.

Based on Karpathy's LLM Wiki pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:58:40 +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
gavrielc 391b729623 Merge pull request #1634 from qwibitai/skill/migrate-nanoclaw
Skill/migrate nanoclaw
2026-04-05 00:29:42 +03:00
gavrielc 3703c9decb feat: suggest /migrate-nanoclaw when user is far behind upstream
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:28:14 +03:00
gavrielc c5cb97b761 Merge pull request #1633 from qwibitai/skill/migrate-from-openclaw
Skill/migrate from openclaw
2026-04-05 00:23:20 +03:00
gavrielc 761d3a1b30 feat: add migrated_from_openclaw field to setup diagnostics
Tracks whether users came through the OpenClaw migration path during setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:22:13 +03:00
github-actions[bot] cbb4da19c7 docs: update token count to 43.7k tokens · 22% of context window 2026-04-04 21:11:05 +00:00
github-actions[bot] b752e5cd34 chore: bump version to 1.2.49 2026-04-04 21:11:00 +00:00
gavrielc a74be06956 Merge pull request #1632 from qwibitai/feat/session-cleanup-pr
feat: auto-prune stale session artifacts
2026-04-05 00:10:49 +03:00
Gavriel Cohen d4a6b4a3b5 fix: portable stat and subshell variable mutation in cleanup script
- Replace macOS-only `stat -f%z` with portable `wc -c` for Linux compat
- Replace `find | while` pipes with process substitution so TOTAL_FREED
  counter survives the loop (pipe runs in subshell, losing mutations)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:09:28 +03:00
Gavriel Cohen 67020f9fbf feat: auto-prune stale session artifacts on startup + daily
Session files (JSONLs, debug logs, todos, telemetry, group logs) accumulate
unboundedly — especially from daily cron tasks. This adds a cleanup script
that prunes old artifacts while protecting active sessions (read from DB),
and wires it into the main process on a 24h interval.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:03:00 +03:00
github-actions[bot] 9019e4e3b8 docs: update token count to 43.5k tokens · 22% of context window 2026-04-04 20:47:43 +00:00
github-actions[bot] 8a02170b21 chore: bump version to 1.2.48 2026-04-04 20:47:36 +00:00
gavrielc db3440f662 feat: upgrade agent SDK to 0.2.92 with 1M context and 200k auto-compact
Use sonnet[1m] for full 1M context window and set auto-compact at 200k
tokens to keep costs down while preserving access to extended context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:47:17 +03:00
gavrielc b2a5a58f8a feat: add /migrate-from-openclaw skill for guided OpenClaw migration
Conversational migration skill that reads an existing OpenClaw installation
and interactively guides users through importing identity, personality,
channel credentials, groups, scheduled tasks, MCP servers, skills, and
plugins into NanoClaw.

8-phase flow: discovery → groups/architecture → settings → identity/memory
→ channel credentials → scheduled tasks → MCP/webhooks/config → summary.

Includes:
- discover-openclaw.ts: finds OpenClaw state dir, parses JSON5 config,
  detects channels (both channels.* and legacy top-level format), groups
  (handles agent:main: prefixed session keys), workspace files (reads
  custom agent.workspace path), skills, config-registered plugins with
  API keys, cron jobs, MCP servers. Dumps raw config keys for robustness.
- extract-channel-credentials.ts: resolves SecretRef formats (plain,
  env template, object), writes credentials directly to .env via
  --write-env flag (never exposes raw values to stdout)
- MIGRATE_CRONS.md: extracted reference for cron job migration, loaded
  only when cron jobs exist
- migration-state.md: persistent state file for recovery after compaction
- Setup hook: detects ~/.openclaw during /setup and offers migration

Tested against real ~/.clawdbot and remote ~/.openclaw installations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 19:27:14 +03:00
gavrielc 426ae0285e feat: add diagnostics telemetry to migrate-nanoclaw skill
Matches the pattern used by /setup and /update-nanoclaw. Captures
migration-specific properties (tier, phase, customization count,
skill interactions). Opt-out permanently disables across all skills.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:13:22 +03:00
gavrielc 7ef1c4f5e0 fix: apply lessons from real-world migration test run
Based on analysis of a live migration (v1.2.42 -> v1.2.47):

1. Absolute worktree paths: Bash tool resets cwd between calls,
   so relative cd .upgrade-worktree fails. Store PROJECT_ROOT and
   WORKTREE as absolute paths, use them throughout.

2. Smarter tier assessment: discount files from skill merges when
   counting — a fork with 3 skills and no other changes is Tier 2,
   not Tier 3 just because 24 files changed.

3. Inter-skill conflict analysis: new "Skill Interactions" section
   in the migration guide captures conflicts between applied skills
   (duplicate declarations, conflicting env var handling).

4. Cleaner swap recipe: use git reset --hard to the upgrade commit
   instead of git checkout -B intermediate branch. Backup tag
   preserves rollback. Copy guide to /tmp before worktree removal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:20:13 +03:00
gavrielc f60bb3c3d5 feat: add /migrate-nanoclaw skill for intent-based upgrades
Replaces merge-based upgrades with a two-phase approach:
1. Extract: analyzes user's fork, captures customizations as a
   migration guide (intent + implementation details in markdown)
2. Upgrade: checks out clean upstream in a worktree, reapplies
   customizations from the guide, validates, and swaps in

Key features:
- Tiered complexity (lightweight/standard/complex)
- Sub-agent exploration with haiku for efficient analysis
- Incremental guide updates instead of full re-extraction
- Live e2e testing via worktree symlinks before swapping
- New-changes guard prevents losing unrecorded work

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 22:06:35 +03:00
github-actions[bot] 3608f05233 docs: update token count to 43.4k tokens · 22% of context window 2026-04-03 13:18:25 +00:00
github-actions[bot] 8f28cde41d chore: bump version to 1.2.47 2026-04-03 13:18:15 +00:00
gavrielc 032ba77a7f feat: mount store rw for main agent and add requiresTrigger to register_group
- Mount store/ separately as read-write so the main agent can access
  the SQLite database directly.
- Add requiresTrigger parameter to the register_group MCP tool
  (host IPC already supported it, but the tool never exposed it).
  Defaults to false (no trigger).
- Update group registration instructions to ask user about trigger.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:17:57 +03:00
gavrielc e9db4d461d Update SKILL.md 2026-04-03 12:49:38 +03:00
gavrielc 584114118d Merge pull request #1610 from qwibitai/fix/changelog-breaking-changes
docs: breaking change entries for Apple Container and pino removal
2026-04-03 12:41:22 +03:00
Gavriel Cohen bf11109825 docs: update breaking changes and Apple Container skill security
- Update OneCLI breaking change entry to note Apple Container alternative
- Add breaking change for pino removal affecting WhatsApp users
- Add credential proxy network binding phase to /convert-to-apple-container
  skill with private/public network guidance and macOS firewall setup
- Add Apple Container networking contributors

Co-Authored-By: MrBlaise <3867275+MrBlaise@users.noreply.github.com>
Co-Authored-By: lbsnrs <47463+lbsnrs@users.noreply.github.com>
Co-Authored-By: spencer-whitman <28708638+spencer-whitman@users.noreply.github.com>
Co-Authored-By: lazure-ocean <43110733+lazure-ocean@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:40:23 +03:00
gavrielc 6f93b20cd1 fix: relax breaking change detection to match [BREAKING] anywhere in line
Previously required `[BREAKING]` at the start of the line, missing
entries formatted as `- [BREAKING] ...` in changelogs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:25:57 +03:00
github-actions[bot] f23a54aea0 docs: update token count to 43.3k tokens · 22% of context window 2026-04-02 17:05:48 +00:00
github-actions[bot] 6e0653f537 chore: bump version to 1.2.46 2026-04-02 17:05:44 +00:00
exe.dev user ee599b9f0c feat: add reply/quoted message context support
Add generic reply context fields to NewMessage (reply_to_message_id,
reply_to_message_content, reply_to_sender_name) so any channel can
pass quoted message context to the agent.

- Add thread_id and reply_to_* fields to NewMessage interface
- Add DB migration for reply context columns on messages table
- Update storeMessage/getMessagesSince/getNewMessages to persist and
  retrieve reply fields
- Render reply context as <quoted_message> XML in formatMessages
- Add DB and formatting tests

Co-Authored-By: Alfred-the-buttler <leon.alfred.bot@gmail.com>
Co-Authored-By: moktamd <moktamd@users.noreply.github.com>
Co-Authored-By: gurixs-carson <gurixs-carson@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:05:24 +00:00
exe.dev user 7b337a7a07 docs: add Telegram channel contributors
Co-Authored-By: Carl Schmidt <carl.schmidt@gmail.com>
Co-Authored-By: Alfred-the-buttler <leon.alfred.bot@gmail.com>
Co-Authored-By: moktamd <moktamd@users.noreply.github.com>
Co-Authored-By: gurixs-carson <gurixs-carson@users.noreply.github.com>
2026-04-02 17:01:28 +00:00
gavrielc 3e2895987b Merge pull request #1595 from glifocat/patch-1
Add Contributor Covenant Code of Conduct
2026-04-02 18:24:29 +03:00
glifocat 22f5d55855 Add Contributor Covenant Code of Conduct
Added Contributor Covenant Code of Conduct to outline community standards and enforcement guidelines.
2026-04-02 12:58:30 +02:00
github-actions[bot] 51f50bbe85 docs: update token count to 43.0k tokens · 22% of context window 2026-04-01 18:53:24 +00:00
github-actions[bot] 4c7bc80299 chore: bump version to 1.2.45 2026-04-01 18:53:21 +00:00
gavrielc 87e89147c9 style: run prettier on container/agent-runner/src/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:53:02 +03:00
github-actions[bot] 7b0d79a6f3 chore: bump version to 1.2.44 2026-04-01 18:51:18 +00:00
gavrielc 468c6170a0 style: run prettier and eslint on src/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:50:59 +03:00
github-actions[bot] 4c8b9cda93 docs: update token count to 42.6k tokens · 21% of context window 2026-03-30 22:28:03 +00:00
github-actions[bot] 78bfb8df85 chore: bump version to 1.2.43 2026-03-30 22:27:59 +00:00
gavrielc a86641f69e Merge pull request #1546 from bitcryptic-gw/fix/stale-session-recovery
fix: auto-recover from stale Claude Code session on exit code 1
2026-03-31 01:27:48 +03:00
gavrielc 59c09effcb Merge branch 'main' into fix/stale-session-recovery 2026-03-31 01:20:19 +03:00
gavrielc 001ee6ec48 fix: correct stale session regex and remove duplicate retry logic
The original regex didn't match the actual error ("No conversation
found with session ID: ..."). Added `no conversation found` pattern.

Removed the inline retry — clearing the session and returning 'error'
lets the existing group-queue.ts backoff loop retry with a fresh
session naturally. Simpler, no duplicate error paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 01:17:27 +03:00
gavrielc 9d97f79476 Merge pull request #1552 from huahang/fix-npm-audit
fix: Fix npm audit errors
2026-03-31 00:29:46 +03:00
huahang d675859c24 fix: Fix npm audit errors
````
4 vulnerabilities (2 moderate, 2 high)

To address all issues, run:
  npm audit fix
````

Signed-off-by: huahang <huahang.liu@gmail.com>
2026-03-30 23:12:49 +08:00
Gary Walker 38009be263 fix: auto-recover from stale Claude Code session on exit code 1
When Claude Code exits with code 1 during a session resume because the
session transcript file no longer exists (ENOENT on .jsonl), clear the
stale session from SQLite and retry once with a fresh session.

Detection is targeted: only triggers on ENOENT referencing a .jsonl
file or explicit "session not found" errors. Transient failures
(network, API) fall through to the normal backoff retry path.

Also removes unrelated ollama files that were mixed in during rebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 23:03:44 +11:00
gavrielc 3098f28b74 Merge branch 'main' into fix/stale-session-recovery 2026-03-30 10:59:57 +03:00
Gary Walker 474346e214 fix: recover from stale Claude Code session IDs instead of retrying infinitely
When Claude Code exits with code 1 during a session resume, the group's
session ID is now cleared from the database and the query is retried with
a fresh session. This prevents the infinite retry loop that occurred when
a stale/corrupt session ID was stored in SQLite.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:09:56 +11:00
gavrielc 29839464bf fix: setup skill skips /use-native-credential-proxy for apple container
The apple-container branch already includes the credential proxy code.
Applying /use-native-credential-proxy on top would conflict. Setup now
inlines the credential collection steps instead of delegating.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:25:56 +03:00
gavrielc a3fb3beb6a docs: warn about silently wrong auto-merges in maintenance guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:59:57 +03:00
Gary Walker 54a8648c95 feat: add model management tools to add-ollama-tool skill
Adds four new MCP tools to the existing ollama integration, consolidating
model management (from #1331) into the single add-ollama-tool skill as
requested by @gavrielc:

- ollama_pull_model  — pull a model from the Ollama registry
- ollama_delete_model — delete a local model to free disk space
- ollama_show_model  — inspect modelfile, parameters, and architecture
- ollama_list_running — list models loaded in memory with VRAM/processor info

All four tools follow the existing patterns in this file: OLLAMA_HOST env
var, ollamaFetch() with host.docker.internal fallback, log() and
writeStatus() helpers. No changes to index.ts or container-runner.ts
needed — OLLAMA_HOST is already forwarded via sdkEnv.

Also updates SKILL.md description, tool list, verify steps, and adds a
troubleshooting entry for large-model pull timeouts.

Closes #1331.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 12:08:54 +11:00
github-actions[bot] 69348510e9 Merge branch 'main' into skill/ollama-tool 2026-03-24 16:05:22 +00:00
github-actions[bot] 17a72938be Merge branch 'main' into skill/ollama-tool 2026-03-24 15:55:59 +00:00
github-actions[bot] 4511644d0d Merge branch 'main' into skill/ollama-tool 2026-03-24 15:46:04 +00:00
github-actions[bot] 86063e0ea0 Merge branch 'main' into skill/ollama-tool 2026-03-24 15:45:48 +00:00
github-actions[bot] d1ce15a4de Merge branch 'main' into skill/ollama-tool 2026-03-24 12:56:05 +00:00
github-actions[bot] 5b24dd4d2e Merge branch 'main' into skill/ollama-tool 2026-03-24 12:55:52 +00:00
github-actions[bot] 0d8f7f8668 Merge branch 'main' into skill/ollama-tool 2026-03-22 14:55:18 +00:00
github-actions[bot] fff32f3028 Merge branch 'main' into skill/ollama-tool 2026-03-21 11:11:06 +00:00
github-actions[bot] 1bb065e655 Merge branch 'main' into skill/ollama-tool 2026-03-21 11:09:15 +00:00
github-actions[bot] ea7561a978 Merge branch 'main' into skill/ollama-tool 2026-03-21 11:08:59 +00:00
github-actions[bot] cfc4b6c28e Merge branch 'main' into skill/ollama-tool 2026-03-21 10:21:22 +00:00
github-actions[bot] dad98b0a8f Merge branch 'main' into skill/ollama-tool 2026-03-21 09:57:40 +00:00
github-actions[bot] 3e41e54e10 Merge branch 'main' into skill/ollama-tool 2026-03-21 09:54:53 +00:00
github-actions[bot] 4d9f0288ee Merge branch 'main' into skill/ollama-tool 2026-03-19 19:05:57 +00:00
github-actions[bot] 972edd14f6 Merge branch 'main' into skill/ollama-tool 2026-03-19 19:05:42 +00:00
github-actions[bot] fd59ff0ec9 Merge branch 'main' into skill/ollama-tool 2026-03-19 19:03:46 +00:00
github-actions[bot] e2e32219c9 Merge branch 'main' into skill/ollama-tool 2026-03-19 19:03:28 +00:00
github-actions[bot] c601aaa947 Merge branch 'main' into skill/ollama-tool 2026-03-19 11:53:11 +00:00
github-actions[bot] d43d53244f Merge branch 'main' into skill/ollama-tool 2026-03-18 10:10:51 +00:00
github-actions[bot] e8326bae62 Merge branch 'main' into skill/ollama-tool 2026-03-18 09:52:36 +00:00
github-actions[bot] d71ffaf7ef Merge branch 'main' into skill/ollama-tool 2026-03-18 09:52:24 +00:00
github-actions[bot] 5b5ee91aa7 Merge branch 'main' into skill/ollama-tool 2026-03-16 17:37:28 +00:00
github-actions[bot] 2007471f4f Merge branch 'main' into skill/ollama-tool 2026-03-16 17:37:12 +00:00
github-actions[bot] 9e90c0712e Merge branch 'main' into skill/ollama-tool 2026-03-14 15:24:11 +00:00
github-actions[bot] 2317302745 Merge branch 'main' into skill/ollama-tool 2026-03-14 15:23:56 +00:00
github-actions[bot] b247357e0d Merge branch 'main' into skill/ollama-tool 2026-03-14 13:26:24 +00:00
github-actions[bot] 4dd27adb84 Merge branch 'main' into skill/ollama-tool 2026-03-13 11:59:53 +00:00
github-actions[bot] cc4f03a203 Merge branch 'main' into skill/ollama-tool 2026-03-13 11:59:19 +00:00
github-actions[bot] 4bc232e513 Merge branch 'main' into skill/ollama-tool 2026-03-11 10:30:55 +00:00
github-actions[bot] c9d1569702 Merge branch 'main' into skill/ollama-tool 2026-03-11 10:25:50 +00:00
github-actions[bot] 5b20e2908a Merge branch 'main' into skill/ollama-tool 2026-03-10 20:59:53 +00:00
github-actions[bot] 089fcea474 Merge branch 'main' into skill/ollama-tool 2026-03-10 20:52:17 +00:00
github-actions[bot] bd64fd667d Merge branch 'main' into skill/ollama-tool 2026-03-10 20:40:11 +00:00
github-actions[bot] f0ac7fbb6d Merge branch 'main' into skill/ollama-tool 2026-03-10 00:25:51 +00:00
gavrielc 207addfa19 Merge commit '4afb5bd' into rebuild-fork 2026-03-10 01:15:07 +02:00
gavrielc 4afb5bd9f1 Merge remote-tracking branch 'origin/main' into skill/ollama-tool 2026-03-09 23:21:01 +02:00
gavrielc dfcdfcac11 Merge remote-tracking branch 'origin/main' into skill/ollama-tool 2026-03-09 00:07:58 +02:00
gavrielc d33e514d04 Merge remote-tracking branch 'origin/main' into skill/ollama-tool 2026-03-08 23:24:40 +02:00
gavrielc 4cb13b2b60 skill/ollama-tool: local Ollama model inference via MCP
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:15:05 +02:00
289 changed files with 28523 additions and 15392 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. 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 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.
+54 -159
View File
@@ -1,203 +1,98 @@
---
name: add-discord
description: Add Discord bot channel integration to NanoClaw.
description: Add Discord bot channel integration via Chat SDK.
---
# Add Discord Channel
This skill adds Discord support to NanoClaw, then walks through interactive setup.
Adds Discord bot support via the Chat SDK bridge.
## Phase 1: Pre-flight
## Install
### Check if already applied
NanoClaw doesn't ship channels in trunk. This skill copies the Discord adapter in from the `channels` branch.
Check if `src/channels/discord.ts` exists. If it does, skip to Phase 3 (Setup). The code changes are already in place.
### Pre-flight (idempotent)
### Ask the user
Skip to **Credentials** if all of these are already in place:
Use `AskUserQuestion` to collect configuration:
- `src/channels/discord.ts` exists
- `src/channels/index.ts` contains `import './discord.js';`
- `@chat-adapter/discord` is listed in `package.json` dependencies
AskUserQuestion: Do you have a Discord bot token, or do you need to create one?
Otherwise continue. Every step below is safe to re-run.
If they have one, collect it now. If not, we'll create one in Phase 3.
## Phase 2: Apply Code Changes
### Ensure channel remote
### 1. Fetch the channels branch
```bash
git remote -v
git fetch origin channels
```
If `discord` is missing, add it:
### 2. Copy the adapter
```bash
git remote add discord https://github.com/qwibitai/nanoclaw-discord.git
git show origin/channels:src/channels/discord.ts > src/channels/discord.ts
```
### Merge the skill branch
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if the line is already present):
```typescript
import './discord.js';
```
### 4. Install the adapter package (pinned)
```bash
git fetch discord main
git merge discord/main || {
git checkout --theirs package-lock.json
git add package-lock.json
git merge --continue
}
pnpm install @chat-adapter/discord@4.26.0
```
This merges in:
- `src/channels/discord.ts` (DiscordChannel class with self-registration via `registerChannel`)
- `src/channels/discord.test.ts` (unit tests with discord.js mock)
- `import './discord.js'` appended to the channel barrel file `src/channels/index.ts`
- `discord.js` npm dependency in `package.json`
- `DISCORD_BOT_TOKEN` in `.env.example`
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
### Validate code changes
### 5. Build
```bash
npm install
npm run build
npx vitest run src/channels/discord.test.ts
pnpm run build
```
All tests must pass (including the new Discord tests) and build must be clean before proceeding.
## Credentials
## Phase 3: Setup
### Create Discord Bot
### Create Discord Bot (if needed)
If the user doesn't have a bot token, tell them:
> I need you to create a 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., "Andy Assistant")
> 3. Go to the **Bot** tab on the left sidebar
> 4. Click **Reset Token** to generate a new bot token — copy it immediately (you can only see it once)
> 5. Under **Privileged Gateway Intents**, enable:
> - **Message Content Intent** (required to read message text)
> - **Server Members Intent** (optional, for member display names)
> 6. Go to **OAuth2** > **URL Generator**:
> - Scopes: select `bot`
> - Bot Permissions: select `Send Messages`, `Read Message History`, `View Channels`
> - Copy the generated URL and open it in your browser to invite the bot to your server
Wait for the user to provide the token.
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=<their-token>
DISCORD_BOT_TOKEN=your-bot-token
DISCORD_APPLICATION_ID=your-application-id
DISCORD_PUBLIC_KEY=your-public-key
```
Channels auto-enable when their credentials are present — no extra configuration needed.
Sync to container: `mkdir -p data/env && cp .env data/env/env`
Sync to container environment:
## Next Steps
```bash
mkdir -p data/env && cp .env data/env/env
```
If you're in the middle of `/setup`, return to the setup flow now.
The container reads environment from `data/env/env`, not `.env` directly.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
### Build and restart
## Channel Info
```bash
npm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
```
## Phase 4: Registration
### Get Channel ID
Tell the user:
> To get the channel ID for registration:
>
> 1. In Discord, go to **User Settings** > **Advanced** > Enable **Developer Mode**
> 2. Right-click the text channel you want the bot to respond in
> 3. Click **Copy Channel ID**
>
> The channel ID will be a long number like `1234567890123456`.
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.
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
```
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
```
## Phase 5: Verify
### Test the connection
Tell the user:
> Send a message in your registered Discord channel:
> - For main channel: Any message works
> - For non-main: @mention the bot in Discord
>
> The bot should respond within a few seconds.
### Check logs if needed
```bash
tail -f logs/nanoclaw.log
```
## Troubleshooting
### Bot not responding
1. Check `DISCORD_BOT_TOKEN` is set in `.env` AND synced to `data/env/env`
2. Check channel is registered: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE 'dc:%'"`
3. For non-main channels: message must include trigger pattern (@mention the bot)
4. Service is running: `launchctl list | grep nanoclaw`
5. Verify the bot has been invited to the server (check OAuth2 URL was used)
### Bot only responds to @mentions
This is the default behavior for non-main channels (`requiresTrigger: true`). To change:
- Update the registered group's `requiresTrigger` to `false`
- Or register the channel as the main channel
### Message Content Intent not enabled
If the bot connects but can't read messages, ensure:
1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
2. Select your application > **Bot** tab
3. Under **Privileged Gateway Intents**, enable **Message Content Intent**
4. Restart NanoClaw
### Getting Channel ID
If you can't copy the channel ID:
- Ensure **Developer Mode** is enabled: User Settings > Advanced > Developer Mode
- Right-click the channel name in the server sidebar > Copy Channel ID
## After Setup
The Discord bot supports:
- Text messages in registered channels
- Attachment descriptions (images, videos, files shown as placeholders)
- Reply context (shows who the user is replying to)
- @mention translation (Discord `<@botId>` → NanoClaw trigger format)
- Message splitting for responses over 2000 characters
- Typing indicators while the agent processes
- **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.
+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
+92
View File
@@ -0,0 +1,92 @@
---
name: add-gchat
description: Add Google Chat channel integration via Chat SDK.
---
# Add Google Chat Channel
Adds Google Chat support via the Chat SDK bridge.
## Install
NanoClaw doesn't ship channels in trunk. This skill copies the Google Chat adapter in from the `channels` branch.
### Pre-flight (idempotent)
Skip to **Credentials** if all of these are already in place:
- `src/channels/gchat.ts` exists
- `src/channels/index.ts` contains `import './gchat.js';`
- `@chat-adapter/gchat` is listed in `package.json` dependencies
Otherwise continue. Every step below is safe to re-run.
### 1. Fetch the channels branch
```bash
git fetch origin channels
```
### 2. Copy the adapter
```bash
git show origin/channels:src/channels/gchat.ts > src/channels/gchat.ts
```
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if the line is already present):
```typescript
import './gchat.js';
```
### 4. Install the adapter package (pinned)
```bash
pnpm install @chat-adapter/gchat@4.26.0
```
### 5. Build
```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
+94
View File
@@ -0,0 +1,94 @@
---
name: add-github
description: Add GitHub channel integration via Chat SDK. PR and issue comment threads as conversations.
---
# Add GitHub Channel
Adds GitHub support via the Chat SDK bridge. The agent participates in PR and issue comment threads.
## Install
NanoClaw doesn't ship channels in trunk. This skill copies the GitHub adapter in from the `channels` branch.
### Pre-flight (idempotent)
Skip to **Credentials** if all of these are already in place:
- `src/channels/github.ts` exists
- `src/channels/index.ts` contains `import './github.js';`
- `@chat-adapter/github` is listed in `package.json` dependencies
Otherwise continue. Every step below is safe to re-run.
### 1. Fetch the channels branch
```bash
git fetch origin channels
```
### 2. Copy the adapter
```bash
git show origin/channels:src/channels/github.ts > src/channels/github.ts
```
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if the line is already present):
```typescript
import './github.js';
```
### 4. Install the adapter package (pinned)
```bash
pnpm install @chat-adapter/github@4.26.0
```
### 5. Build
```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
+113
View File
@@ -0,0 +1,113 @@
---
name: add-imessage
description: Add iMessage channel integration via Chat SDK. Local (macOS) or remote (Photon API) mode.
---
# Add iMessage Channel
Adds iMessage support via the Chat SDK bridge. Two modes: local (macOS with Full Disk Access) or remote (Photon API).
## Install
NanoClaw doesn't ship channels in trunk. This skill copies the iMessage adapter in from the `channels` branch.
### Pre-flight (idempotent)
Skip to **Credentials** if all of these are already in place:
- `src/channels/imessage.ts` exists
- `src/channels/index.ts` contains `import './imessage.js';`
- `chat-adapter-imessage` is listed in `package.json` dependencies
Otherwise continue. Every step below is safe to re-run.
### 1. Fetch the channels branch
```bash
git fetch origin channels
```
### 2. Copy the adapter
```bash
git show origin/channels:src/channels/imessage.ts > src/channels/imessage.ts
```
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if the line is already present):
```typescript
import './imessage.js';
```
### 4. Install the adapter package (pinned)
```bash
pnpm install chat-adapter-imessage@0.1.1
```
### 5. Build
```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.
@@ -0,0 +1,110 @@
---
name: add-karpathy-llm-wiki
description: Add a persistent wiki knowledge base to a NanoClaw group. Based on Karpathy's LLM Wiki pattern. Triggers on "add wiki", "wiki", "knowledge base", "llm wiki", "karpathy wiki".
---
# Add Karpathy LLM Wiki
Set up a persistent wiki knowledge base on NanoClaw, based on Karpathy's LLM Wiki pattern.
## Step 1: Read the pattern
Read `${CLAUDE_SKILL_DIR}/llm-wiki.md` — this is the full LLM Wiki idea as written by Karpathy. Understand it thoroughly before proceeding. Summarize the core idea to the user briefly, then discuss what they want to build.
## Step 2: Choose a group
AskUserQuestion: "Which group should have the wiki?"
1. **Main group** — add to your existing main chat
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 `pnpm exec tsx setup/index.ts --step register`.
## Step 3: Design collaboratively
Discuss with the user based on the pattern:
- What's the wiki's domain or topic?
- What kinds of sources will they add? (URLs, PDFs, images, voice notes, books, transcripts)
- Do they want the full three-layer architecture or a lighter version?
- Any specific conventions they care about? (The pattern intentionally leaves this open.)
Based on this discussion, create three things:
### 3a. Directory structure
Create `wiki/` and `sources/` directories in the group folder. Create initial `index.md` and `log.md` per the pattern's Indexing and Logging section. Adapt to the user's domain.
### 3b. Container skill
Create a `container/skills/wiki/SKILL.md` tailored to this user's wiki. This is the schema layer from the pattern — it tells the agent how to maintain the wiki. Base it on the pattern's Operations section (ingest, query, lint) and the conventions you agreed on with the user. Don't over-prescribe — the pattern says "your LLM figures out the rest."
### 3c. Group CLAUDE.md
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
Based on the source types the user plans to ingest (discussed in Step 3), check whether the agent can already handle those formats — some are supported natively, others need a skill (e.g. `/add-image-vision`, `/add-pdf-reader`, `/add-voice-transcription`). If a needed capability isn't installed, check if there's an available skill for it and help the user get it set up.
### URL handling note
claude has built-in `WebFetch`, but it returns a summary, not the full document. For wiki ingestion of a URL where the full text matters, the container skill and CLAUDE.md should instruct claude to use bash commands to download full files instead. For example:
```bash
curl -sLo sources/filename.pdf "<url>"
```
If the document is a webpage, then claude can use fetch or `agent-browser` to open the page and extract full text if available. The container skill and CLAUDE.md should note this so claude gets full content for sources rather than summaries.
## Step 5: Optional lint schedule
AskUserQuestion: "Want periodic wiki health checks?"
1. **Weekly**
2. **Monthly**
3. **Skip** — lint manually
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
pnpm run build
./container/build.sh
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
```
Tell the user to test by sending a source to the wiki group.
@@ -0,0 +1,75 @@
# LLM Wiki
> Source: [karpathy/llm-wiki.md](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f)
A pattern for building personal knowledge bases using LLMs.
This is an idea file, designed to be copied to your own LLM Agent (e.g. OpenAI Codex, Claude Code, OpenCode / Pi, etc.). Its goal is to communicate the high-level idea, with your agent building out specifics through collaboration with you.
## The Core Idea
Most interactions with LLMs and documents follow RAG patterns: upload files, retrieve relevant chunks at query time, generate answers. The knowledge is re-derived on each question with no accumulation.
The concept here differs fundamentally. Rather than just retrieving from raw documents, the LLM incrementally builds and maintains a persistent wiki — a structured, interlinked markdown collection sitting between you and raw sources. When adding new material, the LLM reads it, extracts key information, and integrates it into existing wiki pages—updating entities, revising summaries, flagging contradictions, strengthening synthesis. Knowledge compiles once and stays current rather than re-deriving on every query.
The wiki becomes a persistent, compounding artifact. Cross-references already exist. Contradictions are flagged. Synthesis reflects everything read. The wiki enriches with every source added and question asked.
You source material and ask questions; the LLM maintains everything—summarizing, cross-referencing, filing, and organizing. The LLM acts as programmer; Obsidian serves as IDE; the wiki functions as codebase.
**Applications include:**
- Personal: tracking goals, health, self-improvement
- Research: deep dives over weeks/months
- Reading: building companion wikis while progressing through books
- Business/teams: internal wikis fed by Slack, transcripts, documents
- Analysis: competitive research, due diligence, trip planning, hobby deep-dives
## Architecture
Three layers comprise the system:
**Raw sources** — immutable curated documents (articles, papers, images, data). The LLM reads but never modifies these.
**The wiki** — LLM-generated markdown directories containing summaries, entity pages, concept pages, comparisons, syntheses. The LLM owns this entirely, creating and updating pages while maintaining cross-references and consistency.
**The schema** — configuration document (e.g., CLAUDE.md) explaining wiki structure, conventions, and workflows for ingestion, querying, and maintenance. This key file transforms the LLM into disciplined wiki maintainer rather than generic chatbot.
## Operations
**Ingest:** Drop new sources into the raw collection; the LLM processes them. The agent reads sources, discusses takeaways, writes summaries, updates indexes, refreshes entity and concept pages, logs entries. Single sources might touch 10-15 wiki pages. Prefer ingesting individually while staying involved, though batch ingestion with less oversight is possible.
**Query:** Ask questions against the wiki. The LLM searches relevant pages, synthesizes answers with citations. Answers take various forms—markdown pages, comparison tables, slide decks, charts, canvas. Good answers can be filed back into the wiki as new pages—explorations compound in the knowledge base rather than disappearing into chat history.
**Lint:** Periodically health-check the wiki. Look for contradictions, stale claims superseded by newer sources, orphan pages lacking inbound links, important concepts lacking dedicated pages, missing cross-references, data gaps. The LLM suggests investigations and sources to pursue, keeping the wiki healthy as it grows.
## Indexing and Logging
Two special files help navigate the growing wiki:
**index.md** — content-oriented catalog of everything (each page with link, one-line summary, optional metadata like dates or source counts), organized by category. The LLM updates it on every ingest. When answering queries, read the index first to locate relevant pages before drilling deeper. This approach works surprisingly well at moderate scale (~100 sources, ~hundreds of pages) while avoiding embedding-based RAG infrastructure needs.
**log.md** — append-only chronological record of what happened and when (ingests, queries, lint passes). Each entry beginning with consistent prefix (e.g., `## [2026-04-02] ingest | Article Title`) becomes parseable with simple tools—`grep "^## \[" log.md | tail -5` yields last 5 entries. The log shows wiki evolution timeline and helps the LLM understand recent activity.
## Optional: CLI Tools
At scale, small tools help the LLM operate more efficiently. Search engine over wiki pages is most obvious—at small scale the index suffices, but as the wiki grows, proper search becomes necessary. qmd (https://github.com/tobi/qmd) offers local search with hybrid BM25/vector search and LLM re-ranking, entirely on-device. It includes both CLI (so LLMs can shell out) and MCP server (native tool integration). Build simpler custom search scripts as needs arise.
## Tips and Tricks
- **Obsidian Web Clipper** converts web articles to markdown for quick source collection
- **Download images locally:** Set attachment folder in Obsidian Settings, bind download hotkey. All images store locally; LLM views and references directly instead of relying on potentially broken URLs
- **Obsidian's graph view** visualizes wiki connectivity—what connects to what, hub pages, orphans
- **Marp** provides markdown-based slide deck format with Obsidian plugin integration
- **Dataview** plugin queries page frontmatter, generating dynamic tables/lists when LLM adds YAML frontmatter
- The wiki is simply a git-backed markdown directory—version history, branching, collaboration included
## Why This Works
Knowledge base maintenance's tedious part is bookkeeping, not reading/thinking: updating cross-references, keeping summaries current, noting data contradictions, maintaining consistency across pages. Humans abandon wikis as maintenance burden outpaces value. LLMs don't bore, don't forget updates, can touch 15 files in one pass. Wiki maintenance becomes nearly free.
Humans curate sources, direct analysis, ask good questions, think about meaning. LLMs handle everything else.
This relates in spirit to Vannevar Bush's 1945 Memex—personal curated knowledge stores with associative document trails. Bush's vision resembled this more than what the web became: private, actively curated, with connections between documents as valuable as documents themselves. Bush couldn't solve maintenance; LLMs handle that.
## Note
This document intentionally remains abstract, describing the idea rather than specific implementation. Directory structure, schema conventions, page formats, tooling—all depend on domain, preferences, and LLM choice. Everything is optional and modular. Pick what's useful; ignore what isn't. Your sources might be text-only (no image handling needed). Your wiki might stay small enough that index files suffice (no search engine required). You might want different output formats entirely. Share this with your LLM agent and work collaboratively to instantiate a version fitting your needs. This document's sole purpose is communicating the pattern; your LLM figures out the rest.
+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
+91
View File
@@ -0,0 +1,91 @@
---
name: add-linear
description: Add Linear channel integration via Chat SDK. Issue comment threads as conversations.
---
# Add Linear Channel
Adds Linear support via the Chat SDK bridge. The agent participates in issue comment threads.
## Install
NanoClaw doesn't ship channels in trunk. This skill copies the Linear adapter in from the `channels` branch.
### Pre-flight (idempotent)
Skip to **Credentials** if all of these are already in place:
- `src/channels/linear.ts` exists
- `src/channels/index.ts` contains `import './linear.js';`
- `@chat-adapter/linear` is listed in `package.json` dependencies
Otherwise continue. Every step below is safe to re-run.
### 1. Fetch the channels branch
```bash
git fetch origin channels
```
### 2. Copy the adapter
```bash
git show origin/channels:src/channels/linear.ts > src/channels/linear.ts
```
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if the line is already present):
```typescript
import './linear.js';
```
### 4. Install the adapter package (pinned)
```bash
pnpm install @chat-adapter/linear@4.26.0
```
### 5. Build
```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
+91
View File
@@ -0,0 +1,91 @@
---
name: add-matrix
description: Add Matrix channel integration via Chat SDK. Works with any Matrix homeserver.
---
# Add Matrix Channel
Adds Matrix support via the Chat SDK bridge.
## Install
NanoClaw doesn't ship channels in trunk. This skill copies the Matrix adapter in from the `channels` branch.
### Pre-flight (idempotent)
Skip to **Credentials** if all of these are already in place:
- `src/channels/matrix.ts` exists
- `src/channels/index.ts` contains `import './matrix.js';`
- `@beeper/chat-adapter-matrix` is listed in `package.json` dependencies
Otherwise continue. Every step below is safe to re-run.
### 1. Fetch the channels branch
```bash
git fetch origin channels
```
### 2. Copy the adapter
```bash
git show origin/channels:src/channels/matrix.ts > src/channels/matrix.ts
```
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if the line is already present):
```typescript
import './matrix.js';
```
### 4. Install the adapter package (pinned)
```bash
pnpm install @beeper/chat-adapter-matrix@0.2.0
```
### 5. Build
```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
```
+187
View File
@@ -0,0 +1,187 @@
---
name: add-opencode
description: Use OpenCode as an agent provider (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
NanoClaw runs agents in a long-lived **poll loop** inside the container. The backend is selected with **`AGENT_PROVIDER`** (`claude` | `opencode` | `mock`).
Trunk ships with only the `claude` provider baked in. This skill copies the OpenCode provider files in from the `providers` branch, wires them into the host and container barrels, installs dependencies, and rebuilds the image.
## Install
### Pre-flight
If all of the following are already present, skip to **Configuration**:
- `src/providers/opencode.ts`
- `container/agent-runner/src/providers/opencode.ts`
- `import './opencode.js';` line in `src/providers/index.ts`
- `import './opencode.js';` line in `container/agent-runner/src/providers/index.ts`
- `@opencode-ai/sdk` in `container/agent-runner/package.json`
- `opencode-ai@${OPENCODE_VERSION}` in the pnpm global-install block in `container/Dockerfile`
Missing pieces — continue below. All steps are idempotent; re-running is safe.
### 1. Fetch the providers branch
```bash
git fetch origin providers
```
### 2. Copy the OpenCode source files
Wholesale copies (owned entirely by this skill — user edits to these files won't survive a re-run, as designed):
```bash
git show origin/providers:src/providers/opencode.ts > src/providers/opencode.ts
git show origin/providers:container/agent-runner/src/providers/opencode.ts > container/agent-runner/src/providers/opencode.ts
git show origin/providers:container/agent-runner/src/providers/mcp-to-opencode.ts > container/agent-runner/src/providers/mcp-to-opencode.ts
git show origin/providers:container/agent-runner/src/providers/mcp-to-opencode.test.ts > container/agent-runner/src/providers/mcp-to-opencode.test.ts
git show origin/providers:container/agent-runner/src/providers/opencode.factory.test.ts > container/agent-runner/src/providers/opencode.factory.test.ts
```
### 3. Append the self-registration imports
Each barrel gets one line appended at the end — skip if the line is already present.
`src/providers/index.ts`:
```typescript
import './opencode.js';
```
`container/agent-runner/src/providers/index.ts`:
```typescript
import './opencode.js';
```
### 4. Add the agent-runner dependency
Pinned. Bump deliberately, not with `bun update`.
```bash
cd container/agent-runner && bun add @opencode-ai/sdk@1.4.3 && cd -
```
### 5. Add `opencode-ai` to the container Dockerfile
Two edits to `container/Dockerfile`, both idempotent (skip if already present):
**(a)** In the "Pin CLI versions" ARG block (around line 18), add after `ARG VERCEL_VERSION=latest`:
```dockerfile
ARG OPENCODE_VERSION=latest
```
**(b)** In the `pnpm install -g` block (around line 80), append `"opencode-ai@${OPENCODE_VERSION}"` to the list:
```dockerfile
pnpm install -g \
"@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
"agent-browser@${AGENT_BROWSER_VERSION}" \
"vercel@${VERCEL_VERSION}" \
"opencode-ai@${OPENCODE_VERSION}"
```
### 6. Build
```bash
pnpm run build # host
pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit # container typecheck
./container/build.sh # agent image
```
## 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`. 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
```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)
Zen's 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`** (DB `agent_provider`) means "run the **OpenCode agent provider**." Separately, **`OPENCODE_PROVIDER=opencode`** in `.env` is OpenCode's **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 Zen's 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
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-loop's 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.js" container/agent-runner/src/providers/index.ts && echo "container barrel: OK"
grep -q "./opencode.js" src/providers/index.ts && echo "host barrel: OK"
grep -q "@opencode-ai/sdk" container/agent-runner/package.json && echo "agent-runner dep: OK"
grep -q "opencode-ai@" container/Dockerfile && echo "Dockerfile install: OK"
cd container/agent-runner && bun test src/providers/ && cd -
```
+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
+93
View File
@@ -0,0 +1,93 @@
---
name: add-resend
description: Add Resend (email) channel integration via Chat SDK.
---
# Add Resend Email Channel
Connect NanoClaw to email via Resend for async email conversations.
## Install
NanoClaw doesn't ship channels in trunk. This skill copies the Resend adapter in from the `channels` branch.
### Pre-flight (idempotent)
Skip to **Credentials** if all of these are already in place:
- `src/channels/resend.ts` exists
- `src/channels/index.ts` contains `import './resend.js';`
- `@resend/chat-sdk-adapter` is listed in `package.json` dependencies
Otherwise continue. Every step below is safe to re-run.
### 1. Fetch the channels branch
```bash
git fetch origin channels
```
### 2. Copy the adapter
```bash
git show origin/channels:src/channels/resend.ts > src/channels/resend.ts
```
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if the line is already present):
```typescript
import './resend.js';
```
### 4. Install the adapter package (pinned)
```bash
pnpm install @resend/chat-sdk-adapter@0.1.1
```
### 5. 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
+61 -156
View File
@@ -1,80 +1,82 @@
---
name: add-slack
description: Add Slack as a channel. Can replace WhatsApp entirely or run alongside it. Uses Socket Mode (no public URL needed).
description: Add Slack channel integration via Chat SDK.
---
# Add Slack Channel
This skill adds Slack support to NanoClaw, then walks through interactive setup.
Adds Slack support via the Chat SDK bridge.
## Phase 1: Pre-flight
## Install
### Check if already applied
NanoClaw doesn't ship channels in trunk. This skill copies the Slack adapter in from the `channels` branch.
Check if `src/channels/slack.ts` exists. If it does, skip to Phase 3 (Setup). The code changes are already in place.
### Pre-flight (idempotent)
### Ask the user
Skip to **Credentials** if all of these are already in place:
**Do they already have a Slack app configured?** If yes, collect the Bot Token and App Token now. If no, we'll create one in Phase 3.
- `src/channels/slack.ts` exists
- `src/channels/index.ts` contains `import './slack.js';`
- `@chat-adapter/slack` is listed in `package.json` dependencies
## Phase 2: Apply Code Changes
Otherwise continue. Every step below is safe to re-run.
### Ensure channel remote
### 1. Fetch the channels branch
```bash
git remote -v
git fetch origin channels
```
If `slack` is missing, add it:
### 2. Copy the adapter
```bash
git remote add slack https://github.com/qwibitai/nanoclaw-slack.git
git show origin/channels:src/channels/slack.ts > src/channels/slack.ts
```
### Merge the skill branch
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if the line is already present):
```typescript
import './slack.js';
```
### 4. Install the adapter package (pinned)
```bash
git fetch slack main
git merge slack/main || {
git checkout --theirs package-lock.json
git add package-lock.json
git merge --continue
}
pnpm install @chat-adapter/slack@4.26.0
```
This merges in:
- `src/channels/slack.ts` (SlackChannel class with self-registration via `registerChannel`)
- `src/channels/slack.test.ts` (46 unit tests)
- `import './slack.js'` appended to the channel barrel file `src/channels/index.ts`
- `@slack/bolt` npm dependency in `package.json`
- `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` in `.env.example`
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
### Validate code changes
### 5. Build
```bash
npm install
npm run build
npx vitest run src/channels/slack.test.ts
pnpm run build
```
All tests must pass (including the new Slack tests) and build must be clean before proceeding.
## Credentials
## Phase 3: Setup
### Create Slack App
### Create Slack App (if needed)
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**
If the user doesn't have a Slack app, share [SLACK_SETUP.md](SLACK_SETUP.md) which has step-by-step instructions with screenshots guidance, troubleshooting, and a token reference table.
### Enable DMs
Quick summary of what's needed:
1. Create a Slack app at [api.slack.com/apps](https://api.slack.com/apps)
2. Enable Socket Mode and generate an App-Level Token (`xapp-...`)
3. Subscribe to bot events: `message.channels`, `message.groups`, `message.im`
4. Add OAuth scopes: `chat:write`, `channels:history`, `groups:history`, `im:history`, `channels:read`, `groups:read`, `users:read`
5. Install to workspace and copy the Bot Token (`xoxb-...`)
6. Go to **App Home** and enable the **Messages Tab**
7. Check **"Allow users to send Slash commands and messages from the messages tab"**
Wait for the user to provide both tokens.
### 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
@@ -82,126 +84,29 @@ Add to `.env`:
```bash
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
SLACK_SIGNING_SECRET=your-signing-secret
```
Channels auto-enable when their credentials are present — no extra configuration needed.
Sync to container: `mkdir -p data/env && cp .env data/env/env`
Sync to container environment:
### Webhook server
```bash
mkdir -p data/env && cp .env data/env/env
```
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.
The container reads environment from `data/env/env`, not `.env` directly.
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`.
### Build and restart
## Next Steps
```bash
npm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
```
If you're in the middle of `/setup`, return to the setup flow now.
## Phase 4: Registration
Otherwise, run `/manage-channels` to wire this channel to an agent group.
### Get Channel ID
## Channel Info
Tell the user:
> 1. Add the bot to a Slack channel (right-click channel → **View channel details** → **Integrations** → **Add apps**)
> 2. In that channel, the channel ID is in the URL when you open it in a browser: `https://app.slack.com/client/T.../C0123456789` — the `C...` part is the channel ID
> 3. Alternatively, right-click the channel name → **Copy link** — the channel ID is the last path segment
>
> The JID format for NanoClaw is: `slack:C0123456789`
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.
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
```
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
```
## Phase 5: Verify
### Test the connection
Tell the user:
> Send a message in your registered Slack channel:
> - For main channel: Any message works
> - For non-main: `@<assistant-name> hello` (using the configured trigger word)
>
> The bot should respond within a few seconds.
### Check logs if needed
```bash
tail -f logs/nanoclaw.log
```
## Troubleshooting
### Bot not responding
1. Check `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` are set in `.env` AND synced to `data/env/env`
2. Check channel is registered: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE 'slack:%'"`
3. For non-main channels: message must include trigger pattern
4. Service is running: `launchctl list | grep nanoclaw`
### Bot connected but not receiving messages
1. Verify Socket Mode is enabled in the Slack app settings
2. Verify the bot is subscribed to the correct events (`message.channels`, `message.groups`, `message.im`)
3. Verify the bot has been added to the channel
4. Check that the bot has the required OAuth scopes
### Bot not seeing messages in channels
By default, bots only see messages in channels they've been explicitly added to. Make sure to:
1. Add the bot to each channel you want it to monitor
2. Check the bot has `channels:history` and/or `groups:history` scopes
### "missing_scope" errors
If the bot logs `missing_scope` errors:
1. Go to **OAuth & Permissions** in your Slack app settings
2. Add the missing scope listed in the error message
3. **Reinstall the app** to your workspace — scope changes require reinstallation
4. Copy the new Bot Token (it changes on reinstall) and update `.env`
5. Sync: `mkdir -p data/env && cp .env data/env/env`
6. Restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`
### Getting channel ID
If the channel ID is hard to find:
- In Slack desktop: right-click channel → **Copy link** → extract the `C...` ID from the URL
- In Slack web: the URL shows `https://app.slack.com/client/TXXXXXXX/C0123456789`
- Via API: `curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" "https://slack.com/api/conversations.list" | jq '.channels[] | {id, name}'`
## After Setup
The Slack channel supports:
- **Public channels** — Bot must be added to the channel
- **Private channels** — Bot must be invited to the channel
- **Direct messages** — Users can DM the bot directly
- **Multi-channel** — Can run alongside WhatsApp or other channels (auto-enabled by credentials)
## Known Limitations
- **Threads are flattened** — Threaded replies are delivered to the agent as regular channel messages. The agent sees them but has no awareness they originated in a thread. Responses always go to the channel, not back into the thread. Users in a thread will need to check the main channel for the bot's reply. Full thread-aware routing (respond in-thread) requires pipeline-wide changes: database schema, `NewMessage` type, `Channel.sendMessage` interface, and routing logic.
- **No typing indicator** — Slack's Bot API does not expose a typing indicator endpoint. The `setTyping()` method is a no-op. Users won't see "bot is typing..." while the agent works.
- **Message splitting is naive** — Long messages are split at a fixed 4000-character boundary, which may break mid-word or mid-sentence. A smarter split (on paragraph or sentence boundaries) would improve readability.
- **No file/image handling** — The bot only processes text content. File uploads, images, and rich message blocks are not forwarded to the agent.
- **Channel metadata sync is unbounded** — `syncChannelMetadata()` paginates through all channels the bot is a member of, but has no upper bound or timeout. Workspaces with thousands of channels may experience slow startup.
- **Workspace admin policies not detected** — If the Slack workspace restricts bot app installation, the setup will fail at the "Install to Workspace" step with no programmatic detection or guidance. See SLACK_SETUP.md troubleshooting section.
- **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.
+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
+207
View File
@@ -0,0 +1,207 @@
---
name: add-teams
description: Add Microsoft Teams channel integration via Chat SDK.
---
# Add Microsoft Teams Channel
Connect NanoClaw to Microsoft Teams for interactive chat in team channels, group chats, and direct messages.
## Install
NanoClaw doesn't ship channels in trunk. This skill copies the Teams adapter in from the `channels` branch.
### Pre-flight (idempotent)
Skip to **Credentials** if all of these are already in place:
- `src/channels/teams.ts` exists
- `src/channels/index.ts` contains `import './teams.js';`
- `@chat-adapter/teams` is listed in `package.json` dependencies
Otherwise continue. Every step below is safe to re-run.
### 1. Fetch the channels branch
```bash
git fetch origin channels
```
### 2. Copy the adapter
```bash
git show origin/channels:src/channels/teams.ts > src/channels/teams.ts
```
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if the line is already present):
```typescript
import './teams.js';
```
### 4. Install the adapter package (pinned)
```bash
pnpm install @chat-adapter/teams@4.26.0
```
### 5. 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
+62 -168
View File
@@ -1,214 +1,108 @@
---
name: add-telegram
description: Add Telegram as a channel. Can replace WhatsApp entirely or run alongside it. Also configurable as a control-only channel (triggers actions) or passive channel (receives notifications only).
description: Add Telegram channel integration via Chat SDK.
---
# Add Telegram Channel
This skill adds Telegram support to NanoClaw, then walks through interactive setup.
Adds Telegram bot support via the Chat SDK bridge.
## Phase 1: Pre-flight
## Install
### Check if already applied
NanoClaw doesn't ship channels in trunk. This skill copies the Telegram adapter, its formatting/pairing helpers, their tests, and the `pair-telegram` setup step in from the `channels` branch.
Check if `src/channels/telegram.ts` exists. If it does, skip to Phase 3 (Setup). The code changes are already in place.
### Pre-flight (idempotent)
### Ask the user
Skip to **Credentials** if all of these are already in place:
Use `AskUserQuestion` to collect configuration:
- `src/channels/telegram.ts`, `telegram-pairing.ts`, `telegram-markdown-sanitize.ts` (and their `.test.ts` siblings) all exist
- `src/channels/index.ts` contains `import './telegram.js';`
- `setup/pair-telegram.ts` exists and `setup/index.ts`'s `STEPS` map contains `'pair-telegram':`
- `@chat-adapter/telegram` is listed in `package.json` dependencies
AskUserQuestion: Do you have a Telegram bot token, or do you need to create one?
Otherwise continue. Every step below is safe to re-run.
If they have one, collect it now. If not, we'll create one in Phase 3.
## Phase 2: Apply Code Changes
### Ensure channel remote
### 1. Fetch the channels branch
```bash
git remote -v
git fetch origin channels
```
If `telegram` is missing, add it:
### 2. Copy the adapter, helpers, tests, and setup step
```bash
git remote add telegram https://github.com/qwibitai/nanoclaw-telegram.git
git show origin/channels:src/channels/telegram.ts > src/channels/telegram.ts
git show origin/channels:src/channels/telegram-pairing.ts > src/channels/telegram-pairing.ts
git show origin/channels:src/channels/telegram-pairing.test.ts > src/channels/telegram-pairing.test.ts
git show origin/channels:src/channels/telegram-markdown-sanitize.ts > src/channels/telegram-markdown-sanitize.ts
git show origin/channels:src/channels/telegram-markdown-sanitize.test.ts > src/channels/telegram-markdown-sanitize.test.ts
git show origin/channels:setup/pair-telegram.ts > setup/pair-telegram.ts
```
### Merge the skill branch
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if already present):
```typescript
import './telegram.js';
```
### 4. Register the setup step
In `setup/index.ts`, add this entry to the `STEPS` map (right after the `register` line is fine; skip if already present):
```typescript
'pair-telegram': () => import('./pair-telegram.js'),
```
### 5. Install the adapter package (pinned)
```bash
git fetch telegram main
git merge telegram/main || {
git checkout --theirs package-lock.json
git add package-lock.json
git merge --continue
}
pnpm install @chat-adapter/telegram@4.26.0
```
This merges in:
- `src/channels/telegram.ts` (TelegramChannel class with self-registration via `registerChannel`)
- `src/channels/telegram.test.ts` (unit tests with grammy mock)
- `import './telegram.js'` appended to the channel barrel file `src/channels/index.ts`
- `grammy` npm dependency in `package.json`
- `TELEGRAM_BOT_TOKEN` in `.env.example`
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
### Validate code changes
### 6. Build
```bash
npm install
npm run build
npx vitest run src/channels/telegram.test.ts
pnpm run build
```
All tests must pass (including the new Telegram tests) and build must be clean before proceeding.
## Credentials
## Phase 3: Setup
### Create Telegram Bot
### Create Telegram Bot (if needed)
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`)
If the user doesn't have a bot token, tell them:
**Important for group chats**: By default, Telegram bots only see @mentions and commands in groups. To let the bot see all messages:
> I need you to create a Telegram bot:
>
> 1. Open Telegram and search for `@BotFather`
> 2. Send `/newbot` and follow prompts:
> - Bot name: Something friendly (e.g., "Andy Assistant")
> - Bot username: Must end with "bot" (e.g., "andy_ai_bot")
> 3. Copy the bot token (looks like `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`)
Wait for the user to provide the token.
1. Open `@BotFather` > `/mybots` > select your bot
2. **Bot Settings** > **Group Privacy** > **Turn off**
### Configure environment
Add to `.env`:
```bash
TELEGRAM_BOT_TOKEN=<their-token>
TELEGRAM_BOT_TOKEN=your-bot-token
```
Channels auto-enable when their credentials are present — no extra configuration needed.
Sync to container: `mkdir -p data/env && cp .env data/env/env`
Sync to container environment:
## Next Steps
```bash
mkdir -p data/env && cp .env data/env/env
```
If you're in the middle of `/setup`, return to the setup flow now.
The container reads environment from `data/env/env`, not `.env` directly.
Otherwise, run `/manage-channels` to wire this channel to an agent group.
### Disable Group Privacy (for group chats)
## Channel Info
Tell the user:
> **Important for group chats**: By default, Telegram bots only see @mentions and commands in groups. To let the bot see all messages:
>
> 1. Open Telegram and search for `@BotFather`
> 2. Send `/mybots` and select your bot
> 3. Go to **Bot Settings** > **Group Privacy** > **Turn off**
>
> This is optional if you only want trigger-based responses via @mentioning the bot.
### Build and restart
```bash
npm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
```
## Phase 4: Registration
### Get Chat ID
Tell the user:
> 1. Open your bot in Telegram (search for its username)
> 2. Send `/chatid` — it will reply with the chat ID
> 3. For groups: add the bot to the group first, then send `/chatid` in the group
Wait for the user to provide the chat ID (format: `tg:123456789` or `tg:-1001234567890`).
### Register the chat
The chat ID, name, and folder name are needed. Use `npx 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
```
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
```
## Phase 5: Verify
### Test the connection
Tell the user:
> Send a message to your registered Telegram chat:
> - For main chat: Any message works
> - For non-main: `@Andy hello` or @mention the bot
>
> The bot should respond within a few seconds.
### Check logs if needed
```bash
tail -f logs/nanoclaw.log
```
## Troubleshooting
### Bot not responding
Check:
1. `TELEGRAM_BOT_TOKEN` is set in `.env` AND synced to `data/env/env`
2. Chat is registered in SQLite (check with: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE 'tg:%'"`)
3. For non-main chats: message includes trigger pattern
4. Service is running: `launchctl list | grep nanoclaw` (macOS) or `systemctl --user status nanoclaw` (Linux)
### Bot only responds to @mentions in groups
Group Privacy is enabled (default). Fix:
1. `@BotFather` > `/mybots` > select bot > **Bot Settings** > **Group Privacy** > **Turn off**
2. Remove and re-add the bot to the group (required for the change to take effect)
### Getting chat ID
If `/chatid` doesn't work:
- Verify token: `curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getMe"`
- Check bot is started: `tail -f logs/nanoclaw.log`
## After Setup
If running `npm run dev` while the service is active:
```bash
# macOS:
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
npm run dev
# When done testing:
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
# Linux:
# systemctl --user stop nanoclaw
# npm run dev
# systemctl --user start nanoclaw
```
## Removal
To remove Telegram integration:
1. Delete `src/channels/telegram.ts` and `src/channels/telegram.test.ts`
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)
- **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.
+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
+88
View File
@@ -0,0 +1,88 @@
---
name: add-webex
description: Add Webex channel integration via Chat SDK.
---
# Add Webex Channel
Adds Cisco Webex support via the Chat SDK bridge.
## Install
NanoClaw doesn't ship channels in trunk. This skill copies the Webex adapter in from the `channels` branch.
### Pre-flight (idempotent)
Skip to **Credentials** if all of these are already in place:
- `src/channels/webex.ts` exists
- `src/channels/index.ts` contains `import './webex.js';`
- `@bitbasti/chat-adapter-webex` is listed in `package.json` dependencies
Otherwise continue. Every step below is safe to re-run.
### 1. Fetch the channels branch
```bash
git fetch origin channels
```
### 2. Copy the adapter
```bash
git show origin/channels:src/channels/webex.ts > src/channels/webex.ts
```
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if the line is already present):
```typescript
import './webex.js';
```
### 4. Install the adapter package (pinned)
```bash
pnpm install @bitbasti/chat-adapter-webex@0.1.0
```
### 5. Build
```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,95 @@
---
name: add-whatsapp-cloud
description: Add WhatsApp Business Cloud API channel via Chat SDK. Official Meta API.
---
# Add WhatsApp Cloud API Channel
Connect NanoClaw to WhatsApp via the official Meta WhatsApp Business Cloud API.
## Install
NanoClaw doesn't ship channels in trunk. This skill copies the WhatsApp Cloud adapter in from the `channels` branch.
### Pre-flight (idempotent)
Skip to **Credentials** if all of these are already in place:
- `src/channels/whatsapp-cloud.ts` exists
- `src/channels/index.ts` contains `import './whatsapp-cloud.js';`
- `@chat-adapter/whatsapp` is listed in `package.json` dependencies
Otherwise continue. Every step below is safe to re-run.
### 1. Fetch the channels branch
```bash
git fetch origin channels
```
### 2. Copy the adapter
```bash
git show origin/channels:src/channels/whatsapp-cloud.ts > src/channels/whatsapp-cloud.ts
```
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if the line is already present):
```typescript
import './whatsapp-cloud.js';
```
### 4. Install the adapter package (pinned)
```bash
pnpm install @chat-adapter/whatsapp@4.26.0
```
### 5. 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.
+114 -222
View File
@@ -1,20 +1,81 @@
---
name: add-whatsapp
description: Add WhatsApp as a channel. Can replace other channels entirely or run alongside them. Uses QR code or pairing code for authentication.
description: Add WhatsApp channel via native Baileys adapter. Direct connection — no Chat SDK bridge. Uses QR code or pairing code for authentication.
---
# Add WhatsApp Channel
This skill adds WhatsApp support to NanoClaw. It installs the WhatsApp channel code, dependencies, and guides through authentication, registration, and configuration.
Adds WhatsApp support via the native Baileys adapter (no Chat SDK bridge).
## Phase 1: Pre-flight
## Install
NanoClaw doesn't ship channels in trunk. This skill copies the native WhatsApp (Baileys) adapter and its `whatsapp-auth` setup step in from the `channels` branch. No Chat SDK bridge.
### Pre-flight (idempotent)
Skip to **Credentials** if all of these are already in place:
- `src/channels/whatsapp.ts` exists
- `src/channels/index.ts` contains `import './whatsapp.js';`
- `setup/whatsapp-auth.ts` and `setup/groups.ts` both exist
- `setup/index.ts`'s `STEPS` map contains both `'whatsapp-auth':` and `groups:`
- `@whiskeysockets/baileys`, `qrcode`, `pino` are listed in `package.json` dependencies
Otherwise continue. Every step below is safe to re-run.
### 1. Fetch the channels branch
```bash
git fetch origin channels
```
### 2. Copy the adapter and setup steps
```bash
git show origin/channels:src/channels/whatsapp.ts > src/channels/whatsapp.ts
git show origin/channels:setup/whatsapp-auth.ts > setup/whatsapp-auth.ts
git show origin/channels:setup/groups.ts > setup/groups.ts
```
### 3. Append the self-registration import
Append to `src/channels/index.ts` (skip if already present):
```typescript
import './whatsapp.js';
```
### 4. Register the setup steps
In `setup/index.ts`, add these entries to the `STEPS` map (skip lines already present):
```typescript
groups: () => import('./groups.js'),
'whatsapp-auth': () => import('./whatsapp-auth.js'),
```
### 5. Install the adapter packages (pinned)
```bash
pnpm install @whiskeysockets/baileys@6.17.16 qrcode@1.5.4 @types/qrcode@1.5.6 pino@9.6.0
```
### 6. 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 configured. If `store/auth/` exists with credential files, skip to Phase 4 (Registration) or Phase 5 (Verify).
Check if WhatsApp is already authenticated. If `store/auth/creds.json` exists, skip to "Shared vs dedicated number".
```bash
ls store/auth/creds.json 2>/dev/null && echo "WhatsApp auth exists" || echo "No WhatsApp auth"
test -f store/auth/creds.json && echo "WhatsApp auth exists" || echo "No WhatsApp auth"
```
### Detect environment
@@ -42,57 +103,6 @@ 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.)
## Phase 2: Apply Code Changes
Check if `src/channels/whatsapp.ts` already exists. If it does, skip to Phase 3 (Authentication).
### Ensure channel remote
```bash
git remote -v
```
If `whatsapp` is missing, add it:
```bash
git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
```
### Merge the skill branch
```bash
git fetch whatsapp main
git merge whatsapp/main || {
git checkout --theirs package-lock.json
git add package-lock.json
git merge --continue
}
```
This merges in:
- `src/channels/whatsapp.ts` (WhatsAppChannel class with self-registration via `registerChannel`)
- `src/channels/whatsapp.test.ts` (41 unit tests)
- `src/whatsapp-auth.ts` (standalone WhatsApp authentication script)
- `setup/whatsapp-auth.ts` (WhatsApp auth setup step)
- `import './whatsapp.js'` appended to the channel barrel file `src/channels/index.ts`
- `'whatsapp-auth'` step added to `setup/index.ts`
- `@whiskeysockets/baileys`, `qrcode`, `qrcode-terminal` npm dependencies in `package.json`
- `ASSISTANT_HAS_OWN_NUMBER` in `.env.example`
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
### Validate code changes
```bash
npm install
npm run build
npx vitest run src/channels/whatsapp.test.ts
```
All tests must pass and build must be clean before proceeding.
## Phase 3: Authentication
### Clean previous auth state (if re-authenticating)
```bash
@@ -104,7 +114,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 +130,12 @@ 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:
(Bash timeout: 150000ms)
Tell the user:
> 1. Open WhatsApp > **Settings** > **Linked Devices** > **Link a Device**
> 2. Scan the QR code displayed in the terminal
@@ -135,7 +147,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):
@@ -155,10 +167,10 @@ Display the code to the user the moment it appears. Tell them:
After the user enters the code, poll for authentication to complete:
```bash
for i in $(seq 1 60); do grep -q 'AUTH_STATUS: authenticated' /tmp/wa-auth.log 2>/dev/null && echo "authenticated" && break; grep -q 'AUTH_STATUS: failed' /tmp/wa-auth.log 2>/dev/null && echo "failed" && break; sleep 2; done
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:** qr_timeout → re-run. logged_out → delete `store/auth/` and re-run. 515 → re-run. timeout → ask user, offer retry.
**If failed:** logged_out → delete `store/auth/` and re-run. timeout → ask user, offer retry.
### Verify authentication succeeded
@@ -166,128 +178,43 @@ for i in $(seq 1 60); do grep -q 'AUTH_STATUS: authenticated' /tmp/wa-auth.log 2
test -f store/auth/creds.json && echo "Authentication successful" || echo "Authentication failed"
```
### Configure environment
### Shared vs dedicated number
Channels auto-enable when their credentials are present — WhatsApp activates when `store/auth/creds.json` exists.
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
Sync to container environment:
If dedicated, add to `.env`:
```bash
mkdir -p data/env && cp .env data/env/env
ASSISTANT_HAS_OWN_NUMBER=true
```
## Phase 4: Registration
## Next Steps
### Configure trigger and channel type
If you're in the middle of `/setup`, return to the setup flow now.
Get the bot's WhatsApp number: `node -e "const c=require('./store/auth/creds.json');console.log(c.me.id.split(':')[0].split('@')[0])"`
Otherwise, run `/manage-channels` to wire this channel to an agent group.
AskUserQuestion: Is this a shared phone number (personal WhatsApp) or a dedicated number (separate device)?
- **Shared number** - Your personal WhatsApp number (recommended: use self-chat or a solo group)
- **Dedicated number** - A separate phone/SIM for the assistant
## Channel Info
AskUserQuestion: What trigger word should activate the assistant?
- **@Andy** - Default trigger
- **@Claw** - Short and easy
- **@Claude** - Match the AI name
- **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.
AskUserQuestion: What should the assistant call itself?
- **Andy** - Default name
- **Claw** - Short and easy
- **Claude** - Match the AI name
### Features
AskUserQuestion: Where do you want to chat with the assistant?
- 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)
**Shared number options:**
- **Self-chat** (Recommended) - Chat in your own "Message Yourself" conversation
- **Solo group** - A group with just you and the linked device
- **Existing group** - An existing WhatsApp group
**Dedicated number options:**
- **DM with bot** (Recommended) - Direct message the bot's number
- **Solo group** - A group with just you and the bot
- **Existing group** - An existing WhatsApp group
### Get the JID
**Self-chat:** JID = your phone number with `@s.whatsapp.net`. Extract from auth credentials:
```bash
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')"
```
**DM with bot:** Ask for the bot's phone number. JID = `NUMBER@s.whatsapp.net`
**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
```
The output shows `JID|GroupName` pairs. Present candidates as AskUserQuestion (names only, not JIDs).
### Register the chat
```bash
npx tsx setup/index.ts --step register \
--jid "<jid>" \
--name "<chat-name>" \
--trigger "@<trigger>" \
--folder "whatsapp_main" \
--channel whatsapp \
--assistant-name "<name>" \
--is-main \
--no-trigger-required # Only for main/self-chat
```
For additional groups (trigger-required):
```bash
npx tsx setup/index.ts --step register \
--jid "<group-jid>" \
--name "<group-name>" \
--trigger "@<trigger>" \
--folder "whatsapp_<group-name>" \
--channel whatsapp
```
## Phase 5: Verify
### Build and restart
```bash
npm run build
```
Restart the service:
```bash
# macOS (launchd)
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
# Linux (systemd)
systemctl --user restart nanoclaw
# Linux (nohup fallback)
bash start-nanoclaw.sh
```
### Test the connection
Tell the user:
> Send a message to your registered WhatsApp chat:
> - For self-chat / main: Any message works
> - For groups: Use the trigger word (e.g., "@Andy hello")
>
> The assistant should respond within a few seconds.
### Check logs if needed
```bash
tail -f logs/nanoclaw.log
```
Not supported (WhatsApp linked device limitation): edit messages, delete messages.
## Troubleshooting
@@ -296,77 +223,42 @@ 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 setup/index.ts --step whatsapp-auth -- --method qr-browser
```
### Pairing code not working
Codes expire in ~60 seconds. To retry:
Codes expire in ~60 seconds. Delete auth and retry:
```bash
rm -rf store/auth/ && npx tsx src/whatsapp-auth.ts --pairing-code --phone <phone>
rm -rf store/auth/ && pnpm exec tsx setup/index.ts --step whatsapp-auth -- --method pairing-code --phone <phone>
```
Enter the code **immediately** when it appears. Also ensure:
1. Phone number is digits only — country code + number, no `+` prefix (e.g., `14155551234` where `1` is country code, `4155551234` is the number)
2. Phone has internet access
3. WhatsApp is updated to the latest version
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/ && 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
### "waiting for this message" on reactions
This happens when two instances connect with the same credentials. Ensure only one NanoClaw process is running:
Signal sessions corrupted from rapid restarts. Clear sessions:
```bash
pkill -f "node dist/index.js"
# Then restart
systemctl --user stop nanoclaw
rm store/auth/session-*.json
systemctl --user start nanoclaw
```
### Bot not responding
Check:
1. Auth credentials exist: `ls store/auth/creds.json`
3. Chat is registered: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE '%whatsapp%' OR jid LIKE '%@g.us' OR jid LIKE '%@s.whatsapp.net'"`
4. Service is running: `launchctl list | grep nanoclaw` (macOS) or `systemctl --user status nanoclaw` (Linux)
5. Logs: `tail -50 logs/nanoclaw.log`
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`
### Group names not showing
### "conflict" disconnection
Run group metadata sync:
```bash
npx 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:
```bash
# macOS:
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
npm run dev
# When done testing:
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
# Linux:
# systemctl --user stop nanoclaw
# npm run dev
# systemctl --user start nanoclaw
```
## Removal
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)
Two instances connected with same credentials. Ensure only one NanoClaw process is running.
+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
```
@@ -45,7 +45,7 @@ Apple Container requires macOS. It does not work on Linux.
grep "CONTAINER_RUNTIME_BIN" src/container-runtime.ts
```
If it already shows `'container'`, the runtime is already Apple Container. Skip to Phase 3.
If it already shows `'container'`, the runtime is already Apple Container. Skip to Phase 4.
## Phase 2: Apply Code Changes
@@ -80,13 +80,50 @@ 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.
## Phase 3: Verify
## Phase 3: Credential proxy network binding
Apple Container uses a bridge network (bridge100) that only exists while containers are running. The credential proxy must start before any container, so it cannot bind to the bridge IP. It must bind to `0.0.0.0`, which exposes port 3001 on all network interfaces — anyone on your local network could route API requests through the proxy using your credentials.
Use AskUserQuestion to ask the user:
**"The credential proxy needs to bind to all interfaces (0.0.0.0). Is this Mac on a trusted private network?"**
Options:
1. **Yes, private/home network** — description: "No firewall rule needed."
2. **No, shared/public network** — description: "Add a macOS firewall rule to block external access to port 3001."
For both options, add `CREDENTIAL_PROXY_HOST=0.0.0.0` to `.env`:
```bash
grep -q 'CREDENTIAL_PROXY_HOST' .env 2>/dev/null || echo 'CREDENTIAL_PROXY_HOST=0.0.0.0' >> .env
```
If they chose the public network option, set up and persist the firewall rule:
```bash
echo "block in on en0 proto tcp to any port 3001" | sudo pfctl -ef -
```
```bash
grep -q 'nanoclaw proxy' /etc/pf.conf 2>/dev/null || echo '# nanoclaw proxy — block LAN access to credential proxy
block in on en0 proto tcp to any port 3001' | sudo tee -a /etc/pf.conf > /dev/null
```
Verify the rule is working:
```bash
curl -sf http://$(ipconfig getifaddr en0):3001 && echo "EXPOSED — rule not working" || echo "BLOCKED — rule active"
```
If the verification shows "EXPOSED", warn the user and retry. If "BLOCKED", confirm success and continue.
## Phase 4: Verify
### Ensure Apple Container runtime is running
@@ -135,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
```
+1 -1
View File
@@ -91,7 +91,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
+121
View File
@@ -0,0 +1,121 @@
---
name: init-first-agent
description: Walk the operator through creating the first NanoClaw 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 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>` 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/SKILL.md`, `.claude/skills/add-telegram/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`.
## 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}"
```
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/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 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/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`
@@ -0,0 +1,100 @@
# Migrating OpenClaw Cron Jobs to NanoClaw Scheduled Tasks
This file is referenced by SKILL.md Phase 5 when cron jobs are detected.
**Before inserting tasks:** Read `src/db.ts` and search for `scheduled_tasks` to verify the current table schema. The schema below is a reference — if columns have been added, removed, or renamed, use the current schema from the source code.
Also verify the `createTask` function signature in `src/db.ts` — it may be simpler to call it via a script than raw SQL.
## OpenClaw Cron Job Format
Source: `<STATE_DIR>/cron/jobs.json` (from `src/cron/types.ts`). If the file format doesn't match what's described below, read the actual file and adapt — OpenClaw may have changed the schema.
The jobs file is `{ version: 1, jobs: CronJob[] }`. Each job has:
- `id`, `name`, `description`, `enabled`, `deleteAfterRun`
- `schedule`: `{ kind: "cron", expr: string, tz?: string }` | `{ kind: "every", everyMs: number }` | `{ kind: "at", at: string }`
- `payload`: `{ kind: "agentTurn", message: string, model?, thinking?, timeoutSeconds? }` | `{ kind: "systemEvent", text: string }`
- `sessionTarget`: `"main"` | `"isolated"` | `"current"` | `"session:<id>"`
- `wakeMode`: `"next-heartbeat"` | `"now"`
- `delivery`: `{ mode: "none" | "announce" | "webhook", channel?, to?, threadId?, bestEffort? }`
- `failureAlert`: `{ after?: number, channel?, to?, cooldownMs? }` | `false`
- `state`: runtime state (nextRunAtMs, lastRunStatus, consecutiveErrors, etc.)
## NanoClaw `scheduled_tasks` Table
Source: `src/db.ts`
| Column | Type | Notes |
|--------|------|-------|
| `id` | TEXT PK | Unique task ID |
| `group_folder` | TEXT | Target group directory (e.g. `"main"`) |
| `chat_jid` | TEXT | Target chat JID |
| `prompt` | TEXT | Task instructions |
| `script` | TEXT | Optional bash pre-check script |
| `schedule_type` | TEXT | `"cron"`, `"interval"`, or `"once"` |
| `schedule_value` | TEXT | Cron expr, ms interval, or ISO timestamp |
| `context_mode` | TEXT | `"group"` or `"isolated"` (default) |
| `next_run` | TEXT | ISO timestamp — must be computed at insert time |
| `last_run` | TEXT | null initially |
| `last_result` | TEXT | null initially |
| `status` | TEXT | `"active"`, `"paused"`, or `"completed"` |
| `created_at` | TEXT | ISO timestamp |
## Field Mapping
- `schedule.kind:"cron"` + `schedule.expr``schedule_type:"cron"`, `schedule_value:<expr>`
- `schedule.kind:"every"` + `schedule.everyMs``schedule_type:"interval"`, `schedule_value:<ms as string>`
- `schedule.kind:"at"` + `schedule.at``schedule_type:"once"`, `schedule_value:<ISO timestamp>`
- `payload.message` or `payload.text``prompt`
- `sessionTarget:"isolated"``context_mode:"isolated"`, `sessionTarget:"main"` or `"current"``context_mode:"group"`
## What Doesn't Map
- `delivery.mode:"webhook"` — NanoClaw has no webhook delivery. Discuss with the user: this could be implemented as a task `script` that runs `curl` to hit the webhook endpoint.
- `failureAlert` — NanoClaw has no failure alert system. Note this to the user.
- `wakeMode` — NanoClaw tasks always wake the agent immediately.
- `payload.model`, `payload.thinking`, `payload.timeoutSeconds` — NanoClaw doesn't support per-task model/thinking config. These are handled by the SDK.
- `deleteAfterRun` — NanoClaw `"once"` tasks are marked `"completed"` after running, not deleted.
## For Each Enabled Job
1. Show what it does: name, schedule, prompt, delivery mode
2. Explain any differences (no retry config, no webhook delivery, no failure alerts)
3. If `delivery.mode:"webhook"`: discuss with the user — a task `script` with `curl` often suffices
4. Ask if they want to keep this task
## Inserting Tasks
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
pnpm exec tsx -e "
const Database = require('better-sqlite3');
const { CronExpressionParser } = require('cron-parser');
const db = new Database('store/messages.db');
// Compute next_run for cron tasks:
// const interval = CronExpressionParser.parse('<expr>', { tz: process.env.TZ || 'UTC' });
// const nextRun = interval.next().toISOString();
db.prepare(\`INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, script, schedule_type, schedule_value, context_mode, next_run, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\`).run(
'migrated-<original-id>',
'<group_folder>',
'<chat_jid>',
'<mapped prompt>',
null,
'<mapped schedule_type>',
'<mapped schedule_value>',
'<mapped context_mode>',
'<computed next_run ISO>',
'active',
new Date().toISOString()
);
db.close();
"
```
**Computing `next_run`:**
- `cron` tasks: use `CronExpressionParser.parse(expr, { tz }).next().toISOString()`
- `interval` tasks: `new Date(Date.now() + ms).toISOString()`
- `once` tasks: `next_run` equals `schedule_value`
If groups haven't been registered yet (database doesn't exist), save the task details to `groups/main/openclaw-migration-tasks.md` with the exact SQL payloads, and tell the user: "These tasks will be created after `/setup` registers your groups."
@@ -0,0 +1,447 @@
---
name: migrate-from-openclaw
description: Migrate from OpenClaw to NanoClaw. Detects existing OpenClaw installation, extracts identity, channel credentials, scheduled tasks, and other config, then guides interactive migration. Triggers on "migrate from openclaw", "openclaw migration", "import from openclaw".
---
# Migrate from OpenClaw
Guide the user through migrating their OpenClaw installation to NanoClaw. This is a conversation, not a batch job. Read OpenClaw state, discuss it with the user, make judgment calls together about what to bring over and how.
**Principle:** Never silently copy data. Read it, explain it, discuss where it belongs in NanoClaw's architecture, show proposed changes before applying. Credentials must be masked when displayed (first 4 + `...` + last 4 characters). Make judgment calls about what's core vs. reference material.
**UX:** Use `AskUserQuestion` for multiple-choice only. Use plain text for free-form input. Don't dump raw data — summarize and explain conversationally.
## Migration State File
Create `migration-state.md` in the project root at the start of Phase 0. Update it after each phase completes. This file is the single source of truth for the migration — if context is compacted or lost, re-read it to recover all decisions and progress.
Before starting any phase, re-read `migration-state.md` to ensure you have current state.
Sections to maintain (add data as each phase completes):
- **Progress** — checkbox list of phases (Phase 07)
- **Discovery** — STATE_DIR, IDENTITY_NAME, channels, groups (with JID mappings), workspace files, cron job count, MCP servers
- **Decisions** — assistant_name, group_model (shared/separate/main-only), main_group (folder + jid)
- **Registered Groups** — table: folder, jid, channel, is_main
- **Settings Migrated** — timezone, anthropic_credential (masked), sender_allowlist (created/skipped)
- **Identity & Memory** — paths of files created, which CLAUDE.md was edited
- **Channel Credentials** — table: channel, status, env_var
- **Scheduled Tasks** — table: original_id, name, migrated/deferred
- **Deferred / Not Applicable** — unsupported channels, discussed customizations, OpenClaw-only features
Keep it factual and terse — this is for machine recovery after compaction, not human reading. Delete the file at the end of Phase 7 (or offer to keep it as a record).
## Phase 0: Discovery
Run the discovery script to find and summarize the OpenClaw installation:
```bash
pnpm exec tsx ${CLAUDE_SKILL_DIR}/scripts/discover-openclaw.ts
```
If the user specifies a custom path, pass it: `--state-dir <path>`
Parse the status block. Key fields: STATUS, STATE_DIR, CHANNELS, WORKSPACE_FILES, DAILY_MEMORY_FILES, SKILL_COUNT, SKILLS, CRON_JOBS, MCP_SERVERS, IDENTITY_NAME, AGENT_COUNT, AGENT_IDS.
**Sanity-check the output:** The discovery script detects known structures but can silently miss data if OpenClaw's format has changed. Check `CONFIG_TOP_KEYS` and `CONFIG_CHANNEL_KEYS` — if you see keys the script didn't report on (e.g. a channel name not in CHANNELS, or a top-level section like `integrations` or `plugins`), read that section of the config directly with the Read tool. Also check `STATE_DIR_CONTENTS` for directories the script doesn't scan (e.g. unexpected folders alongside `workspace/`, `agents/`, `cron/`).
**If STATUS=not_found:** Tell the user no OpenClaw installation was detected at the standard locations (`~/.openclaw`, `~/.clawdbot`). Ask if they have a custom path. If not, exit.
**If STATUS=found:** Present a human-readable summary:
- "I found your OpenClaw installation at `<STATE_DIR>`."
- Identity: name from IDENTITY.md (if found)
- Workspace files: which of SOUL.md, USER.md, MEMORY.md, IDENTITY.md exist
- Channels: list each, note which NanoClaw supports (whatsapp, telegram, slack, discord) and which it doesn't
- Daily memory files: count (if any)
- Skills: count and names (from workspace, shared, personal, project locations)
- Cron jobs: count and names
- MCP servers: count and names
- Agents: count (relevant for Phase 1 groups discussion)
Then explain the key architectural differences. Don't dump a table — paraphrase conversationally:
- **Container isolation:** NanoClaw runs each agent in an isolated Linux container (Docker or Apple Container). OpenClaw runs everything in one process. This means stronger isolation but also means each group is its own sandbox.
- **Group-based memory:** In OpenClaw, all groups under one agent share the same SOUL.md, MEMORY.md, and IDENTITY.md. In NanoClaw, each group has its own filesystem and CLAUDE.md. Shared state goes in `groups/global/CLAUDE.md` (mounted read-only into all non-main containers).
- **Channel skills:** In OpenClaw, channels are configured in `openclaw.json`. In NanoClaw, channels are installed as code via skills (`/add-telegram`, `/add-whatsapp`, etc.) and configured through `.env` variables.
- **Simpler config:** NanoClaw has no config file — behavior is in the code and `CLAUDE.md` files. Credentials live in `.env` or the OneCLI vault.
AskUserQuestion: "Ready to start migrating? I'll go through each area one at a time."
1. **Yes, let's go** — proceed to Phase 1
2. **Tell me more** — explain more about any area they ask about
3. **Skip migration** — exit
## Phase 1: Groups and Architecture
**This discussion must happen before identity/memory, because the shared-vs-isolated decision determines where files go.**
If GROUP_COUNT > 0 or AGENT_COUNT > 1, this is a critical conversation. Even with just one group, explain the model difference so the user understands what they're getting into.
**OpenClaw model:** All groups routed to the same agent share one workspace — the same SOUL.md, MEMORY.md, IDENTITY.md, and tools. When you talk to the bot in your family chat or your work chat, it's the same agent with the same personality and memory. Only the session (conversation history) is separate per group.
**NanoClaw model:** Each group is a completely separate agent running in its own Linux container. Separate filesystem, separate memory, separate CLAUDE.md. The bot in your family chat and your work chat are different agents that don't know about each other — unless you explicitly share state via `groups/global/CLAUDE.md`, which is mounted read-only into all non-main containers.
Explain this conversationally. If the user only has one group, it's simple — just note the difference and move on. If they have multiple groups, discuss:
AskUserQuestion: "In OpenClaw, your groups shared the same personality and memory. In NanoClaw, each group is a fully separate agent. How would you like to handle this?"
1. **Shared personality (recommended if your groups had the same bot)** — "I'll put the shared personality, identity, and user context in `groups/global/CLAUDE.md`. Every group sees it. Each group can add its own customizations on top."
2. **Fully separate** — "Each group gets its own independent personality and memory. Complete isolation between groups."
3. **Just main group for now** — "Set up one group now. We can add others later."
Remember this choice — it determines where identity and memory files go in the next phase.
### Confirm assistant name
Before registering groups, confirm the assistant name — it's used for trigger patterns and CLAUDE.md templates.
IDENTITY_NAME from discovery gives the OpenClaw name. Ask the user: "Your OpenClaw assistant was named `<IDENTITY_NAME>`. Want to keep this name in NanoClaw?" If they want a different name, ask what it should be. If IDENTITY_NAME was empty, ask them to choose a name (default: "Andy").
The register step's `--assistant-name` flag writes `ASSISTANT_NAME` to `.env` and updates CLAUDE.md templates automatically — no manual `.env` write needed.
### Registering groups
The discovery script provides detected groups in the GROUPS field (format: `channel:id(name)=>nanoclaw_jid`). These are extracted from OpenClaw's session store and channel config.
For each group the user wants to bring over, pre-register it:
```bash
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).
Folder naming: `<channel>_<name-slug>` (e.g. `whatsapp_family-chat`, `telegram_dev-team`). Ask the user to confirm each group's name and folder.
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: `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.
## Phase 2: Settings from Config
Before identity/memory, extract settings from `openclaw.json` that map directly to NanoClaw setup. Read the config file with the Read tool (`<STATE_DIR>/openclaw.json` or `clawdbot.json`).
### Timezone
Check `agents.defaults.userTimezone` in the config. If present and it's a valid IANA timezone (e.g. `America/New_York`, `Asia/Jerusalem`), write it to `.env` as `TZ=<timezone>`. NanoClaw's setup step 2a reads `TZ` from `.env` (`src/config.ts:84-97`) and will skip the autodetection prompt.
### Anthropic Credentials
Check for Anthropic API keys or tokens in OpenClaw's auth system. OpenClaw stores credentials in `<STATE_DIR>/auth-profiles.json` or `<STATE_DIR>/agents/main/agent/auth-profiles.json` with this structure:
```json
{
"version": 1,
"profiles": {
"anthropic:default": {
"type": "api_key", // or "token" or "oauth"
"provider": "anthropic",
"key": "sk-ant-..." // for api_key type
}
}
}
```
Profile IDs follow `provider:identifier` format. Look for any profile where `provider` is `"anthropic"`. The credential field depends on the `type`:
- `type: "api_key"``key` field (or `keyRef` for SecretRef)
- `type: "token"``token` field (or `tokenRef` for SecretRef)
- `type: "oauth"``access` field (OAuth access token, may need refresh)
Also check:
1. `<STATE_DIR>/.env` — for `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN`
2. Config `models.providers` — for Anthropic provider entries with `apiKey`
If found, offer to save to `.env`. This pre-fills the NanoClaw setup credential step (step 4) so the user doesn't need to re-enter it. Use the same masking approach — show first 4 + last 4 characters, write the full value directly.
**Important:** If the credential uses `keyRef`/`tokenRef` with `source:"exec"` or `source:"file"`, explain that it can't be auto-extracted and the user will need to enter it during setup. For `type: "oauth"` credentials with an expiry in the past, warn the user the token may need to be refreshed during setup.
### Sender Allowlists
Read the channel configs for access control settings. OpenClaw stores these per-channel:
- `channels.<channel>.allowFrom` — array of allowed sender IDs (E.164 for WhatsApp, numeric IDs for Telegram)
- `channels.<channel>.dmPolicy``"open"`, `"allowlist"`, `"disabled"`
- `channels.<channel>.groupPolicy``"open"`, `"allowlist"`, `"disabled"`
- `channels.<channel>.groupAllowFrom` — array of allowed group member IDs
NanoClaw uses `~/.config/nanoclaw/sender-allowlist.json` with this format:
```json
{
"default": { "allow": "*", "mode": "trigger" },
"chats": {
"<chat-jid>": {
"allow": ["sender-id-1", "sender-id-2"],
"mode": "trigger"
}
},
"logDenied": true
}
```
Fields:
- `allow`: `"*"` (all senders) or `string[]` (specific sender IDs)
- `mode`: `"trigger"` (messages stored but trigger blocked for non-allowed senders) or `"drop"` (messages silently discarded before storage)
- `logDenied`: optional boolean (default `true`), logs denied messages
If OpenClaw had allowlists configured, show the user what was set and offer to create the NanoClaw equivalent. Map:
- `dmPolicy:"allowlist"` + `allowFrom` → per-chat entry with `"allow"` array, `"mode": "trigger"`
- `groupPolicy:"allowlist"` + `groupAllowFrom` → per-group entry with `"allow"` array, `"mode": "trigger"`
- `dmPolicy:"open"``"allow": "*"`
- `dmPolicy:"disabled"` → per-chat entry with `"allow": []`, `"mode": "drop"` (or don't register that chat)
Create the directory and file:
```bash
mkdir -p ~/.config/nanoclaw
```
Then write the JSON file. If no allowlists were configured, skip this.
### Container Timeout
Check `agents.defaults.timeoutSeconds` in the config. This is maximum total agent runtime (wall-clock). NanoClaw's equivalent is `CONTAINER_TIMEOUT` (env var, default 30 min), also configurable per-group via `containerConfig.timeout`. Note: NanoClaw also has a separate `IDLE_TIMEOUT` (max time without output) which resets on activity — OpenClaw has no equivalent.
If the OpenClaw value differs significantly from 30 minutes, note it for the user. They can set `CONTAINER_TIMEOUT=<ms>` in `.env` after setup.
## Phase 3: Identity and Memory
This phase is fully conversational — read files directly and discuss with the user. No script needed.
**Where files go depends on the Phase 1 (groups) decision:**
- **Shared personality:** Core identity goes in `groups/global/CLAUDE.md` (seen by all groups). Group-specific customizations go in each group's own CLAUDE.md.
- **Fully separate:** Everything goes in `groups/main/` (or each group's own folder).
- **Just main group:** Everything goes in `groups/main/`.
### Find workspace files
The STATE_DIR from discovery tells you where OpenClaw lives. Look for workspace files at `<STATE_DIR>/workspace/`. If AGENT_COUNT > 1, also check `<STATE_DIR>/agents/*/workspace/` and ask which agent to migrate.
Use the Read tool to look at each file found.
### IDENTITY.md
Read `<STATE_DIR>/workspace/IDENTITY.md` if it exists. It uses a key:value format (name, emoji, creature, vibe, etc.).
The assistant name was already confirmed and written to `.env` in Phase 1. Here, focus on the rest of the identity — create an `identity.md` file with the full identity details (emoji, creature, vibe, personality traits, etc.). If shared personality was chosen in Phase 1, put it alongside `groups/global/CLAUDE.md`. Otherwise, put it in `groups/main/`.
### SOUL.md
Read `<STATE_DIR>/workspace/SOUL.md` if it exists. Then read `groups/main/CLAUDE.md`.
CLAUDE.md is always loaded into the agent's context — it's the agent's continuous instructions. Not everything from SOUL.md needs to be there. Discuss with the user what belongs where:
- **In CLAUDE.md (always loaded):** Core personality traits, communication style, key behavioral rules. Weave these into the existing CLAUDE.md structure — adjust the opening description under the `# <Name>` heading, modify the tone in the Communication section.
- **In a separate soul file:** Detailed personality backstory, extended guidelines, creative writing style, philosophical grounding — things the agent can reference when relevant but don't need to consume context tokens on every turn.
**File placement depends on Phase 1 choice:**
- Shared personality → edit `groups/global/CLAUDE.md` for the core traits, create `groups/global/soul.md` for the extended content. All groups will see both.
- Separate / main only → edit `groups/main/CLAUDE.md`, create `groups/main/soul.md`.
Add a reference in the relevant CLAUDE.md: "Your personality and extended behavioral guidelines are in `soul.md`. Refer to it for identity questions or when crafting responses that need your full character."
Show proposed edits to the user before applying. This is a thoughtful merge, not a copy-paste.
### USER.md
Read `<STATE_DIR>/workspace/USER.md` if it exists.
Create `groups/main/user-context.md` with the user information. Add a reference in CLAUDE.md: "Information about your user is in `user-context.md`. Read it when you need context about who you're talking to."
Ask if they want any critical user facts (name, timezone, key preferences) directly in CLAUDE.md for always-on awareness.
### MEMORY.md
Read `<STATE_DIR>/workspace/MEMORY.md` if it exists.
Show the contents and discuss what's worth keeping. Some memory entries may be stale or OpenClaw-specific. Create `groups/main/memories.md` for relevant items. Add a reference in CLAUDE.md.
### Daily memory files (`workspace/memory/*.md`)
If DAILY_MEMORY_FILES > 0 in the discovery output, OpenClaw accumulated dated memory files (e.g. `2024-01-01.md`). These contain observations, facts, and context gathered over time.
AskUserQuestion: "You have N daily memory files from OpenClaw. How would you like to handle them?"
1. **Copy as-is (recommended for many files)** — "I'll create a `daily-memories/` folder in your group directory and copy them over. Your agent can reference them when needed."
- Create the folder in the appropriate group directory (per Phase 1 decision)
- Copy all `.md` files: `cp -r <workspace>/memory/*.md <group_dir>/daily-memories/`
- Add a reference in CLAUDE.md: "Historical daily memory files from your previous system are in `daily-memories/`. Refer to them when you need context about past events or observations."
2. **Consolidate into memories** — "I'll read through them, extract the durable facts, and add them to your memories file. This reduces clutter but takes longer."
- Read each file, extract entries worth keeping (skip transient observations, focus on durable facts about the user, preferences, recurring topics)
- Consolidate into `memories.md`
- Use sub-agents for large volumes (>10 files)
3. **Skip** — "Don't bring daily memories over."
### OpenClaw Skills
If SKILL_COUNT > 0 in discovery, OpenClaw had custom skills. The SKILL.md format is a shared standard — skills are directly portable.
The discovery reports skill names and source locations. For each skill, read just the YAML front matter (name + description at the top of SKILL.md) and present a list to the user: skill name, description, source location. Let the user select which ones to bring over.
For confirmed skills, copy the entire skill directory as-is:
```bash
cp -r <skill_source_dir> container/skills/<skill_name>
```
After all skills are copied, a container rebuild is needed — note this for post-migration: `./container/build.sh`.
### Config-registered plugins and skills
If CONFIG_PLUGIN_COUNT > 0 in discovery, OpenClaw had installed plugins/skills with API keys (e.g. `plugins.entries.brave`, `skills.entries.openai-whisper-api`). These are functional tools the agent had access to.
For each detected plugin, present the name to the user and discuss whether to set it up in NanoClaw. Read the OpenClaw config section to understand what it is, then:
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. `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.
4. **If the plugin wraps an API** — discuss with the user what it did and offer to implement the equivalent: save the API key to `.env`, write a container skill with instructions for using the API, or wire it into the message flow if it's something automatic (e.g. voice transcription).
5. **If unclear** — discuss with the user what the plugin did and decide together. Don't install unknown packages or search for replacements — that's a supply chain risk.
For API keys, read the config value directly (don't display raw keys) and write to `.env`. The discovery script reports which plugins have keys but never extracts them.
### Other files (TOOLS.md, HEARTBEAT.md, BOOTSTRAP.md, AGENTS.md)
If these exist, briefly mention them and explain:
- TOOLS.md: NanoClaw agents have their own tool discovery; this doesn't transfer
- HEARTBEAT.md: NanoClaw uses scheduled tasks instead
- BOOTSTRAP.md: NanoClaw uses CLAUDE.md and container skills instead
- AGENTS.md: Already covered in the Phase 1 groups discussion
## Phase 4: Channel Credentials
For each channel found in the discovery results, handle it based on NanoClaw support:
### Supported channels (whatsapp, telegram, slack, discord)
Run the credential extraction script with `--write-env .env` so it writes credentials directly to NanoClaw's `.env` file. The script never emits raw credential values to stdout — only masked versions.
First, run without `--write-env` to preview:
```bash
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.
**If HAS_CREDENTIAL=false but the user expects a credential:** The extraction script may not recognize the config structure. Fall back to reading the channel section of `openclaw.json` directly with the Read tool and look for any field that contains a token or key value. Ask the user to confirm.
If HAS_CREDENTIAL=true: Show the masked credential (`CREDENTIAL_MASKED`). AskUserQuestion:
1. **Use this credential** — run again with `--write-env .env` to save it
2. **Enter a new one** — ask in plain text, write to `.env` manually
3. **Skip this channel** — don't configure
If using the credential:
```bash
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.
**Credential destination note:** Credentials are saved to `.env` for now. During `/setup`, the credential step will either keep them in `.env` (Apple Container) or migrate them to the OneCLI vault (Docker). The user doesn't need to worry about this now.
For Slack: there are two credentials (bot token + app token). The script handles both in one run — check `HAS_CREDENTIAL_2` and `NANOCLAW_ENV_VAR_2` in the status block.
**WhatsApp special case:** WhatsApp uses QR/pairing-code authentication, not a token. Do not copy auth state from OpenClaw — encryption sessions become stale after copying and messages fail to decrypt. Authentication will be handled during `/setup` via the `/add-whatsapp` skill (takes about 60 seconds with a pairing code). Just note that WhatsApp was configured and move on.
**Allowlist note:** If the channel had `allowFrom` or group policies, these were already handled in Phase 2 (sender allowlists). Mention that the allowlist file was created earlier.
### Unsupported channels (signal, matrix, irc, msteams, feishu, etc.)
Explain briefly: "NanoClaw doesn't have a `<channel>` integration yet, but channels are added over time via skills. Any groups from this channel were already registered in Phase 1 — they'll activate when the channel is added."
If there are credentials (tokens, keys) for the unsupported channel, offer to save them to `.env` with a descriptive variable name (e.g. `SIGNAL_ACCOUNT`, `MATRIX_ACCESS_TOKEN`) so they're available when the channel is eventually supported.
Don't invoke channel skills here — just prepare `.env` credentials. Channel code is installed during `/setup`.
## Phase 5: Scheduled Tasks
Read `<STATE_DIR>/cron/jobs.json` with the Read tool. If the file doesn't exist or has no jobs, skip this phase.
If jobs exist, read `${CLAUDE_SKILL_DIR}/MIGRATE_CRONS.md` for the full OpenClaw cron format, NanoClaw table schema, field mapping, and SQL insert template. Follow those instructions for each job.
## Phase 6: Webhooks, MCP, and Other Config
Read relevant sections from `<STATE_DIR>/openclaw.json` directly with the Read tool. This phase is fully conversational.
### MCP Servers
If MCP_SERVERS was non-empty in discovery, these can be ported. Claude Code supports MCP servers natively. Read the OpenClaw config's `mcp.servers` section to get each server's details (`command`, `args`, `env`, `url`).
MCP servers in NanoClaw are registered in the agent-runner source code. Before editing, grep for `mcpServers` in `container/agent-runner/src/` to find the current location — it's expected to be in `index.ts` in the `query()` options, but may have moved. For each OpenClaw MCP server the user wants to bring over:
1. Read its config: command, args, env, url
2. **stdio servers** (have `command`): Add an entry to the `mcpServers` object in `container/agent-runner/src/index.ts`. The command runs inside the container, so it needs to be available there (Node.js/npx-based servers work; custom binaries would need to be added to the Dockerfile).
3. **HTTP/SSE servers** (have `url`): These work if the URL is accessible from inside the container. Add them the same way.
4. **Environment variables**: Any `env` values that reference secrets should be added to `.env` and passed through via `process.env.*` in the mcpServers entry.
After adding all MCP servers, a container rebuild is needed: `./container/build.sh`
Show the user each server and ask which to bring over. For servers that need custom binaries not available in the container, note them for manual setup.
### Webhooks and Endpoints
If the config has webhook sections (in `cron.webhook`, `cron.failureDestination`, or channel-specific webhooks):
- Explain what they were used for
- These don't map directly but NanoClaw can be customized to support them
- Discuss the use case with the user and propose a solution if it's important to them
- For simple webhook notifications: a task script with `curl` often suffices
### Other Config
Scan the config for notable sections and briefly mention anything that doesn't carry over:
- **Exec approvals / command allowlist:** NanoClaw uses container isolation instead — the agent runs with `--dangerously-skip-permissions` inside a sandboxed container
- **Human delay:** Not applicable in NanoClaw's container model
- **Compaction:** Handled by Claude Code SDK automatically
- **TTS:** Not built into NanoClaw
- **Model configuration:** NanoClaw uses whatever Anthropic model the credential provides access to
Don't belabor these — just mention and move on.
## Phase 7: Summary
### Summary
Print a comprehensive summary:
**Migrated:**
- Assistant name → `.env` ASSISTANT_NAME + CLAUDE.md templates updated
- Groups → registered in database, folders created with CLAUDE.md templates
- Timezone → `.env` TZ
- Anthropic credential → `.env` (for setup to pick up)
- Sender allowlists → `~/.config/nanoclaw/sender-allowlist.json`
- Personality → CLAUDE.md (core) + `soul.md` (extended), placed per Phase 1 decision (global or per-group)
- User context → `user-context.md`
- Memories → `memories.md` + daily memory files (copied to `daily-memories/` or consolidated)
- OpenClaw skills → copied to `container/skills/`
- Channel credentials → `.env` (list which channels)
- Scheduled tasks → inserted into database or noted for post-setup
- MCP servers → registered in agent-runner
**Noted for later:**
- Channel code installation (happens during `/setup`)
- Task creation (if deferred due to no registered group yet)
- Container rebuild needed (if skills or MCP servers were added): `./container/build.sh`
**Not applicable:**
- Unsupported channels (list them — groups registered for future)
- OpenClaw-specific features (exec approvals, human delay, TTS, model config, session reset policies, etc.)
**Discussed and deferred:**
- List any customizations agreed on but not yet implemented
Remind: "Run `/setup` next to complete your NanoClaw installation. Channel credentials are already prepared in `.env`. When setup asks which channels to enable, select the ones we configured."
## Troubleshooting
**Config parse error:** If `openclaw.json` fails to parse, it may use JSON5 features the parser doesn't handle. Ask the user to check the file for unusual syntax. As a fallback, the agent can read the file directly and work with it manually.
**Credential not found:** If a channel credential resolves to empty, it may use `source:"exec"` or `source:"file"` SecretRef. These can't be auto-extracted. Ask the user to provide the value directly.
**Multi-agent complexity:** If the user had many agents with different configs, focus on the primary/default agent first. Additional agents can be set up as separate NanoClaw groups later.
@@ -0,0 +1,734 @@
/**
* Discover an existing OpenClaw installation and emit a structured summary.
*
* 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,
* checks cron jobs, MCP servers, and channel credentials.
*
* Emits a status block on stdout:
* === NANOCLAW MIGRATE: DISCOVERY ===
* ...
* === END ===
*/
import fs from 'fs';
import os from 'os';
import path from 'path';
// ---------------------------------------------------------------------------
// JSON5-tolerant parser (no dependency)
// ---------------------------------------------------------------------------
function parseJson5(text: string): unknown {
// Strip single-line comments (// ...) that aren't inside strings
let cleaned = text.replace(
/("(?:[^"\\]|\\.)*")|\/\/[^\n]*/g,
(match, str) => (str ? str : ''),
);
// Strip block comments (/* ... */)
cleaned = cleaned.replace(
/("(?:[^"\\]|\\.)*")|\/\*[\s\S]*?\*\//g,
(match, str) => (str ? str : ''),
);
// Strip trailing commas before } or ]
cleaned = cleaned.replace(/,\s*([}\]])/g, '$1');
return JSON.parse(cleaned);
}
// ---------------------------------------------------------------------------
// Status block emitter (mirrors setup/status.ts convention)
// ---------------------------------------------------------------------------
function emitStatus(fields: Record<string, string | number | boolean>): void {
const lines = ['=== NANOCLAW MIGRATE: DISCOVERY ==='];
for (const [key, value] of Object.entries(fields)) {
lines.push(`${key}: ${value}`);
}
lines.push('=== END ===');
console.log(lines.join('\n'));
}
// ---------------------------------------------------------------------------
// CLI arg parsing
// ---------------------------------------------------------------------------
function parseArgs(): { stateDir?: string } {
const args = process.argv.slice(2);
for (let i = 0; i < args.length; i++) {
if (args[i] === '--state-dir' && args[i + 1]) {
return { stateDir: args[i + 1] };
}
}
return {};
}
// ---------------------------------------------------------------------------
// Path resolution
// ---------------------------------------------------------------------------
function resolveStateDir(explicit?: string): string | null {
const home = os.homedir();
const candidates: string[] = [];
if (explicit) {
// Expand ~ prefix
const expanded = explicit.startsWith('~')
? path.join(home, explicit.slice(1))
: explicit;
candidates.push(expanded);
}
if (process.env.OPENCLAW_STATE_DIR) {
candidates.push(process.env.OPENCLAW_STATE_DIR);
}
candidates.push(path.join(home, '.openclaw'));
candidates.push(path.join(home, '.clawdbot'));
for (const dir of candidates) {
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
return dir;
}
}
return null;
}
// ---------------------------------------------------------------------------
// Config loading
// ---------------------------------------------------------------------------
function loadConfig(
stateDir: string,
): Record<string, unknown> | null {
for (const name of ['openclaw.json', 'clawdbot.json']) {
const configPath = path.join(stateDir, name);
if (fs.existsSync(configPath)) {
try {
const raw = fs.readFileSync(configPath, 'utf-8');
return parseJson5(raw) as Record<string, unknown>;
} catch {
// Try next name
}
}
}
return null;
}
// ---------------------------------------------------------------------------
// Channel detection
// ---------------------------------------------------------------------------
interface ChannelInfo {
name: string;
hasCreds: boolean;
}
const SUPPORTED_CHANNELS = new Set([
'whatsapp',
'telegram',
'slack',
'discord',
]);
// Fields that indicate a credential is present for each channel
const CREDENTIAL_FIELDS: Record<string, string[]> = {
telegram: ['botToken'],
discord: ['token'],
slack: ['botToken', 'appToken'],
whatsapp: [], // Auth-state based, no token
signal: ['account'],
imessage: [],
matrix: ['homeserverUrl', 'accessToken'],
irc: ['server'],
msteams: ['appId'],
feishu: ['appId'],
googlechat: [],
mattermost: ['token', 'url'],
zalo: [],
bluebubbles: ['url'],
};
const ALL_KNOWN_CHANNELS = new Set([
'whatsapp', 'telegram', 'slack', 'discord', 'signal',
'imessage', 'matrix', 'irc', 'msteams', 'feishu',
'googlechat', 'mattermost', 'zalo', 'bluebubbles',
]);
function detectChannels(
config: Record<string, unknown>,
): ChannelInfo[] {
// Check both config.channels.* (newer) and top-level config.* (older/legacy)
const channelsSections: Record<string, unknown> = {};
// Source 1: channels.* (standard location)
const nested = config.channels as Record<string, unknown> | undefined;
if (nested) {
for (const [k, v] of Object.entries(nested)) {
if (v && typeof v === 'object') channelsSections[k] = v;
}
}
// Source 2: top-level keys matching known channel names (legacy format)
for (const key of Object.keys(config)) {
if (ALL_KNOWN_CHANNELS.has(key) && !channelsSections[key]) {
const v = config[key];
if (v && typeof v === 'object') channelsSections[key] = v;
}
}
const results: ChannelInfo[] = [];
for (const [name, section] of Object.entries(channelsSections)) {
if (!section || typeof section !== 'object') continue;
const ch = section as Record<string, unknown>;
// Check if any credential field is present and non-empty
const credFields = CREDENTIAL_FIELDS[name] ?? [];
let hasCreds = false;
for (const field of credFields) {
const val = ch[field];
if (val && (typeof val === 'string' || typeof val === 'object')) {
hasCreds = true;
break;
}
}
// Also check accounts for multi-account setups
if (!hasCreds && ch.accounts && typeof ch.accounts === 'object') {
for (const acct of Object.values(
ch.accounts as Record<string, unknown>,
)) {
if (!acct || typeof acct !== 'object') continue;
const a = acct as Record<string, unknown>;
for (const field of credFields) {
if (
a[field] &&
(typeof a[field] === 'string' || typeof a[field] === 'object')
) {
hasCreds = true;
break;
}
}
if (hasCreds) break;
}
}
// WhatsApp: check for auth state directory instead of token
if (name === 'whatsapp' && !hasCreds) {
// Will be checked separately via agents directory
hasCreds = false;
}
results.push({ name, hasCreds });
}
return results;
}
// ---------------------------------------------------------------------------
// Workspace scanning
// ---------------------------------------------------------------------------
const WORKSPACE_FILES = [
'SOUL.md',
'USER.md',
'MEMORY.md',
'IDENTITY.md',
'TOOLS.md',
'HEARTBEAT.md',
'BOOTSTRAP.md',
'AGENTS.md',
];
function findWorkspace(stateDir: string, config: Record<string, unknown> | null): {
dir: string | null;
files: string[];
} {
// Check config-specified workspace path first (agent.workspace or agents.defaults.workspace)
const configPaths: string[] = [];
if (config) {
const agentWs = (config.agent as Record<string, unknown> | undefined)?.workspace as string | undefined;
if (agentWs) configPaths.push(agentWs.startsWith('~') ? path.join(os.homedir(), agentWs.slice(1)) : agentWs);
const defaultsWs = ((config.agents as Record<string, unknown> | undefined)?.defaults as Record<string, unknown> | undefined)?.workspace as string | undefined;
if (defaultsWs) configPaths.push(defaultsWs.startsWith('~') ? path.join(os.homedir(), defaultsWs.slice(1)) : defaultsWs);
}
// Check config-specified paths, then default locations
const candidates = [
...configPaths,
...['workspace', 'workspace.default'].map((n) => path.join(stateDir, n)),
];
for (const ws of candidates) {
if (fs.existsSync(ws) && fs.statSync(ws).isDirectory()) {
const found = WORKSPACE_FILES.filter((f) =>
fs.existsSync(path.join(ws, f)),
);
if (found.length > 0) {
return { dir: ws, files: found };
}
}
}
// Check agent-specific workspaces
const agentsDir = path.join(stateDir, 'agents');
if (fs.existsSync(agentsDir)) {
for (const agentId of fs.readdirSync(agentsDir)) {
for (const wsName of ['workspace', 'workspace.default']) {
const ws = path.join(agentsDir, agentId, wsName);
if (fs.existsSync(ws) && fs.statSync(ws).isDirectory()) {
const found = WORKSPACE_FILES.filter((f) =>
fs.existsSync(path.join(ws, f)),
);
if (found.length > 0) {
return { dir: ws, files: found };
}
}
}
}
}
return { dir: null, files: [] };
}
// ---------------------------------------------------------------------------
// Daily memory file detection
// ---------------------------------------------------------------------------
function countDailyMemoryFiles(workspaceDir: string | null): number {
if (!workspaceDir) return 0;
const memoryDir = path.join(workspaceDir, 'memory');
if (!fs.existsSync(memoryDir) || !fs.statSync(memoryDir).isDirectory()) {
return 0;
}
try {
return fs
.readdirSync(memoryDir)
.filter((f) => f.endsWith('.md'))
.length;
} catch {
return 0;
}
}
// ---------------------------------------------------------------------------
// Skills detection
// ---------------------------------------------------------------------------
interface SkillInfo {
name: string;
source: string; // 'workspace' | 'shared' | 'personal' | 'project'
path: string;
}
function detectSkills(
stateDir: string,
workspaceDir: string | null,
): SkillInfo[] {
const skills: SkillInfo[] = [];
const seen = new Set<string>();
const scanDir = (dir: string, source: string) => {
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return;
try {
for (const entry of fs.readdirSync(dir)) {
const skillDir = path.join(dir, entry);
if (!fs.statSync(skillDir).isDirectory()) continue;
// A directory is a skill if it contains SKILL.md
if (fs.existsSync(path.join(skillDir, 'SKILL.md'))) {
if (seen.has(entry)) continue;
seen.add(entry);
skills.push({ name: entry, source, path: skillDir });
}
}
} catch {
// ignore read errors
}
};
// 1. Workspace skills
if (workspaceDir) {
scanDir(path.join(workspaceDir, 'skills'), 'workspace');
// 4. Project-level shared skills
scanDir(path.join(workspaceDir, '.agents', 'skills'), 'project');
}
// 2. Managed/shared skills
scanDir(path.join(stateDir, 'skills'), 'shared');
// 3. Personal cross-project skills
const personalSkills = path.join(os.homedir(), '.agents', 'skills');
scanDir(personalSkills, 'personal');
return skills;
}
// ---------------------------------------------------------------------------
// Identity extraction
// ---------------------------------------------------------------------------
function extractIdentityName(stateDir: string, workspaceDir: string | null): string {
if (!workspaceDir) return '';
const identityPath = path.join(workspaceDir, 'IDENTITY.md');
if (!fs.existsSync(identityPath)) return '';
try {
const content = fs.readFileSync(identityPath, 'utf-8');
// IDENTITY.md uses key:value format, e.g. "name: Claw"
const match = content.match(/^name:\s*(.+)/im);
return match ? match[1].trim() : '';
} catch {
return '';
}
}
// ---------------------------------------------------------------------------
// Agent detection
// ---------------------------------------------------------------------------
function detectAgents(stateDir: string): string[] {
const agentsDir = path.join(stateDir, 'agents');
if (!fs.existsSync(agentsDir)) return [];
try {
return fs
.readdirSync(agentsDir)
.filter((f) => {
const p = path.join(agentsDir, f);
return fs.statSync(p).isDirectory() && !f.startsWith('.');
});
} catch {
return [];
}
}
// ---------------------------------------------------------------------------
// Group detection — from session store and channel config
// ---------------------------------------------------------------------------
interface GroupInfo {
channel: string;
id: string; // Platform-specific ID (WhatsApp JID, Telegram chat ID, etc.)
name: string;
source: 'session' | 'config';
}
/**
* Map OpenClaw session key channel:kind:id to NanoClaw JID format.
* OpenClaw keys: "whatsapp:group:120...@g.us", "telegram:group:-10012345"
* NanoClaw JIDs: "120...@g.us", "tg:-10012345", "dc:12345", "slack:C12345"
*/
function toNanoClawJid(channel: string, id: string): string {
switch (channel) {
case 'whatsapp':
return id; // Already in JID format (120...@g.us)
case 'telegram':
return `tg:${id}`;
case 'discord':
return `dc:${id}`;
case 'slack':
return `slack:${id}`;
default:
return `${channel}:${id}`;
}
}
function detectGroups(
stateDir: string,
config: Record<string, unknown> | null,
agents: string[],
): GroupInfo[] {
const groups: GroupInfo[] = [];
const seen = new Set<string>();
// Source 1: Session store — scan for group session keys
for (const agentId of agents) {
const sessionsPath = path.join(
stateDir,
'agents',
agentId,
'sessions',
'sessions.json',
);
if (!fs.existsSync(sessionsPath)) continue;
try {
const raw = fs.readFileSync(sessionsPath, 'utf-8');
const data = JSON.parse(raw) as Record<string, unknown>;
// Sessions can be stored as an object with session keys, or as
// { sessions: { key: entry } } or { entries: [...] }
const entries =
(data.sessions as Record<string, unknown>) ??
(data.entries as Record<string, unknown>) ??
data;
for (const [key, value] of Object.entries(entries)) {
// Match session keys like "whatsapp:group:120...@g.us"
// or prefixed "agent:main:whatsapp:group:120...@g.us"
// Also match DM sessions: "whatsapp:dm:number@s.whatsapp.net"
const match = key.match(/(\w+):(group|dm|channel):(.+)$/i);
if (!match) continue;
const [, channel, kind, id] = match;
// Skip DM sessions for group detection — they're individual chats
if (kind === 'dm') continue;
const dedupKey = `${channel}:${id}`;
if (seen.has(dedupKey)) continue;
seen.add(dedupKey);
// Try to extract display name from session entry
let name = '';
if (value && typeof value === 'object') {
const entry = value as Record<string, unknown>;
name =
(entry.displayName as string) ??
(entry.label as string) ??
(entry.subject as string) ??
'';
}
groups.push({
channel,
id,
name: name || id,
source: 'session',
});
}
} catch {
// Ignore parse errors
}
}
// Source 2: Channel config — groups explicitly configured
if (config) {
const channels =
(config.channels as Record<string, unknown> | undefined) ?? {};
for (const [channelName, channelSection] of Object.entries(channels)) {
if (!channelSection || typeof channelSection !== 'object') continue;
const ch = channelSection as Record<string, unknown>;
// WhatsApp/Telegram: channels.<channel>.groups.<groupId>
const configGroups = ch.groups as Record<string, unknown> | undefined;
if (configGroups) {
for (const groupId of Object.keys(configGroups)) {
const dedupKey = `${channelName}:${groupId}`;
if (seen.has(dedupKey)) continue;
seen.add(dedupKey);
groups.push({
channel: channelName,
id: groupId,
name: groupId,
source: 'config',
});
}
}
// Discord: channels.discord.guilds.<guildId>
if (channelName === 'discord') {
const guilds = ch.guilds as Record<string, unknown> | undefined;
if (guilds) {
for (const guildId of Object.keys(guilds)) {
const dedupKey = `discord:${guildId}`;
if (seen.has(dedupKey)) continue;
seen.add(dedupKey);
groups.push({
channel: 'discord',
id: guildId,
name: guildId,
source: 'config',
});
}
}
}
}
}
return groups;
}
// ---------------------------------------------------------------------------
// Cron job counting
// ---------------------------------------------------------------------------
function countCronJobs(stateDir: string): {
count: number;
summaries: string[];
} {
const jobsPath = path.join(stateDir, 'cron', 'jobs.json');
if (!fs.existsSync(jobsPath)) return { count: 0, summaries: [] };
try {
const raw = fs.readFileSync(jobsPath, 'utf-8');
const data = JSON.parse(raw) as {
jobs?: Array<{ name?: string; enabled?: boolean }>;
};
const jobs = data.jobs ?? [];
const summaries = jobs
.filter((j) => j.enabled !== false)
.map((j) => j.name || 'unnamed')
.slice(0, 10);
return { count: jobs.length, summaries };
} catch {
return { count: 0, summaries: [] };
}
}
// ---------------------------------------------------------------------------
// Config-registered plugins and skills (with API keys)
// ---------------------------------------------------------------------------
interface ConfigPlugin {
name: string;
source: 'skills.entries' | 'plugins.entries';
hasApiKey: boolean;
}
function detectConfigPlugins(
config: Record<string, unknown>,
): ConfigPlugin[] {
const results: ConfigPlugin[] = [];
// Check skills.entries (e.g. openai-whisper-api with apiKey)
const skills = config.skills as Record<string, unknown> | undefined;
const skillEntries = skills?.entries as Record<string, unknown> | undefined;
if (skillEntries) {
for (const [name, entry] of Object.entries(skillEntries)) {
if (!entry || typeof entry !== 'object') continue;
const e = entry as Record<string, unknown>;
const hasKey = !!(e.apiKey || e.token || e.key);
results.push({ name, source: 'skills.entries', hasApiKey: hasKey });
}
}
// Check plugins.entries (e.g. brave with config.webSearch.apiKey)
const plugins = config.plugins as Record<string, unknown> | undefined;
const pluginEntries = plugins?.entries as Record<string, unknown> | undefined;
if (pluginEntries) {
for (const [name, entry] of Object.entries(pluginEntries)) {
if (!entry || typeof entry !== 'object') continue;
// Deep-search for apiKey in nested config
const hasKey = JSON.stringify(entry).includes('apiKey');
results.push({ name, source: 'plugins.entries', hasApiKey: hasKey });
}
}
return results;
}
// ---------------------------------------------------------------------------
// MCP server detection
// ---------------------------------------------------------------------------
function detectMcpServers(
config: Record<string, unknown>,
): string[] {
const mcp = config.mcp as Record<string, unknown> | undefined;
if (!mcp) return [];
const servers = mcp.servers as Record<string, unknown> | undefined;
if (!servers) return [];
return Object.keys(servers);
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
function main(): void {
const { stateDir: explicitDir } = parseArgs();
const stateDir = resolveStateDir(explicitDir);
if (!stateDir) {
emitStatus({ STATUS: 'not_found' });
return;
}
const config = loadConfig(stateDir);
const channels = config ? detectChannels(config) : [];
const { dir: workspaceDir, files: workspaceFiles } =
findWorkspace(stateDir, config);
const identityName = extractIdentityName(stateDir, workspaceDir);
const agents = detectAgents(stateDir);
const groups = detectGroups(stateDir, config, agents);
const { count: cronCount, summaries: cronSummaries } =
countCronJobs(stateDir);
const mcpServers = config ? detectMcpServers(config) : [];
const dailyMemoryFiles = countDailyMemoryFiles(workspaceDir);
const skills = detectSkills(stateDir, workspaceDir);
const configPlugins = config ? detectConfigPlugins(config) : [];
// Format channels as "name(has_creds)" or "name(no_creds)"
const channelList = channels
.map((c) => `${c.name}(${c.hasCreds ? 'has_creds' : 'no_creds'})`)
.join(',');
// Separate supported vs unsupported
const unsupported = channels
.filter((c) => !SUPPORTED_CHANNELS.has(c.name))
.map((c) => c.name)
.join(',');
// Format groups as "channel:id(name)" — also include NanoClaw JID mapping
const groupList = groups
.map(
(g) =>
`${g.channel}:${g.id}(${g.name})=>${toNanoClawJid(g.channel, g.id)}`,
)
.join('|');
// Format skills as "name(source)" list
const skillList = skills
.map((s) => `${s.name}(${s.source})`)
.join(',');
// Dump raw top-level config keys so Claude can see what exists
// beyond what this script specifically detects
const configTopKeys = config ? Object.keys(config).sort().join(',') : 'none';
const configChannelKeys = config?.channels
? Object.keys(config.channels as Record<string, unknown>).sort().join(',')
: 'none';
// List files/dirs at the state dir root for manual inspection
let stateDirContents = 'unknown';
try {
stateDirContents = fs
.readdirSync(stateDir)
.filter((f) => !f.startsWith('.'))
.sort()
.join(',');
} catch {
// ignore
}
emitStatus({
STATUS: 'found',
STATE_DIR: stateDir,
CONFIG_FOUND: config !== null,
CONFIG_TOP_KEYS: configTopKeys,
CONFIG_CHANNEL_KEYS: configChannelKeys,
STATE_DIR_CONTENTS: stateDirContents,
CHANNELS: channelList || 'none',
UNSUPPORTED_CHANNELS: unsupported || 'none',
WORKSPACE_DIR: workspaceDir || 'not_found',
WORKSPACE_FILES: workspaceFiles.join(',') || 'none',
IDENTITY_NAME: identityName || 'unknown',
AGENT_COUNT: agents.length,
AGENT_IDS: agents.join(',') || 'none',
GROUPS: groupList || 'none',
GROUP_COUNT: groups.length,
DAILY_MEMORY_FILES: dailyMemoryFiles,
SKILL_COUNT: skills.length,
SKILLS: skillList || 'none',
CONFIG_PLUGINS: configPlugins.map((p) => `${p.name}(${p.source}${p.hasApiKey ? ',has_key' : ''})`).join(',') || 'none',
CONFIG_PLUGIN_COUNT: configPlugins.length,
CRON_JOBS: cronCount,
CRON_SUMMARIES: cronSummaries.join('|') || 'none',
MCP_SERVERS: mcpServers.join(',') || 'none',
});
}
main();
@@ -0,0 +1,476 @@
/**
* Extract a channel credential from an OpenClaw configuration and write it
* directly to the NanoClaw .env file.
*
* 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:
* - Plain string: "bot-token-value"
* - Env template: "${TELEGRAM_BOT_TOKEN}"
* - SecretRef object: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN" }
*
* Also reads <state-dir>/.env for env-based secrets.
*
* Credential values are NEVER emitted to stdout only masked versions.
* When --write-env is provided, the script writes credentials directly to
* the target .env file so the agent never sees raw secrets.
*
* Emits a status block on stdout:
* === NANOCLAW MIGRATE: CREDENTIAL ===
* ...
* === END ===
*/
import fs from 'fs';
import os from 'os';
import path from 'path';
// ---------------------------------------------------------------------------
// JSON5-tolerant parser (same as discover script)
// ---------------------------------------------------------------------------
function parseJson5(text: string): unknown {
let cleaned = text.replace(
/("(?:[^"\\]|\\.)*")|\/\/[^\n]*/g,
(match, str) => (str ? str : ''),
);
cleaned = cleaned.replace(
/("(?:[^"\\]|\\.)*")|\/\*[\s\S]*?\*\//g,
(match, str) => (str ? str : ''),
);
cleaned = cleaned.replace(/,\s*([}\]])/g, '$1');
return JSON.parse(cleaned);
}
// ---------------------------------------------------------------------------
// Inline dotenv parser (reads key=value, skips comments)
// ---------------------------------------------------------------------------
function parseDotenv(filePath: string): Record<string, string> {
const env: Record<string, string> = {};
if (!fs.existsSync(filePath)) return env;
const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIdx = trimmed.indexOf('=');
if (eqIdx === -1) continue;
const key = trimmed.slice(0, eqIdx).trim();
let value = trimmed.slice(eqIdx + 1).trim();
// Strip surrounding quotes
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
env[key] = value;
}
return env;
}
// ---------------------------------------------------------------------------
// Status block emitter
// ---------------------------------------------------------------------------
function emitStatus(fields: Record<string, string | number | boolean>): void {
const lines = ['=== NANOCLAW MIGRATE: CREDENTIAL ==='];
for (const [key, value] of Object.entries(fields)) {
lines.push(`${key}: ${value}`);
}
lines.push('=== END ===');
console.log(lines.join('\n'));
}
// ---------------------------------------------------------------------------
// Credential masking
// ---------------------------------------------------------------------------
function maskCredential(value: string): string {
if (value.length < 10) return '****';
return `${value.slice(0, 4)}...${value.slice(-4)}`;
}
// ---------------------------------------------------------------------------
// SecretRef resolution
// ---------------------------------------------------------------------------
interface SecretRef {
source: string;
provider?: string;
id: string;
}
function resolveSecretInput(
value: unknown,
dotenvVars: Record<string, string>,
): { resolved: string | null; source: string; note?: string } {
if (!value) return { resolved: null, source: 'missing' };
// Plain string
if (typeof value === 'string') {
// Check for env template: "${VAR_NAME}"
const envMatch = value.match(/^\$\{([^}]+)\}$/);
if (envMatch) {
const envKey = envMatch[1];
const envVal =
dotenvVars[envKey] ?? process.env[envKey] ?? null;
if (envVal) {
return { resolved: envVal, source: 'env_template' };
}
return {
resolved: null,
source: 'env_template',
note: `Environment variable ${envKey} not found`,
};
}
// Plain literal value
return { resolved: value, source: 'plain' };
}
// SecretRef object
if (typeof value === 'object' && value !== null) {
const ref = value as SecretRef;
if (ref.source === 'env') {
const envVal =
dotenvVars[ref.id] ?? process.env[ref.id] ?? null;
if (envVal) {
return { resolved: envVal, source: 'env_ref' };
}
return {
resolved: null,
source: 'env_ref',
note: `Environment variable ${ref.id} not found`,
};
}
if (ref.source === 'file') {
return {
resolved: null,
source: 'file_ref',
note: `File-based secret (${ref.id}) — cannot auto-extract, add manually`,
};
}
if (ref.source === 'exec') {
return {
resolved: null,
source: 'exec_ref',
note: `Exec-based secret (${ref.id}) — cannot auto-extract, add manually`,
};
}
}
return { resolved: null, source: 'unknown' };
}
// ---------------------------------------------------------------------------
// Channel credential mapping
// ---------------------------------------------------------------------------
interface ChannelCredentialSpec {
// Fields to look for in the channel config
fields: string[];
// Corresponding NanoClaw env var names
envVars: string[];
}
const CHANNEL_SPECS: Record<string, ChannelCredentialSpec> = {
telegram: {
fields: ['botToken'],
envVars: ['TELEGRAM_BOT_TOKEN'],
},
discord: {
fields: ['token'],
envVars: ['DISCORD_BOT_TOKEN'],
},
slack: {
fields: ['botToken', 'appToken'],
envVars: ['SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN'],
},
whatsapp: {
fields: [], // Auth-state based, no token field
envVars: [],
},
};
// ---------------------------------------------------------------------------
// CLI arg parsing
// ---------------------------------------------------------------------------
function parseArgs(): { channel: string; stateDir: string; writeEnv: string } {
const args = process.argv.slice(2);
let channel = '';
let stateDir = '';
let writeEnv = '';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--channel' && args[i + 1]) {
channel = args[++i].toLowerCase();
}
if (args[i] === '--state-dir' && args[i + 1]) {
stateDir = args[++i];
}
if (args[i] === '--write-env' && args[i + 1]) {
writeEnv = args[++i];
}
}
if (!channel) {
console.error('Usage: --channel <name> --state-dir <path> [--write-env <path>]');
process.exit(1);
}
// Expand ~ prefix
if (stateDir.startsWith('~')) {
stateDir = path.join(os.homedir(), stateDir.slice(1));
}
// Default state dir
if (!stateDir) {
const home = os.homedir();
if (fs.existsSync(path.join(home, '.openclaw'))) {
stateDir = path.join(home, '.openclaw');
} else if (fs.existsSync(path.join(home, '.clawdbot'))) {
stateDir = path.join(home, '.clawdbot');
} else {
console.error(
'No OpenClaw directory found. Use --state-dir to specify.',
);
process.exit(1);
}
}
return { channel, stateDir, writeEnv };
}
// ---------------------------------------------------------------------------
// .env writer — appends or replaces a KEY=VALUE line
// ---------------------------------------------------------------------------
function writeEnvVar(envPath: string, key: string, value: string): void {
let content = '';
if (fs.existsSync(envPath)) {
content = fs.readFileSync(envPath, 'utf-8');
}
const pattern = new RegExp(`^${key}=.*$`, 'm');
const line = `${key}="${value}"`;
if (pattern.test(content)) {
content = content.replace(pattern, line);
} else {
content = content.trimEnd() + (content ? '\n' : '') + line + '\n';
}
fs.writeFileSync(envPath, content);
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
function main(): void {
const { channel, stateDir, writeEnv } = parseArgs();
const spec = CHANNEL_SPECS[channel];
// Load dotenv from state dir
const dotenvVars = parseDotenv(path.join(stateDir, '.env'));
// Also check auth-profiles.json for API keys
const authProfilesPath = path.join(stateDir, 'auth-profiles.json');
let authProfiles: Record<string, unknown> = {};
if (fs.existsSync(authProfilesPath)) {
try {
authProfiles = JSON.parse(
fs.readFileSync(authProfilesPath, 'utf-8'),
) as Record<string, unknown>;
} catch {
// Ignore parse errors
}
}
// WhatsApp special case: no token, auth-state based.
// OpenClaw stores Baileys auth at <stateDir>/credentials/whatsapp/<accountId>/
// using useMultiFileAuthState (same as NanoClaw). The files are directly compatible.
if (channel === 'whatsapp') {
const authPaths = [
path.join(stateDir, 'credentials', 'whatsapp', 'default'),
path.join(stateDir, 'credentials', 'whatsapp'),
path.join(stateDir, 'wa-auth'),
];
// Also scan credentials/whatsapp/ for any account subdirectory
const waCredsDir = path.join(stateDir, 'credentials', 'whatsapp');
if (fs.existsSync(waCredsDir)) {
try {
for (const entry of fs.readdirSync(waCredsDir)) {
const candidate = path.join(waCredsDir, entry);
if (fs.statSync(candidate).isDirectory()) {
authPaths.push(candidate);
}
}
} catch {
// ignore
}
}
let authStatePath = '';
for (const p of authPaths) {
// Look for creds.json inside the directory — that confirms valid Baileys auth state
if (fs.existsSync(path.join(p, 'creds.json'))) {
authStatePath = p;
break;
}
}
emitStatus({
CHANNEL: 'whatsapp',
HAS_CREDENTIAL: false,
CREDENTIAL_SOURCE: 'auth_state',
NOTE: authStatePath
? `Baileys auth state found at ${authStatePath}. May not be portable across versions — recommend re-authenticating.`
: 'No WhatsApp auth state found. Will need to authenticate during setup.',
AUTH_STATE_PATH: authStatePath || 'not_found',
});
return;
}
// Unknown channel
if (!spec) {
emitStatus({
CHANNEL: channel,
HAS_CREDENTIAL: false,
NOTE: `Channel "${channel}" is not supported by NanoClaw. Supported: telegram, discord, slack, whatsapp.`,
});
return;
}
// Load OpenClaw config
let config: Record<string, unknown> | null = null;
for (const name of ['openclaw.json', 'clawdbot.json']) {
const configPath = path.join(stateDir, name);
if (fs.existsSync(configPath)) {
try {
config = parseJson5(
fs.readFileSync(configPath, 'utf-8'),
) as Record<string, unknown>;
break;
} catch {
// Try next
}
}
}
if (!config) {
emitStatus({
CHANNEL: channel,
HAS_CREDENTIAL: false,
NOTE: 'Could not load openclaw.json',
});
return;
}
const channels =
(config.channels as Record<string, unknown> | undefined) ?? {};
const channelConfig =
(channels[channel] as Record<string, unknown> | undefined) ?? {};
// Try to resolve each credential field
const results: Array<{
envVar: string;
resolved: string | null;
masked: string;
source: string;
note?: string;
}> = [];
for (let i = 0; i < spec.fields.length; i++) {
const field = spec.fields[i];
const envVar = spec.envVars[i];
// Check top-level channel config first
let rawValue = channelConfig[field];
// If not found, check first account
if (!rawValue && channelConfig.accounts) {
const accounts = channelConfig.accounts as Record<string, unknown>;
const firstAccount = Object.values(accounts)[0] as
| Record<string, unknown>
| undefined;
if (firstAccount) {
rawValue = firstAccount[field];
}
}
const { resolved, source, note } = resolveSecretInput(
rawValue,
dotenvVars,
);
results.push({
envVar,
resolved,
masked: resolved ? maskCredential(resolved) : '',
source,
note,
});
}
// Emit results for the primary credential
const primary = results[0];
if (!primary) {
emitStatus({
CHANNEL: channel,
HAS_CREDENTIAL: false,
NOTE: `No credential fields defined for ${channel}`,
});
return;
}
// If --write-env is set and credentials were resolved, write directly to .env.
// Credential values never appear in stdout.
let written = 0;
if (writeEnv) {
for (const r of results) {
if (r.resolved) {
writeEnvVar(writeEnv, r.envVar, r.resolved);
written++;
}
}
}
const fields: Record<string, string | number | boolean> = {
CHANNEL: channel,
HAS_CREDENTIAL: !!primary.resolved,
CREDENTIAL_SOURCE: primary.source,
CREDENTIAL_MASKED: primary.masked || 'none',
NANOCLAW_ENV_VAR: primary.envVar,
};
if (writeEnv && written > 0) {
fields.WRITTEN_TO = writeEnv;
fields.WRITTEN_COUNT = written;
}
if (primary.note) {
fields.NOTE = primary.note;
}
// Additional credentials (e.g. Slack has botToken + appToken)
if (results.length > 1) {
for (let i = 1; i < results.length; i++) {
const extra = results[i];
const suffix = `_${i + 1}`;
fields[`HAS_CREDENTIAL${suffix}`] = !!extra.resolved;
fields[`CREDENTIAL_SOURCE${suffix}`] = extra.source;
fields[`CREDENTIAL_MASKED${suffix}`] = extra.masked || 'none';
fields[`NANOCLAW_ENV_VAR${suffix}`] = extra.envVar;
if (extra.note) {
fields[`NOTE${suffix}`] = extra.note;
}
}
}
emitStatus(fields);
}
main();
+484
View File
@@ -0,0 +1,484 @@
---
name: migrate-nanoclaw
description: Extracts user customizations from a fork, generates a replayable migration guide, and upgrades to upstream by reapplying customizations on a clean base. Replaces merge-based upgrades with intent-based migration.
---
# Context
NanoClaw users fork the repo and customize it — changing config values, editing source files, modifying personas, adding skills. When upstream ships updates or refactors, `git merge` produces painful conflicts because the same core files were changed on both sides.
This skill extracts the user's customizations into a migration guide — capturing both the intent (what they want) and the implementation details (how they did it, with code snippets, API calls, and specific configurations). On upgrade, it checks out clean upstream in a worktree, then reapplies customizations using the guide. No merge conflicts because there's nothing to merge.
The migration guide is markdown, not structured data. It needs to capture the full range of what a user might customize, with enough implementation detail that a fresh Claude session can reapply it without having seen the original code. Standard changes (config values, simple logic) can be described briefly. Non-standard changes (specific APIs, custom integrations, unusual patterns) need code snippets and precise instructions.
Two phases: **Extract** (build the migration guide) and **Upgrade** (use it). If a guide already exists, offer to skip to Upgrade.
# Principles
- Never proceed with a dirty working tree.
- Always create a rollback point (backup branch + tag) before touching anything.
- The migration guide is the source of truth, not diffs.
- Use a worktree to validate before affecting the live install.
- Data directories (`groups/`, `store/`, `data/`, `.env`) are never touched — only code.
- Be helpful: offer to do things (stash, commit, stop services) rather than telling the user to do them.
- **Use sub-agents for exploration.** Spawn haiku sub-agents to explore the codebase, trace skill merges, diff files, and identify customizations. This keeps the main context focused on the user conversation and decision-making.
- **Always use absolute paths in worktrees.** The Bash tool resets the working directory between calls. Never use relative `cd .upgrade-worktree` — always use the full absolute path: `cd /absolute/path/.upgrade-worktree && <command>`. Store the worktree absolute path in a variable at creation time and reference it throughout.
- **Balance exploration and asking.** Don't bombard the user with questions when you can figure things out from the code. Don't burn endless tokens exploring when the user could clarify in one sentence. Use sub-agents to explore first, then ask the user targeted questions about things that are ambiguous or where intent isn't clear from the code alone.
- **Scale effort to complexity.** Not every migration needs the full process. Assess the scope early and take the lightest path that fits.
---
# Phase 1: Extract
## 1.0 Preflight
Run `git status --porcelain`. If non-empty, offer to stash or commit for them (AskUserQuestion: "Stash changes" / "Commit changes" / "I'll handle it"). If they want to commit, stage and commit with a descriptive message. If they want to stash, run `git stash push -m "pre-migration stash"`.
Check remotes with `git remote -v`. If `upstream` is missing, ask for the URL (default: `https://github.com/qwibitai/nanoclaw.git`), add it, then `git fetch upstream --prune`.
Detect upstream branch: check `git branch -r | grep upstream/` for `main` or `master`. Store as UPSTREAM_BRANCH.
## 1.1 Assess scope and determine path
Quickly assess the scale of divergence, check for an existing guide, and determine the right approach — all before asking the user anything.
```bash
BASE=$(git merge-base HEAD upstream/$UPSTREAM_BRANCH)
# Divergence stats
git rev-list --count $BASE..upstream/$UPSTREAM_BRANCH # upstream commits
git rev-list --count $BASE..HEAD # user commits
git diff --name-only $BASE..HEAD | wc -l # user changed files
git diff --stat $BASE..HEAD | tail -1 # insertions/deletions
git diff --name-only $BASE..upstream/$UPSTREAM_BRANCH | wc -l # upstream changed files
```
Check for existing guide: `.nanoclaw-migrations/guide.md` or `.nanoclaw-migrations/index.md`.
**Determine the tier based on the total diff from base:**
### Tier 1: Lightweight — suggest `/update-nanoclaw` instead
Conditions (any of):
- Very few upstream changes (< ~5 commits) AND few user changes (< ~3 changed files)
- User recently updated/migrated (merge-base is close to upstream HEAD)
Tell the user the scope is small and suggest `/update-nanoclaw` might be simpler. Let them choose.
### Tier 2: Standard
Conditions:
- Moderate total diff (3-15 changed files, no large number of new files)
- Manageable scope that fits in a single guide file
### Tier 3: Complex
Conditions (any of):
- Many new files added (indicates many skills applied) — discount files that come purely from skill merges when assessing complexity; a fork with 3 skills and no other changes is simpler than it looks by file count alone
- Deep source changes to core files (`src/index.ts`, `src/container-runner.ts`, etc.) beyond what skills introduced
- Lots of insertions/deletions in user-authored code (not skill-merged code)
- Many skills applied (3+) AND the user confirms or sub-agents find customizations on top of them
Use the full process: multiple sub-agents in parallel, directory-based guide, migration plan.
**Now combine the scope assessment with initial user input in one interaction.** Present the scope summary (how many commits, files, which tier) and ask (AskUserQuestion):
For Tier 1:
- **Use /update-nanoclaw** — simpler merge-based approach
- **Proceed with full migration** — continue
For Tier 2/3 (with or without existing guide):
- If guide exists and is current: **Skip to upgrade** / **Update guide** (add new changes) / **Re-extract from scratch**
- If guide exists but is stale: **Update guide** (recommended) / **Re-extract from scratch** / **Skip to upgrade anyway**
- If no guide: **Yes, let me describe my customizations first** / **Just figure it out** / **A bit of both**
This single interaction replaces what were previously separate steps for scope assessment, user input, and existing guide check.
## 1.2 Update existing guide (if applicable)
If the user chose to update an existing guide rather than re-extract:
1. Read the existing guide
2. Find commits made since the guide was generated (compare guide's recorded base hash against current HEAD)
3. Spawn a haiku sub-agent to analyze only the new changes:
> Diff HEAD against `<guide-recorded-hash>`. For each changed file, summarize what changed and why.
4. Present the new changes to the user for confirmation
5. Append new customizations to the existing guide, update the header hashes
6. Skip to Phase 2
## 1.3 Explore the codebase
Spawn a haiku sub-agent (Agent tool, model: haiku) for initial exploration:
> Explore this NanoClaw fork to identify all changes from the upstream base. Run these commands and report back:
>
> 1. `git diff --name-only $BASE..HEAD` — all changed files
> 2. `git log --oneline $BASE..HEAD` — all commits (look for skill branch merges like `Merge branch 'skill/*'`)
> 3. `git branch -r --list 'upstream/skill/*'` — available upstream skill branches
> 4. `ls .claude/skills/` — installed skills
> 5. For each skill merge found, record the merge commit hash
>
> Report: (a) list of applied skills with their merge commit hashes, (b) list of all changed files, (c) any custom skill directories that don't match upstream branches.
From the sub-agent results, identify:
- **Which files came purely from skill merges** — these will be reapplied by re-merging skill branches in Phase 2
- **Everything else** — all remaining changes are customizations to analyze (whether they're on skill-touched files or not)
Don't try to distinguish "user modified a skill file" from "user made their own change" at this stage. The sub-agents in 1.4 will look at all non-skill changes together and surface what matters.
## 1.4 Analyze customizations
For each applied skill, ask the user in a single batched question (AskUserQuestion, multiSelect):
> "I found these applied skills. Select any you customized further after applying:"
Options: one per skill, plus "None — all used as-is".
Then spawn sub-agents to analyze all non-skill changes. For Tier 2, one or two agents. For Tier 3, run in parallel by area:
- **Config + build files** — one sub-agent
- **Source files** (`src/*.ts`) — one sub-agent
- **Skills the user flagged as modified** (or all of them for Tier 3) — one sub-agent per skill, comparing the user's current files against the skill merge commit version:
```
git diff <merge-commit-hash>..HEAD -- <files-touched-by-skill>
```
- **Container files** — one sub-agent (if changes exist)
Each sub-agent task:
> Read these diffs and the current file contents. For each change:
> 1. `git diff $BASE..HEAD -- <file>` (or `git diff <skill-merge-hash>..HEAD -- <file>` for skill-modified files)
> 2. Read the full current file for context
> 3. Summarize: what changed, what the likely intent is
> 4. Assess detail level: could a fresh Claude session reproduce this from intent alone, or does it need specific code snippets, API details, import paths?
> 5. For non-standard changes, extract the key code, imports, API calls, and configurations verbatim.
**Inter-skill conflicts:** If multiple skills are applied, spawn an additional sub-agent to check for interactions between them. Look for:
- Duplicate declarations (same variable/constant defined by two skill branches)
- Conflicting approaches (one skill throws on missing env var, another provides a fallback)
- Shared files modified by multiple skills
Document any findings in the "Skill Interactions" section of the migration guide so they can be resolved after skill branches are re-merged during upgrade.
## 1.5 Confirm with user
After sub-agents report back, compile the findings and present to the user.
For customizations where the intent is clear (config values, simple modifications): present as a batch for confirmation. Use AskUserQuestion with multiSelect to let the user flag any entries that need correction.
For customizations where the intent is ambiguous: ask specific questions. Don't ask "what did you do?" — instead ask "I see you added X in this file. Was this for Y or something else?"
The user can select "Other" on any question to provide their own description.
## 1.6 Migration plan (Tier 3 only)
For complex migrations, before writing the guide, create a migration plan:
- **Order of operations**: which customizations depend on others, which skills must be applied first
- **Staging**: whether the migration should happen in stages (e.g. apply skills first, validate, then apply source customizations)
- **Risk areas**: customizations that touch files heavily changed by upstream — these may need manual review
- **Interactions**: customizations that interact with each other (e.g. a source change that depends on a skill, or two customizations that touch the same file)
Present the plan to the user for review before proceeding to the guide.
## 1.7 Write the migration guide
**Storage:** `.nanoclaw-migrations/guide.md` for Tier 2. `.nanoclaw-migrations/` directory with `index.md` and section files for Tier 3.
**Verification:** After writing the guide, read it back and verify:
- Every referenced file path exists in the current codebase
- Code snippets match what's actually in the files
- No customizations from the analysis were accidentally omitted
The guide is structured markdown that a fresh Claude session can follow to reproduce this user's exact setup on a clean upstream checkout.
Structure:
```markdown
# NanoClaw Migration Guide
Generated: <timestamp>
Base: <BASE hash>
HEAD at generation: <HEAD hash>
Upstream: <upstream HEAD hash>
## Migration Plan
(Tier 3 only — big-picture overview of order, staging, risks)
## Applied Skills
List each skill with its branch name. These are reapplied by merging the upstream skill branch.
- `add-telegram` — branch `skill/telegram`
- `add-voice-transcription` — branch `skill/voice-transcription`
Custom skills (user-created, not from upstream): `.claude/skills/my-custom-skill/` — copy as-is from main tree.
## Skill Interactions
(Document known conflicts or interactions between applied skills.
When two or more skills modify the same file or depend on shared
config, describe the conflict and how to resolve it after merging.
Example: skill A and skill B both add a PROXY_BIND_HOST declaration —
after merging both, deduplicate. Or: skill A throws if ENV_VAR is
missing, but skill B provides a fallback — use the fallback version.)
## Modifications to Applied Skills
### <Skill name>: <what was modified>
**Intent:** ...
**Files:** ...
**How to apply:** (after the skill branch has been merged)
...
## Customizations
### <Descriptive title for customization>
**Intent:** What the user wants and why.
**Files:** Which files to modify.
**How to apply:**
<For standard changes, a brief description is enough.>
<For non-standard changes, include code snippets, API details,
specific values, import paths — everything needed to reproduce
without seeing the original diff.>
### <Next customization...>
```
**Judging detail level:** For each customization, assess whether a fresh Claude session could reproduce it from intent alone:
- **Standard changes** (config values, simple logic, well-known patterns): describe the intent and the target. Example: "Change `POLL_INTERVAL` in `src/config.ts` from 2000 to 1000."
- **Non-standard changes** (specific API usage, custom integrations, unusual patterns, library-specific configurations): include the actual code snippets, import paths, API endpoints, configuration objects — everything needed to reproduce it without guessing.
Example entries at different detail levels:
**Standard (brief):**
```markdown
### Custom trigger word
**Intent:** Use `@Bob` instead of the default `@Andy`.
**Files:** `src/config.ts`
**How to apply:** Change the default value of `ASSISTANT_NAME` from `'Andy'` to `'Bob'`.
```
**Non-standard (detailed):**
```markdown
### Spanish translation for outbound messages
**Intent:** All outbound messages are translated to Spanish before sending. Uses the DeepL API via the `deepl-node` package.
**Files:** `src/router.ts`, `package.json`
**How to apply:**
1. Add dependency: `npm install deepl-node`
2. In `src/router.ts`, add import at top:
```typescript
import * as deepl from 'deepl-node';
const translator = new deepl.Translator(process.env.DEEPL_API_KEY!);
```
3. In the `formatOutbound` function, before the return statement, add:
```typescript
const result = await translator.translateText(text, null, 'es');
text = result.text;
```
Note: the function needs to be made async if it isn't already.
```
After writing, offer to commit for the user:
```bash
git add .nanoclaw-migrations/
git commit -m "chore: save migration guide"
```
Ask (AskUserQuestion): "Migration guide saved. Want to upgrade now or later?"
- **Upgrade now** — continue to Phase 2
- **Later** — stop here
---
# Phase 2: Upgrade
## 2.0 Preflight
Same checks as 1.0 — clean tree (offer to stash/commit if dirty), upstream configured, fetch latest.
Read the migration guide. If missing, tell the user you need to extract customizations first and ask if they want to do that now.
**New-changes guard:** Compare the guide's "HEAD at generation" hash against current HEAD. If there are commits since the guide was generated, warn the user:
> "You've made changes since the migration guide was generated. These changes won't be included in the upgrade."
AskUserQuestion:
- **Update the guide first** — go to step 1.2 to incorporate new changes
- **Proceed anyway** — user accepts that recent changes will be lost
- **Abort** — stop
## 2.1 Safety net
```bash
HASH=$(git rev-parse --short HEAD)
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
git branch backup/pre-migrate-$HASH-$TIMESTAMP
git tag pre-migrate-$HASH-$TIMESTAMP
```
Save the tag name for rollback instructions at the end.
## 2.2 Preview upstream changes
```bash
BASE=$(git merge-base HEAD upstream/$UPSTREAM_BRANCH)
git log --oneline $BASE..upstream/$UPSTREAM_BRANCH
git diff $BASE..upstream/$UPSTREAM_BRANCH -- CHANGELOG.md
```
If there are `[BREAKING]` entries, show them and explain how they interact with the user's customizations from the migration guide.
Ask (AskUserQuestion) to proceed or abort.
## 2.3 Create upgrade worktree
```bash
PROJECT_ROOT=$(pwd)
git worktree add .upgrade-worktree upstream/$UPSTREAM_BRANCH --detach
WORKTREE="$PROJECT_ROOT/.upgrade-worktree"
```
Store `$PROJECT_ROOT` and `$WORKTREE` as absolute paths. Use `$WORKTREE` in all subsequent commands — never `cd .upgrade-worktree` with a relative path.
## 2.4 Reapply skills in worktree
For each skill listed in the migration guide's "Applied Skills" section:
1. Check if branch exists: `git branch -r --list "upstream/$branch"`
2. If yes, merge it in the worktree:
```bash
cd "$WORKTREE" && git merge upstream/skill/<name> --no-edit
```
3. If missing, warn the user (skill may have been removed or renamed upstream).
4. If any skill merge conflicts, stop and tell the user — the skill needs updating for the new upstream.
Copy any custom skills mentioned in the guide from the main tree into the worktree.
## 2.5 Reapply customizations in worktree
Work in `.upgrade-worktree/`. Follow each customization section in the migration guide, including "Modifications to Applied Skills."
For Tier 3 migrations with a migration plan, follow the plan's ordering and staging. If the plan calls for staged validation (e.g. validate after skills, then validate after source changes), do so.
For each customization:
1. Read the "How to apply" instructions from the guide
2. Read the target file(s) in the worktree to understand the current upstream version
3. Apply the changes as described — use the code snippets and specific instructions from the guide
4. If the target file has changed significantly from what the guide expects (function removed, file restructured, API changed), flag it and ask the user what to do
5. Verify the file has no syntax errors or broken imports after each change
For behavior customizations (CLAUDE.md files): copy from the main tree. These are user content, not code.
## 2.6 Validate in worktree
```bash
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.
## 2.7 Live test (optional)
Ask (AskUserQuestion):
- **Test live** — stop service, run from worktree against real data, send a test message
- **Skip** — trust the build, proceed to swap
If testing live:
1. Stop the service (do this directly):
```bash
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist 2>/dev/null || true
```
2. Symlink data into the worktree:
```bash
ln -s "$PROJECT_ROOT/store" "$WORKTREE/store"
ln -s "$PROJECT_ROOT/data" "$WORKTREE/data"
ln -s "$PROJECT_ROOT/groups" "$WORKTREE/groups"
ln -s "$PROJECT_ROOT/.env" "$WORKTREE/.env"
```
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.
5. After confirmation, stop the dev server.
6. Clean up symlinks:
```bash
rm "$WORKTREE/store" "$WORKTREE/data" "$WORKTREE/groups" "$WORKTREE/.env"
```
## 2.8 Swap into main tree
The swap must be done carefully — the worktree has the upgraded code, but main needs to point to it cleanly. Use absolute paths throughout.
```bash
# 1. Capture the worktree HEAD before removing it
WORKTREE_PATH=$(cd "$PROJECT_ROOT/.upgrade-worktree" && pwd)
UPGRADE_COMMIT=$(git -C "$WORKTREE_PATH" rev-parse HEAD)
# 2. Copy the migration guide out of the worktree before removing it
cp -r "$WORKTREE_PATH/.nanoclaw-migrations" /tmp/nanoclaw-migrations-backup 2>/dev/null || true
# 3. Remove the worktree
git worktree remove "$WORKTREE_PATH" --force
# 4. Point the current branch at the upgraded commit
git reset --hard $UPGRADE_COMMIT
# 5. Restore the migration guide and update its hashes
cp -r /tmp/nanoclaw-migrations-backup/* .nanoclaw-migrations/ 2>/dev/null || true
rm -rf /tmp/nanoclaw-migrations-backup
```
Update the guide's header hashes to reflect the new state. Offer to commit:
```bash
git add .nanoclaw-migrations/
git commit -m "chore: upgrade to upstream $(git rev-parse --short upstream/$UPSTREAM_BRANCH)"
```
Do NOT use `git checkout -B` to create an intermediate branch — this caused issues in practice. The `git reset --hard` to the upgrade commit is the cleanest path since the backup tag already preserves the pre-upgrade state.
## 2.9 Post-upgrade
Run `npm install && pnpm run build` in the main tree to confirm.
Restart the service:
```bash
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
```
Show summary:
- Previous version (backup tag)
- New HEAD
- Customizations reapplied (list from guide)
- Skills reapplied
- Rollback: `git reset --hard <backup-tag>`
- Any customizations that needed manual adjustment
Offer to pop the stash if one was created in preflight: `git stash pop`
## Diagnostics
1. Use the Read tool to read `.claude/skills/migrate-nanoclaw/diagnostics.md`.
2. Follow every step in that file before finishing.
@@ -0,0 +1,52 @@
# Diagnostics
Gather system info:
```bash
node -p "require('./package.json').version"
uname -s
uname -m
node -p "process.versions.node.split('.')[0]"
```
Write `/tmp/nanoclaw-diagnostics.json`. No paths, usernames, hostnames, or IP addresses.
```json
{
"api_key": "phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP",
"event": "migrate_complete",
"distinct_id": "<uuid>",
"properties": {
"success": true,
"nanoclaw_version": "1.2.43",
"os_platform": "darwin",
"arch": "arm64",
"node_major_version": 22,
"migration_phase": "extract|upgrade|both",
"tier": 2,
"customization_count": 3,
"skills_applied_count": 2,
"skill_interaction_count": 0,
"live_test": false,
"breaking_changes_found": false,
"error_count": 0
}
}
```
Show the entire JSON to the user and ask via AskUserQuestion: **Yes** / **No** / **Never ask again**
**Yes**:
```bash
curl -s -X POST https://us.i.posthog.com/capture/ -H 'Content-Type: application/json' -d @/tmp/nanoclaw-diagnostics.json
rm /tmp/nanoclaw-diagnostics.json
```
**No**: `rm /tmp/nanoclaw-diagnostics.json`
**Never ask again**:
1. Replace contents of `.claude/skills/setup/diagnostics.md` with `# Diagnostics — opted out`
2. Replace contents of `.claude/skills/update-nanoclaw/diagnostics.md` with `# Diagnostics — opted out`
3. Replace contents of `.claude/skills/migrate-nanoclaw/diagnostics.md` with `# Diagnostics — opted out`
4. Remove the diagnostics sections from each corresponding SKILL.md
5. `rm /tmp/nanoclaw-diagnostics.json`
+154 -108
View File
@@ -1,58 +1,50 @@
---
name: setup
description: Run initial NanoClaw setup. Use when user wants to install dependencies, authenticate messaging channels, register their main channel, or start the background services. Triggers on "setup", "install", "configure nanoclaw", or first-time setup requests.
allowed-tools: Bash(git remote*), Bash(bash setup.sh), Bash(pnpm exec tsx setup/index.ts*)
---
# 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
---
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
**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.
```!
git remote add upstream https://github.com/qwibitai/nanoclaw.git 2>/dev/null || true
git remote -v
```
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)
Run `bash setup.sh` and parse the status block.
Output from !`bash setup.sh` parse the status block.
- If NODE_OK=false → Node.js is missing or too old. Use `AskUserQuestion: Would you like me to install Node.js 22?` If confirmed:
- macOS: `brew install node@22` (if brew available) or install nvm then `nvm install 22`
@@ -64,69 +56,69 @@ 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.
Output from !`pnpm exec tsx setup/index.ts --step environment` 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
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."
3. **Migrate later** — "Continue setup now, run `/migrate-from-openclaw` anytime later."
If "Migrate now": invoke `/migrate-from-openclaw`, then return here and continue at step 2a (Timezone).
## 2a. Timezone
Run `npx tsx setup/index.ts --step timezone` and parse the status block.
Output from !`pnpm exec tsx setup/index.ts --step timezone` 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:
@@ -146,14 +138,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:
@@ -178,7 +170,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
@@ -187,7 +179,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
@@ -198,48 +190,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. Invoke `/use-native-credential-proxy` to set up the built-in credential proxy instead. That skill handles credential collection, `.env` configuration, and verification.
## 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
**Delegate to the selected channel's skill.** Each channel skill handles its own package installation, authentication, registration, and configuration.
Invoke the matching skill:
- **WhatsApp:** Invoke `/add-whatsapp`
- **Telegram:** Invoke `/add-telegram`
- **Slack:** Invoke `/add-slack`
- **Discord:** Invoke `/add-discord`
- **Slack:** Invoke `/add-slack`
- **Telegram:** Invoke `/add-telegram`
- **GitHub:** Invoke `/add-github`
- **Linear:** Invoke `/add-linear`
- **Microsoft Teams:** Invoke `/add-teams`
- **Google Chat:** Invoke `/add-gchat`
- **WhatsApp Cloud API:** Invoke `/add-whatsapp-cloud`
- **WhatsApp Baileys:** Invoke `/add-whatsapp`
- **Resend:** Invoke `/add-resend`
- **Matrix:** Invoke `/add-matrix`
- **Webex:** Invoke `/add-webex`
- **iMessage:** Invoke `/add-imessage`
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
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 all channel skills complete**, install dependencies and rebuild — channel merges may introduce new packages:
**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
@@ -247,7 +256,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.
@@ -271,27 +280,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.
@@ -302,3 +331,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.
+3
View File
@@ -9,6 +9,8 @@ uname -m
node -p "process.versions.node.split('.')[0]"
```
Check if the user migrated from OpenClaw during this setup session (i.e. `/migrate-from-openclaw` was invoked). If you're unsure (e.g. after context compaction), check for `migration-state.md` in the project root — it exists during and sometimes after migration.
Write `/tmp/nanoclaw-diagnostics.json`. No paths, usernames, hostnames, or IP addresses.
```json
@@ -23,6 +25,7 @@ Write `/tmp/nanoclaw-diagnostics.json`. No paths, usernames, hostnames, or IP ad
"arch": "arm64",
"node_major_version": 22,
"channels_selected": ["telegram", "whatsapp"],
"migrated_from_openclaw": false,
"error_count": 0,
"failed_step": null
}
@@ -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*)"
]
+8 -6
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,9 +109,11 @@ 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.
Present these buckets to the user and ask them to choose one path using AskUserQuestion:
- A) **Full update**: merge all upstream changes
- B) **Selective update**: cherry-pick specific upstream commits
@@ -173,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.
@@ -188,7 +190,7 @@ After validation succeeds, check if the update introduced any breaking changes.
Determine which CHANGELOG entries are new by diffing against the backup tag:
- `git diff <backup-tag-from-step-1>..HEAD -- CHANGELOG.md`
Parse the diff output for lines starting with `+[BREAKING]`. Each such line is one breaking change entry. The format is:
Parse the diff output for lines that contain `[BREAKING]` anywhere in the line. Each such line is one breaking change entry. The format is:
```
[BREAKING] <description>. Run `/<skill-name>` to <action>.
```
@@ -232,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
}
+5 -1
View File
@@ -4,9 +4,13 @@ All notable changes to NanoClaw will be documented in this file.
For detailed release notes, see the [full changelog on the documentation site](https://docs.nanoclaw.dev/changelog).
## [1.2.36] - 2026-03-26
- [BREAKING] Replaced pino logger with built-in logger. WhatsApp users must re-merge the WhatsApp fork to pick up the Baileys logger compatibility fix: `git fetch whatsapp main && git merge whatsapp/main`. If the `whatsapp` remote is not configured: `git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git`.
## [1.2.35] - 2026-03-26
- [BREAKING] OneCLI Agent Vault replaces the built-in credential proxy. Existing `.env` credentials must be migrated to the vault. Run `/init-onecli` to install OneCLI and migrate credentials.
- [BREAKING] OneCLI Agent Vault replaces the built-in credential proxy. Check your runtime: `grep CONTAINER_RUNTIME_BIN src/container-runtime.ts` — if it shows `'container'` you are on Apple Container, if `'docker'` you are on Docker. Docker users: run `/init-onecli` to install OneCLI and migrate `.env` credentials to the vault. Apple Container users: re-merge the skill branch (`git fetch upstream skill/apple-container && git merge upstream/skill/apple-container`) then run `/convert-to-apple-container` and follow all instructions (configures credential proxy networking) — do NOT run `/init-onecli`, it requires Docker.
## [1.2.21] - 2026-03-22
+152 -36
View File
@@ -1,80 +1,196 @@
# 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/`.
## Quick Context
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.
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/isolation-model.md](docs/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 adapter infra (registry, Chat SDK bridge); specific channel adapters are skill-installed from the `channels` branch |
| `src/providers/` | Host-side provider container-config (`claude` baked in; `opencode` etc. installed from the `providers` branch) |
| `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)
## Channels and Providers (skill-installed)
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`.
Trunk does not ship any specific channel adapter or non-default agent provider. The codebase is the registry/infra; the actual adapters and providers live on long-lived sibling branches and get copied in by skills:
- **`channels` branch** — Discord, Slack, Telegram, WhatsApp, Teams, Linear, GitHub, iMessage, Webex, Resend, Matrix, Google Chat, WhatsApp Cloud (+ helpers, tests, channel-specific setup steps). Installed via `/add-<channel>` skills.
- **`providers` branch** — OpenCode (and any future non-default agent providers). Installed via `/add-opencode`.
Each `/add-<name>` skill is idempotent: `git fetch origin <branch>` → copy module(s) into the standard paths → append a self-registration import to the relevant barrel → `pnpm install <pkg>@<pinned-version>` → build.
## Self-Modification
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/`)
- **Channel/provider install skills** — copy the relevant module(s) in from the `channels` or `providers` branch, wire imports, install pinned deps (e.g. `/add-discord`, `/add-slack`, `/add-whatsapp`, `/add-opencode`).
- **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.
## Docs Index
| Doc | Purpose |
|-----|---------|
| [docs/architecture.md](docs/architecture.md) | Full architecture writeup |
| [docs/api-details.md](docs/api-details.md) | Host API + DB schema details |
| [docs/db.md](docs/db.md) | DB architecture overview: three-DB model, cross-mount rules, readers/writers map |
| [docs/db-central.md](docs/db-central.md) | Central DB (`data/v2.db`) — every table + migration system |
| [docs/db-session.md](docs/db-session.md) | Per-session `inbound.db` + `outbound.db` schemas + seq parity |
| [docs/agent-runner-details.md](docs/agent-runner-details.md) | Agent-runner internals + MCP tool interface |
| [docs/isolation-model.md](docs/isolation-model.md) | Three-level channel isolation model |
| [docs/setup-wiring.md](docs/setup-wiring.md) | What's wired, what's open in the setup flow |
| [docs/checklist.md](docs/checklist.md) | Rolling status checklist across all subsystems |
| [docs/architecture-diagram.md](docs/architecture-diagram.md) | Diagram version of the architecture |
| [docs/build-and-runtime.md](docs/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/build-and-runtime.md](docs/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.
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
community@nanoclaw.dev.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+8
View File
@@ -16,3 +16,11 @@ Thanks to everyone who has contributed to NanoClaw!
- [flobo3](https://github.com/flobo3) — Flo
- [edwinwzhe](https://github.com/edwinwzhe) — Edwin He
- [scottgl9](https://github.com/scottgl9) — Scott Glover
- [cschmidt](https://github.com/cschmidt) — Carl Schmidt
- [leonalfredbot-ship-it](https://github.com/leonalfredbot-ship-it) — Alfred-the-buttler
- [moktamd](https://github.com/moktamd)
- [gurixs-carson](https://github.com/gurixs-carson)
- [MrBlaise](https://github.com/MrBlaise) — Balázs Rostás
- [lbsnrs](https://github.com/lbsnrs) — Andreas Liebschner
- [spencer-whitman](https://github.com/spencer-whitman)
- [lazure-ocean](https://github.com/lazure-ocean) — Cyril Ionov
+175
View File
@@ -0,0 +1,175 @@
# NanoClaw Refactor — Forward-Looking Reference
Consolidates what's still relevant from `REFACTOR_PLAN.md` and `REFACTOR_EXECUTION.md`: open decisions, remaining work, operational patterns worth keeping. Historical PR timeline and phase framing have been dropped — the work is in the commit history.
---
## Architecture (still authoritative)
### Module tiers
Three categories, distinguished by shipping model and dependency direction:
| Tier | Where it lives | Loaded by default? | Removal cost |
|------|----------------|--------------------|--------------|
| **Core** | `src/**` (outside `src/modules/`, `src/channels/`, `src/providers/`) | always | N/A — can't remove |
| **Default modules** | `src/modules/<name>/` on main | yes — imported by `src/modules/index.ts` | edit core imports (intentional friction) |
| **Optional modules** | `src/modules/<name>/` on main (for now — see open q #7) | yes, via barrel import | delete files + barrel line + revert `MODULE-HOOK` edits |
| **Channel adapters** | `src/channels/<name>.ts` on `channels` branch | no — cherry-pick via `/add-<name>` | delete files + barrel line |
| **Providers** | on `providers` branch | no — cherry-pick via `/add-<provider>` | delete files + barrel line |
Default modules today: `typing`, `mount-security`, `approvals`, `cli`.
Optional modules: `interactive`, `scheduling`, `permissions`, `agent-to-agent`, `self-mod`.
Dependency rule: **core ← default modules ← optional modules**. Optional modules must not depend on each other. Known transitional violation (flagged): `src/db/messaging-groups.ts` auto-wires `agent_destinations` when agent-to-agent is installed.
### The four registries
Full contract in [`docs/module-contract.md`](docs/module-contract.md). Summary:
1. **Delivery action handlers**`delivery.ts`; modules call `registerDeliveryAction(name, fn)`.
2. **Router inbound gate**`router.ts`; single setter (`setSenderResolver` + `setAccessGate`). Default: allow-all.
3. **Response dispatcher**`response-registry.ts`; modules call `registerResponseHandler(fn)`. First to return `true` claims.
4. **Container MCP tool self-registration**`container/agent-runner/src/mcp-tools/server.ts`; modules call `registerTools([...])` at import.
Anything else single-consumer uses either a `sqlite_master`-guarded inline read or a `MODULE-HOOK:<name>:start/end` skill edit.
### Module distribution (pending)
- **`main`** — core + default modules + default channel (`cli`). Ships clean.
- **`channels`** — fully loaded runnable branch with all channel adapters; skills cherry-pick from it.
- **`providers`** — same pattern for agent providers (OpenCode).
- **`modules` branch** — proposed but NOT created yet. See "Remaining work" below.
---
## Remaining work
### Phase 5: merge `v2``main`
Cut-over the refactor. Pre-reqs (already met): green build, green tests, green service boot, clean `channels` / `providers` syncs.
Open logistics:
- Release versioning: bump to `1.3.0` at merge time or cut a `v2-rc` tag first for internal testing? Non-blocking — decide at merge.
- Coordinate with anyone still running the old `main` (v1.2.53) — breaking change for them.
- Announce the new layout + the one shell command that changed (`pnpm run chat` is new default).
### `modules` branch — create, skip, or defer?
The original plan (PR #10) was to fork a `modules` branch and populate it with the 5 optional modules, so future `/add-<module>` skills pull via `git show origin/modules:path`. Three paths:
- **(a) Create it now.** Matches the `channels`/`providers` pattern for consistency. Extra surface to maintain: every core change must be merged into `modules` at phase boundaries (same cadence as channels/providers). Pays off if we ever want to make a module *truly* optional (not shipped on main).
- **(b) Skip it.** Leave all 5 optional modules shipped on main. No `modules` branch, no install skills, no cherry-picking. Simpler but loses the "opt-in" property for users who want a leaner install.
- **(c) Defer.** Ship main without the modules branch; create it later if someone actually wants to slim their install. No-cost option for now.
Recommendation leans toward (c) — we've already paid the architectural cost (tier boundary, dependency rule, registries) without needing the branch today.
### Per-module follow-ups (tracked as open questions below)
Each has a specific landing zone when we get to it:
- #1113 (admin mechanism, providers registry, container-runner audit) — scope a focused cleanup pass.
- #14 (CLAUDE.md review) — single dedicated PR touching every module.
- #15 (A2A / destinations rethink) — requires design, not just cleanup.
- #1718 (self-mod rethink, per-group source) — requires design.
- #19 (system vs user CLAUDE.md) — requires install-skill tooling.
---
## Operational patterns (keep using these)
### Standing checks for every PR
Non-negotiable; a unit test suite alone doesn't catch circular-import TDZ bugs:
1. `pnpm run build` clean.
2. `pnpm test` + `bun test` (in `container/agent-runner/`) all green.
3. **Service actually starts.** `gtimeout 5 node dist/index.js` (or `launchctl kickstart`) must reach `NanoClaw running`. Unit tests import individual files; only `main()` exercises the module-init order.
4. Expected boot log lines present (at least: `Central DB ready`, `Delivery polls started`, `Host sweep started`, `NanoClaw running`, plus any module lifecycle line like `OneCLI approval handler started` or `CLI channel listening`).
### Module architecture rule (TDZ bug, PR #3)
Any registry state a module writes to at import time must live in a file with **no back-edge to `src/index.ts`** — transitively. `src/index.ts` imports `src/modules/index.js` for side effects; if a module calls `registerX()` at top level and `X` lives in `src/index.ts`, the ES module loader hits a TDZ reference on the const declaration. Fix: registry state lives in its own dependency-free file (e.g. `src/response-registry.ts`). Any new registry follows the same pattern.
### Branch sync procedure
After every `v2` (or future `main`) sync into `channels` / `providers` / future `modules`:
1. **File-presence diff.** Enumerate files that existed pre-sync but are missing post-sync:
```
git ls-tree -r <pre-sync> | awk '{print $4}' | sort > /tmp/pre.txt
git ls-tree -r <post-sync> | awk '{print $4}' | sort > /tmp/post.txt
comm -23 /tmp/pre.txt /tmp/post.txt
```
Classify each missing file:
- **Intentional** (core deleted it) → leave deleted.
- **Branch-owned** (channels branch still needs it) → restore from pre-sync HEAD.
This has caught real losses on both `channels` (17 adapter files plus 3 setup scripts after PR #2's channel move) and `providers` (opencode files after PR #2).
2. **Cross-file consistency.** When restoring a file, check whether something *else* that also changed references it (e.g. `setup/index.ts`'s `STEPS` map).
3. **Run the standing checks** against the synced branch (not just v2).
### Prettier drift pattern
The `format:fix` pre-commit hook sometimes reformats peer files *after* the commit completes, leaving cosmetic-only diffs in the working tree. Discard with `git checkout -- <files>`. Do not re-commit the drift — it's trivial whitespace and noise.
---
## Open questions (curated)
### Design / architecture
1. **`NANOCLAW_ADMIN_USER_IDS` as the admin mechanism.** Host queries `user_roles` at container wake, collapses into env var, container compares sender IDs. Conflates identity-at-send with privilege-at-wake and forces the container to care about namespaced user IDs. Revisit during a container-runner audit.
2. **Host-side `src/providers/` registry.** One real consumer (OpenCode). A registry is probably overkill — the install skill could just edit `container-runner.ts` via `MODULE-HOOK`. Fold into the container-runner audit.
3. **Container-runner audit.** `src/container-runner.ts` has accreted wake/spawn/kill, mount assembly, OneCLI credential application, admin-ID env var, idle timers, image rebuild. Some pieces should pull apart or move into modules. Not blocking. Related to #1 and #2.
4. **Revisit destinations + A2A capability holistically.** The destination projection invariant, dual-purpose routing+ACL table, channel vs agent destination shapes, `createMessagingGroupAgent` auto-wire coupling — more machinery than the feature warrants. Phase 3 moved it out of core intact; a redesign is warranted but scoped post-refactor.
5. **Self-mod approach rethink.** Three separate MCP tools + three delivery actions + three approval handlers for what's essentially "mutate container.config.json and rebuild." Also: post-rebuild latency (host sweep waits up to 60s), and agents sometimes send redundant `add_mcp_server` + `request_rebuild` pairs. Consider collapsing into a single "apply this container-config diff" approval primitive.
6. **Per-agent-group source / per-group base image.** Self-mod today layers packages/MCP on a shared base. As groups diverge (different base images, provider configs, runtime toolchains), the shared-base assumption won't scale. Scope post-refactor.
### Distribution / operational
7. **Providers on a consolidated `modules` branch?** Staying separate for now. Revisit if a second optional provider appears.
8. **Per-group module enablement.** Modules are currently project-wide. If one agent group wants approvals and another doesn't, we'd need per-group feature flags. Flag if asked.
9. **Module removal UX.** We do not drop tables on uninstall. Is that the right default? (Alternative: `/remove-<module>` optionally runs a down migration. YAGNI until requested.)
10. **Cross-module ordering for the response dispatcher.** Registration order determines who claims a given `questionId`. IDs are disjoint in practice (`q-…` vs `appr-…`), so first-match-wins is safe. If a third response-consuming module arrives, we may need keyed dispatch.
11. **Versioned module migrations.** Reinstalls are idempotent (migrator skips anything already in `schema_version`). If a module ships a *new* migration in a later version, the install skill must append the new file + barrel entry without touching prior ones. Simplest rule: install skills are additive; content changes to an already-applied migration are a hard error.
12. **Telegram pairing imports from permissions (channels branch).** `src/channels/telegram.ts` reaches into `src/modules/permissions/db/*` for `grantRole`/`hasAnyOwner`/`upsertUser` in the pairing-bootstrap branch. Cross-branch tier violation. Fix: extract those writes into a pairing helper (e.g. `src/channels/telegram-pairing-accept.ts` or `setup/pair-telegram.ts`). Non-blocking.
### Core slotting (files not explicitly discussed)
13. **`state-sqlite.ts`, `webhook-server.ts`, `timezone.ts`.** state-sqlite is likely core (host tracker). Webhook-server likely core (channel infra). Timezone likely core utility. Confirm if any of them prove to be module-shaped during future audits.
14. **Chat SDK bridge location.** `src/channels/chat-sdk-bridge.ts` is channel infra that bridges adapters on the `channels` branch. Stays in `src/channels/` for now.
15. **OneCLI credential injection.** Lives in `container-runner.ts`. Every agent call uses it, no clean optional boundary. Stays core. Related: `onecli-approvals.ts` is bundled inside the `approvals` default module on the assumption OneCLI stays in core. If OneCLI later moves to its own module, `onecli-approvals` follows.
### Documentation
16. **CLAUDE.md content per module.** Every module ships with project.md + agent.md. Need a dedicated review pass: (a) write the missing agent-to-agent snippets, (b) audit other modules for accuracy/tone, (c) confirm `agent.md` files are actually tailored for the agent vs. copy-pastes of `project.md`.
17. **Split system CLAUDE.md from user CLAUDE.md.** Project `CLAUDE.md` and `groups/global/CLAUDE.md` mix system-authored content (module contracts, install-skill appends) with user customizations. Updates currently risk clobbering user intent. Look at a system-owned region (or separate file) that skills rewrite freely plus a user-owned one that's never touched. Related to #16.
---
## Where the canonical references live
- **Module contract** — [`docs/module-contract.md`](docs/module-contract.md)
- **Architecture overview** — [`docs/architecture.md`](docs/architecture.md)
- **DB layout** — [`docs/db.md`](docs/db.md), [`docs/db-central.md`](docs/db-central.md), [`docs/db-session.md`](docs/db-session.md)
- **Agent-runner internals** — [`docs/agent-runner-details.md`](docs/agent-runner-details.md)
- **Channel isolation model** — [`docs/isolation-model.md`](docs/isolation-model.md)
- **Build + runtime split** — [`docs/build-and-runtime.md`](docs/build-and-runtime.md)
- **Top-level** — [`CLAUDE.md`](CLAUDE.md)
This doc (`REFACTOR.md`) is transient — prune when open questions close; retire entirely once the refactor is fully behind us and the operational patterns have been absorbed into `CLAUDE.md` or `docs/`.
+2
View File
@@ -0,0 +1,2 @@
agent-runner/node_modules
agent-runner/dist
+92 -49
View File
@@ -1,70 +1,113 @@
# 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 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}"
# ---- 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"]
+259
View File
@@ -0,0 +1,259 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "nanoclaw-agent-runner",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
"@modelcontextprotocol/sdk": "^1.12.1",
"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=="],
"@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

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