@rozek/nanoclaw 1.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/.claude/settings.json +1 -0
  2. package/.claude/skills/add-compact/SKILL.md +135 -0
  3. package/.claude/skills/add-discord/SKILL.md +203 -0
  4. package/.claude/skills/add-gmail/SKILL.md +220 -0
  5. package/.claude/skills/add-image-vision/SKILL.md +94 -0
  6. package/.claude/skills/add-ollama-tool/SKILL.md +153 -0
  7. package/.claude/skills/add-parallel/SKILL.md +290 -0
  8. package/.claude/skills/add-pdf-reader/SKILL.md +104 -0
  9. package/.claude/skills/add-reactions/SKILL.md +117 -0
  10. package/.claude/skills/add-slack/SKILL.md +207 -0
  11. package/.claude/skills/add-telegram/SKILL.md +222 -0
  12. package/.claude/skills/add-telegram-swarm/SKILL.md +384 -0
  13. package/.claude/skills/add-voice-transcription/SKILL.md +148 -0
  14. package/.claude/skills/add-whatsapp/SKILL.md +372 -0
  15. package/.claude/skills/convert-to-apple-container/SKILL.md +175 -0
  16. package/.claude/skills/customize/SKILL.md +110 -0
  17. package/.claude/skills/debug/SKILL.md +349 -0
  18. package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
  19. package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
  20. package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
  21. package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
  22. package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
  23. package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
  24. package/.claude/skills/setup/SKILL.md +218 -0
  25. package/.claude/skills/update-nanoclaw/SKILL.md +235 -0
  26. package/.claude/skills/update-skills/SKILL.md +130 -0
  27. package/.claude/skills/use-local-whisper/SKILL.md +152 -0
  28. package/.claude/skills/x-integration/SKILL.md +417 -0
  29. package/.claude/skills/x-integration/agent.ts +243 -0
  30. package/.claude/skills/x-integration/host.ts +159 -0
  31. package/.claude/skills/x-integration/lib/browser.ts +148 -0
  32. package/.claude/skills/x-integration/lib/config.ts +62 -0
  33. package/.claude/skills/x-integration/scripts/like.ts +56 -0
  34. package/.claude/skills/x-integration/scripts/post.ts +66 -0
  35. package/.claude/skills/x-integration/scripts/quote.ts +80 -0
  36. package/.claude/skills/x-integration/scripts/reply.ts +74 -0
  37. package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
  38. package/.claude/skills/x-integration/scripts/setup.ts +87 -0
  39. package/.env.example +1 -0
  40. package/.github/CODEOWNERS +10 -0
  41. package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  42. package/.github/workflows/bump-version.yml +32 -0
  43. package/.github/workflows/ci.yml +25 -0
  44. package/.github/workflows/merge-forward-skills.yml +160 -0
  45. package/.github/workflows/update-tokens.yml +42 -0
  46. package/.husky/pre-commit +1 -0
  47. package/.mcp.json +3 -0
  48. package/.nvmrc +1 -0
  49. package/.prettierrc +3 -0
  50. package/CHANGELOG.md +8 -0
  51. package/CLAUDE.md +64 -0
  52. package/CONTRIBUTING.md +23 -0
  53. package/CONTRIBUTORS.md +15 -0
  54. package/LICENSE +21 -0
  55. package/NanoClaw_with_Web-Support.md +290 -0
  56. package/README.md +261 -0
  57. package/README_zh.md +200 -0
  58. package/assets/nanoclaw-favicon.png +0 -0
  59. package/assets/nanoclaw-icon.png +0 -0
  60. package/assets/nanoclaw-logo-dark.png +0 -0
  61. package/assets/nanoclaw-logo.png +0 -0
  62. package/assets/nanoclaw-profile.jpeg +0 -0
  63. package/assets/nanoclaw-sales.png +0 -0
  64. package/assets/social-preview.jpg +0 -0
  65. package/config-examples/mount-allowlist.json +25 -0
  66. package/container/Dockerfile +70 -0
  67. package/container/agent-runner/package-lock.json +1524 -0
  68. package/container/agent-runner/package.json +21 -0
  69. package/container/agent-runner/src/index.ts +558 -0
  70. package/container/agent-runner/src/ipc-mcp-stdio.ts +338 -0
  71. package/container/agent-runner/tsconfig.json +15 -0
  72. package/container/build.sh +23 -0
  73. package/container/skills/agent-browser/SKILL.md +159 -0
  74. package/container/skills/capabilities/SKILL.md +100 -0
  75. package/container/skills/status/SKILL.md +104 -0
  76. package/dist/channels/index.d.ts +2 -0
  77. package/dist/channels/index.d.ts.map +1 -0
  78. package/dist/channels/index.js +9 -0
  79. package/dist/channels/index.js.map +1 -0
  80. package/dist/channels/registry.d.ts +13 -0
  81. package/dist/channels/registry.d.ts.map +1 -0
  82. package/dist/channels/registry.js +11 -0
  83. package/dist/channels/registry.js.map +1 -0
  84. package/dist/channels/registry.test.d.ts +2 -0
  85. package/dist/channels/registry.test.d.ts.map +1 -0
  86. package/dist/channels/registry.test.js +32 -0
  87. package/dist/channels/registry.test.js.map +1 -0
  88. package/dist/channels/web.d.ts +2 -0
  89. package/dist/channels/web.d.ts.map +1 -0
  90. package/dist/channels/web.js +1738 -0
  91. package/dist/channels/web.js.map +1 -0
  92. package/dist/cli.d.ts +11 -0
  93. package/dist/cli.d.ts.map +1 -0
  94. package/dist/cli.js +182 -0
  95. package/dist/cli.js.map +1 -0
  96. package/dist/config.d.ts +19 -0
  97. package/dist/config.d.ts.map +1 -0
  98. package/dist/config.js +36 -0
  99. package/dist/config.js.map +1 -0
  100. package/dist/container-runner.d.ts +44 -0
  101. package/dist/container-runner.d.ts.map +1 -0
  102. package/dist/container-runner.js +467 -0
  103. package/dist/container-runner.js.map +1 -0
  104. package/dist/container-runner.test.d.ts +2 -0
  105. package/dist/container-runner.test.d.ts.map +1 -0
  106. package/dist/container-runner.test.js +150 -0
  107. package/dist/container-runner.test.js.map +1 -0
  108. package/dist/container-runtime.d.ts +22 -0
  109. package/dist/container-runtime.d.ts.map +1 -0
  110. package/dist/container-runtime.js +96 -0
  111. package/dist/container-runtime.js.map +1 -0
  112. package/dist/container-runtime.test.d.ts +2 -0
  113. package/dist/container-runtime.test.d.ts.map +1 -0
  114. package/dist/container-runtime.test.js +93 -0
  115. package/dist/container-runtime.test.js.map +1 -0
  116. package/dist/credential-proxy.d.ts +21 -0
  117. package/dist/credential-proxy.d.ts.map +1 -0
  118. package/dist/credential-proxy.js +95 -0
  119. package/dist/credential-proxy.js.map +1 -0
  120. package/dist/credential-proxy.test.d.ts +2 -0
  121. package/dist/credential-proxy.test.d.ts.map +1 -0
  122. package/dist/credential-proxy.test.js +134 -0
  123. package/dist/credential-proxy.test.js.map +1 -0
  124. package/dist/db.d.ts +115 -0
  125. package/dist/db.d.ts.map +1 -0
  126. package/dist/db.js +549 -0
  127. package/dist/db.js.map +1 -0
  128. package/dist/db.test.d.ts +2 -0
  129. package/dist/db.test.d.ts.map +1 -0
  130. package/dist/db.test.js +360 -0
  131. package/dist/db.test.js.map +1 -0
  132. package/dist/env.d.ts +8 -0
  133. package/dist/env.d.ts.map +1 -0
  134. package/dist/env.js +42 -0
  135. package/dist/env.js.map +1 -0
  136. package/dist/formatting.test.d.ts +2 -0
  137. package/dist/formatting.test.d.ts.map +1 -0
  138. package/dist/formatting.test.js +183 -0
  139. package/dist/formatting.test.js.map +1 -0
  140. package/dist/group-folder.d.ts +5 -0
  141. package/dist/group-folder.d.ts.map +1 -0
  142. package/dist/group-folder.js +44 -0
  143. package/dist/group-folder.js.map +1 -0
  144. package/dist/group-folder.test.d.ts +2 -0
  145. package/dist/group-folder.test.d.ts.map +1 -0
  146. package/dist/group-folder.test.js +29 -0
  147. package/dist/group-folder.test.js.map +1 -0
  148. package/dist/group-queue.d.ts +34 -0
  149. package/dist/group-queue.d.ts.map +1 -0
  150. package/dist/group-queue.js +263 -0
  151. package/dist/group-queue.js.map +1 -0
  152. package/dist/group-queue.test.d.ts +2 -0
  153. package/dist/group-queue.test.d.ts.map +1 -0
  154. package/dist/group-queue.test.js +341 -0
  155. package/dist/group-queue.test.js.map +1 -0
  156. package/dist/index.d.ts +12 -0
  157. package/dist/index.d.ts.map +1 -0
  158. package/dist/index.js +518 -0
  159. package/dist/index.js.map +1 -0
  160. package/dist/ipc-auth.test.d.ts +2 -0
  161. package/dist/ipc-auth.test.d.ts.map +1 -0
  162. package/dist/ipc-auth.test.js +434 -0
  163. package/dist/ipc-auth.test.js.map +1 -0
  164. package/dist/ipc.d.ts +32 -0
  165. package/dist/ipc.d.ts.map +1 -0
  166. package/dist/ipc.js +311 -0
  167. package/dist/ipc.js.map +1 -0
  168. package/dist/logger.d.ts +3 -0
  169. package/dist/logger.d.ts.map +1 -0
  170. package/dist/logger.js +14 -0
  171. package/dist/logger.js.map +1 -0
  172. package/dist/mount-security.d.ts +34 -0
  173. package/dist/mount-security.d.ts.map +1 -0
  174. package/dist/mount-security.js +325 -0
  175. package/dist/mount-security.js.map +1 -0
  176. package/dist/remote-control.d.ts +32 -0
  177. package/dist/remote-control.d.ts.map +1 -0
  178. package/dist/remote-control.js +185 -0
  179. package/dist/remote-control.js.map +1 -0
  180. package/dist/remote-control.test.d.ts +2 -0
  181. package/dist/remote-control.test.d.ts.map +1 -0
  182. package/dist/remote-control.test.js +321 -0
  183. package/dist/remote-control.test.js.map +1 -0
  184. package/dist/router.d.ts +8 -0
  185. package/dist/router.d.ts.map +1 -0
  186. package/dist/router.js +37 -0
  187. package/dist/router.js.map +1 -0
  188. package/dist/routing.test.d.ts +2 -0
  189. package/dist/routing.test.d.ts.map +1 -0
  190. package/dist/routing.test.js +81 -0
  191. package/dist/routing.test.js.map +1 -0
  192. package/dist/sender-allowlist.d.ts +14 -0
  193. package/dist/sender-allowlist.d.ts.map +1 -0
  194. package/dist/sender-allowlist.js +79 -0
  195. package/dist/sender-allowlist.js.map +1 -0
  196. package/dist/sender-allowlist.test.d.ts +2 -0
  197. package/dist/sender-allowlist.test.d.ts.map +1 -0
  198. package/dist/sender-allowlist.test.js +186 -0
  199. package/dist/sender-allowlist.test.js.map +1 -0
  200. package/dist/session-commands.d.ts +47 -0
  201. package/dist/session-commands.d.ts.map +1 -0
  202. package/dist/session-commands.js +102 -0
  203. package/dist/session-commands.js.map +1 -0
  204. package/dist/session-commands.test.d.ts +2 -0
  205. package/dist/session-commands.test.d.ts.map +1 -0
  206. package/dist/session-commands.test.js +190 -0
  207. package/dist/session-commands.test.js.map +1 -0
  208. package/dist/task-scheduler.d.ts +22 -0
  209. package/dist/task-scheduler.d.ts.map +1 -0
  210. package/dist/task-scheduler.js +210 -0
  211. package/dist/task-scheduler.js.map +1 -0
  212. package/dist/task-scheduler.test.d.ts +2 -0
  213. package/dist/task-scheduler.test.d.ts.map +1 -0
  214. package/dist/task-scheduler.test.js +107 -0
  215. package/dist/task-scheduler.test.js.map +1 -0
  216. package/dist/timezone.d.ts +6 -0
  217. package/dist/timezone.d.ts.map +1 -0
  218. package/dist/timezone.js +17 -0
  219. package/dist/timezone.js.map +1 -0
  220. package/dist/timezone.test.d.ts +2 -0
  221. package/dist/timezone.test.d.ts.map +1 -0
  222. package/dist/timezone.test.js +23 -0
  223. package/dist/timezone.test.js.map +1 -0
  224. package/dist/types.d.ts +78 -0
  225. package/dist/types.d.ts.map +1 -0
  226. package/dist/types.js +2 -0
  227. package/dist/types.js.map +1 -0
  228. package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
  229. package/docs/DEBUG_CHECKLIST.md +143 -0
  230. package/docs/REQUIREMENTS.md +196 -0
  231. package/docs/SDK_DEEP_DIVE.md +643 -0
  232. package/docs/SECURITY.md +122 -0
  233. package/docs/SPEC.md +785 -0
  234. package/docs/docker-sandboxes.md +359 -0
  235. package/docs/nanoclaw-architecture-final.md +1063 -0
  236. package/docs/nanorepo-architecture.md +168 -0
  237. package/docs/skills-as-branches.md +662 -0
  238. package/groups/global/CLAUDE.md +58 -0
  239. package/groups/main/CLAUDE.md +246 -0
  240. package/launchd/com.nanoclaw.plist +32 -0
  241. package/package.json +45 -0
  242. package/repo-tokens/README.md +113 -0
  243. package/repo-tokens/action.yml +186 -0
  244. package/repo-tokens/badge.svg +23 -0
  245. package/repo-tokens/examples/green.svg +14 -0
  246. package/repo-tokens/examples/red.svg +14 -0
  247. package/repo-tokens/examples/yellow-green.svg +14 -0
  248. package/repo-tokens/examples/yellow.svg +14 -0
  249. package/scripts/run-migrations.ts +105 -0
  250. package/setup/container.ts +144 -0
  251. package/setup/environment.test.ts +121 -0
  252. package/setup/environment.ts +94 -0
  253. package/setup/groups.ts +229 -0
  254. package/setup/index.ts +58 -0
  255. package/setup/mounts.ts +115 -0
  256. package/setup/platform.test.ts +120 -0
  257. package/setup/platform.ts +132 -0
  258. package/setup/register.test.ts +257 -0
  259. package/setup/register.ts +177 -0
  260. package/setup/service.test.ts +187 -0
  261. package/setup/service.ts +362 -0
  262. package/setup/status.ts +16 -0
  263. package/setup/verify.ts +192 -0
  264. package/setup.sh +161 -0
  265. package/src/channels/index.ts +12 -0
  266. package/src/channels/registry.test.ts +42 -0
  267. package/src/channels/registry.ts +32 -0
  268. package/src/channels/web.ts +1856 -0
  269. package/src/cli.ts +209 -0
  270. package/src/config.ts +73 -0
  271. package/src/container-runner.test.ts +210 -0
  272. package/src/container-runner.ts +707 -0
  273. package/src/container-runtime.test.ts +149 -0
  274. package/src/container-runtime.ts +127 -0
  275. package/src/credential-proxy.test.ts +192 -0
  276. package/src/credential-proxy.ts +125 -0
  277. package/src/db.test.ts +484 -0
  278. package/src/db.ts +803 -0
  279. package/src/env.ts +42 -0
  280. package/src/formatting.test.ts +256 -0
  281. package/src/group-folder.test.ts +43 -0
  282. package/src/group-folder.ts +44 -0
  283. package/src/group-queue.test.ts +484 -0
  284. package/src/group-queue.ts +365 -0
  285. package/src/index.ts +731 -0
  286. package/src/ipc-auth.test.ts +679 -0
  287. package/src/ipc.ts +461 -0
  288. package/src/logger.ts +16 -0
  289. package/src/mount-security.ts +419 -0
  290. package/src/remote-control.test.ts +397 -0
  291. package/src/remote-control.ts +224 -0
  292. package/src/router.ts +52 -0
  293. package/src/routing.test.ts +170 -0
  294. package/src/sender-allowlist.test.ts +216 -0
  295. package/src/sender-allowlist.ts +128 -0
  296. package/src/session-commands.test.ts +247 -0
  297. package/src/session-commands.ts +163 -0
  298. package/src/task-scheduler.test.ts +129 -0
  299. package/src/task-scheduler.ts +295 -0
  300. package/src/timezone.test.ts +29 -0
  301. package/src/timezone.ts +16 -0
  302. package/src/types.ts +107 -0
  303. package/tsconfig.json +20 -0
  304. package/vitest.config.ts +7 -0
  305. package/vitest.skills.config.ts +7 -0
