@link-assistant/hive-mind 1.69.14 → 1.69.16
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.69.16
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ca0d938: Fix Telegram work-session completion failing with "Bad Request: can't parse entities" when the discovered Pull request URL contained Markdown-significant characters (`_`, `*`, `` ` ``, `[`). `appendPullRequestLine` (issue #1688) inserted the raw URL into a Markdown message even though the surrounding `Issue:` line was already escaped by `buildTelegramInfoBlock`, so a repo slug like `save_visiogetbb/pull/8` opened an italic entity at byte offset 318 that never closed. The appended `Pull request:` line is now passed through `escapeMarkdown`, and `safeReply`/`safeEditMessageText`/`installTelegramFormattingFallback` now log the offending byte-offset window and the plain-text fallback under `--verbose` so future parse errors point straight to the unescaped character. Resolves #1801.
|
|
8
|
+
|
|
9
|
+
## 1.69.15
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- fbda6de: Fix `--auto-fork` failing on private repositories with read-only access when forking is allowed. `handleAutoForkOption` now probes the `allow_forking` repository attribute before bailing out: when it is `true`, fork mode is enabled (the same behaviour already used for public repos without write access); when it is explicitly `false`, the fatal exit explains that direct branch mode needs push/write access, fork mode is disabled, and the maintainer must either grant Write access or enable private forking; when it cannot be determined, we fall through with a verbose warning so `gh repo fork` can produce a precise downstream error. Resolves #1795.
|
|
14
|
+
|
|
3
15
|
## 1.69.14
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -24,9 +24,41 @@ const { log, ghCmdRetry } = lib;
|
|
|
24
24
|
const githubLib = await import('./github.lib.mjs');
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Probe whether the repository allows forking via the GitHub API
|
|
28
|
+
* (`allow_forking` is true for repos that can be forked by users with read
|
|
29
|
+
* access — including private repositories). Returns `null` when the
|
|
30
|
+
* attribute could not be determined so callers can decide a safe default.
|
|
31
|
+
*
|
|
32
|
+
* Kept here (instead of github.lib.mjs) because it's only used by the
|
|
33
|
+
* auto-fork branch and the existing `detectRepositoryVisibility` already
|
|
34
|
+
* makes a separate API call; this avoids reshuffling shared helpers.
|
|
35
|
+
*
|
|
36
|
+
* Issue #1795: a private repository with read-only access can still be
|
|
37
|
+
* forked when `allow_forking` is true, so failing early was overly
|
|
38
|
+
* conservative.
|
|
39
|
+
*/
|
|
40
|
+
async function detectAllowForking(owner, repo) {
|
|
41
|
+
const result = await ghCmdRetry(() => $`gh api repos/${owner}/${repo} --jq .allow_forking`, { label: `allow_forking ${owner}/${repo}` });
|
|
42
|
+
if (result.code !== 0) return null;
|
|
43
|
+
const raw = result.stdout.toString().trim();
|
|
44
|
+
if (raw === 'true') return true;
|
|
45
|
+
if (raw === 'false') return false;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function describeRepoPermissionLevel(permissions) {
|
|
50
|
+
if (permissions.admin === true) return 'Admin';
|
|
51
|
+
if (permissions.maintain === true) return 'Maintain';
|
|
52
|
+
if (permissions.push === true) return 'Write';
|
|
53
|
+
if (permissions.triage === true) return 'Triage';
|
|
54
|
+
if (permissions.pull === true) return 'Read';
|
|
55
|
+
return 'No confirmed repository access';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handle the --auto-fork option: when the user lacks write access, attempt
|
|
60
|
+
* to enable fork mode (including for private repositories where
|
|
61
|
+
* `allow_forking` is true). Only fail when forking is also unavailable.
|
|
30
62
|
*
|
|
31
63
|
* Mutates argv.fork in place when fork mode is enabled.
|
|
32
64
|
*
|
|
@@ -51,19 +83,52 @@ export async function handleAutoForkOption({ owner, repo, argv, safeExit }) {
|
|
|
51
83
|
const { isPublic } = await detectRepositoryVisibility(owner, repo);
|
|
52
84
|
|
|
53
85
|
if (!isPublic) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
await
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
// Issue #1795: read access to a private repo is enough to fork it
|
|
87
|
+
// when the upstream allows forking. Probe `allow_forking` before
|
|
88
|
+
// bailing out so users with limited (read-only) access can still
|
|
89
|
+
// proceed via their own fork.
|
|
90
|
+
const allowForking = await detectAllowForking(owner, repo);
|
|
91
|
+
if (allowForking === false) {
|
|
92
|
+
const permissionLevel = describeRepoPermissionLevel(permissions);
|
|
93
|
+
|
|
94
|
+
await log('');
|
|
95
|
+
await log("❌ --auto-fork failed: Repository is private, you don't have write access, and forking is disabled", { level: 'error' });
|
|
96
|
+
await log('');
|
|
97
|
+
await log(' 🔍 What happened:', { level: 'error' });
|
|
98
|
+
await log(` Repository ${owner}/${repo} is private`, { level: 'error' });
|
|
99
|
+
await log(` Your detected GitHub repository access level is ${permissionLevel}`, { level: 'error' });
|
|
100
|
+
await log(` API permissions: ${JSON.stringify(permissions)}`, { level: 'error' });
|
|
101
|
+
await log(' Direct branch mode requires push/write access, but permissions.push is false', { level: 'error' });
|
|
102
|
+
await log(" Fork mode is also unavailable because the repository owner disabled private forking ('allow_forking' is false)", {
|
|
103
|
+
level: 'error',
|
|
104
|
+
});
|
|
105
|
+
await log('');
|
|
106
|
+
await log(' 💡 Solution:', { level: 'error' });
|
|
107
|
+
await log(' • To let Hive Mind work directly in this repository:', { level: 'error' });
|
|
108
|
+
await log(` Ask an owner/admin to open https://github.com/${owner}/${repo}/settings/access`, { level: 'error' });
|
|
109
|
+
await log(' Then add this GitHub account or its team with the Write role (Maintain/Admin also works)', { level: 'error' });
|
|
110
|
+
await log(' • To let Hive Mind work through a fork instead:', { level: 'error' });
|
|
111
|
+
await log(` Ask an owner/admin to open https://github.com/${owner}/${repo}/settings`, { level: 'error' });
|
|
112
|
+
await log(' Then enable Settings -> General -> Features -> Allow forking', { level: 'error' });
|
|
113
|
+
await log(' For organization-owned private repositories, the organization must also allow private repository forks', {
|
|
114
|
+
level: 'error',
|
|
115
|
+
});
|
|
116
|
+
await log('');
|
|
117
|
+
await safeExit(1, 'Auto-fork failed - private repository without access and forking is disabled');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (allowForking === true) {
|
|
122
|
+
await log('✅ Auto-fork: Read-only access to private repository, enabling fork mode (allow_forking=true)');
|
|
123
|
+
} else {
|
|
124
|
+
await log("✅ Auto-fork: Read-only access to private repository, attempting fork mode (allow_forking couldn't be confirmed)");
|
|
125
|
+
await log(" ⚠️ Could not determine 'allow_forking' for the private repository; letting gh repo fork report the exact result", {
|
|
126
|
+
verbose: true,
|
|
127
|
+
level: 'warning',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
argv.fork = true;
|
|
67
132
|
return;
|
|
68
133
|
}
|
|
69
134
|
|
|
@@ -49,11 +49,30 @@ export function buildTelegramFormattingFallbackText(text, options = {}) {
|
|
|
49
49
|
return `${getFormattingFallbackWarning(locale)}\n\n${stripTelegramMarkdown(text)}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function logFormattingFailure(scope, error, text, verbose = false) {
|
|
52
|
+
function logFormattingFailure(scope, error, text, verbose = false, fallbackText = null) {
|
|
53
53
|
const message = error?.description || error?.message || String(error || '');
|
|
54
54
|
console.error(`[telegram-bot] ${scope}: formatted Telegram message failed: ${message}`);
|
|
55
55
|
if (verbose) {
|
|
56
|
-
|
|
56
|
+
const originalBytes = Buffer.byteLength(String(text ?? ''), 'utf-8');
|
|
57
|
+
console.error(`[telegram-bot] ${scope}: Failing message (${originalBytes} bytes): ${text}`);
|
|
58
|
+
// Issue #1801: when the parser rejects an entity, surface the byte offset
|
|
59
|
+
// from the error along with a small window of the message around it to
|
|
60
|
+
// make pinpointing the offending character trivial on the next iteration.
|
|
61
|
+
const offsetMatch = /byte offset (\d+)/i.exec(message);
|
|
62
|
+
if (offsetMatch) {
|
|
63
|
+
const offset = Number(offsetMatch[1]);
|
|
64
|
+
const buf = Buffer.from(String(text ?? ''), 'utf-8');
|
|
65
|
+
const start = Math.max(0, offset - 32);
|
|
66
|
+
const end = Math.min(buf.length, offset + 32);
|
|
67
|
+
// Decode the window; replacement character is used for bytes that fall
|
|
68
|
+
// mid-codepoint so we still print *something* useful.
|
|
69
|
+
const window = buf.slice(start, end).toString('utf-8');
|
|
70
|
+
console.error(`[telegram-bot] ${scope}: Byte offset ${offset} context [${start}..${end}]: ${JSON.stringify(window)}`);
|
|
71
|
+
}
|
|
72
|
+
if (fallbackText !== null) {
|
|
73
|
+
const fallbackBytes = Buffer.byteLength(String(fallbackText ?? ''), 'utf-8');
|
|
74
|
+
console.error(`[telegram-bot] ${scope}: Fallback message (${fallbackBytes} bytes): ${fallbackText}`);
|
|
75
|
+
}
|
|
57
76
|
}
|
|
58
77
|
}
|
|
59
78
|
|
|
@@ -65,8 +84,8 @@ export async function safeReply(ctx, text, options = {}) {
|
|
|
65
84
|
return await ctx.reply(text, firstOptions);
|
|
66
85
|
} catch (error) {
|
|
67
86
|
if (!isTelegramFormattingError(error)) throw error;
|
|
68
|
-
logFormattingFailure('safeReply', error, text, verbose);
|
|
69
87
|
const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale });
|
|
88
|
+
logFormattingFailure('safeReply', error, text, verbose, fallbackText);
|
|
70
89
|
return await ctx.reply(fallbackText, { ...telegramOptions, parse_mode: undefined });
|
|
71
90
|
}
|
|
72
91
|
}
|
|
@@ -78,8 +97,8 @@ export async function safeEditMessageText(telegram, chatId, messageId, inlineMes
|
|
|
78
97
|
return await telegram.editMessageText(chatId, messageId, inlineMessageId, text, firstOptions);
|
|
79
98
|
} catch (error) {
|
|
80
99
|
if (!isTelegramFormattingError(error)) throw error;
|
|
81
|
-
logFormattingFailure('safeEditMessageText', error, text, verbose);
|
|
82
100
|
const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale });
|
|
101
|
+
logFormattingFailure('safeEditMessageText', error, text, verbose, fallbackText);
|
|
83
102
|
return await telegram.editMessageText(chatId, messageId, inlineMessageId, fallbackText, { ...telegramOptions, parse_mode: undefined });
|
|
84
103
|
}
|
|
85
104
|
}
|
|
@@ -98,9 +117,10 @@ function wrapTelegramMethod(telegram, methodName, textIndex, optionsIndex, defau
|
|
|
98
117
|
return await original.apply(this, args);
|
|
99
118
|
} catch (error) {
|
|
100
119
|
if (!isTelegramFormattingError(error) || typeof text !== 'string') throw error;
|
|
101
|
-
|
|
120
|
+
const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale: fallbackLocale || defaults.fallbackLocale });
|
|
121
|
+
logFormattingFailure(methodName, error, text, verbose || defaults.verbose, fallbackText);
|
|
102
122
|
const retryArgs = [...args];
|
|
103
|
-
retryArgs[textIndex] =
|
|
123
|
+
retryArgs[textIndex] = fallbackText;
|
|
104
124
|
retryArgs[optionsIndex] = { ...telegramOptions, parse_mode: undefined };
|
|
105
125
|
return await original.apply(this, retryArgs);
|
|
106
126
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { t } from './i18n.lib.mjs';
|
|
2
|
+
import { escapeMarkdown } from './telegram-markdown.lib.mjs';
|
|
2
3
|
|
|
3
4
|
const FAILURE_STATUSES = new Set(['failed', 'cancelled', 'canceled', 'error']);
|
|
4
5
|
|
|
@@ -74,7 +75,7 @@ export function formatExecutingWorkSessionMessage({ sessionName = 'unknown', iso
|
|
|
74
75
|
*/
|
|
75
76
|
export function appendPullRequestLine(infoBlock, pullRequestUrl, { locale = null } = {}) {
|
|
76
77
|
if (!pullRequestUrl || !infoBlock) return infoBlock || '';
|
|
77
|
-
if (infoBlock.includes(pullRequestUrl)) return infoBlock;
|
|
78
|
+
if (infoBlock.includes(pullRequestUrl) || infoBlock.includes(escapeMarkdown(pullRequestUrl))) return infoBlock;
|
|
78
79
|
|
|
79
80
|
const lines = infoBlock.split('\n');
|
|
80
81
|
let lastUrlLineIdx = -1;
|
|
@@ -89,7 +90,10 @@ export function appendPullRequestLine(infoBlock, pullRequestUrl, { locale = null
|
|
|
89
90
|
lastUrlLineIdx = i;
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
// Issue #1801: escape underscores/asterisks so Markdown parser doesn't open
|
|
94
|
+
// an entity on URLs like .../save_visiogetbb/pull/8 that the Issue: line
|
|
95
|
+
// above already had escaped at buildTelegramInfoBlock time.
|
|
96
|
+
const prLine = `${text(locale, 'telegram.info_pull_request_label', 'Pull request')}: ${escapeMarkdown(pullRequestUrl)}`;
|
|
93
97
|
if (lastUrlLineIdx === -1) {
|
|
94
98
|
return `${infoBlock}\n${prLine}`;
|
|
95
99
|
}
|