@martinloop/mcp 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -132
- package/dist/discovery-metadata.d.ts +10 -5
- package/dist/discovery-metadata.js +95 -5
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +93 -1
- package/dist/resources.d.ts +9 -1
- package/dist/resources.js +247 -16
- package/dist/server-validation.d.ts +2 -1
- package/dist/server-validation.js +124 -0
- package/dist/server.js +379 -5
- package/dist/tools/doctor.d.ts +14 -1
- package/dist/tools/doctor.js +43 -8
- package/dist/tools/eval.d.ts +24 -0
- package/dist/tools/eval.js +66 -0
- package/dist/tools/get-run.d.ts +2 -0
- package/dist/tools/get-run.js +2 -1
- package/dist/tools/get-status.d.ts +8 -0
- package/dist/tools/get-status.js +18 -0
- package/dist/tools/get-verification-results.d.ts +2 -0
- package/dist/tools/get-verification-results.js +2 -1
- package/dist/tools/logs.d.ts +25 -0
- package/dist/tools/logs.js +49 -0
- package/dist/tools/plan.d.ts +20 -0
- package/dist/tools/plan.js +10 -0
- package/dist/tools/pr-tools.d.ts +31 -0
- package/dist/tools/pr-tools.js +112 -0
- package/dist/tools/preflight.d.ts +24 -1
- package/dist/tools/preflight.js +47 -7
- package/dist/tools/run-controls.d.ts +36 -0
- package/dist/tools/run-controls.js +88 -0
- package/dist/tools/run-dossier.d.ts +16 -0
- package/dist/tools/run-dossier.js +64 -2
- package/dist/tools/run-loop.d.ts +3 -2
- package/dist/tools/run-loop.js +52 -13
- package/dist/tools/tool-errors.d.ts +1 -1
- package/dist/tools/tool-errors.js +1 -1
- package/dist/tools/tool-support.d.ts +6 -3
- package/dist/tools/tool-support.js +37 -3
- package/dist/tools/workflow-governance.d.ts +133 -0
- package/dist/tools/workflow-governance.js +581 -0
- package/dist/vendor/adapters/claude-cli.d.ts +25 -0
- package/dist/vendor/adapters/claude-cli.js +279 -19
- package/dist/vendor/adapters/cli-bridge.d.ts +6 -0
- package/dist/vendor/adapters/cli-bridge.js +58 -9
- package/dist/vendor/adapters/codex-launcher.d.ts +44 -0
- package/dist/vendor/adapters/codex-launcher.js +247 -0
- package/dist/vendor/adapters/index.d.ts +4 -2
- package/dist/vendor/adapters/index.js +4 -1
- package/dist/vendor/adapters/openai-compatible.d.ts +62 -0
- package/dist/vendor/adapters/openai-compatible.js +267 -0
- package/dist/vendor/adapters/runtime-support.d.ts +3 -0
- package/dist/vendor/adapters/runtime-support.js +8 -1
- package/dist/vendor/adapters/verifier-only.js +4 -3
- package/dist/vendor/contracts/index.d.ts +39 -0
- package/dist/vendor/contracts/index.js +2 -0
- package/dist/vendor/core/index.d.ts +23 -3
- package/dist/vendor/core/index.js +88 -15
- package/dist/vendor/core/persistence/index.d.ts +2 -0
- package/dist/vendor/core/persistence/index.js +1 -0
- package/dist/vendor/core/persistence/integrity.d.ts +38 -0
- package/dist/vendor/core/persistence/integrity.js +239 -0
- package/dist/vendor/core/persistence/store.d.ts +7 -0
- package/dist/vendor/core/persistence/store.js +25 -1
- package/dist/vendor/core/policy.d.ts +9 -0
- package/dist/workflow-state.d.ts +25 -0
- package/dist/workflow-state.js +102 -0
- package/package.json +3 -3
- 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
|
|
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 = (
|
|
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
|
-
|
|
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:
|
|
279
|
+
usageSettlement: supportsUsageSettlement,
|
|
104
280
|
diffArtifacts: true,
|
|
105
281
|
structuredErrors: true,
|
|
106
|
-
cachingSignals:
|
|
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
|
|
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
|
-
:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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"], {
|
|
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"], {
|
|
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,4 +26,10 @@ 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[]>;
|
|
30
|
+
export interface SpawnPlan {
|
|
31
|
+
command: string;
|
|
32
|
+
args: string[];
|
|
33
|
+
}
|
|
34
|
+
export declare function createSpawnPlan(command: string, args: string[], cwd: string, preserveRawForInjectedSpawn: boolean): SpawnPlan;
|
|
29
35
|
export declare function splitCommand(command: string): string[];
|
|
@@ -140,22 +140,44 @@ export async function readGitExecutionArtifacts(repoRoot, timeoutMs, spawnImpl)
|
|
|
140
140
|
...(diffStats ? { diffStats } : {})
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
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 [];
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
return parsePorcelainEntries(statusResult.stdout).filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
149
|
+
}
|
|
150
|
+
export function createSpawnPlan(command, args, cwd, preserveRawForInjectedSpawn) {
|
|
151
|
+
if (preserveRawForInjectedSpawn || process.platform !== "win32") {
|
|
149
152
|
return { command, args };
|
|
150
153
|
}
|
|
151
|
-
|
|
154
|
+
// Try to resolve the command to an absolute path using the Windows PATH.
|
|
155
|
+
const resolvedOrUndefined = isAbsolute(command) ? command : resolveWindowsCommand(command, cwd);
|
|
156
|
+
// If resolution failed (command not found in PATH), fall back to cmd.exe shell execution so
|
|
157
|
+
// Windows can resolve the command itself — this covers cases like `pnpm` where the npm global
|
|
158
|
+
// bin directory is present in the shell PATH but not yet visible to this Node.js process.
|
|
159
|
+
if (resolvedOrUndefined === undefined) {
|
|
160
|
+
const cmdStr = [quoteWindowsCmdArg(command), ...args.map(quoteWindowsCmdArg)].join(" ");
|
|
161
|
+
return {
|
|
162
|
+
command: process.env.ComSpec || "cmd.exe",
|
|
163
|
+
args: ["/d", "/c", cmdStr]
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const extension = extname(resolvedOrUndefined).toLowerCase();
|
|
152
167
|
if (extension === ".cmd" || extension === ".bat") {
|
|
168
|
+
const cmdStr = [quoteWindowsCmdArg(resolvedOrUndefined), ...args.map(quoteWindowsCmdArg)].join(" ");
|
|
153
169
|
return {
|
|
154
170
|
command: process.env.ComSpec || "cmd.exe",
|
|
155
|
-
args: ["/d", "/s", "/c",
|
|
171
|
+
args: ["/d", "/s", "/c", cmdStr]
|
|
156
172
|
};
|
|
157
173
|
}
|
|
158
|
-
|
|
174
|
+
if (extension === ".ps1") {
|
|
175
|
+
return {
|
|
176
|
+
command: "powershell.exe",
|
|
177
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", resolvedOrUndefined, ...args]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { command: resolvedOrUndefined, args };
|
|
159
181
|
}
|
|
160
182
|
function resolveWindowsCommand(command, cwd) {
|
|
161
183
|
const hasPathSegment = command.includes("\\") || command.includes("/");
|
|
@@ -178,11 +200,38 @@ function expandWindowsCommandCandidates(command) {
|
|
|
178
200
|
return [command];
|
|
179
201
|
}
|
|
180
202
|
const pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
181
|
-
|
|
203
|
+
const fromPathExt = pathExt
|
|
182
204
|
.split(";")
|
|
183
205
|
.map((extension) => extension.trim())
|
|
184
206
|
.filter(Boolean)
|
|
185
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;
|
|
186
235
|
}
|
|
187
236
|
function windowsPathDirectories() {
|
|
188
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 {};
|