talon-agent 1.53.0 → 2.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.
package/README.md CHANGED
@@ -24,7 +24,7 @@ Multi-platform agentic AI harness. Runs on **Telegram**, **Discord**, **Microsof
24
24
  | **Triggers** | Self-authored watcher scripts (bash/python/node) that wake the bot when conditions are met |
25
25
  | **Task table** | Every unit of agent work — chat turns, heartbeat, dream, isolated cron/trigger jobs — registered live; `talon ps` / `talon kill` |
26
26
  | **Event bus** | Typed internal pub-sub spine (task + turn lifecycle events); subsystems subscribe instead of importing each other; `talon events -f` |
27
- | **VFS** | Unified `talon://` namespace over workspace, skills, scripts, logs, plus /proc-style live views of the task table, event bus, and plugin registry; `talon ls` / `talon cat` |
27
+ | **VFS** | Unified `talon://` namespace over workspace, skills, scripts, logs, plus /proc-style live views of the task table, event bus, and plugin registry a real filesystem at `~/.talon/ns` (FUSE-backed live views), so plain `ls`/`cat` and every tool just work |
28
28
  | **Per-chat settings** | Model, effort level, and pulse toggle per conversation via inline keyboard |
29
29
  | **Model registry** | Models discovered from the active backend at startup — new models appear in all pickers automatically |
30
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talon-agent",
3
- "version": "1.53.0",
3
+ "version": "2.0.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",
@@ -89,7 +89,8 @@
89
89
  "build:driver": "node native/talon-driver/build.mjs",
90
90
  "build:driver:all": "node native/talon-driver/build.mjs --all",
91
91
  "build:warden": "node native/talon-warden/build.mjs",
92
- "build:napi": "node native/blake3-napi/build.mjs"
92
+ "build:napi": "node native/blake3-napi/build.mjs",
93
+ "build:fusefs": "node native/talon-fusefs/build.mjs"
93
94
  },
94
95
  "dependencies": {
95
96
  "@anthropic-ai/claude-agent-sdk": "^0.3.197",
package/src/app.ts CHANGED
@@ -24,6 +24,11 @@ import {
24
24
  import { shutdownTriggers } from "./core/background/triggers/index.js";
25
25
  import { startWatchdog, stopWatchdog } from "./util/watchdog.js";
26
26
  import { log, logError, logWarn } from "./util/log.js";
27
+ import {
28
+ getVfs,
29
+ mountNamespaceFs,
30
+ unmountNamespaceFs,
31
+ } from "./core/vfs/index.js";
27
32
  import { bootstrap, initBackendAndDispatcher } from "./bootstrap.js";
28
33
  import { Gateway } from "./core/engine/gateway.js";
29
34
  import type { Frontend } from "./bootstrap.js";
@@ -179,6 +184,7 @@ async function gracefulShutdown(signal: string): Promise<void> {
179
184
  await destroyPlugins();
180
185
  });
181
186
  }
187
+ await shutdownStep("fuse layer", unmountNamespaceFs);
182
188
  await shutdownStep("pulse timer", stopPulseTimer);
