@deeplake/hivemind 0.7.75 → 0.7.77
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/bundle/cli.js +1077 -230
- package/codex/bundle/graph-on-stop.js +3148 -0
- package/codex/bundle/graph-pull-worker.js +40 -1
- package/codex/bundle/pre-tool-use.js +1237 -21
- package/codex/bundle/session-start.js +49 -0
- package/codex/bundle/shell/deeplake-shell.js +725 -20
- package/codex/skills/hivemind-graph/SKILL.md +94 -0
- package/cursor/bundle/graph-on-stop.js +3148 -0
- package/cursor/bundle/graph-pull-worker.js +40 -1
- package/cursor/bundle/pre-tool-use.js +1232 -8
- package/cursor/bundle/session-start.js +263 -7
- package/cursor/bundle/shell/deeplake-shell.js +725 -20
- package/hermes/bundle/graph-on-stop.js +3148 -0
- package/hermes/bundle/graph-pull-worker.js +40 -1
- package/hermes/bundle/pre-tool-use.js +1225 -8
- package/hermes/bundle/session-start.js +262 -8
- package/hermes/bundle/shell/deeplake-shell.js +725 -20
- package/openclaw/dist/index.js +28 -1
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/package.json +1 -1
- package/package.json +2 -1
- package/scripts/ensure-tree-sitter.mjs +6 -1
|
@@ -53,13 +53,13 @@ var init_index_marker_store = __esm({
|
|
|
53
53
|
|
|
54
54
|
// dist/src/utils/stdin.js
|
|
55
55
|
function readStdin() {
|
|
56
|
-
return new Promise((
|
|
56
|
+
return new Promise((resolve3, reject) => {
|
|
57
57
|
let data = "";
|
|
58
58
|
process.stdin.setEncoding("utf-8");
|
|
59
59
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
60
60
|
process.stdin.on("end", () => {
|
|
61
61
|
try {
|
|
62
|
-
|
|
62
|
+
resolve3(JSON.parse(data));
|
|
63
63
|
} catch (err) {
|
|
64
64
|
reject(new Error(`Failed to parse hook input: ${err}`));
|
|
65
65
|
}
|
|
@@ -521,7 +521,7 @@ function getQueryTimeoutMs() {
|
|
|
521
521
|
return Number(process.env.HIVEMIND_QUERY_TIMEOUT_MS ?? 1e4);
|
|
522
522
|
}
|
|
523
523
|
function sleep2(ms) {
|
|
524
|
-
return new Promise((
|
|
524
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
525
525
|
}
|
|
526
526
|
function isTimeoutError(error) {
|
|
527
527
|
const name = error instanceof Error ? error.name.toLowerCase() : "";
|
|
@@ -551,7 +551,7 @@ var Semaphore = class {
|
|
|
551
551
|
this.active++;
|
|
552
552
|
return;
|
|
553
553
|
}
|
|
554
|
-
await new Promise((
|
|
554
|
+
await new Promise((resolve3) => this.waiting.push(resolve3));
|
|
555
555
|
}
|
|
556
556
|
release() {
|
|
557
557
|
this.active--;
|
|
@@ -1709,7 +1709,7 @@ var EmbedClient = class {
|
|
|
1709
1709
|
}
|
|
1710
1710
|
}
|
|
1711
1711
|
connectOnce() {
|
|
1712
|
-
return new Promise((
|
|
1712
|
+
return new Promise((resolve3, reject) => {
|
|
1713
1713
|
const sock = connect(this.socketPath);
|
|
1714
1714
|
const to = setTimeout(() => {
|
|
1715
1715
|
sock.destroy();
|
|
@@ -1717,7 +1717,7 @@ var EmbedClient = class {
|
|
|
1717
1717
|
}, this.timeoutMs);
|
|
1718
1718
|
sock.once("connect", () => {
|
|
1719
1719
|
clearTimeout(to);
|
|
1720
|
-
|
|
1720
|
+
resolve3(sock);
|
|
1721
1721
|
});
|
|
1722
1722
|
sock.once("error", (e) => {
|
|
1723
1723
|
clearTimeout(to);
|
|
@@ -1799,7 +1799,7 @@ var EmbedClient = class {
|
|
|
1799
1799
|
throw new Error("daemon did not become ready within spawnWaitMs");
|
|
1800
1800
|
}
|
|
1801
1801
|
sendAndWait(sock, req) {
|
|
1802
|
-
return new Promise((
|
|
1802
|
+
return new Promise((resolve3, reject) => {
|
|
1803
1803
|
let buf = "";
|
|
1804
1804
|
const to = setTimeout(() => {
|
|
1805
1805
|
sock.destroy();
|
|
@@ -1814,7 +1814,7 @@ var EmbedClient = class {
|
|
|
1814
1814
|
const line = buf.slice(0, nl);
|
|
1815
1815
|
clearTimeout(to);
|
|
1816
1816
|
try {
|
|
1817
|
-
|
|
1817
|
+
resolve3(JSON.parse(line));
|
|
1818
1818
|
} catch (e) {
|
|
1819
1819
|
reject(e);
|
|
1820
1820
|
}
|
|
@@ -2335,6 +2335,1217 @@ function rewritePaths(cmd) {
|
|
|
2335
2335
|
return cmd.replace(new RegExp(escapeRe(MEMORY_PATH) + tail, "g"), "/").replace(new RegExp(escapeRe(TILDE_PATH) + tail, "g"), "/").replace(new RegExp('"' + escapeRe(HOME_VAR_PATH) + tail + '"', "g"), '"/"').replace(new RegExp(escapeRe(HOME_VAR_PATH) + tail, "g"), "/");
|
|
2336
2336
|
}
|
|
2337
2337
|
|
|
2338
|
+
// dist/src/graph/vfs-handler.js
|
|
2339
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync9, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2340
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2341
|
+
import { join as join14, dirname as dirname6 } from "node:path";
|
|
2342
|
+
|
|
2343
|
+
// dist/src/graph/last-build.js
|
|
2344
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync7, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2345
|
+
import { dirname as dirname3, join as join11 } from "node:path";
|
|
2346
|
+
function lastBuildPath(baseDir, worktreeId) {
|
|
2347
|
+
if (worktreeId !== void 0) {
|
|
2348
|
+
return join11(baseDir, "worktrees", worktreeId, ".last-build.json");
|
|
2349
|
+
}
|
|
2350
|
+
return join11(baseDir, ".last-build.json");
|
|
2351
|
+
}
|
|
2352
|
+
function readLastBuild(baseDir, worktreeId) {
|
|
2353
|
+
let path = lastBuildPath(baseDir, worktreeId);
|
|
2354
|
+
if (!existsSync5(path)) {
|
|
2355
|
+
if (worktreeId === void 0)
|
|
2356
|
+
return null;
|
|
2357
|
+
const legacy = lastBuildPath(baseDir, void 0);
|
|
2358
|
+
if (!existsSync5(legacy))
|
|
2359
|
+
return null;
|
|
2360
|
+
path = legacy;
|
|
2361
|
+
}
|
|
2362
|
+
let raw;
|
|
2363
|
+
try {
|
|
2364
|
+
raw = readFileSync7(path, "utf8");
|
|
2365
|
+
} catch {
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
2368
|
+
let parsed;
|
|
2369
|
+
try {
|
|
2370
|
+
parsed = JSON.parse(raw);
|
|
2371
|
+
} catch {
|
|
2372
|
+
return null;
|
|
2373
|
+
}
|
|
2374
|
+
if (parsed === null || typeof parsed !== "object")
|
|
2375
|
+
return null;
|
|
2376
|
+
const o = parsed;
|
|
2377
|
+
if (typeof o.ts !== "number" || !Number.isFinite(o.ts))
|
|
2378
|
+
return null;
|
|
2379
|
+
if (o.commit_sha !== null && typeof o.commit_sha !== "string")
|
|
2380
|
+
return null;
|
|
2381
|
+
if (typeof o.snapshot_sha256 !== "string")
|
|
2382
|
+
return null;
|
|
2383
|
+
const out = { ts: o.ts, commit_sha: o.commit_sha, snapshot_sha256: o.snapshot_sha256 };
|
|
2384
|
+
if (typeof o.node_count === "number" && Number.isFinite(o.node_count) && o.node_count >= 0) {
|
|
2385
|
+
out.node_count = o.node_count;
|
|
2386
|
+
}
|
|
2387
|
+
if (typeof o.edge_count === "number" && Number.isFinite(o.edge_count) && o.edge_count >= 0) {
|
|
2388
|
+
out.edge_count = o.edge_count;
|
|
2389
|
+
}
|
|
2390
|
+
return out;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// dist/src/graph/snapshot.js
|
|
2394
|
+
import { createHash } from "node:crypto";
|
|
2395
|
+
import { mkdirSync as mkdirSync7, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2396
|
+
import { homedir as homedir9 } from "node:os";
|
|
2397
|
+
import { dirname as dirname5, join as join13 } from "node:path";
|
|
2398
|
+
|
|
2399
|
+
// dist/src/graph/history.js
|
|
2400
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "node:fs";
|
|
2401
|
+
import { dirname as dirname4, join as join12 } from "node:path";
|
|
2402
|
+
|
|
2403
|
+
// dist/src/graph/resolve/cross-file.js
|
|
2404
|
+
import { posix } from "node:path";
|
|
2405
|
+
|
|
2406
|
+
// dist/src/graph/snapshot.js
|
|
2407
|
+
function graphsRoot() {
|
|
2408
|
+
return process.env.HIVEMIND_GRAPHS_HOME ?? join13(homedir9(), ".hivemind", "graphs");
|
|
2409
|
+
}
|
|
2410
|
+
function repoDir(repoKey) {
|
|
2411
|
+
return join13(graphsRoot(), repoKey);
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// dist/src/utils/repo-identity.js
|
|
2415
|
+
import { execSync } from "node:child_process";
|
|
2416
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2417
|
+
import { basename, resolve as resolve2 } from "node:path";
|
|
2418
|
+
var DEFAULT_PORTS = {
|
|
2419
|
+
http: "80",
|
|
2420
|
+
https: "443",
|
|
2421
|
+
ssh: "22",
|
|
2422
|
+
git: "9418"
|
|
2423
|
+
};
|
|
2424
|
+
function normalizeGitRemoteUrl(url) {
|
|
2425
|
+
let s = url.trim();
|
|
2426
|
+
const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i);
|
|
2427
|
+
const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
|
|
2428
|
+
if (schemeMatch)
|
|
2429
|
+
s = s.slice(schemeMatch[0].length);
|
|
2430
|
+
if (!scheme) {
|
|
2431
|
+
const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
|
|
2432
|
+
if (scp)
|
|
2433
|
+
s = `${scp[1]}/${scp[2]}`;
|
|
2434
|
+
}
|
|
2435
|
+
s = s.replace(/^[^@/]+@/, "");
|
|
2436
|
+
if (scheme && DEFAULT_PORTS[scheme]) {
|
|
2437
|
+
s = s.replace(new RegExp(`^([^/]+):${DEFAULT_PORTS[scheme]}(/|$)`), "$1$2");
|
|
2438
|
+
}
|
|
2439
|
+
s = s.replace(/\.git\/?$/i, "");
|
|
2440
|
+
s = s.replace(/\/+$/, "");
|
|
2441
|
+
return s.toLowerCase();
|
|
2442
|
+
}
|
|
2443
|
+
function deriveProjectKey(cwd) {
|
|
2444
|
+
const absCwd = resolve2(cwd);
|
|
2445
|
+
const project = basename(absCwd) || "unknown";
|
|
2446
|
+
let signature = null;
|
|
2447
|
+
try {
|
|
2448
|
+
const raw = execSync("git config --get remote.origin.url", {
|
|
2449
|
+
cwd: absCwd,
|
|
2450
|
+
encoding: "utf-8",
|
|
2451
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2452
|
+
}).trim();
|
|
2453
|
+
signature = raw ? normalizeGitRemoteUrl(raw) : null;
|
|
2454
|
+
} catch {
|
|
2455
|
+
}
|
|
2456
|
+
const input = signature ?? absCwd;
|
|
2457
|
+
const key = createHash2("sha1").update(input).digest("hex").slice(0, 16);
|
|
2458
|
+
return { key, project };
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// dist/src/graph/render/neighborhood.js
|
|
2462
|
+
var CAP = 25;
|
|
2463
|
+
function renderNeighborhood(snap, file) {
|
|
2464
|
+
const allFiles = [...new Set(snap.nodes.map((n) => n.source_file))];
|
|
2465
|
+
let resolved = null;
|
|
2466
|
+
if (allFiles.includes(file)) {
|
|
2467
|
+
resolved = file;
|
|
2468
|
+
} else {
|
|
2469
|
+
const matches = allFiles.filter((f) => f.endsWith(file) || f.includes(file));
|
|
2470
|
+
if (matches.length === 1) {
|
|
2471
|
+
resolved = matches[0];
|
|
2472
|
+
} else if (matches.length > 1) {
|
|
2473
|
+
const lines2 = [];
|
|
2474
|
+
lines2.push(`"${file}" matches multiple files \u2014 which did you mean?`);
|
|
2475
|
+
lines2.push("");
|
|
2476
|
+
for (const m of matches.slice(0, 10))
|
|
2477
|
+
lines2.push(` ${m}`);
|
|
2478
|
+
if (matches.length > 10)
|
|
2479
|
+
lines2.push(` ... and ${matches.length - 10} more`);
|
|
2480
|
+
return lines2.join("\n");
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
if (resolved === null) {
|
|
2484
|
+
const lines2 = [];
|
|
2485
|
+
lines2.push(`No nodes for "${file}".`);
|
|
2486
|
+
const parts = file.split("/").filter((p) => p.length > 2);
|
|
2487
|
+
const close = allFiles.filter((f) => parts.some((p) => f.includes(p))).slice(0, 3);
|
|
2488
|
+
if (close.length > 0) {
|
|
2489
|
+
lines2.push("Did you mean:");
|
|
2490
|
+
for (const c of close)
|
|
2491
|
+
lines2.push(` ${c}`);
|
|
2492
|
+
}
|
|
2493
|
+
return lines2.join("\n");
|
|
2494
|
+
}
|
|
2495
|
+
const fileNodes = snap.nodes.filter((n) => n.source_file === resolved);
|
|
2496
|
+
const fileNodeIds = new Set(fileNodes.map((n) => n.id));
|
|
2497
|
+
const fileOf = /* @__PURE__ */ new Map();
|
|
2498
|
+
for (const n of snap.nodes)
|
|
2499
|
+
fileOf.set(n.id, n.source_file);
|
|
2500
|
+
const sorted = [...fileNodes].sort((a, b) => {
|
|
2501
|
+
const la = parseLocation(a.source_location);
|
|
2502
|
+
const lb = parseLocation(b.source_location);
|
|
2503
|
+
if (la !== lb)
|
|
2504
|
+
return la - lb;
|
|
2505
|
+
return a.label.localeCompare(b.label);
|
|
2506
|
+
});
|
|
2507
|
+
const lines = [];
|
|
2508
|
+
lines.push(`## Symbols in ${resolved}`);
|
|
2509
|
+
lines.push("");
|
|
2510
|
+
if (sorted.length === 0) {
|
|
2511
|
+
lines.push(" (no symbols)");
|
|
2512
|
+
} else {
|
|
2513
|
+
for (const n of sorted) {
|
|
2514
|
+
const exp = n.exported ? "exported" : "internal";
|
|
2515
|
+
lines.push(` ${n.label.padEnd(32)} ${n.kind.padEnd(12)} ${exp.padEnd(10)} ${n.source_location}`);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
lines.push("");
|
|
2519
|
+
lines.push("## Cross-file neighbors");
|
|
2520
|
+
lines.push("");
|
|
2521
|
+
lines.push("Note: 'calls' edges are intra-file only in the current extractor \u2014 cross-file");
|
|
2522
|
+
lines.push("neighbors here are driven mainly by 'imports' edges.");
|
|
2523
|
+
lines.push("");
|
|
2524
|
+
const outgoing = [];
|
|
2525
|
+
const incoming = [];
|
|
2526
|
+
for (const e of snap.links) {
|
|
2527
|
+
const srcIn = fileNodeIds.has(e.source);
|
|
2528
|
+
const tgtIn = fileNodeIds.has(e.target);
|
|
2529
|
+
if (srcIn === tgtIn)
|
|
2530
|
+
continue;
|
|
2531
|
+
if (srcIn) {
|
|
2532
|
+
const tgtFile = fileOf.get(e.target);
|
|
2533
|
+
if (tgtFile !== void 0 && tgtFile !== resolved)
|
|
2534
|
+
outgoing.push(e);
|
|
2535
|
+
} else {
|
|
2536
|
+
const srcFile = fileOf.get(e.source);
|
|
2537
|
+
if (srcFile !== void 0 && srcFile !== resolved)
|
|
2538
|
+
incoming.push(e);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
renderDirectionGroup(lines, outgoing, "Outgoing", "source");
|
|
2542
|
+
renderDirectionGroup(lines, incoming, "Incoming", "target");
|
|
2543
|
+
return lines.join("\n");
|
|
2544
|
+
}
|
|
2545
|
+
function renderDirectionGroup(lines, edges, label, selfField) {
|
|
2546
|
+
const otherField = selfField === "source" ? "target" : "source";
|
|
2547
|
+
const byRelation = /* @__PURE__ */ new Map();
|
|
2548
|
+
for (const e of edges) {
|
|
2549
|
+
const otherId = e[otherField];
|
|
2550
|
+
const rel = e.relation;
|
|
2551
|
+
let nodeMap = byRelation.get(rel);
|
|
2552
|
+
if (!nodeMap) {
|
|
2553
|
+
nodeMap = /* @__PURE__ */ new Map();
|
|
2554
|
+
byRelation.set(rel, nodeMap);
|
|
2555
|
+
}
|
|
2556
|
+
nodeMap.set(otherId, (nodeMap.get(otherId) ?? 0) + 1);
|
|
2557
|
+
}
|
|
2558
|
+
if (byRelation.size === 0) {
|
|
2559
|
+
lines.push(`${label}: (none)`);
|
|
2560
|
+
lines.push("");
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
lines.push(`${label}:`);
|
|
2564
|
+
let totalShown = 0;
|
|
2565
|
+
const sortedRels = [...byRelation.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
2566
|
+
for (const [rel, nodeMap] of sortedRels) {
|
|
2567
|
+
const entries = [...nodeMap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
2568
|
+
lines.push(` ${rel} (${entries.length}):`);
|
|
2569
|
+
let shownInRel = 0;
|
|
2570
|
+
for (const [otherId, cnt] of entries) {
|
|
2571
|
+
if (totalShown >= CAP)
|
|
2572
|
+
break;
|
|
2573
|
+
const suffix = cnt > 1 ? ` \xD7${cnt}` : "";
|
|
2574
|
+
lines.push(` ${otherId}${suffix}`);
|
|
2575
|
+
shownInRel++;
|
|
2576
|
+
totalShown++;
|
|
2577
|
+
}
|
|
2578
|
+
const remaining = entries.length - shownInRel;
|
|
2579
|
+
if (remaining > 0)
|
|
2580
|
+
lines.push(` ... and ${remaining} more`);
|
|
2581
|
+
}
|
|
2582
|
+
if (totalShown >= CAP) {
|
|
2583
|
+
const total = [...byRelation.values()].reduce((s, m) => s + m.size, 0);
|
|
2584
|
+
if (total > CAP)
|
|
2585
|
+
lines.push(` ... and ${total - CAP} more`);
|
|
2586
|
+
}
|
|
2587
|
+
lines.push("");
|
|
2588
|
+
}
|
|
2589
|
+
function parseLocation(loc) {
|
|
2590
|
+
const m = loc.match(/^L(\d+)/);
|
|
2591
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// dist/src/graph/render/layers.js
|
|
2595
|
+
var LAYER_RULES = [
|
|
2596
|
+
{ layer: "Tests", test: (p) => p.includes("/tests/") || p.includes(".test.") || p.includes("/__tests__/") },
|
|
2597
|
+
{ layer: "Hooks", test: (p) => p.includes("/hooks/") },
|
|
2598
|
+
{ layer: "CLI", test: (p) => p.includes("/cli/") || p.includes("/commands/") },
|
|
2599
|
+
{ layer: "Graph", test: (p) => p.includes("/graph/") },
|
|
2600
|
+
{ layer: "Shell/VFS", test: (p) => p.includes("/shell/") },
|
|
2601
|
+
{ layer: "Embeddings", test: (p) => p.includes("/embeddings/") },
|
|
2602
|
+
{ layer: "Skillify", test: (p) => p.includes("/skillify/") },
|
|
2603
|
+
{ layer: "Config", test: (p) => /(?:^|\/)config\.[^/]+$/.test(p) || /\.config\.[^/]+$/.test(p) },
|
|
2604
|
+
{ layer: "Utils", test: (p) => p.includes("/utils/") }
|
|
2605
|
+
];
|
|
2606
|
+
function layerOf(sourceFile) {
|
|
2607
|
+
const p = sourceFile.startsWith("/") ? sourceFile : `/${sourceFile}`;
|
|
2608
|
+
for (const rule of LAYER_RULES) {
|
|
2609
|
+
if (rule.test(p))
|
|
2610
|
+
return rule.layer;
|
|
2611
|
+
}
|
|
2612
|
+
return "Core";
|
|
2613
|
+
}
|
|
2614
|
+
function renderLayers(snap) {
|
|
2615
|
+
try {
|
|
2616
|
+
const layerNodes = /* @__PURE__ */ new Map();
|
|
2617
|
+
const layerFiles = /* @__PURE__ */ new Map();
|
|
2618
|
+
for (const node of snap.nodes) {
|
|
2619
|
+
const layer = layerOf(node.source_file);
|
|
2620
|
+
layerNodes.set(layer, (layerNodes.get(layer) ?? 0) + 1);
|
|
2621
|
+
let fileMap = layerFiles.get(layer);
|
|
2622
|
+
if (!fileMap) {
|
|
2623
|
+
fileMap = /* @__PURE__ */ new Map();
|
|
2624
|
+
layerFiles.set(layer, fileMap);
|
|
2625
|
+
}
|
|
2626
|
+
fileMap.set(node.source_file, (fileMap.get(node.source_file) ?? 0) + 1);
|
|
2627
|
+
}
|
|
2628
|
+
if (layerNodes.size === 0) {
|
|
2629
|
+
return "No nodes in snapshot \u2014 nothing to layer.";
|
|
2630
|
+
}
|
|
2631
|
+
const sorted = [...layerNodes.entries()].sort(([, a], [, b]) => b - a);
|
|
2632
|
+
const lines = [];
|
|
2633
|
+
lines.push("## Architectural Layers");
|
|
2634
|
+
lines.push("");
|
|
2635
|
+
for (const [layer, count] of sorted) {
|
|
2636
|
+
lines.push(`${layer.padEnd(14)} ${String(count).padStart(4)} node${count === 1 ? "" : "s"}`);
|
|
2637
|
+
const fileMap = layerFiles.get(layer);
|
|
2638
|
+
const topFiles = [...fileMap.entries()].sort(([, a], [, b]) => b - a).slice(0, 5);
|
|
2639
|
+
for (const [file, n] of topFiles) {
|
|
2640
|
+
lines.push(` ${String(n).padStart(3)} ${file}`);
|
|
2641
|
+
}
|
|
2642
|
+
if (fileMap.size > 5) {
|
|
2643
|
+
lines.push(` ... and ${fileMap.size - 5} more file${fileMap.size - 5 === 1 ? "" : "s"}`);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
lines.push("");
|
|
2647
|
+
lines.push(`Total: ${snap.nodes.length} node${snap.nodes.length === 1 ? "" : "s"} across ${sorted.length} layer${sorted.length === 1 ? "" : "s"}`);
|
|
2648
|
+
return lines.join("\n");
|
|
2649
|
+
} catch {
|
|
2650
|
+
return "Failed to render layer view.";
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
// dist/src/graph/render/tour.js
|
|
2655
|
+
var LINE_CAP = 60;
|
|
2656
|
+
function renderTour(snap) {
|
|
2657
|
+
if (snap.nodes.length === 0) {
|
|
2658
|
+
return "Graph is empty \u2014 no nodes to tour.";
|
|
2659
|
+
}
|
|
2660
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
2661
|
+
for (const n of snap.nodes)
|
|
2662
|
+
nodeMap.set(n.id, n);
|
|
2663
|
+
const inDegOrig = /* @__PURE__ */ new Map();
|
|
2664
|
+
for (const n of snap.nodes)
|
|
2665
|
+
inDegOrig.set(n.id, 0);
|
|
2666
|
+
for (const e of snap.links) {
|
|
2667
|
+
if (nodeMap.has(e.source) && nodeMap.has(e.target)) {
|
|
2668
|
+
inDegOrig.set(e.target, (inDegOrig.get(e.target) ?? 0) + 1);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
const entryPoints = snap.nodes.filter((n) => n.exported && inDegOrig.get(n.id) === 0).sort((a, b) => a.id.localeCompare(b.id));
|
|
2672
|
+
const entrySet = new Set(entryPoints.map((n) => n.id));
|
|
2673
|
+
const revAdj = /* @__PURE__ */ new Map();
|
|
2674
|
+
const inDegRev = /* @__PURE__ */ new Map();
|
|
2675
|
+
for (const n of snap.nodes) {
|
|
2676
|
+
revAdj.set(n.id, []);
|
|
2677
|
+
inDegRev.set(n.id, 0);
|
|
2678
|
+
}
|
|
2679
|
+
for (const e of snap.links) {
|
|
2680
|
+
if (!nodeMap.has(e.source) || !nodeMap.has(e.target))
|
|
2681
|
+
continue;
|
|
2682
|
+
revAdj.get(e.target).push(e.source);
|
|
2683
|
+
inDegRev.set(e.source, (inDegRev.get(e.source) ?? 0) + 1);
|
|
2684
|
+
}
|
|
2685
|
+
const queue = [];
|
|
2686
|
+
for (const n of snap.nodes) {
|
|
2687
|
+
if (inDegRev.get(n.id) === 0)
|
|
2688
|
+
queue.push(n.id);
|
|
2689
|
+
}
|
|
2690
|
+
queue.sort();
|
|
2691
|
+
const topoOrder = [];
|
|
2692
|
+
while (queue.length > 0) {
|
|
2693
|
+
const id = queue.shift();
|
|
2694
|
+
topoOrder.push(id);
|
|
2695
|
+
const newReady = [];
|
|
2696
|
+
for (const dep of revAdj.get(id) ?? []) {
|
|
2697
|
+
const d = (inDegRev.get(dep) ?? 0) - 1;
|
|
2698
|
+
inDegRev.set(dep, d);
|
|
2699
|
+
if (d === 0)
|
|
2700
|
+
newReady.push(dep);
|
|
2701
|
+
}
|
|
2702
|
+
if (newReady.length > 0) {
|
|
2703
|
+
for (const x of newReady)
|
|
2704
|
+
queue.push(x);
|
|
2705
|
+
queue.sort();
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
const topoSet = new Set(topoOrder);
|
|
2709
|
+
const cyclic = snap.nodes.filter((n) => !topoSet.has(n.id)).sort((a, b) => a.id.localeCompare(b.id));
|
|
2710
|
+
const walkthrough = topoOrder.filter((id) => !entrySet.has(id));
|
|
2711
|
+
const totalNodes = snap.nodes.length;
|
|
2712
|
+
const lines = [];
|
|
2713
|
+
lines.push(`# Code Graph Tour \u2014 ${totalNodes} node${totalNodes !== 1 ? "s" : ""}`);
|
|
2714
|
+
lines.push("");
|
|
2715
|
+
lines.push(`## Entry points (${entryPoints.length})`);
|
|
2716
|
+
if (entryPoints.length === 0) {
|
|
2717
|
+
lines.push(" (none \u2014 all exported nodes have at least one incoming edge)");
|
|
2718
|
+
} else {
|
|
2719
|
+
lines.push(" Exported symbols with no incoming edges \u2014 likely top-level public API.");
|
|
2720
|
+
lines.push("");
|
|
2721
|
+
for (let i = 0; i < entryPoints.length; i++) {
|
|
2722
|
+
if (lines.length >= LINE_CAP) {
|
|
2723
|
+
lines.push(` ... and ${entryPoints.length - i} more`);
|
|
2724
|
+
break;
|
|
2725
|
+
}
|
|
2726
|
+
lines.push(` ${i + 1}. ${entryPoints[i].id} [${entryPoints[i].kind}]`);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
lines.push("");
|
|
2730
|
+
lines.push(`## Walkthrough \u2014 dependency order (${walkthrough.length})`);
|
|
2731
|
+
if (walkthrough.length === 0) {
|
|
2732
|
+
lines.push(" (all non-entry nodes are cyclic)");
|
|
2733
|
+
} else {
|
|
2734
|
+
lines.push(" Dependencies before dependents (bottom-up).");
|
|
2735
|
+
lines.push("");
|
|
2736
|
+
for (let i = 0; i < walkthrough.length; i++) {
|
|
2737
|
+
if (lines.length >= LINE_CAP) {
|
|
2738
|
+
lines.push(` ... and ${walkthrough.length - i} more`);
|
|
2739
|
+
break;
|
|
2740
|
+
}
|
|
2741
|
+
const n = nodeMap.get(walkthrough[i]);
|
|
2742
|
+
lines.push(` ${i + 1}. ${n.id} [${n.kind}]`);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
lines.push("");
|
|
2746
|
+
if (cyclic.length > 0) {
|
|
2747
|
+
lines.push(`## Cyclic / remaining (${cyclic.length})`);
|
|
2748
|
+
lines.push(" These nodes form cycles and were not reached by topological sort.");
|
|
2749
|
+
lines.push("");
|
|
2750
|
+
for (let i = 0; i < cyclic.length; i++) {
|
|
2751
|
+
if (lines.length >= LINE_CAP) {
|
|
2752
|
+
lines.push(` ... and ${cyclic.length - i} more`);
|
|
2753
|
+
break;
|
|
2754
|
+
}
|
|
2755
|
+
lines.push(` ${i + 1}. ${cyclic[i].id} [${cyclic[i].kind}]`);
|
|
2756
|
+
}
|
|
2757
|
+
lines.push("");
|
|
2758
|
+
}
|
|
2759
|
+
lines.push(`Total: ${entryPoints.length} entry + ${walkthrough.length} walkthrough` + (cyclic.length > 0 ? ` + ${cyclic.length} cyclic` : "") + ` = ${totalNodes} nodes`);
|
|
2760
|
+
return lines.join("\n");
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
// dist/src/graph/render/path.js
|
|
2764
|
+
function resolvePattern(snap, pattern) {
|
|
2765
|
+
const needle = pattern.toLowerCase();
|
|
2766
|
+
return snap.nodes.filter((n) => n.id.toLowerCase().includes(needle) || n.label.toLowerCase().includes(needle)).map((n) => n.id).sort();
|
|
2767
|
+
}
|
|
2768
|
+
function buildAdjacency(snap, undirected) {
|
|
2769
|
+
const adj = /* @__PURE__ */ new Map();
|
|
2770
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
2771
|
+
for (const n of snap.nodes) {
|
|
2772
|
+
adj.set(n.id, []);
|
|
2773
|
+
nodeIds.add(n.id);
|
|
2774
|
+
}
|
|
2775
|
+
for (const edge of snap.links) {
|
|
2776
|
+
if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target))
|
|
2777
|
+
continue;
|
|
2778
|
+
adj.get(edge.source).push({ neighborId: edge.target, edge, reversed: false });
|
|
2779
|
+
if (undirected) {
|
|
2780
|
+
adj.get(edge.target).push({ neighborId: edge.source, edge, reversed: true });
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
for (const neighbors of adj.values()) {
|
|
2784
|
+
neighbors.sort((a, b) => a.neighborId.localeCompare(b.neighborId) || a.edge.relation.localeCompare(b.edge.relation) || (a.reversed === b.reversed ? 0 : a.reversed ? 1 : -1));
|
|
2785
|
+
}
|
|
2786
|
+
return adj;
|
|
2787
|
+
}
|
|
2788
|
+
function bfs(adj, fromId, toId) {
|
|
2789
|
+
if (fromId === toId)
|
|
2790
|
+
return [];
|
|
2791
|
+
const parent = /* @__PURE__ */ new Map();
|
|
2792
|
+
const visited = /* @__PURE__ */ new Set([fromId]);
|
|
2793
|
+
const queue = [fromId];
|
|
2794
|
+
while (queue.length > 0) {
|
|
2795
|
+
const current = queue.shift();
|
|
2796
|
+
for (const { neighborId, edge, reversed } of adj.get(current) ?? []) {
|
|
2797
|
+
if (visited.has(neighborId))
|
|
2798
|
+
continue;
|
|
2799
|
+
visited.add(neighborId);
|
|
2800
|
+
parent.set(neighborId, { parentId: current, hop: { edge, reversed } });
|
|
2801
|
+
if (neighborId === toId) {
|
|
2802
|
+
const hops = [];
|
|
2803
|
+
let cur = toId;
|
|
2804
|
+
while (cur !== fromId) {
|
|
2805
|
+
const p = parent.get(cur);
|
|
2806
|
+
hops.unshift(p.hop);
|
|
2807
|
+
cur = p.parentId;
|
|
2808
|
+
}
|
|
2809
|
+
return hops;
|
|
2810
|
+
}
|
|
2811
|
+
queue.push(neighborId);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
return null;
|
|
2815
|
+
}
|
|
2816
|
+
function renderHops(fromId, hops, undirected) {
|
|
2817
|
+
const lines = [];
|
|
2818
|
+
lines.push(`${undirected ? "Undirected path" : "Directed path"} (${hops.length} hop${hops.length === 1 ? "" : "s"}):`);
|
|
2819
|
+
lines.push("");
|
|
2820
|
+
lines.push(` ${fromId}`);
|
|
2821
|
+
for (const { edge, reversed } of hops) {
|
|
2822
|
+
if (reversed) {
|
|
2823
|
+
lines.push(` <--${edge.relation}-- ${edge.source} [real edge: ${edge.source} \u2192 ${edge.target}]`);
|
|
2824
|
+
} else {
|
|
2825
|
+
lines.push(` --${edge.relation}--> ${edge.target}`);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
if (undirected) {
|
|
2829
|
+
lines.push("");
|
|
2830
|
+
lines.push("Note: no directed path exists. Arrows with <-- are traversed against their declared direction.");
|
|
2831
|
+
}
|
|
2832
|
+
return lines.join("\n");
|
|
2833
|
+
}
|
|
2834
|
+
function candidateList(pattern, ids) {
|
|
2835
|
+
const lines = [`"${pattern}" matches ${ids.length} nodes \u2014 be more specific:`];
|
|
2836
|
+
lines.push("");
|
|
2837
|
+
const shown = ids.slice(0, 20);
|
|
2838
|
+
for (let i = 0; i < shown.length; i++)
|
|
2839
|
+
lines.push(` [${i + 1}] ${shown[i]}`);
|
|
2840
|
+
if (ids.length > 20)
|
|
2841
|
+
lines.push(` ... and ${ids.length - 20} more`);
|
|
2842
|
+
return lines.join("\n");
|
|
2843
|
+
}
|
|
2844
|
+
function renderPath(snap, fromPattern, toPattern) {
|
|
2845
|
+
const fromIds = resolvePattern(snap, fromPattern);
|
|
2846
|
+
const toIds = resolvePattern(snap, toPattern);
|
|
2847
|
+
if (fromIds.length === 0) {
|
|
2848
|
+
return `No node matches "${fromPattern}". Try cat memory/graph/find/<pattern> to explore.`;
|
|
2849
|
+
}
|
|
2850
|
+
if (toIds.length === 0) {
|
|
2851
|
+
return `No node matches "${toPattern}". Try cat memory/graph/find/<pattern> to explore.`;
|
|
2852
|
+
}
|
|
2853
|
+
if (fromIds.length > 1)
|
|
2854
|
+
return candidateList(fromPattern, fromIds);
|
|
2855
|
+
if (toIds.length > 1)
|
|
2856
|
+
return candidateList(toPattern, toIds);
|
|
2857
|
+
const fromId = fromIds[0];
|
|
2858
|
+
const toId = toIds[0];
|
|
2859
|
+
if (fromId === toId) {
|
|
2860
|
+
return `"${fromId}" is the same node on both ends \u2014 path length 0.`;
|
|
2861
|
+
}
|
|
2862
|
+
const dirPath = bfs(buildAdjacency(snap, false), fromId, toId);
|
|
2863
|
+
if (dirPath !== null)
|
|
2864
|
+
return renderHops(fromId, dirPath, false);
|
|
2865
|
+
const undirPath = bfs(buildAdjacency(snap, true), fromId, toId);
|
|
2866
|
+
if (undirPath !== null)
|
|
2867
|
+
return renderHops(fromId, undirPath, true);
|
|
2868
|
+
const fromNode = snap.nodes.find((n) => n.id === fromId);
|
|
2869
|
+
const toNode = snap.nodes.find((n) => n.id === toId);
|
|
2870
|
+
const sameFile = fromNode && toNode && fromNode.source_file === toNode.source_file;
|
|
2871
|
+
const context = sameFile ? `Both are in ${fromNode.source_file} \u2014 same file but no connecting edges.` : `Sources: ${fromNode?.source_file ?? "?"} vs ${toNode?.source_file ?? "?"} \u2014 they appear disconnected.`;
|
|
2872
|
+
return [`No path found between:`, ` from: ${fromId}`, ` to: ${toId}`, ``, context].join("\n");
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
// dist/src/graph/render/impact.js
|
|
2876
|
+
var IMPACT_CAP = 80;
|
|
2877
|
+
var MAX_DEPTH = 25;
|
|
2878
|
+
function renderImpact(snap, pattern) {
|
|
2879
|
+
const needle = pattern.toLowerCase();
|
|
2880
|
+
const matches = snap.nodes.filter((n) => n.id.toLowerCase().includes(needle));
|
|
2881
|
+
if (matches.length === 0) {
|
|
2882
|
+
return `No node matches "${pattern}". Try cat memory/graph/find/${pattern} to explore.`;
|
|
2883
|
+
}
|
|
2884
|
+
if (matches.length > 1) {
|
|
2885
|
+
const lines2 = [`"${pattern}" matches ${matches.length} nodes \u2014 be more specific:`, ""];
|
|
2886
|
+
for (const m of matches.slice(0, 20))
|
|
2887
|
+
lines2.push(` ${m.id}`);
|
|
2888
|
+
if (matches.length > 20)
|
|
2889
|
+
lines2.push(` ... and ${matches.length - 20} more`);
|
|
2890
|
+
return lines2.join("\n");
|
|
2891
|
+
}
|
|
2892
|
+
const target = matches[0];
|
|
2893
|
+
const nodeIds = new Set(snap.nodes.map((n) => n.id));
|
|
2894
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
2895
|
+
for (const e of snap.links) {
|
|
2896
|
+
if (!nodeIds.has(e.source))
|
|
2897
|
+
continue;
|
|
2898
|
+
const list = incoming.get(e.target);
|
|
2899
|
+
if (list)
|
|
2900
|
+
list.push(e);
|
|
2901
|
+
else
|
|
2902
|
+
incoming.set(e.target, [e]);
|
|
2903
|
+
}
|
|
2904
|
+
const depthOf = /* @__PURE__ */ new Map();
|
|
2905
|
+
const viaOf = /* @__PURE__ */ new Map();
|
|
2906
|
+
depthOf.set(target.id, 0);
|
|
2907
|
+
let frontier = [target.id];
|
|
2908
|
+
let depth = 0;
|
|
2909
|
+
while (frontier.length > 0 && depth < MAX_DEPTH) {
|
|
2910
|
+
depth++;
|
|
2911
|
+
const next = [];
|
|
2912
|
+
for (const id of frontier) {
|
|
2913
|
+
const edges = (incoming.get(id) ?? []).slice().sort((a, b) => a.source.localeCompare(b.source) || a.relation.localeCompare(b.relation));
|
|
2914
|
+
for (const e of edges) {
|
|
2915
|
+
if (depthOf.has(e.source))
|
|
2916
|
+
continue;
|
|
2917
|
+
depthOf.set(e.source, depth);
|
|
2918
|
+
viaOf.set(e.source, { rel: e.relation, from: id });
|
|
2919
|
+
next.push(e.source);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
next.sort();
|
|
2923
|
+
frontier = next;
|
|
2924
|
+
}
|
|
2925
|
+
const dependents = [...depthOf.entries()].filter(([id]) => id !== target.id);
|
|
2926
|
+
const total = dependents.length;
|
|
2927
|
+
const lines = [];
|
|
2928
|
+
lines.push(`Impact of ${target.id}`);
|
|
2929
|
+
if (target.signature)
|
|
2930
|
+
lines.push(` ${target.signature}`);
|
|
2931
|
+
lines.push("");
|
|
2932
|
+
if (total === 0) {
|
|
2933
|
+
lines.push("No resolved dependents \u2014 nothing in the graph reaches this symbol.");
|
|
2934
|
+
lines.push("(Cross-file resolution is partial; this is a lower bound, not proof it's unused.)");
|
|
2935
|
+
return lines.join("\n");
|
|
2936
|
+
}
|
|
2937
|
+
lines.push(`${total} dependent${total === 1 ? "" : "s"} (transitive), by depth:`);
|
|
2938
|
+
lines.push("");
|
|
2939
|
+
const byDepth = /* @__PURE__ */ new Map();
|
|
2940
|
+
for (const [id, d] of dependents) {
|
|
2941
|
+
const list = byDepth.get(d) ?? [];
|
|
2942
|
+
list.push(id);
|
|
2943
|
+
byDepth.set(d, list);
|
|
2944
|
+
}
|
|
2945
|
+
let shown = 0;
|
|
2946
|
+
for (const d of [...byDepth.keys()].sort((a, b) => a - b)) {
|
|
2947
|
+
const ids = byDepth.get(d).sort();
|
|
2948
|
+
lines.push(` depth ${d} (${ids.length}):`);
|
|
2949
|
+
for (const id of ids) {
|
|
2950
|
+
if (shown >= IMPACT_CAP)
|
|
2951
|
+
break;
|
|
2952
|
+
const via = viaOf.get(id);
|
|
2953
|
+
const tag = via ? ` [${via.rel} \u2192 ${via.from}]` : "";
|
|
2954
|
+
lines.push(` ${id}${tag}`);
|
|
2955
|
+
shown++;
|
|
2956
|
+
}
|
|
2957
|
+
if (shown >= IMPACT_CAP)
|
|
2958
|
+
break;
|
|
2959
|
+
}
|
|
2960
|
+
if (total > shown)
|
|
2961
|
+
lines.push(` ... and ${total - shown} more`);
|
|
2962
|
+
lines.push("");
|
|
2963
|
+
lines.push("Note: only RESOLVED edges are traversed (cross-file resolution is partial),");
|
|
2964
|
+
lines.push("so this is a lower bound on impact, not a completeness guarantee.");
|
|
2965
|
+
return lines.join("\n");
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// dist/src/graph/vfs-handler.js
|
|
2969
|
+
function workTreeIdFor(cwd) {
|
|
2970
|
+
return createHash3("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
2971
|
+
}
|
|
2972
|
+
function handleGraphVfs(subpath, cwd) {
|
|
2973
|
+
const path = subpath.replace(/^\/+/, "");
|
|
2974
|
+
if (path === "" || path === "/") {
|
|
2975
|
+
return { kind: "ok", body: dirListing() };
|
|
2976
|
+
}
|
|
2977
|
+
if (path === "index.md" || path === "index") {
|
|
2978
|
+
return loadSnapshotOrError(cwd, (snap, baseDir) => ({
|
|
2979
|
+
kind: "ok",
|
|
2980
|
+
body: renderIndex(snap, baseDir, cwd)
|
|
2981
|
+
}));
|
|
2982
|
+
}
|
|
2983
|
+
if (path.startsWith("find/")) {
|
|
2984
|
+
const pattern = path.slice("find/".length);
|
|
2985
|
+
if (pattern === "") {
|
|
2986
|
+
return { kind: "not-found", message: "find/ requires a pattern: cat memory/graph/find/<keyword>" };
|
|
2987
|
+
}
|
|
2988
|
+
return loadSnapshotOrError(cwd, (snap, baseDir) => ({
|
|
2989
|
+
kind: "ok",
|
|
2990
|
+
body: renderFind(snap, pattern, baseDir, workTreeIdFor(cwd))
|
|
2991
|
+
}));
|
|
2992
|
+
}
|
|
2993
|
+
if (path.startsWith("show/")) {
|
|
2994
|
+
const key = path.slice("show/".length);
|
|
2995
|
+
if (key === "") {
|
|
2996
|
+
return { kind: "not-found", message: "show/ requires a handle or pattern" };
|
|
2997
|
+
}
|
|
2998
|
+
return loadSnapshotOrError(cwd, (snap, baseDir) => ({
|
|
2999
|
+
kind: "ok",
|
|
3000
|
+
body: renderShow(snap, key, baseDir, workTreeIdFor(cwd))
|
|
3001
|
+
}));
|
|
3002
|
+
}
|
|
3003
|
+
if (path.startsWith("query/")) {
|
|
3004
|
+
const pattern = path.slice("query/".length);
|
|
3005
|
+
if (pattern === "") {
|
|
3006
|
+
return { kind: "not-found", message: "query/ requires a pattern: cat memory/graph/query/<keyword>" };
|
|
3007
|
+
}
|
|
3008
|
+
return loadSnapshotOrError(cwd, (snap, baseDir) => ({
|
|
3009
|
+
kind: "ok",
|
|
3010
|
+
body: renderQuery(snap, pattern, baseDir, workTreeIdFor(cwd))
|
|
3011
|
+
}));
|
|
3012
|
+
}
|
|
3013
|
+
if (path.startsWith("impact/")) {
|
|
3014
|
+
const pattern = path.slice("impact/".length);
|
|
3015
|
+
if (pattern === "") {
|
|
3016
|
+
return { kind: "not-found", message: "impact/ requires a pattern: cat memory/graph/impact/<symbol>" };
|
|
3017
|
+
}
|
|
3018
|
+
return loadSnapshotOrError(cwd, (snap) => ({ kind: "ok", body: renderImpact(snap, pattern) }));
|
|
3019
|
+
}
|
|
3020
|
+
if (path.startsWith("neighborhood/")) {
|
|
3021
|
+
const file = path.slice("neighborhood/".length);
|
|
3022
|
+
if (file === "") {
|
|
3023
|
+
return { kind: "not-found", message: "neighborhood/ requires a file path: cat memory/graph/neighborhood/<file>" };
|
|
3024
|
+
}
|
|
3025
|
+
return loadSnapshotOrError(cwd, (snap) => ({ kind: "ok", body: renderNeighborhood(snap, file) }));
|
|
3026
|
+
}
|
|
3027
|
+
if (path === "layers" || path === "layers/" || path === "layers/index.md") {
|
|
3028
|
+
return loadSnapshotOrError(cwd, (snap) => ({ kind: "ok", body: renderLayers(snap) }));
|
|
3029
|
+
}
|
|
3030
|
+
if (path === "tour" || path === "tour/" || path === "tour/index.md") {
|
|
3031
|
+
return loadSnapshotOrError(cwd, (snap) => ({ kind: "ok", body: renderTour(snap) }));
|
|
3032
|
+
}
|
|
3033
|
+
if (path.startsWith("path/")) {
|
|
3034
|
+
const rest = path.slice("path/".length);
|
|
3035
|
+
const slash = rest.indexOf("/");
|
|
3036
|
+
if (slash <= 0 || slash === rest.length - 1) {
|
|
3037
|
+
return { kind: "not-found", message: "path/ needs two patterns: cat memory/graph/path/<from>/<to> (each a symbol-name substring, no slash)" };
|
|
3038
|
+
}
|
|
3039
|
+
const fromPattern = rest.slice(0, slash);
|
|
3040
|
+
const toPattern = rest.slice(slash + 1);
|
|
3041
|
+
return loadSnapshotOrError(cwd, (snap) => ({ kind: "ok", body: renderPath(snap, fromPattern, toPattern) }));
|
|
3042
|
+
}
|
|
3043
|
+
return {
|
|
3044
|
+
kind: "not-found",
|
|
3045
|
+
message: `Unknown endpoint: graph/${path}
|
|
3046
|
+
Available: index.md, find/<pattern>, query/<pattern>, show/<handle-or-pattern>, impact/<pattern>, neighborhood/<file>, layers, tour, path/<from>/<to>`
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
function loadSnapshotOrError(cwd, fn) {
|
|
3050
|
+
let key;
|
|
3051
|
+
let baseDir;
|
|
3052
|
+
try {
|
|
3053
|
+
key = deriveProjectKey(cwd).key;
|
|
3054
|
+
baseDir = repoDir(key);
|
|
3055
|
+
} catch (e) {
|
|
3056
|
+
return { kind: "no-graph", message: `Cannot derive repo identity: ${e instanceof Error ? e.message : String(e)}` };
|
|
3057
|
+
}
|
|
3058
|
+
const wt = workTreeIdFor(cwd);
|
|
3059
|
+
const last = readLastBuild(baseDir, wt);
|
|
3060
|
+
if (last === null) {
|
|
3061
|
+
return {
|
|
3062
|
+
kind: "no-graph",
|
|
3063
|
+
message: "No local graph for this worktree yet. Run `hivemind graph build` (or `hivemind graph pull` if a teammate has built this commit)."
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
const fileBase = last.commit_sha ?? last.snapshot_sha256;
|
|
3067
|
+
const snapPath = join14(baseDir, "snapshots", `${fileBase}.json`);
|
|
3068
|
+
if (!existsSync7(snapPath)) {
|
|
3069
|
+
return { kind: "no-graph", message: `Snapshot file missing on disk: ${snapPath}` };
|
|
3070
|
+
}
|
|
3071
|
+
let snap;
|
|
3072
|
+
try {
|
|
3073
|
+
snap = JSON.parse(readFileSync9(snapPath, "utf8"));
|
|
3074
|
+
} catch (e) {
|
|
3075
|
+
return { kind: "no-graph", message: `Failed to parse snapshot: ${e instanceof Error ? e.message : String(e)}` };
|
|
3076
|
+
}
|
|
3077
|
+
if (!Array.isArray(snap.nodes) || !Array.isArray(snap.links)) {
|
|
3078
|
+
return { kind: "no-graph", message: "Snapshot schema is invalid (missing nodes/links arrays)." };
|
|
3079
|
+
}
|
|
3080
|
+
try {
|
|
3081
|
+
return fn(snap, baseDir);
|
|
3082
|
+
} catch (e) {
|
|
3083
|
+
return { kind: "no-graph", message: `Failed to render graph view: ${e instanceof Error ? e.message : String(e)}` };
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
function dirListing() {
|
|
3087
|
+
return [
|
|
3088
|
+
"index.md",
|
|
3089
|
+
"find/",
|
|
3090
|
+
"query/",
|
|
3091
|
+
"show/",
|
|
3092
|
+
"impact/",
|
|
3093
|
+
"neighborhood/",
|
|
3094
|
+
"layers",
|
|
3095
|
+
"tour",
|
|
3096
|
+
"path/"
|
|
3097
|
+
].join("\n");
|
|
3098
|
+
}
|
|
3099
|
+
function renderIndex(snap, baseDir, cwd) {
|
|
3100
|
+
const commit = snap.graph.commit_sha?.slice(0, 7) ?? "no-commit";
|
|
3101
|
+
const fullCommit = snap.graph.commit_sha ?? "no-commit";
|
|
3102
|
+
const totalNodes = snap.nodes.length;
|
|
3103
|
+
const totalEdges = snap.links.length;
|
|
3104
|
+
const byFile = {};
|
|
3105
|
+
for (const n of snap.nodes)
|
|
3106
|
+
byFile[n.source_file] = (byFile[n.source_file] ?? 0) + 1;
|
|
3107
|
+
const topFiles = Object.entries(byFile).sort(([, a], [, b]) => b - a).slice(0, 8);
|
|
3108
|
+
const byRel = {};
|
|
3109
|
+
for (const e of snap.links)
|
|
3110
|
+
byRel[e.relation] = (byRel[e.relation] ?? 0) + 1;
|
|
3111
|
+
const byKind = {};
|
|
3112
|
+
for (const n of snap.nodes)
|
|
3113
|
+
byKind[n.kind] = (byKind[n.kind] ?? 0) + 1;
|
|
3114
|
+
const lines = [];
|
|
3115
|
+
lines.push(`# Code Graph \u2014 ${snap.observation.repo_project}`);
|
|
3116
|
+
lines.push("");
|
|
3117
|
+
lines.push(`Commit: ${fullCommit} (built ${snap.observation.ts})`);
|
|
3118
|
+
lines.push(`Branch: ${snap.observation.branch ?? "(detached)"}`);
|
|
3119
|
+
lines.push(`Source: ${join14(baseDir, "snapshots", `${commit ? snap.graph.commit_sha : "?"}.json`)}`);
|
|
3120
|
+
lines.push("");
|
|
3121
|
+
lines.push(`Nodes: ${totalNodes} Edges: ${totalEdges}`);
|
|
3122
|
+
lines.push("");
|
|
3123
|
+
lines.push("## How to query");
|
|
3124
|
+
lines.push(" cat ~/.deeplake/memory/graph/query/<pattern>");
|
|
3125
|
+
lines.push(" 2-in-1: search + expand the top matches with their 1-hop");
|
|
3126
|
+
lines.push(" neighbors (callers/callees/imports/heritage). Start here.");
|
|
3127
|
+
lines.push(" Multi-token AND: query/<a>+<b> requires both tokens.");
|
|
3128
|
+
lines.push("");
|
|
3129
|
+
lines.push(" cat ~/.deeplake/memory/graph/find/<pattern>");
|
|
3130
|
+
lines.push(" Case-insensitive substring match on node id + label.");
|
|
3131
|
+
lines.push(" Emits numbered handles [1] [2] ... saved for this worktree.");
|
|
3132
|
+
lines.push("");
|
|
3133
|
+
lines.push(" cat ~/.deeplake/memory/graph/show/<handle-or-pattern>");
|
|
3134
|
+
lines.push(" <handle>: a digit from a prior `find/`/`query/` (e.g. 3).");
|
|
3135
|
+
lines.push(" <pattern>: a substring; resolves to a unique node if possible,");
|
|
3136
|
+
lines.push(" or shows candidates if ambiguous.");
|
|
3137
|
+
lines.push(" Output: node detail + 1-hop neighbors grouped by edge kind.");
|
|
3138
|
+
lines.push("");
|
|
3139
|
+
lines.push(" Also: neighborhood/<file> \xB7 layers \xB7 tour \xB7 path/<from>/<to>");
|
|
3140
|
+
lines.push("");
|
|
3141
|
+
lines.push("## Node kinds");
|
|
3142
|
+
for (const [k, n] of Object.entries(byKind).sort(([, a], [, b]) => b - a)) {
|
|
3143
|
+
lines.push(` ${k.padEnd(12)} ${n}`);
|
|
3144
|
+
}
|
|
3145
|
+
lines.push("");
|
|
3146
|
+
lines.push("## Edge kinds");
|
|
3147
|
+
for (const [k, n] of Object.entries(byRel).sort(([, a], [, b]) => b - a)) {
|
|
3148
|
+
lines.push(` ${k.padEnd(12)} ${n}`);
|
|
3149
|
+
}
|
|
3150
|
+
lines.push("");
|
|
3151
|
+
lines.push("## Top files by node count");
|
|
3152
|
+
for (const [f, n] of topFiles) {
|
|
3153
|
+
lines.push(` ${String(n).padStart(4)} ${f}`);
|
|
3154
|
+
}
|
|
3155
|
+
lines.push("");
|
|
3156
|
+
lines.push(`Limitations:`);
|
|
3157
|
+
lines.push(` - TypeScript / JavaScript / Python. AST-based, no semantic similarity edges yet.`);
|
|
3158
|
+
lines.push(` - Cross-file 'calls'/'imports'/'extends' ARE resolved for relative named/namespace`);
|
|
3159
|
+
lines.push(` imports; bare (npm)/aliased/barrel/dynamic imports stay unresolved. So a node`);
|
|
3160
|
+
lines.push(` with "Incoming (0)" is not proof of dead code \u2014 a caller may reach it via an`);
|
|
3161
|
+
lines.push(` unresolved import path. (Python cross-file resolution is a follow-up; Python is`);
|
|
3162
|
+
lines.push(` intra-file + structure only for now.)`);
|
|
3163
|
+
lines.push(` - Stale after edits \u2014 if a file's mtime is newer than the build, read the live source.`);
|
|
3164
|
+
void cwd;
|
|
3165
|
+
return lines.join("\n");
|
|
3166
|
+
}
|
|
3167
|
+
function findMatches(snap, pattern) {
|
|
3168
|
+
const tokens = pattern.toLowerCase().split(/[\s+]+/).filter((t) => t.length > 0);
|
|
3169
|
+
if (tokens.length === 0)
|
|
3170
|
+
return [];
|
|
3171
|
+
if (tokens.length === 1) {
|
|
3172
|
+
const needle = tokens[0];
|
|
3173
|
+
const matches2 = [];
|
|
3174
|
+
for (const n of snap.nodes) {
|
|
3175
|
+
if (n.id.toLowerCase().includes(needle) || n.label.toLowerCase().includes(needle))
|
|
3176
|
+
matches2.push(n);
|
|
3177
|
+
}
|
|
3178
|
+
matches2.sort((a, b) => {
|
|
3179
|
+
const ra = rank(a, needle);
|
|
3180
|
+
const rb = rank(b, needle);
|
|
3181
|
+
if (ra !== rb)
|
|
3182
|
+
return ra - rb;
|
|
3183
|
+
return a.id.localeCompare(b.id);
|
|
3184
|
+
});
|
|
3185
|
+
if (matches2.length === 0)
|
|
3186
|
+
return fuzzyMatches(snap, needle);
|
|
3187
|
+
return matches2;
|
|
3188
|
+
}
|
|
3189
|
+
const matches = [];
|
|
3190
|
+
for (const n of snap.nodes) {
|
|
3191
|
+
const id = n.id.toLowerCase();
|
|
3192
|
+
const lbl = n.label.toLowerCase();
|
|
3193
|
+
if (tokens.every((t) => id.includes(t) || lbl.includes(t)))
|
|
3194
|
+
matches.push(n);
|
|
3195
|
+
}
|
|
3196
|
+
const score = (n) => tokens.reduce((s, t) => s + rank(n, t), 0);
|
|
3197
|
+
matches.sort((a, b) => {
|
|
3198
|
+
const sa = score(a);
|
|
3199
|
+
const sb = score(b);
|
|
3200
|
+
if (sa !== sb)
|
|
3201
|
+
return sa - sb;
|
|
3202
|
+
return a.id.localeCompare(b.id);
|
|
3203
|
+
});
|
|
3204
|
+
return matches;
|
|
3205
|
+
}
|
|
3206
|
+
function fuzzyMatches(snap, needle) {
|
|
3207
|
+
if (needle.length < 3)
|
|
3208
|
+
return [];
|
|
3209
|
+
const maxDist = Math.max(1, Math.floor(needle.length / 4));
|
|
3210
|
+
const scored = [];
|
|
3211
|
+
for (const n of snap.nodes) {
|
|
3212
|
+
const d = editDistance(needle, n.label.toLowerCase(), maxDist);
|
|
3213
|
+
if (d <= maxDist)
|
|
3214
|
+
scored.push({ n, d });
|
|
3215
|
+
}
|
|
3216
|
+
scored.sort((a, b) => a.d !== b.d ? a.d - b.d : a.n.id.localeCompare(b.n.id));
|
|
3217
|
+
return scored.slice(0, 25).map((s) => s.n);
|
|
3218
|
+
}
|
|
3219
|
+
function editDistance(a, b, cap) {
|
|
3220
|
+
if (Math.abs(a.length - b.length) > cap)
|
|
3221
|
+
return cap + 1;
|
|
3222
|
+
let prev = new Array(b.length + 1);
|
|
3223
|
+
let cur = new Array(b.length + 1);
|
|
3224
|
+
for (let j = 0; j <= b.length; j++)
|
|
3225
|
+
prev[j] = j;
|
|
3226
|
+
for (let i = 1; i <= a.length; i++) {
|
|
3227
|
+
cur[0] = i;
|
|
3228
|
+
let rowMin = cur[0];
|
|
3229
|
+
for (let j = 1; j <= b.length; j++) {
|
|
3230
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
3231
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
|
|
3232
|
+
if (cur[j] < rowMin)
|
|
3233
|
+
rowMin = cur[j];
|
|
3234
|
+
}
|
|
3235
|
+
if (rowMin > cap)
|
|
3236
|
+
return cap + 1;
|
|
3237
|
+
[prev, cur] = [cur, prev];
|
|
3238
|
+
}
|
|
3239
|
+
return prev[b.length];
|
|
3240
|
+
}
|
|
3241
|
+
function renderFind(snap, pattern, baseDir, worktreeId) {
|
|
3242
|
+
const matches = findMatches(snap, pattern);
|
|
3243
|
+
const capped = matches.slice(0, 50);
|
|
3244
|
+
if (capped.length === 0) {
|
|
3245
|
+
return `No matches for "${pattern}" in ${snap.nodes.length} nodes.
|
|
3246
|
+
Try a shorter or different substring.`;
|
|
3247
|
+
}
|
|
3248
|
+
saveHandles(baseDir, worktreeId, capped.map((n) => n.id), pattern);
|
|
3249
|
+
const lines = [];
|
|
3250
|
+
lines.push(`${matches.length} match${matches.length === 1 ? "" : "es"} for "${pattern}"${matches.length > capped.length ? ` (showing first ${capped.length})` : ""}:`);
|
|
3251
|
+
lines.push("");
|
|
3252
|
+
for (let i = 0; i < capped.length; i++) {
|
|
3253
|
+
const n = capped[i];
|
|
3254
|
+
const tag = n.exported ? "exported" : "internal";
|
|
3255
|
+
lines.push(` [${i + 1}] ${n.id} ${n.kind} (${tag})`);
|
|
3256
|
+
}
|
|
3257
|
+
lines.push("");
|
|
3258
|
+
lines.push("Use: cat ~/.deeplake/memory/graph/show/<N> to see node + 1-hop neighbors");
|
|
3259
|
+
return lines.join("\n");
|
|
3260
|
+
}
|
|
3261
|
+
var QUERY_TOP_N = 5;
|
|
3262
|
+
var QUERY_NEIGHBOR_CAP = 8;
|
|
3263
|
+
function renderQuery(snap, pattern, baseDir, worktreeId) {
|
|
3264
|
+
const matches = findMatches(snap, pattern);
|
|
3265
|
+
if (matches.length === 0) {
|
|
3266
|
+
return `No matches for "${pattern}" in ${snap.nodes.length} nodes.
|
|
3267
|
+
Try a shorter or different substring, or cat memory/graph/find/<pattern>.`;
|
|
3268
|
+
}
|
|
3269
|
+
const top = matches.slice(0, QUERY_TOP_N);
|
|
3270
|
+
saveHandles(baseDir, worktreeId, top.map((n) => n.id), pattern);
|
|
3271
|
+
const topIds = new Set(top.map((n) => n.id));
|
|
3272
|
+
const outByNode = /* @__PURE__ */ new Map();
|
|
3273
|
+
const inByNode = /* @__PURE__ */ new Map();
|
|
3274
|
+
for (const e of snap.links) {
|
|
3275
|
+
if (topIds.has(e.source))
|
|
3276
|
+
(outByNode.get(e.source) ?? setGet(outByNode, e.source)).push(e);
|
|
3277
|
+
if (topIds.has(e.target))
|
|
3278
|
+
(inByNode.get(e.target) ?? setGet(inByNode, e.target)).push(e);
|
|
3279
|
+
}
|
|
3280
|
+
const lines = [];
|
|
3281
|
+
lines.push(`Query "${pattern}" \u2014 ${matches.length} match${matches.length === 1 ? "" : "es"}, expanded top ${top.length} (1 hop)`);
|
|
3282
|
+
lines.push("");
|
|
3283
|
+
for (let i = 0; i < top.length; i++) {
|
|
3284
|
+
const n = top[i];
|
|
3285
|
+
const tags = [n.exported ? "exported" : "internal"];
|
|
3286
|
+
if (n.is_entrypoint)
|
|
3287
|
+
tags.push("entrypoint");
|
|
3288
|
+
if (n.fan_in !== void 0)
|
|
3289
|
+
tags.push(`fan_in=${n.fan_in}`);
|
|
3290
|
+
if (n.fan_out !== void 0)
|
|
3291
|
+
tags.push(`fan_out=${n.fan_out}`);
|
|
3292
|
+
lines.push(`[${i + 1}] ${n.id} ${n.kind} (${tags.join(", ")})`);
|
|
3293
|
+
if (n.signature)
|
|
3294
|
+
lines.push(` ${n.signature}`);
|
|
3295
|
+
renderHopGroup(lines, outByNode.get(n.id) ?? [], "OUT", "target");
|
|
3296
|
+
renderHopGroup(lines, inByNode.get(n.id) ?? [], "IN", "source");
|
|
3297
|
+
lines.push("");
|
|
3298
|
+
}
|
|
3299
|
+
lines.push("Use: cat ~/.deeplake/memory/graph/show/<N> for full detail on a match.");
|
|
3300
|
+
return lines.join("\n");
|
|
3301
|
+
}
|
|
3302
|
+
function setGet(m, key) {
|
|
3303
|
+
const list = [];
|
|
3304
|
+
m.set(key, list);
|
|
3305
|
+
return list;
|
|
3306
|
+
}
|
|
3307
|
+
function renderHopGroup(lines, edges, dir, otherField) {
|
|
3308
|
+
if (edges.length === 0)
|
|
3309
|
+
return;
|
|
3310
|
+
const byRel = /* @__PURE__ */ new Map();
|
|
3311
|
+
for (const e of edges) {
|
|
3312
|
+
let counts = byRel.get(e.relation);
|
|
3313
|
+
if (!counts) {
|
|
3314
|
+
counts = /* @__PURE__ */ new Map();
|
|
3315
|
+
byRel.set(e.relation, counts);
|
|
3316
|
+
}
|
|
3317
|
+
const id = e[otherField];
|
|
3318
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
3319
|
+
}
|
|
3320
|
+
for (const [rel, counts] of [...byRel.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
3321
|
+
const arrow = dir === "OUT" ? `--${rel}-->` : `<--${rel}--`;
|
|
3322
|
+
const ids = [...counts.keys()].sort();
|
|
3323
|
+
const shown = ids.slice(0, QUERY_NEIGHBOR_CAP).map((id) => {
|
|
3324
|
+
const c = counts.get(id);
|
|
3325
|
+
return c > 1 ? `${id} \xD7${c}` : id;
|
|
3326
|
+
});
|
|
3327
|
+
const more = ids.length > shown.length ? ` (+${ids.length - shown.length} more)` : "";
|
|
3328
|
+
lines.push(` ${arrow} ${shown.join(", ")}${more}`);
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
function renderShow(snap, key, baseDir, worktreeId) {
|
|
3332
|
+
if (/^\d+$/.test(key)) {
|
|
3333
|
+
const idx = parseInt(key, 10);
|
|
3334
|
+
const handles = loadHandles(baseDir, worktreeId);
|
|
3335
|
+
if (handles === null) {
|
|
3336
|
+
return `Handle [${idx}] not resolvable: no recent find/ in this worktree. Run cat memory/graph/find/<pattern> first.`;
|
|
3337
|
+
}
|
|
3338
|
+
if (idx < 1 || idx > handles.ids.length) {
|
|
3339
|
+
return `Handle [${idx}] out of range. Last find/${handles.pattern} produced ${handles.ids.length} matches.`;
|
|
3340
|
+
}
|
|
3341
|
+
const nodeId = handles.ids[idx - 1];
|
|
3342
|
+
const node = snap.nodes.find((n) => n.id === nodeId);
|
|
3343
|
+
if (!node) {
|
|
3344
|
+
return `Handle [${idx}] points at "${nodeId}" but that node is no longer in the snapshot (graph rebuilt since last find?). Re-run find.`;
|
|
3345
|
+
}
|
|
3346
|
+
return renderNodeDetail(snap, node);
|
|
3347
|
+
}
|
|
3348
|
+
const needle = key.toLowerCase();
|
|
3349
|
+
const matches = snap.nodes.filter((n) => n.id.toLowerCase().includes(needle));
|
|
3350
|
+
if (matches.length === 0) {
|
|
3351
|
+
return `No node matches "${key}". Try cat memory/graph/find/${key} for fuzzy search.`;
|
|
3352
|
+
}
|
|
3353
|
+
if (matches.length === 1) {
|
|
3354
|
+
return renderNodeDetail(snap, matches[0]);
|
|
3355
|
+
}
|
|
3356
|
+
saveHandles(baseDir, worktreeId, matches.slice(0, 50).map((n) => n.id), key);
|
|
3357
|
+
const lines = [];
|
|
3358
|
+
lines.push(`"${key}" matches ${matches.length} nodes. Pick one:`);
|
|
3359
|
+
lines.push("");
|
|
3360
|
+
for (let i = 0; i < Math.min(matches.length, 50); i++) {
|
|
3361
|
+
lines.push(` [${i + 1}] ${matches[i].id}`);
|
|
3362
|
+
}
|
|
3363
|
+
lines.push("");
|
|
3364
|
+
lines.push("Use: cat ~/.deeplake/memory/graph/show/<N>");
|
|
3365
|
+
return lines.join("\n");
|
|
3366
|
+
}
|
|
3367
|
+
function renderNodeDetail(snap, node) {
|
|
3368
|
+
const incoming = [];
|
|
3369
|
+
const outgoing = [];
|
|
3370
|
+
for (const e of snap.links) {
|
|
3371
|
+
if (e.target === node.id)
|
|
3372
|
+
incoming.push(e);
|
|
3373
|
+
if (e.source === node.id)
|
|
3374
|
+
outgoing.push(e);
|
|
3375
|
+
}
|
|
3376
|
+
const groupBy = (es) => {
|
|
3377
|
+
const m = /* @__PURE__ */ new Map();
|
|
3378
|
+
for (const e of es) {
|
|
3379
|
+
const list = m.get(e.relation) ?? [];
|
|
3380
|
+
list.push(e);
|
|
3381
|
+
m.set(e.relation, list);
|
|
3382
|
+
}
|
|
3383
|
+
return m;
|
|
3384
|
+
};
|
|
3385
|
+
const inGrp = groupBy(incoming);
|
|
3386
|
+
const outGrp = groupBy(outgoing);
|
|
3387
|
+
const lines = [];
|
|
3388
|
+
lines.push(`Node: ${node.id}`);
|
|
3389
|
+
lines.push(` source: ${node.source_file}:${node.source_location}`);
|
|
3390
|
+
lines.push(` kind: ${node.kind}`);
|
|
3391
|
+
lines.push(` label: ${node.label}`);
|
|
3392
|
+
if (node.signature)
|
|
3393
|
+
lines.push(` sig: ${node.signature}`);
|
|
3394
|
+
if (node.doc)
|
|
3395
|
+
lines.push(` doc: ${node.doc}`);
|
|
3396
|
+
const tags = [node.exported ? "exported" : "internal"];
|
|
3397
|
+
if (node.is_entrypoint)
|
|
3398
|
+
tags.push("entrypoint");
|
|
3399
|
+
if (node.fan_in !== void 0)
|
|
3400
|
+
tags.push(`fan_in=${node.fan_in}`);
|
|
3401
|
+
if (node.fan_out !== void 0)
|
|
3402
|
+
tags.push(`fan_out=${node.fan_out}`);
|
|
3403
|
+
lines.push(` ${tags.join(" ")}`);
|
|
3404
|
+
lines.push("");
|
|
3405
|
+
const inHint = incoming.length === 0 ? " \u2014 no resolved callers (cross-file resolution is partial; not proof of dead code)" : ":";
|
|
3406
|
+
lines.push(`Incoming (${incoming.length})${inHint}`);
|
|
3407
|
+
for (const [rel, es] of [...inGrp.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
3408
|
+
lines.push(` ${rel} (${es.length}):`);
|
|
3409
|
+
for (const e of es.slice(0, 20)) {
|
|
3410
|
+
lines.push(` ${e.source}`);
|
|
3411
|
+
}
|
|
3412
|
+
if (es.length > 20)
|
|
3413
|
+
lines.push(` ... and ${es.length - 20} more`);
|
|
3414
|
+
}
|
|
3415
|
+
lines.push("");
|
|
3416
|
+
lines.push(`Outgoing (${outgoing.length})${outgoing.length === 0 ? " \u2014 this node has no edges out" : ":"}`);
|
|
3417
|
+
for (const [rel, es] of [...outGrp.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
3418
|
+
lines.push(` ${rel} (${es.length}):`);
|
|
3419
|
+
for (const e of es.slice(0, 20)) {
|
|
3420
|
+
lines.push(` ${e.target}`);
|
|
3421
|
+
}
|
|
3422
|
+
if (es.length > 20)
|
|
3423
|
+
lines.push(` ... and ${es.length - 20} more`);
|
|
3424
|
+
}
|
|
3425
|
+
return lines.join("\n");
|
|
3426
|
+
}
|
|
3427
|
+
function rank(n, needle) {
|
|
3428
|
+
const lbl = n.label.toLowerCase();
|
|
3429
|
+
const id = n.id.toLowerCase();
|
|
3430
|
+
if (lbl === needle)
|
|
3431
|
+
return 0;
|
|
3432
|
+
if (lbl.startsWith(needle))
|
|
3433
|
+
return 1;
|
|
3434
|
+
if (lbl.includes(needle))
|
|
3435
|
+
return 2;
|
|
3436
|
+
if (id.includes(needle))
|
|
3437
|
+
return 3;
|
|
3438
|
+
return 4;
|
|
3439
|
+
}
|
|
3440
|
+
function handlesPath(baseDir, worktreeId) {
|
|
3441
|
+
return join14(baseDir, "worktrees", worktreeId, ".find-handles.json");
|
|
3442
|
+
}
|
|
3443
|
+
function saveHandles(baseDir, worktreeId, ids, pattern) {
|
|
3444
|
+
const path = handlesPath(baseDir, worktreeId);
|
|
3445
|
+
const payload = { pattern, ts: Date.now(), ids };
|
|
3446
|
+
try {
|
|
3447
|
+
mkdirSync8(dirname6(path), { recursive: true });
|
|
3448
|
+
const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
3449
|
+
writeFileSync7(tmp, JSON.stringify(payload));
|
|
3450
|
+
renameSync5(tmp, path);
|
|
3451
|
+
} catch {
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
function loadHandles(baseDir, worktreeId) {
|
|
3455
|
+
const path = handlesPath(baseDir, worktreeId);
|
|
3456
|
+
if (!existsSync7(path))
|
|
3457
|
+
return null;
|
|
3458
|
+
try {
|
|
3459
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
3460
|
+
if (parsed === null || typeof parsed !== "object")
|
|
3461
|
+
return null;
|
|
3462
|
+
const o = parsed;
|
|
3463
|
+
if (typeof o.pattern !== "string")
|
|
3464
|
+
return null;
|
|
3465
|
+
if (typeof o.ts !== "number")
|
|
3466
|
+
return null;
|
|
3467
|
+
if (!Array.isArray(o.ids))
|
|
3468
|
+
return null;
|
|
3469
|
+
if (!o.ids.every((s) => typeof s === "string"))
|
|
3470
|
+
return null;
|
|
3471
|
+
return { pattern: o.pattern, ts: o.ts, ids: o.ids };
|
|
3472
|
+
} catch {
|
|
3473
|
+
return null;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
// dist/src/graph/graph-command.js
|
|
3478
|
+
var GRAPH_ROOT = "/graph";
|
|
3479
|
+
var GRAPH_PREFIX = "/graph/";
|
|
3480
|
+
function tokenize(s) {
|
|
3481
|
+
return s.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
3482
|
+
}
|
|
3483
|
+
function stripQuotes(p) {
|
|
3484
|
+
if (p.length >= 2 && (p[0] === '"' && p[p.length - 1] === '"' || p[0] === "'" && p[p.length - 1] === "'")) {
|
|
3485
|
+
return p.slice(1, -1);
|
|
3486
|
+
}
|
|
3487
|
+
return p;
|
|
3488
|
+
}
|
|
3489
|
+
function parseReadTargetPath(rewrittenCommand) {
|
|
3490
|
+
const cmd = rewrittenCommand.replace(/\s+2>\S+/g, "").trim();
|
|
3491
|
+
const pipeIdx = cmd.indexOf("|");
|
|
3492
|
+
let readPart = cmd;
|
|
3493
|
+
if (pipeIdx >= 0) {
|
|
3494
|
+
readPart = cmd.slice(0, pipeIdx).trim();
|
|
3495
|
+
const after = cmd.slice(pipeIdx + 1).trim();
|
|
3496
|
+
if (after.includes("|"))
|
|
3497
|
+
return null;
|
|
3498
|
+
if (!/^(?:head|tail)\b/.test(after))
|
|
3499
|
+
return null;
|
|
3500
|
+
if (!/^cat\b/.test(readPart))
|
|
3501
|
+
return null;
|
|
3502
|
+
}
|
|
3503
|
+
const tokens = tokenize(readPart);
|
|
3504
|
+
if (tokens.length === 0)
|
|
3505
|
+
return null;
|
|
3506
|
+
const verb = tokens[0];
|
|
3507
|
+
if (verb !== "cat" && verb !== "head" && verb !== "tail")
|
|
3508
|
+
return null;
|
|
3509
|
+
const operands = [];
|
|
3510
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
3511
|
+
const tok = tokens[i];
|
|
3512
|
+
if (tok.startsWith("-")) {
|
|
3513
|
+
if ((verb === "head" || verb === "tail") && (tok === "-n" || tok === "--lines"))
|
|
3514
|
+
i++;
|
|
3515
|
+
continue;
|
|
3516
|
+
}
|
|
3517
|
+
operands.push(tok);
|
|
3518
|
+
if (operands.length > 1)
|
|
3519
|
+
return null;
|
|
3520
|
+
}
|
|
3521
|
+
return operands.length === 1 ? stripQuotes(operands[0]) : null;
|
|
3522
|
+
}
|
|
3523
|
+
function hasTraversal(virtualPath) {
|
|
3524
|
+
return virtualPath.split("/").includes("..");
|
|
3525
|
+
}
|
|
3526
|
+
function tryGraphRead(rewrittenCommand, cwd) {
|
|
3527
|
+
const ls = rewrittenCommand.replace(/\s+2>\S+/g, "").trim().match(/^ls\s+(?:-\S+\s+)*(\S+)\s*$/);
|
|
3528
|
+
if (ls) {
|
|
3529
|
+
const dir = stripQuotes(ls[1]).replace(/\/+$/, "") || "/";
|
|
3530
|
+
if (dir === GRAPH_ROOT)
|
|
3531
|
+
return "index.md\nfind/\nquery/\nshow/\nimpact/\nneighborhood/\nlayers\ntour\npath/\n";
|
|
3532
|
+
return null;
|
|
3533
|
+
}
|
|
3534
|
+
const virtualPath = parseReadTargetPath(rewrittenCommand);
|
|
3535
|
+
if (virtualPath === null)
|
|
3536
|
+
return null;
|
|
3537
|
+
if (hasTraversal(virtualPath))
|
|
3538
|
+
return null;
|
|
3539
|
+
const normalized = virtualPath.replace(/\/+$/, "") || "/";
|
|
3540
|
+
if (normalized === GRAPH_ROOT)
|
|
3541
|
+
return "index.md\nfind/\nquery/\nshow/\nimpact/\nneighborhood/\nlayers\ntour\npath/\n";
|
|
3542
|
+
if (!virtualPath.startsWith(GRAPH_PREFIX))
|
|
3543
|
+
return null;
|
|
3544
|
+
const subpath = virtualPath.slice(GRAPH_PREFIX.length);
|
|
3545
|
+
const result = handleGraphVfs(subpath, cwd);
|
|
3546
|
+
return result.kind === "ok" ? result.body : `(${result.kind}) ${result.message}`;
|
|
3547
|
+
}
|
|
3548
|
+
|
|
2338
3549
|
// dist/src/hooks/cursor/pre-tool-use.js
|
|
2339
3550
|
var log5 = (msg) => log("cursor-pre-tool-use", msg);
|
|
2340
3551
|
async function main() {
|
|
@@ -2347,6 +3558,19 @@ async function main() {
|
|
|
2347
3558
|
if (!touchesMemory(command))
|
|
2348
3559
|
return;
|
|
2349
3560
|
const rewritten = rewritePaths(command);
|
|
3561
|
+
const graphBody = tryGraphRead(rewritten, input.cwd ?? process.cwd());
|
|
3562
|
+
if (graphBody !== null) {
|
|
3563
|
+
log5(`graph vfs intercept: ${command.slice(0, 80)}`);
|
|
3564
|
+
const echoCmd = `cat <<'__HIVEMIND_RESULT__'
|
|
3565
|
+
${graphBody}
|
|
3566
|
+
__HIVEMIND_RESULT__`;
|
|
3567
|
+
process.stdout.write(JSON.stringify({
|
|
3568
|
+
permission: "allow",
|
|
3569
|
+
updated_input: { command: echoCmd },
|
|
3570
|
+
agent_message: "[Hivemind graph]"
|
|
3571
|
+
}));
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
2350
3574
|
const grepParams = parseBashGrep(rewritten);
|
|
2351
3575
|
if (!grepParams)
|
|
2352
3576
|
return;
|