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.
@@ -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
+ ];
@@ -27,7 +27,8 @@ export type ToolTag =
27
27
  | "web"
28
28
  | "admin"
29
29
  | "models"
30
- | "mesh";
30
+ | "mesh"
31
+ | "native";
31
32
 
32
33
  /** The bridge caller signature — injected into execute(). */
33
34
  export type BridgeFunction = (
@@ -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 =
@@ -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