gitdone-agent 0.6.3 → 0.6.4

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 +59 -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.4'
31
31
 
32
32
  const AGENT_DIR = join(homedir(), '.gitdone-agent')
33
33
  const CONFIG_PATH = join(AGENT_DIR, 'config.json')
@@ -806,6 +806,24 @@ async function downloadSessionImages(sessionId, urls) {
806
806
  return { dir, paths }
807
807
  }
808
808
 
809
+ // Live chat turns, keyed by sessionId → their spawned `claude` child. Lets an
810
+ // `ai_chat_stop` command find and kill the turn a user asked to interrupt
811
+ // (gd-303). A turn registers itself on spawn and removes itself on close.
812
+ const chatChildren = new Map()
813
+
814
+ // Kill a process AND its descendants. `claude` spawns its own children (bash,
815
+ // node hooks), so killing just the top pid would orphan them. Windows needs
816
+ // `taskkill /T`; POSIX kills the process group, falling back to a hard kill.
817
+ function killTree(child) {
818
+ const pid = child?.pid
819
+ if (!pid) return
820
+ if (process.platform === 'win32') {
821
+ try { spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true }) } catch { /* best-effort */ }
822
+ } else {
823
+ try { process.kill(-pid, 'SIGKILL') } catch { try { child.kill('SIGKILL') } catch { /* gone */ } }
824
+ }
825
+ }
826
+
809
827
  // Run one turn of an interactive chat session: `claude -p` (resuming the prior
810
828
  // Claude session when known) with the user's message on stdin, streaming the
811
829
  // reply back to the session console. Long-running + fire-and-forget like ai_run.
@@ -912,6 +930,9 @@ async function runAiChat(cfg, cmd, repoPath) {
912
930
  return
913
931
  }
914
932
 
933
+ // Track this turn so an `ai_chat_stop` command can find and kill it (gd-303).
934
+ chatChildren.set(sessionId, child)
935
+
915
936
  if (child.stdin) {
916
937
  child.stdin.on('error', () => {})
917
938
  try { child.stdin.write(fullPrompt); child.stdin.end() }
@@ -932,10 +953,23 @@ async function runAiChat(cfg, cmd, repoPath) {
932
953
  child.on('error', (err) => push('SYSTEM', `Процесна грешка: ${err.message}`))
933
954
  child.on('close', async (code) => {
934
955
  clearInterval(timer)
956
+ chatChildren.delete(sessionId)
935
957
  if (imgDir) { try { rmSync(imgDir, { recursive: true, force: true }) } catch { /* best-effort */ } }
936
958
  if (buf.trim()) parseStreamLine(buf.trim(), push, onInit, onDelta)
937
959
  liveText = '' // turn is over — drop any lingering live preview
938
960
  await flush()
961
+ // User-initiated stop (gd-303): land on idle with a friendly note, not an
962
+ // "exited with code N" error — the kill's non-zero code is expected here.
963
+ if (child.__stopped) {
964
+ await postSessionEvents(
965
+ cfg, sessionId,
966
+ [{ role: 'SYSTEM', text: '⏹ Спряно. Напиши още нещо, за да продължим разговора.' }],
967
+ 'idle', capturedSession || undefined, '',
968
+ )
969
+ reportCommandResult(cfg, cmd.id, 'done', 'stopped by user')
970
+ log(`⏹ ai_chat ${sessionId} спряно от потребителя`)
971
+ return
972
+ }
939
973
  const ok = code === 0
940
974
  await postSessionEvents(
941
975
  cfg, sessionId,
@@ -949,6 +983,24 @@ async function runAiChat(cfg, cmd, repoPath) {
949
983
  })
950
984
  }
951
985
 
986
+ // ai_chat_stop — interrupt a running chat turn the user asked to stop (gd-303).
987
+ // Kills the tracked child (its `close` handler then posts the "⏹ Спряно" note and
988
+ // flips the session to idle). If nothing is running, just idles the session so
989
+ // the console unlocks.
990
+ function stopAiChat(cfg, cmd) {
991
+ const sessionId = cmd.payload?.sessionId
992
+ const child = sessionId ? chatChildren.get(sessionId) : null
993
+ if (!child) {
994
+ if (sessionId) postSessionEvents(cfg, sessionId, [], 'idle', undefined, '')
995
+ reportCommandResult(cfg, cmd.id, 'done', 'no active turn')
996
+ return
997
+ }
998
+ child.__stopped = true
999
+ killTree(child)
1000
+ log(`⏹ ai_chat_stop ${sessionId} — kill signalled`)
1001
+ reportCommandResult(cfg, cmd.id, 'done', 'stop signalled')
1002
+ }
1003
+
952
1004
  // deploy — run the project's deploy script (scripts/deploy.sh) in the repo.
953
1005
  // Long-running (git pull + npm build + service restart), so we spawn it detached
954
1006
  // from the poll loop and report the result on exit. The agent is its OWN process
@@ -993,6 +1045,12 @@ async function executeCommand(cfg, cmd, repoPath) {
993
1045
  runAiChat(cfg, cmd, repoPath)
994
1046
  return
995
1047
  }
1048
+ // ai_chat_stop — interrupt the running turn for a session (gd-303). Handled
1049
+ // inline (it just signals a kill) and reports its own result.
1050
+ if (cmd.type === 'ai_chat_stop') {
1051
+ stopAiChat(cfg, cmd)
1052
+ return
1053
+ }
996
1054
  // deploy — run the repo's deploy script. Long-running (pull + build + service
997
1055
  // restart), so it's launched in the background and reports its own result on
998
1056
  // 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.4",
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": {