gitdone-agent 0.8.4 → 0.8.6
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 +117 -43
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
mkdirSync, copyFileSync, appendFileSync, readdirSync, rmSync, statSync,
|
|
17
17
|
openSync, writeSync, fsyncSync, closeSync, readSync,
|
|
18
18
|
} from 'node:fs'
|
|
19
|
-
import { resolve, join } from 'node:path'
|
|
19
|
+
import { resolve, join, extname } from 'node:path'
|
|
20
20
|
import { homedir, hostname, tmpdir } from 'node:os'
|
|
21
21
|
import { randomUUID, createHash } from 'node:crypto'
|
|
22
22
|
|
|
@@ -27,9 +27,9 @@ import { randomUUID, createHash } from 'node:crypto'
|
|
|
27
27
|
// when the agent runs in a hidden window).
|
|
28
28
|
|
|
29
29
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
30
|
-
// Keep in lockstep with packages/agent/package.json
|
|
31
|
-
//
|
|
32
|
-
const AGENT_VERSION = '0.8.
|
|
30
|
+
// Keep in lockstep with packages/agent/package.json. The server's offline
|
|
31
|
+
// fallback is bumped only after this release has actually reached npm.
|
|
32
|
+
const AGENT_VERSION = '0.8.6'
|
|
33
33
|
|
|
34
34
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
35
35
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -999,19 +999,32 @@ function scanRepos(roots, maxDepth = 5) {
|
|
|
999
999
|
|
|
1000
1000
|
// ─── Server sync + commands ─────────────────────────────────────────────────────
|
|
1001
1001
|
|
|
1002
|
+
// Keep the deadline below the server-side session stall window. Without a
|
|
1003
|
+
// deadline, a proxy/network connection that never completes can block the
|
|
1004
|
+
// event-posting loop forever, so no heartbeat reaches the server and the
|
|
1005
|
+
// watchdog incorrectly declares a live AI turn stalled.
|
|
1006
|
+
const API_REQUEST_TIMEOUT_MS = 10_000
|
|
1007
|
+
|
|
1002
1008
|
async function apiOnce(cfg, path, body) {
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1009
|
+
const controller = new AbortController()
|
|
1010
|
+
const timer = setTimeout(() => controller.abort(), API_REQUEST_TIMEOUT_MS)
|
|
1011
|
+
try {
|
|
1012
|
+
const res = await fetch(`${cfg.url}${path}`, {
|
|
1013
|
+
method: 'POST',
|
|
1014
|
+
headers: { Authorization: `Bearer ${cfg.key}`, 'Content-Type': 'application/json' },
|
|
1015
|
+
body: JSON.stringify(body),
|
|
1016
|
+
signal: controller.signal,
|
|
1017
|
+
})
|
|
1018
|
+
if (!res.ok) {
|
|
1019
|
+
const text = await res.text().catch(() => '')
|
|
1020
|
+
const err = new Error(`HTTP ${res.status} ${path}: ${text}`)
|
|
1021
|
+
err.status = res.status
|
|
1022
|
+
throw err
|
|
1023
|
+
}
|
|
1024
|
+
return res.json().catch(() => ({}))
|
|
1025
|
+
} finally {
|
|
1026
|
+
clearTimeout(timer)
|
|
1013
1027
|
}
|
|
1014
|
-
return res.json().catch(() => ({}))
|
|
1015
1028
|
}
|
|
1016
1029
|
|
|
1017
1030
|
// gd-514: statuses where the app server demonstrably never processed the body,
|
|
@@ -1840,39 +1853,99 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1840
1853
|
})
|
|
1841
1854
|
}
|
|
1842
1855
|
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1856
|
+
const ATTACHMENT_EXTENSIONS = new Map([
|
|
1857
|
+
['application/pdf', '.pdf'],
|
|
1858
|
+
['application/json', '.json'],
|
|
1859
|
+
['application/ld+json', '.json'],
|
|
1860
|
+
['application/rtf', '.rtf'],
|
|
1861
|
+
['application/msword', '.doc'],
|
|
1862
|
+
['application/vnd.ms-excel', '.xls'],
|
|
1863
|
+
['application/vnd.ms-powerpoint', '.ppt'],
|
|
1864
|
+
['application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.docx'],
|
|
1865
|
+
['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.xlsx'],
|
|
1866
|
+
['application/vnd.openxmlformats-officedocument.presentationml.presentation', '.pptx'],
|
|
1867
|
+
['application/vnd.oasis.opendocument.text', '.odt'],
|
|
1868
|
+
['application/vnd.oasis.opendocument.spreadsheet', '.ods'],
|
|
1869
|
+
['application/epub+zip', '.epub'],
|
|
1870
|
+
['application/zip', '.zip'],
|
|
1871
|
+
['application/x-7z-compressed', '.7z'],
|
|
1872
|
+
['application/gzip', '.gz'],
|
|
1873
|
+
['text/plain', '.txt'],
|
|
1874
|
+
['text/csv', '.csv'],
|
|
1875
|
+
['text/markdown', '.md'],
|
|
1876
|
+
['text/html', '.html'],
|
|
1877
|
+
['text/xml', '.xml'],
|
|
1878
|
+
['application/xml', '.xml'],
|
|
1879
|
+
['application/yaml', '.yaml'],
|
|
1880
|
+
['text/yaml', '.yaml'],
|
|
1881
|
+
['image/png', '.png'],
|
|
1882
|
+
['image/jpeg', '.jpg'],
|
|
1883
|
+
['image/gif', '.gif'],
|
|
1884
|
+
['image/webp', '.webp'],
|
|
1885
|
+
['image/bmp', '.bmp'],
|
|
1886
|
+
['image/svg+xml', '.svg'],
|
|
1887
|
+
])
|
|
1888
|
+
|
|
1889
|
+
function safeAttachmentName(value, contentType, index) {
|
|
1890
|
+
let name = typeof value === 'string' ? value.trim() : ''
|
|
1891
|
+
try { name = decodeURIComponent(name) } catch { /* keep the original */ }
|
|
1892
|
+
name = name.replace(/\\/g, '/').split('/').pop()
|
|
1893
|
+
name = name
|
|
1894
|
+
.replace(/[<>:"/\\|?*\x00-\x1f]/g, '-')
|
|
1895
|
+
.replace(/^\.+/, '')
|
|
1896
|
+
.trim()
|
|
1897
|
+
.slice(0, 140)
|
|
1898
|
+
if (!name) name = `file-${index + 1}`
|
|
1899
|
+
if (!extname(name)) name += ATTACHMENT_EXTENSIONS.get(contentType?.split(';', 1)[0].toLowerCase()) ?? '.bin'
|
|
1900
|
+
return `${index + 1}-${name}`
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function attachmentUrlInfo(rawUrl) {
|
|
1904
|
+
try {
|
|
1905
|
+
const parsed = new URL(rawUrl)
|
|
1906
|
+
const name = new URLSearchParams(parsed.hash.slice(1)).get('gd-name') || ''
|
|
1907
|
+
parsed.hash = ''
|
|
1908
|
+
return { downloadUrl: parsed.toString(), name }
|
|
1909
|
+
} catch {
|
|
1910
|
+
return { downloadUrl: rawUrl.split('#', 1)[0], name: '' }
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// Download files attached to a chat turn to a per-session temp dir and return
|
|
1915
|
+
// their local paths. Neither AI CLI consumes remote attachment URLs directly,
|
|
1916
|
+
// so the prompt points it at these files. Best-effort: one failed download does
|
|
1917
|
+
// not prevent the remaining attachments or message from reaching the AI.
|
|
1918
|
+
async function downloadSessionAttachments(sessionId, urls) {
|
|
1848
1919
|
const dir = join(tmpdir(), `gitdone-ai-${sessionId}`)
|
|
1849
1920
|
mkdirSync(dir, { recursive: true })
|
|
1850
1921
|
const paths = []
|
|
1851
1922
|
for (let i = 0; i < urls.length; i++) {
|
|
1852
1923
|
try {
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1924
|
+
const info = attachmentUrlInfo(urls[i])
|
|
1925
|
+
const res = await fetch(info.downloadUrl)
|
|
1926
|
+
if (!res.ok) { log(`✗ attachment download HTTP ${res.status}: ${info.downloadUrl}`); continue }
|
|
1855
1927
|
const buf = Buffer.from(await res.arrayBuffer())
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1928
|
+
let sourceName = info.name
|
|
1929
|
+
if (!sourceName) {
|
|
1930
|
+
try { sourceName = new URL(info.downloadUrl).pathname.split('/').pop() || '' } catch { /* use fallback */ }
|
|
1931
|
+
}
|
|
1932
|
+
const file = join(dir, safeAttachmentName(sourceName, res.headers.get('content-type'), i))
|
|
1859
1933
|
writeFileSync(file, buf)
|
|
1860
1934
|
paths.push(file)
|
|
1861
1935
|
} catch (e) {
|
|
1862
|
-
log(`✗
|
|
1936
|
+
log(`✗ attachment download failed (${urls[i]}): ${e.message}`)
|
|
1863
1937
|
}
|
|
1864
1938
|
}
|
|
1865
1939
|
return { dir, paths }
|
|
1866
1940
|
}
|
|
1867
1941
|
|
|
1868
|
-
// Point the AI at
|
|
1869
|
-
//
|
|
1870
|
-
// tool
|
|
1871
|
-
|
|
1872
|
-
function withImageNote(prompt, paths) {
|
|
1942
|
+
// Point the AI at every locally downloaded attachment. The exact analysis tool
|
|
1943
|
+
// depends on the file type (Read for text/images/PDFs, or another available
|
|
1944
|
+
// local tool for structured/binary documents).
|
|
1945
|
+
function withAttachmentNote(prompt, paths) {
|
|
1873
1946
|
if (!paths || paths.length === 0) return prompt
|
|
1874
1947
|
const list = paths.map((p) => `- ${p}`).join('\n')
|
|
1875
|
-
const note = `Потребителят прикачи ${paths.length}
|
|
1948
|
+
const note = `Потребителят прикачи ${paths.length} файл${paths.length === 1 ? '' : 'а'}. Прегледай и анализирай прикачванията с наличните ти инструменти. Локални пътища:\n${list}`
|
|
1876
1949
|
return prompt ? `${prompt}\n\n[${note}]` : `[${note}]`
|
|
1877
1950
|
}
|
|
1878
1951
|
|
|
@@ -2213,14 +2286,15 @@ function runCodexChatTurn(cfg, cmd, repoPath, opts) {
|
|
|
2213
2286
|
async function runAiChat(cfg, cmd, repoPath) {
|
|
2214
2287
|
const sessionId = cmd.payload?.sessionId
|
|
2215
2288
|
const prompt = cmd.payload?.prompt ?? ''
|
|
2216
|
-
const
|
|
2289
|
+
const attachmentPayload = Array.isArray(cmd.payload?.attachments) ? cmd.payload.attachments : cmd.payload?.images
|
|
2290
|
+
const attachments = Array.isArray(attachmentPayload) ? attachmentPayload.filter((u) => typeof u === 'string' && u) : []
|
|
2217
2291
|
const claudeSessionId = cmd.payload?.claudeSessionId || null
|
|
2218
2292
|
const allowCommit = cmd.payload?.allowCommit === true
|
|
2219
2293
|
const model = aiModelArg(cmd.payload?.model)
|
|
2220
2294
|
const effort = aiEffortArg(cmd.payload?.effort)
|
|
2221
2295
|
const provider = aiProviderArg(cmd.payload?.provider)
|
|
2222
2296
|
// A turn may be text-only, image-only, or both — but needs at least one.
|
|
2223
|
-
if (!sessionId || (!prompt &&
|
|
2297
|
+
if (!sessionId || (!prompt && attachments.length === 0)) {
|
|
2224
2298
|
reportCommandResult(cfg, cmd.id, 'error', 'invalid ai_chat payload')
|
|
2225
2299
|
return
|
|
2226
2300
|
}
|
|
@@ -2242,13 +2316,13 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
2242
2316
|
}
|
|
2243
2317
|
|
|
2244
2318
|
// Codex (gd-491): one process per turn, prompt on stdin at spawn — so the
|
|
2245
|
-
//
|
|
2319
|
+
// Attachments have to be on disk BEFORE we start it, unlike the Claude path where
|
|
2246
2320
|
// the message is written into an already-running process.
|
|
2247
2321
|
if (provider === 'codex') {
|
|
2248
|
-
const dl =
|
|
2322
|
+
const dl = attachments.length > 0 ? await downloadSessionAttachments(sessionId, attachments) : null
|
|
2249
2323
|
runCodexChatTurn(cfg, cmd, repoPath, {
|
|
2250
2324
|
sessionId,
|
|
2251
|
-
prompt:
|
|
2325
|
+
prompt: withAttachmentNote(prompt, dl?.paths ?? []),
|
|
2252
2326
|
imgDir: dl?.dir ?? null,
|
|
2253
2327
|
resumeId: claudeSessionId,
|
|
2254
2328
|
model,
|
|
@@ -2281,14 +2355,14 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
2281
2355
|
log(`▶ ai_chat proc за ${sessionId} @ ${repoPath} (resume=${claudeSessionId ? 'yes' : 'no'}) via ${claudePath}`)
|
|
2282
2356
|
}
|
|
2283
2357
|
|
|
2284
|
-
// Fetch
|
|
2285
|
-
//
|
|
2358
|
+
// Fetch attachments locally and fold their paths into the prompt; remote URLs
|
|
2359
|
+
// on stdin are not enough for either CLI to inspect the file contents.
|
|
2286
2360
|
let imgDir = null
|
|
2287
2361
|
let fullPrompt = prompt
|
|
2288
|
-
if (
|
|
2289
|
-
const dl = await
|
|
2362
|
+
if (attachments.length > 0) {
|
|
2363
|
+
const dl = await downloadSessionAttachments(sessionId, attachments)
|
|
2290
2364
|
imgDir = dl.dir
|
|
2291
|
-
fullPrompt =
|
|
2365
|
+
fullPrompt = withAttachmentNote(prompt, dl.paths)
|
|
2292
2366
|
}
|
|
2293
2367
|
|
|
2294
2368
|
// Open the turn BEFORE writing the message, so even the earliest output
|
|
@@ -2315,7 +2389,7 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
2315
2389
|
await finishChatTurn(cfg, entry, { ok: false, code: `stdin: ${err.message}` })
|
|
2316
2390
|
return
|
|
2317
2391
|
}
|
|
2318
|
-
log(`▶ ai_chat ход ${sessionId} (
|
|
2392
|
+
log(`▶ ai_chat ход ${sessionId} (attachments=${attachments.length})`)
|
|
2319
2393
|
}
|
|
2320
2394
|
|
|
2321
2395
|
// ai_chat_stop — interrupt a running chat turn the user asked to stop (gd-303).
|