evOo-local/packages/adapters/src/scratchpad-tools.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

220 lines
5.5 KiB
TypeScript

import type { PrismaClient } from "@rakazo/db";
export const SCRATCHPAD_STATUSES = ["open", "parked", "done"] as const;
export type ScratchpadStatus = (typeof SCRATCHPAD_STATUSES)[number];
export const OPEN_SCRATCHPAD_STATUSES: ScratchpadStatus[] = ["open", "parked"];
const TITLE_MAX = 200;
const NOTES_MAX = 4_000;
export type ScratchpadToolDeps = {
prisma: PrismaClient;
};
export type ScratchpadRow = {
id: string;
botId: string;
title: string;
status: string;
notes: string;
createdAt: Date;
updatedAt: Date;
};
export function mapScratchpadItem(row: ScratchpadRow) {
return {
id: row.id,
botId: row.botId,
title: row.title,
status: coerceScratchpadStatus(row.status),
notes: row.notes,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};
}
export function isScratchpadStatus(value: unknown): value is ScratchpadStatus {
return typeof value === "string" && (SCRATCHPAD_STATUSES as readonly string[]).includes(value);
}
export function coerceScratchpadStatus(value: unknown): ScratchpadStatus {
return isScratchpadStatus(value) ? value : "open";
}
export async function listScratchpadItems(
deps: ScratchpadToolDeps,
input: {
spaceId: string;
botId: string;
status?: ScratchpadStatus;
includeDone?: boolean;
},
) {
const statusFilter = input.status
? { status: input.status }
: input.includeDone
? {}
: { status: { in: OPEN_SCRATCHPAD_STATUSES } };
const rows = await deps.prisma.scratchpadItem.findMany({
where: {
spaceId: input.spaceId,
botId: input.botId,
...statusFilter,
},
orderBy: [{ updatedAt: "desc" }, { createdAt: "desc" }],
});
return rows.map(mapScratchpadItem);
}
export async function addScratchpadItemFromTool(
deps: ScratchpadToolDeps,
input: {
spaceId: string;
botId: string;
userId: string;
title: string;
status?: string;
notes?: string;
},
) {
const title = input.title.trim();
if (!title) return { error: "title is required." };
if (title.length > TITLE_MAX) return { error: `title must be at most ${TITLE_MAX} characters.` };
const notes = (input.notes ?? "").trim();
if (notes.length > NOTES_MAX) return { error: `notes must be at most ${NOTES_MAX} characters.` };
let status: ScratchpadStatus = "open";
if (input.status !== undefined) {
if (!isScratchpadStatus(input.status)) {
return { error: "status must be open, parked, or done." };
}
status = input.status;
}
const row = await deps.prisma.scratchpadItem.create({
data: {
spaceId: input.spaceId,
botId: input.botId,
userId: input.userId,
title,
status,
notes,
},
});
return { item: mapScratchpadItem(row) };
}
export async function updateScratchpadItemFromTool(
deps: ScratchpadToolDeps,
input: {
spaceId: string;
botId: string;
userId: string;
itemId: string;
title?: string;
status?: string;
notes?: string;
},
) {
const itemId = input.itemId.trim();
if (!itemId) return { error: "itemId is required." };
const existing = await deps.prisma.scratchpadItem.findFirst({
where: {
id: itemId,
spaceId: input.spaceId,
botId: input.botId,
userId: input.userId,
},
});
if (!existing) return { error: "Scratchpad item not found." };
const data: { title?: string; status?: string; notes?: string } = {};
if (input.title !== undefined) {
const title = input.title.trim();
if (!title) return { error: "title cannot be empty." };
if (title.length > TITLE_MAX)
return { error: `title must be at most ${TITLE_MAX} characters.` };
data.title = title;
}
if (input.status !== undefined) {
if (!isScratchpadStatus(input.status)) {
return { error: "status must be open, parked, or done." };
}
data.status = input.status;
}
if (input.notes !== undefined) {
const notes = input.notes.trim();
if (notes.length > NOTES_MAX)
return { error: `notes must be at most ${NOTES_MAX} characters.` };
data.notes = notes;
}
if (Object.keys(data).length === 0) {
return { error: "Provide title, status, and/or notes to update." };
}
const row = await deps.prisma.scratchpadItem.update({
where: { id: existing.id },
data,
});
return { item: mapScratchpadItem(row) };
}
export async function completeScratchpadItemFromTool(
deps: ScratchpadToolDeps,
input: {
spaceId: string;
botId: string;
userId: string;
itemId: string;
},
) {
return updateScratchpadItemFromTool(deps, {
...input,
status: "done",
});
}
export async function removeScratchpadItemFromTool(
deps: ScratchpadToolDeps,
input: {
spaceId: string;
botId: string;
userId: string;
itemId: string;
},
) {
const itemId = input.itemId.trim();
if (!itemId) return { error: "itemId is required." };
const existing = await deps.prisma.scratchpadItem.findFirst({
where: {
id: itemId,
spaceId: input.spaceId,
botId: input.botId,
userId: input.userId,
},
});
if (!existing) return { error: "Scratchpad item not found." };
await deps.prisma.scratchpadItem.delete({ where: { id: existing.id } });
return { ok: true as const, itemId: existing.id, title: existing.title };
}
export async function listScratchpadItemsFromTool(
deps: ScratchpadToolDeps,
input: {
spaceId: string;
botId: string;
includeDone?: boolean;
},
) {
const items = await listScratchpadItems(deps, {
spaceId: input.spaceId,
botId: input.botId,
includeDone: input.includeDone,
});
return { items };
}