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/patch.mjs
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DESC,
|
|
3
|
-
autoSyntaxCheck,
|
|
4
|
-
resolveInCwd,
|
|
5
|
-
normalizeEOL,
|
|
6
|
-
detectFileEol,
|
|
7
|
-
majorityEol
|
|
8
|
-
} from "./shared.mjs";
|
|
9
|
-
import { markDirty } from "./file.mjs";
|
|
10
|
-
import { execFileSync } from "node:child_process";
|
|
11
|
-
import { mkdir, readFile, lstat, writeFile, unlink } from "node:fs/promises";
|
|
12
|
-
import { existsSync } from "node:fs";
|
|
13
|
-
import { relative, dirname } from "node:path";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Parse a unified diff: returns [{ path, isNew, hunks: [{ ops: [{type:" "|"-"|"+", text}] }] }]
|
|
17
|
-
* Consume hunk lines by the line counts in the @@ header — LLMs often strip context blank lines to pure empty lines,
|
|
18
|
-
* so we use counts rather than first characters to determine hunk boundaries.
|
|
19
|
-
* APPLY-PATCH.md §3: a bare "@@" header (no coordinates) is accepted — the hunk body runs until the next hunk/file header
|
|
20
|
-
* ("@@" / "--- " / "+++ " / "diff " / "index "), purely located by its ops.
|
|
21
|
-
*/
|
|
22
|
-
/**
|
|
23
|
-
* P15.10(2026-09-05 用户裁定——「符合模型直觉」):文件头判定——
|
|
24
|
-
* 完整头(`--- x` 后随 `+++ `)任意老路径形态均认(git 规范);
|
|
25
|
-
* 容缺头(`+++ b/<path>` 配对行省略——模型单文件补丁自然形态)仅认 a//b/ 前缀——
|
|
26
|
-
* newPath 推导 = oldPath;`/dev/null` 容缺仍拒(新文件名从 --- 侧不可推导——parsePatch 内特报);
|
|
27
|
-
* 其他 `--- x` = 普通删行内容(行首标记 - + 内容 `-- x`)——不是文件头——hunk 体不得误断。
|
|
28
|
-
*/
|
|
29
|
-
function isFileHeader(line, nextLine) {
|
|
30
|
-
if (!line.startsWith("--- ")) return false
|
|
31
|
-
if (nextLine?.startsWith("+++ ")) return true
|
|
32
|
-
return /^[ab]\//.test(line.slice(4).trim())
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function parsePatch(patch) {
|
|
36
|
-
// Patch text often comes from CRLF terminals/model output; trailing \r mixed into hunk content breaks context matching, strip uniformly
|
|
37
|
-
const lines = patch.replace(/\r(?=\n|$)/g, "").split("\n")
|
|
38
|
-
const files = []
|
|
39
|
-
let cur = null
|
|
40
|
-
let i = 0
|
|
41
|
-
const stripPrefix = (p) => p.replace(/^[ab]\//, "")
|
|
42
|
-
while (i < lines.length) {
|
|
43
|
-
const line = lines[i]
|
|
44
|
-
if (line.startsWith("--- ")) {
|
|
45
|
-
const oldPath = line.slice(4).trim()
|
|
46
|
-
const plus = lines[i + 1]
|
|
47
|
-
if (plus?.startsWith("+++ ")) {
|
|
48
|
-
const newPath = plus.slice(4).trim()
|
|
49
|
-
if (newPath === "/dev/null") throw new Error("Deleting files via patch is not supported — use the delete tool")
|
|
50
|
-
cur = { path: stripPrefix(newPath), isNew: oldPath === "/dev/null", hunks: [] }
|
|
51
|
-
files.push(cur)
|
|
52
|
-
i += 2
|
|
53
|
-
continue
|
|
54
|
-
}
|
|
55
|
-
// P15.10:容缺头——`--- a/<path>`(或 b/ 前缀)后直接跟 hunk = 对同路径的修改。
|
|
56
|
-
if (/^[ab]\//.test(oldPath)) {
|
|
57
|
-
cur = { path: stripPrefix(oldPath), isNew: false, hunks: [] }
|
|
58
|
-
files.push(cur)
|
|
59
|
-
i += 1
|
|
60
|
-
continue
|
|
61
|
-
}
|
|
62
|
-
if (oldPath === "/dev/null") {
|
|
63
|
-
throw new Error(`"--- /dev/null" needs a "+++ b/<path>" line naming the new file — the --- side does not carry the file name`)
|
|
64
|
-
}
|
|
65
|
-
throw new Error(`Malformed patch: expected "+++" line after "${line}"`)
|
|
66
|
-
}
|
|
67
|
-
if (line.startsWith("@@")) {
|
|
68
|
-
if (!cur) throw new Error("Malformed patch: hunk header before any file header")
|
|
69
|
-
const m = line.match(/^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/)
|
|
70
|
-
if (!m) {
|
|
71
|
-
// APPLY-PATCH.md §3:坐标裸 "@@" 头——hunk 完全靠操作行定位。
|
|
72
|
-
// 无行数可用:操作行以 " " / "-" / "+" 开头(空行宽容为上下文行),
|
|
73
|
-
// 直到下一个 hunk 头 / 文件头 "@@"/"--- "/"+++ "/"diff "/"index " 为止。
|
|
74
|
-
if (!/^@@(?: @@)?\s*$/.test(line)) {
|
|
75
|
-
throw new Error(`Malformed patch: bad hunk header "${line}" (need @@ -old,count +new,count @@ or bare @@)`)
|
|
76
|
-
}
|
|
77
|
-
const hunk = { ops: [] }
|
|
78
|
-
i++
|
|
79
|
-
while (i < lines.length) {
|
|
80
|
-
const hl = lines[i]
|
|
81
|
-
if (hl.startsWith("@") || isFileHeader(hl, lines[i + 1]) || hl.startsWith("+++ ") || hl.startsWith("diff ") || hl.startsWith("index ")) break
|
|
82
|
-
if (hl.startsWith("\\")) { i++; continue } // ""
|
|
83
|
-
// 宽容空行=上下文行——但 patch 文本末尾(或 hunk 之间/文件头之前)的 "" 是
|
|
84
|
-
// 分隔产物而非内容:仅当后继仍是操作行时才当作上下文消费。
|
|
85
|
-
// 复评 #1(2026-09-04):文件头 "--- "/"+++ " 以 -/+ 开头会被 op 前缀判定误收——
|
|
86
|
-
// 文件头前的分隔空行不得吞成幽灵上下文行(会把 - 锚序列尾部拼上 ""——跨文件
|
|
87
|
-
// 零上下文 hunk 因此误报 not-found)。
|
|
88
|
-
const next = lines[i + 1]
|
|
89
|
-
if (hl === "" && (next == null || !/^[ +\-\\]/.test(next) || isFileHeader(next, lines[i + 2]) || next.startsWith("+++ "))) break
|
|
90
|
-
const tag = hl === "" ? " " : hl[0]
|
|
91
|
-
if (tag !== " " && tag !== "-" && tag !== "+") break // metadata / file section end
|
|
92
|
-
hunk.ops.push({ type: tag, text: hl === "" ? "" : hl.slice(1) })
|
|
93
|
-
i++
|
|
94
|
-
}
|
|
95
|
-
if (hunk.ops.length === 0) throw new Error(`Malformed patch: empty coordinate-less hunk "${line.trim()}"`)
|
|
96
|
-
const ctxCount = hunk.ops.filter((o) => o.type === " ").length
|
|
97
|
-
// §3 (APPLY-PATCH.md——2026-09-04):context<2 且含 ≥1 个 - 行 → 接受——定位锚 = hunk 内
|
|
98
|
-
// 匹配行序列(空格上下文行 + - 行——按出现序)连续——唯一匹配即应用(applyHunks 既有锚匹配域——
|
|
99
|
-
// 多匹配 / not-found 语义不变)。0 上下文与 1 上下文同待遇(评审 #4a)。
|
|
100
|
-
// 纯 +(无 - 锚)且 context<2 仍拒——插入位置不可判——报错引导加锚(NF15.8c)。
|
|
101
|
-
const removedCount = hunk.ops.filter((o) => o.type === "-").length
|
|
102
|
-
if (ctxCount < 2 && removedCount === 0) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`Coordinate-less hunk ${cur.hunks.length + 1} in ${cur.path} has ${ctxCount} context line(s) — add more context lines`,
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
cur.hunks.push(hunk)
|
|
108
|
-
continue
|
|
109
|
-
}
|
|
110
|
-
let oldNeed = m[1] == null ? 1 : Number(m[1])
|
|
111
|
-
let newNeed = m[2] == null ? 1 : Number(m[2])
|
|
112
|
-
const hunk = { ops: [] }
|
|
113
|
-
i++
|
|
114
|
-
while (oldNeed > 0 || newNeed > 0) {
|
|
115
|
-
if (i >= lines.length) throw new Error("Malformed patch: hunk truncated (line counts in @@ header not satisfied)")
|
|
116
|
-
const hl = lines[i]
|
|
117
|
-
if (hl.startsWith("\\")) { i++; continue } // ""
|
|
118
|
-
const tag = hl === "" ? " " : hl[0] // pure blank line: treat leniently as context line
|
|
119
|
-
const text = hl === "" ? "" : hl.slice(1)
|
|
120
|
-
if (tag === " ") { hunk.ops.push({ type: " ", text }); oldNeed--; newNeed-- }
|
|
121
|
-
else if (tag === "-") { hunk.ops.push({ type: "-", text }); oldNeed-- }
|
|
122
|
-
else if (tag === "+") { hunk.ops.push({ type: "+", text }); newNeed-- }
|
|
123
|
-
else throw new Error(`Malformed patch: unexpected line "${hl.slice(0, 60)}" inside hunk`)
|
|
124
|
-
i++
|
|
125
|
-
}
|
|
126
|
-
cur.hunks.push(hunk)
|
|
127
|
-
continue
|
|
128
|
-
}
|
|
129
|
-
i++ // skip diff --git / index / blank lines and other metadata
|
|
130
|
-
}
|
|
131
|
-
// P15.10:容缺/完整空段头(头后无任何 hunk)过滤——不虚报 touchedPaths、不触发无谓 read+write
|
|
132
|
-
const withHunks = files.filter((f) => f.hunks.length > 0)
|
|
133
|
-
if (withHunks.length === 0) throw new Error("No file changes found in patch (need --- / +++ headers)")
|
|
134
|
-
return withHunks
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Apply hunks sequentially onto an in-memory line array; any failure throws (caller guarantees nothing is written to disk). Ignores trailing \r when comparing; context lines retain original bytes */
|
|
138
|
-
function applyHunks(fileLines, hunks, eol, path) {
|
|
139
|
-
const cr = eol === "\r\n" ? "\r" : ""
|
|
140
|
-
for (let h = 0; h < hunks.length; h++) {
|
|
141
|
-
const oldSeq = hunks[h].ops.filter((o) => o.type !== "+").map((o) => o.text)
|
|
142
|
-
if (oldSeq.length === 0) throw new Error(`Hunk ${h + 1} in ${path} has no context/removed lines to locate`)
|
|
143
|
-
const matches = []
|
|
144
|
-
for (let pos = 0; pos + oldSeq.length <= fileLines.length; pos++) {
|
|
145
|
-
let ok = true
|
|
146
|
-
for (let j = 0; j < oldSeq.length; j++) {
|
|
147
|
-
if (fileLines[pos + j].replace(/\r$/, "") !== oldSeq[j]) { ok = false; break }
|
|
148
|
-
}
|
|
149
|
-
if (ok) matches.push(pos)
|
|
150
|
-
}
|
|
151
|
-
if (matches.length === 0) {
|
|
152
|
-
const preview = oldSeq.slice(0, 3).join(" ⏎ ")
|
|
153
|
-
throw new Error(`Hunk ${h + 1} in ${path} does not apply — context not found: "${preview}${oldSeq.length > 3 ? "…" : ""}". Read the file first and regenerate the patch from actual content.`)
|
|
154
|
-
}
|
|
155
|
-
if (matches.length > 1) {
|
|
156
|
-
const preview = oldSeq.slice(0, 3).join(" ⏎ ")
|
|
157
|
-
throw new Error(`Hunk ${h + 1} in ${path} matches ${matches.length} locations — add more context lines to make it unique. Anchor: "${preview}${oldSeq.length > 3 ? "…" : ""}"`)
|
|
158
|
-
}
|
|
159
|
-
const pos = matches[0]
|
|
160
|
-
const out = []
|
|
161
|
-
let src = pos
|
|
162
|
-
for (const op of hunks[h].ops) {
|
|
163
|
-
if (op.type === " ") out.push(fileLines[src++]) // preserve original context line (trailing whitespace as-is)
|
|
164
|
-
else if (op.type === "-") src++
|
|
165
|
-
else out.push(op.text + cr)
|
|
166
|
-
}
|
|
167
|
-
fileLines.splice(pos, oldSeq.length, ...out)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export const applyPatchTool = {
|
|
172
|
-
name: "apply_patch",
|
|
173
|
-
description: DESC("apply_patch"),
|
|
174
|
-
parameters: {
|
|
175
|
-
type: "object",
|
|
176
|
-
properties: {
|
|
177
|
-
patch: { type: "string", description: "Unified diff. May span multiple files (multiple --- / +++ header pairs — including creating MULTIPLE new files via --- /dev/null); --- / +++ headers per file, @@ -old,count +new,count @@ hunks (a bare @@ header is also accepted — coordinate-less hunks are located by their anchor lines: context lines plus the removed (-) lines, matched as a contiguous sequence — a unique match applies; a zero/one-context hunk is accepted only when it removes (-) at least one line and that anchor sequence is unique, while anchor-free pure-+ (insert) hunks need at least 2 context lines). The +++ b/<path> pair may be omitted for existing files — a lone --- a/<path> (or --- b/<path>) header followed directly by hunks applies to that path (new files still need --- /dev/null + +++ b/<path>)." },
|
|
178
|
-
},
|
|
179
|
-
required: ["patch"],
|
|
180
|
-
},
|
|
181
|
-
readonly: false,
|
|
182
|
-
/** For the agent layer to track touched files (multi-path, replaces single path parameter) */
|
|
183
|
-
touchedPaths(args) {
|
|
184
|
-
try { return parsePatch(args.patch ?? "").map((f) => f.path) } catch { return [] }
|
|
185
|
-
},
|
|
186
|
-
async execute(args, ctx) {
|
|
187
|
-
const files = parsePatch(args.patch ?? "")
|
|
188
|
-
// Read all into memory first for trial: if any hunk fails, abort entirely — never write a partial patch (atomicity)
|
|
189
|
-
const planned = []
|
|
190
|
-
for (const f of files) {
|
|
191
|
-
const abs = resolveInCwd(ctx, f.path)
|
|
192
|
-
if (f.isNew) {
|
|
193
|
-
if (existsSync(abs)) throw new Error(`Cannot create ${f.path}: file already exists`)
|
|
194
|
-
// New file: follow the directory's majority EOL style (default LF).
|
|
195
|
-
const eol = majorityEol(dirname(abs))
|
|
196
|
-
const content = f.hunks.flatMap((h) => h.ops.filter((o) => o.type === "+").map((o) => o.text)).join(eol) + eol
|
|
197
|
-
planned.push({ abs, path: f.path, content, isNew: true })
|
|
198
|
-
} else {
|
|
199
|
-
const original = await readFile(abs, "utf8").catch(() => { throw new Error(`File not found: ${f.path}`) })
|
|
200
|
-
const eol = detectFileEol(original)
|
|
201
|
-
// Apply hunks in the normalized LF domain, then write back joined with the
|
|
202
|
-
// file's ORIGINAL EOL style — join("\n") here used to rewrite CRLF files as LF.
|
|
203
|
-
const lines = normalizeEOL(original).split("\n")
|
|
204
|
-
applyHunks(lines, f.hunks, "\n", f.path)
|
|
205
|
-
planned.push({ abs, path: f.path, content: lines.join(eol), isNew: false })
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// Multi-file write: write all to .tmp first, rename only after all succeed — failure cleans up written .tmp without affecting committed files
|
|
209
|
-
const { rename } = await import("node:fs/promises") // unlink already statically imported
|
|
210
|
-
const written = []
|
|
211
|
-
try {
|
|
212
|
-
for (const p of planned) {
|
|
213
|
-
await mkdir(dirname(p.abs), { recursive: true })
|
|
214
|
-
await writeFile(p.abs + ".thincoder-tmp", p.content, "utf8")
|
|
215
|
-
written.push(p.abs)
|
|
216
|
-
}
|
|
217
|
-
for (const p of planned) {
|
|
218
|
-
await rename(p.abs + ".thincoder-tmp", p.abs)
|
|
219
|
-
}
|
|
220
|
-
} catch (renameError) {
|
|
221
|
-
// rename phase failed: clean up leftover .tmp files
|
|
222
|
-
for (const abs of written) {
|
|
223
|
-
try { await unlink(abs + ".thincoder-tmp") } catch {}
|
|
224
|
-
}
|
|
225
|
-
throw renameError
|
|
226
|
-
}
|
|
227
|
-
const summary = planned.map((p) => ` ${p.isNew ? "created " : "modified"} ${p.path}`).join("\n")
|
|
228
|
-
// Mark every touched file dirty — insert_after must not run on stale line numbers.
|
|
229
|
-
for (const p of planned) markDirty(p.abs)
|
|
230
|
-
const syntaxChecks = await Promise.all(planned.map(async (p) => {
|
|
231
|
-
const r = await autoSyntaxCheck(p.abs)
|
|
232
|
-
return r ? `${p.path}:${r.replace("Syntax: ", "")}` : ""
|
|
233
|
-
}))
|
|
234
|
-
const syntaxResults = syntaxChecks.filter(Boolean).join("\n")
|
|
235
|
-
return `Applied patch to ${planned.length} file(s):\n${summary}${syntaxResults ? "\n\nSyntax checks:\n" + syntaxResults : ""}`
|
|
236
|
-
},
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// ---------------------------------------------------------------- bash
|
|
242
|
-
|
|
243
|
-
export const deleteTool = {
|
|
244
|
-
name: "delete",
|
|
245
|
-
description: DESC("delete"),
|
|
246
|
-
parameters: {
|
|
247
|
-
type: "object",
|
|
248
|
-
properties: {
|
|
249
|
-
path: { type: "string", description: "File path (relative to cwd or absolute)" },
|
|
250
|
-
force: { type: "boolean", description: "Allow deleting git-tracked files (default false)" },
|
|
251
|
-
},
|
|
252
|
-
required: ["path"],
|
|
253
|
-
},
|
|
254
|
-
readonly: false,
|
|
255
|
-
touchedPaths(args) { return args.path ? [args.path] : [] },
|
|
256
|
-
async execute(args, ctx) {
|
|
257
|
-
const abs = resolveInCwd(ctx, args.path)
|
|
258
|
-
let s
|
|
259
|
-
try {
|
|
260
|
-
s = await lstat(abs)
|
|
261
|
-
} catch {
|
|
262
|
-
throw new Error(`File not found: ${args.path}`)
|
|
263
|
-
}
|
|
264
|
-
if (s.isDirectory()) throw new Error(`"${args.path}" is a directory — use bash to remove directories`)
|
|
265
|
-
// git-tracked files: refuse direct deletion (safety net); untracked: allow
|
|
266
|
-
// Use resolved relative path (normalized forward slashes) to prevent backslash/unusual paths from bypassing ls-files matching
|
|
267
|
-
const rel = relative(ctx.cwd, abs).replace(/\\/g, "/")
|
|
268
|
-
let tracked = false
|
|
269
|
-
try {
|
|
270
|
-
execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: ctx.cwd, stdio: "ignore" })
|
|
271
|
-
tracked = true
|
|
272
|
-
} catch {
|
|
273
|
-
// untracked / non-git repo
|
|
274
|
-
}
|
|
275
|
-
if (tracked && !args.force) throw new Error(`"${args.path}" is git-tracked. Set force=true to delete anyway.`)
|
|
276
|
-
await unlink(abs)
|
|
277
|
-
markDirty(abs)
|
|
278
|
-
return `Deleted ${args.path}`
|
|
279
|
-
},
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// ---------------------------------------------------------------- git_diff
|
package/src/tools/process.md
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
List running processes, optionally filtered by name. Returns process name / PID / memory.
|
|
2
|
-
|
|
3
|
-
**Route to process instead of bash:**
|
|
4
|
-
- `tasklist` (Windows) / `ps aux` (POSIX) → process
|
|
5
|
-
|
|
6
|
-
Parameters:
|
|
7
|
-
- name (optional): substring filter (case-insensitive), e.g. "node", "python"
|
|
8
|
-
|
|
9
|
-
Notes:
|
|
10
|
-
- List-only. To kill a process, use `bash taskkill /PID <pid> /F` (Windows) or `bash kill <pid>` — and confirm with the user first.
|
package/src/tools/question.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Ask the user a question and wait for their response. Use when the task is ambiguous, you need a design decision, or you're stuck and need human judgment.
|
|
2
|
-
|
|
3
|
-
Parameters:
|
|
4
|
-
- question (required): The question to ask the user
|
|
5
|
-
- options: Array of single-choice options for the user to pick from (optional). MUST be plain strings, e.g. ["A", "B", "C"] — never objects.
|
|
6
|
-
|
|
7
|
-
Notes:
|
|
8
|
-
- The agent loop pauses until the user answers
|
|
9
|
-
- The answer is injected as the next user message
|
|
10
|
-
- Returns the user's answer — the chosen option or free text — as the next message; the loop resumes when it arrives.
|
|
11
|
-
- Availability: this tool needs an interactive UI — in contexts without one (headless runs, subagent children) it returns an error instead of asking; put the question in your reply text instead.
|
|
12
|
-
- Use sparingly — prefer making reasonable decisions when possible
|
|
13
|
-
- Ask ONE question per call — never bundle multiple sub-questions into one question string; ask the next one after the answer arrives.
|
|
14
|
-
- Keep the question text short — one or two sentences. Background, context, and analysis belong in your normal reply text, NOT in the question.
|
|
15
|
-
- Routine confirmations (confirm gates) belong in your plain reply text — the user answers in their next message. Use this tool ONLY when you need the user's decision or input to proceed.
|
|
16
|
-
- After receiving an answer about a design convention, tool preference, or recurring pattern: save it with the memory tool (action: put). This prevents asking the same question in future sessions — the user shouldn't have to repeat their preferences.
|
package/src/tools/question.mjs
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { DESC } from "./shared.mjs";
|
|
2
|
-
|
|
3
|
-
export const questionTool = {
|
|
4
|
-
name: "question",
|
|
5
|
-
description: DESC("question"),
|
|
6
|
-
parameters: {
|
|
7
|
-
type: "object",
|
|
8
|
-
properties: {
|
|
9
|
-
question: { type: "string", description: "The question to ask the user" },
|
|
10
|
-
options: {
|
|
11
|
-
type: "array",
|
|
12
|
-
items: { type: "string" },
|
|
13
|
-
description: "Single-choice options for the user to pick from (optional)",
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
required: ["question"],
|
|
17
|
-
},
|
|
18
|
-
readonly: true,
|
|
19
|
-
async execute(args, ctx) {
|
|
20
|
-
if (!ctx.onQuestion) throw new Error("question tool not supported in this context (no UI to ask)")
|
|
21
|
-
// §22 D-Q3 机械限制(2026-09-06 用户裁定:100 字符 / 4 条——超限拒绝回模型,不弹卡、不调 onQuestion)
|
|
22
|
-
if (args.question.length > 100) return "(error: question too long (>100 chars) — ask ONE short question; background belongs in your normal reply text)"
|
|
23
|
-
if (Array.isArray(args.options) && args.options.length > 4) return "(error: too many options (>4) — offer at most 4 plain-string options)"
|
|
24
|
-
return ctx.onQuestion(args.question, args.options ?? [])
|
|
25
|
-
},
|
|
26
|
-
}
|
package/src/tools/read.md
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
Read a text file. Returns numbered lines. Use offset/limit to page large files.
|
|
2
|
-
|
|
3
|
-
**Route to read instead of bash:** `cat file` / `type file` / `node -e "fs.readFileSync(...)"` → read. Reading a file is a read — never shell out for it.
|
|
4
|
-
|
|
5
|
-
**Routing:**
|
|
6
|
-
- Don't know which file? → `repo_outline` / `code_search` / `glob` first
|
|
7
|
-
- Know the symbol but not the location? → `code_search` or `lsp definition`
|
|
8
|
-
- Know the file but not the lines? → `grep` to find line numbers, then read that range with offset/limit
|
|
9
|
-
- Reading an image? → `read_image` instead
|
|
10
|
-
|
|
11
|
-
Parameters:
|
|
12
|
-
- path (required): File path, relative to cwd or absolute (alias: filePath)
|
|
13
|
-
- offset: 1-based line number to start reading from
|
|
14
|
-
- limit: Max lines to return (default 2000) — windows over files beyond that return head + `…(truncated: K lines in middle, use offset to continue)` + the file's real tail lines, so the file end is never hidden
|
|
15
|
-
- hashes: Include SHA256 content hashes per line (for hashline_edit). Set true before using hashline_edit.
|
|
16
|
-
|
|
17
|
-
Notes:
|
|
18
|
-
- Always prefer this over `cat` or shell-based reading — it caps output and avoids large dumps
|
|
19
|
-
- Use offset for pagination when the file is large
|
|
20
|
-
- When you plan to edit the file, set hashes=true to get line hashes for hashline_edit — hash-based editing avoids whitespace/encoding matching failures
|
package/src/tools/read_image.md
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
Read an image file and return it as multimodal content visible to the model. Use this to view screenshots, UI mockups, diagrams, or any visual content. The model only sees images through this tool — it cannot "see" files directly. Supports png, jpg, gif, webp. The image is base64-encoded and included in the response. Large images (>15MB) are rejected.
|
|
2
|
-
|
|
3
|
-
Parameters:
|
|
4
|
-
- path (required): Path to image file (relative to cwd or absolute). Supports png, jpg, gif, webp; svg is returned as text source.
|
|
5
|
-
|
|
6
|
-
Notes:
|
|
7
|
-
- Raster formats only (png/jpg/gif/webp): no mainstream vision API (Kimi, Anthropic, OpenAI, Gemini) accepts svg or bmp, and an unsupported image part in history makes every subsequent request fail with 400. svg files are returned as text source instead (readable by any model); bmp is rejected — convert to PNG first.
|
|
8
|
-
- This tool only works with models that support vision/image input (capability is spec-driven — the model spec decides, not a hardcoded list). Models without vision support will receive an error — except svg, which needs no vision support since it is read as text.
|