create-ironclaws 1.0.0

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 (80) hide show
  1. package/README.md +101 -0
  2. package/bin/create.js +394 -0
  3. package/package.json +33 -0
  4. package/template/.env.example +38 -0
  5. package/template/CLAUDE.md +104 -0
  6. package/template/agent-credentials.yaml +33 -0
  7. package/template/agents.yaml +22 -0
  8. package/template/container/Dockerfile +70 -0
  9. package/template/container/Dockerfile.argus +34 -0
  10. package/template/container/agent-runner/package-lock.json +1524 -0
  11. package/template/container/agent-runner/package.json +23 -0
  12. package/template/container/agent-runner/src/index.ts +630 -0
  13. package/template/container/agent-runner/src/ipc-mcp-stdio.ts +339 -0
  14. package/template/container/agent-runner/tsconfig.json +15 -0
  15. package/template/container/build-argus.sh +25 -0
  16. package/template/container/build.sh +23 -0
  17. package/template/container/skills/agent-browser/SKILL.md +159 -0
  18. package/template/container/skills/agent-status/SKILL.md +69 -0
  19. package/template/container/skills/capabilities/SKILL.md +100 -0
  20. package/template/container/skills/edit-agent/SKILL.md +93 -0
  21. package/template/container/skills/slack-formatting/SKILL.md +92 -0
  22. package/template/container/skills/status/SKILL.md +104 -0
  23. package/template/container/tools/elastic_query.py +161 -0
  24. package/template/container/tools/gdrive_tool.py +185 -0
  25. package/template/container/tools/jira_tool.py +433 -0
  26. package/template/container/tools/slack_history_tool.py +144 -0
  27. package/template/container/tools/youtube_tool.py +174 -0
  28. package/template/docker-compose.yml +54 -0
  29. package/template/docs/how-it-works.md +496 -0
  30. package/template/eslint.config.js +32 -0
  31. package/template/groups/forge/CLAUDE.md +107 -0
  32. package/template/package-lock.json +5278 -0
  33. package/template/package.json +52 -0
  34. package/template/scripts/github-app-token.py +58 -0
  35. package/template/scripts/register-expense-agent.sh +121 -0
  36. package/template/scripts/run-migrations.ts +105 -0
  37. package/template/scripts/setup-onecli-secrets.sh +252 -0
  38. package/template/setup-agents.sh +142 -0
  39. package/template/src/channels/index.ts +13 -0
  40. package/template/src/channels/registry.test.ts +42 -0
  41. package/template/src/channels/registry.ts +28 -0
  42. package/template/src/channels/slack.test.ts +859 -0
  43. package/template/src/channels/slack.ts +373 -0
  44. package/template/src/claw-skill.test.ts +45 -0
  45. package/template/src/config.ts +94 -0
  46. package/template/src/container-runner.test.ts +221 -0
  47. package/template/src/container-runner.ts +1029 -0
  48. package/template/src/container-runtime.test.ts +149 -0
  49. package/template/src/container-runtime.ts +124 -0
  50. package/template/src/db-migration.test.ts +67 -0
  51. package/template/src/db.test.ts +484 -0
  52. package/template/src/db.ts +837 -0
  53. package/template/src/env.ts +42 -0
  54. package/template/src/formatting.test.ts +294 -0
  55. package/template/src/github-token.ts +48 -0
  56. package/template/src/google-token.ts +75 -0
  57. package/template/src/group-folder.test.ts +43 -0
  58. package/template/src/group-folder.ts +44 -0
  59. package/template/src/group-queue.test.ts +484 -0
  60. package/template/src/group-queue.ts +363 -0
  61. package/template/src/http-server.ts +343 -0
  62. package/template/src/index.ts +960 -0
  63. package/template/src/ipc-auth.test.ts +679 -0
  64. package/template/src/ipc.ts +548 -0
  65. package/template/src/logger.ts +16 -0
  66. package/template/src/mount-security.ts +421 -0
  67. package/template/src/network-policy.ts +119 -0
  68. package/template/src/remote-control.test.ts +397 -0
  69. package/template/src/remote-control.ts +224 -0
  70. package/template/src/router.ts +52 -0
  71. package/template/src/routing.test.ts +170 -0
  72. package/template/src/sender-allowlist.test.ts +216 -0
  73. package/template/src/sender-allowlist.ts +128 -0
  74. package/template/src/task-scheduler.test.ts +129 -0
  75. package/template/src/task-scheduler.ts +290 -0
  76. package/template/src/timezone.test.ts +73 -0
  77. package/template/src/timezone.ts +37 -0
  78. package/template/src/types.ts +114 -0
  79. package/template/src/worktree.ts +206 -0
  80. package/template/tsconfig.json +20 -0
