@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.
Files changed (163) hide show
  1. package/README.md +544 -634
  2. package/README.zh-CN.md +511 -576
  3. package/dist/agents.d.ts +6 -1
  4. package/dist/agents.d.ts.map +1 -1
  5. package/dist/agents.js +113 -533
  6. package/dist/agents.js.map +1 -1
  7. package/dist/approval-gate.d.ts +16 -0
  8. package/dist/approval-gate.d.ts.map +1 -1
  9. package/dist/approval-gate.js +8 -1
  10. package/dist/approval-gate.js.map +1 -1
  11. package/dist/blackboard.d.ts.map +1 -1
  12. package/dist/blackboard.js +3 -2
  13. package/dist/blackboard.js.map +1 -1
  14. package/dist/commands.d.ts.map +1 -1
  15. package/dist/commands.js +7 -6
  16. package/dist/commands.js.map +1 -1
  17. package/dist/envprotect/bash-classify.d.ts +42 -0
  18. package/dist/envprotect/bash-classify.d.ts.map +1 -0
  19. package/dist/envprotect/bash-classify.js +221 -0
  20. package/dist/envprotect/bash-classify.js.map +1 -0
  21. package/dist/envprotect/gate-predicates.d.ts +57 -0
  22. package/dist/envprotect/gate-predicates.d.ts.map +1 -0
  23. package/dist/envprotect/gate-predicates.js +174 -0
  24. package/dist/envprotect/gate-predicates.js.map +1 -0
  25. package/dist/envprotect/hook.d.ts +45 -0
  26. package/dist/envprotect/hook.d.ts.map +1 -0
  27. package/dist/envprotect/hook.js +158 -0
  28. package/dist/envprotect/hook.js.map +1 -0
  29. package/dist/envprotect/path-classify.d.ts +16 -0
  30. package/dist/envprotect/path-classify.d.ts.map +1 -0
  31. package/dist/envprotect/path-classify.js +72 -0
  32. package/dist/envprotect/path-classify.js.map +1 -0
  33. package/dist/envprotect/patterns.d.ts +77 -0
  34. package/dist/envprotect/patterns.d.ts.map +1 -0
  35. package/dist/envprotect/patterns.js +182 -0
  36. package/dist/envprotect/patterns.js.map +1 -0
  37. package/dist/envprotect.d.ts +19 -217
  38. package/dist/envprotect.d.ts.map +1 -1
  39. package/dist/envprotect.js +18 -803
  40. package/dist/envprotect.js.map +1 -1
  41. package/dist/fs-safe.d.ts +16 -0
  42. package/dist/fs-safe.d.ts.map +1 -0
  43. package/dist/fs-safe.js +56 -0
  44. package/dist/fs-safe.js.map +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +22 -6
  47. package/dist/index.js.map +1 -1
  48. package/dist/prompts/lead.d.ts +7 -0
  49. package/dist/prompts/lead.d.ts.map +1 -0
  50. package/dist/prompts/lead.js +233 -0
  51. package/dist/prompts/lead.js.map +1 -0
  52. package/dist/prompts/shared.d.ts +8 -0
  53. package/dist/prompts/shared.d.ts.map +1 -0
  54. package/dist/prompts/shared.js +120 -0
  55. package/dist/prompts/shared.js.map +1 -0
  56. package/dist/prompts/specialists.d.ts +12 -0
  57. package/dist/prompts/specialists.d.ts.map +1 -0
  58. package/dist/prompts/specialists.js +241 -0
  59. package/dist/prompts/specialists.js.map +1 -0
  60. package/dist/shell-text.d.ts +14 -0
  61. package/dist/shell-text.d.ts.map +1 -0
  62. package/dist/shell-text.js +16 -0
  63. package/dist/shell-text.js.map +1 -0
  64. package/dist/tm/args-schema.d.ts +42 -0
  65. package/dist/tm/args-schema.d.ts.map +1 -0
  66. package/dist/tm/args-schema.js +213 -0
  67. package/dist/tm/args-schema.js.map +1 -0
  68. package/dist/tm/browser.d.ts +52 -0
  69. package/dist/tm/browser.d.ts.map +1 -0
  70. package/dist/tm/browser.js +361 -0
  71. package/dist/tm/browser.js.map +1 -0
  72. package/dist/tm/client-unwrap.d.ts +38 -0
  73. package/dist/tm/client-unwrap.d.ts.map +1 -0
  74. package/dist/tm/client-unwrap.js +113 -0
  75. package/dist/tm/client-unwrap.js.map +1 -0
  76. package/dist/tm/config.d.ts +55 -19
  77. package/dist/tm/config.d.ts.map +1 -1
  78. package/dist/tm/config.js +74 -19
  79. package/dist/tm/config.js.map +1 -1
  80. package/dist/tm/guard.d.ts.map +1 -1
  81. package/dist/tm/guard.js +4 -7
  82. package/dist/tm/guard.js.map +1 -1
  83. package/dist/tm/index.d.ts +24 -7
  84. package/dist/tm/index.d.ts.map +1 -1
  85. package/dist/tm/index.js +111 -21
  86. package/dist/tm/index.js.map +1 -1
  87. package/dist/tm/memory.d.ts +88 -0
  88. package/dist/tm/memory.d.ts.map +1 -0
  89. package/dist/tm/memory.js +360 -0
  90. package/dist/tm/memory.js.map +1 -0
  91. package/dist/tm/pipelines.d.ts +58 -0
  92. package/dist/tm/pipelines.d.ts.map +1 -0
  93. package/dist/tm/pipelines.js +308 -0
  94. package/dist/tm/pipelines.js.map +1 -0
  95. package/dist/tm/preview.d.ts +6 -1
  96. package/dist/tm/preview.d.ts.map +1 -1
  97. package/dist/tm/preview.js +63 -13
  98. package/dist/tm/preview.js.map +1 -1
  99. package/dist/tm/ptc/budgets.d.ts +38 -0
  100. package/dist/tm/ptc/budgets.d.ts.map +1 -0
  101. package/dist/tm/ptc/budgets.js +59 -0
  102. package/dist/tm/ptc/budgets.js.map +1 -0
  103. package/dist/tm/ptc/contract.d.ts +131 -0
  104. package/dist/tm/ptc/contract.d.ts.map +1 -0
  105. package/dist/tm/ptc/contract.js +47 -0
  106. package/dist/tm/ptc/contract.js.map +1 -0
  107. package/dist/tm/ptc/driver.d.ts +37 -0
  108. package/dist/tm/ptc/driver.d.ts.map +1 -0
  109. package/dist/tm/ptc/driver.js +181 -0
  110. package/dist/tm/ptc/driver.js.map +1 -0
  111. package/dist/tm/ptc/engines.d.ts +32 -0
  112. package/dist/tm/ptc/engines.d.ts.map +1 -0
  113. package/dist/tm/ptc/engines.js +201 -0
  114. package/dist/tm/ptc/engines.js.map +1 -0
  115. package/dist/tm/ptc/gate.d.ts +28 -0
  116. package/dist/tm/ptc/gate.d.ts.map +1 -0
  117. package/dist/tm/ptc/gate.js +130 -0
  118. package/dist/tm/ptc/gate.js.map +1 -0
  119. package/dist/tm/ptc/index.d.ts +19 -0
  120. package/dist/tm/ptc/index.d.ts.map +1 -0
  121. package/dist/tm/ptc/index.js +15 -0
  122. package/dist/tm/ptc/index.js.map +1 -0
  123. package/dist/tm/ptc/pscan.d.ts +11 -0
  124. package/dist/tm/ptc/pscan.d.ts.map +1 -0
  125. package/dist/tm/ptc/pscan.js +27 -0
  126. package/dist/tm/ptc/pscan.js.map +1 -0
  127. package/dist/tm/ptc/summary.d.ts +17 -0
  128. package/dist/tm/ptc/summary.d.ts.map +1 -0
  129. package/dist/tm/ptc/summary.js +112 -0
  130. package/dist/tm/ptc/summary.js.map +1 -0
  131. package/dist/tm/ptc/tool.d.ts +35 -0
  132. package/dist/tm/ptc/tool.d.ts.map +1 -0
  133. package/dist/tm/ptc/tool.js +136 -0
  134. package/dist/tm/ptc/tool.js.map +1 -0
  135. package/dist/tm/ptc.d.ts +45 -26
  136. package/dist/tm/ptc.d.ts.map +1 -1
  137. package/dist/tm/ptc.js +71 -45
  138. package/dist/tm/ptc.js.map +1 -1
  139. package/dist/tm/result.d.ts +34 -0
  140. package/dist/tm/result.d.ts.map +1 -0
  141. package/dist/tm/result.js +87 -0
  142. package/dist/tm/result.js.map +1 -0
  143. package/dist/tm/search.d.ts +58 -0
  144. package/dist/tm/search.d.ts.map +1 -0
  145. package/dist/tm/search.js +229 -0
  146. package/dist/tm/search.js.map +1 -0
  147. package/dist/tm/shell-bridge.d.ts +22 -0
  148. package/dist/tm/shell-bridge.d.ts.map +1 -0
  149. package/dist/tm/shell-bridge.js +116 -0
  150. package/dist/tm/shell-bridge.js.map +1 -0
  151. package/dist/tm/store.d.ts.map +1 -1
  152. package/dist/tm/store.js +2 -1
  153. package/dist/tm/store.js.map +1 -1
  154. package/dist/tm/tools.d.ts +13 -71
  155. package/dist/tm/tools.d.ts.map +1 -1
  156. package/dist/tm/tools.js +22 -656
  157. package/dist/tm/tools.js.map +1 -1
  158. package/dist/tm/webfetch.d.ts +109 -0
  159. package/dist/tm/webfetch.d.ts.map +1 -0
  160. package/dist/tm/webfetch.js +305 -0
  161. package/dist/tm/webfetch.js.map +1 -0
  162. package/docs/installation.md +201 -0
  163. 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 pipelines (T1.2 + T1.3 + T1.4).
