@soederpop/luca 0.0.32 → 0.0.34

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.
Files changed (86) hide show
  1. package/README.md +241 -36
  2. package/bun.lock +24 -5
  3. package/commands/build-python-bridge.ts +43 -0
  4. package/docs/apis/clients/rest.md +7 -7
  5. package/docs/apis/clients/websocket.md +23 -10
  6. package/docs/apis/features/agi/assistant.md +155 -8
  7. package/docs/apis/features/agi/assistants-manager.md +90 -22
  8. package/docs/apis/features/agi/auto-assistant.md +377 -0
  9. package/docs/apis/features/agi/browser-use.md +802 -0
  10. package/docs/apis/features/agi/claude-code.md +6 -1
  11. package/docs/apis/features/agi/conversation-history.md +7 -6
  12. package/docs/apis/features/agi/conversation.md +111 -38
  13. package/docs/apis/features/agi/docs-reader.md +35 -57
  14. package/docs/apis/features/agi/file-tools.md +163 -0
  15. package/docs/apis/features/agi/openapi.md +2 -2
  16. package/docs/apis/features/agi/skills-library.md +227 -0
  17. package/docs/apis/features/node/content-db.md +125 -4
  18. package/docs/apis/features/node/disk-cache.md +11 -11
  19. package/docs/apis/features/node/downloader.md +1 -1
  20. package/docs/apis/features/node/file-manager.md +15 -15
  21. package/docs/apis/features/node/fs.md +78 -21
  22. package/docs/apis/features/node/git.md +50 -10
  23. package/docs/apis/features/node/google-calendar.md +3 -0
  24. package/docs/apis/features/node/google-docs.md +10 -1
  25. package/docs/apis/features/node/google-drive.md +3 -0
  26. package/docs/apis/features/node/google-mail.md +214 -0
  27. package/docs/apis/features/node/google-sheets.md +3 -0
  28. package/docs/apis/features/node/ink.md +10 -10
  29. package/docs/apis/features/node/ipc-socket.md +83 -93
  30. package/docs/apis/features/node/networking.md +5 -5
  31. package/docs/apis/features/node/os.md +7 -7
  32. package/docs/apis/features/node/package-finder.md +14 -14
  33. package/docs/apis/features/node/proc.md +2 -1
  34. package/docs/apis/features/node/process-manager.md +70 -3
  35. package/docs/apis/features/node/python.md +265 -9
  36. package/docs/apis/features/node/redis.md +380 -0
  37. package/docs/apis/features/node/ui.md +13 -13
  38. package/docs/apis/servers/express.md +35 -7
  39. package/docs/apis/servers/mcp.md +3 -3
  40. package/docs/apis/servers/websocket.md +51 -8
  41. package/docs/bootstrap/CLAUDE.md +1 -1
  42. package/docs/bootstrap/SKILL.md +93 -7
  43. package/docs/examples/feature-as-tool-provider.md +143 -0
  44. package/docs/examples/python.md +42 -1
  45. package/docs/introspection.md +15 -5
  46. package/docs/tutorials/00-bootstrap.md +3 -3
  47. package/docs/tutorials/02-container.md +2 -2
  48. package/docs/tutorials/10-creating-features.md +5 -0
  49. package/docs/tutorials/13-introspection.md +12 -2
  50. package/docs/tutorials/19-python-sessions.md +401 -0
  51. package/package.json +8 -4
  52. package/src/agi/container.server.ts +8 -0
  53. package/src/agi/features/assistant.ts +18 -0
  54. package/src/agi/features/autonomous-assistant.ts +435 -0
  55. package/src/agi/features/conversation.ts +58 -6
  56. package/src/agi/features/file-tools.ts +286 -0
  57. package/src/agi/features/luca-coder.ts +643 -0
  58. package/src/bootstrap/generated.ts +705 -17
  59. package/src/cli/build-info.ts +2 -2
  60. package/src/cli/cli.ts +22 -13
  61. package/src/commands/bootstrap.ts +49 -6
  62. package/src/commands/code.ts +369 -0
  63. package/src/commands/describe.ts +7 -2
  64. package/src/commands/index.ts +1 -0
  65. package/src/commands/sandbox-mcp.ts +7 -7
  66. package/src/commands/save-api-docs.ts +1 -1
  67. package/src/container-describer.ts +4 -4
  68. package/src/container.ts +10 -19
  69. package/src/helper.ts +24 -33
  70. package/src/introspection/generated.agi.ts +3026 -590
  71. package/src/introspection/generated.node.ts +1625 -688
  72. package/src/introspection/generated.web.ts +15 -57
  73. package/src/node/container.ts +5 -0
  74. package/src/node/features/figlet-fonts.ts +597 -0
  75. package/src/node/features/fs.ts +3 -9
  76. package/src/node/features/helpers.ts +20 -0
  77. package/src/node/features/python.ts +429 -16
  78. package/src/node/features/redis.ts +446 -0
  79. package/src/node/features/ui.ts +4 -11
  80. package/src/python/bridge.py +220 -0
  81. package/src/python/generated.ts +227 -0
  82. package/src/scaffolds/generated.ts +1 -1
  83. package/test/python-session.test.ts +105 -0
  84. package/assistants/lucaExpert/CORE.md +0 -37
  85. package/assistants/lucaExpert/hooks.ts +0 -9
  86. package/assistants/lucaExpert/tools.ts +0 -177
@@ -1,5 +1,5 @@
1
1
  // Auto-generated bootstrap content
2
- // Generated at: 2026-03-24T09:09:11.574Z
2
+ // Generated at: 2026-03-26T03:30:22.249Z
3
3
  // Source: docs/bootstrap/*.md, docs/bootstrap/templates/*, docs/examples/*.md, docs/tutorials/*.md
4
4
  //
5
5
  // Do not edit manually. Run: luca build-bootstrap
@@ -25,7 +25,7 @@ There are three things to learn, in this order:
25
25
 
26
26
  ## Phase 1: Discover with \`luca describe\`
27
27
 
