@martinloop/mcp 0.2.7 → 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.
Files changed (52) hide show
  1. package/README.md +49 -104
  2. package/dist/package-version.d.ts +1 -1
  3. package/dist/package-version.js +1 -1
  4. package/dist/prompts.d.ts +1 -1
  5. package/dist/resources.d.ts +1 -1
  6. package/dist/resources.js +2 -2
  7. package/dist/server-validation.d.ts +1 -0
  8. package/dist/server-validation.js +8 -0
  9. package/dist/server.js +18 -2
  10. package/dist/tools/doctor.d.ts +12 -1
  11. package/dist/tools/doctor.js +37 -6
  12. package/dist/tools/eval.js +3 -2
  13. package/dist/tools/get-run.d.ts +2 -0
  14. package/dist/tools/get-run.js +2 -1
  15. package/dist/tools/get-verification-results.d.ts +2 -0
  16. package/dist/tools/get-verification-results.js +2 -1
  17. package/dist/tools/pr-tools.js +2 -1
  18. package/dist/tools/preflight.d.ts +14 -1
  19. package/dist/tools/preflight.js +36 -5
  20. package/dist/tools/run-dossier.d.ts +2 -0
  21. package/dist/tools/run-dossier.js +4 -2
  22. package/dist/tools/run-loop.d.ts +3 -2
  23. package/dist/tools/run-loop.js +48 -28
  24. package/dist/tools/tool-errors.js +1 -1
  25. package/dist/tools/tool-support.d.ts +6 -3
  26. package/dist/tools/tool-support.js +12 -5
  27. package/dist/vendor/adapters/claude-cli.d.ts +25 -0
  28. package/dist/vendor/adapters/claude-cli.js +279 -19
  29. package/dist/vendor/adapters/cli-bridge.d.ts +1 -0
  30. package/dist/vendor/adapters/cli-bridge.js +44 -3
  31. package/dist/vendor/adapters/codex-launcher.d.ts +44 -0
  32. package/dist/vendor/adapters/codex-launcher.js +247 -0
  33. package/dist/vendor/adapters/index.d.ts +3 -2
  34. package/dist/vendor/adapters/index.js +3 -2
  35. package/dist/vendor/adapters/openai-compatible.d.ts +19 -4
  36. package/dist/vendor/adapters/openai-compatible.js +44 -19
  37. package/dist/vendor/adapters/runtime-support.d.ts +3 -0
  38. package/dist/vendor/adapters/runtime-support.js +8 -1
  39. package/dist/vendor/adapters/verifier-only.js +4 -3
  40. package/dist/vendor/contracts/index.d.ts +39 -0
  41. package/dist/vendor/contracts/index.js +2 -0
  42. package/dist/vendor/core/index.d.ts +23 -3
  43. package/dist/vendor/core/index.js +88 -15
  44. package/dist/vendor/core/persistence/index.d.ts +2 -0
  45. package/dist/vendor/core/persistence/index.js +1 -0
  46. package/dist/vendor/core/persistence/integrity.d.ts +38 -0
  47. package/dist/vendor/core/persistence/integrity.js +239 -0
  48. package/dist/vendor/core/persistence/store.d.ts +7 -0
  49. package/dist/vendor/core/persistence/store.js +25 -1
  50. package/dist/vendor/core/policy.d.ts +9 -0
  51. package/package.json +1 -1
  52. package/server.json +2 -2
@@ -31,7 +31,14 @@ const MODEL_PRICING = {
31
31
  // Keep legacy names working
32
32
  "claude-opus": { inputPer1K: 0.015, outputPer1K: 0.075 },
33
33
  "claude-sonnet": { inputPer1K: 0.003, outputPer1K: 0.015 },
34
- "claude-haiku": { inputPer1K: 0.00025, outputPer1K: 0.00125 }
34
+ "claude-haiku": { inputPer1K: 0.00025, outputPer1K: 0.00125 },
35
+ // OpenAI coding models
36
+ "codex": { inputPer1K: 0.00125, cachedInputPer1K: 0.000125, outputPer1K: 0.01 },
37
+ "gpt-5-codex": { inputPer1K: 0.00125, cachedInputPer1K: 0.000125, outputPer1K: 0.01 },
38
+ "gpt-5.1-codex": { inputPer1K: 0.00125, cachedInputPer1K: 0.000125, outputPer1K: 0.01 },
39
+ "gpt-5.1-codex-max": { inputPer1K: 0.00125, cachedInputPer1K: 0.000125, outputPer1K: 0.01 },
40
+ "gpt-5.2-codex": { inputPer1K: 0.00175, cachedInputPer1K: 0.000175, outputPer1K: 0.014 },
41
+ "codex-mini-latest": { inputPer1K: 0.0015, cachedInputPer1K: 0.000375, outputPer1K: 0.006 }
35
42
  };
