Files
cobble/src/scripts/seed-bots.ts
T
tbenandClaude Opus 5.5 694c83346f Make Cobble a local install: setup flow, bots-first admin, settings
Local, password-only auth:
- No public pages; everything outside /admin and /auth redirects in
- First run (no users) sends every page to /auth/signup, which creates
  the super admin (user #1) once and then locks
- Login is email/username + password; removed the emailed-code flow,
  /auth/sso and email password reset
- Google login kept for existing accounts only, never creates them
- seed-super-admin now requires SEED_SUPER_ADMIN_PASSWORD

Bots-first admin:
- /admin and closed sections (people, logs, backups) redirect to
  /admin/bots; only bots and settings are live (AdminSections)
- Removed the old admin layout; every /admin page uses the bots rail
- Coordinator bot created at signup/seed and by /admin/bots
  (ensureCoordinatorBot), which redirects to its chat
- bots.system_prompt (markdown) with a default coordinator prompt
- seed-bots reuses the existing coordinator

Settings:
- Sub pages general (default), profile, change-password, listed in the
  rail with a back link; unknown settings paths redirect to general
- Redesigned settings UI: pane header, grouped rows, sticky save bar
- General: System/Light/Dark theme, saved per device

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 17:49:25 +01:00

364 lines
9.2 KiB
TypeScript

import BunSQLite from "@moduletrace/bun-sqlite";
import type {
BUN_SQLITE_COBBLE_BOT_MESSAGES,
BUN_SQLITE_COBBLE_BOT_TASK_STEPS,
BUN_SQLITE_COBBLE_BOT_TASKS,
BUN_SQLITE_COBBLE_BOTS,
BUN_SQLITE_COBBLE_USERS,
BunSQLiteTables,
} from "@/db/types/db";
import type {
BotFace,
BotHue,
BotMessageAuthor,
BotTaskStepState,
} from "@/src/dict/bot-dict";
import ensureCoordinatorBot from "@/src/functions/backend/db/bots/ensure-coordinator-bot";
/**
* Seeds the sample crew from the approved chat design into one user's
* workspace. The coordinator already exists (it's made with the account),
* so its sample messages go onto it. Idempotent: skips a user who already
* has worker bots.
*
* Usage: `bun run seed:bots [email]` (defaults to SEED_SUPER_ADMIN_EMAIL).
*/
const EMAIL =
process.argv[2] ||
process.env.SEED_SUPER_ADMIN_EMAIL ||
"[email protected]";
const MINUTE = 60 * 1000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
type SeedMessage = {
author: BotMessageAuthor;
text: string;
ago: number;
needs_you?: boolean;
};
type SeedTask = {
title: string;
steps: { label: string; state: BotTaskStepState }[];
};
type SeedBot = {
name: string;
hue: BotHue | null;
role: string | null;
face: BotFace;
status_line: string;
unread?: boolean;
messages: SeedMessage[];
task?: SeedTask;
};
const Crew: SeedBot[] = [
{
name: "Coordinator",
hue: null,
role: null,
face: "idle",
status_line: "",
messages: [
{
author: "bot",
text: "Three bots are working. One needs you.",
ago: 20 * 1000,
},
{
author: "bot",
text: "Shelf and Draft are done. Inbox needs you on the Zendesk login.",
ago: 10 * 1000,
},
],
},
{
name: "Inbox",
hue: "ember",
role: "Triage your mail",
face: "needs-you",
status_line: "Waiting on you",
messages: [
{
author: "user",
text: "Chase the Zendesk queue today. Anything you can't get into, tell me.",
ago: 7 * MINUTE,
},
{
author: "bot",
text: "Signed in. 14 tickets triaged, 3 need a decision from you.",
ago: 4 * MINUTE,
},
{
author: "bot",
text: "Zendesk SSO wants a code I can't read. Send it and I'll finish the queue.",
ago: 30 * 1000,
needs_you: true,
},
],
task: {
title: "Zendesk queue",
steps: [
{ label: "Signed in", state: "done" },
{ label: "Triaged 14 tickets", state: "done" },
{ label: "Waiting on SSO code", state: "active" },
],
},
},
{
name: "Ledger",
hue: "amber",
role: "Match receipts to the ledger",
face: "working",
status_line: "Working",
unread: true,
messages: [
{ author: "bot", text: "Matched 14 receipts", ago: 2 * MINUTE },
],
task: {
title: "October receipts",
steps: [
{ label: "Pulled 31 receipts", state: "done" },
{ label: "Matched 14 receipts", state: "active" },
{ label: "Flag the unmatched", state: "pending" },
],
},
},
{
name: "Watcher",
hue: "cobalt",
role: "Watch the build queue",
face: "working",
status_line: "Working",
unread: true,
messages: [
{
author: "bot",
text: "Nothing new since 06:40",
ago: 14 * MINUTE,
},
],
},
{
name: "Shelf",
hue: "moss",
role: "File your downloads",
face: "done",
status_line: "Done",
messages: [{ author: "bot", text: "Filed 32 downloads", ago: HOUR }],
},
{
name: "Reader",
hue: "violet",
role: "Summarise papers you save",
face: "working",
status_line: "Working",
messages: [
{ author: "bot", text: "Summarised 3 papers", ago: 3 * HOUR },
],
},
{
name: "Draft",
hue: "rose",
role: "Draft replies for review",
face: "done",
status_line: "Done",
unread: true,
messages: [
{
author: "bot",
text: "Two replies ready to review",
ago: 5 * HOUR,
},
],
},
{
name: "Sweep",
hue: "teal",
role: "Clear stale branches overnight",
face: "asleep",
status_line: "Asleep until 08:00",
messages: [
{ author: "bot", text: "Asleep until 08:00", ago: 5 * DAY },
],
},
];
type Table = (typeof BunSQLiteTables)[number];
async function insertBot({
user_id,
bot,
now,
}: {
user_id: number;
bot: SeedBot;
now: number;
}) {
const last_ago = Math.min(...bot.messages.map((m) => m.ago));
if (!bot.hue) {
const coordinator_id = await ensureCoordinatorBot({ user_id });
await BunSQLite.sql({
sql: `UPDATE bots SET face = ?, status_line = ?, last_activity_at = ? WHERE id = ?`,
values: [bot.face, bot.status_line, now - last_ago, coordinator_id],
});
await insertMessages({ bot_id: coordinator_id, bot, now });
console.log(` ${bot.name} (#${coordinator_id}, existing)`);
return;
}
const bot_res = await BunSQLite.insert<BUN_SQLITE_COBBLE_BOTS, Table>({
table: "bots",
data: [
{
user_id,
name: bot.name,
kind: bot.hue ? "worker" : "coordinator",
hue: bot.hue || undefined,
role: bot.role || undefined,
face: bot.face,
status_line: bot.status_line,
unread: bot.unread ? 1 : 0,
last_activity_at: now - last_ago,
},
],
});
const bot_id = bot_res.postInsertReturn?.insertId;
if (!bot_res.success || !bot_id) {
throw new Error(`Failed to create bot ${bot.name}: ${bot_res.msg}`);
}
await insertMessages({ bot_id, bot, now });
if (bot.task) {
await insertTask({ bot_id, task: bot.task });
}
console.log(` ${bot.name} (#${bot_id})`);
}
async function insertMessages({
bot_id,
bot,
now,
}: {
bot_id: number;
bot: SeedBot;
now: number;
}) {
const messages_res = await BunSQLite.insert<
BUN_SQLITE_COBBLE_BOT_MESSAGES,
Table
>({
table: "bot_messages",
data: bot.messages.map((message) => ({
bot_id,
author: message.author,
text: message.text,
needs_you: message.needs_you ? 1 : 0,
created_at: now - message.ago,
updated_at: now - message.ago,
})),
});
if (!messages_res.success) {
throw new Error(
`Failed to add messages for ${bot.name}: ${messages_res.msg}`,
);
}
}
async function insertTask({
bot_id,
task,
}: {
bot_id: number;
task: SeedTask;
}) {
const task_res = await BunSQLite.insert<BUN_SQLITE_COBBLE_BOT_TASKS, Table>(
{
table: "bot_tasks",
data: [{ bot_id, title: task.title, status: "active" }],
},
);
const task_id = task_res.postInsertReturn?.insertId;
if (!task_res.success || !task_id) {
throw new Error(`Failed to create task ${task.title}: ${task_res.msg}`);
}
const steps_res = await BunSQLite.insert<
BUN_SQLITE_COBBLE_BOT_TASK_STEPS,
Table
>({
table: "bot_task_steps",
data: task.steps.map((step, position) => ({
task_id,
label: step.label,
state: step.state,
position,
})),
});
if (!steps_res.success) {
throw new Error(`Failed to add steps for ${task.title}`);
}
}
async function seedBots() {
const user = (
await BunSQLite.select<BUN_SQLITE_COBBLE_USERS, Table>({
table: "users",
query: { query: { email: { value: EMAIL } } },
})
).payload?.[0];
if (!user?.id) {
console.error(
`No user with email ${EMAIL}. Run \`bun run seed\` first or pass an email.`,
);
process.exit(1);
}
const existing_workers = (
await BunSQLite.select<BUN_SQLITE_COBBLE_BOTS, Table>({
table: "bots",
query: {
query: {
user_id: { value: user.id },
kind: { value: "worker" },
},
},
})
).payload;
if (existing_workers?.length) {
console.log(
`User #${user.id} <${EMAIL}> already has ${existing_workers.length} worker bots. Skipping.`,
);
return;
}
console.log(`Seeding crew for user #${user.id} <${EMAIL}>:`);
const now = Date.now();
for (const bot of Crew) {
await insertBot({ user_id: Number(user.id), bot, now });
}
}
await seedBots();
process.exit(0);