28
- This is your primary tool. Before reading source files, searching for APIs, or writing any codeask describe. It outputs full documentation for any part of the container: methods, options, events, state, examples.
28
+ This is your primary tool. The \`luca\` binary is a compiled artifact that bundles all introspection data it is the authority on what the container provides. Run \`luca describe\` first — it outputs full documentation for any part of the container: methods, options, events, state, examples. Reading source can be helpful for additional context if it exists in the project, but the source for built-in helpers may not be present — the binary is always the ground truth.
29
29
 
30
30
  ### See what's available
31
31
 
@@ -77,6 +77,18 @@ luca describe fs git --examples # just examples for both
77
77
  luca describe fs --usage --methods # combine sections
78
78
  \`\`\`
79
79
 
80
+ ### Get approximate TypeScript types
81
+
82
+ Need to know the shape of a helper for type-safe code? Use \`--ts\`:
83
+
84
+ \`\`\`shell
85
+ luca describe fs --ts # approximate TS interface for fs
86
+ luca describe conversation --ts # see the conversation feature's type surface
87
+ luca describe rest --ts # client type shape
88
+ \`\`\`
89
+
90
+ This outputs a ~95% accurate TypeScript representation based on runtime introspection. If a type looks wrong or a method signature seems off, verify with \`luca eval\` against the live instance.
91
+
80
92
  ### Describe the container itself
81
93
 
82
94
  \`\`\`shell
@@ -92,7 +104,9 @@ luca describe --help # full flag reference for describe
92
104
  luca help scaffold # help for any command
93
105
  \`\`\`
94
106
 
95
- **Use \`luca describe\` liberally.** It is the fastest, safest way to understand what the container provides. Every feature, client, and server is self-describing — if you know a name, describe will tell you everything about it. Use dot notation (\`ui.banner\`, \`fs.readFile\`) when you need docs on just one method or getter.
107
+ **Use \`luca describe\` liberally.** It is the fastest, safest way to understand what the container provides. Every feature, client, and server is self-describing — if you know a name, describe will tell you everything about it. Use dot notation (\`ui.banner\`, \`fs.readFile\`) when you need docs on just one method or getter. Use \`--ts\` when you need type information for writing code.
108
+
109
+ > **NOTE:** The \`luca\` binary is compiled and bundles all introspection data. \`luca describe\` reflects what actually ships in the binary — source files for built-in helpers may not exist in your project. Reading source can add context when it's available, but \`luca describe\` and \`luca eval\` are always the authority.
96
110
 
97
111
  ---
98
112
 
@@ -239,7 +253,7 @@ Everything \`luca describe\` outputs is also available at runtime in code:
239
253
  \`\`\`js
240
254
  container.features.describe('fs') // markdown docs (same as the CLI)
241
255
  feature.introspect() // structured object: { methods, events, state, options }
242
- container.inspectAsText() // full container overview as markdown
256
+ container.introspectAsText() // full container overview as markdown
243
257
  \`\`\`
244
258
 
245
259
  This is useful inside commands and scripts where you need introspection data programmatically.
@@ -255,11 +269,83 @@ This is useful inside commands and scripts where you need introspection data pro
255
269
  - \`luca serve --any-port\` will open on any port
256
270
 
257
271
 
258
- ## Reference
272
+ ## Framework Index
273
+
274
+ A table of contents for the container. **Run \`luca describe <name>\` for full docs on any item.** Use \`luca describe <name> --ts\` when you need type information. Source may not exist locally for built-in helpers — the compiled binary is the authority.
275
+
276
+ ### Features by Category
277
+
278
+ | Category | Features | What they do |
279
+ |----------|----------|--------------|
280
+ | **File System & Code** | \`fs\`, \`grep\`, \`fileManager\` | Read/write files, search code, watch for changes |
281
+ | **Process & Shell** | \`proc\`, \`processManager\`, \`secureShell\` | Run commands, manage long-running processes, SSH |
282
+ | **AI Assistants** | \`assistant\`, \`assistantsManager\`, \`conversation\`, \`conversationHistory\`, \`fileTools\` | Build AI assistants, manage conversations, tool calling. \`fileTools\` composes lower-level features (\`fs\`, \`grep\`) into an assistant-ready tool surface — a good example of how features can define tools for assistants (see \`references/examples/feature-as-tool-provider.md\`). |
283
+ | **AI Agent Wrappers** | \`claudeCode\`, \`openaiCodex\`, \`lucaCoder\` | Spawn and manage external AI agent CLIs as subprocesses |
284
+ | **Data & Storage** | \`sqlite\`, \`postgres\`, \`diskCache\`, \`contentDb\`, \`redis\` | Databases, caching, document management |
285
+ | **Networking** | \`networking\`, \`dns\`, \`portExposer\` | Network utilities, DNS, tunneling |
286
+ | **Google Workspace** | \`googleAuth\`, \`googleDrive\`, \`googleDocs\`, \`googleSheets\`, \`googleCalendar\`, \`googleMail\` | OAuth and Google service wrappers |
287
+ | **Dev Tools** | \`git\`, \`docker\`, \`esbuild\`, \`vm\`, \`python\`, \`packageFinder\` | Version control, containers, bundling, sandboxed execution |
288
+ | **Content & NLP** | \`docsReader\`, \`nlp\`, \`semanticSearch\`, \`skillsLibrary\`, \`jsonTree\`, \`yamlTree\` | Document Q&A, text analysis, semantic search, skills, structured file ingestion |
289
+ | **UI & Output** | \`ui\`, \`ink\`, \`yaml\` | Terminal UI, colors, ascii art, structured data display |
290
+ | **Media & Browser** | \`browserUse\`, \`tts\`, \`downloader\`, \`opener\`, \`telegram\` | Browser automation, text-to-speech, downloads, messaging |
291
+ | **System** | \`os\`, \`vault\`, \`helpers\`, \`introspectionScanner\`, \`containerLink\`, \`repl\`, \`runpod\` | OS info, secrets, runtime introspection, remote container linking |
292
+
293
+ ### Clients
294
+
295
+ | Client | Purpose |
296
+ |--------|---------|
297
+ | \`openai\` | Chat completions, embeddings, image generation |
298
+ | \`rest\` | Generic HTTP client (GET/POST/PUT/PATCH/DELETE) |
299
+ | \`websocket\` | WebSocket connections |
300
+ | \`elevenlabs\` | Text-to-speech synthesis |
301
+ | \`graph\` | GraphQL queries and mutations |
302
+
303
+ ### Servers
304
+
305
+ | Server | Purpose |
306
+ |--------|---------|
307
+ | \`express\` | HTTP server with file-based endpoint routing |
308
+ | \`mcp\` | Model Context Protocol server for AI tool exposure |
309
+ | \`websocket\` | WebSocket server with JSON framing |
310
+ | \`ipcSocket\` | Local IPC socket server for inter-process communication |
311
+
312
+ ### Type Discovery
313
+
314
+ \`luca describe <name> --ts\` outputs an approximate TypeScript representation of any helper as it exists at runtime — ~95% accurate. This is your go-to for writing type-safe code against the container. The binary compiles in the introspection data, so \`--ts\` reflects what actually exists at runtime even when source isn't available. Reading source can provide additional context when it's there.
315
+
316
+ \`\`\`shell
317
+ luca describe fs --ts # approximate TS interface for the fs feature
318
+ luca describe conversation --ts # conversation feature type surface
319
+ luca describe express --ts # express server type shape
320
+ \`\`\`
321
+
322
+ If a method signature or return type looks wrong, verify with \`luca eval\`:
323
+
324
+ \`\`\`shell
325
+ luca eval "typeof container.feature('fs').readFile"
326
+ luca eval "container.feature('fs').readFile('package.json')"
327
+ \`\`\`
328
+
329
+ ### Bundled Examples and Tutorials
330
+
331
+ The skill directory includes reference material:
259
332
 
260
- - \`references/api-docs/\`full pre-generated API reference for every built-in feature, client, and server
261
- - \`references/examples/\`runnable example docs for each feature (e.g. \`fs.md\`, \`git.md\`, \`proc.md\`)
262
- - \`references/tutorials/\` — step-by-step tutorials covering the container, helpers, commands, endpoints, and more
333
+ - **\`references/examples/\`**short, focused example docs for individual features (e.g. \`fs.md\`, \`git.md\`, \`proc.md\`)
334
+ - **\`references/tutorials/\`**longer-form guides covering the container, helpers, commands, endpoints, state/events, assistants, and more
335
+
336
+ These complement \`luca describe\` — describe gives you the API surface, examples show you patterns in action, and tutorials walk through building things end to end.
337
+
338
+ **Tip:** Runnable markdown is a great artifact to produce when building with luca. \`luca run doc.md\` executes code blocks inside the Luca VM — useful for both testing and documentation. When prototyping a feature or writing a how-to, consider writing it as a markdown file that can be run.
339
+
340
+ ### Container Primitives
341
+
342
+ | Primitive | Access | Purpose |
343
+ |-----------|--------|---------|
344
+ | State | \`container.state\`, \`helper.state\` | Observable key-value state on every object |
345
+ | Events | \`container.on()\`, \`helper.on()\` | Event bus on every object |
346
+ | Paths | \`container.paths\` | \`resolve()\`, \`join()\`, \`cwd\` |
347
+ | Utils | \`container.utils\` | \`uuid()\`, \`lodash\`, \`stringUtils\`, \`hashObject()\` |
348
+ | Registries | \`container.features\`, \`.clients\`, \`.servers\` | Discovery and metadata for all helpers |
263
349
  `,
