@aku11i/phantom 6.3.0-1 → 6.3.0-2
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/app/server/{app-xJWxEPI1.mjs → app-BLBOjh4e.mjs} +422 -9
- package/app/server/app-BLBOjh4e.mjs.map +1 -0
- package/app/server/index.mjs +1 -1
- package/app/server/start.mjs +1 -1
- package/app/web/assets/index-IcxJQt7a.css +2 -0
- package/app/web/assets/index-iSfjEV4I.js +52 -0
- package/app/web/index.html +2 -2
- package/package.json +1 -1
- package/phantom.js +2 -2
- package/app/server/app-xJWxEPI1.mjs.map +0 -1
- package/app/web/assets/index-BnKWip-y.js +0 -52
- package/app/web/assets/index-CVk_aJfU.css +0 -2
|
@@ -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
|
-
} :
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
-
...
|
|
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-
|
|
29147
|
+
//# sourceMappingURL=app-BLBOjh4e.mjs.map
|