@link-assistant/hive-mind 1.47.0 → 1.47.2
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 +12 -0
- package/package.json +1 -1
- package/src/claude.budget-stats.lib.mjs +32 -31
- package/src/github.lib.mjs +2 -2
- package/src/hive.mjs +3 -3
- package/src/lib.mjs +92 -15
- package/src/solve.mjs +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.47.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7afe67b: Fix ghPrView false positive on "Could not resolve" in PR body causing "Failed to get PR details" error on fork PRs, and add stdio log interceptor for terminal/log output parity
|
|
8
|
+
|
|
9
|
+
## 1.47.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 3bbd66e: Improve Context and tokens usage output format: move percentage before unit label, parenthesize cached tokens in Total line, use consistent X / Y (Z%) format for output tokens when limit is known, and show sub-sessions under model heading instead of globally
|
|
14
|
+
|
|
3
15
|
## 1.47.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -165,16 +165,15 @@ export const displayBudgetStats = async (usage, tokenUsage, log) => {
|
|
|
165
165
|
const sub = subSessions[i];
|
|
166
166
|
const subPeak = sub.peakContextUsage || 0;
|
|
167
167
|
// Issue #1539: Only use peak per-request context for context window display.
|
|
168
|
-
//
|
|
169
|
-
// impossible percentages (e.g. 250%). When peak is unknown, skip context display.
|
|
168
|
+
// Issue #1547: Percentage before unit label: X / Y (Z%) input tokens
|
|
170
169
|
const parts = [];
|
|
171
170
|
if (contextLimit && subPeak > 0) {
|
|
172
171
|
const pct = ((subPeak / contextLimit) * 100).toFixed(0);
|
|
173
|
-
parts.push(`${formatNumber(subPeak)} / ${formatNumber(contextLimit)}
|
|
172
|
+
parts.push(`${formatNumber(subPeak)} / ${formatNumber(contextLimit)} (${pct}%) input tokens`);
|
|
174
173
|
}
|
|
175
174
|
if (outputLimit) {
|
|
176
175
|
const outPct = ((sub.outputTokens / outputLimit) * 100).toFixed(0);
|
|
177
|
-
parts.push(`${formatNumber(sub.outputTokens)} / ${formatNumber(outputLimit)}
|
|
176
|
+
parts.push(`${formatNumber(sub.outputTokens)} / ${formatNumber(outputLimit)} (${outPct}%) output tokens`);
|
|
178
177
|
}
|
|
179
178
|
if (parts.length > 0) {
|
|
180
179
|
await log(` ${i + 1}. Context window: ${parts.join(', ')}`);
|
|
@@ -182,14 +181,15 @@ export const displayBudgetStats = async (usage, tokenUsage, log) => {
|
|
|
182
181
|
}
|
|
183
182
|
} else if (peakContext > 0) {
|
|
184
183
|
// Single sub-session with known peak: single-line format
|
|
184
|
+
// Issue #1547: Percentage before unit label
|
|
185
185
|
const parts = [];
|
|
186
186
|
if (contextLimit) {
|
|
187
187
|
const pct = ((peakContext / contextLimit) * 100).toFixed(0);
|
|
188
|
-
parts.push(`${formatNumber(peakContext)} / ${formatNumber(contextLimit)}
|
|
188
|
+
parts.push(`${formatNumber(peakContext)} / ${formatNumber(contextLimit)} (${pct}%) input tokens`);
|
|
189
189
|
}
|
|
190
190
|
if (outputLimit) {
|
|
191
191
|
const outPct = ((usage.outputTokens / outputLimit) * 100).toFixed(0);
|
|
192
|
-
parts.push(`${formatNumber(usage.outputTokens)} / ${formatNumber(outputLimit)}
|
|
192
|
+
parts.push(`${formatNumber(usage.outputTokens)} / ${formatNumber(outputLimit)} (${outPct}%) output tokens`);
|
|
193
193
|
}
|
|
194
194
|
if (parts.length > 0) {
|
|
195
195
|
await log(` Context window: ${parts.join(', ')}`);
|
|
@@ -199,15 +199,20 @@ export const displayBudgetStats = async (usage, tokenUsage, log) => {
|
|
|
199
199
|
// Cumulative totals are shown on the Total line below — no duplication needed.
|
|
200
200
|
|
|
201
201
|
// Cumulative totals — single line
|
|
202
|
+
// Issue #1547: Parenthesized cached format and consistent output format
|
|
202
203
|
const totalInputNonCached = usage.inputTokens + usage.cacheCreationTokens;
|
|
203
204
|
const cachedTokens = usage.cacheReadTokens;
|
|
204
|
-
let totalLine
|
|
205
|
-
if (cachedTokens > 0)
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
let totalLine;
|
|
206
|
+
if (cachedTokens > 0) {
|
|
207
|
+
totalLine = `(${formatNumber(totalInputNonCached)} + ${formatNumber(cachedTokens)} cached) input tokens`;
|
|
208
|
+
} else {
|
|
209
|
+
totalLine = `${formatNumber(totalInputNonCached)} input tokens`;
|
|
210
|
+
}
|
|
208
211
|
if (peakContext === 0 && outputLimit) {
|
|
209
212
|
const outPct = ((usage.outputTokens / outputLimit) * 100).toFixed(0);
|
|
210
|
-
totalLine +=
|
|
213
|
+
totalLine += `, ${formatNumber(usage.outputTokens)} / ${formatNumber(outputLimit)} (${outPct}%) output tokens`;
|
|
214
|
+
} else {
|
|
215
|
+
totalLine += `, ${formatNumber(usage.outputTokens)} output tokens`;
|
|
211
216
|
}
|
|
212
217
|
await log(` Total: ${totalLine}`);
|
|
213
218
|
};
|
|
@@ -325,12 +330,12 @@ const formatContextOutputLine = (peakContext, contextLimit, outputTokens, output
|
|
|
325
330
|
// context window metrics and produce impossible percentages (e.g. 250%).
|
|
326
331
|
if (peakContext > 0) {
|
|
327
332
|
const pct = ((peakContext / contextLimit) * 100).toFixed(0);
|
|
328
|
-
parts.push(`${formatTokensCompact(peakContext)} / ${formatTokensCompact(contextLimit)}
|
|
333
|
+
parts.push(`${formatTokensCompact(peakContext)} / ${formatTokensCompact(contextLimit)} (${pct}%) input tokens`);
|
|
329
334
|
}
|
|
330
335
|
}
|
|
331
336
|
if (outputLimit) {
|
|
332
337
|
const outPct = ((outputTokens / outputLimit) * 100).toFixed(0);
|
|
333
|
-
parts.push(`${formatTokensCompact(outputTokens)} / ${formatTokensCompact(outputLimit)}
|
|
338
|
+
parts.push(`${formatTokensCompact(outputTokens)} / ${formatTokensCompact(outputLimit)} (${outPct}%) output tokens`);
|
|
334
339
|
}
|
|
335
340
|
if (parts.length === 0) return '';
|
|
336
341
|
return `\n${prefix}Context window: ${parts.join(', ')}`;
|
|
@@ -364,15 +369,6 @@ export const buildBudgetStatsString = tokenUsage => {
|
|
|
364
369
|
const subSessions = tokenUsage.subSessions || [];
|
|
365
370
|
const hasMultipleSubSessions = subSessions.length > 1;
|
|
366
371
|
|
|
367
|
-
if (isMultiModel && hasMultipleSubSessions) {
|
|
368
|
-
// Issue #1508: For multi-model sessions, show global sub-sessions once (not per-model),
|
|
369
|
-
// since sub-sessions track compactification boundaries which are session-wide.
|
|
370
|
-
// Per-model context/output limits are shown below under each model heading.
|
|
371
|
-
const primaryModelId = modelIds[0];
|
|
372
|
-
const primaryUsage = tokenUsage.modelUsage[primaryModelId];
|
|
373
|
-
stats += formatSubSessionsList(subSessions, primaryUsage.modelInfo?.limit?.context, primaryUsage.modelInfo?.limit?.output);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
372
|
for (const modelId of modelIds) {
|
|
377
373
|
const usage = tokenUsage.modelUsage[modelId];
|
|
378
374
|
const modelName = usage.modelName || modelId;
|
|
@@ -383,8 +379,9 @@ export const buildBudgetStatsString = tokenUsage => {
|
|
|
383
379
|
|
|
384
380
|
const peakContext = usage.peakContextUsage || 0;
|
|
385
381
|
|
|
386
|
-
if (!isMultiModel
|
|
387
|
-
//
|
|
382
|
+
if (hasMultipleSubSessions && (!isMultiModel || modelId === modelIds[0])) {
|
|
383
|
+
// Issue #1547: Show sub-sessions under the primary model heading (not globally).
|
|
384
|
+
// For single-model sessions, show under that model. For multi-model, under the first model.
|
|
388
385
|
stats += formatSubSessionsList(subSessions, contextLimit, outputLimit);
|
|
389
386
|
} else if (peakContext > 0) {
|
|
390
387
|
// Issue #1526: Single line format for context window + output tokens
|
|
@@ -394,18 +391,22 @@ export const buildBudgetStatsString = tokenUsage => {
|
|
|
394
391
|
// Cumulative totals are shown on the Total line below — no duplication needed.
|
|
395
392
|
|
|
396
393
|
// Cumulative totals per model: input tokens + cached shown separately
|
|
397
|
-
// Issue #
|
|
394
|
+
// Issue #1547: Parenthesized cached format: (X + Y cached) input tokens
|
|
398
395
|
const totalInputNonCached = usage.inputTokens + usage.cacheCreationTokens;
|
|
399
396
|
const cachedTokens = usage.cacheReadTokens;
|
|
400
|
-
let totalLine
|
|
401
|
-
if (cachedTokens > 0)
|
|
402
|
-
|
|
397
|
+
let totalLine;
|
|
398
|
+
if (cachedTokens > 0) {
|
|
399
|
+
totalLine = `(${formatTokensCompact(totalInputNonCached)} + ${formatTokensCompact(cachedTokens)} cached) input tokens`;
|
|
400
|
+
} else {
|
|
401
|
+
totalLine = `${formatTokensCompact(totalInputNonCached)} input tokens`;
|
|
402
|
+
}
|
|
403
403
|
|
|
404
|
-
// Issue #
|
|
405
|
-
// output token percentage in the Total line so no data is lost.
|
|
404
|
+
// Issue #1547: Consistent output format — use X / Y (Z%) output tokens when limit known
|
|
406
405
|
if (peakContext === 0 && outputLimit) {
|
|
407
406
|
const outPct = ((usage.outputTokens / outputLimit) * 100).toFixed(0);
|
|
408
|
-
totalLine +=
|
|
407
|
+
totalLine += `, ${formatTokensCompact(usage.outputTokens)} / ${formatTokensCompact(outputLimit)} (${outPct}%) output tokens`;
|
|
408
|
+
} else {
|
|
409
|
+
totalLine += `, ${formatTokensCompact(usage.outputTokens)} output tokens`;
|
|
409
410
|
}
|
|
410
411
|
|
|
411
412
|
// Issue #1508: Show per-model cost when available
|
package/src/github.lib.mjs
CHANGED
|
@@ -1328,7 +1328,7 @@ export async function ghPrView({ prNumber, owner, repo, jsonFields = 'headRefNam
|
|
|
1328
1328
|
const stderr = prResult.stderr ? prResult.stderr.toString() : '';
|
|
1329
1329
|
const code = prResult.code || 0;
|
|
1330
1330
|
let data = null;
|
|
1331
|
-
if (code === 0 && stdout && !
|
|
1331
|
+
if (code === 0 && stdout && !(stderr && stderr.includes('Could not resolve'))) {
|
|
1332
1332
|
try {
|
|
1333
1333
|
data = JSON.parse(stdout);
|
|
1334
1334
|
} catch {
|
|
@@ -1368,7 +1368,7 @@ export async function ghIssueView({ issueNumber, owner, repo, jsonFields = 'numb
|
|
|
1368
1368
|
const stderr = issueResult.stderr ? issueResult.stderr.toString() : '';
|
|
1369
1369
|
const code = issueResult.code || 0;
|
|
1370
1370
|
let data = null;
|
|
1371
|
-
if (code === 0 && stdout && !
|
|
1371
|
+
if (code === 0 && stdout && !(stderr && stderr.includes('Could not resolve'))) {
|
|
1372
1372
|
try {
|
|
1373
1373
|
data = JSON.parse(stdout);
|
|
1374
1374
|
} catch {
|
package/src/hive.mjs
CHANGED
|
@@ -95,7 +95,7 @@ if (isDirectExecution) {
|
|
|
95
95
|
const fs = (await withTimeout(use('fs'), 30000, 'loading fs')).promises;
|
|
96
96
|
// Import shared library functions
|
|
97
97
|
const lib = await import('./lib.mjs');
|
|
98
|
-
const { log, setLogFile, getAbsoluteLogPath, formatTimestamp, cleanErrorMessage, cleanupTempDirectories, setupVerboseLogInterceptor } = lib;
|
|
98
|
+
const { log, setLogFile, getAbsoluteLogPath, formatTimestamp, cleanErrorMessage, cleanupTempDirectories, setupVerboseLogInterceptor, setupStdioLogInterceptor } = lib;
|
|
99
99
|
const yargsConfigLib = await import('./hive.config.lib.mjs');
|
|
100
100
|
const { createYargsConfig } = yargsConfigLib;
|
|
101
101
|
const claudeLib = await import('./claude.lib.mjs');
|
|
@@ -309,8 +309,8 @@ if (isDirectExecution) {
|
|
|
309
309
|
// Set global verbose mode
|
|
310
310
|
global.verboseMode = argv.verbose;
|
|
311
311
|
|
|
312
|
-
// Issue #1466:
|
|
313
|
-
|
|
312
|
+
setupVerboseLogInterceptor(); // Issue #1466: capture [VERBOSE] output in log files
|
|
313
|
+
setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log file
|
|
314
314
|
|
|
315
315
|
// Use the universal GitHub URL parser
|
|
316
316
|
if (githubUrl) {
|
package/src/lib.mjs
CHANGED
|
@@ -99,18 +99,24 @@ export const log = async (message, options = {}) => {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Write to console based on level
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
102
|
+
// Set guard flag to prevent stdio interceptor from double-logging (issue #1549)
|
|
103
|
+
_writingFromLog = true;
|
|
104
|
+
try {
|
|
105
|
+
switch (level) {
|
|
106
|
+
case 'error':
|
|
107
|
+
console.error(message);
|
|
108
|
+
break;
|
|
109
|
+
case 'warning':
|
|
110
|
+
case 'warn':
|
|
111
|
+
console.warn(message);
|
|
112
|
+
break;
|
|
113
|
+
case 'info':
|
|
114
|
+
default:
|
|
115
|
+
console.log(message);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
_writingFromLog = false;
|
|
114
120
|
}
|
|
115
121
|
};
|
|
116
122
|
|
|
@@ -134,20 +140,90 @@ export const setupVerboseLogInterceptor = () => {
|
|
|
134
140
|
|
|
135
141
|
const originalConsoleLog = console.log.bind(console);
|
|
136
142
|
console.log = (...args) => {
|
|
137
|
-
// Always call original console.log first
|
|
138
|
-
originalConsoleLog(...args);
|
|
139
|
-
|
|
140
143
|
// If a log file is set and the message looks like a [VERBOSE] log, append to file
|
|
144
|
+
// and set guard flag to prevent stdio interceptor from double-logging (issue #1549)
|
|
141
145
|
if (logFile && args.length > 0) {
|
|
142
146
|
const firstArg = String(args[0]);
|
|
143
147
|
if (firstArg.includes('[VERBOSE]')) {
|
|
144
148
|
const message = args.map(a => String(a)).join(' ');
|
|
145
149
|
const logMessage = `[${new Date().toISOString()}] [VERBOSE] ${message}`;
|
|
150
|
+
_writingFromLog = true;
|
|
151
|
+
fs.appendFile(logFile, logMessage + '\n').catch(() => {
|
|
152
|
+
// Silent fail to avoid infinite loops
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Always call original console.log (with guard flag set if [VERBOSE])
|
|
158
|
+
try {
|
|
159
|
+
originalConsoleLog(...args);
|
|
160
|
+
} finally {
|
|
161
|
+
_writingFromLog = false;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Issue #1549: Intercept process.stdout.write and process.stderr.write to capture
|
|
168
|
+
* ALL terminal output in the log file, ensuring 100% parity between terminal and log.
|
|
169
|
+
*
|
|
170
|
+
* The command-stream library uses process.stdout.write/process.stderr.write directly
|
|
171
|
+
* when mirror:true (the default). console.log/console.error also end up calling these.
|
|
172
|
+
* By intercepting at the write() level, we capture everything regardless of source:
|
|
173
|
+
* - command-stream mirror output (e.g., gh CLI JSON responses)
|
|
174
|
+
* - console.log() / console.error() calls
|
|
175
|
+
* - process.stdout.write() / process.stderr.write() direct calls
|
|
176
|
+
*
|
|
177
|
+
* To avoid double-logging (since the log() function already writes to the log file AND
|
|
178
|
+
* calls console.log which calls process.stdout.write), we use a guard flag
|
|
179
|
+
* `_writingFromLog` to skip interception when the write originates from log().
|
|
180
|
+
*
|
|
181
|
+
* This ensures the log file is a complete record of all terminal output.
|
|
182
|
+
* Call this once after setLogFile() to enable the interceptor.
|
|
183
|
+
*/
|
|
184
|
+
let stdioInterceptorInstalled = false;
|
|
185
|
+
let _writingFromLog = false; // Guard flag to prevent double-logging from log()
|
|
186
|
+
export const setupStdioLogInterceptor = () => {
|
|
187
|
+
if (stdioInterceptorInstalled) return;
|
|
188
|
+
stdioInterceptorInstalled = true;
|
|
189
|
+
|
|
190
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
191
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
192
|
+
|
|
193
|
+
process.stdout.write = (chunk, encoding, callback) => {
|
|
194
|
+
// Always write to terminal first
|
|
195
|
+
const result = originalStdoutWrite(chunk, encoding, callback);
|
|
196
|
+
|
|
197
|
+
// Also append to log file if set, but skip if this write originated from log()
|
|
198
|
+
if (logFile && !_writingFromLog) {
|
|
199
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString(encoding || 'utf8');
|
|
200
|
+
if (text.trim()) {
|
|
201
|
+
const logMessage = `[${new Date().toISOString()}] [STDOUT] ${text.replace(/\n$/, '')}`;
|
|
202
|
+
fs.appendFile(logFile, logMessage + '\n').catch(() => {
|
|
203
|
+
// Silent fail to avoid infinite loops
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return result;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
process.stderr.write = (chunk, encoding, callback) => {
|
|
212
|
+
// Always write to terminal first
|
|
213
|
+
const result = originalStderrWrite(chunk, encoding, callback);
|
|
214
|
+
|
|
215
|
+
// Also append to log file if set, but skip if this write originated from log()
|
|
216
|
+
if (logFile && !_writingFromLog) {
|
|
217
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString(encoding || 'utf8');
|
|
218
|
+
if (text.trim()) {
|
|
219
|
+
const logMessage = `[${new Date().toISOString()}] [STDERR] ${text.replace(/\n$/, '')}`;
|
|
146
220
|
fs.appendFile(logFile, logMessage + '\n').catch(() => {
|
|
147
221
|
// Silent fail to avoid infinite loops
|
|
148
222
|
});
|
|
149
223
|
}
|
|
150
224
|
}
|
|
225
|
+
|
|
226
|
+
return result;
|
|
151
227
|
};
|
|
152
228
|
};
|
|
153
229
|
|
|
@@ -611,6 +687,7 @@ export default {
|
|
|
611
687
|
displayFormattedError,
|
|
612
688
|
cleanupTempDirectories,
|
|
613
689
|
setupVerboseLogInterceptor,
|
|
690
|
+
setupStdioLogInterceptor,
|
|
614
691
|
};
|
|
615
692
|
|
|
616
693
|
/**
|
package/src/solve.mjs
CHANGED
|
@@ -48,7 +48,7 @@ const fs = (await use('fs')).promises;
|
|
|
48
48
|
const crypto = (await use('crypto')).default;
|
|
49
49
|
const memoryCheck = await import('./memory-check.mjs');
|
|
50
50
|
const lib = await import('./lib.mjs');
|
|
51
|
-
const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, getVersionInfo, setupVerboseLogInterceptor } = lib;
|
|
51
|
+
const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, getVersionInfo, setupVerboseLogInterceptor, setupStdioLogInterceptor } = lib;
|
|
52
52
|
const githubLib = await import('./github.lib.mjs');
|
|
53
53
|
const { sanitizeLogContent, attachLogToGitHub, getToolDisplayName } = githubLib;
|
|
54
54
|
const validation = await import('./solve.validation.lib.mjs');
|
|
@@ -111,8 +111,8 @@ try {
|
|
|
111
111
|
}
|
|
112
112
|
global.verboseMode = argv.verbose;
|
|
113
113
|
|
|
114
|
-
// Issue #1466:
|
|
115
|
-
|
|
114
|
+
setupVerboseLogInterceptor(); // Issue #1466: capture [VERBOSE] output in log files
|
|
115
|
+
setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log file
|
|
116
116
|
|
|
117
117
|
// Early logs go to cwd; custom log dir takes effect after argv is parsed
|
|
118
118
|
|