package/src/cli.ts ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * cli.ts — NanoClaw CLI entry point
3
+ *
4
+ * Usage:
5
+ * node dist/cli.js [options]
6
+ * npx nanoclaw [options] (requires "bin" field in package.json)
7
+ *
8
+ * Parses arguments, validates them (fail-fast), then starts NanoClaw.
9
+ */
10
+
11
+ import { execSync } from 'child_process';
12
+ import { existsSync } from 'fs';
13
+ import { resolve } from 'path';
14
+ import { parseArgs } from 'util';
15
+
16
+ // ─── Help text ───────────────────────────────────────────────────────────────
17
+
18
+ const HELP = `
19
+ Usage: nanoclaw [options]
20
+
21
+ Options:
22
+ --host <address> Bind address for the web channel (default: 0.0.0.0)
23
+ --port <number> Port for the web channel (default: 3099)
24
+ --workspace <path> Workspace directory (default: current directory)
25
+ --key <api-key> Anthropic API key
26
+ Not required with a Claude Pro/Max subscription —
27
+ NanoClaw uses Claude Code which is included in those plans.
28
+ --token <token> Access token for the web interface (default: no protection)
29
+ Clients must supply it via Authorization: Bearer <token>,
30
+ as a ?token=<value> query parameter, or a session cookie.
31
+ --sandbox <type> Container runtime: "docker" or "apple"
32
+ apple = macOS Sequoia 15+ Apple Container Runtime
33
+ Defaults to auto-detect (docker first, then apple).
34
+ -h, --help Show this help and exit
35
+
36
+ Environment variables (CLI flags take precedence):
37
+ NANOCLAW_HOST Bind address (same as --host)
38
+ NANOCLAW_PORT Port (same as --port)
39
+ NANOCLAW_KEY Anthropic API key (same as --key)
40
+ NANOCLAW_TOKEN Access token (same as --token)
41
+ NANOCLAW_WORKSPACE Workspace directory (same as --workspace)
42
+ NANOCLAW_SANDBOX Container runtime (same as --sandbox)
43
+
44
+ Examples:
45
+ nanoclaw
46
+ nanoclaw --port 8080 --workspace ~/my-workspace
47
+ nanoclaw --sandbox docker --host 127.0.0.1 --token s3cr3t
48
+ NANOCLAW_PORT=8080 NANOCLAW_TOKEN=s3cr3t nanoclaw
49
+ `;
50
+
51
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
52
+
53
+ function die(msg: string): never {
54
+ console.error(`Error: ${msg}`);
55
+ console.error('Run "nanoclaw --help" for usage.');
56
+ process.exit(1);
57
+ }
58
+
59
+ function checkBinary(cmd: string): boolean {
60
+ try {
61
+ execSync(cmd, { stdio: 'ignore' });
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ function detectSandbox(): 'docker' | 'apple' | null {
69
+ if (checkBinary('docker info')) return 'docker';
70
+ if (checkBinary('container --version')) return 'apple';
71
+ return null;
72
+ }
73
+
74
+ // ─── Main ────────────────────────────────────────────────────────────────────
75
+
76
+ async function cli(): Promise<void> {
77
+ // --- Parse arguments -------------------------------------------------------
78
+ let values: Record<string, string | boolean | undefined>;
79
+ try {
80
+ ({ values } = parseArgs({
81
+ args: process.argv.slice(2),
82
+ options: {
83
+ host: { type: 'string' },
84
+ port: { type: 'string' },
85
+ workspace: { type: 'string' },
86
+ key: { type: 'string' },
87
+ token: { type: 'string' },
88
+ sandbox: { type: 'string' },
89
+ help: { type: 'boolean', short: 'h' },
90
+ },
91
+ allowPositionals: false,
92
+ strict: true,
93
+ }));
94
+ } catch (err: any) {
95
+ // parseArgs throws ERR_PARSE_ARGS_UNKNOWN_OPTION for unrecognised flags
96
+ die(err?.message ?? String(err));
97
+ }
98
+
99
+ if (values.help) {
100
+ console.log(HELP);
101
+ process.exit(0);
102
+ }
103
+
104
+ // --- Merge: CLI flags override NANOCLAW_* env vars -------------------------
105
+ // Each setting resolves in priority order: CLI flag > NANOCLAW_* env var > default
106
+ const host = (values.host as string | undefined) ?? process.env.NANOCLAW_HOST;
107
+ const portStr =
108
+ (values.port as string | undefined) ?? process.env.NANOCLAW_PORT;
109
+ const key = (values.key as string | undefined) ?? process.env.NANOCLAW_KEY;
110
+ const token =
111
+ (values.token as string | undefined) ?? process.env.NANOCLAW_TOKEN;
112
+ const workspace =
113
+ (values.workspace as string | undefined) ?? process.env.NANOCLAW_WORKSPACE;
114
+ const sandboxRaw =
115
+ (values.sandbox as string | undefined) ?? process.env.NANOCLAW_SANDBOX;
116
+
117
+ // --- Validate --port -------------------------------------------------------
118
+ if (portStr !== undefined) {
119
+ const port = parseInt(portStr, 10);
120
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
121
+ die(
122
+ `Invalid port value "${portStr}". Must be an integer between 1 and 65535.`,
123
+ );
124
+ }
125
+ }
126
+
127
+ // --- Validate --sandbox ----------------------------------------------------
128
+ let sandboxType: 'docker' | 'apple';
129
+
130
+ if (sandboxRaw) {
131
+ if (sandboxRaw !== 'docker' && sandboxRaw !== 'apple') {
132
+ die(
133
+ `Invalid sandbox value "${sandboxRaw}". Must be "docker" or "apple".`,
134
+ );
135
+ }
136
+ sandboxType = sandboxRaw as 'docker' | 'apple';
137
+ } else {
138
+ const detected = detectSandbox();
139
+ if (!detected) {
140
+ die(
141
+ 'No container sandbox found.\n' +
142
+ ' • Install Docker Desktop (https://www.docker.com/products/docker-desktop)\n' +
143
+ ' • Or use macOS Sequoia 15+ with the Apple Container Runtime.',
144
+ );
145
+ }
146
+ sandboxType = detected;
147
+ console.log(`Auto-detected sandbox: ${sandboxType}`);
148
+ }
149
+
150
+ // --- Validate --workspace --------------------------------------------------
151
+ if (workspace !== undefined) {
152
+ const ws = resolve(workspace);
153
+ if (!existsSync(ws)) {
154
+ die(`Workspace directory not found: ${ws}`);
155
+ }
156
+ process.chdir(ws);
157
+ console.log(`Workspace: ${ws}`);
158
+ }
159
+
160
+ // --- Check "claude" binary -------------------------------------------------
161
+ if (!checkBinary('claude --version')) {
162
+ die(
163
+ '"claude" command not found.\n' +
164
+ ' Install Claude Code via: npm install -g @anthropic-ai/claude-code\n' +
165
+ ' or visit https://claude.ai/code',
166
+ );
167
+ }
168
+
169
+ // --- Check sandbox availability --------------------------------------------
170
+ if (sandboxType === 'docker') {
171
+ if (!checkBinary('docker info')) {
172
+ die(
173
+ 'Docker is not running.\n' +
174
+ ' Please start Docker Desktop or the Docker Engine daemon.',
175
+ );
176
+ }
177
+ } else {
178
+ // apple
179
+ if (!checkBinary('container --version')) {
180
+ die(
181
+ 'Apple Container Runtime not available.\n' +
182
+ ' Requires macOS Sequoia 15 or later.',
183
+ );
184
+ }
185
+ }
186
+
187
+ // --- Apply settings to process.env so downstream modules pick them up ------
188
+ // web.ts reads NANOCLAW_HOST / NANOCLAW_PORT / NANOCLAW_TOKEN;
189
+ // credential-proxy reads ANTHROPIC_API_KEY.
190
+ if (host) process.env.NANOCLAW_HOST = host;
191
+ if (portStr) process.env.NANOCLAW_PORT = portStr;
192
+ if (key) process.env.ANTHROPIC_API_KEY = key;
193
+ if (token) process.env.NANOCLAW_TOKEN = token;
194
+ process.env.NANOCLAW_SANDBOX = sandboxType;
195
+ process.env.CONTAINER_RUNTIME = sandboxType; // backward compat for container-runtime.ts
196
+
197
+ // --- Start NanoClaw --------------------------------------------------------
198
+ const tokenHint = token ? ' — token protection enabled' : '';
199
+ console.log(
200
+ `Starting NanoClaw (sandbox: ${sandboxType}, port: ${process.env.NANOCLAW_PORT ?? 3099}${tokenHint})…`,
201
+ );
202
+ const { main } = await import('./index.js');
203
+ await main();
204
+ }
205
+
206
+ cli().catch((err) => {
207
+ console.error('Fatal error:', err);
208
+ process.exit(1);
209
+ });
package/src/config.ts ADDED
@@ -0,0 +1,73 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+
4
+ import { readEnvFile } from './env.js';
5
+
6
+ // Read config values from .env (falls back to process.env).
7
+ // Secrets (API keys, tokens) are NOT read here — they are loaded only
8
+ // by the credential proxy (credential-proxy.ts), never exposed to containers.
9
+ const envConfig = readEnvFile(['ASSISTANT_NAME', 'ASSISTANT_HAS_OWN_NUMBER']);
10
+
11
+ export const ASSISTANT_NAME =
12
+ process.env.ASSISTANT_NAME || envConfig.ASSISTANT_NAME || 'Andy';
13
+ export const ASSISTANT_HAS_OWN_NUMBER =
14
+ (process.env.ASSISTANT_HAS_OWN_NUMBER ||
15
+ envConfig.ASSISTANT_HAS_OWN_NUMBER) === 'true';
16
+ export const POLL_INTERVAL = 2000;
17
+ export const SCHEDULER_POLL_INTERVAL = 60000;
18
+
19
+ // Absolute paths needed for container mounts
20
+ const PROJECT_ROOT = process.cwd();
21
+ const HOME_DIR = process.env.HOME || os.homedir();
22
+
23
+ // Mount security: allowlist stored OUTSIDE project root, never mounted into containers
24
+ export const MOUNT_ALLOWLIST_PATH = path.join(
25
+ HOME_DIR,
26
+ '.config',
27
+ 'nanoclaw',
28
+ 'mount-allowlist.json',
29
+ );
30
+ export const SENDER_ALLOWLIST_PATH = path.join(
31
+ HOME_DIR,
32
+ '.config',
33
+ 'nanoclaw',
34
+ 'sender-allowlist.json',
35
+ );
36
+ export const STORE_DIR = path.resolve(PROJECT_ROOT, 'store');
37
+ export const GROUPS_DIR = path.resolve(PROJECT_ROOT, 'groups');
38
+ export const DATA_DIR = path.resolve(PROJECT_ROOT, 'data');
39
+
40
+ export const CONTAINER_IMAGE =
41
+ process.env.CONTAINER_IMAGE || 'nanoclaw-agent:latest';
42
+ export const CONTAINER_TIMEOUT = parseInt(
43
+ process.env.CONTAINER_TIMEOUT || '1800000',
44
+ 10,
45
+ );
46
+ export const CONTAINER_MAX_OUTPUT_SIZE = parseInt(
47
+ process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760',
48
+ 10,
49
+ ); // 10MB default
50
+ export const CREDENTIAL_PROXY_PORT = parseInt(
51
+ process.env.CREDENTIAL_PROXY_PORT || '3001',
52
+ 10,
53
+ );
54
+ export const IPC_POLL_INTERVAL = 1000;
55
+ export const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '1800000', 10); // 30min default — how long to keep container alive after last result
56
+ export const MAX_CONCURRENT_CONTAINERS = Math.max(
57
+ 1,
58
+ parseInt(process.env.MAX_CONCURRENT_CONTAINERS || '5', 10) || 5,
59
+ );
60
+
61
+ function escapeRegex(str: string): string {
62
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
63
+ }
64
+
65
+ export const TRIGGER_PATTERN = new RegExp(
66
+ `^@${escapeRegex(ASSISTANT_NAME)}\\b`,
67
+ 'i',
68
+ );
69
+
70
+ // Timezone for scheduled tasks (cron expressions, etc.)
71
+ // Uses system timezone by default
72
+ export const TIMEZONE =
73
+ process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -0,0 +1,210 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ import { EventEmitter } from 'events';
3
+ import { PassThrough } from 'stream';
4
+
5
+ // Sentinel markers must match container-runner.ts
6
+ const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---';
7
+ const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';
8
+
9
+ // Mock config
10
+ vi.mock('./config.js', () => ({
11
+ CONTAINER_IMAGE: 'nanoclaw-agent:latest',
12
+ CONTAINER_MAX_OUTPUT_SIZE: 10485760,
13
+ CONTAINER_TIMEOUT: 1800000, // 30min
14
+ CREDENTIAL_PROXY_PORT: 3001,
15
+ DATA_DIR: '/tmp/nanoclaw-test-data',
16
+ GROUPS_DIR: '/tmp/nanoclaw-test-groups',
17
+ IDLE_TIMEOUT: 1800000, // 30min
18
+ TIMEZONE: 'America/Los_Angeles',
19
+ }));
20
+
21
+ // Mock logger
22
+ vi.mock('./logger.js', () => ({
23
+ logger: {
24
+ debug: vi.fn(),
25
+ info: vi.fn(),
26
+ warn: vi.fn(),
27
+ error: vi.fn(),
28
+ },
29
+ }));
30
+
31
+ // Mock fs
32
+ vi.mock('fs', async () => {
33
+ const actual = await vi.importActual<typeof import('fs')>('fs');
34
+ return {
35
+ ...actual,
36
+ default: {
37
+ ...actual,
38
+ existsSync: vi.fn(() => false),
39
+ mkdirSync: vi.fn(),
40
+ writeFileSync: vi.fn(),
41
+ readFileSync: vi.fn(() => ''),
42
+ readdirSync: vi.fn(() => []),
43
+ statSync: vi.fn(() => ({ isDirectory: () => false })),
44
+ copyFileSync: vi.fn(),
45
+ },
46
+ };
47
+ });
48
+
49
+ // Mock mount-security
50
+ vi.mock('./mount-security.js', () => ({
51
+ validateAdditionalMounts: vi.fn(() => []),
52
+ }));
53
+
54
+ // Create a controllable fake ChildProcess
55
+ function createFakeProcess() {
56
+ const proc = new EventEmitter() as EventEmitter & {
57
+ stdin: PassThrough;
58
+ stdout: PassThrough;
59
+ stderr: PassThrough;
60
+ kill: ReturnType<typeof vi.fn>;
61
+ pid: number;
62
+ };
63
+ proc.stdin = new PassThrough();
64
+ proc.stdout = new PassThrough();
65
+ proc.stderr = new PassThrough();
66
+ proc.kill = vi.fn();
67
+ proc.pid = 12345;
68
+ return proc;
69
+ }
70
+
71
+ let fakeProc: ReturnType<typeof createFakeProcess>;
72
+
73
+ // Mock child_process.spawn
74
+ vi.mock('child_process', async () => {
75
+ const actual =
76
+ await vi.importActual<typeof import('child_process')>('child_process');
77
+ return {
78
+ ...actual,
79
+ spawn: vi.fn(() => fakeProc),
80
+ exec: vi.fn(
81
+ (_cmd: string, _opts: unknown, cb?: (err: Error | null) => void) => {
82
+ if (cb) cb(null);
83
+ return new EventEmitter();
84
+ },
85
+ ),
86
+ };
87
+ });
88
+
89
+ import { runContainerAgent, ContainerOutput } from './container-runner.js';
90
+ import type { RegisteredGroup } from './types.js';
91
+
92
+ const testGroup: RegisteredGroup = {
93
+ name: 'Test Group',
94
+ folder: 'test-group',
95
+ trigger: '@Andy',
96
+ added_at: new Date().toISOString(),
97
+ };
98
+
99
+ const testInput = {
100
+ prompt: 'Hello',
101
+ groupFolder: 'test-group',
102
+ chatJid: 'test@g.us',
103
+ isMain: false,
104
+ };
105
+
106
+ function emitOutputMarker(
107
+ proc: ReturnType<typeof createFakeProcess>,
108
+ output: ContainerOutput,
109
+ ) {
110
+ const json = JSON.stringify(output);
111
+ proc.stdout.push(`${OUTPUT_START_MARKER}\n${json}\n${OUTPUT_END_MARKER}\n`);
112
+ }
113
+
114
+ describe('container-runner timeout behavior', () => {
115
+ beforeEach(() => {
116
+ vi.useFakeTimers();
117
+ fakeProc = createFakeProcess();
118
+ });
119
+
120
+ afterEach(() => {
121
+ vi.useRealTimers();
122
+ });
123
+
124
+ it('timeout after output resolves as success', async () => {
125
+ const onOutput = vi.fn(async () => {});
126
+ const resultPromise = runContainerAgent(
127
+ testGroup,
128
+ testInput,
129
+ () => {},
130
+ onOutput,
131
+ );
132
+
133
+ // Emit output with a result
134
+ emitOutputMarker(fakeProc, {
135
+ status: 'success',
136
+ result: 'Here is my response',
137
+ newSessionId: 'session-123',
138
+ });
139
+
140
+ // Let output processing settle
141
+ await vi.advanceTimersByTimeAsync(10);
142
+
143
+ // Fire the hard timeout (IDLE_TIMEOUT + 30s = 1830000ms)
144
+ await vi.advanceTimersByTimeAsync(1830000);
145
+
146
+ // Emit close event (as if container was stopped by the timeout)
147
+ fakeProc.emit('close', 137);
148
+
149
+ // Let the promise resolve
150
+ await vi.advanceTimersByTimeAsync(10);
151
+
152
+ const result = await resultPromise;
153
+ expect(result.status).toBe('success');
154
+ expect(result.newSessionId).toBe('session-123');
155
+ expect(onOutput).toHaveBeenCalledWith(
156
+ expect.objectContaining({ result: 'Here is my response' }),
157
+ );
158
+ });
159
+
160
+ it('timeout with no output resolves as error', async () => {
161
+ const onOutput = vi.fn(async () => {});
162
+ const resultPromise = runContainerAgent(
163
+ testGroup,
164
+ testInput,
165
+ () => {},
166
+ onOutput,
167
+ );
168
+
169
+ // No output emitted — fire the hard timeout
170
+ await vi.advanceTimersByTimeAsync(1830000);
171
+
172
+ // Emit close event
173
+ fakeProc.emit('close', 137);
174
+
175
+ await vi.advanceTimersByTimeAsync(10);
176
+
177
+ const result = await resultPromise;
178
+ expect(result.status).toBe('error');
179
+ expect(result.error).toContain('timed out');
180
+ expect(onOutput).not.toHaveBeenCalled();
181
+ });
182
+
183
+ it('normal exit after output resolves as success', async () => {
184
+ const onOutput = vi.fn(async () => {});
185
+ const resultPromise = runContainerAgent(
186
+ testGroup,
187
+ testInput,
188
+ () => {},
189
+ onOutput,
190
+ );
191
+
192
+ // Emit output
193
+ emitOutputMarker(fakeProc, {
194
+ status: 'success',
195
+ result: 'Done',
196
+ newSessionId: 'session-456',
197
+ });
198
+
199
+ await vi.advanceTimersByTimeAsync(10);
200
+
201
+ // Normal exit (no timeout)
202
+ fakeProc.emit('close', 0);
203
+
204
+ await vi.advanceTimersByTimeAsync(10);
205
+
206
+ const result = await resultPromise;
207
+ expect(result.status).toBe('success');
208
+ expect(result.newSessionId).toBe('session-456');
209
+ });
210
+ });