@rozek/nanoclaw 0.0.24 → 0.0.25
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.
- package/container/agent-runner/src/index.ts +72 -0
- package/container/agent-runner/src/ipc-mcp-stdio.ts +4 -0
- package/dist/channels/index.d.ts +0 -1
- package/dist/channels/index.d.ts.map +1 -1
- package/dist/channels/index.js +0 -1
- package/dist/channels/index.js.map +1 -1
- package/dist/channels/web.js +89 -49
- package/dist/channels/web.js.map +1 -1
- package/dist/container-runner.d.ts +2 -0
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js.map +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +14 -3
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/ipc.d.ts +1 -0
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +3 -0
- package/dist/ipc.js.map +1 -1
- package/dist/task-scheduler.d.ts.map +1 -1
- package/dist/task-scheduler.js +2 -0
- package/dist/task-scheduler.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -5
- package/setup/register.test.ts +209 -2
- package/setup/register.ts +29 -4
package/setup/register.test.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, describe, it, expect, beforeEach } from 'vitest';
|
|
2
5
|
|
|
3
6
|
import Database from 'better-sqlite3';
|
|
4
7
|
|
|
@@ -6,7 +9,7 @@ import Database from 'better-sqlite3';
|
|
|
6
9
|
* Tests for the register step.
|
|
7
10
|
*
|
|
8
11
|
* Verifies: parameterized SQL (no injection), file templating,
|
|
9
|
-
* apostrophe in names, .env updates.
|
|
12
|
+
* apostrophe in names, .env updates, CLAUDE.md template copy.
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
function createTestDb(): Database.Database {
|
|
@@ -255,3 +258,207 @@ describe('file templating', () => {
|
|
|
255
258
|
expect(envContent).toContain('ASSISTANT_NAME="Nova"');
|
|
256
259
|
});
|
|
257
260
|
});
|
|
261
|
+
|
|
262
|
+
describe('CLAUDE.md template copy', () => {
|
|
263
|
+
let tmpDir: string;
|
|
264
|
+
let groupsDir: string;
|
|
265
|
+
|
|
266
|
+
// Replicates register.ts template copy + name update logic
|
|
267
|
+
function simulateRegister(
|
|
268
|
+
folder: string,
|
|
269
|
+
isMain: boolean,
|
|
270
|
+
assistantName = 'Andy',
|
|
271
|
+
): void {
|
|
272
|
+
const folderDir = path.join(groupsDir, folder);
|
|
273
|
+
fs.mkdirSync(path.join(folderDir, 'logs'), { recursive: true });
|
|
274
|
+
|
|
275
|
+
// Template copy — never overwrite existing (register.ts lines 119-135)
|
|
276
|
+
const dest = path.join(folderDir, 'CLAUDE.md');
|
|
277
|
+
if (!fs.existsSync(dest)) {
|
|
278
|
+
const templatePath = isMain
|
|
279
|
+
? path.join(groupsDir, 'main', 'CLAUDE.md')
|
|
280
|
+
: path.join(groupsDir, 'global', 'CLAUDE.md');
|
|
281
|
+
if (fs.existsSync(templatePath)) {
|
|
282
|
+
fs.copyFileSync(templatePath, dest);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Name update across all groups (register.ts lines 140-165)
|
|
287
|
+
if (assistantName !== 'Andy') {
|
|
288
|
+
const mdFiles = fs
|
|
289
|
+
.readdirSync(groupsDir)
|
|
290
|
+
.map((d) => path.join(groupsDir, d, 'CLAUDE.md'))
|
|
291
|
+
.filter((f) => fs.existsSync(f));
|
|
292
|
+
|
|
293
|
+
for (const mdFile of mdFiles) {
|
|
294
|
+
let content = fs.readFileSync(mdFile, 'utf-8');
|
|
295
|
+
content = content.replace(/^# Andy$/m, `# ${assistantName}`);
|
|
296
|
+
content = content.replace(
|
|
297
|
+
/You are Andy/g,
|
|
298
|
+
`You are ${assistantName}`,
|
|
299
|
+
);
|
|
300
|
+
fs.writeFileSync(mdFile, content);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function readGroupMd(folder: string): string {
|
|
306
|
+
return fs.readFileSync(
|
|
307
|
+
path.join(groupsDir, folder, 'CLAUDE.md'),
|
|
308
|
+
'utf-8',
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
beforeEach(() => {
|
|
313
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nanoclaw-register-test-'));
|
|
314
|
+
groupsDir = path.join(tmpDir, 'groups');
|
|
315
|
+
fs.mkdirSync(path.join(groupsDir, 'main'), { recursive: true });
|
|
316
|
+
fs.mkdirSync(path.join(groupsDir, 'global'), { recursive: true });
|
|
317
|
+
fs.writeFileSync(
|
|
318
|
+
path.join(groupsDir, 'main', 'CLAUDE.md'),
|
|
319
|
+
'# Andy\n\nYou are Andy, a personal assistant.\n\n## Admin Context\n\nThis is the **main channel**.',
|
|
320
|
+
);
|
|
321
|
+
fs.writeFileSync(
|
|
322
|
+
path.join(groupsDir, 'global', 'CLAUDE.md'),
|
|
323
|
+
'# Andy\n\nYou are Andy, a personal assistant.',
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
afterEach(() => {
|
|
328
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('copies global template for non-main group', () => {
|
|
332
|
+
simulateRegister('telegram_dev-team', false);
|
|
333
|
+
|
|
334
|
+
const content = readGroupMd('telegram_dev-team');
|
|
335
|
+
expect(content).toContain('You are Andy');
|
|
336
|
+
expect(content).not.toContain('Admin Context');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('copies main template for main group', () => {
|
|
340
|
+
simulateRegister('whatsapp_main', true);
|
|
341
|
+
|
|
342
|
+
expect(readGroupMd('whatsapp_main')).toContain('Admin Context');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('each channel can have its own main with admin context', () => {
|
|
346
|
+
simulateRegister('whatsapp_main', true);
|
|
347
|
+
simulateRegister('telegram_main', true);
|
|
348
|
+
simulateRegister('slack_main', true);
|
|
349
|
+
simulateRegister('discord_main', true);
|
|
350
|
+
|
|
351
|
+
for (const folder of [
|
|
352
|
+
'whatsapp_main',
|
|
353
|
+
'telegram_main',
|
|
354
|
+
'slack_main',
|
|
355
|
+
'discord_main',
|
|
356
|
+
]) {
|
|
357
|
+
const content = readGroupMd(folder);
|
|
358
|
+
expect(content).toContain('Admin Context');
|
|
359
|
+
expect(content).toContain('You are Andy');
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('non-main groups across channels get global template', () => {
|
|
364
|
+
simulateRegister('whatsapp_main', true);
|
|
365
|
+
simulateRegister('telegram_friends', false);
|
|
366
|
+
simulateRegister('slack_engineering', false);
|
|
367
|
+
simulateRegister('discord_general', false);
|
|
368
|
+
|
|
369
|
+
expect(readGroupMd('whatsapp_main')).toContain('Admin Context');
|
|
370
|
+
for (const folder of [
|
|
371
|
+
'telegram_friends',
|
|
372
|
+
'slack_engineering',
|
|
373
|
+
'discord_general',
|
|
374
|
+
]) {
|
|
375
|
+
const content = readGroupMd(folder);
|
|
376
|
+
expect(content).toContain('You are Andy');
|
|
377
|
+
expect(content).not.toContain('Admin Context');
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('custom name propagates to all channels and groups', () => {
|
|
382
|
+
// Register multiple channels, last one sets custom name
|
|
383
|
+
simulateRegister('whatsapp_main', true);
|
|
384
|
+
simulateRegister('telegram_main', true);
|
|
385
|
+
simulateRegister('slack_devs', false);
|
|
386
|
+
// Final registration triggers name update across all
|
|
387
|
+
simulateRegister('discord_main', true, 'Luna');
|
|
388
|
+
|
|
389
|
+
for (const folder of [
|
|
390
|
+
'main',
|
|
391
|
+
'global',
|
|
392
|
+
'whatsapp_main',
|
|
393
|
+
'telegram_main',
|
|
394
|
+
'slack_devs',
|
|
395
|
+
'discord_main',
|
|
396
|
+
]) {
|
|
397
|
+
const content = readGroupMd(folder);
|
|
398
|
+
expect(content).toContain('# Luna');
|
|
399
|
+
expect(content).toContain('You are Luna');
|
|
400
|
+
expect(content).not.toContain('Andy');
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('never overwrites existing CLAUDE.md on re-registration', () => {
|
|
405
|
+
simulateRegister('slack_main', true);
|
|
406
|
+
// User customizes the file extensively (persona, workspace, rules)
|
|
407
|
+
const mdPath = path.join(groupsDir, 'slack_main', 'CLAUDE.md');
|
|
408
|
+
fs.writeFileSync(
|
|
409
|
+
mdPath,
|
|
410
|
+
'# Gambi\n\nCustom persona with workspace rules and family context.',
|
|
411
|
+
);
|
|
412
|
+
// Re-registering same folder (e.g. re-running /add-slack)
|
|
413
|
+
simulateRegister('slack_main', true);
|
|
414
|
+
|
|
415
|
+
const content = readGroupMd('slack_main');
|
|
416
|
+
expect(content).toContain('Custom persona');
|
|
417
|
+
expect(content).not.toContain('Admin Context');
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('never overwrites when non-main becomes main (isMain changes)', () => {
|
|
421
|
+
// User registers a family group as non-main
|
|
422
|
+
simulateRegister('whatsapp_casa', false);
|
|
423
|
+
// User extensively customizes it (PARA system, task management, etc.)
|
|
424
|
+
const mdPath = path.join(groupsDir, 'whatsapp_casa', 'CLAUDE.md');
|
|
425
|
+
fs.writeFileSync(
|
|
426
|
+
mdPath,
|
|
427
|
+
'# Casa\n\nFamily group with PARA system, task management, shopping lists.',
|
|
428
|
+
);
|
|
429
|
+
// Later, user promotes to main (no trigger required) — CLAUDE.md must be preserved
|
|
430
|
+
simulateRegister('whatsapp_casa', true);
|
|
431
|
+
|
|
432
|
+
const content = readGroupMd('whatsapp_casa');
|
|
433
|
+
expect(content).toContain('PARA system');
|
|
434
|
+
expect(content).not.toContain('Admin Context');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('preserves custom CLAUDE.md across channels when changing main', () => {
|
|
438
|
+
// Real-world scenario: WhatsApp main + customized Discord research channel
|
|
439
|
+
simulateRegister('whatsapp_main', true);
|
|
440
|
+
simulateRegister('discord_main', false);
|
|
441
|
+
const discordPath = path.join(groupsDir, 'discord_main', 'CLAUDE.md');
|
|
442
|
+
fs.writeFileSync(
|
|
443
|
+
discordPath,
|
|
444
|
+
'# Gambi HQ — Research Assistant\n\nResearch workflows for Laura and Ethan.',
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
// Discord becomes main too — custom content must survive
|
|
448
|
+
simulateRegister('discord_main', true);
|
|
449
|
+
expect(readGroupMd('discord_main')).toContain('Research Assistant');
|
|
450
|
+
// WhatsApp main also untouched
|
|
451
|
+
expect(readGroupMd('whatsapp_main')).toContain('Admin Context');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('handles missing templates gracefully', () => {
|
|
455
|
+
fs.unlinkSync(path.join(groupsDir, 'global', 'CLAUDE.md'));
|
|
456
|
+
fs.unlinkSync(path.join(groupsDir, 'main', 'CLAUDE.md'));
|
|
457
|
+
|
|
458
|
+
simulateRegister('discord_general', false);
|
|
459
|
+
|
|
460
|
+
expect(
|
|
461
|
+
fs.existsSync(path.join(groupsDir, 'discord_general', 'CLAUDE.md')),
|
|
462
|
+
).toBe(false);
|
|
463
|
+
});
|
|
464
|
+
});
|
package/setup/register.ts
CHANGED
|
@@ -116,6 +116,30 @@ export async function run(args: string[]): Promise<void> {
|
|
|
116
116
|
recursive: true,
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
// Create CLAUDE.md in the new group folder from template if it doesn't exist.
|
|
120
|
+
// The agent runs with CWD=/workspace/group and loads CLAUDE.md from there.
|
|
121
|
+
// Never overwrite an existing CLAUDE.md — users customize these extensively
|
|
122
|
+
// (persona, workspace structure, communication rules, family context, etc.)
|
|
123
|
+
// and a stock template replacement would destroy that work.
|
|
124
|
+
const groupClaudeMdPath = path.join(
|
|
125
|
+
projectRoot,
|
|
126
|
+
'groups',
|
|
127
|
+
parsed.folder,
|
|
128
|
+
'CLAUDE.md',
|
|
129
|
+
);
|
|
130
|
+
if (!fs.existsSync(groupClaudeMdPath)) {
|
|
131
|
+
const templatePath = parsed.isMain
|
|
132
|
+
? path.join(projectRoot, 'groups', 'main', 'CLAUDE.md')
|
|
133
|
+
: path.join(projectRoot, 'groups', 'global', 'CLAUDE.md');
|
|
134
|
+
if (fs.existsSync(templatePath)) {
|
|
135
|
+
fs.copyFileSync(templatePath, groupClaudeMdPath);
|
|
136
|
+
logger.info(
|
|
137
|
+
{ file: groupClaudeMdPath, template: templatePath },
|
|
138
|
+
'Created CLAUDE.md from template',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
119
143
|
// Update assistant name in CLAUDE.md files if different from default
|
|
120
144
|
let nameUpdated = false;
|
|
121
145
|
if (parsed.assistantName !== 'Andy') {
|
|
@@ -124,10 +148,11 @@ export async function run(args: string[]): Promise<void> {
|
|
|
124
148
|
'Updating assistant name',
|
|
125
149
|
);
|
|
126
150
|
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
const groupsDir = path.join(projectRoot, 'groups');
|
|
152
|
+
const mdFiles = fs
|
|
153
|
+
.readdirSync(groupsDir)
|
|
154
|
+
.map((d) => path.join(groupsDir, d, 'CLAUDE.md'))
|
|
155
|
+
.filter((f) => fs.existsSync(f));
|
|
131
156
|
|
|
132
157
|
for (const mdFile of mdFiles) {
|
|
133
158
|
if (fs.existsSync(mdFile)) {
|