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.
- package/CHANGELOG.md +34 -1
- package/README.md +13 -12
- package/bin/thincoder.mjs +63 -28
- 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 -371
- 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 +32 -10
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +88 -0
- 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-clear.mjs +2 -0
- 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 +10 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +31 -4
- 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 +206 -0
- package/src/tui/index.mjs +40 -12
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +85 -0
- 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 +17 -6
- 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 +49 -17
- package/src/tui/subagent-blocks.mjs +21 -3
- package/src/tui/subagent-children.mjs +86 -14
- package/src/tui/subagent-freeze.mjs +80 -3
- package/src/tui/suspension-drive.mjs +48 -22
- package/src/tui/tool-args.mjs +5 -2
- package/src/tui/tool-display.mjs +16 -2
- package/src/tui/tool-events.mjs +64 -22
- package/src/tui/tui-lifecycle.mjs +9 -2
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +21 -5
- 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 -242
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -228
- 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 -191
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -468
- 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 -289
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -294
- 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 -479
- 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 -453
- 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 -413
- 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 -381
- 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 -83
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- 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 -413
- package/src/memory/core.mjs +0 -300
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -417
- package/src/memory/file-walk.mjs +0 -109
- 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 -217
- package/src/prompts/discipline-normal.md +0 -179
- 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 -55
- package/src/prompts/persona-engineering.md +0 -54
- 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 -214
- package/src/session-guard.mjs +0 -47
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-slots.mjs +0 -489
- package/src/session.mjs +0 -475
- package/src/skills.mjs +0 -153
- 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 -224
package/src/tools/shared.mjs
DELETED
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tools/shared.mjs — shared tool utilities, constants, OpenAI schema conversion
|
|
3
|
-
* Imported by tools/file.mjs / system.mjs / web.mjs / git.mjs
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { spawn, execFileSync, execFile } from "node:child_process"
|
|
7
|
-
import { readFileSync, existsSync, realpathSync, readdirSync, statSync, openSync, readSync, closeSync } from "node:fs"
|
|
8
|
-
import { dirname, join, resolve } from "node:path"
|
|
9
|
-
import { fileURLToPath } from "node:url"
|
|
10
|
-
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
|
-
export const DESC = (name) => readFileSync(join(__dirname, "..", "tools", `${name}.md`), "utf8")
|
|
13
|
-
|
|
14
|
-
export const MAX_READ_LINES = 2000
|
|
15
|
-
export const MAX_OUTPUT_CHARS = 200_000
|
|
16
|
-
|
|
17
|
-
const ENCODING_DETECT_MAX_TRIM = 3
|
|
18
|
-
const SYNTAX_CHECK_TIMEOUT = 10000
|
|
19
|
-
export const BASH_TIMEOUT_MS = 120_000
|
|
20
|
-
export const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", ".turbo", "coverage"])
|
|
21
|
-
|
|
22
|
-
/** SSRF guard: check if a hostname is private/internal. Shared by web.mjs and codemode.mjs.
|
|
23
|
-
* Returns TRUE for private hosts — callers block them. */
|
|
24
|
-
export function isPrivateHost(hostname) {
|
|
25
|
-
const h = hostname.toLowerCase()
|
|
26
|
-
// Local loopback + link-local names: BLOCK (true). These returned false
|
|
27
|
-
// before — the guard was inverted for the most common SSRF targets
|
|
28
|
-
// (localhost/127.x reach internal services unchecked).
|
|
29
|
-
if (h === "localhost" || h === "0.0.0.0" || h.endsWith(".localhost")) return true
|
|
30
|
-
if (h === "127.0.0.1" || h.startsWith("127.")) return true
|
|
31
|
-
if (h === "169.254.169.254" || h === "metadata.google.internal") return true
|
|
32
|
-
// IPv6 private ranges — only check if host contains ":"
|
|
33
|
-
// fe80::/10 link-local covers fe80:…febf:… — startsWith("fe80:") is the
|
|
34
|
-
// practical subset (fe8/fe9/fea/feb all begin fe8/feb — full range regex
|
|
35
|
-
// would be /^fe[89ab][0-9a-f]:/; startsWith fe8 + fe9 + fea + feb covers it).
|
|
36
|
-
if (h.includes(":")) {
|
|
37
|
-
if (h === "::1" || h.startsWith("fc") || h.startsWith("fd")) return true
|
|
38
|
-
if (/^fe[89ab][0-9a-f]:/.test(h)) return true
|
|
39
|
-
}
|
|
40
|
-
const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
|
|
41
|
-
if (m) {
|
|
42
|
-
// Octet range is NOT validated (999.10.0.1 parses but matches no private
|
|
43
|
-
// prefix → treated as public). Intentional: the guard checks known-private
|
|
44
|
-
// prefixes; invalid IPs are harmless false-negatives for SSRF purposes.
|
|
45
|
-
const [a, b] = [Number(m[1]), Number(m[2])]
|
|
46
|
-
if (a === 10 || (a === 172 && b >= 16 && b <= 31) || a === 192 && b === 168 || a === 169 && b === 254 || a === 0) return true
|
|
47
|
-
}
|
|
48
|
-
return false
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Normalize Windows line endings to Unix: \r\n → \n.
|
|
52
|
-
* Applied on every text-file read so that edit/hash matching
|
|
53
|
-
* and hash computation are platform-consistent. */
|
|
54
|
-
export function normalizeEOL(text) {
|
|
55
|
-
return text.replace(/\r\n/g, "\n")
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Detect a file's EOL style by the type of its FIRST newline: "\r\n" first →
|
|
59
|
-
* the whole file is written back as CRLF; a bare "\n" or no newline → LF.
|
|
60
|
-
* Never counts occurrences (mixed files follow the first line's style). */
|
|
61
|
-
export function detectFileEol(text) {
|
|
62
|
-
const i = text.indexOf("\n")
|
|
63
|
-
return i > 0 && text[i - 1] === "\r" ? "\r\n" : "\n"
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Join lines with the EOL style detected from the original text (write-back restore). */
|
|
67
|
-
export function joinWithEol(lines, originalText) {
|
|
68
|
-
return lines.join(detectFileEol(originalText))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const MAJORITY_EOL_MAX_FILES = 20
|
|
72
|
-
const EOL_SNIFF_BYTES = 4096
|
|
73
|
-
|
|
74
|
-
/** Majority EOL style of a directory's existing files (≤20 files, first 4KB each).
|
|
75
|
-
* New files follow the directory's majority style; empty dir / tie / LF majority → "\n". */
|
|
76
|
-
export function majorityEol(dirPath) {
|
|
77
|
-
let names
|
|
78
|
-
try { names = readdirSync(dirPath) } catch { return "\n" }
|
|
79
|
-
let crlf = 0, lf = 0
|
|
80
|
-
for (const name of names) {
|
|
81
|
-
if (crlf + lf >= MAJORITY_EOL_MAX_FILES) break
|
|
82
|
-
try {
|
|
83
|
-
const p = join(dirPath, name)
|
|
84
|
-
if (!statSync(p).isFile()) continue
|
|
85
|
-
const fd = openSync(p, "r")
|
|
86
|
-
let head = ""
|
|
87
|
-
try {
|
|
88
|
-
const buf = Buffer.alloc(EOL_SNIFF_BYTES)
|
|
89
|
-
const n = readSync(fd, buf, 0, EOL_SNIFF_BYTES, 0)
|
|
90
|
-
head = buf.subarray(0, n).toString("utf8")
|
|
91
|
-
} finally {
|
|
92
|
-
closeSync(fd)
|
|
93
|
-
}
|
|
94
|
-
if (detectFileEol(head) === "\r\n") crlf++
|
|
95
|
-
else lf++
|
|
96
|
-
} catch { /* unreadable entry — skip */ }
|
|
97
|
-
}
|
|
98
|
-
return crlf > lf ? "\r\n" : "\n"
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const CANDIDATE_MAX_LEN = 500
|
|
102
|
-
const CANDIDATE_PREVIEW_LEN = 80
|
|
103
|
-
|
|
104
|
-
/** Longest-common-substring length (rolling-row DP). Inputs are pre-truncated by the caller. */
|
|
105
|
-
function lcsLength(a, b) {
|
|
106
|
-
// Reused DP buffers (review R9#6): per-line allocation caused GC pressure on
|
|
107
|
-
// large files — hoist two module-level rows, grow to fit, swap by index.
|
|
108
|
-
const need = b.length + 1
|
|
109
|
-
if (_lcsBuf0.length < need) {
|
|
110
|
-
const size = Math.max(need, _lcsBuf0.length * 2)
|
|
111
|
-
_lcsBuf0 = new Uint16Array(size)
|
|
112
|
-
_lcsBuf1 = new Uint16Array(size)
|
|
113
|
-
}
|
|
114
|
-
let prev = _lcsBuf0, cur = _lcsBuf1
|
|
115
|
-
prev.fill(0, 0, need)
|
|
116
|
-
let best = 0
|
|
117
|
-
for (let i = 1; i <= a.length; i++) {
|
|
118
|
-
cur[0] = 0
|
|
119
|
-
const ca = a.charCodeAt(i - 1)
|
|
120
|
-
for (let j = 1; j < need; j++) {
|
|
121
|
-
if (ca === b.charCodeAt(j - 1)) {
|
|
122
|
-
const v = prev[j - 1] + 1
|
|
123
|
-
cur[j] = v
|
|
124
|
-
if (v > best) best = v
|
|
125
|
-
} else cur[j] = 0 // must reset — buffer is reused
|
|
126
|
-
}
|
|
127
|
-
const t = prev; prev = cur; cur = t
|
|
128
|
-
}
|
|
129
|
-
return best
|
|
130
|
-
}
|
|
131
|
-
let _lcsBuf0 = new Uint16Array(0), _lcsBuf1 = new Uint16Array(0)
|
|
132
|
-
/** Line-level similarity candidates for a failed edit: score = LCS(oldString, line) / max(len).
|
|
133
|
-
* Multi-line old_string matches on its FIRST line only (failures usually diverge there).
|
|
134
|
-
* Both sides are truncated to 500 chars before scoring so minified files can't blow the budget.
|
|
135
|
-
* Returns up to topN [{ line (1-based), preview, score }] with score >= threshold, best first. */
|
|
136
|
-
export function findCandidates(lines, oldString, topN = 3, threshold = 0.5) {
|
|
137
|
-
const needle = oldString.split("\n")[0].slice(0, CANDIDATE_MAX_LEN)
|
|
138
|
-
if (!needle) return []
|
|
139
|
-
const scored = []
|
|
140
|
-
for (let i = 0; i < lines.length; i++) {
|
|
141
|
-
const raw = lines[i]
|
|
142
|
-
if (!raw) continue
|
|
143
|
-
const line = raw.length > CANDIDATE_MAX_LEN ? raw.slice(0, CANDIDATE_MAX_LEN) : raw
|
|
144
|
-
const longer = Math.max(needle.length, line.length)
|
|
145
|
-
const shorter = Math.min(needle.length, line.length)
|
|
146
|
-
// LCS ≤ shorter side — a length ratio below the threshold can never reach it; skip the DP.
|
|
147
|
-
if (shorter / longer < threshold) continue
|
|
148
|
-
const score = lcsLength(needle, line) / longer
|
|
149
|
-
if (score >= threshold) scored.push({ line: i + 1, preview: raw.slice(0, CANDIDATE_PREVIEW_LEN), score })
|
|
150
|
-
}
|
|
151
|
-
scored.sort((a, b) => b.score - a.score || a.line - b.line)
|
|
152
|
-
return scored.slice(0, topN)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Appended to hashline_edit results when the file contains U+FFFD (encoding-corruption probe). */
|
|
156
|
-
export const FFFD_WARNING = "⚠ file contains U+FFFD (replacement char) — encoding may be corrupted; hash-based addressing may be unreliable. Consider fixing the file encoding first."
|
|
157
|
-
|
|
158
|
-
/** Convert to OpenAI tools parameter format */
|
|
159
|
-
export function toOpenAISchema(tool) {
|
|
160
|
-
return {
|
|
161
|
-
type: "function",
|
|
162
|
-
function: {
|
|
163
|
-
name: tool.name,
|
|
164
|
-
description: tool.description,
|
|
165
|
-
parameters: tool.parameters,
|
|
166
|
-
},
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Strip ANSI escape sequences */
|
|
171
|
-
export function sanitizeOutput(s) {
|
|
172
|
-
return s
|
|
173
|
-
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
174
|
-
.replace(/\x1b\[[0-9;?]*[\x40-\x7E]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][0-9A-B]|\x1b[=>#][0-9]?/g, "")
|
|
175
|
-
.replace(/\r\n/g, "\n")
|
|
176
|
-
.replace(/\r/g, "\n")
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Truncate text to max chars, appending a truncation notice */
|
|
180
|
-
export function truncate(text, max = MAX_OUTPUT_CHARS) {
|
|
181
|
-
if (text.length <= max) return text
|
|
182
|
-
return text.slice(0, max) + `\n[... truncated: ${text.length - max} chars omitted — redirect to a file if you need the full output]`
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Streaming decoder: encoding sniffing ASCII→UTF-8→GBK.
|
|
186
|
-
* KNOWN LIMITATION (accepted): the fallback is hardcoded to GBK (Chinese) —
|
|
187
|
-
* Shift-JIS/EUC-KR pages decode as mojibake. Real-world usage is dominated
|
|
188
|
-
* by UTF-8; a charset-aware variant would need the Content-Type header.
|
|
189
|
-
* Each call creates an independent decoder instance — must not be shared across parallel streams (internal decoder state accumulates). */
|
|
190
|
-
export function makeDecoder() {
|
|
191
|
-
let decoder = null
|
|
192
|
-
let pending = Buffer.alloc(0)
|
|
193
|
-
return (d, flush = false) => {
|
|
194
|
-
pending = Buffer.concat([pending, d])
|
|
195
|
-
if (!decoder) {
|
|
196
|
-
const hasHighByte = pending.some((b) => b >= 0x80)
|
|
197
|
-
if (!hasHighByte) { const s = pending.toString("ascii"); pending = Buffer.alloc(0); return s }
|
|
198
|
-
for (let trim = 0; trim <= ENCODING_DETECT_MAX_TRIM && !decoder; trim++) {
|
|
199
|
-
try { new TextDecoder("utf-8", { fatal: true }).decode(pending.subarray(0, pending.length - trim)); decoder = new TextDecoder("utf-8") }
|
|
200
|
-
catch { /* continue */ }
|
|
201
|
-
}
|
|
202
|
-
if (!decoder) decoder = new TextDecoder("gbk")
|
|
203
|
-
}
|
|
204
|
-
const s = decoder.decode(pending, { stream: !flush })
|
|
205
|
-
pending = Buffer.alloc(0)
|
|
206
|
-
return s
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Single-file git diff. Silently returns empty on failure. Large diffs that exceed maxBuffer are truncated rather than swallowed. */
|
|
211
|
-
export function gitDiffOne(cwd, abs) {
|
|
212
|
-
try {
|
|
213
|
-
const diff = execFileSync("git", ["--no-pager", "diff", "--no-color", "--", abs], {
|
|
214
|
-
cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 10 * 1024 * 1024,
|
|
215
|
-
}).trim()
|
|
216
|
-
if (!diff) return ""
|
|
217
|
-
const lines = diff.split("\n")
|
|
218
|
-
if (lines.length <= 200) return diff
|
|
219
|
-
return lines.slice(0, 200).join("\n") + `\n... (${lines.length - 200} more diff lines)`
|
|
220
|
-
} catch (e) {
|
|
221
|
-
// maxBuffer overflow: e.stdout contains partial collected output; other errors (non-git repo etc.) return empty
|
|
222
|
-
if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" && e.stdout) {
|
|
223
|
-
const lines = e.stdout.toString().split("\n")
|
|
224
|
-
return lines.slice(0, 200).join("\n") + `\n... (diff too large, showing first 200 of more lines)`
|
|
225
|
-
}
|
|
226
|
-
return ""
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** Auto syntax check after file modification */
|
|
231
|
-
export async function autoSyntaxCheck(abs) {
|
|
232
|
-
if (!/\.(m?js)$/i.test(abs)) return ""
|
|
233
|
-
try {
|
|
234
|
-
await new Promise((resolve, reject) => {
|
|
235
|
-
const child = execFile("node", ["--check", abs], { timeout: SYNTAX_CHECK_TIMEOUT, stdio: ["ignore", "pipe", "pipe"] })
|
|
236
|
-
let stderr = ""
|
|
237
|
-
child.stderr.on("data", (d) => { stderr += d.toString() })
|
|
238
|
-
child.on("error", reject)
|
|
239
|
-
child.on("close", (code) => {
|
|
240
|
-
if (code === 0) resolve()
|
|
241
|
-
else {
|
|
242
|
-
const err = new Error(stderr.trim() || `node --check exited with code ${code}`)
|
|
243
|
-
err.stderr = stderr
|
|
244
|
-
reject(err)
|
|
245
|
-
}
|
|
246
|
-
})
|
|
247
|
-
})
|
|
248
|
-
return "\nSyntax: OK"
|
|
249
|
-
} catch (e) {
|
|
250
|
-
const err = (e.stderr || e.stdout || e.message || "").toString().split("\n").slice(0, 3).join("\n")
|
|
251
|
-
return `\nSyntax: FAILED — ${err}\n(If this file was corrupted by a bad edit, recover it from a checkpoint: checkpoint action=list then action=rewind with the latest id.)`
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Resolve realpath by walking up the directory tree */
|
|
256
|
-
function realpathNearest(abs) {
|
|
257
|
-
let cur = abs
|
|
258
|
-
const tail = []
|
|
259
|
-
while (!existsSync(cur)) {
|
|
260
|
-
const parent = dirname(cur)
|
|
261
|
-
if (parent === cur) return abs
|
|
262
|
-
tail.unshift(cur.slice(parent.length + 1))
|
|
263
|
-
cur = parent
|
|
264
|
-
}
|
|
265
|
-
try { const real = realpathSync(cur); return tail.length ? join(real, ...tail) : real }
|
|
266
|
-
catch { return abs }
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// cwd is effectively constant per CLI session — the cache never grows in
|
|
270
|
-
// practice. A long-running server with rotating cwds would leak; revisit with
|
|
271
|
-
// an LRU if that usage ever appears.
|
|
272
|
-
const realCwdCache = new Map()
|
|
273
|
-
/** Resolve cwd to realpath, cached */
|
|
274
|
-
function realCwd(cwd) {
|
|
275
|
-
if (!realCwdCache.has(cwd)) realCwdCache.set(cwd, realpathNearest(resolve(cwd)))
|
|
276
|
-
return realCwdCache.get(cwd)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** Resolve a user-supplied path relative to cwd — no directory boundary assertion
|
|
280
|
-
* (§10.1 2026-09-02: workspace confinement removed; resolveInCwd ≡ resolveExternal,
|
|
281
|
-
* trust model + approval gate is the only guard — same boundary as bash). */
|
|
282
|
-
export function resolveInCwd(ctx, p) {
|
|
283
|
-
return resolveExternal(ctx, p)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Resolve a path relative to cwd without boundary check — kept for compatibility (≡ resolveInCwd) */
|
|
287
|
-
export function resolveExternal(ctx, p) {
|
|
288
|
-
const cwd = realCwd(ctx.cwd)
|
|
289
|
-
return resolve(cwd, p)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/** Coarse segmentation for destructive pre-check (also splits on > >> < so destructive detection still works through redirection) */
|
|
293
|
-
export function shellSegments(command) {
|
|
294
|
-
return command.split(/&&|\|\||>>|\$\(|[;|\n<>]|`|[(]/)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Blank out quoted regions (single/double/backtick) with spaces, preserving length.
|
|
299
|
-
* Lets detectDanger ignore shell metacharacters inside quoted script bodies —
|
|
300
|
-
* e.g. `node -e "if (a > b) …"` comparisons are not danger signals.
|
|
301
|
-
*/
|
|
302
|
-
function blankQuoted(command) {
|
|
303
|
-
let out = ""
|
|
304
|
-
let quote = null
|
|
305
|
-
for (let i = 0; i < command.length; i++) {
|
|
306
|
-
const ch = command[i]
|
|
307
|
-
if (quote) {
|
|
308
|
-
if (ch === "\\" && quote !== "'") { out += " "; i++; out += " "; continue }
|
|
309
|
-
if (ch === quote) { quote = null; out += " "; continue }
|
|
310
|
-
// Backticks are COMMAND SUBSTITUTION — the content executes, so it must
|
|
311
|
-
// stay visible to detectDanger (echo `cat > /tmp/x` still runs). Only '
|
|
312
|
-
// and " are literal regions.
|
|
313
|
-
out += quote === "`" ? ch : " "
|
|
314
|
-
} else if (ch === "'" || ch === '"' || ch === "`") {
|
|
315
|
-
quote = ch
|
|
316
|
-
out += " "
|
|
317
|
-
} else {
|
|
318
|
-
out += ch
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return out
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Whether a single command segment is destructive — ALWAYS FALSE (deliberate).
|
|
326
|
-
*
|
|
327
|
-
* 决策(2026-08):文本拦截对恶意模型是安全剧场——空白变体/heredoc/node -e/写脚本执行
|
|
328
|
-
* 都能绕过,拦住的只有正常操作(如清理临时目录、rm node_modules 重装)。
|
|
329
|
-
* 真实防线在工具审批层(autoApprove)与快照兜底(gitGuardSnapshot / checkpoint auto-snapshot),
|
|
330
|
-
* 与 env 过滤、git 破坏操作"快照后放行、永不拦截"同一哲学。
|
|
331
|
-
* 项目工具自带确认门(如 thin5 scripts/db.mjs --write/--danger)不应被双重拦截。
|
|
332
|
-
*/
|
|
333
|
-
export function isDestructiveCommand() {
|
|
334
|
-
return false
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* 危险命令识别(只标注、不拦截)——参考 kimi-code apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts
|
|
339
|
-
* DANGER_PATTERNS。定位:给审批中的人打红色警告标签,提升决策信息,不是机器防线。
|
|
340
|
-
* 拦截无用(可绕过),标注有用(人看到了才知道该多看一眼)。
|
|
341
|
-
*/
|
|
342
|
-
const DANGER_PATTERNS = [
|
|
343
|
-
{ pattern: /\brm\s+(-[a-zA-Z]*[rRfF][a-zA-Z]*|--recursive|--force)/i, label: "recursive delete" },
|
|
344
|
-
{ pattern: /\bsudo\b/i, label: "sudo" },
|
|
345
|
-
{ pattern: /\b(curl|wget)\b[^|]*\|\s*(sh|bash|zsh)\b/i, label: "pipe to shell" },
|
|
346
|
-
{ pattern: /\bdd\b[^|]*\bof=/i, label: "dd write" },
|
|
347
|
-
{ pattern: /\bmkfs\b/i, label: "mkfs" },
|
|
348
|
-
{ pattern: />\s*\/dev\/(sd|nvme|disk|hd)/i, label: "write to raw device" },
|
|
349
|
-
{ pattern: /\bchmod\s+(?:-[rR]\s+)?777\b/i, label: "chmod 777" },
|
|
350
|
-
{ pattern: /:\(\)\s*\{\s*:\|:&\s*\}/i, label: "fork bomb" },
|
|
351
|
-
]
|
|
352
|
-
|
|
353
|
-
/** 返回危险标注 label(如 "recursive delete");无危险返回 undefined。
|
|
354
|
-
* 引号感知:引号(单/双)内的内容清空后再检测——commit message、echo 文本等
|
|
355
|
-
* 纯文本不误标;危险命令(rm -rf "$dir")的参数在引号外,仍命中。
|
|
356
|
-
* 反引号内容保留(命令替换会执行)。 */
|
|
357
|
-
export function detectDanger(command) {
|
|
358
|
-
const s = blankQuoted(String(command ?? ""))
|
|
359
|
-
for (const { pattern, label } of DANGER_PATTERNS) {
|
|
360
|
-
if (pattern.test(s)) return label
|
|
361
|
-
}
|
|
362
|
-
return undefined
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
/** Glob dialect (TOOLS.md §17) lives in glob-dialect.mjs — single authority for
|
|
367
|
-
* brace expansion / exclusion / explicit syntax errors (extracted 2026-09-06 —
|
|
368
|
-
* advisor #5: shared.mjs exceeded 500 lines). Re-exported so existing importers
|
|
369
|
-
* (system.mjs tools, ls filter, tests) keep their import paths unchanged. */
|
|
370
|
-
export {
|
|
371
|
-
globToRegex,
|
|
372
|
-
splitGlobPatterns,
|
|
373
|
-
compileGlobMatchers,
|
|
374
|
-
GLOB_EXTGLOB_ERROR,
|
|
375
|
-
GLOB_EMPTY_BRACE_ERROR,
|
|
376
|
-
GLOB_UNCLOSED_BRACE_ERROR,
|
|
377
|
-
GLOB_NESTED_BRACE_ERROR,
|
|
378
|
-
} from "./glob-dialect.mjs"
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
/** Decode a numeric HTML entity to its code point — invalid/out-of-range
|
|
382
|
-
* values (e.g. �) must not throw RangeError; keep the source
|
|
383
|
-
* text as-is (display-only residue is acceptable). */
|
|
384
|
-
function decodeNumericEntity(_, digits) {
|
|
385
|
-
const n = Number(digits)
|
|
386
|
-
return Number.isSafeInteger(n) && n <= 0x10ffff ? String.fromCodePoint(n) : _
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/** Strip HTML tags. KNOWN LIMITATION (accepted): the `/<[^>]+>/g` regex treats
|
|
390
|
-
* the first `>` (or a `<` inside an attribute value) as the tag boundary —
|
|
391
|
-
* `<img alt="a > b">` truncates the match and leaves text residue. A full
|
|
392
|
-
* HTML parser is out of scope; real-world HTML with angle brackets in
|
|
393
|
-
* attributes is rare and the residue is display-only (never parsed). */
|
|
394
|
-
export function stripTags(html) {
|
|
395
|
-
return html
|
|
396
|
-
.replace(/<[^>]+>/g, "")
|
|
397
|
-
.replace(/�*(\d+);/g, (m, n) => decodeNumericEntity(m, n))
|
|
398
|
-
.replace(/&#x([0-9a-fA-F]+);/g, (m, h) => decodeNumericEntity(m, parseInt(h, 16)))
|
|
399
|
-
.replace(/ | /g, " ")
|
|
400
|
-
.replace(/</g, "<")
|
|
401
|
-
.replace(/>/g, ">")
|
|
402
|
-
.replace(/"/g, '"')
|
|
403
|
-
.replace(/&/g, "&")
|
|
404
|
-
.replace(/\s+/g, " ")
|
|
405
|
-
.trim()
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/** HTML → plain text: strip scripts/styles, newline block tags, strip tags, decode entities, compress blank lines */
|
|
409
|
-
export function htmlToText(html) {
|
|
410
|
-
return html
|
|
411
|
-
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
412
|
-
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
413
|
-
.replace(/<noscript[\s\S]*?<\/noscript>/gi, "")
|
|
414
|
-
.replace(/<\/(p|div|li|ul|ol|h[1-6]|tr|table|section|article|header|footer|blockquote|pre)>/gi, "\n")
|
|
415
|
-
.replace(/<br\s*\/?>/gi, "\n")
|
|
416
|
-
.replace(/<li[^>]*>/gi, "- ")
|
|
417
|
-
.replace(/<[^>]+>/g, "")
|
|
418
|
-
.replace(/�*(\d+);/g, (m, n) => decodeNumericEntity(m, n))
|
|
419
|
-
.replace(/&#x([0-9a-fA-F]+);/g, (m, h) => decodeNumericEntity(m, parseInt(h, 16)))
|
|
420
|
-
.replace(/ | /g, " ")
|
|
421
|
-
.replace(/</g, "<")
|
|
422
|
-
.replace(/>/g, ">")
|
|
423
|
-
.replace(/"/g, '"')
|
|
424
|
-
.replace(/&/g, "&") // & must be decoded last, otherwise &lt; gets double-decoded to <
|
|
425
|
-
.replace(/[ \t]+/g, " ")
|
|
426
|
-
.replace(/\n\s*\n\s*\n+/g, "\n\n")
|
|
427
|
-
.trim()
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/** Execute a git command. maxBuffer 10MB prevents large diff/log overflow; on overflow, returns truncated partial output rather than empty.
|
|
431
|
-
* config: optional array of `-c key=value` overrides (e.g. ["http.proxy=http://10.2.2.112:3128"]) —
|
|
432
|
-
* inserted verbatim after `git`, so network actions (push/fetch/pull/ls-remote) can route through a proxy. */
|
|
433
|
-
export function runGit(cwd, cmdArgs, config = []) {
|
|
434
|
-
try {
|
|
435
|
-
return execFileSync("git", [...config, ...cmdArgs], { cwd, encoding: "utf8", maxBuffer: 10 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }).trim().replace(/\r/g, "")
|
|
436
|
-
} catch (e) {
|
|
437
|
-
// maxBuffer overflow: e.stdout contains partial collected output — return it
|
|
438
|
-
// (callers show "(truncated)"-style tails). ALL OTHER errors (non-git repo,
|
|
439
|
-
// permission, bad command) return "" — matching gitDiffOne's pattern: a
|
|
440
|
-
// failed git call must not masquerade as partial success.
|
|
441
|
-
if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" && e.stdout) {
|
|
442
|
-
return String(e.stdout).trim().replace(/\r/g, "").split("\n").slice(0, 200).join("\n")
|
|
443
|
-
}
|
|
444
|
-
return ""
|
|
445
|
-
}
|
|
446
|
-
}
|
package/src/tools/tree.md
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
Generate a directory tree of the codebase (default depth 3). Skips dotfiles, .git/node_modules/dist/build/bin/obj and other build/vendor dirs, and binary files. Use to quickly see which modules exist and where files live.
|
|
2
|
-
|
|
3
|
-
**Route to tree instead of bash:**
|
|
4
|
-
- `tree` / `find .` / `dir /s` → tree
|
|
5
|
-
|
|
6
|
-
Parameters:
|
|
7
|
-
- path: Root directory (default cwd)
|
|
8
|
-
- depth: Tree depth (default 3, max 6). Directories are listed before files, both sorted.
|
|
9
|
-
|
|
10
|
-
Notes:
|
|
11
|
-
- Capped at 200 entries.
|
|
12
|
-
- Directories end with `/`; tree-drawing uses `├──`/`└──`/`│`.
|
|
13
|
-
- Use depth for a shallow overview; use `ls` for one directory, `glob` for a specific file pattern.
|
|
14
|
-
- Returns the directory text tree — directories first (`dir/`), files after, both sorted, capped at 200 entries.
|
package/src/tools/tree.mjs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tree.mjs — directory tree tool (parity with thinworker `repomap`).
|
|
3
|
-
* Renders a repo's directory tree (default depth 3), skipping dotfiles,
|
|
4
|
-
* build/vendor dirs and binary files, so the model can see which modules
|
|
5
|
-
* exist without shelling out to `tree`/`find`.
|
|
6
|
-
*/
|
|
7
|
-
import { DESC, truncate, resolveInCwd } from "./shared.mjs"
|
|
8
|
-
import { readdir, stat } from "node:fs/promises"
|
|
9
|
-
import { join, basename, extname } from "node:path"
|
|
10
|
-
|
|
11
|
-
const MAX_ENTRIES = 200
|
|
12
|
-
const DEFAULT_DEPTH = 3
|
|
13
|
-
const SKIP_DIRS = new Set(["node_modules", "bin", "obj", "dist", "build", "coverage", "turbo", ".git", ".thincoder", ".vs", ".venv", "__pycache__", ".idea"])
|
|
14
|
-
const BINARY_EXTS = new Set([".exe", ".dll", ".png", ".jpg", ".jpeg", ".gif", ".pdf", ".docx", ".xlsx", ".pptx", ".zip", ".7z", ".mp3", ".mp4", ".woff", ".woff2", ".ico"])
|
|
15
|
-
|
|
16
|
-
export const treeTool = {
|
|
17
|
-
name: "tree",
|
|
18
|
-
description: DESC("tree"),
|
|
19
|
-
parameters: {
|
|
20
|
-
type: "object",
|
|
21
|
-
properties: {
|
|
22
|
-
path: { type: "string", description: "Root directory (default cwd)" },
|
|
23
|
-
depth: { type: "integer", description: `Tree depth (default ${DEFAULT_DEPTH}, max 6)` },
|
|
24
|
-
},
|
|
25
|
-
required: [],
|
|
26
|
-
},
|
|
27
|
-
readonly: true,
|
|
28
|
-
async execute(args, ctx) {
|
|
29
|
-
const root = resolveInCwd(ctx, args.path ?? ".")
|
|
30
|
-
let st
|
|
31
|
-
try { st = await stat(root) } catch { return `Error: directory not found: ${args.path ?? "."}` }
|
|
32
|
-
if (!st.isDirectory()) return `Error: not a directory: ${args.path ?? "."}`
|
|
33
|
-
const d = Number(args.depth)
|
|
34
|
-
const maxDepth = Number.isInteger(d) && d > 0 ? Math.min(d, 6) : DEFAULT_DEPTH
|
|
35
|
-
const lines = [basename(root) + "/"]
|
|
36
|
-
const state = { count: 1, capped: false }
|
|
37
|
-
await walk(root, 0, maxDepth, "", lines, state)
|
|
38
|
-
return truncate(lines.join("\n"))
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function walk(dir, depth, maxDepth, prefix, lines, state) {
|
|
43
|
-
if (state.capped) return
|
|
44
|
-
let entries
|
|
45
|
-
try { entries = await readdir(dir, { withFileTypes: true }) } catch { return }
|
|
46
|
-
const items = []
|
|
47
|
-
for (const e of entries) {
|
|
48
|
-
if (e.name.startsWith(".")) continue // dotfiles + dotdirs
|
|
49
|
-
const isDir = e.isDirectory()
|
|
50
|
-
if (isDir) { if (SKIP_DIRS.has(e.name)) continue; items.push({ name: e.name, isDir: true }) }
|
|
51
|
-
else if (!BINARY_EXTS.has(extname(e.name).toLowerCase())) items.push({ name: e.name, isDir: false })
|
|
52
|
-
}
|
|
53
|
-
items.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))
|
|
54
|
-
|
|
55
|
-
for (let i = 0; i < items.length; i++) {
|
|
56
|
-
if (state.capped) return
|
|
57
|
-
if (state.count >= MAX_ENTRIES) { state.capped = true; lines.push(prefix + "…(更多项已省略)"); return }
|
|
58
|
-
const { name, isDir } = items[i]
|
|
59
|
-
const isLast = i === items.length - 1
|
|
60
|
-
lines.push(prefix + (isLast ? "└── " : "├── ") + (isDir ? name + "/" : name))
|
|
61
|
-
state.count++
|
|
62
|
-
if (isDir && depth + 1 < maxDepth) {
|
|
63
|
-
await walk(join(dir, name), depth + 1, maxDepth, prefix + (isLast ? " " : "│ "), lines, state)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
package/src/tools/wait_for.md
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
Wait until a condition becomes true — polls the condition every interval_ms and returns as soon as it holds or the timeout_ms ceiling passes. Use it ONLY for genuinely asynchronous waits: an async subagent/consult still settling, a file appearing, a port opening. Synchronous tools (advisor, blocking subagent spawn) return when done — waiting after them is NOT needed and wastes time.
|
|
2
|
-
|
|
3
|
-
Parameters:
|
|
4
|
-
- condition (required): a semantic condition expression:
|
|
5
|
-
- `advisor settled` — an in-flight advisor review session has finished (async advisor-role children settled)
|
|
6
|
-
- `subagent id:N done` — async subagent N (the id your async spawn ack returned) has settled
|
|
7
|
-
- `consult done` — every consult_start session has drained
|
|
8
|
-
- `file exists:path` — the file at path exists (path relative to cwd)
|
|
9
|
-
- `port open:N` — something is listening on 127.0.0.1 port N
|
|
10
|
-
- The agent-internal conditions (advisor / subagent / consult) apply to ASYNC sessions only — a synchronous call already completed before it returned and has nothing to wait for. An unknown condition is an explicit error (`wait_for: unsupported condition "..."` — the supported forms are listed above), never a silent wait.
|
|
11
|
-
- interval_ms: poll interval (default 1000, floor 100 — polling never busy-spins)
|
|
12
|
-
- timeout_ms: overall ceiling (default 30000; config.json `agent.waitForTimeoutMs` overrides the default; hard cap 600000 like the execute tool)
|
|
13
|
-
|
|
14
|
-
Returns:
|
|
15
|
-
- `wait_for: condition satisfied after Nms (N checks): "<condition>"` — condition held
|
|
16
|
-
- `wait_for: timed out after Nms waiting for "<condition>" ...` — ceiling passed with the condition still false (never burns beyond the ceiling)
|
|
17
|
-
- interrupts (Ctrl+C / cancel) exit immediately; malformed conditions and unsupported syntax error out explicitly
|
|
18
|
-
|
|
19
|
-
Notes:
|
|
20
|
-
- Read-only and non-destructive — it only observes (agent pools, the filesystem, a local port probe).
|
|
21
|
-
- Blocking by design — call it ALONE in a turn, not batched with calls that depend on its result.
|
|
22
|
-
- Do not use it to wait after synchronous tools (advisor / blocking subagent spawn / verify return only when done).
|