@askexenow/exe-os 0.9.69 → 0.9.70

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.
Files changed (75) hide show
  1. package/deploy/stack-manifests/v0.9.json +96 -16
  2. package/dist/bin/agentic-ontology-backfill.js +6 -0
  3. package/dist/bin/agentic-reflection-backfill.js +6 -0
  4. package/dist/bin/agentic-semantic-label.js +6 -0
  5. package/dist/bin/backfill-conversations.js +6 -0
  6. package/dist/bin/backfill-responses.js +6 -0
  7. package/dist/bin/backfill-vectors.js +6 -0
  8. package/dist/bin/bulk-sync-postgres.js +6 -0
  9. package/dist/bin/cleanup-stale-review-tasks.js +6 -0
  10. package/dist/bin/cli.js +1257 -178
  11. package/dist/bin/exe-agent.js +6 -0
  12. package/dist/bin/exe-assign.js +6 -0
  13. package/dist/bin/exe-boot.js +6 -0
  14. package/dist/bin/exe-call.js +6 -0
  15. package/dist/bin/exe-cloud.js +6 -0
  16. package/dist/bin/exe-dispatch.js +6 -0
  17. package/dist/bin/exe-doctor.js +6 -0
  18. package/dist/bin/exe-export-behaviors.js +6 -0
  19. package/dist/bin/exe-forget.js +6 -0
  20. package/dist/bin/exe-gateway.js +151 -110
  21. package/dist/bin/exe-heartbeat.js +6 -0
  22. package/dist/bin/exe-kill.js +6 -0
  23. package/dist/bin/exe-launch-agent.js +6 -0
  24. package/dist/bin/exe-new-employee.js +6 -0
  25. package/dist/bin/exe-pending-messages.js +6 -0
  26. package/dist/bin/exe-pending-notifications.js +6 -0
  27. package/dist/bin/exe-pending-reviews.js +6 -0
  28. package/dist/bin/exe-rename.js +13 -4
  29. package/dist/bin/exe-review.js +6 -0
  30. package/dist/bin/exe-search.js +6 -0
  31. package/dist/bin/exe-session-cleanup.js +6 -0
  32. package/dist/bin/exe-start-codex.js +6 -0
  33. package/dist/bin/exe-start-opencode.js +6 -0
  34. package/dist/bin/exe-status.js +6 -0
  35. package/dist/bin/exe-team.js +6 -0
  36. package/dist/bin/git-sweep.js +6 -0
  37. package/dist/bin/graph-backfill.js +150 -110
  38. package/dist/bin/graph-export.js +6 -0
  39. package/dist/bin/intercom-check.js +6 -0
  40. package/dist/bin/registry-proxy.js +207 -0
  41. package/dist/bin/scan-tasks.js +6 -0
  42. package/dist/bin/setup.js +6 -0
  43. package/dist/bin/shard-migrate.js +6 -0
  44. package/dist/bin/stack-update.js +128 -0
  45. package/dist/gateway/index.js +151 -110
  46. package/dist/hooks/bug-report-worker.js +6 -0
  47. package/dist/hooks/codex-stop-task-finalizer.js +6 -0
  48. package/dist/hooks/commit-complete.js +6 -0
  49. package/dist/hooks/error-recall.js +6 -0
  50. package/dist/hooks/ingest.js +6 -0
  51. package/dist/hooks/instructions-loaded.js +6 -0
  52. package/dist/hooks/notification.js +6 -0
  53. package/dist/hooks/post-compact.js +6 -0
  54. package/dist/hooks/post-tool-combined.js +6 -0
  55. package/dist/hooks/pre-compact.js +6 -0
  56. package/dist/hooks/pre-tool-use.js +6 -0
  57. package/dist/hooks/prompt-submit.js +6 -0
  58. package/dist/hooks/session-end.js +6 -0
  59. package/dist/hooks/session-start.js +6 -0
  60. package/dist/hooks/stop.js +6 -0
  61. package/dist/hooks/subagent-stop.js +6 -0
  62. package/dist/hooks/summary-worker.js +6 -0
  63. package/dist/index.js +151 -110
  64. package/dist/lib/employee-templates.js +6 -0
  65. package/dist/lib/exe-daemon.js +382 -234
  66. package/dist/lib/hybrid-search.js +6 -0
  67. package/dist/lib/registry-proxy.js +162 -0
  68. package/dist/lib/schedules.js +6 -0
  69. package/dist/lib/store.js +6 -0
  70. package/dist/mcp/server.js +318 -222
  71. package/dist/runtime/index.js +6 -0
  72. package/dist/tui/App.js +6 -0
  73. package/package.json +3 -2
  74. package/stack.release.json +6 -4
  75. package/stack.release.schema.json +89 -18