2
+ * JIT layer-2 tools — assembly of the four governed tools. The machinery
3
+ * lives in sibling modules (one responsibility each):
3
4
  *
4
- * tm_read — governed passthrough of client.file.read (T0.4-verified API)
5
- * tm_grep — governed passthrough of client.find.text (T0.4-verified API)
6
- * tm_bash — read-only allowlisted shell via the host `$` bridge
7
- * tm_fetch — paged/structured retrieval of offloaded payloads (handles)
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
- * Shared governance (every tool):
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 { ENV_PROTECT_MESSAGE, classifyBashCommand, classifyPathFields } from "../envprotect.js";
31
- import { estimateTokens, shouldOffload, shorten } from "./config.js";
32
- import { isExpired, parseRef as parseTmRef, verifyToken } from "./refs.js";
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, chars/4 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).
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 pipelines = buildPipelines(deps);
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 pipelines.tmRead(args ?? {}, ctx)),
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 pipelines.tmGrep(args ?? {}, ctx)),
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 pipelines.tmBash(args ?? {}, ctx)),
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 pipelines.tmFetch(args ?? {}, ctx)),
73
+ execute: async (args, ctx) => toToolResult(await pl.tmFetch(args ?? {}, ctx)),
708
74
  },
709
75
  };
710
76
  }