From 32dda34af49f8d70c188860f87961fe947bafa43 Mon Sep 17 00:00:00 2001 From: tomermesser Date: Sun, 8 Mar 2026 16:38:03 +0200 Subject: [PATCH 01/24] status-icon-01 --- .claude/skills/add-statusbar/SKILL.md | 140 ++++++++++++++++++ .../add-statusbar/add/src/statusbar.swift | 139 +++++++++++++++++ .claude/skills/add-statusbar/manifest.yaml | 10 ++ 3 files changed, 289 insertions(+) create mode 100644 .claude/skills/add-statusbar/SKILL.md create mode 100644 .claude/skills/add-statusbar/add/src/statusbar.swift create mode 100644 .claude/skills/add-statusbar/manifest.yaml diff --git a/.claude/skills/add-statusbar/SKILL.md b/.claude/skills/add-statusbar/SKILL.md new file mode 100644 index 0000000..c0f343c --- /dev/null +++ b/.claude/skills/add-statusbar/SKILL.md @@ -0,0 +1,140 @@ +--- +name: add-statusbar +description: Add a macOS menu bar status indicator for NanoClaw. Shows a ⚡ icon with a green/red dot indicating whether NanoClaw is running, with Start, Stop, and Restart controls. macOS only. +--- + +# Add macOS Menu Bar Status Indicator + +Adds a persistent menu bar icon that shows NanoClaw's running status and lets the user start, stop, or restart the service — similar to how Docker Desktop appears in the menu bar. + +**macOS only.** Requires Xcode Command Line Tools (`swiftc`). + +## Phase 1: Pre-flight + +### Check platform + +If not on macOS, stop and tell the user: + +> This skill is macOS only. The menu bar status indicator uses AppKit and requires `swiftc` (Xcode Command Line Tools). + +### Check for swiftc + +```bash +which swiftc +``` + +If not found, tell the user: + +> Xcode Command Line Tools are required. Install them by running: +> +> ```bash +> xcode-select --install +> ``` +> +> Then re-run `/add-statusbar`. + +### Check if already installed + +```bash +launchctl list | grep com.nanoclaw.statusbar +``` + +If it returns a PID (not `-`), tell the user it's already installed and skip to Phase 4 (Verify). + +## Phase 2: Apply Code Changes + +### Initialize skills system (if needed) + +If `.nanoclaw/` directory doesn't exist yet: + +```bash +npx tsx scripts/apply-skill.ts --init +``` + +### Apply the skill + +```bash +npx tsx scripts/apply-skill.ts .claude/skills/add-statusbar +``` + +This copies `src/statusbar.swift` into the project and records the application in `.nanoclaw/state.yaml`. + +## Phase 3: Compile and Install + +### Compile the Swift binary + +```bash +swiftc -O -o dist/statusbar src/statusbar.swift +``` + +This produces a small (~55KB) native binary at `dist/statusbar`. + +### Create the launchd plist + +Determine the absolute project root: + +```bash +pwd +``` + +Create `~/Library/LaunchAgents/com.nanoclaw.statusbar.plist`, substituting the actual values for `{PROJECT_ROOT}` and `{HOME}`: + +```xml + + + + + Label + com.nanoclaw.statusbar + ProgramArguments + + {PROJECT_ROOT}/dist/statusbar + + RunAtLoad + + KeepAlive + + EnvironmentVariables + + HOME + {HOME} + + StandardOutPath + {PROJECT_ROOT}/logs/statusbar.log + StandardErrorPath + {PROJECT_ROOT}/logs/statusbar.error.log + + +``` + +### Load the service + +```bash +launchctl load ~/Library/LaunchAgents/com.nanoclaw.statusbar.plist +``` + +## Phase 4: Verify + +```bash +launchctl list | grep com.nanoclaw.statusbar +``` + +The first column should show a PID (not `-`). + +Tell the user: + +> The ⚡ icon should now appear in your macOS menu bar. Click it to see NanoClaw's status and control the service. +> +> - **Green dot** — NanoClaw is running +> - **Red dot** — NanoClaw is stopped +> +> Use **Restart** after making code changes, and **View Logs** to open the log file directly. + +## Removal + +```bash +launchctl unload ~/Library/LaunchAgents/com.nanoclaw.statusbar.plist +rm ~/Library/LaunchAgents/com.nanoclaw.statusbar.plist +rm dist/statusbar +rm src/statusbar.swift +``` diff --git a/.claude/skills/add-statusbar/add/src/statusbar.swift b/.claude/skills/add-statusbar/add/src/statusbar.swift new file mode 100644 index 0000000..6fff79a --- /dev/null +++ b/.claude/skills/add-statusbar/add/src/statusbar.swift @@ -0,0 +1,139 @@ +import AppKit + +class StatusBarController: NSObject { + private var statusItem: NSStatusItem! + private var isRunning = false + private var timer: Timer? + + private let plistPath = "\(NSHomeDirectory())/Library/LaunchAgents/com.nanoclaw.plist" + + override init() { + super.init() + setupStatusItem() + isRunning = checkRunning() + updateMenu() + // Poll every 5 seconds to reflect external state changes + timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in + guard let self else { return } + let current = self.checkRunning() + if current != self.isRunning { + self.isRunning = current + self.updateMenu() + } + } + } + + private func setupStatusItem() { + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + if let button = statusItem.button { + if let image = NSImage(systemSymbolName: "bolt.fill", accessibilityDescription: "NanoClaw") { + image.isTemplate = true + button.image = image + } else { + button.title = "⚡" + } + button.toolTip = "NanoClaw" + } + } + + private func checkRunning() -> Bool { + let task = Process() + task.launchPath = "/bin/launchctl" + task.arguments = ["list", "com.nanoclaw"] + let pipe = Pipe() + task.standardOutput = pipe + task.standardError = Pipe() + guard (try? task.run()) != nil else { return false } + task.waitUntilExit() + if task.terminationStatus != 0 { return false } + let output = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" + // launchctl list output: "PID\tExitCode\tLabel" — "-" means not running + let pid = output.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: "\t").first ?? "-" + return pid != "-" + } + + private func updateMenu() { + let menu = NSMenu() + + // Status row with colored dot + let statusItem = NSMenuItem() + let dot = "● " + let dotColor: NSColor = isRunning ? .systemGreen : .systemRed + let attr = NSMutableAttributedString(string: dot, attributes: [.foregroundColor: dotColor]) + let label = isRunning ? "NanoClaw is running" : "NanoClaw is stopped" + attr.append(NSAttributedString(string: label, attributes: [.foregroundColor: NSColor.labelColor])) + statusItem.attributedTitle = attr + statusItem.isEnabled = false + menu.addItem(statusItem) + + menu.addItem(NSMenuItem.separator()) + + if isRunning { + let stop = NSMenuItem(title: "Stop", action: #selector(stopService), keyEquivalent: "") + stop.target = self + menu.addItem(stop) + + let restart = NSMenuItem(title: "Restart", action: #selector(restartService), keyEquivalent: "r") + restart.target = self + menu.addItem(restart) + } else { + let start = NSMenuItem(title: "Start", action: #selector(startService), keyEquivalent: "") + start.target = self + menu.addItem(start) + } + + menu.addItem(NSMenuItem.separator()) + + let logs = NSMenuItem(title: "View Logs", action: #selector(viewLogs), keyEquivalent: "") + logs.target = self + menu.addItem(logs) + + self.statusItem.menu = menu + } + + @objc private func startService() { + run("/bin/launchctl", ["load", plistPath]) + refresh(after: 2) + } + + @objc private func stopService() { + run("/bin/launchctl", ["unload", plistPath]) + refresh(after: 2) + } + + @objc private func restartService() { + let uid = getuid() + run("/bin/launchctl", ["kickstart", "-k", "gui/\(uid)/com.nanoclaw"]) + refresh(after: 3) + } + + @objc private func viewLogs() { + let logPath = "\(NSHomeDirectory())/Documents/Projects/nanoclaw/logs/nanoclaw.log" + NSWorkspace.shared.open(URL(fileURLWithPath: logPath)) + } + + private func refresh(after seconds: Double) { + DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in + guard let self else { return } + self.isRunning = self.checkRunning() + self.updateMenu() + } + } + + @discardableResult + private func run(_ path: String, _ args: [String]) -> Int32 { + let task = Process() + task.launchPath = path + task.arguments = args + task.standardOutput = Pipe() + task.standardError = Pipe() + try? task.run() + task.waitUntilExit() + return task.terminationStatus + } +} + +let app = NSApplication.shared +app.setActivationPolicy(.accessory) +let controller = StatusBarController() +app.run() diff --git a/.claude/skills/add-statusbar/manifest.yaml b/.claude/skills/add-statusbar/manifest.yaml new file mode 100644 index 0000000..0d7d720 --- /dev/null +++ b/.claude/skills/add-statusbar/manifest.yaml @@ -0,0 +1,10 @@ +skill: statusbar +version: 1.0.0 +description: "macOS menu bar status indicator — shows NanoClaw running state with start/stop/restart controls" +core_version: 0.1.0 +adds: + - src/statusbar.swift +modifies: [] +structured: {} +conflicts: [] +depends: [] From 00ff0e00ebd5bc0643956dd6c2b06d0b2857fced Mon Sep 17 00:00:00 2001 From: RichardCao Date: Mon, 23 Mar 2026 16:51:25 +0800 Subject: [PATCH 02/24] fix(db): default Telegram backfill chats to DMs --- src/db-migration.test.ts | 67 ++++++++++++++++++++++++++++++++++++++++ src/db.ts | 7 ++++- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/db-migration.test.ts diff --git a/src/db-migration.test.ts b/src/db-migration.test.ts new file mode 100644 index 0000000..e26873d --- /dev/null +++ b/src/db-migration.test.ts @@ -0,0 +1,67 @@ +import Database from 'better-sqlite3'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { describe, expect, it, vi } from 'vitest'; + +describe('database migrations', () => { + it('defaults Telegram backfill chats to direct messages', async () => { + const repoRoot = process.cwd(); + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nanoclaw-db-test-')); + + try { + process.chdir(tempDir); + fs.mkdirSync(path.join(tempDir, 'store'), { recursive: true }); + + const dbPath = path.join(tempDir, 'store', 'messages.db'); + const legacyDb = new Database(dbPath); + legacyDb.exec(` + CREATE TABLE chats ( + jid TEXT PRIMARY KEY, + name TEXT, + last_message_time TEXT + ); + `); + legacyDb + .prepare( + `INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)`, + ) + .run('tg:12345', 'Telegram DM', '2024-01-01T00:00:00.000Z'); + legacyDb + .prepare( + `INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)`, + ) + .run('tg:-10012345', 'Telegram Group', '2024-01-01T00:00:01.000Z'); + legacyDb + .prepare( + `INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)`, + ) + .run('room@g.us', 'WhatsApp Group', '2024-01-01T00:00:02.000Z'); + legacyDb.close(); + + vi.resetModules(); + const { initDatabase, getAllChats, _closeDatabase } = + await import('./db.js'); + + initDatabase(); + + const chats = getAllChats(); + expect(chats.find((chat) => chat.jid === 'tg:12345')).toMatchObject({ + channel: 'telegram', + is_group: 0, + }); + expect(chats.find((chat) => chat.jid === 'tg:-10012345')).toMatchObject({ + channel: 'telegram', + is_group: 0, + }); + expect(chats.find((chat) => chat.jid === 'room@g.us')).toMatchObject({ + channel: 'whatsapp', + is_group: 1, + }); + + _closeDatabase(); + } finally { + process.chdir(repoRoot); + } + }); +}); diff --git a/src/db.ts b/src/db.ts index 0896f41..1a097c0 100644 --- a/src/db.ts +++ b/src/db.ts @@ -134,7 +134,7 @@ function createSchema(database: Database.Database): void { `UPDATE chats SET channel = 'discord', is_group = 1 WHERE jid LIKE 'dc:%'`, ); database.exec( - `UPDATE chats SET channel = 'telegram', is_group = 1 WHERE jid LIKE 'tg:%'`, + `UPDATE chats SET channel = 'telegram', is_group = 0 WHERE jid LIKE 'tg:%'`, ); } catch { /* columns already exist */ @@ -158,6 +158,11 @@ export function _initTestDatabase(): void { createSchema(db); } +/** @internal - for tests only. */ +export function _closeDatabase(): void { + db.close(); +} + /** * Store chat metadata only (no message content). * Used for all chats to enable group discovery without storing sensitive content. From d40affbdef3fb4c86cf6fbe121d43e96693ad78e Mon Sep 17 00:00:00 2001 From: Shawn Yeager Date: Mon, 23 Mar 2026 13:41:20 +0000 Subject: [PATCH 03/24] fix: skip bump-version and update-tokens on forks These workflows use APP_ID/APP_PRIVATE_KEY secrets that only exist on the upstream repo. Without a fork guard they fail on every push for every fork. merge-forward-skills already has the correct guard. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/bump-version.yml | 1 + .github/workflows/update-tokens.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index fb77595..8191085 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -7,6 +7,7 @@ on: jobs: bump-version: + if: github.repository == 'qwibitai/nanoclaw' runs-on: ubuntu-latest steps: - uses: actions/create-github-app-token@v1 diff --git a/.github/workflows/update-tokens.yml b/.github/workflows/update-tokens.yml index 753da18..9b25c55 100644 --- a/.github/workflows/update-tokens.yml +++ b/.github/workflows/update-tokens.yml @@ -8,6 +8,7 @@ on: jobs: update-tokens: + if: github.repository == 'qwibitai/nanoclaw' runs-on: ubuntu-latest steps: - uses: actions/create-github-app-token@v1 From ff16e93713de67312ae029b8bfb6474d75554881 Mon Sep 17 00:00:00 2001 From: Akasha Date: Sun, 22 Mar 2026 16:53:42 -0400 Subject: [PATCH 04/24] fix: skip mount-allowlist write if file already exists /setup overwrote ~/.config/nanoclaw/mount-allowlist.json unconditionally, clobbering any user customizations made after initial setup. Now checks for the file first and skips with a 'skipped' status if it exists. Co-Authored-By: Claude Sonnet 4.6 --- setup/mounts.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/setup/mounts.ts b/setup/mounts.ts index eb2a5f6..a3377d3 100644 --- a/setup/mounts.ts +++ b/setup/mounts.ts @@ -37,6 +37,21 @@ export async function run(args: string[]): Promise { fs.mkdirSync(configDir, { recursive: true }); + if (fs.existsSync(configFile)) { + logger.info( + { configFile }, + 'Mount allowlist already exists — skipping (use --force to overwrite)', + ); + emitStatus('CONFIGURE_MOUNTS', { + PATH: configFile, + ALLOWED_ROOTS: 0, + NON_MAIN_READ_ONLY: 'unknown', + STATUS: 'skipped', + LOG: 'logs/setup.log', + }); + return; + } + let allowedRoots = 0; let nonMainReadOnly = 'true'; From 5f426465981f6e407412847cae7ea67999cb1e01 Mon Sep 17 00:00:00 2001 From: Akasha Date: Mon, 23 Mar 2026 16:57:09 -0400 Subject: [PATCH 05/24] fix: implement --force flag for mount-allowlist overwrite The skip message mentioned --force but parseArgs didn't handle it, making it a false promise. Now --force is parsed and passed through, allowing users to regenerate the mount allowlist when needed. Co-Authored-By: Claude Sonnet 4.6 --- setup/mounts.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/setup/mounts.ts b/setup/mounts.ts index a3377d3..e14d23b 100644 --- a/setup/mounts.ts +++ b/setup/mounts.ts @@ -10,21 +10,23 @@ import { logger } from '../src/logger.js'; import { isRoot } from './platform.js'; import { emitStatus } from './status.js'; -function parseArgs(args: string[]): { empty: boolean; json: string } { +function parseArgs(args: string[]): { empty: boolean; json: string; force: boolean } { let empty = false; let json = ''; + let force = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--empty') empty = true; + if (args[i] === '--force') force = true; if (args[i] === '--json' && args[i + 1]) { json = args[i + 1]; i++; } } - return { empty, json }; + return { empty, json, force }; } export async function run(args: string[]): Promise { - const { empty, json } = parseArgs(args); + const { empty, json, force } = parseArgs(args); const homeDir = os.homedir(); const configDir = path.join(homeDir, '.config', 'nanoclaw'); const configFile = path.join(configDir, 'mount-allowlist.json'); @@ -37,7 +39,7 @@ export async function run(args: string[]): Promise { fs.mkdirSync(configDir, { recursive: true }); - if (fs.existsSync(configFile)) { + if (fs.existsSync(configFile) && !force) { logger.info( { configFile }, 'Mount allowlist already exists — skipping (use --force to overwrite)', From 724fe7250dd44b336abc7208d2b37f95db646b8c Mon Sep 17 00:00:00 2001 From: Ken Bolton Date: Mon, 23 Mar 2026 20:27:40 -0400 Subject: [PATCH 06/24] fix(claw): mount group folder and sessions into container claw was running containers with no volume mounts, so the agent always saw an empty /workspace/group. Add build_mounts() to replicate the same bind-mounts that container-runner.ts sets up (group folder, .claude sessions, IPC dir, agent-runner source, and project root for main). Also includes upstream fix from qwibitai/nanoclaw#1368: graceful terminate() before kill() on output sentinel, and early return after a successful structured response so exit code stays 0. Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/claw/scripts/claw | 64 ++++++++++++++++++++++++++++++-- src/claw-skill.test.ts | 45 ++++++++++++++++++++++ 2 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 src/claw-skill.test.ts diff --git a/.claude/skills/claw/scripts/claw b/.claude/skills/claw/scripts/claw index 3878e48..b64a225 100644 --- a/.claude/skills/claw/scripts/claw +++ b/.claude/skills/claw/scripts/claw @@ -121,8 +121,48 @@ def find_group(groups: list[dict], query: str) -> dict | None: return None -def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) -> None: - cmd = [runtime, "run", "-i", "--rm", image] +def build_mounts(folder: str, is_main: bool) -> list[tuple[str, str, bool]]: + """Return list of (host_path, container_path, readonly) tuples.""" + groups_dir = NANOCLAW_DIR / "groups" + data_dir = NANOCLAW_DIR / "data" + sessions_dir = data_dir / "sessions" / folder + ipc_dir = data_dir / "ipc" / folder + + # Ensure required dirs exist + group_dir = groups_dir / folder + group_dir.mkdir(parents=True, exist_ok=True) + (sessions_dir / ".claude").mkdir(parents=True, exist_ok=True) + for sub in ("messages", "tasks", "input"): + (ipc_dir / sub).mkdir(parents=True, exist_ok=True) + + agent_runner_src = sessions_dir / "agent-runner-src" + project_agent_runner = NANOCLAW_DIR / "container" / "agent-runner" / "src" + if not agent_runner_src.exists() and project_agent_runner.exists(): + import shutil + shutil.copytree(project_agent_runner, agent_runner_src) + + mounts: list[tuple[str, str, bool]] = [] + if is_main: + mounts.append((str(NANOCLAW_DIR), "/workspace/project", True)) + mounts.append((str(group_dir), "/workspace/group", False)) + mounts.append((str(sessions_dir / ".claude"), "/home/node/.claude", False)) + mounts.append((str(ipc_dir), "/workspace/ipc", False)) + if agent_runner_src.exists(): + mounts.append((str(agent_runner_src), "/app/src", False)) + return mounts + + +def run_container(runtime: str, image: str, payload: dict, + folder: str | None = None, is_main: bool = False, + timeout: int = 300) -> None: + cmd = [runtime, "run", "-i", "--rm"] + if folder: + for host, container, readonly in build_mounts(folder, is_main): + if readonly: + cmd += ["--mount", f"type=bind,source={host},target={container},readonly"] + else: + cmd += ["-v", f"{host}:{container}"] + cmd.append(image) dbg(f"cmd: {' '.join(cmd)}") # Show payload sans secrets @@ -167,7 +207,12 @@ def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) - dbg("output sentinel found, terminating container") done.set() try: - proc.kill() + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + dbg("graceful stop timed out, force killing container") + proc.kill() except ProcessLookupError: pass return @@ -197,6 +242,8 @@ def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) - stdout, re.DOTALL, ) + success = False + if match: try: data = json.loads(match.group(1)) @@ -206,6 +253,7 @@ def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) - session_id = data.get("newSessionId") or data.get("sessionId") if session_id: print(f"\n[session: {session_id}]", file=sys.stderr) + success = True else: print(f"[{status}] {data.get('result', '')}", file=sys.stderr) sys.exit(1) @@ -215,6 +263,9 @@ def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) - # No structured output — print raw stdout print(stdout) + if success: + return + if proc.returncode not in (0, None): sys.exit(proc.returncode) @@ -273,6 +324,7 @@ def main(): # Resolve group → jid jid = args.jid group_name = None + group_folder = None is_main = False if args.group: @@ -281,6 +333,7 @@ def main(): sys.exit(f"error: group '{args.group}' not found. Run --list-groups to see options.") jid = g["jid"] group_name = g["name"] + group_folder = g["folder"] is_main = g["is_main"] elif not jid: # Default: main group @@ -288,6 +341,7 @@ def main(): if mains: jid = mains[0]["jid"] group_name = mains[0]["name"] + group_folder = mains[0]["folder"] is_main = True else: sys.exit("error: no group specified and no main group found. Use -g or -j.") @@ -311,7 +365,9 @@ def main(): payload["resumeAt"] = "latest" print(f"[{group_name or jid}] running via {runtime}...", file=sys.stderr) - run_container(runtime, args.image, payload, timeout=args.timeout) + run_container(runtime, args.image, payload, + folder=group_folder, is_main=is_main, + timeout=args.timeout) if __name__ == "__main__": diff --git a/src/claw-skill.test.ts b/src/claw-skill.test.ts new file mode 100644 index 0000000..24260c9 --- /dev/null +++ b/src/claw-skill.test.ts @@ -0,0 +1,45 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { spawnSync } from 'child_process'; + +import { describe, expect, it } from 'vitest'; + +describe('claw skill script', () => { + it('exits zero after successful structured output even if the runtime is terminated', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claw-skill-test-')); + const binDir = path.join(tempDir, 'bin'); + fs.mkdirSync(binDir, { recursive: true }); + + const runtimePath = path.join(binDir, 'container'); + fs.writeFileSync( + runtimePath, + `#!/bin/sh +cat >/dev/null +printf '%s\n' '---NANOCLAW_OUTPUT_START---' '{"status":"success","result":"4","newSessionId":"sess-1"}' '---NANOCLAW_OUTPUT_END---' +sleep 30 +`, + ); + fs.chmodSync(runtimePath, 0o755); + + const result = spawnSync( + 'python3', + ['.claude/skills/claw/scripts/claw', '-j', 'tg:123', 'What is 2+2?'], + { + cwd: process.cwd(), + encoding: 'utf8', + env: { + ...process.env, + NANOCLAW_DIR: tempDir, + PATH: `${binDir}:${process.env.PATH || ''}`, + }, + timeout: 15000, + }, + ); + + expect(result.status).toBe(0); + expect(result.signal).toBeNull(); + expect(result.stdout).toContain('4'); + expect(result.stderr).toContain('[session: sess-1]'); + }); +}); From 7bfd060536a183a667d8e5f65286ec7157a0ac92 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Mar 2026 20:47:41 +0000 Subject: [PATCH 07/24] chore: bump version to 1.2.33 --- 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 987b285..e8033ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.32", + "version": "1.2.33", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.32", + "version": "1.2.33", "dependencies": { "@onecli-sh/sdk": "^0.2.0", "better-sqlite3": "11.10.0", diff --git a/package.json b/package.json index 0095817..dfa9afa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.32", + "version": "1.2.33", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From 68c59a1abfffc647849b3201513ff17e970532c9 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Wed, 25 Mar 2026 23:09:33 +0200 Subject: [PATCH 08/24] feat(skill): add Emacs channel skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds SKILL.md for the Emacs channel — an HTTP bridge that lets Emacs send messages to NanoClaw and poll for responses. Source code lives on the skill/emacs branch. Co-Authored-By: Ken Bolton Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/add-emacs/SKILL.md | 289 ++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 .claude/skills/add-emacs/SKILL.md diff --git a/.claude/skills/add-emacs/SKILL.md b/.claude/skills/add-emacs/SKILL.md new file mode 100644 index 0000000..09bdbdd --- /dev/null +++ b/.claude/skills/add-emacs/SKILL.md @@ -0,0 +1,289 @@ +--- +name: add-emacs +description: Add Emacs as a channel. Opens an interactive chat buffer and org-mode integration so you can talk to NanoClaw from within Emacs (Doom, Spacemacs, or vanilla). Uses a local HTTP bridge — no bot token or external service needed. +--- + +# Add Emacs Channel + +This skill adds Emacs support to NanoClaw, then walks through interactive setup. +Works with Doom Emacs, Spacemacs, and vanilla Emacs 27.1+. + +## What you can do with this + +- **Ask while coding** — open the chat buffer (`C-c n c` / `SPC N c`), ask about a function or error without leaving Emacs +- **Code review** — select a region and send it with `nanoclaw-org-send`; the response appears as a child heading inline in your org file +- **Meeting notes** — send an org agenda entry; get a summary or action item list back as a child node +- **Draft writing** — send org prose; receive revisions or continuations in place +- **Research capture** — ask a question directly in your org notes; the answer lands exactly where you need it +- **Schedule tasks** — ask Andy to set a reminder or create a scheduled NanoClaw task (e.g. "remind me tomorrow to review the PR") + +## Phase 1: Pre-flight + +### Check if already applied + +Check if `src/channels/emacs.ts` exists: + +```bash +test -f src/channels/emacs.ts && echo "already applied" || echo "not applied" +``` + +If it exists, skip to Phase 3 (Setup). The code changes are already in place. + +## Phase 2: Apply Code Changes + +### Ensure the upstream remote + +```bash +git remote -v +``` + +If an `upstream` remote pointing to `https://github.com/qwibitai/nanoclaw.git` is missing, +add it: + +```bash +git remote add upstream https://github.com/qwibitai/nanoclaw.git +``` + +### Merge the skill branch + +```bash +git fetch upstream skill/emacs +git merge upstream/skill/emacs +``` + +If there are merge conflicts on `package-lock.json`, resolve them by accepting the incoming +version and continuing: + +```bash +git checkout --theirs package-lock.json +git add package-lock.json +git merge --continue +``` + +For any other conflict, read the conflicted file and reconcile both sides manually. + +This adds: +- `src/channels/emacs.ts` — `EmacsBridgeChannel` HTTP server (port 8766) +- `src/channels/emacs.test.ts` — unit tests +- `emacs/nanoclaw.el` — Emacs Lisp package (`nanoclaw-chat`, `nanoclaw-org-send`) +- `import './emacs.js'` appended to `src/channels/index.ts` + +If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides. + +### Validate code changes + +```bash +npm run build +npx vitest run src/channels/emacs.test.ts +``` + +Build must be clean and tests must pass before proceeding. + +## Phase 3: Setup + +### Configure environment (optional) + +The channel works out of the box with defaults. Add to `.env` only if you need non-defaults: + +```bash +EMACS_CHANNEL_PORT=8766 # default — change if 8766 is already in use +EMACS_AUTH_TOKEN= # optional — locks the endpoint to Emacs only +``` + +If you change or add values, sync to the container environment: + +```bash +mkdir -p data/env && cp .env data/env/env +``` + +### Configure Emacs + +The `nanoclaw.el` package requires only Emacs 27.1+ built-in libraries (`url`, `json`, `org`) — no package manager setup needed. + +AskUserQuestion: Which Emacs distribution are you using? +- **Doom Emacs** - config.el with map! keybindings +- **Spacemacs** - dotspacemacs/user-config in ~/.spacemacs +- **Vanilla Emacs / other** - init.el with global-set-key + +**Doom Emacs** — add to `~/.config/doom/config.el` (or `~/.doom.d/config.el`): + +```elisp +;; NanoClaw — personal AI assistant channel +(load (expand-file-name "~/src/nanoclaw/emacs/nanoclaw.el")) + +(map! :leader + :prefix ("N" . "NanoClaw") + :desc "Chat buffer" "c" #'nanoclaw-chat + :desc "Send org" "o" #'nanoclaw-org-send) +``` + +Then reload: `M-x doom/reload` + +**Spacemacs** — add to `dotspacemacs/user-config` in `~/.spacemacs`: + +```elisp +;; NanoClaw — personal AI assistant channel +(load-file "~/src/nanoclaw/emacs/nanoclaw.el") + +(spacemacs/set-leader-keys "aNc" #'nanoclaw-chat) +(spacemacs/set-leader-keys "aNo" #'nanoclaw-org-send) +``` + +Then reload: `M-x dotspacemacs/sync-configuration-layers` or restart Emacs. + +**Vanilla Emacs** — add to `~/.emacs.d/init.el` (or `~/.emacs`): + +```elisp +;; NanoClaw — personal AI assistant channel +(load-file "~/src/nanoclaw/emacs/nanoclaw.el") + +(global-set-key (kbd "C-c n c") #'nanoclaw-chat) +(global-set-key (kbd "C-c n o") #'nanoclaw-org-send) +``` + +Then reload: `M-x eval-buffer` or restart Emacs. + +If `EMACS_AUTH_TOKEN` was set, also add (any distribution): + +```elisp +(setq nanoclaw-auth-token "") +``` + +If `EMACS_CHANNEL_PORT` was changed from the default, also add: + +```elisp +(setq nanoclaw-port ) +``` + +### Restart NanoClaw + +```bash +npm run build +launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS +# Linux: systemctl --user restart nanoclaw +``` + +## Phase 4: Verify + +### Test the HTTP endpoint + +```bash +curl -s "http://localhost:8766/api/messages?since=0" +``` + +Expected: `{"messages":[]}` + +If you set `EMACS_AUTH_TOKEN`: + +```bash +curl -s -H "Authorization: Bearer " "http://localhost:8766/api/messages?since=0" +``` + +### Test from Emacs + +Tell the user: + +> 1. Open the chat buffer with your keybinding (`SPC N c`, `SPC a N c`, or `C-c n c`) +> 2. Type a message and press `RET` +> 3. A response from Andy should appear within a few seconds +> +> For org-mode: open any `.org` file, position the cursor on a heading, and use `SPC N o` / `SPC a N o` / `C-c n o` + +### Check logs if needed + +```bash +tail -f logs/nanoclaw.log +``` + +Look for `Emacs channel listening` at startup and `Emacs message received` when a message is sent. + +## Troubleshooting + +### Port already in use + +``` +Error: listen EADDRINUSE: address already in use :::8766 +``` + +Either a stale NanoClaw process is running, or 8766 is taken by another app. + +Find and kill the stale process: + +```bash +lsof -ti :8766 | xargs kill -9 +``` + +Or change the port in `.env` (`EMACS_CHANNEL_PORT=8767`) and update `nanoclaw-port` in Emacs config. + +### No response from agent + +Check: +1. NanoClaw is running: `launchctl list | grep nanoclaw` (macOS) or `systemctl --user status nanoclaw` (Linux) +2. Emacs group is registered: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid = 'emacs:default'"` +3. Logs show activity: `tail -50 logs/nanoclaw.log` + +If the group is not registered, it will be created automatically on the next NanoClaw restart. + +### Auth token mismatch (401 Unauthorized) + +Verify the token in Emacs matches `.env`: + +```elisp +;; M-x describe-variable RET nanoclaw-auth-token RET +``` + +Must exactly match `EMACS_AUTH_TOKEN` in `.env`. + +### nanoclaw.el not loading + +Check the path is correct: + +```bash +ls ~/src/nanoclaw/emacs/nanoclaw.el +``` + +If NanoClaw is cloned elsewhere, update the `load`/`load-file` path in your Emacs config. + +## After Setup + +If running `npm run dev` while the service is active: + +```bash +# macOS: +launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist +npm run dev +# When done testing: +launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist + +# Linux: +# systemctl --user stop nanoclaw +# npm run dev +# systemctl --user start nanoclaw +``` + +## Agent Formatting + +The Emacs bridge converts markdown → org-mode automatically. Agents should +output standard markdown — **not** org-mode syntax. The conversion handles: + +| Markdown | Org-mode | +|----------|----------| +| `**bold**` | `*bold*` | +| `*italic*` | `/italic/` | +| `~~text~~` | `+text+` | +| `` `code` `` | `~code~` | +| ` ```lang ` | `#+begin_src lang` | + +If an agent outputs org-mode directly, bold/italic/etc. will be double-converted +and render incorrectly. + +## Removal + +To remove the Emacs channel: + +1. Delete `src/channels/emacs.ts`, `src/channels/emacs.test.ts`, and `emacs/nanoclaw.el` +2. Remove `import './emacs.js'` from `src/channels/index.ts` +3. Remove the NanoClaw block from your Emacs config file +4. Remove Emacs registration from SQLite: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid = 'emacs:default'"` +5. Remove `EMACS_CHANNEL_PORT` and `EMACS_AUTH_TOKEN` from `.env` if set +6. Rebuild: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux) \ No newline at end of file From 125757bc7d2b7326ea412dc5dadb0673bc5be937 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Mar 2026 21:29:02 +0000 Subject: [PATCH 09/24] chore: bump version to 1.2.34 --- 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 e8033ad..2c69d40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.33", + "version": "1.2.34", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.33", + "version": "1.2.34", "dependencies": { "@onecli-sh/sdk": "^0.2.0", "better-sqlite3": "11.10.0", diff --git a/package.json b/package.json index dfa9afa..0d76ee5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.33", + "version": "1.2.34", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", From 2cddefbef4616b5cde41afbe954fbd81f4c059e1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Mar 2026 21:29:08 +0000 Subject: [PATCH 10/24] =?UTF-8?q?docs:=20update=20token=20count=20to=2041.?= =?UTF-8?q?3k=20tokens=20=C2=B7=2021%=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 50f3af8..58e9bb3 100644 --- a/repo-tokens/badge.svg +++ b/repo-tokens/badge.svg @@ -1,5 +1,5 @@ - - 41.2k tokens, 21% of context window + + 41.3k tokens, 21% of context window @@ -15,8 +15,8 @@ tokens - - 41.2k + + 41.3k From 2c447085b5de65069d4f7d895312948c67b65969 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Wed, 25 Mar 2026 23:41:24 +0200 Subject: [PATCH 11/24] chore: add edwinwzhe to contributors Co-Authored-By: Claude Opus 4.6 (1M context) --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ca7f3cb..143392b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -14,3 +14,4 @@ Thanks to everyone who has contributed to NanoClaw! - [Michaelliv](https://github.com/Michaelliv) — Michael - [kk17](https://github.com/kk17) — Kyle Zhike Chen - [flobo3](https://github.com/flobo3) — Flo +- [edwinwzhe](https://github.com/edwinwzhe) — Edwin He From 9413ace113b2a1f1c7cfcea91cb3697b99333861 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Wed, 25 Mar 2026 23:43:54 +0200 Subject: [PATCH 12/24] chore: add edwinwzhe and scottgl9 to contributors Co-Authored-By: Edwin He Co-Authored-By: Scott Glover --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 143392b..4038595 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -15,3 +15,4 @@ Thanks to everyone who has contributed to NanoClaw! - [kk17](https://github.com/kk17) — Kyle Zhike Chen - [flobo3](https://github.com/flobo3) — Flo - [edwinwzhe](https://github.com/edwinwzhe) — Edwin He +- [scottgl9](https://github.com/scottgl9) — Scott Glover From 349b54ae9ef8a8d2991394632fce224cae63eae0 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Wed, 25 Mar 2026 23:54:05 +0200 Subject: [PATCH 13/24] fix(add-statusbar): derive log path from binary location, fix SKILL.md - statusbar.swift: derive project root from binary location instead of hardcoding ~/Documents/Projects/nanoclaw - SKILL.md: remove references to non-existent apply-skill.ts, compile directly from skill directory using ${CLAUDE_SKILL_DIR} - SKILL.md: add xattr -cr step for Gatekeeper on macOS Sequoia+ - Remove unused manifest.yaml Co-Authored-By: tomermesser Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/add-statusbar/SKILL.md | 51 ++++++++----------- .../add-statusbar/add/src/statusbar.swift | 10 +++- .claude/skills/add-statusbar/manifest.yaml | 10 ---- 3 files changed, 31 insertions(+), 40 deletions(-) delete mode 100644 .claude/skills/add-statusbar/manifest.yaml diff --git a/.claude/skills/add-statusbar/SKILL.md b/.claude/skills/add-statusbar/SKILL.md index c0f343c..07012bf 100644 --- a/.claude/skills/add-statusbar/SKILL.md +++ b/.claude/skills/add-statusbar/SKILL.md @@ -1,11 +1,12 @@ --- name: add-statusbar -description: Add a macOS menu bar status indicator for NanoClaw. Shows a ⚡ icon with a green/red dot indicating whether NanoClaw is running, with Start, Stop, and Restart controls. macOS only. +description: Add a macOS menu bar status indicator for NanoClaw. Shows a bolt icon with a green/red dot indicating whether NanoClaw is running, with Start, Stop, and Restart controls. macOS only. --- # Add macOS Menu Bar Status Indicator -Adds a persistent menu bar icon that shows NanoClaw's running status and lets the user start, stop, or restart the service — similar to how Docker Desktop appears in the menu bar. +Adds a persistent menu bar icon that shows NanoClaw's running status and lets the user +start, stop, or restart the service — similar to how Docker Desktop appears in the menu bar. **macOS only.** Requires Xcode Command Line Tools (`swiftc`). @@ -39,45 +40,38 @@ If not found, tell the user: launchctl list | grep com.nanoclaw.statusbar ``` -If it returns a PID (not `-`), tell the user it's already installed and skip to Phase 4 (Verify). +If it returns a PID (not `-`), tell the user it's already installed and skip to Phase 3 (Verify). -## Phase 2: Apply Code Changes - -### Initialize skills system (if needed) - -If `.nanoclaw/` directory doesn't exist yet: - -```bash -npx tsx scripts/apply-skill.ts --init -``` - -### Apply the skill - -```bash -npx tsx scripts/apply-skill.ts .claude/skills/add-statusbar -``` - -This copies `src/statusbar.swift` into the project and records the application in `.nanoclaw/state.yaml`. - -## Phase 3: Compile and Install +## Phase 2: Compile and Install ### Compile the Swift binary +The source lives in the skill directory. Compile it into `dist/`: + ```bash -swiftc -O -o dist/statusbar src/statusbar.swift +mkdir -p dist +swiftc -O -o dist/statusbar "${CLAUDE_SKILL_DIR}/add/src/statusbar.swift" ``` -This produces a small (~55KB) native binary at `dist/statusbar`. +This produces a small native binary at `dist/statusbar`. + +On macOS Sequoia or later, clear the quarantine attribute so the binary can run: + +```bash +xattr -cr dist/statusbar +``` ### Create the launchd plist -Determine the absolute project root: +Determine the absolute project root and home directory: ```bash pwd +echo $HOME ``` -Create `~/Library/LaunchAgents/com.nanoclaw.statusbar.plist`, substituting the actual values for `{PROJECT_ROOT}` and `{HOME}`: +Create `~/Library/LaunchAgents/com.nanoclaw.statusbar.plist`, substituting the actual values +for `{PROJECT_ROOT}` and `{HOME}`: ```xml @@ -113,7 +107,7 @@ Create `~/Library/LaunchAgents/com.nanoclaw.statusbar.plist`, substituting the a launchctl load ~/Library/LaunchAgents/com.nanoclaw.statusbar.plist ``` -## Phase 4: Verify +## Phase 3: Verify ```bash launchctl list | grep com.nanoclaw.statusbar @@ -123,7 +117,7 @@ The first column should show a PID (not `-`). Tell the user: -> The ⚡ icon should now appear in your macOS menu bar. Click it to see NanoClaw's status and control the service. +> The bolt icon should now appear in your macOS menu bar. Click it to see NanoClaw's status and control the service. > > - **Green dot** — NanoClaw is running > - **Red dot** — NanoClaw is stopped @@ -136,5 +130,4 @@ Tell the user: launchctl unload ~/Library/LaunchAgents/com.nanoclaw.statusbar.plist rm ~/Library/LaunchAgents/com.nanoclaw.statusbar.plist rm dist/statusbar -rm src/statusbar.swift ``` diff --git a/.claude/skills/add-statusbar/add/src/statusbar.swift b/.claude/skills/add-statusbar/add/src/statusbar.swift index 6fff79a..2577380 100644 --- a/.claude/skills/add-statusbar/add/src/statusbar.swift +++ b/.claude/skills/add-statusbar/add/src/statusbar.swift @@ -7,6 +7,14 @@ class StatusBarController: NSObject { private let plistPath = "\(NSHomeDirectory())/Library/LaunchAgents/com.nanoclaw.plist" + /// Derive the NanoClaw project root from the binary location. + /// The binary is compiled to {project}/dist/statusbar, so the parent of + /// the parent directory is the project root. + private static let projectRoot: String = { + let binary = URL(fileURLWithPath: CommandLine.arguments[0]).resolvingSymlinksInPath() + return binary.deletingLastPathComponent().deletingLastPathComponent().path + }() + override init() { super.init() setupStatusItem() @@ -108,7 +116,7 @@ class StatusBarController: NSObject { } @objc private func viewLogs() { - let logPath = "\(NSHomeDirectory())/Documents/Projects/nanoclaw/logs/nanoclaw.log" + let logPath = "\(StatusBarController.projectRoot)/logs/nanoclaw.log" NSWorkspace.shared.open(URL(fileURLWithPath: logPath)) } diff --git a/.claude/skills/add-statusbar/manifest.yaml b/.claude/skills/add-statusbar/manifest.yaml deleted file mode 100644 index 0d7d720..0000000 --- a/.claude/skills/add-statusbar/manifest.yaml +++ /dev/null @@ -1,10 +0,0 @@ -skill: statusbar -version: 1.0.0 -description: "macOS menu bar status indicator — shows NanoClaw running state with start/stop/restart controls" -core_version: 0.1.0 -adds: - - src/statusbar.swift -modifies: [] -structured: {} -conflicts: [] -depends: [] From e4f15b659e0ba0a0769431fcaa46be86da6aaa0e Mon Sep 17 00:00:00 2001 From: gavrielc Date: Wed, 25 Mar 2026 23:55:21 +0200 Subject: [PATCH 14/24] rename skill to add-macos-statusbar Co-Authored-By: tomermesser Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/{add-statusbar => add-macos-statusbar}/SKILL.md | 4 ++-- .../add/src/statusbar.swift | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename .claude/skills/{add-statusbar => add-macos-statusbar}/SKILL.md (98%) rename .claude/skills/{add-statusbar => add-macos-statusbar}/add/src/statusbar.swift (100%) diff --git a/.claude/skills/add-statusbar/SKILL.md b/.claude/skills/add-macos-statusbar/SKILL.md similarity index 98% rename from .claude/skills/add-statusbar/SKILL.md rename to .claude/skills/add-macos-statusbar/SKILL.md index 07012bf..62855f2 100644 --- a/.claude/skills/add-statusbar/SKILL.md +++ b/.claude/skills/add-macos-statusbar/SKILL.md @@ -1,5 +1,5 @@ --- -name: add-statusbar +name: add-macos-statusbar description: Add a macOS menu bar status indicator for NanoClaw. Shows a bolt icon with a green/red dot indicating whether NanoClaw is running, with Start, Stop, and Restart controls. macOS only. --- @@ -32,7 +32,7 @@ If not found, tell the user: > xcode-select --install > ``` > -> Then re-run `/add-statusbar`. +> Then re-run `/add-macos-statusbar`. ### Check if already installed diff --git a/.claude/skills/add-statusbar/add/src/statusbar.swift b/.claude/skills/add-macos-statusbar/add/src/statusbar.swift similarity index 100% rename from .claude/skills/add-statusbar/add/src/statusbar.swift rename to .claude/skills/add-macos-statusbar/add/src/statusbar.swift From 4c6d9241d4b5eb8fe8b953d5c14e9a87f874e20c Mon Sep 17 00:00:00 2001 From: gavrielc Date: Thu, 26 Mar 2026 13:25:18 +0200 Subject: [PATCH 15/24] docs: update README and security docs to reflect OneCLI Agent Vault adoption Replace references to the old built-in credential proxy with OneCLI's Agent Vault across README (feature list, FAQ) and docs/SECURITY.md (credential isolation section, architecture diagram). Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 3 ++- docs/SECURITY.md | 24 +++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8d1eb37..874a8d7 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Then run `/setup`. Claude Code handles everything: dependencies, authentication, - **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 (macOS/Linux), [Docker Sandboxes](docs/docker-sandboxes.md) (micro VM isolation), or Apple Container (macOS) +- **Credential security** - Agents never hold raw API keys. Outbound requests route through [OneCLI's Agent Vault](https://github.com/onecli/onecli), which injects credentials at request time and enforces per-agent policies and rate limits. - **Agent Swarms** - Spin up teams of specialized agents that collaborate on complex tasks - **Optional integrations** - Add Gmail (`/add-gmail`) and more via skills @@ -160,7 +161,7 @@ Yes. Docker is the default runtime and works on macOS, Linux, and Windows (via W **Is this secure?** -Agents run in containers, not behind application-level permission checks. They can only access explicitly mounted directories. You should still review what you're running, but the codebase is small enough that you actually can. See the [security documentation](https://docs.nanoclaw.dev/concepts/security) for the full security model. +Agents run in containers, not behind application-level permission checks. They can only access explicitly mounted directories. Credentials never enter the container — outbound API requests route through [OneCLI's Agent Vault](https://github.com/onecli/onecli), which injects authentication at the proxy level and supports rate limits and access policies. You should still review what you're running, but the codebase is small enough that you actually can. See the [security documentation](https://docs.nanoclaw.dev/concepts/security) for the full security model. **Why no configuration files?** diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 3562fbd..7cf29f8 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -64,20 +64,22 @@ Messages and task operations are verified against group identity: | View all tasks | ✓ | Own only | | Manage other groups | ✓ | ✗ | -### 5. Credential Isolation (Credential Proxy) +### 5. Credential Isolation (OneCLI Agent Vault) -Real API credentials **never enter containers**. Instead, the host runs an HTTP credential proxy that injects authentication headers transparently. +Real API credentials **never enter containers**. NanoClaw uses [OneCLI's Agent Vault](https://github.com/onecli/onecli) to proxy outbound requests and inject credentials at the gateway level. **How it works:** -1. Host starts a credential proxy on `CREDENTIAL_PROXY_PORT` (default: 3001) -2. Containers receive `ANTHROPIC_BASE_URL=http://host.docker.internal:` and `ANTHROPIC_API_KEY=placeholder` -3. The SDK sends API requests to the proxy with the placeholder key -4. The proxy strips placeholder auth, injects real credentials (`x-api-key` or `Authorization: Bearer`), and forwards to `api.anthropic.com` -5. Agents cannot discover real credentials — not in environment, stdin, files, or `/proc` +1. Credentials are registered once with `onecli secrets create`, stored and managed by OneCLI +2. When NanoClaw spawns a container, it calls `applyContainerConfig()` to route outbound HTTPS through the OneCLI gateway +3. The gateway matches requests by host and path, injects the real credential, and forwards +4. Agents cannot discover real credentials — not in environment, stdin, files, or `/proc` + +**Per-agent policies:** +Each NanoClaw group gets its own OneCLI agent identity. This allows different credential policies per group (e.g. your sales agent vs. support agent). OneCLI supports rate limits, and time-bound access and approval flows are on the roadmap. **NOT Mounted:** -- Channel auth sessions (`store/auth/`) - host only -- Mount allowlist - external, never mounted +- Channel auth sessions (`store/auth/`) — host only +- Mount allowlist — external, never mounted - Any credentials matching blocked patterns - `.env` is shadowed with `/dev/null` in the project root mount @@ -107,7 +109,7 @@ Real API credentials **never enter containers**. Instead, the host runs an HTTP │ • IPC authorization │ │ • Mount validation (external allowlist) │ │ • Container lifecycle │ -│ • Credential proxy (injects auth headers) │ +│ • OneCLI Agent Vault (injects credentials, enforces policies) │ └────────────────────────────────┬─────────────────────────────────┘ │ ▼ Explicit mounts only, no secrets @@ -116,7 +118,7 @@ Real API credentials **never enter containers**. Instead, the host runs an HTTP │ • Agent execution │ │ • Bash commands (sandboxed) │ │ • File operations (limited to mounts) │ -│ • API calls routed through credential proxy │ +│ • API calls routed through OneCLI Agent Vault │ │ • No real credentials in environment or filesystem │ └──────────────────────────────────────────────────────────────────┘ ``` From 8b53a95a5f1daf2fd465eed43b738a03aaee7c68 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Thu, 26 Mar 2026 13:31:31 +0200 Subject: [PATCH 16/24] feat: add /init-onecli skill for OneCLI Agent Vault setup and credential migration Operational skill that installs OneCLI, configures the Agent Vault gateway, and migrates existing .env credentials into the vault. Designed to run after /update-nanoclaw introduces OneCLI as a breaking change. Added [BREAKING] changelog entry so update-nanoclaw automatically offers to run /init-onecli. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/init-onecli/SKILL.md | 241 ++++++++++++++++++++++++++++ CHANGELOG.md | 4 + CLAUDE.md | 1 + 3 files changed, 246 insertions(+) create mode 100644 .claude/skills/init-onecli/SKILL.md diff --git a/.claude/skills/init-onecli/SKILL.md b/.claude/skills/init-onecli/SKILL.md new file mode 100644 index 0000000..54856aa --- /dev/null +++ b/.claude/skills/init-onecli/SKILL.md @@ -0,0 +1,241 @@ +--- +name: init-onecli +description: Install and initialize OneCLI Agent Vault. Migrates existing .env credentials to the vault. Use after /update-nanoclaw brings in OneCLI as a breaking change, or for first-time OneCLI setup. +--- + +# Initialize OneCLI Agent Vault + +This skill installs OneCLI, configures the Agent Vault gateway, and migrates any existing `.env` credentials into it. Run this after `/update-nanoclaw` introduces OneCLI as a breaking change, or any time OneCLI needs to be set up from scratch. + +**Principle:** When something is broken or missing, fix it. Don't tell the user to go fix it themselves unless it genuinely requires their manual action (e.g. pasting a token). + +## Phase 1: Pre-flight + +### Check if OneCLI is already working + +```bash +onecli version 2>/dev/null +``` + +If the command succeeds, OneCLI is installed. Check if the gateway is reachable: + +```bash +curl -sf http://127.0.0.1:10254/health +``` + +If both succeed, check for an Anthropic secret: + +```bash +onecli secrets list +``` + +If an Anthropic secret exists, tell the user OneCLI is already configured and working. Use AskUserQuestion: + +1. **Keep current setup** — description: "OneCLI is installed and has credentials configured. Nothing to do." +2. **Reconfigure** — description: "Start fresh — reinstall OneCLI and re-register credentials." + +If they choose to keep, skip to Phase 5 (Verify). If they choose to reconfigure, continue. + +### Check for native credential proxy + +```bash +grep "credential-proxy" src/index.ts 2>/dev/null +``` + +If `startCredentialProxy` is imported, the native credential proxy skill is active. Tell the user: "You're currently using the native credential proxy (`.env`-based). This skill will switch you to OneCLI's Agent Vault, which adds per-agent policies and rate limits. Your `.env` credentials will be migrated to the vault." + +Use AskUserQuestion: +1. **Continue** — description: "Switch to OneCLI Agent Vault." +2. **Cancel** — description: "Keep the native credential proxy." + +If they cancel, stop. + +### Check the codebase expects OneCLI + +```bash +grep "@onecli-sh/sdk" package.json +``` + +If `@onecli-sh/sdk` is NOT in package.json, the codebase hasn't been updated to use OneCLI yet. Tell the user to run `/update-nanoclaw` first to get the OneCLI integration, then retry `/init-onecli`. Stop here. + +## Phase 2: Install OneCLI + +### Install the gateway and CLI + +```bash +curl -fsSL onecli.sh/install | sh +curl -fsSL onecli.sh/cli/install | sh +``` + +Verify: `onecli version` + +If the command is not found, the CLI was likely installed to `~/.local/bin/`. Add it to PATH: + +```bash +export PATH="$HOME/.local/bin:$PATH" +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 +``` + +Re-verify with `onecli version`. + +### Configure the CLI + +Point the CLI at the local OneCLI instance: + +```bash +onecli config set api-host http://127.0.0.1:10254 +``` + +### Set ONECLI_URL in .env + +```bash +grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=http://127.0.0.1:10254' >> .env +``` + +### Wait for gateway readiness + +The gateway may take a moment to start after installation. Poll for up to 15 seconds: + +```bash +for i in $(seq 1 15); do + curl -sf http://127.0.0.1:10254/health && break + sleep 1 +done +``` + +If it never becomes healthy, check if the gateway process is running: + +```bash +ps aux | grep -i onecli | grep -v grep +``` + +If it's not running, try starting it manually: `onecli start`. If that fails, show the error and stop — the user needs to debug their OneCLI installation. + +## Phase 3: Migrate existing credentials + +### Scan .env for credentials to migrate + +Read the `.env` file and look for these credential variables: + +| .env variable | OneCLI secret type | Host pattern | +|---|---|---| +| `ANTHROPIC_API_KEY` | `anthropic` | `api.anthropic.com` | +| `CLAUDE_CODE_OAUTH_TOKEN` | `anthropic` | `api.anthropic.com` | +| `ANTHROPIC_AUTH_TOKEN` | `anthropic` | `api.anthropic.com` | + +Read `.env`: + +```bash +cat .env +``` + +Parse the file for any of the credential variables listed above. + +### If credentials found in .env + +For each credential found, migrate it to OneCLI: + +**Anthropic API key** (`ANTHROPIC_API_KEY=sk-ant-...`): +```bash +onecli secrets create --name Anthropic --type anthropic --value --host-pattern api.anthropic.com +``` + +**Claude OAuth token** (`CLAUDE_CODE_OAUTH_TOKEN=...` or `ANTHROPIC_AUTH_TOKEN=...`): +```bash +onecli secrets create --name Anthropic --type anthropic --value --host-pattern api.anthropic.com +``` + +After successful migration, remove the credential lines from `.env`. Use the Edit tool to remove only the credential variable lines (`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN`). Keep all other `.env` entries intact (e.g. `ONECLI_URL`, `TELEGRAM_BOT_TOKEN`, channel tokens). + +Verify the secret was registered: +```bash +onecli secrets list +``` + +Tell the user: "Migrated your credentials from `.env` to the OneCLI Agent Vault. The raw keys have been removed from `.env` — they're now managed by OneCLI and will be injected at request time without entering containers." + +### If no credentials found in .env + +No migration needed. Proceed to register credentials fresh. + +Check if OneCLI already has an Anthropic secret: +```bash +onecli secrets list +``` + +If an Anthropic secret already exists, skip to Phase 4. + +Otherwise, register credentials using the same flow as `/setup`: + +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, 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. + +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`" + +#### After either path + +Ask them to let you know when done. + +**If the user's response happens to contain a token or key** (starts with `sk-ant-` or looks like a token): 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. + +## Phase 4: Build and restart + +```bash +npm run build +``` + +If build fails, diagnose and fix. Common issue: `@onecli-sh/sdk` not installed — run `npm install` first. + +Restart the service: +- macOS (launchd): `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` +- Linux (systemd): `systemctl --user restart nanoclaw` +- WSL/manual: stop and re-run `bash start-nanoclaw.sh` + +## Phase 5: Verify + +Check logs for successful OneCLI integration: + +```bash +tail -30 logs/nanoclaw.log | grep -i "onecli\|gateway" +``` + +Expected: `OneCLI gateway config applied` messages when containers start. + +If the service is running and a channel is configured, tell the user to send a test message to verify the agent responds. + +Tell the user: +- OneCLI Agent Vault is now managing credentials +- Agents never see raw API keys — credentials are injected at the gateway level +- To manage secrets: `onecli secrets list`, or open http://127.0.0.1:10254 +- To add rate limits or policies: `onecli rules create --help` + +## Troubleshooting + +**"OneCLI gateway not reachable" in logs:** The gateway isn't running. Check with `curl -sf http://127.0.0.1:10254/health`. Start it with `onecli start` if needed. + +**Container gets no credentials:** Verify `ONECLI_URL` is set in `.env` and the gateway has an Anthropic secret (`onecli secrets list`). + +**Old .env credentials still present:** This skill should have removed them. Double-check `.env` for `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, or `ANTHROPIC_AUTH_TOKEN` and remove them manually if still present. + +**Port 10254 already in use:** Another OneCLI instance may be running. Check with `lsof -i :10254` and kill the old process, or configure a different port. diff --git a/CHANGELOG.md b/CHANGELOG.md index 323c0e1..28178e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to NanoClaw will be documented in this file. For detailed release notes, see the [full changelog on the documentation site](https://docs.nanoclaw.dev/changelog). +## [1.2.35] - 2026-03-26 + +- [BREAKING] OneCLI Agent Vault replaces the built-in credential proxy. Existing `.env` credentials must be migrated to the vault. Run `/init-onecli` to install OneCLI and migrate credentials. + ## [1.2.21] - 2026-03-22 - Added opt-in diagnostics via PostHog with explicit user consent (Yes / No / Never ask again) diff --git a/CLAUDE.md b/CLAUDE.md index 2084578..c9c49ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,7 @@ Four types of skills exist in NanoClaw. See [CONTRIBUTING.md](CONTRIBUTING.md) f | `/customize` | Adding channels, integrations, changing behavior | | `/debug` | Container issues, logs, troubleshooting | | `/update-nanoclaw` | Bring upstream NanoClaw updates into a customized install | +| `/init-onecli` | Install OneCLI Agent Vault and migrate `.env` credentials to it | | `/qodo-pr-resolver` | Fetch and fix Qodo PR review issues interactively or in batch | | `/get-qodo-rules` | Load org- and repo-level coding rules from Qodo before code tasks | From d398ba5ac66a836664214e04761d7ea2aeffd86e Mon Sep 17 00:00:00 2001 From: gavrielc Date: Thu, 26 Mar 2026 13:51:24 +0200 Subject: [PATCH 17/24] feat(init-onecli): offer to migrate non-Anthropic .env credentials to vault After migrating Anthropic credentials, the skill now scans .env for other service tokens (Telegram, Slack, Discord, OpenAI, etc.) and offers to move them into OneCLI Agent Vault as well. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/init-onecli/SKILL.md | 39 ++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/.claude/skills/init-onecli/SKILL.md b/.claude/skills/init-onecli/SKILL.md index 54856aa..9111510 100644 --- a/.claude/skills/init-onecli/SKILL.md +++ b/.claude/skills/init-onecli/SKILL.md @@ -153,7 +153,44 @@ Verify the secret was registered: onecli secrets list ``` -Tell the user: "Migrated your credentials from `.env` to the OneCLI Agent Vault. The raw keys have been removed from `.env` — they're now managed by OneCLI and will be injected at request time without entering containers." +Tell the user: "Migrated your Anthropic credentials from `.env` to the OneCLI Agent Vault. The raw keys have been removed from `.env` — they're now managed by OneCLI and will be injected at request time without entering containers." + +### Offer to migrate other service credentials + +After handling Anthropic credentials (whether migrated or freshly registered), scan `.env` again for any remaining credential variables. Look for variables whose names contain `_TOKEN`, `_KEY`, `_SECRET`, or `_PASSWORD`, excluding non-credential entries like `ONECLI_URL` and other config values. + +Common examples from NanoClaw skills: + +| .env variable | Secret name | Host pattern | +|---|---|---| +| `TELEGRAM_BOT_TOKEN` | `Telegram` | `api.telegram.org` | +| `SLACK_BOT_TOKEN` | `Slack Bot` | `slack.com` | +| `SLACK_APP_TOKEN` | `Slack App` | `slack.com` | +| `DISCORD_BOT_TOKEN` | `Discord` | `discord.com` | +| `OPENAI_API_KEY` | `OpenAI` | `api.openai.com` | +| `PARALLEL_API_KEY` | `Parallel` | `api.parallel.ai` | + +If any such variables are found with non-empty values, present them to the user: + +AskUserQuestion (multiSelect): "These other credentials are still in `.env`. Would you like to move any of them to the OneCLI Agent Vault as well? Credentials in the vault are never exposed to containers and can have rate limits and policies applied." + +- One option per credential found (e.g., "TELEGRAM_BOT_TOKEN" — description: "Telegram bot token, will be proxied through the vault") +- **Skip — keep them in .env** — description: "Leave these credentials in .env for now. You can move them later." + +For each credential the user selects: + +```bash +onecli secrets create --name --type api_key --value --host-pattern +``` + +If a variable isn't in the table above, use a reasonable secret name derived from the variable name (e.g., `MY_SERVICE_KEY` becomes `My Service`) and ask the user what host pattern to use: "What API host does this credential authenticate against? (e.g., `api.example.com`)" + +After migration, remove the migrated lines from `.env` using the Edit tool. Keep any credentials the user chose not to migrate. + +Verify all secrets were registered: +```bash +onecli secrets list +``` ### If no credentials found in .env From a41746530fdf6f7cdaf13b08e92ba0e6873a6b98 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Thu, 26 Mar 2026 13:52:25 +0200 Subject: [PATCH 18/24] fix(init-onecli): only offer to migrate container-facing credentials Channel tokens (Telegram, Slack, Discord) are used by the host process, not by containers via the gateway. Only offer to migrate credentials that containers use for outbound API calls (OpenAI, Parallel, etc.). Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/init-onecli/SKILL.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/.claude/skills/init-onecli/SKILL.md b/.claude/skills/init-onecli/SKILL.md index 9111510..d7727dd 100644 --- a/.claude/skills/init-onecli/SKILL.md +++ b/.claude/skills/init-onecli/SKILL.md @@ -155,27 +155,25 @@ onecli secrets list Tell the user: "Migrated your Anthropic credentials from `.env` to the OneCLI Agent Vault. The raw keys have been removed from `.env` — they're now managed by OneCLI and will be injected at request time without entering containers." -### Offer to migrate other service credentials +### Offer to migrate other container-facing credentials -After handling Anthropic credentials (whether migrated or freshly registered), scan `.env` again for any remaining credential variables. Look for variables whose names contain `_TOKEN`, `_KEY`, `_SECRET`, or `_PASSWORD`, excluding non-credential entries like `ONECLI_URL` and other config values. +After handling Anthropic credentials (whether migrated or freshly registered), scan `.env` again for remaining credential variables that containers use for outbound API calls. -Common examples from NanoClaw skills: +**Important:** Only migrate credentials that containers use via outbound HTTPS. Channel tokens (`TELEGRAM_BOT_TOKEN`, `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, `DISCORD_BOT_TOKEN`) are used by the NanoClaw host process to connect to messaging platforms — they must stay in `.env`. + +Known container-facing credentials: | .env variable | Secret name | Host pattern | |---|---|---| -| `TELEGRAM_BOT_TOKEN` | `Telegram` | `api.telegram.org` | -| `SLACK_BOT_TOKEN` | `Slack Bot` | `slack.com` | -| `SLACK_APP_TOKEN` | `Slack App` | `slack.com` | -| `DISCORD_BOT_TOKEN` | `Discord` | `discord.com` | | `OPENAI_API_KEY` | `OpenAI` | `api.openai.com` | | `PARALLEL_API_KEY` | `Parallel` | `api.parallel.ai` | -If any such variables are found with non-empty values, present them to the user: +If any of these are found with non-empty values, present them to the user: -AskUserQuestion (multiSelect): "These other credentials are still in `.env`. Would you like to move any of them to the OneCLI Agent Vault as well? Credentials in the vault are never exposed to containers and can have rate limits and policies applied." +AskUserQuestion (multiSelect): "These credentials are used by container agents for outbound API calls. Moving them to the vault means agents never see the raw keys, and you can apply rate limits and policies." -- One option per credential found (e.g., "TELEGRAM_BOT_TOKEN" — description: "Telegram bot token, will be proxied through the vault") -- **Skip — keep them in .env** — description: "Leave these credentials in .env for now. You can move them later." +- One option per credential found (e.g., "OPENAI_API_KEY" — description: "Used by voice transcription and other OpenAI integrations inside containers") +- **Skip — keep them in .env** — description: "Leave these in .env for now. You can move them later." For each credential the user selects: @@ -183,9 +181,9 @@ For each credential the user selects: onecli secrets create --name --type api_key --value --host-pattern ``` -If a variable isn't in the table above, use a reasonable secret name derived from the variable name (e.g., `MY_SERVICE_KEY` becomes `My Service`) and ask the user what host pattern to use: "What API host does this credential authenticate against? (e.g., `api.example.com`)" +If there are credential variables not in the table above that look container-facing (i.e. not a channel token), ask the user: "Is `` used by agents inside containers? If so, what API host does it authenticate against? (e.g., `api.example.com`)" — then migrate accordingly. -After migration, remove the migrated lines from `.env` using the Edit tool. Keep any credentials the user chose not to migrate. +After migration, remove the migrated lines from `.env` using the Edit tool. Keep channel tokens and any credentials the user chose not to migrate. Verify all secrets were registered: ```bash From d25b79a5a97a750a09d610e91e355ca2c49abbb7 Mon Sep 17 00:00:00 2001 From: NanoClaw Date: Thu, 26 Mar 2026 13:17:07 +0000 Subject: [PATCH 19/24] docs: add auth credentials guidance to main group CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clarify that only long-lived OAuth tokens (claude setup-token) or API keys should be used — short-lived tokens from the keychain expire within hours and cause recurring 401s. Also update native credential proxy skill to swap the OneCLI reference when applied. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/use-native-credential-proxy/SKILL.md | 10 ++++++++++ groups/main/CLAUDE.md | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/.claude/skills/use-native-credential-proxy/SKILL.md b/.claude/skills/use-native-credential-proxy/SKILL.md index 4cdda4c..71448b1 100644 --- a/.claude/skills/use-native-credential-proxy/SKILL.md +++ b/.claude/skills/use-native-credential-proxy/SKILL.md @@ -64,6 +64,16 @@ This merges in: If the merge reports conflicts beyond `package-lock.json`, resolve them by reading the conflicted files and understanding the intent of both sides. +### Update main group CLAUDE.md + +Replace the OneCLI auth reference with the native proxy: + +In `groups/main/CLAUDE.md`, replace: +> OneCLI manages credentials (including Anthropic auth) — run `onecli --help`. + +with: +> The native credential proxy manages credentials (including Anthropic auth) via `.env` — see `src/credential-proxy.ts`. + ### Validate code changes ```bash diff --git a/groups/main/CLAUDE.md b/groups/main/CLAUDE.md index 6080427..17b39cb 100644 --- a/groups/main/CLAUDE.md +++ b/groups/main/CLAUDE.md @@ -77,6 +77,10 @@ Standard Markdown: `**bold**`, `*italic*`, `[links](url)`, `# headings`. This is the **main channel**, which has elevated privileges. +## Authentication + +Anthropic credentials must be either an API key from console.anthropic.com (`ANTHROPIC_API_KEY`) or a long-lived OAuth token from `claude setup-token` (`CLAUDE_CODE_OAUTH_TOKEN`). Short-lived tokens from the system keychain or `~/.claude/.credentials.json` expire within hours and can cause recurring container 401s. The `/setup` skill walks through this. OneCLI manages credentials (including Anthropic auth) — run `onecli --help`. + ## Container Mounts Main has read-only access to the project and read-write access to its group folder: From 813e1c6fa4d9c170b1e8f748347cb3bb6e3c97e4 Mon Sep 17 00:00:00 2001 From: NanoClaw User Date: Wed, 25 Mar 2026 22:05:29 +0000 Subject: [PATCH 20/24] fix: improve task scripts agent instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reword Task Scripts opening in main template to guide agents toward schedule_task instead of inline bash loops. Add missing Task Scripts section to global template — non-main groups have unrestricted access to schedule_task with script parameter, so omitting instructions just leads to worse patterns. Co-Authored-By: Claude Opus 4.6 (1M context) --- groups/global/CLAUDE.md | 39 +++++++++++++++++++++++++++++++++++++++ groups/main/CLAUDE.md | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/groups/global/CLAUDE.md b/groups/global/CLAUDE.md index c814e39..7018c04 100644 --- a/groups/global/CLAUDE.md +++ b/groups/global/CLAUDE.md @@ -74,3 +74,42 @@ No `##` headings. No `[links](url)`. No `**double stars**`. ### Discord channels (folder starts with `discord_`) Standard Markdown works: `**bold**`, `*italic*`, `[links](url)`, `# headings`. + +--- + +## Task Scripts + +To check or monitor something on a recurring basis, use `schedule_task` — not a bash loop. This way the check survives container restarts and doesn't block other messages. If the user only needs to know when a condition changes, add a `script` to avoid unnecessary wake-ups — the script runs first, and you only wake up when there's something to act on. + +### How it works + +1. You provide a bash `script` alongside the `prompt` when scheduling +2. When the task fires, the script runs first (30-second timeout) +3. Script prints JSON to stdout: `{ "wakeAgent": true/false, "data": {...} }` +4. If `wakeAgent: false` — nothing happens, task waits for next run +5. If `wakeAgent: true` — you wake up and receive the script's data + prompt + +### Always test your script first + +Before scheduling, run the script in your sandbox to verify it works: + +```bash +bash -c 'node --input-type=module -e " + const r = await fetch(\"https://api.github.com/repos/owner/repo/pulls?state=open\"); + const prs = await r.json(); + console.log(JSON.stringify({ wakeAgent: prs.length > 0, data: prs.slice(0, 5) })); +"' +``` + +### When NOT to use scripts + +If a task requires your judgment every time (daily briefings, reminders, reports), skip the script — just use a regular prompt. + +### Frequent task guidance + +If a user wants tasks running more than ~2x daily and a script can't reduce agent wake-ups: + +- Explain that each wake-up uses API credits and risks rate limits +- Suggest restructuring with a script that checks the condition first +- If the user needs an LLM to evaluate data, suggest using an API key with direct Anthropic API calls inside the script +- Help the user find the minimum viable frequency diff --git a/groups/main/CLAUDE.md b/groups/main/CLAUDE.md index 6080427..5e693fa 100644 --- a/groups/main/CLAUDE.md +++ b/groups/main/CLAUDE.md @@ -267,7 +267,7 @@ The task will run in that group's context with access to their files and memory. ## Task Scripts -When scheduling tasks that check a condition before acting (new PRs, website changes, API status), use the `script` parameter. The script runs first — if there's nothing to do, you don't wake up. +To check or monitor something on a recurring basis, use `schedule_task` — not a bash loop. This way the check survives container restarts and doesn't block other messages. If the user only needs to know when a condition changes, add a `script` to avoid unnecessary wake-ups — the script runs first, and you only wake up when there's something to act on. ### How it works From a29ca0835c37ede7ef490e21dda6a6a840bbe4a7 Mon Sep 17 00:00:00 2001 From: NanoClaw User Date: Thu, 26 Mar 2026 11:21:18 +0000 Subject: [PATCH 21/24] fix: rewrite task scripts intro for broader use cases and clarity Broadens the trigger from "check or monitor" to "any recurring task", adds context about API credit usage and account risk for frequent tasks, and prompts the agent to clarify ambiguous requests. Co-Authored-By: Claude Opus 4.6 (1M context) --- groups/global/CLAUDE.md | 2 +- groups/main/CLAUDE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/groups/global/CLAUDE.md b/groups/global/CLAUDE.md index 7018c04..935578a 100644 --- a/groups/global/CLAUDE.md +++ b/groups/global/CLAUDE.md @@ -79,7 +79,7 @@ Standard Markdown works: `**bold**`, `*italic*`, `[links](url)`, `# headings`. ## Task Scripts -To check or monitor something on a recurring basis, use `schedule_task` — not a bash loop. This way the check survives container restarts and doesn't block other messages. If the user only needs to know when a condition changes, add a `script` to avoid unnecessary wake-ups — the script runs first, and you only wake up when there's something to act on. +For any recurring task, use `schedule_task`. Tasks that wake the agent frequently — especially multiple times a day — consume API credits and can risk account restrictions. If a simple check can determine whether you need to act, add a `script` — it runs first, and you only wake up when the check passes. This keeps agent invocations to a minimum. If it's unclear whether the user wants a response every time or only when something requires attention, ask. ### How it works diff --git a/groups/main/CLAUDE.md b/groups/main/CLAUDE.md index 5e693fa..d3ea5f9 100644 --- a/groups/main/CLAUDE.md +++ b/groups/main/CLAUDE.md @@ -267,7 +267,7 @@ The task will run in that group's context with access to their files and memory. ## Task Scripts -To check or monitor something on a recurring basis, use `schedule_task` — not a bash loop. This way the check survives container restarts and doesn't block other messages. If the user only needs to know when a condition changes, add a `script` to avoid unnecessary wake-ups — the script runs first, and you only wake up when there's something to act on. +For any recurring task, use `schedule_task`. Tasks that wake the agent frequently — especially multiple times a day — consume API credits and can risk account restrictions. If a simple check can determine whether you need to act, add a `script` — it runs first, and you only wake up when the check passes. This keeps agent invocations to a minimum. If it's unclear whether the user wants a response every time or only when something requires attention, ask. ### How it works From eda14f472beaa3e7a94e773bdcafeeacc1612ec6 Mon Sep 17 00:00:00 2001 From: NanoClaw User Date: Thu, 26 Mar 2026 12:37:28 +0000 Subject: [PATCH 22/24] fix: include script field in task snapshot for current_tasks.json The task snapshot mappings in index.ts were omitting the script field, making it appear that scheduled tasks had no script even when one was stored in the database. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index 60fe910..bf57823 100644 --- a/src/index.ts +++ b/src/index.ts @@ -329,6 +329,7 @@ async function runAgent( id: t.id, groupFolder: t.group_folder, prompt: t.prompt, + script: t.script || undefined, schedule_type: t.schedule_type, schedule_value: t.schedule_value, status: t.status, @@ -685,6 +686,7 @@ async function main(): Promise { id: t.id, groupFolder: t.group_folder, prompt: t.prompt, + script: t.script || undefined, schedule_type: t.schedule_type, schedule_value: t.schedule_value, status: t.status, From 730ea0d713634edf1abcf16defe887b05a5accc0 Mon Sep 17 00:00:00 2001 From: NanoClaw User Date: Thu, 26 Mar 2026 15:05:53 +0000 Subject: [PATCH 23/24] fix: refine task scripts intro wording Use third-person voice and clearer terminology for the task scripts intro paragraph. Co-Authored-By: Claude Opus 4.6 (1M context) --- groups/global/CLAUDE.md | 2 +- groups/main/CLAUDE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/groups/global/CLAUDE.md b/groups/global/CLAUDE.md index 935578a..11988bc 100644 --- a/groups/global/CLAUDE.md +++ b/groups/global/CLAUDE.md @@ -79,7 +79,7 @@ Standard Markdown works: `**bold**`, `*italic*`, `[links](url)`, `# headings`. ## Task Scripts -For any recurring task, use `schedule_task`. Tasks that wake the agent frequently — especially multiple times a day — consume API credits and can risk account restrictions. If a simple check can determine whether you need to act, add a `script` — it runs first, and you only wake up when the check passes. This keeps agent invocations to a minimum. If it's unclear whether the user wants a response every time or only when something requires attention, ask. +For any recurring task, use `schedule_task`. Frequent agent invocations — especially multiple times a day — consume API credits and can risk account restrictions. If a simple check can determine whether action is needed, add a `script` — it runs first, and the agent is only called when the check passes. This keeps invocations to a minimum. ### How it works diff --git a/groups/main/CLAUDE.md b/groups/main/CLAUDE.md index d3ea5f9..c901813 100644 --- a/groups/main/CLAUDE.md +++ b/groups/main/CLAUDE.md @@ -267,7 +267,7 @@ The task will run in that group's context with access to their files and memory. ## Task Scripts -For any recurring task, use `schedule_task`. Tasks that wake the agent frequently — especially multiple times a day — consume API credits and can risk account restrictions. If a simple check can determine whether you need to act, add a `script` — it runs first, and you only wake up when the check passes. This keeps agent invocations to a minimum. If it's unclear whether the user wants a response every time or only when something requires attention, ask. +For any recurring task, use `schedule_task`. Frequent agent invocations — especially multiple times a day — consume API credits and can risk account restrictions. If a simple check can determine whether action is needed, add a `script` — it runs first, and the agent is only called when the check passes. This keeps invocations to a minimum. ### How it works From 4383e3e61aeaad30ad3cac69ac9e377ac214f89a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Mar 2026 15:39:34 +0000 Subject: [PATCH 24/24] chore: bump version to 1.2.35 --- 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 2c69d40..46a8742 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.34", + "version": "1.2.35", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.34", + "version": "1.2.35", "dependencies": { "@onecli-sh/sdk": "^0.2.0", "better-sqlite3": "11.10.0", diff --git a/package.json b/package.json index 0d76ee5..fca2280 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.34", + "version": "1.2.35", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js",