refactor: remove test/debug
This commit is contained in:
38
bun.lock
38
bun.lock
@@ -9,12 +9,10 @@
|
|||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"commander": "^15.0.0",
|
"commander": "^15.0.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"ora": "^9.4.0",
|
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"supermemory": "^4.24.12",
|
"supermemory": "^4.24.12",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.1",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -25,58 +23,22 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"@openrouter/sdk": ["@openrouter/sdk@0.12.79", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-0ZpwtnuHh3/B1piW9kHCUIQy6PAsaK/vjFdZuHxmCdAenCyUNsLA2mFpmfHNWRNb+bOO3yBc4IALa264UyzmBA=="],
|
"@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=="],
|
"@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=="],
|
"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=="],
|
"commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
|
||||||
|
|
||||||
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,9 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cli": "bun run src/index.ts",
|
"cli": "bun run src/index.ts",
|
||||||
"test": "bun test",
|
|
||||||
"format": "bun run prettier 'src/**/*.ts' -w"
|
"format": "bun run prettier 'src/**/*.ts' -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
|
||||||
"@types/node": "^25.9.1"
|
"@types/node": "^25.9.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -23,7 +21,6 @@
|
|||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"commander": "^15.0.0",
|
"commander": "^15.0.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"ora": "^9.4.0",
|
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"supermemory": "^4.24.12"
|
"supermemory": "^4.24.12"
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,7 @@ import type {
|
|||||||
ChatFunctionTool,
|
ChatFunctionTool,
|
||||||
ChatMessages,
|
ChatMessages,
|
||||||
} from "@openrouter/sdk/models";
|
} from "@openrouter/sdk/models";
|
||||||
import { BrainDBManager, brainManager, type BrainItem } from "./manager";
|
import { brainManager, type BrainItem } from "./manager";
|
||||||
import { MemoryStub } from "./stub";
|
|
||||||
import {
|
import {
|
||||||
translateMessageHistory,
|
translateMessageHistory,
|
||||||
type MessageHistoryEntry,
|
type MessageHistoryEntry,
|
||||||
@@ -35,10 +34,6 @@ import {
|
|||||||
} from "./schedule";
|
} from "./schedule";
|
||||||
import type { FactInput, FactMetadata, SearchHit, Space } from "./types";
|
import type { FactInput, FactMetadata, SearchHit, Space } from "./types";
|
||||||
|
|
||||||
export interface DebugOptions {
|
|
||||||
personality: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BrainCreateResult {
|
export interface BrainCreateResult {
|
||||||
brain: Brain;
|
brain: Brain;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -49,10 +44,9 @@ export class Brain {
|
|||||||
private availabilityCache: Map<string, AvailabilityWindows> = new Map();
|
private availabilityCache: Map<string, AvailabilityWindows> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public db: Supermemory | MemoryStub,
|
public db: Supermemory,
|
||||||
public space: Space,
|
public space: Space,
|
||||||
public brainbase: BrainItem,
|
public brainbase: BrainItem,
|
||||||
public debug: boolean = false,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -125,14 +119,154 @@ export class Brain {
|
|||||||
datetime: Date,
|
datetime: Date,
|
||||||
message: string,
|
message: string,
|
||||||
): Promise<DailySchedule | null> {
|
): Promise<DailySchedule | null> {
|
||||||
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<DailySchedule>(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(
|
async createMonthlySchedule(
|
||||||
datetime: Date,
|
datetime: Date,
|
||||||
message: string,
|
message: string,
|
||||||
): Promise<MonthlySchedule | null> {
|
): Promise<MonthlySchedule | null> {
|
||||||
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<MonthlySchedule>(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(
|
async sleepMemory(
|
||||||
@@ -487,9 +621,64 @@ export class Brain {
|
|||||||
static async create(
|
static async create(
|
||||||
displayName: string,
|
displayName: string,
|
||||||
seed: string,
|
seed: string,
|
||||||
options: { braindbPath?: string; db?: Supermemory | MemoryStub } = {},
|
|
||||||
): Promise<BrainCreateResult | null> {
|
): Promise<BrainCreateResult | null> {
|
||||||
return await runCreateSteps(displayName, seed, options, noopRunner);
|
try {
|
||||||
|
const personaInitInstruction = await loadPrompt("PERSONA_INIT");
|
||||||
|
const description = await llm.call<string>(llm.models.identity, {
|
||||||
|
instruction: personaInitInstruction,
|
||||||
|
message: seed,
|
||||||
|
});
|
||||||
|
|
||||||
|
const personaSystemInstruction = await loadPrompt(
|
||||||
|
"PERSONA_BASE_SYSTEM_PROMPT",
|
||||||
|
);
|
||||||
|
const generatedBaseSystemPrompt = await llm.call<string>(
|
||||||
|
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<Brain | null> {
|
static async load(brainId: string): Promise<Brain | null> {
|
||||||
@@ -500,310 +689,6 @@ export class Brain {
|
|||||||
const space: Space = { name: brainbase.spaceName };
|
const space: Space = { name: brainbase.spaceName };
|
||||||
return new Brain(db, space, brainbase);
|
return new Brain(db, space, brainbase);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async createDebug(
|
|
||||||
options: DebugOptions,
|
|
||||||
db?: Supermemory | MemoryStub,
|
|
||||||
): Promise<Brain> {
|
|
||||||
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<DailySchedule | null> {
|
|
||||||
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<DailySchedule>(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<MonthlySchedule | null> {
|
|
||||||
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<MonthlySchedule>(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<BrainCreateResult | null> {
|
|
||||||
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<string>(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<string>(
|
|
||||||
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 {
|
function formatDatetime(now: Date): string {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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<id, StoredDoc>` 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<string, unknown> | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MemoryStub {
|
|
||||||
readonly docs = new Map<string, StoredDoc>();
|
|
||||||
private nextId = 0;
|
|
||||||
|
|
||||||
documents = {
|
|
||||||
add: async (params: {
|
|
||||||
content: string;
|
|
||||||
containerTag: string;
|
|
||||||
customId?: string;
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
}) => {
|
|
||||||
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<string>; 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<string, unknown> | 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,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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 <T>(model: unknown, options: any): Promise<T> => {
|
|
||||||
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`.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
@@ -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<BrainInitResult> {
|
|
||||||
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 <text>", "Display name for the new brain")
|
|
||||||
.requiredOption(
|
|
||||||
"-s, --seed <text>",
|
|
||||||
"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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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<string, string>): void {
|
|
||||||
const labelWidth = Math.max(...Object.keys(pairs).map((k) => k.length));
|
|
||||||
for (const [key, value] of Object.entries(pairs)) {
|
|
||||||
console.log(` ${key.padEnd(labelWidth)} ${value}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StepDriver {
|
|
||||||
private readonly stepCount: number;
|
|
||||||
private stepIndex = 0;
|
|
||||||
private current: Ora | null = null;
|
|
||||||
private currentLabel = "";
|
|
||||||
|
|
||||||
constructor(stepCount: number) {
|
|
||||||
this.stepCount = stepCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
start(label: string): void {
|
|
||||||
this.stepIndex += 1;
|
|
||||||
this.resolvePrevious();
|
|
||||||
this.currentLabel = label;
|
|
||||||
const text = `Step ${this.stepIndex}/${this.stepCount}: ${label}`;
|
|
||||||
this.current = ora(text).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
done(summary: string): void {
|
|
||||||
if (!this.current) return;
|
|
||||||
const text = this.current.text;
|
|
||||||
this.current.succeed(`${text} — ${summary}`);
|
|
||||||
this.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fail(reason: string): void {
|
|
||||||
if (!this.current) {
|
|
||||||
console.log(`${chalk.red("✖")} ${this.currentLabel} — ${reason}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.current.fail(`${this.current.text} — ${reason}`);
|
|
||||||
this.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private resolvePrevious(): void {
|
|
||||||
if (this.current) {
|
|
||||||
this.current.stop();
|
|
||||||
this.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function snippet(text: string): string {
|
|
||||||
const flat = text.replace(/\s+/g, " ").trim();
|
|
||||||
return flat.length > 80 ? `${flat.slice(0, 77)}...` : flat;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -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<DailyRunResult> {
|
|
||||||
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<MonthlyRunResult> {
|
|
||||||
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 <text>", "User direction for the schedule")
|
|
||||||
.requiredOption("-p, --personality <text>", "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 <text>", "User direction for the schedule")
|
|
||||||
.requiredOption("-p, --personality <text>", "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;
|
|
||||||
}
|
|
||||||
@@ -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 <value>", "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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -6,7 +6,6 @@ import { dirname, join } from "path";
|
|||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
import { register as run } from "@/commands/run";
|
import { register as run } from "@/commands/run";
|
||||||
import { register as brain } from "@/commands/brain";
|
import { register as brain } from "@/commands/brain";
|
||||||
import { register as debug } from "@/commands/debug";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -35,7 +34,6 @@ program
|
|||||||
|
|
||||||
run(program);
|
run(program);
|
||||||
brain(program);
|
brain(program);
|
||||||
debug(program);
|
|
||||||
|
|
||||||
program.on("command:*", () => {
|
program.on("command:*", () => {
|
||||||
logger.error(`Unknown command: ${program.args.join(" ")}`);
|
logger.error(`Unknown command: ${program.args.join(" ")}`);
|
||||||
|
|||||||
@@ -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`;
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"types": ["bun"],
|
|
||||||
|
|
||||||
// Bundler mode
|
// Bundler mode
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
|||||||
Reference in New Issue
Block a user