@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.
- package/CLAUDE.md +71 -0
- package/README.md +78 -0
- package/bun.lock +2928 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-scaffolds.ts +154 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/update-introspection.ts +67 -0
- package/docs/CLI.md +335 -0
- package/docs/README.md +88 -0
- package/docs/TABLE-OF-CONTENTS.md +157 -0
- package/docs/apis/clients/elevenlabs.md +84 -0
- package/docs/apis/clients/graph.md +56 -0
- package/docs/apis/clients/openai.md +69 -0
- package/docs/apis/clients/rest.md +41 -0
- package/docs/apis/clients/websocket.md +107 -0
- package/docs/apis/features/agi/assistant.md +471 -0
- package/docs/apis/features/agi/assistants-manager.md +154 -0
- package/docs/apis/features/agi/claude-code.md +602 -0
- package/docs/apis/features/agi/conversation-history.md +352 -0
- package/docs/apis/features/agi/conversation.md +333 -0
- package/docs/apis/features/agi/docs-reader.md +121 -0
- package/docs/apis/features/agi/openai-codex.md +318 -0
- package/docs/apis/features/agi/openapi.md +138 -0
- package/docs/apis/features/agi/semantic-search.md +387 -0
- package/docs/apis/features/agi/skills-library.md +216 -0
- package/docs/apis/features/node/container-link.md +133 -0
- package/docs/apis/features/node/content-db.md +313 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +651 -0
- package/docs/apis/features/node/docker.md +705 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +59 -0
- package/docs/apis/features/node/file-manager.md +182 -0
- package/docs/apis/features/node/fs.md +581 -0
- package/docs/apis/features/node/git.md +330 -0
- package/docs/apis/features/node/google-auth.md +174 -0
- package/docs/apis/features/node/google-calendar.md +187 -0
- package/docs/apis/features/node/google-docs.md +151 -0
- package/docs/apis/features/node/google-drive.md +225 -0
- package/docs/apis/features/node/google-sheets.md +179 -0
- package/docs/apis/features/node/grep.md +290 -0
- package/docs/apis/features/node/helpers.md +135 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +260 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
- package/docs/apis/features/node/networking.md +281 -0
- package/docs/apis/features/node/nlp.md +133 -0
- package/docs/apis/features/node/opener.md +97 -0
- package/docs/apis/features/node/os.md +118 -0
- package/docs/apis/features/node/package-finder.md +402 -0
- package/docs/apis/features/node/postgres.md +212 -0
- package/docs/apis/features/node/proc.md +430 -0
- package/docs/apis/features/node/process-manager.md +210 -0
- package/docs/apis/features/node/python.md +278 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +673 -0
- package/docs/apis/features/node/secure-shell.md +169 -0
- package/docs/apis/features/node/semantic-search.md +401 -0
- package/docs/apis/features/node/sqlite.md +211 -0
- package/docs/apis/features/node/telegram.md +254 -0
- package/docs/apis/features/node/tts.md +118 -0
- package/docs/apis/features/node/ui.md +703 -0
- package/docs/apis/features/node/vault.md +64 -0
- package/docs/apis/features/node/vm.md +84 -0
- package/docs/apis/features/node/window-manager.md +337 -0
- package/docs/apis/features/node/yaml-tree.md +85 -0
- package/docs/apis/features/node/yaml.md +176 -0
- package/docs/apis/features/web/asset-loader.md +47 -0
- package/docs/apis/features/web/container-link.md +133 -0
- package/docs/apis/features/web/esbuild.md +59 -0
- package/docs/apis/features/web/helpers.md +135 -0
- package/docs/apis/features/web/network.md +30 -0
- package/docs/apis/features/web/speech.md +55 -0
- package/docs/apis/features/web/vault.md +64 -0
- package/docs/apis/features/web/vm.md +84 -0
- package/docs/apis/features/web/voice.md +67 -0
- package/docs/apis/servers/express.md +127 -0
- package/docs/apis/servers/mcp.md +213 -0
- package/docs/apis/servers/websocket.md +99 -0
- package/docs/documentation-audit.md +134 -0
- package/docs/examples/content-db.md +77 -0
- package/docs/examples/disk-cache.md +83 -0
- package/docs/examples/docker.md +101 -0
- package/docs/examples/downloader.md +70 -0
- package/docs/examples/esbuild.md +80 -0
- package/docs/examples/file-manager.md +82 -0
- package/docs/examples/fs.md +83 -0
- package/docs/examples/git.md +85 -0
- package/docs/examples/google-auth.md +88 -0
- package/docs/examples/google-calendar.md +94 -0
- package/docs/examples/google-docs.md +82 -0
- package/docs/examples/google-drive.md +96 -0
- package/docs/examples/google-sheets.md +95 -0
- package/docs/examples/grep.md +85 -0
- package/docs/examples/ink-blocks.md +75 -0
- package/docs/examples/ink-renderer.md +41 -0
- package/docs/examples/ink.md +103 -0
- package/docs/examples/ipc-socket.md +103 -0
- package/docs/examples/json-tree.md +91 -0
- package/docs/examples/launcher-app-command-listener.md +120 -0
- package/docs/examples/networking.md +58 -0
- package/docs/examples/nlp.md +91 -0
- package/docs/examples/opener.md +78 -0
- package/docs/examples/os.md +72 -0
- package/docs/examples/package-finder.md +89 -0
- package/docs/examples/port-exposer.md +89 -0
- package/docs/examples/postgres.md +91 -0
- package/docs/examples/proc.md +81 -0
- package/docs/examples/process-manager.md +79 -0
- package/docs/examples/python.md +91 -0
- package/docs/examples/repl.md +93 -0
- package/docs/examples/runpod.md +119 -0
- package/docs/examples/secure-shell.md +92 -0
- package/docs/examples/sqlite.md +86 -0
- package/docs/examples/telegram.md +77 -0
- package/docs/examples/tts.md +86 -0
- package/docs/examples/ui.md +80 -0
- package/docs/examples/vault.md +70 -0
- package/docs/examples/vm.md +86 -0
- package/docs/examples/window-manager.md +125 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
- package/docs/ideas/container-use-api.md +9 -0
- package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
- package/docs/ideas/feature-stacks.md +22 -0
- package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
- package/docs/ideas/mcp-design.md +9 -0
- package/docs/ideas/web-container-debugging-feature.md +13 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +154 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +38 -0
- package/docs/philosophy.md +85 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/reports/assistant-bugs.md +38 -0
- package/docs/reports/attach-pattern-usage.md +18 -0
- package/docs/reports/code-audit-results.md +391 -0
- package/docs/reports/introspection-audit-tasks.md +378 -0
- package/docs/reports/luca-mcp-improvements.md +128 -0
- package/docs/scaffolds/client.md +140 -0
- package/docs/scaffolds/command.md +106 -0
- package/docs/scaffolds/endpoint.md +176 -0
- package/docs/scaffolds/feature.md +148 -0
- package/docs/scaffolds/server.md +187 -0
- package/docs/tasks/web-container-helper-discovery.md +71 -0
- package/docs/todos.md +1 -0
- package/docs/tutorials/01-getting-started.md +106 -0
- package/docs/tutorials/02-container.md +210 -0
- package/docs/tutorials/03-scripts.md +194 -0
- package/docs/tutorials/04-features-overview.md +196 -0
- package/docs/tutorials/05-state-and-events.md +171 -0
- package/docs/tutorials/06-servers.md +157 -0
- package/docs/tutorials/07-endpoints.md +198 -0
- package/docs/tutorials/08-commands.md +171 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +198 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +147 -0
- package/docs/tutorials/14-type-system.md +174 -0
- package/docs/tutorials/15-project-patterns.md +222 -0
- package/docs/tutorials/16-google-features.md +534 -0
- package/docs/tutorials/17-tui-blocks.md +530 -0
- package/docs/tutorials/18-semantic-search.md +334 -0
- package/index.ts +1 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +154 -0
- package/pyproject.toml +7 -0
- package/scripts/animations/chrome-glitch.ts +55 -0
- package/scripts/animations/index.ts +16 -0
- package/scripts/animations/neon-pulse.ts +64 -0
- package/scripts/animations/types.ts +6 -0
- package/scripts/build-web.ts +28 -0
- package/scripts/examples/ask-luca-expert.ts +42 -0
- package/scripts/examples/assistant-questions.ts +12 -0
- package/scripts/examples/excalidraw-expert.ts +75 -0
- package/scripts/examples/expert-chat.ts +0 -0
- package/scripts/examples/file-manager.ts +14 -0
- package/scripts/examples/ideas.ts +12 -0
- package/scripts/examples/interactive-chat.ts +20 -0
- package/scripts/examples/openai-tool-calls.ts +113 -0
- package/scripts/examples/opening-a-web-browser.ts +5 -0
- package/scripts/examples/telegram-bot.ts +79 -0
- package/scripts/examples/telegram-ink-ui.ts +302 -0
- package/scripts/examples/using-assistant-with-mcp.ts +560 -0
- package/scripts/examples/using-claude-code.ts +10 -0
- package/scripts/examples/using-contentdb.ts +35 -0
- package/scripts/examples/using-conversations.ts +35 -0
- package/scripts/examples/using-disk-cache.ts +10 -0
- package/scripts/examples/using-docker-shell.ts +75 -0
- package/scripts/examples/using-elevenlabs.ts +25 -0
- package/scripts/examples/using-google-calendar.ts +57 -0
- package/scripts/examples/using-google-docs.ts +74 -0
- package/scripts/examples/using-google-drive.ts +74 -0
- package/scripts/examples/using-google-sheets.ts +89 -0
- package/scripts/examples/using-nlp.ts +55 -0
- package/scripts/examples/using-ollama.ts +10 -0
- package/scripts/examples/using-openai-codex.ts +23 -0
- package/scripts/examples/using-postgres.ts +55 -0
- package/scripts/examples/using-runpod.ts +32 -0
- package/scripts/examples/using-tts.ts +40 -0
- package/scripts/examples/vm-loading-esm-modules.ts +16 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/test-command-listener.ts +123 -0
- package/scripts/test-window-manager-lifecycle.ts +86 -0
- package/scripts/test-window-manager.ts +43 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +114 -0
- package/src/agi/endpoints/ask.ts +60 -0
- package/src/agi/endpoints/conversations/[id].ts +45 -0
- package/src/agi/endpoints/conversations.ts +31 -0
- package/src/agi/endpoints/experts.ts +37 -0
- package/src/agi/features/assistant.ts +767 -0
- package/src/agi/features/assistants-manager.ts +260 -0
- package/src/agi/features/claude-code.ts +1111 -0
- package/src/agi/features/conversation-history.ts +497 -0
- package/src/agi/features/conversation.ts +799 -0
- package/src/agi/features/openai-codex.ts +631 -0
- package/src/agi/features/openapi.ts +438 -0
- package/src/agi/features/skills-library.ts +425 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +100 -0
- package/src/cli/cli.ts +70 -0
- package/src/client.ts +461 -0
- package/src/clients/civitai/index.ts +541 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +597 -0
- package/src/clients/elevenlabs/index.ts +291 -0
- package/src/clients/openai/index.ts +451 -0
- package/src/clients/supabase/index.ts +366 -0
- package/src/command.ts +164 -0
- package/src/commands/chat.ts +182 -0
- package/src/commands/console.ts +192 -0
- package/src/commands/describe.ts +433 -0
- package/src/commands/eval.ts +116 -0
- package/src/commands/help.ts +214 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +807 -0
- package/src/commands/run.ts +257 -0
- package/src/commands/sandbox-mcp.ts +439 -0
- package/src/commands/scaffold.ts +79 -0
- package/src/commands/serve.ts +172 -0
- package/src/container.ts +781 -0
- package/src/endpoint.ts +340 -0
- package/src/feature.ts +75 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +543 -0
- package/src/introspection/generated.agi.ts +23388 -0
- package/src/introspection/generated.node.ts +18899 -0
- package/src/introspection/generated.web.ts +2021 -0
- package/src/introspection/index.ts +256 -0
- package/src/introspection/scan.ts +912 -0
- package/src/node/container.ts +354 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +558 -0
- package/src/node/features/content-db.ts +475 -0
- package/src/node/features/disk-cache.ts +382 -0
- package/src/node/features/dns.ts +655 -0
- package/src/node/features/docker.ts +912 -0
- package/src/node/features/downloader.ts +92 -0
- package/src/node/features/esbuild.ts +68 -0
- package/src/node/features/file-manager.ts +357 -0
- package/src/node/features/fs.ts +534 -0
- package/src/node/features/git.ts +492 -0
- package/src/node/features/google-auth.ts +502 -0
- package/src/node/features/google-calendar.ts +300 -0
- package/src/node/features/google-docs.ts +404 -0
- package/src/node/features/google-drive.ts +339 -0
- package/src/node/features/google-sheets.ts +279 -0
- package/src/node/features/grep.ts +406 -0
- package/src/node/features/helpers.ts +374 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +459 -0
- package/src/node/features/json-tree.ts +188 -0
- package/src/node/features/launcher-app-command-listener.ts +388 -0
- package/src/node/features/networking.ts +925 -0
- package/src/node/features/nlp.ts +211 -0
- package/src/node/features/opener.ts +166 -0
- package/src/node/features/os.ts +157 -0
- package/src/node/features/package-finder.ts +539 -0
- package/src/node/features/port-exposer.ts +342 -0
- package/src/node/features/postgres.ts +273 -0
- package/src/node/features/proc.ts +502 -0
- package/src/node/features/process-manager.ts +542 -0
- package/src/node/features/python.ts +444 -0
- package/src/node/features/repl.ts +194 -0
- package/src/node/features/runpod.ts +802 -0
- package/src/node/features/secure-shell.ts +248 -0
- package/src/node/features/semantic-search.ts +924 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +342 -0
- package/src/node/features/tts.ts +184 -0
- package/src/node/features/ui.ts +857 -0
- package/src/node/features/vault.ts +164 -0
- package/src/node/features/vm.ts +312 -0
- package/src/node/features/window-manager.ts +804 -0
- package/src/node/features/yaml-tree.ts +149 -0
- package/src/node/features/yaml.ts +132 -0
- package/src/node.ts +70 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +199 -0
- package/src/scaffolds/generated.ts +1613 -0
- package/src/scaffolds/template.ts +37 -0
- package/src/schemas/base.ts +255 -0
- package/src/server.ts +135 -0
- package/src/servers/express.ts +209 -0
- package/src/servers/mcp.ts +805 -0
- package/src/servers/socket.ts +120 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +82 -0
- package/src/web/container.ts +74 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +64 -0
- package/src/web/features/container-link.ts +385 -0
- package/src/web/features/esbuild.ts +79 -0
- package/src/web/features/helpers.ts +267 -0
- package/src/web/features/network.ts +61 -0
- package/src/web/features/speech.ts +87 -0
- package/src/web/features/vault.ts +189 -0
- package/src/web/features/vm.ts +78 -0
- package/src/web/features/voice-recognition.ts +129 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +216 -0
- package/test/container-link.test.ts +274 -0
- package/test/features.test.ts +160 -0
- package/test/integration.test.ts +787 -0
- package/test/node-container.test.ts +121 -0
- package/test/rate-limit.test.ts +272 -0
- package/test/semantic-search.test.ts +550 -0
- package/test/state.test.ts +121 -0
- package/test-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +123 -0
- package/test-integration/claude-code.test.ts +98 -0
- package/test-integration/conversation-history.test.ts +205 -0
- package/test-integration/conversation.test.ts +137 -0
- package/test-integration/elevenlabs.test.ts +55 -0
- package/test-integration/google-services.test.ts +80 -0
- package/test-integration/helpers.ts +89 -0
- package/test-integration/openai-codex.test.ts +93 -0
- package/test-integration/runpod.test.ts +58 -0
- package/test-integration/server-endpoints.test.ts +97 -0
- package/test-integration/skills-library.test.ts +157 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.json +58 -0
- 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)
|