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.
- package/LICENSE +30 -21
- package/README.md +6 -1
- package/bin/codevf-mcp.js +11 -0
- package/dist/commands/fix.d.ts +5 -1
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +170 -13
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +72 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mcp-tools.d.ts +17 -0
- package/dist/commands/mcp-tools.d.ts.map +1 -0
- package/dist/commands/mcp-tools.js +237 -0
- package/dist/commands/mcp-tools.js.map +1 -0
- package/dist/commands/setup.d.ts +8 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +250 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/welcome.d.ts +9 -0
- package/dist/commands/welcome.d.ts.map +1 -0
- package/dist/commands/welcome.js +175 -0
- package/dist/commands/welcome.js.map +1 -0
- package/dist/index.js +194 -149
- package/dist/index.js.map +1 -1
- package/dist/lib/api/client.d.ts +28 -0
- package/dist/lib/api/client.d.ts.map +1 -0
- package/dist/lib/api/client.js +66 -0
- package/dist/lib/api/client.js.map +1 -0
- package/dist/lib/api/tasks.d.ts +36 -0
- package/dist/lib/api/tasks.d.ts.map +1 -0
- package/dist/lib/api/tasks.js +62 -0
- package/dist/lib/api/tasks.js.map +1 -0
- package/dist/lib/api/websocket.d.ts +50 -0
- package/dist/lib/api/websocket.d.ts.map +1 -0
- package/dist/lib/api/websocket.js +153 -0
- package/dist/lib/api/websocket.js.map +1 -0
- package/dist/lib/auth/oauth-flow.d.ts +37 -0
- package/dist/lib/auth/oauth-flow.d.ts.map +1 -0
- package/dist/lib/auth/oauth-flow.js +119 -0
- package/dist/lib/auth/oauth-flow.js.map +1 -0
- package/dist/lib/auth/token-manager.d.ts +26 -0
- package/dist/lib/auth/token-manager.d.ts.map +1 -0
- package/dist/lib/auth/token-manager.js +87 -0
- package/dist/lib/auth/token-manager.js.map +1 -0
- package/dist/lib/config/manager.d.ts +50 -0
- package/dist/lib/config/manager.d.ts.map +1 -0
- package/dist/lib/config/manager.js +84 -0
- package/dist/lib/config/manager.js.map +1 -0
- package/dist/lib/utils/errors.d.ts +28 -0
- package/dist/lib/utils/errors.d.ts.map +1 -0
- package/dist/lib/utils/errors.js +44 -0
- package/dist/lib/utils/errors.js.map +1 -0
- package/dist/lib/utils/logger.d.ts +20 -0
- package/dist/lib/utils/logger.d.ts.map +1 -0
- package/dist/lib/utils/logger.js +40 -0
- package/dist/lib/utils/logger.js.map +1 -0
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +158 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/tools/chat.d.ts +30 -0
- package/dist/mcp/tools/chat.d.ts.map +1 -0
- package/dist/mcp/tools/chat.js +82 -0
- package/dist/mcp/tools/chat.js.map +1 -0
- package/dist/mcp/tools/instant.d.ts +36 -0
- package/dist/mcp/tools/instant.d.ts.map +1 -0
- package/dist/mcp/tools/instant.js +100 -0
- package/dist/mcp/tools/instant.js.map +1 -0
- package/dist/modules/aiAgent.d.ts +75 -0
- package/dist/modules/aiAgent.d.ts.map +1 -0
- package/dist/modules/aiAgent.js +707 -0
- package/dist/modules/aiAgent.js.map +1 -0
- package/dist/modules/api.d.ts +7 -0
- package/dist/modules/api.d.ts.map +1 -1
- package/dist/modules/api.js +13 -4
- package/dist/modules/api.js.map +1 -1
- package/dist/modules/commandHandler.d.ts +40 -0
- package/dist/modules/commandHandler.d.ts.map +1 -0
- package/dist/modules/commandHandler.js +345 -0
- package/dist/modules/commandHandler.js.map +1 -0
- package/dist/modules/config.d.ts +2 -0
- package/dist/modules/config.d.ts.map +1 -1
- package/dist/modules/config.js +9 -0
- package/dist/modules/config.js.map +1 -1
- package/dist/modules/constants.d.ts +83 -0
- package/dist/modules/constants.d.ts.map +1 -0
- package/dist/modules/constants.js +75 -0
- package/dist/modules/constants.js.map +1 -0
- package/dist/modules/permissions.d.ts +14 -0
- package/dist/modules/permissions.d.ts.map +1 -1
- package/dist/modules/permissions.js +94 -0
- package/dist/modules/permissions.js.map +1 -1
- package/dist/modules/toolRegistry.d.ts +50 -0
- package/dist/modules/toolRegistry.d.ts.map +1 -0
- package/dist/modules/toolRegistry.js +114 -0
- package/dist/modules/toolRegistry.js.map +1 -0
- package/dist/modules/tunnel.d.ts +33 -0
- package/dist/modules/tunnel.d.ts.map +1 -0
- package/dist/modules/tunnel.js +79 -0
- package/dist/modules/tunnel.js.map +1 -0
- package/dist/modules/vibeHelper.d.ts +16 -0
- package/dist/modules/vibeHelper.d.ts.map +1 -0
- package/dist/modules/vibeHelper.js +38 -0
- package/dist/modules/vibeHelper.js.map +1 -0
- package/dist/modules/websocket.d.ts +9 -0
- package/dist/modules/websocket.d.ts.map +1 -1
- package/dist/modules/websocket.js +70 -0
- package/dist/modules/websocket.js.map +1 -1
- package/dist/tools/consultEngineer.d.ts +13 -0
- package/dist/tools/consultEngineer.d.ts.map +1 -0
- package/dist/tools/consultEngineer.js +161 -0
- package/dist/tools/consultEngineer.js.map +1 -0
- package/dist/tools/realtimeChat.d.ts +9 -0
- package/dist/tools/realtimeChat.d.ts.map +1 -0
- package/dist/tools/realtimeChat.js +101 -0
- package/dist/tools/realtimeChat.js.map +1 -0
- package/dist/types/index.d.ts +183 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/ui/InteractiveApp.d.ts +13 -0
- package/dist/ui/InteractiveApp.d.ts.map +1 -0
- package/dist/ui/InteractiveApp.js +84 -0
- package/dist/ui/InteractiveApp.js.map +1 -0
- package/dist/ui/InteractivePrompt.d.ts +53 -0
- package/dist/ui/InteractivePrompt.d.ts.map +1 -0
- package/dist/ui/InteractivePrompt.js +422 -0
- package/dist/ui/InteractivePrompt.js.map +1 -0
- package/dist/ui/LiveSession.d.ts +2 -0
- package/dist/ui/LiveSession.d.ts.map +1 -1
- package/dist/ui/LiveSession.js +461 -180
- package/dist/ui/LiveSession.js.map +1 -1
- package/dist/ui/PromptInput.d.ts +14 -0
- package/dist/ui/PromptInput.d.ts.map +1 -0
- package/dist/ui/PromptInput.js +206 -0
- package/dist/ui/PromptInput.js.map +1 -0
- package/dist/ui/SessionUI.d.ts +40 -0
- package/dist/ui/SessionUI.d.ts.map +1 -0
- package/dist/ui/SessionUI.js +227 -0
- package/dist/ui/SessionUI.js.map +1 -0
- package/dist/ui/input/Command.d.ts +22 -0
- package/dist/ui/input/Command.d.ts.map +1 -0
- package/dist/ui/input/Command.js +30 -0
- package/dist/ui/input/Command.js.map +1 -0
- package/dist/ui/input/CustomInput.d.ts +15 -0
- package/dist/ui/input/CustomInput.d.ts.map +1 -0
- package/dist/ui/input/CustomInput.js +182 -0
- package/dist/ui/input/CustomInput.js.map +1 -0
- package/dist/ui/input/handlers/handleCursor.d.ts +22 -0
- package/dist/ui/input/handlers/handleCursor.d.ts.map +1 -0
- package/dist/ui/input/handlers/handleCursor.js +53 -0
- package/dist/ui/input/handlers/handleCursor.js.map +1 -0
- package/dist/ui/input/handlers/handleEdit.d.ts +18 -0
- package/dist/ui/input/handlers/handleEdit.d.ts.map +1 -0
- package/dist/ui/input/handlers/handleEdit.js +55 -0
- package/dist/ui/input/handlers/handleEdit.js.map +1 -0
- package/dist/ui/input/handlers/handleHistory.d.ts +18 -0
- package/dist/ui/input/handlers/handleHistory.d.ts.map +1 -0
- package/dist/ui/input/handlers/handleHistory.js +85 -0
- package/dist/ui/input/handlers/handleHistory.js.map +1 -0
- package/dist/ui/input/handlers/handlePaste.d.ts +19 -0
- package/dist/ui/input/handlers/handlePaste.d.ts.map +1 -0
- package/dist/ui/input/handlers/handlePaste.js +49 -0
- package/dist/ui/input/handlers/handlePaste.js.map +1 -0
- package/dist/ui/input/handlers/handleSubmit.d.ts +18 -0
- package/dist/ui/input/handlers/handleSubmit.d.ts.map +1 -0
- package/dist/ui/input/handlers/handleSubmit.js +39 -0
- package/dist/ui/input/handlers/handleSubmit.js.map +1 -0
- package/dist/ui/input/helpers.d.ts +4 -0
- package/dist/ui/input/helpers.d.ts.map +1 -0
- package/dist/ui/input/helpers.js +13 -0
- package/dist/ui/input/helpers.js.map +1 -0
- package/dist/ui/input/keyMatchers.d.ts +14 -0
- package/dist/ui/input/keyMatchers.d.ts.map +1 -0
- package/dist/ui/input/keyMatchers.js +49 -0
- package/dist/ui/input/keyMatchers.js.map +1 -0
- package/dist/ui/input/types.d.ts +33 -0
- package/dist/ui/input/types.d.ts.map +1 -0
- package/dist/ui/input/types.js +2 -0
- package/dist/ui/input/types.js.map +1 -0
- package/dist/ui/promptWithModes.d.ts +12 -0
- package/dist/ui/promptWithModes.d.ts.map +1 -0
- package/dist/ui/promptWithModes.js +24 -0
- package/dist/ui/promptWithModes.js.map +1 -0
- package/dist/ui/renderPrompt.d.ts +12 -0
- package/dist/ui/renderPrompt.d.ts.map +1 -0
- package/dist/ui/renderPrompt.js +14 -0
- package/dist/ui/renderPrompt.js.map +1 -0
- package/dist/ui/simplePrompt.d.ts +7 -0
- package/dist/ui/simplePrompt.d.ts.map +1 -0
- package/dist/ui/simplePrompt.js +38 -0
- package/dist/ui/simplePrompt.js.map +1 -0
- package/dist/ui/spinner.d.ts +7 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +13 -0
- package/dist/ui/spinner.js.map +1 -0
- package/package.json +36 -25
- package/ARCHITECTURE.md +0 -285
- package/BUILD_SUMMARY.md +0 -340
- 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
|