| 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 | */ |
| 7 | import { 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. */ |
| 10 | export const SKILL_UPLOAD_MAX_BYTES = 2 * 1024 * 1024; |
| 11 | |
| 12 | export 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. */ |
| 19 | export 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`. */ |
| 24 | export 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 | } |