@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.
- package/dist/modules/formats/types.d.ts +6 -21
- package/dist/modules/formats/types.d.ts.map +1 -1
- package/dist/modules/formats/types.js +6 -21
- package/dist/modules/formats/types.js.map +1 -1
- package/dist/modules/kits/index.d.ts.map +1 -1
- package/dist/modules/kits/index.js +34 -13
- package/dist/modules/kits/index.js.map +1 -1
- package/dist/modules/kits/types.d.ts +66 -2
- package/dist/modules/kits/types.d.ts.map +1 -1
- package/dist/server/from-license.d.ts +35 -0
- package/dist/server/from-license.d.ts.map +1 -1
- package/dist/server/from-license.js +5 -0
- package/dist/server/from-license.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/docs/ADR-001-sdk-design.md +28 -28
- package/docs/ADR-002-transport-protocol.md +248 -0
- package/docs/ADR-003-instance-strategies.md +216 -0
- package/docs/IMPLEMENTATION-PLAN.md +22 -40
- package/docs/KIT-APP-GUIDE.md +932 -0
- package/package.json +1 -1
|
@@ -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
|