prompts-gpt 0.3.3 → 0.3.4

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/dist/cli.js CHANGED
@@ -3,7 +3,8 @@ import { existsSync, readFileSync, statSync, readdirSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { parseArgs } from "node:util";
6
- import { hasTokenUsage, DEFAULT_PROMPTS_GPT_API_URL, DEFAULT_PROMPTS_GPT_OUT_DIR, DEFAULT_RUN_CONFIG_PATH, PROMPTS_GPT_CREDENTIALS_FILE, PromptsGptApiError, PromptsGptClient, doctor, initRunConfig, loadRunConfig, normalizeConcreteProvider, normalizeOrchestrationAgent, ORCHESTRATION_AGENT_PROFILES, runBatch, runPrompt, resolveRunProvider, warnModelProviderMismatch, sweepPrompt, validateRunConfig, discoverWorkspaceAssets, SUPPORTED_AGENT_TARGETS, detectProviders, loadLocalCredentials, saveLocalCredentials, syncPrompts, writeAgentFiles, writePromptManifest, writePromptMarkdownFiles, ensureGitignoreEntry, isCI, orchestrateParallel, orchestratePipeline, orchestrateEval, captureGitBranch, resolveModelWithWarning, getModelCostTier, estimateTokenCost, DEFAULT_MODELS, } from "./index.js";
6
+ import { hasTokenUsage, DEFAULT_PROMPTS_GPT_API_URL, DEFAULT_PROMPTS_GPT_OUT_DIR, DEFAULT_RUN_CONFIG_PATH, PROMPTS_GPT_CREDENTIALS_FILE, PromptsGptApiError, PromptsGptClient, doctor, initRunConfig, loadRunConfig, normalizeConcreteProvider, normalizeOrchestrationAgent, ORCHESTRATION_AGENT_PROFILES, runBatch, runPrompt, resolveRunProvider, warnModelProviderMismatch, sweepPrompt, validateRunConfig, discoverWorkspaceAssets, SUPPORTED_AGENT_TARGETS, detectProviders, loadLocalCredentials, saveLocalCredentials, syncPrompts, writeAgentFiles, writePromptManifest, writePromptMarkdownFiles, ensureGitignoreEntry, isProcessAlive, isCI, orchestrateParallel, orchestratePipeline, orchestrateEval, captureGitBranch, resolveModelWithWarning, getModelCostTier, estimateTokenCost, DEFAULT_MODELS, } from "./index.js";
7
+ import { PROVIDER_MODELS } from "./model-registry.js";
7
8
  const CLI_EXIT_CODES = {
8
9
  success: 0,
9
10
  general: 1,
@@ -210,7 +211,7 @@ async function runCommand(command, flags) {
210
211
  if (setupPicked === "list") {
211
212
  const { spawnSync: spSync } = await import("node:child_process");
212
213
  const cliEntry = resolveCliEntry();
213
- spSync(process.execPath, [cliEntry, "list"], { stdio: "inherit", cwd });
214
+ spSync(process.execPath, [cliEntry, "list"], { stdio: "inherit", cwd, shell: false, windowsHide: true });
214
215
  }
215
216
  else if (setupPicked !== "done") {
216
217
  const [action, file] = setupPicked.split(":", 2);
@@ -218,7 +219,7 @@ async function runCommand(command, flags) {
218
219
  console.log(`\nRunning: prompts-gpt ${cmd} -f ${file}\n`);
219
220
  const { spawnSync: spSync } = await import("node:child_process");
220
221
  const cliEntry = resolveCliEntry();
221
- const setupResult = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd });
222
+ const setupResult = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd, shell: false, windowsHide: true });
222
223
  process.exitCode = setupResult.status ?? 1;
223
224
  }
224
225
  }
@@ -496,12 +497,7 @@ async function runCommand(command, flags) {
496
497
  const lockContent = JSON.parse(readFileSync(sweepLockPath, "utf8"));
497
498
  const lockAge = Date.now() - new Date(lockContent.startedAt ?? 0).getTime();
498
499
  const lockAgeHours = lockAge / (60 * 60 * 1000);
499
- let holderAlive = false;
500
- try {
501
- process.kill(lockContent.pid ?? 0, 0);
502
- holderAlive = true;
503
- }
504
- catch { /* dead */ }
500
+ const holderAlive = isProcessAlive(lockContent.pid);
505
501
  if (!holderAlive) {
506
502
  console.log(`\n${sym("⚠", "!")} Stale sweep lock detected (PID ${lockContent.pid} is no longer running)`);
507
503
  console.log(` Started: ${lockContent.startedAt ?? "unknown"}`);
@@ -673,7 +669,7 @@ async function runCommand(command, flags) {
673
669
  console.log(`\nRunning: prompts-gpt ${cmd} -f ${file}\n`);
674
670
  const { spawnSync: spSync } = await import("node:child_process");
675
671
  const cliEntry = resolveCliEntry();
676
- const result = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd });
672
+ const result = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd, shell: false, windowsHide: true });
677
673
  process.exitCode = result.status ?? 1;
678
674
  }
679
675
  }