@@ -0,0 +1,221 @@
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
+ DATA_DIR: '/tmp/nanoclaw-test-data',
15
+ GROUPS_DIR: '/tmp/nanoclaw-test-groups',
16
+ IDLE_TIMEOUT: 1800000, // 30min
17
+ ONECLI_URL: 'http://localhost:10254',
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
+ // Mock OneCLI SDK
55
+ vi.mock('@onecli-sh/sdk', () => ({
56
+ OneCLI: class {
57
+ applyContainerConfig = vi.fn().mockResolvedValue(true);
58
+ createAgent = vi.fn().mockResolvedValue({ id: 'test' });
59
+ ensureAgent = vi
60
+ .fn()
61
+ .mockResolvedValue({ name: 'test', identifier: 'test', created: true });
62
+ },
63
+ }));
64
+
65
+ // Create a controllable fake ChildProcess
66
+ function createFakeProcess() {
67
+ const proc = new EventEmitter() as EventEmitter & {
68
+ stdin: PassThrough;
69
+ stdout: PassThrough;
70
+ stderr: PassThrough;
71
+ kill: ReturnType<typeof vi.fn>;
72
+ pid: number;
73
+ };
74
+ proc.stdin = new PassThrough();
75
+ proc.stdout = new PassThrough();
76
+ proc.stderr = new PassThrough();
77
+ proc.kill = vi.fn();
78
+ proc.pid = 12345;
79
+ return proc;
80
+ }
81
+
82
+ let fakeProc: ReturnType<typeof createFakeProcess>;
83
+
84
+ // Mock child_process.spawn
85
+ vi.mock('child_process', async () => {
86
+ const actual =
87
+ await vi.importActual<typeof import('child_process')>('child_process');
88
+ return {
89
+ ...actual,
90
+ spawn: vi.fn(() => fakeProc),
91
+ exec: vi.fn(
92
+ (_cmd: string, _opts: unknown, cb?: (err: Error | null) => void) => {
93
+ if (cb) cb(null);
94
+ return new EventEmitter();
95
+ },
96
+ ),
97
+ };
98
+ });
99
+
100
+ import { runContainerAgent, ContainerOutput } from './container-runner.js';
101
+ import type { RegisteredGroup } from './types.js';
102
+
103
+ const testGroup: RegisteredGroup = {
104
+ name: 'Test Group',
105
+ folder: 'test-group',
106
+ trigger: '@Andy',
107
+ added_at: new Date().toISOString(),
108
+ };
109
+
110
+ const testInput = {
111
+ prompt: 'Hello',
112
+ groupFolder: 'test-group',
113
+ chatJid: 'test@g.us',
114
+ isMain: false,
115
+ };
116
+
117
+ function emitOutputMarker(
118
+ proc: ReturnType<typeof createFakeProcess>,
119
+ output: ContainerOutput,
120
+ ) {
121
+ const json = JSON.stringify(output);
122
+ proc.stdout.push(`${OUTPUT_START_MARKER}\n${json}\n${OUTPUT_END_MARKER}\n`);
123
+ }
124
+
125
+ describe('container-runner timeout behavior', () => {
126
+ beforeEach(() => {
127
+ vi.useFakeTimers();
128
+ fakeProc = createFakeProcess();
129
+ });
130
+
131
+ afterEach(() => {
132
+ vi.useRealTimers();
133
+ });
134
+
135
+ it('timeout after output resolves as success', async () => {
136
+ const onOutput = vi.fn(async () => {});
137
+ const resultPromise = runContainerAgent(
138
+ testGroup,
139
+ testInput,
140
+ () => {},
141
+ onOutput,
142
+ );
143
+
144
+ // Emit output with a result
145
+ emitOutputMarker(fakeProc, {
146
+ status: 'success',
147
+ result: 'Here is my response',
148
+ newSessionId: 'session-123',
149
+ });
150
+
151
+ // Let output processing settle
152
+ await vi.advanceTimersByTimeAsync(10);
153
+
154
+ // Fire the hard timeout (IDLE_TIMEOUT + 30s = 1830000ms)
155
+ await vi.advanceTimersByTimeAsync(1830000);
156
+
157
+ // Emit close event (as if container was stopped by the timeout)
158
+ fakeProc.emit('close', 137);
159
+
160
+ // Let the promise resolve
161
+ await vi.advanceTimersByTimeAsync(10);
162
+
163
+ const result = await resultPromise;
164
+ expect(result.status).toBe('success');
165
+ expect(result.newSessionId).toBe('session-123');
166
+ expect(onOutput).toHaveBeenCalledWith(
167
+ expect.objectContaining({ result: 'Here is my response' }),
168
+ );
169
+ });
170
+
171
+ it('timeout with no output resolves as error', async () => {
172
+ const onOutput = vi.fn(async () => {});
173
+ const resultPromise = runContainerAgent(
174
+ testGroup,
175
+ testInput,
176
+ () => {},
177
+ onOutput,
178
+ );
179
+
180
+ // No output emitted — fire the hard timeout
181
+ await vi.advanceTimersByTimeAsync(1830000);
182
+
183
+ // Emit close event
184
+ fakeProc.emit('close', 137);
185
+
186
+ await vi.advanceTimersByTimeAsync(10);
187
+
188
+ const result = await resultPromise;
189
+ expect(result.status).toBe('error');
190
+ expect(result.error).toContain('timed out');
191
+ expect(onOutput).not.toHaveBeenCalled();
192
+ });
193
+
194
+ it('normal exit after output resolves as success', async () => {
195
+ const onOutput = vi.fn(async () => {});
196
+ const resultPromise = runContainerAgent(
197
+ testGroup,
198
+ testInput,
199
+ () => {},
200
+ onOutput,
201
+ );
202
+
203
+ // Emit output
204
+ emitOutputMarker(fakeProc, {
205
+ status: 'success',
206
+ result: 'Done',
207
+ newSessionId: 'session-456',
208
+ });
209
+
210
+ await vi.advanceTimersByTimeAsync(10);
211
+
212
+ // Normal exit (no timeout)
213
+ fakeProc.emit('close', 0);
214
+
215
+ await vi.advanceTimersByTimeAsync(10);
216
+
217
+ const result = await resultPromise;
218
+ expect(result.status).toBe('success');
219
+ expect(result.newSessionId).toBe('session-456');
220
+ });
221
+ });