codevf 1.0.0 → 1.0.1

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 (199) hide show
  1. package/LICENSE +30 -21
  2. package/README.md +6 -1
  3. package/bin/codevf-mcp.js +11 -0
  4. package/dist/commands/fix.d.ts +5 -1
  5. package/dist/commands/fix.d.ts.map +1 -1
  6. package/dist/commands/fix.js +170 -13
  7. package/dist/commands/fix.js.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +72 -2
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/commands/mcp-tools.d.ts +17 -0
  12. package/dist/commands/mcp-tools.d.ts.map +1 -0
  13. package/dist/commands/mcp-tools.js +237 -0
  14. package/dist/commands/mcp-tools.js.map +1 -0
  15. package/dist/commands/setup.d.ts +8 -0
  16. package/dist/commands/setup.d.ts.map +1 -0
  17. package/dist/commands/setup.js +250 -0
  18. package/dist/commands/setup.js.map +1 -0
  19. package/dist/commands/welcome.d.ts +9 -0
  20. package/dist/commands/welcome.d.ts.map +1 -0
  21. package/dist/commands/welcome.js +175 -0
  22. package/dist/commands/welcome.js.map +1 -0
  23. package/dist/index.js +194 -149
  24. package/dist/index.js.map +1 -1
  25. package/dist/lib/api/client.d.ts +28 -0
  26. package/dist/lib/api/client.d.ts.map +1 -0
  27. package/dist/lib/api/client.js +66 -0
  28. package/dist/lib/api/client.js.map +1 -0
  29. package/dist/lib/api/tasks.d.ts +36 -0
  30. package/dist/lib/api/tasks.d.ts.map +1 -0
  31. package/dist/lib/api/tasks.js +62 -0
  32. package/dist/lib/api/tasks.js.map +1 -0
  33. package/dist/lib/api/websocket.d.ts +50 -0
  34. package/dist/lib/api/websocket.d.ts.map +1 -0
  35. package/dist/lib/api/websocket.js +153 -0
  36. package/dist/lib/api/websocket.js.map +1 -0
  37. package/dist/lib/auth/oauth-flow.d.ts +37 -0
  38. package/dist/lib/auth/oauth-flow.d.ts.map +1 -0
  39. package/dist/lib/auth/oauth-flow.js +119 -0
  40. package/dist/lib/auth/oauth-flow.js.map +1 -0
  41. package/dist/lib/auth/token-manager.d.ts +26 -0
  42. package/dist/lib/auth/token-manager.d.ts.map +1 -0
  43. package/dist/lib/auth/token-manager.js +87 -0
  44. package/dist/lib/auth/token-manager.js.map +1 -0
  45. package/dist/lib/config/manager.d.ts +50 -0
  46. package/dist/lib/config/manager.d.ts.map +1 -0
  47. package/dist/lib/config/manager.js +84 -0
  48. package/dist/lib/config/manager.js.map +1 -0
  49. package/dist/lib/utils/errors.d.ts +28 -0
  50. package/dist/lib/utils/errors.d.ts.map +1 -0
  51. package/dist/lib/utils/errors.js +44 -0
  52. package/dist/lib/utils/errors.js.map +1 -0
  53. package/dist/lib/utils/logger.d.ts +20 -0
  54. package/dist/lib/utils/logger.d.ts.map +1 -0
  55. package/dist/lib/utils/logger.js +40 -0
  56. package/dist/lib/utils/logger.js.map +1 -0
  57. package/dist/mcp/index.d.ts +7 -0
  58. package/dist/mcp/index.d.ts.map +1 -0
  59. package/dist/mcp/index.js +158 -0
  60. package/dist/mcp/index.js.map +1 -0
  61. package/dist/mcp/tools/chat.d.ts +30 -0
  62. package/dist/mcp/tools/chat.d.ts.map +1 -0
  63. package/dist/mcp/tools/chat.js +82 -0
  64. package/dist/mcp/tools/chat.js.map +1 -0
  65. package/dist/mcp/tools/instant.d.ts +36 -0
  66. package/dist/mcp/tools/instant.d.ts.map +1 -0
  67. package/dist/mcp/tools/instant.js +100 -0
  68. package/dist/mcp/tools/instant.js.map +1 -0
  69. package/dist/modules/aiAgent.d.ts +75 -0
  70. package/dist/modules/aiAgent.d.ts.map +1 -0
  71. package/dist/modules/aiAgent.js +707 -0
  72. package/dist/modules/aiAgent.js.map +1 -0
  73. package/dist/modules/api.d.ts +7 -0
  74. package/dist/modules/api.d.ts.map +1 -1
  75. package/dist/modules/api.js +13 -4
  76. package/dist/modules/api.js.map +1 -1
  77. package/dist/modules/commandHandler.d.ts +40 -0
  78. package/dist/modules/commandHandler.d.ts.map +1 -0
  79. package/dist/modules/commandHandler.js +345 -0
  80. package/dist/modules/commandHandler.js.map +1 -0
  81. package/dist/modules/config.d.ts +2 -0
  82. package/dist/modules/config.d.ts.map +1 -1
  83. package/dist/modules/config.js +9 -0
  84. package/dist/modules/config.js.map +1 -1
  85. package/dist/modules/constants.d.ts +83 -0
  86. package/dist/modules/constants.d.ts.map +1 -0
  87. package/dist/modules/constants.js +75 -0
  88. package/dist/modules/constants.js.map +1 -0
  89. package/dist/modules/permissions.d.ts +14 -0
  90. package/dist/modules/permissions.d.ts.map +1 -1
  91. package/dist/modules/permissions.js +94 -0
  92. package/dist/modules/permissions.js.map +1 -1
  93. package/dist/modules/toolRegistry.d.ts +50 -0
  94. package/dist/modules/toolRegistry.d.ts.map +1 -0
  95. package/dist/modules/toolRegistry.js +114 -0
  96. package/dist/modules/toolRegistry.js.map +1 -0
  97. package/dist/modules/tunnel.d.ts +33 -0
  98. package/dist/modules/tunnel.d.ts.map +1 -0
  99. package/dist/modules/tunnel.js +79 -0
  100. package/dist/modules/tunnel.js.map +1 -0
  101. package/dist/modules/vibeHelper.d.ts +16 -0
  102. package/dist/modules/vibeHelper.d.ts.map +1 -0
  103. package/dist/modules/vibeHelper.js +38 -0
  104. package/dist/modules/vibeHelper.js.map +1 -0
  105. package/dist/modules/websocket.d.ts +9 -0
  106. package/dist/modules/websocket.d.ts.map +1 -1
  107. package/dist/modules/websocket.js +70 -0
  108. package/dist/modules/websocket.js.map +1 -1
  109. package/dist/tools/consultEngineer.d.ts +13 -0
  110. package/dist/tools/consultEngineer.d.ts.map +1 -0
  111. package/dist/tools/consultEngineer.js +161 -0
  112. package/dist/tools/consultEngineer.js.map +1 -0
  113. package/dist/tools/realtimeChat.d.ts +9 -0
  114. package/dist/tools/realtimeChat.d.ts.map +1 -0
  115. package/dist/tools/realtimeChat.js +101 -0
  116. package/dist/tools/realtimeChat.js.map +1 -0
  117. package/dist/types/index.d.ts +183 -0
  118. package/dist/types/index.d.ts.map +1 -1
  119. package/dist/types/index.js.map +1 -1
  120. package/dist/ui/InteractiveApp.d.ts +13 -0
  121. package/dist/ui/InteractiveApp.d.ts.map +1 -0
  122. package/dist/ui/InteractiveApp.js +84 -0
  123. package/dist/ui/InteractiveApp.js.map +1 -0
  124. package/dist/ui/InteractivePrompt.d.ts +53 -0
  125. package/dist/ui/InteractivePrompt.d.ts.map +1 -0
  126. package/dist/ui/InteractivePrompt.js +422 -0
  127. package/dist/ui/InteractivePrompt.js.map +1 -0
  128. package/dist/ui/LiveSession.d.ts +2 -0
  129. package/dist/ui/LiveSession.d.ts.map +1 -1
  130. package/dist/ui/LiveSession.js +461 -180
  131. package/dist/ui/LiveSession.js.map +1 -1
  132. package/dist/ui/PromptInput.d.ts +14 -0
  133. package/dist/ui/PromptInput.d.ts.map +1 -0
  134. package/dist/ui/PromptInput.js +206 -0
  135. package/dist/ui/PromptInput.js.map +1 -0
  136. package/dist/ui/SessionUI.d.ts +40 -0
  137. package/dist/ui/SessionUI.d.ts.map +1 -0
  138. package/dist/ui/SessionUI.js +227 -0
  139. package/dist/ui/SessionUI.js.map +1 -0
  140. package/dist/ui/input/Command.d.ts +22 -0
  141. package/dist/ui/input/Command.d.ts.map +1 -0
  142. package/dist/ui/input/Command.js +30 -0
  143. package/dist/ui/input/Command.js.map +1 -0
  144. package/dist/ui/input/CustomInput.d.ts +15 -0
  145. package/dist/ui/input/CustomInput.d.ts.map +1 -0
  146. package/dist/ui/input/CustomInput.js +182 -0
  147. package/dist/ui/input/CustomInput.js.map +1 -0
  148. package/dist/ui/input/handlers/handleCursor.d.ts +22 -0
  149. package/dist/ui/input/handlers/handleCursor.d.ts.map +1 -0
  150. package/dist/ui/input/handlers/handleCursor.js +53 -0
  151. package/dist/ui/input/handlers/handleCursor.js.map +1 -0
  152. package/dist/ui/input/handlers/handleEdit.d.ts +18 -0
  153. package/dist/ui/input/handlers/handleEdit.d.ts.map +1 -0
  154. package/dist/ui/input/handlers/handleEdit.js +55 -0
  155. package/dist/ui/input/handlers/handleEdit.js.map +1 -0
  156. package/dist/ui/input/handlers/handleHistory.d.ts +18 -0
  157. package/dist/ui/input/handlers/handleHistory.d.ts.map +1 -0
  158. package/dist/ui/input/handlers/handleHistory.js +85 -0
  159. package/dist/ui/input/handlers/handleHistory.js.map +1 -0
  160. package/dist/ui/input/handlers/handlePaste.d.ts +19 -0
  161. package/dist/ui/input/handlers/handlePaste.d.ts.map +1 -0
  162. package/dist/ui/input/handlers/handlePaste.js +49 -0
  163. package/dist/ui/input/handlers/handlePaste.js.map +1 -0
  164. package/dist/ui/input/handlers/handleSubmit.d.ts +18 -0
  165. package/dist/ui/input/handlers/handleSubmit.d.ts.map +1 -0
  166. package/dist/ui/input/handlers/handleSubmit.js +39 -0
  167. package/dist/ui/input/handlers/handleSubmit.js.map +1 -0
  168. package/dist/ui/input/helpers.d.ts +4 -0
  169. package/dist/ui/input/helpers.d.ts.map +1 -0
  170. package/dist/ui/input/helpers.js +13 -0
  171. package/dist/ui/input/helpers.js.map +1 -0
  172. package/dist/ui/input/keyMatchers.d.ts +14 -0
  173. package/dist/ui/input/keyMatchers.d.ts.map +1 -0
  174. package/dist/ui/input/keyMatchers.js +49 -0
  175. package/dist/ui/input/keyMatchers.js.map +1 -0
  176. package/dist/ui/input/types.d.ts +33 -0
  177. package/dist/ui/input/types.d.ts.map +1 -0
  178. package/dist/ui/input/types.js +2 -0
  179. package/dist/ui/input/types.js.map +1 -0
  180. package/dist/ui/promptWithModes.d.ts +12 -0
  181. package/dist/ui/promptWithModes.d.ts.map +1 -0
  182. package/dist/ui/promptWithModes.js +24 -0
  183. package/dist/ui/promptWithModes.js.map +1 -0
  184. package/dist/ui/renderPrompt.d.ts +12 -0
  185. package/dist/ui/renderPrompt.d.ts.map +1 -0
  186. package/dist/ui/renderPrompt.js +14 -0
  187. package/dist/ui/renderPrompt.js.map +1 -0
  188. package/dist/ui/simplePrompt.d.ts +7 -0
  189. package/dist/ui/simplePrompt.d.ts.map +1 -0
  190. package/dist/ui/simplePrompt.js +38 -0
  191. package/dist/ui/simplePrompt.js.map +1 -0
  192. package/dist/ui/spinner.d.ts +7 -0
  193. package/dist/ui/spinner.d.ts.map +1 -0
  194. package/dist/ui/spinner.js +13 -0
  195. package/dist/ui/spinner.js.map +1 -0
  196. package/package.json +36 -25
  197. package/ARCHITECTURE.md +0 -285
  198. package/BUILD_SUMMARY.md +0 -340
  199. package/QUICKSTART.md +0 -180
