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.
Files changed (2) hide show
  1. package/index.js +93 -32
  2. 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 "version" AND
31
- // src/lib/agentVersion.ts LATEST_AGENT_VERSION.
32
- const AGENT_VERSION = '0.8.5'
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
- // Download the images attached to a chat turn to a per-session temp dir and
1857
- // return their local paths. `claude -p` can't take image URLs on stdin, so we
1858
- // fetch them locally and point Claude at the files (its Read tool renders
1859
- // images). Best-effort: a failed download is skipped, not fatal.
1860
- async function downloadSessionImages(sessionId, urls) {
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 res = await fetch(urls[i])
1867
- if (!res.ok) { log(`✗ image download HTTP ${res.status}: ${urls[i]}`); continue }
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
- const m = /\.(png|jpe?g|gif|webp|bmp)(?:\?|$)/i.exec(urls[i])
1870
- const ext = m ? m[1].toLowerCase() : 'png'
1871
- const file = join(dir, `image-${i + 1}.${ext}`)
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(`✗ image download failed (${urls[i]}): ${e.message}`)
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 the images the user attached: neither CLI renders an image URL
1882
- // off stdin, so they're downloaded locally and referenced by path for its Read
1883
- // tool. Returns the prompt unchanged when nothing was attached (or every
1884
- // download failed), so an image-only turn still says something.
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} изображени${paths.length === 1 ? 'е' : 'я'}. Прегледай ги с Read tool от следните локални пътища:\n${list}`
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 images = Array.isArray(cmd.payload?.images) ? cmd.payload.images.filter((u) => typeof u === 'string' && u) : []
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 && images.length === 0)) {
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
- // images have to be on disk BEFORE we start it, unlike the Claude path where
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 = images.length > 0 ? await downloadSessionImages(sessionId, images) : null
2322
+ const dl = attachments.length > 0 ? await downloadSessionAttachments(sessionId, attachments) : null
2262
2323
  runCodexChatTurn(cfg, cmd, repoPath, {
2263
2324
  sessionId,
2264
- prompt: withImageNote(prompt, dl?.paths ?? []),
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 any attached images locally and fold their paths into the prompt so
2298
- // the AI reads them with its Read tool (URLs on stdin don't render).
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 (images.length > 0) {
2302
- const dl = await downloadSessionImages(sessionId, images)
2362
+ if (attachments.length > 0) {
2363
+ const dl = await downloadSessionAttachments(sessionId, attachments)
2303
2364
  imgDir = dl.dir
2304
- fullPrompt = withImageNote(prompt, dl.paths)
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} (images=${images.length})`)
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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
5
5
  "type": "module",
6
6
  "bin": {