@soulcraft/sdk 1.4.6 → 1.4.8

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.
@@ -0,0 +1,932 @@
1
+ # Kit App Guide — @soulcraft/sdk
2
+
3
+ > **This guide lives at `/docs/KIT-APP-GUIDE.md` in your workspace VFS.**
4
+ > It is here for you (the developer) AND for Briggy (the AI assistant).
5
+ > Reference it any time you need to know what the SDK can do or how to use it.
6
+
7
+ ---
8
+
9
+ ## What You're Building
10
+
11
+ A kit app is a full-stack application powered by three things:
12
+
13
+ ```
14
+ @soulcraft/kits → Domain config (kit.json manifest + SKILL.md prompts)
15
+ @soulcraft/sdk → Runtime: data, AI, files, realtime, auth, notifications
16
+ Your app code → Svelte frontend + Hono/Bun backend that wires them together
17
+ ```
18
+
19
+ The SDK is your single source of truth for everything the app does at runtime.
20
+ You don't need to install Anthropic's SDK, set up WebSockets manually, or build a
21
+ file system abstraction. Everything is in `@soulcraft/sdk`.
22
+
23
+ ---
24
+
25
+ ## Quick Start
26
+
27
+ ```bash
28
+ bun add @soulcraft/sdk @soulcraft/brainy @soulcraft/kits
29
+ ```
30
+
31
+ ```typescript
32
+ // server.ts — minimal kit app server
33
+ import { Hono } from 'hono'
34
+ import { BrainyInstancePool, createSDK, createAuthMiddleware } from '@soulcraft/sdk/server'
35
+ import { AI_MODELS } from '@soulcraft/sdk'
36
+
37
+ const pool = new BrainyInstancePool({
38
+ storage: process.env.STORAGE_TYPE === 'mmap-filesystem' ? 'mmap-filesystem' : 'filesystem',
39
+ dataPath: process.env.BRAINY_DATA_PATH ?? './brainy-data',
40
+ strategy: 'per-user',
41
+ maxInstances: 100,
42
+ })
43
+
44
+ const app = new Hono()
45
+
46
+ app.get('/api/ask', async (c) => {
47
+ const brain = await pool.forUser('user-hash', 'workspace-id')
48
+ const sdk = createSDK({ brain })
49
+
50
+ const kit = await sdk.kits.load('your-kit-id')
51
+ const response = await sdk.ai.complete({
52
+ messages: [{ role: 'user', content: c.req.query('q') ?? 'Hello' }],
53
+ systemPrompt: kit?.shared?.aiPersona,
54
+ model: AI_MODELS.sonnet,
55
+ })
56
+
57
+ return c.json({ answer: response.text })
58
+ })
59
+
60
+ export default { port: 3000, fetch: app.fetch, idleTimeout: 255 }
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Import Guide
66
+
67
+ Three import paths — **always use the right one**:
68
+
69
+ ```typescript
70
+ // 1. SERVER — Hono routes, SvelteKit +server.ts, Bun scripts, background jobs
71
+ import { BrainyInstancePool, createSDK, createAuthMiddleware } from '@soulcraft/sdk/server'
72
+ import type { SoulcraftSessionUser, AuthContext } from '@soulcraft/sdk'
73
+
74
+ // 2. BROWSER — Svelte components, SvelteKit +page.svelte, kit frontend code
75
+ import { createBrainyProxy, HttpTransport, WsTransport, joinHallRoom } from '@soulcraft/sdk/client'
76
+
77
+ // 3. SHARED — Types and constants used on both sides
78
+ import { AI_MODELS } from '@soulcraft/sdk'
79
+ import type { SoulcraftKitConfig, HallRoomHandle, RoomOptions } from '@soulcraft/sdk'
80
+ ```
81
+
82
+ ---
83
+
84
+ ## SDK Modules at a Glance
85
+
86
+ | Module | Server | Browser | What it does |
87
+ |--------|--------|---------|-------------|
88
+ | `sdk.brainy` | ✅ | via proxy | Graph database: entities, relationships, semantic search |
89
+ | `sdk.vfs` | ✅ | via proxy | Virtual filesystem: documents, code, media, kit files |
90
+ | `sdk.ai` | ✅ | — | Claude AI: completions, streaming, tool calls |
91
+ | `sdk.skills` | ✅ | — | SKILL.md prompts: load, list, install custom skills |
92
+ | `sdk.kits` | ✅ | — | Kit manifests: load config, resolve template file paths |
93
+ | `sdk.events` | ✅ | — | Event bus: react to data changes, emit custom events |
94
+ | `sdk.auth` | ✅ | — | Capability tokens for cross-product server calls |
95
+ | `sdk.hall` | ✅ | `joinHallRoom()` | Realtime: video, audio, transcription, concept detection |
96
+ | `sdk.versions` | ✅ | — | Entity version history and snapshots |
97
+ | `sdk.billing` | ✅ | — | Credits, subscriptions, Stripe, usage tracking |
98
+ | `sdk.notifications` | ✅ | — | Email (Postmark) and SMS (Twilio) |
99
+ | `sdk.license` | ✅ | — | License validation, AI provider config, heartbeat |
100
+ | `sdk.formats` | ✅ | ✅ | WDOC / WSLIDE / WVIZ / WQUIZ types (no runtime object) |
101
+
102
+ ---
103
+
104
+ ## sdk.brainy — Graph Data
105
+
106
+ The core data layer. Every entity in your kit app lives here as a graph node.
107
+ Brainy provides semantic search, typed entities, and relationship traversal.
108
+
109
+ ```typescript
110
+ import { NounType, VerbType } from '@soulcraft/brainy'
111
+
112
+ // --- WRITE ---------------------------------------------------------------
113
+
114
+ // Add an entity
115
+ await sdk.brainy.add({
116
+ id: 'product-lavender-soy',
117
+ type: 'Product',
118
+ metadata: {
119
+ name: 'Lavender Soy Candle',
120
+ price: 28.00,
121
+ stock: 45,
122
+ category: 'candles',
123
+ kitId: 'your-kit-id',
124
+ },
125
+ })
126
+
127
+ // Update fields
128
+ await sdk.brainy.update({
129
+ id: 'product-lavender-soy',
130
+ metadata: { stock: 40, lastRestocked: new Date().toISOString() },
131
+ })
132
+
133
+ // Delete
134
+ await sdk.brainy.delete('product-lavender-soy')
135
+
136
+ // --- READ ----------------------------------------------------------------
137
+
138
+ // Semantic search (most powerful — uses vector similarity)
139
+ const results = await sdk.brainy.find({
140
+ query: 'soy candle lavender scent',
141
+ type: 'Product', // optional type filter
142
+ limit: 10,
143
+ })
144
+
145
+ // Keyword + filter
146
+ const inStock = await sdk.brainy.find({
147
+ query: 'candles',
148
+ filter: { stock: { $gt: 0 } },
149
+ })
150
+
151
+ // Exact lookup by ID
152
+ const product = await sdk.brainy.get('product-lavender-soy')
153
+
154
+ // --- RELATIONSHIPS -------------------------------------------------------
155
+
156
+ // Link two entities
157
+ await sdk.brainy.relate({
158
+ from: 'product-lavender-soy',
159
+ to: 'category-candles',
160
+ type: 'BelongsTo',
161
+ })
162
+
163
+ // Traverse relationships
164
+ const relations = await sdk.brainy.getRelations({
165
+ from: 'product-lavender-soy',
166
+ type: 'BelongsTo', // optional — omit to get all
167
+ })
168
+
169
+ // Remove a link
170
+ await sdk.brainy.unrelate({
171
+ from: 'product-lavender-soy',
172
+ to: 'category-candles',
173
+ type: 'BelongsTo',
174
+ })
175
+
176
+ // --- REALTIME (server-side) ----------------------------------------------
177
+ sdk.brainy.onDataChange((event) => {
178
+ // event.event: 'add' | 'update' | 'delete' | 'relate' | 'unrelate'
179
+ // event.entity — the affected entity (add/update/delete)
180
+ // event.relation — { from, to, type } (relate/unrelate)
181
+ if (event.event === 'add') broadcastNewEntity(event.entity)
182
+ })
183
+ ```
184
+
185
+ **Brainy data model:** Every entity has `id`, `type`, `metadata` (arbitrary JSON),
186
+ and optional `nounType` (from `@soulcraft/brainy`'s typed ontology). Use `type` for
187
+ your kit's domain nouns (`'Product'`, `'Recipe'`, `'Character'`). `nounType` is for
188
+ platform-level classification if needed.
189
+
190
+ ---
191
+
192
+ ## sdk.vfs — Virtual Filesystem
193
+
194
+ Store documents, generated content, kit template files, and any binary data.
195
+ VFS paths are hierarchical strings starting with `/`.
196
+
197
+ ```typescript
198
+ // --- WRITE ---------------------------------------------------------------
199
+
200
+ // Write a text document
201
+ await sdk.vfs.writeFile('/projects/my-novel/chapter-01.wdoc', docContent)
202
+
203
+ // Write binary (Buffer or Uint8Array)
204
+ await sdk.vfs.writeFile('/exports/report.pdf', pdfBuffer)
205
+
206
+ // Create a directory
207
+ await sdk.vfs.mkdir('/projects/new-project', { recursive: true })
208
+
209
+ // --- READ ----------------------------------------------------------------
210
+
211
+ // Read file — always returns Buffer; convert for text
212
+ const buffer = await sdk.vfs.readFile('/projects/my-novel/chapter-01.wdoc')
213
+ const text = buffer.toString('utf-8')
214
+
215
+ // List directory contents
216
+ const entries = await sdk.vfs.readdir('/projects')
217
+ // → ['my-novel', 'research-notes', 'exports']
218
+
219
+ // File metadata
220
+ const stat = await sdk.vfs.stat('/projects/my-novel/chapter-01.wdoc')
221
+ // → { size: 4821, mtime: Date, isDirectory: false }
222
+
223
+ // --- MANAGE --------------------------------------------------------------
224
+ await sdk.vfs.rename('/projects/draft-name', '/projects/final-name')
225
+ await sdk.vfs.unlink('/exports/old-report.pdf')
226
+ await sdk.vfs.rmdir('/projects/abandoned-project', { recursive: true })
227
+ ```
228
+
229
+ **Path conventions used across the platform:**
230
+ ```
231
+ /projects/{name}/ Kit app project files
232
+ /skills/{kitId}/ Custom skill overrides (.md files)
233
+ /docs/ Workspace documentation (including this file)
234
+ /exports/ Generated output files
235
+ ```
236
+
237
+ ---
238
+
239
+ ## sdk.ai — Claude AI Integration
240
+
241
+ All three model tiers, completion and streaming, with tool call support.
242
+ Requires `ANTHROPIC_API_KEY` in your environment.
243
+
244
+ ```typescript
245
+ import { AI_MODELS } from '@soulcraft/sdk'
246
+
247
+ // --- BASIC COMPLETION ----------------------------------------------------
248
+
249
+ const response = await sdk.ai.complete({
250
+ messages: [{ role: 'user', content: 'What candles are running low on stock?' }],
251
+ systemPrompt: kit.shared?.aiPersona, // from kit.json → shared.aiPersona
252
+ model: AI_MODELS.haiku, // haiku for fast/cheap, sonnet for most tasks
253
+ })
254
+
255
+ // response.text — Claude's reply (null if only tool calls returned)
256
+ // response.toolCalls — Array of { name, input } (null if no tool calls)
257
+ // response.stopReason — 'end_turn' | 'tool_use' | 'max_tokens'
258
+ // response.usage — { inputTokens, outputTokens }
259
+
260
+ // --- WITH TOOL CALLS -----------------------------------------------------
261
+
262
+ const response = await sdk.ai.complete({
263
+ messages: [{ role: 'user', content: 'Find all products under $20' }],
264
+ systemPrompt: kit.shared?.aiPersona,
265
+ model: AI_MODELS.sonnet,
266
+ tools: [
267
+ {
268
+ name: 'search_products',
269
+ description: 'Search the product inventory by query and price range',
270
+ inputSchema: {
271
+ type: 'object',
272
+ properties: {
273
+ query: { type: 'string', description: 'Search terms' },
274
+ maxPrice: { type: 'number', description: 'Maximum price filter' },
275
+ },
276
+ required: ['query'],
277
+ },
278
+ },
279
+ ],
280
+ })
281
+
282
+ if (response.toolCalls) {
283
+ for (const call of response.toolCalls) {
284
+ if (call.name === 'search_products') {
285
+ const { query, maxPrice } = call.input as { query: string; maxPrice?: number }
286
+ const products = await sdk.brainy.find({
287
+ query,
288
+ filter: maxPrice ? { price: { $lte: maxPrice } } : undefined,
289
+ })
290
+ // Append tool result to messages and call sdk.ai.complete() again
291
+ }
292
+ }
293
+ }
294
+
295
+ // --- STREAMING -----------------------------------------------------------
296
+
297
+ const stream = sdk.ai.stream({
298
+ messages: [{ role: 'user', content: 'Write a product description for lavender candles' }],
299
+ systemPrompt: 'You are a product copywriter for a candle shop.',
300
+ model: AI_MODELS.sonnet,
301
+ })
302
+
303
+ for await (const event of stream) {
304
+ if (event.type === 'text') process.stdout.write(event.text)
305
+ if (event.type === 'tool_use') console.log('Tool call:', event.toolCall.name)
306
+ if (event.type === 'done') console.log('Tokens used:', event.result.usage)
307
+ }
308
+ ```
309
+
310
+ ### Model Tiers
311
+
312
+ | Constant | When to use |
313
+ |----------|-------------|
314
+ | `AI_MODELS.haiku` | Fast lookups, data extraction, classification, simple Q&A (cheapest) |
315
+ | `AI_MODELS.sonnet` | Most kit operations: writing, analysis, reasoning, summaries |
316
+ | `AI_MODELS.opus` | Complex multi-step reasoning, long-form synthesis, deep analysis |
317
+
318
+ ---
319
+
320
+ ## sdk.skills — Domain Skills
321
+
322
+ Skills are `SKILL.md` files that define a kit's AI persona, domain knowledge,
323
+ and step-by-step workflows. The SDK loads them from VFS first (user overrides),
324
+ then falls back to the bundled `@soulcraft/kits` registry.
325
+
326
+ ```typescript
327
+ // Load a single skill (VFS → bundled fallback)
328
+ const skill = await sdk.skills.load('inventory-health', 'your-kit-id')
329
+ if (skill) {
330
+ // skill.id — 'inventory-health'
331
+ // skill.kitId — 'your-kit-id'
332
+ // skill.content — full SKILL.md string (use as system prompt context)
333
+ // skill.source — 'vfs' (user-customized) or 'bundled' (from npm package)
334
+ const systemPrompt = buildSystemPrompt(kit, skill.content)
335
+ }
336
+
337
+ // Load all skills for a kit
338
+ const skills = await sdk.skills.list({ kitId: 'your-kit-id' })
339
+ const systemPrompt = [
340
+ kit.shared?.aiPersona,
341
+ ...skills.map(s => s.content),
342
+ ].join('\n\n---\n\n')
343
+
344
+ // List all skills across all kits
345
+ const allSkills = await sdk.skills.list()
346
+
347
+ // Install a custom skill into VFS (overrides bundled version for this workspace)
348
+ await sdk.skills.install({
349
+ id: 'custom-upsell',
350
+ kitId: 'your-kit-id',
351
+ content: `---
352
+ id: custom-upsell
353
+ title: Upsell Flow
354
+ ---
355
+ You help customers discover complementary products...`,
356
+ })
357
+ ```
358
+
359
+ **VFS path convention:** `/skills/{kitId}/{skillId}.md`
360
+
361
+ ---
362
+
363
+ ## sdk.kits — Kit Registry + File Paths
364
+
365
+ Access kit manifests and resolve template file locations on disk.
366
+
367
+ ```typescript
368
+ // Load a kit's config from the npm registry
369
+ const kit = await sdk.kits.load('wicks-and-whiskers')
370
+ if (kit) {
371
+ console.log(kit.id) // 'wicks-and-whiskers'
372
+ console.log(kit.name) // 'Wicks & Whiskers'
373
+ console.log(kit.description) // one-sentence summary
374
+ console.log(kit.type) // 'venue' | 'content' | 'app' | 'academy' | 'soulcraft'
375
+ console.log(kit.shared?.aiPersona) // AI system prompt from kit.json
376
+ console.log(kit.shared?.suggestions) // array of { label, prompt } suggestion chips
377
+ }
378
+
379
+ // List all available kits
380
+ const kits = await sdk.kits.list()
381
+ console.log(kits.map(k => `${k.id}: ${k.name}`))
382
+
383
+ // Resolve template file paths (for reading kit starter files at initialization)
384
+ const workshopFilesDir = await sdk.kits.resolveFilesPath('blog-series', 'workshop')
385
+ // → '/path/to/node_modules/@soulcraft/kits/kits/blog-series/workshop/files'
386
+
387
+ const venueFilesDir = await sdk.kits.resolveFilesPath('wicks-and-whiskers', 'venue')
388
+ // → '/path/to/node_modules/@soulcraft/kits/kits/wicks-and-whiskers/venue/files'
389
+
390
+ const skillsDir = await sdk.kits.resolveSkillsPath('blog-series')
391
+ // → '/path/to/node_modules/@soulcraft/kits/kits/blog-series/skills'
392
+
393
+ const kitsRoot = await sdk.kits.resolveKitsRoot()
394
+ // → '/path/to/node_modules/@soulcraft/kits/kits'
395
+ ```
396
+
397
+ ---
398
+
399
+ ## sdk.events — Platform Event Bus
400
+
401
+ Subscribe to and emit typed events across your app.
402
+ Events are local to the SDK instance (same server process).
403
+
404
+ ```typescript
405
+ // --- BUILT-IN EVENTS -----------------------------------------------------
406
+
407
+ // React to Brainy data changes
408
+ sdk.events.on('brainy:change', (event) => {
409
+ // event.event: 'add' | 'update' | 'delete' | 'relate' | 'unrelate'
410
+ if (event.event === 'add') console.log('New entity:', event.entity?.id)
411
+ })
412
+
413
+ // React to VFS changes
414
+ sdk.events.on('vfs:write', ({ path, content }) => {
415
+ if (path.endsWith('.wdoc')) notifyCollaborators(path)
416
+ })
417
+
418
+ sdk.events.on('vfs:delete', ({ path }) => { invalidateCache(path) })
419
+ sdk.events.on('vfs:rename', ({ from, to }) => { updateFileTree(from, to) })
420
+
421
+ // --- CUSTOM EVENTS -------------------------------------------------------
422
+
423
+ // Step 1: Declare your types (in a .d.ts file or at the top of your types.ts)
424
+ declare module '@soulcraft/sdk' {
425
+ interface SoulcraftEventMap {
426
+ 'order:placed': { orderId: string; total: number; userId: string }
427
+ 'inventory:low': { productId: string; stock: number; threshold: number }
428
+ }
429
+ }
430
+
431
+ // Step 2: Emit + subscribe with full TypeScript type safety
432
+ sdk.events.emit('order:placed', { orderId: 'ord-123', total: 84.00, userId: user.id })
433
+
434
+ sdk.events.on('inventory:low', ({ productId, stock }) => {
435
+ sendLowStockAlert(productId, stock)
436
+ })
437
+
438
+ // Remove a listener
439
+ const handler = (event) => { ... }
440
+ sdk.events.on('brainy:change', handler)
441
+ sdk.events.off('brainy:change', handler)
442
+ ```
443
+
444
+ ---
445
+
446
+ ## sdk.hall — Realtime Communication
447
+
448
+ Video, audio, live transcription, and AI concept detection.
449
+ Runs as a standalone service at `hall.soulcraft.com`.
450
+
451
+ **Architecture:**
452
+ ```
453
+ Your server ──(product secret)──► Hall server ◄──(session token)── Browser
454
+ ```
455
+ Product secrets are server-only. Browsers get short-lived session tokens issued by your server.
456
+
457
+ ### Server Setup
458
+
459
+ ```typescript
460
+ import { createHallModule } from '@soulcraft/sdk/server'
461
+
462
+ const hall = createHallModule({
463
+ url: process.env.HALL_URL ?? 'wss://hall.soulcraft.com',
464
+ productName: 'workshop', // must match hall.toml [auth.products]
465
+ secret: process.env.HALL_WORKSHOP_SECRET!, // never exposed to browsers
466
+ })
467
+
468
+ await hall.connect()
469
+
470
+ // Create a room (e.g. when a live session starts)
471
+ const room = await hall.createRoom('session-abc123', {
472
+ maxPeers: 8,
473
+ enableTranscription: true,
474
+ enableRecording: false,
475
+ concepts: [
476
+ { nodeId: 'node-1', name: 'Maillard reaction', nounType: 'concept' },
477
+ ],
478
+ })
479
+
480
+ // Server-side room events
481
+ room.on('transcriptEvent', ({ text, isFinal, speakerId }) => {
482
+ if (isFinal) saveTranscriptLine(text)
483
+ })
484
+
485
+ room.on('conceptMentionEvent', ({ nodeId, name, confidence }) => {
486
+ console.log(`Concept mentioned: "${name}" (${(confidence * 100).toFixed(0)}%)`)
487
+ })
488
+
489
+ room.on('relationProposedEvent', async ({ fromNodeId, toNodeId, verb, confidence }) => {
490
+ if (confidence > 0.75) {
491
+ await sdk.brainy.relate({ from: fromNodeId, to: toNodeId, type: verb })
492
+ }
493
+ })
494
+
495
+ room.on('speakerChangedEvent', ({ peerId }) => { updateActiveSpeaker(peerId) })
496
+ room.on('peerJoinedEvent', ({ peerId, displayName }) => { addParticipant(peerId, displayName) })
497
+ room.on('peerLeftEvent', ({ peerId }) => { removeParticipant(peerId) })
498
+ ```
499
+
500
+ ### Issue Session Tokens (Server → Browser)
501
+
502
+ ```typescript
503
+ app.post('/api/hall/token', requireAuth, async (c) => {
504
+ const user = c.get('user')!
505
+ const { roomId } = await c.req.json<{ roomId: string }>()
506
+
507
+ const token = await hall.createSessionToken({
508
+ roomId,
509
+ userId: user.id,
510
+ displayName: user.name ?? user.email,
511
+ // ttlMs: 300_000 // default: 5 minutes
512
+ })
513
+
514
+ return c.json({ token })
515
+ })
516
+ ```
517
+
518
+ ### Browser — Join a Room
519
+
520
+ ```typescript
521
+ import { joinHallRoom } from '@soulcraft/sdk/client'
522
+ import type { HallRoomHandle } from '@soulcraft/sdk'
523
+
524
+ // 1. Get a session token from your backend
525
+ const { token } = await fetch('/api/hall/token', {
526
+ method: 'POST',
527
+ headers: { 'Content-Type': 'application/json' },
528
+ body: JSON.stringify({ roomId: 'session-abc123' }),
529
+ }).then(r => r.json())
530
+
531
+ // 2. Join the room
532
+ const room: HallRoomHandle = await joinHallRoom({
533
+ url: 'wss://hall.soulcraft.com',
534
+ roomId: 'session-abc123',
535
+ token,
536
+ })
537
+
538
+ // 3. Add your media stream
539
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true })
540
+ room.addStream(stream)
541
+
542
+ // 4. Handle remote peers
543
+ room.on('trackAdded', ({ peerId, track, stream }) => {
544
+ const videoEl = document.getElementById(`peer-${peerId}`) as HTMLVideoElement
545
+ videoEl.srcObject = stream
546
+ })
547
+
548
+ room.on('transcriptEvent', ({ text, isFinal }) => {
549
+ appendSubtitle(text, isFinal)
550
+ })
551
+
552
+ room.on('conceptMentionEvent', ({ nodeId, confidence }) => {
553
+ highlightConcept(nodeId, confidence)
554
+ })
555
+
556
+ // 5. Clean up
557
+ room.close()
558
+ ```
559
+
560
+ ---
561
+
562
+ ## Client Transports (Browser → Server Brainy)
563
+
564
+ Kit frontend code connects to the server Brainy instance via one of four transports:
565
+
566
+ ```typescript
567
+ import { createBrainyProxy, HttpTransport, WsTransport, SseTransport } from '@soulcraft/sdk/client'
568
+
569
+ // HTTP — simple, stateless (best for read-heavy kit apps)
570
+ const transport = new HttpTransport('https://your-app.soulcraft.com')
571
+ const brainy = createBrainyProxy(transport)
572
+ const items = await brainy.find({ query: 'inventory' })
573
+
574
+ // WebSocket — bidirectional, real-time change push (best for collaborative apps)
575
+ const wsTransport = new WsTransport(
576
+ 'wss://your-app.soulcraft.com/api/brainy/ws',
577
+ capabilityToken,
578
+ 'scope-key' // optional workspace/scope key
579
+ )
580
+ await wsTransport.connect()
581
+ const brainy = createBrainyProxy(wsTransport)
582
+
583
+ brainy.onDataChange((event) => {
584
+ console.log('Live change:', event.event, event.entity?.id)
585
+ })
586
+
587
+ // SSE — lightweight server push (for read-only live dashboards)
588
+ const sseTransport = new SseTransport('https://your-app.soulcraft.com/api/brainy/sse')
589
+ const brainy = createBrainyProxy(sseTransport)
590
+ ```
591
+
592
+ | Transport | Use when |
593
+ |-----------|---------|
594
+ | HTTP | Read-heavy, stateless kit app pages |
595
+ | WebSocket | Real-time collaboration, live editing, multiplayer |
596
+ | SSE | Dashboards that only need server-push (no writes from client) |
597
+
598
+ ---
599
+
600
+ ## BrainyInstancePool — Managing Data Storage
601
+
602
+ One pool per process. The pool creates, caches, and evicts Brainy instances.
603
+
604
+ ```typescript
605
+ import { BrainyInstancePool } from '@soulcraft/sdk/server'
606
+
607
+ const pool = new BrainyInstancePool({
608
+ storage: 'mmap-filesystem', // 'filesystem' for dev, 'mmap-filesystem' for production GCE
609
+ dataPath: '/mnt/brainy-data', // './brainy-data' for dev
610
+ strategy: 'per-user', // 'per-user' | 'per-tenant' | 'per-scope'
611
+ maxInstances: 200,
612
+ flushOnEvict: true, // save to disk when LRU evicts
613
+ onInit: async (brain, storagePath) => {
614
+ await runMigrations(brain) // optional: runs after first open
615
+ },
616
+ })
617
+
618
+ // per-user (Workshop pattern — one Brainy per user per workspace)
619
+ const brain = await pool.forUser(user.emailHash, workspaceId)
620
+
621
+ // per-tenant (Venue pattern — one shared Brainy per tenant/location)
622
+ const brain = await pool.forTenant('wicks-charlotte')
623
+
624
+ // per-scope (custom key — for multi-brain scenarios)
625
+ const brain = await pool.forScope(
626
+ `content:${courseId}`,
627
+ () => initBrainy(`content/${courseId}`, storagePath)
628
+ )
629
+
630
+ // Shutdown (flush all, clear cache)
631
+ await pool.shutdown()
632
+ ```
633
+
634
+ ---
635
+
636
+ ## Authentication
637
+
638
+ ### Middleware (Hono)
639
+
640
+ ```typescript
641
+ import { betterAuth } from 'better-auth'
642
+ import { Database } from 'bun:sqlite'
643
+ import {
644
+ SOULCRAFT_USER_FIELDS,
645
+ SOULCRAFT_SESSION_CONFIG,
646
+ computeEmailHash,
647
+ createAuthMiddleware,
648
+ } from '@soulcraft/sdk/server'
649
+
650
+ const auth = betterAuth({
651
+ database: new Database('./auth.db'),
652
+ secret: process.env.BETTER_AUTH_SECRET!,
653
+ session: SOULCRAFT_SESSION_CONFIG,
654
+ user: { additionalFields: SOULCRAFT_USER_FIELDS },
655
+ databaseHooks: {
656
+ user: {
657
+ create: {
658
+ before: async (user) => ({
659
+ data: { ...user, emailHash: computeEmailHash(user.email), platformRole: 'creator' }
660
+ }),
661
+ },
662
+ },
663
+ },
664
+ })
665
+
666
+ const { requireAuth, optionalAuth } = createAuthMiddleware(auth)
667
+
668
+ app.all('/api/auth/*', (c) => auth.handler(c.req.raw))
669
+
670
+ app.get('/api/data', requireAuth, async (c) => {
671
+ const user = c.get('user')! // SoulcraftSessionUser — id, email, emailHash, platformRole
672
+ const brain = await pool.forUser(user.emailHash, 'main')
673
+ const sdk = createSDK({ brain })
674
+ // ...
675
+ })
676
+ ```
677
+
678
+ ### Capability Tokens (Cross-Product Server Calls)
679
+
680
+ ```typescript
681
+ import { createCapabilityToken, verifyCapabilityToken } from '@soulcraft/sdk/server'
682
+
683
+ // Server A — issue a scoped token
684
+ const token = await createCapabilityToken({
685
+ email: user.email,
686
+ scope: 'my-tenant',
687
+ secret: process.env.CROSS_PRODUCT_SECRET!,
688
+ ttlMs: 3_600_000, // 1 hour
689
+ })
690
+
691
+ // Server B — verify in the handler
692
+ const claims = await verifyCapabilityToken(token, process.env.CROSS_PRODUCT_SECRET!)
693
+ if (!claims) return c.json({ error: 'Unauthorized' }, 401)
694
+ ```
695
+
696
+ ---
697
+
698
+ ## Notifications
699
+
700
+ ```typescript
701
+ // Send email (uses Postmark when POSTMARK_SERVER_TOKEN is set; logs to console in dev)
702
+ await sdk.notifications.send({
703
+ type: 'email',
704
+ to: 'user@example.com',
705
+ subject: 'Your order is ready',
706
+ html: '<h1>Ready!</h1><p>Your candle order has shipped.</p>',
707
+ text: 'Your candle order has shipped.',
708
+ })
709
+
710
+ // Send SMS (uses Twilio when TWILIO_* env vars are set; logs to console in dev)
711
+ await sdk.notifications.send({
712
+ type: 'sms',
713
+ to: '+15555550100',
714
+ body: 'Your order has shipped!',
715
+ })
716
+ ```
717
+
718
+ In development (no API keys configured), both calls succeed silently and print to console.
719
+ No mock setup required.
720
+
721
+ ---
722
+
723
+ ## Content Formats
724
+
725
+ The SDK re-exports Soulcraft's portable content format types from `@soulcraft/formats`.
726
+ These are types only — no runtime object on the SDK instance.
727
+
728
+ ```typescript
729
+ import type {
730
+ WdocDocument, // .wdoc — Workshop Document (TipTap/ProseMirror)
731
+ WslideSlide, // .wslide — Workshop Slide (presentation format)
732
+ WvizDocument, // .wviz — Workshop Visualization (portable graph/chart)
733
+ WquizQuestion, // .wquiz — Workshop Quiz (multiple choice, fill-in, etc.)
734
+ } from '@soulcraft/sdk'
735
+
736
+ // Parse helpers (from @soulcraft/formats directly):
737
+ import { parseWviz, parseWdoc } from '@soulcraft/formats'
738
+
739
+ const vizData = parseWviz(await sdk.vfs.readFile('/projects/knowledge-graph.wviz'))
740
+ ```
741
+
742
+ | Format | Extension | Description |
743
+ |--------|-----------|-------------|
744
+ | `WdocDocument` | `.wdoc` | Rich text document (TipTap schema) |
745
+ | `WslideSlide` | `.wslide` | Presentation slide with layout + speaker notes |
746
+ | `WvizDocument` | `.wviz` | Self-contained interactive graph visualization |
747
+ | `WquizQuestion` | `.wquiz` | Quiz question with answer options and validation |
748
+
749
+ ---
750
+
751
+ ## Version History
752
+
753
+ ```typescript
754
+ // Get the full version history for an entity
755
+ const history = await sdk.versions.getHistory('product-lavender-soy')
756
+ // → Array<{ version: number, timestamp: string, metadata: Record<string, unknown> }>
757
+
758
+ // Restore a previous version
759
+ await sdk.versions.restore('product-lavender-soy', 3)
760
+ ```
761
+
762
+ ---
763
+
764
+ ## Full Application Template
765
+
766
+ A complete kit app server combining auth, data, AI, and events:
767
+
768
+ ```typescript
769
+ import { Hono } from 'hono'
770
+ import { betterAuth } from 'better-auth'
771
+ import { Database } from 'bun:sqlite'
772
+ import {
773
+ BrainyInstancePool,
774
+ createSDK,
775
+ createAuthMiddleware,
776
+ SOULCRAFT_USER_FIELDS,
777
+ SOULCRAFT_SESSION_CONFIG,
778
+ computeEmailHash,
779
+ } from '@soulcraft/sdk/server'
780
+ import { AI_MODELS } from '@soulcraft/sdk'
781
+
782
+ // ── Kit config ────────────────────────────────────────────────────────────────
783
+
784
+ const KIT_ID = 'your-kit-id'
785
+
786
+ // ── Auth ──────────────────────────────────────────────────────────────────────
787
+
788
+ const auth = betterAuth({
789
+ database: new Database('./auth.db'),
790
+ secret: process.env.BETTER_AUTH_SECRET!,
791
+ session: SOULCRAFT_SESSION_CONFIG,
792
+ user: { additionalFields: SOULCRAFT_USER_FIELDS },
793
+ databaseHooks: {
794
+ user: {
795
+ create: {
796
+ before: async (u) => ({
797
+ data: { ...u, emailHash: computeEmailHash(u.email), platformRole: 'creator' },
798
+ }),
799
+ },
800
+ },
801
+ },
802
+ })
803
+
804
+ const { requireAuth } = createAuthMiddleware(auth)
805
+
806
+ // ── Brainy pool ───────────────────────────────────────────────────────────────
807
+
808
+ const pool = new BrainyInstancePool({
809
+ storage: process.env.STORAGE_TYPE === 'mmap-filesystem' ? 'mmap-filesystem' : 'filesystem',
810
+ dataPath: process.env.BRAINY_DATA_PATH ?? './brainy-data',
811
+ strategy: 'per-user',
812
+ maxInstances: 200,
813
+ flushOnEvict: true,
814
+ })
815
+
816
+ // ── App ───────────────────────────────────────────────────────────────────────
817
+
818
+ const app = new Hono()
819
+
820
+ app.all('/api/auth/*', (c) => auth.handler(c.req.raw))
821
+
822
+ app.get('/api/items', requireAuth, async (c) => {
823
+ const user = c.get('user')!
824
+ const brain = await pool.forUser(user.emailHash, 'main')
825
+ const sdk = createSDK({ brain })
826
+
827
+ const items = await sdk.brainy.find({
828
+ query: 'all items',
829
+ limit: 100,
830
+ })
831
+
832
+ return c.json({ items })
833
+ })
834
+
835
+ app.post('/api/ask', requireAuth, async (c) => {
836
+ const user = c.get('user')!
837
+ const { question } = await c.req.json<{ question: string }>()
838
+
839
+ const brain = await pool.forUser(user.emailHash, 'main')
840
+ const sdk = createSDK({ brain })
841
+
842
+ const kit = await sdk.kits.load(KIT_ID)
843
+ const skills = await sdk.skills.list({ kitId: KIT_ID })
844
+
845
+ const systemPrompt = [
846
+ kit?.shared?.aiPersona,
847
+ ...skills.map(s => s.content),
848
+ ].filter(Boolean).join('\n\n---\n\n')
849
+
850
+ const response = await sdk.ai.complete({
851
+ messages: [{ role: 'user', content: question }],
852
+ systemPrompt,
853
+ model: AI_MODELS.sonnet,
854
+ })
855
+
856
+ return c.json({ answer: response.text })
857
+ })
858
+
859
+ // ── Server ────────────────────────────────────────────────────────────────────
860
+
861
+ export default {
862
+ port: Number(process.env.PORT ?? 3000),
863
+ fetch: app.fetch,
864
+ idleTimeout: 255, // CRITICAL: prevents Bun's 10s default from killing long Brainy inits
865
+ }
866
+ ```
867
+
868
+ ---
869
+
870
+ ## Environment Variables
871
+
872
+ | Variable | Module | Required |
873
+ |----------|--------|---------|
874
+ | `ANTHROPIC_API_KEY` | `sdk.ai` | Yes |
875
+ | `BETTER_AUTH_SECRET` | Auth | Yes |
876
+ | `BRAINY_DATA_PATH` | Pool | No (default: `./brainy-data`) |
877
+ | `STORAGE_TYPE` | Pool | No (default: `filesystem`) |
878
+ | `SOULCRAFT_IDP_URL` | Auth (OIDC) | Production only |
879
+ | `SOULCRAFT_OIDC_CLIENT_ID` | Auth (OIDC) | If IDP_URL is set |
880
+ | `SOULCRAFT_OIDC_CLIENT_SECRET` | Auth (OIDC) | If IDP_URL is set |
881
+ | `HALL_URL` | `sdk.hall` | If using realtime |
882
+ | `HALL_{PRODUCT}_SECRET` | `sdk.hall` | If using realtime |
883
+ | `POSTMARK_SERVER_TOKEN` | `sdk.notifications` | For email delivery |
884
+ | `TWILIO_ACCOUNT_SID` | `sdk.notifications` | For SMS delivery |
885
+ | `TWILIO_AUTH_TOKEN` | `sdk.notifications` | For SMS delivery |
886
+ | `STRIPE_SECRET_KEY` | `sdk.billing` | For subscriptions |
887
+ | `SOULCRAFT_LICENSE_KEY` | `sdk.license` | `SC-XXXX-XXXX-XXXX` format |
888
+
889
+ ---
890
+
891
+ ## For Briggy (AI Assistant)
892
+
893
+ When generating kit app code, always:
894
+
895
+ 1. **Import from the correct entry point** — server code from `@soulcraft/sdk/server`,
896
+ browser code from `@soulcraft/sdk/client`, types from `@soulcraft/sdk`.
897
+
898
+ 2. **One pool per process, one SDK per request:**
899
+ ```typescript
900
+ // ✅ Correct
901
+ const pool = new BrainyInstancePool(...) // module-level singleton
902
+ const brain = await pool.forUser(hash, workspaceId) // in the request handler
903
+ const sdk = createSDK({ brain }) // in the request handler
904
+ ```
905
+
906
+ 3. **Always use `AI_MODELS.*` constants** — never hardcode model ID strings.
907
+
908
+ 4. **Load kit context before AI calls:**
909
+ ```typescript
910
+ const kit = await sdk.kits.load(kitId)
911
+ const skills = await sdk.skills.list({ kitId })
912
+ const systemPrompt = [kit?.shared?.aiPersona, ...skills.map(s => s.content)].join('\n\n')
913
+ ```
914
+
915
+ 5. **`resolveFilesPath` is for initialization only** — read kit template files once at
916
+ workspace creation time, then store them in VFS. Don't call it in request handlers.
917
+
918
+ 6. **Bun server requires `idleTimeout: 255`** — Bun's default 10-second idle timeout kills
919
+ requests during Brainy cold start (~25s). Always include it in the server export.
920
+
921
+ 7. **Events are process-local** — `sdk.events` does not cross process or WebSocket
922
+ boundaries. For cross-client real-time, use the WebSocket transport or Hall.
923
+
924
+ ---
925
+
926
+ ## See Also
927
+
928
+ - `docs/USAGE.md` — Complete SDK reference with all options and edge cases
929
+ - `docs/ADR-001-sdk-design.md` — Architecture decisions and design rationale
930
+ - `@soulcraft/kit-schema` — Full kit manifest schema with product-specific fields
931
+ - `@soulcraft/kits` — The kit registry package (55+ domain kits)
932
+ - `@soulcraft/formats` — WDOC / WSLIDE / WVIZ / WQUIZ types and parse helpers