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.
220 lines
5.5 KiB
TypeScript
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 };
|
|
}
|