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,484 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+
3
+ import {
4
+ _initTestDatabase,
5
+ createTask,
6
+ deleteTask,
7
+ getAllChats,
8
+ getAllRegisteredGroups,
9
+ getMessagesSince,
10
+ getNewMessages,
11
+ getTaskById,
12
+ setRegisteredGroup,
13
+ storeChatMetadata,
14
+ storeMessage,
15
+ updateTask,
16
+ } from './db.js';
17
+
18
+ beforeEach(() => {
19
+ _initTestDatabase();
20
+ });
21
+
22
+ // Helper to store a message using the normalized NewMessage interface
23
+ function store(overrides: {
24
+ id: string;
25
+ chat_jid: string;
26
+ sender: string;
27
+ sender_name: string;
28
+ content: string;
29
+ timestamp: string;
30
+ is_from_me?: boolean;
31
+ }) {
32
+ storeMessage({
33
+ id: overrides.id,
34
+ chat_jid: overrides.chat_jid,
35
+ sender: overrides.sender,
36
+ sender_name: overrides.sender_name,
37
+ content: overrides.content,
38
+ timestamp: overrides.timestamp,
39
+ is_from_me: overrides.is_from_me ?? false,
40
+ });
41
+ }
42
+
43
+ // --- storeMessage (NewMessage format) ---
44
+
45
+ describe('storeMessage', () => {
46
+ it('stores a message and retrieves it', () => {
47
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
48
+
49
+ store({
50
+ id: 'msg-1',
51
+ chat_jid: 'group@g.us',
52
+ sender: '123@s.whatsapp.net',
53
+ sender_name: 'Alice',
54
+ content: 'hello world',
55
+ timestamp: '2024-01-01T00:00:01.000Z',
56
+ });
57
+
58
+ const messages = getMessagesSince(
59
+ 'group@g.us',
60
+ '2024-01-01T00:00:00.000Z',
61
+ 'Andy',
62
+ );
63
+ expect(messages).toHaveLength(1);
64
+ expect(messages[0].id).toBe('msg-1');
65
+ expect(messages[0].sender).toBe('123@s.whatsapp.net');
66
+ expect(messages[0].sender_name).toBe('Alice');
67
+ expect(messages[0].content).toBe('hello world');
68
+ });
69
+
70
+ it('filters out empty content', () => {
71
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
72
+
73
+ store({
74
+ id: 'msg-2',
75
+ chat_jid: 'group@g.us',
76
+ sender: '111@s.whatsapp.net',
77
+ sender_name: 'Dave',
78
+ content: '',
79
+ timestamp: '2024-01-01T00:00:04.000Z',
80
+ });
81
+
82
+ const messages = getMessagesSince(
83
+ 'group@g.us',
84
+ '2024-01-01T00:00:00.000Z',
85
+ 'Andy',
86
+ );
87
+ expect(messages).toHaveLength(0);
88
+ });
89
+
90
+ it('stores is_from_me flag', () => {
91
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
92
+
93
+ store({
94
+ id: 'msg-3',
95
+ chat_jid: 'group@g.us',
96
+ sender: 'me@s.whatsapp.net',
97
+ sender_name: 'Me',
98
+ content: 'my message',
99
+ timestamp: '2024-01-01T00:00:05.000Z',
100
+ is_from_me: true,
101
+ });
102
+
103
+ // Message is stored (we can retrieve it — is_from_me doesn't affect retrieval)
104
+ const messages = getMessagesSince(
105
+ 'group@g.us',
106
+ '2024-01-01T00:00:00.000Z',
107
+ 'Andy',
108
+ );
109
+ expect(messages).toHaveLength(1);
110
+ });
111
+
112
+ it('upserts on duplicate id+chat_jid', () => {
113
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
114
+
115
+ store({
116
+ id: 'msg-dup',
117
+ chat_jid: 'group@g.us',
118
+ sender: '123@s.whatsapp.net',
119
+ sender_name: 'Alice',
120
+ content: 'original',
121
+ timestamp: '2024-01-01T00:00:01.000Z',
122
+ });
123
+
124
+ store({
125
+ id: 'msg-dup',
126
+ chat_jid: 'group@g.us',
127
+ sender: '123@s.whatsapp.net',
128
+ sender_name: 'Alice',
129
+ content: 'updated',
130
+ timestamp: '2024-01-01T00:00:01.000Z',
131
+ });
132
+
133
+ const messages = getMessagesSince(
134
+ 'group@g.us',
135
+ '2024-01-01T00:00:00.000Z',
136
+ 'Andy',
137
+ );
138
+ expect(messages).toHaveLength(1);
139
+ expect(messages[0].content).toBe('updated');
140
+ });
141
+ });
142
+
143
+ // --- getMessagesSince ---
144
+
145
+ describe('getMessagesSince', () => {
146
+ beforeEach(() => {
147
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
148
+
149
+ store({
150
+ id: 'm1',
151
+ chat_jid: 'group@g.us',
152
+ sender: 'Alice@s.whatsapp.net',
153
+ sender_name: 'Alice',
154
+ content: 'first',
155
+ timestamp: '2024-01-01T00:00:01.000Z',
156
+ });
157
+ store({
158
+ id: 'm2',
159
+ chat_jid: 'group@g.us',
160
+ sender: 'Bob@s.whatsapp.net',
161
+ sender_name: 'Bob',
162
+ content: 'second',
163
+ timestamp: '2024-01-01T00:00:02.000Z',
164
+ });
165
+ storeMessage({
166
+ id: 'm3',
167
+ chat_jid: 'group@g.us',
168
+ sender: 'Bot@s.whatsapp.net',
169
+ sender_name: 'Bot',
170
+ content: 'bot reply',
171
+ timestamp: '2024-01-01T00:00:03.000Z',
172
+ is_bot_message: true,
173
+ });
174
+ store({
175
+ id: 'm4',
176
+ chat_jid: 'group@g.us',
177
+ sender: 'Carol@s.whatsapp.net',
178
+ sender_name: 'Carol',
179
+ content: 'third',
180
+ timestamp: '2024-01-01T00:00:04.000Z',
181
+ });
182
+ });
183
+
184
+ it('returns messages after the given timestamp', () => {
185
+ const msgs = getMessagesSince(
186
+ 'group@g.us',
187
+ '2024-01-01T00:00:02.000Z',
188
+ 'Andy',
189
+ );
190
+ // Should exclude m1, m2 (before/at timestamp), m3 (bot message)
191
+ expect(msgs).toHaveLength(1);
192
+ expect(msgs[0].content).toBe('third');
193
+ });
194
+
195
+ it('excludes bot messages via is_bot_message flag', () => {
196
+ const msgs = getMessagesSince(
197
+ 'group@g.us',
198
+ '2024-01-01T00:00:00.000Z',
199
+ 'Andy',
200
+ );
201
+ const botMsgs = msgs.filter((m) => m.content === 'bot reply');
202
+ expect(botMsgs).toHaveLength(0);
203
+ });
204
+
205
+ it('returns all non-bot messages when sinceTimestamp is empty', () => {
206
+ const msgs = getMessagesSince('group@g.us', '', 'Andy');
207
+ // 3 user messages (bot message excluded)
208
+ expect(msgs).toHaveLength(3);
209
+ });
210
+
211
+ it('filters pre-migration bot messages via content prefix backstop', () => {
212
+ // Simulate a message written before migration: has prefix but is_bot_message = 0
213
+ store({
214
+ id: 'm5',
215
+ chat_jid: 'group@g.us',
216
+ sender: 'Bot@s.whatsapp.net',
217
+ sender_name: 'Bot',
218
+ content: 'Andy: old bot reply',
219
+ timestamp: '2024-01-01T00:00:05.000Z',
220
+ });
221
+ const msgs = getMessagesSince(
222
+ 'group@g.us',
223
+ '2024-01-01T00:00:04.000Z',
224
+ 'Andy',
225
+ );
226
+ expect(msgs).toHaveLength(0);
227
+ });
228
+ });
229
+
230
+ // --- getNewMessages ---
231
+
232
+ describe('getNewMessages', () => {
233
+ beforeEach(() => {
234
+ storeChatMetadata('group1@g.us', '2024-01-01T00:00:00.000Z');
235
+ storeChatMetadata('group2@g.us', '2024-01-01T00:00:00.000Z');
236
+
237
+ store({
238
+ id: 'a1',
239
+ chat_jid: 'group1@g.us',
240
+ sender: 'user@s.whatsapp.net',
241
+ sender_name: 'User',
242
+ content: 'g1 msg1',
243
+ timestamp: '2024-01-01T00:00:01.000Z',
244
+ });
245
+ store({
246
+ id: 'a2',
247
+ chat_jid: 'group2@g.us',
248
+ sender: 'user@s.whatsapp.net',
249
+ sender_name: 'User',
250
+ content: 'g2 msg1',
251
+ timestamp: '2024-01-01T00:00:02.000Z',
252
+ });
253
+ storeMessage({
254
+ id: 'a3',
255
+ chat_jid: 'group1@g.us',
256
+ sender: 'user@s.whatsapp.net',
257
+ sender_name: 'User',
258
+ content: 'bot reply',
259
+ timestamp: '2024-01-01T00:00:03.000Z',
260
+ is_bot_message: true,
261
+ });
262
+ store({
263
+ id: 'a4',
264
+ chat_jid: 'group1@g.us',
265
+ sender: 'user@s.whatsapp.net',
266
+ sender_name: 'User',
267
+ content: 'g1 msg2',
268
+ timestamp: '2024-01-01T00:00:04.000Z',
269
+ });
270
+ });
271
+
272
+ it('returns new messages across multiple groups', () => {
273
+ const { messages, newTimestamp } = getNewMessages(
274
+ ['group1@g.us', 'group2@g.us'],
275
+ '2024-01-01T00:00:00.000Z',
276
+ 'Andy',
277
+ );
278
+ // Excludes bot message, returns 3 user messages
279
+ expect(messages).toHaveLength(3);
280
+ expect(newTimestamp).toBe('2024-01-01T00:00:04.000Z');
281
+ });
282
+
283
+ it('filters by timestamp', () => {
284
+ const { messages } = getNewMessages(
285
+ ['group1@g.us', 'group2@g.us'],
286
+ '2024-01-01T00:00:02.000Z',
287
+ 'Andy',
288
+ );
289
+ // Only g1 msg2 (after ts, not bot)
290
+ expect(messages).toHaveLength(1);
291
+ expect(messages[0].content).toBe('g1 msg2');
292
+ });
293
+
294
+ it('returns empty for no registered groups', () => {
295
+ const { messages, newTimestamp } = getNewMessages([], '', 'Andy');
296
+ expect(messages).toHaveLength(0);
297
+ expect(newTimestamp).toBe('');
298
+ });
299
+ });
300
+
301
+ // --- storeChatMetadata ---
302
+
303
+ describe('storeChatMetadata', () => {
304
+ it('stores chat with JID as default name', () => {
305
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
306
+ const chats = getAllChats();
307
+ expect(chats).toHaveLength(1);
308
+ expect(chats[0].jid).toBe('group@g.us');
309
+ expect(chats[0].name).toBe('group@g.us');
310
+ });
311
+
312
+ it('stores chat with explicit name', () => {
313
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z', 'My Group');
314
+ const chats = getAllChats();
315
+ expect(chats[0].name).toBe('My Group');
316
+ });
317
+
318
+ it('updates name on subsequent call with name', () => {
319
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
320
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:01.000Z', 'Updated Name');
321
+ const chats = getAllChats();
322
+ expect(chats).toHaveLength(1);
323
+ expect(chats[0].name).toBe('Updated Name');
324
+ });
325
+
326
+ it('preserves newer timestamp on conflict', () => {
327
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:05.000Z');
328
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:01.000Z');
329
+ const chats = getAllChats();
330
+ expect(chats[0].last_message_time).toBe('2024-01-01T00:00:05.000Z');
331
+ });
332
+ });
333
+
334
+ // --- Task CRUD ---
335
+
336
+ describe('task CRUD', () => {
337
+ it('creates and retrieves a task', () => {
338
+ createTask({
339
+ id: 'task-1',
340
+ group_folder: 'main',
341
+ chat_jid: 'group@g.us',
342
+ prompt: 'do something',
343
+ schedule_type: 'once',
344
+ schedule_value: '2024-06-01T00:00:00.000Z',
345
+ context_mode: 'isolated',
346
+ next_run: '2024-06-01T00:00:00.000Z',
347
+ status: 'active',
348
+ created_at: '2024-01-01T00:00:00.000Z',
349
+ });
350
+
351
+ const task = getTaskById('task-1');
352
+ expect(task).toBeDefined();
353
+ expect(task!.prompt).toBe('do something');
354
+ expect(task!.status).toBe('active');
355
+ });
356
+
357
+ it('updates task status', () => {
358
+ createTask({
359
+ id: 'task-2',
360
+ group_folder: 'main',
361
+ chat_jid: 'group@g.us',
362
+ prompt: 'test',
363
+ schedule_type: 'once',
364
+ schedule_value: '2024-06-01T00:00:00.000Z',
365
+ context_mode: 'isolated',
366
+ next_run: null,
367
+ status: 'active',
368
+ created_at: '2024-01-01T00:00:00.000Z',
369
+ });
370
+
371
+ updateTask('task-2', { status: 'paused' });
372
+ expect(getTaskById('task-2')!.status).toBe('paused');
373
+ });
374
+
375
+ it('deletes a task and its run logs', () => {
376
+ createTask({
377
+ id: 'task-3',
378
+ group_folder: 'main',
379
+ chat_jid: 'group@g.us',
380
+ prompt: 'delete me',
381
+ schedule_type: 'once',
382
+ schedule_value: '2024-06-01T00:00:00.000Z',
383
+ context_mode: 'isolated',
384
+ next_run: null,
385
+ status: 'active',
386
+ created_at: '2024-01-01T00:00:00.000Z',
387
+ });
388
+
389
+ deleteTask('task-3');
390
+ expect(getTaskById('task-3')).toBeUndefined();
391
+ });
392
+ });
393
+
394
+ // --- LIMIT behavior ---
395
+
396
+ describe('message query LIMIT', () => {
397
+ beforeEach(() => {
398
+ storeChatMetadata('group@g.us', '2024-01-01T00:00:00.000Z');
399
+
400
+ for (let i = 1; i <= 10; i++) {
401
+ store({
402
+ id: `lim-${i}`,
403
+ chat_jid: 'group@g.us',
404
+ sender: 'user@s.whatsapp.net',
405
+ sender_name: 'User',
406
+ content: `message ${i}`,
407
+ timestamp: `2024-01-01T00:00:${String(i).padStart(2, '0')}.000Z`,
408
+ });
409
+ }
410
+ });
411
+
412
+ it('getNewMessages caps to limit and returns most recent in chronological order', () => {
413
+ const { messages, newTimestamp } = getNewMessages(
414
+ ['group@g.us'],
415
+ '2024-01-01T00:00:00.000Z',
416
+ 'Andy',
417
+ 3,
418
+ );
419
+ expect(messages).toHaveLength(3);
420
+ expect(messages[0].content).toBe('message 8');
421
+ expect(messages[2].content).toBe('message 10');
422
+ // Chronological order preserved
423
+ expect(messages[1].timestamp > messages[0].timestamp).toBe(true);
424
+ // newTimestamp reflects latest returned row
425
+ expect(newTimestamp).toBe('2024-01-01T00:00:10.000Z');
426
+ });
427
+
428
+ it('getMessagesSince caps to limit and returns most recent in chronological order', () => {
429
+ const messages = getMessagesSince(
430
+ 'group@g.us',
431
+ '2024-01-01T00:00:00.000Z',
432
+ 'Andy',
433
+ 3,
434
+ );
435
+ expect(messages).toHaveLength(3);
436
+ expect(messages[0].content).toBe('message 8');
437
+ expect(messages[2].content).toBe('message 10');
438
+ expect(messages[1].timestamp > messages[0].timestamp).toBe(true);
439
+ });
440
+
441
+ it('returns all messages when count is under the limit', () => {
442
+ const { messages } = getNewMessages(
443
+ ['group@g.us'],
444
+ '2024-01-01T00:00:00.000Z',
445
+ 'Andy',
446
+ 50,
447
+ );
448
+ expect(messages).toHaveLength(10);
449
+ });
450
+ });
451
+
452
+ // --- RegisteredGroup isMain round-trip ---
453
+
454
+ describe('registered group isMain', () => {
455
+ it('persists isMain=true through set/get round-trip', () => {
456
+ setRegisteredGroup('main@s.whatsapp.net', {
457
+ name: 'Main Chat',
458
+ folder: 'whatsapp_main',
459
+ trigger: '@Andy',
460
+ added_at: '2024-01-01T00:00:00.000Z',
461
+ isMain: true,
462
+ });
463
+
464
+ const groups = getAllRegisteredGroups();
465
+ const group = groups['main@s.whatsapp.net'];
466
+ expect(group).toBeDefined();
467
+ expect(group.isMain).toBe(true);
468
+ expect(group.folder).toBe('whatsapp_main');
469
+ });
470
+
471
+ it('omits isMain for non-main groups', () => {
472
+ setRegisteredGroup('group@g.us', {
473
+ name: 'Family Chat',
474
+ folder: 'whatsapp_family-chat',
475
+ trigger: '@Andy',
476
+ added_at: '2024-01-01T00:00:00.000Z',
477
+ });
478
+
479
+ const groups = getAllRegisteredGroups();
480
+ const group = groups['group@g.us'];
481
+ expect(group).toBeDefined();
482
+ expect(group.isMain).toBeUndefined();
483
+ });
484
+ });