thincoder 0.12.61 → 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 (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  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 -371
  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 +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  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-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
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
- }
@@ -1,248 +0,0 @@
1
- /**
2
- * mcp/transport-http.mjs — MCP HTTP + SSE transport (Streamable HTTP)
3
- */
4
- import { rpcId, CALL_TIMEOUT_MS, ENDPOINT_WAIT_MS, INIT_TIMEOUT_MS, withTimeout } from "./helpers.mjs"
5
-
6
- /** Create an MCP HTTP+SSE transport for Streamable HTTP servers */
7
- export function httpTransport(baseURL, extraHeaders = {}) {
8
- const url = baseURL.replace(/\/+$/, "")
9
- let sessionId = null
10
- let closed = false
11
- let eventSource = null
12
- // 2026-09-01 MCP.md §4 D-1:GET SSE 不可用(405/不支持)降级后的纯 POST 模式标记。
13
- // POST-only server(如 glm-websearch)无流可断:isAlive 不得因 eventSource == null 判死。
14
- let postOnly = false
15
- let abortController = null
16
- let postUrl = url
17
- let legacySSE = false
18
- let deadFired = false
19
- let deadListeners = new Set()
20
-
21
- /** 2026-08-31 MCP 会诊 P5:意外死亡通知(SSE 流断/error,非主动 close)。 */
22
- const fireDead = (msg) => {
23
- if (deadFired) return
24
- deadFired = true
25
- for (const cb of deadListeners) { try { cb(msg) } catch { /* listener error */ } }
26
- }
27
- const onDead = (cb) => { deadListeners.add(cb); return () => deadListeners.delete(cb) }
28
-
29
- const headers = () => {
30
- const h = { "Content-Type": "application/json", Accept: "text/event-stream, application/json", ...extraHeaders }
31
- if (sessionId) h["Mcp-Session-Id"] = sessionId
32
- return h
33
- }
34
-
35
- const pending = new Map()
36
-
37
- async function* parseSSE(response) {
38
- const reader = response.body.getReader()
39
- const decoder = new TextDecoder()
40
- let buf = ""
41
- let current = { data: "", event: "message" }
42
- try {
43
- while (true) {
44
- const { done, value } = await reader.read()
45
- if (done) break
46
- buf += decoder.decode(value, { stream: true })
47
- const lines = buf.split("\n")
48
- buf = lines.pop() ?? ""
49
- for (const raw of lines) {
50
- const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw
51
- if (line === "") {
52
- if (current.data) {
53
- yield { event: current.event, data: current.data.trimEnd() }
54
- current = { data: "", event: "message" }
55
- }
56
- } else if (line.startsWith("data:")) {
57
- current.data += (current.data ? "\n" : "") + line.slice(5).replace(/^ /, "")
58
- } else if (line.startsWith("event:")) {
59
- current.event = line.slice(6).trim()
60
- }
61
- }
62
- }
63
- } finally {
64
- reader.releaseLock()
65
- }
66
- }
67
-
68
- async function openSSE() {
69
- if (closed) return
70
- abortController?.abort()
71
- abortController = new AbortController()
72
- const signal = AbortSignal.any([abortController.signal, AbortSignal.timeout(INIT_TIMEOUT_MS)])
73
- const resp = await fetch(url, {
74
- method: "GET",
75
- headers: { Accept: "text/event-stream", ...extraHeaders },
76
- signal,
77
- })
78
- if (!resp.ok) throw new Error(`SSE connect failed: HTTP ${resp.status}`)
79
- eventSource = parseSSE(resp)
80
- let endpointReady
81
- const gotEndpoint = new Promise((resolve) => { endpointReady = resolve })
82
-
83
- ;(async () => {
84
- try {
85
- for await (const { event, data } of eventSource) {
86
- if (closed) break
87
- if (event === "endpoint") {
88
- postUrl = new URL(data.trim(), url).href
89
- legacySSE = true
90
- endpointReady()
91
- continue
92
- }
93
- try {
94
- const msg = JSON.parse(data)
95
- const resolver = pending.get(msg.id)
96
- if (resolver) {
97
- pending.delete(msg.id)
98
- resolver(msg)
99
- }
100
- } catch { /* not JSON, ignore */ }
101
- }
102
- // 正常走完 = server 关闭了流(网络/对端退出)→ 非主动关闭视为死亡
103
- if (!closed) fireDead("SSE stream ended")
104
- } catch (error) {
105
- const wasClosed = closed
106
- if (!wasClosed) {
107
- for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message: `SSE error: ${error.message}` } })
108
- pending.clear()
109
- fireDead(`SSE error: ${error.message}`)
110
- }
111
- }
112
- })()
113
-
114
- const wait = new Promise((resolve) => {
115
- const t = setTimeout(resolve, ENDPOINT_WAIT_MS)
116
- t.unref?.()
117
- })
118
- await Promise.race([gotEndpoint, wait])
119
- }
120
-
121
- async function postRequest(method, params) {
122
- const id = rpcId()
123
- const body = JSON.stringify({ jsonrpc: "2.0", id, method, params })
124
-
125
- if (legacySSE) {
126
- // 2026-08-31 MCP 会诊 #10:legacy 模式下 POST 若直接带回 JSON-RPC body(违规 server,
127
- // 规范是响应经 GET SSE 流回)——原实现对 resp.ok 什么都不做 → pending 挂满 120s。
128
- // 空 body = 规范行为(等待 SSE 流);非空 JSON = 违规 server,直接解析 resolve。
129
- return new Promise((resolve) => {
130
- pending.set(id, resolve)
131
- fetch(postUrl, { method: "POST", headers: headers(), body, signal: AbortSignal.timeout(CALL_TIMEOUT_MS) })
132
- .then(async (resp) => {
133
- if (!resp.ok) {
134
- pending.delete(id)
135
- resolve({ id, error: { code: -32000, message: `POST failed: HTTP ${resp.status}` } })
136
- return
137
- }
138
- const raw = await resp.text().catch(() => "")
139
- if (raw.trim()) {
140
- try {
141
- const msg = JSON.parse(raw)
142
- if (msg.id === id) { pending.delete(id); resolve(msg); return }
143
- } catch { /* ignore — pending 交给 SSE 流 */ }
144
- }
145
- // 空体或非本次 id 的 JSON:保持 pending 等 GET SSE 流回包
146
- })
147
- .catch((e) => {
148
- pending.delete(id)
149
- resolve({ id, error: { code: -32000, message: `POST failed: ${e.message}` } })
150
- })
151
- }).finally(() => pending.delete(id))
152
- }
153
-
154
- // 2026-08-31 MCP 会诊 P4:Streamable HTTP 规范路径(POST→202→GET SSE 回包)。
155
- // 原实现非 legacy 分支从不注册 pending:202 空 body → resp.json() 抛 SyntaxError,
156
- // 而 SSE 流里 pending.get(id) 永远 miss——每次调用挂满 120s。
157
- // 现在两类通道统一:先注册 pending,POST 得到的结果(直接 body / SSE / 202 等待)
158
- // 都经 pending resolve;SSE 流由 openSSE() 转发。200+JSON 直接 body 解析。
159
- return new Promise((resolve) => {
160
- pending.set(id, resolve)
161
- fetch(postUrl, { method: "POST", headers: headers(), body, signal: AbortSignal.timeout(CALL_TIMEOUT_MS) })
162
- .then(async (resp) => {
163
- const ct = resp.headers.get("content-type") ?? ""
164
- const newSessionId = resp.headers.get("Mcp-Session-Id")
165
- if (newSessionId) sessionId = newSessionId
166
- // 202: 已接受,响应将经 GET SSE 流回——pending 保持,由 openSSE 转发 resolve
167
- if (resp.status === 202) return
168
- if (!resp.ok) {
169
- pending.delete(id)
170
- resolve({ id, error: { code: -32000, message: `HTTP ${resp.status}` } })
171
- return
172
- }
173
- if (ct.includes("text/event-stream")) {
174
- // 响应体本身就是一条 SSE(一次性流):解析匹配该 id
175
- try {
176
- for await (const { data } of parseSSE(resp)) {
177
- try {
178
- const msg = JSON.parse(data)
179
- if (msg.id === id) { pending.delete(id); resolve(msg); return }
180
- } catch { /* skip */ }
181
- }
182
- pending.delete(id)
183
- resolve({ id, error: { code: -32000, message: "No JSON-RPC response in SSE stream" } })
184
- } catch (e) {
185
- pending.delete(id)
186
- resolve({ id, error: { code: -32000, message: `SSE response failed: ${e.message}` } })
187
- }
188
- return
189
- }
190
- // 直接 JSON body:拿掉 pending 立即解析
191
- pending.delete(id)
192
- try {
193
- resolve(await resp.json())
194
- } catch (e) {
195
- resolve({ id, error: { code: -32000, message: `invalid JSON body: ${e.message}` } })
196
- }
197
- })
198
- .catch((e) => {
199
- if (pending.delete(id)) {
200
- resolve({ id, error: { code: -32000, message: `POST failed: ${e.message}` } })
201
- }
202
- })
203
- }).finally(() => pending.delete(id))
204
- }
205
-
206
- const send = async (method, params) => withTimeout(postRequest(method, params), CALL_TIMEOUT_MS)
207
-
208
- const notify = (method, params) => {
209
- fetch(postUrl, {
210
- method: "POST",
211
- headers: headers(),
212
- body: JSON.stringify({ jsonrpc: "2.0", method, params }),
213
- signal: AbortSignal.timeout(10_000),
214
- }).catch((e) => {
215
- // notify is fire-and-forget by design, but log network errors for debugging
216
- if (e.name !== "AbortError") console.error(`[mcp] notify failed: ${e.message}`)
217
- })
218
- }
219
-
220
- const close = () => {
221
- closed = true
222
- abortController?.abort()
223
- if (sessionId) {
224
- fetch(postUrl, {
225
- method: "DELETE",
226
- headers: { "Mcp-Session-Id": sessionId, ...extraHeaders },
227
- signal: AbortSignal.timeout(5_000),
228
- }).catch((e) => {
229
- // close is best-effort cleanup; log but don't throw
230
- if (e.name !== "AbortError") console.error(`[mcp] close DELETE failed: ${e.message}`)
231
- })
232
- sessionId = null
233
- }
234
- for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message: "Connection closed" } })
235
- pending.clear()
236
- }
237
-
238
- /** D-1:openSSE 降级(GET 405/不支持)后由 connect 链调用——标记纯 Streamable POST 模式 */
239
- const markPostOnly = () => { postOnly = true }
240
-
241
- return {
242
- send, notify, close, openSSE, url, headers: extraHeaders,
243
- // F1:POST-only 降级(postOnly)与 legacy SSE 流(eventSource)都算活连接——
244
- // 不得因 eventSource == null 误判死(glm-websearch "reconnect failed" 根因)。
245
- isAlive: () => !closed && (eventSource != null || postOnly),
246
- onDead, markPostOnly,
247
- }
248
- }