gitdone-agent 0.6.2 → 0.6.4
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 +106 -9
- 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.6.
|
|
30
|
+
const AGENT_VERSION = '0.6.4'
|
|
31
31
|
|
|
32
32
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
33
33
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -521,19 +521,24 @@ async function postRunEvents(cfg, runId, events, status, result) {
|
|
|
521
521
|
|
|
522
522
|
// Post transcript lines (and optional turn status / Claude session id) for an
|
|
523
523
|
// interactive AiSession chat turn.
|
|
524
|
-
async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId) {
|
|
524
|
+
async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId, streamingText) {
|
|
525
525
|
await api(cfg, '/api/v1/agent/ai-session/events', {
|
|
526
526
|
sessionId,
|
|
527
527
|
events,
|
|
528
528
|
...(status ? { status } : {}),
|
|
529
529
|
...(claudeSessionId ? { claudeSessionId } : {}),
|
|
530
|
+
// Explicit '' clears the live preview; `undefined` leaves it untouched.
|
|
531
|
+
...(streamingText !== undefined ? { streamingText } : {}),
|
|
530
532
|
}).catch((e) => log(`✗ ai-session events post failed: ${e.message}`))
|
|
531
533
|
}
|
|
532
534
|
|
|
533
535
|
// Turn one stream-json line from `claude -p` into console events. Phase 1 shows
|
|
534
536
|
// what the model writes (TEXT) and which tools it calls (TOOL_CALL); raw tool
|
|
535
537
|
// results (Bash stdout etc.) come in Phase 2 via a PostToolUse hook.
|
|
536
|
-
|
|
538
|
+
// With `--include-partial-messages`, Claude also emits `stream_event` lines
|
|
539
|
+
// carrying incremental text deltas — onDelta gets those so chat sessions can
|
|
540
|
+
// show the reply being written live (gd-302).
|
|
541
|
+
function parseStreamLine(line, push, onInit, onDelta) {
|
|
537
542
|
let ev
|
|
538
543
|
try { ev = JSON.parse(line) } catch { return }
|
|
539
544
|
if (ev.type === 'system') {
|
|
@@ -544,6 +549,16 @@ function parseStreamLine(line, push, onInit) {
|
|
|
544
549
|
}
|
|
545
550
|
return
|
|
546
551
|
}
|
|
552
|
+
// Partial-message stream: only the model's visible text deltas feed the live
|
|
553
|
+
// preview. Tool-input JSON deltas and thinking deltas are ignored here — the
|
|
554
|
+
// full block still arrives as a normal `assistant` event below.
|
|
555
|
+
if (ev.type === 'stream_event' && typeof onDelta === 'function') {
|
|
556
|
+
const e = ev.event
|
|
557
|
+
if (e?.type === 'content_block_delta' && e.delta?.type === 'text_delta' && e.delta.text) {
|
|
558
|
+
onDelta(e.delta.text)
|
|
559
|
+
}
|
|
560
|
+
return
|
|
561
|
+
}
|
|
547
562
|
if (ev.type === 'assistant' && ev.message?.content) {
|
|
548
563
|
for (const block of ev.message.content) {
|
|
549
564
|
if (block.type === 'text' && block.text?.trim()) {
|
|
@@ -791,6 +806,24 @@ async function downloadSessionImages(sessionId, urls) {
|
|
|
791
806
|
return { dir, paths }
|
|
792
807
|
}
|
|
793
808
|
|
|
809
|
+
// Live chat turns, keyed by sessionId → their spawned `claude` child. Lets an
|
|
810
|
+
// `ai_chat_stop` command find and kill the turn a user asked to interrupt
|
|
811
|
+
// (gd-303). A turn registers itself on spawn and removes itself on close.
|
|
812
|
+
const chatChildren = new Map()
|
|
813
|
+
|
|
814
|
+
// Kill a process AND its descendants. `claude` spawns its own children (bash,
|
|
815
|
+
// node hooks), so killing just the top pid would orphan them. Windows needs
|
|
816
|
+
// `taskkill /T`; POSIX kills the process group, falling back to a hard kill.
|
|
817
|
+
function killTree(child) {
|
|
818
|
+
const pid = child?.pid
|
|
819
|
+
if (!pid) return
|
|
820
|
+
if (process.platform === 'win32') {
|
|
821
|
+
try { spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true }) } catch { /* best-effort */ }
|
|
822
|
+
} else {
|
|
823
|
+
try { process.kill(-pid, 'SIGKILL') } catch { try { child.kill('SIGKILL') } catch { /* gone */ } }
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
794
827
|
// Run one turn of an interactive chat session: `claude -p` (resuming the prior
|
|
795
828
|
// Claude session when known) with the user's message on stdin, streaming the
|
|
796
829
|
// reply back to the session console. Long-running + fire-and-forget like ai_run.
|
|
@@ -822,6 +855,9 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
822
855
|
'-p',
|
|
823
856
|
'--output-format', 'stream-json',
|
|
824
857
|
'--verbose',
|
|
858
|
+
// Stream the reply token-by-token so the console can show it being written
|
|
859
|
+
// live, not only once each block is complete (gd-302).
|
|
860
|
+
'--include-partial-messages',
|
|
825
861
|
'--permission-mode', 'acceptEdits',
|
|
826
862
|
'--allowedTools', 'Read,Edit,Write,Bash,mcp__gitdone__*',
|
|
827
863
|
// Block git commit/push unless this repo opted in (per-repo aiAutoCommit).
|
|
@@ -837,16 +873,35 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
837
873
|
let pending = []
|
|
838
874
|
let capturedSession = null // Claude's session id, sent (idempotently) for --resume
|
|
839
875
|
let flushing = false
|
|
876
|
+
let liveText = '' // in-progress text of the current assistant block (live preview)
|
|
877
|
+
let sentLive = '' // last streamingText we posted — so we only push on change
|
|
878
|
+
let lastPostAt = 0 // Date.now() of the last post, for the keep-alive heartbeat
|
|
840
879
|
const flush = async () => {
|
|
841
|
-
if (flushing
|
|
880
|
+
if (flushing) return
|
|
881
|
+
const hasEvents = pending.length > 0
|
|
882
|
+
const liveChanged = liveText !== sentLive
|
|
883
|
+
// Heartbeat: with nothing new for a while, still post (bumps lastActivityAt)
|
|
884
|
+
// so the console shows honest "still working" and can spot a real stall.
|
|
885
|
+
const heartbeat = !hasEvents && !liveChanged && Date.now() - lastPostAt > 2500
|
|
886
|
+
if (!hasEvents && !liveChanged && !heartbeat) return
|
|
842
887
|
flushing = true
|
|
843
888
|
const batch = pending.map((e) => ({ role: roleFor(e.kind), text: e.text })); pending = []
|
|
844
|
-
|
|
889
|
+
const streamingText = liveChanged ? liveText : undefined
|
|
890
|
+
sentLive = liveText
|
|
891
|
+
await postSessionEvents(cfg, sessionId, batch, 'running', capturedSession || undefined, streamingText)
|
|
892
|
+
lastPostAt = Date.now()
|
|
845
893
|
flushing = false
|
|
846
894
|
}
|
|
847
|
-
const push = (kind, text) => {
|
|
895
|
+
const push = (kind, text) => {
|
|
896
|
+
if (text == null || String(text) === '') return
|
|
897
|
+
// A completed text block becomes a real event — drop its live preview so the
|
|
898
|
+
// finalised bubble and the cleared preview swap in on the same flush.
|
|
899
|
+
if (kind === 'TEXT') liveText = ''
|
|
900
|
+
pending.push({ kind, text: String(text) })
|
|
901
|
+
}
|
|
848
902
|
const onInit = (sid) => { if (sid && !claudeSessionId) capturedSession = sid }
|
|
849
|
-
const
|
|
903
|
+
const onDelta = (chunk) => { liveText += chunk }
|
|
904
|
+
const timer = setInterval(flush, 500)
|
|
850
905
|
|
|
851
906
|
// Fetch any attached images locally and fold their paths into the prompt so
|
|
852
907
|
// Claude reads them with its Read tool (URLs on stdin don't render).
|
|
@@ -875,6 +930,9 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
875
930
|
return
|
|
876
931
|
}
|
|
877
932
|
|
|
933
|
+
// Track this turn so an `ai_chat_stop` command can find and kill it (gd-303).
|
|
934
|
+
chatChildren.set(sessionId, child)
|
|
935
|
+
|
|
878
936
|
if (child.stdin) {
|
|
879
937
|
child.stdin.on('error', () => {})
|
|
880
938
|
try { child.stdin.write(fullPrompt); child.stdin.end() }
|
|
@@ -888,28 +946,61 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
888
946
|
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
889
947
|
const line = buf.slice(0, nl).trim()
|
|
890
948
|
buf = buf.slice(nl + 1)
|
|
891
|
-
if (line) parseStreamLine(line, push, onInit)
|
|
949
|
+
if (line) parseStreamLine(line, push, onInit, onDelta)
|
|
892
950
|
}
|
|
893
951
|
})
|
|
894
952
|
child.stderr.on('data', (d) => { const s = d.toString().trim(); if (s) push('SYSTEM', s) })
|
|
895
953
|
child.on('error', (err) => push('SYSTEM', `Процесна грешка: ${err.message}`))
|
|
896
954
|
child.on('close', async (code) => {
|
|
897
955
|
clearInterval(timer)
|
|
956
|
+
chatChildren.delete(sessionId)
|
|
898
957
|
if (imgDir) { try { rmSync(imgDir, { recursive: true, force: true }) } catch { /* best-effort */ } }
|
|
899
|
-
if (buf.trim()) parseStreamLine(buf.trim(), push, onInit)
|
|
958
|
+
if (buf.trim()) parseStreamLine(buf.trim(), push, onInit, onDelta)
|
|
959
|
+
liveText = '' // turn is over — drop any lingering live preview
|
|
900
960
|
await flush()
|
|
961
|
+
// User-initiated stop (gd-303): land on idle with a friendly note, not an
|
|
962
|
+
// "exited with code N" error — the kill's non-zero code is expected here.
|
|
963
|
+
if (child.__stopped) {
|
|
964
|
+
await postSessionEvents(
|
|
965
|
+
cfg, sessionId,
|
|
966
|
+
[{ role: 'SYSTEM', text: '⏹ Спряно. Напиши още нещо, за да продължим разговора.' }],
|
|
967
|
+
'idle', capturedSession || undefined, '',
|
|
968
|
+
)
|
|
969
|
+
reportCommandResult(cfg, cmd.id, 'done', 'stopped by user')
|
|
970
|
+
log(`⏹ ai_chat ${sessionId} спряно от потребителя`)
|
|
971
|
+
return
|
|
972
|
+
}
|
|
901
973
|
const ok = code === 0
|
|
902
974
|
await postSessionEvents(
|
|
903
975
|
cfg, sessionId,
|
|
904
976
|
ok ? [] : [{ role: 'SYSTEM', text: `✗ Ходът приключи с код ${code}.` }],
|
|
905
977
|
ok ? 'idle' : 'error',
|
|
906
978
|
capturedSession || undefined,
|
|
979
|
+
'', // clear the live preview on the terminal post
|
|
907
980
|
)
|
|
908
981
|
reportCommandResult(cfg, cmd.id, ok ? 'done' : 'error', `exit ${code}`)
|
|
909
982
|
log(`■ ai_chat ${sessionId} приключи (code ${code})`)
|
|
910
983
|
})
|
|
911
984
|
}
|
|
912
985
|
|
|
986
|
+
// ai_chat_stop — interrupt a running chat turn the user asked to stop (gd-303).
|
|
987
|
+
// Kills the tracked child (its `close` handler then posts the "⏹ Спряно" note and
|
|
988
|
+
// flips the session to idle). If nothing is running, just idles the session so
|
|
989
|
+
// the console unlocks.
|
|
990
|
+
function stopAiChat(cfg, cmd) {
|
|
991
|
+
const sessionId = cmd.payload?.sessionId
|
|
992
|
+
const child = sessionId ? chatChildren.get(sessionId) : null
|
|
993
|
+
if (!child) {
|
|
994
|
+
if (sessionId) postSessionEvents(cfg, sessionId, [], 'idle', undefined, '')
|
|
995
|
+
reportCommandResult(cfg, cmd.id, 'done', 'no active turn')
|
|
996
|
+
return
|
|
997
|
+
}
|
|
998
|
+
child.__stopped = true
|
|
999
|
+
killTree(child)
|
|
1000
|
+
log(`⏹ ai_chat_stop ${sessionId} — kill signalled`)
|
|
1001
|
+
reportCommandResult(cfg, cmd.id, 'done', 'stop signalled')
|
|
1002
|
+
}
|
|
1003
|
+
|
|
913
1004
|
// deploy — run the project's deploy script (scripts/deploy.sh) in the repo.
|
|
914
1005
|
// Long-running (git pull + npm build + service restart), so we spawn it detached
|
|
915
1006
|
// from the poll loop and report the result on exit. The agent is its OWN process
|
|
@@ -954,6 +1045,12 @@ async function executeCommand(cfg, cmd, repoPath) {
|
|
|
954
1045
|
runAiChat(cfg, cmd, repoPath)
|
|
955
1046
|
return
|
|
956
1047
|
}
|
|
1048
|
+
// ai_chat_stop — interrupt the running turn for a session (gd-303). Handled
|
|
1049
|
+
// inline (it just signals a kill) and reports its own result.
|
|
1050
|
+
if (cmd.type === 'ai_chat_stop') {
|
|
1051
|
+
stopAiChat(cfg, cmd)
|
|
1052
|
+
return
|
|
1053
|
+
}
|
|
957
1054
|
// deploy — run the repo's deploy script. Long-running (pull + build + service
|
|
958
1055
|
// restart), so it's launched in the background and reports its own result on
|
|
959
1056
|
// exit, same as ai_run.
|