@thincoder/core 0.9.1
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/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* history-window.mjs — lazy human-line window (restore / render-facing):
|
|
3
|
+
* historyWindow + tool-call/result pairing + the real-user-message predicate.
|
|
4
|
+
*
|
|
5
|
+
* Pure functions — no fs, no host imports (node --test drives it directly).
|
|
6
|
+
* Window semantics follow the restore contract (turnStart / reminder-skip /
|
|
7
|
+
* reasoning), plus one frame rule: an assistant frame becomes ONE message
|
|
8
|
+
* carrying its tool cards nested (rules below).
|
|
9
|
+
*
|
|
10
|
+
* 承接:会话数据面的「人读线」窗口切分(分段恢复 / 逐页回看)——与磁盘会话档
|
|
11
|
+
* 同源(`{hash}.json.N`),本档只读数组、不触盘。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Real user message predicate (shared by the slot metadata digest and the window).
|
|
15
|
+
* NOTE: returns true for an EMPTY string content — rule 1 layers a trim() check on top.
|
|
16
|
+
* Null-safe(非对象 / 缺内容 ⇒ false——调用面含批量过滤)。 */
|
|
17
|
+
export function isRealUserMsg(m) {
|
|
18
|
+
return m?.role === "user" && typeof m?.content === "string" && !m.content.startsWith("[System reminder:")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Page size for lazy history loading (initial paint + scroll-back pages).
|
|
22
|
+
* 200 = the first-window size parity with the main line's initial window; loadOlder
|
|
23
|
+
* scroll-back pages step by the same constant. */
|
|
24
|
+
export const HISTORY_PAGE_SIZE = 200
|
|
25
|
+
|
|
26
|
+
/** Read-side timestamp compat: the real-message writer stamps `ts` (epoch-ms); older
|
|
27
|
+
* files may carry `timestamp`; even older carry neither → null. Field name on the
|
|
28
|
+
* wire stays `timestamp` (consumers zero-rename). */
|
|
29
|
+
function tsOf(m) {
|
|
30
|
+
const ts = m?.ts ?? m?.timestamp
|
|
31
|
+
return typeof ts === "number" ? ts : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function kindOf(m) {
|
|
35
|
+
return m?.type ?? m?.role
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toolCallsOf(m) {
|
|
39
|
+
if (!m || !Array.isArray(m.tool_calls)) return []
|
|
40
|
+
return m.tool_calls.filter((tc) => tc && typeof tc === "object")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function reasoningOf(m) {
|
|
44
|
+
const r = m.reasoning_content ?? m.reasoning
|
|
45
|
+
return typeof r === "string" && r.trim() !== "" ? r : null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Assistant frame text = string content only (multimodal arrays are not renderable). */
|
|
49
|
+
function textOf(m) {
|
|
50
|
+
return typeof m.content === "string" ? m.content : null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Ghost frame (rules 2/6): no visible text ∧ no tool_calls ∧ no reasoning → skip
|
|
54
|
+
* (everything else is kept — a content-null pure-tool frame renders through its cards). */
|
|
55
|
+
function isGhostAssistant(m) {
|
|
56
|
+
const text = textOf(m)
|
|
57
|
+
const hasText = text !== null && text.trim() !== ""
|
|
58
|
+
return !hasText && toolCallsOf(m).length === 0 && reasoningOf(m) === null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Rules 3/4 — full-history rolling pairing: every tool entry is matched to the most
|
|
63
|
+
* recent assistant frame (before it) that declared its tool_call_id; each tool entry
|
|
64
|
+
* is consumed at most once (parallel batches complete out of order — id matching,
|
|
65
|
+
* declaration order preserved per frame). Orphan determination is against the FULL
|
|
66
|
+
* history: a window-local "orphan" whose frame lives outside the window is still
|
|
67
|
+
* consumed here, so it never renders standalone on a second page (防跨页双显).
|
|
68
|
+
* Returns { frameResults, ownerOf }:
|
|
69
|
+
* - frameResults: frame idx → Map(callId → tool entry idx) — cards per frame
|
|
70
|
+
* - ownerOf: tool entry idx → consuming frame idx (absent = true orphan)
|
|
71
|
+
*/
|
|
72
|
+
function pairToolEntries(history) {
|
|
73
|
+
const total = history.length
|
|
74
|
+
const frameResults = new Map()
|
|
75
|
+
const ownerOf = new Map()
|
|
76
|
+
const pending = [] // unpaired declarations (declaration order) — [{ frame, callId }]
|
|
77
|
+
for (let i = 0; i < total; i++) {
|
|
78
|
+
const m = history[i]
|
|
79
|
+
if (kindOf(m) === "assistant") {
|
|
80
|
+
for (const tc of toolCallsOf(m)) {
|
|
81
|
+
if (tc.id !== undefined && tc.id !== null) pending.push({ frame: i, callId: tc.id })
|
|
82
|
+
}
|
|
83
|
+
} else if (kindOf(m) === "tool" && m.tool_call_id !== undefined && m.tool_call_id !== null) {
|
|
84
|
+
// most-recent declarer first (a normal file has at most one unpaired declaration per id)
|
|
85
|
+
for (let p = pending.length - 1; p >= 0; p--) {
|
|
86
|
+
if (String(pending[p].callId) === String(m.tool_call_id)) {
|
|
87
|
+
const { frame } = pending[p]
|
|
88
|
+
pending.splice(p, 1)
|
|
89
|
+
let map = frameResults.get(frame)
|
|
90
|
+
if (!map) { map = new Map(); frameResults.set(frame, map) }
|
|
91
|
+
map.set(String(m.tool_call_id), i)
|
|
92
|
+
ownerOf.set(i, frame)
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { frameResults, ownerOf }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Window into the human line for lazy history loading (rules 1-6). `before` = null
|
|
103
|
+
* takes the LAST page (first paint); otherwise the page [s, e) ending just before
|
|
104
|
+
* `before`. Half-open [s, e) keeps loadOlder pages from re-rendering the boundary
|
|
105
|
+
* message. `idx` values are GLOBAL history indexes — pagination never renumbers.
|
|
106
|
+
*/
|
|
107
|
+
export function historyWindow(history, before, pageSize = HISTORY_PAGE_SIZE) {
|
|
108
|
+
const total = Array.isArray(history) ? history.length : 0
|
|
109
|
+
if (total === 0) return { messages: [], hasOlder: false }
|
|
110
|
+
const { frameResults, ownerOf } = pairToolEntries(history)
|
|
111
|
+
const end = before == null ? total : Math.max(0, Math.min(before, total))
|
|
112
|
+
const start = Math.max(0, end - pageSize)
|
|
113
|
+
const messages = []
|
|
114
|
+
const visible = (i) => {
|
|
115
|
+
const k = kindOf(history[i])
|
|
116
|
+
if (k === "user") {
|
|
117
|
+
const m = history[i]
|
|
118
|
+
return isRealUserMsg(m) && typeof m.content === "string" && m.content.trim() !== ""
|
|
119
|
+
}
|
|
120
|
+
if (k === "assistant") return !isGhostAssistant(history[i])
|
|
121
|
+
if (k === "tool") return !ownerOf.has(i) // true orphan (unowned across the whole history) → top-level fallback
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
for (let i = start; i < end; i++) {
|
|
125
|
+
const m = history[i]
|
|
126
|
+
const kind = kindOf(m)
|
|
127
|
+
if (kind === "user") {
|
|
128
|
+
// Rule 1: skip = ¬(isRealUserMsg ∧ content.trim() ≠ "")
|
|
129
|
+
if (isRealUserMsg(m) && typeof m.content === "string" && m.content.trim() !== "") {
|
|
130
|
+
messages.push({ kind: "user", text: m.content, timestamp: tsOf(m), idx: i })
|
|
131
|
+
}
|
|
132
|
+
} else if (kind === "assistant") {
|
|
133
|
+
if (isGhostAssistant(m)) continue
|
|
134
|
+
// Rule 5: turnStart = the previous VISIBLE message is a user or there is none —
|
|
135
|
+
// scan back past skipped entries (reminder / consumed tool / ghost frame — a skip
|
|
136
|
+
// never resets the turn, matching the main line's in-turn semantics)
|
|
137
|
+
let prevVisibleKind = null
|
|
138
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
139
|
+
if (visible(j)) { prevVisibleKind = kindOf(history[j]); break }
|
|
140
|
+
}
|
|
141
|
+
// Rules 3/4: frame tools[] = one card per declared call, declaration order (a result
|
|
142
|
+
// may sit past the window's end — pairing is against the full history)
|
|
143
|
+
const matched = frameResults.get(i)
|
|
144
|
+
const tools = []
|
|
145
|
+
for (const tc of toolCallsOf(m)) {
|
|
146
|
+
const toolIdx = matched?.get(String(tc.id))
|
|
147
|
+
const toolMsg = toolIdx !== undefined ? history[toolIdx] : null
|
|
148
|
+
tools.push({
|
|
149
|
+
id: tc.id ?? null,
|
|
150
|
+
name: tc.function?.name ?? null,
|
|
151
|
+
args: typeof tc.function?.arguments === "string" ? tc.function.arguments : null,
|
|
152
|
+
result: toolMsg && typeof toolMsg.content === "string" ? toolMsg.content : null,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
messages.push({
|
|
156
|
+
kind: "assistant",
|
|
157
|
+
text: textOf(m),
|
|
158
|
+
reasoning: reasoningOf(m),
|
|
159
|
+
timestamp: tsOf(m),
|
|
160
|
+
idx: i,
|
|
161
|
+
turnStart: prevVisibleKind === null || prevVisibleKind === "user",
|
|
162
|
+
tools,
|
|
163
|
+
})
|
|
164
|
+
} else if (kind === "tool") {
|
|
165
|
+
// A consumed tool entry produces no message of its own (rendered with its frame) —
|
|
166
|
+
// only a true orphan falls back to the top level (rules 3/4)
|
|
167
|
+
if (!ownerOf.has(i)) {
|
|
168
|
+
messages.push({
|
|
169
|
+
kind: "tool",
|
|
170
|
+
name: typeof m.name === "string" ? m.name : null,
|
|
171
|
+
text: typeof m.content === "string" ? m.content : null,
|
|
172
|
+
timestamp: tsOf(m),
|
|
173
|
+
idx: i,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { messages, hasOlder: start > 0 }
|
|
179
|
+
}
|
package/hooks.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hooks.mjs — Lifecycle hook engine
|
|
3
|
+
*
|
|
4
|
+
* Hooks are user-defined shell commands executed at key agent lifecycle points.
|
|
5
|
+
* Configured in ~/.thincoder/config.json under "hooks".
|
|
6
|
+
*
|
|
7
|
+
* Event types:
|
|
8
|
+
* PreToolUse — before each tool call (can block execution)
|
|
9
|
+
* PostToolUse — after successful tool execution
|
|
10
|
+
* PostToolUseFailure — after failed tool execution
|
|
11
|
+
* Stop — main-session run end (no tool name; block is meaningless)
|
|
12
|
+
*
|
|
13
|
+
* Each hook: { matcher?, command, args?, timeout?, action? }
|
|
14
|
+
* matcher: regex against tool name (default: match all)
|
|
15
|
+
* command: executable path/name
|
|
16
|
+
* args: optional CLI args; without args, stdin receives JSON payload
|
|
17
|
+
* timeout: ms (default 10000)
|
|
18
|
+
* action: "allow" | "block" | "notify" (default "notify")
|
|
19
|
+
*
|
|
20
|
+
* "block" hooks: exit code 0 = allow, non-zero = block.
|
|
21
|
+
* "allow"/"notify": exit code ignored.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawn } from "node:child_process"
|
|
25
|
+
|
|
26
|
+
/** @param {string} event @param {object} ctx @returns {Promise<boolean>} false if blocked */
|
|
27
|
+
export async function runHooks(event, ctx) {
|
|
28
|
+
const hooks = ctx.agent?.config?.hooks?.[event]
|
|
29
|
+
if (!hooks?.length) return true
|
|
30
|
+
|
|
31
|
+
for (const hook of hooks) {
|
|
32
|
+
// Matcher = tool-name regex: tool events always carry a toolName; no-tool-name
|
|
33
|
+
// events (Stop) ignore it — a configured matcher must not silently never fire.
|
|
34
|
+
if (hook.matcher && ctx.toolName != null) {
|
|
35
|
+
try {
|
|
36
|
+
if (!new RegExp(hook.matcher).test(ctx.toolName)) continue
|
|
37
|
+
} catch { /* invalid regex → skip */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const allowed = await runOneHook(event, hook, ctx)
|
|
41
|
+
if (hook.action === "block" && !allowed) return false
|
|
42
|
+
}
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function runOneHook(event, hook, ctx) {
|
|
47
|
+
const payload = JSON.stringify({
|
|
48
|
+
event,
|
|
49
|
+
toolName: ctx.toolName ?? null,
|
|
50
|
+
toolArgs: ctx.toolArgs ?? null,
|
|
51
|
+
result: ctx.result ?? null,
|
|
52
|
+
error: ctx.error?.message ?? null,
|
|
53
|
+
// Event-specific fields (Stop: turn / reason) — existing callers pass no extra ⇒ payload unchanged.
|
|
54
|
+
...(ctx.extra ?? {}),
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const timeout = hook.timeout ?? 10_000
|
|
60
|
+
let settled = false
|
|
61
|
+
let guard = null
|
|
62
|
+
const done = (code) => {
|
|
63
|
+
if (settled) return
|
|
64
|
+
settled = true
|
|
65
|
+
if (guard) clearTimeout(guard) // #135-④:守卫句柄收尾清理(unref 后的显式释放)
|
|
66
|
+
resolve(code === 0)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let proc
|
|
70
|
+
try {
|
|
71
|
+
// 不传 spawn 的 `timeout` 选项(#135-④):其内部定时器在 spawn 失败(ENOENT)路径
|
|
72
|
+
// 不被清理、也不 unref ⇒ 单独维持事件循环整整 `timeout` ms(实测:命令不存在时
|
|
73
|
+
// 10023ms 退出,去掉该选项后 6ms)——超时杀子进程改由下方自有 guard 承担(同时点、
|
|
74
|
+
// 同默认信号 SIGTERM,语义等价)。
|
|
75
|
+
if (hook.args?.length) {
|
|
76
|
+
proc = spawn(hook.command, hook.args, {
|
|
77
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
})
|
|
80
|
+
} else {
|
|
81
|
+
proc = spawn(hook.command, [], {
|
|
82
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// command not found or spawn failure — don't block, don't crash
|
|
88
|
+
return resolve(true)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
proc.on("error", () => done(0)) // spawn failure → allow
|
|
92
|
+
proc.on("close", (code) => done(code ?? 0))
|
|
93
|
+
proc.on("exit", (code) => done(code ?? 0))
|
|
94
|
+
|
|
95
|
+
// Send payload via stdin
|
|
96
|
+
try { proc.stdin?.end(payload) } catch { /* */ }
|
|
97
|
+
|
|
98
|
+
// Timeout guard(#135-④:`timeout` ms 到点杀掉子进程 + 放行——原由 spawn 的 `timeout`
|
|
99
|
+
// 选项承担,见上)。句柄捕获 + `unref()`:进程存活期间守卫照常触发(真 CLI 运行事件循环
|
|
100
|
+
// 另有 handle),但不再单独维持事件循环存活 ⇒ 子进程钩子用例的测试进程零尾挂;
|
|
101
|
+
// 任一出口经 `done` 收尾 clearTimeout(house 先例 = mcp/transport-stdio.mjs:20)。
|
|
102
|
+
guard = setTimeout(() => {
|
|
103
|
+
try { proc.kill() } catch { /* already gone */ }
|
|
104
|
+
done(0)
|
|
105
|
+
}, timeout)
|
|
106
|
+
guard.unref?.()
|
|
107
|
+
})
|
|
108
|
+
}
|
package/i18n.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n.mjs — 核内文案常量容器(§2.5 #185 / 丁组 D1 裁定「容器归一、投影端差」)。
|
|
3
|
+
*
|
|
4
|
+
* 形态(用户 2026-09-13 裁定 · 按建议):
|
|
5
|
+
* - `CORE_MESSAGES` = 键 → `{ en, zh }`:核内文案**单一权威容器**(本批收编两端同语义文案)。
|
|
6
|
+
* - `t(key, vars, locale)` = 核侧解析(CLI 直接用常量;缺键回退返回 key——不抛)。
|
|
7
|
+
* - `projectDictionary(locale)` = **VSC 字典投影**:核常量 → VSC `locales/*.json` 的扁平
|
|
8
|
+
* 字典形态(VSC 的 `i18n.mjs` `t()` 壳在 S2 接线时并入本投影,壳内零文案)。
|
|
9
|
+
*
|
|
10
|
+
* 契约(CORE-UNIFICATION §2.12.2 第 12 行 · 输出文案类):**机器消费面冻结**——本表键值
|
|
11
|
+
* 与现 VSC `locales/{en,zh}.json` 逐字一致(已锁定串不得改);人读文案后续变更须登记 +
|
|
12
|
+
* CHANGELOG(S2 落地物)。`${name}` = 插值占位(两端同约定)。
|
|
13
|
+
*
|
|
14
|
+
* 边界:本模块只收「核域」文案(压缩 / 消化 / 限流状态 / 异步池状态 / 会诊 / 任务 / goal /
|
|
15
|
+
* 思考状态);webview 面板专属文案(welcome / toolbar / settings / perm 卡等)留在 VSC 侧
|
|
16
|
+
* 字典(④ 端特有面——宿主 UI)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 支持的语言(与 VSC locales 文件名一致)。 */
|
|
20
|
+
export const SUPPORTED_LOCALES = ["en", "zh"]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 核内文案常量容器:键 → { en, zh }。
|
|
24
|
+
* 值 = 现 VSC `locales/{en,zh}.json` 逐字(机器消费面冻结——改值须走 A11 登记 + CHANGELOG)。
|
|
25
|
+
*/
|
|
26
|
+
export const CORE_MESSAGES = Object.freeze({
|
|
27
|
+
// ── 压缩(context compaction)──
|
|
28
|
+
"compress.start": { en: "Compressing context… (summarizing ${n} messages)", zh: "正在压缩上下文…(摘要 ${n} 条消息)" },
|
|
29
|
+
"compress.starting": { en: "Compressing context…", zh: "正在压缩上下文…" },
|
|
30
|
+
"compress.done": { en: "Compressed: ${tokens} tokens freed (${seconds}s)", zh: "已压缩:释放 ${tokens} tokens(${seconds} 秒)" },
|
|
31
|
+
"compress.failed": { en: "Compression failed: ${error}", zh: "压缩失败:${error}" },
|
|
32
|
+
"compress.fallback": { en: "Compression failed — fallback: truncated to ${n} messages", zh: "压缩失败 — 降级:截断为 ${n} 条消息" },
|
|
33
|
+
// ── 消化(auto-turn digest)──
|
|
34
|
+
"digest.start": { en: "Digesting ${n} background report(s)…", zh: "正在消化 ${n} 份后台报告…" },
|
|
35
|
+
"digest.done": { en: "Digested ${n} background report(s) (${seconds}s)", zh: "已消化 ${n} 份后台报告(${seconds}s)" },
|
|
36
|
+
"digest.aborted": { en: "Digestion interrupted (${seconds}s)", zh: "消化中断(${seconds}s)" },
|
|
37
|
+
"digest.turnLabel": { en: "[auto-turn: digesting finished subagent reports…]", zh: "自动回合:消化已完成的子代理报告…" },
|
|
38
|
+
// M4(2026-09-20 端差·显示面消差批):起跑标签三档(en = CLI `suspension-drive.mjs:175-176`
|
|
39
|
+
// 逐字;zh = 与上方既有档同式译法)——VSC 本地档不重复定义(单一权威容器)。
|
|
40
|
+
"digest.turnLabelAsk": { en: "[auto-turn: answering a subagent's in-flight message…]", zh: "自动回合:答复子代理的在飞提问…" },
|
|
41
|
+
"digest.turnLabelAuto": { en: "[auto-turn: continuing background work…]", zh: "自动回合:继续后台推进…" },
|
|
42
|
+
"digest.capAuto": { en: "[auto-turn: continuing past turn cap…]", zh: "自动回合:越过轮次上限,继续推进…" },
|
|
43
|
+
"digest.capStop": { en: "[auto-turn stopped at ${turns} turns — partial digest; finished reports stay in history]", zh: "自动回合在 ${turns} 轮处停止——部分消化;已完成的报告保留在历史中" },
|
|
44
|
+
// ── 限流 / 供应商状态行 ──
|
|
45
|
+
"status.rateWait": { en: "TPM throttle wait ~${s}s", zh: "TPM 限流等待 ~${s}s" },
|
|
46
|
+
"status.rateLimited": { en: "Rate-limited 429, retry in ${s}s", zh: "限流 429,${s}s 后重试" },
|
|
47
|
+
"status.overloaded": { en: "Server overloaded, retrying in ${s}s", zh: "服务过载,${s}s 后重试" },
|
|
48
|
+
"status.quota": { en: "quota exhausted: ${msg}", zh: "配额耗尽:${msg}" },
|
|
49
|
+
// ── 异步池状态词 ──
|
|
50
|
+
"sub.running": { en: "running", zh: "运行中" },
|
|
51
|
+
"sub.queued": { en: "queued", zh: "排队中" },
|
|
52
|
+
"sub.stopped": { en: "stopped", zh: "已停止" },
|
|
53
|
+
"sub.done": { en: "done", zh: "完成" },
|
|
54
|
+
"sub.error": { en: "error", zh: "错误" },
|
|
55
|
+
"sub.cancelled": { en: "cancelled", zh: "已取消" },
|
|
56
|
+
"sub.awaitingDigest": { en: "done · awaiting digestion", zh: "已完成 · 等待消化" },
|
|
57
|
+
"sub.queueSlot": { en: "queued · position ${n} (slot full)", zh: "排队中 · 位置 ${n}(槽满等位)" },
|
|
58
|
+
// ── 会诊状态词 ──
|
|
59
|
+
"consult.answered": { en: "answered", zh: "已回复" },
|
|
60
|
+
"consult.terminated": { en: "terminated", zh: "已终止" },
|
|
61
|
+
// ── 任务 / goal ──
|
|
62
|
+
"task.in_progress": { en: "in progress", zh: "进行中" },
|
|
63
|
+
"goal.criteria": { en: "Criteria", zh: "验收标准" },
|
|
64
|
+
// ── 思考 / 工具状态词 ──
|
|
65
|
+
"status.stopped": { en: "[stopped]", zh: "[已停止]" },
|
|
66
|
+
"status.thinking": { en: "Thinking", zh: "思考中" },
|
|
67
|
+
"status.turn": { en: "turn ${n}/${m}", zh: "轮次 ${n}/${m}" },
|
|
68
|
+
"tool.running": { en: "running…", zh: "执行中…" },
|
|
69
|
+
"tool.done": { en: "done", zh: "完成" },
|
|
70
|
+
})
|
|
71
|
+
// 深度冻结(机器消费面冻结——嵌套 { en, zh } 也冻结;浅冻结下值可被静默改写)。
|
|
72
|
+
for (const entry of Object.values(CORE_MESSAGES)) Object.freeze(entry)
|
|
73
|
+
|
|
74
|
+
/** 语言归一(BCP-47:`zh-CN` → `zh`;未知 → `en`——镜像 VSC `_load` 的候选回退)。 */
|
|
75
|
+
export function normalizeLocale(locale) {
|
|
76
|
+
const raw = String(locale ?? "").trim()
|
|
77
|
+
if (!raw) return "en"
|
|
78
|
+
if (SUPPORTED_LOCALES.includes(raw)) return raw
|
|
79
|
+
const base = raw.split("-")[0]
|
|
80
|
+
return SUPPORTED_LOCALES.includes(base) ? base : "en"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 核侧解析:`t("compress.starting")` / `t("digest.done", { n: 2, seconds: "1.2" })`。
|
|
85
|
+
* 缺键回退返回 key 本身(两端同口径——不抛)。
|
|
86
|
+
*/
|
|
87
|
+
export function t(key, vars = {}, locale = "en") {
|
|
88
|
+
const entry = CORE_MESSAGES[key]
|
|
89
|
+
if (entry === undefined) return key
|
|
90
|
+
let val = entry[normalizeLocale(locale)] ?? entry.en
|
|
91
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
92
|
+
val = val.replace("${" + k + "}", String(v))
|
|
93
|
+
}
|
|
94
|
+
return val
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* VSC 字典投影:核常量 → 扁平 `{ key: value }`(VSC `locales/*.json` 形态)。
|
|
99
|
+
* S2 接线:VSC `i18n.mjs` 的 `_load` 以本投影为核域底座 + 端特有键(webview 面)叠加。
|
|
100
|
+
*/
|
|
101
|
+
export function projectDictionary(locale = "en") {
|
|
102
|
+
const lang = normalizeLocale(locale)
|
|
103
|
+
const out = {}
|
|
104
|
+
for (const [key, entry] of Object.entries(CORE_MESSAGES)) out[key] = entry[lang] ?? entry.en
|
|
105
|
+
return out
|
|
106
|
+
}
|
package/index-bin.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index-bin.mjs — binary I/O for the semantic vector index(CORE-UNIFICATION §2.5 #137
|
|
3
|
+
* 「融合:随核内索引面一并归位(向量编解码)」)。
|
|
4
|
+
*
|
|
5
|
+
* 来源 = `thincoder-vscode/src/index-bin.mjs`(VSC 拆档——CLI 侧无切分档)——**逐字随迁**;
|
|
6
|
+
* 零依赖(只 `node:buffer` 的 `Buffer`)⇒ 直接归位。
|
|
7
|
+
*
|
|
8
|
+
* vectors.bin layout: [4B dim][4B count][count × 4B offsets][all raw Float32 vectors]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function encodeVectors(dim, vectors) {
|
|
12
|
+
const count = vectors.length
|
|
13
|
+
const headerSize = 8 + count * 4
|
|
14
|
+
const dataSize = count * dim * 4
|
|
15
|
+
const buf = Buffer.alloc(headerSize + dataSize)
|
|
16
|
+
|
|
17
|
+
buf.writeUInt32LE(dim, 0)
|
|
18
|
+
buf.writeUInt32LE(count, 4)
|
|
19
|
+
|
|
20
|
+
let offset = headerSize
|
|
21
|
+
for (let i = 0; i < count; i++) {
|
|
22
|
+
buf.writeUInt32LE(offset, 8 + i * 4)
|
|
23
|
+
const vec = vectors[i]
|
|
24
|
+
for (let j = 0; j < dim; j++) {
|
|
25
|
+
buf.writeFloatLE(vec[j], offset + j * 4)
|
|
26
|
+
}
|
|
27
|
+
offset += dim * 4
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return buf
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function decodeVectors(buf) {
|
|
34
|
+
const dim = buf.readUInt32LE(0)
|
|
35
|
+
const count = buf.readUInt32LE(4)
|
|
36
|
+
|
|
37
|
+
const vectors = []
|
|
38
|
+
for (let i = 0; i < count; i++) {
|
|
39
|
+
const offset = buf.readUInt32LE(8 + i * 4)
|
|
40
|
+
const vec = new Float32Array(dim)
|
|
41
|
+
for (let j = 0; j < dim; j++) {
|
|
42
|
+
vec[j] = buf.readFloatLE(offset + j * 4)
|
|
43
|
+
}
|
|
44
|
+
vectors.push(vec)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { dim, vectors }
|
|
48
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index-discover.mjs — file discovery + kind classification for the semantic index
|
|
3
|
+
* (CORE-UNIFICATION §2.5 #137「融合:随核内索引面一并归位(走查规则)」)。
|
|
4
|
+
*
|
|
5
|
+
* 来源 = `thincoder-vscode/src/index-discover.mjs`(VSC 拆档——CLI 无切分档;VSC 头注自述
|
|
6
|
+
* 「CLI-aligned」⇒ 走查规则两端同源)——**逐字随迁**;依赖 `./conventions.mjs`(核内在位)。
|
|
7
|
+
*
|
|
8
|
+
* PORTABILITY VSC mirror · VP-9 (P9): the extension tables were 14 code + 5 doc
|
|
9
|
+
* entries with no declaration surface — a project in any other mainstream language
|
|
10
|
+
* was invisible AND undeclarable, silently. Now: the tables carry the CLI-aligned
|
|
11
|
+
* additions, `.thincoder/conventions.json` (index.codeExtensions / index.docExtensions)
|
|
12
|
+
* unions in project-specific extensions, and files whose extension is in NO list are
|
|
13
|
+
* TALLIED (`collectUnlisted`) so "my .xyz files are not searchable" stops being
|
|
14
|
+
* invisible (buildIndex → panel hint + a log event).
|
|
15
|
+
*/
|
|
16
|
+
import { readdirSync } from "node:fs"
|
|
17
|
+
import { join, relative } from "node:path"
|
|
18
|
+
import { DEFAULT_CONVENTIONS, loadConventions } from "./conventions.mjs"
|
|
19
|
+
|
|
20
|
+
const CODE_EXTS = new Set([
|
|
21
|
+
".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".py", ".rs", ".go", ".java", ".c", ".cpp", ".h", ".hpp",
|
|
22
|
+
// VP-9 additions (CLI-table alignment): languages whose absence made a whole
|
|
23
|
+
// project invisible. Anything still missing is declarable (index.codeExtensions).
|
|
24
|
+
".mts", ".cts", ".dart", ".lua", ".cs", ".fs", ".fsx", ".clj", ".cljs", ".ex", ".exs", ".erl", ".hrl",
|
|
25
|
+
".scala", ".pl", ".pm", ".r", ".jl", ".zig", ".groovy", ".ps1", ".proto", ".graphql", ".tf", ".hcl",
|
|
26
|
+
])
|
|
27
|
+
const DOC_EXTS = new Set([".md", ".markdown", ".txt", ".rst", ".adoc", ".mdx", ".org", ".wiki", ".tex"])
|
|
28
|
+
export const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".turbo", "coverage", "__pycache__", ".next"])
|
|
29
|
+
|
|
30
|
+
/** Lower-cased ".ext" of a path/name ("" when it has no extension). */
|
|
31
|
+
function extensionOf(p) {
|
|
32
|
+
const s = String(p ?? "")
|
|
33
|
+
const base = s.slice(Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\")) + 1)
|
|
34
|
+
const i = base.lastIndexOf(".")
|
|
35
|
+
return i > 0 ? base.slice(i).toLowerCase() : ""
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Effective extension sets for one conventions object: defaults ∪ declared
|
|
39
|
+
* (declaration is a UNION — §4.1: index.*Extensions APPEND). Cached per frozen
|
|
40
|
+
* conventions object (the loader hands out stable per-root instances). */
|
|
41
|
+
const _knownCache = new WeakMap()
|
|
42
|
+
function knownExtensions(conv) {
|
|
43
|
+
const key = conv ?? DEFAULT_CONVENTIONS
|
|
44
|
+
let hit = _knownCache.get(key)
|
|
45
|
+
if (!hit) {
|
|
46
|
+
const code = new Set(CODE_EXTS)
|
|
47
|
+
const doc = new Set(DOC_EXTS)
|
|
48
|
+
for (const e of key.index?.codeExtensions ?? []) code.add(e)
|
|
49
|
+
for (const e of key.index?.docExtensions ?? []) doc.add(e)
|
|
50
|
+
hit = { code, doc, all: new Set([...code, ...doc]) }
|
|
51
|
+
_knownCache.set(key, hit)
|
|
52
|
+
}
|
|
53
|
+
return hit
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Unlisted-extension tally (VP-9 visibility): counts files the index will NOT
|
|
57
|
+
* pick up because their extension is in no list. `exts` is a capped sample (the
|
|
58
|
+
* count is the signal; the list is the hint). */
|
|
59
|
+
export const UNLISTED_SAMPLE_CAP = 20
|
|
60
|
+
function createUnlistedTally() {
|
|
61
|
+
const counts = new Map()
|
|
62
|
+
let count = 0
|
|
63
|
+
return {
|
|
64
|
+
note(ext) {
|
|
65
|
+
if (!ext) return
|
|
66
|
+
count++
|
|
67
|
+
counts.set(ext, (counts.get(ext) ?? 0) + 1)
|
|
68
|
+
},
|
|
69
|
+
result() {
|
|
70
|
+
const exts = [...counts.entries()]
|
|
71
|
+
.sort((a, b) => b[1] - a[1])
|
|
72
|
+
.slice(0, UNLISTED_SAMPLE_CAP)
|
|
73
|
+
.map(([ext, n]) => ({ ext, count: n }))
|
|
74
|
+
return { count, exts }
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Shared walker — ONE rule set for whole-tree discovery, the ignored-subtree walk
|
|
80
|
+
* (needsRebuild B2) and the memory self-check (listMemoryFiles): SKIP_DIRS / dot-dirs are
|
|
81
|
+
* not entered, .thincoder is entered only for memory/, files are filtered by extension.
|
|
82
|
+
* Keeping a single implementation is what makes "discovery" and "rebuild decision" unable
|
|
83
|
+
* to disagree (§4.2 契约六/七). With `collectUnlisted`, files whose extension is in no
|
|
84
|
+
* list are tallied instead of silently dropped. */
|
|
85
|
+
function collectFiles(rootDir, cwd, inThincoder, signal, conv, collectUnlisted) {
|
|
86
|
+
const { all } = knownExtensions(conv)
|
|
87
|
+
const tally = collectUnlisted ? createUnlistedTally() : null
|
|
88
|
+
const files = []
|
|
89
|
+
function walk(dir, insideThincoder) {
|
|
90
|
+
let entries
|
|
91
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
92
|
+
for (const e of entries) {
|
|
93
|
+
signal?.throwIfAborted()
|
|
94
|
+
// Inside .thincoder keep ONLY memory/ (skip index/, sessions/, checklist.md, …)
|
|
95
|
+
if (insideThincoder && e.name !== "memory") continue
|
|
96
|
+
if (e.isDirectory()) {
|
|
97
|
+
// Enter .thincoder at the project root (and its memory/ subdir); skip other dot-dirs.
|
|
98
|
+
if (SKIP_DIRS.has(e.name)) continue
|
|
99
|
+
if (e.name.startsWith(".") && e.name !== ".thincoder") continue
|
|
100
|
+
walk(join(dir, e.name), e.name === ".thincoder")
|
|
101
|
+
} else if (e.isFile()) {
|
|
102
|
+
if (isIndexableName(e.name, all)) {
|
|
103
|
+
files.push(relative(cwd, join(dir, e.name)).replaceAll("\\", "/"))
|
|
104
|
+
} else {
|
|
105
|
+
tally?.note(extensionOf(e.name))
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
walk(rootDir, inThincoder)
|
|
111
|
+
return { files, unlisted: tally ? tally.result() : { count: 0, exts: [] } }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isIndexableName(name, allExts) {
|
|
115
|
+
const ext = extensionOf(name)
|
|
116
|
+
return ext !== "" && allExts.has(ext)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Discover indexable files under the project root.
|
|
121
|
+
* @param {string} cwd — project root
|
|
122
|
+
* @param {AbortSignal} [signal]
|
|
123
|
+
* @param {{collectUnlisted?: boolean}} [opts] — collectUnlisted: ALSO tally files whose
|
|
124
|
+
* extension is in no list; the return becomes `{ files, unlisted }` where
|
|
125
|
+
* `unlisted = { count, exts: [{ext, count}] }` (sample capped). Default: the plain
|
|
126
|
+
* files array (backward-compatible with needsRebuild / the memory self-check).
|
|
127
|
+
*/
|
|
128
|
+
export function discoverFiles(cwd, signal, { collectUnlisted = false } = {}) {
|
|
129
|
+
const res = collectFiles(cwd, cwd, false, signal, loadConventions(cwd), collectUnlisted)
|
|
130
|
+
return collectUnlisted ? res : res.files
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Indexable files under one subtree, relative to `cwd` — same walk rules as discoverFiles
|
|
134
|
+
* (used by needsRebuild to walk an ignored directory entry: §4.2 契约六). */
|
|
135
|
+
export function discoverFilesUnder(cwd, subdir, { collectUnlisted = false } = {}) {
|
|
136
|
+
const base = String(subdir ?? "").replaceAll("\\", "/").replace(/\/+$/, "")
|
|
137
|
+
if (!base) return collectUnlisted ? { files: [], unlisted: { count: 0, exts: [] } } : []
|
|
138
|
+
const res = collectFiles(join(cwd, base), cwd, false, undefined, loadConventions(cwd), collectUnlisted)
|
|
139
|
+
return collectUnlisted ? res : res.files
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Extension-level predicate (defaults ∪ declared). */
|
|
143
|
+
export function isIndexableFile(filePath, conv) {
|
|
144
|
+
return isIndexableName(String(filePath ?? ""), knownExtensions(conv).all)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Full predicate: is this relative path a file the index tracks? (extension + the same
|
|
148
|
+
* directory exclusions discoverFiles applies — SKIP_DIRS and hidden dirs, except the
|
|
149
|
+
* .thincoder/memory/ special case). Used by needsRebuild so "discovery" and "rebuild
|
|
150
|
+
* decision" can never disagree. */
|
|
151
|
+
export function shouldIndexFile(relPath, conv) {
|
|
152
|
+
const p = relPath.replaceAll("\\", "/")
|
|
153
|
+
if (p.startsWith(".thincoder/memory/")) return isIndexableFile(p, conv)
|
|
154
|
+
for (const d of p.split("/").slice(0, -1)) {
|
|
155
|
+
if (SKIP_DIRS.has(d) || d.startsWith(".")) return false
|
|
156
|
+
}
|
|
157
|
+
return isIndexableFile(p, conv)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function kindFor(filePath, conv) {
|
|
161
|
+
// memory files live in .thincoder/memory/ — check FIRST: they're .md, DOC_EXTS would
|
|
162
|
+
// otherwise classify them as docs.
|
|
163
|
+
if (filePath.startsWith(".thincoder/memory/")) return "memory"
|
|
164
|
+
const { code, doc } = knownExtensions(conv)
|
|
165
|
+
const ext = extensionOf(filePath)
|
|
166
|
+
if (code.has(ext)) return "code"
|
|
167
|
+
if (doc.has(ext)) return "doc"
|
|
168
|
+
return "doc"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Memory files the index tracks — recursive, through the SAME walk as discovery
|
|
172
|
+
* (§4.2 契约七: the old flat readdirSync missed nested files, so they were indexed and
|
|
173
|
+
* then judged file-removed on every needsRebuild — an endless rebuild loop). */
|
|
174
|
+
export function listMemoryFiles(cwd) {
|
|
175
|
+
return discoverFilesUnder(cwd, ".thincoder/memory")
|
|
176
|
+
}
|