@colbymchenry/codegraph-darwin-arm64 0.9.4 → 0.9.5

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 (137) hide show
  1. package/lib/dist/bin/codegraph.js +12 -0
  2. package/lib/dist/bin/codegraph.js.map +1 -1
  3. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  4. package/lib/dist/extraction/grammars.js +14 -1
  5. package/lib/dist/extraction/grammars.js.map +1 -1
  6. package/lib/dist/extraction/index.d.ts +15 -2
  7. package/lib/dist/extraction/index.d.ts.map +1 -1
  8. package/lib/dist/extraction/index.js +170 -78
  9. package/lib/dist/extraction/index.js.map +1 -1
  10. package/lib/dist/extraction/languages/index.d.ts.map +1 -1
  11. package/lib/dist/extraction/languages/index.js +2 -0
  12. package/lib/dist/extraction/languages/index.js.map +1 -1
  13. package/lib/dist/extraction/languages/objc.d.ts +3 -0
  14. package/lib/dist/extraction/languages/objc.d.ts.map +1 -0
  15. package/lib/dist/extraction/languages/objc.js +133 -0
  16. package/lib/dist/extraction/languages/objc.js.map +1 -0
  17. package/lib/dist/extraction/tree-sitter-types.d.ts +4 -0
  18. package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  19. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  20. package/lib/dist/extraction/tree-sitter.js +82 -5
  21. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  22. package/lib/dist/index.d.ts +21 -2
  23. package/lib/dist/index.d.ts.map +1 -1
  24. package/lib/dist/index.js +33 -0
  25. package/lib/dist/index.js.map +1 -1
  26. package/lib/dist/installer/instructions-template.d.ts +2 -2
  27. package/lib/dist/installer/instructions-template.d.ts.map +1 -1
  28. package/lib/dist/installer/instructions-template.js +1 -1
  29. package/lib/dist/mcp/daemon-paths.d.ts +46 -0
  30. package/lib/dist/mcp/daemon-paths.d.ts.map +1 -0
  31. package/lib/dist/mcp/daemon-paths.js +125 -0
  32. package/lib/dist/mcp/daemon-paths.js.map +1 -0
  33. package/lib/dist/mcp/daemon.d.ts +161 -0
  34. package/lib/dist/mcp/daemon.d.ts.map +1 -0
  35. package/lib/dist/mcp/daemon.js +403 -0
  36. package/lib/dist/mcp/daemon.js.map +1 -0
  37. package/lib/dist/mcp/engine.d.ts +100 -0
  38. package/lib/dist/mcp/engine.d.ts.map +1 -0
  39. package/lib/dist/mcp/engine.js +291 -0
  40. package/lib/dist/mcp/engine.js.map +1 -0
  41. package/lib/dist/mcp/index.d.ts +64 -53
  42. package/lib/dist/mcp/index.d.ts.map +1 -1
  43. package/lib/dist/mcp/index.js +307 -387
  44. package/lib/dist/mcp/index.js.map +1 -1
  45. package/lib/dist/mcp/proxy.d.ts +46 -0
  46. package/lib/dist/mcp/proxy.d.ts.map +1 -0
  47. package/lib/dist/mcp/proxy.js +276 -0
  48. package/lib/dist/mcp/proxy.js.map +1 -0
  49. package/lib/dist/mcp/server-instructions.d.ts +1 -1
  50. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  51. package/lib/dist/mcp/server-instructions.js +1 -1
  52. package/lib/dist/mcp/session.d.ts +67 -0
  53. package/lib/dist/mcp/session.d.ts.map +1 -0
  54. package/lib/dist/mcp/session.js +276 -0
  55. package/lib/dist/mcp/session.js.map +1 -0
  56. package/lib/dist/mcp/tools.d.ts +49 -0
  57. package/lib/dist/mcp/tools.d.ts.map +1 -1
  58. package/lib/dist/mcp/tools.js +239 -14
  59. package/lib/dist/mcp/tools.js.map +1 -1
  60. package/lib/dist/mcp/transport.d.ts +111 -29
  61. package/lib/dist/mcp/transport.d.ts.map +1 -1
  62. package/lib/dist/mcp/transport.js +181 -71
  63. package/lib/dist/mcp/transport.js.map +1 -1
  64. package/lib/dist/mcp/version.d.ts +19 -0
  65. package/lib/dist/mcp/version.d.ts.map +1 -0
  66. package/lib/dist/mcp/version.js +71 -0
  67. package/lib/dist/mcp/version.js.map +1 -0
  68. package/lib/dist/resolution/callback-synthesizer.d.ts +3 -2
  69. package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  70. package/lib/dist/resolution/callback-synthesizer.js +274 -3
  71. package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
  72. package/lib/dist/resolution/frameworks/expo-modules.d.ts +3 -0
  73. package/lib/dist/resolution/frameworks/expo-modules.d.ts.map +1 -0
  74. package/lib/dist/resolution/frameworks/expo-modules.js +143 -0
  75. package/lib/dist/resolution/frameworks/expo-modules.js.map +1 -0
  76. package/lib/dist/resolution/frameworks/fabric.d.ts +3 -0
  77. package/lib/dist/resolution/frameworks/fabric.d.ts.map +1 -0
  78. package/lib/dist/resolution/frameworks/fabric.js +354 -0
  79. package/lib/dist/resolution/frameworks/fabric.js.map +1 -0
  80. package/lib/dist/resolution/frameworks/index.d.ts +4 -0
  81. package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
  82. package/lib/dist/resolution/frameworks/index.js +21 -1
  83. package/lib/dist/resolution/frameworks/index.js.map +1 -1
  84. package/lib/dist/resolution/frameworks/react-native.d.ts +3 -0
  85. package/lib/dist/resolution/frameworks/react-native.d.ts.map +1 -0
  86. package/lib/dist/resolution/frameworks/react-native.js +360 -0
  87. package/lib/dist/resolution/frameworks/react-native.js.map +1 -0
  88. package/lib/dist/resolution/frameworks/swift-objc.d.ts +37 -0
  89. package/lib/dist/resolution/frameworks/swift-objc.d.ts.map +1 -0
  90. package/lib/dist/resolution/frameworks/swift-objc.js +252 -0
  91. package/lib/dist/resolution/frameworks/swift-objc.js.map +1 -0
  92. package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
  93. package/lib/dist/resolution/import-resolver.js +1 -0
  94. package/lib/dist/resolution/import-resolver.js.map +1 -1
  95. package/lib/dist/resolution/swift-objc-bridge.d.ts +134 -0
  96. package/lib/dist/resolution/swift-objc-bridge.d.ts.map +1 -0
  97. package/lib/dist/resolution/swift-objc-bridge.js +256 -0
  98. package/lib/dist/resolution/swift-objc-bridge.js.map +1 -0
  99. package/lib/dist/sync/index.d.ts +3 -1
  100. package/lib/dist/sync/index.d.ts.map +1 -1
  101. package/lib/dist/sync/index.js +7 -1
  102. package/lib/dist/sync/index.js.map +1 -1
  103. package/lib/dist/sync/watcher.d.ts +109 -7
  104. package/lib/dist/sync/watcher.d.ts.map +1 -1
  105. package/lib/dist/sync/watcher.js +215 -33
  106. package/lib/dist/sync/watcher.js.map +1 -1
  107. package/lib/dist/sync/worktree.d.ts +54 -0
  108. package/lib/dist/sync/worktree.d.ts.map +1 -0
  109. package/lib/dist/sync/worktree.js +136 -0
  110. package/lib/dist/sync/worktree.js.map +1 -0
  111. package/lib/dist/types.d.ts +1 -1
  112. package/lib/dist/types.d.ts.map +1 -1
  113. package/lib/dist/types.js +1 -0
  114. package/lib/dist/types.js.map +1 -1
  115. package/lib/node_modules/.package-lock.json +29 -1
  116. package/lib/node_modules/chokidar/LICENSE +21 -0
  117. package/lib/node_modules/chokidar/README.md +305 -0
  118. package/lib/node_modules/chokidar/esm/handler.d.ts +90 -0
  119. package/lib/node_modules/chokidar/esm/handler.js +629 -0
  120. package/lib/node_modules/chokidar/esm/index.d.ts +215 -0
  121. package/lib/node_modules/chokidar/esm/index.js +798 -0
  122. package/lib/node_modules/chokidar/esm/package.json +1 -0
  123. package/lib/node_modules/chokidar/handler.d.ts +90 -0
  124. package/lib/node_modules/chokidar/handler.js +635 -0
  125. package/lib/node_modules/chokidar/index.d.ts +215 -0
  126. package/lib/node_modules/chokidar/index.js +804 -0
  127. package/lib/node_modules/chokidar/package.json +69 -0
  128. package/lib/node_modules/readdirp/LICENSE +21 -0
  129. package/lib/node_modules/readdirp/README.md +120 -0
  130. package/lib/node_modules/readdirp/esm/index.d.ts +108 -0
  131. package/lib/node_modules/readdirp/esm/index.js +257 -0
  132. package/lib/node_modules/readdirp/esm/package.json +1 -0
  133. package/lib/node_modules/readdirp/index.d.ts +108 -0
  134. package/lib/node_modules/readdirp/index.js +263 -0
  135. package/lib/node_modules/readdirp/package.json +70 -0
  136. package/lib/package.json +2 -1
  137. package/package.json +1 -1
