gsd-pi 2.37.0 → 2.37.1-dev.49503be

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +21 -20
  2. package/dist/onboarding.js +1 -0
  3. package/dist/resources/extensions/cmux/package.json +7 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +54 -1
  5. package/dist/resources/extensions/gsd/auto-loop.js +18 -4
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
  7. package/dist/resources/extensions/gsd/auto-prompts.js +55 -0
  8. package/dist/resources/extensions/gsd/auto-recovery.js +19 -1
  9. package/dist/resources/extensions/gsd/auto.js +42 -5
  10. package/dist/resources/extensions/gsd/commands.js +80 -33
  11. package/dist/resources/extensions/gsd/files.js +41 -0
  12. package/dist/resources/extensions/gsd/git-service.js +9 -1
  13. package/dist/resources/extensions/gsd/history.js +2 -1
  14. package/dist/resources/extensions/gsd/metrics.js +4 -2
  15. package/dist/resources/extensions/gsd/preferences-types.js +2 -1
  16. package/dist/resources/extensions/gsd/preferences-validation.js +42 -0
  17. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  18. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  19. package/dist/resources/extensions/gsd/session-lock.js +26 -6
  20. package/dist/resources/extensions/shared/format-utils.js +5 -41
  21. package/dist/resources/extensions/shared/layout-utils.js +46 -0
  22. package/dist/resources/extensions/shared/mod.js +2 -1
  23. package/package.json +2 -1
  24. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  25. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  26. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  27. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  28. package/packages/pi-ai/dist/models.generated.js +172 -0
  29. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  30. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  31. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  32. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  33. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  34. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  35. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  36. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  37. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  38. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  39. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  40. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  41. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  42. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  43. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  44. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  45. package/packages/pi-ai/dist/types.d.ts +2 -2
  46. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  47. package/packages/pi-ai/dist/types.js.map +1 -1
  48. package/packages/pi-ai/package.json +1 -0
  49. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  50. package/packages/pi-ai/src/models.generated.ts +172 -0
  51. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  52. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  53. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  54. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  55. package/packages/pi-ai/src/types.ts +2 -0
  56. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  57. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
  58. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  59. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  61. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  62. package/packages/pi-coding-agent/package.json +1 -1
  63. package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
  64. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  65. package/pkg/package.json +1 -1
  66. package/src/resources/extensions/cmux/package.json +7 -0
  67. package/src/resources/extensions/gsd/auto-dispatch.ts +78 -0
  68. package/src/resources/extensions/gsd/auto-loop.ts +24 -6
  69. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
  70. package/src/resources/extensions/gsd/auto-prompts.ts +68 -0
  71. package/src/resources/extensions/gsd/auto-recovery.ts +18 -0
  72. package/src/resources/extensions/gsd/auto.ts +56 -5
  73. package/src/resources/extensions/gsd/commands.ts +85 -31
  74. package/src/resources/extensions/gsd/files.ts +45 -0
  75. package/src/resources/extensions/gsd/git-service.ts +12 -1
  76. package/src/resources/extensions/gsd/history.ts +2 -1
  77. package/src/resources/extensions/gsd/metrics.ts +4 -2
  78. package/src/resources/extensions/gsd/preferences-types.ts +5 -1
  79. package/src/resources/extensions/gsd/preferences-validation.ts +41 -0
  80. package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  81. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  82. package/src/resources/extensions/gsd/session-lock.ts +41 -6
  83. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +37 -1
  84. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
  85. package/src/resources/extensions/gsd/tests/cmux.test.ts +25 -1
  86. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +367 -0
  87. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  88. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
  89. package/src/resources/extensions/gsd/types.ts +41 -0
  90. package/src/resources/extensions/shared/format-utils.ts +5 -44
  91. package/src/resources/extensions/shared/layout-utils.ts +49 -0
  92. package/src/resources/extensions/shared/mod.ts +7 -4
  93. package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
@@ -349,10 +349,18 @@ export class GitServiceImpl {
349
349
  }
350
350
  const wtName = detectWorktreeName(this.basePath);
351
351
  if (wtName) {
352
+ // Auto-mode worktrees use milestone/<MID> branches (wtName = milestone ID)
353
+ const milestoneBranch = `milestone/${wtName}`;
354
+ const currentBranch = nativeGetCurrentBranch(this.basePath);
355
+ // If we're on a milestone/<MID> branch, use it (auto-mode case)
356
+ if (currentBranch.startsWith("milestone/")) {
357
+ return currentBranch;
358
+ }
359
+ // Otherwise check for manual worktree branch (worktree/<name>)
352
360
  const wtBranch = `worktree/${wtName}`;
353
361
  if (nativeBranchExists(this.basePath, wtBranch))
354
362
  return wtBranch;
355
- return nativeGetCurrentBranch(this.basePath);
363
+ return currentBranch;
356
364
  }
357
365
  // Repo-level default detection: origin/HEAD → main → master → current branch.
358
366
  // Native path uses libgit2 (single call), fallback spawns multiple git processes.
@@ -1,6 +1,7 @@
1
1
  // GSD Extension — Session History View
2
2
  // Human-readable display of past auto-mode unit executions.
3
- import { formatDuration, padRight, truncateWithEllipsis } from "../shared/format-utils.js";
3
+ import { formatDuration, truncateWithEllipsis } from "../shared/format-utils.js";
4
+ import { padRight } from "../shared/layout-utils.js";
4
5
  import { getLedger, getProjectTotals, formatCost, formatTokenCount, aggregateBySlice, aggregateByPhase, aggregateByModel, loadLedgerFromDisk, } from "./metrics.js";