@@ -4772,6 +4772,12 @@ var init_platform_procedures = __esm({
4772
4772
  priority: "p0",
4773
4773
  content: "exe-build-adv is MANDATORY for ALL work touching 3+ files. Run /exe-build-adv --auto BEFORE implementation. Pipeline: Spec \u2192 AC \u2192 Tests \u2192 Evaluate \u2192 Fix. No multi-file feature ships without pipeline artifacts. No exceptions \u2014 managers reject work without them."
4774
4774
  },
4775
+ {
4776
+ title: "Code context first for repository orientation",
4777
+ domain: "workflow",
4778
+ priority: "p1",
4779
+ content: "Before broad repo exploration, symbol tracing, blast-radius review, or codebase Q&A, agents should use the consolidated code_context MCP tool instead of manual grep/read loops. Use action=index or stats to refresh/check the index; action=search with query, limit, offset, languages, paths, refresh_index for fresh multi-language code/doc search; action=trace for symbol imports/dependents; action=blast_radius for impact analysis before edits. CLI parity exists via exe-os code-context init|index|status|stats|search|doctor. Keep code_context separate from durable employee memory: promote only validated decisions, procedures, or lessons into store_memory/commit_memory."
4780
+ },
4775
4781
  {
4776
4782
  title: "Commit discipline \u2014 never leave verified work floating",
4777
4783
  domain: "workflow",
@@ -18331,13 +18337,75 @@ import crypto13 from "crypto";
18331
18337
 
18332
18338
  // src/lib/code-chunker.ts
18333
18339
  import ts from "typescript";
18340
+ var LANGUAGE_BY_EXTENSION = {
18341
+ c: "c",
18342
+ cc: "cpp",
18343
+ cpp: "cpp",
18344
+ cxx: "cpp",
18345
+ h: "c",
18346
+ hh: "cpp",
18347
+ hpp: "cpp",
18348
+ cs: "csharp",
18349
+ css: "css",
18350
+ scss: "scss",
18351
+ f: "fortran",
18352
+ f90: "fortran",
18353
+ f95: "fortran",
18354
+ go: "go",
18355
+ html: "html",
18356
+ htm: "html",
18357
+ java: "java",
18358
+ js: "javascript",
18359
+ jsx: "javascript",
18360
+ json: "json",
18361
+ kt: "kotlin",
18362
+ kts: "kotlin",
18363
+ lua: "lua",
18364
+ md: "markdown",
18365
+ mdx: "mdx",
18366
+ pas: "pascal",
18367
+ php: "php",
18368
+ py: "python",
18369
+ r: "r",
18370
+ rb: "ruby",
18371
+ rs: "rust",
18372
+ scala: "scala",
18373
+ sc: "scala",
18374
+ sol: "solidity",
18375
+ sql: "sql",
18376
+ svelte: "svelte",
18377
+ swift: "swift",
18378
+ toml: "toml",
18379
+ ts: "typescript",
18380
+ tsx: "typescript",
18381
+ vue: "vue",
18382
+ xml: "xml",
18383
+ yaml: "yaml",
18384
+ yml: "yaml",
18385
+ sh: "shell",
18386
+ bash: "shell",
18387
+ zsh: "shell"
18388
+ };
18389
+ var TEXT_LIKE_LANGUAGES = /* @__PURE__ */ new Set(["json", "markdown", "mdx", "toml", "yaml", "xml", "html", "css", "scss", "sql"]);
18390
+ function languageForFile(filePath) {
18391
+ const base = filePath.split(/[\\/]/).pop()?.toLowerCase() ?? "";
18392
+ if (["dockerfile", "makefile", "rakefile", "gemfile"].includes(base)) return base;
18393
+ const ext = base.includes(".") ? base.split(".").pop() : void 0;
18394
+ return ext ? LANGUAGE_BY_EXTENSION[ext] : void 0;
18395
+ }
18334
18396
  function chunkSourceFile(source, fileName = "file.ts") {
18397
+ const language = languageForFile(fileName);
18398
+ if (language === "typescript" || language === "javascript") {
18399
+ return chunkTypeScriptLike(source, fileName);
18400
+ }
18401
+ return chunkGenericSource(source, fileName, language);
18402
+ }
18403
+ function chunkTypeScriptLike(source, fileName) {
18335
18404
  const sourceFile = ts.createSourceFile(
18336
18405
  fileName,
18337
18406
  source,
18338
18407
  ts.ScriptTarget.Latest,
18339
18408
  true,
18340
- // setParentNodes
18341
18409
  fileName.endsWith(".tsx") || fileName.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
18342
18410
  );
18343
18411
  const chunks = [];
@@ -18359,144 +18427,117 @@ function chunkSourceFile(source, fileName = "file.ts") {
18359
18427
  if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)) {
18360
18428
  return node.name?.getText(sourceFile) ?? "(anonymous)";
18361
18429
  }
18362
- if (ts.isClassDeclaration(node)) {
18363
- return node.name?.getText(sourceFile) ?? "(anonymous class)";
18364
- }
18365
- if (ts.isInterfaceDeclaration(node)) {
18366
- return node.name.getText(sourceFile);
18367
- }
18368
- if (ts.isTypeAliasDeclaration(node)) {
18369
- return node.name.getText(sourceFile);
18370
- }
18371
- if (ts.isEnumDeclaration(node)) {
18372
- return node.name.getText(sourceFile);
18373
- }
18374
- if (ts.isVariableStatement(node)) {
18375
- const decls = node.declarationList.declarations;
18376
- return decls.map((d) => d.name.getText(sourceFile)).join(", ");
18377
- }
18378
- if (ts.isExportAssignment(node)) {
18379
- return "default export";
18380
- }
18430
+ if (ts.isClassDeclaration(node)) return node.name?.getText(sourceFile) ?? "(anonymous class)";
18431
+ if (ts.isInterfaceDeclaration(node)) return node.name.getText(sourceFile);
18432
+ if (ts.isTypeAliasDeclaration(node)) return node.name.getText(sourceFile);
18433
+ if (ts.isEnumDeclaration(node)) return node.name.getText(sourceFile);
18434
+ if (ts.isVariableStatement(node)) return node.declarationList.declarations.map((d) => d.name.getText(sourceFile)).join(", ");
18435
+ if (ts.isExportAssignment(node)) return "default export";
18381
18436
  return "(unknown)";
18382
18437
  }
18383
18438
  function visitTopLevel(node) {
18384
18439
  if (ts.isImportDeclaration(node)) {
18385
- importLines.push({
18386
- start: getLineNumber(node.getStart(sourceFile)),
18387
- end: getLineNumber(node.getEnd())
18388
- });
18440
+ importLines.push({ start: getLineNumber(node.getStart(sourceFile)), end: getLineNumber(node.getEnd()) });
18389
18441
  return;
18390
18442
  }
18391
18443
  if (ts.isFunctionDeclaration(node)) {
18392
- chunks.push({
18393
- kind: "function",
18394
- name: getName(node),
18395
- text: getNodeText(node),
18396
- startLine: getLineNumber(node.getStart(sourceFile)),
18397
- endLine: getLineNumber(node.getEnd()),
18398
- comment: getLeadingComment(node)
18399
- });
18444
+ chunks.push({ kind: "function", name: getName(node), text: getNodeText(node), startLine: getLineNumber(node.getStart(sourceFile)), endLine: getLineNumber(node.getEnd()), comment: getLeadingComment(node) });
18400
18445
  return;
18401
18446
  }
18402
18447
  if (ts.isClassDeclaration(node)) {
18403
- chunks.push({
18404
- kind: "class",
18405
- name: getName(node),
18406
- text: getNodeText(node),
18407
- startLine: getLineNumber(node.getStart(sourceFile)),
18408
- endLine: getLineNumber(node.getEnd()),
18409
- comment: getLeadingComment(node)
18410
- });
18411
- return;
18412
- }
18413
- if (ts.isInterfaceDeclaration(node)) {
18414
- chunks.push({
18415
- kind: "type",
18416
- name: getName(node),
18417
- text: getNodeText(node),
18418
- startLine: getLineNumber(node.getStart(sourceFile)),
18419
- endLine: getLineNumber(node.getEnd()),
18420
- comment: getLeadingComment(node)
18421
- });
18422
- return;
18423
- }
18424
- if (ts.isTypeAliasDeclaration(node)) {
18425
- chunks.push({
18426
- kind: "type",
18427
- name: getName(node),
18428
- text: getNodeText(node),
18429
- startLine: getLineNumber(node.getStart(sourceFile)),
18430
- endLine: getLineNumber(node.getEnd()),
18431
- comment: getLeadingComment(node)
18432
- });
18448
+ chunks.push({ kind: "class", name: getName(node), text: getNodeText(node), startLine: getLineNumber(node.getStart(sourceFile)), endLine: getLineNumber(node.getEnd()), comment: getLeadingComment(node) });
18433
18449
  return;
18434
18450
  }
18435
- if (ts.isEnumDeclaration(node)) {
18436
- chunks.push({
18437
- kind: "type",
18438
- name: getName(node),
18439
- text: getNodeText(node),
18440
- startLine: getLineNumber(node.getStart(sourceFile)),
18441
- endLine: getLineNumber(node.getEnd()),
18442
- comment: getLeadingComment(node)
18443
- });
18451
+ if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) {
18452
+ chunks.push({ kind: "type", name: getName(node), text: getNodeText(node), startLine: getLineNumber(node.getStart(sourceFile)), endLine: getLineNumber(node.getEnd()), comment: getLeadingComment(node) });
18444
18453
  return;
18445
18454
  }
18446
18455
  if (ts.isVariableStatement(node)) {
18447
- const decls = node.declarationList.declarations;
18448
- const isFnLike = decls.some(
18449
- (d) => d.initializer && (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))
18450
- );
18451
- chunks.push({
18452
- kind: isFnLike ? "function" : "variable",
18453
- name: getName(node),
18454
- text: getNodeText(node),
18455
- startLine: getLineNumber(node.getStart(sourceFile)),
18456
- endLine: getLineNumber(node.getEnd()),
18457
- comment: getLeadingComment(node)
18458
- });
18456
+ const isFnLike = node.declarationList.declarations.some((d) => d.initializer && (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer)));
18457
+ chunks.push({ kind: isFnLike ? "function" : "variable", name: getName(node), text: getNodeText(node), startLine: getLineNumber(node.getStart(sourceFile)), endLine: getLineNumber(node.getEnd()), comment: getLeadingComment(node) });
18459
18458
  return;
18460
18459
  }
