@link-assistant/hive-mind 2.10.1 → 2.10.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.hi.md +2 -0
  3. package/README.md +2 -0
  4. package/README.ru.md +2 -0
  5. package/README.zh.md +2 -0
  6. package/package.json +1 -1
  7. package/src/claude.lib.mjs +0 -4
  8. package/src/cleanup.mjs +18 -6
  9. package/src/codex.lib.mjs +0 -4
  10. package/src/configure-claude.mjs +3 -0
  11. package/src/credential-sanitization-core.lib.mjs +231 -0
  12. package/src/development-log.lib.mjs +39 -6
  13. package/src/fix.mjs +3 -0
  14. package/src/github-error-reporter.lib.mjs +13 -8
  15. package/src/github-issue-auto-close.lib.mjs +2 -1
  16. package/src/github-merge-issue-close.lib.mjs +2 -1
  17. package/src/github.lib.mjs +29 -18
  18. package/src/hive-screens.mjs +3 -0
  19. package/src/instrument.mjs +14 -0
  20. package/src/interactive-mode.lib.mjs +25 -40
  21. package/src/lib.mjs +89 -50
  22. package/src/log-upload.lib.mjs +22 -4
  23. package/src/post-finish-sanitization-sweep.lib.mjs +5 -5
  24. package/src/review.mjs +3 -1
  25. package/src/sentry.lib.mjs +27 -8
  26. package/src/solve.auto-pr.lib.mjs +11 -21
  27. package/src/solve.error-handlers.lib.mjs +2 -1
  28. package/src/solve.progress-monitoring.lib.mjs +20 -9
  29. package/src/solve.results.lib.mjs +9 -15
  30. package/src/start-screen.mjs +3 -0
  31. package/src/task.issue-creation.lib.mjs +5 -3
  32. package/src/task.mjs +21 -8
  33. package/src/telegram-bot.mjs +4 -1
  34. package/src/telegram-log-command.lib.mjs +38 -4
  35. package/src/telegram-safe-reply.lib.mjs +7 -4
  36. package/src/telegram-tokens-command.lib.mjs +1 -1
  37. package/src/token-sanitization.lib.mjs +177 -14
  38. package/src/tool-comments.lib.mjs +5 -5
  39. package/src/youtrack/youtrack-sync.mjs +4 -3
@@ -1,5 +1,24 @@
1
1
  // Sentry integration library for hive-mind
2
2
  import { isSentryEnabled, captureException, captureMessage, startTransaction } from './instrument.mjs';
3
+ import { sanitizeCredentialText } from './credential-sanitization-core.lib.mjs';
4
+
5
+ const sanitizeError = error => {
6
+ const source = error instanceof Error ? error : new Error(String(error));
7
+ const sanitized = new Error(sanitizeCredentialText(source.message));
8
+ sanitized.name = source.name;
9
+ if (source.stack) sanitized.stack = sanitizeCredentialText(source.stack);
10
+ return sanitized;
11
+ };
12
+
13
+ const sanitizeContext = (value, seen = new WeakSet()) => {
14
+ if (typeof value === 'string') return sanitizeCredentialText(value);
15
+ if (value instanceof Error) return sanitizeError(value);
16
+ if (!value || typeof value !== 'object') return value;
17
+ if (seen.has(value)) return '[Circular]';
18
+ seen.add(value);
19
+ if (Array.isArray(value)) return value.map(item => sanitizeContext(item, seen));
20
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeContext(item, seen)]));
21
+ };
3
22
 
4
23
  // Lazy import of Sentry to handle cases where it's not installed
5
24
  let Sentry = null;
