@link-assistant/hive-mind 2.7.4 → 2.8.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 +34 -0
- package/README.hi.md +22 -0
- package/README.md +24 -0
- package/README.ru.md +21 -0
- package/README.zh.md +20 -0
- package/package.json +3 -2
- package/src/fix.ci-cd.lib.mjs +480 -0
- package/src/fix.mjs +242 -0
- package/src/locales/en.lino +6 -1
- package/src/locales/hi.lino +6 -1
- package/src/locales/ru.lino +6 -1
- package/src/locales/zh.lino +6 -1
- package/src/task.issue-creation.lib.mjs +31 -5
- package/src/telegram-bot.mjs +10 -85
- package/src/telegram-fix-command.lib.mjs +164 -0
- package/src/telegram-ui-messages.lib.mjs +11 -1
- package/src/telegram.config.lib.mjs +96 -0
package/src/fix.mjs
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `/fix` command (issue #1733).
|
|
5
|
+
*
|
|
6
|
+
* Currently implements `--ci-cd`: automatically generate a CI/CD remediation
|
|
7
|
+
* issue for a target repository and (optionally) hand it off to
|
|
8
|
+
* `/solve --development-log --deep-analysis --auto-merge`.
|
|
9
|
+
*
|
|
10
|
+
* fix.mjs <github-repository-url> --ci-cd [solve options...]
|
|
11
|
+
*
|
|
12
|
+
* Every option `/fix` does not consume itself (e.g. --tool, --model, --think)
|
|
13
|
+
* is forwarded to `/solve`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { spawn } from 'child_process';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { CI_CD_ISSUE_LABELS, CI_CD_ISSUE_TYPE, buildCiCdIssueBody, buildCiCdIssueTitle, buildSolveArgs, partitionFixArgs, summarizeRunFailures } from './fix.ci-cd.lib.mjs';
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
|
|
23
|
+
function printHelp() {
|
|
24
|
+
console.log(`Usage: fix.mjs <github-repository-url> --ci-cd [options]
|
|
25
|
+
|
|
26
|
+
Automatically generate a CI/CD remediation issue for a repository and hand it
|
|
27
|
+
off to /solve --development-log --deep-analysis --auto-merge.
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--ci-cd Generate a CI/CD remediation issue (required mode)
|
|
31
|
+
--dry-run Print the issue that would be created without creating it
|
|
32
|
+
--no-solve Create the issue but do not start /solve on it
|
|
33
|
+
--version Show version number
|
|
34
|
+
--help, -h Show help
|
|
35
|
+
|
|
36
|
+
All other options (e.g. --tool, --model, --think) are forwarded to /solve.
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
fix.mjs https://github.com/owner/repo --ci-cd
|
|
40
|
+
fix.mjs https://github.com/owner/repo --ci-cd --tool codex --model gpt-5.5
|
|
41
|
+
fix.mjs owner/repo --ci-cd --think max --no-solve`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runCommand(command, args, options = {}) {
|
|
45
|
+
return new Promise(resolve => {
|
|
46
|
+
const child = spawn(command, args, {
|
|
47
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
|
+
env: process.env,
|
|
49
|
+
...options,
|
|
50
|
+
});
|
|
51
|
+
let stdout = '';
|
|
52
|
+
let stderr = '';
|
|
53
|
+
child.stdout.on('data', data => {
|
|
54
|
+
stdout += data.toString();
|
|
55
|
+
});
|
|
56
|
+
child.stderr.on('data', data => {
|
|
57
|
+
stderr += data.toString();
|
|
58
|
+
});
|
|
59
|
+
child.on('error', error => {
|
|
60
|
+
resolve({ code: 1, stdout, stderr: stderr || error.message });
|
|
61
|
+
});
|
|
62
|
+
child.on('close', code => {
|
|
63
|
+
resolve({ code, stdout, stderr });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function commandOutput(command, args) {
|
|
69
|
+
const result = await runCommand(command, args);
|
|
70
|
+
if (result.code !== 0) {
|
|
71
|
+
const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
|
|
72
|
+
throw new Error(output || `${command} exited with code ${result.code}`);
|
|
73
|
+
}
|
|
74
|
+
return result.stdout.trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function detectLanguages(repository) {
|
|
78
|
+
try {
|
|
79
|
+
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/languages`]);
|
|
80
|
+
return JSON.parse(json);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.warn(`⚠️ Could not detect languages: ${error.message}`);
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function getDefaultBranch(repository) {
|
|
88
|
+
try {
|
|
89
|
+
return await commandOutput('gh', ['api', `repos/${repository.fullName}`, '--jq', '.default_branch']);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.warn(`⚠️ Could not determine default branch: ${error.message}`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function getLatestCommit(repository, branch) {
|
|
97
|
+
if (!branch) return null;
|
|
98
|
+
try {
|
|
99
|
+
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/commits/${branch}`, '--jq', '{sha: .sha, message: .commit.message, url: .html_url}']);
|
|
100
|
+
return JSON.parse(json);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.warn(`⚠️ Could not fetch latest commit: ${error.message}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const RUNS_JQ = '[.workflow_runs[] | {name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha}]';
|
|
108
|
+
|
|
109
|
+
async function getRunsForCommit(repository, sha) {
|
|
110
|
+
if (!sha) return [];
|
|
111
|
+
try {
|
|
112
|
+
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/actions/runs?head_sha=${sha}&per_page=100`, '--jq', RUNS_JQ]);
|
|
113
|
+
const parsed = JSON.parse(json);
|
|
114
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.warn(`⚠️ Could not fetch CI/CD runs: ${error.message}`);
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function getRecentBranchRuns(repository, branch) {
|
|
122
|
+
if (!branch) return [];
|
|
123
|
+
try {
|
|
124
|
+
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/actions/runs?branch=${encodeURIComponent(branch)}&per_page=20`, '--jq', RUNS_JQ]);
|
|
125
|
+
const parsed = JSON.parse(json);
|
|
126
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn(`⚠️ Could not fetch recent CI/CD runs for branch ${branch}: ${error.message}`);
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveSolveCommand() {
|
|
134
|
+
return path.join(__dirname, 'solve.mjs');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function main() {
|
|
138
|
+
const rawArgs = process.argv.slice(2);
|
|
139
|
+
|
|
140
|
+
if (rawArgs.includes('--version')) {
|
|
141
|
+
const { getVersion } = await import('./version.lib.mjs');
|
|
142
|
+
try {
|
|
143
|
+
console.log(await getVersion());
|
|
144
|
+
} catch {
|
|
145
|
+
console.error('Error: Unable to determine version');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (rawArgs.length === 0 || rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
152
|
+
printHelp();
|
|
153
|
+
process.exit(rawArgs.length === 0 ? 1 : 0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const parsed = partitionFixArgs(rawArgs);
|
|
157
|
+
|
|
158
|
+
if (!parsed.ciCd) {
|
|
159
|
+
console.error('❌ /fix currently supports only --ci-cd mode. Pass --ci-cd to continue.');
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!parsed.repository) {
|
|
164
|
+
console.error('❌ Missing or invalid GitHub repository URL. Provide it as the first argument, e.g. fix.mjs https://github.com/owner/repo --ci-cd');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const repository = parsed.repository;
|
|
169
|
+
console.log(`🔧 /fix --ci-cd for ${repository.fullName}`);
|
|
170
|
+
|
|
171
|
+
const [languages, defaultBranch] = await Promise.all([detectLanguages(repository), getDefaultBranch(repository)]);
|
|
172
|
+
const commit = await getLatestCommit(repository, defaultBranch);
|
|
173
|
+
let runs = await getRunsForCommit(repository, commit?.sha);
|
|
174
|
+
let runsSource = 'commit';
|
|
175
|
+
|
|
176
|
+
// Release/tag commits frequently produce no runs of their own. Fall back to
|
|
177
|
+
// the most recent runs on the default branch so the issue stays actionable.
|
|
178
|
+
if (runs.length === 0) {
|
|
179
|
+
const branchRuns = await getRecentBranchRuns(repository, defaultBranch);
|
|
180
|
+
if (branchRuns.length > 0) {
|
|
181
|
+
runs = branchRuns;
|
|
182
|
+
runsSource = 'branch';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { total, failing } = summarizeRunFailures(runs);
|
|
187
|
+
console.log(` Default branch: ${defaultBranch || 'unknown'}`);
|
|
188
|
+
console.log(` Latest commit: ${commit?.sha ? commit.sha.slice(0, 7) : 'unknown'}`);
|
|
189
|
+
console.log(` CI/CD runs: ${total} (${failing} not passing)${runsSource === 'branch' ? ' [recent branch runs]' : ''}`);
|
|
190
|
+
|
|
191
|
+
const title = buildCiCdIssueTitle();
|
|
192
|
+
const body = buildCiCdIssueBody({ repository, defaultBranch, commit, runs, languages, runsSource });
|
|
193
|
+
|
|
194
|
+
if (parsed.dryRun) {
|
|
195
|
+
console.log('\n--- DRY RUN: issue that would be created ---\n');
|
|
196
|
+
console.log(`Title: ${title}\n`);
|
|
197
|
+
console.log(body);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log('\n📝 Creating remediation issue...');
|
|
202
|
+
const { createTaskIssue } = await import('./task.issue-creation.lib.mjs');
|
|
203
|
+
const issue = await createTaskIssue({
|
|
204
|
+
repository,
|
|
205
|
+
title,
|
|
206
|
+
body,
|
|
207
|
+
// The Bug type is what makes /solve --deep-analysis emit the root-cause
|
|
208
|
+
// instructions this body omits (issue #1733).
|
|
209
|
+
issueType: CI_CD_ISSUE_TYPE,
|
|
210
|
+
labels: [...CI_CD_ISSUE_LABELS],
|
|
211
|
+
run: runCommand,
|
|
212
|
+
log: message => console.log(message),
|
|
213
|
+
});
|
|
214
|
+
console.log(`✅ Created issue: ${issue.url}`);
|
|
215
|
+
|
|
216
|
+
if (!parsed.runSolve) {
|
|
217
|
+
console.log('ℹ️ --no-solve set; skipping /solve. Run it manually with:');
|
|
218
|
+
console.log(` solve ${buildSolveArgs({ issueUrl: issue.url, passthrough: parsed.passthrough }).join(' ')}`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const solveArgs = buildSolveArgs({ issueUrl: issue.url, passthrough: parsed.passthrough });
|
|
223
|
+
const solveCommand = resolveSolveCommand();
|
|
224
|
+
console.log(`\n🚀 Starting /solve: solve ${solveArgs.join(' ')}`);
|
|
225
|
+
|
|
226
|
+
await new Promise((resolve, reject) => {
|
|
227
|
+
const child = spawn(process.execPath, [solveCommand, ...solveArgs], {
|
|
228
|
+
stdio: 'inherit',
|
|
229
|
+
env: process.env,
|
|
230
|
+
});
|
|
231
|
+
child.on('error', reject);
|
|
232
|
+
child.on('close', code => {
|
|
233
|
+
if (code === 0) resolve();
|
|
234
|
+
else reject(new Error(`solve exited with code ${code}`));
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
main().catch(error => {
|
|
240
|
+
console.error(`❌ ${error.message}`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
});
|
package/src/locales/en.lino
CHANGED
|
@@ -564,6 +564,11 @@ en
|
|
|
564
564
|
enabled "*/split* - Split a GitHub issue into smaller issues"
|
|
565
565
|
usage "Usage: `/split <github-issue-url> [options]` or `/task --split <github-issue-url>`"
|
|
566
566
|
example "Example: `/split https://github.com/owner/repo/issues/123 --split-count 2`"
|
|
567
|
+
fix
|
|
568
|
+
enabled "*/fix* - Create a CI/CD remediation issue for a repository and solve it"
|
|
569
|
+
usage "Usage: `/fix <github-repository-url> [options]`"
|
|
570
|
+
example "Example: `/fix https://github.com/owner/repo --model sonnet`"
|
|
571
|
+
disabled "*/fix* - ❌ Disabled"
|
|
567
572
|
hive
|
|
568
573
|
enabled "*/hive* - Run hive command"
|
|
569
574
|
usage "Usage: `/hive <github-url> [options]`"
|
|
@@ -590,7 +595,7 @@ en
|
|
|
590
595
|
isolation
|
|
591
596
|
mode "🔒 *Isolation Mode:* `{{isolationBackend}}` (experimental)"
|
|
592
597
|
group
|
|
593
|
-
note "⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /watch, /subscribe and /unsubscribe work in private and group chats."
|
|
598
|
+
note "⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /watch, /subscribe and /unsubscribe work in private and group chats."
|
|
594
599
|
common
|
|
595
600
|
options "🔧 *Common Options:*"
|
|
596
601
|
model
|
package/src/locales/hi.lino
CHANGED
|
@@ -564,6 +564,11 @@ hi
|
|
|
564
564
|
enabled "*/split* - GitHub issue को छोटे issues में बाँटें"
|
|
565
565
|
usage "उपयोग: `/split <github-issue-url> [options]` या `/task --split <github-issue-url>`"
|
|
566
566
|
example "उदाहरण: `/split https://github.com/owner/repo/issues/123 --split-count 2`"
|
|
567
|
+
fix
|
|
568
|
+
enabled "*/fix* - repository के लिए CI/CD सुधार issue बनाएँ और उसे हल करें"
|
|
569
|
+
usage "उपयोग: `/fix <github-repository-url> [options]`"
|
|
570
|
+
example "उदाहरण: `/fix https://github.com/owner/repo --model sonnet`"
|
|
571
|
+
disabled "*/fix* - ❌ अक्षम"
|
|
567
572
|
hive
|
|
568
573
|
enabled "*/hive* - hive command चलाएँ"
|
|
569
574
|
usage "उपयोग: `/hive <github-url> [options]`"
|
|
@@ -590,7 +595,7 @@ hi
|
|
|
590
595
|
isolation
|
|
591
596
|
mode "🔒 *Isolation Mode:* `{{isolationBackend}}` (experimental)"
|
|
592
597
|
group
|
|
593
|
-
note "⚠️ *नोट:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop और /start commands केवल group chats में काम करती हैं। /terminal\\_watch, /watch, /subscribe और /unsubscribe private और group chats में काम करती हैं।"
|
|
598
|
+
note "⚠️ *नोट:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop और /start commands केवल group chats में काम करती हैं। /terminal\\_watch, /watch, /subscribe और /unsubscribe private और group chats में काम करती हैं।"
|
|
594
599
|
common
|
|
595
600
|
options "🔧 *Common Options:*"
|
|
596
601
|
model
|
package/src/locales/ru.lino
CHANGED
|
@@ -564,6 +564,11 @@ ru
|
|
|
564
564
|
enabled "*/split* - Разделить задачу GitHub на меньшие задачи"
|
|
565
565
|
usage "Использование: `/split <github-issue-url> [options]` или `/task --split <github-issue-url>`"
|
|
566
566
|
example "Пример: `/split https://github.com/owner/repo/issues/123 --split-count 2`"
|
|
567
|
+
fix
|
|
568
|
+
enabled "*/fix* - Создать задачу об исправлении CI/CD для репозитория и решить её"
|
|
569
|
+
usage "Использование: `/fix <github-repository-url> [options]`"
|
|
570
|
+
example "Пример: `/fix https://github.com/owner/repo --model sonnet`"
|
|
571
|
+
disabled "*/fix* - ❌ Отключено"
|
|
567
572
|
hive
|
|
568
573
|
enabled "*/hive* - Выполнить команду hive"
|
|
569
574
|
usage "Использование: `/hive <github-url> [options]`"
|
|
@@ -590,7 +595,7 @@ ru
|
|
|
590
595
|
isolation
|
|
591
596
|
mode "🔒 *Режим изоляции:* `{{isolationBackend}}` (экспериментально)"
|
|
592
597
|
group
|
|
593
|
-
note "⚠️ *Замечание:* команды /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop и /start работают только в групповых чатах. /terminal\\_watch, /watch, /subscribe и /unsubscribe работают в личных и групповых чатах."
|
|
598
|
+
note "⚠️ *Замечание:* команды /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop и /start работают только в групповых чатах. /terminal\\_watch, /watch, /subscribe и /unsubscribe работают в личных и групповых чатах."
|
|
594
599
|
common
|
|
595
600
|
options "🔧 *Общие опции:*"
|
|
596
601
|
model
|
package/src/locales/zh.lino
CHANGED
|
@@ -564,6 +564,11 @@ zh
|
|
|
564
564
|
enabled "*/split* - 将 GitHub issue 拆分为更小的 issue"
|
|
565
565
|
usage "用法:`/split <github-issue-url> [options]` 或 `/task --split <github-issue-url>`"
|
|
566
566
|
example "示例:`/split https://github.com/owner/repo/issues/123 --split-count 2`"
|
|
567
|
+
fix
|
|
568
|
+
enabled "*/fix* - 为仓库创建 CI/CD 修复 issue 并解决它"
|
|
569
|
+
usage "用法:`/fix <github-repository-url> [options]`"
|
|
570
|
+
example "示例:`/fix https://github.com/owner/repo --model sonnet`"
|
|
571
|
+
disabled "*/fix* - ❌ 已禁用"
|
|
567
572
|
hive
|
|
568
573
|
enabled "*/hive* - 运行 hive 命令"
|
|
569
574
|
usage "用法:`/hive <github-url> [options]`"
|
|
@@ -590,7 +595,7 @@ zh
|
|
|
590
595
|
isolation
|
|
591
596
|
mode "🔒 *隔离模式:* `{{isolationBackend}}`(实验性)"
|
|
592
597
|
group
|
|
593
|
-
note "⚠️ *注意:* /solve、/do、/continue、/claude、/codex、/opencode、/agent、/gemini、/qwen、/task、/split、/hive、/queue、/limits、/version、/accept\\_invites、/merge、/stop 和 /start 仅在群聊中有效。/terminal\\_watch、/watch、/subscribe 和 /unsubscribe 在私聊和群聊中有效。"
|
|
598
|
+
note "⚠️ *注意:* /solve、/do、/continue、/claude、/codex、/opencode、/agent、/gemini、/qwen、/task、/split、/fix、/hive、/queue、/limits、/version、/accept\\_invites、/merge、/stop 和 /start 仅在群聊中有效。/terminal\\_watch、/watch、/subscribe 和 /unsubscribe 在私聊和群聊中有效。"
|
|
594
599
|
common
|
|
595
600
|
options "🔧 *常用选项:*"
|
|
596
601
|
model
|
|
@@ -196,18 +196,44 @@ export function parseCreatedTaskIssueOutput(output) {
|
|
|
196
196
|
throw new Error(`Could not parse created issue URL from gh output: ${String(output || '').trim()}`);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
export
|
|
199
|
+
export function buildCreateIssueArgs({ repository, title, bodyFile, issueType = null, labels = [] }) {
|
|
200
|
+
const args = ['issue', 'create', '--repo', repository.fullName, '--title', title, '--body-file', bodyFile];
|
|
201
|
+
if (issueType) args.push('--type', issueType);
|
|
202
|
+
for (const label of labels) args.push('--label', label);
|
|
203
|
+
return args;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create an issue via `gh issue create`.
|
|
208
|
+
*
|
|
209
|
+
* `issueType` and `labels` are optional and best-effort: issue types are
|
|
210
|
+
* configured per organization and labels per repository, so a target repo may
|
|
211
|
+
* not have them. Rather than failing the whole command, a rejected create is
|
|
212
|
+
* retried once without them (issue #1733).
|
|
213
|
+
*/
|
|
214
|
+
export async function createTaskIssue({ repository, title, body, issueType = null, labels = [], run = runCommand, log = null }) {
|
|
200
215
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hive-mind-task-issue-'));
|
|
201
216
|
const bodyFile = path.join(tempDir, 'body.md');
|
|
202
217
|
|
|
203
218
|
try {
|
|
204
219
|
await fs.writeFile(bodyFile, body);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
|
|
221
|
+
const result = await run('gh', buildCreateIssueArgs({ repository, title, bodyFile, issueType, labels }));
|
|
222
|
+
if (result.code === 0) return parseCreatedTaskIssueOutput(result.stdout);
|
|
223
|
+
|
|
224
|
+
const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
|
|
225
|
+
const usedOptionalMetadata = Boolean(issueType) || labels.length > 0;
|
|
226
|
+
if (!usedOptionalMetadata) {
|
|
208
227
|
throw new Error(output || `gh issue create exited with code ${result.code}`);
|
|
209
228
|
}
|
|
210
|
-
|
|
229
|
+
|
|
230
|
+
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 }));
|
|
232
|
+
if (retry.code !== 0) {
|
|
233
|
+
const retryOutput = `${retry.stderr || ''}${retry.stdout || ''}`.trim();
|
|
234
|
+
throw new Error(retryOutput || `gh issue create exited with code ${retry.code}`);
|
|
235
|
+
}
|
|
236
|
+
return parseCreatedTaskIssueOutput(retry.stdout);
|
|
211
237
|
} finally {
|
|
212
238
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
213
239
|
}
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -23,94 +23,14 @@ dotenvx.config({ quiet: true, ignore: ['MISSING_ENV_FILE'] });
|
|
|
23
23
|
await loadLenvConfig({ override: true, quiet: true });
|
|
24
24
|
|
|
25
25
|
const yargs = getLinoYargsFactory();
|
|
26
|
+
const { createYargsConfig: createTelegramYargsConfig } = await import('./telegram.config.lib.mjs');
|
|
26
27
|
const { createYargsConfig: createSolveYargsConfig, detectMalformedFlags } = await import('./solve.config.lib.mjs');
|
|
27
28
|
const { createYargsConfig: createHiveYargsConfig } = await import('./hive.config.lib.mjs');
|
|
28
29
|
const { enhanceUnknownArgumentError } = await import('./option-suggestions.lib.mjs');
|
|
29
30
|
const { validateBranchInArgs } = await import('./solve.branch.lib.mjs');
|
|
30
31
|
const { extractIsolationFromArgs, isValidPerCommandIsolation } = await import('./telegram-isolation.lib.mjs');
|
|
31
32
|
|
|
32
|
-
const config = yargs(hideBin(process.argv))
|
|
33
|
-
.usage('Usage: hive-telegram-bot [options]')
|
|
34
|
-
.option('configuration', {
|
|
35
|
-
type: 'string',
|
|
36
|
-
description: 'LINO configuration string for environment variables',
|
|
37
|
-
alias: 'c',
|
|
38
|
-
default: getenv('TELEGRAM_CONFIGURATION', ''),
|
|
39
|
-
})
|
|
40
|
-
.option('token', {
|
|
41
|
-
type: 'string',
|
|
42
|
-
description: 'Telegram bot token from @BotFather',
|
|
43
|
-
alias: 't',
|
|
44
|
-
default: getenv('TELEGRAM_BOT_TOKEN', ''),
|
|
45
|
-
})
|
|
46
|
-
.option('allowedChats', {
|
|
47
|
-
type: 'string',
|
|
48
|
-
description: 'Allowed chat IDs in lino notation, e.g., "(\n 123456789\n 987654321\n)"',
|
|
49
|
-
alias: 'allowed-chats',
|
|
50
|
-
default: getenv('TELEGRAM_ALLOWED_CHATS', ''),
|
|
51
|
-
})
|
|
52
|
-
.option('allowedTopics', {
|
|
53
|
-
type: 'string',
|
|
54
|
-
description: 'Allowed topic IDs in Links Notation format "chatId topicId" pairs',
|
|
55
|
-
alias: 'allowed-topics',
|
|
56
|
-
default: getenv('TELEGRAM_ALLOWED_TOPICS', ''),
|
|
57
|
-
})
|
|
58
|
-
.option('solveOverrides', {
|
|
59
|
-
type: 'string',
|
|
60
|
-
description: 'Override options for /solve command in lino notation, e.g., "(\n --auto-continue\n --attach-logs\n)"',
|
|
61
|
-
alias: 'solve-overrides',
|
|
62
|
-
default: getenv('TELEGRAM_SOLVE_OVERRIDES', ''),
|
|
63
|
-
})
|
|
64
|
-
.option('hiveOverrides', {
|
|
65
|
-
type: 'string',
|
|
66
|
-
description: 'Override options for /hive command in lino notation, e.g., "(\n --verbose\n --all-issues\n)"',
|
|
67
|
-
alias: 'hive-overrides',
|
|
68
|
-
default: getenv('TELEGRAM_HIVE_OVERRIDES', ''),
|
|
69
|
-
})
|
|
70
|
-
.option('solve', {
|
|
71
|
-
type: 'boolean',
|
|
72
|
-
description: 'Enable /solve command (use --no-solve to disable)',
|
|
73
|
-
default: getenv('TELEGRAM_SOLVE', 'true') !== 'false',
|
|
74
|
-
})
|
|
75
|
-
.option('hive', {
|
|
76
|
-
type: 'boolean',
|
|
77
|
-
description: 'Enable /hive command (use --no-hive to disable)',
|
|
78
|
-
default: getenv('TELEGRAM_HIVE', 'true') !== 'false',
|
|
79
|
-
})
|
|
80
|
-
.option('task', {
|
|
81
|
-
type: 'boolean',
|
|
82
|
-
description: 'Enable /task and /split commands (use --no-task to disable)',
|
|
83
|
-
default: getenv('TELEGRAM_TASK', 'true') !== 'false',
|
|
84
|
-
})
|
|
85
|
-
.option('auth', {
|
|
86
|
-
type: 'boolean',
|
|
87
|
-
description: 'Enable experimental private /auth command for allowlisted chat owners (use --no-auth to disable)',
|
|
88
|
-
default: getenv('TELEGRAM_AUTH', 'true') !== 'false',
|
|
89
|
-
})
|
|
90
|
-
.option('dryRun', {
|
|
91
|
-
type: 'boolean',
|
|
92
|
-
description: 'Validate configuration and options without starting the bot',
|
|
93
|
-
alias: 'dry-run',
|
|
94
|
-
default: false,
|
|
95
|
-
})
|
|
96
|
-
.option('verbose', {
|
|
97
|
-
type: 'boolean',
|
|
98
|
-
description: 'Enable verbose logging for debugging',
|
|
99
|
-
alias: 'v',
|
|
100
|
-
default: getenv('TELEGRAM_BOT_VERBOSE', 'false') === 'true',
|
|
101
|
-
})
|
|
102
|
-
.option('autoStartScreenWatchMessage', { type: 'boolean', description: 'Experimental: auto-start separate /terminal_watch messages for public /solve sessions', alias: 'auto-start-screen-watch-message', default: getenv('TELEGRAM_AUTO_START_SCREEN_WATCH_MESSAGE', getenv('TELEGRAM_AUTO_WATCH_MESSAGE', 'false')) === 'true' })
|
|
103
|
-
// Issue #594: bot-owner toggle for --show-limits virtual option in /solve and /hive.
|
|
104
|
-
.option('showLimits', { type: 'boolean', description: 'Experimental: allow /solve and /hive callers to use --show-limits to embed Claude/Codex usage at start, end, and delta in the completion message', alias: 'show-limits', default: getenv('TELEGRAM_SHOW_LIMITS', 'true') !== 'false' })
|
|
105
|
-
.option('isolation', { type: 'string', description: "Isolation backend (screen/tmux/docker). Defaults to 'docker' so Telegram-bot work sessions run in Docker isolation; pass --isolation '' (or set TELEGRAM_ISOLATION='') to disable.", default: getenv('TELEGRAM_ISOLATION', 'docker') })
|
|
106
|
-
.help('h')
|
|
107
|
-
.alias('h', 'help')
|
|
108
|
-
.parserConfiguration({
|
|
109
|
-
'boolean-negation': true,
|
|
110
|
-
'strip-dashed': true, // Remove dashed keys from argv to simplify validation
|
|
111
|
-
})
|
|
112
|
-
.strict() // Enable strict mode to reject unknown options (consistent with solve.mjs and hive.mjs)
|
|
113
|
-
.parse();
|
|
33
|
+
const config = createTelegramYargsConfig(yargs(hideBin(process.argv))).parse();
|
|
114
34
|
|
|
115
35
|
// Configuration priority: CLI option > --configuration LINO > .lenv > .env
|
|
116
36
|
if (config.configuration) {
|
|
@@ -150,6 +70,7 @@ const hiveOverrides = resolvedHiveOverrides
|
|
|
150
70
|
const solveEnabled = config.solve;
|
|
151
71
|
const hiveEnabled = config.hive;
|
|
152
72
|
const taskEnabled = config.task;
|
|
73
|
+
const fixEnabled = config.fix;
|
|
153
74
|
const authEnabled = config.auth;
|
|
154
75
|
// Isolation mode (experimental): uses `$` from start-command with specified backend
|
|
155
76
|
const ISOLATION_BACKEND = (config.isolation || getenv('TELEGRAM_ISOLATION', '')).trim().toLowerCase();
|
|
@@ -299,7 +220,7 @@ if (config.dryRun) {
|
|
|
299
220
|
if (allowedTopics && allowedTopics.length > 0) {
|
|
300
221
|
console.log(' Allowed topics:', lino.formatLinks(allowedTopics));
|
|
301
222
|
}
|
|
302
|
-
console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, auth: authEnabled });
|
|
223
|
+
console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, fix: fixEnabled, auth: authEnabled });
|
|
303
224
|
if (solveOverrides.length > 0) {
|
|
304
225
|
console.log(' Solve overrides:', lino.format(solveOverrides));
|
|
305
226
|
}
|
|
@@ -570,6 +491,7 @@ bot.command('help', async ctx => {
|
|
|
570
491
|
stopReason: stopInfo?.reason || DEFAULT_STOP_REASON,
|
|
571
492
|
solveEnabled,
|
|
572
493
|
taskEnabled,
|
|
494
|
+
fixEnabled,
|
|
573
495
|
hiveEnabled,
|
|
574
496
|
solveOverrides,
|
|
575
497
|
hiveOverrides,
|
|
@@ -672,6 +594,8 @@ const { registerSubscribeCommands } = await import('./telegram-subscribers.lib.m
|
|
|
672
594
|
registerSubscribeCommands(bot, sharedCommandOpts);
|
|
673
595
|
const { registerTaskCommands } = await import('./telegram-task-command.lib.mjs');
|
|
674
596
|
const { handleTaskCommand, TASK_COMMAND_NAMES } = registerTaskCommands(bot, { ...sharedCommandOpts, taskEnabled, safeReply, executeAndUpdateMessage, resolveLocale: resolveLocaleFromTelegramCtx });
|
|
597
|
+
const { registerFixCommand } = await import('./telegram-fix-command.lib.mjs');
|
|
598
|
+
const { handleFixCommand, FIX_COMMAND_NAMES } = registerFixCommand(bot, { ...sharedCommandOpts, fixEnabled, safeReply, executeAndUpdateMessage, resolveLocale: resolveLocaleFromTelegramCtx });
|
|
675
599
|
const { registerAuthCommand } = await import('./telegram-auth-command.lib.mjs');
|
|
676
600
|
const { handleAuthCommand } = registerAuthCommand(bot, { ...sharedCommandOpts, allowedChats, authEnabled, safeReply });
|
|
677
601
|
|
|
@@ -1235,7 +1159,8 @@ bot.on('message', async (ctx, next) => {
|
|
|
1235
1159
|
// /subscribe + /unsubscribe (#1688) are intentionally not in the text fallback — Telegraf's bot.command() is sufficient.
|
|
1236
1160
|
const solveHandlers = Object.fromEntries(SOLVE_COMMAND_NAMES.map(command => [command, handleSolveCommand]));
|
|
1237
1161
|
const taskHandlers = Object.fromEntries(TASK_COMMAND_NAMES.map(command => [command, handleTaskCommand]));
|
|
1238
|
-
const
|
|
1162
|
+
const fixHandlers = Object.fromEntries(FIX_COMMAND_NAMES.map(command => [command, handleFixCommand]));
|
|
1163
|
+
const handlers = { ...solveHandlers, ...taskHandlers, ...fixHandlers, auth: handleAuthCommand, hive: handleHiveCommand, queue: handleSolveQueueCommand };
|
|
1239
1164
|
|
|
1240
1165
|
const handler = handlers[extracted.command];
|
|
1241
1166
|
if (!handler) return next();
|
|
@@ -1344,7 +1269,7 @@ if (allowedChats && allowedChats.length > 0) {
|
|
|
1344
1269
|
if (allowedTopics && allowedTopics.length > 0) {
|
|
1345
1270
|
console.log('Allowed topics (lino):', lino.formatLinks(allowedTopics));
|
|
1346
1271
|
}
|
|
1347
|
-
console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, auth: authEnabled });
|
|
1272
|
+
console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, fix: fixEnabled, auth: authEnabled });
|
|
1348
1273
|
if (solveOverrides.length > 0) console.log('Solve overrides (lino):', lino.format(solveOverrides));
|
|
1349
1274
|
if (hiveOverrides.length > 0) console.log('Hive overrides (lino):', lino.format(hiveOverrides));
|
|
1350
1275
|
if (VERBOSE) {
|