talon-agent 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 (89) hide show
  1. package/README.md +137 -0
  2. package/bin/talon.js +5 -0
  3. package/package.json +86 -0
  4. package/prompts/base.md +13 -0
  5. package/prompts/custom.md.example +22 -0
  6. package/prompts/dream.md +41 -0
  7. package/prompts/identity.md +45 -0
  8. package/prompts/teams.md +52 -0
  9. package/prompts/telegram.md +89 -0
  10. package/prompts/terminal.md +13 -0
  11. package/src/__tests__/chat-id.test.ts +91 -0
  12. package/src/__tests__/chat-settings.test.ts +337 -0
  13. package/src/__tests__/config.test.ts +546 -0
  14. package/src/__tests__/cron-store.test.ts +440 -0
  15. package/src/__tests__/daily-log.test.ts +146 -0
  16. package/src/__tests__/dispatcher.test.ts +383 -0
  17. package/src/__tests__/errors.test.ts +240 -0
  18. package/src/__tests__/fuzz.test.ts +302 -0
  19. package/src/__tests__/gateway-actions.test.ts +1453 -0
  20. package/src/__tests__/gateway-context.test.ts +102 -0
  21. package/src/__tests__/gateway-http.test.ts +245 -0
  22. package/src/__tests__/handlers.test.ts +351 -0
  23. package/src/__tests__/history-persistence.test.ts +172 -0
  24. package/src/__tests__/history.test.ts +659 -0
  25. package/src/__tests__/integration.test.ts +189 -0
  26. package/src/__tests__/log.test.ts +110 -0
  27. package/src/__tests__/media-index.test.ts +277 -0
  28. package/src/__tests__/plugin.test.ts +317 -0
  29. package/src/__tests__/prompt-builder.test.ts +71 -0
  30. package/src/__tests__/sessions.test.ts +594 -0
  31. package/src/__tests__/teams-frontend.test.ts +239 -0
  32. package/src/__tests__/telegram.test.ts +177 -0
  33. package/src/__tests__/terminal-commands.test.ts +367 -0
  34. package/src/__tests__/terminal-frontend.test.ts +141 -0
  35. package/src/__tests__/terminal-renderer.test.ts +278 -0
  36. package/src/__tests__/watchdog.test.ts +287 -0
  37. package/src/__tests__/workspace.test.ts +184 -0
  38. package/src/backend/claude-sdk/index.ts +438 -0
  39. package/src/backend/claude-sdk/tools.ts +605 -0
  40. package/src/backend/opencode/index.ts +252 -0
  41. package/src/bootstrap.ts +134 -0
  42. package/src/cli.ts +611 -0
  43. package/src/core/cron.ts +148 -0
  44. package/src/core/dispatcher.ts +126 -0
  45. package/src/core/dream.ts +295 -0
  46. package/src/core/errors.ts +206 -0
  47. package/src/core/gateway-actions.ts +267 -0
  48. package/src/core/gateway.ts +258 -0
  49. package/src/core/plugin.ts +432 -0
  50. package/src/core/prompt-builder.ts +43 -0
  51. package/src/core/pulse.ts +175 -0
  52. package/src/core/types.ts +85 -0
  53. package/src/frontend/teams/actions.ts +101 -0
  54. package/src/frontend/teams/formatting.ts +220 -0
  55. package/src/frontend/teams/graph.ts +297 -0
  56. package/src/frontend/teams/index.ts +308 -0
  57. package/src/frontend/teams/proxy-fetch.ts +28 -0
  58. package/src/frontend/teams/tools.ts +177 -0
  59. package/src/frontend/telegram/actions.ts +437 -0
  60. package/src/frontend/telegram/admin.ts +178 -0
  61. package/src/frontend/telegram/callbacks.ts +251 -0
  62. package/src/frontend/telegram/commands.ts +543 -0
  63. package/src/frontend/telegram/formatting.ts +101 -0
  64. package/src/frontend/telegram/handlers.ts +1008 -0
  65. package/src/frontend/telegram/helpers.ts +105 -0
  66. package/src/frontend/telegram/index.ts +130 -0
  67. package/src/frontend/telegram/middleware.ts +177 -0
  68. package/src/frontend/telegram/userbot.ts +546 -0
  69. package/src/frontend/terminal/commands.ts +303 -0
  70. package/src/frontend/terminal/index.ts +282 -0
  71. package/src/frontend/terminal/input.ts +297 -0
  72. package/src/frontend/terminal/renderer.ts +248 -0
  73. package/src/index.ts +144 -0
  74. package/src/login.ts +89 -0
  75. package/src/storage/chat-settings.ts +218 -0
  76. package/src/storage/cron-store.ts +165 -0
  77. package/src/storage/daily-log.ts +97 -0
  78. package/src/storage/history.ts +278 -0
  79. package/src/storage/media-index.ts +116 -0
  80. package/src/storage/sessions.ts +328 -0
  81. package/src/util/chat-id.ts +21 -0
  82. package/src/util/config.ts +244 -0
  83. package/src/util/log.ts +122 -0
  84. package/src/util/paths.ts +80 -0
  85. package/src/util/time.ts +86 -0
  86. package/src/util/trace.ts +35 -0
  87. package/src/util/watchdog.ts +108 -0
  88. package/src/util/workspace.ts +208 -0
  89. package/tsconfig.json +13 -0
