@zhn2605/topo 0.1.0 → 0.3.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 path29 = unquote(both.slice(0, (both.length - 1) / 2));
927
+ cur = { oldPath: path29, newPath: path29, 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 path29 of untracked) {
978
+ const n = lineCount(join(repo, path29));
961
979
  diffs.push({
962
- newPath: path26,
980
+ newPath: path29,
963
981
  status: "added",
964
982
  oldRanges: [],
965
983
  newRanges: n ? [{ startLine: 1, endLine: n }] : []
@@ -998,10 +1016,10 @@ import fs3 from "node:fs";
998
1016
  import os from "node:os";
999
1017
  import path3 from "node:path";
1000
1018
  function requireSpec(arg) {
1001
- const str = /^(["'`])(.*)\1$/.exec(arg)?.[2];
1002
- if (str !== void 0) {
1003
- const relative2 = /^(\.\.?|@self)\//.test(str) ? str : void 0;
1004
- return { ...relative2 && { relative: relative2 }, tokens: str.split("/") };
1019
+ const str3 = /^(["'`])(.*)\1$/.exec(arg)?.[2];
1020
+ if (str3 !== void 0) {
1021
+ const relative2 = /^(\.\.?|@self)\//.test(str3) ? str3 : void 0;
1022
+ return { ...relative2 && { relative: relative2 }, tokens: str3.split("/") };
1005
1023
  }
1006
1024
  const tokens = (arg.match(/[A-Za-z_][\w-]*/g) ?? []).filter((t) => !LOOKUPS.has(t));
1007
1025
  const chain = /^script((?:\.Parent)*)((?:\.[A-Za-z_]\w*)+)$/.exec(arg.replace(/\s+/g, ""));
@@ -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(path29) {
1793
+ return path29.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 path29 = arg && urlPath(arg);
1859
+ if (!path29) 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(path29);
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: path29 });
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,23 +3466,13 @@ 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
- const str = first?.type === "expression_statement" ? first.namedChildren[0] : void 0;
2906
- return str?.type === "string" ? stripQuotes(str.text) : void 0;
2907
- }
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;
3474
+ const str3 = first?.type === "expression_statement" ? first.namedChildren[0] : void 0;
3475
+ return str3?.type === "string" ? stripQuotes(str3.text) : void 0;
2917
3476
  }
2918
3477
  function dotted(n) {
2919
3478
  if (!n) return void 0;
@@ -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, path29) => byFile.get(file)?.byQname.get(path29.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, (_, str3) => str3 ?? "").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;
@@ -5328,42 +5914,314 @@ Options:
5328
5914
  }
5329
5915
  });
5330
5916
 
5917
+ // packages/cli/src/permissions.ts
5918
+ import fs14 from "node:fs";
5919
+ import path16 from "node:path";
5920
+ function parseSettings(raw) {
5921
+ const p = isRecord2(raw) ? raw.permissions : void 0;
5922
+ return {
5923
+ version: 1,
5924
+ permissions: PERMISSION_MODES.includes(p) ? p : DEFAULT_SETTINGS.permissions
5925
+ };
5926
+ }
5927
+ function readSettings(repo) {
5928
+ try {
5929
+ return parseSettings(JSON.parse(fs14.readFileSync(settingsFile(repo), "utf8")));
5930
+ } catch {
5931
+ return DEFAULT_SETTINGS;
5932
+ }
5933
+ }
5934
+ function writeSettings(repo, s) {
5935
+ writeJsonAtomic(settingsFile(repo), s);
5936
+ }
5937
+ function writeJsonAtomic(file, value) {
5938
+ fs14.mkdirSync(path16.dirname(file), { recursive: true });
5939
+ const tmp = `${file}.${process.pid}.tmp`;
5940
+ fs14.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
5941
+ `);
5942
+ fs14.renameSync(tmp, file);
5943
+ }
5944
+ function workerPermissionArgs(mode) {
5945
+ if (mode === "never") return ["--dangerously-skip-permissions"];
5946
+ const allowed = ["mcp__topo__*", ...mode === "safe" ? SAFE_RULES : []];
5947
+ return [
5948
+ "--permission-mode",
5949
+ "acceptEdits",
5950
+ "--allowedTools",
5951
+ allowed.join(","),
5952
+ "--permission-prompt-tool",
5953
+ "mcp__topo__permission_prompt"
5954
+ ];
5955
+ }
5956
+ function ruleFor(toolName, input) {
5957
+ if (toolName !== "Bash") return toolName;
5958
+ const cmd = isRecord2(input) && typeof input.command === "string" ? input.command.trim() : "";
5959
+ if (!cmd || COMPOUND.test(cmd)) return void 0;
5960
+ const words = cmd.split(/\s+/);
5961
+ const [first = "", second] = words;
5962
+ if (second === void 0) return `Bash(${first})`;
5963
+ if (second.startsWith("-")) return `Bash(${cmd})`;
5964
+ const prefix = SUBCOMMANDS.has(first) ? `${first} ${second}` : first;
5965
+ return `Bash(${prefix} *)`;
5966
+ }
5967
+ function ruleMatches(rule, toolName, input) {
5968
+ const m = /^([^(]+)(?:\((.*)\))?$/.exec(rule.trim());
5969
+ if (!m || m[1] !== toolName) return false;
5970
+ const pattern = m[2];
5971
+ if (pattern === void 0) return true;
5972
+ if (toolName !== "Bash") return false;
5973
+ const cmd = isRecord2(input) && typeof input.command === "string" ? input.command.trim() : "";
5974
+ if (!cmd || COMPOUND.test(cmd)) return false;
5975
+ const prefix = /^(.*?)(?::\*| \*)$/.exec(pattern)?.[1];
5976
+ if (prefix === void 0) return cmd === pattern;
5977
+ return cmd === prefix || cmd.startsWith(`${prefix} `);
5978
+ }
5979
+ function readLocalSettings(repo) {
5980
+ try {
5981
+ const raw = JSON.parse(fs14.readFileSync(localSettingsFile(repo), "utf8"));
5982
+ return isRecord2(raw) ? raw : {};
5983
+ } catch {
5984
+ return {};
5985
+ }
5986
+ }
5987
+ function readLocalAllow(repo) {
5988
+ const perms = readLocalSettings(repo).permissions;
5989
+ const allow = isRecord2(perms) ? perms.allow : void 0;
5990
+ return Array.isArray(allow) ? allow.filter((r) => typeof r === "string") : [];
5991
+ }
5992
+ function withAllowRule(doc, rule) {
5993
+ const perms = isRecord2(doc.permissions) ? doc.permissions : {};
5994
+ const allow = Array.isArray(perms.allow) ? perms.allow : [];
5995
+ if (allow.includes(rule)) return doc;
5996
+ return { ...doc, permissions: { ...perms, allow: [...allow, rule] } };
5997
+ }
5998
+ function addLocalAllowRule(repo, rule) {
5999
+ const file = localSettingsFile(repo);
6000
+ let doc = {};
6001
+ if (fs14.existsSync(file)) {
6002
+ const raw = JSON.parse(fs14.readFileSync(file, "utf8"));
6003
+ if (!isRecord2(raw)) throw new Error(`${file} isn't a JSON object; not changing it`);
6004
+ doc = raw;
6005
+ }
6006
+ writeJsonAtomic(file, withAllowRule(doc, rule));
6007
+ }
6008
+ var PERMISSION_MODES, DEFAULT_SETTINGS, SAFE_RULES, isRecord2, settingsFile, SUBCOMMANDS, COMPOUND, localSettingsFile;
6009
+ var init_permissions = __esm({
6010
+ "packages/cli/src/permissions.ts"() {
6011
+ "use strict";
6012
+ PERMISSION_MODES = ["safe", "ask", "never"];
6013
+ DEFAULT_SETTINGS = { version: 1, permissions: "safe" };
6014
+ SAFE_RULES = [
6015
+ "Bash(ls *)",
6016
+ "Bash(pwd)",
6017
+ "Bash(cat *)",
6018
+ "Bash(head *)",
6019
+ "Bash(tail *)",
6020
+ "Bash(wc *)",
6021
+ "Bash(grep *)",
6022
+ "Bash(rg *)",
6023
+ "Bash(find *)",
6024
+ "Bash(git status *)",
6025
+ "Bash(git diff *)",
6026
+ "Bash(git log *)",
6027
+ "Bash(git show *)",
6028
+ "Bash(git blame *)"
6029
+ ];
6030
+ isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6031
+ settingsFile = (repo) => path16.join(repo, ".topo", "settings.json");
6032
+ SUBCOMMANDS = /* @__PURE__ */ new Set(["git", "npm", "pnpm", "yarn", "bun", "npx", "cargo", "go", "docker"]);
6033
+ COMPOUND = /&&|\|\||[;|`<>]|\$\(/;
6034
+ localSettingsFile = (repo) => path16.join(repo, ".claude", "settings.local.json");
6035
+ }
6036
+ });
6037
+
6038
+ // packages/cli/src/adapters.ts
6039
+ var AGENT_IDS, mcpUrl, str, rec, claude, GEMINI_EDIT_TOOLS, gemini, codex, ADAPTERS;
6040
+ var init_adapters = __esm({
6041
+ "packages/cli/src/adapters.ts"() {
6042
+ "use strict";
6043
+ init_hook();
6044
+ init_permissions();
6045
+ AGENT_IDS = ["claude", "codex", "gemini"];
6046
+ mcpUrl = (port) => `http://127.0.0.1:${port}/mcp`;
6047
+ str = (v) => typeof v === "string" ? v : void 0;
6048
+ rec = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
6049
+ claude = {
6050
+ id: "claude",
6051
+ name: "Claude",
6052
+ bin: "claude",
6053
+ asksMidRun: true,
6054
+ resumes: true,
6055
+ worker: (t) => ({
6056
+ argv: [
6057
+ "-p",
6058
+ t.prompt,
6059
+ "--output-format",
6060
+ "stream-json",
6061
+ "--verbose",
6062
+ "--mcp-config",
6063
+ "{file:mcp}",
6064
+ "--settings",
6065
+ "{file:settings}",
6066
+ ...workerPermissionArgs(t.mode),
6067
+ // Without this, workers run on the user's default model (often the priciest one).
6068
+ "--model",
6069
+ process.env.TOPO_WORKER_MODEL ?? "sonnet",
6070
+ ...t.resume ? ["--resume", t.resume] : []
6071
+ ],
6072
+ json: {
6073
+ mcp: {
6074
+ mcpServers: {
6075
+ topo: { type: "http", url: mcpUrl(t.port), headers: { "x-topo-run": t.runId } }
6076
+ }
6077
+ },
6078
+ // Its edits come in through the same PostToolUse hook `topo hook` prints.
6079
+ settings: hookSettings(t.port)
6080
+ }
6081
+ }),
6082
+ parse: (o) => {
6083
+ if (o.type === "system" && o.subtype === "init" && str(o.session_id))
6084
+ return [{ kind: "started", session: o.session_id }];
6085
+ if (o.type === "result")
6086
+ return [
6087
+ {
6088
+ kind: "finished",
6089
+ failed: o.is_error === true,
6090
+ summary: str(o.result),
6091
+ costUsd: typeof o.total_cost_usd === "number" ? o.total_cost_usd : void 0
6092
+ }
6093
+ ];
6094
+ return [];
6095
+ }
6096
+ };
6097
+ GEMINI_EDIT_TOOLS = /* @__PURE__ */ new Set(["write_file", "replace", "edit"]);
6098
+ gemini = {
6099
+ id: "gemini",
6100
+ name: "Gemini",
6101
+ bin: "gemini",
6102
+ asksMidRun: false,
6103
+ resumes: false,
6104
+ // --resume takes "latest" or a list index, not a session id
6105
+ worker: (t) => ({
6106
+ argv: [
6107
+ "-p",
6108
+ t.prompt,
6109
+ "--output-format",
6110
+ "stream-json",
6111
+ "--skip-trust",
6112
+ "--approval-mode",
6113
+ t.mode === "never" ? "yolo" : "auto_edit",
6114
+ ...process.env.TOPO_GEMINI_MODEL ? ["--model", process.env.TOPO_GEMINI_MODEL] : []
6115
+ ],
6116
+ env: { GEMINI_CLI_SYSTEM_SETTINGS_PATH: "{file:settings}" },
6117
+ json: {
6118
+ settings: {
6119
+ mcpServers: {
6120
+ topo: { httpUrl: mcpUrl(t.port), headers: { "x-topo-run": t.runId }, trust: true }
6121
+ }
6122
+ }
6123
+ }
6124
+ }),
6125
+ parse: (o) => {
6126
+ if (o.type === "init" && str(o.session_id))
6127
+ return [{ kind: "started", session: o.session_id }];
6128
+ if (o.type === "message" && o.role === "assistant" && str(o.content))
6129
+ return [{ kind: "text", text: o.content, append: o.delta === true }];
6130
+ if (o.type === "tool_use" && GEMINI_EDIT_TOOLS.has(str(o.tool_name) ?? "")) {
6131
+ const file = str(rec(o.parameters).file_path);
6132
+ return file ? [{ kind: "edit", file }] : [];
6133
+ }
6134
+ if (o.type === "result") return [{ kind: "finished", failed: o.status !== "success" }];
6135
+ return [];
6136
+ }
6137
+ };
6138
+ codex = {
6139
+ id: "codex",
6140
+ name: "Codex",
6141
+ bin: "codex",
6142
+ asksMidRun: false,
6143
+ resumes: true,
6144
+ worker: (t) => {
6145
+ const opts = [
6146
+ "--json",
6147
+ "-c",
6148
+ `mcp_servers.topo.url=${JSON.stringify(mcpUrl(t.port))}`,
6149
+ "-c",
6150
+ `mcp_servers.topo.http_headers={ "x-topo-run" = ${JSON.stringify(t.runId)} }`,
6151
+ ...t.mode === "never" ? ["--dangerously-bypass-approvals-and-sandbox"] : ["-s", "workspace-write", "-c", 'approval_policy="never"'],
6152
+ ...process.env.TOPO_CODEX_MODEL ? ["-m", process.env.TOPO_CODEX_MODEL] : []
6153
+ ];
6154
+ return {
6155
+ argv: t.resume ? ["exec", "resume", ...opts, t.resume, t.prompt] : ["exec", ...opts, t.prompt]
6156
+ };
6157
+ },
6158
+ parse: (o) => {
6159
+ if (o.type === "thread.started" && str(o.thread_id))
6160
+ return [{ kind: "started", session: o.thread_id }];
6161
+ if (o.type === "item.completed") {
6162
+ const item = rec(o.item);
6163
+ if (item.type === "agent_message" && str(item.text))
6164
+ return [{ kind: "text", text: item.text }];
6165
+ if (item.type === "file_change" && Array.isArray(item.changes))
6166
+ return item.changes.flatMap((c) => {
6167
+ const file = str(rec(c).path);
6168
+ return file ? [{ kind: "edit", file }] : [];
6169
+ });
6170
+ }
6171
+ if (o.type === "turn.completed") return [{ kind: "finished", failed: false }];
6172
+ if (o.type === "turn.failed")
6173
+ return [{ kind: "finished", failed: true, summary: str(rec(o.error).message) }];
6174
+ return [];
6175
+ }
6176
+ };
6177
+ ADAPTERS = { claude, codex, gemini };
6178
+ }
6179
+ });
6180
+
5331
6181
  // packages/cli/src/agents.ts
5332
6182
  import { spawn as nodeSpawn } from "node:child_process";
5333
- import fs13 from "node:fs";
6183
+ import fs15 from "node:fs";
5334
6184
  import os2 from "node:os";
5335
- import path15 from "node:path";
6185
+ import path17 from "node:path";
6186
+ function describeContext(c) {
6187
+ const roots = c.roots ?? [];
6188
+ const what = c.mode === "review" ? "the review of the changes" : c.mode === "flow" ? "a flow" : "a map";
6189
+ const depth = c.depth === void 0 ? "" : c.depth === 0 ? ", just those blocks" : `, with what they call and what calls them ${c.depth} hop(s) out`;
6190
+ const more = roots.length > MAX_CONTEXT_ROOTS ? ` and ${roots.length - MAX_CONTEXT_ROOTS} more` : "";
6191
+ const sel = c.selected?.length ? ` Selected: ${c.selected.join(", ")}.` : "";
6192
+ return roots.length ? `The human's map right now: ${what}, rooted at ${roots.length} block(s)${depth}: ${roots.slice(0, MAX_CONTEXT_ROOTS).join(", ")}${more}.${sel}` : `The human's map right now: ${what}, empty.${sel}`;
6193
+ }
5336
6194
  function resolveBin(bin) {
5337
6195
  const tryExec = (p) => {
5338
6196
  try {
5339
- fs13.accessSync(p, fs13.constants.X_OK);
6197
+ fs15.accessSync(p, fs15.constants.X_OK);
5340
6198
  return p;
5341
6199
  } catch {
5342
6200
  return void 0;
5343
6201
  }
5344
6202
  };
5345
- if (bin.includes(path15.sep)) return tryExec(bin);
5346
- const dirs = (process.env.PATH ?? "").split(path15.delimiter).filter(Boolean);
6203
+ if (bin.includes(path17.sep)) return tryExec(bin);
6204
+ const dirs = (process.env.PATH ?? "").split(path17.delimiter).filter(Boolean);
5347
6205
  const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD").split(";") : [""];
5348
6206
  for (const dir of dirs) {
5349
6207
  for (const ext of exts) {
5350
- const hit = tryExec(path15.join(dir, bin + ext));
6208
+ const hit = tryExec(path17.join(dir, bin + ext));
5351
6209
  if (hit) return hit;
5352
6210
  }
5353
6211
  }
5354
6212
  return void 0;
5355
6213
  }
5356
6214
  function writeTempJson(prefix, value) {
5357
- const file = path15.join(
6215
+ const file = path17.join(
5358
6216
  os2.tmpdir(),
5359
6217
  `${prefix}-${process.pid}-${Math.random().toString(36).slice(2)}.json`
5360
6218
  );
5361
- fs13.writeFileSync(file, JSON.stringify(value));
6219
+ fs15.writeFileSync(file, JSON.stringify(value));
5362
6220
  return file;
5363
6221
  }
5364
6222
  function rmQuiet(file) {
5365
6223
  try {
5366
- fs13.unlinkSync(file);
6224
+ fs15.unlinkSync(file);
5367
6225
  } catch {
5368
6226
  }
5369
6227
  }
@@ -5393,19 +6251,41 @@ function createAgentManager(opts) {
5393
6251
  const spawn3 = opts.spawn ?? nodeSpawn;
5394
6252
  const claudeBin = opts.claudeBin ?? "claude";
5395
6253
  const resolved = resolveBin(claudeBin);
6254
+ const installed = /* @__PURE__ */ new Map();
6255
+ for (const id of AGENT_IDS) {
6256
+ const bin = id === "claude" ? resolved : resolveBin(opts.bins?.[id] ?? ADAPTERS[id].bin);
6257
+ if (bin) installed.set(id, bin);
6258
+ }
5396
6259
  const runs = /* @__PURE__ */ new Map();
5397
6260
  const children = /* @__PURE__ */ new Map();
5398
6261
  let runSeq = 0;
5399
6262
  let cmdSeq = 0;
6263
+ let exchanges = [];
6264
+ let session;
5400
6265
  function emitRuns() {
5401
6266
  opts.bus.emit({ type: "runs", runs: [...runs.values()] });
5402
6267
  }
6268
+ function emitExchanges() {
6269
+ opts.bus.emit({ type: "exchanges", exchanges });
6270
+ }
6271
+ function updateExchange(id, patch) {
6272
+ const i = exchanges.findIndex((x2) => x2.id === id);
6273
+ const x = exchanges[i];
6274
+ if (!x) return;
6275
+ exchanges = exchanges.with(i, { ...x, ...patch(x) });
6276
+ emitExchanges();
6277
+ }
6278
+ opts.bus.subscribe((e) => {
6279
+ if ((e.type === "explain" || e.type === "focus" || e.type === "lens") && e.command)
6280
+ updateExchange(e.command, () => ({ view: e }));
6281
+ else if (e.type === "ask" && e.command) updateExchange(e.command, () => ({ ask: e.questions }));
6282
+ });
5403
6283
  function setTerritoryStatus(territoryId, patch) {
5404
6284
  const territories = opts.getTerritories();
5405
6285
  const next = territories.map((t) => t.id === territoryId ? { ...t, ...patch } : t);
5406
6286
  opts.saveTerritories(next);
5407
6287
  }
5408
- function launch(territoryId, reply) {
6288
+ function launch(territoryId, reply, agent) {
5409
6289
  const territory = opts.getTerritories().find((t) => t.id === territoryId);
5410
6290
  if (!territory) throw new Error(`no territory "${territoryId}"`);
5411
6291
  const territoryRuns = [...runs.values()].filter((r) => r.territoryId === territoryId);
@@ -5415,99 +6295,97 @@ function createAgentManager(opts) {
5415
6295
  if (!reply && !territory.note?.trim()) {
5416
6296
  throw new Error(`"${territoryId}" has no note; write the task before launching`);
5417
6297
  }
5418
- const resumeFrom = reply ? territoryRuns.findLast((r) => r.agent?.startsWith("claude:"))?.agent?.slice("claude:".length) : void 0;
6298
+ const wanted = agent ?? territory.runner;
6299
+ if (wanted !== void 0 && !AGENT_IDS.includes(wanted)) {
6300
+ throw new Error(`unknown agent "${wanted}" (one of ${AGENT_IDS.join(", ")})`);
6301
+ }
6302
+ const adapter = wanted ? ADAPTERS[wanted] : ADAPTERS[[...installed.keys()][0] ?? "claude"];
6303
+ const bin = adapter && installed.get(adapter.id);
6304
+ if (!adapter || !bin) throw new Error(`no \`${adapter?.bin ?? "claude"}\` CLI found on PATH`);
6305
+ const prefix = `${adapter.id}:`;
6306
+ const resumeFrom = reply ? territoryRuns.findLast((r) => r.agent?.startsWith(prefix))?.agent?.slice(prefix.length) : void 0;
6307
+ if (reply && !adapter.resumes) {
6308
+ throw new Error(`${adapter.name} can't continue an earlier run; launch it again instead`);
6309
+ }
5419
6310
  if (reply && !resumeFrom) throw new Error(`"${territoryId}" has no earlier run to continue`);
5420
- if (!resolved) throw new Error("no `claude` CLI found on PATH");
6311
+ if (agent && territory.runner !== adapter.id) {
6312
+ setTerritoryStatus(territoryId, { runner: adapter.id });
6313
+ }
5421
6314
  const id = `run-${++runSeq}`;
5422
- const mcpConfig = writeTempJson("topo-mcp", {
5423
- mcpServers: {
5424
- topo: {
5425
- type: "http",
5426
- url: `http://127.0.0.1:${opts.port()}/mcp`,
5427
- headers: { "x-topo-run": id }
5428
- }
5429
- }
5430
- });
5431
- const settings = writeTempJson("topo-settings", hookSettings(opts.port()));
5432
6315
  const prompt = reply ?? [
5433
6316
  territory.label,
5434
6317
  territory.note,
5435
6318
  `Call get_territory ${territory.id} first. Stay inside it; if you need blocks outside, call request_territory. After changing a block, call report_change with one sentence on why.`
5436
6319
  ].filter((s) => !!s).join("\n\n");
6320
+ const spec = adapter.worker({
6321
+ runId: id,
6322
+ prompt,
6323
+ port: opts.port(),
6324
+ mode: opts.permissionMode?.() ?? "safe",
6325
+ resume: resumeFrom
6326
+ });
6327
+ const files = new Map(
6328
+ Object.entries(spec.json ?? {}).map(([k, v]) => [k, writeTempJson(`topo-${k}`, v)])
6329
+ );
6330
+ const fill = (s) => s.replace(/^\{file:(\w+)\}$/, (_, k) => files.get(k) ?? s);
5437
6331
  const run8 = {
5438
6332
  id,
5439
6333
  role: "worker",
5440
6334
  territoryId,
5441
- label: `Claude \xB7 ${territory.label}`,
6335
+ label: `${adapter.name} \xB7 ${territory.label}`,
5442
6336
  status: "starting",
5443
6337
  startedAt: Date.now()
5444
6338
  };
5445
6339
  runs.set(id, run8);
5446
- const child = spawn3(
5447
- resolved,
5448
- [
5449
- "-p",
5450
- prompt,
5451
- "--output-format",
5452
- "stream-json",
5453
- "--verbose",
5454
- "--mcp-config",
5455
- mcpConfig,
5456
- "--settings",
5457
- settings,
5458
- ...opts.skipPermissions ? ["--dangerously-skip-permissions"] : [
5459
- "--permission-mode",
5460
- "acceptEdits",
5461
- // Topo's own tools never prompt (otherwise every get_territory would wait in the
5462
- // inbox); everything acceptEdits doesn't cover goes to the inbox via the prompt tool.
5463
- "--allowedTools",
5464
- "mcp__topo__*",
5465
- "--permission-prompt-tool",
5466
- "mcp__topo__permission_prompt"
5467
- ],
5468
- // Without this, workers run on the user's default model (often the priciest one).
5469
- "--model",
5470
- process.env.TOPO_WORKER_MODEL ?? "sonnet",
5471
- ...resumeFrom ? ["--resume", resumeFrom] : []
5472
- ],
5473
- { cwd: opts.repo, stdio: ["ignore", "pipe", "pipe"] }
5474
- );
6340
+ const child = spawn3(bin, spec.argv.map(fill), {
6341
+ cwd: opts.repo,
6342
+ stdio: ["ignore", "pipe", "pipe"],
6343
+ ...spec.env && {
6344
+ env: {
6345
+ ...process.env,
6346
+ ...Object.fromEntries(Object.entries(spec.env).map(([k, v]) => [k, fill(v)]))
6347
+ }
6348
+ }
6349
+ });
5475
6350
  children.set(id, child);
5476
6351
  let gotResult = false;
6352
+ let summary;
5477
6353
  const cleanup = () => {
5478
6354
  children.delete(id);
5479
- rmQuiet(mcpConfig);
5480
- rmQuiet(settings);
6355
+ for (const f of files.values()) rmQuiet(f);
5481
6356
  };
5482
6357
  child.stdout?.on(
5483
6358
  "data",
5484
6359
  makeLineFeed((obj) => {
5485
- const type = obj.type;
5486
- if (type === "system" && obj.subtype === "init") {
5487
- const init = obj;
5488
- run8.agent = `claude:${init.session_id}`;
5489
- run8.status = "running";
5490
- setTerritoryStatus(territoryId, { agent: run8.agent, status: "active" });
5491
- emitRuns();
5492
- } else if (type === "result") {
5493
- gotResult = true;
5494
- const result = obj;
5495
- run8.status = result.is_error ? "failed" : "done";
5496
- run8.summary = result.result;
5497
- run8.costUsd = result.total_cost_usd;
5498
- run8.endedAt = Date.now();
5499
- setTerritoryStatus(territoryId, { status: "review" });
5500
- opts.inbox.add({
5501
- kind: "finished",
5502
- title: `${territory.label}: ${run8.status === "failed" ? "failed" : "finished"}`,
5503
- detail: result.result,
5504
- agent: run8.agent,
5505
- label: run8.label,
5506
- runId: id,
5507
- territoryId,
5508
- nodeIds: territory.nodeIds
5509
- });
5510
- emitRuns();
6360
+ for (const ev of adapter.parse(obj)) {
6361
+ if (ev.kind === "started") {
6362
+ run8.agent = `${prefix}${ev.session}`;
6363
+ run8.status = "running";
6364
+ setTerritoryStatus(territoryId, { agent: run8.agent, status: "active" });
6365
+ emitRuns();
6366
+ } else if (ev.kind === "text") {
6367
+ summary = ev.append ? (summary ?? "") + ev.text : ev.text;
6368
+ } else if (ev.kind === "edit") {
6369
+ if (run8.agent) opts.onEdit?.(run8.agent, ev.file);
6370
+ } else if (ev.kind === "finished") {
6371
+ gotResult = true;
6372
+ run8.status = ev.failed ? "failed" : "done";
6373
+ run8.summary = ev.summary ?? summary;
6374
+ run8.costUsd = ev.costUsd;
6375
+ run8.endedAt = Date.now();
6376
+ setTerritoryStatus(territoryId, { status: "review" });
6377
+ opts.inbox.add({
6378
+ kind: "finished",
6379
+ title: `${territory.label}: ${run8.status === "failed" ? "failed" : "finished"}`,
6380
+ detail: run8.summary,
6381
+ agent: run8.agent,
6382
+ label: run8.label,
6383
+ runId: id,
6384
+ territoryId,
6385
+ nodeIds: territory.nodeIds
6386
+ });
6387
+ emitRuns();
6388
+ }
5511
6389
  }
5512
6390
  })
5513
6391
  );
@@ -5544,28 +6422,31 @@ function createAgentManager(opts) {
5544
6422
  const id = `cmd-${++cmdSeq}`;
5545
6423
  const contextLine = context ? `
5546
6424
 
5547
- Context: ${JSON.stringify(context)}` : "";
6425
+ ${describeContext(context)}` : "";
6426
+ exchanges = [
6427
+ ...exchanges.slice(-(MAX_EXCHANGES - 1)),
6428
+ { id, text, status: "running", tools: [], at: Date.now() }
6429
+ ];
5548
6430
  if (!resolved) {
5549
6431
  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
- });
6432
+ opts.bus.emit({ type: "focus", nodeIds: nodes.map((n) => n.id), command: id });
6433
+ 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.";
6434
+ opts.bus.emit({ type: "command", id, status: "done", text: line });
6435
+ updateExchange(id, () => ({ status: "done", answer: line }));
5557
6436
  return id;
5558
6437
  }
6438
+ emitExchanges();
5559
6439
  const mcpConfig = writeTempJson("topo-director-mcp", {
5560
6440
  mcpServers: {
5561
6441
  topo: {
5562
6442
  type: "http",
5563
6443
  url: `http://127.0.0.1:${opts.port()}/mcp`,
5564
- headers: { "x-topo-director": opts.directorToken }
6444
+ headers: { "x-topo-director": opts.directorToken, "x-topo-command": id }
5565
6445
  }
5566
6446
  }
5567
6447
  });
5568
6448
  const model = opts.model ?? process.env.TOPO_COMMAND_MODEL ?? "sonnet";
6449
+ const resumeFrom = session;
5569
6450
  opts.bus.emit({ type: "command", id, status: "running", text });
5570
6451
  const child = spawn3(
5571
6452
  resolved,
@@ -5586,12 +6467,17 @@ Context: ${JSON.stringify(context)}` : "";
5586
6467
  "--model",
5587
6468
  model,
5588
6469
  "--append-system-prompt",
5589
- DIRECTOR_SYSTEM_PROMPT
6470
+ DIRECTOR_SYSTEM_PROMPT,
6471
+ ...resumeFrom ? ["--resume", resumeFrom] : []
5590
6472
  ],
5591
6473
  { cwd: opts.repo, stdio: ["ignore", "pipe", "pipe"] }
5592
6474
  );
5593
6475
  let gotResult = false;
5594
6476
  const cleanup = () => rmQuiet(mcpConfig);
6477
+ const fail = (text2) => {
6478
+ opts.bus.emit({ type: "command", id, status: "failed", text: text2 });
6479
+ updateExchange(id, () => ({ status: "failed", answer: text2 ?? "The director stopped." }));
6480
+ };
5595
6481
  child.stdout?.on(
5596
6482
  "data",
5597
6483
  makeLineFeed((obj) => {
@@ -5607,48 +6493,76 @@ Context: ${JSON.stringify(context)}` : "";
5607
6493
  status: "running",
5608
6494
  text: `${block2.name}(${shortArgs(block2.input)})`
5609
6495
  });
6496
+ const tool = block2.name.replace(/^mcp__topo__/, "");
6497
+ updateExchange(id, (x) => ({ tools: [...x.tools, tool] }));
5610
6498
  }
5611
6499
  }
5612
6500
  } else if (obj.type === "result") {
5613
6501
  gotResult = true;
5614
6502
  const result = obj;
5615
- opts.bus.emit({
5616
- type: "command",
5617
- id,
5618
- status: result.is_error ? "failed" : "done",
5619
- text: result.result
5620
- });
6503
+ const status = result.is_error ? "failed" : "done";
6504
+ if (result.session_id && exchanges.some((x) => x.id === id)) session = result.session_id;
6505
+ opts.bus.emit({ type: "command", id, status, text: result.result });
6506
+ updateExchange(id, () => ({ status, answer: result.result }));
5621
6507
  }
5622
6508
  })
