@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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* glob-dialect.mjs — glob 方言(TOOLS.md §17 — 2026-09-06)单一权威模块(CLI)。
|
|
3
|
+
*
|
|
4
|
+
* `{a,b}` brace expansion is handled BEFORE the sentinel/escape flow (braces must
|
|
5
|
+
* not be literal-escaped — a bare "star-star slash star .{js,txt}" pattern
|
|
6
|
+
* previously escaped to a literal that silently matched nothing). Unsupported
|
|
7
|
+
* extglob dialects (`?(x)`/`@(a|b)`/`+(x)`/`*(x)`/`!(x)`) and malformed braces
|
|
8
|
+
* (empty `{}`, unclosed `{`, nested `{a,{b,c}}`) are EXPLICIT errors — never a
|
|
9
|
+
* silent no-match. Exclude prefixes (`!pattern`) are the CALLER's job: the
|
|
10
|
+
* glob/grep tools whitespace-split the pattern expression (splitGlobPatterns —
|
|
11
|
+
* never inside a brace group) and pass the parts to compileGlobMatchers, which
|
|
12
|
+
* intersects includes and excludes.
|
|
13
|
+
*
|
|
14
|
+
* Extracted from shared.mjs (2026-09-06 advisor #5 — shared.mjs exceeded the
|
|
15
|
+
* 500-line cap after §17); shared.mjs re-exports these symbols so all existing
|
|
16
|
+
* importers (system.mjs / ls filter / tests) keep their import paths.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const BRACE_OPEN = "\u0003", BRACE_SEP = "\u0004", BRACE_CLOSE = "\u0005"
|
|
20
|
+
const EXT_GLOB_RE = /[@?+*!]\(/
|
|
21
|
+
|
|
22
|
+
export const GLOB_EXTGLOB_ERROR =
|
|
23
|
+
"glob syntax error: unsupported extglob syntax — ?(x)/@(a|b)/+(x)/*(x)/!(x) are not supported; use {a,b} brace expansion or multiple space-separated patterns instead"
|
|
24
|
+
export const GLOB_EMPTY_BRACE_ERROR =
|
|
25
|
+
'glob syntax error: empty brace group "{}" — put at least one alternative between the braces (e.g. {js,txt})'
|
|
26
|
+
export const GLOB_UNCLOSED_BRACE_ERROR =
|
|
27
|
+
'glob syntax error: unclosed brace group — add a matching "}" (e.g. {js,txt})'
|
|
28
|
+
export const GLOB_NESTED_BRACE_ERROR =
|
|
29
|
+
"glob syntax error: nested brace groups are not supported (e.g. {a,{b,c}}) — use a flat alternative list (e.g. {a,b,c})"
|
|
30
|
+
|
|
31
|
+
function assertNoExtglob(pattern) {
|
|
32
|
+
if (EXT_GLOB_RE.test(pattern)) throw new Error(GLOB_EXTGLOB_ERROR)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Replace every {a,b,c} group with placeholder alternation markers. Runs BEFORE
|
|
36
|
+
* the sentinel/escape flow so the inserted alternation survives literal escaping. */
|
|
37
|
+
function expandBraces(pattern) {
|
|
38
|
+
for (;;) {
|
|
39
|
+
const open = pattern.indexOf("{")
|
|
40
|
+
if (open === -1) return pattern
|
|
41
|
+
let close = -1
|
|
42
|
+
for (let j = open + 1; j < pattern.length; j++) {
|
|
43
|
+
if (pattern[j] === "{") throw new Error(GLOB_NESTED_BRACE_ERROR)
|
|
44
|
+
if (pattern[j] === "}") { close = j; break }
|
|
45
|
+
}
|
|
46
|
+
if (close === -1) throw new Error(GLOB_UNCLOSED_BRACE_ERROR)
|
|
47
|
+
const inner = pattern.slice(open + 1, close)
|
|
48
|
+
if (inner === "") throw new Error(GLOB_EMPTY_BRACE_ERROR)
|
|
49
|
+
const alts = inner.split(",").map((a) => a.trim())
|
|
50
|
+
pattern = pattern.slice(0, open) + BRACE_OPEN + alts.join(BRACE_SEP) + BRACE_CLOSE + pattern.slice(close + 1)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Convert a single glob pattern to a RegExp (anchored full match). Supports the
|
|
55
|
+
* recursive "star-star" forms, single-star, "?", "[..]" character classes and
|
|
56
|
+
* {a,b} brace expansion. Malformed braces and unsupported extglob syntax throw
|
|
57
|
+
* explicit errors (no silent mismatch). */
|
|
58
|
+
export function globToRegex(pattern) {
|
|
59
|
+
pattern = String(pattern)
|
|
60
|
+
assertNoExtglob(pattern)
|
|
61
|
+
pattern = expandBraces(pattern)
|
|
62
|
+
// Sentinel chars: \u0001/\u0002 never appear in real glob patterns (they
|
|
63
|
+
// come from model output or the filesystem) — safe as **/ and ** placeholders.
|
|
64
|
+
const DS = "\u0001", DP = "\u0002"
|
|
65
|
+
const escaped = pattern
|
|
66
|
+
.replace(/\*\*\//g, DS).replace(/\*\*/g, DP)
|
|
67
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
68
|
+
.replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")
|
|
69
|
+
.replace(new RegExp(DS, "g"), "(?:.+/)?")
|
|
70
|
+
.replace(new RegExp(DP, "g"), ".*")
|
|
71
|
+
.replace(new RegExp(BRACE_OPEN, "g"), "(?:")
|
|
72
|
+
.replace(new RegExp(BRACE_SEP, "g"), "|")
|
|
73
|
+
.replace(new RegExp(BRACE_CLOSE, "g"), ")")
|
|
74
|
+
return new RegExp(`^${escaped}$`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Whitespace-split a glob expression into parts WITHOUT splitting inside {a,b}
|
|
78
|
+
* brace groups and WITHOUT splitting literal spaces inside a single pattern
|
|
79
|
+
* (2026-09-06 audit F1 / advisor #4): a whitespace run is a pattern separator
|
|
80
|
+
* ONLY when the next non-space token starts an exclusion ("!" — the §17
|
|
81
|
+
* "include !exclude" form). So a BARE pattern like "docs/my file/*.md" and an
|
|
82
|
+
* exclude like "!docs/my file/**" each stay ONE part; the standard
|
|
83
|
+
* "js-pattern 空格 !test-pattern" form still splits into its two parts. */
|
|
84
|
+
export function splitGlobPatterns(expr) {
|
|
85
|
+
const s = String(expr ?? "").trim()
|
|
86
|
+
if (!s) return []
|
|
87
|
+
const parts = []
|
|
88
|
+
let depth = 0
|
|
89
|
+
let cur = ""
|
|
90
|
+
for (let i = 0; i < s.length; i++) {
|
|
91
|
+
const ch = s[i]
|
|
92
|
+
if (ch === "{") depth++
|
|
93
|
+
else if (ch === "}") depth = Math.max(0, depth - 1)
|
|
94
|
+
if (/\s/.test(ch) && depth === 0) {
|
|
95
|
+
let j = i
|
|
96
|
+
while (j < s.length && /\s/.test(s[j])) j++
|
|
97
|
+
if (s[j] === "!") {
|
|
98
|
+
if (cur) { parts.push(cur); cur = "" }
|
|
99
|
+
i = j - 1 // next loop iteration continues at the "!"
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
// literal space inside a pattern (or trailing) — keep it in the current part
|
|
103
|
+
cur += ch
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
cur += ch
|
|
107
|
+
}
|
|
108
|
+
if (cur) parts.push(cur)
|
|
109
|
+
return parts
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Compile whitespace-split pattern parts (see splitGlobPatterns) into a matcher:
|
|
113
|
+
* non-"!" parts are includes, leading-"!" parts are excludes — a rel path matches
|
|
114
|
+
* when it matches ≥1 include (or there are no includes) and no exclude. Every
|
|
115
|
+
* part runs the full brace/error dialect through globToRegex (excludes too). */
|
|
116
|
+
export function compileGlobMatchers(parts) {
|
|
117
|
+
const includes = []
|
|
118
|
+
const excludes = []
|
|
119
|
+
for (const p of parts) {
|
|
120
|
+
const part = String(p)
|
|
121
|
+
if (part.startsWith("!")) excludes.push(part.slice(1))
|
|
122
|
+
else if (part !== "") includes.push(part)
|
|
123
|
+
}
|
|
124
|
+
const include = includes.map((p) => globToRegex(p))
|
|
125
|
+
const exclude = excludes.map((p) => globToRegex(p))
|
|
126
|
+
const test = (relPath) =>
|
|
127
|
+
(include.length === 0 || include.some((re) => re.test(relPath))) &&
|
|
128
|
+
!exclude.some((re) => re.test(relPath))
|
|
129
|
+
return { include, exclude, test }
|
|
130
|
+
}
|
package/tools/index.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// tools/index.mjs — backend-compatible re-export
|
|
2
|
+
export { toOpenAISchema } from "./shared.mjs";
|
|
3
|
+
|
|
4
|
+
import { readTool, writeTool, editTool, insertAfterTool, hashlineEditTool, readImageTool } from "./file.mjs";
|
|
5
|
+
import { applyPatchTool, deleteTool } from "./patch.mjs";
|
|
6
|
+
import { bashTool } from "./bash.mjs";
|
|
7
|
+
import { globTool, grepTool, lsTool } from "./search.mjs";
|
|
8
|
+
import { websearchTool, fetchTool } from "./web.mjs";
|
|
9
|
+
import { gitTool } from "./git.mjs";
|
|
10
|
+
import { questionTool } from "./question.mjs";
|
|
11
|
+
import { lintTool } from "./linter.mjs";
|
|
12
|
+
import { lspTool } from "./lsp.mjs";
|
|
13
|
+
import { executeTool } from "./execute.mjs";
|
|
14
|
+
import { fileOpsTool, processTool, getCurrentTimeTool, waitForTool } from "./ops.mjs";
|
|
15
|
+
import { treeTool } from "./tree.mjs";
|
|
16
|
+
|
|
17
|
+
// Instance-independent built-in table. `read_image` is deliberately NOT here:
|
|
18
|
+
// its registration is capability-gated per model — see `assembleBuiltinTools`.
|
|
19
|
+
export const builtinTools = [
|
|
20
|
+
readTool, writeTool, editTool, insertAfterTool, hashlineEditTool, applyPatchTool,
|
|
21
|
+
bashTool, globTool, grepTool,
|
|
22
|
+
websearchTool, lsTool, fetchTool, deleteTool,
|
|
23
|
+
gitTool, questionTool,
|
|
24
|
+
lintTool, lspTool, executeTool,
|
|
25
|
+
fileOpsTool, processTool, getCurrentTimeTool, waitForTool,
|
|
26
|
+
treeTool,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
readTool, writeTool, editTool, insertAfterTool, hashlineEditTool, applyPatchTool,
|
|
31
|
+
readImageTool, bashTool, globTool, grepTool,
|
|
32
|
+
websearchTool, lsTool, fetchTool, deleteTool,
|
|
33
|
+
gitTool, questionTool,
|
|
34
|
+
lintTool, lspTool, executeTool,
|
|
35
|
+
fileOpsTool, processTool, getCurrentTimeTool, waitForTool,
|
|
36
|
+
treeTool,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Full built-in registry (CORE-UNIFICATION TOOLS #70) ─────────────────────
|
|
40
|
+
// The registry owns the COMPLETE built-in tool face for both shells. The VS Code
|
|
41
|
+
// index registers the full table in one place; the CLI splits it between this
|
|
42
|
+
// index and its consumer assembly (`cli/make-agent.mjs`). That consumer assembly
|
|
43
|
+
// face moves here (`assembleBuiltinTools`) so both shells consume one registry:
|
|
44
|
+
// `builtinTools` = the instance-independent static table, plus the instance-bound
|
|
45
|
+
// faces (memory / code+doc search / repo outline / settings / peer instances)
|
|
46
|
+
// whose factories need the shell's memory handle at run time.
|
|
47
|
+
//
|
|
48
|
+
// Host-only tools (VS Code `context` / `focus` — TOOLS #179 ④, IDE capabilities)
|
|
49
|
+
// are NOT part of the core registry: the VS Code shell adds them itself.
|
|
50
|
+
//
|
|
51
|
+
// `read_image` registration follows VS Code (#70): it is registered only when the
|
|
52
|
+
// model accepts image input (`specForModel(model).multimodal`) — the conservative
|
|
53
|
+
// default spec carries no `multimodal`, so an unknown or absent model does NOT get
|
|
54
|
+
// it. The runtime gate in `tools/file.mjs` stays as a second line of defence.
|
|
55
|
+
export async function assembleBuiltinTools({ memory, cwd, projectDir = null, author = "unknown", team = null, model = null } = {}) {
|
|
56
|
+
const { specForModel } = await import("../model-specs.mjs");
|
|
57
|
+
const { memoryTools, codeSearchTool, docSearchTool } = await import("../memory.mjs");
|
|
58
|
+
const { repoOutlineTool } = await import("./repomap.mjs");
|
|
59
|
+
const { settingsTool } = await import("../agent-tools/settings.mjs");
|
|
60
|
+
const { peerInstancesTool } = await import("../peer-instances.mjs");
|
|
61
|
+
const { ledgerQueryTool, ledgerCountTool } = await import("../ledger.mjs"); // 动态 import(ledger 链静态达 node:sqlite——W8 契约②)
|
|
62
|
+
const imageOk = Boolean(specForModel(model)?.multimodal);
|
|
63
|
+
return [
|
|
64
|
+
...builtinTools,
|
|
65
|
+
...(imageOk ? [readImageTool] : []),
|
|
66
|
+
...memoryTools(memory, { cwd, projectDir, author, team }),
|
|
67
|
+
codeSearchTool(memory),
|
|
68
|
+
docSearchTool(memory),
|
|
69
|
+
repoOutlineTool(memory.db, cwd),
|
|
70
|
+
settingsTool(),
|
|
71
|
+
peerInstancesTool,
|
|
72
|
+
// 台账查询命令族(M2 设计 §2.2 命令面接线——查询命令全角色面;写命令住 family-tools depthOnly 分支)
|
|
73
|
+
ledgerQueryTool,
|
|
74
|
+
ledgerCountTool,
|
|
75
|
+
];
|
|
76
|
+
}
|
package/tools/linter.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { DESC, resolveInCwd } from "./shared.mjs"
|
|
2
|
+
// #61(可中断执行按端注入——CORE-UNIFICATION §2.13.4):检查器执行全部经核内执行面单点
|
|
3
|
+
// `runCommand`(默认径 = execFileSync,CLI 语义零行为变;端侧 injected ⇒ 可中断执行器)。
|
|
4
|
+
import { runCommand } from "./exec-run.mjs"
|
|
5
|
+
import { existsSync } from "node:fs"
|
|
6
|
+
import { join } from "node:path"
|
|
7
|
+
|
|
8
|
+
export const lintTool = {
|
|
9
|
+
name: "lint",
|
|
10
|
+
description: DESC("lint"),
|
|
11
|
+
parameters: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
path: { type: "string", description: "File to check (default: most recently modified file)" },
|
|
15
|
+
full: { type: "boolean", description: "Run the full language-aware cascade instead of just node --check (default false)" },
|
|
16
|
+
},
|
|
17
|
+
required: [],
|
|
18
|
+
},
|
|
19
|
+
readonly: true,
|
|
20
|
+
async execute(args, ctx) {
|
|
21
|
+
const abs = args.path
|
|
22
|
+
? resolveInCwd(ctx, args.path)
|
|
23
|
+
: (ctx.agent?._touchedFiles?.at(-1) || null)
|
|
24
|
+
if (!abs) return "lint: no file specified and no recently modified file to check"
|
|
25
|
+
|
|
26
|
+
if (!args.full) {
|
|
27
|
+
// Fast path: node --check only
|
|
28
|
+
return nodeCheckResult(abs, ctx.signal)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Full cascade: language-aware (tsc → node --check, ruff, cargo, go vet —
|
|
32
|
+
// third-party linter cascade removed 2026-09-02, TOOLS.md §10.2: zero-dependency lint)
|
|
33
|
+
const ext = abs.split(".").pop()?.toLowerCase()
|
|
34
|
+
const checkers = LANG_CHECKERS[ext]
|
|
35
|
+
if (!checkers) return nodeCheckResult(abs, ctx.signal) // fall back to node --check
|
|
36
|
+
|
|
37
|
+
for (const checker of checkers) {
|
|
38
|
+
const result = await checker(abs, { cwd: ctx.cwd, signal: ctx.signal, existsSync, join, run: runCommand })
|
|
39
|
+
if (result !== null) return result
|
|
40
|
+
}
|
|
41
|
+
return `lint: no linter available for ${abs}. Install one?`
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function nodeCheckResult(abs, signal) {
|
|
46
|
+
if (!/\.(?:m?js|cjs|m?ts|cts|jsx|tsx)$/.test(abs)) {
|
|
47
|
+
return `lint (check): only JS/TS-family files supported for fast syntax check; use full=true for other languages. Path: ${abs}`
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await runCommand(process.execPath, ["--check", abs], { timeout: 10000, signal })
|
|
51
|
+
return `Syntax OK: ${abs}`
|
|
52
|
+
} catch (e) {
|
|
53
|
+
const msg = (e.stderr || e.stdout || e.message || "").trim()
|
|
54
|
+
return `Syntax error in ${abs}:\n${msg || "(unknown)"}`
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Full-check cascade checkers (third-party linter branch removed 2026-09-02, TOOLS.md §10.2) ──────
|
|
59
|
+
|
|
60
|
+
async function tscCheck(file, { cwd, signal, existsSync, join, run }) {
|
|
61
|
+
if (!existsSync(join(cwd, "tsconfig.json"))) return null
|
|
62
|
+
if (!/\.(ts|tsx|mts|cts)$/.test(file)) return null
|
|
63
|
+
try {
|
|
64
|
+
await run("npx", ["tsc", "--noEmit", "--pretty", "false"], { cwd, timeout: 60000, signal })
|
|
65
|
+
return "✓ tsc: no type errors"
|
|
66
|
+
} catch (e) {
|
|
67
|
+
const stdout = (e.stdout || "").trim()
|
|
68
|
+
if (stdout) return stdout
|
|
69
|
+
return `✗ tsc: ${(e.stderr || e.message).slice(0, 500)}`
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function ruffCheck(file, { cwd, signal, run }) {
|
|
74
|
+
if (!/\.py$/.test(file)) return null
|
|
75
|
+
try {
|
|
76
|
+
await run("ruff", ["check", "--output-format", "concise", file], { cwd, timeout: 30000, signal })
|
|
77
|
+
return "✓ ruff: no issues"
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (e.code === "ENOENT") return "lint: ruff not installed. Run: pip install ruff"
|
|
80
|
+
const stdout = (e.stdout || "").trim()
|
|
81
|
+
if (stdout) return stdout
|
|
82
|
+
return `✗ ruff: ${(e.stderr || e.message).slice(0, 500)}`
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function cargoCheck(file, { cwd, signal, existsSync, join, run }) {
|
|
87
|
+
if (!/\.rs$/.test(file)) return null
|
|
88
|
+
if (!existsSync(join(cwd, "Cargo.toml"))) return null
|
|
89
|
+
const fname = file.split(/[\\/]/).pop()
|
|
90
|
+
try {
|
|
91
|
+
const out = await run("cargo", ["check", "--message-format", "short"], { cwd, timeout: 120000, signal })
|
|
92
|
+
const errors = out.split("\n").filter(l => l.includes(fname))
|
|
93
|
+
return errors.length > 0 ? errors.join("\n") : "✓ cargo check: no errors"
|
|
94
|
+
} catch (e) {
|
|
95
|
+
const combined = ((e.stdout || "") + "\n" + (e.stderr || "")).trim()
|
|
96
|
+
const errors = combined.split("\n").filter(l => l.includes(fname) || l.startsWith("error"))
|
|
97
|
+
return errors.length > 0 ? errors.join("\n") : `✗ cargo check failed:\n${combined.slice(0, 1000)}`
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function goVet(file, { cwd, signal, run }) {
|
|
102
|
+
if (!/\.go$/.test(file)) return null
|
|
103
|
+
try {
|
|
104
|
+
await run("go", ["vet", file], { cwd, timeout: 60000, signal })
|
|
105
|
+
return "✓ go vet: no issues"
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return `✗ go vet: ${(e.stderr || e.message).slice(0, 500)}`
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const LANG_CHECKERS = {
|
|
112
|
+
// js/mjs/cjs/jsx fall back to node --check (no entry — third-party linter cascade removed 2026-09-02, TOOLS.md §10.2)
|
|
113
|
+
ts: [tscCheck],
|
|
114
|
+
tsx: [tscCheck],
|
|
115
|
+
mts: [tscCheck],
|
|
116
|
+
cts: [tscCheck],
|
|
117
|
+
py: [ruffCheck],
|
|
118
|
+
rs: [cargoCheck],
|
|
119
|
+
go: [goVet],
|
|
120
|
+
}
|
package/tools/lsp.mjs
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/lsp.mjs — LSP (Language Server Protocol) code intelligence tool
|
|
3
|
+
* Zero-dependency JSON-RPC 2.0 over stdio client.
|
|
4
|
+
*
|
|
5
|
+
* Provides: go-to-definition, find-references, hover info, document symbols, diagnostics.
|
|
6
|
+
* Lazy-starts language servers on first call. Configurable via config.json lsp.servers.
|
|
7
|
+
*
|
|
8
|
+
* Config format:
|
|
9
|
+
* "lsp": {
|
|
10
|
+
* "servers": {
|
|
11
|
+
* "typescript": { "command": "typescript-language-server", "args": ["--stdio"] },
|
|
12
|
+
* "python": { "command": "pyright-langserver", "args": ["--stdio"] }
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawn } from "node:child_process"
|
|
18
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
19
|
+
import { join, extname } from "node:path"
|
|
20
|
+
import { DESC } from "./shared.mjs"
|
|
21
|
+
|
|
22
|
+
// ─── 宿主语言服务径注入缝(#66——「宿主语言服务径按端注入」,形态参 §2.13.5 注入缝)──────
|
|
23
|
+
/**
|
|
24
|
+
* 宿主语言服务桥注入位(**缺省不覆盖** = 核内 JSON-RPC over stdio 径——CLI 语义,零行为变;
|
|
25
|
+
* 端装配层可覆盖为宿主语言服务径)。核内零端名分支(契约 5——本档只认 `handle` 函数名)。
|
|
26
|
+
* 契约:`handle(args, ctx) → string | null | undefined | Promise<同>`——null / undefined =
|
|
27
|
+
* 未处理 ⇒ 回核内径(其余返回值为该次调用的工具结果)。
|
|
28
|
+
*/
|
|
29
|
+
let injectedHost = null
|
|
30
|
+
export function configureLspHost(impl) {
|
|
31
|
+
injectedHost = impl && typeof impl.handle === "function" ? impl.handle : null
|
|
32
|
+
}
|
|
33
|
+
/** 撤销注入(测试与端装配生命周期用——缺省态 = 核内 JSON-RPC 径)。 */
|
|
34
|
+
export function resetLspHost() { injectedHost = null }
|
|
35
|
+
|
|
36
|
+
// ---- JSON-RPC transport over stdio ----
|
|
37
|
+
|
|
38
|
+
/** Send a JSON-RPC request to the server via stdin */
|
|
39
|
+
function send(proc, message) {
|
|
40
|
+
const body = JSON.stringify(message)
|
|
41
|
+
const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`
|
|
42
|
+
proc.stdin.write(header + body)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read one JSON-RPC message from stdout. Returns parsed JSON, or null on EOF. */
|
|
46
|
+
function readMessage(proc) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
let header = ""
|
|
49
|
+
let contentLength = -1
|
|
50
|
+
|
|
51
|
+
const onData = (chunk) => {
|
|
52
|
+
if (contentLength < 0) {
|
|
53
|
+
header += chunk.toString()
|
|
54
|
+
const match = header.match(/Content-Length: (\d+)\r\n\r\n/)
|
|
55
|
+
if (match) {
|
|
56
|
+
contentLength = parseInt(match[1])
|
|
57
|
+
const bodyStart = header.indexOf("\r\n\r\n") + 4
|
|
58
|
+
const remaining = header.slice(bodyStart)
|
|
59
|
+
header = ""
|
|
60
|
+
if (remaining.length >= contentLength) {
|
|
61
|
+
proc.stdout.removeListener("data", onData)
|
|
62
|
+
try { resolve(JSON.parse(remaining.slice(0, contentLength))) } catch { resolve(null) }
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
// Need more data — leave remaining in a buffer-like state
|
|
66
|
+
proc.stdout.removeListener("data", onData)
|
|
67
|
+
readBody(proc, remaining, contentLength).then(resolve)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
proc.stdout.on("data", onData)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readBody(proc, buf, targetLen) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const onData = (chunk) => {
|
|
79
|
+
buf += chunk.toString()
|
|
80
|
+
if (buf.length >= targetLen) {
|
|
81
|
+
proc.stdout.removeListener("data", onData)
|
|
82
|
+
try { resolve(JSON.parse(buf.slice(0, targetLen))) } catch { resolve(null) }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
proc.stdout.on("data", onData)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Send a request and wait for the matching response */
|
|
90
|
+
async function request(proc, method, params, id) {
|
|
91
|
+
send(proc, { jsonrpc: "2.0", id, method, params })
|
|
92
|
+
while (true) {
|
|
93
|
+
const msg = await readMessage(proc)
|
|
94
|
+
if (!msg) return null
|
|
95
|
+
if (msg.id === id) return msg
|
|
96
|
+
// Store notifications for later retrieval (diagnostics)
|
|
97
|
+
if (msg.method === "textDocument/publishDiagnostics") {
|
|
98
|
+
proc._diagnostics = proc._diagnostics || {}
|
|
99
|
+
proc._diagnostics[msg.params.uri] = msg.params.diagnostics
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Send a notification (no response expected) */
|
|
105
|
+
function notify(proc, method, params) {
|
|
106
|
+
send(proc, { jsonrpc: "2.0", method, params })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- Language server process management ----
|
|
110
|
+
|
|
111
|
+
const servers = new Map() // ext → { proc, rootUri, ready }
|
|
112
|
+
|
|
113
|
+
/** Convert file path to file:// URI */
|
|
114
|
+
function toUri(absPath) {
|
|
115
|
+
return "file:///" + absPath.replace(/\\/g, "/").replace(/^([A-Z]):/, (_, d) => d.toLowerCase() + "%3A")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Resolve which language server to use for a file extension */
|
|
119
|
+
function resolveServerConfig(config, ext) {
|
|
120
|
+
const map = {
|
|
121
|
+
".js": "typescript", ".mjs": "typescript", ".cjs": "typescript",
|
|
122
|
+
".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
|
|
123
|
+
".py": "python", ".pyi": "python",
|
|
124
|
+
".rs": "rust",
|
|
125
|
+
".go": "go",
|
|
126
|
+
}
|
|
127
|
+
const key = map[ext] || ext.slice(1)
|
|
128
|
+
const servers = config?.lsp?.servers ?? {}
|
|
129
|
+
return servers[key] || null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Start or reuse a language server for the given file */
|
|
133
|
+
async function getServer(cwd, filePath, config) {
|
|
134
|
+
const ext = extname(filePath).toLowerCase()
|
|
135
|
+
const srvConfig = resolveServerConfig(config, ext)
|
|
136
|
+
if (!srvConfig) return null
|
|
137
|
+
|
|
138
|
+
const abs = join(cwd, ...filePath.split("/"))
|
|
139
|
+
if (!existsSync(abs)) return null
|
|
140
|
+
|
|
141
|
+
const rootUri = toUri(cwd)
|
|
142
|
+
const key = `${ext}:${cwd}`
|
|
143
|
+
let entry = servers.get(key)
|
|
144
|
+
|
|
145
|
+
if (entry && entry.ready) return entry
|
|
146
|
+
|
|
147
|
+
// Start new server
|
|
148
|
+
const proc = spawn(srvConfig.command, srvConfig.args || [], {
|
|
149
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
150
|
+
windowsHide: true,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
proc.stderr.on("data", () => {}) // suppress stderr noise
|
|
154
|
+
|
|
155
|
+
entry = { proc, rootUri, ready: false }
|
|
156
|
+
servers.set(key, entry)
|
|
157
|
+
|
|
158
|
+
// Initialize handshake
|
|
159
|
+
const initResult = await request(proc, "initialize", {
|
|
160
|
+
processId: null,
|
|
161
|
+
rootUri,
|
|
162
|
+
capabilities: {
|
|
163
|
+
textDocument: {
|
|
164
|
+
definition: { linkSupport: false },
|
|
165
|
+
references: {},
|
|
166
|
+
hover: { contentFormat: ["plaintext"] },
|
|
167
|
+
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
workspace: {},
|
|
171
|
+
}, 1)
|
|
172
|
+
|
|
173
|
+
if (!initResult) {
|
|
174
|
+
proc.kill()
|
|
175
|
+
servers.delete(key)
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
notify(proc, "initialized", {})
|
|
180
|
+
entry.ready = true
|
|
181
|
+
return entry
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Notify the server that a file is open (required before queries) */
|
|
185
|
+
async function ensureOpen(proc, uri, ext) {
|
|
186
|
+
const langMap = { ".js": "javascript", ".mjs": "javascript", ".cjs": "javascript", ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript", ".py": "python", ".rs": "rust", ".go": "go" }
|
|
187
|
+
const languageId = langMap[ext] || ext.slice(1)
|
|
188
|
+
notify(proc, "textDocument/didOpen", {
|
|
189
|
+
textDocument: {
|
|
190
|
+
uri,
|
|
191
|
+
languageId,
|
|
192
|
+
version: 1,
|
|
193
|
+
text: readFileSync(uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":").replace(/\//g, "\\"), "utf8"),
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---- Tool definition ----
|
|
199
|
+
|
|
200
|
+
export const lspTool = {
|
|
201
|
+
name: "lsp",
|
|
202
|
+
description: DESC("lsp"),
|
|
203
|
+
parameters: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
subcommand: {
|
|
207
|
+
type: "string",
|
|
208
|
+
enum: ["definition", "references", "hover", "symbols", "diagnostics"],
|
|
209
|
+
description: "LSP operation to perform",
|
|
210
|
+
},
|
|
211
|
+
uri: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: "Target file path (relative to project root)",
|
|
214
|
+
},
|
|
215
|
+
line: {
|
|
216
|
+
type: "integer",
|
|
217
|
+
description: "1-based line number (for definition/references/hover)",
|
|
218
|
+
},
|
|
219
|
+
character: {
|
|
220
|
+
type: "integer",
|
|
221
|
+
description: "1-based character offset (for definition/references/hover)",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
required: ["subcommand", "uri"],
|
|
225
|
+
},
|
|
226
|
+
readonly: true,
|
|
227
|
+
|
|
228
|
+
async execute(args, ctx) {
|
|
229
|
+
const config = ctx.agent?.config ?? {}
|
|
230
|
+
const cwd = ctx.cwd
|
|
231
|
+
const filePath = args.uri
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
// 宿主语言服务径(#66):注入面命中(非 null/undefined)⇒ 以其返回值为准;未处理回核内径。
|
|
235
|
+
if (injectedHost) {
|
|
236
|
+
const out = await injectedHost(args, ctx)
|
|
237
|
+
if (out !== null && out !== undefined) return out
|
|
238
|
+
}
|
|
239
|
+
const entry = await getServer(cwd, filePath, config)
|
|
240
|
+
if (!entry) {
|
|
241
|
+
const ext = extname(filePath).toLowerCase()
|
|
242
|
+
return `No LSP server configured for "${ext}" files. Add one to config.json:\n"lsp": { "servers": { "${ext.slice(1)}": { "command": "...", "args": ["--stdio"] } } }`
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const { proc } = entry
|
|
246
|
+
const abs = join(cwd, ...filePath.split("/"))
|
|
247
|
+
const uri = toUri(abs)
|
|
248
|
+
const ext = extname(filePath).toLowerCase()
|
|
249
|
+
await ensureOpen(proc, uri, ext)
|
|
250
|
+
|
|
251
|
+
// Wait briefly for diagnostics to arrive
|
|
252
|
+
await new Promise((r) => setTimeout(r, 300))
|
|
253
|
+
|
|
254
|
+
switch (args.subcommand) {
|
|
255
|
+
case "definition": {
|
|
256
|
+
if (!args.line || !args.character) return "Error: line and character required for definition"
|
|
257
|
+
const res = await request(proc, "textDocument/definition", {
|
|
258
|
+
textDocument: { uri },
|
|
259
|
+
position: { line: args.line - 1, character: args.character - 1 },
|
|
260
|
+
}, 10)
|
|
261
|
+
if (!res?.result) return "(no definition found)"
|
|
262
|
+
const locs = Array.isArray(res.result) ? res.result : [res.result]
|
|
263
|
+
return locs.map((l) => {
|
|
264
|
+
const path = l.uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":")
|
|
265
|
+
return `${path}:${l.range.start.line + 1}:${l.range.start.character + 1}`
|
|
266
|
+
}).join("\n")
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case "references": {
|
|
270
|
+
if (!args.line || !args.character) return "Error: line and character required for references"
|
|
271
|
+
const res = await request(proc, "textDocument/references", {
|
|
272
|
+
textDocument: { uri },
|
|
273
|
+
position: { line: args.line - 1, character: args.character - 1 },
|
|
274
|
+
context: { includeDeclaration: false },
|
|
275
|
+
}, 10)
|
|
276
|
+
if (!res?.result?.length) return "(no references found)"
|
|
277
|
+
return res.result.slice(0, 50).map((l) => {
|
|
278
|
+
const path = l.uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":")
|
|
279
|
+
return `${path}:${l.range.start.line + 1}:${l.range.start.character + 1}`
|
|
280
|
+
}).join("\n") + (res.result.length > 50 ? `\n... and ${res.result.length - 50} more` : "")
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
case "hover": {
|
|
284
|
+
if (!args.line || !args.character) return "Error: line and character required for hover"
|
|
285
|
+
const res = await request(proc, "textDocument/hover", {
|
|
286
|
+
textDocument: { uri },
|
|
287
|
+
position: { line: args.line - 1, character: args.character - 1 },
|
|
288
|
+
}, 10)
|
|
289
|
+
if (!res?.result?.contents) return "(no hover info)"
|
|
290
|
+
const contents = res.result.contents
|
|
291
|
+
if (typeof contents === "string") return contents
|
|
292
|
+
if (Array.isArray(contents)) return contents.map((c) => typeof c === "string" ? c : c.value).join("\n")
|
|
293
|
+
if (contents.value) return contents.value
|
|
294
|
+
return JSON.stringify(contents)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
case "symbols": {
|
|
298
|
+
const res = await request(proc, "textDocument/documentSymbol", {
|
|
299
|
+
textDocument: { uri },
|
|
300
|
+
}, 10)
|
|
301
|
+
if (!res?.result?.length) return "(no symbols found)"
|
|
302
|
+
function render(nodes, depth) {
|
|
303
|
+
const lines = []
|
|
304
|
+
for (const n of nodes) {
|
|
305
|
+
const kind = n.kind != null ? ` [${symbolKind(n.kind)}]` : ""
|
|
306
|
+
lines.push(`${" ".repeat(depth)}${n.name}${kind} — L${n.range.start.line + 1}`)
|
|
307
|
+
if (n.children?.length) lines.push(...render(n.children, depth + 1))
|
|
308
|
+
}
|
|
309
|
+
return lines
|
|
310
|
+
}
|
|
311
|
+
return render(res.result, 0).join("\n")
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
case "diagnostics": {
|
|
315
|
+
const diags = proc._diagnostics?.[uri]
|
|
316
|
+
if (!diags?.length) return "(no diagnostics)"
|
|
317
|
+
return diags.slice(0, 30).map((d) => {
|
|
318
|
+
const sev = { 1: "ERROR", 2: "WARN", 3: "INFO", 4: "HINT" }[d.severity] || "?"
|
|
319
|
+
return `L${d.range.start.line + 1}: ${sev}: ${d.message}${d.code ? ` [${d.code}]` : ""}`
|
|
320
|
+
}).join("\n") + (diags.length > 30 ? `\n... and ${diags.length - 30} more` : "")
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
default:
|
|
324
|
+
return `Unknown subcommand: ${args.subcommand}`
|
|
325
|
+
}
|
|
326
|
+
} catch (err) {
|
|
327
|
+
return `lsp error: ${err.message}`
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function symbolKind(k) {
|
|
333
|
+
const kinds = { 1: "file", 2: "module", 3: "namespace", 4: "package", 5: "class", 6: "method", 7: "property", 8: "field", 9: "constructor", 10: "enum", 11: "interface", 12: "function", 13: "variable", 14: "constant", 15: "string", 16: "number", 17: "boolean", 18: "array", 19: "object", 20: "key", 21: "null", 22: "enumMember", 23: "struct", 24: "event", 25: "operator", 26: "typeParameter" }
|
|
334
|
+
return kinds[k] || `kind-${k}`
|
|
335
|
+
}
|