264
350
  "CLAUDE": `# Luca Project
265
351
 
@@ -295,7 +381,7 @@ The \`luca\` binary is available in the path. Key commands:
295
381
  1. **Discover** — Run \`luca describe features\`, \`luca describe clients\`, \`luca describe servers\` to see what's available. Then \`luca describe <name>\` for full docs on any helper, or \`luca describe <name>.<member>\` to drill into a specific method or getter. This is your first move, always. (See \`.claude/skills/luca-framework/SKILL.md\` for the full mental model.)
296
382
  2. **Build** — Run \`luca scaffold <type> --tutorial\` before creating a new helper. It covers the full guide for that type.
297
383
  3. **Prototype** — Use \`luca eval "expression"\` to test container code before wiring up full handlers. Reach for eval when you're stuck — it gives you full runtime access.
298
- 4. **Reference** — Browse \`.claude/skills/luca-framework/references/\` for pre-generated API docs, runnable examples, and tutorials
384
+ 4. **Reference** — The skill file (\`.claude/skills/luca-framework/SKILL.md\`) includes a full Framework Index with every feature, client, and server organized by category
299
385
 
300
386
  ## Project Structure
301
387
 
@@ -1433,9 +1519,50 @@ console.log('Install exit code:', result.exitCode)
1433
1519
 
1434
1520
  When no \`installCommand\` is provided, the feature infers the correct command from the detected environment type (e.g., \`uv sync\` for uv, \`pip install -e .\` for venv).
1435
1521
 
1522
+ ## Persistent Sessions
1523
+
1524
+ Start a long-lived Python process where state persists across calls. Ideal for working inside real Python codebases, data analysis, and anything with expensive setup.
1525
+
1526
+ \`\`\`ts skip
1527
+ const python = container.feature('python', { dir: '/path/to/python-project' })
1528
+ await python.enable()
1529
+ await python.startSession()
1530
+
1531
+ // State persists across calls
1532
+ await python.run('x = 42')
1533
+ const result = await python.run('print(x * 2)')
1534
+ console.log('stdout:', result.stdout) // '84\\n'
1535
+
1536
+ // Evaluate expressions and get values back
1537
+ const val = await python.eval('x + 1')
1538
+ console.log('eval:', val) // 43
1539
+
1540
+ // Import project modules (sys.path is set up automatically)
1541
+ await python.importModule('json')
1542
+ const encoded = await python.call('json.dumps', [{ key: 'value' }], { indent: 2 })
1543
+ console.log('call:', encoded)
1544
+
1545
+ // Inspect the namespace
1546
+ const locals = await python.getLocals()
1547
+ console.log('locals:', Object.keys(locals))
1548
+
1549
+ // Errors don't crash the session
1550
+ const bad = await python.run('raise ValueError("oops")')
1551
+ console.log('error:', bad.error) // 'oops'
1552
+
1553
+ // Still alive
1554
+ const check = await python.run('print("still here")')
1555
+ console.log(check.stdout) // 'still here\\n'
1556
+
1557
+ // Clean up
1558
+ await python.stopSession()
1559
+ \`\`\`
1560
+
1561
+ See the [Python Sessions tutorial](../tutorials/19-python-sessions.md) for real-world patterns (data pipelines, Django, ML models).
1562
+
1436
1563
  ## Summary
1437
1564
 
1438
- The \`python\` feature bridges Luca and Python by auto-detecting environments, managing dependencies, and providing inline code execution with variable injection. It supports uv, conda, venv, and system Python installations.
1565
+ The \`python\` feature bridges Luca and Python by auto-detecting environments, managing dependencies, and providing both stateless execution and persistent sessions. It supports uv, conda, venv, and system Python installations.
1439
1566
  `,