18461
18460
  if (ts.isExportAssignment(node)) {
18462
- chunks.push({
18463
- kind: "export",
18464
- name: "default export",
18465
- text: getNodeText(node),
18466
- startLine: getLineNumber(node.getStart(sourceFile)),
18467
- endLine: getLineNumber(node.getEnd()),
18468
- comment: getLeadingComment(node)
18469
- });
18461
+ chunks.push({ kind: "export", name: "default export", text: getNodeText(node), startLine: getLineNumber(node.getStart(sourceFile)), endLine: getLineNumber(node.getEnd()), comment: getLeadingComment(node) });
18470
18462
  return;
18471
18463
  }
18472
18464
  if (ts.isExpressionStatement(node)) {
18473
18465
  const text3 = getNodeText(node);
18474
- if (text3.length > 10) {
18475
- chunks.push({
18476
- kind: "other",
18477
- name: text3.slice(0, 40).replace(/\n/g, " "),
18478
- text: text3,
18479
- startLine: getLineNumber(node.getStart(sourceFile)),
18480
- endLine: getLineNumber(node.getEnd())
18481
- });
18482
- }
18483
- return;
18466
+ if (text3.length > 10) chunks.push({ kind: "other", name: text3.slice(0, 40).replace(/\n/g, " "), text: text3, startLine: getLineNumber(node.getStart(sourceFile)), endLine: getLineNumber(node.getEnd()) });
18484
18467
  }
18485
18468
  }
18486
18469
  sourceFile.statements.forEach(visitTopLevel);
