gitdone-agent 0.6.3 → 0.6.5

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 +94 -1
  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.3'
30
+ const AGENT_VERSION = '0.6.5'
31
31
 
32
32
  const AGENT_DIR = join(homedir(), '.gitdone-agent')
33
33
  const CONFIG_PATH = join(AGENT_DIR, 'config.json')
@@ -675,6 +675,32 @@ function ensureAiSessionHookFiles() {
675
675
  return AI_SESSION_SETTINGS_PATH
676
676
  }
677
677
 
678
+ // gitdone MCP config for headless sessions (gd-308). We provide it ourselves —
679
+ // with an X-Gitdone-Machine-Id header — so the server can attribute the AI agents
680
+ // this session registers (ai_agent_start) to THIS computer, and the „АИ Агенти"
681
+ // panel shows one sub-panel per machine instead of a single „Локален агент".
682
+ // Used with --strict-mcp-config so the session uses exactly this server. The
683
+ // agent's own key (a gdo_ key) authenticates the MCP endpoint.
684
+ const AI_MCP_CONFIG_PATH = join(AGENT_DIR, 'ai-mcp-config.json')
685
+
686
+ function ensureAiMcpConfig(cfg) {
687
+ ensureAgentDir()
688
+ const config = {
689
+ mcpServers: {
690
+ gitdone: {
691
+ type: 'http',
692
+ url: `${cfg.url}/api/mcp`,
693
+ headers: {
694
+ Authorization: `Bearer ${cfg.key}`,
695
+ 'X-Gitdone-Machine-Id': cfg.machineId,
696
+ },
697
+ },
698
+ },
699
+ }
700
+ writeFileSync(AI_MCP_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8')
701
+ return AI_MCP_CONFIG_PATH
702
+ }
703
+
678
704
  // Run a headless Claude Code session for an `ai_run` command and stream its
679
705
  // output back. Long-running and fire-and-forget: it wires up async handlers and
680
706
  // returns immediately so the agent's snapshot loop is never blocked.
@@ -698,6 +724,8 @@ function runAiCommand(cfg, cmd, repoPath) {
698
724
 
699
725
  let settingsPath
700
726
  try { settingsPath = ensureAiHookFiles() } catch (e) { log(`✗ ai hook setup failed: ${e.message}`) }
727
+ let mcpConfigPath
728
+ try { mcpConfigPath = ensureAiMcpConfig(cfg) } catch (e) { log(`✗ ai mcp config setup failed: ${e.message}`) }
701
729
 
702
730
  // The prompt is delivered on STDIN, not as a `-p <arg>` command-line value.
703
731
  // A multi-line, non-ASCII (Cyrillic) prompt passed as an argv element gets
@@ -714,6 +742,8 @@ function runAiCommand(cfg, cmd, repoPath) {
714
742
  // Block git commit/push unless this repo opted in (per-repo aiAutoCommit).
715
743
  ...(allowCommit ? [] : ['--disallowedTools', 'Bash(git commit *),Bash(git push *)']),
716
744
  ...(settingsPath ? ['--settings', settingsPath] : []),
745
+ // Our own gitdone MCP, tagged with this machine (gd-308).
746
+ ...(mcpConfigPath ? ['--mcp-config', mcpConfigPath, '--strict-mcp-config'] : []),
717
747
  ]
718
748
 
719
749
  // The PostToolUse hook reads these from its env to post raw tool output.
@@ -806,6 +836,24 @@ async function downloadSessionImages(sessionId, urls) {
806
836
  return { dir, paths }
807
837
  }
808
838
 
839
+ // Live chat turns, keyed by sessionId → their spawned `claude` child. Lets an
840
+ // `ai_chat_stop` command find and kill the turn a user asked to interrupt
841
+ // (gd-303). A turn registers itself on spawn and removes itself on close.
842
+ const chatChildren = new Map()
843
+
844
+ // Kill a process AND its descendants. `claude` spawns its own children (bash,
845
+ // node hooks), so killing just the top pid would orphan them. Windows needs
846
+ // `taskkill /T`; POSIX kills the process group, falling back to a hard kill.
847
+ function killTree(child) {
848
+ const pid = child?.pid
849
+ if (!pid) return
850
+ if (process.platform === 'win32') {
851
+ try { spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true }) } catch { /* best-effort */ }
852
+ } else {
853
+ try { process.kill(-pid, 'SIGKILL') } catch { try { child.kill('SIGKILL') } catch { /* gone */ } }
854
+ }
855
+ }
856
+
809
857
  // Run one turn of an interactive chat session: `claude -p` (resuming the prior
810
858
  // Claude session when known) with the user's message on stdin, streaming the
811
859
  // reply back to the session console. Long-running + fire-and-forget like ai_run.
