thincoder 0.12.61 → 0.12.63

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 (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -371
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
package/src/tools/web.mjs DELETED
@@ -1,224 +0,0 @@
1
- import { DESC, truncate, stripTags, htmlToText, isPrivateHost } from "./shared.mjs";
2
- import { URL } from "node:url";
3
- import { proxyFetch } from "../proxy.mjs";
4
-
5
- export const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
6
- const FETCH_TIMEOUT = 15_000
7
- // §14 D-TF3:网络失败错误文本追加 proxy 提示——纯文本提示,不自动路由(2026-08-31 裁定:proxy 显式传不自动应用)
8
- const PROXY_HINT = "network failure — retry with proxy: 'http://host:port' if the target is blocked"
9
-
10
- // ── Web search (Bing; direct by default, through proxy when configured and web toggle on) ──
11
-
12
- function extractBing(html) {
13
- const results = []
14
- // RSS format: <item><title>..</title><link>..</link><description>..</description></item>
15
- for (const m of html.matchAll(/<item>([\s\S]*?)<\/item>/gi)) {
16
- const block = m[1]
17
- const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1] ?? ""
18
- const href = block.match(/<link>([\s\S]*?)<\/link>/)?.[1] ?? ""
19
- const snippet = block.match(/<description>([\s\S]*?)<\/description>/)?.[1] ?? ""
20
- if (!href) continue
21
- results.push({ href, title: stripTags(title), snippet: stripTags(snippet) })
22
- }
23
- // Fallback: HTML b_algo blocks (older server-rendered pages)
24
- if (results.length === 0) {
25
- const blocks = html.split('<li class="b_algo"').slice(1)
26
- for (const block of blocks) {
27
- const link = block.match(/<h2[^>]*><a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/)
28
- if (!link) continue
29
- const snippet = block.match(/<p[^>]*>([\s\S]*?)<\/p>/)
30
- results.push({ href: link[1], title: stripTags(link[2]), snippet: snippet ? stripTags(snippet[1]) : "" })
31
- }
32
- }
33
- return results
34
- }
35
-
36
- function bingUrl(query, page) {
37
- let u = `https://www.bing.com/search?q=${encodeURIComponent(query)}&format=rss&setlang=en`
38
- if (page > 1) u += `&first=${(page - 1) * 10 + 1}`
39
- return u
40
- }
41
-
42
- const ENGINES = [{ name: "bing", label: "Bing", url: bingUrl, extract: extractBing, ua: UA }]
43
- const ENGINE_NAMES = ENGINES.map(e => e.name)
44
-
45
- /** Structured search via Tavily (optional — config.websearch.apiKey). Returns
46
- * { engine, results } or null to fall back to Bing HTML scraping.
47
- * proxyUri: explicit per-call proxy (args.proxy) — never the config.json one. */
48
- async function fetchTavily(query, limit, ctx, proxyUri) {
49
- const apiKey = ctx?.agent?.config?.websearch?.apiKey
50
- if (!apiKey) return null
51
- const ctrl = new AbortController()
52
- const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT)
53
- try {
54
- const response = await proxyFetch("https://api.tavily.com/search", {
55
- method: "POST",
56
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
57
- body: JSON.stringify({ query, search_depth: "basic", max_results: limit, include_answer: false, include_raw_content: false }),
58
- signal: ctrl.signal,
59
- }, proxyUri)
60
- if (!response.ok) return null
61
- const data = await response.json()
62
- const results = (Array.isArray(data.results) ? data.results : []).map((r) => ({
63
- href: r.url, title: r.title ?? "", snippet: r.content ?? "", _engine: "tavily",
64
- }))
65
- return { engine: "tavily", results }
66
- } catch { return null }
67
- finally { clearTimeout(timer) }
68
- }
69
-
70
- async function fetchEngine(engine, query, page, proxyUri) {
71
- const ctrl = new AbortController()
72
- const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT)
73
- try {
74
- const response = await proxyFetch(engine.url(query, page), {
75
- headers: { "User-Agent": engine.ua, "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8" },
76
- signal: ctrl.signal,
77
- }, proxyUri)
78
- if (!response.ok) return null
79
- const html = await response.text()
80
- const results = engine.extract(html)
81
- return { engine: engine.name, results }
82
- } catch { return null }
83
- finally { clearTimeout(timer) }
84
- }
85
-
86
- export const websearchTool = {
87
- name: "websearch",
88
- description: DESC("websearch"),
89
- parameters: {
90
- type: "object",
91
- properties: {
92
- query: { type: "string", description: "Search query" },
93
- limit: { type: "number", description: "Max results (default 8, max 20)" },
94
- engine: { type: "string", enum: ENGINE_NAMES, description: "Specific engine — \"bing\" (Bing). Omit to search all engines concurrently." },
95
- page: { type: "number", description: "Page number for pagination (1-based, default 1). Only used when engine is specified." },
96
- proxy: { type: "string", description: "http://host:port explicit proxy (optional) — use ONLY when passed; no proxy = direct. config.json proxy is NOT auto-applied (2026-08-31 ruling: fixed config breaks domestic sites)" },
97
- },
98
- required: ["query"],
99
- },
100
- readonly: true,
101
- async execute(args, ctx) {
102
- const limit = Math.min(args.limit ?? 8, 20)
103
- const page = Math.max(1, args.page ?? 1)
104
- // 2026-08-31 ruling: proxy is a PER-CALL decision (args.proxy), never the config.json
105
- // fixed configuration. Model picks by target: github/foreign → pass proxy; gitee/domestic → omit.
106
- const proxyUri = args.proxy ?? null
107
- // Structured search first when a Tavily key is configured — stable, dated,
108
- // no HTML scraping. Falls back to Bing silently.
109
- const tavily = await fetchTavily(args.query, limit, ctx, proxyUri)
110
- if (tavily && tavily.results.length > 0) {
111
- return truncate(tavily.results.slice(0, limit).map((r, i) => `${i + 1}. [tavily] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
112
- }
113
- if (args.engine) {
114
- const engine = ENGINES.find(e => e.name === args.engine)
115
- if (!engine) return `Unknown engine '${args.engine}'. Available: ${ENGINE_NAMES.join(", ")}`
116
- const fetched = await fetchEngine(engine, args.query, page, proxyUri)
117
- if (!fetched || fetched.results.length === 0) return "(no results)"
118
- return truncate(fetched.results.slice(0, limit).map((r, i) => `${i + 1}. [${engine.label}] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
119
- }
120
- const promises = ENGINES.map(e => fetchEngine(e, args.query, 1, proxyUri))
121
- const fetched = (await Promise.all(promises)).filter(Boolean)
122
- if (fetched.length === 0) return "(no results)"
123
- const merged = [], indexes = fetched.map(() => 0)
124
- let done = false
125
- while (!done && merged.length < limit) {
126
- done = true
127
- for (let i = 0; i < fetched.length; i++) {
128
- if (indexes[i] < fetched[i].results.length) {
129
- merged.push({ ...fetched[i].results[indexes[i]], _engine: fetched[i].engine })
130
- indexes[i]++; done = false
131
- if (merged.length >= limit) break
132
- }
133
- }
134
- }
135
- return truncate(merged.slice(0, limit).map((r, i) => `${i + 1}. [${r._engine}] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
136
- },
137
- }
138
-
139
- // ── Fetch tool (with proxy support) ──────
140
-
141
- function isPrivateUrl(urlStr) {
142
- let u; try { u = new URL(urlStr) } catch { return true }
143
- return isPrivateHost(u.hostname)
144
- }
145
-
146
- // proxyFetch returns a native Response (Headers object, needs .get()) without proxy,
147
- // but a Response-like with a plain lowercase-keyed Record through the CONNECT tunnel — handle both.
148
- function headerOf(res, name) {
149
- const h = res.headers
150
- if (!h) return null
151
- if (typeof h.get === "function") return h.get(name)
152
- return h[name.toLowerCase()] ?? null
153
- }
154
-
155
- /** Validate a redirect target: resolve relative → absolute, http/https only, and
156
- * SSRF-checked. Returns { target } on success or { error } (never follows into a
157
- * private host — the redirect SSRF bypass). */
158
- export function resolveRedirectTarget(loc, baseUrl) {
159
- let target
160
- try { target = new URL(loc, baseUrl).toString() } catch { return { error: "invalid redirect location" } }
161
- if (!/^https?:\/\//.test(target)) return { error: "redirect target must be http/https" }
162
- if (isPrivateUrl(target)) return { error: "redirect target is internal/private/metadata" }
163
- return { target }
164
- }
165
-
166
- /** Heuristic: a fetch result that is a near-empty shell or a region-block page hides
167
- * the real content. Flags the pattern so the model switches strategy instead of
168
- * re-fetching blind (2026-08-31: 21-char MiniMax SPA shell + "App unavailable in
169
- * region" Claude page both wasted an hour of round trips). */
170
- export function detectSparseHtml(html, text) {
171
- if (text.length >= 300) return ""
172
- if (/unavailable in (your )?region|not available in (your )?region|app-unavailable|enable.?javascript|just a moment|attention required|cf-browser-verification/i.test(html)) {
173
- return "\n\n[fetch hint: page looks region-blocked or JS-gated (body <300 chars). Try a search MCP tool (configured provider) or the site's .md/raw/mirror endpoint]"
174
- }
175
- if (/<div[^>]+id="(app|root)"[^>]*>\s*<\/div>|id="app"|id="root"/i.test(html)) {
176
- return "\n\n[fetch hint: page is a JS-rendered SPA shell (body <300 chars) — content loads client-side. Try a search MCP tool or the site's .md/API endpoint]"
177
- }
178
- return ""
179
- }
180
-
181
- export const fetchTool = {
182
- name: "fetch",
183
- description: DESC("fetch"),
184
- parameters: {
185
- type: "object",
186
- properties: {
187
- url: { type: "string", description: "http/https URL" },
188
- proxy: { type: "string", description: "http://host:port explicit proxy (optional) — use ONLY when passed; no proxy = direct. config.json proxy is NOT auto-applied (2026-08-31 ruling); pick per target (github/foreign sites need a proxy, gitee/domestic don't)" },
189
- },
190
- required: ["url"],
191
- },
192
- readonly: true,
193
- async execute(args, _ctx) {
194
- if (!/^https?:\/\//.test(args.url)) throw new Error("url must start with http:// or https://")
195
- if (isPrivateUrl(args.url)) throw new Error("fetch blocked: internal/private/metadata addresses are not allowed")
196
- try {
197
- // 2026-08-31 ruling: per-call decision — args.proxy when passed, direct otherwise.
198
- const proxyUri = args.proxy ?? null
199
- const response = await proxyFetch(args.url, { headers: { "User-Agent": UA } }, proxyUri)
200
- if (!response.ok) {
201
- if ([301, 302, 307, 308].includes(response.status)) {
202
- const loc = headerOf(response, "location")
203
- if (loc) {
204
- // SSRF-check the redirect target (resolve relative → absolute) before
205
- // following — a 3xx must not bounce a public URL into a private host.
206
- const r = resolveRedirectTarget(loc, args.url)
207
- if (r.error) throw new Error(`fetch failed: ${r.error}`)
208
- const r2 = await proxyFetch(r.target, { headers: { "User-Agent": UA } }, proxyUri)
209
- if (!r2.ok) throw new Error(`fetch failed: HTTP ${r2.status}\n${PROXY_HINT}`)
210
- const ct2 = headerOf(r2, "content-type") ?? ""
211
- const b2 = await r2.text()
212
- if (ct2.includes("text/html")) { const t = htmlToText(b2); return truncate(t + detectSparseHtml(b2, t)) }
213
- return truncate(b2)
214
- }
215
- }
216
- throw new Error(`fetch failed: HTTP ${response.status}\n${PROXY_HINT}`)
217
- }
218
- const ct = headerOf(response, "content-type") ?? ""
219
- const body = await response.text()
220
- if (ct.includes("text/html")) { const t = htmlToText(body); return truncate(t + detectSparseHtml(body, t)) }
221
- return truncate(body)
222
- } catch (e) { throw new Error(`fetch failed: ${e.cause?.code ?? e.message}\n${PROXY_HINT}`, { cause: e }) }
223
- },
224
- }
@@ -1,16 +0,0 @@
1
- Search the web via Bing. Returns result titles, URLs, and snippets. Use for looking up current information, docs, error messages.
2
-
3
- Parameters:
4
- - query (required): Search query
5
- - limit: Max results (default 8, max 20)
6
- - engine: Specific engine to use — "bing" (Bing). Omit to search all engines concurrently.
7
- - page: Page number for pagination (1-based, default 1). Only used when engine is specified.
8
- - proxy: http://host:port explicit proxy (optional) — use ONLY when passed; no proxy = direct. config.json proxy is NOT auto-applied (2026-08-31 ruling); Bing/foreign sites usually need a proxy, domestic targets don't
9
-
10
- Notes:
11
- - Before searching the web, call the memory tool (action: search) first — you may already know the answer from a previous session. Only reach for websearch if memory comes up empty.
12
- - Runs synchronously — returns when the search completes (all selected engines run in parallel).
13
- - Use this for information that is NOT in the local codebase — current docs, error messages, API references
14
- - Follow up with `fetch` to read full pages from the results
15
- - **Weak engine warning**: Bing's index is noisy for technical queries — if a first websearch returns irrelevant/townhall-grade results, DO NOT retry the same query. Configure a search MCP tool (e.g. `glm-websearch` via the MCP config) for technical lookups; websearch is the fallback.
16
- - Proxy: NOT auto-applied from config.json (2026-08-31 ruling). Pass `proxy: "http://host:port"` explicitly when the target needs one; omit for domestic targets.
@@ -1,11 +0,0 @@
1
- Write content to a file. Creates parent directories; overwrites existing file.
2
-
3
- Parameters:
4
- - path (required): File path, relative to cwd or absolute
5
- - content (required): Full content to write
6
-
7
- Notes:
8
- - write replaces the WHOLE file — read it first and confirm you intend to rewrite it entirely; for a small change use edit / insert_after.
9
- - Returns `Wrote N chars to <path>` + git diff + syntax-check note.
10
- - This overwrites the entire file — use `edit` for targeted changes
11
- - The file is atomic: it either writes completely or fails
@@ -1,224 +0,0 @@
1
- /**
2
- * trace-store.mjs — §18.6 完整轨迹存档(AGENT-LOOP.md §18.6 D-TR1..TR8 权威规格)。
3
- *
4
- * 完整请求-响应轨迹落盘:每次 chat() 调用一个 JSONL 文件到
5
- * ~/.thincoder/traces/YYYY-MM-DD/<sessionKey>-<seq>.jsonl。采集点唯一 = core.mjs
6
- * chat() 导出出口(所有模型调用——主回合/消化轮/compress/distill/advisor/子代理/
7
- * auto-think/consult——都经该函数)——续写/重试在出口已合并(reasoning 全量)。
8
- *
9
- * 纪律(与 log.mjs 同源惯例):
10
- * - 真 fire-and-forget(F-TR3——模型调用路径零额外阻塞):seq 同步预留(原子号位),
11
- * 写盘异步(node:fs/promises——不 await);落盘失败静默降级(不抛错、不阻塞
12
- * chat() 返回)。recordChatTrace 返回落盘 promise——仅供测试/显式消费方 await。
13
- * - 测试隔离:node --test 进程(NODE_TEST_CONTEXT)默认不写盘——防测试事件污染真实
14
- * 轨迹目录(既有测试跑真实 agent 管线会产生数百次 chat 调用);显式设置
15
- * THINCODER_TRACES_DIR 强制写入该目录(traces.test.mjs 用它隔离临时目录——
16
- * 与 log.mjs 的 THINCODER_LOG_DIR 同惯例)。
17
- * - 脱敏(D-TR2):复用 log.mjs 字段名黑名单(apikey/designtoken/password/secret/
18
- * token/authorization/proxyuri/proxy)+ SECRET_FORM 形态扫描(redactSecret)——对
19
- * messages/content/reasoning/toolCalls/error 全字段递归应用;不发明新遮蔽模式。
20
- * - 容量有界(N-TR1):单条轨迹一个文件,大小不限("成本不是问题"——不截断、不
21
- * 脱漏行);目录按日组织(YYYY-MM-DD),可按日/会话过滤;不自动清理(D-TR8——
22
- * 清理机制记 docs/TODO.md 技术组)。
23
- * - seq = 当日目录内最大已有 seq + 1(D-TR3——跨会话/进程重启不覆写既有旧轨迹——
24
- * 18.6.1 评审 #3)。
25
- * - 日期分日按本地时区(D-TR3——2026-09-04 fix round1:初版 toISOString()=UTC——
26
- * 本地 00:00-08:00 的调用会落进前一日目录——改本地日期字符串 YYYY-MM-DD——T-TR12)。
27
- * - isContinuation(D-TR1——2026-09-04 fix round1):续写/重试链标记——true = 该调用
28
- * 是续写链的一环(core.mjs 续写递归传出 logCtx.isContinuation);新调用 false。
29
- * 记录不输出 round 字段(全设计无 round 定义——删——不发明无来源字段)。
30
- */
31
- import { readdirSync, existsSync } from "node:fs"
32
- import { appendFile, mkdir, readdir, stat, unlink, rmdir } from "node:fs/promises"
33
- import { join } from "node:path"
34
- import { createHash } from "node:crypto"
35
- import { configDir } from "../config.mjs"
36
- import { redactSecret, errText, classifyErr } from "../log.mjs"
37
- import { normalizeCwd } from "../session-slots.mjs"
38
-
39
- /** 轨迹根目录:THINCODER_TRACES_DIR(测试隔离/override——同 THINCODER_LOG_DIR
40
- * 惯例)> ~/.thincoder/traces(configDir——D-TR3——与 sessions/ 同域)。 */
41
- export function tracesRoot() {
42
- return process.env.THINCODER_TRACES_DIR ?? join(configDir, "traces")
43
- }
44
-
45
- /** 写门(测试隔离):test runner 进程(NODE_TEST_CONTEXT)默认跳过——除显式
46
- * THINCODER_TRACES_DIR override(traces.test.mjs 隔离临时目录)。 */
47
- function writeEnabled() {
48
- if (process.env.NODE_TEST_CONTEXT && !process.env.THINCODER_TRACES_DIR) return false
49
- return true
50
- }
51
-
52
- /** sessionKey = sha1(normalizeCwd(cwd))[:12](D-TR3——与 session-slots sessionPath
53
- * 同算法同 cwd 归一——两端 hash 一致)。 */
54
- export function traceSessionKey(cwd) {
55
- return createHash("sha1").update(normalizeCwd(cwd)).digest("hex").slice(0, 12)
56
- }
57
-
58
- /** 本地时区日期字符串 YYYY-MM-DD(D-TR3——2026-09-04 fix round1:初版
59
- * toISOString()=UTC 日期——本地 00:00-08:00 落前一日目录;本地日期才是用户视角的
60
- * "今天"——T-TR12)。recordChatTrace 与测试读取 helpers 共用同一实现——不双写。 */
61
- export function localDateStr(date = new Date()) {
62
- const y = date.getFullYear()
63
- const m = String(date.getMonth() + 1).padStart(2, "0")
64
- const d = String(date.getDate()).padStart(2, "0")
65
- return `${y}-${m}-${d}`
66
- }
67
-
68
- /** Test hooks(_rateHooks 同惯例——rate.mjs:测试可替换 now——T-TR12 注入本地日 ≠
69
- * UTC 日的时刻验证分日;生产永远走真实时钟)。 */
70
- export const _traceHooks = {
71
- now: () => new Date(),
72
- }
73
-
74
- /** 当日轨迹目录(D-TR3:traces/YYYY-MM-DD——分日组织——N-TR1)。 */
75
- export function tracesDirFor(dateStr) {
76
- return join(tracesRoot(), dateStr)
77
- }
78
-
79
- // 进程内 seq 预留表:同步预留(原子——异步写盘在途时并发调用不撞号);键 = 目录。
80
- // 跨进程/重启由磁盘 max 兜底(同键无预留时磁盘值即事实——T-TR10)。
81
- const _reservedSeq = new Map()
82
-
83
- /** seq = max(当日目录最大已有 seq, 进程内已预留) + 1(D-TR3——跨会话/进程重启
84
- * 不覆写——T-TR8/T-TR10;同步预留 = 异步写盘启动前的原子号位分配)。 */
85
- export function nextTraceSeq(dateStr) {
86
- const dir = tracesDirFor(dateStr)
87
- let max = 0
88
- if (existsSync(dir)) {
89
- let names
90
- try {
91
- names = readdirSync(dir)
92
- } catch {
93
- // 目录不可读——按预留表续号(不静默撞号)
94
- max = _reservedSeq.get(dir) ?? 0
95
- _reservedSeq.set(dir, max + 1)
96
- return max + 1
97
- }
98
- for (const name of names) {
99
- const m = name.match(/-(\d+)\.jsonl$/)
100
- if (m) max = Math.max(max, Number(m[1]))
101
- }
102
- }
103
- const seq = Math.max(max, _reservedSeq.get(dir) ?? 0) + 1
104
- _reservedSeq.set(dir, seq)
105
- return seq
106
- }
107
-
108
- /** 开关(D-TR6):traces.enabled 缺省 on(默认全采集);logCtx.traces === false
109
- * 显式关闭(调用点读取 agent.config.traces.enabled——D-TR6)。 */
110
- export function tracesEnabled(logCtx) {
111
- return logCtx?.traces !== false
112
- }
113
-
114
- /** 递归脱敏(D-TR2):对 messages/content/reasoning/toolCalls/error 全字段应用——
115
- * 字段名命中黑名单 → 遮蔽;字符串内容命中密钥形态(sk-/Bearer/-key=)→ 截断到
116
- * 形态前 + 标记。数组/对象递归(消息 content 可为 parts 数组、toolCalls 嵌套)。 */
117
- function redactValue(fieldKey, value) {
118
- if (typeof value === "string") return redactSecret(fieldKey, value)
119
- if (Array.isArray(value)) return value.map((v) => redactValue(fieldKey, v))
120
- if (value && typeof value === "object") {
121
- const out = {}
122
- for (const [k, v] of Object.entries(value)) out[k] = redactValue(k, v)
123
- return out
124
- }
125
- return value
126
- }
127
-
128
- /**
129
- * 收集一次 chat 调用轨迹(D-TR1 字段集)——fire-and-forget(F-TR3)。
130
- * 由 core.mjs chat() 导出出口调用(唯一采集点——N-TR2);签名零参数膨胀——
131
- * 数据全部来自调用方已传入的 opts(logCtx 元数据——D-TR4)+ provider + result/error。
132
- *
133
- * @param {Object} provider chat() 的 provider 参数(provider/model 字段来源)
134
- * @param {Object} opts chat() 原始 opts —— messages(输入)与 logCtx(元数据:
135
- * role/depth/kind/session/cwd/stage/turn/traces——调用点增补,D-TR4)
136
- * @param {Object|null} result chatImpl 返回值(成功路径——content/reasoning/
137
- * toolCalls/usage/finishReason;N-TR3:出口汇总——续写/重试后全量)
138
- * @param {Error|null} error chat() 抛出的错误(失败路径——D-TR5:error(errText
139
- * 截断 + 类别)+ finishReason:null——失败轨迹恰是分析纠结点最有效的材料)
140
- */
141
- export function recordChatTrace(provider, opts = {}, result = null, error = null) {
142
- if (!writeEnabled()) return
143
- const logCtx = opts.logCtx ?? {}
144
- if (!tracesEnabled(logCtx)) return
145
- // cwd/session 经 logCtx 增补(调用点传 agent.cwd / agent._sessionStart——
146
- // D-TR3 的 cwdHash/命名与 D-TR1 的 session 字段所需);无 agent 作用域的调用点
147
- // 回退 process.cwd()——CLI 会话即工作区。该类点开关状态:distill 已含
148
- // traces 开关(D-TR6 fix round1);auto-think 已闭环(D-TS12——chat 调用点
149
- // logCtx 全字段补传:traces/session/cwd/role/depth/kind——无残留点)。
150
- const cwd = logCtx.cwd ?? process.cwd()
151
- // D-TR3(fix round1):分日按本地时区(_traceHooks.now——T-TR12 注入点);
152
- // ts 与 dateStr 同源(同一时刻)——记录时间戳与目录日不撕裂。
153
- const now = _traceHooks.now()
154
- const dateStr = localDateStr(now)
155
- const seq = nextTraceSeq(dateStr)
156
- const sessionKey = traceSessionKey(cwd)
157
- const record = {
158
- ts: now.toISOString(),
159
- session: logCtx.session ?? null,
160
- cwdHash: createHash("sha1").update(normalizeCwd(cwd)).digest("hex"),
161
- role: logCtx.role ?? null,
162
- depth: logCtx.depth ?? null,
163
- turn: logCtx.turn ?? null,
164
- provider: provider?.name ?? provider?.model ?? "unknown",
165
- model: provider?.model ?? "",
166
- stage: logCtx.stage ?? null,
167
- kind: logCtx.kind ?? null,
168
- // D-TR1(fix round1):续写/重试链标记——true = 该调用是续写链的一环
169
- // (core.mjs 续写递归传出);新调用缺省 false——round 字段已删(无来源)。
170
- isContinuation: logCtx.isContinuation === true,
171
- messages: redactValue("messages", opts.messages ?? []),
172
- content: redactValue("content", result?.content ?? null),
173
- reasoning: redactValue("reasoning", result?.reasoning ?? null),
174
- toolCalls: redactValue("toolCalls", result?.toolCalls ?? null),
175
- usage: result?.usage ?? null,
176
- finishReason: result?.finishReason ?? null,
177
- }
178
- if (error != null) {
179
- // D-TR5:错误路径轨迹——error(errText 截断 + 类别)+ finishReason:null
180
- record.error = {
181
- err: redactValue("err", errText(error, 500)),
182
- kind: classifyErr(error, opts.signal),
183
- }
184
- }
185
- // 真 fire-and-forget(F-TR3——模型调用路径零额外阻塞):seq 已在上面同步预留
186
- // (原子号位——写盘在途并发不撞号);写盘异步(不 await——chat() 出口立即返回)。
187
- // 返回值 = 落盘 promise(测试 await 用;chat() 不消费——fire-and-forget 语义)。
188
- return (async () => {
189
- try {
190
- const dir = tracesDirFor(dateStr)
191
- await mkdir(dir, { recursive: true }) // D-TR3:写前建目录(与 sessions/tool-results 同惯例)
192
- await appendFile(join(dir, `${sessionKey}-${seq}.jsonl`), JSON.stringify(record) + "\n", "utf8")
193
- } catch {
194
- // F-TR3:落盘失败静默降级——不抛错、不阻塞 chat() 返回
195
- }
196
- })()
197
- }
198
-
199
- /**
200
- * D-TR10(2026-09-05 用户裁定——发布隐私 + 磁盘卫生):启动清理——删除 traces 根下
201
- * mtime 超过保留期的轨迹文件(保留期 = config.traces.retentionHours,默认 24h);
202
- * 删空的日期目录(YYYY-MM-DD)。目录里非 .jsonl 文件不碰。CLI 启动点 fire-and-forget
203
- * 调用(不 await——不阻塞启动——失败静默——与轨迹写盘同纪律)。返回删除文件数。
204
- */
205
- export async function cleanupTraces({ dir = tracesRoot(), retentionHours = 24 } = {}) {
206
- const cutoff = Date.now() - retentionHours * 3_600_000
207
- let days
208
- try { days = await readdir(dir) } catch { return 0 } // 目录不存在/不可读 → 无事可做
209
- let removed = 0
210
- for (const day of days) {
211
- const dayDir = join(dir, day)
212
- try { if (!(await stat(dayDir)).isDirectory()) continue } catch { continue }
213
- let names
214
- try { names = await readdir(dayDir) } catch { continue }
215
- for (const n of names) {
216
- if (!n.endsWith(".jsonl")) continue
217
- try {
218
- if ((await stat(join(dayDir, n))).mtimeMs < cutoff) { await unlink(join(dayDir, n)); removed++ }
219
- } catch { /* 单个文件失败不影响其余 */ }
220
- }
221
- try { if ((await readdir(dayDir)).length === 0) await rmdir(dayDir) } catch {}
222
- }
223
- return removed
224
- }