@@ -0,0 +1,337 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+
4
+ // Mock log module
5
+ vi.mock("../util/log.js", () => ({
6
+ log: vi.fn(),
7
+ logError: vi.fn(),
8
+ logWarn: vi.fn(),
9
+ }));
10
+
11
+ // Mock fs to avoid real filesystem side effects
12
+ vi.mock("node:fs", () => ({
13
+ existsSync: vi.fn(() => false),
14
+ readFileSync: vi.fn(() => "{}"),
15
+ writeFileSync: vi.fn(),
16
+ mkdirSync: vi.fn(),
17
+ }));
18
+
19
+ const {
20
+ getChatSettings,
21
+ setChatModel,
22
+ setChatEffort,
23
+ setChatPulse,
24
+ setChatPulseInterval,
25
+ getRegisteredPulseChats,
26
+ loadChatSettings,
27
+ resolveModelName,
28
+ EFFORT_LEVELS,
29
+ MODEL_ALIASES,
30
+ } = await import("../storage/chat-settings.js");
31
+
32
+ describe("chat-settings", () => {
33
+ describe("getChatSettings", () => {
34
+ it("returns empty object for unknown chat", () => {
35
+ const settings = getChatSettings("unknown-chat-abc");
36
+ expect(settings).toEqual({});
37
+ });
38
+ });
39
+
40
+ describe("setChatModel", () => {
41
+ it("persists model setting", () => {
42
+ const chatId = "test-model-set";
43
+ setChatModel(chatId, "claude-opus-4-6");
44
+ expect(getChatSettings(chatId).model).toBe("claude-opus-4-6");
45
+ });
46
+
47
+ it("clears model when set to undefined", () => {
48
+ const chatId = "test-model-clear";
49
+ setChatModel(chatId, "claude-opus-4-6");
50
+ setChatModel(chatId, undefined);
51
+ expect(getChatSettings(chatId).model).toBeUndefined();
52
+ });
53
+ });
54
+
55
+ describe("setChatEffort", () => {
56
+ it("persists effort setting", () => {
57
+ const chatId = "test-effort-set";
58
+ setChatEffort(chatId, "high");
59
+ expect(getChatSettings(chatId).effort).toBe("high");
60
+ });
61
+
62
+ it("clears effort when set to undefined", () => {
63
+ const chatId = "test-effort-clear";
64
+ setChatEffort(chatId, "max");
65
+ setChatEffort(chatId, undefined);
66
+ expect(getChatSettings(chatId).effort).toBeUndefined();
67
+ });
68
+
69
+ it("accepts all valid effort levels", () => {
70
+ for (const level of EFFORT_LEVELS) {
71
+ const chatId = `test-effort-${level}`;
72
+ setChatEffort(chatId, level);
73
+ expect(getChatSettings(chatId).effort).toBe(level);
74
+ }
75
+ });
76
+ });
77
+
78
+ describe("resolveModelName", () => {
79
+ it("resolves 'sonnet' to claude-sonnet-4-6", () => {
80
+ expect(resolveModelName("sonnet")).toBe("claude-sonnet-4-6");
81
+ });
82
+
83
+ it("resolves 'opus' to claude-opus-4-6", () => {
84
+ expect(resolveModelName("opus")).toBe("claude-opus-4-6");
85
+ });
86
+
87
+ it("resolves 'haiku' to claude-haiku-4-5", () => {
88
+ expect(resolveModelName("haiku")).toBe("claude-haiku-4-5");
89
+ });
90
+
91
+ it("resolves versioned aliases", () => {
92
+ expect(resolveModelName("sonnet-4.6")).toBe("claude-sonnet-4-6");
93
+ expect(resolveModelName("opus-4.6")).toBe("claude-opus-4-6");
94
+ expect(resolveModelName("haiku-4.5")).toBe("claude-haiku-4-5");
95
+ });
96
+
97
+ it("resolves dash-separated aliases", () => {
98
+ expect(resolveModelName("sonnet-4-6")).toBe("claude-sonnet-4-6");
99
+ expect(resolveModelName("opus-4-6")).toBe("claude-opus-4-6");
100
+ expect(resolveModelName("haiku-4-5")).toBe("claude-haiku-4-5");
101
+ });
102
+
103
+ it("is case-insensitive", () => {
104
+ expect(resolveModelName("Sonnet")).toBe("claude-sonnet-4-6");
105
+ expect(resolveModelName("OPUS")).toBe("claude-opus-4-6");
106
+ });
107
+
108
+ it("trims whitespace", () => {
109
+ expect(resolveModelName(" sonnet ")).toBe("claude-sonnet-4-6");
110
+ });
111
+
112
+ it("passes through unknown model names unchanged", () => {
113
+ expect(resolveModelName("gpt-4")).toBe("gpt-4");
114
+ expect(resolveModelName("claude-sonnet-4-6")).toBe("claude-sonnet-4-6");
115
+ });
116
+ });
117
+
118
+ describe("resolveModelName — exhaustive alias coverage", () => {
119
+ it("resolves all base aliases correctly", () => {
120
+ expect(resolveModelName("sonnet")).toBe("claude-sonnet-4-6");
121
+ expect(resolveModelName("opus")).toBe("claude-opus-4-6");
122
+ expect(resolveModelName("haiku")).toBe("claude-haiku-4-5");
123
+ });
124
+
125
+ it("resolves all dot-separated version aliases", () => {
126
+ expect(resolveModelName("sonnet-4.6")).toBe("claude-sonnet-4-6");
127
+ expect(resolveModelName("opus-4.6")).toBe("claude-opus-4-6");
128
+ expect(resolveModelName("haiku-4.5")).toBe("claude-haiku-4-5");
129
+ });
130
+
131
+ it("resolves all dash-separated version aliases", () => {
132
+ expect(resolveModelName("sonnet-4-6")).toBe("claude-sonnet-4-6");
133
+ expect(resolveModelName("opus-4-6")).toBe("claude-opus-4-6");
134
+ expect(resolveModelName("haiku-4-5")).toBe("claude-haiku-4-5");
135
+ });
136
+
137
+ it("passes through completely unknown model names unchanged", () => {
138
+ expect(resolveModelName("gpt-4")).toBe("gpt-4");
139
+ expect(resolveModelName("llama-3")).toBe("llama-3");
140
+ expect(resolveModelName("mistral-large")).toBe("mistral-large");
141
+ });
142
+
143
+ it("passes through full claude model names unchanged (not aliases)", () => {
144
+ expect(resolveModelName("claude-sonnet-4-6")).toBe("claude-sonnet-4-6");
145
+ expect(resolveModelName("claude-opus-4-6")).toBe("claude-opus-4-6");
146
+ expect(resolveModelName("claude-haiku-4-5")).toBe("claude-haiku-4-5");
147
+ });
148
+
149
+ it("preserves original casing for unknown models", () => {
150
+ expect(resolveModelName("MyCustomModel")).toBe("MyCustomModel");
151
+ });
152
+ });
153
+
154
+ describe("EFFORT_LEVELS", () => {
155
+ it("contains all valid levels", () => {
156
+ expect(EFFORT_LEVELS).toEqual(["off", "low", "medium", "high", "max"]);
157
+ });
158
+
159
+ it("has 5 levels", () => {
160
+ expect(EFFORT_LEVELS).toHaveLength(5);
161
+ });
162
+ });
163
+
164
+ describe("MODEL_ALIASES", () => {
165
+ it("contains all expected aliases", () => {
166
+ expect(Object.keys(MODEL_ALIASES).length).toBeGreaterThanOrEqual(9);
167
+ expect(MODEL_ALIASES.sonnet).toBe("claude-sonnet-4-6");
168
+ expect(MODEL_ALIASES.opus).toBe("claude-opus-4-6");
169
+ expect(MODEL_ALIASES.haiku).toBe("claude-haiku-4-5");
170
+ });
171
+ });
172
+
173
+ describe("setChatPulse", () => {
174
+ it("enables pulse for a chat", () => {
175
+ const chatId = "test-pulse-enable";
176
+ setChatPulse(chatId, true);
177
+ expect(getChatSettings(chatId).pulse).toBe(true);
178
+ });
179
+
180
+ it("disables pulse for a chat", () => {
181
+ const chatId = "test-pulse-disable";
182
+ setChatPulse(chatId, true);
183
+ setChatPulse(chatId, false);
184
+ expect(getChatSettings(chatId).pulse).toBe(false);
185
+ });
186
+
187
+ it("clears pulse when set to undefined", () => {
188
+ const chatId = "test-pulse-clear";
189
+ setChatPulse(chatId, true);
190
+ setChatPulse(chatId, undefined);
191
+ expect(getChatSettings(chatId).pulse).toBeUndefined();
192
+ });
193
+ });
194
+
195
+ describe("setChatPulseInterval", () => {
196
+ it("sets pulse interval in milliseconds", () => {
197
+ const chatId = "test-pulse-interval";
198
+ setChatPulseInterval(chatId, 60000);
199
+ expect(getChatSettings(chatId).pulseIntervalMs).toBe(60000);
200
+ });
201
+
202
+ it("clears pulse interval when set to undefined", () => {
203
+ const chatId = "test-pulse-interval-clear";
204
+ setChatPulseInterval(chatId, 30000);
205
+ setChatPulseInterval(chatId, undefined);
206
+ expect(getChatSettings(chatId).pulseIntervalMs).toBeUndefined();
207
+ });
208
+
209
+ it("updates an existing interval", () => {
210
+ const chatId = "test-pulse-interval-update";
211
+ setChatPulseInterval(chatId, 30000);
212
+ setChatPulseInterval(chatId, 120000);
213
+ expect(getChatSettings(chatId).pulseIntervalMs).toBe(120000);
214
+ });
215
+ });
216
+
217
+ describe("getRegisteredPulseChats", () => {
218
+ it("returns chat IDs where pulse is explicitly true", () => {
219
+ const id1 = "pulse-reg-1";
220
+ const id2 = "pulse-reg-2";
221
+ const id3 = "pulse-reg-3";
222
+ setChatPulse(id1, true);
223
+ setChatPulse(id2, false);
224
+ setChatPulse(id3, true);
225
+
226
+ const result = getRegisteredPulseChats();
227
+ expect(result).toContain(id1);
228
+ expect(result).toContain(id3);
229
+ expect(result).not.toContain(id2);
230
+ });
231
+
232
+ it("does not include chats without pulse setting", () => {
233
+ const id = "pulse-reg-no-setting";
234
+ setChatModel(id, "claude-opus-4-6"); // set something else, not pulse
235
+ const result = getRegisteredPulseChats();
236
+ expect(result).not.toContain(id);
237
+ });
238
+ });
239
+
240
+ describe("loadChatSettings (migration)", () => {
241
+ it("migrates maxThinkingTokens=0 to effort=off", () => {
242
+ const mockData = {
243
+ "migrate-1": { maxThinkingTokens: 0 },
244
+ };
245
+ vi.mocked(existsSync).mockReturnValueOnce(true);
246
+ vi.mocked(readFileSync).mockReturnValueOnce(JSON.stringify(mockData));
247
+ // Also mock the dir check for save()
248
+ vi.mocked(existsSync).mockReturnValueOnce(true);
249
+
250
+ loadChatSettings();
251
+
252
+ const settings = getChatSettings("migrate-1");
253
+ expect(settings.effort).toBe("off");
254
+ // maxThinkingTokens should be removed
255
+ expect((settings as Record<string, unknown>).maxThinkingTokens).toBeUndefined();
256
+ });
257
+
258
+ it("migrates maxThinkingTokens=1000 to effort=low", () => {
259
+ const mockData = {
260
+ "migrate-2": { maxThinkingTokens: 1000 },
261
+ };
262
+ vi.mocked(existsSync).mockReturnValueOnce(true);
263
+ vi.mocked(readFileSync).mockReturnValueOnce(JSON.stringify(mockData));
264
+ vi.mocked(existsSync).mockReturnValueOnce(true);
265
+
266
+ loadChatSettings();
267
+
268
+ expect(getChatSettings("migrate-2").effort).toBe("low");
269
+ });
270
+
271
+ it("migrates maxThinkingTokens=5000 to effort=medium", () => {
272
+ const mockData = {
273
+ "migrate-3": { maxThinkingTokens: 5000 },
274
+ };
275
+ vi.mocked(existsSync).mockReturnValueOnce(true);
276
+ vi.mocked(readFileSync).mockReturnValueOnce(JSON.stringify(mockData));
277
+ vi.mocked(existsSync).mockReturnValueOnce(true);
278
+
279
+ loadChatSettings();
280
+
281
+ expect(getChatSettings("migrate-3").effort).toBe("medium");
282
+ });
283
+
284
+ it("migrates maxThinkingTokens=12000 to effort=high", () => {
285
+ const mockData = {
286
+ "migrate-4": { maxThinkingTokens: 12000 },
287
+ };
288
+ vi.mocked(existsSync).mockReturnValueOnce(true);
289
+ vi.mocked(readFileSync).mockReturnValueOnce(JSON.stringify(mockData));
290
+ vi.mocked(existsSync).mockReturnValueOnce(true);
291
+
292
+ loadChatSettings();
293
+
294
+ expect(getChatSettings("migrate-4").effort).toBe("high");
295
+ });
296
+
297
+ it("migrates maxThinkingTokens=20000 to effort=max", () => {
298
+ const mockData = {
299
+ "migrate-5": { maxThinkingTokens: 20000 },
300
+ };
301
+ vi.mocked(existsSync).mockReturnValueOnce(true);
302
+ vi.mocked(readFileSync).mockReturnValueOnce(JSON.stringify(mockData));
303
+ vi.mocked(existsSync).mockReturnValueOnce(true);
304
+
305
+ loadChatSettings();
306
+
307
+ expect(getChatSettings("migrate-5").effort).toBe("max");
308
+ });
309
+
310
+ it("removes maxThinkingTokens when effort already set", () => {
311
+ const mockData = {
312
+ "migrate-6": { maxThinkingTokens: 5000, effort: "high" },
313
+ };
314
+ vi.mocked(existsSync).mockReturnValueOnce(true);
315
+ vi.mocked(readFileSync).mockReturnValueOnce(JSON.stringify(mockData));
316
+ vi.mocked(existsSync).mockReturnValueOnce(true);
317
+
318
+ loadChatSettings();
319
+
320
+ const settings = getChatSettings("migrate-6");
321
+ // Should keep existing effort, just clean up old field
322
+ expect(settings.effort).toBe("high");
323
+ expect((settings as Record<string, unknown>).maxThinkingTokens).toBeUndefined();
324
+ });
325
+
326
+ it("handles missing store file gracefully", () => {
327
+ vi.mocked(existsSync).mockReturnValueOnce(false);
328
+ expect(() => loadChatSettings()).not.toThrow();
329
+ });
330
+
331
+ it("handles corrupt JSON gracefully", () => {
332
+ vi.mocked(existsSync).mockReturnValueOnce(true);
333
+ vi.mocked(readFileSync).mockReturnValueOnce("not valid json{{{");
334
+ expect(() => loadChatSettings()).not.toThrow();
335
+ });
336
+ });
337
+ });