1440
1567
  "yaml.md": `---
1441
1568
  title: "YAML"
@@ -2003,6 +2130,150 @@ Supports pagination via \`pageToken\`, ordering by \`startTime\` or \`updated\`,
2003
2130
  ## Summary
2004
2131
 
2005
2132
  The \`googleCalendar\` feature provides read access to Google Calendar events. Use the convenience methods \`getToday()\` and \`getUpcoming()\` for quick lookups, \`searchEvents()\` for text search, or \`listEvents()\` for full query control. Authentication is handled by \`googleAuth\`. Key methods: \`listCalendars()\`, \`getToday()\`, \`getUpcoming()\`, \`searchEvents()\`, \`listEvents()\`.
2133
+ `,
2134
+ "feature-as-tool-provider.md": `---
2135
+ title: "Features as Tool Providers for Assistants"
2136
+ tags: [feature, tools, assistant, composition, use, setupToolsConsumer]
2137
+ lastTested: null
2138
+ lastTestPassed: null
2139
+ ---
2140
+
2141
+ # Features as Tool Providers for Assistants
2142
+
2143
+ Any feature can expose tools that assistants pick up via \`assistant.use(feature)\`. This is how you compose lower-level container capabilities into an assistant-ready tool surface. The built-in \`fileTools\` feature is the canonical example — it wraps \`fs\` and \`grep\` into a focused set of tools modeled on what coding assistants need.
2144
+
2145
+ ## The Pattern
2146
+
2147
+ A feature becomes a tool provider by defining three things:
2148
+
2149
+ 1. **\`static tools\`** — a record mapping tool names to Zod schemas with descriptions
2150
+ 2. **Matching methods** — instance methods whose names match the keys in \`static tools\`
2151
+ 3. **\`setupToolsConsumer()\`** (optional) — a hook that runs when an assistant calls \`use()\`, perfect for injecting system prompt guidance
2152
+
2153
+ When an assistant calls \`assistant.use(feature)\`, the framework:
2154
+ - Reads \`static tools\` to register each tool with its schema
2155
+ - Routes tool calls to the matching instance methods
2156
+ - Calls \`setupToolsConsumer()\` so the feature can configure the assistant (e.g. add system prompt extensions)
2157
+
2158
+ ## Anatomy of fileTools
2159
+
2160
+ Here's the structure of the built-in \`fileTools\` feature (simplified for clarity):
2161
+
2162
+ \`\`\`ts
2163
+ import { z } from 'zod'
2164
+ import { Feature } from '@soederpop/luca/feature'
2165
+
2166
+ export class FileTools extends Feature {
2167
+ static { Feature.register(this, 'fileTools') }
2168
+
2169
+ // ── 1. Declare tools with Zod schemas ──────────────────────────
2170
+ static tools = {
2171
+ readFile: {
2172
+ description: 'Read the contents of a file.',
2173
+ schema: z.object({
2174
+ path: z.string().describe('File path relative to the project root'),
2175
+ offset: z.number().optional().describe('Line number to start reading from'),
2176
+ limit: z.number().optional().describe('Maximum number of lines to read'),
2177
+ }),
2178
+ },
2179
+ searchFiles: {
2180
+ description: 'Search file contents for a pattern using ripgrep.',
2181
+ schema: z.object({
2182
+ pattern: z.string().describe('Search pattern (regex supported)'),
2183
+ path: z.string().optional().describe('Directory to search in'),
2184
+ include: z.string().optional().describe('Glob pattern to filter files'),
2185
+ }),
2186
+ },
2187
+ editFile: {
2188
+ description: 'Replace an exact string match in a file.',
2189
+ schema: z.object({
2190
+ path: z.string().describe('File path relative to the project root'),
2191
+ oldString: z.string().describe('The exact text to find and replace'),
2192
+ newString: z.string().describe('The replacement text'),
2193
+ }),
2194
+ },
2195
+ // ... more tools
2196
+ }
2197
+
2198
+ // ── 2. Implement each tool as an instance method ───────────────
2199
+ // Method names must match the keys in static tools exactly.
2200
+ // Each receives the parsed args object and returns a string.
2201
+
2202
+ async readFile(args: { path: string; offset?: number; limit?: number }) {
2203
+ const fs = this.container.feature('fs')
2204
+ const content = await fs.readFileAsync(args.path)
2205
+ // ... handle offset/limit
2206
+ return content
2207
+ }
2208
+
2209
+ async searchFiles(args: { pattern: string; path?: string; include?: string }) {
2210
+ const grep = this.container.feature('grep')
2211
+ const results = await grep.search({ pattern: args.pattern, path: args.path, include: args.include })
2212
+ return JSON.stringify(results.map(r => ({ file: r.file, line: r.line, content: r.content })))
2213
+ }
2214
+
2215
+ async editFile(args: { path: string; oldString: string; newString: string }) {
2216
+ const fs = this.container.feature('fs')
2217
+ const content = await fs.readFileAsync(args.path)
2218
+ const updated = content.replace(args.oldString, args.newString)
2219
+ await fs.writeFileAsync(args.path, updated)
2220
+ return \`Edited \${args.path}\`
2221
+ }
2222
+
2223
+ // ── 3. Configure the assistant when it calls use() ─────────────
2224
+ override setupToolsConsumer(consumer) {
2225
+ // If the consumer is an assistant, inject guidance into its system prompt
2226
+ if (typeof consumer.addSystemPromptExtension === 'function') {
2227
+ consumer.addSystemPromptExtension('fileTools', [
2228
+ '## File Tools',
2229
+ '- All file paths are relative to the project root unless they start with /',
2230
+ '- Use searchFiles to understand code before modifying it',
2231
+ '- Use editFile for surgical changes — prefer it over writeFile',
2232
+ ].join('\\n'))
2233
+ }
2234
+ }
2235
+ }
2236
+ \`\`\`
2237
+
2238
+ ## Using It
2239
+
2240
+ \`\`\`ts
2241
+ const assistant = container.feature('assistant', {
2242
+ systemPrompt: 'You are a coding assistant.',
2243
+ model: 'gpt-4.1-mini',
2244
+ })
2245
+
2246
+ const fileTools = container.feature('fileTools')
2247
+ assistant.use(fileTools)
2248
+ await assistant.start()
2249
+
2250
+ // The assistant now has readFile, searchFiles, editFile, etc.
2251
+ // and its system prompt includes the fileTools guidance.
2252
+ console.log(Object.keys(assistant.tools))
2253
+ \`\`\`
2254
+
2255
+ ### Selective tool registration
2256
+
2257
+ You can expose only a subset of tools:
2258
+
2259
+ \`\`\`ts
2260
+ assistant.use(fileTools.toTools({ only: ['readFile', 'searchFiles', 'listDirectory'] }))
2261
+ \`\`\`
2262
+
2263
+ ## Why This Pattern Matters
2264
+
2265
+ This is how features compose for AI. Instead of the assistant importing \`fs\` and \`grep\` directly:
2266
+
2267
+ - The **feature** owns the tool surface — schemas, descriptions, and implementations in one place
2268
+ - The **assistant** gets a curated interface, not raw container access
2269
+ - **\`setupToolsConsumer()\`** lets the feature teach the assistant how to use the tools well
2270
+ - **\`toTools({ only })\`** lets you scope down what the assistant can do
2271
+
2272
+ Any feature you build can follow this same pattern. Define \`static tools\`, implement matching methods, optionally override \`setupToolsConsumer()\`, and assistants can \`use()\` it.
2273
+
2274
+ ## Summary
2275
+
2276
+ Features are the natural place to package tools for assistants. The \`static tools\` record declares the schema, instance methods implement the logic, and \`setupToolsConsumer()\` wires up assistant-specific configuration like system prompt extensions. This keeps tool definitions, implementations, and assistant guidance co-located in a single feature class.
2006
2277
  `,
