feat: replace identitydb with supermemory

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

View File

@@ -1,4 +1,5 @@
DB_PATH=./brainbox.db SUPERMEMORY_API_KEY=
BRAINDB_PATH=./braindb.json BRAINDB_PATH=./braindb.json
OPENROUTER_API_KEY= OPENROUTER_API_KEY=

View File

@@ -9,9 +9,9 @@
"chalk": "^5.6.2", "chalk": "^5.6.2",
"commander": "^15.0.0", "commander": "^15.0.0",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"identitydb": "^0.5.2",
"ora": "^9.4.0", "ora": "^9.4.0",
"prettier": "^3.8.3", "prettier": "^3.8.3",
"supermemory": "^4.24.12",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
@@ -31,8 +31,6 @@
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "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=="],
@@ -43,90 +41,40 @@
"commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"identitydb": ["identitydb@0.5.2", "", { "dependencies": { "kysely": "^0.28.8", "mysql2": "^3.15.3", "pg": "^8.16.0" } }, "sha512-AkUmmAvpkgtIiHi7l1cTYFQDBYDMSi9HG94IAPWAq8Qa3wHbwaUaDCbPgxJM1ypN63SiW8vBllRTa28W3JXa3Q=="],
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="],
"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=="], "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=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"mysql2": ["mysql2@3.22.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-CtXYlmL7ZamiYKbmqkamQHWJROUHSfm+f3kByzGfknw7kW51mcB2ouMUqYq1XfYxbXmnWo6RhPydx6OCqdgcmQ=="],
"named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], "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=="], "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=="],
"pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="],
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
"pg-connection-string": ["pg-connection-string@2.13.0", "", {}, "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
"pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="],
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"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=="], "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="],
"stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="], "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=="], "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=="], "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=="], "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=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], "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=="],

View File

@@ -23,8 +23,8 @@
"chalk": "^5.6.2", "chalk": "^5.6.2",
"commander": "^15.0.0", "commander": "^15.0.0",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"identitydb": "^0.5.2",
"ora": "^9.4.0", "ora": "^9.4.0",
"prettier": "^3.8.3" "prettier": "^3.8.3",
"supermemory": "^4.24.12"
} }
} }

View File

@@ -16,7 +16,7 @@ You will be given:
5. ALWAYS reply in real time. The user expects a person typing back, not a polished essay. 5. ALWAYS reply in real time. The user expects a person typing back, not a polished essay.
6. ALWAYS filter every response through the persona's voice, vocabulary, and emotional weather. 6. ALWAYS filter every response through the persona's voice, vocabulary, and emotional weather.
7. ALWAYS stay consistent with the date, time, and schedules you were given. Do not contradict them. 7. ALWAYS stay consistent with the date, time, and schedules you were given. Do not contradict them.
8. ALWAYS remember what you already know about the user. Do not ask for facts you already have; use the `searchIdentityDB` tool to look them up. 8. ALWAYS remember what you already know about the user. Do not ask for facts you already have; use the `searchMemory` tool to look them up.
### HOW TO REPLY ### HOW TO REPLY
@@ -29,8 +29,8 @@ You will be given:
### WHEN TO USE TOOLS ### WHEN TO USE TOOLS
- Call `addReplyMessage` for every bubble you want to send. When you have no more to say, end your turn (do not call any tool, return plain text). - Call `addReplyMessage` for every bubble you want to send. When you have no more to say, end your turn (do not call any tool, return plain text).
- Call `searchIdentityDB` whenever the user references something you might already know but you cannot recall precisely. Use the natural-language query that would best match the relevant fact. - Call `searchMemory` whenever the user references something you might already know but you cannot recall precisely. Use the natural-language query that would best match the relevant fact.
- Do not call `searchIdentityDB` for greetings, small talk, or anything you can answer from the persona's own knowledge of the user. - Do not call `searchMemory` for greetings, small talk, or anything you can answer from the persona's own knowledge of the user.
### FINAL MANDATE ### FINAL MANDATE

View File

@@ -1,17 +0,0 @@
import { llm } from "@/openrouter";
import { extractedFactSchema, type ExtractedFactResult } from "@/openrouter/schema";
import { LlmFactExtractor } from "identitydb";
export const factExtractor = new LlmFactExtractor({
model: {
async generateText({ instruction, input }) {
const result = await llm.call<ExtractedFactResult>(llm.models.identity, {
instruction,
message: input,
jsonSchemaName: "fact-extractor",
jsonSchema: extractedFactSchema,
});
return result.items;
},
},
});

View File

