fixo-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +530 -0
  3. package/dist/agent/agent-client.d.ts +108 -0
  4. package/dist/agent/agent-client.d.ts.map +1 -0
  5. package/dist/agent/agent-client.js +1247 -0
  6. package/dist/agent/agent-client.js.map +1 -0
  7. package/dist/agent/agent-pool.d.ts +20 -0
  8. package/dist/agent/agent-pool.d.ts.map +1 -0
  9. package/dist/agent/agent-pool.js +217 -0
  10. package/dist/agent/agent-pool.js.map +1 -0
  11. package/dist/agent/background-awareness.d.ts +55 -0
  12. package/dist/agent/background-awareness.d.ts.map +1 -0
  13. package/dist/agent/background-awareness.js +104 -0
  14. package/dist/agent/background-awareness.js.map +1 -0
  15. package/dist/agent/command-parser.d.ts +33 -0
  16. package/dist/agent/command-parser.d.ts.map +1 -0
  17. package/dist/agent/command-parser.js +120 -0
  18. package/dist/agent/command-parser.js.map +1 -0
  19. package/dist/agent/context-budget.d.ts +91 -0
  20. package/dist/agent/context-budget.d.ts.map +1 -0
  21. package/dist/agent/context-budget.js +219 -0
  22. package/dist/agent/context-budget.js.map +1 -0
  23. package/dist/agent/conversation.d.ts +190 -0
  24. package/dist/agent/conversation.d.ts.map +1 -0
  25. package/dist/agent/conversation.js +547 -0
  26. package/dist/agent/conversation.js.map +1 -0
  27. package/dist/agent/hooks.d.ts +72 -0
  28. package/dist/agent/hooks.d.ts.map +1 -0
  29. package/dist/agent/hooks.js +214 -0
  30. package/dist/agent/hooks.js.map +1 -0
  31. package/dist/agent/mcp-bridge.d.ts +13 -0
  32. package/dist/agent/mcp-bridge.d.ts.map +1 -0
  33. package/dist/agent/mcp-bridge.js +86 -0
  34. package/dist/agent/mcp-bridge.js.map +1 -0
  35. package/dist/agent/mcp-client.d.ts +24 -0
  36. package/dist/agent/mcp-client.d.ts.map +1 -0
  37. package/dist/agent/mcp-client.js +146 -0
  38. package/dist/agent/mcp-client.js.map +1 -0
  39. package/dist/agent/mcp-manager.d.ts +13 -0
  40. package/dist/agent/mcp-manager.d.ts.map +1 -0
  41. package/dist/agent/mcp-manager.js +84 -0
  42. package/dist/agent/mcp-manager.js.map +1 -0
  43. package/dist/agent/mcp-registry.d.ts +45 -0
  44. package/dist/agent/mcp-registry.d.ts.map +1 -0
  45. package/dist/agent/mcp-registry.js +98 -0
  46. package/dist/agent/mcp-registry.js.map +1 -0
  47. package/dist/agent/orchestrator.d.ts +14 -0
  48. package/dist/agent/orchestrator.d.ts.map +1 -0
  49. package/dist/agent/orchestrator.js +118 -0
  50. package/dist/agent/orchestrator.js.map +1 -0
  51. package/dist/agent/parser-adapter.d.ts +120 -0
  52. package/dist/agent/parser-adapter.d.ts.map +1 -0
  53. package/dist/agent/parser-adapter.js +265 -0
  54. package/dist/agent/parser-adapter.js.map +1 -0
  55. package/dist/agent/parsers/imports.d.ts +11 -0
  56. package/dist/agent/parsers/imports.d.ts.map +1 -0
  57. package/dist/agent/parsers/imports.js +94 -0
  58. package/dist/agent/parsers/imports.js.map +1 -0
  59. package/dist/agent/parsers/shell.d.ts +23 -0
  60. package/dist/agent/parsers/shell.d.ts.map +1 -0
  61. package/dist/agent/parsers/shell.js +200 -0
  62. package/dist/agent/parsers/shell.js.map +1 -0
  63. package/dist/agent/parsers/symbols.d.ts +17 -0
  64. package/dist/agent/parsers/symbols.d.ts.map +1 -0
  65. package/dist/agent/parsers/symbols.js +103 -0
  66. package/dist/agent/parsers/symbols.js.map +1 -0
  67. package/dist/agent/permissions.d.ts +65 -0
  68. package/dist/agent/permissions.d.ts.map +1 -0
  69. package/dist/agent/permissions.js +219 -0
  70. package/dist/agent/permissions.js.map +1 -0
  71. package/dist/agent/predictive-gate.d.ts +69 -0
  72. package/dist/agent/predictive-gate.d.ts.map +1 -0
  73. package/dist/agent/predictive-gate.js +128 -0
  74. package/dist/agent/predictive-gate.js.map +1 -0
  75. package/dist/agent/provider-cooldown.d.ts +144 -0
  76. package/dist/agent/provider-cooldown.d.ts.map +1 -0
  77. package/dist/agent/provider-cooldown.js +300 -0
  78. package/dist/agent/provider-cooldown.js.map +1 -0
  79. package/dist/agent/providers-manager.d.ts +109 -0
  80. package/dist/agent/providers-manager.d.ts.map +1 -0
  81. package/dist/agent/providers-manager.js +464 -0
  82. package/dist/agent/providers-manager.js.map +1 -0
  83. package/dist/agent/repo-map.d.ts +6 -0
  84. package/dist/agent/repo-map.d.ts.map +1 -0
  85. package/dist/agent/repo-map.js +221 -0
  86. package/dist/agent/repo-map.js.map +1 -0
  87. package/dist/agent/retry.d.ts +103 -0
  88. package/dist/agent/retry.d.ts.map +1 -0
  89. package/dist/agent/retry.js +276 -0
  90. package/dist/agent/retry.js.map +1 -0
  91. package/dist/agent/search/index.d.ts +61 -0
  92. package/dist/agent/search/index.d.ts.map +1 -0
  93. package/dist/agent/search/index.js +314 -0
  94. package/dist/agent/search/index.js.map +1 -0
  95. package/dist/agent/single-agent.d.ts +76 -0
  96. package/dist/agent/single-agent.d.ts.map +1 -0
  97. package/dist/agent/single-agent.js +697 -0
  98. package/dist/agent/single-agent.js.map +1 -0
  99. package/dist/agent/skills.d.ts +22 -0
  100. package/dist/agent/skills.d.ts.map +1 -0
  101. package/dist/agent/skills.js +139 -0
  102. package/dist/agent/skills.js.map +1 -0
  103. package/dist/agent/stream-glue.d.ts +85 -0
  104. package/dist/agent/stream-glue.d.ts.map +1 -0
  105. package/dist/agent/stream-glue.js +120 -0
  106. package/dist/agent/stream-glue.js.map +1 -0
  107. package/dist/agent/subagent.d.ts +72 -0
  108. package/dist/agent/subagent.d.ts.map +1 -0
  109. package/dist/agent/subagent.js +193 -0
  110. package/dist/agent/subagent.js.map +1 -0
  111. package/dist/agent/telemetry.d.ts +192 -0
  112. package/dist/agent/telemetry.d.ts.map +1 -0
  113. package/dist/agent/telemetry.js +400 -0
  114. package/dist/agent/telemetry.js.map +1 -0
  115. package/dist/agent/tokenizer.d.ts +42 -0
  116. package/dist/agent/tokenizer.d.ts.map +1 -0
  117. package/dist/agent/tokenizer.js +107 -0
  118. package/dist/agent/tokenizer.js.map +1 -0
  119. package/dist/agent/tool-executor.d.ts +289 -0
  120. package/dist/agent/tool-executor.d.ts.map +1 -0
  121. package/dist/agent/tool-executor.js +2519 -0
  122. package/dist/agent/tool-executor.js.map +1 -0
  123. package/dist/agent/web-impl.d.ts +2 -0
  124. package/dist/agent/web-impl.d.ts.map +1 -0
  125. package/dist/agent/web-impl.js +34 -0
  126. package/dist/agent/web-impl.js.map +1 -0
  127. package/dist/agent/web.d.ts +8 -0
  128. package/dist/agent/web.d.ts.map +1 -0
  129. package/dist/agent/web.js +8 -0
  130. package/dist/agent/web.js.map +1 -0
  131. package/dist/agent/worker-agent.d.ts +27 -0
  132. package/dist/agent/worker-agent.d.ts.map +1 -0
  133. package/dist/agent/worker-agent.js +503 -0
  134. package/dist/agent/worker-agent.js.map +1 -0
  135. package/dist/config.d.ts +162 -0
  136. package/dist/config.d.ts.map +1 -0
  137. package/dist/config.js +138 -0
  138. package/dist/config.js.map +1 -0
  139. package/dist/context/fixo-md-watcher.d.ts +42 -0
  140. package/dist/context/fixo-md-watcher.d.ts.map +1 -0
  141. package/dist/context/fixo-md-watcher.js +126 -0
  142. package/dist/context/fixo-md-watcher.js.map +1 -0
  143. package/dist/context/fixo-md.d.ts +50 -0
  144. package/dist/context/fixo-md.d.ts.map +1 -0
  145. package/dist/context/fixo-md.js +118 -0
  146. package/dist/context/fixo-md.js.map +1 -0
  147. package/dist/context/todo.d.ts +65 -0
  148. package/dist/context/todo.d.ts.map +1 -0
  149. package/dist/context/todo.js +194 -0
  150. package/dist/context/todo.js.map +1 -0
  151. package/dist/git/git-manager.d.ts +33 -0
  152. package/dist/git/git-manager.d.ts.map +1 -0
  153. package/dist/git/git-manager.js +293 -0
  154. package/dist/git/git-manager.js.map +1 -0
  155. package/dist/git/git-ops.d.ts +10 -0
  156. package/dist/git/git-ops.d.ts.map +1 -0
  157. package/dist/git/git-ops.js +131 -0
  158. package/dist/git/git-ops.js.map +1 -0
  159. package/dist/index.d.ts +3 -0
  160. package/dist/index.d.ts.map +1 -0
  161. package/dist/index.js +352 -0
  162. package/dist/index.js.map +1 -0
  163. package/dist/indexer.d.ts +30 -0
  164. package/dist/indexer.d.ts.map +1 -0
  165. package/dist/indexer.js +273 -0
  166. package/dist/indexer.js.map +1 -0
  167. package/dist/lsp/lsp-client.d.ts +24 -0
  168. package/dist/lsp/lsp-client.d.ts.map +1 -0
  169. package/dist/lsp/lsp-client.js +205 -0
  170. package/dist/lsp/lsp-client.js.map +1 -0
  171. package/dist/lsp/lsp-manager.d.ts +17 -0
  172. package/dist/lsp/lsp-manager.d.ts.map +1 -0
  173. package/dist/lsp/lsp-manager.js +154 -0
  174. package/dist/lsp/lsp-manager.js.map +1 -0
  175. package/dist/lsp/lsp-pre-save.d.ts +137 -0
  176. package/dist/lsp/lsp-pre-save.d.ts.map +1 -0
  177. package/dist/lsp/lsp-pre-save.js +245 -0
  178. package/dist/lsp/lsp-pre-save.js.map +1 -0
  179. package/dist/lsp/syntax-fallback.d.ts +83 -0
  180. package/dist/lsp/syntax-fallback.d.ts.map +1 -0
  181. package/dist/lsp/syntax-fallback.js +275 -0
  182. package/dist/lsp/syntax-fallback.js.map +1 -0
  183. package/dist/model-outcomes.d.ts +12 -0
  184. package/dist/model-outcomes.d.ts.map +1 -0
  185. package/dist/model-outcomes.js +46 -0
  186. package/dist/model-outcomes.js.map +1 -0
  187. package/dist/planner.d.ts +32 -0
  188. package/dist/planner.d.ts.map +1 -0
  189. package/dist/planner.js +163 -0
  190. package/dist/planner.js.map +1 -0
  191. package/dist/project-memory.d.ts +29 -0
  192. package/dist/project-memory.d.ts.map +1 -0
  193. package/dist/project-memory.js +349 -0
  194. package/dist/project-memory.js.map +1 -0
  195. package/dist/review.d.ts +2 -0
  196. package/dist/review.d.ts.map +1 -0
  197. package/dist/review.js +61 -0
  198. package/dist/review.js.map +1 -0
  199. package/dist/runtime/background-jobs.d.ts +97 -0
  200. package/dist/runtime/background-jobs.d.ts.map +1 -0
  201. package/dist/runtime/background-jobs.js +331 -0
  202. package/dist/runtime/background-jobs.js.map +1 -0
  203. package/dist/runtime/credential-vault.d.ts +124 -0
  204. package/dist/runtime/credential-vault.d.ts.map +1 -0
  205. package/dist/runtime/credential-vault.js +184 -0
  206. package/dist/runtime/credential-vault.js.map +1 -0
  207. package/dist/runtime/loop-trap.d.ts +197 -0
  208. package/dist/runtime/loop-trap.d.ts.map +1 -0
  209. package/dist/runtime/loop-trap.js +420 -0
  210. package/dist/runtime/loop-trap.js.map +1 -0
  211. package/dist/runtime/policy.d.ts +15 -0
  212. package/dist/runtime/policy.d.ts.map +1 -0
  213. package/dist/runtime/policy.js +60 -0
  214. package/dist/runtime/policy.js.map +1 -0
  215. package/dist/runtime/redaction.d.ts +66 -0
  216. package/dist/runtime/redaction.d.ts.map +1 -0
  217. package/dist/runtime/redaction.js +155 -0
  218. package/dist/runtime/redaction.js.map +1 -0
  219. package/dist/runtime/session-snapshots.d.ts +76 -0
  220. package/dist/runtime/session-snapshots.d.ts.map +1 -0
  221. package/dist/runtime/session-snapshots.js +166 -0
  222. package/dist/runtime/session-snapshots.js.map +1 -0
  223. package/dist/runtime/staging.d.ts +205 -0
  224. package/dist/runtime/staging.d.ts.map +1 -0
  225. package/dist/runtime/staging.js +526 -0
  226. package/dist/runtime/staging.js.map +1 -0
  227. package/dist/runtime/task-session.d.ts +95 -0
  228. package/dist/runtime/task-session.d.ts.map +1 -0
  229. package/dist/runtime/task-session.js +263 -0
  230. package/dist/runtime/task-session.js.map +1 -0
  231. package/dist/runtime/worktree.d.ts +55 -0
  232. package/dist/runtime/worktree.d.ts.map +1 -0
  233. package/dist/runtime/worktree.js +175 -0
  234. package/dist/runtime/worktree.js.map +1 -0
  235. package/dist/setup-wizard.d.ts +8 -0
  236. package/dist/setup-wizard.d.ts.map +1 -0
  237. package/dist/setup-wizard.js +73 -0
  238. package/dist/setup-wizard.js.map +1 -0
  239. package/dist/shared/content.d.ts +43 -0
  240. package/dist/shared/content.d.ts.map +1 -0
  241. package/dist/shared/content.js +61 -0
  242. package/dist/shared/content.js.map +1 -0
  243. package/dist/shared/types.d.ts +217 -0
  244. package/dist/shared/types.d.ts.map +1 -0
  245. package/dist/shared/types.js +3 -0
  246. package/dist/shared/types.js.map +1 -0
  247. package/dist/test-runner.d.ts +5 -0
  248. package/dist/test-runner.d.ts.map +1 -0
  249. package/dist/test-runner.js +42 -0
  250. package/dist/test-runner.js.map +1 -0
  251. package/dist/types.d.ts +85 -0
  252. package/dist/types.d.ts.map +1 -0
  253. package/dist/types.js +2 -0
  254. package/dist/types.js.map +1 -0
  255. package/dist/ui/ascii.d.ts +23 -0
  256. package/dist/ui/ascii.d.ts.map +1 -0
  257. package/dist/ui/ascii.js +45 -0
  258. package/dist/ui/ascii.js.map +1 -0
  259. package/dist/ui/colors.d.ts +111 -0
  260. package/dist/ui/colors.d.ts.map +1 -0
  261. package/dist/ui/colors.js +166 -0
  262. package/dist/ui/colors.js.map +1 -0
  263. package/dist/ui/image-attach.d.ts +27 -0
  264. package/dist/ui/image-attach.d.ts.map +1 -0
  265. package/dist/ui/image-attach.js +100 -0
  266. package/dist/ui/image-attach.js.map +1 -0
  267. package/dist/ui/index.d.ts +18 -0
  268. package/dist/ui/index.d.ts.map +1 -0
  269. package/dist/ui/index.js +18 -0
  270. package/dist/ui/index.js.map +1 -0
  271. package/dist/ui/markdown-stream.d.ts +91 -0
  272. package/dist/ui/markdown-stream.d.ts.map +1 -0
  273. package/dist/ui/markdown-stream.js +524 -0
  274. package/dist/ui/markdown-stream.js.map +1 -0
  275. package/dist/ui/plan-renderer.d.ts +36 -0
  276. package/dist/ui/plan-renderer.d.ts.map +1 -0
  277. package/dist/ui/plan-renderer.js +79 -0
  278. package/dist/ui/plan-renderer.js.map +1 -0
  279. package/dist/ui/prompt.d.ts +11 -0
  280. package/dist/ui/prompt.d.ts.map +1 -0
  281. package/dist/ui/prompt.js +1960 -0
  282. package/dist/ui/prompt.js.map +1 -0
  283. package/dist/ui/render-primitives.d.ts +117 -0
  284. package/dist/ui/render-primitives.d.ts.map +1 -0
  285. package/dist/ui/render-primitives.js +322 -0
  286. package/dist/ui/render-primitives.js.map +1 -0
  287. package/dist/ui/render.d.ts +133 -0
  288. package/dist/ui/render.d.ts.map +1 -0
  289. package/dist/ui/render.js +547 -0
  290. package/dist/ui/render.js.map +1 -0
  291. package/dist/ui/session-header.d.ts +30 -0
  292. package/dist/ui/session-header.d.ts.map +1 -0
  293. package/dist/ui/session-header.js +74 -0
  294. package/dist/ui/session-header.js.map +1 -0
  295. package/dist/workspace-guard.d.ts +68 -0
  296. package/dist/workspace-guard.d.ts.map +1 -0
  297. package/dist/workspace-guard.js +168 -0
  298. package/dist/workspace-guard.js.map +1 -0
  299. package/dist/workspace-lock.d.ts +27 -0
  300. package/dist/workspace-lock.d.ts.map +1 -0
  301. package/dist/workspace-lock.js +95 -0
  302. package/dist/workspace-lock.js.map +1 -0
  303. package/package.json +63 -0