2007
2278
  "content-db.md": `---
2008
2279
  title: "Content Database"
@@ -5175,6 +5446,11 @@ Then anyone (human or AI) can discover your feature:
5175
5446
  \`\`\`typescript
5176
5447
  container.features.describe('sessionManager')
5177
5448
  // Returns the full markdown documentation extracted from your JSDoc
5449
+
5450
+ // Quick discovery — list available methods and getters
5451
+ const session = container.feature('sessionManager')
5452
+ session.$methods // => ['createSession', ...]
5453
+ session.$getters // => ['activeCount', ...]
5178
5454
  \`\`\`
5179
5455
 
5180
5456
  ## Best Practices
@@ -5205,11 +5481,11 @@ Introspection serves two audiences:
5205
5481
 
5206
5482
  \`\`\`typescript
5207
5483
  // Structured data about the entire container
5208
- const info = container.inspect()
5484
+ const info = container.introspect()
5209
5485
  // Returns: registries, enabled features, state schema, available helpers
5210
5486
 
5211
5487
  // Human-readable markdown
5212
- const docs = container.inspectAsText()
5488
+ const docs = container.introspectAsText()
5213
5489
  \`\`\`
5214
5490
 
5215
5491
  ## Registry-Level Discovery
@@ -5260,6 +5536,16 @@ const info = fs.introspect()
5260
5536
  const docs = fs.introspectAsText()
5261
5537
  \`\`\`
5262
5538
 
5539
+ ### Quick Discovery with $getters and $methods
5540
+
5541
+ Every helper exposes \`$getters\` and \`$methods\` — string arrays listing what's available on the instance. Useful for quick exploration without parsing the full introspection object:
5542
+
5543
+ \`\`\`typescript
5544
+ const fs = container.feature('fs')
5545
+ fs.$methods // => ['readFile', 'writeFile', 'walk', 'readdir', ...]
5546
+ fs.$getters // => ['cwd', 'sep', ...]
5547
+ \`\`\`
5548
+
5263
5549
  ### What's in the Introspection Data?
5264
5550
 
5265
5551
  - **Class name** and description (from JSDoc)
@@ -5638,6 +5924,408 @@ export async function post(params: any, ctx: EndpointContext) {
5638
5924
  response.end()
5639
5925
  }
5640
5926
  \`\`\`
5927
+ `,
5928
+ "19-python-sessions.md": `---
5929
+ title: Working with Python Projects
5930
+ tags: [python, sessions, persistent, bridge, codebase, interop, data-science]
5931
+ ---
5932
+
5933
+ # Working with Python Projects
5934
+
5935
+ Luca's \`python\` feature has two modes: **stateless** execution (fire-and-forget, one process per call) and **persistent sessions** (a long-lived Python process that maintains state across calls). This tutorial focuses on sessions — the mode that lets you actually work inside a Python codebase.
5936
+
5937
+ ## When to Use Sessions
5938
+
5939
+ Stateless \`execute()\` is fine for one-off scripts. But if you need any of these, you want a session:
5940
+
5941
+ - **Imports that persist** — load \`pandas\` once, use it across many calls
5942
+ - **State that builds up** — query a database, filter results, then export
5943
+ - **Working inside a real project** — import your own modules, call your own functions
5944
+ - **Expensive setup** — ML model loading, database connections, API client initialization
5945
+
5946
+ ## Quick Start
5947
+
5948
+ \`\`\`ts skip
5949
+ const python = container.feature('python', { dir: '/path/to/my-python-project' })
5950
+ await python.enable()
5951
+ await python.startSession()
5952
+
5953
+ // Everything below runs in the same Python process.
5954
+ // Variables, imports, and state persist across calls.
5955
+
5956
+ await python.run('import pandas as pd')
5957
+ await python.run('df = pd.read_csv("data/sales.csv")')
5958
+
5959
+ const result = await python.run('print(df.shape)')
5960
+ console.log(result.stdout) // '(1000, 12)\\n'
5961
+
5962
+ const total = await python.eval('df["revenue"].sum()')
5963
+ console.log('Total revenue:', total)
5964
+
5965
+ await python.stopSession()
5966
+ \`\`\`
5967
+
5968
+ ## Project Directory
5969
+
5970
+ The \`dir\` option tells Luca where the Python project lives. This determines:
5971
+
5972
+ 1. **sys.path** — the bridge adds the project root (and \`src/\`, \`lib/\` if they exist) so your imports work
5973
+ 2. **Environment detection** — Luca looks for \`uv.lock\`, \`pyproject.toml\`, \`venv/\`, etc. in this directory
5974
+ 3. **Working directory** — the bridge process runs with \`cwd\` set to this path
5975
+
5976
+ \`\`\`ts skip
5977
+ // Explicit project directory
5978
+ const python = container.feature('python', { dir: '/Users/me/projects/my-api' })
5979
+
5980
+ // Or defaults to wherever luca was invoked from
5981
+ const python = container.feature('python')
5982
+ \`\`\`
5983
+
5984
+ If your project uses a \`src/\` layout (common in modern Python), the bridge automatically adds it to \`sys.path\`:
5985
+
5986
+ \`\`\`
5987
+ my-project/
5988
+ src/
5989
+ myapp/
5990
+ __init__.py
5991
+ models.py
5992
+ pyproject.toml
5993
+ \`\`\`
5994
+
5995
+ \`\`\`ts skip
5996
+ await python.startSession()
5997
+ // This works because src/ was added to sys.path
5998
+ await python.importModule('myapp.models', 'models')
5999
+ \`\`\`
6000
+
6001
+ ## Session Lifecycle
6002
+
6003
+ ### Starting
6004
+
6005
+ \`startSession()\` spawns a Python bridge process that talks to Luca over stdin/stdout using a JSON-line protocol. The bridge sets up \`sys.path\` and signals when it's ready.
6006
+
6007
+ \`\`\`ts skip
6008
+ await python.enable()
6009
+ await python.startSession()
6010
+
6011
+ console.log(python.state.get('sessionActive')) // true
6012
+ console.log(python.state.get('sessionId')) // uuid
6013
+ \`\`\`
6014
+
6015
+ ### Stopping
6016
+
6017
+ \`stopSession()\` kills the bridge process and cleans up. Any pending requests are rejected.
6018
+
6019
+ \`\`\`ts skip
6020
+ await python.stopSession()
6021
+ console.log(python.state.get('sessionActive')) // false
6022
+ \`\`\`
6023
+
6024
+ ### Crash Recovery
6025
+
6026
+ If the Python process dies unexpectedly (segfault, killed externally), the feature:
6027
+ - Sets \`sessionActive\` to \`false\`
6028
+ - Rejects all pending requests
6029
+ - Emits a \`sessionError\` event
6030
+
6031
+ \`\`\`ts skip
6032
+ python.on('sessionError', ({ error, sessionId }) => {
6033
+ console.error('Python session error:', error)
6034
+ // You could restart: await python.startSession()
6035
+ })
6036
+ \`\`\`
6037
+
6038
+ ## The Session API
6039
+
6040
+ ### run(code, variables?)
6041
+
6042
+ Execute Python code in the persistent namespace. This is the workhorse method.
6043
+
6044
+ \`\`\`ts skip
6045
+ // Simple execution
6046
+ const result = await python.run('print("hello")')
6047
+ // result.ok === true
6048
+ // result.stdout === 'hello\\n'
6049
+
6050
+ // With variable injection
6051
+ const result = await python.run('print(f"Processing {count} items")', { count: 42 })
6052
+
6053
+ // Errors don't crash the session
6054
+ const bad = await python.run('raise ValueError("oops")')
6055
+ // bad.ok === false
6056
+ // bad.error === 'oops'
6057
+ // bad.traceback === 'Traceback (most recent call last):\\n...'
6058
+
6059
+ // Session still alive after error
6060
+ const good = await python.run('print("still here")')
6061
+ // good.ok === true
6062
+ \`\`\`
6063
+
6064
+ ### eval(expression)
6065
+
6066
+ Evaluate a Python expression and return its value to JavaScript.
6067
+
6068
+ \`\`\`ts skip
6069
+ await python.run('x = [1, 2, 3]')
6070
+ const length = await python.eval('len(x)') // 3
6071
+ const doubled = await python.eval('[i*2 for i in x]') // [2, 4, 6]
6072
+ \`\`\`
6073
+
6074
+ Values are JSON-serialized. Complex types that can't be serialized come back as their \`repr()\` string.
6075
+
6076
+ ### importModule(name, alias?)
6077
+
6078
+ Import a module into the session namespace. The alias defaults to the last segment of the module path.
6079
+
6080
+ \`\`\`ts skip
6081
+ await python.importModule('json') // import json
6082
+ await python.importModule('myapp.models', 'models') // import myapp.models as models
6083
+ await python.importModule('os.path') // import os.path (available as "path")
6084
+ \`\`\`
6085
+
6086
+ ### call(funcPath, args?, kwargs?)
6087
+
6088
+ Call a function by its dotted path in the namespace.
6089
+
6090
+ \`\`\`ts skip
6091
+ await python.importModule('json')
6092
+ const encoded = await python.call('json.dumps', [{ a: 1 }], { indent: 2 })
6093
+ // '{\\n "a": 1\\n}'
6094
+
6095
+ // Works with your own functions too
6096
+ await python.run('def add(a, b): return a + b')
6097
+ const sum = await python.call('add', [3, 4]) // 7
6098
+ \`\`\`
6099
+
6100
+ ### getLocals()
6101
+
6102
+ Inspect everything in the session namespace.
6103
+
6104
+ \`\`\`ts skip
6105
+ await python.run('x = 42')
6106
+ await python.importModule('json')
6107
+ const locals = await python.getLocals()
6108
+ // { x: 42, json: '<module ...>' }
6109
+ \`\`\`
6110
+
6111
+ ### resetSession()
6112
+
6113
+ Clear all variables and imports without restarting the process.
6114
+
6115
+ \`\`\`ts skip
6116
+ await python.run('big_model = load_model()')
6117
+ await python.resetSession()
6118
+ // big_model is gone, but the session process is still running
6119
+ \`\`\`
6120
+
6121
+ ## Real-World Patterns
6122
+
6123
+ ### Data Analysis Pipeline
6124
+
6125
+ \`\`\`ts skip
6126
+ const python = container.feature('python', { dir: '/path/to/analytics' })
6127
+ await python.enable()
6128
+ await python.startSession()
6129
+
6130
+ // Setup
6131
+ await python.run('import pandas as pd')
6132
+ await python.run('import matplotlib')
6133
+ await python.run('matplotlib.use("Agg")') // headless
6134
+ await python.run('import matplotlib.pyplot as plt')
6135
+
6136
+ // Load and analyze
6137
+ await python.run('df = pd.read_csv("data/events.csv")')
6138
+ const shape = await python.eval('list(df.shape)')
6139
+ console.log(\`Loaded \${shape[0]} rows, \${shape[1]} columns\`)
6140
+
6141
+ const columns = await python.eval('list(df.columns)')
6142
+ console.log('Columns:', columns)
6143
+
6144
+ // Filter and aggregate
6145
+ await python.run(\`
6146
+ filtered = df[df["status"] == "completed"]
6147
+ summary = filtered.groupby("category")["amount"].agg(["sum", "mean", "count"])
6148
+ \`)
6149
+
6150
+ const summary = await python.eval('summary.to_dict()')
6151
+ console.log('Summary:', summary)
6152
+
6153
+ // Generate a chart
6154
+ await python.run(\`
6155
+ fig, ax = plt.subplots(figsize=(10, 6))
6156
+ summary["sum"].plot(kind="bar", ax=ax)
6157
+ ax.set_title("Revenue by Category")
6158
+ fig.savefig("output/revenue.png", dpi=150, bbox_inches="tight")
6159
+ plt.close(fig)
6160
+ \`)
6161
+
6162
+ await python.stopSession()
6163
+ \`\`\`
6164
+
6165
+ ### Working with a Django Project
6166
+
6167
+ \`\`\`ts skip
6168
+ const python = container.feature('python', { dir: '/path/to/django-project' })
6169
+ await python.enable()
6170
+ await python.startSession()
6171
+
6172
+ // Django requires this before you can import models
6173
+ await python.run(\`
6174
+ import os
6175
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
6176
+
6177
+ import django
6178
+ django.setup()
6179
+ \`)
6180
+
6181
+ // Now you can work with the ORM
6182
+ await python.run('from myapp.models import User, Order')
6183
+
6184
+ const userCount = await python.eval('User.objects.count()')
6185
+ console.log(\`\${userCount} users in database\`)
6186
+
6187
+ const recentOrders = await python.eval(\`
6188
+ list(Order.objects.filter(status="pending").values("id", "total", "created_at")[:10])
6189
+ \`)
6190
+ console.log('Recent pending orders:', recentOrders)
6191
+
6192
+ await python.stopSession()
6193
+ \`\`\`
6194
+
6195
+ ### ML Model Interaction
6196
+
6197
+ \`\`\`ts skip
6198
+ const python = container.feature('python', { dir: '/path/to/ml-project' })
6199
+ await python.enable()
6200
+ await python.startSession()
6201
+
6202
+ // Expensive setup — only happens once
6203
+ await python.run(\`
6204
+ from transformers import pipeline
6205
+ classifier = pipeline("sentiment-analysis")
6206
+ print("Model loaded")
6207
+ \`)
6208
+
6209
+ // Now you can call it cheaply many times
6210
+ async function classify(text: string) {
6211
+ return python.call('classifier', [text])
6212
+ }
6213
+
6214
+ const results = await Promise.all([
6215
+ classify('I love this product!'),
6216
+ classify('Terrible experience.'),
6217
+ classify('It was okay, nothing special.'),
6218
+ ])
6219
+
6220
+ console.log(results)
6221
+ // [
6222
+ // [{ label: 'POSITIVE', score: 0.9998 }],
6223
+ // [{ label: 'NEGATIVE', score: 0.9994 }],
6224
+ // [{ label: 'NEGATIVE', score: 0.7231 }],
6225
+ // ]
6226
+
6227
+ await python.stopSession()
6228
+ \`\`\`
6229
+
6230
+ ### Luca Command That Uses Python
6231
+
6232
+ \`\`\`ts skip
6233
+ // commands/analyze.ts
6234
+ import { z } from 'zod'
6235
+ import type { ContainerContext } from '@soederpop/luca'
6236
+ import { CommandOptionsSchema } from '@soederpop/luca/schemas'
6237
+
6238
+ export const positionals = ['target']
6239
+ export const argsSchema = CommandOptionsSchema.extend({
6240
+ target: z.string().describe('Path to CSV file to analyze'),
6241
+ })
6242
+
6243
+ async function handler(options: z.infer<typeof argsSchema>, context: ContainerContext) {
6244
+ const container = context.container as any
6245
+ const python = container.feature('python')
6246
+ await python.enable()
6247
+ await python.startSession()
6248
+
6249
+ try {
6250
+ await python.run('import pandas as pd')
6251
+ await python.run(\`df = pd.read_csv("\${options.target}")\`)
6252
+
6253
+ const shape = await python.eval('list(df.shape)')
6254
+ const dtypes = await python.eval('dict(df.dtypes.astype(str))')
6255
+ const nulls = await python.eval('dict(df.isnull().sum())')
6256
+
6257
+ console.log(\`Rows: \${shape[0]}, Columns: \${shape[1]}\`)
6258
+ console.log('Column types:', dtypes)
6259
+ console.log('Null counts:', nulls)
6260
+ } finally {
6261
+ await python.stopSession()
6262
+ }
6263
+ }
6264
+
6265
+ export default {
6266
+ description: 'Analyze a CSV file using pandas',
6267
+ argsSchema,
6268
+ handler,
6269
+ }
6270
+ \`\`\`
6271
+
6272
+ \`\`\`bash
6273
+ luca analyze data/sales.csv
6274
+ \`\`\`
6275
+
6276
+ ## Stateless vs. Session: Choosing the Right Mode
6277
+
6278
+ | | \`execute()\` (stateless) | \`run()\` (session) |
6279
+ |---|---|---|
6280
+ | Process | Fresh per call | Shared, long-lived |
6281
+ | State | None — each call starts clean | Persists across calls |
6282
+ | Imports | Re-imported every time | Imported once, reused |
6283
+ | Startup cost | ~50-200ms per call | ~200ms once, then ~1ms per call |
6284
+ | Use case | One-off scripts, simple eval | Real projects, data pipelines, REPL-like |
6285
+ | Error isolation | Perfect — crash is contained | Errors caught, session survives |
6286
+
6287
+ Both modes use the same environment detection (uv, conda, venv, system) and respect the same \`dir\` and \`pythonPath\` options.
6288
+
6289
+ ## Environment Detection
6290
+
6291
+ The feature detects Python environments in this order:
6292
+
6293
+ 1. **Explicit** — \`pythonPath\` option overrides everything
6294
+ 2. **uv** — \`uv.lock\` or \`pyproject.toml\` present, \`uv run python\` works
6295
+ 3. **conda** — \`environment.yml\` or \`conda.yml\` present
6296
+ 4. **venv** — \`venv/\` or \`.venv/\` directory with a Python binary inside
6297
+ 5. **system** — falls back to \`python3\` or \`python\` on PATH
6298
+
6299
+ \`\`\`ts skip
6300
+ const python = container.feature('python', { dir: '/path/to/project' })
6301
+ await python.enable()
6302
+ console.log(python.environmentType) // 'uv' | 'conda' | 'venv' | 'system'
6303
+ console.log(python.pythonPath) // e.g. '/Users/me/.local/bin/uv run python'
6304
+ \`\`\`
6305
+
6306
+ ## Events
6307
+
6308
+ The session emits events you can listen to for monitoring and debugging:
6309
+
6310
+ \`\`\`ts skip
6311
+ python.on('sessionStarted', ({ sessionId }) => {
6312
+ console.log('Session started:', sessionId)
6313
+ })
6314
+
6315
+ python.on('sessionStopped', ({ sessionId }) => {
6316
+ console.log('Session stopped:', sessionId)
6317
+ })
6318
+
6319
+ python.on('sessionError', ({ error, sessionId }) => {
6320
+ console.error('Session error:', error)
6321
+ })
6322
+ \`\`\`
6323
+
6324
+ ## What's Next
6325
+
6326
+ - [Creating Features](./10-creating-features.md) — build your own feature that wraps a Python service
6327
+ - [Commands](./08-commands.md) — create CLI commands that leverage Python
6328
+ - [Servers and Endpoints](./06-servers.md) — expose Python-powered analysis via HTTP
5641
6329
  `,
