@hyperdrive.bot/bmad-workflow 1.0.3 → 1.0.6
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/README.md +373 -790
- package/dist/models/provider.d.ts +1 -1
- package/dist/models/provider.js +5 -0
- package/dist/services/agents/agent-runner-factory.js +5 -1
- package/dist/services/agents/index.d.ts +1 -0
- package/dist/services/agents/index.js +1 -0
- package/dist/services/agents/opencode-agent-runner.d.ts +92 -0
- package/dist/services/agents/opencode-agent-runner.js +392 -0
- package/dist/services/orchestration/task-decomposition-service.js +79 -126
- package/package.json +1 -1
package/dist/models/provider.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { ClaudeAgentRunner } from './claude-agent-runner.js';
|
|
7
7
|
import { GeminiAgentRunner } from './gemini-agent-runner.js';
|
|
8
|
+
import { OpenCodeAgentRunner } from './opencode-agent-runner.js';
|
|
8
9
|
/**
|
|
9
10
|
* Create an AI provider runner for the specified provider
|
|
10
11
|
*
|
|
@@ -28,6 +29,9 @@ export function createAgentRunner(provider, logger) {
|
|
|
28
29
|
case 'gemini': {
|
|
29
30
|
return new GeminiAgentRunner(logger);
|
|
30
31
|
}
|
|
32
|
+
case 'opencode': {
|
|
33
|
+
return new OpenCodeAgentRunner(logger);
|
|
34
|
+
}
|
|
31
35
|
default: {
|
|
32
36
|
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
33
37
|
}
|
|
@@ -40,5 +44,5 @@ export function createAgentRunner(provider, logger) {
|
|
|
40
44
|
* @returns true if the provider is supported
|
|
41
45
|
*/
|
|
42
46
|
export function isProviderSupported(provider) {
|
|
43
|
-
return provider === 'claude' || provider === 'gemini';
|
|
47
|
+
return provider === 'claude' || provider === 'gemini' || provider === 'opencode';
|
|
44
48
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCodeAgentRunner Service
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates Open Code CLI process spawning with comprehensive error handling,
|
|
5
|
+
* logging, and timeout management for reliable AI agent execution.
|
|
6
|
+
*/
|
|
7
|
+
import type pino from 'pino';
|
|
8
|
+
import type { AgentOptions, AgentResult } from '../../models/index.js';
|
|
9
|
+
import type { AIProvider } from '../../models/provider.js';
|
|
10
|
+
import type { AIProviderRunner } from './agent-runner.js';
|
|
11
|
+
/**
|
|
12
|
+
* OpenCodeAgentRunner service for executing Open Code AI agents
|
|
13
|
+
*
|
|
14
|
+
* Spawns Open Code CLI processes to execute AI agents with specified prompts.
|
|
15
|
+
* Handles timeout, error collection, result formatting, and process cleanup.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const logger = createLogger({ namespace: 'agent-runner' })
|
|
20
|
+
* const runner = new OpenCodeAgentRunner(logger)
|
|
21
|
+
* const result = await runner.runAgent({
|
|
22
|
+
* prompt: '@.bmad-core/agents/architect.md Create epic for user auth',
|
|
23
|
+
* agentType: 'architect',
|
|
24
|
+
* timeout: 300000
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare class OpenCodeAgentRunner implements AIProviderRunner {
|
|
29
|
+
readonly provider: AIProvider;
|
|
30
|
+
private readonly config;
|
|
31
|
+
private readonly logger;
|
|
32
|
+
/**
|
|
33
|
+
* Create a new OpenCodeAgentRunner instance
|
|
34
|
+
*
|
|
35
|
+
* @param logger - Logger instance for structured logging
|
|
36
|
+
*/
|
|
37
|
+
constructor(logger: pino.Logger);
|
|
38
|
+
/**
|
|
39
|
+
* Get the count of active processes
|
|
40
|
+
* (Useful for testing and monitoring purposes)
|
|
41
|
+
*
|
|
42
|
+
* @returns Number of currently active child processes
|
|
43
|
+
*/
|
|
44
|
+
getActiveProcessCount(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Execute an Open Code AI agent with the specified prompt and options
|
|
47
|
+
*
|
|
48
|
+
* This method spawns an Open Code CLI process, captures output, handles errors,
|
|
49
|
+
* and enforces timeouts. It never throws exceptions - all errors are returned
|
|
50
|
+
* as typed AgentResult objects.
|
|
51
|
+
*
|
|
52
|
+
* @param prompt - The prompt to execute with the agent
|
|
53
|
+
* @param options - Agent execution options (without prompt)
|
|
54
|
+
* @returns AgentResult with success status, output, errors, and metadata
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const result = await runner.runAgent('@.bmad-core/agents/architect.md Create epic for user auth', {
|
|
59
|
+
* agentType: 'architect',
|
|
60
|
+
* timeout: 300000
|
|
61
|
+
* })
|
|
62
|
+
*
|
|
63
|
+
* if (result.success) {
|
|
64
|
+
* console.log('Output:', result.output)
|
|
65
|
+
* } else {
|
|
66
|
+
* console.error('Error:', result.errors)
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
runAgent(prompt: string, options: Omit<AgentOptions, 'prompt'>): Promise<AgentResult>;
|
|
71
|
+
/**
|
|
72
|
+
* Parse JSON output from Open Code CLI
|
|
73
|
+
*
|
|
74
|
+
* Open Code's --format json outputs structured events.
|
|
75
|
+
* This method extracts the actual response content.
|
|
76
|
+
*
|
|
77
|
+
* @param stdoutData - Raw stdout from Open Code CLI
|
|
78
|
+
* @returns Extracted content string
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
private parseJsonOutput;
|
|
82
|
+
/**
|
|
83
|
+
* Build detailed error context for debugging process failures
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
private buildErrorContext;
|
|
87
|
+
/**
|
|
88
|
+
* Invoke onResponse callback and return result
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
private returnWithCallback;
|
|
92
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCodeAgentRunner Service
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates Open Code CLI process spawning with comprehensive error handling,
|
|
5
|
+
* logging, and timeout management for reliable AI agent execution.
|
|
6
|
+
*/
|
|
7
|
+
import { exec } from 'node:child_process';
|
|
8
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
import { PROVIDER_CONFIGS } from '../../models/provider.js';
|
|
14
|
+
/**
|
|
15
|
+
* Track active child processes for cleanup on SIGINT
|
|
16
|
+
*/
|
|
17
|
+
const activeProcesses = new Set();
|
|
18
|
+
/**
|
|
19
|
+
* Track if SIGINT handler has been registered
|
|
20
|
+
*/
|
|
21
|
+
let sigintHandlerRegistered = false;
|
|
22
|
+
/**
|
|
23
|
+
* Register SIGINT handler for graceful process cleanup
|
|
24
|
+
*/
|
|
25
|
+
const registerSigintHandler = (logger) => {
|
|
26
|
+
if (sigintHandlerRegistered)
|
|
27
|
+
return;
|
|
28
|
+
process.on('SIGINT', () => {
|
|
29
|
+
logger.info({
|
|
30
|
+
activeProcessCount: activeProcesses.size,
|
|
31
|
+
}, 'Received SIGINT, cleaning up active processes');
|
|
32
|
+
// Kill all active child processes
|
|
33
|
+
for (const childProcess of activeProcesses) {
|
|
34
|
+
try {
|
|
35
|
+
childProcess.kill('SIGTERM');
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
logger.error({
|
|
39
|
+
error: error.message,
|
|
40
|
+
}, 'Error killing child process');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Clear the set
|
|
44
|
+
activeProcesses.clear();
|
|
45
|
+
// Exit the main process
|
|
46
|
+
process.exit(130); // 130 = 128 + SIGINT signal number (2)
|
|
47
|
+
});
|
|
48
|
+
sigintHandlerRegistered = true;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* OpenCodeAgentRunner service for executing Open Code AI agents
|
|
52
|
+
*
|
|
53
|
+
* Spawns Open Code CLI processes to execute AI agents with specified prompts.
|
|
54
|
+
* Handles timeout, error collection, result formatting, and process cleanup.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const logger = createLogger({ namespace: 'agent-runner' })
|
|
59
|
+
* const runner = new OpenCodeAgentRunner(logger)
|
|
60
|
+
* const result = await runner.runAgent({
|
|
61
|
+
* prompt: '@.bmad-core/agents/architect.md Create epic for user auth',
|
|
62
|
+
* agentType: 'architect',
|
|
63
|
+
* timeout: 300000
|
|
64
|
+
* })
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export class OpenCodeAgentRunner {
|
|
68
|
+
provider = 'opencode';
|
|
69
|
+
config = PROVIDER_CONFIGS.opencode;
|
|
70
|
+
logger;
|
|
71
|
+
/**
|
|
72
|
+
* Create a new OpenCodeAgentRunner instance
|
|
73
|
+
*
|
|
74
|
+
* @param logger - Logger instance for structured logging
|
|
75
|
+
*/
|
|
76
|
+
constructor(logger) {
|
|
77
|
+
this.logger = logger;
|
|
78
|
+
registerSigintHandler(logger);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get the count of active processes
|
|
82
|
+
* (Useful for testing and monitoring purposes)
|
|
83
|
+
*
|
|
84
|
+
* @returns Number of currently active child processes
|
|
85
|
+
*/
|
|
86
|
+
getActiveProcessCount() {
|
|
87
|
+
return activeProcesses.size;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Execute an Open Code AI agent with the specified prompt and options
|
|
91
|
+
*
|
|
92
|
+
* This method spawns an Open Code CLI process, captures output, handles errors,
|
|
93
|
+
* and enforces timeouts. It never throws exceptions - all errors are returned
|
|
94
|
+
* as typed AgentResult objects.
|
|
95
|
+
*
|
|
96
|
+
* @param prompt - The prompt to execute with the agent
|
|
97
|
+
* @param options - Agent execution options (without prompt)
|
|
98
|
+
* @returns AgentResult with success status, output, errors, and metadata
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const result = await runner.runAgent('@.bmad-core/agents/architect.md Create epic for user auth', {
|
|
103
|
+
* agentType: 'architect',
|
|
104
|
+
* timeout: 300000
|
|
105
|
+
* })
|
|
106
|
+
*
|
|
107
|
+
* if (result.success) {
|
|
108
|
+
* console.log('Output:', result.output)
|
|
109
|
+
* } else {
|
|
110
|
+
* console.error('Error:', result.errors)
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
async runAgent(prompt, options) {
|
|
115
|
+
const startTime = Date.now();
|
|
116
|
+
const timeout = options.timeout ?? 1_800_000; // Default 30 minutes
|
|
117
|
+
// Input validation
|
|
118
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
119
|
+
return this.returnWithCallback({
|
|
120
|
+
agentType: options.agentType,
|
|
121
|
+
duration: Date.now() - startTime,
|
|
122
|
+
errors: 'Prompt is required and cannot be empty',
|
|
123
|
+
exitCode: -1,
|
|
124
|
+
output: '',
|
|
125
|
+
success: false,
|
|
126
|
+
}, options.onResponse);
|
|
127
|
+
}
|
|
128
|
+
// Log execution start with metadata
|
|
129
|
+
this.logger.info({
|
|
130
|
+
agentType: options.agentType,
|
|
131
|
+
promptLength: prompt.length,
|
|
132
|
+
references: options.references?.length ?? 0,
|
|
133
|
+
timeout,
|
|
134
|
+
}, 'Executing Open Code agent');
|
|
135
|
+
// Invoke onPrompt callback if provided
|
|
136
|
+
if (options.onPrompt) {
|
|
137
|
+
try {
|
|
138
|
+
// Create options object without callbacks for the callback
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
140
|
+
const { onPrompt, onResponse, ...callbackOptions } = options;
|
|
141
|
+
await onPrompt(prompt, callbackOptions);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
this.logger.warn({ error: error.message }, 'Error in onPrompt callback');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// For large prompts or prompts with special characters, use a temp file
|
|
148
|
+
// This avoids shell escaping issues
|
|
149
|
+
let tempDir = null;
|
|
150
|
+
let tempFile = null;
|
|
151
|
+
let stdoutData = '';
|
|
152
|
+
let stderrData = '';
|
|
153
|
+
try {
|
|
154
|
+
// Create temp directory and file
|
|
155
|
+
tempDir = await mkdtemp(join(tmpdir(), 'opencode-prompt-'));
|
|
156
|
+
tempFile = join(tempDir, 'prompt.txt');
|
|
157
|
+
await writeFile(tempFile, prompt, 'utf8');
|
|
158
|
+
// Build command with --file flag (Open Code specific)
|
|
159
|
+
// Format: opencode run --format json --file "${tempFile}"
|
|
160
|
+
const flags = this.config.flags.join(' ');
|
|
161
|
+
const command = `${this.config.command} ${flags} --file "${tempFile}"`;
|
|
162
|
+
// Log the command being executed
|
|
163
|
+
this.logger.info({
|
|
164
|
+
command,
|
|
165
|
+
promptLength: prompt.length,
|
|
166
|
+
tempFile,
|
|
167
|
+
}, 'Executing command with temp file');
|
|
168
|
+
// Use exec for better shell compatibility
|
|
169
|
+
const { stderr, stdout } = await execAsync(command, {
|
|
170
|
+
env: process.env,
|
|
171
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
172
|
+
shell: process.env.SHELL || '/bin/bash',
|
|
173
|
+
timeout,
|
|
174
|
+
});
|
|
175
|
+
stdoutData = stdout;
|
|
176
|
+
stderrData = stderr;
|
|
177
|
+
const duration = Date.now() - startTime;
|
|
178
|
+
this.logger.info({
|
|
179
|
+
duration,
|
|
180
|
+
stderrLength: stderrData.length,
|
|
181
|
+
stdoutLength: stdoutData.length,
|
|
182
|
+
}, 'Open Code CLI process completed successfully');
|
|
183
|
+
// Parse JSON output from Open Code
|
|
184
|
+
const parsedOutput = this.parseJsonOutput(stdoutData);
|
|
185
|
+
const result = {
|
|
186
|
+
agentType: options.agentType,
|
|
187
|
+
duration,
|
|
188
|
+
errors: stderrData,
|
|
189
|
+
exitCode: 0,
|
|
190
|
+
output: parsedOutput,
|
|
191
|
+
success: true,
|
|
192
|
+
};
|
|
193
|
+
return this.returnWithCallback(result, options.onResponse);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
// Handle exec errors (includes timeout, non-zero exit, etc.)
|
|
197
|
+
const duration = Date.now() - startTime;
|
|
198
|
+
// Extract stdout/stderr from exec error if available
|
|
199
|
+
const execError = error;
|
|
200
|
+
if (execError.stdout)
|
|
201
|
+
stdoutData = execError.stdout;
|
|
202
|
+
if (execError.stderr)
|
|
203
|
+
stderrData = execError.stderr;
|
|
204
|
+
const exitCode = execError.code ?? -1;
|
|
205
|
+
const isTimeout = execError.killed === true && execError.signal === 'SIGTERM';
|
|
206
|
+
const wasKilled = execError.killed === true;
|
|
207
|
+
const signal = execError.signal ?? null;
|
|
208
|
+
// Build detailed error context for debugging
|
|
209
|
+
const errorContext = this.buildErrorContext({
|
|
210
|
+
cmd: execError.cmd,
|
|
211
|
+
duration,
|
|
212
|
+
exitCode,
|
|
213
|
+
isTimeout,
|
|
214
|
+
signal,
|
|
215
|
+
stderrData,
|
|
216
|
+
stdoutData,
|
|
217
|
+
wasKilled,
|
|
218
|
+
});
|
|
219
|
+
this.logger.error({
|
|
220
|
+
cmd: execError.cmd,
|
|
221
|
+
duration,
|
|
222
|
+
error: execError.message,
|
|
223
|
+
exitCode,
|
|
224
|
+
isTimeout,
|
|
225
|
+
signal,
|
|
226
|
+
stderrLength: stderrData.length,
|
|
227
|
+
stdoutLength: stdoutData.length,
|
|
228
|
+
wasKilled,
|
|
229
|
+
}, isTimeout ? 'Open Code CLI process timeout' : 'Open Code CLI process error');
|
|
230
|
+
return this.returnWithCallback({
|
|
231
|
+
agentType: options.agentType,
|
|
232
|
+
duration,
|
|
233
|
+
errors: errorContext,
|
|
234
|
+
exitCode: isTimeout ? 124 : exitCode,
|
|
235
|
+
output: stdoutData,
|
|
236
|
+
success: false,
|
|
237
|
+
}, options.onResponse);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
// Clean up temp file and directory
|
|
241
|
+
if (tempDir) {
|
|
242
|
+
try {
|
|
243
|
+
await rm(tempDir, { force: true, recursive: true });
|
|
244
|
+
this.logger.debug({ tempDir }, 'Cleaned up temp directory');
|
|
245
|
+
}
|
|
246
|
+
catch (cleanupError) {
|
|
247
|
+
this.logger.warn({ error: cleanupError.message, tempDir }, 'Failed to clean up temp directory');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Parse JSON output from Open Code CLI
|
|
254
|
+
*
|
|
255
|
+
* Open Code's --format json outputs structured events.
|
|
256
|
+
* This method extracts the actual response content.
|
|
257
|
+
*
|
|
258
|
+
* @param stdoutData - Raw stdout from Open Code CLI
|
|
259
|
+
* @returns Extracted content string
|
|
260
|
+
* @private
|
|
261
|
+
*/
|
|
262
|
+
parseJsonOutput(stdoutData) {
|
|
263
|
+
if (!stdoutData || stdoutData.trim().length === 0) {
|
|
264
|
+
return '';
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(stdoutData);
|
|
268
|
+
// Try common JSON response structures
|
|
269
|
+
// Open Code may output different structures depending on version
|
|
270
|
+
if (typeof parsed === 'string') {
|
|
271
|
+
return parsed;
|
|
272
|
+
}
|
|
273
|
+
if (parsed.content) {
|
|
274
|
+
return String(parsed.content);
|
|
275
|
+
}
|
|
276
|
+
if (parsed.message) {
|
|
277
|
+
return String(parsed.message);
|
|
278
|
+
}
|
|
279
|
+
if (parsed.response) {
|
|
280
|
+
return String(parsed.response);
|
|
281
|
+
}
|
|
282
|
+
if (parsed.output) {
|
|
283
|
+
return String(parsed.output);
|
|
284
|
+
}
|
|
285
|
+
if (parsed.text) {
|
|
286
|
+
return String(parsed.text);
|
|
287
|
+
}
|
|
288
|
+
// If we have a result array, try to extract content
|
|
289
|
+
if (Array.isArray(parsed)) {
|
|
290
|
+
const messages = parsed
|
|
291
|
+
.filter((item) => item.type === 'message' || item.content || item.text)
|
|
292
|
+
.map((item) => item.content || item.text || item.message || '')
|
|
293
|
+
.filter(Boolean);
|
|
294
|
+
if (messages.length > 0) {
|
|
295
|
+
return messages.join('\n');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Fallback: stringify the parsed object
|
|
299
|
+
this.logger.debug({ parsedKeys: Object.keys(parsed) }, 'Unknown JSON structure, returning stringified');
|
|
300
|
+
return JSON.stringify(parsed, null, 2);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// JSON parsing failed, return raw output
|
|
304
|
+
this.logger.debug({ stdoutLength: stdoutData.length }, 'JSON parsing failed, returning raw output');
|
|
305
|
+
return stdoutData;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Build detailed error context for debugging process failures
|
|
310
|
+
* @private
|
|
311
|
+
*/
|
|
312
|
+
buildErrorContext(info) {
|
|
313
|
+
const lines = [];
|
|
314
|
+
// Determine the type of failure
|
|
315
|
+
if (info.isTimeout) {
|
|
316
|
+
lines.push(`Process timeout after ${info.duration}ms`);
|
|
317
|
+
}
|
|
318
|
+
else if (info.wasKilled) {
|
|
319
|
+
lines.push('Process was killed externally');
|
|
320
|
+
// Explain common exit codes
|
|
321
|
+
switch (info.exitCode) {
|
|
322
|
+
case 130: {
|
|
323
|
+
lines.push('Exit code 130 = SIGINT (user interrupt, Ctrl+C)');
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case 137: {
|
|
327
|
+
lines.push('Exit code 137 = SIGKILL (process forcefully killed)', 'Possible causes:', ' - System OOM killer', ' - Exceeded memory limits', ' - Docker/container killed the process');
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case 143: {
|
|
331
|
+
lines.push('Exit code 143 = SIGTERM (process terminated)', 'Possible causes:', ' - System OOM killer terminated the process', ' - Another process sent SIGTERM', ' - Parent process cleanup', ' - Docker/container resource limits');
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
// No default
|
|
335
|
+
}
|
|
336
|
+
if (info.signal) {
|
|
337
|
+
lines.push(`Signal received: ${info.signal}`);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
lines.push('No signal information available (signal: null)');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
lines.push(`Process exited with code ${info.exitCode}`);
|
|
345
|
+
}
|
|
346
|
+
// Add duration context
|
|
347
|
+
lines.push(`Duration: ${(info.duration / 1000).toFixed(2)}s`);
|
|
348
|
+
// Add output context
|
|
349
|
+
if (info.stdoutData.length > 0) {
|
|
350
|
+
lines.push(`Stdout (${info.stdoutData.length} chars):`);
|
|
351
|
+
// Truncate if too long, show last 500 chars which are often most relevant
|
|
352
|
+
const truncatedStdout = info.stdoutData.length > 500 ? `...[truncated]...\n${info.stdoutData.slice(-500)}` : info.stdoutData;
|
|
353
|
+
lines.push(truncatedStdout);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
lines.push('Stdout: (empty - no output captured)');
|
|
357
|
+
}
|
|
358
|
+
if (info.stderrData.length > 0) {
|
|
359
|
+
lines.push(`Stderr (${info.stderrData.length} chars):`);
|
|
360
|
+
const truncatedStderr = info.stderrData.length > 500 ? `...[truncated]...\n${info.stderrData.slice(-500)}` : info.stderrData;
|
|
361
|
+
lines.push(truncatedStderr);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
lines.push('Stderr: (empty - no error output)');
|
|
365
|
+
}
|
|
366
|
+
// Add command for reference (truncate if too long)
|
|
367
|
+
if (info.cmd) {
|
|
368
|
+
const truncatedCmd = info.cmd.length > 200 ? `${info.cmd.slice(0, 200)}...[truncated]` : info.cmd;
|
|
369
|
+
lines.push(`Command: ${truncatedCmd}`);
|
|
370
|
+
}
|
|
371
|
+
// Add troubleshooting suggestions for empty output
|
|
372
|
+
if (info.stdoutData.length === 0 && info.stderrData.length === 0) {
|
|
373
|
+
lines.push('', 'Troubleshooting (no output captured):', ' 1. Check system resources (memory, CPU)', ' 2. Verify Open Code CLI is properly installed: `opencode --version`', ' 3. Check for Open Code API issues', ' 4. Review system logs: `dmesg | tail -50` (for OOM killer)', ' 5. Try running the command manually to see output');
|
|
374
|
+
}
|
|
375
|
+
return lines.join('\n');
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Invoke onResponse callback and return result
|
|
379
|
+
* @private
|
|
380
|
+
*/
|
|
381
|
+
async returnWithCallback(result, onResponse) {
|
|
382
|
+
if (onResponse) {
|
|
383
|
+
try {
|
|
384
|
+
await onResponse(result);
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
this.logger.warn({ error: error.message }, 'Error in onResponse callback');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
}
|