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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for workspace migrateLayout, identity seeding, and prompt seeding.
|
|
3
|
+
* Uses temp directories and mocks process.cwd() + os.homedir().
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
mkdirSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
existsSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
|
|
17
|
+
const TEST_ROOT = join(tmpdir(), `talon-migrate-test-${Date.now()}`);
|
|
18
|
+
const OLD_WORKSPACE = join(TEST_ROOT, "workspace");
|
|
19
|
+
const NEW_ROOT = join(TEST_ROOT, ".talon");
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.resetModules();
|
|
23
|
+
if (existsSync(TEST_ROOT)) rmSync(TEST_ROOT, { recursive: true });
|
|
24
|
+
mkdirSync(TEST_ROOT, { recursive: true });
|
|
25
|
+
|
|
26
|
+
vi.doMock("node:os", async (importOriginal) => {
|
|
27
|
+
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
28
|
+
return { ...actual, homedir: () => TEST_ROOT };
|
|
29
|
+
});
|
|
30
|
+
vi.doMock("../util/log.js", () => ({
|
|
31
|
+
log: vi.fn(),
|
|
32
|
+
logError: vi.fn(),
|
|
33
|
+
logWarn: vi.fn(),
|
|
34
|
+
logDebug: vi.fn(),
|
|
35
|
+
}));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
if (existsSync(TEST_ROOT)) rmSync(TEST_ROOT, { recursive: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("migrateLayout", () => {
|
|
43
|
+
it("is a no-op when workspace/ does not exist", async () => {
|
|
44
|
+
const { migrateLayout } = await import("../util/workspace.js");
|
|
45
|
+
expect(() => migrateLayout()).not.toThrow();
|
|
46
|
+
// workspace/sessions.json should NOT have been created since migration never ran
|
|
47
|
+
expect(existsSync(join(NEW_ROOT, "data", "sessions.json"))).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("is a no-op when .talon/ already exists", async () => {
|
|
51
|
+
mkdirSync(OLD_WORKSPACE, { recursive: true });
|
|
52
|
+
mkdirSync(NEW_ROOT, { recursive: true });
|
|
53
|
+
|
|
54
|
+
const { migrateLayout } = await import("../util/workspace.js");
|
|
55
|
+
expect(() => migrateLayout()).not.toThrow();
|
|
56
|
+
// workspace/ should still exist — migration was skipped
|
|
57
|
+
expect(existsSync(OLD_WORKSPACE)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("migrates files from workspace/ to .talon/ layout", async () => {
|
|
61
|
+
mkdirSync(OLD_WORKSPACE, { recursive: true });
|
|
62
|
+
writeFileSync(join(OLD_WORKSPACE, "sessions.json"), '{"chat1":{}}');
|
|
63
|
+
writeFileSync(join(OLD_WORKSPACE, "history.json"), "{}");
|
|
64
|
+
writeFileSync(join(OLD_WORKSPACE, "talon.json"), '{"frontend":"telegram"}');
|
|
65
|
+
|
|
66
|
+
const originalCwd = process.cwd;
|
|
67
|
+
process.cwd = () => TEST_ROOT;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const { migrateLayout } = await import("../util/workspace.js");
|
|
71
|
+
migrateLayout();
|
|
72
|
+
|
|
73
|
+
const dataDir = join(NEW_ROOT, "data");
|
|
74
|
+
expect(existsSync(join(dataDir, "sessions.json"))).toBe(true);
|
|
75
|
+
expect(existsSync(join(dataDir, "history.json"))).toBe(true);
|
|
76
|
+
expect(existsSync(join(NEW_ROOT, "config.json"))).toBe(true);
|
|
77
|
+
// Original files should be gone
|
|
78
|
+
expect(existsSync(join(OLD_WORKSPACE, "sessions.json"))).toBe(false);
|
|
79
|
+
} finally {
|
|
80
|
+
process.cwd = originalCwd;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("migrates directories from workspace/ to .talon/workspace/ layout", async () => {
|
|
85
|
+
mkdirSync(OLD_WORKSPACE, { recursive: true });
|
|
86
|
+
const memoryDir = join(OLD_WORKSPACE, "memory");
|
|
87
|
+
mkdirSync(memoryDir);
|
|
88
|
+
writeFileSync(join(memoryDir, "notes.md"), "# Memory");
|
|
89
|
+
|
|
90
|
+
const originalCwd = process.cwd;
|
|
91
|
+
process.cwd = () => TEST_ROOT;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const { migrateLayout } = await import("../util/workspace.js");
|
|
95
|
+
migrateLayout();
|
|
96
|
+
|
|
97
|
+
const newMemory = join(NEW_ROOT, "workspace", "memory");
|
|
98
|
+
expect(existsSync(newMemory)).toBe(true);
|
|
99
|
+
expect(existsSync(join(newMemory, "notes.md"))).toBe(true);
|
|
100
|
+
} finally {
|
|
101
|
+
process.cwd = originalCwd;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("removes empty workspace/ after migration", async () => {
|
|
106
|
+
mkdirSync(OLD_WORKSPACE, { recursive: true });
|
|
107
|
+
// No files — workspace/ is empty
|
|
108
|
+
|
|
109
|
+
const originalCwd = process.cwd;
|
|
110
|
+
process.cwd = () => TEST_ROOT;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const { migrateLayout } = await import("../util/workspace.js");
|
|
114
|
+
migrateLayout();
|
|
115
|
+
|
|
116
|
+
// Empty workspace/ should be removed
|
|
117
|
+
expect(existsSync(OLD_WORKSPACE)).toBe(false);
|
|
118
|
+
} finally {
|
|
119
|
+
process.cwd = originalCwd;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("falls back to copy+delete when renameSync throws (cross-filesystem simulation)", async () => {
|
|
124
|
+
mkdirSync(OLD_WORKSPACE, { recursive: true });
|
|
125
|
+
writeFileSync(join(OLD_WORKSPACE, "sessions.json"), '{"chat1":{}}');
|
|
126
|
+
const memDir = join(OLD_WORKSPACE, "memory");
|
|
127
|
+
mkdirSync(memDir);
|
|
128
|
+
writeFileSync(join(memDir, "notes.md"), "# notes");
|
|
129
|
+
|
|
130
|
+
const originalCwd = process.cwd;
|
|
131
|
+
process.cwd = () => TEST_ROOT;
|
|
132
|
+
|
|
133
|
+
// Override renameSync to simulate cross-device link error
|
|
134
|
+
vi.doMock("node:fs", async (importOriginal) => {
|
|
135
|
+
const actual = await importOriginal<typeof import("node:fs")>();
|
|
136
|
+
return {
|
|
137
|
+
...actual,
|
|
138
|
+
renameSync: vi.fn(() => {
|
|
139
|
+
throw Object.assign(
|
|
140
|
+
new Error("EXDEV: cross-device link not permitted"),
|
|
141
|
+
{ code: "EXDEV" },
|
|
142
|
+
);
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const { migrateLayout } = await import("../util/workspace.js");
|
|
149
|
+
migrateLayout();
|
|
150
|
+
|
|
151
|
+
// File was copied via copyFileSync fallback (line 57)
|
|
152
|
+
expect(existsSync(join(NEW_ROOT, "data", "sessions.json"))).toBe(true);
|
|
153
|
+
// Directory was copied via cpSync fallback (line 81)
|
|
154
|
+
expect(
|
|
155
|
+
existsSync(join(NEW_ROOT, "workspace", "memory", "notes.md")),
|
|
156
|
+
).toBe(true);
|
|
157
|
+
} finally {
|
|
158
|
+
process.cwd = originalCwd;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("leaves workspace/ when non-migration files remain after migration", async () => {
|
|
163
|
+
mkdirSync(OLD_WORKSPACE, { recursive: true });
|
|
164
|
+
// A file that is NOT in the migration list
|
|
165
|
+
writeFileSync(join(OLD_WORKSPACE, "unknown-extra-file.txt"), "extra");
|
|
166
|
+
|
|
167
|
+
const originalCwd = process.cwd;
|
|
168
|
+
process.cwd = () => TEST_ROOT;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const { migrateLayout } = await import("../util/workspace.js");
|
|
172
|
+
migrateLayout();
|
|
173
|
+
|
|
174
|
+
// workspace/ should still exist since it's not empty
|
|
175
|
+
expect(existsSync(OLD_WORKSPACE)).toBe(true);
|
|
176
|
+
expect(existsSync(join(OLD_WORKSPACE, "unknown-extra-file.txt"))).toBe(
|
|
177
|
+
true,
|
|
178
|
+
);
|
|
179
|
+
} finally {
|
|
180
|
+
process.cwd = originalCwd;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("initWorkspace — identity and prompt seeding", () => {
|
|
186
|
+
it("creates identity.md when it does not exist", async () => {
|
|
187
|
+
const originalCwd = process.cwd;
|
|
188
|
+
process.cwd = () => TEST_ROOT;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const { initWorkspace } = await import("../util/workspace.js");
|
|
192
|
+
initWorkspace(join(TEST_ROOT, "ws"));
|
|
193
|
+
|
|
194
|
+
// identity.md is at ~/.talon/workspace/identity.md
|
|
195
|
+
const identityFile = join(NEW_ROOT, "workspace", "identity.md");
|
|
196
|
+
expect(existsSync(identityFile)).toBe(true);
|
|
197
|
+
const content = readFileSync(identityFile, "utf-8");
|
|
198
|
+
expect(content).toContain("Identity");
|
|
199
|
+
} finally {
|
|
200
|
+
process.cwd = originalCwd;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("seeds .md prompt files from prompts/ directory", async () => {
|
|
205
|
+
// prompts/ is resolved relative to process.cwd()
|
|
206
|
+
const promptsDir = join(TEST_ROOT, "prompts");
|
|
207
|
+
mkdirSync(promptsDir, { recursive: true });
|
|
208
|
+
writeFileSync(join(promptsDir, "system.md"), "# System Prompt");
|
|
209
|
+
writeFileSync(join(promptsDir, "dream.md"), "# Dream Prompt");
|
|
210
|
+
writeFileSync(join(promptsDir, "not-a-prompt.txt"), "ignored");
|
|
211
|
+
|
|
212
|
+
const originalCwd = process.cwd;
|
|
213
|
+
process.cwd = () => TEST_ROOT;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const { initWorkspace } = await import("../util/workspace.js");
|
|
217
|
+
initWorkspace(join(TEST_ROOT, "ws"));
|
|
218
|
+
|
|
219
|
+
// prompts are seeded to ~/.talon/prompts/
|
|
220
|
+
const talonPromptsDir = join(NEW_ROOT, "prompts");
|
|
221
|
+
expect(existsSync(join(talonPromptsDir, "system.md"))).toBe(true);
|
|
222
|
+
expect(existsSync(join(talonPromptsDir, "dream.md"))).toBe(true);
|
|
223
|
+
// .txt file should NOT be copied
|
|
224
|
+
expect(existsSync(join(talonPromptsDir, "not-a-prompt.txt"))).toBe(false);
|
|
225
|
+
} finally {
|
|
226
|
+
process.cwd = originalCwd;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("does not overwrite existing prompt files", async () => {
|
|
231
|
+
const promptsDir = join(TEST_ROOT, "prompts");
|
|
232
|
+
mkdirSync(promptsDir, { recursive: true });
|
|
233
|
+
writeFileSync(join(promptsDir, "custom.md"), "# Package version");
|
|
234
|
+
|
|
235
|
+
const talonPromptsDir = join(NEW_ROOT, "prompts");
|
|
236
|
+
mkdirSync(talonPromptsDir, { recursive: true });
|
|
237
|
+
writeFileSync(
|
|
238
|
+
join(talonPromptsDir, "custom.md"),
|
|
239
|
+
"# User customized version",
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const originalCwd = process.cwd;
|
|
243
|
+
process.cwd = () => TEST_ROOT;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const { initWorkspace } = await import("../util/workspace.js");
|
|
247
|
+
initWorkspace(join(TEST_ROOT, "ws"));
|
|
248
|
+
|
|
249
|
+
// User version should be preserved
|
|
250
|
+
const content = readFileSync(join(talonPromptsDir, "custom.md"), "utf-8");
|
|
251
|
+
expect(content).toBe("# User customized version");
|
|
252
|
+
} finally {
|
|
253
|
+
process.cwd = originalCwd;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
mkdirSync,
|
|
4
|
+
writeFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
} from "node:fs";
|
|
3
11
|
import { join } from "node:path";
|
|
4
12
|
import { tmpdir } from "node:os";
|
|
5
13
|
|
|
@@ -17,6 +25,7 @@ import {
|
|
|
17
25
|
cleanupUploads,
|
|
18
26
|
startUploadCleanup,
|
|
19
27
|
stopUploadCleanup,
|
|
28
|
+
migrateLayout,
|
|
20
29
|
} from "../util/workspace.js";
|
|
21
30
|
|
|
22
31
|
const TEST_ROOT = join(tmpdir(), `talon-ws-test-${Date.now()}`);
|
|
@@ -182,3 +191,56 @@ describe("startUploadCleanup / stopUploadCleanup", () => {
|
|
|
182
191
|
expect(() => stopUploadCleanup()).not.toThrow();
|
|
183
192
|
});
|
|
184
193
|
});
|
|
194
|
+
|
|
195
|
+
describe("migrateLayout", () => {
|
|
196
|
+
it("is a no-op when workspace/ directory does not exist", () => {
|
|
197
|
+
// No workspace/ dir → should not throw or create anything
|
|
198
|
+
expect(() => migrateLayout()).not.toThrow();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("is a no-op when .talon/ already exists", () => {
|
|
202
|
+
// Even if workspace/ exists, skip migration if .talon/ already there
|
|
203
|
+
expect(() => migrateLayout()).not.toThrow();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("getWorkspaceDiskUsage — edge cases", () => {
|
|
208
|
+
it("returns 0 for non-existent directory", () => {
|
|
209
|
+
expect(getWorkspaceDiskUsage("/non/existent/path/xyz123")).toBe(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("counts multiple files correctly", () => {
|
|
213
|
+
mkdirSync(TEST_ROOT, { recursive: true });
|
|
214
|
+
writeFileSync(join(TEST_ROOT, "a.txt"), "12345"); // 5 bytes
|
|
215
|
+
writeFileSync(join(TEST_ROOT, "b.txt"), "123"); // 3 bytes
|
|
216
|
+
const usage = getWorkspaceDiskUsage(TEST_ROOT);
|
|
217
|
+
expect(usage).toBe(8);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("skips symlinks — entry.isFile() FALSE branch (L147)", () => {
|
|
221
|
+
mkdirSync(TEST_ROOT, { recursive: true });
|
|
222
|
+
writeFileSync(join(TEST_ROOT, "real.txt"), "hello"); // 5 bytes
|
|
223
|
+
// symlink: isDirectory()=false, isFile()=false → skipped by walk
|
|
224
|
+
symlinkSync(join(TEST_ROOT, "real.txt"), join(TEST_ROOT, "link.txt"));
|
|
225
|
+
const usage = getWorkspaceDiskUsage(TEST_ROOT);
|
|
226
|
+
// Only real.txt counts (5 bytes); symlink is not counted
|
|
227
|
+
expect(usage).toBe(5);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("startUploadCleanup — setInterval callback (function coverage)", () => {
|
|
232
|
+
it("fires periodic cleanup via setInterval arrow function", () => {
|
|
233
|
+
vi.useFakeTimers();
|
|
234
|
+
mkdirSync(TEST_ROOT, { recursive: true });
|
|
235
|
+
mkdirSync(join(TEST_ROOT, "uploads"), { recursive: true });
|
|
236
|
+
writeFileSync(join(TEST_ROOT, "uploads", "file.jpg"), "data");
|
|
237
|
+
|
|
238
|
+
startUploadCleanup(TEST_ROOT);
|
|
239
|
+
// Advance past CLEANUP_INTERVAL_MS (1 hour) to fire the setInterval callback
|
|
240
|
+
vi.advanceTimersByTime(60 * 60 * 1000 + 1);
|
|
241
|
+
stopUploadCleanup();
|
|
242
|
+
|
|
243
|
+
vi.useRealTimers();
|
|
244
|
+
// Just verify no error was thrown — the arrow function was exercised
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -113,7 +113,10 @@ Examples:
|
|
|
113
113
|
first_name: z.string().optional().describe("Contact first name"),
|
|
114
114
|
last_name: z.string().optional().describe("Contact last name"),
|
|
115
115
|
title: z.string().optional().describe("Audio title (for type=audio)"),
|
|
116
|
-
performer: z
|
|
116
|
+
performer: z
|
|
117
|
+
.string()
|
|
118
|
+
.optional()
|
|
119
|
+
.describe("Audio performer/artist (for type=audio)"),
|
|
117
120
|
emoji: z.string().optional().describe("Dice emoji (🎲🎯🏀⚽🎳🎰)"),
|
|
118
121
|
delay_seconds: z
|
|
119
122
|
.number()
|
|
@@ -370,7 +373,9 @@ server.tool(
|
|
|
370
373
|
"download_media",
|
|
371
374
|
"Download a photo, document, or other media from a message by its ID. Saves the file to the workspace and returns the file path so you can read/analyze it. Use this when you see a [photo] or [document] in chat history but don't have the file.",
|
|
372
375
|
{
|
|
373
|
-
message_id: z
|
|
376
|
+
message_id: z
|
|
377
|
+
.number()
|
|
378
|
+
.describe("Message ID containing the media to download"),
|
|
374
379
|
},
|
|
375
380
|
async (params) => textResult(await callBridge("download_media", params)),
|
|
376
381
|
);
|
|
@@ -379,7 +384,11 @@ server.tool(
|
|
|
379
384
|
"get_sticker_pack",
|
|
380
385
|
"Get all stickers in a sticker pack by its name. Returns emoji + file_id for each sticker so you can send them. Use when you see a sticker set name in chat history.",
|
|
381
386
|
{
|
|
382
|
-
set_name: z
|
|
387
|
+
set_name: z
|
|
388
|
+
.string()
|
|
389
|
+
.describe(
|
|
390
|
+
"Sticker set name (e.g. 'AnimatedEmojies' or from sticker metadata)",
|
|
391
|
+
),
|
|
383
392
|
},
|
|
384
393
|
async (params) => textResult(await callBridge("get_sticker_pack", params)),
|
|
385
394
|
);
|
|
@@ -388,7 +397,9 @@ server.tool(
|
|
|
388
397
|
"download_sticker",
|
|
389
398
|
"Download a sticker image to workspace so you can view its contents. Returns the file path.",
|
|
390
399
|
{
|
|
391
|
-
file_id: z
|
|
400
|
+
file_id: z
|
|
401
|
+
.string()
|
|
402
|
+
.describe("Sticker file_id from chat history or sticker pack listing"),
|
|
392
403
|
},
|
|
393
404
|
async (params) => textResult(await callBridge("download_sticker", params)),
|
|
394
405
|
);
|
|
@@ -437,10 +448,19 @@ Type "message" sends the content as a text message.
|
|
|
437
448
|
Type "query" runs the content as a Claude prompt with full tool access (can search, create files, send messages, etc).`,
|
|
438
449
|
{
|
|
439
450
|
name: z.string().describe("Human-readable name for the job"),
|
|
440
|
-
schedule: z
|
|
441
|
-
|
|
451
|
+
schedule: z
|
|
452
|
+
.string()
|
|
453
|
+
.describe("Cron expression (5-field: minute hour day month weekday)"),
|
|
454
|
+
type: z
|
|
455
|
+
.enum(["message", "query"])
|
|
456
|
+
.describe("Job type: 'message' sends text, 'query' runs a Claude prompt"),
|
|
442
457
|
content: z.string().describe("Message text or query prompt"),
|
|
443
|
-
timezone: z
|
|
458
|
+
timezone: z
|
|
459
|
+
.string()
|
|
460
|
+
.optional()
|
|
461
|
+
.describe(
|
|
462
|
+
"IANA timezone (e.g. 'America/New_York'). Defaults to system timezone.",
|
|
463
|
+
),
|
|
444
464
|
},
|
|
445
465
|
async (params) => textResult(await callBridge("create_cron_job", params)),
|
|
446
466
|
);
|
|
@@ -496,11 +516,23 @@ The set name will automatically get "_by_<botname>" appended if needed.
|
|
|
496
516
|
Example: create_sticker_set(user_id=123, name="cool_pack", title="Cool Stickers", file_path="/path/to/sticker.png", emoji_list=["😎"])`,
|
|
497
517
|
{
|
|
498
518
|
user_id: z.number().describe("Telegram user ID who will own the pack"),
|
|
499
|
-
name: z
|
|
519
|
+
name: z
|
|
520
|
+
.string()
|
|
521
|
+
.describe(
|
|
522
|
+
"Short name for the pack (a-z, 0-9, underscores). Will get _by_<botname> appended.",
|
|
523
|
+
),
|
|
500
524
|
title: z.string().describe("Display title for the pack (1-64 chars)"),
|
|
501
|
-
file_path: z
|
|
502
|
-
|
|
503
|
-
|
|
525
|
+
file_path: z
|
|
526
|
+
.string()
|
|
527
|
+
.describe("Path to the sticker image file (PNG/WEBP, 512x512 max)"),
|
|
528
|
+
emoji_list: z
|
|
529
|
+
.array(z.string())
|
|
530
|
+
.optional()
|
|
531
|
+
.describe("Emojis for this sticker (default: ['🎨'])"),
|
|
532
|
+
format: z
|
|
533
|
+
.enum(["static", "animated", "video"])
|
|
534
|
+
.optional()
|
|
535
|
+
.describe("Sticker format (default: static)"),
|
|
504
536
|
},
|
|
505
537
|
async (params) => textResult(await callBridge("create_sticker_set", params)),
|
|
506
538
|
);
|
|
@@ -512,8 +544,14 @@ server.tool(
|
|
|
512
544
|
user_id: z.number().describe("Telegram user ID who owns the pack"),
|
|
513
545
|
name: z.string().describe("Sticker set name (including _by_<botname>)"),
|
|
514
546
|
file_path: z.string().describe("Path to the sticker image file"),
|
|
515
|
-
emoji_list: z
|
|
516
|
-
|
|
547
|
+
emoji_list: z
|
|
548
|
+
.array(z.string())
|
|
549
|
+
.optional()
|
|
550
|
+
.describe("Emojis for this sticker (default: ['🎨'])"),
|
|
551
|
+
format: z
|
|
552
|
+
.enum(["static", "animated", "video"])
|
|
553
|
+
.optional()
|
|
554
|
+
.describe("Sticker format (default: static)"),
|
|
517
555
|
},
|
|
518
556
|
async (params) => textResult(await callBridge("add_sticker_to_set", params)),
|
|
519
557
|
);
|
|
@@ -522,9 +560,12 @@ server.tool(
|
|
|
522
560
|
"delete_sticker_from_set",
|
|
523
561
|
"Remove a specific sticker from a pack by its file_id.",
|
|
524
562
|
{
|
|
525
|
-
sticker_file_id: z
|
|
563
|
+
sticker_file_id: z
|
|
564
|
+
.string()
|
|
565
|
+
.describe("file_id of the sticker to remove (get from get_sticker_pack)"),
|
|
526
566
|
},
|
|
527
|
-
async (params) =>
|
|
567
|
+
async (params) =>
|
|
568
|
+
textResult(await callBridge("delete_sticker_from_set", params)),
|
|
528
569
|
);
|
|
529
570
|
|
|
530
571
|
server.tool(
|
|
@@ -534,7 +575,8 @@ server.tool(
|
|
|
534
575
|
name: z.string().describe("Sticker set name"),
|
|
535
576
|
title: z.string().describe("New title (1-64 chars)"),
|
|
536
577
|
},
|
|
537
|
-
async (params) =>
|
|
578
|
+
async (params) =>
|
|
579
|
+
textResult(await callBridge("set_sticker_set_title", params)),
|
|
538
580
|
);
|
|
539
581
|
|
|
540
582
|
server.tool(
|
|
@@ -566,9 +608,13 @@ server.tool(
|
|
|
566
608
|
"list_media",
|
|
567
609
|
"List recent photos, documents, and other media in the current chat with file paths. Use this to find a previously sent photo or file to re-read or reference.",
|
|
568
610
|
{
|
|
569
|
-
limit: z
|
|
611
|
+
limit: z
|
|
612
|
+
.number()
|
|
613
|
+
.optional()
|
|
614
|
+
.describe("Number of entries (default 10, max 20)"),
|
|
570
615
|
},
|
|
571
|
-
async (params) =>
|
|
616
|
+
async (params) =>
|
|
617
|
+
textResult(await callBridge("list_media", { limit: params.limit })),
|
|
572
618
|
);
|
|
573
619
|
|
|
574
620
|
// ── Web ─────────────────────────────────────────────────────────────────────
|
package/src/bootstrap.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { initDispatcher } from "./core/dispatcher.js";
|
|
|
21
21
|
import { initPulse, resetPulseTimer } from "./core/pulse.js";
|
|
22
22
|
import { initCron } from "./core/cron.js";
|
|
23
23
|
import { initDream } from "./core/dream.js";
|
|
24
|
+
import { initHeartbeat } from "./core/heartbeat.js";
|
|
24
25
|
import { log } from "./util/log.js";
|
|
25
26
|
import type { TalonConfig } from "./util/config.js";
|
|
26
27
|
import type { QueryBackend, ContextManager } from "./core/types.js";
|
|
@@ -63,14 +64,11 @@ export async function bootstrap(
|
|
|
63
64
|
|
|
64
65
|
// Load plugins (external tool packages)
|
|
65
66
|
if (config.plugins.length > 0) {
|
|
66
|
-
const { loadPlugins, getPluginPromptAdditions } =
|
|
67
|
-
"./core/plugin.js"
|
|
68
|
-
);
|
|
67
|
+
const { loadPlugins, getPluginPromptAdditions } =
|
|
68
|
+
await import("./core/plugin.js");
|
|
69
69
|
const frontends =
|
|
70
70
|
options.frontendNames ??
|
|
71
|
-
(Array.isArray(config.frontend)
|
|
72
|
-
? config.frontend
|
|
73
|
-
: [config.frontend]);
|
|
71
|
+
(Array.isArray(config.frontend) ? config.frontend : [config.frontend]);
|
|
74
72
|
await loadPlugins(config.plugins, frontends);
|
|
75
73
|
rebuildSystemPrompt(config, getPluginPromptAdditions());
|
|
76
74
|
}
|
|
@@ -99,18 +97,14 @@ export async function initBackendAndDispatcher(
|
|
|
99
97
|
let backend: QueryBackend;
|
|
100
98
|
|
|
101
99
|
if (config.backend === "opencode") {
|
|
102
|
-
const {
|
|
103
|
-
|
|
104
|
-
handleMessage: opencodeHandleMessage,
|
|
105
|
-
} = await import("./backend/opencode/index.js");
|
|
100
|
+
const { initOpenCodeAgent, handleMessage: opencodeHandleMessage } =
|
|
101
|
+
await import("./backend/opencode/index.js");
|
|
106
102
|
initOpenCodeAgent(config, frontend.getBridgePort);
|
|
107
103
|
backend = { query: (params) => opencodeHandleMessage(params) };
|
|
108
104
|
log("bot", "Backend: OpenCode");
|
|
109
105
|
} else {
|
|
110
|
-
const {
|
|
111
|
-
|
|
112
|
-
handleMessage: claudeHandleMessage,
|
|
113
|
-
} = await import("./backend/claude-sdk/index.js");
|
|
106
|
+
const { initAgent: initClaudeAgent, handleMessage: claudeHandleMessage } =
|
|
107
|
+
await import("./backend/claude-sdk/index.js");
|
|
114
108
|
initClaudeAgent(config, frontend.getBridgePort);
|
|
115
109
|
backend = { query: (params) => claudeHandleMessage(params) };
|
|
116
110
|
log("bot", "Backend: Claude SDK");
|
|
@@ -131,4 +125,10 @@ export async function initBackendAndDispatcher(
|
|
|
131
125
|
claudeBinary: config.claudeBinary,
|
|
132
126
|
workspace: config.workspace,
|
|
133
127
|
});
|
|
128
|
+
initHeartbeat({
|
|
129
|
+
model: config.model,
|
|
130
|
+
heartbeatModel: config.heartbeatModel,
|
|
131
|
+
claudeBinary: config.claudeBinary,
|
|
132
|
+
workspace: config.workspace,
|
|
133
|
+
});
|
|
134
134
|
}
|