talon-agent 1.36.0 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/backend/claude-sdk/handler.ts +6 -2
- package/src/backend/codex/handler/events.ts +1 -1
- package/src/backend/codex/handler/message.ts +1 -0
- package/src/backend/kilo/handler/message.ts +1 -0
- package/src/backend/kilo/handler/turn.ts +1 -0
- package/src/backend/openai-agents/handler/events.ts +1 -1
- package/src/backend/openai-agents/handler/message.ts +5 -1
- package/src/backend/opencode/handler/message.ts +1 -0
- package/src/backend/opencode/handler/turn.ts +1 -0
- package/src/backend/remote-server/events.ts +3 -1
- package/src/backend/shared/metrics.ts +26 -51
- package/src/core/engine/gateway-actions/native.ts +89 -56
- package/src/core/mesh/service.ts +137 -32
- package/src/core/mesh/teleport.ts +86 -32
- package/src/core/tools/mesh.ts +1 -1
- package/src/core/tools/native.ts +1 -1
- package/src/frontend/discord/commands/admin.ts +9 -2
- package/src/frontend/discord/helpers.ts +7 -6
- package/src/frontend/native/chats.ts +2 -2
- package/src/frontend/telegram/commands/admin.ts +10 -2
- package/src/frontend/telegram/helpers/diagnostics.ts +7 -6
- package/src/storage/db.ts +6 -1
- package/src/storage/repositories/sessions-repo.ts +20 -1
- package/src/storage/sessions.ts +348 -4
- package/src/storage/sql/db.sql +5 -0
- package/src/storage/sql/schema.sql +2 -1
- package/src/storage/sql/sessions.sql +3 -3
- package/src/storage/sql/statements.generated.ts +8 -4
- package/src/util/exec-output.ts +64 -0
- package/src/util/metrics.ts +148 -60
package/package.json
CHANGED
|
@@ -336,7 +336,7 @@ export async function* runChatTurn(
|
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
for (const tool of result.tools) {
|
|
339
|
-
recordToolCall(tool.name, "claude");
|
|
339
|
+
recordToolCall(chatId, tool.name, "claude");
|
|
340
340
|
captureIntoState(tool.name, tool.input);
|
|
341
341
|
if (isTurnTerminator(tool.name, tool.input)) {
|
|
342
342
|
state.turnTerminated = true;
|
|
@@ -487,6 +487,7 @@ export async function* runChatTurn(
|
|
|
487
487
|
|
|
488
488
|
const durationMs = Date.now() - t0;
|
|
489
489
|
recordTurnMetrics({
|
|
490
|
+
chatId,
|
|
490
491
|
backend: "claude",
|
|
491
492
|
durationMs,
|
|
492
493
|
toolCalls: state.toolCalls,
|
|
@@ -543,7 +544,10 @@ export async function* runChatTurn(
|
|
|
543
544
|
: ({ violated: false } as const);
|
|
544
545
|
|
|
545
546
|
if (violation.violated) {
|
|
546
|
-
recordFlowViolation(
|
|
547
|
+
recordFlowViolation(
|
|
548
|
+
chatId,
|
|
549
|
+
violation.shouldRetry ? "retried" : "cap_exhausted",
|
|
550
|
+
);
|
|
547
551
|
log(
|
|
548
552
|
"agent",
|
|
549
553
|
`[${chatId}] flow violation: ${violation.reason}. ${
|
|
@@ -182,7 +182,7 @@ function recordCodexToolMetric(
|
|
|
182
182
|
// Shared vocabulary: `tool_calls.<bare>` (prefix-stripped, so codex
|
|
183
183
|
// MCP calls land on the same keys as every other backend) plus the
|
|
184
184
|
// `backend.codex.tool_calls` dimension.
|
|
185
|
-
recordToolCall(toolName, "codex");
|
|
185
|
+
recordToolCall(ctx.chatId, toolName, "codex");
|
|
186
186
|
ctx.codexToolMetrics.count += 1;
|
|
187
187
|
}
|
|
188
188
|
|
|
@@ -103,7 +103,7 @@ function handleToolCalled(item: unknown, ctx: HandleRunItemContext): void {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
recordToolCall(toolName, "openai-agents");
|
|
106
|
+
recordToolCall(ctx.chatId, toolName, "openai-agents");
|
|
107
107
|
recordToolUse(ctx.state, toolName, input);
|
|
108
108
|
|
|
109
109
|
if (ctx.onToolUse) {
|
|
@@ -398,6 +398,7 @@ export async function handleMessage(
|
|
|
398
398
|
const responseText = finalizeResponseText(streamState);
|
|
399
399
|
const durationMs = Date.now() - t0;
|
|
400
400
|
recordTurnMetrics({
|
|
401
|
+
chatId,
|
|
401
402
|
backend: "openai-agents",
|
|
402
403
|
durationMs,
|
|
403
404
|
toolCalls: streamState.toolCalls,
|
|
@@ -443,7 +444,10 @@ export async function handleMessage(
|
|
|
443
444
|
: ({ violated: false } as const);
|
|
444
445
|
|
|
445
446
|
if (violation.violated) {
|
|
446
|
-
recordFlowViolation(
|
|
447
|
+
recordFlowViolation(
|
|
448
|
+
chatId,
|
|
449
|
+
violation.shouldRetry ? "retried" : "cap_exhausted",
|
|
450
|
+
);
|
|
447
451
|
log(
|
|
448
452
|
"agent",
|
|
449
453
|
`[${chatId}] flow violation: trailing prose (${violation.trailing.length} chars) without end_turn/send. ${
|
|
@@ -63,6 +63,8 @@ export type ProcessEventOutcome =
|
|
|
63
63
|
|
|
64
64
|
/** Common context passed to the per-event helper. */
|
|
65
65
|
export interface EventProcessingContext {
|
|
66
|
+
/** Chat id for session-scoped metrics. */
|
|
67
|
+
chatId: string;
|
|
66
68
|
/** Session id we're scoped to — events for other sessions are dropped. */
|
|
67
69
|
sessionId: string;
|
|
68
70
|
/** Stream state accumulator (shared/). */
|
|
@@ -277,7 +279,7 @@ async function processPartUpdate(
|
|
|
277
279
|
}
|
|
278
280
|
// Count every tool the model calls — shared vocabulary across
|
|
279
281
|
// backends (prefix-stripped name + backend dimension).
|
|
280
|
-
recordToolCall(toolName, ctx.backendLabel.toLowerCase());
|
|
282
|
+
recordToolCall(ctx.chatId, toolName, ctx.backendLabel.toLowerCase());
|
|
281
283
|
recordToolUse(ctx.state, toolName, input);
|
|
282
284
|
if (ctx.onToolUse) {
|
|
283
285
|
try {
|
|
@@ -38,10 +38,14 @@
|
|
|
38
38
|
* registry. Never interpolate chat ids, model ids, or user input.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
import { incrementCounter, recordHistogram } from "../../util/metrics.js";
|
|
42
41
|
import { stripMcpPrefix } from "../../core/tools/index.js";
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
42
|
+
import type { TokenUsageSnapshot } from "./usage.js";
|
|
43
|
+
import {
|
|
44
|
+
clearLiveTurn,
|
|
45
|
+
recordSessionMetricEvent,
|
|
46
|
+
recordSessionMetrics,
|
|
47
|
+
recordUsage,
|
|
48
|
+
} from "../../storage/sessions.js";
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* Count one tool call. `toolName` may be raw from any backend —
|
|
@@ -49,13 +53,21 @@ import { clearLiveTurn, recordUsage } from "../../storage/sessions.js";
|
|
|
49
53
|
* (`talon-tools-123_send`), or bare — it is normalised so every
|
|
50
54
|
* backend lands on the same `tool_calls.<bare>` key.
|
|
51
55
|
*/
|
|
52
|
-
export function recordToolCall(
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
export function recordToolCall(
|
|
57
|
+
chatId: string,
|
|
58
|
+
toolName: string,
|
|
59
|
+
backend?: string,
|
|
60
|
+
): void {
|
|
61
|
+
recordSessionMetricEvent(chatId, {
|
|
62
|
+
toolCalls: 1,
|
|
63
|
+
toolName: stripMcpPrefix(toolName),
|
|
64
|
+
backend,
|
|
65
|
+
});
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
/** Per-turn rollup recorded once at the end of every chat turn. */
|
|
58
69
|
export type TurnMetricInputs = {
|
|
70
|
+
chatId: string;
|
|
59
71
|
/** Backend id, e.g. "claude", "codex", "kilo", "openai-agents", "opencode". */
|
|
60
72
|
backend: string;
|
|
61
73
|
durationMs: number;
|
|
@@ -78,45 +90,7 @@ export type TurnMetricInputs = {
|
|
|
78
90
|
* ("queries_total")` pairs (and codex's private `codex.*` family).
|
|
79
91
|
*/
|
|
80
92
|
export function recordTurnMetrics(inputs: TurnMetricInputs): void {
|
|
81
|
-
|
|
82
|
-
recordHistogram(
|
|
83
|
-
`backend.${inputs.backend}.response_latency_ms`,
|
|
84
|
-
inputs.durationMs,
|
|
85
|
-
);
|
|
86
|
-
incrementCounter("queries_total");
|
|
87
|
-
incrementCounter(`backend.${inputs.backend}.queries`);
|
|
88
|
-
|
|
89
|
-
if (inputs.failed) incrementCounter(`backend.${inputs.backend}.turn_failed`);
|
|
90
|
-
|
|
91
|
-
if (inputs.toolCalls !== undefined) {
|
|
92
|
-
recordHistogram("tool_calls_per_turn", inputs.toolCalls);
|
|
93
|
-
if (inputs.toolCalls > 0) incrementCounter("turns_with_tools_total");
|
|
94
|
-
}
|
|
95
|
-
if (inputs.apiCalls !== undefined && inputs.apiCalls > 0) {
|
|
96
|
-
incrementCounter("api_calls_total", inputs.apiCalls);
|
|
97
|
-
recordHistogram("api_calls_per_turn", inputs.apiCalls);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (inputs.usage) {
|
|
101
|
-
const u = inputs.usage;
|
|
102
|
-
const b = `backend.${inputs.backend}`;
|
|
103
|
-
incrementCounter("tokens.input_total", u.inputTokens);
|
|
104
|
-
incrementCounter("tokens.output_total", u.outputTokens);
|
|
105
|
-
incrementCounter("tokens.cache_read_total", u.cacheRead);
|
|
106
|
-
incrementCounter("tokens.cache_write_total", u.cacheWrite);
|
|
107
|
-
incrementCounter(`${b}.tokens.input`, u.inputTokens);
|
|
108
|
-
incrementCounter(`${b}.tokens.output`, u.outputTokens);
|
|
109
|
-
incrementCounter(`${b}.tokens.cache_read`, u.cacheRead);
|
|
110
|
-
incrementCounter(`${b}.tokens.cache_write`, u.cacheWrite);
|
|
111
|
-
// Only meaningful when the turn actually consumed input — an
|
|
112
|
-
// all-zero snapshot (failed turn, usage-less backend) would skew
|
|
113
|
-
// the distribution with hard zeros.
|
|
114
|
-
if (u.inputTokens + u.cacheRead > 0) {
|
|
115
|
-
const pct = cacheHitPercent(u);
|
|
116
|
-
recordHistogram("cache_hit_percent", pct);
|
|
117
|
-
recordHistogram(`${b}.cache_hit_percent`, pct);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
93
|
+
recordSessionMetrics(inputs.chatId, inputs);
|
|
120
94
|
}
|
|
121
95
|
|
|
122
96
|
/**
|
|
@@ -152,6 +126,7 @@ export function recordFailedTurnAccounting(inputs: {
|
|
|
152
126
|
contextWindow?: number;
|
|
153
127
|
}): void {
|
|
154
128
|
recordTurnMetrics({
|
|
129
|
+
chatId: inputs.chatId,
|
|
155
130
|
backend: inputs.backend,
|
|
156
131
|
durationMs: inputs.durationMs,
|
|
157
132
|
toolCalls: inputs.toolCalls,
|
|
@@ -188,12 +163,12 @@ export function recordFailedTurnAccounting(inputs: {
|
|
|
188
163
|
* reminder" unanswerable for that backend.
|
|
189
164
|
*/
|
|
190
165
|
export function recordFlowViolation(
|
|
166
|
+
chatId: string,
|
|
191
167
|
outcome: "retried" | "cap_exhausted",
|
|
192
168
|
): void {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
outcome === "retried"
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
);
|
|
169
|
+
recordSessionMetricEvent(chatId, {
|
|
170
|
+
trailingTextDropped: 1,
|
|
171
|
+
flowViolationRetries: outcome === "retried" ? 1 : 0,
|
|
172
|
+
flowViolationCapExhausted: outcome === "cap_exhausted" ? 1 : 0,
|
|
173
|
+
});
|
|
199
174
|
}
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Native tools — Talon's own shell/filesystem tools, replacing the SDK's
|
|
3
3
|
* built-in Bash/Read/Write/Edit/Glob/Grep when `config.nativeTools` is on.
|
|
4
4
|
*
|
|
5
|
-
* Their defining feature: every one checks the
|
|
6
|
-
* no teleport, they run on the daemon
|
|
7
|
-
* ripgrep). With a teleport engaged, they
|
|
8
|
-
* mesh exec/fs channel — so
|
|
9
|
-
*
|
|
5
|
+
* Their defining feature: every one checks the current chat's active
|
|
6
|
+
* `teleport` target. With no teleport for that chat, they run on the daemon
|
|
7
|
+
* host (local spawn / local fs / local ripgrep). With a teleport engaged, they
|
|
8
|
+
* run ON the companion device via the mesh exec/fs channel — so
|
|
9
|
+
* `bash`/`read`/`write`/… transparently operate on the phone for that chat.
|
|
10
10
|
*
|
|
11
11
|
* teleport(device) → native tools target that device
|
|
12
12
|
* teleport_back() → native tools run locally again
|
|
@@ -31,6 +31,10 @@ import {
|
|
|
31
31
|
setTeleport,
|
|
32
32
|
setTeleportCwd,
|
|
33
33
|
} from "../../mesh/teleport.js";
|
|
34
|
+
import {
|
|
35
|
+
clampExecOutput,
|
|
36
|
+
createOutputCapture,
|
|
37
|
+
} from "../../../util/exec-output.js";
|
|
34
38
|
import type { SharedActionHandlers } from "./types.js";
|
|
35
39
|
|
|
36
40
|
type Result = { ok: boolean; text: string };
|
|
@@ -56,33 +60,28 @@ const CWD_OPEN = "__TALON_CWD__";
|
|
|
56
60
|
const CWD_CLOSE = "__TALON_CWD_END__";
|
|
57
61
|
|
|
58
62
|
export const nativeHandlers: SharedActionHandlers = {
|
|
59
|
-
teleport: (body) => teleport(body.device),
|
|
60
|
-
teleport_back: () => teleportBack(),
|
|
61
|
-
native_bash: (body) =>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
teleport: (body, chatId) => teleport(chatId, body.device),
|
|
64
|
+
teleport_back: (_body, chatId) => teleportBack(chatId),
|
|
65
|
+
native_bash: (body, chatId) =>
|
|
66
|
+
bash(chatId, body.command, body.cwd, body.timeout_sec),
|
|
67
|
+
native_read: (body, chatId) =>
|
|
68
|
+
read(chatId, body.path, body.offset, body.limit),
|
|
69
|
+
native_write: (body, chatId) => write(chatId, body.path, body.content),
|
|
70
|
+
native_edit: (body, chatId) =>
|
|
71
|
+
edit(chatId, body.path, body.old_string, body.new_string, body.replace_all),
|
|
72
|
+
native_glob: (body, chatId) => glob(chatId, body.pattern, body.path),
|
|
73
|
+
native_search: (body, chatId) =>
|
|
74
|
+
search(chatId, body.pattern, body.path, body.glob, body.case_insensitive),
|
|
69
75
|
};
|
|
70
76
|
|
|
71
77
|
// ── Teleport control ────────────────────────────────────────────────────────
|
|
72
78
|
|
|
73
|
-
async function teleport(query: unknown): Promise<Result> {
|
|
79
|
+
async function teleport(chatId: number, query: unknown): Promise<Result> {
|
|
74
80
|
const svc = getMeshService();
|
|
75
81
|
await svc.list();
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
ok: false,
|
|
80
|
-
text:
|
|
81
|
-
typeof query === "string" && query.trim()
|
|
82
|
-
? `No mesh device matches "${query}". Use list_devices to see them.`
|
|
83
|
-
: "No mesh device to teleport to. Register a companion first.",
|
|
84
|
-
};
|
|
85
|
-
}
|
|
82
|
+
const resolved = svc.resolveDevice(query);
|
|
83
|
+
if ("error" in resolved) return { ok: false, text: resolved.error };
|
|
84
|
+
const target = resolved.target;
|
|
86
85
|
if (!target.online) {
|
|
87
86
|
return {
|
|
88
87
|
ok: false,
|
|
@@ -95,7 +94,7 @@ async function teleport(query: unknown): Promise<Result> {
|
|
|
95
94
|
text: `${target.name} does not advertise the "exec" capability, so teleport can't run commands on it.`,
|
|
96
95
|
};
|
|
97
96
|
}
|
|
98
|
-
await setTeleport(target.id, target.name);
|
|
97
|
+
await setTeleport(chatId, target.id, target.name);
|
|
99
98
|
return {
|
|
100
99
|
ok: true,
|
|
101
100
|
text: [
|
|
@@ -106,8 +105,8 @@ async function teleport(query: unknown): Promise<Result> {
|
|
|
106
105
|
};
|
|
107
106
|
}
|
|
108
107
|
|
|
109
|
-
async function teleportBack(): Promise<Result> {
|
|
110
|
-
const prior = await clearTeleport();
|
|
108
|
+
async function teleportBack(chatId: number): Promise<Result> {
|
|
109
|
+
const prior = await clearTeleport(chatId);
|
|
111
110
|
return {
|
|
112
111
|
ok: true,
|
|
113
112
|
text: prior
|
|
@@ -119,6 +118,7 @@ async function teleportBack(): Promise<Result> {
|
|
|
119
118
|
// ── bash ────────────────────────────────────────────────────────────────────
|
|
120
119
|
|
|
121
120
|
async function bash(
|
|
121
|
+
chatId: number,
|
|
122
122
|
command: unknown,
|
|
123
123
|
cwd: unknown,
|
|
124
124
|
timeoutSec: unknown,
|
|
@@ -126,8 +126,8 @@ async function bash(
|
|
|
126
126
|
const cmd = typeof command === "string" ? command : "";
|
|
127
127
|
if (!cmd.trim()) return { ok: false, text: "No command given." };
|
|
128
128
|
const timeoutMs = clampTimeout(timeoutSec);
|
|
129
|
-
const active = await getTeleport();
|
|
130
|
-
if (active) return bashTeleported(active.deviceId, cmd, timeoutMs);
|
|
129
|
+
const active = await getTeleport(chatId);
|
|
130
|
+
if (active) return bashTeleported(chatId, active.deviceId, cmd, timeoutMs);
|
|
131
131
|
return bashLocal(cmd, typeof cwd === "string" ? cwd : undefined, timeoutMs);
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -145,8 +145,8 @@ function bashLocal(
|
|
|
145
145
|
env: process.env,
|
|
146
146
|
detached,
|
|
147
147
|
});
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
const stdout = createOutputCapture();
|
|
149
|
+
const stderr = createOutputCapture();
|
|
150
150
|
let killed = false;
|
|
151
151
|
const timer = setTimeout(() => {
|
|
152
152
|
killed = true;
|
|
@@ -160,8 +160,8 @@ function bashLocal(
|
|
|
160
160
|
}
|
|
161
161
|
child.kill("SIGKILL");
|
|
162
162
|
}, timeoutMs);
|
|
163
|
-
child.stdout.on("data",
|
|
164
|
-
child.stderr.on("data",
|
|
163
|
+
child.stdout.on("data", stdout.push);
|
|
164
|
+
child.stderr.on("data", stderr.push);
|
|
165
165
|
child.on("error", (err) => {
|
|
166
166
|
clearTimeout(timer);
|
|
167
167
|
resolvePromise({ ok: false, text: `Failed to run: ${err.message}` });
|
|
@@ -173,18 +173,19 @@ function bashLocal(
|
|
|
173
173
|
: `exit ${code ?? 0}`;
|
|
174
174
|
resolvePromise({
|
|
175
175
|
ok: !killed && (code ?? 0) === 0,
|
|
176
|
-
text: renderExec("local", exit, stdout, stderr),
|
|
176
|
+
text: renderExec("local", exit, stdout.value(), stderr.value()),
|
|
177
177
|
});
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
async function bashTeleported(
|
|
183
|
+
chatId: number,
|
|
183
184
|
deviceId: string,
|
|
184
185
|
cmd: string,
|
|
185
186
|
timeoutMs: number,
|
|
186
187
|
): Promise<Result> {
|
|
187
|
-
const active = await getTeleport();
|
|
188
|
+
const active = await getTeleport(chatId);
|
|
188
189
|
const cwd = active?.cwd;
|
|
189
190
|
// Wrap so the resulting working dir is reported back and persists across
|
|
190
191
|
// calls (a `cd` in `cmd` carries forward), while the real exit code is
|
|
@@ -204,6 +205,8 @@ async function bashTeleported(
|
|
|
204
205
|
const data = result.data ?? {};
|
|
205
206
|
let stdout = typeof data.stdout === "string" ? data.stdout : "";
|
|
206
207
|
const stderr = typeof data.stderr === "string" ? data.stderr : "";
|
|
208
|
+
const via =
|
|
209
|
+
typeof data.via === "string" && data.via ? ` via ${data.via}` : "";
|
|
207
210
|
const exitCode =
|
|
208
211
|
typeof data.exitCode === "number" ? data.exitCode : undefined;
|
|
209
212
|
// Recover + strip the trailing cwd marker.
|
|
@@ -213,7 +216,7 @@ async function bashTeleported(
|
|
|
213
216
|
if (close !== -1) {
|
|
214
217
|
const newCwd = stdout.slice(open + CWD_OPEN.length, close).trim();
|
|
215
218
|
stdout = stdout.slice(0, open);
|
|
216
|
-
if (newCwd) await setTeleportCwd(newCwd);
|
|
219
|
+
if (newCwd) await setTeleportCwd(chatId, newCwd);
|
|
217
220
|
}
|
|
218
221
|
}
|
|
219
222
|
if (!result.ok && exitCode === undefined) {
|
|
@@ -224,13 +227,19 @@ async function bashTeleported(
|
|
|
224
227
|
}
|
|
225
228
|
return {
|
|
226
229
|
ok: exitCode === 0,
|
|
227
|
-
text: renderExec(
|
|
230
|
+
text: renderExec(
|
|
231
|
+
`${target.name}${via}`,
|
|
232
|
+
`exit ${exitCode ?? "?"}`,
|
|
233
|
+
stdout,
|
|
234
|
+
stderr,
|
|
235
|
+
),
|
|
228
236
|
};
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
// ── read / write / edit ─────────────────────────────────────────────────────
|
|
232
240
|
|
|
233
241
|
async function read(
|
|
242
|
+
chatId: number,
|
|
234
243
|
path: unknown,
|
|
235
244
|
offset: unknown,
|
|
236
245
|
limit: unknown,
|
|
@@ -239,7 +248,7 @@ async function read(
|
|
|
239
248
|
if (!p) return { ok: false, text: "A file path is required." };
|
|
240
249
|
const start = num(offset) ?? 0;
|
|
241
250
|
const max = Math.min(num(limit) ?? MAX_READ_LINES, MAX_READ_LINES);
|
|
242
|
-
const active = await getTeleport();
|
|
251
|
+
const active = await getTeleport(chatId);
|
|
243
252
|
let content: string;
|
|
244
253
|
let where: string;
|
|
245
254
|
if (active) {
|
|
@@ -270,11 +279,15 @@ async function read(
|
|
|
270
279
|
};
|
|
271
280
|
}
|
|
272
281
|
|
|
273
|
-
async function write(
|
|
282
|
+
async function write(
|
|
283
|
+
chatId: number,
|
|
284
|
+
path: unknown,
|
|
285
|
+
content: unknown,
|
|
286
|
+
): Promise<Result> {
|
|
274
287
|
const p = str(path);
|
|
275
288
|
if (!p) return { ok: false, text: "A file path is required." };
|
|
276
289
|
const body = typeof content === "string" ? content : "";
|
|
277
|
-
const active = await getTeleport();
|
|
290
|
+
const active = await getTeleport(chatId);
|
|
278
291
|
if (active) {
|
|
279
292
|
return getMeshService().writeFileToDevice(active.deviceId, p, body);
|
|
280
293
|
}
|
|
@@ -288,6 +301,7 @@ async function write(path: unknown, content: unknown): Promise<Result> {
|
|
|
288
301
|
}
|
|
289
302
|
|
|
290
303
|
async function edit(
|
|
304
|
+
chatId: number,
|
|
291
305
|
path: unknown,
|
|
292
306
|
oldString: unknown,
|
|
293
307
|
newString: unknown,
|
|
@@ -299,7 +313,7 @@ async function edit(
|
|
|
299
313
|
const to = typeof newString === "string" ? newString : "";
|
|
300
314
|
if (from === to)
|
|
301
315
|
return { ok: false, text: "old_string and new_string are identical." };
|
|
302
|
-
const active = await getTeleport();
|
|
316
|
+
const active = await getTeleport(chatId);
|
|
303
317
|
const svc = getMeshService();
|
|
304
318
|
|
|
305
319
|
let content: string;
|
|
@@ -350,11 +364,15 @@ async function edit(
|
|
|
350
364
|
|
|
351
365
|
// ── glob / search ───────────────────────────────────────────────────────────
|
|
352
366
|
|
|
353
|
-
async function glob(
|
|
367
|
+
async function glob(
|
|
368
|
+
chatId: number,
|
|
369
|
+
pattern: unknown,
|
|
370
|
+
path: unknown,
|
|
371
|
+
): Promise<Result> {
|
|
354
372
|
const pat = str(pattern);
|
|
355
373
|
if (!pat) return { ok: false, text: "A glob pattern is required." };
|
|
356
374
|
const root = str(path) ?? ".";
|
|
357
|
-
const active = await getTeleport();
|
|
375
|
+
const active = await getTeleport(chatId);
|
|
358
376
|
if (active) {
|
|
359
377
|
// Prefer rg on the device; fall back to find (basename patterns via
|
|
360
378
|
// -name, path patterns via -path). `command -v` gates the choice so a
|
|
@@ -366,7 +384,7 @@ async function glob(pattern: unknown, path: unknown): Promise<Result> {
|
|
|
366
384
|
`if command -v rg >/dev/null 2>&1; ` +
|
|
367
385
|
`then rg --files -g ${shellQuote(pat)} ${shellQuote(root)}; ` +
|
|
368
386
|
`else find ${shellQuote(root)} ${findExpr} 2>/dev/null; fi`;
|
|
369
|
-
return bashTeleported(active.deviceId, cmd, 30_000);
|
|
387
|
+
return bashTeleported(chatId, active.deviceId, cmd, 30_000);
|
|
370
388
|
}
|
|
371
389
|
const res = await runLocal(rgBin(), ["--files", "-g", pat, root]);
|
|
372
390
|
let files: string[];
|
|
@@ -392,6 +410,7 @@ async function glob(pattern: unknown, path: unknown): Promise<Result> {
|
|
|
392
410
|
}
|
|
393
411
|
|
|
394
412
|
async function search(
|
|
413
|
+
chatId: number,
|
|
395
414
|
pattern: unknown,
|
|
396
415
|
path: unknown,
|
|
397
416
|
globPat: unknown,
|
|
@@ -410,7 +429,7 @@ async function search(
|
|
|
410
429
|
...(ci ? ["-i"] : []),
|
|
411
430
|
...(g ? ["-g", g] : []),
|
|
412
431
|
];
|
|
413
|
-
const active = await getTeleport();
|
|
432
|
+
const active = await getTeleport(chatId);
|
|
414
433
|
if (active) {
|
|
415
434
|
// Prefer rg on the device, fall back to grep (Android toybox has grep
|
|
416
435
|
// but rarely rg). --include is grep's closest analogue of -g.
|
|
@@ -423,7 +442,7 @@ async function search(
|
|
|
423
442
|
`if command -v rg >/dev/null 2>&1; ` +
|
|
424
443
|
`then rg ${flags.map(shellQuote).join(" ")} -e ${shellQuote(pat)} ${shellQuote(root)}; ` +
|
|
425
444
|
`else grep ${grepFlags.map(shellQuote).join(" ")} -e ${shellQuote(pat)} ${shellQuote(root)} 2>/dev/null; fi`;
|
|
426
|
-
return bashTeleported(active.deviceId, cmd, 30_000);
|
|
445
|
+
return bashTeleported(chatId, active.deviceId, cmd, 30_000);
|
|
427
446
|
}
|
|
428
447
|
const res = await runLocal(rgBin(), [...flags, "-e", pat, root]);
|
|
429
448
|
let lines: string[];
|
|
@@ -525,13 +544,23 @@ function runLocal(
|
|
|
525
544
|
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
526
545
|
return new Promise((resolvePromise) => {
|
|
527
546
|
const child = spawn(bin, args, { env: process.env });
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
child.stdout.on("data",
|
|
531
|
-
child.stderr.on("data",
|
|
532
|
-
child.on("error", () =>
|
|
547
|
+
const stdout = createOutputCapture();
|
|
548
|
+
const stderr = createOutputCapture();
|
|
549
|
+
child.stdout.on("data", stdout.push);
|
|
550
|
+
child.stderr.on("data", stderr.push);
|
|
551
|
+
child.on("error", () =>
|
|
552
|
+
resolvePromise({
|
|
553
|
+
code: 127,
|
|
554
|
+
stdout: stdout.value(),
|
|
555
|
+
stderr: stderr.value(),
|
|
556
|
+
}),
|
|
557
|
+
);
|
|
533
558
|
child.on("close", (code) =>
|
|
534
|
-
resolvePromise({
|
|
559
|
+
resolvePromise({
|
|
560
|
+
code: code ?? 0,
|
|
561
|
+
stdout: stdout.value(),
|
|
562
|
+
stderr: stderr.value(),
|
|
563
|
+
}),
|
|
535
564
|
);
|
|
536
565
|
});
|
|
537
566
|
}
|
|
@@ -544,9 +573,13 @@ function renderExec(
|
|
|
544
573
|
): string {
|
|
545
574
|
const parts = [`[${where}] ${status}`];
|
|
546
575
|
if (stdout.trim())
|
|
547
|
-
parts.push(
|
|
576
|
+
parts.push(
|
|
577
|
+
`--- stdout ---\n${clampExecOutput(stdout.replace(/\s+$/, ""))}`,
|
|
578
|
+
);
|
|
548
579
|
if (stderr.trim())
|
|
549
|
-
parts.push(
|
|
580
|
+
parts.push(
|
|
581
|
+
`--- stderr ---\n${clampExecOutput(stderr.replace(/\s+$/, ""))}`,
|
|
582
|
+
);
|
|
550
583
|
if (!stdout.trim() && !stderr.trim()) parts.push("(no output)");
|
|
551
584
|
return parts.join("\n");
|
|
552
585
|
}
|