talon-agent 5.0.0 → 5.0.1
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 +1 -1
- package/src/app.ts +3 -3
- package/src/backend/claude-sdk/factory.ts +21 -41
- package/src/backend/claude-sdk/host/in-process.ts +147 -0
- package/src/backend/codex/mcp-config.ts +1 -1
- package/src/backend/openai-agents/mcp-pool.ts +1 -1
- package/src/backend/runtime/index.ts +2 -2
- package/src/backend/runtime/turn/delivery.ts +1 -1
- package/src/backend/runtime/turn/turn-phases.ts +1 -1
- package/src/bootstrap.ts +2 -2
- package/src/cli.ts +5 -2
- package/src/core/agent-runtime/README.md +15 -0
- package/src/core/agent-runtime/agent-host.ts +387 -0
- package/src/core/background/cron/scheduler.ts +1 -1
- package/src/core/background/triggers/command.ts +1 -1
- package/src/{util → core/config}/harden.ts +1 -1
- package/src/core/config/index.ts +1 -1
- package/src/core/daemon/pidfile.ts +1 -1
- package/src/core/daemon/resource-sampler.ts +2 -2
- package/src/{util → core/daemon}/respawn.ts +1 -1
- package/src/core/doctor/index.ts +1 -1
- package/src/core/engine/gateway-actions/fetch-url.ts +1 -1
- package/src/core/engine/gateway-actions/index.ts +1 -1
- package/src/core/engine/gateway-actions/memory.ts +2 -2
- package/src/core/engine/gateway-actions/native/exec-background.ts +129 -0
- package/src/core/engine/gateway-actions/native/exec-remote.ts +85 -0
- package/src/core/engine/gateway-actions/native/exec.ts +182 -0
- package/src/core/engine/gateway-actions/native/index.ts +46 -0
- package/src/core/engine/gateway-actions/native/params.ts +46 -0
- package/src/core/engine/gateway-actions/native/read.ts +182 -0
- package/src/core/engine/gateway-actions/native/results.ts +34 -0
- package/src/core/engine/gateway-actions/native/search.ts +212 -0
- package/src/core/engine/gateway-actions/native/shell.ts +39 -0
- package/src/core/engine/gateway-actions/native/teleport.ts +55 -0
- package/src/core/engine/gateway-actions/native/write.ts +170 -0
- package/src/core/frontend-runtime/builtins.ts +2 -2
- package/src/core/mcp-hub/index.ts +1 -1
- package/src/core/mcp-hub/talon-server.ts +1 -1
- package/src/core/memory/taps.ts +1 -1
- package/src/core/plugin/mcp.ts +1 -1
- package/src/core/scripts/lua.ts +1 -1
- package/src/core/tools/{chat.ts → chat/chat.ts} +1 -1
- package/src/core/tools/{cross-send.ts → chat/cross-send.ts} +1 -1
- package/src/core/tools/{history.ts → chat/history.ts} +2 -2
- package/src/core/tools/{media.ts → chat/media.ts} +1 -1
- package/src/core/tools/{members.ts → chat/members.ts} +2 -2
- package/src/core/tools/{messaging.ts → chat/messaging.ts} +2 -2
- package/src/core/tools/{moderation.ts → chat/moderation.ts} +2 -2
- package/src/core/tools/{stickers.ts → chat/stickers.ts} +2 -2
- package/src/core/tools/{whatsapp.ts → chat/whatsapp.ts} +1 -1
- package/src/core/tools/{memory.ts → content/memory.ts} +2 -2
- package/src/core/tools/{web.ts → content/web.ts} +1 -1
- package/src/core/tools/index.ts +20 -20
- package/src/core/tools/{admin.ts → ops/admin.ts} +1 -1
- package/src/core/tools/{bridge.ts → ops/bridge.ts} +2 -2
- package/src/core/tools/{goals.ts → ops/goals.ts} +2 -2
- package/src/core/tools/{mesh.ts → ops/mesh.ts} +1 -1
- package/src/core/tools/{models.ts → ops/models.ts} +1 -1
- package/src/core/tools/{native.ts → ops/native.ts} +1 -1
- package/src/core/tools/{scheduling.ts → ops/scheduling.ts} +1 -1
- package/src/core/tools/{scripts.ts → ops/scripts.ts} +1 -1
- package/src/core/tools/{skills.ts → ops/skills.ts} +1 -1
- package/src/core/tools/{triggers.ts → ops/triggers.ts} +1 -1
- package/src/{util → core/vfs}/workspace.ts +2 -2
- package/src/frontend/discord/callbacks/components/effort.ts +1 -1
- package/src/frontend/discord/callbacks/components/index.ts +1 -1
- package/src/frontend/discord/callbacks/components/settings.ts +1 -1
- package/src/frontend/discord/commands/admin.ts +1 -1
- package/src/frontend/discord/commands/info.ts +1 -1
- package/src/frontend/discord/commands/interaction.ts +1 -1
- package/src/frontend/discord/commands/session.ts +3 -3
- package/src/frontend/discord/commands/settings.ts +1 -1
- package/src/frontend/discord/handlers/messages.ts +1 -1
- package/src/frontend/discord/handlers/state.ts +1 -1
- package/src/frontend/discord/middleware.ts +1 -1
- package/src/frontend/discord/ready.ts +1 -1
- package/src/frontend/discord/render.ts +41 -259
- package/src/frontend/native/chats/chats.ts +1 -1
- package/src/frontend/native/surface/models.ts +1 -1
- package/src/frontend/native/turn/context.ts +1 -1
- package/src/frontend/native/turn/emit.ts +1 -1
- package/src/frontend/{shared → presentation}/format.ts +4 -3
- package/src/frontend/{shared → presentation}/reasoning-levels.ts +12 -0
- package/src/frontend/presentation/reports.ts +493 -0
- package/src/frontend/{shared → presentation}/session-status.ts +1 -1
- package/src/frontend/teams/commands.ts +2 -2
- package/src/frontend/teams/turn.ts +1 -1
- package/src/frontend/telegram/admin/health.ts +1 -1
- package/src/frontend/telegram/admin/sessions.ts +1 -1
- package/src/frontend/telegram/callbacks/effort.ts +2 -2
- package/src/frontend/telegram/callbacks/metrics.ts +1 -1
- package/src/frontend/telegram/callbacks/model/views.ts +1 -1
- package/src/frontend/telegram/callbacks/query.ts +1 -1
- package/src/frontend/telegram/callbacks/settings.ts +2 -2
- package/src/frontend/telegram/commands/admin.ts +4 -4
- package/src/frontend/telegram/commands/info.ts +2 -2
- package/src/frontend/telegram/commands/session.ts +4 -4
- package/src/frontend/telegram/commands/settings.ts +4 -2
- package/src/frontend/telegram/handlers/state.ts +1 -1
- package/src/frontend/telegram/model-menu.ts +1 -1
- package/src/frontend/telegram/render/html.ts +30 -0
- package/src/frontend/telegram/{helpers → render}/menu.ts +46 -19
- package/src/frontend/telegram/render/reports.ts +182 -0
- package/src/frontend/terminal/builtins/context.ts +2 -2
- package/src/frontend/terminal/builtins/session.ts +1 -1
- package/src/frontend/terminal/builtins/status.ts +2 -2
- package/src/frontend/terminal/index.ts +2 -2
- package/src/frontend/terminal/renderer.ts +1 -1
- package/src/frontend/whatsapp/commands.ts +5 -5
- package/src/frontend/whatsapp/registry.ts +1 -1
- package/src/index.ts +5 -2
- package/src/util/runtime.ts +1 -1
- package/src/core/engine/gateway-actions/native.ts +0 -1035
- package/src/frontend/telegram/helpers/diagnostics.ts +0 -428
- package/src/frontend/telegram/helpers/format.ts +0 -42
- package/src/frontend/telegram/helpers/index.ts +0 -13
- package/src/util/cleanup-registry.ts +0 -36
- /package/src/{util → core/daemon}/boot-timer.ts +0 -0
- /package/src/{util → core/frontend-runtime}/chat-id.ts +0 -0
- /package/src/{util/mcp-launcher.ts → core/mcp-hub/launcher.ts} +0 -0
- /package/src/{util → core/tools/content}/web-content.ts +0 -0
- /package/src/core/tools/{mcp-env.ts → ops/mcp-env.ts} +0 -0
- /package/src/{util → core/weaver}/session-name.ts +0 -0
- /package/src/frontend/{shared → presentation}/access.ts +0 -0
- /package/src/frontend/{shared → presentation}/model-commands.ts +0 -0
- /package/src/frontend/{shared → presentation}/plan-usage-report.ts +0 -0
- /package/src/frontend/{shared → presentation}/status-context.ts +0 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `native_glob` / `native_search` — find files by pattern, find text inside
|
|
3
|
+
* them.
|
|
4
|
+
*
|
|
5
|
+
* ripgrep is the fast path on both hosts; when it isn't installed the local
|
|
6
|
+
* runs fall back to a pure-JS walker rather than lying "no matches", and a
|
|
7
|
+
* teleported run falls back to the device's find/grep.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { glob as fsGlob, readFile, stat as fsStat } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { getTeleport } from "../../../mesh/teleport.js";
|
|
13
|
+
import { bashTeleported } from "./exec-remote.js";
|
|
14
|
+
import { resolvePathParam, str } from "./params.js";
|
|
15
|
+
import type { Result } from "./results.js";
|
|
16
|
+
import { runLocal, shellQuote } from "./shell.js";
|
|
17
|
+
import type { SharedActionHandlers } from "../types.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ripgrep binary, resolved from PATH (env-overridable for tests). NOT a
|
|
21
|
+
* hardcoded absolute path: /usr/bin/rg only exists on some Linux installs,
|
|
22
|
+
* and a missing binary must fall back loudly (or to the pure-JS walker),
|
|
23
|
+
* never masquerade as "no matches".
|
|
24
|
+
*/
|
|
25
|
+
function rgBin(): string {
|
|
26
|
+
return process.env.TALON_NATIVE_RG ?? "rg";
|
|
27
|
+
}
|
|
28
|
+
/** Caps for the pure-JS glob/search fallbacks (rg unavailable). */
|
|
29
|
+
const MAX_JS_RESULTS = 2_000;
|
|
30
|
+
const MAX_JS_FILE_BYTES = 2 * 1024 * 1024;
|
|
31
|
+
const SKIP_DIRS_RE = /(^|[\\/])(node_modules|\.git)([\\/]|$)/;
|
|
32
|
+
|
|
33
|
+
export const searchHandlers: SharedActionHandlers = {
|
|
34
|
+
native_glob: (body, chatId) => glob(chatId, body.pattern, body.path),
|
|
35
|
+
native_search: (body, chatId) =>
|
|
36
|
+
search(chatId, body.pattern, body.path, body.glob, body.case_insensitive),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
async function glob(
|
|
40
|
+
chatId: number,
|
|
41
|
+
pattern: unknown,
|
|
42
|
+
path: unknown,
|
|
43
|
+
): Promise<Result> {
|
|
44
|
+
const pat = str(pattern);
|
|
45
|
+
if (!pat) return { ok: false, text: "A glob pattern is required." };
|
|
46
|
+
const active = await getTeleport(chatId);
|
|
47
|
+
const root = resolvePathParam(str(path) ?? ".", active?.deviceName);
|
|
48
|
+
if (active) {
|
|
49
|
+
// Prefer rg on the device; fall back to find (basename patterns via
|
|
50
|
+
// -name, path patterns via -path). `command -v` gates the choice so a
|
|
51
|
+
// no-match rg exit (1) isn't misread as "rg missing, run find too".
|
|
52
|
+
const findExpr = pat.includes("/")
|
|
53
|
+
? `-type f -path ${shellQuote(`*${pat}`)}`
|
|
54
|
+
: `-type f -name ${shellQuote(pat)}`;
|
|
55
|
+
const cmd =
|
|
56
|
+
`if command -v rg >/dev/null 2>&1; ` +
|
|
57
|
+
`then rg --files -g ${shellQuote(pat)} ${shellQuote(root)}; ` +
|
|
58
|
+
`else find ${shellQuote(root)} ${findExpr} 2>/dev/null; fi`;
|
|
59
|
+
return bashTeleported(chatId, active.deviceId, cmd, 30_000);
|
|
60
|
+
}
|
|
61
|
+
const res = await runLocal(rgBin(), ["--files", "-g", pat, root]);
|
|
62
|
+
let files: string[];
|
|
63
|
+
if (res.code === 127) {
|
|
64
|
+
// rg not installed — pure-JS fallback rather than lying "no matches".
|
|
65
|
+
files = await globJs(pat, root);
|
|
66
|
+
} else if (res.code > 1 && !res.stdout.trim()) {
|
|
67
|
+
// exit 2 with output = partial results (e.g. permission-denied subdirs);
|
|
68
|
+
// exit 2 with none = a real error worth surfacing.
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
text: `glob failed: ${res.stderr.trim() || `ripgrep exit ${res.code}`}`,
|
|
72
|
+
};
|
|
73
|
+
} else {
|
|
74
|
+
files = res.stdout.trim().split("\n").filter(Boolean);
|
|
75
|
+
}
|
|
76
|
+
// rg's parallel directory walk (and the JS fallback's) emit in
|
|
77
|
+
// nondeterministic order — sort so identical calls render identically.
|
|
78
|
+
files.sort();
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
text: files.length
|
|
82
|
+
? `${files.length} match(es):\n${files.slice(0, 200).join("\n")}${files.length > 200 ? `\n… (${files.length - 200} more)` : ""}`
|
|
83
|
+
: `No files match ${pat} under ${root}.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function search(
|
|
88
|
+
chatId: number,
|
|
89
|
+
pattern: unknown,
|
|
90
|
+
path: unknown,
|
|
91
|
+
globPat: unknown,
|
|
92
|
+
caseInsensitive: unknown,
|
|
93
|
+
): Promise<Result> {
|
|
94
|
+
const pat = str(pattern);
|
|
95
|
+
if (!pat) return { ok: false, text: "A search pattern is required." };
|
|
96
|
+
const active = await getTeleport(chatId);
|
|
97
|
+
const root = resolvePathParam(str(path) ?? ".", active?.deviceName);
|
|
98
|
+
const g = str(globPat);
|
|
99
|
+
const ci = caseInsensitive === true;
|
|
100
|
+
// `-e` keeps a pattern that starts with "-" from being parsed as a flag
|
|
101
|
+
// (same idiom for rg and grep).
|
|
102
|
+
const flags = [
|
|
103
|
+
"-n",
|
|
104
|
+
"--color=never",
|
|
105
|
+
...(ci ? ["-i"] : []),
|
|
106
|
+
...(g ? ["-g", g] : []),
|
|
107
|
+
];
|
|
108
|
+
if (active) {
|
|
109
|
+
// Prefer rg on the device, fall back to grep (Android toybox has grep
|
|
110
|
+
// but rarely rg). --include is grep's closest analogue of -g.
|
|
111
|
+
const grepFlags = [
|
|
112
|
+
"-rn",
|
|
113
|
+
...(ci ? ["-i"] : []),
|
|
114
|
+
...(g ? [`--include=${g}`] : []),
|
|
115
|
+
];
|
|
116
|
+
const cmd =
|
|
117
|
+
`if command -v rg >/dev/null 2>&1; ` +
|
|
118
|
+
`then rg ${flags.map(shellQuote).join(" ")} -e ${shellQuote(pat)} ${shellQuote(root)}; ` +
|
|
119
|
+
`else grep ${grepFlags.map(shellQuote).join(" ")} -e ${shellQuote(pat)} ${shellQuote(root)} 2>/dev/null; fi`;
|
|
120
|
+
return bashTeleported(chatId, active.deviceId, cmd, 30_000);
|
|
121
|
+
}
|
|
122
|
+
const res = await runLocal(rgBin(), [...flags, "-e", pat, root]);
|
|
123
|
+
let lines: string[];
|
|
124
|
+
if (res.code === 127) {
|
|
125
|
+
// rg not installed — pure-JS fallback rather than lying "no matches".
|
|
126
|
+
try {
|
|
127
|
+
lines = await searchJs(pat, root, g, ci);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return { ok: false, text: `search failed: ${(err as Error).message}` };
|
|
130
|
+
}
|
|
131
|
+
} else if (res.code > 1 && !res.stdout.trim()) {
|
|
132
|
+
// exit 2 with output = partial results; exit 2 with none = real error.
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
text: `search failed: ${res.stderr.trim() || `ripgrep exit ${res.code}`}`,
|
|
136
|
+
};
|
|
137
|
+
} else {
|
|
138
|
+
const out = res.stdout.trim();
|
|
139
|
+
lines = out ? out.split("\n") : [];
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
text: lines.length
|
|
144
|
+
? `${lines.length} match line(s):\n${lines.slice(0, 200).join("\n")}${lines.length > 200 ? `\n… (${lines.length - 200} more)` : ""}`
|
|
145
|
+
: `No matches for ${pat} under ${root}.`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── pure-JS glob/search fallbacks (no ripgrep on the host) ──────────────────
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Glob without ripgrep, via node:fs `glob`. Mirrors rg's -g semantics for
|
|
153
|
+
* bare names (a pattern without "/" matches at any depth) and skips
|
|
154
|
+
* node_modules/.git, which rg would exclude via gitignore.
|
|
155
|
+
*/
|
|
156
|
+
async function globJs(pat: string, root: string): Promise<string[]> {
|
|
157
|
+
const pattern = pat.includes("/") ? pat : `**/${pat}`;
|
|
158
|
+
const out: string[] = [];
|
|
159
|
+
try {
|
|
160
|
+
for await (const entry of fsGlob(pattern, {
|
|
161
|
+
cwd: root,
|
|
162
|
+
exclude: (e: unknown) => {
|
|
163
|
+
const name =
|
|
164
|
+
typeof e === "string" ? e : ((e as { name?: string }).name ?? "");
|
|
165
|
+
return (
|
|
166
|
+
SKIP_DIRS_RE.test(name) || name === "node_modules" || name === ".git"
|
|
167
|
+
);
|
|
168
|
+
},
|
|
169
|
+
})) {
|
|
170
|
+
out.push(join(root, String(entry)));
|
|
171
|
+
if (out.length >= MAX_JS_RESULTS) break;
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// unreadable root etc. — empty result, caller reports "no matches"
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Content search without ripgrep: walk text files and regex-match lines. */
|
|
180
|
+
async function searchJs(
|
|
181
|
+
pat: string,
|
|
182
|
+
root: string,
|
|
183
|
+
globPat: string | undefined,
|
|
184
|
+
caseInsensitive: boolean,
|
|
185
|
+
): Promise<string[]> {
|
|
186
|
+
const re = new RegExp(pat, caseInsensitive ? "i" : "");
|
|
187
|
+
let files: string[];
|
|
188
|
+
try {
|
|
189
|
+
const st = await fsStat(root);
|
|
190
|
+
files = st.isFile() ? [root] : await globJs(globPat ?? "**/*", root);
|
|
191
|
+
} catch {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const lines: string[] = [];
|
|
195
|
+
for (const f of files) {
|
|
196
|
+
let content: string;
|
|
197
|
+
try {
|
|
198
|
+
const st = await fsStat(f);
|
|
199
|
+
if (!st.isFile() || st.size > MAX_JS_FILE_BYTES) continue;
|
|
200
|
+
content = await readFile(f, "utf8");
|
|
201
|
+
} catch {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (content.includes("\0")) continue; // binary
|
|
205
|
+
const fileLines = content.split("\n");
|
|
206
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
207
|
+
if (re.test(fileLines[i])) lines.push(`${f}:${i + 1}:${fileLines[i]}`);
|
|
208
|
+
if (lines.length >= MAX_JS_RESULTS) return lines;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return lines;
|
|
212
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Talking to a shell: POSIX single-quoting for a command Talon builds, and a
|
|
3
|
+
* plain local spawn that collects exit code + stdout + stderr (used for the
|
|
4
|
+
* ripgrep invocations behind glob/search).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { createOutputCapture } from "../../../../util/exec-output.js";
|
|
9
|
+
|
|
10
|
+
export function shellQuote(s: string): string {
|
|
11
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function runLocal(
|
|
15
|
+
bin: string,
|
|
16
|
+
args: string[],
|
|
17
|
+
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
18
|
+
return new Promise((resolvePromise) => {
|
|
19
|
+
const child = spawn(bin, args, { env: process.env });
|
|
20
|
+
const stdout = createOutputCapture();
|
|
21
|
+
const stderr = createOutputCapture();
|
|
22
|
+
child.stdout.on("data", stdout.push);
|
|
23
|
+
child.stderr.on("data", stderr.push);
|
|
24
|
+
child.on("error", () =>
|
|
25
|
+
resolvePromise({
|
|
26
|
+
code: 127,
|
|
27
|
+
stdout: stdout.value(),
|
|
28
|
+
stderr: stderr.value(),
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
child.on("close", (code) =>
|
|
32
|
+
resolvePromise({
|
|
33
|
+
code: code ?? 0,
|
|
34
|
+
stdout: stdout.value(),
|
|
35
|
+
stderr: stderr.value(),
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teleport control — which host the native tools in this directory target.
|
|
3
|
+
*
|
|
4
|
+
* teleport(device) → native tools run ON that companion device
|
|
5
|
+
* teleport_back() → native tools run on the daemon host again
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getMeshService } from "../../../mesh/index.js";
|
|
9
|
+
import { clearTeleport, setTeleport } from "../../../mesh/teleport.js";
|
|
10
|
+
import type { Result } from "./results.js";
|
|
11
|
+
import type { SharedActionHandlers } from "../types.js";
|
|
12
|
+
|
|
13
|
+
export const teleportHandlers: SharedActionHandlers = {
|
|
14
|
+
teleport: (body, chatId) => teleport(chatId, body.device),
|
|
15
|
+
teleport_back: (_body, chatId) => teleportBack(chatId),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
async function teleport(chatId: number, query: unknown): Promise<Result> {
|
|
19
|
+
const svc = getMeshService();
|
|
20
|
+
await svc.list();
|
|
21
|
+
const resolved = svc.resolveDevice(query);
|
|
22
|
+
if ("error" in resolved) return { ok: false, text: resolved.error };
|
|
23
|
+
const target = resolved.target;
|
|
24
|
+
if (!target.online) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
text: `${target.name} appears offline — cannot teleport onto a device that isn't connected.`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (target.capabilities && !target.capabilities.includes("exec")) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
text: `${target.name} does not advertise the "exec" capability, so teleport can't run commands on it.`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
await setTeleport(chatId, target.id, target.name);
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
text: [
|
|
40
|
+
`🛰️ Teleported onto ${target.name}. Native bash/read/write/edit/glob/search now run ON that device.`,
|
|
41
|
+
`Working dir starts at the device default; \`cd\` in bash persists across calls.`,
|
|
42
|
+
`Call teleport_back to return to the daemon host.`,
|
|
43
|
+
].join(" "),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function teleportBack(chatId: number): Promise<Result> {
|
|
48
|
+
const prior = await clearTeleport(chatId);
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
text: prior
|
|
52
|
+
? `↩️ Teleported back from ${prior.deviceName}. Native tools run on the daemon host again.`
|
|
53
|
+
: "Not teleported — native tools already run on the daemon host.",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `native_write` / `native_edit` — the file-mutating half of the native
|
|
3
|
+
* tools, local or teleported.
|
|
4
|
+
*
|
|
5
|
+
* `edit` splices literally (never String.replace, whose `$` patterns would
|
|
6
|
+
* corrupt shell snippets and regexes), refuses binary files, and points at
|
|
7
|
+
* the closest-looking line when old_string doesn't match byte-for-byte.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
import { getMeshService } from "../../../mesh/index.js";
|
|
13
|
+
import { getTeleport } from "../../../mesh/teleport.js";
|
|
14
|
+
import { resolvePathParam, str } from "./params.js";
|
|
15
|
+
import type { Result } from "./results.js";
|
|
16
|
+
import type { SharedActionHandlers } from "../types.js";
|
|
17
|
+
|
|
18
|
+
export const writeHandlers: SharedActionHandlers = {
|
|
19
|
+
native_write: (body, chatId) => write(chatId, body.path, body.content),
|
|
20
|
+
native_edit: (body, chatId) =>
|
|
21
|
+
edit(chatId, body.path, body.old_string, body.new_string, body.replace_all),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function write(
|
|
25
|
+
chatId: number,
|
|
26
|
+
path: unknown,
|
|
27
|
+
content: unknown,
|
|
28
|
+
): Promise<Result> {
|
|
29
|
+
const address = str(path);
|
|
30
|
+
if (!address) return { ok: false, text: "A file path is required." };
|
|
31
|
+
const body = typeof content === "string" ? content : "";
|
|
32
|
+
const active = await getTeleport(chatId);
|
|
33
|
+
const p = resolvePathParam(address, active?.deviceName);
|
|
34
|
+
const shown = p === address ? p : `${address} → ${p}`;
|
|
35
|
+
if (active) {
|
|
36
|
+
return getMeshService().writeFileToDevice(active.deviceId, p, body);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
await mkdir(dirname(p), { recursive: true });
|
|
40
|
+
await writeFile(p, body);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
text: `Cannot write ${shown}: ${(err as Error).message}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
text: `Wrote ${Buffer.byteLength(body, "utf8")} bytes to ${shown} [local].`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function edit(
|
|
54
|
+
chatId: number,
|
|
55
|
+
path: unknown,
|
|
56
|
+
oldString: unknown,
|
|
57
|
+
newString: unknown,
|
|
58
|
+
replaceAll: unknown,
|
|
59
|
+
): Promise<Result> {
|
|
60
|
+
const address = str(path);
|
|
61
|
+
if (!address) return { ok: false, text: "A file path is required." };
|
|
62
|
+
const from = typeof oldString === "string" ? oldString : "";
|
|
63
|
+
const to = typeof newString === "string" ? newString : "";
|
|
64
|
+
if (from === to)
|
|
65
|
+
return { ok: false, text: "old_string and new_string are identical." };
|
|
66
|
+
const active = await getTeleport(chatId);
|
|
67
|
+
const p = resolvePathParam(address, active?.deviceName);
|
|
68
|
+
const shown = p === address ? p : `${address} → ${p}`;
|
|
69
|
+
const svc = getMeshService();
|
|
70
|
+
|
|
71
|
+
let content: string;
|
|
72
|
+
if (active) {
|
|
73
|
+
const res = await svc.readFileBytes(active.deviceId, p);
|
|
74
|
+
if ("error" in res) return { ok: false, text: res.error };
|
|
75
|
+
content = res.data.toString("utf8");
|
|
76
|
+
} else {
|
|
77
|
+
try {
|
|
78
|
+
content = await readFile(p, "utf8");
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
text: `Cannot read ${shown}: ${(err as Error).message}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Binary guard: decoding binary as UTF-8 is lossy (invalid sequences
|
|
88
|
+
// become U+FFFD), so a read→replace→write round-trip would corrupt every
|
|
89
|
+
// non-text byte — even when old_string matches a clean region.
|
|
90
|
+
if (content.includes("\0")) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
text: `${shown} looks binary — refusing a text edit that would corrupt it. Use bash for byte-level changes.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const count = from ? content.split(from).length - 1 : 0;
|
|
98
|
+
if (count === 0) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
text: `old_string not found in ${shown}.${nearMissHint(content, from)}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (count > 1 && replaceAll !== true) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
text: `old_string appears ${count}× in ${shown}; pass replace_all or make it unique.`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Splice, not String.replace: replace() treats dollar-sign substitution
|
|
111
|
+
// patterns ($&, $', $BACKTICK, $$) in the replacement as directives, silently
|
|
112
|
+
// corrupting any new_string that contains them (shell snippets, regexes,
|
|
113
|
+
// Makefiles...). split/join and index-splice are both literal.
|
|
114
|
+
const updated =
|
|
115
|
+
replaceAll === true
|
|
116
|
+
? content.split(from).join(to)
|
|
117
|
+
: spliceOnce(content, from, to);
|
|
118
|
+
const firstLine = content.slice(0, content.indexOf(from)).split("\n").length;
|
|
119
|
+
|
|
120
|
+
if (active) {
|
|
121
|
+
const res = await svc.writeFileToDevice(active.deviceId, p, updated);
|
|
122
|
+
return res.ok
|
|
123
|
+
? {
|
|
124
|
+
ok: true,
|
|
125
|
+
text: `Edited ${shown} [${active.deviceName}] (${count} replacement${count === 1 ? "" : "s"}, first at line ${firstLine}).`,
|
|
126
|
+
}
|
|
127
|
+
: res;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await writeFile(p, updated);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
text: `Cannot write ${shown}: ${(err as Error).message}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
ok: true,
|
|
139
|
+
text: `Edited ${shown} [local] (${count} replacement${count === 1 ? "" : "s"}, first at line ${firstLine}).`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* When an edit's old_string doesn't match verbatim, the cause is almost
|
|
145
|
+
* always invisible: indentation, tabs vs spaces, or a trailing space. Point
|
|
146
|
+
* at the closest-looking line so the model re-reads that region instead of
|
|
147
|
+
* blindly retrying the same string.
|
|
148
|
+
*/
|
|
149
|
+
function nearMissHint(content: string, from: string): string {
|
|
150
|
+
const probe =
|
|
151
|
+
from
|
|
152
|
+
.split("\n")
|
|
153
|
+
.map((l) => l.trim())
|
|
154
|
+
.find((l) => l.length >= 8) ?? from.trim();
|
|
155
|
+
if (!probe) return "";
|
|
156
|
+
const lines = content.split("\n");
|
|
157
|
+
const idx = lines.findIndex((l) => l.trim().includes(probe));
|
|
158
|
+
if (idx === -1) return "";
|
|
159
|
+
return (
|
|
160
|
+
`\nLine ${idx + 1} looks close — whitespace/indentation must match the file byte-for-byte. ` +
|
|
161
|
+
`Re-read that region before retrying:\n${String(idx + 1).padStart(6)}\t${lines[idx]}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Replace the first occurrence of `from` with `to`, both taken literally. */
|
|
166
|
+
function spliceOnce(content: string, from: string, to: string): string {
|
|
167
|
+
const idx = content.indexOf(from);
|
|
168
|
+
if (idx === -1) return content;
|
|
169
|
+
return content.slice(0, idx) + to + content.slice(idx + from.length);
|
|
170
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* imports `src/frontend/factories.ts`.
|
|
8
8
|
*
|
|
9
9
|
* Chat-id shapes are the long-standing conventions from
|
|
10
|
-
*
|
|
10
|
+
* `./chat-id.ts`. Priorities encode the historical resolution order
|
|
11
11
|
* (terminal, native, teams, discord, telegram) — load-bearing in two
|
|
12
12
|
* places: telegram's numeric matcher is a near-catch-all so it must run
|
|
13
13
|
* last, and the terminal claims the legacy chat id "1" that telegram's
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
isTelegramChatId,
|
|
26
26
|
isTerminalChatId,
|
|
27
27
|
isWhatsAppChatId,
|
|
28
|
-
} from "
|
|
28
|
+
} from "./chat-id.js";
|
|
29
29
|
|
|
30
30
|
function registerBuiltinFrontends(): void {
|
|
31
31
|
registerFrontend({
|
|
@@ -38,7 +38,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
38
38
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
39
39
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
40
40
|
import { getPluginMcpServers } from "../plugin/index.js";
|
|
41
|
-
import { wrapMcpServer } from "
|
|
41
|
+
import { wrapMcpServer } from "./launcher.js";
|
|
42
42
|
import { log, logError } from "../../util/log.js";
|
|
43
43
|
import { buildTalonToolServer, VALID_TOOL_FRONTENDS } from "./talon-server.js";
|
|
44
44
|
import { buildProxyServer } from "./proxy-server.js";
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
18
|
import { composeTools } from "../tools/index.js";
|
|
19
|
-
import { createBridge, textResult } from "../tools/bridge.js";
|
|
19
|
+
import { createBridge, textResult } from "../tools/ops/bridge.js";
|
|
20
20
|
import type { ToolFrontend, ToolTag } from "../tools/types.js";
|
|
21
21
|
|
|
22
22
|
export const VALID_TOOL_FRONTENDS: ReadonlySet<string> = new Set([
|
package/src/core/memory/taps.ts
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
type MemorySource,
|
|
38
38
|
type MemoryTrust,
|
|
39
39
|
} from "../../storage/memory.js";
|
|
40
|
-
import { chatScope } from "
|
|
40
|
+
import { chatScope } from "../frontend-runtime/chat-id.js";
|
|
41
41
|
import { log, logWarn } from "../../util/log.js";
|
|
42
42
|
|
|
43
43
|
/**
|
package/src/core/plugin/mcp.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
import { logWarn } from "../../util/log.js";
|
|
9
|
-
import { wrapMcpServer } from "
|
|
9
|
+
import { wrapMcpServer } from "../mcp-hub/launcher.js";
|
|
10
10
|
import { isBunRuntime } from "../../util/runtime.js";
|
|
11
11
|
import { registry, reloadState } from "./registry.js";
|
|
12
12
|
import type { McpServerConfig } from "./types.js";
|
package/src/core/scripts/lua.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (kill/respawn, SIGTERM grace, log streaming, TALON_FIRE line scanning)
|
|
8
8
|
* is built around child processes. Lua therefore runs as `talon _lua-run
|
|
9
9
|
* <script>` — the same self-reinvocation shape MCP supervision uses
|
|
10
|
-
* (
|
|
10
|
+
* (core/mcp-hub/launcher.ts) — so it works identically for tsx source runs,
|
|
11
11
|
* npm installs, and bun-compiled binaries with no source tree on disk.
|
|
12
12
|
*
|
|
13
13
|
* Sandbox (verified empirically, not aspirational):
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import type { ToolDefinition } from "
|
|
7
|
-
import { snowflakeOrIdSchema } from "
|
|
6
|
+
import type { ToolDefinition } from "../types.js";
|
|
7
|
+
import { snowflakeOrIdSchema } from "../schemas.js";
|
|
8
8
|
|
|
9
9
|
export const historyTools: ToolDefinition[] = [
|
|
10
10
|
{
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import type { ToolDefinition } from "
|
|
7
|
-
import { snowflakeOrIdSchema } from "
|
|
6
|
+
import type { ToolDefinition } from "../types.js";
|
|
7
|
+
import { snowflakeOrIdSchema } from "../schemas.js";
|
|
8
8
|
|
|
9
9
|
export const memberTools: ToolDefinition[] = [
|
|
10
10
|
{
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import type { ToolDefinition } from "
|
|
8
|
-
import { chatIdSchema, idSchema, snowflakeOrIdSchema } from "
|
|
7
|
+
import type { ToolDefinition } from "../types.js";
|
|
8
|
+
import { chatIdSchema, idSchema, snowflakeOrIdSchema } from "../schemas.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Throw a typed Error when a bridge response reports failure
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import type { ToolDefinition } from "
|
|
9
|
-
import { chatIdSchema, snowflakeOrIdSchema } from "
|
|
8
|
+
import type { ToolDefinition } from "../types.js";
|
|
9
|
+
import { chatIdSchema, snowflakeOrIdSchema } from "../schemas.js";
|
|
10
10
|
|
|
11
11
|
export const moderationTools: ToolDefinition[] = [
|
|
12
12
|
{
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import type { ToolDefinition } from "
|
|
7
|
-
import { idSchema } from "
|
|
6
|
+
import type { ToolDefinition } from "../types.js";
|
|
7
|
+
import { idSchema } from "../schemas.js";
|
|
8
8
|
|
|
9
9
|
export const stickerTools: ToolDefinition[] = [
|
|
10
10
|
{
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { z } from "zod";
|
|
12
|
-
import type { ToolDefinition } from "
|
|
13
|
-
import type { MemoryKind } from "
|
|
12
|
+
import type { ToolDefinition } from "../types.js";
|
|
13
|
+
import type { MemoryKind } from "../../../storage/memory.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* The store's kinds, spelled out here rather than imported.
|