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.
- package/CHANGELOG.md +9 -0
- package/README.md +13 -12
- package/bin/thincoder.mjs +53 -27
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -379
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +1 -1
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +1 -1
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +6 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +24 -1
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +33 -11
- package/src/tui/index.mjs +9 -9
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +26 -10
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +10 -5
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +14 -14
- package/src/tui/subagent-blocks.mjs +20 -3
- package/src/tui/subagent-freeze.mjs +73 -2
- package/src/tui/suspension-drive.mjs +46 -22
- package/src/tui/tool-events.mjs +11 -8
- package/src/tui/wizard.mjs +3 -3
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -244
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -243
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -204
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -473
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -295
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -309
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -482
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -459
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -417
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -392
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -88
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/ledger.mjs +0 -227
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -415
- package/src/memory/core.mjs +0 -299
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -419
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/scan.mjs +0 -95
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -258
- package/src/prompts/discipline-normal.md +0 -185
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -60
- package/src/prompts/persona-engineering.md +0 -55
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -221
- package/src/session-guard.mjs +0 -59
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-segments.mjs +0 -100
- package/src/session-slots.mjs +0 -492
- package/src/session-store.mjs +0 -441
- package/src/session.mjs +0 -492
- package/src/skills.mjs +0 -153
- package/src/text-budget.mjs +0 -46
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- 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
|
-
}
|
package/src/mcp/helpers.mjs
DELETED
|
@@ -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
|
-
}
|