@@ -41,7 +41,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.ToolHandler = exports.tools = void 0;
42
42
  exports.getExploreBudget = getExploreBudget;
43
43
  exports.getExploreOutputBudget = getExploreOutputBudget;
44
+ exports.formatStaleBanner = formatStaleBanner;
45
+ exports.formatStaleFooter = formatStaleFooter;
44
46
  const index_1 = __importStar(require("../index"));
47
+ const worktree_1 = require("../sync/worktree");
45
48
  const crypto_1 = require("crypto");
46
49
  const fs_1 = require("fs");
47
50
  const utils_1 = require("../utils");
@@ -241,6 +244,42 @@ function markSessionConsulted(sessionId) {
241
244
  // to write rather than overwrite an attacker-chosen target.
242
245
  }
243
246
  }
247
+ /**
248
+ * Per-file staleness banner emitted at the top of a tool response when the
249
+ * file watcher has pending events for files referenced by the response.
250
+ * The agent uses this to fall back to Read for those specific files
251
+ * without waiting for the debounced sync (issue #403).
252
+ */
253
+ function formatStaleBanner(stale) {
254
+ const now = Date.now();
255
+ const lines = stale.map((p) => {
256
+ const ageMs = Math.max(0, now - p.lastSeenMs);
257
+ const label = p.indexing ? 'indexing in progress' : 'pending sync';
258
+ return ` - ${p.path} (edited ${ageMs}ms ago, ${label})`;
259
+ });
260
+ return ('⚠️ Some files referenced below were edited since the last index sync — ' +
261
+ 'their codegraph entries may be stale:\n' +
262
+ lines.join('\n') +
263
+ '\nFor accurate content of those specific files, Read them directly. ' +
264
+ 'The rest of this response is fresh.');
265
+ }
266
+ /**
267
+ * Compact footer listing pending files that are NOT referenced in this
268
+ * response. Gives the agent a complete project-wide freshness picture
269
+ * without bloating the main banner.
270
+ */
271
+ function formatStaleFooter(stale) {
272
+ const MAX = 5;
273
+ const now = Date.now();
274
+ const shown = stale.slice(0, MAX);
275
+ const lines = shown.map((p) => {
276
+ const ageMs = Math.max(0, now - p.lastSeenMs);
277
+ return ` - ${p.path} (edited ${ageMs}ms ago)`;
278
+ });
279
+ const more = stale.length > MAX ? `\n - …and ${stale.length - MAX} more` : '';
280
+ return (`(Note: ${stale.length} file(s) elsewhere in this project are pending index ` +
281
+ `sync but were not referenced above:\n${lines.join('\n')}${more})`);
282
+ }
244
283
  /**
245
284
  * Common projectPath property for cross-project queries
246
285
  */
