feat: replace identitydb with supermemory
This commit is contained in:
@@ -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`.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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}┘`);
|
||||
}
|
||||
|
||||
63
src/commands/debug/output.ts
Normal file
63
src/commands/debug/output.ts
Normal 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;
|
||||
}
|
||||
@@ -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"));
|
||||
|
||||
@@ -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}┘`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user