@link-assistant/hive-mind 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +769 -0
  4. package/package.json +58 -0
  5. package/src/agent.lib.mjs +705 -0
  6. package/src/agent.prompts.lib.mjs +196 -0
  7. package/src/buildUserMention.lib.mjs +71 -0
  8. package/src/claude-limits.lib.mjs +389 -0
  9. package/src/claude.lib.mjs +1445 -0
  10. package/src/claude.prompts.lib.mjs +203 -0
  11. package/src/codex.lib.mjs +552 -0
  12. package/src/codex.prompts.lib.mjs +194 -0
  13. package/src/config.lib.mjs +207 -0
  14. package/src/contributing-guidelines.lib.mjs +268 -0
  15. package/src/exit-handler.lib.mjs +205 -0
  16. package/src/git.lib.mjs +145 -0
  17. package/src/github-issue-creator.lib.mjs +246 -0
  18. package/src/github-linking.lib.mjs +152 -0
  19. package/src/github.batch.lib.mjs +272 -0
  20. package/src/github.graphql.lib.mjs +258 -0
  21. package/src/github.lib.mjs +1479 -0
  22. package/src/hive.config.lib.mjs +254 -0
  23. package/src/hive.mjs +1500 -0
  24. package/src/instrument.mjs +191 -0
  25. package/src/interactive-mode.lib.mjs +1000 -0
  26. package/src/lenv-reader.lib.mjs +206 -0
  27. package/src/lib.mjs +490 -0
  28. package/src/lino.lib.mjs +176 -0
  29. package/src/local-ci-checks.lib.mjs +324 -0
  30. package/src/memory-check.mjs +419 -0
  31. package/src/model-mapping.lib.mjs +145 -0
  32. package/src/model-validation.lib.mjs +278 -0
  33. package/src/opencode.lib.mjs +479 -0
  34. package/src/opencode.prompts.lib.mjs +194 -0
  35. package/src/protect-branch.mjs +159 -0
  36. package/src/review.mjs +433 -0
  37. package/src/reviewers-hive.mjs +643 -0
  38. package/src/sentry.lib.mjs +284 -0
  39. package/src/solve.auto-continue.lib.mjs +568 -0
  40. package/src/solve.auto-pr.lib.mjs +1374 -0
  41. package/src/solve.branch-errors.lib.mjs +341 -0
  42. package/src/solve.branch.lib.mjs +230 -0
  43. package/src/solve.config.lib.mjs +342 -0
  44. package/src/solve.error-handlers.lib.mjs +256 -0
  45. package/src/solve.execution.lib.mjs +291 -0
  46. package/src/solve.feedback.lib.mjs +436 -0
  47. package/src/solve.mjs +1128 -0
  48. package/src/solve.preparation.lib.mjs +210 -0
  49. package/src/solve.repo-setup.lib.mjs +114 -0
  50. package/src/solve.repository.lib.mjs +961 -0
  51. package/src/solve.results.lib.mjs +558 -0
  52. package/src/solve.session.lib.mjs +135 -0
  53. package/src/solve.validation.lib.mjs +325 -0
  54. package/src/solve.watch.lib.mjs +572 -0
  55. package/src/start-screen.mjs +324 -0
  56. package/src/task.mjs +308 -0
  57. package/src/telegram-bot.mjs +1481 -0
  58. package/src/telegram-markdown.lib.mjs +64 -0
  59. package/src/usage-limit.lib.mjs +218 -0
  60. package/src/version.lib.mjs +41 -0
  61. package/src/youtrack/solve.youtrack.lib.mjs +116 -0
  62. package/src/youtrack/youtrack-sync.mjs +219 -0
  63. package/src/youtrack/youtrack.lib.mjs +425 -0