5623
6509
  );
5624
6510
  child.on("error", (e) => {
5625
- if (!gotResult) opts.bus.emit({ type: "command", id, status: "failed", text: e.message });
6511
+ if (!gotResult) fail(e.message);
5626
6512
  cleanup();
5627
6513
  });
5628
6514
  child.on("close", () => {
5629
- if (!gotResult) opts.bus.emit({ type: "command", id, status: "failed" });
6515
+ if (!gotResult) fail();
5630
6516
  cleanup();
5631
6517
  });
5632
6518
  return id;
5633
6519
  }
6520
+ function newConversation() {
6521
+ exchanges = [];
6522
+ session = void 0;
6523
+ emitExchanges();
6524
+ }
5634
6525
  return {
5635
6526
  get available() {
6527
+ return installed.size > 0;
6528
+ },
6529
+ get director() {
5636
6530
  return resolved !== void 0;
5637
6531
  },
6532
+ agents: () => [...installed.keys()].map((id) => ({
6533
+ id,
6534
+ name: ADAPTERS[id].name,
6535
+ asksMidRun: ADAPTERS[id].asksMidRun,
6536
+ resumes: ADAPTERS[id].resumes
6537
+ })),
5638
6538
  list: () => [...runs.values()],
5639
6539
  get: (runId) => runs.get(runId),
5640
6540
  launch,
5641
6541
  stop,
5642
- runCommand
6542
+ runCommand,
6543
+ exchanges: () => exchanges,
6544
+ newConversation
5643
6545
  };
