blob: 66b911ce1548e0c42d7ce303e1116eae25a0f118 [file]
import type { AttachmentRef, StorageRef } from './events.js';
/** Lives in core so @maka/runtime and @maka/storage share one type without a package cycle. */
export type AttachmentByteReader = (
ref: StorageRef,
) => Promise<{ ok: true; bytes: Uint8Array } | { ok: false; reason: string }>;
/** Per-send cap on attachment count, shared by renderer preflight and main resolve. */
export const MAX_ATTACHMENT_COUNT = 8;
/** Per-file byte cap, shared by renderer preflight, preload encode, and main resolve. */
export const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
/** Raw-byte cap for workspace images returned by Read, leaving room for Base64 transport overhead. */
export const MAX_READ_IMAGE_BYTES = 5 * 1024 * 1024;
export const MAX_MODEL_IMAGE_EDGE = 2000;
export const READ_IMAGE_TOO_LARGE_MESSAGE = `Image exceeds the ${MAX_READ_IMAGE_BYTES / 1024 / 1024}MB model input limit; downscale it and try again.`;
/** Leaves room for Base64 expansion, text, and tool schemas under provider request limits. */
export const MAX_PROVIDER_IMAGE_REQUEST_BYTES = 12 * 1024 * 1024;
export const PROVIDER_IMAGE_BUDGET_EXCEEDED_MESSAGE = `Image was read, but the per-request image budget (${MAX_PROVIDER_IMAGE_REQUEST_BYTES / 1024 / 1024}MB across all images this turn) was exceeded; earlier images were sent and this one was omitted. Read fewer or smaller images.`;
const MIME_BY_EXTENSION: Readonly<Record<string, string>> = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
webp: 'image/webp',
bmp: 'image/bmp',
pdf: 'application/pdf',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
doc: 'application/msword',
xls: 'application/vnd.ms-excel',
ppt: 'application/vnd.ms-powerpoint',
};
/**
* Best-effort MIME from a file name, used when the picker gives no MIME
* (Electron's openDialog only returns paths). Falls back to
* `application/octet-stream` so downstream validation always sees a MIME.
*/
export function guessMimeFromName(fileName: string): string {
const dot = fileName.lastIndexOf('.');
if (dot < 0 || dot === fileName.length - 1) return 'application/octet-stream';
const ext = fileName.slice(dot + 1).toLowerCase();
return MIME_BY_EXTENSION[ext] ?? 'application/octet-stream';
}
/**
* Route a MIME type to an {@link AttachmentRef} kind. The runtime
* consumption split is image vs. everything-else (images become provider
* image parts; other kinds are read on demand by the model via Read), so this
* only needs to single out the kinds that change
* consumption or display. Unknown / unmapped MIME falls back to `other`.
*
* `fileName` is consulted for kinds whose MIME is unreliable across OSes
* (Office documents arrive as `application/octet-stream` or a long
* `vnd.openxmlformats` string depending on the source); MIME still wins
* when it is present and specific.
*/
export function attachmentKindFromMimeType(
mimeType: string,
fileName?: string,
): AttachmentRef['kind'] {
const mime = mimeType.toLowerCase();
if (mime.startsWith('image/')) return 'image';
if (mime === 'application/pdf') return 'pdf';
if (fileName) {
const lowerName = fileName.toLowerCase();
if (
lowerName.endsWith('.docx') ||
lowerName.endsWith('.doc') ||
lowerName.endsWith('.xlsx') ||
lowerName.endsWith('.xls') ||
lowerName.endsWith('.pptx') ||
lowerName.endsWith('.ppt')
) {
return 'doc';
}
}
return 'other';
}