im-pickle-rick 0.1.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 (128) hide show
  1. package/README.md +242 -0
  2. package/bin.js +3 -0
  3. package/dist/pickle +0 -0
  4. package/dist/worker-executor.js +207 -0
  5. package/package.json +53 -0
  6. package/src/games/GameSidebarManager.test.ts +64 -0
  7. package/src/games/GameSidebarManager.ts +78 -0
  8. package/src/games/gameboy/GameboyView.test.ts +25 -0
  9. package/src/games/gameboy/GameboyView.ts +100 -0
  10. package/src/games/gameboy/gameboy-polyfills.ts +313 -0
  11. package/src/games/index.test.ts +9 -0
  12. package/src/games/index.ts +4 -0
  13. package/src/games/snake/SnakeGame.test.ts +35 -0
  14. package/src/games/snake/SnakeGame.ts +145 -0
  15. package/src/games/snake/SnakeView.test.ts +25 -0
  16. package/src/games/snake/SnakeView.ts +290 -0
  17. package/src/index.test.ts +24 -0
  18. package/src/index.ts +141 -0
  19. package/src/services/commands/worker.test.ts +14 -0
  20. package/src/services/commands/worker.ts +262 -0
  21. package/src/services/config/index.ts +2 -0
  22. package/src/services/config/settings.test.ts +42 -0
  23. package/src/services/config/settings.ts +220 -0
  24. package/src/services/config/state.test.ts +88 -0
  25. package/src/services/config/state.ts +130 -0
  26. package/src/services/config/types.ts +39 -0
  27. package/src/services/execution/index.ts +1 -0
  28. package/src/services/execution/pickle-source.test.ts +88 -0
  29. package/src/services/execution/pickle-source.ts +264 -0
  30. package/src/services/execution/prompt.test.ts +93 -0
  31. package/src/services/execution/prompt.ts +322 -0
  32. package/src/services/execution/sequential.test.ts +91 -0
  33. package/src/services/execution/sequential.ts +422 -0
  34. package/src/services/execution/worker-client.ts +94 -0
  35. package/src/services/execution/worker-executor.ts +41 -0
  36. package/src/services/execution/worker.test.ts +73 -0
  37. package/src/services/git/branch.test.ts +147 -0
  38. package/src/services/git/branch.ts +128 -0
  39. package/src/services/git/diff.test.ts +113 -0
  40. package/src/services/git/diff.ts +323 -0
  41. package/src/services/git/index.ts +4 -0
  42. package/src/services/git/pr.test.ts +104 -0
  43. package/src/services/git/pr.ts +192 -0
  44. package/src/services/git/worktree.test.ts +99 -0
  45. package/src/services/git/worktree.ts +141 -0
  46. package/src/services/providers/base.test.ts +86 -0
  47. package/src/services/providers/base.ts +438 -0
  48. package/src/services/providers/codex.test.ts +39 -0
  49. package/src/services/providers/codex.ts +208 -0
  50. package/src/services/providers/gemini.test.ts +40 -0
  51. package/src/services/providers/gemini.ts +169 -0
  52. package/src/services/providers/index.test.ts +28 -0
  53. package/src/services/providers/index.ts +41 -0
  54. package/src/services/providers/opencode.test.ts +64 -0
  55. package/src/services/providers/opencode.ts +228 -0
  56. package/src/services/providers/types.ts +44 -0
  57. package/src/skills/code-implementer.md +105 -0
  58. package/src/skills/code-researcher.md +78 -0
  59. package/src/skills/implementation-planner.md +105 -0
  60. package/src/skills/plan-reviewer.md +100 -0
  61. package/src/skills/prd-drafter.md +123 -0
  62. package/src/skills/research-reviewer.md +79 -0
  63. package/src/skills/ruthless-refactorer.md +52 -0
  64. package/src/skills/ticket-manager.md +135 -0
  65. package/src/types/index.ts +2 -0
  66. package/src/types/rpc.ts +14 -0
  67. package/src/types/tasks.ts +50 -0
  68. package/src/types.d.ts +9 -0
  69. package/src/ui/common.ts +28 -0
  70. package/src/ui/components/FilePickerView.test.ts +79 -0
  71. package/src/ui/components/FilePickerView.ts +161 -0
  72. package/src/ui/components/MultiLineInput.test.ts +27 -0
  73. package/src/ui/components/MultiLineInput.ts +233 -0
  74. package/src/ui/components/SessionChip.test.ts +69 -0
  75. package/src/ui/components/SessionChip.ts +481 -0
  76. package/src/ui/components/ToyboxSidebar.test.ts +36 -0
  77. package/src/ui/components/ToyboxSidebar.ts +329 -0
  78. package/src/ui/components/refactor_plan.md +35 -0
  79. package/src/ui/controllers/DashboardController.integration.test.ts +43 -0
  80. package/src/ui/controllers/DashboardController.ts +650 -0
  81. package/src/ui/dashboard.test.ts +43 -0
  82. package/src/ui/dashboard.ts +309 -0
  83. package/src/ui/dialogs/DashboardDialog.test.ts +146 -0
  84. package/src/ui/dialogs/DashboardDialog.ts +399 -0
  85. package/src/ui/dialogs/Dialog.test.ts +50 -0
  86. package/src/ui/dialogs/Dialog.ts +241 -0
  87. package/src/ui/dialogs/DialogSidebar.test.ts +60 -0
  88. package/src/ui/dialogs/DialogSidebar.ts +71 -0
  89. package/src/ui/dialogs/DiffViewDialog.test.ts +57 -0
  90. package/src/ui/dialogs/DiffViewDialog.ts +510 -0
  91. package/src/ui/dialogs/PRPreviewDialog.test.ts +50 -0
  92. package/src/ui/dialogs/PRPreviewDialog.ts +346 -0
  93. package/src/ui/dialogs/test-utils.ts +232 -0
  94. package/src/ui/file-picker-utils.test.ts +71 -0
  95. package/src/ui/file-picker-utils.ts +200 -0
  96. package/src/ui/input-chrome.test.ts +62 -0
  97. package/src/ui/input-chrome.ts +172 -0
  98. package/src/ui/logger.test.ts +68 -0
  99. package/src/ui/logger.ts +45 -0
  100. package/src/ui/mock-factory.ts +6 -0
  101. package/src/ui/spinner.test.ts +65 -0
  102. package/src/ui/spinner.ts +41 -0
  103. package/src/ui/test-setup.ts +300 -0
  104. package/src/ui/theme.test.ts +23 -0
  105. package/src/ui/theme.ts +16 -0
  106. package/src/ui/views/LandingView.integration.test.ts +21 -0
  107. package/src/ui/views/LandingView.test.ts +24 -0
  108. package/src/ui/views/LandingView.ts +221 -0
  109. package/src/ui/views/LogView.test.ts +24 -0
  110. package/src/ui/views/LogView.ts +277 -0
  111. package/src/ui/views/ToyboxView.test.ts +46 -0
  112. package/src/ui/views/ToyboxView.ts +323 -0
  113. package/src/utils/clipboard.test.ts +86 -0
  114. package/src/utils/clipboard.ts +100 -0
  115. package/src/utils/index.test.ts +68 -0
  116. package/src/utils/index.ts +95 -0
  117. package/src/utils/persona.test.ts +12 -0
  118. package/src/utils/persona.ts +8 -0
  119. package/src/utils/project-root.test.ts +38 -0
  120. package/src/utils/project-root.ts +22 -0
  121. package/src/utils/resources.test.ts +64 -0
  122. package/src/utils/resources.ts +92 -0
  123. package/src/utils/search.test.ts +48 -0
  124. package/src/utils/search.ts +103 -0
  125. package/src/utils/session-tracker.test.ts +46 -0
  126. package/src/utils/session-tracker.ts +67 -0
  127. package/src/utils/spinner.test.ts +54 -0
  128. package/src/utils/spinner.ts +87 -0
