thincoder 0.12.45 → 0.12.47
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 +13 -0
- package/package.json +2 -2
- package/src/config.mjs +1 -0
- package/src/tools/checklist.md +4 -1
- package/src/tools/checklist.mjs +105 -46
- package/src/tools/read_image.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
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.47] — 2026-08-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **GLM-5.3-Flash 模型支持**:智谱 2026-08-26 发布的原生多模态 Flash 档位模型——1M 上下文 + 128K 输出 + 文本/图片输入,`/model glm:glm-5.3-flash` 即可选用(不改默认预设)
|
|
10
|
+
|
|
11
|
+
## [0.12.46] — 2026-08-27
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **checklist 工具坐标系断裂**:`add` 返回任务 ID、`mark` 却只收列表位置 index,agent 拿 ID 定位不到条目、只能猜 index——误标无关条目(线上事故)。修复:`mark` 加 `id` 参数(优先于 index);auto-ID 按「最大根号+1」分配(含 `checklist-done.md` 双文件扫描,归档 ID 恒占位不复用);历史重复 `T[\d.]+:` 前缀读入即归一;标记父任务 done 时子任务非全 done 则拒绝(防静默丢弃子树),全 done 则递归归档整棵子树
|
|
16
|
+
- **子 agent/advisor 模型显示补录**(TUI.md 文档欠账,功能此前已实现)
|
|
17
|
+
|
|
5
18
|
## [0.12.45] — 2026-08-26
|
|
6
19
|
|
|
7
20
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thincoder",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.47",
|
|
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,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"test": "node --test \"test/*.mjs\"",
|
|
35
|
-
"prepublishOnly": "node --test \"test/*.mjs\"",
|
|
35
|
+
"prepublishOnly": "npm run lint && node --test \"test/*.mjs\"",
|
|
36
36
|
"lint": "eslint src"
|
|
37
37
|
},
|
|
38
38
|
"author": "liwei <liwei@51marine.com> (上海新舶)",
|
package/src/config.mjs
CHANGED
|
@@ -108,6 +108,7 @@ const MODEL_SPECS = [
|
|
|
108
108
|
// GLM-5.3: thinking always-on (no "disabled"); effort converges to low/high/max — NOT the
|
|
109
109
|
// 7-level glm-5.2 enum (verified vs docs.bigmodel.cn GLM-5.3 page, 2026-08)
|
|
110
110
|
["glm-5.3", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
|
|
111
|
+
["glm-5.3-flash", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
|
|
111
112
|
["glm-5.2", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
|
|
112
113
|
["glm-5", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
|
|
113
114
|
["glm-4", { context: 128_000, maxOutput: 32_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", tempRange: [0, 1], noUsageStream: true }],
|
package/src/tools/checklist.md
CHANGED
|
@@ -4,7 +4,10 @@ Items support tree hierarchy via indentation (2 spaces per level) and auto-assig
|
|
|
4
4
|
|
|
5
5
|
Parameters:
|
|
6
6
|
- action: "add" | "mark" | "list"
|
|
7
|
+
- id: task ID to mark, e.g. "T3" (with "mark"; preferred — use the ID returned by `add`, or from `list`)
|
|
7
8
|
- item: text for new item (with "add")
|
|
8
|
-
- index: 1-based index (with "mark")
|
|
9
|
+
- index: 1-based index (with "mark"; fallback — use only when you have no ID)
|
|
9
10
|
- status: "pending" | "in_progress" | "done" (with "mark")
|
|
10
11
|
- parent: parent task ID for hierarchical tasks, e.g. "T1" (with "add")
|
|
12
|
+
|
|
13
|
+
Note: marking a parent "done" requires all its child tasks already done — otherwise it is rejected (complete the children before marking the parent done).
|
package/src/tools/checklist.mjs
CHANGED
|
@@ -31,42 +31,79 @@ function parse(filePath) {
|
|
|
31
31
|
const status = raw === "x" ? "done" : raw === "~" ? "in_progress" : "pending"
|
|
32
32
|
const text = m[3].trim()
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
text: idMatch ? text.slice(idMatch[0].length) : text,
|
|
43
|
-
children: [],
|
|
34
|
+
// Strip ALL leading "T[\d.]+:" tokens (historical dirty data can accumulate
|
|
35
|
+
// "T15: T15: T15:"); keep the first token as the ID and the rest as text.
|
|
36
|
+
let id = null
|
|
37
|
+
let bareText = text
|
|
38
|
+
let idTok
|
|
39
|
+
while ((idTok = bareText.match(/^(T[\d.]+):\s*/))) {
|
|
40
|
+
if (id == null) id = idTok[1]
|
|
41
|
+
bareText = bareText.slice(idTok[0].length)
|
|
44
42
|
}
|
|
43
|
+
const node = { id, index: flatIdx, depth, status, text: bareText, children: [] }
|
|
45
44
|
|
|
46
45
|
// Find parent by popping stack until we find a node at depth-1
|
|
47
46
|
while (stack.length > 1 && stack.at(-1).depth >= depth) stack.pop()
|
|
48
47
|
const parent = stack.at(-1)
|
|
49
48
|
parent.children.push(node)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
node.id = `T${rootIdx + 1}`
|
|
49
|
+
stack.push({ children: node.children, depth, id: node.id })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Assign stable IDs to lines that lacked an explicit one, exactly once.
|
|
53
|
+
// IDs are "max existing number + 1" (not position-based) so gaps left by
|
|
54
|
+
// archived items never collide, and persisted IDs never drift on re-read.
|
|
55
|
+
let assigned = false
|
|
56
|
+
function assignIds(nodes, parentId) {
|
|
57
|
+
for (const n of nodes) {
|
|
58
|
+
if (!n.id) {
|
|
59
|
+
n.id = parentId ? nextChildId(parentId, nodes) : nextRootId(nodes, doneRoots)
|
|
60
|
+
assigned = true
|
|
63
61
|
}
|
|
62
|
+
if (n.children?.length) assignIds(n.children, n.id)
|
|
64
63
|
}
|
|
65
|
-
stack.push({ children: node.children, depth, id: node.id })
|
|
66
64
|
}
|
|
65
|
+
// Root IDs archived to the done file also reserve numbers (mirrors the `add`
|
|
66
|
+
// path's double-file scan), so auto-assigned IDs never collide with them.
|
|
67
|
+
const doneRoots = readDoneRoots(join(dirname(filePath), DONE))
|
|
68
|
+
assignIds(items, null)
|
|
69
|
+
if (assigned) write(filePath, items)
|
|
70
|
+
|
|
67
71
|
return items
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
function readDoneRoots(doneFile) {
|
|
75
|
+
if (!existsSync(doneFile)) return []
|
|
76
|
+
const roots = []
|
|
77
|
+
for (const line of readFileSync(doneFile, "utf-8").split("\n")) {
|
|
78
|
+
const m = line.match(/^- \[.\] (T\d+): /)
|
|
79
|
+
if (m) roots.push({ id: m[1] })
|
|
80
|
+
}
|
|
81
|
+
return roots
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function nextRootId(items, doneItems) {
|
|
85
|
+
let max = 0
|
|
86
|
+
for (const list of [items, doneItems]) {
|
|
87
|
+
for (const c of list ?? []) {
|
|
88
|
+
const m = c.id?.match(/^T(\d+)$/)
|
|
89
|
+
if (m) max = Math.max(max, parseInt(m[1]))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return `T${max + 1}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function nextChildId(parentId, children) {
|
|
96
|
+
let max = 0
|
|
97
|
+
const prefix = `${parentId}.`
|
|
98
|
+
for (const c of children) {
|
|
99
|
+
if (c.id?.startsWith(prefix)) {
|
|
100
|
+
const suffix = c.id.slice(prefix.length)
|
|
101
|
+
if (/^\d+$/.test(suffix)) max = Math.max(max, parseInt(suffix))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return `${prefix}${max + 1}`
|
|
105
|
+
}
|
|
106
|
+
|
|
70
107
|
/** Write items back to file, preserving tree structure */
|
|
71
108
|
function write(filePath, items, _depth = 0) {
|
|
72
109
|
if (_depth === 0) mkdirSync(dirname(filePath), { recursive: true })
|
|
@@ -108,6 +145,26 @@ function flatten(items, out = []) {
|
|
|
108
145
|
return out
|
|
109
146
|
}
|
|
110
147
|
|
|
148
|
+
/** True if every descendant (children, grandchildren, …) is done. */
|
|
149
|
+
function allChildrenDone(node) {
|
|
150
|
+
for (const c of node.children ?? []) {
|
|
151
|
+
if (c.status !== "done" || !allChildrenDone(c)) return false
|
|
152
|
+
}
|
|
153
|
+
return true
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Recursively clone a subtree for archiving, forcing every status to done. */
|
|
157
|
+
function archiveSubtree(node) {
|
|
158
|
+
return {
|
|
159
|
+
id: node.id,
|
|
160
|
+
index: 0,
|
|
161
|
+
depth: 0,
|
|
162
|
+
status: "done",
|
|
163
|
+
text: node.text,
|
|
164
|
+
children: (node.children ?? []).map(archiveSubtree),
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
111
168
|
/** Parse pending items only (for context injection) */
|
|
112
169
|
export function pendingItems(cwd) {
|
|
113
170
|
const flat = flatten(parse(checklistPath(cwd)))
|
|
@@ -125,13 +182,17 @@ export const checklistTool = {
|
|
|
125
182
|
enum: ["add", "mark", "list"],
|
|
126
183
|
description: "add a new item / mark item status / list all items"
|
|
127
184
|
},
|
|
185
|
+
id: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: "Task ID to mark (preferred — use the ID returned by add, e.g. 'T3')"
|
|
188
|
+
},
|
|
128
189
|
item: {
|
|
129
190
|
type: "string",
|
|
130
191
|
description: "Item text (required for add)"
|
|
131
192
|
},
|
|
132
193
|
index: {
|
|
133
194
|
type: "number",
|
|
134
|
-
description: "1-based item index (
|
|
195
|
+
description: "1-based item index (fallback for mark, only when id is absent)"
|
|
135
196
|
},
|
|
136
197
|
status: {
|
|
137
198
|
type: "string",
|
|
@@ -161,48 +222,46 @@ export const checklistTool = {
|
|
|
161
222
|
parentId = found.item.id
|
|
162
223
|
}
|
|
163
224
|
|
|
164
|
-
|
|
165
|
-
let id
|
|
166
|
-
if (parentId) {
|
|
167
|
-
id = `${parentId}.${target.length + 1}`
|
|
168
|
-
} else {
|
|
169
|
-
let maxIdx = 0
|
|
170
|
-
for (const c of items) {
|
|
171
|
-
const m = c.id?.match(/^T(\d+)$/)
|
|
172
|
-
if (m) maxIdx = Math.max(maxIdx, parseInt(m[1]))
|
|
173
|
-
}
|
|
174
|
-
id = `T${maxIdx + 1}`
|
|
175
|
-
}
|
|
176
|
-
|
|
225
|
+
const id = parentId ? nextChildId(parentId, target) : nextRootId(items, parse(donePath(ctx.cwd)))
|
|
177
226
|
const node = { id, index: 0, depth: parentId ? 1 : 0, status: "pending", text: args.item, children: [] }
|
|
178
227
|
target.push(node)
|
|
179
228
|
write(checklistPath(ctx.cwd), items)
|
|
180
229
|
return `Added: [ ] ${id}: ${args.item}${parentId ? ` (under ${parentId})` : ""}`
|
|
181
230
|
}
|
|
182
231
|
case "mark": {
|
|
183
|
-
if (args.index == null) return "Error: 'index' is required for mark"
|
|
232
|
+
if (args.id == null && args.index == null) return "Error: 'id' or 'index' is required for mark"
|
|
184
233
|
const status = args.status
|
|
185
234
|
if (!status || !["pending", "in_progress", "done"].includes(status)) return "Error: 'status' is required (pending|in_progress|done)"
|
|
186
235
|
const cp = checklistPath(ctx.cwd)
|
|
187
236
|
const items = parse(cp)
|
|
188
|
-
|
|
189
|
-
if (args.
|
|
190
|
-
|
|
237
|
+
let item
|
|
238
|
+
if (args.id != null) {
|
|
239
|
+
const found = findById(items, args.id)
|
|
240
|
+
if (!found) return `Error: id '${args.id}' not found. Use 'list' to see all task IDs.`
|
|
241
|
+
item = found.item
|
|
242
|
+
} else {
|
|
243
|
+
const flat = flatten(items)
|
|
244
|
+
if (args.index < 1 || args.index > flat.length) return `Error: index ${args.index} out of range (1-${flat.length})`
|
|
245
|
+
item = flat[args.index - 1]
|
|
246
|
+
}
|
|
191
247
|
const old = item.status
|
|
192
248
|
if (old === status) return `Already ${status}: ${item.text}`
|
|
249
|
+
if (status === "done" && item.children?.length && !allChildrenDone(item)) {
|
|
250
|
+
return "Error: 父任务仍有未完成的子任务,先处理子任务再标父 done"
|
|
251
|
+
}
|
|
193
252
|
item.status = status
|
|
194
253
|
if (status === "done") {
|
|
195
|
-
// Move to done file
|
|
254
|
+
// Move the whole subtree to the done file (hierarchy preserved).
|
|
196
255
|
const dp = donePath(ctx.cwd)
|
|
197
256
|
const doneItems = parse(dp)
|
|
198
|
-
doneItems.push(
|
|
257
|
+
doneItems.push(archiveSubtree(item))
|
|
199
258
|
write(dp, doneItems)
|
|
200
|
-
// Remove from tree
|
|
259
|
+
// Remove the subtree from the tree.
|
|
201
260
|
const found = findById(items, item.id)
|
|
202
261
|
if (found) found.parent.splice(found.idx, 1)
|
|
203
262
|
}
|
|
204
263
|
write(cp, items)
|
|
205
|
-
return `Marked
|
|
264
|
+
return `Marked ${item.id} ${old} → ${status}`
|
|
206
265
|
}
|
|
207
266
|
case "list": {
|
|
208
267
|
const items = parse(checklistPath(ctx.cwd))
|
package/src/tools/read_image.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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 (>
|
|
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
2
|
|
|
3
3
|
Parameters:
|
|
4
4
|
- path (required): Path to image file (relative to cwd or absolute). Supports png, jpg, gif, webp; svg is returned as text source.
|
|
5
5
|
|
|
6
6
|
Notes:
|
|
7
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 (Kimi K3, Qwen3.
|
|
8
|
+
- This tool only works with models that support vision/image input (Kimi K3, Qwen3.8, MiniMax M3, GLM-5.3-Flash). Pure text models (DeepSeek V4, GLM-5) will receive an error — except svg, which needs no vision support since it is read as text.
|