| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
import type { Database } from "bun:sqlite"; |
| 13 |
import { claudeRunner, drawArgs, resultText, type RunClaude } from "./claude.ts"; |
| 14 |
import { ask, COUNT, parseReply, type Kind } from "./glyph.ts"; |
| 15 |
import { emit } from "./live.ts"; |
| 16 |
|
| 17 |
|
| 18 |
export type Drawer = (prompt: string, kind: Kind) => Promise<string[]>; |
| 19 |
|
| 20 |
export const NO_TOKEN = |
| 21 |
"Drawing needs a Claude token: set CLAUDE_CODE_OAUTH_TOKEN in Settings → App → Environment (from `claude setup-token`), then try again."; |
| 22 |
|
| 23 |
let drawer: Drawer = async () => { |
| 24 |
throw new Error(NO_TOKEN); |
| 25 |
}; |
| 26 |
|
| 27 |
export function setDrawer(next: Drawer): void { |
| 28 |
drawer = next; |
| 29 |
} |
| 30 |
|
| 31 |
|
| 32 |
export function claudeDrawer(run: RunClaude): Drawer { |
| 33 |
return async (prompt, kind) => { |
| 34 |
const answer = resultText(await run(drawArgs(), ask(prompt, kind))); |
| 35 |
if (!answer.ok) throw new Error(answer.why); |
| 36 |
const svgs = parseReply(answer.text); |
| 37 |
if (svgs.length === 0) throw new Error("Claude answered, but no drawing in it passed the checks. Try again."); |
| 38 |
return svgs.slice(0, COUNT); |
| 39 |
}; |
| 40 |
} |
| 41 |
|
| 42 |
|
| 43 |
export function drawerFromEnv( |
| 44 |
dataDir: string, |
| 45 |
from: Readonly<Record<string, string | undefined>> = process.env, |
| 46 |
): Drawer | undefined { |
| 47 |
const token = from.CLAUDE_CODE_OAUTH_TOKEN ?? ""; |
| 48 |
return token === "" ? undefined : claudeDrawer(claudeRunner(token, dataDir)); |
| 49 |
} |
| 50 |
|
| 51 |
|
| 52 |
const running = new Set<Promise<void>>(); |
| 53 |
export const settled = (): Promise<unknown> => Promise.all([...running]); |
| 54 |
|
| 55 |
|
| 56 |
export function kick(db: Database, id: number, prompt: string, kind: Kind): void { |
| 57 |
const job = (async () => { |
| 58 |
let row: unknown; |
| 59 |
try { |
| 60 |
const svgs = await drawer(prompt, kind); |
| 61 |
row = db |
| 62 |
.query("UPDATE drawing SET status = 'done', svgs = ?1, error = NULL, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?2 AND status = 'drawing' RETURNING *") |
| 63 |
.get(JSON.stringify(svgs), id); |
| 64 |
} catch (error) { |
| 65 |
const message = error instanceof Error ? error.message : String(error); |
| 66 |
row = db |
| 67 |
.query("UPDATE drawing SET status = 'failed', error = ?1, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?2 AND status = 'drawing' RETURNING *") |
| 68 |
.get(message.slice(0, 500), id); |
| 69 |
} |
| 70 |
if (row !== null) emit("drawing.changed", row); |
| 71 |
})().catch((error) => console.error("drawing", id, error)); |
| 72 |
running.add(job); |
| 73 |
void job.finally(() => running.delete(job)); |
| 74 |
} |
| 75 |
|
| 76 |
|
| 77 |
export function recover(db: Database): number { |
| 78 |
return db |
| 79 |
.query("UPDATE drawing SET status = 'failed', error = 'Interrupted: the app restarted while drawing. Try again.' WHERE status = 'drawing'") |
| 80 |
.run().changes; |
| 81 |
} |