@@ -483,6 +522,12 @@ class ToolHandler {
483
522
  // The directory the server last searched for a default project. Surfaced in
484
523
  // the "not initialized" error so users can see why detection missed.
485
524
  defaultProjectHint = null;
525
+ // Per-start-path cache of the git worktree/index mismatch (issue #155). The
526
+ // mismatch is a fixed property of (where the request came from → which
527
+ // .codegraph/ it resolves to), so the up-to-two `git rev-parse` spawns run
528
+ // once and every later tool call reuses the result — never shelling out to
529
+ // git on the hot path. `undefined` = not computed yet; `null` = no mismatch.
530
+ worktreeMismatchCache = new Map();
486
531
  constructor(cg) {
487
532
  this.cg = cg;
488
533
  }
@@ -632,6 +677,7 @@ class ToolHandler {
632
677
  cg.close();
633
678
  }
634
679
  this.projectCache.clear();
680
+ this.worktreeMismatchCache.clear();
635
681
  }
636
682
  /**
637
683
  * Validate that a value is a non-empty string within length bounds.
@@ -665,6 +711,132 @@ class ToolHandler {
665
711
  }
666
712
  return value;
667
713
  }
714
+ /**
715
+ * Cached git worktree/index mismatch for a tool call's effective project.
716
+ *
717
+ * The "effective project" is what the request targets: an explicit
718
+ * `projectPath` arg, else the directory the server resolved its default
719
+ * project from (`defaultProjectHint`), else cwd. Memoized per start path —
720
+ * see `worktreeMismatchCache`. Best-effort: if the project can't be resolved
721
+ * (e.g. nothing initialized yet), it reports "no mismatch" so a tool is never
722
+ * broken by this check.
723
+ */
724
+ worktreeMismatchFor(projectPath) {
725
+ const startPath = projectPath ?? this.defaultProjectHint ?? process.cwd();
726
+ const cached = this.worktreeMismatchCache.get(startPath);
727
+ if (cached !== undefined)
728
+ return cached;
729
+ let mismatch = null;
730
+ try {
731
+ mismatch = (0, worktree_1.detectWorktreeIndexMismatch)(startPath, this.getCodeGraph(projectPath).getProjectRoot());
732
+ }
733
+ catch {
734
+ // No resolvable project (or any other resolution error) → nothing to warn.
735
+ mismatch = null;
736
+ }
737
+ this.worktreeMismatchCache.set(startPath, mismatch);
738
+ return mismatch;
739
+ }
740
+ /**
741
+ * Prefix a successful read-tool result with a compact worktree-mismatch
742
+ * notice when the resolved index belongs to a different git working tree than
743
+ * the caller's (issue #155). Without this, an agent in a nested worktree
744
+ * silently trusts main-branch results. No-op on error results and when there
745
+ * is no mismatch. `codegraph_status` is excluded — it embeds its own verbose
746
+ * warning — so it stays out of this path.
747
+ */
748
+ withWorktreeNotice(result, projectPath) {
749
+ if (result.isError)
750
+ return result;
751
+ const mismatch = this.worktreeMismatchFor(projectPath);
752
+ if (!mismatch)
753
+ return result;
754
+ const notice = (0, worktree_1.worktreeMismatchNotice)(mismatch);
755
+ const [first, ...rest] = result.content;
756
+ if (first && first.type === 'text') {
757
+ return { ...result, content: [{ type: 'text', text: `${notice}\n\n${first.text}` }, ...rest] };
758
+ }
759
+ return result;
760
+ }
761
+ /**
762
+ * Annotate a successful read-tool result with per-file staleness — the
763
+ * non-blocking answer to issue #403. The file watcher tracks every event
764
+ * it sees per path; here we intersect "files referenced in this response"
765
+ * against that pending set and prepend a compact banner so the agent can
766
+ * fall back to Read for those *specific* files without waiting for the
767
+ * debounced sync to fire. Other pending files in the project (not
768
+ * referenced by this response) get a small footer so the agent has a
769
+ * complete picture without bloating the banner.
770
+ *
771
+ * Cost when nothing is pending — the common case — is one boolean check.
772
+ * No I/O, no parsing of markdown beyond a per-pending-file substring scan.
773
+ */
774
+ withStalenessNotice(result, projectPath) {
775
+ if (result.isError)
776
+ return result;
777
+ let cg;
778
+ try {
779
+ cg = this.getCodeGraph(projectPath);
780
+ }
781
+ catch {
782
+ return result; // no default project — leave as is
783
+ }
784
+ // Cross-project `projectPath` calls open a cached CodeGraph WITHOUT a
785
+ // watcher (watchers are only attached to the default session project).
786
+ // When the cross-project path happens to be the same project as the
787
+ // default cg, the cached instance is the wrong one — its pendingFiles is
788
+ // permanently empty. Detect the equal-path case and prefer the default
789
+ // cg so the staleness signal still fires when an agent passes the
790
+ // explicit projectPath form of its own project.
791
+ if (this.cg && cg !== this.cg) {
792
+ try {
793
+ const sameProject = (0, path_1.resolve)(this.cg.getProjectRoot()) === (0, path_1.resolve)(cg.getProjectRoot());
794
+ if (sameProject)
795
+ cg = this.cg;
796
+ }
797
+ catch {
798
+ /* getProjectRoot may throw on a closed instance — leave cg as is */
799
+ }
800
+ }
801
+ // Defensive: some test fakes inject a partial CodeGraph stub without the
802
+ // newer pending-files API. Treat missing/throwing as "no pending files."
803
+ let pending = [];
804
+ try {
805
+ pending = cg.getPendingFiles?.() ?? [];
806
+ }
807
+ catch {
808
+ return result;
809
+ }
810
+ if (pending.length === 0)
811
+ return result;
812
+ const [first, ...rest] = result.content;
813
+ if (!first || first.type !== 'text')
814
+ return result;
815
+ const text = first.text;
816
+ const inResponse = [];
817
+ const elsewhere = [];
818
+ for (const p of pending) {
819
+ // Substring match against the project-relative POSIX path — that's
820
+ // exactly the format both the watcher and every codegraph response
821
+ // emit, so a plain includes() is sufficient and avoids regex pitfalls.
822
+ if (text.includes(p.path))
823
+ inResponse.push(p);
824
+ else
825
+ elsewhere.push(p);
826
+ }
827
+ let banner = '';
828
+ if (inResponse.length > 0) {
829
+ banner = formatStaleBanner(inResponse);
830
+ }
831
+ let footer = '';
832
+ if (elsewhere.length > 0) {
833
+ footer = formatStaleFooter(elsewhere);
834
+ }
835
+ if (!banner && !footer)
836
+ return result;
837
+ const composed = [banner, text, footer].filter(Boolean).join('\n\n');
838
+ return { ...result, content: [{ type: 'text', text: composed }, ...rest] };
839
+ }
668
840
  /**
669
841
  * Execute a tool by name
670
842
  */
