gitdone-agent 0.6.16 → 0.6.17
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 +77 -23
- package/package.json +15 -15
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.17'
|
|
31
31
|
|
|
32
32
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
33
33
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -759,20 +759,24 @@ async function reportCommandResult(cfg, id, status, result) {
|
|
|
759
759
|
|
|
760
760
|
// Post a batch of console events (and optional lifecycle status) for an AiRun.
|
|
761
761
|
// `usage` is sent once on the final (done/error) post so the server can record
|
|
762
|
-
// how many tokens the run cost.
|
|
763
|
-
|
|
762
|
+
// how many tokens the run cost. `streamingText` / `activityText` (gd-419) carry
|
|
763
|
+
// the live "being typed" preview and the latest thinking snippet — explicit ''
|
|
764
|
+
// clears them; `undefined` leaves them untouched.
|
|
765
|
+
async function postRunEvents(cfg, runId, events, status, result, usage, streamingText, activityText) {
|
|
764
766
|
await api(cfg, '/api/v1/agent/ai-run/events', {
|
|
765
767
|
runId,
|
|
766
768
|
events,
|
|
767
769
|
...(status ? { status } : {}),
|
|
768
770
|
...(result !== undefined ? { result } : {}),
|
|
769
771
|
...(usage ? { usage } : {}),
|
|
772
|
+
...(streamingText !== undefined ? { streamingText } : {}),
|
|
773
|
+
...(activityText !== undefined ? { activityText } : {}),
|
|
770
774
|
}).catch((e) => log(`✗ ai-run events post failed: ${e.message}`))
|
|
771
775
|
}
|
|
772
776
|
|
|
773
777
|
// Post transcript lines (and optional turn status / Claude session id) for an
|
|
774
778
|
// interactive AiSession chat turn.
|
|
775
|
-
async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId, streamingText) {
|
|
779
|
+
async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId, streamingText, activityText) {
|
|
776
780
|
await api(cfg, '/api/v1/agent/ai-session/events', {
|
|
777
781
|
sessionId,
|
|
778
782
|
events,
|
|
@@ -780,6 +784,8 @@ async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId
|
|
|
780
784
|
...(claudeSessionId ? { claudeSessionId } : {}),
|
|
781
785
|
// Explicit '' clears the live preview; `undefined` leaves it untouched.
|
|
782
786
|
...(streamingText !== undefined ? { streamingText } : {}),
|
|
787
|
+
// Same semantics for the live thinking snippet (gd-419).
|
|
788
|
+
...(activityText !== undefined ? { activityText } : {}),
|
|
783
789
|
}).catch((e) => log(`✗ ai-session events post failed: ${e.message}`))
|
|
784
790
|
}
|
|
785
791
|
|
|
@@ -810,13 +816,18 @@ function parseStreamLine(line, push, onInit, onDelta, onMeta) {
|
|
|
810
816
|
}
|
|
811
817
|
return
|
|
812
818
|
}
|
|
813
|
-
// Partial-message stream:
|
|
814
|
-
// preview
|
|
815
|
-
//
|
|
819
|
+
// Partial-message stream: the model's visible text deltas feed the live
|
|
820
|
+
// preview, its thinking deltas feed the gray "какво прави АИ-то" snippet
|
|
821
|
+
// (gd-419). Tool-input JSON deltas stay ignored — the full block still
|
|
822
|
+
// arrives as a normal `assistant` event below.
|
|
816
823
|
if (ev.type === 'stream_event' && typeof onDelta === 'function') {
|
|
817
824
|
const e = ev.event
|
|
818
|
-
if (e?.type === 'content_block_delta'
|
|
819
|
-
onDelta(e.delta.text)
|
|
825
|
+
if (e?.type === 'content_block_delta') {
|
|
826
|
+
if (e.delta?.type === 'text_delta' && e.delta.text) onDelta(e.delta.text, 'text')
|
|
827
|
+
else if (e.delta?.type === 'thinking_delta' && e.delta.thinking) onDelta(e.delta.thinking, 'thinking')
|
|
828
|
+
} else if (e?.type === 'content_block_start' && e.content_block?.type === 'thinking') {
|
|
829
|
+
// A fresh thought begins — reset the snippet so old and new don't blend.
|
|
830
|
+
onDelta('', 'thinking-start')
|
|
820
831
|
}
|
|
821
832
|
return
|
|
822
833
|
}
|
|
@@ -1043,6 +1054,9 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1043
1054
|
'-p',
|
|
1044
1055
|
'--output-format', 'stream-json',
|
|
1045
1056
|
'--verbose',
|
|
1057
|
+
// Stream text token-by-token + thinking deltas so the terminal shows the
|
|
1058
|
+
// reply being written live, ред по ред, not in whole-block batches (gd-419).
|
|
1059
|
+
'--include-partial-messages',
|
|
1046
1060
|
// Model chosen in gitDone (project default / task); omitted → machine default (gd-354).
|
|
1047
1061
|
...(model ? ['--model', model] : []),
|
|
1048
1062
|
'--permission-mode', 'acceptEdits',
|
|
@@ -1060,15 +1074,39 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1060
1074
|
// Batch events on a timer so we don't hammer the server per token/line.
|
|
1061
1075
|
let pending = []
|
|
1062
1076
|
let flushing = false
|
|
1077
|
+
let liveText = '' // in-progress text of the current assistant block (live preview)
|
|
1078
|
+
let sentLive = '' // last streamingText we posted — only push on change
|
|
1079
|
+
let liveActivity = '' // latest thinking snippet — "какво прави АИ-то" (gd-419)
|
|
1080
|
+
let sentActivity = ''
|
|
1063
1081
|
const flush = async () => {
|
|
1064
|
-
if (flushing
|
|
1082
|
+
if (flushing) return
|
|
1083
|
+
const liveChanged = liveText !== sentLive
|
|
1084
|
+
const activityChanged = liveActivity !== sentActivity
|
|
1085
|
+
if (pending.length === 0 && !liveChanged && !activityChanged) return
|
|
1065
1086
|
flushing = true
|
|
1066
1087
|
const batch = pending; pending = []
|
|
1067
|
-
|
|
1088
|
+
const streamingText = liveChanged ? liveText : undefined
|
|
1089
|
+
const activityText = activityChanged ? liveActivity : undefined
|
|
1090
|
+
sentLive = liveText
|
|
1091
|
+
sentActivity = liveActivity
|
|
1092
|
+
await postRunEvents(cfg, runId, batch, undefined, undefined, undefined, streamingText, activityText)
|
|
1068
1093
|
flushing = false
|
|
1069
1094
|
}
|
|
1070
|
-
const push = (kind, text) => {
|
|
1071
|
-
|
|
1095
|
+
const push = (kind, text) => {
|
|
1096
|
+
if (text == null || String(text) === '') return
|
|
1097
|
+
// A completed text block becomes a real event — drop its live preview; any
|
|
1098
|
+
// new event also supersedes the last thinking snippet.
|
|
1099
|
+
if (kind === 'TEXT') liveText = ''
|
|
1100
|
+
liveActivity = ''
|
|
1101
|
+
pending.push({ kind, text: String(text) })
|
|
1102
|
+
}
|
|
1103
|
+
const onDelta = (chunk, type) => {
|
|
1104
|
+
if (type === 'thinking-start') { liveActivity = ''; return }
|
|
1105
|
+
// Keep only the tail — the freshest thought is what the gray line shows.
|
|
1106
|
+
if (type === 'thinking') { liveActivity = (liveActivity + chunk).slice(-4000); return }
|
|
1107
|
+
liveText += chunk
|
|
1108
|
+
}
|
|
1109
|
+
const timer = setInterval(flush, 500)
|
|
1072
1110
|
|
|
1073
1111
|
// Accumulate model + token usage across the stream (model from init, usage
|
|
1074
1112
|
// from the final result event) to report once on exit (gd-334).
|
|
@@ -1107,14 +1145,16 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1107
1145
|
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
1108
1146
|
const line = buf.slice(0, nl).trim()
|
|
1109
1147
|
buf = buf.slice(nl + 1)
|
|
1110
|
-
if (line) parseStreamLine(line, push, undefined,
|
|
1148
|
+
if (line) parseStreamLine(line, push, undefined, onDelta, onMeta)
|
|
1111
1149
|
}
|
|
1112
1150
|
})
|
|
1113
1151
|
child.stderr.on('data', (d) => { const s = d.toString().trim(); if (s) push('SYSTEM', s) })
|
|
1114
1152
|
child.on('error', (err) => push('SYSTEM', `Процесна грешка: ${err.message}`))
|
|
1115
1153
|
child.on('close', async (code) => {
|
|
1116
1154
|
clearInterval(timer)
|
|
1117
|
-
if (buf.trim()) parseStreamLine(buf.trim(), push, undefined,
|
|
1155
|
+
if (buf.trim()) parseStreamLine(buf.trim(), push, undefined, onDelta, onMeta)
|
|
1156
|
+
liveText = '' // run is over — drop any lingering live preview / thought
|
|
1157
|
+
liveActivity = ''
|
|
1118
1158
|
await flush()
|
|
1119
1159
|
const ok = code === 0
|
|
1120
1160
|
// Build the usage payload + a console summary line from what we saw.
|
|
@@ -1238,32 +1278,44 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
1238
1278
|
let flushing = false
|
|
1239
1279
|
let liveText = '' // in-progress text of the current assistant block (live preview)
|
|
1240
1280
|
let sentLive = '' // last streamingText we posted — so we only push on change
|
|
1281
|
+
let liveActivity = '' // latest thinking snippet — "какво прави АИ-то" (gd-419)
|
|
1282
|
+
let sentActivity = ''
|
|
1241
1283
|
let lastPostAt = 0 // Date.now() of the last post, for the keep-alive heartbeat
|
|
1242
1284
|
const flush = async () => {
|
|
1243
1285
|
if (flushing) return
|
|
1244
1286
|
const hasEvents = pending.length > 0
|
|
1245
1287
|
const liveChanged = liveText !== sentLive
|
|
1288
|
+
const activityChanged = liveActivity !== sentActivity
|
|
1246
1289
|
// Heartbeat: with nothing new for a while, still post (bumps lastActivityAt)
|
|
1247
1290
|
// so the console shows honest "still working" and can spot a real stall.
|
|
1248
|
-
const heartbeat = !hasEvents && !liveChanged && Date.now() - lastPostAt > 2500
|
|
1249
|
-
if (!hasEvents && !liveChanged && !heartbeat) return
|
|
1291
|
+
const heartbeat = !hasEvents && !liveChanged && !activityChanged && Date.now() - lastPostAt > 2500
|
|
1292
|
+
if (!hasEvents && !liveChanged && !activityChanged && !heartbeat) return
|
|
1250
1293
|
flushing = true
|
|
1251
1294
|
const batch = pending.map((e) => ({ role: roleFor(e.kind), text: e.text })); pending = []
|
|
1252
1295
|
const streamingText = liveChanged ? liveText : undefined
|
|
1296
|
+
const activityText = activityChanged ? liveActivity : undefined
|
|
1253
1297
|
sentLive = liveText
|
|
1254
|
-
|
|
1298
|
+
sentActivity = liveActivity
|
|
1299
|
+
await postSessionEvents(cfg, sessionId, batch, 'running', capturedSession || undefined, streamingText, activityText)
|
|
1255
1300
|
lastPostAt = Date.now()
|
|
1256
1301
|
flushing = false
|
|
1257
1302
|
}
|
|
1258
1303
|
const push = (kind, text) => {
|
|
1259
1304
|
if (text == null || String(text) === '') return
|
|
1260
1305
|
// A completed text block becomes a real event — drop its live preview so the
|
|
1261
|
-
// finalised bubble and the cleared preview swap in on the same flush.
|
|
1306
|
+
// finalised bubble and the cleared preview swap in on the same flush. Any
|
|
1307
|
+
// new event also supersedes the last thinking snippet (gd-419).
|
|
1262
1308
|
if (kind === 'TEXT') liveText = ''
|
|
1309
|
+
liveActivity = ''
|
|
1263
1310
|
pending.push({ kind, text: String(text) })
|
|
1264
1311
|
}
|
|
1265
1312
|
const onInit = (sid) => { if (sid && !claudeSessionId) capturedSession = sid }
|
|
1266
|
-
const onDelta = (chunk) => {
|
|
1313
|
+
const onDelta = (chunk, type) => {
|
|
1314
|
+
if (type === 'thinking-start') { liveActivity = ''; return }
|
|
1315
|
+
// Keep only the tail — the freshest thought is what the gray line shows.
|
|
1316
|
+
if (type === 'thinking') { liveActivity = (liveActivity + chunk).slice(-4000); return }
|
|
1317
|
+
liveText += chunk
|
|
1318
|
+
}
|
|
1267
1319
|
const timer = setInterval(flush, 500)
|
|
1268
1320
|
|
|
1269
1321
|
// Fetch any attached images locally and fold their paths into the prompt so
|
|
@@ -1319,7 +1371,8 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
1319
1371
|
chatChildren.delete(sessionId)
|
|
1320
1372
|
if (imgDir) { try { rmSync(imgDir, { recursive: true, force: true }) } catch { /* best-effort */ } }
|
|
1321
1373
|
if (buf.trim()) parseStreamLine(buf.trim(), push, onInit, onDelta)
|
|
1322
|
-
liveText = '' // turn is over — drop any lingering live preview
|
|
1374
|
+
liveText = '' // turn is over — drop any lingering live preview / thought
|
|
1375
|
+
liveActivity = ''
|
|
1323
1376
|
await flush()
|
|
1324
1377
|
// User-initiated stop (gd-303): land on idle with a friendly note, not an
|
|
1325
1378
|
// "exited with code N" error — the kill's non-zero code is expected here.
|
|
@@ -1327,7 +1380,7 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
1327
1380
|
await postSessionEvents(
|
|
1328
1381
|
cfg, sessionId,
|
|
1329
1382
|
[{ role: 'SYSTEM', text: '⏹ Спряно. Напиши още нещо, за да продължим разговора.' }],
|
|
1330
|
-
'idle', capturedSession || undefined, '',
|
|
1383
|
+
'idle', capturedSession || undefined, '', '',
|
|
1331
1384
|
)
|
|
1332
1385
|
reportCommandResult(cfg, cmd.id, 'done', 'stopped by user')
|
|
1333
1386
|
log(`⏹ ai_chat ${sessionId} спряно от потребителя`)
|
|
@@ -1340,6 +1393,7 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
1340
1393
|
ok ? 'idle' : 'error',
|
|
1341
1394
|
capturedSession || undefined,
|
|
1342
1395
|
'', // clear the live preview on the terminal post
|
|
1396
|
+
'', // …and the thinking snippet (gd-419)
|
|
1343
1397
|
)
|
|
1344
1398
|
reportCommandResult(cfg, cmd.id, ok ? 'done' : 'error', `exit ${code}`)
|
|
1345
1399
|
log(`■ ai_chat ${sessionId} приключи (code ${code})`)
|
|
@@ -1354,7 +1408,7 @@ function stopAiChat(cfg, cmd) {
|
|
|
1354
1408
|
const sessionId = cmd.payload?.sessionId
|
|
1355
1409
|
const child = sessionId ? chatChildren.get(sessionId) : null
|
|
1356
1410
|
if (!child) {
|
|
1357
|
-
if (sessionId) postSessionEvents(cfg, sessionId, [], 'idle', undefined, '')
|
|
1411
|
+
if (sessionId) postSessionEvents(cfg, sessionId, [], 'idle', undefined, '', '')
|
|
1358
1412
|
reportCommandResult(cfg, cmd.id, 'done', 'no active turn')
|
|
1359
1413
|
return
|
|
1360
1414
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "gitdone-agent",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"gitdone-agent": "index.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"start": "node index.js"
|
|
11
|
-
},
|
|
12
|
-
"engines": {
|
|
13
|
-
"node": "\u003e=18"
|
|
14
|
-
}
|
|
15
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "gitdone-agent",
|
|
3
|
+
"version": "0.6.17",
|
|
4
|
+
"description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gitdone-agent": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node index.js"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": "\u003e=18"
|
|
14
|
+
}
|
|
15
|
+
}
|