5
6
  /**
6
7
  * Show recent unit execution history with cost, tokens, and duration.
@@ -17,8 +17,10 @@ import { gsdRoot } from "./paths.js";
17
17
  import { getAndClearSkills } from "./skill-telemetry.js";
18
18
  import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
19
19
  import { parseUnitId } from "./unit-id.js";
20
- // Re-export from shared — canonical implementation lives in format-utils.
21
- export { formatTokenCount } from "../shared/mod.js";
20
+ // Re-export from shared — import directly from format-utils to avoid pulling
21
+ // in the full barrel (mod.js → ui.js → @gsd/pi-tui) which breaks when loaded
22
+ // outside jiti's alias resolution (e.g. dynamic import in auto-loop reports).
23
+ export { formatTokenCount } from "../shared/format-utils.js";
22
24
  export function classifyUnitPhase(unitType) {
23
25
  switch (unitType) {
24
26
  case "research-milestone":
@@ -65,11 +65,12 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
65
65
  "compression_strategy",
66
66
  "context_selection",
67
67
  "widget_mode",
68
+ "reactive_execution",
68
69
  ]);
69
70
  /** Canonical list of all dispatch unit types. */
70
71
  export const KNOWN_UNIT_TYPES = [
71
72
  "research-milestone", "plan-milestone", "research-slice", "plan-slice",
72
- "execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
73
+ "execute-task", "reactive-execute", "complete-slice", "replan-slice", "reassess-roadmap",
73
74
  "run-uat", "complete-milestone",
74
75
  ];
75
76
  export const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
@@ -500,6 +500,48 @@ export function validatePreferences(preferences) {
500
500
  validated.parallel = parallel;
501
501
  }
502
502
  }
503
+ // ─── Reactive Execution ─────────────────────────────────────────────────
504
+ if (preferences.reactive_execution !== undefined) {
505
+ if (typeof preferences.reactive_execution === "object" && preferences.reactive_execution !== null) {
506
+ const re = preferences.reactive_execution;
507
+ const validRe = {};
508
+ if (re.enabled !== undefined) {
509
+ if (typeof re.enabled === "boolean")
510
+ validRe.enabled = re.enabled;
511
+ else
512
+ errors.push("reactive_execution.enabled must be a boolean");
513
+ }
514
+ if (re.max_parallel !== undefined) {
515
+ const mp = typeof re.max_parallel === "number" ? re.max_parallel : Number(re.max_parallel);
516
+ if (Number.isFinite(mp) && mp >= 1 && mp <= 8) {
517
+ validRe.max_parallel = Math.floor(mp);
518
+ }
519
+ else {
520
+ errors.push("reactive_execution.max_parallel must be a number between 1 and 8");
521
+ }
522
+ }
523
+ if (re.isolation_mode !== undefined) {
524
+ if (re.isolation_mode === "same-tree") {
525
+ validRe.isolation_mode = "same-tree";
526
+ }
527
+ else {
528
+ errors.push('reactive_execution.isolation_mode must be "same-tree"');
529
+ }
530
+ }
531
+ const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode"]);
532
+ for (const key of Object.keys(re)) {
533
+ if (!knownReKeys.has(key)) {
534
+ warnings.push(`unknown reactive_execution key "${key}" — ignored`);
535
+ }
536
+ }
537
+ if (Object.keys(validRe).length > 0) {
538
+ validated.reactive_execution = validRe;
539
+ }
540
+ }
541
+ else {
542
+ errors.push("reactive_execution must be an object");
543
+ }
544
+ }
503
545
  // ─── Verification Preferences ───────────────────────────────────────────
