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.
- package/CHANGELOG.md +34 -1
- package/README.md +13 -12
- package/bin/thincoder.mjs +63 -28
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -371
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +32 -10
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +88 -0
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +10 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +31 -4
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +206 -0
- package/src/tui/index.mjs +40 -12
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +85 -0
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +17 -6
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +49 -17
- package/src/tui/subagent-blocks.mjs +21 -3
- package/src/tui/subagent-children.mjs +86 -14
- package/src/tui/subagent-freeze.mjs +80 -3
- package/src/tui/suspension-drive.mjs +48 -22
- package/src/tui/tool-args.mjs +5 -2
- package/src/tui/tool-display.mjs +16 -2
- package/src/tui/tool-events.mjs +64 -22
- package/src/tui/tui-lifecycle.mjs +9 -2
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +21 -5
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -242
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -228
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -191
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -468
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -289
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -294
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -479
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -453
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -413
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -381
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -83
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -413
- package/src/memory/core.mjs +0 -300
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -417
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -217
- package/src/prompts/discipline-normal.md +0 -179
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -55
- package/src/prompts/persona-engineering.md +0 -54
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -214
- package/src/session-guard.mjs +0 -47
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-slots.mjs +0 -489
- package/src/session.mjs +0 -475
- package/src/skills.mjs +0 -153
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- 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
|
-
}
|
package/src/tools/websearch.md
DELETED
|
@@ -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.
|
package/src/tools/write.md
DELETED
|
@@ -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
|
-
}
|