From 5ca0633c270e182ba4ffe76903f3143d379ca541 Mon Sep 17 00:00:00 2001 From: Michael Bravo Date: Tue, 10 Mar 2026 15:58:24 +0200 Subject: [PATCH 01/54] fix: refresh tasks snapshot immediately after IPC task mutations Previously, current_tasks.json was only written at container-start time, so tasks created (or paused/cancelled/updated) during a session were invisible to list_tasks until the next invocation. Add an onTasksChanged callback to IpcDeps, called after every successful mutation in processTaskIpc (schedule_task, pause_task, resume_task, cancel_task, update_task). index.ts wires it up to write fresh snapshots for all registered groups immediately, keeping no new coupling between ipc.ts and the container layer. Co-Authored-By: Claude Sonnet 4.6 --- src/index.ts | 15 +++++++++++++++ src/ipc-auth.test.ts | 1 + src/ipc.ts | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/src/index.ts b/src/index.ts index c6295c5..bfdcdec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -575,6 +575,21 @@ async function main(): Promise { getAvailableGroups, writeGroupsSnapshot: (gf, im, ag, rj) => writeGroupsSnapshot(gf, im, ag, rj), + onTasksChanged: () => { + const tasks = getAllTasks(); + const taskRows = tasks.map((t) => ({ + id: t.id, + groupFolder: t.group_folder, + prompt: t.prompt, + schedule_type: t.schedule_type, + schedule_value: t.schedule_value, + status: t.status, + next_run: t.next_run, + })); + for (const group of Object.values(registeredGroups)) { + writeTasksSnapshot(group.folder, group.isMain === true, taskRows); + } + }, }); queue.setProcessMessagesFn(processGroupMessages); recoverPendingMessages(); diff --git a/src/ipc-auth.test.ts b/src/ipc-auth.test.ts index 1aa681e..0adf899 100644 --- a/src/ipc-auth.test.ts +++ b/src/ipc-auth.test.ts @@ -62,6 +62,7 @@ beforeEach(() => { syncGroups: async () => {}, getAvailableGroups: () => [], writeGroupsSnapshot: () => {}, + onTasksChanged: () => {}, }; }); diff --git a/src/ipc.ts b/src/ipc.ts index 7a972c0..48efeb5 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -22,6 +22,7 @@ export interface IpcDeps { availableGroups: AvailableGroup[], registeredJids: Set, ) => void; + onTasksChanged: () => void; } let ipcWatcherRunning = false; @@ -270,6 +271,7 @@ export async function processTaskIpc( { taskId, sourceGroup, targetFolder, contextMode }, 'Task created via IPC', ); + deps.onTasksChanged(); } break; @@ -282,6 +284,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup }, 'Task paused via IPC', ); + deps.onTasksChanged(); } else { logger.warn( { taskId: data.taskId, sourceGroup }, @@ -300,6 +303,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup }, 'Task resumed via IPC', ); + deps.onTasksChanged(); } else { logger.warn( { taskId: data.taskId, sourceGroup }, @@ -318,6 +322,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup }, 'Task cancelled via IPC', ); + deps.onTasksChanged(); } else { logger.warn( { taskId: data.taskId, sourceGroup }, @@ -388,6 +393,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup, updates }, 'Task updated via IPC', ); + deps.onTasksChanged(); } break; From 8cbd715ee2fcb7640103379008a97d4f6307da37 Mon Sep 17 00:00:00 2001 From: Akshan Krithick Date: Sat, 14 Mar 2026 21:33:48 -0700 Subject: [PATCH 02/54] add read-only /capabilities and /status skills --- CLAUDE.md | 2 + container/skills/capabilities/SKILL.md | 100 ++++++++++++++++++++++++ container/skills/status/SKILL.md | 104 +++++++++++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 container/skills/capabilities/SKILL.md create mode 100644 container/skills/status/SKILL.md diff --git a/CLAUDE.md b/CLAUDE.md index 318d6dd..b86c0ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,6 +31,8 @@ Single Node.js process with skill-based channel system. Channels (WhatsApp, Tele | `/update-nanoclaw` | Bring upstream NanoClaw updates into a customized install | | `/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 | +| `/capabilities` | Show installed skills, tools, and system info (main channel, read-only) | +| `/status` | Quick health check — session, workspace, tools, tasks (main channel, read-only) | ## Development diff --git a/container/skills/capabilities/SKILL.md b/container/skills/capabilities/SKILL.md new file mode 100644 index 0000000..8e8be14 --- /dev/null +++ b/container/skills/capabilities/SKILL.md @@ -0,0 +1,100 @@ +--- +name: capabilities +description: Show what this NanoClaw instance can do — installed skills, available tools, and system info. Read-only. Use when the user asks what the bot can do, what's installed, or runs /capabilities. +--- + +# /capabilities — System Capabilities Report + +Generate a structured read-only report of what this NanoClaw instance can do. + +**Main-channel check:** Only the main channel has `/workspace/project` mounted. Run: + +```bash +test -d /workspace/project && echo "MAIN" || echo "NOT_MAIN" +``` + +If `NOT_MAIN`, respond with: +> This command is available in your main chat only. Send `/capabilities` there to see what I can do. + +Then stop — do not generate the report. + +## How to gather the information + +Run these commands and compile the results into the report format below. + +### 1. Installed skills + +List skill directories available to you: + +```bash +ls -1 /home/node/.claude/skills/ 2>/dev/null || echo "No skills found" +``` + +Each directory is an installed skill. The directory name is the skill name (e.g., `agent-browser` → `/agent-browser`). + +### 2. Available tools + +Read the allowed tools from your SDK configuration. You always have access to: +- **Core:** Bash, Read, Write, Edit, Glob, Grep +- **Web:** WebSearch, WebFetch +- **Orchestration:** Task, TaskOutput, TaskStop, TeamCreate, TeamDelete, SendMessage +- **Other:** TodoWrite, ToolSearch, Skill, NotebookEdit +- **MCP:** mcp__nanoclaw__* (messaging, tasks, group management) + +### 3. MCP server tools + +The NanoClaw MCP server exposes these tools (via `mcp__nanoclaw__*` prefix): +- `send_message` — send a message to the user/group +- `schedule_task` — schedule a recurring or one-time task +- `list_tasks` — list scheduled tasks +- `pause_task` — pause a scheduled task +- `resume_task` — resume a paused task +- `cancel_task` — cancel and delete a task +- `update_task` — update an existing task +- `register_group` — register a new chat/group (main only) + +### 4. Container skills (Bash tools) + +Check for executable tools in the container: + +```bash +which agent-browser 2>/dev/null && echo "agent-browser: available" || echo "agent-browser: not found" +``` + +### 5. Group info + +```bash +ls /workspace/group/CLAUDE.md 2>/dev/null && echo "Group memory: yes" || echo "Group memory: no" +ls /workspace/extra/ 2>/dev/null && echo "Extra mounts: $(ls /workspace/extra/ 2>/dev/null | wc -l | tr -d ' ')" || echo "Extra mounts: none" +``` + +## Report format + +Present the report as a clean, readable message. Example: + +``` +📋 *NanoClaw Capabilities* + +*Installed Skills:* +• /agent-browser — Browse the web, fill forms, extract data +• /capabilities — This report +(list all found skills) + +*Tools:* +• Core: Bash, Read, Write, Edit, Glob, Grep +• Web: WebSearch, WebFetch +• Orchestration: Task, TeamCreate, SendMessage +• MCP: send_message, schedule_task, list_tasks, pause/resume/cancel/update_task, register_group + +*Container Tools:* +• agent-browser: ✓ + +*System:* +• Group memory: yes/no +• Extra mounts: N directories +• Main channel: yes +``` + +Adapt the output based on what you actually find — don't list things that aren't installed. + +**See also:** `/status` for a quick health check of session, workspace, and tasks. diff --git a/container/skills/status/SKILL.md b/container/skills/status/SKILL.md new file mode 100644 index 0000000..3a99fcc --- /dev/null +++ b/container/skills/status/SKILL.md @@ -0,0 +1,104 @@ +--- +name: status +description: Quick read-only health check — session context, workspace mounts, tool availability, and task snapshot. Use when the user asks for system status or runs /status. +--- + +# /status — System Status Check + +Generate a quick read-only status report of the current agent environment. + +**Main-channel check:** Only the main channel has `/workspace/project` mounted. Run: + +```bash +test -d /workspace/project && echo "MAIN" || echo "NOT_MAIN" +``` + +If `NOT_MAIN`, respond with: +> This command is available in your main chat only. Send `/status` there to check system status. + +Then stop — do not generate the report. + +## How to gather the information + +Run the checks below and compile results into the report format. + +### 1. Session context + +```bash +echo "Timestamp: $(date)" +echo "Working dir: $(pwd)" +echo "Channel: main" +``` + +### 2. Workspace and mount visibility + +```bash +echo "=== Workspace ===" +ls /workspace/ 2>/dev/null +echo "=== Group folder ===" +ls /workspace/group/ 2>/dev/null | head -20 +echo "=== Extra mounts ===" +ls /workspace/extra/ 2>/dev/null || echo "none" +echo "=== IPC ===" +ls /workspace/ipc/ 2>/dev/null +``` + +### 3. Tool availability + +Confirm which tool families are available to you: + +- **Core:** Bash, Read, Write, Edit, Glob, Grep +- **Web:** WebSearch, WebFetch +- **Orchestration:** Task, TaskOutput, TaskStop, TeamCreate, TeamDelete, SendMessage +- **MCP:** mcp__nanoclaw__* (send_message, schedule_task, list_tasks, pause_task, resume_task, cancel_task, update_task, register_group) + +### 4. Container utilities + +```bash +which agent-browser 2>/dev/null && echo "agent-browser: available" || echo "agent-browser: not installed" +node --version 2>/dev/null +claude --version 2>/dev/null +``` + +### 5. Task snapshot + +Use the MCP tool to list tasks: + +``` +Call mcp__nanoclaw__list_tasks to get scheduled tasks. +``` + +If no tasks exist, report "No scheduled tasks." + +## Report format + +Present as a clean, readable message: + +``` +🔍 *NanoClaw Status* + +*Session:* +• Channel: main +• Time: 2026-03-14 09:30 UTC +• Working dir: /workspace/group + +*Workspace:* +• Group folder: ✓ (N files) +• Extra mounts: none / N directories +• IPC: ✓ (messages, tasks, input) + +*Tools:* +• Core: ✓ Web: ✓ Orchestration: ✓ MCP: ✓ + +*Container:* +• agent-browser: ✓ / not installed +• Node: vXX.X.X +• Claude Code: vX.X.X + +*Scheduled Tasks:* +• N active tasks / No scheduled tasks +``` + +Adapt based on what you actually find. Keep it concise — this is a quick health check, not a deep diagnostic. + +**See also:** `/capabilities` for a full list of installed skills and tools. From de62ef6b3f612043e614d85103c37a9fb553cd38 Mon Sep 17 00:00:00 2001 From: Akshan Krithick Date: Sat, 14 Mar 2026 21:41:56 -0700 Subject: [PATCH 03/54] format remote-control files with Prettier --- src/remote-control.test.ts | 43 +++++++++++++++++++++++++------------- src/remote-control.ts | 8 ++++--- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/remote-control.test.ts b/src/remote-control.test.ts index 4b5ab2f..1fa434b 100644 --- a/src/remote-control.test.ts +++ b/src/remote-control.test.ts @@ -45,14 +45,20 @@ describe('remote-control', () => { stdoutFileContent = ''; // Default fs mocks - mkdirSyncSpy = vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined as any); - writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + mkdirSyncSpy = vi + .spyOn(fs, 'mkdirSync') + .mockImplementation(() => undefined as any); + writeFileSyncSpy = vi + .spyOn(fs, 'writeFileSync') + .mockImplementation(() => {}); unlinkSyncSpy = vi.spyOn(fs, 'unlinkSync').mockImplementation(() => {}); openSyncSpy = vi.spyOn(fs, 'openSync').mockReturnValue(42 as any); closeSyncSpy = vi.spyOn(fs, 'closeSync').mockImplementation(() => {}); // readFileSync: return stdoutFileContent for the stdout file, state file, etc. - readFileSyncSpy = vi.spyOn(fs, 'readFileSync').mockImplementation(((p: string) => { + readFileSyncSpy = vi.spyOn(fs, 'readFileSync').mockImplementation((( + p: string, + ) => { if (p.endsWith('remote-control.stdout')) return stdoutFileContent; if (p.endsWith('remote-control.json')) { throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); @@ -74,7 +80,8 @@ describe('remote-control', () => { spawnMock.mockReturnValue(proc); // Simulate URL appearing in stdout file on first poll - stdoutFileContent = 'Session URL: https://claude.ai/code?bridge=env_abc123\n'; + stdoutFileContent = + 'Session URL: https://claude.ai/code?bridge=env_abc123\n'; vi.spyOn(process, 'kill').mockImplementation((() => true) as any); const result = await startRemoteControl('user1', 'tg:123', '/project'); @@ -157,7 +164,9 @@ describe('remote-control', () => { spawnMock.mockReturnValueOnce(proc1).mockReturnValueOnce(proc2); // First start: process alive, URL found - const killSpy = vi.spyOn(process, 'kill').mockImplementation((() => true) as any); + const killSpy = vi + .spyOn(process, 'kill') + .mockImplementation((() => true) as any); stdoutFileContent = 'https://claude.ai/code?bridge=env_first\n'; await startRemoteControl('user1', 'tg:123', '/project'); @@ -239,7 +248,9 @@ describe('remote-control', () => { const proc = createMockProcess(55555); spawnMock.mockReturnValue(proc); stdoutFileContent = 'https://claude.ai/code?bridge=env_stop\n'; - const killSpy = vi.spyOn(process, 'kill').mockImplementation((() => true) as any); + const killSpy = vi + .spyOn(process, 'kill') + .mockImplementation((() => true) as any); await startRemoteControl('user1', 'tg:123', '/project'); @@ -337,7 +348,9 @@ describe('remote-control', () => { if (p.endsWith('remote-control.json')) return JSON.stringify(session); return ''; }) as any); - const killSpy = vi.spyOn(process, 'kill').mockImplementation((() => true) as any); + const killSpy = vi + .spyOn(process, 'kill') + .mockImplementation((() => true) as any); restoreRemoteControl(); expect(getActiveSession()).not.toBeNull(); @@ -365,13 +378,15 @@ describe('remote-control', () => { restoreRemoteControl(); - return startRemoteControl('user2', 'tg:456', '/project').then((result) => { - expect(result).toEqual({ - ok: true, - url: 'https://claude.ai/code?bridge=env_restored', - }); - expect(spawnMock).not.toHaveBeenCalled(); - }); + return startRemoteControl('user2', 'tg:456', '/project').then( + (result) => { + expect(result).toEqual({ + ok: true, + url: 'https://claude.ai/code?bridge=env_restored', + }); + expect(spawnMock).not.toHaveBeenCalled(); + }, + ); }); }); }); diff --git a/src/remote-control.ts b/src/remote-control.ts index df8f646..015aa7f 100644 --- a/src/remote-control.ts +++ b/src/remote-control.ts @@ -196,9 +196,11 @@ export async function startRemoteControl( }); } -export function stopRemoteControl(): { - ok: true; -} | { ok: false; error: string } { +export function stopRemoteControl(): + | { + ok: true; + } + | { ok: false; error: string } { if (!activeSession) { return { ok: false, error: 'No active Remote Control session' }; } From 9200612dd1619aed22d4a1608230dd576cf0eba0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Mar 2026 09:52:20 +0000 Subject: [PATCH 04/54] chore: bump version to 1.2.16 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ee97d7c..ad5f762 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.15", + "version": "1.2.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.15", + "version": "1.2.16", "dependencies": { "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", diff --git a/package.json b/package.json index 97c3a6f..44312f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.15", + "version": "1.2.16", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From e7d0ffb208aef464ec5b30e9e3bfc0c7c87f4bb1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Mar 2026 09:52:28 +0000 Subject: [PATCH 05/54] =?UTF-8?q?docs:=20update=20token=20count=20to=2040.?= =?UTF-8?q?6k=20tokens=20=C2=B7=2020%=20of=20context=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- repo-tokens/badge.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/repo-tokens/badge.svg b/repo-tokens/badge.svg index 480cd9f..ce35723 100644 --- a/repo-tokens/badge.svg +++ b/repo-tokens/badge.svg @@ -1,5 +1,5 @@ - - 40.5k tokens, 20% of context window + + 40.6k tokens, 20% of context window @@ -15,8 +15,8 @@ tokens - - 40.5k + + 40.6k From 96852f686e08be28a3ee3fd2da4a74934b850b28 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Wed, 18 Mar 2026 12:08:22 +0200 Subject: [PATCH 06/54] Apply suggestion from @gavrielc --- CLAUDE.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b86c0ed..318d6dd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,8 +31,6 @@ Single Node.js process with skill-based channel system. Channels (WhatsApp, Tele | `/update-nanoclaw` | Bring upstream NanoClaw updates into a customized install | | `/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 | -| `/capabilities` | Show installed skills, tools, and system info (main channel, read-only) | -| `/status` | Quick health check — session, workspace, tools, tasks (main channel, read-only) | ## Development From c71c7b7e830d477e239bb566b3a7aabd49a825f2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Mar 2026 10:10:45 +0000 Subject: [PATCH 07/54] chore: bump version to 1.2.17 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad5f762..8496c15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.16", + "version": "1.2.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.16", + "version": "1.2.17", "dependencies": { "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", diff --git a/package.json b/package.json index 44312f4..f51ca85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.16", + "version": "1.2.17", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From c75de24029a79f9cf1a2d1218ac2ea5861256746 Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Wed, 18 Mar 2026 19:43:46 +0900 Subject: [PATCH 08/54] docs: add Japanese README --- README.md | 1 + README_ja.md | 232 +++++++++++++++++++++++++++++++++++++++++++++++++++ README_zh.md | 1 + 3 files changed, 234 insertions(+) create mode 100644 README_ja.md diff --git a/README.md b/README.md index 56d9331..d76d33b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@

nanoclaw.dev  •   中文  •   + 日本語  •   Discord  •   34.9k tokens, 17% of context window

diff --git a/README_ja.md b/README_ja.md new file mode 100644 index 0000000..5c3f648 --- /dev/null +++ b/README_ja.md @@ -0,0 +1,232 @@ +

+ NanoClaw +

+ +

+ エージェントを専用コンテナで安全に実行するAIアシスタント。軽量で、理解しやすく、あなたのニーズに完全にカスタマイズできるように設計されています。 +

+ +

+ nanoclaw.dev  •   + English  •   + 中文  •   + Discord  •   + 34.9k tokens, 17% of context window +

+ +--- + +

🐳 Dockerサンドボックスで動作

+

各エージェントはマイクロVM内の独立したコンテナで実行されます。
ハイパーバイザーレベルの分離。ミリ秒で起動。複雑なセットアップ不要。

+ +**macOS (Apple Silicon)** +```bash +curl -fsSL https://nanoclaw.dev/install-docker-sandboxes.sh | bash +``` + +**Windows (WSL)** +```bash +curl -fsSL https://nanoclaw.dev/install-docker-sandboxes-windows.sh | bash +``` + +> 現在、macOS(Apple Silicon)とWindows(x86)に対応しています。Linux対応は近日公開予定。 + +

発表記事を読む →  ·  手動セットアップガイド →

+ +--- + +## NanoClawを作った理由 + +[OpenClaw](https://github.com/openclaw/openclaw)は素晴らしいプロジェクトですが、理解しきれない複雑なソフトウェアに自分の生活へのフルアクセスを与えたまま安心して眠れるとは思えませんでした。OpenClawは約50万行のコード、53の設定ファイル、70以上の依存関係を持っています。セキュリティはアプリケーションレベル(許可リスト、ペアリングコード)であり、真のOS レベルの分離ではありません。すべてが共有メモリを持つ1つのNodeプロセスで動作します。 + +NanoClawは同じコア機能を提供しますが、理解できる規模のコードベースで実現しています:1つのプロセスと少数のファイル。Claudeエージェントは単なるパーミッションチェックの背後ではなく、ファイルシステム分離された独自のLinuxコンテナで実行されます。 + +## クイックスタート + +```bash +gh repo fork qwibitai/nanoclaw --clone +cd nanoclaw +claude +``` + +
+GitHub CLIなしの場合 + +1. GitHub上で[qwibitai/nanoclaw](https://github.com/qwibitai/nanoclaw)をフォーク(Forkボタンをクリック) +2. `git clone https://github.com/<あなたのユーザー名>/nanoclaw.git` +3. `cd nanoclaw` +4. `claude` + +
+ +その後、`/setup`を実行します。Claude Codeがすべてを処理します:依存関係、認証、コンテナセットアップ、サービス設定。 + +> **注意:** `/`で始まるコマンド(`/setup`、`/add-whatsapp`など)は[Claude Codeスキル](https://code.claude.com/docs/en/skills)です。通常のターミナルではなく、`claude` CLIプロンプト内で入力してください。Claude Codeをインストールしていない場合は、[claude.com/product/claude-code](https://claude.com/product/claude-code)から入手してください。 + +## 設計思想 + +**理解できる規模。** 1つのプロセス、少数のソースファイル、マイクロサービスなし。NanoClawのコードベース全体を理解したい場合は、Claude Codeに説明を求めるだけです。 + +**分離によるセキュリティ。** エージェントはLinuxコンテナ(macOSではApple Container、またはDocker)で実行され、明示的にマウントされたものだけが見えます。コマンドはホストではなくコンテナ内で実行されるため、Bashアクセスは安全です。 + +**個人ユーザー向け。** NanoClawはモノリシックなフレームワークではなく、各ユーザーのニーズに正確にフィットするソフトウェアです。肥大化するのではなく、オーダーメイドになるよう設計されています。自分のフォークを作成し、Claude Codeにニーズに合わせて変更させます。 + +**カスタマイズ=コード変更。** 設定ファイルの肥大化なし。動作を変えたい?コードを変更するだけ。コードベースは変更しても安全な規模です。 + +**AIネイティブ。** +- インストールウィザードなし — Claude Codeがセットアップを案内。 +- モニタリングダッシュボードなし — Claudeに状況を聞くだけ。 +- デバッグツールなし — 問題を説明すればClaudeが修正。 + +**機能追加ではなくスキル。** コードベースに機能(例:Telegram対応)を追加する代わりに、コントリビューターは`/add-telegram`のような[Claude Codeスキル](https://code.claude.com/docs/en/skills)を提出し、あなたのフォークを変換します。あなたが必要なものだけを正確に実行するクリーンなコードが手に入ります。 + +**最高のハーネス、最高のモデル。** NanoClawはClaude Agent SDK上で動作します。つまり、Claude Codeを直接実行しているということです。Claude Codeは高い能力を持ち、そのコーディングと問題解決能力によってNanoClawを変更・拡張し、各ユーザーに合わせてカスタマイズできます。 + +## サポート機能 + +- **マルチチャネルメッセージング** - WhatsApp、Telegram、Discord、Slack、Gmailからアシスタントと会話。`/add-whatsapp`や`/add-telegram`などのスキルでチャネルを追加。1つでも複数でも同時に実行可能。 +- **グループごとの分離コンテキスト** - 各グループは独自の`CLAUDE.md`メモリ、分離されたファイルシステムを持ち、そのファイルシステムのみがマウントされた専用コンテナサンドボックスで実行。 +- **メインチャネル** - 管理制御用のプライベートチャネル(セルフチャット)。各グループは完全に分離。 +- **スケジュールタスク** - Claudeを実行し、メッセージを返せる定期ジョブ。 +- **Webアクセス** - Webからのコンテンツ検索・取得。 +- **コンテナ分離** - エージェントは[Dockerサンドボックス](https://nanoclaw.dev/blog/nanoclaw-docker-sandboxes)(マイクロVM分離)、Apple Container(macOS)、またはDocker(macOS/Linux)でサンドボックス化。 +- **エージェントスウォーム** - 複雑なタスクで協力する専門エージェントチームを起動。 +- **オプション連携** - Gmail(`/add-gmail`)などをスキルで追加。 + +## 使い方 + +トリガーワード(デフォルト:`@Andy`)でアシスタントに話しかけます: + +``` +@Andy 毎朝9時に営業パイプラインの概要を送って(Obsidian vaultフォルダにアクセス可能) +@Andy 毎週金曜に過去1週間のgit履歴をレビューして、差異があればREADMEを更新して +@Andy 毎週月曜の朝8時に、Hacker NewsとTechCrunchからAI関連のニュースをまとめてブリーフィングを送って +``` + +メインチャネル(セルフチャット)から、グループやタスクを管理できます: +``` +@Andy 全グループのスケジュールタスクを一覧表示して +@Andy 月曜のブリーフィングタスクを一時停止して +@Andy Family Chatグループに参加して +``` + +## カスタマイズ + +NanoClawは設定ファイルを使いません。変更するには、Claude Codeに伝えるだけです: + +- 「トリガーワードを@Bobに変更して」 +- 「今後はレスポンスをもっと短く直接的にして」 +- 「おはようと言ったらカスタム挨拶を追加して」 +- 「会話の要約を毎週保存して」 + +または`/customize`を実行してガイド付きの変更を行えます。 + +コードベースは十分に小さいため、Claudeが安全に変更できます。 + +## コントリビューション + +**機能を追加するのではなく、スキルを追加してください。** + +Telegram対応を追加したい場合、コアコードベースにTelegramを追加するPRを作成しないでください。代わりに、NanoClawをフォークし、ブランチでコード変更を行い、PRを開いてください。あなたのPRから`skill/telegram`ブランチを作成し、他のユーザーが自分のフォークにマージできるようにします。 + +ユーザーは自分のフォークで`/add-telegram`を実行するだけで、あらゆるユースケースに対応しようとする肥大化したシステムではなく、必要なものだけを正確に実行するクリーンなコードが手に入ります。 + +### RFS(スキル募集) + +私たちが求めているスキル: + +**コミュニケーションチャネル** +- `/add-signal` - Signalをチャネルとして追加 + +**セッション管理** +- `/clear` - 会話をコンパクト化する`/clear`コマンドの追加(同一セッション内で重要な情報を保持しながらコンテキストを要約)。Claude Agent SDKを通じてプログラム的にコンパクト化をトリガーする方法の解明が必要。 + +## 必要条件 + +- macOSまたはLinux +- Node.js 20以上 +- [Claude Code](https://claude.ai/download) +- [Apple Container](https://github.com/apple/container)(macOS)または[Docker](https://docker.com/products/docker-desktop)(macOS/Linux) + +## アーキテクチャ + +``` +チャネル --> SQLite --> ポーリングループ --> コンテナ(Claude Agent SDK) --> レスポンス +``` + +単一のNode.jsプロセス。チャネルはスキルで追加され、起動時に自己登録します — オーケストレーターは認証情報が存在するチャネルを接続します。エージェントはファイルシステム分離された独立したLinuxコンテナで実行されます。マウントされたディレクトリのみアクセス可能。グループごとのメッセージキューと同時実行制御。ファイルシステム経由のIPC。 + +詳細なアーキテクチャについては、[docs/SPEC.md](docs/SPEC.md)を参照してください。 + +主要ファイル: +- `src/index.ts` - オーケストレーター:状態、メッセージループ、エージェント呼び出し +- `src/channels/registry.ts` - チャネルレジストリ(起動時の自己登録) +- `src/ipc.ts` - IPCウォッチャーとタスク処理 +- `src/router.ts` - メッセージフォーマットとアウトバウンドルーティング +- `src/group-queue.ts` - グローバル同時実行制限付きのグループごとのキュー +- `src/container-runner.ts` - ストリーミングエージェントコンテナの起動 +- `src/task-scheduler.ts` - スケジュールタスクの実行 +- `src/db.ts` - SQLite操作(メッセージ、グループ、セッション、状態) +- `groups/*/CLAUDE.md` - グループごとのメモリ + +## FAQ + +**なぜDockerなのか?** + +Dockerはクロスプラットフォーム対応(macOS、Linux、さらにWSL2経由のWindows)と成熟したエコシステムを提供します。macOSでは、`/convert-to-apple-container`でオプションとしてApple Containerに切り替え、より軽量なネイティブランタイムを使用できます。 + +**Linuxで実行できますか?** + +はい。DockerがデフォルトのランタイムでmacOSとLinuxの両方で動作します。`/setup`を実行するだけです。 + +**セキュリティは大丈夫ですか?** + +エージェントはアプリケーションレベルのパーミッションチェックの背後ではなく、コンテナで実行されます。明示的にマウントされたディレクトリのみアクセスできます。実行するものをレビューすべきですが、コードベースは十分に小さいため実際にレビュー可能です。完全なセキュリティモデルについては[docs/SECURITY.md](docs/SECURITY.md)を参照してください。 + +**なぜ設定ファイルがないのか?** + +設定の肥大化を避けたいからです。すべてのユーザーがNanoClawをカスタマイズし、汎用的なシステムを設定するのではなく、コードが必要なことを正確に実行するようにすべきです。設定ファイルが欲しい場合は、Claudeに追加するよう伝えることができます。 + +**サードパーティやオープンソースモデルを使えますか?** + +はい。NanoClawはClaude API互換のモデルエンドポイントに対応しています。`.env`ファイルで以下の環境変数を設定してください: + +```bash +ANTHROPIC_BASE_URL=https://your-api-endpoint.com +ANTHROPIC_AUTH_TOKEN=your-token-here +``` + +以下が使用可能です: +- [Ollama](https://ollama.ai)とAPIプロキシ経由のローカルモデル +- [Together AI](https://together.ai)、[Fireworks](https://fireworks.ai)等でホストされたオープンソースモデル +- Anthropic互換APIのカスタムモデルデプロイメント + +注意:最高の互換性のため、モデルはAnthropic APIフォーマットに対応している必要があります。 + +**問題のデバッグ方法は?** + +Claude Codeに聞いてください。「スケジューラーが動いていないのはなぜ?」「最近のログには何がある?」「このメッセージに返信がなかったのはなぜ?」これがNanoClawの基盤となるAIネイティブなアプローチです。 + +**セットアップがうまくいかない場合は?** + +問題がある場合、セットアップ中にClaudeが動的に修正を試みます。それでもうまくいかない場合は、`claude`を実行してから`/debug`を実行してください。Claudeが他のユーザーにも影響する可能性のある問題を見つけた場合は、セットアップのSKILL.mdを修正するPRを開いてください。 + +**どのような変更がコードベースに受け入れられますか?** + +セキュリティ修正、バグ修正、明確な改善のみが基本設定に受け入れられます。それだけです。 + +それ以外のすべて(新機能、OS互換性、ハードウェアサポート、機能拡張)はスキルとしてコントリビューションすべきです。 + +これにより、基本システムを最小限に保ち、すべてのユーザーが不要な機能を継承することなく、自分のインストールをカスタマイズできます。 + +## コミュニティ + +質問やアイデアは?[Discordに参加](https://discord.gg/VDdww8qS42)してください。 + +## 変更履歴 + +破壊的変更と移行ノートについては[CHANGELOG.md](CHANGELOG.md)を参照してください。 + +## ライセンス + +MIT diff --git a/README_zh.md b/README_zh.md index a05265a..714bd87 100644 --- a/README_zh.md +++ b/README_zh.md @@ -9,6 +9,7 @@

nanoclaw.dev  •   English  •   + 日本語  •   Discord  •   34.9k tokens, 17% of context window

From cf899049f770f7daff9c36139702fa3c72ffb9e8 Mon Sep 17 00:00:00 2001 From: moktamd Date: Tue, 17 Mar 2026 15:07:09 +0000 Subject: [PATCH 09/54] security: stop logging user prompt content on container errors Container error logs wrote the full ContainerInput (including user prompt) to disk on every non-zero exit. The structured log stream also included the first 200 chars of agent output. - container-runner: only include full input at verbose level; error path now logs prompt length and session ID instead - index: log output length instead of content snippet Fixes #1150 --- src/container-runner.ts | 20 +++++++++++++++++--- src/index.ts | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/container-runner.ts b/src/container-runner.ts index be6f356..59bccd8 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -503,10 +503,24 @@ export async function runContainerAgent( const isError = code !== 0; if (isVerbose || isError) { + // On error, log input metadata only — not the full prompt. + // Full input is only included at verbose level to avoid + // persisting user conversation content on every non-zero exit. + if (isVerbose) { + logLines.push( + `=== Input ===`, + JSON.stringify(input, null, 2), + ``, + ); + } else { + logLines.push( + `=== Input Summary ===`, + `Prompt length: ${input.prompt.length} chars`, + `Session ID: ${input.sessionId || 'new'}`, + ``, + ); + } logLines.push( - `=== Input ===`, - JSON.stringify(input, null, 2), - ``, `=== Container Args ===`, containerArgs.join(' '), ``, diff --git a/src/index.ts b/src/index.ts index 98682fb..42329a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -221,7 +221,7 @@ async function processGroupMessages(chatJid: string): Promise { : JSON.stringify(result.result); // Strip ... blocks — agent uses these for internal reasoning const text = raw.replace(/[\s\S]*?<\/internal>/g, '').trim(); - logger.info({ group: group.name }, `Agent output: ${raw.slice(0, 200)}`); + logger.info({ group: group.name }, `Agent output: ${raw.length} chars`); if (text) { await channel.sendMessage(chatJid, text); outputSentToUser = true; From c78042e90e44056ab2c958b324f50d5f7703ce11 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 19 Mar 2026 19:03:27 +0000 Subject: [PATCH 10/54] chore: bump version to 1.2.18 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8496c15..916adb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.17", + "version": "1.2.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.17", + "version": "1.2.18", "dependencies": { "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", diff --git a/package.json b/package.json index f51ca85..7a06e75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.17", + "version": "1.2.18", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From 7a8c24b0927d76d3d8439263ce50d3e983e20ab8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 19 Mar 2026 19:03:33 +0000 Subject: [PATCH 11/54] =?UTF-8?q?docs:=20update=20token=20count=20to=2040.?= =?UTF-8?q?7k=20tokens=20=C2=B7=2020%=20of=20context=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- repo-tokens/badge.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/repo-tokens/badge.svg b/repo-tokens/badge.svg index ce35723..b268ecc 100644 --- a/repo-tokens/badge.svg +++ b/repo-tokens/badge.svg @@ -1,5 +1,5 @@ - - 40.6k tokens, 20% of context window + + 40.7k tokens, 20% of context window @@ -15,8 +15,8 @@ tokens - - 40.6k + + 40.7k From cf3d9dcbd52e2f90bb01b9fb5d52523029b2794d Mon Sep 17 00:00:00 2001 From: sasaki takeru Date: Wed, 18 Mar 2026 08:23:51 +0900 Subject: [PATCH 12/54] fix: reduce docker stop timeout for faster restarts Pass -t 1 to docker stop, reducing SIGTERM-to-SIGKILL grace period from 10s to 1s. NanoClaw containers are stateless (--rm, mounted filesystems) so they don't need a long grace period. Makes restarts ~10x faster. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/container-runtime.test.ts | 6 +++--- src/container-runtime.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/container-runtime.test.ts b/src/container-runtime.test.ts index 08ffd59..d111bf6 100644 --- a/src/container-runtime.test.ts +++ b/src/container-runtime.test.ts @@ -41,7 +41,7 @@ describe('readonlyMountArgs', () => { describe('stopContainer', () => { it('returns stop command using CONTAINER_RUNTIME_BIN', () => { expect(stopContainer('nanoclaw-test-123')).toBe( - `${CONTAINER_RUNTIME_BIN} stop nanoclaw-test-123`, + `${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-test-123`, ); }); }); @@ -93,12 +93,12 @@ describe('cleanupOrphans', () => { expect(mockExecSync).toHaveBeenCalledTimes(3); expect(mockExecSync).toHaveBeenNthCalledWith( 2, - `${CONTAINER_RUNTIME_BIN} stop nanoclaw-group1-111`, + `${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-group1-111`, { stdio: 'pipe' }, ); expect(mockExecSync).toHaveBeenNthCalledWith( 3, - `${CONTAINER_RUNTIME_BIN} stop nanoclaw-group2-222`, + `${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-group2-222`, { stdio: 'pipe' }, ); expect(logger.info).toHaveBeenCalledWith( diff --git a/src/container-runtime.ts b/src/container-runtime.ts index c4acdba..5a4f91e 100644 --- a/src/container-runtime.ts +++ b/src/container-runtime.ts @@ -59,7 +59,7 @@ export function readonlyMountArgs( /** Returns the shell command to stop a container by name. */ export function stopContainer(name: string): string { - return `${CONTAINER_RUNTIME_BIN} stop ${name}`; + return `${CONTAINER_RUNTIME_BIN} stop -t 1 ${name}`; } /** Ensure the container runtime is running, starting it if needed. */ From 91f17a11b265b94c9fefe83209887a5915c24536 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 19 Mar 2026 19:05:42 +0000 Subject: [PATCH 13/54] chore: bump version to 1.2.19 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 916adb2..261f226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.18", + "version": "1.2.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.18", + "version": "1.2.19", "dependencies": { "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", diff --git a/package.json b/package.json index 7a06e75..beacf9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.18", + "version": "1.2.19", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From f04a8955aa0b90c70a0408661c0a387c76eb07e9 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Thu, 19 Mar 2026 15:05:24 +0000 Subject: [PATCH 14/54] feat: add opt-in diagnostics via PostHog Per-event consent diagnostics that sends anonymous install/update/skill data to PostHog. Conflict filenames are gated against upstream. Supports --dry-run to show exact payload before sending, and "never ask again" opt-out via state.yaml. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/_shared/diagnostics.md | 99 ++++++ .claude/skills/add-compact/SKILL.md | 5 + .claude/skills/add-discord/SKILL.md | 5 + .claude/skills/add-gmail/SKILL.md | 5 + .claude/skills/add-image-vision/SKILL.md | 5 + .claude/skills/add-ollama-tool/SKILL.md | 5 + .claude/skills/add-parallel/SKILL.md | 5 + .claude/skills/add-pdf-reader/SKILL.md | 5 + .claude/skills/add-reactions/SKILL.md | 5 + .claude/skills/add-slack/SKILL.md | 5 + .claude/skills/add-telegram-swarm/SKILL.md | 5 + .claude/skills/add-telegram/SKILL.md | 5 + .../skills/add-voice-transcription/SKILL.md | 5 + .claude/skills/add-whatsapp/SKILL.md | 5 + .../convert-to-apple-container/SKILL.md | 5 + .claude/skills/customize/SKILL.md | 5 + .claude/skills/debug/SKILL.md | 5 + .claude/skills/get-qodo-rules/SKILL.md | 5 + .claude/skills/qodo-pr-resolver/SKILL.md | 5 + .claude/skills/setup/SKILL.md | 5 + .claude/skills/update-nanoclaw/SKILL.md | 5 + .claude/skills/update-skills/SKILL.md | 5 + .claude/skills/use-local-whisper/SKILL.md | 5 + .claude/skills/x-integration/SKILL.md | 6 +- scripts/send-diagnostics.ts | 310 ++++++++++++++++++ 25 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/_shared/diagnostics.md create mode 100644 scripts/send-diagnostics.ts diff --git a/.claude/skills/_shared/diagnostics.md b/.claude/skills/_shared/diagnostics.md new file mode 100644 index 0000000..c509a70 --- /dev/null +++ b/.claude/skills/_shared/diagnostics.md @@ -0,0 +1,99 @@ +# Diagnostics (end-of-skill instructions) + +Follow these steps **after all other skill steps are complete**. + +## 1. Skip check + +If this skill was invoked from within another skill (e.g., `/add-whatsapp` called during `/setup`), **skip diagnostics entirely** — the parent skill handles it. Return without doing anything below. + +## 2. Determine event type + +- `/setup` → `setup_complete` +- `/update-nanoclaw` → `update_complete` +- Any other skill → `skill_applied` + +## 3. Build event-specific data + +Estimate `error_count` from the conversation (how many errors/retries occurred during the run). + +**For `setup_complete`:** +```json +{ + "channels_selected": ["whatsapp", "telegram"], + "error_count": 0, + "failed_step": null, + "exit_code": null +} +``` +- `channels_selected`: which channels the user chose to install +- `failed_step`: if setup failed, which step (environment / container / groups / register / mounts / service / verify) +- `exit_code`: if a step failed, its exit code + +**For `skill_applied`:** +```json +{ + "skill_name": "add-telegram", + "is_upstream_skill": true, + "conflict_files": ["package.json", "src/index.ts"], + "error_count": 0 +} +``` +- `skill_name`: the skill that was run. Use the upstream skill name if it's an upstream skill, otherwise use `"custom"` +- `is_upstream_skill`: true if this is a known upstream skill +- `conflict_files`: filenames that had merge conflicts (the script will gate these against upstream — pass them all, the script filters) + +**For `update_complete`:** +```json +{ + "version_age_days": 45, + "update_method": "merge", + "conflict_files": ["package.json"], + "breaking_changes_found": false, + "breaking_changes_skills_run": [], + "error_count": 0 +} +``` +- `version_age_days`: estimate from the backup tag or commit date how many days old the previous version was +- `update_method`: "merge" or "rebase" +- `breaking_changes_found`: whether breaking changes were detected during the update +- `breaking_changes_skills_run`: which skills had to be re-run to fix breaking changes + +## 4. Dry run + +Run with `--dry-run` to get the full payload: + +```bash +npx tsx scripts/send-diagnostics.ts --event --success --data '' --dry-run +``` + +Use `--failure` instead of `--success` if the skill failed. + +If the command produces no output, the user has opted out permanently — skip the rest. + +## 5. Show the user and ask + +Show the JSON output and ask: + +> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" +> +> (show the JSON) +> +> **Yes** / **No** / **Never ask again** + +Use AskUserQuestion. + +## 6. Handle response + +- **Yes**: Run the same command without `--dry-run`: + ```bash + npx tsx scripts/send-diagnostics.ts --event --success --data '' + ``` + Confirm: "Diagnostics sent." + +- **No**: Do nothing. User will be asked again next time. + +- **Never ask again**: Run: + ```bash + npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" + ``` + Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/add-compact/SKILL.md b/.claude/skills/add-compact/SKILL.md index 0c46165..fe7ca8a 100644 --- a/.claude/skills/add-compact/SKILL.md +++ b/.claude/skills/add-compact/SKILL.md @@ -133,3 +133,8 @@ npm test - **"Session commands require admin access"**: Only the device owner (`is_from_me`) or main-group senders can use `/compact`. Other users are denied. - **No compact_boundary in logs**: The SDK may not emit this event in all versions. Check the agent-runner logs for the warning message. Compaction may still have succeeded. - **Pre-compact failure**: If messages before `/compact` fail to process, the error message says "Failed to process messages before /compact." The cursor advances past sent output to prevent duplicates; `/compact` remains pending for the next attempt. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-discord/SKILL.md b/.claude/skills/add-discord/SKILL.md index e46bd3e..f4e98aa 100644 --- a/.claude/skills/add-discord/SKILL.md +++ b/.claude/skills/add-discord/SKILL.md @@ -201,3 +201,8 @@ The Discord bot supports: - @mention translation (Discord `<@botId>` → NanoClaw trigger format) - Message splitting for responses over 2000 characters - Typing indicators while the agent processes + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-gmail/SKILL.md b/.claude/skills/add-gmail/SKILL.md index 781a0eb..b51a098 100644 --- a/.claude/skills/add-gmail/SKILL.md +++ b/.claude/skills/add-gmail/SKILL.md @@ -218,3 +218,8 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp 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) + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-image-vision/SKILL.md b/.claude/skills/add-image-vision/SKILL.md index 072bf7b..d42e394 100644 --- a/.claude/skills/add-image-vision/SKILL.md +++ b/.claude/skills/add-image-vision/SKILL.md @@ -92,3 +92,8 @@ All tests must pass and build must be clean before proceeding. - **"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. - **Agent doesn't mention image content**: Check container logs for "Loaded image" messages. If missing, ensure agent-runner source was synced to group caches. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-ollama-tool/SKILL.md b/.claude/skills/add-ollama-tool/SKILL.md index a347b49..a28b8ea 100644 --- a/.claude/skills/add-ollama-tool/SKILL.md +++ b/.claude/skills/add-ollama-tool/SKILL.md @@ -151,3 +151,8 @@ The agent is trying to run `ollama` CLI inside the container instead of using th ### Agent doesn't use Ollama tools The agent may not know about the tools. Try being explicit: "use the ollama_generate tool with gemma3:1b to answer: ..." + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-parallel/SKILL.md b/.claude/skills/add-parallel/SKILL.md index f4c1982..12eb58c 100644 --- a/.claude/skills/add-parallel/SKILL.md +++ b/.claude/skills/add-parallel/SKILL.md @@ -288,3 +288,8 @@ To remove Parallel AI integration: 3. Remove Web Research Tools section from groups/main/CLAUDE.md 4. Rebuild: `./container/build.sh && npm run build` 5. Restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-pdf-reader/SKILL.md b/.claude/skills/add-pdf-reader/SKILL.md index a01e530..960d7fb 100644 --- a/.claude/skills/add-pdf-reader/SKILL.md +++ b/.claude/skills/add-pdf-reader/SKILL.md @@ -102,3 +102,8 @@ The PDF may be scanned (image-based). pdftotext only handles text-based PDFs. Co ### WhatsApp PDF not detected Verify the message has `documentMessage` with `mimetype: application/pdf`. Some file-sharing apps send PDFs as generic files without the correct mimetype. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-reactions/SKILL.md b/.claude/skills/add-reactions/SKILL.md index de86768..9eacebd 100644 --- a/.claude/skills/add-reactions/SKILL.md +++ b/.claude/skills/add-reactions/SKILL.md @@ -115,3 +115,8 @@ Ask the agent to react to a message via the `react_to_message` MCP tool. Check y - Check IPC logs for `Unauthorized IPC reaction attempt blocked` — the agent can only react in its own group's chat - Verify WhatsApp is connected: check logs for connection status + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-slack/SKILL.md b/.claude/skills/add-slack/SKILL.md index 4c86e19..32a2cf0 100644 --- a/.claude/skills/add-slack/SKILL.md +++ b/.claude/skills/add-slack/SKILL.md @@ -205,3 +205,8 @@ The Slack channel supports: - **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. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-telegram-swarm/SKILL.md b/.claude/skills/add-telegram-swarm/SKILL.md index ac4922c..b6e5923 100644 --- a/.claude/skills/add-telegram-swarm/SKILL.md +++ b/.claude/skills/add-telegram-swarm/SKILL.md @@ -382,3 +382,8 @@ To remove Agent Swarm support while keeping basic Telegram: 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) + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-telegram/SKILL.md b/.claude/skills/add-telegram/SKILL.md index 10f25ab..86a137f 100644 --- a/.claude/skills/add-telegram/SKILL.md +++ b/.claude/skills/add-telegram/SKILL.md @@ -220,3 +220,8 @@ To remove Telegram integration: 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) + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-voice-transcription/SKILL.md b/.claude/skills/add-voice-transcription/SKILL.md index 8ccec32..d9f44b6 100644 --- a/.claude/skills/add-voice-transcription/SKILL.md +++ b/.claude/skills/add-voice-transcription/SKILL.md @@ -146,3 +146,8 @@ Check logs for the specific error. Common causes: ### Agent doesn't respond to voice notes Verify the chat is registered and the agent is running. Voice transcription only runs for registered groups. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-whatsapp/SKILL.md b/.claude/skills/add-whatsapp/SKILL.md index 0774799..c22a835 100644 --- a/.claude/skills/add-whatsapp/SKILL.md +++ b/.claude/skills/add-whatsapp/SKILL.md @@ -370,3 +370,8 @@ To remove WhatsApp integration: 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) + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/convert-to-apple-container/SKILL.md b/.claude/skills/convert-to-apple-container/SKILL.md index caf9c22..bcd1929 100644 --- a/.claude/skills/convert-to-apple-container/SKILL.md +++ b/.claude/skills/convert-to-apple-container/SKILL.md @@ -173,3 +173,8 @@ Check directory permissions on the host. The container runs as uid 1000. | `src/container-runner.ts` | .env shadow mount removed, main containers start as root with privilege drop | | `container/Dockerfile` | Entrypoint: `mount --bind` for .env shadowing, `setpriv` privilege drop | | `container/build.sh` | Default runtime: `docker` → `container` | + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/customize/SKILL.md b/.claude/skills/customize/SKILL.md index 614a979..310f1ed 100644 --- a/.claude/skills/customize/SKILL.md +++ b/.claude/skills/customize/SKILL.md @@ -108,3 +108,8 @@ User: "Add Telegram as an input channel" 3. Create `src/channels/telegram.ts` implementing the `Channel` interface (see `src/channels/whatsapp.ts`) 4. Add the channel to `main()` in `src/index.ts` 5. Tell user how to authenticate and test + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/debug/SKILL.md b/.claude/skills/debug/SKILL.md index 03c34de..e0fc3c7 100644 --- a/.claude/skills/debug/SKILL.md +++ b/.claude/skills/debug/SKILL.md @@ -347,3 +347,8 @@ echo -e "\n8. Session continuity working?" SESSIONS=$(grep "Session initialized" logs/nanoclaw.log 2>/dev/null | tail -5 | awk '{print $NF}' | sort -u | wc -l) [ "$SESSIONS" -le 2 ] && echo "OK (recent sessions reusing IDs)" || echo "CHECK - multiple different session IDs, may indicate resumption issues" ``` + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/get-qodo-rules/SKILL.md b/.claude/skills/get-qodo-rules/SKILL.md index 69abaf7..4a2cf16 100644 --- a/.claude/skills/get-qodo-rules/SKILL.md +++ b/.claude/skills/get-qodo-rules/SKILL.md @@ -120,3 +120,8 @@ See `~/.qodo/config.json` for API key setup. Set `QODO_ENVIRONMENT_NAME` env var - **Not in git repo** - Inform the user that a git repository is required and exit gracefully; do not attempt code generation - **No API key** - Inform the user with setup instructions; set `QODO_API_KEY` or create `~/.qodo/config.json` - **No rules found** - Inform the user; set up rules at app.qodo.ai + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/qodo-pr-resolver/SKILL.md b/.claude/skills/qodo-pr-resolver/SKILL.md index c0cbe22..165bbe2 100644 --- a/.claude/skills/qodo-pr-resolver/SKILL.md +++ b/.claude/skills/qodo-pr-resolver/SKILL.md @@ -324,3 +324,8 @@ Use the inline comment ID preserved during deduplication (Step 3b) to reply dire See [providers.md § Reply to Inline Comments](./resources/providers.md#reply-to-inline-comments) for provider-specific commands and reply format. Keep replies short (one line). If a reply fails, log it and continue. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index d173927..170e303 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -216,3 +216,8 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/ **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. **Unload service:** macOS: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist` | Linux: `systemctl --user stop nanoclaw` + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/update-nanoclaw/SKILL.md b/.claude/skills/update-nanoclaw/SKILL.md index b0b478c..5e93c6c 100644 --- a/.claude/skills/update-nanoclaw/SKILL.md +++ b/.claude/skills/update-nanoclaw/SKILL.md @@ -233,3 +233,8 @@ Tell the user: - 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` + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/update-skills/SKILL.md b/.claude/skills/update-skills/SKILL.md index cbbff39..ade436b 100644 --- a/.claude/skills/update-skills/SKILL.md +++ b/.claude/skills/update-skills/SKILL.md @@ -128,3 +128,8 @@ Show: - Any conflicts that were resolved (list files) If the service is running, remind the user to restart it to pick up changes. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/use-local-whisper/SKILL.md b/.claude/skills/use-local-whisper/SKILL.md index ec18a09..b7d9814 100644 --- a/.claude/skills/use-local-whisper/SKILL.md +++ b/.claude/skills/use-local-whisper/SKILL.md @@ -150,3 +150,8 @@ whisper-cli -m data/models/ggml-base.bin -f /tmp/test.wav --no-timestamps -nt **Slow transcription**: The base model processes ~30s of audio in <1s on M1+. If slower, check CPU usage — another process may be competing. **Wrong language**: whisper.cpp auto-detects language. To force a language, you can set `WHISPER_LANG` and modify `src/transcription.ts` to pass `-l $WHISPER_LANG`. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/x-integration/SKILL.md b/.claude/skills/x-integration/SKILL.md index 29a7be6..86ae041 100644 --- a/.claude/skills/x-integration/SKILL.md +++ b/.claude/skills/x-integration/SKILL.md @@ -414,4 +414,8 @@ docker run nanoclaw-agent ls -la /app/src/skills/ - `data/x-browser-profile/` - Contains X session cookies (in `.gitignore`) - `data/x-auth.json` - Auth state marker (in `.gitignore`) - Only main group can use X tools (enforced in `agent.ts` and `host.ts`) -- Scripts run as subprocesses with limited environment \ No newline at end of file +- Scripts run as subprocesses with limited environment + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/scripts/send-diagnostics.ts b/scripts/send-diagnostics.ts new file mode 100644 index 0000000..157307c --- /dev/null +++ b/scripts/send-diagnostics.ts @@ -0,0 +1,310 @@ +/** + * send-diagnostics.ts — opt-in, privacy-first diagnostics for NanoClaw. + * + * Collects system info, accepts event-specific data via --data JSON arg, + * gates conflict filenames against upstream, and sends to PostHog. + * + * Usage: + * npx tsx scripts/send-diagnostics.ts \ + * --event \ + * [--success|--failure] \ + * [--data ''] \ + * [--dry-run] + * + * Never exits non-zero on telemetry failures. + */ + +import { execSync } from 'child_process'; +import crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; + +const POSTHOG_ENDPOINT = 'https://us.i.posthog.com/capture/'; +const POSTHOG_TOKEN = 'phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP'; +const SEND_TIMEOUT_MS = 5000; + +const PROJECT_ROOT = path.resolve(import.meta.dirname, '..'); +const STATE_YAML_PATH = path.join(PROJECT_ROOT, '.nanoclaw', 'state.yaml'); + +// --- Args --- + +function parseArgs(): { + event: string; + success?: boolean; + data: Record; + dryRun: boolean; +} { + const args = process.argv.slice(2); + let event = ''; + let success: boolean | undefined; + let data: Record = {}; + let dryRun = false; + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--event': + event = args[++i] || ''; + break; + case '--success': + success = true; + break; + case '--failure': + success = false; + break; + case '--data': + try { + data = JSON.parse(args[++i] || '{}'); + } catch { + console.error('Warning: --data JSON parse failed, ignoring'); + } + break; + case '--dry-run': + dryRun = true; + break; + } + } + + if (!event) { + console.error('Error: --event is required'); + process.exit(0); // exit 0 — never fail on diagnostics + } + + return { event, success, data, dryRun }; +} + +// --- State (neverAsk) --- + +function readState(): Record { + try { + const raw = fs.readFileSync(STATE_YAML_PATH, 'utf-8'); + return parseYaml(raw) || {}; + } catch { + return {}; + } +} + +function isNeverAsk(): boolean { + const state = readState(); + return state.neverAsk === true; +} + +export function setNeverAsk(): void { + const state = readState(); + state.neverAsk = true; + const dir = path.dirname(STATE_YAML_PATH); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(STATE_YAML_PATH, stringifyYaml(state)); +} + +// --- Git helpers --- + +/** Resolve the upstream remote ref (could be 'upstream/main' or 'origin/main'). */ +function resolveUpstreamRef(): string | null { + for (const ref of ['upstream/main', 'origin/main']) { + try { + execSync(`git rev-parse --verify ${ref}`, { + cwd: PROJECT_ROOT, + stdio: 'ignore', + }); + return ref; + } catch { + continue; + } + } + return null; +} + +// --- System info --- + +function getNanoclawVersion(): string { + try { + const pkg = JSON.parse( + fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf-8'), + ); + return pkg.version || 'unknown'; + } catch { + return 'unknown'; + } +} + +function getNodeMajorVersion(): number | null { + const match = process.version.match(/^v(\d+)/); + return match ? parseInt(match[1], 10) : null; +} + +function getContainerRuntime(): string { + try { + const src = fs.readFileSync( + path.join(PROJECT_ROOT, 'src', 'container-runtime.ts'), + 'utf-8', + ); + const match = src.match(/CONTAINER_RUNTIME_BIN\s*=\s*['"]([^'"]+)['"]/); + return match ? match[1] : 'unknown'; + } catch { + return 'unknown'; + } +} + +function isUpstreamCommit(): boolean { + const ref = resolveUpstreamRef(); + if (!ref) return false; + try { + const head = execSync('git rev-parse HEAD', { + encoding: 'utf-8', + cwd: PROJECT_ROOT, + stdio: ['pipe', 'pipe', 'ignore'], + }).trim(); + execSync(`git merge-base --is-ancestor ${head} ${ref}`, { + cwd: PROJECT_ROOT, + stdio: 'ignore', + }); + return true; + } catch { + return false; + } +} + +function collectSystemInfo(): Record { + return { + nanoclaw_version: getNanoclawVersion(), + os_platform: process.platform, + arch: process.arch, + node_major_version: getNodeMajorVersion(), + container_runtime: getContainerRuntime(), + is_upstream_commit: isUpstreamCommit(), + }; +} + +// --- Conflict filename gating --- + +function getUpstreamFiles(): Set | null { + const ref = resolveUpstreamRef(); + if (!ref) return null; + try { + const output = execSync(`git ls-tree -r --name-only ${ref}`, { + encoding: 'utf-8', + cwd: PROJECT_ROOT, + stdio: ['pipe', 'pipe', 'ignore'], + }); + return new Set(output.trim().split('\n').filter(Boolean)); + } catch { + return null; + } +} + +function gateConflictFiles(data: Record): void { + if (!Array.isArray(data.conflict_files)) return; + + const rawFiles: string[] = data.conflict_files; + const upstreamFiles = getUpstreamFiles(); + const totalCount = rawFiles.length; + + if (!upstreamFiles) { + // Can't verify — fail-closed + data.conflict_files = []; + data.conflict_count = totalCount; + data.has_non_upstream_conflicts = totalCount > 0; + return; + } + + const safe: string[] = []; + let hasNonUpstream = false; + + for (const file of rawFiles) { + if (upstreamFiles.has(file)) { + safe.push(file); + } else { + hasNonUpstream = true; + } + } + + data.conflict_files = safe; + data.conflict_count = totalCount; + data.has_non_upstream_conflicts = hasNonUpstream; +} + +// --- Build & send --- + +function buildPayload( + event: string, + systemInfo: Record, + eventData: Record, + success?: boolean, +): Record { + const properties: Record = { + $process_person_profile: false, + $lib: 'nanoclaw-diagnostics', + ...systemInfo, + ...eventData, + }; + + if (success !== undefined) { + properties.success = success; + } + + return { + api_key: POSTHOG_TOKEN, + event, + distinct_id: crypto.randomUUID(), + properties, + }; +} + +async function sendToPostHog( + payload: Record, +): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS); + + try { + const response = await fetch(POSTHOG_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: controller.signal, + }); + if (response.ok) { + console.log('Diagnostics sent successfully.'); + } else { + console.error( + `Diagnostics send failed (HTTP ${response.status}). This is fine.`, + ); + } + } catch (err) { + console.error('Diagnostics send failed (network error). This is fine.'); + } finally { + clearTimeout(timeout); + } +} + +// --- Main --- + +async function main(): Promise { + try { + if (isNeverAsk()) { + // User opted out permanently — exit silently + return; + } + + const { event, success, data, dryRun } = parseArgs(); + + // Gate conflict filenames before building payload + gateConflictFiles(data); + + const systemInfo = collectSystemInfo(); + const payload = buildPayload(event, systemInfo, data, success); + + if (dryRun) { + console.log(JSON.stringify(payload, null, 2)); + return; + } + + await sendToPostHog(payload); + } catch (err) { + // Never fail on diagnostics + console.error('Diagnostics error (this is fine):', (err as Error).message); + } +} + +main(); From 33874de17507359ccfa114a14fd268548c612dea Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Thu, 19 Mar 2026 21:17:55 +0000 Subject: [PATCH 15/54] fix: strip api_key from dry-run output shown to user Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/send-diagnostics.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/send-diagnostics.ts b/scripts/send-diagnostics.ts index 157307c..80d5124 100644 --- a/scripts/send-diagnostics.ts +++ b/scripts/send-diagnostics.ts @@ -296,7 +296,9 @@ async function main(): Promise { const payload = buildPayload(event, systemInfo, data, success); if (dryRun) { - console.log(JSON.stringify(payload, null, 2)); + // Strip secrets before showing to user + const { api_key, ...visible } = payload; + console.log(JSON.stringify(visible, null, 2)); return; } From 3747dfeacc104fd392a4cfecd3b7e65eb02cfe02 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Thu, 19 Mar 2026 21:19:16 +0000 Subject: [PATCH 16/54] fix: also strip distinct_id from dry-run output Ephemeral UUID is harmless but showing it to users creates unnecessary doubt about whether they're being tracked. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/send-diagnostics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/send-diagnostics.ts b/scripts/send-diagnostics.ts index 80d5124..fe37585 100644 --- a/scripts/send-diagnostics.ts +++ b/scripts/send-diagnostics.ts @@ -296,8 +296,8 @@ async function main(): Promise { const payload = buildPayload(event, systemInfo, data, success); if (dryRun) { - // Strip secrets before showing to user - const { api_key, ...visible } = payload; + // Strip internal fields before showing to user + const { api_key, distinct_id, ...visible } = payload; console.log(JSON.stringify(visible, null, 2)); return; } From 8c1d5598bafdda58763a6cbaa6e1ad0822436e27 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Thu, 19 Mar 2026 21:21:29 +0000 Subject: [PATCH 17/54] fix: strip PostHog internal fields from dry-run output Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/send-diagnostics.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/send-diagnostics.ts b/scripts/send-diagnostics.ts index fe37585..5b5399c 100644 --- a/scripts/send-diagnostics.ts +++ b/scripts/send-diagnostics.ts @@ -298,6 +298,9 @@ async function main(): Promise { if (dryRun) { // Strip internal fields before showing to user const { api_key, distinct_id, ...visible } = payload; + const props = visible.properties as Record; + delete props.$process_person_profile; + delete props.$lib; console.log(JSON.stringify(visible, null, 2)); return; } From 1734be725990bd7d6cbd7fa94ce738082aeefcf4 Mon Sep 17 00:00:00 2001 From: NanoClaw User Date: Thu, 19 Mar 2026 22:20:11 +0000 Subject: [PATCH 18/54] fix: collect diagnostics for sub-skills invoked during setup Previously, sub-skills (e.g. /add-telegram) skipped diagnostics when called from a parent skill like /setup. This lost channel-level events. Now all events are collected and shown to the user in a single prompt. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/_shared/diagnostics.md | 31 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/.claude/skills/_shared/diagnostics.md b/.claude/skills/_shared/diagnostics.md index c509a70..ac5994b 100644 --- a/.claude/skills/_shared/diagnostics.md +++ b/.claude/skills/_shared/diagnostics.md @@ -2,11 +2,18 @@ Follow these steps **after all other skill steps are complete**. -## 1. Skip check +## 1. Collect all events -If this skill was invoked from within another skill (e.g., `/add-whatsapp` called during `/setup`), **skip diagnostics entirely** — the parent skill handles it. Return without doing anything below. +Every skill sends its own diagnostics event — even when invoked from a parent skill. A `/setup` that invokes `/add-telegram` produces **two** events: `skill_applied` (for add-telegram) and `setup_complete` (for setup). A `/setup` with no channels produces just `setup_complete`. -## 2. Determine event type +Build the list of events to send: + +1. If sub-skills were invoked during this run (e.g., `/add-telegram` during `/setup`), add a `skill_applied` event for each. +2. Add the event for the current (outermost) skill. + +**If running a sub-skill standalone** (not called from a parent), there is just one event. + +## 2. Determine event type (for each event) - `/setup` → `setup_complete` - `/update-nanoclaw` → `update_complete` @@ -58,25 +65,25 @@ Estimate `error_count` from the conversation (how many errors/retries occurred d - `breaking_changes_found`: whether breaking changes were detected during the update - `breaking_changes_skills_run`: which skills had to be re-run to fix breaking changes -## 4. Dry run +## 4. Dry run all events -Run with `--dry-run` to get the full payload: +For **each** event, run with `--dry-run` to get the payload: ```bash npx tsx scripts/send-diagnostics.ts --event --success --data '' --dry-run ``` -Use `--failure` instead of `--success` if the skill failed. +Use `--failure` instead of `--success` if that particular skill/step failed. -If the command produces no output, the user has opted out permanently — skip the rest. +If **any** dry-run produces no output, the user has opted out permanently — skip the rest for all events. -## 5. Show the user and ask +## 5. Show the user and ask once -Show the JSON output and ask: +Show **all** payloads together and ask **once** (not per-event): > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show the JSON) +> (show all JSON payloads) > > **Yes** / **No** / **Never ask again** @@ -84,11 +91,11 @@ Use AskUserQuestion. ## 6. Handle response -- **Yes**: Run the same command without `--dry-run`: +- **Yes**: Send **all** events (run each command without `--dry-run`): ```bash npx tsx scripts/send-diagnostics.ts --event --success --data '' ``` - Confirm: "Diagnostics sent." + Confirm: "Diagnostics sent (N events)." or "Diagnostics sent." if only one. - **No**: Do nothing. User will be asked again next time. From 0ce11f6f4d09dd7035b39d4d5087b84f8fae924f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 06:55:51 +0000 Subject: [PATCH 19/54] feat: add Slack formatting skill for NanoClaw agents Add a new skill that teaches agents how to format messages using Slack's mrkdwn syntax. Updates agent CLAUDE.md files to detect channel type from folder name prefix and use appropriate formatting. - container/skills/slack-formatting/SKILL.md: comprehensive mrkdwn reference - groups/global/CLAUDE.md: channel-aware formatting instructions - groups/main/CLAUDE.md: same, plus emoji shortcode examples https://claude.ai/code/session_01W44WtL2gRETr9YBB6h62YM --- container/skills/slack-formatting/SKILL.md | 94 ++++++++++++++++++++++ groups/global/CLAUDE.md | 30 +++++-- groups/main/CLAUDE.md | 32 ++++++-- 3 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 container/skills/slack-formatting/SKILL.md diff --git a/container/skills/slack-formatting/SKILL.md b/container/skills/slack-formatting/SKILL.md new file mode 100644 index 0000000..29a1b87 --- /dev/null +++ b/container/skills/slack-formatting/SKILL.md @@ -0,0 +1,94 @@ +--- +name: slack-formatting +description: Format messages for Slack using mrkdwn syntax. Use when responding to Slack channels (folder starts with "slack_" or JID contains slack identifiers). +--- + +# Slack Message Formatting (mrkdwn) + +When responding to Slack channels, use Slack's mrkdwn syntax instead of standard Markdown. + +## How to detect Slack context + +Check your group folder name or workspace path: +- Folder starts with `slack_` (e.g., `slack_engineering`, `slack_general`) +- Or check `/workspace/group/` path for `slack_` prefix + +## Formatting reference + +### Text styles + +| Style | Syntax | Example | +|-------|--------|---------| +| Bold | `*text*` | *bold text* | +| Italic | `_text_` | _italic text_ | +| Strikethrough | `~text~` | ~strikethrough~ | +| Code (inline) | `` `code` `` | `inline code` | +| Code block | ` ```code``` ` | Multi-line code | + +### Links and mentions + +``` + # Named link + # Auto-linked URL +<@U1234567890> # Mention user by ID +<#C1234567890> # Mention channel by ID + # @here + # @channel +``` + +### Lists + +Slack supports simple bullet lists but NOT numbered lists: + +``` +• First item +• Second item +• Third item +``` + +Use `•` (bullet character) or `- ` or `* ` for bullets. + +### Block quotes + +``` +> This is a block quote +> It can span multiple lines +``` + +### Emoji + +Use standard emoji shortcodes: `:white_check_mark:`, `:x:`, `:rocket:`, `:tada:` + +## What NOT to use + +- **NO** `##` headings (use `*Bold text*` for headers instead) +- **NO** `**double asterisks**` for bold (use `*single asterisks*`) +- **NO** `[text](url)` links (use `` instead) +- **NO** `1.` numbered lists (use bullets with numbers: `• 1. First`) +- **NO** tables (use code blocks or plain text alignment) +- **NO** `---` horizontal rules + +## Example message + +``` +*Daily Standup Summary* + +_March 21, 2026_ + +• *Completed:* Fixed authentication bug in login flow +• *In Progress:* Building new dashboard widgets +• *Blocked:* Waiting on API access from DevOps + +> Next sync: Monday 10am + +:white_check_mark: All tests passing | +``` + +## Quick rules + +1. Use `*bold*` not `**bold**` +2. Use `` not `[text](url)` +3. Use `•` bullets, avoid numbered lists +4. Use `:emoji:` shortcodes +5. Quote blocks with `>` +6. Skip headings — use bold text instead diff --git a/groups/global/CLAUDE.md b/groups/global/CLAUDE.md index 28e97a7..c814e39 100644 --- a/groups/global/CLAUDE.md +++ b/groups/global/CLAUDE.md @@ -49,10 +49,28 @@ When you learn something important: ## Message Formatting -NEVER use markdown. Only use WhatsApp/Telegram formatting: -- *single asterisks* for bold (NEVER **double asterisks**) -- _underscores_ for italic -- • bullet points -- ```triple backticks``` for code +Format messages based on the channel you're responding to. Check your group folder name: -No ## headings. No [links](url). No **double stars**. +### Slack channels (folder starts with `slack_`) + +Use Slack mrkdwn syntax. Run `/slack-formatting` for the full reference. Key rules: +- `*bold*` (single asterisks) +- `_italic_` (underscores) +- `` for links (NOT `[text](url)`) +- `•` bullets (no numbered lists) +- `:emoji:` shortcodes +- `>` for block quotes +- No `##` headings — use `*Bold text*` instead + +### WhatsApp/Telegram channels (folder starts with `whatsapp_` or `telegram_`) + +- `*bold*` (single asterisks, NEVER **double**) +- `_italic_` (underscores) +- `•` bullet points +- ` ``` ` code blocks + +No `##` headings. No `[links](url)`. No `**double stars**`. + +### Discord channels (folder starts with `discord_`) + +Standard Markdown works: `**bold**`, `*italic*`, `[links](url)`, `# headings`. diff --git a/groups/main/CLAUDE.md b/groups/main/CLAUDE.md index 11e846b..d4e3258 100644 --- a/groups/main/CLAUDE.md +++ b/groups/main/CLAUDE.md @@ -43,15 +43,33 @@ When you learn something important: - Split files larger than 500 lines into folders - Keep an index in your memory for the files you create -## WhatsApp Formatting (and other messaging apps) +## Message Formatting -Do NOT use markdown headings (##) in WhatsApp messages. Only use: -- *Bold* (single asterisks) (NEVER **double asterisks**) -- _Italic_ (underscores) -- • Bullets (bullet points) -- ```Code blocks``` (triple backticks) +Format messages based on the channel. Check the group folder name prefix: -Keep messages clean and readable for WhatsApp. +### Slack channels (folder starts with `slack_`) + +Use Slack mrkdwn syntax. Run `/slack-formatting` for the full reference. Key rules: +- `*bold*` (single asterisks) +- `_italic_` (underscores) +- `` for links (NOT `[text](url)`) +- `•` bullets (no numbered lists) +- `:emoji:` shortcodes like `:white_check_mark:`, `:rocket:` +- `>` for block quotes +- No `##` headings — use `*Bold text*` instead + +### WhatsApp/Telegram (folder starts with `whatsapp_` or `telegram_`) + +- `*bold*` (single asterisks, NEVER **double**) +- `_italic_` (underscores) +- `•` bullet points +- ` ``` ` code blocks + +No `##` headings. No `[links](url)`. No `**double stars**`. + +### Discord (folder starts with `discord_`) + +Standard Markdown: `**bold**`, `*italic*`, `[links](url)`, `# headings`. --- From b7420c65627adefc71d701bde6c19b77e62a6495 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 21 Mar 2026 09:54:51 +0000 Subject: [PATCH 20/54] chore: bump version to 1.2.20 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 261f226..dc7d2dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.19", + "version": "1.2.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", diff --git a/package.json b/package.json index beacf9b..d34df9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.19", + "version": "1.2.20", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From 30ebcaa61e51f01e7b8de1225b6d4d308d917aad Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 18:37:51 +0000 Subject: [PATCH 21/54] feat: add ESLint with error-handling rules Add ESLint v9.35+ with typescript-eslint recommended config and error-handling rules: preserve-caught-error (enforces { cause } when re-throwing), no-unused-vars with caughtErrors:all, and eslint-plugin-no-catch-all (warns on catch blocks that don't rethrow). Fix existing violations: add error cause to container-runtime rethrow, prefix unused vars with underscore, remove unused imports. https://claude.ai/code/session_01JPjzhBp9PR5LtfLWVDrYrH --- eslint.config.js | 32 + package-lock.json | 1376 +++++++++++++++++++++++++++++++++ package.json | 7 + src/channels/registry.test.ts | 2 +- src/container-runner.ts | 2 +- src/container-runtime.ts | 2 +- src/group-queue.test.ts | 6 +- src/group-queue.ts | 2 +- src/index.ts | 1 - src/remote-control.test.ts | 4 +- src/routing.test.ts | 2 +- src/sender-allowlist.test.ts | 2 +- 12 files changed, 1426 insertions(+), 12 deletions(-) create mode 100644 eslint.config.js diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..fa257de --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,32 @@ +import globals from 'globals' +import pluginJs from '@eslint/js' +import tseslint from 'typescript-eslint' +import noCatchAll from 'eslint-plugin-no-catch-all' + +export default [ + { ignores: ['node_modules/', 'dist/', 'container/', 'groups/'] }, + { files: ['src/**/*.{js,ts}'] }, + { languageOptions: { globals: globals.node } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + plugins: { 'no-catch-all': noCatchAll }, + rules: { + 'preserve-caught-error': ['error', { requireCatchParameter: true }], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'all', + argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + 'no-catch-all/no-catch-all': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + }, + }, +] diff --git a/package-lock.json b/package-lock.json index dc7d2dd..904bc5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,13 +16,18 @@ "zod": "^4.3.6" }, "devDependencies": { + "@eslint/js": "^9.35.0", "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.10.0", "@vitest/coverage-v8": "^4.0.18", + "eslint": "^9.35.0", + "eslint-plugin-no-catch-all": "^1.1.0", + "globals": "^15.12.0", "husky": "^9.1.7", "prettier": "^3.8.1", "tsx": "^4.19.0", "typescript": "^5.7.0", + "typescript-eslint": "^8.35.0", "vitest": "^4.0.18" }, "engines": { @@ -531,6 +536,228 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -957,6 +1184,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", @@ -967,6 +1201,288 @@ "undici-types": "~6.21.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/type-utils": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.1", + "@typescript-eslint/types": "^8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.1", + "@typescript-eslint/tsconfig-utils": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitest/coverage-v8": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", @@ -1109,6 +1625,69 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1140,6 +1719,13 @@ "node": ">=8.0.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1191,6 +1777,17 @@ "readable-stream": "^3.4.0" } }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1215,6 +1812,16 @@ "ieee754": "^1.1.13" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1225,18 +1832,62 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/cron-parser": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.5.0.tgz", @@ -1249,6 +1900,21 @@ "node": ">=18" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -1258,6 +1924,24 @@ "node": "*" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -1282,6 +1966,13 @@ "node": ">=4.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1349,6 +2040,173 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-no-catch-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-catch-all/-/eslint-plugin-no-catch-all-1.1.0.tgz", + "integrity": "sha512-VkP62jLTmccPrFGN/W6V7a3SEwdtTZm+Su2k4T3uyJirtkm0OMMm97h7qd8pRFAHus/jQg9FpUpLRc7sAylBEQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=2.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1359,6 +2217,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1384,6 +2252,27 @@ "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -1408,12 +2297,63 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -1454,6 +2394,32 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1513,6 +2479,43 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1525,6 +2528,36 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -1580,6 +2613,87 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -1639,6 +2753,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1654,6 +2781,13 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1679,6 +2813,13 @@ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-abi": { "version": "3.87.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", @@ -1720,6 +2861,89 @@ "wrappy": "1" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1872,6 +3096,16 @@ "node": ">=10" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", @@ -1914,6 +3148,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", @@ -1967,6 +3211,16 @@ "node": ">= 12.13.0" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -2079,6 +3333,29 @@ "node": ">=10" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -2288,6 +3565,19 @@ "node": ">=14.0.0" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -2320,6 +3610,19 @@ "node": "*" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2334,6 +3637,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", + "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -2341,6 +3668,16 @@ "dev": true, "license": "MIT" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2500,6 +3837,22 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -2517,6 +3870,16 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2538,6 +3901,19 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index d34df9d..3817505 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "prepare": "husky", "setup": "tsx setup/index.ts", "auth": "tsx src/whatsapp-auth.ts", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", "test": "vitest run", "test:watch": "vitest" }, @@ -27,13 +29,18 @@ "zod": "^4.3.6" }, "devDependencies": { + "@eslint/js": "^9.35.0", "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.10.0", "@vitest/coverage-v8": "^4.0.18", + "eslint": "^9.35.0", + "eslint-plugin-no-catch-all": "^1.1.0", + "globals": "^15.12.0", "husky": "^9.1.7", "prettier": "^3.8.1", "tsx": "^4.19.0", "typescript": "^5.7.0", + "typescript-eslint": "^8.35.0", "vitest": "^4.0.18" }, "engines": { diff --git a/src/channels/registry.test.ts b/src/channels/registry.test.ts index e47b1bf..e89f62b 100644 --- a/src/channels/registry.test.ts +++ b/src/channels/registry.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { registerChannel, diff --git a/src/container-runner.ts b/src/container-runner.ts index 59bccd8..5eb85d0 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -698,7 +698,7 @@ export function writeGroupsSnapshot( groupFolder: string, isMain: boolean, groups: AvailableGroup[], - registeredJids: Set, + _registeredJids: Set, ): void { const groupIpcDir = resolveGroupIpcPath(groupFolder); fs.mkdirSync(groupIpcDir, { recursive: true }); diff --git a/src/container-runtime.ts b/src/container-runtime.ts index 5a4f91e..c7324e2 100644 --- a/src/container-runtime.ts +++ b/src/container-runtime.ts @@ -96,7 +96,7 @@ export function ensureContainerRuntimeRunning(): void { console.error( '╚════════════════════════════════════════════════════════════════╝\n', ); - throw new Error('Container runtime is required but failed to start'); + throw new Error('Container runtime is required but failed to start', { cause: err }); } } diff --git a/src/group-queue.test.ts b/src/group-queue.test.ts index ca2702a..d7de517 100644 --- a/src/group-queue.test.ts +++ b/src/group-queue.test.ts @@ -40,7 +40,7 @@ describe('GroupQueue', () => { let concurrentCount = 0; let maxConcurrent = 0; - const processMessages = vi.fn(async (groupJid: string) => { + const processMessages = vi.fn(async (_groupJid: string) => { concurrentCount++; maxConcurrent = Math.max(maxConcurrent, concurrentCount); // Simulate async work @@ -69,7 +69,7 @@ describe('GroupQueue', () => { let maxActive = 0; const completionCallbacks: Array<() => void> = []; - const processMessages = vi.fn(async (groupJid: string) => { + const processMessages = vi.fn(async (_groupJid: string) => { activeCount++; maxActive = Math.max(maxActive, activeCount); await new Promise((resolve) => completionCallbacks.push(resolve)); @@ -104,7 +104,7 @@ describe('GroupQueue', () => { const executionOrder: string[] = []; let resolveFirst: () => void; - const processMessages = vi.fn(async (groupJid: string) => { + const processMessages = vi.fn(async (_groupJid: string) => { if (executionOrder.length === 0) { // First call: block until we release it await new Promise((resolve) => { diff --git a/src/group-queue.ts b/src/group-queue.ts index f2984ce..a3b547d 100644 --- a/src/group-queue.ts +++ b/src/group-queue.ts @@ -351,7 +351,7 @@ export class GroupQueue { // via idle timeout or container timeout. The --rm flag cleans them up on exit. // This prevents WhatsApp reconnection restarts from killing working agents. const activeContainers: string[] = []; - for (const [jid, state] of this.groups) { + for (const [_jid, state] of this.groups) { if (state.process && !state.process.killed && state.containerName) { activeContainers.push(state.containerName); } diff --git a/src/index.ts b/src/index.ts index 42329a0..db274f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,6 @@ import { getAllTasks, getMessagesSince, getNewMessages, - getRegisteredGroup, getRouterState, initDatabase, setRegisteredGroup, diff --git a/src/remote-control.test.ts b/src/remote-control.test.ts index 24e1b11..7dbf69c 100644 --- a/src/remote-control.test.ts +++ b/src/remote-control.test.ts @@ -37,7 +37,7 @@ describe('remote-control', () => { let readFileSyncSpy: ReturnType; let writeFileSyncSpy: ReturnType; let unlinkSyncSpy: ReturnType; - let mkdirSyncSpy: ReturnType; + let _mkdirSyncSpy: ReturnType; let openSyncSpy: ReturnType; let closeSyncSpy: ReturnType; @@ -50,7 +50,7 @@ describe('remote-control', () => { stdoutFileContent = ''; // Default fs mocks - mkdirSyncSpy = vi + _mkdirSyncSpy = vi .spyOn(fs, 'mkdirSync') .mockImplementation(() => undefined as any); writeFileSyncSpy = vi diff --git a/src/routing.test.ts b/src/routing.test.ts index 32bfc1f..6e44586 100644 --- a/src/routing.test.ts +++ b/src/routing.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { _initTestDatabase, getAllChats, storeChatMetadata } from './db.js'; +import { _initTestDatabase, storeChatMetadata } from './db.js'; import { getAvailableGroups, _setRegisteredGroups } from './index.js'; beforeEach(() => { diff --git a/src/sender-allowlist.test.ts b/src/sender-allowlist.test.ts index 9e2513f..5bb8569 100644 --- a/src/sender-allowlist.test.ts +++ b/src/sender-allowlist.test.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { isSenderAllowed, From b30b5a6a8fe11c267151afa3788fe8cfabc14f3e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 18:38:25 +0000 Subject: [PATCH 22/54] style: apply prettier formatting to modified files https://claude.ai/code/session_01JPjzhBp9PR5LtfLWVDrYrH --- src/container-runner.ts | 6 +----- src/container-runtime.ts | 4 +++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/container-runner.ts b/src/container-runner.ts index 5eb85d0..a6b58d7 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -507,11 +507,7 @@ export async function runContainerAgent( // Full input is only included at verbose level to avoid // persisting user conversation content on every non-zero exit. if (isVerbose) { - logLines.push( - `=== Input ===`, - JSON.stringify(input, null, 2), - ``, - ); + logLines.push(`=== Input ===`, JSON.stringify(input, null, 2), ``); } else { logLines.push( `=== Input Summary ===`, diff --git a/src/container-runtime.ts b/src/container-runtime.ts index c7324e2..9f32d10 100644 --- a/src/container-runtime.ts +++ b/src/container-runtime.ts @@ -96,7 +96,9 @@ export function ensureContainerRuntimeRunning(): void { console.error( '╚════════════════════════════════════════════════════════════════╝\n', ); - throw new Error('Container runtime is required but failed to start', { cause: err }); + throw new Error('Container runtime is required but failed to start', { + cause: err, + }); } } From c3b19876eb3b795757c0e6d3e2c86618af983f07 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 21 Mar 2026 09:57:39 +0000 Subject: [PATCH 23/54] chore: bump version to 1.2.21 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 904bc5e..fae72c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.20", + "version": "1.2.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.20", + "version": "1.2.21", "dependencies": { "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", diff --git a/package.json b/package.json index 3817505..b30dd39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.20", + "version": "1.2.21", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From 18469294ce7f5af48d2fd3e9cd955678a46bebd1 Mon Sep 17 00:00:00 2001 From: Ken Bolton Date: Fri, 20 Mar 2026 13:51:18 -0400 Subject: [PATCH 24/54] Add claw CLI skill --- .claude/skills/claw/SKILL.md | 449 +++++++++++++++++++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 .claude/skills/claw/SKILL.md diff --git a/.claude/skills/claw/SKILL.md b/.claude/skills/claw/SKILL.md new file mode 100644 index 0000000..1493199 --- /dev/null +++ b/.claude/skills/claw/SKILL.md @@ -0,0 +1,449 @@ +--- +name: claw +description: Install the claw CLI tool — run NanoClaw agent containers from the command line without opening a chat app. +author: kenbolton +--- + +# claw — NanoClaw CLI + +`claw` is a Python CLI script that lets you send prompts directly to a NanoClaw agent container from your terminal. It reads registered groups from the NanoClaw database, picks up your secrets from `.env`, and pipes a JSON payload into a container run — no chat app required. + +## What it does + +- Send a prompt to any registered group by name, folder, or JID +- Default target is the main group (no `-g` needed for most use) +- Resume a previous session with `-s ` +- Read prompts from stdin (`--pipe`) for scripting and piping +- List all registered groups with `--list-groups` +- Auto-detects `container` or `docker` runtime (or override with `--runtime`) +- Prints the agent's response to stdout; session ID to stderr +- Verbose mode (`-v`) shows the command, redacted payload, and exit code + +## Prerequisites + +- Python 3.10 or later +- NanoClaw installed at `~/src/nanoclaw` with a built and tagged container image (`nanoclaw-agent:latest`) +- Either `container` (Apple Container, macOS 15+) or `docker` available in `PATH` + +## Install + +> **Note:** Run this skill from within your NanoClaw directory (`cd ~/src/nanoclaw` or wherever you installed it). The script auto-detects its location, so the symlink always points to the right place. + +### 1. Write the script + +Create the scripts directory if it doesn't exist, then write the script: + +```bash +mkdir -p scripts +``` + +Write the following to `scripts/claw`: + +```python +#!/usr/bin/env python3 +""" +claw — NanoClaw CLI +Run a NanoClaw agent container from the command line. + +Usage: + claw "What is 2+2?" + claw -g "Review this code" + claw -g "" "What's the latest issue?" + claw -j "" "Hello" + claw -g -s "Continue" + claw --list-groups + echo "prompt text" | claw --pipe -g + cat prompt.txt | claw --pipe +""" + +import argparse +import json +import os +import re +import sqlite3 +import subprocess +import sys +import threading +from pathlib import Path + +# ── Globals ───────────────────────────────────────────────────────────────── + +VERBOSE = False + +def dbg(*args): + if VERBOSE: + print("»", *args, file=sys.stderr) + +# ── Config ────────────────────────────────────────────────────────────────── + +def _find_nanoclaw_dir() -> Path: + """Locate the NanoClaw installation directory. + + Resolution order: + 1. NANOCLAW_DIR env var + 2. The directory containing this script (if it looks like a NanoClaw install) + 3. ~/src/nanoclaw (legacy default) + """ + if env := os.environ.get("NANOCLAW_DIR"): + return Path(env).expanduser() + # If this script lives inside the NanoClaw tree (e.g. scripts/claw), walk up + here = Path(__file__).resolve() + for parent in [here.parent, here.parent.parent]: + if (parent / "store" / "messages.db").exists() or (parent / ".env").exists(): + return parent + return Path.home() / "src" / "nanoclaw" + +NANOCLAW_DIR = _find_nanoclaw_dir() +DB_PATH = NANOCLAW_DIR / "store" / "messages.db" +ENV_FILE = NANOCLAW_DIR / ".env" +IMAGE = "nanoclaw-agent:latest" + +SECRET_KEYS = [ + "CLAUDE_CODE_OAUTH_TOKEN", + "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", + "ANTHROPIC_AUTH_TOKEN", + "OLLAMA_HOST", +] + +# ── Helpers ────────────────────────────────────────────────────────────────── + +def detect_runtime(preference: str | None) -> str: + if preference: + dbg(f"runtime: forced to {preference}") + return preference + for rt in ("container", "docker"): + result = subprocess.run(["which", rt], capture_output=True) + if result.returncode == 0: + dbg(f"runtime: auto-detected {rt} at {result.stdout.decode().strip()}") + return rt + sys.exit("error: neither 'container' nor 'docker' found. Install one or pass --runtime.") + + +def read_secrets(env_file: Path) -> dict: + secrets = {} + if not env_file.exists(): + return secrets + for line in env_file.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" in line: + key, _, val = line.partition("=") + key = key.strip() + if key in SECRET_KEYS: + secrets[key] = val.strip() + return secrets + + +def get_groups(db: Path) -> list[dict]: + conn = sqlite3.connect(db) + rows = conn.execute( + "SELECT jid, name, folder, is_main FROM registered_groups ORDER BY name" + ).fetchall() + conn.close() + return [{"jid": r[0], "name": r[1], "folder": r[2], "is_main": bool(r[3])} for r in rows] + + +def find_group(groups: list[dict], query: str) -> dict | None: + q = query.lower() + # Exact name match + for g in groups: + if g["name"].lower() == q or g["folder"].lower() == q: + return g + # Partial match + matches = [g for g in groups if q in g["name"].lower() or q in g["folder"].lower()] + if len(matches) == 1: + return matches[0] + if len(matches) > 1: + names = ", ".join(f'"{g["name"]}"' for g in matches) + sys.exit(f"error: ambiguous group '{query}'. Matches: {names}") + return None + + +def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) -> None: + cmd = [runtime, "run", "-i", "--rm", image] + dbg(f"cmd: {' '.join(cmd)}") + + # Show payload sans secrets + if VERBOSE: + safe = {k: v for k, v in payload.items() if k != "secrets"} + safe["secrets"] = {k: "***" for k in payload.get("secrets", {})} + dbg(f"payload: {json.dumps(safe, indent=2)}") + + proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + dbg(f"container pid: {proc.pid}") + + # Write JSON payload and close stdin + proc.stdin.write(json.dumps(payload).encode()) + proc.stdin.close() + dbg("stdin closed, waiting for response...") + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] + done = threading.Event() + + def stream_stderr(): + for raw in proc.stderr: + line = raw.decode(errors="replace").rstrip() + if line.startswith("npm notice"): + continue + stderr_lines.append(line) + print(line, file=sys.stderr) + + def stream_stdout(): + for raw in proc.stdout: + line = raw.decode(errors="replace").rstrip() + stdout_lines.append(line) + dbg(f"stdout: {line}") + # Kill the container as soon as we see the closing sentinel — + # the Node.js event loop often keeps the process alive indefinitely. + if line.strip() == "---NANOCLAW_OUTPUT_END---": + dbg("output sentinel found, terminating container") + done.set() + try: + proc.kill() + except ProcessLookupError: + pass + return + + t_err = threading.Thread(target=stream_stderr, daemon=True) + t_out = threading.Thread(target=stream_stdout, daemon=True) + t_err.start() + t_out.start() + + # Wait for sentinel or timeout + if not done.wait(timeout=timeout): + # Also check if process exited naturally + t_out.join(timeout=2) + if not done.is_set(): + proc.kill() + sys.exit(f"error: container timed out after {timeout}s (no output sentinel received)") + + t_err.join(timeout=5) + t_out.join(timeout=5) + proc.wait() + dbg(f"container done (rc={proc.returncode}), {len(stdout_lines)} stdout lines") + stdout = "\n".join(stdout_lines) + + # Parse output block + match = re.search( + r"---NANOCLAW_OUTPUT_START---\n(.*?)\n---NANOCLAW_OUTPUT_END---", + stdout, + re.DOTALL, + ) + if match: + try: + data = json.loads(match.group(1)) + status = data.get("status", "unknown") + if status == "success": + print(data.get("result", "")) + session_id = data.get("newSessionId") or data.get("sessionId") + if session_id: + print(f"\n[session: {session_id}]", file=sys.stderr) + else: + print(f"[{status}] {data.get('result', '')}", file=sys.stderr) + sys.exit(1) + except json.JSONDecodeError: + print(match.group(1)) + else: + # No structured output — print raw stdout + print(stdout) + + if proc.returncode not in (0, None): + sys.exit(proc.returncode) + + +# ── Main ───────────────────────────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser( + prog="claw", + description="Run a NanoClaw agent from the command line.", + ) + parser.add_argument("prompt", nargs="?", help="Prompt to send") + parser.add_argument("-g", "--group", help="Group name or folder (fuzzy match)") + parser.add_argument("-j", "--jid", help="Chat JID (exact)") + parser.add_argument("-s", "--session", help="Session ID to resume") + parser.add_argument("-p", "--pipe", action="store_true", + help="Read prompt from stdin (can be combined with a prompt arg as prefix)") + parser.add_argument("--runtime", choices=["docker", "container"], + help="Container runtime (default: auto-detect)") + parser.add_argument("--image", default=IMAGE, help=f"Container image (default: {IMAGE})") + parser.add_argument("--list-groups", action="store_true", help="List registered groups and exit") + parser.add_argument("--raw", action="store_true", help="Print raw JSON output") + parser.add_argument("--timeout", type=int, default=300, metavar="SECS", + help="Max seconds to wait for a response (default: 300)") + parser.add_argument("-v", "--verbose", action="store_true", + help="Show debug info: cmd, payload (secrets redacted), stdout lines, exit code") + args = parser.parse_args() + + global VERBOSE + VERBOSE = args.verbose + + groups = get_groups(DB_PATH) if DB_PATH.exists() else [] + + if args.list_groups: + print(f"{'NAME':<35} {'FOLDER':<30} {'JID'}") + print("-" * 100) + for g in groups: + main_tag = " [main]" if g["is_main"] else "" + print(f"{g['name']:<35} {g['folder']:<30} {g['jid']}{main_tag}") + return + + # Resolve prompt: --pipe reads stdin, optionally prepended with positional arg + if args.pipe or (not sys.stdin.isatty() and not args.prompt): + stdin_text = sys.stdin.read().strip() + if args.prompt: + prompt = f"{args.prompt}\n\n{stdin_text}" + else: + prompt = stdin_text + else: + prompt = args.prompt + + if not prompt: + parser.print_help() + sys.exit(1) + + # Resolve group → jid + jid = args.jid + group_name = None + is_main = False + + if args.group: + g = find_group(groups, args.group) + if g is None: + sys.exit(f"error: group '{args.group}' not found. Run --list-groups to see options.") + jid = g["jid"] + group_name = g["name"] + is_main = g["is_main"] + elif not jid: + # Default: main group + mains = [g for g in groups if g["is_main"]] + if mains: + jid = mains[0]["jid"] + group_name = mains[0]["name"] + is_main = True + else: + sys.exit("error: no group specified and no main group found. Use -g or -j.") + + runtime = detect_runtime(args.runtime) + secrets = read_secrets(ENV_FILE) + + if not secrets: + print("warning: no secrets found in .env — agent may not be authenticated", file=sys.stderr) + + payload: dict = { + "prompt": prompt, + "chatJid": jid, + "isMain": is_main, + "secrets": secrets, + } + if group_name: + payload["groupFolder"] = group_name + if args.session: + payload["sessionId"] = args.session + payload["resumeAt"] = "latest" + + print(f"[{group_name or jid}] running via {runtime}...", file=sys.stderr) + run_container(runtime, args.image, payload, timeout=args.timeout) + + +if __name__ == "__main__": + main() +``` + +### 2. Make executable and symlink + +```bash +chmod +x scripts/claw +mkdir -p ~/bin +ln -sf "$(pwd)/scripts/claw" ~/bin/claw +``` + +Make sure `~/bin` is in your `PATH`. Add this to `~/.zshrc` or `~/.bashrc` if needed: + +```bash +export PATH="$HOME/bin:$PATH" +``` + +Then reload your shell: + +```bash +source ~/.zshrc # or ~/.bashrc +``` + +### 3. Verify + +```bash +claw --list-groups +``` + +You should see your registered groups. If NanoClaw isn't running or the database doesn't exist yet, the list will be empty — that's fine. + +## Usage Examples + +```bash +# Send a prompt to the main group +claw "What's on my calendar today?" + +# Send to a specific group by name (fuzzy match) +claw -g "family" "Remind everyone about dinner at 7" + +# Send to a group by exact JID +claw -j "120363336345536173@g.us" "Hello" + +# Resume a previous session +claw -s abc123 "Continue where we left off" + +# Read prompt from stdin +echo "Summarize this" | claw --pipe -g dev + +# Pipe a file +cat report.txt | claw --pipe "Summarize this report" + +# List all registered groups +claw --list-groups + +# Force a specific runtime +claw --runtime docker "Hello" + +# Verbose mode (debug info, secrets redacted) +claw -v "Hello" + +# Custom timeout for long-running tasks +claw --timeout 600 "Run the full analysis" +``` + +## Troubleshooting + +### "neither 'container' nor 'docker' found" + +Install Docker Desktop or Apple Container (macOS 15+), or pass `--runtime` explicitly. + +### "no secrets found in .env" + +The script auto-detects your NanoClaw directory and reads `.env` from it. Check that the file exists and contains at least one of: `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`. + +### Container times out + +The default timeout is 300 seconds. For longer tasks, pass `--timeout 600` (or higher). If the container consistently hangs, check that your `nanoclaw-agent:latest` image is up to date by running `./container/build.sh` in your NanoClaw directory. + +### "group not found" + +Run `claw --list-groups` to see what's registered. Group lookup does a fuzzy partial match on name and folder — if your query matches multiple groups, you'll get an error listing the ambiguous matches. + +### Override the NanoClaw directory + +If `claw` can't find your database or `.env`, set the `NANOCLAW_DIR` environment variable: + +```bash +export NANOCLAW_DIR=/path/to/your/nanoclaw +``` + +Or add it permanently to your shell profile. From b2377bb39087fa550bc9782869a8992d88d8cc0f Mon Sep 17 00:00:00 2001 From: Ken Bolton Date: Fri, 20 Mar 2026 17:27:03 -0400 Subject: [PATCH 25/54] Fix Python 3.8 compat, document --image flag and --rm behavior --- .claude/skills/claw/SKILL.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.claude/skills/claw/SKILL.md b/.claude/skills/claw/SKILL.md index 1493199..9c59b3a 100644 --- a/.claude/skills/claw/SKILL.md +++ b/.claude/skills/claw/SKILL.md @@ -21,7 +21,7 @@ author: kenbolton ## Prerequisites -- Python 3.10 or later +- Python 3.8 or later - NanoClaw installed at `~/src/nanoclaw` with a built and tagged container image (`nanoclaw-agent:latest`) - Either `container` (Apple Container, macOS 15+) or `docker` available in `PATH` @@ -56,6 +56,8 @@ Usage: cat prompt.txt | claw --pipe """ +from __future__ import annotations + import argparse import json import os @@ -413,6 +415,9 @@ claw --list-groups # Force a specific runtime claw --runtime docker "Hello" +# Use a custom image tag (e.g. after rebuilding with a new tag) +claw --image nanoclaw-agent:dev "Hello" + # Verbose mode (debug info, secrets redacted) claw -v "Hello" @@ -438,6 +443,20 @@ The default timeout is 300 seconds. For longer tasks, pass `--timeout 600` (or h Run `claw --list-groups` to see what's registered. Group lookup does a fuzzy partial match on name and folder — if your query matches multiple groups, you'll get an error listing the ambiguous matches. +### Container crashes mid-stream + +`claw` runs containers with `--rm`, so they are automatically removed whether they exit cleanly or crash. If the agent crashes before emitting the output sentinel, `claw` will fall back to printing raw stdout. Use `-v` to see what the container produced. Rebuild the image with `./container/build.sh` if crashes are consistent. + +### Use a custom image tag + +If you built the image with a different tag (e.g. during development), pass `--image`: + +```bash +claw --image nanoclaw-agent:dev "Hello" +``` + +Set `NANOCLAW_IMAGE=nanoclaw-agent:dev` in your shell profile to make it the default. + ### Override the NanoClaw directory If `claw` can't find your database or `.env`, set the `NANOCLAW_DIR` environment variable: From bf1e2a381941ec510dd4e96881d6aa7de1156c88 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 12:16:57 +0200 Subject: [PATCH 26/54] refactor: extract claw script from SKILL.md into separate file Move the Python CLI script from inline markdown into scripts/claw, aligning with the Claude Code skills standard (code in files, not md). Remove non-standard `author` frontmatter field. SKILL.md now uses ${CLAUDE_SKILL_DIR} substitution to copy the script during install. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/claw/SKILL.md | 361 +------------------------------ .claude/skills/claw/scripts/claw | 318 +++++++++++++++++++++++++++ 2 files changed, 330 insertions(+), 349 deletions(-) create mode 100644 .claude/skills/claw/scripts/claw diff --git a/.claude/skills/claw/SKILL.md b/.claude/skills/claw/SKILL.md index 9c59b3a..10e0dc3 100644 --- a/.claude/skills/claw/SKILL.md +++ b/.claude/skills/claw/SKILL.md @@ -1,12 +1,11 @@ --- name: claw description: Install the claw CLI tool — run NanoClaw agent containers from the command line without opening a chat app. -author: kenbolton --- # claw — NanoClaw CLI -`claw` is a Python CLI script that lets you send prompts directly to a NanoClaw agent container from your terminal. It reads registered groups from the NanoClaw database, picks up your secrets from `.env`, and pipes a JSON payload into a container run — no chat app required. +`claw` is a Python CLI that sends prompts directly to a NanoClaw agent container from the terminal. It reads registered groups from the NanoClaw database, picks up secrets from `.env`, and pipes a JSON payload into a container run — no chat app required. ## What it does @@ -22,359 +21,35 @@ author: kenbolton ## Prerequisites - Python 3.8 or later -- NanoClaw installed at `~/src/nanoclaw` with a built and tagged container image (`nanoclaw-agent:latest`) +- NanoClaw installed with a built and tagged container image (`nanoclaw-agent:latest`) - Either `container` (Apple Container, macOS 15+) or `docker` available in `PATH` ## Install -> **Note:** Run this skill from within your NanoClaw directory (`cd ~/src/nanoclaw` or wherever you installed it). The script auto-detects its location, so the symlink always points to the right place. +Run this skill from within the NanoClaw directory. The script auto-detects its location, so the symlink always points to the right place. -### 1. Write the script - -Create the scripts directory if it doesn't exist, then write the script: +### 1. Copy the script ```bash mkdir -p scripts +cp "${CLAUDE_SKILL_DIR}/scripts/claw" scripts/claw +chmod +x scripts/claw ``` -Write the following to `scripts/claw`: - -```python -#!/usr/bin/env python3 -""" -claw — NanoClaw CLI -Run a NanoClaw agent container from the command line. - -Usage: - claw "What is 2+2?" - claw -g "Review this code" - claw -g "" "What's the latest issue?" - claw -j "" "Hello" - claw -g -s "Continue" - claw --list-groups - echo "prompt text" | claw --pipe -g - cat prompt.txt | claw --pipe -""" - -from __future__ import annotations - -import argparse -import json -import os -import re -import sqlite3 -import subprocess -import sys -import threading -from pathlib import Path - -# ── Globals ───────────────────────────────────────────────────────────────── - -VERBOSE = False - -def dbg(*args): - if VERBOSE: - print("»", *args, file=sys.stderr) - -# ── Config ────────────────────────────────────────────────────────────────── - -def _find_nanoclaw_dir() -> Path: - """Locate the NanoClaw installation directory. - - Resolution order: - 1. NANOCLAW_DIR env var - 2. The directory containing this script (if it looks like a NanoClaw install) - 3. ~/src/nanoclaw (legacy default) - """ - if env := os.environ.get("NANOCLAW_DIR"): - return Path(env).expanduser() - # If this script lives inside the NanoClaw tree (e.g. scripts/claw), walk up - here = Path(__file__).resolve() - for parent in [here.parent, here.parent.parent]: - if (parent / "store" / "messages.db").exists() or (parent / ".env").exists(): - return parent - return Path.home() / "src" / "nanoclaw" - -NANOCLAW_DIR = _find_nanoclaw_dir() -DB_PATH = NANOCLAW_DIR / "store" / "messages.db" -ENV_FILE = NANOCLAW_DIR / ".env" -IMAGE = "nanoclaw-agent:latest" - -SECRET_KEYS = [ - "CLAUDE_CODE_OAUTH_TOKEN", - "ANTHROPIC_API_KEY", - "ANTHROPIC_BASE_URL", - "ANTHROPIC_AUTH_TOKEN", - "OLLAMA_HOST", -] - -# ── Helpers ────────────────────────────────────────────────────────────────── - -def detect_runtime(preference: str | None) -> str: - if preference: - dbg(f"runtime: forced to {preference}") - return preference - for rt in ("container", "docker"): - result = subprocess.run(["which", rt], capture_output=True) - if result.returncode == 0: - dbg(f"runtime: auto-detected {rt} at {result.stdout.decode().strip()}") - return rt - sys.exit("error: neither 'container' nor 'docker' found. Install one or pass --runtime.") - - -def read_secrets(env_file: Path) -> dict: - secrets = {} - if not env_file.exists(): - return secrets - for line in env_file.read_text().splitlines(): - line = line.strip() - if not line or line.startswith("#"): - continue - if "=" in line: - key, _, val = line.partition("=") - key = key.strip() - if key in SECRET_KEYS: - secrets[key] = val.strip() - return secrets - - -def get_groups(db: Path) -> list[dict]: - conn = sqlite3.connect(db) - rows = conn.execute( - "SELECT jid, name, folder, is_main FROM registered_groups ORDER BY name" - ).fetchall() - conn.close() - return [{"jid": r[0], "name": r[1], "folder": r[2], "is_main": bool(r[3])} for r in rows] - - -def find_group(groups: list[dict], query: str) -> dict | None: - q = query.lower() - # Exact name match - for g in groups: - if g["name"].lower() == q or g["folder"].lower() == q: - return g - # Partial match - matches = [g for g in groups if q in g["name"].lower() or q in g["folder"].lower()] - if len(matches) == 1: - return matches[0] - if len(matches) > 1: - names = ", ".join(f'"{g["name"]}"' for g in matches) - sys.exit(f"error: ambiguous group '{query}'. Matches: {names}") - return None - - -def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) -> None: - cmd = [runtime, "run", "-i", "--rm", image] - dbg(f"cmd: {' '.join(cmd)}") - - # Show payload sans secrets - if VERBOSE: - safe = {k: v for k, v in payload.items() if k != "secrets"} - safe["secrets"] = {k: "***" for k in payload.get("secrets", {})} - dbg(f"payload: {json.dumps(safe, indent=2)}") - - proc = subprocess.Popen( - cmd, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - dbg(f"container pid: {proc.pid}") - - # Write JSON payload and close stdin - proc.stdin.write(json.dumps(payload).encode()) - proc.stdin.close() - dbg("stdin closed, waiting for response...") - - stdout_lines: list[str] = [] - stderr_lines: list[str] = [] - done = threading.Event() - - def stream_stderr(): - for raw in proc.stderr: - line = raw.decode(errors="replace").rstrip() - if line.startswith("npm notice"): - continue - stderr_lines.append(line) - print(line, file=sys.stderr) - - def stream_stdout(): - for raw in proc.stdout: - line = raw.decode(errors="replace").rstrip() - stdout_lines.append(line) - dbg(f"stdout: {line}") - # Kill the container as soon as we see the closing sentinel — - # the Node.js event loop often keeps the process alive indefinitely. - if line.strip() == "---NANOCLAW_OUTPUT_END---": - dbg("output sentinel found, terminating container") - done.set() - try: - proc.kill() - except ProcessLookupError: - pass - return - - t_err = threading.Thread(target=stream_stderr, daemon=True) - t_out = threading.Thread(target=stream_stdout, daemon=True) - t_err.start() - t_out.start() - - # Wait for sentinel or timeout - if not done.wait(timeout=timeout): - # Also check if process exited naturally - t_out.join(timeout=2) - if not done.is_set(): - proc.kill() - sys.exit(f"error: container timed out after {timeout}s (no output sentinel received)") - - t_err.join(timeout=5) - t_out.join(timeout=5) - proc.wait() - dbg(f"container done (rc={proc.returncode}), {len(stdout_lines)} stdout lines") - stdout = "\n".join(stdout_lines) - - # Parse output block - match = re.search( - r"---NANOCLAW_OUTPUT_START---\n(.*?)\n---NANOCLAW_OUTPUT_END---", - stdout, - re.DOTALL, - ) - if match: - try: - data = json.loads(match.group(1)) - status = data.get("status", "unknown") - if status == "success": - print(data.get("result", "")) - session_id = data.get("newSessionId") or data.get("sessionId") - if session_id: - print(f"\n[session: {session_id}]", file=sys.stderr) - else: - print(f"[{status}] {data.get('result', '')}", file=sys.stderr) - sys.exit(1) - except json.JSONDecodeError: - print(match.group(1)) - else: - # No structured output — print raw stdout - print(stdout) - - if proc.returncode not in (0, None): - sys.exit(proc.returncode) - - -# ── Main ───────────────────────────────────────────────────────────────────── - -def main(): - parser = argparse.ArgumentParser( - prog="claw", - description="Run a NanoClaw agent from the command line.", - ) - parser.add_argument("prompt", nargs="?", help="Prompt to send") - parser.add_argument("-g", "--group", help="Group name or folder (fuzzy match)") - parser.add_argument("-j", "--jid", help="Chat JID (exact)") - parser.add_argument("-s", "--session", help="Session ID to resume") - parser.add_argument("-p", "--pipe", action="store_true", - help="Read prompt from stdin (can be combined with a prompt arg as prefix)") - parser.add_argument("--runtime", choices=["docker", "container"], - help="Container runtime (default: auto-detect)") - parser.add_argument("--image", default=IMAGE, help=f"Container image (default: {IMAGE})") - parser.add_argument("--list-groups", action="store_true", help="List registered groups and exit") - parser.add_argument("--raw", action="store_true", help="Print raw JSON output") - parser.add_argument("--timeout", type=int, default=300, metavar="SECS", - help="Max seconds to wait for a response (default: 300)") - parser.add_argument("-v", "--verbose", action="store_true", - help="Show debug info: cmd, payload (secrets redacted), stdout lines, exit code") - args = parser.parse_args() - - global VERBOSE - VERBOSE = args.verbose - - groups = get_groups(DB_PATH) if DB_PATH.exists() else [] - - if args.list_groups: - print(f"{'NAME':<35} {'FOLDER':<30} {'JID'}") - print("-" * 100) - for g in groups: - main_tag = " [main]" if g["is_main"] else "" - print(f"{g['name']:<35} {g['folder']:<30} {g['jid']}{main_tag}") - return - - # Resolve prompt: --pipe reads stdin, optionally prepended with positional arg - if args.pipe or (not sys.stdin.isatty() and not args.prompt): - stdin_text = sys.stdin.read().strip() - if args.prompt: - prompt = f"{args.prompt}\n\n{stdin_text}" - else: - prompt = stdin_text - else: - prompt = args.prompt - - if not prompt: - parser.print_help() - sys.exit(1) - - # Resolve group → jid - jid = args.jid - group_name = None - is_main = False - - if args.group: - g = find_group(groups, args.group) - if g is None: - sys.exit(f"error: group '{args.group}' not found. Run --list-groups to see options.") - jid = g["jid"] - group_name = g["name"] - is_main = g["is_main"] - elif not jid: - # Default: main group - mains = [g for g in groups if g["is_main"]] - if mains: - jid = mains[0]["jid"] - group_name = mains[0]["name"] - is_main = True - else: - sys.exit("error: no group specified and no main group found. Use -g or -j.") - - runtime = detect_runtime(args.runtime) - secrets = read_secrets(ENV_FILE) - - if not secrets: - print("warning: no secrets found in .env — agent may not be authenticated", file=sys.stderr) - - payload: dict = { - "prompt": prompt, - "chatJid": jid, - "isMain": is_main, - "secrets": secrets, - } - if group_name: - payload["groupFolder"] = group_name - if args.session: - payload["sessionId"] = args.session - payload["resumeAt"] = "latest" - - print(f"[{group_name or jid}] running via {runtime}...", file=sys.stderr) - run_container(runtime, args.image, payload, timeout=args.timeout) - - -if __name__ == "__main__": - main() -``` - -### 2. Make executable and symlink +### 2. Symlink into PATH ```bash -chmod +x scripts/claw mkdir -p ~/bin ln -sf "$(pwd)/scripts/claw" ~/bin/claw ``` -Make sure `~/bin` is in your `PATH`. Add this to `~/.zshrc` or `~/.bashrc` if needed: +Make sure `~/bin` is in `PATH`. Add this to `~/.zshrc` or `~/.bashrc` if needed: ```bash export PATH="$HOME/bin:$PATH" ``` -Then reload your shell: +Then reload the shell: ```bash source ~/.zshrc # or ~/.bashrc @@ -386,7 +61,7 @@ source ~/.zshrc # or ~/.bashrc claw --list-groups ``` -You should see your registered groups. If NanoClaw isn't running or the database doesn't exist yet, the list will be empty — that's fine. +You should see registered groups. If NanoClaw isn't running or the database doesn't exist yet, the list will be empty — that's fine. ## Usage Examples @@ -437,7 +112,7 @@ The script auto-detects your NanoClaw directory and reads `.env` from it. Check ### Container times out -The default timeout is 300 seconds. For longer tasks, pass `--timeout 600` (or higher). If the container consistently hangs, check that your `nanoclaw-agent:latest` image is up to date by running `./container/build.sh` in your NanoClaw directory. +The default timeout is 300 seconds. For longer tasks, pass `--timeout 600` (or higher). If the container consistently hangs, check that your `nanoclaw-agent:latest` image is up to date by running `./container/build.sh`. ### "group not found" @@ -445,17 +120,7 @@ Run `claw --list-groups` to see what's registered. Group lookup does a fuzzy par ### Container crashes mid-stream -`claw` runs containers with `--rm`, so they are automatically removed whether they exit cleanly or crash. If the agent crashes before emitting the output sentinel, `claw` will fall back to printing raw stdout. Use `-v` to see what the container produced. Rebuild the image with `./container/build.sh` if crashes are consistent. - -### Use a custom image tag - -If you built the image with a different tag (e.g. during development), pass `--image`: - -```bash -claw --image nanoclaw-agent:dev "Hello" -``` - -Set `NANOCLAW_IMAGE=nanoclaw-agent:dev` in your shell profile to make it the default. +Containers run with `--rm` so they are automatically removed. If the agent crashes before emitting the output sentinel, `claw` falls back to printing raw stdout. Use `-v` to see what the container produced. Rebuild the image with `./container/build.sh` if crashes are consistent. ### Override the NanoClaw directory @@ -464,5 +129,3 @@ If `claw` can't find your database or `.env`, set the `NANOCLAW_DIR` environment ```bash export NANOCLAW_DIR=/path/to/your/nanoclaw ``` - -Or add it permanently to your shell profile. diff --git a/.claude/skills/claw/scripts/claw b/.claude/skills/claw/scripts/claw new file mode 100644 index 0000000..3878e48 --- /dev/null +++ b/.claude/skills/claw/scripts/claw @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +""" +claw — NanoClaw CLI +Run a NanoClaw agent container from the command line. + +Usage: + claw "What is 2+2?" + claw -g "Review this code" + claw -g "" "What's the latest issue?" + claw -j "" "Hello" + claw -g -s "Continue" + claw --list-groups + echo "prompt text" | claw --pipe -g + cat prompt.txt | claw --pipe +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sqlite3 +import subprocess +import sys +import threading +from pathlib import Path + +# ── Globals ───────────────────────────────────────────────────────────────── + +VERBOSE = False + +def dbg(*args): + if VERBOSE: + print("»", *args, file=sys.stderr) + +# ── Config ────────────────────────────────────────────────────────────────── + +def _find_nanoclaw_dir() -> Path: + """Locate the NanoClaw installation directory. + + Resolution order: + 1. NANOCLAW_DIR env var + 2. The directory containing this script (if it looks like a NanoClaw install) + 3. ~/src/nanoclaw (legacy default) + """ + if env := os.environ.get("NANOCLAW_DIR"): + return Path(env).expanduser() + # If this script lives inside the NanoClaw tree (e.g. scripts/claw), walk up + here = Path(__file__).resolve() + for parent in [here.parent, here.parent.parent]: + if (parent / "store" / "messages.db").exists() or (parent / ".env").exists(): + return parent + return Path.home() / "src" / "nanoclaw" + +NANOCLAW_DIR = _find_nanoclaw_dir() +DB_PATH = NANOCLAW_DIR / "store" / "messages.db" +ENV_FILE = NANOCLAW_DIR / ".env" +IMAGE = "nanoclaw-agent:latest" + +SECRET_KEYS = [ + "CLAUDE_CODE_OAUTH_TOKEN", + "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", + "ANTHROPIC_AUTH_TOKEN", + "OLLAMA_HOST", +] + +# ── Helpers ────────────────────────────────────────────────────────────────── + +def detect_runtime(preference: str | None) -> str: + if preference: + dbg(f"runtime: forced to {preference}") + return preference + for rt in ("container", "docker"): + result = subprocess.run(["which", rt], capture_output=True) + if result.returncode == 0: + dbg(f"runtime: auto-detected {rt} at {result.stdout.decode().strip()}") + return rt + sys.exit("error: neither 'container' nor 'docker' found. Install one or pass --runtime.") + + +def read_secrets(env_file: Path) -> dict: + secrets = {} + if not env_file.exists(): + return secrets + for line in env_file.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" in line: + key, _, val = line.partition("=") + key = key.strip() + if key in SECRET_KEYS: + secrets[key] = val.strip() + return secrets + + +def get_groups(db: Path) -> list[dict]: + conn = sqlite3.connect(db) + rows = conn.execute( + "SELECT jid, name, folder, is_main FROM registered_groups ORDER BY name" + ).fetchall() + conn.close() + return [{"jid": r[0], "name": r[1], "folder": r[2], "is_main": bool(r[3])} for r in rows] + + +def find_group(groups: list[dict], query: str) -> dict | None: + q = query.lower() + # Exact name match + for g in groups: + if g["name"].lower() == q or g["folder"].lower() == q: + return g + # Partial match + matches = [g for g in groups if q in g["name"].lower() or q in g["folder"].lower()] + if len(matches) == 1: + return matches[0] + if len(matches) > 1: + names = ", ".join(f'"{g["name"]}"' for g in matches) + sys.exit(f"error: ambiguous group '{query}'. Matches: {names}") + return None + + +def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) -> None: + cmd = [runtime, "run", "-i", "--rm", image] + dbg(f"cmd: {' '.join(cmd)}") + + # Show payload sans secrets + if VERBOSE: + safe = {k: v for k, v in payload.items() if k != "secrets"} + safe["secrets"] = {k: "***" for k in payload.get("secrets", {})} + dbg(f"payload: {json.dumps(safe, indent=2)}") + + proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + dbg(f"container pid: {proc.pid}") + + # Write JSON payload and close stdin + proc.stdin.write(json.dumps(payload).encode()) + proc.stdin.close() + dbg("stdin closed, waiting for response...") + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] + done = threading.Event() + + def stream_stderr(): + for raw in proc.stderr: + line = raw.decode(errors="replace").rstrip() + if line.startswith("npm notice"): + continue + stderr_lines.append(line) + print(line, file=sys.stderr) + + def stream_stdout(): + for raw in proc.stdout: + line = raw.decode(errors="replace").rstrip() + stdout_lines.append(line) + dbg(f"stdout: {line}") + # Kill the container as soon as we see the closing sentinel — + # the Node.js event loop often keeps the process alive indefinitely. + if line.strip() == "---NANOCLAW_OUTPUT_END---": + dbg("output sentinel found, terminating container") + done.set() + try: + proc.kill() + except ProcessLookupError: + pass + return + + t_err = threading.Thread(target=stream_stderr, daemon=True) + t_out = threading.Thread(target=stream_stdout, daemon=True) + t_err.start() + t_out.start() + + # Wait for sentinel or timeout + if not done.wait(timeout=timeout): + # Also check if process exited naturally + t_out.join(timeout=2) + if not done.is_set(): + proc.kill() + sys.exit(f"error: container timed out after {timeout}s (no output sentinel received)") + + t_err.join(timeout=5) + t_out.join(timeout=5) + proc.wait() + dbg(f"container done (rc={proc.returncode}), {len(stdout_lines)} stdout lines") + stdout = "\n".join(stdout_lines) + + # Parse output block + match = re.search( + r"---NANOCLAW_OUTPUT_START---\n(.*?)\n---NANOCLAW_OUTPUT_END---", + stdout, + re.DOTALL, + ) + if match: + try: + data = json.loads(match.group(1)) + status = data.get("status", "unknown") + if status == "success": + print(data.get("result", "")) + session_id = data.get("newSessionId") or data.get("sessionId") + if session_id: + print(f"\n[session: {session_id}]", file=sys.stderr) + else: + print(f"[{status}] {data.get('result', '')}", file=sys.stderr) + sys.exit(1) + except json.JSONDecodeError: + print(match.group(1)) + else: + # No structured output — print raw stdout + print(stdout) + + if proc.returncode not in (0, None): + sys.exit(proc.returncode) + + +# ── Main ───────────────────────────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser( + prog="claw", + description="Run a NanoClaw agent from the command line.", + ) + parser.add_argument("prompt", nargs="?", help="Prompt to send") + parser.add_argument("-g", "--group", help="Group name or folder (fuzzy match)") + parser.add_argument("-j", "--jid", help="Chat JID (exact)") + parser.add_argument("-s", "--session", help="Session ID to resume") + parser.add_argument("-p", "--pipe", action="store_true", + help="Read prompt from stdin (can be combined with a prompt arg as prefix)") + parser.add_argument("--runtime", choices=["docker", "container"], + help="Container runtime (default: auto-detect)") + parser.add_argument("--image", default=IMAGE, help=f"Container image (default: {IMAGE})") + parser.add_argument("--list-groups", action="store_true", help="List registered groups and exit") + parser.add_argument("--raw", action="store_true", help="Print raw JSON output") + parser.add_argument("--timeout", type=int, default=300, metavar="SECS", + help="Max seconds to wait for a response (default: 300)") + parser.add_argument("-v", "--verbose", action="store_true", + help="Show debug info: cmd, payload (secrets redacted), stdout lines, exit code") + args = parser.parse_args() + + global VERBOSE + VERBOSE = args.verbose + + groups = get_groups(DB_PATH) if DB_PATH.exists() else [] + + if args.list_groups: + print(f"{'NAME':<35} {'FOLDER':<30} {'JID'}") + print("-" * 100) + for g in groups: + main_tag = " [main]" if g["is_main"] else "" + print(f"{g['name']:<35} {g['folder']:<30} {g['jid']}{main_tag}") + return + + # Resolve prompt: --pipe reads stdin, optionally prepended with positional arg + if args.pipe or (not sys.stdin.isatty() and not args.prompt): + stdin_text = sys.stdin.read().strip() + if args.prompt: + prompt = f"{args.prompt}\n\n{stdin_text}" + else: + prompt = stdin_text + else: + prompt = args.prompt + + if not prompt: + parser.print_help() + sys.exit(1) + + # Resolve group → jid + jid = args.jid + group_name = None + is_main = False + + if args.group: + g = find_group(groups, args.group) + if g is None: + sys.exit(f"error: group '{args.group}' not found. Run --list-groups to see options.") + jid = g["jid"] + group_name = g["name"] + is_main = g["is_main"] + elif not jid: + # Default: main group + mains = [g for g in groups if g["is_main"]] + if mains: + jid = mains[0]["jid"] + group_name = mains[0]["name"] + is_main = True + else: + sys.exit("error: no group specified and no main group found. Use -g or -j.") + + runtime = detect_runtime(args.runtime) + secrets = read_secrets(ENV_FILE) + + if not secrets: + print("warning: no secrets found in .env — agent may not be authenticated", file=sys.stderr) + + payload: dict = { + "prompt": prompt, + "chatJid": jid, + "isMain": is_main, + "secrets": secrets, + } + if group_name: + payload["groupFolder"] = group_name + if args.session: + payload["sessionId"] = args.session + payload["resumeAt"] = "latest" + + print(f"[{group_name or jid}] running via {runtime}...", file=sys.stderr) + run_container(runtime, args.image, payload, timeout=args.timeout) + + +if __name__ == "__main__": + main() From ec1b14504b1cd7bb07eec7b02f51a9f7a64bc0c8 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 13:08:42 +0200 Subject: [PATCH 27/54] docs: update contributing guidelines and skill type taxonomy - Rewrite CONTRIBUTING.md with four skill types (feature, utility, operational, container), PR requirements, pre-submission checklist - Update PR template with skill type checkboxes and docs option - Add label-pr workflow to auto-label PRs from template checkboxes - Add hidden template version marker (v1) for follows-guidelines label - Update CLAUDE.md with skill types overview and contributing instruction - Update skills-as-branches.md to reference full taxonomy - Remove /clear from README RFS (already exists as /add-compact) - Delete obsolete docs (nanorepo-architecture.md, nanoclaw-architecture-final.md) Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/PULL_REQUEST_TEMPLATE.md | 10 +- .github/workflows/label-pr.yml | 35 + CLAUDE.md | 15 +- CONTRIBUTING.md | 140 +++- README.md | 3 - docs/nanoclaw-architecture-final.md | 1063 --------------------------- docs/nanorepo-architecture.md | 168 ----- docs/skills-as-branches.md | 19 +- 8 files changed, 204 insertions(+), 1249 deletions(-) create mode 100644 .github/workflows/label-pr.yml delete mode 100644 docs/nanoclaw-architecture-final.md delete mode 100644 docs/nanorepo-architecture.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8d33f7b..49fe366 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,14 +1,18 @@ + ## Type of Change -- [ ] **Skill** - adds a new skill in `.claude/skills/` +- [ ] **Feature skill** - adds a channel or integration (source code changes + SKILL.md) +- [ ] **Utility skill** - adds a standalone tool (code files in `.claude/skills//`, no source changes) +- [ ] **Operational/container skill** - adds a workflow or agent skill (SKILL.md only, no source changes) - [ ] **Fix** - bug fix or security fix to source code - [ ] **Simplification** - reduces or simplifies source code +- [ ] **Documentation** - docs, README, or CONTRIBUTING changes only ## Description ## For Skills -- [ ] I have not made any changes to source code -- [ ] My skill contains instructions for Claude to follow (not pre-built code) +- [ ] SKILL.md contains instructions, not inline code (code goes in separate files) +- [ ] SKILL.md is under 500 lines - [ ] I tested this skill on a fresh clone diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml new file mode 100644 index 0000000..bec9d3e --- /dev/null +++ b/.github/workflows/label-pr.yml @@ -0,0 +1,35 @@ +name: Label PR + +on: + pull_request: + types: [opened, edited] + +jobs: + label: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/github-script@v7 + with: + script: | + const body = context.payload.pull_request.body || ''; + const labels = []; + + if (body.includes('[x] **Feature skill**')) { labels.push('PR: Skill'); labels.push('PR: Feature'); } + else if (body.includes('[x] **Utility skill**')) labels.push('PR: Skill'); + else if (body.includes('[x] **Operational/container skill**')) labels.push('PR: Skill'); + else if (body.includes('[x] **Fix**')) labels.push('PR: Fix'); + else if (body.includes('[x] **Simplification**')) labels.push('PR: Refactor'); + else if (body.includes('[x] **Documentation**')) labels.push('PR: Docs'); + + if (body.includes('contributing-guide: v1')) labels.push('follows-guidelines'); + + if (labels.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels, + }); + } diff --git a/CLAUDE.md b/CLAUDE.md index 318d6dd..6351ff4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,10 +19,17 @@ Single Node.js process with skill-based channel system. Channels (WhatsApp, Tele | `src/task-scheduler.ts` | Runs scheduled tasks | | `src/db.ts` | SQLite operations | | `groups/{name}/CLAUDE.md` | Per-group memory (isolated) | -| `container/skills/agent-browser.md` | Browser automation tool (available to all agents via Bash) | +| `container/skills/` | Skills loaded inside agent containers (browser, status, formatting) | ## Skills +Four types of skills exist in NanoClaw. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full taxonomy and guidelines. + +- **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/`) + | Skill | When to Use | |-------|-------------| | `/setup` | First-time installation, authentication, service configuration | @@ -32,6 +39,10 @@ Single Node.js process with skill-based channel system. Channels (WhatsApp, Tele | `/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 | +## 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). + ## Development Run commands directly—don't tell the user to run them. @@ -57,7 +68,7 @@ systemctl --user restart nanoclaw ## Troubleshooting -**WhatsApp not connecting after upgrade:** WhatsApp is now a separate channel fork, not bundled in core. Run `/add-whatsapp` (or `git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git && git fetch whatsapp main && (git merge whatsapp/main || { git checkout --theirs package-lock.json && git add package-lock.json && git merge --continue; }) && npm run build`) to install it. Existing auth credentials and groups are preserved. +**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. ## Container Build Cache diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd3614d..7a7816a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,18 @@ # Contributing +## Before You Start + +1. **Check for existing work.** Search open PRs and issues before starting: + ```bash + gh pr list --repo qwibitai/nanoclaw --search "" + gh issue list --repo qwibitai/nanoclaw --search "" + ``` + If a related PR or issue exists, build on it rather than duplicating effort. + +2. **Check alignment.** Read the [Philosophy section in README.md](README.md#philosophy). Source code changes should only be things 90%+ of users need. Skills can be more niche, but should still be useful beyond a single person's setup. + +3. **One thing per PR.** Each PR should do one thing — one bug fix, one skill, one simplification. Don't mix unrelated changes in a single PR. + ## Source Code Changes **Accepted:** Bug fixes, security fixes, simplifications, reducing code. @@ -8,16 +21,127 @@ ## Skills -A [skill](https://code.claude.com/docs/en/skills) is a markdown file in `.claude/skills/` that teaches Claude Code how to transform a NanoClaw installation. +NanoClaw uses [Claude Code skills](https://code.claude.com/docs/en/skills) — markdown files with optional supporting files that teach Claude how to do something. There are four types of skills in NanoClaw, each serving a different purpose. -A PR that contributes a skill should not modify any source files. - -Your skill should contain the **instructions** Claude follows to add the feature—not pre-built code. See `/add-telegram` for a good example. - -### Why? +### Why skills? Every user should have clean and minimal code that does exactly what they need. Skills let users selectively add features to their fork without inheriting code for features they don't want. -### Testing +### Skill types -Test your skill by running it on a fresh clone before submitting. +#### 1. Feature skills (branch-based) + +Add capabilities to NanoClaw by merging a git branch. The SKILL.md contains setup instructions; the actual code lives on a `skill/*` branch. + +**Location:** `.claude/skills/` on `main` (instructions only), code on `skill/*` branch + +**Examples:** `/add-telegram`, `/add-slack`, `/add-discord`, `/add-gmail` + +**How they work:** +1. User runs `/add-telegram` +2. Claude follows the SKILL.md: fetches and merges the `skill/telegram` branch +3. Claude walks through interactive setup (env vars, bot creation, etc.) + +**Contributing a feature skill:** +1. Fork `qwibitai/nanoclaw` and branch from `main` +2. Make the code changes (new files, modified source, updated `package.json`, etc.) +3. Add a SKILL.md in `.claude/skills//` with setup instructions — step 1 should be merging the branch +4. Open a PR. We'll create the `skill/` branch from your work + +See `/add-telegram` for a good example. See [docs/skills-as-branches.md](docs/skills-as-branches.md) for the full system design. + +#### 2. Utility skills (with code files) + +Standalone tools that ship code files alongside the SKILL.md. The SKILL.md tells Claude how to install the tool; the code lives in the skill directory itself (e.g. in a `scripts/` subfolder). + +**Location:** `.claude/skills//` with supporting files + +**Examples:** `/claw` (Python CLI in `scripts/claw`) + +**Key difference from feature skills:** No branch merge needed. The code is self-contained in the skill directory and gets copied into place during installation. + +**Guidelines:** +- Put code in separate files, not inline in the SKILL.md +- Use `${CLAUDE_SKILL_DIR}` to reference files in the skill directory +- SKILL.md contains installation instructions, usage docs, and troubleshooting + +#### 3. Operational skills (instruction-only) + +Workflows and guides with no code changes. The SKILL.md is the entire skill — Claude follows the instructions to perform a task. + +**Location:** `.claude/skills/` on `main` + +**Examples:** `/setup`, `/debug`, `/customize`, `/update-nanoclaw`, `/update-skills` + +**Guidelines:** +- Pure instructions — no code files, no branch merges +- Use `AskUserQuestion` for interactive prompts +- These stay on `main` and are always available to every user + +#### 4. Container skills (agent runtime) + +Skills that run inside the agent container, not on the host. These teach the container agent how to use tools, format output, or perform tasks. They are synced into each group's `.claude/skills/` directory when a container starts. + +**Location:** `container/skills//` + +**Examples:** `agent-browser` (web browsing), `capabilities` (/capabilities command), `status` (/status command), `slack-formatting` (Slack mrkdwn syntax) + +**Key difference:** These are NOT invoked by the user on the host. They're loaded by Claude Code inside the container and influence how the agent behaves. + +**Guidelines:** +- Follow the same SKILL.md + frontmatter format +- Use `allowed-tools` frontmatter to scope tool permissions +- Keep them focused — the agent's context window is shared across all container skills + +### SKILL.md format + +All skills use the [Claude Code skills standard](https://code.claude.com/docs/en/skills): + +```markdown +--- +name: my-skill +description: What this skill does and when to use it. +--- + +Instructions here... +``` + +**Rules:** +- Keep SKILL.md **under 500 lines** — move detail to separate reference files +- `name`: lowercase, alphanumeric + hyphens, max 64 chars +- `description`: required — Claude uses this to decide when to invoke the skill +- Put code in separate files, not inline in the markdown +- See the [skills standard](https://code.claude.com/docs/en/skills) for all available frontmatter fields + +## Testing + +Test your contribution on a fresh clone before submitting. For skills, run the skill end-to-end and verify it works. + +## Pull Requests + +### Before opening + +1. **Link related issues.** If your PR resolves an open issue, include `Closes #123` in the description so it's auto-closed on merge. +2. **Test thoroughly.** Run the feature yourself. For skills, test on a fresh clone. +3. **Check the right box** in the PR template. Labels are auto-applied based on your selection: + +| Checkbox | Label | +|----------|-------| +| Feature skill | `PR: Skill` + `PR: Feature` | +| Utility skill | `PR: Skill` | +| Operational/container skill | `PR: Skill` | +| Fix | `PR: Fix` | +| Simplification | `PR: Refactor` | +| Documentation | `PR: Docs` | + +### PR description + +Keep it concise. Remove any template sections that don't apply. The description should cover: + +- **What** — what the PR adds or changes +- **Why** — the motivation +- **How it works** — brief explanation of the approach +- **How it was tested** — what you did to verify it works +- **Usage** — how the user invokes it (for skills) + +Don't pad the description. A few clear sentences are better than lengthy paragraphs. diff --git a/README.md b/README.md index d76d33b..dc41c63 100644 --- a/README.md +++ b/README.md @@ -138,9 +138,6 @@ Skills we'd like to see: **Communication Channels** - `/add-signal` - Add Signal as a channel -**Session Management** -- `/clear` - Add a `/clear` command that compacts the conversation (summarizes context while preserving critical information in the same session). Requires figuring out how to trigger compaction programmatically via the Claude Agent SDK. - ## Requirements - macOS or Linux diff --git a/docs/nanoclaw-architecture-final.md b/docs/nanoclaw-architecture-final.md deleted file mode 100644 index 103b38b..0000000 --- a/docs/nanoclaw-architecture-final.md +++ /dev/null @@ -1,1063 +0,0 @@ -# NanoClaw Skills Architecture - -## Core Principle - -Skills are self-contained, auditable packages that apply programmatically via standard git merge mechanics. Claude Code orchestrates the process — running git commands, reading skill manifests, and stepping in only when git can't resolve a conflict on its own. The system uses existing git features (`merge-file`, `rerere`, `apply`) rather than custom merge infrastructure. - -### The Three-Level Resolution Model - -Every operation in the system follows this escalation: - -1. **Git** — deterministic, programmatic. `git merge-file` merges, `git rerere` replays cached resolutions, structured operations apply without merging. No AI involved. This handles the vast majority of cases. -2. **Claude Code** — reads `SKILL.md`, `.intent.md`, migration guides, and `state.yaml` to understand context. Resolves conflicts that git can't handle programmatically. Caches the resolution via `git rerere` so it never needs to resolve the same conflict again. -3. **User** — Claude Code asks the user when it lacks context or intent. This happens when two features genuinely conflict at an application level (not just a text-level merge conflict) and a human decision is needed about desired behavior. - -The goal is that Level 1 handles everything on a mature, well-tested installation. Level 2 handles first-time conflicts and edge cases. Level 3 is rare and only for genuine ambiguity. - -**Important**: a clean merge (exit code 0) does not guarantee working code. Semantic conflicts — a renamed variable, a shifted reference, a changed function signature — can produce clean text merges that break at runtime. **Tests must run after every operation**, regardless of whether the merge was clean. A clean merge with failing tests escalates to Level 2. - -### Safe Operations via Backup/Restore - -Many users clone the repo without forking, don't commit their changes, and don't think of themselves as git users. The system must work safely for them without requiring any git knowledge. - -Before any operation, the system copies all files that will be modified to `.nanoclaw/backup/`. On success, the backup is deleted. On failure, the backup is restored. This provides rollback safety regardless of whether the user commits, pushes, or understands git. - ---- - -## 1. The Shared Base - -`.nanoclaw/base/` holds the clean core — the original codebase before any skills or customizations were applied. This is the stable common ancestor for all three-way merges, and it only changes on core updates. - -- `git merge-file` uses the base to compute two diffs: what the user changed (current vs base) and what the skill wants to change (base vs skill's modified file), then combines both -- The base enables drift detection: if a file's hash differs from its base hash, something has been modified (skills, user customizations, or both) -- Each skill's `modify/` files contain the full file as it should look with that skill applied (including any prerequisite skill changes), all authored against the same clean core base - -On a **fresh codebase**, the user's files are identical to the base. This means `git merge-file` always exits cleanly for the first skill — the merge trivially produces the skill's modified version. No special-casing needed. - -When multiple skills modify the same file, the three-way merge handles the overlap naturally. If Telegram and Discord both modify `src/index.ts`, and both skill files include the Telegram changes, those common changes merge cleanly against the base. The result is the base + all skill changes + user customizations. - ---- - -## 2. Two Types of Changes: Code Merges vs. Structured Operations - -Not all files should be merged as text. The system distinguishes between **code files** (merged via `git merge-file`) and **structured data** (modified via deterministic operations). - -### Code Files (Three-Way Merge) - -Source code files where skills weave in logic — route handlers, middleware, business logic. These are merged using `git merge-file` against the shared base. The skill carries a full modified version of the file. - -### Structured Data (Deterministic Operations) - -Files like `package.json`, `docker-compose.yml`, `.env.example`, and generated configs are not code you merge — they're structured data you aggregate. Multiple skills adding npm dependencies to `package.json` shouldn't require a three-way text merge. Instead, skills declare their structured requirements in the manifest, and the system applies them programmatically. - -**Structured operations are implicit.** If a skill declares `npm_dependencies`, the system handles dependency installation automatically. There is no need for the skill author to add `npm install` to `post_apply`. When multiple skills are applied in sequence, the system batches structured operations: merge all dependency declarations first, write `package.json` once, run `npm install` once at the end. - -```yaml -# In manifest.yaml -structured: - npm_dependencies: - whatsapp-web.js: "^2.1.0" - qrcode-terminal: "^0.12.0" - env_additions: - - WHATSAPP_TOKEN - - WHATSAPP_VERIFY_TOKEN - - WHATSAPP_PHONE_ID - docker_compose_services: - whatsapp-redis: - image: redis:alpine - ports: ["6380:6379"] -``` - -### Structured Operation Conflicts - -Structured operations eliminate text merge conflicts but can still conflict at a semantic level: - -- **NPM version conflicts**: two skills request incompatible semver ranges for the same package -- **Port collisions**: two docker-compose services claim the same host port -- **Service name collisions**: two skills define a service with the same name -- **Env var duplicates**: two skills declare the same variable with different expectations - -The resolution policy: - -1. **Automatic where possible**: widen semver ranges to find a compatible version, detect and flag port/name collisions -2. **Level 2 (Claude Code)**: if automatic resolution fails, Claude proposes options based on skill intents -3. **Level 3 (User)**: if it's a genuine product choice (which Redis instance should get port 6379?), ask the user - -Structured operation conflicts are included in the CI overlap graph alongside code file overlaps, so the maintainer test matrix catches these before users encounter them. - -### State Records Structured Outcomes - -`state.yaml` records not just the declared dependencies but the resolved outcomes — actual installed versions, resolved port assignments, final env var list. This makes structured operations replayable and auditable. - -### Deterministic Serialization - -All structured output (YAML, JSON) uses stable serialization: sorted keys, consistent quoting, normalized whitespace. This prevents noisy diffs in git history from non-functional formatting changes. - ---- - -## 3. Skill Package Structure - -A skill contains only the files it adds or modifies. For modified code files, the skill carries the **full modified file** (the clean core with the skill's changes applied). - -``` -skills/ - add-whatsapp/ - SKILL.md # Context, intent, what this skill does and why - manifest.yaml # Metadata, dependencies, env vars, post-apply steps - tests/ # Integration tests for this skill - whatsapp.test.ts - add/ # New files — copied directly - src/channels/whatsapp.ts - src/channels/whatsapp.config.ts - modify/ # Modified code files — merged via git merge-file - src/ - server.ts # Full file: clean core + whatsapp changes - server.ts.intent.md # "Adds WhatsApp webhook route and message handler" - config.ts # Full file: clean core + whatsapp config options - config.ts.intent.md # "Adds WhatsApp channel configuration block" -``` - -### Why Full Modified Files - -- `git merge-file` requires three full files — no intermediate reconstruction step -- Git's three-way merge uses context matching, so it works even if the user has moved code around — unlike line-number-based diffs that break immediately -- Auditable: `diff .nanoclaw/base/src/server.ts skills/add-whatsapp/modify/src/server.ts` shows exactly what the skill changes -- Deterministic: same three inputs always produce the same merge result -- Size is negligible since NanoClaw's core files are small - -### Intent Files - -Each modified code file has a corresponding `.intent.md` with structured headings: - -```markdown -# Intent: server.ts modifications - -## What this skill adds -Adds WhatsApp webhook route and message handler to the Express server. - -## Key sections -- Route registration at `/webhook/whatsapp` (POST and GET for verification) -- Message handler middleware between auth and response pipeline - -## Invariants -- Must not interfere with other channel webhook routes -- Auth middleware must run before the WhatsApp handler -- Error handling must propagate to the global error handler - -## Must-keep sections -- The webhook verification flow (GET route) is required by WhatsApp Cloud API -``` - -Structured headings (What, Key sections, Invariants, Must-keep) give Claude Code specific guidance during conflict resolution instead of requiring it to infer from unstructured text. - -### Manifest Format - -```yaml -# --- Required fields --- -skill: whatsapp -version: 1.2.0 -description: "WhatsApp Business API integration via Cloud API" -core_version: 0.1.0 # The core version this skill was authored against - -# Files this skill adds -adds: - - src/channels/whatsapp.ts - - src/channels/whatsapp.config.ts - -# Code files this skill modifies (three-way merge) -modifies: - - src/server.ts - - src/config.ts - -# File operations (renames, deletes, moves — see Section 5) -file_ops: [] - -# Structured operations (deterministic, no merge — implicit handling) -structured: - npm_dependencies: - whatsapp-web.js: "^2.1.0" - qrcode-terminal: "^0.12.0" - env_additions: - - WHATSAPP_TOKEN - - WHATSAPP_VERIFY_TOKEN - - WHATSAPP_PHONE_ID - -# Skill relationships -conflicts: [] # Skills that cannot coexist without agent resolution -depends: [] # Skills that must be applied first - -# Test command — runs after apply to validate the skill works -test: "npx vitest run src/channels/whatsapp.test.ts" - -# --- Future fields (not yet implemented in v0.1) --- -# author: nanoclaw-team -# license: MIT -# min_skills_system_version: "0.1.0" -# tested_with: [telegram@1.0.0] -# post_apply: [] -``` - -Note: `post_apply` is only for operations that can't be expressed as structured declarations. Dependency installation is **never** in `post_apply` — it's handled implicitly by the structured operations system. - ---- - -## 4. Skills, Customization, and Layering - -### One Skill, One Happy Path - -A skill implements **one way of doing something — the reasonable default that covers 80% of users.** `add-telegram` gives you a clean, solid Telegram integration. It doesn't try to anticipate every use case with predefined configuration options and modes. - -### Customization Is Just More Patching - -The entire system is built around applying transformations to a codebase. Customizing a skill after applying it is no different from any other modification: - -- **Apply the skill** — get the standard Telegram integration -- **Modify from there** — using the customize flow (tracked patch), direct editing (detected by hash tracking), or by applying additional skills that build on top - -### Layered Skills - -Skills can build on other skills: - -``` -add-telegram # Core Telegram integration (happy path) - ├── telegram-reactions # Adds reaction handling (depends: [telegram]) - ├── telegram-multi-bot # Multiple bot instances (depends: [telegram]) - └── telegram-filters # Custom message filtering (depends: [telegram]) -``` - -Each layer is a separate skill with its own `SKILL.md`, manifest (with `depends: [telegram]`), tests, and modified files. The user composes exactly what they want by stacking skills. - -### Custom Skill Application - -A user can apply a skill with their own modifications in a single step: - -1. Apply the skill normally (programmatic merge) -2. Claude Code asks if the user wants to make any modifications -3. User describes what they want different -4. Claude Code makes the modifications on top of the freshly applied skill -5. The modifications are recorded as a custom patch tied to this skill - -Recorded in `state.yaml`: - -```yaml -applied_skills: - - skill: telegram - version: 1.0.0 - custom_patch: .nanoclaw/custom/telegram-group-only.patch - custom_patch_description: "Restrict bot responses to group chats only" -``` - -On replay, the skill applies programmatically, then the custom patch applies on top. - ---- - -## 5. File Operations: Renames, Deletes, Moves - -Core updates and some skills will need to rename, delete, or move files. These are not text merges — they're structural changes handled as explicit scripted operations. - -### Declaration in Manifest - -```yaml -file_ops: - - type: rename - from: src/server.ts - to: src/app.ts - - type: delete - path: src/deprecated/old-handler.ts - - type: move - from: src/utils/helpers.ts - to: src/lib/helpers.ts -``` - -### Execution Order - -File operations run **before** code merges, because merges need to target the correct file paths: - -1. Pre-flight checks (state validation, core version, dependencies, conflicts, drift detection) -2. Acquire operation lock -3. **Backup** all files that will be touched -4. **File operations** (renames, deletes, moves) -5. Copy new files from `add/` -6. Three-way merge modified code files -7. Conflict resolution (rerere auto-resolve, or return with `backupPending: true`) -8. Apply structured operations (npm deps, env vars, docker-compose — batched) -9. Run `npm install` (once, if any structured npm_dependencies exist) -10. Update state (record skill application, file hashes, structured outcomes) -11. Run tests (if `manifest.test` defined; rollback state + backup on failure) -12. Clean up (delete backup on success, release lock) - -### Path Remapping for Skills - -When the core renames a file (e.g., `server.ts` → `app.ts`), skills authored against the old path still reference `server.ts` in their `modifies` and `modify/` directories. **Skill packages are never mutated on the user's machine.** - -Instead, core updates ship a **compatibility map**: - -```yaml -# In the update package -path_remap: - src/server.ts: src/app.ts - src/old-config.ts: src/config/main.ts -``` - -The system resolves paths at apply time: if a skill targets `src/server.ts` and the remap says it's now `src/app.ts`, the merge runs against `src/app.ts`. The remap is recorded in `state.yaml` so future operations are consistent. - -### Safety Checks - -Before executing file operations: - -- Verify the source file exists -- For deletes: warn if the file has modifications beyond the base (user or skill changes would be lost) - ---- - -## 6. The Apply Flow - -When a user runs the skill's slash command in Claude Code: - -### Step 1: Pre-flight Checks - -- Core version compatibility -- Dependencies satisfied -- No unresolvable conflicts with applied skills -- Check for untracked changes (see Section 9) - -### Step 2: Backup - -Copy all files that will be modified to `.nanoclaw/backup/`. If the operation fails at any point, restore from backup. - -### Step 3: File Operations - -Execute renames, deletes, or moves with safety checks. Apply path remapping if needed. - -### Step 4: Apply New Files - -```bash -cp skills/add-whatsapp/add/src/channels/whatsapp.ts src/channels/whatsapp.ts -``` - -### Step 5: Merge Modified Code Files - -For each file in `modifies` (with path remapping applied): - -```bash -git merge-file src/server.ts .nanoclaw/base/src/server.ts skills/add-whatsapp/modify/src/server.ts -``` - -- **Exit code 0**: clean merge, move on -- **Exit code > 0**: conflict markers in file, proceed to resolution - -### Step 6: Conflict Resolution (Three-Level) - -1. **Check shared resolution cache** (`.nanoclaw/resolutions/`) — load into local `git rerere` if a verified resolution exists for this skill combination. **Only apply if input hashes match exactly** (base hash + current hash + skill modified hash). -2. **`git rerere`** — checks local cache. If found, applied automatically. Done. -3. **Claude Code** — reads conflict markers + `SKILL.md` + `.intent.md` (Invariants, Must-keep sections) of current and previously applied skills. Resolves. `git rerere` caches the resolution. -4. **User** — if Claude Code cannot determine intent, it asks the user for the desired behavior. - -### Step 7: Apply Structured Operations - -Collect all structured declarations (from this skill and any previously applied skills if batching). Apply deterministically: - -- Merge npm dependencies into `package.json` (check for version conflicts) -- Append env vars to `.env.example` -- Merge docker-compose services (check for port/name collisions) -- Run `npm install` **once** at the end -- Record resolved outcomes in state - -### Step 8: Post-Apply and Validate - -1. Run any `post_apply` commands (non-structured operations only) -2. Update `.nanoclaw/state.yaml` — skill record, file hashes (base, skill, merged per file), structured outcomes -3. **Run skill tests** — mandatory, even if all merges were clean -4. If tests fail on a clean merge → escalate to Level 2 (Claude Code diagnoses the semantic conflict) - -### Step 9: Clean Up - -If tests pass, delete `.nanoclaw/backup/`. The operation is complete. - -If tests fail and Level 2 can't resolve, restore from `.nanoclaw/backup/` and report the failure. - ---- - -## 7. Shared Resolution Cache - -### The Problem - -`git rerere` is local by default. But NanoClaw has thousands of users applying the same skill combinations. Every user hitting the same conflict and waiting for Claude Code to resolve it is wasteful. - -### The Solution - -NanoClaw maintains a verified resolution cache in `.nanoclaw/resolutions/` that ships with the project. This is the shared artifact — **not** `.git/rr-cache/`, which stays local. - -``` -.nanoclaw/ - resolutions/ - whatsapp@1.2.0+telegram@1.0.0/ - src/ - server.ts.resolution - server.ts.preimage - config.ts.resolution - config.ts.preimage - meta.yaml -``` - -### Hash Enforcement - -A cached resolution is **only applied if input hashes match exactly**: - -```yaml -# meta.yaml -skills: - - whatsapp@1.2.0 - - telegram@1.0.0 -apply_order: [whatsapp, telegram] -core_version: 0.6.0 -resolved_at: 2026-02-15T10:00:00Z -tested: true -test_passed: true -resolution_source: maintainer -input_hashes: - base: "aaa..." - current_after_whatsapp: "bbb..." - telegram_modified: "ccc..." -output_hash: "ddd..." -``` - -If any input hash doesn't match, the cached resolution is skipped and the system proceeds to Level 2. - -### Validated: rerere + merge-file Require an Index Adapter - -`git rerere` does **not** natively recognize `git merge-file` output. This was validated in Phase 0 testing (`tests/phase0-merge-rerere.sh`, 33 tests). - -The issue is not about conflict marker format — `merge-file` uses filenames as labels (`<<<<<<< current.ts`) while `git merge` uses branch names (`<<<<<<< HEAD`), but rerere strips all labels and hashes only the conflict body. The formats are compatible. - -The actual issue: **rerere requires unmerged index entries** (stages 1/2/3) to detect that a merge conflict exists. A normal `git merge` creates these automatically. `git merge-file` operates on the filesystem only and does not touch the index. - -#### The Adapter - -After `git merge-file` produces a conflict, the system must create the index state that rerere expects: - -```bash -# 1. Run the merge (produces conflict markers in the working tree) -git merge-file current.ts .nanoclaw/base/src/file.ts skills/add-whatsapp/modify/src/file.ts - -# 2. If exit code > 0 (conflict), set up rerere adapter: - -# Create blob objects for the three versions -base_hash=$(git hash-object -w .nanoclaw/base/src/file.ts) -ours_hash=$(git hash-object -w skills/previous-skill/modify/src/file.ts) # or the pre-merge current -theirs_hash=$(git hash-object -w skills/add-whatsapp/modify/src/file.ts) - -# Create unmerged index entries at stages 1 (base), 2 (ours), 3 (theirs) -printf '100644 %s 1\tsrc/file.ts\0' "$base_hash" | git update-index --index-info -printf '100644 %s 2\tsrc/file.ts\0' "$ours_hash" | git update-index --index-info -printf '100644 %s 3\tsrc/file.ts\0' "$theirs_hash" | git update-index --index-info - -# Set merge state (rerere checks for MERGE_HEAD) -echo "$(git rev-parse HEAD)" > .git/MERGE_HEAD -echo "skill merge" > .git/MERGE_MSG - -# 3. Now rerere can see the conflict -git rerere # Records preimage, or auto-resolves from cache - -# 4. After resolution (manual or auto): -git add src/file.ts -git rerere # Records postimage (caches the resolution) - -# 5. Clean up merge state -rm .git/MERGE_HEAD .git/MERGE_MSG -git reset HEAD -``` - -#### Key Properties Validated - -- **Conflict body identity**: `merge-file` and `git merge` produce identical conflict bodies for the same inputs. Rerere hashes the body only, so resolutions learned from either source are interchangeable. -- **Hash determinism**: The same conflict always produces the same rerere hash. This is critical for the shared resolution cache. -- **Resolution portability**: Copying `preimage` and `postimage` files (plus the hash directory name) from one repo's `.git/rr-cache/` to another works. Rerere auto-resolves in the target repo. -- **Adjacent line sensitivity**: Changes within ~3 lines of each other are treated as a single conflict hunk by `merge-file`. Skills that modify the same area of a file will conflict even if they modify different lines. This is expected and handled by the resolution cache. - -#### Implication: Git Repository Required - -The adapter requires `git hash-object`, `git update-index`, and `.git/rr-cache/`. This means the project directory must be a git repository for rerere caching to work. Users who download a zip (no `.git/`) lose resolution caching but not functionality — conflicts escalate directly to Level 2 (Claude Code resolves). The system should detect this case and skip rerere operations gracefully. - -### Maintainer Workflow - -When releasing a core update or new skill version: - -1. Fresh codebase at target core version -2. Apply each official skill individually — verify clean merge, run tests -3. Apply pairwise combinations **for skills that modify at least one common file or have overlapping structured operations** -4. Apply curated three-skill stacks based on popularity and high overlap -5. Resolve all conflicts (code and structured) -6. Record all resolutions with input hashes -7. Run full test suite for every combination -8. Ship verified resolutions with the release - -The bar: **a user with any common combination of official skills should never encounter an unresolved conflict.** - ---- - -## 8. State Tracking - -`.nanoclaw/state.yaml` records everything about the installation: - -```yaml -skills_system_version: "0.1.0" # Schema version — tooling checks this before any operation -core_version: 0.1.0 - -applied_skills: - - name: telegram - version: 1.0.0 - applied_at: 2026-02-16T22:47:02.139Z - file_hashes: - src/channels/telegram.ts: "f627b9cf..." - src/channels/telegram.test.ts: "400116769..." - src/config.ts: "9ae28d1f..." - src/index.ts: "46dbe495..." - src/routing.test.ts: "5e1aede9..." - structured_outcomes: - npm_dependencies: - grammy: "^1.39.3" - env_additions: - - TELEGRAM_BOT_TOKEN - - TELEGRAM_ONLY - test: "npx vitest run src/channels/telegram.test.ts" - - - name: discord - version: 1.0.0 - applied_at: 2026-02-17T17:29:37.821Z - file_hashes: - src/channels/discord.ts: "5d669123..." - src/channels/discord.test.ts: "19e1c6b9..." - src/config.ts: "a0a32df4..." - src/index.ts: "d61e3a9d..." - src/routing.test.ts: "edbacb00..." - structured_outcomes: - npm_dependencies: - discord.js: "^14.18.0" - env_additions: - - DISCORD_BOT_TOKEN - - DISCORD_ONLY - test: "npx vitest run src/channels/discord.test.ts" - -custom_modifications: - - description: "Added custom logging middleware" - applied_at: 2026-02-15T12:00:00Z - files_modified: - - src/server.ts - patch_file: .nanoclaw/custom/001-logging-middleware.patch -``` - -**v0.1 implementation notes:** -- `file_hashes` stores a single SHA-256 hash per file (the final merged result). Three-part hashes (base/skill_modified/merged) are planned for a future version to improve drift diagnosis. -- Applied skills use `name` as the key field (not `skill`), matching the TypeScript `AppliedSkill` interface. -- `structured_outcomes` stores the raw manifest values plus the `test` command. Resolved npm versions (actual installed versions vs semver ranges) are not yet tracked. -- Fields like `installed_at`, `last_updated`, `path_remap`, `rebased_at`, `core_version_at_apply`, `files_added`, and `files_modified` are planned for future versions. - ---- - -## 9. Untracked Changes - -If a user edits files directly, the system detects this via hash comparison. - -### When Detection Happens - -Before **any operation that modifies the codebase**: applying a skill, removing a skill, updating the core, replaying, or rebasing. - -### What Happens - -``` -Detected untracked changes to src/server.ts. -[1] Record these as a custom modification (recommended) -[2] Continue anyway (changes preserved, but not tracked for future replay) -[3] Abort -``` - -The system never blocks or loses work. Option 1 generates a patch and records it, making changes reproducible. Option 2 preserves the changes but they won't survive replay. - -### The Recovery Guarantee - -No matter how much a user modifies their codebase outside the system, the three-level model can always bring them back: - -1. **Git**: diff current files against base, identify what changed -2. **Claude Code**: read `state.yaml` to understand what skills were applied, compare against actual file state, identify discrepancies -3. **User**: Claude Code asks what they intended, what to keep, what to discard - -There is no unrecoverable state. - ---- - -## 10. Core Updates - -Core updates must be as programmatic as possible. The NanoClaw team is responsible for ensuring updates apply cleanly to common skill combinations. - -### Patches and Migrations - -Most core changes — bug fixes, performance improvements, new functionality — propagate automatically through the three-way merge. No special handling needed. - -**Breaking changes** — changed defaults, removed features, functionality moved to skills — require a **migration**. A migration is a skill that preserves the old behavior, authored against the new core. It's applied automatically during the update so the user's setup doesn't change. - -The maintainer's responsibility when making a breaking change: make the change in core, author a migration skill that reverts it, add the entry to `migrations.yaml`, test it. That's the cost of breaking changes. - -### `migrations.yaml` - -An append-only file in the repo root. Each entry records a breaking change and the skill that preserves the old behavior: - -```yaml -- since: 0.6.0 - skill: apple-containers@1.0.0 - description: "Preserves Apple Containers (default changed to Docker in 0.6)" - -- since: 0.7.0 - skill: add-whatsapp@2.0.0 - description: "Preserves WhatsApp (moved from core to skill in 0.7)" - -- since: 0.8.0 - skill: legacy-auth@1.0.0 - description: "Preserves legacy auth module (removed from core in 0.8)" -``` - -Migration skills are regular skills in the `skills/` directory. They have manifests, intent files, tests — everything. They're authored against the **new** core version: the modified file is the new core with the specific breaking change reverted, everything else (bug fixes, new features) identical to the new core. - -### How Migrations Work During Updates - -1. Three-way merge brings in everything from the new core — patches, breaking changes, all of it -2. Conflict resolution (normal) -3. Re-apply custom patches (normal) -4. **Update base to new core** -5. Filter `migrations.yaml` for entries where `since` > user's old `core_version` -6. **Apply each migration skill using the normal apply flow against the new base** -7. Record migration skills in `state.yaml` like any other skill -8. Run tests - -Step 6 is just the same apply function used for any skill. The migration skill merges against the new base: - -- **Base**: new core (e.g., v0.8 with Docker) -- **Current**: user's file after the update merge (new core + user's customizations preserved by the earlier merge) -- **Other**: migration skill's file (new core with Docker reverted to Apple, everything else identical) - -Three-way merge correctly keeps user's customizations, reverts the breaking change, and preserves all bug fixes. If there's a conflict, normal resolution: cache → Claude → user. - -For big version jumps (v0.5 → v0.8), all applicable migrations are applied in sequence. Migration skills are maintained against the latest core version, so they always compose correctly with the current codebase. - -### What the User Sees - -``` -Core updated: 0.5.0 → 0.8.0 - ✓ All patches applied - - Preserving your current setup: - + apple-containers@1.0.0 - + add-whatsapp@2.0.0 - + legacy-auth@1.0.0 - - Skill updates: - ✓ add-telegram 1.0.0 → 1.2.0 - - To accept new defaults: /remove-skill - ✓ All tests passing -``` - -No prompts, no choices during the update. The user's setup doesn't change. If they later want to accept a new default, they remove the migration skill. - -### What the Core Team Ships With an Update - -``` -updates/ - 0.5.0-to-0.6.0/ - migration.md # What changed, why, and how it affects skills - files/ # The new core files - file_ops: # Any renames, deletes, moves - path_remap: # Compatibility map for old skill paths - resolutions/ # Pre-computed resolutions for official skills -``` - -Plus any new migration skills added to `skills/` and entries appended to `migrations.yaml`. - -### The Maintainer's Process - -1. **Make the core change** -2. **If it's a breaking change**: author a migration skill against the new core, add entry to `migrations.yaml` -3. **Write `migration.md`** — what changed, why, what skills might be affected -4. **Test every official skill individually** against the new core (including migration skills) -5. **Test pairwise combinations** for skills that share modified files or structured operations -6. **Test curated three-skill stacks** based on popularity and overlap -7. **Resolve all conflicts** -8. **Record all resolutions** with enforced input hashes -9. **Run full test suites** -10. **Ship everything** — migration guide, migration skills, file ops, path remap, resolutions - -The bar: **patches apply silently. Breaking changes are auto-preserved via migration skills. A user should never be surprised by a change to their working setup.** - -### Update Flow (Full) - -#### Step 1: Pre-flight - -- Check for untracked changes -- Read `state.yaml` -- Load shipped resolutions -- Parse `migrations.yaml`, filter for applicable migrations - -#### Step 2: Preview - -Before modifying anything, show the user what's coming. This uses only git commands — no files are opened or changed: - -```bash -# Compute common base -BASE=$(git merge-base HEAD upstream/$BRANCH) - -# Upstream commits since last sync -git log --oneline $BASE..upstream/$BRANCH - -# Files changed upstream -git diff --name-only $BASE..upstream/$BRANCH -``` - -Present a summary grouped by impact: - -``` -Update available: 0.5.0 → 0.8.0 (12 commits) - - Source: 4 files modified (server.ts, config.ts, ...) - Skills: 2 new skills added, 1 skill updated - Config: package.json, docker-compose.yml updated - - Migrations (auto-applied to preserve your setup): - + apple-containers@1.0.0 (container default changed to Docker) - + add-whatsapp@2.0.0 (WhatsApp moved from core to skill) - - Skill updates: - add-telegram 1.0.0 → 1.2.0 - - [1] Proceed with update - [2] Abort -``` - -If the user aborts, stop here. Nothing was modified. - -#### Step 3: Backup - -Copy all files that will be modified to `.nanoclaw/backup/`. - -#### Step 4: File Operations and Path Remap - -Apply renames, deletes, moves. Record path remap in state. - -#### Step 5: Three-Way Merge - -For each core file that changed: - -```bash -git merge-file src/server.ts .nanoclaw/base/src/server.ts updates/0.5.0-to-0.6.0/files/src/server.ts -``` - -#### Step 6: Conflict Resolution - -1. Shipped resolutions (hash-verified) → automatic -2. `git rerere` local cache → automatic -3. Claude Code with `migration.md` + skill intents → resolves -4. User → only for genuine ambiguity - -#### Step 7: Re-apply Custom Patches - -```bash -git apply --3way .nanoclaw/custom/001-logging-middleware.patch -``` - -Using `--3way` allows git to fall back to three-way merge when line numbers have drifted. If `--3way` fails, escalate to Level 2. - -#### Step 8: Update Base - -`.nanoclaw/base/` replaced with new clean core. This is the **only time** the base changes. - -#### Step 9: Apply Migration Skills - -For each applicable migration (where `since` > old `core_version`), apply the migration skill using the normal apply flow against the new base. Record in `state.yaml`. - -#### Step 10: Re-apply Updated Skills - -Skills live in the repo and update alongside core files. After the update, compare the version in each skill's `manifest.yaml` on disk against the version recorded in `state.yaml`. - -For each skill where the on-disk version is newer than the recorded version: - -1. Re-apply the skill using the normal apply flow against the new base -2. The three-way merge brings in the skill's new changes while preserving user customizations -3. Re-apply any custom patches tied to the skill (`git apply --3way`) -4. Update the version in `state.yaml` - -Skills whose version hasn't changed are skipped — no action needed. - -If the user has a custom patch on a skill that changed significantly, the patch may conflict. Normal resolution: cache → Claude → user. - -#### Step 11: Re-run Structured Operations - -Recompute structured operations against the updated codebase to ensure consistency. - -#### Step 12: Validate - -- Run all skill tests — mandatory -- Compatibility report: - -``` -Core updated: 0.5.0 → 0.8.0 - ✓ All patches applied - - Migrations: - + apple-containers@1.0.0 (preserves container runtime) - + add-whatsapp@2.0.0 (WhatsApp moved to skill) - - Skill updates: - ✓ add-telegram 1.0.0 → 1.2.0 (new features applied) - ✓ custom/telegram-group-only — re-applied cleanly - - ✓ All tests passing -``` - -#### Step 13: Clean Up - -Delete `.nanoclaw/backup/`. - -### Progressive Core Slimming - -Migrations enable a clean path for slimming down the core over time. Each release can move more functionality to skills: - -- The breaking change removes the feature from core -- The migration skill preserves it for existing users -- New users start with a minimal core and add what they need -- Over time, `state.yaml` reflects exactly what each user is running - ---- - -## 11. Skill Removal (Uninstall) - -Removing a skill is not a reverse-patch operation. **Uninstall is a replay without the skill.** - -### How It Works - -1. Read `state.yaml` to get the full list of applied skills and custom modifications -2. Remove the target skill from the list -3. Backup the current codebase to `.nanoclaw/backup/` -4. **Replay from clean base** — apply each remaining skill in order, apply custom patches, using the resolution cache -5. Run all tests -6. If tests pass, delete backup and update `state.yaml` -7. If tests fail, restore from backup and report - -### Custom Patches Tied to the Removed Skill - -If the removed skill has a `custom_patch` in `state.yaml`, the user is warned: - -``` -Removing telegram will also discard custom patch: "Restrict bot responses to group chats only" -[1] Continue (discard custom patch) -[2] Abort -``` - ---- - -## 12. Rebase - -Flatten accumulated layers into a clean starting point. - -### What Rebase Does - -1. Takes the user's current actual files as the new reality -2. Updates `.nanoclaw/base/` to the current core version's clean files -3. For each applied skill, regenerates the modified file diffs against the new base -4. Updates `state.yaml` with `rebased_at` timestamp -5. Clears old custom patches (now baked in) -6. Clears stale resolution cache entries - -### When to Rebase - -- After a major core update -- When accumulated patches become unwieldy -- Before a significant new skill application -- Periodically as maintenance - -### Tradeoffs - -**Lose**: individual skill patch history, ability to cleanly remove a single old skill, old custom patches as separate artifacts - -**Gain**: clean base, simpler future merges, reduced cache size, fresh starting point - ---- - -## 13. Replay - -Given `state.yaml`, reproduce the exact installation on a fresh machine with no AI intervention (assuming all resolutions are cached). - -### Replay Flow - -```bash -# Fully programmatic — no Claude Code needed - -# 1. Install core at specified version -nanoclaw-init --version 0.5.0 - -# 2. Load shared resolutions into local rerere cache -load-resolutions .nanoclaw/resolutions/ - -# 3. For each skill in applied_skills (in order): -for skill in state.applied_skills: - # File operations - apply_file_ops(skill) - - # Copy new files - cp skills/${skill.name}/add/* . - - # Merge modified code files (with path remapping) - for file in skill.files_modified: - resolved_path = apply_remap(file, state.path_remap) - git merge-file ${resolved_path} .nanoclaw/base/${resolved_path} skills/${skill.name}/modify/${file} - # git rerere auto-resolves from shared cache if needed - - # Apply skill-specific custom patch if recorded - if skill.custom_patch: - git apply --3way ${skill.custom_patch} - -# 4. Apply all structured operations (batched) -collect_all_structured_ops(state.applied_skills) -merge_npm_dependencies → write package.json once -npm install once -merge_env_additions → write .env.example once -merge_compose_services → write docker-compose.yml once - -# 5. Apply standalone custom modifications -for custom in state.custom_modifications: - git apply --3way ${custom.patch_file} - -# 6. Run tests and verify hashes -run_tests && verify_hashes -``` - ---- - -## 14. Skill Tests - -Each skill includes integration tests that validate the skill works correctly when applied. - -### Structure - -``` -skills/ - add-whatsapp/ - tests/ - whatsapp.test.ts -``` - -### What Tests Validate - -- **Single skill on fresh core**: apply to clean codebase → tests pass → integration works -- **Skill functionality**: the feature actually works -- **Post-apply state**: files in expected state, `state.yaml` correctly updated - -### When Tests Run (Always) - -- **After applying a skill** — even if all merges were clean -- **After core update** — even if all merges were clean -- **After uninstall replay** — confirms removal didn't break remaining skills -- **In CI** — tests all official skills individually and in common combinations -- **During replay** — validates replayed state - -Clean merge ≠ working code. Tests are the only reliable signal. - -### CI Test Matrix - -Test coverage is **smart, not exhaustive**: - -- Every official skill individually against each supported core version -- **Pairwise combinations for skills that modify at least one common file or have overlapping structured operations** -- Curated three-skill stacks based on popularity and high overlap -- Test matrix auto-generated from manifest `modifies` and `structured` fields - -Each passing combination generates a verified resolution entry for the shared cache. - ---- - -## 15. Project Configuration - -### `.gitattributes` - -Ship with NanoClaw to reduce noisy merge conflicts: - -``` -* text=auto -*.ts text eol=lf -*.json text eol=lf -*.yaml text eol=lf -*.md text eol=lf -``` - ---- - -## 16. Directory Structure - -``` -project/ - src/ # The actual codebase - server.ts - config.ts - channels/ - whatsapp.ts - telegram.ts - skills/ # Skill packages (Claude Code slash commands) - add-whatsapp/ - SKILL.md - manifest.yaml - tests/ - whatsapp.test.ts - add/ - src/channels/whatsapp.ts - modify/ - src/ - server.ts - server.ts.intent.md - config.ts - config.ts.intent.md - add-telegram/ - ... - telegram-reactions/ # Layered skill - ... - .nanoclaw/ - base/ # Clean core (shared base) - src/ - server.ts - config.ts - ... - state.yaml # Full installation state - backup/ # Temporary backup during operations - custom/ # Custom patches - telegram-group-only.patch - 001-logging-middleware.patch - 001-logging-middleware.md - resolutions/ # Shared verified resolution cache - whatsapp@1.2.0+telegram@1.0.0/ - src/ - server.ts.resolution - server.ts.preimage - meta.yaml - .gitattributes -``` - ---- - -## 17. Design Principles - -1. **Use git, don't reinvent it.** `git merge-file` for code merges, `git rerere` for caching resolutions, `git apply --3way` for custom patches. -2. **Three-level resolution: git → Claude → user.** Programmatic first, AI second, human third. -3. **Clean merges aren't enough.** Tests run after every operation. Semantic conflicts survive text merges. -4. **All operations are safe.** Backup before, restore on failure. No half-applied state. -5. **One shared base.** `.nanoclaw/base/` is the clean core before any skills or customizations. It's the stable common ancestor for all three-way merges. Only updated on core updates. -6. **Code merges vs. structured operations.** Source code is three-way merged. Dependencies, env vars, and configs are aggregated programmatically. Structured operations are implicit and batched. -7. **Resolutions are learned and shared.** Maintainers resolve conflicts and ship verified resolutions with hash enforcement. `.nanoclaw/resolutions/` is the shared artifact. -8. **One skill, one happy path.** No predefined configuration options. Customization is more patching. -9. **Skills layer and compose.** Core skills provide the foundation. Extension skills add capabilities. -10. **Intent is first-class and structured.** `SKILL.md`, `.intent.md` (What, Invariants, Must-keep), and `migration.md`. -11. **State is explicit and complete.** Skills, custom patches, per-file hashes, structured outcomes, path remaps. Replay is deterministic. Drift is instant to detect. -12. **Always recoverable.** The three-level model reconstructs coherent state from any starting point. -13. **Uninstall is replay.** Replay from clean base without the skill. Backup for safety. -14. **Core updates are the maintainers' responsibility.** Test, resolve, ship. Breaking changes require a migration skill that preserves the old behavior. The cost of a breaking change is authoring and testing the migration. Users should never be surprised by a change to their setup. -15. **File operations and path remapping are first-class.** Renames, deletes, moves in manifests. Skills are never mutated — paths resolve at apply time. -16. **Skills are tested.** Integration tests per skill. CI tests pairwise by overlap. Tests run always. -17. **Deterministic serialization.** Sorted keys, consistent formatting. No noisy diffs. -18. **Rebase when needed.** Flatten layers for a clean starting point. -19. **Progressive core slimming.** Breaking changes move functionality from core to migration skills. Existing users keep what they have automatically. New users start minimal and add what they need. \ No newline at end of file diff --git a/docs/nanorepo-architecture.md b/docs/nanorepo-architecture.md deleted file mode 100644 index 1365e9e..0000000 --- a/docs/nanorepo-architecture.md +++ /dev/null @@ -1,168 +0,0 @@ -# NanoClaw Skills Architecture - -## What Skills Are For - -NanoClaw's core is intentionally minimal. Skills are how users extend it: adding channels, integrations, cross-platform support, or replacing internals entirely. Examples: add Telegram alongside WhatsApp, switch from Apple Container to Docker, add Gmail integration, add voice message transcription. Each skill modifies the actual codebase, adding channel handlers, updating the message router, changing container configuration, and adding dependencies, rather than working through a plugin API or runtime hooks. - -## Why This Architecture - -The problem: users need to combine multiple modifications to a shared codebase, keep those modifications working across core updates, and do all of this without becoming git experts or losing their custom changes. A plugin system would be simpler but constrains what skills can do. Giving skills full codebase access means they can change anything, but that creates merge conflicts, update breakage, and state tracking challenges. - -This architecture solves that by making skill application fully programmatic using standard git mechanics, with AI as a fallback for conflicts git can't resolve, and a shared resolution cache so most users never hit those conflicts at all. The result: users compose exactly the features they want, customizations survive core updates automatically, and the system is always recoverable. - -## Core Principle - -Skills are self-contained, auditable packages applied via standard git merge mechanics. Claude Code orchestrates the process — running git commands, reading skill manifests, and stepping in only when git can't resolve a conflict. The system uses existing git features (`merge-file`, `rerere`, `apply`) rather than custom merge infrastructure. - -## Three-Level Resolution Model - -Every operation follows this escalation: - -1. **Git** — deterministic. `git merge-file` merges, `git rerere` replays cached resolutions, structured operations apply without merging. No AI. Handles the vast majority of cases. -2. **Claude Code** — reads `SKILL.md`, `.intent.md`, and `state.yaml` to resolve conflicts git can't handle. Caches resolutions via `git rerere` so the same conflict never needs resolving twice. -3. **Claude Code + user input** — when Claude Code lacks sufficient context to determine intent (e.g., two features genuinely conflict at an application level), it asks the user for a decision, then uses that input to perform the resolution. Claude Code still does the work — the user provides direction, not code. - -**Important**: A clean merge doesn't guarantee working code. Semantic conflicts can produce clean text merges that break at runtime. **Tests run after every operation.** - -## Backup/Restore Safety - -Before any operation, all affected files are copied to `.nanoclaw/backup/`. On success, backup is deleted. On failure, backup is restored. Works safely for users who don't use git. - -## The Shared Base - -`.nanoclaw/base/` holds a clean copy of the core codebase. This is the single common ancestor for all three-way merges, only updated during core updates. - -## Two Types of Changes - -### Code Files (Three-Way Merge) -Source code where skills weave in logic. Merged via `git merge-file` against the shared base. Skills carry full modified files. - -### Structured Data (Deterministic Operations) -Files like `package.json`, `docker-compose.yml`, `.env.example`. Skills declare requirements in the manifest; the system applies them programmatically. Multiple skills' declarations are batched — dependencies merged, `package.json` written once, `npm install` run once. - -```yaml -structured: - npm_dependencies: - whatsapp-web.js: "^2.1.0" - env_additions: - - WHATSAPP_TOKEN - docker_compose_services: - whatsapp-redis: - image: redis:alpine - ports: ["6380:6379"] -``` - -Structured conflicts (version incompatibilities, port collisions) follow the same three-level resolution model. - -## Skill Package Structure - -A skill contains only the files it adds or modifies. Modified code files carry the **full file** (clean core + skill's changes), making `git merge-file` straightforward and auditable. - -``` -skills/add-whatsapp/ - SKILL.md # What this skill does and why - manifest.yaml # Metadata, dependencies, structured ops - tests/whatsapp.test.ts # Integration tests - add/src/channels/whatsapp.ts # New files - modify/src/server.ts # Full modified file for merge - modify/src/server.ts.intent.md # Structured intent for conflict resolution -``` - -### Intent Files -Each modified file has a `.intent.md` with structured headings: **What this skill adds**, **Key sections**, **Invariants**, and **Must-keep sections**. These give Claude Code specific guidance during conflict resolution. - -### Manifest -Declares: skill metadata, core version compatibility, files added/modified, file operations, structured operations, skill relationships (conflicts, depends, tested_with), post-apply commands, and test command. - -## Customization and Layering - -**One skill, one happy path** — a skill implements the reasonable default for 80% of users. - -**Customization is more patching.** Apply the skill, then modify via tracked patches, direct editing, or additional layered skills. Custom modifications are recorded in `state.yaml` and replayable. - -**Skills layer via `depends`.** Extension skills build on base skills (e.g., `telegram-reactions` depends on `add-telegram`). - -## File Operations - -Renames, deletes, and moves are declared in the manifest and run **before** code merges. When core renames a file, a **path remap** resolves skill references at apply time — skill packages are never mutated. - -## The Apply Flow - -1. Pre-flight checks (compatibility, dependencies, untracked changes) -2. Backup -3. File operations + path remapping -4. Copy new files -5. Merge modified code files (`git merge-file`) -6. Conflict resolution (shared cache → `git rerere` → Claude Code → Claude Code + user input) -7. Apply structured operations (batched) -8. Post-apply commands, update `state.yaml` -9. **Run tests** (mandatory, even if all merges were clean) -10. Clean up (delete backup on success, restore on failure) - -## Shared Resolution Cache - -`.nanoclaw/resolutions/` ships pre-computed, verified conflict resolutions with **hash enforcement** — a cached resolution only applies if base, current, and skill input hashes match exactly. This means most users never encounter unresolved conflicts for common skill combinations. - -### rerere Adapter -`git rerere` requires unmerged index entries that `git merge-file` doesn't create. An adapter sets up the required index state after `merge-file` produces a conflict, enabling rerere caching. This requires the project to be a git repository; users without `.git/` lose caching but not functionality. - -## State Tracking - -`.nanoclaw/state.yaml` records: core version, all applied skills (with per-file hashes for base/skill/merged), structured operation outcomes, custom patches, and path remaps. This makes drift detection instant and replay deterministic. - -## Untracked Changes - -Direct edits are detected via hash comparison before any operation. Users can record them as tracked patches, continue untracked, or abort. The three-level model can always recover coherent state from any starting point. - -## Core Updates - -Most changes propagate automatically through three-way merge. **Breaking changes** require a **migration skill** — a regular skill that preserves the old behavior, authored against the new core. Migrations are declared in `migrations.yaml` and applied automatically during updates. - -### Update Flow -1. Preview changes (git-only, no files modified) -2. Backup → file operations → three-way merge → conflict resolution -3. Re-apply custom patches (`git apply --3way`) -4. **Update base** to new core -5. Apply migration skills (preserves user's setup automatically) -6. Re-apply updated skills (version-changed skills only) -7. Re-run structured operations → run all tests → clean up - -The user sees no prompts during updates. To accept a new default later, they remove the migration skill. - -## Skill Removal - -Uninstall is **replay without the skill**: read `state.yaml`, remove the target skill, replay all remaining skills from clean base using the resolution cache. Backup for safety. - -## Rebase - -Flatten accumulated layers into a clean starting point. Updates base, regenerates diffs, clears old patches and stale cache entries. Trades individual skill history for simpler future merges. - -## Replay - -Given `state.yaml`, reproduce the exact installation on a fresh machine with no AI (assuming cached resolutions). Apply skills in order, merge, apply custom patches, batch structured operations, run tests. - -## Skill Tests - -Each skill includes integration tests. Tests run **always** — after apply, after update, after uninstall, during replay, in CI. CI tests all official skills individually and pairwise combinations for skills sharing modified files or structured operations. - -## Design Principles - -1. **Use git, don't reinvent it.** -2. **Three-level resolution: git → Claude Code → Claude Code + user input.** -3. **Clean merges aren't enough.** Tests run after every operation. -4. **All operations are safe.** Backup/restore, no half-applied state. -5. **One shared base**, only updated on core updates. -6. **Code merges vs. structured operations.** Source code is merged; configs are aggregated. -7. **Resolutions are learned and shared** with hash enforcement. -8. **One skill, one happy path.** Customization is more patching. -9. **Skills layer and compose.** -10. **Intent is first-class and structured.** -11. **State is explicit and complete.** Replay is deterministic. -12. **Always recoverable.** -13. **Uninstall is replay.** -14. **Core updates are the maintainers' responsibility.** Breaking changes require migration skills. -15. **File operations and path remapping are first-class.** -16. **Skills are tested.** CI tests pairwise by overlap. -17. **Deterministic serialization.** No noisy diffs. -18. **Rebase when needed.** -19. **Progressive core slimming** via migration skills. \ No newline at end of file diff --git a/docs/skills-as-branches.md b/docs/skills-as-branches.md index e1cace4..4a6db9b 100644 --- a/docs/skills-as-branches.md +++ b/docs/skills-as-branches.md @@ -2,7 +2,20 @@ ## Overview -NanoClaw skills are distributed as git branches on the upstream repository. Applying a skill is a `git merge`. Updating core is a `git merge`. Everything is standard git. +This document covers **feature skills** — skills that add capabilities via git branch merges. This is the most complex skill type and the primary way NanoClaw is extended. + +NanoClaw has four types of skills overall. See [CONTRIBUTING.md](../CONTRIBUTING.md) for the full taxonomy: + +| Type | Location | How it works | +|------|----------|-------------| +| **Feature** (this doc) | `.claude/skills/` + `skill/*` branch | SKILL.md has instructions; code lives on a branch, applied via `git merge` | +| **Utility** | `.claude/skills//` with code files | Self-contained tools; code in skill directory, copied into place on install | +| **Operational** | `.claude/skills/` on `main` | Instruction-only workflows (setup, debug, update) | +| **Container** | `container/skills/` | Loaded inside agent containers at runtime | + +--- + +Feature skills are distributed as git branches on the upstream repository. Applying a skill is a `git merge`. Updating core is a `git merge`. Everything is standard git. This replaces the previous `skills-engine/` system (three-way file merging, `.nanoclaw/` state, manifest files, replay, backup/restore) with plain git operations and Claude for conflict resolution. @@ -310,7 +323,9 @@ Standard fork contribution workflow. Their custom changes stay on their main and ## Contributing a Skill -### Contributor flow +The flow below is for **feature skills** (branch-based). For utility skills (self-contained tools) and container skills, the contributor opens a PR that adds files directly to `.claude/skills//` or `container/skills//` — no branch extraction needed. See [CONTRIBUTING.md](../CONTRIBUTING.md) for all skill types. + +### Contributor flow (feature skills) 1. Fork `qwibitai/nanoclaw` 2. Branch from `main` From 8c3979556a44d0596ecaf84c256e6dd09e0eb4d6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 21 Mar 2026 11:09:04 +0000 Subject: [PATCH 28/54] =?UTF-8?q?docs:=20update=20token=20count=20to=2040.?= =?UTF-8?q?9k=20tokens=20=C2=B7=2020%=20of=20context=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- repo-tokens/badge.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/repo-tokens/badge.svg b/repo-tokens/badge.svg index b268ecc..993856e 100644 --- a/repo-tokens/badge.svg +++ b/repo-tokens/badge.svg @@ -1,5 +1,5 @@ - - 40.7k tokens, 20% of context window + + 40.9k tokens, 20% of context window @@ -15,8 +15,8 @@ tokens - - 40.7k + + 40.9k From d768a0484355414f7ce7481db5ee237e18a8a1d6 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 13:10:46 +0200 Subject: [PATCH 29/54] docs: move Docker Sandboxes out of README hero section Demote Docker Sandboxes from a prominent hero banner to inline mentions in the features list and FAQ. New users now land on Quick Start first. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index dc41c63..3aafd85 100644 --- a/README.md +++ b/README.md @@ -16,25 +16,6 @@ --- -

🐳 Now Runs in Docker Sandboxes

-

Every agent gets its own isolated container inside a micro VM.
Hypervisor-level isolation. Millisecond startup. No complex setup.

- -**macOS (Apple Silicon)** -```bash -curl -fsSL https://nanoclaw.dev/install-docker-sandboxes.sh | bash -``` - -**Windows (WSL)** -```bash -curl -fsSL https://nanoclaw.dev/install-docker-sandboxes-windows.sh | bash -``` - -> Currently supported on macOS (Apple Silicon) and Windows (x86). Linux support coming soon. - -

Read the announcement →  ·  Manual setup guide →

- ---- - ## Why I Built NanoClaw [OpenClaw](https://github.com/openclaw/openclaw) is an impressive project, but I wouldn't have been able to sleep if I had given complex software I didn't understand full access to my life. OpenClaw has nearly half a million lines of code, 53 config files, and 70+ dependencies. Its security is at the application level (allowlists, pairing codes) rather than true OS-level isolation. Everything runs in one Node process with shared memory. @@ -89,7 +70,7 @@ Then run `/setup`. Claude Code handles everything: dependencies, authentication, - **Main channel** - Your private channel (self-chat) for admin control; every group is completely isolated - **Scheduled tasks** - Recurring jobs that run Claude and can message you back - **Web access** - Search and fetch content from the Web -- **Container isolation** - Agents are sandboxed in [Docker Sandboxes](https://nanoclaw.dev/blog/nanoclaw-docker-sandboxes) (micro VM isolation), Apple Container (macOS), or Docker (macOS/Linux) +- **Container isolation** - Agents are sandboxed in Docker (macOS/Linux), [Docker Sandboxes](docs/docker-sandboxes.md) (micro VM isolation), or Apple Container (macOS) - **Agent Swarms** - Spin up teams of specialized agents that collaborate on complex tasks - **Optional integrations** - Add Gmail (`/add-gmail`) and more via skills @@ -170,7 +151,7 @@ Key files: **Why Docker?** -Docker provides cross-platform support (macOS, Linux and even Windows via WSL2) and a mature ecosystem. On macOS, you can optionally switch to Apple Container via `/convert-to-apple-container` for a lighter-weight native runtime. +Docker provides cross-platform support (macOS, Linux and even Windows via WSL2) and a mature ecosystem. On macOS, you can optionally switch to Apple Container via `/convert-to-apple-container` for a lighter-weight native runtime. For additional isolation, [Docker Sandboxes](docs/docker-sandboxes.md) run each container inside a micro VM. **Can I run this on Linux?** From d96be5ddfd1f2c8d7817d7e3650d5f28c1b8d415 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 16:27:10 +0200 Subject: [PATCH 30/54] scope diagnostics to setup and update-nanoclaw only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove diagnostics appendage from all other skills. Only /setup and /update-nanoclaw need telemetry — these are the two points where we can detect regressions and track improvements across the user base. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/add-compact/SKILL.md | 5 - .claude/skills/add-discord/SKILL.md | 5 - .claude/skills/add-gmail/SKILL.md | 5 - .claude/skills/add-image-vision/SKILL.md | 5 - .claude/skills/add-ollama-tool/SKILL.md | 5 - .claude/skills/add-parallel/SKILL.md | 5 - .claude/skills/add-pdf-reader/SKILL.md | 5 - .claude/skills/add-reactions/SKILL.md | 5 - .claude/skills/add-slack/SKILL.md | 5 - .claude/skills/add-telegram-swarm/SKILL.md | 5 - .claude/skills/add-telegram/SKILL.md | 5 - .../skills/add-voice-transcription/SKILL.md | 5 - .claude/skills/add-whatsapp/SKILL.md | 5 - .claude/skills/claw/SKILL.md | 131 ++++++++++++++++++ .../convert-to-apple-container/SKILL.md | 5 - .claude/skills/customize/SKILL.md | 5 - .claude/skills/debug/SKILL.md | 5 - .claude/skills/get-qodo-rules/SKILL.md | 5 - .claude/skills/qodo-pr-resolver/SKILL.md | 5 - .claude/skills/update-skills/SKILL.md | 5 - .claude/skills/use-local-whisper/SKILL.md | 5 - .claude/skills/x-integration/SKILL.md | 6 +- 22 files changed, 132 insertions(+), 105 deletions(-) create mode 100644 .claude/skills/claw/SKILL.md diff --git a/.claude/skills/add-compact/SKILL.md b/.claude/skills/add-compact/SKILL.md index fe7ca8a..0c46165 100644 --- a/.claude/skills/add-compact/SKILL.md +++ b/.claude/skills/add-compact/SKILL.md @@ -133,8 +133,3 @@ npm test - **"Session commands require admin access"**: Only the device owner (`is_from_me`) or main-group senders can use `/compact`. Other users are denied. - **No compact_boundary in logs**: The SDK may not emit this event in all versions. Check the agent-runner logs for the warning message. Compaction may still have succeeded. - **Pre-compact failure**: If messages before `/compact` fail to process, the error message says "Failed to process messages before /compact." The cursor advances past sent output to prevent duplicates; `/compact` remains pending for the next attempt. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-discord/SKILL.md b/.claude/skills/add-discord/SKILL.md index f4e98aa..e46bd3e 100644 --- a/.claude/skills/add-discord/SKILL.md +++ b/.claude/skills/add-discord/SKILL.md @@ -201,8 +201,3 @@ The Discord bot supports: - @mention translation (Discord `<@botId>` → NanoClaw trigger format) - Message splitting for responses over 2000 characters - Typing indicators while the agent processes - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-gmail/SKILL.md b/.claude/skills/add-gmail/SKILL.md index b51a098..781a0eb 100644 --- a/.claude/skills/add-gmail/SKILL.md +++ b/.claude/skills/add-gmail/SKILL.md @@ -218,8 +218,3 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp 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) - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-image-vision/SKILL.md b/.claude/skills/add-image-vision/SKILL.md index d42e394..072bf7b 100644 --- a/.claude/skills/add-image-vision/SKILL.md +++ b/.claude/skills/add-image-vision/SKILL.md @@ -92,8 +92,3 @@ All tests must pass and build must be clean before proceeding. - **"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. - **Agent doesn't mention image content**: Check container logs for "Loaded image" messages. If missing, ensure agent-runner source was synced to group caches. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-ollama-tool/SKILL.md b/.claude/skills/add-ollama-tool/SKILL.md index a28b8ea..a347b49 100644 --- a/.claude/skills/add-ollama-tool/SKILL.md +++ b/.claude/skills/add-ollama-tool/SKILL.md @@ -151,8 +151,3 @@ The agent is trying to run `ollama` CLI inside the container instead of using th ### Agent doesn't use Ollama tools The agent may not know about the tools. Try being explicit: "use the ollama_generate tool with gemma3:1b to answer: ..." - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-parallel/SKILL.md b/.claude/skills/add-parallel/SKILL.md index 12eb58c..f4c1982 100644 --- a/.claude/skills/add-parallel/SKILL.md +++ b/.claude/skills/add-parallel/SKILL.md @@ -288,8 +288,3 @@ To remove Parallel AI integration: 3. Remove Web Research Tools section from groups/main/CLAUDE.md 4. Rebuild: `./container/build.sh && npm run build` 5. Restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-pdf-reader/SKILL.md b/.claude/skills/add-pdf-reader/SKILL.md index 960d7fb..a01e530 100644 --- a/.claude/skills/add-pdf-reader/SKILL.md +++ b/.claude/skills/add-pdf-reader/SKILL.md @@ -102,8 +102,3 @@ The PDF may be scanned (image-based). pdftotext only handles text-based PDFs. Co ### WhatsApp PDF not detected Verify the message has `documentMessage` with `mimetype: application/pdf`. Some file-sharing apps send PDFs as generic files without the correct mimetype. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-reactions/SKILL.md b/.claude/skills/add-reactions/SKILL.md index 9eacebd..de86768 100644 --- a/.claude/skills/add-reactions/SKILL.md +++ b/.claude/skills/add-reactions/SKILL.md @@ -115,8 +115,3 @@ Ask the agent to react to a message via the `react_to_message` MCP tool. Check y - Check IPC logs for `Unauthorized IPC reaction attempt blocked` — the agent can only react in its own group's chat - Verify WhatsApp is connected: check logs for connection status - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-slack/SKILL.md b/.claude/skills/add-slack/SKILL.md index 32a2cf0..4c86e19 100644 --- a/.claude/skills/add-slack/SKILL.md +++ b/.claude/skills/add-slack/SKILL.md @@ -205,8 +205,3 @@ The Slack channel supports: - **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. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-telegram-swarm/SKILL.md b/.claude/skills/add-telegram-swarm/SKILL.md index b6e5923..ac4922c 100644 --- a/.claude/skills/add-telegram-swarm/SKILL.md +++ b/.claude/skills/add-telegram-swarm/SKILL.md @@ -382,8 +382,3 @@ To remove Agent Swarm support while keeping basic Telegram: 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) - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-telegram/SKILL.md b/.claude/skills/add-telegram/SKILL.md index 86a137f..10f25ab 100644 --- a/.claude/skills/add-telegram/SKILL.md +++ b/.claude/skills/add-telegram/SKILL.md @@ -220,8 +220,3 @@ To remove Telegram integration: 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) - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-voice-transcription/SKILL.md b/.claude/skills/add-voice-transcription/SKILL.md index d9f44b6..8ccec32 100644 --- a/.claude/skills/add-voice-transcription/SKILL.md +++ b/.claude/skills/add-voice-transcription/SKILL.md @@ -146,8 +146,3 @@ Check logs for the specific error. Common causes: ### Agent doesn't respond to voice notes Verify the chat is registered and the agent is running. Voice transcription only runs for registered groups. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-whatsapp/SKILL.md b/.claude/skills/add-whatsapp/SKILL.md index c22a835..0774799 100644 --- a/.claude/skills/add-whatsapp/SKILL.md +++ b/.claude/skills/add-whatsapp/SKILL.md @@ -370,8 +370,3 @@ To remove WhatsApp integration: 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) - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/claw/SKILL.md b/.claude/skills/claw/SKILL.md new file mode 100644 index 0000000..10e0dc3 --- /dev/null +++ b/.claude/skills/claw/SKILL.md @@ -0,0 +1,131 @@ +--- +name: claw +description: Install the claw CLI tool — run NanoClaw agent containers from the command line without opening a chat app. +--- + +# claw — NanoClaw CLI + +`claw` is a Python CLI that sends prompts directly to a NanoClaw agent container from the terminal. It reads registered groups from the NanoClaw database, picks up secrets from `.env`, and pipes a JSON payload into a container run — no chat app required. + +## What it does + +- Send a prompt to any registered group by name, folder, or JID +- Default target is the main group (no `-g` needed for most use) +- Resume a previous session with `-s ` +- Read prompts from stdin (`--pipe`) for scripting and piping +- List all registered groups with `--list-groups` +- Auto-detects `container` or `docker` runtime (or override with `--runtime`) +- Prints the agent's response to stdout; session ID to stderr +- Verbose mode (`-v`) shows the command, redacted payload, and exit code + +## Prerequisites + +- Python 3.8 or later +- NanoClaw installed with a built and tagged container image (`nanoclaw-agent:latest`) +- Either `container` (Apple Container, macOS 15+) or `docker` available in `PATH` + +## Install + +Run this skill from within the NanoClaw directory. The script auto-detects its location, so the symlink always points to the right place. + +### 1. Copy the script + +```bash +mkdir -p scripts +cp "${CLAUDE_SKILL_DIR}/scripts/claw" scripts/claw +chmod +x scripts/claw +``` + +### 2. Symlink into PATH + +```bash +mkdir -p ~/bin +ln -sf "$(pwd)/scripts/claw" ~/bin/claw +``` + +Make sure `~/bin` is in `PATH`. Add this to `~/.zshrc` or `~/.bashrc` if needed: + +```bash +export PATH="$HOME/bin:$PATH" +``` + +Then reload the shell: + +```bash +source ~/.zshrc # or ~/.bashrc +``` + +### 3. Verify + +```bash +claw --list-groups +``` + +You should see registered groups. If NanoClaw isn't running or the database doesn't exist yet, the list will be empty — that's fine. + +## Usage Examples + +```bash +# Send a prompt to the main group +claw "What's on my calendar today?" + +# Send to a specific group by name (fuzzy match) +claw -g "family" "Remind everyone about dinner at 7" + +# Send to a group by exact JID +claw -j "120363336345536173@g.us" "Hello" + +# Resume a previous session +claw -s abc123 "Continue where we left off" + +# Read prompt from stdin +echo "Summarize this" | claw --pipe -g dev + +# Pipe a file +cat report.txt | claw --pipe "Summarize this report" + +# List all registered groups +claw --list-groups + +# Force a specific runtime +claw --runtime docker "Hello" + +# Use a custom image tag (e.g. after rebuilding with a new tag) +claw --image nanoclaw-agent:dev "Hello" + +# Verbose mode (debug info, secrets redacted) +claw -v "Hello" + +# Custom timeout for long-running tasks +claw --timeout 600 "Run the full analysis" +``` + +## Troubleshooting + +### "neither 'container' nor 'docker' found" + +Install Docker Desktop or Apple Container (macOS 15+), or pass `--runtime` explicitly. + +### "no secrets found in .env" + +The script auto-detects your NanoClaw directory and reads `.env` from it. Check that the file exists and contains at least one of: `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`. + +### Container times out + +The default timeout is 300 seconds. For longer tasks, pass `--timeout 600` (or higher). If the container consistently hangs, check that your `nanoclaw-agent:latest` image is up to date by running `./container/build.sh`. + +### "group not found" + +Run `claw --list-groups` to see what's registered. Group lookup does a fuzzy partial match on name and folder — if your query matches multiple groups, you'll get an error listing the ambiguous matches. + +### Container crashes mid-stream + +Containers run with `--rm` so they are automatically removed. If the agent crashes before emitting the output sentinel, `claw` falls back to printing raw stdout. Use `-v` to see what the container produced. Rebuild the image with `./container/build.sh` if crashes are consistent. + +### Override the NanoClaw directory + +If `claw` can't find your database or `.env`, set the `NANOCLAW_DIR` environment variable: + +```bash +export NANOCLAW_DIR=/path/to/your/nanoclaw +``` diff --git a/.claude/skills/convert-to-apple-container/SKILL.md b/.claude/skills/convert-to-apple-container/SKILL.md index bcd1929..caf9c22 100644 --- a/.claude/skills/convert-to-apple-container/SKILL.md +++ b/.claude/skills/convert-to-apple-container/SKILL.md @@ -173,8 +173,3 @@ Check directory permissions on the host. The container runs as uid 1000. | `src/container-runner.ts` | .env shadow mount removed, main containers start as root with privilege drop | | `container/Dockerfile` | Entrypoint: `mount --bind` for .env shadowing, `setpriv` privilege drop | | `container/build.sh` | Default runtime: `docker` → `container` | - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/customize/SKILL.md b/.claude/skills/customize/SKILL.md index 310f1ed..614a979 100644 --- a/.claude/skills/customize/SKILL.md +++ b/.claude/skills/customize/SKILL.md @@ -108,8 +108,3 @@ User: "Add Telegram as an input channel" 3. Create `src/channels/telegram.ts` implementing the `Channel` interface (see `src/channels/whatsapp.ts`) 4. Add the channel to `main()` in `src/index.ts` 5. Tell user how to authenticate and test - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/debug/SKILL.md b/.claude/skills/debug/SKILL.md index e0fc3c7..03c34de 100644 --- a/.claude/skills/debug/SKILL.md +++ b/.claude/skills/debug/SKILL.md @@ -347,8 +347,3 @@ echo -e "\n8. Session continuity working?" SESSIONS=$(grep "Session initialized" logs/nanoclaw.log 2>/dev/null | tail -5 | awk '{print $NF}' | sort -u | wc -l) [ "$SESSIONS" -le 2 ] && echo "OK (recent sessions reusing IDs)" || echo "CHECK - multiple different session IDs, may indicate resumption issues" ``` - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/get-qodo-rules/SKILL.md b/.claude/skills/get-qodo-rules/SKILL.md index 4a2cf16..69abaf7 100644 --- a/.claude/skills/get-qodo-rules/SKILL.md +++ b/.claude/skills/get-qodo-rules/SKILL.md @@ -120,8 +120,3 @@ See `~/.qodo/config.json` for API key setup. Set `QODO_ENVIRONMENT_NAME` env var - **Not in git repo** - Inform the user that a git repository is required and exit gracefully; do not attempt code generation - **No API key** - Inform the user with setup instructions; set `QODO_API_KEY` or create `~/.qodo/config.json` - **No rules found** - Inform the user; set up rules at app.qodo.ai - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/qodo-pr-resolver/SKILL.md b/.claude/skills/qodo-pr-resolver/SKILL.md index 165bbe2..c0cbe22 100644 --- a/.claude/skills/qodo-pr-resolver/SKILL.md +++ b/.claude/skills/qodo-pr-resolver/SKILL.md @@ -324,8 +324,3 @@ Use the inline comment ID preserved during deduplication (Step 3b) to reply dire See [providers.md § Reply to Inline Comments](./resources/providers.md#reply-to-inline-comments) for provider-specific commands and reply format. Keep replies short (one line). If a reply fails, log it and continue. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/update-skills/SKILL.md b/.claude/skills/update-skills/SKILL.md index ade436b..cbbff39 100644 --- a/.claude/skills/update-skills/SKILL.md +++ b/.claude/skills/update-skills/SKILL.md @@ -128,8 +128,3 @@ Show: - Any conflicts that were resolved (list files) If the service is running, remind the user to restart it to pick up changes. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/use-local-whisper/SKILL.md b/.claude/skills/use-local-whisper/SKILL.md index b7d9814..ec18a09 100644 --- a/.claude/skills/use-local-whisper/SKILL.md +++ b/.claude/skills/use-local-whisper/SKILL.md @@ -150,8 +150,3 @@ whisper-cli -m data/models/ggml-base.bin -f /tmp/test.wav --no-timestamps -nt **Slow transcription**: The base model processes ~30s of audio in <1s on M1+. If slower, check CPU usage — another process may be competing. **Wrong language**: whisper.cpp auto-detects language. To force a language, you can set `WHISPER_LANG` and modify `src/transcription.ts` to pass `-l $WHISPER_LANG`. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/x-integration/SKILL.md b/.claude/skills/x-integration/SKILL.md index 86ae041..29a7be6 100644 --- a/.claude/skills/x-integration/SKILL.md +++ b/.claude/skills/x-integration/SKILL.md @@ -414,8 +414,4 @@ docker run nanoclaw-agent ls -la /app/src/skills/ - `data/x-browser-profile/` - Contains X session cookies (in `.gitignore`) - `data/x-auth.json` - Auth state marker (in `.gitignore`) - Only main group can use X tools (enforced in `agent.ts` and `host.ts`) -- Scripts run as subprocesses with limited environment - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. +- Scripts run as subprocesses with limited environment \ No newline at end of file From 31ac74f5f2462fba512d28a69d8d3a1055e5bae8 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 16:28:36 +0200 Subject: [PATCH 31/54] fix: remove claw skill accidentally added to this branch Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/claw/SKILL.md | 131 ----------------------------------- 1 file changed, 131 deletions(-) delete mode 100644 .claude/skills/claw/SKILL.md diff --git a/.claude/skills/claw/SKILL.md b/.claude/skills/claw/SKILL.md deleted file mode 100644 index 10e0dc3..0000000 --- a/.claude/skills/claw/SKILL.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -name: claw -description: Install the claw CLI tool — run NanoClaw agent containers from the command line without opening a chat app. ---- - -# claw — NanoClaw CLI - -`claw` is a Python CLI that sends prompts directly to a NanoClaw agent container from the terminal. It reads registered groups from the NanoClaw database, picks up secrets from `.env`, and pipes a JSON payload into a container run — no chat app required. - -## What it does - -- Send a prompt to any registered group by name, folder, or JID -- Default target is the main group (no `-g` needed for most use) -- Resume a previous session with `-s ` -- Read prompts from stdin (`--pipe`) for scripting and piping -- List all registered groups with `--list-groups` -- Auto-detects `container` or `docker` runtime (or override with `--runtime`) -- Prints the agent's response to stdout; session ID to stderr -- Verbose mode (`-v`) shows the command, redacted payload, and exit code - -## Prerequisites - -- Python 3.8 or later -- NanoClaw installed with a built and tagged container image (`nanoclaw-agent:latest`) -- Either `container` (Apple Container, macOS 15+) or `docker` available in `PATH` - -## Install - -Run this skill from within the NanoClaw directory. The script auto-detects its location, so the symlink always points to the right place. - -### 1. Copy the script - -```bash -mkdir -p scripts -cp "${CLAUDE_SKILL_DIR}/scripts/claw" scripts/claw -chmod +x scripts/claw -``` - -### 2. Symlink into PATH - -```bash -mkdir -p ~/bin -ln -sf "$(pwd)/scripts/claw" ~/bin/claw -``` - -Make sure `~/bin` is in `PATH`. Add this to `~/.zshrc` or `~/.bashrc` if needed: - -```bash -export PATH="$HOME/bin:$PATH" -``` - -Then reload the shell: - -```bash -source ~/.zshrc # or ~/.bashrc -``` - -### 3. Verify - -```bash -claw --list-groups -``` - -You should see registered groups. If NanoClaw isn't running or the database doesn't exist yet, the list will be empty — that's fine. - -## Usage Examples - -```bash -# Send a prompt to the main group -claw "What's on my calendar today?" - -# Send to a specific group by name (fuzzy match) -claw -g "family" "Remind everyone about dinner at 7" - -# Send to a group by exact JID -claw -j "120363336345536173@g.us" "Hello" - -# Resume a previous session -claw -s abc123 "Continue where we left off" - -# Read prompt from stdin -echo "Summarize this" | claw --pipe -g dev - -# Pipe a file -cat report.txt | claw --pipe "Summarize this report" - -# List all registered groups -claw --list-groups - -# Force a specific runtime -claw --runtime docker "Hello" - -# Use a custom image tag (e.g. after rebuilding with a new tag) -claw --image nanoclaw-agent:dev "Hello" - -# Verbose mode (debug info, secrets redacted) -claw -v "Hello" - -# Custom timeout for long-running tasks -claw --timeout 600 "Run the full analysis" -``` - -## Troubleshooting - -### "neither 'container' nor 'docker' found" - -Install Docker Desktop or Apple Container (macOS 15+), or pass `--runtime` explicitly. - -### "no secrets found in .env" - -The script auto-detects your NanoClaw directory and reads `.env` from it. Check that the file exists and contains at least one of: `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`. - -### Container times out - -The default timeout is 300 seconds. For longer tasks, pass `--timeout 600` (or higher). If the container consistently hangs, check that your `nanoclaw-agent:latest` image is up to date by running `./container/build.sh`. - -### "group not found" - -Run `claw --list-groups` to see what's registered. Group lookup does a fuzzy partial match on name and folder — if your query matches multiple groups, you'll get an error listing the ambiguous matches. - -### Container crashes mid-stream - -Containers run with `--rm` so they are automatically removed. If the agent crashes before emitting the output sentinel, `claw` falls back to printing raw stdout. Use `-v` to see what the container produced. Rebuild the image with `./container/build.sh` if crashes are consistent. - -### Override the NanoClaw directory - -If `claw` can't find your database or `.env`, set the `NANOCLAW_DIR` environment variable: - -```bash -export NANOCLAW_DIR=/path/to/your/nanoclaw -``` From e10b136df6f15cd65d5a020f947289b479676895 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 16:31:59 +0200 Subject: [PATCH 32/54] refactor: move diagnostics into each skill's own directory Replace shared _shared/diagnostics.md with dedicated diagnostics.md files in setup/ and update-nanoclaw/. Each contains only the event types relevant to that skill. References updated to local links. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/_shared/diagnostics.md | 106 ------------------ .claude/skills/setup/SKILL.md | 2 +- .claude/skills/setup/diagnostics.md | 81 +++++++++++++ .claude/skills/update-nanoclaw/SKILL.md | 2 +- .claude/skills/update-nanoclaw/diagnostics.md | 59 ++++++++++ 5 files changed, 142 insertions(+), 108 deletions(-) delete mode 100644 .claude/skills/_shared/diagnostics.md create mode 100644 .claude/skills/setup/diagnostics.md create mode 100644 .claude/skills/update-nanoclaw/diagnostics.md diff --git a/.claude/skills/_shared/diagnostics.md b/.claude/skills/_shared/diagnostics.md deleted file mode 100644 index ac5994b..0000000 --- a/.claude/skills/_shared/diagnostics.md +++ /dev/null @@ -1,106 +0,0 @@ -# Diagnostics (end-of-skill instructions) - -Follow these steps **after all other skill steps are complete**. - -## 1. Collect all events - -Every skill sends its own diagnostics event — even when invoked from a parent skill. A `/setup` that invokes `/add-telegram` produces **two** events: `skill_applied` (for add-telegram) and `setup_complete` (for setup). A `/setup` with no channels produces just `setup_complete`. - -Build the list of events to send: - -1. If sub-skills were invoked during this run (e.g., `/add-telegram` during `/setup`), add a `skill_applied` event for each. -2. Add the event for the current (outermost) skill. - -**If running a sub-skill standalone** (not called from a parent), there is just one event. - -## 2. Determine event type (for each event) - -- `/setup` → `setup_complete` -- `/update-nanoclaw` → `update_complete` -- Any other skill → `skill_applied` - -## 3. Build event-specific data - -Estimate `error_count` from the conversation (how many errors/retries occurred during the run). - -**For `setup_complete`:** -```json -{ - "channels_selected": ["whatsapp", "telegram"], - "error_count": 0, - "failed_step": null, - "exit_code": null -} -``` -- `channels_selected`: which channels the user chose to install -- `failed_step`: if setup failed, which step (environment / container / groups / register / mounts / service / verify) -- `exit_code`: if a step failed, its exit code - -**For `skill_applied`:** -```json -{ - "skill_name": "add-telegram", - "is_upstream_skill": true, - "conflict_files": ["package.json", "src/index.ts"], - "error_count": 0 -} -``` -- `skill_name`: the skill that was run. Use the upstream skill name if it's an upstream skill, otherwise use `"custom"` -- `is_upstream_skill`: true if this is a known upstream skill -- `conflict_files`: filenames that had merge conflicts (the script will gate these against upstream — pass them all, the script filters) - -**For `update_complete`:** -```json -{ - "version_age_days": 45, - "update_method": "merge", - "conflict_files": ["package.json"], - "breaking_changes_found": false, - "breaking_changes_skills_run": [], - "error_count": 0 -} -``` -- `version_age_days`: estimate from the backup tag or commit date how many days old the previous version was -- `update_method`: "merge" or "rebase" -- `breaking_changes_found`: whether breaking changes were detected during the update -- `breaking_changes_skills_run`: which skills had to be re-run to fix breaking changes - -## 4. Dry run all events - -For **each** event, run with `--dry-run` to get the payload: - -```bash -npx tsx scripts/send-diagnostics.ts --event --success --data '' --dry-run -``` - -Use `--failure` instead of `--success` if that particular skill/step failed. - -If **any** dry-run produces no output, the user has opted out permanently — skip the rest for all events. - -## 5. Show the user and ask once - -Show **all** payloads together and ask **once** (not per-event): - -> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" -> -> (show all JSON payloads) -> -> **Yes** / **No** / **Never ask again** - -Use AskUserQuestion. - -## 6. Handle response - -- **Yes**: Send **all** events (run each command without `--dry-run`): - ```bash - npx tsx scripts/send-diagnostics.ts --event --success --data '' - ``` - Confirm: "Diagnostics sent (N events)." or "Diagnostics sent." if only one. - -- **No**: Do nothing. User will be asked again next time. - -- **Never ask again**: Run: - ```bash - npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" - ``` - Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index 170e303..c1c5d60 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -220,4 +220,4 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/ ## Diagnostics (Optional) -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. +After completing all steps above, read and follow [diagnostics.md](diagnostics.md). diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md new file mode 100644 index 0000000..f371e41 --- /dev/null +++ b/.claude/skills/setup/diagnostics.md @@ -0,0 +1,81 @@ +# Diagnostics (end-of-setup instructions) + +Follow these steps **after all other setup steps are complete**. + +## 1. Collect events + +Build the list of events to send: + +1. If channel skills were invoked during setup (e.g., `/add-telegram`), add a `skill_applied` event for each. +2. Add a `setup_complete` event for setup itself. + +A `/setup` with no channels produces just `setup_complete`. + +## 2. Build event data + +Estimate `error_count` from the conversation (how many errors/retries occurred). + +**For `setup_complete`:** +```json +{ + "channels_selected": ["whatsapp", "telegram"], + "error_count": 0, + "failed_step": null, + "exit_code": null +} +``` +- `channels_selected`: which channels the user chose to install +- `failed_step`: if setup failed, which step (environment / container / groups / register / mounts / service / verify) +- `exit_code`: if a step failed, its exit code + +**For each `skill_applied`:** +```json +{ + "skill_name": "add-telegram", + "is_upstream_skill": true, + "conflict_files": ["package.json", "src/index.ts"], + "error_count": 0 +} +``` +- `skill_name`: upstream skill name, or `"custom"` for non-upstream skills +- `conflict_files`: filenames with merge conflicts (the script gates these against upstream) + +## 3. Dry run all events + +For **each** event, run with `--dry-run` to get the payload: + +```bash +npx tsx scripts/send-diagnostics.ts --event --success --data '' --dry-run +``` + +Use `--failure` instead of `--success` if that particular skill/step failed. + +If **any** dry-run produces no output, the user has opted out permanently — skip the rest. + +## 4. Show the user and ask once + +Show **all** payloads together and ask **once** (not per-event): + +> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" +> +> (show all JSON payloads) +> +> **Yes** / **No** / **Never ask again** + +Use AskUserQuestion. + +## 5. Handle response + +- **Yes**: Send **all** events (run each command without `--dry-run`): + ```bash + npx tsx scripts/send-diagnostics.ts --event --success --data '' + ``` + Confirm: "Diagnostics sent (N events)." or "Diagnostics sent." if only one. + +- **No**: Do nothing. User will be asked again next time. + +- **Never ask again**: Run: + ```bash + npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" + ``` + Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/update-nanoclaw/SKILL.md b/.claude/skills/update-nanoclaw/SKILL.md index 5e93c6c..e8197d2 100644 --- a/.claude/skills/update-nanoclaw/SKILL.md +++ b/.claude/skills/update-nanoclaw/SKILL.md @@ -237,4 +237,4 @@ Tell the user: ## Diagnostics (Optional) -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. +After completing all steps above, read and follow [diagnostics.md](diagnostics.md). diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md new file mode 100644 index 0000000..9028dc8 --- /dev/null +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -0,0 +1,59 @@ +# Diagnostics (end-of-update instructions) + +Follow these steps **after all other update steps are complete**. + +## 1. Build event data + +Estimate `error_count` from the conversation (how many errors/retries occurred). + +```json +{ + "version_age_days": 45, + "update_method": "merge", + "conflict_files": ["package.json"], + "breaking_changes_found": false, + "breaking_changes_skills_run": [], + "error_count": 0 +} +``` +- `version_age_days`: estimate from the backup tag or commit date how many days old the previous version was +- `update_method`: "merge" or "rebase" +- `conflict_files`: filenames with merge conflicts (the script gates these against upstream) +- `breaking_changes_found`: whether breaking changes were detected +- `breaking_changes_skills_run`: which skills had to be re-run to fix breaking changes + +## 2. Dry run + +```bash +npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '' --dry-run +``` + +Use `--failure` instead of `--success` if the update failed. + +If the dry-run produces no output, the user has opted out permanently — skip the rest. + +## 3. Show the user and ask + +> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" +> +> (show JSON payload) +> +> **Yes** / **No** / **Never ask again** + +Use AskUserQuestion. + +## 4. Handle response + +- **Yes**: Run without `--dry-run`: + ```bash + npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '' + ``` + Confirm: "Diagnostics sent." + +- **No**: Do nothing. User will be asked again next time. + +- **Never ask again**: Run: + ```bash + npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" + ``` + Confirm: "Got it — you won't be asked again." From e2423171e1579f1cf6713a68153030647990f8b1 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:36:08 +0200 Subject: [PATCH 33/54] simplify diagnostics instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show example commands with placeholder values. Claude fills in the actual values from the session in one shot — no multi-step build process. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/diagnostics.md | 87 +++++-------------- .claude/skills/update-nanoclaw/diagnostics.md | 58 ++++--------- 2 files changed, 42 insertions(+), 103 deletions(-) diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index f371e41..3f463b9 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -1,81 +1,42 @@ -# Diagnostics (end-of-setup instructions) +# Diagnostics (Optional) -Follow these steps **after all other setup steps are complete**. +After setup is complete, offer to send anonymous diagnostics. -## 1. Collect events - -Build the list of events to send: - -1. If channel skills were invoked during setup (e.g., `/add-telegram`), add a `skill_applied` event for each. -2. Add a `setup_complete` event for setup itself. - -A `/setup` with no channels produces just `setup_complete`. - -## 2. Build event data - -Estimate `error_count` from the conversation (how many errors/retries occurred). - -**For `setup_complete`:** -```json -{ - "channels_selected": ["whatsapp", "telegram"], - "error_count": 0, - "failed_step": null, - "exit_code": null -} -``` -- `channels_selected`: which channels the user chose to install -- `failed_step`: if setup failed, which step (environment / container / groups / register / mounts / service / verify) -- `exit_code`: if a step failed, its exit code - -**For each `skill_applied`:** -```json -{ - "skill_name": "add-telegram", - "is_upstream_skill": true, - "conflict_files": ["package.json", "src/index.ts"], - "error_count": 0 -} -``` -- `skill_name`: upstream skill name, or `"custom"` for non-upstream skills -- `conflict_files`: filenames with merge conflicts (the script gates these against upstream) - -## 3. Dry run all events - -For **each** event, run with `--dry-run` to get the payload: +## 1. Check opt-out ```bash -npx tsx scripts/send-diagnostics.ts --event --success --data '' --dry-run +npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{}' --dry-run ``` -Use `--failure` instead of `--success` if that particular skill/step failed. +If no output, the user opted out permanently — stop here. -If **any** dry-run produces no output, the user has opted out permanently — skip the rest. +## 2. Prepare events -## 4. Show the user and ask once +For each channel skill invoked during setup (e.g. `/add-telegram`), prepare a `skill_applied` event. Then prepare a `setup_complete` event for setup itself. -Show **all** payloads together and ask **once** (not per-event): +Run `--dry-run` for each to get the final payload: + +```bash +npx tsx scripts/send-diagnostics.ts --event skill_applied --success --data '{"skill_name":"add-telegram","is_upstream_skill":true,"conflict_files":[],"error_count":0}' --dry-run +npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{"channels_selected":["telegram"],"error_count":0,"failed_step":null,"exit_code":null}' --dry-run +``` + +Use `--failure` instead of `--success` if that step failed. Fill in the values based on what actually happened during the session. + +## 3. Ask the user + +Show all payloads and ask once: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show all JSON payloads) +> (show JSON payloads) > > **Yes** / **No** / **Never ask again** Use AskUserQuestion. -## 5. Handle response +## 4. Handle response -- **Yes**: Send **all** events (run each command without `--dry-run`): - ```bash - npx tsx scripts/send-diagnostics.ts --event --success --data '' - ``` - Confirm: "Diagnostics sent (N events)." or "Diagnostics sent." if only one. - -- **No**: Do nothing. User will be asked again next time. - -- **Never ask again**: Run: - ```bash - npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" - ``` - Confirm: "Got it — you won't be asked again." +- **Yes**: Run each command again without `--dry-run`. Confirm: "Diagnostics sent." +- **No**: Do nothing. +- **Never ask again**: Run `npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();"` — confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index 9028dc8..58638b3 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -1,38 +1,26 @@ -# Diagnostics (end-of-update instructions) +# Diagnostics (Optional) -Follow these steps **after all other update steps are complete**. +After the update is complete, offer to send anonymous diagnostics. -## 1. Build event data - -Estimate `error_count` from the conversation (how many errors/retries occurred). - -```json -{ - "version_age_days": 45, - "update_method": "merge", - "conflict_files": ["package.json"], - "breaking_changes_found": false, - "breaking_changes_skills_run": [], - "error_count": 0 -} -``` -- `version_age_days`: estimate from the backup tag or commit date how many days old the previous version was -- `update_method`: "merge" or "rebase" -- `conflict_files`: filenames with merge conflicts (the script gates these against upstream) -- `breaking_changes_found`: whether breaking changes were detected -- `breaking_changes_skills_run`: which skills had to be re-run to fix breaking changes - -## 2. Dry run +## 1. Check opt-out ```bash -npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '' --dry-run +npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '{}' --dry-run ``` -Use `--failure` instead of `--success` if the update failed. +If no output, the user opted out permanently — stop here. -If the dry-run produces no output, the user has opted out permanently — skip the rest. +## 2. Prepare event -## 3. Show the user and ask +Run `--dry-run` to get the final payload: + +```bash +npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '{"version_age_days":45,"update_method":"merge","conflict_files":[],"breaking_changes_found":false,"breaking_changes_skills_run":[],"error_count":0}' --dry-run +``` + +Use `--failure` instead of `--success` if the update failed. Fill in the values based on what actually happened during the session. + +## 3. Ask the user > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > @@ -44,16 +32,6 @@ Use AskUserQuestion. ## 4. Handle response -- **Yes**: Run without `--dry-run`: - ```bash - npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '' - ``` - Confirm: "Diagnostics sent." - -- **No**: Do nothing. User will be asked again next time. - -- **Never ask again**: Run: - ```bash - npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" - ``` - Confirm: "Got it — you won't be asked again." +- **Yes**: Run the command again without `--dry-run`. Confirm: "Diagnostics sent." +- **No**: Do nothing. +- **Never ask again**: Run `npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();"` — confirm: "Got it — you won't be asked again." From f33c66b04650b120b87f9e096bd33b52854d43f7 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:37:13 +0200 Subject: [PATCH 34/54] simplify setup diagnostics to single event One setup_complete event at the end, not per-skill events. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/diagnostics.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index 3f463b9..d59f633 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -10,26 +10,21 @@ npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{}' If no output, the user opted out permanently — stop here. -## 2. Prepare events - -For each channel skill invoked during setup (e.g. `/add-telegram`), prepare a `skill_applied` event. Then prepare a `setup_complete` event for setup itself. - -Run `--dry-run` for each to get the final payload: +## 2. Dry run ```bash -npx tsx scripts/send-diagnostics.ts --event skill_applied --success --data '{"skill_name":"add-telegram","is_upstream_skill":true,"conflict_files":[],"error_count":0}' --dry-run npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{"channels_selected":["telegram"],"error_count":0,"failed_step":null,"exit_code":null}' --dry-run ``` -Use `--failure` instead of `--success` if that step failed. Fill in the values based on what actually happened during the session. +Use `--failure` instead of `--success` if setup failed. Fill in the values based on what happened during the session. ## 3. Ask the user -Show all payloads and ask once: +Show the payload and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show JSON payloads) +> (show JSON payload) > > **Yes** / **No** / **Never ask again** @@ -37,6 +32,6 @@ Use AskUserQuestion. ## 4. Handle response -- **Yes**: Run each command again without `--dry-run`. Confirm: "Diagnostics sent." +- **Yes**: Run the command again without `--dry-run`. Confirm: "Diagnostics sent." - **No**: Do nothing. - **Never ask again**: Run `npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();"` — confirm: "Got it — you won't be asked again." From 09d833c310a34560979ac4c277a5ef601b8cd20b Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:45:04 +0200 Subject: [PATCH 35/54] replace diagnostics script with curl, simplify flow Remove send-diagnostics.ts entirely. Claude writes the JSON, shows it to the user, and sends via curl. Opt-out is permanent: Claude replaces diagnostics.md contents and removes the section from SKILL.md. No dependencies, no state files, no .nanoclaw/ directory. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/diagnostics.md | 58 ++-- .claude/skills/update-nanoclaw/diagnostics.md | 60 ++-- scripts/send-diagnostics.ts | 315 ------------------ 3 files changed, 80 insertions(+), 353 deletions(-) delete mode 100644 scripts/send-diagnostics.ts diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index d59f633..deb6509 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -2,36 +2,56 @@ After setup is complete, offer to send anonymous diagnostics. -## 1. Check opt-out +## 1. Write the event -```bash -npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{}' --dry-run +Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the setup outcome. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. + +```json +{ + "event": "setup_complete", + "success": true, + "properties": { + "nanoclaw_version": "1.2.21", + "os_platform": "darwin", + "arch": "arm64", + "node_major_version": 22, + "channels_selected": ["telegram", "whatsapp"], + "error_count": 0, + "failed_step": null + } +} ``` -If no output, the user opted out permanently — stop here. +Fill in the values based on what happened during the session. -## 2. Dry run +## 2. Show and ask -```bash -npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{"channels_selected":["telegram"],"error_count":0,"failed_step":null,"exit_code":null}' --dry-run -``` - -Use `--failure` instead of `--success` if setup failed. Fill in the values based on what happened during the session. - -## 3. Ask the user - -Show the payload and ask: +Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show JSON payload) +> (show JSON) > > **Yes** / **No** / **Never ask again** Use AskUserQuestion. -## 4. Handle response +## 3. Handle response -- **Yes**: Run the command again without `--dry-run`. Confirm: "Diagnostics sent." -- **No**: Do nothing. -- **Never ask again**: Run `npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();"` — confirm: "Got it — you won't be asked again." +**Yes**: Send it: +```bash +curl -s -X POST https://us.i.posthog.com/capture/ \ + -H 'Content-Type: application/json' \ + -d "{\"api_key\":\"phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP\",\"event\":\"$(jq -r .event /tmp/nanoclaw-diagnostics.json)\",\"distinct_id\":\"$(uuidgen)\",\"properties\":$(jq .properties /tmp/nanoclaw-diagnostics.json)}" +rm /tmp/nanoclaw-diagnostics.json +``` +Confirm: "Diagnostics sent." + +**No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. + +**Never ask again**: Replace the contents of this file (`diagnostics.md`) with: +``` +# Diagnostics — opted out +``` +Then remove the "Diagnostics (Optional)" section from the end of `SKILL.md` in this skill directory. Delete `/tmp/nanoclaw-diagnostics.json`. +Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index 58638b3..5d98855 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -2,36 +2,58 @@ After the update is complete, offer to send anonymous diagnostics. -## 1. Check opt-out +## 1. Write the event -```bash -npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '{}' --dry-run +Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the update outcome. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. + +```json +{ + "event": "update_complete", + "success": true, + "properties": { + "nanoclaw_version": "1.2.21", + "os_platform": "darwin", + "arch": "arm64", + "node_major_version": 22, + "version_age_days": 45, + "update_method": "merge", + "conflict_count": 0, + "breaking_changes_found": false, + "error_count": 0 + } +} ``` -If no output, the user opted out permanently — stop here. +Fill in the values based on what happened during the session. -## 2. Prepare event +## 2. Show and ask -Run `--dry-run` to get the final payload: - -```bash -npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '{"version_age_days":45,"update_method":"merge","conflict_files":[],"breaking_changes_found":false,"breaking_changes_skills_run":[],"error_count":0}' --dry-run -``` - -Use `--failure` instead of `--success` if the update failed. Fill in the values based on what actually happened during the session. - -## 3. Ask the user +Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show JSON payload) +> (show JSON) > > **Yes** / **No** / **Never ask again** Use AskUserQuestion. -## 4. Handle response +## 3. Handle response -- **Yes**: Run the command again without `--dry-run`. Confirm: "Diagnostics sent." -- **No**: Do nothing. -- **Never ask again**: Run `npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();"` — confirm: "Got it — you won't be asked again." +**Yes**: Send it: +```bash +curl -s -X POST https://us.i.posthog.com/capture/ \ + -H 'Content-Type: application/json' \ + -d "{\"api_key\":\"phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP\",\"event\":\"$(jq -r .event /tmp/nanoclaw-diagnostics.json)\",\"distinct_id\":\"$(uuidgen)\",\"properties\":$(jq .properties /tmp/nanoclaw-diagnostics.json)}" +rm /tmp/nanoclaw-diagnostics.json +``` +Confirm: "Diagnostics sent." + +**No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. + +**Never ask again**: Replace the contents of this file (`diagnostics.md`) with: +``` +# Diagnostics — opted out +``` +Then remove the "Diagnostics (Optional)" section from the end of `SKILL.md` in this skill directory. Delete `/tmp/nanoclaw-diagnostics.json`. +Confirm: "Got it — you won't be asked again." diff --git a/scripts/send-diagnostics.ts b/scripts/send-diagnostics.ts deleted file mode 100644 index 5b5399c..0000000 --- a/scripts/send-diagnostics.ts +++ /dev/null @@ -1,315 +0,0 @@ -/** - * send-diagnostics.ts — opt-in, privacy-first diagnostics for NanoClaw. - * - * Collects system info, accepts event-specific data via --data JSON arg, - * gates conflict filenames against upstream, and sends to PostHog. - * - * Usage: - * npx tsx scripts/send-diagnostics.ts \ - * --event \ - * [--success|--failure] \ - * [--data ''] \ - * [--dry-run] - * - * Never exits non-zero on telemetry failures. - */ - -import { execSync } from 'child_process'; -import crypto from 'crypto'; -import fs from 'fs'; -import path from 'path'; -import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; - -const POSTHOG_ENDPOINT = 'https://us.i.posthog.com/capture/'; -const POSTHOG_TOKEN = 'phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP'; -const SEND_TIMEOUT_MS = 5000; - -const PROJECT_ROOT = path.resolve(import.meta.dirname, '..'); -const STATE_YAML_PATH = path.join(PROJECT_ROOT, '.nanoclaw', 'state.yaml'); - -// --- Args --- - -function parseArgs(): { - event: string; - success?: boolean; - data: Record; - dryRun: boolean; -} { - const args = process.argv.slice(2); - let event = ''; - let success: boolean | undefined; - let data: Record = {}; - let dryRun = false; - - for (let i = 0; i < args.length; i++) { - switch (args[i]) { - case '--event': - event = args[++i] || ''; - break; - case '--success': - success = true; - break; - case '--failure': - success = false; - break; - case '--data': - try { - data = JSON.parse(args[++i] || '{}'); - } catch { - console.error('Warning: --data JSON parse failed, ignoring'); - } - break; - case '--dry-run': - dryRun = true; - break; - } - } - - if (!event) { - console.error('Error: --event is required'); - process.exit(0); // exit 0 — never fail on diagnostics - } - - return { event, success, data, dryRun }; -} - -// --- State (neverAsk) --- - -function readState(): Record { - try { - const raw = fs.readFileSync(STATE_YAML_PATH, 'utf-8'); - return parseYaml(raw) || {}; - } catch { - return {}; - } -} - -function isNeverAsk(): boolean { - const state = readState(); - return state.neverAsk === true; -} - -export function setNeverAsk(): void { - const state = readState(); - state.neverAsk = true; - const dir = path.dirname(STATE_YAML_PATH); - fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(STATE_YAML_PATH, stringifyYaml(state)); -} - -// --- Git helpers --- - -/** Resolve the upstream remote ref (could be 'upstream/main' or 'origin/main'). */ -function resolveUpstreamRef(): string | null { - for (const ref of ['upstream/main', 'origin/main']) { - try { - execSync(`git rev-parse --verify ${ref}`, { - cwd: PROJECT_ROOT, - stdio: 'ignore', - }); - return ref; - } catch { - continue; - } - } - return null; -} - -// --- System info --- - -function getNanoclawVersion(): string { - try { - const pkg = JSON.parse( - fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf-8'), - ); - return pkg.version || 'unknown'; - } catch { - return 'unknown'; - } -} - -function getNodeMajorVersion(): number | null { - const match = process.version.match(/^v(\d+)/); - return match ? parseInt(match[1], 10) : null; -} - -function getContainerRuntime(): string { - try { - const src = fs.readFileSync( - path.join(PROJECT_ROOT, 'src', 'container-runtime.ts'), - 'utf-8', - ); - const match = src.match(/CONTAINER_RUNTIME_BIN\s*=\s*['"]([^'"]+)['"]/); - return match ? match[1] : 'unknown'; - } catch { - return 'unknown'; - } -} - -function isUpstreamCommit(): boolean { - const ref = resolveUpstreamRef(); - if (!ref) return false; - try { - const head = execSync('git rev-parse HEAD', { - encoding: 'utf-8', - cwd: PROJECT_ROOT, - stdio: ['pipe', 'pipe', 'ignore'], - }).trim(); - execSync(`git merge-base --is-ancestor ${head} ${ref}`, { - cwd: PROJECT_ROOT, - stdio: 'ignore', - }); - return true; - } catch { - return false; - } -} - -function collectSystemInfo(): Record { - return { - nanoclaw_version: getNanoclawVersion(), - os_platform: process.platform, - arch: process.arch, - node_major_version: getNodeMajorVersion(), - container_runtime: getContainerRuntime(), - is_upstream_commit: isUpstreamCommit(), - }; -} - -// --- Conflict filename gating --- - -function getUpstreamFiles(): Set | null { - const ref = resolveUpstreamRef(); - if (!ref) return null; - try { - const output = execSync(`git ls-tree -r --name-only ${ref}`, { - encoding: 'utf-8', - cwd: PROJECT_ROOT, - stdio: ['pipe', 'pipe', 'ignore'], - }); - return new Set(output.trim().split('\n').filter(Boolean)); - } catch { - return null; - } -} - -function gateConflictFiles(data: Record): void { - if (!Array.isArray(data.conflict_files)) return; - - const rawFiles: string[] = data.conflict_files; - const upstreamFiles = getUpstreamFiles(); - const totalCount = rawFiles.length; - - if (!upstreamFiles) { - // Can't verify — fail-closed - data.conflict_files = []; - data.conflict_count = totalCount; - data.has_non_upstream_conflicts = totalCount > 0; - return; - } - - const safe: string[] = []; - let hasNonUpstream = false; - - for (const file of rawFiles) { - if (upstreamFiles.has(file)) { - safe.push(file); - } else { - hasNonUpstream = true; - } - } - - data.conflict_files = safe; - data.conflict_count = totalCount; - data.has_non_upstream_conflicts = hasNonUpstream; -} - -// --- Build & send --- - -function buildPayload( - event: string, - systemInfo: Record, - eventData: Record, - success?: boolean, -): Record { - const properties: Record = { - $process_person_profile: false, - $lib: 'nanoclaw-diagnostics', - ...systemInfo, - ...eventData, - }; - - if (success !== undefined) { - properties.success = success; - } - - return { - api_key: POSTHOG_TOKEN, - event, - distinct_id: crypto.randomUUID(), - properties, - }; -} - -async function sendToPostHog( - payload: Record, -): Promise { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS); - - try { - const response = await fetch(POSTHOG_ENDPOINT, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - signal: controller.signal, - }); - if (response.ok) { - console.log('Diagnostics sent successfully.'); - } else { - console.error( - `Diagnostics send failed (HTTP ${response.status}). This is fine.`, - ); - } - } catch (err) { - console.error('Diagnostics send failed (network error). This is fine.'); - } finally { - clearTimeout(timeout); - } -} - -// --- Main --- - -async function main(): Promise { - try { - if (isNeverAsk()) { - // User opted out permanently — exit silently - return; - } - - const { event, success, data, dryRun } = parseArgs(); - - // Gate conflict filenames before building payload - gateConflictFiles(data); - - const systemInfo = collectSystemInfo(); - const payload = buildPayload(event, systemInfo, data, success); - - if (dryRun) { - // Strip internal fields before showing to user - const { api_key, distinct_id, ...visible } = payload; - const props = visible.properties as Record; - delete props.$process_person_profile; - delete props.$lib; - console.log(JSON.stringify(visible, null, 2)); - return; - } - - await sendToPostHog(payload); - } catch (err) { - // Never fail on diagnostics - console.error('Diagnostics error (this is fine):', (err as Error).message); - } -} - -main(); From f97394656c7c358edd495b1ea133a7eb00c3e2e4 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:47:54 +0200 Subject: [PATCH 36/54] cross-skill opt-out and gather system info via shell - "Never ask again" now removes diagnostics from both skills - Added shell commands to gather version, platform, arch, node version - Show only properties object to user, not api_key/distinct_id - Write full PostHog payload to temp file, send with curl -d @file Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/diagnostics.md | 42 ++++++++++------- .claude/skills/update-nanoclaw/diagnostics.md | 45 ++++++++++++------- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index deb6509..2511d06 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -2,15 +2,26 @@ After setup is complete, offer to send anonymous diagnostics. -## 1. Write the event +## 1. Gather system info -Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the setup outcome. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. +```bash +node -p "require('./package.json').version" +uname -s +uname -m +node -p "process.versions.node.split('.')[0]" +``` + +## 2. Write the event + +Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from the commands above and the outcome from the session. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. ```json { + "api_key": "phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP", "event": "setup_complete", - "success": true, + "distinct_id": "", "properties": { + "success": true, "nanoclaw_version": "1.2.21", "os_platform": "darwin", "arch": "arm64", @@ -22,36 +33,33 @@ Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the setup outcome. Us } ``` -Fill in the values based on what happened during the session. - -## 2. Show and ask +## 3. Show and ask Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show JSON) +> (show the properties object only — not the api_key or distinct_id) > > **Yes** / **No** / **Never ask again** Use AskUserQuestion. -## 3. Handle response +## 4. Handle response -**Yes**: Send it: +**Yes**: ```bash -curl -s -X POST https://us.i.posthog.com/capture/ \ - -H 'Content-Type: application/json' \ - -d "{\"api_key\":\"phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP\",\"event\":\"$(jq -r .event /tmp/nanoclaw-diagnostics.json)\",\"distinct_id\":\"$(uuidgen)\",\"properties\":$(jq .properties /tmp/nanoclaw-diagnostics.json)}" +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 ``` Confirm: "Diagnostics sent." **No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. -**Never ask again**: Replace the contents of this file (`diagnostics.md`) with: -``` -# Diagnostics — opted out -``` -Then remove the "Diagnostics (Optional)" section from the end of `SKILL.md` in this skill directory. Delete `/tmp/nanoclaw-diagnostics.json`. +**Never ask again**: Opt out of diagnostics in both setup and update-nanoclaw: +1. Replace the contents of `.claude/skills/setup/diagnostics.md` with just `# Diagnostics — opted out` +2. Replace the contents of `.claude/skills/update-nanoclaw/diagnostics.md` with just `# Diagnostics — opted out` +3. Remove the "Diagnostics (Optional)" section from the end of both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` +4. `rm /tmp/nanoclaw-diagnostics.json` + Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index 5d98855..1ab9bad 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -2,15 +2,29 @@ After the update is complete, offer to send anonymous diagnostics. -## 1. Write the event +## 1. Gather system info -Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the update outcome. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. +```bash +node -p "require('./package.json').version" +uname -s +uname -m +node -p "process.versions.node.split('.')[0]" +git log -1 --format=%ci HEAD@{1} 2>/dev/null || echo "unknown" +``` + +The last command gets the date of the previous HEAD (before the update) to estimate version age. + +## 2. Write the event + +Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from the commands above and the outcome from the session. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. ```json { + "api_key": "phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP", "event": "update_complete", - "success": true, + "distinct_id": "", "properties": { + "success": true, "nanoclaw_version": "1.2.21", "os_platform": "darwin", "arch": "arm64", @@ -24,36 +38,33 @@ Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the update outcome. U } ``` -Fill in the values based on what happened during the session. - -## 2. Show and ask +## 3. Show and ask Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show JSON) +> (show the properties object only — not the api_key or distinct_id) > > **Yes** / **No** / **Never ask again** Use AskUserQuestion. -## 3. Handle response +## 4. Handle response -**Yes**: Send it: +**Yes**: ```bash -curl -s -X POST https://us.i.posthog.com/capture/ \ - -H 'Content-Type: application/json' \ - -d "{\"api_key\":\"phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP\",\"event\":\"$(jq -r .event /tmp/nanoclaw-diagnostics.json)\",\"distinct_id\":\"$(uuidgen)\",\"properties\":$(jq .properties /tmp/nanoclaw-diagnostics.json)}" +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 ``` Confirm: "Diagnostics sent." **No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. -**Never ask again**: Replace the contents of this file (`diagnostics.md`) with: -``` -# Diagnostics — opted out -``` -Then remove the "Diagnostics (Optional)" section from the end of `SKILL.md` in this skill directory. Delete `/tmp/nanoclaw-diagnostics.json`. +**Never ask again**: Opt out of diagnostics in both setup and update-nanoclaw: +1. Replace the contents of `.claude/skills/setup/diagnostics.md` with just `# Diagnostics — opted out` +2. Replace the contents of `.claude/skills/update-nanoclaw/diagnostics.md` with just `# Diagnostics — opted out` +3. Remove the "Diagnostics (Optional)" section from the end of both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` +4. `rm /tmp/nanoclaw-diagnostics.json` + Confirm: "Got it — you won't be asked again." From a4fbc9d615a23a4b612441053413835083b875e7 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:51:15 +0200 Subject: [PATCH 37/54] show full payload to user, not just properties Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/diagnostics.md | 2 +- .claude/skills/update-nanoclaw/diagnostics.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index 2511d06..4741777 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -39,7 +39,7 @@ Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show the properties object only — not the api_key or distinct_id) +> (show the entire JSON file) > > **Yes** / **No** / **Never ask again** diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index 1ab9bad..46286b7 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -44,7 +44,7 @@ Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show the properties object only — not the api_key or distinct_id) +> (show the entire JSON file) > > **Yes** / **No** / **Never ask again** From 02d51afe09340db99e38102df05da7850d82c0c8 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:53:53 +0200 Subject: [PATCH 38/54] trim diagnostics verbosity Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/SKILL.md | 4 +- .claude/skills/setup/diagnostics.md | 39 +++++------------- .claude/skills/update-nanoclaw/SKILL.md | 4 +- .claude/skills/update-nanoclaw/diagnostics.md | 41 +++++-------------- 4 files changed, 24 insertions(+), 64 deletions(-) diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index c1c5d60..2041f80 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -218,6 +218,6 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/ **Unload service:** macOS: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist` | Linux: `systemctl --user stop nanoclaw` -## Diagnostics (Optional) +## Diagnostics -After completing all steps above, read and follow [diagnostics.md](diagnostics.md). +Read and follow [diagnostics.md](diagnostics.md). diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index 4741777..a76b4ab 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -1,8 +1,6 @@ -# Diagnostics (Optional) +# Diagnostics -After setup is complete, offer to send anonymous diagnostics. - -## 1. Gather system info +Gather system info: ```bash node -p "require('./package.json').version" @@ -11,15 +9,13 @@ uname -m node -p "process.versions.node.split('.')[0]" ``` -## 2. Write the event - -Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from the commands above and the outcome from the session. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. +Write `/tmp/nanoclaw-diagnostics.json`. No paths, usernames, hostnames, or IP addresses. ```json { "api_key": "phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP", "event": "setup_complete", - "distinct_id": "", + "distinct_id": "", "properties": { "success": true, "nanoclaw_version": "1.2.21", @@ -33,33 +29,18 @@ Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from } ``` -## 3. Show and ask - -Show the contents of the file to the user and ask: - -> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" -> -> (show the entire JSON file) -> -> **Yes** / **No** / **Never ask again** - -Use AskUserQuestion. - -## 4. Handle response +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 ``` -Confirm: "Diagnostics sent." -**No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. +**No**: `rm /tmp/nanoclaw-diagnostics.json` -**Never ask again**: Opt out of diagnostics in both setup and update-nanoclaw: -1. Replace the contents of `.claude/skills/setup/diagnostics.md` with just `# Diagnostics — opted out` -2. Replace the contents of `.claude/skills/update-nanoclaw/diagnostics.md` with just `# Diagnostics — opted out` -3. Remove the "Diagnostics (Optional)" section from the end of both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` +**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. Remove the "Diagnostics" section from both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` 4. `rm /tmp/nanoclaw-diagnostics.json` - -Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/update-nanoclaw/SKILL.md b/.claude/skills/update-nanoclaw/SKILL.md index e8197d2..0af5713 100644 --- a/.claude/skills/update-nanoclaw/SKILL.md +++ b/.claude/skills/update-nanoclaw/SKILL.md @@ -235,6 +235,6 @@ Tell the user: - If running manually: restart `npm run dev` -## Diagnostics (Optional) +## Diagnostics -After completing all steps above, read and follow [diagnostics.md](diagnostics.md). +Read and follow [diagnostics.md](diagnostics.md). diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index 46286b7..bc659ab 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -1,8 +1,6 @@ -# Diagnostics (Optional) +# Diagnostics -After the update is complete, offer to send anonymous diagnostics. - -## 1. Gather system info +Gather system info: ```bash node -p "require('./package.json').version" @@ -12,17 +10,13 @@ node -p "process.versions.node.split('.')[0]" git log -1 --format=%ci HEAD@{1} 2>/dev/null || echo "unknown" ``` -The last command gets the date of the previous HEAD (before the update) to estimate version age. - -## 2. Write the event - -Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from the commands above and the outcome from the session. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. +Write `/tmp/nanoclaw-diagnostics.json`. No paths, usernames, hostnames, or IP addresses. ```json { "api_key": "phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP", "event": "update_complete", - "distinct_id": "", + "distinct_id": "", "properties": { "success": true, "nanoclaw_version": "1.2.21", @@ -38,33 +32,18 @@ Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from } ``` -## 3. Show and ask - -Show the contents of the file to the user and ask: - -> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" -> -> (show the entire JSON file) -> -> **Yes** / **No** / **Never ask again** - -Use AskUserQuestion. - -## 4. Handle response +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 ``` -Confirm: "Diagnostics sent." -**No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. +**No**: `rm /tmp/nanoclaw-diagnostics.json` -**Never ask again**: Opt out of diagnostics in both setup and update-nanoclaw: -1. Replace the contents of `.claude/skills/setup/diagnostics.md` with just `# Diagnostics — opted out` -2. Replace the contents of `.claude/skills/update-nanoclaw/diagnostics.md` with just `# Diagnostics — opted out` -3. Remove the "Diagnostics (Optional)" section from the end of both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` +**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. Remove the "Diagnostics" section from both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` 4. `rm /tmp/nanoclaw-diagnostics.json` - -Confirm: "Got it — you won't be asked again." From e9369617fb556a087dfdf2655f0e670c737bd13b Mon Sep 17 00:00:00 2001 From: Guy Ben Aharon Date: Wed, 18 Mar 2026 17:56:57 +0200 Subject: [PATCH 39/54] feat: replace credential proxy with OneCLI gateway for secret injection --- .claude/skills/setup/SKILL.md | 49 +++++++++++++++++++++++++++++------ container/Dockerfile | 2 +- package-lock.json | 10 +++++++ package.json | 1 + src/config.ts | 10 +++---- src/container-runner.test.ts | 10 ++++++- src/container-runner.ts | 43 +++++++++++++++--------------- src/container-runtime.ts | 30 --------------------- src/index.ts | 32 ++++++++++++++++------- 9 files changed, 109 insertions(+), 78 deletions(-) diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index 2041f80..dbb9028 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -50,7 +50,7 @@ Already configured. Continue. **Verify:** `git remote -v` should show `origin` → user's repo, `upstream` → `qwibitai/nanoclaw.git`. -## 1. Bootstrap (Node.js + Dependencies) +## 1. Bootstrap (Node.js + Dependencies + OneCLI) Run `bash setup.sh` and parse the status block. @@ -62,6 +62,20 @@ Run `bash setup.sh` and parse the status block. - If NATIVE_OK=false → better-sqlite3 failed to load. Install build tools and re-run. - Record PLATFORM and IS_WSL for later steps. +After bootstrap succeeds, install OneCLI and its CLI tool: + +```bash +curl -fsSL onecli.sh/install | sh +curl -fsSL onecli.sh/cli/install | sh +``` + +Verify both installed: `onecli version`. If the command is not found, the install script may have printed a path — add it to PATH or use the full path. + +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://localhost:10254' >> .env +``` + ## 2. Check Environment Run `npx tsx setup/index.ts --step environment` and parse the status block. @@ -112,15 +126,34 @@ Run `npx tsx setup/index.ts --step container -- --runtime ` and parse th **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. Claude Authentication (No Script) +## 4. Anthropic Credentials via OneCLI -If HAS_ENV=true from step 2, read `.env` and check for `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`. If present, confirm with user: keep or reconfigure? +NanoClaw uses OneCLI to manage credentials — API keys are never stored in `.env` or exposed to containers. The OneCLI gateway injects them at request time. -AskUserQuestion: Claude subscription (Pro/Max) vs Anthropic API key? +Check if a secret already exists: +```bash +onecli secrets list +``` -**Subscription:** Tell user to run `claude setup-token` in another terminal, copy the token, add `CLAUDE_CODE_OAUTH_TOKEN=` to `.env`. Do NOT collect the token in chat. +If an Anthropic secret is listed, confirm with user: keep or reconfigure? If keeping, skip to step 5. -**API key:** Tell user to add `ANTHROPIC_API_KEY=` to `.env`. +First, look up the exact command for creating an Anthropic secret: +```bash +onecli secrets create --help +``` + +Then AskUserQuestion, providing the user with two options: + +1. **OneCLI dashboard** — open http://localhost:10254 in the browser and add the secret there +2. **CLI** — run the `onecli secrets create` command with the right flags for an Anthropic secret (show them the exact command with a placeholder for the key value, based on the `--help` output) + +Tell the user to get an API key from https://console.anthropic.com/settings/keys if they don't have one. + +Ask them to let you know when done. + +**If the user's response happens to contain an API key** (starts with `sk-ant-`): handle it gracefully — run the `onecli secrets create` command with that key on their behalf. + +**After user confirms:** verify with `onecli secrets list` that an Anthropic secret exists. If not, ask again. ## 5. Set Up Channels @@ -198,7 +231,7 @@ Run `npx 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=not_found → re-run step 7 -- CREDENTIALS=missing → re-run step 4 +- CREDENTIALS=missing → re-run step 4 (check `onecli secrets list` for Anthropic secret) - 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` @@ -207,7 +240,7 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/ ## Troubleshooting -**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 7), missing `.env` (step 4), missing channel credentials (re-invoke channel skill). +**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 7), OneCLI not running (check `curl http://localhost:10254/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`. diff --git a/container/Dockerfile b/container/Dockerfile index e8537c3..2fe1b22 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -1,7 +1,7 @@ # NanoClaw Agent Container # Runs Claude Agent SDK in isolated Linux VM with browser automation -FROM node:22-slim +FROM node:24-slim # Install system dependencies for Chromium RUN apt-get update && apt-get install -y \ diff --git a/package-lock.json b/package-lock.json index fae72c7..e325d6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "nanoclaw", "version": "1.2.21", "dependencies": { + "@onecli-sh/sdk": "^0.1.6", "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", "pino": "^9.6.0", @@ -786,6 +787,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@onecli-sh/sdk": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@onecli-sh/sdk/-/sdk-0.1.6.tgz", + "integrity": "sha512-kqVg8BOI6kapJaQjpTLBv91DhdKNykuSZIUsfb1pH5puyNlShWlXw5DWwxRVmxBihBMaIm+JyN9VRJMrVKZ5vQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", diff --git a/package.json b/package.json index b30dd39..990f001 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:watch": "vitest" }, "dependencies": { + "@onecli-sh/sdk": "^0.1.6", "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", "pino": "^9.6.0", diff --git a/src/config.ts b/src/config.ts index 43db54f..63d1207 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,9 +4,7 @@ import path from 'path'; import { readEnvFile } from './env.js'; // Read config values from .env (falls back to process.env). -// Secrets (API keys, tokens) are NOT read here — they are loaded only -// by the credential proxy (credential-proxy.ts), never exposed to containers. -const envConfig = readEnvFile(['ASSISTANT_NAME', 'ASSISTANT_HAS_OWN_NUMBER']); +const envConfig = readEnvFile(['ASSISTANT_NAME', 'ASSISTANT_HAS_OWN_NUMBER', 'ONECLI_URL']); export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || envConfig.ASSISTANT_NAME || 'Andy'; @@ -47,10 +45,8 @@ export const CONTAINER_MAX_OUTPUT_SIZE = parseInt( process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760', 10, ); // 10MB default -export const CREDENTIAL_PROXY_PORT = parseInt( - process.env.CREDENTIAL_PROXY_PORT || '3001', - 10, -); +export const ONECLI_URL = + process.env.ONECLI_URL || envConfig.ONECLI_URL || 'http://localhost:10254'; export const IPC_POLL_INTERVAL = 1000; export const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '1800000', 10); // 30min default — how long to keep container alive after last result export const MAX_CONCURRENT_CONTAINERS = Math.max( diff --git a/src/container-runner.test.ts b/src/container-runner.test.ts index c830176..58c7e0d 100644 --- a/src/container-runner.test.ts +++ b/src/container-runner.test.ts @@ -11,10 +11,10 @@ vi.mock('./config.js', () => ({ CONTAINER_IMAGE: 'nanoclaw-agent:latest', CONTAINER_MAX_OUTPUT_SIZE: 10485760, CONTAINER_TIMEOUT: 1800000, // 30min - CREDENTIAL_PROXY_PORT: 3001, DATA_DIR: '/tmp/nanoclaw-test-data', GROUPS_DIR: '/tmp/nanoclaw-test-groups', IDLE_TIMEOUT: 1800000, // 30min + ONECLI_URL: 'http://localhost:10254', TIMEZONE: 'America/Los_Angeles', })); @@ -51,6 +51,14 @@ vi.mock('./mount-security.js', () => ({ validateAdditionalMounts: vi.fn(() => []), })); +// Mock OneCLI SDK +vi.mock('@onecli-sh/sdk', () => ({ + OneCLI: class { + applyContainerConfig = vi.fn().mockResolvedValue(true); + createAgent = vi.fn().mockResolvedValue({ id: 'test' }); + }, +})); + // Create a controllable fake ChildProcess function createFakeProcess() { const proc = new EventEmitter() as EventEmitter & { diff --git a/src/container-runner.ts b/src/container-runner.ts index a6b58d7..1dc607f 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -10,25 +10,26 @@ import { CONTAINER_IMAGE, CONTAINER_MAX_OUTPUT_SIZE, CONTAINER_TIMEOUT, - CREDENTIAL_PROXY_PORT, DATA_DIR, GROUPS_DIR, IDLE_TIMEOUT, + ONECLI_URL, TIMEZONE, } from './config.js'; import { resolveGroupFolderPath, resolveGroupIpcPath } from './group-folder.js'; import { logger } from './logger.js'; import { - CONTAINER_HOST_GATEWAY, CONTAINER_RUNTIME_BIN, hostGatewayArgs, readonlyMountArgs, stopContainer, } from './container-runtime.js'; -import { detectAuthMode } from './credential-proxy.js'; +import { OneCLI } from '@onecli-sh/sdk'; import { validateAdditionalMounts } from './mount-security.js'; import { RegisteredGroup } from './types.js'; +const onecli = new OneCLI({ url: ONECLI_URL }); + // Sentinel markers for robust output parsing (must match agent-runner) const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---'; const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---'; @@ -77,7 +78,7 @@ function buildVolumeMounts( }); // Shadow .env so the agent cannot read secrets from the mounted project root. - // Credentials are injected by the credential proxy, never exposed to containers. + // Credentials are injected by the OneCLI gateway, never exposed to containers. const envFile = path.join(projectRoot, '.env'); if (fs.existsSync(envFile)) { mounts.push({ @@ -212,30 +213,26 @@ function buildVolumeMounts( return mounts; } -function buildContainerArgs( +async function buildContainerArgs( mounts: VolumeMount[], containerName: string, -): string[] { + agentIdentifier?: string, +): Promise { const args: string[] = ['run', '-i', '--rm', '--name', containerName]; // Pass host timezone so container's local time matches the user's args.push('-e', `TZ=${TIMEZONE}`); - // Route API traffic through the credential proxy (containers never see real secrets) - args.push( - '-e', - `ANTHROPIC_BASE_URL=http://${CONTAINER_HOST_GATEWAY}:${CREDENTIAL_PROXY_PORT}`, - ); - - // Mirror the host's auth method with a placeholder value. - // API key mode: SDK sends x-api-key, proxy replaces with real key. - // OAuth mode: SDK exchanges placeholder token for temp API key, - // proxy injects real OAuth token on that exchange request. - const authMode = detectAuthMode(); - if (authMode === 'api-key') { - args.push('-e', 'ANTHROPIC_API_KEY=placeholder'); + // OneCLI gateway handles credential injection — containers never see real secrets. + // The gateway intercepts HTTPS traffic and injects API keys or OAuth tokens. + const onecliApplied = await onecli.applyContainerConfig(args, { + addHostMapping: false, // Nanoclaw already handles host gateway + agent: agentIdentifier, + }); + if (onecliApplied) { + logger.info({ containerName }, 'OneCLI gateway config applied'); } else { - args.push('-e', 'CLAUDE_CODE_OAUTH_TOKEN=placeholder'); + logger.warn({ containerName }, 'OneCLI gateway not reachable — container will have no credentials'); } // Runtime-specific args for host gateway resolution @@ -278,7 +275,11 @@ export async function runContainerAgent( const mounts = buildVolumeMounts(group, input.isMain); const safeName = group.folder.replace(/[^a-zA-Z0-9-]/g, '-'); const containerName = `nanoclaw-${safeName}-${Date.now()}`; - const containerArgs = buildContainerArgs(mounts, containerName); + // Main group uses the default OneCLI agent; others use their own agent. + const agentIdentifier = input.isMain + ? undefined + : group.folder.toLowerCase().replace(/_/g, '-'); + const containerArgs = await buildContainerArgs(mounts, containerName, agentIdentifier); logger.debug( { diff --git a/src/container-runtime.ts b/src/container-runtime.ts index 9f32d10..6326fde 100644 --- a/src/container-runtime.ts +++ b/src/container-runtime.ts @@ -3,7 +3,6 @@ * All runtime-specific logic lives here so swapping runtimes means changing one file. */ import { execSync } from 'child_process'; -import fs from 'fs'; import os from 'os'; import { logger } from './logger.js'; @@ -11,35 +10,6 @@ import { logger } from './logger.js'; /** The container runtime binary name. */ export const CONTAINER_RUNTIME_BIN = 'docker'; -/** Hostname containers use to reach the host machine. */ -export const CONTAINER_HOST_GATEWAY = 'host.docker.internal'; - -/** - * Address the credential proxy binds to. - * Docker Desktop (macOS): 127.0.0.1 — the VM routes host.docker.internal to loopback. - * Docker (Linux): bind to the docker0 bridge IP so only containers can reach it, - * falling back to 0.0.0.0 if the interface isn't found. - */ -export const PROXY_BIND_HOST = - process.env.CREDENTIAL_PROXY_HOST || detectProxyBindHost(); - -function detectProxyBindHost(): string { - if (os.platform() === 'darwin') return '127.0.0.1'; - - // WSL uses Docker Desktop (same VM routing as macOS) — loopback is correct. - // Check /proc filesystem, not env vars — WSL_DISTRO_NAME isn't set under systemd. - if (fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop')) return '127.0.0.1'; - - // Bare-metal Linux: bind to the docker0 bridge IP instead of 0.0.0.0 - const ifaces = os.networkInterfaces(); - const docker0 = ifaces['docker0']; - if (docker0) { - const ipv4 = docker0.find((a) => a.family === 'IPv4'); - if (ipv4) return ipv4.address; - } - return '0.0.0.0'; -} - /** CLI args needed for the container to resolve the host gateway. */ export function hostGatewayArgs(): string[] { // On Linux, host.docker.internal isn't built-in — add it explicitly diff --git a/src/index.ts b/src/index.ts index db274f0..a7fa9e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,16 @@ import fs from 'fs'; import path from 'path'; +import { OneCLI } from '@onecli-sh/sdk'; + import { ASSISTANT_NAME, - CREDENTIAL_PROXY_PORT, IDLE_TIMEOUT, + ONECLI_URL, POLL_INTERVAL, TIMEZONE, TRIGGER_PATTERN, } from './config.js'; -import { startCredentialProxy } from './credential-proxy.js'; import './channels/index.js'; import { getChannelFactory, @@ -24,7 +25,6 @@ import { import { cleanupOrphans, ensureContainerRuntimeRunning, - PROXY_BIND_HOST, } from './container-runtime.js'; import { getAllChats, @@ -72,6 +72,8 @@ let messageLoopRunning = false; const channels: Channel[] = []; const queue = new GroupQueue(); +const onecli = new OneCLI({ url: ONECLI_URL }); + function loadState(): void { lastTimestamp = getRouterState('last_timestamp') || ''; const agentTs = getRouterState('last_agent_timestamp'); @@ -112,6 +114,23 @@ function registerGroup(jid: string, group: RegisteredGroup): void { // Create group folder fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true }); + // Create a corresponding OneCLI agent (best-effort, non-blocking) + const identifier = group.folder.toLowerCase().replace(/_/g, '-'); + onecli.createAgent({ name: group.name, identifier }).then( + (agent) => { + logger.info( + { jid, agentId: agent.id, identifier }, + 'OneCLI agent created', + ); + }, + (err) => { + logger.debug( + { jid, identifier, err: String(err) }, + 'OneCLI agent creation skipped', + ); + }, + ); + logger.info( { jid, name: group.name, folder: group.folder }, 'Group registered', @@ -476,16 +495,9 @@ async function main(): Promise { loadState(); restoreRemoteControl(); - // Start credential proxy (containers route API calls through this) - const proxyServer = await startCredentialProxy( - CREDENTIAL_PROXY_PORT, - PROXY_BIND_HOST, - ); - // Graceful shutdown handlers const shutdown = async (signal: string) => { logger.info({ signal }, 'Shutdown signal received'); - proxyServer.close(); await queue.shutdown(10000); for (const ch of channels) await ch.disconnect(); process.exit(0); From b7f8c20a2535b97ccec0fc9e70b0eb2bfdb57d8f Mon Sep 17 00:00:00 2001 From: NanoClaw Setup Date: Thu, 19 Mar 2026 11:57:54 +0000 Subject: [PATCH 40/54] fix: setup skill uses 127.0.0.1 for OneCLI and offers dashboard vs CLI choice - Configure CLI api-host to local instance (defaults to cloud otherwise) - Use 127.0.0.1 instead of localhost to avoid IPv6 resolution issues - Present dashboard and CLI as two options with platform guidance - Accept ONECLI_URL as valid credentials in verify step Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/SKILL.md | 15 ++++++++++----- setup/verify.ts | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index dbb9028..b83cf61 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -71,9 +71,14 @@ curl -fsSL onecli.sh/cli/install | sh Verify both installed: `onecli version`. If the command is not found, the install script may have printed a path — add it to PATH or use the full path. +Point the CLI at the local OneCLI instance (it defaults to the cloud service otherwise): +```bash +onecli config set api-host http://127.0.0.1:10254 +``` + 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://localhost:10254' >> .env +grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=http://127.0.0.1:10254' >> .env ``` ## 2. Check Environment @@ -142,10 +147,10 @@ First, look up the exact command for creating an Anthropic secret: onecli secrets create --help ``` -Then AskUserQuestion, providing the user with two options: +Then AskUserQuestion with two options. Use the `description` field to include the one-liner guidance and the concrete instructions for each option so the user sees everything in the question itself (avoids the interactive modal hiding text above it): -1. **OneCLI dashboard** — open http://localhost:10254 in the browser and add the secret there -2. **CLI** — run the `onecli secrets create` command with the right flags for an Anthropic secret (show them the exact command with a placeholder for the key value, based on the `--help` output) +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." +2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_KEY --host-pattern api.anthropic.com`" Tell the user to get an API key from https://console.anthropic.com/settings/keys if they don't have one. @@ -240,7 +245,7 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/ ## Troubleshooting -**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 7), OneCLI not running (check `curl http://localhost:10254/api/health`), missing channel credentials (re-invoke channel skill). +**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 7), OneCLI not running (check `curl http://127.0.0.1:10254/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`. diff --git a/setup/verify.ts b/setup/verify.ts index f64e4d0..e039e52 100644 --- a/setup/verify.ts +++ b/setup/verify.ts @@ -101,7 +101,7 @@ export async function run(_args: string[]): Promise { const envFile = path.join(projectRoot, '.env'); if (fs.existsSync(envFile)) { const envContent = fs.readFileSync(envFile, 'utf-8'); - if (/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY)=/m.test(envContent)) { + if (/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY|ONECLI_URL)=/m.test(envContent)) { credentials = 'configured'; } } From 7f6298a1bbc0d9354cd0d584f6e573d54e159e33 Mon Sep 17 00:00:00 2001 From: NanoClaw Setup Date: Thu, 19 Mar 2026 12:00:03 +0000 Subject: [PATCH 41/54] fix: add onecli CLI to PATH if not found after install Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/SKILL.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index b83cf61..2f8d821 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -69,7 +69,16 @@ curl -fsSL onecli.sh/install | sh curl -fsSL onecli.sh/cli/install | sh ``` -Verify both installed: `onecli version`. If the command is not found, the install script may have printed a path — add it to PATH or use the full path. +Verify both installed: `onecli version`. If the command is not found, the CLI was likely installed to `~/.local/bin/`. Add it to PATH for the current session and persist it: + +```bash +export PATH="$HOME/.local/bin:$PATH" +# Persist for future sessions (append to shell profile if not already present) +grep -q '.local/bin' ~/.bashrc 2>/dev/null || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc +grep -q '.local/bin' ~/.zshrc 2>/dev/null || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc +``` + +Then re-verify with `onecli version`. Point the CLI at the local OneCLI instance (it defaults to the cloud service otherwise): ```bash From 2583af7ead511cc45b4b78e706307d30aa26e77c Mon Sep 17 00:00:00 2001 From: Guy Ben Aharon Date: Mon, 23 Mar 2026 14:45:41 +0200 Subject: [PATCH 42/54] fix: ensure OneCLI agents exist for all groups on startup --- package-lock.json | 8 +++---- package.json | 2 +- src/container-runner.test.ts | 1 + src/index.ts | 44 +++++++++++++++++++++++------------- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index e325d6e..afca823 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "nanoclaw", "version": "1.2.21", "dependencies": { - "@onecli-sh/sdk": "^0.1.6", + "@onecli-sh/sdk": "^0.2.0", "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", "pino": "^9.6.0", @@ -788,9 +788,9 @@ } }, "node_modules/@onecli-sh/sdk": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@onecli-sh/sdk/-/sdk-0.1.6.tgz", - "integrity": "sha512-kqVg8BOI6kapJaQjpTLBv91DhdKNykuSZIUsfb1pH5puyNlShWlXw5DWwxRVmxBihBMaIm+JyN9VRJMrVKZ5vQ==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@onecli-sh/sdk/-/sdk-0.2.0.tgz", + "integrity": "sha512-u7PqWROEvTV9f0ADVkjigTrd2AZn3klbPrv7GGpeRHIJpjAxJUdlWqxr5kiGt6qTDKL8t3nq76xr4X2pxTiyBg==", "license": "MIT", "engines": { "node": ">=20" diff --git a/package.json b/package.json index 990f001..54185a0 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:watch": "vitest" }, "dependencies": { - "@onecli-sh/sdk": "^0.1.6", + "@onecli-sh/sdk": "^0.2.0", "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", "pino": "^9.6.0", diff --git a/src/container-runner.test.ts b/src/container-runner.test.ts index 58c7e0d..2de45c5 100644 --- a/src/container-runner.test.ts +++ b/src/container-runner.test.ts @@ -56,6 +56,7 @@ vi.mock('@onecli-sh/sdk', () => ({ OneCLI: class { applyContainerConfig = vi.fn().mockResolvedValue(true); createAgent = vi.fn().mockResolvedValue({ id: 'test' }); + ensureAgent = vi.fn().mockResolvedValue({ name: 'test', identifier: 'test', created: true }); }, })); diff --git a/src/index.ts b/src/index.ts index a7fa9e7..3f5e710 100644 --- a/src/index.ts +++ b/src/index.ts @@ -74,6 +74,25 @@ const queue = new GroupQueue(); const onecli = new OneCLI({ url: ONECLI_URL }); +function ensureOneCLIAgent(jid: string, group: RegisteredGroup): void { + if (group.isMain) return; + const identifier = group.folder.toLowerCase().replace(/_/g, '-'); + onecli.ensureAgent({ name: group.name, identifier }).then( + (res) => { + logger.info( + { jid, identifier, created: res.created }, + 'OneCLI agent ensured', + ); + }, + (err) => { + logger.debug( + { jid, identifier, err: String(err) }, + 'OneCLI agent ensure skipped', + ); + }, + ); +} + function loadState(): void { lastTimestamp = getRouterState('last_timestamp') || ''; const agentTs = getRouterState('last_agent_timestamp'); @@ -114,22 +133,8 @@ function registerGroup(jid: string, group: RegisteredGroup): void { // Create group folder fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true }); - // Create a corresponding OneCLI agent (best-effort, non-blocking) - const identifier = group.folder.toLowerCase().replace(/_/g, '-'); - onecli.createAgent({ name: group.name, identifier }).then( - (agent) => { - logger.info( - { jid, agentId: agent.id, identifier }, - 'OneCLI agent created', - ); - }, - (err) => { - logger.debug( - { jid, identifier, err: String(err) }, - 'OneCLI agent creation skipped', - ); - }, - ); + // Ensure a corresponding OneCLI agent exists (best-effort, non-blocking) + ensureOneCLIAgent(jid, group); logger.info( { jid, name: group.name, folder: group.folder }, @@ -493,6 +498,13 @@ async function main(): Promise { initDatabase(); logger.info('Database initialized'); loadState(); + + // Ensure OneCLI agents exist for all registered groups. + // Recovers from missed creates (e.g. OneCLI was down at registration time). + for (const [jid, group] of Object.entries(registeredGroups)) { + ensureOneCLIAgent(jid, group); + } + restoreRemoteControl(); // Graceful shutdown handlers From def3748d02f3ecde039d69b9eedcf40395f0af28 Mon Sep 17 00:00:00 2001 From: NanoClaw Setup Date: Sun, 22 Mar 2026 15:47:25 +0000 Subject: [PATCH 43/54] fix: restore subscription vs API key choice in setup step 4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OneCLI integration removed the upstream subscription/API key question and only offered dashboard vs CLI. This restores the choice so users with a Claude Pro/Max subscription can use `claude setup-token` to get their OAuth token, while API key users get the existing flow. Both paths converge to the same `onecli secrets create --type anthropic` command — OneCLI handles both token types transparently. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/SKILL.md | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index 2f8d821..86726ff 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -151,21 +151,34 @@ onecli secrets list If an Anthropic secret is listed, confirm with user: keep or reconfigure? If keeping, skip to step 5. -First, look up the exact command for creating an Anthropic secret: -```bash -onecli secrets create --help -``` +AskUserQuestion: Do you want to use your **Claude subscription** (Pro/Max) or an **Anthropic API key**? -Then AskUserQuestion with two options. Use the `description` field to include the one-liner guidance and the concrete instructions for each option so the user sees everything in the question itself (avoids the interactive modal hiding text above it): +1. **Claude subscription (Pro/Max)** — description: "Uses your existing Claude Pro or Max subscription. You'll run `claude setup-token` in another terminal to get your token." +2. **Anthropic API key** — description: "Pay-per-use API key from console.anthropic.com." + +### Subscription path + +Tell the user to run `claude setup-token` in another terminal and copy the token it outputs. Do NOT collect the token in chat. + +Once they have the token, 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." +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 + +Tell the user to get an API key from https://console.anthropic.com/settings/keys if they don't have one. + +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." 2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_KEY --host-pattern api.anthropic.com`" -Tell the user to get an API key from https://console.anthropic.com/settings/keys if they don't have one. +### After either path Ask them to let you know when done. -**If the user's response happens to contain an API key** (starts with `sk-ant-`): handle it gracefully — run the `onecli secrets create` command with that key on their behalf. +**If the user's response happens to contain a token or key** (starts with `sk-ant-`): handle it gracefully — run the `onecli secrets create` command with that value on their behalf. **After user confirms:** verify with `onecli secrets list` that an Anthropic secret exists. If not, ask again. From 4f7efd3c6758c15409fc27097be571c6d9e90c6c Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Mon, 23 Mar 2026 14:37:47 +0000 Subject: [PATCH 44/54] fix: make diagnostics step explicit so Claude actually follows it The diagnostics section used a markdown link that Claude never resolved, so the prompt was silently skipped. Replace with a numbered step (setup) and mandatory final step (update-nanoclaw) that instructs Claude to use the Read tool on the full file path. Update opt-out instructions to match the renamed section headings. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/SKILL.md | 4 ++-- .claude/skills/setup/diagnostics.md | 2 +- .claude/skills/update-nanoclaw/SKILL.md | 4 ++-- .claude/skills/update-nanoclaw/diagnostics.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index 2041f80..f532ead 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -218,6 +218,6 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/ **Unload service:** macOS: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist` | Linux: `systemctl --user stop nanoclaw` -## Diagnostics +## 9. Diagnostics -Read and follow [diagnostics.md](diagnostics.md). +Use the Read tool to open `.claude/skills/setup/diagnostics.md` and follow every instruction in it. Do not skip this step. diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index a76b4ab..669ea82 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -42,5 +42,5 @@ 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. Remove the "Diagnostics" section from both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` +3. Remove the `## 9. Diagnostics` section from `.claude/skills/setup/SKILL.md` and the `## Diagnostics (mandatory final step)` section from `.claude/skills/update-nanoclaw/SKILL.md` 4. `rm /tmp/nanoclaw-diagnostics.json` diff --git a/.claude/skills/update-nanoclaw/SKILL.md b/.claude/skills/update-nanoclaw/SKILL.md index 0af5713..1def6c6 100644 --- a/.claude/skills/update-nanoclaw/SKILL.md +++ b/.claude/skills/update-nanoclaw/SKILL.md @@ -235,6 +235,6 @@ Tell the user: - If running manually: restart `npm run dev` -## Diagnostics +## Diagnostics (mandatory final step) -Read and follow [diagnostics.md](diagnostics.md). +Use the Read tool to open `.claude/skills/update-nanoclaw/diagnostics.md` and follow every instruction in it. Do not skip this step. diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index bc659ab..f15b45c 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -45,5 +45,5 @@ 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. Remove the "Diagnostics" section from both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` +3. Remove the `## 9. Diagnostics` section from `.claude/skills/setup/SKILL.md` and the `## Diagnostics (mandatory final step)` section from `.claude/skills/update-nanoclaw/SKILL.md` 4. `rm /tmp/nanoclaw-diagnostics.json` From 81f67031022a8fd2bf13f46f808c712e2f433190 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Mar 2026 12:55:54 +0000 Subject: [PATCH 45/54] chore: bump version to 1.2.22 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index afca823..cd2dbef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.21", + "version": "1.2.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.21", + "version": "1.2.22", "dependencies": { "@onecli-sh/sdk": "^0.2.0", "better-sqlite3": "^11.8.1", diff --git a/package.json b/package.json index 54185a0..3457b67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.21", + "version": "1.2.22", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From b0671ef9e64784038aa89d9c0772fe3843b4ac86 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Mar 2026 12:55:57 +0000 Subject: [PATCH 46/54] =?UTF-8?q?docs:=20update=20token=20count=20to=2040.?= =?UTF-8?q?7k=20tokens=20=C2=B7=2020%=20of=20context=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- repo-tokens/badge.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/repo-tokens/badge.svg b/repo-tokens/badge.svg index 993856e..b268ecc 100644 --- a/repo-tokens/badge.svg +++ b/repo-tokens/badge.svg @@ -1,5 +1,5 @@ - - 40.9k tokens, 20% of context window + + 40.7k tokens, 20% of context window @@ -15,8 +15,8 @@ tokens - - 40.9k + + 40.7k From 14247d0b577cf77a8d1f29aa3d3796b62bd1870c Mon Sep 17 00:00:00 2001 From: Gabi Simons Date: Tue, 24 Mar 2026 15:37:27 +0200 Subject: [PATCH 47/54] skill: add /use-native-credential-proxy, remove dead proxy code Add SKILL.md for the native credential proxy feature skill. Delete src/credential-proxy.ts and src/credential-proxy.test.ts which became dead code after PR #1237 (OneCLI integration). These files live on the skill/native-credential-proxy branch. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../use-native-credential-proxy/SKILL.md | 157 ++++++++++++++ src/credential-proxy.test.ts | 192 ------------------ src/credential-proxy.ts | 125 ------------ 3 files changed, 157 insertions(+), 317 deletions(-) create mode 100644 .claude/skills/use-native-credential-proxy/SKILL.md delete mode 100644 src/credential-proxy.test.ts delete mode 100644 src/credential-proxy.ts diff --git a/.claude/skills/use-native-credential-proxy/SKILL.md b/.claude/skills/use-native-credential-proxy/SKILL.md new file mode 100644 index 0000000..4cdda4c --- /dev/null +++ b/.claude/skills/use-native-credential-proxy/SKILL.md @@ -0,0 +1,157 @@ +--- +name: use-native-credential-proxy +description: Replace OneCLI gateway with the built-in credential proxy. For users who want simple .env-based credential management without installing OneCLI. Reads API key or OAuth token from .env and injects into container API requests. +--- + +# Use Native Credential Proxy + +This skill replaces the OneCLI gateway with NanoClaw's built-in credential proxy. Containers get credentials injected via a local HTTP proxy that reads from `.env` — no external services needed. + +## Phase 1: Pre-flight + +### Check if already applied + +Check if `src/credential-proxy.ts` is imported in `src/index.ts`: + +```bash +grep "credential-proxy" src/index.ts +``` + +If it shows an import for `startCredentialProxy`, the native proxy is already active. Skip to Phase 3 (Setup). + +### Check if OneCLI is active + +```bash +grep "@onecli-sh/sdk" package.json +``` + +If `@onecli-sh/sdk` appears, OneCLI is the active credential provider. Proceed with Phase 2 to replace it. + +If neither check matches, you may be on an older version. Run `/update-nanoclaw` first, then retry. + +## Phase 2: Apply Code Changes + +### Ensure upstream remote + +```bash +git remote -v +``` + +If `upstream` is missing, add it: + +```bash +git remote add upstream https://github.com/qwibitai/nanoclaw.git +``` + +### Merge the skill branch + +```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 merge --continue +} +``` + +This merges in: +- `src/credential-proxy.ts` and `src/credential-proxy.test.ts` (the proxy implementation) +- Restored credential proxy usage in `src/index.ts`, `src/container-runner.ts`, `src/container-runtime.ts`, `src/config.ts` +- Removed `@onecli-sh/sdk` dependency +- Restored `CREDENTIAL_PROXY_PORT` config (default 3001) +- 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. + +### Validate code changes + +```bash +npm install +npm run build +npx vitest run src/credential-proxy.test.ts src/container-runner.test.ts +``` + +All tests must pass and build must be clean before proceeding. + +## Phase 3: Setup Credentials + +AskUserQuestion: Do you want to use your **Claude subscription** (Pro/Max) or an **Anthropic API key**? + +1. **Claude subscription (Pro/Max)** — description: "Uses your existing Claude Pro or Max subscription. You'll run `claude setup-token` in another terminal to get your token." +2. **Anthropic API key** — description: "Pay-per-use API key from console.anthropic.com." + +### Subscription path + +Tell the user to run `claude setup-token` in another terminal and copy the token it outputs. Do NOT collect the token in chat. + +Once they have the token, add it to `.env`: + +```bash +# Add to .env (create file if needed) +echo 'CLAUDE_CODE_OAUTH_TOKEN=' >> .env +``` + +Note: `ANTHROPIC_AUTH_TOKEN` is also supported as a fallback. + +### API key path + +Tell the user to get an API key from https://console.anthropic.com/settings/keys if they don't have one. + +Add it to `.env`: + +```bash +echo 'ANTHROPIC_API_KEY=' >> .env +``` + +### After either path + +**If the user's response happens to contain a token or key** (starts with `sk-ant-` or looks like a token): write it to `.env` on their behalf using the appropriate variable name. + +**Optional:** If the user needs a custom API endpoint, they can add `ANTHROPIC_BASE_URL=` to `.env` (defaults to `https://api.anthropic.com`). + +## Phase 4: Verify + +1. Rebuild and restart: + +```bash +npm run build +``` + +Then restart the service: +- macOS: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` +- Linux: `systemctl --user restart nanoclaw` +- WSL/manual: stop and re-run `bash start-nanoclaw.sh` + +2. Check logs for successful proxy startup: + +```bash +tail -20 logs/nanoclaw.log | grep "Credential proxy" +``` + +Expected: `Credential proxy started` with port and auth mode. + +3. Send a test message in the registered chat to verify the agent responds. + +4. Note: after applying this skill, the OneCLI credential steps in `/setup` no longer apply. `.env` is now the credential source. + +## Troubleshooting + +**"Credential proxy upstream error" in logs:** Check that `.env` has a valid `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN`. Verify the API is reachable: `curl -s https://api.anthropic.com/v1/messages -H "x-api-key: test" | head`. + +**Port 3001 already in use:** Set `CREDENTIAL_PROXY_PORT=` in `.env` or as an environment variable. + +**Container can't reach proxy (Linux):** The proxy binds to the `docker0` bridge IP by default. If that interface doesn't exist (e.g. rootless Docker), set `CREDENTIAL_PROXY_HOST=0.0.0.0` as an environment variable. + +**OAuth token expired (401 errors):** Re-run `claude setup-token` in a terminal and update the token in `.env`. + +## Removal + +To revert to OneCLI gateway: + +1. Find the merge commit: `git log --oneline --merges -5` +2. Revert it: `git revert -m 1` (undoes the skill branch merge, keeps your other changes) +3. `npm install` (re-adds `@onecli-sh/sdk`) +4. `npm run build` +5. Follow `/setup` step 4 to configure OneCLI credentials +6. Remove `ANTHROPIC_API_KEY` / `CLAUDE_CODE_OAUTH_TOKEN` from `.env` diff --git a/src/credential-proxy.test.ts b/src/credential-proxy.test.ts deleted file mode 100644 index de76c89..0000000 --- a/src/credential-proxy.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import http from 'http'; -import type { AddressInfo } from 'net'; - -const mockEnv: Record = {}; -vi.mock('./env.js', () => ({ - readEnvFile: vi.fn(() => ({ ...mockEnv })), -})); - -vi.mock('./logger.js', () => ({ - logger: { info: vi.fn(), error: vi.fn(), debug: vi.fn(), warn: vi.fn() }, -})); - -import { startCredentialProxy } from './credential-proxy.js'; - -function makeRequest( - port: number, - options: http.RequestOptions, - body = '', -): Promise<{ - statusCode: number; - body: string; - headers: http.IncomingHttpHeaders; -}> { - return new Promise((resolve, reject) => { - const req = http.request( - { ...options, hostname: '127.0.0.1', port }, - (res) => { - const chunks: Buffer[] = []; - res.on('data', (c) => chunks.push(c)); - res.on('end', () => { - resolve({ - statusCode: res.statusCode!, - body: Buffer.concat(chunks).toString(), - headers: res.headers, - }); - }); - }, - ); - req.on('error', reject); - req.write(body); - req.end(); - }); -} - -describe('credential-proxy', () => { - let proxyServer: http.Server; - let upstreamServer: http.Server; - let proxyPort: number; - let upstreamPort: number; - let lastUpstreamHeaders: http.IncomingHttpHeaders; - - beforeEach(async () => { - lastUpstreamHeaders = {}; - - upstreamServer = http.createServer((req, res) => { - lastUpstreamHeaders = { ...req.headers }; - res.writeHead(200, { 'content-type': 'application/json' }); - res.end(JSON.stringify({ ok: true })); - }); - await new Promise((resolve) => - upstreamServer.listen(0, '127.0.0.1', resolve), - ); - upstreamPort = (upstreamServer.address() as AddressInfo).port; - }); - - afterEach(async () => { - await new Promise((r) => proxyServer?.close(() => r())); - await new Promise((r) => upstreamServer?.close(() => r())); - for (const key of Object.keys(mockEnv)) delete mockEnv[key]; - }); - - async function startProxy(env: Record): Promise { - Object.assign(mockEnv, env, { - ANTHROPIC_BASE_URL: `http://127.0.0.1:${upstreamPort}`, - }); - proxyServer = await startCredentialProxy(0); - return (proxyServer.address() as AddressInfo).port; - } - - it('API-key mode injects x-api-key and strips placeholder', async () => { - proxyPort = await startProxy({ ANTHROPIC_API_KEY: 'sk-ant-real-key' }); - - await makeRequest( - proxyPort, - { - method: 'POST', - path: '/v1/messages', - headers: { - 'content-type': 'application/json', - 'x-api-key': 'placeholder', - }, - }, - '{}', - ); - - expect(lastUpstreamHeaders['x-api-key']).toBe('sk-ant-real-key'); - }); - - it('OAuth mode replaces Authorization when container sends one', async () => { - proxyPort = await startProxy({ - CLAUDE_CODE_OAUTH_TOKEN: 'real-oauth-token', - }); - - await makeRequest( - proxyPort, - { - method: 'POST', - path: '/api/oauth/claude_cli/create_api_key', - headers: { - 'content-type': 'application/json', - authorization: 'Bearer placeholder', - }, - }, - '{}', - ); - - expect(lastUpstreamHeaders['authorization']).toBe( - 'Bearer real-oauth-token', - ); - }); - - it('OAuth mode does not inject Authorization when container omits it', async () => { - proxyPort = await startProxy({ - CLAUDE_CODE_OAUTH_TOKEN: 'real-oauth-token', - }); - - // Post-exchange: container uses x-api-key only, no Authorization header - await makeRequest( - proxyPort, - { - method: 'POST', - path: '/v1/messages', - headers: { - 'content-type': 'application/json', - 'x-api-key': 'temp-key-from-exchange', - }, - }, - '{}', - ); - - expect(lastUpstreamHeaders['x-api-key']).toBe('temp-key-from-exchange'); - expect(lastUpstreamHeaders['authorization']).toBeUndefined(); - }); - - it('strips hop-by-hop headers', async () => { - proxyPort = await startProxy({ ANTHROPIC_API_KEY: 'sk-ant-real-key' }); - - await makeRequest( - proxyPort, - { - method: 'POST', - path: '/v1/messages', - headers: { - 'content-type': 'application/json', - connection: 'keep-alive', - 'keep-alive': 'timeout=5', - 'transfer-encoding': 'chunked', - }, - }, - '{}', - ); - - // Proxy strips client hop-by-hop headers. Node's HTTP client may re-add - // its own Connection header (standard HTTP/1.1 behavior), but the client's - // custom keep-alive and transfer-encoding must not be forwarded. - expect(lastUpstreamHeaders['keep-alive']).toBeUndefined(); - expect(lastUpstreamHeaders['transfer-encoding']).toBeUndefined(); - }); - - it('returns 502 when upstream is unreachable', async () => { - Object.assign(mockEnv, { - ANTHROPIC_API_KEY: 'sk-ant-real-key', - ANTHROPIC_BASE_URL: 'http://127.0.0.1:59999', - }); - proxyServer = await startCredentialProxy(0); - proxyPort = (proxyServer.address() as AddressInfo).port; - - const res = await makeRequest( - proxyPort, - { - method: 'POST', - path: '/v1/messages', - headers: { 'content-type': 'application/json' }, - }, - '{}', - ); - - expect(res.statusCode).toBe(502); - expect(res.body).toBe('Bad Gateway'); - }); -}); diff --git a/src/credential-proxy.ts b/src/credential-proxy.ts deleted file mode 100644 index 8a893dd..0000000 --- a/src/credential-proxy.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Credential proxy for container isolation. - * Containers connect here instead of directly to the Anthropic API. - * The proxy injects real credentials so containers never see them. - * - * Two auth modes: - * API key: Proxy injects x-api-key on every request. - * OAuth: Container CLI exchanges its placeholder token for a temp - * API key via /api/oauth/claude_cli/create_api_key. - * Proxy injects real OAuth token on that exchange request; - * subsequent requests carry the temp key which is valid as-is. - */ -import { createServer, Server } from 'http'; -import { request as httpsRequest } from 'https'; -import { request as httpRequest, RequestOptions } from 'http'; - -import { readEnvFile } from './env.js'; -import { logger } from './logger.js'; - -export type AuthMode = 'api-key' | 'oauth'; - -export interface ProxyConfig { - authMode: AuthMode; -} - -export function startCredentialProxy( - port: number, - host = '127.0.0.1', -): Promise { - const secrets = readEnvFile([ - 'ANTHROPIC_API_KEY', - 'CLAUDE_CODE_OAUTH_TOKEN', - 'ANTHROPIC_AUTH_TOKEN', - 'ANTHROPIC_BASE_URL', - ]); - - const authMode: AuthMode = secrets.ANTHROPIC_API_KEY ? 'api-key' : 'oauth'; - const oauthToken = - secrets.CLAUDE_CODE_OAUTH_TOKEN || secrets.ANTHROPIC_AUTH_TOKEN; - - const upstreamUrl = new URL( - secrets.ANTHROPIC_BASE_URL || 'https://api.anthropic.com', - ); - const isHttps = upstreamUrl.protocol === 'https:'; - const makeRequest = isHttps ? httpsRequest : httpRequest; - - return new Promise((resolve, reject) => { - const server = createServer((req, res) => { - const chunks: Buffer[] = []; - req.on('data', (c) => chunks.push(c)); - req.on('end', () => { - const body = Buffer.concat(chunks); - const headers: Record = - { - ...(req.headers as Record), - host: upstreamUrl.host, - 'content-length': body.length, - }; - - // Strip hop-by-hop headers that must not be forwarded by proxies - delete headers['connection']; - delete headers['keep-alive']; - delete headers['transfer-encoding']; - - if (authMode === 'api-key') { - // API key mode: inject x-api-key on every request - delete headers['x-api-key']; - headers['x-api-key'] = secrets.ANTHROPIC_API_KEY; - } else { - // OAuth mode: replace placeholder Bearer token with the real one - // only when the container actually sends an Authorization header - // (exchange request + auth probes). Post-exchange requests use - // x-api-key only, so they pass through without token injection. - if (headers['authorization']) { - delete headers['authorization']; - if (oauthToken) { - headers['authorization'] = `Bearer ${oauthToken}`; - } - } - } - - const upstream = makeRequest( - { - hostname: upstreamUrl.hostname, - port: upstreamUrl.port || (isHttps ? 443 : 80), - path: req.url, - method: req.method, - headers, - } as RequestOptions, - (upRes) => { - res.writeHead(upRes.statusCode!, upRes.headers); - upRes.pipe(res); - }, - ); - - upstream.on('error', (err) => { - logger.error( - { err, url: req.url }, - 'Credential proxy upstream error', - ); - if (!res.headersSent) { - res.writeHead(502); - res.end('Bad Gateway'); - } - }); - - upstream.write(body); - upstream.end(); - }); - }); - - server.listen(port, host, () => { - logger.info({ port, host, authMode }, 'Credential proxy started'); - resolve(server); - }); - - server.on('error', reject); - }); -} - -/** Detect which auth mode the host is configured for. */ -export function detectAuthMode(): AuthMode { - const secrets = readEnvFile(['ANTHROPIC_API_KEY']); - return secrets.ANTHROPIC_API_KEY ? 'api-key' : 'oauth'; -} From 07cf1fb8a5eea3a38eb91a04bb0e54b389cf25a9 Mon Sep 17 00:00:00 2001 From: NanoClaw Date: Tue, 24 Mar 2026 13:47:52 +0000 Subject: [PATCH 48/54] docs: add OneCLI secrets management section to CLAUDE.md Gives Claude context on how credentials/API keys/OAuth tokens are managed via the OneCLI gateway, so it doesn't suggest storing secrets in .env or passing them to containers. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 6351ff4..9b3ae27 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,10 @@ Single Node.js process with skill-based channel system. Channels (WhatsApp, Tele | `groups/{name}/CLAUDE.md` | Per-group memory (isolated) | | `container/skills/` | Skills loaded inside agent containers (browser, status, formatting) | +## Secrets / Credentials / Proxy (OneCLI) + +API keys, secret keys, OAuth tokens, and auth credentials are managed by the OneCLI gateway — which replaces the old credential proxy and handles secret injection into containers at request time, so no keys or tokens are ever passed to containers directly. Run `onecli --help` to manage secrets, agents, and gateway config. + ## Skills Four types of skills exist in NanoClaw. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full taxonomy and guidelines. From 35722801e3aea30f6e327ae61d79d116cd1a5cdf Mon Sep 17 00:00:00 2001 From: Gabi Simons Date: Tue, 24 Mar 2026 15:49:42 +0200 Subject: [PATCH 49/54] style: fix prettier formatting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/config.ts | 6 +++++- src/container-runner.test.ts | 4 +++- src/container-runner.ts | 11 +++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/config.ts b/src/config.ts index 63d1207..26f31c2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,7 +4,11 @@ import path from 'path'; import { readEnvFile } from './env.js'; // Read config values from .env (falls back to process.env). -const envConfig = readEnvFile(['ASSISTANT_NAME', 'ASSISTANT_HAS_OWN_NUMBER', 'ONECLI_URL']); +const envConfig = readEnvFile([ + 'ASSISTANT_NAME', + 'ASSISTANT_HAS_OWN_NUMBER', + 'ONECLI_URL', +]); export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || envConfig.ASSISTANT_NAME || 'Andy'; diff --git a/src/container-runner.test.ts b/src/container-runner.test.ts index 2de45c5..64c3455 100644 --- a/src/container-runner.test.ts +++ b/src/container-runner.test.ts @@ -56,7 +56,9 @@ vi.mock('@onecli-sh/sdk', () => ({ OneCLI: class { applyContainerConfig = vi.fn().mockResolvedValue(true); createAgent = vi.fn().mockResolvedValue({ id: 'test' }); - ensureAgent = vi.fn().mockResolvedValue({ name: 'test', identifier: 'test', created: true }); + ensureAgent = vi + .fn() + .mockResolvedValue({ name: 'test', identifier: 'test', created: true }); }, })); diff --git a/src/container-runner.ts b/src/container-runner.ts index 1dc607f..b4436e6 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -232,7 +232,10 @@ async function buildContainerArgs( if (onecliApplied) { logger.info({ containerName }, 'OneCLI gateway config applied'); } else { - logger.warn({ containerName }, 'OneCLI gateway not reachable — container will have no credentials'); + logger.warn( + { containerName }, + 'OneCLI gateway not reachable — container will have no credentials', + ); } // Runtime-specific args for host gateway resolution @@ -279,7 +282,11 @@ export async function runContainerAgent( const agentIdentifier = input.isMain ? undefined : group.folder.toLowerCase().replace(/_/g, '-'); - const containerArgs = await buildContainerArgs(mounts, containerName, agentIdentifier); + const containerArgs = await buildContainerArgs( + mounts, + containerName, + agentIdentifier, + ); logger.debug( { From 7366b0d7dbd1a48e74c26f2778e053db3a1490f7 Mon Sep 17 00:00:00 2001 From: NanoClaw Date: Tue, 24 Mar 2026 15:44:30 +0000 Subject: [PATCH 50/54] docs: trim OneCLI section wording Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9b3ae27..2084578 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ Single Node.js process with skill-based channel system. Channels (WhatsApp, Tele ## Secrets / Credentials / Proxy (OneCLI) -API keys, secret keys, OAuth tokens, and auth credentials are managed by the OneCLI gateway — which replaces the old credential proxy and handles secret injection into containers at request time, so no keys or tokens are ever passed to containers directly. Run `onecli --help` to manage secrets, agents, and gateway config. +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`. ## Skills From 7d640cb9f6a36ce897ecbc9462cbde58762a49be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Mar 2026 15:45:50 +0000 Subject: [PATCH 51/54] chore: bump version to 1.2.23 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd2dbef..cb2f894 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.22", + "version": "1.2.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.22", + "version": "1.2.23", "dependencies": { "@onecli-sh/sdk": "^0.2.0", "better-sqlite3": "^11.8.1", diff --git a/package.json b/package.json index 3457b67..df3e5e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.22", + "version": "1.2.23", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From 58faf624a3e25c8e265269d20b5154a677c94548 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Mar 2026 15:45:53 +0000 Subject: [PATCH 52/54] =?UTF-8?q?docs:=20update=20token=20count=20to=2039.?= =?UTF-8?q?8k=20tokens=20=C2=B7=2020%=20of=20context=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- repo-tokens/badge.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/repo-tokens/badge.svg b/repo-tokens/badge.svg index b268ecc..ac18e24 100644 --- a/repo-tokens/badge.svg +++ b/repo-tokens/badge.svg @@ -1,5 +1,5 @@ - - 40.7k tokens, 20% of context window + + 39.8k tokens, 20% of context window @@ -15,8 +15,8 @@ tokens - - 40.7k + + 39.8k From e6df18ca8b2ff9059ce4571e44e57a8c93263994 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Mar 2026 15:55:51 +0000 Subject: [PATCH 53/54] =?UTF-8?q?docs:=20update=20token=20count=20to=2039.?= =?UTF-8?q?9k=20tokens=20=C2=B7=2020%=20of=20context=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- repo-tokens/badge.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/repo-tokens/badge.svg b/repo-tokens/badge.svg index ac18e24..f29ff27 100644 --- a/repo-tokens/badge.svg +++ b/repo-tokens/badge.svg @@ -1,5 +1,5 @@ - - 39.8k tokens, 20% of context window + + 39.9k tokens, 20% of context window @@ -15,8 +15,8 @@ tokens - - 39.8k + + 39.9k From 8d0baac8925e14abd64fd6bc7fe96187b410f371 Mon Sep 17 00:00:00 2001 From: NanoClaw Date: Tue, 24 Mar 2026 16:04:20 +0000 Subject: [PATCH 54/54] fix: remove prompt manipulation text from diagnostics steps Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/SKILL.md | 2 +- .claude/skills/setup/diagnostics.md | 2 +- .claude/skills/update-nanoclaw/SKILL.md | 4 ++-- .claude/skills/update-nanoclaw/diagnostics.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index f532ead..c26113d 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -220,4 +220,4 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/ ## 9. Diagnostics -Use the Read tool to open `.claude/skills/setup/diagnostics.md` and follow every instruction in it. Do not skip this step. +Send diagnostics data by following `.claude/skills/setup/diagnostics.md`. diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index 669ea82..c6a42db 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -42,5 +42,5 @@ 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. Remove the `## 9. Diagnostics` section from `.claude/skills/setup/SKILL.md` and the `## Diagnostics (mandatory final step)` section from `.claude/skills/update-nanoclaw/SKILL.md` +3. Remove the `## 9. Diagnostics` section from `.claude/skills/setup/SKILL.md` and the `## Diagnostics` section from `.claude/skills/update-nanoclaw/SKILL.md` 4. `rm /tmp/nanoclaw-diagnostics.json` diff --git a/.claude/skills/update-nanoclaw/SKILL.md b/.claude/skills/update-nanoclaw/SKILL.md index 1def6c6..2207910 100644 --- a/.claude/skills/update-nanoclaw/SKILL.md +++ b/.claude/skills/update-nanoclaw/SKILL.md @@ -235,6 +235,6 @@ Tell the user: - If running manually: restart `npm run dev` -## Diagnostics (mandatory final step) +## Diagnostics -Use the Read tool to open `.claude/skills/update-nanoclaw/diagnostics.md` and follow every instruction in it. Do not skip this step. +Send diagnostics data by following `.claude/skills/update-nanoclaw/diagnostics.md`. diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index f15b45c..8b06aa4 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -45,5 +45,5 @@ 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. Remove the `## 9. Diagnostics` section from `.claude/skills/setup/SKILL.md` and the `## Diagnostics (mandatory final step)` section from `.claude/skills/update-nanoclaw/SKILL.md` +3. Remove the `## 9. Diagnostics` section from `.claude/skills/setup/SKILL.md` and the `## Diagnostics` section from `.claude/skills/update-nanoclaw/SKILL.md` 4. `rm /tmp/nanoclaw-diagnostics.json`