@link-assistant/hive-mind 1.6.2 → 1.6.3

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.6.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix Anthropic cost extraction from JSON stream when session has error_during_execution
8
+ - Added anthropicTotalCostUSD to all failure return paths in executeClaudeCommand
9
+ - Changed cost capture logic to only extract from `subtype === 'success'` results
10
+ - This is explicit and reliable - error_during_execution results have zero cost
11
+ - Added case study documentation for issue #1104
12
+
13
+ Fixes #1104
14
+
15
+ Synchronize line count checks in CI/CD
16
+ - Add ESLint max-lines rule (1500 lines) to match CI workflow check
17
+ - Extract handleClaudeRuntimeSwitch to claude.runtime-switch.lib.mjs
18
+ - Reduce claude.lib.mjs from 1506 to 1354 lines
19
+ - Add case study documentation for issue #1141
20
+
21
+ Fixes #1141
22
+
3
23
  ## 1.6.2
4
24
 
5
25
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -8,7 +8,7 @@ const { $ } = await use('command-stream');
8
8
  const fs = (await use('fs')).promises;
9
9
  const path = (await use('path')).default;
10
10
  // Import log from general lib
11
- import { log, cleanErrorMessage } from './lib.mjs';
11
+ import { log } from './lib.mjs';
12
12
  import { reportError } from './sentry.lib.mjs';
13
13
  import { timeouts, retryLimits, claudeCode, getClaudeEnv } from './config.lib.mjs';
14
14
  import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
@@ -16,6 +16,8 @@ import { createInteractiveHandler } from './interactive-mode.lib.mjs';
16
16
  import { displayBudgetStats } from './claude.budget-stats.lib.mjs';
17
17
  // Import Claude command builder for generating resume commands
18
18
  import { buildClaudeResumeCommand } from './claude.command-builder.lib.mjs';
19
+ // Import runtime switch module (extracted to maintain file line limits, see issue #1141)
20
+ import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs';
19
21
 
20
22
  // Helper to display resume command at end of session
21
23
  const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
@@ -214,163 +216,9 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
214
216
  // Start the validation with retry logic
215
217
  return await attemptValidation();
216
218
  };
