talon-agent 1.35.0 → 1.36.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 +3 -3
- package/src/backend/claude-sdk/options.ts +41 -1
- package/src/bootstrap.ts +1 -0
- package/src/core/engine/gateway-actions/index.ts +2 -0
- package/src/core/engine/gateway-actions/mesh.ts +27 -0
- package/src/core/engine/gateway-actions/native.ts +574 -0
- package/src/core/mcp-hub/index.ts +3 -0
- package/src/core/mcp-hub/talon-server.ts +3 -0
- package/src/core/mesh/persist.ts +49 -0
- package/src/core/mesh/registry.ts +10 -18
- package/src/core/mesh/service.ts +632 -25
- package/src/core/mesh/teleport.ts +104 -0
- package/src/core/mesh/transfers.ts +186 -0
- package/src/core/tools/bridge.ts +85 -4
- package/src/core/tools/index.ts +23 -2
- package/src/core/tools/mesh.ts +82 -0
- package/src/core/tools/native.ts +138 -0
- package/src/core/tools/types.ts +2 -1
- package/src/frontend/native/index.ts +2 -0
- package/src/frontend/native/server.ts +40 -1
- package/src/util/config.ts +10 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDefinition } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Native tools — Talon's own shell/filesystem primitives, the replacement
|
|
6
|
+
* for the SDK's built-in Bash/Read/Write/Edit/Glob/Grep. They are only
|
|
7
|
+
* surfaced when `config.nativeTools` is enabled (see the tool composition in
|
|
8
|
+
* the MCP server); otherwise the built-ins are used.
|
|
9
|
+
*
|
|
10
|
+
* Their extra power over the built-ins: every one honours the active
|
|
11
|
+
* `teleport` target, so with a teleport engaged they run ON a companion
|
|
12
|
+
* device (via the mesh exec/fs channel) instead of on the daemon host.
|
|
13
|
+
*/
|
|
14
|
+
export const nativeTools: ToolDefinition[] = [
|
|
15
|
+
{
|
|
16
|
+
name: "teleport",
|
|
17
|
+
description:
|
|
18
|
+
"Switch Talon's native shell/file tools to run ON a companion mesh device (e.g. your phone). After teleporting, bash/read/write/edit/glob/search execute on that device until teleport_back. The device must be online and advertise the 'exec' capability.",
|
|
19
|
+
schema: {
|
|
20
|
+
device: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe(
|
|
24
|
+
"Device id or name fragment (see list_devices). Defaults to the most recent mobile device.",
|
|
25
|
+
),
|
|
26
|
+
},
|
|
27
|
+
execute: (params, bridge) => bridge("teleport", params),
|
|
28
|
+
tag: "native",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "teleport_back",
|
|
32
|
+
description:
|
|
33
|
+
"Return native tools to the daemon host — bash/read/write/edit/glob/search run locally again.",
|
|
34
|
+
schema: {},
|
|
35
|
+
execute: (_params, bridge) => bridge("teleport_back", {}),
|
|
36
|
+
tag: "native",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "bash",
|
|
40
|
+
description:
|
|
41
|
+
"Run a shell command. Runs on the daemon host, or ON the active teleport device if one is engaged. On a teleported device, `cd` persists across calls (a real working-directory session).",
|
|
42
|
+
schema: {
|
|
43
|
+
command: z.string().describe("The shell command to run."),
|
|
44
|
+
cwd: z
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe(
|
|
48
|
+
"Working directory (local runs only; teleport tracks its own cwd).",
|
|
49
|
+
),
|
|
50
|
+
timeout_sec: z
|
|
51
|
+
.number()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe(
|
|
54
|
+
"Max seconds before the command is killed (default 60, max 300).",
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
execute: (params, bridge) => bridge("native_bash", params),
|
|
58
|
+
tag: "native",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "read",
|
|
62
|
+
description:
|
|
63
|
+
"Read a file (with line numbers). Runs on the daemon host or the active teleport device. Supports offset/limit for large files.",
|
|
64
|
+
schema: {
|
|
65
|
+
path: z.string().describe("Absolute file path."),
|
|
66
|
+
offset: z.number().optional().describe("0-based line to start from."),
|
|
67
|
+
limit: z
|
|
68
|
+
.number()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("Max lines to return (default/max 2000)."),
|
|
71
|
+
},
|
|
72
|
+
execute: (params, bridge) => bridge("native_read", params),
|
|
73
|
+
tag: "native",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "write",
|
|
77
|
+
description:
|
|
78
|
+
"Write (create/overwrite) a file with the given content. Runs on the daemon host or the active teleport device.",
|
|
79
|
+
schema: {
|
|
80
|
+
path: z.string().describe("Absolute file path."),
|
|
81
|
+
content: z.string().describe("Full file content to write."),
|
|
82
|
+
},
|
|
83
|
+
execute: (params, bridge) => bridge("native_write", params),
|
|
84
|
+
tag: "native",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "edit",
|
|
88
|
+
description:
|
|
89
|
+
"Exact-string replacement in a file. old_string must be unique unless replace_all is set. Runs on the daemon host or the active teleport device.",
|
|
90
|
+
schema: {
|
|
91
|
+
path: z.string().describe("Absolute file path."),
|
|
92
|
+
old_string: z.string().describe("Exact text to replace."),
|
|
93
|
+
new_string: z.string().describe("Replacement text."),
|
|
94
|
+
replace_all: z
|
|
95
|
+
.boolean()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe("Replace every occurrence (default false)."),
|
|
98
|
+
},
|
|
99
|
+
execute: (params, bridge) => bridge("native_edit", params),
|
|
100
|
+
tag: "native",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "glob",
|
|
104
|
+
description:
|
|
105
|
+
"Find files matching a glob pattern (ripgrep-backed). Runs on the daemon host or the active teleport device.",
|
|
106
|
+
schema: {
|
|
107
|
+
pattern: z.string().describe('Glob pattern, e.g. "**/*.ts".'),
|
|
108
|
+
path: z
|
|
109
|
+
.string()
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("Root directory to search (default cwd)."),
|
|
112
|
+
},
|
|
113
|
+
execute: (params, bridge) => bridge("native_glob", params),
|
|
114
|
+
tag: "native",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "search",
|
|
118
|
+
description:
|
|
119
|
+
"Search file contents with a regex (ripgrep-backed). Runs on the daemon host or the active teleport device.",
|
|
120
|
+
schema: {
|
|
121
|
+
pattern: z.string().describe("Regex pattern to search for."),
|
|
122
|
+
path: z
|
|
123
|
+
.string()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("Root directory or file (default cwd)."),
|
|
126
|
+
glob: z
|
|
127
|
+
.string()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe('Filter files by glob, e.g. "*.ts".'),
|
|
130
|
+
case_insensitive: z
|
|
131
|
+
.boolean()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe("Case-insensitive match (default false)."),
|
|
134
|
+
},
|
|
135
|
+
execute: (params, bridge) => bridge("native_search", params),
|
|
136
|
+
tag: "native",
|
|
137
|
+
},
|
|
138
|
+
];
|
package/src/core/tools/types.ts
CHANGED
|
@@ -1176,6 +1176,8 @@ export function createNativeFrontend(
|
|
|
1176
1176
|
storeLocation: (body) => mesh.storeLocation(body),
|
|
1177
1177
|
listDevices: () => mesh.list(),
|
|
1178
1178
|
completeCommand: (body) => mesh.completeCommand(body),
|
|
1179
|
+
acceptFileUpload: (token, body) => mesh.acceptFileUpload(token, body),
|
|
1180
|
+
openFileDownload: (token) => mesh.openFileDownload(token),
|
|
1179
1181
|
};
|
|
1180
1182
|
|
|
1181
1183
|
const nativeCfg = config.native ?? { port: 19880, host: "127.0.0.1" };
|
|
@@ -117,6 +117,15 @@ export type BridgeServerHandlers = {
|
|
|
117
117
|
| Promise<{ devices: DeviceInfo[]; locations: DeviceLocation[] }>;
|
|
118
118
|
/** A device answered a device_command; true when a call was waiting. */
|
|
119
119
|
completeCommand(body: Record<string, unknown>): boolean;
|
|
120
|
+
/** A device streams a pull-transfer's file body up (raw request body). */
|
|
121
|
+
acceptFileUpload(
|
|
122
|
+
token: string,
|
|
123
|
+
body: IncomingMessage,
|
|
124
|
+
): Promise<{ ok: true; bytes: number } | { ok: false; error: string }>;
|
|
125
|
+
/** Resolve a push-transfer token to the file to stream down, or null. */
|
|
126
|
+
openFileDownload(
|
|
127
|
+
token: string,
|
|
128
|
+
): Promise<{ path: string; size: number } | null>;
|
|
120
129
|
};
|
|
121
130
|
|
|
122
131
|
const SSE_PING_MS = 25_000;
|
|
@@ -268,7 +277,7 @@ export class BridgeServer {
|
|
|
268
277
|
backend: s.backend,
|
|
269
278
|
model: s.model,
|
|
270
279
|
activeChats: s.activeChats,
|
|
271
|
-
capabilities: ["mesh", "mesh-commands"],
|
|
280
|
+
capabilities: ["mesh", "mesh-commands", "mesh-file-stream"],
|
|
272
281
|
});
|
|
273
282
|
}
|
|
274
283
|
|
|
@@ -394,6 +403,36 @@ export class BridgeServer {
|
|
|
394
403
|
});
|
|
395
404
|
}
|
|
396
405
|
|
|
406
|
+
// Streamed device file transfers (see core/mesh/transfers.ts). The
|
|
407
|
+
// one-time `transfer` token authorizes exactly one direction+path;
|
|
408
|
+
// these sit behind the bridge bearer auth like every device route.
|
|
409
|
+
if (path === "/devices/file") {
|
|
410
|
+
const token = url.searchParams.get("transfer") ?? "";
|
|
411
|
+
if (!token)
|
|
412
|
+
return this.json(res, 400, { ok: false, error: "transfer required" });
|
|
413
|
+
if (method === "POST") {
|
|
414
|
+
const result = await this.handlers.acceptFileUpload(token, req);
|
|
415
|
+
return this.json(res, result.ok ? 200 : 409, result);
|
|
416
|
+
}
|
|
417
|
+
if (method === "GET") {
|
|
418
|
+
const file = await this.handlers.openFileDownload(token);
|
|
419
|
+
if (!file)
|
|
420
|
+
return this.json(res, 404, {
|
|
421
|
+
ok: false,
|
|
422
|
+
error: "Unknown or already-used transfer token",
|
|
423
|
+
});
|
|
424
|
+
res.writeHead(200, {
|
|
425
|
+
"Content-Type": "application/octet-stream",
|
|
426
|
+
"Content-Length": String(file.size),
|
|
427
|
+
...this.corsHeaders(),
|
|
428
|
+
});
|
|
429
|
+
const stream = createReadStream(file.path);
|
|
430
|
+
stream.on("error", () => res.destroy());
|
|
431
|
+
stream.pipe(res);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
397
436
|
if (method === "POST" && path === "/upload") {
|
|
398
437
|
const filename = url.searchParams.get("filename") ?? "upload";
|
|
399
438
|
const contentType =
|
package/src/util/config.ts
CHANGED
|
@@ -422,6 +422,16 @@ const configSchema = z.object({
|
|
|
422
422
|
// Native — local bridge for the Electron companion app (apps/desktop)
|
|
423
423
|
native: nativeConfigSchema.optional(),
|
|
424
424
|
|
|
425
|
+
// Native tools — replace the SDK's built-in Read/Write/Edit/Bash/Glob/Grep
|
|
426
|
+
// with Talon's own MCP equivalents (bash/read/write/edit/glob/search). The
|
|
427
|
+
// native tools additionally route to the active `teleport` node, so file
|
|
428
|
+
// and shell operations can transparently run on a companion device. When
|
|
429
|
+
// true, the built-ins are dropped from the model's tool whitelist and the
|
|
430
|
+
// native tools take their place. Off by default: enabling swaps the model's
|
|
431
|
+
// own hands, so the cutover should be deliberate and observed (flip to true
|
|
432
|
+
// and restart), with an instant rollback by flipping back to false.
|
|
433
|
+
nativeTools: z.boolean().default(false),
|
|
434
|
+
|
|
425
435
|
// Display name shown in terminal UI (defaults to "Talon")
|
|
426
436
|
botDisplayName: z.string().default("Talon"),
|
|
427
437
|
|