@link-assistant/hive-mind 1.50.9 → 1.50.10

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,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.50.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 0dc1613: Fix log upload raw URL resolution so gist metadata lookups do not mirror full gist contents to stdout, and harden stdio handling when the terminal pipe is already broken.
8
+
3
9
  ## 1.50.9
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.50.9",
3
+ "version": "1.50.10",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
package/src/lib.mjs CHANGED
@@ -188,16 +188,107 @@ export const setupVerboseLogInterceptor = () => {
188
188
  */
189
189
  let stdioInterceptorInstalled = false;
190
190
  let _writingFromLog = false; // Guard flag to prevent double-logging from log()
191
+ let stdoutBroken = false;
192
+ let stderrBroken = false;
193
+ let brokenPipeDiagnosticsWritten = false;
194
+
195
+ const isBrokenPipeError = error => {
196
+ return error?.code === 'EPIPE' || error?.code === 'ERR_STREAM_DESTROYED';
197
+ };
198
+
199
+ const invokeWriteCallback = (callback, error = null) => {
200
+ if (typeof callback === 'function') {
201
+ callback(error);
202
+ }
203
+ };
204
+
205
+ const appendInternalDiagnostic = async message => {
206
+ if (!logFile) return;
207
+ const prefix = `[${new Date().toISOString()}] [INTERNAL]`;
208
+ await fs.appendFile(logFile, `${prefix} ${message}\n`).catch(() => {
209
+ // Silent fail to avoid recursive logging errors
210
+ });
211
+ };
212
+
213
+ const formatStreamDiagnostic = stream => {
214
+ return JSON.stringify({
215
+ isTTY: Boolean(stream?.isTTY),
216
+ destroyed: Boolean(stream?.destroyed),
217
+ writable: stream?.writable,
218
+ writableEnded: Boolean(stream?.writableEnded),
219
+ writableFinished: Boolean(stream?.writableFinished),
220
+ errored: stream?.errored?.code || stream?.errored?.message || null,
221
+ fd: typeof stream?.fd === 'number' ? stream.fd : null,
222
+ });
223
+ };
224
+
225
+ const normalizeWriteCallback = (encoding, callback) => {
226
+ return typeof encoding === 'function' ? encoding : callback;
227
+ };
228
+
229
+ const safeTerminalWrite = ({ originalWrite, chunk, encoding, callback, streamName }) => {
230
+ const isStdout = streamName === 'stdout';
231
+ const normalizedCallback = normalizeWriteCallback(encoding, callback);
232
+ if ((isStdout && stdoutBroken) || (!isStdout && stderrBroken)) {
233
+ invokeWriteCallback(normalizedCallback);
234
+ return false;
235
+ }
236
+
237
+ try {
238
+ return originalWrite(chunk, encoding, callback);
239
+ } catch (error) {
240
+ if (!isBrokenPipeError(error)) {
241
+ throw error;
242
+ }
243
+
244
+ if (isStdout) {
245
+ stdoutBroken = true;
246
+ } else {
247
+ stderrBroken = true;
248
+ }
249
+
250
+ invokeWriteCallback(normalizedCallback, error);
251
+ return false;
252
+ }
253
+ };
254
+
255
+ const installBrokenPipeGuard = (stream, streamName) => {
256
+ stream.on('error', error => {
257
+ if (isBrokenPipeError(error)) {
258
+ if (streamName === 'stdout') {
259
+ stdoutBroken = true;
260
+ } else {
261
+ stderrBroken = true;
262
+ }
263
+ if (!brokenPipeDiagnosticsWritten) {
264
+ brokenPipeDiagnosticsWritten = true;
265
+ void appendInternalDiagnostic(`Detected broken ${streamName} stream (${error.code || 'unknown'}). Stream state=${formatStreamDiagnostic(stream)}. Further terminal writes will be skipped when possible.`);
266
+ }
267
+ return;
268
+ }
269
+
270
+ throw error;
271
+ });
272
+ };
273
+
191
274
  export const setupStdioLogInterceptor = () => {
192
275
  if (stdioInterceptorInstalled) return;
193
276
  stdioInterceptorInstalled = true;
194
277
 
195
278
  const originalStdoutWrite = process.stdout.write.bind(process.stdout);
196
279
  const originalStderrWrite = process.stderr.write.bind(process.stderr);
280
+ installBrokenPipeGuard(process.stdout, 'stdout');
281
+ installBrokenPipeGuard(process.stderr, 'stderr');
197
282
 
198
283
  process.stdout.write = (chunk, encoding, callback) => {
199
- // Always write to terminal first
200
- const result = originalStdoutWrite(chunk, encoding, callback);
284
+ // Always write to terminal first, unless the output pipe is already broken.
285
+ const result = safeTerminalWrite({
286
+ originalWrite: originalStdoutWrite,
287
+ chunk,
288
+ encoding,
289
+ callback,
290
+ streamName: 'stdout',
291
+ });
201
292
 
202
293
  // Also append to log file if set, but skip if this write originated from log()
203
294
  if (logFile && !_writingFromLog) {
@@ -214,8 +305,14 @@ export const setupStdioLogInterceptor = () => {
214
305
  };
215
306
 
216
307
  process.stderr.write = (chunk, encoding, callback) => {
217
- // Always write to terminal first
218
- const result = originalStderrWrite(chunk, encoding, callback);
308
+ // Always write to terminal first, unless the output pipe is already broken.
309
+ const result = safeTerminalWrite({
310
+ originalWrite: originalStderrWrite,
311
+ chunk,
312
+ encoding,
313
+ callback,
314
+ streamName: 'stderr',
315
+ });
219
316
 
220
317
  // Also append to log file if set, but skip if this write originated from log()
221
318
  if (logFile && !_writingFromLog) {
@@ -11,6 +11,7 @@ const use = globalThis.use;
11
11
 
12
12
  // Use command-stream for consistent $ behavior across runtimes
13
13
  const { $ } = await use('command-stream');
14
+ const $silent = $({ mirror: false, capture: true });
14
15
 
15
16
  // Import shared library functions
16
17
  const lib = await import('./lib.mjs');
@@ -20,6 +21,12 @@ const { log } = lib;
20
21
  const sentryLib = await import('./sentry.lib.mjs');
21
22
  const { reportError } = sentryLib;
22
23
 
24
+ const summarizeCommandOutput = value => {
25
+ const text = value?.toString()?.trim() || '';
26
+ if (!text) return '';
27
+ return text.length > 500 ? `${text.slice(0, 500)}... [truncated ${text.length - 500} chars]` : text;
28
+ };
29
+
23
30
  /**
24
31
  * Upload a log file using gh-upload-log command
25
32
  * @param {Object} options - Upload options
@@ -92,12 +99,18 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
92
99
  // For gist: get raw URL from gist API
93
100
  const gistId = result.url.split('/').pop();
94
101
  try {
95
- const gistDetailsResult = await $`gh api gists/${gistId} --jq '{owner: .owner.login, files: .files, history: .history}'`;
102
+ if (verbose) {
103
+ await log(` 🔍 Fetching gist metadata for raw URL resolution (gistId=${gistId})`, { verbose: true });
104
+ }
105
+ const gistDetailsResult = await $silent`gh api gists/${gistId} --jq '{owner: .owner.login, history: .history, fileNames: (.files | keys)}'`;
106
+ if (verbose) {
107
+ await log(` 📥 Gist metadata fetch completed (code=${gistDetailsResult.code ?? 'unknown'})`, { verbose: true });
108
+ }
96
109
  if (gistDetailsResult.code === 0) {
97
110
  const gistDetails = JSON.parse(gistDetailsResult.stdout.toString());
98
111
  const gistOwner = gistDetails.owner;
99
112
  const commitSha = gistDetails.history?.[0]?.version;
100
- const fileNames = gistDetails.files ? Object.keys(gistDetails.files) : [];
113
+ const fileNames = Array.isArray(gistDetails.fileNames) ? gistDetails.fileNames : [];
101
114
  const fileName = fileNames.length > 0 ? fileNames[0] : 'log.txt';
102
115
 
103
116
  if (commitSha) {
@@ -105,6 +118,18 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
105
118
  } else {
106
119
  result.rawUrl = `https://gist.githubusercontent.com/${gistOwner}/${gistId}/raw/${fileName}`;
107
120
  }
121
+ if (verbose) {
122
+ await log(` 🧩 Gist metadata resolved owner=${gistOwner}, commitSha=${commitSha || 'latest'}, fileName=${fileName}`, { verbose: true });
123
+ }
124
+ } else if (verbose) {
125
+ const stderrSummary = summarizeCommandOutput(gistDetailsResult.stderr);
126
+ const stdoutSummary = summarizeCommandOutput(gistDetailsResult.stdout);
127
+ if (stderrSummary) {
128
+ await log(` ⚠️ Gist metadata stderr: ${stderrSummary}`, { verbose: true });
129
+ }
130
+ if (stdoutSummary) {
131
+ await log(` ⚠️ Gist metadata stdout: ${stdoutSummary}`, { verbose: true });
132
+ }
108
133
  }
109
134
  } catch (apiError) {
110
135
  if (verbose) {
@@ -121,7 +146,13 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
121
146
  try {
122
147
  const repoUrl = result.url;
123
148
  const repoPath = repoUrl.replace('https://github.com/', '');
124
- const contentsResult = await $`gh api repos/${repoPath}/contents --jq '.[].name'`;
149
+ if (verbose) {
150
+ await log(` 🔍 Fetching repository contents for raw URL resolution (repoPath=${repoPath})`, { verbose: true });
151
+ }
152
+ const contentsResult = await $silent`gh api repos/${repoPath}/contents --jq '.[].name'`;
153
+ if (verbose) {
154
+ await log(` 📥 Repository contents fetch completed (code=${contentsResult.code ?? 'unknown'})`, { verbose: true });
155
+ }
125
156
  if (contentsResult.code === 0) {
126
157
  const files = contentsResult.stdout
127
158
  .toString()
@@ -131,6 +162,18 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
131
162
  if (files.length > 0) {
132
163
  const fileName = files[0];
133
164
  result.rawUrl = `${repoUrl}/raw/main/${fileName}`;
165
+ if (verbose) {
166
+ await log(` 🧩 Repository contents resolved fileName=${fileName}`, { verbose: true });
167
+ }
168
+ }
169
+ } else if (verbose) {
170
+ const stderrSummary = summarizeCommandOutput(contentsResult.stderr);
171
+ const stdoutSummary = summarizeCommandOutput(contentsResult.stdout);
172
+ if (stderrSummary) {
173
+ await log(` ⚠️ Repository contents stderr: ${stderrSummary}`, { verbose: true });
174
+ }
175
+ if (stdoutSummary) {
176
+ await log(` ⚠️ Repository contents stdout: ${stdoutSummary}`, { verbose: true });
134
177
  }
135
178
  }
136
179
  } catch (apiError) {