@rozek/nanoclaw 0.0.4 → 0.0.6

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 (132) hide show
  1. package/container/agent-runner/package-lock.json +1524 -0
  2. package/dist/cli.js +75 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +34 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +7 -1
  8. package/.claude/settings.json +0 -1
  9. package/.claude/skills/add-compact/SKILL.md +0 -135
  10. package/.claude/skills/add-discord/SKILL.md +0 -203
  11. package/.claude/skills/add-gmail/SKILL.md +0 -220
  12. package/.claude/skills/add-image-vision/SKILL.md +0 -94
  13. package/.claude/skills/add-ollama-tool/SKILL.md +0 -153
  14. package/.claude/skills/add-parallel/SKILL.md +0 -290
  15. package/.claude/skills/add-pdf-reader/SKILL.md +0 -104
  16. package/.claude/skills/add-reactions/SKILL.md +0 -117
  17. package/.claude/skills/add-slack/SKILL.md +0 -207
  18. package/.claude/skills/add-telegram/SKILL.md +0 -222
  19. package/.claude/skills/add-telegram-swarm/SKILL.md +0 -384
  20. package/.claude/skills/add-voice-transcription/SKILL.md +0 -148
  21. package/.claude/skills/add-whatsapp/SKILL.md +0 -372
  22. package/.claude/skills/convert-to-apple-container/SKILL.md +0 -175
  23. package/.claude/skills/customize/SKILL.md +0 -110
  24. package/.claude/skills/debug/SKILL.md +0 -349
  25. package/.claude/skills/get-qodo-rules/SKILL.md +0 -122
  26. package/.claude/skills/get-qodo-rules/references/output-format.md +0 -41
  27. package/.claude/skills/get-qodo-rules/references/pagination.md +0 -33
  28. package/.claude/skills/get-qodo-rules/references/repository-scope.md +0 -26
  29. package/.claude/skills/qodo-pr-resolver/SKILL.md +0 -326
  30. package/.claude/skills/qodo-pr-resolver/resources/providers.md +0 -329
  31. package/.claude/skills/setup/SKILL.md +0 -218
  32. package/.claude/skills/update-nanoclaw/SKILL.md +0 -235
  33. package/.claude/skills/update-skills/SKILL.md +0 -130
  34. package/.claude/skills/use-local-whisper/SKILL.md +0 -152
  35. package/.claude/skills/x-integration/SKILL.md +0 -417
  36. package/.claude/skills/x-integration/agent.ts +0 -243
  37. package/.claude/skills/x-integration/host.ts +0 -159
  38. package/.claude/skills/x-integration/lib/browser.ts +0 -148
  39. package/.claude/skills/x-integration/lib/config.ts +0 -62
  40. package/.claude/skills/x-integration/scripts/like.ts +0 -56
  41. package/.claude/skills/x-integration/scripts/post.ts +0 -66
  42. package/.claude/skills/x-integration/scripts/quote.ts +0 -80
  43. package/.claude/skills/x-integration/scripts/reply.ts +0 -74
  44. package/.claude/skills/x-integration/scripts/retweet.ts +0 -62
  45. package/.claude/skills/x-integration/scripts/setup.ts +0 -87
  46. package/.env.example +0 -1
  47. package/.github/CODEOWNERS +0 -10
  48. package/.github/PULL_REQUEST_TEMPLATE.md +0 -14
  49. package/.github/workflows/bump-version.yml +0 -32
  50. package/.github/workflows/ci.yml +0 -25
  51. package/.github/workflows/merge-forward-skills.yml +0 -160
  52. package/.github/workflows/update-tokens.yml +0 -42
  53. package/.husky/pre-commit +0 -1
  54. package/.mcp.json +0 -3
  55. package/.nvmrc +0 -1
  56. package/.prettierrc +0 -3
  57. package/CHANGELOG.md +0 -8
  58. package/CONTRIBUTING.md +0 -23
  59. package/CONTRIBUTORS.md +0 -15
  60. package/NanoClaw_with_Web-Support.md +0 -325
  61. package/README_zh.md +0 -200
  62. package/assets/nanoclaw-favicon.png +0 -0
  63. package/assets/nanoclaw-icon.png +0 -0
  64. package/assets/nanoclaw-logo-dark.png +0 -0
  65. package/assets/nanoclaw-logo.png +0 -0
  66. package/assets/nanoclaw-profile.jpeg +0 -0
  67. package/assets/nanoclaw-sales.png +0 -0
  68. package/assets/social-preview.jpg +0 -0
  69. package/config-examples/mount-allowlist.json +0 -25
  70. package/docs/APPLE-CONTAINER-NETWORKING.md +0 -90
  71. package/docs/DEBUG_CHECKLIST.md +0 -143
  72. package/docs/REQUIREMENTS.md +0 -196
  73. package/docs/SDK_DEEP_DIVE.md +0 -643
  74. package/docs/SECURITY.md +0 -122
  75. package/docs/SPEC.md +0 -785
  76. package/docs/docker-sandboxes.md +0 -359
  77. package/docs/nanoclaw-architecture-final.md +0 -1063
  78. package/docs/nanorepo-architecture.md +0 -168
  79. package/docs/skills-as-branches.md +0 -662
  80. package/groups/global/CLAUDE.md +0 -58
  81. package/groups/main/CLAUDE.md +0 -246
  82. package/launchd/com.nanoclaw.plist +0 -32
  83. package/repo-tokens/README.md +0 -113
  84. package/repo-tokens/action.yml +0 -186
  85. package/repo-tokens/badge.svg +0 -23
  86. package/repo-tokens/examples/green.svg +0 -14
  87. package/repo-tokens/examples/red.svg +0 -14
  88. package/repo-tokens/examples/yellow-green.svg +0 -14
  89. package/repo-tokens/examples/yellow.svg +0 -14
  90. package/scripts/run-migrations.ts +0 -105
  91. package/setup.sh +0 -161
  92. package/src/channels/index.ts +0 -15
  93. package/src/channels/registry.test.ts +0 -42
  94. package/src/channels/registry.ts +0 -32
  95. package/src/channels/web.ts +0 -1931
  96. package/src/cli.ts +0 -210
  97. package/src/config.ts +0 -73
  98. package/src/container-runner.test.ts +0 -210
  99. package/src/container-runner.ts +0 -768
  100. package/src/container-runtime.test.ts +0 -149
  101. package/src/container-runtime.ts +0 -127
  102. package/src/credential-proxy.test.ts +0 -192
  103. package/src/credential-proxy.ts +0 -125
  104. package/src/db.test.ts +0 -484
  105. package/src/db.ts +0 -803
  106. package/src/env.ts +0 -42
  107. package/src/formatting.test.ts +0 -256
  108. package/src/group-folder.test.ts +0 -43
  109. package/src/group-folder.ts +0 -44
  110. package/src/group-queue.test.ts +0 -484
  111. package/src/group-queue.ts +0 -379
  112. package/src/index.ts +0 -854
  113. package/src/ipc-auth.test.ts +0 -679
  114. package/src/ipc.ts +0 -461
  115. package/src/logger.ts +0 -16
  116. package/src/mount-security.ts +0 -419
  117. package/src/remote-control.test.ts +0 -397
  118. package/src/remote-control.ts +0 -224
  119. package/src/router.ts +0 -52
  120. package/src/routing.test.ts +0 -170
  121. package/src/sender-allowlist.test.ts +0 -216
  122. package/src/sender-allowlist.ts +0 -128
  123. package/src/session-commands.test.ts +0 -247
  124. package/src/session-commands.ts +0 -163
  125. package/src/task-scheduler.test.ts +0 -129
  126. package/src/task-scheduler.ts +0 -328
  127. package/src/timezone.test.ts +0 -29
  128. package/src/timezone.ts +0 -16
  129. package/src/types.ts +0 -109
  130. package/tsconfig.json +0 -20
  131. package/vitest.config.ts +0 -7
  132. package/vitest.skills.config.ts +0 -7