504
546
  if (preferences.verification_commands !== undefined) {
505
547
  if (Array.isArray(preferences.verification_commands)) {
@@ -0,0 +1,41 @@
1
+ # Reactive Task Execution — Parallel Dispatch
2
+
3
+ **Working directory:** `{{workingDirectory}}`
4
+ **Milestone:** {{milestoneId}} — {{milestoneTitle}}
5
+ **Slice:** {{sliceId}} — {{sliceTitle}}
6
+
7
+ ## Mission
8
+
9
+ You are executing **multiple tasks in parallel** for this slice. The task graph below shows which tasks are ready for simultaneous execution based on their input/output dependencies.
10
+
11
+ **Critical rule:** Use the `subagent` tool in **parallel mode** to dispatch all ready tasks simultaneously. Each subagent gets a self-contained execute-task prompt. After all subagents return, verify each task's outputs and write summaries.
12
+
13
+ ## Task Dependency Graph
14
+
15
+ {{graphContext}}
16
+
17
+ ## Ready Tasks for Parallel Dispatch
18
+
19
+ {{readyTaskCount}} tasks are ready for parallel execution:
20
+
21
+ {{readyTaskList}}
22
+
23
+ ## Execution Protocol
24
+
25
+ 1. **Dispatch all ready tasks** using `subagent` in parallel mode. Each subagent prompt is provided below.
26
+ 2. **Wait for all subagents** to complete.
27
+ 3. **Verify each task's outputs** — check that expected files were created/modified and that verification commands pass.
28
+ 4. **Write task summaries** for each completed task using the task-summary template.
29
+ 5. **Mark completed tasks** as done in the slice plan (checkbox `[x]`).
30
+ 6. **Commit** all changes with a clear message covering the parallel batch.
31
+
32
+ If any subagent fails:
33
+ - Write a summary for the failed task with `blocker_discovered: true`
34
+ - Continue marking the successful tasks as done
35
+ - The orchestrator will handle re-dispatch on the next iteration
36
+
37
+ ## Subagent Prompts
38
+
39
+ {{subagentPrompts}}
40
+
41
+ {{inlinedTemplates}}
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Reactive Task Graph — derives dependency edges from task plan IO signatures.
3
+ *
4
+ * Pure functions that build a DAG from task IO intersections and resolve
5
+ * which tasks are currently ready for parallel dispatch. Used by the
6
+ * reactive-execute dispatch path (ADR-004).
7
+ *
8
+ * Graph derivation and resolution functions are pure (no filesystem access).
9
+ * The `loadSliceTaskIO` loader at the bottom is the only async/IO function.
10
+ */
11
+ import { loadFile, parsePlan, parseTaskPlanIO } from "./files.js";
12
+ import { resolveTasksDir, resolveTaskFiles } from "./paths.js";
13
+ import { join } from "node:path";
14
+ import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
15
+ import { existsSync, unlinkSync } from "node:fs";
16
+ // ─── Graph Construction ───────────────────────────────────────────────────
17
+ /**
18
+ * Build a dependency graph from task IO signatures.
19
+ *
20
+ * A task T_b depends on T_a when any of T_b's inputFiles appear in T_a's
21
+ * outputFiles. Self-references are excluded.
22
+ *
23
+ * Tasks are returned in the same order as the input array.
24
+ */
25
+ export function deriveTaskGraph(tasks) {
26
+ // Build output → producer lookup
27
+ const outputToProducer = new Map();
28
+ for (const task of tasks) {
29
+ for (const outFile of task.outputFiles) {
30
+ const existing = outputToProducer.get(outFile);
31
+ if (existing) {
32
+ existing.push(task.id);
33
+ }
34
+ else {
35
+ outputToProducer.set(outFile, [task.id]);
36
+ }
37
+ }
38
+ }
39
+ return tasks.map((task) => {
40
+ const deps = new Set();
41
+ for (const inFile of task.inputFiles) {
42
+ const producers = outputToProducer.get(inFile);
43
+ if (producers) {
44
+ for (const pid of producers) {
45
+ if (pid !== task.id)
46
+ deps.add(pid);
47
+ }
48
+ }
49
+ }
50
+ return {
51
+ ...task,
52
+ dependsOn: [...deps].sort(),
53
+ };
54
+ });
55
+ }
56
+ // ─── Ready Set Resolution ─────────────────────────────────────────────────
57
+ /**
58
+ * Return task IDs whose dependencies are all in `completed`.
59
+ * Excludes tasks that are already done or in-flight.
60
+ */
61
+ export function getReadyTasks(graph, completed, inFlight) {
62
+ return graph
63
+ .filter((node) => {
64
+ if (node.done || completed.has(node.id) || inFlight.has(node.id))
65
+ return false;
66
+ return node.dependsOn.every((dep) => completed.has(dep));
67
+ })
68
+ .map((node) => node.id);
69
+ }
70
+ // ─── Conflict-Free Subset Selection ──────────────────────────────────────
71
+ /**
72
+ * Greedy selection of non-conflicting tasks up to `maxParallel`.
73
+ *
74
+ * Two tasks conflict if they share any outputFile. We also exclude tasks
75
+ * whose outputs overlap with `inFlightOutputs` (files being written by
76
+ * tasks currently in progress).
77
+ */
78
+ export function chooseNonConflictingSubset(readyIds, graph, maxParallel, inFlightOutputs) {
79
+ const nodeMap = new Map(graph.map((n) => [n.id, n]));
80
+ const claimed = new Set(inFlightOutputs);
81
+ const selected = [];
82
+ for (const id of readyIds) {
83
+ if (selected.length >= maxParallel)
84
+ break;
85
+ const node = nodeMap.get(id);
86
+ if (!node)
87
+ continue;
88
+ // Check for output overlap with already-selected or in-flight
89
+ const conflicts = node.outputFiles.some((f) => claimed.has(f));
90
+ if (conflicts)
91
+ continue;
92
+ // Claim this task's outputs
93
+ for (const f of node.outputFiles)
94
+ claimed.add(f);
95
+ selected.push(id);
96
+ }
97
+ return selected;
98
+ }
99
+ // ─── Graph Quality Checks ─────────────────────────────────────────────────
100
+ /**
101
+ * Returns true if any incomplete task has 0 inputFiles AND 0 outputFiles.
102
+ *
103
+ * An ambiguous graph means IO annotations are too sparse to derive reliable
104
+ * edges — the dispatcher should fall back to sequential execution.
105
+ */
106
+ export function isGraphAmbiguous(graph) {
107
+ return graph.some((node) => !node.done &&
108
+ node.inputFiles.length === 0 &&
109
+ node.outputFiles.length === 0);
110
+ }
111
+ /**
112
+ * Detect deadlock: no tasks are ready and none are in-flight, yet incomplete
113
+ * tasks remain. This indicates a circular dependency or impossible state.
114
+ */
115
+ export function detectDeadlock(graph, completed, inFlight) {
116
+ const incomplete = graph.filter((n) => !n.done && !completed.has(n.id) && !inFlight.has(n.id));
117
+ if (incomplete.length === 0)
118
+ return false; // all done
119
+ if (inFlight.size > 0)
120
+ return false; // something is running, wait for it
121
+ // Nothing in flight, but incomplete tasks remain — check if any are ready
122
+ const ready = getReadyTasks(graph, completed, inFlight);
123
+ return ready.length === 0;
124
+ }
125
+ // ─── Graph Metrics ────────────────────────────────────────────────────────
126
+ /** Compute summary metrics for logging. */
127
+ export function graphMetrics(graph) {
128
+ const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
129
+ const ready = getReadyTasks(graph, completed, new Set());
130
+ const edgeCount = graph.reduce((sum, n) => sum + n.dependsOn.length, 0);
131
+ return {
132
+ taskCount: graph.length,
133
+ edgeCount,
134
+ readySetSize: ready.length,
135
+ ambiguous: isGraphAmbiguous(graph),
136
+ };
137
+ }
138
+ // ─── IO Loader (async, filesystem) ────────────────────────────────────────
139
+ /**
140
+ * Load TaskIO for all tasks in a slice by reading the slice plan (for done
141
+ * status and task IDs) and individual task plan files (for IO sections).
142
+ *
143
+ * Returns [] when the slice plan or tasks directory doesn't exist.
144
+ */
145
+ export async function loadSliceTaskIO(basePath, mid, sid) {
146
+ const { resolveSliceFile } = await import("./paths.js");
147
+ const slicePlanPath = resolveSliceFile(basePath, mid, sid, "PLAN");
148
+ const planContent = slicePlanPath ? await loadFile(slicePlanPath) : null;
149
+ if (!planContent)
150
+ return [];
151
+ const plan = parsePlan(planContent);
152
+ const tDir = resolveTasksDir(basePath, mid, sid);
153
+ if (!tDir)
154
+ return [];
155
+ const results = [];
156
+ for (const taskEntry of plan.tasks) {
157
+ const planFiles = resolveTaskFiles(tDir, "PLAN");
158
+ const taskFileName = planFiles.find((f) => f.toUpperCase().startsWith(taskEntry.id.toUpperCase() + "-"));
159
+ if (!taskFileName) {
160
+ // Task plan file missing — include with empty IO (will trigger ambiguous)
161
+ results.push({
162
+ id: taskEntry.id,
163
+ title: taskEntry.title,
164
+ inputFiles: [],
165
+ outputFiles: [],
166
+ done: taskEntry.done,
167
+ });
168
+ continue;
169
+ }
170
+ const taskContent = await loadFile(join(tDir, taskFileName));
171
+ if (!taskContent) {
172
+ results.push({
173
+ id: taskEntry.id,
174
+ title: taskEntry.title,
175
+ inputFiles: [],
176
+ outputFiles: [],
177
+ done: taskEntry.done,
178
+ });
179
+ continue;
180
+ }
181
+ const io = parseTaskPlanIO(taskContent);
182
+ results.push({
183
+ id: taskEntry.id,
184
+ title: taskEntry.title,
185
+ inputFiles: io.inputFiles,
186
+ outputFiles: io.outputFiles,
187
+ done: taskEntry.done,
188
+ });
189
+ }
190
+ return results;
191
+ }
192
+ // ─── State Persistence ────────────────────────────────────────────────────
193
+ function reactiveStatePath(basePath, mid, sid) {
194
+ return join(basePath, ".gsd", "runtime", `${mid}-${sid}-reactive.json`);
195
+ }
196
+ function isReactiveState(data) {
197
+ if (!data || typeof data !== "object")
198
+ return false;
199
+ const d = data;
200
+ return typeof d.sliceId === "string" && Array.isArray(d.completed);
201
+ }
202
+ /**
203
+ * Load persisted reactive execution state for a slice.
204
+ * Returns null when no state file exists or the file is invalid.
205
+ */
206
+ export function loadReactiveState(basePath, mid, sid) {
207
+ return loadJsonFileOrNull(reactiveStatePath(basePath, mid, sid), isReactiveState);
208
+ }
209
+ /**
210
+ * Save reactive execution state to disk.
211
+ */
212
+ export function saveReactiveState(basePath, mid, sid, state) {
213
+ saveJsonFile(reactiveStatePath(basePath, mid, sid), state);
214
+ }
215
+ /**
216
+ * Remove the reactive state file when a slice completes.
217
+ */
218
+ export function clearReactiveState(basePath, mid, sid) {
219
+ const path = reactiveStatePath(basePath, mid, sid);
220
+ try {
221
+ if (existsSync(path))
222
+ unlinkSync(path);
223
+ }
224
+ catch {
225
+ // Non-fatal
226
+ }
227
+ }
@@ -320,7 +320,7 @@ export function updateSessionLock(basePath, unitType, unitId, completedUnits, se
320
320
  *
321
321
  * This is called periodically during the dispatch loop.
322
322
  */
323
- export function validateSessionLock(basePath) {
323
+ export function getSessionLockStatus(basePath) {
324
324
  // Lock was compromised by proper-lockfile (mtime drift from sleep, stall, etc.)
325
325
  if (_lockCompromised) {
326
326
  // Recovery gate (#1512): Before declaring the lock lost, check if the lock
@@ -335,27 +335,47 @@ export function validateSessionLock(basePath) {
335
335
  const result = acquireSessionLock(basePath);
336
336
  if (result.acquired) {
337
337
  process.stderr.write(`[gsd] Lock recovered after onCompromised — lock file PID matched, re-acquired.\n`);
338
- return true;
338
+ return { valid: true, recovered: true };
339
339
  }
340
340
  }
341
341
  catch {
342
342
  // Re-acquisition failed — fall through to return false
343
343
  }
344
344
  }
345
- return false;
345
+ return {
346
+ valid: false,
347
+ failureReason: "compromised",
348
+ existingPid: existing?.pid,
349
+ expectedPid: process.pid,
350
+ };
346
351
  }
347
352
  // If we have an OS-level lock, we're still the owner
348
353
  if (_releaseFunction && _lockedPath === basePath) {
349
- return true;
354
+ return { valid: true };
350
355
  }
351
356
  // Fallback: check the lock file PID
352
357
  const lp = lockPath(basePath);
353
358
  const existing = readExistingLockData(lp);
354
359
  if (!existing) {
355
360
  // Lock file was deleted — we lost ownership
356
- return false;
361
+ return {
362
+ valid: false,
363
+ failureReason: "missing-metadata",
364
+ expectedPid: process.pid,
365
+ };
366
+ }
367
+ if (existing.pid !== process.pid) {
368
+ return {
369
+ valid: false,
370
+ failureReason: "pid-mismatch",
371
+ existingPid: existing.pid,
372
+ expectedPid: process.pid,
373
+ };
357
374
  }
358
- return existing.pid === process.pid;
375
+ return { valid: true };
376
+ }
377
+ export function validateSessionLock(basePath) {
378
+ return getSessionLockStatus(basePath).valid;
359
379
  }
360
380
  /**
361
381
  * Release the session lock. Called on clean stop/pause.
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Shared formatting and layout utilities for TUI dashboard components.
2
+ * Shared pure formatting utilities no @gsd/pi-tui dependency.
3
3
  *
4
- * Consolidates helpers that were previously duplicated across
5
- * auto-dashboard.ts, dashboard-overlay.ts, and visualizer-views.ts.
4
+ * ANSI-aware layout helpers (padRight, joinColumns, centerLine, fitColumns)
5
+ * live in layout-utils.ts to avoid pulling @gsd/pi-tui into modules that
6
+ * run outside jiti's alias resolution (e.g. HTML report generation via
7
+ * dynamic import in auto-loop).
6
8
  */
7
- import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
8
9
  // ─── Duration Formatting ──────────────────────────────────────────────────────
9
10
  /** Format a millisecond duration as a compact human-readable string. */
10
11
  export function formatDuration(ms) {
@@ -30,43 +31,6 @@ export function formatTokenCount(count) {
30
31
  return `${(count / 1000).toFixed(1)}k`;
31
32
  return `${(count / 1_000_000).toFixed(2)}M`;
32
33
  }
33
- // ─── Layout Helpers ───────────────────────────────────────────────────────────
34
- /** Pad a string with trailing spaces to fill `width` (ANSI-aware). */
35
- export function padRight(content, width) {
36
- const vis = visibleWidth(content);
37
- return content + " ".repeat(Math.max(0, width - vis));
38
- }
39
- /** Build a line with left-aligned and right-aligned content. */
40
- export function joinColumns(left, right, width) {
41
- const leftW = visibleWidth(left);
42
- const rightW = visibleWidth(right);
43
- if (leftW + rightW + 2 > width) {
44
- return truncateToWidth(`${left} ${right}`, width);
45
- }
46
- return left + " ".repeat(width - leftW - rightW) + right;
47
- }
48
- /** Center content within `width` (ANSI-aware). */
49
- export function centerLine(content, width) {
50
- const vis = visibleWidth(content);
51
- if (vis >= width)
52
- return truncateToWidth(content, width);
53
- const leftPad = Math.floor((width - vis) / 2);
54
- return " ".repeat(leftPad) + content;
55
- }
56
- /** Join as many parts as fit within `width`, separated by `separator`. */
57
- export function fitColumns(parts, width, separator = " ") {
58
- const filtered = parts.filter(Boolean);
59
- if (filtered.length === 0)
60
- return "";
61
- let result = filtered[0];
62
- for (let i = 1; i < filtered.length; i++) {
63
- const candidate = `${result}${separator}${filtered[i]}`;
64
- if (visibleWidth(candidate) > width)
65
- break;
66
- result = candidate;
67
- }
68
- return truncateToWidth(result, width);
69
- }
70
34
  // ─── Text Truncation ─────────────────────────────────────────────────────────
71
35
  /** Truncate a string to `maxLength` characters, replacing the last character with an ellipsis if needed. */
72
36
  export function truncateWithEllipsis(text, maxLength) {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * ANSI-aware TUI layout utilities that depend on @gsd/pi-tui.
3
+ *
4
+ * Separated from format-utils.ts so that modules needing only pure
5
+ * formatting (e.g. HTML report generation) can import format-utils
6
+ * without pulling in the @gsd/pi-tui dependency — which fails when
7
+ * loaded outside jiti's alias resolution context.
8
+ */
9
+ import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
10
+ // ─── Layout Helpers ───────────────────────────────────────────────────────────
11
+ /** Pad a string with trailing spaces to fill `width` (ANSI-aware). */
12
+ export function padRight(content, width) {
13
+ const vis = visibleWidth(content);
14
+ return content + " ".repeat(Math.max(0, width - vis));
15
+ }
16
+ /** Build a line with left-aligned and right-aligned content. */
17
+ export function joinColumns(left, right, width) {
18
+ const leftW = visibleWidth(left);
19
+ const rightW = visibleWidth(right);
20
+ if (leftW + rightW + 2 > width) {
21
+ return truncateToWidth(`${left} ${right}`, width);
22
+ }
23
+ return left + " ".repeat(width - leftW - rightW) + right;
24
+ }
25
+ /** Center content within `width` (ANSI-aware). */
26
+ export function centerLine(content, width) {
27
+ const vis = visibleWidth(content);
28
+ if (vis >= width)
29
+ return truncateToWidth(content, width);
30
+ const leftPad = Math.floor((width - vis) / 2);
31
+ return " ".repeat(leftPad) + content;
32
+ }
33
+ /** Join as many parts as fit within `width`, separated by `separator`. */
34
+ export function fitColumns(parts, width, separator = " ") {
35
+ const filtered = parts.filter(Boolean);
36
+ if (filtered.length === 0)
37
+ return "";
38
+ let result = filtered[0];
39
+ for (let i = 1; i < filtered.length; i++) {
40
+ const candidate = `${result}${separator}${filtered[i]}`;
41
+ if (visibleWidth(candidate) > width)
42
+ break;
43
+ result = candidate;
44
+ }
45
+ return truncateToWidth(result, width);
46
+ }
@@ -1,6 +1,7 @@
1
1
  // Barrel file — re-exports consumed by external modules
2
2
  export { makeUI, GLYPH, INDENT, STATUS_GLYPH, STATUS_COLOR, } from "./ui.js";
3
- export { stripAnsi, formatTokenCount, formatDuration, padRight, joinColumns, centerLine, fitColumns, sparkline, normalizeStringArray, fileLink, } from "./format-utils.js";
3
+ export { stripAnsi, formatTokenCount, formatDuration, sparkline, normalizeStringArray, fileLink, } from "./format-utils.js";
4
+ export { padRight, joinColumns, centerLine, fitColumns, } from "./layout-utils.js";
4
5
  export { shortcutDesc } from "./terminal.js";
5
6
  export { toPosixPath } from "./path-display.js";
6
7
  export { showInterviewRound } from "./interview-ui.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.37.0",
3
+ "version": "2.37.1-dev.49503be",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -84,6 +84,7 @@
84
84
  },
85
85
  "dependencies": {
86
86
  "@anthropic-ai/sdk": "^0.73.0",
87
+ "@anthropic-ai/vertex-sdk": "^0.14.4",
87
88
  "@aws-sdk/client-bedrock-runtime": "^3.983.0",
88
89
  "@clack/prompts": "^1.1.0",
89
90
  "@google/genai": "^1.40.0",
@@ -53,6 +53,19 @@ export function getEnvApiKey(provider) {
53
53
  if (provider === "anthropic") {
54
54
  return process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
55
55
  }
56
+ // Anthropic on Vertex AI uses Application Default Credentials.
57
+ // Detected via ANTHROPIC_VERTEX_PROJECT_ID (same env var as Claude Code).
58
+ if (provider === "anthropic-vertex") {
59
+ const hasProject = !!process.env.ANTHROPIC_VERTEX_PROJECT_ID;
60
+ if (hasProject) {
61
+ return "<authenticated>";
62
+ }
63
+ // Fall back to Google Cloud project env vars
64
+ const hasGoogleProject = !!(process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT);
65
+ if (hasGoogleProject && hasVertexAdcCredentials()) {
66
+ return "<authenticated>";
67
+ }
68
+ }
56
69
  // Vertex AI uses Application Default Credentials, not API keys.
57
70
  // Auth is configured via `gcloud auth application-default login`.
58
71
  if (provider === "google-vertex") {
@@ -1 +1 @@
1
- {"version":3,"file":"env-api-keys.js","sourceRoot":"","sources":["../src/env-api-keys.ts"],"names":[],"mappings":"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","sourcesContent":["// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui)\nlet _existsSync: typeof import(\"node:fs\").existsSync | null = null;\nlet _homedir: typeof import(\"node:os\").homedir | null = null;\nlet _join: typeof import(\"node:path\").join | null = null;\n\ntype DynamicImport = (specifier: string) => Promise<unknown>;\n\nconst dynamicImport: DynamicImport = (specifier) => import(specifier);\nconst NODE_FS_SPECIFIER = \"node:\" + \"fs\";\nconst NODE_OS_SPECIFIER = \"node:\" + \"os\";\nconst NODE_PATH_SPECIFIER = \"node:\" + \"path\";\n\n// Eagerly load in Node.js/Bun environment only\nif (typeof process !== \"undefined\" && (process.versions?.node || process.versions?.bun)) {\n\tdynamicImport(NODE_FS_SPECIFIER).then((m) => {\n\t\t_existsSync = (m as typeof import(\"node:fs\")).existsSync;\n\t});\n\tdynamicImport(NODE_OS_SPECIFIER).then((m) => {\n\t\t_homedir = (m as typeof import(\"node:os\")).homedir;\n\t});\n\tdynamicImport(NODE_PATH_SPECIFIER).then((m) => {\n\t\t_join = (m as typeof import(\"node:path\")).join;\n\t});\n}\n\nimport type { KnownProvider } from \"./types.js\";\n\nlet cachedVertexAdcCredentialsExists: boolean | null = null;\n\nfunction hasVertexAdcCredentials(): boolean {\n\tif (cachedVertexAdcCredentialsExists === null) {\n\t\t// If node modules haven't loaded yet (async import race at startup),\n\t\t// return false WITHOUT caching so the next call retries once they're ready.\n\t\t// Only cache false permanently in a browser environment where fs is never available.\n\t\tif (!_existsSync || !_homedir || !_join) {\n\t\t\tconst isNode = typeof process !== \"undefined\" && (process.versions?.node || process.versions?.bun);\n\t\t\tif (!isNode) {\n\t\t\t\t// Definitively in a browser — safe to cache false permanently\n\t\t\t\tcachedVertexAdcCredentialsExists = false;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\t// Check GOOGLE_APPLICATION_CREDENTIALS env var first (standard way)\n\t\tconst gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;\n\t\tif (gacPath) {\n\t\t\tcachedVertexAdcCredentialsExists = _existsSync(gacPath);\n\t\t} else {\n\t\t\t// Fall back to default ADC path (lazy evaluation)\n\t\t\tcachedVertexAdcCredentialsExists = _existsSync(\n\t\t\t\t_join(_homedir(), \".config\", \"gcloud\", \"application_default_credentials.json\"),\n\t\t\t);\n\t\t}\n\t}\n\treturn cachedVertexAdcCredentialsExists;\n}\n\n/**\n * Get API key for provider from known environment variables, e.g. OPENAI_API_KEY.\n *\n * Will not return API keys for providers that require OAuth tokens.\n */\nexport function getEnvApiKey(provider: KnownProvider): string | undefined;\nexport function getEnvApiKey(provider: string): string | undefined;\nexport function getEnvApiKey(provider: any): string | undefined {\n\t// Fall back to environment variables\n\tif (provider === \"github-copilot\") {\n\t\treturn process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;\n\t}\n\n\t// ANTHROPIC_OAUTH_TOKEN takes precedence over ANTHROPIC_API_KEY\n\tif (provider === \"anthropic\") {\n\t\treturn process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\t}\n\n\t// Vertex AI uses Application Default Credentials, not API keys.\n\t// Auth is configured via `gcloud auth application-default login`.\n\tif (provider === \"google-vertex\") {\n\t\tconst hasCredentials = hasVertexAdcCredentials();\n\t\tconst hasProject = !!(process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT);\n\t\tconst hasLocation = !!process.env.GOOGLE_CLOUD_LOCATION;\n\n\t\tif (hasCredentials && hasProject && hasLocation) {\n\t\t\treturn \"<authenticated>\";\n\t\t}\n\t}\n\n\tif (provider === \"amazon-bedrock\") {\n\t\t// Amazon Bedrock supports multiple credential sources:\n\t\t// 1. AWS_PROFILE - named profile from ~/.aws/credentials\n\t\t// 2. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY - standard IAM keys\n\t\t// 3. AWS_BEARER_TOKEN_BEDROCK - Bedrock API keys (bearer token)\n\t\t// 4. AWS_CONTAINER_CREDENTIALS_RELATIVE_URI - ECS task roles\n\t\t// 5. AWS_CONTAINER_CREDENTIALS_FULL_URI - ECS task roles (full URI)\n\t\t// 6. AWS_WEB_IDENTITY_TOKEN_FILE - IRSA (IAM Roles for Service Accounts)\n\t\tif (\n\t\t\tprocess.env.AWS_PROFILE ||\n\t\t\t(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) ||\n\t\t\tprocess.env.AWS_BEARER_TOKEN_BEDROCK ||\n\t\t\tprocess.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI ||\n\t\t\tprocess.env.AWS_CONTAINER_CREDENTIALS_FULL_URI ||\n\t\t\tprocess.env.AWS_WEB_IDENTITY_TOKEN_FILE\n\t\t) {\n\t\t\treturn \"<authenticated>\";\n\t\t}\n\t}\n\n\tconst envMap: Record<string, string> = {\n\t\topenai: \"OPENAI_API_KEY\",\n\t\t\"azure-openai-responses\": \"AZURE_OPENAI_API_KEY\",\n\t\tgoogle: \"GEMINI_API_KEY\",\n\t\tgroq: \"GROQ_API_KEY\",\n\t\tcerebras: \"CEREBRAS_API_KEY\",\n\t\txai: \"XAI_API_KEY\",\n\t\topenrouter: \"OPENROUTER_API_KEY\",\n\t\t\"vercel-ai-gateway\": \"AI_GATEWAY_API_KEY\",\n\t\tzai: \"ZAI_API_KEY\",\n\t\tmistral: \"MISTRAL_API_KEY\",\n\t\tminimax: \"MINIMAX_API_KEY\",\n\t\t\"minimax-cn\": \"MINIMAX_CN_API_KEY\",\n\t\thuggingface: \"HF_TOKEN\",\n\t\topencode: \"OPENCODE_API_KEY\",\n\t\t\"opencode-go\": \"OPENCODE_API_KEY\",\n\t\t\"kimi-coding\": \"KIMI_API_KEY\",\n\t\t\"alibaba-coding-plan\": \"ALIBABA_API_KEY\",\n\t\t\"ollama-cloud\": \"OLLAMA_API_KEY\",\n\t\t\"custom-openai\": \"CUSTOM_OPENAI_API_KEY\",\n\t};\n\n\tconst envVar = envMap[provider];\n\treturn envVar ? process.env[envVar] : undefined;\n}\n"]}
1
+ {"version":3,"file":"env-api-keys.js","sourceRoot":"","sources":["../src/env-api-keys.ts"],"names":[],"mappings":"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","sourcesContent":["// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui)\nlet _existsSync: typeof import(\"node:fs\").existsSync | null = null;\nlet _homedir: typeof import(\"node:os\").homedir | null = null;\nlet _join: typeof import(\"node:path\").join | null = null;\n\ntype DynamicImport = (specifier: string) => Promise<unknown>;\n\nconst dynamicImport: DynamicImport = (specifier) => import(specifier);\nconst NODE_FS_SPECIFIER = \"node:\" + \"fs\";\nconst NODE_OS_SPECIFIER = \"node:\" + \"os\";\nconst NODE_PATH_SPECIFIER = \"node:\" + \"path\";\n\n// Eagerly load in Node.js/Bun environment only\nif (typeof process !== \"undefined\" && (process.versions?.node || process.versions?.bun)) {\n\tdynamicImport(NODE_FS_SPECIFIER).then((m) => {\n\t\t_existsSync = (m as typeof import(\"node:fs\")).existsSync;\n\t});\n\tdynamicImport(NODE_OS_SPECIFIER).then((m) => {\n\t\t_homedir = (m as typeof import(\"node:os\")).homedir;\n\t});\n\tdynamicImport(NODE_PATH_SPECIFIER).then((m) => {\n\t\t_join = (m as typeof import(\"node:path\")).join;\n\t});\n}\n\nimport type { KnownProvider } from \"./types.js\";\n\nlet cachedVertexAdcCredentialsExists: boolean | null = null;\n\nfunction hasVertexAdcCredentials(): boolean {\n\tif (cachedVertexAdcCredentialsExists === null) {\n\t\t// If node modules haven't loaded yet (async import race at startup),\n\t\t// return false WITHOUT caching so the next call retries once they're ready.\n\t\t// Only cache false permanently in a browser environment where fs is never available.\n\t\tif (!_existsSync || !_homedir || !_join) {\n\t\t\tconst isNode = typeof process !== \"undefined\" && (process.versions?.node || process.versions?.bun);\n\t\t\tif (!isNode) {\n\t\t\t\t// Definitively in a browser — safe to cache false permanently\n\t\t\t\tcachedVertexAdcCredentialsExists = false;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\t// Check GOOGLE_APPLICATION_CREDENTIALS env var first (standard way)\n\t\tconst gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;\n\t\tif (gacPath) {\n\t\t\tcachedVertexAdcCredentialsExists = _existsSync(gacPath);\n\t\t} else {\n\t\t\t// Fall back to default ADC path (lazy evaluation)\n\t\t\tcachedVertexAdcCredentialsExists = _existsSync(\n\t\t\t\t_join(_homedir(), \".config\", \"gcloud\", \"application_default_credentials.json\"),\n\t\t\t);\n\t\t}\n\t}\n\treturn cachedVertexAdcCredentialsExists;\n}\n\n/**\n * Get API key for provider from known environment variables, e.g. OPENAI_API_KEY.\n *\n * Will not return API keys for providers that require OAuth tokens.\n */\nexport function getEnvApiKey(provider: KnownProvider): string | undefined;\nexport function getEnvApiKey(provider: string): string | undefined;\nexport function getEnvApiKey(provider: any): string | undefined {\n\t// Fall back to environment variables\n\tif (provider === \"github-copilot\") {\n\t\treturn process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;\n\t}\n\n\t// ANTHROPIC_OAUTH_TOKEN takes precedence over ANTHROPIC_API_KEY\n\tif (provider === \"anthropic\") {\n\t\treturn process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\t}\n\n\t// Anthropic on Vertex AI uses Application Default Credentials.\n\t// Detected via ANTHROPIC_VERTEX_PROJECT_ID (same env var as Claude Code).\n\tif (provider === \"anthropic-vertex\") {\n\t\tconst hasProject = !!process.env.ANTHROPIC_VERTEX_PROJECT_ID;\n\t\tif (hasProject) {\n\t\t\treturn \"<authenticated>\";\n\t\t}\n\t\t// Fall back to Google Cloud project env vars\n\t\tconst hasGoogleProject = !!(process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT);\n\t\tif (hasGoogleProject && hasVertexAdcCredentials()) {\n\t\t\treturn \"<authenticated>\";\n\t\t}\n\t}\n\n\t// Vertex AI uses Application Default Credentials, not API keys.\n\t// Auth is configured via `gcloud auth application-default login`.\n\tif (provider === \"google-vertex\") {\n\t\tconst hasCredentials = hasVertexAdcCredentials();\n\t\tconst hasProject = !!(process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT);\n\t\tconst hasLocation = !!process.env.GOOGLE_CLOUD_LOCATION;\n\n\t\tif (hasCredentials && hasProject && hasLocation) {\n\t\t\treturn \"<authenticated>\";\n\t\t}\n\t}\n\n\tif (provider === \"amazon-bedrock\") {\n\t\t// Amazon Bedrock supports multiple credential sources:\n\t\t// 1. AWS_PROFILE - named profile from ~/.aws/credentials\n\t\t// 2. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY - standard IAM keys\n\t\t// 3. AWS_BEARER_TOKEN_BEDROCK - Bedrock API keys (bearer token)\n\t\t// 4. AWS_CONTAINER_CREDENTIALS_RELATIVE_URI - ECS task roles\n\t\t// 5. AWS_CONTAINER_CREDENTIALS_FULL_URI - ECS task roles (full URI)\n\t\t// 6. AWS_WEB_IDENTITY_TOKEN_FILE - IRSA (IAM Roles for Service Accounts)\n\t\tif (\n\t\t\tprocess.env.AWS_PROFILE ||\n\t\t\t(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) ||\n\t\t\tprocess.env.AWS_BEARER_TOKEN_BEDROCK ||\n\t\t\tprocess.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI ||\n\t\t\tprocess.env.AWS_CONTAINER_CREDENTIALS_FULL_URI ||\n\t\t\tprocess.env.AWS_WEB_IDENTITY_TOKEN_FILE\n\t\t) {\n\t\t\treturn \"<authenticated>\";\n\t\t}\n\t}\n\n\tconst envMap: Record<string, string> = {\n\t\topenai: \"OPENAI_API_KEY\",\n\t\t\"azure-openai-responses\": \"AZURE_OPENAI_API_KEY\",\n\t\tgoogle: \"GEMINI_API_KEY\",\n\t\tgroq: \"GROQ_API_KEY\",\n\t\tcerebras: \"CEREBRAS_API_KEY\",\n\t\txai: \"XAI_API_KEY\",\n\t\topenrouter: \"OPENROUTER_API_KEY\",\n\t\t\"vercel-ai-gateway\": \"AI_GATEWAY_API_KEY\",\n\t\tzai: \"ZAI_API_KEY\",\n\t\tmistral: \"MISTRAL_API_KEY\",\n\t\tminimax: \"MINIMAX_API_KEY\",\n\t\t\"minimax-cn\": \"MINIMAX_CN_API_KEY\",\n\t\thuggingface: \"HF_TOKEN\",\n\t\topencode: \"OPENCODE_API_KEY\",\n\t\t\"opencode-go\": \"OPENCODE_API_KEY\",\n\t\t\"kimi-coding\": \"KIMI_API_KEY\",\n\t\t\"alibaba-coding-plan\": \"ALIBABA_API_KEY\",\n\t\t\"ollama-cloud\": \"OLLAMA_API_KEY\",\n\t\t\"custom-openai\": \"CUSTOM_OPENAI_API_KEY\",\n\t};\n\n\tconst envVar = envMap[provider];\n\treturn envVar ? process.env[envVar] : undefined;\n}\n"]}