thincoder 0.12.62 → 0.12.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +53 -27
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +25 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-reindex.mjs +1 -1
  31. package/src/tui/cmd-restore.mjs +2 -2
  32. package/src/tui/cmd-session.mjs +24 -1
  33. package/src/tui/cmd-skills.mjs +1 -1
  34. package/src/tui/cmd-think.mjs +22 -9
  35. package/src/tui/config-helpers.mjs +1 -1
  36. package/src/tui/display-budget.mjs +33 -11
  37. package/src/tui/index.mjs +9 -9
  38. package/src/tui/interaction.mjs +16 -7
  39. package/src/tui/key-modes.mjs +9 -4
  40. package/src/tui/ledger-surface.mjs +26 -10
  41. package/src/tui/model-catalog.mjs +4 -4
  42. package/src/tui/model-picker.mjs +8 -7
  43. package/src/tui/mouse.mjs +11 -6
  44. package/src/tui/pickers.mjs +15 -2
  45. package/src/tui/render-conversation.mjs +1 -1
  46. package/src/tui/render-frame.mjs +10 -5
  47. package/src/tui/render-loop.mjs +1 -1
  48. package/src/tui/render-segments.mjs +3 -1
  49. package/src/tui/slash-commands.mjs +1 -1
  50. package/src/tui/startup.mjs +14 -14
  51. package/src/tui/subagent-blocks.mjs +20 -3
  52. package/src/tui/subagent-freeze.mjs +73 -2
  53. package/src/tui/suspension-drive.mjs +46 -22
  54. package/src/tui/tool-events.mjs +11 -8
  55. package/src/tui/wizard.mjs +3 -3
  56. package/src/abort-provenance.mjs +0 -116
  57. package/src/advisor/citations.mjs +0 -139
  58. package/src/advisor/compaction.mjs +0 -174
  59. package/src/advisor/convergence.mjs +0 -80
  60. package/src/advisor/history.mjs +0 -77
  61. package/src/advisor/loop.mjs +0 -293
  62. package/src/advisor/messages.mjs +0 -299
  63. package/src/advisor/project-context.mjs +0 -194
  64. package/src/advisor/repos.mjs +0 -150
  65. package/src/advisor/run.mjs +0 -293
  66. package/src/advisor/truncate.mjs +0 -57
  67. package/src/advisor.mjs +0 -290
  68. package/src/agent/completion.mjs +0 -146
  69. package/src/agent/dispatch.mjs +0 -489
  70. package/src/agent/helpers.mjs +0 -384
  71. package/src/agent/post-turn.mjs +0 -70
  72. package/src/agent/record-results.mjs +0 -174
  73. package/src/agent/relay-prefix.mjs +0 -39
  74. package/src/agent/run-stages.mjs +0 -244
  75. package/src/agent/setup-reminders.mjs +0 -69
  76. package/src/agent/setup.mjs +0 -354
  77. package/src/agent/spawn-child.mjs +0 -243
  78. package/src/agent-tools/advisor-async.mjs +0 -346
  79. package/src/agent-tools/advisor-settle.mjs +0 -231
  80. package/src/agent-tools/advisor.mjs +0 -260
  81. package/src/agent-tools/async-settle.mjs +0 -204
  82. package/src/agent-tools/batch-segment.mjs +0 -195
  83. package/src/agent-tools/consult.mjs +0 -473
  84. package/src/agent-tools/design-token.mjs +0 -117
  85. package/src/agent-tools/digest-budget.mjs +0 -76
  86. package/src/agent-tools/eng.mjs +0 -67
  87. package/src/agent-tools/escalate-async.mjs +0 -295
  88. package/src/agent-tools/goal.mjs +0 -119
  89. package/src/agent-tools/plan.mjs +0 -81
  90. package/src/agent-tools/read-history.mjs +0 -309
  91. package/src/agent-tools/recent-changes.mjs +0 -24
  92. package/src/agent-tools/review-streak.mjs +0 -93
  93. package/src/agent-tools/settings.mjs +0 -265
  94. package/src/agent-tools/skill.mjs +0 -47
  95. package/src/agent-tools/subagent-actions.mjs +0 -482
  96. package/src/agent-tools/subagent-async.mjs +0 -434
  97. package/src/agent-tools/subagent-panel.mjs +0 -160
  98. package/src/agent-tools/subagent-run.mjs +0 -205
  99. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  100. package/src/agent-tools/subagent-spawn.mjs +0 -459
  101. package/src/agent-tools/subagent.mjs +0 -404
  102. package/src/agent-tools/task.mjs +0 -87
  103. package/src/agent-tools/timer.mjs +0 -46
  104. package/src/agent-tools/verify.mjs +0 -271
  105. package/src/agent-tools.mjs +0 -17
  106. package/src/agent.mjs +0 -417
  107. package/src/auto-think.mjs +0 -115
  108. package/src/config-migrate.mjs +0 -70
  109. package/src/config.mjs +0 -496
  110. package/src/context.mjs +0 -392
  111. package/src/conventions.mjs +0 -223
  112. package/src/embedding.mjs +0 -120
  113. package/src/escape.mjs +0 -152
  114. package/src/expand-home.mjs +0 -16
  115. package/src/explore-distill.mjs +0 -155
  116. package/src/generate-title.mjs +0 -88
  117. package/src/git/checkpoint.mjs +0 -448
  118. package/src/git/gitmem.mjs +0 -100
  119. package/src/hooks.mjs +0 -97
  120. package/src/ledger.mjs +0 -227
  121. package/src/log.mjs +0 -195
  122. package/src/markdown.mjs +0 -106
  123. package/src/mcp/helpers.mjs +0 -51
  124. package/src/mcp/transport-http.mjs +0 -248
  125. package/src/mcp/transport-stdio.mjs +0 -140
  126. package/src/mcp/transport-ws.mjs +0 -122
  127. package/src/mcp.mjs +0 -295
  128. package/src/memory/code-index.mjs +0 -219
  129. package/src/memory/code-sync.mjs +0 -415
  130. package/src/memory/core.mjs +0 -299
  131. package/src/memory/delete.mjs +0 -236
  132. package/src/memory/docs.mjs +0 -419
  133. package/src/memory/file-walk.mjs +0 -109
  134. package/src/memory/scan.mjs +0 -95
  135. package/src/memory/schema.mjs +0 -452
  136. package/src/memory.mjs +0 -21
  137. package/src/model-ref.mjs +0 -66
  138. package/src/model-specs.mjs +0 -179
  139. package/src/peer-domains.mjs +0 -265
  140. package/src/peer-instances.mjs +0 -231
  141. package/src/prompt-overlays.mjs +0 -82
  142. package/src/prompts/advisor-design.md +0 -41
  143. package/src/prompts/advisor-round1.md +0 -41
  144. package/src/prompts/advisor-round2.md +0 -46
  145. package/src/prompts/advisor-round3.md +0 -42
  146. package/src/prompts/common.md +0 -115
  147. package/src/prompts/consult-base.md +0 -19
  148. package/src/prompts/discipline-engineering.md +0 -258
  149. package/src/prompts/discipline-normal.md +0 -185
  150. package/src/prompts/persona-coder.md +0 -21
  151. package/src/prompts/persona-eng-coder.md +0 -37
  152. package/src/prompts/persona-eng-designer.md +0 -60
  153. package/src/prompts/persona-engineering.md +0 -55
  154. package/src/prompts/persona-explore.md +0 -15
  155. package/src/prompts/persona-normal.md +0 -27
  156. package/src/prompts/persona-plan.md +0 -26
  157. package/src/provider/anthropic.mjs +0 -225
  158. package/src/provider/core.mjs +0 -476
  159. package/src/provider/errors.mjs +0 -101
  160. package/src/provider/google.mjs +0 -257
  161. package/src/provider/index.mjs +0 -7
  162. package/src/provider/list-models.mjs +0 -93
  163. package/src/provider/normalize.mjs +0 -81
  164. package/src/provider/rate.mjs +0 -108
  165. package/src/provider/responses.mjs +0 -495
  166. package/src/provider/retry.mjs +0 -88
  167. package/src/provider/sse.mjs +0 -264
  168. package/src/proxy.mjs +0 -261
  169. package/src/rules.mjs +0 -53
  170. package/src/session-gc.mjs +0 -221
  171. package/src/session-guard.mjs +0 -59
  172. package/src/session-migrate.mjs +0 -48
  173. package/src/session-rename.mjs +0 -38
  174. package/src/session-segments.mjs +0 -100
  175. package/src/session-slots.mjs +0 -492
  176. package/src/session-store.mjs +0 -441
  177. package/src/session.mjs +0 -492
  178. package/src/skills.mjs +0 -153
  179. package/src/text-budget.mjs +0 -46
  180. package/src/token-ttl.mjs +0 -274
  181. package/src/tools/apply_patch.md +0 -15
  182. package/src/tools/bash.md +0 -37
  183. package/src/tools/bash.mjs +0 -268
  184. package/src/tools/checklist-sync.mjs +0 -181
  185. package/src/tools/checklist.md +0 -13
  186. package/src/tools/checklist.mjs +0 -299
  187. package/src/tools/delete.md +0 -13
  188. package/src/tools/edit-batch.mjs +0 -191
  189. package/src/tools/edit-diff.mjs +0 -348
  190. package/src/tools/edit.md +0 -30
  191. package/src/tools/execute.md +0 -21
  192. package/src/tools/execute.mjs +0 -228
  193. package/src/tools/fetch.md +0 -12
  194. package/src/tools/file.mjs +0 -469
  195. package/src/tools/file_ops.md +0 -17
  196. package/src/tools/get_current_time.md +0 -8
  197. package/src/tools/git-checkpoint.mjs +0 -143
  198. package/src/tools/git-ext.mjs +0 -173
  199. package/src/tools/git.md +0 -54
  200. package/src/tools/git.mjs +0 -356
  201. package/src/tools/glob-dialect.mjs +0 -130
  202. package/src/tools/glob.md +0 -11
  203. package/src/tools/grep.md +0 -19
  204. package/src/tools/hashline_edit.md +0 -14
  205. package/src/tools/index.mjs +0 -36
  206. package/src/tools/insert_after.md +0 -15
  207. package/src/tools/lint.md +0 -10
  208. package/src/tools/linter.mjs +0 -128
  209. package/src/tools/ls.md +0 -12
  210. package/src/tools/lsp.md +0 -10
  211. package/src/tools/lsp.mjs +0 -316
  212. package/src/tools/ops.mjs +0 -299
  213. package/src/tools/patch.mjs +0 -282
  214. package/src/tools/process.md +0 -10
  215. package/src/tools/question.md +0 -16
  216. package/src/tools/question.mjs +0 -26
  217. package/src/tools/read.md +0 -20
  218. package/src/tools/read_image.md +0 -8
  219. package/src/tools/repomap.mjs +0 -314
  220. package/src/tools/search.mjs +0 -236
  221. package/src/tools/shared.mjs +0 -446
  222. package/src/tools/tree.md +0 -14
  223. package/src/tools/tree.mjs +0 -66
  224. package/src/tools/wait_for.md +0 -22
  225. package/src/tools/web.mjs +0 -224
  226. package/src/tools/websearch.md +0 -16
  227. package/src/tools/write.md +0 -11
  228. package/src/traces/trace-store.mjs +0 -355
