@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
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "nanoclaw-agent-runner",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Container-side agent runner for NanoClaw",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "start": "node dist/index.js"
10
+ },
11
+ "dependencies": {
12
+ "@anthropic-ai/claude-agent-sdk": "^0.2.76",
13
+ "@modelcontextprotocol/sdk": "^1.12.1",
14
+ "cron-parser": "^5.0.0",
15
+ "zod": "^4.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.10.7",
19
+ "typescript": "^5.7.3"
20
+ }
21
+ }
@@ -0,0 +1,558 @@
1
+ /**
2
+ * NanoClaw Agent Runner
3
+ * Runs inside a container, receives config via stdin, outputs result to stdout
4
+ *
5
+ * Input protocol:
6
+ * Stdin: Full ContainerInput JSON (read until EOF, like before)
7
+ * IPC: Follow-up messages written as JSON files to /workspace/ipc/input/
8
+ * Files: {type:"message", text:"..."}.json — polled and consumed
9
+ * Sentinel: /workspace/ipc/input/_close — signals session end
10
+ *
11
+ * Stdout protocol:
12
+ * Each result is wrapped in OUTPUT_START_MARKER / OUTPUT_END_MARKER pairs.
13
+ * Multiple results may be emitted (one per agent teams result).
14
+ * Final marker after loop ends signals completion.
15
+ */
16
+
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+ import { query, HookCallback, PreCompactHookInput } from '@anthropic-ai/claude-agent-sdk';
20
+ import { fileURLToPath } from 'url';
21
+
22
+ interface ContainerInput {
23
+ prompt: string;
24
+ sessionId?: string;
25
+ groupFolder: string;
26
+ chatJid: string;
27
+ isMain: boolean;
28
+ isScheduledTask?: boolean;
29
+ assistantName?: string;
30
+ }
31
+
32
+ interface ContainerOutput {
33
+ status: 'success' | 'error';
34
+ result: string | null;
35
+ newSessionId?: string;
36
+ error?: string;
37
+ }
38
+
39
+ interface SessionEntry {
40
+ sessionId: string;
41
+ fullPath: string;
42
+ summary: string;
43
+ firstPrompt: string;
44
+ }
45
+
46
+ interface SessionsIndex {
47
+ entries: SessionEntry[];
48
+ }
49
+
50
+ interface SDKUserMessage {
51
+ type: 'user';
52
+ message: { role: 'user'; content: string };
53
+ parent_tool_use_id: null;
54
+ session_id: string;
55
+ }
56
+
57
+ const IPC_INPUT_DIR = '/workspace/ipc/input';
58
+ const IPC_INPUT_CLOSE_SENTINEL = path.join(IPC_INPUT_DIR, '_close');
59
+ const IPC_POLL_MS = 500;
60
+
61
+ /**
62
+ * Push-based async iterable for streaming user messages to the SDK.
63
+ * Keeps the iterable alive until end() is called, preventing isSingleUserTurn.
64
+ */
65
+ class MessageStream {
66
+ private queue: SDKUserMessage[] = [];
67
+ private waiting: (() => void) | null = null;
68
+ private done = false;
69
+
70
+ push(text: string): void {
71
+ this.queue.push({
72
+ type: 'user',
73
+ message: { role: 'user', content: text },
74
+ parent_tool_use_id: null,
75
+ session_id: '',
76
+ });
77
+ this.waiting?.();
78
+ }
79
+
80
+ end(): void {
81
+ this.done = true;
82
+ this.waiting?.();
83
+ }
84
+
85
+ async *[Symbol.asyncIterator](): AsyncGenerator<SDKUserMessage> {
86
+ while (true) {
87
+ while (this.queue.length > 0) {
88
+ yield this.queue.shift()!;
89
+ }
90
+ if (this.done) return;
91
+ await new Promise<void>(r => { this.waiting = r; });
92
+ this.waiting = null;
93
+ }
94
+ }
95
+ }
96
+
97
+ async function readStdin(): Promise<string> {
98
+ return new Promise((resolve, reject) => {
99
+ let data = '';
100
+ process.stdin.setEncoding('utf8');
101
+ process.stdin.on('data', chunk => { data += chunk; });
102
+ process.stdin.on('end', () => resolve(data));
103
+ process.stdin.on('error', reject);
104
+ });
105
+ }
106
+
107
+ const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---';
108
+ const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';
109
+
110
+ function writeOutput(output: ContainerOutput): void {
111
+ console.log(OUTPUT_START_MARKER);
112
+ console.log(JSON.stringify(output));
113
+ console.log(OUTPUT_END_MARKER);
114
+ }
115
+
116
+ function log(message: string): void {
117
+ console.error(`[agent-runner] ${message}`);
118
+ }
119
+
120
+ function getSessionSummary(sessionId: string, transcriptPath: string): string | null {
121
+ const projectDir = path.dirname(transcriptPath);
122
+ const indexPath = path.join(projectDir, 'sessions-index.json');
123
+
124
+ if (!fs.existsSync(indexPath)) {
125
+ log(`Sessions index not found at ${indexPath}`);
126
+ return null;
127
+ }
128
+
129
+ try {
130
+ const index: SessionsIndex = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
131
+ const entry = index.entries.find(e => e.sessionId === sessionId);
132
+ if (entry?.summary) {
133
+ return entry.summary;
134
+ }
135
+ } catch (err) {
136
+ log(`Failed to read sessions index: ${err instanceof Error ? err.message : String(err)}`);
137
+ }
138
+
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * Archive the full transcript to conversations/ before compaction.
144
+ */
145
+ function createPreCompactHook(assistantName?: string): HookCallback {
146
+ return async (input, _toolUseId, _context) => {
147
+ const preCompact = input as PreCompactHookInput;
148
+ const transcriptPath = preCompact.transcript_path;
149
+ const sessionId = preCompact.session_id;
150
+
151
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) {
152
+ log('No transcript found for archiving');
153
+ return {};
154
+ }
155
+
156
+ try {
157
+ const content = fs.readFileSync(transcriptPath, 'utf-8');
158
+ const messages = parseTranscript(content);
159
+
160
+ if (messages.length === 0) {
161
+ log('No messages to archive');
162
+ return {};
163
+ }
164
+
165
+ const summary = getSessionSummary(sessionId, transcriptPath);
166
+ const name = summary ? sanitizeFilename(summary) : generateFallbackName();
167
+
168
+ const conversationsDir = '/workspace/group/conversations';
169
+ fs.mkdirSync(conversationsDir, { recursive: true });
170
+
171
+ const date = new Date().toISOString().split('T')[0];
172
+ const filename = `${date}-${name}.md`;
173
+ const filePath = path.join(conversationsDir, filename);
174
+
175
+ const markdown = formatTranscriptMarkdown(messages, summary, assistantName);
176
+ fs.writeFileSync(filePath, markdown);
177
+
178
+ log(`Archived conversation to ${filePath}`);
179
+ } catch (err) {
180
+ log(`Failed to archive transcript: ${err instanceof Error ? err.message : String(err)}`);
181
+ }
182
+
183
+ return {};
184
+ };
185
+ }
186
+
187
+ function sanitizeFilename(summary: string): string {
188
+ return summary
189
+ .toLowerCase()
190
+ .replace(/[^a-z0-9]+/g, '-')
191
+ .replace(/^-+|-+$/g, '')
192
+ .slice(0, 50);
193
+ }
194
+
195
+ function generateFallbackName(): string {
196
+ const time = new Date();
197
+ return `conversation-${time.getHours().toString().padStart(2, '0')}${time.getMinutes().toString().padStart(2, '0')}`;
198
+ }
199
+
200
+ interface ParsedMessage {
201
+ role: 'user' | 'assistant';
202
+ content: string;
203
+ }
204
+
205
+ function parseTranscript(content: string): ParsedMessage[] {
206
+ const messages: ParsedMessage[] = [];
207
+
208
+ for (const line of content.split('\n')) {
209
+ if (!line.trim()) continue;
210
+ try {
211
+ const entry = JSON.parse(line);
212
+ if (entry.type === 'user' && entry.message?.content) {
213
+ const text = typeof entry.message.content === 'string'
214
+ ? entry.message.content
215
+ : entry.message.content.map((c: { text?: string }) => c.text || '').join('');
216
+ if (text) messages.push({ role: 'user', content: text });
217
+ } else if (entry.type === 'assistant' && entry.message?.content) {
218
+ const textParts = entry.message.content
219
+ .filter((c: { type: string }) => c.type === 'text')
220
+ .map((c: { text: string }) => c.text);
221
+ const text = textParts.join('');
222
+ if (text) messages.push({ role: 'assistant', content: text });
223
+ }
224
+ } catch {
225
+ }
226
+ }
227
+
228
+ return messages;
229
+ }
230
+
231
+ function formatTranscriptMarkdown(messages: ParsedMessage[], title?: string | null, assistantName?: string): string {
232
+ const now = new Date();
233
+ const formatDateTime = (d: Date) => d.toLocaleString('en-US', {
234
+ month: 'short',
235
+ day: 'numeric',
236
+ hour: 'numeric',
237
+ minute: '2-digit',
238
+ hour12: true
239
+ });
240
+
241
+ const lines: string[] = [];
242
+ lines.push(`# ${title || 'Conversation'}`);
243
+ lines.push('');
244
+ lines.push(`Archived: ${formatDateTime(now)}`);
245
+ lines.push('');
246
+ lines.push('---');
247
+ lines.push('');
248
+
249
+ for (const msg of messages) {
250
+ const sender = msg.role === 'user' ? 'User' : (assistantName || 'Assistant');
251
+ const content = msg.content.length > 2000
252
+ ? msg.content.slice(0, 2000) + '...'
253
+ : msg.content;
254
+ lines.push(`**${sender}**: ${content}`);
255
+ lines.push('');
256
+ }
257
+
258
+ return lines.join('\n');
259
+ }
260
+
261
+ /**
262
+ * Check for _close sentinel.
263
+ */
264
+ function shouldClose(): boolean {
265
+ if (fs.existsSync(IPC_INPUT_CLOSE_SENTINEL)) {
266
+ try { fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); } catch { /* ignore */ }
267
+ return true;
268
+ }
269
+ return false;
270
+ }
271
+
272
+ /**
273
+ * Drain all pending IPC input messages.
274
+ * Returns messages found, or empty array.
275
+ */
276
+ function drainIpcInput(): string[] {
277
+ try {
278
+ fs.mkdirSync(IPC_INPUT_DIR, { recursive: true });
279
+ const files = fs.readdirSync(IPC_INPUT_DIR)
280
+ .filter(f => f.endsWith('.json'))
281
+ .sort();
282
+
283
+ const messages: string[] = [];
284
+ for (const file of files) {
285
+ const filePath = path.join(IPC_INPUT_DIR, file);
286
+ try {
287
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
288
+ fs.unlinkSync(filePath);
289
+ if (data.type === 'message' && data.text) {
290
+ messages.push(data.text);
291
+ }
292
+ } catch (err) {
293
+ log(`Failed to process input file ${file}: ${err instanceof Error ? err.message : String(err)}`);
294
+ try { fs.unlinkSync(filePath); } catch { /* ignore */ }
295
+ }
296
+ }
297
+ return messages;
298
+ } catch (err) {
299
+ log(`IPC drain error: ${err instanceof Error ? err.message : String(err)}`);
300
+ return [];
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Wait for a new IPC message or _close sentinel.
306
+ * Returns the messages as a single string, or null if _close.
307
+ */
308
+ function waitForIpcMessage(): Promise<string | null> {
309
+ return new Promise((resolve) => {
310
+ const poll = () => {
311
+ if (shouldClose()) {
312
+ resolve(null);
313
+ return;
314
+ }
315
+ const messages = drainIpcInput();
316
+ if (messages.length > 0) {
317
+ resolve(messages.join('\n'));
318
+ return;
319
+ }
320
+ setTimeout(poll, IPC_POLL_MS);
321
+ };
322
+ poll();
323
+ });
324
+ }
325
+
326
+ /**
327
+ * Run a single query and stream results via writeOutput.
328
+ * Uses MessageStream (AsyncIterable) to keep isSingleUserTurn=false,
329
+ * allowing agent teams subagents to run to completion.
330
+ * Also pipes IPC messages into the stream during the query.
331
+ */
332
+ async function runQuery(
333
+ prompt: string,
334
+ sessionId: string | undefined,
335
+ mcpServerPath: string,
336
+ containerInput: ContainerInput,
337
+ sdkEnv: Record<string, string | undefined>,
338
+ resumeAt?: string,
339
+ ): Promise<{ newSessionId?: string; lastAssistantUuid?: string; closedDuringQuery: boolean }> {
340
+ const stream = new MessageStream();
341
+ stream.push(prompt);
342
+
343
+ // Poll IPC for follow-up messages and _close sentinel during the query
344
+ let ipcPolling = true;
345
+ let closedDuringQuery = false;
346
+ const pollIpcDuringQuery = () => {
347
+ if (!ipcPolling) return;
348
+ if (shouldClose()) {
349
+ log('Close sentinel detected during query, ending stream');
350
+ closedDuringQuery = true;
351
+ stream.end();
352
+ ipcPolling = false;
353
+ return;
354
+ }
355
+ const messages = drainIpcInput();
356
+ for (const text of messages) {
357
+ log(`Piping IPC message into active query (${text.length} chars)`);
358
+ stream.push(text);
359
+ }
360
+ setTimeout(pollIpcDuringQuery, IPC_POLL_MS);
361
+ };
362
+ setTimeout(pollIpcDuringQuery, IPC_POLL_MS);
363
+
364
+ let newSessionId: string | undefined;
365
+ let lastAssistantUuid: string | undefined;
366
+ let messageCount = 0;
367
+ let resultCount = 0;
368
+
369
+ // Load global CLAUDE.md as additional system context (shared across all groups)
370
+ const globalClaudeMdPath = '/workspace/global/CLAUDE.md';
371
+ let globalClaudeMd: string | undefined;
372
+ if (!containerInput.isMain && fs.existsSync(globalClaudeMdPath)) {
373
+ globalClaudeMd = fs.readFileSync(globalClaudeMdPath, 'utf-8');
374
+ }
375
+
376
+ // Discover additional directories mounted at /workspace/extra/*
377
+ // These are passed to the SDK so their CLAUDE.md files are loaded automatically
378
+ const extraDirs: string[] = [];
379
+ const extraBase = '/workspace/extra';
380
+ if (fs.existsSync(extraBase)) {
381
+ for (const entry of fs.readdirSync(extraBase)) {
382
+ const fullPath = path.join(extraBase, entry);
383
+ if (fs.statSync(fullPath).isDirectory()) {
384
+ extraDirs.push(fullPath);
385
+ }
386
+ }
387
+ }
388
+ if (extraDirs.length > 0) {
389
+ log(`Additional directories: ${extraDirs.join(', ')}`);
390
+ }
391
+
392
+ for await (const message of query({
393
+ prompt: stream,
394
+ options: {
395
+ cwd: '/workspace/group',
396
+ additionalDirectories: extraDirs.length > 0 ? extraDirs : undefined,
397
+ resume: sessionId,
398
+ resumeSessionAt: resumeAt,
399
+ systemPrompt: globalClaudeMd
400
+ ? { type: 'preset' as const, preset: 'claude_code' as const, append: globalClaudeMd }
401
+ : undefined,
402
+ allowedTools: [
403
+ 'Bash',
404
+ 'Read', 'Write', 'Edit', 'Glob', 'Grep',
405
+ 'WebSearch', 'WebFetch',
406
+ 'Task', 'TaskOutput', 'TaskStop',
407
+ 'TeamCreate', 'TeamDelete', 'SendMessage',
408
+ 'TodoWrite', 'ToolSearch', 'Skill',
409
+ 'NotebookEdit',
410
+ 'mcp__nanoclaw__*'
411
+ ],
412
+ env: sdkEnv,
413
+ permissionMode: 'bypassPermissions',
414
+ allowDangerouslySkipPermissions: true,
415
+ settingSources: ['project', 'user'],
416
+ mcpServers: {
417
+ nanoclaw: {
418
+ command: 'node',
419
+ args: [mcpServerPath],
420
+ env: {
421
+ NANOCLAW_CHAT_JID: containerInput.chatJid,
422
+ NANOCLAW_GROUP_FOLDER: containerInput.groupFolder,
423
+ NANOCLAW_IS_MAIN: containerInput.isMain ? '1' : '0',
424
+ },
425
+ },
426
+ },
427
+ hooks: {
428
+ PreCompact: [{ hooks: [createPreCompactHook(containerInput.assistantName)] }],
429
+ },
430
+ }
431
+ })) {
432
+ messageCount++;
433
+ const msgType = message.type === 'system' ? `system/${(message as { subtype?: string }).subtype}` : message.type;
434
+ log(`[msg #${messageCount}] type=${msgType}`);
435
+
436
+ if (message.type === 'assistant' && 'uuid' in message) {
437
+ lastAssistantUuid = (message as { uuid: string }).uuid;
438
+ }
439
+
440
+ if (message.type === 'system' && message.subtype === 'init') {
441
+ newSessionId = message.session_id;
442
+ log(`Session initialized: ${newSessionId}`);
443
+ }
444
+
445
+ if (message.type === 'system' && (message as { subtype?: string }).subtype === 'task_notification') {
446
+ const tn = message as { task_id: string; status: string; summary: string };
447
+ log(`Task notification: task=${tn.task_id} status=${tn.status} summary=${tn.summary}`);
448
+ }
449
+
450
+ if (message.type === 'result') {
451
+ resultCount++;
452
+ const textResult = 'result' in message ? (message as { result?: string }).result : null;
453
+ log(`Result #${resultCount}: subtype=${message.subtype}${textResult ? ` text=${textResult.slice(0, 200)}` : ''}`);
454
+ writeOutput({
455
+ status: 'success',
456
+ result: textResult || null,
457
+ newSessionId
458
+ });
459
+ }
460
+ }
461
+
462
+ ipcPolling = false;
463
+ log(`Query done. Messages: ${messageCount}, results: ${resultCount}, lastAssistantUuid: ${lastAssistantUuid || 'none'}, closedDuringQuery: ${closedDuringQuery}`);
464
+ return { newSessionId, lastAssistantUuid, closedDuringQuery };
465
+ }
466
+
467
+ async function main(): Promise<void> {
468
+ let containerInput: ContainerInput;
469
+
470
+ try {
471
+ const stdinData = await readStdin();
472
+ containerInput = JSON.parse(stdinData);
473
+ try { fs.unlinkSync('/tmp/input.json'); } catch { /* may not exist */ }
474
+ log(`Received input for group: ${containerInput.groupFolder}`);
475
+ } catch (err) {
476
+ writeOutput({
477
+ status: 'error',
478
+ result: null,
479
+ error: `Failed to parse input: ${err instanceof Error ? err.message : String(err)}`
480
+ });
481
+ process.exit(1);
482
+ }
483
+
484
+ // Credentials are injected by the host's credential proxy via ANTHROPIC_BASE_URL.
485
+ // No real secrets exist in the container environment.
486
+ const sdkEnv: Record<string, string | undefined> = { ...process.env };
487
+
488
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
489
+ const mcpServerPath = path.join(__dirname, 'ipc-mcp-stdio.js');
490
+
491
+ let sessionId = containerInput.sessionId;
492
+ fs.mkdirSync(IPC_INPUT_DIR, { recursive: true });
493
+
494
+ // Clean up stale _close sentinel from previous container runs
495
+ try { fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); } catch { /* ignore */ }
496
+
497
+ // Build initial prompt (drain any pending IPC messages too)
498
+ let prompt = containerInput.prompt;
499
+ if (containerInput.isScheduledTask) {
500
+ prompt = `[SCHEDULED TASK - The following message was sent automatically and is not coming directly from the user or group.]\n\n${prompt}`;
501
+ }
502
+ const pending = drainIpcInput();
503
+ if (pending.length > 0) {
504
+ log(`Draining ${pending.length} pending IPC messages into initial prompt`);
505
+ prompt += '\n' + pending.join('\n');
506
+ }
507
+
508
+ // Query loop: run query → wait for IPC message → run new query → repeat
509
+ let resumeAt: string | undefined;
510
+ try {
511
+ while (true) {
512
+ log(`Starting query (session: ${sessionId || 'new'}, resumeAt: ${resumeAt || 'latest'})...`);
513
+
514
+ const queryResult = await runQuery(prompt, sessionId, mcpServerPath, containerInput, sdkEnv, resumeAt);
515
+ if (queryResult.newSessionId) {
516
+ sessionId = queryResult.newSessionId;
517
+ }
518
+ if (queryResult.lastAssistantUuid) {
519
+ resumeAt = queryResult.lastAssistantUuid;
520
+ }
521
+
522
+ // If _close was consumed during the query, exit immediately.
523
+ // Don't emit a session-update marker (it would reset the host's
524
+ // idle timer and cause a 30-min delay before the next _close).
525
+ if (queryResult.closedDuringQuery) {
526
+ log('Close sentinel consumed during query, exiting');
527
+ break;
528
+ }
529
+
530
+ // Emit session update so host can track it
531
+ writeOutput({ status: 'success', result: null, newSessionId: sessionId });
532
+
533
+ log('Query ended, waiting for next IPC message...');
534
+
535
+ // Wait for the next message or _close sentinel
536
+ const nextMessage = await waitForIpcMessage();
537
+ if (nextMessage === null) {
538
+ log('Close sentinel received, exiting');
539
+ break;
540
+ }
541
+
542
+ log(`Got new message (${nextMessage.length} chars), starting new query`);
543
+ prompt = nextMessage;
544
+ }
545
+ } catch (err) {
546
+ const errorMessage = err instanceof Error ? err.message : String(err);
547
+ log(`Agent error: ${errorMessage}`);
548
+ writeOutput({
549
+ status: 'error',
550
+ result: null,
551
+ newSessionId: sessionId,
552
+ error: errorMessage
553
+ });
554
+ process.exit(1);
555
+ }
556
+ }
557
+
558
+ main();