@@ -0,0 +1,331 @@
1
+ /**
2
+ * background-jobs.ts — Async command execution registry.
3
+ *
4
+ * Phase 3.1: lets the agent spawn long-running commands
5
+ * (`run_command_async`) and poll their status without blocking
6
+ * the REPL or the agent loop. Every job is keyed by a
7
+ * `job_<short-uuid>` and stored in an in-memory `Map` so a
8
+ * crashed CLI invocation can rebuild the tail buffers from the
9
+ * on-disk JSON snapshot.
10
+ *
11
+ * Safety:
12
+ * - `spawn` reuses `isCommandSafe` from `command-parser.ts` so
13
+ * a workspace-escape or sensitive-file read is rejected
14
+ * before any process is started.
15
+ * - The `cwd` argument is resolved through `WorkspaceGuard`
16
+ * so the spawned process cannot chdir outside the workspace.
17
+ * - The 64 KiB per-stream cap matches the staging-manager
18
+ * contract — no unbounded buffer growth.
19
+ * - A 1-hour reaper (`setInterval` in `startReaper`) kills any
20
+ * job whose `pid` is no longer alive, even if the executor
21
+ * never received an `exit` event.
22
+ */
23
+ import { spawn } from 'node:child_process';
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { randomUUID } from 'node:crypto';
27
+ import { isCommandSafe } from '../agent/command-parser.js';
28
+ import { WorkspaceGuard } from '../workspace-guard.js';
29
+ import { recordTelemetry, telemetry } from '../agent/telemetry.js';
30
+ /* ──────────────────────── Constants ──────────────────────── */
31
+ const STREAM_CAP_BYTES = 64 * 1024;
32
+ const SNAPSHOT_FLUSH_MS = 5_000;
33
+ const REAPER_INTERVAL_MS = 60_000;
34
+ const REAPER_MAX_AGE_MS = 60 * 60 * 1_000; // 1 hour
35
+ /* ──────────────────────── Helpers ──────────────────────── */
36
+ function appendCapped(buffer, chunk) {
37
+ const next = buffer.text + chunk;
38
+ const bytes = Buffer.byteLength(next, 'utf-8');
39
+ if (bytes > STREAM_CAP_BYTES) {
40
+ // Slice from the END so the user always sees the most recent
41
+ // output, not the oldest. The `totalStdoutBytes` counter
42
+ // preserves the original length so `sinceBytes` can still
43
+ // compute deltas.
44
+ const overflow = bytes - STREAM_CAP_BYTES;
45
+ let startIdx = 0;
46
+ let acc = 0;
47
+ // Walk forward until the cumulative byte count equals the
48
+ // overflow, then slice from there. This is a character-safe
49
+ // approach that avoids splitting a multi-byte UTF-8 char.
50
+ for (let i = 0; i < next.length; i++) {
51
+ acc += Buffer.byteLength(next[i], 'utf-8');
52
+ if (acc > overflow) {
53
+ startIdx = i;
54
+ break;
55
+ }
56
+ }
57
+ buffer.text = next.slice(startIdx);
58
+ buffer.truncated = true;
59
+ }
60
+ else {
61
+ buffer.text = next;
62
+ }
63
+ buffer.bytes += Buffer.byteLength(chunk, 'utf-8');
64
+ }
65
+ function isPidAlive(pid) {
66
+ try {
67
+ process.kill(pid, 0);
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ /* ──────────────────────── Registry ──────────────────────── */
75
+ export class BackgroundJobRegistry {
76
+ jobs = new Map();
77
+ processes = new Map();
78
+ cwd;
79
+ snapshotDir;
80
+ flushTimer = null;
81
+ reaperTimer = null;
82
+ constructor(cwd, opts = {}) {
83
+ this.cwd = cwd;
84
+ this.snapshotDir = opts.snapshotDir ?? path.join(cwd, '.fixo', 'jobs');
85
+ fs.mkdirSync(this.snapshotDir, { recursive: true });
86
+ this.startSnapshotFlusher();
87
+ if (!opts.disableReaper)
88
+ this.startReaper();
89
+ }
90
+ /** Spawn a new background command. Reuses `isCommandSafe` for AST validation. */
91
+ async register(input) {
92
+ if (input.cmd.trim().length === 0) {
93
+ return { ok: false, error: 'cmd is empty' };
94
+ }
95
+ // Resolve the requested cwd through the workspace guard so the
96
+ // child process cannot chdir outside the workspace root.
97
+ const guard = new WorkspaceGuard(this.cwd);
98
+ let resolvedCwd;
99
+ try {
100
+ resolvedCwd = guard.resolve(input.cwd, 'background-job cwd');
101
+ }
102
+ catch (err) {
103
+ return { ok: false, error: err.message };
104
+ }
105
+ const safety = await isCommandSafe(`${input.cmd} ${input.args.join(' ')}`.trim(), this.cwd);
106
+ if (!safety.safe) {
107
+ return { ok: false, error: `command rejected by command-parser: ${safety.reason ?? 'unsafe'}` };
108
+ }
109
+ const id = `job_${randomUUID().slice(0, 8)}`;
110
+ const job = {
111
+ id,
112
+ cmd: input.cmd,
113
+ args: [...input.args],
114
+ cwd: resolvedCwd,
115
+ status: 'running',
116
+ startedAt: new Date().toISOString(),
117
+ stdout: '',
118
+ stderr: '',
119
+ totalStdoutBytes: 0,
120
+ totalStderrBytes: 0,
121
+ stdoutTruncated: false,
122
+ stderrTruncated: false,
123
+ };
124
+ let child;
125
+ try {
126
+ child = spawn(input.cmd, input.args, {
127
+ cwd: resolvedCwd,
128
+ env: { ...process.env, FIXO_BACKGROUND_JOB: id },
129
+ stdio: ['ignore', 'pipe', 'pipe'],
130
+ });
131
+ }
132
+ catch (err) {
133
+ job.status = 'failed';
134
+ job.failureReason = 'spawn threw';
135
+ job.lastError = err.message;
136
+ job.exitedAt = new Date().toISOString();
137
+ this.jobs.set(id, job);
138
+ return { ok: false, jobId: id, error: job.lastError };
139
+ }
140
+ job.pid = child.pid;
141
+ this.jobs.set(id, job);
142
+ this.processes.set(id, child);
143
+ const stdoutBuf = { text: job.stdout, truncated: false, bytes: 0 };
144
+ const stderrBuf = { text: job.stderr, truncated: false, bytes: 0 };
145
+ child.stdout?.setEncoding('utf-8');
146
+ child.stderr?.setEncoding('utf-8');
147
+ child.stdout?.on('data', (chunk) => {
148
+ appendCapped(stdoutBuf, chunk);
149
+ job.stdout = stdoutBuf.text;
150
+ job.stdoutTruncated = stdoutBuf.truncated;
151
+ job.totalStdoutBytes = stdoutBuf.bytes;
152
+ });
153
+ child.stderr?.on('data', (chunk) => {
154
+ appendCapped(stderrBuf, chunk);
155
+ job.stderr = stderrBuf.text;
156
+ job.stderrTruncated = stderrBuf.truncated;
157
+ job.totalStderrBytes = stderrBuf.bytes;
158
+ });
159
+ child.on('error', (err) => {
160
+ job.lastError = err.message;
161
+ // Spawn failures do not emit an `exit` event — flip the
162
+ // status to `failed` here so pollers can detect the
163
+ // terminal state without waiting forever.
164
+ if (job.status === 'running') {
165
+ job.status = 'failed';
166
+ job.failureReason = err.message;
167
+ job.exitedAt = new Date().toISOString();
168
+ this.processes.delete(id);
169
+ }
170
+ });
171
+ child.on('exit', (code, signal) => {
172
+ job.exitedAt = new Date().toISOString();
173
+ if (signal === 'SIGTERM' || signal === 'SIGKILL') {
174
+ job.status = 'killed';
175
+ }
176
+ else if (code === 0) {
177
+ job.status = 'exited';
178
+ }
179
+ else {
180
+ job.status = 'failed';
181
+ job.failureReason = job.lastError ?? `exit code ${String(code)}`;
182
+ }
183
+ job.exitCode = code ?? undefined;
184
+ this.processes.delete(id);
185
+ recordTelemetry(telemetry.asyncSpawn({
186
+ jobId: id,
187
+ cmd: input.cmd,
188
+ pid: job.pid,
189
+ }));
190
+ });
191
+ recordTelemetry(telemetry.asyncSpawn({ jobId: id, cmd: input.cmd, pid: job.pid }));
192
+ return { ok: true, jobId: id, pid: job.pid };
193
+ }
194
+ /** Read a snapshot. Honours `tailLines` and `sinceBytes` for the streams. */
195
+ poll(input) {
196
+ const job = this.jobs.get(input.jobId);
197
+ if (!job)
198
+ return null;
199
+ const tailLines = input.tailLines;
200
+ const sinceBytes = input.sinceBytes ?? 0;
201
+ const sliceTail = (text) => {
202
+ if (tailLines === undefined)
203
+ return text;
204
+ if (tailLines <= 0)
205
+ return '';
206
+ // Trim the trailing empty that comes from a terminal newline
207
+ // so "tail 2" returns the last 2 *content* lines, not
208
+ // "<last content>\n".
209
+ const lines = text.split('\n');
210
+ if (lines.length > 0 && lines[lines.length - 1] === '') {
211
+ lines.pop();
212
+ }
213
+ return lines.slice(Math.max(0, lines.length - tailLines)).join('\n');
214
+ };
215
+ return {
216
+ id: job.id,
217
+ status: job.status,
218
+ exitCode: job.exitCode,
219
+ startedAt: job.startedAt,
220
+ exitedAt: job.exitedAt,
221
+ cmd: job.cmd,
222
+ args: job.args,
223
+ cwd: job.cwd,
224
+ stdout: sliceTail(job.stdout),
225
+ stderr: sliceTail(job.stderr),
226
+ totalStdoutBytes: job.totalStdoutBytes,
227
+ totalStderrBytes: job.totalStderrBytes,
228
+ stdoutTruncated: job.stdoutTruncated,
229
+ stderrTruncated: job.stderrTruncated,
230
+ stdoutDelta: sinceBytes > 0 ? sliceTail(job.stdout.slice(sinceBytes)) : undefined,
231
+ stderrDelta: sinceBytes > 0 ? sliceTail(job.stderr.slice(sinceBytes)) : undefined,
232
+ };
233
+ }
234
+ /** Kill a running job. Returns false if the job was already terminal. */
235
+ kill(jobId) {
236
+ const child = this.processes.get(jobId);
237
+ const job = this.jobs.get(jobId);
238
+ if (!job)
239
+ return { ok: false, error: 'no such job' };
240
+ if (!child)
241
+ return { ok: false, error: `job is ${job.status}; nothing to kill` };
242
+ try {
243
+ child.kill('SIGTERM');
244
+ return { ok: true };
245
+ }
246
+ catch (err) {
247
+ return { ok: false, error: err.message };
248
+ }
249
+ }
250
+ /** List all jobs (newest first). */
251
+ list() {
252
+ return Array.from(this.jobs.values()).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
253
+ }
254
+ /** Read a job directly (no tail / sinceBytes shaping). */
255
+ get(jobId) {
256
+ return this.jobs.get(jobId) ?? null;
257
+ }
258
+ /** Stop timers and kill all running children. Called on CLI shutdown. */
259
+ shutdown() {
260
+ if (this.flushTimer) {
261
+ clearInterval(this.flushTimer);
262
+ this.flushTimer = null;
263
+ }
264
+ if (this.reaperTimer) {
265
+ clearInterval(this.reaperTimer);
266
+ this.reaperTimer = null;
267
+ }
268
+ for (const child of this.processes.values()) {
269
+ try {
270
+ child.kill('SIGTERM');
271
+ }
272
+ catch {
273
+ // best-effort
274
+ }
275
+ }
276
+ this.processes.clear();
277
+ }
278
+ /* ──────── internals ──────── */
279
+ startSnapshotFlusher() {
280
+ if (this.flushTimer)
281
+ return;
282
+ this.flushTimer = setInterval(() => {
283
+ this.flushAllSnapshots();
284
+ }, SNAPSHOT_FLUSH_MS);
285
+ // The flusher is a per-process helper; if the event loop exits
286
+ // (process exit), Node tears it down automatically. We still
287
+ // call `.unref()` so a stuck flusher never holds the loop open.
288
+ this.flushTimer.unref?.();
289
+ }
290
+ flushAllSnapshots() {
291
+ for (const job of this.jobs.values()) {
292
+ const file = path.join(this.snapshotDir, `${job.id}.json`);
293
+ try {
294
+ const tmp = `${file}.tmp`;
295
+ fs.writeFileSync(tmp, JSON.stringify(job, null, 2), { encoding: 'utf-8', mode: 0o600 });
296
+ fs.renameSync(tmp, file);
297
+ }
298
+ catch {
299
+ // best-effort
300
+ }
301
+ }
302
+ }
303
+ startReaper() {
304
+ if (this.reaperTimer)
305
+ return;
306
+ this.reaperTimer = setInterval(() => {
307
+ const now = Date.now();
308
+ for (const [id, child] of this.processes.entries()) {
309
+ const job = this.jobs.get(id);
310
+ if (!job)
311
+ continue;
312
+ const ageMs = now - new Date(job.startedAt).getTime();
313
+ const pidDead = job.pid !== undefined && !isPidAlive(job.pid);
314
+ if (ageMs > REAPER_MAX_AGE_MS || pidDead) {
315
+ try {
316
+ child.kill('SIGKILL');
317
+ }
318
+ catch {
319
+ // best-effort
320
+ }
321
+ job.status = pidDead ? 'failed' : 'killed';
322
+ job.failureReason = pidDead ? 'pid no longer alive' : 'exceeded 1-hour reaper window';
323
+ job.exitedAt = new Date().toISOString();
324
+ this.processes.delete(id);
325
+ }
326
+ }
327
+ }, REAPER_INTERVAL_MS);
328
+ this.reaperTimer.unref?.();
329
+ }
330
+ }
331
+ //# sourceMappingURL=background-jobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"background-jobs.js","sourceRoot":"","sources":["../../src/runtime/background-jobs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Restricted Credential Sandbox — Pillar 4 of the Phase 2 safety
3
+ * refactor. The problem: API keys for OpenAI, Anthropic, Google,
4
+ * AWS, OpenRouter, etc. are long-lived secrets with catastrophic
5
+ * blast radius if leaked into a tool result, an LLM prompt, or a
6
+ * log line. The legacy `ProvidersManager.getDirectConfig(name)`
7
+ * returns the raw key in an object — every caller that touches
8
+ * that object now has the key on its stack, in error messages,
9
+ * in telemetry, and in crash dumps.
10
+ *
11
+ * The fix: a {@link ProviderKeyVault} that owns the credentials
12
+ * in a private, read-only `Map` and exposes them **only** inside
13
+ * a scoped executor block. Outside the block, the vault reveals
14
+ * nothing but metadata (provider names, presence, count). There
15
+ * is no `getApiKey`, no `peek`, no `toJSON`. The only way to
16
+ * read a key is to hand a callback to the vault, and the key
17
+ * is never visible to the caller after the callback returns.
18
+ *
19
+ * The vault is in-memory and process-local; persistence is
20
+ * delegated to `ProvidersManager`, which calls `vault.ingest()`
21
+ * after a disk read. Test code can call `ingest()` directly with
22
+ * fixture credentials.
23
+ *
24
+ * The class is intentionally small (no `any`, no async-only
25
+ * fast paths) so the security boundary is auditable in one read.
26
+ */
27
+ /** A scoped credential, exposed only inside a vault callback. */
28
+ export interface ProviderCredential {
29
+ /** The provider name as it appears in `PROVIDER_REGISTRY`. */
30
+ readonly providerName: string;
31
+ /** The raw API key. Never log, never stringify, never serialise. */
32
+ readonly apiKey: string;
33
+ /** Provider base URL. */
34
+ readonly baseUrl: string;
35
+ /** Display name (e.g. "OpenAI"). */
36
+ readonly displayName: string;
37
+ }
38
+ /** Callback that receives a key. */
39
+ export type KeyCallback<T> = (key: string) => Promise<T> | T;
40
+ /** Callback that receives a full credential. */
41
+ export type CredentialCallback<T> = (cred: ProviderCredential) => Promise<T> | T;
42
+ /** Options for the vault. */
43
+ export interface ProviderKeyVaultOptions {
44
+ /**
45
+ * When true (default), `ingest()` rejects empty / whitespace-only
46
+ * keys. Disable for tests that exercise the empty-key error path
47
+ * via direct construction.
48
+ */
49
+ rejectEmptyKeys?: boolean;
50
+ }
51
+ /** Thrown when a requested provider is not in the vault. */
52
+ export declare class ProviderNotInVaultError extends Error {
53
+ readonly providerName: string;
54
+ constructor(providerName: string);
55
+ }
56
+ /** Thrown when ingest() is called with an empty / blank key. */
57
+ export declare class EmptyKeyRejectedError extends Error {
58
+ constructor(providerName: string);
59
+ }
60
+ export declare class ProviderKeyVault {
61
+ #private;
62
+ constructor(options?: ProviderKeyVaultOptions);
63
+ /**
64
+ * Add or replace a credential. Existing entries for the same
65
+ * provider are silently overwritten; the old `ProviderCredential`
66
+ * is dropped, and the previous key becomes unreachable.
67
+ */
68
+ ingest(providerName: string, apiKey: string, baseUrl: string, displayName?: string): void;
69
+ /**
70
+ * True if the vault has a credential for the given provider.
71
+ * Safe to call from anywhere — does not leak the key.
72
+ */
73
+ hasProvider(providerName: string): boolean;
74
+ /** Number of providers currently in the vault. */
75
+ size(): number;
76
+ /** Sorted list of configured provider names. */
77
+ listProviderNames(): string[];
78
+ /**
79
+ * Resolve the provider name for a given model id by walking a
80
+ * caller-supplied resolver. The vault itself does not know the
81
+ * provider registry; callers pass a pure function so the vault
82
+ * stays a pure credential container.
83
+ *
84
+ * Returns the resolved provider name or null if `resolveModel`
85
+ * returned null.
86
+ */
87
+ providerForModel(model: string, resolveModel: (model: string) => string | null): string | null;
88
+ /**
89
+ * The ONLY way to read an API key. Pass a callback; the vault
90
+ * invokes it with the key as the sole argument. The key is
91
+ * never returned, never serialised, never assigned to a wider
92
+ * scope. If the provider is not configured, the callback is
93
+ * never called and {@link ProviderNotInVaultError} is thrown.
94
+ */
95
+ withApiKey<T>(providerName: string, fn: KeyCallback<T>): Promise<T>;
96
+ /**
97
+ * Synchronous variant of {@link withApiKey}. Throws if the
98
+ * callback returns a promise (use `withApiKey` for async).
99
+ */
100
+ withApiKeySync<T>(providerName: string, fn: KeyCallback<T>): T;
101
+ /**
102
+ * Like {@link withApiKey} but exposes the entire
103
+ * {@link ProviderCredential} (apiKey, baseUrl, displayName).
104
+ * Use this for the common "build a request to a provider" case
105
+ * where the URL is part of the credential surface.
106
+ */
107
+ withCredential<T>(providerName: string, fn: CredentialCallback<T>): Promise<T>;
108
+ /** Remove a single provider's credential. */
109
+ evict(providerName: string): boolean;
110
+ /** Remove every credential. Used by `/fixo vault:reset`. */
111
+ clearAll(): void;
112
+ /**
113
+ * Internal: build a `Headers`-shaped object for the provider
114
+ * using a callback. The callback receives the credential and
115
+ * returns a `Record<string, string>`. The callback is the only
116
+ * place where the key is materialised, and the returned object
117
+ * is owned by the caller (which can pass it to `fetch`).
118
+ */
119
+ buildAuthHeaders(providerName: string, builder: (cred: ProviderCredential) => Record<string, string>): Promise<Record<string, string>>;
120
+ }
121
+ export declare function getProviderKeyVault(): ProviderKeyVault;
122
+ /** Test hook — drop the cached singleton. */
123
+ export declare function resetProviderKeyVault(): void;
124
+ //# sourceMappingURL=credential-vault.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-vault.d.ts","sourceRoot":"","sources":["../../src/runtime/credential-vault.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,iEAAiE;AACjE,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yBAAyB;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oCAAoC;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,oCAAoC;AACpC,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAC7D,gDAAgD;AAChD,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,kBAAkB,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAEjF,6BAA6B;AAC7B,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAID,4DAA4D;AAC5D,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,SAAgB,YAAY,EAAE,MAAM,CAAC;gBACzB,YAAY,EAAE,MAAM;CAKjC;AAED,gEAAgE;AAChE,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,YAAY,EAAE,MAAM;CAIjC;AAID,qBAAa,gBAAgB;;gBAUf,OAAO,GAAE,uBAA4B;IAIjD;;;;OAIG;IACI,MAAM,CACX,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI;IAeP;;;OAGG;IACI,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAIjD,kDAAkD;IAC3C,IAAI,IAAI,MAAM;IAIrB,gDAAgD;IACzC,iBAAiB,IAAI,MAAM,EAAE;IAIpC;;;;;;;;OAQG;IACI,gBAAgB,CACrB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAC7C,MAAM,GAAG,IAAI;IAMhB;;;;;;OAMG;IACU,UAAU,CAAC,CAAC,EACvB,YAAY,EAAE,MAAM,EACpB,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,GACjB,OAAO,CAAC,CAAC,CAAC;IAMb;;;OAGG;IACI,cAAc,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC;IAYrE;;;;;OAKG;IACU,cAAc,CAAC,CAAC,EAC3B,YAAY,EAAE,MAAM,EACpB,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAMb,6CAA6C;IACtC,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAI3C,4DAA4D;IACrD,QAAQ,IAAI,IAAI;IAIvB;;;;;;OAMG;IACU,gBAAgB,CAC3B,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5D,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAKnC;AAYD,wBAAgB,mBAAmB,IAAI,gBAAgB,CAGtD;AAED,6CAA6C;AAC7C,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Restricted Credential Sandbox — Pillar 4 of the Phase 2 safety
3
+ * refactor. The problem: API keys for OpenAI, Anthropic, Google,
4
+ * AWS, OpenRouter, etc. are long-lived secrets with catastrophic
5
+ * blast radius if leaked into a tool result, an LLM prompt, or a
6
+ * log line. The legacy `ProvidersManager.getDirectConfig(name)`
7
+ * returns the raw key in an object — every caller that touches
8
+ * that object now has the key on its stack, in error messages,
9
+ * in telemetry, and in crash dumps.
10
+ *
11
+ * The fix: a {@link ProviderKeyVault} that owns the credentials
12
+ * in a private, read-only `Map` and exposes them **only** inside
13
+ * a scoped executor block. Outside the block, the vault reveals
14
+ * nothing but metadata (provider names, presence, count). There
15
+ * is no `getApiKey`, no `peek`, no `toJSON`. The only way to
16
+ * read a key is to hand a callback to the vault, and the key
17
+ * is never visible to the caller after the callback returns.
18
+ *
19
+ * The vault is in-memory and process-local; persistence is
20
+ * delegated to `ProvidersManager`, which calls `vault.ingest()`
21
+ * after a disk read. Test code can call `ingest()` directly with
22
+ * fixture credentials.
23
+ *
24
+ * The class is intentionally small (no `any`, no async-only
25
+ * fast paths) so the security boundary is auditable in one read.
26
+ */
27
+ /* ──────────────────────── Errors ──────────────────────── */
28
+ /** Thrown when a requested provider is not in the vault. */
29
+ export class ProviderNotInVaultError extends Error {
30
+ providerName;
31
+ constructor(providerName) {
32
+ super(`ProviderKeyVault: no credential for "${providerName}"`);
33
+ this.name = 'ProviderNotInVaultError';
34
+ this.providerName = providerName;
35
+ }
36
+ }
37
+ /** Thrown when ingest() is called with an empty / blank key. */
38
+ export class EmptyKeyRejectedError extends Error {
39
+ constructor(providerName) {
40
+ super(`ProviderKeyVault: refused to ingest empty key for "${providerName}"`);
41
+ this.name = 'EmptyKeyRejectedError';
42
+ }
43
+ }
44
+ /* ──────────────────────── ProviderKeyVault ──────────────────────── */
45
+ export class ProviderKeyVault {
46
+ /**
47
+ * Backing store. Marked `readonly` so a misbehaving consumer
48
+ * cannot reassign the map, but the map itself is private so
49
+ * the `ProviderCredential` objects are unreachable from
50
+ * outside the class.
51
+ */
52
+ #store = new Map();
53
+ #rejectEmpty;
54
+ constructor(options = {}) {
55
+ this.#rejectEmpty = options.rejectEmptyKeys ?? true;
56
+ }
57
+ /**
58
+ * Add or replace a credential. Existing entries for the same
59
+ * provider are silently overwritten; the old `ProviderCredential`
60
+ * is dropped, and the previous key becomes unreachable.
61
+ */
62
+ ingest(providerName, apiKey, baseUrl, displayName) {
63
+ if (!providerName) {
64
+ throw new Error('ProviderKeyVault.ingest: providerName is required');
65
+ }
66
+ if (this.#rejectEmpty && (!apiKey || !apiKey.trim())) {
67
+ throw new EmptyKeyRejectedError(providerName);
68
+ }
69
+ this.#store.set(providerName, {
70
+ providerName,
71
+ apiKey,
72
+ baseUrl,
73
+ displayName: displayName ?? providerName,
74
+ });
75
+ }
76
+ /**
77
+ * True if the vault has a credential for the given provider.
78
+ * Safe to call from anywhere — does not leak the key.
79
+ */
80
+ hasProvider(providerName) {
81
+ return this.#store.has(providerName);
82
+ }
83
+ /** Number of providers currently in the vault. */
84
+ size() {
85
+ return this.#store.size;
86
+ }
87
+ /** Sorted list of configured provider names. */
88
+ listProviderNames() {
89
+ return Array.from(this.#store.keys()).sort();
90
+ }
91
+ /**
92
+ * Resolve the provider name for a given model id by walking a
93
+ * caller-supplied resolver. The vault itself does not know the
94
+ * provider registry; callers pass a pure function so the vault
95
+ * stays a pure credential container.
96
+ *
97
+ * Returns the resolved provider name or null if `resolveModel`
98
+ * returned null.
99
+ */
100
+ providerForModel(model, resolveModel) {
101
+ const name = resolveModel(model);
102
+ if (!name)
103
+ return null;
104
+ return this.#store.has(name) ? name : null;
105
+ }
106
+ /**
107
+ * The ONLY way to read an API key. Pass a callback; the vault
108
+ * invokes it with the key as the sole argument. The key is
109
+ * never returned, never serialised, never assigned to a wider
110
+ * scope. If the provider is not configured, the callback is
111
+ * never called and {@link ProviderNotInVaultError} is thrown.
112
+ */
113
+ async withApiKey(providerName, fn) {
114
+ const cred = this.#store.get(providerName);
115
+ if (!cred)
116
+ throw new ProviderNotInVaultError(providerName);
117
+ return await fn(cred.apiKey);
118
+ }
119
+ /**
120
+ * Synchronous variant of {@link withApiKey}. Throws if the
121
+ * callback returns a promise (use `withApiKey` for async).
122
+ */
123
+ withApiKeySync(providerName, fn) {
124
+ const cred = this.#store.get(providerName);
125
+ if (!cred)
126
+ throw new ProviderNotInVaultError(providerName);
127
+ const result = fn(cred.apiKey);
128
+ if (result instanceof Promise) {
129
+ throw new Error('ProviderKeyVault.withApiKeySync: callback returned a Promise; use withApiKey() instead');
130
+ }
131
+ return result;
132
+ }
133
+ /**
134
+ * Like {@link withApiKey} but exposes the entire
135
+ * {@link ProviderCredential} (apiKey, baseUrl, displayName).
136
+ * Use this for the common "build a request to a provider" case
137
+ * where the URL is part of the credential surface.
138
+ */
139
+ async withCredential(providerName, fn) {
140
+ const cred = this.#store.get(providerName);
141
+ if (!cred)
142
+ throw new ProviderNotInVaultError(providerName);
143
+ return await fn(cred);
144
+ }
145
+ /** Remove a single provider's credential. */
146
+ evict(providerName) {
147
+ return this.#store.delete(providerName);
148
+ }
149
+ /** Remove every credential. Used by `/fixo vault:reset`. */
150
+ clearAll() {
151
+ this.#store.clear();
152
+ }
153
+ /**
154
+ * Internal: build a `Headers`-shaped object for the provider
155
+ * using a callback. The callback receives the credential and
156
+ * returns a `Record<string, string>`. The callback is the only
157
+ * place where the key is materialised, and the returned object
158
+ * is owned by the caller (which can pass it to `fetch`).
159
+ */
160
+ async buildAuthHeaders(providerName, builder) {
161
+ const cred = this.#store.get(providerName);
162
+ if (!cred)
163
+ throw new ProviderNotInVaultError(providerName);
164
+ return builder(cred);
165
+ }
166
+ }
167
+ /* ──────────────────────── Process-wide singleton ─────────────────── */
168
+ /**
169
+ * Process-wide vault singleton. Lazily created on first call to
170
+ * {@link getProviderKeyVault}. Tests that need an isolated vault
171
+ * should construct their own {@link ProviderKeyVault} directly
172
+ * and use it without going through this singleton.
173
+ */
174
+ let cachedVault = null;
175
+ export function getProviderKeyVault() {
176
+ if (!cachedVault)
177
+ cachedVault = new ProviderKeyVault();
178
+ return cachedVault;
179
+ }
180
+ /** Test hook — drop the cached singleton. */
181
+ export function resetProviderKeyVault() {
182
+ cachedVault = null;
183
+ }
184
+ //# sourceMappingURL=credential-vault.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-vault.js","sourceRoot":"","sources":["../../src/runtime/credential-vault.ts"],"names":[],"mappings":"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"}