gitdone-agent 0.6.5 → 0.6.7
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 +110 -12
- 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.6'
|
|
31
31
|
|
|
32
32
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
33
33
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -403,6 +403,42 @@ function decodeDiffText(buf) {
|
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
// Git C-quotes paths with spaces / special chars / non-ASCII bytes, wrapping them
|
|
407
|
+
// in double quotes with backslash escapes (`"export coca_cola/x.cs"`, octal `\NNN`
|
|
408
|
+
// for raw bytes). Both `git status --porcelain` and the `diff --git` header do it.
|
|
409
|
+
// Left unhandled, files with spaces (or Cyrillic) got mis-keyed and their diff was
|
|
410
|
+
// dropped (gd-321). This reverses it back to a plain path so the diff key matches
|
|
411
|
+
// the file name shown in the list.
|
|
412
|
+
function unquoteGitPath(s) {
|
|
413
|
+
if (typeof s !== 'string' || s.length < 2 || s[0] !== '"' || s[s.length - 1] !== '"') return s
|
|
414
|
+
const body = s.slice(1, -1)
|
|
415
|
+
const bytes = []
|
|
416
|
+
for (let i = 0; i < body.length; i++) {
|
|
417
|
+
if (body[i] === '\\' && i + 1 < body.length) {
|
|
418
|
+
const n = body[i + 1]
|
|
419
|
+
if (n === 'n') { bytes.push(10); i++ }
|
|
420
|
+
else if (n === 't') { bytes.push(9); i++ }
|
|
421
|
+
else if (n === 'r') { bytes.push(13); i++ }
|
|
422
|
+
else if (n === '"') { bytes.push(34); i++ }
|
|
423
|
+
else if (n === '\\') { bytes.push(92); i++ }
|
|
424
|
+
else if (n >= '0' && n <= '7') { bytes.push(parseInt(body.substr(i + 1, 3), 8) & 0xff); i += 3 }
|
|
425
|
+
else { bytes.push(body.charCodeAt(i)) }
|
|
426
|
+
} else {
|
|
427
|
+
bytes.push(body.charCodeAt(i) & 0xff)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return Buffer.from(bytes).toString('utf8')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Read the b-side path from a `diff --git` header line, handling git's quoting
|
|
434
|
+
// of paths with spaces / special chars (gd-321).
|
|
435
|
+
function diffHeaderPath(hdr) {
|
|
436
|
+
const q = hdr.match(/ ("b\/.*")$/) // quoted: "a/x" "b/x"
|
|
437
|
+
if (q) return unquoteGitPath(q[1]).replace(/^b\//, '')
|
|
438
|
+
const u = hdr.match(/ b\/(.*)$/) // plain: a/x b/x
|
|
439
|
+
return u ? u[1] : null
|
|
440
|
+
}
|
|
441
|
+
|
|
406
442
|
function parseDiffByFile(diffBuf) {
|
|
407
443
|
const files = {}
|
|
408
444
|
if (!diffBuf || diffBuf.length === 0) return files
|
|
@@ -413,10 +449,11 @@ function parseDiffByFile(diffBuf) {
|
|
|
413
449
|
for (const section of sections) {
|
|
414
450
|
if (!section.trim()) continue
|
|
415
451
|
// Decode THIS file's bytes on their own (UTF-8 or CP1251), then read the
|
|
416
|
-
// filename from the decoded header.
|
|
452
|
+
// filename from the decoded header (quoted or not).
|
|
417
453
|
const text = decodeDiffText(Buffer.from(section, 'latin1'))
|
|
418
|
-
const
|
|
419
|
-
|
|
454
|
+
const hdr = text.match(/^diff --git (.+)$/m)
|
|
455
|
+
const name = hdr ? diffHeaderPath(hdr[1]) : null
|
|
456
|
+
if (name) files[name] = text
|
|
420
457
|
}
|
|
421
458
|
return files
|
|
422
459
|
}
|
|
@@ -430,7 +467,7 @@ function getSnapshot(repoPath) {
|
|
|
430
467
|
const statuses = {}
|
|
431
468
|
for (const line of statusLines) {
|
|
432
469
|
const xy = line.slice(0, 2)
|
|
433
|
-
const file = line.slice(3)
|
|
470
|
+
const file = unquoteGitPath(line.slice(3))
|
|
434
471
|
statuses[file] = xy
|
|
435
472
|
if (xy[0] !== ' ' && xy[0] !== '?') staged.push(file)
|
|
436
473
|
if (xy[1] !== ' ') modified.push(file)
|
|
@@ -510,12 +547,15 @@ async function reportCommandResult(cfg, id, status, result) {
|
|
|
510
547
|
}
|
|
511
548
|
|
|
512
549
|
// Post a batch of console events (and optional lifecycle status) for an AiRun.
|
|
513
|
-
|
|
550
|
+
// `usage` is sent once on the final (done/error) post so the server can record
|
|
551
|
+
// how many tokens the run cost.
|
|
552
|
+
async function postRunEvents(cfg, runId, events, status, result, usage) {
|
|
514
553
|
await api(cfg, '/api/v1/agent/ai-run/events', {
|
|
515
554
|
runId,
|
|
516
555
|
events,
|
|
517
556
|
...(status ? { status } : {}),
|
|
518
557
|
...(result !== undefined ? { result } : {}),
|
|
558
|
+
...(usage ? { usage } : {}),
|
|
519
559
|
}).catch((e) => log(`✗ ai-run events post failed: ${e.message}`))
|
|
520
560
|
}
|
|
521
561
|
|
|
@@ -538,7 +578,15 @@ async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId
|
|
|
538
578
|
// With `--include-partial-messages`, Claude also emits `stream_event` lines
|
|
539
579
|
// carrying incremental text deltas — onDelta gets those so chat sessions can
|
|
540
580
|
// show the reply being written live (gd-302).
|
|
541
|
-
|
|
581
|
+
// "12.3k" / "1.2M" compaction for the console token summary line.
|
|
582
|
+
function fmtTokens(n) {
|
|
583
|
+
n = Number(n) || 0
|
|
584
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
|
585
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`
|
|
586
|
+
return String(n)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function parseStreamLine(line, push, onInit, onDelta, onMeta) {
|
|
542
590
|
let ev
|
|
543
591
|
try { ev = JSON.parse(line) } catch { return }
|
|
544
592
|
if (ev.type === 'system') {
|
|
@@ -546,6 +594,8 @@ function parseStreamLine(line, push, onInit, onDelta) {
|
|
|
546
594
|
push('SYSTEM', `Сесия стартирана${ev.model ? ` (${ev.model})` : ''}.`)
|
|
547
595
|
// Surface Claude's own session id so chat sessions can --resume it.
|
|
548
596
|
if (ev.session_id && typeof onInit === 'function') onInit(ev.session_id)
|
|
597
|
+
// Remember which model ran, for the token/cost record (gd-334).
|
|
598
|
+
if (ev.model && typeof onMeta === 'function') onMeta({ model: ev.model })
|
|
549
599
|
}
|
|
550
600
|
return
|
|
551
601
|
}
|
|
@@ -570,8 +620,40 @@ function parseStreamLine(line, push, onInit, onDelta) {
|
|
|
570
620
|
}
|
|
571
621
|
return
|
|
572
622
|
}
|
|
573
|
-
if (ev.type === 'result'
|
|
574
|
-
push('SYSTEM', `Резултат: ${ev.subtype}`)
|
|
623
|
+
if (ev.type === 'result') {
|
|
624
|
+
if (ev.subtype && ev.subtype !== 'success') push('SYSTEM', `Резултат: ${ev.subtype}`)
|
|
625
|
+
// claude's final result carries cumulative token usage + its own cost.
|
|
626
|
+
// Prefer `modelUsage` (summed over every model/subagent turn) which is the
|
|
627
|
+
// true cumulative; top-level `usage` is often just the last turn. Fall back
|
|
628
|
+
// to `usage` when modelUsage is absent (older CLI).
|
|
629
|
+
if (typeof onMeta === 'function' && (ev.modelUsage || ev.usage || typeof ev.total_cost_usd === 'number')) {
|
|
630
|
+
let usage
|
|
631
|
+
let model
|
|
632
|
+
if (ev.modelUsage && typeof ev.modelUsage === 'object') {
|
|
633
|
+
const acc = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0 }
|
|
634
|
+
for (const [name, mu] of Object.entries(ev.modelUsage)) {
|
|
635
|
+
if (!model) model = name
|
|
636
|
+
acc.inputTokens += mu.inputTokens ?? mu.input_tokens ?? 0
|
|
637
|
+
acc.outputTokens += mu.outputTokens ?? mu.output_tokens ?? 0
|
|
638
|
+
acc.cacheReadTokens += mu.cacheReadInputTokens ?? mu.cache_read_input_tokens ?? 0
|
|
639
|
+
acc.cacheCreateTokens += mu.cacheCreationInputTokens ?? mu.cache_creation_input_tokens ?? 0
|
|
640
|
+
}
|
|
641
|
+
usage = acc
|
|
642
|
+
} else if (ev.usage) {
|
|
643
|
+
const u = ev.usage
|
|
644
|
+
usage = {
|
|
645
|
+
inputTokens: u.input_tokens ?? 0,
|
|
646
|
+
outputTokens: u.output_tokens ?? 0,
|
|
647
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
648
|
+
cacheCreateTokens: u.cache_creation_input_tokens ?? 0,
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
onMeta({
|
|
652
|
+
...(usage ? { usage } : {}),
|
|
653
|
+
...(model ? { model } : {}),
|
|
654
|
+
costUsd: typeof ev.total_cost_usd === 'number' ? ev.total_cost_usd : undefined,
|
|
655
|
+
})
|
|
656
|
+
}
|
|
575
657
|
}
|
|
576
658
|
}
|
|
577
659
|
|
|
@@ -762,6 +844,15 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
762
844
|
const push = (kind, text) => { if (text != null && String(text) !== '') pending.push({ kind, text: String(text) }) }
|
|
763
845
|
const timer = setInterval(flush, 800)
|
|
764
846
|
|
|
847
|
+
// Accumulate model + token usage across the stream (model from init, usage
|
|
848
|
+
// from the final result event) to report once on exit (gd-334).
|
|
849
|
+
const meta = { model: undefined, usage: undefined, costUsd: undefined }
|
|
850
|
+
const onMeta = (m) => {
|
|
851
|
+
if (m.model) meta.model = m.model
|
|
852
|
+
if (m.usage) meta.usage = m.usage
|
|
853
|
+
if (typeof m.costUsd === 'number') meta.costUsd = m.costUsd
|
|
854
|
+
}
|
|
855
|
+
|
|
765
856
|
log(`▶ ai_run ${runId} @ ${repoPath} via ${claudePath}`)
|
|
766
857
|
postRunEvents(cfg, runId, [{ kind: 'SYSTEM', text: `Стартиране на Claude Code в ${repoPath}…` }], 'running')
|
|
767
858
|
|
|
@@ -790,21 +881,28 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
790
881
|
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
791
882
|
const line = buf.slice(0, nl).trim()
|
|
792
883
|
buf = buf.slice(nl + 1)
|
|
793
|
-
if (line) parseStreamLine(line, push)
|
|
884
|
+
if (line) parseStreamLine(line, push, undefined, undefined, onMeta)
|
|
794
885
|
}
|
|
795
886
|
})
|
|
796
887
|
child.stderr.on('data', (d) => { const s = d.toString().trim(); if (s) push('SYSTEM', s) })
|
|
797
888
|
child.on('error', (err) => push('SYSTEM', `Процесна грешка: ${err.message}`))
|
|
798
889
|
child.on('close', async (code) => {
|
|
799
890
|
clearInterval(timer)
|
|
800
|
-
if (buf.trim()) parseStreamLine(buf.trim(), push)
|
|
891
|
+
if (buf.trim()) parseStreamLine(buf.trim(), push, undefined, undefined, onMeta)
|
|
801
892
|
await flush()
|
|
802
893
|
const ok = code === 0
|
|
894
|
+
// Build the usage payload + a console summary line from what we saw.
|
|
895
|
+
const usage = meta.usage
|
|
896
|
+
? { model: meta.model, ...meta.usage, ...(typeof meta.costUsd === 'number' ? { costUsd: meta.costUsd } : {}) }
|
|
897
|
+
: undefined
|
|
898
|
+
const events = [{ kind: 'SYSTEM', text: ok ? '✓ Готово.' : `✗ Процесът приключи с код ${code}.` }]
|
|
899
|
+
if (usage) events.push({ kind: 'SYSTEM', text: `📊 Токени: ${fmtTokens(usage.inputTokens)} вход · ${fmtTokens(usage.outputTokens)} изход${usage.cacheReadTokens ? ` · ${fmtTokens(usage.cacheReadTokens)} кеш` : ''}${typeof usage.costUsd === 'number' ? ` · $${usage.costUsd.toFixed(4)}` : ''}` })
|
|
803
900
|
await postRunEvents(
|
|
804
901
|
cfg, runId,
|
|
805
|
-
|
|
902
|
+
events,
|
|
806
903
|
ok ? 'done' : 'error',
|
|
807
904
|
ok ? 'ok' : `exit ${code}`,
|
|
905
|
+
usage,
|
|
808
906
|
)
|
|
809
907
|
reportCommandResult(cfg, cmd.id, ok ? 'done' : 'error', `exit ${code}`)
|
|
810
908
|
log(`■ ai_run ${runId} приключи (code ${code})`)
|