@maxgfr/codeindex 2.16.0 → 2.17.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/README.md +51 -3
- package/package.json +2 -2
- package/scripts/engine.d.mts +7 -4
- package/scripts/engine.mjs +231 -11
- package/src/engine-cli.ts +130 -6
- package/src/engine.ts +7 -0
- package/src/mcp.ts +34 -4
- package/src/rewrite.ts +169 -0
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -192,17 +192,65 @@ Full details incl. the HTTP protocol (build your own server):
|
|
|
192
192
|
|
|
193
193
|
## Use as an MCP server
|
|
194
194
|
|
|
195
|
-
`codeindex mcp` (or `node scripts/cli.mjs mcp`) serves the engine over stdio
|
|
196
|
-
|
|
197
|
-
`grep`. Register it in Claude Code with:
|
|
195
|
+
`codeindex mcp` (or `node scripts/cli.mjs mcp`) serves the engine over stdio.
|
|
196
|
+
Register it in Claude Code with:
|
|
198
197
|
|
|
199
198
|
```sh
|
|
200
199
|
claude mcp add codeindex -- codeindex mcp
|
|
201
200
|
```
|
|
202
201
|
|
|
202
|
+
**26 tools**, grouped by what they answer:
|
|
203
|
+
|
|
204
|
+
| group | tools |
|
|
205
|
+
|---|---|
|
|
206
|
+
| orient | `scan_summary`, `repo_map`, `graph`, `mermaid`, `workspaces` |
|
|
207
|
+
| find | `search`, `grep`, `find_symbol`, `symbols`, `symbols_overview` |
|
|
208
|
+
| impact | `find_references`, `callers`, `dead_code` |
|
|
209
|
+
| risk | `hotspots`, `churn`, `coupling`, `complexity`, `check_rules` |
|
|
210
|
+
| edit *(write)* | `replace_symbol_body`, `insert_after_symbol`, `insert_before_symbol` |
|
|
211
|
+
| memory | `write_memory`, `read_memory`, `list_memories`, `delete_memory` *(write except reads)* |
|
|
212
|
+
| embeddings | `embed_status` |
|
|
213
|
+
|
|
214
|
+
### Pinning the server to one repository
|
|
215
|
+
|
|
216
|
+
Every tool takes a `repo` argument. A host that runs one server per workspace
|
|
217
|
+
can pin it instead, so `repo` becomes optional on every tool — the pin is
|
|
218
|
+
reflected in the advertised schema, not merely tolerated at call time:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
codeindex mcp --repo /path/to/workspace
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
An explicit per-call `repo` still wins, so a pinned server can still answer
|
|
225
|
+
about another checkout. `--server-name <name>` overrides the announced
|
|
226
|
+
`serverInfo.name` for hosts that embed the server under their own identity.
|
|
227
|
+
|
|
228
|
+
**Prime the index first** and activation becomes a load, not a rebuild:
|
|
229
|
+
`codeindex index --repo <dir> --out <dir>/.codeindex`. The first tool call
|
|
230
|
+
deserializes those artifacts when the engine version, commit and artifact
|
|
231
|
+
hashes all match.
|
|
232
|
+
|
|
203
233
|
`engine.mjs` is a pure side-effect-free library (safe for consumers to inline
|
|
204
234
|
into their own CLIs); `cli.mjs` is the thin standalone CLI/MCP wrapper.
|
|
205
235
|
|
|
236
|
+
## Command rewriting
|
|
237
|
+
|
|
238
|
+
`codeindex rewrite '<command line>'` maps an expensive tree-wide search onto
|
|
239
|
+
its indexed equivalent, for agent harnesses that intercept shell commands
|
|
240
|
+
(iterion's `rewriters` plugin kind, generalizing rtk):
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
$ codeindex rewrite 'grep -rn TODO src'
|
|
244
|
+
codeindex grep TODO --scope src
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
It prints the replacement and exits `0`, or exits `1` with empty stdout when it
|
|
248
|
+
has no opinion — run the original. The parser is deliberately conservative: any
|
|
249
|
+
shell metacharacter (pipe, redirect, substitution, chaining), any unrecognized
|
|
250
|
+
flag, a non-recursive `grep`, or more than one search path all refuse the
|
|
251
|
+
rewrite. A refusal costs nothing; a wrong rewrite silently changes what the
|
|
252
|
+
agent asked for.
|
|
253
|
+
|
|
206
254
|
## Versioning
|
|
207
255
|
|
|
208
256
|
- `ENGINE_VERSION` — the release tag, embedded greppably in the bundle.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxgfr/codeindex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.1",
|
|
4
4
|
"description": "Self-contained, deterministic repo-indexing engine: walk + language detection + symbol/import extraction (tree-sitter AST with regex fallback) + import resolution + typed cross-file link-graph + analytics. Ships as a single zero-dependency engine.mjs that consumer tools vendor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"tsup": "^8.3.0",
|
|
78
78
|
"typescript": "^5.5.0",
|
|
79
79
|
"vitest": "^4.1.0",
|
|
80
|
-
"web-tree-sitter": "^0.26.
|
|
80
|
+
"web-tree-sitter": "^0.26.11"
|
|
81
81
|
},
|
|
82
82
|
"pnpm": {
|
|
83
83
|
"onlyBuiltDependencies": [
|
package/scripts/engine.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare const ENGINE_VERSION = "2.
|
|
1
|
+
declare const ENGINE_VERSION = "2.17.1";
|
|
2
2
|
declare const SCHEMA_VERSION = 4;
|
|
3
3
|
declare const EXTRACTOR_VERSION = 10;
|
|
4
4
|
type FileKind = "code" | "doc" | "config" | "asset" | "other";
|
|
@@ -255,7 +255,7 @@ declare function extractAst(rel: string, ext: string, content: string, opts?: {
|
|
|
255
255
|
maxCalls?: number;
|
|
256
256
|
}): AstResult | undefined;
|
|
257
257
|
|
|
258
|
-
declare const DEFAULT_GRAMMARS_URL = "https://github.com/maxgfr/codeindex/releases/download/v2.
|
|
258
|
+
declare const DEFAULT_GRAMMARS_URL = "https://github.com/maxgfr/codeindex/releases/download/v2.17.1/grammars-2.17.1.tar.gz";
|
|
259
259
|
interface GrammarsPullTarget {
|
|
260
260
|
url: string;
|
|
261
261
|
sha256Url?: string;
|
|
@@ -745,9 +745,12 @@ interface McpServerOptions {
|
|
|
745
745
|
name?: string;
|
|
746
746
|
version?: string;
|
|
747
747
|
};
|
|
748
|
+
defaultRepo?: string;
|
|
748
749
|
}
|
|
749
750
|
declare function runMcpServer(opts?: McpServerOptions): Promise<void>;
|
|
750
751
|
|
|
752
|
+
declare function rewriteCommand(cmd: string, bin?: string): string | undefined;
|
|
753
|
+
|
|
751
754
|
declare function sha1(s: string | Uint8Array): string;
|
|
752
755
|
declare function shortHash(s: string, n?: number): string;
|
|
753
756
|
|
|
@@ -777,6 +780,6 @@ declare function keywords(question: string): string[];
|
|
|
777
780
|
declare function rankedKeywords(question: string): string[];
|
|
778
781
|
declare function rrf<T>(lists: T[][], keyOf: (item: T) => string, k?: number): Map<string, number>;
|
|
779
782
|
|
|
780
|
-
declare function runCli(
|
|
783
|
+
declare function runCli(rawArgv: string[]): Promise<void>;
|
|
781
784
|
|
|
782
|
-
export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_GRAMMARS_URL, DEFAULT_MAX_FILES, type DeadSymbol, type DiffFile, type DiffSpec, EMBED_VERSION, ENGINE_VERSION, EXTRACTOR_VERSION, type Edge, type EdgeKind, type EditResult, type EmbedEndpointOptions, type EmbedPullTarget, type EmbeddingIndex, type EmbeddingRecord, type EmbeddingUnit, type FileCategory, type FileKind, type FileNode, type FileRecord, type FindSymbolOptions, type ForbiddenEdgeRule, type GrammarsPullResult, type GrammarsPullTarget, type GrammarsTier, type GrammarsTierName, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type McpServerOptions, type MermaidOptions, type ModuleInfo, type ModuleNode, type RawCallerIndex, type RawCallerSite, type RawRef, type RenderScipOptions, type RepoMapOptions, type RepoScan, type Resolution, type ResolveContext, type RiskHotspot, type RuleSeverity, type RuleViolation, SCHEMA_VERSION, type ScanOptions, type SearchHit, type SearchOptions, type SearchResult, type SemanticSearchOptions, type SemanticSearchResult, type ShResult, type StaticEmbedModel, type SurpriseEdge, type SymbolComplexity, type SymbolIndex, type SymbolMatch, type SymbolReferences, type TestMap, type Tier, type WalkOptions, type WalkResult, type WalkedFile, type WarmGrammarsOptions, type WarmGrammarsResult, type WorkspaceInfo, type WorkspaceKind, type WorkspacePackage, allGrammarKeys, applyCentrality, basicTokenize, betweennessOf, buildArtifactsFromScan, buildCallerIndex, buildEmbeddingIndex, buildEndpointIndex, buildGraph, buildIndexArtifacts, buildModules, buildRawCallerIndex, buildResolveContext, buildSymbolIndex, byKey, byStr, categorize, changeCoupling, changedSince, checkRules, classify, clip, clipInline, communityOf, compileGlobs, complexityOfSource, computeImportPairs, computeSurprises, computeSymbolRefs, computeTestMap, deleteMemory, deserializeEmbeddings, detectCommunities, detectWorkspaces, diffFiles, diffHunks, embedEndpointUrl, embedViaEndpoint, embeddingUnits, enclosingSymbol, encode, encodeQueryViaEndpoint, ensureGrammars, escapeRegExp, extToLang, extractAst, extractCode, extractGrammarsTarball, extractMarkdown, extractSymbols, extractTarInto, fetchExpectedSha256, fetchGrammarsTarball, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarKeysForExts, grammarReady, grepRepo, hasEmbedModel, have, headCommit, healthzUrl, insertAfterSymbol, insertBeforeSymbol, intDot, isCode, isDoc, isGitWorktree, isIgnored, isSurprising, isTestFile, isTestPath, keywords, languageOf, listMemories, loadEmbedModel, pagerankOf, parseGitignore, parseRules, probeEndpoint, pullGrammars, quantize, rankHotspots, rankedKeywords, readMemory, readText, renderGraphJson, renderMermaid, renderRepoMap, renderScip, renderSymbolsJson, replaceSymbolBody, resolveBaseRef, resolveCallEdges, resolveDocLink, resolveEmbedEndpoint, resolveEmbedModelDir, resolveEmbedPullUrl, resolveGrammarsDir, resolveGrammarsPullTarget, resolveGrammarsTier, resolveImport, resolveUniqueSymbol, riskHotspots, roundHalfToEven, rrf, runCli, runMcpServer, scanRepo, searchIndex, searchSemantic, serializeEmbeddings, sh, sha1, sharedGrammarsCacheDir, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, tokenize, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, warmGrammars, wordpiece, writeMemory };
|
|
785
|
+
export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_GRAMMARS_URL, DEFAULT_MAX_FILES, type DeadSymbol, type DiffFile, type DiffSpec, EMBED_VERSION, ENGINE_VERSION, EXTRACTOR_VERSION, type Edge, type EdgeKind, type EditResult, type EmbedEndpointOptions, type EmbedPullTarget, type EmbeddingIndex, type EmbeddingRecord, type EmbeddingUnit, type FileCategory, type FileKind, type FileNode, type FileRecord, type FindSymbolOptions, type ForbiddenEdgeRule, type GrammarsPullResult, type GrammarsPullTarget, type GrammarsTier, type GrammarsTierName, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type McpServerOptions, type MermaidOptions, type ModuleInfo, type ModuleNode, type RawCallerIndex, type RawCallerSite, type RawRef, type RenderScipOptions, type RepoMapOptions, type RepoScan, type Resolution, type ResolveContext, type RiskHotspot, type RuleSeverity, type RuleViolation, SCHEMA_VERSION, type ScanOptions, type SearchHit, type SearchOptions, type SearchResult, type SemanticSearchOptions, type SemanticSearchResult, type ShResult, type StaticEmbedModel, type SurpriseEdge, type SymbolComplexity, type SymbolIndex, type SymbolMatch, type SymbolReferences, type TestMap, type Tier, type WalkOptions, type WalkResult, type WalkedFile, type WarmGrammarsOptions, type WarmGrammarsResult, type WorkspaceInfo, type WorkspaceKind, type WorkspacePackage, allGrammarKeys, applyCentrality, basicTokenize, betweennessOf, buildArtifactsFromScan, buildCallerIndex, buildEmbeddingIndex, buildEndpointIndex, buildGraph, buildIndexArtifacts, buildModules, buildRawCallerIndex, buildResolveContext, buildSymbolIndex, byKey, byStr, categorize, changeCoupling, changedSince, checkRules, classify, clip, clipInline, communityOf, compileGlobs, complexityOfSource, computeImportPairs, computeSurprises, computeSymbolRefs, computeTestMap, deleteMemory, deserializeEmbeddings, detectCommunities, detectWorkspaces, diffFiles, diffHunks, embedEndpointUrl, embedViaEndpoint, embeddingUnits, enclosingSymbol, encode, encodeQueryViaEndpoint, ensureGrammars, escapeRegExp, extToLang, extractAst, extractCode, extractGrammarsTarball, extractMarkdown, extractSymbols, extractTarInto, fetchExpectedSha256, fetchGrammarsTarball, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarKeysForExts, grammarReady, grepRepo, hasEmbedModel, have, headCommit, healthzUrl, insertAfterSymbol, insertBeforeSymbol, intDot, isCode, isDoc, isGitWorktree, isIgnored, isSurprising, isTestFile, isTestPath, keywords, languageOf, listMemories, loadEmbedModel, pagerankOf, parseGitignore, parseRules, probeEndpoint, pullGrammars, quantize, rankHotspots, rankedKeywords, readMemory, readText, renderGraphJson, renderMermaid, renderRepoMap, renderScip, renderSymbolsJson, replaceSymbolBody, resolveBaseRef, resolveCallEdges, resolveDocLink, resolveEmbedEndpoint, resolveEmbedModelDir, resolveEmbedPullUrl, resolveGrammarsDir, resolveGrammarsPullTarget, resolveGrammarsTier, resolveImport, resolveUniqueSymbol, rewriteCommand, riskHotspots, roundHalfToEven, rrf, runCli, runMcpServer, scanRepo, searchIndex, searchSemantic, serializeEmbeddings, sh, sha1, sharedGrammarsCacheDir, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, tokenize, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, warmGrammars, wordpiece, writeMemory };
|
package/scripts/engine.mjs
CHANGED
|
@@ -14,7 +14,7 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
|
|
|
14
14
|
var init_types = __esm({
|
|
15
15
|
"src/types.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
ENGINE_VERSION = "2.
|
|
17
|
+
ENGINE_VERSION = "2.17.1";
|
|
18
18
|
SCHEMA_VERSION = 4;
|
|
19
19
|
EXTRACTOR_VERSION = 10;
|
|
20
20
|
}
|
|
@@ -10440,8 +10440,8 @@ async function warmGrammarsForRepo(repo) {
|
|
|
10440
10440
|
const { files } = walk(repo, {});
|
|
10441
10441
|
await ensureGrammars(grammarKeysForExts(files.map((f) => f.ext)));
|
|
10442
10442
|
}
|
|
10443
|
-
async function callTool(name2, args2) {
|
|
10444
|
-
const repo = str(args2.repo);
|
|
10443
|
+
async function callTool(name2, args2, defaultRepo) {
|
|
10444
|
+
const repo = str(args2.repo) ?? defaultRepo;
|
|
10445
10445
|
if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
|
|
10446
10446
|
const scanOpts = { scope: str(args2.scope), include: strArray(args2.include), exclude: strArray(args2.exclude) };
|
|
10447
10447
|
if (!SCANLESS_TOOLS.has(name2)) await warmGrammarsForRepo(repo);
|
|
@@ -10636,11 +10636,26 @@ async function callTool(name2, args2) {
|
|
|
10636
10636
|
}
|
|
10637
10637
|
throw new Error(`unknown tool: ${name2}`);
|
|
10638
10638
|
}
|
|
10639
|
+
function toolsFor(defaultRepo) {
|
|
10640
|
+
if (!defaultRepo) return TOOLS;
|
|
10641
|
+
return TOOLS.map((t) => ({
|
|
10642
|
+
...t,
|
|
10643
|
+
inputSchema: {
|
|
10644
|
+
...t.inputSchema,
|
|
10645
|
+
properties: {
|
|
10646
|
+
...t.inputSchema.properties,
|
|
10647
|
+
repo: { type: "string", description: `Absolute path to the repository root (optional \u2014 defaults to ${defaultRepo})` }
|
|
10648
|
+
},
|
|
10649
|
+
required: t.inputSchema.required.filter((r) => r !== "repo")
|
|
10650
|
+
}
|
|
10651
|
+
}));
|
|
10652
|
+
}
|
|
10639
10653
|
async function runMcpServer(opts = {}) {
|
|
10640
10654
|
const serverInfo = {
|
|
10641
10655
|
name: opts.serverInfo?.name ?? "codeindex",
|
|
10642
10656
|
version: opts.serverInfo?.version ?? ENGINE_VERSION
|
|
10643
10657
|
};
|
|
10658
|
+
const tools = toolsFor(opts.defaultRepo);
|
|
10644
10659
|
const send = (msg) => {
|
|
10645
10660
|
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n");
|
|
10646
10661
|
};
|
|
@@ -10673,13 +10688,13 @@ async function runMcpServer(opts = {}) {
|
|
|
10673
10688
|
} else if (req.method === "ping") {
|
|
10674
10689
|
send({ id: req.id, result: {} });
|
|
10675
10690
|
} else if (req.method === "tools/list") {
|
|
10676
|
-
send({ id: req.id, result: { tools
|
|
10691
|
+
send({ id: req.id, result: { tools } });
|
|
10677
10692
|
} else if (req.method === "tools/call") {
|
|
10678
10693
|
const params = req.params ?? {};
|
|
10679
10694
|
const name2 = str(params.name) ?? "";
|
|
10680
10695
|
const args2 = params.arguments ?? {};
|
|
10681
10696
|
try {
|
|
10682
|
-
const text = await callTool(name2, args2);
|
|
10697
|
+
const text = await callTool(name2, args2, opts.defaultRepo);
|
|
10683
10698
|
send({ id: req.id, result: { content: [{ type: "text", text }] } });
|
|
10684
10699
|
} catch (e) {
|
|
10685
10700
|
send({
|
|
@@ -10989,6 +11004,121 @@ var init_mcp = __esm({
|
|
|
10989
11004
|
}
|
|
10990
11005
|
});
|
|
10991
11006
|
|
|
11007
|
+
// src/rewrite.ts
|
|
11008
|
+
var rewrite_exports = {};
|
|
11009
|
+
__export(rewrite_exports, {
|
|
11010
|
+
rewriteCommand: () => rewriteCommand,
|
|
11011
|
+
shellQuote: () => shellQuote,
|
|
11012
|
+
tokenize: () => tokenize2
|
|
11013
|
+
});
|
|
11014
|
+
function tokenize2(line) {
|
|
11015
|
+
const out2 = [];
|
|
11016
|
+
let cur = "";
|
|
11017
|
+
let quote;
|
|
11018
|
+
let started = false;
|
|
11019
|
+
for (let i2 = 0; i2 < line.length; i2++) {
|
|
11020
|
+
const c2 = line[i2];
|
|
11021
|
+
if (quote) {
|
|
11022
|
+
if (c2 === quote) quote = void 0;
|
|
11023
|
+
else cur += c2;
|
|
11024
|
+
continue;
|
|
11025
|
+
}
|
|
11026
|
+
if (c2 === '"' || c2 === "'") {
|
|
11027
|
+
quote = c2;
|
|
11028
|
+
started = true;
|
|
11029
|
+
continue;
|
|
11030
|
+
}
|
|
11031
|
+
if (c2 === " " || c2 === " ") {
|
|
11032
|
+
if (started || cur) out2.push(cur);
|
|
11033
|
+
cur = "";
|
|
11034
|
+
started = false;
|
|
11035
|
+
continue;
|
|
11036
|
+
}
|
|
11037
|
+
cur += c2;
|
|
11038
|
+
}
|
|
11039
|
+
if (quote) return void 0;
|
|
11040
|
+
if (started || cur) out2.push(cur);
|
|
11041
|
+
return out2;
|
|
11042
|
+
}
|
|
11043
|
+
function shellQuote(s) {
|
|
11044
|
+
if (s !== "" && !/[^A-Za-z0-9_\-./=@:]/.test(s)) return s;
|
|
11045
|
+
return "'" + s.replace(/'/g, `'\\''`) + "'";
|
|
11046
|
+
}
|
|
11047
|
+
function parseSearch(bin, args2) {
|
|
11048
|
+
const p = { ignoreCase: false, includes: [], recursive: bin !== "grep" && bin !== "egrep" };
|
|
11049
|
+
const positionals = [];
|
|
11050
|
+
for (let i2 = 0; i2 < args2.length; i2++) {
|
|
11051
|
+
const a = args2[i2];
|
|
11052
|
+
if (a === void 0) continue;
|
|
11053
|
+
if (a === "--") {
|
|
11054
|
+
positionals.push(...args2.slice(i2 + 1));
|
|
11055
|
+
break;
|
|
11056
|
+
}
|
|
11057
|
+
if (!a.startsWith("-") || a === "-") {
|
|
11058
|
+
positionals.push(a);
|
|
11059
|
+
continue;
|
|
11060
|
+
}
|
|
11061
|
+
if (a === "-i" || a === "--ignore-case") {
|
|
11062
|
+
p.ignoreCase = true;
|
|
11063
|
+
} else if (a === "-r" || a === "-R" || a === "--recursive") {
|
|
11064
|
+
p.recursive = true;
|
|
11065
|
+
} else if (a === "-n" || a === "--line-number" || a === "-H" || a === "--with-filename" || a === "--no-heading") {
|
|
11066
|
+
} else if (a === "-e" || a === "--regexp") {
|
|
11067
|
+
const v = args2[++i2];
|
|
11068
|
+
if (v === void 0 || p.pattern !== void 0) return void 0;
|
|
11069
|
+
p.pattern = v;
|
|
11070
|
+
} else if (a.startsWith("--include=")) {
|
|
11071
|
+
p.includes.push(a.slice("--include=".length));
|
|
11072
|
+
} else if (a === "--include" || a === "-g" || a === "--glob") {
|
|
11073
|
+
const v = args2[++i2];
|
|
11074
|
+
if (v === void 0) return void 0;
|
|
11075
|
+
p.includes.push(v);
|
|
11076
|
+
} else if (a.length > 2 && /^-[a-zA-Z]+$/.test(a)) {
|
|
11077
|
+
const expanded = a.slice(1).split("").map((c2) => `-${c2}`);
|
|
11078
|
+
args2.splice(i2, 1, ...expanded);
|
|
11079
|
+
i2--;
|
|
11080
|
+
} else {
|
|
11081
|
+
return void 0;
|
|
11082
|
+
}
|
|
11083
|
+
}
|
|
11084
|
+
if (p.pattern === void 0) {
|
|
11085
|
+
const first = positionals.shift();
|
|
11086
|
+
if (first === void 0 || first === "") return void 0;
|
|
11087
|
+
p.pattern = first;
|
|
11088
|
+
}
|
|
11089
|
+
if (positionals.length > 1) return void 0;
|
|
11090
|
+
p.path = positionals[0];
|
|
11091
|
+
return p;
|
|
11092
|
+
}
|
|
11093
|
+
function rewriteCommand(cmd, bin = "codeindex") {
|
|
11094
|
+
const line = cmd.trim();
|
|
11095
|
+
if (!line || SHELL_METACHARS.test(line)) return void 0;
|
|
11096
|
+
const tokens = tokenize2(line);
|
|
11097
|
+
if (!tokens || tokens.length < 2) return void 0;
|
|
11098
|
+
const [head, ...args2] = tokens;
|
|
11099
|
+
if (head === void 0 || !GREP_BINARIES.has(head)) return void 0;
|
|
11100
|
+
const p = parseSearch(head, args2);
|
|
11101
|
+
if (!p || p.pattern === void 0) return void 0;
|
|
11102
|
+
const pattern = p.pattern;
|
|
11103
|
+
if (!p.recursive) return void 0;
|
|
11104
|
+
const path = p.path;
|
|
11105
|
+
const out2 = [bin, "grep", shellQuote(pattern)];
|
|
11106
|
+
if (path && path !== "." && path !== "./") {
|
|
11107
|
+
out2.push("--scope", shellQuote(path.replace(/\/+$/, "")));
|
|
11108
|
+
}
|
|
11109
|
+
if (p.ignoreCase) out2.push("--ignore-case");
|
|
11110
|
+
for (const g of p.includes) out2.push("--include", shellQuote(g));
|
|
11111
|
+
return out2.join(" ");
|
|
11112
|
+
}
|
|
11113
|
+
var SHELL_METACHARS, GREP_BINARIES;
|
|
11114
|
+
var init_rewrite = __esm({
|
|
11115
|
+
"src/rewrite.ts"() {
|
|
11116
|
+
"use strict";
|
|
11117
|
+
SHELL_METACHARS = /[|&;<>`\n\r$(){}]/;
|
|
11118
|
+
GREP_BINARIES = /* @__PURE__ */ new Set(["grep", "egrep", "rg", "ripgrep"]);
|
|
11119
|
+
}
|
|
11120
|
+
});
|
|
11121
|
+
|
|
10992
11122
|
// src/engine.ts
|
|
10993
11123
|
init_types();
|
|
10994
11124
|
init_walk();
|
|
@@ -11601,6 +11731,7 @@ init_deadcode();
|
|
|
11601
11731
|
init_complexity();
|
|
11602
11732
|
init_viz();
|
|
11603
11733
|
init_mcp();
|
|
11734
|
+
init_rewrite();
|
|
11604
11735
|
init_hash();
|
|
11605
11736
|
init_sort();
|
|
11606
11737
|
init_util();
|
|
@@ -11677,11 +11808,31 @@ Commands:
|
|
|
11677
11808
|
repomap Token-budgeted map of the highest-PageRank files (--budget-tokens)
|
|
11678
11809
|
hotspots Churn \xD7 size ranking of the files where work concentrates (JSON)
|
|
11679
11810
|
coupling Change coupling: files that change together (JSON; --since <ref>)
|
|
11680
|
-
|
|
11681
|
-
|
|
11811
|
+
deadcode Dead-code candidates in two labeled tiers: 'unreferenced' (no
|
|
11812
|
+
call site binds AND nothing references the name) and 'uncalled'
|
|
11813
|
+
(referenced \u2014 re-export, type position \u2014 but never called)
|
|
11814
|
+
complexity Cyclomatic-complexity estimates, most-complex first. Pass a file
|
|
11815
|
+
positional for one file; omit for the repo-wide top
|
|
11816
|
+
risk Complexity \xD7 git-churn ranking (JSON; --since <ref> to bound)
|
|
11817
|
+
mermaid Mermaid diagram of the module graph; pass a module positional to
|
|
11818
|
+
focus on one neighborhood
|
|
11819
|
+
rewrite Map an expensive tree-wide search onto its indexed equivalent:
|
|
11820
|
+
cli.mjs rewrite '<command line>'. Prints the replacement command
|
|
11821
|
+
and exits 0, or exits 1 when it has no opinion (run the original).
|
|
11822
|
+
Deliberately conservative \u2014 any shell metacharacter or unknown
|
|
11823
|
+
flag refuses the rewrite
|
|
11824
|
+
mcp Run as an MCP server over stdio (26 tools: scan_summary, graph,
|
|
11825
|
+
symbols, callers, workspaces, churn, symbols_overview,
|
|
11826
|
+
find_symbol, find_references, repo_map, hotspots, coupling,
|
|
11827
|
+
dead_code, complexity, mermaid, grep, search, embed_status,
|
|
11828
|
+
check_rules, the memory quartet and the three symbolic-edit
|
|
11829
|
+
writes). Flags: --repo <dir> pins ONE repository so the per-tool
|
|
11830
|
+
repo argument becomes optional (an explicit per-call repo still
|
|
11831
|
+
wins); --server-name <name> overrides the announced serverInfo
|
|
11682
11832
|
version Print the engine version
|
|
11683
11833
|
|
|
11684
|
-
Flags:
|
|
11834
|
+
Flags (accepted before OR after the subcommand: '--repo X scan' and
|
|
11835
|
+
'scan --repo X' are equivalent):
|
|
11685
11836
|
--repo <dir> Repo root (default: cwd)
|
|
11686
11837
|
--out <file> Write output to a file instead of stdout (\`scip\`: --out -
|
|
11687
11838
|
writes the binary index to stdout)
|
|
@@ -11708,6 +11859,8 @@ Flags:
|
|
|
11708
11859
|
--recall \`callers\`: recall-oriented binding (issue #7) \u2014 relaxes
|
|
11709
11860
|
the JS/TS import gate to unique repo-wide names and labels
|
|
11710
11861
|
each site corroborated|unique-name
|
|
11862
|
+
--ignore-case \`grep\`: case-insensitive matching
|
|
11863
|
+
--max-hits <n> \`grep\`: cap returned hits (default 200)
|
|
11711
11864
|
`;
|
|
11712
11865
|
function parseFlags(args2) {
|
|
11713
11866
|
const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, ignoreDirs: [], noAst: false, fuzzy: true, semantic: false };
|
|
@@ -11774,7 +11927,62 @@ function scanOptions(flags2, precomputedWalk) {
|
|
|
11774
11927
|
};
|
|
11775
11928
|
}
|
|
11776
11929
|
var SCANLESS_COMMANDS = /* @__PURE__ */ new Set(["grep", "churn", "coupling", "workspaces", "grammars"]);
|
|
11777
|
-
|
|
11930
|
+
function parseMcpFlags(argv) {
|
|
11931
|
+
let defaultRepo;
|
|
11932
|
+
let name2;
|
|
11933
|
+
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
11934
|
+
const a = argv[i2];
|
|
11935
|
+
if (a === "--repo") {
|
|
11936
|
+
const v = argv[++i2];
|
|
11937
|
+
if (!v) throw new Error("--repo requires a directory");
|
|
11938
|
+
defaultRepo = resolve2(v);
|
|
11939
|
+
} else if (a === "--server-name") {
|
|
11940
|
+
const v = argv[++i2];
|
|
11941
|
+
if (!v) throw new Error("--server-name requires a value");
|
|
11942
|
+
name2 = v;
|
|
11943
|
+
} else {
|
|
11944
|
+
throw new Error(`unknown flag for \`mcp\`: ${a}`);
|
|
11945
|
+
}
|
|
11946
|
+
}
|
|
11947
|
+
if (defaultRepo && !existsSync5(defaultRepo)) throw new Error(`--repo path does not exist: ${defaultRepo}`);
|
|
11948
|
+
return { defaultRepo, serverInfo: name2 ? { name: name2 } : void 0 };
|
|
11949
|
+
}
|
|
11950
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
11951
|
+
"--repo",
|
|
11952
|
+
"--out",
|
|
11953
|
+
"--project-root",
|
|
11954
|
+
"--include",
|
|
11955
|
+
"--exclude",
|
|
11956
|
+
"--scope",
|
|
11957
|
+
"--ignore-dir",
|
|
11958
|
+
"--max-files",
|
|
11959
|
+
"--max-bytes",
|
|
11960
|
+
"--max-calls",
|
|
11961
|
+
"--max-hits",
|
|
11962
|
+
"--budget-tokens",
|
|
11963
|
+
"--since",
|
|
11964
|
+
"--config",
|
|
11965
|
+
"--limit",
|
|
11966
|
+
"--server-name"
|
|
11967
|
+
]);
|
|
11968
|
+
function hoistLeadingFlags(argv) {
|
|
11969
|
+
const lead = [];
|
|
11970
|
+
let i2 = 0;
|
|
11971
|
+
while (i2 < argv.length) {
|
|
11972
|
+
const a = argv[i2];
|
|
11973
|
+
if (a === void 0 || !a.startsWith("-")) break;
|
|
11974
|
+
lead.push(a);
|
|
11975
|
+
i2++;
|
|
11976
|
+
if (VALUE_FLAGS.has(a) && i2 < argv.length) {
|
|
11977
|
+
lead.push(argv[i2]);
|
|
11978
|
+
i2++;
|
|
11979
|
+
}
|
|
11980
|
+
}
|
|
11981
|
+
if (lead.length === 0 || i2 >= argv.length) return argv;
|
|
11982
|
+
return [argv[i2], ...lead, ...argv.slice(i2 + 1)];
|
|
11983
|
+
}
|
|
11984
|
+
async function runCli(rawArgv) {
|
|
11985
|
+
const argv = hoistLeadingFlags(rawArgv);
|
|
11778
11986
|
const [cmd, ...rest] = argv;
|
|
11779
11987
|
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
11780
11988
|
process.stdout.write(HELP);
|
|
@@ -11784,9 +11992,19 @@ async function runCli(argv) {
|
|
|
11784
11992
|
process.stdout.write(ENGINE_VERSION + "\n");
|
|
11785
11993
|
return;
|
|
11786
11994
|
}
|
|
11995
|
+
if (cmd === "rewrite") {
|
|
11996
|
+
const { rewriteCommand: rewriteCommand2 } = await Promise.resolve().then(() => (init_rewrite(), rewrite_exports));
|
|
11997
|
+
const rewritten = rewriteCommand2(rest.join(" "));
|
|
11998
|
+
if (!rewritten) {
|
|
11999
|
+
process.exitCode = 1;
|
|
12000
|
+
return;
|
|
12001
|
+
}
|
|
12002
|
+
process.stdout.write(rewritten + "\n");
|
|
12003
|
+
return;
|
|
12004
|
+
}
|
|
11787
12005
|
if (cmd === "mcp") {
|
|
11788
12006
|
const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
|
|
11789
|
-
await runMcpServer2();
|
|
12007
|
+
await runMcpServer2(parseMcpFlags(rest));
|
|
11790
12008
|
return;
|
|
11791
12009
|
}
|
|
11792
12010
|
const flags2 = parseFlags(rest);
|
|
@@ -12117,7 +12335,8 @@ async function runCli(argv) {
|
|
|
12117
12335
|
emit(renderMermaid(graph, { module: flags2.positional }), flags2.out);
|
|
12118
12336
|
} else if (cmd === "grep") {
|
|
12119
12337
|
if (!flags2.positional) throw new Error("grep needs a pattern: cli.mjs grep <pattern> --repo <dir>");
|
|
12120
|
-
const
|
|
12338
|
+
const scopeGlobs = flags2.scope ? [`${flags2.scope.replace(/\/+$/, "")}/**`] : [];
|
|
12339
|
+
const globs = [...scopeGlobs, ...flags2.include, ...flags2.exclude.map((g) => `!${g}`)];
|
|
12121
12340
|
const hits = grepRepo(flags2.repo, flags2.positional, {
|
|
12122
12341
|
globs: globs.length ? globs : void 0,
|
|
12123
12342
|
ignoreCase: flags2.ignoreCase,
|
|
@@ -12246,6 +12465,7 @@ export {
|
|
|
12246
12465
|
resolveGrammarsTier,
|
|
12247
12466
|
resolveImport,
|
|
12248
12467
|
resolveUniqueSymbol,
|
|
12468
|
+
rewriteCommand,
|
|
12249
12469
|
riskHotspots,
|
|
12250
12470
|
roundHalfToEven,
|
|
12251
12471
|
rrf,
|
package/src/engine-cli.ts
CHANGED
|
@@ -77,11 +77,31 @@ Commands:
|
|
|
77
77
|
repomap Token-budgeted map of the highest-PageRank files (--budget-tokens)
|
|
78
78
|
hotspots Churn × size ranking of the files where work concentrates (JSON)
|
|
79
79
|
coupling Change coupling: files that change together (JSON; --since <ref>)
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
deadcode Dead-code candidates in two labeled tiers: 'unreferenced' (no
|
|
81
|
+
call site binds AND nothing references the name) and 'uncalled'
|
|
82
|
+
(referenced — re-export, type position — but never called)
|
|
83
|
+
complexity Cyclomatic-complexity estimates, most-complex first. Pass a file
|
|
84
|
+
positional for one file; omit for the repo-wide top
|
|
85
|
+
risk Complexity × git-churn ranking (JSON; --since <ref> to bound)
|
|
86
|
+
mermaid Mermaid diagram of the module graph; pass a module positional to
|
|
87
|
+
focus on one neighborhood
|
|
88
|
+
rewrite Map an expensive tree-wide search onto its indexed equivalent:
|
|
89
|
+
cli.mjs rewrite '<command line>'. Prints the replacement command
|
|
90
|
+
and exits 0, or exits 1 when it has no opinion (run the original).
|
|
91
|
+
Deliberately conservative — any shell metacharacter or unknown
|
|
92
|
+
flag refuses the rewrite
|
|
93
|
+
mcp Run as an MCP server over stdio (26 tools: scan_summary, graph,
|
|
94
|
+
symbols, callers, workspaces, churn, symbols_overview,
|
|
95
|
+
find_symbol, find_references, repo_map, hotspots, coupling,
|
|
96
|
+
dead_code, complexity, mermaid, grep, search, embed_status,
|
|
97
|
+
check_rules, the memory quartet and the three symbolic-edit
|
|
98
|
+
writes). Flags: --repo <dir> pins ONE repository so the per-tool
|
|
99
|
+
repo argument becomes optional (an explicit per-call repo still
|
|
100
|
+
wins); --server-name <name> overrides the announced serverInfo
|
|
82
101
|
version Print the engine version
|
|
83
102
|
|
|
84
|
-
Flags:
|
|
103
|
+
Flags (accepted before OR after the subcommand: '--repo X scan' and
|
|
104
|
+
'scan --repo X' are equivalent):
|
|
85
105
|
--repo <dir> Repo root (default: cwd)
|
|
86
106
|
--out <file> Write output to a file instead of stdout (\`scip\`: --out -
|
|
87
107
|
writes the binary index to stdout)
|
|
@@ -108,6 +128,8 @@ Flags:
|
|
|
108
128
|
--recall \`callers\`: recall-oriented binding (issue #7) — relaxes
|
|
109
129
|
the JS/TS import gate to unique repo-wide names and labels
|
|
110
130
|
each site corroborated|unique-name
|
|
131
|
+
--ignore-case \`grep\`: case-insensitive matching
|
|
132
|
+
--max-hits <n> \`grep\`: cap returned hits (default 200)
|
|
111
133
|
`;
|
|
112
134
|
|
|
113
135
|
interface CliFlags {
|
|
@@ -212,7 +234,85 @@ function scanOptions(flags: CliFlags, precomputedWalk?: WalkResult): BuildIndexO
|
|
|
212
234
|
// version/help/mcp return before we get there.
|
|
213
235
|
const SCANLESS_COMMANDS = new Set(["grep", "churn", "coupling", "workspaces", "grammars"]);
|
|
214
236
|
|
|
215
|
-
|
|
237
|
+
// Flags for `codeindex mcp`. Kept separate from parseFlags on purpose (see the
|
|
238
|
+
// dispatch site). `--repo` is resolved to an absolute path and must exist: a
|
|
239
|
+
// server pinned to a typo'd directory would otherwise answer every tool call
|
|
240
|
+
// with the same confusing per-call error instead of failing at startup.
|
|
241
|
+
export function parseMcpFlags(argv: string[]): { defaultRepo?: string; serverInfo?: { name?: string } } {
|
|
242
|
+
let defaultRepo: string | undefined;
|
|
243
|
+
let name: string | undefined;
|
|
244
|
+
for (let i = 0; i < argv.length; i++) {
|
|
245
|
+
const a = argv[i];
|
|
246
|
+
if (a === "--repo") {
|
|
247
|
+
const v = argv[++i];
|
|
248
|
+
if (!v) throw new Error("--repo requires a directory");
|
|
249
|
+
defaultRepo = resolve(v);
|
|
250
|
+
} else if (a === "--server-name") {
|
|
251
|
+
const v = argv[++i];
|
|
252
|
+
if (!v) throw new Error("--server-name requires a value");
|
|
253
|
+
name = v;
|
|
254
|
+
} else {
|
|
255
|
+
throw new Error(`unknown flag for \`mcp\`: ${a}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (defaultRepo && !existsSync(defaultRepo)) throw new Error(`--repo path does not exist: ${defaultRepo}`);
|
|
259
|
+
return { defaultRepo, serverInfo: name ? { name } : undefined };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Flags that consume the following argv element. Needed to hoist leading flags
|
|
263
|
+
// past the subcommand without mistaking a flag's VALUE for the command name
|
|
264
|
+
// (`--repo /x scan`: `/x` must not be read as the command).
|
|
265
|
+
const VALUE_FLAGS = new Set([
|
|
266
|
+
"--repo",
|
|
267
|
+
"--out",
|
|
268
|
+
"--project-root",
|
|
269
|
+
"--include",
|
|
270
|
+
"--exclude",
|
|
271
|
+
"--scope",
|
|
272
|
+
"--ignore-dir",
|
|
273
|
+
"--max-files",
|
|
274
|
+
"--max-bytes",
|
|
275
|
+
"--max-calls",
|
|
276
|
+
"--max-hits",
|
|
277
|
+
"--budget-tokens",
|
|
278
|
+
"--since",
|
|
279
|
+
"--config",
|
|
280
|
+
"--limit",
|
|
281
|
+
"--server-name",
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
// Accept global flags BEFORE the subcommand as well as after, so
|
|
285
|
+
// `codeindex --repo /x scan` and `codeindex scan --repo /x` agree. A strict
|
|
286
|
+
// subcommand-first parser reads the leading flag as the command name and fails
|
|
287
|
+
// with a baffling "unknown flag: scan".
|
|
288
|
+
//
|
|
289
|
+
// This is not only ergonomics. A host that wraps the CLI may splice a flag in
|
|
290
|
+
// right after the binary name — iterion's rewriter `inject_flag` does exactly
|
|
291
|
+
// that, turning `codeindex grep foo` into `codeindex --max-hits 40 grep foo` —
|
|
292
|
+
// and without hoisting that command cannot run at all.
|
|
293
|
+
//
|
|
294
|
+
// Returns argv unchanged when there is nothing to hoist, so `--help`,
|
|
295
|
+
// `--version` and a bare subcommand all keep their existing behaviour.
|
|
296
|
+
export function hoistLeadingFlags(argv: string[]): string[] {
|
|
297
|
+
const lead: string[] = [];
|
|
298
|
+
let i = 0;
|
|
299
|
+
while (i < argv.length) {
|
|
300
|
+
const a = argv[i];
|
|
301
|
+
if (a === undefined || !a.startsWith("-")) break;
|
|
302
|
+
lead.push(a);
|
|
303
|
+
i++;
|
|
304
|
+
if (VALUE_FLAGS.has(a) && i < argv.length) {
|
|
305
|
+
lead.push(argv[i] as string);
|
|
306
|
+
i++;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// No leading flags, or they were the whole line (`--help`, `--version`).
|
|
310
|
+
if (lead.length === 0 || i >= argv.length) return argv;
|
|
311
|
+
return [argv[i] as string, ...lead, ...argv.slice(i + 1)];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export async function runCli(rawArgv: string[]): Promise<void> {
|
|
315
|
+
const argv = hoistLeadingFlags(rawArgv);
|
|
216
316
|
const [cmd, ...rest] = argv;
|
|
217
317
|
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
218
318
|
process.stdout.write(HELP);
|
|
@@ -222,9 +322,28 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
222
322
|
process.stdout.write(ENGINE_VERSION + "\n");
|
|
223
323
|
return;
|
|
224
324
|
}
|
|
325
|
+
if (cmd === "rewrite") {
|
|
326
|
+
// Host contract (iterion's `rewriters` kind, rtk's generalization): stdin
|
|
327
|
+
// is nothing, argv is ONE full command line, stdout is the command to run
|
|
328
|
+
// instead, and the exit code says whether to use it. Exit 1 = "no opinion,
|
|
329
|
+
// run the original" — the overwhelmingly common, deliberately cheap case.
|
|
330
|
+
const { rewriteCommand } = await import("./rewrite.js");
|
|
331
|
+
const rewritten = rewriteCommand(rest.join(" "));
|
|
332
|
+
if (!rewritten) {
|
|
333
|
+
process.exitCode = 1;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
process.stdout.write(rewritten + "\n");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
225
339
|
if (cmd === "mcp") {
|
|
340
|
+
// `mcp` takes a deliberately tiny, self-contained flag set rather than
|
|
341
|
+
// going through parseFlags: the shared parser owns positional/scope
|
|
342
|
+
// semantics that mean nothing to a long-lived server, and every unknown
|
|
343
|
+
// flag there is fatal. Pinning is OPT-IN — a bare `codeindex mcp` keeps
|
|
344
|
+
// the historical contract where each tool call carries its own `repo`.
|
|
226
345
|
const { runMcpServer } = await import("./mcp.js");
|
|
227
|
-
await runMcpServer();
|
|
346
|
+
await runMcpServer(parseMcpFlags(rest));
|
|
228
347
|
return;
|
|
229
348
|
}
|
|
230
349
|
|
|
@@ -646,7 +765,12 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
646
765
|
emit(renderMermaid(graph, { module: flags.positional }), flags.out);
|
|
647
766
|
} else if (cmd === "grep") {
|
|
648
767
|
if (!flags.positional) throw new Error("grep needs a pattern: cli.mjs grep <pattern> --repo <dir>");
|
|
649
|
-
|
|
768
|
+
// `--scope <dir>` is documented as global sugar for `--include '<dir>/**'`;
|
|
769
|
+
// every other command gets it via scanOptions, but grep bypasses the scan
|
|
770
|
+
// and builds its own glob list — so it has to fold the sugar in itself, or
|
|
771
|
+
// the flag would be silently ignored here alone.
|
|
772
|
+
const scopeGlobs = flags.scope ? [`${flags.scope.replace(/\/+$/, "")}/**`] : [];
|
|
773
|
+
const globs = [...scopeGlobs, ...flags.include, ...flags.exclude.map((g) => `!${g}`)];
|
|
650
774
|
const hits = grepRepo(flags.repo, flags.positional, {
|
|
651
775
|
globs: globs.length ? globs : undefined,
|
|
652
776
|
ignoreCase: flags.ignoreCase,
|
package/src/engine.ts
CHANGED
|
@@ -185,6 +185,13 @@ export type { RepoMapOptions } from "./repomap.js";
|
|
|
185
185
|
export { runMcpServer } from "./mcp.js";
|
|
186
186
|
export type { McpServerOptions } from "./mcp.js";
|
|
187
187
|
|
|
188
|
+
// Command rewriting: an expensive tree-wide search → its indexed equivalent.
|
|
189
|
+
// Exported so a host can decide for itself rather than shelling out to
|
|
190
|
+
// `engine.mjs rewrite` (same conservative refusal semantics either way).
|
|
191
|
+
// Only the decision function is public — the tokenizer/quoter are internal
|
|
192
|
+
// details, and `tokenize` is already taken here by the wordpiece tokenizer.
|
|
193
|
+
export { rewriteCommand } from "./rewrite.js";
|
|
194
|
+
|
|
188
195
|
// General-purpose helpers shared by consumers (deterministic, dependency-free).
|
|
189
196
|
export { sha1, shortHash } from "./hash.js";
|
|
190
197
|
export { byStr, byKey } from "./sort.js";
|
package/src/mcp.ts
CHANGED
|
@@ -667,8 +667,11 @@ const SCANLESS_TOOLS = new Set([
|
|
|
667
667
|
"embed_status",
|
|
668
668
|
]);
|
|
669
669
|
|
|
670
|
-
async function callTool(name: string, args: Record<string, unknown
|
|
671
|
-
|
|
670
|
+
async function callTool(name: string, args: Record<string, unknown>, defaultRepo?: string): Promise<string> {
|
|
671
|
+
// An explicit per-call `repo` always wins; `defaultRepo` is the server-level
|
|
672
|
+
// pin (`codeindex mcp --repo <dir>`) that lets a host bind one server process
|
|
673
|
+
// to one workspace, so agents need not know — or restate — the absolute path.
|
|
674
|
+
const repo = str(args.repo) ?? defaultRepo;
|
|
672
675
|
if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
|
|
673
676
|
const scanOpts = { scope: str(args.scope), include: strArray(args.include), exclude: strArray(args.exclude) };
|
|
674
677
|
// Scan-needing tools warm the present-language grammars (re-derived per call)
|
|
@@ -886,11 +889,36 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<st
|
|
|
886
889
|
throw new Error(`unknown tool: ${name}`);
|
|
887
890
|
}
|
|
888
891
|
|
|
892
|
+
// The advertised tool list. Without a server-level repo pin this is TOOLS
|
|
893
|
+
// verbatim (byte-compat for existing consumers). With one, every tool drops
|
|
894
|
+
// `repo` from its `required` set and documents the default — so a client that
|
|
895
|
+
// omits it is spec-correct rather than relying on the server being lenient.
|
|
896
|
+
// The pin is reflected in the schema, not just honoured at call time.
|
|
897
|
+
function toolsFor(defaultRepo?: string): readonly unknown[] {
|
|
898
|
+
if (!defaultRepo) return TOOLS;
|
|
899
|
+
return TOOLS.map((t) => ({
|
|
900
|
+
...t,
|
|
901
|
+
inputSchema: {
|
|
902
|
+
...t.inputSchema,
|
|
903
|
+
properties: {
|
|
904
|
+
...t.inputSchema.properties,
|
|
905
|
+
repo: { type: "string", description: `Absolute path to the repository root (optional — defaults to ${defaultRepo})` },
|
|
906
|
+
},
|
|
907
|
+
required: (t.inputSchema.required as readonly string[]).filter((r) => r !== "repo"),
|
|
908
|
+
},
|
|
909
|
+
}));
|
|
910
|
+
}
|
|
911
|
+
|
|
889
912
|
export interface McpServerOptions {
|
|
890
913
|
// Override the serverInfo announced in the initialize response — for
|
|
891
914
|
// downstream consumers embedding this server under their own identity.
|
|
892
915
|
// Omitted fields keep the defaults (name "codeindex", ENGINE_VERSION).
|
|
893
916
|
serverInfo?: { name?: string; version?: string };
|
|
917
|
+
// Bind the server to ONE repository, so `repo` becomes optional on every
|
|
918
|
+
// tool (an explicit per-call `repo` still wins). This is what lets a host
|
|
919
|
+
// spawn one server per workspace — `codeindex mcp --repo <dir>` — instead of
|
|
920
|
+
// requiring the agent to thread an absolute path through every single call.
|
|
921
|
+
defaultRepo?: string;
|
|
894
922
|
}
|
|
895
923
|
|
|
896
924
|
export async function runMcpServer(opts: McpServerOptions = {}): Promise<void> {
|
|
@@ -898,6 +926,8 @@ export async function runMcpServer(opts: McpServerOptions = {}): Promise<void> {
|
|
|
898
926
|
name: opts.serverInfo?.name ?? "codeindex",
|
|
899
927
|
version: opts.serverInfo?.version ?? ENGINE_VERSION,
|
|
900
928
|
};
|
|
929
|
+
// Derived ONCE: the pin cannot change mid-session, so neither can the list.
|
|
930
|
+
const tools = toolsFor(opts.defaultRepo);
|
|
901
931
|
// No startup warm: each scan-needing tool warms the present-language grammars
|
|
902
932
|
// for its repo before it runs (warmGrammarsForRepo re-derives them per call),
|
|
903
933
|
// so a session that never scans — or only touches one language — loads no
|
|
@@ -940,13 +970,13 @@ export async function runMcpServer(opts: McpServerOptions = {}): Promise<void> {
|
|
|
940
970
|
} else if (req.method === "ping") {
|
|
941
971
|
send({ id: req.id, result: {} });
|
|
942
972
|
} else if (req.method === "tools/list") {
|
|
943
|
-
send({ id: req.id, result: { tools
|
|
973
|
+
send({ id: req.id, result: { tools } });
|
|
944
974
|
} else if (req.method === "tools/call") {
|
|
945
975
|
const params = req.params ?? {};
|
|
946
976
|
const name = str(params.name) ?? "";
|
|
947
977
|
const args = (params.arguments ?? {}) as Record<string, unknown>;
|
|
948
978
|
try {
|
|
949
|
-
const text = await callTool(name, args);
|
|
979
|
+
const text = await callTool(name, args, opts.defaultRepo);
|
|
950
980
|
send({ id: req.id, result: { content: [{ type: "text", text }] } });
|
|
951
981
|
} catch (e) {
|
|
952
982
|
send({
|
package/src/rewrite.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Command rewriting: map an expensive full-tree text search onto the engine's
|
|
2
|
+
// indexed equivalent, so an agent shell that runs `grep -r foo .` gets bounded,
|
|
3
|
+
// structured, gitignore-correct hits instead of an unbounded wall of text.
|
|
4
|
+
//
|
|
5
|
+
// The contract is a HOST contract, not a shell one: the caller hands us a full
|
|
6
|
+
// command line and takes our stdout as the command to run instead (iterion's
|
|
7
|
+
// `rewriters` plugin kind, rtk's generalization). That makes a wrong rewrite
|
|
8
|
+
// far worse than no rewrite — it silently changes what the agent asked for.
|
|
9
|
+
// So this module is deliberately, aggressively conservative:
|
|
10
|
+
//
|
|
11
|
+
// * anything the parser cannot prove it understands → NO rewrite
|
|
12
|
+
// * any shell metacharacter at all → NO rewrite (we refuse to reason about
|
|
13
|
+
// pipelines, redirection, substitution or chaining)
|
|
14
|
+
// * any flag not on the explicit allowlist → NO rewrite
|
|
15
|
+
//
|
|
16
|
+
// A refusal is cheap (the original command runs untouched); a bad rewrite is
|
|
17
|
+
// not. Every branch here defaults to refusing.
|
|
18
|
+
|
|
19
|
+
// Shell constructs we will not reason about. Their presence anywhere in the
|
|
20
|
+
// line — quoted or not — refuses the rewrite. Over-refusing is the point: a
|
|
21
|
+
// pattern containing a literal `|` is rare, mis-rewriting a pipeline is fatal.
|
|
22
|
+
const SHELL_METACHARS = /[|&;<>`\n\r$(){}]/;
|
|
23
|
+
|
|
24
|
+
// `grep` output is line-oriented text; `codeindex grep` returns JSON hits. That
|
|
25
|
+
// is the intended trade (bounded + structured), but it means we only rewrite
|
|
26
|
+
// invocations whose INTENT is "search the tree", never "search this one file"
|
|
27
|
+
// (cheap already) and never anything whose flags shape the output format.
|
|
28
|
+
const GREP_BINARIES = new Set(["grep", "egrep", "rg", "ripgrep"]);
|
|
29
|
+
|
|
30
|
+
interface Parsed {
|
|
31
|
+
pattern?: string;
|
|
32
|
+
path?: string;
|
|
33
|
+
ignoreCase: boolean;
|
|
34
|
+
includes: string[];
|
|
35
|
+
recursive: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Split on whitespace honouring single/double quotes. Returns undefined on an
|
|
39
|
+
// unterminated quote — an unparseable line is a refusal, not a guess.
|
|
40
|
+
export function tokenize(line: string): string[] | undefined {
|
|
41
|
+
const out: string[] = [];
|
|
42
|
+
let cur = "";
|
|
43
|
+
let quote: '"' | "'" | undefined;
|
|
44
|
+
let started = false;
|
|
45
|
+
for (let i = 0; i < line.length; i++) {
|
|
46
|
+
const c = line[i];
|
|
47
|
+
if (quote) {
|
|
48
|
+
if (c === quote) quote = undefined;
|
|
49
|
+
else cur += c;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (c === '"' || c === "'") {
|
|
53
|
+
quote = c;
|
|
54
|
+
started = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (c === " " || c === "\t") {
|
|
58
|
+
if (started || cur) out.push(cur);
|
|
59
|
+
cur = "";
|
|
60
|
+
started = false;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
cur += c;
|
|
64
|
+
}
|
|
65
|
+
if (quote) return undefined; // unterminated quote
|
|
66
|
+
if (started || cur) out.push(cur);
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Wrap a token so the shell reproduces it verbatim. Single-quoting is total
|
|
71
|
+
// (no escapes are interpreted inside), so the only case needing care is a
|
|
72
|
+
// literal single quote, spliced via the standard '\'' idiom.
|
|
73
|
+
export function shellQuote(s: string): string {
|
|
74
|
+
if (s !== "" && !/[^A-Za-z0-9_\-./=@:]/.test(s)) return s;
|
|
75
|
+
return "'" + s.replace(/'/g, `'\\''`) + "'";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Parse a grep/rg invocation into the fields we can faithfully re-express.
|
|
79
|
+
// Returns undefined the moment anything is unrecognized.
|
|
80
|
+
function parseSearch(bin: string, args: string[]): Parsed | undefined {
|
|
81
|
+
const p: Parsed = { ignoreCase: false, includes: [], recursive: bin !== "grep" && bin !== "egrep" };
|
|
82
|
+
const positionals: string[] = [];
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < args.length; i++) {
|
|
85
|
+
const a = args[i];
|
|
86
|
+
if (a === undefined) continue;
|
|
87
|
+
if (a === "--") {
|
|
88
|
+
// Everything after `--` is positional by definition.
|
|
89
|
+
positionals.push(...args.slice(i + 1));
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (!a.startsWith("-") || a === "-") {
|
|
93
|
+
positionals.push(a);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (a === "-i" || a === "--ignore-case") {
|
|
97
|
+
p.ignoreCase = true;
|
|
98
|
+
} else if (a === "-r" || a === "-R" || a === "--recursive") {
|
|
99
|
+
p.recursive = true;
|
|
100
|
+
} else if (a === "-n" || a === "--line-number" || a === "-H" || a === "--with-filename" || a === "--no-heading") {
|
|
101
|
+
// Pure output-shaping flags whose effect codeindex already provides
|
|
102
|
+
// unconditionally (every hit carries file + line). Safe to drop.
|
|
103
|
+
} else if (a === "-e" || a === "--regexp") {
|
|
104
|
+
const v = args[++i];
|
|
105
|
+
if (v === undefined || p.pattern !== undefined) return undefined;
|
|
106
|
+
p.pattern = v;
|
|
107
|
+
} else if (a.startsWith("--include=")) {
|
|
108
|
+
p.includes.push(a.slice("--include=".length));
|
|
109
|
+
} else if (a === "--include" || a === "-g" || a === "--glob") {
|
|
110
|
+
const v = args[++i];
|
|
111
|
+
if (v === undefined) return undefined;
|
|
112
|
+
p.includes.push(v);
|
|
113
|
+
} else if (a.length > 2 && /^-[a-zA-Z]+$/.test(a)) {
|
|
114
|
+
// A bundled short-flag cluster (-rn, -ri, -rni…). Expand and re-check;
|
|
115
|
+
// any member outside the allowlist refuses the whole line.
|
|
116
|
+
const expanded = a.slice(1).split("").map((c) => `-${c}`);
|
|
117
|
+
args.splice(i, 1, ...expanded);
|
|
118
|
+
i--;
|
|
119
|
+
} else {
|
|
120
|
+
return undefined; // unknown flag → refuse
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// With an -e pattern already bound, every positional is a path; otherwise the
|
|
125
|
+
// first positional is the pattern. Either way at most ONE path may remain —
|
|
126
|
+
// multi-path search has no single-scope equivalent, so it refuses.
|
|
127
|
+
if (p.pattern === undefined) {
|
|
128
|
+
const first = positionals.shift();
|
|
129
|
+
if (first === undefined || first === "") return undefined;
|
|
130
|
+
p.pattern = first;
|
|
131
|
+
}
|
|
132
|
+
if (positionals.length > 1) return undefined;
|
|
133
|
+
p.path = positionals[0];
|
|
134
|
+
return p;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Rewrite `cmd` to its codeindex equivalent, or return undefined to leave it
|
|
138
|
+
// alone. `bin` is the codeindex executable name to emit (the host may have it
|
|
139
|
+
// on PATH under a mount point of its choosing).
|
|
140
|
+
export function rewriteCommand(cmd: string, bin = "codeindex"): string | undefined {
|
|
141
|
+
const line = cmd.trim();
|
|
142
|
+
if (!line || SHELL_METACHARS.test(line)) return undefined;
|
|
143
|
+
|
|
144
|
+
const tokens = tokenize(line);
|
|
145
|
+
if (!tokens || tokens.length < 2) return undefined;
|
|
146
|
+
|
|
147
|
+
// Refuse env-prefixed or path-qualified invocations (`FOO=1 grep …`,
|
|
148
|
+
// `/usr/bin/grep …`): resolving those faithfully is not worth the risk.
|
|
149
|
+
const [head, ...args] = tokens;
|
|
150
|
+
if (head === undefined || !GREP_BINARIES.has(head)) return undefined;
|
|
151
|
+
|
|
152
|
+
const p = parseSearch(head, args);
|
|
153
|
+
if (!p || p.pattern === undefined) return undefined;
|
|
154
|
+
const pattern = p.pattern;
|
|
155
|
+
// A non-recursive `grep pattern file.ts` is already cheap and its semantics
|
|
156
|
+
// (one file, text output) are not what the indexed search provides.
|
|
157
|
+
if (!p.recursive) return undefined;
|
|
158
|
+
|
|
159
|
+
// A path that is not the tree root means "search this subtree"; express it as
|
|
160
|
+
// a scope rather than silently widening to the whole repo.
|
|
161
|
+
const path = p.path;
|
|
162
|
+
const out = [bin, "grep", shellQuote(pattern)];
|
|
163
|
+
if (path && path !== "." && path !== "./") {
|
|
164
|
+
out.push("--scope", shellQuote(path.replace(/\/+$/, "")));
|
|
165
|
+
}
|
|
166
|
+
if (p.ignoreCase) out.push("--ignore-case");
|
|
167
|
+
for (const g of p.includes) out.push("--include", shellQuote(g));
|
|
168
|
+
return out.join(" ");
|
|
169
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Single source of truth for the engine version the bundle reports. Kept in
|
|
2
2
|
// lockstep with package.json by the release pipeline. Do not edit by hand
|
|
3
3
|
// outside a release.
|
|
4
|
-
export const ENGINE_VERSION = "2.
|
|
4
|
+
export const ENGINE_VERSION = "2.17.1";
|
|
5
5
|
|
|
6
6
|
// Bumped whenever the on-disk artifact shape changes, so a consumer can reject
|
|
7
7
|
// an index written by an incompatible engine instead of misreading it. The
|