thincoder 0.12.43 → 0.12.44
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 +19 -0
- package/package.json +8 -3
- package/src/advisor/repos.mjs +2 -2
- package/src/agent/dispatch.mjs +1 -1
- package/src/agent-tools/subagent.mjs +8 -0
- package/src/config.mjs +1 -1
- package/src/git/checkpoint.mjs +2 -2
- package/src/git/gitmem.mjs +3 -1
- package/src/memory/core.mjs +1 -1
- package/src/tools/file.mjs +1 -1
- package/src/tools/system.mjs +2 -2
- package/src/tools/web.mjs +1 -1
- package/src/tui/layout.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录 ThinCoder CLI 的发布历史。格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),版本遵循[语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.12.44] — 2026-08-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **安全修复:subagent 变形 role 绕过工程模式门禁**——`role="Coder"/" coder"` 等非精确字符串穿透两个模式门禁(精确比较),fallthrough 到全工具/无 overlay 子代理,绕过设计评审拿到完整写权限。修复:execute 入口 ROLES 白名单,未知 role 直接 throw(fail-closed);防回归测试锁定 7 种变形值 × 两种模式
|
|
10
|
+
- IK9UZ8 思考型模型标题生成(vscode 端同修对齐)
|
|
11
|
+
- 文档状态/TODO 销账(24+13 处"待评审"→"已实现";TODO 7 条已实现条目核对关闭)
|
|
12
|
+
- TUI.md 章节号重编号(## 4-10 顺延,无重复)
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **ESLint 引入**(规则基线对齐 vscode 端):lint script + 21 个 error 清零(死赋值/cause 补全)
|
|
17
|
+
- **跨端同构模块语义锚点比对测试**:14 个跨仓库契约(advisor 收敛协议、蒸馏语义、64K 阈值)两端必须一致——单边漂移立即红
|
|
18
|
+
- **RELEASE.md**:npm 发布流程 + 4 条踩坑记录(vsce 自动 bump、Open VSX 异步激活、ovsx 无 TTY 静默失败、prepublishOnly)
|
|
19
|
+
|
|
20
|
+
### Docs
|
|
21
|
+
|
|
22
|
+
- 文档债收口:6 处"待合并"全部处理(真碎片合并/归档/独立保留定性)
|
|
23
|
+
|
|
5
24
|
## [0.12.43] — 2026-08-25
|
|
6
25
|
|
|
7
26
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thincoder",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.44",
|
|
4
4
|
"description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -32,8 +32,13 @@
|
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"test": "node --test \"test/*.mjs\"",
|
|
35
|
-
"prepublishOnly": "node --test \"test/*.mjs\""
|
|
35
|
+
"prepublishOnly": "node --test \"test/*.mjs\"",
|
|
36
|
+
"lint": "eslint src"
|
|
36
37
|
},
|
|
37
38
|
"author": "liwei <liwei@51marine.com> (上海新舶)",
|
|
38
|
-
"license": "MIT"
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"eslint": "^9.0.0",
|
|
42
|
+
"@eslint/js": "^9.0.0"
|
|
43
|
+
}
|
|
39
44
|
}
|
package/src/advisor/repos.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export function collectRepoSnapshots(repos, cwd) {
|
|
|
49
49
|
const targets = repos.length > 0 ? repos : [cwd]
|
|
50
50
|
const parts = []
|
|
51
51
|
for (const repo of targets) {
|
|
52
|
-
let status
|
|
52
|
+
let status, diff
|
|
53
53
|
try {
|
|
54
54
|
status = execFileSync("git", ["status", "--porcelain"], {
|
|
55
55
|
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -153,7 +153,7 @@ export function isDocOnlyChange(repos, cwd) {
|
|
|
153
153
|
const targets = repos.length > 0 ? repos : [cwd]
|
|
154
154
|
let sawChanges = false
|
|
155
155
|
for (const repo of targets) {
|
|
156
|
-
let status
|
|
156
|
+
let status
|
|
157
157
|
try {
|
|
158
158
|
status = execFileSync("git", ["status", "--porcelain"], {
|
|
159
159
|
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
package/src/agent/dispatch.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
49
49
|
const prepared = []
|
|
50
50
|
for (const toolCall of toolCalls) {
|
|
51
51
|
const tool = toolByName.get(toolCall.name)
|
|
52
|
-
let args
|
|
52
|
+
let args
|
|
53
53
|
try {
|
|
54
54
|
args = JSON.parse(toolCall.arguments || "{}")
|
|
55
55
|
} catch {
|
|
@@ -77,6 +77,14 @@ export const subagentTool = {
|
|
|
77
77
|
const parent = ctx.agent
|
|
78
78
|
const role = args.role
|
|
79
79
|
|
|
80
|
+
// Role normalization + whitelist (2026-08-25, coder-leak fix): exact-string gates let
|
|
81
|
+
// variant roles ("Coder", " coder") bypass BOTH mode gates and fall through to
|
|
82
|
+
// full tools / no overlay — a full-write coder without design review. Schema enums are
|
|
83
|
+
// advisory; providers don't enforce them. Fail closed on unknown roles.
|
|
84
|
+
const ROLES = new Set(["explore", "plan", "coder", "eng-coder"])
|
|
85
|
+
if (!ROLES.has(role)) {
|
|
86
|
+
throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder (exact spelling).`)
|
|
87
|
+
}
|
|
80
88
|
// Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"
|
|
81
89
|
if (parent.config?.agent?.engineering && role === "coder") {
|
|
82
90
|
throw new Error("Engineering mode: use role='eng-coder' for implementation tasks.")
|
package/src/config.mjs
CHANGED
|
@@ -235,7 +235,7 @@ export function loadConfig() {
|
|
|
235
235
|
try {
|
|
236
236
|
config = JSON.parse(readFileSync(configPath, "utf8"))
|
|
237
237
|
} catch (error) {
|
|
238
|
-
throw new Error(`Config file is not valid JSON, check or delete it: ${configPath}\n ${error.message}
|
|
238
|
+
throw new Error(`Config file is not valid JSON, check or delete it: ${configPath}\n ${error.message}`, { cause: error })
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
|
package/src/git/checkpoint.mjs
CHANGED
|
@@ -29,7 +29,7 @@ function git(cwd, args, { allowFail = false } = {}) {
|
|
|
29
29
|
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim()
|
|
30
30
|
} catch (error) {
|
|
31
31
|
if (allowFail) return null
|
|
32
|
-
throw new Error(`git ${args.join(" ")} failed: ${error.stderr?.toString().trim() || error.message}
|
|
32
|
+
throw new Error(`git ${args.join(" ")} failed: ${error.stderr?.toString().trim() || error.message}`, { cause: error })
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -324,7 +324,7 @@ export async function listFileVersions(cwd, filePath) {
|
|
|
324
324
|
} else if ((meta.tracked ?? []).includes(filePath) || (meta.untracked ?? []).includes(filePath)) {
|
|
325
325
|
// Legacy snapshot (no per-file meta): fall back to stat-ing the copy
|
|
326
326
|
const src = join((meta.tracked ?? []).includes(filePath) ? join(root, id, "files") : join(root, id, "untracked"), filePath)
|
|
327
|
-
let size
|
|
327
|
+
let size, sha
|
|
328
328
|
try {
|
|
329
329
|
const buf = await readFile(src)
|
|
330
330
|
size = buf.length
|
package/src/git/gitmem.mjs
CHANGED
|
@@ -55,14 +55,16 @@ export async function pullTeam(dir) {
|
|
|
55
55
|
try { await git(dir, ["rebase", "--abort"]) } catch {
|
|
56
56
|
abortFailed = true
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
const conflictError = new Error(
|
|
59
59
|
`Team memory sync conflict: local and remote modified the same entry.\n` +
|
|
60
60
|
`Please resolve manually in ${dir} with \`git pull\`, then re-run \`thincoder sync\`.\n` +
|
|
61
61
|
(abortFailed
|
|
62
62
|
? `(WARNING: git rebase --abort also failed — the repo may be in a conflicted state. ` +
|
|
63
63
|
`Run \`cd ${dir} && git rebase --abort\` manually to clean up.)`
|
|
64
64
|
: `(The local repo has been restored to its pre-sync state — nothing was lost.)`),
|
|
65
|
+
{ cause: error },
|
|
65
66
|
)
|
|
67
|
+
throw conflictError
|
|
66
68
|
}
|
|
67
69
|
throw error
|
|
68
70
|
}
|
package/src/memory/core.mjs
CHANGED
|
@@ -198,7 +198,7 @@ export async function putMarkdown(memory, { layer, dir, type, title, content, ta
|
|
|
198
198
|
* vanished entries are removed from the index.
|
|
199
199
|
*/
|
|
200
200
|
export async function syncDir(memory, { layer, dir }) {
|
|
201
|
-
let names
|
|
201
|
+
let names
|
|
202
202
|
try {
|
|
203
203
|
names = (await readdir(dir)).filter((n) => n.endsWith(".md"))
|
|
204
204
|
} catch {
|
package/src/tools/file.mjs
CHANGED
|
@@ -261,7 +261,7 @@ export const insertAfterTool = {
|
|
|
261
261
|
try {
|
|
262
262
|
regex = new RegExp(args.after_regex)
|
|
263
263
|
} catch (e) {
|
|
264
|
-
throw new Error(`after_regex /${args.after_regex}/ is not a valid JavaScript regex: ${e.message}
|
|
264
|
+
throw new Error(`after_regex /${args.after_regex}/ is not a valid JavaScript regex: ${e.message}`, { cause: e })
|
|
265
265
|
}
|
|
266
266
|
const matches = []
|
|
267
267
|
for (let i = 0; i < lines.length; i++) {
|
package/src/tools/system.mjs
CHANGED
|
@@ -339,7 +339,7 @@ export const grepTool = {
|
|
|
339
339
|
const pat = args.literal ? escapeRegExp(String(args.pattern)) : args.pattern
|
|
340
340
|
regex = new RegExp(pat, args.ignoreCase ? "i" : "")
|
|
341
341
|
} catch (e) {
|
|
342
|
-
throw new Error(`grep pattern /${args.pattern}/ is not a valid regex: ${e.message}
|
|
342
|
+
throw new Error(`grep pattern /${args.pattern}/ is not a valid regex: ${e.message}`, { cause: e })
|
|
343
343
|
}
|
|
344
344
|
const fileFilter = args.glob ? globToRegex(args.glob) : null
|
|
345
345
|
const before = Math.max(0, Math.floor(args.before ?? 0))
|
|
@@ -439,7 +439,7 @@ export const lsTool = {
|
|
|
439
439
|
try {
|
|
440
440
|
entries = await readdir(abs, { withFileTypes: true })
|
|
441
441
|
} catch (e) {
|
|
442
|
-
if (e.code === "ENOENT" || e.code === "ENOTDIR") throw new Error(`ls: ${args.path ?? "."} — ${e.code === "ENOTDIR" ? "not a directory" : "not found"}
|
|
442
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") throw new Error(`ls: ${args.path ?? "."} — ${e.code === "ENOTDIR" ? "not a directory" : "not found"}`, { cause: e })
|
|
443
443
|
throw e
|
|
444
444
|
}
|
|
445
445
|
const rows = await Promise.all(
|
package/src/tools/web.mjs
CHANGED
|
@@ -187,6 +187,6 @@ export const fetchTool = {
|
|
|
187
187
|
const ct = headerOf(response, "content-type") ?? ""
|
|
188
188
|
const body = await response.text()
|
|
189
189
|
return ct.includes("text/html") ? truncate(htmlToText(body)) : truncate(body)
|
|
190
|
-
} catch (e) { throw new Error(`fetch failed: ${e.cause?.code ?? e.message}
|
|
190
|
+
} catch (e) { throw new Error(`fetch failed: ${e.cause?.code ?? e.message}`, { cause: e }) }
|
|
191
191
|
},
|
|
192
192
|
}
|
package/src/tui/layout.mjs
CHANGED
|
@@ -61,7 +61,7 @@ export function computeLayout(state, { cols, rows }) {
|
|
|
61
61
|
const pickerH = overlay ? Math.min(overlay.lines.length + 1, Math.max(6, rows - 12)) : 0
|
|
62
62
|
|
|
63
63
|
// Todo
|
|
64
|
-
let visibleTasks
|
|
64
|
+
let visibleTasks
|
|
65
65
|
if (state.tasks.length <= MAX_TASK_LINES) {
|
|
66
66
|
visibleTasks = state.tasks
|
|
67
67
|
} else {
|