@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.
- package/CHANGELOG.md +6 -0
- package/README.hi.md +2 -0
- package/README.md +2 -0
- package/README.ru.md +2 -0
- package/README.zh.md +2 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +0 -4
- package/src/cleanup.mjs +18 -6
- package/src/codex.lib.mjs +0 -4
- package/src/configure-claude.mjs +3 -0
- package/src/credential-sanitization-core.lib.mjs +231 -0
- package/src/development-log.lib.mjs +39 -6
- package/src/fix.mjs +3 -0
- package/src/github-error-reporter.lib.mjs +13 -8
- package/src/github-issue-auto-close.lib.mjs +2 -1
- package/src/github-merge-issue-close.lib.mjs +2 -1
- package/src/github.lib.mjs +29 -18
- package/src/hive-screens.mjs +3 -0
- package/src/instrument.mjs +14 -0
- package/src/interactive-mode.lib.mjs +25 -40
- package/src/lib.mjs +89 -50
- package/src/log-upload.lib.mjs +22 -4
- package/src/post-finish-sanitization-sweep.lib.mjs +5 -5
- package/src/review.mjs +3 -1
- package/src/sentry.lib.mjs +27 -8
- package/src/solve.auto-pr.lib.mjs +11 -21
- package/src/solve.error-handlers.lib.mjs +2 -1
- package/src/solve.progress-monitoring.lib.mjs +20 -9
- package/src/solve.results.lib.mjs +9 -15
- package/src/start-screen.mjs +3 -0
- package/src/task.issue-creation.lib.mjs +5 -3
- package/src/task.mjs +21 -8
- package/src/telegram-bot.mjs +4 -1
- package/src/telegram-log-command.lib.mjs +38 -4
- package/src/telegram-safe-reply.lib.mjs +7 -4
- package/src/telegram-tokens-command.lib.mjs +1 -1
- package/src/token-sanitization.lib.mjs +177 -14
- package/src/tool-comments.lib.mjs +5 -5
- package/src/youtrack/youtrack-sync.mjs +4 -3
package/src/sentry.lib.mjs
CHANGED
|
@@ -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
|
-
|
|
912
|
-
await
|
|
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
|
-
|
|
919
|
-
await
|
|
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
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
37
|
-
//
|
|
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
|
|
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.
|
|
642
|
-
//
|
|
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
|
-
|
|
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
|
|
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}"`;
|
package/src/start-screen.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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));
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -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 ?
|
|
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
|
|
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(
|
|
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
|
-
|
|
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.' :
|
|
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
|
|
69
|
+
* The masked form is `first-3…last-3` per maskToken's default.
|
|
70
70
|
*/
|
|
71
71
|
export const formatTokenList = tokens => {
|
|
72
72
|
if (!tokens || tokens.length === 0) {
|