gipity 1.0.428 → 1.0.429

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.
@@ -8,12 +8,63 @@
8
8
  // Uses the same presigned init -> PUT -> complete flow the app file-upload
9
9
  // service exposes, with `public: true`. Cleanup goes through the matching
10
10
  // DELETE /api/:appGuid/uploads/:guid, which removes the public object too.
11
- import { readFileSync, statSync } from 'node:fs';
12
- import { basename } from 'node:path';
11
+ import { readFileSync, statSync, existsSync, readdirSync } from 'node:fs';
12
+ import { basename, resolve, relative, join } from 'node:path';
13
13
  import { post, del } from './api.js';
14
+ import { getProjectRoot } from './config.js';
14
15
  import { guessMime } from './upload.js';
16
+ /** Dirs a hand-made asset is never in, and that are expensive to walk. */
17
+ const FIND_SKIP = new Set(['node_modules', '.git', '.gipity', 'dist', 'build', '.next', 'coverage']);
18
+ /** Find files named `name` anywhere under `root` (breadth-first, bounded). */
19
+ function findByBasename(root, name, limit = 3) {
20
+ const hits = [];
21
+ const queue = [root];
22
+ let visited = 0;
23
+ while (queue.length && hits.length < limit && visited < 500) {
24
+ const dir = queue.shift();
25
+ visited++;
26
+ let entries;
27
+ try {
28
+ entries = readdirSync(dir, { withFileTypes: true });
29
+ }
30
+ catch {
31
+ continue;
32
+ }
33
+ for (const e of entries) {
34
+ if (e.isDirectory()) {
35
+ if (!FIND_SKIP.has(e.name))
36
+ queue.push(join(dir, e.name));
37
+ }
38
+ else if (e.name === name && hits.length < limit) {
39
+ hits.push(join(dir, e.name));
40
+ }
41
+ }
42
+ }
43
+ return hits;
44
+ }
45
+ /** Every `page` flag that takes a local file resolves it against the CURRENT
46
+ * DIRECTORY, and a bare ENOENT from deep inside the upload path ("stat
47
+ * './tmp/fist.jpg'") tells the caller nothing about where we looked or where
48
+ * the file actually is. That costs an ls/find round-trip every time a file was
49
+ * written somewhere other than where the caller assumed - which is exactly what
50
+ * happens after a `cd` that failed, or a `gipity generate` run from a different
51
+ * directory. So: name the absolute path we tried, the directory it was resolved
52
+ * against, and - when a file with that basename exists elsewhere in the project
53
+ * - the path that would have worked. Recovery becomes copy-paste, not a search. */
54
+ export function assertLocalAsset(flag, localPath) {
55
+ const abs = resolve(localPath);
56
+ if (existsSync(abs))
57
+ return;
58
+ const root = getProjectRoot() ?? process.cwd();
59
+ const elsewhere = findByBasename(root, basename(localPath)).filter(p => p !== abs);
60
+ const found = elsewhere.length
61
+ ? `\nThat file DOES exist here — pass this path instead:\n${elsewhere.map(p => ` ${flag} ${relative(process.cwd(), p) || p}`).join('\n')}`
62
+ : `\nNothing named "${basename(localPath)}" under ${root} either. Generate a frame with \`gipity generate image "<description>" -o ${localPath}\`, or check the path.`;
63
+ throw new Error(`${flag} ${localPath}: no such file — looked for ${abs} (relative paths resolve against the current directory, ${process.cwd()}).${found}`);
64
+ }
15
65
  /** Upload a local file to the app's public file store and return its URL. */