@@ -826,6 +822,9 @@ async function runCommand(command, flags) {
826
822
  }
827
823
  if (command === "run") {
828
824
  const cwd = getResolvedCwd(flags);
825
+ if (isCI() && !flags["non-interactive"]) {
826
+ flags["non-interactive"] = true;
827
+ }
829
828
  const promptFile = getStringFlag(flags, "prompt-file");
830
829
  const config = await loadRunConfig(cwd);
831
830
  warnOnConfigIssues(config);
@@ -1014,6 +1013,10 @@ async function runCommand(command, flags) {
1014
1013
  const doRun = async () => {
1015
1014
  if (running)
1016
1015
  return;
1016
+ if (!existsSync(resolvedWatch)) {
1017
+ console.log(` File deleted: ${resolvedWatch}. Skipping run.`);
1018
+ return;
1019
+ }
1017
1020
  running = true;
1018
1021
  console.log(`\n${colorize("▶ Running...", "\x1b[36m")} (${new Date().toLocaleTimeString()})`);
1019
1022
  try {
@@ -1041,20 +1044,28 @@ async function runCommand(command, flags) {
1041
1044
  };
1042
1045
  await doRun();
1043
1046
  let debounce = null;
1047
+ const closeWatcher = (message) => {
1048
+ if (debounce) {
1049
+ clearTimeout(debounce);
1050
+ debounce = null;
1051
+ }
1052
+ if (message)
1053
+ console.log(message);
1054
+ watcher.close();
1055
+ };
1044
1056
  const watcher = fsWatch(resolvedWatch, () => {
1057
+ if (!existsSync(resolvedWatch)) {
1058
+ closeWatcher(` File deleted: ${resolvedWatch}. Exiting watch mode.`);
1059
+ return;
1060
+ }
1045
1061
  if (debounce)
1046
1062
  clearTimeout(debounce);
1047
1063
  debounce = setTimeout(doRun, 500);
1048
1064
  debounce.unref?.();
1049
1065
  });
1050
1066
  watcher.on("error", (err) => {
1051
- if (debounce) {
1052
- clearTimeout(debounce);
1053
- debounce = null;
1054
- }
1055
1067
  console.error(`Watch error: ${err instanceof Error ? err.message : String(err)}`);
1056
- console.log("File may have been deleted. Exiting watch mode.");
1057
- watcher.close();
1068
+ closeWatcher("File may have been deleted. Exiting watch mode.");
1058
1069
  });
1059
1070
  await new Promise((resolve) => {
1060
1071
  let settled = false;
@@ -1141,11 +1152,11 @@ async function runCommand(command, flags) {
1141
1152
  try {
1142
1153
  const { spawn: openSpawn } = await import("node:child_process");
1143
1154
  if (process.platform === "win32") {
1144
- openSpawn("cmd", ["/c", "start", "", result.summaryFile], { detached: true, stdio: "ignore", windowsHide: true }).unref();
1155
+ openSpawn("cmd", ["/c", "start", "", result.summaryFile], { detached: true, stdio: "ignore", windowsHide: true, shell: false }).unref();
1145
1156
  }
1146
1157
  else {
1147
1158
  const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
1148
- openSpawn(openCmd, [result.summaryFile], { detached: true, stdio: "ignore" }).unref();
1159
+ openSpawn(openCmd, [result.summaryFile], { detached: true, stdio: "ignore", shell: false }).unref();
1149
1160
  }
1150
1161
  }
1151
1162
  catch { /* ignore */ }
@@ -1965,11 +1976,11 @@ async function runCommand(command, flags) {
1965
1976
  try {
1966
1977
  const { spawn: openSpawn } = await import("node:child_process");
1967
1978
  if (process.platform === "win32") {
1968
- openSpawn("cmd", ["/c", "start", "", result.manifestFile], { detached: true, stdio: "ignore", windowsHide: true }).unref();
1979
+ openSpawn("cmd", ["/c", "start", "", result.manifestFile], { detached: true, stdio: "ignore", windowsHide: true, shell: false }).unref();
1969
1980
  }
1970
1981
  else {
1971
1982
  const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
1972
- openSpawn(openCmd, [result.manifestFile], { detached: true, stdio: "ignore" }).unref();
1983
+ openSpawn(openCmd, [result.manifestFile], { detached: true, stdio: "ignore", shell: false }).unref();
1973
1984
  }
1974
1985
  }
1975
1986
  catch { /* ignore */ }
@@ -2181,15 +2192,12 @@ async function runCommand(command, flags) {
2181
2192
  let pipelineDefaults = {};
2182
2193
  try {
2183
2194
  const parsed = JSON.parse(readFileSync(resolvedStepsFile, "utf8"));
2184
- if (Array.isArray(parsed)) {
2195
+ if (isPipelineStepArray(parsed)) {
2185
2196
  stepsJson = parsed;
2186
2197
  }
2187
- else if (parsed && typeof parsed === "object" && Array.isArray(parsed.steps)) {
2188
- const wrapper = parsed;
2189
- stepsJson = wrapper.steps;
2190
- if (wrapper.defaults && typeof wrapper.defaults === "object") {
2191
- pipelineDefaults = wrapper.defaults;
2192
- }
2198
+ else if (isPipelineConfigObject(parsed)) {
2199
+ stepsJson = parsed.steps;
2200
+ pipelineDefaults = parsed.defaults ?? {};
2193
2201
  }
2194
2202
  else {
2195
2203
  throw new Error("Pipeline config must be an array of steps or an object with { defaults?, steps }.");
@@ -2213,9 +2221,13 @@ async function runCommand(command, flags) {
2213
2221
  if (!step || typeof step.name !== "string" || typeof step.promptFile !== "string") {
2214
2222
  throw new CliError(`Pipeline step entries must include name and promptFile fields: ${resolvedStepsFile}`, CLI_EXIT_CODES.validation);
2215
2223
  }
2216
- if (step.promptFile && !existsSync(path.resolve(cwd, step.promptFile))) {
2224
+ const stepPromptPath = path.resolve(cwd, step.promptFile);
2225
+ if (!existsSync(stepPromptPath)) {
2217
2226
  throw new CliError(`Pipeline step "${step.name}" references missing prompt file: ${step.promptFile}`, CLI_EXIT_CODES.usage);
2218
2227
  }
2228
+ if (!statSync(stepPromptPath).isFile()) {
2229
+ throw new CliError(`Pipeline step "${step.name}" promptFile is not a file: ${step.promptFile}`, CLI_EXIT_CODES.usage);
2230
+ }
2219
2231
  const effectiveAgent = typeof step.agent === "string" && step.agent.trim() ? step.agent : defaultPipelineAgent;
2220
2232
  normalizeOrchestrationAgent(effectiveAgent);
2221
2233
  step.agent = effectiveAgent;
@@ -2441,7 +2453,19 @@ async function runCommand(command, flags) {
2441
2453
  if (!existsSync(runDir)) {
2442
2454
  throw new CliError(`Run directory not found: ${runDir}`, CLI_EXIT_CODES.usage);
2443
2455
  }
2444
- const { readFile: fsRead } = await import("node:fs/promises");
2456
+ const { lstat: fsLstat, readFile: fsRead, realpath: fsRealpath } = await import("node:fs/promises");
2457
+ const runDirStat = await fsLstat(runDir).catch((error) => {
2458
+ throw new CliError(`Cannot inspect run directory ${runDir}: ${error instanceof Error ? error.message : String(error)}`, CLI_EXIT_CODES.usage);
2459
+ });
2460
+ if (!runDirStat.isDirectory() || runDirStat.isSymbolicLink()) {
2461
+ throw new CliError(`Invalid run-id: ${runId} is not a regular run directory.`, CLI_EXIT_CODES.usage);
2462
+ }
2463
+ const [realRunDir, realArtifactsDir] = await Promise.all([fsRealpath(runDir), fsRealpath(artifactsDir)]);
2464
+ const realRunDirCheck = caseInsensitive ? realRunDir.toLowerCase() : realRunDir;
2465
+ const realArtifactsDirCheck = caseInsensitive ? realArtifactsDir.toLowerCase() : realArtifactsDir;
2466
+ if (!realRunDirCheck.startsWith(realArtifactsDirCheck + path.sep) && realRunDirCheck !== realArtifactsDirCheck) {
2467
+ throw new CliError("Invalid run-id: resolved path escapes the artifacts directory.", CLI_EXIT_CODES.usage);
2468
+ }
2445
2469
  const deltaFile = path.join(runDir, "worktree-delta.diff");
2446
2470
  if (existsSync(deltaFile)) {
2447
2471
  const delta = await fsRead(deltaFile, "utf8");
@@ -2484,7 +2508,7 @@ async function runCommand(command, flags) {
2484
2508
  console.log("======================");
2485
2509
  console.log("");
2486
2510
  const { spawnSync: gitCheck } = await import("node:child_process");
2487
- const gitResult = gitCheck("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8", timeout: 5000, windowsHide: true });
2511
+ const gitResult = gitCheck("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8", timeout: 5000, windowsHide: true, shell: false });
2488
2512
  if (gitResult.status !== 0) {
2489
2513
  console.log(`${sym("⚠", "!")} Not inside a git repository. Prompts-GPT works best in a git repo for worktree tracking.`);
2490
2514
  console.log(" Run: git init\n");
@@ -2619,7 +2643,7 @@ async function runCommand(command, flags) {
2619
2643
  console.log(`\nRunning: prompts-gpt ${cmd} -f ${file}\n`);
2620
2644
  const { spawnSync: spSync } = await import("node:child_process");
2621
2645
  const cliEntry = resolveCliEntry();
2622
- const result = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd });
2646
+ const result = spSync(process.execPath, [cliEntry, cmd, "-f", file], { stdio: "inherit", cwd, shell: false, windowsHide: true });
2623
2647
  process.exitCode = result.status ?? 1;
2624
2648
  return;
2625
2649
  }
@@ -2757,7 +2781,7 @@ async function runCommand(command, flags) {
2757
2781
  if (runQs.toLowerCase() !== "n" && runQs.toLowerCase() !== "no") {
2758
2782
  const { spawnSync: spSync } = await import("node:child_process");
2759
2783
  const cliEntry = resolveCliEntry();
2760
- const qsResult = spSync(process.execPath, [cliEntry, "quickstart", "--cwd", cwd], { stdio: "inherit", cwd });
2784
+ const qsResult = spSync(process.execPath, [cliEntry, "quickstart", "--cwd", cwd], { stdio: "inherit", cwd, shell: false, windowsHide: true });
2761
2785
  process.exitCode = qsResult.status ?? 1;
2762
2786
  return;
2763
2787
  }
@@ -2865,7 +2889,7 @@ async function runCommand(command, flags) {
2865
2889
  console.log(`\nRunning: prompts-gpt run -f ${genFile}\n`);
2866
2890
  const { spawnSync: spSync } = await import("node:child_process");
2867
2891
  const cliEntry = resolveCliEntry();
2868
- const genResult = spSync(process.execPath, [cliEntry, "run", "-f", genFile], { stdio: "inherit", cwd });
2892
+ const genResult = spSync(process.execPath, [cliEntry, "run", "-f", genFile], { stdio: "inherit", cwd, shell: false, windowsHide: true });
2869
2893
  process.exitCode = genResult.status ?? 1;
2870
2894
  }
2871
2895
  }
@@ -3153,7 +3177,7 @@ async function runCommand(command, flags) {
3153
3177
  const { spawnSync: spSync } = await import("node:child_process");
3154
3178
  const cliEntry = resolveCliEntry();
3155
3179
  const projCwd = getResolvedCwd(flags);
3156
- const syncResult = spSync(process.execPath, [cliEntry, "sync", "--cwd", projCwd], { stdio: "inherit", cwd: projCwd });
3180
+ const syncResult = spSync(process.execPath, [cliEntry, "sync", "--cwd", projCwd], { stdio: "inherit", cwd: projCwd, shell: false, windowsHide: true });
3157
3181
  process.exitCode = syncResult.status ?? 1;
3158
3182
  }
3159
3183
  }
@@ -3748,6 +3772,7 @@ function readTokenFromPrompt(command) {
3748
3772
  stdin.setRawMode?.(true);
3749
3773
  }
3750
3774
  catch {
3775
+ cleanup();
3751
3776
  reject(new CliError("Cannot enable raw mode for token prompt. Use --token-stdin instead.", CLI_EXIT_CODES.usage, { helpCommand: command }));
3752
3777
  return;
3753
3778
  }
@@ -3835,15 +3860,54 @@ async function applyDoctorFixes(cwd) {
3835
3860
  skipped.push(`${DEFAULT_PROMPTS_GPT_OUT_DIR}/ directory already exists`);
3836
3861
  }
3837
3862
  try {
3838
- await ensureGitignoreEntry(cwd, ".prompts-gpt/.credentials.json");
3839
- await ensureGitignoreEntry(cwd, ".prompts-gpt/.models.json");
3840
- await ensureGitignoreEntry(cwd, ".scripts/runs/");
3841
- await ensureGitignoreEntry(cwd, ".sweep.lock");
3842
- applied.push("Updated .gitignore with sensitive file and sweep artifact patterns");
3863
+ const gitignoreEntries = [
3864
+ ".prompts-gpt/.credentials.json",
3865
+ ".prompts-gpt/.models.json",
3866
+ ".scripts/runs",
3867
+ ".sweep.lock",
3868
+ ];
3869
+ const gitignoreUpdates = [];
3870
+ for (const entry of gitignoreEntries) {
3871
+ gitignoreUpdates.push(await ensureGitignoreEntry(cwd, entry));
3872
+ }
3873
+ if (gitignoreUpdates.some(Boolean)) {
3874
+ applied.push("Updated .gitignore with sensitive file and sweep artifact patterns");
3875
+ }
3876
+ else {
3877
+ skipped.push(".gitignore already contains sensitive file and sweep artifact patterns");
3878
+ }
3843
3879
  }
3844
3880
  catch {
3845
3881
  failed.push("Could not update .gitignore with sensitive file and sweep artifact patterns");
3846
3882
  }
3883
+ const sweepLockPath = path.resolve(cwd, ".sweep.lock");
3884
+ if (existsSync(sweepLockPath)) {
3885
+ try {
3886
+ const lockRaw = readFileSync(sweepLockPath, "utf8");
3887
+ const lockData = JSON.parse(lockRaw);
3888
+ const lockAge = Date.now() - new Date(lockData.startedAt ?? 0).getTime();
3889
+ const lockAgeHours = lockAge / (60 * 60 * 1000);
3890
+ const holderAlive = isProcessAlive(lockData.pid);
3891
+ if (!holderAlive || (Number.isFinite(lockAgeHours) && lockAgeHours >= 4)) {
3892
+ const { rm: fsRm } = await import("node:fs/promises");
3893
+ await fsRm(sweepLockPath, { force: true });
3894
+ applied.push("Removed stale sweep lock");
3895
+ }
3896
+ else {
3897
+ skipped.push("Sweep lock is active (held by running process)");
3898
+ }
3899
+ }
3900
+ catch {
3901
+ try {
3902
+ const { rm: fsRm } = await import("node:fs/promises");
3903
+ await fsRm(sweepLockPath, { force: true });
3904
+ applied.push("Removed corrupt sweep lock file");
3905
+ }
3906
+ catch {
3907
+ failed.push("Could not remove corrupt sweep lock file");
3908
+ }
3909
+ }
3910
+ }
3847
3911
  return { applied, skipped, failed };
3848
3912
  }
3849
3913
  function getStringFlag(flags, name) {
@@ -3939,6 +4003,51 @@ function truncatePreview(text, maxLen) {
3939
4003
  return clean;
3940
4004
  return clean.slice(0, maxLen - 3) + "...";
3941
4005
  }
4006
+ function isPlainRecord(value) {
4007
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4008
+ }
4009
+ function isOptionalString(value) {
4010
+ return value === undefined || typeof value === "string";
4011
+ }
4012
+ function isOptionalNumber(value) {
4013
+ return value === undefined || (typeof value === "number" && Number.isFinite(value));
4014
+ }
4015
+ function isStringRecord(value) {
4016
+ if (value === undefined)
4017
+ return true;
4018
+ if (!isPlainRecord(value))
4019
+ return false;
4020
+ return Object.entries(value).every(([key, entry]) => key !== "__proto__" && key !== "constructor" && key !== "prototype" && typeof entry === "string");
4021
+ }
4022
+ function isPipelineStepEntry(value) {
4023
+ if (!isPlainRecord(value))
4024
+ return false;
4025
+ return typeof value.name === "string" &&
4026
+ typeof value.promptFile === "string" &&
4027
+ isOptionalString(value.agent) &&
4028
+ isOptionalString(value.model) &&
4029
+ isOptionalNumber(value.timeout) &&
4030
+ isOptionalNumber(value.retries) &&
4031
+ isOptionalString(value.sandboxMode) &&
4032
+ isStringRecord(value.env);
4033
+ }
4034
+ function isPipelineStepArray(value) {
4035
+ return Array.isArray(value) && value.every(isPipelineStepEntry);
4036
+ }
4037
+ function isPipelineDefaults(value) {
4038
+ if (value === undefined)
4039
+ return true;
4040
+ if (!isPlainRecord(value))
4041
+ return false;
4042
+ return isOptionalString(value.agent) &&
4043
+ isOptionalString(value.model) &&
4044
+ isOptionalNumber(value.timeout) &&
4045
+ isOptionalNumber(value.retries) &&
4046
+ isOptionalString(value.sandboxMode);
4047
+ }
4048
+ function isPipelineConfigObject(value) {
4049
+ return isPlainRecord(value) && isPipelineStepArray(value.steps) && isPipelineDefaults(value.defaults);
4050
+ }
3942
4051
  function formatList(values) {
3943
4052
  if (!Array.isArray(values) || values.length === 0)
3944
4053
  return "None";
@@ -4149,82 +4258,6 @@ async function saveLastUsedModel(cwd, provider, model) {
4149
4258
  }
4150
4259
  const CODEX_API_KEY_ENV_NAMES = ["OPENAI_API_KEY", "CODEX_API_KEY", "OPENAI_API_KEY_FILE"];
4151
4260
  const CODEX_CHATGPT_UNSUPPORTED_MODELS = new Set(["gpt-5.5-pro", "gpt-5.4-pro", "gpt-5.5-high-fast"]);
4152
- const PROVIDER_MODELS = Object.freeze({
4153
- codex: [
4154
- { value: "gpt-5.5", label: "gpt-5.5 — frontier coding & reasoning", tier: "frontier" },
4155
- { value: "gpt-5.5-pro", label: "gpt-5.5-pro — smarter, more precise", tier: "frontier" },
4156
- { value: "gpt-5.4", label: "gpt-5.4 — strong coding model", tier: "standard" },
4157
- { value: "gpt-5.4-pro", label: "gpt-5.4-pro — enhanced responses", tier: "standard" },
4158
- { value: "gpt-5.4-mini", label: "gpt-5.4-mini — fast coding & subagents", tier: "fast" },
4159
- { value: "gpt-5.4-nano", label: "gpt-5.4-nano — cheapest high-volume", tier: "budget" },
4160
- { value: "gpt-5.3-codex", label: "gpt-5.3-codex — merged GPT-5 + Codex", tier: "standard" },
4161
- { value: "gpt-5.3-codex-spark", label: "gpt-5.3-codex-spark — 15x faster gen", tier: "fast" },
4162
- { value: "o3", label: "o3 — advanced reasoning", tier: "frontier" },
4163
- ],
4164
- claude: [
4165
- { value: "claude-opus-4-7", label: "claude-opus-4-7 — most capable model", tier: "frontier" },
4166
- { value: "claude-opus-4-6", label: "claude-opus-4-6 — previous gen opus", tier: "frontier" },
4167
- { value: "claude-sonnet-4-6", label: "claude-sonnet-4-6 — balanced default", tier: "standard" },
4168
- { value: "claude-sonnet-4-5", label: "claude-sonnet-4-5 — previous gen sonnet", tier: "standard" },
4169
- { value: "claude-3-5-haiku", label: "claude-3-5-haiku — fastest & cheapest", tier: "fast" },
4170
- { value: "claude-haiku-4-5", label: "claude-haiku-4-5 — fast haiku", tier: "fast" },
4171
- ],
4172
- cursor: [
4173
- { value: "auto", label: "auto — Cursor auto-selects best", tier: "standard" },
4174
- { value: "claude-4.7-opus", label: "claude-4.7-opus — Claude Opus 4.7", tier: "frontier" },
4175
- { value: "claude-4.7-opus-fast", label: "claude-4.7-opus-fast — Claude Opus 4.7 Fast", tier: "frontier" },
4176
- { value: "claude-4.6-opus-high", label: "claude-4.6-opus-high — Claude Opus 4.6 High", tier: "frontier" },
4177
- { value: "claude-4.6-opus-high-thinking", label: "claude-4.6-opus-high-thinking — Claude Opus 4.6 Thinking", tier: "frontier" },
4178
- { value: "claude-4.6-sonnet-high", label: "claude-4.6-sonnet-high — fast + smart", tier: "standard" },
4179
- { value: "gpt-5.5", label: "gpt-5.5 — OpenAI frontier", tier: "frontier" },
4180
- { value: "gpt-5.5-high-fast", label: "gpt-5.5-high-fast — GPT frontier fast", tier: "frontier" },
4181
- { value: "gpt-5.4", label: "gpt-5.4 — OpenAI strong coding", tier: "standard" },
4182
- { value: "gpt-5.4-mini", label: "gpt-5.4-mini — fast & affordable", tier: "fast" },
4183
- { value: "gpt-5.4-nano", label: "gpt-5.4-nano — cheapest", tier: "budget" },
4184
- { value: "gpt-5.3-codex", label: "gpt-5.3-codex — OpenAI codex", tier: "standard" },
4185
- { value: "gpt-5.2", label: "gpt-5.2 — OpenAI coding", tier: "standard" },
4186
- { value: "gpt-5", label: "gpt-5 — OpenAI base", tier: "standard" },
4187
- { value: "gpt-5-fast", label: "gpt-5-fast — faster GPT-5", tier: "standard" },
4188
- { value: "gpt-5-mini", label: "gpt-5-mini — small GPT-5", tier: "fast" },
4189
- { value: "o3-pro-high", label: "o3-pro-high — advanced reasoning", tier: "frontier" },
4190
- { value: "composer-2.5", label: "composer-2.5 — Cursor latest model", tier: "standard" },
4191
- { value: "composer-2", label: "composer-2 — balanced multi-file", tier: "standard" },
4192
- { value: "composer-2-fast", label: "composer-2-fast — speed optimized", tier: "fast" },
4193
- { value: "composer-1.5", label: "composer-1.5 — legacy capable", tier: "standard" },
4194
- { value: "composer-1", label: "composer-1 — legacy model", tier: "fast" },
4195
- { value: "gemini-3.1-pro", label: "gemini-3.1-pro — Google latest", tier: "standard" },
4196
- { value: "gemini-3-pro", label: "gemini-3-pro — Google frontier", tier: "standard" },
4197
- { value: "gemini-3-flash", label: "gemini-3-flash — Google fast", tier: "fast" },
4198
- { value: "gemini-2.5-flash", label: "gemini-2.5-flash — Google budget", tier: "fast" },
4199
- { value: "grok-4.3", label: "grok-4.3 — xAI frontier", tier: "frontier" },
4200
- { value: "grok-4.20", label: "grok-4.20 — xAI standard", tier: "standard" },
4201
- { value: "kimi-k2.5", label: "kimi-k2.5 — Moonshot coding", tier: "standard" },
4202
- { value: "claude-4.5-opus", label: "claude-4.5-opus — previous gen opus", tier: "standard" },
4203
- { value: "claude-4.5-haiku", label: "claude-4.5-haiku — fast haiku", tier: "fast" },
4204
- ],
4205
- copilot: [
4206
- { value: "auto", label: "auto — Copilot auto-selects", tier: "standard" },
4207
- { value: "claude-opus-4.7", label: "claude-opus-4.7 — Anthropic frontier", tier: "frontier" },
4208
- { value: "claude-opus-4.6", label: "claude-opus-4.6 — Anthropic frontier", tier: "frontier" },
4209
- { value: "claude-sonnet-4.6", label: "claude-sonnet-4.6 — Anthropic balanced", tier: "standard" },
4210
- { value: "claude-sonnet-4.5", label: "claude-sonnet-4.5 — Anthropic previous gen", tier: "standard" },
4211
- { value: "claude-opus-4.5", label: "claude-opus-4.5 — Anthropic legacy opus", tier: "standard" },
4212
- { value: "claude-haiku-4.5", label: "claude-haiku-4.5 — fastest", tier: "fast" },
4213
- { value: "gpt-5.5", label: "gpt-5.5 — OpenAI frontier", tier: "frontier" },
4214
- { value: "gpt-5.4", label: "gpt-5.4 — OpenAI standard", tier: "standard" },
4215
- { value: "gpt-5.4-mini", label: "gpt-5.4-mini — fast included", tier: "fast" },
4216
- { value: "gpt-5.4-nano", label: "gpt-5.4-nano — cheapest high-volume", tier: "budget" },
4217
- { value: "gpt-5.3-codex", label: "gpt-5.3-codex — OpenAI coding", tier: "standard" },
4218
- { value: "gpt-5-mini", label: "gpt-5-mini — included fast model", tier: "fast" },
4219
- { value: "gemini-3.1-pro", label: "gemini-3.1-pro — Google latest", tier: "frontier" },
4220
- { value: "gemini-3-pro", label: "gemini-3-pro — Google frontier", tier: "standard" },
4221
- { value: "gemini-2.5-pro", label: "gemini-2.5-pro — Google frontier", tier: "standard" },
4222
- { value: "gemini-2.5-flash", label: "gemini-2.5-flash — Google fast & affordable", tier: "fast" },
4223
- { value: "gemini-3-flash", label: "gemini-3-flash — Google fast", tier: "fast" },
4224
- { value: "raptor-mini", label: "raptor-mini — preview tuned mini", tier: "fast" },
4225
- { value: "goldeneye", label: "goldeneye — preview tuned coding model", tier: "budget" },
4226
- ],
4227
- });
4228
4261
  // Model names are used directly — no alias resolution needed
4229
4262
  function hasCodexApiKeyAuth(env = process.env) {
4230
4263
  return CODEX_API_KEY_ENV_NAMES.some((name) => String(env[name] ?? "").trim().length > 0);
@@ -5185,8 +5218,8 @@ function slugifyFilename(text, fallback) {
5185
5218
  }
5186
5219
  function buildProgressBar(completed, total, width) {
5187
5220
  const safeWidth = Number.isFinite(width) && width > 0 ? Math.trunc(width) : 20;
5188
- const safeTotal = Math.max(total, 1);
5189
- const safeCompleted = Math.max(0, Math.min(completed, safeTotal));
5221
+ const safeTotal = Number.isFinite(total) && total > 0 ? Math.trunc(total) : 1;
5222
+ const safeCompleted = Number.isFinite(completed) ? Math.max(0, Math.min(Math.trunc(completed), safeTotal)) : 0;
5190
5223
  const fraction = safeCompleted / safeTotal;
5191
5224
  const filled = Math.min(Math.floor(fraction * safeWidth), safeWidth);
5192
5225
  const empty = safeWidth - filled;
@@ -5272,9 +5305,12 @@ function interactiveSelect(prompt, options) {
5272
5305
  const maxVisible = Math.min(options.length, Math.max(3, termRows - 4));
5273
5306
  const useUnicode = supportsUnicode();
5274
5307
  const pointer = useUnicode ? "\u276f" : ">";
5275
- const formatLine = (entry, index, selected) => {
5276
- const numberWidth = String(getVisibleOptions().length).length;
5277
- const num = String(index + 1).padStart(numberWidth, " ");
5308
+ const getScrollStart = (visible) => visible.length <= maxVisible
5309
+ ? 0
5310
+ : Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), visible.length - maxVisible));
5311
+ const formatLine = (entry, shortcutIndex, selected) => {
5312
+ const numberWidth = String(Math.min(9, maxVisible)).length;
5313
+ const num = shortcutIndex <= 9 ? String(shortcutIndex).padStart(numberWidth, " ") : " ".repeat(numberWidth);
5278
5314
  const prefix = selected ? colorize(pointer, "\x1b[36m") : " ";
5279
5315
  const label = selected ? colorize(entry.label, "\x1b[1m") : entry.label;
5280
5316
  return ` ${prefix} ${num}. ${label}`;
@@ -5295,15 +5331,13 @@ function interactiveSelect(prompt, options) {
5295
5331
  const visibleCount = Math.min(maxVisible, visible.length);
5296
5332
  const posLabel = visible.length > 1 ? ` (${cursor + 1}/${visible.length})` : "";
5297
5333
  const filterLabel = filterText ? colorize(` filter: "${filterText}"`, "\x1b[33m") : "";
5298
- const scrollStart = visible.length <= maxVisible
5299
- ? 0
5300
- : Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), visible.length - maxVisible));
5334
+ const scrollStart = getScrollStart(visible);
5301
5335
  const lines = [];
5302
5336
  lines.push(`${prompt}${posLabel}${filterLabel}`);
5303
5337
  for (let vi = 0; vi < visibleCount; vi++) {
5304
5338
  const i = scrollStart + vi;
5305
5339
  if (i < visible.length) {
5306
- lines.push(formatLine(visible[i], i, i === cursor));
5340
+ lines.push(formatLine(visible[i], vi + 1, i === cursor));
5307
5341
  }
5308
5342
  }
5309
5343
  if (visible.length > maxVisible) {
@@ -5360,7 +5394,7 @@ function interactiveSelect(prompt, options) {
5360
5394
  return;
5361
5395
  }
5362
5396
  stdin.resume();
5363
- const onSigint = () => {
5397
+ const cancelSelection = () => {
5364
5398
  if (settled)
5365
5399
  return;
5366
5400
  settled = true;
@@ -5371,11 +5405,17 @@ function interactiveSelect(prompt, options) {
5371
5405
  catch { /* stdout may be closed */ }
5372
5406
  reject(new CliError("Selection cancelled.", CLI_EXIT_CODES.general));
5373
5407
  };
5408
+ const onSigint = () => { cancelSelection(); };
5409
+ const onSigterm = () => { cancelSelection(); };
5374
5410
  const cleanup = () => {
5375
5411
  try {
5376
5412
  process.removeListener("SIGINT", onSigint);
5377
5413
  }
5378
5414
  catch { /* ignore */ }
5415
+ try {
5416
+ process.removeListener("SIGTERM", onSigterm);
5417
+ }
5418
+ catch { /* ignore */ }
5379
5419
  try {
5380
5420
  stdin.removeListener("data", onData);
5381
5421
  }
@@ -5407,6 +5447,7 @@ function interactiveSelect(prompt, options) {
5407
5447
  stdin.on("end", onEnd);
5408
5448
  stdin.on("error", onError);
5409
5449
  process.on("SIGINT", onSigint);
5450
+ process.on("SIGTERM", onSigterm);
5410
5451
  const onData = (data) => {
5411
5452
  const visible = getVisibleOptions();
5412
5453
  for (let ci = 0; ci < data.length; ci++) {
@@ -5542,17 +5583,18 @@ function interactiveSelect(prompt, options) {
5542
5583
  }
5543
5584
  else if (ch >= "1" && ch <= "9" && !filterText) {
5544
5585
  const idx = parseInt(ch, 10) - 1;
5545
- if (idx < visible.length && idx < 9) {
5586
+ const visibleIndex = getScrollStart(visible) + idx;
5587
+ if (visibleIndex < visible.length && idx < 9) {
5546
5588
  if (settled)
5547
5589
  return;
5548
5590
  settled = true;
5549
5591
  cleanup();
5550
5592
  stdout.write("\n");
5551
- resolve(visible[idx].value);
5593
+ resolve(visible[visibleIndex].value);
5552
5594
  return;
5553
5595
  }
5554
5596
  }
5555
- else if (ch >= " " && ch <= "~") {
5597
+ else if (isPrintableInteractiveInput(ch)) {
5556
5598
  filterText += ch;
5557
5599
  cursor = 0;
5558
5600
  render();
@@ -5562,6 +5604,11 @@ function interactiveSelect(prompt, options) {
5562
5604
  stdin.on("data", onData);
5563
5605
  });
5564
5606
  }
5607
+ function isPrintableInteractiveInput(value) {
5608
+ if (!value)
5609
+ return false;
5610
+ return !/[\x00-\x1f\x7f]/.test(value);
5611
+ }
5565
5612
  async function interactiveInput(prompt, defaultValue) {
5566
5613
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
5567
5614
  throw new CliError("Interactive input requires a TTY.", CLI_EXIT_CODES.usage);
@@ -5585,6 +5632,14 @@ async function interactiveInput(prompt, defaultValue) {
5585
5632
  resolved = true;
5586
5633
  resolve(defaultValue || "");
5587
5634
  });
5635
+ rl.on("error", (err) => {
5636
+ if (resolved)
5637
+ return;
5638
+ resolved = true;
5639
+ rl.close();
5640
+ rl.removeAllListeners();
5641
+ reject(new CliError(`Input stream error: ${err.message}`, CLI_EXIT_CODES.general));
5642
+ });
5588
5643
  rl.on("SIGINT", () => {
5589
5644
  if (resolved)
5590
5645
  return;
@@ -5596,14 +5651,26 @@ async function interactiveInput(prompt, defaultValue) {
5596
5651
  });
5597
5652
  }
5598
5653
  async function checkPromptsGptSiteReachable(apiUrl) {
5654
+ if (typeof globalThis.fetch !== "function") {
5655
+ return { ok: false, status: null };
5656
+ }
5599
5657
  const target = new URL("/", apiUrl).toString();
5600
- const request = (method) => Promise.race([
5601
- globalThis.fetch(target, { method }),
5602
- new Promise((_, rej) => {
5603
- const timer = setTimeout(() => rej(new Error("timeout")), 5000);
5604
- timer.unref?.();
5605
- }),
5606
- ]);
5658
+ const request = async (method) => {
5659
+ let timer = null;
5660
+ try {
5661
+ return await Promise.race([
5662
+ globalThis.fetch(target, { method }),
5663
+ new Promise((_, reject) => {
5664
+ timer = setTimeout(() => reject(new Error("timeout")), 5000);
5665
+ timer.unref?.();
5666
+ }),
5667
+ ]);
5668
+ }
5669
+ finally {
5670
+ if (timer)
5671
+ clearTimeout(timer);
5672
+ }
5673
+ };
5607
5674
  let response = await request("HEAD");
5608
5675
  if (response.status === 405) {
5609
5676
  response = await request("GET");