feat: replace identitydb with supermemory

This commit is contained in:
2026-06-12 23:55:14 +09:00
parent e281b8a38f
commit 282c6f1348
17 changed files with 1207 additions and 1071 deletions

View File

@@ -1,8 +1,5 @@
import { randomUUID } from "node:crypto";
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { existsSync, readdirSync } from "fs";
import { unlink, writeFile } from "fs/promises";
import type { ExtractedFact } from "identitydb";
import { tmpdir } from "os";
interface RecordedCall {
@@ -19,36 +16,6 @@ const llmCalls: RecordedCall[] = [];
const PERSONA_DESCRIPTION = "A 34yo night-shift nurse, hides exhaustion behind sarcasm.";
const GENERATED_BASE_SYSTEM_PROMPT =
"You are Maren. You text in lowercase. You use '...' when tired.";
const EXTRACTED_FACTS: ExtractedFact[] = [
{
statement: "Maren is 34 years old.",
summary: "Maren is 34 years old.",
source: "persona-init",
confidence: 1.0,
topics: [
{
name: "maren-age",
category: "temporal",
granularity: "concrete",
role: "attribute",
},
],
},
{
statement: "Maren is a night-shift nurse.",
summary: "Maren is a night-shift nurse.",
source: "persona-init",
confidence: 1.0,
topics: [
{
name: "maren-occupation",
category: "entity",
granularity: "concrete",
role: "attribute",
},
],
},
];
const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
llmCalls.push({ model, options });
@@ -64,9 +31,6 @@ const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
) {
return GENERATED_BASE_SYSTEM_PROMPT as unknown as T;
}
if (options.jsonSchemaName === "fact-extractor") {
return { items: EXTRACTED_FACTS } as unknown as T;
}
throw new Error(
`unexpected LLM call: model=${model} instruction=${options.instruction?.slice(0, 80)}`,
);
@@ -82,26 +46,12 @@ mock.module("@/openrouter", () => ({
mock.module("@/config", () => ({
config: {
openrouterApiKey: "test-key",
dbPath: ":memory:",
supermemoryApiKey: "test-supermemory-key",
braindbPath: "/tmp/brainbox-test-braindb-debug-brain-IGNORED.json",
},
}));
mock.module("@/openrouter/embedding", () => ({
OpenRouterEmbeddingProvider: class {
model = "test-embed";
dimensions = 4;
async embed(_input: string): Promise<number[]> {
return [0, 0, 0, 0];
}
async embedMany(inputs: string[]): Promise<number[][]> {
return inputs.map(() => [0, 0, 0, 0]);
}
},
}));
const { runDebugBrainInit } = await import("./brain");
const { Brain: ProdBrain } = await import("@/brain");
beforeEach(() => {
llmCalls.length = 0;
@@ -109,22 +59,23 @@ beforeEach(() => {
});
afterEach(async () => {
const { unlink } = await import("fs/promises");
const tmpFiles = readdirSync(tmpdir()).filter((f) =>
f.startsWith("brainbox-debug-brain-"),
);
for (const f of tmpFiles) {
try {
const { unlink } = await import("fs/promises");
await unlink(`${tmpdir()}/${f}`);
} catch {}
}
});
describe("runDebugBrainInit", () => {
test("B1: returns ok result with full description, baseSystemPrompt, extractedFacts, and uses the supplied seed", async () => {
test("B1: returns ok result with full description, baseSystemPrompt, storedFacts, and uses the supplied seed", async () => {
const result = await runDebugBrainInit({
displayName: "Maren",
seed: "Maren, 34, night-shift nurse, hides exhaustion behind sarcasm",
noSupermemory: true,
});
expect(result.ok).toBe(true);
@@ -148,18 +99,22 @@ describe("runDebugBrainInit", () => {
),
);
expect(result.extractedFacts).toEqual(EXTRACTED_FACTS);
expect(result.storedFacts).toHaveLength(1);
expect(result.storedFacts[0]!.customId).toBe("persona");
expect(result.storedFacts[0]!.content).toContain(PERSONA_DESCRIPTION);
expect(typeof result.elapsedMs).toBe("number");
expect(result.elapsedMs).toBeGreaterThanOrEqual(0);
});
test("B2: invokes the LLM exactly 3 times — PERSONA_INIT, PERSONA_BASE_SYSTEM_PROMPT, fact-extractor", async () => {
test("B2: invokes the LLM exactly 2 times — PERSONA_INIT and PERSONA_BASE_SYSTEM_PROMPT", async () => {
await runDebugBrainInit({
displayName: "Test",
seed: "a seed",
noSupermemory: true,
});
expect(llmCalls.length).toBe(3);
expect(llmCalls.length).toBe(2);
const initCall = llmCalls[0]!;
expect(initCall.options.message).toBe("a seed");
@@ -168,32 +123,28 @@ describe("runDebugBrainInit", () => {
const systemCall = llmCalls[1]!;
expect(systemCall.options.jsonSchemaName).toBeUndefined();
expect(systemCall.options.message).toBe(PERSONA_DESCRIPTION);
const factCall = llmCalls[2]!;
expect(factCall.options.jsonSchemaName).toBe("fact-extractor");
expect(factCall.options.message).toBe(PERSONA_DESCRIPTION);
});
test("B3: writes no real on-disk state — no brainbox.db, no brainbox.json, no leftover temp braindb in /tmp", async () => {
test("B3: writes no real on-disk state — no leftover temp braindb in /tmp, no stray files in cwd", async () => {
const cwd = process.cwd();
const beforeDb = existsSync(`${cwd}/brainbox.db`);
const beforeJson = existsSync(`${cwd}/brainbox.json`);
const beforeCwdEntries = readdirSync(cwd);
const beforeTmp = readdirSync(tmpdir()).filter((f) =>
f.startsWith("brainbox-debug-brain-"),
);
await runDebugBrainInit({ displayName: "NoDiskCheck", seed: "x" });
await runDebugBrainInit({ displayName: "NoDiskCheck", seed: "x", noSupermemory: true });
const afterDb = existsSync(`${cwd}/brainbox.db`);
const afterJson = existsSync(`${cwd}/brainbox.json`);
const afterCwdEntries = readdirSync(cwd);
const afterTmp = readdirSync(tmpdir()).filter((f) =>
f.startsWith("brainbox-debug-brain-"),
);
expect(afterDb).toBe(beforeDb);
expect(afterJson).toBe(beforeJson);
expect(afterCwdEntries).toEqual(beforeCwdEntries);
expect(afterTmp).toHaveLength(0);
expect(existsSync(`${cwd}/brainbox.db`)).toBe(false);
expect(existsSync(`${cwd}/brainbox.json`)).toBe(false);
});
test("B4: when Brain.create returns null (e.g. LLM throws), result is {ok: false, error}", async () => {
@@ -204,6 +155,7 @@ describe("runDebugBrainInit", () => {
const result = await runDebugBrainInit({
displayName: "Doomed",
seed: "x",
noSupermemory: true,
});
expect(result.ok).toBe(false);
@@ -213,10 +165,11 @@ describe("runDebugBrainInit", () => {
expect(result.elapsedMs).toBeGreaterThanOrEqual(0);
});
test("B5: with no DB_PATH / BRAINDB_PATH env, runDebugBrainInit still works (no env dependency)", async () => {
test("B5: with no BRAINDB_PATH env, runDebugBrainInit still works (no env dependency)", async () => {
const result = await runDebugBrainInit({
displayName: "EnvFree",
seed: "no env",
noSupermemory: true,
});
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("expected ok");
@@ -225,48 +178,12 @@ describe("runDebugBrainInit", () => {
});
});
describe("Brain.create (production path — debug: false)", () => {
test("B6: with debug omitted (default), uses db.ingestStatements and does NOT return extractedFacts", async () => {
const braindbPath = `${tmpdir()}/brainbox-prod-brain-${randomUUID()}.json`;
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
const result = await ProdBrain.create("ProdMaren", "a prod seed", {
dbPath: ":memory:",
braindbPath,
});
try {
expect(result).not.toBeNull();
if (!result) throw new Error("expected result");
expect(result.brain).toBeDefined();
expect(result.description).toBe(PERSONA_DESCRIPTION);
expect(result.baseSystemPrompt).toContain(GENERATED_BASE_SYSTEM_PROMPT);
expect(result.extractedFacts).toBeUndefined();
} finally {
try {
await unlink(braindbPath);
} catch {}
}
});
test("B7: with debug: false (explicit), same as default — uses db.ingestStatements, no extractedFacts", async () => {
const braindbPath = `${tmpdir()}/brainbox-prod-brain-${randomUUID()}.json`;
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
const result = await ProdBrain.create("ProdMaren2", "seed", {
dbPath: ":memory:",
braindbPath,
debug: false,
});
try {
expect(result).not.toBeNull();
if (!result) throw new Error("expected result");
expect(result.extractedFacts).toBeUndefined();
} finally {
try {
await unlink(braindbPath);
} catch {}
}
});
});
// ---------------------------------------------------------------------------
// Removed: B6 and B7 (production path with `debug: true|false` option).
//
// Reason: `Brain.create` no longer accepts a `debug` option. The production
// path is now identical to the debug path — `Brain.create` always persists
// facts to supermemory and returns `{ brain, description, baseSystemPrompt }`
// (no `extractedFacts`). B1 already exercises the post-refactor production
// behavior end-to-end through `runDebugBrainInit`.
// ---------------------------------------------------------------------------

View File

@@ -3,15 +3,20 @@ import { unlink, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import type { Command } from "commander";
import ora from "ora";
import type { ExtractedFact } from "identitydb";
import { Brain } from "@/brain";
import { runCreateSteps } from "@/brain";
import { MemoryStub } from "@/brain/stub";
import { formatDuration } from "@/utils/duration";
import { logger } from "@/utils/logger";
import {
StepDriver,
printKeyValue,
printSection,
} from "./output";
export interface BrainInitOptions {
displayName: string;
seed: string;
noSupermemory: boolean;
}
export type BrainInitResult =
@@ -23,26 +28,12 @@ export type BrainInitResult =
spaceName: string;
description: string;
baseSystemPrompt: string;
extractedFacts: ExtractedFact[];
storedFacts: Array<{ customId: string | null; content: string }>;
storageMode: "supermemory" | "stub";
elapsedMs: number;
}
| { ok: false; error: string; elapsedMs: number };
/**
* Exercise the full `Brain.create` flow (PERSONA_INIT → PERSONA_BASE_SYSTEM_PROMPT
* LLM calls → SQLite DB upsert → fact extraction via `factExtractor.extract` →
* braindb save) without touching real on-disk state.
*
* - SQLite DB uses `:memory:` (ephemeral, dies with the process).
* - The braindb JSON is written to a fresh temp file under `os.tmpdir()`
* and unlinked after the run.
*
* Prints the full text of:
* 1. the generated `description` (PERSONA_INIT output)
* 2. the concatenated `baseSystemPrompt` (generated + fixed)
* 3. the `extractedFacts` (obtained by directly calling
* `factExtractor.extract(description)`)
*/
export async function runDebugBrainInit(
opts: BrainInitOptions,
): Promise<BrainInitResult> {
@@ -52,56 +43,53 @@ export async function runDebugBrainInit(
`brainbox-debug-brain-${randomUUID()}.json`,
);
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
const spinner = ora(
`Initializing brain "${opts.displayName}" with LLM (debug, no real disk state)...`,
).start();
const storageMode = opts.noSupermemory ? "stub" : "supermemory";
const db = opts.noSupermemory ? new MemoryStub() : undefined;
try {
const result = await Brain.create(opts.displayName, opts.seed, {
dbPath: ":memory:",
const steps = new StepDriver(4);
const result = await runCreateSteps(opts.displayName, opts.seed, {
braindbPath,
debug: true,
});
db,
}, steps);
if (!result) {
spinner.fail("Brain initialization failed");
const elapsedMs = Date.now() - startTime;
return { ok: false, error: "Brain initialization failed", elapsedMs };
}
const {
brain,
description,
baseSystemPrompt,
extractedFacts,
} = result;
const factCount = extractedFacts?.length ?? 0;
spinner.succeed(
`Brain initialized (id=${brain.brainbase.brainId}, space=${brain.brainbase.spaceName}, ${factCount} fact(s) extracted)`,
);
const { brain, description, baseSystemPrompt } = result;
const storedFacts = await brain.list();
printSection(`Description (PERSONA_INIT output)`);
console.log();
printSection(`Brain — ${brain.brainbase.displayName}`);
printKeyValue({
brainId: brain.brainbase.brainId,
spaceName: brain.brainbase.spaceName,
storage: storageMode,
documents: String(storedFacts.length),
});
console.log();
printSection(`Step 1 output — Description (PERSONA_INIT)`);
console.log(description);
console.log();
printSection(`baseSystemPrompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)`);
printSection(`Step 2 output — baseSystemPrompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)`);
console.log(baseSystemPrompt);
console.log();
printSection(
`Extracted facts (factExtractor.extract — ${factCount})`,
);
if (extractedFacts && extractedFacts.length > 0) {
extractedFacts.forEach((fact, i) => {
console.log(`\n[${i + 1}/${extractedFacts.length}]`);
console.log(` statement: ${fact.statement ?? ""}`);
console.log(` summary: ${fact.summary ?? ""}`);
console.log(` source: ${fact.source ?? ""}`);
console.log(` confidence: ${fact.confidence ?? ""}`);
console.log(` topics: ${JSON.stringify(fact.topics)}`);
if (fact.metadata) {
console.log(` metadata: ${JSON.stringify(fact.metadata)}`);
}
printSection(`Step 3 output — Stored documents (brain.list() — ${storedFacts.length})`);
if (storedFacts.length > 0) {
storedFacts.forEach((doc, i) => {
console.log();
console.log(`[${i + 1}/${storedFacts.length}]`);
printKeyValue({
customId: doc.customId ?? "(none)",
content: doc.content,
});
});
} else {
console.log(" (no facts extracted)");
console.log(" (no documents stored)");
}
console.log();
@@ -118,14 +106,10 @@ export async function runDebugBrainInit(
spaceName: brain.brainbase.spaceName,
description,
baseSystemPrompt,
extractedFacts: extractedFacts ?? [],
storedFacts,
storageMode,
elapsedMs,
};
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
spinner.fail("Brain initialization failed");
const elapsedMs = Date.now() - startTime;
return { ok: false, error: reason, elapsedMs };
} finally {
try {
await unlink(braindbPath);
@@ -136,37 +120,35 @@ export async function runDebugBrainInit(
export function addBrainSubcommand(parent: Command): Command {
const cmd = parent
.command("brain")
.description(
"Debug tools for brain lifecycle (no real disk writes)",
);
.description("Debug tools for brain lifecycle (no real disk writes)");
cmd
.command("init")
.description(
"Initialize a new brain with LLM (in-memory DB, temp braindb; nothing persisted)",
"Initialize a new brain with LLM (temp braindb; nothing persisted to repo)",
)
.requiredOption("-n, --name <text>", "Display name for the new brain")
.requiredOption(
"-s, --seed <text>",
"Seed text used to generate the persona biography",
)
.action(async (opts: { name: string; seed: string }) => {
const result = await runDebugBrainInit({
displayName: opts.name,
seed: opts.seed,
});
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
});
.option(
"--no-supermemory",
"Use an in-memory stub instead of the real supermemory API (no network, no API key required)",
)
.action(
async (opts: { name: string; seed: string; supermemory: boolean }) => {
const result = await runDebugBrainInit({
displayName: opts.name,
seed: opts.seed,
noSupermemory: opts.supermemory === false,
});
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
},
);
return cmd;
}
function printSection(title: string): void {
const line = "─".repeat(Math.max(40, title.length + 4));
console.log(`\n┌${line}`);
console.log(`${title}`);
console.log(`${line}`);
}

View File

@@ -0,0 +1,63 @@
import chalk from "chalk";
import ora, { type Ora } from "ora";
export function printSection(title: string): void {
const line = "─".repeat(Math.max(40, title.length + 4));
console.log(`\n┌${line}`);
console.log(`${title}`);
console.log(`${line}`);
}
export function printKeyValue(pairs: Record<string, string>): void {
const labelWidth = Math.max(...Object.keys(pairs).map((k) => k.length));
for (const [key, value] of Object.entries(pairs)) {
console.log(` ${key.padEnd(labelWidth)} ${value}`);
}
}
export class StepDriver {
private readonly stepCount: number;
private stepIndex = 0;
private current: Ora | null = null;
private currentLabel = "";
constructor(stepCount: number) {
this.stepCount = stepCount;
}
start(label: string): void {
this.stepIndex += 1;
this.resolvePrevious();
this.currentLabel = label;
const text = `Step ${this.stepIndex}/${this.stepCount}: ${label}`;
this.current = ora(text).start();
}
done(summary: string): void {
if (!this.current) return;
const text = this.current.text;
this.current.succeed(`${text}${summary}`);
this.current = null;
}
fail(reason: string): void {
if (!this.current) {
console.log(`${chalk.red("✖")} ${this.currentLabel}${reason}`);
return;
}
this.current.fail(`${this.current.text}${reason}`);
this.current = null;
}
private resolvePrevious(): void {
if (this.current) {
this.current.stop();
this.current = null;
}
}
}
export function snippet(text: string): string {
const flat = text.replace(/\s+/g, " ").trim();
return flat.length > 80 ? `${flat.slice(0, 77)}...` : flat;
}

View File

@@ -101,6 +101,7 @@ describe("runDebugScheduleDaily", () => {
const result = await runDebugScheduleDaily({
message: "focus on writing",
personality: "test-personality-XYZ",
noSupermemory: true,
});
expect(result.ok).toBe(true);
@@ -131,6 +132,7 @@ describe("runDebugScheduleDaily", () => {
const result = await runDebugScheduleDaily({
message: "",
personality: "p",
noSupermemory: true,
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected !ok");
@@ -145,6 +147,7 @@ describe("runDebugScheduleMonthly", () => {
const result = await runDebugScheduleMonthly({
message: "study for GRE",
personality: "test-personality-ABC",
noSupermemory: true,
});
expect(result.ok).toBe(true);
@@ -169,6 +172,7 @@ describe("runDebugScheduleMonthly", () => {
const result = await runDebugScheduleMonthly({
message: "",
personality: "p",
noSupermemory: true,
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected !ok");
@@ -186,7 +190,7 @@ describe("debug schedule no-disk invariant", () => {
const beforeDb = existsSync(resolve(process.cwd(), "brainbox.db"));
const beforeJson = existsSync(resolve(process.cwd(), "brainbox.json"));
await runDebugScheduleDaily({ message: "m", personality: "p" });
await runDebugScheduleDaily({ message: "m", personality: "p", noSupermemory: true });
const afterDb = existsSync(resolve(process.cwd(), "brainbox.db"));
const afterJson = existsSync(resolve(process.cwd(), "brainbox.json"));

View File

@@ -1,6 +1,10 @@
import type { Command } from "commander";
import ora from "ora";
import { Brain } from "@/brain";
import {
Brain,
runCreateDailyScheduleSteps,
runCreateMonthlyScheduleSteps,
} from "@/brain";
import { MemoryStub } from "@/brain/stub";
import {
type AvailabilityWindows,
type DailySchedule,
@@ -9,10 +13,16 @@ import {
import { formatDuration } from "@/utils/duration";
import { logger } from "@/utils/logger";
import { formatDateKey, nextMonth, pad2 } from "@/brain/schedule";
import {
StepDriver,
printKeyValue,
printSection,
} from "./output";
export interface ScheduleOptions {
message: string;
personality: string;
noSupermemory: boolean;
}
export type DailyRunResult =
@@ -23,6 +33,7 @@ export type DailyRunResult =
tomorrow: Date;
schedule: DailySchedule;
availability: AvailabilityWindows;
storageMode: "supermemory" | "stub";
elapsedMs: number;
}
| { ok: false; error: string; elapsedMs: number };
@@ -34,6 +45,7 @@ export type MonthlyRunResult =
monthKey: string;
daysInMonth: number;
schedule: MonthlySchedule;
storageMode: "supermemory" | "stub";
elapsedMs: number;
}
| { ok: false; error: string; elapsedMs: number };
@@ -49,15 +61,23 @@ export async function runDebugScheduleDaily(
today.getDate() + 1,
);
const dateKey = formatDateKey(tomorrow);
const storageMode = opts.noSupermemory ? "stub" : "supermemory";
const db = opts.noSupermemory ? new MemoryStub() : undefined;
const brain = await Brain.createDebug({ personality: opts.personality });
const brain = await Brain.createDebug(
{ personality: opts.personality },
db,
);
const scheduleSpinner = ora(
`Generating daily schedule for ${dateKey}...`,
).start();
const schedule = await brain.createDailySchedule(today, opts.message);
const steps = new StepDriver(4);
const schedule = await runCreateDailyScheduleSteps(
brain,
today,
opts.message,
steps,
);
if (!schedule) {
scheduleSpinner.fail("Daily schedule generation failed");
const elapsedMs = Date.now() - startTime;
return {
ok: false,
@@ -65,19 +85,11 @@ export async function runDebugScheduleDaily(
elapsedMs,
};
}
scheduleSpinner.succeed(
`Daily schedule generated (${schedule.items.length} slots)`,
);
printSection(
`Daily Schedule — ${dateKey} (${tomorrow.toLocaleDateString("en-US", { weekday: "long" })})`,
);
console.log(JSON.stringify(schedule, null, 2));
const availSpinner = ora("Deriving availability...").start();
steps.start("deriving availability (SCHEDULE_AVAILABILITY)");
const availability = await brain.deriveAvailabilityFromSchedule(schedule);
if (!availability) {
availSpinner.fail("Availability derivation failed");
steps.fail("see error above");
const elapsedMs = Date.now() - startTime;
return {
ok: false,
@@ -85,16 +97,29 @@ export async function runDebugScheduleDaily(
elapsedMs,
};
}
availSpinner.succeed(
`Availability derived (${availability.items.length} windows)`,
);
steps.done(`${availability.items.length} windows`);
printSection(`Availability — ${dateKey}`);
console.log();
printSection(`Schedule — daily (${dateKey})`);
printKeyValue({
dateKey,
weekday: tomorrow.toLocaleDateString("en-US", { weekday: "long" }),
storage: storageMode,
slots: String(schedule.items.length),
});
console.log();
printSection(`Step 1/2 output — Daily Schedule (DAILY_SCHEDULE)`);
console.log(JSON.stringify(schedule, null, 2));
console.log();
printSection(`Step 2/2 output — Availability (SCHEDULE_AVAILABILITY)`);
console.log(JSON.stringify(availability, null, 2));
console.log();
const elapsedMs = Date.now() - startTime;
logger.info(
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to disk.`,
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to real disk.`,
);
return {
@@ -104,6 +129,7 @@ export async function runDebugScheduleDaily(
tomorrow,
schedule,
availability,
storageMode,
elapsedMs,
};
}
@@ -115,15 +141,23 @@ export async function runDebugScheduleMonthly(
const today = new Date();
const next = nextMonth(today);
const monthKey = `${next.year}-${pad2(next.month + 1)}`;
const storageMode = opts.noSupermemory ? "stub" : "supermemory";
const db = opts.noSupermemory ? new MemoryStub() : undefined;
const brain = await Brain.createDebug({ personality: opts.personality });
const brain = await Brain.createDebug(
{ personality: opts.personality },
db,
);
const scheduleSpinner = ora(
`Generating monthly schedule for ${monthKey} (${next.daysInMonth} days)...`,
).start();
const schedule = await brain.createMonthlySchedule(today, opts.message);
const steps = new StepDriver(3);
const schedule = await runCreateMonthlyScheduleSteps(
brain,
today,
opts.message,
steps,
);
if (!schedule) {
scheduleSpinner.fail("Monthly schedule generation failed");
const elapsedMs = Date.now() - startTime;
return {
ok: false,
@@ -131,16 +165,24 @@ export async function runDebugScheduleMonthly(
elapsedMs,
};
}
scheduleSpinner.succeed(
`Monthly schedule generated (${schedule.items.length} day summaries)`,
);
printSection(`Monthly Schedule — ${monthKey} (${next.daysInMonth} days)`);
console.log();
printSection(`Schedule — monthly (${monthKey})`);
printKeyValue({
monthKey,
daysInMonth: String(next.daysInMonth),
storage: storageMode,
summaries: String(schedule.items.length),
});
console.log();
printSection(`Step 1/1 output — Monthly Schedule (MONTHLY_SCHEDULE)`);
console.log(JSON.stringify(schedule, null, 2));
console.log();
const elapsedMs = Date.now() - startTime;
logger.info(
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to disk. (Availability applies per-day and is not generated for the monthly view.)`,
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to real disk. (Availability applies per-day and is not generated for the monthly view.)`,
);
return {
@@ -149,6 +191,7 @@ export async function runDebugScheduleMonthly(
monthKey,
daysInMonth: next.daysInMonth,
schedule,
storageMode,
elapsedMs,
};
}
@@ -158,38 +201,53 @@ export function addScheduleSubcommand(parent: Command): Command {
.command("schedule")
.description("Generate a test schedule (no disk writes)");
cmd.command("daily")
cmd
.command("daily")
.description(
"Generate a daily schedule for tomorrow and print schedule + availability",
)
.requiredOption("-m, --message <text>", "User direction for the schedule")
.requiredOption("-p, --personality <text>", "Brain personality to use")
.action(async (opts: ScheduleOptions) => {
const result = await runDebugScheduleDaily(opts);
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
});
.option(
"--no-supermemory",
"Use an in-memory stub instead of the real supermemory API (no network, no API key required)",
)
.action(
async (opts: { message: string; personality: string; supermemory: boolean }) => {
const result = await runDebugScheduleDaily({
message: opts.message,
personality: opts.personality,
noSupermemory: opts.supermemory === false,
});
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
},
);
cmd.command("monthly")
cmd
.command("monthly")
.description("Generate a monthly schedule for next month and print it")
.requiredOption("-m, --message <text>", "User direction for the schedule")
.requiredOption("-p, --personality <text>", "Brain personality to use")
.action(async (opts: ScheduleOptions) => {
const result = await runDebugScheduleMonthly(opts);
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
});
.option(
"--no-supermemory",
"Use an in-memory stub instead of the real supermemory API (no network, no API key required)",
)
.action(
async (opts: { message: string; personality: string; supermemory: boolean }) => {
const result = await runDebugScheduleMonthly({
message: opts.message,
personality: opts.personality,
noSupermemory: opts.supermemory === false,
});
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
},
);
return cmd;
}
function printSection(title: string): void {
const line = "─".repeat(Math.max(40, title.length + 4));
console.log(`\n┌${line}`);
console.log(`${title}`);
console.log(`${line}`);
}