5644
6546
  }
5645
- var DIRECTOR_SYSTEM_PROMPT, isActive;
6547
+ var DIRECTOR_SYSTEM_PROMPT, MAX_EXCHANGES, MAX_CONTEXT_ROOTS, isActive;
5646
6548
  var init_agents = __esm({
5647
6549
  async "packages/cli/src/agents.ts"() {
5648
6550
  "use strict";
5649
6551
  await init_src();
5650
- 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.";
6552
+ init_adapters();
6553
+ DIRECTOR_SYSTEM_PROMPT = [
6554
+ "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.",
6555
+ "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 the steps in execution order. Size it to the question: about 5 steps for the gist, every step that matters when asked for all of it or the details. 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.",
6556
+ "Each message ends with a line saying what the human's map shows right now. For walk me through this, the current map, these changes, or what changed: the blocks are that map's roots (for every change instead, call view with no roots and depth 1 and use its roots). Read them with get_block, then call explain with them in run order (callers before what they call). Each step's text is 2-4 sentences: what the block does, and for a changed block what changed there and why it matters. A tour or walkthrough request always ends in explain, never in only a view change.",
6557
+ "For a request to change the view (a lens, a flow, focusing something), territories, agents or the inbox: use those tools and do it.",
6558
+ "For a request to change the code (fix, add, rename, refactor, remove): the request is the go-ahead, so hand it to a worker without asking. Find the blocks it touches, create_territory with a short label, those blocks, and a note that states the task in full (the worker sees only the note and its blocks), then launch_agent on it. Reply in a line or two: what the worker will do, and that its edits will show on the map. Ask first (with ask_user) only when the request is too vague to write the note.",
6559
+ "To plan a change (plan a change to X, how would I add Y): trace the blocks involved, then call explain with the plan as steps on the blocks it touches (what changes in each and why), and create_territory for each piece of work with a note that states it in full. Use ask_user where the plan forks. Don't launch_agent until the human says go.",
6560
+ `When a request is ambiguous in a way that changes what you'd do (how many agents, which territories to create, how long a walkthrough, which of several areas), call ask_user with concrete options and end your turn; the answer comes as the next message. Don't ask when a sensible default exists, and never ask for a go-ahead on a change already requested. Never end a reply by offering choices in prose ("I can do A, or B. Which?"): put them in ask_user so the human can click one.`,
6561
+ "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.",
6562
+ "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."
6563
+ ].join("\n\n");
6564
+ MAX_EXCHANGES = 50;
6565
+ MAX_CONTEXT_ROOTS = 60;
5652
6566
  isActive = (status) => status === "starting" || status === "running";
5653
6567
  }
5654
6568
  });