@@ -8,7 +8,7 @@ import {
test, test,
} from "bun:test"; } from "bun:test";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { IdentityDB, type Space } from "identitydb"; import type { Space } from "./types";
const llmCalls: Array<{ model: unknown; options: any }> = []; const llmCalls: Array<{ model: unknown; options: any }> = [];
let customMonthlyDays: Array<{ day: number; summary: string }> | null = null; let customMonthlyDays: Array<{ day: number; summary: string }> | null = null;
@@ -24,11 +24,6 @@ let customAvailability: Array<{
status: string; status: string;
}> | null = null; }> | null = null;
/**
* Queue of LLM responses for tool-calling flows (sendMessage). Each entry is
* returned in order. Shape matches OpenRouter's `ChatResult.choices[0]`
* reduced form: `{ content, tool_calls, finish_reason }`.
*/
type ToolCallResponse = { type ToolCallResponse = {
id: string; id: string;
name: string; name: string;
@@ -136,6 +131,9 @@ const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
}, },
} as unknown as T; } 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}`); throw new Error(`unexpected jsonSchemaName: ${options.jsonSchemaName}`);
}); });
@@ -150,14 +148,191 @@ mock.module("@/openrouter", () => ({
mock.module("@/config", () => ({ mock.module("@/config", () => ({
config: { config: {
openrouterApiKey: "test-key", openrouterApiKey: "test-key",
dbPath: ":memory:", supermemoryApiKey: "test-supermemory-key",
braindbPath: "/tmp/brainbox-test-braindb.json", braindbPath: "/tmp/brainbox-test-braindb.json",
}, },
})); }));
interface StoredDoc {
id: string;
customId: string | null;
containerTag: string;
content: string;
summary: string | null;
metadata: Record<string, unknown> | null;
}
class MockSupermemory {
docs = new Map<string, StoredDoc>();
private nextId = 0;
documentsAddCalls = 0;
constructor(_options: { apiKey: string }) {}
documents = {
add: async (params: {
content: string;
containerTag: string;
customId?: string;
metadata?: Record<string, unknown>;
}) => {
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<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,
summary: d.summary,
metadata: d.metadata as
| string
| number
| boolean
| Record<string, unknown>
| Array<unknown>
| 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<string, unknown>
| Array<unknown>
| 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<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,
};
},
};
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 { Brain } = await import("./index");
const { brainManager } = await import("./manager"); const { brainManager } = await import("./manager");
const { formatDateKey, nextDay, nextMonth } = await import("./schedule"); const { formatDateKey, nextMonth } = await import("./schedule");
type BrainItem = import("./manager").BrainItem; type BrainItem = import("./manager").BrainItem;
beforeAll(async () => { beforeAll(async () => {
@@ -168,16 +343,10 @@ beforeAll(async () => {
afterAll(async () => {}); afterAll(async () => {});
async function makeBrain( async function makeBrain(): Promise<InstanceType<typeof Brain>> {
embeddingProvider: unknown = NOOP_EMBEDDING_PROVIDER, const db = new MockSupermemory({ apiKey: "test-supermemory-key" });
): Promise<InstanceType<typeof Brain>> {
const db = await IdentityDB.connect({
client: "sqlite",
filename: ":memory:",
});
await db.initialize();
const spaceName = `test-space-${randomUUID()}`; const spaceName = `test-space-${randomUUID()}`;
const space: Space = await db.upsertSpace({ name: spaceName }); const space: Space = { name: spaceName, description: "Test Brain space" };
const brainbase: BrainItem = { const brainbase: BrainItem = {
brainId: randomUUID(), brainId: randomUUID(),
spaceName, spaceName,
@@ -185,7 +354,7 @@ async function makeBrain(
baseSystemPrompt: baseSystemPrompt:
"Test personality: night owl, introverted, studies at midnight.", "Test personality: night owl, introverted, studies at midnight.",
}; };
return new Brain(db, space, brainbase, false, embeddingProvider as never); return new Brain(db as never, space, brainbase, false);
} }
beforeEach(() => { beforeEach(() => {
@@ -197,10 +366,11 @@ beforeEach(() => {
}); });
describe("Brain.createDailySchedule", () => { describe("Brain.createDailySchedule", () => {
test("S1: returns 48 slots in 30-min intervals and persists a fact", async () => { test("S1: returns 48 slots in 30-min intervals and persists a document", async () => {
const brain = await makeBrain(); const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 5, 5); const today = new Date(2026, 5, 5);
const expectedTomorrow = nextDay(today); const expectedTomorrow = (await import("./schedule")).nextDay(today);
const expectedKey = formatDateKey(expectedTomorrow); const expectedKey = formatDateKey(expectedTomorrow);
const result = await brain.createDailySchedule(today, "focus on writing"); const result = await brain.createDailySchedule(today, "focus on writing");
@@ -228,50 +398,39 @@ describe("Brain.createDailySchedule", () => {
expect(llmCall!.options.message).toContain("focus on writing"); expect(llmCall!.options.message).toContain("focus on writing");
expect(llmCall!.options.message).toContain("Test personality"); expect(llmCall!.options.message).toContain("Test personality");
const facts = await brain.db.getTopicFacts( const stored = db.findByCustomId(`daily-schedule:${expectedKey}`);
`daily-schedule:${expectedKey}`, expect(stored).toBeDefined();
{ expect(stored!.containerTag).toBe(brain.space.name);
spaceName: brain.space.name, expect(JSON.parse(stored!.content).items).toHaveLength(48);
},
);
expect(facts).toHaveLength(1);
expect(JSON.parse(facts[0]!.statement).items).toHaveLength(48);
}); });
test("S4: month wrap (June 30 -> July 1)", async () => { test("S4: month wrap (June 30 -> July 1)", async () => {
const brain = await makeBrain(); const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 5, 30); const today = new Date(2026, 5, 30);
const expectedKey = formatDateKey(new Date(2026, 6, 1)); const expectedKey = formatDateKey(new Date(2026, 6, 1));
await brain.createDailySchedule(today, ""); await brain.createDailySchedule(today, "");
const facts = await brain.db.getTopicFacts( const stored = db.findByCustomId(`daily-schedule:${expectedKey}`);
`daily-schedule:${expectedKey}`, expect(stored).toBeDefined();
{
spaceName: brain.space.name,
},
);
expect(facts).toHaveLength(1);
}); });
test("S4b: year wrap (December 31 -> January 1 next year)", async () => { test("S4b: year wrap (December 31 -> January 1 next year)", async () => {
const brain = await makeBrain(); const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 11, 31); const today = new Date(2026, 11, 31);
const expectedKey = "2027-01-01"; const expectedKey = "2027-01-01";
await brain.createDailySchedule(today, ""); await brain.createDailySchedule(today, "");
const facts = await brain.db.getTopicFacts( const stored = db.findByCustomId(`daily-schedule:${expectedKey}`);
`daily-schedule:${expectedKey}`, expect(stored).toBeDefined();
{
spaceName: brain.space.name,
},
);
expect(facts).toHaveLength(1);
}); });
test("S6: consumes monthly summary for the target day when present", async () => { test("S6: consumes monthly summary for the target day when present", async () => {
const brain = await makeBrain(); const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
customMonthlyDays = Array.from({ length: 30 }, (_, i) => ({ customMonthlyDays = Array.from({ length: 30 }, (_, i) => ({
day: i + 1, day: i + 1,
@@ -282,13 +441,8 @@ describe("Brain.createDailySchedule", () => {
const todayForMonthly = new Date(2026, 4, 15); const todayForMonthly = new Date(2026, 4, 15);
await brain.createMonthlySchedule(todayForMonthly, ""); await brain.createMonthlySchedule(todayForMonthly, "");
const monthlyFacts = await brain.db.getTopicFacts( const monthlyStored = db.findByCustomId("monthly-schedule:2026-06");
`monthly-schedule:2026-06`, expect(monthlyStored).toBeDefined();
{
spaceName: brain.space.name,
},
);
expect(monthlyFacts).toHaveLength(1);
llmCalls.length = 0; llmCalls.length = 0;
customDailySlots = build48Slots(); customDailySlots = build48Slots();
@@ -304,11 +458,63 @@ describe("Brain.createDailySchedule", () => {
"UNIQUE_SUMMARY_FOR_DAY_10", "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", () => { describe("Brain.createMonthlySchedule", () => {
test("S2: returns N day summaries (N = days in next month) and persists a fact", async () => { test("S2: returns N day summaries (N = days in next month) and persists a document", async () => {
const brain = await makeBrain(); const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 0, 15); const today = new Date(2026, 0, 15);
const expected = nextMonth(today); const expected = nextMonth(today);
const expectedKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`; const expectedKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`;
@@ -329,20 +535,16 @@ describe("Brain.createMonthlySchedule", () => {
expect(llmCall!.options.message).toContain("study for GRE"); expect(llmCall!.options.message).toContain("study for GRE");
expect(llmCall!.options.message).toContain("Test personality"); expect(llmCall!.options.message).toContain("Test personality");
const facts = await brain.db.getTopicFacts( const stored = db.findByCustomId(`monthly-schedule:${expectedKey}`);
`monthly-schedule:${expectedKey}`, expect(stored).toBeDefined();
{ expect(JSON.parse(stored!.content).items).toHaveLength(
spaceName: brain.space.name,
},
);
expect(facts).toHaveLength(1);
expect(JSON.parse(facts[0]!.statement).items).toHaveLength(
expected.daysInMonth, expected.daysInMonth,
); );
}); });
test("S5: year wrap (December 15 -> January next year)", async () => { test("S5: year wrap (December 15 -> January next year)", async () => {
const brain = await makeBrain(); const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 11, 15); const today = new Date(2026, 11, 15);
const expectedKey = "2027-01"; const expectedKey = "2027-01";
@@ -351,13 +553,8 @@ describe("Brain.createMonthlySchedule", () => {
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(result!.items).toHaveLength(31); expect(result!.items).toHaveLength(31);
const facts = await brain.db.getTopicFacts( const stored = db.findByCustomId(`monthly-schedule:${expectedKey}`);
`monthly-schedule:${expectedKey}`, expect(stored).toBeDefined();
{
spaceName: brain.space.name,
},
);
expect(facts).toHaveLength(1);
}); });
}); });
@@ -366,25 +563,14 @@ describe("Brain.getTodayScheduledAvailability", () => {
const brain = await makeBrain(); const brain = await makeBrain();
const today = new Date(2026, 5, 10); const today = new Date(2026, 5, 10);
const todayKey = formatDateKey(today); const todayKey = formatDateKey(today);
await brain.db.addFact({ await brain.add({
spaceName: brain.space.name, customId: `daily-schedule:${todayKey}`,
statement: JSON.stringify({ items: build48Slots() }), content: JSON.stringify({ items: build48Slots() }),
summary: "test daily", metadata: {
kind: "schedule",
source: "test", source: "test",
confidence: 1.0, date: todayKey,
topics: [
{
name: `daily-schedule:${todayKey}`,
category: "temporal",
granularity: "concrete",
}, },
{
name: "daily-schedule",
category: "concept",
granularity: "abstract",
},
{ name: todayKey, category: "temporal", granularity: "concrete" },
],
}); });
const result = await brain.getTodayScheduledAvailability(today); const result = await brain.getTodayScheduledAvailability(today);
@@ -417,25 +603,14 @@ describe("Brain.removeScheduledAvailability", () => {
const brain = await makeBrain(); const brain = await makeBrain();
const today = new Date(2026, 5, 10); const today = new Date(2026, 5, 10);
const todayKey = formatDateKey(today); const todayKey = formatDateKey(today);
await brain.db.addFact({ await brain.add({
spaceName: brain.space.name, customId: `daily-schedule:${todayKey}`,
statement: JSON.stringify({ items: build48Slots() }), content: JSON.stringify({ items: build48Slots() }),
summary: "test daily", metadata: {
kind: "schedule",
source: "test", source: "test",
confidence: 1.0, date: todayKey,
topics: [
{
name: `daily-schedule:${todayKey}`,
category: "temporal",
granularity: "concrete",
}, },
{
name: "daily-schedule",
category: "concept",
granularity: "abstract",
},
{ name: todayKey, category: "temporal", granularity: "concrete" },
],
}); });
const r1 = await brain.getTodayScheduledAvailability(today); const r1 = await brain.getTodayScheduledAvailability(today);
@@ -463,25 +638,19 @@ describe("S8: regression on existing methods", () => {
}); });
describe("Brain.createDebug", () => { describe("Brain.createDebug", () => {
test("D1: returns a Brain with debug=true, the supplied personality, and no disk file created", async () => { test("D1: returns a Brain with debug=true and the supplied personality under the brain:debug namespace", async () => {
const { existsSync } = await import("fs");
const { resolve } = await import("path");
const before = existsSync(resolve(process.cwd(), "brainbox.db"));
const brain = await Brain.createDebug({ personality: "test-personality-Q" }); const brain = await Brain.createDebug({ personality: "test-personality-Q" });
expect(brain).toBeInstanceOf(Brain); expect(brain).toBeInstanceOf(Brain);
expect(brain.debug).toBe(true); expect(brain.debug).toBe(true);
expect(brain.brainbase.baseSystemPrompt).toBe("test-personality-Q"); expect(brain.brainbase.baseSystemPrompt).toBe("test-personality-Q");
expect(brain.brainbase.displayName).toBe("Debug Brain"); expect(brain.brainbase.displayName).toBe("Debug Brain");
expect(brain.space.name).toBe("brain:debug");
const after = existsSync(resolve(process.cwd(), "brainbox.db"));
expect(after).toBe(before);
}); });
test("D2: createDailySchedule on a debug brain returns a schedule and does NOT add a fact to the DB", async () => { test("D2: createDailySchedule on a debug brain returns a schedule and persists to brain:debug", async () => {
const brain = await Brain.createDebug({ personality: "p" }); const brain = await Brain.createDebug({ personality: "p" });
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 5, 5); const today = new Date(2026, 5, 5);
const tomorrow = new Date(2026, 5, 6); const tomorrow = new Date(2026, 5, 6);
const tomorrowKey = formatDateKey(tomorrow); const tomorrowKey = formatDateKey(tomorrow);
@@ -490,14 +659,14 @@ describe("Brain.createDebug", () => {
expect(schedule).not.toBeNull(); expect(schedule).not.toBeNull();
expect(schedule!.items).toHaveLength(48); expect(schedule!.items).toHaveLength(48);
const facts = await brain.db.getTopicFacts(`daily-schedule:${tomorrowKey}`, { const stored = db.findByCustomId(`daily-schedule:${tomorrowKey}`);
spaceName: brain.space.name, expect(stored).toBeDefined();
}); expect(stored!.containerTag).toBe("brain:debug");
expect(facts).toHaveLength(0);
}); });
test("D3: createMonthlySchedule on a debug brain returns a schedule and does NOT add a fact to the DB", async () => { test("D3: createMonthlySchedule on a debug brain returns a schedule and persists to brain:debug", async () => {
const brain = await Brain.createDebug({ personality: "p" }); const brain = await Brain.createDebug({ personality: "p" });
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 0, 15); const today = new Date(2026, 0, 15);
const expected = nextMonth(today); const expected = nextMonth(today);
const monthKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`; const monthKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`;
@@ -506,42 +675,12 @@ describe("Brain.createDebug", () => {
expect(schedule).not.toBeNull(); expect(schedule).not.toBeNull();
expect(schedule!.items).toHaveLength(expected.daysInMonth); expect(schedule!.items).toHaveLength(expected.daysInMonth);
const facts = await brain.db.getTopicFacts( const stored = db.findByCustomId(`monthly-schedule:${monthKey}`);
`monthly-schedule:${monthKey}`, expect(stored).toBeDefined();
{ spaceName: brain.space.name }, expect(stored!.containerTag).toBe("brain:debug");
);
expect(facts).toHaveLength(0);
}); });
}); });
const NOOP_EMBEDDING_PROVIDER = {
model: "test-embed",
dimensions: 4,
async embed(_input: string): Promise<number[]> {
return [0, 0, 0, 0];
},
async embedMany(inputs: string[]): Promise<number[][]> {
return inputs.map(() => [0, 0, 0, 0]);
},
};
const SCORING_EMBEDDING_PROVIDER = {
model: "test-embed-scoring",
dimensions: 4,
async embed(input: string): Promise<number[]> {
if (input.includes("coffee")) return [1, 0, 0, 0];
if (input.includes("pizza")) return [0, 1, 0, 0];
return [0, 0, 1, 0];
},
async embedMany(inputs: string[]): Promise<number[][]> {
return inputs.map((s) => {
if (s.includes("coffee")) return [1, 0, 0, 0];
if (s.includes("pizza")) return [0, 1, 0, 0];
return [0, 0, 1, 0];
});
},
};
describe("Brain.sendMessage — translateMessageHistory helper", () => { describe("Brain.sendMessage — translateMessageHistory helper", () => {
test("SM1: translateMessageHistory produces the documented format with persona label and timestamps", async () => { test("SM1: translateMessageHistory produces the documented format with persona label and timestamps", async () => {
const { translateMessageHistory } = await import("./messageHistory"); const { translateMessageHistory } = await import("./messageHistory");
@@ -600,7 +739,7 @@ describe("Brain.sendMessage — tool-calling flow", () => {
}> }>
).map((t) => t.function.name); ).map((t) => t.function.name);
expect(toolNames).toContain("addReplyMessage"); expect(toolNames).toContain("addReplyMessage");
expect(toolNames).toContain("searchIdentityDB"); expect(toolNames).toContain("searchMemory");
}); });
test("SM4: sendMessage accumulates addReplyMessage tool calls and returns them in order", async () => { test("SM4: sendMessage accumulates addReplyMessage tool calls and returns them in order", async () => {
@@ -635,20 +774,13 @@ describe("Brain.sendMessage — tool-calling flow", () => {
expect(out).toEqual(["어.", "왜불러"]); expect(out).toEqual(["어.", "왜불러"]);
}); });
test("SM5: sendMessage feeds searchIdentityDB tool result back to the LLM", async () => { test("SM5: sendMessage feeds searchMemory tool result back to the LLM", async () => {
const brain = await makeBrain(SCORING_EMBEDDING_PROVIDER); const brain = await makeBrain();
const fact = await brain.db.addFact({ await brain.add({
spaceName: brain.space.name, customId: "fact-coffee",
statement: "사용자는 커피를 좋아한다", content: "사용자는 커피를 좋아한다",
summary: "user loves coffee", metadata: { kind: "fact", source: "test" },
source: "test",
confidence: 1.0,
topics: [
{ name: "사용자", category: "entity", granularity: "concrete" },
{ name: "커피", category: "concept", granularity: "abstract" },
],
}); });
await brain.indexFactEmbeddingFor(fact);
chatResponses = [ chatResponses = [
{ {
@@ -656,7 +788,7 @@ describe("Brain.sendMessage — tool-calling flow", () => {
tool_calls: [ tool_calls: [
{ {
id: "call_s", id: "call_s",
name: "searchIdentityDB", name: "searchMemory",
arguments: JSON.stringify({ query: "커피" }), arguments: JSON.stringify({ query: "커피" }),
}, },
], ],
@@ -732,8 +864,8 @@ describe("Brain.sendMessage — tool-calling flow", () => {
expect(userMsg!.content).toContain("하이"); expect(userMsg!.content).toContain("하이");
}); });
test("SM7: createDailySchedule auto-indexes the new fact so it is searchable via the provider", async () => { test("SM7: createDailySchedule persists a document reachable via brain.get", async () => {
const brain = await makeBrain(SCORING_EMBEDDING_PROVIDER); const brain = await makeBrain();
const today = new Date(2026, 5, 5); const today = new Date(2026, 5, 5);
const tomorrow = new Date(2026, 5, 6); const tomorrow = new Date(2026, 5, 6);
const tomorrowKey = formatDateKey(tomorrow); const tomorrowKey = formatDateKey(tomorrow);
@@ -741,43 +873,20 @@ describe("Brain.sendMessage — tool-calling flow", () => {
customDailySlots = build48Slots(); customDailySlots = build48Slots();
await brain.createDailySchedule(today, "msg"); await brain.createDailySchedule(today, "msg");
const hits = await brain.db.searchFacts({ const stored = await brain.get(`daily-schedule:${tomorrowKey}`);
spaceName: brain.space.name, expect(stored).not.toBeNull();
query: "slot-0", expect(stored!.content).toContain("slot-0");
provider: SCORING_EMBEDDING_PROVIDER as never, expect(stored!.metadata).toEqual({
limit: 5, kind: "schedule",
source: "createDailySchedule",
date: tomorrowKey,
}); });
expect(hits.length).toBeGreaterThan(0);
const matched = hits.find((h) =>
h.statement.includes(`"activity":"slot-0"`),
);
expect(matched).toBeDefined();
const topicFacts = await brain.db.getTopicFacts(
`daily-schedule:${tomorrowKey}`,
{ spaceName: brain.space.name },
);
expect(topicFacts).toHaveLength(1);
}); });
test("SM8: sendMessage no longer calls indexFactEmbeddings on every turn (uses per-fact init)", async () => { test("SM8: sendMessage does not call brain.add (no documents added during chat)", async () => {
const brain = await makeBrain(NOOP_EMBEDDING_PROVIDER); const brain = await makeBrain();
let embedManyCalls = 0; const db = brain.db as unknown as MockSupermemory;
const trackingProvider = { const before = db.documentsAddCalls;
model: "track-embed",
dimensions: 4,
async embed(_input: string): Promise<number[]> {
return [0, 0, 0, 0];
},
async embedMany(inputs: string[]): Promise<number[][]> {
embedManyCalls += 1;
return inputs.map(() => [0, 0, 0, 0]);
},
};
Object.defineProperty(brain, "embeddingProvider", {
value: trackingProvider,
configurable: true,
});
chatResponses = [ chatResponses = [
{ {
@@ -796,40 +905,19 @@ describe("Brain.sendMessage — tool-calling flow", () => {
[{ sender: "user", time: new Date(2026, 5, 10, 9, 0, 0), content: "hi" }], [{ sender: "user", time: new Date(2026, 5, 10, 9, 0, 0), content: "hi" }],
[], [],
); );
expect(embedManyCalls).toBe(0); expect(db.documentsAddCalls - before).toBe(0);
}); });
test("SM9: initializeEmbeddings backfills missing embeddings for facts added out-of-band", async () => { test("SM9: out-of-band add() facts are queryable via brain.search without backfill", async () => {
const brain = await makeBrain(SCORING_EMBEDDING_PROVIDER); const brain = await makeBrain();
await brain.db.addFact({ await brain.add({
spaceName: brain.space.name, customId: "fact-pizza",
statement: "사용자는 피자를 좋아한다", content: "사용자는 피자를 좋아한다",
summary: "user loves pizza", metadata: { kind: "fact", source: "test" },
source: "test",
confidence: 1.0,
topics: [
{ name: "사용자", category: "entity", granularity: "concrete" },
{ name: "피자", category: "concept", granularity: "abstract" },
],
}); });
let preInitHits = await brain.db.searchFacts({ const hits = await brain.search("피자", 5);
spaceName: brain.space.name, expect(hits.length).toBeGreaterThan(0);
query: "피자", expect(hits[0]!.content).toContain("피자");
provider: SCORING_EMBEDDING_PROVIDER as never,
limit: 5,
});
expect(preInitHits).toHaveLength(0);
await brain.initializeEmbeddings();
const postInitHits = await brain.db.searchFacts({
spaceName: brain.space.name,
query: "피자",
provider: SCORING_EMBEDDING_PROVIDER as never,
limit: 5,
});
expect(postInitHits.length).toBeGreaterThan(0);
expect(postInitHits[0]!.statement).toContain("피자");
}); });
}); });

View File

@@ -1,13 +1,7 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import Supermemory from "supermemory";
import { config } from "@/config"; import { config } from "@/config";
import {
IdentityDB,
type EmbeddingProvider,
type ExtractedFact,
type Space,
} from "identitydb";
import { llm } from "@/openrouter"; import { llm } from "@/openrouter";
import { OpenRouterEmbeddingProvider } from "@/openrouter/embedding";
import { loadPrompt } from "@/openrouter/promptLoader"; import { loadPrompt } from "@/openrouter/promptLoader";
import { import {
availabilitySchema, availabilitySchema,
@@ -15,11 +9,19 @@ import {
monthlyScheduleSchema, monthlyScheduleSchema,
type AvailabilityWindows, type AvailabilityWindows,
type DailySchedule, type DailySchedule,
type DailySlot,
type MonthlySchedule, type MonthlySchedule,
} from "@/openrouter/schema"; } from "@/openrouter/schema";
import { logger } from "@/utils/logger"; import { logger } from "@/utils/logger";
import { factExtractor } from "./factExtractor"; import { BadRequestResponseError } from "@openrouter/sdk/models/errors";
import type {
ChatAssistantMessage,
ChatChoice,
ChatFunctionTool,
ChatMessages,
} from "@openrouter/sdk/models";
import { BrainDBManager, brainManager, type BrainItem } from "./manager"; import { BrainDBManager, brainManager, type BrainItem } from "./manager";
import { MemoryStub } from "./stub";
import { import {
translateMessageHistory, translateMessageHistory,
type MessageHistoryEntry, type MessageHistoryEntry,
@@ -31,13 +33,7 @@ import {
nextMonth, nextMonth,
pad2, pad2,
} from "./schedule"; } from "./schedule";
import { BadRequestResponseError } from "@openrouter/sdk/models/errors"; import type { FactInput, FactMetadata, SearchHit, Space } from "./types";
import type {
ChatAssistantMessage,
ChatChoice,
ChatFunctionTool,
ChatMessages,
} from "@openrouter/sdk/models";
export interface DebugOptions { export interface DebugOptions {
personality: string; personality: string;
@@ -47,169 +43,96 @@ export interface BrainCreateResult {
brain: Brain; brain: Brain;
description: string; description: string;
baseSystemPrompt: string; baseSystemPrompt: string;
/**
* Raw facts as returned by `factExtractor.extract(description)`. Populated
* only when `Brain.create` is called with `debug: true`; in production
* (the default), facts are persisted via `db.ingestStatements` which does
* not surface the raw extractor output to the caller.
*/
extractedFacts?: ExtractedFact[];
} }
export class Brain { export class Brain {
private availabilityCache: Map<string, AvailabilityWindows> = new Map(); private availabilityCache: Map<string, AvailabilityWindows> = new Map();
private embeddingProvider: EmbeddingProvider;
constructor( constructor(
public db: IdentityDB, public db: Supermemory | MemoryStub,
public space: Space, public space: Space,
public brainbase: BrainItem, public brainbase: BrainItem,
public debug: boolean = false, public debug: boolean = false,
embeddingProvider?: EmbeddingProvider, ) {}
) {
this.embeddingProvider = // ---------------------------------------------------------------------------
embeddingProvider ?? new OpenRouterEmbeddingProvider(); // Memory primitives — thin wrappers over supermemory's `documents` API.
//
// containerTag = space.name
// customId = the stable lookup key (e.g. "daily-schedule:2026-06-10")
// content = the fact text or JSON-encoded schedule
// metadata = filterable bag: { kind, source, ... }
// ---------------------------------------------------------------------------
async add(input: FactInput): Promise<{ id: string }> {
const response = await this.db.documents.add({
content: input.content,
containerTag: this.space.name,
customId: input.customId,
metadata: input.metadata,
});
return { id: response.id };
} }
async get(
customId: string,
): Promise<{ content: string; metadata: FactMetadata | null } | null> {
const listed = await this.db.documents.list({
containerTags: [this.space.name],
limit: 200,
});
const match = (listed.memories ?? []).find((m) => m.customId === customId);
if (!match) return null;
const full = await this.db.documents.get(match.id);
return {
content: full.content ?? "",
metadata: (full.metadata ?? null) as FactMetadata | null,
};
}
async list(): Promise<Array<{ customId: string | null; content: string }>> {
const listed = await this.db.documents.list({
containerTags: [this.space.name],
limit: 200,
});
return (listed.memories ?? []).map((d) => ({
customId: d.customId,
content: d.content ?? "",
}));
}
async search(query: string, limit = 5): Promise<SearchHit[]> {
const response = await this.db.search.execute({
q: query,
containerTag: this.space.name,
limit,
onlyMatchingChunks: true,
});
return (response.results ?? []).map((r) => {
const firstChunk = r.chunks?.[0];
return {
content: firstChunk?.content ?? "",
score: r.score,
};
});
}
// ---------------------------------------------------------------------------
// Domain methods
// ---------------------------------------------------------------------------
async createDailySchedule( async createDailySchedule(
datetime: Date, datetime: Date,
message: string, message: string,
): Promise<DailySchedule | null> { ): Promise<DailySchedule | null> {
try { return await runCreateDailyScheduleSteps(this, datetime, message, noopRunner);
const target = nextDay(datetime);
const dateKey = formatDateKey(target);
const topicName = `daily-schedule:${dateKey}`;
const monthlySummary = await this.getMonthlySummaryForDay(target);
const history = await this.getHistoryFacts();
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 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,
});
if (!this.debug) {
const fact = await this.db.addFact({
spaceName: this.space.name,
statement: JSON.stringify(schedule),
summary: `Daily schedule for ${dateKey} (${schedule.items.length} slots)`,
source: "createDailySchedule",
confidence: 1.0,
topics: [
{
name: topicName,
category: "temporal",
granularity: "concrete",
role: "schedule",
},
{
name: "daily-schedule",
category: "concept",
granularity: "abstract",
role: "schedule",
},
{
name: dateKey,
category: "temporal",
granularity: "concrete",
role: "date",
},
],
});
await this.indexFactEmbeddingFor(fact);
}
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> {
try { return await runCreateMonthlyScheduleSteps(this, datetime, message, noopRunner);
const next = nextMonth(datetime);
const monthKey = `${next.year}-${pad2(next.month + 1)}`;
const topicName = `monthly-schedule:${monthKey}`;
const history = await this.getHistoryFacts();
const instruction = await loadPrompt("MONTHLY_SCHEDULE");
const promptMessage = [
`Target month: ${monthKey} (${next.daysInMonth} days)`,
`Personality: ${this.brainbase.baseSystemPrompt}`,
`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,
});
if (!this.debug) {
const fact = await this.db.addFact({
spaceName: this.space.name,
statement: JSON.stringify(schedule),
summary: `Monthly schedule for ${monthKey} (${schedule.items.length} days)`,
source: "createMonthlySchedule",
confidence: 1.0,
topics: [
{
name: topicName,
category: "temporal",
granularity: "concrete",
role: "schedule",
},
{
name: "monthly-schedule",
category: "concept",
granularity: "abstract",
role: "schedule",
},
{
name: monthKey,
category: "temporal",
granularity: "concrete",
role: "period",
},
],
});
await this.indexFactEmbeddingFor(fact);
}
return schedule;
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.error(`createMonthlySchedule failed: ${reason}`);
return null;
}
} }
async getTodayScheduledAvailability( async getTodayScheduledAvailability(
@@ -220,20 +143,10 @@ export class Brain {
const cached = this.availabilityCache.get(dateKey); const cached = this.availabilityCache.get(dateKey);
if (cached) return cached; if (cached) return cached;
if (this.debug) { const stored = await this.get(`daily-schedule:${dateKey}`);
logger.warn( if (!stored) return null;
"getTodayScheduledAvailability requires a persisted daily schedule; debug brains have no DB. Use deriveAvailabilityFromSchedule(schedule) instead.",
);
return null;
}
const topicName = `daily-schedule:${dateKey}`; const dailySchedule = JSON.parse(stored.content) as DailySchedule;
const facts = await this.db.getTopicFacts(topicName, {
spaceName: this.space.name,
});
if (facts.length === 0) return null;
const dailySchedule = JSON.parse(facts[0]!.statement) as DailySchedule;
const availability = const availability =
await this.deriveAvailabilityFromSchedule(dailySchedule); await this.deriveAvailabilityFromSchedule(dailySchedule);
@@ -246,6 +159,30 @@ export class Brain {
} }
} }
async getCurrentAndAdjacentSlots(now: Date): Promise<DailySlot[]> {
const dateKey = formatDateKey(now);
const stored = await this.get(`daily-schedule:${dateKey}`);
if (!stored) return [];
let schedule: DailySchedule;
try {
schedule = JSON.parse(stored.content) as DailySchedule;
} catch {
return [];
}
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const toMinutes = (hhmm: string): number => {
const [h = 0, m = 0] = hhmm.split(":").map((x) => parseInt(x, 10));
return h * 60 + m;
};
const index = schedule.items.findIndex(
(slot) =>
toMinutes(slot.start) <= currentMinutes &&
currentMinutes < toMinutes(slot.end),
);
if (index === -1) return [];
return schedule.items.slice(Math.max(0, index - 1), index + 2);
}
async deriveAvailabilityFromSchedule( async deriveAvailabilityFromSchedule(
schedule: DailySchedule, schedule: DailySchedule,
): Promise<AvailabilityWindows> { ): Promise<AvailabilityWindows> {
@@ -273,45 +210,6 @@ export class Brain {
this.availabilityCache.clear(); this.availabilityCache.clear();
} }
/**
* Embeds a single fact in the embedding table. Called automatically by
* Brain methods that add facts (createDailySchedule, createMonthlySchedule,
* Brain.create). Callers who add facts via `db.addFact` directly should
* invoke this so the LLM can recall the fact via `searchIdentityDB`. A
* no-op in debug mode (where there is no persisted state).
*/
async indexFactEmbeddingFor(fact: { id: string }): Promise<void> {
if (this.debug) return;
try {
await this.db.indexFactEmbedding(fact.id, {
spaceName: this.space.name,
provider: this.embeddingProvider,
});
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.warn(`indexFactEmbeddingFor(${fact.id}) failed: ${reason}`);
}
}
/**
* Backfills embeddings for every fact in this brain's space. Intended
* for `Brain.create` and `Brain.load` — runs once at initialization so
* facts added by older code paths (or out-of-band) become searchable.
* No-op in debug mode and when the space has no facts.
*/
async initializeEmbeddings(): Promise<void> {
if (this.debug) return;
try {
await this.db.indexFactEmbeddings({
spaceName: this.space.name,
provider: this.embeddingProvider,
});
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.warn(`initializeEmbeddings failed: ${reason}`);
}
}
async sendMessage( async sendMessage(
history: ReadonlyArray<MessageHistoryEntry>, history: ReadonlyArray<MessageHistoryEntry>,
newMessages: ReadonlyArray<MessageHistoryEntry>, newMessages: ReadonlyArray<MessageHistoryEntry>,
@@ -394,7 +292,7 @@ export class Brain {
}); });
continue; continue;
} }
if (call.function.name === "searchIdentityDB") { if (call.function.name === "searchMemory") {
const result = await this.executeSearchTool(call.function.arguments); const result = await this.executeSearchTool(call.function.arguments);
messages.push({ messages.push({
role: "tool", role: "tool",
@@ -415,7 +313,7 @@ export class Brain {
if ( if (
!hasContent && !hasContent &&
toolCalls.every((c) => c.function.name === "searchIdentityDB") toolCalls.every((c) => c.function.name === "searchMemory")
) { ) {
continue; continue;
} }
@@ -433,14 +331,23 @@ export class Brain {
} }
private async buildScheduleBlock(now: Date): Promise<string> { private async buildScheduleBlock(now: Date): Promise<string> {
const dateKey = formatDateKey(now);
const currentSlots = await this.getCurrentAndAdjacentSlots(now);
const currentBlock = currentSlots.length > 0
? `Currently (around ${now.toTimeString().slice(0, 5)}):\n${currentSlots
.map(
(s) =>
` ${s.start}-${s.end} ${s.activity}${s.notes ? ` (${s.notes})` : ""}`,
)
.join("\n")}`
: `Currently (${dateKey} ${now.toTimeString().slice(0, 5)}): (no matching slot in today's schedule)`;
const days: { label: string; date: Date }[] = [ const days: { label: string; date: Date }[] = [
{ {
label: "Yesterday", label: "Yesterday",
date: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1), date: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1),
}, },
{ label: "Today", date: now }, { label: "Tomorrow",
{
label: "Tomorrow",
date: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1), date: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1),
}, },
]; ];
@@ -452,19 +359,16 @@ export class Brain {
`${label} (${key}): ${summary ?? "(no daily schedule on file)"}`, `${label} (${key}): ${summary ?? "(no daily schedule on file)"}`,
); );
} }
return `Schedule context:\n${blocks.join("\n")}`; return `Schedule context:\n${currentBlock}\n\n${blocks.join("\n")}`;
} }
private async getDailyScheduleSummary( private async getDailyScheduleSummary(
dateKey: string, dateKey: string,
): Promise<string | null> { ): Promise<string | null> {
if (this.debug) return null;
try { try {
const facts = await this.db.getTopicFacts(`daily-schedule:${dateKey}`, { const stored = await this.get(`daily-schedule:${dateKey}`);
spaceName: this.space.name, if (!stored) return null;
}); const schedule = JSON.parse(stored.content) as DailySchedule;
if (facts.length === 0) return null;
const schedule = JSON.parse(facts[0]!.statement) as DailySchedule;
const first = schedule.items[0]; const first = schedule.items[0];
const last = schedule.items[schedule.items.length - 1]; const last = schedule.items[schedule.items.length - 1];
if (!first || !last) return null; if (!first || !last) return null;
@@ -481,15 +385,9 @@ export class Brain {
return JSON.stringify({ ok: false, error: "missing query" }); return JSON.stringify({ ok: false, error: "missing query" });
} }
try { try {
const hits = await this.db.searchFacts({ const hits = await this.search(query, 5);
spaceName: this.space.name,
query,
provider: this.embeddingProvider,
limit: 5,
});
const compact = hits.map((hit) => ({ const compact = hits.map((hit) => ({
statement: hit.statement, content: hit.content,
summary: hit.summary,
score: hit.score, score: hit.score,
})); }));
return JSON.stringify({ ok: true, hits: compact }); return JSON.stringify({ ok: true, hits: compact });
@@ -499,17 +397,13 @@ export class Brain {
} }
} }
private async getMonthlySummaryForDay(target: Date): Promise<string | null> { async getMonthlySummaryForDay(target: Date): Promise<string | null> {
if (this.debug) return null;
try { try {
const monthKey = formatMonthKey(target); const monthKey = formatMonthKey(target);
const topicName = `monthly-schedule:${monthKey}`; const stored = await this.get(`monthly-schedule:${monthKey}`);
const facts = await this.db.getTopicFacts(topicName, { if (!stored) return null;
spaceName: this.space.name,
});
if (facts.length === 0) return null;
const monthly = JSON.parse(facts[0]!.statement) as MonthlySchedule; const monthly = JSON.parse(stored.content) as MonthlySchedule;
const day = target.getDate(); const day = target.getDate();
const entry = monthly.items.find((d) => d.day === day); const entry = monthly.items.find((d) => d.day === day);
return entry?.summary ?? null; return entry?.summary ?? null;
@@ -518,21 +412,13 @@ export class Brain {
} }
} }
private async getHistoryFacts(): Promise<string> { async getHistoryFacts(): Promise<string> {
if (this.debug) return "";
try { try {
const topics = await this.db.listTopics({ const docs = await this.list();
spaceName: this.space.name, return docs
includeFacts: true, .map((d) => d.content)
}); .slice(-30)
const statements: string[] = []; .join("\n");
for (const topic of topics) {
const t = topic as { facts?: Array<{ statement: string }> };
if (t.facts) {
for (const f of t.facts) statements.push(f.statement);
}
}
return statements.slice(-30).join("\n");
} catch { } catch {
return ""; return "";
} }
@@ -541,26 +427,237 @@ export class Brain {
static async create( static async create(
displayName: string, displayName: string,
seed: string, seed: string,
options: { options: { braindbPath?: string; db?: Supermemory | MemoryStub } = {},
dbPath?: string;
braindbPath?: string;
debug?: boolean;
embeddingProvider?: EmbeddingProvider;
} = {},
): Promise<BrainCreateResult | null> { ): Promise<BrainCreateResult | null> {
const dbPath = options.dbPath ?? config.dbPath; return await runCreateSteps(displayName, seed, options, noopRunner);
}
static async load(brainId: string): Promise<Brain | null> {
const brainbase = await brainManager.loadBrain(brainId);
if (!brainbase) return null;
const db = new Supermemory({ apiKey: config.supermemoryApiKey });
const space: Space = { name: brainbase.spaceName };
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 {
runner.start("gathering context");
const target = nextDay(datetime);
const dateKey = formatDateKey(target);
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 {
runner.start("gathering context");
const next = nextMonth(datetime);
const monthKey = `${next.year}-${pad2(next.month + 1)}`;
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 const manager = options.braindbPath
? new BrainDBManager(options.braindbPath) ? new BrainDBManager(options.braindbPath)
: brainManager; : brainManager;
const embeddingProvider =
options.embeddingProvider ?? new OpenRouterEmbeddingProvider();
try { try {
runner.start("generating persona description (PERSONA_INIT)");
const personaInitInstruction = await loadPrompt("PERSONA_INIT"); const personaInitInstruction = await loadPrompt("PERSONA_INIT");
const description = await llm.call<string>(llm.models.identity, { const description = await llm.call<string>(llm.models.identity, {
instruction: personaInitInstruction, instruction: personaInitInstruction,
message: seed, message: seed,
}); });
runner.done(snippet80(description));
runner.start(
"generating base system prompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)",
);
const personaSystemInstruction = await loadPrompt( const personaSystemInstruction = await loadPrompt(
"PERSONA_BASE_SYSTEM_PROMPT", "PERSONA_BASE_SYSTEM_PROMPT",
); );
@@ -576,99 +673,53 @@ export class Brain {
"PERSONA_BASE_SYSTEM_PROMPT_FIXED", "PERSONA_BASE_SYSTEM_PROMPT_FIXED",
); );
const baseSystemPrompt = `${generatedBaseSystemPrompt}\n\n${personaSystemFixed}`; const baseSystemPrompt = `${generatedBaseSystemPrompt}\n\n${personaSystemFixed}`;
runner.done(snippet80(baseSystemPrompt));
const db = await IdentityDB.connect({ const db =
client: "sqlite", options.db ?? new Supermemory({ apiKey: config.supermemoryApiKey });
filename: dbPath,
});
await db.initialize();
const brainId = randomUUID(); const brainId = randomUUID();
const spaceName = `brain:${brainId}`; const space: Space = {
const space = await db.upsertSpace({ name: `brain:${brainId}`,
name: spaceName,
description: displayName, description: displayName,
};
const brain = new Brain(db, space, {
brainId,
spaceName: space.name,
displayName,
baseSystemPrompt,
}); });
let extractedFacts: ExtractedFact[] | undefined; runner.start("persisting persona document");
if (options.debug) { await brain.add({
extractedFacts = await factExtractor.extract(description); customId: "persona",
for (const fact of extractedFacts) { content: description,
const created = await db.addFact({ metadata: { kind: "persona", source: "persona-init" },
spaceName,
statement: fact.statement ?? description,
summary: fact.summary,
source: fact.source,
confidence: fact.confidence,
topics: fact.topics,
metadata: fact.metadata,
}); });
await db.indexFactEmbedding(created.id, { runner.done(`customId=persona, contentLength=${description.length}`);
spaceName,
provider: embeddingProvider,
});
}
} else {
await db.ingestStatements(description, {
extractor: factExtractor,
embeddingProvider,
spaceName,
});
}
runner.start("saving braindb index");
const brainbase: BrainItem = { const brainbase: BrainItem = {
brainId, brainId,
spaceName, spaceName: space.name,
displayName, displayName,
baseSystemPrompt, baseSystemPrompt,
}; };
await manager.saveBrain(brainId, brainbase); await manager.saveBrain(brainId, brainbase);
runner.done(`brainId=${brainId}`);
const brain = new Brain(db, space, brainbase, false, embeddingProvider); return { brain, description, baseSystemPrompt };
return { brain, description, baseSystemPrompt, extractedFacts };
} catch (error) { } catch (error) {
const reason = error instanceof Error ? error.message : String(error); const reason = error instanceof Error ? error.message : String(error);
logger.error(`Failed to create brain "${displayName}": ${reason}`); logger.error(`Failed to create brain "${displayName}": ${reason}`);
runner.fail(reason);
return null; return null;
} }
} }
static async load(brainId: string): Promise<Brain | null> { function snippet80(text: string): string {
const brain = await brainManager.loadBrain(brainId); const flat = text.replace(/\s+/g, " ").trim();
if (!brain) return null; return flat.length > 80 ? `${flat.slice(0, 77)}...` : flat;
const db = await IdentityDB.connect({
client: "sqlite",
filename: config.dbPath,
});
const space = await db.getSpaceByName(brain.spaceName);
if (!space) return null;
const brainInstance = new Brain(db, space, brain);
await brainInstance.initializeEmbeddings();
return brainInstance;
}
static async createDebug(options: DebugOptions): Promise<Brain> {
const db = await IdentityDB.connect({
client: "sqlite",
filename: ":memory:",
});
await db.initialize();
const space = await db.upsertSpace({
name: "debug",
description: "Debug Brain",
});
const brainbase: BrainItem = {
brainId: "debug",
spaceName: "debug",
displayName: "Debug Brain",
baseSystemPrompt: options.personality,
};
return new Brain(db, space, brainbase, true);
}
} }
function formatDatetime(now: Date): string { function formatDatetime(now: Date): string {
@@ -699,9 +750,9 @@ function buildSendMessageTools(): ChatFunctionTool[] {
{ {
type: "function", type: "function",
function: { function: {
name: "searchIdentityDB", name: "searchMemory",
description: description:
"Semantic search over the long-term memory of facts about the persona and the user. Returns the most relevant stored statements for a natural-language query.", "Semantic search over the long-term memory of facts about the persona and the user. Returns the most relevant stored content for a natural-language query.",
parameters: { parameters: {
type: "object", type: "object",
additionalProperties: false, additionalProperties: false,

148
src/brain/stub.ts Normal file
View File

@@ -0,0 +1,148 @@
/**
* 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,
};
},
};
}

26
src/brain/types.ts Normal file
View File

@@ -0,0 +1,26 @@
/** A brain's logical namespace. Maps to supermemory's `containerTag`. */
export interface Space {
name: string;
description?: string;
}
/** Metadata bag stored alongside a document in supermemory. */
export type FactMetadata = Record<string, string | number | boolean | string[]>;
/**
* A fact/document to store in the brain's memory. Mirrors supermemory's
* `documents.add` parameter shape: `content` is the canonical text (or
* JSON-encoded schedule), `customId` is the stable lookup key, and
* `metadata` is the filterable bag.
*/
export interface FactInput {
customId: string;
content: string;
metadata?: FactMetadata;
}
/** A single semantic-search hit. */
export interface SearchHit {
content: string;
score: number;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,13 +3,16 @@ import { join } from "path";
export interface Config { export interface Config {
openrouterApiKey: string; openrouterApiKey: string;
dbPath: string; supermemoryApiKey: string;
braindbPath: string; braindbPath: string;
} }
const openrouterApiKey = process.env["OPENROUTER_API_KEY"]; const openrouterApiKey = process.env["OPENROUTER_API_KEY"];
if (!openrouterApiKey) throw new Error("OPENROUTER_API_KEY is missing"); if (!openrouterApiKey) throw new Error("OPENROUTER_API_KEY is missing");
const dbPath = join(process.cwd(), process.env["DB_PATH"] ?? "brainbox.db");
const supermemoryApiKey = process.env["SUPERMEMORY_API_KEY"];
if (!supermemoryApiKey) throw new Error("SUPERMEMORY_API_KEY is missing");
const braindbPath = join( const braindbPath = join(
process.cwd(), process.cwd(),
process.env["BRAINDB_PATH"] ?? "brainbox.json", process.env["BRAINDB_PATH"] ?? "brainbox.json",
@@ -17,6 +20,6 @@ const braindbPath = join(
export const config: Config = { export const config: Config = {
openrouterApiKey, openrouterApiKey,
dbPath, supermemoryApiKey,
braindbPath, braindbPath,
}; };

View File

@@ -1,57 +0,0 @@
import { config } from "@/config";
import { OpenRouter } from "@openrouter/sdk";
import type { EmbeddingProvider } from "identitydb";
export const QWEN_EMBEDDING_MODEL = "qwen/qwen3-embedding-8b" as const;
export const QWEN_EMBEDDING_DIMENSIONS = 512 as const;
export class OpenRouterEmbeddingProvider implements EmbeddingProvider {
readonly model: string = QWEN_EMBEDDING_MODEL;
readonly dimensions: number = QWEN_EMBEDDING_DIMENSIONS;
private client: OpenRouter;
constructor(apiKey: string = config.openrouterApiKey) {
this.client = new OpenRouter({ apiKey, appTitle: "boxbrain" });
}
async embed(input: string): Promise<number[]> {
const result = await this.embedBatch([input]);
return result[0]!;
}
async embedMany(inputs: string[]): Promise<number[][]> {
if (inputs.length === 0) return [];
return await this.embedBatch(inputs);
}
private async embedBatch(inputs: string[]): Promise<number[][]> {
const response = await this.client.embeddings.generate({
requestBody: {
model: this.model,
input: inputs,
dimensions: this.dimensions,
encodingFormat: "float",
},
});
if (typeof response === "string") {
throw new Error("OpenRouter returned a non-JSON embeddings response");
}
const ordered = new Array<number[]>(inputs.length);
for (const item of response.data) {
if (typeof item.embedding === "string") {
throw new Error(
"OpenRouter returned a base64 embedding but float was requested",
);
}
const index = item.index ?? 0;
ordered[index] = item.embedding;
}
for (let i = 0; i < ordered.length; i += 1) {
if (!ordered[i]) {
throw new Error(`OpenRouter omitted embedding for input index ${i}`);
}
}
return ordered;
}
}

View File

@@ -1,54 +1,3 @@
export const extractedFactSchema = {
type: "object",
additionalProperties: false,
properties: {
items: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: {
statement: { type: "string" },
summary: { type: "string" },
source: { type: "string" },
confidence: { type: "number" },
metadata: { type: "object", additionalProperties: false },
topics: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: {
name: { type: "string" },
category: {
type: "string",
enum: ["entity", "concept", "temporal", "custom"],
},
granularity: {
type: "string",
enum: ["abstract", "concrete", "mixed"],
},
role: { type: "string" },
description: { type: "string" },
metadata: { type: "object", additionalProperties: false },
},
required: [
"name",
"category",
"granularity",
"role",
"description",
],
},
},
},
required: ["statement", "summary", "source", "confidence", "topics"],
},
},
},
required: ["items"],
};
const timeString = { const timeString = {
type: "string", type: "string",
pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$", pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$",
@@ -133,9 +82,6 @@ export const availabilitySchema = {
// Types — co-located with their schemas. // Types — co-located with their schemas.
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
import type { ExtractedFact } from "identitydb";
/** A single 30-minute slot in a daily schedule. Matches `dailyScheduleSchema.items.items`. */
export type DailySlot = { export type DailySlot = {
start: string; start: string;
end: string; end: string;
@@ -143,52 +89,27 @@ export type DailySlot = {
notes: string; notes: string;
}; };
/**
* A complete daily schedule: a wrapped object containing exactly 48 half-hour
* slots. Matches `dailyScheduleSchema` (the LLM is constrained to return the
* `{ items: [...] }` envelope).
*/
export type DailySchedule = { export type DailySchedule = {
items: DailySlot[]; items: DailySlot[];
}; };
/** A single day's summary inside a monthly schedule. Matches `monthlyScheduleSchema.items.items`. */
export type MonthlyDay = { export type MonthlyDay = {
day: number; day: number;
summary: string; summary: string;
}; };
/**
* A complete monthly schedule: a wrapped object containing one entry per day
* of the month. Matches `monthlyScheduleSchema`.
*/
export type MonthlySchedule = { export type MonthlySchedule = {
items: MonthlyDay[]; items: MonthlyDay[];
}; };
/** Reachability status for a single availability window. */
export type AvailabilityStatus = "online" | "do-not-disturb" | "offline"; export type AvailabilityStatus = "online" | "do-not-disturb" | "offline";
/** A single availability window. Matches `availabilitySchema.items.items`. */
export type Availability = { export type Availability = {
start: string; start: string;
end: string; end: string;
status: AvailabilityStatus; status: AvailabilityStatus;
}; };
/**
* The full set of availability windows for a day: a wrapped object containing
* one or more windows. Matches `availabilitySchema`.
*/
export type AvailabilityWindows = { export type AvailabilityWindows = {
items: Availability[]; items: Availability[];
}; };
/**
* The wrapped envelope the LLM returns for `extractedFactSchema`. The inner
* `items` array is the list of `ExtractedFact` (which is defined in the
* external `identitydb` package).
*/
export type ExtractedFactResult = {
items: ExtractedFact[];
};