@@ -84,7 +103,7 @@ export const withSentry = (fn, name, op = 'task') => {
84
103
  return result;
85
104
  } catch (error) {
86
105
  transaction.setStatus('internal_error');
87
- captureException(error, {
106
+ captureException(sanitizeError(error), {
88
107
  operation: name,
89
108
  args: args.length > 0 ? `${args.length} arguments` : 'no arguments',
90
109
  });
@@ -139,7 +158,7 @@ export const logToSentry = (message, level = 'info', context = {}) => {
139
158
  return;
140
159
  }
141
160
 
142
- captureMessage(message, level, context);
161
+ captureMessage(sanitizeCredentialText(message), level, sanitizeContext(context));
143
162
  };
144
163
 
145
164
  /**
@@ -153,7 +172,7 @@ export const reportError = (error, context = {}) => {
153
172
  return;
154
173
  }
155
174
 
156
- captureException(error, { ...context, level: 'error' });
175
+ captureException(sanitizeError(error), { ...sanitizeContext(context), level: 'error' });
157
176
  };
158
177
 
159
178
  /**
@@ -169,7 +188,7 @@ export const reportWarning = (warning, context = {}) => {
169
188
 
170
189
  // Convert string warnings to Error objects for better stack traces
171
190
  const warningError = typeof warning === 'string' ? new Error(warning) : warning;
172
- captureException(warningError, { ...context, level: 'warning' });
191
+ captureException(sanitizeError(warningError), { ...sanitizeContext(context), level: 'warning' });
173
192
  };
174
193
 
175
194
  /**
@@ -183,7 +202,7 @@ export const addBreadcrumb = async breadcrumb => {
183
202
 
184
203
  const sentry = await getSentry();
185
204
  if (sentry) {
186
- sentry.addBreadcrumb(breadcrumb);
205
+ sentry.addBreadcrumb(sanitizeContext(breadcrumb));
187
206
  }
188
207
  };
189
208
 
@@ -198,7 +217,7 @@ export const setUserContext = async user => {
198
217
 
199
218
  const sentry = await getSentry();
200
219
  if (sentry) {
201
- sentry.setUser(user);
220
+ sentry.setUser(sanitizeContext(user));
202
221
  }
203
222
  };
204
223
 
@@ -214,7 +233,7 @@ export const setExtraContext = async (key, value) => {
214
233
 
215
234
  const sentry = await getSentry();
216
235
  if (sentry) {
217
- sentry.setExtra(key, value);
236
+ sentry.setExtra(key, sanitizeContext(value));
218
237
  }
219
238
  };
220
239
 
@@ -229,7 +248,7 @@ export const setTags = async tags => {
229
248
 
230
249
  const sentry = await getSentry();
231
250
  if (sentry) {
232
- sentry.setTags(tags);
251
+ sentry.setTags(sanitizeContext(tags));
233
252
  }
234
253
  };
235
254
 
@@ -11,6 +11,7 @@ import { handleCompareApiNotReady } from './solve.auto-pr-compare-readiness.lib.
11
11
 
12
12
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry, isTransientCompareApiError } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. Issue #1756: execGhWithRetry retries on transient 5xx (504) too. Issue #1829: isTransientCompareApiError lets the compare-API readiness gate degrade gracefully on transient diff-render failures.
13
13
  import { stagePlaceholderFileOrExplain, explainNothingStagedAndThrow } from './solve.auto-pr-placeholder.lib.mjs'; // Issue #1825: handles the seed placeholder when the target repo gitignores it.
14
+ import { sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
14
15
 
15
16
  export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNumber, owner, repo, defaultBranch, forkedRepo, isContinueMode, prNumber, log, formatAligned, $, reportError, path, fs }) {
16
17
  // Skip auto-PR creation if:
@@ -906,17 +907,19 @@ ${prBody}`,
906
907
  // single transient 5xx (e.g. `HTTP 504: 504 Gateway Timeout
907
908
  // (https://api.github.com/graphql)`) or rate-limit response retries
908
909
  // instead of aborting the whole solve session.
910
+ let prBodyFile = null;
911
+ let prTitleFile = null;
909
912
  try {
910
913
  // Write PR body to temp file to avoid shell escaping issues
911
- const prBodyFile = `/tmp/pr-body-${Date.now()}.md`;
912
- await fs.writeFile(prBodyFile, prBody);
914
+ prBodyFile = `/tmp/pr-body-${Date.now()}.md`;
915
+ await writeSanitizedPublicationFile(prBodyFile, prBody);
913
916
 
914
917
  // Write PR title to temp file to avoid shell escaping issues with quotes/apostrophes
915
918
  // This solves the issue where titles containing apostrophes (e.g., "don't") would cause
916
919
  // "Unterminated quoted string" errors
917
- const prTitle = `[WIP] ${issueTitle}`;
918
- const prTitleFile = `/tmp/pr-title-${Date.now()}.txt`;
919
- await fs.writeFile(prTitleFile, prTitle);
920
+ const prTitle = await sanitizeForPublication(`[WIP] ${issueTitle}`);
921
+ prTitleFile = `/tmp/pr-title-${Date.now()}.txt`;
922
+ await writeSanitizedPublicationFile(prTitleFile, prTitle);
920
923
 
921
924
  // Build command with optional assignee and handle forks
922
925
  // Note: targetBranch is already defined above
@@ -994,22 +997,6 @@ ${prBody}`,
994
997
  }
995
998
  }
996
999
 
997
- // Clean up temp files
998
- await fs.unlink(prBodyFile).catch(unlinkError => {
999
- reportError(unlinkError, {
1000
- context: 'pr_body_file_cleanup',
1001
- prBodyFile,
1002
- operation: 'delete_temp_file',
1003
- });
1004
- });
1005
- await fs.unlink(prTitleFile).catch(unlinkError => {
1006
- reportError(unlinkError, {
1007
- context: 'pr_title_file_cleanup',
1008
- prTitleFile,
1009
- operation: 'delete_temp_file',
1010
- });
1011
- });
1012
-
1013
1000
  // Log gh pr create output for debugging (Issue #1462)
1014
1001
  if (argv.verbose) {
1015
1002
  await log(` gh pr create stdout: ${(output || '').trim() || '(empty)'}`, { verbose: true });
@@ -1279,6 +1266,9 @@ ${prBody}`,
1279
1266
  } else {
1280
1267
  throw new Error(`PR creation failed: ${cleanError}`, { cause: prCreateError });
1281
1268
  }
1269
+ } finally {
1270
+ if (prBodyFile) await fs.unlink(prBodyFile).catch(() => {});
1271
+ if (prTitleFile) await fs.unlink(prTitleFile).catch(() => {});
1282
1272
  }
1283
1273
  }
1284
1274
  }
@@ -11,6 +11,7 @@ import { reportError } from './sentry.lib.mjs';
11
11
 
12
12
  // Import GitHub error reporter
13
13
  import { handleErrorWithIssueCreation } from './github-error-reporter.lib.mjs';
14
+ import { sanitizeForPublication } from './token-sanitization.lib.mjs';
14
15
 
15
16
  export const isErrorIssueAutoCreationDisabled = argv => !!(argv?.disableReportIssue || argv?.disableIssueAutoCreationOnError);
16
17
 
@@ -115,7 +116,7 @@ export const handleFailure = async options => {
115
116
  if (argv.autoClosePullRequestOnFail && global.createdPR && global.createdPR.number) {
116
117
  await log('\n🔒 Auto-closing pull request due to failure...');
117
118
  try {
118
- const closeMessage = errorType === 'uncaughtException' ? 'Auto-closed due to uncaught exception. Logs have been attached for debugging.' : errorType === 'unhandledRejection' ? 'Auto-closed due to unhandled rejection. Logs have been attached for debugging.' : 'Auto-closed due to execution failure. Logs have been attached for debugging.';
119
+ const closeMessage = await sanitizeForPublication(errorType === 'uncaughtException' ? 'Auto-closed due to uncaught exception. Logs have been attached for debugging.' : errorType === 'unhandledRejection' ? 'Auto-closed due to unhandled rejection. Logs have been attached for debugging.' : 'Auto-closed due to execution failure. Logs have been attached for debugging.');
119
120
 
120
121
  const result = await $`gh pr close ${global.createdPR.number} --repo ${global.owner || owner}/${global.repo || repo} --comment ${closeMessage}`;
121
122
  if (result.exitCode === 0) {
@@ -27,6 +27,7 @@
27
27
  // Issue #1625: centralized markers + tracking helpers so the live-progress
28
28
  // comment is excluded from --auto-attach-solution-summary's AI-comment check.
29
29
  import { LIVE_PROGRESS_SECTION_START_MARKER, LIVE_PROGRESS_SECTION_END_MARKER, postTrackedCommentFromFile, trackToolCommentId } from './tool-comments.lib.mjs';
30
+ import { writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
30
31
 
31
32
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
32
33
  /**
@@ -225,9 +226,12 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
225
226
  // Edit existing comment
226
227
  const fs = (await import('fs')).promises;
227
228
  const tempFile = `/tmp/pr-progress-comment-${prNumber}-${Date.now()}.md`;
228
- await fs.writeFile(tempFile, progressSection);
229
- await $`gh api repos/${owner}/${repo}/issues/comments/${state.commentId} --method PATCH --field body=@${tempFile}`;
230
- await fs.unlink(tempFile).catch(() => {});
229
+ await writeSanitizedPublicationFile(tempFile, progressSection);
230
+ try {
231
+ await $`gh api repos/${owner}/${repo}/issues/comments/${state.commentId} --method PATCH --field body=@${tempFile}`;
232
+ } finally {
233
+ await fs.unlink(tempFile).catch(() => {});
234
+ }
231
235
  } else {
232
236
  // Create new comment. Issue #1625: post via postTrackedCommentFromFile
233
237
  // so the comment ID is captured directly from the GitHub API response
@@ -235,9 +239,13 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
235
239
  // posted comments from the "did the AI post anything?" check).
236
240
  const fs = (await import('fs')).promises;
237
241
  const tempFile = `/tmp/pr-progress-comment-${prNumber}-${Date.now()}.md`;
238
- await fs.writeFile(tempFile, progressSection);
239
- const posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber: prNumber, bodyFile: tempFile });
240
- await fs.unlink(tempFile).catch(() => {});
242
+ await writeSanitizedPublicationFile(tempFile, progressSection);
243
+ let posted;
244
+ try {
245
+ posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber: prNumber, bodyFile: tempFile });
246
+ } finally {
247
+ await fs.unlink(tempFile).catch(() => {});
248
+ }
241
249
 
242
250
  if (posted.ok && posted.commentId) {
243
251
  state.commentId = posted.commentId;
@@ -300,9 +308,12 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
300
308
  // Write to temp file and update PR
301
309
  const fs = (await import('fs')).promises;
302
310
  const tempBodyFile = `/tmp/pr-progress-${prNumber}-${Date.now()}.md`;
303
- await fs.writeFile(tempBodyFile, updatedBody);
304
- await $`gh pr edit ${prNumber} --repo ${owner}/${repo} --body-file ${tempBodyFile}`;
305
- await fs.unlink(tempBodyFile).catch(() => {});
311
+ await writeSanitizedPublicationFile(tempBodyFile, updatedBody);
312
+ try {
313
+ await $`gh pr edit ${prNumber} --repo ${owner}/${repo} --body-file ${tempBodyFile}`;
314
+ } finally {
315
+ await fs.unlink(tempBodyFile).catch(() => {});
316
+ }
306
317
 
307
318
  const stats = calculateProgress(todos);
308
319
  await log(`📊 Updated PR progress: ${stats.percentage}% (${stats.completed}/${stats.total} tasks completed)`);
@@ -31,10 +31,10 @@ const { sanitizeLogContent, attachLogToGitHub } = githubLib;
31
31
 
32
32
  // Issue #1745: process-wide sanitization counters used to print a one-line
33
33
  // "we masked N secrets" summary at the end of each run.
34
- const { formatSanitizationSummary } = await import('./token-sanitization.lib.mjs');
34
+ const { formatSanitizationSummary, sanitizeForPublication, writeSanitizedPublicationFile } = await import('./token-sanitization.lib.mjs');
35
35
  // Issue #1745: post-finish retroactive sanitization of bot-authored PR
36
- // comments and the PR description. Runs by default; can be skipped via
37
- // --dangerously-skip-output-sanitization.
36
+ // comments and the PR description. This external repair boundary always runs
37
+ // when PR coordinates are available.
38
38
  const { runPostFinishSweep } = await import('./post-finish-sanitization-sweep.lib.mjs');
39
39
 
40
40
  // Import continuation functions (session resumption, PR detection)
@@ -153,7 +153,7 @@ export const ensurePullRequestIssueLink = async ({ prNumber, issueNumber, owner,
153
153
 
154
154
  const fs = (await use('fs')).promises;
155
155
  const tempBodyFile = `/tmp/pr-body-update-${prNumber}-${Date.now()}.md`;
156
- await fs.writeFile(tempBodyFile, linkResult.body);
156
+ await writeSanitizedPublicationFile(tempBodyFile, linkResult.body);
157
157
 
158
158
  try {
159
159
  const updateResult = await command`gh pr edit ${prNumber} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
@@ -638,25 +638,19 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
638
638
  // Issue #1745: post-finish retroactive sanitization sweep. Re-reads
639
639
  // bot-authored PR comments and the PR description, runs them through
640
640
  // sanitizeOutput, and edits in place if a leak slipped past the live
641
- // sanitizer. Honors --dangerously-skip-output-sanitization and the related
642
- // active-tokens flag.
641
+ // sanitizer. Publication repair is a strict external boundary, so local
642
+ // diagnostic bypass flags never disable it.
643
643
  try {
644
644
  const owner = argv.owner;
645
645
  const repo = argv.repo;
646
646
  const prNumber = argv.prNumber;
647
- const skipOutputSanitization = argv['dangerously-skip-output-sanitization'] === true;
648
- const skipActiveTokensOutputSanitization = argv['dangerously-skip-active-tokens-output-sanitization'] === true;
649
- if (owner && repo && prNumber && !skipOutputSanitization) {
647
+ if (owner && repo && prNumber) {
650
648
  const sweepResult = await runPostFinishSweep({
651
649
  $,
652
650
  owner,
653
651
  repo,
654
652
  prNumber,
655
653
  log,
656
- sanitizationOptions: {
657
- warnOnMismatch: false,
658
- skipActiveTokensOutputSanitization,
659
- },
660
654
  });
661
655
  if (sweepResult.totalEdited > 0) {
662
656
  await log(`🔒 Post-finish sweep: edited ${sweepResult.totalEdited} bot-authored item(s) to mask leaked tokens.`);
@@ -771,7 +765,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
771
765
  // Skip cleanup if auto-restart-on-non-updated-pull-request-description is enabled
772
766
  // (let the agent handle it on restart instead)
773
767
  if (prTitleHasPlaceholder && !argv.autoRestartOnNonUpdatedPullRequestDescription) {
774
- const updatedTitle = pr.title.replace(/^\[WIP\]\s*/, '');
768
+ const updatedTitle = await sanitizeForPublication(pr.title.replace(/^\[WIP\]\s*/, ''));
775
769
  await log(` 📝 Removing [WIP] prefix from PR title...`);
776
770
  const titleResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --title "${updatedTitle}"`;
777
771
  if (titleResult.code === 0) {
@@ -819,7 +813,7 @@ Fixes ${issueRef}
819
813
  *This PR was created automatically by the AI issue solver*`;
820
814
 
821
815
  const tempBodyFile = `/tmp/pr-body-finalize-${pr.number}-${Date.now()}.md`;
822
- await fs.writeFile(tempBodyFile, newDescription);
816
+ await writeSanitizedPublicationFile(tempBodyFile, newDescription);
823
817
 
824
818
  try {
825
819
  const descResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
@@ -4,6 +4,9 @@
4
4
  import { exec } from 'child_process';
5
5
  import { promisify } from 'util';
6
6
  import { parseCliArgumentsWithLino } from './cli-arguments.lib.mjs';
7
+ import { setupStdioLogInterceptor } from './lib.mjs';
8
+
9
+ setupStdioLogInterceptor();
7
10
 
8
11
  const execAsync = promisify(exec);
9
12
 
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { spawn } from 'child_process';
4
4
  import { promises as fs } from 'fs';
5
5
  import { parseGitHubUrl } from './github.lib.mjs';
6
+ import { sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
6
7
 
7
8
  export const TASK_ISSUE_TITLE_MAX_LENGTH = 256;
8
9
 
@@ -216,9 +217,10 @@ export async function createTaskIssue({ repository, title, body, issueType = nul
216
217
  const bodyFile = path.join(tempDir, 'body.md');
217
218
 
218
219
  try {
219
- await fs.writeFile(bodyFile, body);
220
+ await writeSanitizedPublicationFile(bodyFile, body);
221
+ const sanitizedTitle = await sanitizeForPublication(title);
220
222
 
221
- const result = await run('gh', buildCreateIssueArgs({ repository, title, bodyFile, issueType, labels }));
223
+ const result = await run('gh', buildCreateIssueArgs({ repository, title: sanitizedTitle, bodyFile, issueType, labels }));
222
224
  if (result.code === 0) return parseCreatedTaskIssueOutput(result.stdout);
223
225
 
224
226
  const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
@@ -228,7 +230,7 @@ export async function createTaskIssue({ repository, title, body, issueType = nul
228
230
  }
229
231
 
230
232
  await log?.(`⚠️ Could not create issue with type/labels (${output || `exit code ${result.code}`}); retrying without them`);
231
- const retry = await run('gh', buildCreateIssueArgs({ repository, title, bodyFile }));
233
+ const retry = await run('gh', buildCreateIssueArgs({ repository, title: sanitizedTitle, bodyFile }));
232
234
  if (retry.code !== 0) {
233
235
  const retryOutput = `${retry.stderr || ''}${retry.stdout || ''}`.trim();
234
236
  throw new Error(retryOutput || `gh issue create exited with code ${retry.code}`);
package/src/task.mjs CHANGED
@@ -8,6 +8,11 @@ import { buildStartAgentArgs, resolveStartAgentCommand } from './task.agent-comm
8
8
  import { getDefaultTaskModel, parseTaskArguments } from './task.config.lib.mjs';
9
9
  import { validateModelName } from './models/index.mjs';
10
10
  import { appendOrReplaceParentSplitSection, buildAddSubIssueApiArgs, buildIssueRestIdApiArgs, buildTaskSplitPrompt, buildTaskSplitSystemPrompt, extractTaskSplitJson, formatChildIssueBody, normalizeSplitTasks, parseCreatedIssueUrl, parseTaskIssueUrl } from './task.split.lib.mjs';
11
+ import { setupStdioLogInterceptor } from './lib.mjs';
12
+ import { sanitizeCredentialText } from './credential-sanitization-core.lib.mjs';
13
+ import { sanitizeForPublication } from './token-sanitization.lib.mjs';
14
+
15
+ setupStdioLogInterceptor();
11
16
 
12
17
  const earlyArgs = process.argv.slice(2);
13
18
 
@@ -73,10 +78,14 @@ const logFile = path.join(scriptDir, `task-${timestamp}.log`);
73
78
  async function log(message, options = {}) {
74
79
  const { level = 'info', verbose = false } = options;
75
80
  if (verbose && !argv.verbose) return;
76
- await fs.appendFile(logFile, `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}\n`).catch(() => {});
77
- if (level === 'error') console.error(message);
78
- else if (level === 'warning' || level === 'warn') console.warn(message);
79
- else console.log(message);
81
+ const sanitizedMessage = sanitizeCredentialText(message);
82
+ await fs
83
+ .appendFile(logFile, `[${new Date().toISOString()}] [${level.toUpperCase()}] ${sanitizedMessage}\n`, { mode: 0o600 })
84
+ .then(() => fs.chmod(logFile, 0o600))
85
+ .catch(() => {});
86
+ if (level === 'error') console.error(sanitizedMessage);
87
+ else if (level === 'warning' || level === 'warn') console.warn(sanitizedMessage);
88
+ else console.log(sanitizedMessage);
80
89
  }
81
90
 
82
91
  function formatAligned(icon, label, value, indent = 0) {
@@ -172,7 +181,8 @@ async function fetchIssueRestId(issue) {
172
181
  }
173
182
 
174
183
  async function createChildIssue(parentIssue, task, index, splitCount) {
175
- const args = ['issue', 'create', '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--title', task.title, '--body', formatChildIssueBody({ parentIssue, task, index, splitCount })];
184
+ const [safeTitle, safeBody] = await Promise.all([sanitizeForPublication(task.title), sanitizeForPublication(formatChildIssueBody({ parentIssue, task, index, splitCount }))]);
185
+ const args = ['issue', 'create', '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--title', safeTitle, '--body', safeBody];
176
186
  if (parentIssue.labels.length > 0) {
177
187
  args.push('--label', parentIssue.labels.join(','));
178
188
  }
@@ -196,9 +206,11 @@ async function linkChildIssue(parentIssue, childIssue) {
196
206
 
197
207
  async function updateParentIssue(parentIssue, childIssues) {
198
208
  const body = appendOrReplaceParentSplitSection(parentIssue.body, childIssues);
199
- await commandOutput('gh', ['issue', 'edit', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', body]);
209
+ const safeBody = await sanitizeForPublication(body);
210
+ await commandOutput('gh', ['issue', 'edit', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', safeBody]);
200
211
  const childList = childIssues.map(issue => `- #${issue.number} ${issue.title}`).join('\n');
201
- await commandOutput('gh', ['issue', 'comment', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', `Split into ${childIssues.length} tasks:\n\n${childList}`]);
212
+ const safeComment = await sanitizeForPublication(`Split into ${childIssues.length} tasks:\n\n${childList}`);
213
+ await commandOutput('gh', ['issue', 'comment', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', safeComment]);
202
214
  }
203
215
 
204
216
  async function runSplitMode() {
@@ -268,7 +280,8 @@ ${results.clarification ? `Clarification analysis:\n${results.clarification}\n\n
268
280
  }
269
281
 
270
282
  try {
271
- await fs.writeFile(logFile, `# Task Log - ${new Date().toISOString()}\n\n`);
283
+ await fs.writeFile(logFile, `# Task Log - ${new Date().toISOString()}\n\n`, { mode: 0o600 });
284
+ await fs.chmod(logFile, 0o600);
272
285
  await log(`📁 Log file: ${logFile}`);
273
286
  await log('\n🎯 Task Processing Started');
274
287
  await log(formatAligned('📝', 'Task input:', taskInput));
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { ensureUseM } from './use-m-bootstrap.lib.mjs';
3
+ import { maskToken, setupStdioLogInterceptor } from './lib.mjs';
4
+
5
+ setupStdioLogInterceptor();
3
6
  // Early exit for --version (issue #1318: avoid dotenvx MISSING_ENV_FILE warnings)
4
7
  if (process.argv.includes('--version')) {
5
8
  const v = await import('./version.lib.mjs').then(m => m.getVersion()).catch(() => 'unknown');
@@ -212,7 +215,7 @@ if (hiveEnabled && hiveOverrides.length > 0) {
212
215
  if (config.dryRun) {
213
216
  console.log('\n✅ Dry-run mode: All validations passed successfully!');
214
217
  console.log('\nConfiguration summary:');
215
- console.log(' Token:', BOT_TOKEN ? `${BOT_TOKEN.substring(0, 10)}...` : 'not set');
218
+ console.log(' Token:', BOT_TOKEN ? maskToken(BOT_TOKEN) : 'not set');
216
219
  if (allowedChats && allowedChats.length > 0) {
217
220
  console.log(' Allowed chats:', lino.format(allowedChats));
218
221
  } else {
@@ -21,8 +21,10 @@
21
21
  */
22
22
 
23
23
  import path from 'path';
24
+ import os from 'os';
24
25
  import fs from 'fs/promises';
25
26
  import { constants as fsConstants } from 'fs';
27
+ import { sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
26
28
 
27
29
  const UUID_RE = /\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/i;
28
30
  const ISOLATION_BACKENDS = new Set(['screen', 'tmux', 'docker']);
@@ -30,6 +32,29 @@ const ISOLATION_BACKENDS = new Set(['screen', 'tmux', 'docker']);
30
32
  // https://core.telegram.org/bots/api#senddocument
31
33
  const TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
32
34
 
35
+ /**
36
+ * Build a private, sanitized upload artifact without modifying the raw audit
37
+ * log. The returned cleanup function must be called after the network request.
38
+ */
39
+ async function prepareSanitizedLogUpload(logPath, caption) {
40
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hive-mind-telegram-log-'));
41
+ await fs.chmod(tempDir, 0o700);
42
+ const sanitizedPath = path.join(tempDir, path.basename(logPath));
43
+
44
+ try {
45
+ const [rawLog, safeCaption] = await Promise.all([fs.readFile(logPath, 'utf8'), sanitizeForPublication(caption)]);
46
+ await writeSanitizedPublicationFile(sanitizedPath, rawLog);
47
+ return {
48
+ path: sanitizedPath,
49
+ caption: safeCaption,
50
+ cleanup: () => fs.rm(tempDir, { recursive: true, force: true }),
51
+ };
52
+ } catch (error) {
53
+ await fs.rm(tempDir, { recursive: true, force: true });
54
+ throw error;
55
+ }
56
+ }
57
+
33
58
  /**
34
59
  * Extract the first RFC 4122 v4-shaped UUID found in `text`.
35
60
  *
@@ -305,11 +330,15 @@ export async function registerLogCommand(bot, options) {
305
330
 
306
331
  if (decision.destination === 'chat') {
307
332
  // Public repository → reply with the document directly in the chat.
333
+ let upload;
308
334
  try {
309
- await ctx.replyWithDocument({ source: logPath, filename }, { reply_to_message_id: message.message_id, caption, parse_mode: 'Markdown' });
335
+ upload = await prepareSanitizedLogUpload(logPath, caption);
336
+ await ctx.replyWithDocument({ source: upload.path, filename }, { reply_to_message_id: message.message_id, caption: upload.caption, parse_mode: 'Markdown' });
310
337
  } catch (error) {
311
338
  console.error('[ERROR] /log: replyWithDocument failed:', error);
312
- await ctx.reply(`❌ Failed to upload log: ${error.message || String(error)}`, { reply_to_message_id: message.message_id });
339
+ await ctx.reply('❌ Failed to sanitize or upload the log.', { reply_to_message_id: message.message_id });
340
+ } finally {
341
+ await upload?.cleanup();
313
342
  }
314
343
  return;
315
344
  }
@@ -348,16 +377,21 @@ export async function registerLogCommand(bot, options) {
348
377
  console.error('[ERROR] /log: DM forwarding step failed:', error);
349
378
  }
350
379
 
380
+ let upload;
351
381
  try {
382
+ upload = await prepareSanitizedLogUpload(logPath, caption);
352
383
  const replyOpts = forwardedMessageId ? { reply_to_message_id: forwardedMessageId, caption, parse_mode: 'Markdown' } : { caption, parse_mode: 'Markdown' };
353
- await ctx.telegram.sendDocument(userId, { source: logPath, filename }, replyOpts);
384
+ replyOpts.caption = upload.caption;
385
+ await ctx.telegram.sendDocument(userId, { source: upload.path, filename }, replyOpts);
354
386
  } catch (error) {
355
387
  console.error('[ERROR] /log: sendDocument to DM failed:', error);
356
388
  // Tell the user, in their original chat, that DM delivery failed
357
389
  // (commonly because they have not started a chat with the bot).
358
- const friendly = error?.code === 403 || /chat not found|bot can't initiate conversation/i.test(error?.message || '') ? 'I could not send you a DM. Please open a private chat with me and send /start, then try again.' : `Failed to send the log via DM: ${error.message || String(error)}`;
390
+ const friendly = error?.code === 403 || /chat not found|bot can't initiate conversation/i.test(error?.message || '') ? 'I could not send you a DM. Please open a private chat with me and send /start, then try again.' : 'Failed to sanitize or send the log via DM.';
359
391
  await ctx.reply(`❌ ${friendly}`, { reply_to_message_id: message.message_id });
360
392
  return;
393
+ } finally {
394
+ await upload?.cleanup();
361
395
  }
362
396
 
363
397
  // Acknowledge in the original chat (only if it wasn't already a DM).
@@ -1,4 +1,5 @@
1
1
  import { normalizeLocale, t } from './i18n.lib.mjs';
2
+ import { sanitizeForPublication } from './token-sanitization.lib.mjs';
2
3
 
3
4
  const FORMATTING_FALLBACK_INSTALLED = Symbol.for('hiveMind.telegramFormattingFallbackInstalled');
4
5
  const DEFAULT_FORMATTING_FALLBACK_WARNING = '⚠️ Formatting error detected. Showing plain text fallback.';
@@ -385,7 +386,7 @@ export async function safeReply(ctx, text, options = {}) {
385
386
  const { telegramOptions, fallbackLocale, verbose } = splitOptions(options);
386
387
  const firstOptions = { parse_mode: 'Markdown', ...telegramOptions };
387
388
  return await sendTelegramTextChunks({
388
- text,
389
+ text: await sanitizeForPublication(text),
389
390
  telegramOptions: firstOptions,
390
391
  fallbackLocale,
391
392
  verbose,
@@ -398,7 +399,7 @@ export async function safeEditMessageText(telegram, chatId, messageId, inlineMes
398
399
  const { telegramOptions, fallbackLocale, verbose } = splitOptions(options);
399
400
  const firstOptions = { parse_mode: 'Markdown', ...telegramOptions };
400
401
  return await editTelegramTextChunks({
401
- text,
402
+ text: await sanitizeForPublication(text),
402
403
  telegramOptions: firstOptions,
403
404
  fallbackLocale,
404
405
  verbose,
@@ -419,9 +420,10 @@ function wrapTelegramSendMessage(telegram, defaults = {}) {
419
420
  args[2] = telegramOptions;
420
421
 
421
422
  if (typeof text !== 'string') return await original.apply(this, args);
423
+ const sanitizedText = await sanitizeForPublication(text);
422
424
 
423
425
  return await sendTelegramTextChunks({
424
- text,
426
+ text: sanitizedText,
425
427
  telegramOptions,
426
428
  fallbackLocale: fallbackLocale || defaults.fallbackLocale,
427
429
  verbose: verbose || defaults.verbose,
@@ -447,9 +449,10 @@ function wrapTelegramEditMessageText(telegram, defaults = {}) {
447
449
  args[4] = telegramOptions;
448
450
 
449
451
  if (typeof text !== 'string') return await original.apply(this, args);
452
+ const sanitizedText = await sanitizeForPublication(text);
450
453
 
451
454
  return await editTelegramTextChunks({
452
- text,
455
+ text: sanitizedText,
453
456
  telegramOptions,
454
457
  fallbackLocale: fallbackLocale || defaults.fallbackLocale,
455
458
  verbose: verbose || defaults.verbose,
@@ -66,7 +66,7 @@ const isOperatorOfAnyAllowedChat = async ({ telegram, userId, allowedChatIds })
66
66
 
67
67
  /**
68
68
  * Format the token list for display. Each line: `name (source): masked`.
69
- * The masked form is `first-3 *** last-3` per maskToken's new default.
69
+ * The masked form is `first-3last-3` per maskToken's default.
70
70
  */
71
71
  export const formatTokenList = tokens => {
72
72
  if (!tokens || tokens.length === 0) {