gbos 1.3.22 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -155,6 +155,7 @@ Stored at `~/.gbos/session.json`:
155
155
  | Variable | Description |
156
156
  |----------|-------------|
157
157
  | `DEBUG=1` | Enable debug output |
158
+ | `GBOS_API_URL` | Override API endpoint (default: `https://api.gbos.io/api/v1`) |
158
159
 
159
160
  ## API Endpoints
160
161
 
@@ -260,7 +261,7 @@ gbos auth --force
260
261
  ## Links
261
262
 
262
263
  - **GBOS Platform**: [https://gbos.io](https://gbos.io)
263
- - **API Documentation**: [https://gbos-api-579767694933.us-south1.run.app/docs](https://gbos-api-579767694933.us-south1.run.app/docs)
264
+ - **API Documentation**: [https://api.gbos.io/docs](https://api.gbos.io/docs)
264
265
  - **Issues**: [https://github.com/mystroanalytics/gbos-node-local/issues](https://github.com/mystroanalytics/gbos-node-local/issues)
265
266
 
266
267
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gbos",
3
- "version": "1.3.22",
3
+ "version": "1.4.0",
4
4
  "description": "GBOS - Command line interface for GBOS services",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -9,6 +9,7 @@ const logoutCommand = require('./commands/logout');
9
9
  const { tasksCommand, nextTaskCommand, continueCommand, fallbackCommand, addTaskCommand, completedCommand } = require('./commands/tasks');
10
10
  const { syncStartCommand, syncStopCommand, syncStatusCommand, syncNowCommand, repoCreateCommand, repoListCommand, repoCloneCommand } = require('./commands/gitlab');
11
11
  const { registryLoginCommand, registryImagesCommand, registryPushCommand, registryPullCommand } = require('./commands/registry');
12
+ const { startCommand, resumeCommand, stopCommand, runsCommand } = require('./commands/orchestrator');
12
13
  const config = require('./lib/config');
13
14
  const { displayStatus, printBanner } = require('./lib/display');
14
15
 
@@ -133,6 +134,40 @@ program
133
134
  .description('Create a new task interactively')
134
135
  .action(addTaskCommand);
135
136
 
137
+ // ==================== Orchestrator Commands ====================
138
+
139
+ program
140
+ .command('start')
141
+ .description('Start the GBOS orchestrator to automatically process tasks')
142
+ .option('-a, --agent <agent>', 'Agent to use (claude-code, codex, gemini)', 'claude-code')
143
+ .option('-d, --dir <directory>', 'Working directory')
144
+ .option('--auto-approve', 'Auto-approve agent actions')
145
+ .option('--no-mr', 'Skip merge request creation')
146
+ .option('-c, --continuous', 'Continuously process tasks')
147
+ .option('-n, --max-tasks <number>', 'Maximum tasks to process', '1')
148
+ .option('--show-prompt', 'Show the generated prompt')
149
+ .action(startCommand);
150
+
151
+ program
152
+ .command('resume')
153
+ .description('Resume a paused orchestrator run')
154
+ .option('-r, --run-id <runId>', 'Specific run ID to resume')
155
+ .option('--no-mr', 'Skip merge request creation')
156
+ .action(resumeCommand);
157
+
158
+ program
159
+ .command('stop')
160
+ .description('Stop an active orchestrator run')
161
+ .option('-r, --run-id <runId>', 'Specific run ID to stop')
162
+ .option('-f, --force', 'Force stop and mark as failed')
163
+ .action(stopCommand);
164
+
165
+ program
166
+ .command('runs')
167
+ .description('List recent orchestrator runs')
168
+ .option('-l, --limit <number>', 'Number of runs to show', '10')
169
+ .action(runsCommand);
170
+
136
171
  program
137
172
  .command('logout')
138
173
  .description('Log out from GBOS services and clear credentials')
@@ -244,7 +279,7 @@ program
244
279
  cmd.outputHelp();
245
280
  } else {
246
281
  console.log(`Unknown command: ${command}`);
247
- console.log('Available commands: auth, connect, disconnect, status, tasks, next, continue, completed, fallback, add_task, logout, gitlab, registry, help');
282
+ console.log('Available commands: auth, connect, disconnect, status, tasks, next, continue, completed, fallback, add_task, start, resume, stop, runs, logout, gitlab, registry, help');
248
283
  }
249
284
  } else {
250
285
  program.outputHelp();
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Orchestrator Commands
3
+ * CLI commands for start, resume, and stop
4
+ */
5
+
6
+ const config = require('../lib/config');
7
+ const { displayMessageBox, fg, LOGO_PURPLE, RESET, BOLD, DIM, getTerminalWidth } = require('../lib/display');
8
+ const Orchestrator = require('../orchestrator/orchestrator');
9
+ const { StateMachine, STATES, RUNS_DIR } = require('../orchestrator/state-machine');
10
+ const { checkInstalledAdapters } = require('../orchestrator/adapters');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ // Colors
15
+ const GREEN = '\x1b[32m';
16
+ const YELLOW = '\x1b[33m';
17
+ const RED = '\x1b[31m';
18
+ const CYAN = '\x1b[36m';
19
+
20
+ /**
21
+ * gbos start - Start the orchestrator
22
+ */
23
+ async function startCommand(options) {
24
+ // Check authentication
25
+ if (!config.isAuthenticated()) {
26
+ displayMessageBox('Not Authenticated', 'Please run "gbos auth" first.', 'warning');
27
+ process.exit(1);
28
+ }
29
+
30
+ // Check connection
31
+ const connection = config.getConnection();
32
+ if (!connection) {
33
+ displayMessageBox('Not Connected', 'Please run "gbos connect" first.', 'warning');
34
+ process.exit(1);
35
+ }
36
+
37
+ const termWidth = getTerminalWidth();
38
+ const tableWidth = Math.min(80, termWidth - 4);
39
+
40
+ console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
41
+ console.log(`${BOLD} GBOS Orchestrator${RESET}`);
42
+ console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
43
+
44
+ // Check for existing active run
45
+ const activeRun = StateMachine.getActiveRun();
46
+ if (activeRun) {
47
+ console.log(` ${YELLOW}!${RESET} An active run exists: ${activeRun.runId}`);
48
+ console.log(` ${DIM}State: ${activeRun.state}${RESET}`);
49
+ console.log(` ${DIM}Use "gbos resume" to continue or "gbos stop" to cancel.${RESET}\n`);
50
+ return;
51
+ }
52
+
53
+ // Check agent availability
54
+ console.log(` ${DIM}Checking agent availability...${RESET}`);
55
+ const adapters = await checkInstalledAdapters();
56
+ const agentName = options.agent || 'claude-code';
57
+ const agentInfo = adapters[agentName] || adapters['claude-code'];
58
+
59
+ if (!agentInfo?.available) {
60
+ displayMessageBox('Agent Not Found', `Agent "${agentName}" is not installed.\n\nAvailable agents: ${Object.entries(adapters).filter(([_, v]) => v.available).map(([k]) => k).join(', ') || 'none'}`, 'error');
61
+ process.exit(1);
62
+ }
63
+
64
+ console.log(` ${GREEN}✓${RESET} Agent: ${agentName} (${agentInfo.version || 'installed'})`);
65
+ console.log(` ${DIM}Application: ${connection.application?.name || 'N/A'}${RESET}`);
66
+ console.log(` ${DIM}Node: ${connection.node?.name || 'N/A'}${RESET}`);
67
+ console.log('');
68
+
69
+ // Create orchestrator
70
+ const orchestrator = new Orchestrator({
71
+ agent: agentName,
72
+ autoApprove: options.autoApprove || false,
73
+ createMR: options.mr !== false,
74
+ continuous: options.continuous || false,
75
+ maxTasks: options.maxTasks ? parseInt(options.maxTasks) : 1,
76
+ workingDir: options.dir ? path.resolve(options.dir) : null,
77
+ });
78
+
79
+ // Set up event handlers
80
+ orchestrator.on('started', ({ runId }) => {
81
+ console.log(` ${GREEN}✓${RESET} Run started: ${runId}\n`);
82
+ });
83
+
84
+ orchestrator.on('stage', ({ stage }) => {
85
+ console.log(` ${CYAN}▸${RESET} ${stage.replace(/_/g, ' ')}`);
86
+ });
87
+
88
+ orchestrator.on('log', ({ message }) => {
89
+ console.log(` ${DIM}${message}${RESET}`);
90
+ });
91
+
92
+ orchestrator.on('prompt', ({ prompt }) => {
93
+ if (options.showPrompt) {
94
+ console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
95
+ console.log(`${BOLD} Task Prompt${RESET}`);
96
+ console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
97
+ console.log(prompt);
98
+ console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
99
+ }
100
+ });
101
+
102
+ orchestrator.on('committed', (result) => {
103
+ if (result.commit) {
104
+ console.log(` ${GREEN}✓${RESET} Committed: ${result.commit.shortHash}`);
105
+ }
106
+ if (result.mergeRequest) {
107
+ console.log(` ${GREEN}✓${RESET} MR: ${result.mergeRequest.url}`);
108
+ }
109
+ });
110
+
111
+ orchestrator.on('completed', ({ tasksCompleted }) => {
112
+ console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
113
+ console.log(`${GREEN}✓${RESET} ${BOLD}Orchestrator completed${RESET}`);
114
+ console.log(` ${DIM}Tasks completed: ${tasksCompleted}${RESET}`);
115
+ console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
116
+ });
117
+
118
+ orchestrator.on('failed', ({ error }) => {
119
+ console.log(`\n${RED}✗${RESET} ${BOLD}Orchestrator failed${RESET}`);
120
+ console.log(` ${DIM}Error: ${error.message}${RESET}\n`);
121
+ });
122
+
123
+ // Handle interrupts
124
+ process.on('SIGINT', async () => {
125
+ console.log(`\n\n ${YELLOW}!${RESET} Stopping orchestrator...`);
126
+ await orchestrator.stop();
127
+ console.log(` ${DIM}Run paused. Use "gbos resume" to continue.${RESET}\n`);
128
+ process.exit(0);
129
+ });
130
+
131
+ // Start the orchestrator
132
+ try {
133
+ await orchestrator.start();
134
+ } catch (error) {
135
+ console.log(`\n${RED}✗${RESET} ${error.message}\n`);
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * gbos resume - Resume a paused run
142
+ */
143
+ async function resumeCommand(options) {
144
+ const termWidth = getTerminalWidth();
145
+ const tableWidth = Math.min(80, termWidth - 4);
146
+
147
+ console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
148
+ console.log(`${BOLD} Resume Orchestrator${RESET}`);
149
+ console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
150
+
151
+ // Find run to resume
152
+ let runId = options.runId;
153
+ let run;
154
+
155
+ if (runId) {
156
+ try {
157
+ run = StateMachine.loadRun(runId);
158
+ } catch (e) {
159
+ displayMessageBox('Run Not Found', `Run "${runId}" not found.`, 'error');
160
+ process.exit(1);
161
+ }
162
+ } else {
163
+ run = StateMachine.getActiveRun();
164
+ if (!run) {
165
+ console.log(` ${DIM}No active runs to resume.${RESET}`);
166
+ console.log(` ${DIM}Use "gbos start" to begin a new run.${RESET}\n`);
167
+ return;
168
+ }
169
+ runId = run.runId;
170
+ }
171
+
172
+ if (!run.isResumable()) {
173
+ console.log(` ${YELLOW}!${RESET} Run ${runId} is not resumable (state: ${run.state})`);
174
+ return;
175
+ }
176
+
177
+ console.log(` ${DIM}Resuming run: ${runId}${RESET}`);
178
+ console.log(` ${DIM}State: ${run.state}${RESET}`);
179
+ console.log(` ${DIM}Task: ${run.context.taskKey || run.context.taskId || 'N/A'}${RESET}\n`);
180
+
181
+ // Create orchestrator and resume
182
+ const orchestrator = new Orchestrator({
183
+ agent: run.context.agentVendor || 'claude-code',
184
+ createMR: options.mr !== false,
185
+ });
186
+
187
+ // Set up event handlers (same as start)
188
+ orchestrator.on('stage', ({ stage }) => {
189
+ console.log(` ${CYAN}▸${RESET} ${stage.replace(/_/g, ' ')}`);
190
+ });
191
+
192
+ orchestrator.on('log', ({ message }) => {
193
+ console.log(` ${DIM}${message}${RESET}`);
194
+ });
195
+
196
+ orchestrator.on('completed', ({ tasksCompleted }) => {
197
+ console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
198
+ console.log(`${GREEN}✓${RESET} ${BOLD}Orchestrator completed${RESET}`);
199
+ console.log(` ${DIM}Tasks completed: ${tasksCompleted}${RESET}`);
200
+ console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
201
+ });
202
+
203
+ orchestrator.on('failed', ({ error }) => {
204
+ console.log(`\n${RED}✗${RESET} ${BOLD}Orchestrator failed${RESET}`);
205
+ console.log(` ${DIM}Error: ${error.message}${RESET}\n`);
206
+ });
207
+
208
+ process.on('SIGINT', async () => {
209
+ console.log(`\n\n ${YELLOW}!${RESET} Stopping orchestrator...`);
210
+ await orchestrator.stop();
211
+ process.exit(0);
212
+ });
213
+
214
+ try {
215
+ await orchestrator.resume(runId);
216
+ } catch (error) {
217
+ console.log(`\n${RED}✗${RESET} ${error.message}\n`);
218
+ process.exit(1);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * gbos stop - Stop an active run
224
+ */
225
+ async function stopCommand(options) {
226
+ const termWidth = getTerminalWidth();
227
+ const tableWidth = Math.min(80, termWidth - 4);
228
+
229
+ console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
230
+ console.log(`${BOLD} Stop Orchestrator${RESET}`);
231
+ console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
232
+
233
+ // Find active run
234
+ let runId = options.runId;
235
+ let run;
236
+
237
+ if (runId) {
238
+ try {
239
+ run = StateMachine.loadRun(runId);
240
+ } catch (e) {
241
+ displayMessageBox('Run Not Found', `Run "${runId}" not found.`, 'error');
242
+ process.exit(1);
243
+ }
244
+ } else {
245
+ run = StateMachine.getActiveRun();
246
+ if (!run) {
247
+ console.log(` ${DIM}No active runs to stop.${RESET}\n`);
248
+ return;
249
+ }
250
+ runId = run.runId;
251
+ }
252
+
253
+ if (run.state === STATES.COMPLETED || run.state === STATES.FAILED) {
254
+ console.log(` ${DIM}Run ${runId} is already ${run.state}.${RESET}\n`);
255
+ return;
256
+ }
257
+
258
+ // Transition to paused or failed
259
+ if (options.force) {
260
+ run.transition(STATES.FAILED, { reason: 'Forcefully stopped by user' });
261
+ console.log(` ${RED}✗${RESET} Run ${runId} forcefully stopped.\n`);
262
+ } else {
263
+ run.transition(STATES.PAUSED, { reason: 'Stopped by user' });
264
+ console.log(` ${YELLOW}!${RESET} Run ${runId} paused.`);
265
+ console.log(` ${DIM}Use "gbos resume" to continue.${RESET}\n`);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * gbos runs - List recent runs
271
+ */
272
+ async function runsCommand(options) {
273
+ const termWidth = getTerminalWidth();
274
+ const tableWidth = Math.min(100, termWidth - 4);
275
+
276
+ console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
277
+ console.log(`${BOLD} Orchestrator Runs${RESET}`);
278
+ console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
279
+
280
+ StateMachine.ensureRunsDir();
281
+
282
+ const files = fs.readdirSync(RUNS_DIR)
283
+ .filter(f => f.endsWith('.json'))
284
+ .sort()
285
+ .reverse()
286
+ .slice(0, options.limit ? parseInt(options.limit) : 10);
287
+
288
+ if (files.length === 0) {
289
+ console.log(` ${DIM}No runs found.${RESET}`);
290
+ console.log(` ${DIM}Use "gbos start" to begin a new run.${RESET}\n`);
291
+ return;
292
+ }
293
+
294
+ for (const file of files) {
295
+ try {
296
+ const runId = file.replace('.json', '');
297
+ const run = StateMachine.loadRun(runId);
298
+ const summary = run.getSummary();
299
+
300
+ const stateColors = {
301
+ [STATES.COMPLETED]: GREEN,
302
+ [STATES.FAILED]: RED,
303
+ [STATES.PAUSED]: YELLOW,
304
+ };
305
+ const stateColor = stateColors[summary.state] || CYAN;
306
+
307
+ console.log(` ${stateColor}●${RESET} ${BOLD}${summary.runId}${RESET}`);
308
+ console.log(` ${DIM}State: ${summary.state} | Agent: ${summary.agent}${RESET}`);
309
+ if (summary.taskKey) {
310
+ console.log(` ${DIM}Task: ${summary.taskKey}${RESET}`);
311
+ }
312
+ if (summary.startTime) {
313
+ console.log(` ${DIM}Started: ${new Date(summary.startTime).toLocaleString()}${RESET}`);
314
+ }
315
+ console.log('');
316
+ } catch (e) {
317
+ // Skip invalid files
318
+ }
319
+ }
320
+
321
+ console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
322
+ }
323
+
324
+ module.exports = {
325
+ startCommand,
326
+ resumeCommand,
327
+ stopCommand,
328
+ runsCommand,
329
+ };
package/src/lib/api.js CHANGED
@@ -1,6 +1,11 @@
1
1
  const config = require('./config');
2
2
 
3
- const API_BASE_URL = 'https://gbos-api-579767694933.us-south1.run.app/api/v1';
3
+ // Default API endpoint (new domain)
4
+ const DEFAULT_API_URL = 'https://api.gbos.io/api/v1';
5
+
6
+ // Support GBOS_API_URL env var for backwards compatibility or custom endpoints
7
+ // e.g., GBOS_API_URL=https://gbos-api-579767694933.us-south1.run.app/api/v1
8
+ const API_BASE_URL = process.env.GBOS_API_URL || DEFAULT_API_URL;
4
9
 
5
10
  class GbosApiClient {
6
11
  constructor() {
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Base Agent Adapter
3
+ * Defines the interface for all agent adapters
4
+ */
5
+
6
+ const { EventEmitter } = require('events');
7
+
8
+ class BaseAdapter extends EventEmitter {
9
+ constructor(config = {}) {
10
+ super();
11
+ this.name = 'base';
12
+ this.config = config;
13
+ this.process = null;
14
+ this.isRunning = false;
15
+ this.supportsNonInteractive = false;
16
+ this.supportsInteractive = true;
17
+ }
18
+
19
+ /**
20
+ * Check if the agent CLI is available
21
+ * @returns {Promise<boolean>}
22
+ */
23
+ async isAvailable() {
24
+ throw new Error('isAvailable() must be implemented');
25
+ }
26
+
27
+ /**
28
+ * Get the agent version
29
+ * @returns {Promise<string>}
30
+ */
31
+ async getVersion() {
32
+ throw new Error('getVersion() must be implemented');
33
+ }
34
+
35
+ /**
36
+ * Generate the command to run the agent
37
+ * @param {Object} options - Run options
38
+ * @returns {Object} { command, args, env }
39
+ */
40
+ getCommand(options = {}) {
41
+ throw new Error('getCommand() must be implemented');
42
+ }
43
+
44
+ /**
45
+ * Format a prompt for this specific agent
46
+ * @param {Object} task - Task object from GBOS API
47
+ * @param {Object} context - Additional context (repo, cloudRunUrl, etc.)
48
+ * @returns {string} Formatted prompt
49
+ */
50
+ formatPrompt(task, context = {}) {
51
+ throw new Error('formatPrompt() must be implemented');
52
+ }
53
+
54
+ /**
55
+ * Detect if the agent has completed its work
56
+ * Used for interactive mode
57
+ * @param {string} output - Recent output from the agent
58
+ * @returns {boolean}
59
+ */
60
+ detectCompletion(output) {
61
+ // Default: look for common completion patterns
62
+ const completionPatterns = [
63
+ /task.*completed?/i,
64
+ /done.*with.*changes/i,
65
+ /all.*changes.*committed/i,
66
+ /finished.*working/i,
67
+ /ready.*for.*review/i,
68
+ ];
69
+ return completionPatterns.some(p => p.test(output));
70
+ }
71
+
72
+ /**
73
+ * Detect if the agent is waiting for input
74
+ * @param {string} output - Recent output
75
+ * @returns {boolean}
76
+ */
77
+ detectWaitingForInput(output) {
78
+ const waitPatterns = [
79
+ /\?.*$/m,
80
+ /\(y\/n\)/i,
81
+ /press.*enter/i,
82
+ /continue\?/i,
83
+ ];
84
+ return waitPatterns.some(p => p.test(output));
85
+ }
86
+
87
+ /**
88
+ * Detect if the agent encountered an error
89
+ * @param {string} output - Recent output
90
+ * @returns {boolean}
91
+ */
92
+ detectError(output) {
93
+ const errorPatterns = [
94
+ /error:/i,
95
+ /failed:/i,
96
+ /exception:/i,
97
+ /fatal:/i,
98
+ ];
99
+ return errorPatterns.some(p => p.test(output));
100
+ }
101
+
102
+ /**
103
+ * Parse the output to extract useful information
104
+ * @param {string} output - Full output from the agent
105
+ * @returns {Object} Parsed information
106
+ */
107
+ parseOutput(output) {
108
+ return {
109
+ raw: output,
110
+ filesModified: this.extractFilesModified(output),
111
+ testsRun: this.extractTestResults(output),
112
+ errors: this.extractErrors(output),
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Extract files modified from output
118
+ * @param {string} output
119
+ * @returns {string[]}
120
+ */
121
+ extractFilesModified(output) {
122
+ const files = new Set();
123
+ // Common patterns for file modifications
124
+ const patterns = [
125
+ /(?:created?|modified?|updated?|wrote?|edited?)\s+[`']?([^\s`']+\.[a-z]+)/gi,
126
+ /(?:file|path):\s*[`']?([^\s`']+\.[a-z]+)/gi,
127
+ ];
128
+ patterns.forEach(pattern => {
129
+ let match;
130
+ while ((match = pattern.exec(output)) !== null) {
131
+ files.add(match[1]);
132
+ }
133
+ });
134
+ return Array.from(files);
135
+ }
136
+
137
+ /**
138
+ * Extract test results from output
139
+ * @param {string} output
140
+ * @returns {Object|null}
141
+ */
142
+ extractTestResults(output) {
143
+ // Look for common test output patterns
144
+ const jestMatch = output.match(/Tests:\s*(\d+)\s*passed,?\s*(\d+)?\s*failed?/i);
145
+ if (jestMatch) {
146
+ return {
147
+ framework: 'jest',
148
+ passed: parseInt(jestMatch[1]) || 0,
149
+ failed: parseInt(jestMatch[2]) || 0,
150
+ };
151
+ }
152
+
153
+ const pytestMatch = output.match(/(\d+)\s*passed.*?(\d+)?\s*failed?/i);
154
+ if (pytestMatch) {
155
+ return {
156
+ framework: 'pytest',
157
+ passed: parseInt(pytestMatch[1]) || 0,
158
+ failed: parseInt(pytestMatch[2]) || 0,
159
+ };
160
+ }
161
+
162
+ return null;
163
+ }
164
+
165
+ /**
166
+ * Extract errors from output
167
+ * @param {string} output
168
+ * @returns {string[]}
169
+ */
170
+ extractErrors(output) {
171
+ const errors = [];
172
+ const lines = output.split('\n');
173
+ for (const line of lines) {
174
+ if (/error:|failed:|exception:|fatal:/i.test(line)) {
175
+ errors.push(line.trim());
176
+ }
177
+ }
178
+ return errors;
179
+ }
180
+
181
+ /**
182
+ * Stop the running agent
183
+ */
184
+ async stop() {
185
+ if (this.process) {
186
+ this.process.kill('SIGTERM');
187
+ this.isRunning = false;
188
+ this.emit('stopped');
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Force kill the agent
194
+ */
195
+ async kill() {
196
+ if (this.process) {
197
+ this.process.kill('SIGKILL');
198
+ this.isRunning = false;
199
+ this.emit('killed');
200
+ }
201
+ }
202
+ }
203
+
204
+ module.exports = BaseAdapter;