16
- export async function uploadPublicFixture(projectGuid, localPath) {
66
+ export async function uploadPublicFixture(projectGuid, localPath, flag = '--fixture') {
67
+ assertLocalAsset(flag, localPath);
17
68
  const name = basename(localPath);
18
69
  const size = statSync(localPath).size;
19
70
  const contentType = guessMime(localPath);
@@ -38,4 +89,42 @@ export async function uploadPublicFixture(projectGuid, localPath) {
38
89
  export async function deleteFixture(projectGuid, guid) {
39
90
  await del(`/api/${projectGuid}/uploads/${guid}`);
40
91
  }
92
+ // ── camera feed ─────────────────────────────────────────────────────────────
93
+ // A headless browser has no webcam, so a vision app (MediaPipe, YOLOX, any
94
+ // getUserMedia consumer) can't be exercised at all: getUserMedia rejects, the
95
+ // app shows its no-camera state, and the code path the user actually cares
96
+ // about never runs. `--fake-media` alone only fixes the rejection — Chrome's
97
+ // built-in synthetic device is a rolling test pattern, so a gesture/pose/object
98
+ // model still sees nothing recognizable and every agent ends up stubbing the
99
+ // model's own output to test around it.
100
+ //
101
+ // --camera closes that: host a real image/video and let the browser play it as
102
+ // the webcam's frames, so the app's REAL pipeline (frames → model → app logic)
103
+ // runs headlessly on input you control. Hosting reuses the fixture path above;
104
+ // the browser side (fetch the file, feed it to Chrome's fake capture device)
105
+ // is the server's job — the CLI just names the file and hands over its URL.
106
+ /** Container formats Chrome's fake video-capture device can be fed from, plus
107
+ * the still-image types the server transcodes into a looping single-frame
108
+ * feed. Anything else is rejected here (fast, local) rather than after an
109
+ * upload + a browser launch. */
110
+ const CAMERA_EXTS = ['.png', '.jpg', '.jpeg', '.webp', '.mp4', '.webm', '.y4m', '.mjpeg'];
111
+ /** Validate a --camera path before uploading it. Throws with the accepted list
112
+ * and the in-platform way to produce a frame, so a wrong file type costs one
113
+ * local error instead of an upload plus an opaque browser failure. */
114
+ export function assertCameraFile(localPath) {
115
+ assertLocalAsset('--camera', localPath);
116
+ const ext = localPath.slice(localPath.lastIndexOf('.')).toLowerCase();
117
+ if (CAMERA_EXTS.includes(ext))
118
+ return;
119
+ throw new Error(`--camera ${basename(localPath)}: unsupported file type "${ext || '(none)'}" — the camera feed must be an image or video (${CAMERA_EXTS.join(', ')}).\n` +
120
+ `A still image is played as a looping single-frame feed, which is what a gesture/pose/object model needs.\n` +
121
+ `No frame to hand? Generate one: gipity generate image "a hand making a closed fist, palm to camera, plain background"`);
122
+ }
123
+ /** Host a local image/video as the browser's webcam feed. Same public-upload
124
+ * path as a fixture (so it is fetchable from the browser container and is
125
+ * deleted the same way), validated first. */
126
+ export async function uploadCameraFeed(projectGuid, localPath) {
127
+ assertCameraFile(localPath);
128
+ return uploadPublicFixture(projectGuid, localPath, '--camera');
129
+ }
41
130
  //# sourceMappingURL=page-fixtures.js.map
@@ -42,17 +42,17 @@ export const GEMINI_TTS_VOICES_DOC = `Zephyr, Puck, Charon, Kore, Fenrir, Leda,
42
42
  export const GEMINI_TTS_VOICES_SHORT = `Kore, Puck, Zephyr, Charon, Fenrir, Leda, Orus, Aoede, and 22 more`;
43
43
  export const IMAGE_GEMINI_ASPECT_RATIOS = `1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 4:5, 5:4, 21:9`;
44
44
  export const IMAGE_GEMINI_SIZES = `512, 1K, 2K, 4K`;
45
- export const IMAGE_MODELS_DOC = `openai: gpt-image-2, gpt-image-1.5. bfl: flux-2-pro, flux-2-flex, flux-2-max, flux-dev. gemini: gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image`;
46
- export const IMAGE_PROVIDERS_BULLET = `- **OpenAI**: \`gpt-image-2, gpt-image-1.5\`
47
- - **BFL/Flux**: \`flux-2-pro, flux-2-flex, flux-2-max, flux-dev\`
48
- - **Gemini/Nano Banana**: \`gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image\``;
45
+ export const IMAGE_MODELS_DOC = `openai: gpt-image-2. bfl: flux-2-pro, flux-2-flex, flux-2-max, flux-2-klein-9b, flux-2-klein-4b. gemini: gemini-3.1-flash-lite-image, gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image`;
46
+ export const IMAGE_PROVIDERS_BULLET = `- **OpenAI**: \`gpt-image-2\`
47
+ - **BFL/Flux**: \`flux-2-pro, flux-2-flex, flux-2-max, flux-2-klein-9b, flux-2-klein-4b\`
48
+ - **Gemini/Nano Banana**: \`gemini-3.1-flash-lite-image, gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image\``;
49
49
  export const IMAGE_PROVIDERS_LIST = `openai, bfl, gemini`;
50
50
  export const IMAGE_PROVIDER_DESCRIPTIONS = {
51
- 'openai': `OpenAI (gpt-image-2, gpt-image-1.5)`,
52
- 'bfl': `BFL/Flux (flux-2-pro, flux-2-flex, flux-2-max, flux-dev)`,
53
- 'gemini': `Gemini/Nano Banana (gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image)`,
51
+ 'openai': `OpenAI (gpt-image-2)`,
52
+ 'bfl': `BFL/Flux (flux-2-pro, flux-2-flex, flux-2-max, flux-2-klein-9b, flux-2-klein-4b)`,
53
+ 'gemini': `Gemini/Nano Banana (gemini-3.1-flash-lite-image, gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image)`,
54
54
  };
55
- export const LLM_DEFAULT_MODELS_DOC = `OpenAI: gpt-4.1-nano (cheapest), gpt-5.4-mini (cheap reasoning). Anthropic: claude-haiku-4-5 (cheapest). Gemini: gemini-2.5-flash-lite (cheapest, 1M context)`;
55
+ export const LLM_DEFAULT_MODELS_DOC = `OpenAI: gpt-5.4-nano (cheapest), gpt-5.4-mini (cheap reasoning). Anthropic: claude-haiku-4-5 (cheapest). Gemini: gemini-2.5-flash-lite (cheapest, 1M context)`;
56
56
  export const LLM_PROVIDERS_LIST = `anthropic, openai, gemini`;
57
57
  export const OPENAI_TTS_VOICES_DOC = `alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer, verse`;
58
58
  export const TRANSCRIBE_PROVIDERS_DOC = `elevenlabs (default, Scribe v2), openai (GPT-4o Transcribe), gemini (Gemini 2.5 Flash - cheapest, multilingual)`;
@@ -63,7 +63,7 @@ export const TTS_PROVIDER_DESCRIPTIONS = {
63
63
  'gemini': `Gemini (30 voices: Kore, Puck, Zephyr, Charon, Fenrir, Leda, Orus, Aoede, and 22 more). Multi-speaker (up to 2) and 60+ languages`,
64
64
  };
65
65
  export const VIDEO_ASPECT_RATIOS = `16:9 (landscape), 9:16 (portrait), 1:1 (square)`;
66
- export const VIDEO_MODELS_DOC = `veo-3.1-generate-preview (best quality, ~$0.40/sec), veo-3.1-fast-generate-preview (faster, ~$0.15/sec), veo-3.1-lite-generate-preview (budget, ~$0.07/sec)`;
67
- export const VIDEO_MODELS_LIST = `veo-3.1-generate-preview, veo-3.1-fast-generate-preview, veo-3.1-lite-generate-preview`;
66
+ export const VIDEO_MODELS_DOC = `veo-3.1-generate-preview (best quality, up to 4K, ~$0.40/sec), veo-3.1-fast-generate-preview (faster, ~$0.10/sec), veo-3.1-lite-generate-preview (budget, ~$0.05/sec), gemini-omni-flash-preview (3-10s @ 720p, conversational editing, ~$0.10/sec)`;
67
+ export const VIDEO_MODELS_LIST = `veo-3.1-generate-preview, veo-3.1-fast-generate-preview, veo-3.1-lite-generate-preview, gemini-omni-flash-preview`;
68
68
  export const VIDEO_RESOLUTIONS = `720p, 1080p, 4k`;
69
69
  //# sourceMappingURL=provider-docs.js.map
@@ -12,6 +12,7 @@ import { post } from '../api.js';
12
12
  import * as state from './state.js';
13
13
  import { createLineSplitter, parseEvent, mapEventToEntries, } from './stream-json.js';
14
14
  import { IngestQueue } from './ingest-queue.js';
15
+ import { ImageBlockRewriter } from './media-upload.js';
15
16
  import { DeltaAccumulator, DeltaBatcher } from './stream-delta.js';
16
17
  import { randomUUID } from 'crypto';
17
18
  import { deviceFetch, bridgeAbort as bridgeAbortImpl } from './device-http.js';
@@ -875,7 +876,13 @@ async function handleDispatch(claimed) {
875
876
  // retry instead of the old fire-and-forget loss on network errors.
876
877
  // Daemon-authored entries get a random source_uuid so retried batches
877
878
  // dedup server-side.
878
- const queue = new IngestQueue((entries) => postIngest(d.conversation_guid, entries), { onWarn: (msg, meta) => log('warn', msg, { id: d.short_guid, ...meta }) });
879
+ // The rewriter uploads any base64 image blocks (Read-of-screenshot
880
+ // results) to VFS and swaps in image_ref blocks before the batch posts —
881
+ // transcripts carry URLs, never image bytes. Retries re-enter it, which
882
+ // is safe: rewritten entries have nothing left to upload, and the server
883
+ // stores by content hash so a replayed upload dedups.
884
+ const rewriter = new ImageBlockRewriter(d.conversation_guid, (msg, meta) => log('warn', msg, { id: d.short_guid, ...meta }));
885
+ const queue = new IngestQueue(async (entries) => postIngest(d.conversation_guid, await rewriter.rewrite(entries)), { onWarn: (msg, meta) => log('warn', msg, { id: d.short_guid, ...meta }) });
879
886
  const pushSystem = (content) => {
880
887
  queue.push({ kind: 'system', content, ts: new Date().toISOString(), source_uuid: randomUUID() });
881
888
  };
@@ -886,6 +893,9 @@ async function handleDispatch(claimed) {
886
893
  let bootstrapped;
887
894
  try {
888
895
  ({ cwd, bootstrapped } = await resolveCwdForProject(d));
896
+ // Lets the rewriter map an absolute Read path (…/screenshots/x.png)
897
+ // to its project-relative VFS path for content-hash dedup.
898
+ rewriter.setCwd(cwd);
889
899
  log('debug', 'resolved project cwd', { id: d.short_guid, project: d.project_slug, cwd, bootstrapped });
890
900
  }
891
901
  catch (err) {
@@ -1583,7 +1593,11 @@ export async function spawnGipityClaude(args, cwd, d, queue, meta) {
1583
1593
  // no longer drops stream content permanently (source_uuid dedup makes
1584
1594
  // the retries safe). Falls back to a local queue when the caller
1585
1595
  // didn't pass one (tests, direct invocation).
1586
- const q = queue ?? new IngestQueue((entries) => postIngest(d.conversation_guid, entries), { onWarn: (msg, meta) => log('warn', msg, { id: d.short_guid, ...meta }) });
1596
+ const q = queue ?? (() => {
1597
+ const rw = new ImageBlockRewriter(d.conversation_guid, (msg, meta) => log('warn', msg, { id: d.short_guid, ...meta }));
1598
+ rw.setCwd(cwd);
1599
+ return new IngestQueue(async (entries) => postIngest(d.conversation_guid, await rw.rewrite(entries)), { onWarn: (msg, meta) => log('warn', msg, { id: d.short_guid, ...meta }) });
1600
+ })();
1587
1601
  const ownQueue = !queue;
1588
1602
  // Progress heartbeat state. Measured at the daemon boundary - this is
1589
1603
  // what we actually observe, not anything the child self-reports. These
@@ -31,6 +31,28 @@ export function bridgeAbort(outer, inner) {
31
31
  outer.addEventListener('abort', onAbort, { once: true });
32
32
  return () => outer.removeEventListener('abort', onAbort);
33
33
  }
34
+ /** Device-auth binary POST — raw bytes body (no JSON/base64 wrapping).
35
+ * Used by the transcript-media upload, where the whole point is that the
36
+ * image travels as bytes instead of base64. */
37
+ export async function deviceFetchBinary(method, path, body, contentType, timeoutMs) {
38
+ const controller = timeoutMs ? new AbortController() : null;
39
+ const timer = controller ? setTimeout(() => controller.abort('timeout'), timeoutMs) : null;
40
+ try {
41
+ return await fetch(`${apiBase()}${path}`, {
42
+ method,
43
+ headers: {
44
+ 'Authorization': `Bearer ${deviceToken()}`,
45
+ 'Content-Type': contentType,
46
+ },
47
+ body: new Uint8Array(body),
48
+ signal: controller?.signal,
49
+ });
50
+ }
51
+ finally {
52
+ if (timer)
53
+ clearTimeout(timer);
54
+ }
55
+ }
34
56
  /** Device-auth HTTP helper. Sends `Authorization: Bearer <device-token>`
35
57
  * + `Content-Type: application/json` to the configured API base. */
36
58
  export async function deviceFetch(method, path, body, timeoutMs, signal) {
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Transcript image rewrite — the relay's "no inline base64" chokepoint.
3
+ *
4
+ * Claude Code's `Read` of an image file (screenshots, generated art, …)
5
+ * returns the bytes as a `{type:'image', source:{type:'base64', data}}`
6
+ * block inside the tool_result. Shipping that through ingest bloats the
7
+ * transcript DB and, worse, blows the server's 200 KB per-entry cap so
8
+ * real screenshots silently never reach the web client at all.
9
+ *
10
+ * `ImageBlockRewriter` sits in front of the ingest POST: it uploads each
11
+ * base64 image block's bytes to `POST /remote-sessions/:convGuid/media`
12
+ * (raw binary body) and swaps the block for a small `image_ref` pointing
13
+ * at the stored VFS file. The server dedups by content hash — a `Read` of
14
+ * an already-synced screenshot references the existing node.
15
+ *
16
+ * Failure handling: an upload that fails leaves small images inline
17
+ * (status-quo rendering still works) but replaces oversize ones with a
18
+ * text stub so one bad image can't poison the whole ingest batch. The
19
+ * IngestQueue retries a failed batch through this rewriter again, and the
20
+ * server's content-addressed storage makes replayed uploads idempotent.
21
+ */
22
+ import path from 'path';
23
+ import { deviceFetchBinary } from './device-http.js';
24
+ /** Below this the upload round-trip outweighs the base64: tiny images
25
+ * (icons, favicons) stay inline. */
26
+ const MIN_UPLOAD_BYTES = 4 * 1024;
27
+ /** Server-side cap (TRANSCRIPT_MEDIA_MAX_BYTES). Larger images can't be
28
+ * stored; they get stubbed rather than shipped. */
29
+ const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
30
+ /** Base64 payloads above this would break the server's 200 KB tool_result
31
+ * cap if left inline — when the upload fails, stub instead of poisoning
32
+ * the batch. Leaves headroom for the rest of the tool_result JSON. */
33
+ const INLINE_SAFE_B64_CHARS = 120_000;
34
+ /** How many tool_use → file_path pairs to remember for captioning/dedup. */
35
+ const PATH_MAP_CAP = 500;
36
+ /** Default uploader: raw-binary POST to the transcript-media endpoint. */
37
+ export const uploadViaDevice = async (convGuid, buf, opts) => {
38
+ const qs = new URLSearchParams({ filename: opts.filename, media_type: opts.mediaType });
39
+ if (opts.suggestedPath)
40
+ qs.set('suggested_path', opts.suggestedPath);
41
+ const res = await deviceFetchBinary('POST', `/remote-sessions/${encodeURIComponent(convGuid)}/media?${qs.toString()}`, buf, opts.mediaType, 30_000);
42
+ if (!res.ok) {
43
+ const body = await res.text().catch(() => '');
44
+ throw new Error(`HTTP ${res.status} ${body.slice(0, 120)}`);
45
+ }
46
+ const json = await res.json();
47
+ return json.data;
48
+ };
49
+ function extForMime(mime) {
50
+ switch (mime) {
51
+ case 'image/jpeg': return '.jpg';
52
+ case 'image/gif': return '.gif';
53
+ case 'image/webp': return '.webp';
54
+ case 'image/svg+xml': return '.svg';
55
+ default: return '.png';
56
+ }
57
+ }
58
+ function isBase64ImageBlock(b) {
59
+ return b?.type === 'image'
60
+ && b.source?.type === 'base64'
61
+ && typeof b.source.data === 'string'
62
+ && b.source.data.length > 0;
63
+ }
64
+ export class ImageBlockRewriter {
65
+ convGuid;
66
+ onWarn;
67
+ upload;
68
+ /** tool_use_id → the file_path the tool was asked to read (for naming +
69
+ * server-side dedup against the synced node at that path). */
70
+ pathsByToolUseId = new Map();
71
+ /** Project root on this machine, once the dispatch resolves it — lets an
72
+ * absolute Read path map to its project-relative VFS path. */
73
+ cwd = null;
74
+ constructor(convGuid, onWarn, upload = uploadViaDevice) {
75
+ this.convGuid = convGuid;
76
+ this.onWarn = onWarn;
77
+ this.upload = upload;
78
+ }
79
+ setCwd(cwd) {
80
+ this.cwd = cwd;
81
+ }
82
+ /** Rewrite every base64 image block in the batch to an image_ref. Never
83
+ * throws — per-image failures degrade per the policy above. */
84
+ async rewrite(entries) {
85
+ const out = [];
86
+ for (const entry of entries) {
87
+ if (entry.kind === 'tool_use') {
88
+ this.recordToolPath(entry);
89
+ out.push(entry);
90
+ continue;
91
+ }
92
+ if (entry.kind !== 'tool_result' || !Array.isArray(entry.content) || typeof entry.tool_use_id !== 'string') {
93
+ out.push(entry);
94
+ continue;
95
+ }
96
+ const blocks = [];
97
+ for (const block of entry.content) {
98
+ blocks.push(isBase64ImageBlock(block)
99
+ ? await this.rewriteBlock(block, entry.tool_use_id)
100
+ : block);
101
+ }
102
+ out.push({ ...entry, content: blocks });
103
+ }
104
+ return out;
105
+ }
106
+ recordToolPath(entry) {
107
+ if (typeof entry.tool_use_id !== 'string')
108
+ return;
109
+ const input = entry.tool_input;
110
+ const p = input && typeof input === 'object'
111
+ ? (input.file_path ?? input.path ?? input.notebook_path)
112
+ : undefined;
113
+ if (typeof p !== 'string' || !p)
114
+ return;
115
+ if (this.pathsByToolUseId.size >= PATH_MAP_CAP) {
116
+ const oldest = this.pathsByToolUseId.keys().next().value;
117
+ if (oldest !== undefined)
118
+ this.pathsByToolUseId.delete(oldest);
119
+ }
120
+ this.pathsByToolUseId.set(entry.tool_use_id, p);
121
+ }
122
+ async rewriteBlock(block, toolUseId) {
123
+ const b64 = block.source.data;
124
+ let buf;
125
+ try {
126
+ buf = Buffer.from(b64, 'base64');
127
+ }
128
+ catch {
129
+ return block; // not decodable — leave as-is
130
+ }
131
+ if (buf.length < MIN_UPLOAD_BYTES)
132
+ return block;
133
+ const mediaType = block.source.media_type || 'image/png';
134
+ const sourcePath = this.pathsByToolUseId.get(toolUseId);
135
+ const filename = sourcePath ? path.basename(sourcePath) : `image${extForMime(mediaType)}`;
136
+ const suggested = this.projectRelative(sourcePath);
137
+ if (buf.length > MAX_UPLOAD_BYTES) {
138
+ this.onWarn?.('transcript image exceeds upload cap - stubbed', { bytes: buf.length, filename });
139
+ return this.stub(filename, buf.length, 'too large to store');
140
+ }
141
+ try {
142
+ const ref = await this.upload(this.convGuid, buf, {
143
+ filename, mediaType, ...(suggested ? { suggestedPath: suggested } : {}),
144
+ });
145
+ return {
146
+ type: 'image_ref',
147
+ url: ref.url,
148
+ ...(ref.thumb_url ? { thumb_url: ref.thumb_url } : {}),
149
+ media_type: mediaType,
150
+ path: ref.path,
151
+ ...(ref.width != null ? { width: ref.width } : {}),
152
+ ...(ref.height != null ? { height: ref.height } : {}),
153
+ bytes: ref.bytes,
154
+ };
155
+ }
156
+ catch (err) {
157
+ this.onWarn?.('transcript image upload failed', { filename, err: err?.message });
158
+ // Small enough to survive the ingest cap inline → keep the original
159
+ // (renders exactly as before this feature). Oversize → stub, because
160
+ // shipping it would 400 the entire batch and lose sibling entries.
161
+ if (b64.length <= INLINE_SAFE_B64_CHARS)
162
+ return block;
163
+ return this.stub(filename, buf.length, `upload failed: ${err?.message || 'unknown error'}`);
164
+ }
165
+ }
166
+ stub(filename, bytes, reason) {
167
+ return {
168
+ type: 'text',
169
+ text: `[image ${filename} (${Math.round(bytes / 1024)} KB) omitted: ${reason}]`,
170
+ };
171
+ }
172
+ /** Map an absolute local path to its project-relative VFS path, when it
173
+ * falls inside the dispatch's project root. */
174
+ projectRelative(p) {
175
+ if (!p || !this.cwd)
176
+ return undefined;
177
+ const abs = path.resolve(p);
178
+ const root = path.resolve(this.cwd);
179
+ if (!abs.startsWith(root + path.sep))
180
+ return undefined;
181
+ return abs.slice(root.length + 1).split(path.sep).join('/');
182
+ }
183
+ }
184
+ //# sourceMappingURL=media-upload.js.map
package/dist/setup.js CHANGED
@@ -50,7 +50,10 @@ export const AIDER_CONF_FILE = '.aider.conf.yml';
50
50
  * scratch (the `_vsd_tmp/`/`_convert_tmp/` dirs that bloated past deploys)
51
51
  * can't leak in either. Reference material to KEEP (diagrams, decks, ADRs) goes
52
52
  * in `docs/` instead - synced and versioned, but outside `src/` so it's never
53
- * deployed. Gitignore-glob form, matched by the `ignore` package in config.ts. */
53
+ * deployed. Build screenshots follow the same keep-but-don't-deploy pattern in
54
+ * `screenshots/` (page-screenshot.ts writes there; the server excludes both
55
+ * dirs from root deploys in s3-deploy.ts). Gitignore-glob form, matched by the
56
+ * `ignore` package in config.ts. */
54
57
  export const SCRATCH_IGNORE = ['tmp/', '.tmp/', '*_tmp/', '.gipityscratch/'];
55
58
  export const DEFAULT_SYNC_IGNORE = [
56
59
  'node_modules', '.git', '.gipity.json', '.gipity/', '.claude/', '.gitignore', AIDER_CONF_FILE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gipity",
3
- "version": "1.0.428",
3
+ "version": "1.0.429",
4
4
  "description": "The full-stack platform tuned for AI agents. Database, storage, auth, functions, deploy, and drop-in kits - all agent-tuned. Pair with Claude Code or use standalone.",
5
5
  "bin": {
6
6
  "gipity": "dist/updater/shim.js",
@@ -14,7 +14,7 @@
14
14
  "prepack": "npm run build",
15
15
  "dev": "tsc --watch",
16
16
  "test": "npm run test:smoke",
17
- "test:smoke": "tsc && node --test dist/__tests__/utils.test.js dist/__tests__/platform.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-unretrievable.test.js dist/__tests__/sync-clean-check.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/api-401-retry.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/session-pool.test.js dist/__tests__/relay-diagnostics.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/ingest-queue.test.js dist/__tests__/stream-delta.test.js dist/__tests__/phase-tracker.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/capture-lock.test.js dist/__tests__/capture-resolve.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-bug.test.js dist/__tests__/cli-cmd-bug-queue.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-storage.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-setup.test.js dist/__tests__/cli-cmd-doctor.test.js dist/__tests__/claude-setup.test.js dist/__tests__/cli-cmd-sandbox.test.js dist/__tests__/cli-cmd-add.test.js dist/__tests__/cli-cmd-remove.test.js dist/__tests__/cli-cmd-skill.test.js dist/__tests__/cli-cmd-test.test.js dist/__tests__/cli-cmd-workflow.test.js dist/__tests__/setup-skills-block.test.js dist/__tests__/setup-hooks.test.js dist/__tests__/trace.test.js",
17
+ "test:smoke": "npm run build && node --test dist/__tests__/utils.test.js dist/__tests__/platform.test.js dist/__tests__/colors.test.js dist/__tests__/config.test.js dist/__tests__/sync.test.js dist/__tests__/sync-apply.test.js dist/__tests__/sync-unretrievable.test.js dist/__tests__/sync-clean-check.test.js dist/__tests__/sync-lock.test.js dist/__tests__/auth-lock.test.js dist/__tests__/api-401-retry.test.js dist/__tests__/push-cas.test.js dist/__tests__/upload.test.js dist/__tests__/progress.test.js dist/__tests__/updater.test.js dist/__tests__/cli-smoke.test.js dist/__tests__/claude-noninteractive.test.js dist/__tests__/claude-trust.test.js dist/__tests__/relay-state.test.js dist/__tests__/relay-daemon.test.js dist/__tests__/session-pool.test.js dist/__tests__/relay-diagnostics.test.js dist/__tests__/relay-installers.test.js dist/__tests__/relay-bridge-abort.test.js dist/__tests__/relay-redact.test.js dist/__tests__/relay-machine-id.test.js dist/__tests__/stream-json.test.js dist/__tests__/ingest-queue.test.js dist/__tests__/media-upload.test.js dist/__tests__/stream-delta.test.js dist/__tests__/phase-tracker.test.js dist/__tests__/relay-ingest-contract.test.js dist/__tests__/prompts.test.js dist/__tests__/capture-transcript.test.js dist/__tests__/capture-lock.test.js dist/__tests__/capture-resolve.test.js dist/__tests__/flag-aliases.test.js dist/__tests__/client-context.test.js dist/__tests__/adopt-cwd.test.js dist/__tests__/cli-cmd-agent.test.js dist/__tests__/cli-cmd-approval.test.js dist/__tests__/cli-cmd-audit.test.js dist/__tests__/cli-cmd-bug.test.js dist/__tests__/cli-cmd-bug-queue.test.js dist/__tests__/cli-cmd-chat.test.js dist/__tests__/cli-cmd-credits.test.js dist/__tests__/cli-cmd-db.test.js dist/__tests__/cli-cmd-deploy.test.js dist/__tests__/cli-cmd-domain.test.js dist/__tests__/cli-cmd-email.test.js dist/__tests__/cli-cmd-file.test.js dist/__tests__/cli-cmd-storage.test.js dist/__tests__/cli-cmd-fn.test.js dist/__tests__/cli-cmd-service.test.js dist/__tests__/cli-cmd-job.test.js dist/__tests__/cli-cmd-generate.test.js dist/__tests__/cli-cmd-gmail.test.js dist/__tests__/cli-cmd-info.test.js dist/__tests__/cli-cmd-init.test.js dist/__tests__/cli-cmd-location.test.js dist/__tests__/cli-cmd-text.test.js dist/__tests__/cli-cmd-login.test.js dist/__tests__/cli-cmd-logout.test.js dist/__tests__/cli-cmd-token.test.js dist/__tests__/cli-cmd-logs.test.js dist/__tests__/cli-cmd-memory.test.js dist/__tests__/cli-cmd-page.test.js dist/__tests__/cli-cmd-plan.test.js dist/__tests__/cli-cmd-project.test.js dist/__tests__/cli-cmd-rbac.test.js dist/__tests__/cli-cmd-realtime.test.js dist/__tests__/cli-cmd-records.test.js dist/__tests__/cli-cmd-relay.test.js dist/__tests__/cli-cmd-setup.test.js dist/__tests__/cli-cmd-doctor.test.js dist/__tests__/claude-setup.test.js dist/__tests__/cli-cmd-sandbox.test.js dist/__tests__/cli-cmd-add.test.js dist/__tests__/cli-cmd-remove.test.js dist/__tests__/cli-cmd-skill.test.js dist/__tests__/cli-cmd-test.test.js dist/__tests__/cli-cmd-workflow.test.js dist/__tests__/setup-skills-block.test.js dist/__tests__/setup-hooks.test.js dist/__tests__/trace.test.js",
18
18
  "test:smoke:quick": "node scripts/smoke-quick.mjs",
19
19
  "test:e2e": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-live.test.js dist/__tests__/cli-e2e-sync-live.test.js dist/__tests__/cli-e2e-sync-stress-live.test.js dist/__tests__/cli-e2e-rollback-live.test.js dist/__tests__/cli-e2e-services-media-live.test.js dist/__tests__/cli-e2e-workflow-live.test.js dist/__tests__/cli-e2e-sandbox-live.test.js dist/__tests__/cli-e2e-page-fetch-live.test.js dist/__tests__/cli-e2e-page-test-live.test.js",
20
20
  "test:e2e:sync": "tsc && GIPITY_E2E=1 node --test --test-timeout=180000 dist/__tests__/cli-e2e-sync-live.test.js",