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,40 @@
1
+ import { expect, test, describe, mock } from "bun:test";
2
+ import { GeminiProvider } from "./gemini.js";
3
+
4
+ // Global mock for fs/promises
5
+ mock.module("node:fs/promises", () => ({
6
+ readFile: async (path: string) => {
7
+ if (path.includes("/existent/.gemini/settings.json")) {
8
+ return JSON.stringify({
9
+ model: {
10
+ name: "gemini-3-flash-preview"
11
+ }
12
+ });
13
+ }
14
+ throw new Error("File not found");
15
+ },
16
+ writeFile: async () => {},
17
+ unlink: async () => {}
18
+ }));
19
+
20
+ // Mocking os
21
+ let mockHome = "/existent";
22
+ mock.module("node:os", () => ({
23
+ homedir: () => mockHome
24
+ }));
25
+
26
+ describe("GeminiProvider", () => {
27
+ test("getModelName should return model name from settings", async () => {
28
+ mockHome = "/existent";
29
+ const provider = new GeminiProvider();
30
+ const modelName = await provider.getModelName();
31
+ expect(modelName).toBe("gemini-3-flash-preview");
32
+ });
33
+
34
+ test("getModelName should return undefined if file reading fails", async () => {
35
+ mockHome = "/non-existent";
36
+ const provider = new GeminiProvider();
37
+ const modelName = await provider.getModelName();
38
+ expect(modelName).toBeUndefined();
39
+ });
40
+ });
@@ -0,0 +1,169 @@
1
+ import {
2
+ BaseProvider,
3
+ checkForErrors,
4
+ execCommand,
5
+ execCommandStreaming,
6
+ parseStreamJsonResult,
7
+ detectStepFromOutput,
8
+ } from "./base.js";
9
+ import type { AIResult, ProviderOptions } from "./types.js";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { writeFile, unlink, readFile } from "node:fs/promises";
13
+
14
+ export class GeminiProvider extends BaseProvider {
15
+ name = "Gemini CLI";
16
+ cliCommand = "gemini";
17
+
18
+ async getModelName(): Promise<string | undefined> {
19
+ try {
20
+ const settingsPath = join(homedir(), ".gemini/settings.json");
21
+ const content = await readFile(settingsPath, "utf-8");
22
+ const settings = JSON.parse(content);
23
+ return settings.model?.name;
24
+ } catch (e) {
25
+ return undefined;
26
+ }
27
+ }
28
+
29
+ async execute(
30
+ prompt: string,
31
+ workDir: string,
32
+ options?: ProviderOptions,
33
+ ): Promise<AIResult> {
34
+ return this.executeStreaming(prompt, workDir, () => {}, options);
35
+ }
36
+
37
+ async executeStreaming(
38
+ prompt: string,
39
+ workDir: string,
40
+ onProgress: (step: string, content?: string) => void,
41
+ options?: ProviderOptions,
42
+ ): Promise<AIResult> {
43
+ // Disable the pickle-rick extension to prevent recursion or interference
44
+ try {
45
+ await execCommand(this.cliCommand, ["extensions", "disable", "pickle-rick"], workDir);
46
+ } catch (e) {
47
+ // Silently ignore if it fails (might not be installed)
48
+ }
49
+
50
+ const extensionPath = join(homedir(), ".gemini/extensions/pickle-rick");
51
+ const promptFile = join(workDir, ".gemini-prompt.txt");
52
+ await writeFile(promptFile, prompt, "utf-8");
53
+
54
+ const geminiArgs = [
55
+ "-s",
56
+ "-y",
57
+ "-o",
58
+ "stream-json",
59
+ "--include-directories",
60
+ extensionPath,
61
+ "--include-directories",
62
+ join(extensionPath, "sessions"),
63
+ "--include-directories",
64
+ join(extensionPath, "jar"),
65
+ "--include-directories",
66
+ join(extensionPath, "worktrees"),
67
+ ];
68
+
69
+ if (options?.resumeSessionId) {
70
+ geminiArgs.push("-r", options.resumeSessionId);
71
+ }
72
+
73
+ if (options?.providerArgs) {
74
+ geminiArgs.push(...options.providerArgs);
75
+ }
76
+
77
+ if (options?.extraIncludes) {
78
+ for (const include of options.extraIncludes) {
79
+ geminiArgs.push("--include-directories", include);
80
+ }
81
+ }
82
+
83
+ const command = "sh";
84
+ const shellArgs = [
85
+ "-c",
86
+ `cat "${promptFile}" | ${this.cliCommand} ${geminiArgs.join(" ")}`,
87
+ ];
88
+
89
+ const outputLines: string[] = [];
90
+ let sessionId: string | undefined;
91
+
92
+ try {
93
+ const { exitCode } = await execCommandStreaming(
94
+ command,
95
+ shellArgs,
96
+ workDir,
97
+ (line) => {
98
+ outputLines.push(line);
99
+ try {
100
+ const parsed = JSON.parse(line);
101
+
102
+ if (parsed.type === "init" && parsed.session_id) {
103
+ sessionId = parsed.session_id;
104
+ }
105
+
106
+ if (
107
+ parsed.type === "message" &&
108
+ parsed.role === "assistant" &&
109
+ parsed.content
110
+ ) {
111
+ onProgress("thinking", parsed.content);
112
+ }
113
+
114
+ const step = detectStepFromOutput(line);
115
+ if (step) {
116
+ onProgress(step);
117
+ }
118
+ } catch {} // Ignore JSON parsing errors
119
+ },
120
+ );
121
+
122
+ const fullOutput = outputLines.join("\n");
123
+
124
+ let error = checkForErrors(fullOutput);
125
+
126
+ // If exit code is bad but no JSON error found, use the raw output (likely stderr)
127
+ if (exitCode !== 0 && !error) {
128
+ // Filter out likely non-error lines (json) to find the error message
129
+ const rawLines = outputLines.filter((l) => !l.trim().startsWith("{"));
130
+ error =
131
+ rawLines.join("\n") ||
132
+ "Unknown execution error (exit code " + exitCode + ")";
133
+ }
134
+
135
+ if (error) {
136
+ return {
137
+ success: false,
138
+ response: "",
139
+ inputTokens: 0,
140
+ outputTokens: 0,
141
+ error,
142
+ };
143
+ }
144
+
145
+ const { response, inputTokens, outputTokens } =
146
+ parseStreamJsonResult(fullOutput);
147
+
148
+ return {
149
+ success: exitCode === 0 && !error,
150
+ response,
151
+ inputTokens,
152
+ outputTokens,
153
+ error:
154
+ error ||
155
+ (exitCode !== 0 ? "Process exited with code " + exitCode : undefined),
156
+ sessionId,
157
+ };
158
+ } finally {
159
+ try {
160
+ await unlink(promptFile);
161
+ } catch (e) {} // Ignore errors during cleanup
162
+
163
+ // Re-enable the extension
164
+ try {
165
+ await execCommand(this.cliCommand, ["extensions", "enable", "pickle-rick"], workDir);
166
+ } catch (e) {} // Ignore errors during cleanup
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,28 @@
1
+ import { expect, test, describe, mock } from "bun:test";
2
+ import { createProvider, getConfiguredModel } from "./index.js";
3
+ import { GeminiProvider } from "./gemini.js";
4
+ import { CodexProvider } from "./codex.js";
5
+ import { OpencodeProvider } from "./opencode.js";
6
+
7
+ mock.module("../config/settings.js", () => ({
8
+ getConfiguredProvider: async () => "gemini",
9
+ loadSettings: async () => ({
10
+ model: {
11
+ provider: "gemini",
12
+ model: "gemini-3-flash"
13
+ }
14
+ })
15
+ }));
16
+
17
+ describe("Providers Index", () => {
18
+ test("createProvider should return GeminiProvider for 'gemini' setting", async () => {
19
+ const provider = await createProvider();
20
+ expect(provider).toBeInstanceOf(GeminiProvider);
21
+ expect(provider.name).toBe("Gemini CLI");
22
+ });
23
+
24
+ test("getConfiguredModel should return model from settings", async () => {
25
+ const model = await getConfiguredModel();
26
+ expect(model).toBe("gemini-3-flash");
27
+ });
28
+ });
@@ -0,0 +1,41 @@
1
+ export * from "./types.js";
2
+ export * from "./base.js";
3
+ export * from "./codex.js";
4
+ export * from "./gemini.js";
5
+ export * from "./opencode.js";
6
+
7
+ import { CodexProvider } from "./codex.js";
8
+ import { GeminiProvider } from "./gemini.js";
9
+ import { OpencodeProvider } from "./opencode.js";
10
+ import type { AIProvider, AIProviderName } from "./types.js";
11
+ import { loadSettings, getConfiguredProvider } from "../config/settings.js";
12
+
13
+ /**
14
+ * Create a provider instance based on settings or default to Gemini
15
+ */
16
+ export async function createProvider(): Promise<AIProvider> {
17
+ const configuredProvider = await getConfiguredProvider();
18
+
19
+ switch (configuredProvider) {
20
+ case "codex":
21
+ return new CodexProvider();
22
+ case "opencode":
23
+ return new OpencodeProvider();
24
+ case "gemini":
25
+ default:
26
+ return new GeminiProvider();
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Get the currently configured provider name
32
+ */
33
+ export { getConfiguredProvider };
34
+
35
+ /**
36
+ * Get the currently configured model name from settings
37
+ */
38
+ export async function getConfiguredModel(): Promise<string | undefined> {
39
+ const settings = await loadSettings();
40
+ return settings.model?.model;
41
+ }
@@ -0,0 +1,64 @@
1
+ import { expect, test, describe, mock } from "bun:test";
2
+ import { OpencodeProvider } from "./opencode.js";
3
+
4
+ // Track which paths should succeed
5
+ let shouldJsonSucceed = true;
6
+ let shouldYamlSucceed = false;
7
+
8
+ // Global mock for fs/promises
9
+ mock.module("node:fs/promises", () => ({
10
+ readFile: async (path: string) => {
11
+ if (path.includes("/.config/opencode/config.json") && shouldJsonSucceed) {
12
+ return JSON.stringify({
13
+ model: "anthropic/claude-sonnet-4-5"
14
+ });
15
+ }
16
+ if (path.includes("/.opencode/config.yaml") && shouldYamlSucceed) {
17
+ return `model: openai/gpt-4\nother: value`;
18
+ }
19
+ throw new Error("File not found");
20
+ },
21
+ writeFile: async () => {},
22
+ unlink: async () => {}
23
+ }));
24
+
25
+ mock.module("node:os", () => ({
26
+ homedir: () => "/home/testuser"
27
+ }));
28
+
29
+ describe("OpencodeProvider", () => {
30
+ test("should have correct name and cliCommand", () => {
31
+ const provider = new OpencodeProvider();
32
+ expect(provider.name).toBe("OpenCode");
33
+ expect(provider.cliCommand).toBe("opencode");
34
+ });
35
+
36
+ test("getModelName should return model from JSON config", async () => {
37
+ shouldJsonSucceed = true;
38
+ shouldYamlSucceed = false;
39
+ const provider = new OpencodeProvider();
40
+ const modelName = await provider.getModelName();
41
+ expect(modelName).toBe("anthropic/claude-sonnet-4-5");
42
+ });
43
+
44
+ test("getModelName should return model from YAML config if JSON not found", async () => {
45
+ shouldJsonSucceed = false;
46
+ shouldYamlSucceed = true;
47
+ const provider = new OpencodeProvider();
48
+ const modelName = await provider.getModelName();
49
+ expect(modelName).toBe("openai/gpt-4");
50
+ });
51
+
52
+ test("getModelName should return undefined if no config found", async () => {
53
+ shouldJsonSucceed = false;
54
+ shouldYamlSucceed = false;
55
+ const provider = new OpencodeProvider();
56
+ const modelName = await provider.getModelName();
57
+ expect(modelName).toBeUndefined();
58
+ });
59
+
60
+ test("isAvailable should check if opencode command exists", async () => {
61
+ const provider = new OpencodeProvider();
62
+ expect(typeof provider.isAvailable).toBe("function");
63
+ });
64
+ });
@@ -0,0 +1,228 @@
1
+ import {
2
+ BaseProvider,
3
+ checkForErrors,
4
+ detectStepFromOutput,
5
+ execCommand,
6
+ execCommandStreaming,
7
+ } from "./base.js";
8
+ import type { AIResult, ProviderOptions } from "./types.js";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { writeFile, unlink, readFile } from "node:fs/promises";
12
+
13
+ interface OpencodeEvent {
14
+ type: string;
15
+ timestamp: number;
16
+ sessionID?: string;
17
+ part?: {
18
+ id?: string;
19
+ sessionID?: string;
20
+ messageID?: string;
21
+ type?: string;
22
+ text?: string;
23
+ tool?: string;
24
+ callID?: string;
25
+ state?: {
26
+ status?: string;
27
+ input?: Record<string, unknown>;
28
+ output?: string;
29
+ title?: string;
30
+ metadata?: Record<string, unknown>;
31
+ time?: {
32
+ start?: number;
33
+ end?: number;
34
+ };
35
+ };
36
+ reason?: string;
37
+ snapshot?: string;
38
+ cost?: number;
39
+ tokens?: {
40
+ input?: number;
41
+ output?: number;
42
+ reasoning?: number;
43
+ cache?: {
44
+ read?: number;
45
+ write?: number;
46
+ };
47
+ };
48
+ };
49
+ error?: {
50
+ name?: string;
51
+ data?: {
52
+ message?: string;
53
+ statusCode?: number;
54
+ isRetryable?: boolean;
55
+ };
56
+ };
57
+ }
58
+
59
+
60
+
61
+ export class OpencodeProvider extends BaseProvider {
62
+ name = "OpenCode";
63
+ cliCommand = "opencode";
64
+
65
+ async getModelName(): Promise<string | undefined> {
66
+ // Try JSON config first
67
+ try {
68
+ const configPath = join(homedir(), ".config/opencode/config.json");
69
+ const content = await readFile(configPath, "utf-8");
70
+ const config = JSON.parse(content);
71
+ if (config.model) {
72
+ return config.model;
73
+ }
74
+ } catch {
75
+ // Fall through to try YAML config
76
+ }
77
+
78
+ // Try YAML config
79
+ try {
80
+ const yamlConfigPath = join(homedir(), ".opencode/config.yaml");
81
+ const content = await readFile(yamlConfigPath, "utf-8");
82
+ // Simple YAML parsing - look for model: line
83
+ const match = content.match(/^model:\s*(.+)$/m);
84
+ if (match) {
85
+ return match[1].trim();
86
+ }
87
+ } catch {
88
+ // Config not found or unreadable
89
+ }
90
+
91
+ return undefined;
92
+ }
93
+
94
+ async execute(
95
+ prompt: string,
96
+ workDir: string,
97
+ options?: ProviderOptions,
98
+ ): Promise<AIResult> {
99
+ return this.executeStreaming(prompt, workDir, () => {}, options);
100
+ }
101
+
102
+ async executeStreaming(
103
+ prompt: string,
104
+ workDir: string,
105
+ onProgress: (step: string, content?: string) => void,
106
+ options?: ProviderOptions,
107
+ ): Promise<AIResult> {
108
+ const promptFile = join(workDir, ".opencode-prompt.txt");
109
+ await writeFile(promptFile, prompt, "utf-8");
110
+
111
+ const opencodeArgs = ["run", "--format", "json"];
112
+
113
+ if (options?.resumeSessionId) {
114
+ opencodeArgs.push("-s", options.resumeSessionId);
115
+ }
116
+
117
+ if (options?.providerArgs) {
118
+ opencodeArgs.push(...options.providerArgs);
119
+ }
120
+
121
+ // Read prompt from stdin
122
+ const command = "sh";
123
+ const shellArgs = [
124
+ "-c",
125
+ `cat "${promptFile}" | ${this.cliCommand} ${opencodeArgs.join(" ")}`,
126
+ ];
127
+
128
+ const outputLines: string[] = [];
129
+ let sessionId: string | undefined;
130
+ let accumulatedResponse = "";
131
+ let inputTokens = 0;
132
+ let outputTokens = 0;
133
+ let error: string | undefined;
134
+
135
+ try {
136
+ const { exitCode } = await execCommandStreaming(
137
+ command,
138
+ shellArgs,
139
+ workDir,
140
+ (line) => {
141
+ outputLines.push(line);
142
+ try {
143
+ const event: OpencodeEvent = JSON.parse(line);
144
+
145
+ // Extract session ID from step_start
146
+ if (event.type === "step_start" && event.sessionID) {
147
+ sessionId = event.sessionID;
148
+ }
149
+
150
+ // Accumulate text response
151
+ if (event.type === "text" && event.part?.text) {
152
+ accumulatedResponse += event.part.text;
153
+ onProgress("thinking", event.part.text);
154
+ }
155
+
156
+ // Detect step from tool_use
157
+ const step = detectStepFromOutput(line);
158
+ if (step) {
159
+ onProgress(step);
160
+ }
161
+
162
+ // Extract tokens and cost from step_finish
163
+ if (event.type === "step_finish" && event.part) {
164
+ if (event.part.tokens?.input) {
165
+ inputTokens = event.part.tokens.input;
166
+ }
167
+ if (event.part.tokens?.output) {
168
+ outputTokens = event.part.tokens.output;
169
+ }
170
+ // Check for final completion
171
+ if (event.part.reason === "stop") {
172
+ // Final step
173
+ }
174
+ }
175
+
176
+ // Check for errors
177
+ if (event.type === "error" && event.error?.data?.message) {
178
+ error = event.error.data.message;
179
+ }
180
+ } catch {
181
+ // Ignore JSON parsing errors
182
+ }
183
+ },
184
+ );
185
+
186
+ // Check for errors in raw output
187
+ const fullOutput = outputLines.join("\n");
188
+ const parsedError = checkForErrors(fullOutput);
189
+ if (parsedError) {
190
+ error = parsedError;
191
+ }
192
+
193
+ // If exit code is bad but no error found, use raw output
194
+ if (exitCode !== 0 && !error) {
195
+ const rawLines = outputLines.filter((l) => !l.trim().startsWith("{"));
196
+ error =
197
+ rawLines.join("\n") ||
198
+ `Unknown execution error (exit code ${exitCode})`;
199
+ }
200
+
201
+ if (error) {
202
+ return {
203
+ success: false,
204
+ response: accumulatedResponse,
205
+ inputTokens,
206
+ outputTokens,
207
+ error,
208
+ };
209
+ }
210
+
211
+ return {
212
+ success: exitCode === 0 && !error,
213
+ response: accumulatedResponse || "Task completed",
214
+ inputTokens,
215
+ outputTokens,
216
+ error: exitCode !== 0 ? `Process exited with code ${exitCode}` : undefined,
217
+ sessionId,
218
+ };
219
+ } finally {
220
+ // Cleanup
221
+ try {
222
+ await unlink(promptFile);
223
+ } catch {
224
+ // Ignore cleanup errors
225
+ }
226
+ }
227
+ }
228
+ }
@@ -0,0 +1,44 @@
1
+ // ... existing imports
2
+ export interface AIResult {
3
+ success: boolean;
4
+ response: string;
5
+ inputTokens: number;
6
+ outputTokens: number;
7
+ sessionId?: string; // Added field
8
+ cost?: string;
9
+ error?: string;
10
+ }
11
+
12
+ export interface ProviderOptions {
13
+ modelOverride?: string;
14
+ resumeSessionId?: string; // Added field
15
+ providerArgs?: string[];
16
+ extraIncludes?: string[]; // Added: Force include directories (e.g. for Worktrees)
17
+ }
18
+
19
+ // ... rest of file
20
+ export type ProgressCallback = (step: string, content?: string) => void;
21
+
22
+ export interface AIProvider {
23
+ name: string;
24
+ cliCommand: string;
25
+ isAvailable(): Promise<boolean>;
26
+ getModelName?(): Promise<string | undefined>;
27
+ execute(prompt: string, workDir: string, options?: ProviderOptions): Promise<AIResult>;
28
+ executeStreaming?(
29
+ prompt: string,
30
+ workDir: string,
31
+ onProgress: ProgressCallback,
32
+ options?: ProviderOptions,
33
+ ): Promise<AIResult>;
34
+ }
35
+
36
+ export type AIProviderName =
37
+ | "claude"
38
+ | "opencode"
39
+ | "cursor"
40
+ | "codex"
41
+ | "qwen"
42
+ | "droid"
43
+ | "copilot"
44
+ | "gemini";