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.
Files changed (2) hide show
  1. package/index.js +77 -5
  2. 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.4'
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 match = text.match(/^diff --git a\/.+ b\/(.+)$/m)
419
- if (match) files[match[1]] = text
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 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
5
5
  "type": "module",
6
6
  "bin": {