@iderouter/index-mcp 0.2.0-beta.1

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 (69) hide show
  1. package/README.md +93 -0
  2. package/package.json +26 -0
  3. package/scripts/benchmark-all.mjs +177 -0
  4. package/scripts/benchmark-auto-continuation.mjs +188 -0
  5. package/scripts/benchmark-background-fine-resume.mjs +245 -0
  6. package/scripts/benchmark-background-fine-wait.mjs +76 -0
  7. package/scripts/benchmark-background-fine.mjs +132 -0
  8. package/scripts/benchmark-clean-snapshot.mjs +83 -0
  9. package/scripts/benchmark-coarse-ready-search.mjs +161 -0
  10. package/scripts/benchmark-deferred.mjs +62 -0
  11. package/scripts/benchmark-first-semantic-visible.mjs +151 -0
  12. package/scripts/benchmark-gate.mjs +107 -0
  13. package/scripts/benchmark-generic-resumed-single-chunk-embed.mjs +104 -0
  14. package/scripts/benchmark-noop.mjs +24 -0
  15. package/scripts/benchmark-priority-ready-search.mjs +165 -0
  16. package/scripts/benchmark-repeat-search.mjs +148 -0
  17. package/scripts/benchmark-resumed-retry-burst.mjs +187 -0
  18. package/scripts/benchmark-resumed-single-chunk-success.mjs +154 -0
  19. package/scripts/benchmark-resumed-single-chunk.mjs +146 -0
  20. package/scripts/benchmark-single-priority-chunk-embed.mjs +145 -0
  21. package/scripts/benchmark-small-change.mjs +146 -0
  22. package/scripts/benchmark-stage-summary.mjs +88 -0
  23. package/scripts/lib/auto-continuation-state.mjs +34 -0
  24. package/scripts/lib/benchmark-query-packs.mjs +123 -0
  25. package/scripts/lib/benchmark-snapshot.mjs +109 -0
  26. package/scripts/lib/mcp-bench.mjs +455 -0
  27. package/src/architecture-query-fallback.js +50 -0
  28. package/src/background-definition-chunks.js +199 -0
  29. package/src/background-embedding-profile.js +64 -0
  30. package/src/background-fine-budget.js +18 -0
  31. package/src/background-fine-runtime.js +179 -0
  32. package/src/background-fine-selection.js +332 -0
  33. package/src/checkpoint-policy.js +16 -0
  34. package/src/conflict-policy.js +17 -0
  35. package/src/deferred-retry-delay.js +14 -0
  36. package/src/deferred-retry-status.js +10 -0
  37. package/src/embedding-attempt-ordinal.js +17 -0
  38. package/src/embedding-failure-penalty.js +60 -0
  39. package/src/embedding-failure-policy.js +52 -0
  40. package/src/embedding-flush-timeout.js +33 -0
  41. package/src/embedding-inflight-status.js +18 -0
  42. package/src/embedding-model-policy.js +44 -0
  43. package/src/embedding-next-switch.js +18 -0
  44. package/src/embedding-request-status-detail.js +25 -0
  45. package/src/embedding-request-status.js +22 -0
  46. package/src/embedding-selection-order.js +23 -0
  47. package/src/fine-run-queue.js +14 -0
  48. package/src/index.js +7970 -0
  49. package/src/job-supersession.js +25 -0
  50. package/src/priority-progress.js +20 -0
  51. package/src/priority-ready-anchor-coverage-normalize.js +18 -0
  52. package/src/priority-ready-anchor-coverage.js +23 -0
  53. package/src/priority-ready-hotspots.js +344 -0
  54. package/src/priority-ready-status.js +30 -0
  55. package/src/priority-ready-targets.js +45 -0
  56. package/src/priority-usable-attempt-plan.js +44 -0
  57. package/src/priority-usable-attempt-timeout.js +18 -0
  58. package/src/priority-usable-fast-path.js +11 -0
  59. package/src/priority-usable-probe-order.js +34 -0
  60. package/src/remote-strategy-failure-cache.js +55 -0
  61. package/src/resume-seed.js +9 -0
  62. package/src/semantic-first-checkpoint.js +8 -0
  63. package/src/semantic-slow-path.js +10 -0
  64. package/src/single-chunk-attempt-timeout.js +13 -0
  65. package/src/single-chunk-embedding-content.js +26 -0
  66. package/src/single-chunk-embedding-policy.js +18 -0
  67. package/src/single-chunk-provider-order.js +12 -0
  68. package/src/single-chunk-provider-policy.js +63 -0
  69. package/src/worker-lock-retry.js +24 -0
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # IDERouter Index MCP
2
+
3
+ Hybrid local code indexing MCP for Cursor, Codex, and Claude Code.
4
+
5
+ The MCP process runs locally so it can read your repository files. It uses your
6
+ IDERouter API key for embedding requests and stores the generated index under:
7
+
8
+ ```text
9
+ ~/.iderouter/index-mcp/indexes
10
+ ```
11
+
12
+ Re-running `index_codebase` is incremental: unchanged files are reused from the
13
+ local manifest without being read again, and unchanged chunks reuse their
14
+ existing vectors. Only new or modified chunks call the embedding API.
15
+
16
+ The MCP uses a fixed two-model embedding policy. New fine indexes prefer
17
+ `qwen/qwen3-embedding-8b` and automatically fall back to
18
+ `openai/text-embedding-3-small` when the primary model is unavailable or slow.
19
+ Users only configure an IDERouter API key; the endpoint and model policy are
20
+ built in. Existing completed indexes keep using the model stored with that index
21
+ so vector spaces are not mixed.
22
+
23
+ `search_code` also checks the workspace manifest before searching. If files were
24
+ added, removed, or changed, it performs the same incremental refresh first so
25
+ agents do not search stale code.
26
+
27
+ Indexing starts as a background job by default, so AI agents can keep working
28
+ while the local index is being built. To use async indexing:
29
+
30
+ 1. Call `index_codebase` with the target `path` and omit `wait` (or set
31
+ `wait=false`).
32
+ 2. Continue with other work while the job runs in the background.
33
+ 3. Call `get_indexing_status` with the same `path` whenever progress is needed.
34
+
35
+ The product is local-first: repository files stay on the machine running the
36
+ MCP. Remote usage is limited to embedding requests and aggregate billing
37
+ metadata.
38
+
39
+ MCP installation itself cannot reliably auto-install client-side skills or
40
+ rules across Cursor, Codex, and Claude Code. The supported path is to pair the
41
+ MCP config with a companion prompt or rules block in the client so the agent
42
+ prefers `iderouter-index` before broad file reads.
43
+
44
+ ## Environment
45
+
46
+ ```bash
47
+ export IDEROUTER_API_KEY="ir-..."
48
+ ```
49
+
50
+ Optional advanced tuning:
51
+
52
+ ```bash
53
+ # Primary probe slower than this will compare the built-in fallback model.
54
+ export IDEROUTER_EMBEDDING_HEALTH_SLOW_MS=8000
55
+ ```
56
+
57
+ ## Optional Codex Skill Install
58
+
59
+ If you want Codex to prefer `iderouter-index` automatically, install the
60
+ companion skill explicitly:
61
+
62
+ ```bash
63
+ npx @iderouter/index-mcp init codex
64
+ ```
65
+
66
+ This writes:
67
+
68
+ ```text
69
+ ~/.codex/skills/iderouter-index/SKILL.md
70
+ ```
71
+
72
+ The install step is explicit on purpose; normal MCP startup does not modify the
73
+ client skill catalog automatically.
74
+
75
+ ## Tools
76
+
77
+ - `index_codebase`: index a local directory.
78
+ - `search_code`: semantic search against the local index.
79
+ - `ask_codebase`: answer a repository question from indexed code and cite matching snippets.
80
+ - `clear_index`: delete an index.
81
+ - `get_indexing_status`: show indexed file/chunk counts and reuse diagnostics.
82
+
83
+ ## Codex Config
84
+
85
+ ```toml
86
+ [mcp_servers.iderouter-index]
87
+ type = "stdio"
88
+ command = "npx"
89
+ args = ["-y", "@iderouter/index-mcp"]
90
+
91
+ [mcp_servers.iderouter-index.env]
92
+ IDEROUTER_API_KEY = "ir-..."
93
+ ```
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@iderouter/index-mcp",
3
+ "version": "0.2.0-beta.1",
4
+ "description": "Hybrid local code index MCP powered by IDERouter embeddings.",
5
+ "type": "module",
6
+ "bin": {
7
+ "iderouter-index-mcp": "./src/index.js"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "scripts",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "check": "node --check src/index.js",
19
+ "benchmark:noop": "node scripts/benchmark-noop.mjs",
20
+ "benchmark:small-change": "node scripts/benchmark-small-change.mjs",
21
+ "benchmark:deferred": "node scripts/benchmark-deferred.mjs",
22
+ "benchmark:all": "node scripts/benchmark-all.mjs",
23
+ "benchmark:clean-snapshot": "node scripts/benchmark-clean-snapshot.mjs",
24
+ "benchmark:stage-summary": "node scripts/benchmark-stage-summary.mjs"
25
+ }
26
+ }
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from "node:child_process";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { promisify } from "node:util";
7
+ import { gitWorkspaceStats } from "./lib/mcp-bench.mjs";
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const TARGET_PATH = path.resolve(process.argv[2] || process.cwd());
11
+ const SCRIPT_DIR = path.resolve("scripts");
12
+
13
+ async function runScript(scriptName) {
14
+ const scriptPath = path.join(SCRIPT_DIR, scriptName);
15
+ const { stdout } = await execFileAsync(process.execPath, [scriptPath, TARGET_PATH], {
16
+ cwd: process.cwd(),
17
+ env: process.env,
18
+ maxBuffer: 8 * 1024 * 1024,
19
+ });
20
+ return JSON.parse(String(stdout || "").trim());
21
+ }
22
+
23
+ function summarize(result) {
24
+ if (result?.priority_ready && Array.isArray(result?.searches)) {
25
+ return {
26
+ priority_ready_ms: result.priority_ready?.parsed?.priority_ready_ms ?? result.priority_ready?.wallMs ?? 0,
27
+ priority_ready_anchor_completed: result.priority_ready_anchor_coverage?.completed ?? null,
28
+ priority_ready_anchor_total: result.priority_ready_anchor_coverage?.total ?? null,
29
+ query_count: result.searches.length,
30
+ all_top1_hit: result.searches.every((item) => item?.top1_hit === true),
31
+ max_search_ms: Math.max(...result.searches.map((item) => Number(item?.wall_ms || 0)), 0),
32
+ searches: result.searches.map((item) => ({
33
+ name: item.name,
34
+ wall_ms: item.wall_ms,
35
+ top1_hit: item.top1_hit,
36
+ top1: item.top1,
37
+ })),
38
+ };
39
+ }
40
+ if (result?.priority_ready && result?.first && result?.second) {
41
+ return {
42
+ priority_ready_ms: result.priority_ready?.parsed?.priority_ready_ms ?? result.priority_ready?.wallMs ?? 0,
43
+ priority_ready_anchor_completed: result.priority_ready_anchor_coverage?.completed ?? null,
44
+ priority_ready_anchor_total: result.priority_ready_anchor_coverage?.total ?? null,
45
+ repeat_query: result.repeat_query || "",
46
+ first_ms: result.first?.wall_ms ?? null,
47
+ second_ms: result.second?.wall_ms ?? null,
48
+ speedup_ratio: result.speedup_ratio ?? null,
49
+ };
50
+ }
51
+ return {
52
+ wall_ms: result.wall_ms ?? 0,
53
+ scan_ms: result.scan_ms ?? 0,
54
+ total_ms: result.total_ms ?? 0,
55
+ strategy_version: result.strategy_version || "",
56
+ strategy_source: result.strategy_source || "",
57
+ embedding_model_source: result.embedding_model_source || "",
58
+ final_status: result.final_status || "",
59
+ };
60
+ }
61
+
62
+ async function main() {
63
+ const workspace = await gitWorkspaceStats(TARGET_PATH);
64
+ const output = {
65
+ path: TARGET_PATH,
66
+ api_key_present: Boolean(process.env.IDEROUTER_API_KEY),
67
+ generated_at: new Date().toISOString(),
68
+ workspace,
69
+ benchmarks: {},
70
+ };
71
+
72
+ const cases = [
73
+ ["noop", "benchmark-noop.mjs"],
74
+ ["small_change", "benchmark-small-change.mjs"],
75
+ ["deferred", "benchmark-deferred.mjs"],
76
+ ["priority_ready_search", "benchmark-priority-ready-search.mjs"],
77
+ ["repeat_search", "benchmark-repeat-search.mjs"],
78
+ ["stage_summary", "benchmark-stage-summary.mjs"],
79
+ ];
80
+
81
+ for (const [name, scriptName] of cases) {
82
+ try {
83
+ const result = await runScript(scriptName);
84
+ output.benchmarks[name] = {
85
+ ok: true,
86
+ summary: summarize(result),
87
+ result,
88
+ };
89
+ } catch (error) {
90
+ output.benchmarks[name] = {
91
+ ok: false,
92
+ error: error instanceof Error ? error.message : String(error),
93
+ };
94
+ }
95
+ }
96
+
97
+ if (String(process.env.IDEROUTER_BENCH_INCLUDE_CLEAN_SNAPSHOT || "").toLowerCase() === "true") {
98
+ try {
99
+ const result = await runScript("benchmark-clean-snapshot.mjs");
100
+ output.benchmarks.clean_snapshot = {
101
+ ok: true,
102
+ summary: summarize(result.measure || result),
103
+ result,
104
+ };
105
+ } catch (error) {
106
+ output.benchmarks.clean_snapshot = {
107
+ ok: false,
108
+ error: error instanceof Error ? error.message : String(error),
109
+ };
110
+ }
111
+ }
112
+
113
+ output.comparison = buildComparison(output);
114
+
115
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
116
+ }
117
+
118
+ function buildComparison(output) {
119
+ const noop = output.benchmarks.noop?.result;
120
+ const smallChange = output.benchmarks.small_change?.result;
121
+ const deferred = output.benchmarks.deferred?.result;
122
+ const priorityReadySearch = output.benchmarks.priority_ready_search?.result;
123
+ const repeatSearch = output.benchmarks.repeat_search?.result;
124
+ const cleanSnapshot = output.benchmarks.clean_snapshot?.result;
125
+ const stageSummary = output.benchmarks.stage_summary?.result;
126
+ const resumeRounds = Array.isArray(stageSummary?.resume_rounds) ? stageSummary.resume_rounds : [];
127
+ const finalResumeAnchorCoverage = resumeRounds.length > 0 ? resumeRounds[resumeRounds.length - 1]?.priority_ready_anchor_coverage : null;
128
+ const workspace = output.workspace || {};
129
+ const noOpComparable = workspace.workspace_dirty === false || Number(workspace.indexable_dirty_entries || 0) === 0;
130
+ const cleanComparable = Boolean(cleanSnapshot?.comparable_to_auggie);
131
+ return {
132
+ no_op_comparable_to_auggie: noOpComparable,
133
+ clean_snapshot_available: Boolean(cleanSnapshot),
134
+ clean_snapshot_comparable_to_auggie: cleanComparable,
135
+ note: noOpComparable
136
+ ? "No-op benchmark is comparable to Auggie daily ready path."
137
+ : cleanComparable
138
+ ? "Workspace is dirty, but clean snapshot benchmark is available for fair Auggie comparison."
139
+ : "Workspace has indexable dirty files; no-op benchmark is not a true clean-repo comparison against Auggie.",
140
+ current_signals: {
141
+ noop_total_ms: noop?.total_ms ?? null,
142
+ clean_snapshot_noop_total_ms: cleanSnapshot?.measure?.total_ms ?? null,
143
+ clean_snapshot_strategy_mode: cleanSnapshot?.strategy_mode ?? null,
144
+ small_change_changed: smallChange?.observed_index_change ?? null,
145
+ deferred_stable: deferred?.stable_status ?? null,
146
+ deferred_resumed_unexpectedly: deferred?.resumed_unexpectedly ?? null,
147
+ priority_ready_ms: priorityReadySearch?.priority_ready?.parsed?.priority_ready_ms ?? null,
148
+ priority_ready_anchor_completed: priorityReadySearch?.priority_ready_anchor_coverage?.completed ?? null,
149
+ priority_ready_anchor_total: priorityReadySearch?.priority_ready_anchor_coverage?.total ?? null,
150
+ priority_ready_all_top1_hit: Array.isArray(priorityReadySearch?.searches)
151
+ ? priorityReadySearch.searches.every((item) => item?.top1_hit === true)
152
+ : null,
153
+ priority_ready_max_search_ms: Array.isArray(priorityReadySearch?.searches)
154
+ ? Math.max(...priorityReadySearch.searches.map((item) => Number(item?.wall_ms || 0)), 0)
155
+ : null,
156
+ repeat_search_first_ms: repeatSearch?.first?.wall_ms ?? null,
157
+ repeat_search_second_ms: repeatSearch?.second?.wall_ms ?? null,
158
+ repeat_search_speedup_ratio: repeatSearch?.speedup_ratio ?? null,
159
+ repeat_search_priority_ready_anchor_completed: repeatSearch?.priority_ready_anchor_coverage?.completed ?? null,
160
+ repeat_search_priority_ready_anchor_total: repeatSearch?.priority_ready_anchor_coverage?.total ?? null,
161
+ stage_summary_priority_ready_ms: stageSummary?.priority_ready_ms ?? null,
162
+ stage_summary_priority_ready_anchor_completed: stageSummary?.priority_ready_anchor_completed ?? null,
163
+ stage_summary_priority_ready_anchor_total: stageSummary?.priority_ready_anchor_total ?? null,
164
+ stage_summary_resume_rounds_run: stageSummary?.resume_rounds_run ?? null,
165
+ stage_summary_resume_final_anchor_completed: finalResumeAnchorCoverage?.completed ?? null,
166
+ stage_summary_resume_final_anchor_total: finalResumeAnchorCoverage?.total ?? null,
167
+ stage_summary_resume_cumulative_semantic_files: stageSummary?.resume_cumulative_semantic_files ?? null,
168
+ stage_summary_composite_score: stageSummary?.composite_score ?? null,
169
+ stage_summary_composite_band: stageSummary?.composite_band ?? null,
170
+ },
171
+ };
172
+ }
173
+
174
+ main().catch((error) => {
175
+ console.error(error instanceof Error ? error.message : String(error));
176
+ process.exitCode = 1;
177
+ });
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+
8
+ import {
9
+ cleanupBenchmarkRun,
10
+ installBenchmarkSignalCleanup,
11
+ readIndexMeta,
12
+ readJobStatus,
13
+ sleep,
14
+ spawnMcpClient,
15
+ } from "./lib/mcp-bench.mjs";
16
+ import {
17
+ continuationAlreadyMaterialized,
18
+ hasAdvanced,
19
+ isCoarseDeferredReady,
20
+ isFullReadyState,
21
+ } from "./lib/auto-continuation-state.mjs";
22
+ import { createBenchmarkSnapshot } from "./lib/benchmark-snapshot.mjs";
23
+
24
+ const TARGET_PATH = path.resolve(process.argv[2] || process.cwd());
25
+ const POLL_MS = Number(process.env.IDEROUTER_BENCH_POLL_MS || 1000);
26
+ const FIRST_PHASE_TIMEOUT_MS = Number(process.env.IDEROUTER_BENCH_AUTO_CONTINUE_FIRST_PHASE_TIMEOUT_MS || 180000);
27
+ const CONTINUATION_TIMEOUT_MS = Number(process.env.IDEROUTER_BENCH_AUTO_CONTINUE_TIMEOUT_MS || 180000);
28
+
29
+ function snapshotState(meta, job, statusText) {
30
+ return {
31
+ status_text: statusText.split("\n")[0],
32
+ job_id: job?.id || "",
33
+ job_status: job?.status || "",
34
+ current_step: job?.currentStep || "",
35
+ continuation_pass_count: Number(job?.continuationPassCount || 0),
36
+ progress: Number(job?.progress || 0),
37
+ embedded_count: Number(job?.embeddedCount || 0),
38
+ total_chunks: Number(job?.totalChunks || 0),
39
+ background_fine_deferred: Boolean(job?.backgroundFineDeferred),
40
+ semantic_retry_deferred: Boolean(job?.semanticRetryDeferred),
41
+ searchable_modes: job?.searchableModes || meta?.searchableModes || "",
42
+ priority_fine_progress: Number(job?.priorityFineProgress || meta?.priorityFineProgress || 0),
43
+ fine_semantic_files: Number(meta?.fineSemanticFileCount || 0),
44
+ fine_semantic_chunks: Number(meta?.fineSemanticChunkCount || 0),
45
+ background_bucket_cursor: String(meta?.backgroundBucketCursor || ""),
46
+ background_fine_target_files: Number(meta?.backgroundFineTargetFiles || job?.diagnostics?.backgroundFineTargetFiles || 0),
47
+ background_fine_target_chunks: Number(meta?.backgroundFineTargetChunks || job?.diagnostics?.backgroundFineTargetChunks || 0),
48
+ semantic_retry_deferred_at: String(job?.semanticRetryDeferredAt || ""),
49
+ deferred_resume_worker_pid: Number(job?.deferredResumeWorkerPid || 0),
50
+ updated_at: job?.updatedAt || meta?.updatedAt || "",
51
+ };
52
+ }
53
+
54
+ async function waitForDeferredReady(client, snapshotDir, indexHome) {
55
+ const startedAt = Date.now();
56
+ let lastText = "";
57
+ while (Date.now() - startedAt < FIRST_PHASE_TIMEOUT_MS) {
58
+ lastText = await client.callTool("get_indexing_status", { path: snapshotDir });
59
+ const meta = await readIndexMeta(indexHome, snapshotDir);
60
+ const job = await readJobStatus(indexHome, snapshotDir);
61
+ const deferredReady =
62
+ Boolean(job?.backgroundFineDeferred) &&
63
+ Boolean(job?.priorityFineReady) &&
64
+ /^Priority semantic search is ready\b/.test(lastText);
65
+ const coarseDeferredReady =
66
+ Boolean(job?.backgroundFineDeferred) &&
67
+ !Boolean(job?.priorityFineReady) &&
68
+ isCoarseDeferredReady(snapshotState(meta, job, lastText));
69
+ const fullReady = Boolean(meta?.complete) || isFullReadyState(snapshotState(meta, job, lastText));
70
+ if (fullReady) {
71
+ return {
72
+ settled: true,
73
+ wall_ms: Date.now() - startedAt,
74
+ state: snapshotState(meta, job, lastText),
75
+ };
76
+ }
77
+ if (deferredReady) {
78
+ return {
79
+ settled: true,
80
+ wall_ms: Date.now() - startedAt,
81
+ state: snapshotState(meta, job, lastText),
82
+ };
83
+ }
84
+ if (coarseDeferredReady) {
85
+ return {
86
+ settled: true,
87
+ wall_ms: Date.now() - startedAt,
88
+ state: snapshotState(meta, job, lastText),
89
+ };
90
+ }
91
+ await sleep(POLL_MS);
92
+ }
93
+ const meta = await readIndexMeta(indexHome, snapshotDir);
94
+ const job = await readJobStatus(indexHome, snapshotDir);
95
+ return {
96
+ settled: false,
97
+ wall_ms: Date.now() - startedAt,
98
+ state: snapshotState(meta, job, lastText),
99
+ };
100
+ }
101
+
102
+ async function waitForAutoContinuation(client, snapshotDir, indexHome, baselineState) {
103
+ const startedAt = Date.now();
104
+ let lastText = "";
105
+ while (Date.now() - startedAt < CONTINUATION_TIMEOUT_MS) {
106
+ lastText = await client.callTool("get_indexing_status", { path: snapshotDir });
107
+ const meta = await readIndexMeta(indexHome, snapshotDir);
108
+ const job = await readJobStatus(indexHome, snapshotDir);
109
+ const currentState = snapshotState(meta, job, lastText);
110
+ const continuationOccurred = Number(currentState.continuation_pass_count || 0) >= Number(baselineState.continuation_pass_count || 0);
111
+ const activeOrNewJob =
112
+ String(currentState.job_id || "") !== String(baselineState.job_id || "") ||
113
+ ["queued", "scanning", "indexing"].includes(String(currentState.job_status || ""));
114
+ if (continuationOccurred && activeOrNewJob && hasAdvanced(baselineState, currentState)) {
115
+ return {
116
+ settled: true,
117
+ wall_ms: Date.now() - startedAt,
118
+ state: currentState,
119
+ };
120
+ }
121
+ await sleep(POLL_MS);
122
+ }
123
+ const meta = await readIndexMeta(indexHome, snapshotDir);
124
+ const job = await readJobStatus(indexHome, snapshotDir);
125
+ return {
126
+ settled: false,
127
+ wall_ms: Date.now() - startedAt,
128
+ state: snapshotState(meta, job, lastText),
129
+ };
130
+ }
131
+
132
+ async function main() {
133
+ const snapshot = await createBenchmarkSnapshot(TARGET_PATH, process.env);
134
+ const snapshotDir = snapshot.snapshotDir;
135
+ const isolatedIndexHome = await fs.mkdtemp(path.join(os.tmpdir(), "iderouter-bench-auto-continue-"));
136
+ const env = {
137
+ ...process.env,
138
+ IDEROUTER_INDEX_HOME: isolatedIndexHome,
139
+ };
140
+ const { child, client } = spawnMcpClient(env);
141
+ const uninstallSignalCleanup = installBenchmarkSignalCleanup(() => ({
142
+ child,
143
+ indexHome: isolatedIndexHome,
144
+ targetPath: snapshotDir,
145
+ snapshotDir,
146
+ }));
147
+
148
+ try {
149
+ await client.initialize();
150
+ const startText = await client.callTool("index_codebase", { path: snapshotDir, wait: false, cloud: false });
151
+ const firstPhase = await waitForDeferredReady(client, snapshotDir, isolatedIndexHome);
152
+ const continuation = !firstPhase.settled
153
+ ? { settled: false, wall_ms: 0, state: firstPhase.state }
154
+ : continuationAlreadyMaterialized(firstPhase.state)
155
+ ? { settled: true, wall_ms: 0, state: firstPhase.state }
156
+ : await waitForAutoContinuation(client, snapshotDir, isolatedIndexHome, firstPhase.state);
157
+
158
+ process.stdout.write(
159
+ `${JSON.stringify(
160
+ {
161
+ path: TARGET_PATH,
162
+ snapshot_path: snapshotDir,
163
+ snapshot_mode: snapshot.mode,
164
+ isolated_index_home: isolatedIndexHome,
165
+ start_status: startText.split("\n")[0],
166
+ first_phase: firstPhase,
167
+ continuation,
168
+ auto_continuation_confirmed: Boolean(firstPhase.settled && continuation.settled),
169
+ },
170
+ null,
171
+ 2,
172
+ )}\n`,
173
+ );
174
+ } finally {
175
+ uninstallSignalCleanup();
176
+ await cleanupBenchmarkRun({
177
+ child,
178
+ indexHome: isolatedIndexHome,
179
+ targetPath: snapshotDir,
180
+ snapshotDir,
181
+ });
182
+ }
183
+ }
184
+
185
+ main().catch((error) => {
186
+ console.error(error instanceof Error ? error.message : String(error));
187
+ process.exitCode = 1;
188
+ });