gitdone-agent 0.8.5 → 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 +93 -32
- 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')
|
|
@@ -1853,39 +1853,99 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1853
1853
|
})
|
|
1854
1854
|
}
|
|
1855
1855
|
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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) {
|
|
1861
1919
|
const dir = join(tmpdir(), `gitdone-ai-${sessionId}`)
|
|
1862
1920
|
mkdirSync(dir, { recursive: true })
|
|
1863
1921
|
const paths = []
|
|
1864
1922
|
for (let i = 0; i < urls.length; i++) {
|
|
1865
1923
|
try {
|
|
1866
|
-
const
|
|
1867
|
-
|
|
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 }
|
|
1868
1927
|
const buf = Buffer.from(await res.arrayBuffer())
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
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))
|
|
1872
1933
|
writeFileSync(file, buf)
|
|
1873
1934
|
paths.push(file)
|
|
1874
1935
|
} catch (e) {
|
|
1875
|
-
log(`✗
|
|
1936
|
+
log(`✗ attachment download failed (${urls[i]}): ${e.message}`)
|
|
1876
1937
|
}
|
|
1877
1938
|
}
|
|
1878
1939
|
return { dir, paths }
|
|
1879
1940
|
}
|
|
1880
1941
|
|
|
1881
|
-
// Point the AI at
|
|
1882
|
-
//
|
|
1883
|
-
// tool
|
|
1884
|
-
|
|
1885
|
-
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) {
|
|
1886
1946
|
if (!paths || paths.length === 0) return prompt
|
|
1887
1947
|
const list = paths.map((p) => `- ${p}`).join('\n')
|
|
1888
|
-
const note = `Потребителят прикачи ${paths.length}
|
|
1948
|
+
const note = `Потребителят прикачи ${paths.length} файл${paths.length === 1 ? '' : 'а'}. Прегледай и анализирай прикачванията с наличните ти инструменти. Локални пътища:\n${list}`
|
|
1889
1949
|
return prompt ? `${prompt}\n\n[${note}]` : `[${note}]`
|
|
1890
1950
|
}
|
|
1891
1951
|
|
|
@@ -2226,14 +2286,15 @@ function runCodexChatTurn(cfg, cmd, repoPath, opts) {
|
|
|
2226
2286
|
async function runAiChat(cfg, cmd, repoPath) {
|
|
2227
2287
|
const sessionId = cmd.payload?.sessionId
|
|
2228
2288
|
const prompt = cmd.payload?.prompt ?? ''
|
|
2229
|
-
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) : []
|
|
2230
2291
|
const claudeSessionId = cmd.payload?.claudeSessionId || null
|
|
2231
2292
|
const allowCommit = cmd.payload?.allowCommit === true
|
|
2232
2293
|
const model = aiModelArg(cmd.payload?.model)
|
|
2233
2294
|
const effort = aiEffortArg(cmd.payload?.effort)
|
|
2234
2295
|
const provider = aiProviderArg(cmd.payload?.provider)
|
|
2235
2296
|
// A turn may be text-only, image-only, or both — but needs at least one.
|
|
2236
|
-
if (!sessionId || (!prompt &&
|
|
2297
|
+
if (!sessionId || (!prompt && attachments.length === 0)) {
|
|
2237
2298
|
reportCommandResult(cfg, cmd.id, 'error', 'invalid ai_chat payload')
|
|
2238
2299
|
return
|
|
2239
2300
|
}
|
|
@@ -2255,13 +2316,13 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
2255
2316
|
}
|
|
2256
2317
|
|
|
2257
2318
|
// Codex (gd-491): one process per turn, prompt on stdin at spawn — so the
|
|
2258
|
-
//
|
|
2319
|
+
// Attachments have to be on disk BEFORE we start it, unlike the Claude path where
|
|
2259
2320
|
// the message is written into an already-running process.
|
|
2260
2321
|
if (provider === 'codex') {
|
|
2261
|
-
const dl =
|
|
2322
|
+
const dl = attachments.length > 0 ? await downloadSessionAttachments(sessionId, attachments) : null
|
|
2262
2323
|
runCodexChatTurn(cfg, cmd, repoPath, {
|
|
2263
2324
|
sessionId,
|
|
2264
|
-
prompt:
|
|
2325
|
+
prompt: withAttachmentNote(prompt, dl?.paths ?? []),
|
|
2265
2326
|
imgDir: dl?.dir ?? null,
|
|
2266
2327
|
resumeId: claudeSessionId,
|
|
2267
2328
|
model,
|
|
@@ -2294,14 +2355,14 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
2294
2355
|
log(`▶ ai_chat proc за ${sessionId} @ ${repoPath} (resume=${claudeSessionId ? 'yes' : 'no'}) via ${claudePath}`)
|
|
2295
2356
|
}
|
|
2296
2357
|
|
|
2297
|
-
// Fetch
|
|
2298
|
-
//
|
|
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.
|
|
2299
2360
|
let imgDir = null
|
|
2300
2361
|
let fullPrompt = prompt
|
|
2301
|
-
if (
|
|
2302
|
-
const dl = await
|
|
2362
|
+
if (attachments.length > 0) {
|
|
2363
|
+
const dl = await downloadSessionAttachments(sessionId, attachments)
|
|
2303
2364
|
imgDir = dl.dir
|
|
2304
|
-
fullPrompt =
|
|
2365
|
+
fullPrompt = withAttachmentNote(prompt, dl.paths)
|
|
2305
2366
|
}
|
|
2306
2367
|
|
|
2307
2368
|
// Open the turn BEFORE writing the message, so even the earliest output
|
|
@@ -2328,7 +2389,7 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
2328
2389
|
await finishChatTurn(cfg, entry, { ok: false, code: `stdin: ${err.message}` })
|
|
2329
2390
|
return
|
|
2330
2391
|
}
|
|
2331
|
-
log(`▶ ai_chat ход ${sessionId} (
|
|
2392
|
+
log(`▶ ai_chat ход ${sessionId} (attachments=${attachments.length})`)
|
|
2332
2393
|
}
|
|
2333
2394
|
|
|
2334
2395
|
// ai_chat_stop — interrupt a running chat turn the user asked to stop (gd-303).
|