@@ -832,6 +880,8 @@ async function runAiChat(cfg, cmd, repoPath) {
832
880
 
833
881
  let settingsPath
834
882
  try { settingsPath = ensureAiSessionHookFiles() } catch (e) { log(`✗ ai session hook setup failed: ${e.message}`) }
883
+ let mcpConfigPath
884
+ try { mcpConfigPath = ensureAiMcpConfig(cfg) } catch (e) { log(`✗ ai mcp config setup failed: ${e.message}`) }
835
885
 
836
886
  const args = [
837
887
  '-p',
@@ -846,6 +896,9 @@ async function runAiChat(cfg, cmd, repoPath) {
846
896
  ...(allowCommit ? [] : ['--disallowedTools', 'Bash(git commit *),Bash(git push *)']),
847
897
  ...(claudeSessionId ? ['--resume', claudeSessionId] : []),
848
898
  ...(settingsPath ? ['--settings', settingsPath] : []),
899
+ // Our own gitdone MCP, tagged with this machine so ai_agent_start attributes
900
+ // the agents to this computer (gd-308). --strict = use exactly this server.
901
+ ...(mcpConfigPath ? ['--mcp-config', mcpConfigPath, '--strict-mcp-config'] : []),
849
902
  ]
850
903
 
851
904
  const childEnv = { ...process.env, GITDONE_URL: cfg.url, GITDONE_KEY: cfg.key, GITDONE_SESSION_ID: sessionId }
@@ -912,6 +965,9 @@ async function runAiChat(cfg, cmd, repoPath) {
912
965
  return
913
966
  }
914
967
 
968
+ // Track this turn so an `ai_chat_stop` command can find and kill it (gd-303).
969
+ chatChildren.set(sessionId, child)
970
+
915
971
  if (child.stdin) {
916
972
  child.stdin.on('error', () => {})
917
973
  try { child.stdin.write(fullPrompt); child.stdin.end() }
@@ -932,10 +988,23 @@ async function runAiChat(cfg, cmd, repoPath) {
932
988
  child.on('error', (err) => push('SYSTEM', `Процесна грешка: ${err.message}`))
933
989
  child.on('close', async (code) => {
934
990
  clearInterval(timer)
991
+ chatChildren.delete(sessionId)
935
992
  if (imgDir) { try { rmSync(imgDir, { recursive: true, force: true }) } catch { /* best-effort */ } }
936
993
  if (buf.trim()) parseStreamLine(buf.trim(), push, onInit, onDelta)
937
994
  liveText = '' // turn is over — drop any lingering live preview
938
995
  await flush()
996
+ // User-initiated stop (gd-303): land on idle with a friendly note, not an
997
+ // "exited with code N" error — the kill's non-zero code is expected here.
998
+ if (child.__stopped) {
999
+ await postSessionEvents(
1000
+ cfg, sessionId,
1001
+ [{ role: 'SYSTEM', text: '⏹ Спряно. Напиши още нещо, за да продължим разговора.' }],
1002
+ 'idle', capturedSession || undefined, '',
1003
+ )
1004
+ reportCommandResult(cfg, cmd.id, 'done', 'stopped by user')
1005
+ log(`⏹ ai_chat ${sessionId} спряно от потребителя`)
1006
+ return
1007
+ }
939
1008
  const ok = code === 0
940
1009
  await postSessionEvents(
941
1010
  cfg, sessionId,
@@ -949,6 +1018,24 @@ async function runAiChat(cfg, cmd, repoPath) {
949
1018
  })
950
1019
  }
951
1020
 
1021
+ // ai_chat_stop — interrupt a running chat turn the user asked to stop (gd-303).
1022
+ // Kills the tracked child (its `close` handler then posts the "⏹ Спряно" note and
1023
+ // flips the session to idle). If nothing is running, just idles the session so
1024
+ // the console unlocks.
1025
+ function stopAiChat(cfg, cmd) {
1026
+ const sessionId = cmd.payload?.sessionId
1027
+ const child = sessionId ? chatChildren.get(sessionId) : null
1028
+ if (!child) {
1029
+ if (sessionId) postSessionEvents(cfg, sessionId, [], 'idle', undefined, '')
1030
+ reportCommandResult(cfg, cmd.id, 'done', 'no active turn')
1031
+ return
1032
+ }
1033
+ child.__stopped = true
1034
+ killTree(child)
1035
+ log(`⏹ ai_chat_stop ${sessionId} — kill signalled`)
1036
+ reportCommandResult(cfg, cmd.id, 'done', 'stop signalled')
1037
+ }
1038
+
952
1039
  // deploy — run the project's deploy script (scripts/deploy.sh) in the repo.
953
1040
  // Long-running (git pull + npm build + service restart), so we spawn it detached
954
1041
  // from the poll loop and report the result on exit. The agent is its OWN process
@@ -993,6 +1080,12 @@ async function executeCommand(cfg, cmd, repoPath) {
993
1080
  runAiChat(cfg, cmd, repoPath)
994
1081
  return
995
1082
  }
1083
+ // ai_chat_stop — interrupt the running turn for a session (gd-303). Handled
1084
+ // inline (it just signals a kill) and reports its own result.
1085
+ if (cmd.type === 'ai_chat_stop') {
1086
+ stopAiChat(cfg, cmd)
1087
+ return
1088
+ }
996
1089
  // deploy — run the repo's deploy script. Long-running (pull + build + service
997
1090
  // restart), so it's launched in the background and reports its own result on
998
1091
  // exit, same as ai_run.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
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": {