prompts-gpt 0.2.8
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/LICENSE +75 -0
- package/README.md +202 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3650 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +227 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1119 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +225 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1490 -0
- package/dist/runtime.js.map +1 -0
- package/dist/sweep.d.ts +180 -0
- package/dist/sweep.d.ts.map +1 -0
- package/dist/sweep.js +765 -0
- package/dist/sweep.js.map +1 -0
- package/package.json +66 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,1490 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { cpus } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
export const DEFAULT_RUN_ARTIFACTS_DIR = ".scripts/runs";
|
|
7
|
+
export const DEFAULT_RUN_CONFIG_PATH = ".prompts-gpt/config.json";
|
|
8
|
+
export const ORCHESTRATION_AGENT_PROFILES = ["codex", "cursor", "claude", "copilot", "router"];
|
|
9
|
+
const DEFAULT_PROVIDER_ORDER = ["codex", "cursor", "claude", "copilot"];
|
|
10
|
+
const PROVIDER_ALIASES = {
|
|
11
|
+
codex: "codex",
|
|
12
|
+
openai: "codex",
|
|
13
|
+
"openai-codex": "codex",
|
|
14
|
+
cursor: "cursor",
|
|
15
|
+
"cursor-agent": "cursor",
|
|
16
|
+
agent: "cursor",
|
|
17
|
+
claude: "claude",
|
|
18
|
+
"claude-code": "claude",
|
|
19
|
+
anthropic: "claude",
|
|
20
|
+
copilot: "copilot",
|
|
21
|
+
"github-copilot": "copilot",
|
|
22
|
+
"gh-copilot": "copilot",
|
|
23
|
+
router: "router",
|
|
24
|
+
};
|
|
25
|
+
export const DEFAULT_MODELS = {
|
|
26
|
+
codex: "gpt-5.4-mini",
|
|
27
|
+
cursor: "auto",
|
|
28
|
+
claude: "claude-sonnet-4-6",
|
|
29
|
+
copilot: "auto",
|
|
30
|
+
};
|
|
31
|
+
const NON_CODEX_MAX_PROMPT_BYTES = 100_000;
|
|
32
|
+
const RETRYABLE_RUN_ERROR_CODES = new Set(["PROVIDER_SPAWN_FAILED", "PROVIDER_TIMEOUT"]);
|
|
33
|
+
export function isCI() {
|
|
34
|
+
const env = process.env;
|
|
35
|
+
return Boolean(env.CI || env.CONTINUOUS_INTEGRATION || env.GITHUB_ACTIONS ||
|
|
36
|
+
env.GITLAB_CI || env.CIRCLECI || env.JENKINS_URL || env.BUILDKITE ||
|
|
37
|
+
env.TRAVIS || env.TF_BUILD || env.CODEBUILD_BUILD_ID);
|
|
38
|
+
}
|
|
39
|
+
class ProviderRunError extends Error {
|
|
40
|
+
code;
|
|
41
|
+
constructor(code, message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "ProviderRunError";
|
|
44
|
+
this.code = code;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function normalizeOrchestrationAgent(value) {
|
|
48
|
+
const raw = String(value ?? "router").trim().toLowerCase();
|
|
49
|
+
return PROVIDER_ALIASES[raw] ?? "router";
|
|
50
|
+
}
|
|
51
|
+
export function normalizeConcreteProvider(value) {
|
|
52
|
+
const parsed = parseConcreteProvider(value);
|
|
53
|
+
if (!parsed) {
|
|
54
|
+
throw new Error("Invalid provider. Use codex, cursor, claude, or copilot.");
|
|
55
|
+
}
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
function parseConcreteProvider(value) {
|
|
59
|
+
const raw = String(value ?? "").trim().toLowerCase();
|
|
60
|
+
if (!raw || raw === "router") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const normalized = normalizeOrchestrationAgent(raw);
|
|
64
|
+
if (normalized === "router") {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
export async function loadRunConfig(cwd = process.cwd()) {
|
|
70
|
+
const configPath = path.resolve(cwd, DEFAULT_RUN_CONFIG_PATH);
|
|
71
|
+
let parsed = {};
|
|
72
|
+
const configWarnings = [];
|
|
73
|
+
if (existsSync(configPath)) {
|
|
74
|
+
try {
|
|
75
|
+
const data = JSON.parse(await readFile(configPath, "utf8"));
|
|
76
|
+
parsed = data && typeof data === "object" ? data : {};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
parsed = {};
|
|
80
|
+
configWarnings.push(`Could not parse ${DEFAULT_RUN_CONFIG_PATH}: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const env = process.env;
|
|
84
|
+
const envProviderOrder = parseProviderOrderEnv(env.PROMPTS_GPT_RUN_PROVIDER_ORDER);
|
|
85
|
+
const envDefaultAgent = normalizeOptionalAgent(env.PROMPTS_GPT_RUN_AGENT);
|
|
86
|
+
const envTimeoutSeconds = parsePositiveIntEnv(env.PROMPTS_GPT_RUN_TIMEOUT_SECONDS);
|
|
87
|
+
const envRetryCount = parseNonNegativeIntEnv(env.PROMPTS_GPT_RUN_RETRY_COUNT);
|
|
88
|
+
const envArtifactsDir = env.PROMPTS_GPT_RUN_ARTIFACTS_DIR?.trim();
|
|
89
|
+
const envSafety = parseBooleanEnv(env.PROMPTS_GPT_RUN_DISALLOW_DESTRUCTIVE_GIT);
|
|
90
|
+
const envModelOverrides = readModelOverridesFromEnv(env);
|
|
91
|
+
const configProviderOrder = Array.isArray(parsed.providerOrder) && parsed.providerOrder.length > 0
|
|
92
|
+
? parsed.providerOrder
|
|
93
|
+
.map((p) => parseConcreteProvider(String(p)))
|
|
94
|
+
.filter((provider) => provider !== null)
|
|
95
|
+
: DEFAULT_PROVIDER_ORDER;
|
|
96
|
+
const providerOrder = envProviderOrder.length > 0 ? envProviderOrder : configProviderOrder;
|
|
97
|
+
const uniqueProviderOrder = [...new Set(providerOrder)].filter((p) => DEFAULT_PROVIDER_ORDER.includes(p));
|
|
98
|
+
const defaultAgent = envDefaultAgent ?? normalizeOrchestrationAgent(parsed.defaultAgent ?? "router");
|
|
99
|
+
const defaultPromptFile = typeof parsed.defaultPromptFile === "string" && parsed.defaultPromptFile.trim()
|
|
100
|
+
? path.resolve(cwd, parsed.defaultPromptFile.trim())
|
|
101
|
+
: null;
|
|
102
|
+
const timeoutRaw = envTimeoutSeconds ?? Number(parsed.timeoutSeconds);
|
|
103
|
+
const timeoutSeconds = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? Math.trunc(timeoutRaw) : 900;
|
|
104
|
+
const retryRaw = envRetryCount ?? Number(parsed.retryCount);
|
|
105
|
+
const retryCount = Number.isFinite(retryRaw) && retryRaw >= 0 ? Math.trunc(retryRaw) : 0;
|
|
106
|
+
const artifactsDir = path.resolve(cwd, envArtifactsDir || parsed.artifactsDir?.trim() || DEFAULT_RUN_ARTIFACTS_DIR);
|
|
107
|
+
const batchDefaults = resolveConfiguredBatchDefaults(cwd, parsed.batchDefaults);
|
|
108
|
+
return {
|
|
109
|
+
providerOrder: uniqueProviderOrder.length ? uniqueProviderOrder : DEFAULT_PROVIDER_ORDER,
|
|
110
|
+
defaultAgent,
|
|
111
|
+
defaultPromptFile,
|
|
112
|
+
modelOverrides: {
|
|
113
|
+
...(parsed.modelOverrides ?? {}),
|
|
114
|
+
...envModelOverrides,
|
|
115
|
+
},
|
|
116
|
+
customModels: parsed.customModels ?? {},
|
|
117
|
+
timeoutSeconds,
|
|
118
|
+
retryCount,
|
|
119
|
+
artifactsDir,
|
|
120
|
+
batchDefaults,
|
|
121
|
+
safety: {
|
|
122
|
+
disallowDestructiveGit: envSafety ?? parsed.safety?.disallowDestructiveGit ?? true,
|
|
123
|
+
},
|
|
124
|
+
configPath,
|
|
125
|
+
configWarnings,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export async function detectProviders(cwd = process.cwd()) {
|
|
129
|
+
const env = process.env;
|
|
130
|
+
const providers = ["codex", "cursor", "claude", "copilot"];
|
|
131
|
+
const checks = providers.map((provider) => {
|
|
132
|
+
const bin = resolveProviderBin(provider, env);
|
|
133
|
+
const available = isExecutableAvailable(bin);
|
|
134
|
+
const version = available ? readProviderVersion(provider, bin, cwd) : null;
|
|
135
|
+
return {
|
|
136
|
+
provider,
|
|
137
|
+
bin,
|
|
138
|
+
available,
|
|
139
|
+
version,
|
|
140
|
+
modelDefault: DEFAULT_MODELS[provider],
|
|
141
|
+
checkedAt: new Date().toISOString(),
|
|
142
|
+
installHint: installHintForProvider(provider),
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
return checks;
|
|
146
|
+
}
|
|
147
|
+
export async function doctor(cwd = process.cwd()) {
|
|
148
|
+
const resolvedCwd = path.resolve(cwd);
|
|
149
|
+
const config = await loadRunConfig(resolvedCwd);
|
|
150
|
+
const providers = await detectProviders(resolvedCwd);
|
|
151
|
+
const notes = [];
|
|
152
|
+
if (!existsSync(config.configPath)) {
|
|
153
|
+
notes.push(`No ${DEFAULT_RUN_CONFIG_PATH} found. Defaults are active.`);
|
|
154
|
+
notes.push(`Run \`prompts-gpt setup\` to scaffold a project-local run config.`);
|
|
155
|
+
}
|
|
156
|
+
const gitResult = spawnSync("git", ["--version"], { encoding: "utf8", windowsHide: true, timeout: 5_000 });
|
|
157
|
+
if (gitResult.error || gitResult.status !== 0) {
|
|
158
|
+
notes.push("Git is not available on PATH. Git is required for worktree tracking and sweep safety.");
|
|
159
|
+
}
|
|
160
|
+
const nodeVersionParts = process.version.replace("v", "").split(".").map(Number);
|
|
161
|
+
if ((nodeVersionParts[0] ?? 0) < 18 || ((nodeVersionParts[0] ?? 0) === 18 && (nodeVersionParts[1] ?? 0) < 18)) {
|
|
162
|
+
notes.push(`Node.js ${process.version} is below the minimum required (>=18.18). Upgrade Node.js.`);
|
|
163
|
+
}
|
|
164
|
+
notes.push(...config.configWarnings);
|
|
165
|
+
if (!providers.some((p) => p.available)) {
|
|
166
|
+
notes.push("No supported provider CLI found on PATH. Install Codex, Cursor Agent, Claude Code, or Copilot CLI.");
|
|
167
|
+
}
|
|
168
|
+
if (config.retryCount > 0) {
|
|
169
|
+
notes.push(`Run retries enabled: ${config.retryCount}. Only spawn and timeout failures are retried automatically.`);
|
|
170
|
+
}
|
|
171
|
+
if (config.providerOrder.length > 0) {
|
|
172
|
+
notes.push(`Router provider order: ${config.providerOrder.join(", ")}.`);
|
|
173
|
+
}
|
|
174
|
+
if (config.defaultPromptFile) {
|
|
175
|
+
notes.push(`Default prompt file: ${config.defaultPromptFile}.`);
|
|
176
|
+
}
|
|
177
|
+
if (config.batchDefaults.manifestPath) {
|
|
178
|
+
notes.push(`Batch default manifest: ${config.batchDefaults.manifestPath}.`);
|
|
179
|
+
}
|
|
180
|
+
else if (config.batchDefaults.promptFiles.length > 0) {
|
|
181
|
+
notes.push(`Batch default prompt files: ${config.batchDefaults.promptFiles.length}.`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
notes.push("No batch prompt source configured. Run `prompts-gpt setup` to scan prompt files or set one explicitly.");
|
|
185
|
+
}
|
|
186
|
+
if (config.safety.disallowDestructiveGit) {
|
|
187
|
+
notes.push("Destructive Git protection is configured, but provider-side enforcement still depends on prompt/runtime policy in this local-first V1 runner.");
|
|
188
|
+
}
|
|
189
|
+
const sweepDir = path.resolve(resolvedCwd, ".prompts-gpt/sweeps");
|
|
190
|
+
if (existsSync(sweepDir)) {
|
|
191
|
+
try {
|
|
192
|
+
const sweepEntries = await readdir(sweepDir, { withFileTypes: true });
|
|
193
|
+
const sweepFiles = sweepEntries.filter((e) => e.isFile() && e.name.endsWith(".md"));
|
|
194
|
+
notes.push(`Sweep files: ${sweepFiles.length} found in .prompts-gpt/sweeps/`);
|
|
195
|
+
}
|
|
196
|
+
catch { /* skip */ }
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
notes.push("Sweep files: none (create .prompts-gpt/sweeps/<name>.md to add sweeps)");
|
|
200
|
+
}
|
|
201
|
+
const lockPath = path.resolve(resolvedCwd, ".sweep.lock");
|
|
202
|
+
if (existsSync(lockPath)) {
|
|
203
|
+
try {
|
|
204
|
+
const lockContent = JSON.parse(await readFile(lockPath, "utf8"));
|
|
205
|
+
notes.push(`Sweep lock: active (PID ${lockContent.pid}, started ${lockContent.startedAt}). Remove with: rm ${lockPath}`);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
notes.push(`Sweep lock: found but unreadable at ${lockPath}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
cwd: resolvedCwd,
|
|
213
|
+
nodeVersion: process.version,
|
|
214
|
+
osPlatform: process.platform,
|
|
215
|
+
osArch: process.arch,
|
|
216
|
+
cpuCount: cpus().length,
|
|
217
|
+
configPath: config.configPath,
|
|
218
|
+
configFound: existsSync(config.configPath),
|
|
219
|
+
providers,
|
|
220
|
+
notes,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export async function initRunConfig(input = {}) {
|
|
224
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
225
|
+
const configPath = path.resolve(cwd, DEFAULT_RUN_CONFIG_PATH);
|
|
226
|
+
const providers = await detectProviders(cwd);
|
|
227
|
+
const discovered = await detectPromptSources(cwd, input);
|
|
228
|
+
const config = {
|
|
229
|
+
providerOrder: normalizeProviderOrderInput(input.providerOrder, providers),
|
|
230
|
+
defaultAgent: normalizeOrchestrationAgent(input.defaultAgent ?? "router"),
|
|
231
|
+
defaultPromptFile: discovered.defaultPromptFile ? toProjectConfigPath(cwd, discovered.defaultPromptFile) : undefined,
|
|
232
|
+
modelOverrides: buildScaffoldModelOverrides(input.modelOverrides),
|
|
233
|
+
customModels: {},
|
|
234
|
+
timeoutSeconds: resolveTimeoutSeconds(input.timeoutSeconds, 900),
|
|
235
|
+
retryCount: resolveRetryCount(input.retryCount),
|
|
236
|
+
artifactsDir: toProjectConfigPath(cwd, input.artifactsDir?.trim() || DEFAULT_RUN_ARTIFACTS_DIR),
|
|
237
|
+
batchDefaults: {
|
|
238
|
+
manifestPath: discovered.manifestPath ? toProjectConfigPath(cwd, discovered.manifestPath) : undefined,
|
|
239
|
+
promptFiles: discovered.promptFiles.map((file) => toProjectConfigPath(cwd, file)),
|
|
240
|
+
},
|
|
241
|
+
safety: {
|
|
242
|
+
disallowDestructiveGit: input.disallowDestructiveGit ?? true,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
246
|
+
if (input.overwrite) {
|
|
247
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
try {
|
|
251
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { flag: "wx" });
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
if (err.code === "EEXIST") {
|
|
255
|
+
throw new Error(`Run config already exists: ${configPath}. Re-run with overwrite enabled to replace it.`);
|
|
256
|
+
}
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
await ensureGitignoreEntry(cwd, toGitignorePath(cwd, path.resolve(cwd, config.artifactsDir ?? DEFAULT_RUN_ARTIFACTS_DIR)));
|
|
261
|
+
await ensureGitignoreEntry(cwd, ".sweep.lock");
|
|
262
|
+
return {
|
|
263
|
+
configPath,
|
|
264
|
+
config,
|
|
265
|
+
providerSummary: providers,
|
|
266
|
+
sourceSummary: discovered,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
export async function runBatch(input) {
|
|
270
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
271
|
+
const config = await loadRunConfig(cwd);
|
|
272
|
+
const promptFiles = [...new Set(await resolveBatchPromptFiles(cwd, input, config))];
|
|
273
|
+
const results = [];
|
|
274
|
+
for (const promptFile of promptFiles) {
|
|
275
|
+
const result = await runPrompt({
|
|
276
|
+
cwd,
|
|
277
|
+
promptFile,
|
|
278
|
+
agent: input.agent,
|
|
279
|
+
model: input.model,
|
|
280
|
+
timeoutSeconds: input.timeoutSeconds,
|
|
281
|
+
artifactsDir: input.artifactsDir,
|
|
282
|
+
});
|
|
283
|
+
results.push(result);
|
|
284
|
+
}
|
|
285
|
+
const success = results.filter((result) => result.exitCode === 0).length;
|
|
286
|
+
return {
|
|
287
|
+
total: results.length,
|
|
288
|
+
success,
|
|
289
|
+
failed: results.length - success,
|
|
290
|
+
results,
|
|
291
|
+
tokenUsage: aggregateTokenUsage(results.map((result) => result.tokenUsage)),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
export async function runPrompt(input) {
|
|
295
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
296
|
+
const config = await loadRunConfig(cwd);
|
|
297
|
+
const providers = await detectProviders(cwd);
|
|
298
|
+
const agent = normalizeOrchestrationAgent(input.agent ?? config.defaultAgent);
|
|
299
|
+
const provider = resolveRunProvider(agent, providers, config.providerOrder);
|
|
300
|
+
const providerHealth = providers.find((item) => item.provider === provider);
|
|
301
|
+
if (!providerHealth || !providerHealth.available) {
|
|
302
|
+
throw new Error(`Provider ${provider} is not available. ${installHintForProvider(provider)} Run \`prompts-gpt doctor\` for details.`);
|
|
303
|
+
}
|
|
304
|
+
const promptFile = input.promptFile
|
|
305
|
+
? path.resolve(cwd, input.promptFile)
|
|
306
|
+
: await resolveDefaultPromptFile(cwd, config);
|
|
307
|
+
if (!existsSync(promptFile)) {
|
|
308
|
+
throw new Error(`Prompt file not found: ${promptFile}`);
|
|
309
|
+
}
|
|
310
|
+
const model = input.model?.trim() || config.modelOverrides[provider]?.trim() || DEFAULT_MODELS[provider];
|
|
311
|
+
const timeoutSeconds = resolveTimeoutSeconds(input.timeoutSeconds, config.timeoutSeconds);
|
|
312
|
+
const hex = Array.from({ length: 4 }, () => Math.floor(Math.random() * 256).toString(16).padStart(2, "0")).join("");
|
|
313
|
+
const runId = input.runId?.trim() || `agent-${new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14)}-${process.pid}-${hex}`;
|
|
314
|
+
const runDir = path.resolve(input.artifactsDir ? path.resolve(cwd, input.artifactsDir) : config.artifactsDir, runId);
|
|
315
|
+
try {
|
|
316
|
+
await mkdir(runDir, { recursive: true });
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
const code = err.code;
|
|
320
|
+
if (code === "EPERM" || code === "EACCES") {
|
|
321
|
+
throw new Error(`Cannot create run directory ${runDir}: permission denied. Check filesystem permissions or use --artifacts-dir.`);
|
|
322
|
+
}
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
const summaryFile = path.join(runDir, "summary.md");
|
|
326
|
+
const logFile = path.join(runDir, "agent.log");
|
|
327
|
+
const worktreeBeforeFile = path.join(runDir, "worktree-before.txt");
|
|
328
|
+
const worktreeAfterFile = path.join(runDir, "worktree-after.txt");
|
|
329
|
+
const worktreeDeltaFile = path.join(runDir, "worktree-delta.diff");
|
|
330
|
+
const before = captureWorktreeStatus(cwd);
|
|
331
|
+
await writeFile(worktreeBeforeFile, before);
|
|
332
|
+
const startedAt = new Date();
|
|
333
|
+
const startedMs = Date.now();
|
|
334
|
+
let run;
|
|
335
|
+
try {
|
|
336
|
+
const promptText = await readFile(promptFile, "utf8");
|
|
337
|
+
assertPromptFitsLaunch(provider, promptText, promptFile);
|
|
338
|
+
run = await executeProviderCommandWithRetries({
|
|
339
|
+
provider,
|
|
340
|
+
bin: providerHealth.bin,
|
|
341
|
+
model,
|
|
342
|
+
cwd,
|
|
343
|
+
promptText,
|
|
344
|
+
promptFile,
|
|
345
|
+
summaryFile,
|
|
346
|
+
logFile,
|
|
347
|
+
timeoutSeconds,
|
|
348
|
+
retryCount: config.retryCount,
|
|
349
|
+
approveMcps: input.approveMcps ?? true,
|
|
350
|
+
sandboxMode: input.sandboxMode ?? "workspace-write",
|
|
351
|
+
background: input.background,
|
|
352
|
+
permissionMode: input.permissionMode,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
const after = captureWorktreeStatus(cwd);
|
|
357
|
+
await writeFile(worktreeAfterFile, after);
|
|
358
|
+
await writeFile(worktreeDeltaFile, buildWorktreeDelta(before, after));
|
|
359
|
+
}
|
|
360
|
+
const finishedAt = new Date();
|
|
361
|
+
const tokenUsage = await readTokenUsageFromLog(logFile);
|
|
362
|
+
return {
|
|
363
|
+
runId,
|
|
364
|
+
provider,
|
|
365
|
+
model,
|
|
366
|
+
promptFile,
|
|
367
|
+
runDir,
|
|
368
|
+
summaryFile,
|
|
369
|
+
logFile,
|
|
370
|
+
worktreeBeforeFile,
|
|
371
|
+
worktreeAfterFile,
|
|
372
|
+
worktreeDeltaFile,
|
|
373
|
+
exitCode: run.exitCode,
|
|
374
|
+
startedAt: startedAt.toISOString(),
|
|
375
|
+
finishedAt: finishedAt.toISOString(),
|
|
376
|
+
durationMs: Date.now() - startedMs,
|
|
377
|
+
commandPreview: run.commandPreview,
|
|
378
|
+
tokenUsage,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const TOKEN_USAGE_NUMERIC_KEYS = new Set([
|
|
382
|
+
"input_tokens",
|
|
383
|
+
"prompt_tokens",
|
|
384
|
+
"output_tokens",
|
|
385
|
+
"completion_tokens",
|
|
386
|
+
"total_tokens",
|
|
387
|
+
"reasoning_tokens",
|
|
388
|
+
"cached_input_tokens",
|
|
389
|
+
"cache_read_input_tokens",
|
|
390
|
+
"cache_creation_input_tokens",
|
|
391
|
+
"cache_write_input_tokens",
|
|
392
|
+
"inputTokens",
|
|
393
|
+
"promptTokens",
|
|
394
|
+
"outputTokens",
|
|
395
|
+
"completionTokens",
|
|
396
|
+
"totalTokens",
|
|
397
|
+
"reasoningTokens",
|
|
398
|
+
"cacheReadInputTokens",
|
|
399
|
+
"cacheWriteInputTokens",
|
|
400
|
+
"cacheReadTokens",
|
|
401
|
+
"cacheWriteTokens",
|
|
402
|
+
]);
|
|
403
|
+
export function emptyTokenUsage() {
|
|
404
|
+
return {
|
|
405
|
+
inputTokens: 0,
|
|
406
|
+
outputTokens: 0,
|
|
407
|
+
totalTokens: 0,
|
|
408
|
+
reasoningTokens: 0,
|
|
409
|
+
cacheReadTokens: 0,
|
|
410
|
+
cacheWriteTokens: 0,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
export function hasTokenUsage(usage) {
|
|
414
|
+
if (!usage)
|
|
415
|
+
return false;
|
|
416
|
+
return Object.values(usage).some((value) => Number.isFinite(value) && value > 0);
|
|
417
|
+
}
|
|
418
|
+
export function aggregateTokenUsage(usages) {
|
|
419
|
+
const totals = emptyTokenUsage();
|
|
420
|
+
let found = false;
|
|
421
|
+
for (const usage of usages) {
|
|
422
|
+
if (!hasTokenUsage(usage))
|
|
423
|
+
continue;
|
|
424
|
+
found = true;
|
|
425
|
+
totals.inputTokens += usage.inputTokens;
|
|
426
|
+
totals.outputTokens += usage.outputTokens;
|
|
427
|
+
totals.totalTokens += usage.totalTokens;
|
|
428
|
+
totals.reasoningTokens += usage.reasoningTokens;
|
|
429
|
+
totals.cacheReadTokens += usage.cacheReadTokens;
|
|
430
|
+
totals.cacheWriteTokens += usage.cacheWriteTokens;
|
|
431
|
+
}
|
|
432
|
+
return found ? totals : null;
|
|
433
|
+
}
|
|
434
|
+
export async function readTokenUsageFromLog(logFile) {
|
|
435
|
+
if (!existsSync(logFile))
|
|
436
|
+
return null;
|
|
437
|
+
try {
|
|
438
|
+
const content = await readFile(logFile, "utf8");
|
|
439
|
+
return extractTokenUsageFromLog(content);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
export function extractTokenUsageFromLog(logContent) {
|
|
446
|
+
if (!logContent.trim())
|
|
447
|
+
return null;
|
|
448
|
+
const terminalCandidates = [];
|
|
449
|
+
const generalCandidates = [];
|
|
450
|
+
for (const rawLine of logContent.split("\n")) {
|
|
451
|
+
const trimmed = rawLine.trim();
|
|
452
|
+
if (!trimmed)
|
|
453
|
+
continue;
|
|
454
|
+
if (trimmed.startsWith("{")) {
|
|
455
|
+
try {
|
|
456
|
+
const parsed = JSON.parse(trimmed);
|
|
457
|
+
const candidate = extractBestTokenUsageCandidate(parsed);
|
|
458
|
+
if (!candidate)
|
|
459
|
+
continue;
|
|
460
|
+
const lineType = String(parsed.type ?? parsed.event ?? parsed.kind ?? "").toLowerCase();
|
|
461
|
+
if (lineType === "result" ||
|
|
462
|
+
lineType === "final" ||
|
|
463
|
+
lineType === "response.completed" ||
|
|
464
|
+
lineType === "message_stop") {
|
|
465
|
+
terminalCandidates.push(candidate);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
generalCandidates.push(candidate);
|
|
469
|
+
}
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// Fall through to text parsing below.
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const textCandidate = extractTokenUsageFromText(trimmed);
|
|
477
|
+
if (textCandidate) {
|
|
478
|
+
generalCandidates.push(textCandidate);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (terminalCandidates.length > 0) {
|
|
482
|
+
return aggregateTokenUsage(terminalCandidates);
|
|
483
|
+
}
|
|
484
|
+
if (generalCandidates.length > 0) {
|
|
485
|
+
return generalCandidates.reduce((best, current) => {
|
|
486
|
+
if (!best)
|
|
487
|
+
return current;
|
|
488
|
+
if (current.totalTokens > best.totalTokens)
|
|
489
|
+
return current;
|
|
490
|
+
if (current.totalTokens === best.totalTokens && current.inputTokens + current.outputTokens > best.inputTokens + best.outputTokens) {
|
|
491
|
+
return current;
|
|
492
|
+
}
|
|
493
|
+
return best;
|
|
494
|
+
}, null);
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
function extractBestTokenUsageCandidate(value) {
|
|
499
|
+
const candidates = collectTokenUsageCandidates(value);
|
|
500
|
+
return candidates.reduce((best, current) => {
|
|
501
|
+
if (!best)
|
|
502
|
+
return current;
|
|
503
|
+
if (current.totalTokens > best.totalTokens)
|
|
504
|
+
return current;
|
|
505
|
+
if (current.totalTokens === best.totalTokens && current.inputTokens + current.outputTokens > best.inputTokens + best.outputTokens) {
|
|
506
|
+
return current;
|
|
507
|
+
}
|
|
508
|
+
return best;
|
|
509
|
+
}, null);
|
|
510
|
+
}
|
|
511
|
+
function collectTokenUsageCandidates(value) {
|
|
512
|
+
if (!value || typeof value !== "object")
|
|
513
|
+
return [];
|
|
514
|
+
const record = value;
|
|
515
|
+
const candidates = [];
|
|
516
|
+
const directCandidate = normalizeTokenUsageCandidate(record);
|
|
517
|
+
if (directCandidate) {
|
|
518
|
+
candidates.push(directCandidate);
|
|
519
|
+
}
|
|
520
|
+
for (const nestedValue of Object.values(record)) {
|
|
521
|
+
if (!nestedValue || typeof nestedValue !== "object")
|
|
522
|
+
continue;
|
|
523
|
+
candidates.push(...collectTokenUsageCandidates(nestedValue));
|
|
524
|
+
}
|
|
525
|
+
return candidates;
|
|
526
|
+
}
|
|
527
|
+
function normalizeTokenUsageCandidate(value) {
|
|
528
|
+
const hasRelevantKey = Object.keys(value).some((key) => TOKEN_USAGE_NUMERIC_KEYS.has(key));
|
|
529
|
+
if (!hasRelevantKey)
|
|
530
|
+
return null;
|
|
531
|
+
const usage = {
|
|
532
|
+
inputTokens: readNumericField(value, ["input_tokens", "prompt_tokens", "inputTokens", "promptTokens"]),
|
|
533
|
+
outputTokens: readNumericField(value, ["output_tokens", "completion_tokens", "outputTokens", "completionTokens"]),
|
|
534
|
+
totalTokens: readNumericField(value, ["total_tokens", "totalTokens"]),
|
|
535
|
+
reasoningTokens: readNumericField(value, ["reasoning_tokens", "reasoningTokens"]),
|
|
536
|
+
cacheReadTokens: readNumericField(value, ["cached_input_tokens", "cache_read_input_tokens", "cacheReadInputTokens", "cacheReadTokens"]),
|
|
537
|
+
cacheWriteTokens: readNumericField(value, ["cache_creation_input_tokens", "cache_write_input_tokens", "cacheWriteInputTokens", "cacheWriteTokens"]),
|
|
538
|
+
};
|
|
539
|
+
if (usage.totalTokens <= 0) {
|
|
540
|
+
usage.totalTokens = usage.inputTokens + usage.outputTokens;
|
|
541
|
+
}
|
|
542
|
+
return hasTokenUsage(usage) ? usage : null;
|
|
543
|
+
}
|
|
544
|
+
function extractTokenUsageFromText(text) {
|
|
545
|
+
const usage = emptyTokenUsage();
|
|
546
|
+
const patterns = [
|
|
547
|
+
["inputTokens", [/\binput[_ ]tokens?\b[^0-9]*(\d+)/i, /\bprompt[_ ]tokens?\b[^0-9]*(\d+)/i]],
|
|
548
|
+
["outputTokens", [/\boutput[_ ]tokens?\b[^0-9]*(\d+)/i, /\bcompletion[_ ]tokens?\b[^0-9]*(\d+)/i]],
|
|
549
|
+
["totalTokens", [/\btotal[_ ]tokens?\b[^0-9]*(\d+)/i]],
|
|
550
|
+
["reasoningTokens", [/\breasoning[_ ]tokens?\b[^0-9]*(\d+)/i]],
|
|
551
|
+
["cacheReadTokens", [/\bcache(?:d|[_ ]read(?:[_ ]input)?)?[_ ]tokens?\b[^0-9]*(\d+)/i]],
|
|
552
|
+
["cacheWriteTokens", [/\bcache(?:[_ ]creation|[_ ]write(?:[_ ]input)?)?[_ ]tokens?\b[^0-9]*(\d+)/i]],
|
|
553
|
+
];
|
|
554
|
+
for (const [field, regexes] of patterns) {
|
|
555
|
+
for (const regex of regexes) {
|
|
556
|
+
const match = text.match(regex);
|
|
557
|
+
if (!match)
|
|
558
|
+
continue;
|
|
559
|
+
usage[field] = parseInt(match[1] ?? "0", 10) || 0;
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (usage.totalTokens <= 0) {
|
|
564
|
+
usage.totalTokens = usage.inputTokens + usage.outputTokens;
|
|
565
|
+
}
|
|
566
|
+
return hasTokenUsage(usage) ? usage : null;
|
|
567
|
+
}
|
|
568
|
+
function readNumericField(value, keys) {
|
|
569
|
+
for (const key of keys) {
|
|
570
|
+
const raw = value[key];
|
|
571
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
572
|
+
return Math.max(0, Math.trunc(raw));
|
|
573
|
+
}
|
|
574
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
575
|
+
const parsed = Number(raw);
|
|
576
|
+
if (Number.isFinite(parsed)) {
|
|
577
|
+
return Math.max(0, Math.trunc(parsed));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return 0;
|
|
582
|
+
}
|
|
583
|
+
export async function executeProviderCommandWithRetries(input) {
|
|
584
|
+
let attempt = 0;
|
|
585
|
+
let lastError;
|
|
586
|
+
while (attempt <= input.retryCount) {
|
|
587
|
+
try {
|
|
588
|
+
return await executeProviderCommand(input, attempt);
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
lastError = error;
|
|
592
|
+
if (!(error instanceof ProviderRunError) || !RETRYABLE_RUN_ERROR_CODES.has(error.code) || attempt >= input.retryCount) {
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
attempt += 1;
|
|
596
|
+
const note = `Retrying ${input.provider} after ${error.code} (${attempt}/${input.retryCount})\n`;
|
|
597
|
+
await appendFileSafe(input.logFile, note);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
601
|
+
}
|
|
602
|
+
export async function executeProviderCommand(input, attempt = 0) {
|
|
603
|
+
const { provider, bin, model, cwd, promptText, promptFile, summaryFile, logFile, timeoutSeconds, approveMcps, sandboxMode } = input;
|
|
604
|
+
const command = buildProviderCommand(provider, bin, model, cwd, promptText, summaryFile, approveMcps, sandboxMode, {
|
|
605
|
+
background: input.background,
|
|
606
|
+
permissionMode: input.permissionMode,
|
|
607
|
+
});
|
|
608
|
+
const commandPreview = buildCommandPreview(command, provider);
|
|
609
|
+
const timeoutMs = timeoutSeconds * 1_000;
|
|
610
|
+
const MAX_OUTPUT_BYTES = 50 * 1024 * 1024; // 50 MB cap per stream
|
|
611
|
+
const { createWriteStream } = await import("node:fs");
|
|
612
|
+
const outcome = await new Promise((resolve, reject) => {
|
|
613
|
+
let child;
|
|
614
|
+
try {
|
|
615
|
+
child = spawn(command.command, command.args, {
|
|
616
|
+
cwd,
|
|
617
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
618
|
+
windowsHide: true,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
reject(new ProviderRunError("PROVIDER_SPAWN_FAILED", `Failed to launch ${provider} via ${command.command}: ${error instanceof Error ? error.message : String(error)}`));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const err = [];
|
|
626
|
+
let errSize = 0;
|
|
627
|
+
let didTimeout = false;
|
|
628
|
+
let settled = false;
|
|
629
|
+
const header = attempt > 0 ? `# Attempt ${attempt + 1}\n` : "";
|
|
630
|
+
const logStream = createWriteStream(logFile, { flags: "a" });
|
|
631
|
+
logStream.on("error", () => undefined);
|
|
632
|
+
if (header)
|
|
633
|
+
logStream.write(header);
|
|
634
|
+
child.stdout.on("data", (chunk) => {
|
|
635
|
+
try {
|
|
636
|
+
logStream.write(chunk);
|
|
637
|
+
}
|
|
638
|
+
catch { /* pipe broken */ }
|
|
639
|
+
});
|
|
640
|
+
child.stderr.on("data", (chunk) => {
|
|
641
|
+
const buf = Buffer.from(chunk);
|
|
642
|
+
if (errSize < MAX_OUTPUT_BYTES) {
|
|
643
|
+
err.push(buf);
|
|
644
|
+
errSize += buf.length;
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
const timer = setTimeout(() => {
|
|
648
|
+
didTimeout = true;
|
|
649
|
+
child.kill("SIGTERM");
|
|
650
|
+
setTimeout(() => { try {
|
|
651
|
+
child.kill("SIGKILL");
|
|
652
|
+
}
|
|
653
|
+
catch { } }, 5_000).unref?.();
|
|
654
|
+
}, timeoutMs);
|
|
655
|
+
timer.unref?.();
|
|
656
|
+
child.stdin.on("error", () => undefined);
|
|
657
|
+
if (provider === "codex") {
|
|
658
|
+
child.stdin.write(promptText);
|
|
659
|
+
}
|
|
660
|
+
child.stdin.end();
|
|
661
|
+
child.on("error", (error) => {
|
|
662
|
+
if (settled)
|
|
663
|
+
return;
|
|
664
|
+
settled = true;
|
|
665
|
+
clearTimeout(timer);
|
|
666
|
+
const done = new Promise((res) => {
|
|
667
|
+
logStream.on("finish", res);
|
|
668
|
+
logStream.on("error", res);
|
|
669
|
+
});
|
|
670
|
+
logStream.end();
|
|
671
|
+
done.then(() => {
|
|
672
|
+
reject(new ProviderRunError("PROVIDER_SPAWN_FAILED", `Failed to launch ${provider} via ${command.command}: ${error.message}`));
|
|
673
|
+
}).catch(() => {
|
|
674
|
+
reject(new ProviderRunError("PROVIDER_SPAWN_FAILED", `Failed to launch ${provider} via ${command.command}: ${error.message}`));
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
child.on("close", (code, signal) => {
|
|
678
|
+
if (settled)
|
|
679
|
+
return;
|
|
680
|
+
settled = true;
|
|
681
|
+
clearTimeout(timer);
|
|
682
|
+
const stderrText = Buffer.concat(err).toString("utf8");
|
|
683
|
+
if (stderrText.trim()) {
|
|
684
|
+
logStream.write(`\n--- stderr ---\n${stderrText}`);
|
|
685
|
+
}
|
|
686
|
+
const streamDone = new Promise((res) => {
|
|
687
|
+
logStream.on("finish", res);
|
|
688
|
+
logStream.on("error", res);
|
|
689
|
+
});
|
|
690
|
+
logStream.end();
|
|
691
|
+
const stderrWrite = stderrText.trim()
|
|
692
|
+
? appendFileSafe(logFile.replace(/\.log$/, ".stderr"), stderrText)
|
|
693
|
+
: Promise.resolve();
|
|
694
|
+
Promise.all([streamDone, stderrWrite]).then(() => {
|
|
695
|
+
if (provider !== "codex") {
|
|
696
|
+
return readFile(logFile, "utf8").then((content) => writeFile(summaryFile, content)).catch(() => undefined);
|
|
697
|
+
}
|
|
698
|
+
else if (!existsSync(summaryFile)) {
|
|
699
|
+
return readFile(logFile, "utf8").then((content) => writeFile(summaryFile, content)).catch(() => undefined);
|
|
700
|
+
}
|
|
701
|
+
return undefined;
|
|
702
|
+
}).then(() => {
|
|
703
|
+
if (didTimeout) {
|
|
704
|
+
reject(new ProviderRunError("PROVIDER_TIMEOUT", `${provider} exceeded the ${timeoutSeconds}s timeout. Increase --timeout or reduce prompt scope.`));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const exitCode = typeof code === "number" ? code : (signal === "SIGTERM" ? 143 : signal === "SIGKILL" ? 137 : 1);
|
|
708
|
+
resolve({ exitCode });
|
|
709
|
+
}).catch(reject);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
if (provider !== "codex" && existsSync(logFile) && !existsSync(summaryFile)) {
|
|
713
|
+
await writeFile(summaryFile, await readFile(logFile, "utf8"));
|
|
714
|
+
}
|
|
715
|
+
if (provider === "codex" && !existsSync(summaryFile)) {
|
|
716
|
+
await writeFile(summaryFile, `No summary output produced for prompt file ${promptFile}\n`);
|
|
717
|
+
}
|
|
718
|
+
if (outcome.exitCode !== 0 && existsSync(summaryFile)) {
|
|
719
|
+
const summaryContent = await readFile(summaryFile, "utf8");
|
|
720
|
+
if (summaryContent.startsWith("[stderr]") && summaryContent.includes("ERROR:")) {
|
|
721
|
+
const errorPrefix = `# Run Failed (exit code ${outcome.exitCode})\n\nThe ${provider} provider reported an error. See the full log for details:\n ${logFile}\n\n---\n\n`;
|
|
722
|
+
await writeFile(summaryFile, errorPrefix + summaryContent);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return { exitCode: outcome.exitCode, commandPreview };
|
|
726
|
+
}
|
|
727
|
+
export function buildProviderCommand(provider, bin, model, cwd, promptText, summaryFile, approveMcps, sandboxMode, options) {
|
|
728
|
+
if (provider === "codex") {
|
|
729
|
+
return {
|
|
730
|
+
command: bin,
|
|
731
|
+
args: ["exec", "-m", model, "--sandbox", sandboxMode, "-C", cwd, "--color", "never", "-o", summaryFile, "-"],
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
if (provider === "cursor") {
|
|
735
|
+
const args = options?.background
|
|
736
|
+
? ["--background", "--force", "--trust", "--workspace", cwd, "--output-format", "stream-json"]
|
|
737
|
+
: ["--print", "--force", "--trust", "--workspace", cwd, "--output-format", "stream-json"];
|
|
738
|
+
if (model !== "auto") {
|
|
739
|
+
args.push("--model", model);
|
|
740
|
+
}
|
|
741
|
+
if (approveMcps) {
|
|
742
|
+
args.push("--approve-mcps");
|
|
743
|
+
}
|
|
744
|
+
args.push(promptText);
|
|
745
|
+
return { command: bin, args };
|
|
746
|
+
}
|
|
747
|
+
if (provider === "claude") {
|
|
748
|
+
const permissionMode = options?.permissionMode ?? "acceptEdits";
|
|
749
|
+
return {
|
|
750
|
+
command: bin,
|
|
751
|
+
args: ["-p", "--model", model, "--output-format", "stream-json", "--permission-mode", permissionMode, "--add-dir", cwd, promptText],
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
command: bin,
|
|
756
|
+
args: ["-p", promptText, "--model", model, "--output-format=json", "--add-dir", cwd, "--no-color"],
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
export function resolveRunProvider(requested, providers, providerOrder) {
|
|
760
|
+
if (requested !== "router") {
|
|
761
|
+
return requested;
|
|
762
|
+
}
|
|
763
|
+
for (const provider of providerOrder) {
|
|
764
|
+
const health = providers.find((item) => item.provider === provider);
|
|
765
|
+
if (health?.available) {
|
|
766
|
+
return provider;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
for (const provider of DEFAULT_PROVIDER_ORDER) {
|
|
770
|
+
const health = providers.find((item) => item.provider === provider);
|
|
771
|
+
if (health?.available)
|
|
772
|
+
return provider;
|
|
773
|
+
}
|
|
774
|
+
return providerOrder[0] ?? "codex";
|
|
775
|
+
}
|
|
776
|
+
export function resolveTimeoutSeconds(override, fallback) {
|
|
777
|
+
const MAX_TIMEOUT_SECONDS = 86_400;
|
|
778
|
+
const raw = Number(override);
|
|
779
|
+
if (Number.isFinite(raw) && raw > 0) {
|
|
780
|
+
return Math.min(Math.trunc(raw), MAX_TIMEOUT_SECONDS);
|
|
781
|
+
}
|
|
782
|
+
return Math.min(fallback, MAX_TIMEOUT_SECONDS);
|
|
783
|
+
}
|
|
784
|
+
function resolveProviderBin(provider, env) {
|
|
785
|
+
const requested = readProviderBinOverride(provider, env);
|
|
786
|
+
if (requested) {
|
|
787
|
+
return requested;
|
|
788
|
+
}
|
|
789
|
+
const candidates = providerCandidates(provider);
|
|
790
|
+
const match = candidates.find((candidate) => isExecutableAvailable(candidate));
|
|
791
|
+
return match ?? providerDefaultBin(provider);
|
|
792
|
+
}
|
|
793
|
+
function readProviderBinOverride(provider, env) {
|
|
794
|
+
const aliases = providerBinEnvAliases(provider);
|
|
795
|
+
for (const name of aliases) {
|
|
796
|
+
const value = String(env[name] ?? "").trim();
|
|
797
|
+
if (value) {
|
|
798
|
+
return value;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return "";
|
|
802
|
+
}
|
|
803
|
+
function providerBinEnvAliases(provider) {
|
|
804
|
+
if (provider === "codex")
|
|
805
|
+
return ["CODEX_BIN", "OPENAI_CODEX_BIN"];
|
|
806
|
+
if (provider === "cursor")
|
|
807
|
+
return ["AGENT_BIN", "CURSOR_AGENT_BIN", "CURSOR_BIN"];
|
|
808
|
+
if (provider === "claude")
|
|
809
|
+
return ["CLAUDE_BIN", "CLAUDE_CODE_BIN"];
|
|
810
|
+
return ["COPILOT_BIN", "GITHUB_COPILOT_BIN", "GH_COPILOT_BIN"];
|
|
811
|
+
}
|
|
812
|
+
function providerDefaultBin(provider) {
|
|
813
|
+
if (provider === "codex")
|
|
814
|
+
return "codex";
|
|
815
|
+
if (provider === "cursor")
|
|
816
|
+
return "agent";
|
|
817
|
+
if (provider === "claude")
|
|
818
|
+
return "claude";
|
|
819
|
+
return "copilot";
|
|
820
|
+
}
|
|
821
|
+
function isWSL() {
|
|
822
|
+
if (process.platform !== "linux")
|
|
823
|
+
return false;
|
|
824
|
+
try {
|
|
825
|
+
return existsSync("/proc/version") && readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft");
|
|
826
|
+
}
|
|
827
|
+
catch {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function providerCandidates(provider) {
|
|
832
|
+
const home = process.env.HOME || process.env.USERPROFILE || (process.env.HOMEDRIVE && process.env.HOMEPATH ? `${process.env.HOMEDRIVE}${process.env.HOMEPATH}` : "");
|
|
833
|
+
const user = process.env.USER || process.env.USERNAME || "user";
|
|
834
|
+
const wsl = isWSL();
|
|
835
|
+
if (provider === "codex") {
|
|
836
|
+
return [
|
|
837
|
+
"codex",
|
|
838
|
+
"codex.exe",
|
|
839
|
+
path.join(home, ".local", "bin", "codex"),
|
|
840
|
+
"/usr/local/bin/codex",
|
|
841
|
+
"/opt/homebrew/bin/codex",
|
|
842
|
+
...(wsl ? [`/mnt/c/Users/${user}/AppData/Local/OpenAI/Codex/bin/codex.exe`] : []),
|
|
843
|
+
];
|
|
844
|
+
}
|
|
845
|
+
if (provider === "cursor") {
|
|
846
|
+
return [
|
|
847
|
+
"agent",
|
|
848
|
+
"cursor-agent",
|
|
849
|
+
path.join(home, ".local", "bin", "agent"),
|
|
850
|
+
path.join(home, ".cursor", "bin", "agent"),
|
|
851
|
+
"/usr/local/bin/agent",
|
|
852
|
+
"/usr/local/bin/cursor-agent",
|
|
853
|
+
...(wsl ? [
|
|
854
|
+
`/mnt/c/Users/${user}/AppData/Local/Programs/cursor/resources/app/bin/agent`,
|
|
855
|
+
`/mnt/c/Users/${user}/AppData/Local/Cursor/resources/app/bin/agent`,
|
|
856
|
+
] : []),
|
|
857
|
+
];
|
|
858
|
+
}
|
|
859
|
+
if (provider === "claude") {
|
|
860
|
+
return [
|
|
861
|
+
"claude",
|
|
862
|
+
path.join(home, ".local", "bin", "claude"),
|
|
863
|
+
path.join(home, ".claude", "local", "claude"),
|
|
864
|
+
"/usr/local/bin/claude",
|
|
865
|
+
"/opt/homebrew/bin/claude",
|
|
866
|
+
];
|
|
867
|
+
}
|
|
868
|
+
return [
|
|
869
|
+
"copilot",
|
|
870
|
+
"copilot.exe",
|
|
871
|
+
"gh-copilot",
|
|
872
|
+
path.join(home, ".local", "bin", "copilot"),
|
|
873
|
+
"/usr/local/bin/copilot",
|
|
874
|
+
"/opt/homebrew/bin/copilot",
|
|
875
|
+
...(wsl ? [`/mnt/c/Users/${user}/AppData/Local/Microsoft/WinGet/Links/copilot.exe`] : []),
|
|
876
|
+
];
|
|
877
|
+
}
|
|
878
|
+
function isExecutableAvailable(command) {
|
|
879
|
+
if (!command.trim())
|
|
880
|
+
return false;
|
|
881
|
+
if (/[\x00-\x1f]/.test(command))
|
|
882
|
+
return false;
|
|
883
|
+
if (/[;&|`$(){}]/.test(command))
|
|
884
|
+
return false;
|
|
885
|
+
try {
|
|
886
|
+
if (path.isAbsolute(command)) {
|
|
887
|
+
return existsSync(command);
|
|
888
|
+
}
|
|
889
|
+
const result = spawnSync(command, ["--version"], { stdio: "ignore", windowsHide: true, timeout: 8_000 });
|
|
890
|
+
return !result.error;
|
|
891
|
+
}
|
|
892
|
+
catch {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function readProviderVersion(_provider, bin, cwd) {
|
|
897
|
+
const result = spawnSync(bin, ["--version"], { cwd, encoding: "utf8", windowsHide: true, timeout: 10_000 });
|
|
898
|
+
if (result.status !== 0) {
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
|
|
902
|
+
return output.split(/\r?\n/).find((line) => {
|
|
903
|
+
const t = line.trim();
|
|
904
|
+
return t.length > 0 && !t.toLowerCase().startsWith("warning") && !t.toLowerCase().startsWith("deprecat");
|
|
905
|
+
}) ?? output.split(/\r?\n/).find((line) => line.trim().length > 0) ?? null;
|
|
906
|
+
}
|
|
907
|
+
function installHintForProvider(provider) {
|
|
908
|
+
if (provider === "codex")
|
|
909
|
+
return "Install OpenAI Codex CLI and ensure `codex` is on PATH.";
|
|
910
|
+
if (provider === "cursor")
|
|
911
|
+
return "Install Cursor Agent CLI (`agent`) and ensure PATH includes it.";
|
|
912
|
+
if (provider === "claude")
|
|
913
|
+
return "Install Claude Code CLI (`claude`) and ensure PATH includes it.";
|
|
914
|
+
return "Install GitHub Copilot CLI (`copilot`) and ensure PATH includes it.";
|
|
915
|
+
}
|
|
916
|
+
function parseProviderOrderEnv(value) {
|
|
917
|
+
if (!value)
|
|
918
|
+
return [];
|
|
919
|
+
return value
|
|
920
|
+
.split(",")
|
|
921
|
+
.map((item) => item.trim())
|
|
922
|
+
.filter(Boolean)
|
|
923
|
+
.map((item) => parseConcreteProvider(item))
|
|
924
|
+
.filter((provider) => provider !== null)
|
|
925
|
+
.filter((provider, index, arr) => arr.indexOf(provider) === index);
|
|
926
|
+
}
|
|
927
|
+
function normalizeOptionalAgent(value) {
|
|
928
|
+
const raw = value?.trim();
|
|
929
|
+
if (!raw)
|
|
930
|
+
return undefined;
|
|
931
|
+
return normalizeOrchestrationAgent(raw);
|
|
932
|
+
}
|
|
933
|
+
function parsePositiveIntEnv(value) {
|
|
934
|
+
const raw = Number(value);
|
|
935
|
+
if (!Number.isFinite(raw) || raw <= 0)
|
|
936
|
+
return undefined;
|
|
937
|
+
return Math.trunc(raw);
|
|
938
|
+
}
|
|
939
|
+
function parseNonNegativeIntEnv(value) {
|
|
940
|
+
const raw = Number(value);
|
|
941
|
+
if (!Number.isFinite(raw) || raw < 0)
|
|
942
|
+
return undefined;
|
|
943
|
+
return Math.trunc(raw);
|
|
944
|
+
}
|
|
945
|
+
function parseBooleanEnv(value) {
|
|
946
|
+
const raw = value?.trim().toLowerCase();
|
|
947
|
+
if (!raw)
|
|
948
|
+
return undefined;
|
|
949
|
+
if (["1", "true", "yes", "on"].includes(raw))
|
|
950
|
+
return true;
|
|
951
|
+
if (["0", "false", "no", "off"].includes(raw))
|
|
952
|
+
return false;
|
|
953
|
+
return undefined;
|
|
954
|
+
}
|
|
955
|
+
function toGitignorePath(cwd, absolutePath) {
|
|
956
|
+
const relative = path.relative(cwd, absolutePath).replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
957
|
+
return relative || DEFAULT_RUN_ARTIFACTS_DIR;
|
|
958
|
+
}
|
|
959
|
+
export async function ensureGitignoreEntry(cwd, entry) {
|
|
960
|
+
const gitignorePath = path.resolve(cwd, ".gitignore");
|
|
961
|
+
const existing = existsSync(gitignorePath) ? await readFile(gitignorePath, "utf8") : "";
|
|
962
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
963
|
+
const lines = existing.split(/\r?\n/).map((line) => line.trim());
|
|
964
|
+
if (lines.includes(entry))
|
|
965
|
+
return;
|
|
966
|
+
const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n") && !existing.endsWith("\r\n");
|
|
967
|
+
const prefix = needsLeadingNewline ? eol : "";
|
|
968
|
+
await writeFile(gitignorePath, `${existing}${prefix}${entry}${eol}`);
|
|
969
|
+
}
|
|
970
|
+
function sanitizeModelName(value) {
|
|
971
|
+
return value.replace(/[^\w./-]/g, "").slice(0, 64);
|
|
972
|
+
}
|
|
973
|
+
function readModelOverridesFromEnv(env) {
|
|
974
|
+
const globalModel = env.PROMPTS_GPT_MODEL?.trim();
|
|
975
|
+
const entries = [
|
|
976
|
+
["codex", env.PROMPTS_GPT_RUN_MODEL_CODEX],
|
|
977
|
+
["cursor", env.PROMPTS_GPT_RUN_MODEL_CURSOR],
|
|
978
|
+
["claude", env.PROMPTS_GPT_RUN_MODEL_CLAUDE],
|
|
979
|
+
["copilot", env.PROMPTS_GPT_RUN_MODEL_COPILOT],
|
|
980
|
+
];
|
|
981
|
+
const overrides = {};
|
|
982
|
+
for (const [provider, value] of entries) {
|
|
983
|
+
const trimmed = value?.trim() || globalModel;
|
|
984
|
+
if (trimmed) {
|
|
985
|
+
overrides[provider] = sanitizeModelName(trimmed);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return overrides;
|
|
989
|
+
}
|
|
990
|
+
async function resolveBatchPromptFiles(cwd, input, config) {
|
|
991
|
+
if (Array.isArray(input.promptFiles) && input.promptFiles.length > 0) {
|
|
992
|
+
return [...new Set(input.promptFiles.map((file) => path.resolve(cwd, file)))];
|
|
993
|
+
}
|
|
994
|
+
if (input.manifestPath) {
|
|
995
|
+
return readPromptFilesFromManifest(path.resolve(cwd, input.manifestPath), cwd);
|
|
996
|
+
}
|
|
997
|
+
if (config?.batchDefaults.promptFiles.length) {
|
|
998
|
+
return [...new Set(config.batchDefaults.promptFiles.map((file) => path.resolve(file)))];
|
|
999
|
+
}
|
|
1000
|
+
const manifestPath = path.resolve(cwd, config?.batchDefaults.manifestPath || ".prompts-gpt/manifest.json");
|
|
1001
|
+
return readPromptFilesFromManifest(manifestPath, cwd);
|
|
1002
|
+
}
|
|
1003
|
+
async function readPromptFilesFromManifest(manifestPath, cwd, options = {}) {
|
|
1004
|
+
if (!existsSync(manifestPath)) {
|
|
1005
|
+
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
1006
|
+
}
|
|
1007
|
+
let parsed;
|
|
1008
|
+
try {
|
|
1009
|
+
parsed = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1010
|
+
}
|
|
1011
|
+
catch {
|
|
1012
|
+
throw new Error(`Manifest contains invalid JSON: ${manifestPath}`);
|
|
1013
|
+
}
|
|
1014
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.prompts)) {
|
|
1015
|
+
throw new Error(`Manifest has invalid structure (expected { prompts: [...] }): ${manifestPath}`);
|
|
1016
|
+
}
|
|
1017
|
+
const manifestDir = path.dirname(manifestPath);
|
|
1018
|
+
const promptFiles = (parsed.prompts ?? [])
|
|
1019
|
+
.map((item) => {
|
|
1020
|
+
if (typeof item?.recommendedPath === "string" && item.recommendedPath.trim()) {
|
|
1021
|
+
return path.resolve(cwd, item.recommendedPath);
|
|
1022
|
+
}
|
|
1023
|
+
if (typeof item?.file === "string" && item.file.trim()) {
|
|
1024
|
+
return path.resolve(manifestDir, item.file);
|
|
1025
|
+
}
|
|
1026
|
+
return "";
|
|
1027
|
+
})
|
|
1028
|
+
.filter(Boolean);
|
|
1029
|
+
if (promptFiles.length === 0) {
|
|
1030
|
+
throw new Error(`Manifest has no prompt file entries: ${manifestPath}`);
|
|
1031
|
+
}
|
|
1032
|
+
const unique = [...new Set(promptFiles)];
|
|
1033
|
+
const missing = unique.filter((f) => !existsSync(f));
|
|
1034
|
+
if (!options.allowMissing && missing.length > 0) {
|
|
1035
|
+
throw new Error(`Manifest references ${missing.length} missing file(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ` (and ${missing.length - 5} more)` : ""}`);
|
|
1036
|
+
}
|
|
1037
|
+
return unique;
|
|
1038
|
+
}
|
|
1039
|
+
export function captureWorktreeStatus(cwd) {
|
|
1040
|
+
const output = spawnSync("git", ["-C", cwd, "status", "--porcelain=v1", "-uall"], {
|
|
1041
|
+
encoding: "utf8",
|
|
1042
|
+
windowsHide: true,
|
|
1043
|
+
timeout: 30_000,
|
|
1044
|
+
});
|
|
1045
|
+
if (output.status !== 0) {
|
|
1046
|
+
return "git status unavailable\n";
|
|
1047
|
+
}
|
|
1048
|
+
return output.stdout || "";
|
|
1049
|
+
}
|
|
1050
|
+
export function buildWorktreeDelta(before, after) {
|
|
1051
|
+
if (before === after) {
|
|
1052
|
+
return "No worktree delta.\n";
|
|
1053
|
+
}
|
|
1054
|
+
return [
|
|
1055
|
+
"=== BEFORE ===",
|
|
1056
|
+
before.trimEnd(),
|
|
1057
|
+
"",
|
|
1058
|
+
"=== AFTER ===",
|
|
1059
|
+
after.trimEnd(),
|
|
1060
|
+
"",
|
|
1061
|
+
].join("\n");
|
|
1062
|
+
}
|
|
1063
|
+
function resolveConfiguredBatchDefaults(cwd, batchDefaults) {
|
|
1064
|
+
const manifestPath = typeof batchDefaults?.manifestPath === "string" && batchDefaults.manifestPath.trim()
|
|
1065
|
+
? path.resolve(cwd, batchDefaults.manifestPath.trim())
|
|
1066
|
+
: null;
|
|
1067
|
+
const promptFiles = Array.isArray(batchDefaults?.promptFiles)
|
|
1068
|
+
? batchDefaults.promptFiles
|
|
1069
|
+
.map((file) => String(file ?? "").trim())
|
|
1070
|
+
.filter(Boolean)
|
|
1071
|
+
.map((file) => path.resolve(cwd, file))
|
|
1072
|
+
: [];
|
|
1073
|
+
return {
|
|
1074
|
+
manifestPath,
|
|
1075
|
+
promptFiles: [...new Set(promptFiles)],
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
async function detectPromptSources(cwd, input) {
|
|
1079
|
+
const explicitPromptFile = input.promptFile ? path.resolve(cwd, input.promptFile) : null;
|
|
1080
|
+
const explicitPromptFiles = Array.isArray(input.promptFiles)
|
|
1081
|
+
? input.promptFiles.map((file) => path.resolve(cwd, file)).filter(Boolean)
|
|
1082
|
+
: [];
|
|
1083
|
+
if (explicitPromptFile) {
|
|
1084
|
+
return {
|
|
1085
|
+
defaultPromptFile: explicitPromptFile,
|
|
1086
|
+
manifestPath: input.manifestPath ? path.resolve(cwd, input.manifestPath) : null,
|
|
1087
|
+
promptFiles: [...new Set([explicitPromptFile, ...explicitPromptFiles])],
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
if (explicitPromptFiles.length > 0) {
|
|
1091
|
+
return {
|
|
1092
|
+
defaultPromptFile: explicitPromptFiles[0] ?? null,
|
|
1093
|
+
manifestPath: input.manifestPath ? path.resolve(cwd, input.manifestPath) : null,
|
|
1094
|
+
promptFiles: [...new Set(explicitPromptFiles)],
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
const manifestCandidate = path.resolve(cwd, input.manifestPath || ".prompts-gpt/manifest.json");
|
|
1098
|
+
if (existsSync(manifestCandidate)) {
|
|
1099
|
+
const promptFiles = await readPromptFilesFromManifest(manifestCandidate, cwd, { allowMissing: true });
|
|
1100
|
+
return {
|
|
1101
|
+
defaultPromptFile: promptFiles[0] ?? null,
|
|
1102
|
+
manifestPath: manifestCandidate,
|
|
1103
|
+
promptFiles,
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
const promptDirs = input.promptDir
|
|
1107
|
+
? [path.resolve(cwd, input.promptDir)]
|
|
1108
|
+
: [
|
|
1109
|
+
path.resolve(cwd, ".prompts-gpt"),
|
|
1110
|
+
path.resolve(cwd, ".scripts", "prompts"),
|
|
1111
|
+
path.resolve(cwd, "prompts"),
|
|
1112
|
+
path.resolve(cwd, ".prompts"),
|
|
1113
|
+
];
|
|
1114
|
+
const promptFiles = await discoverPromptFiles(promptDirs);
|
|
1115
|
+
if (promptFiles.length === 0) {
|
|
1116
|
+
return { defaultPromptFile: null, manifestPath: null, promptFiles: [] };
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
defaultPromptFile: promptFiles[0] ?? null,
|
|
1120
|
+
manifestPath: null,
|
|
1121
|
+
promptFiles,
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
async function discoverPromptFiles(promptDirs) {
|
|
1125
|
+
const files = [];
|
|
1126
|
+
for (const promptDir of promptDirs) {
|
|
1127
|
+
if (!existsSync(promptDir)) {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
const entries = await readdir(promptDir, { withFileTypes: true });
|
|
1131
|
+
for (const entry of entries) {
|
|
1132
|
+
if (!entry.isFile()) {
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
const name = entry.name.toLowerCase();
|
|
1136
|
+
if ((name.endsWith(".md") || name.endsWith(".txt")) && name !== "readme.md") {
|
|
1137
|
+
files.push(path.join(promptDir, entry.name));
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return [...new Set(files)].sort();
|
|
1142
|
+
}
|
|
1143
|
+
function normalizeProviderOrderInput(providerOrder, providers) {
|
|
1144
|
+
const normalized = Array.isArray(providerOrder) && providerOrder.length > 0
|
|
1145
|
+
? providerOrder
|
|
1146
|
+
.map((provider) => parseConcreteProvider(String(provider)))
|
|
1147
|
+
.filter((provider) => provider !== null)
|
|
1148
|
+
: derivePreferredProviderOrder(providers);
|
|
1149
|
+
return [...new Set(normalized)].filter((provider) => DEFAULT_PROVIDER_ORDER.includes(provider));
|
|
1150
|
+
}
|
|
1151
|
+
function derivePreferredProviderOrder(providers) {
|
|
1152
|
+
const available = providers.filter((provider) => provider.available).map((provider) => provider.provider);
|
|
1153
|
+
const unavailable = DEFAULT_PROVIDER_ORDER.filter((provider) => !available.includes(provider));
|
|
1154
|
+
return [...available, ...unavailable];
|
|
1155
|
+
}
|
|
1156
|
+
function buildScaffoldModelOverrides(overrides) {
|
|
1157
|
+
const result = {};
|
|
1158
|
+
for (const provider of DEFAULT_PROVIDER_ORDER) {
|
|
1159
|
+
const override = overrides?.[provider]?.trim();
|
|
1160
|
+
if (override) {
|
|
1161
|
+
result[provider] = override;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return result;
|
|
1165
|
+
}
|
|
1166
|
+
function resolveRetryCount(value) {
|
|
1167
|
+
const raw = Number(value);
|
|
1168
|
+
if (Number.isFinite(raw) && raw >= 0) {
|
|
1169
|
+
return Math.trunc(raw);
|
|
1170
|
+
}
|
|
1171
|
+
return 0;
|
|
1172
|
+
}
|
|
1173
|
+
function toProjectConfigPath(cwd, value) {
|
|
1174
|
+
const absolute = path.isAbsolute(value) ? value : path.resolve(cwd, value);
|
|
1175
|
+
const relative = path.relative(cwd, absolute).replace(/\\/g, "/");
|
|
1176
|
+
if (!relative || relative === "") {
|
|
1177
|
+
return ".";
|
|
1178
|
+
}
|
|
1179
|
+
if (!relative.startsWith("..")) {
|
|
1180
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
1181
|
+
}
|
|
1182
|
+
const normalized = value.replace(/\\/g, "/").trim();
|
|
1183
|
+
if (!normalized)
|
|
1184
|
+
return DEFAULT_RUN_ARTIFACTS_DIR;
|
|
1185
|
+
if (path.isAbsolute(normalized))
|
|
1186
|
+
return normalized;
|
|
1187
|
+
return normalized.replace(/^\.?\//, "./");
|
|
1188
|
+
}
|
|
1189
|
+
export async function resolveDefaultPromptFile(cwd, config) {
|
|
1190
|
+
if (config.defaultPromptFile) {
|
|
1191
|
+
return config.defaultPromptFile;
|
|
1192
|
+
}
|
|
1193
|
+
if (config.batchDefaults.promptFiles.length > 0) {
|
|
1194
|
+
return config.batchDefaults.promptFiles[0];
|
|
1195
|
+
}
|
|
1196
|
+
if (config.batchDefaults.manifestPath) {
|
|
1197
|
+
const promptFiles = await readPromptFilesFromManifest(config.batchDefaults.manifestPath, cwd);
|
|
1198
|
+
if (promptFiles.length > 0) {
|
|
1199
|
+
return promptFiles[0];
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
const discovered = await detectPromptSources(cwd, {});
|
|
1203
|
+
if (discovered.defaultPromptFile && existsSync(discovered.defaultPromptFile)) {
|
|
1204
|
+
const ext = path.extname(discovered.defaultPromptFile).toLowerCase();
|
|
1205
|
+
if (ext === ".md" || ext === ".txt" || ext === "") {
|
|
1206
|
+
return discovered.defaultPromptFile;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
throw new Error("No default prompt file configured. Run `prompts-gpt list` to see available prompts, or pass `--prompt-file`.");
|
|
1210
|
+
}
|
|
1211
|
+
export function warnModelProviderMismatch(provider, model) {
|
|
1212
|
+
if (provider === "codex" && model.includes("claude")) {
|
|
1213
|
+
return `Model "${model}" is an Anthropic model but provider is codex (OpenAI). Use --agent claude instead.`;
|
|
1214
|
+
}
|
|
1215
|
+
if (provider === "claude" && (model.includes("gpt") || model.includes("codex") || model.includes("o4"))) {
|
|
1216
|
+
return `Model "${model}" is an OpenAI model but provider is claude. Use --agent codex instead.`;
|
|
1217
|
+
}
|
|
1218
|
+
if (provider === "codex" && model === "gpt-5.1-codex") {
|
|
1219
|
+
return `Model "gpt-5.1-codex" may not be available for all Codex accounts. If it fails, try --model o4-mini.`;
|
|
1220
|
+
}
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
export function assertPromptFitsLaunch(provider, promptText, promptFile) {
|
|
1224
|
+
if (!promptText || !promptText.trim()) {
|
|
1225
|
+
throw new ProviderRunError("PROMPT_EMPTY", `Prompt file ${promptFile} is empty or contains only whitespace.`);
|
|
1226
|
+
}
|
|
1227
|
+
const nullByteIndex = promptText.indexOf("\0");
|
|
1228
|
+
if (nullByteIndex !== -1) {
|
|
1229
|
+
throw new ProviderRunError("PROMPT_BINARY", `Prompt file ${promptFile} contains null bytes at position ${nullByteIndex}. It may be a binary file.`);
|
|
1230
|
+
}
|
|
1231
|
+
if (provider === "codex") {
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
const size = Buffer.byteLength(promptText, "utf8");
|
|
1235
|
+
if (size > NON_CODEX_MAX_PROMPT_BYTES) {
|
|
1236
|
+
throw new ProviderRunError("PROMPT_TOO_LARGE", `Prompt file ${promptFile} is ${size} bytes. ${provider} V1 execution passes prompt text on the command line and currently supports up to ${NON_CODEX_MAX_PROMPT_BYTES} bytes. Split the prompt, shorten it, or use codex/router.`);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function buildCommandPreview(command, provider) {
|
|
1240
|
+
const MAX_PREVIEW_ARG_LEN = 120;
|
|
1241
|
+
const safeArgs = command.args.map((arg) => {
|
|
1242
|
+
if (arg.length > MAX_PREVIEW_ARG_LEN) {
|
|
1243
|
+
return `<${provider}-prompt:${arg.length}chars>`;
|
|
1244
|
+
}
|
|
1245
|
+
let safe = arg;
|
|
1246
|
+
for (const pattern of [
|
|
1247
|
+
/pgpt_[a-zA-Z0-9]{4,}/g,
|
|
1248
|
+
/sk-[a-zA-Z0-9]{20,}/g,
|
|
1249
|
+
/ghp_[a-zA-Z0-9]{36,}/g,
|
|
1250
|
+
/ghu_[a-zA-Z0-9]{36,}/g,
|
|
1251
|
+
/Bearer\s+[a-zA-Z0-9_.-]{20,}/gi,
|
|
1252
|
+
/ANTHROPIC_API_KEY=[^\s]+/gi,
|
|
1253
|
+
/OPENAI_API_KEY=[^\s]+/gi,
|
|
1254
|
+
/xai-[a-zA-Z0-9]{20,}/g,
|
|
1255
|
+
]) {
|
|
1256
|
+
safe = safe.replace(pattern, (match) => `${match.slice(0, 6)}***`);
|
|
1257
|
+
}
|
|
1258
|
+
return safe;
|
|
1259
|
+
});
|
|
1260
|
+
return [command.command, ...safeArgs].join(" ");
|
|
1261
|
+
}
|
|
1262
|
+
export function formatCombinedOutput(stdoutText, stderrText) {
|
|
1263
|
+
const out = stdoutText.trimEnd();
|
|
1264
|
+
const err = stderrText.trimEnd();
|
|
1265
|
+
if (!out && !err)
|
|
1266
|
+
return "No provider output captured.\n";
|
|
1267
|
+
if (!err)
|
|
1268
|
+
return `${out}\n`;
|
|
1269
|
+
if (!out)
|
|
1270
|
+
return `[stderr]\n${err}\n`;
|
|
1271
|
+
return `${out}\n\n[stderr]\n${err}\n`;
|
|
1272
|
+
}
|
|
1273
|
+
export async function appendFileSafe(filePath, content) {
|
|
1274
|
+
const { appendFile: nodeAppend } = await import("node:fs/promises");
|
|
1275
|
+
await nodeAppend(filePath, content).catch(async (err) => {
|
|
1276
|
+
if (err.code === "ENOENT") {
|
|
1277
|
+
await writeFile(filePath, content);
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
/** Discover all runnable assets in the workspace: prompts, sweeps, agent files. */
|
|
1282
|
+
export async function discoverWorkspaceAssets(cwd = process.cwd()) {
|
|
1283
|
+
const resolvedCwd = path.resolve(cwd);
|
|
1284
|
+
const configPath = path.resolve(resolvedCwd, DEFAULT_RUN_CONFIG_PATH);
|
|
1285
|
+
const manifestPath = path.resolve(resolvedCwd, ".prompts-gpt/manifest.json");
|
|
1286
|
+
const credsPath = path.resolve(resolvedCwd, ".prompts-gpt/.credentials.json");
|
|
1287
|
+
const prompts = [];
|
|
1288
|
+
const sweeps = [];
|
|
1289
|
+
const agents = [];
|
|
1290
|
+
if (existsSync(manifestPath)) {
|
|
1291
|
+
try {
|
|
1292
|
+
const raw = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1293
|
+
for (const p of raw.prompts ?? []) {
|
|
1294
|
+
if (p.file) {
|
|
1295
|
+
prompts.push({
|
|
1296
|
+
file: p.file,
|
|
1297
|
+
title: p.title ?? p.slug ?? p.file,
|
|
1298
|
+
slug: p.slug ?? p.file.replace(/\.md$/, ""),
|
|
1299
|
+
source: p.source ?? "library",
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
catch { /* malformed manifest */ }
|
|
1305
|
+
}
|
|
1306
|
+
if (prompts.length === 0) {
|
|
1307
|
+
const promptDir = path.resolve(resolvedCwd, ".prompts-gpt");
|
|
1308
|
+
if (existsSync(promptDir)) {
|
|
1309
|
+
try {
|
|
1310
|
+
const entries = await readdir(promptDir, { withFileTypes: true });
|
|
1311
|
+
for (const entry of entries) {
|
|
1312
|
+
if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
1313
|
+
prompts.push({
|
|
1314
|
+
file: entry.name,
|
|
1315
|
+
title: entry.name.replace(/\.md$/, "").replace(/-/g, " "),
|
|
1316
|
+
slug: entry.name.replace(/\.md$/, ""),
|
|
1317
|
+
source: "local",
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
catch { /* dir read failed */ }
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const sweepDir = path.resolve(resolvedCwd, ".prompts-gpt/sweeps");
|
|
1326
|
+
if (existsSync(sweepDir)) {
|
|
1327
|
+
try {
|
|
1328
|
+
const entries = await readdir(sweepDir, { withFileTypes: true });
|
|
1329
|
+
for (const entry of entries) {
|
|
1330
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1331
|
+
sweeps.push({
|
|
1332
|
+
file: `.prompts-gpt/sweeps/${entry.name}`,
|
|
1333
|
+
name: entry.name.replace(/\.md$/, ""),
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
catch (err) {
|
|
1339
|
+
const code = err.code;
|
|
1340
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
1341
|
+
throw new Error(`Cannot read sweep directory ${sweepDir}: permission denied.`);
|
|
1342
|
+
}
|
|
1343
|
+
if (code !== "ENOENT") {
|
|
1344
|
+
throw err;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const agentFiles = [
|
|
1349
|
+
["AGENTS.md", "codex"],
|
|
1350
|
+
["CLAUDE.md", "claude-code"],
|
|
1351
|
+
["GEMINI.md", "gemini-cli"],
|
|
1352
|
+
["AGENT.md", "amp"],
|
|
1353
|
+
];
|
|
1354
|
+
for (const [file, target] of agentFiles) {
|
|
1355
|
+
if (existsSync(path.resolve(resolvedCwd, file))) {
|
|
1356
|
+
agents.push({ file, target });
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const agentDirs = [
|
|
1360
|
+
[".cursor/rules", "cursor"],
|
|
1361
|
+
[".github/prompts", "copilot"],
|
|
1362
|
+
[".continue/rules", "continue"],
|
|
1363
|
+
[".windsurf/rules", "windsurf"],
|
|
1364
|
+
[".clinerules", "cline"],
|
|
1365
|
+
[".junie", "junie"],
|
|
1366
|
+
];
|
|
1367
|
+
for (const [dir, target] of agentDirs) {
|
|
1368
|
+
const dirPath = path.resolve(resolvedCwd, dir);
|
|
1369
|
+
if (existsSync(dirPath)) {
|
|
1370
|
+
try {
|
|
1371
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
1372
|
+
const pgptFiles = entries.filter((e) => e.isFile() && e.name.startsWith("prompts-gpt-"));
|
|
1373
|
+
if (pgptFiles.length > 0) {
|
|
1374
|
+
agents.push({ file: dir, target });
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
catch { /* dir read failed */ }
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return {
|
|
1381
|
+
prompts,
|
|
1382
|
+
sweeps,
|
|
1383
|
+
agents,
|
|
1384
|
+
configFound: existsSync(configPath),
|
|
1385
|
+
manifestFound: existsSync(manifestPath),
|
|
1386
|
+
credentialsFound: existsSync(credsPath),
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
/** Validate a run config file and return structured diagnostics. */
|
|
1390
|
+
export async function validateRunConfig(cwd = process.cwd()) {
|
|
1391
|
+
const configPath = path.resolve(cwd, DEFAULT_RUN_CONFIG_PATH);
|
|
1392
|
+
const errors = [];
|
|
1393
|
+
const warnings = [];
|
|
1394
|
+
if (!existsSync(configPath)) {
|
|
1395
|
+
return { valid: true, configPath, errors, warnings: ["No config file found — using defaults."] };
|
|
1396
|
+
}
|
|
1397
|
+
let raw;
|
|
1398
|
+
try {
|
|
1399
|
+
raw = await readFile(configPath, "utf8");
|
|
1400
|
+
}
|
|
1401
|
+
catch (err) {
|
|
1402
|
+
errors.push(`Cannot read config file: ${err instanceof Error ? err.message : String(err)}`);
|
|
1403
|
+
return { valid: false, configPath, errors, warnings };
|
|
1404
|
+
}
|
|
1405
|
+
let parsed;
|
|
1406
|
+
try {
|
|
1407
|
+
parsed = JSON.parse(raw);
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
errors.push("Config file contains invalid JSON.");
|
|
1411
|
+
return { valid: false, configPath, errors, warnings };
|
|
1412
|
+
}
|
|
1413
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1414
|
+
errors.push("Config must be a JSON object.");
|
|
1415
|
+
return { valid: false, configPath, errors, warnings };
|
|
1416
|
+
}
|
|
1417
|
+
if (parsed.providerOrder !== undefined) {
|
|
1418
|
+
if (!Array.isArray(parsed.providerOrder)) {
|
|
1419
|
+
errors.push("providerOrder must be an array of provider names.");
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
const validProviders = new Set(DEFAULT_PROVIDER_ORDER);
|
|
1423
|
+
for (const item of parsed.providerOrder) {
|
|
1424
|
+
const p = parseConcreteProvider(String(item));
|
|
1425
|
+
if (!p || !validProviders.has(p)) {
|
|
1426
|
+
warnings.push(`providerOrder contains unknown provider: ${String(item)}`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (parsed.timeoutSeconds !== undefined) {
|
|
1432
|
+
const val = Number(parsed.timeoutSeconds);
|
|
1433
|
+
if (!Number.isFinite(val) || val <= 0) {
|
|
1434
|
+
errors.push("timeoutSeconds must be a positive number.");
|
|
1435
|
+
}
|
|
1436
|
+
else if (val > 86400) {
|
|
1437
|
+
warnings.push("timeoutSeconds exceeds 24 hours — this may be unintentional.");
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
if (parsed.retryCount !== undefined) {
|
|
1441
|
+
const val = Number(parsed.retryCount);
|
|
1442
|
+
if (!Number.isFinite(val) || val < 0) {
|
|
1443
|
+
errors.push("retryCount must be a non-negative integer.");
|
|
1444
|
+
}
|
|
1445
|
+
else if (val > 10) {
|
|
1446
|
+
warnings.push("retryCount exceeds 10 — excessive retries can waste resources.");
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (parsed.modelOverrides !== undefined && typeof parsed.modelOverrides === "object" && parsed.modelOverrides !== null) {
|
|
1450
|
+
const validProviderKeys = new Set(DEFAULT_PROVIDER_ORDER);
|
|
1451
|
+
for (const key of Object.keys(parsed.modelOverrides)) {
|
|
1452
|
+
if (!validProviderKeys.has(key)) {
|
|
1453
|
+
warnings.push(`modelOverrides contains unknown provider key: ${key}`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (parsed.defaultAgent !== undefined) {
|
|
1458
|
+
const agent = normalizeOrchestrationAgent(String(parsed.defaultAgent));
|
|
1459
|
+
if (agent === "router" && String(parsed.defaultAgent).toLowerCase() !== "router") {
|
|
1460
|
+
warnings.push(`Unrecognized defaultAgent: ${String(parsed.defaultAgent)} — will fall back to router.`);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
if (typeof parsed.defaultPromptFile === "string" && parsed.defaultPromptFile.trim()) {
|
|
1464
|
+
const resolved = path.resolve(cwd, parsed.defaultPromptFile.trim());
|
|
1465
|
+
if (!existsSync(resolved)) {
|
|
1466
|
+
warnings.push(`defaultPromptFile does not exist: ${parsed.defaultPromptFile}`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
if (typeof parsed.batchDefaults === "object" && parsed.batchDefaults !== null) {
|
|
1470
|
+
const bd = parsed.batchDefaults;
|
|
1471
|
+
if (typeof bd.manifestPath === "string" && bd.manifestPath.trim()) {
|
|
1472
|
+
const resolved = path.resolve(cwd, bd.manifestPath.trim());
|
|
1473
|
+
if (!existsSync(resolved)) {
|
|
1474
|
+
warnings.push(`batchDefaults.manifestPath does not exist: ${bd.manifestPath}`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (Array.isArray(bd.promptFiles)) {
|
|
1478
|
+
for (const pf of bd.promptFiles) {
|
|
1479
|
+
if (typeof pf === "string" && pf.trim()) {
|
|
1480
|
+
const resolved = path.resolve(cwd, pf.trim());
|
|
1481
|
+
if (!existsSync(resolved)) {
|
|
1482
|
+
warnings.push(`batchDefaults.promptFiles entry does not exist: ${pf}`);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return { valid: errors.length === 0, configPath, errors, warnings };
|
|
1489
|
+
}
|
|
1490
|
+
//# sourceMappingURL=runtime.js.map
|