@@ -0,0 +1,705 @@
1
+ #!/usr/bin/env node
2
+ // Agent-related utility functions
3
+
4
+ // Check if use is already defined (when imported from solve.mjs)
5
+ // If not, fetch it (when running standalone)
6
+ if (typeof globalThis.use === 'undefined') {
7
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
8
+ }
9
+
10
+ const { $ } = await use('command-stream');
11
+ const fs = (await use('fs')).promises;
12
+ const path = (await use('path')).default;
13
+ const os = (await use('os')).default;
14
+
15
+ // Import log from general lib
16
+ import { log } from './lib.mjs';
17
+ import { reportError } from './sentry.lib.mjs';
18
+ import { timeouts } from './config.lib.mjs';
19
+ import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
20
+
21
+ // Import pricing functions from claude.lib.mjs
22
+ // We reuse fetchModelInfo to get pricing data from models.dev API
23
+ const claudeLib = await import('./claude.lib.mjs');
24
+ const { fetchModelInfo } = claudeLib;
25
+
26
+ /**
27
+ * Parse agent JSON output to extract token usage from step_finish events
28
+ * Agent outputs NDJSON (newline-delimited JSON) with step_finish events containing token data
29
+ * @param {string} output - Raw stdout output from agent command
30
+ * @returns {Object} Aggregated token usage and cost data
31
+ */
32
+ export const parseAgentTokenUsage = (output) => {
33
+ const usage = {
34
+ inputTokens: 0,
35
+ outputTokens: 0,
36
+ reasoningTokens: 0,
37
+ cacheReadTokens: 0,
38
+ cacheWriteTokens: 0,
39
+ totalCost: 0,
40
+ stepCount: 0
41
+ };
42
+
43
+ // Try to parse each line as JSON (agent outputs NDJSON format)
44
+ const lines = output.split('\n');
45
+ for (const line of lines) {
46
+ const trimmedLine = line.trim();
47
+ if (!trimmedLine || !trimmedLine.startsWith('{')) continue;
48
+
49
+ try {
50
+ const parsed = JSON.parse(trimmedLine);
51
+
52
+ // Look for step_finish events which contain token usage
53
+ if (parsed.type === 'step_finish' && parsed.part?.tokens) {
54
+ const tokens = parsed.part.tokens;
55
+ usage.stepCount++;
56
+
57
+ // Add token counts
58
+ if (tokens.input) usage.inputTokens += tokens.input;
59
+ if (tokens.output) usage.outputTokens += tokens.output;
60
+ if (tokens.reasoning) usage.reasoningTokens += tokens.reasoning;
61
+
62
+ // Handle cache tokens (can be in different formats)
63
+ if (tokens.cache) {
64
+ if (tokens.cache.read) usage.cacheReadTokens += tokens.cache.read;
65
+ if (tokens.cache.write) usage.cacheWriteTokens += tokens.cache.write;
66
+ }
67
+
68
+ // Add cost from step_finish (usually 0 for free models like grok-code)
69
+ if (parsed.part.cost !== undefined) {
70
+ usage.totalCost += parsed.part.cost;
71
+ }
72
+ }
73
+ } catch {
74
+ // Skip lines that aren't valid JSON
75
+ continue;
76
+ }
77
+ }
78
+
79
+ return usage;
80
+ };
81
+
82
+ /**
83
+ * Calculate pricing for agent tool usage using models.dev API
84
+ * @param {string} modelId - The model ID used (e.g., 'opencode/grok-code')
85
+ * @param {Object} tokenUsage - Token usage data from parseAgentTokenUsage
86
+ * @returns {Object} Pricing information
87
+ */
88
+ export const calculateAgentPricing = async (modelId, tokenUsage) => {
89
+ // Extract the model name from provider/model format
90
+ // e.g., 'opencode/grok-code' -> 'grok-code'
91
+ const modelName = modelId.includes('/') ? modelId.split('/').pop() : modelId;
92
+
93
+ try {
94
+ // Fetch model info from models.dev API
95
+ const modelInfo = await fetchModelInfo(modelName);
96
+
97
+ if (modelInfo && modelInfo.cost) {
98
+ const cost = modelInfo.cost;
99
+
100
+ // Calculate cost based on token usage
101
+ // Prices are per 1M tokens, so divide by 1,000,000
102
+ const inputCost = (tokenUsage.inputTokens * (cost.input || 0)) / 1_000_000;
103
+ const outputCost = (tokenUsage.outputTokens * (cost.output || 0)) / 1_000_000;
104
+ const cacheReadCost = (tokenUsage.cacheReadTokens * (cost.cache_read || 0)) / 1_000_000;
105
+ const cacheWriteCost = (tokenUsage.cacheWriteTokens * (cost.cache_write || 0)) / 1_000_000;
106
+
107
+ const totalCost = inputCost + outputCost + cacheReadCost + cacheWriteCost;
108
+
109
+ return {
110
+ modelId,
111
+ modelName: modelInfo.name || modelName,
112
+ provider: modelInfo.provider || 'OpenCode Zen',
113
+ pricing: {
114
+ inputPerMillion: cost.input || 0,
115
+ outputPerMillion: cost.output || 0,
116
+ cacheReadPerMillion: cost.cache_read || 0,
117
+ cacheWritePerMillion: cost.cache_write || 0
118
+ },
119
+ tokenUsage,
120
+ breakdown: {
121
+ input: inputCost,
122
+ output: outputCost,
123
+ cacheRead: cacheReadCost,
124
+ cacheWrite: cacheWriteCost
125
+ },
126
+ totalCostUSD: totalCost,
127
+ isFreeModel: cost.input === 0 && cost.output === 0
128
+ };
129
+ }
130
+
131
+ // Model not found in API, return what we have
132
+ return {
133
+ modelId,
134
+ modelName,
135
+ provider: 'Unknown',
136
+ tokenUsage,
137
+ totalCostUSD: null,
138
+ error: 'Model not found in models.dev API'
139
+ };
140
+ } catch (error) {
141
+ // Error fetching pricing, return with error info
142
+ return {
143
+ modelId,
144
+ modelName,
145
+ tokenUsage,
146
+ totalCostUSD: null,
147
+ error: error.message
148
+ };
149
+ }
150
+ };
151
+
152
+ // Model mapping to translate aliases to full model IDs for Agent
153
+ // Agent uses OpenCode's JSON interface and models
154
+ export const mapModelToId = (model) => {
155
+ const modelMap = {
156
+ 'grok': 'opencode/grok-code',
157
+ 'grok-code': 'opencode/grok-code',
158
+ 'grok-code-fast-1': 'opencode/grok-code',
159
+ 'big-pickle': 'opencode/big-pickle',
160
+ 'gpt-5-nano': 'openai/gpt-5-nano',
161
+ 'sonnet': 'anthropic/claude-3-5-sonnet',
162
+ 'haiku': 'anthropic/claude-3-5-haiku',
163
+ 'opus': 'anthropic/claude-3-opus',
164
+ 'gemini-3-pro': 'google/gemini-3-pro',
165
+ };
166
+
167
+ // Return mapped model ID if it's an alias, otherwise return as-is
168
+ return modelMap[model] || model;
169
+ };
170
+
171
+ // Function to validate Agent connection
172
+ export const validateAgentConnection = async (model = 'grok-code-fast-1') => {
173
+ // Map model alias to full ID
174
+ const mappedModel = mapModelToId(model);
175
+
176
+ // Retry configuration
177
+ const maxRetries = 3;
178
+ let retryCount = 0;
179
+
180
+ const attemptValidation = async () => {
181
+ try {
182
+ if (retryCount === 0) {
183
+ await log('šŸ” Validating Agent connection...');
184
+ } else {
185
+ await log(`šŸ”„ Retry attempt ${retryCount}/${maxRetries} for Agent validation...`);
186
+ }
187
+
188
+ // Check if Agent CLI is installed and get version
189
+ try {
190
+ const versionResult = await $`timeout ${Math.floor(timeouts.opencodeCli / 1000)} agent --version`;
191
+ if (versionResult.code === 0) {
192
+ const version = versionResult.stdout?.toString().trim();
193
+ if (retryCount === 0) {
194
+ await log(`šŸ“¦ Agent CLI version: ${version}`);
195
+ }
196
+ }
197
+ } catch (versionError) {
198
+ if (retryCount === 0) {
199
+ await log(`āš ļø Agent CLI version check failed (${versionError.code}), proceeding with connection test...`);
200
+ }
201
+ }
202
+
203
+ // Test basic Agent functionality with a simple "hi" message
204
+ // Agent uses the same JSON interface as OpenCode
205
+ const testResult = await $`printf "hi" | timeout ${Math.floor(timeouts.opencodeCli / 1000)} agent --model ${mappedModel}`;
206
+
207
+ if (testResult.code !== 0) {
208
+ const stderr = testResult.stderr?.toString() || '';
209
+
210
+ if (stderr.includes('auth') || stderr.includes('login')) {
211
+ await log('āŒ Agent authentication failed', { level: 'error' });
212
+ await log(' šŸ’” Note: Agent uses OpenCode models. For premium models, you may need: opencode auth', { level: 'error' });
213
+ return false;
214
+ }
215
+
216
+ await log(`āŒ Agent validation failed with exit code ${testResult.code}`, { level: 'error' });
217
+ if (stderr) await log(` Error: ${stderr.trim()}`, { level: 'error' });
218
+ return false;
219
+ }
220
+
221
+ // Success
222
+ await log('āœ… Agent connection validated successfully');
223
+ return true;
224
+ } catch (error) {
225
+ await log(`āŒ Failed to validate Agent connection: ${error.message}`, { level: 'error' });
226
+ await log(' šŸ’” Make sure @link-assistant/agent is installed globally: bun install -g @link-assistant/agent', { level: 'error' });
227
+ return false;
228
+ }
229
+ };
230
+
231
+ // Start the validation
232
+ return await attemptValidation();
233
+ };
234
+
235
+ // Function to handle Agent runtime switching (if applicable)
236
+ export const handleAgentRuntimeSwitch = async () => {
237
+ // Agent is run via Bun as a CLI tool, runtime switching may not be applicable
238
+ // This function can be used for any runtime-specific configurations if needed
239
+ await log('ā„¹ļø Agent runtime handling not required for this operation');
240
+ };
241
+
242
+ // Main function to execute Agent with prompts and settings
243
+ export const executeAgent = async (params) => {
244
+ const {
245
+ issueUrl,
246
+ issueNumber,
247
+ prNumber,
248
+ prUrl,
249
+ branchName,
250
+ tempDir,
251
+ isContinueMode,
252
+ mergeStateStatus,
253
+ forkedRepo,
254
+ feedbackLines,
255
+ forkActionsUrl,
256
+ owner,
257
+ repo,
258
+ argv,
259
+ log,
260
+ formatAligned,
261
+ getResourceSnapshot,
262
+ agentPath = 'agent',
263
+ $
264
+ } = params;
265
+
266
+ // Import prompt building functions from agent.prompts.lib.mjs
267
+ const { buildUserPrompt, buildSystemPrompt } = await import('./agent.prompts.lib.mjs');
268
+
269
+ // Build the user prompt
270
+ const prompt = buildUserPrompt({
271
+ issueUrl,
272
+ issueNumber,
273
+ prNumber,
274
+ prUrl,
275
+ branchName,
276
+ tempDir,
277
+ isContinueMode,
278
+ mergeStateStatus,
279
+ forkedRepo,
280
+ feedbackLines,
281
+ forkActionsUrl,
282
+ owner,
283
+ repo,
284
+ argv
285
+ });
286
+
287
+ // Build the system prompt
288
+ const systemPrompt = buildSystemPrompt({
289
+ owner,
290
+ repo,
291
+ issueNumber,
292
+ prNumber,
293
+ branchName,
294
+ tempDir,
295
+ isContinueMode,
296
+ forkedRepo,
297
+ argv
298
+ });
299
+
300
+ // Log prompt details in verbose mode
301
+ if (argv.verbose) {
302
+ await log('\nšŸ“ Final prompt structure:', { verbose: true });
303
+ await log(` Characters: ${prompt.length}`, { verbose: true });
304
+ await log(` System prompt characters: ${systemPrompt.length}`, { verbose: true });
305
+ if (feedbackLines && feedbackLines.length > 0) {
306
+ await log(' Feedback info: Included', { verbose: true });
307
+ }
308
+
309
+ if (argv.dryRun) {
310
+ await log('\nšŸ“‹ User prompt content:', { verbose: true });
311
+ await log('---BEGIN USER PROMPT---', { verbose: true });
312
+ await log(prompt, { verbose: true });
313
+ await log('---END USER PROMPT---', { verbose: true });
314
+ await log('\nšŸ“‹ System prompt content:', { verbose: true });
315
+ await log('---BEGIN SYSTEM PROMPT---', { verbose: true });
316
+ await log(systemPrompt, { verbose: true });
317
+ await log('---END SYSTEM PROMPT---', { verbose: true });
318
+ }
319
+ }
320
+
321
+ // Execute the Agent command
322
+ return await executeAgentCommand({
323
+ tempDir,
324
+ branchName,
325
+ prompt,
326
+ systemPrompt,
327
+ argv,
328
+ log,
329
+ formatAligned,
330
+ getResourceSnapshot,
331
+ forkedRepo,
332
+ feedbackLines,
333
+ agentPath,
334
+ $
335
+ });
336
+ };
337
+
338
+ export const executeAgentCommand = async (params) => {
339
+ const {
340
+ tempDir,
341
+ branchName,
342
+ prompt,
343
+ systemPrompt,
344
+ argv,
345
+ log,
346
+ formatAligned,
347
+ getResourceSnapshot,
348
+ forkedRepo,
349
+ feedbackLines,
350
+ agentPath,
351
+ $
352
+ } = params;
353
+
354
+ // Retry configuration
355
+ const maxRetries = 3;
356
+ let retryCount = 0;
357
+
358
+ const executeWithRetry = async () => {
359
+ // Execute agent command from the cloned repository directory
360
+ if (retryCount === 0) {
361
+ await log(`\n${formatAligned('šŸ¤–', 'Executing Agent:', argv.model.toUpperCase())}`);
362
+ } else {
363
+ await log(`\n${formatAligned('šŸ”„', 'Retry attempt:', `${retryCount}/${maxRetries}`)}`);
364
+ }
365
+
366
+ if (argv.verbose) {
367
+ await log(` Model: ${argv.model}`, { verbose: true });
368
+ await log(` Working directory: ${tempDir}`, { verbose: true });
369
+ await log(` Branch: ${branchName}`, { verbose: true });
370
+ await log(` Prompt length: ${prompt.length} chars`, { verbose: true });
371
+ await log(` System prompt length: ${systemPrompt.length} chars`, { verbose: true });
372
+ if (feedbackLines && feedbackLines.length > 0) {
373
+ await log(` Feedback info included: Yes (${feedbackLines.length} lines)`, { verbose: true });
374
+ } else {
375
+ await log(' Feedback info included: No', { verbose: true });
376
+ }
377
+ }
378
+
379
+ // Take resource snapshot before execution
380
+ const resourcesBefore = await getResourceSnapshot();
381
+ await log('šŸ“ˆ System resources before execution:', { verbose: true });
382
+ await log(` Memory: ${resourcesBefore.memory.split('\n')[1]}`, { verbose: true });
383
+ await log(` Load: ${resourcesBefore.load}`, { verbose: true });
384
+
385
+ // Build Agent command
386
+ let execCommand;
387
+
388
+ // Map model alias to full ID
389
+ const mappedModel = mapModelToId(argv.model);
390
+
391
+ // Build agent command arguments
392
+ let agentArgs = `--model ${mappedModel}`;
393
+
394
+ // Agent supports stdin in both plain text and JSON format
395
+ // We'll combine system and user prompts into a single message
396
+ const combinedPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
397
+
398
+ // Write the combined prompt to a file for piping
399
+ // Use OS temporary directory instead of repository workspace to avoid polluting the repo
400
+ const promptFile = path.join(os.tmpdir(), `agent_prompt_${Date.now()}_${process.pid}.txt`);
401
+ await fs.writeFile(promptFile, combinedPrompt);
402
+
403
+ // Build the full command - pipe the prompt file to agent
404
+ const fullCommand = `(cd "${tempDir}" && cat "${promptFile}" | ${agentPath} ${agentArgs})`;
405
+
406
+ await log(`\n${formatAligned('šŸ“', 'Raw command:', '')}`);
407
+ await log(`${fullCommand}`);
408
+ await log('');
409
+
410
+ try {
411
+ // Pipe the prompt file to agent via stdin
412
+ execCommand = $({
413
+ cwd: tempDir,
414
+ mirror: false
415
+ })`cat ${promptFile} | ${agentPath} --model ${mappedModel}`;
416
+
417
+ await log(`${formatAligned('šŸ“‹', 'Command details:', '')}`);
418
+ await log(formatAligned('šŸ“‚', 'Working directory:', tempDir, 2));
419
+ await log(formatAligned('🌿', 'Branch:', branchName, 2));
420
+ await log(formatAligned('šŸ¤–', 'Model:', `Agent ${argv.model.toUpperCase()}`, 2));
421
+ if (argv.fork && forkedRepo) {
422
+ await log(formatAligned('šŸ“', 'Fork:', forkedRepo, 2));
423
+ }
424
+
425
+ await log(`\n${formatAligned('ā–¶ļø', 'Streaming output:', '')}\n`);
426
+
427
+ let exitCode = 0;
428
+ let sessionId = null;
429
+ let limitReached = false;
430
+ let limitResetTime = null;
431
+ let lastMessage = '';
432
+ let fullOutput = ''; // Collect all output for pricing calculation and error detection
433
+
434
+ for await (const chunk of execCommand.stream()) {
435
+ if (chunk.type === 'stdout') {
436
+ const output = chunk.data.toString();
437
+ await log(output);
438
+ lastMessage = output;
439
+ fullOutput += output; // Collect for both pricing calculation and error detection
440
+ }
441
+
442
+ if (chunk.type === 'stderr') {
443
+ const errorOutput = chunk.data.toString();
444
+ if (errorOutput) {
445
+ await log(errorOutput, { stream: 'stderr' });
446
+ }
447
+ } else if (chunk.type === 'exit') {
448
+ exitCode = chunk.code;
449
+ }
450
+ }
451
+
452
+ // Simplified error detection for agent tool
453
+ // Issue #886: Trust exit code - agent now properly returns code 1 on errors with JSON error response
454
+ // Don't scan output for error patterns as this causes false positives during normal operation
455
+ // (e.g., AI executing bash commands that produce "Permission denied" warnings but succeed)
456
+ //
457
+ // Error detection is now based on:
458
+ // 1. Non-zero exit code (agent returns 1 on errors)
459
+ // 2. Explicit JSON error messages from agent (type: "error")
460
+ // 3. Usage limit detection (handled separately)
461
+ const detectAgentErrors = (stdoutOutput) => {
462
+ const lines = stdoutOutput.split('\n');
463
+
464
+ for (const line of lines) {
465
+ if (!line.trim()) continue;
466
+
467
+ try {
468
+ const msg = JSON.parse(line);
469
+
470
+ // Check for explicit error message types from agent
471
+ if (msg.type === 'error' || msg.type === 'step_error') {
472
+ return { detected: true, type: 'AgentError', match: msg.message || line.substring(0, 100) };
473
+ }
474
+ } catch {
475
+ // Not JSON - ignore for error detection
476
+ continue;
477
+ }
478
+ }
479
+
480
+ return { detected: false };
481
+ };
482
+
483
+ // Only check for JSON error messages, not pattern matching in output
484
+ const outputError = detectAgentErrors(fullOutput);
485
+
486
+ if (exitCode !== 0 || outputError.detected) {
487
+ // Build JSON error structure for consistent error reporting
488
+ const errorInfo = {
489
+ type: 'error',
490
+ exitCode,
491
+ errorDetectedInOutput: outputError.detected,
492
+ errorType: outputError.detected ? outputError.type : (exitCode !== 0 ? 'NonZeroExitCode' : null),
493
+ errorMatch: outputError.detected ? outputError.match : null,
494
+ message: null,
495
+ sessionId,
496
+ limitReached: false,
497
+ limitResetTime: null
498
+ };
499
+
500
+ // Check for usage limit errors first (more specific)
501
+ const limitInfo = detectUsageLimit(lastMessage);
502
+ if (limitInfo.isUsageLimit) {
503
+ limitReached = true;
504
+ limitResetTime = limitInfo.resetTime;
505
+ errorInfo.limitReached = true;
506
+ errorInfo.limitResetTime = limitResetTime;
507
+ errorInfo.errorType = 'UsageLimit';
508
+
509
+ // Format and display user-friendly message
510
+ const messageLines = formatUsageLimitMessage({
511
+ tool: 'Agent',
512
+ resetTime: limitInfo.resetTime,
513
+ sessionId,
514
+ resumeCommand: sessionId ? `${process.argv[0]} ${process.argv[1]} ${argv.url} --resume ${sessionId}` : null
515
+ });
516
+
517
+ for (const line of messageLines) {
518
+ await log(line, { level: 'warning' });
519
+ }
520
+ } else if (outputError.detected) {
521
+ // Explicit JSON error message from agent
522
+ errorInfo.message = `Agent reported error: ${outputError.match}`;
523
+ await log(`\n\nāŒ ${errorInfo.message}`, { level: 'error' });
524
+ } else {
525
+ errorInfo.message = `Agent command failed with exit code ${exitCode}`;
526
+ await log(`\n\nāŒ ${errorInfo.message}`, { level: 'error' });
527
+ }
528
+
529
+ // Log error as JSON for structured output (since agent expects JSON input/output)
530
+ await log('\nšŸ“‹ Error details (JSON):', { level: 'error' });
531
+ await log(JSON.stringify(errorInfo, null, 2), { level: 'error' });
532
+
533
+ const resourcesAfter = await getResourceSnapshot();
534
+ await log('\nšŸ“ˆ System resources after execution:', { verbose: true });
535
+ await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
536
+ await log(` Load: ${resourcesAfter.load}`, { verbose: true });
537
+
538
+ // Parse token usage even on failure (partial work may have been done)
539
+ const tokenUsage = parseAgentTokenUsage(fullOutput);
540
+ const pricingInfo = await calculateAgentPricing(mappedModel, tokenUsage);
541
+
542
+ return {
543
+ success: false,
544
+ sessionId,
545
+ limitReached,
546
+ limitResetTime,
547
+ errorInfo, // Include structured error information
548
+ tokenUsage,
549
+ pricingInfo,
550
+ publicPricingEstimate: pricingInfo.totalCostUSD
551
+ };
552
+ }
553
+
554
+ await log('\n\nāœ… Agent command completed');
555
+
556
+ // Parse token usage from collected output
557
+ const tokenUsage = parseAgentTokenUsage(fullOutput);
558
+ const pricingInfo = await calculateAgentPricing(mappedModel, tokenUsage);
559
+
560
+ // Log pricing information
561
+ if (tokenUsage.stepCount > 0) {
562
+ await log('\nšŸ’° Token Usage Summary:');
563
+ await log(` šŸ“Š ${pricingInfo.modelName || mappedModel}:`);
564
+ await log(` Input tokens: ${tokenUsage.inputTokens.toLocaleString()}`);
565
+ await log(` Output tokens: ${tokenUsage.outputTokens.toLocaleString()}`);
566
+ if (tokenUsage.reasoningTokens > 0) {
567
+ await log(` Reasoning tokens: ${tokenUsage.reasoningTokens.toLocaleString()}`);
568
+ }
569
+ if (tokenUsage.cacheReadTokens > 0 || tokenUsage.cacheWriteTokens > 0) {
570
+ await log(` Cache read: ${tokenUsage.cacheReadTokens.toLocaleString()}`);
571
+ await log(` Cache write: ${tokenUsage.cacheWriteTokens.toLocaleString()}`);
572
+ }
573
+
574
+ if (pricingInfo.totalCostUSD !== null) {
575
+ if (pricingInfo.isFreeModel) {
576
+ await log(' Cost: $0.00 (Free model)');
577
+ } else {
578
+ await log(` Cost: $${pricingInfo.totalCostUSD.toFixed(6)}`);
579
+ }
580
+ await log(` Provider: ${pricingInfo.provider || 'OpenCode Zen'}`);
581
+ } else {
582
+ await log(' Cost: Not available (could not fetch pricing)');
583
+ }
584
+ }
585
+
586
+ return {
587
+ success: true,
588
+ sessionId,
589
+ limitReached,
590
+ limitResetTime,
591
+ tokenUsage,
592
+ pricingInfo,
593
+ publicPricingEstimate: pricingInfo.totalCostUSD
594
+ };
595
+ } catch (error) {
596
+ reportError(error, {
597
+ context: 'execute_agent',
598
+ command: params.command,
599
+ agentPath: params.agentPath,
600
+ operation: 'run_agent_command'
601
+ });
602
+
603
+ await log(`\n\nāŒ Error executing Agent command: ${error.message}`, { level: 'error' });
604
+ return {
605
+ success: false,
606
+ sessionId: null,
607
+ limitReached: false,
608
+ limitResetTime: null,
609
+ tokenUsage: null,
610
+ pricingInfo: null,
611
+ publicPricingEstimate: null
612
+ };
613
+ }
614
+ };
615
+
616
+ // Start the execution with retry logic
617
+ return await executeWithRetry();
618
+ };
619
+
620
+ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchName, $, log, autoCommit = false, autoRestartEnabled = true) => {
621
+ // Similar to OpenCode version, check for uncommitted changes
622
+ await log('\nšŸ” Checking for uncommitted changes...');
623
+ try {
624
+ const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
625
+
626
+ if (gitStatusResult.code === 0) {
627
+ const statusOutput = gitStatusResult.stdout.toString().trim();
628
+
629
+ if (statusOutput) {
630
+ await log('šŸ“ Found uncommitted changes');
631
+ await log('Changes:');
632
+ for (const line of statusOutput.split('\n')) {
633
+ await log(` ${line}`);
634
+ }
635
+
636
+ if (autoCommit) {
637
+ await log('šŸ’¾ Auto-committing changes (--auto-commit-uncommitted-changes is enabled)...');
638
+
639
+ const addResult = await $({ cwd: tempDir })`git add -A`;
640
+ if (addResult.code === 0) {
641
+ const commitMessage = 'Auto-commit: Changes made by Agent during problem-solving session';
642
+ const commitResult = await $({ cwd: tempDir })`git commit -m ${commitMessage}`;
643
+
644
+ if (commitResult.code === 0) {
645
+ await log('āœ… Changes committed successfully');
646
+
647
+ const pushResult = await $({ cwd: tempDir })`git push origin ${branchName}`;
648
+
649
+ if (pushResult.code === 0) {
650
+ await log('āœ… Changes pushed successfully');
651
+ } else {
652
+ await log(`āš ļø Warning: Could not push changes: ${pushResult.stderr?.toString().trim()}`, { level: 'warning' });
653
+ }
654
+ } else {
655
+ await log(`āš ļø Warning: Could not commit changes: ${commitResult.stderr?.toString().trim()}`, { level: 'warning' });
656
+ }
657
+ } else {
658
+ await log(`āš ļø Warning: Could not stage changes: ${addResult.stderr?.toString().trim()}`, { level: 'warning' });
659
+ }
660
+ return false;
661
+ } else if (autoRestartEnabled) {
662
+ await log('');
663
+ await log('āš ļø IMPORTANT: Uncommitted changes detected!');
664
+ await log(' Agent made changes that were not committed.');
665
+ await log('');
666
+ await log('šŸ”„ AUTO-RESTART: Restarting Agent to handle uncommitted changes...');
667
+ await log(' Agent will review the changes and decide what to commit.');
668
+ await log('');
669
+ return true;
670
+ } else {
671
+ await log('');
672
+ await log('āš ļø Uncommitted changes detected but auto-restart is disabled.');
673
+ await log(' Use --auto-restart-on-uncommitted-changes to enable or commit manually.');
674
+ await log('');
675
+ return false;
676
+ }
677
+ } else {
678
+ await log('āœ… No uncommitted changes found');
679
+ return false;
680
+ }
681
+ } else {
682
+ await log(`āš ļø Warning: Could not check git status: ${gitStatusResult.stderr?.toString().trim()}`, { level: 'warning' });
683
+ return false;
684
+ }
685
+ } catch (gitError) {
686
+ reportError(gitError, {
687
+ context: 'check_uncommitted_changes_agent',
688
+ tempDir,
689
+ operation: 'git_status_check'
690
+ });
691
+ await log(`āš ļø Warning: Error checking for uncommitted changes: ${gitError.message}`, { level: 'warning' });
692
+ return false;
693
+ }
694
+ };
695
+
696
+ // Export all functions as default object too
697
+ export default {
698
+ validateAgentConnection,
699
+ handleAgentRuntimeSwitch,
700
+ executeAgent,
701
+ executeAgentCommand,
702
+ checkForUncommittedChanges,
703
+ parseAgentTokenUsage,
704
+ calculateAgentPricing
705
+ };