talon-agent 1.10.1 → 1.11.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/package.json +16 -3
- package/src/backend/claude-sdk/options.ts +19 -9
- package/src/core/gateway.ts +10 -3
- package/src/frontend/telegram/handlers.ts +108 -5
- package/src/__tests__/chat-id.test.ts +0 -91
- package/src/__tests__/chat-settings.test.ts +0 -471
- package/src/__tests__/claude-sdk-models.test.ts +0 -146
- package/src/__tests__/claude-sdk-options.test.ts +0 -205
- package/src/__tests__/cleanup-registry.test.ts +0 -58
- package/src/__tests__/compose-tools.test.ts +0 -216
- package/src/__tests__/config.test.ts +0 -716
- package/src/__tests__/cron-store-extended.test.ts +0 -661
- package/src/__tests__/cron-store.test.ts +0 -574
- package/src/__tests__/daily-log.test.ts +0 -357
- package/src/__tests__/disallowed-tools.test.ts +0 -64
- package/src/__tests__/dispatcher.test.ts +0 -784
- package/src/__tests__/dream.test.ts +0 -1145
- package/src/__tests__/end-turn.test.ts +0 -307
- package/src/__tests__/errors-extended.test.ts +0 -428
- package/src/__tests__/errors.test.ts +0 -332
- package/src/__tests__/fixtures/test-mcp-server.ts +0 -37
- package/src/__tests__/fuzz.test.ts +0 -375
- package/src/__tests__/gateway-actions.test.ts +0 -1772
- package/src/__tests__/gateway-context.test.ts +0 -102
- package/src/__tests__/gateway-http.test.ts +0 -436
- package/src/__tests__/gateway-retry.test.ts +0 -355
- package/src/__tests__/gateway-withRetry-extended.test.ts +0 -343
- package/src/__tests__/graph.test.ts +0 -830
- package/src/__tests__/handlers-stream.test.ts +0 -203
- package/src/__tests__/handlers.test.ts +0 -2972
- package/src/__tests__/heartbeat.test.ts +0 -388
- package/src/__tests__/history-extended.test.ts +0 -775
- package/src/__tests__/history-persistence.test.ts +0 -227
- package/src/__tests__/history.test.ts +0 -693
- package/src/__tests__/integration/sdk-stub.test.ts +0 -208
- package/src/__tests__/integration/stub-claude/build-sea.mjs +0 -114
- package/src/__tests__/integration/stub-claude/fake-claude.mjs +0 -352
- package/src/__tests__/integration/stub-claude/helpers.ts +0 -263
- package/src/__tests__/integration/stub-claude/protocol.ts +0 -108
- package/src/__tests__/integration/stub-claude/sea-config.json +0 -7
- package/src/__tests__/integration/talon-bootstrap.ts +0 -206
- package/src/__tests__/integration/talon-functional.test.ts +0 -190
- package/src/__tests__/integration.test.ts +0 -224
- package/src/__tests__/log-init.test.ts +0 -129
- package/src/__tests__/log.test.ts +0 -129
- package/src/__tests__/mcp-launcher-functional.test.ts +0 -334
- package/src/__tests__/mcp-launcher.test.ts +0 -139
- package/src/__tests__/mcp-lifecycle.test.ts +0 -165
- package/src/__tests__/media-index.test.ts +0 -559
- package/src/__tests__/mempalace-plugin.test.ts +0 -350
- package/src/__tests__/metrics.test.ts +0 -76
- package/src/__tests__/opencode-models.test.ts +0 -117
- package/src/__tests__/opencode-summary.test.ts +0 -105
- package/src/__tests__/opencode-ui.test.ts +0 -94
- package/src/__tests__/package.functional.test.ts +0 -178
- package/src/__tests__/plugin.test.ts +0 -962
- package/src/__tests__/reload-plugins.test.ts +0 -342
- package/src/__tests__/sessions.test.ts +0 -877
- package/src/__tests__/storage-save-errors.test.ts +0 -342
- package/src/__tests__/teams-frontend.test.ts +0 -762
- package/src/__tests__/telegram-formatting.test.ts +0 -86
- package/src/__tests__/telegram-helpers.test.ts +0 -151
- package/src/__tests__/telegram.test.ts +0 -176
- package/src/__tests__/terminal-commands.test.ts +0 -666
- package/src/__tests__/terminal-frontend.test.ts +0 -141
- package/src/__tests__/terminal-renderer.test.ts +0 -501
- package/src/__tests__/time.test.ts +0 -107
- package/src/__tests__/tool-functional.test.ts +0 -615
- package/src/__tests__/tool-id-coercion.test.ts +0 -136
- package/src/__tests__/watchdog.test.ts +0 -285
- package/src/__tests__/workspace-migrate.test.ts +0 -256
- package/src/__tests__/workspace.test.ts +0 -284
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
|
|
5
5
|
"author": "Dylan Neve",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,16 @@
|
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"bin/",
|
|
33
|
-
"src/",
|
|
33
|
+
"src/backend/",
|
|
34
|
+
"src/core/",
|
|
35
|
+
"src/frontend/",
|
|
36
|
+
"src/plugins/",
|
|
37
|
+
"src/storage/",
|
|
38
|
+
"src/util/",
|
|
39
|
+
"src/bootstrap.ts",
|
|
40
|
+
"src/cli.ts",
|
|
41
|
+
"src/index.ts",
|
|
42
|
+
"src/login.ts",
|
|
34
43
|
"prompts/",
|
|
35
44
|
"README.md",
|
|
36
45
|
"tsconfig.json"
|
|
@@ -43,6 +52,9 @@
|
|
|
43
52
|
"test": "vitest run",
|
|
44
53
|
"test:ci": "vitest run --reporter=verbose --reporter=json --outputFile=test-results.json",
|
|
45
54
|
"test:functional": "vitest run --reporter=verbose --reporter=json --outputFile=functional-results.json src/__tests__/package.functional.test.ts src/__tests__/tool-functional.test.ts src/__tests__/mcp-launcher.test.ts src/__tests__/mcp-launcher-functional.test.ts src/__tests__/integration/sdk-stub.test.ts src/__tests__/integration/talon-functional.test.ts",
|
|
55
|
+
"test:integration": "vitest run --reporter=verbose --reporter=json --outputFile=integration-results.json src/__tests__/integration/talon-mcp-functional.test.ts",
|
|
56
|
+
"test:integration:all": "vitest run --reporter=verbose src/__tests__/integration/",
|
|
57
|
+
"tarball:check": "node .github/scripts/tarball-check.mjs",
|
|
46
58
|
"build:stub-sea": "node src/__tests__/integration/stub-claude/build-sea.mjs",
|
|
47
59
|
"test:watch": "vitest",
|
|
48
60
|
"test:coverage": "vitest run --coverage",
|
|
@@ -91,6 +103,7 @@
|
|
|
91
103
|
},
|
|
92
104
|
"overrides": {
|
|
93
105
|
"@anthropic-ai/sdk": "^0.95.0",
|
|
94
|
-
"ip-address": "^10.1.1"
|
|
106
|
+
"ip-address": "^10.1.1",
|
|
107
|
+
"fast-uri": "^3.1.2"
|
|
95
108
|
}
|
|
96
109
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
9
10
|
import type {
|
|
10
11
|
Options,
|
|
11
12
|
PostToolBatchHookInput,
|
|
@@ -45,10 +46,17 @@ export function buildMcpServers(
|
|
|
45
46
|
const config = getConfig();
|
|
46
47
|
const bridgeUrl = `http://127.0.0.1:${getBridgePort()}`;
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// tsx as a Node loader is passed via `--import <url>`. Node accepts URLs
|
|
50
|
+
// or absolute paths, but on Windows a raw backslash path (`D:\…\tsx`) is
|
|
51
|
+
// ambiguous between path and URL — the loader hook fails to register and
|
|
52
|
+
// every subsequent `import` of a .ts file throws. `pathToFileURL` produces
|
|
53
|
+
// a cross-platform `file://` URL that Node always treats as a loader URL.
|
|
54
|
+
const tsxImport = pathToFileURL(
|
|
55
|
+
resolve(
|
|
56
|
+
import.meta.dirname ?? ".",
|
|
57
|
+
"../../../node_modules/tsx/dist/esm/index.mjs",
|
|
58
|
+
),
|
|
59
|
+
).href;
|
|
52
60
|
const mcpServerPath = resolve(
|
|
53
61
|
import.meta.dirname ?? ".",
|
|
54
62
|
"../../core/tools/mcp-server.ts",
|
|
@@ -72,12 +80,14 @@ export function buildMcpServers(
|
|
|
72
80
|
TALON_CHAT_ID: chatId,
|
|
73
81
|
TALON_FRONTEND: frontend,
|
|
74
82
|
};
|
|
83
|
+
// `node --import <tsx-loader>` everywhere — tsx as a Node loader works
|
|
84
|
+
// identically on Windows and POSIX, and avoids spawning `npx.cmd` (which
|
|
85
|
+
// Node 20.19+ refuses to execute via child_process.spawn without
|
|
86
|
+
// shell:true; CVE-2024-27980 mitigation). The wrapping launcher would
|
|
87
|
+
// hit the same .cmd ban when calling its child.
|
|
75
88
|
servers[serverName] = wrapMcpServer({
|
|
76
|
-
command:
|
|
77
|
-
args:
|
|
78
|
-
process.platform === "win32"
|
|
79
|
-
? ["tsx", mcpServerPath]
|
|
80
|
-
: ["--import", tsxImport, mcpServerPath],
|
|
89
|
+
command: "node",
|
|
90
|
+
args: ["--import", tsxImport, mcpServerPath],
|
|
81
91
|
env: mcpEnv,
|
|
82
92
|
});
|
|
83
93
|
}
|
package/src/core/gateway.ts
CHANGED
|
@@ -298,9 +298,16 @@ export class Gateway {
|
|
|
298
298
|
});
|
|
299
299
|
httpServer.listen(p, "127.0.0.1", () => {
|
|
300
300
|
this.server = httpServer;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
// When the caller asks for port 0 the OS assigns a random free
|
|
302
|
+
// port — read the actual port off the listening socket instead
|
|
303
|
+
// of saving the requested 0.
|
|
304
|
+
const addr = httpServer.address();
|
|
305
|
+
this.port =
|
|
306
|
+
typeof addr === "object" && addr !== null
|
|
307
|
+
? (addr as { port: number }).port
|
|
308
|
+
: p;
|
|
309
|
+
log("gateway", `Action gateway on :${this.port}`);
|
|
310
|
+
resolve(this.port);
|
|
304
311
|
});
|
|
305
312
|
};
|
|
306
313
|
tryPort(port);
|
|
@@ -159,11 +159,117 @@ export async function isAccessAllowed(
|
|
|
159
159
|
return false;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Maximum length of an unauthorized message body to retain in logs.
|
|
164
|
+
* Keeps abusive payloads (large pastes, attachment captions etc.) bounded
|
|
165
|
+
* while still preserving enough context to understand what was sent.
|
|
166
|
+
*/
|
|
167
|
+
const UNAUTHORIZED_BODY_MAX_LEN = 1024;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Best-effort preview of an unauthorized message for forensics.
|
|
171
|
+
*
|
|
172
|
+
* Returns the visible text payload (text or caption), a short tag for
|
|
173
|
+
* media-only messages (`[sticker: 🤖]`, `[photo]`, `[voice 14s]`, etc.),
|
|
174
|
+
* or `undefined` if there's nothing meaningful to capture (e.g. a service
|
|
175
|
+
* message or empty content).
|
|
176
|
+
*
|
|
177
|
+
* Truncated to UNAUTHORIZED_BODY_MAX_LEN to keep log lines bounded.
|
|
178
|
+
*/
|
|
179
|
+
export function extractUnauthorizedPreview(
|
|
180
|
+
message:
|
|
181
|
+
| {
|
|
182
|
+
text?: string;
|
|
183
|
+
caption?: string;
|
|
184
|
+
sticker?: { emoji?: string; set_name?: string };
|
|
185
|
+
photo?: unknown;
|
|
186
|
+
voice?: { duration?: number };
|
|
187
|
+
video?: unknown;
|
|
188
|
+
video_note?: unknown;
|
|
189
|
+
audio?: unknown;
|
|
190
|
+
animation?: unknown;
|
|
191
|
+
document?: { file_name?: string };
|
|
192
|
+
contact?: unknown;
|
|
193
|
+
location?: unknown;
|
|
194
|
+
poll?: { question?: string };
|
|
195
|
+
dice?: { emoji?: string };
|
|
196
|
+
}
|
|
197
|
+
| undefined,
|
|
198
|
+
): string | undefined {
|
|
199
|
+
if (!message) return undefined;
|
|
200
|
+
|
|
201
|
+
const text = message.text ?? message.caption;
|
|
202
|
+
if (typeof text === "string" && text.trim().length > 0) {
|
|
203
|
+
return text.length > UNAUTHORIZED_BODY_MAX_LEN
|
|
204
|
+
? `${text.slice(0, UNAUTHORIZED_BODY_MAX_LEN)}… [truncated]`
|
|
205
|
+
: text;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (message.sticker) {
|
|
209
|
+
const emoji = message.sticker.emoji ?? "?";
|
|
210
|
+
const set = message.sticker.set_name
|
|
211
|
+
? ` from ${message.sticker.set_name}`
|
|
212
|
+
: "";
|
|
213
|
+
return `[sticker: ${emoji}${set}]`;
|
|
214
|
+
}
|
|
215
|
+
if (message.photo) return "[photo]";
|
|
216
|
+
if (message.voice) {
|
|
217
|
+
const dur = message.voice.duration;
|
|
218
|
+
return dur ? `[voice ${dur}s]` : "[voice]";
|
|
219
|
+
}
|
|
220
|
+
if (message.video_note) return "[video note]";
|
|
221
|
+
if (message.video) return "[video]";
|
|
222
|
+
if (message.audio) return "[audio]";
|
|
223
|
+
if (message.animation) return "[animation]";
|
|
224
|
+
if (message.document) {
|
|
225
|
+
return message.document.file_name
|
|
226
|
+
? `[document: ${message.document.file_name}]`
|
|
227
|
+
: "[document]";
|
|
228
|
+
}
|
|
229
|
+
if (message.contact) return "[contact]";
|
|
230
|
+
if (message.location) return "[location]";
|
|
231
|
+
if (message.poll) {
|
|
232
|
+
return message.poll.question
|
|
233
|
+
? `[poll: ${message.poll.question}]`
|
|
234
|
+
: "[poll]";
|
|
235
|
+
}
|
|
236
|
+
if (message.dice) return `[dice: ${message.dice.emoji ?? "🎲"}]`;
|
|
237
|
+
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
162
241
|
async function notifyUnauthorized(
|
|
163
242
|
bot: Bot,
|
|
164
243
|
ctx: Context,
|
|
165
244
|
type: "dm" | "group",
|
|
166
245
|
): Promise<void> {
|
|
246
|
+
const sender = getSenderName(ctx.from);
|
|
247
|
+
const username = ctx.from?.username ? ` (@${ctx.from.username})` : "";
|
|
248
|
+
const userId = ctx.from?.id ?? "unknown";
|
|
249
|
+
|
|
250
|
+
// Capture message body BEFORE the cooldown check — every unauthorized
|
|
251
|
+
// attempt should be recorded for forensics, even if the user-facing
|
|
252
|
+
// warning + admin notification are suppressed by cooldown. Without
|
|
253
|
+
// this, follow-up DMs from a known social-engineering account vanish
|
|
254
|
+
// entirely from logs.
|
|
255
|
+
const body = extractUnauthorizedPreview(
|
|
256
|
+
ctx.message as Parameters<typeof extractUnauthorizedPreview>[0],
|
|
257
|
+
);
|
|
258
|
+
if (body) {
|
|
259
|
+
try {
|
|
260
|
+
appendDailyLog(
|
|
261
|
+
`⚠️ UNAUTHORIZED ${sender}${username} [id:${userId}]`,
|
|
262
|
+
body,
|
|
263
|
+
);
|
|
264
|
+
} catch {
|
|
265
|
+
/* daily log unavailable — fall through to talon.log */
|
|
266
|
+
}
|
|
267
|
+
logWarn(
|
|
268
|
+
"access",
|
|
269
|
+
`Unauthorized ${type} body from ${sender}${username} [id:${userId}]: ${body.slice(0, 200)}`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
167
273
|
const key = type === "dm" ? `dm:${ctx.from?.id}` : `group:${ctx.chat?.id}`;
|
|
168
274
|
const now = Date.now();
|
|
169
275
|
const lastWarned = unauthorizedCooldown.get(key);
|
|
@@ -173,10 +279,6 @@ async function notifyUnauthorized(
|
|
|
173
279
|
}
|
|
174
280
|
unauthorizedCooldown.set(key, now);
|
|
175
281
|
|
|
176
|
-
const sender = getSenderName(ctx.from);
|
|
177
|
-
const username = ctx.from?.username ? ` (@${ctx.from.username})` : "";
|
|
178
|
-
const userId = ctx.from?.id ?? "unknown";
|
|
179
|
-
|
|
180
282
|
// Warn the user
|
|
181
283
|
try {
|
|
182
284
|
await bot.api.sendMessage(
|
|
@@ -193,8 +295,9 @@ async function notifyUnauthorized(
|
|
|
193
295
|
type === "dm"
|
|
194
296
|
? `🚨 Unauthorized DM from ${sender}${username} [id:${userId}]`
|
|
195
297
|
: `🚨 Unauthorized group access: "${(ctx.chat as { title?: string })?.title ?? ctx.chat!.id}" [id:${ctx.chat!.id}] by ${sender}${username}`;
|
|
298
|
+
const detailWithBody = body ? `${detail}\n\n${body.slice(0, 400)}` : detail;
|
|
196
299
|
try {
|
|
197
|
-
await bot.api.sendMessage(adminId,
|
|
300
|
+
await bot.api.sendMessage(adminId, detailWithBody);
|
|
198
301
|
} catch {
|
|
199
302
|
/* admin unreachable — ignore */
|
|
200
303
|
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
deriveNumericChatId,
|
|
5
|
-
generateTerminalChatId,
|
|
6
|
-
isTerminalChatId,
|
|
7
|
-
} from "../util/chat-id.js";
|
|
8
|
-
|
|
9
|
-
describe("deriveNumericChatId", () => {
|
|
10
|
-
it("returns a positive number", () => {
|
|
11
|
-
const id = deriveNumericChatId("test-chat");
|
|
12
|
-
expect(id).toBeGreaterThan(0);
|
|
13
|
-
expect(Number.isInteger(id)).toBe(true);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("returns the same value for the same input", () => {
|
|
17
|
-
const a = deriveNumericChatId("stable-id");
|
|
18
|
-
const b = deriveNumericChatId("stable-id");
|
|
19
|
-
expect(a).toBe(b);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("returns different values for different inputs", () => {
|
|
23
|
-
const a = deriveNumericChatId("chat-alpha");
|
|
24
|
-
const b = deriveNumericChatId("chat-beta");
|
|
25
|
-
expect(a).not.toBe(b);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("handles terminal-style IDs", () => {
|
|
29
|
-
const id = deriveNumericChatId("t_1711360000000");
|
|
30
|
-
expect(id).toBeGreaterThan(0);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("handles empty string", () => {
|
|
34
|
-
const id = deriveNumericChatId("");
|
|
35
|
-
expect(id).toBeGreaterThanOrEqual(0);
|
|
36
|
-
expect(Number.isInteger(id)).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("generateTerminalChatId", () => {
|
|
41
|
-
it("returns a string starting with t_", () => {
|
|
42
|
-
const id = generateTerminalChatId();
|
|
43
|
-
expect(id).toMatch(/^t_\d+$/);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("returns a string with numeric timestamp portion", () => {
|
|
47
|
-
const id = generateTerminalChatId();
|
|
48
|
-
const ts = Number(id.slice(2));
|
|
49
|
-
expect(Number.isNaN(ts)).toBe(false);
|
|
50
|
-
expect(ts).toBeGreaterThan(0);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("uses a recent timestamp", () => {
|
|
54
|
-
const before = Date.now();
|
|
55
|
-
const id = generateTerminalChatId();
|
|
56
|
-
const after = Date.now();
|
|
57
|
-
const ts = Number(id.slice(2));
|
|
58
|
-
expect(ts).toBeGreaterThanOrEqual(before);
|
|
59
|
-
expect(ts).toBeLessThanOrEqual(after);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("isTerminalChatId", () => {
|
|
64
|
-
it('returns true for "1" (legacy ID)', () => {
|
|
65
|
-
expect(isTerminalChatId("1")).toBe(true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("returns true for t_ prefixed IDs", () => {
|
|
69
|
-
expect(isTerminalChatId("t_1711360000000")).toBe(true);
|
|
70
|
-
expect(isTerminalChatId("t_0")).toBe(true);
|
|
71
|
-
expect(isTerminalChatId("t_abc")).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("returns false for Telegram numeric IDs", () => {
|
|
75
|
-
expect(isTerminalChatId("123456789")).toBe(false);
|
|
76
|
-
expect(isTerminalChatId("-100123456")).toBe(false);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("returns false for Teams IDs", () => {
|
|
80
|
-
expect(isTerminalChatId("teams_chat_abc123")).toBe(false);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("returns false for empty string", () => {
|
|
84
|
-
expect(isTerminalChatId("")).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('returns false for "10" and other strings starting with 1', () => {
|
|
88
|
-
expect(isTerminalChatId("10")).toBe(false);
|
|
89
|
-
expect(isTerminalChatId("100")).toBe(false);
|
|
90
|
-
});
|
|
91
|
-
});
|