thincoder 0.12.54 → 0.12.59
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 +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +565 -128
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +3 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +11 -32
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-tools/read-history.mjs — read_history tool (SESSION.md §9).
|
|
3
|
+
*
|
|
4
|
+
* Query THIS session's message history — the full human-readable record
|
|
5
|
+
* (agent._fullHistory — NEVER compacted, audit-complete). Use to recall what
|
|
6
|
+
* was said or done earlier: design decisions, tool-call timing, past rulings.
|
|
7
|
+
*
|
|
8
|
+
* Filters AND together: role / keyword (message text) / tool (tool messages by
|
|
9
|
+
* name + assistant messages that declared the call) / since-until (epoch ms
|
|
10
|
+
* window, inclusive-inclusive, matches ONLY messages that carry ts) /
|
|
11
|
+
* limit (default 50, clamped to 200) / direction (oldest/newest — which end of
|
|
12
|
+
* the matched set the limit window is taken from).
|
|
13
|
+
*
|
|
14
|
+
* Returns a JSON array in chronological order. Every message without ts comes
|
|
15
|
+
* back as ts:null and can never match a time window (legacy sessions). Content
|
|
16
|
+
* is truncated to ~500 chars with an explicit marker — full text lives in the
|
|
17
|
+
* session file. assistant tool_calls are summarized to a name list (arguments
|
|
18
|
+
* never expanded).
|
|
19
|
+
*
|
|
20
|
+
* readonly: true — planMode pass / no permission ask. Registered depth-0 only:
|
|
21
|
+
* subagents get their own throwaway history, so querying "the session" from a
|
|
22
|
+
* child would be semantically confusing (SESSION.md §9.5 refinement 1).
|
|
23
|
+
* Mirrored 1:1 in thincoder-vscode/src/agent-tools/read-history.mjs.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const DEFAULT_LIMIT = 50
|
|
27
|
+
const MAX_LIMIT = 200
|
|
28
|
+
const CONTENT_CAP = 500
|
|
29
|
+
const VALID_ROLES = new Set(["user", "assistant", "tool"])
|
|
30
|
+
|
|
31
|
+
/** Message text for keyword matching + output: strings pass through; multimodal content arrays → text parts joined (never crashes, empty parts skipped, images ignored). */
|
|
32
|
+
function messageText(m) {
|
|
33
|
+
if (typeof m?.content === "string") return m.content
|
|
34
|
+
if (Array.isArray(m?.content)) {
|
|
35
|
+
return m.content
|
|
36
|
+
.map((p) => (p && typeof p === "object" && p.type === "text" ? p.text ?? "" : ""))
|
|
37
|
+
.filter((t) => t.length > 0)
|
|
38
|
+
.join(" ")
|
|
39
|
+
}
|
|
40
|
+
return ""
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Truncate long content (~500 chars) with an explicit marker. The cut never splits a UTF-16
|
|
44
|
+
* surrogate pair (emoji etc.) — a lone high surrogate in tool output would be an eyesore at
|
|
45
|
+
* minimum; the send layer sanitizes it anyway, but clean output costs nothing (setup.mjs
|
|
46
|
+
* safeSliceUTF16 same rule). */
|
|
47
|
+
function truncateContent(text) {
|
|
48
|
+
const t = String(text ?? "")
|
|
49
|
+
if (t.length <= CONTENT_CAP) return t
|
|
50
|
+
let end = CONTENT_CAP
|
|
51
|
+
if (t.charCodeAt(end - 1) >= 0xd800 && t.charCodeAt(end - 1) <= 0xdbff) end-- // high surrogate at the cut → step back
|
|
52
|
+
return t.slice(0, end) + `\n… (truncated: ${t.length - end} chars — full text is in the session file)`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Tool name across both stored shapes ({function:{name}} and flat {name}). */
|
|
56
|
+
function toolCallName(tc) {
|
|
57
|
+
return tc?.function?.name ?? tc?.name ?? ""
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Parse a ts window boundary (epoch ms number; numeric strings tolerated). Returns the number or an error string. */
|
|
61
|
+
function parseTs(value, label) {
|
|
62
|
+
if (value === undefined || value === null) return null
|
|
63
|
+
const n = typeof value === "number" ? value : Number(value)
|
|
64
|
+
if (!Number.isFinite(n)) return `Error: invalid ${label} "${value}" — must be epoch milliseconds (number)`
|
|
65
|
+
return n
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Map one matched message to its JSON entry shape. */
|
|
69
|
+
function toEntry(m) {
|
|
70
|
+
const entry = {
|
|
71
|
+
ts: typeof m.ts === "number" ? m.ts : null,
|
|
72
|
+
role: m.role ?? null,
|
|
73
|
+
}
|
|
74
|
+
if (m.name !== undefined) entry.name = m.name
|
|
75
|
+
if (m.tool_call_id !== undefined) entry.tool_call_id = m.tool_call_id
|
|
76
|
+
entry.content = truncateContent(messageText(m))
|
|
77
|
+
if (Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
|
|
78
|
+
entry.tool_calls = m.tool_calls.map(toolCallName).filter(Boolean)
|
|
79
|
+
}
|
|
80
|
+
return entry
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const readHistoryTool = {
|
|
84
|
+
name: "read_history",
|
|
85
|
+
description:
|
|
86
|
+
"Query THIS session's message history (the full record — never compacted, audit-complete). " +
|
|
87
|
+
"Use when you need to recall what was said or done earlier: design decisions, tool-call timing, past rulings. " +
|
|
88
|
+
"Filters combine with AND: role / keyword (case-insensitive substring of message text) / " +
|
|
89
|
+
"tool (tool result messages by name AND the assistant messages that declared the call — pair with tool_call_id / ts for timing) / " +
|
|
90
|
+
"since-until (epoch ms time window; only messages with ts can match) / limit (default 50, clamped to 200) / direction (which end of the matches to take). " +
|
|
91
|
+
"Returns a JSON array in chronological order: [{ts, role, name?, tool_call_id?, content (≈500 chars, truncated marker), tool_calls (names only)}]. " +
|
|
92
|
+
"Messages without ts return ts:null. Content is truncated — the full text is in the session file. " +
|
|
93
|
+
"For file-level changes this run (not messages), use recent_changes.",
|
|
94
|
+
parameters: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
role: { type: "string", enum: ["user", "assistant", "tool"], description: "Only messages with this role." },
|
|
98
|
+
keyword: { type: "string", description: "Case-insensitive substring of the message text (multimodal messages match on their text parts)." },
|
|
99
|
+
tool: { type: "string", description: "Only messages for this tool: role=tool messages with name=tool, plus assistant messages that declared a call to it." },
|
|
100
|
+
since: { type: "integer", description: "Earliest ts to match, epoch ms, INCLUSIVE. Messages without ts never match a time window." },
|
|
101
|
+
until: { type: "integer", description: "Latest ts to match, epoch ms, INCLUSIVE. since > until yields an empty result." },
|
|
102
|
+
limit: { type: "integer", description: "Maximum messages to return (default 50; larger values are clamped to 200)." },
|
|
103
|
+
direction: { type: "string", enum: ["oldest", "newest"], description: "Take the limit window from the oldest or newest end of the matched set (default newest)." },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
readonly: true,
|
|
107
|
+
execute(args, ctx) {
|
|
108
|
+
const a = args ?? {}
|
|
109
|
+
const role = a.role
|
|
110
|
+
if (role !== undefined && (typeof role !== "string" || !VALID_ROLES.has(role))) {
|
|
111
|
+
return `Error: invalid role "${role}" — valid roles: user, assistant, tool`
|
|
112
|
+
}
|
|
113
|
+
const direction = a.direction ?? "newest"
|
|
114
|
+
if (direction !== "oldest" && direction !== "newest") {
|
|
115
|
+
return `Error: invalid direction "${direction}" — valid values: oldest, newest`
|
|
116
|
+
}
|
|
117
|
+
const since = parseTs(a.since, "since")
|
|
118
|
+
if (typeof since === "string") return since
|
|
119
|
+
const until = parseTs(a.until, "until")
|
|
120
|
+
if (typeof until === "string") return until
|
|
121
|
+
let limit = DEFAULT_LIMIT
|
|
122
|
+
if (a.limit !== undefined) {
|
|
123
|
+
limit = Math.floor(Number(a.limit))
|
|
124
|
+
if (!Number.isFinite(limit)) return `Error: invalid limit "${a.limit}" — must be a number`
|
|
125
|
+
limit = Math.min(Math.max(1, limit), MAX_LIMIT)
|
|
126
|
+
}
|
|
127
|
+
const keyword = typeof a.keyword === "string" && a.keyword.length > 0 ? a.keyword : null
|
|
128
|
+
// Case-insensitive substring WITHOUT copying the full message text: the human line is
|
|
129
|
+
// never compacted — single tool results can be hundreds of KB to MBs. Lowercase the
|
|
130
|
+
// needle once and run a regex-i test over the haystack (escaping regex metachars so the
|
|
131
|
+
// keyword stays a literal substring).
|
|
132
|
+
const kwRe = keyword ? new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : null
|
|
133
|
+
const tool = typeof a.tool === "string" && a.tool.length > 0 ? a.tool : null
|
|
134
|
+
|
|
135
|
+
const history = Array.isArray(ctx.agent?._fullHistory) ? ctx.agent._fullHistory : []
|
|
136
|
+
const matched = history.filter((m) => {
|
|
137
|
+
if (!m || typeof m !== "object") return false
|
|
138
|
+
if (role !== undefined && m.role !== role) return false
|
|
139
|
+
if (kwRe) {
|
|
140
|
+
const text = messageText(m)
|
|
141
|
+
if (!kwRe.test(text)) return false
|
|
142
|
+
}
|
|
143
|
+
if (tool) {
|
|
144
|
+
const byName = m.role === "tool" && m.name === tool
|
|
145
|
+
const byDeclaration = m.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.some((tc) => toolCallName(tc) === tool)
|
|
146
|
+
if (!byName && !byDeclaration) return false
|
|
147
|
+
}
|
|
148
|
+
const ts = m.ts
|
|
149
|
+
if (since !== null || until !== null) {
|
|
150
|
+
if (typeof ts !== "number") return false // no ts → no time-window match
|
|
151
|
+
if (since !== null && ts < since) return false
|
|
152
|
+
if (until !== null && ts > until) return false
|
|
153
|
+
}
|
|
154
|
+
return true
|
|
155
|
+
})
|
|
156
|
+
// Direction picks the END of the matched set; output stays chronological either way.
|
|
157
|
+
const windowed = direction === "oldest" ? matched.slice(0, limit) : matched.slice(-limit)
|
|
158
|
+
return JSON.stringify(windowed.map(toEntry))
|
|
159
|
+
},
|
|
160
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings tool — agent-side configuration adjustment (SETTINGS-TOOL.md, 2026-09-05).
|
|
3
|
+
* 用户裁定:全量 config.json 任意键(点分路径)+ 单工具多动作 list/get/set + 双端同批。
|
|
4
|
+
* 语义:set = 写盘(config.json——磁盘真相最小化:只写被设的键——默认值不固化)+
|
|
5
|
+
* 热应用(ctx.agent.config 内存对象立即更新——回合边界键下回合生效)。
|
|
6
|
+
* 护栏:敏感键(路径段含 apiKey/key/token/secret/password)回显/错误文本永不出现明文
|
|
7
|
+
* (••••(masked)——防密钥泄漏进会话历史/trace);已知键类型校验(类型表自动派生自
|
|
8
|
+
* config.mjs DEFAULTS——不手写防漂移);set 侧效走审批门(dispatch 动作级分类)。
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
11
|
+
import { join } from "node:path"
|
|
12
|
+
import { DEFAULTS, configPath } from "../config.mjs"
|
|
13
|
+
|
|
14
|
+
/** 敏感键段判定(完整点分路径的段级匹配——apiKey/api_key/api-key/token/secret/password 形态) */
|
|
15
|
+
const SENSITIVE_SEGMENT = /(^|[._-])(api[_-]?key|key|token|secret|password)($|[._-])/i
|
|
16
|
+
const MASKED = "••••(masked)"
|
|
17
|
+
|
|
18
|
+
function isSensitiveKey(path) {
|
|
19
|
+
return SENSITIVE_SEGMENT.test(path)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 类型表:递归遍历 DEFAULTS——叶子(非对象/数组值)记 `路径 → typeof 默认值`。
|
|
24
|
+
* 数组(providersList 等)不递归(下标元素无类型约束——全量域);对象节递归到叶子。
|
|
25
|
+
*/
|
|
26
|
+
function buildTypeMap(obj, prefix = "", out = {}) {
|
|
27
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
28
|
+
const p = prefix ? `${prefix}.${k}` : k
|
|
29
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) buildTypeMap(v, p, out)
|
|
30
|
+
else out[p] = Array.isArray(v) ? "array" : typeof v
|
|
31
|
+
}
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
const TYPE_MAP = buildTypeMap(DEFAULTS)
|
|
35
|
+
|
|
36
|
+
/** 点分路径解析(含数组数字段)——逐段下钻;返回 { ok, value } 或缺失段信息 */
|
|
37
|
+
function resolvePath(obj, path) {
|
|
38
|
+
const segs = String(path).split(".")
|
|
39
|
+
let cur = obj
|
|
40
|
+
for (let i = 0; i < segs.length; i++) {
|
|
41
|
+
if (cur === null || typeof cur !== "object" || !(segs[i] in cur)) {
|
|
42
|
+
return { ok: false, reached: segs.slice(0, i).join("."), missing: segs[i], depth: i }
|
|
43
|
+
}
|
|
44
|
+
cur = cur[segs[i]]
|
|
45
|
+
}
|
|
46
|
+
return { ok: true, value: cur }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 点分路径写入(自动建中间对象——cmd-config 数值项同款);返回旧值 */
|
|
50
|
+
function setKeyPath(obj, path, value) {
|
|
51
|
+
const segs = String(path).split(".")
|
|
52
|
+
let cur = obj
|
|
53
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
54
|
+
if (cur[segs[i]] === null || typeof cur[segs[i]] !== "object") cur[segs[i]] = {}
|
|
55
|
+
cur = cur[segs[i]]
|
|
56
|
+
}
|
|
57
|
+
const last = segs[segs.length - 1]
|
|
58
|
+
const old = cur[last]
|
|
59
|
+
cur[last] = value
|
|
60
|
+
return old
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 递归展平 config 对象(数组下标段形态)→ 排序的 { path, value } 列表 */
|
|
64
|
+
function flatten(obj, prefix = "", out = []) {
|
|
65
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
66
|
+
const p = prefix ? `${prefix}.${k}` : k
|
|
67
|
+
if (v !== null && typeof v === "object") flatten(v, p, out)
|
|
68
|
+
else out.push({ path: p, value: v })
|
|
69
|
+
}
|
|
70
|
+
return out.sort((a, b) => (a.path < b.path ? -1 : 1))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 行格式化:`key = value (type)`——敏感键值遮罩 */
|
|
74
|
+
function formatLine({ path, value }) {
|
|
75
|
+
const shown = isSensitiveKey(path) ? MASKED : value
|
|
76
|
+
return `${path} = ${shown} (${Array.isArray(value) ? "array" : typeof value})`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseValue(raw) {
|
|
80
|
+
const s = String(raw)
|
|
81
|
+
try {
|
|
82
|
+
const v = JSON.parse(s)
|
|
83
|
+
// JSON 顶层标量都接受;解析失败(裸字符串)→ 字符串字面
|
|
84
|
+
if (typeof v === "number" || typeof v === "boolean" || v === null || Array.isArray(v) || typeof v === "object") return v
|
|
85
|
+
return s // "abc"(带引号传的字符串)——JSON.parse 成功但返回字符串——保持解析结果
|
|
86
|
+
} catch {
|
|
87
|
+
return s // 裸 abc → 字符串字面
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function settingsTool(opts = {}) {
|
|
92
|
+
const cfgPath = opts.configPath ?? configPath // 测试注入 tmp 文件;默认全局 configPath
|
|
93
|
+
|
|
94
|
+
async function readDisk() {
|
|
95
|
+
let text
|
|
96
|
+
try { text = readFileSync(cfgPath, "utf8") } catch { return {} } // 不存在 → 空对象(写盘最小化——默认不固化)
|
|
97
|
+
try { return JSON.parse(text) } catch { throw new Error(`settings: config file not parseable — refusing to overwrite: ${cfgPath}`) }
|
|
98
|
+
}
|
|
99
|
+
async function writeDisk(disk) {
|
|
100
|
+
mkdirSync(join(cfgPath, ".."), { recursive: true })
|
|
101
|
+
writeFileSync(cfgPath, JSON.stringify(disk, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
name: "settings",
|
|
106
|
+
description:
|
|
107
|
+
"Adjust ThinCoder runtime configuration — persisted to config.json AND hot-applied to the live agent config.\n" +
|
|
108
|
+
"Actions: list (all keys + values, flattened) | get <key> | set <key> <value> — dot paths into config.json (agent.maxTurns, traces.enabled, providers.0.model, any nesting).\n" +
|
|
109
|
+
"set persists to disk (only the set key is written — defaults are never baked in) and takes effect in the running session immediately (turn-boundary keys apply next turn); the value survives restarts.\n" +
|
|
110
|
+
"Known keys are type-checked against the built-in defaults (agent.maxTurns must be a number, traces.enabled a boolean); unknown keys under a known section are stored as given. Values parse as JSON first (true/false/numbers/objects/arrays), else stay strings.\n" +
|
|
111
|
+
"SENSITIVE keys (path segment contains apiKey/key/token/secret/password) are NEVER echoed in plaintext — list/get/set replies show ••••(masked); setting a sensitive key is allowed and stored, but never echoed back.\n" +
|
|
112
|
+
"list/get are read-only (planMode ok, no approval); set is a side effect (approval gate). The /config TUI command is the human equivalent.",
|
|
113
|
+
parameters: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
action: { type: "string", enum: ["list", "get", "set"], description: "list — full key/value inventory; get <key> — one value; set <key> <value> — persist + hot-apply" },
|
|
117
|
+
key: { type: "string", description: "Dot path into config.json, e.g. agent.maxTurns / traces.enabled / providers.0.model" },
|
|
118
|
+
value: { type: "string", description: "New value for set — parsed as JSON first (true/5/{...}/[...]), otherwise kept as string" },
|
|
119
|
+
},
|
|
120
|
+
required: ["action"],
|
|
121
|
+
},
|
|
122
|
+
readonly: false, // set 是侧效——dispatch 动作级分类放行 list/get
|
|
123
|
+
async execute(args, ctx) {
|
|
124
|
+
const action = args?.action
|
|
125
|
+
if (!["list", "get", "set"].includes(action)) throw new Error(`settings: action must be one of list/get/set — got ${JSON.stringify(action)}`)
|
|
126
|
+
const config = ctx?.agent?.config ?? null
|
|
127
|
+
if (!config) throw new Error("settings: no live agent config (ctx.agent.config missing)")
|
|
128
|
+
if (action === "list") {
|
|
129
|
+
const rows = flatten(config)
|
|
130
|
+
if (rows.length === 0) return "(空配置——无已设置键)"
|
|
131
|
+
return rows.map(formatLine).join("\n")
|
|
132
|
+
}
|
|
133
|
+
if (action === "get") {
|
|
134
|
+
if (!args.key) throw new Error("settings get: key is required (dot path, e.g. agent.maxTurns)")
|
|
135
|
+
const r = resolvePath(config, args.key)
|
|
136
|
+
if (!r.ok) {
|
|
137
|
+
const hint = r.depth === 0 ? "(config 顶层无此键——顶层可用键见 settings list)" : `(父键 ${r.reached || "顶层"} 存在——无 ${r.missing} 子键)`
|
|
138
|
+
throw new Error(`settings get: no such key "${args.key}" ${hint}`)
|
|
139
|
+
}
|
|
140
|
+
return formatLine({ path: args.key, value: r.value })
|
|
141
|
+
}
|
|
142
|
+
// set
|
|
143
|
+
if (!args.key || args.value === undefined) throw new Error("settings set: key and value are required")
|
|
144
|
+
const value = parseValue(args.value)
|
|
145
|
+
// 已知键类型校验(类型表派生自 DEFAULTS——未知键 JSON 原样)
|
|
146
|
+
const want = TYPE_MAP[String(args.key)]
|
|
147
|
+
if (want && want !== "array") {
|
|
148
|
+
const got = value === null ? "null" : typeof value
|
|
149
|
+
if (got !== want && !(value === null && want === "object")) {
|
|
150
|
+
throw new Error(`settings set: "${args.key}" expects ${want} — got ${got} (${JSON.stringify(args.value)})`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// 写盘(磁盘真相最小化:只改被设键——默认不固化)+ 热应用(内存对象)
|
|
154
|
+
const disk = await readDisk()
|
|
155
|
+
setKeyPath(disk, args.key, value)
|
|
156
|
+
await writeDisk(disk)
|
|
157
|
+
setKeyPath(config, args.key, value)
|
|
158
|
+
const shown = isSensitiveKey(String(args.key)) ? MASKED : value
|
|
159
|
+
return `settings set: ${args.key} = ${shown} (${Array.isArray(value) ? "array" : typeof value})${isSensitiveKey(String(args.key)) ? " — stored(值不回显)" : " — persisted + hot-applied(运行中已生效)"}`
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -9,7 +9,8 @@ import { escapeXml } from "../agent.mjs"
|
|
|
9
9
|
export const skillTool = {
|
|
10
10
|
name: "skill",
|
|
11
11
|
description:
|
|
12
|
-
"Load a project skill from .thincoder/skills/. Skills contain reusable instructions, workflows, or reference material. Use this when the user references a skill by name, or when a task matches a known skill's description. Call with action='list' to see available skills; call with action='load' and name=<skill> to activate one."
|
|
12
|
+
"Load a project skill from .thincoder/skills/. Skills contain reusable instructions, workflows, or reference material. Use this when the user references a skill by name, or when a task matches a known skill's description. Call with action='list' to see available skills; call with action='load' and name=<skill> to activate one. " +
|
|
13
|
+
"Returns the skill list ('list'), the load confirmation ('load' — instructions arrive in the next message), or Error: ... with the available skills.",
|
|
13
14
|
parameters: {
|
|
14
15
|
type: "object",
|
|
15
16
|
properties: {
|