@aku11i/phantom 6.3.0-1 → 6.3.0-3

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.
@@ -3,6 +3,7 @@ import { join } from "path";
3
3
  import { createReadStream, existsSync, statSync } from "fs";
4
4
  import fs, { copyFile, mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
5
5
  import path, { basename, dirname, extname, isAbsolute, join as join$1, relative, resolve, sep } from "node:path";
6
+ import { inflateSync } from "node:zlib";
6
7
  import { execFile, spawn } from "node:child_process";
7
8
  import { promisify } from "node:util";
8
9
  import { existsSync as existsSync$1 } from "node:fs";
@@ -24903,6 +24904,10 @@ function createUserInput(text, options = {}) {
24903
24904
  name: file.name,
24904
24905
  path: file.path
24905
24906
  });
24907
+ for (const attachment of options.attachments ?? []) input.push({
24908
+ type: "localImage",
24909
+ path: attachment.path
24910
+ });
24906
24911
  return input;
24907
24912
  }
24908
24913
  var CodexBridge = class {
@@ -25320,6 +25325,12 @@ const projectRecordBaseSchema = object({
25320
25325
  updatedAt: string(),
25321
25326
  lastOpenedAt: string()
25322
25327
  });
25328
+ const chatAttachmentRecordBaseSchema = object({
25329
+ name: string(),
25330
+ path: string(),
25331
+ mimeType: string(),
25332
+ size: number$1().int().nonnegative()
25333
+ });
25323
25334
  const chatMessageRecordBaseSchema = object({
25324
25335
  id: string(),
25325
25336
  chatId: string(),
@@ -25330,6 +25341,7 @@ const chatMessageRecordBaseSchema = object({
25330
25341
  "error"
25331
25342
  ]),
25332
25343
  text: string(),
25344
+ attachments: array(chatAttachmentRecordBaseSchema).optional(),
25333
25345
  eventType: string().optional(),
25334
25346
  eventData: unknown().optional(),
25335
25347
  itemId: string().optional(),
@@ -25344,6 +25356,7 @@ const queuedMessageRecordBaseSchema = object({
25344
25356
  chatId: string(),
25345
25357
  messageId: string(),
25346
25358
  text: string(),
25359
+ attachments: array(chatAttachmentRecordBaseSchema).optional(),
25347
25360
  effort: string().optional(),
25348
25361
  files: array(turnContextItemBaseSchema).optional(),
25349
25362
  model: string().optional(),
@@ -25493,6 +25506,14 @@ function createTimestamp() {
25493
25506
  //#endregion
25494
25507
  //#region src/services.ts
25495
25508
  const worktreeNameInferenceModel = "gpt-5.4-mini";
25509
+ const maxAttachmentBytes = 10 * 1024 * 1024;
25510
+ const maxDecodedImageBytes = 64 * 1024 * 1024;
25511
+ var QueuedAttachmentValidationError = class extends Error {
25512
+ constructor(error) {
25513
+ super(toErrorMessage(error));
25514
+ this.name = "QueuedAttachmentValidationError";
25515
+ }
25516
+ };
25496
25517
  function createWorktreeNameInferencePrompt(message) {
25497
25518
  return [
25498
25519
  "Infer a concise Git worktree and branch name from this user request.",
@@ -25527,6 +25548,7 @@ var ServeServices = class {
25527
25548
  eventHub;
25528
25549
  store;
25529
25550
  codex;
25551
+ attachmentDir;
25530
25552
  loadedThreadIds = /* @__PURE__ */ new Set();
25531
25553
  approvalRequests = /* @__PURE__ */ new Map();
25532
25554
  pendingTurnEvents = /* @__PURE__ */ new Map();
@@ -25544,6 +25566,7 @@ var ServeServices = class {
25544
25566
  this.eventHub = options.eventHub ?? new EventHub();
25545
25567
  this.store = options.store ?? new ServeStateStore();
25546
25568
  this.codex = options.codex ?? new CodexBridge();
25569
+ this.attachmentDir = options.attachmentDir ?? join$1(getServeDataDir(), "attachments");
25547
25570
  this.codex.onNotification((message) => {
25548
25571
  this.handleCodexNotification(message);
25549
25572
  });
@@ -26046,6 +26069,27 @@ var ServeServices = class {
26046
26069
  if (!isChatInActiveTurn(chat) && (isDrainingQueuedMessage || !this.pendingChatTurns.has(chatId) && state.queuedMessages.some((message) => message.chatId === chatId))) return this.queueMessage(chatId, input);
26047
26070
  return this.submitMessage(chatId, input, { requireActiveTurn: false });
26048
26071
  }
26072
+ async uploadAttachment(chatId, input) {
26073
+ const state = await this.store.load();
26074
+ this.requireChat(state, chatId);
26075
+ if (input.size <= 0 || input.bytes.byteLength <= 0) throw new Error("Attachment file is empty");
26076
+ if (input.size > 10485760 || input.bytes.byteLength > 10485760) throw new Error("Attachment file is too large");
26077
+ const mimeType = detectSupportedImageMimeType(input.bytes);
26078
+ if (!mimeType) throw new Error("Attachment file is not a supported image");
26079
+ const safeName = sanitizeAttachmentName(input.name);
26080
+ const extension = sanitizeAttachmentExtension(safeName, mimeType);
26081
+ const attachmentId = createRecordId("att");
26082
+ const chatAttachmentDir = join$1(this.attachmentDir, chatId);
26083
+ const attachmentPath = join$1(chatAttachmentDir, `${attachmentId}${extension}`);
26084
+ await mkdir(chatAttachmentDir, { recursive: true });
26085
+ await writeFile(attachmentPath, input.bytes);
26086
+ return {
26087
+ name: safeName,
26088
+ path: attachmentPath,
26089
+ mimeType,
26090
+ size: input.size
26091
+ };
26092
+ }
26049
26093
  async steerMessage(chatId, input) {
26050
26094
  return this.submitMessage(chatId, input, { requireActiveTurn: true });
26051
26095
  }
@@ -26071,6 +26115,7 @@ var ServeServices = class {
26071
26115
  shouldDrainQueuedMessages = !isQueueBlocked;
26072
26116
  shouldRequestPendingDrain = isDrainingQueuedMessage;
26073
26117
  const userMessage = createMessage(chat.id, "user", text, "chat.message.queued");
26118
+ userMessage.attachments = cloneAttachmentRecords(input.attachments);
26074
26119
  const queuedMessage = createQueuedMessage(chat.id, userMessage.id, input);
26075
26120
  queuedUserMessage = userMessage;
26076
26121
  return {
@@ -26171,7 +26216,7 @@ var ServeServices = class {
26171
26216
  if (this.pendingChatTurns.has(chatId)) throw new Error("Chat already has an active Codex turn");
26172
26217
  this.pendingChatTurns.add(chatId);
26173
26218
  }
26174
- const turnOptions = await this.createCodexTurnOptions(input, chat).catch(async (error) => {
26219
+ const turnOptions = options.prevalidatedTurnOptions ?? await this.createCodexTurnOptions(input, chat).catch(async (error) => {
26175
26220
  clearPendingChatTurn();
26176
26221
  if (options.queuedMessageId) await this.store.update((nextState) => ({
26177
26222
  ...nextState,
@@ -26187,7 +26232,10 @@ var ServeServices = class {
26187
26232
  const userMessage = existingUserMessage ? {
26188
26233
  ...existingUserMessage,
26189
26234
  eventType: void 0
26190
- } : createMessage(chat.id, "user", text, isSteeringActiveTurn ? "chat.message.steered" : void 0, isSteeringActiveTurn ? chat.activeTurnId ?? void 0 : void 0);
26235
+ } : {
26236
+ ...createMessage(chat.id, "user", text, isSteeringActiveTurn ? "chat.message.steered" : void 0, isSteeringActiveTurn ? chat.activeTurnId ?? void 0 : void 0),
26237
+ attachments: cloneAttachmentRecords(input.attachments)
26238
+ };
26191
26239
  let nextStatus = null;
26192
26240
  let nextActiveTurnId;
26193
26241
  let pendingTurnThreadId = null;
@@ -26316,10 +26364,12 @@ var ServeServices = class {
26316
26364
  return normalizeFileRecords(await this.codex.searchFiles(trimmedQuery, [chat.worktreePath]));
26317
26365
  }
26318
26366
  async createCodexTurnOptions(input, chat) {
26367
+ const attachments = await this.normalizeAttachmentItems(input.attachments, chat.id);
26319
26368
  const files = await normalizeFileContextItems(input.files, chat.worktreePath);
26320
26369
  const skills = await this.normalizeSkillContextItems(input.skills, chat.worktreePath);
26321
- if (!input.effort && !input.model && !input.serviceTier && files.length === 0 && skills.length === 0) return;
26370
+ if (!input.effort && !input.model && !input.serviceTier && attachments.length === 0 && files.length === 0 && skills.length === 0) return;
26322
26371
  const options = {};
26372
+ if (attachments.length > 0) options.attachments = attachments;
26323
26373
  if (input.effort) options.effort = input.effort;
26324
26374
  if (files.length > 0) options.files = files;
26325
26375
  if (input.model) options.model = input.model;
@@ -26327,6 +26377,39 @@ var ServeServices = class {
26327
26377
  if (skills.length > 0) options.skills = skills;
26328
26378
  return options;
26329
26379
  }
26380
+ async normalizeAttachmentItems(items, chatId) {
26381
+ const normalized = normalizeAttachmentRecords(items);
26382
+ if (normalized.length === 0) return [];
26383
+ const chatAttachmentDir = join$1(this.attachmentDir, chatId);
26384
+ return await Promise.all(normalized.map(async (item) => {
26385
+ const resolvedPath = resolve(item.path);
26386
+ if (!isPathInside(chatAttachmentDir, resolvedPath)) throw new Error(`Attachment path must be within this chat's attachment storage: ${item.path}`);
26387
+ let realChatAttachmentDir;
26388
+ try {
26389
+ realChatAttachmentDir = await realpath(chatAttachmentDir);
26390
+ } catch {
26391
+ throw new Error(`Attachment path is not an existing file: ${item.path}`);
26392
+ }
26393
+ let realAttachmentPath;
26394
+ try {
26395
+ realAttachmentPath = await realpath(resolvedPath);
26396
+ } catch {
26397
+ throw new Error(`Attachment path is not an existing file: ${item.path}`);
26398
+ }
26399
+ if (!isPathInside(realChatAttachmentDir, realAttachmentPath)) throw new Error(`Attachment path must resolve within this chat's attachment storage: ${item.path}`);
26400
+ const attachmentStat = await stat(realAttachmentPath);
26401
+ if (!attachmentStat.isFile()) throw new Error(`Attachment path is not a file: ${item.path}`);
26402
+ if (attachmentStat.size <= 0 || attachmentStat.size > 10485760) throw new Error("Attachment file is too large");
26403
+ const mimeType = detectSupportedImageMimeType(await readFile(realAttachmentPath));
26404
+ if (!mimeType) throw new Error("Attachment file is not a supported image");
26405
+ return {
26406
+ name: item.name,
26407
+ path: realAttachmentPath,
26408
+ mimeType,
26409
+ size: attachmentStat.size
26410
+ };
26411
+ }));
26412
+ }
26330
26413
  async normalizeSkillContextItems(items, worktreePath) {
26331
26414
  const normalized = normalizeTurnContextItems(items);
26332
26415
  if (normalized.length === 0) return [];
@@ -26478,6 +26561,12 @@ var ServeServices = class {
26478
26561
  if (isChatInActiveTurn(chat) || this.pendingChatTurns.has(chatId)) return;
26479
26562
  const queuedMessage = state.queuedMessages.find((message) => message.chatId === chatId);
26480
26563
  if (!queuedMessage) return;
26564
+ let prevalidatedTurnOptions;
26565
+ if ((queuedMessage.attachments ?? []).length > 0) try {
26566
+ prevalidatedTurnOptions = await this.createCodexTurnOptions(queuedMessageToSendInput(queuedMessage), chat);
26567
+ } catch (error) {
26568
+ throw new QueuedAttachmentValidationError(error);
26569
+ }
26481
26570
  const claimedQueuedMessageRef = { current: null };
26482
26571
  let queuedUserMessageMissing = false;
26483
26572
  await this.store.update((nextState) => {
@@ -26512,6 +26601,7 @@ var ServeServices = class {
26512
26601
  }
26513
26602
  await this.submitMessage(chatId, queuedMessageToSendInput(claimedQueuedMessage), {
26514
26603
  existingUserMessageId: claimedQueuedMessage.messageId,
26604
+ prevalidatedTurnOptions,
26515
26605
  requireActiveTurn: false
26516
26606
  });
26517
26607
  }
@@ -26529,7 +26619,8 @@ var ServeServices = class {
26529
26619
  } catch (error) {
26530
26620
  const wasReported = this.isAgentErrorReported(error);
26531
26621
  await this.addAgentErrorMessage(chatId, error);
26532
- if ((await this.store.load()).queuedMessages.some((message) => message.chatId === chatId)) this.pendingQueuedMessageDrainChatIds.add(chatId);
26622
+ const state = await this.store.load();
26623
+ if (!(error instanceof QueuedAttachmentValidationError) && state.queuedMessages.some((message) => message.chatId === chatId)) this.pendingQueuedMessageDrainChatIds.add(chatId);
26533
26624
  if (!wasReported) this.emitAgentError(chatId, error);
26534
26625
  }
26535
26626
  } while (this.pendingQueuedMessageDrainChatIds.has(chatId));
@@ -26838,7 +26929,7 @@ var ServeServices = class {
26838
26929
  const delta = typeof paramObject.delta === "string" ? paramObject.delta : "";
26839
26930
  if (!itemId || !delta) return;
26840
26931
  await this.store.update((state) => {
26841
- const existingMessage = state.messages.find((message) => message.chatId === chatId && message.itemId === itemId);
26932
+ const existingMessage = state.messages.find((message) => message.chatId === chatId && message.role === "assistant" && message.eventType === method && message.itemId === itemId);
26842
26933
  if (!existingMessage) return {
26843
26934
  ...state,
26844
26935
  messages: [...state.messages, createMessage(chatId, "assistant", delta, method, itemId)]
@@ -27118,6 +27209,7 @@ function createQueuedMessage(chatId, messageId, input) {
27118
27209
  chatId,
27119
27210
  messageId,
27120
27211
  text: input.text.trim(),
27212
+ attachments: cloneAttachmentRecords(input.attachments),
27121
27213
  effort: input.effort,
27122
27214
  files: cloneContextItems(input.files),
27123
27215
  model: input.model,
@@ -27128,6 +27220,7 @@ function createQueuedMessage(chatId, messageId, input) {
27128
27220
  }
27129
27221
  function queuedMessageToSendInput(message) {
27130
27222
  return {
27223
+ attachments: cloneAttachmentRecords(message.attachments),
27131
27224
  effort: message.effort,
27132
27225
  files: cloneContextItems(message.files),
27133
27226
  model: message.model,
@@ -27143,6 +27236,10 @@ function cloneContextItems(items) {
27143
27236
  path: item.path
27144
27237
  }));
27145
27238
  }
27239
+ function cloneAttachmentRecords(items) {
27240
+ const normalized = normalizeAttachmentRecords(items);
27241
+ return normalized.length > 0 ? normalized : void 0;
27242
+ }
27146
27243
  function isChatInActiveTurn(chat) {
27147
27244
  return Boolean(chat.activeTurnId && (chat.status === "running" || chat.status === "waitingForApproval"));
27148
27245
  }
@@ -27382,7 +27479,10 @@ function mergeCodexAndLocalMessages(codexMessages, localMessages, activeTurnId,
27382
27479
  const fallbackTranscriptCodexIndex = fallbackTranscriptLocalMessageMatches.get(message.id);
27383
27480
  if (fallbackTranscriptCodexIndex !== void 0 && unmatchedCodexMessageIndexes.includes(fallbackTranscriptCodexIndex)) {
27384
27481
  const fallbackCodexMessage = mergedCodexMessages[fallbackTranscriptCodexIndex];
27385
- if (fallbackCodexMessage) localMessageCodexOrderMatches.set(message.id, fallbackCodexMessage.codexOrder ?? fallbackTranscriptCodexIndex);
27482
+ if (fallbackCodexMessage) {
27483
+ localMessageCodexOrderMatches.set(message.id, fallbackCodexMessage.codexOrder ?? fallbackTranscriptCodexIndex);
27484
+ mergedCodexMessages[fallbackTranscriptCodexIndex] = mergeLocalMessageMetadataIntoCodexMessage(fallbackCodexMessage, message);
27485
+ }
27386
27486
  unmatchedCodexMessageIndexes.splice(unmatchedCodexMessageIndexes.indexOf(fallbackTranscriptCodexIndex), 1);
27387
27487
  return false;
27388
27488
  }
@@ -27403,15 +27503,25 @@ function mergeCodexAndLocalMessages(codexMessages, localMessages, activeTurnId,
27403
27503
  const matchedCodexIndex = findDeduplicatedCodexMessageIndex(unmatchedCodexMessageIndexes, mergedCodexMessages, message, steeredLocalMessageCounts, liveAssistantDeltaCodexOrderLimits, activeTurnId);
27404
27504
  if (matchedCodexIndex === void 0) return true;
27405
27505
  const matchedCodexMessage = mergedCodexMessages[matchedCodexIndex];
27406
- if (matchedCodexMessage) localMessageCodexOrderMatches.set(message.id, matchedCodexMessage.codexOrder ?? matchedCodexIndex);
27506
+ if (matchedCodexMessage) {
27507
+ localMessageCodexOrderMatches.set(message.id, matchedCodexMessage.codexOrder ?? matchedCodexIndex);
27508
+ mergedCodexMessages[matchedCodexIndex] = mergeLocalMessageMetadataIntoCodexMessage(matchedCodexMessage, message);
27509
+ }
27407
27510
  if (message.eventType === "chat.message.steered" && matchedCodexMessage) mergedCodexMessages[matchedCodexIndex] = {
27408
- ...matchedCodexMessage,
27511
+ ...mergedCodexMessages[matchedCodexIndex],
27409
27512
  createdAt: message.createdAt
27410
27513
  };
27411
27514
  unmatchedCodexMessageIndexes.splice(unmatchedCodexMessageIndexes.indexOf(matchedCodexIndex), 1);
27412
27515
  return false;
27413
27516
  }), localMessages, localMessageCodexOrderMatches, liveAssistantDeltaCodexOrderLimits, steeredLocalMessageCounts, activeTurnId).sort(compareMergedMessages).map(stripInternalCodexMessageMetadata);
27414
27517
  }
27518
+ function mergeLocalMessageMetadataIntoCodexMessage(codexMessage, localMessage) {
27519
+ const attachments = cloneAttachmentRecords(localMessage.attachments);
27520
+ return attachments ? {
27521
+ ...codexMessage,
27522
+ attachments
27523
+ } : codexMessage;
27524
+ }
27415
27525
  function localMessagesWithoutStaleSteeredState(chat, localMessages) {
27416
27526
  if (isChatInActiveTurn(chat)) return localMessages;
27417
27527
  return localMessages.map((message) => message.eventType === "chat.message.steered" ? {
@@ -27775,6 +27885,243 @@ function normalizeTurnContextItems(items) {
27775
27885
  path: item.path.trim()
27776
27886
  })).filter((item) => item.name && item.path);
27777
27887
  }
27888
+ function normalizeAttachmentRecords(items) {
27889
+ return (items ?? []).map((item) => ({
27890
+ name: item.name.trim(),
27891
+ path: item.path.trim(),
27892
+ mimeType: item.mimeType.trim().toLowerCase(),
27893
+ size: item.size
27894
+ })).filter((item) => item.name && item.path && item.mimeType.startsWith("image/") && Number.isInteger(item.size) && item.size > 0 && item.size <= 10485760);
27895
+ }
27896
+ function sanitizeAttachmentName(name) {
27897
+ return basename(name.trim()).replace(/[^\w .-]+/g, "_").trim() || "attachment";
27898
+ }
27899
+ function sanitizeAttachmentExtension(name, mimeType) {
27900
+ const currentExtension = extname(name).toLowerCase();
27901
+ if (mimeType === "image/png" && currentExtension === ".png") return currentExtension;
27902
+ return ".png";
27903
+ }
27904
+ function detectSupportedImageMimeType(bytes) {
27905
+ if (isValidPng(bytes)) return "image/png";
27906
+ return null;
27907
+ }
27908
+ function isValidPng(bytes) {
27909
+ if (bytes.length < 33 || bytes[0] !== 137 || bytes[1] !== 80 || bytes[2] !== 78 || bytes[3] !== 71 || bytes[4] !== 13 || bytes[5] !== 10 || bytes[6] !== 26 || bytes[7] !== 10) return false;
27910
+ let offset = 8;
27911
+ let sawIhdr = false;
27912
+ const idatChunks = [];
27913
+ let pngHeader = null;
27914
+ let finishedIdat = false;
27915
+ let sawIdat = false;
27916
+ let sawPlte = false;
27917
+ while (offset + 12 <= bytes.length) {
27918
+ const chunkLength = readUint32(bytes, offset);
27919
+ const chunkTypeOffset = offset + 4;
27920
+ const chunkDataOffset = offset + 8;
27921
+ const nextOffset = chunkDataOffset + chunkLength + 4;
27922
+ if (nextOffset > bytes.length) return false;
27923
+ const chunkType = readAscii(bytes, chunkTypeOffset, 4);
27924
+ if (!sawIhdr) {
27925
+ const header = parsePngHeader(bytes.subarray(chunkDataOffset, chunkDataOffset + 13));
27926
+ if (chunkType !== "IHDR" || chunkLength !== 13 || !header) return false;
27927
+ pngHeader = header;
27928
+ sawIhdr = true;
27929
+ } else if (chunkType === "IHDR") return false;
27930
+ if (crc32(bytes.subarray(chunkTypeOffset, chunkDataOffset + chunkLength)) !== readUint32(bytes, chunkDataOffset + chunkLength)) return false;
27931
+ if (chunkType === "IEND") return chunkLength === 0 && sawIdat && isValidPngPaletteState(pngHeader, sawPlte) && pngHeader != null && nextOffset === bytes.length && isValidPngImageData(idatChunks, pngHeader);
27932
+ if (sawIdat && chunkType !== "IDAT") finishedIdat = true;
27933
+ if (chunkType === "PLTE") {
27934
+ if (!pngHeader || sawIdat || sawPlte || !isValidPngPaletteChunk(chunkLength, pngHeader)) return false;
27935
+ sawPlte = true;
27936
+ }
27937
+ if (chunkType === "IDAT") {
27938
+ if (chunkLength === 0 || finishedIdat || !isValidPngPaletteState(pngHeader, sawPlte)) return false;
27939
+ idatChunks.push(bytes.subarray(chunkDataOffset, chunkDataOffset + chunkLength));
27940
+ sawIdat = true;
27941
+ }
27942
+ offset = nextOffset;
27943
+ }
27944
+ return false;
27945
+ }
27946
+ function isValidPngPaletteState(header, sawPlte) {
27947
+ if (!header) return false;
27948
+ if (header.colorType === 3) return sawPlte;
27949
+ return header.colorType === 2 || header.colorType === 6 ? true : !sawPlte;
27950
+ }
27951
+ function isValidPngPaletteChunk(chunkLength, header) {
27952
+ if (header.colorType !== 3 && header.colorType !== 2 && header.colorType !== 6) return false;
27953
+ const paletteEntries = chunkLength / 3;
27954
+ return Number.isInteger(paletteEntries) && paletteEntries >= 1 && paletteEntries <= 256 && (header.colorType !== 3 || paletteEntries <= 2 ** header.bitDepth);
27955
+ }
27956
+ function isValidPngImageData(chunks, header) {
27957
+ const expectedByteLength = getExpectedPngImageDataByteLength(header);
27958
+ if (expectedByteLength == null || expectedByteLength <= 0 || expectedByteLength > maxDecodedImageBytes) return false;
27959
+ const totalLength = chunks.reduce((total, chunk) => total + chunk.length, 0);
27960
+ const bytes = new Uint8Array(totalLength);
27961
+ let offset = 0;
27962
+ for (const chunk of chunks) {
27963
+ bytes.set(chunk, offset);
27964
+ offset += chunk.length;
27965
+ }
27966
+ try {
27967
+ const inflated = inflateSync(bytes, { maxOutputLength: expectedByteLength + 1 });
27968
+ return inflated.byteLength === expectedByteLength && hasValidPngScanlineFilters(inflated, header);
27969
+ } catch {
27970
+ return false;
27971
+ }
27972
+ }
27973
+ function hasValidPngScanlineFilters(bytes, header) {
27974
+ const rowLengths = getPngImageDataRowLengths(header);
27975
+ if (!rowLengths) return false;
27976
+ let offset = 0;
27977
+ for (const rowLength of rowLengths) {
27978
+ const filterType = bytes[offset];
27979
+ if (filterType == null || filterType > 4) return false;
27980
+ offset += rowLength;
27981
+ }
27982
+ return offset === bytes.length;
27983
+ }
27984
+ function parsePngHeader(bytes) {
27985
+ const width = readUint32(bytes, 0);
27986
+ const height = readUint32(bytes, 4);
27987
+ const bitDepth = bytes[8];
27988
+ const colorType = bytes[9];
27989
+ const compressionMethod = bytes[10];
27990
+ const filterMethod = bytes[11];
27991
+ const interlaceMethod = bytes[12];
27992
+ if (bytes.length === 13 && width > 0 && height > 0 && isValidPngColorTypeAndBitDepth(colorType, bitDepth) && compressionMethod === 0 && filterMethod === 0 && (interlaceMethod === 0 || interlaceMethod === 1)) return {
27993
+ bitDepth: bitDepth ?? 0,
27994
+ colorType: colorType ?? 0,
27995
+ height,
27996
+ interlaceMethod,
27997
+ width
27998
+ };
27999
+ return null;
28000
+ }
28001
+ function getExpectedPngImageDataByteLength(header) {
28002
+ const rowLengths = getPngImageDataRowLengths(header);
28003
+ if (!rowLengths) return null;
28004
+ const totalByteLength = rowLengths.reduce((total, rowLength) => total + rowLength, 0);
28005
+ return Number.isSafeInteger(totalByteLength) ? totalByteLength : null;
28006
+ }
28007
+ function getPngImageDataRowLengths(header) {
28008
+ const samplesPerPixel = getPngSamplesPerPixel(header.colorType);
28009
+ if (samplesPerPixel == null) return null;
28010
+ if (header.interlaceMethod === 0) {
28011
+ const rowLength = getPngRowLength(header.width, samplesPerPixel, header);
28012
+ return rowLength == null ? null : Array(header.height).fill(rowLength);
28013
+ }
28014
+ if (header.interlaceMethod !== 1) return null;
28015
+ const rowLengths = [];
28016
+ for (const pass of adam7Passes) {
28017
+ const passWidth = getAdam7PassSize(header.width, pass.xStart, pass.xStep);
28018
+ const passHeight = getAdam7PassSize(header.height, pass.yStart, pass.yStep);
28019
+ if (passWidth === 0 || passHeight === 0) continue;
28020
+ const rowLength = getPngRowLength(passWidth, samplesPerPixel, header);
28021
+ if (rowLength == null) return null;
28022
+ rowLengths.push(...Array(passHeight).fill(rowLength));
28023
+ }
28024
+ return rowLengths.length > 0 ? rowLengths : null;
28025
+ }
28026
+ const adam7Passes = [
28027
+ {
28028
+ xStart: 0,
28029
+ xStep: 8,
28030
+ yStart: 0,
28031
+ yStep: 8
28032
+ },
28033
+ {
28034
+ xStart: 4,
28035
+ xStep: 8,
28036
+ yStart: 0,
28037
+ yStep: 8
28038
+ },
28039
+ {
28040
+ xStart: 0,
28041
+ xStep: 4,
28042
+ yStart: 4,
28043
+ yStep: 8
28044
+ },
28045
+ {
28046
+ xStart: 2,
28047
+ xStep: 4,
28048
+ yStart: 0,
28049
+ yStep: 4
28050
+ },
28051
+ {
28052
+ xStart: 0,
28053
+ xStep: 2,
28054
+ yStart: 2,
28055
+ yStep: 4
28056
+ },
28057
+ {
28058
+ xStart: 1,
28059
+ xStep: 2,
28060
+ yStart: 0,
28061
+ yStep: 2
28062
+ },
28063
+ {
28064
+ xStart: 0,
28065
+ xStep: 1,
28066
+ yStart: 1,
28067
+ yStep: 2
28068
+ }
28069
+ ];
28070
+ function getAdam7PassSize(imageSize, passStart, passStep) {
28071
+ return imageSize > passStart ? Math.floor((imageSize - passStart + passStep - 1) / passStep) : 0;
28072
+ }
28073
+ function getPngRowLength(width, samplesPerPixel, header) {
28074
+ const bitsPerScanline = width * samplesPerPixel * header.bitDepth;
28075
+ const rowLength = Math.ceil(bitsPerScanline / 8) + 1;
28076
+ return Number.isSafeInteger(rowLength) ? rowLength : null;
28077
+ }
28078
+ function getPngSamplesPerPixel(colorType) {
28079
+ switch (colorType) {
28080
+ case 0:
28081
+ case 3: return 1;
28082
+ case 2: return 3;
28083
+ case 4: return 2;
28084
+ case 6: return 4;
28085
+ default: return null;
28086
+ }
28087
+ }
28088
+ function isValidPngColorTypeAndBitDepth(colorType, bitDepth) {
28089
+ switch (colorType) {
28090
+ case 0: return [
28091
+ 1,
28092
+ 2,
28093
+ 4,
28094
+ 8,
28095
+ 16
28096
+ ].includes(bitDepth ?? 0);
28097
+ case 2:
28098
+ case 4:
28099
+ case 6: return bitDepth === 8 || bitDepth === 16;
28100
+ case 3: return [
28101
+ 1,
28102
+ 2,
28103
+ 4,
28104
+ 8
28105
+ ].includes(bitDepth ?? 0);
28106
+ default: return false;
28107
+ }
28108
+ }
28109
+ function readUint32(bytes, offset) {
28110
+ return (bytes[offset] ?? 0) * 16777216 + ((bytes[offset + 1] ?? 0) << 16) + ((bytes[offset + 2] ?? 0) << 8) + (bytes[offset + 3] ?? 0) >>> 0;
28111
+ }
28112
+ function readAscii(bytes, offset, length) {
28113
+ let value = "";
28114
+ for (let index = 0; index < length; index += 1) value += String.fromCharCode(bytes[offset + index] ?? 0);
28115
+ return value;
28116
+ }
28117
+ function crc32(bytes) {
28118
+ let crc = 4294967295;
28119
+ for (const byte of bytes) {
28120
+ crc ^= byte;
28121
+ for (let bit = 0; bit < 8; bit += 1) crc = crc >>> 1 ^ (crc & 1 ? 3988292384 : 0);
28122
+ }
28123
+ return (crc ^ 4294967295) >>> 0;
28124
+ }
27778
28125
  async function normalizeFileContextItems(items, worktreePath) {
27779
28126
  const normalized = normalizeTurnContextItems(items);
27780
28127
  if (normalized.length === 0) return [];
@@ -28161,6 +28508,12 @@ const contextItemSchema = object({
28161
28508
  name: string().min(1),
28162
28509
  path: string().min(1)
28163
28510
  });
28511
+ const attachmentSchema = object({
28512
+ name: string().min(1),
28513
+ path: string().min(1),
28514
+ mimeType: string().min(1),
28515
+ size: number$1().int().nonnegative()
28516
+ });
28164
28517
  const createProjectSchema = object({ path: string().min(1, "Project path is required") });
28165
28518
  const createChatSchema = object({
28166
28519
  name: string().optional(),
@@ -28197,6 +28550,7 @@ const deleteWorktreeSchema = worktreeSchema.extend({
28197
28550
  });
28198
28551
  const sendMessageSchema = object({
28199
28552
  text: string().min(1, "Message text is required"),
28553
+ attachments: array(attachmentSchema).optional(),
28200
28554
  effort: string().nullable().optional(),
28201
28555
  model: string().nullable().optional(),
28202
28556
  serviceTier: _enum(["fast", "flex"]).nullable().optional(),
@@ -28208,6 +28562,7 @@ const chatMessageSchema = object({
28208
28562
  chatId: string().min(1),
28209
28563
  role: _enum(["user"]),
28210
28564
  text: string(),
28565
+ attachments: array(attachmentSchema).optional(),
28211
28566
  eventType: literal("chat.message.queued"),
28212
28567
  itemId: string().optional(),
28213
28568
  createdAt: string().min(1)
@@ -28217,6 +28572,7 @@ const queuedMessageSchema = object({
28217
28572
  chatId: string().min(1),
28218
28573
  messageId: string().min(1),
28219
28574
  text: string().min(1),
28575
+ attachments: array(attachmentSchema).optional(),
28220
28576
  effort: string().optional(),
28221
28577
  files: array(contextItemSchema).optional(),
28222
28578
  model: string().optional(),
@@ -28243,6 +28599,7 @@ const chatQuerySchema = object({
28243
28599
  context: string().optional(),
28244
28600
  fileQuery: string().optional()
28245
28601
  });
28602
+ const maxAttachmentMultipartBytes = maxAttachmentBytes + 64 * 1024;
28246
28603
  function jsonError(c, message, status = 400) {
28247
28604
  return c.json({ error: { message } }, status);
28248
28605
  }
@@ -28276,6 +28633,52 @@ function contextItems(value) {
28276
28633
  path: item.path
28277
28634
  }));
28278
28635
  }
28636
+ async function parseAttachmentUpload(c) {
28637
+ const file = (await parseLimitedFormData(c.req.raw, maxAttachmentMultipartBytes)).get("file");
28638
+ if (!(file instanceof File)) throw new Error("Attachment file is required");
28639
+ return {
28640
+ bytes: new Uint8Array(await file.arrayBuffer()),
28641
+ mimeType: file.type,
28642
+ name: file.name,
28643
+ size: file.size
28644
+ };
28645
+ }
28646
+ async function parseLimitedFormData(request, maxBytes) {
28647
+ const bytes = await readRequestBodyWithLimit(request, maxBytes);
28648
+ const body = new ArrayBuffer(bytes.byteLength);
28649
+ new Uint8Array(body).set(bytes);
28650
+ return await new Request(request.url, {
28651
+ body,
28652
+ headers: request.headers,
28653
+ method: request.method
28654
+ }).formData();
28655
+ }
28656
+ async function readRequestBodyWithLimit(request, maxBytes) {
28657
+ const contentLength = Number(request.headers.get("content-length") ?? "");
28658
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) throw new Error("Attachment file is too large");
28659
+ const body = request.body;
28660
+ if (!body) return new Uint8Array();
28661
+ const reader = body.getReader();
28662
+ const chunks = [];
28663
+ let totalBytes = 0;
28664
+ for (;;) {
28665
+ const { done, value } = await reader.read();
28666
+ if (done) break;
28667
+ totalBytes += value.byteLength;
28668
+ if (totalBytes > maxBytes) {
28669
+ await reader.cancel();
28670
+ throw new Error("Attachment file is too large");
28671
+ }
28672
+ chunks.push(value);
28673
+ }
28674
+ const bytes = new Uint8Array(totalBytes);
28675
+ let offset = 0;
28676
+ for (const chunk of chunks) {
28677
+ bytes.set(chunk, offset);
28678
+ offset += chunk.byteLength;
28679
+ }
28680
+ return bytes;
28681
+ }
28279
28682
  const rpcRoutes = new Hono().get("/health", async (c) => {
28280
28683
  try {
28281
28684
  return c.json(await getServeServices().getHealth(), 200);
@@ -28418,10 +28821,18 @@ const rpcRoutes = new Hono().get("/health", async (c) => {
28418
28821
  } catch (error) {
28419
28822
  return handleApiError(c, error);
28420
28823
  }
28824
+ }).post("/chats/:chatId/attachments", async (c) => {
28825
+ try {
28826
+ const attachment = await getServeServices().uploadAttachment(c.req.param("chatId"), await parseAttachmentUpload(c));
28827
+ return c.json({ attachment }, 201);
28828
+ } catch (error) {
28829
+ return handleApiError(c, error);
28830
+ }
28421
28831
  }).post("/chats/:chatId/messages", jsonBody(sendMessageSchema), async (c) => {
28422
28832
  try {
28423
28833
  const body = c.req.valid("json");
28424
28834
  const chat = await getServeServices().sendMessage(c.req.param("chatId"), {
28835
+ attachments: body.attachments,
28425
28836
  effort: optionalString(body.effort),
28426
28837
  files: contextItems(body.files),
28427
28838
  model: optionalString(body.model),
@@ -28459,6 +28870,7 @@ const rpcRoutes = new Hono().get("/health", async (c) => {
28459
28870
  try {
28460
28871
  const body = c.req.valid("json");
28461
28872
  const chat = await getServeServices().steerMessage(c.req.param("chatId"), {
28873
+ attachments: body.attachments,
28462
28874
  effort: optionalString(body.effort),
28463
28875
  files: contextItems(body.files),
28464
28876
  model: optionalString(body.model),
@@ -28474,6 +28886,7 @@ const rpcRoutes = new Hono().get("/health", async (c) => {
28474
28886
  try {
28475
28887
  const body = c.req.valid("json");
28476
28888
  const chat = await getServeServices().queueMessage(c.req.param("chatId"), {
28889
+ attachments: body.attachments,
28477
28890
  effort: optionalString(body.effort),
28478
28891
  files: contextItems(body.files),
28479
28892
  model: optionalString(body.model),
@@ -28731,4 +29144,4 @@ function createApp() {
28731
29144
  //#endregion
28732
29145
  export { EventHub as a, getServeServices as i, rpcRoutes as n, createSseResponse as o, ServeServices as r, parseLastEventId as s, createApp as t };
28733
29146
 
28734
- //# sourceMappingURL=app-xJWxEPI1.mjs.map
29147
+ //# sourceMappingURL=app-BLBOjh4e.mjs.map