thincoder 0.12.59 → 0.12.61
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 +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +18 -98
- package/src/provider/errors.mjs +101 -0
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +40 -38
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
package/src/tools/patch.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { relative, dirname } from "node:path";
|
|
|
16
16
|
* Parse a unified diff: returns [{ path, isNew, hunks: [{ ops: [{type:" "|"-"|"+", text}] }] }]
|
|
17
17
|
* Consume hunk lines by the line counts in the @@ header — LLMs often strip context blank lines to pure empty lines,
|
|
18
18
|
* so we use counts rather than first characters to determine hunk boundaries.
|
|
19
|
-
*
|
|
19
|
+
* APPLY-PATCH.md §3: a bare "@@" header (no coordinates) is accepted — the hunk body runs until the next hunk/file header
|
|
20
20
|
* ("@@" / "--- " / "+++ " / "diff " / "index "), purely located by its ops.
|
|
21
21
|
*/
|
|
22
22
|
/**
|
|
@@ -68,7 +68,7 @@ function parsePatch(patch) {
|
|
|
68
68
|
if (!cur) throw new Error("Malformed patch: hunk header before any file header")
|
|
69
69
|
const m = line.match(/^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/)
|
|
70
70
|
if (!m) {
|
|
71
|
-
//
|
|
71
|
+
// APPLY-PATCH.md §3:坐标裸 "@@" 头——hunk 完全靠操作行定位。
|
|
72
72
|
// 无行数可用:操作行以 " " / "-" / "+" 开头(空行宽容为上下文行),
|
|
73
73
|
// 直到下一个 hunk 头 / 文件头 "@@"/"--- "/"+++ "/"diff "/"index " 为止。
|
|
74
74
|
if (!/^@@(?: @@)?\s*$/.test(line)) {
|
|
@@ -94,7 +94,7 @@ function parsePatch(patch) {
|
|
|
94
94
|
}
|
|
95
95
|
if (hunk.ops.length === 0) throw new Error(`Malformed patch: empty coordinate-less hunk "${line.trim()}"`)
|
|
96
96
|
const ctxCount = hunk.ops.filter((o) => o.type === " ").length
|
|
97
|
-
// §
|
|
97
|
+
// §3 (APPLY-PATCH.md——2026-09-04):context<2 且含 ≥1 个 - 行 → 接受——定位锚 = hunk 内
|
|
98
98
|
// 匹配行序列(空格上下文行 + - 行——按出现序)连续——唯一匹配即应用(applyHunks 既有锚匹配域——
|
|
99
99
|
// 多匹配 / not-found 语义不变)。0 上下文与 1 上下文同待遇(评审 #4a)。
|
|
100
100
|
// 纯 +(无 - 锚)且 context<2 仍拒——插入位置不可判——报错引导加锚(NF15.8c)。
|
package/src/tools/question.md
CHANGED
|
@@ -8,5 +8,9 @@ Notes:
|
|
|
8
8
|
- The agent loop pauses until the user answers
|
|
9
9
|
- The answer is injected as the next user message
|
|
10
10
|
- Returns the user's answer — the chosen option or free text — as the next message; the loop resumes when it arrives.
|
|
11
|
+
- Availability: this tool needs an interactive UI — in contexts without one (headless runs, subagent children) it returns an error instead of asking; put the question in your reply text instead.
|
|
11
12
|
- Use sparingly — prefer making reasonable decisions when possible
|
|
13
|
+
- Ask ONE question per call — never bundle multiple sub-questions into one question string; ask the next one after the answer arrives.
|
|
14
|
+
- Keep the question text short — one or two sentences. Background, context, and analysis belong in your normal reply text, NOT in the question.
|
|
15
|
+
- Routine confirmations (confirm gates) belong in your plain reply text — the user answers in their next message. Use this tool ONLY when you need the user's decision or input to proceed.
|
|
12
16
|
- After receiving an answer about a design convention, tool preference, or recurring pattern: save it with the memory tool (action: put). This prevents asking the same question in future sessions — the user shouldn't have to repeat their preferences.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DESC } from "./shared.mjs";
|
|
2
|
+
|
|
3
|
+
export const questionTool = {
|
|
4
|
+
name: "question",
|
|
5
|
+
description: DESC("question"),
|
|
6
|
+
parameters: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
question: { type: "string", description: "The question to ask the user" },
|
|
10
|
+
options: {
|
|
11
|
+
type: "array",
|
|
12
|
+
items: { type: "string" },
|
|
13
|
+
description: "Single-choice options for the user to pick from (optional)",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
required: ["question"],
|
|
17
|
+
},
|
|
18
|
+
readonly: true,
|
|
19
|
+
async execute(args, ctx) {
|
|
20
|
+
if (!ctx.onQuestion) throw new Error("question tool not supported in this context (no UI to ask)")
|
|
21
|
+
// §22 D-Q3 机械限制(2026-09-06 用户裁定:100 字符 / 4 条——超限拒绝回模型,不弹卡、不调 onQuestion)
|
|
22
|
+
if (args.question.length > 100) return "(error: question too long (>100 chars) — ask ONE short question; background belongs in your normal reply text)"
|
|
23
|
+
if (Array.isArray(args.options) && args.options.length > 4) return "(error: too many options (>4) — offer at most 4 plain-string options)"
|
|
24
|
+
return ctx.onQuestion(args.question, args.options ?? [])
|
|
25
|
+
},
|
|
26
|
+
}
|
package/src/tools/read.md
CHANGED
|
@@ -7,12 +7,11 @@ Read a text file. Returns numbered lines. Use offset/limit to page large files.
|
|
|
7
7
|
- Know the symbol but not the location? → `code_search` or `lsp definition`
|
|
8
8
|
- Know the file but not the lines? → `grep` to find line numbers, then read that range with offset/limit
|
|
9
9
|
- Reading an image? → `read_image` instead
|
|
10
|
-
- Reading a PDF? → `read_pdf` instead (read would decode the binary as UTF-8 garbage)
|
|
11
10
|
|
|
12
11
|
Parameters:
|
|
13
12
|
- path (required): File path, relative to cwd or absolute (alias: filePath)
|
|
14
13
|
- offset: 1-based line number to start reading from
|
|
15
|
-
- limit: Max lines to return (default 2000)
|
|
14
|
+
- limit: Max lines to return (default 2000) — windows over files beyond that return head + `…(truncated: K lines in middle, use offset to continue)` + the file's real tail lines, so the file end is never hidden
|
|
16
15
|
- hashes: Include SHA256 content hashes per line (for hashline_edit). Set true before using hashline_edit.
|
|
17
16
|
|
|
18
17
|
Notes:
|
package/src/tools/read_image.md
CHANGED
|
@@ -5,4 +5,4 @@ Parameters:
|
|
|
5
5
|
|
|
6
6
|
Notes:
|
|
7
7
|
- Raster formats only (png/jpg/gif/webp): no mainstream vision API (Kimi, Anthropic, OpenAI, Gemini) accepts svg or bmp, and an unsupported image part in history makes every subsequent request fail with 400. svg files are returned as text source instead (readable by any model); bmp is rejected — convert to PNG first.
|
|
8
|
-
- This tool only works with models that support vision/image input (
|
|
8
|
+
- This tool only works with models that support vision/image input (capability is spec-driven — the model spec decides, not a hardcoded list). Models without vision support will receive an error — except svg, which needs no vision support since it is read as text.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DESC,
|
|
3
|
+
truncate,
|
|
4
|
+
resolveInCwd,
|
|
5
|
+
globToRegex,
|
|
6
|
+
splitGlobPatterns,
|
|
7
|
+
compileGlobMatchers,
|
|
8
|
+
normalizeEOL,
|
|
9
|
+
IGNORED_DIRS,
|
|
10
|
+
} from "./shared.mjs";
|
|
11
|
+
import { readFile, readdir, stat, lstat } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
/** Escape a string for literal regex matching (grep literal=true). */
|
|
15
|
+
function escapeRegExp(s) {
|
|
16
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------- glob
|
|
20
|
+
|
|
21
|
+
export const globTool = {
|
|
22
|
+
name: "glob",
|
|
23
|
+
description: DESC("glob"),
|
|
24
|
+
parameters: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
pattern: { type: "string", description: "Glob pattern — supports **, *, ?, [..], {a,b} braces, and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
28
|
+
path: { type: "string", description: "Directory to search in (default cwd)" },
|
|
29
|
+
},
|
|
30
|
+
required: ["pattern"],
|
|
31
|
+
},
|
|
32
|
+
readonly: true,
|
|
33
|
+
async execute(args, ctx) {
|
|
34
|
+
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
35
|
+
// §17 调用侧拆分(评审 #5 职责分层):空格分隔 include !exclude 多模式在这里拆;
|
|
36
|
+
// globToRegex 只收单个模式(数组由 compileGlobMatchers 收)。
|
|
37
|
+
let match
|
|
38
|
+
try {
|
|
39
|
+
const parts = splitGlobPatterns(args.pattern)
|
|
40
|
+
if (parts.length === 0) return "Error: pattern is required"
|
|
41
|
+
match = compileGlobMatchers(parts).test
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return `glob error: invalid pattern "${args.pattern}": ${e.message}`
|
|
44
|
+
}
|
|
45
|
+
const results = []
|
|
46
|
+
for await (const relPath of walkFiles(base)) {
|
|
47
|
+
if (match(relPath)) {
|
|
48
|
+
results.push(relPath)
|
|
49
|
+
if (results.length >= 1000) break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (results.length === 0) return "(no matches)"
|
|
53
|
+
return truncate(results.sort().join("\n"))
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Recursively traverse files, yield relative paths (skip IGNORED_DIRS) */
|
|
58
|
+
async function* walkFiles(dir, rel = "") {
|
|
59
|
+
let entries
|
|
60
|
+
try {
|
|
61
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
62
|
+
} catch {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
// Skip ignored dirs AND symbolic links (symlinks to directories would cause infinite loops)
|
|
67
|
+
if (e.isSymbolicLink()) continue
|
|
68
|
+
if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
|
|
69
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name
|
|
70
|
+
if (e.isDirectory()) {
|
|
71
|
+
yield* walkFiles(join(dir, e.name), relPath)
|
|
72
|
+
} else {
|
|
73
|
+
yield relPath
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------- grep
|
|
79
|
+
|
|
80
|
+
export const grepTool = {
|
|
81
|
+
name: "grep",
|
|
82
|
+
description: DESC("grep"),
|
|
83
|
+
parameters: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
pattern: { type: "string", description: "Regular expression, or a literal string when literal=true" },
|
|
87
|
+
path: { type: "string", description: "Directory or file to search (default cwd)" },
|
|
88
|
+
glob: { type: "string", description: "Only search files matching this glob — supports **, *, ?, [..], {a,b} braces and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
89
|
+
ignoreCase: { type: "boolean", description: "Case-insensitive match (default false)" },
|
|
90
|
+
literal: { type: "boolean", description: "Literal string match — no regex interpretation (default false)" },
|
|
91
|
+
before: { type: "integer", description: "Lines of context to show before each match (grep -B). Default 0" },
|
|
92
|
+
after: { type: "integer", description: "Lines of context to show after each match (grep -A). Default 0" },
|
|
93
|
+
},
|
|
94
|
+
required: ["pattern"],
|
|
95
|
+
},
|
|
96
|
+
readonly: true,
|
|
97
|
+
async execute(args, ctx) {
|
|
98
|
+
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
99
|
+
let regex
|
|
100
|
+
try {
|
|
101
|
+
const pat = args.literal ? escapeRegExp(String(args.pattern)) : args.pattern
|
|
102
|
+
regex = new RegExp(pat, args.ignoreCase ? "i" : "")
|
|
103
|
+
} catch (e) {
|
|
104
|
+
throw new Error(`grep pattern /${args.pattern}/ is not a valid regex: ${e.message}`, { cause: e })
|
|
105
|
+
}
|
|
106
|
+
// §17 调用侧拆分:glob 参数支持空格分隔 include !exclude 多模式(include/exclude 求交)。
|
|
107
|
+
let fileTest = null
|
|
108
|
+
if (args.glob) {
|
|
109
|
+
try {
|
|
110
|
+
const parts = splitGlobPatterns(args.glob)
|
|
111
|
+
if (parts.length > 0) fileTest = compileGlobMatchers(parts).test
|
|
112
|
+
} catch (e) {
|
|
113
|
+
throw new Error(`grep glob /${args.glob}/ is invalid: ${e.message}`, { cause: e })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const before = Math.max(0, Math.floor(args.before ?? 0))
|
|
117
|
+
const after = Math.max(0, Math.floor(args.after ?? 0))
|
|
118
|
+
const wantCtx = before > 0 || after > 0
|
|
119
|
+
const hits = [] // { file, line(1-based), text }
|
|
120
|
+
const fileLines = new Map() // file -> string[] (cached only when context lines requested)
|
|
121
|
+
|
|
122
|
+
async function search(file) {
|
|
123
|
+
let content
|
|
124
|
+
try {
|
|
125
|
+
// Large file guard: skip files over 10MB to prevent OOM
|
|
126
|
+
const fst = await stat(file)
|
|
127
|
+
if (fst.size > 10_000_000) return
|
|
128
|
+
content = normalizeEOL(await readFile(file, "utf8"))
|
|
129
|
+
} catch {
|
|
130
|
+
return // Skip unreadable files; binary files will be read as UTF-8 and searched (may produce garbled matches)
|
|
131
|
+
}
|
|
132
|
+
const lines = content.split("\n")
|
|
133
|
+
if (wantCtx) fileLines.set(file, lines)
|
|
134
|
+
for (let i = 0; i < lines.length; i++) {
|
|
135
|
+
if (regex.test(lines[i])) {
|
|
136
|
+
hits.push({ file, line: i + 1, text: lines[i] })
|
|
137
|
+
if (hits.length >= 200) return
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function walk(target, rel) {
|
|
143
|
+
if (hits.length >= 200) return
|
|
144
|
+
// Use lstat to avoid following symlinks — prevents ./evil → /etc from making grep scan the entire system
|
|
145
|
+
let s
|
|
146
|
+
try { s = await lstat(target) } catch { return }
|
|
147
|
+
if (!s.isDirectory()) {
|
|
148
|
+
// rel 为相对搜索基的路径(排除前缀如 !test/** 必须按目录路径作用——不能用裸文件名);
|
|
149
|
+
// 单文件目标(path 指向文件)时 rel 为空 → 退化为按文件名过滤(既有语义)。
|
|
150
|
+
if (!fileTest || fileTest(rel || target.split(/[\\/]/).pop())) await search(target)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
let entries
|
|
154
|
+
try {
|
|
155
|
+
entries = await readdir(target, { withFileTypes: true })
|
|
156
|
+
} catch {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
for (const e of entries) {
|
|
160
|
+
if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
|
|
161
|
+
await walk(join(target, e.name), rel ? `${rel}/${e.name}` : e.name)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await walk(base, "")
|
|
166
|
+
if (hits.length === 0) return "(no matches)"
|
|
167
|
+
|
|
168
|
+
// No context: keep original path:line: content format
|
|
169
|
+
if (!wantCtx) {
|
|
170
|
+
return truncate(hits.map((h) => `${h.file}:${h.line}: ${h.text}`).join("\n"))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// With context: matching lines use ':', context lines use '-' (like ripgrep); overlapping ranges in same file are merged and de-duplicated
|
|
174
|
+
const fileMatched = new Map() // file -> Set<line>
|
|
175
|
+
for (const h of hits) {
|
|
176
|
+
if (!fileMatched.has(h.file)) fileMatched.set(h.file, new Set())
|
|
177
|
+
fileMatched.get(h.file).add(h.line)
|
|
178
|
+
}
|
|
179
|
+
const out = []
|
|
180
|
+
for (const [file, matchedLines] of fileMatched) {
|
|
181
|
+
const lines = fileLines.get(file) ?? []
|
|
182
|
+
const lineSet = new Set()
|
|
183
|
+
for (const ml of matchedLines) {
|
|
184
|
+
for (let l = Math.max(1, ml - before); l <= Math.min(lines.length, ml + after); l++) lineSet.add(l)
|
|
185
|
+
}
|
|
186
|
+
for (const l of [...lineSet].sort((a, b) => a - b)) {
|
|
187
|
+
const sep = matchedLines.has(l) ? ":" : "-"
|
|
188
|
+
out.push(`${file}${sep}${l}${sep} ${lines[l - 1]}`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return truncate(out.join("\n"))
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const lsTool = {
|
|
196
|
+
name: "ls",
|
|
197
|
+
description: DESC("ls"),
|
|
198
|
+
parameters: {
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {
|
|
201
|
+
path: { type: "string", description: "Directory path (default cwd)" },
|
|
202
|
+
filter: { type: "string", description: "Only list entries matching this glob (e.g. '*.mjs', '*test*')" },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
readonly: true,
|
|
206
|
+
async execute(args, ctx) {
|
|
207
|
+
const abs = resolveInCwd(ctx, args.path ?? ".")
|
|
208
|
+
const filterRe = args.filter ? globToRegex(args.filter) : null
|
|
209
|
+
let entries
|
|
210
|
+
try {
|
|
211
|
+
entries = await readdir(abs, { withFileTypes: true })
|
|
212
|
+
} catch (e) {
|
|
213
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") throw new Error(`ls: ${args.path ?? "."} — ${e.code === "ENOTDIR" ? "not a directory" : "not found"}`, { cause: e })
|
|
214
|
+
throw e
|
|
215
|
+
}
|
|
216
|
+
const rows = await Promise.all(
|
|
217
|
+
entries
|
|
218
|
+
.filter((e) => !filterRe || filterRe.test(e.name))
|
|
219
|
+
.slice(0, 500)
|
|
220
|
+
.map(async (e) => {
|
|
221
|
+
const s = await stat(join(abs, e.name)).catch(() => null)
|
|
222
|
+
const isDir = e.isDirectory()
|
|
223
|
+
return {
|
|
224
|
+
dir: isDir,
|
|
225
|
+
name: e.name + (isDir ? "/" : ""),
|
|
226
|
+
size: s?.size ?? 0,
|
|
227
|
+
mtime: s ? s.mtime.toISOString().slice(0, 16).replace("T", " ") : "?",
|
|
228
|
+
}
|
|
229
|
+
}),
|
|
230
|
+
)
|
|
231
|
+
rows.sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1))
|
|
232
|
+
if (rows.length === 0) return "(no entries)"
|
|
233
|
+
const out = rows.map((r) => `${r.dir ? "d" : "-"} ${r.name.padEnd(40)} ${String(r.size).padStart(10)} ${r.mtime}`)
|
|
234
|
+
return truncate(out.join("\n"))
|
|
235
|
+
},
|
|
236
|
+
}
|
package/src/tools/shared.mjs
CHANGED
|
@@ -363,19 +363,20 @@ export function detectDanger(command) {
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
|
|
366
|
-
/**
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
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
|
+
|
|
379
380
|
|
|
380
381
|
/** Decode a numeric HTML entity to its code point — invalid/out-of-range
|
|
381
382
|
* values (e.g. �) must not throw RangeError; keep the source
|
|
@@ -0,0 +1,22 @@
|
|
|
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).
|