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 +1 -1
- package/package.json +3 -2
- package/src/app.ts +11 -0
- package/src/cli/index.ts +0 -15
- package/src/core/doctor.ts +20 -0
- package/src/core/engine/gateway-actions/index.ts +0 -3
- package/src/core/engine/gateway-actions/native.ts +108 -100
- package/src/core/engine/gateway.ts +0 -28
- package/src/core/tools/index.ts +0 -2
- package/src/core/tools/native.ts +16 -8
- package/src/core/tools/types.ts +1 -2
- package/src/core/vfs/fusefs.ts +269 -0
- package/src/core/vfs/index.ts +21 -2
- package/src/core/vfs/nsdir.ts +85 -0
- package/src/core/vfs/rewrite.ts +170 -0
- package/src/core/vfs/vfs.ts +34 -19
- package/src/native/fusefs.ts +97 -0
- package/src/util/config.ts +8 -0
- package/src/util/log.ts +1 -0
- package/src/util/paths.ts +7 -0
- package/src/cli/fs.ts +0 -138
- package/src/core/engine/gateway-actions/vfs.ts +0 -69
- package/src/core/tools/vfs.ts +0 -61
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
|
|
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": "
|
|
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
|
);
|
package/src/core/doctor.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
* can't cross address spaces: talon:// names
|
|
466
|
-
* chat's paths belong to the device, so
|
|
467
|
-
* than resolved against the wrong
|
|
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
|
|
507
|
+
function resolveAddress(
|
|
470
508
|
address: string,
|
|
471
509
|
teleportedTo: string | undefined,
|
|
472
|
-
):
|
|
510
|
+
): { path: string } | { error: string } {
|
|
473
511
|
if (teleportedTo !== undefined) {
|
|
474
512
|
return {
|
|
475
|
-
|
|
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
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
|
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
|
|
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
|
|
499
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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 (
|
|
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
|
-
|
|
626
|
-
if (
|
|
627
|
-
|
|
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
|
-
|
|
668
|
-
if (
|
|
669
|
-
|
|
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.
|
package/src/core/tools/index.ts
CHANGED
|
@@ -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
|
/**
|
package/src/core/tools/native.ts
CHANGED
|
@@ -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
|
-
|
|
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://
|
|
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://
|
|
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://
|
|
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."),
|