5642
6330
  "18-semantic-search.md": `---
5643
6331
  title: Semantic Search
@@ -6432,8 +7120,8 @@ fs.introspectAsText()
6432
7120
  The container itself is introspectable:
6433
7121
 
6434
7122
  \`\`\`js
6435
- container.inspect() // structured object with all registries, state, events
6436
- container.inspectAsText() // full markdown overview
7123
+ container.introspect() // structured object with all registries, state, events
7124
+ container.introspectAsText() // full markdown overview
6437
7125
  \`\`\`
6438
7126
 
6439
7127
  ## The REPL
@@ -6468,7 +7156,7 @@ luca eval "grep.search('.', 'TODO')"
6468
7156
  | Structured introspection? | \`feature.introspect()\` |
6469
7157
  | What state does it have? | \`feature.state.current\` |
6470
7158
  | What events does it emit? | \`feature.introspect().events\` |
6471
- | Full container overview? | \`container.inspectAsText()\` |
7159
+ | Full container overview? | \`container.introspectAsText()\` |
6472
7160
  | CLI docs for a helper? | \`luca describe <name>\` |
6473
7161
 
6474
7162
  ## Gotchas
@@ -7127,10 +7815,10 @@ Discover everything about the container at runtime:
7127
7815
 
7128
7816
  \`\`\`typescript
7129
7817
  // Structured introspection data
7130
- const info = container.inspect()
7818
+ const info = container.introspect()
7131
7819
 
7132
7820
  // Human-readable markdown
7133
- const docs = container.inspectAsText()
7821
+ const docs = container.introspectAsText()
7134
7822
  \`\`\`
7135
7823
 
7136
7824
  This is what makes Luca especially powerful for AI agents -- they can discover the entire API surface at runtime without reading documentation.