@openez-graph/cli 1.2.0 → 1.3.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 +12 -10
- package/dist/CHANGELOG.md +24 -0
- package/dist/cli.cjs +397 -68
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -9,12 +9,12 @@ OpenEZ Graph indexes your codebase into a local SQLite database, builds a code g
|
|
|
9
9
|
|
|
10
10
|
**Zero config. No Docker. No Postgres. No Redis. Just install and go.**
|
|
11
11
|
|
|
12
|
-
> **v1.0: Bun-powered, Rust-native parsing.** The CLI now runs exclusively on [Bun](https://bun.sh) 1.1+ with native `bun:sqlite` (no `better-sqlite3` compilation step). TS/JS parsing uses [oxc-parser](https://oxc.rs) (Rust-based, ~13x faster than Babel) instead of `ts-morph`. Python/Go/Rust use tree-sitter
|
|
12
|
+
> **v1.0: Bun-powered, Rust-native parsing.** The CLI now runs exclusively on [Bun](https://bun.sh) 1.1+ with native `bun:sqlite` (no `better-sqlite3` compilation step). TS/JS parsing uses [oxc-parser](https://oxc.rs) (Rust-based, ~13x faster than Babel) instead of `ts-morph`. Python/Go/Rust/Ruby use tree-sitter (WASM) with regex fallback. Requires Bun 1.1+.
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
16
|
- **Bun-powered** — native `bun:sqlite` driver, no native compilation, near-instant startup
|
|
17
|
-
- **Rust-native parsing** — [oxc-parser](https://oxc.rs) for TS/JS (~13x faster than Babel), tree-sitter for Python/Go/Rust
|
|
17
|
+
- **Rust-native parsing** — [oxc-parser](https://oxc.rs) for TS/JS (~13x faster than Babel), tree-sitter (WASM) for Python/Go/Rust/Ruby with regex fallback
|
|
18
18
|
- **Zero-config** — auto-registers workspace, auto-indexes; opt-in auto-sync via `OPENEZ_MCP_WATCH=1`
|
|
19
19
|
- **SQLite-first** — all data stored locally in `.openez/` per workspace, no Postgres/Redis
|
|
20
20
|
- **FTS5 full-text search** — SQLite FTS5 with BM25 ranking and porter tokenizer
|
|
@@ -106,14 +106,16 @@ Valid config keys: `embedding.provider`, `embedding.openai_api_key`, `embedding.
|
|
|
106
106
|
|
|
107
107
|
## Supported languages
|
|
108
108
|
|
|
109
|
-
| Language
|
|
110
|
-
|
|
|
111
|
-
| TypeScript / JavaScript
|
|
112
|
-
| Python
|
|
113
|
-
| Go
|
|
114
|
-
| Rust
|
|
115
|
-
|
|
|
116
|
-
|
|
|
109
|
+
| Language | Parser | Indexing depth |
|
|
110
|
+
| ----------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------- |
|
|
111
|
+
| TypeScript / JavaScript | [oxc-parser](https://oxc.rs) | Richest — symbol extraction, imports, calls (~13x faster than Babel) |
|
|
112
|
+
| Python | tree-sitter (WASM) | Symbol extraction, decorators, imports, calls |
|
|
113
|
+
| Go | tree-sitter (WASM) | Symbol extraction, receiver-qualified methods, calls |
|
|
114
|
+
| Rust | tree-sitter (WASM) | Symbol extraction, impl blocks, calls |
|
|
115
|
+
| Ruby | tree-sitter (WASM) | Symbol extraction, `class << self` context, `self.foo` calls, `require_relative` imports |
|
|
116
|
+
| CoffeeScript / Slim / CSS / SCSS / SASS / LESS / Haml | Fallback parser | Scanned and chunked (no symbol extraction) |
|
|
117
|
+
| YAML / JSON / TOML | Structure-aware | Structure-aware chunking |
|
|
118
|
+
| Markdown | Section-oriented | Section-oriented chunking |
|
|
117
119
|
|
|
118
120
|
## Retrieval quality
|
|
119
121
|
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -5,12 +5,34 @@ All notable changes to OpenEZ Graph are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.1] - 2026-08-16
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Reindex and DB open performance on large workspaces** — `openez status` and `openez reindex` on a 92K-chunk workspace (e.g. Zed) went from >8 minutes (100% CPU, never completing) to ~0.15s (status) and ~25s (reindex). Two root causes fixed:
|
|
13
|
+
- **FTS repair scan on every DB open** — `initializeWorkspaceSchema` ran an O(n²) `LEFT JOIN chunks_fts` (chunk_id is UNINDEXED in FTS5) plus an orphan-cleanup `DELETE ... NOT IN` on every open, even when FTS was fully in sync. Now skips the expensive repair when a count + shape check confirms FTS is in sync, and only runs the full repair when counts diverge or orphaned rows are detected.
|
|
14
|
+
- **FTS trigger storm during full reindex** — `resetIndexArtifacts` deleted 92K chunks with FTS triggers active, firing 92K triggered `DELETE FROM chunks_fts WHERE chunk_id = old.id` (each a full FTS shadow-table scan). Now drops FTS triggers and the FTS table before deleting chunks, then recreates an empty FTS table for the reindex to populate via `bulkInsertFts`.
|
|
15
|
+
|
|
16
|
+
## [1.3.0] - 2026-08-15
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Ruby indexing** — tree-sitter WASM symbol extraction for classes, modules, methods, singleton methods, named lambdas, `class << self` nesting, and `require_relative` import edges.
|
|
21
|
+
- **Ruby and frontend language scanning** — `.rb`, `.rake`, `.gemspec`, CoffeeScript, Slim, CSS, SCSS, Sass, Less, and Haml files are now indexed; unsupported languages use fallback chunking.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **Ruby graph extraction** — preserves calls inside ordinary assignments, qualifies `self` calls in class bodies, keeps regex fallback nesting correct, and aligns method export metadata with the AST parser.
|
|
26
|
+
|
|
8
27
|
## [1.2.0] - 2026-08-11
|
|
9
28
|
|
|
10
29
|
### Added
|
|
11
30
|
|
|
12
31
|
- **Lease-based indexing ownership** — indexing claims a 60-second lease with 15-second heartbeat. If the lease expires (e.g. process crash), another process can take over. Completion and failure writes are fenced by owner token — a stale owner cannot overwrite the status set by a newer owner.
|
|
13
32
|
- **Graph build warnings in MCP** — `code_query` response includes a `warnings` array when graph expansion fails, so callers see FTS-only results instead of silently missing graph expansion.
|
|
33
|
+
- **Web UI: Reindex button** — workspace detail page now has a "Reindex workspace" button with loading state and error display, triggering a full rebuild via `startIndexRun(workspaceId, "full")`.
|
|
34
|
+
- **Web UI: Build & Open Graph** — workspace detail page shows "Build & Open Graph" button when graph data is empty, or "Open Graph" when graph exists. Graph is built lazily on first access via `ensureGraphReady`.
|
|
35
|
+
- **Web UI: Local embedding model picker** — settings page now supports selecting local embedding model preset (`jina-code-static-256`) with server-side validation against `LOCAL_EMBEDDING_MODELS` catalog. Provider dropdown includes "OpenEZ local" option alongside OpenAI and Ollama.
|
|
14
36
|
|
|
15
37
|
### Fixed
|
|
16
38
|
|
|
@@ -285,7 +307,9 @@ Remediation release — index/graph correctness, data protection, and web flow f
|
|
|
285
307
|
- Error handling and validation for import path extraction
|
|
286
308
|
- CLI npm packaging
|
|
287
309
|
|
|
310
|
+
[1.3.1]: https://github.com/asta-nguyen/openez-graph/compare/v1.3.0...v1.3.1
|
|
288
311
|
[1.2.0]: https://github.com/asta-nguyen/openez-graph/compare/v1.1.0...v1.2.0
|
|
312
|
+
[1.3.0]: https://github.com/asta-nguyen/openez-graph/compare/v1.2.0...v1.3.0
|
|
289
313
|
[1.1.0]: https://github.com/asta-nguyen/openez-graph/compare/v1.0.3...v1.1.0
|
|
290
314
|
[1.0.3]: https://github.com/asta-nguyen/openez-graph/compare/v1.0.2...v1.0.3
|
|
291
315
|
[1.0.2]: https://github.com/asta-nguyen/openez-graph/compare/v1.0.1...v1.0.2
|
package/dist/cli.cjs
CHANGED
|
@@ -11910,6 +11910,13 @@ function createFtsOps(native, stmts, deps) {
|
|
|
11910
11910
|
},
|
|
11911
11911
|
// ── Reset ──
|
|
11912
11912
|
resetIndexArtifacts() {
|
|
11913
|
+
native.exec("DROP TRIGGER IF EXISTS chunks_fts_insert");
|
|
11914
|
+
native.exec("DROP TRIGGER IF EXISTS chunks_fts_delete");
|
|
11915
|
+
native.exec("DROP TRIGGER IF EXISTS chunks_fts_update");
|
|
11916
|
+
native.exec("DROP TABLE IF EXISTS chunks_fts");
|
|
11917
|
+
native.exec(
|
|
11918
|
+
"CREATE VIRTUAL TABLE chunks_fts USING fts5(chunk_id UNINDEXED, path, heading, language, search_text, tokenize = 'unicode61')"
|
|
11919
|
+
);
|
|
11913
11920
|
native.exec("DELETE FROM graph_edges");
|
|
11914
11921
|
native.exec("DELETE FROM graph_nodes");
|
|
11915
11922
|
native.exec("DELETE FROM chunks");
|
|
@@ -12114,20 +12121,36 @@ function initializeWorkspaceSchema(sqlite) {
|
|
|
12114
12121
|
INNER JOIN documents d ON d.id = c.document_id;
|
|
12115
12122
|
`);
|
|
12116
12123
|
} else {
|
|
12117
|
-
sqlite.
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12124
|
+
const chunkCount = sqlite.prepare("SELECT count(*) as c FROM chunks").get().c;
|
|
12125
|
+
const ftsCount = sqlite.prepare("SELECT count(*) as c FROM chunks_fts").get().c;
|
|
12126
|
+
let ftsInSync = chunkCount === ftsCount;
|
|
12127
|
+
if (ftsInSync) {
|
|
12128
|
+
const ftsShape = sqlite.prepare(
|
|
12129
|
+
`
|
|
12130
|
+
SELECT count(DISTINCT f.chunk_id) AS distinct_count,
|
|
12131
|
+
count(*) - count(c.id) AS orphan_count
|
|
12132
|
+
FROM chunks_fts f
|
|
12133
|
+
LEFT JOIN chunks c ON c.id = f.chunk_id
|
|
12134
|
+
`
|
|
12135
|
+
).get();
|
|
12136
|
+
ftsInSync = ftsShape.distinct_count === chunkCount && ftsShape.orphan_count === 0;
|
|
12137
|
+
}
|
|
12138
|
+
if (!ftsInSync) {
|
|
12139
|
+
sqlite.exec(`
|
|
12140
|
+
INSERT INTO chunks_fts (chunk_id, path, heading, language, search_text)
|
|
12141
|
+
SELECT c.id, d.path, coalesce(c.heading, ''),
|
|
12142
|
+
coalesce(d.language, ''),
|
|
12143
|
+
${composeFtsSearchTextSql("c.metadata", "c.content")}
|
|
12144
|
+
FROM chunks c
|
|
12145
|
+
INNER JOIN documents d ON d.id = c.document_id
|
|
12146
|
+
LEFT JOIN chunks_fts f ON f.chunk_id = c.id
|
|
12147
|
+
WHERE f.chunk_id IS NULL;
|
|
12148
|
+
`);
|
|
12149
|
+
sqlite.exec(`
|
|
12150
|
+
DELETE FROM chunks_fts
|
|
12151
|
+
WHERE chunk_id NOT IN (SELECT id FROM chunks);
|
|
12152
|
+
`);
|
|
12153
|
+
}
|
|
12131
12154
|
}
|
|
12132
12155
|
const hasTypeLabelIdx = sqlite.prepare(
|
|
12133
12156
|
"SELECT count(*) as c FROM sqlite_master WHERE type='index' AND name='idx_graph_nodes_type_label'"
|
|
@@ -33034,6 +33057,15 @@ function inferDocumentKind(filePath) {
|
|
|
33034
33057
|
if (codeExtensions.has(extension)) {
|
|
33035
33058
|
return { kind: "code", language: codeExtensions.get(extension) ?? null, extension };
|
|
33036
33059
|
}
|
|
33060
|
+
if (styleExtensions.has(extension)) {
|
|
33061
|
+
return { kind: "code", language: styleExtensions.get(extension) ?? null, extension };
|
|
33062
|
+
}
|
|
33063
|
+
if (templateExtensions.has(extension)) {
|
|
33064
|
+
return { kind: "code", language: templateExtensions.get(extension) ?? null, extension };
|
|
33065
|
+
}
|
|
33066
|
+
if (scriptExtensions.has(extension)) {
|
|
33067
|
+
return { kind: "code", language: scriptExtensions.get(extension) ?? null, extension };
|
|
33068
|
+
}
|
|
33037
33069
|
if (configExtensions.has(extension)) {
|
|
33038
33070
|
return { kind: "config", language: configExtensions.get(extension) ?? null, extension };
|
|
33039
33071
|
}
|
|
@@ -33537,6 +33569,83 @@ function parseRust(content, counter = exactTokenCounter) {
|
|
|
33537
33569
|
callExpressions
|
|
33538
33570
|
};
|
|
33539
33571
|
}
|
|
33572
|
+
function findRubyBlockEnd(codeLines, startIndex) {
|
|
33573
|
+
const openingLine = codeLines[startIndex] ?? "";
|
|
33574
|
+
if (openingLine.includes(";") && /\bend\s*(?:#.*)?$/.test(openingLine)) {
|
|
33575
|
+
return startIndex + 1;
|
|
33576
|
+
}
|
|
33577
|
+
const baseIndent = codeLines[startIndex]?.search(/\S/) ?? 0;
|
|
33578
|
+
for (let i = startIndex + 1; i < codeLines.length; i++) {
|
|
33579
|
+
const trimmed = codeLines[i].trim();
|
|
33580
|
+
if (trimmed === "") continue;
|
|
33581
|
+
const indent = codeLines[i].search(/\S/);
|
|
33582
|
+
if (indent < 0) continue;
|
|
33583
|
+
if (indent < baseIndent) {
|
|
33584
|
+
return i;
|
|
33585
|
+
}
|
|
33586
|
+
if (indent === baseIndent && (trimmed === "end" || trimmed.startsWith("end ") || trimmed.startsWith("end "))) {
|
|
33587
|
+
return i + 1;
|
|
33588
|
+
}
|
|
33589
|
+
}
|
|
33590
|
+
return codeLines.length;
|
|
33591
|
+
}
|
|
33592
|
+
function parseRuby(content, counter = exactTokenCounter) {
|
|
33593
|
+
const lines = content.split("\n");
|
|
33594
|
+
const codeLines = stripNonCode(content, { hashComments: true }).split("\n");
|
|
33595
|
+
const definedSymbols = [];
|
|
33596
|
+
const importPaths = [];
|
|
33597
|
+
const calledIdentifiers = /* @__PURE__ */ new Set();
|
|
33598
|
+
const callExpressions = [];
|
|
33599
|
+
const symbolRegex = /^\s*(?:def\s+(?:self\.)?(\w+[?!]?)|class\s+(\w+(?:::\w+)*)|module\s+(\w+(?:::\w+)*))/;
|
|
33600
|
+
const requireRelativeRegex = /\brequire_relative\s+['"]([^'"]+)['"]/g;
|
|
33601
|
+
const contextStack = [];
|
|
33602
|
+
for (let i = 0; i < codeLines.length; i++) {
|
|
33603
|
+
const line = codeLines[i];
|
|
33604
|
+
const trimmed = line.trim();
|
|
33605
|
+
while (contextStack.length > 0 && i >= contextStack[contextStack.length - 1].endLine) {
|
|
33606
|
+
contextStack.pop();
|
|
33607
|
+
}
|
|
33608
|
+
const match2 = symbolRegex.exec(trimmed);
|
|
33609
|
+
if (match2) {
|
|
33610
|
+
const rawName = match2[1] ?? match2[2] ?? match2[3];
|
|
33611
|
+
if (!rawName) continue;
|
|
33612
|
+
const isMethod = Boolean(match2[1]);
|
|
33613
|
+
const symbolType2 = isMethod ? "function" : match2[2] ? "class" : "module";
|
|
33614
|
+
const parentName = contextStack.length > 0 ? contextStack[contextStack.length - 1].name : null;
|
|
33615
|
+
const name = parentName ? `${parentName}::${rawName}` : rawName;
|
|
33616
|
+
const endLine = findRubyBlockEnd(codeLines, i);
|
|
33617
|
+
definedSymbols.push({
|
|
33618
|
+
name,
|
|
33619
|
+
symbolType: symbolType2,
|
|
33620
|
+
type: symbolType2,
|
|
33621
|
+
exported: isMethod ? false : !rawName.startsWith("_"),
|
|
33622
|
+
startLine: i + 1,
|
|
33623
|
+
endLine
|
|
33624
|
+
});
|
|
33625
|
+
contextStack.push({ name, endLine });
|
|
33626
|
+
continue;
|
|
33627
|
+
}
|
|
33628
|
+
requireRelativeRegex.lastIndex = 0;
|
|
33629
|
+
let reqMatch;
|
|
33630
|
+
while ((reqMatch = requireRelativeRegex.exec(lines[i] ?? "")) !== null) {
|
|
33631
|
+
importPaths.push(reqMatch[1]);
|
|
33632
|
+
}
|
|
33633
|
+
}
|
|
33634
|
+
const chunks2 = createSymbolChunks(definedSymbols, lines, "ruby", counter);
|
|
33635
|
+
if (chunks2.length === 0) {
|
|
33636
|
+
return {
|
|
33637
|
+
...makeFallbackChunks(content, lines, counter),
|
|
33638
|
+
importPaths: [...new Set(importPaths)]
|
|
33639
|
+
};
|
|
33640
|
+
}
|
|
33641
|
+
return {
|
|
33642
|
+
chunks: chunks2,
|
|
33643
|
+
importPaths: [...new Set(importPaths)],
|
|
33644
|
+
definedSymbols,
|
|
33645
|
+
calledIdentifiers: [...calledIdentifiers],
|
|
33646
|
+
callExpressions
|
|
33647
|
+
};
|
|
33648
|
+
}
|
|
33540
33649
|
function indexConfig(content, language, counter = exactTokenCounter) {
|
|
33541
33650
|
switch (language) {
|
|
33542
33651
|
case "yaml":
|
|
@@ -33808,7 +33917,7 @@ function makeFallbackChunks(content, lines, counter = exactTokenCounter) {
|
|
|
33808
33917
|
callExpressions: []
|
|
33809
33918
|
};
|
|
33810
33919
|
}
|
|
33811
|
-
var import_node_path11, codeExtensions, configExtensions, markdownExtensions, PYTHON_CALL_IGNORES, GO_CALL_IGNORES, RUST_CALL_IGNORES;
|
|
33920
|
+
var import_node_path11, codeExtensions, configExtensions, styleExtensions, templateExtensions, scriptExtensions, markdownExtensions, PYTHON_CALL_IGNORES, GO_CALL_IGNORES, RUST_CALL_IGNORES;
|
|
33812
33921
|
var init_languages = __esm({
|
|
33813
33922
|
"../../packages/indexer/src/languages.ts"() {
|
|
33814
33923
|
"use strict";
|
|
@@ -33826,7 +33935,10 @@ var init_languages = __esm({
|
|
|
33826
33935
|
[".cts", "typescript"],
|
|
33827
33936
|
[".py", "python"],
|
|
33828
33937
|
[".go", "go"],
|
|
33829
|
-
[".rs", "rust"]
|
|
33938
|
+
[".rs", "rust"],
|
|
33939
|
+
[".rb", "ruby"],
|
|
33940
|
+
[".rake", "ruby"],
|
|
33941
|
+
[".gemspec", "ruby"]
|
|
33830
33942
|
]);
|
|
33831
33943
|
configExtensions = /* @__PURE__ */ new Map([
|
|
33832
33944
|
[".yaml", "yaml"],
|
|
@@ -33834,6 +33946,20 @@ var init_languages = __esm({
|
|
|
33834
33946
|
[".json", "json"],
|
|
33835
33947
|
[".toml", "toml"]
|
|
33836
33948
|
]);
|
|
33949
|
+
styleExtensions = /* @__PURE__ */ new Map([
|
|
33950
|
+
[".css", "css"],
|
|
33951
|
+
[".scss", "scss"],
|
|
33952
|
+
[".sass", "scss"],
|
|
33953
|
+
[".less", "less"]
|
|
33954
|
+
]);
|
|
33955
|
+
templateExtensions = /* @__PURE__ */ new Map([
|
|
33956
|
+
[".slim", "slim"],
|
|
33957
|
+
[".haml", "haml"]
|
|
33958
|
+
]);
|
|
33959
|
+
scriptExtensions = /* @__PURE__ */ new Map([
|
|
33960
|
+
[".coffee", "coffeescript"],
|
|
33961
|
+
[".litcoffee", "coffeescript"]
|
|
33962
|
+
]);
|
|
33837
33963
|
markdownExtensions = /* @__PURE__ */ new Set([".md", ".mdx"]);
|
|
33838
33964
|
PYTHON_CALL_IGNORES = /* @__PURE__ */ new Set([
|
|
33839
33965
|
"if",
|
|
@@ -34863,44 +34989,62 @@ function walkTree(root, config2, lines) {
|
|
|
34863
34989
|
}
|
|
34864
34990
|
const symbolRule = symbolRuleMap.get(node.type);
|
|
34865
34991
|
if (symbolRule) {
|
|
34866
|
-
|
|
34867
|
-
|
|
34868
|
-
|
|
34869
|
-
|
|
34870
|
-
|
|
34871
|
-
|
|
34872
|
-
|
|
34873
|
-
|
|
34874
|
-
|
|
34875
|
-
|
|
34876
|
-
|
|
34877
|
-
|
|
34878
|
-
|
|
34879
|
-
|
|
34880
|
-
|
|
34881
|
-
|
|
34882
|
-
|
|
34883
|
-
|
|
34884
|
-
|
|
34885
|
-
|
|
34886
|
-
|
|
34887
|
-
|
|
34888
|
-
|
|
34889
|
-
|
|
34890
|
-
|
|
34891
|
-
|
|
34892
|
-
|
|
34893
|
-
|
|
34894
|
-
|
|
34895
|
-
|
|
34896
|
-
|
|
34897
|
-
|
|
34898
|
-
|
|
34899
|
-
|
|
34900
|
-
|
|
34901
|
-
|
|
34902
|
-
const
|
|
34903
|
-
|
|
34992
|
+
if (symbolRule.contextOnly) {
|
|
34993
|
+
const contextName = symbolRule.extractContextName ? symbolRule.extractContextName(node, contextStack) ?? null : null;
|
|
34994
|
+
if (contextName) {
|
|
34995
|
+
contextStack.push({
|
|
34996
|
+
name: contextName,
|
|
34997
|
+
endRow,
|
|
34998
|
+
kind: symbolRule.contextKind
|
|
34999
|
+
});
|
|
35000
|
+
}
|
|
35001
|
+
} else {
|
|
35002
|
+
const name = symbolRule.extractName ? symbolRule.extractName(node) : getNodeName(node, symbolRule.nameField ?? "name");
|
|
35003
|
+
if (name) {
|
|
35004
|
+
const parentName = contextStack.length > 0 ? contextStack[contextStack.length - 1].name : null;
|
|
35005
|
+
const exported = symbolRule.isExported ? symbolRule.isExported(name, node) : false;
|
|
35006
|
+
let receiver;
|
|
35007
|
+
let receiverVar = null;
|
|
35008
|
+
let receiverType = null;
|
|
35009
|
+
if (symbolRule.receiverField) {
|
|
35010
|
+
const rawReceiver = node.childForFieldName(symbolRule.receiverField)?.text;
|
|
35011
|
+
if (rawReceiver) {
|
|
35012
|
+
receiver = symbolRule.normalizeReceiver ? symbolRule.normalizeReceiver(rawReceiver) : rawReceiver;
|
|
35013
|
+
receiverVar = goReceiverName(rawReceiver);
|
|
35014
|
+
receiverType = goReceiverType(rawReceiver);
|
|
35015
|
+
}
|
|
35016
|
+
}
|
|
35017
|
+
const fullName = receiverType ? `${receiverType}::${name}` : parentName ? `${parentName}::${name}` : name;
|
|
35018
|
+
symbols.push({
|
|
35019
|
+
name: fullName,
|
|
35020
|
+
symbolType: symbolRule.symbolType,
|
|
35021
|
+
type: symbolRule.symbolType,
|
|
35022
|
+
exported,
|
|
35023
|
+
startLine: startRow,
|
|
35024
|
+
endLine: endRow,
|
|
35025
|
+
...receiver ? { receiver } : {}
|
|
35026
|
+
});
|
|
35027
|
+
const receiverInfo = receiverVar && receiverType ? { varName: receiverVar, typeName: receiverType } : void 0;
|
|
35028
|
+
const isContextNode = symbolRule.establishesContext || config2.contextNodeTypes.has(node.type);
|
|
35029
|
+
const contextName = isContextNode ? symbolRule.extractContextName?.(node, contextStack) ?? fullName : null;
|
|
35030
|
+
const callContext = contextName && symbolRule.contextKind ? [...contextStack, { name: contextName, endRow, kind: symbolRule.contextKind }] : contextStack;
|
|
35031
|
+
extractCallsInNode(
|
|
35032
|
+
node,
|
|
35033
|
+
config2,
|
|
35034
|
+
fullName,
|
|
35035
|
+
receiverInfo,
|
|
35036
|
+
calledIdentifiers,
|
|
35037
|
+
callExpressions,
|
|
35038
|
+
callContext
|
|
35039
|
+
);
|
|
35040
|
+
if (isContextNode) {
|
|
35041
|
+
contextStack.push({
|
|
35042
|
+
name: contextName ?? fullName,
|
|
35043
|
+
endRow,
|
|
35044
|
+
kind: symbolRule.contextKind,
|
|
35045
|
+
receiver: receiverInfo
|
|
35046
|
+
});
|
|
35047
|
+
}
|
|
34904
35048
|
}
|
|
34905
35049
|
}
|
|
34906
35050
|
} else if (isCallNode(node, config2.callRule)) {
|
|
@@ -34919,9 +35063,19 @@ function walkTree(root, config2, lines) {
|
|
|
34919
35063
|
}
|
|
34920
35064
|
return { symbols, importPaths, calledIdentifiers, callExpressions };
|
|
34921
35065
|
}
|
|
34922
|
-
function extractCallsInNode(symbolNode, config2, callerName, receiverInfo, calledIdentifiers, callExpressions) {
|
|
34923
|
-
const
|
|
34924
|
-
const nestedSymbols = symbolNode.descendantsOfType(
|
|
35066
|
+
function extractCallsInNode(symbolNode, config2, callerName, receiverInfo, calledIdentifiers, callExpressions, contextStack) {
|
|
35067
|
+
const symbolRuleMap = new Map(config2.symbolRules.map((rule) => [rule.nodeType, rule]));
|
|
35068
|
+
const nestedSymbols = symbolNode.descendantsOfType(config2.symbolRules.map((rule) => rule.nodeType)).filter((node) => {
|
|
35069
|
+
if (node.startIndex === symbolNode.startIndex && node.endIndex === symbolNode.endIndex) {
|
|
35070
|
+
return false;
|
|
35071
|
+
}
|
|
35072
|
+
const rule = symbolRuleMap.get(node.type);
|
|
35073
|
+
if (!rule) return false;
|
|
35074
|
+
if (rule.contextOnly) return true;
|
|
35075
|
+
return Boolean(
|
|
35076
|
+
rule.extractName ? rule.extractName(node) : getNodeName(node, rule.nameField ?? "name")
|
|
35077
|
+
);
|
|
35078
|
+
});
|
|
34925
35079
|
const callNodes = symbolNode.descendantsOfType(config2.callRule.nodeType);
|
|
34926
35080
|
for (const callNode of callNodes) {
|
|
34927
35081
|
const insideNested = nestedSymbols.some(
|
|
@@ -34932,11 +35086,13 @@ function extractCallsInNode(symbolNode, config2, callerName, receiverInfo, calle
|
|
|
34932
35086
|
if (!calleeName2) continue;
|
|
34933
35087
|
const normalized = config2.normalizeCallName(calleeName2);
|
|
34934
35088
|
const qualifiedCallee = receiverInfo && calleeName2.startsWith(`${receiverInfo.varName}.`) ? `${receiverInfo.typeName}::${normalized}` : normalized;
|
|
34935
|
-
|
|
35089
|
+
const hookQualified = config2.qualifyCall?.(callNode, calleeName2, normalized, contextStack);
|
|
35090
|
+
const finalCallee = hookQualified ?? qualifiedCallee;
|
|
35091
|
+
if (!normalized || config2.callIgnores.has(calleeName2) || config2.callIgnores.has(normalized) || finalCallee === callerName) {
|
|
34936
35092
|
continue;
|
|
34937
35093
|
}
|
|
34938
|
-
calledIdentifiers.add(
|
|
34939
|
-
callExpressions.push({ callerName, calleeName:
|
|
35094
|
+
calledIdentifiers.add(finalCallee);
|
|
35095
|
+
callExpressions.push({ callerName, calleeName: finalCallee });
|
|
34940
35096
|
}
|
|
34941
35097
|
}
|
|
34942
35098
|
async function parseWithTreeSitter(config2, content, counter) {
|
|
@@ -35053,7 +35209,42 @@ function extractRustImports(node) {
|
|
|
35053
35209
|
}
|
|
35054
35210
|
return paths;
|
|
35055
35211
|
}
|
|
35056
|
-
|
|
35212
|
+
function normalizeRubyCallName(value) {
|
|
35213
|
+
const parts = value.split(/[.::]/).filter(Boolean);
|
|
35214
|
+
return parts[parts.length - 1] ?? value;
|
|
35215
|
+
}
|
|
35216
|
+
function extractRubyImports(node) {
|
|
35217
|
+
const paths = [];
|
|
35218
|
+
if (node.type === "call") {
|
|
35219
|
+
const methodNode = node.childForFieldName("method");
|
|
35220
|
+
if (methodNode && methodNode.text === "require_relative") {
|
|
35221
|
+
const argsNode = node.childForFieldName("arguments");
|
|
35222
|
+
if (argsNode) {
|
|
35223
|
+
const stringNode = argsNode.namedChildren.find(
|
|
35224
|
+
(c) => c.type === "string" || c.type === "argument_list"
|
|
35225
|
+
);
|
|
35226
|
+
const target = stringNode?.type === "string" ? stringNode : stringNode?.namedChildren.find((c) => c.type === "string");
|
|
35227
|
+
if (target) {
|
|
35228
|
+
const text2 = target.text.replace(/^['"]|['"]$/g, "");
|
|
35229
|
+
if (text2) paths.push(text2);
|
|
35230
|
+
}
|
|
35231
|
+
}
|
|
35232
|
+
}
|
|
35233
|
+
}
|
|
35234
|
+
return paths;
|
|
35235
|
+
}
|
|
35236
|
+
function rubyQualifyCall(callNode, _calleeName, normalized, contextStack) {
|
|
35237
|
+
const receiverNode = callNode.childForFieldName("receiver");
|
|
35238
|
+
if (!receiverNode || receiverNode.text !== "self") return null;
|
|
35239
|
+
for (let i = contextStack.length - 1; i >= 0; i--) {
|
|
35240
|
+
const frame = contextStack[i];
|
|
35241
|
+
if (frame.kind === "class" || frame.kind === "module") {
|
|
35242
|
+
return `${frame.name}::${normalized}`;
|
|
35243
|
+
}
|
|
35244
|
+
}
|
|
35245
|
+
return null;
|
|
35246
|
+
}
|
|
35247
|
+
var PYTHON_CALL_IGNORES2, pythonConfig, GO_CALL_IGNORES2, goConfig, RUST_CALL_IGNORES2, rustConfig, RUBY_CALL_IGNORES, rubyConfig;
|
|
35057
35248
|
var init_configs = __esm({
|
|
35058
35249
|
"../../packages/indexer/src/tree-sitter/configs.ts"() {
|
|
35059
35250
|
"use strict";
|
|
@@ -35280,7 +35471,7 @@ var init_configs = __esm({
|
|
|
35280
35471
|
}
|
|
35281
35472
|
return typeName ? `impl ${typeName}` : null;
|
|
35282
35473
|
},
|
|
35283
|
-
extractContextName: (node) => {
|
|
35474
|
+
extractContextName: (node, _contextStack) => {
|
|
35284
35475
|
return node.childForFieldName("type")?.text ?? null;
|
|
35285
35476
|
},
|
|
35286
35477
|
establishesContext: true,
|
|
@@ -35318,6 +35509,132 @@ var init_configs = __esm({
|
|
|
35318
35509
|
contextNodeTypes: /* @__PURE__ */ new Set(["function_item", "trait_item", "impl_item"]),
|
|
35319
35510
|
contextNameField: "name"
|
|
35320
35511
|
};
|
|
35512
|
+
RUBY_CALL_IGNORES = /* @__PURE__ */ new Set([
|
|
35513
|
+
"if",
|
|
35514
|
+
"unless",
|
|
35515
|
+
"while",
|
|
35516
|
+
"until",
|
|
35517
|
+
"for",
|
|
35518
|
+
"return",
|
|
35519
|
+
"yield",
|
|
35520
|
+
"break",
|
|
35521
|
+
"next",
|
|
35522
|
+
"redo",
|
|
35523
|
+
"retry",
|
|
35524
|
+
"puts",
|
|
35525
|
+
"pp",
|
|
35526
|
+
"p",
|
|
35527
|
+
"print",
|
|
35528
|
+
"raise",
|
|
35529
|
+
"require",
|
|
35530
|
+
"require_relative",
|
|
35531
|
+
"load",
|
|
35532
|
+
"autoload",
|
|
35533
|
+
"attr_accessor",
|
|
35534
|
+
"attr_reader",
|
|
35535
|
+
"attr_writer",
|
|
35536
|
+
"include",
|
|
35537
|
+
"extend",
|
|
35538
|
+
"define_method",
|
|
35539
|
+
"lambda",
|
|
35540
|
+
"proc",
|
|
35541
|
+
"super",
|
|
35542
|
+
"self",
|
|
35543
|
+
"nil",
|
|
35544
|
+
"true",
|
|
35545
|
+
"false",
|
|
35546
|
+
"new",
|
|
35547
|
+
"Array",
|
|
35548
|
+
"Hash",
|
|
35549
|
+
"String",
|
|
35550
|
+
"Integer",
|
|
35551
|
+
"Float",
|
|
35552
|
+
"Symbol"
|
|
35553
|
+
]);
|
|
35554
|
+
rubyConfig = {
|
|
35555
|
+
language: "ruby",
|
|
35556
|
+
symbolRules: [
|
|
35557
|
+
{
|
|
35558
|
+
nodeType: "class",
|
|
35559
|
+
symbolType: "class",
|
|
35560
|
+
nameField: "name",
|
|
35561
|
+
establishesContext: true,
|
|
35562
|
+
contextKind: "class",
|
|
35563
|
+
isExported: (name) => name[0] >= "A" && name[0] <= "Z"
|
|
35564
|
+
},
|
|
35565
|
+
{
|
|
35566
|
+
nodeType: "module",
|
|
35567
|
+
symbolType: "module",
|
|
35568
|
+
nameField: "name",
|
|
35569
|
+
establishesContext: true,
|
|
35570
|
+
contextKind: "module",
|
|
35571
|
+
isExported: (name) => name[0] >= "A" && name[0] <= "Z"
|
|
35572
|
+
},
|
|
35573
|
+
{
|
|
35574
|
+
nodeType: "method",
|
|
35575
|
+
symbolType: "function",
|
|
35576
|
+
nameField: "name",
|
|
35577
|
+
establishesContext: true,
|
|
35578
|
+
isExported: () => false
|
|
35579
|
+
},
|
|
35580
|
+
{
|
|
35581
|
+
nodeType: "singleton_method",
|
|
35582
|
+
symbolType: "function",
|
|
35583
|
+
nameField: "name",
|
|
35584
|
+
establishesContext: true,
|
|
35585
|
+
isExported: () => false
|
|
35586
|
+
},
|
|
35587
|
+
{
|
|
35588
|
+
nodeType: "singleton_class",
|
|
35589
|
+
symbolType: "class",
|
|
35590
|
+
contextOnly: true,
|
|
35591
|
+
extractContextName: (_node, contextStack) => {
|
|
35592
|
+
for (let i = contextStack.length - 1; i >= 0; i--) {
|
|
35593
|
+
const frame = contextStack[i];
|
|
35594
|
+
if (frame.kind === "class" || frame.kind === "module") {
|
|
35595
|
+
return frame.name;
|
|
35596
|
+
}
|
|
35597
|
+
}
|
|
35598
|
+
return null;
|
|
35599
|
+
},
|
|
35600
|
+
isExported: () => false
|
|
35601
|
+
},
|
|
35602
|
+
{
|
|
35603
|
+
nodeType: "assignment",
|
|
35604
|
+
symbolType: "lambda",
|
|
35605
|
+
extractName: (node) => {
|
|
35606
|
+
const rightNode = node.childForFieldName("right");
|
|
35607
|
+
if (!rightNode) return null;
|
|
35608
|
+
if (rightNode.type === "lambda") {
|
|
35609
|
+
const leftNode = node.childForFieldName("left");
|
|
35610
|
+
return leftNode?.text ?? null;
|
|
35611
|
+
}
|
|
35612
|
+
if (rightNode.type === "call") {
|
|
35613
|
+
const methodNode = rightNode.childForFieldName("method");
|
|
35614
|
+
const methodName = methodNode?.text;
|
|
35615
|
+
if (methodName === "proc" || methodName === "lambda") {
|
|
35616
|
+
const leftNode = node.childForFieldName("left");
|
|
35617
|
+
return leftNode?.text ?? null;
|
|
35618
|
+
}
|
|
35619
|
+
const receiverNode = rightNode.childForFieldName("receiver");
|
|
35620
|
+
if (receiverNode?.text === "Proc" && methodName === "new") {
|
|
35621
|
+
const leftNode = node.childForFieldName("left");
|
|
35622
|
+
return leftNode?.text ?? null;
|
|
35623
|
+
}
|
|
35624
|
+
}
|
|
35625
|
+
return null;
|
|
35626
|
+
},
|
|
35627
|
+
isExported: () => false
|
|
35628
|
+
}
|
|
35629
|
+
],
|
|
35630
|
+
importRules: [{ nodeType: "call", extract: extractRubyImports }],
|
|
35631
|
+
callRule: { nodeType: "call", functionField: "method" },
|
|
35632
|
+
callIgnores: RUBY_CALL_IGNORES,
|
|
35633
|
+
normalizeCallName: normalizeRubyCallName,
|
|
35634
|
+
contextNodeTypes: /* @__PURE__ */ new Set(["class", "module", "method", "singleton_method"]),
|
|
35635
|
+
contextNameField: "name",
|
|
35636
|
+
qualifyCall: rubyQualifyCall
|
|
35637
|
+
};
|
|
35321
35638
|
}
|
|
35322
35639
|
});
|
|
35323
35640
|
|
|
@@ -35345,12 +35662,14 @@ var init_tree_sitter_parser = __esm({
|
|
|
35345
35662
|
LANGUAGE_CONFIGS = {
|
|
35346
35663
|
python: pythonConfig,
|
|
35347
35664
|
go: goConfig,
|
|
35348
|
-
rust: rustConfig
|
|
35665
|
+
rust: rustConfig,
|
|
35666
|
+
ruby: rubyConfig
|
|
35349
35667
|
};
|
|
35350
35668
|
REGEX_FALLBACKS = {
|
|
35351
35669
|
python: parsePython,
|
|
35352
35670
|
go: parseGo,
|
|
35353
|
-
rust: parseRust
|
|
35671
|
+
rust: parseRust,
|
|
35672
|
+
ruby: parseRuby
|
|
35354
35673
|
};
|
|
35355
35674
|
TreeSitterParser = class {
|
|
35356
35675
|
name = "tree-sitter";
|
|
@@ -43052,12 +43371,18 @@ var init_scanner = __esm({
|
|
|
43052
43371
|
DEFAULT_INCLUDE_PATTERNS = [
|
|
43053
43372
|
...Array.from(codeExtensions.keys()).map((ext) => `**/*${ext}`),
|
|
43054
43373
|
...Array.from(configExtensions.keys()).map((ext) => `**/*${ext}`),
|
|
43055
|
-
...Array.from(markdownExtensions).map((ext) => `**/*${ext}`)
|
|
43374
|
+
...Array.from(markdownExtensions).map((ext) => `**/*${ext}`),
|
|
43375
|
+
...Array.from(styleExtensions.keys()).map((ext) => `**/*${ext}`),
|
|
43376
|
+
...Array.from(templateExtensions.keys()).map((ext) => `**/*${ext}`),
|
|
43377
|
+
...Array.from(scriptExtensions.keys()).map((ext) => `**/*${ext}`)
|
|
43056
43378
|
];
|
|
43057
43379
|
ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
43058
43380
|
...codeExtensions.keys(),
|
|
43059
43381
|
...configExtensions.keys(),
|
|
43060
|
-
...markdownExtensions
|
|
43382
|
+
...markdownExtensions,
|
|
43383
|
+
...styleExtensions.keys(),
|
|
43384
|
+
...templateExtensions.keys(),
|
|
43385
|
+
...scriptExtensions.keys()
|
|
43061
43386
|
]);
|
|
43062
43387
|
DEFAULT_EXCLUDE_PATTERNS = [
|
|
43063
43388
|
"**/node_modules",
|
|
@@ -43178,6 +43503,9 @@ function createWorkspaceFileResolver(workspaceRoot, files) {
|
|
|
43178
43503
|
const resolved = resolvePythonImport(importerRelativePath, importPath);
|
|
43179
43504
|
if (resolved) return resolved;
|
|
43180
43505
|
}
|
|
43506
|
+
if (language === "ruby") {
|
|
43507
|
+
return resolveRelativeImport(importerRelativePath, importPath);
|
|
43508
|
+
}
|
|
43181
43509
|
if (importPath.startsWith(".")) {
|
|
43182
43510
|
return resolveRelativeImport(importerRelativePath, importPath);
|
|
43183
43511
|
}
|
|
@@ -44116,7 +44444,8 @@ var init_index_workspace = __esm({
|
|
|
44116
44444
|
".cts",
|
|
44117
44445
|
".md",
|
|
44118
44446
|
".mdx",
|
|
44119
|
-
".py"
|
|
44447
|
+
".py",
|
|
44448
|
+
".rb"
|
|
44120
44449
|
];
|
|
44121
44450
|
PARSER_VERSION_OXC = "oxc-v2";
|
|
44122
44451
|
PARSER_VERSION_NATIVE = "native-v1";
|
|
@@ -59961,7 +60290,7 @@ __export(mcp_bridge_exports, {
|
|
|
59961
60290
|
startMcpServer: () => startMcpServer
|
|
59962
60291
|
});
|
|
59963
60292
|
async function startMcpServer(defaultPath, version4) {
|
|
59964
|
-
await createAndStartMcpServer({ defaultPath, version: version4, build: "
|
|
60293
|
+
await createAndStartMcpServer({ defaultPath, version: version4, build: "5060798-dirty" });
|
|
59965
60294
|
}
|
|
59966
60295
|
var init_mcp_bridge = __esm({
|
|
59967
60296
|
"src/mcp-bridge.ts"() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openez-graph/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Local-first code intelligence engine — index, query, and graph your codebase with zero config. SQLite-only, MCP-first.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Asta Nguyen",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"oxc-parser": "^0.143.0",
|
|
52
52
|
"tree-sitter-go": "^0.25.0",
|
|
53
53
|
"tree-sitter-python": "^0.25.0",
|
|
54
|
+
"tree-sitter-ruby": "^0.23.1",
|
|
54
55
|
"tree-sitter-rust": "^0.24.0",
|
|
55
56
|
"web-tree-sitter": "^0.26.11"
|
|
56
57
|
},
|