package/src/ledger.mjs DELETED
@@ -1,227 +0,0 @@
1
- /**
2
- * ledger.mjs — 台账(需求池 / 技术待办)单源读取面(LEDGER-SURFACE 批——设计档 §2.30.3)。
3
- *
4
- * 数字单源(F7):解析(`scanGroups`)/ 计数 / 老化 / 阈值一处实现——机检器
5
- * (`scripts/check-ledger.mjs`)与显示面(CLI TUI `src/tui/ledger-surface.mjs`)共用本口径;
6
- * VSC 端为独立实现、语义同源(不跨仓 import)。
7
- * 口径(需求档 §1.18):计数 = `##` 组内 `- [ ]` 未决条目;老化 = 技术组无 `触发=` 且行龄
8
- * > 30 天(行龄未知不计);阈值 = 池 ≥ 3 或任一板块 ≥ 2;可动作 = 老化 > 0 或阈值达成。
9
- * 行文本逐字契约 = 设计档 §2.30.3.3(本模块只产出文本,色 / 态归各端显示面)。
10
- */
11
- import { mkdirSync, opendirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs"
12
- import { basename, dirname, join, resolve } from "node:path"
13
- import { execFileSync } from "node:child_process"
14
- import { configDir } from "./config.mjs"
15
-
16
- /** 老化阈值(天——口径 = 需求档 §1.18;`days` 参数可覆盖)。 */
17
- export const AGING_DAYS = 30
18
- /** 「可开批」阈值:同一板块(池条目指针所指需求档文档名)未决 ≥ 2。 */
19
- export const THRESHOLD_BOARD = 2
20
- /** 「可开批」阈值:需求池未决 ≥ 3。 */
21
- export const THRESHOLD_POOL = 3
22
- /** 刷新周期(N2 实现常量;VSC 端另有换项目事件触发)。 */
23
- export const REFRESH_MS = 120000
24
- /** 变化行去重档(跨会话、跨端共享——需求档 §1.18 F5;configDir 先例 = crash-reports 同区)。 */
25
- export const NOTIFY_FILE = join(configDir, "ledger-notify.json")
26
- /** 空族提示(仅命令面——运行时面静默,N1 / 设计档 §2.30.3.3)。 */
27
- export const EMPTY_FAMILY_LINE = "台账:未发现台账(docs/TODO.md)。"
28
-
29
- const LEDGER_REL = join("docs", "TODO.md")
30
- const H2_RE = /^##\s+(.*)$/
31
- const DECL_RE = /((\d+)\s*条)/
32
- const OPEN_ENTRY_RE = /^- \[ \]\s+/
33
- const TRIGGER_RE = /触发\s*=\s*([^·\n]*)/
34
- const BOARD_REF_RE = /requirements\/([A-Za-z0-9_.-]+\.md)/
35
-
36
- /** `##` 组扫描(与机检器 L2/L3 同源——解析唯一实现):[{name,line,declared,entries:[{line,text}]}]。 */
37
- export function scanGroups(lines) {
38
- const groups = []
39
- lines.forEach((l, i) => {
40
- const h = H2_RE.exec(l)
41
- if (h) groups.push({ name: h[1].trim(), line: i + 1, declared: Number((DECL_RE.exec(h[1]) ?? [])[1] ?? NaN), entries: [] })
42
- else if (groups.length && OPEN_ENTRY_RE.test(l)) groups[groups.length - 1].entries.push({ line: i + 1, text: l })
43
- })
44
- return groups
45
- }
46
-
47
- /** 条目归一化文本 = 条目键(去 `- [ ] ` 前缀 + 去首尾空白 + 连续空白折叠单空格)。
48
- * 位置无关(位移 / 他条编辑稳定);自身文本变更 = 键变(最坏一次重报)。§2.30.3.2。 */
49
- export function normalizeEntry(text) {
50
- return text.replace(/^- \[[ x]\]\s+/, "").trim().replace(/\s+/g, " ")
51
- }
52
-
53
- /** 条目标题:首个 `**…**` 段;无粗体段 → 归一化文本前 20 字(超出加 `…`)。§2.30.3.3 L3。 */
54
- export function entryTitle(text) {
55
- const m = /\*\*(.+?)\*\*/.exec(text)
56
- if (m) return m[1]
57
- const norm = normalizeEntry(text)
58
- return norm.length > 20 ? norm.slice(0, 20) + "…" : norm
59
- }
60
-
61
- const isFile = (p) => { try { return statSync(p).isFile() } catch { return false } }
62
-
63
- /** 向上(含自身)最近的含台账目录 → {root, ledger} / null(F4——CLI 锚 = cwd)。 */
64
- export function findProject(anchor) {
65
- let dir = resolve(anchor)
66
- for (;;) {
67
- const ledger = join(dir, LEDGER_REL)
68
- if (isFile(ledger)) return { root: dir, ledger }
69
- const parent = dirname(dir)
70
- if (parent === dir) return null
71
- dir = parent
72
- }
73
- }
74
-
75
- /** 同级枚举上限(N2 成本有界:父目录超此规模 = 缓存 / 临时等非仓族形态 → 退化空集,只显当前项目)。 */
76
- export const MAX_SIBLING_SCAN = 100
77
-
78
- /** 目录下含台账的直接子目录(目录名升序;不可读 → [];**惰性枚举 + 上限**——大目录零全量扫描)。 */
79
- function ledgerChildren(dir) {
80
- const out = []
81
- let handle
82
- try { handle = opendirSync(dir) } catch { return out }
83
- try {
84
- let n = 0
85
- for (;;) {
86
- const ent = handle.readSync()
87
- if (!ent) break
88
- if (++n > MAX_SIBLING_SCAN) return []
89
- if (!ent.isDirectory()) continue
90
- const root = join(dir, ent.name)
91
- const ledger = join(root, LEDGER_REL)
92
- if (isFile(ledger)) out.push({ root, ledger })
93
- }
94
- } catch { /* 读中断 → 已得子集(尽力而为) */ }
95
- finally { try { handle.closeSync() } catch { /* 已关 */ } }
96
- return out.sort((a, b) => (a.root < b.root ? -1 : a.root > b.root ? 1 : 0))
97
- }
98
-
99
- /** 项目族发现:{current, projects}——projects = current + 其同级含台账目录(**发现序:current 在前**,
100
- * 其余按目录名升序);current 缺(容器目录,K6)→ 上下文目录向上最近「含台账子目录」者取其子目录。 */
101
- export function discoverFamily(anchor) {
102
- const current = findProject(anchor)
103
- if (current) {
104
- const siblings = ledgerChildren(dirname(current.root)).filter((p) => p.root !== current.root)
105
- return { current, projects: [current, ...siblings] }
106
- }
107
- let dir = resolve(anchor)
108
- for (;;) {
109
- const kids = ledgerChildren(dir)
110
- if (kids.length) return { current: null, projects: kids }
111
- const parent = dirname(dir)
112
- if (parent === dir) return { current: null, projects: [] }
113
- dir = parent
114
- }
115
- }
116
-
117
- /** 行龄 Map:全档一次 `git blame --porcelain` → Map<行号, 天>;非 git / 不可判定 → null(零假阳降级)。 */
118
- export function blameAges(abs) {
119
- let out
120
- try {
121
- out = execFileSync("git", ["blame", "--porcelain", "--", abs], {
122
- cwd: dirname(abs), encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 32 * 1024 * 1024,
123
- })
124
- } catch { return null }
125
- const times = new Map(), ages = new Map()
126
- const now = Date.now() / 1000
127
- let sha = null, line = 0
128
- for (const l of out.split("\n")) {
129
- const h = /^([0-9a-f^]{7,40})\s+\d+\s+(\d+)(?:\s+\d+)?$/.exec(l)
130
- if (h) { sha = h[1]; line = Number(h[2]); continue }
131
- if (l.startsWith("author-time ")) { times.set(sha, Number(l.slice(12))); continue }
132
- if (l.startsWith("\t")) {
133
- const t = times.get(sha)
134
- ages.set(line, t != null ? (now - t) / 86400 : null)
135
- }
136
- }
137
- return ages
138
- }
139
-
140
- /** 单项目汇总 → {name,root,ledger,pool,tech,aged,agedKeys,agedTitles,thresholdReached,boards,actionable}。
141
- * `ageOf(abs)` → Map<行号,天>|null(注入面——确定性用例);**无候选条目不调 ageOf**(N2)。 */
142
- export function summarizeLedger(project, { days = AGING_DAYS, ageOf = blameAges } = {}) {
143
- const groups = scanGroups(readFileSync(project.ledger, "utf8").split("\n"))
144
- const poolEntries = groups.filter((g) => g.name.includes("需求池")).flatMap((g) => g.entries)
145
- const techEntries = groups.filter((g) => g.name.includes("技术")).flatMap((g) => g.entries)
146
- const candidates = techEntries.filter((e) => !TRIGGER_RE.test(e.text))
147
- const ages = candidates.length ? ageOf(project.ledger) : null
148
- const aged = candidates.filter((e) => { const a = ages?.get(e.line); return a != null && a > days })
149
- const boards = new Map()
150
- for (const e of poolEntries) {
151
- const m = BOARD_REF_RE.exec(e.text)
152
- if (m) boards.set(m[1], (boards.get(m[1]) ?? 0) + 1)
153
- }
154
- const thresholdReached = poolEntries.length >= THRESHOLD_POOL || [...boards.values()].some((n) => n >= THRESHOLD_BOARD)
155
- return {
156
- name: basename(project.root), root: project.root, ledger: project.ledger,
157
- pool: poolEntries.length, tech: techEntries.length, aged: aged.length,
158
- agedKeys: aged.map((e) => normalizeEntry(e.text)), agedTitles: aged.map((e) => entryTitle(e.text)),
159
- thresholdReached, boards, actionable: aged.length > 0 || thresholdReached,
160
- }
161
- }
162
-
163
- /** L1 状态标记(逐字——§2.30.3.3)。 */
164
- export function formatMarker(scan) {
165
- return scan ? `台账 ${scan.pool}·${scan.tech}` : null
166
- }
167
-
168
- /** L2 明细行(逐字——`(老化 <n>)` 恒显;阈值达成加 ` — 可开批`)。 */
169
- export function formatDetailLine(scan) {
170
- return `台账 ${scan.name}:需求池 ${scan.pool} · 技术待办 ${scan.tech}(老化 ${scan.aged})${scan.thresholdReached ? " — 可开批" : ""}`
171
- }
172
-
173
- /** L3 变化行·老化(titles = 新增老化条目标题;> 3 条时第三项后接 `;…`)。 */
174
- export function formatAgingLine(scan, titles) {
175
- return `台账变化:${scan.name} 老化首次越线 ${titles.length} 条(超 30 天未处置):${titles.slice(0, 3).join(";")}${titles.length > 3 ? ";…" : ""}`
176
- }
177
-
178
- /** L4 变化行·阈值。 */
179
- export function formatThresholdLine(scan) {
180
- return `台账变化:${scan.name} 需求池达阈值(${scan.pool} 条)— 可开批`
181
- }
182
-
183
- /** 明细行集 = {current} ∪ {可动作项目}(发现序——current 在前,其余按目录名升序;同项去重)。 */
184
- export function detailScans(scans, current) {
185
- const out = []
186
- if (current) out.push(current)
187
- for (const s of scans) if (s !== current && s.actionable) out.push(s)
188
- return out
189
- }
190
-
191
- /** 变化行规划(纯函数):{lines:[{text,warn}], next:{aged,threshold}}(去重口径 §2.30.3.2)。 */
192
- export function planChangeLines(prev, scan) {
193
- const p = prev && typeof prev === "object" ? prev : {}
194
- const before = new Set(Array.isArray(p.aged) ? p.aged : [])
195
- const freshTitles = scan.agedKeys.map((k, i) => (before.has(k) ? null : scan.agedTitles[i])).filter((t) => t != null)
196
- const lines = []
197
- if (freshTitles.length) lines.push({ text: formatAgingLine(scan, freshTitles), warn: true })
198
- if (scan.thresholdReached && !p.threshold) lines.push({ text: formatThresholdLine(scan), warn: true })
199
- return { lines, next: { aged: scan.agedKeys, threshold: scan.thresholdReached } }
200
- }
201
-
202
- /** 去重档键 = 台账绝对路径·正斜杠 + **盘符大写**(跨端同规则——CLI 的 `process.cwd()` 盘符大写、
203
- * VSC 的 `uri.fsPath` 小写(`session-slots.mjs` `normalizeCwd` 同一契约——session / checkpoint /
204
- * trace 跨端共享即赖此);CLI 写的键 VSC 须逐字认得)。 */
205
- export function notifyKey(ledger) {
206
- return resolve(ledger).replace(/\\/g, "/").replace(/^([a-z]):/, (_, d) => `${d.toUpperCase()}:`)
207
- }
208
-
209
- /** 去重档读(缺失 / 坏 JSON → 空态——N1 降级不崩)。 */
210
- export function loadNotifyState(file = NOTIFY_FILE) {
211
- try {
212
- const j = JSON.parse(readFileSync(file, "utf8"))
213
- if (j && typeof j === "object" && j.ledgers && typeof j.ledgers === "object") return { version: 1, ledgers: j.ledgers }
214
- } catch { /* 缺失 / 坏档 → 空态 */ }
215
- return { version: 1, ledgers: {} }
216
- }
217
-
218
- /** 去重档写(temp + rename 防撕裂;写失败静默返回 false——唯一写面,仅送达后调)。 */
219
- export function saveNotifyState(file, state) {
220
- try {
221
- mkdirSync(dirname(file), { recursive: true })
222
- const tmp = `${file}.tmp-${process.pid}`
223
- writeFileSync(tmp, JSON.stringify(state), "utf8")
224
- renameSync(tmp, file)
225
- return true
226
- } catch { return false }
227
- }
package/src/log.mjs DELETED
@@ -1,195 +0,0 @@
1
- /**
2
- * log.mjs — 诊断事件日志(docs/design/LOGGING.md 权威规格)。
3
- * 双端同构:thincoder/src/log.mjs 与 thincoder-vscode/src/log.mjs 同一实现语义
4
- * (共享 ~/.thincoder/logs/、同格式、同事件面——F-L6)。
5
- *
6
- * 常驻骨架日志:回合/LLM/工具/子代理/挂起的关键事件——每行一个 JSON 事件,按天轮转
7
- * (agent-YYYY-MM-DD.log),保留 1 天——用户零手动维护(F-L3/NF-L4)。问题发生时
8
- * 直接 tail/grep 定位断点(根治"临时插桩再删"循环)。
9
- *
10
- * 纪律(实现侧逐条落实):
11
- * - fire-and-forget:logEvent 失败静默降级(NF-L1)——主流程零影响。进程内首次写失败
12
- * 即置死(_dead),当日不再尝试(磁盘满/权限错不逐事件空转)。
13
- * - 单事件行 <512 字符(NF-L2):head(LLM ≤300 / 工具 ≤200)、err ≤200 由调用方截断,
14
- * 本模块对任意字符串字段做上限兜底(head 300 / err 200 / 其余 120)+ 超长时丢可选字段。
15
- * - 敏感字段零落盘(NF-L3/§2.4,2026-09-03 评审 refinement #7 匹配语义定稿):
16
- * ① 字段名精确匹配黑名单(apiKey/designToken/password/secret/token,大小写不敏感;
17
- * 另加防御性名称 authorization/proxy/proxyUri——防凭据型 URL/头字段泄漏——§2.5)
18
- * → 整个字段丢弃;
19
- * ② 内容只扫密钥形态(sk-xxx / Bearer xxx / key=… 等)→ 截断到形态之前
20
- * ——宁可丢信息不漏密钥。
21
- * 工具事件不记 args;URL 不入事件(llm/tool 事件从不携带 URL;err 文本经形态扫描)。
22
- * - 摘要截断(B 方案,2026-09-03 用户裁定):截断处带 "…" 标记(截后仍 ≤上限)。
23
- * - 测试隔离:node --test 进程(NODE_TEST_CONTEXT)默认不写盘——防测试事件污染真实
24
- * 诊断日志(两端测试套件都会跑真实 agent 管线);显式设置 THINCODER_LOG_DIR 强制
25
- * 写入该目录(log.test.mjs 用它隔离临时目录——refinement #6)。
26
- * - 轮转清理(refinement #3——长驻进程覆盖):非仅启动时——每进程每日**首次写事件**
27
- * 时顺带清理 >1 天的 agent-*.log(extension host 可长驻数月,启动清理覆盖不到)。
28
- * - seq:每进程单调计数器。双端同写一个文件时 seq 会各自重复——定位同文件时序以
29
- * ts 为准,seq 仅进程内参照(refinement #8)。
30
- */
31
-
32
- import { appendFileSync, mkdirSync, readdirSync, rmSync, existsSync, statSync } from "node:fs"
33
- import { homedir } from "node:os"
34
- import { join } from "node:path"
35
-
36
- /** 单行长度硬上限(NF-L2) */
37
- export const MAX_LINE = 512
38
- /** 字段名精确黑名单(NF-L3/§2.4——2026-09-03 评审 refinement #7:与 §2.4 词表一致含 token) */
39
- const BLACKLIST_FIELDS = new Set(["apikey", "designtoken", "password", "secret", "token", "authorization", "proxyuri", "proxy"])
40
- /** 内容密钥形态扫描(大小写不敏感)——命中即截断到形态前 */
41
- const SECRET_FORM = /(\bsk-[A-Za-z0-9_-]{6,}|\bBearer\s+[A-Za-z0-9._~+/=-]{6,}|\b(?:api[_-]?key|key|token|secret|password|pwd|passwd)\s*[=:]\s*['"]?[A-Za-z0-9._~+/=-]{6,})/i
42
- /** 字符串字段长度兜底:head ≤300 / err ≤200 / 其余 ≤120 */
43
- const FIELD_CAPS = { head: 300, err: 200 }
44
-
45
- let _seq = 0
46
- let _dead = false // 进程内写失败即死(NF-L1 静默降级——不逐事件重复空转)
47
- let _cleanupDate = null // 本进程已执行过清理的日期(每日首次写时清一次)
48
-
49
- /** 日志目录:THINCODER_LOG_DIR(测试隔离/override)> ~/.thincoder/logs(与 sessions/ 同域) */
50
- export function logsDir() {
51
- return process.env.THINCODER_LOG_DIR ?? join(homedir(), ".thincoder", "logs")
52
- }
53
-
54
- /** 今日日志文件路径(agent-YYYY-MM-DD.log) */
55
- export function todayLogPath(now = new Date()) {
56
- const ymd = now.toISOString().slice(0, 10)
57
- return join(logsDir(), `agent-${ymd}.log`)
58
- }
59
-
60
- /** 写门(测试隔离):test runner 进程(NODE_TEST_CONTEXT)默认跳过——除显式
61
- * THINCODER_LOG_DIR override(log.test.mjs 隔离临时目录——refinement #6)。 */
62
- function writeEnabled() {
63
- if (_dead) return false
64
- if (process.env.NODE_TEST_CONTEXT && !process.env.THINCODER_LOG_DIR) return false
65
- return true
66
- }
67
-
68
- /** 清理 >1 天的 agent-*.log(NF-L4——refinement #3:机会式,非仅启动时)。
69
- * 保留窗口(2026-09-03 code review #1 修正):以文件名日期的**当日结束**为龄基准——
70
- * 今天+昨天的文件保留(任何事件至少留存 24h、最多 48h),早于昨天的删除。
71
- * 无法按名解析日期的 agent-*.log 按 mtime 兜底(>1 天未写的删除)。
72
- * 导出供测试直呼(T-L7/T-L7b)。静默:任何失败都不影响主流程。 */
73
- export const LOG_RETENTION_MS = 24 * 60 * 60 * 1000
74
- export function cleanupOldLogs(now = new Date()) {
75
- const dir = logsDir()
76
- if (!existsSync(dir)) return
77
- let names
78
- try {
79
- names = readdirSync(dir)
80
- } catch {
81
- return
82
- }
83
- for (const name of names) {
84
- if (!name.startsWith("agent-") || !name.endsWith(".log")) continue
85
- const m = name.match(/^agent-(\d{4}-\d{2}-\d{2})\.log$/)
86
- const ts = m ? Date.parse(m[1] + "T00:00:00Z") : NaN
87
- // 文件名日期 → 龄以该日结束(ts+24h)计:事件在删除时至少已留存 24h
88
- const ageMs = Number.isFinite(ts) ? now - (ts + LOG_RETENTION_MS) : (tryStatAge(dir, name, now) ?? Infinity)
89
- if (ageMs > LOG_RETENTION_MS) {
90
- try { rmSync(join(dir, name), { force: true }) } catch { /* 静默 */ }
91
- }
92
- }
93
- }
94
-
95
- function tryStatAge(dir, name, now) {
96
- try {
97
- return now - statSync(join(dir, name)).mtimeMs
98
- } catch {
99
- return null
100
- }
101
- }
102
-
103
- /**
104
- * 写入一条事件(fire-and-forget)。fields 内所有字符串都经黑名单/截断处理。
105
- * 事件行结构:{"ts":ISO,"ev":kind,"seq":N,...fields}——单行 JSON <512 字符。
106
- */
107
- export function logEvent(kind, fields = {}) {
108
- if (!writeEnabled()) return
109
- const now = new Date()
110
- // 机会式轮转清理:每进程每日首次写事件时执行一次(refinement #3——长驻进程覆盖)
111
- const ymd = now.toISOString().slice(0, 10)
112
- if (_cleanupDate !== ymd) {
113
- _cleanupDate = ymd
114
- cleanupOldLogs(now)
115
- }
116
- const entry = { ts: now.toISOString(), ev: kind, seq: ++_seq }
117
- for (const [k, v] of Object.entries(fields ?? {})) {
118
- if (v === undefined || v === null) continue
119
- // ① 字段名精确黑名单 → 丢弃(token 族永不落盘)
120
- if (BLACKLIST_FIELDS.has(String(k).toLowerCase())) continue
121
- if (typeof v === "string") entry[k] = sanitizeString(k, v)
122
- else if (typeof v === "number" || typeof v === "boolean") entry[k] = v
123
- }
124
- let line = JSON.stringify(entry)
125
- // ② 防御性收尾(NF-L2):字段上限后仍超长 → 丢可选字段(保留 ts/ev/seq)直至 <512
126
- while (line.length > MAX_LINE && Object.keys(entry).length > 3) {
127
- for (const k of Object.keys(entry)) {
128
- if (k !== "ts" && k !== "ev" && k !== "seq") { delete entry[k]; break }
129
- }
130
- line = JSON.stringify(entry)
131
- }
132
- try {
133
- const dir = logsDir()
134
- mkdirSync(dir, { recursive: true })
135
- appendFileSync(join(dir, `agent-${ymd}.log`), line + "\n", "utf8")
136
- } catch {
137
- _dead = true // NF-L1:写失败静默降级——主流程零影响
138
- }
139
- }
140
-
141
- /** 共享脱敏(§18.6 D-TR2——trace-store 复用;logEvent 自身保持"丢弃字段"语义,
142
- * 本函数提供"遮蔽标记"语义——同一字段名黑名单 + 同一 SECRET_FORM 形态扫描,
143
- * 不发明新遮蔽模式)。字段名命中黑名单 → "[REDACTED](整个字段遮蔽);内容命中
144
- * 密钥形态 → 截断到形态前 + "[redacted]" 标记(宁可丢信息不漏密钥——§2.4)。 */
145
- export function redactSecret(fieldKey, value) {
146
- const s = String(value)
147
- if (BLACKLIST_FIELDS.has(String(fieldKey).toLowerCase())) return "[REDACTED]"
148
- const hit = s.match(SECRET_FORM)
149
- return hit ? s.slice(0, hit.index) + "[redacted]" : s
150
- }
151
-
152
- /** 内容级净化:密钥形态截断到形态前(②);超长截断 + "…" 标记。 */
153
- export function sanitizeString(key, value) {
154
- let s = String(value)
155
- // 密钥形态 → 截断到形态前(宁可丢信息不漏密钥——§2.4)
156
- const hit = s.match(SECRET_FORM)
157
- if (hit) s = s.slice(0, hit.index)
158
- const cap = FIELD_CAPS[key] ?? 120
159
- if (s.length > cap) s = s.slice(0, cap - 1) + "…"
160
- return s
161
- }
162
-
163
- /** 错误文本提取:message + cause 链首条、压成单行、≤max(默认 200——NF-L2/§2.2) */
164
- export function errText(err, max = 200) {
165
- let msg = err?.message ?? String(err ?? "")
166
- if (!msg && err?.cause) msg = String(err.cause)
167
- if (err?.cause?.message && !/^LLM API error/.test(msg)) msg += ` (${err.cause.message})`
168
- msg = msg.replace(/\s*\n\s*/g, " ").trim()
169
- if (msg.length > max) msg = msg.slice(0, max - 1) + "…"
170
- return msg
171
- }
172
-
173
- /** 错误 kind 分类(llm:error 字段):timeout(网关超时/超时信号)/ abort(用户中止)/
174
- * error(其余——API/网络/未分类)。signal 为请求中止信号(chat 调用点直传)。 */
175
- export function classifyErr(err, signal) {
176
- const msg = String(err?.message ?? "").toLowerCase() + " " + String(err?.cause?.message ?? "").toLowerCase()
177
- if (/timeout|timed ?out|etimedout|time limit exceeded/i.test(msg)) return "timeout"
178
- if (err?.name === "AbortError" || signal?.aborted) {
179
- return signal?.reason?.name === "TimeoutError" ? "timeout" : "abort"
180
- }
181
- return "error"
182
- }
183
-
184
- /** 文本头截取:前 max 字符、单行化(JSON 单行约束)、截断带 "…" 标记。
185
- * paragraph=true(llm:done 响应头):取首段(首个空行前)——B 方案"首段文本"语义。 */
186
- export function headText(text, max, { paragraph = false } = {}) {
187
- let s = String(text ?? "")
188
- if (paragraph) {
189
- const p = s.split(/\n\s*\n/, 1)[0]
190
- if (p.length < s.length) s = p
191
- }
192
- s = s.replace(/\s*\n+\s*/g, " ").trim()
193
- if (s.length > max) s = s.slice(0, max - 1) + "…"
194
- return s
195
- }
package/src/markdown.mjs DELETED
@@ -1,106 +0,0 @@
1
- /**
2
- * markdown.mjs — markdown + frontmatter format for memory entries
3
- * Zero-dependency parsing/serialization. Entry format see ARCHITECTURE-v2.md.
4
- */
5
-
6
- const VALID_TYPES = new Set(["rule", "knowledge", "decision", "pattern"])
7
-
8
- /**
9
- * Parse a markdown entry.
10
- * → { meta: { type, title, tags, author, created, embedding? }, content }
11
- * Throws if frontmatter is missing or required fields absent.
12
- */
13
- export function parseEntry(text) {
14
- const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
15
- if (!match) throw new Error("entry missing frontmatter (expected --- ... ---)")
16
-
17
- const meta = parseFrontmatter(match[1])
18
- const content = match[2].trim()
19
-
20
- if (!VALID_TYPES.has(meta.type)) {
21
- throw new Error(`invalid type "${meta.type}"; expected one of: ${[...VALID_TYPES].join(", ")}`)
22
- }
23
- if (!meta.title) throw new Error("frontmatter missing required field: title")
24
-
25
- return {
26
- meta: {
27
- type: meta.type,
28
- title: meta.title,
29
- tags: Array.isArray(meta.tags) ? meta.tags : meta.tags ? [meta.tags] : [],
30
- author: meta.author ?? "unknown",
31
- created: meta.created ?? "",
32
- ...(meta.embedding ? { embedding: meta.embedding } : {}),
33
- },
34
- content,
35
- }
36
- }
37
-
38
- /**
39
- * Serialize to markdown entry text.
40
- */
41
- export function serializeEntry(meta, content) {
42
- if (!VALID_TYPES.has(meta.type)) throw new Error(`invalid type "${meta.type}"`)
43
- if (!meta.title) throw new Error("meta.title is required")
44
- // frontmatter scalars must be single-line: newlines in title/author would inject fake frontmatter rows
45
- // (e.g. title "x\ntype: rule" would override real type when parsed), same for tags with newlines/commas
46
- const tags = (meta.tags ?? []).map((t) => oneLine(t).replaceAll(",", " ")).join(", ")
47
- const lines = [
48
- "---",
49
- `type: ${meta.type}`,
50
- `title: ${oneLine(meta.title)}`,
51
- `tags: [${tags}]`,
52
- `author: ${oneLine(meta.author ?? "unknown")}`,
53
- `created: ${oneLine(meta.created ?? new Date().toISOString().slice(0, 10))}`,
54
- ]
55
- if (meta.embedding) lines.push(`embedding: ${meta.embedding}`)
56
- lines.push("---", "", content.trim(), "")
57
- return lines.join("\n")
58
- }
59
-
60
- /** Convert title to filename slug: keep alphanumeric + CJK, convert rest to hyphens */
61
- export function slugify(title) {
62
- return title
63
- .trim()
64
- .toLowerCase()
65
- .replace(/[^\w一-鿿]+/g, "-")
66
- .replace(/^-+|-+$/g, "")
67
- .slice(0, 50) || "untitled"
68
- }
69
-
70
- /** Generate entry filename: YYYYMMDD-<slug>-<rand4>.md */
71
- export function entryFilename(title, date = new Date()) {
72
- const ymd = date.toISOString().slice(0, 10).replaceAll("-", "")
73
- const rand = Math.random().toString(36).slice(2, 6)
74
- return `${ymd}-${slugify(title)}-${rand}.md`
75
- }
76
-
77
- // ---------------------------------------------------------------- internal
78
-
79
- /** Collapse to single line (for frontmatter scalars): fold newlines into spaces, prevent injecting fake field lines */
80
- function oneLine(v) {
81
- return String(v).replace(/\s*\r?\n\s*/g, " ").trim()
82
- }
83
-
84
- /**
85
- * Minimal YAML subset parser: only supports `key: value` and `key: [a, b, c]`.
86
- * Our frontmatter is self-generated, no need for full YAML.
87
- */
88
- export function parseFrontmatter(text) {
89
- const meta = {}
90
- for (const line of text.split(/\r?\n/)) {
91
- const m = line.match(/^(\w[\w-]*)\s*:\s*(.*)$/)
92
- if (!m) continue
93
- const [, key, raw] = m
94
- const value = raw.trim()
95
- if (value.startsWith("[") && value.endsWith("]")) {
96
- meta[key] = value
97
- .slice(1, -1)
98
- .split(",")
99
- .map((s) => s.trim())
100
- .filter(Boolean)
101
- } else {
102
- meta[key] = value
103
- }
104
- }
105
- return meta
106
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * mcp/helpers.mjs — MCP shared utility functions and constants
3
- */
4
-
5
- export const INIT_TIMEOUT_MS = 30_000
6
- export const CALL_TIMEOUT_MS = 120_000
7
- export const ENDPOINT_WAIT_MS = 5_000
8
-
9
- let nextRpcId = 0
10
- /** Generate a unique incrementing RPC ID string */
11
- export function rpcId() {
12
- return String(++nextRpcId)
13
- }
14
-
15
- /** Race a promise against a timeout, rejecting after ms milliseconds */
16
- export function withTimeout(promise, ms) {
17
- let timer
18
- const timeout = new Promise((_, reject) => {
19
- timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms)
20
- timer.unref?.()
21
- })
22
- return Promise.race([promise.finally(() => clearTimeout(timer)), timeout])
23
- }
24
-
25
- /** Quote a shell argument: wrap in double-quotes if it contains whitespace or quotes */
26
- export function quoteArg(s) {
27
- return /[\s"]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
28
- }
29
-
30
- /** Convert an Authorization Bearer token into a WebSocket subprotocol.
31
- * 2026-08-31 MCP 会诊 #10:原实现把 token 塞进 URL query——代理/网关日志会泄露凭证,
32
- * 且无标准依据。Node 内置 WebSocket(undici)无法自定义请求头,MCP 生态的标准替代
33
- * 通道是 subprotocol(`bearer.<token>`)。用户 URL 自带的 query token 不动(兼容)。
34
- * @returns {{ url: string, protocols: string[] }} — protocols 为空数组表示无认证。 */
35
- export function withAuthToken(wsUrl, authorization) {
36
- if (!authorization) return { url: wsUrl, protocols: [] }
37
- const token = authorization.replace(/^Bearer\s+/i, "")
38
- let u
39
- try {
40
- u = new URL(wsUrl)
41
- } catch {
42
- throw new Error(`Invalid WebSocket URL: ${String(wsUrl).slice(0, 120)}`)
43
- }
44
- // subprotocol 不进入 URL/日志,token 不再注入 query
45
- return { url: u.href, protocols: [`bearer.${token}`] }
46
- }
47
-
48
- /** Sanitize a tool name: replace non-alphanumeric chars with underscores, cap at 64 chars */
49
- export function sanitizeToolName(name) {
50
- return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64)
51
- }