@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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +24 -0
- package/README.md +769 -0
- package/package.json +58 -0
- package/src/agent.lib.mjs +705 -0
- package/src/agent.prompts.lib.mjs +196 -0
- package/src/buildUserMention.lib.mjs +71 -0
- package/src/claude-limits.lib.mjs +389 -0
- package/src/claude.lib.mjs +1445 -0
- package/src/claude.prompts.lib.mjs +203 -0
- package/src/codex.lib.mjs +552 -0
- package/src/codex.prompts.lib.mjs +194 -0
- package/src/config.lib.mjs +207 -0
- package/src/contributing-guidelines.lib.mjs +268 -0
- package/src/exit-handler.lib.mjs +205 -0
- package/src/git.lib.mjs +145 -0
- package/src/github-issue-creator.lib.mjs +246 -0
- package/src/github-linking.lib.mjs +152 -0
- package/src/github.batch.lib.mjs +272 -0
- package/src/github.graphql.lib.mjs +258 -0
- package/src/github.lib.mjs +1479 -0
- package/src/hive.config.lib.mjs +254 -0
- package/src/hive.mjs +1500 -0
- package/src/instrument.mjs +191 -0
- package/src/interactive-mode.lib.mjs +1000 -0
- package/src/lenv-reader.lib.mjs +206 -0
- package/src/lib.mjs +490 -0
- package/src/lino.lib.mjs +176 -0
- package/src/local-ci-checks.lib.mjs +324 -0
- package/src/memory-check.mjs +419 -0
- package/src/model-mapping.lib.mjs +145 -0
- package/src/model-validation.lib.mjs +278 -0
- package/src/opencode.lib.mjs +479 -0
- package/src/opencode.prompts.lib.mjs +194 -0
- package/src/protect-branch.mjs +159 -0
- package/src/review.mjs +433 -0
- package/src/reviewers-hive.mjs +643 -0
- package/src/sentry.lib.mjs +284 -0
- package/src/solve.auto-continue.lib.mjs +568 -0
- package/src/solve.auto-pr.lib.mjs +1374 -0
- package/src/solve.branch-errors.lib.mjs +341 -0
- package/src/solve.branch.lib.mjs +230 -0
- package/src/solve.config.lib.mjs +342 -0
- package/src/solve.error-handlers.lib.mjs +256 -0
- package/src/solve.execution.lib.mjs +291 -0
- package/src/solve.feedback.lib.mjs +436 -0
- package/src/solve.mjs +1128 -0
- package/src/solve.preparation.lib.mjs +210 -0
- package/src/solve.repo-setup.lib.mjs +114 -0
- package/src/solve.repository.lib.mjs +961 -0
- package/src/solve.results.lib.mjs +558 -0
- package/src/solve.session.lib.mjs +135 -0
- package/src/solve.validation.lib.mjs +325 -0
- package/src/solve.watch.lib.mjs +572 -0
- package/src/start-screen.mjs +324 -0
- package/src/task.mjs +308 -0
- package/src/telegram-bot.mjs +1481 -0
- package/src/telegram-markdown.lib.mjs +64 -0
- package/src/usage-limit.lib.mjs +218 -0
- package/src/version.lib.mjs +41 -0
- package/src/youtrack/solve.youtrack.lib.mjs +116 -0
- package/src/youtrack/youtrack-sync.mjs +219 -0
- 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
|
+
};
|