@@ -5690,7 +6604,7 @@ var init_attribution = __esm({
5690
6604
 
5691
6605
  // packages/cli/src/editor.ts
5692
6606
  import { spawn } from "node:child_process";
5693
- import path16 from "node:path";
6607
+ import path18 from "node:path";
5694
6608
  function editorTemplate(flag) {
5695
6609
  return flag ?? process.env.TOPO_EDITOR ?? DEFAULT_EDITOR;
5696
6610
  }
@@ -5719,10 +6633,10 @@ function splitTemplate(template) {
5719
6633
  return out;
5720
6634
  }
5721
6635
  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)) {
6636
+ const root = path18.resolve(repo);
6637
+ const abs = path18.resolve(root, file);
6638
+ const rel = path18.relative(root, abs);
6639
+ if (rel === "" || rel.startsWith("..") || path18.isAbsolute(rel)) {
5726
6640
  throw new Error(`path outside repository: ${file}`);
5727
6641
  }
5728
6642
  return abs;
@@ -5784,6 +6698,18 @@ function createInbox(bus, onDecision) {
5784
6698
  return {
5785
6699
  list: () => items,
5786
6700
  add(input) {
6701
+ if (input.kind === "out_of_territory" && input.agent !== void 0) {
6702
+ const idx = items.findIndex(
6703
+ (it) => it.kind === input.kind && it.agent === input.agent && it.status === "open"
6704
+ );
6705
+ if (idx !== -1) {
6706
+ const [prev] = items.splice(idx, 1);
6707
+ const item2 = { ...prev, ...input, at: Date.now() };
6708
+ items.unshift(item2);
6709
+ bus.emit({ type: "inbox", item: item2 });
6710
+ return item2;
6711
+ }
6712
+ }
5787
6713
  const item = {
5788
6714
  ...input,
5789
6715
  id: `inbox-${++counter}`,
@@ -5926,6 +6852,72 @@ var init_conflicts = __esm({
5926
6852
  }
5927
6853
  });
5928
6854
 
6855
+ // packages/cli/src/reviews.ts
6856
+ import fs16 from "node:fs";
6857
+ import path19 from "node:path";
6858
+ function readDoc(repo) {
6859
+ try {
6860
+ const doc = JSON.parse(fs16.readFileSync(reviewsFile(repo), "utf8"));
6861
+ if (doc?.version === 1 && doc.reviews && typeof doc.reviews === "object") return doc;
6862
+ } catch {
6863
+ }
6864
+ return { version: 1, reviews: {} };
6865
+ }
6866
+ function readReviews(repo, key) {
6867
+ const saved = readDoc(repo).reviews[key];
6868
+ if (!saved || typeof saved !== "object") return /* @__PURE__ */ new Map();
6869
+ return new Map(Object.entries(saved).filter(([, r]) => isReview(r)));
6870
+ }
6871
+ function writeReviews(repo, key, reviews) {
6872
+ const doc = readDoc(repo);
6873
+ delete doc.reviews[key];
6874
+ if (reviews.size) doc.reviews[key] = Object.fromEntries(reviews);
6875
+ for (const old of Object.keys(doc.reviews).slice(0, -KEEP_REVIEWS)) delete doc.reviews[old];
6876
+ const file = reviewsFile(repo);
6877
+ fs16.mkdirSync(path19.dirname(file), { recursive: true });
6878
+ const ignore = path19.join(path19.dirname(file), ".gitignore");
6879
+ const ignored = fs16.existsSync(ignore) ? fs16.readFileSync(ignore, "utf8") : "";
6880
+ if (!/^reviews\.json$/m.test(ignored))
6881
+ fs16.appendFileSync(ignore, `${ignored && !ignored.endsWith("\n") ? "\n" : ""}reviews.json
6882
+ `);
6883
+ const tmp = `${file}.${process.pid}.tmp`;
6884
+ fs16.writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
6885
+ `);
6886
+ fs16.renameSync(tmp, file);
6887
+ }
6888
+ function fingerprint(change, head) {
6889
+ return `${change.changeType}:${head?.bodyHash ?? ""}`;
6890
+ }
6891
+ function currentMark(mark, change, head) {
6892
+ if (!mark) return { review: "pending" };
6893
+ if (mark.decision === "viewed" && mark.fp !== void 0 && mark.fp !== fingerprint(change, head))
6894
+ return { review: "pending", stale: true };
6895
+ return { review: mark.decision, ...mark.note && { note: mark.note } };
6896
+ }
6897
+ function askTerritory(agent, territories) {
6898
+ return agent && agent !== "you" ? territories.find((t) => t.agent === agent)?.id : void 0;
6899
+ }
6900
+ function askReply(asks) {
6901
+ return [
6902
+ "I reviewed your change in Topo. Please make these changes:",
6903
+ ...asks.map((a) => `- ${a.nodeId}: ${a.note ?? "needs another look"}`),
6904
+ "After each one, call report_change with one sentence on what you did."
6905
+ ].join("\n");
6906
+ }
6907
+ var KEEP_REVIEWS, reviewKey, reviewsFile, isReview;
6908
+ var init_reviews = __esm({
6909
+ "packages/cli/src/reviews.ts"() {
6910
+ "use strict";
6911
+ KEEP_REVIEWS = 20;
6912
+ reviewKey = (cs) => `${cs.baseSha}..${cs.headRef}`;
6913
+ reviewsFile = (repo) => path19.join(repo, ".topo", "reviews.json");
6914
+ isReview = (r) => {
6915
+ const x = r;
6916
+ return !!x && (x.decision === "viewed" || x.decision === "changes_requested") && (x.note === void 0 || typeof x.note === "string") && (x.fp === void 0 || typeof x.fp === "string");
6917
+ };
6918
+ }
6919
+ });
6920
+
5929
6921
  // packages/cli/src/live.ts
5930
6922
  function createEventBus() {
5931
6923
  const listeners = /* @__PURE__ */ new Set();
@@ -5980,8 +6972,19 @@ function labelFor(attribution, agent) {
5980
6972
  }
5981
6973
  function annotateChangeSet(changeSet, opts) {
5982
6974
  const blocks = changeSet.blocks.map((raw) => {
5983
- const verdict = opts.tracking.reviews.get(raw.nodeId);
5984
- const b = verdict ? { ...raw, review: verdict.decision, ...verdict.note && { reviewNote: verdict.note } } : raw;
6975
+ const mark = currentMark(
6976
+ opts.tracking.reviews.get(raw.nodeId),
6977
+ raw,
6978
+ opts.index.byId.get(raw.nodeId)
6979
+ );
6980
+ const agentTerritory = askTerritory(opts.tracking.blockAgent.get(raw.nodeId), opts.territories);
6981
+ const b = {
6982
+ ...raw,
6983
+ review: mark.review,
6984
+ ...mark.note && { reviewNote: mark.note },
6985
+ ...mark.stale && { viewedStale: true },
6986
+ ...agentTerritory && { agentTerritory }
6987
+ };
5985
6988
  const rationale = opts.tracking.rationale.get(b.nodeId);
5986
6989
  const agent = opts.tracking.blockAgent.get(b.nodeId);
5987
6990
  const territory = agent ? opts.territories.find((t) => t.agent === agent) : void 0;
@@ -6013,13 +7016,17 @@ function createLive(opts) {
6013
7016
  const territory = territories.find(
6014
7017
  (t) => t.agent === activity.agent || activity.session !== void 0 && t.agent === activity.session
6015
7018
  );
7019
+ const open = opts.inbox.list().find(
7020
+ (it) => it.kind === "out_of_territory" && it.agent === activity.agent && it.status === "open"
7021
+ );
7022
+ const all = [.../* @__PURE__ */ new Set([...open?.nodeIds ?? [], ...outOfTerritory])];
6016
7023
  opts.inbox.add({
6017
7024
  kind: "out_of_territory",
6018
- title: `${ownLabel} edited ${outOfTerritory.length} block${outOfTerritory.length === 1 ? "" : "s"} outside ${territory?.label ?? "its territory"}`,
7025
+ title: `${ownLabel} edited ${all.length} block${all.length === 1 ? "" : "s"} outside ${territory?.label ?? "its territory"}`,
6019
7026
  agent: activity.agent,
6020
7027
  label: activity.label,
6021
7028
  territoryId: territory?.id,
6022
- nodeIds: outOfTerritory
7029
+ nodeIds: all
6023
7030
  });
6024
7031
  }
6025
7032
  const changed = nodeIds.map((nodeId2) => ({
@@ -6113,12 +7120,13 @@ var init_live2 = __esm({
6113
7120
  await init_src();
6114
7121
  await init_attribution();
6115
7122
  await init_conflicts();
7123
+ init_reviews();
6116
7124
  }
6117
7125
  });
6118
7126
 
6119
7127
  // packages/cli/src/mcp.ts
6120
- import fs14 from "node:fs";
6121
- import path17 from "node:path";
7128
+ import fs17 from "node:fs";
7129
+ import path20 from "node:path";
6122
7130
  function qualifiedName(n) {
6123
7131
  const i = n.id.indexOf("#");
6124
7132
  return i === -1 ? n.name : n.id.slice(i + 1);
@@ -6181,6 +7189,38 @@ function requireString(args, key) {
6181
7189
  if (typeof v !== "string" || !v) throw new Error(`missing required "${key}"`);
6182
7190
  return v;
6183
7191
  }
7192
+ function parseQuestions(raw) {
7193
+ if (!Array.isArray(raw) || raw.length < 1 || raw.length > 4)
7194
+ throw new Error('"questions" must hold 1-4 {question, header?, options, multiSelect?} items');
7195
+ const problems = [];
7196
+ const questions = raw.map((q, i) => {
7197
+ const at = `question ${i + 1}`;
7198
+ const r = isRecord3(q) ? q : {};
7199
+ const question = str2(r.question);
7200
+ const header = str2(r.header);
7201
+ const opts = Array.isArray(r.options) ? r.options : [];
7202
+ if (!question) problems.push(`${at}: missing "question"`);
7203
+ if (header.length > 12) problems.push(`${at}: "header" is over 12 characters`);
7204
+ if (opts.length < 2 || opts.length > 4) problems.push(`${at}: give 2-4 options`);
7205
+ const options = opts.map((o, j) => {
7206
+ const label = str2(isRecord3(o) ? o.label : void 0);
7207
+ const description = str2(isRecord3(o) ? o.description : void 0);
7208
+ if (!label) problems.push(`${at}, option ${j + 1}: missing "label"`);
7209
+ return description ? { label, description } : { label };
7210
+ });
7211
+ if (new Set(options.map((o) => o.label)).size < options.length)
7212
+ problems.push(`${at}: option labels repeat`);
7213
+ return {
7214
+ question,
7215
+ ...header && { header },
7216
+ options,
7217
+ ...r.multiSelect === true && { multiSelect: true }
7218
+ };
7219
+ });
7220
+ if (problems.length)
7221
+ throw new Error(["nothing asked; fix these and call ask_user again:", ...problems].join("\n"));
7222
+ return questions;
7223
+ }
6184
7224
  function isToolAllowed(name, ctx) {
6185
7225
  const tool = TOOLS[name];
6186
7226
  if (!tool) return false;
@@ -6262,7 +7302,7 @@ function shapeViewTool(deps, args, ctx) {
6262
7302
  label,
6263
7303
  ...typeof args.mode === "string" && { mode: args.mode },
6264
7304
  ...roots && { roots },
6265
- view: isRecord2(args.view) ? args.view : {}
7305
+ view: isRecord3(args.view) ? args.view : {}
6266
7306
  };
6267
7307
  const problem = lensProblem(candidate);
6268
7308
  if (problem) throw new Error(`invalid lens: ${problem}`);
@@ -6287,25 +7327,25 @@ function shapeViewTool(deps, args, ctx) {
6287
7327
  return `proposed lens "${name}"${savedNote}${skipNote}; the human will approve or deny it`;
6288
7328
  }
6289
7329
  function saveLensToFile(repo, lens) {
6290
- const file = path17.join(repo, ".topo", "lenses.json");
7330
+ const file = path20.join(repo, ".topo", "lenses.json");
6291
7331
  let existing = [];
6292
7332
  try {
6293
- const raw = JSON.parse(fs14.readFileSync(file, "utf8"));
7333
+ const raw = JSON.parse(fs17.readFileSync(file, "utf8"));
6294
7334
  const list = Array.isArray(raw) ? raw : raw?.lenses;
6295
7335
  if (Array.isArray(list)) existing = list;
6296
7336
  } catch {
6297
7337
  }
6298
7338
  const byName = new Map(existing.map((l) => [l.name, l]));
6299
7339
  byName.set(lens.name, lens);
6300
- fs14.mkdirSync(path17.dirname(file), { recursive: true });
7340
+ fs17.mkdirSync(path20.dirname(file), { recursive: true });
6301
7341
  const tmp = `${file}.${process.pid}.tmp`;
6302
- fs14.writeFileSync(tmp, `${JSON.stringify({ lenses: [...byName.values()] }, null, 2)}
7342
+ fs17.writeFileSync(tmp, `${JSON.stringify({ lenses: [...byName.values()] }, null, 2)}
6303
7343
  `);
6304
- fs14.renameSync(tmp, file);
7344
+ fs17.renameSync(tmp, file);
6305
7345
  }
6306
7346
  async function callTool(deps, params, ctx) {
6307
7347
  const name = params.name;
6308
- const args = isRecord2(params.arguments) ? params.arguments : {};
7348
+ const args = isRecord3(params.arguments) ? params.arguments : {};
6309
7349
  if (typeof name !== "string" || !TOOLS[name]) {
6310
7350
  return { content: [{ type: "text", text: `unknown tool "${String(name)}"` }], isError: true };
6311
7351
  }
@@ -6330,13 +7370,13 @@ function createMcpHandler(deps) {
6330
7370
  } catch {
6331
7371
  return { status: 200, body: rpcError(null, -32700, "Parse error") };
6332
7372
  }
6333
- if (!isRecord2(msg) || msg.jsonrpc !== "2.0" || typeof msg.method !== "string") {
6334
- const id2 = isRecord2(msg) && typeof msg.id !== "undefined" ? msg.id : null;
7373
+ if (!isRecord3(msg) || msg.jsonrpc !== "2.0" || typeof msg.method !== "string") {
7374
+ const id2 = isRecord3(msg) && typeof msg.id !== "undefined" ? msg.id : null;
6335
7375
  return { status: 200, body: rpcError(id2, -32600, "Invalid Request") };
6336
7376
  }
6337
7377
  if (!("id" in msg)) return { status: 202 };
6338
7378
  const id = msg.id;
6339
- const params = isRecord2(msg.params) ? msg.params : {};
7379
+ const params = isRecord3(msg.params) ? msg.params : {};
6340
7380
  switch (msg.method) {
6341
7381
  case "ping":
6342
7382
  return { status: 200, body: rpcResult(id, {}) };
@@ -6373,18 +7413,23 @@ function createMcpHandler(deps) {
6373
7413
  }
6374
7414
  };
6375
7415
  }
6376
- var PROTOCOL_VERSIONS, DEFAULT_PROTOCOL_VERSION, INSTRUCTIONS, isRecord2, isStrArr, isDirection, isStatus, TOOLS, rpcResult, rpcError;
7416
+ var PROTOCOL_VERSIONS, DEFAULT_PROTOCOL_VERSION, INSTRUCTIONS, isRecord3, isStrArr, isDirection, isStatus, str2, MAX_LAYER_FILES, withCommand, MAX_STEPS, TOOLS, rpcResult, rpcError;
6377
7417
  var init_mcp = __esm({
6378
7418
  async "packages/cli/src/mcp.ts"() {
6379
7419
  "use strict";
6380
7420
  await init_src();
7421
+ init_permissions();
6381
7422
  PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
6382
7423
  DEFAULT_PROTOCOL_VERSION = "2025-06-18";
6383
7424
  INSTRUCTIONS = "This is the live code map the human reviewing this repo is looking at right now. Prefer these tools over grep when you need to know who calls something, what would break if you change it, or where something lives \u2014 they're backed by a resolved call graph, not text search.";
6384
- isRecord2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7425
+ isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6385
7426
  isStrArr = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
6386
7427
  isDirection = (v) => v === "out" || v === "in" || v === "both";
6387
7428
  isStatus = (v) => v === "proposed" || v === "active" || v === "review" || v === "done";
7429
+ str2 = (v) => typeof v === "string" ? v.trim() : "";
7430
+ MAX_LAYER_FILES = 300;
7431
+ withCommand = (ctx) => ctx.command ? { command: ctx.command } : {};
7432
+ MAX_STEPS = 40;
6388
7433
  TOOLS = {
6389
7434
  find: {
6390
7435
  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.",
@@ -6563,21 +7608,21 @@ var init_mcp = __esm({
6563
7608
  }
6564
7609
  },
6565
7610
  review_feedback: {
6566
- description: "The human's review of changed blocks: which need changes (with their note) and how many they approved. Use when you finish, or when told your change was reviewed. An agent Topo launched sees only its own territory and the blocks it changed.",
7611
+ description: "The human's review of changed blocks: which need changes (with their note) and how many they've looked at. Use when you finish, or when told your change was reviewed. An agent Topo launched sees only its own territory and the blocks it changed.",
6567
7612
  inputSchema: { type: "object", properties: {} },
6568
7613
  run: (deps, _args, ctx) => {
6569
7614
  const index = deps.getIndex();
6570
7615
  const run8 = ctx.runId ? deps.agents?.get(ctx.runId) : void 0;
6571
7616
  const territory = run8 && deps.getTerritories().find((t) => t.id === run8.territoryId);
6572
7617
  const mine = (id) => !territory || deps.tracking.blockAgent.get(id) === run8?.agent || outsideTerritory(index, territory.nodeIds, [id]).length === 0;
6573
- const reviewed = deps.getPayload().changeSet.blocks.filter((b) => b.review === "approved" || b.review === "changes_requested").filter((b) => mine(b.nodeId));
7618
+ const reviewed = deps.getPayload().changeSet.blocks.filter((b) => b.review === "viewed" || b.review === "changes_requested").filter((b) => mine(b.nodeId));
6574
7619
  if (!reviewed.length) return "no review feedback yet";
6575
7620
  const asks = reviewed.filter((b) => b.review === "changes_requested");
6576
7621
  return [
6577
7622
  ...asks.map(
6578
7623
  (b) => `${b.nodeId} CHANGES REQUESTED${b.reviewNote ? `: ${b.reviewNote}` : ""}`
6579
7624
  ),
6580
- `${reviewed.length - asks.length} block(s) approved`
7625
+ `${reviewed.length - asks.length} block(s) viewed, no changes asked`
6581
7626
  ].join("\n");
6582
7627
  }
6583
7628
  },
@@ -6596,16 +7641,23 @@ var init_mcp = __esm({
6596
7641
  const input = args.input;
6597
7642
  const run8 = deps.agents?.get(ctx.runId);
6598
7643
  const label = run8?.label ?? "An agent";
6599
- const detail = toolName === "Bash" && isRecord2(input) && typeof input.command === "string" ? input.command : JSON.stringify(input);
7644
+ const detail = toolName === "Bash" && isRecord3(input) && typeof input.command === "string" ? input.command : JSON.stringify(input);
7645
+ const allow = JSON.stringify({ behavior: "allow", updatedInput: input });
7646
+ if ((deps.autoAllowRules?.() ?? []).some((r) => ruleMatches(r, toolName, input)))
7647
+ return allow;
7648
+ const rule = ruleFor(toolName, input);
6600
7649
  const item = deps.inbox.add({
6601
7650
  kind: "permission",
6602
- title: `${label} wants to run ${toolName}: ${detail}`,
7651
+ // The command goes in detail, so the card can show it on one line as code.
7652
+ title: `${label} wants to run ${toolName}`,
7653
+ detail,
6603
7654
  agent: run8?.agent,
6604
7655
  label: run8?.label,
6605
- runId: ctx.runId
7656
+ runId: ctx.runId,
7657
+ ...rule && { rule, actions: ["approve", "always", "deny"] }
6606
7658
  });
6607
7659
  const status = await deps.inbox.wait(item.id, 10 * 60 * 1e3);
6608
- return status === "approved" ? JSON.stringify({ behavior: "allow", updatedInput: input }) : JSON.stringify({ behavior: "deny", message: "The human denied this in Topo." });
7660
+ return status === "approved" ? allow : JSON.stringify({ behavior: "deny", message: "The human denied this in Topo." });
6609
7661
  }
6610
7662
  },
6611
7663
  list_territories: {
@@ -6661,7 +7713,7 @@ var init_mcp = __esm({
6661
7713
  label: { type: "string" }
6662
7714
  }
6663
7715
  },
6664
- run: (deps, args) => {
7716
+ run: (deps, args, ctx) => {
6665
7717
  const index = deps.getIndex();
6666
7718
  const { ids: roots, skipped } = Array.isArray(args.roots) ? resolveRefs(index, args.roots) : { ids: [], skipped: [] };
6667
7719
  if (skipped.length && !roots.length) {
@@ -6673,11 +7725,16 @@ var init_mcp = __esm({
6673
7725
  label,
6674
7726
  ...typeof args.mode === "string" && { mode: args.mode },
6675
7727
  ...roots.length && { roots },
6676
- view: isRecord2(args.view) ? args.view : {}
7728
+ view: isRecord3(args.view) ? args.view : {}
6677
7729
  };
6678
7730
  const problem = lensProblem(candidate);
6679
7731
  if (problem) throw new Error(`invalid lens: ${problem}`);
6680
- deps.bus.emit({ type: "lens", lens: candidate, apply: true });
7732
+ deps.bus.emit({
7733
+ type: "lens",
7734
+ lens: candidate,
7735
+ apply: true,
7736
+ ...withCommand(ctx)
7737
+ });
6681
7738
  const skipNote = skipped.length ? `; unresolved roots skipped: ${skipped.join(", ")}` : "";
6682
7739
  return `applied view "${label}"${skipNote}`;
6683
7740
  }
@@ -6690,15 +7747,184 @@ var init_mcp = __esm({
6690
7747
  properties: { blocks: { type: "array", items: { type: "string" } } },
6691
7748
  required: ["blocks"]
6692
7749
  },
6693
- run: (deps, args) => {
7750
+ run: (deps, args, ctx) => {
6694
7751
  if (!Array.isArray(args.blocks)) throw new Error('missing required "blocks" (array)');
6695
7752
  const { ids, skipped } = resolveRefs(deps.getIndex(), args.blocks);
6696
7753
  if (!ids.length) throw new Error("none of those blocks resolved");
6697
- deps.bus.emit({ type: "focus", nodeIds: ids });
7754
+ deps.bus.emit({ type: "focus", nodeIds: ids, ...withCommand(ctx) });
6698
7755
  const skipNote = skipped.length ? `; unresolved blocks skipped: ${skipped.join(", ")}` : "";
6699
7756
  return `focused on ${ids.length} block(s)${skipNote}`;
6700
7757
  }
6701
7758
  },
7759
+ explain: {
7760
+ access: "director",
7761
+ description: `Walk the human through how something works, or through a change: the map tours these blocks in step order, numbered, with a card per step. Use for 'how does X work / where is X / what happens when / walk me through this or the changes' requests, after tracing with find/callees/callers/get_block. Give the steps in execution (or reading) order, sized to the question (about 5 for the gist, every step that matters for all of it, at most ${MAX_STEPS}), only blocks that matter; each step's text says what happens there in 2-4 sentences (for a changed block, also what changed and why it matters).`,
7762
+ inputSchema: {
7763
+ type: "object",
7764
+ properties: {
7765
+ title: { type: "string" },
7766
+ steps: {
7767
+ type: "array",
7768
+ items: {
7769
+ type: "object",
7770
+ properties: { block: { type: "string" }, text: { type: "string" } },
7771
+ required: ["block", "text"]
7772
+ }
7773
+ }
7774
+ },
7775
+ required: ["title", "steps"]
7776
+ },
7777
+ run: (deps, args, ctx) => {
7778
+ const title = requireString(args, "title");
7779
+ const raw = Array.isArray(args.steps) ? args.steps : [];
7780
+ if (!raw.length || raw.length > MAX_STEPS) {
7781
+ throw new Error(`"steps" must hold 1-${MAX_STEPS} {block, text} items`);
7782
+ }
7783
+ const index = deps.getIndex();
7784
+ const steps = [];
7785
+ const problems = [];
7786
+ raw.forEach((s, i) => {
7787
+ const ref = isRecord3(s) && typeof s.block === "string" ? s.block : "";
7788
+ const text = isRecord3(s) && typeof s.text === "string" ? s.text.trim() : "";
7789
+ const r = resolveOrText(index, ref);
7790
+ if ("node" in r) steps.push({ nodeId: r.node.id, text });
7791
+ else problems.push(`step ${i + 1} (${ref || "no block"}): ${r.text}`);
7792
+ });
7793
+ if (problems.length) {
7794
+ throw new Error(
7795
+ ["nothing shown; fix these steps and call explain again:", ...problems].join("\n")
7796
+ );
7797
+ }
7798
+ deps.bus.emit({ type: "explain", title, steps, ...withCommand(ctx) });
7799
+ return `showing "${title}": ${steps.length} step(s) on the map`;
7800
+ }
7801
+ },
7802
+ ask_user: {
7803
+ access: "director",
7804
+ description: "Ask the human 1-4 multiple-choice questions, shown as buttons under your reply (they can also type their own answer). Use it when the request is ambiguous in a way that changes what you'd do: how many agents, which territories to make, how long a walkthrough, which of several areas. Each option is a concrete choice you can act on. After calling it, end your turn with at most a line of reply; the answer arrives as the human's next message.",
7805
+ inputSchema: {
7806
+ type: "object",
7807
+ properties: {
7808
+ questions: {
7809
+ type: "array",
7810
+ minItems: 1,
7811
+ maxItems: 4,
7812
+ items: {
7813
+ type: "object",
7814
+ properties: {
7815
+ question: { type: "string" },
7816
+ header: { type: "string", description: "a label of 12 characters at most" },
7817
+ options: {
7818
+ type: "array",
7819
+ minItems: 2,
7820
+ maxItems: 4,
7821
+ items: {
7822
+ type: "object",
7823
+ properties: { label: { type: "string" }, description: { type: "string" } },
7824
+ required: ["label"]
7825
+ }
7826
+ },
7827
+ multiSelect: { type: "boolean" }
7828
+ },
7829
+ required: ["question", "options"]
7830
+ }
7831
+ }
7832
+ },
7833
+ required: ["questions"]
7834
+ },
7835
+ run: (deps, args, ctx) => {
7836
+ const questions = parseQuestions(args.questions);
7837
+ deps.bus.emit({ type: "ask", questions, ...withCommand(ctx) });
7838
+ return `asked ${questions.length} question(s); they show under your reply. End your turn now: the human's choice comes back as their next message.`;
7839
+ }
7840
+ },
7841
+ get_layers: {
7842
+ 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.",
7843
+ inputSchema: { type: "object", properties: {} },
7844
+ run: (deps) => {
7845
+ const graph = deps.getReview().head;
7846
+ const doc = readLayers(deps.repo);
7847
+ const lines = [];
7848
+ if (doc) {
7849
+ const { byFile: byFile2 } = layerMap(doc, graph);
7850
+ lines.push("layers (.topo/layers.json):");
7851
+ for (const l of doc.layers) {
7852
+ const n = Object.values(byFile2).filter((x) => x === l.name).length;
7853
+ lines.push(`${l.name} ${l.paths.join(", ")} (${n} files)`);
7854
+ }
7855
+ for (const r of doc.rules)
7856
+ lines.push(`rule: ${r.from} may not reach ${r.notTo}${r.why ? ` (${r.why})` : ""}`);
7857
+ } else {
7858
+ lines.push("no .topo/layers.json yet; folder-based draft:");
7859
+ for (const l of suggestLayers(graph).layers) lines.push(`${l.name} ${l.paths.join(", ")}`);
7860
+ }
7861
+ const byFile = doc ? layerMap(doc, graph).byFile : {};
7862
+ const blocks = /* @__PURE__ */ new Map();
7863
+ for (const n of graph.nodes)
7864
+ if (n.kind !== "file")
7865
+ blocks.set(n.file, [...blocks.get(n.file) ?? [], qualifiedName(n)]);
7866
+ const files = graph.nodes.filter((n) => n.kind === "file").map((n) => n.file).sort();
7867
+ lines.push("", `files (${files.length}):`);
7868
+ for (const f of files.slice(0, MAX_LAYER_FILES)) {
7869
+ const b = blocks.get(f) ?? [];
7870
+ const names = b.slice(0, 5).join(", ") + (b.length > 5 ? ` +${b.length - 5}` : "");
7871
+ lines.push(`${f} [${byFile[f] ?? (doc ? "no layer" : "-")}] ${names}`);
7872
+ }
7873
+ if (files.length > MAX_LAYER_FILES) lines.push(`\u2026 +${files.length - MAX_LAYER_FILES} more`);
7874
+ return lines.join("\n");
7875
+ }
7876
+ },
7877
+ set_layers: {
7878
+ access: "director",
7879
+ 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.",
7880
+ inputSchema: {
7881
+ type: "object",
7882
+ properties: {
7883
+ layers: {
7884
+ type: "array",
7885
+ items: {
7886
+ type: "object",
7887
+ properties: {
7888
+ name: { type: "string" },
7889
+ paths: { type: "array", items: { type: "string" } }
7890
+ },
7891
+ required: ["name", "paths"]
7892
+ }
7893
+ },
7894
+ rules: {
7895
+ type: "array",
7896
+ items: {
7897
+ type: "object",
7898
+ properties: {
7899
+ from: { type: "string" },
7900
+ notTo: { type: "string" },
7901
+ why: { type: "string" }
7902
+ },
7903
+ required: ["from", "notTo"]
7904
+ }
7905
+ }
7906
+ },
7907
+ required: ["layers"]
7908
+ },
7909
+ run: (deps, args) => {
7910
+ const rules = Array.isArray(args.rules) ? args.rules : readLayers(deps.repo)?.rules ?? [];
7911
+ const doc = parseLayers({ layers: args.layers, rules });
7912
+ if (!doc?.layers.length || doc.layers.some((l) => !l.name || !l.paths.length)) {
7913
+ throw new Error('"layers" must be a non-empty list of {name, paths: [glob, ...]}');
7914
+ }
7915
+ const map = layerMap(doc, deps.getReview().head);
7916
+ writeLayers(deps.repo, doc);
7917
+ deps.bus.emit({ type: "layers", layers: map });
7918
+ const files = deps.getReview().head.nodes.filter((n) => n.kind === "file").map((n) => n.file);
7919
+ const counts = doc.layers.map(
7920
+ (l) => `${l.name}: ${files.filter((f) => map.byFile[f] === l.name).length} files`
7921
+ );
7922
+ const loose = files.filter((f) => !map.byFile[f]);
7923
+ const looseNote = loose.length ? `
7924
+ ${loose.length} file(s) in no layer: ${loose.slice(0, 20).join(", ")}${loose.length > 20 ? " \u2026" : ""}` : "";
7925
+ return `wrote .topo/layers.json; ${counts.join(", ")}${looseNote}`;
7926
+ }
7927
+ },
6702
7928
  create_territory: {
6703
7929
  access: "director",
6704
7930
  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.",
@@ -6783,16 +8009,17 @@ var init_mcp = __esm({
6783
8009
  },
6784
8010
  launch_agent: {
6785
8011
  access: "director",
6786
- description: "Launch a worker agent on a territory. Use when the human asks to start work.",
8012
+ description: "Launch a worker agent on a territory. Use when the human asks to start work or asks for a code change (you can't edit code; the worker can). `agent` picks the CLI (claude, codex, gemini) when the human names one; otherwise the territory's own or the default.",
6787
8013
  inputSchema: {
6788
8014
  type: "object",
6789
- properties: { territory: { type: "string" } },
8015
+ properties: { territory: { type: "string" }, agent: { type: "string" } },
6790
8016
  required: ["territory"]
6791
8017
  },
6792
8018
  run: (deps, args) => {
6793
8019
  if (!deps.agents?.available) throw new Error("agent launching isn't available right now");
6794
8020
  const territory = requireString(args, "territory");
6795
- const run8 = deps.agents.launch(territory);
8021
+ const agent = typeof args.agent === "string" && args.agent ? args.agent : void 0;
8022
+ const run8 = deps.agents.launch(territory, void 0, agent);
6796
8023
  return `launched ${run8.label} (${run8.id}) on territory "${territory}"`;
6797
8024
  }
6798
8025
  },
@@ -6867,11 +8094,11 @@ var init_mcp = __esm({
6867
8094
  });
6868
8095
 
6869
8096
  // packages/cli/src/territories.ts
6870
- import fs15 from "node:fs";
6871
- import path18 from "node:path";
8097
+ import fs18 from "node:fs";
8098
+ import path21 from "node:path";
6872
8099
  function readTerritories(repo) {
6873
8100
  try {
6874
- const doc = parseTerritoryDoc(JSON.parse(fs15.readFileSync(territoriesFile(repo), "utf8")));
8101
+ const doc = parseTerritoryDoc(JSON.parse(fs18.readFileSync(territoriesFile(repo), "utf8")));
6875
8102
  if (doc) return doc;
6876
8103
  } catch {
6877
8104
  }
@@ -6879,11 +8106,11 @@ function readTerritories(repo) {
6879
8106
  }
6880
8107
  function writeTerritories(repo, doc) {
6881
8108
  const file = territoriesFile(repo);
6882
- fs15.mkdirSync(path18.dirname(file), { recursive: true });
8109
+ fs18.mkdirSync(path21.dirname(file), { recursive: true });
6883
8110
  const tmp = `${file}.${process.pid}.tmp`;
6884
- fs15.writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
8111
+ fs18.writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
6885
8112
  `);
6886
- fs15.renameSync(tmp, file);
8113
+ fs18.renameSync(tmp, file);
6887
8114
  }
6888
8115
  function parseTerritoryDoc(v) {
6889
8116
  const o = v;
@@ -6899,6 +8126,7 @@ function parseTerritoryDoc(v) {
6899
8126
  if (typeof r.label !== "string" || !isStrArr2(r.nodeIds)) return void 0;
6900
8127
  if (r.agent !== void 0 && typeof r.agent !== "string") return void 0;
6901
8128
  if (r.note !== void 0 && typeof r.note !== "string") return void 0;
8129
+ if (r.runner !== void 0 && typeof r.runner !== "string") return void 0;
6902
8130
  if (r.status !== void 0 && !STATUSES.includes(r.status)) return void 0;
6903
8131
  seen.add(r.id);
6904
8132
  territories.push({
@@ -6907,6 +8135,7 @@ function parseTerritoryDoc(v) {
6907
8135
  nodeIds: r.nodeIds,
6908
8136
  ...r.agent !== void 0 ? { agent: r.agent } : {},
6909
8137
  ...r.note !== void 0 ? { note: r.note } : {},
8138
+ ...r.runner !== void 0 ? { runner: r.runner } : {},
6910
8139
  ...r.status !== void 0 ? { status: r.status } : {}
6911
8140
  });
6912
8141
  }
@@ -6917,7 +8146,7 @@ var init_territories = __esm({
6917
8146
  "packages/cli/src/territories.ts"() {
6918
8147
  "use strict";
6919
8148
  SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6920
- territoriesFile = (repo) => path18.join(repo, ".topo", "territories.json");
8149
+ territoriesFile = (repo) => path21.join(repo, ".topo", "territories.json");
6921
8150
  isStrArr2 = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
6922
8151
  STATUSES = ["proposed", "active", "review", "done"];
6923
8152
  }
@@ -6925,9 +8154,9 @@ var init_territories = __esm({
6925
8154
 
6926
8155
  // packages/cli/src/server.ts
6927
8156
  import crypto from "node:crypto";
6928
- import fs16 from "node:fs";
8157
+ import fs19 from "node:fs";
6929
8158
  import http from "node:http";
6930
- import path19 from "node:path";
8159
+ import path22 from "node:path";
6931
8160
  import { fileURLToPath as fileURLToPath3 } from "node:url";
6932
8161
  function send(res, status, body, type) {
6933
8162
  res.writeHead(status, { "content-type": type, "cache-control": "no-store" });
@@ -6966,27 +8195,27 @@ function parseView(v) {
6966
8195
  }
6967
8196
  function parseLayout(v) {
6968
8197
  const o = v;
6969
- if (!isRecord3(o) || o.version !== 1 || !isRecord3(o.views) || !Object.values(o.views).every((pins) => isRecord3(pins) && Object.values(pins).every(isPos))) {
8198
+ if (!isRecord4(o) || o.version !== 1 || !isRecord4(o.views) || !Object.values(o.views).every((pins) => isRecord4(pins) && Object.values(pins).every(isPos))) {
6970
8199
  throw new HttpError(400, "invalid layout");
6971
8200
  }
6972
8201
  return { version: 1, views: o.views };
6973
8202
  }
6974
8203
  function serveStatic(res, staticDir, urlPath2) {
6975
- const root = path19.resolve(staticDir);
6976
- const index = path19.join(root, "index.html");
8204
+ const root = path22.resolve(staticDir);
8205
+ const index = path22.join(root, "index.html");
6977
8206
  let decoded;
6978
8207
  try {
6979
8208
  decoded = decodeURIComponent(urlPath2);
6980
8209
  } catch {
6981
8210
  throw new HttpError(400, "bad path");
6982
8211
  }
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";
8212
+ let file = path22.resolve(root, `.${decoded}`);
8213
+ if (file !== root && !file.startsWith(root + path22.sep)) throw new HttpError(403, "forbidden");
8214
+ if (!fs19.statSync(file, { throwIfNoEntry: false })?.isFile()) file = index;
8215
+ if (!fs19.existsSync(file)) return send(res, 200, MISSING_DIST, TYPES[".html"]);
8216
+ const type = TYPES[path22.extname(file).toLowerCase()] ?? "application/octet-stream";
6988
8217
  res.writeHead(200, { "content-type": type });
6989
- fs16.createReadStream(file).pipe(res);
8218
+ fs19.createReadStream(file).pipe(res);
6990
8219
  }
6991
8220
  function isLocalhostOrigin(origin) {
6992
8221
  try {
@@ -7002,7 +8231,7 @@ function constantTimeEqual(a, b) {
7002
8231
  }
7003
8232
  function parseCommandContext(v) {
7004
8233
  if (v === void 0) return void 0;
7005
- if (!isRecordLike(v) || v.mode !== void 0 && typeof v.mode !== "string" || v.roots !== void 0 && !isStrArr3(v.roots) || v.selected !== void 0 && !isStrArr3(v.selected)) {
8234
+ if (!isRecordLike(v) || v.mode !== void 0 && typeof v.mode !== "string" || v.roots !== void 0 && !isStrArr3(v.roots) || v.depth !== void 0 && !Number.isInteger(v.depth) || v.selected !== void 0 && !isStrArr3(v.selected)) {
7006
8235
  throw new HttpError(400, "invalid command context");
7007
8236
  }
7008
8237
  return v;
@@ -7023,12 +8252,15 @@ function createServer(opts) {
7023
8252
  if (origin && !isLocalhostOrigin(origin)) throw new HttpError(403, "forbidden origin");
7024
8253
  const rawRunId = req.headers["x-topo-run"];
7025
8254
  const runId = typeof rawRunId === "string" && opts.agents?.get(rawRunId) ? rawRunId : void 0;
8255
+ const director = constantTimeEqual(
8256
+ req.headers["x-topo-director"],
8257
+ opts.directorToken
8258
+ );
8259
+ const command = req.headers["x-topo-command"];
7026
8260
  const ctx = {
7027
8261
  runId,
7028
- director: constantTimeEqual(
7029
- req.headers["x-topo-director"],
7030
- opts.directorToken
7031
- )
8262
+ director,
8263
+ ...director && typeof command === "string" && { command }
7032
8264
  };
7033
8265
  const result = await opts.mcp(await readBody(req), ctx);
7034
8266
  if (result.body === void 0) {
@@ -7040,7 +8272,7 @@ function createServer(opts) {
7040
8272
  if (route === "GET /api/meta") {
7041
8273
  return json(res, 200, {
7042
8274
  api: API_VERSION,
7043
- languages: ["typescript", "javascript", "luau", "python", "rust"],
8275
+ languages: ["typescript", "javascript", "luau", "python", "rust", "c", "cpp"],
7044
8276
  mcp: "/mcp",
7045
8277
  events: "/api/events",
7046
8278
  docs: "https://github.com/zhn2605/topo/blob/main/docs/api.md"
@@ -7079,15 +8311,32 @@ function createServer(opts) {
7079
8311
  opts.liveEvents?.emit({ type: "territories", territories: doc.territories });
7080
8312
  return json(res, 200, doc);
7081
8313
  }
8314
+ if (route === "GET /api/settings" && opts.getSettings)
8315
+ return json(res, 200, opts.getSettings());
8316
+ if (route === "PUT /api/settings" && opts.saveSettings) {
8317
+ const body = await readJson(req);
8318
+ if (!PERMISSION_MODES.includes(body?.permissions)) {
8319
+ throw new HttpError(400, `expected {permissions: ${PERMISSION_MODES.join("|")}}`);
8320
+ }
8321
+ return json(res, 200, opts.saveSettings(body));
8322
+ }
7082
8323
  if (route === "GET /api/inbox" && opts.inbox) return json(res, 200, opts.inbox.list());
7083
8324
  const inboxDecide = req.method === "POST" && /^\/api\/inbox\/[^/]+$/.test(url.pathname);
7084
8325
  if (inboxDecide && opts.inbox) {
7085
8326
  const id = decodeURIComponent(url.pathname.slice("/api/inbox/".length));
7086
8327
  const body = await readJson(req);
7087
- if (body?.decision !== "approve" && body?.decision !== "deny" && body?.decision !== "dismiss") {
7088
- throw new HttpError(400, "expected {decision: approve|deny|dismiss}");
8328
+ const decision = body?.decision;
8329
+ if (decision !== "approve" && decision !== "always" && decision !== "deny" && decision !== "dismiss") {
8330
+ throw new HttpError(400, "expected {decision: approve|always|deny|dismiss}");
7089
8331
  }
7090
- const item = opts.inbox.decide(id, body.decision);
8332
+ if (decision === "always") {
8333
+ const open = opts.inbox.list().find((it) => it.id === id);
8334
+ if (!open?.rule || !open.actions.includes("always") || !opts.allowAlways) {
8335
+ throw new HttpError(400, "this item has no rule to always allow");
8336
+ }
8337
+ opts.allowAlways(open);
8338
+ }
8339
+ const item = opts.inbox.decide(id, decision === "always" ? "approve" : decision);
7091
8340
  if (!item) throw new HttpError(404, "no such inbox item");
7092
8341
  return json(res, 200, item);
7093
8342
  }
@@ -7097,10 +8346,11 @@ function createServer(opts) {
7097
8346
  try {
7098
8347
  const body = await readJson(req).catch(() => null);
7099
8348
  const reply = typeof body?.reply === "string" && body.reply.trim() ? body.reply : void 0;
8349
+ const agent = typeof body?.agent === "string" && body.agent ? body.agent : void 0;
7100
8350
  return json(
7101
8351
  res,
7102
8352
  200,
7103
- opts.agents.launch(decodeURIComponent(launchMatch[1]), reply)
8353
+ opts.agents.launch(decodeURIComponent(launchMatch[1]), reply, agent)
7104
8354
  );
7105
8355
  } catch (e) {
7106
8356
  throw new HttpError(400, e.message);
@@ -7120,6 +8370,10 @@ function createServer(opts) {
7120
8370
  const id = opts.agents.runCommand(body.text, parseCommandContext(body.context));
7121
8371
  return json(res, 200, { id });
7122
8372
  }
8373
+ if (route === "POST /api/command/new" && opts.agents) {
8374
+ opts.agents.newConversation();
8375
+ return json(res, 200, { ok: true });
8376
+ }
7123
8377
  if (route === "POST /api/open") {
7124
8378
  const body = await readJson(req);
7125
8379
  const line = Number(body?.line ?? 1);
@@ -7153,11 +8407,11 @@ function createServer(opts) {
7153
8407
  }
7154
8408
  if (route === "POST /api/blocks/review" && opts.reviewBlock) {
7155
8409
  const b = await readJson(req);
7156
- const decision = b?.decision;
7157
- if (typeof b?.nodeId !== "string" || decision !== "approved" && decision !== "changes_requested" && decision !== "pending" || b.note !== void 0 && (typeof b.note !== "string" || b.note.length > 4e3)) {
8410
+ const decision = b?.decision === "approved" ? "viewed" : b?.decision;
8411
+ if (typeof b?.nodeId !== "string" || decision !== "viewed" && decision !== "changes_requested" && decision !== "pending" || b.note !== void 0 && (typeof b.note !== "string" || b.note.length > 4e3)) {
7158
8412
  throw new HttpError(
7159
8413
  400,
7160
- "expected {nodeId, decision: approved|changes_requested|pending, note?}"
8414
+ "expected {nodeId, decision: viewed|changes_requested|pending, note?}"
7161
8415
  );
7162
8416
  }
7163
8417
  if (!opts.reviewBlock(b.nodeId, decision, b.note)) {
@@ -7165,6 +8419,15 @@ function createServer(opts) {
7165
8419
  }
7166
8420
  return json(res, 200, { ok: true });
7167
8421
  }
8422
+ if (route === "POST /api/blocks/ask" && opts.askChanges) {
8423
+ const b = await readJson(req);
8424
+ if (typeof b?.nodeId !== "string" || typeof b.note !== "string" || !b.note.trim() || b.note.length > 4e3) {
8425
+ throw new HttpError(400, "expected {nodeId, note} with a non-empty note");
8426
+ }
8427
+ const result = opts.askChanges(b.nodeId, b.note.trim());
8428
+ if (!result) throw new HttpError(404, "no such changed block");
8429
+ return json(res, 200, result);
8430
+ }
7168
8431
  if (route === "POST /api/activity" && opts.postActivity) {
7169
8432
  const body = await readJson(req);
7170
8433
  const accepted = opts.postActivity(body);
@@ -7188,17 +8451,18 @@ function createServer(opts) {
7188
8451
  }
7189
8452
  });
7190
8453
  }
7191
- var KEEPALIVE_MS, REPO_WEB_DIST, DEFAULT_STATIC_DIR, MAX_BODY, TYPES, MISSING_DIST, HttpError, isStrArr3, isPos, isRecord3, seenShapes, isRecordLike;
8454
+ var KEEPALIVE_MS, REPO_WEB_DIST, DEFAULT_STATIC_DIR, MAX_BODY, TYPES, MISSING_DIST, HttpError, isStrArr3, isPos, isRecord4, seenShapes, isRecordLike;
7192
8455
  var init_server = __esm({
7193
8456
  async "packages/cli/src/server.ts"() {
7194
8457
  "use strict";
7195
8458
  await init_src();
7196
8459
  init_editor();
8460
+ init_permissions();
7197
8461
  init_territories();
7198
8462
  KEEPALIVE_MS = 15e3;
7199
8463
  REPO_WEB_DIST = fileURLToPath3(new URL("../../../apps/web/dist/", import.meta.url));
7200
8464
  DEFAULT_STATIC_DIR = [fileURLToPath3(new URL("./web/", import.meta.url)), REPO_WEB_DIST].find(
7201
- (d) => fs16.existsSync(path19.join(d, "index.html"))
8465
+ (d) => fs19.existsSync(path22.join(d, "index.html"))
7202
8466
  ) ?? REPO_WEB_DIST;
7203
8467
  MAX_BODY = 1024 * 1024;
7204
8468
  TYPES = {
@@ -7229,15 +8493,15 @@ var init_server = __esm({
7229
8493
  };
7230
8494
  isStrArr3 = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
7231
8495
  isPos = (p) => Array.isArray(p) && p.length === 2 && p.every((n) => typeof n === "number" && Number.isFinite(n));
7232
- isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
8496
+ isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7233
8497
  seenShapes = /* @__PURE__ */ new Set();
7234
8498
  isRecordLike = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7235
8499
  }
7236
8500
  });
7237
8501
 
7238
8502
  // packages/cli/src/watch.ts
7239
- import fs17 from "node:fs";
7240
- import path20 from "node:path";
8503
+ import fs20 from "node:fs";
8504
+ import path23 from "node:path";
7241
8505
  function isIndexable(relPath) {
7242
8506
  const parts = relPath.split(/[\\/]/).filter(Boolean);
7243
8507
  const base = parts.at(-1);
@@ -7279,14 +8543,14 @@ function watchRepo(root, onBatch, opts = {}) {
7279
8543
  if (watchers.has(dir)) return;
7280
8544
  let w;
7281
8545
  try {
7282
- w = fs17.watch(dir, (_event, filename) => {
8546
+ w = fs20.watch(dir, (_event, filename) => {
7283
8547
  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);
8548
+ const abs = path23.join(dir, filename.toString());
8549
+ if (!watchers.has(abs) && fs20.statSync(abs, { throwIfNoEntry: false })?.isDirectory()) {
8550
+ if (!isIgnoredDir(path23.basename(abs))) walkDirs(abs);
7287
8551
  return;
7288
8552
  }
7289
- const rel = path20.relative(root, abs).split(path20.sep).join("/");
8553
+ const rel = path23.relative(root, abs).split(path23.sep).join("/");
7290
8554
  if (!isIndexable(rel)) return;
7291
8555
  pending.add(rel);
7292
8556
  scheduleFlush();
@@ -7304,11 +8568,11 @@ function watchRepo(root, onBatch, opts = {}) {
7304
8568
  watchDir(dir);
7305
8569
  let entries = [];
7306
8570
  try {
7307
- entries = fs17.readdirSync(dir, { withFileTypes: true });
8571
+ entries = fs20.readdirSync(dir, { withFileTypes: true });
7308
8572
  } catch {
7309
8573
  }
7310
8574
  for (const e of entries)
7311
- if (e.isDirectory() && !isIgnoredDir(e.name)) walkDirs(path20.join(dir, e.name));
8575
+ if (e.isDirectory() && !isIgnoredDir(e.name)) walkDirs(path23.join(dir, e.name));
7312
8576
  };
7313
8577
  walkDirs(root);
7314
8578
  return {
@@ -7333,8 +8597,8 @@ var init_watch = __esm({
7333
8597
 
7334
8598
  // packages/cli/src/worktrees.ts
7335
8599
  import { execFileSync } from "node:child_process";
7336
- import fs18 from "node:fs";
7337
- import path21 from "node:path";
8600
+ import fs21 from "node:fs";
8601
+ import path24 from "node:path";
7338
8602
  function listWorktrees(repo) {
7339
8603
  let out;
7340
8604
  try {
@@ -7348,17 +8612,17 @@ function listWorktrees(repo) {
7348
8612
  }
7349
8613
  const roots = out.split("\n").filter((l) => l.startsWith("worktree ")).map((l) => l.slice("worktree ".length));
7350
8614
  const own = roots.find((r) => isInside2(r, repo));
7351
- return roots.filter((r) => r !== own).map((root) => ({ root, name: path21.basename(root) }));
8615
+ return roots.filter((r) => r !== own).map((root) => ({ root, name: path24.basename(root) }));
7352
8616
  }
7353
8617
  function mapWorktreePath(repo, worktrees, file) {
7354
8618
  const wt = worktrees.filter((w) => isInside2(w.root, file)).sort((a, b) => b.root.length - a.root.length)[0];
7355
8619
  if (!wt) return void 0;
7356
8620
  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 };
8621
+ const sub = mainRoot ? path24.relative(mainRoot, repo) : "";
8622
+ const inTree = path24.relative(wt.root, file);
8623
+ const rel = sub ? path24.relative(sub, inTree) : inTree;
8624
+ if (rel.startsWith("..") || path24.isAbsolute(rel)) return void 0;
8625
+ return { rel: rel.split(path24.sep).join("/"), worktree: wt };
7362
8626
  }
7363
8627
  function gitRoot(dir) {
7364
8628
  try {
@@ -7372,7 +8636,7 @@ function gitRoot(dir) {
7372
8636
  }
7373
8637
  }
7374
8638
  function changedLines(base, changed) {
7375
- if (!fs18.existsSync(base) || !fs18.existsSync(changed)) return [];
8639
+ if (!fs21.existsSync(base) || !fs21.existsSync(changed)) return [];
7376
8640
  let text = "";
7377
8641
  try {
7378
8642
  execFileSync("git", ["diff", "--no-index", "-U0", "--no-color", base, changed], {
@@ -7405,8 +8669,8 @@ var init_worktrees = __esm({
7405
8669
  "packages/cli/src/worktrees.ts"() {
7406
8670
  "use strict";
7407
8671
  isInside2 = (dir, p) => {
7408
- const rel = path21.relative(dir, p);
7409
- return rel === "" || !rel.startsWith("..") && !path21.isAbsolute(rel);
8672
+ const rel = path24.relative(dir, p);
8673
+ return rel === "" || !rel.startsWith("..") && !path24.isAbsolute(rel);
7410
8674
  };
7411
8675
  }
7412
8676
  });
@@ -7422,8 +8686,8 @@ __export(review_exports, {
7422
8686
  });
7423
8687
  import { spawn as spawn2 } from "node:child_process";
7424
8688
  import crypto2 from "node:crypto";
7425
- import fs19 from "node:fs";
7426
- import path22 from "node:path";
8689
+ import fs22 from "node:fs";
8690
+ import path25 from "node:path";
7427
8691
  import { parseArgs as parseArgs4 } from "node:util";
7428
8692
  function openBrowser(url) {
7429
8693
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
@@ -7462,7 +8726,7 @@ function run4(argv) {
7462
8726
  if (!base) throw new Error(`missing <base> ref
7463
8727
 
7464
8728
  ${HELP4}`);
7465
- const repo = path22.resolve(values.repo ?? process.cwd());
8729
+ const repo = path25.resolve(values.repo ?? process.cwd());
7466
8730
  return serve({
7467
8731
  compute: () => computeReview(repo, base, values.head),
7468
8732
  repo,
@@ -7477,7 +8741,7 @@ ${HELP4}`);
7477
8741
  }
7478
8742
  function readLayout(repo) {
7479
8743
  try {
7480
- const doc = JSON.parse(fs19.readFileSync(layoutFile(repo), "utf8"));
8744
+ const doc = JSON.parse(fs22.readFileSync(layoutFile(repo), "utf8"));
7481
8745
  if (doc?.version === 1 && doc.views && typeof doc.views === "object") return doc;
7482
8746
  } catch {
7483
8747
  }
@@ -7485,12 +8749,12 @@ function readLayout(repo) {
7485
8749
  }
7486
8750
  function writeLayout(repo, doc) {
7487
8751
  const file = layoutFile(repo);
7488
- fs19.mkdirSync(path22.dirname(file), { recursive: true });
8752
+ fs22.mkdirSync(path25.dirname(file), { recursive: true });
7489
8753
  const tmp = `${file}.${process.pid}.tmp`;
7490
8754
  const text = JSON.stringify(doc, null, 2).replace(/\[\s+(-?\d+),\s+(-?\d+)\s+\]/g, "[$1, $2]");
7491
- fs19.writeFileSync(tmp, `${text}
8755
+ fs22.writeFileSync(tmp, `${text}
7492
8756
  `);
7493
- fs19.renameSync(tmp, file);
8757
+ fs22.renameSync(tmp, file);
7494
8758
  }
7495
8759
  function serve(opts) {
7496
8760
  const port = Number(opts.port);
@@ -7499,6 +8763,7 @@ function serve(opts) {
7499
8763
  const { repo } = opts;
7500
8764
  const editorCommand = editorTemplate(opts.editor);
7501
8765
  const tracking = { blockAgent: /* @__PURE__ */ new Map(), rationale: /* @__PURE__ */ new Map(), reviews: /* @__PURE__ */ new Map() };
8766
+ let marksFor;
7502
8767
  const compute = () => {
7503
8768
  const review = opts.compute();
7504
8769
  tagHubs(review.head);
@@ -7509,6 +8774,11 @@ function serve(opts) {
7509
8774
  lenses: loadLenses(repo),
7510
8775
  territories
7511
8776
  };
8777
+ const key = reviewKey(payload.changeSet);
8778
+ if (key !== marksFor) {
8779
+ marksFor = key;
8780
+ tracking.reviews = readReviews(repo, key);
8781
+ }
7512
8782
  payload.changeSet = annotateChangeSet(payload.changeSet, { territories, tracking, index });
7513
8783
  return { review, payload, index };
7514
8784
  };
@@ -7539,7 +8809,7 @@ function serve(opts) {
7539
8809
  const nodeIds = blocksAt(
7540
8810
  state.index,
7541
8811
  parsed.file,
7542
- changedLines(path22.join(repo, parsed.file), parsed.absFile)
8812
+ changedLines(path25.join(repo, parsed.file), parsed.absFile)
7543
8813
  );
7544
8814
  const run9 = agents.list().findLast((r) => r.agent === parsed.agent || r.agent === parsed.session);
7545
8815
  const { absFile: _abs, worktree, ...activity } = parsed;
@@ -7571,6 +8841,7 @@ function serve(opts) {
7571
8841
  return true;
7572
8842
  };
7573
8843
  const directorToken = crypto2.randomBytes(24).toString("hex");
8844
+ const permissionMode = () => opts.skipPermissions ? "never" : readSettings(repo).permissions;
7574
8845
  let boundPort = port;
7575
8846
  const agents = createAgentManager({
7576
8847
  repo,
@@ -7581,7 +8852,9 @@ function serve(opts) {
7581
8852
  saveTerritories: saveTerritoriesAndEmit,
7582
8853
  directorToken,
7583
8854
  getIndex: () => state.index,
7584
- skipPermissions: opts.skipPermissions
8855
+ permissionMode,
8856
+ // Agents without Claude's edit hook report their edits in their own stream.
8857
+ onEdit: (agent, file) => postActivity({ agent, file })
7585
8858
  });
7586
8859
  let live;
7587
8860
  let watcher;
@@ -7620,15 +8893,47 @@ function serve(opts) {
7620
8893
  saveTerritories: saveTerritoriesAndEmit,
7621
8894
  inbox,
7622
8895
  agents,
7623
- tracking
8896
+ tracking,
8897
+ autoAllowRules: () => {
8898
+ const mode = permissionMode();
8899
+ return mode === "never" ? [] : [...mode === "safe" ? SAFE_RULES : [], ...readLocalAllow(repo)];
8900
+ }
7624
8901
  });
8902
+ const markBlock = (nodeId2, decision, note) => {
8903
+ const block2 = state.payload.changeSet.blocks.find((b) => b.nodeId === nodeId2);
8904
+ if (!block2) return false;
8905
+ if (decision === "pending") tracking.reviews.delete(nodeId2);
8906
+ else
8907
+ tracking.reviews.set(nodeId2, {
8908
+ decision,
8909
+ ...note && { note },
8910
+ ...decision === "viewed" && { fp: fingerprint(block2, state.index.byId.get(nodeId2)) }
8911
+ });
8912
+ block2.review = decision;
8913
+ delete block2.viewedStale;
8914
+ if (note && decision === "changes_requested") block2.reviewNote = note;
8915
+ else delete block2.reviewNote;
8916
+ try {
8917
+ writeReviews(repo, reviewKey(state.payload.changeSet), tracking.reviews);
8918
+ } catch (e) {
8919
+ console.error(
8920
+ `topo: couldn't save .topo/reviews.json: ${e instanceof Error ? e.message : e}`
8921
+ );
8922
+ }
8923
+ return true;
8924
+ };
7625
8925
  const server = createServer({
7626
8926
  getPayload: (refresh) => {
7627
8927
  if (refresh) state = compute();
7628
8928
  state.payload.inbox = inbox.list();
7629
8929
  state.payload.runs = agents.list();
7630
- state.payload.commandBar = agents.available ? "agent" : "search";
8930
+ state.payload.commandBar = agents.director ? "agent" : "search";
8931
+ state.payload.agentClis = agents.agents();
8932
+ state.payload.exchanges = agents.exchanges();
7631
8933
  if (opts.skipPermissions) state.payload.skipPermissions = true;
8934
+ state.payload.agentPermissions = permissionMode();
8935
+ const layers = readLayers(repo);
8936
+ state.payload.layers = layers && layerMap(layers, state.review.head);
7632
8937
  return state.payload;
7633
8938
  },
7634
8939
  index: () => state.index,
@@ -7641,22 +8946,39 @@ function serve(opts) {
7641
8946
  directorToken,
7642
8947
  inbox,
7643
8948
  agents,
8949
+ allowAlways: (item) => item.rule && addLocalAllowRule(repo, item.rule),
8950
+ getSettings: () => readSettings(repo),
8951
+ saveSettings: (raw) => {
8952
+ const s = parseSettings(raw);
8953
+ writeSettings(repo, s);
8954
+ return s;
8955
+ },
7644
8956
  liveEvents: bus,
7645
8957
  postActivity,
7646
- reviewBlock: (nodeId2, decision, note) => {
8958
+ reviewBlock: markBlock,
8959
+ askChanges: (nodeId2, note) => {
8960
+ if (!markBlock(nodeId2, "changes_requested", note)) return void 0;
7647
8961
  const block2 = state.payload.changeSet.blocks.find((b) => b.nodeId === nodeId2);
7648
- if (!block2) return false;
7649
- if (decision === "pending") {
7650
- tracking.reviews.delete(nodeId2);
7651
- block2.review = "pending";
7652
- delete block2.reviewNote;
7653
- } else {
7654
- tracking.reviews.set(nodeId2, { decision, ...note && { note } });
7655
- block2.review = decision;
7656
- if (note) block2.reviewNote = note;
7657
- else delete block2.reviewNote;
8962
+ try {
8963
+ const own = block2?.agentTerritory;
8964
+ if (own) {
8965
+ agents.launch(own, askReply([{ nodeId: nodeId2, note }]));
8966
+ return { to: "agent", territoryId: own };
8967
+ }
8968
+ const territories = getTerritories();
8969
+ const label = `Fix ${state.index.byId.get(nodeId2)?.name ?? nodeId2}`;
8970
+ const id = uniqueSlug(slugify(label), territories);
8971
+ saveTerritoriesAndEmit([
8972
+ ...territories,
8973
+ { id, label, nodeIds: [nodeId2], note: `${note}
8974
+
8975
+ The block: ${nodeId2}` }
8976
+ ]);
8977
+ agents.launch(id);
8978
+ return { to: "worker", territoryId: id };
8979
+ } catch (e) {
8980
+ return { to: "saved", error: e instanceof Error ? e.message : String(e) };
7658
8981
  }
7659
- return true;
7660
8982
  },
7661
8983
  staticDir: DEFAULT_STATIC_DIR,
7662
8984
  editorCommand,
@@ -7695,6 +9017,8 @@ var init_review2 = __esm({
7695
9017
  init_inbox();
7696
9018
  await init_live2();
7697
9019
  await init_mcp();
9020
+ init_permissions();
9021
+ init_reviews();
7698
9022
  await init_server();
7699
9023
  init_territories();
7700
9024
  init_watch();
@@ -7716,7 +9040,7 @@ Options:
7716
9040
  --dangerously-skip-permissions launched agents run with no permission prompts (like Claude Code's
7717
9041
  flag of the same name): nothing waits in the Inbox. For this server session only.
7718
9042
  -h, --help`;
7719
- layoutFile = (repo) => path22.join(repo, ".topo", "layout.json");
9043
+ layoutFile = (repo) => path25.join(repo, ".topo", "layout.json");
7720
9044
  }
7721
9045
  });
7722
9046
 
@@ -7727,8 +9051,8 @@ __export(export_exports, {
7727
9051
  run: () => run5,
7728
9052
  snapshotPage: () => snapshotPage
7729
9053
  });
7730
- import fs20 from "node:fs";
7731
- import path23 from "node:path";
9054
+ import fs23 from "node:fs";
9055
+ import path26 from "node:path";
7732
9056
  import { parseArgs as parseArgs5 } from "node:util";
7733
9057
  function snapshotPage(template, data) {
7734
9058
  const json2 = JSON.stringify(data).replaceAll("<", "\\u003c");
@@ -7758,7 +9082,7 @@ function run5(argv) {
7758
9082
  if (!base) throw new Error(`missing <base> ref
7759
9083
 
7760
9084
  ${HELP5}`);
7761
- const repo = path23.resolve(values.repo ?? process.cwd());
9085
+ const repo = path26.resolve(values.repo ?? process.cwd());
7762
9086
  const review = computeReview(repo, base, values.head);
7763
9087
  const payload = { ...toPayload(repo, review, ""), live: void 0 };
7764
9088
  const sources = {};
@@ -7768,14 +9092,14 @@ ${HELP5}`);
7768
9092
  if (s) sources[`${side} ${b.nodeId}`] = s;
7769
9093
  }
7770
9094
  }
7771
- const templateFile = path23.join(DEFAULT_STATIC_DIR, "snapshot.html");
7772
- if (!fs20.existsSync(templateFile)) {
9095
+ const templateFile = path26.join(DEFAULT_STATIC_DIR, "snapshot.html");
9096
+ if (!fs23.existsSync(templateFile)) {
7773
9097
  throw new Error("the web UI isn't built: run `pnpm --filter @topo/web build` first");
7774
9098
  }
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);
9099
+ const template = fs23.readFileSync(templateFile, "utf8");
9100
+ const out = path26.resolve(values.out);
9101
+ fs23.writeFileSync(out, snapshotPage(template, { payload, sources }));
9102
+ const kb = Math.round(fs23.statSync(out).size / 1024);
7779
9103
  console.log(`topo: ${review.changeSet.blocks.length} changed block(s) \u2192 ${out} (${kb} kB)`);
7780
9104
  }
7781
9105
  var HELP5;
@@ -7804,8 +9128,8 @@ __export(layers_exports, {
7804
9128
  HELP: () => HELP6,
7805
9129
  run: () => run6
7806
9130
  });
7807
- import fs21 from "node:fs";
7808
- import path24 from "node:path";
9131
+ import fs24 from "node:fs";
9132
+ import path27 from "node:path";
7809
9133
  import { parseArgs as parseArgs6 } from "node:util";
7810
9134
  function run6(argv) {
7811
9135
  const { values, positionals } = parseArgs6({
@@ -7817,18 +9141,18 @@ function run6(argv) {
7817
9141
  console.log(HELP6);
7818
9142
  return;
7819
9143
  }
7820
- const repo = path24.resolve(positionals[0] ?? process.cwd());
9144
+ const repo = path27.resolve(positionals[0] ?? process.cwd());
7821
9145
  const graph = indexRepo(repo);
7822
9146
  let doc = readLayers(repo);
7823
9147
  if (values.suggest) {
7824
9148
  if (doc) {
7825
9149
  console.log(
7826
- `topo: ${path24.relative(process.cwd(), layersFile(repo)) || layersFile(repo)} already exists; edit it instead.`
9150
+ `topo: ${path27.relative(process.cwd(), layersFile(repo)) || layersFile(repo)} already exists; edit it instead.`
7827
9151
  );
7828
9152
  } else {
7829
9153
  doc = suggestLayers(graph);
7830
- fs21.mkdirSync(path24.dirname(layersFile(repo)), { recursive: true });
7831
- fs21.writeFileSync(layersFile(repo), `${JSON.stringify(doc, null, 2)}
9154
+ fs24.mkdirSync(path27.dirname(layersFile(repo)), { recursive: true });
9155
+ fs24.writeFileSync(layersFile(repo), `${JSON.stringify(doc, null, 2)}
7832
9156
  `);
7833
9157
  console.log(
7834
9158
  `topo: wrote ${doc.layers.length} suggested layer(s) to ${layersFile(repo)}; name them, merge them, and add rules.`
@@ -7878,7 +9202,7 @@ __export(map_exports, {
7878
9202
  HELP: () => HELP7,
7879
9203
  run: () => run7
7880
9204
  });
7881
- import path25 from "node:path";
9205
+ import path28 from "node:path";
7882
9206
  import { parseArgs as parseArgs7 } from "node:util";
7883
9207
  function run7(argv) {
7884
9208
  const { values, positionals } = parseArgs7({
@@ -7901,7 +9225,7 @@ function run7(argv) {
7901
9225
  return;
7902
9226
  }
7903
9227
  if (!values.cache) process.env.TOPO_NO_CACHE = "1";
7904
- const dir = path25.resolve(positionals[0] ?? process.cwd());
9228
+ const dir = path28.resolve(positionals[0] ?? process.cwd());
7905
9229
  return serve({
7906
9230
  compute: () => computeExplore(dir),
7907
9231
  repo: dir,
@@ -7984,7 +9308,7 @@ async function main(argv) {
7984
9308
  ${HELP8}`);
7985
9309
  await (await cmd()).run(rest);
7986
9310
  }
7987
- if (import.meta.main) {
9311
+ if (true) {
7988
9312
  main(process.argv.slice(2)).catch((e) => {
7989
9313
  console.error(`topo: ${friendlyError(e)}`);
7990
9314
  process.exit(1);