talon-agent 1.0.0 → 1.2.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.
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/package.json +15 -11
- package/prompts/dream.md +7 -3
- package/prompts/heartbeat.md +30 -0
- package/prompts/identity.md +1 -0
- package/prompts/teams.md +3 -0
- package/prompts/telegram.md +1 -0
- package/src/__tests__/chat-settings.test.ts +108 -2
- package/src/__tests__/cleanup-registry.test.ts +58 -0
- package/src/__tests__/config.test.ts +118 -52
- package/src/__tests__/cron-store-extended.test.ts +661 -0
- package/src/__tests__/cron-store.test.ts +145 -11
- package/src/__tests__/daily-log.test.ts +224 -13
- package/src/__tests__/dispatcher.test.ts +424 -23
- package/src/__tests__/dream.test.ts +1028 -0
- package/src/__tests__/errors-extended.test.ts +428 -0
- package/src/__tests__/errors.test.ts +95 -3
- package/src/__tests__/fuzz.test.ts +87 -15
- package/src/__tests__/gateway-actions.test.ts +1174 -433
- package/src/__tests__/gateway-http.test.ts +210 -19
- package/src/__tests__/gateway-retry.test.ts +359 -0
- package/src/__tests__/gateway-withRetry-extended.test.ts +343 -0
- package/src/__tests__/graph.test.ts +830 -0
- package/src/__tests__/handlers-stream.test.ts +208 -0
- package/src/__tests__/handlers.test.ts +2539 -70
- package/src/__tests__/heartbeat.test.ts +364 -0
- package/src/__tests__/history-extended.test.ts +775 -0
- package/src/__tests__/history-persistence.test.ts +74 -19
- package/src/__tests__/history.test.ts +113 -79
- package/src/__tests__/integration.test.ts +43 -8
- package/src/__tests__/log-init.test.ts +129 -0
- package/src/__tests__/log.test.ts +23 -5
- package/src/__tests__/media-index.test.ts +317 -35
- package/src/__tests__/plugin.test.ts +314 -0
- package/src/__tests__/prompt-builder-extended.test.ts +296 -0
- package/src/__tests__/prompt-builder.test.ts +44 -9
- package/src/__tests__/sessions.test.ts +258 -4
- package/src/__tests__/storage-save-errors.test.ts +342 -0
- package/src/__tests__/teams-frontend.test.ts +526 -31
- package/src/__tests__/telegram-formatting.test.ts +82 -0
- package/src/__tests__/terminal-commands.test.ts +208 -1
- package/src/__tests__/terminal-renderer.test.ts +223 -0
- package/src/__tests__/time.test.ts +107 -0
- package/src/__tests__/workspace-migrate.test.ts +256 -0
- package/src/__tests__/workspace.test.ts +63 -1
- package/src/backend/claude-sdk/tools.ts +64 -18
- package/src/bootstrap.ts +14 -14
- package/src/cli.ts +440 -125
- package/src/core/cron.ts +20 -5
- package/src/core/dispatcher.ts +27 -9
- package/src/core/dream.ts +79 -24
- package/src/core/errors.ts +12 -2
- package/src/core/gateway-actions.ts +182 -46
- package/src/core/gateway.ts +93 -41
- package/src/core/heartbeat.ts +515 -0
- package/src/core/plugin.ts +1 -1
- package/src/core/prompt-builder.ts +1 -4
- package/src/core/pulse.ts +4 -3
- package/src/frontend/teams/actions.ts +3 -1
- package/src/frontend/teams/formatting.ts +47 -8
- package/src/frontend/teams/graph.ts +35 -11
- package/src/frontend/teams/index.ts +155 -57
- package/src/frontend/teams/tools.ts +4 -6
- package/src/frontend/telegram/actions.ts +358 -82
- package/src/frontend/telegram/admin.ts +162 -72
- package/src/frontend/telegram/callbacks.ts +16 -10
- package/src/frontend/telegram/commands.ts +37 -21
- package/src/frontend/telegram/formatting.ts +2 -4
- package/src/frontend/telegram/handlers.ts +262 -66
- package/src/frontend/telegram/index.ts +39 -14
- package/src/frontend/telegram/middleware.ts +14 -4
- package/src/frontend/telegram/userbot.ts +16 -4
- package/src/frontend/terminal/renderer.ts +1 -4
- package/src/index.ts +28 -4
- package/src/storage/chat-settings.ts +32 -9
- package/src/storage/cron-store.ts +53 -11
- package/src/storage/daily-log.ts +72 -19
- package/src/storage/history.ts +39 -21
- package/src/storage/media-index.ts +37 -12
- package/src/storage/sessions.ts +3 -2
- package/src/util/cleanup-registry.ts +34 -0
- package/src/util/config.ts +85 -23
- package/src/util/log.ts +47 -17
- package/src/util/paths.ts +10 -0
- package/src/util/time.ts +29 -6
- package/src/util/watchdog.ts +5 -1
- package/src/util/workspace.ts +51 -10
|
@@ -12,15 +12,22 @@ describe("config", () => {
|
|
|
12
12
|
function mockFs(
|
|
13
13
|
configJson: Record<string, unknown> | null,
|
|
14
14
|
promptFiles: Record<string, string> = {},
|
|
15
|
-
workspaceEntries?: {
|
|
15
|
+
workspaceEntries?: {
|
|
16
|
+
name: string;
|
|
17
|
+
isDir: boolean;
|
|
18
|
+
size?: number;
|
|
19
|
+
children?: { name: string; size: number }[];
|
|
20
|
+
}[],
|
|
16
21
|
) {
|
|
17
22
|
vi.doMock("node:fs", () => ({
|
|
18
23
|
existsSync: vi.fn((path: string) => {
|
|
19
|
-
if (path.includes("config.json") || path.includes("talon.json"))
|
|
24
|
+
if (path.includes("config.json") || path.includes("talon.json"))
|
|
25
|
+
return configJson !== null;
|
|
20
26
|
// .talon directory checks (root, data)
|
|
21
27
|
if (path.endsWith(".talon") || path.endsWith("/data")) return true;
|
|
22
28
|
// workspace directory check
|
|
23
|
-
if (path.endsWith("workspace") && workspaceEntries !== undefined)
|
|
29
|
+
if (path.endsWith("workspace") && workspaceEntries !== undefined)
|
|
30
|
+
return true;
|
|
24
31
|
if (typeof path === "string") {
|
|
25
32
|
for (const key of Object.keys(promptFiles)) {
|
|
26
33
|
if (path.includes(key)) return true;
|
|
@@ -29,7 +36,8 @@ describe("config", () => {
|
|
|
29
36
|
return false;
|
|
30
37
|
}),
|
|
31
38
|
readFileSync: vi.fn((path: string) => {
|
|
32
|
-
if (path.includes("config.json") || path.includes("talon.json"))
|
|
39
|
+
if (path.includes("config.json") || path.includes("talon.json"))
|
|
40
|
+
return JSON.stringify(configJson ?? {});
|
|
33
41
|
for (const [key, val] of Object.entries(promptFiles)) {
|
|
34
42
|
if (path.includes(key)) return val;
|
|
35
43
|
}
|
|
@@ -268,7 +276,9 @@ describe("config", () => {
|
|
|
268
276
|
|
|
269
277
|
const { loadConfig } = await import("../util/config.js");
|
|
270
278
|
const config = loadConfig();
|
|
271
|
-
expect(config.systemPrompt).toContain(
|
|
279
|
+
expect(config.systemPrompt).toContain(
|
|
280
|
+
"You are running in terminal mode.",
|
|
281
|
+
);
|
|
272
282
|
});
|
|
273
283
|
|
|
274
284
|
it("loads telegram.md prompt for telegram frontend", async () => {
|
|
@@ -287,13 +297,18 @@ describe("config", () => {
|
|
|
287
297
|
|
|
288
298
|
const { loadConfig } = await import("../util/config.js");
|
|
289
299
|
const config = loadConfig();
|
|
290
|
-
expect(config.systemPrompt).toContain(
|
|
300
|
+
expect(config.systemPrompt).toContain(
|
|
301
|
+
"You are a sharp and helpful AI assistant.",
|
|
302
|
+
);
|
|
291
303
|
});
|
|
292
304
|
|
|
293
305
|
it("custom.md overrides base.md", async () => {
|
|
294
306
|
mockFs(
|
|
295
307
|
{ frontend: "terminal" },
|
|
296
|
-
{
|
|
308
|
+
{
|
|
309
|
+
"custom.md": "Custom prompt override.",
|
|
310
|
+
"base.md": "Default base prompt.",
|
|
311
|
+
},
|
|
297
312
|
);
|
|
298
313
|
|
|
299
314
|
const { loadConfig } = await import("../util/config.js");
|
|
@@ -330,14 +345,10 @@ describe("config", () => {
|
|
|
330
345
|
});
|
|
331
346
|
|
|
332
347
|
it("includes workspace file listing when files exist", async () => {
|
|
333
|
-
mockFs(
|
|
334
|
-
{
|
|
335
|
-
{},
|
|
336
|
-
|
|
337
|
-
{ name: "notes.txt", isDir: false, size: 512 },
|
|
338
|
-
{ name: "data.csv", isDir: false, size: 2048 },
|
|
339
|
-
],
|
|
340
|
-
);
|
|
348
|
+
mockFs({ frontend: "terminal" }, {}, [
|
|
349
|
+
{ name: "notes.txt", isDir: false, size: 512 },
|
|
350
|
+
{ name: "data.csv", isDir: false, size: 2048 },
|
|
351
|
+
]);
|
|
341
352
|
|
|
342
353
|
const { loadConfig } = await import("../util/config.js");
|
|
343
354
|
const config = loadConfig();
|
|
@@ -348,16 +359,12 @@ describe("config", () => {
|
|
|
348
359
|
});
|
|
349
360
|
|
|
350
361
|
it("skips hidden files and node_modules in workspace listing", async () => {
|
|
351
|
-
mockFs(
|
|
352
|
-
{
|
|
353
|
-
{},
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
{ name: "talon.log", isDir: false, size: 500 },
|
|
358
|
-
{ name: "visible.txt", isDir: false, size: 200 },
|
|
359
|
-
],
|
|
360
|
-
);
|
|
362
|
+
mockFs({ frontend: "terminal" }, {}, [
|
|
363
|
+
{ name: ".hidden", isDir: false, size: 100 },
|
|
364
|
+
{ name: "node_modules", isDir: true },
|
|
365
|
+
{ name: "talon.log", isDir: false, size: 500 },
|
|
366
|
+
{ name: "visible.txt", isDir: false, size: 200 },
|
|
367
|
+
]);
|
|
361
368
|
|
|
362
369
|
const { loadConfig } = await import("../util/config.js");
|
|
363
370
|
const config = loadConfig();
|
|
@@ -373,13 +380,9 @@ describe("config", () => {
|
|
|
373
380
|
for (let i = 0; i < 10; i++) {
|
|
374
381
|
manyChildren.push({ name: `file${i}.txt`, size: 100 });
|
|
375
382
|
}
|
|
376
|
-
mockFs(
|
|
377
|
-
{
|
|
378
|
-
|
|
379
|
-
[
|
|
380
|
-
{ name: "bigdir", isDir: true, children: manyChildren },
|
|
381
|
-
],
|
|
382
|
-
);
|
|
383
|
+
mockFs({ frontend: "terminal" }, {}, [
|
|
384
|
+
{ name: "bigdir", isDir: true, children: manyChildren },
|
|
385
|
+
]);
|
|
383
386
|
|
|
384
387
|
const { loadConfig } = await import("../util/config.js");
|
|
385
388
|
const config = loadConfig();
|
|
@@ -387,26 +390,37 @@ describe("config", () => {
|
|
|
387
390
|
});
|
|
388
391
|
|
|
389
392
|
it("lists subdirectory files when 8 or fewer", async () => {
|
|
390
|
-
mockFs(
|
|
391
|
-
{
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
name: "
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
],
|
|
401
|
-
},
|
|
402
|
-
],
|
|
403
|
-
);
|
|
393
|
+
mockFs({ frontend: "terminal" }, {}, [
|
|
394
|
+
{
|
|
395
|
+
name: "smalldir",
|
|
396
|
+
isDir: true,
|
|
397
|
+
children: [
|
|
398
|
+
{ name: "a.txt", size: 50 },
|
|
399
|
+
{ name: "b.txt", size: 75 },
|
|
400
|
+
],
|
|
401
|
+
},
|
|
402
|
+
]);
|
|
404
403
|
|
|
405
404
|
const { loadConfig } = await import("../util/config.js");
|
|
406
405
|
const config = loadConfig();
|
|
407
406
|
expect(config.systemPrompt).toContain("smalldir/a.txt");
|
|
408
407
|
expect(config.systemPrompt).toContain("smalldir/b.txt");
|
|
409
408
|
});
|
|
409
|
+
|
|
410
|
+
it("omits empty subdirectory from listing (line 163 FALSE branch: sub.length=0)", async () => {
|
|
411
|
+
// A directory entry with no children → listDir returns [] → sub.length=0
|
|
412
|
+
// → `else if (sub.length > 8)` is FALSE → omitted from listing
|
|
413
|
+
mockFs({ frontend: "terminal" }, {}, [
|
|
414
|
+
{ name: "emptydir", isDir: true }, // no children → sub.length = 0
|
|
415
|
+
{ name: "notes.txt", isDir: false, size: 100 },
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
const { loadConfig } = await import("../util/config.js");
|
|
419
|
+
const config = loadConfig();
|
|
420
|
+
// The empty subdirectory should NOT appear in listing
|
|
421
|
+
expect(config.systemPrompt).not.toContain("emptydir");
|
|
422
|
+
expect(config.systemPrompt).toContain("notes.txt");
|
|
423
|
+
});
|
|
410
424
|
});
|
|
411
425
|
|
|
412
426
|
describe("getFrontends", () => {
|
|
@@ -433,7 +447,8 @@ describe("config", () => {
|
|
|
433
447
|
it("does nothing when pluginAdditions is empty", async () => {
|
|
434
448
|
mockFs({ frontend: "terminal" });
|
|
435
449
|
|
|
436
|
-
const { loadConfig, rebuildSystemPrompt } =
|
|
450
|
+
const { loadConfig, rebuildSystemPrompt } =
|
|
451
|
+
await import("../util/config.js");
|
|
437
452
|
const config = loadConfig();
|
|
438
453
|
const originalPrompt = config.systemPrompt;
|
|
439
454
|
rebuildSystemPrompt(config, []);
|
|
@@ -443,7 +458,8 @@ describe("config", () => {
|
|
|
443
458
|
it("appends plugin prompt additions to system prompt", async () => {
|
|
444
459
|
mockFs({ frontend: "terminal" });
|
|
445
460
|
|
|
446
|
-
const { loadConfig, rebuildSystemPrompt } =
|
|
461
|
+
const { loadConfig, rebuildSystemPrompt } =
|
|
462
|
+
await import("../util/config.js");
|
|
447
463
|
const config = loadConfig();
|
|
448
464
|
rebuildSystemPrompt(config, [
|
|
449
465
|
"## Plugin A\nPlugin A instructions.",
|
|
@@ -459,7 +475,8 @@ describe("config", () => {
|
|
|
459
475
|
{ "terminal.md": "Terminal-specific prompt." },
|
|
460
476
|
);
|
|
461
477
|
|
|
462
|
-
const { loadConfig, rebuildSystemPrompt } =
|
|
478
|
+
const { loadConfig, rebuildSystemPrompt } =
|
|
479
|
+
await import("../util/config.js");
|
|
463
480
|
const config = loadConfig();
|
|
464
481
|
rebuildSystemPrompt(config, ["## Test Plugin\nTest addition."]);
|
|
465
482
|
// Should use terminal (first in array) as the active frontend
|
|
@@ -473,12 +490,25 @@ describe("config", () => {
|
|
|
473
490
|
{ "terminal.md": "Terminal mode active." },
|
|
474
491
|
);
|
|
475
492
|
|
|
476
|
-
const { loadConfig, rebuildSystemPrompt } =
|
|
493
|
+
const { loadConfig, rebuildSystemPrompt } =
|
|
494
|
+
await import("../util/config.js");
|
|
477
495
|
const config = loadConfig();
|
|
478
496
|
rebuildSystemPrompt(config, ["## My Plugin\nDo special things."]);
|
|
479
497
|
expect(config.systemPrompt).toContain("Terminal mode active.");
|
|
480
498
|
expect(config.systemPrompt).toContain("Do special things.");
|
|
481
499
|
});
|
|
500
|
+
|
|
501
|
+
it("uses telegram as default frontend when config.frontend is undefined", async () => {
|
|
502
|
+
mockFs({ frontend: "terminal" });
|
|
503
|
+
|
|
504
|
+
const { loadConfig, rebuildSystemPrompt } =
|
|
505
|
+
await import("../util/config.js");
|
|
506
|
+
const config = loadConfig();
|
|
507
|
+
// Force frontend to undefined to trigger the ?? "telegram" fallback on line 132
|
|
508
|
+
(config as Record<string, unknown>).frontend = undefined;
|
|
509
|
+
// Should not throw — uses telegram as default frontend file
|
|
510
|
+
expect(() => rebuildSystemPrompt(config, [])).not.toThrow();
|
|
511
|
+
});
|
|
482
512
|
});
|
|
483
513
|
|
|
484
514
|
describe("zod validation boundaries", () => {
|
|
@@ -525,12 +555,14 @@ describe("config", () => {
|
|
|
525
555
|
// Simulate a corrupt JSON by having readFileSync throw
|
|
526
556
|
vi.doMock("node:fs", () => ({
|
|
527
557
|
existsSync: vi.fn((path: string) => {
|
|
528
|
-
if (path.includes("config.json") || path.includes("talon.json"))
|
|
558
|
+
if (path.includes("config.json") || path.includes("talon.json"))
|
|
559
|
+
return true;
|
|
529
560
|
if (path.endsWith(".talon") || path.endsWith("/data")) return true;
|
|
530
561
|
return false;
|
|
531
562
|
}),
|
|
532
563
|
readFileSync: vi.fn((path: string) => {
|
|
533
|
-
if (path.includes("config.json") || path.includes("talon.json"))
|
|
564
|
+
if (path.includes("config.json") || path.includes("talon.json"))
|
|
565
|
+
throw new Error("corrupt file");
|
|
534
566
|
return "";
|
|
535
567
|
}),
|
|
536
568
|
mkdirSync: vi.fn(),
|
|
@@ -544,3 +576,37 @@ describe("config", () => {
|
|
|
544
576
|
});
|
|
545
577
|
});
|
|
546
578
|
});
|
|
579
|
+
|
|
580
|
+
describe("loadConfig — teams webhook validation", () => {
|
|
581
|
+
beforeEach(() => {
|
|
582
|
+
vi.resetModules();
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("throws when teams frontend has no teamsWebhookUrl", async () => {
|
|
586
|
+
vi.doMock("../util/log.js", () => ({
|
|
587
|
+
log: vi.fn(),
|
|
588
|
+
logError: vi.fn(),
|
|
589
|
+
logWarn: vi.fn(),
|
|
590
|
+
logDebug: vi.fn(),
|
|
591
|
+
}));
|
|
592
|
+
vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
|
|
593
|
+
vi.doMock("node:fs", () => ({
|
|
594
|
+
existsSync: vi.fn((path: string) => {
|
|
595
|
+
if (path.includes("config.json")) return true;
|
|
596
|
+
return false;
|
|
597
|
+
}),
|
|
598
|
+
readFileSync: vi.fn(() =>
|
|
599
|
+
JSON.stringify({
|
|
600
|
+
frontend: "teams",
|
|
601
|
+
// teamsWebhookUrl intentionally omitted
|
|
602
|
+
}),
|
|
603
|
+
),
|
|
604
|
+
mkdirSync: vi.fn(),
|
|
605
|
+
readdirSync: vi.fn(() => []),
|
|
606
|
+
statSync: vi.fn(() => ({ size: 0 })),
|
|
607
|
+
}));
|
|
608
|
+
|
|
609
|
+
const { loadConfig } = await import("../util/config.js");
|
|
610
|
+
expect(() => loadConfig()).toThrow("teamsWebhookUrl");
|
|
611
|
+
});
|
|
612
|
+
});
|