github-router 0.3.71 → 0.3.73
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/README.md +7 -6
- package/dist/browser-ext/background.js +1 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/lib/tree-sitter-pool/worker.js +543 -0
- package/dist/lifecycle-BL4rWSrT.js +4 -0
- package/dist/{lifecycle-NQRdfY1u.js → lifecycle-CMPthagV.js} +2 -2
- package/dist/{lifecycle-NQRdfY1u.js.map → lifecycle-CMPthagV.js.map} +1 -1
- package/dist/{lifecycle-C2kZwv-z.js → lifecycle-CQlm3YlF.js} +2 -2
- package/dist/lifecycle-yaqqtsV1.js +452 -0
- package/dist/lifecycle-yaqqtsV1.js.map +1 -0
- package/dist/main.js +4657 -1709
- package/dist/main.js.map +1 -1
- package/dist/{paths-DhM3Yi80.js → paths-BGx0RpNs.js} +1 -1
- package/dist/{paths-CoFnpNZl.js → paths-yJ97KlKp.js} +26 -3
- package/dist/paths-yJ97KlKp.js.map +1 -0
- package/package.json +1 -1
- package/dist/paths-CoFnpNZl.js.map +0 -1
package/README.md
CHANGED
|
@@ -79,27 +79,28 @@ Then run `claude` as normal.
|
|
|
79
79
|
|
|
80
80
|
`github-router claude` auto-wires four peer-model adversarial reviewers plus a coordinator into the spawned Claude Code session. No setup, no prior MCP config, no `.claude/agents/` files needed — they appear as Task `subagent_type` options the session can delegate to. Opt out with `--no-codex-mcp`.
|
|
81
81
|
|
|
82
|
-
Each persona is exposed both as a Claude Code subagent (callable via the `Task` tool) AND as an MCP tool at `
|
|
82
|
+
Each persona is exposed both as a Claude Code subagent (callable via the `Task` tool) AND as an MCP tool at `mcp__peers__<name>`. Personas are stateless: each invocation runs a fresh request against its model with a baked persona prompt — they have no access to your scrollback or project memory, so the lead must paste the artifact into the brief.
|
|
83
83
|
|
|
84
84
|
| Subagent | Model | Endpoint | Effort tiers (default) |
|
|
85
85
|
|---|---|---|---|
|
|
86
86
|
| `codex-critic` | gpt-5.5 | `/v1/responses` | low \| medium \| high \| xhigh (xhigh) |
|
|
87
87
|
| `codex-reviewer` | gpt-5.3-codex | `/v1/responses` | low \| medium \| high \| xhigh (xhigh) |
|
|
88
|
+
| `gemini-reviewer` | gemini-3.1-pro-preview | `/v1/chat/completions` | low \| medium \| high (high) |
|
|
88
89
|
| `opus-critic` | claude-opus-4-6 | `/v1/messages` | low \| medium \| high (high) |
|
|
89
90
|
| `gemini-critic` | gemini-3.1-pro-preview | `/v1/chat/completions` | low \| medium \| high (high) |
|
|
90
91
|
| `peer-review-coordinator` | (meta) | — | — |
|
|
91
92
|
|
|
92
|
-
`peer-review-coordinator` is a subagent (not an MCP tool) that fans out to the right combination of the
|
|
93
|
+
`peer-review-coordinator` is a subagent (not an MCP tool) that fans out to the right combination of the critics in parallel based on artifact type — plan, diff, single file, or long-context — and aggregates findings.
|
|
93
94
|
|
|
94
|
-
**Effort tiers** are exposed via the MCP tool's `effort` argument; subagents pass it through.
|
|
95
|
+
**Effort tiers** are exposed via the MCP tool's `effort` argument; subagents pass it through. `xhigh` routinely runs 60–90s; the proxy responds to `tools/call` requests with SSE-streamed responses (per MCP 2025-06-18 Streamable HTTP transport spec) so the connection stays open past the standard ~60s MCP per-tool-call ceiling and long calls complete transparently with no user setup.
|
|
95
96
|
|
|
96
|
-
`gemini-critic` only
|
|
97
|
+
`gemini-critic` and `gemini-reviewer` both register only when `gemini-3.1-pro-preview` (or another `gemini-3.x-pro` model) is present in your Copilot model catalog — `gemini-critic` is the architecture-level critic, `gemini-reviewer` the line-level second-lab code reviewer on the same model at its highest reasoning tier. If absent, both personas are silently dropped from the MCP `tools/list` and the subagent set, and `peer-review-coordinator` skips them in routing decisions.
|
|
97
98
|
|
|
98
99
|
For codex-side write capability (a `codex-implementer` persona that can mutate files via Codex's tool-use sandbox), pass `--codex-cli`. Requires `codex` CLI 0.129+ on `PATH`; falls back to HTTP-only with a warning if codex is missing or older. Pass `--codex-mcp-only` to also pass `--strict-mcp-config` to Claude Code so only the proxy's MCP servers are loaded (hides any MCP servers in your existing `~/.claude/mcp.json`).
|
|
99
100
|
|
|
100
|
-
### Code search (`
|
|
101
|
+
### Code search (`mcp__search__code`)
|
|
101
102
|
|
|
102
|
-
Alongside the peer reviewers, the same MCP surface exposes a `
|
|
103
|
+
Alongside the peer reviewers, the same MCP surface exposes a `code` tool (under the `search` server — `mcp__search__code`) — fast structured code search over the workspace, ranked by **BM25F** (Robertson, Zaragoza, Taylor 2004) over four code-aware fields: matched line, surrounding context, file path tokens, and a symbol-definition heuristic. On top of that, the top hits get a tree-sitter pass that promotes true identifier-definition sites over incidental string matches; depth is controlled by the optional `structural` argument (`"full"` parses the top 50 hits, `"topN"` parses the top 10 for tighter latency on big repos). A single `notice` field surfaces in the response on the rare occasions an actionable degradation fires — the structural pass overran its 200ms wall-clock budget, or the response hit the 256KB size cap and was truncated; the message text tells the model what to retry. Defaults to a "ranked" mode with shoulder pruning so models get the few right answers, not a flood of substring matches. `literal` and `regex` modes are also available for exact searches; single-identifier queries in `ranked`/`literal` mode auto-expand across camelCase / snake_case / kebab-case skeletons so `getUserName` also matches `get_user_name`.
|
|
103
104
|
|
|
104
105
|
`workspace` is any absolute path the proxy process can read — typically the project root or a sub-tree you're working in. The model picks it. There's no allow-set or secret-shape file denylist: the threat model is symmetric since Claude Code already has Read / Bash / Edit tools that reach the same paths, so gating one tool would have been inconsistency rather than defense. Paths in results are returned relative to the workspace, never absolute.
|
|
105
106
|
|
|
@@ -859,7 +859,7 @@ function clampNum(v, min, max) {
|
|
|
859
859
|
// attachment, so two parallel browser_mouse / browser_drag / browser_type
|
|
860
860
|
// calls on the same tab would interleave and corrupt each other (one
|
|
861
861
|
// call's mouseMoved would land mid-drag of another). The global
|
|
862
|
-
// MAX_INFLIGHT_TOOLS_CALL=
|
|
862
|
+
// MAX_INFLIGHT_TOOLS_CALL=32 cap doesn't help — it's global, not per-tab.
|
|
863
863
|
// This mutex is per-tab, layered on top.
|
|
864
864
|
const tabInputLockTails = new Map() // tabId → Promise (tail of the lock chain)
|
|
865
865
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "github-router browser bridge",
|
|
4
4
|
"short_name": "gh-router-browser",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.73",
|
|
6
6
|
"description": "Bridge between Claude (via github-router /mcp) and the browser. Implements tab control, navigation, clicks, form fill, downloads, screenshots, devtools eval. Blocks navigation to chrome://settings.",
|
|
7
7
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqJElxuBlonBS3TVW9FJN0mGTtShB3L1hoaYf6k39SOr1ogGYmF90EjRxy1i21k9wQQjPf26bcBu/9X67KrQjQV0uB38CaNukgiSeoLjfptN811u+PJHx6BP+jx3Qa6/3VenNPxHC8WEU0GXql8QSjIHEyCwKb6fMASXOK94JyB5Ywov2x8mt/+9ncqBBBMVzf6r5Sagy4PL1XnryLsuADD/vOEkPet8wXgH/Oj7v5tTsQQZ7U1JT51PoDs2BFnXc5v3TkVgZwd32k3ONh+nkDw1Hof+4zwUGOyJE6eMrlYzRlKM4Qxdf9JpavQvqfieAbTRWcyKeclnHeoIfE7cDBQIDAQAB",
|
|
8
8
|
"background": {
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { parentPort } from "node:worker_threads";
|
|
4
|
+
import Parser from "web-tree-sitter";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import consola from "consola";
|
|
7
|
+
|
|
8
|
+
//#region rolldown:runtime
|
|
9
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/lib/tree-sitter-grammars.ts
|
|
13
|
+
/**
|
|
14
|
+
* Cap the per-file size we'll parse. 1MB of source covers all
|
|
15
|
+
* reasonable hand-written files; bigger files are almost always
|
|
16
|
+
* generated code or vendored bundles whose AST signal is worthless
|
|
17
|
+
* for ranking real definitions.
|
|
18
|
+
*/
|
|
19
|
+
const STRUCTURAL_MAX_FILE_BYTES = 1024 * 1024;
|
|
20
|
+
/**
|
|
21
|
+
* Grammar key → wasm filename under `node_modules/tree-sitter-wasms/out/`.
|
|
22
|
+
* Resolved at runtime from `node_modules`; the file paths are stable
|
|
23
|
+
* because `tree-sitter-wasms` ships prebuilt binaries (no per-install
|
|
24
|
+
* codegen).
|
|
25
|
+
*/
|
|
26
|
+
const GRAMMAR_FILES = {
|
|
27
|
+
typescript: "tree-sitter-typescript.wasm",
|
|
28
|
+
tsx: "tree-sitter-tsx.wasm",
|
|
29
|
+
javascript: "tree-sitter-javascript.wasm",
|
|
30
|
+
python: "tree-sitter-python.wasm",
|
|
31
|
+
go: "tree-sitter-go.wasm",
|
|
32
|
+
rust: "tree-sitter-rust.wasm",
|
|
33
|
+
java: "tree-sitter-java.wasm",
|
|
34
|
+
c: "tree-sitter-c.wasm",
|
|
35
|
+
cpp: "tree-sitter-cpp.wasm"
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Per-language definition-shape node types. When a matched identifier
|
|
39
|
+
* sits inside one of these nodes AND is at the node's "name" position,
|
|
40
|
+
* we have AST-confirmed evidence the line is an identifier-definition
|
|
41
|
+
* site. The brief's enumeration plus a handful of language-idiomatic
|
|
42
|
+
* extras (e.g., `lexical_declaration` for TS/JS top-level `const`s,
|
|
43
|
+
* `mod_item` for Rust modules).
|
|
44
|
+
*
|
|
45
|
+
* The set lookup is per-language so a node type that means
|
|
46
|
+
* "definition" in one language but "reference" in another won't
|
|
47
|
+
* cross-pollute.
|
|
48
|
+
*/
|
|
49
|
+
const DEFINITION_NODE_TYPES = {
|
|
50
|
+
typescript: new Set([
|
|
51
|
+
"function_declaration",
|
|
52
|
+
"function_signature",
|
|
53
|
+
"function_expression",
|
|
54
|
+
"method_definition",
|
|
55
|
+
"method_signature",
|
|
56
|
+
"class_declaration",
|
|
57
|
+
"interface_declaration",
|
|
58
|
+
"type_alias_declaration",
|
|
59
|
+
"enum_declaration",
|
|
60
|
+
"variable_declarator",
|
|
61
|
+
"generator_function_declaration",
|
|
62
|
+
"abstract_method_signature",
|
|
63
|
+
"public_field_definition",
|
|
64
|
+
"property_signature"
|
|
65
|
+
]),
|
|
66
|
+
tsx: new Set([
|
|
67
|
+
"function_declaration",
|
|
68
|
+
"function_signature",
|
|
69
|
+
"function_expression",
|
|
70
|
+
"method_definition",
|
|
71
|
+
"method_signature",
|
|
72
|
+
"class_declaration",
|
|
73
|
+
"interface_declaration",
|
|
74
|
+
"type_alias_declaration",
|
|
75
|
+
"enum_declaration",
|
|
76
|
+
"variable_declarator",
|
|
77
|
+
"generator_function_declaration",
|
|
78
|
+
"abstract_method_signature",
|
|
79
|
+
"public_field_definition",
|
|
80
|
+
"property_signature"
|
|
81
|
+
]),
|
|
82
|
+
javascript: new Set([
|
|
83
|
+
"function_declaration",
|
|
84
|
+
"function_expression",
|
|
85
|
+
"method_definition",
|
|
86
|
+
"class_declaration",
|
|
87
|
+
"variable_declarator",
|
|
88
|
+
"generator_function_declaration"
|
|
89
|
+
]),
|
|
90
|
+
python: new Set([
|
|
91
|
+
"function_definition",
|
|
92
|
+
"class_definition",
|
|
93
|
+
"decorated_definition"
|
|
94
|
+
]),
|
|
95
|
+
go: new Set([
|
|
96
|
+
"function_declaration",
|
|
97
|
+
"method_declaration",
|
|
98
|
+
"type_spec",
|
|
99
|
+
"type_alias",
|
|
100
|
+
"const_spec",
|
|
101
|
+
"var_spec"
|
|
102
|
+
]),
|
|
103
|
+
rust: new Set([
|
|
104
|
+
"function_item",
|
|
105
|
+
"impl_item",
|
|
106
|
+
"trait_item",
|
|
107
|
+
"struct_item",
|
|
108
|
+
"enum_item",
|
|
109
|
+
"mod_item",
|
|
110
|
+
"type_item",
|
|
111
|
+
"const_item",
|
|
112
|
+
"static_item",
|
|
113
|
+
"macro_definition"
|
|
114
|
+
]),
|
|
115
|
+
java: new Set([
|
|
116
|
+
"class_declaration",
|
|
117
|
+
"interface_declaration",
|
|
118
|
+
"method_declaration",
|
|
119
|
+
"constructor_declaration",
|
|
120
|
+
"enum_declaration",
|
|
121
|
+
"field_declaration",
|
|
122
|
+
"annotation_type_declaration"
|
|
123
|
+
]),
|
|
124
|
+
c: new Set([
|
|
125
|
+
"function_definition",
|
|
126
|
+
"declaration",
|
|
127
|
+
"struct_specifier",
|
|
128
|
+
"enum_specifier",
|
|
129
|
+
"union_specifier",
|
|
130
|
+
"type_definition"
|
|
131
|
+
]),
|
|
132
|
+
cpp: new Set([
|
|
133
|
+
"function_definition",
|
|
134
|
+
"declaration",
|
|
135
|
+
"struct_specifier",
|
|
136
|
+
"class_specifier",
|
|
137
|
+
"enum_specifier",
|
|
138
|
+
"union_specifier",
|
|
139
|
+
"type_definition",
|
|
140
|
+
"namespace_definition",
|
|
141
|
+
"template_declaration"
|
|
142
|
+
])
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Node types that the AST exposes as "this token is an identifier".
|
|
146
|
+
* The match-position lookup uses these to filter out parent-node hits
|
|
147
|
+
* before checking the definition-site predicate.
|
|
148
|
+
*/
|
|
149
|
+
const IDENTIFIER_NODE_TYPES = new Set([
|
|
150
|
+
"identifier",
|
|
151
|
+
"type_identifier",
|
|
152
|
+
"field_identifier",
|
|
153
|
+
"property_identifier",
|
|
154
|
+
"shorthand_property_identifier_pattern",
|
|
155
|
+
"shorthand_property_identifier",
|
|
156
|
+
"scoped_identifier",
|
|
157
|
+
"name"
|
|
158
|
+
]);
|
|
159
|
+
let _grammarBundle;
|
|
160
|
+
/**
|
|
161
|
+
* Resolve the `tree-sitter-wasms/out/` directory at the package root.
|
|
162
|
+
* `require.resolve` is used through a try/catch — the bundled-only
|
|
163
|
+
* fallback runs in environments where node_modules has been pruned to
|
|
164
|
+
* just runtime deps.
|
|
165
|
+
*/
|
|
166
|
+
function resolveGrammarRoot() {
|
|
167
|
+
try {
|
|
168
|
+
const pkgPath = __require.resolve("tree-sitter-wasms/package.json");
|
|
169
|
+
return path.join(path.dirname(pkgPath), "out");
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Pre-load all grammars at module-init time so the first search
|
|
176
|
+
* doesn't pay a ~500ms cold-start cost. The Promise is captured at
|
|
177
|
+
* import time and awaited per-call; per-grammar failures are caught
|
|
178
|
+
* individually so one broken grammar can't take the whole tool down.
|
|
179
|
+
*/
|
|
180
|
+
function getGrammarBundle() {
|
|
181
|
+
if (_grammarBundle) return _grammarBundle;
|
|
182
|
+
_grammarBundle = { ready: (async () => {
|
|
183
|
+
const out = /* @__PURE__ */ new Map();
|
|
184
|
+
try {
|
|
185
|
+
await Parser.init();
|
|
186
|
+
} catch (err) {
|
|
187
|
+
consola.warn(`[code_search] tree-sitter Parser.init failed; structural ranking disabled: ${err.message}`);
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
const root = resolveGrammarRoot();
|
|
191
|
+
if (!root) {
|
|
192
|
+
consola.warn("[code_search] tree-sitter-wasms package not resolvable; structural ranking disabled");
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
for (const [key, filename] of Object.entries(GRAMMAR_FILES)) {
|
|
196
|
+
const wasmPath = path.join(root, filename);
|
|
197
|
+
try {
|
|
198
|
+
const lang = await Parser.Language.load(wasmPath);
|
|
199
|
+
out.set(key, lang);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
consola.warn(`[code_search] failed to load tree-sitter grammar '${key}' from ${filename}: ${err.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
})() };
|
|
206
|
+
return _grammarBundle;
|
|
207
|
+
}
|
|
208
|
+
getGrammarBundle().ready.catch(() => {});
|
|
209
|
+
/**
|
|
210
|
+
* Robustness bound on outline entries per file. Normal source is far
|
|
211
|
+
* under it; generated/pathological files hit it and `outlineFile` then
|
|
212
|
+
* sets a `notice` so the model knows the map was truncated.
|
|
213
|
+
*/
|
|
214
|
+
const MAX_OUTLINE_ENTRIES = 1e3;
|
|
215
|
+
/**
|
|
216
|
+
* First identifier-typed named child found in a pre-order walk. Mirrors
|
|
217
|
+
* the structural-pass helper in `code-search.ts` (kept local here so
|
|
218
|
+
* the grammar module has no dependency back on the ranking layer).
|
|
219
|
+
*/
|
|
220
|
+
function firstIdentifierLeaf(node) {
|
|
221
|
+
if (IDENTIFIER_NODE_TYPES.has(node.type)) return node;
|
|
222
|
+
for (const child of node.namedChildren) {
|
|
223
|
+
const r = firstIdentifierLeaf(child);
|
|
224
|
+
if (r) return r;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Derive a human-readable name for a definition node. Tries the
|
|
230
|
+
* grammar's standard `name` field first, then the `declarator` /
|
|
231
|
+
* `type` fields (C/C++/Java declarators, Rust/Go type specs), then any
|
|
232
|
+
* identifier-typed named child as a last resort. Returns `null` when no
|
|
233
|
+
* name can be recovered — the caller skips such nodes.
|
|
234
|
+
*/
|
|
235
|
+
function deriveDefinitionName(node) {
|
|
236
|
+
const nameField = node.childForFieldName("name");
|
|
237
|
+
if (nameField && nameField.text.length > 0) return nameField.text;
|
|
238
|
+
const declarator = node.childForFieldName("declarator");
|
|
239
|
+
if (declarator) {
|
|
240
|
+
const leaf = firstIdentifierLeaf(declarator);
|
|
241
|
+
if (leaf && leaf.text.length > 0) return leaf.text;
|
|
242
|
+
}
|
|
243
|
+
const typeField = node.childForFieldName("type");
|
|
244
|
+
if (typeField) {
|
|
245
|
+
const leaf = firstIdentifierLeaf(typeField);
|
|
246
|
+
if (leaf && leaf.text.length > 0) return leaf.text;
|
|
247
|
+
}
|
|
248
|
+
const fallback = firstIdentifierLeaf(node);
|
|
249
|
+
if (fallback && fallback.text.length > 0) return fallback.text;
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Collect EVERY definition node from the parse tree — top-level AND
|
|
254
|
+
* nested (class methods, methods' inner functions, nested classes, …) —
|
|
255
|
+
* so the outline is a COMPLETE structural map the model can rely on to
|
|
256
|
+
* decide what to read. Recurses through non-definition wrappers (TS
|
|
257
|
+
* `export_statement`, Python `decorated_definition`, C++
|
|
258
|
+
* `template_declaration`, …) at the same depth, and INTO each definition
|
|
259
|
+
* at depth+1 to surface its members.
|
|
260
|
+
*
|
|
261
|
+
* `defTypes` is the language's definition-node-type set. Each node yields
|
|
262
|
+
* one entry; the `name` is derived per `deriveDefinitionName` (a node
|
|
263
|
+
* with no recoverable name is skipped, but the walk still descends into
|
|
264
|
+
* it so its named members aren't lost). Bounded at `MAX_OUTLINE_ENTRIES`.
|
|
265
|
+
*/
|
|
266
|
+
function collectDefinitions(root, defTypes, signal) {
|
|
267
|
+
const out = [];
|
|
268
|
+
const visit = (node, depth) => {
|
|
269
|
+
if (signal?.aborted || out.length >= MAX_OUTLINE_ENTRIES) return;
|
|
270
|
+
for (const child of node.namedChildren) {
|
|
271
|
+
if (signal?.aborted || out.length >= MAX_OUTLINE_ENTRIES) return;
|
|
272
|
+
if (defTypes.has(child.type)) {
|
|
273
|
+
const name = deriveDefinitionName(child);
|
|
274
|
+
if (name !== null) out.push({
|
|
275
|
+
kind: child.type,
|
|
276
|
+
name,
|
|
277
|
+
line: child.startPosition.row + 1,
|
|
278
|
+
depth
|
|
279
|
+
});
|
|
280
|
+
visit(child, depth + 1);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
visit(child, depth);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
visit(root, 0);
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Build a `FileOutlineResult` from an ALREADY-PARSED tree — walk-only,
|
|
291
|
+
* no read / parse / `delete`. The tree's ownership stays with the caller
|
|
292
|
+
* (e.g. the code-search structural pass's `_treeCache`), so this lets the
|
|
293
|
+
* outline step REUSE a tree the structural pass already parsed instead of
|
|
294
|
+
* re-reading + re-parsing the file. Never throws.
|
|
295
|
+
*/
|
|
296
|
+
function outlineFromTree(tree, language, signal) {
|
|
297
|
+
if (signal?.aborted) return {
|
|
298
|
+
outline: [],
|
|
299
|
+
language
|
|
300
|
+
};
|
|
301
|
+
const defTypes = DEFINITION_NODE_TYPES[language];
|
|
302
|
+
if (!defTypes) return {
|
|
303
|
+
outline: [],
|
|
304
|
+
language,
|
|
305
|
+
notice: "outline unavailable (parse error)"
|
|
306
|
+
};
|
|
307
|
+
try {
|
|
308
|
+
const outline = collectDefinitions(tree.rootNode, defTypes, signal);
|
|
309
|
+
if (signal?.aborted) return {
|
|
310
|
+
outline: [],
|
|
311
|
+
language
|
|
312
|
+
};
|
|
313
|
+
outline.sort((a, b) => a.line - b.line);
|
|
314
|
+
if (outline.length >= MAX_OUTLINE_ENTRIES) return {
|
|
315
|
+
outline,
|
|
316
|
+
language,
|
|
317
|
+
notice: `outline truncated at ${MAX_OUTLINE_ENTRIES} symbols (very large file)`
|
|
318
|
+
};
|
|
319
|
+
return {
|
|
320
|
+
outline,
|
|
321
|
+
language
|
|
322
|
+
};
|
|
323
|
+
} catch {
|
|
324
|
+
return {
|
|
325
|
+
outline: [],
|
|
326
|
+
language,
|
|
327
|
+
notice: "outline unavailable (parse error)"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Compute the absolute byte offset where line `lineNumber1` starts in
|
|
333
|
+
* `source`. Lines are counted by LF; CRLF files have the same line starts as
|
|
334
|
+
* LF files (the \r is part of the previous line's content). 1-indexed to match
|
|
335
|
+
* ripgrep. Returns -1 if the line is past EOF.
|
|
336
|
+
*/
|
|
337
|
+
function lineStartByte(source, lineNumber1) {
|
|
338
|
+
if (lineNumber1 <= 1) return 0;
|
|
339
|
+
let line = 1;
|
|
340
|
+
for (let i = 0; i < source.length; i++) if (source.charCodeAt(i) === 10) {
|
|
341
|
+
line += 1;
|
|
342
|
+
if (line === lineNumber1) return i + 1;
|
|
343
|
+
}
|
|
344
|
+
return -1;
|
|
345
|
+
}
|
|
346
|
+
function containsByteRange(outer, inner) {
|
|
347
|
+
return outer.startIndex <= inner.startIndex && outer.endIndex >= inner.endIndex;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Walk up from a matched identifier node looking for the closest
|
|
351
|
+
* definition-shape ancestor (per the language's allowed types). When found,
|
|
352
|
+
* verify the matched identifier is at the definition's "name" slot — NOT inside
|
|
353
|
+
* a parameter type, a body, or a parent's signature. Returns true iff this is a
|
|
354
|
+
* real definition site for the identifier the rg submatch landed on. Depth
|
|
355
|
+
* bound 6 — definition names sit close to their definition node in every
|
|
356
|
+
* supported grammar; deeper walks risk false positives.
|
|
357
|
+
*/
|
|
358
|
+
function isDefiningSite(matchedNode, langKey) {
|
|
359
|
+
const defTypes = DEFINITION_NODE_TYPES[langKey];
|
|
360
|
+
if (!defTypes) return false;
|
|
361
|
+
let cur = matchedNode.parent;
|
|
362
|
+
let depth = 0;
|
|
363
|
+
while (cur && depth < 6) {
|
|
364
|
+
if (defTypes.has(cur.type)) {
|
|
365
|
+
const nameField = cur.childForFieldName("name");
|
|
366
|
+
if (nameField && containsByteRange(nameField, matchedNode)) return true;
|
|
367
|
+
const declarator = cur.childForFieldName("declarator");
|
|
368
|
+
if (declarator && containsByteRange(declarator, matchedNode)) {
|
|
369
|
+
const first = firstIdentifierLeaf(declarator);
|
|
370
|
+
if (first && first.startIndex === matchedNode.startIndex) return true;
|
|
371
|
+
}
|
|
372
|
+
const typeField = cur.childForFieldName("type");
|
|
373
|
+
if (typeField && containsByteRange(typeField, matchedNode)) {
|
|
374
|
+
const first = firstIdentifierLeaf(typeField);
|
|
375
|
+
if (first && first.startIndex === matchedNode.startIndex) return true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
cur = cur.parent;
|
|
379
|
+
depth += 1;
|
|
380
|
+
}
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Run the AST-confirmation walk over `hits` against an ALREADY-PARSED tree.
|
|
385
|
+
* Returns the subset of input indices whose matched identifier is at a real
|
|
386
|
+
* definition site. Borrowed tree — never deleted. Pure + deterministic: the
|
|
387
|
+
* output set is a function of (tree, source, hits) only, independent of call
|
|
388
|
+
* order, so the in-process path and the worker path return identical sets.
|
|
389
|
+
*
|
|
390
|
+
* Never throws — a per-hit walk failure just omits that index (matches the
|
|
391
|
+
* in-process pass's per-hit try/catch). `signal` short-circuits between hits.
|
|
392
|
+
*/
|
|
393
|
+
function confirmDefinitionSites(tree, source, language, hits, signal) {
|
|
394
|
+
const confirmed = [];
|
|
395
|
+
if (!DEFINITION_NODE_TYPES[language]) return confirmed;
|
|
396
|
+
for (let i = 0; i < hits.length; i++) {
|
|
397
|
+
if (signal?.aborted) break;
|
|
398
|
+
const hit = hits[i];
|
|
399
|
+
const lineStart = lineStartByte(source, hit.line);
|
|
400
|
+
if (lineStart < 0) continue;
|
|
401
|
+
const matchByteStart = lineStart + hit.matchStart;
|
|
402
|
+
const matchByteEnd = lineStart + hit.matchEnd;
|
|
403
|
+
let node;
|
|
404
|
+
try {
|
|
405
|
+
node = tree.rootNode.descendantForIndex(matchByteStart, matchByteEnd);
|
|
406
|
+
} catch {
|
|
407
|
+
node = null;
|
|
408
|
+
}
|
|
409
|
+
if (!node) continue;
|
|
410
|
+
if (!IDENTIFIER_NODE_TYPES.has(node.type)) {
|
|
411
|
+
let cur = node;
|
|
412
|
+
let depth = 0;
|
|
413
|
+
while (cur && !IDENTIFIER_NODE_TYPES.has(cur.type) && depth < 3) {
|
|
414
|
+
const leaf = firstIdentifierLeaf(cur);
|
|
415
|
+
if (leaf && leaf.startIndex === matchByteStart) {
|
|
416
|
+
cur = leaf;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
cur = cur.parent;
|
|
420
|
+
depth += 1;
|
|
421
|
+
}
|
|
422
|
+
node = cur;
|
|
423
|
+
}
|
|
424
|
+
if (!node || !IDENTIFIER_NODE_TYPES.has(node.type)) continue;
|
|
425
|
+
if (isDefiningSite(node, language)) confirmed.push(i);
|
|
426
|
+
}
|
|
427
|
+
return confirmed;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/lib/tree-sitter-pool/worker.ts
|
|
432
|
+
if (!parentPort) throw new Error("tree-sitter worker must be spawned via worker_threads");
|
|
433
|
+
const port = parentPort;
|
|
434
|
+
let grammars = /* @__PURE__ */ new Map();
|
|
435
|
+
const parsers = /* @__PURE__ */ new Map();
|
|
436
|
+
let cancelled = false;
|
|
437
|
+
function handleJob(job) {
|
|
438
|
+
if (process.env.GH_ROUTER_TS_WORKER_CRASH === "1") throw new Error("injected worker crash (test)");
|
|
439
|
+
if (cancelled) return {
|
|
440
|
+
id: job.id,
|
|
441
|
+
ok: true,
|
|
442
|
+
mtimeMs: job.mtimeMs
|
|
443
|
+
};
|
|
444
|
+
const lang = grammars.get(job.language);
|
|
445
|
+
if (!lang) return {
|
|
446
|
+
id: job.id,
|
|
447
|
+
ok: false,
|
|
448
|
+
error: "grammar not loaded"
|
|
449
|
+
};
|
|
450
|
+
let mtimeMs;
|
|
451
|
+
let size;
|
|
452
|
+
try {
|
|
453
|
+
const st = statSync(job.absPath);
|
|
454
|
+
mtimeMs = st.mtimeMs;
|
|
455
|
+
size = st.size;
|
|
456
|
+
} catch (err) {
|
|
457
|
+
return {
|
|
458
|
+
id: job.id,
|
|
459
|
+
ok: false,
|
|
460
|
+
error: `stat failed: ${err.message}`
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (size > STRUCTURAL_MAX_FILE_BYTES) return {
|
|
464
|
+
id: job.id,
|
|
465
|
+
ok: false,
|
|
466
|
+
error: "file too large"
|
|
467
|
+
};
|
|
468
|
+
let source;
|
|
469
|
+
try {
|
|
470
|
+
source = readFileSync(job.absPath, "utf8");
|
|
471
|
+
} catch (err) {
|
|
472
|
+
return {
|
|
473
|
+
id: job.id,
|
|
474
|
+
ok: false,
|
|
475
|
+
error: `read failed: ${err.message}`
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
let parser = parsers.get(job.language);
|
|
479
|
+
if (!parser) {
|
|
480
|
+
parser = new Parser();
|
|
481
|
+
parser.setLanguage(lang);
|
|
482
|
+
parsers.set(job.language, parser);
|
|
483
|
+
}
|
|
484
|
+
let tree = null;
|
|
485
|
+
try {
|
|
486
|
+
tree = parser.parse(source);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
return {
|
|
489
|
+
id: job.id,
|
|
490
|
+
ok: false,
|
|
491
|
+
error: `parse failed: ${err.message}`
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (!tree) return {
|
|
495
|
+
id: job.id,
|
|
496
|
+
ok: false,
|
|
497
|
+
error: "parse returned null"
|
|
498
|
+
};
|
|
499
|
+
try {
|
|
500
|
+
const reply = {
|
|
501
|
+
id: job.id,
|
|
502
|
+
ok: true,
|
|
503
|
+
mtimeMs
|
|
504
|
+
};
|
|
505
|
+
if (job.want.confirmHits && job.want.confirmHits.length > 0) reply.confirmedHitIndexes = confirmDefinitionSites(tree, source, job.language, job.want.confirmHits);
|
|
506
|
+
if (job.want.outline) reply.outlineEntries = outlineFromTree(tree, job.language).outline;
|
|
507
|
+
return reply;
|
|
508
|
+
} catch (err) {
|
|
509
|
+
return {
|
|
510
|
+
id: job.id,
|
|
511
|
+
ok: false,
|
|
512
|
+
error: `walk failed: ${err.message}`
|
|
513
|
+
};
|
|
514
|
+
} finally {
|
|
515
|
+
try {
|
|
516
|
+
tree.delete();
|
|
517
|
+
} catch {}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
port.on("message", (msg) => {
|
|
521
|
+
if ("type" in msg) {
|
|
522
|
+
if (msg.type === "cancel") cancelled = true;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
cancelled = false;
|
|
526
|
+
const reply = handleJob(msg);
|
|
527
|
+
port.postMessage(reply);
|
|
528
|
+
});
|
|
529
|
+
(async () => {
|
|
530
|
+
try {
|
|
531
|
+
grammars = await getGrammarBundle().ready;
|
|
532
|
+
} catch {
|
|
533
|
+
grammars = /* @__PURE__ */ new Map();
|
|
534
|
+
}
|
|
535
|
+
const ready = {
|
|
536
|
+
type: "ready",
|
|
537
|
+
loaded: [...grammars.keys()]
|
|
538
|
+
};
|
|
539
|
+
port.postMessage(ready);
|
|
540
|
+
})();
|
|
541
|
+
|
|
542
|
+
//#endregion
|
|
543
|
+
export { };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import "./paths-yJ97KlKp.js";
|
|
2
|
+
import { a as trackChild, i as sweepStaleColbertMetaAtBoot, n as registerColbertExitHandlers, r as sweepLiveChildren, t as getColbertInstanceUuid } from "./lifecycle-yaqqtsV1.js";
|
|
3
|
+
|
|
4
|
+
export { getColbertInstanceUuid, registerColbertExitHandlers, sweepLiveChildren, sweepStaleColbertMetaAtBoot, trackChild };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as writeRuntimeFileSecure, t as PATHS } from "./paths-
|
|
1
|
+
import { l as writeRuntimeFileSecure, t as PATHS } from "./paths-yJ97KlKp.js";
|
|
2
2
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
@@ -307,4 +307,4 @@ async function sweepStaleWorktreesAtBoot() {
|
|
|
307
307
|
|
|
308
308
|
//#endregion
|
|
309
309
|
export { sweepRegistry as a, registerExitHandlers as i, getInstanceUuid as n, sweepStaleWorktreesAtBoot as o, recordWorkerRepo as r, WorktreeRegistry as t };
|
|
310
|
-
//# sourceMappingURL=lifecycle-
|
|
310
|
+
//# sourceMappingURL=lifecycle-CMPthagV.js.map
|