@soulcraft/sdk 1.0.0
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/dist/client/index.d.ts +62 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +60 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/ai/index.d.ts +55 -0
- package/dist/modules/ai/index.d.ts.map +1 -0
- package/dist/modules/ai/index.js +263 -0
- package/dist/modules/ai/index.js.map +1 -0
- package/dist/modules/ai/types.d.ts +216 -0
- package/dist/modules/ai/types.d.ts.map +1 -0
- package/dist/modules/ai/types.js +30 -0
- package/dist/modules/ai/types.js.map +1 -0
- package/dist/modules/auth/backchannel.d.ts +85 -0
- package/dist/modules/auth/backchannel.d.ts.map +1 -0
- package/dist/modules/auth/backchannel.js +168 -0
- package/dist/modules/auth/backchannel.js.map +1 -0
- package/dist/modules/auth/config.d.ts +122 -0
- package/dist/modules/auth/config.d.ts.map +1 -0
- package/dist/modules/auth/config.js +158 -0
- package/dist/modules/auth/config.js.map +1 -0
- package/dist/modules/auth/middleware.d.ts +146 -0
- package/dist/modules/auth/middleware.d.ts.map +1 -0
- package/dist/modules/auth/middleware.js +204 -0
- package/dist/modules/auth/middleware.js.map +1 -0
- package/dist/modules/auth/types.d.ts +162 -0
- package/dist/modules/auth/types.d.ts.map +1 -0
- package/dist/modules/auth/types.js +14 -0
- package/dist/modules/auth/types.js.map +1 -0
- package/dist/modules/billing/types.d.ts +7 -0
- package/dist/modules/billing/types.d.ts.map +1 -0
- package/dist/modules/billing/types.js +7 -0
- package/dist/modules/billing/types.js.map +1 -0
- package/dist/modules/brainy/auth.d.ts +104 -0
- package/dist/modules/brainy/auth.d.ts.map +1 -0
- package/dist/modules/brainy/auth.js +144 -0
- package/dist/modules/brainy/auth.js.map +1 -0
- package/dist/modules/brainy/errors.d.ts +118 -0
- package/dist/modules/brainy/errors.d.ts.map +1 -0
- package/dist/modules/brainy/errors.js +142 -0
- package/dist/modules/brainy/errors.js.map +1 -0
- package/dist/modules/brainy/events.d.ts +63 -0
- package/dist/modules/brainy/events.d.ts.map +1 -0
- package/dist/modules/brainy/events.js +14 -0
- package/dist/modules/brainy/events.js.map +1 -0
- package/dist/modules/brainy/proxy.d.ts +48 -0
- package/dist/modules/brainy/proxy.d.ts.map +1 -0
- package/dist/modules/brainy/proxy.js +95 -0
- package/dist/modules/brainy/proxy.js.map +1 -0
- package/dist/modules/brainy/types.d.ts +83 -0
- package/dist/modules/brainy/types.d.ts.map +1 -0
- package/dist/modules/brainy/types.js +21 -0
- package/dist/modules/brainy/types.js.map +1 -0
- package/dist/modules/events/index.d.ts +41 -0
- package/dist/modules/events/index.d.ts.map +1 -0
- package/dist/modules/events/index.js +53 -0
- package/dist/modules/events/index.js.map +1 -0
- package/dist/modules/events/types.d.ts +129 -0
- package/dist/modules/events/types.d.ts.map +1 -0
- package/dist/modules/events/types.js +32 -0
- package/dist/modules/events/types.js.map +1 -0
- package/dist/modules/formats/types.d.ts +7 -0
- package/dist/modules/formats/types.d.ts.map +1 -0
- package/dist/modules/formats/types.js +7 -0
- package/dist/modules/formats/types.js.map +1 -0
- package/dist/modules/hall/types.d.ts +56 -0
- package/dist/modules/hall/types.d.ts.map +1 -0
- package/dist/modules/hall/types.js +16 -0
- package/dist/modules/hall/types.js.map +1 -0
- package/dist/modules/kits/types.d.ts +7 -0
- package/dist/modules/kits/types.d.ts.map +1 -0
- package/dist/modules/kits/types.js +7 -0
- package/dist/modules/kits/types.js.map +1 -0
- package/dist/modules/license/types.d.ts +7 -0
- package/dist/modules/license/types.d.ts.map +1 -0
- package/dist/modules/license/types.js +7 -0
- package/dist/modules/license/types.js.map +1 -0
- package/dist/modules/notifications/types.d.ts +7 -0
- package/dist/modules/notifications/types.d.ts.map +1 -0
- package/dist/modules/notifications/types.js +7 -0
- package/dist/modules/notifications/types.js.map +1 -0
- package/dist/modules/skills/index.d.ts +60 -0
- package/dist/modules/skills/index.d.ts.map +1 -0
- package/dist/modules/skills/index.js +253 -0
- package/dist/modules/skills/index.js.map +1 -0
- package/dist/modules/skills/types.d.ts +127 -0
- package/dist/modules/skills/types.d.ts.map +1 -0
- package/dist/modules/skills/types.js +23 -0
- package/dist/modules/skills/types.js.map +1 -0
- package/dist/modules/versions/types.d.ts +31 -0
- package/dist/modules/versions/types.d.ts.map +1 -0
- package/dist/modules/versions/types.js +9 -0
- package/dist/modules/versions/types.js.map +1 -0
- package/dist/modules/vfs/types.d.ts +26 -0
- package/dist/modules/vfs/types.d.ts.map +1 -0
- package/dist/modules/vfs/types.js +11 -0
- package/dist/modules/vfs/types.js.map +1 -0
- package/dist/server/create-sdk.d.ts +70 -0
- package/dist/server/create-sdk.d.ts.map +1 -0
- package/dist/server/create-sdk.js +125 -0
- package/dist/server/create-sdk.js.map +1 -0
- package/dist/server/hall-handlers.d.ts +195 -0
- package/dist/server/hall-handlers.d.ts.map +1 -0
- package/dist/server/hall-handlers.js +239 -0
- package/dist/server/hall-handlers.js.map +1 -0
- package/dist/server/handlers.d.ts +216 -0
- package/dist/server/handlers.d.ts.map +1 -0
- package/dist/server/handlers.js +214 -0
- package/dist/server/handlers.js.map +1 -0
- package/dist/server/index.d.ts +52 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +50 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/instance-pool.d.ts +299 -0
- package/dist/server/instance-pool.d.ts.map +1 -0
- package/dist/server/instance-pool.js +359 -0
- package/dist/server/instance-pool.js.map +1 -0
- package/dist/transports/http.d.ts +86 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +134 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/transports/local.d.ts +76 -0
- package/dist/transports/local.d.ts.map +1 -0
- package/dist/transports/local.js +101 -0
- package/dist/transports/local.js.map +1 -0
- package/dist/transports/sse.d.ts +99 -0
- package/dist/transports/sse.d.ts.map +1 -0
- package/dist/transports/sse.js +192 -0
- package/dist/transports/sse.js.map +1 -0
- package/dist/transports/transport.d.ts +68 -0
- package/dist/transports/transport.d.ts.map +1 -0
- package/dist/transports/transport.js +14 -0
- package/dist/transports/transport.js.map +1 -0
- package/dist/transports/ws.d.ts +135 -0
- package/dist/transports/ws.d.ts.map +1 -0
- package/dist/transports/ws.js +331 -0
- package/dist/transports/ws.js.map +1 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/docs/ADR-001-sdk-design.md +282 -0
- package/docs/IMPLEMENTATION-PLAN.md +708 -0
- package/docs/USAGE.md +646 -0
- package/docs/kit-sdk-guide.md +474 -0
- package/package.json +61 -0
package/docs/USAGE.md
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
# @soulcraft/sdk — Usage Guide
|
|
2
|
+
|
|
3
|
+
This guide is for engineers, AI assistants, kit developers, and anyone building on
|
|
4
|
+
the Soulcraft platform. It covers every implemented module with working examples.
|
|
5
|
+
|
|
6
|
+
**Three rules to remember:**
|
|
7
|
+
1. Server code (Hono, SvelteKit routes, Bun backends) imports from `@soulcraft/sdk/server`
|
|
8
|
+
2. Browser code (kit apps, Svelte components) imports from `@soulcraft/sdk/client`
|
|
9
|
+
3. Shared types import from `@soulcraft/sdk`
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
1. [Installation](#installation)
|
|
16
|
+
2. [Server Setup — createSDK](#server-setup--createsdk)
|
|
17
|
+
3. [Client Setup — Transports](#client-setup--transports)
|
|
18
|
+
4. [sdk.brainy — Graph Data](#sdkbrainy--graph-data)
|
|
19
|
+
5. [sdk.vfs — Virtual Filesystem](#sdkvfs--virtual-filesystem)
|
|
20
|
+
6. [sdk.auth — Authentication & Tokens](#sdkauth--authentication--tokens)
|
|
21
|
+
7. [sdk.ai — Claude AI Integration](#sdkai--claude-ai-integration)
|
|
22
|
+
8. [sdk.events — Platform Event Bus](#sdkevents--platform-event-bus)
|
|
23
|
+
9. [sdk.skills — Skill System](#sdkskills--skill-system)
|
|
24
|
+
10. [BrainyInstancePool — Instance Lifecycle](#brainyinstancepool--instance-lifecycle)
|
|
25
|
+
11. [Capability Tokens — Cross-Product Access](#capability-tokens--cross-product-access)
|
|
26
|
+
12. [Auth Middleware](#auth-middleware)
|
|
27
|
+
13. [For AI Assistants](#for-ai-assistants)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Requires npm login to the @soulcraft org:
|
|
35
|
+
npm login
|
|
36
|
+
bun add @soulcraft/sdk
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Peer dependencies — must already be in your project:
|
|
40
|
+
```bash
|
|
41
|
+
bun add @soulcraft/brainy @soulcraft/cortex
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**GCE VM deploys:** The SDK cannot be installed on the VM without an npm auth token.
|
|
45
|
+
Bundle it from local `node_modules` in your `build.sh`:
|
|
46
|
+
```bash
|
|
47
|
+
cp -r node_modules/@soulcraft/sdk build/node_modules/@soulcraft/sdk
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Server Setup — createSDK
|
|
53
|
+
|
|
54
|
+
Use `createSDK` in any request handler that has already resolved a Brainy instance.
|
|
55
|
+
`createSDK` is a thin wrapper — create one per request, not one per process.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { BrainyInstancePool, createSDK } from '@soulcraft/sdk/server'
|
|
59
|
+
|
|
60
|
+
// One pool per process — module singleton
|
|
61
|
+
const pool = new BrainyInstancePool({
|
|
62
|
+
storage: process.env.STORAGE_TYPE === 'mmap-filesystem' ? 'mmap-filesystem' : 'filesystem',
|
|
63
|
+
dataPath: process.env.BRAINY_DATA_PATH ?? './brainy-data',
|
|
64
|
+
strategy: 'per-user',
|
|
65
|
+
maxInstances: 200,
|
|
66
|
+
flushOnEvict: true,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// In a request handler:
|
|
70
|
+
app.get('/api/data', requireAuth, async (c) => {
|
|
71
|
+
const user = c.get('user')!
|
|
72
|
+
const brain = await pool.forUser(user.emailHash, 'main')
|
|
73
|
+
const sdk = createSDK({ brain })
|
|
74
|
+
|
|
75
|
+
// All namespaces now available:
|
|
76
|
+
const items = await sdk.brainy.find({ query: 'inventory items' })
|
|
77
|
+
const readme = await sdk.vfs.readFile('/projects/README.md')
|
|
78
|
+
const skill = await sdk.skills.load('inventory-health', 'wicks-and-whiskers')
|
|
79
|
+
|
|
80
|
+
return c.json({ items })
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Instance Strategies
|
|
85
|
+
|
|
86
|
+
| Strategy | When to use | Pool method |
|
|
87
|
+
|----------|-------------|-------------|
|
|
88
|
+
| `per-user` | One Brainy per user (Workshop) | `pool.forUser(emailHash, workspaceId)` |
|
|
89
|
+
| `per-tenant` | One Brainy per org/location (Venue) | `pool.forTenant(slug)` |
|
|
90
|
+
| `per-scope` | Custom key — Academy or bespoke | `pool.forScope(key, factory)` |
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// per-scope example (Academy — content vs learner brains):
|
|
94
|
+
const contentPool = new BrainyInstancePool({ strategy: 'per-scope', ... })
|
|
95
|
+
const brain = await contentPool.forScope(
|
|
96
|
+
`content:${workspaceId}`,
|
|
97
|
+
() => initBrainy(`content/${workspaceId}`, storagePath)
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Client Setup — Transports
|
|
104
|
+
|
|
105
|
+
Used in browser kit apps and remote connections.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { createBrainyProxy, HttpTransport, WsTransport } from '@soulcraft/sdk/client'
|
|
109
|
+
|
|
110
|
+
// HTTP transport — stateless, for simple kit apps:
|
|
111
|
+
const transport = new HttpTransport('https://workshop.soulcraft.com', undefined, true)
|
|
112
|
+
const brainy = createBrainyProxy(transport)
|
|
113
|
+
|
|
114
|
+
const results = await brainy.find({ query: 'inventory' })
|
|
115
|
+
|
|
116
|
+
// WebSocket transport — real-time, change push events:
|
|
117
|
+
const wsTransport = new WsTransport(
|
|
118
|
+
'wss://venue.soulcraft.com/api/brainy/ws',
|
|
119
|
+
capabilityToken,
|
|
120
|
+
'wicks-and-whiskers'
|
|
121
|
+
)
|
|
122
|
+
await wsTransport.connect()
|
|
123
|
+
const brainy = createBrainyProxy(wsTransport)
|
|
124
|
+
|
|
125
|
+
brainy.onDataChange((event) => {
|
|
126
|
+
console.log('Live change:', event)
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Which transport to use?
|
|
131
|
+
|
|
132
|
+
| Scenario | Transport | Why |
|
|
133
|
+
|----------|-----------|-----|
|
|
134
|
+
| Kit app reading/writing data | `http` | Stateless, simple, no connection overhead |
|
|
135
|
+
| Real-time sync + change events | `ws` | Bidirectional, MessagePack, change push |
|
|
136
|
+
| Live update notifications only | `sse` | Lightweight server-push, auto-reconnects |
|
|
137
|
+
| Server handler (same process) | `local` (via `createSDK`) | Zero overhead |
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## sdk.brainy — Graph Data
|
|
142
|
+
|
|
143
|
+
The full Brainy graph API. Works identically in server mode (via `createSDK`) and client mode (via transport proxy).
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { NounType, VerbType } from '@soulcraft/brainy'
|
|
147
|
+
|
|
148
|
+
// Add entities
|
|
149
|
+
await sdk.brainy.add({
|
|
150
|
+
id: 'product-lavender',
|
|
151
|
+
type: 'Product',
|
|
152
|
+
metadata: { name: 'Lavender Soy Candle', price: 28, stock: 45 },
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Find with semantic search
|
|
156
|
+
const results = await sdk.brainy.find({
|
|
157
|
+
query: 'soy candle lavender scent',
|
|
158
|
+
type: 'Product',
|
|
159
|
+
limit: 10,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Find with filter
|
|
163
|
+
const inStock = await sdk.brainy.find({
|
|
164
|
+
query: 'candles',
|
|
165
|
+
filter: { stock: { $gt: 0 } },
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Get by ID
|
|
169
|
+
const product = await sdk.brainy.get('product-lavender')
|
|
170
|
+
|
|
171
|
+
// Update
|
|
172
|
+
await sdk.brainy.update({
|
|
173
|
+
id: 'product-lavender',
|
|
174
|
+
metadata: { stock: 40 },
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Delete
|
|
178
|
+
await sdk.brainy.delete('product-lavender')
|
|
179
|
+
|
|
180
|
+
// Relationships
|
|
181
|
+
await sdk.brainy.relate({
|
|
182
|
+
from: 'product-lavender',
|
|
183
|
+
to: 'category-candles',
|
|
184
|
+
type: 'BelongsTo',
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const relations = await sdk.brainy.getRelations({ from: 'product-lavender' })
|
|
188
|
+
await sdk.brainy.unrelate({ from: 'product-lavender', to: 'category-candles', type: 'BelongsTo' })
|
|
189
|
+
|
|
190
|
+
// Real-time change events (local transport: use sdk.events; WS transport: use onDataChange)
|
|
191
|
+
sdk.brainy.onDataChange((event) => {
|
|
192
|
+
// event: { type: 'change', event: 'add'|'update'|'delete'|'relate'|'unrelate', entity?, relation? }
|
|
193
|
+
console.log('Brainy change:', event.event, event.entity?.id)
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## sdk.vfs — Virtual Filesystem
|
|
200
|
+
|
|
201
|
+
VFS stores documents, code, slides, and any file content alongside Brainy graph data.
|
|
202
|
+
Accessed via `sdk.vfs.*` — the same API as `brain.vfs.*` but through the SDK proxy.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Write files
|
|
206
|
+
await sdk.vfs.writeFile('/projects/my-project/README.md', '# My Project\n...')
|
|
207
|
+
await sdk.vfs.writeFile('/notes/todo.txt', 'Buy more candles')
|
|
208
|
+
|
|
209
|
+
// Read files — returns Buffer; convert to string for text:
|
|
210
|
+
const buffer = await sdk.vfs.readFile('/projects/my-project/README.md')
|
|
211
|
+
const content = buffer.toString('utf-8')
|
|
212
|
+
|
|
213
|
+
// List directory
|
|
214
|
+
const entries = await sdk.vfs.readdir('/projects/my-project')
|
|
215
|
+
// → ['README.md', 'src', 'package.json']
|
|
216
|
+
|
|
217
|
+
// File info
|
|
218
|
+
const stat = await sdk.vfs.stat('/projects/my-project/README.md')
|
|
219
|
+
// → { size, mtime, isDirectory, ... }
|
|
220
|
+
|
|
221
|
+
// Rename / move
|
|
222
|
+
await sdk.vfs.rename('/projects/old-name', '/projects/new-name')
|
|
223
|
+
|
|
224
|
+
// Delete
|
|
225
|
+
await sdk.vfs.unlink('/notes/todo.txt')
|
|
226
|
+
await sdk.vfs.rmdir('/projects/old-project', { recursive: true })
|
|
227
|
+
|
|
228
|
+
// Make directories
|
|
229
|
+
await sdk.vfs.mkdir('/projects/new-project', { recursive: true })
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## sdk.auth — Authentication & Tokens
|
|
235
|
+
|
|
236
|
+
The auth namespace handles capability tokens for cross-product server-to-server calls.
|
|
237
|
+
Session auth (OIDC, middleware) is configured at the product level — see [Auth Middleware](#auth-middleware).
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import { createCapabilityToken, verifyCapabilityToken } from '@soulcraft/sdk/server'
|
|
241
|
+
|
|
242
|
+
// Or via sdk.auth (same functions):
|
|
243
|
+
const token = await sdk.auth.createToken({
|
|
244
|
+
email: user.email,
|
|
245
|
+
scope: 'wicks-and-whiskers',
|
|
246
|
+
secret: process.env.VENUE_RPC_SECRET!,
|
|
247
|
+
// ttlMs: 3_600_000 // default: 1 hour
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// On the receiving server:
|
|
251
|
+
const claims = await sdk.auth.verifyToken(token, process.env.VENUE_RPC_SECRET!)
|
|
252
|
+
if (!claims) return c.json({ error: 'Unauthorized' }, 401)
|
|
253
|
+
console.log(claims.email, claims.scope, claims.exp)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Token format: `<base64url(JSON payload)>.<base64url(HMAC-SHA256 signature)>`
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## sdk.ai — Claude AI Integration
|
|
261
|
+
|
|
262
|
+
Wraps the Anthropic Claude API with Soulcraft defaults. Requires `ANTHROPIC_API_KEY`.
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { AI_MODELS } from '@soulcraft/sdk'
|
|
266
|
+
|
|
267
|
+
// Basic completion
|
|
268
|
+
const response = await sdk.ai.complete({
|
|
269
|
+
messages: [{ role: 'user', content: 'What candles are low in stock?' }],
|
|
270
|
+
systemPrompt: kit.shared.aiPersona, // from kit.json → shared.aiPersona
|
|
271
|
+
model: AI_MODELS.haiku, // fast + cheap for simple lookups
|
|
272
|
+
tools: [
|
|
273
|
+
{
|
|
274
|
+
name: 'search_inventory',
|
|
275
|
+
description: 'Search Brainy for inventory data',
|
|
276
|
+
inputSchema: {
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: { query: { type: 'string' } },
|
|
279
|
+
required: ['query'],
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// response.text — Claude's text reply (null if only tool calls)
|
|
286
|
+
// response.toolCalls — tool invocations Claude wants to make (null if none)
|
|
287
|
+
// response.stopReason — 'end_turn' | 'tool_use' | 'max_tokens'
|
|
288
|
+
// response.usage — { inputTokens, outputTokens }
|
|
289
|
+
|
|
290
|
+
// Handle tool calls:
|
|
291
|
+
if (response.toolCalls) {
|
|
292
|
+
for (const call of response.toolCalls) {
|
|
293
|
+
if (call.name === 'search_inventory') {
|
|
294
|
+
const results = await sdk.brainy.find({ query: call.input.query as string })
|
|
295
|
+
// Continue the conversation by appending a tool_result message
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Streaming — yields text, tool_use, and done events:
|
|
301
|
+
for await (const event of sdk.ai.stream({
|
|
302
|
+
messages: [{ role: 'user', content: 'Write a product description for lavender candles' }],
|
|
303
|
+
systemPrompt: 'You are a Wicks & Whiskers product writer.',
|
|
304
|
+
model: AI_MODELS.sonnet,
|
|
305
|
+
})) {
|
|
306
|
+
if (event.type === 'text') process.stdout.write(event.text)
|
|
307
|
+
if (event.type === 'tool_use') console.log('Tool call:', event.toolCall.name)
|
|
308
|
+
if (event.type === 'done') console.log('Total tokens:', event.result.usage)
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Model tiers
|
|
313
|
+
|
|
314
|
+
| Constant | Model ID | When to use |
|
|
315
|
+
|----------|----------|-------------|
|
|
316
|
+
| `AI_MODELS.haiku` | `claude-haiku-4-5-20251001` | Fast lookups, data extraction, simple Q&A |
|
|
317
|
+
| `AI_MODELS.sonnet` | `claude-sonnet-4-6` | Most kit operations, content generation, analysis |
|
|
318
|
+
| `AI_MODELS.opus` | `claude-opus-4-6` | Complex reasoning, long-form synthesis |
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## sdk.events — Platform Event Bus
|
|
323
|
+
|
|
324
|
+
A typed EventEmitter for coordinating platform events across modules and kit code.
|
|
325
|
+
Events are local to the SDK instance — they do not cross process boundaries.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// Subscribe to built-in platform events:
|
|
329
|
+
sdk.events.on('brainy:change', (event) => {
|
|
330
|
+
if (event.event === 'add' && event.entity?.nounType === 'Product') {
|
|
331
|
+
updateInventoryCache(event.entity)
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
sdk.events.on('vfs:write', ({ path, content }) => {
|
|
336
|
+
if (path.endsWith('.md')) {
|
|
337
|
+
broadcastDocumentUpdate(path, content)
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
sdk.events.on('vfs:delete', ({ path }) => {
|
|
342
|
+
invalidateCache(path)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
sdk.events.on('vfs:rename', ({ from, to }) => {
|
|
346
|
+
updateFileTree(from, to)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// Emit custom application events:
|
|
350
|
+
sdk.events.emit('kit:session-completed', {
|
|
351
|
+
userId: user.id,
|
|
352
|
+
sessionId,
|
|
353
|
+
kitId: 'wicks-and-whiskers',
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// Remove a listener:
|
|
357
|
+
const handler = (event) => { ... }
|
|
358
|
+
sdk.events.on('brainy:change', handler)
|
|
359
|
+
sdk.events.off('brainy:change', handler)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Adding custom event types
|
|
363
|
+
|
|
364
|
+
Extend `SoulcraftEventMap` with declaration merging for type-safe custom events:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// In your project (e.g. types.d.ts):
|
|
368
|
+
declare module '@soulcraft/sdk' {
|
|
369
|
+
interface SoulcraftEventMap {
|
|
370
|
+
'kit:session-completed': { userId: string; sessionId: string; kitId: string }
|
|
371
|
+
'venue:booking-created': { bookingId: string; tenantId: string }
|
|
372
|
+
'academy:enrollment': { userId: string; courseId: string; workspaceId: string }
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
After this declaration, `sdk.events.on('kit:session-completed', ...)` is fully typed.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## sdk.skills — Skill System
|
|
382
|
+
|
|
383
|
+
Skills are SKILL.md files that define an AI persona, capabilities, and workflow for
|
|
384
|
+
a kit domain. The skills module loads from VFS first, then falls back to the bundled
|
|
385
|
+
`@soulcraft/kits` registry.
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// Load a specific skill (VFS first, then bundled):
|
|
389
|
+
const skill = await sdk.skills.load('inventory-health', 'wicks-and-whiskers')
|
|
390
|
+
if (skill) {
|
|
391
|
+
console.log(skill.id) // 'inventory-health'
|
|
392
|
+
console.log(skill.kitId) // 'wicks-and-whiskers'
|
|
393
|
+
console.log(skill.source) // 'vfs' or 'bundled'
|
|
394
|
+
console.log(skill.content) // full SKILL.md content string
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// List all skills for a kit:
|
|
398
|
+
const skills = await sdk.skills.list({ kitId: 'wicks-and-whiskers' })
|
|
399
|
+
const systemPrompt = buildSystemPrompt(kit, skills.map(s => s.content))
|
|
400
|
+
|
|
401
|
+
// List all skills across all kits:
|
|
402
|
+
const allSkills = await sdk.skills.list()
|
|
403
|
+
|
|
404
|
+
// List only VFS skills (user-installed), no bundled:
|
|
405
|
+
const userSkills = await sdk.skills.list({
|
|
406
|
+
kitId: 'wicks-and-whiskers',
|
|
407
|
+
includeBundled: false,
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
// Install a skill into the VFS (makes it available to future load() calls):
|
|
411
|
+
await sdk.skills.install({
|
|
412
|
+
id: 'custom-flow',
|
|
413
|
+
kitId: 'wicks-and-whiskers',
|
|
414
|
+
content: `---
|
|
415
|
+
id: custom-flow
|
|
416
|
+
title: Custom Checkout Flow
|
|
417
|
+
---
|
|
418
|
+
You are a candle shop checkout specialist...`,
|
|
419
|
+
})
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### VFS path convention for skills
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
/skills/{kitId}/{skillId}.md
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Example: `/skills/wicks-and-whiskers/inventory-health.md`
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## BrainyInstancePool — Instance Lifecycle
|
|
433
|
+
|
|
434
|
+
The pool manages Brainy instances: creation, caching, LRU eviction, and graceful shutdown.
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
import { BrainyInstancePool } from '@soulcraft/sdk/server'
|
|
438
|
+
|
|
439
|
+
const pool = new BrainyInstancePool({
|
|
440
|
+
storage: 'mmap-filesystem', // or 'filesystem' for dev
|
|
441
|
+
dataPath: '/mnt/brainy-data',
|
|
442
|
+
strategy: 'per-user',
|
|
443
|
+
maxInstances: 200,
|
|
444
|
+
flushOnEvict: true,
|
|
445
|
+
onInit: async (brain, storagePath) => {
|
|
446
|
+
// Optional: post-init hook for migrations, integrity checks, etc.
|
|
447
|
+
await runMigrations(brain)
|
|
448
|
+
},
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
// per-user (Workshop):
|
|
452
|
+
const brain = await pool.forUser(user.emailHash, workspaceId)
|
|
453
|
+
|
|
454
|
+
// per-tenant (Venue):
|
|
455
|
+
const brain = await pool.forTenant('wicks-and-whiskers')
|
|
456
|
+
|
|
457
|
+
// per-scope (custom key — Academy, multi-branch, etc.):
|
|
458
|
+
const brain = await pool.forScope(
|
|
459
|
+
`content:${workspaceId}`,
|
|
460
|
+
() => initBrainy('content', storagePath)
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
// Pool stats:
|
|
464
|
+
const stats = pool.getStats()
|
|
465
|
+
// { size: 12, maxSize: 200, keys: ['abc123:ws-1', ...] }
|
|
466
|
+
|
|
467
|
+
// Graceful shutdown (flush all, clear cache):
|
|
468
|
+
await pool.shutdown()
|
|
469
|
+
|
|
470
|
+
// Flush and evict one instance:
|
|
471
|
+
await pool.flush('abc123:workspace-1')
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Concurrent init deduplication
|
|
475
|
+
|
|
476
|
+
If two requests arrive simultaneously for the same scope key while Brainy is still
|
|
477
|
+
initializing, the pool deduplicates — only one Brainy instance is ever created per key,
|
|
478
|
+
and both requests await the same initialization promise.
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Capability Tokens — Cross-Product Access
|
|
483
|
+
|
|
484
|
+
Used when a product backend (e.g. Workshop) needs to call another product's Brainy RPC
|
|
485
|
+
endpoint. Short-lived HMAC-SHA256 tokens replace session cookies for server-to-server calls.
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { createCapabilityToken, verifyCapabilityToken } from '@soulcraft/sdk/server'
|
|
489
|
+
|
|
490
|
+
// Issuing server — create a scoped token:
|
|
491
|
+
const token = await createCapabilityToken({
|
|
492
|
+
email: user.email,
|
|
493
|
+
scope: 'wicks-and-whiskers', // optional — restricts to one tenant
|
|
494
|
+
secret: process.env.VENUE_RPC_SECRET!,
|
|
495
|
+
ttlMs: 3_600_000, // 1 hour (default)
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
// Send token in Authorization header:
|
|
499
|
+
// Authorization: Bearer <token>
|
|
500
|
+
|
|
501
|
+
// Receiving server — verify in the RPC handler:
|
|
502
|
+
const claims = await verifyCapabilityToken(token, process.env.VENUE_RPC_SECRET!)
|
|
503
|
+
if (!claims) return new Response('Unauthorized', { status: 401 })
|
|
504
|
+
|
|
505
|
+
console.log(claims.email) // original user's email
|
|
506
|
+
console.log(claims.scope) // 'wicks-and-whiskers'
|
|
507
|
+
console.log(claims.exp) // expiry timestamp (ms)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Auth Middleware
|
|
513
|
+
|
|
514
|
+
The SDK provides Hono middleware that resolves better-auth sessions and injects the
|
|
515
|
+
typed user into request context.
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
import { betterAuth } from 'better-auth'
|
|
519
|
+
import {
|
|
520
|
+
SOULCRAFT_USER_FIELDS,
|
|
521
|
+
SOULCRAFT_SESSION_CONFIG,
|
|
522
|
+
computeEmailHash,
|
|
523
|
+
createAuthMiddleware,
|
|
524
|
+
} from '@soulcraft/sdk/server'
|
|
525
|
+
import type { AuthContext } from '@soulcraft/sdk/server'
|
|
526
|
+
|
|
527
|
+
// 1. Configure better-auth with Soulcraft fields:
|
|
528
|
+
const auth = betterAuth({
|
|
529
|
+
database: new Database('./auth.db'),
|
|
530
|
+
secret: process.env.BETTER_AUTH_SECRET!,
|
|
531
|
+
session: SOULCRAFT_SESSION_CONFIG, // 30-day sessions, 24h refresh
|
|
532
|
+
user: { additionalFields: SOULCRAFT_USER_FIELDS },
|
|
533
|
+
databaseHooks: {
|
|
534
|
+
user: {
|
|
535
|
+
create: {
|
|
536
|
+
before: async (user) => ({
|
|
537
|
+
data: { ...user, emailHash: computeEmailHash(user.email), platformRole: 'creator' }
|
|
538
|
+
})
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
// 2. Create middleware:
|
|
545
|
+
const { requireAuth, optionalAuth } = createAuthMiddleware(auth)
|
|
546
|
+
|
|
547
|
+
// 3. Use in routes:
|
|
548
|
+
app.all('/api/auth/*', (c) => auth.handler(c.req.raw))
|
|
549
|
+
|
|
550
|
+
app.get('/api/data', requireAuth, async (c) => {
|
|
551
|
+
const user = c.get('user')! // SoulcraftSessionUser — always non-null after requireAuth
|
|
552
|
+
// user.id, user.email, user.emailHash, user.platformRole
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
app.get('/api/public', optionalAuth, async (c) => {
|
|
556
|
+
const user = c.get('user') // SoulcraftSessionUser | null
|
|
557
|
+
})
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Auth modes
|
|
561
|
+
|
|
562
|
+
| Mode | When | How |
|
|
563
|
+
|------|------|-----|
|
|
564
|
+
| Standalone | `SOULCRAFT_IDP_URL` not set | better-auth runs locally with SQLite — local dev default |
|
|
565
|
+
| OIDC client | `SOULCRAFT_IDP_URL=https://auth.soulcraft.com` | Delegates all auth to the central IdP — all production deployments |
|
|
566
|
+
|
|
567
|
+
Production always uses OIDC. Standalone is the dev default — no internet connection,
|
|
568
|
+
no credentials, no setup required.
|
|
569
|
+
|
|
570
|
+
### Backchannel logout (OIDC)
|
|
571
|
+
|
|
572
|
+
For products running in OIDC client mode:
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
import { createBackchannelLogoutHandler } from '@soulcraft/sdk/server'
|
|
576
|
+
|
|
577
|
+
const backchannelHandler = createBackchannelLogoutHandler({
|
|
578
|
+
auth,
|
|
579
|
+
clientSecret: process.env.SOULCRAFT_OIDC_CLIENT_SECRET!,
|
|
580
|
+
idpUrl: process.env.SOULCRAFT_IDP_URL!,
|
|
581
|
+
clientId: 'workshop',
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
app.post('/api/auth/backchannel-logout', backchannelHandler)
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## For AI Assistants
|
|
590
|
+
|
|
591
|
+
When helping implement Soulcraft server code, follow this checklist:
|
|
592
|
+
|
|
593
|
+
**1. Import correctly:**
|
|
594
|
+
```typescript
|
|
595
|
+
// Server code (Hono, SvelteKit server routes, Bun scripts):
|
|
596
|
+
import { BrainyInstancePool, createSDK, createAuthMiddleware, computeEmailHash } from '@soulcraft/sdk/server'
|
|
597
|
+
import type { SoulcraftSessionUser, AuthContext } from '@soulcraft/sdk'
|
|
598
|
+
|
|
599
|
+
// Shared types only:
|
|
600
|
+
import { AI_MODELS } from '@soulcraft/sdk'
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**2. Resolve Brainy from the pool, then create SDK:**
|
|
604
|
+
```typescript
|
|
605
|
+
const brain = await pool.forUser(user.emailHash, workspaceId) // or forTenant, forScope
|
|
606
|
+
const sdk = createSDK({ brain })
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**3. Call modules on the sdk object:**
|
|
610
|
+
```typescript
|
|
611
|
+
await sdk.brainy.find({ query: '...' })
|
|
612
|
+
await sdk.vfs.readFile('/path/to/file')
|
|
613
|
+
await sdk.ai.complete({ messages, systemPrompt, model: AI_MODELS.haiku })
|
|
614
|
+
await sdk.skills.load('skill-id', 'kit-id')
|
|
615
|
+
sdk.events.on('brainy:change', handler)
|
|
616
|
+
sdk.events.emit('custom:event', payload)
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**4. Module implementation status:**
|
|
620
|
+
|
|
621
|
+
| Module | Status | Notes |
|
|
622
|
+
|--------|--------|-------|
|
|
623
|
+
| `sdk.brainy.*` | ✅ Implemented | Full Brainy API via proxy |
|
|
624
|
+
| `sdk.vfs.*` | ✅ Implemented | Brainy VFS sub-API via proxy |
|
|
625
|
+
| `sdk.auth.*` | ✅ Implemented | Capability tokens only; middleware is product-level |
|
|
626
|
+
| `sdk.ai.*` | ✅ Implemented | Claude complete() + stream() |
|
|
627
|
+
| `sdk.events.*` | ✅ Implemented | EventEmitter, typed, local to SDK instance |
|
|
628
|
+
| `sdk.skills.*` | ✅ Implemented | VFS + bundled registry fallback |
|
|
629
|
+
| `sdk.versions.*` | ✅ Types only | Runtime proxy works; Brainy versions API |
|
|
630
|
+
| `sdk.license.*` | ❌ Not yet | Will throw if accessed |
|
|
631
|
+
| `sdk.kits.*` | ❌ Not yet | Will throw if accessed |
|
|
632
|
+
| `sdk.formats.*` | ❌ Not yet | Will throw if accessed |
|
|
633
|
+
| `sdk.billing.*` | ❌ Not yet | Will throw if accessed |
|
|
634
|
+
| `sdk.notifications.*` | ❌ Not yet | Will throw if accessed |
|
|
635
|
+
|
|
636
|
+
**5. Environment variables required:**
|
|
637
|
+
|
|
638
|
+
| Variable | Module | Required in prod |
|
|
639
|
+
|----------|--------|-----------------|
|
|
640
|
+
| `ANTHROPIC_API_KEY` | `sdk.ai` | Yes |
|
|
641
|
+
| `SOULCRAFT_IDP_URL` | Auth (OIDC mode) | Production only |
|
|
642
|
+
| `SOULCRAFT_OIDC_CLIENT_ID` | Auth (OIDC mode) | If SOULCRAFT_IDP_URL is set |
|
|
643
|
+
| `SOULCRAFT_OIDC_CLIENT_SECRET` | Auth (OIDC mode) | If SOULCRAFT_IDP_URL is set |
|
|
644
|
+
| `BETTER_AUTH_SECRET` | better-auth | Yes |
|
|
645
|
+
| `BRAINY_DATA_PATH` | BrainyInstancePool | Yes (defaults to `./brainy-data`) |
|
|
646
|
+
| `STORAGE_TYPE` | BrainyInstancePool | Yes (`filesystem` or `mmap-filesystem`) |
|