@link-assistant/hive-mind 1.59.7 → 1.61.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.
@@ -0,0 +1,203 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import { spawn } from 'child_process';
4
+ import { promises as fs } from 'fs';
5
+ import { parseGitHubUrl } from './github.lib.mjs';
6
+
7
+ export const TASK_ISSUE_TITLE_MAX_LENGTH = 256;
8
+
9
+ function normalizeNewlines(value) {
10
+ return String(value || '').replace(/\r\n?/g, '\n');
11
+ }
12
+
13
+ function cleanRepositoryCandidate(value) {
14
+ return String(value || '')
15
+ .trim()
16
+ .replace(/^[<([{]+/, '')
17
+ .replace(/[>\])}.,;:]+$/, '');
18
+ }
19
+
20
+ export function stripTaskCommandPrefix(text) {
21
+ const value = normalizeNewlines(text).trimStart();
22
+ return value.replace(/^\/(?:task|split)(?:@\S+)?(?:[ \t]+|\n|$)/i, '').trim();
23
+ }
24
+
25
+ export function resolveTaskIssueCreationInput({ commandText = '', replyText = '' } = {}) {
26
+ const inlineText = stripTaskCommandPrefix(commandText);
27
+ if (inlineText) return inlineText;
28
+ return normalizeNewlines(replyText).trim();
29
+ }
30
+
31
+ export function parseTaskRepository(value) {
32
+ const candidate = cleanRepositoryCandidate(value);
33
+ const parsed = parseGitHubUrl(candidate);
34
+ if (!parsed.valid || parsed.type !== 'repo') return null;
35
+ return {
36
+ owner: parsed.owner,
37
+ repo: parsed.repo,
38
+ fullName: `${parsed.owner}/${parsed.repo}`,
39
+ url: `https://github.com/${parsed.owner}/${parsed.repo}`,
40
+ };
41
+ }
42
+
43
+ function parseRepositoryDirective(line) {
44
+ const trimmed = line.trim();
45
+ if (!trimmed.startsWith('--repository')) return { matched: false };
46
+
47
+ const match = trimmed.match(/^--repository(?:=(\S+)|\s+(\S+))$/);
48
+ if (!match) {
49
+ return {
50
+ matched: true,
51
+ error: 'Invalid --repository syntax. Use --repository <github-repository-url>.',
52
+ };
53
+ }
54
+
55
+ const repository = parseTaskRepository(match[1] || match[2]);
56
+ if (!repository) {
57
+ return {
58
+ matched: true,
59
+ error: '--repository must point to a GitHub repository URL.',
60
+ };
61
+ }
62
+
63
+ return { matched: true, repository };
64
+ }
65
+
66
+ function parseRepositoryLine(line) {
67
+ const trimmed = line.trim();
68
+ if (!trimmed || /\s/.test(trimmed)) return null;
69
+ return parseTaskRepository(trimmed);
70
+ }
71
+
72
+ function setRepository(currentRepository, nextRepository) {
73
+ if (!nextRepository) return { repository: currentRepository };
74
+ if (currentRepository) {
75
+ return {
76
+ repository: currentRepository,
77
+ error: 'Only one GitHub repository may be provided.',
78
+ };
79
+ }
80
+ return { repository: nextRepository };
81
+ }
82
+
83
+ export function buildTaskIssueTitle(issueText, maxLength = TASK_ISSUE_TITLE_MAX_LENGTH) {
84
+ const firstLine = normalizeNewlines(issueText).trim().split('\n')[0]?.trim() || 'New task';
85
+ if (firstLine.length <= maxLength) return firstLine;
86
+ return `${firstLine.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
87
+ }
88
+
89
+ export function parseTaskIssueCreationInput(input) {
90
+ const normalized = normalizeNewlines(input).trim();
91
+ if (!normalized) {
92
+ return { valid: false, error: 'Missing repository and issue text.' };
93
+ }
94
+
95
+ const lines = normalized.split('\n');
96
+ let repository = null;
97
+ let bodyLines = [];
98
+
99
+ for (const line of lines) {
100
+ const directive = parseRepositoryDirective(line);
101
+ if (!directive.matched) {
102
+ bodyLines.push(line);
103
+ continue;
104
+ }
105
+ if (directive.error) return { valid: false, error: directive.error };
106
+ const next = setRepository(repository, directive.repository);
107
+ if (next.error) return { valid: false, error: next.error };
108
+ repository = next.repository;
109
+ }
110
+
111
+ if (!repository) {
112
+ bodyLines = [];
113
+ for (const line of lines) {
114
+ const lineRepository = parseRepositoryLine(line);
115
+ if (!lineRepository) {
116
+ bodyLines.push(line);
117
+ continue;
118
+ }
119
+ const next = setRepository(repository, lineRepository);
120
+ if (next.error) return { valid: false, error: next.error };
121
+ repository = next.repository;
122
+ }
123
+ }
124
+
125
+ if (!repository) {
126
+ return {
127
+ valid: false,
128
+ error: 'Missing GitHub repository URL. Provide it on its own line or with --repository <github-repository-url>.',
129
+ };
130
+ }
131
+
132
+ const issueText = bodyLines.join('\n').trim();
133
+ if (!issueText) {
134
+ return { valid: false, error: 'Missing issue text.' };
135
+ }
136
+
137
+ return {
138
+ valid: true,
139
+ repository,
140
+ issueText,
141
+ title: buildTaskIssueTitle(issueText),
142
+ };
143
+ }
144
+
145
+ function runCommand(command, args, options = {}) {
146
+ return new Promise(resolve => {
147
+ const child = spawn(command, args, {
148
+ stdio: ['ignore', 'pipe', 'pipe'],
149
+ env: process.env,
150
+ ...options,
151
+ });
152
+
153
+ let stdout = '';
154
+ let stderr = '';
155
+ child.stdout.on('data', data => {
156
+ stdout += data.toString();
157
+ });
158
+ child.stderr.on('data', data => {
159
+ stderr += data.toString();
160
+ });
161
+ child.on('error', error => {
162
+ resolve({ code: 1, stdout, stderr: stderr || error.message });
163
+ });
164
+ child.on('close', code => {
165
+ resolve({ code, stdout, stderr });
166
+ });
167
+ });
168
+ }
169
+
170
+ export function parseCreatedTaskIssueOutput(output) {
171
+ const tokens = String(output || '')
172
+ .split(/\s+/)
173
+ .filter(Boolean);
174
+ for (const token of tokens) {
175
+ const parsed = parseGitHubUrl(cleanRepositoryCandidate(token));
176
+ if (parsed.valid && parsed.type === 'issue') {
177
+ return {
178
+ owner: parsed.owner,
179
+ repo: parsed.repo,
180
+ number: parsed.number,
181
+ url: parsed.normalized,
182
+ };
183
+ }
184
+ }
185
+ throw new Error(`Could not parse created issue URL from gh output: ${String(output || '').trim()}`);
186
+ }
187
+
188
+ export async function createTaskIssue({ repository, title, body, run = runCommand }) {
189
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hive-mind-task-issue-'));
190
+ const bodyFile = path.join(tempDir, 'body.md');
191
+
192
+ try {
193
+ await fs.writeFile(bodyFile, body);
194
+ const result = await run('gh', ['issue', 'create', '--repo', repository.fullName, '--title', title, '--body-file', bodyFile]);
195
+ if (result.code !== 0) {
196
+ const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
197
+ throw new Error(output || `gh issue create exited with code ${result.code}`);
198
+ }
199
+ return parseCreatedTaskIssueOutput(result.stdout);
200
+ } finally {
201
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
202
+ }
203
+ }