18487
18470
  if (importLines.length > 0) {
18488
18471
  const startLine = importLines[0].start;
18489
18472
  const endLine = importLines[importLines.length - 1].end;
18490
- const importText = lines.slice(startLine - 1, endLine).join("\n");
18491
- chunks.unshift({
18492
- kind: "import",
18493
- name: `${importLines.length} imports`,
18494
- text: importText,
18495
- startLine,
18473
+ chunks.unshift({ kind: "import", name: `${importLines.length} imports`, text: lines.slice(startLine - 1, endLine).join("\n"), startLine, endLine });
18474
+ }
18475
+ chunks.sort((a, b) => a.startLine - b.startLine);
18476
+ return chunks.length > 0 ? chunks : chunkByWindows(source, 80);
18477
+ }
18478
+ function chunkGenericSource(source, _fileName, language) {
18479
+ const lines = source.split("\n");
18480
+ if (source.trim().length === 0) return [];
18481
+ if (language && TEXT_LIKE_LANGUAGES.has(language)) return chunkTextLike(lines, language);
18482
+ const chunks = [];
18483
+ const patterns = [
18484
+ { kind: "function", regex: /^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)\s*\(/, name: (m) => m[1] },
18485
+ { kind: "class", regex: /^\s*class\s+([A-Za-z_][\w]*)\b/, name: (m) => m[1] },
18486
+ { kind: "function", regex: /^\s*(?:pub\s+)?fn\s+([A-Za-z_][\w]*)\s*[<(]/, name: (m) => m[1] },
18487
+ { kind: "class", regex: /^\s*(?:pub\s+)?(?:struct|enum|trait)\s+([A-Za-z_][\w]*)\b/, name: (m) => m[1] },
18488
+ { kind: "function", regex: /^\s*func\s+(?:\([^)]*\)\s*)?([A-Za-z_][\w]*)\s*\(/, name: (m) => m[1] },
18489
+ { kind: "function", regex: /^\s*(?:public|private|protected|static|final|suspend|fun|override|open|internal|export|async|func|function|subroutine)\s+.*?([A-Za-z_][\w]*)\s*\(/, name: (m) => m[1] },
18490
+ { kind: "function", regex: /^\s*function\s+([A-Za-z_][\w]*)\s*\(/, name: (m) => m[1] },
18491
+ { kind: "type", regex: /^\s*(?:interface|type|enum|record|data\s+class|case\s+class|contract|library)\s+([A-Za-z_][\w]*)\b/, name: (m) => m[1] },
18492
+ { kind: "section", regex: /^\s{0,3}#{1,6}\s+(.+)$/, name: (m) => m[1].trim() }
18493
+ ];
18494
+ const starts = [];
18495
+ for (let i = 0; i < lines.length; i++) {
18496
+ const line = lines[i];
18497
+ for (const pattern of patterns) {
18498
+ const match = line.match(pattern.regex);
18499
+ if (match) {
18500
+ starts.push({ line: i + 1, kind: pattern.kind, name: pattern.name(match) });
18501
+ break;
18502
+ }
18503
+ }
18504
+ }
18505
+ if (starts.length === 0) return chunkByWindows(source, 80);
18506
+ for (let i = 0; i < starts.length; i++) {
18507
+ const start = starts[i];
18508
+ const next = starts[i + 1]?.line ?? lines.length + 1;
18509
+ const endLine = Math.max(start.line, next - 1);
18510
+ chunks.push({
18511
+ kind: start.kind,
18512
+ name: start.name,
18513
+ text: lines.slice(start.line - 1, endLine).join("\n"),
18514
+ startLine: start.line,
18496
18515
  endLine
18497
18516
  });
18498
18517
  }
18499
- chunks.sort((a, b) => a.startLine - b.startLine);
18518
+ return chunks;
18519
+ }
18520
+ function chunkTextLike(lines, language) {
18521
+ if (language === "markdown" || language === "mdx") {
18522
+ const starts = lines.map((line, i) => ({ line, i })).filter(({ line }) => /^\s{0,3}#{1,6}\s+/.test(line));
18523
+ if (starts.length > 0) {
18524
+ return starts.map((start, idx) => {
18525
+ const end = starts[idx + 1]?.i ?? lines.length;
18526
+ const title = start.line.replace(/^\s{0,3}#{1,6}\s+/, "").trim();
18527
+ return { kind: "section", name: title, text: lines.slice(start.i, end).join("\n"), startLine: start.i + 1, endLine: end };
18528
+ });
18529
+ }
18530
+ }
18531
+ return chunkByWindows(lines.join("\n"), 80);
18532
+ }
18533
+ function chunkByWindows(source, windowLines) {
18534
+ const lines = source.split("\n");
18535
+ const chunks = [];
18536
+ for (let i = 0; i < lines.length; i += windowLines) {
18537
+ const end = Math.min(lines.length, i + windowLines);
18538
+ const text3 = lines.slice(i, end).join("\n");
18539
+ if (text3.trim()) chunks.push({ kind: "other", name: `lines ${i + 1}-${end}`, text: text3, startLine: i + 1, endLine: end });
18540
+ }
18500
18541
  return chunks;
18501
18542
  }
18502
18543
  function summarizeChunk(chunk, filePath) {
@@ -18515,13 +18556,14 @@ function summarizeChunk(chunk, filePath) {
18515
18556
  return `Variable ${chunk.name} in ${location}`;
18516
18557
  case "export":
18517
18558
  return `Default export in ${location}`;
18559
+ case "section":
18560
+ return `Section ${chunk.name} in ${location}`;
18518
18561
  default:
18519
18562
  return `${chunk.kind} in ${location}`;
18520
18563
  }
18521
18564
  }
18522
18565
  function isChunkable(filePath) {
18523
- const ext = filePath.split(".").pop()?.toLowerCase();
18524
- return ext === "ts" || ext === "tsx" || ext === "js" || ext === "jsx";
18566
+ return Boolean(languageForFile(filePath));
18525
18567
  }
18526
18568
 
18527
18569
  // src/lib/graph-rag.ts
@@ -26962,12 +27004,11 @@ import { z as z88 } from "zod";
26962
27004
  init_config();
26963
27005
  import crypto19 from "crypto";
26964
27006
  import path50 from "path";
26965
- import { existsSync as existsSync39, mkdirSync as mkdirSync21, readFileSync as readFileSync32, statSync as statSync9, writeFileSync as writeFileSync23 } from "fs";
27007
+ import { existsSync as existsSync39, mkdirSync as mkdirSync21, readFileSync as readFileSync32, readdirSync as readdirSync14, statSync as statSync9, writeFileSync as writeFileSync23 } from "fs";
26966
27008
  import { spawnSync } from "child_process";
26967
- var INDEX_VERSION = 1;
26968
- var DEFAULT_MAX_FILES = 2e3;
26969
- var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
26970
- var IGNORE_SEGMENTS = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", "coverage", ".worktrees", ".next", "build"]);
27009
+ var INDEX_VERSION = 2;
27010
+ var DEFAULT_MAX_FILES = 5e3;
27011
+ var IGNORE_SEGMENTS = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", "coverage", ".worktrees", ".next", "build", "target", "vendor"]);
26971
27012
  function normalizeProjectRoot(projectRoot) {
26972
27013
  return path50.resolve(projectRoot || process.cwd());
26973
27014
  }
@@ -26985,78 +27026,53 @@ function getCodeContextIndexPath(projectRoot) {
26985
27026
  return path50.join(indexDir(), `${rootHash}.json`);
26986
27027
  }
26987
27028
  function currentBranch(projectRoot) {
26988
- const result2 = spawnSync("git", ["branch", "--show-current"], {
26989
- cwd: projectRoot,
26990
- encoding: "utf8",
26991
- timeout: 2e3
26992
- });
27029
+ const result2 = spawnSync("git", ["branch", "--show-current"], { cwd: projectRoot, encoding: "utf8", timeout: 2e3 });
26993
27030
  const branch = result2.status === 0 ? result2.stdout.trim() : "";
26994
27031
  return branch || "detached-or-unknown";
26995
27032
  }
26996
27033
  function shouldIgnore(relPath) {
26997
- const parts = relPath.split(path50.sep);
27034
+ const parts = relPath.split(/[\\/]/);
26998
27035
  return parts.some((part) => IGNORE_SEGMENTS.has(part));
26999
27036
  }
27000
- function isCodeFile(relPath) {
27001
- return CODE_EXTENSIONS.has(path50.extname(relPath).toLowerCase());
27037
+ function listRecursive(projectRoot, dir = projectRoot, out = []) {
27038
+ for (const entry of readdirSync14(dir, { withFileTypes: true })) {
27039
+ const abs = path50.join(dir, entry.name);
27040
+ const rel = path50.relative(projectRoot, abs).replaceAll(path50.sep, "/");
27041
+ if (shouldIgnore(rel)) continue;
27042
+ if (entry.isDirectory()) listRecursive(projectRoot, abs, out);
27043
+ else if (entry.isFile()) out.push(rel);
27044
+ }
27045
+ return out;
27002
27046
  }
27003
27047
  function listCodeFiles(projectRoot, maxFiles) {
27004
- const git = spawnSync("git", ["ls-files", "*.ts", "*.tsx", "*.js", "*.jsx"], {
27005
- cwd: projectRoot,
27006
- encoding: "utf8",
27007
- timeout: 5e3,
27008
- maxBuffer: 1024 * 1024 * 8
27009
- });
27048
+ const git = spawnSync("git", ["ls-files"], { cwd: projectRoot, encoding: "utf8", timeout: 5e3, maxBuffer: 1024 * 1024 * 16 });
27010
27049
  let files = [];
27011
27050
  if (git.status === 0 && git.stdout.trim()) {
27012
27051
  files = git.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
27013
27052
  } else {
27014
- const rg = spawnSync("rg", ["--files", "-g", "*.ts", "-g", "*.tsx", "-g", "*.js", "-g", "*.jsx"], {
27015
- cwd: projectRoot,
27016
- encoding: "utf8",
27017
- timeout: 5e3,
27018
- maxBuffer: 1024 * 1024 * 8
27019
- });
27020
- if (rg.status === 0 && rg.stdout.trim()) {
27021
- files = rg.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
27022
- }
27053
+ const rg = spawnSync("rg", ["--files"], { cwd: projectRoot, encoding: "utf8", timeout: 5e3, maxBuffer: 1024 * 1024 * 16 });
27054
+ files = rg.status === 0 && rg.stdout.trim() ? rg.stdout.split("\n").map((s) => s.trim()).filter(Boolean) : listRecursive(projectRoot);
27023
27055
  }
27024
- return files.filter((file) => isCodeFile(file) && !shouldIgnore(file)).slice(0, maxFiles).sort();
27056
+ return files.map((file) => file.replaceAll(path50.sep, "/")).filter((file) => isChunkable(file) && !shouldIgnore(file)).slice(0, maxFiles).sort();
27025
27057
  }
27026
27058
  function parseImportPaths(importText) {
27027
27059
  const paths = [];
27028
- const fromRegex = /from\s+["']([^"']+)["']/g;
27029
- let match;
27030
- while ((match = fromRegex.exec(importText)) !== null) paths.push(match[1]);
27031
- const bareRegex = /^import\s+["']([^"']+)["']/gm;
27032
- while ((match = bareRegex.exec(importText)) !== null) {
27033
- if (!paths.includes(match[1])) paths.push(match[1]);
27034
- }
27035
- const requireRegex = /require\(["']([^"']+)["']\)/g;
27036
- while ((match = requireRegex.exec(importText)) !== null) {
27037
- if (!paths.includes(match[1])) paths.push(match[1]);
27060
+ const patterns = [/from\s+["']([^"']+)["']/g, /^import\s+["']([^"']+)["']/gm, /require\(["']([^"']+)["']\)/g, /use\s+([A-Za-z0-9_:]+)::/g, /#include\s+[<"]([^>"]+)[>"]/g];
27061
+ for (const regex of patterns) {
27062
+ let match;
27063
+ while ((match = regex.exec(importText)) !== null) if (!paths.includes(match[1])) paths.push(match[1]);
27038
27064
  }
27039
27065
  return paths;
27040
27066
  }
27041
27067
  function resolveImport(fromFile, importPath, allFiles) {
27042
27068
  if (!importPath.startsWith(".")) return null;
27043
27069
  const base = path50.posix.normalize(path50.posix.join(path50.posix.dirname(fromFile.replaceAll(path50.sep, "/")), importPath));
27044
- const withoutKnownExt = base.replace(/\.(?:js|jsx|ts|tsx)$/, "");
27045
- const candidates = [
27046
- base,
27047
- `${withoutKnownExt}.ts`,
27048
- `${withoutKnownExt}.tsx`,
27049
- `${withoutKnownExt}.js`,
27050
- `${withoutKnownExt}.jsx`,
27051
- `${base}.ts`,
27052
- `${base}.tsx`,
27053
- `${base}.js`,
27054
- `${base}.jsx`,
27055
- path50.posix.join(base, "index.ts"),
27056
- path50.posix.join(base, "index.tsx"),
27057
- path50.posix.join(base, "index.js"),
27058
- path50.posix.join(base, "index.jsx")
27059
- ];
27070
+ const withoutKnownExt = base.replace(/\.(?:[a-z0-9]+)$/i, "");
27071
+ const candidates = [base];
27072
+ for (const ext of ["ts", "tsx", "js", "jsx", "py", "rs", "go", "java", "cs", "cpp", "c", "rb", "php", "swift", "kt", "scala", "sql", "md", "json", "yaml", "yml"]) {
27073
+ candidates.push(`${withoutKnownExt}.${ext}`, `${base}.${ext}`);
27074
+ }
27075
+ for (const indexName of ["index.ts", "index.tsx", "index.js", "mod.rs", "__init__.py"]) candidates.push(path50.posix.join(base, indexName));
27060
27076
  return candidates.find((candidate) => allFiles.has(candidate)) ?? null;
27061
27077
  }
27062
27078
  function symbolId(filePath, chunk) {
@@ -27082,39 +27098,32 @@ function buildFileRecord(projectRoot, relPath, allFiles, previous) {
27082
27098
  try {
27083
27099
  stat = statSync9(absPath);
27084
27100
  } catch {
27085
- return null;
27101
+ return { record: null, reused: false };
27086
27102
  }
27087
- if (!stat.isFile()) return null;
27103
+ if (!stat.isFile()) return { record: null, reused: false };
27104
+ const language = languageForFile(relPath);
27105
+ if (!language || !isChunkable(relPath)) return { record: null, reused: false };
27088
27106
  const source = readFileSync32(absPath, "utf8");
27089
27107
  const hash = hashText(source);
27090
- if (previous && previous.hash === hash && previous.mtimeMs === stat.mtimeMs && previous.size === stat.size) {
27091
- return previous;
27108
+ if (previous && previous.hash === hash && previous.mtimeMs === stat.mtimeMs && previous.size === stat.size && previous.language === language) {
27109
+ return { record: previous, reused: true };
27092
27110
  }
27093
- if (!isChunkable(relPath)) return null;
27094
27111
  const chunks = chunkSourceFile(source, relPath);
27095
27112
  const imports = chunks.filter((chunk) => chunk.kind === "import").flatMap((chunk) => parseImportPaths(chunk.text));
27096
27113
  const resolvedImports = imports.map((importPath) => resolveImport(relPath, importPath, allFiles)).filter((file) => Boolean(file));
27097
- const symbols = chunks.filter((chunk) => chunk.kind !== "import" && chunk.name !== "(unknown)").map((chunk) => ({
27114
+ const symbols = chunks.filter((chunk) => chunk.name !== "(unknown)").map((chunk) => ({
27098
27115
  id: symbolId(relPath, chunk),
27099
27116
  name: chunk.name,
27100
27117
  kind: chunk.kind,
27101
27118
  filePath: relPath,
27119
+ language,
27102
27120
  startLine: chunk.startLine,
27103
27121
  endLine: chunk.endLine,
27104
27122
  summary: summarizeChunk(chunk, relPath),
27105
- text: chunk.text.slice(0, 6e3),
27123
+ text: chunk.text.slice(0, 8e3),
27106
27124
  comment: chunk.comment
27107
27125
  }));
27108
- return {
27109
- path: relPath,
27110
- absPath,
27111
- hash,
27112
- mtimeMs: stat.mtimeMs,
27113
- size: stat.size,
27114
- imports,
27115
- resolvedImports,
27116
- symbols
27117
- };
27126
+ return { record: { path: relPath, absPath, language, hash, mtimeMs: stat.mtimeMs, size: stat.size, imports, resolvedImports, symbols }, reused: false };
27118
27127
  }
27119
27128
  function buildCodeContextIndex(options = {}) {
27120
27129
  const projectRoot = normalizeProjectRoot(options.projectRoot);
@@ -27124,17 +27133,27 @@ function buildCodeContextIndex(options = {}) {
27124
27133
  const files = listCodeFiles(projectRoot, maxFiles);
27125
27134
  const allFiles = new Set(files.map((file) => file.replaceAll(path50.sep, "/")));
27126
27135
  const fileRecords = {};
27136
+ let rebuiltFiles = 0;
27137
+ let reusedFiles = 0;
27138
+ let skippedFiles = 0;
27127
27139
  for (const rel of files) {
27128
27140
  const normalized = rel.replaceAll(path50.sep, "/");
27129
- const record = buildFileRecord(projectRoot, normalized, allFiles, previous?.files[normalized]);
27130
- if (record) fileRecords[normalized] = record;
27131
- }
27141
+ const { record, reused } = buildFileRecord(projectRoot, normalized, allFiles, previous?.files[normalized]);
27142
+ if (record) {
27143
+ fileRecords[normalized] = record;
27144
+ if (reused) reusedFiles++;
27145
+ else rebuiltFiles++;
27146
+ } else skippedFiles++;
27147
+ }
27148
+ const languageBreakdown = {};
27149
+ for (const file of Object.values(fileRecords)) languageBreakdown[file.language] = (languageBreakdown[file.language] ?? 0) + 1;
27132
27150
  const index = {
27133
27151
  version: INDEX_VERSION,
27134
27152
  projectRoot,
27135
27153
  rootHash: hashText(projectRoot).slice(0, 16),
27136
27154
  branch,
27137
27155
  indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
27156
+ stats: { filesSeen: files.length, filesIndexed: Object.keys(fileRecords).length, rebuiltFiles, reusedFiles, skippedFiles, languageBreakdown },
27138
27157
  files: fileRecords
27139
27158
  };
27140
27159
  saveIndex(index);
@@ -27162,13 +27181,60 @@ function loadOrBuildCodeContextIndex(options = {}) {
27162
27181
  }
27163
27182
  return buildCodeContextIndex(options);
27164
27183
  }
27184
+ function normalizeLanguage(language) {
27185
+ return language.toLowerCase().replace(/^c\+\+$/, "cpp").replace(/^c#$/, "csharp").replace(/^js$/, "javascript").replace(/^ts$/, "typescript");
27186
+ }
27165
27187
  function tokenize(query) {
27166
- return query.toLowerCase().split(/[^a-z0-9_.$/-]+/).map((s) => s.trim()).filter((s) => s.length >= 2);
27188
+ const raw = query.toLowerCase().replace(/([a-z])([A-Z])/g, "$1 $2").split(/[^a-z0-9_.$/-]+/).map((s) => s.trim()).filter((s) => s.length >= 2);
27189
+ const expanded = /* @__PURE__ */ new Set();
27190
+ for (const term of raw) {
27191
+ expanded.add(term);
27192
+ for (const part of term.split(/[_.$/-]+/)) if (part.length >= 2) expanded.add(part);
27193
+ const dashed = term.replace(/[_.$/]+/g, "-");
27194
+ if (dashed.length >= 2) expanded.add(dashed);
27195
+ if (!term.includes("-")) expanded.add(`${term}s`);
27196
+ if (term.endsWith("s") && term.length > 3) expanded.add(term.slice(0, -1));
27197
+ }
27198
+ return [...expanded];
27199
+ }
27200
+ function ngrams(terms) {
27201
+ const grams = [...terms];
27202
+ for (let i = 0; i < terms.length - 1; i++) grams.push(`${terms[i]} ${terms[i + 1]}`);
27203
+ return grams;
27204
+ }
27205
+ function globToRegex(pattern) {
27206
+ let out = "";
27207
+ for (let i = 0; i < pattern.length; i++) {
27208
+ const ch = pattern[i];
27209
+ const next = pattern[i + 1];
27210
+ if (ch === "*" && next === "*") {
27211
+ out += ".*";
27212
+ i++;
27213
+ continue;
27214
+ }
27215
+ if (ch === "*") {
27216
+ out += "[^/]*";
27217
+ continue;
27218
+ }
27219
+ if (".+^${}()|[]\\".includes(ch)) out += `\\${ch}`;
27220
+ else out += ch;
27221
+ }
27222
+ return new RegExp(`^${out}$`);
27223
+ }
27224
+ function matchesPath(filePath, patterns) {
27225
+ if (!patterns || patterns.length === 0) return true;
27226
+ const normalized = filePath.replaceAll(path50.sep, "/");
27227
+ return patterns.some((pattern) => {
27228
+ const p = pattern.replaceAll(path50.sep, "/").replace(/^\.\//, "");
27229
+ return normalized === p || normalized.startsWith(`${p}/`) || normalized.endsWith(p) || globToRegex(p).test(normalized);
27230
+ });
27167
27231
  }
27168
27232
  function scoreSymbol(symbol, terms) {
27233
+ const grams = ngrams(terms);
27169
27234
  const haystacks = {
27170
27235
  name: symbol.name.toLowerCase(),
27171
27236
  path: symbol.filePath.toLowerCase(),
27237
+ language: symbol.language.toLowerCase(),
27172
27238
  summary: symbol.summary.toLowerCase(),
27173
27239
  text: symbol.text.toLowerCase()
27174
27240
  };
@@ -27176,41 +27242,73 @@ function scoreSymbol(symbol, terms) {
27176
27242
  const matches = [];
27177
27243
  for (const term of terms) {
27178
27244
  if (haystacks.name === term) {
27179
- score += 80;
27245
+ score += 100;
27180
27246
  matches.push(`name=${term}`);
27181
27247
  continue;
27182
27248
  }
27183
27249
  if (haystacks.name.includes(term)) {
27184
- score += 40;
27250
+ score += 45;
27185
27251
  matches.push(`name~${term}`);
27186
27252
  }
27187
27253
  if (haystacks.path.includes(term)) {
27188
- score += 15;
27254
+ score += 18;
27189
27255
  matches.push(`path~${term}`);
27190
27256
  }
27257
+ if (haystacks.language === term) {
27258
+ score += 18;
27259
+ matches.push(`language=${term}`);
27260
+ }
27191
27261
  if (haystacks.summary.includes(term)) {
27192
- score += 12;
27262
+ score += 16;
27193
27263
  matches.push(`summary~${term}`);
27194
27264
  }
27195
27265
  if (haystacks.text.includes(term)) {
27196
- score += 4;
27266
+ score += 5;
27197
27267
  matches.push(`text~${term}`);
27198
27268
  }
27199
27269
  }
27270
+ for (const gram of grams.filter((g) => g.includes(" "))) {
27271
+ if (haystacks.text.includes(gram) || haystacks.summary.includes(gram)) {
27272
+ score += 20;
27273
+ matches.push(`phrase~${gram}`);
27274
+ }
27275
+ }
27276
+ const uniqueMatches = new Set(matches.map((m) => m.replace(/^[^=~]+[=~]/, ""))).size;
27277
+ score += uniqueMatches * 3;
27200
27278
  return { score, matches };
27201
27279
  }
27280
+ function filteredFiles(index, options = {}) {
27281
+ const languages = options.languages?.map(normalizeLanguage).filter(Boolean);
27282
+ return Object.values(index.files).filter((file) => {
27283
+ if (languages && languages.length > 0 && !languages.includes(normalizeLanguage(file.language))) return false;
27284
+ return matchesPath(file.path, options.paths);
27285
+ });
27286
+ }
27202
27287
  function searchCodeContext(query, options = {}) {
27203
27288
  const terms = tokenize(query);
27204
27289
  if (terms.length === 0) return [];
27205
- const index = loadOrBuildCodeContextIndex(options);
27290
+ const index = loadOrBuildCodeContextIndex({ projectRoot: options.projectRoot, force: options.force || options.refreshIndex, maxFiles: options.maxFiles });
27206
27291
  const results = [];
27207
- for (const file of Object.values(index.files)) {
27292
+ for (const file of filteredFiles(index, options)) {
27208
27293
  for (const symbol of file.symbols) {
27209
27294
  const scored = scoreSymbol(symbol, terms);
27210
- if (scored.score > 0) results.push({ symbol, score: scored.score, matches: scored.matches });
27295
+ if (scored.score > 0) {
27296
+ results.push({
27297
+ symbol,
27298
+ score: scored.score,
27299
+ matches: scored.matches,
27300
+ filePath: symbol.filePath,
27301
+ language: symbol.language,
27302
+ content: symbol.text,
27303
+ startLine: symbol.startLine,
27304
+ endLine: symbol.endLine
27305
+ });
27306
+ }
27211
27307
  }
27212
27308
  }
27213
- return results.sort((a, b) => b.score - a.score || a.symbol.filePath.localeCompare(b.symbol.filePath)).slice(0, options.limit ?? 20);
27309
+ const offset = Math.max(0, options.offset ?? 0);
27310
+ const limit = options.limit ?? 20;
27311
+ return results.sort((a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath)).slice(offset, offset + limit);
27214
27312
  }
27215
27313
  function dependentsMap(index) {
27216
27314
  const map = /* @__PURE__ */ new Map();
@@ -27238,19 +27336,10 @@ function traceCodeSymbol(symbolName, options = {}) {
27238
27336
  const index = loadOrBuildCodeContextIndex(options);
27239
27337
  const matches = findSymbols(index, symbolName, options.limit ?? 20);
27240
27338
  const dependents = dependentsMap(index);
27241
- return {
27242
- query: symbolName,
27243
- matches,
27244
- definitions: matches.map((symbol) => {
27245
- const file = index.files[symbol.filePath];
27246
- return {
27247
- symbol,
27248
- imports: file.resolvedImports,
27249
- dependents: [...dependents.get(symbol.filePath) ?? /* @__PURE__ */ new Set()].sort(),
27250
- relatedSymbols: file.symbols.filter((s) => s.id !== symbol.id).slice(0, 20)
27251
- };
27252
- })
27253
- };
27339
+ return { query: symbolName, matches, definitions: matches.map((symbol) => {
27340
+ const file = index.files[symbol.filePath];
27341
+ return { symbol, imports: file.resolvedImports, dependents: [...dependents.get(symbol.filePath) ?? /* @__PURE__ */ new Set()].sort(), relatedSymbols: file.symbols.filter((s) => s.id !== symbol.id).slice(0, 20) };
27342
+ }) };
27254
27343
  }
27255
27344
  function resolveTargetFile(index, input) {
27256
27345
  if (input.filePath) {
@@ -27290,20 +27379,10 @@ function analyzeBlastRadius(input) {
27290
27379
  const lower = file.toLowerCase();
27291
27380
  return (lower.includes("test") || lower.includes("spec")) && (lower.includes(targetBase) || (symbolLower ? index.files[file].symbols.some((s) => s.text.toLowerCase().includes(symbolLower)) : false));
27292
27381
  });
27293
- for (const test of tests) {
27294
- if (!impacted.has(test)) impacted.set(test, { distance: 1, reason: "related test/spec" });
27295
- }
27382
+ for (const test of tests) if (!impacted.has(test)) impacted.set(test, { distance: 1, reason: "related test/spec" });
27296
27383
  const impactedFiles = [...impacted.entries()].map(([filePath, value]) => ({ filePath, distance: value.distance, reason: value.reason })).sort((a, b) => a.distance - b.distance || a.filePath.localeCompare(b.filePath));
27297
27384
  const nonTestImpacted = impactedFiles.filter((f) => !f.filePath.match(/(test|spec)\./i)).length;
27298
- const riskLevel = nonTestImpacted >= 8 ? "high" : nonTestImpacted >= 4 ? "medium" : "low";
27299
- return {
27300
- target: target.target,
27301
- targetFile: target.filePath,
27302
- impactedFiles,
27303
- tests,
27304
- symbolsInTarget: index.files[target.filePath]?.symbols ?? [],
27305
- riskLevel
27306
- };
27385
+ return { target: target.target, targetFile: target.filePath, impactedFiles, tests, symbolsInTarget: index.files[target.filePath]?.symbols ?? [], riskLevel: nonTestImpacted >= 8 ? "high" : nonTestImpacted >= 4 ? "medium" : "low" };
27307
27386
  }
27308
27387
  function getCodeContextStats(options = {}) {
27309
27388
  const index = loadOrBuildCodeContextIndex(options);
@@ -27311,9 +27390,14 @@ function getCodeContextStats(options = {}) {
27311
27390
  projectRoot: index.projectRoot,
27312
27391
  branch: index.branch,
27313
27392
  indexedAt: index.indexedAt,
27393
+ indexAgeMs: Math.max(0, Date.now() - Date.parse(index.indexedAt)),
27314
27394
  files: Object.keys(index.files).length,
27315
27395
  symbols: Object.values(index.files).reduce((sum, file) => sum + file.symbols.length, 0),
27316
27396
  imports: Object.values(index.files).reduce((sum, file) => sum + file.resolvedImports.length, 0),
27397
+ languageBreakdown: index.stats.languageBreakdown,
27398
+ rebuiltFiles: index.stats.rebuiltFiles,
27399
+ reusedFiles: index.stats.reusedFiles,
27400
+ skippedFiles: index.stats.skippedFiles,
27317
27401
  indexPath: getCodeContextIndexPath(index.projectRoot)
27318
27402
  };
27319
27403
  }
@@ -27337,10 +27421,14 @@ function registerCodeContext(server2) {
27337
27421
  file_path: z88.string().optional().describe("File path for blast_radius"),
27338
27422
  force: z88.boolean().optional().describe("Force rebuild before answering"),
27339
27423
  limit: z88.coerce.number().int().min(1).max(100).optional().describe("Max results"),
27424
+ offset: z88.coerce.number().int().min(0).optional().describe("Search pagination offset"),
27425
+ refresh_index: z88.boolean().optional().describe("Refresh/rebuild index before searching"),
27426
+ languages: z88.array(z88.string()).optional().describe('Language filters, e.g. ["python", "typescript"]'),
27427
+ paths: z88.array(z88.string()).optional().describe("Path/glob filters"),
27340
27428
  depth: z88.coerce.number().int().min(1).max(5).optional().describe("Dependent traversal depth for blast_radius"),
27341
27429
  max_files: z88.coerce.number().int().min(1).max(1e4).optional().describe("Max code files to index")
27342
27430
  }
27343
- }, async ({ action, project_root, query, symbol, file_path, force, limit, depth, max_files }) => {
27431
+ }, async ({ action, project_root, query, symbol, file_path, force, limit, offset, refresh_index, languages, paths, depth, max_files }) => {
27344
27432
  const opts = { projectRoot: project_root, force, maxFiles: max_files };
27345
27433
  if (action === "index") {
27346
27434
  const index = buildCodeContextIndex(opts);
@@ -27358,7 +27446,15 @@ function registerCodeContext(server2) {
27358
27446
  }
27359
27447
  if (action === "search") {
27360
27448
  if (!query) return errorResult10('code_context action "search" requires query');
27361
- return jsonResult({ query, results: searchCodeContext(query, { ...opts, limit }) });
27449
+ return jsonResult({
27450
+ query,
27451
+ limit: limit ?? 20,
27452
+ offset: offset ?? 0,
27453
+ refresh_index: refresh_index ?? false,
27454
+ languages: languages ?? [],
27455
+ paths: paths ?? [],
27456
+ results: searchCodeContext(query, { ...opts, limit, offset, refreshIndex: refresh_index, languages, paths })
27457
+ });
27362
27458
  }
27363
27459
  if (action === "trace") {
27364
27460
  if (!symbol) return errorResult10('code_context action "trace" requires symbol');