gitdone-agent 0.6.15 → 0.6.17
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 +128 -27
- package/package.json +15 -15
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.17'
|
|
31
31
|
|
|
32
32
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
33
33
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -186,10 +186,52 @@ function findClaude() {
|
|
|
186
186
|
return { path: 'claude', shell: true, found: false }
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// ─── Single instance (gd-407) ─────────────────────────────────────────────────
|
|
190
|
+
// Two agents on one machine (an old one surviving an update, or a manual
|
|
191
|
+
// `npx gitdone-agent` next to the autostarted one) flap the machine's online
|
|
192
|
+
// state and race dispatches. The running agent claims the machine via a pid
|
|
193
|
+
// file; a newcomer that finds a LIVE agent behind it exits with EXIT_DUPLICATE,
|
|
194
|
+
// which the supervisor treats as "do not restart me". First one wins.
|
|
195
|
+
// 86 is outside node's own exit-code range (1-13) — keep in lockstep with the
|
|
196
|
+
// run-agent.cmd template in installStartup().
|
|
197
|
+
const EXIT_DUPLICATE = 86
|
|
198
|
+
const PID_PATH = join(AGENT_DIR, 'agent.pid')
|
|
199
|
+
|
|
200
|
+
// Command line of a live process, '' when unreadable (dead process, no rights).
|
|
201
|
+
function processCmdline(pid) {
|
|
202
|
+
try {
|
|
203
|
+
if (process.platform === 'win32') {
|
|
204
|
+
return execSync(
|
|
205
|
+
`powershell -NoProfile -NonInteractive -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CommandLine"`,
|
|
206
|
+
{ encoding: 'utf8' },
|
|
207
|
+
).trim()
|
|
208
|
+
}
|
|
209
|
+
return execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8' }).trim()
|
|
210
|
+
} catch { return '' }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function ensureSingleInstance() {
|
|
214
|
+
try {
|
|
215
|
+
const pid = parseInt(readFileSync(PID_PATH, 'utf8'), 10)
|
|
216
|
+
if (pid && pid !== process.pid) {
|
|
217
|
+
process.kill(pid, 0) // throws when that pid is no longer alive
|
|
218
|
+
// PID reuse guard: only defer to a process that really looks like an
|
|
219
|
+
// agent (stable copy agent.mjs, or any gitdone-agent npx/global run).
|
|
220
|
+
if (/gitdone-agent|agent\.mjs/i.test(processCmdline(pid))) {
|
|
221
|
+
log(`✗ друг gitdone-agent вече върви (PID ${pid}) — този процес излиза, за да няма два агента на машината`)
|
|
222
|
+
process.exit(EXIT_DUPLICATE)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch { /* no/stale pid file → the machine is free, we take over */ }
|
|
226
|
+
try { ensureAgentDir(); writeFileSync(PID_PATH, String(process.pid)) } catch { /* best-effort */ }
|
|
227
|
+
}
|
|
228
|
+
|
|
189
229
|
// Kill any previously-running agent processes so an update applies WITHOUT a
|
|
190
|
-
// Windows re-login. Targets node processes
|
|
191
|
-
//
|
|
192
|
-
// npx cache
|
|
230
|
+
// Windows re-login. Targets node processes that look like a gitdone agent —
|
|
231
|
+
// both the stable copy in ~/.gitdone-agent AND one running straight from the
|
|
232
|
+
// npx cache / global install (its command line contains "gitdone-agent") —
|
|
233
|
+
// excluding ourselves and our parent (the npx wrapper that launched us).
|
|
234
|
+
// Best-effort; failures are non-fatal.
|
|
193
235
|
function stopRunningAgents() {
|
|
194
236
|
// Kill the supervisor loops (cmd.exe running run-agent.cmd) FIRST, then the
|
|
195
237
|
// agents — the other order lets a still-alive supervisor immediately respawn
|
|
@@ -200,7 +242,7 @@ function stopRunningAgents() {
|
|
|
200
242
|
" Where-Object { $_.CommandLine -like '*run-agent.cmd*' } |",
|
|
201
243
|
' ForEach-Object { Stop-Process -Id $_.ProcessId -Force }',
|
|
202
244
|
"Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" |",
|
|
203
|
-
` Where-Object { $_.CommandLine -like '
|
|
245
|
+
` Where-Object { ($_.CommandLine -like '*gitdone-agent*' -or $_.CommandLine -like '*agent.mjs*') -and $_.ProcessId -ne ${process.pid} -and $_.ProcessId -ne ${process.ppid || 0} } |`,
|
|
204
246
|
' ForEach-Object { Stop-Process -Id $_.ProcessId -Force }',
|
|
205
247
|
].join('\n')
|
|
206
248
|
try {
|
|
@@ -254,12 +296,16 @@ function installStartup() {
|
|
|
254
296
|
`"%NODE%" "${agentPath}" 2>> "${crashLog}"`,
|
|
255
297
|
'set CODE=%errorlevel%',
|
|
256
298
|
`echo [%date% %time%] agent exited (code %CODE%) - restart in 10s >> "${crashLog}"`,
|
|
299
|
+
'if "%CODE%"=="86" goto duplicate',
|
|
257
300
|
'if "%CODE%"=="-1073741502" goto dead',
|
|
258
301
|
'if "%CODE%"=="-1073741515" goto dead',
|
|
259
302
|
'ping -n 11 127.0.0.1 >nul 2>nul || goto dead',
|
|
260
303
|
'goto loop',
|
|
261
304
|
':dead',
|
|
262
305
|
`echo [%date% %time%] node cannot start (code %CODE%) - supervisor giving up until next login >> "${crashLog}"`,
|
|
306
|
+
'exit /b',
|
|
307
|
+
':duplicate',
|
|
308
|
+
`echo [%date% %time%] another agent already runs this machine (code 86) - this supervisor exits >> "${crashLog}"`,
|
|
263
309
|
].join('\r\n')
|
|
264
310
|
const cmdPath = join(AGENT_DIR, 'run-agent.cmd')
|
|
265
311
|
writeFileSync(cmdPath, cmd, 'utf8')
|
|
@@ -713,20 +759,24 @@ async function reportCommandResult(cfg, id, status, result) {
|
|
|
713
759
|
|
|
714
760
|
// Post a batch of console events (and optional lifecycle status) for an AiRun.
|
|
715
761
|
// `usage` is sent once on the final (done/error) post so the server can record
|
|
716
|
-
// how many tokens the run cost.
|
|
717
|
-
|
|
762
|
+
// how many tokens the run cost. `streamingText` / `activityText` (gd-419) carry
|
|
763
|
+
// the live "being typed" preview and the latest thinking snippet — explicit ''
|
|
764
|
+
// clears them; `undefined` leaves them untouched.
|
|
765
|
+
async function postRunEvents(cfg, runId, events, status, result, usage, streamingText, activityText) {
|
|
718
766
|
await api(cfg, '/api/v1/agent/ai-run/events', {
|
|
719
767
|
runId,
|
|
720
768
|
events,
|
|
721
769
|
...(status ? { status } : {}),
|
|
722
770
|
...(result !== undefined ? { result } : {}),
|
|
723
771
|
...(usage ? { usage } : {}),
|
|
772
|
+
...(streamingText !== undefined ? { streamingText } : {}),
|
|
773
|
+
...(activityText !== undefined ? { activityText } : {}),
|
|
724
774
|
}).catch((e) => log(`✗ ai-run events post failed: ${e.message}`))
|
|
725
775
|
}
|
|
726
776
|
|
|
727
777
|
// Post transcript lines (and optional turn status / Claude session id) for an
|
|
728
778
|
// interactive AiSession chat turn.
|
|
729
|
-
async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId, streamingText) {
|
|
779
|
+
async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId, streamingText, activityText) {
|
|
730
780
|
await api(cfg, '/api/v1/agent/ai-session/events', {
|
|
731
781
|
sessionId,
|
|
732
782
|
events,
|
|
@@ -734,6 +784,8 @@ async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId
|
|
|
734
784
|
...(claudeSessionId ? { claudeSessionId } : {}),
|
|
735
785
|
// Explicit '' clears the live preview; `undefined` leaves it untouched.
|
|
736
786
|
...(streamingText !== undefined ? { streamingText } : {}),
|
|
787
|
+
// Same semantics for the live thinking snippet (gd-419).
|
|
788
|
+
...(activityText !== undefined ? { activityText } : {}),
|
|
737
789
|
}).catch((e) => log(`✗ ai-session events post failed: ${e.message}`))
|
|
738
790
|
}
|
|
739
791
|
|
|
@@ -764,13 +816,18 @@ function parseStreamLine(line, push, onInit, onDelta, onMeta) {
|
|
|
764
816
|
}
|
|
765
817
|
return
|
|
766
818
|
}
|
|
767
|
-
// Partial-message stream:
|
|
768
|
-
// preview
|
|
769
|
-
//
|
|
819
|
+
// Partial-message stream: the model's visible text deltas feed the live
|
|
820
|
+
// preview, its thinking deltas feed the gray "какво прави АИ-то" snippet
|
|
821
|
+
// (gd-419). Tool-input JSON deltas stay ignored — the full block still
|
|
822
|
+
// arrives as a normal `assistant` event below.
|
|
770
823
|
if (ev.type === 'stream_event' && typeof onDelta === 'function') {
|
|
771
824
|
const e = ev.event
|
|
772
|
-
if (e?.type === 'content_block_delta'
|
|
773
|
-
onDelta(e.delta.text)
|
|
825
|
+
if (e?.type === 'content_block_delta') {
|
|
826
|
+
if (e.delta?.type === 'text_delta' && e.delta.text) onDelta(e.delta.text, 'text')
|
|
827
|
+
else if (e.delta?.type === 'thinking_delta' && e.delta.thinking) onDelta(e.delta.thinking, 'thinking')
|
|
828
|
+
} else if (e?.type === 'content_block_start' && e.content_block?.type === 'thinking') {
|
|
829
|
+
// A fresh thought begins — reset the snippet so old and new don't blend.
|
|
830
|
+
onDelta('', 'thinking-start')
|
|
774
831
|
}
|
|
775
832
|
return
|
|
776
833
|
}
|
|
@@ -997,6 +1054,9 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
997
1054
|
'-p',
|
|
998
1055
|
'--output-format', 'stream-json',
|
|
999
1056
|
'--verbose',
|
|
1057
|
+
// Stream text token-by-token + thinking deltas so the terminal shows the
|
|
1058
|
+
// reply being written live, ред по ред, not in whole-block batches (gd-419).
|
|
1059
|
+
'--include-partial-messages',
|
|
1000
1060
|
// Model chosen in gitDone (project default / task); omitted → machine default (gd-354).
|
|
1001
1061
|
...(model ? ['--model', model] : []),
|
|
1002
1062
|
'--permission-mode', 'acceptEdits',
|
|
@@ -1014,15 +1074,39 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1014
1074
|
// Batch events on a timer so we don't hammer the server per token/line.
|
|
1015
1075
|
let pending = []
|
|
1016
1076
|
let flushing = false
|
|
1077
|
+
let liveText = '' // in-progress text of the current assistant block (live preview)
|
|
1078
|
+
let sentLive = '' // last streamingText we posted — only push on change
|
|
1079
|
+
let liveActivity = '' // latest thinking snippet — "какво прави АИ-то" (gd-419)
|
|
1080
|
+
let sentActivity = ''
|
|
1017
1081
|
const flush = async () => {
|
|
1018
|
-
if (flushing
|
|
1082
|
+
if (flushing) return
|
|
1083
|
+
const liveChanged = liveText !== sentLive
|
|
1084
|
+
const activityChanged = liveActivity !== sentActivity
|
|
1085
|
+
if (pending.length === 0 && !liveChanged && !activityChanged) return
|
|
1019
1086
|
flushing = true
|
|
1020
1087
|
const batch = pending; pending = []
|
|
1021
|
-
|
|
1088
|
+
const streamingText = liveChanged ? liveText : undefined
|
|
1089
|
+
const activityText = activityChanged ? liveActivity : undefined
|
|
1090
|
+
sentLive = liveText
|
|
1091
|
+
sentActivity = liveActivity
|
|
1092
|
+
await postRunEvents(cfg, runId, batch, undefined, undefined, undefined, streamingText, activityText)
|
|
1022
1093
|
flushing = false
|
|
1023
1094
|
}
|
|
1024
|
-
const push = (kind, text) => {
|
|
1025
|
-
|
|
1095
|
+
const push = (kind, text) => {
|
|
1096
|
+
if (text == null || String(text) === '') return
|
|
1097
|
+
// A completed text block becomes a real event — drop its live preview; any
|
|
1098
|
+
// new event also supersedes the last thinking snippet.
|
|
1099
|
+
if (kind === 'TEXT') liveText = ''
|
|
1100
|
+
liveActivity = ''
|
|
1101
|
+
pending.push({ kind, text: String(text) })
|
|
1102
|
+
}
|
|
1103
|
+
const onDelta = (chunk, type) => {
|
|
1104
|
+
if (type === 'thinking-start') { liveActivity = ''; return }
|
|
1105
|
+
// Keep only the tail — the freshest thought is what the gray line shows.
|
|
1106
|
+
if (type === 'thinking') { liveActivity = (liveActivity + chunk).slice(-4000); return }
|
|
1107
|
+
liveText += chunk
|
|
1108
|
+
}
|
|
1109
|
+
const timer = setInterval(flush, 500)
|
|
1026
1110
|
|
|
1027
1111
|
// Accumulate model + token usage across the stream (model from init, usage
|
|
1028
1112
|
// from the final result event) to report once on exit (gd-334).
|
|
@@ -1061,14 +1145,16 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1061
1145
|
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
1062
1146
|
const line = buf.slice(0, nl).trim()
|
|
1063
1147
|
buf = buf.slice(nl + 1)
|
|
1064
|
-
if (line) parseStreamLine(line, push, undefined,
|
|
1148
|
+
if (line) parseStreamLine(line, push, undefined, onDelta, onMeta)
|
|
1065
1149
|
}
|
|
1066
1150
|
})
|
|
1067
1151
|
child.stderr.on('data', (d) => { const s = d.toString().trim(); if (s) push('SYSTEM', s) })
|
|
1068
1152
|
child.on('error', (err) => push('SYSTEM', `Процесна грешка: ${err.message}`))
|
|
1069
1153
|
child.on('close', async (code) => {
|
|
1070
1154
|
clearInterval(timer)
|
|
1071
|
-
if (buf.trim()) parseStreamLine(buf.trim(), push, undefined,
|
|
1155
|
+
if (buf.trim()) parseStreamLine(buf.trim(), push, undefined, onDelta, onMeta)
|
|
1156
|
+
liveText = '' // run is over — drop any lingering live preview / thought
|
|
1157
|
+
liveActivity = ''
|
|
1072
1158
|
await flush()
|
|
1073
1159
|
const ok = code === 0
|
|
1074
1160
|
// Build the usage payload + a console summary line from what we saw.
|
|
@@ -1192,32 +1278,44 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
1192
1278
|
let flushing = false
|
|
1193
1279
|
let liveText = '' // in-progress text of the current assistant block (live preview)
|
|
1194
1280
|
let sentLive = '' // last streamingText we posted — so we only push on change
|
|
1281
|
+
let liveActivity = '' // latest thinking snippet — "какво прави АИ-то" (gd-419)
|
|
1282
|
+
let sentActivity = ''
|
|
1195
1283
|
let lastPostAt = 0 // Date.now() of the last post, for the keep-alive heartbeat
|
|
1196
1284
|
const flush = async () => {
|
|
1197
1285
|
if (flushing) return
|
|
1198
1286
|
const hasEvents = pending.length > 0
|
|
1199
1287
|
const liveChanged = liveText !== sentLive
|
|
1288
|
+
const activityChanged = liveActivity !== sentActivity
|
|
1200
1289
|
// Heartbeat: with nothing new for a while, still post (bumps lastActivityAt)
|
|
1201
1290
|
// so the console shows honest "still working" and can spot a real stall.
|
|
1202
|
-
const heartbeat = !hasEvents && !liveChanged && Date.now() - lastPostAt > 2500
|
|
1203
|
-
if (!hasEvents && !liveChanged && !heartbeat) return
|
|
1291
|
+
const heartbeat = !hasEvents && !liveChanged && !activityChanged && Date.now() - lastPostAt > 2500
|
|
1292
|
+
if (!hasEvents && !liveChanged && !activityChanged && !heartbeat) return
|
|
1204
1293
|
flushing = true
|
|
1205
1294
|
const batch = pending.map((e) => ({ role: roleFor(e.kind), text: e.text })); pending = []
|
|
1206
1295
|
const streamingText = liveChanged ? liveText : undefined
|
|
1296
|
+
const activityText = activityChanged ? liveActivity : undefined
|
|
1207
1297
|
sentLive = liveText
|
|
1208
|
-
|
|
1298
|
+
sentActivity = liveActivity
|
|
1299
|
+
await postSessionEvents(cfg, sessionId, batch, 'running', capturedSession || undefined, streamingText, activityText)
|
|
1209
1300
|
lastPostAt = Date.now()
|
|
1210
1301
|
flushing = false
|
|
1211
1302
|
}
|
|
1212
1303
|
const push = (kind, text) => {
|
|
1213
1304
|
if (text == null || String(text) === '') return
|
|
1214
1305
|
// A completed text block becomes a real event — drop its live preview so the
|
|
1215
|
-
// finalised bubble and the cleared preview swap in on the same flush.
|
|
1306
|
+
// finalised bubble and the cleared preview swap in on the same flush. Any
|
|
1307
|
+
// new event also supersedes the last thinking snippet (gd-419).
|
|
1216
1308
|
if (kind === 'TEXT') liveText = ''
|
|
1309
|
+
liveActivity = ''
|
|
1217
1310
|
pending.push({ kind, text: String(text) })
|
|
1218
1311
|
}
|
|
1219
1312
|
const onInit = (sid) => { if (sid && !claudeSessionId) capturedSession = sid }
|
|
1220
|
-
const onDelta = (chunk) => {
|
|
1313
|
+
const onDelta = (chunk, type) => {
|
|
1314
|
+
if (type === 'thinking-start') { liveActivity = ''; return }
|
|
1315
|
+
// Keep only the tail — the freshest thought is what the gray line shows.
|
|
1316
|
+
if (type === 'thinking') { liveActivity = (liveActivity + chunk).slice(-4000); return }
|
|
1317
|
+
liveText += chunk
|
|
1318
|
+
}
|
|
1221
1319
|
const timer = setInterval(flush, 500)
|
|
1222
1320
|
|
|
1223
1321
|
// Fetch any attached images locally and fold their paths into the prompt so
|
|
@@ -1273,7 +1371,8 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
1273
1371
|
chatChildren.delete(sessionId)
|
|
1274
1372
|
if (imgDir) { try { rmSync(imgDir, { recursive: true, force: true }) } catch { /* best-effort */ } }
|
|
1275
1373
|
if (buf.trim()) parseStreamLine(buf.trim(), push, onInit, onDelta)
|
|
1276
|
-
liveText = '' // turn is over — drop any lingering live preview
|
|
1374
|
+
liveText = '' // turn is over — drop any lingering live preview / thought
|
|
1375
|
+
liveActivity = ''
|
|
1277
1376
|
await flush()
|
|
1278
1377
|
// User-initiated stop (gd-303): land on idle with a friendly note, not an
|
|
1279
1378
|
// "exited with code N" error — the kill's non-zero code is expected here.
|
|
@@ -1281,7 +1380,7 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
1281
1380
|
await postSessionEvents(
|
|
1282
1381
|
cfg, sessionId,
|
|
1283
1382
|
[{ role: 'SYSTEM', text: '⏹ Спряно. Напиши още нещо, за да продължим разговора.' }],
|
|
1284
|
-
'idle', capturedSession || undefined, '',
|
|
1383
|
+
'idle', capturedSession || undefined, '', '',
|
|
1285
1384
|
)
|
|
1286
1385
|
reportCommandResult(cfg, cmd.id, 'done', 'stopped by user')
|
|
1287
1386
|
log(`⏹ ai_chat ${sessionId} спряно от потребителя`)
|
|
@@ -1294,6 +1393,7 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
1294
1393
|
ok ? 'idle' : 'error',
|
|
1295
1394
|
capturedSession || undefined,
|
|
1296
1395
|
'', // clear the live preview on the terminal post
|
|
1396
|
+
'', // …and the thinking snippet (gd-419)
|
|
1297
1397
|
)
|
|
1298
1398
|
reportCommandResult(cfg, cmd.id, ok ? 'done' : 'error', `exit ${code}`)
|
|
1299
1399
|
log(`■ ai_chat ${sessionId} приключи (code ${code})`)
|
|
@@ -1308,7 +1408,7 @@ function stopAiChat(cfg, cmd) {
|
|
|
1308
1408
|
const sessionId = cmd.payload?.sessionId
|
|
1309
1409
|
const child = sessionId ? chatChildren.get(sessionId) : null
|
|
1310
1410
|
if (!child) {
|
|
1311
|
-
if (sessionId) postSessionEvents(cfg, sessionId, [], 'idle', undefined, '')
|
|
1411
|
+
if (sessionId) postSessionEvents(cfg, sessionId, [], 'idle', undefined, '', '')
|
|
1312
1412
|
reportCommandResult(cfg, cmd.id, 'done', 'no active turn')
|
|
1313
1413
|
return
|
|
1314
1414
|
}
|
|
@@ -1603,6 +1703,7 @@ async function streamCommands(cfg) {
|
|
|
1603
1703
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
1604
1704
|
|
|
1605
1705
|
async function runLoop(cfg) {
|
|
1706
|
+
ensureSingleInstance()
|
|
1606
1707
|
log(`gitdone-agent started — machine: ${cfg.hostname}, server: ${cfg.url}, interval: ${cfg.interval}s`)
|
|
1607
1708
|
log(` roots: ${cfg.roots.join(', ') || '(none — add one with --root)'}`)
|
|
1608
1709
|
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "gitdone-agent",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"gitdone-agent": "index.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"start": "node index.js"
|
|
11
|
-
},
|
|
12
|
-
"engines": {
|
|
13
|
-
"node": "\u003e=18"
|
|
14
|
-
}
|
|
15
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "gitdone-agent",
|
|
3
|
+
"version": "0.6.17",
|
|
4
|
+
"description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gitdone-agent": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node index.js"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": "\u003e=18"
|
|
14
|
+
}
|
|
15
|
+
}
|