@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.
- package/README.md +49 -104
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/prompts.d.ts +1 -1
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +2 -2
- package/dist/server-validation.d.ts +1 -0
- package/dist/server-validation.js +8 -0
- package/dist/server.js +18 -2
- package/dist/tools/doctor.d.ts +12 -1
- package/dist/tools/doctor.js +37 -6
- package/dist/tools/eval.js +3 -2
- package/dist/tools/get-run.d.ts +2 -0
- package/dist/tools/get-run.js +2 -1
- package/dist/tools/get-verification-results.d.ts +2 -0
- package/dist/tools/get-verification-results.js +2 -1
- package/dist/tools/pr-tools.js +2 -1
- package/dist/tools/preflight.d.ts +14 -1
- package/dist/tools/preflight.js +36 -5
- package/dist/tools/run-dossier.d.ts +2 -0
- package/dist/tools/run-dossier.js +4 -2
- package/dist/tools/run-loop.d.ts +3 -2
- package/dist/tools/run-loop.js +48 -28
- package/dist/tools/tool-errors.js +1 -1
- package/dist/tools/tool-support.d.ts +6 -3
- package/dist/tools/tool-support.js +12 -5
- 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 +1 -0
- package/dist/vendor/adapters/cli-bridge.js +44 -3
- 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 +3 -2
- package/dist/vendor/adapters/index.js +3 -2
- package/dist/vendor/adapters/openai-compatible.d.ts +19 -4
- package/dist/vendor/adapters/openai-compatible.js +44 -19
- 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/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, extname, resolve } from "node:path";
|
|
4
|
+
function isInsideGitRepository(workingDirectory) {
|
|
5
|
+
let current = resolve(workingDirectory);
|
|
6
|
+
while (true) {
|
|
7
|
+
if (existsSync(resolve(current, ".git"))) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
const parent = dirname(current);
|
|
11
|
+
if (parent === current) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
current = parent;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function selectResolvedPath(candidates, platform) {
|
|
18
|
+
const cleaned = candidates
|
|
19
|
+
.map((line) => line.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
if (cleaned.length === 0) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
if (platform !== "win32") {
|
|
25
|
+
return cleaned[0];
|
|
26
|
+
}
|
|
27
|
+
const preference = (candidate) => {
|
|
28
|
+
switch (extname(candidate).toLowerCase()) {
|
|
29
|
+
case ".cmd":
|
|
30
|
+
return 0;
|
|
31
|
+
case ".bat":
|
|
32
|
+
return 1;
|
|
33
|
+
case ".ps1":
|
|
34
|
+
return 2;
|
|
35
|
+
case ".exe":
|
|
36
|
+
return 3;
|
|
37
|
+
case "":
|
|
38
|
+
return 4;
|
|
39
|
+
default:
|
|
40
|
+
return 5;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const [firstCandidate, ...remainingCandidates] = cleaned;
|
|
44
|
+
if (firstCandidate === undefined) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
let bestCandidate = firstCandidate;
|
|
48
|
+
let bestPreference = preference(bestCandidate);
|
|
49
|
+
for (const candidate of remainingCandidates) {
|
|
50
|
+
const candidatePreference = preference(candidate);
|
|
51
|
+
if (candidatePreference < bestPreference) {
|
|
52
|
+
bestCandidate = candidate;
|
|
53
|
+
bestPreference = candidatePreference;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return bestCandidate;
|
|
57
|
+
}
|
|
58
|
+
function buildProbeCommand(command, args, platform) {
|
|
59
|
+
if (platform !== "win32") {
|
|
60
|
+
return { command, args };
|
|
61
|
+
}
|
|
62
|
+
const extension = extname(command).toLowerCase();
|
|
63
|
+
switch (extension) {
|
|
64
|
+
case ".cmd":
|
|
65
|
+
case ".bat":
|
|
66
|
+
return {
|
|
67
|
+
command: process.env.ComSpec || "cmd.exe",
|
|
68
|
+
args: ["/d", "/c", command, ...args]
|
|
69
|
+
};
|
|
70
|
+
case ".ps1":
|
|
71
|
+
return {
|
|
72
|
+
command: "powershell.exe",
|
|
73
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command, ...args]
|
|
74
|
+
};
|
|
75
|
+
default:
|
|
76
|
+
return { command, args };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function resolveCliCommandAvailability(command, options = {}) {
|
|
80
|
+
const platform = options.platform ?? process.platform;
|
|
81
|
+
const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
|
|
82
|
+
const locator = platform === "win32" ? "where.exe" : "which";
|
|
83
|
+
const result = spawnSyncImpl(locator, [command], {
|
|
84
|
+
encoding: "utf8",
|
|
85
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
86
|
+
});
|
|
87
|
+
const resolvedPath = result.status === 0
|
|
88
|
+
? selectResolvedPath((result.stdout ?? "").split(/\r?\n/u), platform)
|
|
89
|
+
: undefined;
|
|
90
|
+
return result.status === 0
|
|
91
|
+
? {
|
|
92
|
+
command,
|
|
93
|
+
available: true,
|
|
94
|
+
locator,
|
|
95
|
+
detail: `${command} is available on PATH.`,
|
|
96
|
+
...(resolvedPath ? { resolvedPath } : {})
|
|
97
|
+
}
|
|
98
|
+
: {
|
|
99
|
+
command,
|
|
100
|
+
available: false,
|
|
101
|
+
locator,
|
|
102
|
+
detail: `${command} is not available on PATH.`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function detectCodexHostPlatform(env = process.env, platform = process.platform) {
|
|
106
|
+
if (platform === "win32") {
|
|
107
|
+
return "windows";
|
|
108
|
+
}
|
|
109
|
+
if (platform === "darwin") {
|
|
110
|
+
return "macos";
|
|
111
|
+
}
|
|
112
|
+
if (env["WSL_DISTRO_NAME"] || env["WSL_INTEROP"]) {
|
|
113
|
+
return "wsl";
|
|
114
|
+
}
|
|
115
|
+
return "linux";
|
|
116
|
+
}
|
|
117
|
+
export function diagnoseCodexHost(availability, options = {}) {
|
|
118
|
+
const hostPlatform = detectCodexHostPlatform(options.env ?? process.env, options.platform ?? process.platform);
|
|
119
|
+
const warnings = [];
|
|
120
|
+
if (!availability.available) {
|
|
121
|
+
return {
|
|
122
|
+
hostPlatform,
|
|
123
|
+
nativeInstallValid: false,
|
|
124
|
+
warnings,
|
|
125
|
+
remediation: "Install or expose the Codex CLI on PATH before running governed Codex work."
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const resolvedPath = availability.resolvedPath?.replace(/\\/gu, "/").toLowerCase() ?? "";
|
|
129
|
+
const looksWindowsShim = resolvedPath.endsWith(".cmd") ||
|
|
130
|
+
resolvedPath.endsWith(".bat") ||
|
|
131
|
+
resolvedPath.endsWith(".ps1") ||
|
|
132
|
+
resolvedPath.includes("/appdata/roaming/npm/");
|
|
133
|
+
const looksMountedWindowsPath = resolvedPath.startsWith("/mnt/c/");
|
|
134
|
+
if ((hostPlatform === "linux" || hostPlatform === "wsl") && (looksWindowsShim || looksMountedWindowsPath)) {
|
|
135
|
+
warnings.push("Codex resolves to a Windows-hosted install from a Linux/WSL environment.");
|
|
136
|
+
return {
|
|
137
|
+
hostPlatform,
|
|
138
|
+
nativeInstallValid: false,
|
|
139
|
+
warnings,
|
|
140
|
+
remediation: "Install Codex natively inside this Linux/WSL environment instead of relying on a Windows PATH shim."
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
hostPlatform,
|
|
145
|
+
nativeInstallValid: true,
|
|
146
|
+
warnings
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function probeCodexLaunch(input) {
|
|
150
|
+
const availability = input.availability ??
|
|
151
|
+
resolveCliCommandAvailability("codex", {
|
|
152
|
+
platform: input.platform,
|
|
153
|
+
spawnSyncImpl: input.spawnSyncImpl
|
|
154
|
+
});
|
|
155
|
+
const diagnosis = diagnoseCodexHost(availability, {
|
|
156
|
+
env: input.env,
|
|
157
|
+
platform: input.platform
|
|
158
|
+
});
|
|
159
|
+
const args = [
|
|
160
|
+
"exec",
|
|
161
|
+
"--cd",
|
|
162
|
+
input.workingDirectory,
|
|
163
|
+
"--sandbox",
|
|
164
|
+
"workspace-write",
|
|
165
|
+
"--json",
|
|
166
|
+
"--color",
|
|
167
|
+
"never",
|
|
168
|
+
"--help"
|
|
169
|
+
];
|
|
170
|
+
if (!availability.available) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
summary: availability.detail,
|
|
174
|
+
availability,
|
|
175
|
+
diagnosis,
|
|
176
|
+
command: availability.command,
|
|
177
|
+
args
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (!diagnosis.nativeInstallValid) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
summary: diagnosis.remediation ?? "Codex host installation is not valid for this environment.",
|
|
184
|
+
availability,
|
|
185
|
+
diagnosis,
|
|
186
|
+
command: availability.resolvedPath ?? availability.command,
|
|
187
|
+
args
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (!isInsideGitRepository(input.workingDirectory)) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
summary: "Working directory is not inside a git repository. Codex exec requires a trusted repo unless --skip-git-repo-check is explicitly enabled.",
|
|
194
|
+
availability,
|
|
195
|
+
diagnosis,
|
|
196
|
+
command: availability.resolvedPath ?? availability.command,
|
|
197
|
+
args
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const spawnSyncImpl = input.spawnSyncImpl ?? spawnSync;
|
|
201
|
+
const platform = input.platform ?? process.platform;
|
|
202
|
+
const spawnPlan = buildProbeCommand(availability.resolvedPath ?? availability.command, args, platform);
|
|
203
|
+
const result = spawnSyncImpl(spawnPlan.command, spawnPlan.args, {
|
|
204
|
+
cwd: input.workingDirectory,
|
|
205
|
+
encoding: "utf8",
|
|
206
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
207
|
+
});
|
|
208
|
+
if (result.error) {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
summary: `Codex launch probe failed: ${result.error.message}`,
|
|
212
|
+
availability,
|
|
213
|
+
diagnosis,
|
|
214
|
+
command: availability.resolvedPath ?? availability.command,
|
|
215
|
+
args,
|
|
216
|
+
stderr: result.stderr ?? "",
|
|
217
|
+
stdout: result.stdout ?? ""
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (result.status !== 0) {
|
|
221
|
+
const stderr = (result.stderr ?? "").trim();
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
summary: stderr.length > 0
|
|
225
|
+
? `Codex launch probe failed: ${stderr}`
|
|
226
|
+
: "Codex launch probe exited non-zero.",
|
|
227
|
+
availability,
|
|
228
|
+
diagnosis,
|
|
229
|
+
command: availability.resolvedPath ?? availability.command,
|
|
230
|
+
args,
|
|
231
|
+
exitCode: result.status ?? undefined,
|
|
232
|
+
stderr: result.stderr ?? "",
|
|
233
|
+
stdout: result.stdout ?? ""
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
ok: true,
|
|
238
|
+
summary: "Codex exec launch probe passed for the current MartinLoop invocation shape.",
|
|
239
|
+
availability,
|
|
240
|
+
diagnosis,
|
|
241
|
+
command: availability.resolvedPath ?? availability.command,
|
|
242
|
+
args,
|
|
243
|
+
exitCode: result.status ?? undefined,
|
|
244
|
+
stderr: result.stderr ?? "",
|
|
245
|
+
stdout: result.stdout ?? ""
|
|
246
|
+
};
|
|
247
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { createDirectProviderAdapter, type DirectProviderAdapterOptions } from "./direct-provider.js";
|
|
2
2
|
export { createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-direct-provider.js";
|
|
3
3
|
export { createStubAgentCliAdapter, type StubAgentCliAdapterOptions } from "./stub-agent-cli.js";
|
|
4
|
-
export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type CodexCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
|
|
4
|
+
export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type CodexCliAdapterOptions, type GeminiCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
|
|
5
5
|
export { createVerifierOnlyAdapter, type VerifierOnlyAdapterOptions } from "./verifier-only.js";
|
|
6
|
-
export { createOpenAiCompatibleAdapter, type OpenAiCompatibleAdapterOptions } from "./openai-compatible.js";
|
|
6
|
+
export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig, type OpenAiCompatibleAdapterOptions } from "./openai-compatible.js";
|
|
7
|
+
export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexHostDiagnosis, type CodexHostPlatform, type CodexLaunchProbeResult } from "./codex-launcher.js";
|
|
7
8
|
export { createSpawnPlan, type SpawnLike, type SpawnPlan, type SubprocessResult, type VerificationOutcome } from "./cli-bridge.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { createDirectProviderAdapter } from "./direct-provider.js";
|
|
2
2
|
export { createStubDirectProviderAdapter } from "./stub-direct-provider.js";
|
|
3
3
|
export { createStubAgentCliAdapter } from "./stub-agent-cli.js";
|
|
4
|
-
export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter } from "./claude-cli.js";
|
|
4
|
+
export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter } from "./claude-cli.js";
|
|
5
5
|
export { createVerifierOnlyAdapter } from "./verifier-only.js";
|
|
6
|
-
export { createOpenAiCompatibleAdapter } from "./openai-compatible.js";
|
|
6
|
+
export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig } from "./openai-compatible.js";
|
|
7
|
+
export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability } from "./codex-launcher.js";
|
|
7
8
|
export { createSpawnPlan } from "./cli-bridge.js";
|
|
@@ -12,24 +12,30 @@
|
|
|
12
12
|
* Llama 3.x, Mistral 7B, Phi-4, Gemma 3, any GGUF model.
|
|
13
13
|
*
|
|
14
14
|
* Usage:
|
|
15
|
+
* # Defaults to OpenAI's hosted endpoint when MARTIN_OPENAI_BASE_URL is unset.
|
|
16
|
+
* MARTIN_OPENAI_API_KEY=sk-...
|
|
17
|
+
* MARTIN_OPENAI_MODEL=gpt-4.1-mini
|
|
18
|
+
* martin-loop run "fix the bug" --engine openai
|
|
19
|
+
*
|
|
20
|
+
* # Or route to a third-party / self-hosted OpenAI-compatible endpoint:
|
|
15
21
|
* MARTIN_OPENAI_BASE_URL=https://openrouter.ai/api
|
|
16
22
|
* MARTIN_OPENAI_API_KEY=sk-or-...
|
|
17
23
|
* MARTIN_OPENAI_MODEL=deepseek/deepseek-chat
|
|
18
|
-
* martin run "fix the bug" --engine openai
|
|
24
|
+
* martin-loop run "fix the bug" --engine openai
|
|
19
25
|
*
|
|
20
26
|
* Or for Ollama:
|
|
21
27
|
* MARTIN_OPENAI_BASE_URL=http://localhost:11434
|
|
22
28
|
* MARTIN_OPENAI_MODEL=llama3.3
|
|
23
|
-
* martin run "fix the bug" --engine openai
|
|
29
|
+
* martin-loop run "fix the bug" --engine openai
|
|
24
30
|
*/
|
|
25
31
|
import type { MartinAdapter } from "../core/index.js";
|
|
26
32
|
export interface OpenAiCompatibleAdapterOptions {
|
|
27
33
|
/** Base URL of the OpenAI-compatible API. No trailing slash. */
|
|
28
|
-
baseUrl
|
|
34
|
+
baseUrl?: string;
|
|
29
35
|
/** API key. Empty string for local (Ollama/LM Studio) endpoints. */
|
|
30
36
|
apiKey?: string;
|
|
31
37
|
/** Model identifier passed as-is to the API (e.g. "deepseek/deepseek-chat"). */
|
|
32
|
-
model
|
|
38
|
+
model?: string;
|
|
33
39
|
/**
|
|
34
40
|
* System prompt prepended before the MartinLoop task prompt.
|
|
35
41
|
* Default instructs the model to act as a focused coding assistant.
|
|
@@ -44,4 +50,13 @@ export interface OpenAiCompatibleAdapterOptions {
|
|
|
44
50
|
/** Optional fetch override for testing. */
|
|
45
51
|
fetchImpl?: typeof fetch;
|
|
46
52
|
}
|
|
53
|
+
export declare const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
|
|
54
|
+
export declare const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
|
|
55
|
+
export declare function resolveOpenAiCompatibleRuntimeConfig(env?: NodeJS.ProcessEnv): {
|
|
56
|
+
baseUrl: string;
|
|
57
|
+
model: string;
|
|
58
|
+
apiKey: string;
|
|
59
|
+
apiKeyConfigured: boolean;
|
|
60
|
+
authPosture: "api_key" | "anonymous_or_local";
|
|
61
|
+
};
|
|
47
62
|
export declare function createOpenAiCompatibleAdapter(options: OpenAiCompatibleAdapterOptions): MartinAdapter;
|
|
@@ -12,17 +12,23 @@
|
|
|
12
12
|
* Llama 3.x, Mistral 7B, Phi-4, Gemma 3, any GGUF model.
|
|
13
13
|
*
|
|
14
14
|
* Usage:
|
|
15
|
+
* # Defaults to OpenAI's hosted endpoint when MARTIN_OPENAI_BASE_URL is unset.
|
|
16
|
+
* MARTIN_OPENAI_API_KEY=sk-...
|
|
17
|
+
* MARTIN_OPENAI_MODEL=gpt-4.1-mini
|
|
18
|
+
* martin-loop run "fix the bug" --engine openai
|
|
19
|
+
*
|
|
20
|
+
* # Or route to a third-party / self-hosted OpenAI-compatible endpoint:
|
|
15
21
|
* MARTIN_OPENAI_BASE_URL=https://openrouter.ai/api
|
|
16
22
|
* MARTIN_OPENAI_API_KEY=sk-or-...
|
|
17
23
|
* MARTIN_OPENAI_MODEL=deepseek/deepseek-chat
|
|
18
|
-
* martin run "fix the bug" --engine openai
|
|
24
|
+
* martin-loop run "fix the bug" --engine openai
|
|
19
25
|
*
|
|
20
26
|
* Or for Ollama:
|
|
21
27
|
* MARTIN_OPENAI_BASE_URL=http://localhost:11434
|
|
22
28
|
* MARTIN_OPENAI_MODEL=llama3.3
|
|
23
|
-
* martin run "fix the bug" --engine openai
|
|
29
|
+
* martin-loop run "fix the bug" --engine openai
|
|
24
30
|
*/
|
|
25
|
-
import {
|
|
31
|
+
import { readGitChangedFiles, runVerification } from "./cli-bridge.js";
|
|
26
32
|
import { createAdapterCapabilities, normalizeUsage } from "./runtime-support.js";
|
|
27
33
|
// ---------------------------------------------------------------------------
|
|
28
34
|
// OpenRouter/OpenAI-compatible model pricing ($/1K tokens)
|
|
@@ -73,6 +79,18 @@ Follow these rules exactly:
|
|
|
73
79
|
- If the task asks you to write or modify code, output the complete file content with changes applied.
|
|
74
80
|
- Be precise, minimal, and test-backed in all changes.
|
|
75
81
|
- State what you changed and why at the end of your response.`;
|
|
82
|
+
export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
|
|
83
|
+
export const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
|
|
84
|
+
export function resolveOpenAiCompatibleRuntimeConfig(env = process.env) {
|
|
85
|
+
const apiKey = env["MARTIN_OPENAI_API_KEY"] ?? "";
|
|
86
|
+
return {
|
|
87
|
+
baseUrl: env["MARTIN_OPENAI_BASE_URL"] ?? DEFAULT_OPENAI_BASE_URL,
|
|
88
|
+
model: env["MARTIN_OPENAI_MODEL"] ?? DEFAULT_OPENAI_MODEL,
|
|
89
|
+
apiKey,
|
|
90
|
+
apiKeyConfigured: apiKey.length > 0,
|
|
91
|
+
authPosture: apiKey.length > 0 ? "api_key" : "anonymous_or_local"
|
|
92
|
+
};
|
|
93
|
+
}
|
|
76
94
|
function buildPrompt(request) {
|
|
77
95
|
const lines = [
|
|
78
96
|
`TASK: ${request.context.taskTitle}`,
|
|
@@ -105,13 +123,17 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
105
123
|
const verifyTimeoutMs = options.verifyTimeoutMs ?? 60_000;
|
|
106
124
|
const systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
107
125
|
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
|
126
|
+
const runtimeConfig = resolveOpenAiCompatibleRuntimeConfig();
|
|
127
|
+
const baseUrl = (options.baseUrl ?? runtimeConfig.baseUrl).replace(/\/$/, "");
|
|
128
|
+
const model = options.model ?? runtimeConfig.model;
|
|
129
|
+
const apiKey = options.apiKey ?? runtimeConfig.apiKey;
|
|
108
130
|
return {
|
|
109
|
-
adapterId: `openai-compatible:${
|
|
131
|
+
adapterId: `openai-compatible:${model}`,
|
|
110
132
|
kind: "direct-provider",
|
|
111
|
-
label: `OpenAI-compatible: ${
|
|
133
|
+
label: `OpenAI-compatible: ${model}`,
|
|
112
134
|
metadata: {
|
|
113
135
|
providerId: "openai-compatible",
|
|
114
|
-
model
|
|
136
|
+
model,
|
|
115
137
|
transport: "http",
|
|
116
138
|
capabilities: createAdapterCapabilities({
|
|
117
139
|
preflight: true,
|
|
@@ -121,7 +143,8 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
121
143
|
},
|
|
122
144
|
async execute(request) {
|
|
123
145
|
const prompt = buildPrompt(request);
|
|
124
|
-
const estimated = estimateCost(
|
|
146
|
+
const estimated = estimateCost(model, prompt.length, 2000);
|
|
147
|
+
const baselineChangedFiles = new Set(await readGitChangedFiles(workingDirectory, 5_000));
|
|
125
148
|
// Preflight: bail if projected cost exceeds remaining budget
|
|
126
149
|
if (request.context.remainingBudgetUsd > 0 &&
|
|
127
150
|
estimated.actualUsd > request.context.remainingBudgetUsd * 0.95) {
|
|
@@ -140,7 +163,7 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
140
163
|
};
|
|
141
164
|
}
|
|
142
165
|
// Call the OpenAI-compatible endpoint
|
|
143
|
-
const endpoint = `${
|
|
166
|
+
const endpoint = `${baseUrl}/v1/chat/completions`;
|
|
144
167
|
let responseText = "";
|
|
145
168
|
let tokensIn = estimated.tokensIn;
|
|
146
169
|
let tokensOut = 0;
|
|
@@ -148,10 +171,10 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
148
171
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
149
172
|
try {
|
|
150
173
|
const headers = { "Content-Type": "application/json" };
|
|
151
|
-
if (
|
|
152
|
-
headers["Authorization"] = `Bearer ${
|
|
174
|
+
if (apiKey)
|
|
175
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
153
176
|
// OpenRouter requires a site URL header for attribution
|
|
154
|
-
if (
|
|
177
|
+
if (baseUrl.includes("openrouter")) {
|
|
155
178
|
headers["HTTP-Referer"] = "https://martinloop.com";
|
|
156
179
|
headers["X-Title"] = "MartinLoop";
|
|
157
180
|
}
|
|
@@ -159,7 +182,7 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
159
182
|
method: "POST",
|
|
160
183
|
headers,
|
|
161
184
|
body: JSON.stringify({
|
|
162
|
-
model
|
|
185
|
+
model,
|
|
163
186
|
messages: [
|
|
164
187
|
{ role: "system", content: systemPrompt },
|
|
165
188
|
{ role: "user", content: prompt }
|
|
@@ -174,7 +197,7 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
174
197
|
const errMsg = body.error?.message ?? `HTTP ${res.status}`;
|
|
175
198
|
return {
|
|
176
199
|
status: "failed",
|
|
177
|
-
summary: `${
|
|
200
|
+
summary: `${model} API error: ${errMsg}`,
|
|
178
201
|
usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
|
|
179
202
|
verification: { passed: false, summary: "API call failed before verifier." },
|
|
180
203
|
failure: { message: errMsg, classHint: "infrastructure_error" }
|
|
@@ -191,7 +214,7 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
191
214
|
}
|
|
192
215
|
catch (error) {
|
|
193
216
|
const isAbort = error instanceof Error && error.name === "AbortError";
|
|
194
|
-
const message = isAbort ? `${
|
|
217
|
+
const message = isAbort ? `${model} request timed out after ${timeoutMs}ms` : String(error);
|
|
195
218
|
return {
|
|
196
219
|
status: "failed",
|
|
197
220
|
summary: message,
|
|
@@ -206,7 +229,7 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
206
229
|
if (!responseText.trim()) {
|
|
207
230
|
return {
|
|
208
231
|
status: "failed",
|
|
209
|
-
summary: `${
|
|
232
|
+
summary: `${model} returned an empty response.`,
|
|
210
233
|
usage: normalizeUsage({ actualUsd: 0, tokensIn, tokensOut: 0, provenance: "actual" }),
|
|
211
234
|
verification: { passed: false, summary: "Empty response — nothing to verify." },
|
|
212
235
|
failure: { message: "empty_response" }
|
|
@@ -214,8 +237,10 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
214
237
|
}
|
|
215
238
|
// Run verification
|
|
216
239
|
const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack);
|
|
217
|
-
const execution =
|
|
218
|
-
|
|
240
|
+
const execution = {
|
|
241
|
+
changedFiles: (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
|
|
242
|
+
};
|
|
243
|
+
const pricing = KNOWN_MODEL_PRICING[model] ?? {
|
|
219
244
|
inputPer1K: FALLBACK_INPUT_PER_1K,
|
|
220
245
|
outputPer1K: FALLBACK_OUTPUT_PER_1K
|
|
221
246
|
};
|
|
@@ -223,8 +248,8 @@ export function createOpenAiCompatibleAdapter(options) {
|
|
|
223
248
|
return {
|
|
224
249
|
status: verification.passed ? "completed" : "failed",
|
|
225
250
|
summary: verification.passed
|
|
226
|
-
? `${
|
|
227
|
-
: `${
|
|
251
|
+
? `${model} completed the task. Verifier passed.`
|
|
252
|
+
: `${model} completed but verifier failed: ${verification.summary}`,
|
|
228
253
|
usage: normalizeUsage({
|
|
229
254
|
actualUsd,
|
|
230
255
|
tokensIn,
|
|
@@ -7,7 +7,10 @@ export declare function normalizeUsage(input: {
|
|
|
7
7
|
estimatedUsd?: number;
|
|
8
8
|
tokensIn?: number;
|
|
9
9
|
tokensOut?: number;
|
|
10
|
+
cachedInputTokens?: number;
|
|
11
|
+
reasoningTokensOut?: number;
|
|
10
12
|
provenance?: MartinAdapterResult["usage"]["provenance"];
|
|
13
|
+
providerSettlement?: MartinAdapterResult["usage"]["providerSettlement"];
|
|
11
14
|
}): MartinAdapterResult["usage"];
|
|
12
15
|
export declare function diffStatsFromNumstat(stdout: string): DiffStats | undefined;
|
|
13
16
|
export declare function normalizeStructuredErrors(errors: StructuredError[] | undefined): StructuredError[];
|
|
@@ -21,7 +21,14 @@ export function normalizeUsage(input) {
|
|
|
21
21
|
: {}),
|
|
22
22
|
tokensIn: input.tokensIn ?? 0,
|
|
23
23
|
tokensOut: input.tokensOut ?? 0,
|
|
24
|
-
|
|
24
|
+
...(input.cachedInputTokens !== undefined
|
|
25
|
+
? { cachedInputTokens: input.cachedInputTokens }
|
|
26
|
+
: {}),
|
|
27
|
+
...(input.reasoningTokensOut !== undefined
|
|
28
|
+
? { reasoningTokensOut: input.reasoningTokensOut }
|
|
29
|
+
: {}),
|
|
30
|
+
provenance,
|
|
31
|
+
...(input.providerSettlement ? { providerSettlement: input.providerSettlement } : {})
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
34
|
export function diffStatsFromNumstat(stdout) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readGitChangedFiles, runVerification } from "./cli-bridge.js";
|
|
2
2
|
import { createAdapterCapabilities, normalizeUsage } from "./runtime-support.js";
|
|
3
3
|
export function createVerifierOnlyAdapter(options = {}) {
|
|
4
4
|
const workingDirectory = options.workingDirectory ?? process.cwd();
|
|
@@ -17,9 +17,10 @@ export function createVerifierOnlyAdapter(options = {}) {
|
|
|
17
17
|
})
|
|
18
18
|
},
|
|
19
19
|
async execute(request) {
|
|
20
|
+
const baselineChangedFiles = new Set(await readGitChangedFiles(workingDirectory, 5_000));
|
|
20
21
|
const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack);
|
|
21
|
-
const
|
|
22
|
-
const
|
|
22
|
+
const changedFiles = (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file));
|
|
23
|
+
const execution = { changedFiles };
|
|
23
24
|
if (verification.passed) {
|
|
24
25
|
return {
|
|
25
26
|
status: "completed",
|
|
@@ -49,6 +49,40 @@ export interface LoopCost {
|
|
|
49
49
|
avoidedUsd: number;
|
|
50
50
|
tokensIn: number;
|
|
51
51
|
tokensOut: number;
|
|
52
|
+
estimatedUsd?: number;
|
|
53
|
+
provenance?: CostProvenance;
|
|
54
|
+
providerSettlement?: ProviderUsageSettlement;
|
|
55
|
+
}
|
|
56
|
+
export type UsageSettlementSource = "claude_json" | "codex_jsonl" | "gemini_json" | "estimated_fallback" | "unavailable";
|
|
57
|
+
export interface ProviderUsageSettlement {
|
|
58
|
+
providerId: string;
|
|
59
|
+
model: string;
|
|
60
|
+
transport?: "cli" | "http" | "routed_http";
|
|
61
|
+
source: UsageSettlementSource;
|
|
62
|
+
inputTokens: number;
|
|
63
|
+
cachedInputTokens?: number;
|
|
64
|
+
outputTokens: number;
|
|
65
|
+
reasoningOutputTokens?: number;
|
|
66
|
+
rawUsageAvailable: boolean;
|
|
67
|
+
settledAt: string;
|
|
68
|
+
}
|
|
69
|
+
export interface ReceiptScope {
|
|
70
|
+
repoRoot?: string;
|
|
71
|
+
workingDirectory?: string;
|
|
72
|
+
invocationRoot?: string;
|
|
73
|
+
runsRoot?: string;
|
|
74
|
+
}
|
|
75
|
+
export type ReceiptIntegrityState = "verified" | "unsigned" | "tamper_detected";
|
|
76
|
+
export interface ReceiptIntegritySummary {
|
|
77
|
+
state: ReceiptIntegrityState;
|
|
78
|
+
keyId?: string;
|
|
79
|
+
signedAt?: string;
|
|
80
|
+
loopRecordSha256?: string;
|
|
81
|
+
ledgerSha256?: string;
|
|
82
|
+
ledgerHeadHash?: string;
|
|
83
|
+
entryCount?: number;
|
|
84
|
+
reason?: string;
|
|
85
|
+
warnings?: string[];
|
|
52
86
|
}
|
|
53
87
|
export interface LoopArtifact {
|
|
54
88
|
artifactId: string;
|
|
@@ -90,6 +124,8 @@ export interface LoopRecord {
|
|
|
90
124
|
metadata: Record<string, string>;
|
|
91
125
|
createdAt: string;
|
|
92
126
|
updatedAt: string;
|
|
127
|
+
receiptScope?: ReceiptScope;
|
|
128
|
+
receiptIntegrity?: ReceiptIntegritySummary;
|
|
93
129
|
}
|
|
94
130
|
export interface LoopRecordDraft {
|
|
95
131
|
loopId?: string;
|
|
@@ -107,6 +143,8 @@ export interface LoopRecordDraft {
|
|
|
107
143
|
metadata?: Record<string, string>;
|
|
108
144
|
createdAt?: string;
|
|
109
145
|
updatedAt?: string;
|
|
146
|
+
receiptScope?: ReceiptScope;
|
|
147
|
+
receiptIntegrity?: ReceiptIntegritySummary;
|
|
110
148
|
}
|
|
111
149
|
export type { MartinErrorCategory, MartinOutputMode, MartinRunListFilters, MartinRunSelector } from "./operator.js";
|
|
112
150
|
export { MARTIN_ERROR_CATEGORIES } from "./operator.js";
|
|
@@ -272,6 +310,7 @@ export interface BudgetSettlement {
|
|
|
272
310
|
usd: number;
|
|
273
311
|
provenance: CostProvenance;
|
|
274
312
|
};
|
|
313
|
+
providerSettlement?: ProviderUsageSettlement;
|
|
275
314
|
totalActualUsd: number;
|
|
276
315
|
preflightEstimateUsd: number;
|
|
277
316
|
varianceUsd: number;
|
|
@@ -36,6 +36,8 @@ export function createLoopRecord(draft, options = {}) {
|
|
|
36
36
|
},
|
|
37
37
|
createdAt: draft.createdAt ?? now,
|
|
38
38
|
updatedAt: draft.updatedAt ?? now,
|
|
39
|
+
...(draft.receiptScope ? { receiptScope: draft.receiptScope } : {}),
|
|
40
|
+
...(draft.receiptIntegrity ? { receiptIntegrity: draft.receiptIntegrity } : {}),
|
|
39
41
|
...(draft.teamId ? { teamId: draft.teamId } : {})
|
|
40
42
|
};
|
|
41
43
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ApprovalPolicy, type CostProvenance, type ExecutionProfile, type FailureClass, type InterventionType, type LoopArtifact, type LoopAttempt, type LoopBudget, type MutationMode, type LoopRecord, type LoopTask } from "../contracts/index.js";
|
|
1
|
+
import { type ApprovalPolicy, type CostProvenance, type ExecutionProfile, type FailureClass, type InterventionType, type LoopArtifact, type LoopAttempt, type LoopBudget, type ProviderUsageSettlement, type MutationMode, type LoopRecord, type LoopTask, type ReceiptScope } from "../contracts/index.js";
|
|
2
2
|
import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, type ExitDecision } from "./policy.js";
|
|
3
3
|
import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
|
|
4
4
|
import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
|
|
@@ -13,8 +13,8 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
|
13
13
|
export type { ContextIntegrityPrecheck, ContextIntegrityVerdict } from "./context-integrity.js";
|
|
14
14
|
export { compilePromptPacket } from "./compiler.js";
|
|
15
15
|
export type { PromptPacket, CompilerAdapterRequest } from "./compiler.js";
|
|
16
|
-
export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot } from "./persistence/index.js";
|
|
17
|
-
export type { AttemptArtifacts, LedgerEvent, LedgerEventKind, LoopAttemptRecord, LoopRunRecord, RunContract, RunStore } from "./persistence/index.js";
|
|
16
|
+
export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
17
|
+
export type { AttemptArtifacts, LedgerEvent, LedgerEventKind, LoopAttemptRecord, LoopRunRecord, ReceiptIntegrityChainEntry, RunContract, RunStore, StoredReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
18
18
|
export { compileAndPersistContext } from "./persistence/index.js";
|
|
19
19
|
export type { CompileResult } from "./persistence/index.js";
|
|
20
20
|
export interface MartinAdapterRequest {
|
|
@@ -44,6 +44,20 @@ export interface MartinAdapterRequest {
|
|
|
44
44
|
};
|
|
45
45
|
previousAttempts: LoopAttempt[];
|
|
46
46
|
}
|
|
47
|
+
export interface MartinVerificationStep {
|
|
48
|
+
command: string;
|
|
49
|
+
launched: boolean;
|
|
50
|
+
exitCode?: number;
|
|
51
|
+
timedOut: boolean;
|
|
52
|
+
fastFail?: boolean;
|
|
53
|
+
detail?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface MartinVerificationOutcome {
|
|
56
|
+
passed: boolean;
|
|
57
|
+
summary: string;
|
|
58
|
+
steps?: MartinVerificationStep[];
|
|
59
|
+
warnings?: string[];
|
|
60
|
+
}
|
|
47
61
|
export interface MartinAdapterResult {
|
|
48
62
|
status: "completed" | "failed";
|
|
49
63
|
summary: string;
|
|
@@ -52,11 +66,16 @@ export interface MartinAdapterResult {
|
|
|
52
66
|
estimatedUsd?: number;
|
|
53
67
|
tokensIn: number;
|
|
54
68
|
tokensOut: number;
|
|
69
|
+
cachedInputTokens?: number;
|
|
70
|
+
reasoningTokensOut?: number;
|
|
55
71
|
provenance?: CostProvenance;
|
|
72
|
+
providerSettlement?: ProviderUsageSettlement;
|
|
56
73
|
};
|
|
57
74
|
verification: {
|
|
58
75
|
passed: boolean;
|
|
59
76
|
summary: string;
|
|
77
|
+
steps?: MartinVerificationStep[];
|
|
78
|
+
warnings?: string[];
|
|
60
79
|
};
|
|
61
80
|
execution?: {
|
|
62
81
|
changedFiles?: string[];
|
|
@@ -135,6 +154,7 @@ export interface RunMartinInput {
|
|
|
135
154
|
maxRecentAttempts?: number;
|
|
136
155
|
fallbackModels?: string[];
|
|
137
156
|
fallbackAdapters?: MartinAdapter[];
|
|
157
|
+
receiptScope?: ReceiptScope;
|
|
138
158
|
/** Optional persistence store. When provided, runMartin writes artifacts on each lifecycle event. */
|
|
139
159
|
store?: RunStore;
|
|
140
160
|
}
|