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.
- package/index.js +94 -1
- 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.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.
|