From e0189fb0021fe03b6480ea4a04d010fb2930ad38 Mon Sep 17 00:00:00 2001 From: p-sw Date: Thu, 25 Jun 2026 22:43:33 +0900 Subject: [PATCH] refactor: remove test/debug --- bun.lock | 38 - package.json | 3 - src/brain/index.test.ts | 1169 --------------------------- src/brain/index.ts | 517 +++++------- src/brain/schedule.test.ts | 147 ---- src/brain/stub.ts | 148 ---- src/commands/debug/brain.test.ts | 189 ----- src/commands/debug/brain.ts | 154 ---- src/commands/debug/index.ts | 16 - src/commands/debug/output.ts | 63 -- src/commands/debug/schedule.test.ts | 204 ----- src/commands/debug/schedule.ts | 253 ------ src/commands/index.test.ts | 50 -- src/index.ts | 2 - src/utils/duration.ts | 15 - tsconfig.json | 1 - 16 files changed, 201 insertions(+), 2768 deletions(-) delete mode 100644 src/brain/index.test.ts delete mode 100644 src/brain/schedule.test.ts delete mode 100644 src/brain/stub.ts delete mode 100644 src/commands/debug/brain.test.ts delete mode 100644 src/commands/debug/brain.ts delete mode 100644 src/commands/debug/index.ts delete mode 100644 src/commands/debug/output.ts delete mode 100644 src/commands/debug/schedule.test.ts delete mode 100644 src/commands/debug/schedule.ts delete mode 100644 src/commands/index.test.ts delete mode 100644 src/utils/duration.ts diff --git a/bun.lock b/bun.lock index c9a9bf9..6198775 100644 --- a/bun.lock +++ b/bun.lock @@ -9,12 +9,10 @@ "chalk": "^5.6.2", "commander": "^15.0.0", "dotenv": "^17.4.2", - "ora": "^9.4.0", "prettier": "^3.8.3", "supermemory": "^4.24.12", }, "devDependencies": { - "@types/bun": "latest", "@types/node": "^25.9.1", }, "peerDependencies": { @@ -25,58 +23,22 @@ "packages": { "@openrouter/sdk": ["@openrouter/sdk@0.12.79", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-0ZpwtnuHh3/B1piW9kHCUIQy6PAsaK/vjFdZuHxmCdAenCyUNsLA2mFpmfHNWRNb+bOO3yBc4IALa264UyzmBA=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - - "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], - "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], - - "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], - - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - - "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], - - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - - "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - - "ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="], - "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="], - - "string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], - - "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "supermemory": ["supermemory@4.24.12", "", { "bin": { "supermemory": "bin/cli" } }, "sha512-xAFextuqk4JuoW33jJaFGqT1oMppN2IgfWUrV18Fv3qAAZ6M1SR1tb+7EBq8vrEQIx4iY2MQh5p+qnfL6lI8Yw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], - "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], } } diff --git a/package.json b/package.json index 710cc05..1470fb2 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,9 @@ }, "scripts": { "cli": "bun run src/index.ts", - "test": "bun test", "format": "bun run prettier 'src/**/*.ts' -w" }, "devDependencies": { - "@types/bun": "latest", "@types/node": "^25.9.1" }, "peerDependencies": { @@ -23,7 +21,6 @@ "chalk": "^5.6.2", "commander": "^15.0.0", "dotenv": "^17.4.2", - "ora": "^9.4.0", "prettier": "^3.8.3", "supermemory": "^4.24.12" } diff --git a/src/brain/index.test.ts b/src/brain/index.test.ts deleted file mode 100644 index 7486b64..0000000 --- a/src/brain/index.test.ts +++ /dev/null @@ -1,1169 +0,0 @@ -import { - afterAll, - beforeAll, - beforeEach, - describe, - expect, - mock, - test, -} from "bun:test"; -import { randomUUID } from "node:crypto"; -import type { Space } from "./types"; - -const llmCalls: Array<{ model: unknown; options: any }> = []; -let customMonthlyDays: Array<{ day: number; summary: string }> | null = null; -let customDailySlots: Array<{ - start: string; - end: string; - activity: string; - notes: string; -}> | null = null; -let customAvailability: Array<{ - start: string; - end: string; - status: string; -}> | null = null; - -type ToolCallResponse = { - id: string; - name: string; - arguments: string; -}; -type LLMChatResponse = - | { kind: "text"; text: string } - | { kind: "tool_calls"; tool_calls: ToolCallResponse[] }; -let chatResponses: LLMChatResponse[] = []; - -function build48Slots(): Array<{ - start: string; - end: string; - activity: string; - notes: string; -}> { - const slots: Array<{ - start: string; - end: string; - activity: string; - notes: string; - }> = []; - for (let i = 0; i < 48; i++) { - const startHour = Math.floor(i / 2); - const startMin = (i % 2) * 30; - const start = `${String(startHour).padStart(2, "0")}:${String(startMin).padStart(2, "0")}`; - let end: string; - if (i === 47) { - end = "24:00"; - } else { - const endHour = Math.floor((i + 1) / 2); - const endMin = ((i + 1) % 2) * 30; - end = `${String(endHour).padStart(2, "0")}:${String(endMin).padStart(2, "0")}`; - } - slots.push({ start, end, activity: `slot-${i}`, notes: "" }); - } - return slots; -} - -function build30Days(): Array<{ day: number; summary: string }> { - return Array.from({ length: 30 }, (_, i) => ({ - day: i + 1, - summary: `Day ${i + 1} summary`, - })); -} - -function buildAvailability(): Array<{ - start: string; - end: string; - status: string; -}> { - return [ - { start: "00:00", end: "07:00", status: "offline" }, - { start: "07:00", end: "09:00", status: "online" }, - { start: "09:00", end: "17:00", status: "do-not-disturb" }, - { start: "17:00", end: "23:30", status: "online" }, - { start: "23:30", end: "24:00", status: "offline" }, - ]; -} - -const mockCall = mock(async (model: unknown, options: any): Promise => { - llmCalls.push({ model, options }); - if (options.jsonSchemaName === "daily-schedule") { - return { items: customDailySlots ?? build48Slots() } as unknown as T; - } - if (options.jsonSchemaName === "monthly-schedule") { - if (customMonthlyDays) { - return { items: customMonthlyDays } as unknown as T; - } - const match = options.message.match(/\((\d+) days\)/); - const days = match ? parseInt(match[1]!, 10) : 30; - return { - items: Array.from({ length: days }, (_, i) => ({ - day: i + 1, - summary: `Day ${i + 1} summary`, - })), - } as unknown as T; - } - if (options.jsonSchemaName === "availability") { - return { items: customAvailability ?? buildAvailability() } as unknown as T; - } - if (Array.isArray(options.tools)) { - const next = chatResponses.shift(); - if (!next) { - throw new Error("mockCall: no chatResponses queued for tool-using call"); - } - if (next.kind === "text") { - return { - finish_reason: "stop", - index: 0, - message: { role: "assistant", content: next.text }, - } as unknown as T; - } - return { - finish_reason: "tool_calls", - index: 0, - message: { - role: "assistant", - content: null, - toolCalls: next.tool_calls.map((tc) => ({ - id: tc.id, - type: "function", - function: { name: tc.name, arguments: tc.arguments }, - })), - }, - } as unknown as T; - } - if (typeof options.message === "string" || options.message === undefined) { - return "test-description" as unknown as T; - } - throw new Error(`unexpected jsonSchemaName: ${options.jsonSchemaName}`); -}); - -mock.module("@/openrouter", () => ({ - llm: { - models: { conversation: "test-conv", identity: "test-id" }, - call: mockCall, - chatWithTools: mockCall, - }, -})); - -mock.module("@/config", () => ({ - config: { - openrouterApiKey: "test-key", - supermemoryApiKey: "test-supermemory-key", - braindbPath: "/tmp/brainbox-test-braindb.json", - }, -})); - -interface StoredDoc { - id: string; - customId: string | null; - containerTag: string; - content: string; - summary: string | null; - metadata: Record | null; -} - -class MockSupermemory { - docs = new Map(); - private nextId = 0; - documentsAddCalls = 0; - - constructor(_options: { apiKey: string }) {} - - documents = { - add: async (params: { - content: string; - containerTag: string; - customId?: string; - metadata?: Record; - }) => { - this.documentsAddCalls += 1; - const id = `mock-${++this.nextId}`; - const stored: StoredDoc = { - id, - customId: params.customId ?? null, - containerTag: params.containerTag, - content: params.content, - summary: null, - metadata: params.metadata ?? null, - }; - this.docs.set(id, stored); - return { id, status: "done" }; - }, - list: async (params: { - containerTags?: Array; - limit?: number; - }) => { - const tags = params.containerTags ?? []; - const limit = params.limit ?? 200; - const all = Array.from(this.docs.values()).filter((d) => - tags.length === 0 ? true : tags.includes(d.containerTag), - ); - const memories = all.slice(0, limit).map((d) => ({ - id: d.id, - customId: d.customId, - containerTag: d.containerTag, - summary: d.summary, - metadata: d.metadata as - | string - | number - | boolean - | Record - | Array - | null, - content: d.content, - createdAt: "2026-01-01T00:00:00Z", - updatedAt: "2026-01-01T00:00:00Z", - status: "done" as const, - type: "text" as const, - connectionId: null, - filepath: null, - title: null, - })); - return { - memories, - pagination: { - currentPage: 1, - totalItems: memories.length, - totalPages: 1, - limit, - }, - }; - }, - get: async (id: string) => { - const d = this.docs.get(id); - if (!d) { - throw new Error(`MockSupermemory.documents.get: no such id ${id}`); - } - return { - id: d.id, - customId: d.customId, - containerTag: d.containerTag, - content: d.content, - summary: d.summary, - metadata: d.metadata as - | string - | number - | boolean - | Record - | Array - | null, - createdAt: "2026-01-01T00:00:00Z", - updatedAt: "2026-01-01T00:00:00Z", - status: "done" as const, - type: "text" as const, - connectionId: null, - filepath: null, - title: null, - source: null, - ogImage: null, - raw: null, - spatialPoint: null, - taskType: "memory" as const, - url: null, - }; - }, - }; - - search = { - execute: async (params: { - q: string; - containerTag?: string; - limit?: number; - onlyMatchingChunks?: boolean; - }) => { - const q = params.q.toLowerCase(); - const limit = params.limit ?? 5; - const hits = Array.from(this.docs.values()) - .filter( - (d) => - (params.containerTag - ? d.containerTag === params.containerTag - : true) && d.content.toLowerCase().includes(q), - ) - .slice(0, limit) - .map((d, i) => ({ - chunks: [ - { - content: d.content, - isRelevant: true, - score: 1 - i * 0.1, - }, - ], - summary: d.summary, - score: 1 - i * 0.1, - documentId: d.id, - metadata: (d.metadata as Record) ?? null, - createdAt: "2026-01-01T00:00:00Z", - updatedAt: "2026-01-01T00:00:00Z", - title: d.customId, - type: "text" as const, - })); - return { - results: hits, - total: hits.length, - timing: 0, - }; - }, - }; - - findByCustomId(customId: string): StoredDoc | undefined { - for (const d of this.docs.values()) { - if (d.customId === customId) return d; - } - return undefined; - } - - reset(): void { - this.docs.clear(); - this.nextId = 0; - this.documentsAddCalls = 0; - } -} - -/** - * Replace the real `supermemory` SDK with our in-memory mock. The - * static factories `Brain.create` / `Brain.createDebug` / `Brain.load` - * all do `new Supermemory({ apiKey })` internally; this mock is what - * they pick up. - */ -mock.module("supermemory", () => ({ - default: MockSupermemory, -})); - -const { Brain } = await import("./index"); -const { brainManager } = await import("./manager"); -const { formatDateKey, nextMonth } = await import("./schedule"); -type BrainItem = import("./manager").BrainItem; - -beforeAll(async () => { - try { - await brainManager.deleteBrain("smoke-test-id"); - } catch {} -}); - -afterAll(async () => {}); - -async function makeBrain(): Promise> { - const db = new MockSupermemory({ apiKey: "test-supermemory-key" }); - const spaceName = `test-space-${randomUUID()}`; - const space: Space = { name: spaceName, description: "Test Brain space" }; - const brainbase: BrainItem = { - brainId: randomUUID(), - spaceName, - displayName: "Test Brain", - baseSystemPrompt: - "Test personality: night owl, introverted, studies at midnight.", - }; - return new Brain(db as never, space, brainbase, false); -} - -beforeEach(() => { - llmCalls.length = 0; - customMonthlyDays = null; - customDailySlots = null; - customAvailability = null; - chatResponses = []; -}); - -describe("Brain.createDailySchedule", () => { - test("S1: returns 48 slots in 30-min intervals and persists a document", async () => { - const brain = await makeBrain(); - const db = brain.db as unknown as MockSupermemory; - const today = new Date(2026, 5, 5); - const expectedTomorrow = (await import("./schedule")).nextDay(today); - const expectedKey = formatDateKey(expectedTomorrow); - - const result = await brain.createDailySchedule(today, "focus on writing"); - - expect(result).not.toBeNull(); - expect(result!.items).toHaveLength(48); - expect(result!.items[0]).toEqual({ - start: "00:00", - end: "00:30", - activity: "slot-0", - notes: "", - }); - expect(result!.items[47]).toEqual({ - start: "23:30", - end: "24:00", - activity: "slot-47", - notes: "", - }); - - const llmCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "daily-schedule", - ); - expect(llmCall).toBeDefined(); - expect(llmCall!.options.message).toContain(expectedKey); - expect(llmCall!.options.message).toContain("focus on writing"); - expect(llmCall!.options.message).toContain("Test personality"); - - const stored = db.findByCustomId(`daily-schedule:${expectedKey}`); - expect(stored).toBeDefined(); - expect(stored!.containerTag).toBe(brain.space.name); - expect(JSON.parse(stored!.content).items).toHaveLength(48); - }); - - test("S4: month wrap (June 30 -> July 1)", async () => { - const brain = await makeBrain(); - const db = brain.db as unknown as MockSupermemory; - const today = new Date(2026, 5, 30); - const expectedKey = formatDateKey(new Date(2026, 6, 1)); - - await brain.createDailySchedule(today, ""); - - const stored = db.findByCustomId(`daily-schedule:${expectedKey}`); - expect(stored).toBeDefined(); - }); - - test("S4b: year wrap (December 31 -> January 1 next year)", async () => { - const brain = await makeBrain(); - const db = brain.db as unknown as MockSupermemory; - const today = new Date(2026, 11, 31); - const expectedKey = "2027-01-01"; - - await brain.createDailySchedule(today, ""); - - const stored = db.findByCustomId(`daily-schedule:${expectedKey}`); - expect(stored).toBeDefined(); - }); - - test("S6: consumes monthly summary for the target day when present", async () => { - const brain = await makeBrain(); - const db = brain.db as unknown as MockSupermemory; - - customMonthlyDays = Array.from({ length: 30 }, (_, i) => ({ - day: i + 1, - summary: - i + 1 === 10 ? "UNIQUE_SUMMARY_FOR_DAY_10" : `Day ${i + 1} summary`, - })); - - const todayForMonthly = new Date(2026, 4, 15); - await brain.createMonthlySchedule(todayForMonthly, ""); - - const monthlyStored = db.findByCustomId("monthly-schedule:2026-06"); - expect(monthlyStored).toBeDefined(); - - llmCalls.length = 0; - customDailySlots = build48Slots(); - - const todayForDaily = new Date(2026, 5, 9); - await brain.createDailySchedule(todayForDaily, ""); - - const dailyLlmCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "daily-schedule", - ); - expect(dailyLlmCall).toBeDefined(); - expect(dailyLlmCall!.options.message).toContain( - "UNIQUE_SUMMARY_FOR_DAY_10", - ); - }); - - test("S9: injects 2-days-ago schedule as recent context when one exists", async () => { - const brain = await makeBrain(); - const db = brain.db as unknown as MockSupermemory; - - const twoDaysAgoTarget = new Date(2026, 5, 7); - const twoDaysAgoTomorrow = (await import("./schedule")).nextDay( - twoDaysAgoTarget, - ); - const twoDaysAgoKey = formatDateKey(twoDaysAgoTomorrow); - - await brain.add({ - customId: `daily-schedule:${twoDaysAgoKey}`, - content: JSON.stringify({ - items: Array.from({ length: 48 }, (_, i) => ({ - start: `${String(Math.floor(i / 2)).padStart(2, "0")}:${String((i % 2) * 30).padStart(2, "0")}`, - end: `${String(Math.floor((i + 1) / 2)).padStart(2, "0")}:${String(((i + 1) % 2) * 30).padStart(2, "0")}`, - activity: `prior-day-activity-${i}`, - notes: "", - })), - }), - metadata: { kind: "schedule", source: "createDailySchedule", date: twoDaysAgoKey }, - }); - - llmCalls.length = 0; - const today = new Date(2026, 5, 9); - await brain.createDailySchedule(today, ""); - - const dailyLlmCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "daily-schedule", - ); - expect(dailyLlmCall).toBeDefined(); - expect(dailyLlmCall!.options.message).toContain( - `Recent schedule (${twoDaysAgoKey}, 2 days ago):`, - ); - expect(dailyLlmCall!.options.message).toContain("prior-day-activity-0"); - }); - - test("S10: 2-days-ago context says 'no schedule on file' when prior day is missing", async () => { - const brain = await makeBrain(); - const today = new Date(2026, 5, 9); - await brain.createDailySchedule(today, ""); - - const dailyLlmCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "daily-schedule", - ); - expect(dailyLlmCall).toBeDefined(); - expect(dailyLlmCall!.options.message).toContain( - "(no schedule on file for 2 days ago)", - ); - }); -}); - -describe("Brain.createMonthlySchedule", () => { - test("S2: returns N day summaries (N = days in next month) and persists a document", async () => { - const brain = await makeBrain(); - const db = brain.db as unknown as MockSupermemory; - const today = new Date(2026, 0, 15); - const expected = nextMonth(today); - const expectedKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`; - - const result = await brain.createMonthlySchedule(today, "study for GRE"); - - expect(result).not.toBeNull(); - expect(result!.items).toHaveLength(expected.daysInMonth); - expect(result!.items[0]!.day).toBe(1); - expect(result!.items[result!.items.length - 1]!.day).toBe( - expected.daysInMonth, - ); - - const llmCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "monthly-schedule", - ); - expect(llmCall).toBeDefined(); - expect(llmCall!.options.message).toContain("study for GRE"); - expect(llmCall!.options.message).toContain("Test personality"); - - const stored = db.findByCustomId(`monthly-schedule:${expectedKey}`); - expect(stored).toBeDefined(); - expect(JSON.parse(stored!.content).items).toHaveLength( - expected.daysInMonth, - ); - }); - - test("S5: year wrap (December 15 -> January next year)", async () => { - const brain = await makeBrain(); - const db = brain.db as unknown as MockSupermemory; - const today = new Date(2026, 11, 15); - const expectedKey = "2027-01"; - - const result = await brain.createMonthlySchedule(today, ""); - - expect(result).not.toBeNull(); - expect(result!.items).toHaveLength(31); - - const stored = db.findByCustomId(`monthly-schedule:${expectedKey}`); - expect(stored).toBeDefined(); - }); -}); - -describe("Brain.createDailySchedule early return", () => { - test("returns existing schedule without calling LLM when daily-schedule for tomorrow already exists", async () => { - const brain = await makeBrain(); - const today = new Date(2026, 5, 9); - const tomorrow = (await import("./schedule")).nextDay(today); - const tomorrowKey = formatDateKey(tomorrow); - const preseeded = { - items: [ - { - start: "06:00", - end: "07:00", - activity: "preserved-morning", - notes: "n/a", - }, - { - start: "22:00", - end: "23:00", - activity: "preserved-evening", - notes: "n/a", - }, - ], - }; - await brain.add({ - customId: `daily-schedule:${tomorrowKey}`, - content: JSON.stringify(preseeded), - metadata: { kind: "schedule", source: "test-seed", date: tomorrowKey }, - }); - - llmCalls.length = 0; - const result = await brain.createDailySchedule(today, "ignored user message"); - - expect(result).toEqual(preseeded); - const dailyLlmCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "daily-schedule", - ); - expect(dailyLlmCall).toBeUndefined(); - }); - - test("falls through to generation when stored content is malformed", async () => { - const brain = await makeBrain(); - const db = brain.db as unknown as MockSupermemory; - const today = new Date(2026, 5, 9); - const tomorrow = (await import("./schedule")).nextDay(today); - const tomorrowKey = formatDateKey(tomorrow); - await brain.add({ - customId: `daily-schedule:${tomorrowKey}`, - content: "{not valid json", - metadata: { kind: "schedule", source: "test-seed", date: tomorrowKey }, - }); - - const result = await brain.createDailySchedule(today, ""); - - expect(result).not.toBeNull(); - expect(result!.items).toHaveLength(48); - const dailyLlmCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "daily-schedule", - ); - expect(dailyLlmCall).toBeDefined(); - expect(db.findByCustomId(`daily-schedule:${tomorrowKey}`)).toBeDefined(); - }); -}); - -describe("Brain.createMonthlySchedule early return", () => { - test("returns existing schedule without calling LLM when monthly-schedule for next month already exists", async () => { - const brain = await makeBrain(); - const today = new Date(2026, 0, 15); - const next = nextMonth(today); - const monthKey = `${next.year}-${String(next.month + 1).padStart(2, "0")}`; - const preseeded = { - items: [ - { day: 1, summary: "preserved-day-1" }, - { day: 2, summary: "preserved-day-2" }, - { day: 3, summary: "preserved-day-3" }, - ], - }; - await brain.add({ - customId: `monthly-schedule:${monthKey}`, - content: JSON.stringify(preseeded), - metadata: { kind: "schedule", source: "test-seed", month: monthKey }, - }); - - llmCalls.length = 0; - const result = await brain.createMonthlySchedule(today, "ignored user message"); - - expect(result).toEqual(preseeded); - const monthlyLlmCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "monthly-schedule", - ); - expect(monthlyLlmCall).toBeUndefined(); - }); - - test("falls through to generation when stored content is malformed", async () => { - const brain = await makeBrain(); - const today = new Date(2026, 0, 15); - const next = nextMonth(today); - const monthKey = `${next.year}-${String(next.month + 1).padStart(2, "0")}`; - await brain.add({ - customId: `monthly-schedule:${monthKey}`, - content: "{not valid json", - metadata: { kind: "schedule", source: "test-seed", month: monthKey }, - }); - - const result = await brain.createMonthlySchedule(today, ""); - - expect(result).not.toBeNull(); - const monthlyLlmCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "monthly-schedule", - ); - expect(monthlyLlmCall).toBeDefined(); - }); -}); - -describe("Brain.getTodayScheduledAvailability", () => { - test("S3: returns availability windows when today's daily schedule exists", async () => { - const brain = await makeBrain(); - const today = new Date(2026, 5, 10); - const todayKey = formatDateKey(today); - await brain.add({ - customId: `daily-schedule:${todayKey}`, - content: JSON.stringify({ items: build48Slots() }), - metadata: { - kind: "schedule", - source: "test", - date: todayKey, - }, - }); - - const result = await brain.getTodayScheduledAvailability(today); - - expect(result).not.toBeNull(); - expect(result!.items.length).toBeGreaterThan(0); - for (const w of result!.items) { - expect(["online", "do-not-disturb", "offline"]).toContain(w.status); - expect(w.start).toMatch(/^([01][0-9]|2[0-3]):[0-5][0-9]$/); - expect(w.end).toMatch(/^([01][0-9]|2[0-3]):[0-5][0-9]$|^24:00$/); - } - - const availabilityCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "availability", - ); - expect(availabilityCall).toBeDefined(); - }); - - test("returns null when no daily schedule exists for today", async () => { - const brain = await makeBrain(); - const today = new Date(2026, 5, 10); - const result = await brain.getTodayScheduledAvailability(today); - expect(result).toBeNull(); - expect(llmCalls).toHaveLength(0); - }); -}); - -describe("Brain.invalidateScheduledAvailability", () => { - test("S7a: today's cached availability is preserved after invalidateScheduledAvailability()", async () => { - const brain = await makeBrain(); - const today = new Date(2026, 5, 10); - const todayKey = formatDateKey(today); - await brain.add({ - customId: `daily-schedule:${todayKey}`, - content: JSON.stringify({ items: build48Slots() }), - metadata: { - kind: "schedule", - source: "test", - date: todayKey, - }, - }); - - const r1 = await brain.getTodayScheduledAvailability(today); - expect(r1).not.toBeNull(); - const callCountAfterFirst = llmCalls.length; - expect(callCountAfterFirst).toBe(1); - - const r2 = await brain.getTodayScheduledAvailability(today); - expect(r2).not.toBeNull(); - expect(llmCalls.length).toBe(callCountAfterFirst); - - brain.invalidateScheduledAvailability(today); - - const r3 = await brain.getTodayScheduledAvailability(today); - expect(r3).not.toBeNull(); - expect(llmCalls.length).toBe(callCountAfterFirst); - }); - - test("S7b: previous days' cached availability is removed, today/future preserved", async () => { - const brain = await makeBrain(); - const today = new Date(2026, 5, 10); - const todayKey = formatDateKey(today); - const yesterday = new Date(2026, 5, 9); - const yesterdayKey = formatDateKey(yesterday); - const tomorrow = new Date(2026, 5, 11); - const tomorrowKey = formatDateKey(tomorrow); - const slots = { items: build48Slots() }; - for (const key of [yesterdayKey, todayKey, tomorrowKey]) { - await brain.add({ - customId: `daily-schedule:${key}`, - content: JSON.stringify(slots), - metadata: { kind: "schedule", source: "test", date: key }, - }); - } - - await brain.getTodayScheduledAvailability(yesterday); - await brain.getTodayScheduledAvailability(today); - await brain.getTodayScheduledAvailability(tomorrow); - - const beforeInvalidate = llmCalls.length; - await brain.getTodayScheduledAvailability(today); - await brain.getTodayScheduledAvailability(tomorrow); - expect(llmCalls.length).toBe(beforeInvalidate); - - brain.invalidateScheduledAvailability(today); - - const afterToday = llmCalls.length; - await brain.getTodayScheduledAvailability(today); - expect(llmCalls.length).toBe(afterToday); - - await brain.getTodayScheduledAvailability(tomorrow); - expect(llmCalls.length).toBe(afterToday); - - await brain.getTodayScheduledAvailability(yesterday); - expect(llmCalls.length).toBe(afterToday + 1); - }); -}); - -describe("S8: regression on existing methods", () => { - test("Brain.create and Brain.load are still defined as static methods", () => { - expect(typeof Brain.create).toBe("function"); - expect(typeof Brain.load).toBe("function"); - }); -}); - -describe("Brain.createDebug", () => { - test("D1: returns a Brain with debug=true and the supplied personality under the brain:debug namespace", async () => { - const brain = await Brain.createDebug({ personality: "test-personality-Q" }); - - expect(brain).toBeInstanceOf(Brain); - expect(brain.debug).toBe(true); - expect(brain.brainbase.baseSystemPrompt).toBe("test-personality-Q"); - expect(brain.brainbase.displayName).toBe("Debug Brain"); - expect(brain.space.name).toBe("brain:debug"); - }); - - test("D2: createDailySchedule on a debug brain returns a schedule and persists to brain:debug", async () => { - const brain = await Brain.createDebug({ personality: "p" }); - const db = brain.db as unknown as MockSupermemory; - const today = new Date(2026, 5, 5); - const tomorrow = new Date(2026, 5, 6); - const tomorrowKey = formatDateKey(tomorrow); - - const schedule = await brain.createDailySchedule(today, "msg"); - expect(schedule).not.toBeNull(); - expect(schedule!.items).toHaveLength(48); - - const stored = db.findByCustomId(`daily-schedule:${tomorrowKey}`); - expect(stored).toBeDefined(); - expect(stored!.containerTag).toBe("brain:debug"); - }); - - test("D3: createMonthlySchedule on a debug brain returns a schedule and persists to brain:debug", async () => { - const brain = await Brain.createDebug({ personality: "p" }); - const db = brain.db as unknown as MockSupermemory; - const today = new Date(2026, 0, 15); - const expected = nextMonth(today); - const monthKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`; - - const schedule = await brain.createMonthlySchedule(today, "msg"); - expect(schedule).not.toBeNull(); - expect(schedule!.items).toHaveLength(expected.daysInMonth); - - const stored = db.findByCustomId(`monthly-schedule:${monthKey}`); - expect(stored).toBeDefined(); - expect(stored!.containerTag).toBe("brain:debug"); - }); -}); - -describe("Brain.sendMessage — translateMessageHistory helper", () => { - test("SM1: translateMessageHistory produces the documented format with persona label and timestamps", async () => { - const { translateMessageHistory } = await import("./messageHistory"); - const t1 = new Date(2026, 5, 10, 9, 30, 0); - const t2 = new Date(2026, 5, 10, 9, 31, 0); - const t3 = new Date(2026, 5, 10, 9, 32, 0); - const out = translateMessageHistory("Mika", [ - { sender: "persona", time: t1, content: "다음에 보자" }, - { sender: "user", time: t2, content: "그래" }, - { sender: "user", time: t3, content: "지금 뭐해?" }, - ]); - const lines = out.split("\n"); - expect(lines).toHaveLength(3); - expect(lines[0]!.startsWith("Mika@")).toBe(true); - expect(lines[0]!.endsWith(": 다음에 보자")).toBe(true); - expect(lines[1]!.startsWith("사용자@")).toBe(true); - expect(lines[1]!.endsWith(": 그래")).toBe(true); - expect(lines[2]!.startsWith("사용자@")).toBe(true); - expect(lines[2]!.endsWith(": 지금 뭐해?")).toBe(true); - }); - - test("SM2: translateMessageHistory returns empty string for empty history", async () => { - const { translateMessageHistory } = await import("./messageHistory"); - expect(translateMessageHistory("Mika", [])).toBe(""); - }); -}); - -describe("Brain.sendMessage — tool-calling flow", () => { - test("SM3: sendMessage returns the LLM's final text when no tools are called", async () => { - const brain = await makeBrain(); - chatResponses = [ - { - kind: "tool_calls", - tool_calls: [ - { - id: "call_r1", - name: "addReplyMessage", - arguments: JSON.stringify({ content: "안녕!" }), - }, - ], - }, - { kind: "text", text: "(end)" }, - ]; - const out = await brain.sendMessage( - [{ sender: "user", time: new Date(2026, 5, 10, 9, 0, 0), content: "안녕" }], - [], - ); - expect(out).toEqual(["안녕!"]); - const toolsCall = llmCalls.find( - (c) => Array.isArray(c.options.tools) && c.options.tools.length > 0, - ); - expect(toolsCall).toBeDefined(); - const toolNames = ( - toolsCall!.options.tools as Array<{ - function: { name: string }; - }> - ).map((t) => t.function.name); - expect(toolNames).toContain("addReplyMessage"); - expect(toolNames).toContain("searchMemory"); - }); - - test("SM4: sendMessage accumulates addReplyMessage tool calls and returns them in order", async () => { - const brain = await makeBrain(); - chatResponses = [ - { - kind: "tool_calls", - tool_calls: [ - { - id: "call_1", - name: "addReplyMessage", - arguments: JSON.stringify({ content: "어." }), - }, - ], - }, - { - kind: "tool_calls", - tool_calls: [ - { - id: "call_2", - name: "addReplyMessage", - arguments: JSON.stringify({ content: "왜불러" }), - }, - ], - }, - { kind: "text", text: "(end)" }, - ]; - const out = await brain.sendMessage( - [{ sender: "user", time: new Date(2026, 5, 10, 9, 0, 0), content: "야" }], - [], - ); - expect(out).toEqual(["어.", "왜불러"]); - }); - - test("SM5: sendMessage feeds searchMemory tool result back to the LLM", async () => { - const brain = await makeBrain(); - await brain.add({ - customId: "fact-coffee", - content: "사용자는 커피를 좋아한다", - metadata: { kind: "fact", source: "test" }, - }); - - chatResponses = [ - { - kind: "tool_calls", - tool_calls: [ - { - id: "call_s", - name: "searchMemory", - arguments: JSON.stringify({ query: "커피" }), - }, - ], - }, - { - kind: "tool_calls", - tool_calls: [ - { - id: "call_r", - name: "addReplyMessage", - arguments: JSON.stringify({ content: "커피 좋아하잖아" }), - }, - ], - }, - { kind: "text", text: "(end)" }, - ]; - const out = await brain.sendMessage( - [{ sender: "user", time: new Date(2026, 5, 10, 9, 0, 0), content: "뭐 좋아하는지 알아?" }], - [], - ); - expect(out).toEqual(["커피 좋아하잖아"]); - - const toolsCalls = llmCalls.filter( - (c) => Array.isArray(c.options.tools) && c.options.tools.length > 0, - ); - expect(toolsCalls.length).toBeGreaterThanOrEqual(2); - const messages = toolsCalls[1]!.options.messages as Array<{ - role: string; - content?: string; - toolCallId?: string; - }>; - const toolMsg = messages.find( - (m) => m.role === "tool" && m.toolCallId === "call_s", - ); - expect(toolMsg).toBeDefined(); - expect(toolMsg!.content).toContain("커피"); - }); - - test("SM6: sendMessage with empty history still works and includes translated user messages", async () => { - const brain = await makeBrain(); - chatResponses = [ - { - kind: "tool_calls", - tool_calls: [ - { - id: "call_r1", - name: "addReplyMessage", - arguments: JSON.stringify({ content: "first" }), - }, - ], - }, - { kind: "text", text: "(end)" }, - ]; - const out = await brain.sendMessage( - [], - [ - { - sender: "user", - time: new Date(2026, 5, 10, 9, 0, 0), - content: "하이", - }, - ], - ); - expect(out).toEqual(["first"]); - const toolsCall = llmCalls.find( - (c) => Array.isArray(c.options.tools) && c.options.tools.length > 0, - ); - expect(toolsCall).toBeDefined(); - const userMsg = ( - toolsCall!.options.messages as Array<{ role: string; content?: string }> - ).find((m) => m.role === "user"); - expect(userMsg!.content).toContain("사용자@"); - expect(userMsg!.content).toContain("하이"); - }); - - test("SM7: createDailySchedule persists a document reachable via brain.get", async () => { - const brain = await makeBrain(); - const today = new Date(2026, 5, 5); - const tomorrow = new Date(2026, 5, 6); - const tomorrowKey = formatDateKey(tomorrow); - - customDailySlots = build48Slots(); - await brain.createDailySchedule(today, "msg"); - - const stored = await brain.get(`daily-schedule:${tomorrowKey}`); - expect(stored).not.toBeNull(); - expect(stored!.content).toContain("slot-0"); - expect(stored!.metadata).toEqual({ - kind: "schedule", - source: "createDailySchedule", - date: tomorrowKey, - }); - }); - - test("SM8: sendMessage does not call brain.add (no documents added during chat)", async () => { - const brain = await makeBrain(); - const db = brain.db as unknown as MockSupermemory; - const before = db.documentsAddCalls; - - chatResponses = [ - { - kind: "tool_calls", - tool_calls: [ - { - id: "call_r1", - name: "addReplyMessage", - arguments: JSON.stringify({ content: "ok" }), - }, - ], - }, - { kind: "text", text: "(end)" }, - ]; - await brain.sendMessage( - [{ sender: "user", time: new Date(2026, 5, 10, 9, 0, 0), content: "hi" }], - [], - ); - expect(db.documentsAddCalls - before).toBe(0); - }); - - test("SM9: out-of-band add() facts are queryable via brain.search without backfill", async () => { - const brain = await makeBrain(); - await brain.add({ - customId: "fact-pizza", - content: "사용자는 피자를 좋아한다", - metadata: { kind: "fact", source: "test" }, - }); - - const hits = await brain.search("피자", 5); - expect(hits.length).toBeGreaterThan(0); - expect(hits[0]!.content).toContain("피자"); - }); -}); - -describe("Brain.sleepMemory", () => { - test("SJM1: persists a daily-journal document and returns the LLM text", async () => { - const brain = await makeBrain(); - const db = brain.db as unknown as MockSupermemory; - const datetime = new Date(2026, 5, 10, 23, 0, 0); - const dateKey = formatDateKey(datetime); - const history = [ - { - sender: "user" as const, - time: new Date(2026, 5, 10, 9, 0, 0), - content: "오늘 뭐 했어?", - }, - { - sender: "persona" as const, - time: new Date(2026, 5, 10, 9, 0, 30), - content: "그냥 잤어", - }, - ]; - - const result = await brain.sleepMemory(datetime, history); - - expect(result).toBe("test-description"); - const memoirCall = llmCalls.find( - (c) => - c.options.jsonSchemaName === undefined && - typeof c.options.message === "string" && - c.options.message.includes("Conversation log:"), - ); - expect(memoirCall).toBeDefined(); - expect(memoirCall!.options.message).toContain(`Date: ${dateKey}`); - expect(memoirCall!.options.message).toContain("Test personality"); - expect(memoirCall!.options.message).toContain("오늘 뭐 했어?"); - expect(memoirCall!.options.message).toContain("그냥 잤어"); - expect(memoirCall!.options.message).toContain("사용자@"); - - const stored = db.findByCustomId(`daily-journal:${dateKey}`); - expect(stored).toBeDefined(); - expect(stored!.containerTag).toBe(brain.space.name); - expect(stored!.content).toBe("test-description"); - expect(stored!.metadata).toEqual({ - kind: "daily-journal", - source: "sleepMemory", - date: dateKey, - }); - }); - - test("SJM2: empty history returns null without calling the LLM", async () => { - const brain = await makeBrain(); - const datetime = new Date(2026, 5, 10, 23, 0, 0); - - llmCalls.length = 0; - const result = await brain.sleepMemory(datetime, []); - - expect(result).toBeNull(); - expect(llmCalls).toHaveLength(0); - }); - - test("SJM3: stored daily-journal is readable via brain.get with kind=daily-journal", async () => { - const brain = await makeBrain(); - const datetime = new Date(2026, 5, 10, 23, 0, 0); - const dateKey = formatDateKey(datetime); - - await brain.sleepMemory(datetime, [ - { - sender: "user" as const, - time: new Date(2026, 5, 10, 9, 0, 0), - content: "hi", - }, - ]); - - const stored = await brain.get(`daily-journal:${dateKey}`); - expect(stored).not.toBeNull(); - expect(stored!.content).toBe("test-description"); - expect(stored!.metadata?.kind).toBe("daily-journal"); - }); - - test("SJM4: datetime defaults to today when undefined is passed", async () => { - const brain = await makeBrain(); - const db = brain.db as unknown as MockSupermemory; - const expectedKey = formatDateKey(new Date()); - - const result = await brain.sleepMemory(undefined as unknown as Date, [ - { - sender: "user" as const, - time: new Date(), - content: "ping", - }, - ]); - - expect(result).toBe("test-description"); - const stored = db.findByCustomId(`daily-journal:${expectedKey}`); - expect(stored).toBeDefined(); - }); -}); diff --git a/src/brain/index.ts b/src/brain/index.ts index 8e51320..853b171 100644 --- a/src/brain/index.ts +++ b/src/brain/index.ts @@ -20,8 +20,7 @@ import type { ChatFunctionTool, ChatMessages, } from "@openrouter/sdk/models"; -import { BrainDBManager, brainManager, type BrainItem } from "./manager"; -import { MemoryStub } from "./stub"; +import { brainManager, type BrainItem } from "./manager"; import { translateMessageHistory, type MessageHistoryEntry, @@ -35,10 +34,6 @@ import { } from "./schedule"; import type { FactInput, FactMetadata, SearchHit, Space } from "./types"; -export interface DebugOptions { - personality: string; -} - export interface BrainCreateResult { brain: Brain; description: string; @@ -49,10 +44,9 @@ export class Brain { private availabilityCache: Map = new Map(); constructor( - public db: Supermemory | MemoryStub, + public db: Supermemory, public space: Space, public brainbase: BrainItem, - public debug: boolean = false, ) {} // --------------------------------------------------------------------------- @@ -125,14 +119,154 @@ export class Brain { datetime: Date, message: string, ): Promise { - return await runCreateDailyScheduleSteps(this, datetime, message, noopRunner); + try { + const target = nextDay(datetime); + const dateKey = formatDateKey(target); + const existing = await this.get(`daily-schedule:${dateKey}`); + if (existing) { + try { + return JSON.parse(existing.content) as DailySchedule; + } catch { + // fall through to regeneration if stored content is malformed + } + } + + const twoDaysAgo = new Date(target); + twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); + const twoDaysAgoKey = formatDateKey(twoDaysAgo); + const [monthlySummary, history, twoDaysAgoStored] = await Promise.all([ + this.getMonthlySummaryForDay(target), + this.getHistoryFacts(), + this.get(`daily-schedule:${twoDaysAgoKey}`), + ]); + let twoDaysAgoSchedule: DailySchedule | null = null; + if (twoDaysAgoStored) { + try { + twoDaysAgoSchedule = JSON.parse(twoDaysAgoStored.content) as DailySchedule; + } catch { + twoDaysAgoSchedule = null; + } + } + + const instruction = await loadPrompt("DAILY_SCHEDULE"); + const promptMessage = [ + `Target date: ${dateKey} (${target.toLocaleDateString("en-US", { weekday: "long" })})`, + `Personality: ${this.brainbase.baseSystemPrompt}`, + monthlySummary + ? `Monthly summary for this day: ${monthlySummary}` + : "(no monthly summary available for this date)", + `Recent schedule (${twoDaysAgoKey}, 2 days ago): ${ + twoDaysAgoSchedule + ? twoDaysAgoSchedule.items + .map((s) => `${s.start} ${s.activity}`) + .join(", ") + : "(no schedule on file for 2 days ago)" + }`, + `Recent history (facts):`, + history, + `User direction: ${message}`, + ].join("\n\n"); + + const schedule = await llm.call(llm.models.identity, { + instruction, + message: promptMessage, + jsonSchemaName: "daily-schedule", + jsonSchema: dailyScheduleSchema, + }); + + await this.add({ + customId: `daily-schedule:${dateKey}`, + content: JSON.stringify(schedule), + metadata: { + kind: "schedule", + source: "createDailySchedule", + date: dateKey, + }, + }); + + return schedule; + } catch (error) { + let reason = + error instanceof Error + ? error.message + `(${error.name})` + : String(error); + if (error instanceof BadRequestResponseError) + reason = reason + `${error.body}`; + logger.error(`createDailySchedule failed: ${reason}`); + return null; + } } async createMonthlySchedule( datetime: Date, message: string, ): Promise { - return await runCreateMonthlyScheduleSteps(this, datetime, message, noopRunner); + try { + const next = nextMonth(datetime); + const monthKey = `${next.year}-${pad2(next.month + 1)}`; + const existing = await this.get(`monthly-schedule:${monthKey}`); + if (existing) { + try { + return JSON.parse(existing.content) as MonthlySchedule; + } catch { + // fall through to regeneration if stored content is malformed + } + } + + const twoMonthsAgo = new Date(next.year, next.month - 2, 1); + const twoMonthsAgoKey = `${twoMonthsAgo.getFullYear()}-${pad2(twoMonthsAgo.getMonth() + 1)}`; + const [history, twoMonthsAgoStored] = await Promise.all([ + this.getHistoryFacts(), + this.get(`monthly-schedule:${twoMonthsAgoKey}`), + ]); + let twoMonthsAgoSchedule: MonthlySchedule | null = null; + if (twoMonthsAgoStored) { + try { + twoMonthsAgoSchedule = JSON.parse(twoMonthsAgoStored.content) as MonthlySchedule; + } catch { + twoMonthsAgoSchedule = null; + } + } + + const instruction = await loadPrompt("MONTHLY_SCHEDULE"); + const promptMessage = [ + `Target month: ${monthKey} (${next.daysInMonth} days)`, + `Personality: ${this.brainbase.baseSystemPrompt}`, + `Recent schedule (${twoMonthsAgoKey}, 2 months ago): ${ + twoMonthsAgoSchedule + ? twoMonthsAgoSchedule.items + .map((s) => `Day ${s.day}: ${s.summary}`) + .join(", ") + : "(no schedule on file for 2 months ago)" + }`, + `Recent history (facts):`, + history, + `User direction: ${message}`, + ].join("\n\n"); + + const schedule = await llm.call(llm.models.identity, { + instruction, + message: promptMessage, + jsonSchemaName: "monthly-schedule", + jsonSchema: monthlyScheduleSchema, + }); + + await this.add({ + customId: `monthly-schedule:${monthKey}`, + content: JSON.stringify(schedule), + metadata: { + kind: "schedule", + source: "createMonthlySchedule", + month: monthKey, + }, + }); + + return schedule; + } catch (error) { + const reason = error instanceof Error ? error.message : String(error); + logger.error(`createMonthlySchedule failed: ${reason}`); + return null; + } } async sleepMemory( @@ -487,9 +621,64 @@ export class Brain { static async create( displayName: string, seed: string, - options: { braindbPath?: string; db?: Supermemory | MemoryStub } = {}, ): Promise { - return await runCreateSteps(displayName, seed, options, noopRunner); + try { + const personaInitInstruction = await loadPrompt("PERSONA_INIT"); + const description = await llm.call(llm.models.identity, { + instruction: personaInitInstruction, + message: seed, + }); + + const personaSystemInstruction = await loadPrompt( + "PERSONA_BASE_SYSTEM_PROMPT", + ); + const generatedBaseSystemPrompt = await llm.call( + llm.models.identity, + { + instruction: personaSystemInstruction, + message: description, + }, + ); + + const personaSystemFixed = await loadPrompt( + "PERSONA_BASE_SYSTEM_PROMPT_FIXED", + ); + const baseSystemPrompt = `${generatedBaseSystemPrompt}\n\n${personaSystemFixed}`; + + const db = new Supermemory({ apiKey: config.supermemoryApiKey }); + const brainId = randomUUID(); + const space: Space = { + name: `brain:${brainId}`, + description: displayName, + }; + + const brain = new Brain(db, space, { + brainId, + spaceName: space.name, + displayName, + baseSystemPrompt, + }); + + await brain.add({ + customId: "persona", + content: description, + metadata: { kind: "persona", source: "persona-init" }, + }); + + const brainbase: BrainItem = { + brainId, + spaceName: space.name, + displayName, + baseSystemPrompt, + }; + await brainManager.saveBrain(brainId, brainbase); + + return { brain, description, baseSystemPrompt }; + } catch (error) { + const reason = error instanceof Error ? error.message : String(error); + logger.error(`Failed to create brain "${displayName}": ${reason}`); + return null; + } } static async load(brainId: string): Promise { @@ -500,310 +689,6 @@ export class Brain { const space: Space = { name: brainbase.spaceName }; return new Brain(db, space, brainbase); } - - static async createDebug( - options: DebugOptions, - db?: Supermemory | MemoryStub, - ): Promise { - const client = db ?? new Supermemory({ apiKey: config.supermemoryApiKey }); - const space: Space = { name: "brain:debug", description: "Debug Brain" }; - const brainbase: BrainItem = { - brainId: "debug", - spaceName: space.name, - displayName: "Debug Brain", - baseSystemPrompt: options.personality, - }; - return new Brain(client, space, brainbase, true); - } -} - -export type ScheduleStep = - | { kind: "gather-context" } - | { - kind: "generate-schedule"; - jsonSchemaName: string; - schedule: DailySchedule | MonthlySchedule; - } - | { kind: "persist-schedule"; customId: string; contentLength: number } - | { kind: "derive-availability"; availability: AvailabilityWindows }; - -export type ScheduleProgress = (step: ScheduleStep) => void; - -const noScheduleProgress: ScheduleProgress = () => {}; - -export interface StepRunner { - start(label: string): void; - done(summary: string): void; - fail(reason: string): void; -} - -const noopRunner: StepRunner = { - start: () => {}, - done: () => {}, - fail: () => {}, -}; - -export async function runCreateDailyScheduleSteps( - brain: Brain, - datetime: Date, - message: string, - runner: StepRunner = noopRunner, -): Promise { - try { - const target = nextDay(datetime); - const dateKey = formatDateKey(target); - const existing = await brain.get(`daily-schedule:${dateKey}`); - if (existing) { - try { - const parsed = JSON.parse(existing.content) as DailySchedule; - runner.start("checking for existing schedule"); - runner.done(`existing schedule found (customId=daily-schedule:${dateKey}), skipping generation`); - return parsed; - } catch { - // fall through to regeneration if stored content is malformed - } - } - - runner.start("gathering context"); - const twoDaysAgo = new Date(target); - twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); - const twoDaysAgoKey = formatDateKey(twoDaysAgo); - const [monthlySummary, history, twoDaysAgoStored] = await Promise.all([ - brain.getMonthlySummaryForDay(target), - brain.getHistoryFacts(), - brain.get(`daily-schedule:${twoDaysAgoKey}`), - ]); - let twoDaysAgoSchedule: DailySchedule | null = null; - if (twoDaysAgoStored) { - try { - twoDaysAgoSchedule = JSON.parse(twoDaysAgoStored.content) as DailySchedule; - } catch { - twoDaysAgoSchedule = null; - } - } - runner.done(""); - - runner.start("generating schedule (daily-schedule)"); - const instruction = await loadPrompt("DAILY_SCHEDULE"); - const promptMessage = [ - `Target date: ${dateKey} (${target.toLocaleDateString("en-US", { weekday: "long" })})`, - `Personality: ${brain.brainbase.baseSystemPrompt}`, - monthlySummary - ? `Monthly summary for this day: ${monthlySummary}` - : "(no monthly summary available for this date)", - `Recent schedule (${twoDaysAgoKey}, 2 days ago): ${ - twoDaysAgoSchedule - ? twoDaysAgoSchedule.items - .map((s) => `${s.start} ${s.activity}`) - .join(", ") - : "(no schedule on file for 2 days ago)" - }`, - `Recent history (facts):`, - history, - `User direction: ${message}`, - ].join("\n\n"); - - const schedule = await llm.call(llm.models.identity, { - instruction, - message: promptMessage, - jsonSchemaName: "daily-schedule", - jsonSchema: dailyScheduleSchema, - }); - runner.done(`${schedule.items.length} items`); - - runner.start("persisting schedule"); - await brain.add({ - customId: `daily-schedule:${dateKey}`, - content: JSON.stringify(schedule), - metadata: { - kind: "schedule", - source: "createDailySchedule", - date: dateKey, - }, - }); - runner.done(`customId=daily-schedule:${dateKey}`); - - return schedule; - } catch (error) { - let reason = - error instanceof Error - ? error.message + `(${error.name})` - : String(error); - if (error instanceof BadRequestResponseError) - reason = reason + `${error.body}`; - logger.error(`createDailySchedule failed: ${reason}`); - runner.fail(reason); - return null; - } -} - -export async function runCreateMonthlyScheduleSteps( - brain: Brain, - datetime: Date, - message: string, - runner: StepRunner = noopRunner, -): Promise { - try { - const next = nextMonth(datetime); - const monthKey = `${next.year}-${pad2(next.month + 1)}`; - const existing = await brain.get(`monthly-schedule:${monthKey}`); - if (existing) { - try { - const parsed = JSON.parse(existing.content) as MonthlySchedule; - runner.start("checking for existing schedule"); - runner.done(`existing schedule found (customId=monthly-schedule:${monthKey}), skipping generation`); - return parsed; - } catch { - // fall through to regeneration if stored content is malformed - } - } - - runner.start("gathering context"); - const twoMonthsAgo = new Date(next.year, next.month - 2, 1); - const twoMonthsAgoKey = `${twoMonthsAgo.getFullYear()}-${pad2(twoMonthsAgo.getMonth() + 1)}`; - const [history, twoMonthsAgoStored] = await Promise.all([ - brain.getHistoryFacts(), - brain.get(`monthly-schedule:${twoMonthsAgoKey}`), - ]); - let twoMonthsAgoSchedule: MonthlySchedule | null = null; - if (twoMonthsAgoStored) { - try { - twoMonthsAgoSchedule = JSON.parse(twoMonthsAgoStored.content) as MonthlySchedule; - } catch { - twoMonthsAgoSchedule = null; - } - } - runner.done(""); - - runner.start("generating schedule (monthly-schedule)"); - const instruction = await loadPrompt("MONTHLY_SCHEDULE"); - const promptMessage = [ - `Target month: ${monthKey} (${next.daysInMonth} days)`, - `Personality: ${brain.brainbase.baseSystemPrompt}`, - `Recent schedule (${twoMonthsAgoKey}, 2 months ago): ${ - twoMonthsAgoSchedule - ? twoMonthsAgoSchedule.items - .map((s) => `Day ${s.day}: ${s.summary}`) - .join(", ") - : "(no schedule on file for 2 months ago)" - }`, - `Recent history (facts):`, - history, - `User direction: ${message}`, - ].join("\n\n"); - - const schedule = await llm.call(llm.models.identity, { - instruction, - message: promptMessage, - jsonSchemaName: "monthly-schedule", - jsonSchema: monthlyScheduleSchema, - }); - runner.done(`${schedule.items.length} items`); - - runner.start("persisting schedule"); - await brain.add({ - customId: `monthly-schedule:${monthKey}`, - content: JSON.stringify(schedule), - metadata: { - kind: "schedule", - source: "createMonthlySchedule", - month: monthKey, - }, - }); - runner.done(`customId=monthly-schedule:${monthKey}`); - - return schedule; - } catch (error) { - const reason = error instanceof Error ? error.message : String(error); - logger.error(`createMonthlySchedule failed: ${reason}`); - runner.fail(reason); - return null; - } -} - -export async function runCreateSteps( - displayName: string, - seed: string, - options: { braindbPath?: string; db?: Supermemory | MemoryStub } = {}, - runner: StepRunner = noopRunner, -): Promise { - const manager = options.braindbPath - ? new BrainDBManager(options.braindbPath) - : brainManager; - try { - runner.start("generating persona description (PERSONA_INIT)"); - const personaInitInstruction = await loadPrompt("PERSONA_INIT"); - const description = await llm.call(llm.models.identity, { - instruction: personaInitInstruction, - message: seed, - }); - runner.done(snippet80(description)); - - runner.start( - "generating base system prompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)", - ); - const personaSystemInstruction = await loadPrompt( - "PERSONA_BASE_SYSTEM_PROMPT", - ); - const generatedBaseSystemPrompt = await llm.call( - llm.models.identity, - { - instruction: personaSystemInstruction, - message: description, - }, - ); - - const personaSystemFixed = await loadPrompt( - "PERSONA_BASE_SYSTEM_PROMPT_FIXED", - ); - const baseSystemPrompt = `${generatedBaseSystemPrompt}\n\n${personaSystemFixed}`; - runner.done(snippet80(baseSystemPrompt)); - - const db = - options.db ?? new Supermemory({ apiKey: config.supermemoryApiKey }); - const brainId = randomUUID(); - const space: Space = { - name: `brain:${brainId}`, - description: displayName, - }; - - const brain = new Brain(db, space, { - brainId, - spaceName: space.name, - displayName, - baseSystemPrompt, - }); - - runner.start("persisting persona document"); - await brain.add({ - customId: "persona", - content: description, - metadata: { kind: "persona", source: "persona-init" }, - }); - runner.done(`customId=persona, contentLength=${description.length}`); - - runner.start("saving braindb index"); - const brainbase: BrainItem = { - brainId, - spaceName: space.name, - displayName, - baseSystemPrompt, - }; - await manager.saveBrain(brainId, brainbase); - runner.done(`brainId=${brainId}`); - - return { brain, description, baseSystemPrompt }; - } catch (error) { - const reason = error instanceof Error ? error.message : String(error); - logger.error(`Failed to create brain "${displayName}": ${reason}`); - runner.fail(reason); - return null; - } -} - -function snippet80(text: string): string { - const flat = text.replace(/\s+/g, " ").trim(); - return flat.length > 80 ? `${flat.slice(0, 77)}...` : flat; } function formatDatetime(now: Date): string { diff --git a/src/brain/schedule.test.ts b/src/brain/schedule.test.ts deleted file mode 100644 index 63bd53a..0000000 --- a/src/brain/schedule.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - formatDateKey, - formatMonthKey, - nextDay, - nextMonth, - pad2, -} from "./schedule"; - -describe("pad2", () => { - test("zero-pads single digit", () => { - expect(pad2(0)).toBe("00"); - expect(pad2(1)).toBe("01"); - expect(pad2(9)).toBe("09"); - }); - test("does not pad two digits", () => { - expect(pad2(10)).toBe("10"); - expect(pad2(31)).toBe("31"); - expect(pad2(99)).toBe("99"); - }); -}); - -describe("formatDateKey", () => { - test("formats YYYY-MM-DD with zero padding", () => { - expect(formatDateKey(new Date(2026, 0, 5))).toBe("2026-01-05"); - expect(formatDateKey(new Date(2026, 11, 31))).toBe("2026-12-31"); - expect(formatDateKey(new Date(2026, 5, 30))).toBe("2026-06-30"); - }); - test("handles months 1-9 with zero padding", () => { - expect(formatDateKey(new Date(2026, 8, 9))).toBe("2026-09-09"); - }); -}); - -describe("formatMonthKey", () => { - test("formats YYYY-MM with zero padding", () => { - expect(formatMonthKey(new Date(2026, 0, 1))).toBe("2026-01"); - expect(formatMonthKey(new Date(2026, 11, 15))).toBe("2026-12"); - }); - test("handles month 9 with zero padding", () => { - expect(formatMonthKey(new Date(2026, 8, 30))).toBe("2026-09"); - }); -}); - -describe("nextDay", () => { - test("returns next day within the same month", () => { - const d = new Date(2026, 5, 5); - const n = nextDay(d); - expect(n.getFullYear()).toBe(2026); - expect(n.getMonth()).toBe(5); - expect(n.getDate()).toBe(6); - }); - test("wraps month on last day", () => { - const d = new Date(2026, 5, 30); - const n = nextDay(d); - expect(n.getFullYear()).toBe(2026); - expect(n.getMonth()).toBe(6); - expect(n.getDate()).toBe(1); - }); - test("wraps year on December 31", () => { - const d = new Date(2026, 11, 31); - const n = nextDay(d); - expect(n.getFullYear()).toBe(2027); - expect(n.getMonth()).toBe(0); - expect(n.getDate()).toBe(1); - }); - test("DST-safe: returns midnight in local time after US spring-forward", () => { - // US DST 2026 starts March 8, 2026 - const d = new Date(2026, 2, 8); - const n = nextDay(d); - expect(n.getFullYear()).toBe(2026); - expect(n.getMonth()).toBe(2); - expect(n.getDate()).toBe(9); - expect(n.getHours()).toBe(0); - expect(n.getMinutes()).toBe(0); - }); - test("DST-safe: returns midnight in local time after US fall-back", () => { - // US DST 2026 ends November 1, 2026 - const d = new Date(2026, 10, 1); - const n = nextDay(d); - expect(n.getFullYear()).toBe(2026); - expect(n.getMonth()).toBe(10); - expect(n.getDate()).toBe(2); - expect(n.getHours()).toBe(0); - expect(n.getMinutes()).toBe(0); - }); - test("handles February in leap year", () => { - const d = new Date(2024, 1, 28); - const n = nextDay(d); - expect(n.getFullYear()).toBe(2024); - expect(n.getMonth()).toBe(1); - expect(n.getDate()).toBe(29); - }); - test("handles February in non-leap year", () => { - const d = new Date(2026, 1, 28); - const n = nextDay(d); - expect(n.getFullYear()).toBe(2026); - expect(n.getMonth()).toBe(2); - expect(n.getDate()).toBe(1); - }); - test("does not mutate input date", () => { - const d = new Date(2026, 5, 15); - const originalTime = d.getTime(); - nextDay(d); - expect(d.getTime()).toBe(originalTime); - }); -}); - -describe("nextMonth", () => { - test("returns next month within the same year", () => { - const r = nextMonth(new Date(2026, 0, 15)); - expect(r.year).toBe(2026); - expect(r.month).toBe(1); - expect(r.daysInMonth).toBe(28); - }); - test("returns daysInMonth for 30-day months", () => { - expect(nextMonth(new Date(2026, 2, 15)).daysInMonth).toBe(30); // April - expect(nextMonth(new Date(2026, 3, 15)).daysInMonth).toBe(31); // May - }); - test("returns 29 for February in a leap year", () => { - const r = nextMonth(new Date(2024, 0, 15)); - expect(r.year).toBe(2024); - expect(r.month).toBe(1); - expect(r.daysInMonth).toBe(29); - }); - test("returns 28 for February in a non-leap year", () => { - const r = nextMonth(new Date(2026, 0, 15)); - expect(r.year).toBe(2026); - expect(r.month).toBe(1); - expect(r.daysInMonth).toBe(28); - }); - test("returns 31 for January", () => { - const r = nextMonth(new Date(2025, 11, 15)); - expect(r.year).toBe(2026); - expect(r.month).toBe(0); - expect(r.daysInMonth).toBe(31); - }); - test("wraps year on December 15", () => { - const r = nextMonth(new Date(2026, 11, 15)); - expect(r.year).toBe(2027); - expect(r.month).toBe(0); - expect(r.daysInMonth).toBe(31); - }); - test("month is zero-indexed (0 = January)", () => { - const r = nextMonth(new Date(2026, 6, 15)); - expect(r.month).toBe(7); - }); -}); diff --git a/src/brain/stub.ts b/src/brain/stub.ts deleted file mode 100644 index 464b5f6..0000000 --- a/src/brain/stub.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * In-memory implementation of the supermemory SDK surface that the - * `Brain` class uses. Lets debug commands exercise the full Brain - * flow (including `Brain.add`, `Brain.get`, `Brain.search`, etc.) - * without any network calls or API key. - * - * Storage shape: a `Map` keyed by an internal id; lookups - * by `customId` are done by linear scan. `containerTag` is recorded at - * `add()` time and filtered on `list()`. `search.execute` does a - * case-insensitive substring match against `content`. - * - * This is NOT a complete supermemory clone — it only implements the - * methods Brain calls. Adding a new Brain method that needs a - * different SDK method will require extending this stub. - */ -interface StoredDoc { - id: string; - customId: string | null; - containerTag: string; - content: string; - summary: string | null; - metadata: Record | null; -} - -export class MemoryStub { - readonly docs = new Map(); - private nextId = 0; - - documents = { - add: async (params: { - content: string; - containerTag: string; - customId?: string; - metadata?: Record; - }) => { - const id = `stub-${++this.nextId}`; - this.docs.set(id, { - id, - customId: params.customId ?? null, - containerTag: params.containerTag, - content: params.content, - summary: null, - metadata: params.metadata ?? null, - }); - return { id, status: "done" }; - }, - list: async (params: { containerTags?: Array; limit?: number }) => { - const tags = params.containerTags ?? []; - const limit = params.limit ?? 200; - const all = Array.from(this.docs.values()).filter((d) => - tags.length === 0 ? true : tags.includes(d.containerTag), - ); - const memories = all.slice(0, limit).map((d) => ({ - id: d.id, - customId: d.customId, - containerTag: d.containerTag, - content: d.content, - summary: d.summary, - metadata: d.metadata, - createdAt: "2026-01-01T00:00:00Z", - updatedAt: "2026-01-01T00:00:00Z", - status: "done" as const, - type: "text" as const, - connectionId: null, - filepath: null, - title: null, - })); - return { - memories, - pagination: { - currentPage: 1, - totalItems: memories.length, - totalPages: 1, - limit, - }, - }; - }, - get: async (id: string) => { - const d = this.docs.get(id); - if (!d) { - throw new Error(`MemoryStub.documents.get: no such id ${id}`); - } - return { - id: d.id, - customId: d.customId, - containerTag: d.containerTag, - content: d.content, - summary: d.summary, - metadata: d.metadata, - createdAt: "2026-01-01T00:00:00Z", - updatedAt: "2026-01-01T00:00:00Z", - status: "done" as const, - type: "text" as const, - connectionId: null, - filepath: null, - title: null, - source: null, - ogImage: null, - raw: null, - spatialPoint: null, - taskType: "memory" as const, - url: null, - }; - }, - }; - - search = { - execute: async (params: { - q: string; - containerTag?: string; - limit?: number; - onlyMatchingChunks?: boolean; - }) => { - const q = params.q.toLowerCase(); - const limit = params.limit ?? 5; - const hits = Array.from(this.docs.values()) - .filter( - (d) => - (params.containerTag - ? d.containerTag === params.containerTag - : true) && d.content.toLowerCase().includes(q), - ) - .slice(0, limit) - .map((d, i) => ({ - chunks: [ - { - content: d.content, - isRelevant: true, - score: 1 - i * 0.1, - }, - ], - summary: d.summary, - score: 1 - i * 0.1, - documentId: d.id, - metadata: d.metadata as Record | null, - createdAt: "2026-01-01T00:00:00Z", - updatedAt: "2026-01-01T00:00:00Z", - title: d.customId, - type: "text" as const, - })); - return { - results: hits, - total: hits.length, - timing: 0, - }; - }, - }; -} diff --git a/src/commands/debug/brain.test.ts b/src/commands/debug/brain.test.ts deleted file mode 100644 index b760aa7..0000000 --- a/src/commands/debug/brain.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; -import { existsSync, readdirSync } from "fs"; -import { tmpdir } from "os"; - -interface RecordedCall { - model: unknown; - options: { - jsonSchemaName?: string; - instruction?: string; - message?: string; - }; -} - -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 mockCall = mock(async (model: unknown, options: any): Promise => { - llmCalls.push({ model, options }); - if ( - options.instruction?.includes("depth psychologist") || - options.instruction?.includes("forensic biographer") - ) { - return PERSONA_DESCRIPTION as unknown as T; - } - if ( - options.instruction?.includes("prompt engineer") || - options.instruction?.includes("LLM character embodiment") - ) { - return GENERATED_BASE_SYSTEM_PROMPT as unknown as T; - } - throw new Error( - `unexpected LLM call: model=${model} instruction=${options.instruction?.slice(0, 80)}`, - ); -}); - -mock.module("@/openrouter", () => ({ - llm: { - models: { conversation: "test-conv", identity: "test-id" }, - call: mockCall, - }, -})); - -mock.module("@/config", () => ({ - config: { - openrouterApiKey: "test-key", - supermemoryApiKey: "test-supermemory-key", - braindbPath: "/tmp/brainbox-test-braindb-debug-brain-IGNORED.json", - }, -})); - -const { runDebugBrainInit } = await import("./brain"); - -beforeEach(() => { - llmCalls.length = 0; - mockCall.mockClear(); -}); - -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 { - await unlink(`${tmpdir()}/${f}`); - } catch {} - } -}); - -describe("runDebugBrainInit", () => { - 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); - if (!result.ok) throw new Error("expected ok"); - - expect(result.kind).toBe("init"); - expect(result.displayName).toBe("Maren"); - expect(result.brainId).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, - ); - expect(result.spaceName).toBe(`brain:${result.brainId}`); - - expect(result.description).toBe(PERSONA_DESCRIPTION); - - expect(result.baseSystemPrompt).toContain(GENERATED_BASE_SYSTEM_PROMPT); - expect(result.baseSystemPrompt).toContain("You exist in a text chat."); - expect(result.baseSystemPrompt).toBe( - `${GENERATED_BASE_SYSTEM_PROMPT}\n\n` + - result.baseSystemPrompt.slice( - GENERATED_BASE_SYSTEM_PROMPT.length + 2, - ), - ); - - 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 2 times — PERSONA_INIT and PERSONA_BASE_SYSTEM_PROMPT", async () => { - await runDebugBrainInit({ - displayName: "Test", - seed: "a seed", - noSupermemory: true, - }); - - expect(llmCalls.length).toBe(2); - - const initCall = llmCalls[0]!; - expect(initCall.options.message).toBe("a seed"); - expect(initCall.options.jsonSchemaName).toBeUndefined(); - - const systemCall = llmCalls[1]!; - expect(systemCall.options.jsonSchemaName).toBeUndefined(); - expect(systemCall.options.message).toBe(PERSONA_DESCRIPTION); - }); - - 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 beforeCwdEntries = readdirSync(cwd); - const beforeTmp = readdirSync(tmpdir()).filter((f) => - f.startsWith("brainbox-debug-brain-"), - ); - - await runDebugBrainInit({ displayName: "NoDiskCheck", seed: "x", noSupermemory: true }); - - const afterCwdEntries = readdirSync(cwd); - const afterTmp = readdirSync(tmpdir()).filter((f) => - f.startsWith("brainbox-debug-brain-"), - ); - - 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 () => { - mockCall.mockImplementationOnce(async () => { - throw new Error("simulated LLM failure on PERSONA_INIT"); - }); - - const result = await runDebugBrainInit({ - displayName: "Doomed", - seed: "x", - noSupermemory: true, - }); - - expect(result.ok).toBe(false); - if (result.ok) throw new Error("expected !ok"); - expect(result.error).toMatch(/Brain initialization failed/); - expect(typeof result.elapsedMs).toBe("number"); - expect(result.elapsedMs).toBeGreaterThanOrEqual(0); - }); - - 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"); - expect(typeof result.elapsedMs).toBe("number"); - expect(result.elapsedMs).toBeGreaterThanOrEqual(0); - }); -}); - -// --------------------------------------------------------------------------- -// 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`. -// --------------------------------------------------------------------------- diff --git a/src/commands/debug/brain.ts b/src/commands/debug/brain.ts deleted file mode 100644 index 81b68f1..0000000 --- a/src/commands/debug/brain.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { unlink, writeFile } from "fs/promises"; -import { tmpdir } from "os"; -import { join } from "path"; -import type { Command } from "commander"; -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 = - | { - ok: true; - kind: "init"; - displayName: string; - brainId: string; - spaceName: string; - description: string; - baseSystemPrompt: string; - storedFacts: Array<{ customId: string | null; content: string }>; - storageMode: "supermemory" | "stub"; - elapsedMs: number; - } - | { ok: false; error: string; elapsedMs: number }; - -export async function runDebugBrainInit( - opts: BrainInitOptions, -): Promise { - const startTime = Date.now(); - const braindbPath = join( - tmpdir(), - `brainbox-debug-brain-${randomUUID()}.json`, - ); - await writeFile(braindbPath, "{}", { encoding: "utf-8" }); - const storageMode = opts.noSupermemory ? "stub" : "supermemory"; - const db = opts.noSupermemory ? new MemoryStub() : undefined; - - try { - const steps = new StepDriver(4); - - const result = await runCreateSteps(opts.displayName, opts.seed, { - braindbPath, - db, - }, steps); - if (!result) { - const elapsedMs = Date.now() - startTime; - return { ok: false, error: "Brain initialization failed", elapsedMs }; - } - const { brain, description, baseSystemPrompt } = result; - const storedFacts = await brain.list(); - - 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(`Step 2 output — baseSystemPrompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)`); - console.log(baseSystemPrompt); - console.log(); - - 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 documents stored)"); - } - console.log(); - - const elapsedMs = Date.now() - startTime; - logger.info( - `Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to real disk.`, - ); - - return { - ok: true, - kind: "init", - displayName: opts.displayName, - brainId: brain.brainbase.brainId, - spaceName: brain.brainbase.spaceName, - description, - baseSystemPrompt, - storedFacts, - storageMode, - elapsedMs, - }; - } finally { - try { - await unlink(braindbPath); - } catch {} - } -} - -export function addBrainSubcommand(parent: Command): Command { - const cmd = parent - .command("brain") - .description("Debug tools for brain lifecycle (no real disk writes)"); - - cmd - .command("init") - .description( - "Initialize a new brain with LLM (temp braindb; nothing persisted to repo)", - ) - .requiredOption("-n, --name ", "Display name for the new brain") - .requiredOption( - "-s, --seed ", - "Seed text used to generate the persona biography", - ) - .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; -} diff --git a/src/commands/debug/index.ts b/src/commands/debug/index.ts deleted file mode 100644 index c0259c7..0000000 --- a/src/commands/debug/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Command } from "commander"; -import { registerCommand } from "@/commands"; -import { addBrainSubcommand } from "./brain"; -import { addScheduleSubcommand } from "./schedule"; - -export function register(program: Command): Command { - return registerCommand(program, { - name: "debug", - description: - "Dry-run tools: exercise code paths without writing to the database or braindb", - configure: (cmd) => { - addScheduleSubcommand(cmd); - addBrainSubcommand(cmd); - }, - }); -} diff --git a/src/commands/debug/output.ts b/src/commands/debug/output.ts deleted file mode 100644 index 2d1dd3d..0000000 --- a/src/commands/debug/output.ts +++ /dev/null @@ -1,63 +0,0 @@ -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): 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; -} diff --git a/src/commands/debug/schedule.test.ts b/src/commands/debug/schedule.test.ts deleted file mode 100644 index 0d636fe..0000000 --- a/src/commands/debug/schedule.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; -import type { - Availability, - DailySlot, - MonthlyDay, -} from "@/openrouter/schema"; - -interface RecordedCall { - model: unknown; - options: { jsonSchemaName?: string; message?: string }; -} - -const llmCalls: RecordedCall[] = []; - -function build48Slots(): DailySlot[] { - const slots: DailySlot[] = []; - for (let i = 0; i < 48; i++) { - const startHour = Math.floor(i / 2); - const startMin = (i % 2) * 30; - const start = `${String(startHour).padStart(2, "0")}:${String(startMin).padStart(2, "0")}`; - let end: string; - if (i === 47) { - end = "24:00"; - } else { - const endHour = Math.floor((i + 1) / 2); - const endMin = ((i + 1) % 2) * 30; - end = `${String(endHour).padStart(2, "0")}:${String(endMin).padStart(2, "0")}`; - } - slots.push({ start, end, activity: `slot-${i}`, notes: "" }); - } - return slots; -} - -function buildAvailability(): Availability[] { - return [ - { start: "00:00", end: "07:00", status: "offline" }, - { start: "07:00", end: "23:30", status: "online" }, - { start: "23:30", end: "24:00", status: "offline" }, - ]; -} - -function buildMonthlyDays(): MonthlyDay[] { - return Array.from({ length: 30 }, (_, i) => ({ - day: i + 1, - summary: `Day ${i + 1} summary`, - })); -} - -const mockCall = mock(async (_model: unknown, options: any) => { - llmCalls.push({ model: _model, options }); - if (options.jsonSchemaName === "daily-schedule") - return { items: build48Slots() }; - if (options.jsonSchemaName === "monthly-schedule") { - const match = (options.message as string).match(/\((\d+) days\)/); - const days = match ? parseInt(match[1]!, 10) : 30; - return { - items: Array.from({ length: days }, (_, i) => ({ - day: i + 1, - summary: `Day ${i + 1} summary`, - })), - }; - } - if (options.jsonSchemaName === "availability") - return { items: buildAvailability() }; - throw new Error(`unexpected jsonSchemaName: ${options.jsonSchemaName}`); -}); - -mock.module("@/openrouter", () => ({ - llm: { - models: { conversation: "test-conv", identity: "test-id" }, - call: mockCall, - }, -})); - -mock.module("@/config", () => ({ - config: { - openrouterApiKey: "test-key", - dbPath: ":memory:", - braindbPath: "/tmp/brainbox-test-braindb-debug-schedule.json", - }, -})); - -const { runDebugScheduleDaily, runDebugScheduleMonthly } = await import( - "./schedule" -); - -beforeEach(() => { - llmCalls.length = 0; - mockCall.mockClear(); -}); - -afterEach(async () => { - const { unlink } = await import("fs/promises"); - try { - await unlink("/tmp/brainbox-test-braindb-debug-schedule.json"); - } catch {} -}); - -describe("runDebugScheduleDaily", () => { - test("T1: returns ok result with schedule and availability, uses the supplied personality", async () => { - const result = await runDebugScheduleDaily({ - message: "focus on writing", - personality: "test-personality-XYZ", - noSupermemory: true, - }); - - expect(result.ok).toBe(true); - if (!result.ok) throw new Error("expected ok"); - - expect(result.kind).toBe("daily"); - expect(result.schedule.items).toHaveLength(48); - expect(result.availability.items.length).toBeGreaterThan(0); - expect(result.dateKey).toMatch(/^\d{4}-\d{2}-\d{2}$/); - expect(typeof result.elapsedMs).toBe("number"); - expect(result.elapsedMs).toBeGreaterThanOrEqual(0); - - const dailyCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "daily-schedule", - ); - expect(dailyCall).toBeDefined(); - expect(dailyCall!.options.message).toContain("test-personality-XYZ"); - expect(dailyCall!.options.message).toContain("focus on writing"); - - const availCall = llmCalls.find( - (c) => c.options.jsonSchemaName === "availability", - ); - expect(availCall).toBeDefined(); - }); - - test("T2: when LLM returns null for daily, returns {ok: false, error}", async () => { - mockCall.mockImplementationOnce(async () => null as unknown as never); - const result = await runDebugScheduleDaily({ - message: "", - personality: "p", - noSupermemory: true, - }); - expect(result.ok).toBe(false); - if (result.ok) throw new Error("expected !ok"); - expect(result.error).toMatch(/Daily schedule generation failed/); - expect(typeof result.elapsedMs).toBe("number"); - expect(result.elapsedMs).toBeGreaterThanOrEqual(0); - }); -}); - -describe("runDebugScheduleMonthly", () => { - test("T3: returns ok result with monthly schedule, uses the supplied personality", async () => { - const result = await runDebugScheduleMonthly({ - message: "study for GRE", - personality: "test-personality-ABC", - noSupermemory: true, - }); - - expect(result.ok).toBe(true); - if (!result.ok) throw new Error("expected ok"); - - expect(result.kind).toBe("monthly"); - expect(result.schedule.items).toHaveLength(result.daysInMonth); - expect(result.monthKey).toMatch(/^\d{4}-\d{2}$/); - expect(typeof result.elapsedMs).toBe("number"); - expect(result.elapsedMs).toBeGreaterThanOrEqual(0); - - const call = llmCalls.find( - (c) => c.options.jsonSchemaName === "monthly-schedule", - ); - expect(call).toBeDefined(); - expect(call!.options.message).toContain("test-personality-ABC"); - expect(call!.options.message).toContain("study for GRE"); - }); - - test("T4: when LLM returns null for monthly, returns {ok: false, error}", async () => { - mockCall.mockImplementationOnce(async () => null as unknown as never); - const result = await runDebugScheduleMonthly({ - message: "", - personality: "p", - noSupermemory: true, - }); - expect(result.ok).toBe(false); - if (result.ok) throw new Error("expected !ok"); - expect(result.error).toMatch(/Monthly schedule generation failed/); - expect(typeof result.elapsedMs).toBe("number"); - expect(result.elapsedMs).toBeGreaterThanOrEqual(0); - }); -}); - -describe("debug schedule no-disk invariant", () => { - test("T5: running a debug schedule does not create a brainbox db file on disk", async () => { - const { existsSync } = await import("fs"); - const { resolve } = await import("path"); - - const beforeDb = existsSync(resolve(process.cwd(), "brainbox.db")); - const beforeJson = existsSync(resolve(process.cwd(), "brainbox.json")); - - await runDebugScheduleDaily({ message: "m", personality: "p", noSupermemory: true }); - - const afterDb = existsSync(resolve(process.cwd(), "brainbox.db")); - const afterJson = existsSync(resolve(process.cwd(), "brainbox.json")); - - expect(beforeDb).toBe(false); - expect(beforeJson).toBe(false); - expect(afterDb).toBe(false); - expect(afterJson).toBe(false); - }); -}); - diff --git a/src/commands/debug/schedule.ts b/src/commands/debug/schedule.ts deleted file mode 100644 index 1ce34ea..0000000 --- a/src/commands/debug/schedule.ts +++ /dev/null @@ -1,253 +0,0 @@ -import type { Command } from "commander"; -import { - Brain, - runCreateDailyScheduleSteps, - runCreateMonthlyScheduleSteps, -} from "@/brain"; -import { MemoryStub } from "@/brain/stub"; -import { - type AvailabilityWindows, - type DailySchedule, - type MonthlySchedule, -} from "@/openrouter/schema"; -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 = - | { - ok: true; - kind: "daily"; - dateKey: string; - tomorrow: Date; - schedule: DailySchedule; - availability: AvailabilityWindows; - storageMode: "supermemory" | "stub"; - elapsedMs: number; - } - | { ok: false; error: string; elapsedMs: number }; - -export type MonthlyRunResult = - | { - ok: true; - kind: "monthly"; - monthKey: string; - daysInMonth: number; - schedule: MonthlySchedule; - storageMode: "supermemory" | "stub"; - elapsedMs: number; - } - | { ok: false; error: string; elapsedMs: number }; - -export async function runDebugScheduleDaily( - opts: ScheduleOptions, -): Promise { - const startTime = Date.now(); - const today = new Date(); - const tomorrow = new Date( - today.getFullYear(), - today.getMonth(), - 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 }, - db, - ); - - const steps = new StepDriver(4); - - const schedule = await runCreateDailyScheduleSteps( - brain, - today, - opts.message, - steps, - ); - if (!schedule) { - const elapsedMs = Date.now() - startTime; - return { - ok: false, - error: "Daily schedule generation failed", - elapsedMs, - }; - } - - steps.start("deriving availability (SCHEDULE_AVAILABILITY)"); - const availability = await brain.deriveAvailabilityFromSchedule(schedule); - if (!availability) { - steps.fail("see error above"); - const elapsedMs = Date.now() - startTime; - return { - ok: false, - error: "Availability derivation failed", - elapsedMs, - }; - } - steps.done(`${availability.items.length} windows`); - - 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 real disk.`, - ); - - return { - ok: true, - kind: "daily", - dateKey, - tomorrow, - schedule, - availability, - storageMode, - elapsedMs, - }; -} - -export async function runDebugScheduleMonthly( - opts: ScheduleOptions, -): Promise { - const startTime = Date.now(); - 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 }, - db, - ); - - const steps = new StepDriver(3); - - const schedule = await runCreateMonthlyScheduleSteps( - brain, - today, - opts.message, - steps, - ); - if (!schedule) { - const elapsedMs = Date.now() - startTime; - return { - ok: false, - error: "Monthly schedule generation failed", - elapsedMs, - }; - } - - 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 real disk. (Availability applies per-day and is not generated for the monthly view.)`, - ); - - return { - ok: true, - kind: "monthly", - monthKey, - daysInMonth: next.daysInMonth, - schedule, - storageMode, - elapsedMs, - }; -} - -export function addScheduleSubcommand(parent: Command): Command { - const cmd = parent - .command("schedule") - .description("Generate a test schedule (no disk writes)"); - - cmd - .command("daily") - .description( - "Generate a daily schedule for tomorrow and print schedule + availability", - ) - .requiredOption("-m, --message ", "User direction for the schedule") - .requiredOption("-p, --personality ", "Brain personality to use") - .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") - .description("Generate a monthly schedule for next month and print it") - .requiredOption("-m, --message ", "User direction for the schedule") - .requiredOption("-p, --personality ", "Brain personality to use") - .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; -} diff --git a/src/commands/index.test.ts b/src/commands/index.test.ts deleted file mode 100644 index abb6265..0000000 --- a/src/commands/index.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { Command } from "commander"; -import { registerCommand } from "./index"; - -describe("registerCommand", () => { - test("attaches a subcommand with the given name and description", () => { - const program = new Command(); - const cmd = registerCommand(program, { - name: "foo", - description: "Foo command", - }); - expect(cmd).toBeDefined(); - expect(cmd.name()).toBe("foo"); - expect(cmd.description()).toBe("Foo command"); - }); - - test("returns the newly-created subcommand", () => { - const program = new Command(); - const cmd = registerCommand(program, { - name: "bar", - description: "Bar command", - }); - expect(program.commands.find((c) => c.name() === "bar")).toBe(cmd); - }); - - test("invokes the configure callback with the new subcommand", () => { - const program = new Command(); - let received: Command | undefined; - registerCommand(program, { - name: "baz", - description: "Baz command", - configure: (cmd) => { - received = cmd; - cmd.option("-x, --xtra ", "extra"); - }, - }); - expect(received).toBeDefined(); - expect(received!.name()).toBe("baz"); - const baz = program.commands.find((c) => c.name() === "baz"); - expect(baz?.options.find((o) => o.long === "--xtra")).toBeDefined(); - }); - - test("omitting configure is allowed and does not throw", () => { - const program = new Command(); - expect(() => - registerCommand(program, { name: "qux", description: "Qux" }), - ).not.toThrow(); - expect(program.commands.find((c) => c.name() === "qux")).toBeDefined(); - }); -}); diff --git a/src/index.ts b/src/index.ts index 2805cb9..6b0c1a4 100755 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,6 @@ import { dirname, join } from "path"; import { logger } from "@/utils/logger"; import { register as run } from "@/commands/run"; import { register as brain } from "@/commands/brain"; -import { register as debug } from "@/commands/debug"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -35,7 +34,6 @@ program run(program); brain(program); -debug(program); program.on("command:*", () => { logger.error(`Unknown command: ${program.args.join(" ")}`); diff --git a/src/utils/duration.ts b/src/utils/duration.ts deleted file mode 100644 index 968f375..0000000 --- a/src/utils/duration.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Format a millisecond duration as a human-readable string. - * - < 1s: "450ms" - * - < 60s: "1.23s" - * - >= 60s: "2m 5s" - */ -export function formatDuration(ms: number): string { - if (ms < 0 || !Number.isFinite(ms)) return "0ms"; - if (ms < 1000) return `${Math.round(ms)}ms`; - const seconds = ms / 1000; - if (seconds < 60) return `${seconds.toFixed(2)}s`; - const minutes = Math.floor(seconds / 60); - const remainingSeconds = Math.floor(seconds % 60); - return `${minutes}m ${remainingSeconds}s`; -} diff --git a/tsconfig.json b/tsconfig.json index fb243ad..aadc6fb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,6 @@ "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, - "types": ["bun"], // Bundler mode "baseUrl": ".",