gitdone-agent 0.6.4 → 0.6.6
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 -5
- 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)
|
|
@@ -675,6 +712,32 @@ function ensureAiSessionHookFiles() {
|
|
|
675
712
|
return AI_SESSION_SETTINGS_PATH
|
|
676
713
|
}
|
|
677
714
|
|
|
715
|
+
// gitdone MCP config for headless sessions (gd-308). We provide it ourselves —
|
|
716
|
+
// with an X-Gitdone-Machine-Id header — so the server can attribute the AI agents
|
|
717
|
+
// this session registers (ai_agent_start) to THIS computer, and the „АИ Агенти"
|
|
718
|
+
// panel shows one sub-panel per machine instead of a single „Локален агент".
|
|
719
|
+
// Used with --strict-mcp-config so the session uses exactly this server. The
|
|
720
|
+
// agent's own key (a gdo_ key) authenticates the MCP endpoint.
|
|
721
|
+
const AI_MCP_CONFIG_PATH = join(AGENT_DIR, 'ai-mcp-config.json')
|
|
722
|
+
|
|
723
|
+
function ensureAiMcpConfig(cfg) {
|
|
724
|
+
ensureAgentDir()
|
|
725
|
+
const config = {
|
|
726
|
+
mcpServers: {
|
|
727
|
+
gitdone: {
|
|
728
|
+
type: 'http',
|
|
729
|
+
url: `${cfg.url}/api/mcp`,
|
|
730
|
+
headers: {
|
|
731
|
+
Authorization: `Bearer ${cfg.key}`,
|
|
732
|
+
'X-Gitdone-Machine-Id': cfg.machineId,
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
}
|
|
737
|
+
writeFileSync(AI_MCP_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8')
|
|
738
|
+
return AI_MCP_CONFIG_PATH
|
|
739
|
+
}
|
|
740
|
+
|
|
678
741
|
// Run a headless Claude Code session for an `ai_run` command and stream its
|
|
679
742
|
// output back. Long-running and fire-and-forget: it wires up async handlers and
|
|
680
743
|
// returns immediately so the agent's snapshot loop is never blocked.
|
|
@@ -698,6 +761,8 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
698
761
|
|
|
699
762
|
let settingsPath
|
|
700
763
|
try { settingsPath = ensureAiHookFiles() } catch (e) { log(`✗ ai hook setup failed: ${e.message}`) }
|
|
764
|
+
let mcpConfigPath
|
|
765
|
+
try { mcpConfigPath = ensureAiMcpConfig(cfg) } catch (e) { log(`✗ ai mcp config setup failed: ${e.message}`) }
|
|
701
766
|
|
|
702
767
|
// The prompt is delivered on STDIN, not as a `-p <arg>` command-line value.
|
|
703
768
|
// A multi-line, non-ASCII (Cyrillic) prompt passed as an argv element gets
|
|
@@ -714,6 +779,8 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
714
779
|
// Block git commit/push unless this repo opted in (per-repo aiAutoCommit).
|
|
715
780
|
...(allowCommit ? [] : ['--disallowedTools', 'Bash(git commit *),Bash(git push *)']),
|
|
716
781
|
...(settingsPath ? ['--settings', settingsPath] : []),
|
|
782
|
+
// Our own gitdone MCP, tagged with this machine (gd-308).
|
|
783
|
+
...(mcpConfigPath ? ['--mcp-config', mcpConfigPath, '--strict-mcp-config'] : []),
|
|
717
784
|
]
|
|
718
785
|
|
|
719
786
|
// The PostToolUse hook reads these from its env to post raw tool output.
|
|
@@ -850,6 +917,8 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
850
917
|
|
|
851
918
|
let settingsPath
|
|
852
919
|
try { settingsPath = ensureAiSessionHookFiles() } catch (e) { log(`✗ ai session hook setup failed: ${e.message}`) }
|
|
920
|
+
let mcpConfigPath
|
|
921
|
+
try { mcpConfigPath = ensureAiMcpConfig(cfg) } catch (e) { log(`✗ ai mcp config setup failed: ${e.message}`) }
|
|
853
922
|
|
|
854
923
|
const args = [
|
|
855
924
|
'-p',
|
|
@@ -864,6 +933,9 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
864
933
|
...(allowCommit ? [] : ['--disallowedTools', 'Bash(git commit *),Bash(git push *)']),
|
|
865
934
|
...(claudeSessionId ? ['--resume', claudeSessionId] : []),
|
|
866
935
|
...(settingsPath ? ['--settings', settingsPath] : []),
|
|
936
|
+
// Our own gitdone MCP, tagged with this machine so ai_agent_start attributes
|
|
937
|
+
// the agents to this computer (gd-308). --strict = use exactly this server.
|
|
938
|
+
...(mcpConfigPath ? ['--mcp-config', mcpConfigPath, '--strict-mcp-config'] : []),
|
|
867
939
|
]
|
|
868
940
|
|
|
869
941
|
const childEnv = { ...process.env, GITDONE_URL: cfg.url, GITDONE_KEY: cfg.key, GITDONE_SESSION_ID: sessionId }
|