gitdone-agent 0.5.1 → 0.5.2
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/index.js +169 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
27
27
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
28
28
|
// Keep in lockstep with packages/agent/package.json "version" AND
|
|
29
29
|
// src/lib/agentVersion.ts LATEST_AGENT_VERSION.
|
|
30
|
-
const AGENT_VERSION = '0.5.
|
|
30
|
+
const AGENT_VERSION = '0.5.2'
|
|
31
31
|
|
|
32
32
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
33
33
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -371,14 +371,29 @@ async function postRunEvents(cfg, runId, events, status, result) {
|
|
|
371
371
|
}).catch((e) => log(`✗ ai-run events post failed: ${e.message}`))
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
// Post transcript lines (and optional turn status / Claude session id) for an
|
|
375
|
+
// interactive AiSession chat turn.
|
|
376
|
+
async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId) {
|
|
377
|
+
await api(cfg, '/api/v1/agent/ai-session/events', {
|
|
378
|
+
sessionId,
|
|
379
|
+
events,
|
|
380
|
+
...(status ? { status } : {}),
|
|
381
|
+
...(claudeSessionId ? { claudeSessionId } : {}),
|
|
382
|
+
}).catch((e) => log(`✗ ai-session events post failed: ${e.message}`))
|
|
383
|
+
}
|
|
384
|
+
|
|
374
385
|
// Turn one stream-json line from `claude -p` into console events. Phase 1 shows
|
|
375
386
|
// what the model writes (TEXT) and which tools it calls (TOOL_CALL); raw tool
|
|
376
387
|
// results (Bash stdout etc.) come in Phase 2 via a PostToolUse hook.
|
|
377
|
-
function parseStreamLine(line, push) {
|
|
388
|
+
function parseStreamLine(line, push, onInit) {
|
|
378
389
|
let ev
|
|
379
390
|
try { ev = JSON.parse(line) } catch { return }
|
|
380
391
|
if (ev.type === 'system') {
|
|
381
|
-
if (ev.subtype === 'init')
|
|
392
|
+
if (ev.subtype === 'init') {
|
|
393
|
+
push('SYSTEM', `Сесия стартирана${ev.model ? ` (${ev.model})` : ''}.`)
|
|
394
|
+
// Surface Claude's own session id so chat sessions can --resume it.
|
|
395
|
+
if (ev.session_id && typeof onInit === 'function') onInit(ev.session_id)
|
|
396
|
+
}
|
|
382
397
|
return
|
|
383
398
|
}
|
|
384
399
|
if (ev.type === 'assistant' && ev.message?.content) {
|
|
@@ -448,12 +463,62 @@ function ensureAiHookFiles() {
|
|
|
448
463
|
return AI_SETTINGS_PATH
|
|
449
464
|
}
|
|
450
465
|
|
|
466
|
+
// Same PostToolUse hook, for interactive chat sessions: posts raw tool output to
|
|
467
|
+
// the session endpoint, reading GITDONE_SESSION_ID from the per-turn env.
|
|
468
|
+
const AI_SESSION_HOOK_SCRIPT = join(AGENT_DIR, 'ai-session-hook.mjs')
|
|
469
|
+
const AI_SESSION_SETTINGS_PATH = join(AGENT_DIR, 'ai-session-settings.json')
|
|
470
|
+
|
|
471
|
+
function ensureAiSessionHookFiles() {
|
|
472
|
+
ensureAgentDir()
|
|
473
|
+
const hookSrc = [
|
|
474
|
+
"import process from 'node:process'",
|
|
475
|
+
"let data = ''",
|
|
476
|
+
"process.stdin.setEncoding('utf8')",
|
|
477
|
+
"process.stdin.on('data', (c) => { data += c })",
|
|
478
|
+
'process.stdin.on(\'end\', async () => {',
|
|
479
|
+
' try {',
|
|
480
|
+
" const ev = JSON.parse(data || '{}')",
|
|
481
|
+
' const url = process.env.GITDONE_URL, key = process.env.GITDONE_KEY, sessionId = process.env.GITDONE_SESSION_ID',
|
|
482
|
+
' if (url && key && sessionId) {',
|
|
483
|
+
" const name = ev.tool_name || 'tool'",
|
|
484
|
+
' const o = ev.tool_output',
|
|
485
|
+
" let out = ''",
|
|
486
|
+
" if (typeof o === 'string') out = o",
|
|
487
|
+
" else if (o && typeof o === 'object') out = o.text || o.stdout || o.content || JSON.stringify(o)",
|
|
488
|
+
" out = String(out || '').slice(0, 2000)",
|
|
489
|
+
" const inp = ev.tool_input ? JSON.stringify(ev.tool_input).slice(0, 300) : ''",
|
|
490
|
+
" const text = name + (inp ? ' ' + inp : '') + (out ? '\\n' + out : ' ✓')",
|
|
491
|
+
" await fetch(url + '/api/v1/agent/ai-session/events', {",
|
|
492
|
+
" method: 'POST',",
|
|
493
|
+
" headers: { Authorization: 'Bearer ' + key, 'Content-Type': 'application/json' },",
|
|
494
|
+
" body: JSON.stringify({ sessionId, events: [{ role: 'TOOL_RESULT', text }] }),",
|
|
495
|
+
' }).catch(() => {})',
|
|
496
|
+
' }',
|
|
497
|
+
' } catch {}',
|
|
498
|
+
' process.exit(0)',
|
|
499
|
+
'})',
|
|
500
|
+
].join('\n')
|
|
501
|
+
writeFileSync(AI_SESSION_HOOK_SCRIPT, hookSrc, 'utf8')
|
|
502
|
+
|
|
503
|
+
const nodePath = getNodePath()
|
|
504
|
+
const settings = {
|
|
505
|
+
hooks: {
|
|
506
|
+
PostToolUse: [
|
|
507
|
+
{ matcher: '', hooks: [{ type: 'command', command: `"${nodePath}" "${AI_SESSION_HOOK_SCRIPT}"` }] },
|
|
508
|
+
],
|
|
509
|
+
},
|
|
510
|
+
}
|
|
511
|
+
writeFileSync(AI_SESSION_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8')
|
|
512
|
+
return AI_SESSION_SETTINGS_PATH
|
|
513
|
+
}
|
|
514
|
+
|
|
451
515
|
// Run a headless Claude Code session for an `ai_run` command and stream its
|
|
452
516
|
// output back. Long-running and fire-and-forget: it wires up async handlers and
|
|
453
517
|
// returns immediately so the agent's snapshot loop is never blocked.
|
|
454
518
|
function runAiCommand(cfg, cmd, repoPath) {
|
|
455
519
|
const runId = cmd.payload?.runId
|
|
456
520
|
const prompt = cmd.payload?.prompt ?? ''
|
|
521
|
+
const allowCommit = cmd.payload?.allowCommit === true
|
|
457
522
|
if (!runId || !prompt) {
|
|
458
523
|
reportCommandResult(cfg, cmd.id, 'error', 'invalid ai_run payload')
|
|
459
524
|
return
|
|
@@ -476,7 +541,8 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
476
541
|
'--verbose',
|
|
477
542
|
'--permission-mode', 'acceptEdits',
|
|
478
543
|
'--allowedTools', 'Read,Edit,Write,Bash,mcp__gitdone__*',
|
|
479
|
-
|
|
544
|
+
// Block git commit/push unless this repo opted in (per-repo aiAutoCommit).
|
|
545
|
+
...(allowCommit ? [] : ['--disallowedTools', 'Bash(git commit *),Bash(git push *)']),
|
|
480
546
|
...(settingsPath ? ['--settings', settingsPath] : []),
|
|
481
547
|
]
|
|
482
548
|
|
|
@@ -545,6 +611,100 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
545
611
|
})
|
|
546
612
|
}
|
|
547
613
|
|
|
614
|
+
// Run one turn of an interactive chat session: `claude -p` (resuming the prior
|
|
615
|
+
// Claude session when known) with the user's message on stdin, streaming the
|
|
616
|
+
// reply back to the session console. Long-running + fire-and-forget like ai_run.
|
|
617
|
+
function runAiChat(cfg, cmd, repoPath) {
|
|
618
|
+
const sessionId = cmd.payload?.sessionId
|
|
619
|
+
const prompt = cmd.payload?.prompt ?? ''
|
|
620
|
+
const claudeSessionId = cmd.payload?.claudeSessionId || null
|
|
621
|
+
const allowCommit = cmd.payload?.allowCommit === true
|
|
622
|
+
if (!sessionId || !prompt) {
|
|
623
|
+
reportCommandResult(cfg, cmd.id, 'error', 'invalid ai_chat payload')
|
|
624
|
+
return
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const { path: claudePath, shell } = findClaude()
|
|
628
|
+
|
|
629
|
+
let settingsPath
|
|
630
|
+
try { settingsPath = ensureAiSessionHookFiles() } catch (e) { log(`✗ ai session hook setup failed: ${e.message}`) }
|
|
631
|
+
|
|
632
|
+
const args = [
|
|
633
|
+
'-p',
|
|
634
|
+
'--output-format', 'stream-json',
|
|
635
|
+
'--verbose',
|
|
636
|
+
'--permission-mode', 'acceptEdits',
|
|
637
|
+
'--allowedTools', 'Read,Edit,Write,Bash,mcp__gitdone__*',
|
|
638
|
+
// Block git commit/push unless this repo opted in (per-repo aiAutoCommit).
|
|
639
|
+
...(allowCommit ? [] : ['--disallowedTools', 'Bash(git commit *),Bash(git push *)']),
|
|
640
|
+
...(claudeSessionId ? ['--resume', claudeSessionId] : []),
|
|
641
|
+
...(settingsPath ? ['--settings', settingsPath] : []),
|
|
642
|
+
]
|
|
643
|
+
|
|
644
|
+
const childEnv = { ...process.env, GITDONE_URL: cfg.url, GITDONE_KEY: cfg.key, GITDONE_SESSION_ID: sessionId }
|
|
645
|
+
|
|
646
|
+
// Map console kinds → session transcript roles (model text → ASSISTANT).
|
|
647
|
+
const roleFor = (kind) => (kind === 'TEXT' ? 'ASSISTANT' : kind)
|
|
648
|
+
let pending = []
|
|
649
|
+
let capturedSession = null // Claude's session id, sent (idempotently) for --resume
|
|
650
|
+
let flushing = false
|
|
651
|
+
const flush = async () => {
|
|
652
|
+
if (flushing || pending.length === 0) return
|
|
653
|
+
flushing = true
|
|
654
|
+
const batch = pending.map((e) => ({ role: roleFor(e.kind), text: e.text })); pending = []
|
|
655
|
+
await postSessionEvents(cfg, sessionId, batch, 'running', capturedSession || undefined)
|
|
656
|
+
flushing = false
|
|
657
|
+
}
|
|
658
|
+
const push = (kind, text) => { if (text != null && String(text) !== '') pending.push({ kind, text: String(text) }) }
|
|
659
|
+
const onInit = (sid) => { if (sid && !claudeSessionId) capturedSession = sid }
|
|
660
|
+
const timer = setInterval(flush, 800)
|
|
661
|
+
|
|
662
|
+
log(`▶ ai_chat ${sessionId} @ ${repoPath} (resume=${claudeSessionId ? 'yes' : 'no'}) via ${claudePath}`)
|
|
663
|
+
|
|
664
|
+
let child
|
|
665
|
+
try {
|
|
666
|
+
child = spawn(claudePath, args, { cwd: repoPath, shell, windowsHide: true, env: childEnv })
|
|
667
|
+
} catch (err) {
|
|
668
|
+
clearInterval(timer)
|
|
669
|
+
postSessionEvents(cfg, sessionId, [{ role: 'SYSTEM', text: `Грешка при стартиране: ${err.message}` }], 'error')
|
|
670
|
+
reportCommandResult(cfg, cmd.id, 'error', err.message)
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (child.stdin) {
|
|
675
|
+
child.stdin.on('error', () => {})
|
|
676
|
+
try { child.stdin.write(prompt); child.stdin.end() }
|
|
677
|
+
catch (e) { push('SYSTEM', `stdin write failed: ${e.message}`) }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
let buf = ''
|
|
681
|
+
child.stdout.on('data', (d) => {
|
|
682
|
+
buf += d.toString()
|
|
683
|
+
let nl
|
|
684
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
685
|
+
const line = buf.slice(0, nl).trim()
|
|
686
|
+
buf = buf.slice(nl + 1)
|
|
687
|
+
if (line) parseStreamLine(line, push, onInit)
|
|
688
|
+
}
|
|
689
|
+
})
|
|
690
|
+
child.stderr.on('data', (d) => { const s = d.toString().trim(); if (s) push('SYSTEM', s) })
|
|
691
|
+
child.on('error', (err) => push('SYSTEM', `Процесна грешка: ${err.message}`))
|
|
692
|
+
child.on('close', async (code) => {
|
|
693
|
+
clearInterval(timer)
|
|
694
|
+
if (buf.trim()) parseStreamLine(buf.trim(), push, onInit)
|
|
695
|
+
await flush()
|
|
696
|
+
const ok = code === 0
|
|
697
|
+
await postSessionEvents(
|
|
698
|
+
cfg, sessionId,
|
|
699
|
+
ok ? [] : [{ role: 'SYSTEM', text: `✗ Ходът приключи с код ${code}.` }],
|
|
700
|
+
ok ? 'idle' : 'error',
|
|
701
|
+
capturedSession || undefined,
|
|
702
|
+
)
|
|
703
|
+
reportCommandResult(cfg, cmd.id, ok ? 'done' : 'error', `exit ${code}`)
|
|
704
|
+
log(`■ ai_chat ${sessionId} приключи (code ${code})`)
|
|
705
|
+
})
|
|
706
|
+
}
|
|
707
|
+
|
|
548
708
|
async function executeCommand(cfg, cmd, repoPath) {
|
|
549
709
|
// ai_run is long-running + streaming — launch it in the background and return
|
|
550
710
|
// so the snapshot loop keeps going. It reports its own result on exit.
|
|
@@ -552,6 +712,11 @@ async function executeCommand(cfg, cmd, repoPath) {
|
|
|
552
712
|
runAiCommand(cfg, cmd, repoPath)
|
|
553
713
|
return
|
|
554
714
|
}
|
|
715
|
+
// ai_chat — one interactive turn, same fire-and-forget model.
|
|
716
|
+
if (cmd.type === 'ai_chat') {
|
|
717
|
+
runAiChat(cfg, cmd, repoPath)
|
|
718
|
+
return
|
|
719
|
+
}
|
|
555
720
|
|
|
556
721
|
let result = 'ok'
|
|
557
722
|
let status = 'done'
|