@gzmagyari/kanbanboard 1.0.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,417 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createInterface } from 'node:readline';
3
+
4
+ function parseArgsEnv(val) {
5
+ const s = String(val ?? '').trim();
6
+ if (!s) return [];
7
+ // Allow JSON array for safer args passing: '["--foo","bar"]'
8
+ if (s.startsWith('[')) {
9
+ try {
10
+ const arr = JSON.parse(s);
11
+ if (Array.isArray(arr)) return arr.map((x) => String(x));
12
+ } catch {
13
+ // fall through
14
+ }
15
+ }
16
+ // Fallback: whitespace split
17
+ return s.split(/\s+/g).filter(Boolean);
18
+ }
19
+
20
+ export function getRepoGroundingConfig() {
21
+ const cmd = String(process.env.REPO_GROUND_CLI_CMD || process.env.CLAUDE_CLI_CMD || 'claude').trim();
22
+ const args = parseArgsEnv(process.env.REPO_GROUND_CLI_ARGS || process.env.CLAUDE_CLI_ARGS || '');
23
+
24
+ const timeout_ms = Number(process.env.REPO_GROUND_TIMEOUT_MS || 120_000);
25
+ const max_output_bytes = Number(process.env.REPO_GROUND_MAX_OUTPUT_BYTES || 2_000_000);
26
+
27
+ return {
28
+ cmd,
29
+ args,
30
+ timeout_ms: Number.isFinite(timeout_ms) ? timeout_ms : 120_000,
31
+ max_output_bytes: Number.isFinite(max_output_bytes) ? max_output_bytes : 2_000_000
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Runs an external CLI (intended for Claude CLI) in `cwd=repoPath`.
37
+ * Sends `prompt` to stdin, captures stdout/stderr.
38
+ *
39
+ * Configure via:
40
+ * - REPO_GROUND_CLI_CMD (default: "claude")
41
+ * - REPO_GROUND_CLI_ARGS (string, whitespace-split OR JSON array string)
42
+ * - REPO_GROUND_TIMEOUT_MS (default: 120000)
43
+ * - REPO_GROUND_MAX_OUTPUT_BYTES (default: 2000000)
44
+ */
45
+ export function runRepoGrounding({ repoPath, prompt, timeout_ms, max_output_bytes } = {}) {
46
+ const cfg = getRepoGroundingConfig();
47
+ const cmd = cfg.cmd;
48
+ const args = cfg.args;
49
+ const timeout = Number.isFinite(timeout_ms) ? timeout_ms : cfg.timeout_ms;
50
+ const maxBytes = Number.isFinite(max_output_bytes) ? max_output_bytes : cfg.max_output_bytes;
51
+
52
+ if (!cmd) {
53
+ const err = new Error('Repo grounding CLI is not configured (set REPO_GROUND_CLI_CMD)');
54
+ err.code = 'REPO_GROUNDING_NOT_CONFIGURED';
55
+ throw err;
56
+ }
57
+
58
+ const started = Date.now();
59
+
60
+ return new Promise((resolve, reject) => {
61
+ let stdout = '';
62
+ let stderr = '';
63
+ let outBytes = 0;
64
+ let errBytes = 0;
65
+ let timed_out = false;
66
+ let killed_for_limit = false;
67
+
68
+ const child = spawn(cmd, args, {
69
+ cwd: repoPath,
70
+ stdio: ['pipe', 'pipe', 'pipe'],
71
+ shell: true
72
+ });
73
+
74
+ const timer = setTimeout(() => {
75
+ timed_out = true;
76
+ try {
77
+ child.kill('SIGKILL');
78
+ } catch {
79
+ // ignore
80
+ }
81
+ }, timeout);
82
+
83
+ child.on('error', (e) => {
84
+ clearTimeout(timer);
85
+ reject(e);
86
+ });
87
+
88
+ child.stdout.setEncoding('utf8');
89
+ child.stderr.setEncoding('utf8');
90
+
91
+ child.stdout.on('data', (chunk) => {
92
+ outBytes += Buffer.byteLength(chunk, 'utf8');
93
+ if (outBytes > maxBytes) {
94
+ killed_for_limit = true;
95
+ clearTimeout(timer);
96
+ try {
97
+ child.kill('SIGKILL');
98
+ } catch {
99
+ // ignore
100
+ }
101
+ return reject(new Error(`Repo grounding stdout exceeded limit (${maxBytes} bytes)`));
102
+ }
103
+ stdout += chunk;
104
+ });
105
+
106
+ child.stderr.on('data', (chunk) => {
107
+ errBytes += Buffer.byteLength(chunk, 'utf8');
108
+ if (errBytes > maxBytes) {
109
+ killed_for_limit = true;
110
+ clearTimeout(timer);
111
+ try {
112
+ child.kill('SIGKILL');
113
+ } catch {
114
+ // ignore
115
+ }
116
+ return reject(new Error(`Repo grounding stderr exceeded limit (${maxBytes} bytes)`));
117
+ }
118
+ stderr += chunk;
119
+ });
120
+
121
+ child.on('close', (code, signal) => {
122
+ clearTimeout(timer);
123
+ resolve({
124
+ cmd,
125
+ args,
126
+ repoPath,
127
+ code: code ?? 0,
128
+ signal: signal ?? null,
129
+ stdout,
130
+ stderr,
131
+ timed_out,
132
+ killed_for_limit,
133
+ duration_ms: Date.now() - started
134
+ });
135
+ });
136
+
137
+ try {
138
+ child.stdin.write(String(prompt ?? ''), 'utf8');
139
+ child.stdin.end();
140
+ } catch (e) {
141
+ clearTimeout(timer);
142
+ reject(e);
143
+ }
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Streaming variant of runRepoGrounding.
149
+ * Spawns Claude CLI with `--output-format stream-json --verbose` and reads
150
+ * NDJSON events line-by-line from stdout, calling `onEvent(event)` for each.
151
+ *
152
+ * The final `result` event's `result` field (a string) contains the CLI's
153
+ * text output. This is placed in `stdout` on the returned object so callers
154
+ * can use `extractFirstJsonObject()` as usual.
155
+ *
156
+ * Returns the same shape as runRepoGrounding().
157
+ */
158
+ export function runRepoGroundingStreaming({ repoPath, prompt, timeout_ms, max_output_bytes, onEvent } = {}) {
159
+ const cfg = getRepoGroundingConfig();
160
+ const cmd = cfg.cmd;
161
+ const baseArgs = cfg.args;
162
+ const timeout = Number.isFinite(timeout_ms) ? timeout_ms : cfg.timeout_ms;
163
+ const maxBytes = Number.isFinite(max_output_bytes) ? max_output_bytes : cfg.max_output_bytes;
164
+
165
+ if (!cmd) {
166
+ const err = new Error('Repo grounding CLI is not configured (set REPO_GROUND_CLI_CMD)');
167
+ err.code = 'REPO_GROUNDING_NOT_CONFIGURED';
168
+ throw err;
169
+ }
170
+
171
+ // Add streaming flags to args
172
+ const args = ['-p', '--output-format', 'stream-json', '--verbose', ...baseArgs];
173
+
174
+ const started = Date.now();
175
+
176
+ return new Promise((resolve, reject) => {
177
+ let stderr = '';
178
+ let outBytes = 0;
179
+ let errBytes = 0;
180
+ let timed_out = false;
181
+ let killed_for_limit = false;
182
+
183
+ // Track result event and last assistant text as fallback
184
+ let resultEvent = null;
185
+ const lastAssistantTexts = [];
186
+
187
+ const child = spawn(cmd, args, {
188
+ cwd: repoPath,
189
+ stdio: ['pipe', 'pipe', 'pipe'],
190
+ shell: true
191
+ });
192
+
193
+ const timer = setTimeout(() => {
194
+ timed_out = true;
195
+ try { child.kill('SIGKILL'); } catch { /* ignore */ }
196
+ }, timeout);
197
+
198
+ child.on('error', (e) => {
199
+ clearTimeout(timer);
200
+ reject(e);
201
+ });
202
+
203
+ // Read stdout line-by-line as NDJSON
204
+ const rl = createInterface({ input: child.stdout });
205
+ rl.on('line', (line) => {
206
+ outBytes += Buffer.byteLength(line, 'utf8') + 1; // +1 for newline
207
+ if (outBytes > maxBytes) {
208
+ killed_for_limit = true;
209
+ clearTimeout(timer);
210
+ rl.close();
211
+ try { child.kill('SIGKILL'); } catch { /* ignore */ }
212
+ return reject(new Error(`Repo grounding stdout exceeded limit (${maxBytes} bytes)`));
213
+ }
214
+
215
+ if (!line.trim()) return;
216
+
217
+ let event;
218
+ try { event = JSON.parse(line); } catch { return; } // skip non-JSON lines
219
+
220
+ // Track result event (final output)
221
+ if (event.type === 'result') {
222
+ resultEvent = event;
223
+ }
224
+ // Track assistant text blocks as fallback
225
+ if (event.type === 'assistant') {
226
+ for (const block of (event.message?.content || [])) {
227
+ if (block.type === 'text' && block.text?.trim()) {
228
+ lastAssistantTexts.push(block.text.trim());
229
+ }
230
+ }
231
+ }
232
+
233
+ // Notify caller
234
+ if (onEvent) {
235
+ try { onEvent(event); } catch { /* ignore callback errors */ }
236
+ }
237
+ });
238
+
239
+ child.stderr.setEncoding('utf8');
240
+ child.stderr.on('data', (chunk) => {
241
+ errBytes += Buffer.byteLength(chunk, 'utf8');
242
+ if (errBytes > maxBytes) {
243
+ killed_for_limit = true;
244
+ clearTimeout(timer);
245
+ try { child.kill('SIGKILL'); } catch { /* ignore */ }
246
+ return reject(new Error(`Repo grounding stderr exceeded limit (${maxBytes} bytes)`));
247
+ }
248
+ stderr += chunk;
249
+ });
250
+
251
+ child.on('close', (code, signal) => {
252
+ clearTimeout(timer);
253
+
254
+ // Extract stdout text: prefer result event's `result` field, then last assistant text
255
+ let stdout = '';
256
+ if (resultEvent) {
257
+ stdout = String(resultEvent.result ?? '');
258
+ }
259
+ if (!stdout && lastAssistantTexts.length) {
260
+ stdout = lastAssistantTexts[lastAssistantTexts.length - 1];
261
+ }
262
+
263
+ resolve({
264
+ cmd,
265
+ args: baseArgs, // return original args (without streaming flags) for consistency
266
+ repoPath,
267
+ code: code ?? 0,
268
+ signal: signal ?? null,
269
+ stdout,
270
+ stderr,
271
+ timed_out,
272
+ killed_for_limit,
273
+ duration_ms: Date.now() - started
274
+ });
275
+ });
276
+
277
+ try {
278
+ child.stdin.write(String(prompt ?? ''), 'utf8');
279
+ child.stdin.end();
280
+ } catch (e) {
281
+ clearTimeout(timer);
282
+ reject(e);
283
+ }
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Agent-oriented streaming variant of runRepoGrounding.
289
+ * Extends runRepoGroundingStreaming with support for:
290
+ * - Session persistence via --resume <sessionId>
291
+ * - System prompt injection via --append-system-prompt-file
292
+ * - Tool restrictions via --allowedTools
293
+ * - Permission bypass via --dangerously-skip-permissions
294
+ * - Session ID extraction from stream events
295
+ *
296
+ * Returns the same shape as runRepoGroundingStreaming plus `session_id`.
297
+ */
298
+ export function runAgentStreaming({
299
+ repoPath,
300
+ prompt,
301
+ resumeSessionId,
302
+ appendSystemPromptFile,
303
+ allowedTools,
304
+ dangerouslySkipPermissions,
305
+ onEvent
306
+ } = {}) {
307
+ const cfg = getRepoGroundingConfig();
308
+ const cmd = cfg.cmd;
309
+ const baseArgs = cfg.args;
310
+
311
+ if (!cmd) {
312
+ const err = new Error('Repo grounding CLI is not configured (set REPO_GROUND_CLI_CMD)');
313
+ err.code = 'REPO_GROUNDING_NOT_CONFIGURED';
314
+ throw err;
315
+ }
316
+
317
+ // Build args dynamically based on agent config
318
+ const args = ['-p', '--output-format', 'stream-json', '--verbose'];
319
+ if (resumeSessionId) {
320
+ args.push('--resume', resumeSessionId);
321
+ }
322
+ if (appendSystemPromptFile) {
323
+ args.push('--append-system-prompt-file', appendSystemPromptFile);
324
+ }
325
+ if (dangerouslySkipPermissions) {
326
+ args.push('--dangerously-skip-permissions');
327
+ } else if (allowedTools) {
328
+ const tools = allowedTools.split(',').map(t => t.trim()).filter(Boolean);
329
+ if (tools.length > 0) {
330
+ args.push('--allowedTools');
331
+ args.push(...tools);
332
+ }
333
+ }
334
+ args.push(...baseArgs);
335
+
336
+ const started = Date.now();
337
+
338
+ return new Promise((resolve, reject) => {
339
+ let stderr = '';
340
+ let resultEvent = null;
341
+ const lastAssistantTexts = [];
342
+ let initSessionId = null;
343
+
344
+ const child = spawn(cmd, args, {
345
+ cwd: repoPath,
346
+ stdio: ['pipe', 'pipe', 'pipe'],
347
+ shell: true
348
+ });
349
+
350
+ child.on('error', (e) => reject(e));
351
+
352
+ const rl = createInterface({ input: child.stdout });
353
+ rl.on('line', (line) => {
354
+ if (!line.trim()) return;
355
+
356
+ let event;
357
+ try { event = JSON.parse(line); } catch { return; }
358
+
359
+ if (event.type === 'system' && event.subtype === 'init') {
360
+ initSessionId = event.session_id || null;
361
+ }
362
+ if (event.type === 'result') {
363
+ resultEvent = event;
364
+ }
365
+ if (event.type === 'assistant') {
366
+ for (const block of (event.message?.content || [])) {
367
+ if (block.type === 'text' && block.text?.trim()) {
368
+ lastAssistantTexts.push(block.text.trim());
369
+ }
370
+ }
371
+ }
372
+
373
+ if (onEvent) {
374
+ try { onEvent(event); } catch { /* ignore */ }
375
+ }
376
+ });
377
+
378
+ child.stderr.setEncoding('utf8');
379
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
380
+
381
+ child.on('close', (code, signal) => {
382
+ let stdout = '';
383
+ if (resultEvent) {
384
+ stdout = String(resultEvent.result ?? '');
385
+ }
386
+ if (!stdout && lastAssistantTexts.length) {
387
+ stdout = lastAssistantTexts[lastAssistantTexts.length - 1];
388
+ }
389
+
390
+ let sessionId = null;
391
+ if (resultEvent && typeof resultEvent.session_id === 'string') {
392
+ sessionId = resultEvent.session_id;
393
+ } else if (initSessionId) {
394
+ sessionId = initSessionId;
395
+ }
396
+
397
+ resolve({
398
+ cmd,
399
+ args: baseArgs,
400
+ repoPath,
401
+ code: code ?? 0,
402
+ signal: signal ?? null,
403
+ stdout,
404
+ stderr,
405
+ duration_ms: Date.now() - started,
406
+ session_id: sessionId
407
+ });
408
+ });
409
+
410
+ try {
411
+ child.stdin.write(String(prompt ?? ''), 'utf8');
412
+ child.stdin.end();
413
+ } catch (e) {
414
+ reject(e);
415
+ }
416
+ });
417
+ }