183
189
  await shutdownStep("heartbeat", async () => {
184
190
  stopHeartbeatTimer();
@@ -230,6 +236,11 @@ process.on("unhandledRejection", (reason) => {
230
236
  // ── Start ────────────────────────────────────────────────────────────────────
231
237
 
232
238
  async function main(): Promise<void> {
239
+ // Namespace on disk first: symlink farm always, live views (proc/,
240
+ // plugins/) via FUSE when the host can. Bounded and never-throwing —
241
+ // a host without FUSE boots identically, minus live views.
242
+ await mountNamespaceFs({ mode: config.fuse, vfs: getVfs() });
243
+
233
244
  await Promise.all(frontends.map((frontend) => frontend.init()));
234
245
  log("bot", "Starting Talon...");
235
246
 
package/src/cli/index.ts CHANGED
@@ -32,7 +32,6 @@ import { startChat } from "./chat.js";
32
32
  import { daemonStart, daemonStop, daemonRestart } from "./daemon.js";
33
33
  import { showTasks, killTask } from "./tasks.js";
34
34
  import { showEvents } from "./events.js";
35
- import { showLs, showCat } from "./fs.js";
36
35
  import { runPluginCommand } from "./plugin.js";
37
36
  import { runSkillCommand } from "./skill.js";
38
37
  import { mainMenu } from "./menu.js";
@@ -63,8 +62,6 @@ const CLI_COMMANDS = [
63
62
  "ps",
64
63
  "kill",
65
64
  "events",
66
- "ls",
67
- "cat",
68
65
  "plugin",
69
66
  "skill",
70
67
  ];
@@ -126,12 +123,6 @@ export async function runCli(): Promise<void> {
126
123
  });
127
124
  break;
128
125
  }
129
- case "ls":
130
- await showLs(process.argv[3]);
131
- break;
132
- case "cat":
133
- await showCat(process.argv[3]);
134
- break;
135
126
  case "plugin":
136
127
  await runPluginCommand(process.argv.slice(3));
137
128
  break;
@@ -162,12 +153,6 @@ export async function runCli(): Promise<void> {
162
153
  console.log(
163
154
  ` ${pc.cyan("events")} Tail the event bus (-f follows, --history [N] reads the journal)`,
164
155
  );
165
- console.log(
166
- ` ${pc.cyan("ls")} List the talon:// namespace (ls proc/tasks)`,
167
- );
168
- console.log(
169
- ` ${pc.cyan("cat")} Read a talon:// file (cat proc/events)`,
170
- );
171
156
  console.log(
172
157
  ` ${pc.cyan("plugin")} Manage plugins (install/enable/disable)`,
173
158
  );
@@ -395,6 +395,26 @@ export async function collectDoctorReport(opts: {
395
395
  );
396
396
  }
397
397
 
398
+ // Same optional contract for the namespace FUSE addon: absent means
399
+ // ~/.talon/ns is the symlink farm only (file mounts fine, no live
400
+ // proc//plugins views), so it's informational, not an issue.
401
+ {
402
+ const { nativeFuseFs } = await import("../native/fusefs.js");
403
+ const addon = nativeFuseFs();
404
+ checks.push(
405
+ addon
406
+ ? {
407
+ label: `Namespace FUSE: talon-fusefs addon v${addon.version()}`,
408
+ status: "ok",
409
+ }
410
+ : {
411
+ label: "Namespace FUSE: symlink farm only (no addon)",
412
+ status: "info",
413
+ detail: "npm run build:fusefs",
414
+ },
415
+ );
416
+ }
417
+
398
418
  // Package-owned prompt templates must be loadable. On a standalone
399
419
  // `bun build --compile` binary these are embedded file assets (no
400
420
  // source tree on disk); a failure here means the embed manifest is
@@ -17,7 +17,6 @@
17
17
  * - `plugins` — plugin hot-reload
18
18
  * - `models` — model / backend discovery
19
19
  * - `mesh` — companion device mesh (presence + location)
20
- * - `vfs` — the unified talon:// namespace (list/read/write)
21
20
  */
22
21
 
23
22
  import type { ActionResult } from "../../types.js";
@@ -34,7 +33,6 @@ import { pluginHandlers } from "./plugins.js";
34
33
  import { modelHandlers } from "./models.js";
35
34
  import { meshHandlers } from "./mesh.js";
36
35
  import { nativeHandlers } from "./native.js";
37
- import { vfsHandlers } from "./vfs.js";
38
36
 
39
37
  // Null-prototype so a request `action` of "toString" / "constructor" / etc.
40
38
  // can't resolve an inherited Object.prototype method — `handlers[action]` only
@@ -51,7 +49,6 @@ const handlers: SharedActionHandlers = Object.assign(Object.create(null), {
51
49
  ...modelHandlers,
52
50
  ...meshHandlers,
53
51
  ...nativeHandlers,
54
- ...vfsHandlers,
55
52
  });
56
53
 
57
54
  export async function handleSharedAction(
@@ -24,7 +24,7 @@ import {
24
24
  stat as fsStat,
25
25
  writeFile,
26
26
  } from "node:fs/promises";
27
- import { tmpdir } from "node:os";
27
+ import { homedir, tmpdir } from "node:os";
28
28
  import { dirname, extname, join } from "node:path";
29
29
  import { getMeshService } from "../../mesh/index.js";
30
30
  import {
@@ -33,7 +33,12 @@ import {
33
33
  setTeleport,
34
34
  setTeleportCwd,
35
35
  } from "../../mesh/teleport.js";
36
- import { getVfs } from "../../vfs/index.js";
36
+ import {
37
+ getVfs,
38
+ isNamespaceFsMounted,
39
+ resolveNamespacePath,
40
+ rewriteNamespaceRefs,
41
+ } from "../../vfs/index.js";
37
42
  import {
38
43
  clampExecOutput,
39
44
  createOutputCapture,
@@ -164,24 +169,61 @@ async function bash(
164
169
  timeoutSec: unknown,
165
170
  background?: unknown,
166
171
  ): Promise<Result> {
167
- const cmd = typeof command === "string" ? command : "";
172
+ let cmd = typeof command === "string" ? command : "";
168
173
  if (!cmd.trim()) return { ok: false, text: "No command given." };
169
174
  const timeoutMs = clampTimeout(timeoutSec);
170
175
  const active = await getTeleport(chatId);
171
- if (background === true) {
172
- if (active) {
173
- return {
174
- ok: false,
175
- text:
176
- "background:true runs on the daemon host only. On a teleported device, " +
177
- "background it in-shell instead: `cmd > /tmp/out.log 2>&1 &`, then poll " +
178
- "the log with read.",
179
- };
176
+
177
+ // talon:// references in the command translate to real paths before the
178
+ // shell ever sees them — teleported they can't (wrong address space).
179
+ let mappings: string[] = [];
180
+ if (cmd.includes("talon:") && !active) {
181
+ const rewritten = rewriteNamespaceRefs(
182
+ cmd,
183
+ getVfs(),
184
+ isNamespaceFsMounted(),
185
+ );
186
+ if (!rewritten.ok) return { ok: false, text: rewritten.error };
187
+ cmd = rewritten.command;
188
+ mappings = rewritten.mappings;
189
+ }
190
+ if (cmd.includes(NAMESPACE_SCHEME) && active) {
191
+ return {
192
+ ok: false,
193
+ text:
194
+ `talon:// names the daemon's namespace, but native tools are ` +
195
+ `teleported onto ${active.deviceName} — teleport_back first, or use a device path.`,
196
+ };
197
+ }
198
+
199
+ let dir = typeof cwd === "string" && cwd.trim() ? cwd.trim() : undefined;
200
+ if (dir !== undefined && !active) {
201
+ const resolved = resolvePathParam(dir, undefined);
202
+ if ("error" in resolved) return { ok: false, text: resolved.error };
203
+ dir = resolved.path;
204
+ }
205
+
206
+ const result = await (async () => {
207
+ if (background === true) {
208
+ if (active) {
209
+ return {
210
+ ok: false,
211
+ text:
212
+ "background:true runs on the daemon host only. On a teleported device, " +
213
+ "background it in-shell instead: `cmd > /tmp/out.log 2>&1 &`, then poll " +
214
+ "the log with read.",
215
+ };
216
+ }
217
+ return bashBackground(cmd, dir);
180
218
  }
181
- return bashBackground(cmd, typeof cwd === "string" ? cwd : undefined);
219
+ if (active) return bashTeleported(chatId, active.deviceId, cmd, timeoutMs);
220
+ return bashLocal(cmd, dir, timeoutMs);
221
+ })();
222
+ // Surface the translation so it's visible, never silent.
223
+ if (mappings.length > 0) {
224
+ return { ...result, text: `↪ ${mappings.join(" ")}\n${result.text}` };
182
225
  }
183
- if (active) return bashTeleported(chatId, active.deviceId, cmd, timeoutMs);
184
- return bashLocal(cmd, typeof cwd === "string" ? cwd : undefined, timeoutMs);
226
+ return result;
185
227
  }
186
228
 
187
229
  /**
@@ -452,59 +494,63 @@ async function bashTeleported(
452
494
 
453
495
  const NAMESPACE_SCHEME = "talon://";
454
496
 
455
- type AddressResolution =
456
- | { kind: "disk"; path: string }
457
- | { kind: "virtual" }
458
- | { kind: "error"; text: string };
459
-
460
497
  /**
461
- * Native fs tools accept talon:// addresses: `Vfs.locate` maps them to the
462
- * disk location the address names, and the tool then operates on the real
463
- * file. Synthetic nodes (proc/, plugins/) have no disk location reads are
464
- * served straight from the namespace, mutation refuses honestly. Teleport
465
- * can't cross address spaces: talon:// names daemon state, and a teleported
466
- * chat's paths belong to the device, so the combination is refused rather
467
- * than resolved against the wrong filesystem.
498
+ * talon:// addresses translate to real host paths (core/vfs/rewrite.ts)
499
+ * and then flow through the exact same fs code as any other path no
500
+ * virtual nodes, no special branches. With the FUSE layer mounted even
501
+ * proc/ and plugins/ are ordinary files; without it they are refused at
502
+ * resolution. Teleport can't cross address spaces: talon:// names
503
+ * daemon state, and a teleported chat's paths belong to the device, so
504
+ * the combination is refused rather than resolved against the wrong
505
+ * filesystem.
468
506
  */
469
- function resolveNamespaceAddress(
507
+ function resolveAddress(
470
508
  address: string,
471
509
  teleportedTo: string | undefined,
472
- ): AddressResolution {
510
+ ): { path: string } | { error: string } {
473
511
  if (teleportedTo !== undefined) {
474
512
  return {
475
- kind: "error",
476
- text:
513
+ error:
477
514
  `${address} names the daemon's namespace, but native tools are ` +
478
515
  `teleported onto ${teleportedTo} — teleport_back first, or use a device path.`,
479
516
  };
480
517
  }
481
- const located = getVfs().locate(address);
482
- if (!located.ok) {
483
- return {
484
- kind: "error",
485
- text: `Cannot resolve ${address}: ${located.error}${located.detail ? ` — ${located.detail}` : ""}`,
486
- };
518
+ const resolved = resolveNamespacePath(
519
+ address,
520
+ getVfs(),
521
+ isNamespaceFsMounted(),
522
+ );
523
+ return resolved.ok ? { path: resolved.path } : { error: resolved.error };
524
+ }
525
+
526
+ /** Expand a leading `~` — local runs only; a device's home is not ours. */
527
+ function expandHome(path: string): string {
528
+ if (path === "~") return homedir();
529
+ if (path.startsWith("~/")) return join(homedir(), path.slice(2));
530
+ return path;
531
+ }
532
+
533
+ /**
534
+ * One rule for every path parameter: talon:// translates, `~` expands
535
+ * (locally), everything else passes through untouched.
536
+ */
537
+ function resolvePathParam(
538
+ path: string,
539
+ teleportedTo: string | undefined,
540
+ ): { path: string } | { error: string } {
541
+ if (path.startsWith(NAMESPACE_SCHEME)) {
542
+ return resolveAddress(path, teleportedTo);
487
543
  }
488
- return located.value === undefined
489
- ? { kind: "virtual" }
490
- : { kind: "disk", path: located.value };
544
+ return { path: teleportedTo !== undefined ? path : expandHome(path) };
491
545
  }
492
546
 
493
- /** glob/search root: a talon:// address must name a disk-backed directory. */
547
+ /** glob/search root same rule, defaulting to the working directory. */
494
548
  function resolveSearchRoot(
495
549
  path: string | undefined,
496
550
  teleportedTo: string | undefined,
497
551
  ): { root: string } | { error: string } {
498
- const root = path ?? ".";
499
- if (!root.startsWith(NAMESPACE_SCHEME)) return { root };
500
- const resolved = resolveNamespaceAddress(root, teleportedTo);
501
- if (resolved.kind === "error") return { error: resolved.text };
502
- if (resolved.kind === "virtual") {
503
- return {
504
- error: `${root} is a synthetic view with no files on disk — browse it with vfs_list instead.`,
505
- };
506
- }
507
- return { root: resolved.path };
552
+ const resolved = resolvePathParam(path ?? ".", teleportedTo);
553
+ return "error" in resolved ? resolved : { root: resolved.path };
508
554
  }
509
555
 
510
556
  // ── read / write / edit ─────────────────────────────────────────────────────
@@ -520,33 +566,15 @@ async function read(
520
566
  const active = await getTeleport(chatId);
521
567
  const where = active ? active.deviceName : "local";
522
568
 
523
- let p = address;
524
- let virtualContent: string | undefined;
525
- if (address.startsWith(NAMESPACE_SCHEME)) {
526
- const resolved = resolveNamespaceAddress(address, active?.deviceName);
527
- if (resolved.kind === "error") return { ok: false, text: resolved.text };
528
- if (resolved.kind === "disk") {
529
- p = resolved.path;
530
- } else {
531
- const served = getVfs().read(address);
532
- if (!served.ok) {
533
- return {
534
- ok: false,
535
- text: `Cannot read ${address}: ${served.error}${served.detail ? ` — ${served.detail}` : ""}`,
536
- };
537
- }
538
- virtualContent = served.value;
539
- }
540
- }
569
+ const resolved = resolvePathParam(address, active?.deviceName);
570
+ if ("error" in resolved) return { ok: false, text: resolved.error };
571
+ const p = resolved.path;
541
572
  const shown = p === address ? p : `${address} → ${p}`;
542
573
 
543
574
  // Image files: return a viewable image block, not the raw bytes decoded as
544
575
  // UTF-8. Without this, `read`ing a photo/screenshot/design hands the model
545
576
  // mojibake and it can't see the picture at all.
546
- const mime =
547
- virtualContent === undefined
548
- ? IMAGE_MIME[extname(p).toLowerCase()]
549
- : undefined;
577
+ const mime = IMAGE_MIME[extname(p).toLowerCase()];
550
578
  if (mime) {
551
579
  let bytes: Buffer;
552
580
  if (active) {
@@ -582,9 +610,7 @@ async function read(
582
610
  const start = num(offset) ?? 0;
583
611
  const max = Math.min(num(limit) ?? MAX_READ_LINES, MAX_READ_LINES);
584
612
  let content: string;
585
- if (virtualContent !== undefined) {
586
- content = virtualContent;
587
- } else if (active) {
613
+ if (active) {
588
614
  const res = await getMeshService().readFileBytes(active.deviceId, p);
589
615
  if ("error" in res) return { ok: false, text: res.error };
590
616
  content = res.data.toString("utf8");
@@ -622,18 +648,9 @@ async function write(
622
648
  if (!address) return { ok: false, text: "A file path is required." };
623
649
  const body = typeof content === "string" ? content : "";
624
650
  const active = await getTeleport(chatId);
625
- let p = address;
626
- if (address.startsWith(NAMESPACE_SCHEME)) {
627
- const resolved = resolveNamespaceAddress(address, active?.deviceName);
628
- if (resolved.kind === "error") return { ok: false, text: resolved.text };
629
- if (resolved.kind === "virtual") {
630
- return {
631
- ok: false,
632
- text: `${address} is a synthetic view — there is no file on disk to write. Writable mounts: vfs_list "" shows where each lives.`,
633
- };
634
- }
635
- p = resolved.path;
636
- }
651
+ const resolved = resolvePathParam(address, active?.deviceName);
652
+ if ("error" in resolved) return { ok: false, text: resolved.error };
653
+ const p = resolved.path;
637
654
  const shown = p === address ? p : `${address} → ${p}`;
638
655
  if (active) {
639
656
  return getMeshService().writeFileToDevice(active.deviceId, p, body);
@@ -664,18 +681,9 @@ async function edit(
664
681
  if (from === to)
665
682
  return { ok: false, text: "old_string and new_string are identical." };
666
683
  const active = await getTeleport(chatId);
667
- let p = address;
668
- if (address.startsWith(NAMESPACE_SCHEME)) {
669
- const resolved = resolveNamespaceAddress(address, active?.deviceName);
670
- if (resolved.kind === "error") return { ok: false, text: resolved.text };
671
- if (resolved.kind === "virtual") {
672
- return {
673
- ok: false,
674
- text: `${address} is a synthetic view — there is no file on disk to edit. Read it with vfs_read instead.`,
675
- };
676
- }
677
- p = resolved.path;
678
- }
684
+ const resolved = resolvePathParam(address, active?.deviceName);
685
+ if ("error" in resolved) return { ok: false, text: resolved.error };
686
+ const p = resolved.path;
679
687
  const shown = p === address ? p : `${address} → ${p}`;
680
688
  const svc = getMeshService();
681
689
 
@@ -414,34 +414,6 @@ export class Gateway {
414
414
  return;
415
415
  }
416
416
 
417
- if (
418
- req.method === "GET" &&
419
- (req.url?.startsWith("/vfs/list") || req.url?.startsWith("/vfs/read"))
420
- ) {
421
- // The talon:// namespace — the transport for `talon ls` /
422
- // `talon cat`, which need the daemon's live synthetic mounts
423
- // (proc, plugins). Same 127.0.0.1 trust boundary as /action.
424
- const url = new URL(req.url, "http://gateway");
425
- const path = url.searchParams.get("path") ?? "";
426
- const { getVfs } = await import("../vfs/index.js");
427
- const vfs = getVfs();
428
- let result: Record<string, unknown>;
429
- if (url.pathname === "/vfs/list") {
430
- const listed = vfs.list(path);
431
- result = listed.ok
432
- ? { ok: true, entries: listed.value }
433
- : { ok: false, error: listed.error, detail: listed.detail };
434
- } else {
435
- const content = vfs.read(path);
436
- result = content.ok
437
- ? { ok: true, content: content.value }
438
- : { ok: false, error: content.error, detail: content.detail };
439
- }
440
- res.writeHead(200, { "Content-Type": "application/json" });
441
- res.end(JSON.stringify(result));
442
- return;
443
- }
444
-
445
417
  if (req.method === "GET" && req.url === "/tasks") {
446
418
  // The task table — every live/recent unit of agent work. Read by
447
419
  // `talon ps`. Same 127.0.0.1 trust boundary as /action.
@@ -23,7 +23,6 @@ import { adminTools } from "./admin.js";
23
23
  import { modelTools } from "./models.js";
24
24
  import { meshTools } from "./mesh.js";
25
25
  import { nativeTools } from "./native.js";
26
- import { vfsTools } from "./vfs.js";
27
26
 
28
27
  /** All built-in tool definitions. */
29
28
  export const ALL_TOOLS: readonly ToolDefinition[] = [
@@ -42,7 +41,6 @@ export const ALL_TOOLS: readonly ToolDefinition[] = [
42
41
  ...adminTools,
43
42
  ...modelTools,
44
43
  ...meshTools,
45
- ...vfsTools,
46
44
  ];
47
45
 
48
46
  /**
@@ -10,7 +10,16 @@ import type { ToolDefinition } from "./types.js";
10
10
  * Their extra power over the built-ins: every one honours the active
11
11
  * `teleport` target, so with a teleport engaged they run ON a companion
12
12
  * device (via the mesh exec/fs channel) instead of on the daemon host.
13
+ * And every one speaks talon:// — Talon's own namespace, which is a real
14
+ * filesystem location, so addresses translate to legitimate paths.
13
15
  */
16
+
17
+ /** One shared description of the namespace, stated once. */
18
+ const NAMESPACE_DOC =
19
+ "talon:// is Talon's namespace, on disk at ~/.talon/ns: home/ (the workspace), " +
20
+ "skills/, scripts/, logs/, and — while the FUSE layer is mounted — the live views " +
21
+ "proc/ (task table at proc/tasks/<id>, event ring at proc/events, both JSON) and " +
22
+ "plugins/ (registry).";
14
23
  export const nativeTools: ToolDefinition[] = [
15
24
  {
16
25
  name: "teleport",
@@ -40,14 +49,14 @@ export const nativeTools: ToolDefinition[] = [
40
49
  description:
41
50
  "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
51
  "Foreground runs are killed at timeout_sec — for streaming/never-ending commands (adb logcat, tail -f, dev servers, watchers) set background:true instead: the command is launched detached in its own process group with stdout+stderr captured to a log file, and the tool returns immediately with the pid + log path so you can poll the log and kill it when done. " +
43
- "talon:// addresses are not OS paths the shell needs the disk locations the namespace root listing shows (vfs_list).",
52
+ `${NAMESPACE_DOC} talon:// references in the command are translated to real paths before the shell runs (local only), so \`ls talon://home\` or \`cat talon://proc/events | jq\` just work.`,
44
53
  schema: {
45
54
  command: z.string().describe("The shell command to run."),
46
55
  cwd: z
47
56
  .string()
48
57
  .optional()
49
58
  .describe(
50
- "Working directory (local runs only; teleport tracks its own cwd).",
59
+ "Working directory (local runs only; teleport tracks its own cwd). talon:// accepted.",
51
60
  ),
52
61
  timeout_sec: z
53
62
  .number()
@@ -67,13 +76,12 @@ export const nativeTools: ToolDefinition[] = [
67
76
  },
68
77
  {
69
78
  name: "read",
70
- description:
71
- "Read a file (with line numbers). Runs on the daemon host or the active teleport device. Supports offset/limit for large files.",
79
+ description: `Read a file (with line numbers). Runs on the daemon host or the active teleport device. Supports offset/limit for large files. ${NAMESPACE_DOC}`,
72
80
  schema: {
73
81
  path: z
74
82
  .string()
75
83
  .describe(
76
- "Absolute file path, or a talon:// namespace address (resolved on the daemon host).",
84
+ "Absolute file path, ~ path, or a talon:// address (translated on the daemon host).",
77
85
  ),
78
86
  offset: z.number().optional().describe("0-based line to start from."),
79
87
  limit: z
@@ -87,12 +95,12 @@ export const nativeTools: ToolDefinition[] = [
87
95
  {
88
96
  name: "write",
89
97
  description:
90
- "Write (create/overwrite) a file with the given content. Runs on the daemon host or the active teleport device.",
98
+ "Write (create/overwrite) a file with the given content. Runs on the daemon host or the active teleport device. Writable talon:// mounts: home/, skills/, scripts/ (live views are read-only).",
91
99
  schema: {
92
100
  path: z
93
101
  .string()
94
102
  .describe(
95
- "Absolute file path, or a talon:// namespace address (resolved on the daemon host).",
103
+ "Absolute file path, ~ path, or a talon:// address (translated on the daemon host).",
96
104
  ),
97
105
  content: z.string().describe("Full file content to write."),
98
106
  },
@@ -107,7 +115,7 @@ export const nativeTools: ToolDefinition[] = [
107
115
  path: z
108
116
  .string()
109
117
  .describe(
110
- "Absolute file path, or a talon:// namespace address (resolved on the daemon host).",
118
+ "Absolute file path, ~ path, or a talon:// address (translated on the daemon host).",
111
119
  ),
112
120
  old_string: z.string().describe("Exact text to replace."),
113
121
  new_string: z.string().describe("Replacement text."),
@@ -28,8 +28,7 @@ export type ToolTag =
28
28
  | "admin"
29
29
  | "models"
30
30
  | "mesh"
31
- | "native"
32
- | "vfs";
31
+ | "native";
33
32
 
34
33
  /** The bridge caller signature — injected into execute(). */
35
34
  export type BridgeFunction = (