thincoder 0.12.60 → 0.12.61
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 +24 -1
- package/README.md +8 -6
- package/bin/thincoder.mjs +27 -122
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +36 -134
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +124 -329
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +3 -2
- package/src/agent/completion.mjs +1 -1
- package/src/agent/dispatch.mjs +47 -12
- package/src/agent/helpers.mjs +71 -13
- package/src/agent/record-results.mjs +13 -5
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +21 -6
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +26 -9
- package/src/agent-tools/advisor-async.mjs +70 -180
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +69 -20
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +23 -10
- package/src/agent-tools/design-token.mjs +14 -1
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +3 -3
- package/src/agent-tools/escalate-async.mjs +16 -13
- package/src/agent-tools/read-history.mjs +13 -3
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +15 -6
- package/src/agent-tools/subagent-async.mjs +66 -14
- package/src/agent-tools/subagent-panel.mjs +22 -15
- package/src/agent-tools/subagent-run.mjs +9 -6
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +63 -16
- package/src/agent-tools/subagent.mjs +175 -49
- package/src/agent-tools/verify.mjs +13 -34
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +38 -21
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +23 -7
- package/src/cli/memory-command.mjs +2 -2
- package/src/cli/setup-wizard.mjs +29 -9
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +132 -63
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +7 -2
- package/src/expand-home.mjs +16 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +70 -31
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +10 -6
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/prompt-overlays.mjs +73 -16
- package/src/prompts/advisor-design.md +18 -8
- package/src/prompts/advisor-round1.md +7 -1
- package/src/prompts/advisor-round2.md +13 -2
- package/src/prompts/advisor-round3.md +13 -2
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +13 -32
- package/src/provider/errors.mjs +26 -1
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-slots.mjs +7 -2
- package/src/session.mjs +59 -38
- package/src/token-ttl.mjs +2 -1
- package/src/tools/{system.mjs → bash.mjs} +6 -243
- package/src/tools/file.mjs +30 -10
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +15 -34
- package/src/tools/index.mjs +4 -2
- package/src/tools/ops.mjs +20 -7
- package/src/tools/question.md +1 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -1
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tui/agent-turn.mjs +32 -13
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +108 -37
- package/src/tui/cmd-eng.mjs +11 -27
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/index.mjs +38 -37
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +47 -10
- package/src/tui/pickers.mjs +28 -410
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +5 -19
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/subagent-blocks.mjs +69 -90
- package/src/tui/subagent-children.mjs +50 -64
- package/src/tui/subagent-freeze.mjs +40 -43
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +28 -82
- package/src/tui/tool-args.mjs +4 -2
- package/src/tui/tool-display.mjs +4 -3
- package/src/tui/tool-events.mjs +23 -16
- package/src/tui/tui-lifecycle.mjs +18 -5
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -13
- package/src/prompts/discipline.md +0 -84
- package/src/prompts/eng-coder.md +0 -19
- package/src/prompts/engineering-sub.md +0 -14
- package/src/prompts/engineering.md +0 -87
- package/src/prompts/explore.md +0 -12
- package/src/prompts/main.md +0 -34
- package/src/prompts/methodology-template.md +0 -38
- package/src/prompts/plan.md +0 -9
- package/src/prompts/system.md +0 -44
package/src/config.mjs
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* config.mjs — configuration loading and saving
|
|
3
|
-
*
|
|
3
|
+
* Model-merge schema v2 (2026-09-10 MODEL-SELECTION): providers[] carry ONE default model
|
|
4
|
+
* per channel (`providers[].model` — the new-install seed / empty-slot fallback); the
|
|
5
|
+
* candidates list field models[] is gone — the available-model list is fetched from the
|
|
6
|
+
* provider at runtime (`GET /models`, PROVIDER.md §16). config.defaultModel (top level,
|
|
7
|
+
* "provider:model" composite) is the new-session starting point; activeProvider/activeModel
|
|
8
|
+
* are gone from config (session slots keep their own double fields). Legacy fields migrate
|
|
9
|
+
* on load (write-back failure never blocks startup).
|
|
4
10
|
* Config file: ~/.thincoder/config.json
|
|
5
11
|
* API key can fall back to environment variables (when not configured in providers).
|
|
6
12
|
*/
|
|
@@ -8,13 +14,27 @@
|
|
|
8
14
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
9
15
|
import { homedir } from "node:os"
|
|
10
16
|
import { dirname, join } from "node:path"
|
|
17
|
+
import { parseModelRef, resolveRuntimeProvider, defaultModelReason } from "./model-ref.mjs"
|
|
18
|
+
// 老形态迁移核(M7 v2——纯函数零依赖——本文件超 500 行硬限拆分,VSC 同构文件)
|
|
19
|
+
import { migrateLegacyModelFields } from "./config-migrate.mjs"
|
|
20
|
+
import { expandHome } from "./expand-home.mjs"
|
|
21
|
+
|
|
22
|
+
export { parseModelRef, resolveRuntimeProvider, defaultModelReason, migrateLegacyModelFields }
|
|
11
23
|
|
|
12
24
|
export const configDir = join(homedir(), ".thincoder")
|
|
13
25
|
export const configPath = join(configDir, "config.json")
|
|
14
26
|
|
|
15
|
-
/**
|
|
27
|
+
/** Test seam: override the config file location (mirrors thincoder-vscode config-io.mjs). */
|
|
28
|
+
let _pathOverride = null
|
|
29
|
+
export function _setConfigPathForTest(p) { _pathOverride = p }
|
|
30
|
+
export function _resetConfigPathForTest() { _pathOverride = null }
|
|
31
|
+
function cfgPath() { return _pathOverride ?? configPath }
|
|
32
|
+
|
|
33
|
+
/** Built-in provider presets: shared by /provider add <preset> and first-run wizard.
|
|
34
|
+
* MODEL-SELECTION v2 (2026-09-10): each preset carries exactly ONE default model
|
|
35
|
+
* (`model` — 新装启动种子/槽位空兜底; 候选清单不再预置——运行期从 provider 拉取). */
|
|
16
36
|
export const PROVIDER_PRESETS = {
|
|
17
|
-
deepseek: { baseURL: "https://api.deepseek.com", model: "deepseek-
|
|
37
|
+
deepseek: { baseURL: "https://api.deepseek.com", model: "deepseek-flash", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 393216, desc: "DeepSeek" },
|
|
18
38
|
kimi: { baseURL: "https://api.moonshot.cn/v1", model: "kimi-k3", thinking: null, reasoningEffort: "max", maxTokens: 131072, desc: "Kimi / Moonshot" },
|
|
19
39
|
"kimi-code": { baseURL: "https://api.kimi.com/coding/v1", model: "k3", thinking: null, reasoningEffort: "max", maxTokens: 131072, desc: "Kimi For Coding (platform.kimi.com — sk-kimi- keys; NOT interchangeable with Moonshot)" },
|
|
20
40
|
glm: { baseURL: "https://open.bigmodel.cn/api/paas/v4", model: "glm-5.2", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 128000, desc: "Zhipu GLM" },
|
|
@@ -37,7 +57,7 @@ export const PROVIDER_PRESETS = {
|
|
|
37
57
|
}
|
|
38
58
|
|
|
39
59
|
export const DEFAULTS = {
|
|
40
|
-
|
|
60
|
+
defaultModel: null, // top-level "provider:model" composite — new-session starting point (F-1)
|
|
41
61
|
agent: {
|
|
42
62
|
maxTurns: 200,
|
|
43
63
|
subagentTurns: 100,
|
|
@@ -54,15 +74,19 @@ export const DEFAULTS = {
|
|
|
54
74
|
streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
|
|
55
75
|
advisor: { guard: false }, // code review is always available; guard: true pushes completion back until reviewed (opt-in). Also accepts provider/model/thinking/reasoningEffort/timeoutMs overrides. Deprecated: enabled (2026-08-21)
|
|
56
76
|
autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
|
|
57
|
-
engineering: false, // strict methodology enforcement —
|
|
58
|
-
// Async
|
|
59
|
-
// { engCoder, other } — eng-coder pool / other-role
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
77
|
+
engineering: false, // strict methodology enforcement — design-before-code (design review + user approval before code)
|
|
78
|
+
// Async pool limits (AGENT-LOOP.md §11.1 D-24a/R14 + §11.2 R13 — POOL-CONFIG-
|
|
79
|
+
// UNIFIED 2026-09-09): { engCoder, other, advisor } — eng-coder pool / other-role
|
|
80
|
+
// pool / advisor-review pool, defaults 4/4/4 (user ruling "eng-coder 四路,其他
|
|
81
|
+
// 4 路" + advisor 评审池并入同一可配体系——三池统一默认 4)。engCoder/other
|
|
82
|
+
// runtime-validated at every subagent pool admission (subagent-async
|
|
83
|
+
// resolvePoolLimits); advisor runtime-validated at every advisor launch
|
|
84
|
+
// (advisor-async advisorPoolLimitFor——F-2——两域读取器独立不共享)——非法/缺省
|
|
85
|
+
// 回退 4——settings tool 与 /config 并发池菜单写此键——变更下个 spawn/launch 生效。
|
|
86
|
+
// ⚠ 与 subagent-async.mjs ASYNC_POOL_LIMITS(两键)/advisor-async.mjs
|
|
87
|
+
// ADVISOR_POOL_LIMIT 逐键同值(运行时回退常量)——耦合锚 T-24a4 断言锁住——
|
|
88
|
+
// 勿单侧改默认。
|
|
89
|
+
poolLimits: { engCoder: 4, other: 4, advisor: 4 },
|
|
66
90
|
},
|
|
67
91
|
memory: {
|
|
68
92
|
dbPath: join(configDir, "memory.db"),
|
|
@@ -78,7 +102,7 @@ export const DEFAULTS = {
|
|
|
78
102
|
servers: [],
|
|
79
103
|
},
|
|
80
104
|
websearch: {
|
|
81
|
-
|
|
105
|
+
// Structured search via Tavily when a key is set — empty apiKey → Bing RSS/HTML fallback (zero-config).
|
|
82
106
|
apiKey: "", // Tavily key (tvly-...) — optional
|
|
83
107
|
},
|
|
84
108
|
traces: {
|
|
@@ -90,8 +114,8 @@ export const DEFAULTS = {
|
|
|
90
114
|
// Model capability table + spec lookup live in model-specs.mjs (2026-08-31
|
|
91
115
|
// extract — config.mjs had grown past the 300-line advisory). Re-exported here
|
|
92
116
|
// so the 23 existing importers keep their import paths.
|
|
93
|
-
import { specForModel, providerSpec } from "./model-specs.mjs"
|
|
94
|
-
export { specForModel, providerSpec }
|
|
117
|
+
import { specForModel, providerSpec, specMatch } from "./model-specs.mjs"
|
|
118
|
+
export { specForModel, providerSpec, specMatch }
|
|
95
119
|
|
|
96
120
|
|
|
97
121
|
// Window utilization threshold: compacts at 60% context, reserving 40% headroom
|
|
@@ -144,6 +168,44 @@ export function resolveEnableThinking(provider, spec) {
|
|
|
144
168
|
/** Module-level one-time warn dedupe for invalid providers[].context (PROVIDER.md §15 D-C1). */
|
|
145
169
|
const warnedContextProviders = new Set()
|
|
146
170
|
|
|
171
|
+
/** F-4 (IKCDMR) 软失败清洗(D-S1 范式——loadConfig 是 CLI 启动砖点):consultModels 非法
|
|
172
|
+
* 条目过滤不 throw——非数组 → []、形状非法/未知渠道丢弃、超 5 截断(keep 前 5)。
|
|
173
|
+
* 返回 { keep, dropped }——dropped 供一次性警告;merged.agent.consultModelsFiltered 挂载
|
|
174
|
+
* 供后续首帧引导消费(数据层——本批文件面无消费方——同 providerInvalidReason 载体先例)。 */
|
|
175
|
+
function sanitizeConsultModels(cm, providerNames) {
|
|
176
|
+
if (cm === undefined || cm === null) return { keep: [], dropped: [] }
|
|
177
|
+
if (!Array.isArray(cm)) {
|
|
178
|
+
return { keep: [], dropped: [`agent.consultModels must be an array of { provider, model } entries (got ${typeof cm})`] }
|
|
179
|
+
}
|
|
180
|
+
const names = new Set(providerNames)
|
|
181
|
+
const keep = []
|
|
182
|
+
const dropped = []
|
|
183
|
+
for (const entry of cm) {
|
|
184
|
+
if (keep.length >= 5) { dropped.push(`over the 5-entry cap — dropped ${JSON.stringify(entry)}`); continue }
|
|
185
|
+
if (!entry || typeof entry !== "object" || typeof entry.provider !== "string" || !entry.provider.trim()
|
|
186
|
+
|| typeof entry.model !== "string" || !entry.model.trim()) {
|
|
187
|
+
dropped.push(`invalid entry (expected { provider: string, model: string }) — got ${JSON.stringify(entry)}`)
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
if (!names.has(entry.provider)) {
|
|
191
|
+
dropped.push(`entry "${entry.provider}:${entry.model}" references unknown provider "${entry.provider}" (available: ${[...names].join(", ") || "none"})`)
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
keep.push(entry)
|
|
195
|
+
}
|
|
196
|
+
return { keep, dropped }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** F-4:过滤警告——进程级一次性(loadConfig 同进程可多次调用:TUI 启动 + reloadConfig)。 */
|
|
200
|
+
let warnedConsultModels = false
|
|
201
|
+
function warnConsultModelsFiltered(dropped, path) {
|
|
202
|
+
if (warnedConsultModels || dropped.length === 0) return
|
|
203
|
+
warnedConsultModels = true
|
|
204
|
+
console.warn(`[config] agent.consultModels: ${dropped.length} invalid entr${dropped.length === 1 ? "y ignored" : "ies ignored"} (filtered — startup continues; no crash):\n` +
|
|
205
|
+
dropped.map((d) => ` - ${d}`).join("\n") +
|
|
206
|
+
`\n Fix: clean agent.consultModels in ${path} or use /config → consult/escalate pool menu.`)
|
|
207
|
+
}
|
|
208
|
+
|
|
147
209
|
/**
|
|
148
210
|
* Find provider by name in providers[].
|
|
149
211
|
* Throws if name is non-empty but not found — a typo in activeProvider silently falling to the first provider would use the wrong key on the wrong endpoint.
|
|
@@ -187,29 +249,58 @@ function sanitizeProviderHeaders(p) {
|
|
|
187
249
|
return p
|
|
188
250
|
}
|
|
189
251
|
|
|
252
|
+
/** loadConfig 内联迁移核已迁 config-migrate.mjs(500 行硬限拆分——VSC 同构)——
|
|
253
|
+
* migrateLegacyModelFields 纯函数 + 本文件 import/写回编排(折中 C 见其头注释)。 */
|
|
190
254
|
export function loadConfig() {
|
|
191
255
|
let config = {}
|
|
192
|
-
|
|
256
|
+
const path = cfgPath()
|
|
257
|
+
if (existsSync(path)) {
|
|
193
258
|
try {
|
|
194
|
-
config = JSON.parse(readFileSync(
|
|
259
|
+
config = JSON.parse(readFileSync(path, "utf8"))
|
|
195
260
|
} catch (error) {
|
|
196
|
-
throw new Error(`Config file is not valid JSON, check or delete it: ${
|
|
261
|
+
throw new Error(`Config file is not valid JSON, check or delete it: ${path}\n ${error.message}`, { cause: error })
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── 迁移(M7 v2):检测老字段(models[] / active* / 垃圾值)→ 内存迁移态先行;写回失败绝不阻断(下次 load 重试——幂等)──
|
|
266
|
+
if (migrateLegacyModelFields(config)) {
|
|
267
|
+
try {
|
|
268
|
+
const r = writeConfigAtomic(path, migrateLegacyModelFields) // 磁盘 fresh raw 同变换
|
|
269
|
+
if (r.ok === false) console.warn(`[config] migration write-back skipped (${r.reason}) — memory state continues, retried on next load`)
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.warn(`[config] migration write-back failed — memory state continues, retried on next load: ${e.message}`)
|
|
197
272
|
}
|
|
198
273
|
}
|
|
199
274
|
|
|
200
275
|
const merged = {
|
|
201
276
|
...DEFAULTS,
|
|
202
277
|
...config,
|
|
278
|
+
defaultModel: typeof config.defaultModel === "string" && config.defaultModel.trim() ? config.defaultModel : null,
|
|
203
279
|
providers: Array.isArray(config.providers)
|
|
204
280
|
? config.providers.map((p) => sanitizeProviderHeaders({ ...p }))
|
|
205
281
|
: [],
|
|
206
|
-
activeProvider: config.activeProvider ?? "",
|
|
207
282
|
agent: { ...DEFAULTS.agent, ...config.agent },
|
|
208
283
|
memory: { ...DEFAULTS.memory, ...config.memory },
|
|
209
284
|
embedding: { ...DEFAULTS.embedding, ...config.embedding },
|
|
210
285
|
traces: { ...DEFAULTS.traces, ...config.traces },
|
|
211
286
|
}
|
|
212
287
|
|
|
288
|
+
// 家目录展开(第 29 批):config 路径字段单一规范化点——只读归一(磁盘原文保留)
|
|
289
|
+
merged.memory.dbPath = expandHome(merged.memory.dbPath)
|
|
290
|
+
merged.memory.projectDir = expandHome(merged.memory.projectDir)
|
|
291
|
+
const team = merged.memory.team
|
|
292
|
+
if (team && typeof team === "object" && !Array.isArray(team) && team.dir !== undefined) {
|
|
293
|
+
merged.memory.team = { ...team, dir: expandHome(team.dir) } // 无 dir 键不注入(零键面变化)
|
|
294
|
+
}
|
|
295
|
+
merged.shell = expandHome(merged.shell)
|
|
296
|
+
|
|
297
|
+
// providers[].model 内存归一(v2 M3:非空字符串保留;非字符串/空串归一删除)——渠道默认模型
|
|
298
|
+
// 单值;无默认模型合法(模型选择经 /models 拉取候选——准入判据见 M8/M9)。
|
|
299
|
+
for (const p of merged.providers) {
|
|
300
|
+
if (typeof p.model === "string" && p.model.trim()) continue
|
|
301
|
+
if (p.model !== undefined) delete p.model
|
|
302
|
+
}
|
|
303
|
+
|
|
213
304
|
// providers[].context (K units, PROVIDER.md §15 D-C1): positive integer only — invalid
|
|
214
305
|
// values (0/negative/non-numeric) are IGNORED (spec value applies) with a ONE-TIME warn
|
|
215
306
|
// per provider name (module-level dedupe, same precedent as warnedModels in model-specs.mjs).
|
|
@@ -223,27 +314,15 @@ export function loadConfig() {
|
|
|
223
314
|
delete p.context
|
|
224
315
|
}
|
|
225
316
|
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (
|
|
235
|
-
// Fail fast at load: a pool entry whose provider doesn't exist in providers[] fails
|
|
236
|
-
// every consult/escalate call at runtime with a quiet error string (eats a turn).
|
|
237
|
-
const providerNames = merged.providers.map((p) => p.name)
|
|
238
|
-
for (const entry of cm) {
|
|
239
|
-
if (!entry || typeof entry !== "object" || typeof entry.provider !== "string" || typeof entry.model !== "string") {
|
|
240
|
-
throw new Error(`agent.consultModels entries must be { provider: string, model: string } objects (got ${JSON.stringify(entry)})`)
|
|
241
|
-
}
|
|
242
|
-
if (!providerNames.includes(entry.provider)) {
|
|
243
|
-
throw new Error(`agent.consultModels entry "${entry.provider}:${entry.model}" references unknown provider "${entry.provider}" (available: ${providerNames.join(", ") || "none"})`)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
317
|
+
// F-4 (IKCDMR) consultModels 软失败化——D-S1 范式(defaultModel 无效同族):非法条目
|
|
318
|
+
// 过滤不 throw(去 startup brick——无修复入口的硬崩消);一次性启动警告内嵌修复指引
|
|
319
|
+
// (引导清条目载体——保留 discoverability);过滤记录挂 merged.agent.consultModelsFiltered
|
|
320
|
+
// (数据层——consumer-ready——首帧 UI 消费点同 D-S1 promptProviderIfInvalid 属后续批)。
|
|
321
|
+
// VSC loadAgentSettings 同规则(共享 config——双端锁步)。
|
|
322
|
+
const cmClean = sanitizeConsultModels(merged.agent.consultModels, merged.providers.map((p) => p.name))
|
|
323
|
+
warnConsultModelsFiltered(cmClean.dropped, path)
|
|
324
|
+
merged.agent.consultModels = cmClean.keep
|
|
325
|
+
if (cmClean.dropped.length) merged.agent.consultModelsFiltered = cmClean.dropped
|
|
247
326
|
|
|
248
327
|
// Backward compatibility: promote root-level config fields to agent sub-object
|
|
249
328
|
if (config.verifyGuard !== undefined) {
|
|
@@ -259,35 +338,24 @@ export function loadConfig() {
|
|
|
259
338
|
// 保证 agent.config.proxy 永远是规范形态或 undefined
|
|
260
339
|
merged.proxy = normalizeProxy(merged.proxy)
|
|
261
340
|
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
} catch {
|
|
271
|
-
active = {}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Build runtime provider object (for agent.provider usage)
|
|
275
|
-
const runtimeProvider = { ...active }
|
|
276
|
-
|
|
277
|
-
// activeModel overrides provider's default model (config only)
|
|
278
|
-
if (merged.activeModel) runtimeProvider.model = merged.activeModel
|
|
279
|
-
merged.activeModel = merged.activeModel || null // normalize for agent.activeModel
|
|
341
|
+
// Runtime provider = config.defaultModel 复合解析(F-2——resolveRuntimeProvider)。
|
|
342
|
+
// 无效/未设 → {} + providerInvalidReason(D-S1 处置不 throw——make-agent 打 _providerInvalid
|
|
343
|
+
// 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用 findProvider
|
|
344
|
+
// throw 契约——findProvider 保留给 advisor/run.mjs 等直接调用方)。
|
|
345
|
+
merged.provider = resolveRuntimeProvider(merged.providers, merged.defaultModel)
|
|
346
|
+
merged.providerInvalidReason = merged.provider.name
|
|
347
|
+
? null
|
|
348
|
+
: defaultModelReason(merged.providers, merged.defaultModel)
|
|
280
349
|
|
|
281
350
|
// Compaction threshold follows the model (provider-level context override honored — providerSpec)
|
|
282
351
|
const explicitThreshold = config.agent?.compactThreshold
|
|
283
|
-
const { value, auto } = resolveCompactThreshold(explicitThreshold,
|
|
352
|
+
const { value, auto } = resolveCompactThreshold(explicitThreshold, merged.provider)
|
|
284
353
|
merged.agent.compactThreshold = value
|
|
285
354
|
merged.agent.compactThresholdAuto = auto
|
|
286
355
|
|
|
287
356
|
// Write back to merged for convenient access by upper layers
|
|
288
|
-
merged.provider = runtimeProvider
|
|
289
357
|
// fetch 超时可配置(2026-09-01:agent.fetchTimeoutMs——provider/core.mjs effectiveFetchTimeoutMs 消费)
|
|
290
|
-
|
|
358
|
+
merged.provider.fetchTimeoutMs = Number.isFinite(merged.agent?.fetchTimeoutMs) && merged.agent.fetchTimeoutMs > 0
|
|
291
359
|
? merged.agent.fetchTimeoutMs : undefined
|
|
292
360
|
merged.providersList = merged.providers
|
|
293
361
|
merged.advisor = { ...merged.agent.advisor } // promote for consistent access (decoupled copy)
|
|
@@ -344,7 +412,8 @@ function readMcpSection(path = configPath) {
|
|
|
344
412
|
if (!existsSync(path)) return { ok: true, servers: [] }
|
|
345
413
|
const raw = JSON.parse(readFileSync(path, "utf8"))
|
|
346
414
|
const servers = raw?.mcp?.servers
|
|
347
|
-
|
|
415
|
+
// 非数组 = 畸形磁盘配置(2.3 代码正确性批)——ok:false 走调用方既有畸形回退(reloadMcpFromDisk 早退)
|
|
416
|
+
if (servers !== undefined && !Array.isArray(servers)) return { ok: false, error: "mcp.servers must be an array" }
|
|
348
417
|
return { ok: true, servers: Array.isArray(servers) ? servers : [] }
|
|
349
418
|
} catch (error) {
|
|
350
419
|
return { ok: false, error: error?.message ?? String(error) }
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conventions.mjs — the single authority for code / doc / temp path classification,
|
|
3
|
+
* plus the project convention declaration surface (`.thincoder/conventions.json`).
|
|
4
|
+
*
|
|
5
|
+
* Why one module: engineering-mode gates and guards (design gate, review-doc gate,
|
|
6
|
+
* mutation accounting, verify fast path) each carried their own copy of the
|
|
7
|
+
* "what counts as product code" predicate — anchored `^src/` regexes, `docs/`
|
|
8
|
+
* prefix checks, component regexes. Each copy drifted, and each hardcoded THIS
|
|
9
|
+
* repository's layout: a project whose code lives outside `src/` slipped through
|
|
10
|
+
* the design gate silently (PORTABILITY FR12 / PO-10). One classifier + one
|
|
11
|
+
* declaration file = one truth.
|
|
12
|
+
*
|
|
13
|
+
* Defaults are DATA (`DEFAULT_CODE_PATHS`) — overridable per project through the
|
|
14
|
+
* declaration file (§4.1 schema). Missing file → pure defaults (no noise);
|
|
15
|
+
* corrupt/unreadable file → defaults + console.warn + a log event (never crash,
|
|
16
|
+
* never swallow — PORTABILITY FR10).
|
|
17
|
+
*
|
|
18
|
+
* Classification vocabulary (PORTABILITY design §3.1):
|
|
19
|
+
* code — inside a declared code segment (default: the path segment `src`), or
|
|
20
|
+
* not a documentation extension; doc — documentation extension outside
|
|
21
|
+
* any code segment; temp — tmp-* name or .tmp/.temp extension.
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync } from "node:fs"
|
|
24
|
+
import { join, resolve } from "node:path"
|
|
25
|
+
import { logEvent } from "./log.mjs"
|
|
26
|
+
|
|
27
|
+
/** Default code-path segments (data, not logic — a project may replace them). */
|
|
28
|
+
export const DEFAULT_CODE_PATHS = ["src"]
|
|
29
|
+
|
|
30
|
+
/** Project declaration file, relative to the project root. */
|
|
31
|
+
export const CONVENTIONS_REL_PATH = ".thincoder/conventions.json"
|
|
32
|
+
|
|
33
|
+
/** Documentation predicate (moved here verbatim from advisor/repos.mjs — one copy). */
|
|
34
|
+
const DOC_FILE = /(?:^|[/\\])(?:LICENSE|NOTICE|CHANGELOG|AUTHORS)(?:\.\w+)?$|\.(?:md|markdown|mdx|txt|rst|adoc)$/i
|
|
35
|
+
|
|
36
|
+
/** Temp/scratch predicate (moved here verbatim from advisor/repos.mjs). */
|
|
37
|
+
const TEMP_FILE = /(?:^|[/\\])tmp-[^/\\]+$|\.(?:tmp|temp)$/i
|
|
38
|
+
|
|
39
|
+
/** True when a path is a throwaway temp file (tmp-* name or .tmp/.temp extension). */
|
|
40
|
+
export function isTempPath(p) {
|
|
41
|
+
return TEMP_FILE.test(p ?? "")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Path → segments (both separators accepted; absolute and relative alike). */
|
|
45
|
+
function segmentsOf(p) {
|
|
46
|
+
return String(p ?? "").replace(/\\/g, "/").split("/").filter(Boolean)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* True when the path contains a declared code segment sequence at any depth.
|
|
51
|
+
* Segment matching (not a prefix anchor) is what closes the nested-layout hole:
|
|
52
|
+
* `packages/foo/src/x.md` is product code, not a document. Comparison is
|
|
53
|
+
* case-insensitive — on case-insensitive filesystems `Src/x.mjs` is the same
|
|
54
|
+
* directory, and the gate must not be bypassable by casing.
|
|
55
|
+
*/
|
|
56
|
+
function hasCodeSegment(p, conv) {
|
|
57
|
+
const parts = segmentsOf(p).map((s) => s.toLowerCase())
|
|
58
|
+
const wanted = conv?.codePaths ?? DEFAULT_CODE_PATHS
|
|
59
|
+
for (const entry of wanted) {
|
|
60
|
+
const want = segmentsOf(entry).map((s) => s.toLowerCase())
|
|
61
|
+
if (want.length === 0) continue
|
|
62
|
+
for (let i = 0; i + want.length <= parts.length; i++) {
|
|
63
|
+
if (want.every((seg, j) => parts[i + j] === seg)) return true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** "code" | "doc" | "temp" — the single classification decision.
|
|
70
|
+
* Precedence: code segment first (src/** stays product code even when the name
|
|
71
|
+
* looks scratch — the pre-existing unconditional-src rule), then temp, then a
|
|
72
|
+
* documentation extension, else code (anything not doc/temp is product code). */
|
|
73
|
+
export function classifyPath(p, conv) {
|
|
74
|
+
const s = String(p ?? "")
|
|
75
|
+
if (hasCodeSegment(s, conv)) return "code"
|
|
76
|
+
if (TEMP_FILE.test(s)) return "temp"
|
|
77
|
+
if (DOC_FILE.test(s)) return "doc"
|
|
78
|
+
return "code"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** True when the path is product code (see classifyPath for the precedence). */
|
|
82
|
+
export function isCodePath(p, conv) {
|
|
83
|
+
return classifyPath(p, conv) === "code"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** True when the path is a documentation file — a doc extension that does NOT
|
|
87
|
+
* live inside a declared code segment (src/prompts/*.md is product code). */
|
|
88
|
+
export function isDocPath(p, conv) {
|
|
89
|
+
const s = String(p ?? "")
|
|
90
|
+
return DOC_FILE.test(s) && !hasCodeSegment(s, conv)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
// Declaration loading (cached per project root — `clearConventionsCache()` is
|
|
95
|
+
// the test seam; declaration files change rarely and only at session scope).
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function normalizeExtensions(v) {
|
|
99
|
+
if (!Array.isArray(v)) return []
|
|
100
|
+
const out = []
|
|
101
|
+
for (const e of v) {
|
|
102
|
+
if (typeof e !== "string") continue
|
|
103
|
+
const t = e.trim().toLowerCase()
|
|
104
|
+
if (!t) continue
|
|
105
|
+
const ext = t.startsWith(".") ? t : `.${t}`
|
|
106
|
+
if (!out.includes(ext)) out.push(ext)
|
|
107
|
+
}
|
|
108
|
+
return out
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Declared code paths replace the default (replacement, not union — §4.1). */
|
|
112
|
+
function normalizeCodePaths(v) {
|
|
113
|
+
if (!Array.isArray(v)) return null
|
|
114
|
+
const out = []
|
|
115
|
+
for (const e of v) {
|
|
116
|
+
if (typeof e !== "string") continue
|
|
117
|
+
const s = e.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")
|
|
118
|
+
if (!s || out.includes(s)) continue
|
|
119
|
+
out.push(s)
|
|
120
|
+
}
|
|
121
|
+
return out.length > 0 ? out : null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeString(v) {
|
|
125
|
+
return typeof v === "string" && v.trim() ? v.trim() : ""
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Per-key type check for recognized keys (present but wrong type). §3.2/§4.1: a
|
|
129
|
+
* type error degrades WITH a warning — never silently (the fallback semantics stay
|
|
130
|
+
* per-key; only the visibility is added here). */
|
|
131
|
+
function typeErrorsOf(raw) {
|
|
132
|
+
const isObj = (v) => v !== undefined && v !== null && typeof v === "object" && !Array.isArray(v)
|
|
133
|
+
const strArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string")
|
|
134
|
+
const errs = []
|
|
135
|
+
if (raw.codePaths !== undefined && !strArray(raw.codePaths)) errs.push("codePaths must be an array of strings")
|
|
136
|
+
const idx = raw.index
|
|
137
|
+
if (idx !== undefined && !isObj(idx)) errs.push("index must be an object")
|
|
138
|
+
else if (isObj(idx)) {
|
|
139
|
+
for (const k of ["codeExtensions", "docExtensions"]) {
|
|
140
|
+
if (idx[k] !== undefined && !strArray(idx[k])) errs.push(`index.${k} must be an array of strings`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const adv = raw.advisor
|
|
144
|
+
if (adv !== undefined && !isObj(adv)) errs.push("advisor must be an object")
|
|
145
|
+
else if (isObj(adv)) {
|
|
146
|
+
for (const k of ["docMap", "standardsDoc"]) {
|
|
147
|
+
if (adv[k] !== undefined && typeof adv[k] !== "string") errs.push(`advisor.${k} must be a string`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return errs
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildConventions(raw) {
|
|
154
|
+
const codePaths = normalizeCodePaths(raw?.codePaths)
|
|
155
|
+
const codeExtensions = normalizeExtensions(raw?.index?.codeExtensions)
|
|
156
|
+
const docExtensions = normalizeExtensions(raw?.index?.docExtensions)
|
|
157
|
+
const docMap = normalizeString(raw?.advisor?.docMap)
|
|
158
|
+
const standardsDoc = normalizeString(raw?.advisor?.standardsDoc)
|
|
159
|
+
// `declared` = the declaration actually took effect (at least one recognized key
|
|
160
|
+
// honored) — the design-gate hint reads it to decide whether to point at the
|
|
161
|
+
// declaration file ("declare project conventions … to adjust").
|
|
162
|
+
const declared = Boolean(codePaths || codeExtensions.length || docExtensions.length || docMap || standardsDoc)
|
|
163
|
+
return Object.freeze({
|
|
164
|
+
declared,
|
|
165
|
+
codePaths: Object.freeze(codePaths ?? [...DEFAULT_CODE_PATHS]),
|
|
166
|
+
index: Object.freeze({
|
|
167
|
+
codeExtensions: Object.freeze(codeExtensions),
|
|
168
|
+
docExtensions: Object.freeze(docExtensions),
|
|
169
|
+
}),
|
|
170
|
+
advisor: Object.freeze({ docMap, standardsDoc }),
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Full-default conventions (no declaration) — the fallback every consumer gets. */
|
|
175
|
+
export const DEFAULT_CONVENTIONS = buildConventions(null)
|
|
176
|
+
|
|
177
|
+
const _cache = new Map()
|
|
178
|
+
|
|
179
|
+
/** Drop the per-root cache (test seam — declaration files are read once per root). */
|
|
180
|
+
export function clearConventionsCache() {
|
|
181
|
+
_cache.clear()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Load (and cache) the normalized conventions for a project root.
|
|
186
|
+
* @param {string} cwd — project root (declaration lives at .thincoder/conventions.json)
|
|
187
|
+
* @returns {Readonly<{declared: boolean, codePaths: readonly string[],
|
|
188
|
+
* index: {codeExtensions: string[], docExtensions: string[]},
|
|
189
|
+
* advisor: {docMap: string, standardsDoc: string}}>}
|
|
190
|
+
*/
|
|
191
|
+
export function loadConventions(cwd) {
|
|
192
|
+
const root = resolve(cwd ?? process.cwd())
|
|
193
|
+
const hit = _cache.get(root)
|
|
194
|
+
if (hit) return hit
|
|
195
|
+
let conv = DEFAULT_CONVENTIONS
|
|
196
|
+
try {
|
|
197
|
+
const text = readFileSync(join(root, CONVENTIONS_REL_PATH), "utf8")
|
|
198
|
+
try {
|
|
199
|
+
const raw = JSON.parse(text)
|
|
200
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("top level must be a JSON object")
|
|
201
|
+
conv = buildConventions(raw)
|
|
202
|
+
const typeErrs = typeErrorsOf(raw)
|
|
203
|
+
if (typeErrs.length > 0) {
|
|
204
|
+
// Wrong-typed keys fall back per-key — but the user must SEE that their
|
|
205
|
+
// declaration did not take effect (never silently swallowed).
|
|
206
|
+
console.warn(`[conventions] ${CONVENTIONS_REL_PATH} has invalid value types (${typeErrs.join("; ")}) — those keys fall back to defaults`)
|
|
207
|
+
logEvent("conventions:error", { cwd: root, err: `type errors: ${typeErrs.join("; ").slice(0, 160)}` })
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// Corrupt file / wrong shape → defaults, visible: warn + event (never silent).
|
|
211
|
+
console.warn(`[conventions] ${CONVENTIONS_REL_PATH} unreadable (${e?.message ?? e}) — falling back to defaults`)
|
|
212
|
+
logEvent("conventions:error", { cwd: root, err: String(e?.message ?? e).slice(0, 200) })
|
|
213
|
+
}
|
|
214
|
+
} catch (e) {
|
|
215
|
+
if (e?.code !== "ENOENT") {
|
|
216
|
+
// File exists but cannot be read (EACCES etc.) — same visible degradation.
|
|
217
|
+
console.warn(`[conventions] ${CONVENTIONS_REL_PATH} not readable (${e?.message ?? e}) — falling back to defaults`)
|
|
218
|
+
logEvent("conventions:error", { cwd: root, err: String(e?.message ?? e).slice(0, 200) })
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
_cache.set(root, conv)
|
|
222
|
+
return conv
|
|
223
|
+
}
|
package/src/crash-reports.mjs
CHANGED
|
@@ -31,10 +31,15 @@ export function crashReportsDir() {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** 记录文件模式(评审 #8 定死):crash-*.json = 自写(F-R25a);report.*.json = Node
|
|
34
|
-
* fatal(默认命名 report.YYYYMMDD.HHMMSS.<pid>.<seq>.json——2026-09-07 实现批实测)。
|
|
34
|
+
* fatal(默认命名 report.YYYYMMDD.HHMMSS.<pid>.<seq>.json——2026-09-07 实现批实测)。
|
|
35
|
+
* purge 另含 tui-stderr-*.log(TUI-STDERR-CAPTURE F-2——30 天同族淘汰)——但 recentCrashHint
|
|
36
|
+
* 不计该类:tui-stderr 每次 TUI 启动都生成(正常退出也留档)——计入即正常会话误报"异常终止"。 */
|
|
35
37
|
function isCrashRecordName(name) {
|
|
36
38
|
return /^crash-.+\.json$/.test(name) || /^report\..+\.json$/.test(name)
|
|
37
39
|
}
|
|
40
|
+
function isPurgeRecordName(name) {
|
|
41
|
+
return isCrashRecordName(name) || /^tui-stderr-.+\.log$/.test(name)
|
|
42
|
+
}
|
|
38
43
|
|
|
39
44
|
/** >30 天淘汰(评审 #6)——写时自清理;搭车点 = 写 / 入口 mkdir / 启动扫描(复审 #4)。 */
|
|
40
45
|
function purgeOldCrashReports(dir) {
|
|
@@ -42,7 +47,7 @@ function purgeOldCrashReports(dir) {
|
|
|
42
47
|
let names
|
|
43
48
|
try { names = readdirSync(dir) } catch { return } // 目录不存在/不可读 → 无事可做
|
|
44
49
|
for (const name of names) {
|
|
45
|
-
if (!
|
|
50
|
+
if (!isPurgeRecordName(name)) continue
|
|
46
51
|
try {
|
|
47
52
|
if (statSync(join(dir, name)).mtimeMs < cutoff) unlinkSync(join(dir, name))
|
|
48
53
|
} catch { /* 单个文件失败不影响其余 */ }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* expand-home.mjs — 配置路径字段家目录展开器(第 29 批 HOME-EXPANSION,2026-09-11)。
|
|
3
|
+
* `loadConfig()` 单一规范化点(设计 MEMORY.md §9.3a):纯函数、零依赖、home 可注入。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { homedir } from "node:os"
|
|
7
|
+
import { join } from "node:path"
|
|
8
|
+
|
|
9
|
+
/** 展开配置路径字段的前缀 `~`(`~` / `~/` / `~\`)为主目录绝对路径;不识别形态原样返回。
|
|
10
|
+
* home 第二参 = 测试注入缝(生产缺省 homedir())。 */
|
|
11
|
+
export function expandHome(p, home = homedir()) {
|
|
12
|
+
if (typeof p !== "string" || !p.startsWith("~")) return p // 非字符串 / 非 ~ 前缀 → 原样
|
|
13
|
+
if (p === "~") return home // 裸 ~ = 主目录
|
|
14
|
+
if (p[1] !== "/" && p[1] !== "\\") return p // ~user 等非分隔符 → 原样(不猜用户)
|
|
15
|
+
return join(home, p.slice(2).replaceAll("\\", "/")) // 余段分隔符归一(跨端统一)
|
|
16
|
+
}
|
package/src/generate-title.mjs
CHANGED
|
@@ -46,7 +46,7 @@ export async function generateTitle(userContent, provider) {
|
|
|
46
46
|
const url = `${provider.baseURL.replace(/\/+$/, "")}${chatPath}`
|
|
47
47
|
const opts = {
|
|
48
48
|
method: "POST",
|
|
49
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${provider.apiKey}` },
|
|
49
|
+
headers: { ...(provider.headers ?? {}), "Content-Type": "application/json", Authorization: `Bearer ${provider.apiKey}` }, // 定制头展开(PROVIDER.md §21)——定制头在前、内置头在后:内置头胜出
|
|
50
50
|
body,
|
|
51
51
|
signal: AbortSignal.timeout(10000),
|
|
52
52
|
}
|
package/src/hooks.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* PreToolUse — before each tool call (can block execution)
|
|
9
9
|
* PostToolUse — after successful tool execution
|
|
10
10
|
* PostToolUseFailure — after failed tool execution
|
|
11
|
-
*
|
|
11
|
+
* Stop — main-session run end (no tool name; block is meaningless)
|
|
12
12
|
*
|
|
13
13
|
* Each hook: { matcher?, command, args?, timeout?, action? }
|
|
14
14
|
* matcher: regex against tool name (default: match all)
|
|
@@ -29,9 +29,11 @@ export async function runHooks(event, ctx) {
|
|
|
29
29
|
if (!hooks?.length) return true
|
|
30
30
|
|
|
31
31
|
for (const hook of hooks) {
|
|
32
|
-
|
|
32
|
+
// Matcher = tool-name regex: tool events always carry a toolName; no-tool-name
|
|
33
|
+
// events (Stop) ignore it — a configured matcher must not silently never fire.
|
|
34
|
+
if (hook.matcher && ctx.toolName != null) {
|
|
33
35
|
try {
|
|
34
|
-
if (!new RegExp(hook.matcher).test(ctx.toolName
|
|
36
|
+
if (!new RegExp(hook.matcher).test(ctx.toolName)) continue
|
|
35
37
|
} catch { /* invalid regex → skip */ }
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -48,6 +50,8 @@ async function runOneHook(event, hook, ctx) {
|
|
|
48
50
|
toolArgs: ctx.toolArgs ?? null,
|
|
49
51
|
result: ctx.result ?? null,
|
|
50
52
|
error: ctx.error?.message ?? null,
|
|
53
|
+
// Event-specific fields (Stop: turn / reason) — existing callers pass no extra ⇒ payload unchanged.
|
|
54
|
+
...(ctx.extra ?? {}),
|
|
51
55
|
timestamp: new Date().toISOString(),
|
|
52
56
|
})
|
|
53
57
|
|
|
@@ -8,14 +8,20 @@ import { segmentCJK, CODE_EXTS, DOC_EXTS, SKIP_DIRS, BIG_FILE_LINES } from "./sc
|
|
|
8
8
|
export function detectLanguage(filename) {
|
|
9
9
|
const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase()
|
|
10
10
|
const map = {
|
|
11
|
-
".mjs": "javascript", ".js": "javascript", ".
|
|
11
|
+
".mjs": "javascript", ".js": "javascript", ".cjs": "javascript", ".jsx": "jsx",
|
|
12
|
+
".ts": "typescript", ".tsx": "tsx", ".mts": "typescript", ".cts": "typescript",
|
|
12
13
|
".py": "python", ".rs": "rust", ".go": "go", ".java": "java",
|
|
13
14
|
".c": "c", ".h": "c", ".cpp": "cpp", ".hpp": "cpp",
|
|
14
|
-
".rb": "ruby", ".swift": "swift", ".kt": "kotlin",
|
|
15
|
+
".rb": "ruby", ".swift": "swift", ".kt": "kotlin", ".dart": "dart", ".lua": "lua",
|
|
16
|
+
".cs": "csharp", ".fs": "fsharp", ".fsx": "fsharp",
|
|
17
|
+
".clj": "clojure", ".cljs": "clojure", ".ex": "elixir", ".exs": "elixir",
|
|
18
|
+
".erl": "erlang", ".hrl": "erlang", ".scala": "scala", ".groovy": "groovy",
|
|
19
|
+
".pl": "perl", ".pm": "perl", ".r": "r", ".jl": "julia", ".zig": "zig",
|
|
20
|
+
".ps1": "powershell", ".proto": "protobuf", ".graphql": "graphql", ".tf": "terraform", ".hcl": "hcl",
|
|
15
21
|
".sh": "bash", ".bash": "bash", ".sql": "sql",
|
|
16
22
|
".yaml": "yaml", ".yml": "yaml", ".toml": "toml", ".json": "json",
|
|
17
23
|
".css": "css", ".html": "html", ".vue": "vue", ".svelte": "svelte",
|
|
18
|
-
".md": "markdown", ".mdc": "markdown",
|
|
24
|
+
".md": "markdown", ".mdc": "markdown", ".mdx": "markdown", ".org": "org", ".wiki": "wiki", ".tex": "tex",
|
|
19
25
|
}
|
|
20
26
|
return map[ext] ?? ext.slice(1)
|
|
21
27
|
}
|