@@ -0,0 +1,707 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+ import chalk from 'chalk';
5
+ import { ConfigManager } from './config.js';
6
+ /**
7
+ * Wrapper around the opencode SDK using the client-only mode.
8
+ * - Uses dynamic import so the CLI still works without the SDK installed.
9
+ * - Avoids starting a local server by using `createOpencodeClient` (caller must have a server running).
10
+ * - Creates a session per request, sends prompt, prints response parts, and logs transcripts locally.
11
+ */
12
+ export class AiAgent {
13
+ constructor(configManager = new ConfigManager()) {
14
+ this.configManager = configManager;
15
+ }
16
+ async run(prompt, opts) {
17
+ const config = this.configManager.loadConfig();
18
+ if (!config.ai?.enabled) {
19
+ throw new Error('Local AI is disabled. Enable it via codevf init.');
20
+ }
21
+ const baseUrlEnv = config.ai.sdk.baseUrlEnv || 'OPENCODE_BASE_URL';
22
+ const baseUrl = opts?.baseUrl || AiAgent.baseUrlCache || process.env[baseUrlEnv] || 'http://localhost:4096';
23
+ const verbose = opts?.verbose || process.env.CODEVF_AI_VERBOSE === '1';
24
+ let sdkModule;
25
+ try {
26
+ sdkModule = await import('@opencode-ai/sdk');
27
+ }
28
+ catch (error) {
29
+ throw new Error('opencode SDK not found. Install it with "npm install @opencode-ai/sdk" (or yarn/pnpm) in this project.');
30
+ }
31
+ // Prefer starting local server if available (more reliable), otherwise use client-only.
32
+ const createOpencodeClient = sdkModule.createOpencodeClient;
33
+ const createOpencode = sdkModule.createOpencode;
34
+ if (!createOpencodeClient && !createOpencode) {
35
+ throw new Error('opencode SDK missing createOpencode/createOpencodeClient. Check SDK version.');
36
+ }
37
+ // Check if aborted before proceeding
38
+ if (opts?.signal?.aborted) {
39
+ throw new Error('Aborted');
40
+ }
41
+ // Reuse an existing client if we already initialized one.
42
+ if (AiAgent.clientCache) {
43
+ if (verbose) {
44
+ console.log(chalk.dim(` [debug] Reusing cached client`));
45
+ }
46
+ return this.executeWithClient(AiAgent.clientCache, prompt, baseUrl, baseUrlEnv, config, opts);
47
+ }
48
+ let client;
49
+ // Try starting local server first if supported and not explicitly disabled.
50
+ if (createOpencode && opts?.startServer !== false && !AiAgent.serverStarted) {
51
+ if (verbose) {
52
+ console.log(chalk.dim(` [debug] Starting OpenCode server...`));
53
+ }
54
+ try {
55
+ const started = await createOpencode({
56
+ hostname: '127.0.0.1',
57
+ port: 4096,
58
+ config: {
59
+ model: config.ai.sdk.model || undefined,
60
+ },
61
+ });
62
+ client = started.client;
63
+ const serverUrl = started?.server?.url || baseUrl;
64
+ process.env[baseUrlEnv] = serverUrl;
65
+ AiAgent.baseUrlCache = serverUrl;
66
+ AiAgent.serverStarted = true;
67
+ if (started?.server?.close) {
68
+ AiAgent.serverCloser = async () => {
69
+ try {
70
+ await started.server.close();
71
+ }
72
+ catch {
73
+ // ignore
74
+ }
75
+ };
76
+ }
77
+ if (verbose) {
78
+ console.log(chalk.green(` [✓] OpenCode server started: ${serverUrl}`));
79
+ }
80
+ }
81
+ catch (error) {
82
+ console.log(chalk.red(` [×] Failed to start OpenCode server: ${error?.message || error}`));
83
+ console.log(chalk.yellow(` [!] Please ensure you have the OpenCode SDK installed and configured`));
84
+ console.log(chalk.dim(` [!] Visit https://opencode.ai for setup instructions`));
85
+ AiAgent.serverStarted = true; // avoid repeated start attempts in this process
86
+ }
87
+ }
88
+ // If no client yet, try connecting to existing server.
89
+ if (!client && createOpencodeClient) {
90
+ if (verbose) {
91
+ console.log(chalk.dim(` [debug] Attempting to connect to OpenCode at ${baseUrl}...`));
92
+ }
93
+ try {
94
+ client = await createOpencodeClient({ baseUrl });
95
+ AiAgent.baseUrlCache = baseUrl;
96
+ if (verbose) {
97
+ console.log(chalk.green(` [✓] Connected to OpenCode server`));
98
+ }
99
+ }
100
+ catch (error) {
101
+ const msg = `Failed to connect to opencode server at ${baseUrl}: ${error?.message || error}`;
102
+ if (verbose) {
103
+ console.log(chalk.red(` [×] ${msg}`));
104
+ }
105
+ // If we already tried starting a server or are not allowed to, surface the error.
106
+ if (!createOpencode || opts?.startServer === false || AiAgent.serverStarted) {
107
+ throw new Error(`Cannot connect to OpenCode server. Please ensure:
108
+ 1. OpenCode SDK is installed: npm install @opencode-ai/sdk
109
+ 2. You have signed in: Visit https://opencode.ai/auth
110
+ 3. The server is accessible at ${baseUrl}
111
+
112
+ Error: ${error?.message || error}`);
113
+ }
114
+ }
115
+ }
116
+ if (!client) {
117
+ throw new Error(`Failed to create OpenCode client.
118
+
119
+ Please ensure:
120
+ 1. OpenCode SDK is installed: npm install @opencode-ai/sdk
121
+ 2. You have authenticated at https://opencode.ai/auth
122
+ 3. Your OPENCODE_API_KEY environment variable is set (if required)
123
+
124
+ Visit https://opencode.ai for setup instructions.`);
125
+ }
126
+ AiAgent.clientCache = client;
127
+ this.setupExitCleanup();
128
+ return this.executeWithClient(client, prompt, baseUrl, baseUrlEnv, config, opts, verbose);
129
+ }
130
+ /**
131
+ * Run AI with tool calling support
132
+ *
133
+ * This method wraps the standard run() method to add tool calling capabilities.
134
+ * It detects TOOL_CALL patterns in the AI's response, executes the requested tools,
135
+ * and feeds the results back to the AI in a conversation loop.
136
+ */
137
+ async runWithTools(prompt, tools, opts) {
138
+ const maxToolCalls = opts?.maxToolCalls || 10; // Safety limit
139
+ let toolCallCount = 0;
140
+ const fullTranscript = [];
141
+ // Build system prompt with tool definitions
142
+ const systemPrompt = this.buildSystemPromptWithTools(tools);
143
+ let currentPrompt = `${systemPrompt}\n\nUser request: ${prompt}`;
144
+ // Conversation loop
145
+ while (toolCallCount < maxToolCalls) {
146
+ // Run AI with current prompt
147
+ const result = await this.run(currentPrompt, opts);
148
+ fullTranscript.push(...result.transcript);
149
+ // Check for tool calls in the output
150
+ const toolCall = this.detectToolCall(result.output);
151
+ if (!toolCall) {
152
+ // No tool call - return final result
153
+ return {
154
+ ...result,
155
+ transcript: fullTranscript,
156
+ };
157
+ }
158
+ // Execute the tool
159
+ toolCallCount++;
160
+ console.log(chalk.dim(` [Tool ${toolCallCount}/${maxToolCalls}]`));
161
+ const toolResult = await this.executeTool(tools, toolCall.toolName, toolCall.parameters);
162
+ // Format tool result for AI
163
+ const toolResultText = this.formatToolResult(toolCall, toolResult);
164
+ fullTranscript.push(`Tool: ${toolCall.toolName}`, `Result: ${JSON.stringify(toolResult)}`);
165
+ // Continue conversation with tool result
166
+ currentPrompt = `${toolResultText}\n\nPlease continue your response, incorporating the tool result above.`;
167
+ // Check if aborted
168
+ if (opts?.signal?.aborted) {
169
+ return {
170
+ output: result.output,
171
+ transcript: fullTranscript,
172
+ confidence: 'failed',
173
+ suggestFallback: true,
174
+ };
175
+ }
176
+ }
177
+ // Hit max tool calls limit
178
+ console.log(chalk.yellow(` [!] Reached maximum tool calls (${maxToolCalls}). Stopping.`));
179
+ return {
180
+ output: "I apologize, but I've reached the maximum number of tool calls for this request. Please try breaking down your request into smaller parts.",
181
+ transcript: fullTranscript,
182
+ confidence: 'failed',
183
+ suggestFallback: true,
184
+ };
185
+ }
186
+ /**
187
+ * Execute a tool directly from the tools array
188
+ */
189
+ async executeTool(tools, toolName, params) {
190
+ const tool = tools.find((t) => t.name === toolName);
191
+ if (!tool) {
192
+ return {
193
+ success: false,
194
+ error: `Tool "${toolName}" not found. Available: ${tools.map((t) => t.name).join(', ')}`,
195
+ };
196
+ }
197
+ try {
198
+ return await tool.execute(params);
199
+ }
200
+ catch (error) {
201
+ return {
202
+ success: false,
203
+ error: `Tool execution failed: ${error.message}`,
204
+ };
205
+ }
206
+ }
207
+ /**
208
+ * Build system prompt with tool definitions
209
+ */
210
+ buildSystemPromptWithTools(tools) {
211
+ const toolDescriptions = tools
212
+ .map((tool) => {
213
+ const params = JSON.stringify(tool.parameters, null, 2);
214
+ return `**${tool.name}**
215
+ Description: ${tool.description}
216
+ Parameters: ${params}`;
217
+ })
218
+ .join('\n\n');
219
+ const behaviorNotes = [];
220
+ if (tools.some((tool) => tool.name === 'consultEngineer')) {
221
+ behaviorNotes.push('- `consultEngineer` is a one-shot escalation. It returns a single engineer answer (taskMode: realtime_answer), not an ongoing chat. Include the full question and all necessary context in that single call.');
222
+ }
223
+ const behaviorSection = behaviorNotes.length > 0 ? `\n**Tool behavior notes:**\n${behaviorNotes.join('\n')}\n` : '';
224
+ return `You are an AI assistant with access to the following tools:
225
+
226
+ ${toolDescriptions}
227
+
228
+ ${behaviorSection}
229
+ **How to use tools:**
230
+ When you need to use a tool, respond with this exact format:
231
+ TOOL_CALL: {"toolName": "name_of_tool", "parameters": {"param1": "value1", "param2": "value2"}}
232
+
233
+ After calling a tool, wait for the tool result before continuing your response.
234
+
235
+ **When to use tools:**
236
+ - Use \`consultEngineer\` when you encounter technical questions you cannot confidently answer
237
+ - Use tools when you need external information or actions beyond your knowledge
238
+ - Provide clear, specific parameters when calling tools
239
+
240
+ **Routing help:**
241
+ - If the user asks for real-time chat with a human engineer, instruct them to type /human or press Tab until human mode is active.
242
+
243
+ **Important:**
244
+ - Only call ONE tool at a time
245
+ - Wait for the tool result before proceeding
246
+ - Use the tool result to inform your final answer`;
247
+ }
248
+ /**
249
+ * Detect TOOL_CALL pattern in AI output
250
+ */
251
+ detectToolCall(output) {
252
+ // Look for TOOL_CALL: {json} pattern
253
+ const toolCallRegex = /TOOL_CALL:\s*(\{[^}]+\})/i;
254
+ const match = output.match(toolCallRegex);
255
+ if (!match) {
256
+ return null;
257
+ }
258
+ try {
259
+ const parsed = JSON.parse(match[1]);
260
+ if (!parsed.toolName) {
261
+ console.log(chalk.yellow(' [!] Tool call missing "toolName" field'));
262
+ return null;
263
+ }
264
+ return {
265
+ toolName: parsed.toolName,
266
+ parameters: parsed.parameters || {},
267
+ callId: `call_${Date.now()}`,
268
+ };
269
+ }
270
+ catch (error) {
271
+ console.log(chalk.yellow(` [!] Failed to parse tool call: ${error}`));
272
+ return null;
273
+ }
274
+ }
275
+ /**
276
+ * Format tool result for AI consumption
277
+ */
278
+ formatToolResult(toolCall, result) {
279
+ if (result.success) {
280
+ return `TOOL_RESULT for ${toolCall.toolName}:
281
+ Success: true
282
+ Data: ${JSON.stringify(result.data, null, 2)}
283
+ ${result.creditsUsed ? `Credits used: ${result.creditsUsed}` : ''}`;
284
+ }
285
+ else {
286
+ return `TOOL_RESULT for ${toolCall.toolName}:
287
+ Success: false
288
+ Error: ${result.error}
289
+
290
+ Please try a different approach or inform the user about the limitation.`;
291
+ }
292
+ }
293
+ async executeWithClient(client, prompt, baseUrl, baseUrlEnv, config, opts, verbose) {
294
+ // Check if aborted
295
+ if (opts?.signal?.aborted) {
296
+ throw new Error('Aborted');
297
+ }
298
+ const timeoutMs = typeof opts?.timeoutMs === 'number'
299
+ ? opts.timeoutMs
300
+ : typeof config.ai.maxRunMs === 'number'
301
+ ? config.ai.maxRunMs
302
+ : null;
303
+ const args = {
304
+ ...(config.ai.sdk.defaultArgs || {}),
305
+ ...(config.ai.defaultArgs || {}),
306
+ model: config.ai.sdk.model || undefined,
307
+ };
308
+ const runPromise = this.invoke(client, prompt, args, verbose, opts?.signal, opts?.agent, opts?.spinner);
309
+ const result = timeoutMs ? await this.withTimeout(runPromise, timeoutMs) : await runPromise;
310
+ if (config.ai.logTranscripts) {
311
+ this.appendTranscript(prompt, result.transcript);
312
+ }
313
+ // Remember base URL for future runs in this process
314
+ AiAgent.baseUrlCache = process.env[baseUrlEnv] || baseUrl;
315
+ return result;
316
+ }
317
+ setupExitCleanup() {
318
+ if (AiAgent.serverCloser) {
319
+ const cleanup = async () => {
320
+ if (AiAgent.serverCloser) {
321
+ await AiAgent.serverCloser();
322
+ AiAgent.serverCloser = null;
323
+ }
324
+ };
325
+ ['exit', 'SIGINT', 'SIGTERM'].forEach((evt) => {
326
+ process.once(evt, () => {
327
+ cleanup().finally(() => {
328
+ if (evt !== 'exit') {
329
+ process.exit();
330
+ }
331
+ });
332
+ });
333
+ });
334
+ }
335
+ }
336
+ async invoke(client, prompt, args, verbose, signal, agent, spinner) {
337
+ // Check if aborted
338
+ if (signal?.aborted) {
339
+ throw new Error('Aborted');
340
+ }
341
+ const transcript = [`User: ${prompt}`];
342
+ let output = '';
343
+ // Initialize session with agent
344
+ let session;
345
+ try {
346
+ // Use session.create() and pass agent via system prompt if specified
347
+ const sessionResponse = await client.session.create({
348
+ body: { title: 'CodeVF AI session' },
349
+ });
350
+ session = sessionResponse.data || sessionResponse;
351
+ if (verbose) {
352
+ console.log(chalk.dim(` [debug] Session created: ${session.id}`));
353
+ if (agent) {
354
+ console.log(chalk.dim(` [debug] Using agent mode: ${agent}`));
355
+ }
356
+ }
357
+ }
358
+ catch (error) {
359
+ throw new Error(`Failed to initialize opencode session: ${error?.message || error}`);
360
+ }
361
+ // Send prompt with agent mode if specified
362
+ let result;
363
+ try {
364
+ // Prepend agent mode instruction if specified
365
+ const humanRoutingHint = 'If the user asks for a real-time chat with a human engineer, tell them to type /human or press Tab until human mode is active.';
366
+ let finalPrompt = `${humanRoutingHint}\n\nUser request: ${prompt}`;
367
+ if (agent === 'plan') {
368
+ const agentInstructions = 'You are in PLAN mode. Focus on creating detailed plans, breaking down tasks, and explaining implementation strategies without writing code.';
369
+ finalPrompt = `${agentInstructions}\n\n${humanRoutingHint}\n\nUser request: ${prompt}`;
370
+ }
371
+ // Subscribe to events for real-time streaming
372
+ const events = await client.event.subscribe();
373
+ // Start the prompt (don't await yet)
374
+ const promptPromise = client.session.prompt({
375
+ path: { id: session.id },
376
+ body: {
377
+ model: args.model
378
+ ? {
379
+ providerID: args.model.split('/')[0],
380
+ modelID: args.model.split('/').slice(1).join('/'),
381
+ }
382
+ : undefined,
383
+ parts: [{ type: 'text', text: finalPrompt }],
384
+ },
385
+ });
386
+ // Process events in real-time on a single updating line
387
+ let hasOutput = false;
388
+ const updateSpinner = (status) => {
389
+ if (hasOutput)
390
+ return; // Don't update status once we have actual output
391
+ if (spinner) {
392
+ // Update the ora spinner text
393
+ spinner.text = status;
394
+ }
395
+ else {
396
+ // Fallback to manual line updating if no spinner
397
+ process.stdout.write('\r');
398
+ readline.clearLine(process.stdout, 0);
399
+ readline.cursorTo(process.stdout, 0);
400
+ process.stdout.write(' ' + status);
401
+ }
402
+ };
403
+ const stopSpinner = () => {
404
+ if (spinner) {
405
+ spinner.stop();
406
+ // Clear the spinner line
407
+ readline.clearLine(process.stdout, 0);
408
+ readline.cursorTo(process.stdout, 0);
409
+ }
410
+ };
411
+ // Handle events and prompt completion in parallel
412
+ const streamHandler = (async () => {
413
+ try {
414
+ for await (const event of events.stream) {
415
+ // Only process events for our session
416
+ if (event.properties?.sessionId !== session.id) {
417
+ continue;
418
+ }
419
+ const eventType = event.type;
420
+ const props = event.properties || {};
421
+ // Debug logging for all events
422
+ if (verbose) {
423
+ stopSpinner();
424
+ console.log(chalk.dim(` [debug] Event: ${eventType}, props: ${JSON.stringify(props).substring(0, 100)}`));
425
+ }
426
+ if (eventType === 'thinking_started' || eventType === 'thinking') {
427
+ updateSpinner(chalk.cyan('💭 Thinking...'));
428
+ }
429
+ else if (eventType === 'thinking_complete') {
430
+ updateSpinner(chalk.green('💭 Thinking... ✓'));
431
+ }
432
+ else if (eventType === 'tool_started' || eventType === 'tool_call') {
433
+ const toolName = props.tool || props.name || 'unknown';
434
+ let toolStatus = chalk.yellow(`🔧 ${toolName}`);
435
+ // Show tool args if available
436
+ if (props.args) {
437
+ const argsStr = JSON.stringify(props.args);
438
+ const truncated = argsStr.length > 60 ? argsStr.substring(0, 60) + '...' : argsStr;
439
+ toolStatus += chalk.dim(` ${truncated}`);
440
+ }
441
+ updateSpinner(toolStatus);
442
+ }
443
+ else if (eventType === 'tool_complete' || eventType === 'tool_result') {
444
+ const toolName = props.tool || props.name || 'unknown';
445
+ updateSpinner(chalk.green(`🔧 ${toolName} ✓`));
446
+ }
447
+ else if (eventType === 'text_delta' || eventType === 'content_delta') {
448
+ // Stop spinner and start showing actual output
449
+ if (!hasOutput) {
450
+ stopSpinner();
451
+ hasOutput = true;
452
+ // Add a newline to separate from spinner line
453
+ process.stdout.write('\n');
454
+ }
455
+ // Stream text as it comes
456
+ const text = props.text || props.content || '';
457
+ if (text) {
458
+ output += text;
459
+ const highlighted = this.highlightOutput(text);
460
+ process.stdout.write(highlighted);
461
+ }
462
+ }
463
+ else if (eventType === 'message_complete' || eventType === 'done') {
464
+ // Session complete
465
+ if (!hasOutput) {
466
+ stopSpinner();
467
+ }
468
+ break;
469
+ }
470
+ }
471
+ }
472
+ catch (err) {
473
+ // Stream ended or error - this is okay
474
+ if (verbose) {
475
+ stopSpinner();
476
+ console.log(chalk.dim(` [debug] Event stream ended: ${err}`));
477
+ }
478
+ }
479
+ })();
480
+ // Wait for both the prompt and stream to complete
481
+ result = await Promise.race([promptPromise, streamHandler.then(() => promptPromise)]);
482
+ // Clean up spinner if still running
483
+ if (!hasOutput) {
484
+ stopSpinner();
485
+ }
486
+ if (hasOutput && !output.endsWith('\n')) {
487
+ console.log(); // Ensure we end with newline
488
+ }
489
+ }
490
+ catch (error) {
491
+ const errorMsg = error?.message || error?.toString() || 'Unknown error';
492
+ // Provide helpful context based on error type
493
+ if (errorMsg.includes('fetch failed') || errorMsg.includes('ECONNREFUSED')) {
494
+ throw new Error(`Connection to OpenCode server failed. The server may have crashed or stopped.
495
+
496
+ Troubleshooting:
497
+ 1. Check if the server is running: curl http://localhost:4096/health
498
+ 2. Try restarting the CLI
499
+ 3. Check OpenCode logs for errors
500
+ 4. Ensure you're authenticated: visit https://opencode.ai/auth
501
+
502
+ Original error: ${errorMsg}`);
503
+ }
504
+ else if (errorMsg.includes('401') ||
505
+ errorMsg.includes('403') ||
506
+ errorMsg.includes('Unauthorized')) {
507
+ throw new Error(`Authentication failed with OpenCode.
508
+
509
+ Please:
510
+ 1. Visit https://opencode.ai/auth to sign in
511
+ 2. Ensure your API key is valid
512
+ 3. Check that OPENCODE_API_KEY environment variable is set (if required)
513
+
514
+ Original error: ${errorMsg}`);
515
+ }
516
+ else {
517
+ throw new Error(`Failed to call OpenCode prompt: ${errorMsg}`);
518
+ }
519
+ }
520
+ // If we didn't get output from streaming, extract from final response
521
+ if (!output || output.trim().length === 0) {
522
+ const responseData = result?.data || result;
523
+ const parts = responseData?.parts || responseData?.info?.parts || responseData?.body?.parts || [];
524
+ if (verbose) {
525
+ console.log(chalk.dim(` [debug] Response structure: ${JSON.stringify(Object.keys(responseData || {}))}`));
526
+ }
527
+ if (Array.isArray(parts) && parts.length > 0) {
528
+ for (const part of parts) {
529
+ if (part?.type === 'text' && typeof part.text === 'string') {
530
+ output += part.text;
531
+ }
532
+ }
533
+ }
534
+ // Display the output with highlighting if we got it from final response
535
+ if (output) {
536
+ const highlighted = this.highlightOutput(output);
537
+ console.log(highlighted);
538
+ if (!output.endsWith('\n')) {
539
+ console.log();
540
+ }
541
+ }
542
+ else {
543
+ console.log(chalk.yellow(' [!] No response from AI'));
544
+ return { output: '', transcript, confidence: 'failed', suggestFallback: true };
545
+ }
546
+ }
547
+ // else: output was already displayed via streaming
548
+ // Analyze response quality to determine if fallback might be needed
549
+ const confidence = this.analyzeResponseQuality(output);
550
+ const suggestFallback = confidence === 'low' || confidence === 'failed';
551
+ // Check if AI is asking a clarification question (for vibe mode)
552
+ const clarificationQuestion = this.detectClarificationQuestion(output);
553
+ const needsMoreInfo = clarificationQuestion !== null;
554
+ transcript.push(`AI: ${output}`);
555
+ return {
556
+ output,
557
+ transcript,
558
+ confidence,
559
+ suggestFallback,
560
+ needsMoreInfo,
561
+ clarificationQuestion: clarificationQuestion || undefined,
562
+ };
563
+ }
564
+ analyzeResponseQuality(output) {
565
+ // Check for explicit failure indicators
566
+ const failurePatterns = [
567
+ /i (?:can't|cannot|am unable to|don't know how to)/i,
568
+ /(?:sorry|unfortunately).*(?:can't|cannot|unable)/i,
569
+ /i (?:don't have|lack) (?:access|information|ability)/i,
570
+ /(?:this is )?beyond my (?:capabilities|knowledge)/i,
571
+ /you (?:should|need to|might want to) (?:contact|ask|consult) (?:a |an )?(?:expert|human|engineer)/i,
572
+ ];
573
+ for (const pattern of failurePatterns) {
574
+ if (pattern.test(output)) {
575
+ return 'failed';
576
+ }
577
+ }
578
+ // Check for low confidence indicators
579
+ const lowConfidencePatterns = [
580
+ /i'm not (?:sure|certain|confident)/i,
581
+ /(?:might|may|could possibly) (?:need|require|want)/i,
582
+ /without more (?:information|context|details)/i,
583
+ /i (?:suggest|recommend) consulting/i,
584
+ ];
585
+ for (const pattern of lowConfidencePatterns) {
586
+ if (pattern.test(output)) {
587
+ return 'low';
588
+ }
589
+ }
590
+ // Check response length (very short responses might be low quality)
591
+ if (output.trim().length < 50) {
592
+ return 'low';
593
+ }
594
+ // Default to high confidence
595
+ return 'high';
596
+ }
597
+ detectClarificationQuestion(output) {
598
+ // Check if AI is asking for more information
599
+ const questionPatterns = [
600
+ /could you (?:provide|share|clarify|explain)/i,
601
+ /can you (?:provide|share|tell me|clarify)/i,
602
+ /(?:what|which|how) (?:is|are|does)/i,
603
+ /i (?:need|would need) (?:to know|more information about)/i,
604
+ /to (?:help|assist|provide).*, i need/i,
605
+ ];
606
+ for (const pattern of questionPatterns) {
607
+ if (pattern.test(output)) {
608
+ // Extract the question (sentences ending with ?)
609
+ const questions = output.match(/[^.!?]*\?/g);
610
+ if (questions && questions.length > 0) {
611
+ return questions[questions.length - 1].trim(); // Return last question
612
+ }
613
+ }
614
+ }
615
+ return null;
616
+ }
617
+ appendTranscript(prompt, transcript) {
618
+ try {
619
+ const logsDir = this.configManager.getLogsDir();
620
+ const filePath = path.join(logsDir, 'ai_transcripts.log');
621
+ const now = new Date().toISOString();
622
+ const lines = [`[${now}] prompt: ${prompt}`, ...transcript, '---', ''];
623
+ fs.appendFileSync(filePath, lines.join('\n'), 'utf-8');
624
+ }
625
+ catch (error) {
626
+ console.log(chalk.yellow('Warning: failed to write AI transcript locally.'));
627
+ }
628
+ }
629
+ highlightOutput(text) {
630
+ // Highlight code blocks with cyan background
631
+ let highlighted = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
632
+ const langLabel = lang ? chalk.dim(`[${lang}]`) : '';
633
+ return chalk.bgCyan.black(`${langLabel}\n${code}\n`) + chalk.reset('```');
634
+ });
635
+ // Highlight inline code with cyan
636
+ highlighted = highlighted.replace(/`([^`]+)`/g, (match, code) => {
637
+ return chalk.cyan(code);
638
+ });
639
+ // Highlight file paths with blue
640
+ highlighted = highlighted.replace(/([\/\\][\w\/\\.-]+\.\w+)/g, (match, path) => {
641
+ return chalk.blue(path);
642
+ });
643
+ // Highlight important keywords
644
+ const keywords = [
645
+ 'error',
646
+ 'warning',
647
+ 'success',
648
+ 'failed',
649
+ 'completed',
650
+ 'important',
651
+ 'note',
652
+ 'tip',
653
+ ];
654
+ keywords.forEach((keyword) => {
655
+ const regex = new RegExp(`\\b(${keyword})\\b`, 'gi');
656
+ highlighted = highlighted.replace(regex, (match) => {
657
+ const lower = match.toLowerCase();
658
+ switch (lower) {
659
+ case 'error':
660
+ case 'failed':
661
+ return chalk.red.bold(match);
662
+ case 'warning':
663
+ return chalk.yellow.bold(match);
664
+ case 'success':
665
+ case 'completed':
666
+ return chalk.green.bold(match);
667
+ case 'important':
668
+ return chalk.magenta.bold(match);
669
+ case 'note':
670
+ case 'tip':
671
+ return chalk.cyan.bold(match);
672
+ default:
673
+ return match;
674
+ }
675
+ });
676
+ });
677
+ // Highlight URLs with underline
678
+ highlighted = highlighted.replace(/(https?:\/\/[^\s]+)/g, (match, url) => {
679
+ return chalk.underline.blue(url);
680
+ });
681
+ // Highlight function calls with yellow
682
+ highlighted = highlighted.replace(/\b(\w+)\(/g, (match, func) => {
683
+ return chalk.yellow(func) + '(';
684
+ });
685
+ // Highlight numbers with magenta
686
+ highlighted = highlighted.replace(/\b(\d+)\b/g, (match, num) => {
687
+ return chalk.magenta(num);
688
+ });
689
+ return highlighted;
690
+ }
691
+ async withTimeout(promise, ms) {
692
+ let timeout;
693
+ const timeoutPromise = new Promise((_, reject) => {
694
+ timeout = setTimeout(() => reject(new Error(`AI run timed out after ${ms}ms`)), ms);
695
+ });
696
+ const result = await Promise.race([promise, timeoutPromise]);
697
+ if (timeout) {
698
+ clearTimeout(timeout);
699
+ }
700
+ return result;
701
+ }
702
+ }
703
+ AiAgent.clientCache = null;
704
+ AiAgent.baseUrlCache = null;
705
+ AiAgent.serverStarted = false;
706
+ AiAgent.serverCloser = null;
707
+ //# sourceMappingURL=aiAgent.js.map