Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how. | 1 | /** |
| 2 | * The files `make_file` makes (docs.g1t.sh/guides/agent-skills/): a PDF or | |
| 3 | * a Word document from Markdown, a spreadsheet or CSV from rows, or the | |
| 4 | * Markdown itself. Checked and written here, pure; tools.ts keeps the file | |
| 5 | * with an artifact. Nothing runs: each is written byte by byte. | |
| 6 | */ | |
| 7 | import { DOC_MAX_FILE_BYTES } from "../../../packages/contracts/src/docs.ts"; | |
| 8 | import { type MakeFileFormat, MAKE_FILE_FORMATS } from "../../../packages/contracts/src/skills.ts"; | |
| 9 | ||
| 10 | import { type Cell, type Sheet, workbook, markdownDocx } from "./ooxml.ts"; | |
| 11 | import { markdownPdf } from "./pdf.ts"; | |
| 12 | ||
| 13 | /** The longest Markdown a file is made from. */ | |
| 14 | export const MAX_FILE_MARKDOWN = 200_000; | |
| 15 | export const MAX_SHEETS = 10; | |
| 16 | export const MAX_SHEET_ROWS = 5_000; | |
| 17 | export const MAX_SHEET_COLUMNS = 50; | |
| 18 | export const MAX_CELL_CHARS = 2_000; | |
| 19 | ||
| 20 | export const CONTENT_TYPES: Record<MakeFileFormat, string> = { | |
| 21 | pdf: "application/pdf", | |
| 22 | docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| 23 | xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |
| 24 | csv: "text/csv", | |
| 25 | md: "text/markdown", | |
| 26 | }; | |
| 27 | ||
| 28 | const LABELS: Record<MakeFileFormat, string> = { pdf: "PDF", docx: "Word document", xlsx: "spreadsheet", csv: "CSV file", md: "Markdown file" }; | |
| 29 | ||
| 30 | export type MadeFile = { name: string; content_type: string; bytes: Uint8Array; format: MakeFileFormat; summary: string }; | |
| 31 | ||
| 32 | /** A file name from a title: `Q3 report` becomes `Q3 report.pdf`. */ | |
| 33 | export function fileName(title: string, format: MakeFileFormat): string { | |
| 34 | const base = | |
| 35 | String(title ?? "") | |
| 36 | .replace(/[\\/:*?"<>|\u0000-\u001f]+/g, " ") | |
| 37 | .replace(/\s+/g, " ") | |
| 38 | .trim() | |
| 39 | .replace(/\.(pdf|docx|xlsx|csv|md)$/i, "") | |
| 40 | .slice(0, 120) || "file"; | |
| 41 | return `${base}.${format}`; | |
| 42 | } | |
| 43 | ||
| 44 | /** Sheets from the model's input: a list of { name, rows }, rows of cells. */ | |
| 45 | export function readSheets(input: unknown): { ok: true; sheets: Sheet[] } | { ok: false; message: string } { | |
| 46 | if (!Array.isArray(input) || !input.length) return { ok: false, message: "Give sheets: a list of { name, rows }, the first row each sheet's header." }; | |
| 47 | if (input.length > MAX_SHEETS) return { ok: false, message: `A spreadsheet has at most ${MAX_SHEETS} sheets.` }; | |
| 48 | const sheets: Sheet[] = []; | |
| 49 | for (const [i, raw] of input.entries()) { | |
| 50 | const given = raw && typeof raw === "object" ? (raw as { name?: unknown; rows?: unknown }) : {}; | |
| 51 | if (!Array.isArray(given.rows) || !given.rows.length) return { ok: false, message: `Sheet ${i + 1} has no rows.` }; | |
| 52 | if (given.rows.length > MAX_SHEET_ROWS) return { ok: false, message: `A sheet has at most ${MAX_SHEET_ROWS} rows.` }; | |
| 53 | const rows: Cell[][] = []; | |
| 54 | for (const row of given.rows) { | |
| 55 | const cells = Array.isArray(row) ? row : [row]; | |
| 56 | if (cells.length > MAX_SHEET_COLUMNS) return { ok: false, message: `A sheet has at most ${MAX_SHEET_COLUMNS} columns.` }; | |
| 57 | rows.push( | |
| 58 | cells.map((cell): Cell => { | |
| 59 | if (cell === null || cell === undefined) return null; | |
| 60 | if (typeof cell === "number" || typeof cell === "boolean") return cell; | |
| 61 | return String(typeof cell === "object" ? JSON.stringify(cell) : cell).slice(0, MAX_CELL_CHARS); | |
| 62 | }), | |
| 63 | ); | |
| 64 | } | |
| 65 | sheets.push({ name: typeof given.name === "string" ? given.name : `Sheet${i + 1}`, rows }); | |
| 66 | } | |
| 67 | return { ok: true, sheets }; | |
| 68 | } | |
| 69 | ||
| 70 | /** One sheet as CSV (RFC 4180), with a byte-order mark so spreadsheet apps read it as UTF-8. */ | |
| 71 | export function csv(rows: Cell[][]): string { | |
| 72 | const field = (cell: Cell) => { | |
| 73 | const text = cell === null || cell === undefined ? "" : String(cell); | |
| 74 | return /[",\r\n]/.test(text) || /^\s|\s$/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; | |
| 75 | }; | |
| 76 | return `${rows.map((row) => row.map(field).join(",")).join("\r\n")}\r\n`; | |
| 77 | } | |
| 78 | ||
| 79 | /** Up to `limit` rows of a sheet as a Markdown table, for the doc a file is attached to. */ | |
| 80 | export function previewTable(sheet: Sheet, limit = 20): string { | |
| 81 | const columns = Math.max(1, ...sheet.rows.map((row) => row.length)); | |
| 82 | const cell = (value: Cell) => String(value ?? "").replace(/\|/g, "\\|").replace(/\s+/g, " ").slice(0, 80); | |
| 83 | const [header = [], ...body] = sheet.rows; | |
| 84 | const line = (row: Cell[]) => `| ${Array.from({ length: columns }, (_, c) => cell(row[c] ?? null)).join(" | ")} |`; | |
| 85 | const shown = body.slice(0, limit); | |
| 86 | return [line(header), `|${" --- |".repeat(columns)}`, ...shown.map(line), ...(body.length > shown.length ? ["", `_First ${shown.length} of ${body.length} rows._`] : [])].join("\n"); | |
| 87 | } | |
| 88 | ||
| 89 | /** | |
| 90 | * The file for one `make_file` call, or why not. `content` is Markdown | |
| 91 | * (pdf, docx, md); `sheets` are rows (xlsx, csv). | |
| 92 | */ | |
| 93 | export function makeFile(input: { format: unknown; title: string; content?: unknown; sheets?: unknown }, at = new Date()): { ok: true; file: MadeFile } | { ok: false; message: string } { | |
| 94 | const format = String(input.format ?? "").toLowerCase().replace(/^\./, "") as MakeFileFormat; | |
| 95 | if (!MAKE_FILE_FORMATS.includes(format)) { | |
| 96 | return { ok: false, message: `make_file writes ${MAKE_FILE_FORMATS.join(", ")}. Slide decks and images aren't available yet: say so, and offer a PDF or a doc instead.` }; | |
| 97 | } | |
| 98 | const title = String(input.title ?? "").trim().slice(0, 200); | |
| 99 | if (!title) return { ok: false, message: "Give the file a title." }; | |
| 100 | const name = fileName(title, format); | |
| 101 | let bytes: Uint8Array; | |
| 102 | let summary: string; | |
| 103 | if (format === "pdf" || format === "docx" || format === "md") { | |
| 104 | const markdown = typeof input.content === "string" ? input.content : ""; | |
| 105 | if (!markdown.trim()) return { ok: false, message: `Give the ${LABELS[format]}'s content as Markdown in content.` }; | |
| 106 | if (markdown.length > MAX_FILE_MARKDOWN) return { ok: false, message: `Content is at most ${MAX_FILE_MARKDOWN.toLocaleString("en-US")} characters: split it into more than one file.` }; | |
| 107 | if (format === "pdf") { | |
| 108 | const pdf = markdownPdf(title, markdown); | |
| 109 | bytes = pdf.bytes; | |
| 110 | summary = `${pdf.pages} ${pdf.pages === 1 ? "page" : "pages"}${pdf.truncated ? ", cut at the page limit" : ""}`; | |
| 111 | } else if (format === "docx") { | |
| 112 | bytes = markdownDocx(title, markdown, at); | |
| 113 | summary = "Word document"; | |
| 114 | } else { | |
| 115 | bytes = new TextEncoder().encode(markdown.endsWith("\n") ? markdown : `${markdown}\n`); | |
| 116 | summary = "Markdown"; | |
| 117 | } | |
| 118 | } else { | |
| 119 | const read = readSheets(input.sheets); | |
| 120 | if (!read.ok) return read; | |
| 121 | if (format === "csv") { | |
| 122 | if (read.sheets.length > 1) return { ok: false, message: "A CSV holds one sheet: make an xlsx for more, or one CSV per sheet." }; | |
| 123 | bytes = new TextEncoder().encode(csv(read.sheets[0]!.rows)); | |
| 124 | } else bytes = workbook(title, read.sheets, at); | |
| 125 | const rows = read.sheets.reduce((sum, sheet) => sum + Math.max(0, sheet.rows.length - 1), 0); | |
| 126 | summary = `${read.sheets.length === 1 ? "" : `${read.sheets.length} sheets, `}${rows} ${rows === 1 ? "row" : "rows"}`; | |
| 127 | } | |
| 128 | if (bytes.length > DOC_MAX_FILE_BYTES) return { ok: false, message: `That comes to more than ${DOC_MAX_FILE_BYTES / 1024 / 1024} MB: make it smaller, or split it.` }; | |
| 129 | return { ok: true, file: { name, content_type: CONTENT_TYPES[format], bytes, format, summary } }; | |
| 130 | } | |
| 131 | ||
| 132 | /** "34 KB". */ | |
| 133 | export function sizeLabel(bytes: number): string { | |
| 134 | if (bytes < 1024) return `${bytes} bytes`; | |
| 135 | if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`; | |
| 136 | return `${(bytes / 1024 / 1024).toFixed(1)} MB`; | |
| 137 | } | |
| 138 | ||
| 139 | /** Bytes as base64, in chunks so a large file never overflows the call stack. */ | |
| 140 | export function base64(bytes: Uint8Array): string { | |
| 141 | let binary = ""; | |
| 142 | for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); | |
| 143 | return btoa(binary); | |
| 144 | } |