@@ -0,0 +1,438 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { AIProvider, AIResult, ProviderOptions, ProgressCallback } from "./types.js";
3
+
4
+ // Check if running in Bun
5
+ const isBun = typeof Bun !== "undefined";
6
+ const isWindows = process.platform === "win32";
7
+
8
+ /**
9
+ * Check if a command is available in PATH
10
+ */
11
+ export async function commandExists(command: string): Promise<boolean> {
12
+ try {
13
+ const checkCommand = isWindows ? "where" : "which";
14
+ if (isBun) {
15
+ const proc = Bun.spawn([checkCommand, command], {
16
+ stdout: "pipe",
17
+ stderr: "pipe",
18
+ });
19
+ const exitCode = await proc.exited;
20
+ return exitCode === 0;
21
+ }
22
+ // Node.js fallback - where/which don't need shell
23
+ return new Promise((resolve) => {
24
+ const proc = spawn(checkCommand, [command], { stdio: "pipe" });
25
+ proc.on("close", (code) => resolve(code === 0));
26
+ proc.on("error", () => resolve(false));
27
+ });
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Execute a command and return stdout
35
+ * @param stdinContent - Optional content to pass via stdin (useful for multi-line prompts on Windows)
36
+ */
37
+ export async function execCommand(
38
+ command: string,
39
+ args: string[],
40
+ workDir: string,
41
+ env?: Record<string, string>,
42
+ stdinContent?: string,
43
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
44
+ if (isBun) {
45
+ // On Windows, run through cmd.exe to handle .cmd wrappers (npm global packages)
46
+ const spawnArgs = isWindows ? ["cmd.exe", "/c", command, ...args] : [command, ...args];
47
+ const proc = Bun.spawn(spawnArgs, {
48
+ cwd: workDir,
49
+ stdin: stdinContent ? "pipe" : "ignore",
50
+ stdout: "pipe",
51
+ stderr: "pipe",
52
+ env: { ...process.env, ...env },
53
+ });
54
+
55
+ // Write stdin content if provided
56
+ if (stdinContent && proc.stdin) {
57
+ proc.stdin.write(stdinContent);
58
+ proc.stdin.end();
59
+ }
60
+
61
+ const [stdout, stderr, exitCode] = await Promise.all([
62
+ new Response(proc.stdout).text(),
63
+ new Response(proc.stderr).text(),
64
+ proc.exited,
65
+ ]);
66
+
67
+ return { stdout, stderr, exitCode };
68
+ }
69
+
70
+ // Node.js fallback - use shell on Windows to execute .cmd wrappers
71
+ return new Promise((resolve) => {
72
+ const proc = spawn(command, args, {
73
+ cwd: workDir,
74
+ env: { ...process.env, ...env },
75
+ stdio: [stdinContent ? "pipe" : "ignore", "pipe", "pipe"],
76
+ shell: isWindows, // Required on Windows for npm global commands (.cmd wrappers)
77
+ });
78
+
79
+ // Write stdin content if provided
80
+ if (stdinContent && proc.stdin) {
81
+ proc.stdin.write(stdinContent);
82
+ proc.stdin.end();
83
+ }
84
+
85
+ let stdout = "";
86
+ let stderr = "";
87
+
88
+ proc.stdout?.on("data", (data) => {
89
+ stdout += data.toString();
90
+ });
91
+
92
+ proc.stderr?.on("data", (data) => {
93
+ stderr += data.toString();
94
+ });
95
+
96
+ proc.on("close", (exitCode) => {
97
+ resolve({ stdout, stderr, exitCode: exitCode ?? 1 });
98
+ });
99
+
100
+ proc.on("error", (err) => {
101
+ // Maintain backward compatibility - don't reject, include error in stderr
102
+ stderr += `\nSpawn error: ${err.message}`;
103
+ resolve({ stdout, stderr, exitCode: 1 });
104
+ });
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Parse token counts from stream-json output (Claude/Qwen/Gemini format)
110
+ */
111
+ export function parseStreamJsonResult(output: string): {
112
+ response: string;
113
+ inputTokens: number;
114
+ outputTokens: number;
115
+ } {
116
+ const lines = output.split("\n").filter(Boolean);
117
+ let response = "";
118
+ let inputTokens = 0;
119
+ let outputTokens = 0;
120
+
121
+ for (const line of lines) {
122
+ try {
123
+ const parsed = JSON.parse(line);
124
+ if (parsed.type === "message" && parsed.role === "assistant") {
125
+ response += parsed.content || "";
126
+ } else if (parsed.type === "result") {
127
+ inputTokens = parsed.usage?.input_tokens || parsed.stats?.input_tokens || 0;
128
+ outputTokens = parsed.usage?.output_tokens || parsed.stats?.output_tokens || 0;
129
+ }
130
+ } catch {
131
+ // Ignore non-JSON lines
132
+ }
133
+ }
134
+
135
+ return { response: response || "Task completed", inputTokens, outputTokens };
136
+ }
137
+
138
+ /**
139
+ * Check for errors in stream-json output
140
+ */
141
+ export function checkForErrors(output: string): string | null {
142
+ const lines = output.split("\n").filter(Boolean);
143
+
144
+ for (const line of lines) {
145
+ try {
146
+ const parsed = JSON.parse(line);
147
+ if (parsed.type === "error") {
148
+ return parsed.error?.message || parsed.message || "Unknown error";
149
+ }
150
+ } catch {
151
+ // Ignore non-JSON lines
152
+ }
153
+ }
154
+
155
+ return null;
156
+ }
157
+
158
+ /**
159
+ * Read a stream line by line, calling onLine for each non-empty line
160
+ */
161
+ async function readStream(
162
+ stream: ReadableStream<Uint8Array>,
163
+ onLine: (line: string) => void,
164
+ ): Promise<void> {
165
+ const reader = stream.getReader();
166
+ const decoder = new TextDecoder();
167
+ let buffer = "";
168
+ try {
169
+ while (true) {
170
+ const { done, value } = await reader.read();
171
+ if (done) break;
172
+ buffer += decoder.decode(value, { stream: true });
173
+ const lines = buffer.split("\n");
174
+ buffer = lines.pop() || "";
175
+ for (const line of lines) {
176
+ if (line.trim()) onLine(line);
177
+ }
178
+ }
179
+ if (buffer.trim()) onLine(buffer);
180
+ } finally {
181
+ reader.releaseLock();
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Execute a command with streaming output, calling onLine for each line
187
+ * @param stdinContent - Optional content to pass via stdin (useful for multi-line prompts on Windows)
188
+ */
189
+ export async function execCommandStreaming(
190
+ command: string,
191
+ args: string[],
192
+ workDir: string,
193
+ onLine: (line: string) => void,
194
+ env?: Record<string, string>,
195
+ stdinContent?: string,
196
+ ): Promise<{ exitCode: number }> {
197
+ if (isBun) {
198
+ // On Windows, run through cmd.exe to handle .cmd wrappers (npm global packages)
199
+ const spawnArgs = isWindows ? ["cmd.exe", "/c", command, ...args] : [command, ...args];
200
+ const proc = Bun.spawn(spawnArgs, {
201
+ cwd: workDir,
202
+ stdin: stdinContent ? "pipe" : "ignore",
203
+ stdout: "pipe",
204
+ stderr: "pipe",
205
+ env: { ...process.env, ...env },
206
+ });
207
+
208
+ // Write stdin content if provided
209
+ if (stdinContent && proc.stdin) {
210
+ proc.stdin.write(stdinContent);
211
+ proc.stdin.end();
212
+ }
213
+
214
+ // Process both stdout and stderr in parallel
215
+ await Promise.all([readStream(proc.stdout, onLine), readStream(proc.stderr, onLine)]);
216
+
217
+ const exitCode = await proc.exited;
218
+ return { exitCode };
219
+ }
220
+
221
+ // Node.js fallback - use shell on Windows to execute .cmd wrappers
222
+ return new Promise((resolve) => {
223
+ const proc = spawn(command, args, {
224
+ cwd: workDir,
225
+ env: { ...process.env, ...env },
226
+ stdio: [stdinContent ? "pipe" : "ignore", "pipe", "pipe"],
227
+ shell: isWindows, // Required on Windows for npm global commands (.cmd wrappers)
228
+ });
229
+
230
+ // Write stdin content if provided
231
+ if (stdinContent && proc.stdin) {
232
+ proc.stdin.write(stdinContent);
233
+ proc.stdin.end();
234
+ }
235
+
236
+ let stdoutBuffer = "";
237
+ let stderrBuffer = "";
238
+
239
+ const processBuffer = (buffer: string, isStderr = false) => {
240
+ const lines = buffer.split("\n");
241
+ const remaining = lines.pop() || "";
242
+ for (const line of lines) {
243
+ if (line.trim()) onLine(line);
244
+ }
245
+ return remaining;
246
+ };
247
+
248
+ proc.stdout?.on("data", (data) => {
249
+ stdoutBuffer += data.toString();
250
+ stdoutBuffer = processBuffer(stdoutBuffer);
251
+ });
252
+
253
+ proc.stderr?.on("data", (data) => {
254
+ stderrBuffer += data.toString();
255
+ stderrBuffer = processBuffer(stderrBuffer, true);
256
+ });
257
+
258
+ proc.on("close", (exitCode) => {
259
+ // Process any remaining data
260
+ if (stdoutBuffer.trim()) onLine(stdoutBuffer);
261
+ if (stderrBuffer.trim()) onLine(stderrBuffer);
262
+ resolve({ exitCode: exitCode ?? 1 });
263
+ });
264
+
265
+ proc.on("error", (err) => {
266
+ // Maintain backward compatibility - don't reject, report error via onLine
267
+ onLine(`Spawn error: ${err.message}`);
268
+ resolve({ exitCode: 1 });
269
+ });
270
+ });
271
+ }
272
+
273
+ /**
274
+ * Check if a file path looks like a test file
275
+ */
276
+ function isTestFile(filePath: string): boolean {
277
+ const lower = filePath.toLowerCase();
278
+ return (
279
+ lower.includes(".test.") ||
280
+ lower.includes(".spec.") ||
281
+ lower.includes("__tests__") ||
282
+ lower.includes("_test.go")
283
+ );
284
+ }
285
+
286
+ /**
287
+ * Detect the current step from a JSON output line
288
+ * Returns step name like "Reading code", "Implementing", etc.
289
+ */
290
+ export function detectStepFromOutput(line: string): string | null {
291
+ // Fast path: skip non-JSON lines
292
+ const trimmed = line.trim();
293
+ if (!trimmed.startsWith("{")) {
294
+ return null;
295
+ }
296
+
297
+ try {
298
+ const parsed = JSON.parse(trimmed);
299
+
300
+ // Normalize the object structure to a common shape
301
+ const root = parsed.part || parsed.item || parsed;
302
+
303
+ const toolName =
304
+ root.tool?.toLowerCase() ||
305
+ root.name?.toLowerCase() ||
306
+ root.tool_name?.toLowerCase() ||
307
+ parsed.tool?.toLowerCase() || // Fallback to root
308
+ "";
309
+
310
+ const command = (
311
+ root.command ||
312
+ root.input?.command ||
313
+ root.state?.input?.command ||
314
+ parsed.command
315
+ )?.toLowerCase() || "";
316
+
317
+ const filePath = (
318
+ root.file_path ||
319
+ root.filePath ||
320
+ root.path ||
321
+ root.files?.[0] || // Handle array
322
+ root.paths?.[0] || // Handle array
323
+ parsed.file_path ||
324
+ ""
325
+ ).toLowerCase();
326
+
327
+ const description = (
328
+ root.description ||
329
+ root.metadata?.description ||
330
+ root.state?.title ||
331
+ parsed.description ||
332
+ ""
333
+ ).toLowerCase();
334
+
335
+ const title = (root.title || root.metadata?.title || "").toLowerCase();
336
+ const combined = `${title} ${description} ${command}`.toLowerCase();
337
+
338
+ // Check tool name first to determine operation type
339
+ const isReadOperation =
340
+ toolName.includes("read") ||
341
+ toolName.includes("glob") ||
342
+ toolName.includes("grep") ||
343
+ toolName.includes("search") ||
344
+ toolName.includes("list");
345
+
346
+ const isWriteOperation =
347
+ toolName.includes("write") ||
348
+ toolName.includes("edit") ||
349
+ toolName.includes("patch") ||
350
+ toolName.includes("file");
351
+
352
+ // Reading code
353
+ if (isReadOperation) {
354
+ return "Reading code";
355
+ }
356
+
357
+ // Git commit
358
+ if (combined.includes("git commit")) {
359
+ return "Committing";
360
+ }
361
+
362
+ // Git add/staging
363
+ if (combined.includes("git add")) {
364
+ return "Staging";
365
+ }
366
+
367
+ // Linting
368
+ if (
369
+ combined.includes("lint") ||
370
+ combined.includes("eslint") ||
371
+ combined.includes("biome") ||
372
+ combined.includes("prettier")
373
+ ) {
374
+ return "Linting";
375
+ }
376
+
377
+ // Testing
378
+ if (
379
+ combined.includes("vitest") ||
380
+ combined.includes("jest") ||
381
+ combined.includes("bun test") ||
382
+ combined.includes("npm test") ||
383
+ combined.includes("pytest") ||
384
+ combined.includes("go test")
385
+ ) {
386
+ return "Testing";
387
+ }
388
+
389
+ // Writing tests
390
+ if (isWriteOperation && isTestFile(filePath)) {
391
+ return "Writing tests";
392
+ }
393
+
394
+ // Writing/Editing code
395
+ if (isWriteOperation) {
396
+ return "Implementing";
397
+ }
398
+
399
+ // Generic command fallback
400
+ if (toolName === "bash" || toolName === "exec" || toolName.includes("command")) {
401
+ if (READ_COMMAND_HINTS.some(h => command.startsWith(h) || command.includes(" " + h))) {
402
+ return "Reading code";
403
+ }
404
+ }
405
+
406
+ return null;
407
+ } catch {
408
+ return null;
409
+ }
410
+ }
411
+
412
+ const READ_COMMAND_HINTS = [
413
+ "rg", "ripgrep", "grep", "sed", "cat", "ls", "find", "fd", "tree", "head", "tail"
414
+ ];
415
+
416
+ /**
417
+ * Base implementation for AI providers
418
+ */
419
+ export abstract class BaseProvider implements AIProvider {
420
+ abstract name: string;
421
+ abstract cliCommand: string;
422
+
423
+ async isAvailable(): Promise<boolean> {
424
+ return commandExists(this.cliCommand);
425
+ }
426
+
427
+ abstract execute(prompt: string, workDir: string, options?: ProviderOptions): Promise<AIResult>;
428
+
429
+ /**
430
+ * Execute with streaming progress updates (optional implementation)
431
+ */
432
+ executeStreaming?(
433
+ prompt: string,
434
+ workDir: string,
435
+ onProgress: (step: string, content?: string) => void,
436
+ options?: ProviderOptions,
437
+ ): Promise<AIResult>;
438
+ }
@@ -0,0 +1,39 @@
1
+ import { expect, test, describe, mock } from "bun:test";
2
+ import { CodexProvider } from "./codex.js";
3
+
4
+ let shouldConfigSucceed = true;
5
+
6
+ mock.module("node:fs/promises", () => ({
7
+ readFile: async (path: string) => {
8
+ if (path.includes("/.codex/config.toml") && shouldConfigSucceed) {
9
+ return 'model = "gpt-5.2"';
10
+ }
11
+ throw new Error("File not found");
12
+ },
13
+ }));
14
+
15
+ mock.module("node:os", () => ({
16
+ homedir: () => "/home/testuser",
17
+ }));
18
+
19
+ describe("CodexProvider", () => {
20
+ test("should have correct name and cliCommand", () => {
21
+ const provider = new CodexProvider();
22
+ expect(provider.name).toBe("Codex");
23
+ expect(provider.cliCommand).toBe("codex");
24
+ });
25
+
26
+ test("getModelName should return model from config", async () => {
27
+ shouldConfigSucceed = true;
28
+ const provider = new CodexProvider();
29
+ const modelName = await provider.getModelName();
30
+ expect(modelName).toBe("gpt-5.2");
31
+ });
32
+
33
+ test("getModelName should return undefined if config missing", async () => {
34
+ shouldConfigSucceed = false;
35
+ const provider = new CodexProvider();
36
+ const modelName = await provider.getModelName();
37
+ expect(modelName).toBeUndefined();
38
+ });
39
+ });
@@ -0,0 +1,208 @@
1
+ import {
2
+ BaseProvider,
3
+ checkForErrors,
4
+ detectStepFromOutput,
5
+ execCommandStreaming,
6
+ } from "./base.js";
7
+ import type { AIResult, ProviderOptions } from "./types.js";
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { readFile } from "node:fs/promises";
11
+
12
+ interface CodexUsage {
13
+ input_tokens?: number;
14
+ output_tokens?: number;
15
+ cached_input_tokens?: number;
16
+ }
17
+
18
+ interface CodexItem {
19
+ id?: string;
20
+ type?: string;
21
+ status?: string;
22
+ text?: string;
23
+ command?: string;
24
+ path?: string;
25
+ paths?: string[];
26
+ files?: string[];
27
+ changes?: Array<{
28
+ path?: string;
29
+ }>;
30
+ tool_name?: string;
31
+ input?: {
32
+ command?: string;
33
+ };
34
+ metadata?: {
35
+ description?: string;
36
+ title?: string;
37
+ };
38
+ }
39
+
40
+ interface CodexEvent {
41
+ type: string;
42
+ thread_id?: string;
43
+ usage?: CodexUsage;
44
+ item?: CodexItem;
45
+ error?: {
46
+ message?: string;
47
+ data?: {
48
+ message?: string;
49
+ };
50
+ };
51
+ message?: string;
52
+ }
53
+
54
+
55
+
56
+ function extractErrorMessage(event: CodexEvent): string | undefined {
57
+ return (
58
+ event.error?.message ||
59
+ event.error?.data?.message ||
60
+ event.message ||
61
+ undefined
62
+ );
63
+ }
64
+
65
+ export class CodexProvider extends BaseProvider {
66
+ name = "Codex";
67
+ cliCommand = "codex";
68
+
69
+ async getModelName(): Promise<string | undefined> {
70
+ try {
71
+ const configPath = join(homedir(), ".codex/config.toml");
72
+ const content = await readFile(configPath, "utf-8");
73
+ const match = content.match(/^model\s*=\s*["']?([^"'\n]+)["']?\s*$/m);
74
+ if (match) {
75
+ return match[1].trim();
76
+ }
77
+ } catch {
78
+ // Config not found or unreadable
79
+ }
80
+
81
+ return undefined;
82
+ }
83
+
84
+ async execute(
85
+ prompt: string,
86
+ workDir: string,
87
+ options?: ProviderOptions,
88
+ ): Promise<AIResult> {
89
+ return this.executeStreaming(prompt, workDir, () => {}, options);
90
+ }
91
+
92
+ async executeStreaming(
93
+ prompt: string,
94
+ workDir: string,
95
+ onProgress: (step: string, content?: string) => void,
96
+ options?: ProviderOptions,
97
+ ): Promise<AIResult> {
98
+ const codexArgs: string[] = ["exec"];
99
+
100
+ if (options?.resumeSessionId) {
101
+ codexArgs.push("resume", options.resumeSessionId);
102
+ }
103
+
104
+ codexArgs.push("--json");
105
+
106
+ if (options?.modelOverride) {
107
+ codexArgs.push("--model", options.modelOverride);
108
+ }
109
+
110
+ if (options?.providerArgs) {
111
+ codexArgs.push(...options.providerArgs);
112
+ }
113
+
114
+ // Codex CLI currently rejects --add-dir; ignore extraIncludes for this provider to avoid errors
115
+
116
+ codexArgs.push("-");
117
+
118
+ const outputLines: string[] = [];
119
+ let sessionId: string | undefined;
120
+ let accumulatedResponse = "";
121
+ let inputTokens = 0;
122
+ let outputTokens = 0;
123
+ let error: string | undefined;
124
+
125
+ const { exitCode } = await execCommandStreaming(
126
+ this.cliCommand,
127
+ codexArgs,
128
+ workDir,
129
+ (line) => {
130
+ outputLines.push(line);
131
+ try {
132
+ const event: CodexEvent = JSON.parse(line);
133
+
134
+ if (event.type === "thread.started" && event.thread_id) {
135
+ sessionId = event.thread_id;
136
+ }
137
+
138
+ if (event.item) {
139
+ const itemType = event.item.type?.toLowerCase() || "";
140
+ const normalizedItemType = itemType.replace(/[^a-z0-9]/g, "");
141
+ if (
142
+ itemType === "agent_message" ||
143
+ normalizedItemType === "agentmessage"
144
+ ) {
145
+ if (event.item.text) {
146
+ accumulatedResponse += event.item.text;
147
+ onProgress("thinking", event.item.text);
148
+ }
149
+ }
150
+
151
+ const step = detectStepFromOutput(line);
152
+ if (step) {
153
+ onProgress(step);
154
+ }
155
+ }
156
+
157
+ if (event.usage) {
158
+ if (event.usage.input_tokens !== undefined) {
159
+ inputTokens = event.usage.input_tokens;
160
+ }
161
+ if (event.usage.output_tokens !== undefined) {
162
+ outputTokens = event.usage.output_tokens;
163
+ }
164
+ }
165
+
166
+ if (event.type.endsWith(".failed") || event.type === "error") {
167
+ error = error || extractErrorMessage(event) || "Unknown error";
168
+ }
169
+ } catch {
170
+ // Ignore JSON parsing errors
171
+ }
172
+ },
173
+ undefined,
174
+ prompt,
175
+ );
176
+
177
+ const fullOutput = outputLines.join("\n");
178
+ const parsedError = checkForErrors(fullOutput);
179
+ if (parsedError) {
180
+ error = parsedError;
181
+ }
182
+
183
+ if (exitCode !== 0 && !error) {
184
+ const rawLines = outputLines.filter((l) => !l.trim().startsWith("{"));
185
+ error = rawLines.join("\n") || `Unknown execution error (exit code ${exitCode})`;
186
+ }
187
+
188
+ if (error) {
189
+ const compactError = error.replace(/\s+/g, " ").trim();
190
+ return {
191
+ success: false,
192
+ response: accumulatedResponse,
193
+ inputTokens,
194
+ outputTokens,
195
+ error: compactError,
196
+ };
197
+ }
198
+
199
+ return {
200
+ success: exitCode === 0 && !error,
201
+ response: accumulatedResponse || "Task completed",
202
+ inputTokens,
203
+ outputTokens,
204
+ error: exitCode !== 0 ? `Process exited with code ${exitCode}` : undefined,
205
+ sessionId,
206
+ };
207
+ }
208
+ }