@te-river/opencode-team-mode 1.5.5 → 1.5.7
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 +544 -634
- package/README.zh-CN.md +511 -576
- package/dist/agents.d.ts +6 -1
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +113 -533
- package/dist/agents.js.map +1 -1
- package/dist/approval-gate.d.ts +16 -0
- package/dist/approval-gate.d.ts.map +1 -1
- package/dist/approval-gate.js +8 -1
- package/dist/approval-gate.js.map +1 -1
- package/dist/blackboard.d.ts.map +1 -1
- package/dist/blackboard.js +3 -2
- package/dist/blackboard.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +7 -6
- package/dist/commands.js.map +1 -1
- package/dist/envprotect/bash-classify.d.ts +42 -0
- package/dist/envprotect/bash-classify.d.ts.map +1 -0
- package/dist/envprotect/bash-classify.js +221 -0
- package/dist/envprotect/bash-classify.js.map +1 -0
- package/dist/envprotect/gate-predicates.d.ts +57 -0
- package/dist/envprotect/gate-predicates.d.ts.map +1 -0
- package/dist/envprotect/gate-predicates.js +174 -0
- package/dist/envprotect/gate-predicates.js.map +1 -0
- package/dist/envprotect/hook.d.ts +45 -0
- package/dist/envprotect/hook.d.ts.map +1 -0
- package/dist/envprotect/hook.js +158 -0
- package/dist/envprotect/hook.js.map +1 -0
- package/dist/envprotect/path-classify.d.ts +16 -0
- package/dist/envprotect/path-classify.d.ts.map +1 -0
- package/dist/envprotect/path-classify.js +72 -0
- package/dist/envprotect/path-classify.js.map +1 -0
- package/dist/envprotect/patterns.d.ts +77 -0
- package/dist/envprotect/patterns.d.ts.map +1 -0
- package/dist/envprotect/patterns.js +182 -0
- package/dist/envprotect/patterns.js.map +1 -0
- package/dist/envprotect.d.ts +19 -217
- package/dist/envprotect.d.ts.map +1 -1
- package/dist/envprotect.js +18 -803
- package/dist/envprotect.js.map +1 -1
- package/dist/fs-safe.d.ts +16 -0
- package/dist/fs-safe.d.ts.map +1 -0
- package/dist/fs-safe.js +56 -0
- package/dist/fs-safe.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -6
- package/dist/index.js.map +1 -1
- package/dist/prompts/lead.d.ts +7 -0
- package/dist/prompts/lead.d.ts.map +1 -0
- package/dist/prompts/lead.js +233 -0
- package/dist/prompts/lead.js.map +1 -0
- package/dist/prompts/shared.d.ts +8 -0
- package/dist/prompts/shared.d.ts.map +1 -0
- package/dist/prompts/shared.js +120 -0
- package/dist/prompts/shared.js.map +1 -0
- package/dist/prompts/specialists.d.ts +12 -0
- package/dist/prompts/specialists.d.ts.map +1 -0
- package/dist/prompts/specialists.js +241 -0
- package/dist/prompts/specialists.js.map +1 -0
- package/dist/shell-text.d.ts +14 -0
- package/dist/shell-text.d.ts.map +1 -0
- package/dist/shell-text.js +16 -0
- package/dist/shell-text.js.map +1 -0
- package/dist/tm/args-schema.d.ts +42 -0
- package/dist/tm/args-schema.d.ts.map +1 -0
- package/dist/tm/args-schema.js +213 -0
- package/dist/tm/args-schema.js.map +1 -0
- package/dist/tm/browser.d.ts +52 -0
- package/dist/tm/browser.d.ts.map +1 -0
- package/dist/tm/browser.js +361 -0
- package/dist/tm/browser.js.map +1 -0
- package/dist/tm/client-unwrap.d.ts +38 -0
- package/dist/tm/client-unwrap.d.ts.map +1 -0
- package/dist/tm/client-unwrap.js +113 -0
- package/dist/tm/client-unwrap.js.map +1 -0
- package/dist/tm/config.d.ts +55 -19
- package/dist/tm/config.d.ts.map +1 -1
- package/dist/tm/config.js +74 -19
- package/dist/tm/config.js.map +1 -1
- package/dist/tm/guard.d.ts.map +1 -1
- package/dist/tm/guard.js +4 -7
- package/dist/tm/guard.js.map +1 -1
- package/dist/tm/index.d.ts +24 -7
- package/dist/tm/index.d.ts.map +1 -1
- package/dist/tm/index.js +111 -21
- package/dist/tm/index.js.map +1 -1
- package/dist/tm/memory.d.ts +88 -0
- package/dist/tm/memory.d.ts.map +1 -0
- package/dist/tm/memory.js +360 -0
- package/dist/tm/memory.js.map +1 -0
- package/dist/tm/pipelines.d.ts +58 -0
- package/dist/tm/pipelines.d.ts.map +1 -0
- package/dist/tm/pipelines.js +308 -0
- package/dist/tm/pipelines.js.map +1 -0
- package/dist/tm/preview.d.ts +6 -1
- package/dist/tm/preview.d.ts.map +1 -1
- package/dist/tm/preview.js +63 -13
- package/dist/tm/preview.js.map +1 -1
- package/dist/tm/ptc/budgets.d.ts +38 -0
- package/dist/tm/ptc/budgets.d.ts.map +1 -0
- package/dist/tm/ptc/budgets.js +59 -0
- package/dist/tm/ptc/budgets.js.map +1 -0
- package/dist/tm/ptc/contract.d.ts +131 -0
- package/dist/tm/ptc/contract.d.ts.map +1 -0
- package/dist/tm/ptc/contract.js +47 -0
- package/dist/tm/ptc/contract.js.map +1 -0
- package/dist/tm/ptc/driver.d.ts +37 -0
- package/dist/tm/ptc/driver.d.ts.map +1 -0
- package/dist/tm/ptc/driver.js +181 -0
- package/dist/tm/ptc/driver.js.map +1 -0
- package/dist/tm/ptc/engines.d.ts +32 -0
- package/dist/tm/ptc/engines.d.ts.map +1 -0
- package/dist/tm/ptc/engines.js +201 -0
- package/dist/tm/ptc/engines.js.map +1 -0
- package/dist/tm/ptc/gate.d.ts +28 -0
- package/dist/tm/ptc/gate.d.ts.map +1 -0
- package/dist/tm/ptc/gate.js +130 -0
- package/dist/tm/ptc/gate.js.map +1 -0
- package/dist/tm/ptc/index.d.ts +19 -0
- package/dist/tm/ptc/index.d.ts.map +1 -0
- package/dist/tm/ptc/index.js +15 -0
- package/dist/tm/ptc/index.js.map +1 -0
- package/dist/tm/ptc/pscan.d.ts +11 -0
- package/dist/tm/ptc/pscan.d.ts.map +1 -0
- package/dist/tm/ptc/pscan.js +27 -0
- package/dist/tm/ptc/pscan.js.map +1 -0
- package/dist/tm/ptc/summary.d.ts +17 -0
- package/dist/tm/ptc/summary.d.ts.map +1 -0
- package/dist/tm/ptc/summary.js +112 -0
- package/dist/tm/ptc/summary.js.map +1 -0
- package/dist/tm/ptc/tool.d.ts +35 -0
- package/dist/tm/ptc/tool.d.ts.map +1 -0
- package/dist/tm/ptc/tool.js +136 -0
- package/dist/tm/ptc/tool.js.map +1 -0
- package/dist/tm/ptc.d.ts +45 -26
- package/dist/tm/ptc.d.ts.map +1 -1
- package/dist/tm/ptc.js +71 -45
- package/dist/tm/ptc.js.map +1 -1
- package/dist/tm/result.d.ts +34 -0
- package/dist/tm/result.d.ts.map +1 -0
- package/dist/tm/result.js +87 -0
- package/dist/tm/result.js.map +1 -0
- package/dist/tm/search.d.ts +58 -0
- package/dist/tm/search.d.ts.map +1 -0
- package/dist/tm/search.js +229 -0
- package/dist/tm/search.js.map +1 -0
- package/dist/tm/shell-bridge.d.ts +22 -0
- package/dist/tm/shell-bridge.d.ts.map +1 -0
- package/dist/tm/shell-bridge.js +116 -0
- package/dist/tm/shell-bridge.js.map +1 -0
- package/dist/tm/store.d.ts.map +1 -1
- package/dist/tm/store.js +2 -1
- package/dist/tm/store.js.map +1 -1
- package/dist/tm/tools.d.ts +13 -71
- package/dist/tm/tools.d.ts.map +1 -1
- package/dist/tm/tools.js +22 -656
- package/dist/tm/tools.js.map +1 -1
- package/dist/tm/webfetch.d.ts +109 -0
- package/dist/tm/webfetch.d.ts.map +1 -0
- package/dist/tm/webfetch.js +305 -0
- package/dist/tm/webfetch.js.map +1 -0
- package/docs/installation.md +201 -0
- package/package.json +4 -3
package/dist/tm/tools.js
CHANGED
|
@@ -1,294 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JIT layer-2 tools — the four governed
|
|
2
|
+
* JIT layer-2 tools — assembly of the four governed tools. The machinery
|
|
3
|
+
* lives in sibling modules (one responsibility each):
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* pipelines.ts — the four governed pipelines + threshold governance
|
|
6
|
+
* result.ts — structured errors + ToolResult rendering contract
|
|
7
|
+
* client-unwrap.ts — host client result unwrapping + text extraction
|
|
8
|
+
* shell-bridge.ts — the host `$` shell bridge + shell error hygiene
|
|
9
|
+
* args-schema.ts — zod raw-shape arg schemas (descriptor fallback)
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
* - ToolResult contract (official tool.d.ts:39-46): execute() returns
|
|
11
|
-
* {output: string} — ALWAYS. Handles, structured errors, pages and
|
|
12
|
-
* degraded warnings are formatted INTO the output text; a bare object
|
|
13
|
-
* return crashes the host result pipeline (`c.split`) and the model
|
|
14
|
-
* would never see the handle it needs to drive tm_fetch.
|
|
15
|
-
* - R6 reuse: env-file paths / env commands are refused through the SAME
|
|
16
|
-
* envprotect matchers the global hook uses (anti-backdoor requirement);
|
|
17
|
-
* the hook layer additionally aliases tm_* onto read/grep/bash so even a
|
|
18
|
-
* future tool-code regression cannot bypass R6.
|
|
19
|
-
* - Threshold offload: estimateTokens(content) >= TM_OFFLOAD_THRESHOLD
|
|
20
|
-
* (default 2000; == threshold also offloads) -> full payload to the run
|
|
21
|
-
* store + trajectory, handle {offloaded, ref, access_token, expire_at,
|
|
22
|
-
* tokens, preview} back. Below -> content inline.
|
|
23
|
-
* - Structured errors (T1.4): pipelines still produce
|
|
24
|
-
* {error: {tool, phase, message, line?}} internally — never a bare
|
|
25
|
-
* throw — and execute() renders them readably into output; shell errors
|
|
26
|
-
* carry a line when one can be extracted and stdout noise is stripped.
|
|
27
|
-
* - Degradation: an offload (store) failure returns truncated content plus
|
|
28
|
-
* a warning — a governance fault must never fail the task.
|
|
11
|
+
* Behavior unchanged from the former tools.ts hub.
|
|
29
12
|
*/
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { buildPreview, buildStructureSummary, contentTypeForPath, detectContentType, } from "./preview.js";
|
|
34
|
-
import { assertReadablePath, classifyReadonlyCommand } from "./guard.js";
|
|
35
|
-
export function tmError(tool, phase, message, line) {
|
|
36
|
-
const body = { tool, phase, message };
|
|
37
|
-
if (typeof line === "number" && Number.isFinite(line))
|
|
38
|
-
body.line = line;
|
|
39
|
-
return { error: body };
|
|
40
|
-
}
|
|
41
|
-
/** Fixed invalid-handle message (spec-pinned wording for tm_fetch). */
|
|
42
|
-
export const HANDLE_INVALID_MESSAGE = "载荷已清理或 run 不匹配,建议重跑原工具。";
|
|
43
|
-
// ---------- ToolResult rendering (official tool.d.ts:39-46 contract) ----------
|
|
44
|
-
function isTmErrorBody(res) {
|
|
45
|
-
return Boolean(res) && typeof res === "object" && "error" in res;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Render one pipeline result as the host ToolResult: `{output: string}`.
|
|
49
|
-
* The model can ONLY see this text — so the handle fields it needs to drive
|
|
50
|
-
* tm_fetch (ref/access_token/expire_at), the preview, paging hints, error
|
|
51
|
-
* phase/message and degraded warnings are all formatted INTO the output.
|
|
52
|
-
*/
|
|
53
|
-
function toToolResult(res) {
|
|
54
|
-
if (typeof res === "string")
|
|
55
|
-
return { output: res };
|
|
56
|
-
if (!res || typeof res !== "object")
|
|
57
|
-
return { output: String(res ?? "") };
|
|
58
|
-
if (isTmErrorBody(res)) {
|
|
59
|
-
const e = res.error;
|
|
60
|
-
const head = `[${e.tool} 失败 · phase=${e.phase}` + (typeof e.line === "number" ? ` · line=${e.line}` : "") + "]";
|
|
61
|
-
return { output: `${head}\n${e.message}` };
|
|
62
|
-
}
|
|
63
|
-
const o = res;
|
|
64
|
-
// tm_fetch structure page
|
|
65
|
-
if (o.mode === "structure") {
|
|
66
|
-
return {
|
|
67
|
-
output: [
|
|
68
|
-
`ref: ${String(o.ref)}`,
|
|
69
|
-
`mode: structure | total_lines: ${String(o.total_lines)}${o.expire_at != null ? ` | expire_at: ${String(o.expire_at)}` : ""}`,
|
|
70
|
-
"结构摘要:",
|
|
71
|
-
String(o.summary ?? ""),
|
|
72
|
-
].join("\n"),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
// tm_fetch lines page — hint already carries total/returned/remaining/next offset
|
|
76
|
-
if (o.mode === "lines") {
|
|
77
|
-
return {
|
|
78
|
-
output: `ref: ${String(o.ref)}\n${String(o.hint ?? "")}\n--- 内容 ---\n${String(o.content ?? "")}`,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// offload handle — the model's ONLY window onto ref + token + preview
|
|
82
|
-
if (o.offloaded === true) {
|
|
83
|
-
return {
|
|
84
|
-
output: [
|
|
85
|
-
`结果过大(约 ${String(o.tokens)} tokens),已卸载到 run 存储。以下句柄是取回全文的唯一入口:`,
|
|
86
|
-
`ref: ${String(o.ref)}`,
|
|
87
|
-
`access_token: ${String(o.access_token)}`,
|
|
88
|
-
`expire_at: ${String(o.expire_at)}`,
|
|
89
|
-
`tokens: ${String(o.tokens)}`,
|
|
90
|
-
`content_type: ${String(o.content_type ?? "text")}`,
|
|
91
|
-
"preview:",
|
|
92
|
-
String(o.preview ?? ""),
|
|
93
|
-
`用 tm_fetch(ref, access_token) 分页取回;大载荷先试 mode:"structure"。`,
|
|
94
|
-
].join("\n"),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
// degraded path — store failure never fails the task
|
|
98
|
-
if (o.degraded === true) {
|
|
99
|
-
return {
|
|
100
|
-
output: `[警告] ${String(o.warning ?? "")}\n--- 内容(截断) ---\n${String(o.content ?? "")}`,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
return { output: JSON.stringify(res, null, 2) };
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return { output: String(res) };
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Client result unwrapping — TWO live shapes + the legacy test shape:
|
|
112
|
-
* - 1.18.x REAL host (tester-probed): `{ data: <payload>, request, response }`
|
|
113
|
-
* with NO `ok` field — file.read data = {type:"text", content},
|
|
114
|
-
* find.text data = Match[]. Absent `ok` is NOT a failure; `data`
|
|
115
|
-
* presence decides success.
|
|
116
|
-
* - T0.4 legacy: `{ ok: true, data }` (what the fake clients in tests emit).
|
|
117
|
-
* RequestResult hygiene: ok === true can STILL carry the error envelope
|
|
118
|
-
* {error: {name, data: {message, ref}}} — self-check `data.error`. A
|
|
119
|
-
* top-level error / ok===false fails even when a data payload rides along.
|
|
120
|
-
*/
|
|
121
|
-
function unwrapClientResult(res) {
|
|
122
|
-
if (!res || typeof res !== "object") {
|
|
123
|
-
return { ok: false, message: "客户端返回为空或格式异常" };
|
|
124
|
-
}
|
|
125
|
-
const r = res;
|
|
126
|
-
if (r.ok === false || r.error != null) {
|
|
127
|
-
const e = r.error;
|
|
128
|
-
const msg = e?.message ?? e?.data?.message ?? e?.name;
|
|
129
|
-
return { ok: false, message: msg ? String(msg) : `客户端返回 ok=${String(r.ok)}` };
|
|
130
|
-
}
|
|
131
|
-
if ("data" in r) {
|
|
132
|
-
const data = r.data;
|
|
133
|
-
if (data && typeof data === "object") {
|
|
134
|
-
const errVal = data.error;
|
|
135
|
-
if (errVal != null) {
|
|
136
|
-
if (typeof errVal === "string")
|
|
137
|
-
return { ok: false, message: errVal };
|
|
138
|
-
const envelope = errVal;
|
|
139
|
-
const msg = envelope?.data?.message ?? envelope?.name ?? "unknown client error envelope";
|
|
140
|
-
return { ok: false, message: String(msg) };
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return { ok: true, data };
|
|
144
|
-
}
|
|
145
|
-
if (r.ok === true)
|
|
146
|
-
return { ok: true, data: undefined };
|
|
147
|
-
return { ok: false, message: "客户端返回格式异常(缺少 data 字段)" };
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* One client.find.text match object -> ripgrep-style `path:line: text` lines.
|
|
151
|
-
* Key table (live-probed shape; tester re-verifies each key against real
|
|
152
|
-
* host data):
|
|
153
|
-
* path — file path of the hit (aliases: file, file_path, filePath)
|
|
154
|
-
* lines — matched lines, string[] (aliases: text, content, match, line_text,
|
|
155
|
-
* line-as-string)
|
|
156
|
-
* line — 1-based line number (aliases: line_number, lineNumber; must be
|
|
157
|
-
* numeric — a textual `line` value falls through to the text keys)
|
|
158
|
-
* Returns null when neither a path nor any hit text is found.
|
|
159
|
-
*/
|
|
160
|
-
function matchObjectText(m) {
|
|
161
|
-
const p = [m.path, m.file, m.file_path, m.filePath].find((v) => typeof v === "string" && v.length > 0);
|
|
162
|
-
let texts = [];
|
|
163
|
-
for (const key of ["lines", "text", "content", "match", "line_text", "line"]) {
|
|
164
|
-
const v = m[key];
|
|
165
|
-
if (Array.isArray(v)) {
|
|
166
|
-
texts = v.filter((x) => typeof x === "string");
|
|
167
|
-
if (texts.length > 0)
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
else if (typeof v === "string" && v.length > 0) {
|
|
171
|
-
texts = [v];
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const numKeys = ["line_number", "lineNumber", "line"]
|
|
176
|
-
.map((k) => Number(m[k]))
|
|
177
|
-
.find((n) => Number.isInteger(n) && Number.isFinite(n));
|
|
178
|
-
if (!p && texts.length === 0)
|
|
179
|
-
return null;
|
|
180
|
-
const prefix = p ? (typeof numKeys === "number" ? `${p}:${numKeys}` : p) : "";
|
|
181
|
-
if (texts.length === 0)
|
|
182
|
-
return prefix;
|
|
183
|
-
return texts.map((t) => (prefix ? `${prefix}: ${t}` : t)).join("\n");
|
|
184
|
-
}
|
|
185
|
-
/** Best-effort text extraction from verified client payloads. */
|
|
186
|
-
function extractText(data) {
|
|
187
|
-
if (typeof data === "string")
|
|
188
|
-
return data;
|
|
189
|
-
if (data == null)
|
|
190
|
-
return "";
|
|
191
|
-
if (Array.isArray(data)) {
|
|
192
|
-
// find.text real-host shape: an array of match objects. Render each as
|
|
193
|
-
// `path:line: text` so path/line/hit structure survives into previews
|
|
194
|
-
// (the log branch lifts file:line retrieval clues) and aggregation.
|
|
195
|
-
if (data.length > 0 && data.every((x) => x !== null && typeof x === "object" && !Array.isArray(x))) {
|
|
196
|
-
const rendered = data.map((x) => matchObjectText(x));
|
|
197
|
-
if (rendered.some((s) => s !== null)) {
|
|
198
|
-
return rendered.filter((s) => s !== null).join("\n");
|
|
199
|
-
}
|
|
200
|
-
// Key-table mismatch — fall back to JSON so nothing is silently dropped.
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
return data.map((x) => (typeof x === "string" ? x : JSON.stringify(x))).join("\n");
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
const o = data;
|
|
207
|
-
for (const key of ["content", "text", "output", "stdout"]) {
|
|
208
|
-
if (typeof o[key] === "string")
|
|
209
|
-
return o[key];
|
|
210
|
-
}
|
|
211
|
-
try {
|
|
212
|
-
return JSON.stringify(data, null, 2);
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
return String(data);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
// ---------- shell bridge ----------
|
|
219
|
-
/**
|
|
220
|
-
* Run a command through the host `$` bridge. The REAL host `$` is a
|
|
221
|
-
* Bun-shell tagged template (probed keys: Shell/ShellPromise/ShellError/
|
|
222
|
-
* braces/escape) — both `$(["cmd"])` and `$("cmd")` are rejected with
|
|
223
|
-
* "Please use '$' as a tagged template function", which failed every
|
|
224
|
-
* tm_bash call at phase=execute. A tagged-template call desugars to
|
|
225
|
-
* `$(Object.assign([command], { raw: [command] }))` — tester-verified OK on
|
|
226
|
-
* the real host — so that shape goes FIRST; the legacy call shapes stay as
|
|
227
|
-
* fallbacks for other hosts and the test fakes (which only need *some*
|
|
228
|
-
* call to return `{text}`). Returns stdout text.
|
|
229
|
-
*/
|
|
230
|
-
async function runShellCommand($, command) {
|
|
231
|
-
if (typeof $ !== "function") {
|
|
232
|
-
throw new Error("宿主 shell 桥($)不可用");
|
|
233
|
-
}
|
|
234
|
-
const shell = $;
|
|
235
|
-
let proc;
|
|
236
|
-
let lastErr;
|
|
237
|
-
for (const attempt of [
|
|
238
|
-
() => shell(Object.assign([command], { raw: [command] })), // tagged-template desugar: $`command`
|
|
239
|
-
() => shell([command]), // plain-array host variant
|
|
240
|
-
() => shell(command), // plain-string host variant
|
|
241
|
-
]) {
|
|
242
|
-
try {
|
|
243
|
-
proc = attempt();
|
|
244
|
-
// a call shape may return undefined WITHOUT throwing — keep trying the
|
|
245
|
-
// next shape instead of breaking with lastErr still undefined
|
|
246
|
-
if (proc != null)
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
catch (err) {
|
|
250
|
-
lastErr = err;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
if (proc == null) {
|
|
254
|
-
// rethrow the ORIGINAL error (Error or shell-error object) — wrapping it
|
|
255
|
-
// through String() would destroy stderr/stdout before cleanShellError
|
|
256
|
-
// can extract line numbers
|
|
257
|
-
throw lastErr ?? new Error("shell 桥调用失败");
|
|
258
|
-
}
|
|
259
|
-
const p = proc;
|
|
260
|
-
if (typeof p.text === "function")
|
|
261
|
-
return String(await p.text());
|
|
262
|
-
if (p.stdout !== undefined)
|
|
263
|
-
return String(p.stdout);
|
|
264
|
-
throw new Error("shell 结果不可读");
|
|
265
|
-
}
|
|
266
|
-
/** Strip ANSI + empty-line noise, cap length, best-effort line number. */
|
|
267
|
-
function cleanShellError(err) {
|
|
268
|
-
const e = err;
|
|
269
|
-
const raw = [e?.stderr, e?.message, e?.stdout]
|
|
270
|
-
.filter((v) => typeof v === "string" && v.length > 0)
|
|
271
|
-
.join("\n");
|
|
272
|
-
let msg = String(raw || e?.message || String(err))
|
|
273
|
-
.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "")
|
|
274
|
-
.split(/\r?\n/)
|
|
275
|
-
.map((l) => l.trimEnd())
|
|
276
|
-
.filter((l) => l.trim().length > 0)
|
|
277
|
-
.slice(0, 6)
|
|
278
|
-
.join("\n");
|
|
279
|
-
if (msg.length > 400)
|
|
280
|
-
msg = msg.slice(0, 400) + " …";
|
|
281
|
-
const m = /(?:line|行)\s*[:#]?\s*(\d+)/i.exec(raw);
|
|
282
|
-
return { message: msg || "shell 执行失败", line: m ? Number(m[1]) : undefined };
|
|
283
|
-
}
|
|
284
|
-
// ---------- args coercion ----------
|
|
285
|
-
function strArg(v) {
|
|
286
|
-
return typeof v === "string" ? v : v == null ? "" : String(v);
|
|
287
|
-
}
|
|
288
|
-
function intArg(v, def) {
|
|
289
|
-
const n = typeof v === "number" ? v : Number.parseInt(String(v ?? ""), 10);
|
|
290
|
-
return Number.isFinite(n) ? Math.trunc(n) : def;
|
|
291
|
-
}
|
|
13
|
+
import { buildArgsSchemas } from "./args-schema.js";
|
|
14
|
+
import { buildPipelines } from "./pipelines.js";
|
|
15
|
+
import { toToolResult } from "./result.js";
|
|
292
16
|
// ---------- descriptions ----------
|
|
293
17
|
// Deliberately explicit about dialect + governance: the model must prefer the
|
|
294
18
|
// governed tools over raw bash equivalents (see /#14791 for tm_grep) and must
|
|
@@ -297,7 +21,7 @@ const TM_READ_DESCRIPTION = `Read a file inside the project (governed passthroug
|
|
|
297
21
|
- Path semantics: paths are relative to the PROJECT ROOT (not the agent's working directory). To read pt07/workspace/src/config.js, pass path="pt07/workspace/src/config.js".
|
|
298
22
|
- Read scope (P2, fail-closed): project root + blackboard dir + trajectory dir; anything outside is rejected, and so is a nonexistent/unresolvable path (realpath-verified).
|
|
299
23
|
- R6 applies: env files (.env, *.env, .bashrc family) are refused — same interception source as the built-in read; tm_* is NOT a bypass.
|
|
300
|
-
- Governance: results up to TM_OFFLOAD_THRESHOLD tokens (default 2000,
|
|
24
|
+
- Governance: results up to TM_OFFLOAD_THRESHOLD tokens (default 2000, CJK-aware estimate) return inline; larger payloads are offloaded to a handle {offloaded, ref, access_token, expire_at, tokens, preview} — page through with tm_fetch (try mode:"structure" first).
|
|
301
25
|
- The preview is content-aware (JSON / CSV / log / code / binary branches, hard-capped at 80 tokens) and embeds retrieval clues.`;
|
|
302
26
|
const TM_GREP_DESCRIPTION = `Full-text regex search inside the project (governed passthrough of the host's ripgrep index). PREFER this over running rg inside tm_bash: it uses the host's search index and auto-governs oversized results instead of flooding the context.
|
|
303
27
|
- Args: pattern (required, regex), path (optional scope directory, relative to project root — default project root itself).
|
|
@@ -306,7 +30,7 @@ const TM_GREP_DESCRIPTION = `Full-text regex search inside the project (governed
|
|
|
306
30
|
- Governance: results up to TM_OFFLOAD_THRESHOLD tokens return inline; larger ones are offloaded to a handle {offloaded, ref, access_token, expire_at, tokens, preview} whose preview carries match clues (file:line). Page through with tm_fetch.
|
|
307
31
|
- Strategy: aggregate first (narrow pattern, counts), fetch raw lines only when needed.`;
|
|
308
32
|
const TM_BASH_DESCRIPTION = `Run a READ-ONLY shell command in the project (governed passthrough of the built-in bash). Dialect: bash on POSIX, PowerShell-like on Windows (Get-Content / Get-ChildItem / Select-String work; ls/cat/dir are aliased).
|
|
309
|
-
- Allowlist only (P3): ls cat head tail grep rg find awk sort uniq wc cut dir Get-Content Get-ChildItem Select-String Measure-Object — extend via TM_BASH_READONLY_ALLOWED. Anything else is rejected with a read-only suggestion or an ask to HUMAN.
|
|
33
|
+
- Allowlist only (P3): ls cat head tail grep rg find awk sort uniq wc cut dir Get-Content Get-ChildItem Select-String Measure-Object Select-Object Where-Object Sort-Object Group-Object — extend via TM_BASH_READONLY_ALLOWED. Anything else is rejected with a read-only suggestion or an ask to HUMAN.
|
|
310
34
|
- R6 still applies on top: env dumps (env/printenv/set, $env:) and env-file paths are blocked exactly like the built-in bash — two layers, non-conflicting (R6 forbids, the allowlist permits).
|
|
311
35
|
- Escapes rejected: output redirection (> >>), command substitution ($(), backticks, <() >()), find -delete/-exec, tail -f, Get-Content/Get-ChildItem -Wait, awk system(), rg --pre.
|
|
312
36
|
- Governance: output up to TM_OFFLOAD_THRESHOLD tokens returns inline; larger output is offloaded to a tm_fetch handle. Use for aggregations (count/sort/uniq); for plain search prefer tm_grep (host index, auto-governed).`;
|
|
@@ -314,397 +38,39 @@ const TM_FETCH_DESCRIPTION = `Page through an offloaded tool result by its handl
|
|
|
314
38
|
- Args: ref (required, tm://runs/{run_id}/steps/{step_id}/result), access_token (required — from the handle; may also ride the ref fragment), offset (0-based line), limit (lines per fetch, capped at TM_FETCH_MAX_LINES, default 2000), mode ("lines" default | "structure" — a ~100-token TOC / key-tree / error-line map; try it FIRST on big payloads).
|
|
315
39
|
- Handles are run-scoped and HMAC-signed: a foreign-run, tampered or expired handle (TM_BLACKBOARD_TTL, default 7 days) is rejected with "payload cleared or run mismatch — rerun the original tool".
|
|
316
40
|
- Returns the ref, a paging hint (total/returned/remaining/next offset) and the content slice. Aggregate first; fetch further slices only when needed.`;
|
|
317
|
-
export function buildPipelines(deps) {
|
|
318
|
-
const { cfg, store } = deps;
|
|
319
|
-
let stepCounter = 0;
|
|
320
|
-
const nextStepId = () => `s${String(++stepCounter).padStart(4, "0")}`;
|
|
321
|
-
function ctxDir(ctx) {
|
|
322
|
-
const d = ctx?.directory;
|
|
323
|
-
return typeof d === "string" && d ? d : process.cwd();
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Threshold governance. Inline under the threshold; otherwise store full
|
|
327
|
-
* content + append trajectory and return the handle. A store failure
|
|
328
|
-
* degrades to truncated content + warning (never fails the task).
|
|
329
|
-
*/
|
|
330
|
-
function govern(stepId, tool, content, opts) {
|
|
331
|
-
const tokens = estimateTokens(content);
|
|
332
|
-
if (!shouldOffload(tokens, cfg.offloadThreshold)) {
|
|
333
|
-
store.appendTrajectory({ tool, step_id: stepId, event: "result", offloaded: false, tokens });
|
|
334
|
-
return content;
|
|
335
|
-
}
|
|
336
|
-
const preview = buildPreview(content, opts.contentType, {
|
|
337
|
-
lines: cfg.previewLines,
|
|
338
|
-
maxTokens: cfg.previewMaxTokens,
|
|
339
|
-
clue: opts.clue,
|
|
340
|
-
});
|
|
341
|
-
try {
|
|
342
|
-
const stored = store.writeResult(stepId, {
|
|
343
|
-
tool,
|
|
344
|
-
content,
|
|
345
|
-
tokens,
|
|
346
|
-
contentType: opts.contentType,
|
|
347
|
-
preview,
|
|
348
|
-
expireAt: deps.expireAt,
|
|
349
|
-
});
|
|
350
|
-
store.appendTrajectory({
|
|
351
|
-
tool,
|
|
352
|
-
step_id: stepId,
|
|
353
|
-
seq: stored.seq,
|
|
354
|
-
event: "result",
|
|
355
|
-
offloaded: true,
|
|
356
|
-
tokens,
|
|
357
|
-
ref: stored.ref,
|
|
358
|
-
});
|
|
359
|
-
return {
|
|
360
|
-
offloaded: true,
|
|
361
|
-
ref: stored.ref,
|
|
362
|
-
access_token: deps.accessToken,
|
|
363
|
-
expire_at: stored.expireAt,
|
|
364
|
-
content_type: opts.contentType,
|
|
365
|
-
tokens,
|
|
366
|
-
preview,
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
catch (err) {
|
|
370
|
-
return {
|
|
371
|
-
offloaded: false,
|
|
372
|
-
degraded: true,
|
|
373
|
-
truncated: true,
|
|
374
|
-
warning: `结果治理(黑板卸载)失败,已降级为截断返回:${shorten(err?.message ?? err, 120)}。` +
|
|
375
|
-
`治理故障不使任务失败;需要全文请重跑原工具或缩小查询范围。`,
|
|
376
|
-
content: content.slice(0, Math.max(0, cfg.offloadThreshold) * 4),
|
|
377
|
-
tokens_estimate: tokens,
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// ---------- tm_read ----------
|
|
382
|
-
async function tmRead(rawArgs, ctx) {
|
|
383
|
-
const tool = "tm_read";
|
|
384
|
-
try {
|
|
385
|
-
const args = rawArgs ?? {};
|
|
386
|
-
const requested = strArg(args.path).trim();
|
|
387
|
-
if (!requested)
|
|
388
|
-
return tmError(tool, "args", "缺少 path 参数");
|
|
389
|
-
// R6 reuse — same matcher source as the global hook (anti-backdoor).
|
|
390
|
-
const r6 = classifyPathFields(args, deps.mode, deps.extra);
|
|
391
|
-
if (r6)
|
|
392
|
-
return tmError(tool, "permission", `${ENV_PROTECT_MESSAGE} [category=${r6}]`);
|
|
393
|
-
const stepId = nextStepId();
|
|
394
|
-
store.appendTrajectory({ tool, step_id: stepId, event: "call" });
|
|
395
|
-
const scope = assertReadablePath(ctxDir(ctx), requested, [
|
|
396
|
-
store.blackboardRoot,
|
|
397
|
-
store.trajectoryRoot,
|
|
398
|
-
]);
|
|
399
|
-
if (!scope.ok)
|
|
400
|
-
return tmError(tool, "permission", scope.message);
|
|
401
|
-
const client = deps.client;
|
|
402
|
-
if (!client || typeof client.file?.read !== "function") {
|
|
403
|
-
return tmError(tool, "client", "宿主 client 不可用(client.file.read 缺失)");
|
|
404
|
-
}
|
|
405
|
-
// METHOD call (property-access site keeps the SDK `this` binding) —
|
|
406
|
-
// a fetched-and-unbound call dies synchronously on the real host
|
|
407
|
-
// (same P0 class as the approval-gate reply bug; §7-style mocks pin it)
|
|
408
|
-
const res = await client.file.read({
|
|
409
|
-
query: { path: scope.abs, directory: ctxDir(ctx) },
|
|
410
|
-
});
|
|
411
|
-
const unwrapped = unwrapClientResult(res);
|
|
412
|
-
if (!unwrapped.ok)
|
|
413
|
-
return tmError(tool, "client", unwrapped.message);
|
|
414
|
-
const content = extractText(unwrapped.data);
|
|
415
|
-
return govern(stepId, tool, content, {
|
|
416
|
-
contentType: detectContentType(content, contentTypeForPath(scope.abs)),
|
|
417
|
-
clue: `path=${shorten(scope.abs, 120)}`,
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
catch (err) {
|
|
421
|
-
const info = cleanShellError(err);
|
|
422
|
-
return tmError(tool, "execute", info.message, info.line);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
// ---------- tm_grep ----------
|
|
426
|
-
async function tmGrep(rawArgs, ctx) {
|
|
427
|
-
const tool = "tm_grep";
|
|
428
|
-
try {
|
|
429
|
-
const args = rawArgs ?? {};
|
|
430
|
-
const pattern = strArg(args.pattern).trim();
|
|
431
|
-
if (!pattern)
|
|
432
|
-
return tmError(tool, "args", "缺少 pattern 参数");
|
|
433
|
-
const r6 = classifyPathFields(args, deps.mode, deps.extra);
|
|
434
|
-
if (r6)
|
|
435
|
-
return tmError(tool, "permission", `${ENV_PROTECT_MESSAGE} [category=${r6}]`);
|
|
436
|
-
const stepId = nextStepId();
|
|
437
|
-
store.appendTrajectory({ tool, step_id: stepId, event: "call" });
|
|
438
|
-
let scopeDir = ctxDir(ctx);
|
|
439
|
-
const requested = strArg(args.path).trim();
|
|
440
|
-
if (requested) {
|
|
441
|
-
const scope = assertReadablePath(ctxDir(ctx), requested, [
|
|
442
|
-
store.blackboardRoot,
|
|
443
|
-
store.trajectoryRoot,
|
|
444
|
-
]);
|
|
445
|
-
if (!scope.ok)
|
|
446
|
-
return tmError(tool, "permission", scope.message);
|
|
447
|
-
scopeDir = scope.abs;
|
|
448
|
-
}
|
|
449
|
-
const client = deps.client;
|
|
450
|
-
if (!client || typeof client.find?.text !== "function") {
|
|
451
|
-
return tmError(tool, "client", "宿主 client 不可用(client.find.text 缺失)");
|
|
452
|
-
}
|
|
453
|
-
// METHOD call — see the tm_read note above (unbound SDK call = dead)
|
|
454
|
-
const res = await client.find.text({
|
|
455
|
-
query: { pattern, directory: scopeDir },
|
|
456
|
-
});
|
|
457
|
-
const unwrapped = unwrapClientResult(res);
|
|
458
|
-
if (!unwrapped.ok)
|
|
459
|
-
return tmError(tool, "client", unwrapped.message);
|
|
460
|
-
const content = extractText(unwrapped.data);
|
|
461
|
-
const matchLines = content ? content.split(/\r?\n/).filter((l) => l.trim()).length : 0;
|
|
462
|
-
return govern(stepId, tool, content, {
|
|
463
|
-
contentType: detectContentType(content),
|
|
464
|
-
clue: `pattern=${shorten(pattern, 60)}, 命中 ${matchLines} 行, dir=${shorten(scopeDir, 80)}`,
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
catch (err) {
|
|
468
|
-
const info = cleanShellError(err);
|
|
469
|
-
return tmError(tool, "execute", info.message, info.line);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
// ---------- tm_bash ----------
|
|
473
|
-
async function tmBash(rawArgs, ctx) {
|
|
474
|
-
const tool = "tm_bash";
|
|
475
|
-
try {
|
|
476
|
-
const args = rawArgs ?? {};
|
|
477
|
-
const command = strArg(args.command).trim();
|
|
478
|
-
if (!command)
|
|
479
|
-
return tmError(tool, "args", "缺少 command 参数");
|
|
480
|
-
// Layer 1 — R6 (forbidden set, same source as the built-in bash hook).
|
|
481
|
-
const r6 = classifyBashCommand(command, deps.mode, deps.extra);
|
|
482
|
-
if (r6)
|
|
483
|
-
return tmError(tool, "permission", `${ENV_PROTECT_MESSAGE} [category=${r6}]`);
|
|
484
|
-
// Layer 2 — P3 allowlist (permitted set, read-only).
|
|
485
|
-
const verdict = classifyReadonlyCommand(command, cfg.bashReadonlyAllowed);
|
|
486
|
-
if (!verdict.ok) {
|
|
487
|
-
return tmError(tool, "permission", `${verdict.reason ?? "命令被拒绝"}。${verdict.suggestion ?? ""}`.trim());
|
|
488
|
-
}
|
|
489
|
-
const stepId = nextStepId();
|
|
490
|
-
store.appendTrajectory({ tool, step_id: stepId, event: "call" });
|
|
491
|
-
const output = await runShellCommand(deps.$, command);
|
|
492
|
-
return govern(stepId, tool, output, {
|
|
493
|
-
contentType: detectContentType(output),
|
|
494
|
-
clue: `cmd=${shorten(command, 60)}, dir=${shorten(ctxDir(ctx), 80)}`,
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
catch (err) {
|
|
498
|
-
const info = cleanShellError(err);
|
|
499
|
-
return tmError(tool, "execute", info.message, info.line);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
// ---------- tm_fetch ----------
|
|
503
|
-
async function tmFetch(rawArgs, _ctx) {
|
|
504
|
-
const tool = "tm_fetch";
|
|
505
|
-
try {
|
|
506
|
-
const args = rawArgs ?? {};
|
|
507
|
-
const ref = strArg(args.ref).trim();
|
|
508
|
-
if (!ref)
|
|
509
|
-
return tmError(tool, "args", "缺少 ref 参数(offload 句柄中的 ref)");
|
|
510
|
-
const parsed = parseTmRef(ref);
|
|
511
|
-
if (!parsed) {
|
|
512
|
-
return tmError(tool, "args", "ref 格式无效,期望 tm://runs/{run_id}/steps/{step_id}/result");
|
|
513
|
-
}
|
|
514
|
-
let token = strArg(args.access_token).trim();
|
|
515
|
-
if (!token && parsed.token)
|
|
516
|
-
token = parsed.token;
|
|
517
|
-
if (!token)
|
|
518
|
-
return tmError(tool, "args", "缺少 access_token(offload 句柄中携带)");
|
|
519
|
-
// Auth: run must match the current run AND the token must verify.
|
|
520
|
-
let reason;
|
|
521
|
-
if (parsed.runId !== deps.runId) {
|
|
522
|
-
reason = "run 不匹配";
|
|
523
|
-
}
|
|
524
|
-
else if (!verifyToken(deps.hmacKey, deps.runId, token)) {
|
|
525
|
-
reason = "token 校验失败";
|
|
526
|
-
}
|
|
527
|
-
else {
|
|
528
|
-
reason = "";
|
|
529
|
-
}
|
|
530
|
-
if (reason) {
|
|
531
|
-
return tmError(tool, "permission", `句柄校验失败(${reason})。${HANDLE_INVALID_MESSAGE}`);
|
|
532
|
-
}
|
|
533
|
-
const entry = store.findIndexEntry(ref);
|
|
534
|
-
const now = Date.now();
|
|
535
|
-
if (entry && isExpired(entry.expire_at, now)) {
|
|
536
|
-
return tmError(tool, "permission", `句柄已过期(expire_at=${entry.expire_at})。${HANDLE_INVALID_MESSAGE}`);
|
|
537
|
-
}
|
|
538
|
-
const loaded = store.readStepFile(parsed.stepId);
|
|
539
|
-
if (!loaded) {
|
|
540
|
-
return tmError(tool, "store", `找不到载荷文件。${HANDLE_INVALID_MESSAGE}`);
|
|
541
|
-
}
|
|
542
|
-
const lines = loaded.content.split(/\r?\n/);
|
|
543
|
-
const mode = strArg(args.mode).trim() === "structure" ? "structure" : "lines";
|
|
544
|
-
store.appendTrajectory({ tool, step_id: parsed.stepId, event: "fetch", ref, mode });
|
|
545
|
-
if (mode === "structure") {
|
|
546
|
-
return {
|
|
547
|
-
ref,
|
|
548
|
-
mode: "structure",
|
|
549
|
-
total_lines: lines.length,
|
|
550
|
-
summary: buildStructureSummary(loaded.content, entry?.content_type ?? "text"),
|
|
551
|
-
expire_at: entry?.expire_at ?? null,
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
let offset = intArg(args.offset, 0);
|
|
555
|
-
if (offset < 0)
|
|
556
|
-
offset = 0;
|
|
557
|
-
let limit = intArg(args.limit, cfg.fetchMaxLines);
|
|
558
|
-
if (limit <= 0)
|
|
559
|
-
limit = cfg.fetchMaxLines;
|
|
560
|
-
if (limit > cfg.fetchMaxLines)
|
|
561
|
-
limit = cfg.fetchMaxLines;
|
|
562
|
-
const slice = lines.slice(offset, offset + limit);
|
|
563
|
-
// offset past EOF must not produce a negative remaining (#9)
|
|
564
|
-
const remaining = Math.max(0, lines.length - (offset + slice.length));
|
|
565
|
-
return {
|
|
566
|
-
ref,
|
|
567
|
-
mode: "lines",
|
|
568
|
-
offset,
|
|
569
|
-
limit,
|
|
570
|
-
total_lines: lines.length,
|
|
571
|
-
returned_lines: slice.length,
|
|
572
|
-
remaining_lines: remaining,
|
|
573
|
-
next_offset: remaining > 0 ? offset + slice.length : null,
|
|
574
|
-
hint: remaining > 0
|
|
575
|
-
? `共 ${lines.length} 行,已返回 ${slice.length} 行(offset=${offset}),剩余 ${remaining} 行;继续取回用 offset=${offset + slice.length};先聚合再决定是否翻页。`
|
|
576
|
-
: `共 ${lines.length} 行,已返回 ${slice.length} 行(offset=${offset}),已到末尾。`,
|
|
577
|
-
content: slice.join("\n"),
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
catch (err) {
|
|
581
|
-
const info = cleanShellError(err);
|
|
582
|
-
return tmError(tool, "execute", info.message, info.line);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
return { nextStepId, govern, tmRead, tmGrep, tmBash, tmFetch };
|
|
586
|
-
}
|
|
587
|
-
// ---------- tool set assembly ----------
|
|
588
|
-
/**
|
|
589
|
-
* Load zod when the host environment provides it (opencode ships it as a
|
|
590
|
-
* peer); fall back to plain arg descriptors so the plugin never hard-depends
|
|
591
|
-
* on it. execute() is defensive either way — it coerces raw args itself.
|
|
592
|
-
*/
|
|
593
|
-
async function loadZod() {
|
|
594
|
-
try {
|
|
595
|
-
const spec = "zod";
|
|
596
|
-
const mod = await import(spec);
|
|
597
|
-
const z = mod?.z ??
|
|
598
|
-
mod?.default?.z ??
|
|
599
|
-
null;
|
|
600
|
-
return z && typeof z.object === "function"
|
|
601
|
-
? z
|
|
602
|
-
: null;
|
|
603
|
-
}
|
|
604
|
-
catch {
|
|
605
|
-
return null;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
function describe(z, schema, fallback) {
|
|
609
|
-
if (z && typeof z.string === "function") {
|
|
610
|
-
return schema;
|
|
611
|
-
}
|
|
612
|
-
return { descriptor: fallback };
|
|
613
|
-
}
|
|
614
|
-
/**
|
|
615
|
-
* Per-tool `args` — a ZodRawShape (plain `{key: validator}` object, official
|
|
616
|
-
* tool.d.ts:47-50), NOT a z.object(): the host serializes the raw shape into
|
|
617
|
-
* the LLM parameter spec. Wrapping it in z.object produced `{def:{command:
|
|
618
|
-
* ...}}` garbage args in real sessions (the model wrapped its args 3/3
|
|
619
|
-
* times), even though flat args reached execute fine.
|
|
620
|
-
*/
|
|
621
|
-
async function buildArgsSchemas() {
|
|
622
|
-
const z = await loadZod();
|
|
623
|
-
if (!z) {
|
|
624
|
-
return {
|
|
625
|
-
tm_read: describe(null, null, "path: string (required)"),
|
|
626
|
-
tm_grep: describe(null, null, "pattern: string (required), path: string (optional)"),
|
|
627
|
-
tm_bash: describe(null, null, "command: string (required, read-only allowlisted)"),
|
|
628
|
-
tm_fetch: describe(null, null, "ref: string (required), access_token: string, offset: int, limit: int, mode: lines|structure"),
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
const zz = z;
|
|
632
|
-
// RAW SHAPES — no zz.object() wrapper (BUG#3).
|
|
633
|
-
return {
|
|
634
|
-
tm_read: {
|
|
635
|
-
path: zz
|
|
636
|
-
.string()
|
|
637
|
-
.describe("File path, relative to the project root (or absolute within it). Env files (.env, *.env, .bashrc family) are rejected — same R6 source as the built-in read."),
|
|
638
|
-
},
|
|
639
|
-
tm_grep: {
|
|
640
|
-
pattern: zz.string().describe("Regex to search (ripgrep syntax)."),
|
|
641
|
-
path: zz
|
|
642
|
-
.string()
|
|
643
|
-
.describe("Optional directory scope, relative to the project root.")
|
|
644
|
-
.optional(),
|
|
645
|
-
},
|
|
646
|
-
tm_bash: {
|
|
647
|
-
command: zz
|
|
648
|
-
.string()
|
|
649
|
-
.describe("Read-only shell command (allowlisted heads only). bash on POSIX, PowerShell-like on Windows."),
|
|
650
|
-
},
|
|
651
|
-
tm_fetch: {
|
|
652
|
-
ref: zz
|
|
653
|
-
.string()
|
|
654
|
-
.describe("Offload handle ref: tm://runs/{run_id}/steps/{step_id}/result"),
|
|
655
|
-
access_token: zz
|
|
656
|
-
.string()
|
|
657
|
-
.describe("HMAC token from the offload handle (may also ride the ref fragment).")
|
|
658
|
-
.optional(),
|
|
659
|
-
offset: zz
|
|
660
|
-
.number()
|
|
661
|
-
.int()
|
|
662
|
-
.min(0)
|
|
663
|
-
.describe("0-based line offset of the first line to return.")
|
|
664
|
-
.optional(),
|
|
665
|
-
limit: zz
|
|
666
|
-
.number()
|
|
667
|
-
.int()
|
|
668
|
-
.min(1)
|
|
669
|
-
.describe("Max lines per fetch, capped at TM_FETCH_MAX_LINES (default 2000).")
|
|
670
|
-
.optional(),
|
|
671
|
-
mode: zz
|
|
672
|
-
.enum(["lines", "structure"])
|
|
673
|
-
.describe("structure = ~100-token TOC / key tree / error-line map; try it first.")
|
|
674
|
-
.optional(),
|
|
675
|
-
},
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
41
|
/**
|
|
679
42
|
* Build the four tool definitions. Async only because of the optional zod
|
|
680
43
|
* load; every pipeline itself is runtime-defensive against raw args. Every
|
|
681
44
|
* execute() funnels through toToolResult so the return value ALWAYS satisfies
|
|
682
45
|
* the host ToolResult contract ({output: string}) — handles, structured
|
|
683
46
|
* errors, pages and degraded warnings included.
|
|
47
|
+
* `pipelines` lets the caller inject a SHARED pipeline instance (tm/index
|
|
48
|
+
* reuses one instance for the main tools AND tm_webfetch so their step ids
|
|
49
|
+
* never collide); when omitted, a fresh instance is built from deps.
|
|
684
50
|
*/
|
|
685
|
-
export async function buildTmTools(deps) {
|
|
686
|
-
const
|
|
51
|
+
export async function buildTmTools(deps, pipelines) {
|
|
52
|
+
const pl = pipelines ?? buildPipelines(deps);
|
|
687
53
|
const argsSchemas = await buildArgsSchemas();
|
|
688
54
|
return {
|
|
689
55
|
tm_read: {
|
|
690
56
|
description: TM_READ_DESCRIPTION,
|
|
691
57
|
args: argsSchemas.tm_read,
|
|
692
|
-
execute: async (args, ctx) => toToolResult(await
|
|
58
|
+
execute: async (args, ctx) => toToolResult(await pl.tmRead(args ?? {}, ctx)),
|
|
693
59
|
},
|
|
694
60
|
tm_grep: {
|
|
695
61
|
description: TM_GREP_DESCRIPTION,
|
|
696
62
|
args: argsSchemas.tm_grep,
|
|
697
|
-
execute: async (args, ctx) => toToolResult(await
|
|
63
|
+
execute: async (args, ctx) => toToolResult(await pl.tmGrep(args ?? {}, ctx)),
|
|
698
64
|
},
|
|
699
65
|
tm_bash: {
|
|
700
66
|
description: TM_BASH_DESCRIPTION,
|
|
701
67
|
args: argsSchemas.tm_bash,
|
|
702
|
-
execute: async (args, ctx) => toToolResult(await
|
|
68
|
+
execute: async (args, ctx) => toToolResult(await pl.tmBash(args ?? {}, ctx)),
|
|
703
69
|
},
|
|
704
70
|
tm_fetch: {
|
|
705
71
|
description: TM_FETCH_DESCRIPTION,
|
|
706
72
|
args: argsSchemas.tm_fetch,
|
|
707
|
-
execute: async (args, ctx) => toToolResult(await
|
|
73
|
+
execute: async (args, ctx) => toToolResult(await pl.tmFetch(args ?? {}, ctx)),
|
|
708
74
|
},
|
|
709
75
|
};
|
|
710
76
|
}
|