@@ -1,379 +0,0 @@
1
- import { ChildProcess } from 'child_process';
2
- import fs from 'fs';
3
- import path from 'path';
4
-
5
- import { DATA_DIR, MAX_CONCURRENT_CONTAINERS } from './config.js';
6
- import { logger } from './logger.js';
7
-
8
- interface QueuedTask {
9
- id: string;
10
- groupJid: string;
11
- fn: () => Promise<void>;
12
- }
13
-
14
- const MAX_RETRIES = 5;
15
- const BASE_RETRY_MS = 5000;
16
-
17
- interface GroupState {
18
- active: boolean;
19
- idleWaiting: boolean;
20
- isTaskContainer: boolean;
21
- runningTaskId: string | null;
22
- pendingMessages: boolean;
23
- pendingTasks: QueuedTask[];
24
- process: ChildProcess | null;
25
- containerName: string | null;
26
- groupFolder: string | null;
27
- retryCount: number;
28
- }
29
-
30
- export class GroupQueue {
31
- private groups = new Map<string, GroupState>();
32
- private activeCount = 0;
33
- private waitingGroups: string[] = [];
34
- private processMessagesFn: ((groupJid: string) => Promise<boolean>) | null =
35
- null;
36
- private shuttingDown = false;
37
-
38
- private getGroup(groupJid: string): GroupState {
39
- let state = this.groups.get(groupJid);
40
- if (!state) {
41
- state = {
42
- active: false,
43
- idleWaiting: false,
44
- isTaskContainer: false,
45
- runningTaskId: null,
46
- pendingMessages: false,
47
- pendingTasks: [],
48
- process: null,
49
- containerName: null,
50
- groupFolder: null,
51
- retryCount: 0,
52
- };
53
- this.groups.set(groupJid, state);
54
- }
55
- return state;
56
- }
57
-
58
- setProcessMessagesFn(fn: (groupJid: string) => Promise<boolean>): void {
59
- this.processMessagesFn = fn;
60
- }
61
-
62
- enqueueMessageCheck(groupJid: string): void {
63
- if (this.shuttingDown) return;
64
-
65
- const state = this.getGroup(groupJid);
66
-
67
- if (state.active) {
68
- state.pendingMessages = true;
69
- logger.debug({ groupJid }, 'Container active, message queued');
70
- return;
71
- }
72
-
73
- if (this.activeCount >= MAX_CONCURRENT_CONTAINERS) {
74
- state.pendingMessages = true;
75
- if (!this.waitingGroups.includes(groupJid)) {
76
- this.waitingGroups.push(groupJid);
77
- }
78
- logger.debug(
79
- { groupJid, activeCount: this.activeCount },
80
- 'At concurrency limit, message queued',
81
- );
82
- return;
83
- }
84
-
85
- this.runForGroup(groupJid, 'messages').catch((err) =>
86
- logger.error({ groupJid, err }, 'Unhandled error in runForGroup'),
87
- );
88
- }
89
-
90
- enqueueTask(groupJid: string, taskId: string, fn: () => Promise<void>): void {
91
- if (this.shuttingDown) return;
92
-
93
- const state = this.getGroup(groupJid);
94
-
95
- // Prevent double-queuing: check both pending and currently-running task
96
- if (state.runningTaskId === taskId) {
97
- logger.debug({ groupJid, taskId }, 'Task already running, skipping');
98
- return;
99
- }
100
- if (state.pendingTasks.some((t) => t.id === taskId)) {
101
- logger.debug({ groupJid, taskId }, 'Task already queued, skipping');
102
- return;
103
- }
104
-
105
- if (state.active) {
106
- state.pendingTasks.push({ id: taskId, groupJid, fn });
107
- if (state.idleWaiting) {
108
- this.closeStdin(groupJid);
109
- }
110
- logger.debug({ groupJid, taskId }, 'Container active, task queued');
111
- return;
112
- }
113
-
114
- if (this.activeCount >= MAX_CONCURRENT_CONTAINERS) {
115
- state.pendingTasks.push({ id: taskId, groupJid, fn });
116
- if (!this.waitingGroups.includes(groupJid)) {
117
- this.waitingGroups.push(groupJid);
118
- }
119
- logger.debug(
120
- { groupJid, taskId, activeCount: this.activeCount },
121
- 'At concurrency limit, task queued',
122
- );
123
- return;
124
- }
125
-
126
- // Run immediately
127
- this.runTask(groupJid, { id: taskId, groupJid, fn }).catch((err) =>
128
- logger.error({ groupJid, taskId, err }, 'Unhandled error in runTask'),
129
- );
130
- }
131
-
132
- registerProcess(
133
- groupJid: string,
134
- proc: ChildProcess,
135
- containerName: string,
136
- groupFolder?: string,
137
- ): void {
138
- const state = this.getGroup(groupJid);
139
- state.process = proc;
140
- state.containerName = containerName;
141
- if (groupFolder) state.groupFolder = groupFolder;
142
- }
143
-
144
- /**
145
- * Mark the container as idle-waiting (finished work, waiting for IPC input).
146
- * If tasks are pending, preempt the idle container immediately.
147
- */
148
- notifyIdle(groupJid: string): void {
149
- const state = this.getGroup(groupJid);
150
- state.idleWaiting = true;
151
- if (state.pendingTasks.length > 0) {
152
- this.closeStdin(groupJid);
153
- }
154
- }
155
-
156
- /**
157
- * Send a follow-up message to the active container via IPC file.
158
- * Returns true if the message was written, false if no active container.
159
- */
160
- sendMessage(groupJid: string, text: string): boolean {
161
- const state = this.getGroup(groupJid);
162
- if (!state.active || !state.groupFolder || state.isTaskContainer)
163
- return false;
164
- state.idleWaiting = false; // Agent is about to receive work, no longer idle
165
-
166
- const inputDir = path.join(DATA_DIR, 'ipc', state.groupFolder, 'input');
167
- try {
168
- fs.mkdirSync(inputDir, { recursive: true });
169
- const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}.json`;
170
- const filepath = path.join(inputDir, filename);
171
- const tempPath = `${filepath}.tmp`;
172
- fs.writeFileSync(tempPath, JSON.stringify({ type: 'message', text }));
173
- fs.renameSync(tempPath, filepath);
174
- return true;
175
- } catch {
176
- return false;
177
- }
178
- }
179
-
180
- /**
181
- * Signal the active container to wind down by writing a close sentinel.
182
- */
183
- closeStdin(groupJid: string): void {
184
- const state = this.getGroup(groupJid);
185
- if (!state.active || !state.groupFolder) return;
186
-
187
- const inputDir = path.join(DATA_DIR, 'ipc', state.groupFolder, 'input');
188
- try {
189
- fs.mkdirSync(inputDir, { recursive: true });
190
- fs.writeFileSync(path.join(inputDir, '_close'), '');
191
- } catch {
192
- // ignore
193
- }
194
- }
195
-
196
- /**
197
- * Forcefully cancel the active container for a group.
198
- * Writes the close sentinel (graceful) and immediately kills the spawned
199
- * container process so the current SDK query is aborted right away.
200
- */
201
- cancelContainer(groupJid: string): void {
202
- this.closeStdin(groupJid); // graceful signal first
203
- const state = this.getGroup(groupJid);
204
- if (state.process && !state.process.killed) {
205
- logger.info({ groupJid }, 'Killing container process for cancel request');
206
- state.process.kill('SIGTERM');
207
- }
208
- }
209
-
210
- private async runForGroup(
211
- groupJid: string,
212
- reason: 'messages' | 'drain',
213
- ): Promise<void> {
214
- const state = this.getGroup(groupJid);
215
- state.active = true;
216
- state.idleWaiting = false;
217
- state.isTaskContainer = false;
218
- state.pendingMessages = false;
219
- this.activeCount++;
220
-
221
- logger.debug(
222
- { groupJid, reason, activeCount: this.activeCount },
223
- 'Starting container for group',
224
- );
225
-
226
- try {
227
- if (this.processMessagesFn) {
228
- const success = await this.processMessagesFn(groupJid);
229
- if (success) {
230
- state.retryCount = 0;
231
- } else {
232
- this.scheduleRetry(groupJid, state);
233
- }
234
- }
235
- } catch (err) {
236
- logger.error({ groupJid, err }, 'Error processing messages for group');
237
- this.scheduleRetry(groupJid, state);
238
- } finally {
239
- state.active = false;
240
- state.process = null;
241
- state.containerName = null;
242
- state.groupFolder = null;
243
- this.activeCount--;
244
- this.drainGroup(groupJid);
245
- }
246
- }
247
-
248
- private async runTask(groupJid: string, task: QueuedTask): Promise<void> {
249
- const state = this.getGroup(groupJid);
250
- state.active = true;
251
- state.idleWaiting = false;
252
- state.isTaskContainer = true;
253
- state.runningTaskId = task.id;
254
- this.activeCount++;
255
-
256
- logger.debug(
257
- { groupJid, taskId: task.id, activeCount: this.activeCount },
258
- 'Running queued task',
259
- );
260
-
261
- try {
262
- await task.fn();
263
- } catch (err) {
264
- logger.error({ groupJid, taskId: task.id, err }, 'Error running task');
265
- } finally {
266
- state.active = false;
267
- state.isTaskContainer = false;
268
- state.runningTaskId = null;
269
- state.process = null;
270
- state.containerName = null;
271
- state.groupFolder = null;
272
- this.activeCount--;
273
- this.drainGroup(groupJid);
274
- }
275
- }
276
-
277
- private scheduleRetry(groupJid: string, state: GroupState): void {
278
- state.retryCount++;
279
- if (state.retryCount > MAX_RETRIES) {
280
- logger.error(
281
- { groupJid, retryCount: state.retryCount },
282
- 'Max retries exceeded, dropping messages (will retry on next incoming message)',
283
- );
284
- state.retryCount = 0;
285
- return;
286
- }
287
-
288
- const delayMs = BASE_RETRY_MS * Math.pow(2, state.retryCount - 1);
289
- logger.info(
290
- { groupJid, retryCount: state.retryCount, delayMs },
291
- 'Scheduling retry with backoff',
292
- );
293
- setTimeout(() => {
294
- if (!this.shuttingDown) {
295
- this.enqueueMessageCheck(groupJid);
296
- }
297
- }, delayMs);
298
- }
299
-
300
- private drainGroup(groupJid: string): void {
301
- if (this.shuttingDown) return;
302
-
303
- const state = this.getGroup(groupJid);
304
-
305
- // Tasks first (they won't be re-discovered from SQLite like messages)
306
- if (state.pendingTasks.length > 0) {
307
- const task = state.pendingTasks.shift()!;
308
- this.runTask(groupJid, task).catch((err) =>
309
- logger.error(
310
- { groupJid, taskId: task.id, err },
311
- 'Unhandled error in runTask (drain)',
312
- ),
313
- );
314
- return;
315
- }
316
-
317
- // Then pending messages
318
- if (state.pendingMessages) {
319
- this.runForGroup(groupJid, 'drain').catch((err) =>
320
- logger.error(
321
- { groupJid, err },
322
- 'Unhandled error in runForGroup (drain)',
323
- ),
324
- );
325
- return;
326
- }
327
-
328
- // Nothing pending for this group; check if other groups are waiting for a slot
329
- this.drainWaiting();
330
- }
331
-
332
- private drainWaiting(): void {
333
- while (
334
- this.waitingGroups.length > 0 &&
335
- this.activeCount < MAX_CONCURRENT_CONTAINERS
336
- ) {
337
- const nextJid = this.waitingGroups.shift()!;
338
- const state = this.getGroup(nextJid);
339
-
340
- // Prioritize tasks over messages
341
- if (state.pendingTasks.length > 0) {
342
- const task = state.pendingTasks.shift()!;
343
- this.runTask(nextJid, task).catch((err) =>
344
- logger.error(
345
- { groupJid: nextJid, taskId: task.id, err },
346
- 'Unhandled error in runTask (waiting)',
347
- ),
348
- );
349
- } else if (state.pendingMessages) {
350
- this.runForGroup(nextJid, 'drain').catch((err) =>
351
- logger.error(
352
- { groupJid: nextJid, err },
353
- 'Unhandled error in runForGroup (waiting)',
354
- ),
355
- );
356
- }
357
- // If neither pending, skip this group
358
- }
359
- }
360
-
361
- async shutdown(_gracePeriodMs: number): Promise<void> {
362
- this.shuttingDown = true;
363
-
364
- // Count active containers but don't kill them — they'll finish on their own
365
- // via idle timeout or container timeout. The --rm flag cleans them up on exit.
366
- // This prevents WhatsApp reconnection restarts from killing working agents.
367
- const activeContainers: string[] = [];
368
- for (const [jid, state] of this.groups) {
369
- if (state.process && !state.process.killed && state.containerName) {
370
- activeContainers.push(state.containerName);
371
- }
372
- }
373
-
374
- logger.info(
375
- { activeCount: this.activeCount, detachedContainers: activeContainers },
376
- 'GroupQueue shutting down (containers detached, not killed)',
377
- );
378
- }
379
- }