36
43
  function extractUsage(parsed, modelLabel) {
37
44
  if (!parsed?.usage) {
@@ -42,21 +49,189 @@ function extractUsage(parsed, modelLabel) {
42
49
  provenance: "unavailable"
43
50
  });
44
51
  }
45
- const tokensIn = (parsed.usage.inputTokens ?? parsed.usage.input_tokens ?? 0) +
46
- (parsed.usage.cacheReadInputTokens ?? parsed.usage.cache_read_input_tokens ?? 0) +
52
+ const promptTokens = (parsed.usage.inputTokens ?? parsed.usage.input_tokens ?? 0) +
47
53
  (parsed.usage.cacheCreationInputTokens ?? parsed.usage.cache_creation_input_tokens ?? 0);
54
+ const cachedInputTokens = parsed.usage.cacheReadInputTokens ?? parsed.usage.cache_read_input_tokens ?? 0;
55
+ const tokensIn = promptTokens + cachedInputTokens;
48
56
  const tokensOut = parsed.usage.outputTokens ?? parsed.usage.output_tokens ?? 0;
49
57
  const pricing = (modelLabel ? MODEL_PRICING[modelLabel] : undefined) ??
50
58
  { inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
51
- const actualUsd = (tokensIn / 1000) * pricing.inputPer1K +
59
+ const actualUsd = (promptTokens / 1000) * pricing.inputPer1K +
60
+ (cachedInputTokens / 1000) * (pricing.cachedInputPer1K ?? pricing.inputPer1K) +
52
61
  (tokensOut / 1000) * pricing.outputPer1K;
53
62
  return normalizeUsage({
54
63
  actualUsd: Number(actualUsd.toFixed(6)),
55
64
  tokensIn,
56
65
  tokensOut,
57
- provenance: "actual"
66
+ cachedInputTokens,
67
+ provenance: "actual",
68
+ providerSettlement: {
69
+ providerId: "claude",
70
+ model: modelLabel ?? "claude",
71
+ transport: "cli",
72
+ source: "claude_json",
73
+ inputTokens: promptTokens,
74
+ cachedInputTokens,
75
+ outputTokens: tokensOut,
76
+ rawUsageAvailable: true,
77
+ settledAt: new Date().toISOString()
78
+ }
58
79
  });
59
80
  }
