Skip to content
34 linesCodeBlameRaw
1/**
2 * Files people upload for a skill (the editor's files, Import's upload),
3 * as the agents service takes them: text as it is, anything else as
4 * standard base64. The service checks the folder (@g1t/contracts
5 * skill-format.ts); this only refuses what is plainly too large to send.
6 */
7import { SKILL_FOLDER_MAX_BYTES, type SkillFile } from "@g1t/contracts";
8
9/** The most one upload sends: a zip of a 1 MB folder is smaller. */
10export const SKILL_UPLOAD_MAX_BYTES = 2 * 1024 * 1024;
11
12export function base64Of(bytes: Uint8Array): string {
13 let binary = "";
14 for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
15 return btoa(binary);
16}
17
18/** The uploaded files in a form field that are real files, not empty inputs. */
19export function uploadedFiles(form: FormData, name: string): File[] {
20 return form.getAll(name).filter((value): value is File => typeof value === "object" && value !== null && "arrayBuffer" in value && (value as File).size > 0);
21}
22
23/** One uploaded file as a skill's file at `path`. */
24export async function skillFileOf(path: string, file: File): Promise<SkillFile | { error: string }> {
25 if (file.size > SKILL_FOLDER_MAX_BYTES) return { error: `${file.name} is over 1 MB, the most a skill holds.` };
26 const bytes = new Uint8Array(await file.arrayBuffer());
27 try {
28 const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes);
29 if (!text.includes("\u0000")) return { path, content: text, encoding: "utf8" };
30 } catch {
31 // Not text: sent as bytes.
32 }
33 return { path, content: base64Of(bytes), encoding: "base64" };
34}