@@ -695,30 +867,51 @@ class ToolHandler {
695
867
  if (typeof check === 'object' && check !== undefined)
696
868
  return check;
697
869
  }
870
+ // Read tools resolve through a single result variable so cross-cutting
871
+ // notices — worktree-index mismatch (issue #155) and per-file
872
+ // staleness (issue #403) — can be applied in one place. status embeds
873
+ // its own verbose worktree warning but still flows through the
874
+ // staleness wrapper so its pending-files section stays consistent
875
+ // with what the read tools surface.
876
+ let result;
698
877
  switch (toolName) {
699
878
  case 'codegraph_search':
700
- return await this.handleSearch(args);
879
+ result = await this.handleSearch(args);
880
+ break;
701
881
  case 'codegraph_context':
702
- return await this.handleContext(args);
882
+ result = await this.handleContext(args);
883
+ break;
703
884
  case 'codegraph_callers':
704
- return await this.handleCallers(args);
885
+ result = await this.handleCallers(args);
886
+ break;
705
887
  case 'codegraph_callees':
706
- return await this.handleCallees(args);
888
+ result = await this.handleCallees(args);
889
+ break;
707
890
  case 'codegraph_impact':
708
- return await this.handleImpact(args);
891
+ result = await this.handleImpact(args);
892
+ break;
709
893
  case 'codegraph_explore':
710
- return await this.handleExplore(args);
894
+ result = await this.handleExplore(args);
895
+ break;
711
896
  case 'codegraph_node':
712
- return await this.handleNode(args);
897
+ result = await this.handleNode(args);
898
+ break;
713
899
  case 'codegraph_status':
900
+ // status embeds the pending-files list as a first-class section
901
+ // (see handleStatus), so we skip the auto-banner wrapper here to
902
+ // avoid duplicating the same info at the top of the response.
714
903
  return await this.handleStatus(args);
715
904
  case 'codegraph_files':
716
- return await this.handleFiles(args);
905
+ result = await this.handleFiles(args);
906
+ break;
717
907
  case 'codegraph_trace':
718
- return await this.handleTrace(args);
908
+ result = await this.handleTrace(args);
909
+ break;
719
910
  default:
720
911
  return this.errorResult(`Unknown tool: ${toolName}`);
721
912
  }