81
+ function extractCodexJsonlResult(stdout, modelLabel) {
82
+ const events = stdout
83
+ .split(/\r?\n/u)
84
+ .map((line) => line.trim())
85
+ .filter(Boolean)
86
+ .map((line) => {
87
+ try {
88
+ return JSON.parse(line);
89
+ }
90
+ catch {
91
+ return undefined;
92
+ }
93
+ })
94
+ .filter((event) => event !== undefined);
95
+ if (events.length === 0) {
96
+ return undefined;
97
+ }
98
+ const latestAgentMessage = [...events]
99
+ .reverse()
100
+ .find((event) => event.type === "item.completed" && event.item?.type === "agent_message");
101
+ const latestTurnCompleted = [...events]
102
+ .reverse()
103
+ .find((event) => event.type === "turn.completed" && event.usage !== undefined);
104
+ const summary = typeof latestAgentMessage?.item?.text === "string" && latestAgentMessage.item.text.trim().length > 0
105
+ ? latestAgentMessage.item.text.trim()
106
+ : stdout.trim();
107
+ if (!latestTurnCompleted?.usage) {
108
+ return {
109
+ summary,
110
+ usage: normalizeUsage({
111
+ actualUsd: 0,
112
+ tokensIn: 0,
113
+ tokensOut: 0,
114
+ provenance: "unavailable",
115
+ providerSettlement: {
116
+ providerId: "codex",
117
+ model: modelLabel ?? "codex",
118
+ transport: "cli",
119
+ source: "unavailable",
120
+ inputTokens: 0,
121
+ outputTokens: 0,
122
+ rawUsageAvailable: false,
123
+ settledAt: new Date().toISOString()
124
+ }
125
+ })
126
+ };
127
+ }
128
+ const promptTokens = latestTurnCompleted.usage.input_tokens ?? 0;
129
+ const cachedInputTokens = latestTurnCompleted.usage.cached_input_tokens ?? 0;
130
+ const outputTokens = latestTurnCompleted.usage.output_tokens ?? 0;
131
+ const reasoningOutputTokens = latestTurnCompleted.usage.reasoning_output_tokens ?? 0;
132
+ const tokensIn = promptTokens + cachedInputTokens;
133
+ const tokensOut = outputTokens + reasoningOutputTokens;
134
+ const pricing = (modelLabel ? MODEL_PRICING[modelLabel] : undefined) ??
135
+ MODEL_PRICING["codex"] ??
136
+ { inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
137
+ const actualUsd = (promptTokens / 1000) * pricing.inputPer1K +
138
+ (cachedInputTokens / 1000) * (pricing.cachedInputPer1K ?? pricing.inputPer1K) +
139
+ (tokensOut / 1000) * pricing.outputPer1K;
140
+ return {
141
+ summary,
142
+ usage: normalizeUsage({
143
+ actualUsd: Number(actualUsd.toFixed(6)),
144
+ tokensIn,
145
+ tokensOut,
146
+ cachedInputTokens,
147
+ reasoningTokensOut: reasoningOutputTokens,
148
+ provenance: "actual",
149
+ providerSettlement: {
150
+ providerId: "codex",
151
+ model: modelLabel ?? "codex",
152
+ transport: "cli",
153
+ source: "codex_jsonl",
154
+ inputTokens: promptTokens,
155
+ cachedInputTokens,
156
+ outputTokens,
157
+ reasoningOutputTokens,
158
+ rawUsageAvailable: true,
159
+ settledAt: new Date().toISOString()
160
+ }
161
+ })
162
+ };
163
+ }
164
+ function extractGeminiJsonResult(stdout, modelLabel) {
165
+ let parsed;
166
+ try {
167
+ parsed = JSON.parse(stdout);
168
+ }
169
+ catch {
170
+ return undefined;
171
+ }
172
+ const summary = typeof parsed.response === "string" && parsed.response.trim().length > 0
173
+ ? parsed.response.trim()
174
+ : typeof parsed.error?.message === "string" && parsed.error.message.trim().length > 0
175
+ ? parsed.error.message.trim()
176
+ : stdout.trim();
177
+ const promptTokens = parsed.stats?.inputTokens ?? 0;
178
+ const cachedInputTokens = parsed.stats?.cachedReadTokens ?? 0;
179
+ const outputTokens = parsed.stats?.outputTokens ?? 0;
180
+ const reasoningOutputTokens = parsed.stats?.thoughtTokens ?? 0;
181
+ const hasUsage = parsed.stats !== undefined &&
182
+ (promptTokens > 0 || cachedInputTokens > 0 || outputTokens > 0 || reasoningOutputTokens > 0);
183
+ if (!hasUsage) {
184
+ return {
185
+ summary,
186
+ usage: normalizeUsage({
187
+ actualUsd: 0,
188
+ tokensIn: 0,
189
+ tokensOut: 0,
190
+ provenance: "unavailable",
191
+ providerSettlement: {
192
+ providerId: "gemini",
193
+ model: modelLabel ?? "flash",
194
+ transport: "cli",
195
+ source: "unavailable",
196
+ inputTokens: 0,
197
+ outputTokens: 0,
198
+ rawUsageAvailable: false,
199
+ settledAt: new Date().toISOString()
200
+ }
201
+ })
202
+ };
203
+ }
204
+ const tokensIn = promptTokens + cachedInputTokens;
205
+ const tokensOut = outputTokens + reasoningOutputTokens;
206
+ const pricing = (modelLabel ? MODEL_PRICING[modelLabel] : undefined) ??
207
+ { inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
208
+ const actualUsd = (promptTokens / 1000) * pricing.inputPer1K +
209
+ (cachedInputTokens / 1000) * (pricing.cachedInputPer1K ?? pricing.inputPer1K) +
210
+ (tokensOut / 1000) * pricing.outputPer1K;
211
+ return {
212
+ summary,
213
+ usage: normalizeUsage({
214
+ actualUsd: Number(actualUsd.toFixed(6)),
215
+ tokensIn,
216
+ tokensOut,
217
+ cachedInputTokens,
218
+ reasoningTokensOut: reasoningOutputTokens,
219
+ provenance: "actual",
220
+ providerSettlement: {
221
+ providerId: "gemini",
222
+ model: modelLabel ?? "flash",
223
+ transport: "cli",
224
+ source: "gemini_json",
225
+ inputTokens: promptTokens,
226
+ cachedInputTokens,
227
+ outputTokens,
228
+ reasoningOutputTokens,
229
+ rawUsageAvailable: true,
230
+ settledAt: new Date().toISOString()
231
+ }
232
+ })
233
+ };
234
+ }
60
235
  // ---------------------------------------------------------------------------
61
236
  // Structural failure hint detection
62
237
  //
@@ -90,6 +265,7 @@ export function createAgentCliAdapter(options) {
90
265
  const verifyTimeoutMs = options.verifyTimeoutMs ?? 60_000;
91
266
  const adapterId = `agent-cli:${options.adapterIdSuffix ?? options.command}`;
92
267
  const supportsJsonOutput = options.supportsJsonOutput === true;
268
+ const supportsUsageSettlement = supportsJsonOutput || options.command === "codex" || options.command === "gemini";
93
269
  const adapter = {
94
270
  adapterId,
95
271
  kind: "agent-cli",
@@ -100,10 +276,10 @@ export function createAgentCliAdapter(options) {
100
276
  transport: "cli",
101
277
  capabilities: createAdapterCapabilities({
102
278
  preflight: true,
103
- usageSettlement: supportsJsonOutput,
279
+ usageSettlement: supportsUsageSettlement,
104
280
  diffArtifacts: true,
105
281
  structuredErrors: true,
106
- cachingSignals: supportsJsonOutput
282
+ cachingSignals: supportsUsageSettlement
107
283
  })
108
284
  },
109
285
  async execute(request) {
@@ -180,24 +356,58 @@ export function createAgentCliAdapter(options) {
180
356
  // Fall through to plain-text handling
181
357
  }
182
358
  }
183
- const agentText = parsed?.result ?? agentResult.stdout.trim();
359
+ const codexJsonlResult = !supportsJsonOutput && options.command === "codex"
360
+ ? extractCodexJsonlResult(agentResult.stdout, options.model)
361
+ : undefined;
362
+ const geminiJsonResult = !supportsJsonOutput && options.command === "gemini"
363
+ ? extractGeminiJsonResult(agentResult.stdout, options.model)
364
+ : undefined;
365
+ const agentText = codexJsonlResult?.summary ??
366
+ geminiJsonResult?.summary ??
367
+ parsed?.result ??
368
+ agentResult.stdout.trim();
184
369
  const summary = truncate(agentText, 2000);
185
370
  const usage = parsed?.usage
186
371
  ? extractUsage(parsed, options.model)
187
- : normalizeUsage({
188
- actualUsd: estimatedUsage.actualUsd,
189
- estimatedUsd: estimatedUsage.actualUsd,
190
- tokensIn: estimatedUsage.tokensIn,
191
- tokensOut: Math.max(estimatedUsage.tokensOut, Math.ceil(agentText.length / 4)),
192
- provenance: "estimated"
193
- });
372
+ : codexJsonlResult?.usage ??
373
+ geminiJsonResult?.usage ??
374
+ normalizeUsage({
375
+ actualUsd: estimatedUsage.actualUsd,
376
+ estimatedUsd: estimatedUsage.actualUsd,
377
+ tokensIn: estimatedUsage.tokensIn,
378
+ tokensOut: Math.max(estimatedUsage.tokensOut, Math.ceil(agentText.length / 4)),
379
+ provenance: "estimated",
380
+ providerSettlement: options.command === "codex"
381
+ ? {
382
+ providerId: "codex",
383
+ model: options.model ?? "codex",
384
+ transport: "cli",
385
+ source: "estimated_fallback",
386
+ inputTokens: estimatedUsage.tokensIn,
387
+ outputTokens: Math.max(estimatedUsage.tokensOut, Math.ceil(agentText.length / 4)),
388
+ rawUsageAvailable: false,
389
+ settledAt: new Date().toISOString()
390
+ }
391
+ : options.command === "gemini"
392
+ ? {
393
+ providerId: "gemini",
394
+ model: options.model ?? "flash",
395
+ transport: "cli",
396
+ source: "estimated_fallback",
397
+ inputTokens: estimatedUsage.tokensIn,
398
+ outputTokens: Math.max(estimatedUsage.tokensOut, Math.ceil(agentText.length / 4)),
399
+ rawUsageAvailable: false,
400
+ settledAt: new Date().toISOString()
401
+ }
402
+ : undefined
403
+ });
194
404
  const verificationStack = request.context.verificationStack;
195
405
  const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, verificationStack, options.spawnImpl);
196
406
  // Check for zero-diff (agent ran but made no file changes)
197
407
  const repoRoot = request.context.repoRoot;
198
408
  let noDiff = false;
199
409
  if (repoRoot) {
200
- noDiff = await checkNoDiff(repoRoot);
410
+ noDiff = await checkNoDiff(repoRoot, options.spawnImpl);
201
411
  }
202
412
  // Extract structured errors from stderr/stdout for better failure context
203
413
  const structuredErrors = normalizeStructuredErrors(extractStructuredErrors(agentResult.stderr, agentResult.stdout));
@@ -208,7 +418,11 @@ export function createAgentCliAdapter(options) {
208
418
  let scopeViolations = [];
209
419
  const scopeCtx = request.context;
210
420
  if (repoRoot && (scopeCtx.allowedPaths?.length || scopeCtx.deniedPaths?.length)) {
211
- const diffResult = await runSubprocess("git", ["diff", "--name-only", "HEAD"], { cwd: repoRoot, timeoutMs: 5000 });
421
+ const diffResult = await runSubprocess("git", ["diff", "--name-only", "HEAD"], {
422
+ cwd: repoRoot,
423
+ timeoutMs: 5000,
424
+ spawnImpl: options.spawnImpl
425
+ });
212
426
  if (diffResult.exitCode === 0 && diffResult.stdout.trim()) {
213
427
  const touchedFiles = diffResult.stdout.trim().split("\n").filter(Boolean);
214
428
  const allowed = scopeCtx.allowedPaths ?? [];
@@ -392,6 +606,7 @@ export function createCodexCliAdapter(options = {}) {
392
606
  workingDirectory,
393
607
  "--sandbox",
394
608
  sandbox,
609
+ "--json",
395
610
  "--color",
396
611
  "never",
397
612
  ...modelArgs,
@@ -402,6 +617,47 @@ export function createCodexCliAdapter(options = {}) {
402
617
  });
403
618
  }
404
619
  // ---------------------------------------------------------------------------
620
+ // Pre-configured: Gemini CLI
621
+ // ---------------------------------------------------------------------------
622
+ /**
623
+ * Spawns `gemini --model <model> --prompt "" --approval-mode <mode> --output-format json [...]`.
624
+ *
625
+ * The prompt is delivered via stdin while forcing headless mode with `--prompt ""`,
626
+ * which keeps large MartinLoop prompts off the command line on Windows.
627
+ *
628
+ * Requires the Gemini CLI to be installed and authenticated:
629
+ * npm install -g @google/gemini-cli
630
+ */
631
+ export function createGeminiCliAdapter(options = {}) {
632
+ const model = options.model ?? "flash";
633
+ const approvalMode = options.approvalMode ?? "yolo";
634
+ const extraArgs = options.extraArgs ?? [];
635
+ return createAgentCliAdapter({
636
+ command: "gemini",
637
+ adapterIdSuffix: "gemini",
638
+ model,
639
+ label: options.label ?? "Gemini CLI adapter",
640
+ workingDirectory: options.workingDirectory,
641
+ timeoutMs: options.timeoutMs,
642
+ verifyTimeoutMs: options.verifyTimeoutMs,
643
+ supportsJsonOutput: false,
644
+ spawnImpl: options.spawnImpl,
645
+ argsBuilder: () => [
646
+ "--model",
647
+ model,
648
+ "--prompt",
649
+ "",
650
+ "--approval-mode",
651
+ approvalMode,
652
+ ...(options.sandbox ? ["--sandbox"] : []),
653
+ "--output-format",
654
+ "json",
655
+ ...extraArgs
656
+ ],
657
+ stdinBuilder: (prompt) => prompt
658
+ });
659
+ }
660
+ // ---------------------------------------------------------------------------
405
661
  // Prompt builder
406
662
  //
407
663
  // Implements Qralph-style context isolation:
@@ -579,7 +835,11 @@ function extractStructuredErrors(stderr, stdout) {
579
835
  }
580
836
  return errors.slice(0, 10); // cap at 10 to avoid bloating prompts
581
837
  }
582
- async function checkNoDiff(repoRoot) {
583
- const result = await runSubprocess("git", ["diff", "--name-only", "HEAD"], { cwd: repoRoot, timeoutMs: 5000 });
838
+ async function checkNoDiff(repoRoot, spawnImpl) {
839
+ const result = await runSubprocess("git", ["diff", "--name-only", "HEAD"], {
840
+ cwd: repoRoot,
841
+ timeoutMs: 5000,
842
+ spawnImpl
843
+ });
584
844
  return result.exitCode === 0 && result.stdout.trim().length === 0;
585
845
  }
@@ -26,6 +26,7 @@ export declare function readGitExecutionArtifacts(repoRoot: string, timeoutMs: n
26
26
  changedFiles?: string[];
27
27
  diffStats?: ReturnType<typeof diffStatsFromNumstat>;
28
28
  }>;
29
+ export declare function readGitChangedFiles(repoRoot: string, timeoutMs: number, spawnImpl?: SpawnLike): Promise<string[]>;
29
30
  export interface SpawnPlan {
30
31
  command: string;
31
32
  args: string[];
@@ -140,6 +140,13 @@ export async function readGitExecutionArtifacts(repoRoot, timeoutMs, spawnImpl)
140
140
  ...(diffStats ? { diffStats } : {})
141
141
  };
142
142
  }
143
+ export async function readGitChangedFiles(repoRoot, timeoutMs, spawnImpl) {
144
+ const statusResult = await runSubprocess("git", ["status", "-z", "--porcelain=v1", "--untracked-files=all", "--ignore-submodules=all"], { cwd: repoRoot, timeoutMs, spawnImpl });
145
+ if (statusResult.exitCode !== 0) {
146
+ return [];
147
+ }
148
+ return parsePorcelainEntries(statusResult.stdout).filter((entry) => typeof entry === "string" && entry.length > 0);
149
+ }
143
150
  export function createSpawnPlan(command, args, cwd, preserveRawForInjectedSpawn) {
144
151
  if (preserveRawForInjectedSpawn || process.platform !== "win32") {
145
152
  return { command, args };
@@ -153,14 +160,21 @@ export function createSpawnPlan(command, args, cwd, preserveRawForInjectedSpawn)
153
160
  const cmdStr = [quoteWindowsCmdArg(command), ...args.map(quoteWindowsCmdArg)].join(" ");
154
161
  return {
155
162
  command: process.env.ComSpec || "cmd.exe",
156
- args: ["/d", "/s", "/c", cmdStr]
163
+ args: ["/d", "/c", cmdStr]
157
164
  };
158
165
  }
159
166
  const extension = extname(resolvedOrUndefined).toLowerCase();
160
167
  if (extension === ".cmd" || extension === ".bat") {
168
+ const cmdStr = [quoteWindowsCmdArg(resolvedOrUndefined), ...args.map(quoteWindowsCmdArg)].join(" ");
161
169
  return {
162
170
  command: process.env.ComSpec || "cmd.exe",
163
- args: ["/d", "/s", "/c", [quoteWindowsCmdArg(resolvedOrUndefined), ...args.map(quoteWindowsCmdArg)].join(" ")]
171
+ args: ["/d", "/s", "/c", cmdStr]
172
+ };
173
+ }
174
+ if (extension === ".ps1") {
175
+ return {
176
+ command: "powershell.exe",
177
+ args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", resolvedOrUndefined, ...args]
164
178
  };
165
179
  }
166
180
  return { command: resolvedOrUndefined, args };
@@ -186,11 +200,38 @@ function expandWindowsCommandCandidates(command) {
186
200
  return [command];
187
201
  }
188
202
  const pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
189
- return pathExt
203
+ const fromPathExt = pathExt
190
204
  .split(";")
191
205
  .map((extension) => extension.trim())
192
206
  .filter(Boolean)
193
207
  .map((extension) => `${command}${extension.toLowerCase()}`);
208
+ const candidates = [...fromPathExt, `${command}.ps1`];
209
+ return Array.from(new Set(candidates));
210
+ }
211
+ function parsePorcelainEntries(stdout) {
212
+ const entries = stdout.split("\u0000").filter((entry) => entry.length > 0);
213
+ const changedFiles = [];
214
+ for (let index = 0; index < entries.length; index += 1) {
215
+ const entry = entries[index];
216
+ if (entry === undefined || entry.length < 4) {
217
+ continue;
218
+ }
219
+ const status = entry.slice(0, 2);
220
+ const payload = entry.slice(3);
221
+ if (!payload) {
222
+ continue;
223
+ }
224
+ if (status.includes("R") || status.includes("C")) {
225
+ const renamedPath = entries[index + 1];
226
+ if (renamedPath && renamedPath.length > 0) {
227
+ changedFiles.push(renamedPath);
228
+ index += 1;
229
+ continue;
230
+ }
231
+ }
232
+ changedFiles.push(payload);
233
+ }
234
+ return changedFiles;
194
235
  }
195
236
  function windowsPathDirectories() {
196
237
  const rawPath = process.env.Path ?? process.env.PATH ?? "";
@@ -0,0 +1,44 @@
1
+ import { spawnSync } from "node:child_process";
2
+ export interface CliCommandAvailability {
3
+ command: string;
4
+ available: boolean;
5
+ locator: string;
6
+ detail: string;
7
+ resolvedPath?: string;
8
+ }
9
+ export type CodexHostPlatform = "windows" | "linux" | "wsl" | "macos";
10
+ export interface CodexHostDiagnosis {
11
+ hostPlatform: CodexHostPlatform;
12
+ nativeInstallValid: boolean;
13
+ warnings: string[];
14
+ remediation?: string;
15
+ }
16
+ export interface CodexLaunchProbeResult {
17
+ ok: boolean;
18
+ summary: string;
19
+ availability: CliCommandAvailability;
20
+ diagnosis: CodexHostDiagnosis;
21
+ command: string;
22
+ args: string[];
23
+ exitCode?: number;
24
+ stdout?: string;
25
+ stderr?: string;
26
+ }
27
+ type SpawnSyncLike = typeof spawnSync;
28
+ export declare function resolveCliCommandAvailability(command: string, options?: {
29
+ platform?: NodeJS.Platform;
30
+ spawnSyncImpl?: SpawnSyncLike;
31
+ }): CliCommandAvailability;
32
+ export declare function detectCodexHostPlatform(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): CodexHostPlatform;
33
+ export declare function diagnoseCodexHost(availability: CliCommandAvailability, options?: {
34
+ env?: NodeJS.ProcessEnv;
35
+ platform?: NodeJS.Platform;
36
+ }): CodexHostDiagnosis;
37
+ export declare function probeCodexLaunch(input: {
38
+ workingDirectory: string;
39
+ availability?: CliCommandAvailability;
40
+ env?: NodeJS.ProcessEnv;
41
+ platform?: NodeJS.Platform;
42
+ spawnSyncImpl?: SpawnSyncLike;
43
+ }): CodexLaunchProbeResult;
44
+ export {};