@soederpop/luca 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. package/uv.lock +8 -0
@@ -0,0 +1,497 @@
1
+ import { z } from 'zod'
2
+ import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
3
+ import type { Container } from '@soederpop/luca/container'
4
+ import { type AvailableFeatures } from '@soederpop/luca/feature'
5
+ import { features, Feature } from '@soederpop/luca/feature'
6
+ import { NodeContainer, type DiskCache, type NodeFeatures } from '@soederpop/luca/node/container'
7
+ import type { Message } from './conversation'
8
+
9
+ declare module '@soederpop/luca/feature' {
10
+ interface AvailableFeatures {
11
+ conversationHistory: typeof ConversationHistory
12
+ }
13
+ }
14
+
15
+ export interface ConversationRecord {
16
+ id: string
17
+ title: string
18
+ model: string
19
+ messages: Message[]
20
+ tags: string[]
21
+ thread: string
22
+ createdAt: string
23
+ updatedAt: string
24
+ messageCount: number
25
+ metadata: Record<string, any>
26
+ }
27
+
28
+ export type ConversationMeta = Omit<ConversationRecord, 'messages'>
29
+
30
+ export interface SearchOptions {
31
+ tag?: string
32
+ tags?: string[]
33
+ thread?: string
34
+ model?: string
35
+ before?: string | Date
36
+ after?: string | Date
37
+ query?: string
38
+ limit?: number
39
+ offset?: number
40
+ }
41
+
42
+ export const ConversationHistoryOptionsSchema = FeatureOptionsSchema.extend({
43
+ cachePath: z.string().optional().describe('Custom cache directory for conversation storage'),
44
+ namespace: z.string().optional().describe('Namespace prefix for cache keys to isolate datasets'),
45
+ })
46
+
47
+ export const ConversationHistoryStateSchema = FeatureStateSchema.extend({
48
+ conversationCount: z.number().describe('Total number of stored conversations'),
49
+ lastSaved: z.string().optional().describe('ISO timestamp of the last save operation'),
50
+ })
51
+
52
+ export type ConversationHistoryOptions = z.infer<typeof ConversationHistoryOptionsSchema>
53
+ export type ConversationHistoryState = z.infer<typeof ConversationHistoryStateSchema>
54
+
55
+ /**
56
+ * Persists conversations to disk using the diskCache feature (cacache).
57
+ * Each conversation is stored as a JSON blob keyed by ID, with metadata
58
+ * stored alongside for efficient listing and search without loading full message arrays.
59
+ *
60
+ * @extends Feature
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const history = container.feature('conversationHistory', {
65
+ * namespace: 'my-app',
66
+ * cachePath: '/tmp/conversations'
67
+ * })
68
+ *
69
+ * // Create and retrieve conversations
70
+ * const record = await history.create({ messages, title: 'My Chat' })
71
+ * const loaded = await history.load(record.id)
72
+ *
73
+ * // Search and filter
74
+ * const results = await history.search({ tag: 'important', limit: 10 })
75
+ * ```
76
+ */
77
+ export class ConversationHistory extends Feature<ConversationHistoryState, ConversationHistoryOptions> {
78
+ static override stateSchema = ConversationHistoryStateSchema
79
+ static override optionsSchema = ConversationHistoryOptionsSchema
80
+ static override shortcut = 'features.conversationHistory' as const
81
+
82
+ static attach(container: Container<AvailableFeatures, any>) {
83
+ features.register('conversationHistory', ConversationHistory)
84
+ return container
85
+ }
86
+
87
+ /** @returns Default state with zero conversations and no last-saved timestamp. */
88
+ override get initialState(): ConversationHistoryState {
89
+ return {
90
+ ...super.initialState,
91
+ conversationCount: 0,
92
+ lastSaved: undefined,
93
+ }
94
+ }
95
+
96
+ /** @returns The parent NodeContainer, narrowed from the base Container type. */
97
+ override get container() {
98
+ return super.container as NodeContainer<NodeFeatures, any>
99
+ }
100
+
101
+ /** @returns The diskCache feature instance used for persistence, configured with the optional cachePath. */
102
+ get diskCache(): DiskCache {
103
+ const opts: Record<string, any> = {}
104
+ if (this.options.cachePath) {
105
+ opts.path = this.options.cachePath
106
+ }
107
+ return this.container.feature('diskCache', opts) as DiskCache
108
+ }
109
+
110
+ /** @returns The namespace prefix used for all cache keys, defaults to 'conversation-history'. */
111
+ get namespace(): string {
112
+ return this.options.namespace || 'conversation-history'
113
+ }
114
+
115
+ private buildCacheKey(id: string): string {
116
+ return `${this.namespace}:${id}`
117
+ }
118
+
119
+ private metaKey(id: string): string {
120
+ return `${this.namespace}:meta:${id}`
121
+ }
122
+
123
+ private indexKey(): string {
124
+ return `${this.namespace}:__index__`
125
+ }
126
+
127
+ /**
128
+ * Save a conversation. Creates or overwrites by ID.
129
+ *
130
+ * @param {ConversationRecord} record - The full conversation record to persist
131
+ * @returns {Promise<void>}
132
+ */
133
+ async save(record: ConversationRecord): Promise<void> {
134
+ record.updatedAt = new Date().toISOString()
135
+ record.messageCount = record.messages.length
136
+
137
+ // store the full conversation (messages included)
138
+ await this.diskCache.set(this.buildCacheKey(record.id), record)
139
+
140
+ // store lightweight metadata separately for fast listing
141
+ const meta: ConversationMeta = { ...record }
142
+ delete (meta as any).messages
143
+ await this.diskCache.set(this.metaKey(record.id), meta)
144
+
145
+ // update the index
146
+ await this.addToIndex(record.id)
147
+
148
+ this.state.set('lastSaved', record.updatedAt)
149
+ this.emit('saved', record.id)
150
+ }
151
+
152
+ /**
153
+ * Create a new conversation from messages, returning the saved record.
154
+ *
155
+ * @param {object} opts - Creation options including messages, optional title, model, tags, thread, and metadata
156
+ * @returns {Promise<ConversationRecord>} The newly created and persisted conversation record
157
+ */
158
+ async create(opts: {
159
+ id?: string
160
+ title?: string
161
+ model?: string
162
+ messages: Message[]
163
+ tags?: string[]
164
+ thread?: string
165
+ metadata?: Record<string, any>
166
+ }): Promise<ConversationRecord> {
167
+ const now = new Date().toISOString()
168
+ const record: ConversationRecord = {
169
+ id: opts.id || crypto.randomUUID(),
170
+ title: opts.title || 'Untitled',
171
+ model: opts.model || 'unknown',
172
+ messages: opts.messages,
173
+ tags: opts.tags || [],
174
+ thread: opts.thread || 'default',
175
+ createdAt: now,
176
+ updatedAt: now,
177
+ messageCount: opts.messages.length,
178
+ metadata: opts.metadata || {},
179
+ }
180
+
181
+ await this.save(record)
182
+ return record
183
+ }
184
+
185
+ /**
186
+ * Load a full conversation by ID, including all messages.
187
+ *
188
+ * @param {string} id - The conversation ID
189
+ * @returns {Promise<ConversationRecord | null>} The full record, or null if not found
190
+ */
191
+ async load(id: string): Promise<ConversationRecord | null> {
192
+ const exists = await this.diskCache.has(this.buildCacheKey(id))
193
+ if (!exists) return null
194
+ return this.diskCache.get(this.buildCacheKey(id), true)
195
+ }
196
+
197
+ /**
198
+ * Load just the metadata for a conversation (no messages).
199
+ *
200
+ * @param {string} id - The conversation ID
201
+ * @returns {Promise<ConversationMeta | null>} The lightweight metadata record, or null if not found
202
+ */
203
+ async getMeta(id: string): Promise<ConversationMeta | null> {
204
+ const exists = await this.diskCache.has(this.metaKey(id))
205
+ if (!exists) return null
206
+ return this.diskCache.get(this.metaKey(id), true)
207
+ }
208
+
209
+ /**
210
+ * Append messages to an existing conversation.
211
+ *
212
+ * @param {string} id - The conversation ID to append to
213
+ * @param {Message[]} messages - The messages to append
214
+ * @returns {Promise<ConversationRecord | null>} The updated record, or null if the conversation was not found
215
+ */
216
+ async append(id: string, messages: Message[]): Promise<ConversationRecord | null> {
217
+ const record = await this.load(id)
218
+ if (!record) return null
219
+
220
+ record.messages.push(...messages)
221
+ await this.save(record)
222
+ return record
223
+ }
224
+
225
+ /**
226
+ * Delete a conversation by ID.
227
+ *
228
+ * @param {string} id - The conversation ID to delete
229
+ * @returns {Promise<boolean>} True if the conversation existed and was deleted
230
+ */
231
+ async delete(id: string): Promise<boolean> {
232
+ const exists = await this.diskCache.has(this.buildCacheKey(id))
233
+ if (!exists) return false
234
+
235
+ await this.diskCache.rm(this.buildCacheKey(id))
236
+ await this.diskCache.rm(this.metaKey(id)).catch(() => {})
237
+ await this.removeFromIndex(id)
238
+
239
+ this.emit('deleted', id)
240
+ return true
241
+ }
242
+
243
+ /**
244
+ * List all conversation metadata, with optional search/filter.
245
+ * Loads only the lightweight meta records, never the full messages.
246
+ *
247
+ * @param {SearchOptions} [options] - Optional filters for tag, thread, model, date range, and text query
248
+ * @returns {Promise<ConversationMeta[]>} Filtered and sorted metadata records (newest first)
249
+ */
250
+ async list(options?: SearchOptions): Promise<ConversationMeta[]> {
251
+ const ids = await this.getIndex()
252
+ const metas: ConversationMeta[] = []
253
+
254
+ for (const id of ids) {
255
+ const meta = await this.getMeta(id)
256
+ if (meta) metas.push(meta)
257
+ }
258
+
259
+ return this.applyFilters(metas, options)
260
+ }
261
+
262
+ /**
263
+ * Search conversations by text query across titles, tags, and metadata.
264
+ * Also supports filtering by tag, thread, model, and date range.
265
+ *
266
+ * @param {SearchOptions} options - Search and filter criteria
267
+ * @returns {Promise<ConversationMeta[]>} Matching metadata records (newest first)
268
+ */
269
+ async search(options: SearchOptions): Promise<ConversationMeta[]> {
270
+ return this.list(options)
271
+ }
272
+
273
+ /**
274
+ * Get all unique tags across all conversations.
275
+ *
276
+ * @returns {Promise<string[]>} Sorted array of unique tag strings
277
+ */
278
+ async allTags(): Promise<string[]> {
279
+ const metas = await this.list()
280
+ const tags = new Set<string>()
281
+ for (const meta of metas) {
282
+ for (const tag of meta.tags) {
283
+ tags.add(tag)
284
+ }
285
+ }
286
+ return [...tags].sort()
287
+ }
288
+
289
+ /**
290
+ * Get all unique threads across all conversations.
291
+ *
292
+ * @returns {Promise<string[]>} Sorted array of unique thread identifiers
293
+ */
294
+ async allThreads(): Promise<string[]> {
295
+ const metas = await this.list()
296
+ const threads = new Set<string>()
297
+ for (const meta of metas) {
298
+ threads.add(meta.thread)
299
+ }
300
+ return [...threads].sort()
301
+ }
302
+
303
+ /**
304
+ * Tag a conversation. Adds tags without duplicates.
305
+ *
306
+ * @param {string} id - The conversation ID
307
+ * @param {...string} tags - One or more tags to add
308
+ * @returns {Promise<boolean>} True if the conversation was found and updated
309
+ */
310
+ async tag(id: string, ...tags: string[]): Promise<boolean> {
311
+ const record = await this.load(id)
312
+ if (!record) return false
313
+
314
+ const tagSet = new Set(record.tags)
315
+ for (const t of tags) tagSet.add(t)
316
+ record.tags = [...tagSet]
317
+
318
+ await this.save(record)
319
+ return true
320
+ }
321
+
322
+ /**
323
+ * Remove tags from a conversation.
324
+ *
325
+ * @param {string} id - The conversation ID
326
+ * @param {...string} tags - One or more tags to remove
327
+ * @returns {Promise<boolean>} True if the conversation was found and updated
328
+ */
329
+ async untag(id: string, ...tags: string[]): Promise<boolean> {
330
+ const record = await this.load(id)
331
+ if (!record) return false
332
+
333
+ const removeSet = new Set(tags)
334
+ record.tags = record.tags.filter(t => !removeSet.has(t))
335
+
336
+ await this.save(record)
337
+ return true
338
+ }
339
+
340
+ /**
341
+ * Update metadata on a conversation without touching messages.
342
+ *
343
+ * @param {string} id - The conversation ID
344
+ * @param {object} updates - Partial updates for title, tags, thread, and/or metadata
345
+ * @returns {Promise<boolean>} True if the conversation was found and updated
346
+ */
347
+ async updateMeta(id: string, updates: Partial<Pick<ConversationRecord, 'title' | 'tags' | 'thread' | 'metadata'>>): Promise<boolean> {
348
+ const record = await this.load(id)
349
+ if (!record) return false
350
+
351
+ if (updates.title !== undefined) record.title = updates.title
352
+ if (updates.tags !== undefined) record.tags = updates.tags
353
+ if (updates.thread !== undefined) record.thread = updates.thread
354
+ if (updates.metadata !== undefined) record.metadata = { ...record.metadata, ...updates.metadata }
355
+
356
+ await this.save(record)
357
+ return true
358
+ }
359
+
360
+ /**
361
+ * Find the most recent conversation for an exact thread ID.
362
+ *
363
+ * @param {string} thread - The exact thread ID to match
364
+ * @returns {Promise<ConversationRecord | null>} The full record with messages, or null if none found
365
+ */
366
+ async findByThread(thread: string): Promise<ConversationRecord | null> {
367
+ const metas = await this.list({ thread })
368
+ if (!metas.length) return null
369
+ return this.load(metas[0]!.id)
370
+ }
371
+
372
+ /**
373
+ * Find all conversations whose thread starts with a prefix.
374
+ *
375
+ * @param {string} prefix - The thread prefix to match
376
+ * @returns {Promise<ConversationMeta[]>} Matching metadata records (newest first)
377
+ */
378
+ async findByThreadPrefix(prefix: string): Promise<ConversationMeta[]> {
379
+ const all = await this.list()
380
+ return all.filter(m => m.thread.startsWith(prefix))
381
+ }
382
+
383
+ /**
384
+ * Delete all conversations for an exact thread.
385
+ *
386
+ * @param {string} thread - The exact thread ID
387
+ * @returns {Promise<number>} Number of conversations deleted
388
+ */
389
+ async deleteThread(thread: string): Promise<number> {
390
+ const metas = await this.list({ thread })
391
+ let count = 0
392
+ for (const meta of metas) {
393
+ if (await this.delete(meta.id)) count++
394
+ }
395
+ return count
396
+ }
397
+
398
+ /**
399
+ * Delete all conversations matching a thread prefix.
400
+ *
401
+ * @param {string} prefix - The thread prefix to match
402
+ * @returns {Promise<number>} Number of conversations deleted
403
+ */
404
+ async deleteByThreadPrefix(prefix: string): Promise<number> {
405
+ const metas = await this.findByThreadPrefix(prefix)
406
+ let count = 0
407
+ for (const meta of metas) {
408
+ if (await this.delete(meta.id)) count++
409
+ }
410
+ return count
411
+ }
412
+
413
+ // -- index management --
414
+
415
+ private async getIndex(): Promise<string[]> {
416
+ const exists = await this.diskCache.has(this.indexKey())
417
+ if (!exists) return []
418
+ return this.diskCache.get(this.indexKey(), true)
419
+ }
420
+
421
+ private async setIndex(ids: string[]): Promise<void> {
422
+ await this.diskCache.set(this.indexKey(), ids)
423
+ this.state.set('conversationCount', ids.length)
424
+ }
425
+
426
+ private async addToIndex(id: string): Promise<void> {
427
+ const ids = await this.getIndex()
428
+ if (!ids.includes(id)) {
429
+ ids.push(id)
430
+ await this.setIndex(ids)
431
+ }
432
+ }
433
+
434
+ private async removeFromIndex(id: string): Promise<void> {
435
+ const ids = await this.getIndex()
436
+ await this.setIndex(ids.filter(i => i !== id))
437
+ }
438
+
439
+ // -- filtering --
440
+
441
+ private applyFilters(metas: ConversationMeta[], options?: SearchOptions): ConversationMeta[] {
442
+ if (!options) return metas
443
+
444
+ let results = metas
445
+
446
+ if (options.tag) {
447
+ results = results.filter(m => m.tags.includes(options.tag!))
448
+ }
449
+
450
+ if (options.tags && options.tags.length) {
451
+ results = results.filter(m => options.tags!.every(t => m.tags.includes(t)))
452
+ }
453
+
454
+ if (options.thread) {
455
+ results = results.filter(m => m.thread === options.thread)
456
+ }
457
+
458
+ if (options.model) {
459
+ results = results.filter(m => m.model === options.model)
460
+ }
461
+
462
+ if (options.after) {
463
+ const after = new Date(options.after).getTime()
464
+ results = results.filter(m => new Date(m.createdAt).getTime() >= after)
465
+ }
466
+
467
+ if (options.before) {
468
+ const before = new Date(options.before).getTime()
469
+ results = results.filter(m => new Date(m.createdAt).getTime() <= before)
470
+ }
471
+
472
+ if (options.query) {
473
+ const q = options.query.toLowerCase()
474
+ results = results.filter(m =>
475
+ m.title.toLowerCase().includes(q) ||
476
+ m.tags.some(t => t.toLowerCase().includes(q)) ||
477
+ m.thread.toLowerCase().includes(q) ||
478
+ JSON.stringify(m.metadata).toLowerCase().includes(q)
479
+ )
480
+ }
481
+
482
+ // sort newest first
483
+ results.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
484
+
485
+ if (options.offset) {
486
+ results = results.slice(options.offset)
487
+ }
488
+
489
+ if (options.limit) {
490
+ results = results.slice(0, options.limit)
491
+ }
492
+
493
+ return results
494
+ }
495
+ }
496
+
497
+ export default features.register('conversationHistory', ConversationHistory)