@zhn2605/topo 0.1.0 → 0.2.0

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/dist/topo.js CHANGED
@@ -315,6 +315,9 @@ function findEntryPoints(graph, rootDir) {
315
315
  if (n.language === "rust" && n.kind === "function" && n.name === "main" && RUST_BIN.test(n.file)) {
316
316
  add(n.id, "nativeMain", "the binary's fn main");
317
317
  }
318
+ if ((n.language === "c" || n.language === "cpp") && n.id === `${n.file}#main`) {
319
+ add(n.id, "nativeMain", "the program's main()");
320
+ }
318
321
  }
319
322
  for (const n of graph.nodes) {
320
323
  if (n.kind !== "function" || !isStarter(n) || !n.exported) continue;
@@ -446,7 +449,7 @@ var init_entry = __esm({
446
449
  robloxScript: 80,
447
450
  // Foo.server.luau / Foo.client.luau: a Script/LocalScript Roblox runs itself
448
451
  nativeMain: 80,
449
- // Rust fn main in src/main.rs or src/bin/*.rs; a Python file with a __main__ guard
452
+ // Rust fn main in src/main.rs or src/bin/*.rs; C/C++ main(); a Python __main__ guard
450
453
  scriptRoot: 20,
451
454
  // …that is a folder's init.server/init.client (the root of a script tree)
452
455
  moduleRoot: 10,
@@ -504,12 +507,13 @@ var init_hubs = __esm({
504
507
  import fs2 from "node:fs";
505
508
  import path2 from "node:path";
506
509
  function readLayers(repo) {
507
- let raw;
508
510
  try {
509
- raw = JSON.parse(fs2.readFileSync(layersFile(repo), "utf8"));
511
+ return parseLayers(JSON.parse(fs2.readFileSync(layersFile(repo), "utf8")));
510
512
  } catch {
511
513
  return void 0;
512
514
  }
515
+ }
516
+ function parseLayers(raw) {
513
517
  const d = raw;
514
518
  if (!d || !Array.isArray(d.layers)) return void 0;
515
519
  const layers = d.layers.filter(
@@ -521,6 +525,20 @@ function readLayers(repo) {
521
525
  );
522
526
  return { version: 1, layers, rules };
523
527
  }
528
+ function writeLayers(repo, doc) {
529
+ fs2.mkdirSync(path2.dirname(layersFile(repo)), { recursive: true });
530
+ fs2.writeFileSync(layersFile(repo), `${JSON.stringify(doc, null, 2)}
531
+ `);
532
+ }
533
+ function layerMap(doc, graph) {
534
+ const layerOf = layerIndex(doc);
535
+ const byFile = {};
536
+ for (const n of graph.nodes) {
537
+ const l = n.kind === "file" ? layerOf(n.file) : void 0;
538
+ if (l) byFile[n.file] = l;
539
+ }
540
+ return { names: doc.layers.map((l) => l.name), byFile };
541
+ }
524
542
  function layerIndex(doc) {
525
543
  const compiled = doc.layers.map((l) => ({ name: l.name, res: l.paths.map(globToRegExp) }));
526
544
  const memo = /* @__PURE__ */ new Map();
@@ -905,8 +923,8 @@ function parseUnifiedDiff(text) {
905
923
  if (line.startsWith("diff --git ")) {
906
924
  flush();
907
925
  const both = line.slice("diff --git ".length);
908
- const path26 = unquote(both.slice(0, (both.length - 1) / 2));
909
- cur = { oldPath: path26, newPath: path26, status: "modified", oldRanges: [], newRanges: [] };
926
+ const path27 = unquote(both.slice(0, (both.length - 1) / 2));
927
+ cur = { oldPath: path27, newPath: path27, status: "modified", oldRanges: [], newRanges: [] };
910
928
  } else if (!cur) {
911
929
  } else if (line.startsWith("new file mode")) {
912
930
  cur.status = "added";
@@ -956,10 +974,10 @@ function getFileDiffs(repo, baseSha, headRef) {
956
974
  const diffs = parseUnifiedDiff(git(repo, args).toString());
957
975
  if (headRef) return diffs;
958
976
  const untracked = git(repo, ["ls-files", "--others", "--exclude-standard", "-z"]).toString().split("\0").filter((p) => p && !p.endsWith("/"));
959
- for (const path26 of untracked) {
960
- const n = lineCount(join(repo, path26));
977
+ for (const path27 of untracked) {
978
+ const n = lineCount(join(repo, path27));
961
979
  diffs.push({
962
- newPath: path26,
980
+ newPath: path27,
963
981
  status: "added",
964
982
  oldRanges: [],
965
983
  newRanges: n ? [{ startLine: 1, endLine: n }] : []
@@ -1042,13 +1060,17 @@ function matchTarget(spec, targets, gamePath) {
1042
1060
  }
1043
1061
  function findTool(name, root) {
1044
1062
  const rokit = path3.join(os.homedir(), ".rokit");
1045
- const stored = (() => {
1063
+ const store = path3.join(rokit, "tool-storage");
1064
+ const ls = (dir) => {
1046
1065
  try {
1047
- return fs3.globSync(`tool-storage/*/${name}/*/${name}`, { cwd: rokit }).sort((a, b) => a.localeCompare(b, "en", { numeric: true })).map((p) => path3.join(rokit, p)).reverse();
1066
+ return fs3.readdirSync(dir);
1048
1067
  } catch {
1049
1068
  return [];
1050
1069
  }
1051
- })();
1070
+ };
1071
+ const stored = ls(store).flatMap(
1072
+ (author) => ls(path3.join(store, author, name)).map((v) => path3.join(author, name, v, name))
1073
+ ).filter((p) => fs3.existsSync(path3.join(store, p))).sort((a, b) => a.localeCompare(b, "en", { numeric: true })).map((p) => path3.join(store, p)).reverse();
1052
1074
  for (const bin of [name, path3.join(rokit, "bin", name), ...stored]) {
1053
1075
  if (spawnSync2(bin, ["--version"], { cwd: root, stdio: "ignore" }).status === 0) return bin;
1054
1076
  }
@@ -1200,6 +1222,7 @@ function indexerVersion() {
1200
1222
  h.update(depVersion(path4.join(ENGINE_ROOT, "package.json"), "web-tree-sitter"));
1201
1223
  h.update(depVersion(path4.join(ENGINE_ROOT, "package.json"), "tree-sitter-python"));
1202
1224
  h.update(depVersion(path4.join(ENGINE_ROOT, "package.json"), "tree-sitter-rust"));
1225
+ h.update(depVersion(path4.join(ENGINE_ROOT, "package.json"), "tree-sitter-cpp"));
1203
1226
  h.update(luauLspIdentity());
1204
1227
  memoVersion = h.digest("hex").slice(0, 16);
1205
1228
  return memoVersion;
@@ -1766,8 +1789,8 @@ function calleeName(call) {
1766
1789
  if (ts2.isPropertyAccessExpression(c) && ts2.isIdentifier(c.name)) return c.name.text;
1767
1790
  return void 0;
1768
1791
  }
1769
- function segsOf(path26) {
1770
- return path26.split("/").filter(Boolean).map((s) => s.includes("*") || s.startsWith("[...") || s.startsWith("[[...") ? "*" : s).map((s) => /^[:{[]/.test(s) ? ":" : s);
1792
+ function segsOf(path27) {
1793
+ return path27.split("/").filter(Boolean).map((s) => s.includes("*") || s.startsWith("[...") || s.startsWith("[[...") ? "*" : s).map((s) => /^[:{[]/.test(s) ? ":" : s);
1771
1794
  }
1772
1795
  function urlPath(e) {
1773
1796
  let text;
@@ -1832,8 +1855,8 @@ function routeCall(r, call, site, target, handlerOf) {
1832
1855
  if (to) r.servers.push({ method: shape.method, segs: segsOf(shape.path), to });
1833
1856
  if (to !== void 0) return;
1834
1857
  }
1835
- const path26 = arg && urlPath(arg);
1836
- if (!path26) return;
1858
+ const path27 = arg && urlPath(arg);
1859
+ if (!path27) return;
1837
1860
  const m = name.toLowerCase();
1838
1861
  let method;
1839
1862
  if (ts2.isIdentifier(call.expression) && CLIENT_FNS.has(name)) method = fetchMethod(call);
@@ -1841,9 +1864,9 @@ function routeCall(r, call, site, target, handlerOf) {
1841
1864
  method = m.toUpperCase();
1842
1865
  else if (target && (target.kind === "function" || target.kind === "method")) method = "ANY";
1843
1866
  else return;
1844
- const segs = segsOf(path26);
1867
+ const segs = segsOf(path27);
1845
1868
  if (segs.some((s) => s !== ":" && s !== "*"))
1846
- r.clients.push({ ...site, method, segs, raw: path26 });
1869
+ r.clients.push({ ...site, method, segs, raw: path27 });
1847
1870
  }
1848
1871
  function serverLiteral(r, lit, to, prefix = false) {
1849
1872
  const m = isStr(lit) ? ROUTE_LITERAL.exec(lit.text) : null;
@@ -2342,6 +2365,16 @@ function grammarWasm(pkg) {
2342
2365
  const bundled = fileURLToPath2(new URL(`./grammars/${pkg}.wasm`, import.meta.url));
2343
2366
  return fs5.existsSync(bundled) ? bundled : createRequire(import.meta.url).resolve(`${pkg}/${pkg}.wasm`);
2344
2367
  }
2368
+ function leadingComments(stmt) {
2369
+ const out = [];
2370
+ let at = stmt;
2371
+ for (let p = stmt.previousNamedSibling; p?.type === "comment"; p = p.previousNamedSibling) {
2372
+ if (p.endPosition.row < at.startPosition.row - 1) break;
2373
+ out.unshift(p);
2374
+ at = p;
2375
+ }
2376
+ return out;
2377
+ }
2345
2378
  var treeSitterReady;
2346
2379
  var init_tree_sitter = __esm({
2347
2380
  "packages/engine/src/indexer/tree-sitter.ts"() {
@@ -2350,11 +2383,547 @@ var init_tree_sitter = __esm({
2350
2383
  }
2351
2384
  });
2352
2385
 
2353
- // packages/engine/src/indexer/luau.ts
2354
- import { createHash as createHash4 } from "node:crypto";
2386
+ // packages/engine/src/indexer/cpp.ts
2355
2387
  import fs6 from "node:fs";
2356
2388
  import path6 from "node:path";
2357
2389
  import { Language, Parser as Parser2 } from "web-tree-sitter";
2390
+ function qualSegs(n) {
2391
+ if (!n) return void 0;
2392
+ switch (n.type) {
2393
+ case "identifier":
2394
+ case "field_identifier":
2395
+ case "type_identifier":
2396
+ case "namespace_identifier":
2397
+ case "destructor_name":
2398
+ case "operator_name":
2399
+ return [n.text];
2400
+ case "template_type":
2401
+ case "template_function":
2402
+ case "template_method":
2403
+ return qualSegs(n.childForFieldName("name"));
2404
+ case "qualified_identifier": {
2405
+ const scope = n.childForFieldName("scope");
2406
+ const head = scope ? qualSegs(scope) : [];
2407
+ const tail = qualSegs(n.childForFieldName("name"));
2408
+ return head && tail ? [...head, ...tail] : void 0;
2409
+ }
2410
+ }
2411
+ return void 0;
2412
+ }
2413
+ function templateArg(n) {
2414
+ const t = n.type === "qualified_identifier" ? innermost(n) : n;
2415
+ const arg = t.childForFieldName("arguments")?.namedChildren[0];
2416
+ return arg?.type === "type_descriptor" ? arg.childForFieldName("type") : null;
2417
+ }
2418
+ function typeSegs(t) {
2419
+ if (!t) return void 0;
2420
+ if (t.type === "struct_specifier" || t.type === "class_specifier")
2421
+ return qualSegs(t.childForFieldName("name"));
2422
+ const segs = qualSegs(t);
2423
+ if (segs && SMART_PTRS.has(segs.at(-1) ?? "")) return typeSegs(templateArg(t));
2424
+ return segs;
2425
+ }
2426
+ function declName(d) {
2427
+ for (let n = d; n; ) {
2428
+ if (n.type === "identifier" || n.type === "field_identifier") return n.text;
2429
+ if (n.type === "function_declarator") return void 0;
2430
+ n = n.childForFieldName("declarator") ?? n.namedChildren.find((c) => c?.type === "identifier" || c?.type.endsWith("declarator")) ?? null;
2431
+ }
2432
+ return void 0;
2433
+ }
2434
+ function fnDeclarator(d) {
2435
+ for (let n = d; n; ) {
2436
+ if (n.type === "function_declarator") return n;
2437
+ n = n.childForFieldName("declarator") ?? n.namedChildren.find((c) => c?.type.endsWith("declarator")) ?? null;
2438
+ }
2439
+ return void 0;
2440
+ }
2441
+ function builtType(v) {
2442
+ if (v?.type === "new_expression") return typeSegs(v.childForFieldName("type"));
2443
+ if (v?.type === "call_expression") {
2444
+ const fn = v.childForFieldName("function");
2445
+ if (fn && MAKERS.has(qualSegs(fn)?.at(-1) ?? "")) return typeSegs(templateArg(fn));
2446
+ }
2447
+ return void 0;
2448
+ }
2449
+ function collect(file, text, tags, root) {
2450
+ const language = /\.c$/.test(file) ? "c" : "cpp";
2451
+ const lineCount2 = text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
2452
+ const fileNode = {
2453
+ id: fileId(file),
2454
+ kind: "file",
2455
+ name: path6.posix.basename(file),
2456
+ file,
2457
+ range: { startLine: 1, endLine: Math.max(1, lineCount2) },
2458
+ bodyHash: bodyHash(text),
2459
+ exported: false,
2460
+ language,
2461
+ tags: [...tags],
2462
+ mappingStatus: "full"
2463
+ };
2464
+ const out = {
2465
+ file,
2466
+ nodes: [fileNode],
2467
+ includes: [],
2468
+ defs: [],
2469
+ classes: [],
2470
+ calls: [],
2471
+ declared: /* @__PURE__ */ new Set()
2472
+ };
2473
+ const block2 = (item, outer, name, kind, bodyStart, exported) => {
2474
+ const comments2 = leadingComments(outer);
2475
+ const doc = comments2.map((c) => c.text.replace(/^\/\/\/?\s?|^\/\*+\s?|\s*\*+\/$/g, "").replace(/^\s*\* ?/gm, "")).join("\n").trim();
2476
+ return {
2477
+ id: "",
2478
+ // set once the qualified name is known
2479
+ kind,
2480
+ name,
2481
+ file,
2482
+ range: {
2483
+ startLine: (comments2[0] ?? outer).startPosition.row + 1,
2484
+ endLine: outer.endPosition.row + 1
2485
+ },
2486
+ signature: collapse(text.slice(item.startIndex, bodyStart)),
2487
+ ...doc && { doc: doc.slice(0, DOC_CAP) },
2488
+ bodyHash: bodyHash(item.text, name),
2489
+ exported,
2490
+ language,
2491
+ tags: [...tags],
2492
+ mappingStatus: "full"
2493
+ };
2494
+ };
2495
+ const ctxOf = (s) => s.def ? defCtx(s.def) : s.ns;
2496
+ const walk2 = (n, s, inLoop, access) => {
2497
+ for (const c of n.namedChildren) {
2498
+ if (!c || c.type === "comment") continue;
2499
+ if (c.type === "access_specifier" && access) {
2500
+ access.v = c.text;
2501
+ continue;
2502
+ }
2503
+ if (c.type === "preproc_include") {
2504
+ const p = c.childForFieldName("path");
2505
+ if (p?.type === "string_literal" || p?.type === "system_lib_string") {
2506
+ const angle = p.type === "system_lib_string";
2507
+ out.includes.push({
2508
+ path: angle ? p.text.slice(1, -1) : p.text.replace(/^"|"$/g, ""),
2509
+ angle,
2510
+ line: c.startPosition.row + 1
2511
+ });
2512
+ }
2513
+ continue;
2514
+ }
2515
+ if (c.type === "namespace_definition") {
2516
+ const name = c.childForFieldName("name")?.text;
2517
+ const body = c.childForFieldName("body");
2518
+ const segs = name ? name.split("::") : [];
2519
+ if (body) walk2(body, { ...s, ns: [...s.ns, ...segs] }, inLoop);
2520
+ continue;
2521
+ }
2522
+ const outer = c;
2523
+ const item = c.type === "template_declaration" ? c.namedChildren.at(-1) ?? c : c;
2524
+ const spec = item.type === "type_definition" ? item.childForFieldName("type") : item;
2525
+ if ((spec?.type === "class_specifier" || spec?.type === "struct_specifier") && spec.childForFieldName("body") && !s.def) {
2526
+ const body = spec.childForFieldName("body");
2527
+ const name = spec.childForFieldName("name")?.text ?? (item !== spec ? declName(item.childForFieldName("declarator")) : void 0);
2528
+ if (!name) {
2529
+ walk2(body, s, inLoop);
2530
+ continue;
2531
+ }
2532
+ const node = block2(spec, outer, name, "class", body.startIndex, true);
2533
+ const info = {
2534
+ q: join2([...s.ns, name]),
2535
+ node,
2536
+ ns: s.ns,
2537
+ fields: /* @__PURE__ */ new Map(),
2538
+ bases: []
2539
+ };
2540
+ node.id = nodeId(file, info.q);
2541
+ node.parentId = s.cls?.node.id ?? fileNode.id;
2542
+ for (const b of spec.namedChildren)
2543
+ if (b?.type === "base_class_clause")
2544
+ for (const t of b.namedChildren) {
2545
+ const segs = typeSegs(t);
2546
+ if (segs) info.bases.push(segs);
2547
+ }
2548
+ out.nodes.push(node);
2549
+ out.classes.push(info);
2550
+ for (const f of body.namedChildren) {
2551
+ if (f?.type !== "field_declaration") continue;
2552
+ const t = typeSegs(f.childForFieldName("type"));
2553
+ if (!t) continue;
2554
+ for (const d of f.childrenForFieldName("declarator")) {
2555
+ const fname = declName(d);
2556
+ if (fname) info.fields.set(fname, t);
2557
+ }
2558
+ }
2559
+ const isStruct = spec.type === "struct_specifier";
2560
+ walk2(body, { from: node, ns: [...s.ns, name], cls: info }, false, {
2561
+ v: isStruct ? "public" : "private"
2562
+ });
2563
+ continue;
2564
+ }
2565
+ if (item.type === "function_definition" && !s.def) {
2566
+ const fd = fnDeclarator(item.childForFieldName("declarator"));
2567
+ const segs = qualSegs(fd?.childForFieldName("declarator") ?? null);
2568
+ const body = item.childForFieldName("body");
2569
+ const name = segs?.at(-1);
2570
+ if (!fd || !segs || !name || !body) continue;
2571
+ const isStatic = item.namedChildren.some(
2572
+ (m) => m?.type === "storage_class_specifier" && m.text === "static"
2573
+ );
2574
+ const inClass = !!s.cls;
2575
+ const exported = inClass ? access?.v !== "private" : !isStatic;
2576
+ const node = block2(
2577
+ item,
2578
+ outer,
2579
+ name,
2580
+ inClass ? "method" : "function",
2581
+ body.startIndex,
2582
+ exported
2583
+ );
2584
+ const def = {
2585
+ node,
2586
+ name,
2587
+ ns: s.ns,
2588
+ scope: segs.slice(0, -1),
2589
+ ...s.cls && { cls: s.cls.q },
2590
+ locals: /* @__PURE__ */ new Map()
2591
+ };
2592
+ if (s.cls) node.parentId = s.cls.node.id;
2593
+ for (const p of fd.childForFieldName("parameters")?.namedChildren ?? []) {
2594
+ if (p?.type !== "parameter_declaration" && p?.type !== "optional_parameter_declaration")
2595
+ continue;
2596
+ const name2 = declName(p.childForFieldName("declarator"));
2597
+ const t = typeSegs(p.childForFieldName("type"));
2598
+ if (name2 && t) def.locals.set(name2, t);
2599
+ }
2600
+ out.nodes.push(node);
2601
+ out.defs.push(def);
2602
+ if (!s.cls) out.declared.add(join2([...s.ns, ...segs]));
2603
+ const inner = { from: def, ns: s.ns, def };
2604
+ for (const init of item.namedChildren)
2605
+ if (init?.type === "field_initializer_list") walk2(init, inner, false);
2606
+ walk2(body, inner, false);
2607
+ continue;
2608
+ }
2609
+ if ((item.type === "declaration" || item.type === "field_declaration") && !s.def) {
2610
+ const fd = fnDeclarator(item.childForFieldName("declarator"));
2611
+ const segs = fd && qualSegs(fd.childForFieldName("declarator"));
2612
+ if (segs) {
2613
+ if (!s.cls) out.declared.add(join2([...s.ns, ...segs]));
2614
+ continue;
2615
+ }
2616
+ }
2617
+ if (c.type === "declaration" && s.def) {
2618
+ const t = typeSegs(c.childForFieldName("type"));
2619
+ for (const d of c.childrenForFieldName("declarator")) {
2620
+ const wrapped = d?.type === "init_declarator" || d?.type === "function_declarator";
2621
+ const target = wrapped ? d.childForFieldName("declarator") : d;
2622
+ const v = d?.type === "init_declarator" ? d.childForFieldName("value") : null;
2623
+ const name = declName(target);
2624
+ const type = t ?? builtType(v);
2625
+ if (name && type) s.def.locals.set(name, type);
2626
+ const constructs = !v || v.type === "argument_list" || v.type === "initializer_list";
2627
+ if (t && target?.type === "identifier" && constructs) {
2628
+ out.calls.push({
2629
+ from: s.from,
2630
+ ctx: ctxOf(s),
2631
+ form: "ctor",
2632
+ segs: t,
2633
+ line: c.startPosition.row + 1,
2634
+ inLoop
2635
+ });
2636
+ }
2637
+ }
2638
+ }
2639
+ if (c.type === "new_expression") {
2640
+ const t = typeSegs(c.childForFieldName("type"));
2641
+ if (t)
2642
+ out.calls.push({
2643
+ from: s.from,
2644
+ ctx: ctxOf(s),
2645
+ form: "ctor",
2646
+ segs: t,
2647
+ line: c.startPosition.row + 1,
2648
+ inLoop
2649
+ });
2650
+ }
2651
+ if (c.type === "call_expression") {
2652
+ const fn = c.childForFieldName("function");
2653
+ const site = { from: s.from, ctx: ctxOf(s), line: c.startPosition.row + 1, inLoop };
2654
+ if (fn?.type === "field_expression") {
2655
+ const recv = fn.childForFieldName("argument");
2656
+ const method = qualSegs(fn.childForFieldName("field"))?.at(-1);
2657
+ let segs;
2658
+ if (recv?.type === "this") {
2659
+ if (method) out.calls.push({ ...site, form: "this", segs: [], method });
2660
+ } else if (recv?.type === "identifier") segs = [recv.text];
2661
+ else if (recv?.type === "field_expression" && recv.childForFieldName("argument")?.type === "this")
2662
+ segs = ["this", recv.childForFieldName("field")?.text ?? ""];
2663
+ if (segs && method) out.calls.push({ ...site, form: "member", segs, method });
2664
+ } else if (fn) {
2665
+ const segs = qualSegs(fn);
2666
+ if (segs && MAKERS.has(segs.at(-1) ?? "")) {
2667
+ const t = typeSegs(templateArg(fn));
2668
+ if (t) out.calls.push({ ...site, form: "ctor", segs: t });
2669
+ } else if (segs) out.calls.push({ ...site, form: "name", segs });
2670
+ }
2671
+ }
2672
+ walk2(c, s, inLoop || LOOPS.has(c.type));
2673
+ }
2674
+ };
2675
+ walk2(root, { from: fileNode, ns: [] }, false);
2676
+ return out;
2677
+ }
2678
+ function inScope(ctx, segs, map) {
2679
+ for (let i = ctx.length; i >= 0; i--) {
2680
+ const k = join2([...ctx.slice(0, i), ...segs]);
2681
+ if (map.has(k)) return k;
2682
+ }
2683
+ return void 0;
2684
+ }
2685
+ function includeDirs(root, files) {
2686
+ const dirs = /* @__PURE__ */ new Set();
2687
+ for (const f of files) {
2688
+ const segs = f.split("/");
2689
+ const i = segs.lastIndexOf("include");
2690
+ if (i >= 0 && i < segs.length - 1) dirs.add(segs.slice(0, i + 1).join("/"));
2691
+ }
2692
+ const read = (name) => {
2693
+ try {
2694
+ return fs6.readFileSync(path6.join(root, name), "utf8");
2695
+ } catch {
2696
+ return "";
2697
+ }
2698
+ };
2699
+ const flags = [...read("Makefile").matchAll(/-I\s*([^\s"']+)/g)].map((m) => m[1]);
2700
+ for (const m of read("CMakeLists.txt").matchAll(/include_directories\s*\(([^)]*)\)/gi))
2701
+ flags.push(...m[1].split(/\s+/));
2702
+ for (const raw of flags) {
2703
+ const d = raw.replace(/^"|"$/g, "").replace(/^\$\{(CMAKE_CURRENT_SOURCE_DIR|PROJECT_SOURCE_DIR|CMAKE_SOURCE_DIR)\}\/?/, "");
2704
+ if (!d || d.startsWith("/") || d.includes("$") || /^[A-Z]+$/.test(d)) continue;
2705
+ dirs.add(path6.posix.normalize(d).replace(/\/$/, "").replace(/^\.$/, ""));
2706
+ }
2707
+ dirs.add("");
2708
+ return [...dirs];
2709
+ }
2710
+ function buildGraph(root, parsed) {
2711
+ const byFile = new Map(parsed.map((f) => [f.file, f]));
2712
+ const dirs = includeDirs(root, [...byFile.keys()]);
2713
+ const { edges, add } = edgeSink();
2714
+ const classes = /* @__PURE__ */ new Map();
2715
+ for (const f of parsed) for (const c of f.classes) if (!classes.has(c.q)) classes.set(c.q, c);
2716
+ const methods = /* @__PURE__ */ new Map();
2717
+ const funcs = /* @__PURE__ */ new Map();
2718
+ const push2 = (m, k, d) => m.set(k, [...m.get(k) ?? [], d]);
2719
+ for (const f of parsed) {
2720
+ const fileNode = f.nodes[0];
2721
+ const taken = new Set(f.classes.map((c) => c.q));
2722
+ for (const d of f.defs) {
2723
+ if (!d.cls && d.scope.length) {
2724
+ const cls = inScope(d.ns, d.scope, classes);
2725
+ if (cls) {
2726
+ d.cls = cls;
2727
+ d.node.kind = "method";
2728
+ const owner = classes.get(cls);
2729
+ d.node.parentId = owner.node.file === f.file ? owner.node.id : fileNode.id;
2730
+ d.node.exported = true;
2731
+ }
2732
+ }
2733
+ d.node.parentId ??= fileNode.id;
2734
+ const base = d.cls ? `${d.cls}.${d.name}` : join2([...d.ns, ...d.scope, d.name]);
2735
+ let q = base;
2736
+ for (let i = 2; taken.has(q); i++) q = `${base}@${d.node.kind}${i === 2 ? "" : i}`;
2737
+ taken.add(q);
2738
+ d.node.id = nodeId(f.file, q);
2739
+ push2(d.cls ? methods : funcs, base, d);
2740
+ }
2741
+ }
2742
+ const resolveInclude = (from, inc) => {
2743
+ const tries = inc.angle ? [] : [path6.posix.join(path6.posix.dirname(from), inc.path)];
2744
+ for (const d of dirs) tries.push(path6.posix.join(d, inc.path));
2745
+ return tries.map((t) => path6.posix.normalize(t)).find((t) => byFile.has(t));
2746
+ };
2747
+ const direct = /* @__PURE__ */ new Map();
2748
+ for (const f of parsed) {
2749
+ const to = [];
2750
+ for (const inc of f.includes) {
2751
+ const hit = resolveInclude(f.file, inc);
2752
+ if (!hit) continue;
2753
+ to.push(hit);
2754
+ add(fileId(f.file), "imports", fileId(hit), ["static"], 1, { line: inc.line, inLoop: false });
2755
+ }
2756
+ direct.set(f.file, to);
2757
+ }
2758
+ const closures = /* @__PURE__ */ new Map();
2759
+ const closure = (file) => {
2760
+ let seen = closures.get(file);
2761
+ if (seen) return seen;
2762
+ seen = /* @__PURE__ */ new Set([file]);
2763
+ const stack = [file];
2764
+ while (stack.length) {
2765
+ for (const g of direct.get(stack.pop()) ?? []) {
2766
+ if (!seen.has(g)) {
2767
+ seen.add(g);
2768
+ stack.push(g);
2769
+ }
2770
+ }
2771
+ }
2772
+ closures.set(file, seen);
2773
+ return seen;
2774
+ };
2775
+ const ctorOf = (q) => methods.get(`${q}.${classes.get(q).node.name}`)?.[0]?.node.id;
2776
+ const methodOf = (q, name, depth = 0) => {
2777
+ const hit = methods.get(`${q}.${name}`)?.[0];
2778
+ if (hit || depth > 4) return hit;
2779
+ const cls = classes.get(q);
2780
+ for (const b of cls?.bases ?? []) {
2781
+ const bq = inScope(cls?.ns ?? [], b, classes);
2782
+ const m = bq && methodOf(bq, name, depth + 1);
2783
+ if (m) return m;
2784
+ }
2785
+ return void 0;
2786
+ };
2787
+ const fieldOf = (q, name, depth = 0) => {
2788
+ const cls = classes.get(q);
2789
+ if (!cls || depth > 4) return void 0;
2790
+ const t = cls.fields.get(name);
2791
+ if (t) return { type: t, ns: cls.ns };
2792
+ for (const b of cls.bases) {
2793
+ const bq = inScope(cls.ns, b, classes);
2794
+ const hit = bq && fieldOf(bq, name, depth + 1);
2795
+ if (hit) return hit;
2796
+ }
2797
+ return void 0;
2798
+ };
2799
+ for (const f of parsed) {
2800
+ const seen = closure(f.file);
2801
+ const visible = (q) => [...seen].some((g) => byFile.get(g)?.declared.has(q));
2802
+ const pickFn = (q) => {
2803
+ const cands = funcs.get(q) ?? [];
2804
+ const here = cands.find((d) => d.node.file === f.file);
2805
+ if (here) return { id: here.node.id, how: STATIC };
2806
+ const inc = cands.filter((d) => seen.has(d.node.file));
2807
+ if (inc.length === 1) return { id: inc[0].node.id, how: CROSS };
2808
+ if (cands.length !== 1) return void 0;
2809
+ return { id: cands[0].node.id, how: visible(q) ? CROSS : GUESS };
2810
+ };
2811
+ const how = (to) => to.split("#")[0] === f.file ? STATIC : CROSS;
2812
+ for (const c of f.calls) {
2813
+ const def = "locals" in c.from ? c.from : void 0;
2814
+ const fromId = def ? def.node.id : c.from.id;
2815
+ const site = { line: c.line, inLoop: c.inLoop };
2816
+ const link = (to, h = to ? how(to) : STATIC) => {
2817
+ if (to) add(fromId, "calls", to, ...h, site);
2818
+ };
2819
+ if (c.form === "ctor") {
2820
+ const q = inScope(c.ctx, c.segs, classes);
2821
+ if (q) link(ctorOf(q));
2822
+ continue;
2823
+ }
2824
+ if (c.form === "this") {
2825
+ if (def?.cls && c.method) link(methodOf(def.cls, c.method)?.node.id);
2826
+ continue;
2827
+ }
2828
+ if (c.form === "member") {
2829
+ const [head, field] = c.segs;
2830
+ let t;
2831
+ if (head === "this") t = def?.cls && field ? fieldOf(def.cls, field) : void 0;
2832
+ else {
2833
+ const local = def?.locals.get(head);
2834
+ t = local ? { type: local, ns: c.ctx } : def?.cls ? fieldOf(def.cls, head) : void 0;
2835
+ }
2836
+ const q = t && inScope(t.ns, t.type, classes);
2837
+ if (q && c.method) link(methodOf(q, c.method)?.node.id);
2838
+ continue;
2839
+ }
2840
+ const name = c.segs.at(-1);
2841
+ if (c.segs.length === 1 && def?.cls) {
2842
+ const m = methodOf(def.cls, name);
2843
+ if (m) {
2844
+ link(m.node.id);
2845
+ continue;
2846
+ }
2847
+ }
2848
+ if (c.segs.length > 1) {
2849
+ const cls = inScope(c.ctx, c.segs.slice(0, -1), classes);
2850
+ if (cls) {
2851
+ link(methodOf(cls, name)?.node.id);
2852
+ continue;
2853
+ }
2854
+ }
2855
+ const fq = inScope(c.ctx, c.segs, funcs);
2856
+ const fn = fq && pickFn(fq);
2857
+ if (fn) {
2858
+ link(fn.id, fn.how);
2859
+ continue;
2860
+ }
2861
+ const cq = inScope(c.ctx, c.segs, classes);
2862
+ if (cq) link(ctorOf(cq));
2863
+ }
2864
+ for (const cls of f.classes) {
2865
+ for (const b of cls.bases) {
2866
+ const bq = inScope(cls.ns, b, classes);
2867
+ if (bq) {
2868
+ const to = classes.get(bq).node.id;
2869
+ add(cls.node.id, "inherits", to, ...how(to), {
2870
+ line: cls.node.range.startLine,
2871
+ inLoop: false
2872
+ });
2873
+ }
2874
+ }
2875
+ }
2876
+ }
2877
+ return { nodes: parsed.flatMap((f) => f.nodes), edges: [...edges.values()] };
2878
+ }
2879
+ function indexCpp(root, files, tagsFor2) {
2880
+ const parsed = [];
2881
+ for (const file of [...files].sort()) {
2882
+ const text = fs6.readFileSync(path6.join(root, file), "utf8");
2883
+ if (GENERATED.test(text.split("\n", 30).join("\n"))) continue;
2884
+ const tree = parser.parse(text);
2885
+ if (!tree) continue;
2886
+ try {
2887
+ parsed.push(collect(file, text, tagsFor2(file), tree.rootNode));
2888
+ } finally {
2889
+ tree.delete();
2890
+ }
2891
+ }
2892
+ return buildGraph(root, parsed);
2893
+ }
2894
+ var parser, STATIC, CROSS, GUESS, LOOPS, SMART_PTRS, MAKERS, GENERATED, join2, innermost, defCtx;
2895
+ var init_cpp = __esm({
2896
+ async "packages/engine/src/indexer/cpp.ts"() {
2897
+ "use strict";
2898
+ init_ids();
2899
+ init_blocks();
2900
+ init_edges();
2901
+ init_tree_sitter();
2902
+ await treeSitterReady;
2903
+ parser = new Parser2();
2904
+ parser.setLanguage(await Language.load(grammarWasm("tree-sitter-cpp")));
2905
+ STATIC = [["static"], 0.95];
2906
+ CROSS = [["inferred"], 0.9];
2907
+ GUESS = [["inferred"], 0.5];
2908
+ LOOPS = /* @__PURE__ */ new Set(["for_statement", "for_range_loop", "while_statement", "do_statement"]);
2909
+ SMART_PTRS = /* @__PURE__ */ new Set(["unique_ptr", "shared_ptr", "weak_ptr"]);
2910
+ MAKERS = /* @__PURE__ */ new Set(["make_unique", "make_shared"]);
2911
+ GENERATED = /generated by|do not edit/i;
2912
+ join2 = (segs) => segs.join(".");
2913
+ innermost = (n) => {
2914
+ let at = n;
2915
+ while (at.type === "qualified_identifier") at = at.childForFieldName("name") ?? at;
2916
+ return at;
2917
+ };
2918
+ defCtx = (d) => d.cls ? d.cls.split(".") : [...d.ns, ...d.scope];
2919
+ }
2920
+ });
2921
+
2922
+ // packages/engine/src/indexer/luau.ts
2923
+ import { createHash as createHash4 } from "node:crypto";
2924
+ import fs7 from "node:fs";
2925
+ import path7 from "node:path";
2926
+ import { Language as Language2, Parser as Parser3 } from "web-tree-sitter";
2358
2927
  function chainOf(n) {
2359
2928
  if (n?.type === "identifier") return n.text;
2360
2929
  if (n?.type !== "dot_index_expression") return void 0;
@@ -2371,7 +2940,7 @@ function ownLine(c) {
2371
2940
  const prev = c.previousSibling;
2372
2941
  return !prev || prev.endPosition.row < c.startPosition.row;
2373
2942
  }
2374
- function leadingComments(stmt) {
2943
+ function leadingComments2(stmt) {
2375
2944
  const out = [];
2376
2945
  let below = stmt;
2377
2946
  for (let c = above(stmt); c?.type === "comment"; c = above(c)) {
@@ -2398,20 +2967,20 @@ function signatureOf(fn, text) {
2398
2967
  return collapse(text.slice(start, stop?.startIndex ?? params.endIndex));
2399
2968
  }
2400
2969
  function walkFile(file, text, tags) {
2401
- const tree = parser.parse(text);
2970
+ const tree = parser2.parse(text);
2402
2971
  if (!tree) throw new Error(`tree-sitter could not parse ${file}`);
2403
2972
  try {
2404
- return collect(file, text, tags, tree.rootNode);
2973
+ return collect2(file, text, tags, tree.rootNode);
2405
2974
  } finally {
2406
2975
  tree.delete();
2407
2976
  }
2408
2977
  }
2409
- function collect(file, text, tags, root) {
2978
+ function collect2(file, text, tags, root) {
2410
2979
  const lineCount2 = text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
2411
2980
  const fileNode = {
2412
2981
  id: fileId(file),
2413
2982
  kind: "file",
2414
- name: path6.posix.basename(file),
2983
+ name: path7.posix.basename(file),
2415
2984
  file,
2416
2985
  range: { startLine: 1, endLine: Math.max(1, lineCount2) },
2417
2986
  bodyHash: bodyHash(text),
@@ -2450,7 +3019,7 @@ function collect(file, text, tags, root) {
2450
3019
  q = `${qname}@${kind}`;
2451
3020
  for (let n = 2; out.byQname.has(q); n++) q = `${qname}@${kind}${n}`;
2452
3021
  }
2453
- const docs = leadingComments(stmt);
3022
+ const docs = leadingComments2(stmt);
2454
3023
  const node = {
2455
3024
  id: nodeId(file, q),
2456
3025
  kind,
@@ -2790,7 +3359,7 @@ function parseAllFiles(root, files, tagsFor2, cacheRepo) {
2790
3359
  const nextCache = {};
2791
3360
  const parsed = /* @__PURE__ */ new Map();
2792
3361
  for (const file of [...files].sort()) {
2793
- const text = fs6.readFileSync(path6.join(root, file), "utf8");
3362
+ const text = fs7.readFileSync(path7.join(root, file), "utf8");
2794
3363
  const hash = createHash4("sha256").update(text).digest("hex");
2795
3364
  const cached = onDisk[file];
2796
3365
  const lf = cached?.hash === hash ? deserializeLuauFile(cached.data) : walkFile(file, text, tagsFor2(file));
@@ -2800,7 +3369,7 @@ function parseAllFiles(root, files, tagsFor2, cacheRepo) {
2800
3369
  if (useCache) writeJsonCacheAtomic(cacheFile, nextCache);
2801
3370
  return parsed;
2802
3371
  }
2803
- function buildGraph(parsed, graph) {
3372
+ function buildGraph2(parsed, graph) {
2804
3373
  if (!graph && !warned) {
2805
3374
  warned = true;
2806
3375
  console.warn("topo: luau-lsp not found or failed; Luau imports resolve relative requires only");
@@ -2826,12 +3395,12 @@ function buildGraph(parsed, graph) {
2826
3395
  function indexLuau(root, files, tagsFor2, cacheRepo = root) {
2827
3396
  const parsed = parseAllFiles(root, files, tagsFor2, cacheRepo);
2828
3397
  const graph = requireGraph(root, [...parsed.keys()]);
2829
- return buildGraph(parsed, graph);
3398
+ return buildGraph2(parsed, graph);
2830
3399
  }
2831
3400
  function indexLuauState(root, files, tagsFor2, cacheRepo = root) {
2832
3401
  const parsed = parseAllFiles(root, files, tagsFor2, cacheRepo);
2833
3402
  const graph = requireGraph(root, [...parsed.keys()]);
2834
- return { graph: buildGraph(parsed, graph), state: { parsed, requireGraph: graph } };
3403
+ return { graph: buildGraph2(parsed, graph), state: { parsed, requireGraph: graph } };
2835
3404
  }
2836
3405
  function updateLuau(root, files, changedFiles, tagsFor2, prev, forceRequireGraph = false) {
2837
3406
  const fileSet = new Set(files);
@@ -2845,13 +3414,13 @@ function updateLuau(root, files, changedFiles, tagsFor2, prev, forceRequireGraph
2845
3414
  parsed.set(f, old);
2846
3415
  continue;
2847
3416
  }
2848
- const text = fs6.readFileSync(path6.join(root, f), "utf8");
3417
+ const text = fs7.readFileSync(path7.join(root, f), "utf8");
2849
3418
  const fresh = walkFile(f, text, tagsFor2(f));
2850
3419
  if (!old || !sameSpecs(old.requires, fresh.requires)) requiresChanged = true;
2851
3420
  parsed.set(f, fresh);
2852
3421
  }
2853
3422
  const graph = requiresChanged ? requireGraph(root, files) : prev.requireGraph;
2854
- return { graph: buildGraph(parsed, graph), state: { parsed, requireGraph: graph } };
3423
+ return { graph: buildGraph2(parsed, graph), state: { parsed, requireGraph: graph } };
2855
3424
  }
2856
3425
  function resolveRef2(f, r, files) {
2857
3426
  if (r.base === void 0) {
@@ -2861,14 +3430,14 @@ function resolveRef2(f, r, files) {
2861
3430
  const req = r.binding?.req;
2862
3431
  if (req) {
2863
3432
  const to2 = !r.base.includes(".") && req.target ? files.get(req.target)?.exports.get(r.name) : void 0;
2864
- return to2 && { to: to2, how: CROSS };
3433
+ return to2 && { to: to2, how: CROSS2 };
2865
3434
  }
2866
3435
  const self = r.binding?.selfOf;
2867
3436
  const table = self ? r.base.replace(/^self\b/, self) : r.base;
2868
3437
  const to = f.byQname.get(`${table}.${r.name}`);
2869
3438
  return to && to.kind !== "file" ? { to, how: CALL } : void 0;
2870
3439
  }
2871
- var parser, SKIP, CALL, CROSS, NOT_DOC, optDoc2, named2, qnameOf, isRequire, unwrapCast, warned, sameSpecs, same2;
3440
+ var parser2, SKIP, CALL, CROSS2, NOT_DOC, optDoc2, named2, qnameOf, isRequire, unwrapCast, warned, sameSpecs, same2;
2872
3441
  var init_luau = __esm({
2873
3442
  async "packages/engine/src/indexer/luau.ts"() {
2874
3443
  "use strict";
@@ -2879,11 +3448,11 @@ var init_luau = __esm({
2879
3448
  init_luau_requires();
2880
3449
  init_tree_sitter();
2881
3450
  await treeSitterReady;
2882
- parser = new Parser2();
2883
- parser.setLanguage(await Language.load(grammarWasm("tree-sitter-luau")));
3451
+ parser2 = new Parser3();
3452
+ parser2.setLanguage(await Language2.load(grammarWasm("tree-sitter-luau")));
2884
3453
  SKIP = /* @__PURE__ */ new Set(["comment", "type_definition", "number", "true", "false", "nil"]);
2885
3454
  CALL = [["static"], 0.95];
2886
- CROSS = [["inferred"], 0.9];
3455
+ CROSS2 = [["inferred"], 0.9];
2887
3456
  NOT_DOC = /^--!|^--\s*(selene|stylua):|copyright|SPDX-License-Identifier/i;
2888
3457
  optDoc2 = (doc) => doc ? { doc } : {};
2889
3458
  named2 = (n, type) => n.namedChildren.find((c) => c.type === type);
@@ -2897,24 +3466,14 @@ var init_luau = __esm({
2897
3466
  });
2898
3467
 
2899
3468
  // packages/engine/src/indexer/python.ts
2900
- import fs7 from "node:fs";
2901
- import path7 from "node:path";
2902
- import { Language as Language2, Parser as Parser3 } from "web-tree-sitter";
3469
+ import fs8 from "node:fs";
3470
+ import path8 from "node:path";
3471
+ import { Language as Language3, Parser as Parser4 } from "web-tree-sitter";
2903
3472
  function docstring(body) {
2904
3473
  const first = body?.namedChildren[0];
2905
3474
  const str = first?.type === "expression_statement" ? first.namedChildren[0] : void 0;
2906
3475
  return str?.type === "string" ? stripQuotes(str.text) : void 0;
2907
3476
  }
2908
- function leadingComments2(stmt) {
2909
- const out = [];
2910
- let at = stmt;
2911
- for (let p = stmt.previousNamedSibling; p?.type === "comment"; p = p.previousNamedSibling) {
2912
- if (p.endPosition.row < at.startPosition.row - 1) break;
2913
- out.unshift(p);
2914
- at = p;
2915
- }
2916
- return out;
2917
- }
2918
3477
  function dotted(n) {
2919
3478
  if (!n) return void 0;
2920
3479
  if (n.type === "identifier") return [n.text];
@@ -2923,13 +3482,13 @@ function dotted(n) {
2923
3482
  const attr = n.childForFieldName("attribute");
2924
3483
  return head && attr ? [...head, attr.text] : void 0;
2925
3484
  }
2926
- function collect2(file, text, tags, root) {
3485
+ function collect3(file, text, tags, root) {
2927
3486
  const lineCount2 = text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
2928
3487
  const fileDoc = docstring(root);
2929
3488
  const fileNode = {
2930
3489
  id: fileId(file),
2931
3490
  kind: "file",
2932
- name: path7.posix.basename(file),
3491
+ name: path8.posix.basename(file),
2933
3492
  file,
2934
3493
  range: { startLine: 1, endLine: Math.max(1, lineCount2) },
2935
3494
  ...fileDoc && { doc: fileDoc.slice(0, DOC_CAP) },
@@ -2956,7 +3515,7 @@ function collect2(file, text, tags, root) {
2956
3515
  const base = parent.kind === "file" ? name : `${qnameOf2(parent)}.${name}`;
2957
3516
  let q = base;
2958
3517
  for (let i = 2; out.byQname.has(q); i++) q = `${base}@${kind}${i === 2 ? "" : i}`;
2959
- const comments2 = leadingComments2(outer);
3518
+ const comments2 = leadingComments(outer);
2960
3519
  const doc = docstring(def.childForFieldName("body")) ?? comments2.map((c) => c.text.replace(/^#\s?/, "")).join("\n");
2961
3520
  const params = def.childForFieldName(isClass ? "superclasses" : "parameters")?.text ?? "";
2962
3521
  const ret = def.childForFieldName("return_type")?.text;
@@ -3029,7 +3588,7 @@ function collect2(file, text, tags, root) {
3029
3588
  });
3030
3589
  }
3031
3590
  }
3032
- walk2(c, block2, fnScopes, classQ, inLoop || LOOPS.has(c.type));
3591
+ walk2(c, block2, fnScopes, classQ, inLoop || LOOPS2.has(c.type));
3033
3592
  }
3034
3593
  };
3035
3594
  walk2(root, fileNode, [], void 0, false);
@@ -3067,9 +3626,9 @@ function importsOf(stmt) {
3067
3626
  function importRoots(files) {
3068
3627
  const roots = /* @__PURE__ */ new Set(["", "src"]);
3069
3628
  for (const f of files) {
3070
- if (path7.posix.basename(f) !== "__init__.py") continue;
3071
- const parent = path7.posix.dirname(path7.posix.dirname(f));
3072
- if (!files.has(path7.posix.join(parent, "__init__.py"))) roots.add(parent === "." ? "" : parent);
3629
+ if (path8.posix.basename(f) !== "__init__.py") continue;
3630
+ const parent = path8.posix.dirname(path8.posix.dirname(f));
3631
+ if (!files.has(path8.posix.join(parent, "__init__.py"))) roots.add(parent === "." ? "" : parent);
3073
3632
  }
3074
3633
  return [...roots];
3075
3634
  }
@@ -3077,15 +3636,15 @@ function moduleFile(files, roots, from, module, level) {
3077
3636
  const parts = module ? module.split(".") : [];
3078
3637
  let bases;
3079
3638
  if (level > 0) {
3080
- let dir = path7.posix.dirname(from);
3081
- for (let i = 1; i < level; i++) dir = path7.posix.dirname(dir);
3639
+ let dir = path8.posix.dirname(from);
3640
+ for (let i = 1; i < level; i++) dir = path8.posix.dirname(dir);
3082
3641
  bases = [dir];
3083
3642
  } else {
3084
- bases = [...roots, path7.posix.dirname(from)];
3643
+ bases = [...roots, path8.posix.dirname(from)];
3085
3644
  }
3086
3645
  for (const b of bases) {
3087
- const p = path7.posix.join(b === "." ? "" : b, ...parts);
3088
- for (const f of [`${p}.py`, path7.posix.join(p, "__init__.py")]) if (files.has(f)) return f;
3646
+ const p = path8.posix.join(b === "." ? "" : b, ...parts);
3647
+ for (const f of [`${p}.py`, path8.posix.join(p, "__init__.py")]) if (files.has(f)) return f;
3089
3648
  }
3090
3649
  return void 0;
3091
3650
  }
@@ -3119,7 +3678,7 @@ function bindingsOf(f, files, roots, add) {
3119
3678
  }
3120
3679
  return binds;
3121
3680
  }
3122
- function buildGraph2(parsed) {
3681
+ function buildGraph3(parsed) {
3123
3682
  const byFile = new Map(parsed.map((f) => [f.file, f]));
3124
3683
  const files = new Set(byFile.keys());
3125
3684
  const roots = importRoots(files);
@@ -3131,32 +3690,32 @@ function buildGraph2(parsed) {
3131
3690
  roots,
3132
3691
  (to, line) => add(fileId(f.file), "imports", fileId(to), ["static"], 1, { line, inLoop: false })
3133
3692
  );
3134
- const inModule = (file, path26) => byFile.get(file)?.byQname.get(path26.join("."));
3693
+ const inModule = (file, path27) => byFile.get(file)?.byQname.get(path27.join("."));
3135
3694
  const resolve2 = (callee, fnScopes, classQ) => {
3136
3695
  const [head, ...rest] = callee;
3137
3696
  if (!head) return void 0;
3138
3697
  if ((head === "self" || head === "cls") && classQ && rest.length === 1) {
3139
3698
  const hit = f.byQname.get(`${classQ}.${rest[0]}`);
3140
- return hit && { node: hit, how: STATIC };
3699
+ return hit && { node: hit, how: STATIC2 };
3141
3700
  }
3142
3701
  if (rest.length === 0) {
3143
3702
  for (const scope of [...fnScopes].reverse()) {
3144
3703
  const hit = f.byQname.get(`${scope}.${head}`);
3145
- if (hit) return { node: hit, how: STATIC };
3704
+ if (hit) return { node: hit, how: STATIC2 };
3146
3705
  }
3147
3706
  }
3148
3707
  const local = f.byQname.get(callee.join("."));
3149
- if (local) return { node: local, how: STATIC };
3708
+ if (local) return { node: local, how: STATIC2 };
3150
3709
  for (let i = callee.length - 1; i >= 1; i--) {
3151
3710
  const b2 = binds.get(callee.slice(0, i).join("."));
3152
3711
  if (!b2) continue;
3153
3712
  const hit = inModule(b2.module, [...b2.member ? [b2.member] : [], ...callee.slice(i)]);
3154
- return hit && { node: hit, how: CROSS2 };
3713
+ return hit && { node: hit, how: CROSS3 };
3155
3714
  }
3156
3715
  const b = binds.get(head);
3157
3716
  if (b?.member && rest.length === 0) {
3158
3717
  const hit = inModule(b.module, [b.member]);
3159
- return hit && { node: hit, how: CROSS2 };
3718
+ return hit && { node: hit, how: CROSS3 };
3160
3719
  }
3161
3720
  return void 0;
3162
3721
  };
@@ -3175,18 +3734,18 @@ function buildGraph2(parsed) {
3175
3734
  function indexPython(root, files, tagsFor2) {
3176
3735
  const parsed = [];
3177
3736
  for (const file of [...files].sort()) {
3178
- const text = fs7.readFileSync(path7.join(root, file), "utf8");
3179
- const tree = parser2.parse(text);
3737
+ const text = fs8.readFileSync(path8.join(root, file), "utf8");
3738
+ const tree = parser3.parse(text);
3180
3739
  if (!tree) continue;
3181
3740
  try {
3182
- parsed.push(collect2(file, text, tagsFor2(file), tree.rootNode));
3741
+ parsed.push(collect3(file, text, tagsFor2(file), tree.rootNode));
3183
3742
  } finally {
3184
3743
  tree.delete();
3185
3744
  }
3186
3745
  }
3187
- return buildGraph2(parsed);
3746
+ return buildGraph3(parsed);
3188
3747
  }
3189
- var parser2, STATIC, CROSS2, LOOPS, stripQuotes;
3748
+ var parser3, STATIC2, CROSS3, LOOPS2, stripQuotes;
3190
3749
  var init_python = __esm({
3191
3750
  async "packages/engine/src/indexer/python.ts"() {
3192
3751
  "use strict";
@@ -3195,23 +3754,23 @@ var init_python = __esm({
3195
3754
  init_edges();
3196
3755
  init_tree_sitter();
3197
3756
  await treeSitterReady;
3198
- parser2 = new Parser3();
3199
- parser2.setLanguage(await Language2.load(grammarWasm("tree-sitter-python")));
3200
- STATIC = [["static"], 0.95];
3201
- CROSS2 = [["inferred"], 0.9];
3202
- LOOPS = /* @__PURE__ */ new Set(["for_statement", "while_statement", "list_comprehension", "for_in_clause"]);
3757
+ parser3 = new Parser4();
3758
+ parser3.setLanguage(await Language3.load(grammarWasm("tree-sitter-python")));
3759
+ STATIC2 = [["static"], 0.95];
3760
+ CROSS3 = [["inferred"], 0.9];
3761
+ LOOPS2 = /* @__PURE__ */ new Set(["for_statement", "while_statement", "list_comprehension", "for_in_clause"]);
3203
3762
  stripQuotes = (s) => s.replace(/^[rbuf]*("""|'''|"|')/i, "").replace(/("""|'''|"|')$/, "").trim();
3204
3763
  }
3205
3764
  });
3206
3765
 
3207
3766
  // packages/engine/src/indexer/rust.ts
3208
- import fs8 from "node:fs";
3209
- import path8 from "node:path";
3210
- import { Language as Language3, Parser as Parser4 } from "web-tree-sitter";
3767
+ import fs9 from "node:fs";
3768
+ import path9 from "node:path";
3769
+ import { Language as Language4, Parser as Parser5 } from "web-tree-sitter";
3211
3770
  function moduleOf(root, file) {
3212
- for (let dir = path8.posix.dirname(file); ; dir = path8.posix.dirname(dir)) {
3213
- if (fs8.existsSync(path8.join(root, dir, "Cargo.toml"))) {
3214
- const rel = path8.posix.relative(path8.posix.join(dir, "src"), file);
3771
+ for (let dir = path9.posix.dirname(file); ; dir = path9.posix.dirname(dir)) {
3772
+ if (fs9.existsSync(path9.join(root, dir, "Cargo.toml"))) {
3773
+ const rel = path9.posix.relative(path9.posix.join(dir, "src"), file);
3215
3774
  const bin = rel === "main.rs" || rel.startsWith("bin/") || rel.startsWith("..");
3216
3775
  if (bin) return { crate: file, segs: [], lib: dir };
3217
3776
  const segs = rel.replace(/\.rs$/, "").split("/");
@@ -3226,7 +3785,7 @@ function moduleOf(root, file) {
3226
3785
  function libName(root, crate) {
3227
3786
  let toml;
3228
3787
  try {
3229
- toml = fs8.readFileSync(path8.join(root, crate, "Cargo.toml"), "utf8");
3788
+ toml = fs9.readFileSync(path9.join(root, crate, "Cargo.toml"), "utf8");
3230
3789
  } catch {
3231
3790
  return void 0;
3232
3791
  }
@@ -3272,12 +3831,12 @@ function pathSegs(n) {
3272
3831
  const h = pathSegs(head);
3273
3832
  return h ? [...h, name] : void 0;
3274
3833
  }
3275
- function collect3(root, file, text, tags, tree, ix) {
3834
+ function collect4(root, file, text, tags, tree, ix) {
3276
3835
  const lineCount2 = text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
3277
3836
  const fileNode = {
3278
3837
  id: fileId(file),
3279
3838
  kind: "file",
3280
- name: path8.posix.basename(file),
3839
+ name: path9.posix.basename(file),
3281
3840
  file,
3282
3841
  range: { startLine: 1, endLine: Math.max(1, lineCount2) },
3283
3842
  bodyHash: bodyHash(text),
@@ -3419,7 +3978,7 @@ function collect3(root, file, text, tags, tree, ix) {
3419
3978
  });
3420
3979
  }
3421
3980
  }
3422
- walk2(c, block2, mod, prefix, implType, inLoop || LOOPS2.has(c.type), testy);
3981
+ walk2(c, block2, mod, prefix, implType, inLoop || LOOPS3.has(c.type), testy);
3423
3982
  }
3424
3983
  };
3425
3984
  walk2(tree, fileNode, fileMod, "", void 0, false, false);
@@ -3484,17 +4043,17 @@ function indexRust(root, files, tagsFor2) {
3484
4043
  };
3485
4044
  const parsed = [];
3486
4045
  for (const file of [...files].sort()) {
3487
- const text = fs8.readFileSync(path8.join(root, file), "utf8");
3488
- const tree = parser3.parse(text);
4046
+ const text = fs9.readFileSync(path9.join(root, file), "utf8");
4047
+ const tree = parser4.parse(text);
3489
4048
  if (!tree) continue;
3490
4049
  try {
3491
- parsed.push(collect3(root, file, text, tagsFor2(file), tree.rootNode, ix));
4050
+ parsed.push(collect4(root, file, text, tagsFor2(file), tree.rootNode, ix));
3492
4051
  } finally {
3493
4052
  tree.delete();
3494
4053
  }
3495
4054
  }
3496
4055
  const { edges, add } = edgeSink();
3497
- const how = (from, to) => ix.fileOf.get(to) === from.split("#")[0] ? STATIC2 : CROSS3;
4056
+ const how = (from, to) => ix.fileOf.get(to) === from.split("#")[0] ? STATIC3 : CROSS4;
3498
4057
  for (const f of parsed) {
3499
4058
  for (const c of f.calls) {
3500
4059
  const [head, ...rest] = c.callee;
@@ -3528,7 +4087,7 @@ function indexRust(root, files, tagsFor2) {
3528
4087
  const commands = new Map(parsed.flatMap((f) => f.commands.map((c) => [c.name, c.id])));
3529
4088
  return { nodes: parsed.flatMap((f) => f.nodes), edges: [...edges.values()], commands };
3530
4089
  }
3531
- var parser3, STATIC2, CROSS3, LOOPS2, TYPE_ITEMS, modKey;
4090
+ var parser4, STATIC3, CROSS4, LOOPS3, TYPE_ITEMS, modKey;
3532
4091
  var init_rust = __esm({
3533
4092
  async "packages/engine/src/indexer/rust.ts"() {
3534
4093
  "use strict";
@@ -3537,11 +4096,11 @@ var init_rust = __esm({
3537
4096
  init_edges();
3538
4097
  init_tree_sitter();
3539
4098
  await treeSitterReady;
3540
- parser3 = new Parser4();
3541
- parser3.setLanguage(await Language3.load(grammarWasm("tree-sitter-rust")));
3542
- STATIC2 = [["static"], 0.95];
3543
- CROSS3 = [["inferred"], 0.9];
3544
- LOOPS2 = /* @__PURE__ */ new Set(["for_expression", "while_expression", "loop_expression"]);
4099
+ parser4 = new Parser5();
4100
+ parser4.setLanguage(await Language4.load(grammarWasm("tree-sitter-rust")));
4101
+ STATIC3 = [["static"], 0.95];
4102
+ CROSS4 = [["inferred"], 0.9];
4103
+ LOOPS3 = /* @__PURE__ */ new Set(["for_expression", "while_expression", "loop_expression"]);
3545
4104
  TYPE_ITEMS = {
3546
4105
  struct_item: "class",
3547
4106
  enum_item: "class",
@@ -3554,13 +4113,13 @@ var init_rust = __esm({
3554
4113
  });
3555
4114
 
3556
4115
  // packages/engine/src/indexer/tauri.ts
3557
- import fs9 from "node:fs";
3558
- import path9 from "node:path";
4116
+ import fs10 from "node:fs";
4117
+ import path10 from "node:path";
3559
4118
  function linkTauri(root, files, graph, commands) {
3560
4119
  if (commands.size === 0) return [];
3561
4120
  const { edges, add } = edgeSink();
3562
4121
  for (const file of files) {
3563
- const text = fs9.readFileSync(path9.join(root, file), "utf8");
4122
+ const text = fs10.readFileSync(path10.join(root, file), "utf8");
3564
4123
  if (!text.includes("invoke")) continue;
3565
4124
  const blocks = graph.nodes.filter((n) => n.file === file && n.kind !== "file");
3566
4125
  for (const m of text.matchAll(INVOKE)) {
@@ -3589,18 +4148,18 @@ var init_tauri = __esm({
3589
4148
  });
3590
4149
 
3591
4150
  // packages/engine/src/indexer/index.ts
3592
- import fs10 from "node:fs";
3593
- import path10 from "node:path";
4151
+ import fs11 from "node:fs";
4152
+ import path11 from "node:path";
3594
4153
  import { API } from "typescript/unstable/sync";
3595
4154
  function isRepoSource(root, abs) {
3596
- const rel = path10.relative(root, abs);
3597
- return !rel.startsWith("..") && !path10.isAbsolute(rel) && SOURCE_EXT.test(rel) && !DECL_FILE.test(rel) && !toPosix(rel).split("/").includes("node_modules");
4155
+ const rel = path11.relative(root, abs);
4156
+ return !rel.startsWith("..") && !path11.isAbsolute(rel) && SOURCE_EXT.test(rel) && !DECL_FILE.test(rel) && !toPosix(rel).split("/").includes("node_modules");
3598
4157
  }
3599
4158
  function walk(dir, skip, want, out = []) {
3600
- for (const e of fs10.readdirSync(dir, { withFileTypes: true })) {
3601
- const full = path10.join(dir, e.name);
4159
+ for (const e of fs11.readdirSync(dir, { withFileTypes: true })) {
4160
+ const full = path11.join(dir, e.name);
3602
4161
  if (e.isDirectory()) {
3603
- const nested = fs10.existsSync(path10.join(full, ".git"));
4162
+ const nested = fs11.existsSync(path11.join(full, ".git"));
3604
4163
  if (!skip.has(e.name) && !e.name.startsWith(".") && !nested) walk(full, skip, want, out);
3605
4164
  } else if (e.isFile() && want(e.name)) {
3606
4165
  out.push(full);
@@ -3609,22 +4168,22 @@ function walk(dir, skip, want, out = []) {
3609
4168
  return out;
3610
4169
  }
3611
4170
  function references(config) {
3612
- if (!fs10.existsSync(config)) return [];
4171
+ if (!fs11.existsSync(config)) return [];
3613
4172
  try {
3614
- const json2 = fs10.readFileSync(config, "utf8").replace(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, str) => str ?? "").replace(/,(\s*[}\]])/g, "$1");
4173
+ const json2 = fs11.readFileSync(config, "utf8").replace(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, str) => str ?? "").replace(/,(\s*[}\]])/g, "$1");
3615
4174
  const refs = JSON.parse(json2).references ?? [];
3616
4175
  return refs.map((r) => {
3617
- const p = path10.resolve(path10.dirname(config), r.path);
3618
- return fs10.existsSync(p) && fs10.statSync(p).isDirectory() ? path10.join(p, "tsconfig.json") : p;
3619
- }).filter((p) => fs10.existsSync(p));
4176
+ const p = path11.resolve(path11.dirname(config), r.path);
4177
+ return fs11.existsSync(p) && fs11.statSync(p).isDirectory() ? path11.join(p, "tsconfig.json") : p;
4178
+ }).filter((p) => fs11.existsSync(p));
3620
4179
  } catch {
3621
4180
  return [];
3622
4181
  }
3623
4182
  }
3624
4183
  function discover(api, root, sources) {
3625
- const listed = (c) => api.parseConfigFile(c).fileNames.filter((f) => isRepoSource(root, f)).map((f) => path10.resolve(f));
3626
- const rootConfig = path10.join(root, "tsconfig.json");
3627
- if (fs10.existsSync(rootConfig)) {
4184
+ const listed = (c) => api.parseConfigFile(c).fileNames.filter((f) => isRepoSource(root, f)).map((f) => path11.resolve(f));
4185
+ const rootConfig = path11.join(root, "tsconfig.json");
4186
+ if (fs11.existsSync(rootConfig)) {
3628
4187
  const files = listed(rootConfig);
3629
4188
  if (files.length) return { units: [{ config: rootConfig, files }], loose: [] };
3630
4189
  }
@@ -3632,7 +4191,7 @@ function discover(api, root, sources) {
3632
4191
  const configs = [.../* @__PURE__ */ new Set([...found, ...references(rootConfig)])].sort();
3633
4192
  const projects = configs.map((config) => ({
3634
4193
  config,
3635
- dir: path10.dirname(config) + path10.sep,
4194
+ dir: path11.dirname(config) + path11.sep,
3636
4195
  listed: new Set(listed(config)),
3637
4196
  files: []
3638
4197
  })).filter((p) => p.listed.size).sort((a, b) => b.dir.length - a.dir.length);
@@ -3654,56 +4213,52 @@ function tagsFor(file) {
3654
4213
  return tags;
3655
4214
  }
3656
4215
  function discoverSourceFiles(rootDir) {
3657
- const root = path10.resolve(rootDir);
4216
+ const root = path11.resolve(rootDir);
3658
4217
  const found = walk(
3659
4218
  root,
3660
4219
  SKIP_DIRS,
3661
- (n) => isSourceName(n) || LUAU_EXT.test(n) || PY_EXT.test(n) || RS_EXT.test(n)
4220
+ (n) => isSourceName(n) || LUAU_EXT.test(n) || PY_EXT.test(n) || RS_EXT.test(n) || CPP_EXT.test(n)
3662
4221
  );
3663
- const ts4 = found.filter((f) => isSourceName(path10.basename(f)));
3664
- const rel = found.map((f) => toPosix(path10.relative(root, f)));
4222
+ const ts4 = found.filter((f) => isSourceName(path11.basename(f)));
4223
+ const rel = found.map((f) => toPosix(path11.relative(root, f)));
3665
4224
  return {
3666
4225
  ts: ts4,
3667
4226
  luau: rel.filter(isLuauSourcePath),
3668
4227
  py: rel.filter(isPySourcePath),
3669
- rs: rel.filter(isRsSourcePath)
4228
+ rs: rel.filter(isRsSourcePath),
4229
+ cpp: rel.filter(isCppSourcePath)
3670
4230
  };
3671
4231
  }
3672
4232
  function mergeGraphs(root, ts4, parts) {
3673
- const graphs = [parts.ts, parts.luau, parts.py, parts.rs].filter((g) => !!g);
4233
+ const graphs = [parts.ts, parts.luau, parts.py, parts.rs, parts.cpp].filter(
4234
+ (g) => !!g
4235
+ );
3674
4236
  const merged = { nodes: graphs.flatMap((g) => g.nodes), edges: graphs.flatMap((g) => g.edges) };
3675
4237
  if (parts.ts && parts.rs?.commands.size) {
3676
- const rel = ts4.map((f) => toPosix(path10.relative(root, f)));
4238
+ const rel = ts4.map((f) => toPosix(path11.relative(root, f)));
3677
4239
  merged.edges.push(...linkTauri(root, rel, parts.ts, parts.rs.commands));
3678
4240
  }
3679
4241
  return merged;
3680
4242
  }
3681
4243
  function indexRepo(rootDir, cacheRepo = rootDir) {
3682
- const root = path10.resolve(rootDir);
3683
- const { ts: ts4, luau, py, rs } = discoverSourceFiles(rootDir);
4244
+ const root = path11.resolve(rootDir);
4245
+ const { ts: ts4, luau, py, rs, cpp } = discoverSourceFiles(rootDir);
3684
4246
  return mergeGraphs(root, ts4, {
3685
4247
  ...ts4.length > 0 && { ts: indexTypeScript(root, ts4) },
3686
4248
  ...luau.length > 0 && { luau: indexLuau(root, luau, tagsFor, cacheRepo) },
3687
4249
  ...py.length > 0 && { py: indexPython(root, py, tagsFor) },
3688
- ...rs.length > 0 && { rs: indexRust(root, rs, tagsFor) }
4250
+ ...rs.length > 0 && { rs: indexRust(root, rs, tagsFor) },
4251
+ ...cpp.length > 0 && { cpp: indexCpp(root, cpp, tagsFor) }
3689
4252
  });
3690
4253
  }
3691
4254
  function filesRead(rootDir) {
3692
- const root = path10.resolve(rootDir);
3693
- const found = walk(
3694
- root,
3695
- SKIP_DIRS,
3696
- (n) => isSourceName(n) || LUAU_EXT.test(n) || FINGERPRINT_CONFIG_NAMES.has(n)
3697
- );
3698
- return found.filter((f) => {
3699
- const base = path10.basename(f);
3700
- if (isSourceName(base) || FINGERPRINT_CONFIG_NAMES.has(base)) return true;
3701
- const rel = toPosix(path10.relative(root, f));
3702
- return !LUAU_DECL.test(rel) && !rel.split("/").some((s) => LUAU_SKIP_DIRS.has(s));
3703
- });
4255
+ const root = path11.resolve(rootDir);
4256
+ const { ts: ts4, luau, py, rs, cpp } = discoverSourceFiles(root);
4257
+ const rel = [...luau, ...py, ...rs, ...cpp].map((f) => path11.join(root, f));
4258
+ return [...ts4, ...rel, ...walk(root, SKIP_DIRS, (n) => FINGERPRINT_CONFIG_NAMES.has(n))];
3704
4259
  }
3705
4260
  function openTs(root, sources) {
3706
- const virtualConfig = path10.join(root, VIRTUAL_CONFIG);
4261
+ const virtualConfig = path11.join(root, VIRTUAL_CONFIG);
3707
4262
  const virtualBox = { text: "" };
3708
4263
  const api = new API({
3709
4264
  cwd: root,
@@ -3745,7 +4300,7 @@ function collectGraph(root, open) {
3745
4300
  });
3746
4301
  for (const { ctx, sfs } of projects) {
3747
4302
  for (const sf of sfs) {
3748
- const file = toPosix(path10.relative(root, sf.fileName));
4303
+ const file = toPosix(path11.relative(root, sf.fileName));
3749
4304
  collectFile(ctx, sf, file, tagsFor(file));
3750
4305
  }
3751
4306
  }
@@ -3781,11 +4336,12 @@ function reopenTsLive(root, sources, prevOpen) {
3781
4336
  prevOpen.api.close();
3782
4337
  return indexTsLive(root, sources);
3783
4338
  }
3784
- var SOURCE_EXT, DECL_FILE, LUAU_EXT, LUAU_DECL, LUAU_SKIP_DIRS, PY_EXT, RS_EXT, PY_SKIP_DIRS, SKIP_DIRS, SKIP_CONFIG_DIRS, UTIL_SEGMENTS, DEFAULT_OPTIONS, VIRTUAL_CONFIG, toPosix, isTsSourcePath, isLuauSourcePath, isPySourcePath, isRsSourcePath, isSourceName, FINGERPRINT_CONFIG_NAMES;
4339
+ var SOURCE_EXT, DECL_FILE, LUAU_EXT, LUAU_DECL, LUAU_SKIP_DIRS, PY_EXT, RS_EXT, CPP_EXT, CPP_SKIP_DIRS, PY_SKIP_DIRS, SKIP_DIRS, SKIP_CONFIG_DIRS, UTIL_SEGMENTS, DEFAULT_OPTIONS, VIRTUAL_CONFIG, toPosix, isTsSourcePath, isLuauSourcePath, isPySourcePath, isRsSourcePath, isCppSourcePath, isSourceName, FINGERPRINT_CONFIG_NAMES;
3785
4340
  var init_indexer = __esm({
3786
4341
  async "packages/engine/src/indexer/index.ts"() {
3787
4342
  "use strict";
3788
4343
  init_blocks();
4344
+ await init_cpp();
3789
4345
  init_edges();
3790
4346
  await init_luau();
3791
4347
  await init_python();
@@ -3798,6 +4354,16 @@ var init_indexer = __esm({
3798
4354
  LUAU_SKIP_DIRS = /* @__PURE__ */ new Set(["vendor", "Packages", "ServerPackages", "DevPackages"]);
3799
4355
  PY_EXT = /\.py$/;
3800
4356
  RS_EXT = /\.rs$/;
4357
+ CPP_EXT = /\.(c|h|cc|cpp|cxx|hpp|hh|hxx)$/;
4358
+ CPP_SKIP_DIRS = /* @__PURE__ */ new Set([
4359
+ "build",
4360
+ "out",
4361
+ "third_party",
4362
+ "external",
4363
+ "extern",
4364
+ "vendor",
4365
+ "deps"
4366
+ ]);
3801
4367
  PY_SKIP_DIRS = /* @__PURE__ */ new Set(["venv", "env", "site-packages", "__pycache__", "build"]);
3802
4368
  SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist"]);
3803
4369
  SKIP_CONFIG_DIRS = /* @__PURE__ */ new Set([...SKIP_DIRS, "build", "coverage", "fixtures"]);
@@ -3815,25 +4381,29 @@ var init_indexer = __esm({
3815
4381
  types: ["node"]
3816
4382
  };
3817
4383
  VIRTUAL_CONFIG = "__topo__.tsconfig.json";
3818
- toPosix = (p) => p.split(path10.sep).join("/");
3819
- isTsSourcePath = (rel) => isSourceName(path10.posix.basename(rel));
4384
+ toPosix = (p) => p.split(path11.sep).join("/");
4385
+ isTsSourcePath = (rel) => isSourceName(path11.posix.basename(rel));
3820
4386
  isLuauSourcePath = (rel) => LUAU_EXT.test(rel) && !LUAU_DECL.test(rel) && !rel.split("/").some((s) => LUAU_SKIP_DIRS.has(s));
3821
4387
  isPySourcePath = (rel) => PY_EXT.test(rel) && !rel.split("/").some((s) => PY_SKIP_DIRS.has(s));
3822
4388
  isRsSourcePath = (rel) => RS_EXT.test(rel) && !rel.split("/").includes("target");
4389
+ isCppSourcePath = (rel) => CPP_EXT.test(rel) && !rel.split("/").some((s) => CPP_SKIP_DIRS.has(s) || s.startsWith("cmake-build-"));
3823
4390
  isSourceName = (n) => SOURCE_EXT.test(n) && !DECL_FILE.test(n);
3824
4391
  FINGERPRINT_CONFIG_NAMES = /* @__PURE__ */ new Set([
3825
4392
  "tsconfig.json",
3826
4393
  "package.json",
3827
4394
  ".luaurc",
3828
4395
  "sourcemap.json",
3829
- "default.project.json"
4396
+ "default.project.json",
4397
+ "Cargo.toml",
4398
+ "Makefile",
4399
+ "CMakeLists.txt"
3830
4400
  ]);
3831
4401
  }
3832
4402
  });
3833
4403
 
3834
4404
  // packages/engine/src/lenses.ts
3835
- import fs11 from "node:fs";
3836
- import path11 from "node:path";
4405
+ import fs12 from "node:fs";
4406
+ import path12 from "node:path";
3837
4407
  function lensProblem(l) {
3838
4408
  if (typeof l.name !== "string" || !l.name) return "missing name";
3839
4409
  if (l.mode !== void 0 && l.mode !== "review" && l.mode !== "flow") return "bad mode";
@@ -3850,10 +4420,10 @@ function lensProblem(l) {
3850
4420
  return void 0;
3851
4421
  }
3852
4422
  function loadLenses(dir, warn = console.warn) {
3853
- const file = path11.join(dir, ".topo", "lenses.json");
4423
+ const file = path12.join(dir, ".topo", "lenses.json");
3854
4424
  let raw;
3855
4425
  try {
3856
- raw = JSON.parse(fs11.readFileSync(file, "utf8"));
4426
+ raw = JSON.parse(fs12.readFileSync(file, "utf8"));
3857
4427
  } catch (e) {
3858
4428
  if (e.code !== "ENOENT") warn(`topo: ignoring ${file}: ${e}`);
3859
4429
  return BUILTIN_LENSES;
@@ -3950,15 +4520,21 @@ var init_lenses = __esm({
3950
4520
  });
3951
4521
 
3952
4522
  // packages/engine/src/live.ts
3953
- import path12 from "node:path";
4523
+ import path13 from "node:path";
3954
4524
  function toRel(root, f) {
3955
- const abs = path12.isAbsolute(f) ? f : path12.resolve(root, f);
3956
- return toPosix(path12.relative(root, abs));
4525
+ const abs = path13.isAbsolute(f) ? f : path13.resolve(root, f);
4526
+ return toPosix(path13.relative(root, abs));
3957
4527
  }
3958
4528
  function createLiveIndexer(root, opts) {
3959
- const rootAbs = path12.resolve(root);
4529
+ const rootAbs = path13.resolve(root);
3960
4530
  const cacheRepo = opts?.cacheRepo ?? rootAbs;
3961
- let { ts: tsFiles, luau: luauFiles, py: pyFiles, rs: rsFiles } = discoverSourceFiles(rootAbs);
4531
+ let {
4532
+ ts: tsFiles,
4533
+ luau: luauFiles,
4534
+ py: pyFiles,
4535
+ rs: rsFiles,
4536
+ cpp: cppFiles
4537
+ } = discoverSourceFiles(rootAbs);
3962
4538
  let tsState;
3963
4539
  let tsGraph = { nodes: [], edges: [] };
3964
4540
  if (tsFiles.length) {
@@ -3975,17 +4551,19 @@ function createLiveIndexer(root, opts) {
3975
4551
  }
3976
4552
  let pyGraph = pyFiles.length ? indexPython(rootAbs, pyFiles, tagsFor) : { nodes: [], edges: [] };
3977
4553
  let rsGraph = rsFiles.length ? indexRust(rootAbs, rsFiles, tagsFor) : void 0;
4554
+ let cppGraph = cppFiles.length ? indexCpp(rootAbs, cppFiles, tagsFor) : { nodes: [], edges: [] };
3978
4555
  const all = () => mergeGraphs(rootAbs, tsFiles, {
3979
4556
  ts: tsGraph,
3980
4557
  luau: luauGraph,
3981
4558
  py: pyGraph,
3982
- ...rsGraph && { rs: rsGraph }
4559
+ ...rsGraph && { rs: rsGraph },
4560
+ cpp: cppGraph
3983
4561
  });
3984
4562
  let current = all();
3985
4563
  function update(changedFiles) {
3986
4564
  const prev = current;
3987
4565
  const rel = [...new Set(changedFiles.map((f) => toRel(rootAbs, f)))];
3988
- const basenames = rel.map((f) => path12.posix.basename(f));
4566
+ const basenames = rel.map((f) => path13.posix.basename(f));
3989
4567
  const tsConfigChanged = basenames.some((b) => TS_CONFIG_NAMES.has(b));
3990
4568
  const luauConfigChanged = basenames.some((b) => LUAU_CONFIG_NAMES.has(b));
3991
4569
  const tsTouched = rel.filter(isTsSourcePath);
@@ -3997,7 +4575,7 @@ function createLiveIndexer(root, opts) {
3997
4575
  if (!tsConfigChanged && sameFileSet && tsState) {
3998
4576
  const r = updateTsLive(
3999
4577
  tsState,
4000
- tsTouched.map((f) => path12.resolve(rootAbs, f))
4578
+ tsTouched.map((f) => path13.resolve(rootAbs, f))
4001
4579
  );
4002
4580
  tsState = r.state;
4003
4581
  tsGraph = r.graph;
@@ -4026,6 +4604,10 @@ function createLiveIndexer(root, opts) {
4026
4604
  rsFiles = discoverSourceFiles(rootAbs).rs;
4027
4605
  rsGraph = rsFiles.length ? indexRust(rootAbs, rsFiles, tagsFor) : void 0;
4028
4606
  }
4607
+ if (rel.some(isCppSourcePath)) {
4608
+ cppFiles = discoverSourceFiles(rootAbs).cpp;
4609
+ cppGraph = indexCpp(rootAbs, cppFiles, tagsFor);
4610
+ }
4029
4611
  current = all();
4030
4612
  return { graph: cloneGraph(current), delta: graphDelta(prev, current) };
4031
4613
  }
@@ -4045,6 +4627,7 @@ var init_live = __esm({
4045
4627
  async "packages/engine/src/live.ts"() {
4046
4628
  "use strict";
4047
4629
  init_delta();
4630
+ await init_cpp();
4048
4631
  await init_indexer();
4049
4632
  await init_luau();
4050
4633
  await init_python();
@@ -4308,7 +4891,7 @@ function build(text, root) {
4308
4891
  }
4309
4892
  function highlightLuau(text) {
4310
4893
  try {
4311
- const tree = parser.parse(text);
4894
+ const tree = parser2.parse(text);
4312
4895
  if (!tree) return [];
4313
4896
  try {
4314
4897
  return build(text, tree.rootNode);
@@ -4529,15 +5112,15 @@ var init_highlight = __esm({
4529
5112
  });
4530
5113
 
4531
5114
  // packages/engine/src/source.ts
4532
- import fs12 from "node:fs";
4533
- import path13 from "node:path";
5115
+ import fs13 from "node:fs";
5116
+ import path14 from "node:path";
4534
5117
  function readInside(dir, file) {
4535
- const root = path13.resolve(dir);
4536
- const abs = path13.resolve(root, file);
4537
- if (!abs.startsWith(root + path13.sep)) return void 0;
5118
+ const root = path14.resolve(dir);
5119
+ const abs = path14.resolve(root, file);
5120
+ if (!abs.startsWith(root + path14.sep)) return void 0;
4538
5121
  try {
4539
- const text = fs12.readFileSync(abs, "utf8");
4540
- const { mtimeMs } = fs12.statSync(abs);
5122
+ const text = fs13.readFileSync(abs, "utf8");
5123
+ const { mtimeMs } = fs13.statSync(abs);
4541
5124
  let hit = tokenCache.get(abs);
4542
5125
  if (hit?.mtimeMs !== mtimeMs) {
4543
5126
  hit = { mtimeMs, tokens: highlight(text, file) };
@@ -4911,6 +5494,7 @@ __export(src_exports, {
4911
5494
  indexRepo: () => indexRepo,
4912
5495
  indexerVersion: () => indexerVersion,
4913
5496
  layerIndex: () => layerIndex,
5497
+ layerMap: () => layerMap,
4914
5498
  layersFile: () => layersFile,
4915
5499
  lensProblem: () => lensProblem,
4916
5500
  loadLenses: () => loadLenses,
@@ -4919,6 +5503,7 @@ __export(src_exports, {
4919
5503
  noCache: () => noCache,
4920
5504
  nodeId: () => nodeId,
4921
5505
  outsideTerritory: () => outsideTerritory,
5506
+ parseLayers: () => parseLayers,
4922
5507
  readLayers: () => readLayers,
4923
5508
  refreshReview: () => refreshReview,
4924
5509
  repoCacheRoot: () => repoCacheRoot,
@@ -4930,7 +5515,8 @@ __export(src_exports, {
4930
5515
  tagHubs: () => tagHubs,
4931
5516
  territoryContext: () => territoryContext,
4932
5517
  toPayload: () => toPayload,
4933
- visibleSubgraph: () => visibleSubgraph
5518
+ visibleSubgraph: () => visibleSubgraph,
5519
+ writeLayers: () => writeLayers
4934
5520
  });
4935
5521
  var init_src = __esm({
4936
5522
  async "packages/engine/src/index.ts"() {
@@ -5141,7 +5727,7 @@ var init_graph = __esm({
5141
5727
  await init_format();
5142
5728
  HELP = `Usage: topo graph [path] [--json] [--no-cache]
5143
5729
 
5144
- Index a codebase (TypeScript, Luau, Python, Rust) and print a summary with likely entry points (or the full graph with --json).
5730
+ Index a codebase (TypeScript, Luau, Python, Rust, C/C++) and print a summary with likely entry points (or the full graph with --json).
5145
5731
 
5146
5732
  Options:
5147
5733
  --json print the full Graph as JSON
@@ -5216,12 +5802,12 @@ Options:
5216
5802
  });
5217
5803
 
5218
5804
  // packages/cli/src/activity.ts
5219
- import path14 from "node:path";
5805
+ import path15 from "node:path";
5220
5806
  function toRepoRelative(repo, file) {
5221
- const abs = path14.isAbsolute(file) ? file : path14.resolve(repo, file);
5222
- const rel = path14.relative(repo, abs);
5223
- if (rel === "" || rel.startsWith("..") || path14.isAbsolute(rel)) return void 0;
5224
- return rel.split(path14.sep).join("/");
5807
+ const abs = path15.isAbsolute(file) ? file : path15.resolve(repo, file);
5808
+ const rel = path15.relative(repo, abs);
5809
+ if (rel === "" || rel.startsWith("..") || path15.isAbsolute(rel)) return void 0;
5810
+ return rel.split(path15.sep).join("/");
5225
5811
  }
5226
5812
  function parseActivity(body, repo, inWorktree) {
5227
5813
  if (!isRecord(body)) return void 0;
@@ -5229,7 +5815,7 @@ function parseActivity(body, repo, inWorktree) {
5229
5815
  if (input) {
5230
5816
  const file = input.file_path ?? input.notebook_path;
5231
5817
  if (typeof file !== "string") return void 0;
5232
- const abs = path14.isAbsolute(file) ? file : path14.resolve(repo, file);
5818
+ const abs = path15.isAbsolute(file) ? file : path15.resolve(repo, file);
5233
5819
  const wt = inWorktree?.(abs);
5234
5820
  const rel = wt?.rel ?? toRepoRelative(repo, file);
5235
5821
  if (!rel) return void 0;
@@ -5330,40 +5916,40 @@ Options:
5330
5916
 
5331
5917
  // packages/cli/src/agents.ts
5332
5918
  import { spawn as nodeSpawn } from "node:child_process";
5333
- import fs13 from "node:fs";
5919
+ import fs14 from "node:fs";
5334
5920
  import os2 from "node:os";
5335
- import path15 from "node:path";
5921
+ import path16 from "node:path";
5336
5922
  function resolveBin(bin) {
5337
5923
  const tryExec = (p) => {
5338
5924
  try {
5339
- fs13.accessSync(p, fs13.constants.X_OK);
5925
+ fs14.accessSync(p, fs14.constants.X_OK);
5340
5926
  return p;
5341
5927
  } catch {
5342
5928
  return void 0;
5343
5929
  }
5344
5930
  };
5345
- if (bin.includes(path15.sep)) return tryExec(bin);
5346
- const dirs = (process.env.PATH ?? "").split(path15.delimiter).filter(Boolean);
5931
+ if (bin.includes(path16.sep)) return tryExec(bin);
5932
+ const dirs = (process.env.PATH ?? "").split(path16.delimiter).filter(Boolean);
5347
5933
  const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD").split(";") : [""];
5348
5934
  for (const dir of dirs) {
5349
5935
  for (const ext of exts) {
5350
- const hit = tryExec(path15.join(dir, bin + ext));
5936
+ const hit = tryExec(path16.join(dir, bin + ext));
5351
5937
  if (hit) return hit;
5352
5938
  }
5353
5939
  }
5354
5940
  return void 0;
5355
5941
  }
5356
5942
  function writeTempJson(prefix, value) {
5357
- const file = path15.join(
5943
+ const file = path16.join(
5358
5944
  os2.tmpdir(),
5359
5945
  `${prefix}-${process.pid}-${Math.random().toString(36).slice(2)}.json`
5360
5946
  );
5361
- fs13.writeFileSync(file, JSON.stringify(value));
5947
+ fs14.writeFileSync(file, JSON.stringify(value));
5362
5948
  return file;
5363
5949
  }
5364
5950
  function rmQuiet(file) {
5365
5951
  try {
5366
- fs13.unlinkSync(file);
5952
+ fs14.unlinkSync(file);
5367
5953
  } catch {
5368
5954
  }
5369
5955
  }
@@ -5397,9 +5983,25 @@ function createAgentManager(opts) {
5397
5983
  const children = /* @__PURE__ */ new Map();
5398
5984
  let runSeq = 0;
5399
5985
  let cmdSeq = 0;
5986
+ let exchanges = [];
5987
+ let session;
5400
5988
  function emitRuns() {
5401
5989
  opts.bus.emit({ type: "runs", runs: [...runs.values()] });
5402
5990
  }
5991
+ function emitExchanges() {
5992
+ opts.bus.emit({ type: "exchanges", exchanges });
5993
+ }
5994
+ function updateExchange(id, patch) {
5995
+ const i = exchanges.findIndex((x2) => x2.id === id);
5996
+ const x = exchanges[i];
5997
+ if (!x) return;
5998
+ exchanges = exchanges.with(i, { ...x, ...patch(x) });
5999
+ emitExchanges();
6000
+ }
6001
+ opts.bus.subscribe((e) => {
6002
+ if ((e.type === "explain" || e.type === "focus" || e.type === "lens") && e.command)
6003
+ updateExchange(e.command, () => ({ view: e }));
6004
+ });
5403
6005
  function setTerritoryStatus(territoryId, patch) {
5404
6006
  const territories = opts.getTerritories();
5405
6007
  const next = territories.map((t) => t.id === territoryId ? { ...t, ...patch } : t);
@@ -5545,27 +6147,30 @@ function createAgentManager(opts) {
5545
6147
  const contextLine = context ? `
5546
6148
 
5547
6149
  Context: ${JSON.stringify(context)}` : "";
6150
+ exchanges = [
6151
+ ...exchanges.slice(-(MAX_EXCHANGES - 1)),
6152
+ { id, text, status: "running", tools: [], at: Date.now() }
6153
+ ];
5548
6154
  if (!resolved) {
5549
6155
  const nodes = findBlocks(opts.getIndex().graph, text, 20);
5550
- opts.bus.emit({ type: "focus", nodeIds: nodes.map((n) => n.id) });
5551
- opts.bus.emit({
5552
- type: "command",
5553
- id,
5554
- status: "done",
5555
- text: "No agent CLI found; showing search results"
5556
- });
6156
+ opts.bus.emit({ type: "focus", nodeIds: nodes.map((n) => n.id), command: id });
6157
+ const line = nodes.length ? "No agent CLI found, so this is a plain search: the matches are on the map." : "No agent CLI found, so this is a plain search, and nothing matched.";
6158
+ opts.bus.emit({ type: "command", id, status: "done", text: line });
6159
+ updateExchange(id, () => ({ status: "done", answer: line }));
5557
6160
  return id;
5558
6161
  }
6162
+ emitExchanges();
5559
6163
  const mcpConfig = writeTempJson("topo-director-mcp", {
5560
6164
  mcpServers: {
5561
6165
  topo: {
5562
6166
  type: "http",
5563
6167
  url: `http://127.0.0.1:${opts.port()}/mcp`,
5564
- headers: { "x-topo-director": opts.directorToken }
6168
+ headers: { "x-topo-director": opts.directorToken, "x-topo-command": id }
5565
6169
  }
5566
6170
  }
5567
6171
  });
5568
6172
  const model = opts.model ?? process.env.TOPO_COMMAND_MODEL ?? "sonnet";
6173
+ const resumeFrom = session;
5569
6174
  opts.bus.emit({ type: "command", id, status: "running", text });
5570
6175
  const child = spawn3(
5571
6176
  resolved,
@@ -5586,12 +6191,17 @@ Context: ${JSON.stringify(context)}` : "";
5586
6191
  "--model",
5587
6192
  model,
5588
6193
  "--append-system-prompt",
5589
- DIRECTOR_SYSTEM_PROMPT
6194
+ DIRECTOR_SYSTEM_PROMPT,
6195
+ ...resumeFrom ? ["--resume", resumeFrom] : []
5590
6196
  ],
5591
6197
  { cwd: opts.repo, stdio: ["ignore", "pipe", "pipe"] }
5592
6198
  );
5593
6199
  let gotResult = false;
5594
6200
  const cleanup = () => rmQuiet(mcpConfig);
6201
+ const fail = (text2) => {
6202
+ opts.bus.emit({ type: "command", id, status: "failed", text: text2 });
6203
+ updateExchange(id, () => ({ status: "failed", answer: text2 ?? "The director stopped." }));
6204
+ };
5595
6205
  child.stdout?.on(
5596
6206
  "data",
5597
6207
  makeLineFeed((obj) => {
@@ -5607,30 +6217,35 @@ Context: ${JSON.stringify(context)}` : "";
5607
6217
  status: "running",
5608
6218
  text: `${block2.name}(${shortArgs(block2.input)})`
5609
6219
  });
6220
+ const tool = block2.name.replace(/^mcp__topo__/, "");
6221
+ updateExchange(id, (x) => ({ tools: [...x.tools, tool] }));
5610
6222
  }
5611
6223
  }
5612
6224
  } else if (obj.type === "result") {
5613
6225
  gotResult = true;
5614
6226
  const result = obj;
5615
- opts.bus.emit({
5616
- type: "command",
5617
- id,
5618
- status: result.is_error ? "failed" : "done",
5619
- text: result.result
5620
- });
6227
+ const status = result.is_error ? "failed" : "done";
6228
+ if (result.session_id && exchanges.some((x) => x.id === id)) session = result.session_id;
6229
+ opts.bus.emit({ type: "command", id, status, text: result.result });
6230
+ updateExchange(id, () => ({ status, answer: result.result }));
5621
6231
  }
5622
6232
  })
5623
6233
  );
5624
6234
  child.on("error", (e) => {
5625
- if (!gotResult) opts.bus.emit({ type: "command", id, status: "failed", text: e.message });
6235
+ if (!gotResult) fail(e.message);
5626
6236
  cleanup();
5627
6237
  });
5628
6238
  child.on("close", () => {
5629
- if (!gotResult) opts.bus.emit({ type: "command", id, status: "failed" });
6239
+ if (!gotResult) fail();
5630
6240
  cleanup();
5631
6241
  });
5632
6242
  return id;
5633
6243
  }
6244
+ function newConversation() {
6245
+ exchanges = [];
6246
+ session = void 0;
6247
+ emitExchanges();
6248
+ }
5634
6249
  return {
5635
6250
  get available() {
5636
6251
  return resolved !== void 0;
@@ -5639,16 +6254,25 @@ Context: ${JSON.stringify(context)}` : "";
5639
6254
  get: (runId) => runs.get(runId),
5640
6255
  launch,
5641
6256
  stop,
5642
- runCommand
6257
+ runCommand,
6258
+ exchanges: () => exchanges,
6259
+ newConversation
5643
6260
  };
5644
6261
  }
5645
- var DIRECTOR_SYSTEM_PROMPT, isActive;
6262
+ var DIRECTOR_SYSTEM_PROMPT, MAX_EXCHANGES, isActive;
5646
6263
  var init_agents = __esm({
5647
6264
  async "packages/cli/src/agents.ts"() {
5648
6265
  "use strict";
5649
6266
  await init_src();
5650
6267
  init_hook();
5651
- DIRECTOR_SYSTEM_PROMPT = "You drive the Topo map for the human through the topo tools: change the view, focus blocks, make or update territories, launch or stop agents, decide inbox items. You never edit code. Finish with one short sentence saying what you did.";
6268
+ DIRECTOR_SYSTEM_PROMPT = [
6269
+ "You answer the human's questions about this codebase and drive the Topo map for them, through the topo tools only. You never edit code.",
6270
+ "For a question like how does X work, where is X, or what happens when Y: trace it with find, callees, callers and get_block (read the source of the blocks that matter), then call explain once with 3-8 steps in execution order. Pick only blocks that matter to the question; leave out UI, logging and plumbing that merely touches it. If explain reports unresolved steps, fix them and call it again.",
6271
+ "For a request to change the view (a lens, a flow, focusing something), territories, agents or the inbox: use those tools and do it.",
6272
+ "To sort the code into layers (or regions, areas, parts): call get_layers, read what the files hold, then set_layers with 3-8 layers named by what the code does, so every source file lands in one. Say what each layer is in a line.",
6273
+ "Then reply in plain prose: a few sentences, or a numbered list that mirrors the steps. No headings, no tables, no preamble about which tools you called."
6274
+ ].join("\n\n");
6275
+ MAX_EXCHANGES = 50;
5652
6276
  isActive = (status) => status === "starting" || status === "running";
5653
6277
  }
5654
6278
  });
@@ -5690,7 +6314,7 @@ var init_attribution = __esm({
5690
6314
 
5691
6315
  // packages/cli/src/editor.ts
5692
6316
  import { spawn } from "node:child_process";
5693
- import path16 from "node:path";
6317
+ import path17 from "node:path";
5694
6318
  function editorTemplate(flag) {
5695
6319
  return flag ?? process.env.TOPO_EDITOR ?? DEFAULT_EDITOR;
5696
6320
  }
@@ -5719,10 +6343,10 @@ function splitTemplate(template) {
5719
6343
  return out;
5720
6344
  }
5721
6345
  function resolveInRepo(repo, file) {
5722
- const root = path16.resolve(repo);
5723
- const abs = path16.resolve(root, file);
5724
- const rel = path16.relative(root, abs);
5725
- if (rel === "" || rel.startsWith("..") || path16.isAbsolute(rel)) {
6346
+ const root = path17.resolve(repo);
6347
+ const abs = path17.resolve(root, file);
6348
+ const rel = path17.relative(root, abs);
6349
+ if (rel === "" || rel.startsWith("..") || path17.isAbsolute(rel)) {
5726
6350
  throw new Error(`path outside repository: ${file}`);
5727
6351
  }
5728
6352
  return abs;
@@ -6117,8 +6741,8 @@ var init_live2 = __esm({
6117
6741
  });
6118
6742
 
6119
6743
  // packages/cli/src/mcp.ts
6120
- import fs14 from "node:fs";
6121
- import path17 from "node:path";
6744
+ import fs15 from "node:fs";
6745
+ import path18 from "node:path";
6122
6746
  function qualifiedName(n) {
6123
6747
  const i = n.id.indexOf("#");
6124
6748
  return i === -1 ? n.name : n.id.slice(i + 1);
@@ -6287,21 +6911,21 @@ function shapeViewTool(deps, args, ctx) {
6287
6911
  return `proposed lens "${name}"${savedNote}${skipNote}; the human will approve or deny it`;
6288
6912
  }
6289
6913
  function saveLensToFile(repo, lens) {
6290
- const file = path17.join(repo, ".topo", "lenses.json");
6914
+ const file = path18.join(repo, ".topo", "lenses.json");
6291
6915
  let existing = [];
6292
6916
  try {
6293
- const raw = JSON.parse(fs14.readFileSync(file, "utf8"));
6917
+ const raw = JSON.parse(fs15.readFileSync(file, "utf8"));
6294
6918
  const list = Array.isArray(raw) ? raw : raw?.lenses;
6295
6919
  if (Array.isArray(list)) existing = list;
6296
6920
  } catch {
6297
6921
  }
6298
6922
  const byName = new Map(existing.map((l) => [l.name, l]));
6299
6923
  byName.set(lens.name, lens);
6300
- fs14.mkdirSync(path17.dirname(file), { recursive: true });
6924
+ fs15.mkdirSync(path18.dirname(file), { recursive: true });
6301
6925
  const tmp = `${file}.${process.pid}.tmp`;
6302
- fs14.writeFileSync(tmp, `${JSON.stringify({ lenses: [...byName.values()] }, null, 2)}
6926
+ fs15.writeFileSync(tmp, `${JSON.stringify({ lenses: [...byName.values()] }, null, 2)}
6303
6927
  `);
6304
- fs14.renameSync(tmp, file);
6928
+ fs15.renameSync(tmp, file);
6305
6929
  }
6306
6930
  async function callTool(deps, params, ctx) {
6307
6931
  const name = params.name;
@@ -6373,7 +6997,7 @@ function createMcpHandler(deps) {
6373
6997
  }
6374
6998
  };
6375
6999
  }
6376
- var PROTOCOL_VERSIONS, DEFAULT_PROTOCOL_VERSION, INSTRUCTIONS, isRecord2, isStrArr, isDirection, isStatus, TOOLS, rpcResult, rpcError;
7000
+ var PROTOCOL_VERSIONS, DEFAULT_PROTOCOL_VERSION, INSTRUCTIONS, isRecord2, isStrArr, isDirection, isStatus, MAX_LAYER_FILES, withCommand, MAX_STEPS, TOOLS, rpcResult, rpcError;
6377
7001
  var init_mcp = __esm({
6378
7002
  async "packages/cli/src/mcp.ts"() {
6379
7003
  "use strict";
@@ -6385,6 +7009,9 @@ var init_mcp = __esm({
6385
7009
  isStrArr = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
6386
7010
  isDirection = (v) => v === "out" || v === "in" || v === "both";
6387
7011
  isStatus = (v) => v === "proposed" || v === "active" || v === "review" || v === "done";
7012
+ MAX_LAYER_FILES = 300;
7013
+ withCommand = (ctx) => ctx.command ? { command: ctx.command } : {};
7014
+ MAX_STEPS = 12;
6388
7015
  TOOLS = {
6389
7016
  find: {
6390
7017
  description: "Search blocks (files, classes, functions, methods) by name or text. Use this instead of grep to locate where something lives before reading or changing it.",
@@ -6661,7 +7288,7 @@ var init_mcp = __esm({
6661
7288
  label: { type: "string" }
6662
7289
  }
6663
7290
  },
6664
- run: (deps, args) => {
7291
+ run: (deps, args, ctx) => {
6665
7292
  const index = deps.getIndex();
6666
7293
  const { ids: roots, skipped } = Array.isArray(args.roots) ? resolveRefs(index, args.roots) : { ids: [], skipped: [] };
6667
7294
  if (skipped.length && !roots.length) {
@@ -6677,7 +7304,12 @@ var init_mcp = __esm({
6677
7304
  };
6678
7305
  const problem = lensProblem(candidate);
6679
7306
  if (problem) throw new Error(`invalid lens: ${problem}`);
6680
- deps.bus.emit({ type: "lens", lens: candidate, apply: true });
7307
+ deps.bus.emit({
7308
+ type: "lens",
7309
+ lens: candidate,
7310
+ apply: true,
7311
+ ...withCommand(ctx)
7312
+ });
6681
7313
  const skipNote = skipped.length ? `; unresolved roots skipped: ${skipped.join(", ")}` : "";
6682
7314
  return `applied view "${label}"${skipNote}`;
6683
7315
  }
@@ -6690,15 +7322,145 @@ var init_mcp = __esm({
6690
7322
  properties: { blocks: { type: "array", items: { type: "string" } } },
6691
7323
  required: ["blocks"]
6692
7324
  },
6693
- run: (deps, args) => {
7325
+ run: (deps, args, ctx) => {
6694
7326
  if (!Array.isArray(args.blocks)) throw new Error('missing required "blocks" (array)');
6695
7327
  const { ids, skipped } = resolveRefs(deps.getIndex(), args.blocks);
6696
7328
  if (!ids.length) throw new Error("none of those blocks resolved");
6697
- deps.bus.emit({ type: "focus", nodeIds: ids });
7329
+ deps.bus.emit({ type: "focus", nodeIds: ids, ...withCommand(ctx) });
6698
7330
  const skipNote = skipped.length ? `; unresolved blocks skipped: ${skipped.join(", ")}` : "";
6699
7331
  return `focused on ${ids.length} block(s)${skipNote}`;
6700
7332
  }
6701
7333
  },
7334
+ explain: {
7335
+ access: "director",
7336
+ description: "Walk the human through how something works: the map shows only these blocks, numbered in step order, with the calls between them. Use for 'how does X work / where is X / what happens when' questions, after tracing with find/callees/callers/get_block. Give 3-8 steps in execution (or reading) order, only blocks that matter to the question; each step's text says what happens there in one or two sentences.",
7337
+ inputSchema: {
7338
+ type: "object",
7339
+ properties: {
7340
+ title: { type: "string" },
7341
+ steps: {
7342
+ type: "array",
7343
+ items: {
7344
+ type: "object",
7345
+ properties: { block: { type: "string" }, text: { type: "string" } },
7346
+ required: ["block", "text"]
7347
+ }
7348
+ }
7349
+ },
7350
+ required: ["title", "steps"]
7351
+ },
7352
+ run: (deps, args, ctx) => {
7353
+ const title = requireString(args, "title");
7354
+ const raw = Array.isArray(args.steps) ? args.steps : [];
7355
+ if (!raw.length || raw.length > MAX_STEPS) {
7356
+ throw new Error(`"steps" must hold 1-${MAX_STEPS} {block, text} items`);
7357
+ }
7358
+ const index = deps.getIndex();
7359
+ const steps = [];
7360
+ const problems = [];
7361
+ raw.forEach((s, i) => {
7362
+ const ref = isRecord2(s) && typeof s.block === "string" ? s.block : "";
7363
+ const text = isRecord2(s) && typeof s.text === "string" ? s.text.trim() : "";
7364
+ const r = resolveOrText(index, ref);
7365
+ if ("node" in r) steps.push({ nodeId: r.node.id, text });
7366
+ else problems.push(`step ${i + 1} (${ref || "no block"}): ${r.text}`);
7367
+ });
7368
+ if (problems.length) {
7369
+ throw new Error(
7370
+ ["nothing shown; fix these steps and call explain again:", ...problems].join("\n")
7371
+ );
7372
+ }
7373
+ deps.bus.emit({ type: "explain", title, steps, ...withCommand(ctx) });
7374
+ return `showing "${title}": ${steps.length} step(s) on the map`;
7375
+ }
7376
+ },
7377
+ get_layers: {
7378
+ description: "The repo's layers (.topo/layers.json: named groups of files by path glob, which color the map) and which files are in none. With no layers yet, a folder-based draft. Lists every source file with its main blocks, so you can group them by what the code does.",
7379
+ inputSchema: { type: "object", properties: {} },
7380
+ run: (deps) => {
7381
+ const graph = deps.getReview().head;
7382
+ const doc = readLayers(deps.repo);
7383
+ const lines = [];
7384
+ if (doc) {
7385
+ const { byFile: byFile2 } = layerMap(doc, graph);
7386
+ lines.push("layers (.topo/layers.json):");
7387
+ for (const l of doc.layers) {
7388
+ const n = Object.values(byFile2).filter((x) => x === l.name).length;
7389
+ lines.push(`${l.name} ${l.paths.join(", ")} (${n} files)`);
7390
+ }
7391
+ for (const r of doc.rules)
7392
+ lines.push(`rule: ${r.from} may not reach ${r.notTo}${r.why ? ` (${r.why})` : ""}`);
7393
+ } else {
7394
+ lines.push("no .topo/layers.json yet; folder-based draft:");
7395
+ for (const l of suggestLayers(graph).layers) lines.push(`${l.name} ${l.paths.join(", ")}`);
7396
+ }
7397
+ const byFile = doc ? layerMap(doc, graph).byFile : {};
7398
+ const blocks = /* @__PURE__ */ new Map();
7399
+ for (const n of graph.nodes)
7400
+ if (n.kind !== "file")
7401
+ blocks.set(n.file, [...blocks.get(n.file) ?? [], qualifiedName(n)]);
7402
+ const files = graph.nodes.filter((n) => n.kind === "file").map((n) => n.file).sort();
7403
+ lines.push("", `files (${files.length}):`);
7404
+ for (const f of files.slice(0, MAX_LAYER_FILES)) {
7405
+ const b = blocks.get(f) ?? [];
7406
+ const names = b.slice(0, 5).join(", ") + (b.length > 5 ? ` +${b.length - 5}` : "");
7407
+ lines.push(`${f} [${byFile[f] ?? (doc ? "no layer" : "-")}] ${names}`);
7408
+ }
7409
+ if (files.length > MAX_LAYER_FILES) lines.push(`\u2026 +${files.length - MAX_LAYER_FILES} more`);
7410
+ return lines.join("\n");
7411
+ }
7412
+ },
7413
+ set_layers: {
7414
+ access: "director",
7415
+ description: "Write .topo/layers.json: 3-8 layers named by what the code does (physics, rendering, input), each a list of path globs (src/physics/**, src/render*.cpp, exact files). A file belongs to the first layer that matches; aim for every source file landing in one. Replaces the layers; existing rules are kept unless you pass rules. The map colors blocks by layer right away.",
7416
+ inputSchema: {
7417
+ type: "object",
7418
+ properties: {
7419
+ layers: {
7420
+ type: "array",
7421
+ items: {
7422
+ type: "object",
7423
+ properties: {
7424
+ name: { type: "string" },
7425
+ paths: { type: "array", items: { type: "string" } }
7426
+ },
7427
+ required: ["name", "paths"]
7428
+ }
7429
+ },
7430
+ rules: {
7431
+ type: "array",
7432
+ items: {
7433
+ type: "object",
7434
+ properties: {
7435
+ from: { type: "string" },
7436
+ notTo: { type: "string" },
7437
+ why: { type: "string" }
7438
+ },
7439
+ required: ["from", "notTo"]
7440
+ }
7441
+ }
7442
+ },
7443
+ required: ["layers"]
7444
+ },
7445
+ run: (deps, args) => {
7446
+ const rules = Array.isArray(args.rules) ? args.rules : readLayers(deps.repo)?.rules ?? [];
7447
+ const doc = parseLayers({ layers: args.layers, rules });
7448
+ if (!doc?.layers.length || doc.layers.some((l) => !l.name || !l.paths.length)) {
7449
+ throw new Error('"layers" must be a non-empty list of {name, paths: [glob, ...]}');
7450
+ }
7451
+ const map = layerMap(doc, deps.getReview().head);
7452
+ writeLayers(deps.repo, doc);
7453
+ deps.bus.emit({ type: "layers", layers: map });
7454
+ const files = deps.getReview().head.nodes.filter((n) => n.kind === "file").map((n) => n.file);
7455
+ const counts = doc.layers.map(
7456
+ (l) => `${l.name}: ${files.filter((f) => map.byFile[f] === l.name).length} files`
7457
+ );
7458
+ const loose = files.filter((f) => !map.byFile[f]);
7459
+ const looseNote = loose.length ? `
7460
+ ${loose.length} file(s) in no layer: ${loose.slice(0, 20).join(", ")}${loose.length > 20 ? " \u2026" : ""}` : "";
7461
+ return `wrote .topo/layers.json; ${counts.join(", ")}${looseNote}`;
7462
+ }
7463
+ },
6702
7464
  create_territory: {
6703
7465
  access: "director",
6704
7466
  description: "Create a territory (a region of the map) and optionally hand it to an agent. Use to carve out scope before launching a worker.",
@@ -6867,11 +7629,11 @@ var init_mcp = __esm({
6867
7629
  });
6868
7630
 
6869
7631
  // packages/cli/src/territories.ts
6870
- import fs15 from "node:fs";
6871
- import path18 from "node:path";
7632
+ import fs16 from "node:fs";
7633
+ import path19 from "node:path";
6872
7634
  function readTerritories(repo) {
6873
7635
  try {
6874
- const doc = parseTerritoryDoc(JSON.parse(fs15.readFileSync(territoriesFile(repo), "utf8")));
7636
+ const doc = parseTerritoryDoc(JSON.parse(fs16.readFileSync(territoriesFile(repo), "utf8")));
6875
7637
  if (doc) return doc;
6876
7638
  } catch {
6877
7639
  }
@@ -6879,11 +7641,11 @@ function readTerritories(repo) {
6879
7641
  }
6880
7642
  function writeTerritories(repo, doc) {
6881
7643
  const file = territoriesFile(repo);
6882
- fs15.mkdirSync(path18.dirname(file), { recursive: true });
7644
+ fs16.mkdirSync(path19.dirname(file), { recursive: true });
6883
7645
  const tmp = `${file}.${process.pid}.tmp`;
6884
- fs15.writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
7646
+ fs16.writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
6885
7647
  `);
6886
- fs15.renameSync(tmp, file);
7648
+ fs16.renameSync(tmp, file);
6887
7649
  }
6888
7650
  function parseTerritoryDoc(v) {
6889
7651
  const o = v;
@@ -6917,7 +7679,7 @@ var init_territories = __esm({
6917
7679
  "packages/cli/src/territories.ts"() {
6918
7680
  "use strict";
6919
7681
  SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6920
- territoriesFile = (repo) => path18.join(repo, ".topo", "territories.json");
7682
+ territoriesFile = (repo) => path19.join(repo, ".topo", "territories.json");
6921
7683
  isStrArr2 = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
6922
7684
  STATUSES = ["proposed", "active", "review", "done"];
6923
7685
  }
@@ -6925,9 +7687,9 @@ var init_territories = __esm({
6925
7687
 
6926
7688
  // packages/cli/src/server.ts
6927
7689
  import crypto from "node:crypto";
6928
- import fs16 from "node:fs";
7690
+ import fs17 from "node:fs";
6929
7691
  import http from "node:http";
6930
- import path19 from "node:path";
7692
+ import path20 from "node:path";
6931
7693
  import { fileURLToPath as fileURLToPath3 } from "node:url";
6932
7694
  function send(res, status, body, type) {
6933
7695
  res.writeHead(status, { "content-type": type, "cache-control": "no-store" });
@@ -6972,21 +7734,21 @@ function parseLayout(v) {
6972
7734
  return { version: 1, views: o.views };
6973
7735
  }
6974
7736
  function serveStatic(res, staticDir, urlPath2) {
6975
- const root = path19.resolve(staticDir);
6976
- const index = path19.join(root, "index.html");
7737
+ const root = path20.resolve(staticDir);
7738
+ const index = path20.join(root, "index.html");
6977
7739
  let decoded;
6978
7740
  try {
6979
7741
  decoded = decodeURIComponent(urlPath2);
6980
7742
  } catch {
6981
7743
  throw new HttpError(400, "bad path");
6982
7744
  }
6983
- let file = path19.resolve(root, `.${decoded}`);
6984
- if (file !== root && !file.startsWith(root + path19.sep)) throw new HttpError(403, "forbidden");
6985
- if (!fs16.statSync(file, { throwIfNoEntry: false })?.isFile()) file = index;
6986
- if (!fs16.existsSync(file)) return send(res, 200, MISSING_DIST, TYPES[".html"]);
6987
- const type = TYPES[path19.extname(file).toLowerCase()] ?? "application/octet-stream";
7745
+ let file = path20.resolve(root, `.${decoded}`);
7746
+ if (file !== root && !file.startsWith(root + path20.sep)) throw new HttpError(403, "forbidden");
7747
+ if (!fs17.statSync(file, { throwIfNoEntry: false })?.isFile()) file = index;
7748
+ if (!fs17.existsSync(file)) return send(res, 200, MISSING_DIST, TYPES[".html"]);
7749
+ const type = TYPES[path20.extname(file).toLowerCase()] ?? "application/octet-stream";
6988
7750
  res.writeHead(200, { "content-type": type });
6989
- fs16.createReadStream(file).pipe(res);
7751
+ fs17.createReadStream(file).pipe(res);
6990
7752
  }
6991
7753
  function isLocalhostOrigin(origin) {
6992
7754
  try {
@@ -7023,12 +7785,15 @@ function createServer(opts) {
7023
7785
  if (origin && !isLocalhostOrigin(origin)) throw new HttpError(403, "forbidden origin");
7024
7786
  const rawRunId = req.headers["x-topo-run"];
7025
7787
  const runId = typeof rawRunId === "string" && opts.agents?.get(rawRunId) ? rawRunId : void 0;
7788
+ const director = constantTimeEqual(
7789
+ req.headers["x-topo-director"],
7790
+ opts.directorToken
7791
+ );
7792
+ const command = req.headers["x-topo-command"];
7026
7793
  const ctx = {
7027
7794
  runId,
7028
- director: constantTimeEqual(
7029
- req.headers["x-topo-director"],
7030
- opts.directorToken
7031
- )
7795
+ director,
7796
+ ...director && typeof command === "string" && { command }
7032
7797
  };
7033
7798
  const result = await opts.mcp(await readBody(req), ctx);
7034
7799
  if (result.body === void 0) {
@@ -7040,7 +7805,7 @@ function createServer(opts) {
7040
7805
  if (route === "GET /api/meta") {
7041
7806
  return json(res, 200, {
7042
7807
  api: API_VERSION,
7043
- languages: ["typescript", "javascript", "luau", "python", "rust"],
7808
+ languages: ["typescript", "javascript", "luau", "python", "rust", "c", "cpp"],
7044
7809
  mcp: "/mcp",
7045
7810
  events: "/api/events",
7046
7811
  docs: "https://github.com/zhn2605/topo/blob/main/docs/api.md"
@@ -7120,6 +7885,10 @@ function createServer(opts) {
7120
7885
  const id = opts.agents.runCommand(body.text, parseCommandContext(body.context));
7121
7886
  return json(res, 200, { id });
7122
7887
  }
7888
+ if (route === "POST /api/command/new" && opts.agents) {
7889
+ opts.agents.newConversation();
7890
+ return json(res, 200, { ok: true });
7891
+ }
7123
7892
  if (route === "POST /api/open") {
7124
7893
  const body = await readJson(req);
7125
7894
  const line = Number(body?.line ?? 1);
@@ -7198,7 +7967,7 @@ var init_server = __esm({
7198
7967
  KEEPALIVE_MS = 15e3;
7199
7968
  REPO_WEB_DIST = fileURLToPath3(new URL("../../../apps/web/dist/", import.meta.url));
7200
7969
  DEFAULT_STATIC_DIR = [fileURLToPath3(new URL("./web/", import.meta.url)), REPO_WEB_DIST].find(
7201
- (d) => fs16.existsSync(path19.join(d, "index.html"))
7970
+ (d) => fs17.existsSync(path20.join(d, "index.html"))
7202
7971
  ) ?? REPO_WEB_DIST;
7203
7972
  MAX_BODY = 1024 * 1024;
7204
7973
  TYPES = {
@@ -7236,8 +8005,8 @@ var init_server = __esm({
7236
8005
  });
7237
8006
 
7238
8007
  // packages/cli/src/watch.ts
7239
- import fs17 from "node:fs";
7240
- import path20 from "node:path";
8008
+ import fs18 from "node:fs";
8009
+ import path21 from "node:path";
7241
8010
  function isIndexable(relPath) {
7242
8011
  const parts = relPath.split(/[\\/]/).filter(Boolean);
7243
8012
  const base = parts.at(-1);
@@ -7279,14 +8048,14 @@ function watchRepo(root, onBatch, opts = {}) {
7279
8048
  if (watchers.has(dir)) return;
7280
8049
  let w;
7281
8050
  try {
7282
- w = fs17.watch(dir, (_event, filename) => {
8051
+ w = fs18.watch(dir, (_event, filename) => {
7283
8052
  if (!filename) return;
7284
- const abs = path20.join(dir, filename.toString());
7285
- if (!watchers.has(abs) && fs17.statSync(abs, { throwIfNoEntry: false })?.isDirectory()) {
7286
- if (!isIgnoredDir(path20.basename(abs))) walkDirs(abs);
8053
+ const abs = path21.join(dir, filename.toString());
8054
+ if (!watchers.has(abs) && fs18.statSync(abs, { throwIfNoEntry: false })?.isDirectory()) {
8055
+ if (!isIgnoredDir(path21.basename(abs))) walkDirs(abs);
7287
8056
  return;
7288
8057
  }
7289
- const rel = path20.relative(root, abs).split(path20.sep).join("/");
8058
+ const rel = path21.relative(root, abs).split(path21.sep).join("/");
7290
8059
  if (!isIndexable(rel)) return;
7291
8060
  pending.add(rel);
7292
8061
  scheduleFlush();
@@ -7304,11 +8073,11 @@ function watchRepo(root, onBatch, opts = {}) {
7304
8073
  watchDir(dir);
7305
8074
  let entries = [];
7306
8075
  try {
7307
- entries = fs17.readdirSync(dir, { withFileTypes: true });
8076
+ entries = fs18.readdirSync(dir, { withFileTypes: true });
7308
8077
  } catch {
7309
8078
  }
7310
8079
  for (const e of entries)
7311
- if (e.isDirectory() && !isIgnoredDir(e.name)) walkDirs(path20.join(dir, e.name));
8080
+ if (e.isDirectory() && !isIgnoredDir(e.name)) walkDirs(path21.join(dir, e.name));
7312
8081
  };
7313
8082
  walkDirs(root);
7314
8083
  return {
@@ -7333,8 +8102,8 @@ var init_watch = __esm({
7333
8102
 
7334
8103
  // packages/cli/src/worktrees.ts
7335
8104
  import { execFileSync } from "node:child_process";
7336
- import fs18 from "node:fs";
7337
- import path21 from "node:path";
8105
+ import fs19 from "node:fs";
8106
+ import path22 from "node:path";
7338
8107
  function listWorktrees(repo) {
7339
8108
  let out;
7340
8109
  try {
@@ -7348,17 +8117,17 @@ function listWorktrees(repo) {
7348
8117
  }
7349
8118
  const roots = out.split("\n").filter((l) => l.startsWith("worktree ")).map((l) => l.slice("worktree ".length));
7350
8119
  const own = roots.find((r) => isInside2(r, repo));
7351
- return roots.filter((r) => r !== own).map((root) => ({ root, name: path21.basename(root) }));
8120
+ return roots.filter((r) => r !== own).map((root) => ({ root, name: path22.basename(root) }));
7352
8121
  }
7353
8122
  function mapWorktreePath(repo, worktrees, file) {
7354
8123
  const wt = worktrees.filter((w) => isInside2(w.root, file)).sort((a, b) => b.root.length - a.root.length)[0];
7355
8124
  if (!wt) return void 0;
7356
8125
  const mainRoot = gitRoot(repo);
7357
- const sub = mainRoot ? path21.relative(mainRoot, repo) : "";
7358
- const inTree = path21.relative(wt.root, file);
7359
- const rel = sub ? path21.relative(sub, inTree) : inTree;
7360
- if (rel.startsWith("..") || path21.isAbsolute(rel)) return void 0;
7361
- return { rel: rel.split(path21.sep).join("/"), worktree: wt };
8126
+ const sub = mainRoot ? path22.relative(mainRoot, repo) : "";
8127
+ const inTree = path22.relative(wt.root, file);
8128
+ const rel = sub ? path22.relative(sub, inTree) : inTree;
8129
+ if (rel.startsWith("..") || path22.isAbsolute(rel)) return void 0;
8130
+ return { rel: rel.split(path22.sep).join("/"), worktree: wt };
7362
8131
  }
7363
8132
  function gitRoot(dir) {
7364
8133
  try {
@@ -7372,7 +8141,7 @@ function gitRoot(dir) {
7372
8141
  }
7373
8142
  }
7374
8143
  function changedLines(base, changed) {
7375
- if (!fs18.existsSync(base) || !fs18.existsSync(changed)) return [];
8144
+ if (!fs19.existsSync(base) || !fs19.existsSync(changed)) return [];
7376
8145
  let text = "";
7377
8146
  try {
7378
8147
  execFileSync("git", ["diff", "--no-index", "-U0", "--no-color", base, changed], {
@@ -7405,8 +8174,8 @@ var init_worktrees = __esm({
7405
8174
  "packages/cli/src/worktrees.ts"() {
7406
8175
  "use strict";
7407
8176
  isInside2 = (dir, p) => {
7408
- const rel = path21.relative(dir, p);
7409
- return rel === "" || !rel.startsWith("..") && !path21.isAbsolute(rel);
8177
+ const rel = path22.relative(dir, p);
8178
+ return rel === "" || !rel.startsWith("..") && !path22.isAbsolute(rel);
7410
8179
  };
7411
8180
  }
7412
8181
  });
@@ -7422,8 +8191,8 @@ __export(review_exports, {
7422
8191
  });
7423
8192
  import { spawn as spawn2 } from "node:child_process";
7424
8193
  import crypto2 from "node:crypto";
7425
- import fs19 from "node:fs";
7426
- import path22 from "node:path";
8194
+ import fs20 from "node:fs";
8195
+ import path23 from "node:path";
7427
8196
  import { parseArgs as parseArgs4 } from "node:util";
7428
8197
  function openBrowser(url) {
7429
8198
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
@@ -7462,7 +8231,7 @@ function run4(argv) {
7462
8231
  if (!base) throw new Error(`missing <base> ref
7463
8232
 
7464
8233
  ${HELP4}`);
7465
- const repo = path22.resolve(values.repo ?? process.cwd());
8234
+ const repo = path23.resolve(values.repo ?? process.cwd());
7466
8235
  return serve({
7467
8236
  compute: () => computeReview(repo, base, values.head),
7468
8237
  repo,
@@ -7477,7 +8246,7 @@ ${HELP4}`);
7477
8246
  }
7478
8247
  function readLayout(repo) {
7479
8248
  try {
7480
- const doc = JSON.parse(fs19.readFileSync(layoutFile(repo), "utf8"));
8249
+ const doc = JSON.parse(fs20.readFileSync(layoutFile(repo), "utf8"));
7481
8250
  if (doc?.version === 1 && doc.views && typeof doc.views === "object") return doc;
7482
8251
  } catch {
7483
8252
  }
@@ -7485,12 +8254,12 @@ function readLayout(repo) {
7485
8254
  }
7486
8255
  function writeLayout(repo, doc) {
7487
8256
  const file = layoutFile(repo);
7488
- fs19.mkdirSync(path22.dirname(file), { recursive: true });
8257
+ fs20.mkdirSync(path23.dirname(file), { recursive: true });
7489
8258
  const tmp = `${file}.${process.pid}.tmp`;
7490
8259
  const text = JSON.stringify(doc, null, 2).replace(/\[\s+(-?\d+),\s+(-?\d+)\s+\]/g, "[$1, $2]");
7491
- fs19.writeFileSync(tmp, `${text}
8260
+ fs20.writeFileSync(tmp, `${text}
7492
8261
  `);
7493
- fs19.renameSync(tmp, file);
8262
+ fs20.renameSync(tmp, file);
7494
8263
  }
7495
8264
  function serve(opts) {
7496
8265
  const port = Number(opts.port);
@@ -7539,7 +8308,7 @@ function serve(opts) {
7539
8308
  const nodeIds = blocksAt(
7540
8309
  state.index,
7541
8310
  parsed.file,
7542
- changedLines(path22.join(repo, parsed.file), parsed.absFile)
8311
+ changedLines(path23.join(repo, parsed.file), parsed.absFile)
7543
8312
  );
7544
8313
  const run9 = agents.list().findLast((r) => r.agent === parsed.agent || r.agent === parsed.session);
7545
8314
  const { absFile: _abs, worktree, ...activity } = parsed;
@@ -7628,7 +8397,10 @@ function serve(opts) {
7628
8397
  state.payload.inbox = inbox.list();
7629
8398
  state.payload.runs = agents.list();
7630
8399
  state.payload.commandBar = agents.available ? "agent" : "search";
8400
+ state.payload.exchanges = agents.exchanges();
7631
8401
  if (opts.skipPermissions) state.payload.skipPermissions = true;
8402
+ const layers = readLayers(repo);
8403
+ state.payload.layers = layers && layerMap(layers, state.review.head);
7632
8404
  return state.payload;
7633
8405
  },
7634
8406
  index: () => state.index,
@@ -7716,7 +8488,7 @@ Options:
7716
8488
  --dangerously-skip-permissions launched agents run with no permission prompts (like Claude Code's
7717
8489
  flag of the same name): nothing waits in the Inbox. For this server session only.
7718
8490
  -h, --help`;
7719
- layoutFile = (repo) => path22.join(repo, ".topo", "layout.json");
8491
+ layoutFile = (repo) => path23.join(repo, ".topo", "layout.json");
7720
8492
  }
7721
8493
  });
7722
8494
 
@@ -7727,8 +8499,8 @@ __export(export_exports, {
7727
8499
  run: () => run5,
7728
8500
  snapshotPage: () => snapshotPage
7729
8501
  });
7730
- import fs20 from "node:fs";
7731
- import path23 from "node:path";
8502
+ import fs21 from "node:fs";
8503
+ import path24 from "node:path";
7732
8504
  import { parseArgs as parseArgs5 } from "node:util";
7733
8505
  function snapshotPage(template, data) {
7734
8506
  const json2 = JSON.stringify(data).replaceAll("<", "\\u003c");
@@ -7758,7 +8530,7 @@ function run5(argv) {
7758
8530
  if (!base) throw new Error(`missing <base> ref
7759
8531
 
7760
8532
  ${HELP5}`);
7761
- const repo = path23.resolve(values.repo ?? process.cwd());
8533
+ const repo = path24.resolve(values.repo ?? process.cwd());
7762
8534
  const review = computeReview(repo, base, values.head);
7763
8535
  const payload = { ...toPayload(repo, review, ""), live: void 0 };
7764
8536
  const sources = {};
@@ -7768,14 +8540,14 @@ ${HELP5}`);
7768
8540
  if (s) sources[`${side} ${b.nodeId}`] = s;
7769
8541
  }
7770
8542
  }
7771
- const templateFile = path23.join(DEFAULT_STATIC_DIR, "snapshot.html");
7772
- if (!fs20.existsSync(templateFile)) {
8543
+ const templateFile = path24.join(DEFAULT_STATIC_DIR, "snapshot.html");
8544
+ if (!fs21.existsSync(templateFile)) {
7773
8545
  throw new Error("the web UI isn't built: run `pnpm --filter @topo/web build` first");
7774
8546
  }
7775
- const template = fs20.readFileSync(templateFile, "utf8");
7776
- const out = path23.resolve(values.out);
7777
- fs20.writeFileSync(out, snapshotPage(template, { payload, sources }));
7778
- const kb = Math.round(fs20.statSync(out).size / 1024);
8547
+ const template = fs21.readFileSync(templateFile, "utf8");
8548
+ const out = path24.resolve(values.out);
8549
+ fs21.writeFileSync(out, snapshotPage(template, { payload, sources }));
8550
+ const kb = Math.round(fs21.statSync(out).size / 1024);
7779
8551
  console.log(`topo: ${review.changeSet.blocks.length} changed block(s) \u2192 ${out} (${kb} kB)`);
7780
8552
  }
7781
8553
  var HELP5;
@@ -7804,8 +8576,8 @@ __export(layers_exports, {
7804
8576
  HELP: () => HELP6,
7805
8577
  run: () => run6
7806
8578
  });
7807
- import fs21 from "node:fs";
7808
- import path24 from "node:path";
8579
+ import fs22 from "node:fs";
8580
+ import path25 from "node:path";
7809
8581
  import { parseArgs as parseArgs6 } from "node:util";
7810
8582
  function run6(argv) {
7811
8583
  const { values, positionals } = parseArgs6({
@@ -7817,18 +8589,18 @@ function run6(argv) {
7817
8589
  console.log(HELP6);
7818
8590
  return;
7819
8591
  }
7820
- const repo = path24.resolve(positionals[0] ?? process.cwd());
8592
+ const repo = path25.resolve(positionals[0] ?? process.cwd());
7821
8593
  const graph = indexRepo(repo);
7822
8594
  let doc = readLayers(repo);
7823
8595
  if (values.suggest) {
7824
8596
  if (doc) {
7825
8597
  console.log(
7826
- `topo: ${path24.relative(process.cwd(), layersFile(repo)) || layersFile(repo)} already exists; edit it instead.`
8598
+ `topo: ${path25.relative(process.cwd(), layersFile(repo)) || layersFile(repo)} already exists; edit it instead.`
7827
8599
  );
7828
8600
  } else {
7829
8601
  doc = suggestLayers(graph);
7830
- fs21.mkdirSync(path24.dirname(layersFile(repo)), { recursive: true });
7831
- fs21.writeFileSync(layersFile(repo), `${JSON.stringify(doc, null, 2)}
8602
+ fs22.mkdirSync(path25.dirname(layersFile(repo)), { recursive: true });
8603
+ fs22.writeFileSync(layersFile(repo), `${JSON.stringify(doc, null, 2)}
7832
8604
  `);
7833
8605
  console.log(
7834
8606
  `topo: wrote ${doc.layers.length} suggested layer(s) to ${layersFile(repo)}; name them, merge them, and add rules.`
@@ -7878,7 +8650,7 @@ __export(map_exports, {
7878
8650
  HELP: () => HELP7,
7879
8651
  run: () => run7
7880
8652
  });
7881
- import path25 from "node:path";
8653
+ import path26 from "node:path";
7882
8654
  import { parseArgs as parseArgs7 } from "node:util";
7883
8655
  function run7(argv) {
7884
8656
  const { values, positionals } = parseArgs7({
@@ -7901,7 +8673,7 @@ function run7(argv) {
7901
8673
  return;
7902
8674
  }
7903
8675
  if (!values.cache) process.env.TOPO_NO_CACHE = "1";
7904
- const dir = path25.resolve(positionals[0] ?? process.cwd());
8676
+ const dir = path26.resolve(positionals[0] ?? process.cwd());
7905
8677
  return serve({
7906
8678
  compute: () => computeExplore(dir),
7907
8679
  repo: dir,
@@ -7984,7 +8756,7 @@ async function main(argv) {
7984
8756
  ${HELP8}`);
7985
8757
  await (await cmd()).run(rest);
7986
8758
  }
7987
- if (import.meta.main) {
8759
+ if (true) {
7988
8760
  main(process.argv.slice(2)).catch((e) => {
7989
8761
  console.error(`topo: ${friendlyError(e)}`);
7990
8762
  process.exit(1);