@link-assistant/hive-mind 1.47.1 → 1.48.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.48.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 28f7ace: Add /do and /continue as alias commands for /solve in telegram bot
8
+
9
+ ## 1.47.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 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
14
+
3
15
  ## 1.47.1
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.47.1",
3
+ "version": "1.48.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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 && !stdout.includes('Could not resolve')) {
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 && !stdout.includes('Could not resolve')) {
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: Intercept console.log to capture [VERBOSE] output in log files
313
- setupVerboseLogInterceptor();
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
- switch (level) {
103
- case 'error':
104
- console.error(message);
105
- break;
106
- case 'warning':
107
- case 'warn':
108
- console.warn(message);
109
- break;
110
- case 'info':
111
- default:
112
- console.log(message);
113
- break;
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: Intercept console.log to capture [VERBOSE] output in log files
115
- setupVerboseLogInterceptor();
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
 
@@ -658,7 +658,7 @@ bot.command('help', async ctx => {
658
658
  message += '📝 *Available Commands:*\n\n';
659
659
 
660
660
  if (solveEnabled) {
661
- message += '*/solve* - Solve a GitHub issue\n';
661
+ message += '*/solve* (aliases: */do*, */continue*) - Solve a GitHub issue\n';
662
662
  message += 'Usage: `/solve <github-url> [options]`\n';
663
663
  message += 'Example: `/solve https://github.com/owner/repo/issues/123 --model sonnet`\n';
664
664
  message += 'Or reply to a message with a GitHub link: `/solve`\n';
@@ -667,7 +667,7 @@ bot.command('help', async ctx => {
667
667
  }
668
668
  message += '\n';
669
669
  } else {
670
- message += '*/solve* - ❌ Disabled\n\n';
670
+ message += '*/solve* (aliases: */do*, */continue*) - ❌ Disabled\n\n';
671
671
  }
672
672
 
673
673
  if (hiveEnabled) {
@@ -695,7 +695,7 @@ bot.command('help', async ctx => {
695
695
  message += '🔔 *Session Notifications:* The bot monitors sessions and notifies when they complete.\n';
696
696
  if (ISOLATION_BACKEND) message += `🔒 *Isolation Mode:* \`${ISOLATION_BACKEND}\` (experimental)\n`;
697
697
  message += '\n';
698
- message += '⚠️ *Note:* /solve, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats.\n\n';
698
+ message += '⚠️ *Note:* /solve, /do, /continue, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats.\n\n';
699
699
  message += '🔧 *Common Options:*\n';
700
700
  message += `• \`--model <model>\` or \`-m\` - ${buildModelOptionDescription()}\n`;
701
701
  message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
@@ -1041,7 +1041,7 @@ async function handleSolveCommand(ctx) {
1041
1041
  }
1042
1042
  }
1043
1043
 
1044
- bot.command(/^solve$/i, handleSolveCommand);
1044
+ bot.command([/^solve$/i, /^do$/i, /^continue$/i], handleSolveCommand); // /do and /continue are aliases (issue #525)
1045
1045
 
1046
1046
  // Named handler for /hive command - extracted for reuse by text-based fallback (issue #1207)
1047
1047
  async function handleHiveCommand(ctx) {
@@ -1268,12 +1268,8 @@ bot.on('message', async (ctx, next) => {
1268
1268
  }
1269
1269
 
1270
1270
  // Check if this is a command we handle
1271
- const handlers = {
1272
- solve: handleSolveCommand,
1273
- hive: handleHiveCommand,
1274
- solve_queue: handleSolveQueueCommand,
1275
- solvequeue: handleSolveQueueCommand,
1276
- };
1271
+ // /do and /continue are aliases for /solve (issue #525)
1272
+ const handlers = { solve: handleSolveCommand, do: handleSolveCommand, continue: handleSolveCommand, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand };
1277
1273
 
1278
1274
  const handler = handlers[extracted.command];
1279
1275
  if (!handler) return next();