@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,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Using Clients
|
|
3
|
+
tags: [clients, rest, graphql, websocket, http, api, axios]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Using Clients
|
|
7
|
+
|
|
8
|
+
Clients connect your application to external services. Luca provides built-in clients for REST APIs, GraphQL, and WebSocket connections.
|
|
9
|
+
|
|
10
|
+
## REST Client
|
|
11
|
+
|
|
12
|
+
The REST client wraps axios with Luca's helper patterns (state, events, introspection):
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
const api = container.client('rest', {
|
|
16
|
+
baseURL: 'https://api.example.com',
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: 'Bearer my-token',
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
await api.connect()
|
|
23
|
+
|
|
24
|
+
// Standard HTTP methods
|
|
25
|
+
const users = await api.get('/users')
|
|
26
|
+
const user = await api.get('/users/123')
|
|
27
|
+
const created = await api.post('/users', { name: 'Alice', email: 'alice@example.com' })
|
|
28
|
+
const updated = await api.put('/users/123', { name: 'Alice Updated' })
|
|
29
|
+
await api.delete('/users/123')
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### REST Client Events
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
api.on('failure', (error) => {
|
|
36
|
+
console.error('Request failed:', error.message)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// State changes track connection status
|
|
40
|
+
api.state.observe((type, key, value) => {
|
|
41
|
+
if (key === 'connected') {
|
|
42
|
+
console.log(`Client connected: ${value}`)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## GraphQL Client
|
|
48
|
+
|
|
49
|
+
For GraphQL APIs, use the REST client's `post()` method to send queries and mutations:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const graph = container.client('rest', {
|
|
53
|
+
baseURL: 'https://api.example.com/graphql',
|
|
54
|
+
headers: { Authorization: 'Bearer my-token' },
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
await graph.connect()
|
|
58
|
+
|
|
59
|
+
// Send a query
|
|
60
|
+
const result = await graph.post('/', {
|
|
61
|
+
query: `
|
|
62
|
+
query GetUser($id: ID!) {
|
|
63
|
+
user(id: $id) {
|
|
64
|
+
name
|
|
65
|
+
email
|
|
66
|
+
posts { title }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
`,
|
|
70
|
+
variables: { id: '123' },
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Send a mutation
|
|
74
|
+
const mutationResult = await graph.post('/', {
|
|
75
|
+
query: `
|
|
76
|
+
mutation CreatePost($input: PostInput!) {
|
|
77
|
+
createPost(input: $input) {
|
|
78
|
+
id
|
|
79
|
+
title
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
`,
|
|
83
|
+
variables: { input: { title: 'Hello World', body: '...' } },
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## WebSocket Client
|
|
88
|
+
|
|
89
|
+
The WebSocket client wraps a raw `WebSocket` connection:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const ws = container.client('websocket', {
|
|
93
|
+
baseURL: 'wss://realtime.example.com',
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
await ws.connect()
|
|
97
|
+
|
|
98
|
+
// Access the underlying WebSocket via ws.ws
|
|
99
|
+
ws.ws.onmessage = (event) => {
|
|
100
|
+
console.log('Received:', event.data)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ws.ws.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }))
|
|
104
|
+
|
|
105
|
+
// Clean up
|
|
106
|
+
ws.ws.close()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Discovering Clients
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
container.clients.available // ['rest', 'graph', 'websocket']
|
|
113
|
+
container.clients.describe('rest')
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Using Clients in Endpoints
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// endpoints/proxy.ts
|
|
120
|
+
import { z } from 'zod'
|
|
121
|
+
import type { EndpointContext } from '@soederpop/luca'
|
|
122
|
+
|
|
123
|
+
export const path = '/api/external-data'
|
|
124
|
+
|
|
125
|
+
export const getSchema = z.object({
|
|
126
|
+
query: z.string().describe('Search query'),
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
export async function get(params: z.infer<typeof getSchema>, ctx: EndpointContext) {
|
|
130
|
+
const api = ctx.container.client('rest', {
|
|
131
|
+
baseURL: 'https://external-api.com',
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
await api.connect()
|
|
135
|
+
const data = await api.get(`/search?q=${encodeURIComponent(params.query)}`)
|
|
136
|
+
|
|
137
|
+
return { results: data }
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Using Clients in Features
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
class WeatherService extends Feature<WeatherState, WeatherOptions> {
|
|
145
|
+
private api: any
|
|
146
|
+
|
|
147
|
+
async initialize() {
|
|
148
|
+
this.api = this.container.client('rest', {
|
|
149
|
+
baseURL: 'https://api.weather.com',
|
|
150
|
+
headers: { 'X-API-Key': this.options.apiKey },
|
|
151
|
+
})
|
|
152
|
+
await this.api.connect()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getForecast(city: string) {
|
|
156
|
+
const data = await this.api.get(`/forecast/${encodeURIComponent(city)}`)
|
|
157
|
+
this.state.set('lastForecast', data)
|
|
158
|
+
this.emit('forecastFetched', data)
|
|
159
|
+
return data
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Creating Custom Features
|
|
3
|
+
tags: [features, custom, extend, zod, state, events, module-augmentation, helper]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Creating Custom Features
|
|
7
|
+
|
|
8
|
+
You can create your own features to encapsulate domain logic, then register them so they're available through `container.feature('yourFeature')` with full type safety.
|
|
9
|
+
|
|
10
|
+
## Anatomy of a Feature
|
|
11
|
+
|
|
12
|
+
A feature has:
|
|
13
|
+
- **State** -- observable, defined by a Zod schema
|
|
14
|
+
- **Options** -- configuration passed at creation, defined by a Zod schema
|
|
15
|
+
- **Events** -- typed event bus
|
|
16
|
+
- **Methods** -- your domain logic
|
|
17
|
+
- **Access to the container** -- via `this.container`
|
|
18
|
+
|
|
19
|
+
## Basic Example
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { z } from 'zod'
|
|
23
|
+
import { Feature, features, FeatureStateSchema, FeatureOptionsSchema } from '@soederpop/luca'
|
|
24
|
+
|
|
25
|
+
// Define state schema by extending the base FeatureStateSchema
|
|
26
|
+
export const CounterStateSchema = FeatureStateSchema.extend({
|
|
27
|
+
count: z.number().describe('Current count value'),
|
|
28
|
+
lastUpdated: z.string().optional().describe('ISO timestamp of last update'),
|
|
29
|
+
})
|
|
30
|
+
export type CounterState = z.infer<typeof CounterStateSchema>
|
|
31
|
+
|
|
32
|
+
// Define options schema by extending the base FeatureOptionsSchema
|
|
33
|
+
export const CounterOptionsSchema = FeatureOptionsSchema.extend({
|
|
34
|
+
initialCount: z.number().default(0).describe('Starting count value'),
|
|
35
|
+
step: z.number().default(1).describe('Increment step size'),
|
|
36
|
+
})
|
|
37
|
+
export type CounterOptions = z.infer<typeof CounterOptionsSchema>
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A simple counter feature that demonstrates the feature pattern.
|
|
41
|
+
* Tracks a count value with observable state and events.
|
|
42
|
+
*/
|
|
43
|
+
export class Counter extends Feature<CounterState, CounterOptions> {
|
|
44
|
+
static override stateSchema = CounterStateSchema
|
|
45
|
+
static override optionsSchema = CounterOptionsSchema
|
|
46
|
+
|
|
47
|
+
/** Called when the feature is created */
|
|
48
|
+
async initialize() {
|
|
49
|
+
this.state.set('count', this.options.initialCount ?? 0)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Increment the counter by the configured step */
|
|
53
|
+
increment() {
|
|
54
|
+
const current = this.state.get('count') || 0
|
|
55
|
+
const next = current + (this.options.step ?? 1)
|
|
56
|
+
this.state.set('count', next)
|
|
57
|
+
this.state.set('lastUpdated', new Date().toISOString())
|
|
58
|
+
this.emit('incremented', next)
|
|
59
|
+
return next
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Decrement the counter by the configured step */
|
|
63
|
+
decrement() {
|
|
64
|
+
const current = this.state.get('count') || 0
|
|
65
|
+
const next = current - (this.options.step ?? 1)
|
|
66
|
+
this.state.set('count', next)
|
|
67
|
+
this.state.set('lastUpdated', new Date().toISOString())
|
|
68
|
+
this.emit('decremented', next)
|
|
69
|
+
return next
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Reset the counter to its initial value */
|
|
73
|
+
reset() {
|
|
74
|
+
this.state.set('count', this.options.initialCount ?? 0)
|
|
75
|
+
this.emit('reset')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Get the current count */
|
|
79
|
+
get value(): number {
|
|
80
|
+
return this.state.get('count') || 0
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Register the feature
|
|
85
|
+
features.register('counter', Counter)
|
|
86
|
+
|
|
87
|
+
// Module augmentation for type safety
|
|
88
|
+
declare module '@soederpop/luca' {
|
|
89
|
+
interface AvailableFeatures {
|
|
90
|
+
counter: typeof Counter
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Using Your Feature
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import './features/counter' // Side-effect import to register
|
|
99
|
+
|
|
100
|
+
const counter = container.feature('counter', { initialCount: 10, step: 5 })
|
|
101
|
+
|
|
102
|
+
counter.on('incremented', (value) => {
|
|
103
|
+
console.log(`Count is now ${value}`)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
counter.increment() // 15
|
|
107
|
+
counter.increment() // 20
|
|
108
|
+
counter.value // 20
|
|
109
|
+
counter.reset() // Back to 10
|
|
110
|
+
|
|
111
|
+
// Observe state changes
|
|
112
|
+
counter.state.observe((type, key, value) => {
|
|
113
|
+
console.log(`${key} ${type}d:`, value)
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Enabling on the Container
|
|
118
|
+
|
|
119
|
+
If your feature should be a container-level singleton with a shortcut:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
export class Counter extends Feature<CounterState, CounterOptions> {
|
|
123
|
+
// This creates the container.counter shortcut when enabled
|
|
124
|
+
static override shortcut = 'features.counter' as const
|
|
125
|
+
// ...
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Enable it
|
|
129
|
+
container.feature('counter', { enable: true })
|
|
130
|
+
|
|
131
|
+
// Now accessible as:
|
|
132
|
+
container.counter.increment()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Feature with Container Access
|
|
136
|
+
|
|
137
|
+
Features can access other features and the full container:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
export class Analytics extends Feature<AnalyticsState, AnalyticsOptions> {
|
|
141
|
+
/** Log an event, writing to disk cache for persistence */
|
|
142
|
+
async logEvent(name: string, data: Record<string, any>) {
|
|
143
|
+
const cache = this.container.feature('diskCache', { path: './.analytics' })
|
|
144
|
+
const timestamp = new Date().toISOString()
|
|
145
|
+
|
|
146
|
+
await cache.set(`event:${timestamp}`, { name, data, timestamp })
|
|
147
|
+
|
|
148
|
+
this.state.set('totalEvents', (this.state.get('totalEvents') || 0) + 1)
|
|
149
|
+
this.emit('eventLogged', { name, data })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Get recent events from the cache */
|
|
153
|
+
async recentEvents(limit = 10) {
|
|
154
|
+
const fs = this.container.fs
|
|
155
|
+
// ... read from cache directory
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Documenting Your Feature
|
|
161
|
+
|
|
162
|
+
Document your classes, methods, and getters with JSDoc. This is important because Luca's introspection system extracts these docs and makes them available at runtime:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
/**
|
|
166
|
+
* Manages user sessions with automatic expiration and renewal.
|
|
167
|
+
* Sessions are persisted to disk and can survive process restarts.
|
|
168
|
+
*/
|
|
169
|
+
export class SessionManager extends Feature<SessionState, SessionOptions> {
|
|
170
|
+
/**
|
|
171
|
+
* Create a new session for the given user.
|
|
172
|
+
* Returns a session token that can be used for authentication.
|
|
173
|
+
*/
|
|
174
|
+
async createSession(userId: string): Promise<string> {
|
|
175
|
+
// ...
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The number of currently active sessions */
|
|
179
|
+
get activeCount(): number {
|
|
180
|
+
return this.state.get('sessions')?.length || 0
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Then anyone (human or AI) can discover your feature:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
container.features.describe('sessionManager')
|
|
189
|
+
// Returns the full markdown documentation extracted from your JSDoc
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Best Practices
|
|
193
|
+
|
|
194
|
+
1. **Use Zod `.describe()` on schema fields** -- these descriptions appear in introspection and help documentation
|
|
195
|
+
2. **Emit events for significant actions** -- enables reactive patterns and decoupled observers
|
|
196
|
+
3. **Use state for observable values** -- don't hide important state in private variables if consumers need to watch it
|
|
197
|
+
4. **Access the container, not imports** -- prefer `this.container.feature('fs')` over importing fs directly, so the feature works in any container
|
|
198
|
+
5. **Document everything** -- JSDoc on the class, methods, and getters feeds the introspection system
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Contentbase - Markdown as a Database
|
|
3
|
+
tags: [contentbase, contentdb, markdown, database, models, query, collections]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Contentbase - Markdown as a Database
|
|
7
|
+
|
|
8
|
+
Contentbase lets you treat folders of markdown files as queryable database collections. Define models with Zod schemas, extract structured data from frontmatter and content, and query it with a fluent API.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import container from '@soederpop/luca'
|
|
14
|
+
|
|
15
|
+
const db = container.feature('contentDb', { rootPath: './content' })
|
|
16
|
+
const { defineModel, section, hasMany, belongsTo } = db.library
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Directory Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
content/
|
|
23
|
+
├── posts/
|
|
24
|
+
│ ├── hello-world.md
|
|
25
|
+
│ ├── getting-started.md
|
|
26
|
+
│ └── advanced-tips.md
|
|
27
|
+
├── authors/
|
|
28
|
+
│ ├── alice.md
|
|
29
|
+
│ └── bob.md
|
|
30
|
+
└── tags/
|
|
31
|
+
├── javascript.md
|
|
32
|
+
└── typescript.md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Defining Models
|
|
36
|
+
|
|
37
|
+
Models map to subdirectories and define the shape of your content:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { z } from 'zod'
|
|
41
|
+
|
|
42
|
+
const Post = defineModel('Post', {
|
|
43
|
+
// Maps to content/posts/
|
|
44
|
+
prefix: 'posts',
|
|
45
|
+
|
|
46
|
+
// Frontmatter schema
|
|
47
|
+
meta: z.object({
|
|
48
|
+
title: z.string(),
|
|
49
|
+
date: z.string(),
|
|
50
|
+
status: z.enum(['draft', 'published', 'archived']),
|
|
51
|
+
author: z.string().optional(),
|
|
52
|
+
tags: z.array(z.string()).default([]),
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
// Extract structured data from the markdown body
|
|
56
|
+
sections: {
|
|
57
|
+
summary: section('Summary', {
|
|
58
|
+
extract: (query) => query.select('paragraph')?.toString() || '',
|
|
59
|
+
schema: z.string(),
|
|
60
|
+
}),
|
|
61
|
+
codeExamples: section('Code Examples', {
|
|
62
|
+
extract: (query) => query.selectAll('code').map((n: any) => n.toString()),
|
|
63
|
+
schema: z.array(z.string()),
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Relationships
|
|
68
|
+
relationships: {
|
|
69
|
+
author: belongsTo(() => Author, { key: 'meta.author' }),
|
|
70
|
+
tags: hasMany(() => Tag, { heading: 'Tags' }),
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const Author = defineModel('Author', {
|
|
75
|
+
prefix: 'authors',
|
|
76
|
+
meta: z.object({
|
|
77
|
+
name: z.string(),
|
|
78
|
+
email: z.string().email(),
|
|
79
|
+
role: z.enum(['writer', 'editor', 'admin']),
|
|
80
|
+
}),
|
|
81
|
+
relationships: {
|
|
82
|
+
posts: hasMany(() => Post, { foreignKey: 'meta.author' }),
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Registering and Loading
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
db.register(Post)
|
|
91
|
+
db.register(Author)
|
|
92
|
+
await db.load() // Parses all markdown files and builds the queryable index
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Querying
|
|
96
|
+
|
|
97
|
+
Contentbase provides a fluent query API:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// Fetch all posts
|
|
101
|
+
const allPosts = await db.query(Post).fetchAll()
|
|
102
|
+
|
|
103
|
+
// Filter by frontmatter fields
|
|
104
|
+
const published = await db.query(Post)
|
|
105
|
+
.where('meta.status', 'published')
|
|
106
|
+
.fetchAll()
|
|
107
|
+
|
|
108
|
+
// Multiple filters
|
|
109
|
+
const recentPosts = await db.query(Post)
|
|
110
|
+
.where('meta.status', 'published')
|
|
111
|
+
.where('meta.tags', 'includes', 'javascript')
|
|
112
|
+
.fetchAll()
|
|
113
|
+
|
|
114
|
+
// Get a single document by slug (filename without .md)
|
|
115
|
+
const post = await db.query(Post).find('hello-world')
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Markdown File Format
|
|
119
|
+
|
|
120
|
+
Each markdown file has YAML frontmatter and a body:
|
|
121
|
+
|
|
122
|
+
```markdown
|
|
123
|
+
---
|
|
124
|
+
title: Hello World
|
|
125
|
+
date: 2024-01-15
|
|
126
|
+
status: published
|
|
127
|
+
author: alice
|
|
128
|
+
tags: [javascript, tutorial]
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
# Hello World
|
|
132
|
+
|
|
133
|
+
This is the post content.
|
|
134
|
+
|
|
135
|
+
## Summary
|
|
136
|
+
|
|
137
|
+
A brief introduction to our blog.
|
|
138
|
+
|
|
139
|
+
## Code Examples
|
|
140
|
+
|
|
141
|
+
\`\`\`javascript
|
|
142
|
+
console.log('Hello!')
|
|
143
|
+
\`\`\`
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Use Cases
|
|
147
|
+
|
|
148
|
+
- **Documentation sites** -- query and render docs with frontmatter metadata
|
|
149
|
+
- **Blog engines** -- posts with authors, tags, categories
|
|
150
|
+
- **Knowledge bases** -- structured content with relationships
|
|
151
|
+
- **Project management** -- epics, stories, tasks as markdown with status tracking
|
|
152
|
+
- **Configuration** -- human-readable config files that are also queryable
|
|
153
|
+
|
|
154
|
+
## Full Example: Blog Engine
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import container from '@soederpop/luca'
|
|
158
|
+
import { z } from 'zod'
|
|
159
|
+
|
|
160
|
+
const db = container.feature('contentDb', { rootPath: './blog' })
|
|
161
|
+
const { defineModel, section, hasMany } = db.library
|
|
162
|
+
|
|
163
|
+
const Post = defineModel('Post', {
|
|
164
|
+
prefix: 'posts',
|
|
165
|
+
meta: z.object({
|
|
166
|
+
title: z.string(),
|
|
167
|
+
date: z.string(),
|
|
168
|
+
status: z.enum(['draft', 'published']),
|
|
169
|
+
tags: z.array(z.string()).default([]),
|
|
170
|
+
}),
|
|
171
|
+
sections: {
|
|
172
|
+
excerpt: section('Excerpt', {
|
|
173
|
+
extract: (q) => q.select('paragraph')?.toString() || '',
|
|
174
|
+
schema: z.string(),
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
db.register(Post)
|
|
180
|
+
await db.load()
|
|
181
|
+
|
|
182
|
+
// Get published posts for the homepage
|
|
183
|
+
const posts = await db.query(Post)
|
|
184
|
+
.where('meta.status', 'published')
|
|
185
|
+
.fetchAll()
|
|
186
|
+
|
|
187
|
+
for (const post of posts) {
|
|
188
|
+
console.log(`${post.meta.title} (${post.meta.date})`)
|
|
189
|
+
console.log(` ${post.sections.excerpt}`)
|
|
190
|
+
}
|
|
191
|
+
```
|