@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.
- package/.env.example +48 -0
- package/API.md +1256 -0
- package/README.md +138 -0
- package/bin/cli.mjs +437 -0
- package/cron-sync.mjs +9 -0
- package/db.mjs +378 -0
- package/docs/project-manager-chat.md +202 -0
- package/kanban-mcp-server.mjs +377 -0
- package/kanban.mjs +127 -0
- package/lib/paths.mjs +136 -0
- package/llm.mjs +307 -0
- package/package.json +52 -0
- package/public/index.html +4747 -0
- package/repo-grounding.mjs +417 -0
- package/server.mjs +8607 -0
|
@@ -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
|
+
}
|