gitdone-agent 0.7.6 → 0.7.7
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 +101 -9
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { execSync, execFileSync, spawn } from 'node:child_process'
|
|
|
13
13
|
import {
|
|
14
14
|
existsSync, writeFileSync, readFileSync, unlinkSync, renameSync,
|
|
15
15
|
mkdirSync, copyFileSync, appendFileSync, readdirSync, rmSync, statSync,
|
|
16
|
+
openSync, writeSync, fsyncSync, closeSync,
|
|
16
17
|
} from 'node:fs'
|
|
17
18
|
import { resolve, join } from 'node:path'
|
|
18
19
|
import { homedir, hostname, tmpdir } from 'node:os'
|
|
@@ -27,10 +28,12 @@ import { randomUUID, createHash } from 'node:crypto'
|
|
|
27
28
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
28
29
|
// Keep in lockstep with packages/agent/package.json "version" AND
|
|
29
30
|
// src/lib/agentVersion.ts LATEST_AGENT_VERSION.
|
|
30
|
-
const AGENT_VERSION = '0.7.
|
|
31
|
+
const AGENT_VERSION = '0.7.7'
|
|
31
32
|
|
|
32
33
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
33
34
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
35
|
+
// Last-known-good copy of config.json, refreshed once per successful start.
|
|
36
|
+
const CONFIG_BAK_PATH = CONFIG_PATH + '.bak'
|
|
34
37
|
const STABLE_AGENT = join(AGENT_DIR, 'agent.mjs')
|
|
35
38
|
const LOG_PATH = join(AGENT_DIR, 'agent.log')
|
|
36
39
|
|
|
@@ -80,20 +83,98 @@ function survive(kind, err) {
|
|
|
80
83
|
process.on('uncaughtException', (err) => survive('uncaught exception', err))
|
|
81
84
|
process.on('unhandledRejection', (err) => survive('unhandled rejection', err))
|
|
82
85
|
|
|
83
|
-
function
|
|
84
|
-
try {
|
|
86
|
+
function parseConfigFile(path) {
|
|
87
|
+
try {
|
|
88
|
+
// Strip a UTF-8 BOM: we never write one, but a config repaired by hand in an
|
|
89
|
+
// editor that adds one would otherwise be unparsable — and "I fixed the file
|
|
90
|
+
// and it still says no config" is the worst possible dead end (gd-478).
|
|
91
|
+
const cfg = JSON.parse(readFileSync(path, 'utf8').replace(/^/, ''))
|
|
92
|
+
return cfg && typeof cfg === 'object' && cfg.key ? cfg : null
|
|
93
|
+
} catch { return null }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Rebuild the bare minimum from ai-mcp-config.json — last resort when both
|
|
97
|
+
// config.json and its backup are gone. It is written from cfg.key/cfg.machineId
|
|
98
|
+
// on every AI session, so it carries the two values that cannot be regenerated
|
|
99
|
+
// locally (a new machineId would show up as a SECOND machine on the server, and
|
|
100
|
+
// there is no way to invent the API key). Roots are lost, but an agent that is
|
|
101
|
+
// online with no roots can be fixed from the web UI; an offline one cannot.
|
|
102
|
+
function recoverConfigFromAiMcp() {
|
|
103
|
+
try {
|
|
104
|
+
const mcp = JSON.parse(readFileSync(join(AGENT_DIR, 'ai-mcp-config.json'), 'utf8'))
|
|
105
|
+
const server = mcp?.mcpServers?.gitdone
|
|
106
|
+
const key = server?.headers?.Authorization?.replace(/^Bearer\s+/, '')
|
|
107
|
+
const machineId = server?.headers?.['X-Gitdone-Machine-Id']
|
|
108
|
+
if (!key || !machineId || !server.url) return null
|
|
109
|
+
return {
|
|
110
|
+
key,
|
|
111
|
+
url: server.url.replace(/\/api\/mcp\/?$/, ''),
|
|
112
|
+
interval: 30,
|
|
113
|
+
machineId,
|
|
114
|
+
hostname: hostname(),
|
|
115
|
+
roots: [],
|
|
116
|
+
}
|
|
117
|
+
} catch { return null }
|
|
85
118
|
}
|
|
86
119
|
|
|
120
|
+
// True when the running config had to be rebuilt — surfaced by --doctor.
|
|
121
|
+
let configWasRecovered = null
|
|
122
|
+
|
|
123
|
+
// An unclean shutdown (power loss, BSOD) can leave config.json committed by the
|
|
124
|
+
// filesystem as a NUL-filled file of the right length: intact on disk, garbage
|
|
125
|
+
// to JSON.parse. Before gd-478 that bricked the agent for good — every autostart
|
|
126
|
+
// exited with "no config found" and the supervisor just respawned it every 10s,
|
|
127
|
+
// so the machine stayed offline until somebody hand-edited the file. A user
|
|
128
|
+
// without filesystem access had no way out at all. So never trust the primary
|
|
129
|
+
// copy blindly: fall back to the last-known-good backup, then to the AI MCP
|
|
130
|
+
// config, and put whichever survived back in place.
|
|
131
|
+
function readConfig() {
|
|
132
|
+
const primary = parseConfigFile(CONFIG_PATH)
|
|
133
|
+
if (primary) return primary
|
|
134
|
+
// Nothing to recover from on a fresh machine (install path) — stay quiet.
|
|
135
|
+
if (!existsSync(CONFIG_PATH) && !existsSync(CONFIG_BAK_PATH)) return null
|
|
136
|
+
|
|
137
|
+
for (const [source, cfg] of [
|
|
138
|
+
['config.json.bak', parseConfigFile(CONFIG_BAK_PATH)],
|
|
139
|
+
['ai-mcp-config.json', recoverConfigFromAiMcp()],
|
|
140
|
+
]) {
|
|
141
|
+
if (!cfg) continue
|
|
142
|
+
configWasRecovered = source
|
|
143
|
+
log(`✗ config.json е повреден (най-вероятно от неочаквано изключване) — възстановен от ${source}`)
|
|
144
|
+
try { writeConfig(cfg) } catch (err) { log(`✗ записът на възстановения config не успя: ${err.message}`) }
|
|
145
|
+
return cfg
|
|
146
|
+
}
|
|
147
|
+
log('✗ config.json е повреден и няма от какво да се възстанови — пусни gitdone-agent --key=... --root=... --install')
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Atomic AND durable: write to a temp file, force it to the platter, and only
|
|
152
|
+
// then rename over the real one. The fsync is the part that matters — without
|
|
153
|
+
// it the rename can be committed while the data is still in the page cache, and
|
|
154
|
+
// a power cut in that window is exactly what produced the NUL-filled config in
|
|
155
|
+
// gd-478. Losing the rename is harmless (the old config survives); losing the
|
|
156
|
+
// data is not.
|
|
87
157
|
function writeConfig(cfg) {
|
|
88
158
|
ensureAgentDir()
|
|
89
|
-
// Write-then-rename so a crash / reboot mid-write can never leave a truncated
|
|
90
|
-
// config.json — a corrupt config makes every subsequent autostart exit
|
|
91
|
-
// immediately, which reads as "агентът умря и рестартът не помага" (gd-403).
|
|
92
159
|
const tmp = CONFIG_PATH + '.tmp'
|
|
93
|
-
|
|
160
|
+
const fd = openSync(tmp, 'w')
|
|
161
|
+
try {
|
|
162
|
+
writeSync(fd, JSON.stringify(cfg, null, 2), null, 'utf8')
|
|
163
|
+
fsyncSync(fd)
|
|
164
|
+
} finally {
|
|
165
|
+
closeSync(fd)
|
|
166
|
+
}
|
|
94
167
|
renameSync(tmp, CONFIG_PATH)
|
|
95
168
|
}
|
|
96
169
|
|
|
170
|
+
// Refresh the last-known-good copy. Called once per successful start, so the
|
|
171
|
+
// backup is always a config that actually booted the agent — never a half-baked
|
|
172
|
+
// one. Best-effort: a failed backup must not stop the agent from running.
|
|
173
|
+
function backupConfig(cfg) {
|
|
174
|
+
try { writeFileSync(CONFIG_BAK_PATH, JSON.stringify(cfg, null, 2), 'utf8') }
|
|
175
|
+
catch (err) { log(`✗ backup на config.json не успя: ${err.message}`) }
|
|
176
|
+
}
|
|
177
|
+
|
|
97
178
|
// ─── Arg parsing ─────────────────────────────────────────────────────────────
|
|
98
179
|
|
|
99
180
|
function parseArgs() {
|
|
@@ -458,7 +539,12 @@ function runDoctor() {
|
|
|
458
539
|
let hb = '(none yet)'
|
|
459
540
|
try { hb = `(последен: преди ${Math.round((Date.now() - statSync(HEARTBEAT_PATH).mtimeMs) / 1000)}s)` } catch { /* no beat yet */ }
|
|
460
541
|
console.log(` heartbeat : ${HEARTBEAT_PATH} ${hb}`)
|
|
461
|
-
|
|
542
|
+
// cfg comes from readConfig() above, so a corrupt config.json has already been
|
|
543
|
+
// repaired by the time we print this — say so rather than a bare "(ok)".
|
|
544
|
+
const cfgState = configWasRecovered ? `(ВЪЗСТАНОВЕН от ${configWasRecovered} — беше повреден)`
|
|
545
|
+
: cfg ? '(ok)' : existsSync(CONFIG_PATH) ? '(ПОВРЕДЕН)' : '(MISSING)'
|
|
546
|
+
console.log(` config : ${CONFIG_PATH} ${cfgState}`)
|
|
547
|
+
console.log(` config backup : ${CONFIG_BAK_PATH} ${parseConfigFile(CONFIG_BAK_PATH) ? '(ok)' : '(няма — прави се при първия успешен старт)'}`)
|
|
462
548
|
console.log(` log file : ${LOG_PATH} ${existsSync(LOG_PATH) ? '(ok)' : '(none yet)'}`)
|
|
463
549
|
console.log(` crash log : ${join(AGENT_DIR, 'agent-crash.log')} ${existsSync(join(AGENT_DIR, 'agent-crash.log')) ? '(има записи — виж го при проблеми)' : '(празен — няма крашове)'}`)
|
|
464
550
|
const claude = findClaude()
|
|
@@ -2094,6 +2180,9 @@ async function runLoop(cfg) {
|
|
|
2094
2180
|
// start so in-place-updated agents get the watchdog without a re-install.
|
|
2095
2181
|
startHeartbeat()
|
|
2096
2182
|
ensureWatchdog()
|
|
2183
|
+
// This config just booted an agent, so it is known-good — snapshot it for
|
|
2184
|
+
// readConfig() to fall back on (gd-478).
|
|
2185
|
+
backupConfig(cfg)
|
|
2097
2186
|
log(`gitdone-agent started — machine: ${cfg.hostname}, server: ${cfg.url}, interval: ${cfg.interval}s`)
|
|
2098
2187
|
log(` roots: ${cfg.roots.join(', ') || '(none — add one with --root)'}`)
|
|
2099
2188
|
|
|
@@ -2169,7 +2258,10 @@ async function main() {
|
|
|
2169
2258
|
// No args → running mode (this is how autostart launches us): read config.
|
|
2170
2259
|
const cfg = readConfig()
|
|
2171
2260
|
if (!cfg || !cfg.key) {
|
|
2172
|
-
|
|
2261
|
+
// Through agent.log, not just stderr — the autostart runs hidden, so the
|
|
2262
|
+
// crash log is the only other place this would land and nobody reads it
|
|
2263
|
+
// until someone already suspects the agent (gd-478).
|
|
2264
|
+
log(`✗ няма използваем config в ${CONFIG_PATH} — пусни gitdone-agent --key=... --root=... --install`)
|
|
2173
2265
|
process.exit(1)
|
|
2174
2266
|
}
|
|
2175
2267
|
await runLoop(cfg)
|