217
- // Function to handle Claude runtime switching between Node.js and Bun
218
- export const handleClaudeRuntimeSwitch = async argv => {
219
- if (argv['force-claude-bun-run']) {
220
- await log('\nšŸ”§ Switching Claude runtime to bun...');
221
- try {
222
- try {
223
- await $`which bun`;
224
- await log(' āœ… Bun runtime found');
225
- } catch (bunError) {
226
- reportError(bunError, {
227
- context: 'claude.lib.mjs - bun availability check',
228
- level: 'error',
229
- });
230
- await log('āŒ Bun runtime not found. Please install bun first: https://bun.sh/', { level: 'error' });
231
- process.exit(1);
232
- }
233
-
234
- // Find Claude executable path
235
- const claudePathResult = await $`which claude`;
236
- const claudePath = claudePathResult.stdout.toString().trim();
237
-
238
- if (!claudePath) {
239
- await log('āŒ Claude executable not found', { level: 'error' });
240
- process.exit(1);
241
- }
242
-
243
- await log(` Claude path: ${claudePath}`);
244
-
245
- try {
246
- await fs.access(claudePath, fs.constants.W_OK);
247
- } catch (accessError) {
248
- reportError(accessError, {
249
- context: 'claude.lib.mjs - Claude executable write permission check (bun)',
250
- level: 'error',
251
- });
252
- await log('āŒ Cannot write to Claude executable (permission denied)', { level: 'error' });
253
- await log(' Try running with sudo or changing file permissions', { level: 'error' });
254
- process.exit(1);
255
- }
256
- // Read current shebang
257
- const firstLine = await $`head -1 "${claudePath}"`;
258
- const currentShebang = firstLine.stdout.toString().trim();
259
- await log(` Current shebang: ${currentShebang}`);
260
- if (currentShebang.includes('bun')) {
261
- await log(' āœ… Claude is already configured to use bun');
262
- process.exit(0);
263
- }
264
-
265
- // Create backup
266
- const backupPath = `${claudePath}.nodejs-backup`;
267
- await $`cp "${claudePath}" "${backupPath}"`;
268
- await log(` šŸ“¦ Backup created: ${backupPath}`);
269
-
270
- // Read file content and replace shebang
271
- const content = await fs.readFile(claudePath, 'utf8');
272
- const newContent = content.replace(/^#!.*node.*$/m, '#!/usr/bin/env bun');
273
-
274
- if (content === newContent) {
275
- await log('āš ļø No Node.js shebang found to replace', { level: 'warning' });
276
- await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
277
- process.exit(0);
278
- }
279
-
280
- await fs.writeFile(claudePath, newContent);
281
- await log(' āœ… Claude shebang updated to use bun');
282
- await log(' šŸ”„ Claude will now run with bun runtime');
283
- } catch (error) {
284
- await log(`āŒ Failed to switch Claude to bun: ${cleanErrorMessage(error)}`, { level: 'error' });
285
- process.exit(1);
286
- }
287
-
288
- // Exit after switching runtime
289
- process.exit(0);
290
- }
291
-
292
- if (argv['force-claude-nodejs-run']) {
293
- await log('\nšŸ”§ Restoring Claude runtime to Node.js...');
294
- try {
295
- try {
296
- await $`which node`;
297
- await log(' āœ… Node.js runtime found');
298
- } catch (nodeError) {
299
- reportError(nodeError, {
300
- context: 'claude.lib.mjs - Node.js availability check',
301
- level: 'error',
302
- });
303
- await log('āŒ Node.js runtime not found. Please install Node.js first', { level: 'error' });
304
- process.exit(1);
305
- }
306
-
307
- // Find Claude executable path
308
- const claudePathResult = await $`which claude`;
309
- const claudePath = claudePathResult.stdout.toString().trim();
310
-
311
- if (!claudePath) {
312
- await log('āŒ Claude executable not found', { level: 'error' });
313
- process.exit(1);
314
- }
315
-
316
- await log(` Claude path: ${claudePath}`);
317
-
318
- try {
319
- await fs.access(claudePath, fs.constants.W_OK);
320
- } catch (accessError) {
321
- reportError(accessError, {
322
- context: 'claude.lib.mjs - Claude executable write permission check (nodejs)',
323
- level: 'error',
324
- });
325
- await log('āŒ Cannot write to Claude executable (permission denied)', { level: 'error' });
326
- await log(' Try running with sudo or changing file permissions', { level: 'error' });
327
- process.exit(1);
328
- }
329
- // Read current shebang
330
- const firstLine = await $`head -1 "${claudePath}"`;
331
- const currentShebang = firstLine.stdout.toString().trim();
332
- await log(` Current shebang: ${currentShebang}`);
333
- if (currentShebang.includes('node') && !currentShebang.includes('bun')) {
334
- await log(' āœ… Claude is already configured to use Node.js');
335
- process.exit(0);
336
- }
337
-
338
- const backupPath = `${claudePath}.nodejs-backup`;
339
- try {
340
- await fs.access(backupPath);
341
- // Restore from backup
342
- await $`cp "${backupPath}" "${claudePath}"`;
343
- await log(` āœ… Restored Claude from backup: ${backupPath}`);
344
- } catch (backupError) {
345
- reportError(backupError, {
346
- context: 'claude_restore_backup',
347
- level: 'info',
348
- });
349
- // No backup available, manually update shebang
350
- await log(' šŸ“ No backup found, manually updating shebang...');
351
- const content = await fs.readFile(claudePath, 'utf8');
352
- const newContent = content.replace(/^#!.*bun.*$/m, '#!/usr/bin/env node');
353
-
354
- if (content === newContent) {
355
- await log('āš ļø No bun shebang found to replace', { level: 'warning' });
356
- await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
357
- process.exit(0);
358
- }
359
-
360
- await fs.writeFile(claudePath, newContent);
361
- await log(' āœ… Claude shebang updated to use Node.js');
362
- }
363
-
364
- await log(' šŸ”„ Claude will now run with Node.js runtime');
365
- } catch (error) {
366
- await log(`āŒ Failed to restore Claude to Node.js: ${cleanErrorMessage(error)}`, { level: 'error' });
367
- process.exit(1);
368
- }
369
-
370
- // Exit after restoring runtime
371
- process.exit(0);
372
- }
373
- };
219
+ // handleClaudeRuntimeSwitch is imported from ./claude.runtime-switch.lib.mjs (see issue #1141)
220
+ // Re-export it for backwards compatibility
221
+ export { handleClaudeRuntimeSwitch };
374
222
  /**
375
223
  * Check if Playwright MCP is available and connected to Claude
376
224
  * @returns {Promise<boolean>} True if Playwright MCP is available, false otherwise
@@ -1038,9 +886,13 @@ export const executeClaudeCommand = async params => {
1038
886
  // Handle session result type from Claude CLI (emitted when session completes)
1039
887
  // Subtypes: "success", "error_during_execution" (work may have been done), etc.
1040
888
  if (data.type === 'result') {
1041
- if (data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
889
+ // Issue #1104: Only extract cost from subtype 'success' results
890
+ // This is explicit and reliable - error_during_execution results have zero cost
891
+ if (data.subtype === 'success' && data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
1042
892
  anthropicTotalCostUSD = data.total_cost_usd;
1043
- await log(`šŸ’° Anthropic official cost captured: $${anthropicTotalCostUSD.toFixed(6)}`, { verbose: true });
893
+ await log(`šŸ’° Anthropic official cost captured from success result: $${anthropicTotalCostUSD.toFixed(6)}`, { verbose: true });
894
+ } else if (data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
895
+ await log(`šŸ’° Anthropic cost from ${data.subtype || 'unknown'} result ignored: $${data.total_cost_usd.toFixed(6)}`, { verbose: true });
1044
896
  }
1045
897
  if (data.is_error === true) {
1046
898
  lastMessage = data.result || JSON.stringify(data);
@@ -1170,6 +1022,7 @@ export const executeClaudeCommand = async params => {
1170
1022
  limitTimezone: null,
1171
1023
  messageCount,
1172
1024
  toolUseCount,
1025
+ anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1173
1026
  };
1174
1027
  }
1175
1028
  }
@@ -1218,6 +1071,7 @@ export const executeClaudeCommand = async params => {
1218
1071
  messageCount,
1219
1072
  toolUseCount,
1220
1073
  is503Error: true,
1074
+ anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1221
1075
  };
1222
1076
  }
1223
1077
  }
@@ -1296,6 +1150,7 @@ export const executeClaudeCommand = async params => {
1296
1150
  messageCount,
1297
1151
  toolUseCount,
1298
1152
  errorDuringExecution,
1153
+ anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1299
1154
  };
1300
1155
  }
1301
1156
  // Issue #1088: If error_during_execution occurred but command didn't fail,
@@ -1414,6 +1269,7 @@ export const executeClaudeCommand = async params => {
1414
1269
  limitTimezone: null,
1415
1270
  messageCount,
1416
1271
  toolUseCount,
1272
+ anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1417
1273
  };
1418
1274
  }
1419
1275
  }; // End of executeWithRetry function
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ // Claude runtime switching module
3
+ // Extracted from claude.lib.mjs to maintain file line limits
4
+ // See: docs/case-studies/issue-1141
5
+
6
+ // If not, fetch it (when running standalone)
7
+ if (typeof globalThis.use === 'undefined') {
8
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
9
+ }
10
+ const { $ } = await use('command-stream');
11
+ const fs = (await use('fs')).promises;
12
+ import { log, cleanErrorMessage } from './lib.mjs';
13
+ import { reportError } from './sentry.lib.mjs';
14
+
15
+ // Function to handle Claude runtime switching between Node.js and Bun
16
+ export const handleClaudeRuntimeSwitch = async argv => {
17
+ if (argv['force-claude-bun-run']) {
18
+ await log('\nšŸ”§ Switching Claude runtime to bun...');
19
+ try {
20
+ try {
21
+ await $`which bun`;
22
+ await log(' āœ… Bun runtime found');
23
+ } catch (bunError) {
24
+ reportError(bunError, {
25
+ context: 'claude.runtime-switch.lib.mjs - bun availability check',
26
+ level: 'error',
27
+ });
28
+ await log('āŒ Bun runtime not found. Please install bun first: https://bun.sh/', { level: 'error' });
29
+ process.exit(1);
30
+ }
31
+
32
+ // Find Claude executable path
33
+ const claudePathResult = await $`which claude`;
34
+ const claudePath = claudePathResult.stdout.toString().trim();
35
+
36
+ if (!claudePath) {
37
+ await log('āŒ Claude executable not found', { level: 'error' });
38
+ process.exit(1);
39
+ }
40
+
41
+ await log(` Claude path: ${claudePath}`);
42
+
43
+ try {
44
+ await fs.access(claudePath, fs.constants.W_OK);
45
+ } catch (accessError) {
46
+ reportError(accessError, {
47
+ context: 'claude.runtime-switch.lib.mjs - Claude executable write permission check (bun)',
48
+ level: 'error',
49
+ });
50
+ await log('āŒ Cannot write to Claude executable (permission denied)', { level: 'error' });
51
+ await log(' Try running with sudo or changing file permissions', { level: 'error' });
52
+ process.exit(1);
53
+ }
54
+ // Read current shebang
55
+ const firstLine = await $`head -1 "${claudePath}"`;
56
+ const currentShebang = firstLine.stdout.toString().trim();
57
+ await log(` Current shebang: ${currentShebang}`);
58
+ if (currentShebang.includes('bun')) {
59
+ await log(' āœ… Claude is already configured to use bun');
60
+ process.exit(0);
61
+ }
62
+
63
+ // Create backup
64
+ const backupPath = `${claudePath}.nodejs-backup`;
65
+ await $`cp "${claudePath}" "${backupPath}"`;
66
+ await log(` šŸ“¦ Backup created: ${backupPath}`);
67
+
68
+ // Read file content and replace shebang
69
+ const content = await fs.readFile(claudePath, 'utf8');
70
+ const newContent = content.replace(/^#!.*node.*$/m, '#!/usr/bin/env bun');
71
+
72
+ if (content === newContent) {
73
+ await log('āš ļø No Node.js shebang found to replace', { level: 'warning' });
74
+ await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
75
+ process.exit(0);
76
+ }
77
+
78
+ await fs.writeFile(claudePath, newContent);
79
+ await log(' āœ… Claude shebang updated to use bun');
80
+ await log(' šŸ”„ Claude will now run with bun runtime');
81
+ } catch (error) {
82
+ await log(`āŒ Failed to switch Claude to bun: ${cleanErrorMessage(error)}`, { level: 'error' });
83
+ process.exit(1);
84
+ }
85
+
86
+ // Exit after switching runtime
87
+ process.exit(0);
88
+ }
89
+
90
+ if (argv['force-claude-nodejs-run']) {
91
+ await log('\nšŸ”§ Restoring Claude runtime to Node.js...');
92
+ try {
93
+ try {
94
+ await $`which node`;
95
+ await log(' āœ… Node.js runtime found');
96
+ } catch (nodeError) {
97
+ reportError(nodeError, {
98
+ context: 'claude.runtime-switch.lib.mjs - Node.js availability check',
99
+ level: 'error',
100
+ });
101
+ await log('āŒ Node.js runtime not found. Please install Node.js first', { level: 'error' });
102
+ process.exit(1);
103
+ }
104
+
105
+ // Find Claude executable path
106
+ const claudePathResult = await $`which claude`;
107
+ const claudePath = claudePathResult.stdout.toString().trim();
108
+
109
+ if (!claudePath) {
110
+ await log('āŒ Claude executable not found', { level: 'error' });
111
+ process.exit(1);
112
+ }
113
+
114
+ await log(` Claude path: ${claudePath}`);
115
+
116
+ try {
117
+ await fs.access(claudePath, fs.constants.W_OK);
118
+ } catch (accessError) {
119
+ reportError(accessError, {
120
+ context: 'claude.runtime-switch.lib.mjs - Claude executable write permission check (nodejs)',
121
+ level: 'error',
122
+ });
123
+ await log('āŒ Cannot write to Claude executable (permission denied)', { level: 'error' });
124
+ await log(' Try running with sudo or changing file permissions', { level: 'error' });
125
+ process.exit(1);
126
+ }
127
+ // Read current shebang
128
+ const firstLine = await $`head -1 "${claudePath}"`;
129
+ const currentShebang = firstLine.stdout.toString().trim();
130
+ await log(` Current shebang: ${currentShebang}`);
131
+ if (currentShebang.includes('node') && !currentShebang.includes('bun')) {
132
+ await log(' āœ… Claude is already configured to use Node.js');
133
+ process.exit(0);
134
+ }
135
+
136
+ const backupPath = `${claudePath}.nodejs-backup`;
137
+ try {
138
+ await fs.access(backupPath);
139
+ // Restore from backup
140
+ await $`cp "${backupPath}" "${claudePath}"`;
141
+ await log(` āœ… Restored Claude from backup: ${backupPath}`);
142
+ } catch (backupError) {
143
+ reportError(backupError, {
144
+ context: 'claude_restore_backup',
145
+ level: 'info',
146
+ });
147
+ // No backup available, manually update shebang
148
+ await log(' šŸ“ No backup found, manually updating shebang...');
149
+ const content = await fs.readFile(claudePath, 'utf8');
150
+ const newContent = content.replace(/^#!.*bun.*$/m, '#!/usr/bin/env node');
151
+
152
+ if (content === newContent) {
153
+ await log('āš ļø No bun shebang found to replace', { level: 'warning' });
154
+ await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
155
+ process.exit(0);
156
+ }
157
+
158
+ await fs.writeFile(claudePath, newContent);
159
+ await log(' āœ… Claude shebang updated to use Node.js');
160
+ }
161
+
162
+ await log(' šŸ”„ Claude will now run with Node.js runtime');
163
+ } catch (error) {
164
+ await log(`āŒ Failed to restore Claude to Node.js: ${cleanErrorMessage(error)}`, { level: 'error' });
165
+ process.exit(1);
166
+ }
167
+
168
+ // Exit after restoring runtime
169
+ process.exit(0);
170
+ }
171
+ };
172
+
173
+ export default {
174
+ handleClaudeRuntimeSwitch,
175
+ };