913
+ const withWorktree = this.withWorktreeNotice(result, args.projectPath);
914
+ return this.withStalenessNotice(withWorktree, args.projectPath);
722
915
  }
723
916
  catch (err) {
724
917
  return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -1870,16 +2063,34 @@ class ToolHandler {
1870
2063
  * Handle codegraph_status
1871
2064
  */
1872
2065
  async handleStatus(args) {
1873
- const cg = this.getCodeGraph(args.projectPath);
2066
+ let cg = this.getCodeGraph(args.projectPath);
2067
+ // Same trick as withStalenessNotice — when an explicit projectPath
2068
+ // resolves to the same project as the default session cg, prefer the
2069
+ // default so getPendingFiles() (only populated by the default's watcher)
2070
+ // is non-empty when there are pending edits.
2071
+ if (this.cg && cg !== this.cg) {
2072
+ try {
2073
+ if ((0, path_1.resolve)(this.cg.getProjectRoot()) === (0, path_1.resolve)(cg.getProjectRoot())) {
2074
+ cg = this.cg;
2075
+ }
2076
+ }
2077
+ catch { /* closed instance — leave as is */ }
2078
+ }
1874
2079
  const stats = cg.getStats();
2080
+ // Warn when this index actually belongs to a different git working tree
2081
+ // (e.g. the server resolved up from a nested worktree to the main checkout).
2082
+ // Queries then reflect that tree's branch, not the worktree being edited.
2083
+ // status shows the verbose, multi-line form; the read tools get the compact
2084
+ // one-liner via withWorktreeNotice. Both share the cached detection.
2085
+ const mismatch = this.worktreeMismatchFor(args.projectPath);
1875
2086
  const lines = [
1876
2087
  '## CodeGraph Status',
1877
2088
  '',
1878
- `**Files indexed:** ${stats.fileCount}`,
1879
- `**Total nodes:** ${stats.nodeCount}`,
1880
- `**Total edges:** ${stats.edgeCount}`,
1881
- `**Database size:** ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`,
1882
2089
  ];
2090
+ if (mismatch) {
2091
+ lines.push(`> ⚠ ${(0, worktree_1.worktreeMismatchWarning)(mismatch).replace(/\n/g, '\n> ')}`, '');
2092
+ }
2093
+ lines.push(`**Files indexed:** ${stats.fileCount}`, `**Total nodes:** ${stats.nodeCount}`, `**Total edges:** ${stats.edgeCount}`, `**Database size:** ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`);
1883
2094
  // Surface the active SQLite backend (node:sqlite, Node's built-in real
1884
2095
  // SQLite — full WAL + FTS5, no native build).
1885
2096
  lines.push(`**Backend:** node:sqlite (Node built-in) — full WAL + FTS5`);
@@ -1907,6 +2118,20 @@ class ToolHandler {
1907
2118
  lines.push(`- ${lang}: ${count}`);
1908
2119
  }
1909
2120
  }
2121
+ // Per-file freshness — the inverse of the auto-prepended staleness banner
2122
+ // (issue #403). Surfacing it inside `status` gives the agent a single
2123
+ // place to ask "is the index caught up?" rather than inferring from
2124
+ // banners on other tool calls.
2125
+ const pending = cg.getPendingFiles();
2126
+ if (pending.length > 0) {
2127
+ lines.push('', '### Pending sync:');
2128
+ const now = Date.now();
2129
+ for (const p of pending) {
2130
+ const ageMs = Math.max(0, now - p.lastSeenMs);
2131
+ const label = p.indexing ? 'indexing in progress' : 'pending sync';
2132
+ lines.push(`- ${p.path} (edited ${ageMs}ms ago, ${label})`);
2133
+ }
2134
+ }
1910
2135
  return this.textResult(lines.join('\n'));
1911
2136
  }
1912
2137
  /**