@youichi/repograph 0.0.0-stage → 0.1.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 +183 -2
- package/dist/callables.js +63 -0
- package/dist/cbm.js +72 -0
- package/dist/classes.js +192 -0
- package/dist/cli.js +66 -0
- package/dist/index.js +140 -0
- package/package.json +39 -4
package/README.md
CHANGED
|
@@ -1,3 +1,184 @@
|
|
|
1
|
-
#
|
|
1
|
+
# repograph
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Export repository indexes as JSON graphs for other programs using the [codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp) CLI. No language model, API key, or MCP client is required.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Requires Node.js 22 or later. Run directly with `npx`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @youichi/repograph export /path/to/repository
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The project pins `codebase-memory-mcp@0.12.1`. The initial installation downloads the native binary for your platform and requires access to npm and GitHub. Subsequent indexing runs locally. No global installation or editor/agent configuration through `codebase-memory-mcp install` is required.
|
|
14
|
+
|
|
15
|
+
## Exporting graphs
|
|
16
|
+
|
|
17
|
+
Run these commands from any directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Export all indexed nodes and relationships, plus source-derived class members and dependencies
|
|
21
|
+
npx @youichi/repograph export ../anureo-sdk
|
|
22
|
+
|
|
23
|
+
# Specify an output file
|
|
24
|
+
npx @youichi/repograph export /path/to/repository --out .repograph/repository.json
|
|
25
|
+
|
|
26
|
+
# Export only CALLS edges, functions/methods, and their endpoints
|
|
27
|
+
npx @youichi/repograph export ../anureo-sdk --kind calls --out .repograph/calls.json
|
|
28
|
+
|
|
29
|
+
# Show available options
|
|
30
|
+
npx @youichi/repograph --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The default output is `.repograph/graph.json` under the current working directory. Progress goes to stderr. On success, stdout reports the output path and node and edge counts. Failures exit with code 1, and partial results never replace an existing output file.
|
|
34
|
+
|
|
35
|
+
| Option | Default | Purpose |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `--out`, `-o` | `.repograph/graph.json` | JSON output path |
|
|
38
|
+
| `--kind` | `all` | `all` includes class structure and dependencies; `calls` keeps only direct call relationships |
|
|
39
|
+
| `--project` | Directory name + hash of the absolute path | Override the upstream index name |
|
|
40
|
+
| `--page-size` | `500` | Rows per query batch, from 1 to 10000 |
|
|
41
|
+
|
|
42
|
+
Each export refreshes the index before reading nodes and edges separately. Queries support pagination and response cursors. Export stops if it detects evaluation limits, truncation without a continuation cursor, duplicate IDs, missing endpoints, or count mismatches. Call graphs retain functions and methods with no call edges.
|
|
43
|
+
|
|
44
|
+
## Output structure
|
|
45
|
+
|
|
46
|
+
The following is a simplified example. Actual properties depend on the language and upstream analysis results:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"schemaVersion": 1,
|
|
51
|
+
"generatedAt": "2026-11-12T00:00:00.000Z",
|
|
52
|
+
"repositoryPath": "/path/to/repository",
|
|
53
|
+
"project": "repository-0123456789ab",
|
|
54
|
+
"kind": "calls",
|
|
55
|
+
"indexReport": { "status": "indexed", "nodes": 2, "edges": 1 },
|
|
56
|
+
"files": {
|
|
57
|
+
"main.ts": { "lines": 7 }
|
|
58
|
+
},
|
|
59
|
+
"nodes": [
|
|
60
|
+
{
|
|
61
|
+
"id": "1",
|
|
62
|
+
"labels": ["Function"],
|
|
63
|
+
"name": "checkout",
|
|
64
|
+
"qualifiedName": "repository.main.checkout",
|
|
65
|
+
"filePath": "main.ts",
|
|
66
|
+
"startLine": 1,
|
|
67
|
+
"endLine": 3,
|
|
68
|
+
"properties": {},
|
|
69
|
+
"callable": { "parameters": [], "returnType": null }
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "2",
|
|
73
|
+
"labels": ["Function"],
|
|
74
|
+
"name": "validate",
|
|
75
|
+
"qualifiedName": "repository.main.validate",
|
|
76
|
+
"filePath": "main.ts",
|
|
77
|
+
"startLine": 5,
|
|
78
|
+
"endLine": 7,
|
|
79
|
+
"properties": {},
|
|
80
|
+
"callable": { "parameters": [], "returnType": null }
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"edges": [
|
|
84
|
+
{ "id": "1", "source": "1", "target": "2", "type": "CALLS", "properties": {} }
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Node IDs and edge IDs are strings in separate namespaces. An edge's `source` and `target` refer to node IDs. IDs link records within the current export and are not guaranteed to remain stable across indexing runs. `indexReport` preserves the upstream indexing report; its counts describe the upstream index and can differ from the exported graph after filtering or adding source-derived class data.
|
|
90
|
+
|
|
91
|
+
TypeScript and JavaScript `Function` and `Method` nodes include `callable` metadata in both `all` and `calls` exports. The exporter parses upstream `properties.signature` and `properties.return_type` with the TypeScript parser, without loading the target project's dependencies or inferring types. Supported file extensions are `.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.jsx`, `.mjs`, and `.cjs`. Other languages and node labels omit `callable`. The original `properties` remain intact, and this additive field retains `schemaVersion: 1`.
|
|
92
|
+
|
|
93
|
+
In `all` exports, TypeScript and JavaScript `Class` and `Interface` nodes include a `members` array for declared fields and accessors. Entries retain declared type text (or `null` when absent), source lines, visibility, static, optional, and readonly flags. The exporter also adds `DEPENDS_ON` edges for resolvable field, method-parameter, and method-return type references, plus method calls between classes. These source-derived edges have `properties.derivedFrom: "typescript-ast"` and a `reason`; they are omitted when a referenced class cannot be resolved without ambiguity. `calls` exports intentionally omit these class members and dependencies. Other languages retain whatever class, field, inheritance, implementation, and dependency nodes and edges the upstream indexer supplies.
|
|
94
|
+
|
|
95
|
+
For example, `Customer` with an `address?: Address` field and a `save(input: Address): Result` method includes:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"labels": ["Class"],
|
|
100
|
+
"name": "Customer",
|
|
101
|
+
"members": [
|
|
102
|
+
{
|
|
103
|
+
"name": "address",
|
|
104
|
+
"kind": "field",
|
|
105
|
+
"type": "Address",
|
|
106
|
+
"filePath": "model.ts",
|
|
107
|
+
"startLine": 6,
|
|
108
|
+
"endLine": 6,
|
|
109
|
+
"visibility": "public",
|
|
110
|
+
"static": false,
|
|
111
|
+
"optional": true,
|
|
112
|
+
"readonly": false
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The graph also contains edges such as `{ "source": "<Customer id>", "target": "<Address id>", "type": "DEPENDS_ON", "properties": { "derivedFrom": "typescript-ast", "reason": "field-type", "member": "address" } }`. Existing upstream `INHERITS` and `IMPLEMENTS` edges are retained as-is.
|
|
119
|
+
|
|
120
|
+
For example, `function load(id: string, count = 1, ...tags: string[]): Promise<string[]>` produces:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"callable": {
|
|
125
|
+
"parameters": [
|
|
126
|
+
{ "name": "id", "type": "string", "optional": false, "rest": false },
|
|
127
|
+
{ "name": "count", "type": null, "optional": true, "rest": false },
|
|
128
|
+
{ "name": "tags", "type": "string[]", "optional": false, "rest": true }
|
|
129
|
+
],
|
|
130
|
+
"returnType": "Promise<string[]>"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Parameter order follows the signature. `name` retains the source binding name or destructuring pattern. `type` is the declared type text, or `null` when no annotation is available. `optional` is true for a `?` marker or default initializer; `rest` identifies a `...` parameter. Default values do not cause type inference. Source-level TypeScript `this` parameters are retained when present in the upstream signature.
|
|
136
|
+
|
|
137
|
+
`parameters: []` means a known empty parameter list; `parameters: null` means the upstream signature is missing or could not be parsed. `returnType` excludes the leading colon and is `null` when no annotation is available; explicit `void`, `never`, and `Promise<T>` are preserved. If the combined signature and return annotation are malformed, both fields are `null`. Missing types are never replaced with `any` or `void`. Only the signature retained on each upstream node is exported; separate overloads, accessor pairs, generic parameter declarations, and omitted upstream symbols are not reconstructed.
|
|
138
|
+
|
|
139
|
+
`files` uses each `File` node's `filePath` as its key and records the physical line count in `lines`, including blank lines and comments. Empty files have `0` lines, and a trailing line ending does not add an extra line. LF, CRLF, and CR line endings are supported. Unreadable files have `null` lines. Counts come directly from source files at export time, rather than being inferred from upstream nodes' `properties.lines`. Both `all` and `calls` retain file information from the complete index, even when file nodes are filtered out of the call graph. Files outside the index are not scanned.
|
|
140
|
+
|
|
141
|
+
## TypeScript usage
|
|
142
|
+
|
|
143
|
+
The project also exports a thin wrapper that uses the CLI internally:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { exportGraph } from "./src/index.ts";
|
|
147
|
+
|
|
148
|
+
const graph = await exportGraph("../anureo-sdk", {
|
|
149
|
+
kind: "calls",
|
|
150
|
+
onProgress: console.error,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
console.log(graph.nodes, graph.edges);
|
|
154
|
+
|
|
155
|
+
for (const node of graph.nodes) {
|
|
156
|
+
if (node.callable) console.log(node.name, node.callable.parameters, node.callable.returnType);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Development and validation
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
bun run typecheck
|
|
164
|
+
bun test
|
|
165
|
+
bun run test:integration
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Unit tests cover pagination, cursors, truncation handling, file line counts, and structured callable metadata. The integration test uses the real native backend to index a temporary TypeScript project, checks cross-file calls, isolated functions, parameter and return annotations, methods, and export counts, and cleans up its temporary index.
|
|
169
|
+
|
|
170
|
+
## Publishing
|
|
171
|
+
|
|
172
|
+
Releases are published from GitHub Actions when a version tag is pushed. Before the first release, configure npm Trusted Publishing for package `@youichi/repograph` with GitHub user `hi-youichi`, repository `repograph`, and workflow file `publish-npm.yml`; allow direct `npm publish` for that publisher. Then update `package.json` to the new version and push the matching tag, for example `v0.1.1`. The workflow checks that the tag matches the package version, installs dependencies, runs typecheck and unit tests, then builds and publishes with npm provenance. No npm token is stored in GitHub Secrets.
|
|
173
|
+
|
|
174
|
+
## Limitations
|
|
175
|
+
|
|
176
|
+
- Exports contain the static relationships actually indexed upstream and do not guarantee coverage of all runtime calls. Support for reflection, dynamic dispatch, and framework conventions varies by language.
|
|
177
|
+
- The upstream indexer skips `node_modules`, files matching ignore rules, and other excluded inputs. Use the skip and parse counts in `indexReport` to assess coverage.
|
|
178
|
+
- `calls` mode retains only `CALLS` edges. It does not treat `CALL_REFERENCE` or `USAGE` as direct calls. The upstream analyzer may also record recursion in properties while omitting self-call edges.
|
|
179
|
+
- Source-derived class members and type dependencies currently cover TypeScript and JavaScript files. Other languages depend on the class, field, and relationship data available from the pinned upstream indexer; type references are not inferred from unannotated initializers.
|
|
180
|
+
- Keep source files and the project's index unchanged during export, and prevent other processes from rebuilding the index concurrently. Exports are not transactional snapshots across queries.
|
|
181
|
+
- Results are assembled in memory before being written to disk. Large repositories require sufficient memory, and upstream evaluation limits cause an error.
|
|
182
|
+
- Place output outside the target repository to prevent a later indexing run from treating exported files as input. Upstream indexes remain in the local cache.
|
|
183
|
+
|
|
184
|
+
The project currently exports JSON only. Graphviz, Cytoscape, or other frontend tools can consume it for visualization.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// src/callables.ts
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
var EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
5
|
+
function addCallables(nodes) {
|
|
6
|
+
const inputs = nodes.filter((node) => node.labels.some((label) => label === "Function" || label === "Method") && EXTENSIONS.has(extname(node.filePath).toLowerCase()));
|
|
7
|
+
if (inputs.length === 0)
|
|
8
|
+
return;
|
|
9
|
+
const sources = new Map;
|
|
10
|
+
const signatures = inputs.map((node, i) => {
|
|
11
|
+
const rawSignature = node.properties.signature;
|
|
12
|
+
const signature = typeof rawSignature === "string" && rawSignature.trim() !== "" ? rawSignature.trim() : null;
|
|
13
|
+
const rawReturn = node.properties.return_type;
|
|
14
|
+
const returnType = typeof rawReturn === "string" && rawReturn.trim() !== "" ? rawReturn.trim().replace(/^:\s*/, "") : null;
|
|
15
|
+
const jsx = [".tsx", ".jsx"].includes(extname(node.filePath).toLowerCase());
|
|
16
|
+
const path = `/__repograph_signatures__/${i}.${jsx ? "tsx" : "ts"}`;
|
|
17
|
+
const text = `function __repograph${signature ?? "()"}${returnType !== null ? `: ${returnType}` : ""}
|
|
18
|
+
{}`;
|
|
19
|
+
sources.set(path, ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, jsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS));
|
|
20
|
+
return { node, path, signature };
|
|
21
|
+
});
|
|
22
|
+
const options = {
|
|
23
|
+
noLib: true,
|
|
24
|
+
noResolve: true,
|
|
25
|
+
noEmit: true,
|
|
26
|
+
target: ts.ScriptTarget.Latest,
|
|
27
|
+
jsx: ts.JsxEmit.Preserve
|
|
28
|
+
};
|
|
29
|
+
const host = {
|
|
30
|
+
getSourceFile: (path) => sources.get(path),
|
|
31
|
+
getDefaultLibFileName: () => "",
|
|
32
|
+
writeFile: () => {},
|
|
33
|
+
getCurrentDirectory: () => "/",
|
|
34
|
+
getDirectories: () => [],
|
|
35
|
+
fileExists: (path) => sources.has(path),
|
|
36
|
+
readFile: (path) => sources.get(path)?.text,
|
|
37
|
+
getCanonicalFileName: (path) => path,
|
|
38
|
+
useCaseSensitiveFileNames: () => true,
|
|
39
|
+
getNewLine: () => `
|
|
40
|
+
`
|
|
41
|
+
};
|
|
42
|
+
const program = ts.createProgram([...sources.keys()], options, host);
|
|
43
|
+
for (const { node, path, signature } of signatures) {
|
|
44
|
+
const callable = { parameters: null, returnType: null };
|
|
45
|
+
node.callable = callable;
|
|
46
|
+
const source = sources.get(path);
|
|
47
|
+
const declaration = source.statements[0];
|
|
48
|
+
if (program.getSyntacticDiagnostics(source).length !== 0 || source.statements.length !== 1 || !declaration || !ts.isFunctionDeclaration(declaration) || declaration.name?.text !== "__repograph" || !declaration.body || declaration.body.statements.length !== 0)
|
|
49
|
+
continue;
|
|
50
|
+
if (signature !== null) {
|
|
51
|
+
callable.parameters = declaration.parameters.map((parameter) => ({
|
|
52
|
+
name: parameter.name.getText(source),
|
|
53
|
+
type: parameter.type?.getText(source) ?? null,
|
|
54
|
+
optional: parameter.questionToken !== undefined || parameter.initializer !== undefined,
|
|
55
|
+
rest: parameter.dotDotDotToken !== undefined
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
callable.returnType = declaration.type?.getText(source) ?? null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
addCallables
|
|
63
|
+
};
|
package/dist/cbm.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/cbm.ts
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
var runTool = async (tool, args) => {
|
|
5
|
+
const shim = createRequire(import.meta.url).resolve("codebase-memory-mcp/bin.js");
|
|
6
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(process.execPath, [shim, "cli", "--json", tool], { stdio: ["pipe", "pipe", "inherit"] });
|
|
8
|
+
let output = "";
|
|
9
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
10
|
+
output += chunk;
|
|
11
|
+
});
|
|
12
|
+
child.on("error", reject);
|
|
13
|
+
child.on("close", (code) => code === 0 ? resolve(output) : reject(new Error(`codebase-memory-mcp ${tool} exited with ${code}`)));
|
|
14
|
+
child.stdin.end(JSON.stringify(args));
|
|
15
|
+
});
|
|
16
|
+
const envelope = JSON.parse(stdout);
|
|
17
|
+
const message = envelope.content?.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
|
|
18
|
+
`) ?? "";
|
|
19
|
+
if (envelope.isError)
|
|
20
|
+
throw new Error(message || `${tool} failed`);
|
|
21
|
+
const payload = envelope.structuredContent ?? JSON.parse(message);
|
|
22
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
23
|
+
throw new Error(`${tool} returned an invalid payload`);
|
|
24
|
+
}
|
|
25
|
+
return payload;
|
|
26
|
+
};
|
|
27
|
+
async function collectRows(run, project, baseQuery, pageSize) {
|
|
28
|
+
const result = [];
|
|
29
|
+
for (let offset = 0;; offset += pageSize) {
|
|
30
|
+
const query = `${baseQuery} ORDER BY id SKIP ${offset} LIMIT ${pageSize}`;
|
|
31
|
+
let cursor;
|
|
32
|
+
let count = 0;
|
|
33
|
+
const seenCursors = new Set;
|
|
34
|
+
do {
|
|
35
|
+
const page = await run("query_graph", {
|
|
36
|
+
project,
|
|
37
|
+
query,
|
|
38
|
+
format: "json",
|
|
39
|
+
max_rows: pageSize,
|
|
40
|
+
max_output_tokens: 1e6,
|
|
41
|
+
...cursor ? { cursor } : {}
|
|
42
|
+
});
|
|
43
|
+
if (page.total_relation !== "eq" || page.warning) {
|
|
44
|
+
throw new Error(`Query evaluation is incomplete: ${page.warning ?? page.truncation_reason ?? "inexact total"}`);
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(page.columns) || !page.columns.every((x) => typeof x === "string") || !Array.isArray(page.rows)) {
|
|
47
|
+
throw new Error("Unexpected query_graph response shape");
|
|
48
|
+
}
|
|
49
|
+
const columns = page.columns;
|
|
50
|
+
for (const row of page.rows) {
|
|
51
|
+
if (!Array.isArray(row) || row.length !== columns.length || !row.every((value) => typeof value === "string")) {
|
|
52
|
+
throw new Error("Unexpected query_graph row shape");
|
|
53
|
+
}
|
|
54
|
+
result.push(Object.fromEntries(columns.map((name, i) => [name, row[i]])));
|
|
55
|
+
count++;
|
|
56
|
+
}
|
|
57
|
+
if (page.has_more === false && page.truncated === false)
|
|
58
|
+
break;
|
|
59
|
+
if (typeof page.next_cursor !== "string" || seenCursors.has(page.next_cursor)) {
|
|
60
|
+
throw new Error("Query result was truncated without an advancing cursor; export aborted");
|
|
61
|
+
}
|
|
62
|
+
cursor = page.next_cursor;
|
|
63
|
+
seenCursors.add(cursor);
|
|
64
|
+
} while (true);
|
|
65
|
+
if (count < pageSize)
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
runTool,
|
|
71
|
+
collectRows
|
|
72
|
+
};
|
package/dist/classes.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// src/classes.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { extname, resolve } from "node:path";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
var CLASS_LABELS = new Set(["Class", "Interface", "Struct", "Trait"]);
|
|
7
|
+
var SCRIPT_KINDS = {
|
|
8
|
+
".ts": ts.ScriptKind.TS,
|
|
9
|
+
".tsx": ts.ScriptKind.TSX,
|
|
10
|
+
".mts": ts.ScriptKind.TS,
|
|
11
|
+
".cts": ts.ScriptKind.TS,
|
|
12
|
+
".js": ts.ScriptKind.JS,
|
|
13
|
+
".jsx": ts.ScriptKind.JSX,
|
|
14
|
+
".mjs": ts.ScriptKind.JS,
|
|
15
|
+
".cjs": ts.ScriptKind.JS
|
|
16
|
+
};
|
|
17
|
+
function modifiers(node) {
|
|
18
|
+
return ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : [];
|
|
19
|
+
}
|
|
20
|
+
function memberName(name) {
|
|
21
|
+
if (!name)
|
|
22
|
+
return;
|
|
23
|
+
if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name))
|
|
24
|
+
return name.text;
|
|
25
|
+
if (ts.isStringLiteral(name) || ts.isNumericLiteral(name))
|
|
26
|
+
return name.text;
|
|
27
|
+
if (ts.isComputedPropertyName(name) && (ts.isStringLiteral(name.expression) || ts.isNumericLiteral(name.expression)))
|
|
28
|
+
return name.expression.text;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
function typeNames(type, into = new Set) {
|
|
32
|
+
const visit = (node) => {
|
|
33
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
34
|
+
const name = node.typeName.getText().split(".").at(-1);
|
|
35
|
+
if (name)
|
|
36
|
+
into.add(name);
|
|
37
|
+
}
|
|
38
|
+
ts.forEachChild(node, visit);
|
|
39
|
+
};
|
|
40
|
+
visit(type);
|
|
41
|
+
return into;
|
|
42
|
+
}
|
|
43
|
+
function sourceLine(file, node) {
|
|
44
|
+
return file.getLineAndCharacterOfPosition(node.getStart(file)).line + 1;
|
|
45
|
+
}
|
|
46
|
+
function sourceEndLine(file, node) {
|
|
47
|
+
return file.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
|
|
48
|
+
}
|
|
49
|
+
function toMember(file, filePath, node) {
|
|
50
|
+
if (!ts.isPropertyDeclaration(node) && !ts.isPropertySignature(node) && !ts.isGetAccessorDeclaration(node) && !ts.isSetAccessorDeclaration(node))
|
|
51
|
+
return;
|
|
52
|
+
const name = memberName(node.name);
|
|
53
|
+
if (name === undefined)
|
|
54
|
+
return;
|
|
55
|
+
const flags = modifiers(node);
|
|
56
|
+
const visibility = flags.some((flag) => flag.kind === ts.SyntaxKind.PrivateKeyword) || name.startsWith("#") ? "private" : flags.some((flag) => flag.kind === ts.SyntaxKind.ProtectedKeyword) ? "protected" : "public";
|
|
57
|
+
const kind = ts.isGetAccessorDeclaration(node) ? "getter" : ts.isSetAccessorDeclaration(node) ? "setter" : "field";
|
|
58
|
+
return {
|
|
59
|
+
name,
|
|
60
|
+
kind,
|
|
61
|
+
type: node.type?.getText(file) ?? null,
|
|
62
|
+
filePath,
|
|
63
|
+
startLine: sourceLine(file, node),
|
|
64
|
+
endLine: sourceEndLine(file, node),
|
|
65
|
+
visibility,
|
|
66
|
+
static: flags.some((flag) => flag.kind === ts.SyntaxKind.StaticKeyword),
|
|
67
|
+
optional: !!node.questionToken,
|
|
68
|
+
readonly: flags.some((flag) => flag.kind === ts.SyntaxKind.ReadonlyKeyword)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function declaredType(node) {
|
|
72
|
+
if (ts.isPropertyDeclaration(node) || ts.isPropertySignature(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node))
|
|
73
|
+
return node.type;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
function isClassLike(node) {
|
|
77
|
+
return ts.isClassDeclaration(node) || ts.isClassExpression(node) || ts.isInterfaceDeclaration(node);
|
|
78
|
+
}
|
|
79
|
+
function hash(value) {
|
|
80
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 20);
|
|
81
|
+
}
|
|
82
|
+
async function addClassMetadata(repositoryPath, nodes, edges) {
|
|
83
|
+
const byFileAndName = new Map;
|
|
84
|
+
const classNodes = nodes.filter((node) => node.labels.some((label) => CLASS_LABELS.has(label)));
|
|
85
|
+
for (const node of classNodes) {
|
|
86
|
+
const key = `${node.filePath}\x00${node.name}`;
|
|
87
|
+
byFileAndName.set(key, [...byFileAndName.get(key) ?? [], node]);
|
|
88
|
+
}
|
|
89
|
+
const byName = new Map;
|
|
90
|
+
for (const node of classNodes)
|
|
91
|
+
byName.set(node.name, [...byName.get(node.name) ?? [], node]);
|
|
92
|
+
const sources = new Map;
|
|
93
|
+
const paths = new Set(classNodes.map((node) => node.filePath).filter(Boolean));
|
|
94
|
+
for (const filePath of paths) {
|
|
95
|
+
const scriptKind = SCRIPT_KINDS[extname(filePath).toLowerCase()];
|
|
96
|
+
if (scriptKind === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
try {
|
|
99
|
+
const text = await readFile(resolve(repositoryPath, filePath), "utf8");
|
|
100
|
+
sources.set(filePath, ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, scriptKind));
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
const dependencies = new Map;
|
|
104
|
+
const existingDependencyPairs = new Set(edges.filter((edge) => edge.type === "DEPENDS_ON").map((edge) => `${edge.source}\x00${edge.target}`));
|
|
105
|
+
const addDependency = (source, target, reason, member) => {
|
|
106
|
+
if (source.id === target.id)
|
|
107
|
+
return;
|
|
108
|
+
const pair = `${source.id}\x00${target.id}`;
|
|
109
|
+
if (existingDependencyPairs.has(pair))
|
|
110
|
+
return;
|
|
111
|
+
const id = `derived:${hash(`${source.id}\x00${target.id}\x00${reason}\x00${member}`)}`;
|
|
112
|
+
dependencies.set(id, {
|
|
113
|
+
id,
|
|
114
|
+
source: source.id,
|
|
115
|
+
target: target.id,
|
|
116
|
+
type: "DEPENDS_ON",
|
|
117
|
+
properties: { derivedFrom: "typescript-ast", reason, member }
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
for (const [filePath, file] of sources) {
|
|
121
|
+
const visit = (node) => {
|
|
122
|
+
if (!isClassLike(node) || !node.name) {
|
|
123
|
+
ts.forEachChild(node, visit);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const name = node.name.text;
|
|
127
|
+
const candidates = byFileAndName.get(`${filePath}\x00${name}`) ?? [];
|
|
128
|
+
const start = sourceLine(file, node);
|
|
129
|
+
const owner = candidates.find((candidate) => candidate.startLine === start) ?? (candidates.length === 1 ? candidates[0] : undefined);
|
|
130
|
+
if (!owner) {
|
|
131
|
+
ts.forEachChild(node, visit);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const members = [];
|
|
135
|
+
const collect = (member) => {
|
|
136
|
+
const value = toMember(file, filePath, member);
|
|
137
|
+
const propertyType = declaredType(member);
|
|
138
|
+
if (value) {
|
|
139
|
+
members.push(value);
|
|
140
|
+
if (propertyType) {
|
|
141
|
+
for (const typeName of typeNames(propertyType)) {
|
|
142
|
+
const targets = byName.get(typeName) ?? [];
|
|
143
|
+
const target = targets.length === 1 ? targets[0] : targets.find((item) => item.filePath === filePath);
|
|
144
|
+
if (target)
|
|
145
|
+
addDependency(owner, target, "field-type", value.name);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if ((ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) && (member.type || member.parameters.length)) {
|
|
150
|
+
for (const parameter of member.parameters) {
|
|
151
|
+
if (!parameter.type)
|
|
152
|
+
continue;
|
|
153
|
+
for (const typeName of typeNames(parameter.type)) {
|
|
154
|
+
const targets = byName.get(typeName) ?? [];
|
|
155
|
+
const target = targets.length === 1 ? targets[0] : targets.find((item) => item.filePath === filePath);
|
|
156
|
+
if (target)
|
|
157
|
+
addDependency(owner, target, "method-parameter", member.name.getText(file));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (member.type) {
|
|
161
|
+
for (const typeName of typeNames(member.type)) {
|
|
162
|
+
const targets = byName.get(typeName) ?? [];
|
|
163
|
+
const target = targets.length === 1 ? targets[0] : targets.find((item) => item.filePath === filePath);
|
|
164
|
+
if (target)
|
|
165
|
+
addDependency(owner, target, "method-return", member.name.getText(file));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
for (const member of node.members)
|
|
171
|
+
collect(member);
|
|
172
|
+
if (members.length)
|
|
173
|
+
owner.members = members;
|
|
174
|
+
ts.forEachChild(node, visit);
|
|
175
|
+
};
|
|
176
|
+
visit(file);
|
|
177
|
+
}
|
|
178
|
+
const classesByQualifiedName = new Map(classNodes.map((node) => [node.qualifiedName, node]));
|
|
179
|
+
const methodOwners = new Map(nodes.filter((node) => node.labels.includes("Method") && typeof node.properties.parent_class === "string").map((node) => [node.id, classesByQualifiedName.get(node.properties.parent_class)]));
|
|
180
|
+
const nodesById = new Map(nodes.map((node) => [node.id, node]));
|
|
181
|
+
for (const edge of edges.filter((item) => item.type === "CALLS")) {
|
|
182
|
+
const source = methodOwners.get(edge.source);
|
|
183
|
+
const targetNode = nodesById.get(edge.target);
|
|
184
|
+
const target = targetNode && methodOwners.get(targetNode.id) || (targetNode?.labels.some((label) => CLASS_LABELS.has(label)) ? targetNode : undefined);
|
|
185
|
+
if (source && target)
|
|
186
|
+
addDependency(source, target, "method-call", targetNode?.name ?? "");
|
|
187
|
+
}
|
|
188
|
+
edges.push(...dependencies.values());
|
|
189
|
+
}
|
|
190
|
+
export {
|
|
191
|
+
addClassMetadata
|
|
192
|
+
};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { exportGraph } from "./index.js";
|
|
9
|
+
var HELP = `repograph — export repository graphs with codebase-memory-mcp
|
|
10
|
+
|
|
11
|
+
Usage: repograph export <repository> [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-o, --out <file> JSON output (default: .repograph/graph.json)
|
|
15
|
+
--kind <all|calls> Export all relationships or CALLS only (default: all)
|
|
16
|
+
--project <name> Override the path-derived index name
|
|
17
|
+
--page-size <n> Query batch size, 1–10000 (default: 500)
|
|
18
|
+
-h, --help Show help
|
|
19
|
+
`;
|
|
20
|
+
async function main() {
|
|
21
|
+
const { values, positionals } = parseArgs({
|
|
22
|
+
args: process.argv.slice(2),
|
|
23
|
+
allowPositionals: true,
|
|
24
|
+
strict: true,
|
|
25
|
+
options: {
|
|
26
|
+
out: { type: "string", short: "o" },
|
|
27
|
+
kind: { type: "string" },
|
|
28
|
+
project: { type: "string" },
|
|
29
|
+
"page-size": { type: "string" },
|
|
30
|
+
help: { type: "boolean", short: "h" }
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
if (values.help || positionals.length === 0) {
|
|
34
|
+
console.log(HELP);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (positionals[0] !== "export" || positionals.length !== 2) {
|
|
38
|
+
throw new Error("Expected: export <repository>. Use --help for usage.");
|
|
39
|
+
}
|
|
40
|
+
const kind = values.kind ?? "all";
|
|
41
|
+
if (kind !== "all" && kind !== "calls")
|
|
42
|
+
throw new Error("--kind must be all or calls");
|
|
43
|
+
if (values.project !== undefined && !values.project.trim())
|
|
44
|
+
throw new Error("--project cannot be empty");
|
|
45
|
+
const graph = await exportGraph(positionals[1], {
|
|
46
|
+
kind,
|
|
47
|
+
project: values.project,
|
|
48
|
+
pageSize: values["page-size"] === undefined ? undefined : Number(values["page-size"]),
|
|
49
|
+
onProgress: (message) => console.error(message)
|
|
50
|
+
});
|
|
51
|
+
const output = resolve(values.out ?? ".repograph/graph.json");
|
|
52
|
+
await mkdir(dirname(output), { recursive: true });
|
|
53
|
+
const temporary = `${output}.${randomUUID()}.tmp`;
|
|
54
|
+
try {
|
|
55
|
+
await writeFile(temporary, `${JSON.stringify(graph, null, 2)}
|
|
56
|
+
`, { flag: "wx" });
|
|
57
|
+
await rename(temporary, output);
|
|
58
|
+
} finally {
|
|
59
|
+
await rm(temporary, { force: true });
|
|
60
|
+
}
|
|
61
|
+
console.log(JSON.stringify({ output, nodes: graph.nodes.length, edges: graph.edges.length, kind }));
|
|
62
|
+
}
|
|
63
|
+
main().catch((error) => {
|
|
64
|
+
console.error(`repograph: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { collectRows, runTool } from "./cbm.js";
|
|
6
|
+
import { addCallables } from "./callables.js";
|
|
7
|
+
import { addClassMetadata } from "./classes.js";
|
|
8
|
+
var NODE_QUERY = `MATCH (n) RETURN id(n) AS id, labels(n) AS labels,
|
|
9
|
+
n.name AS name, n.qualified_name AS qualifiedName, n.file_path AS filePath,
|
|
10
|
+
n.start_line AS startLine, n.end_line AS endLine, properties(n) AS properties`;
|
|
11
|
+
var EDGE_QUERY = `MATCH (a)-[r]->(b) RETURN id(r) AS id,
|
|
12
|
+
id(a) AS source, id(b) AS target, type(r) AS type, properties(r) AS properties`;
|
|
13
|
+
function field(row, key) {
|
|
14
|
+
const value = row[key];
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
throw new Error(`Missing graph field: ${key}`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function properties(text) {
|
|
20
|
+
const value = JSON.parse(text);
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
22
|
+
throw new Error("Invalid graph properties");
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function line(value) {
|
|
27
|
+
if (value === "" || value === "null" || value === "0")
|
|
28
|
+
return null;
|
|
29
|
+
const number = Number(value);
|
|
30
|
+
if (!Number.isSafeInteger(number) || number < 0)
|
|
31
|
+
throw new Error(`Invalid line: ${value}`);
|
|
32
|
+
return number;
|
|
33
|
+
}
|
|
34
|
+
async function fileLines(path) {
|
|
35
|
+
let text;
|
|
36
|
+
try {
|
|
37
|
+
text = await readFile(path, "utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (text.length === 0)
|
|
42
|
+
return 0;
|
|
43
|
+
let lines = 0;
|
|
44
|
+
for (let i = 0;i < text.length; i++) {
|
|
45
|
+
if (text[i] === "\r") {
|
|
46
|
+
lines++;
|
|
47
|
+
if (text[i + 1] === `
|
|
48
|
+
`)
|
|
49
|
+
i++;
|
|
50
|
+
} else if (text[i] === `
|
|
51
|
+
`) {
|
|
52
|
+
lines++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return lines + (text.endsWith(`
|
|
56
|
+
`) || text.endsWith("\r") ? 0 : 1);
|
|
57
|
+
}
|
|
58
|
+
async function exportGraph(repository, options = {}, run = runTool) {
|
|
59
|
+
const repositoryPath = await realpath(repository);
|
|
60
|
+
if (!(await stat(repositoryPath)).isDirectory())
|
|
61
|
+
throw new Error("Repository must be a directory");
|
|
62
|
+
const pageSize = options.pageSize ?? 500;
|
|
63
|
+
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 1e4) {
|
|
64
|
+
throw new Error("pageSize must be an integer between 1 and 10000");
|
|
65
|
+
}
|
|
66
|
+
const kind = options.kind ?? "all";
|
|
67
|
+
if (kind !== "all" && kind !== "calls")
|
|
68
|
+
throw new Error("kind must be all or calls");
|
|
69
|
+
const suffix = createHash("sha256").update(repositoryPath).digest("hex").slice(0, 12);
|
|
70
|
+
const name = options.project ?? `${basename(repositoryPath)}-${suffix}`;
|
|
71
|
+
options.onProgress?.(`Indexing ${repositoryPath}`);
|
|
72
|
+
const indexReport = await run("index_repository", { repo_path: repositoryPath, name });
|
|
73
|
+
if (indexReport.status !== "indexed" || typeof indexReport.project !== "string") {
|
|
74
|
+
throw new Error(`Indexing did not complete: ${JSON.stringify(indexReport)}`);
|
|
75
|
+
}
|
|
76
|
+
const project = indexReport.project;
|
|
77
|
+
options.onProgress?.("Exporting nodes");
|
|
78
|
+
const nodeRows = await collectRows(run, project, NODE_QUERY, pageSize);
|
|
79
|
+
const nodes = nodeRows.map((row) => {
|
|
80
|
+
const labels = JSON.parse(field(row, "labels"));
|
|
81
|
+
if (!Array.isArray(labels) || !labels.every((label) => typeof label === "string")) {
|
|
82
|
+
throw new Error("Invalid node labels");
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
id: field(row, "id"),
|
|
86
|
+
labels,
|
|
87
|
+
name: field(row, "name"),
|
|
88
|
+
qualifiedName: field(row, "qualifiedName"),
|
|
89
|
+
filePath: field(row, "filePath"),
|
|
90
|
+
startLine: line(field(row, "startLine")),
|
|
91
|
+
endLine: line(field(row, "endLine")),
|
|
92
|
+
properties: properties(field(row, "properties"))
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
options.onProgress?.("Exporting edges");
|
|
96
|
+
const edgeRows = await collectRows(run, project, EDGE_QUERY, pageSize);
|
|
97
|
+
const edges = edgeRows.map((row) => ({
|
|
98
|
+
id: field(row, "id"),
|
|
99
|
+
source: field(row, "source"),
|
|
100
|
+
target: field(row, "target"),
|
|
101
|
+
type: field(row, "type"),
|
|
102
|
+
properties: properties(field(row, "properties"))
|
|
103
|
+
}));
|
|
104
|
+
if (nodes.length !== indexReport.nodes || edges.length !== indexReport.edges) {
|
|
105
|
+
throw new Error("Export counts differ from the index; the index may have changed or queries were limited");
|
|
106
|
+
}
|
|
107
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
108
|
+
if (ids.size !== nodes.length || new Set(edges.map((edge) => edge.id)).size !== edges.length || edges.some((edge) => !ids.has(edge.source) || !ids.has(edge.target))) {
|
|
109
|
+
throw new Error("Graph contains duplicate IDs or missing edge endpoints");
|
|
110
|
+
}
|
|
111
|
+
options.onProgress?.("Counting file lines");
|
|
112
|
+
const filePaths = new Set(nodes.filter((node) => node.labels.includes("File")).map((node) => node.filePath).filter((path) => path !== "" && path !== "{}" && path !== "null"));
|
|
113
|
+
const files = Object.create(null);
|
|
114
|
+
for (const path of filePaths) {
|
|
115
|
+
files[path] = { lines: await fileLines(resolve(repositoryPath, path)) };
|
|
116
|
+
}
|
|
117
|
+
const selectedEdges = kind === "calls" ? edges.filter((edge) => edge.type === "CALLS") : edges;
|
|
118
|
+
const endpoints = new Set(selectedEdges.flatMap((edge) => [edge.source, edge.target]));
|
|
119
|
+
const selectedNodes = kind === "calls" ? nodes.filter((node) => endpoints.has(node.id) || node.labels.some((label) => label === "Function" || label === "Method")) : nodes;
|
|
120
|
+
options.onProgress?.("Extracting callable signatures");
|
|
121
|
+
addCallables(selectedNodes);
|
|
122
|
+
if (kind === "all") {
|
|
123
|
+
options.onProgress?.("Extracting class members and type dependencies");
|
|
124
|
+
addClassMetadata(repositoryPath, nodes, edges);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
schemaVersion: 1,
|
|
128
|
+
generatedAt: new Date().toISOString(),
|
|
129
|
+
repositoryPath,
|
|
130
|
+
project,
|
|
131
|
+
kind,
|
|
132
|
+
indexReport,
|
|
133
|
+
files,
|
|
134
|
+
nodes: selectedNodes,
|
|
135
|
+
edges: selectedEdges
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
exportGraph
|
|
140
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youichi/repograph",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Export repository code graphs with codebase-memory-mcp.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/hi-youichi/repograph.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"bin": {
|
|
11
|
+
"repograph": "dist/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"exports": "./dist/index.js",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "bun run src/cli.ts",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "bun test",
|
|
18
|
+
"test:integration": "REPOGRAPH_INTEGRATION=1 bun test test/integration.test.ts",
|
|
19
|
+
"build": "bun build ./src/cli.ts ./src/index.ts ./src/cbm.ts ./src/callables.ts ./src/classes.ts --target node --outdir dist --external \"*\"",
|
|
20
|
+
"prepack": "bun run build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"codebase-memory-mcp": "0.12.1",
|
|
24
|
+
"typescript": "^5.9.3"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/bun": "^1.3.6"
|
|
28
|
+
},
|
|
29
|
+
"trustedDependencies": [
|
|
30
|
+
"codebase-memory-mcp"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|