@soederpop/luca 0.0.7 → 0.0.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/RUNME.md ADDED
@@ -0,0 +1,56 @@
1
+ # Runnable Markdown
2
+
3
+ The `luca` CLI allows you to run markdown blocks as long as they're tagged with `ts` in the language.
4
+
5
+ ```ts
6
+ const banner = ui.banner('LUCA', {
7
+ font: 'Puffy',
8
+ colors: ['red','white','blue']
9
+ })
10
+
11
+ ui.print(banner)
12
+ ```
13
+
14
+ What is kind of cool is ( so long as there's no top-level-await in the block ) the context will preserve:
15
+
16
+ ```ts
17
+ if(typeof banner === 'undefined') {
18
+ ui.print.red('uh oh, something broke.')
19
+ }
20
+ ```
21
+
22
+ You can skip blocks too with the skip tag in the language of the fenced block
23
+
24
+ ```ts skip
25
+ console.log('Not gonna say anything')
26
+ ```
27
+
28
+ Did you hear something? No.
29
+
30
+ Something even cooler is the ability to render React blocks. This makes luca kind of like a poor man's MDX. I just define some Blocks in the markdown by creating an h2 `## Blocks` section with a fenced codeblock that uses `tsx`. The `ink.components` and `ink.React` globals are injected into the scope.
31
+
32
+ ## Blocks
33
+
34
+ ```tsx
35
+ const { Box, Text } = ink.components
36
+ const React = ink.React
37
+
38
+ function Greeting({ name, role }) {
39
+ return (
40
+ <Box borderStyle="round" padding={1}>
41
+ <Text color="green" bold>Hello {name}!</Text>
42
+ <Text dimColor> ({role})</Text>
43
+ </Box>
44
+ )
45
+ }
46
+ ```
47
+
48
+ ## Rendering React Blocks
49
+
50
+ Then I can use the Blocks in code.
51
+
52
+ ```ts
53
+ await render('Greeting', { name: 'Jon', role: 'Humble Servant' })
54
+ ```
55
+
56
+
@@ -0,0 +1,54 @@
1
+ # Runnable Markdown
2
+
3
+ The `luca` CLI allows you to run markdown blocks as long as they're tagged with `ts` in the language.
4
+
5
+ ```ts
6
+ const banner = ui.banner('LUCA', {
7
+ font: 'Puffy',
8
+ colors: ['red','white','blue']
9
+ })
10
+
11
+ ui.print(banner)
12
+ ```
13
+
14
+ What is kind of cool is ( so long as there's no top-level-await in the block ) the context will preserve:
15
+
16
+ ```ts
17
+ if(typeof banner === 'undefined') {
18
+ ui.print.red('uh oh, something broke.')
19
+ }
20
+ ```
21
+
22
+ You can skip blocks too with the skip tag in the language of the fenced block
23
+
24
+ ```ts skip
25
+ console.log('Not gonna say anything')
26
+ ```
27
+
28
+ Did you hear something? No.
29
+
30
+ Something even cooler is the ability to render React blocks. This makes luca kind of like a poor man's MDX. I just define some Blocks in the markdown by creating an h2 `## Blocks` section with a fenced codeblock that uses `tsx`. The `ink.components` and `ink.React` globals are injected into the scope.
31
+
32
+ ## Blocks
33
+
34
+ ```tsx
35
+ const { Box, Text } = ink.components
36
+ const React = ink.React
37
+
38
+ function Greeting({ name, role }) {
39
+ return (
40
+ <Box borderStyle="round" padding={1}>
41
+ <Text color="green" bold>Hello {name}!</Text>
42
+ <Text dimColor> ({role})</Text>
43
+ </Box>
44
+ )
45
+ }
46
+ ```
47
+
48
+ ## Rendering React Blocks
49
+
50
+ Then I can use the Blocks in code.
51
+
52
+ ```ts
53
+ await render('Greeting', { name: 'Jon', role: 'Humble Servant' })
54
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soederpop/luca",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "website": "https://luca.soederpop.com",
5
5
  "description": "lightweight universal conversational architecture AKA Le Ultimate Component Architecture AKA Last Universal Common Ancestor, part AI part Human",
6
6
  "author": "jon soeder aka the people's champ <jon@soederpop.com>",
@@ -1,5 +1,5 @@
1
1
  // Auto-generated bootstrap content
2
- // Generated at: 2026-03-19T00:28:07.695Z
2
+ // Generated at: 2026-03-19T04:48:18.099Z
3
3
  // Source: docs/bootstrap/*.md, docs/bootstrap/templates/*
4
4
  //
5
5
  // Do not edit manually. Run: luca build-bootstrap
@@ -536,5 +536,60 @@ export async function get(_params: any, ctx: any) {
536
536
  uptime: process.uptime(),
537
537
  }
538
538
  }
539
+ `,
540
+ "runme": `# Runnable Markdown
541
+
542
+ The \`luca\` CLI allows you to run markdown blocks as long as they're tagged with \`ts\` in the language.
543
+
544
+ \`\`\`ts
545
+ const banner = ui.banner('LUCA', {
546
+ font: 'Puffy',
547
+ colors: ['red','white','blue']
548
+ })
549
+
550
+ ui.print(banner)
551
+ \`\`\`
552
+
553
+ What is kind of cool is ( so long as there's no top-level-await in the block ) the context will preserve:
554
+
555
+ \`\`\`ts
556
+ if(typeof banner === 'undefined') {
557
+ ui.print.red('uh oh, something broke.')
558
+ }
559
+ \`\`\`
560
+
561
+ You can skip blocks too with the skip tag in the language of the fenced block
562
+
563
+ \`\`\`ts skip
564
+ console.log('Not gonna say anything')
565
+ \`\`\`
566
+
567
+ Did you hear something? No.
568
+
569
+ Something even cooler is the ability to render React blocks. This makes luca kind of like a poor man's MDX. I just define some Blocks in the markdown by creating an h2 \`## Blocks\` section with a fenced codeblock that uses \`tsx\`. The \`ink.components\` and \`ink.React\` globals are injected into the scope.
570
+
571
+ ## Blocks
572
+
573
+ \`\`\`tsx
574
+ const { Box, Text } = ink.components
575
+ const React = ink.React
576
+
577
+ function Greeting({ name, role }) {
578
+ return (
579
+ <Box borderStyle="round" padding={1}>
580
+ <Text color="green" bold>Hello {name}!</Text>
581
+ <Text dimColor> ({role})</Text>
582
+ </Box>
583
+ )
584
+ }
585
+ \`\`\`
586
+
587
+ ## Rendering React Blocks
588
+
589
+ Then I can use the Blocks in code.
590
+
591
+ \`\`\`ts
592
+ await render('Greeting', { name: 'Jon', role: 'Humble Servant' })
593
+ \`\`\`
539
594
  `
540
595
  }
@@ -74,7 +74,10 @@ async function bootstrap(options: z.infer<typeof argsSchema>, context: Container
74
74
  // ── 8. luca.cli.ts ─────────────────────────────────────────────
75
75
  await writeFile(fs, ui, mkPath('luca.cli.ts'), bootstrapTemplates['luca-cli'] || '', 'luca.cli.ts')
76
76
 
77
- // ── 9. .claude/settings.json (permissions for AI coding tools) ──
77
+ // ── 9. RUNME.md ────────────────────────────────────────────────
78
+ await writeFile(fs, ui, mkPath('RUNME.md'), bootstrapTemplates['runme'] || '', 'RUNME.md')
79
+
80
+ // ── 10. .claude/settings.json (permissions for AI coding tools) ──
78
81
  const settingsPath = mkPath('.claude', 'settings.json')
79
82
  const claudeSettings = {
80
83
  permissions: {
@@ -113,6 +116,7 @@ async function bootstrap(options: z.infer<typeof argsSchema>, context: Container
113
116
  ui.print(' luca about — project info + discovered helpers')
114
117
  ui.print(' luca serve — start the API server (try /api/health)')
115
118
  ui.print(' luca describe fs — learn about any built-in feature')
119
+ ui.print(' luca RUNME — run the interactive markdown demo')
116
120
  ui.print('')
117
121
  ui.print(' Need to build something? Use scaffold:\n')
118
122
  ui.print(' luca scaffold command deploy — add a CLI command')
@@ -2,7 +2,7 @@ import { z } from 'zod'
2
2
  import { commands } from '../command.js'
3
3
  import { CommandOptionsSchema } from '../schemas/base.js'
4
4
  import type { ContainerContext } from '../container.js'
5
- import type { IntrospectionSection } from '../introspection/index.js'
5
+ import type { IntrospectionSection, MethodIntrospection, GetterIntrospection } from '../introspection/index.js'
6
6
 
7
7
  declare module '../command.js' {
8
8
  interface AvailableCommands {
@@ -67,6 +67,7 @@ type ResolvedTarget =
67
67
  | { kind: 'container' }
68
68
  | { kind: 'registry'; name: RegistryName }
69
69
  | { kind: 'helper'; registry: RegistryName; id: string }
70
+ | { kind: 'member'; registry: RegistryName; id: string; member: string; memberType: 'method' | 'getter' }
70
71
 
71
72
  class DescribeError extends Error {
72
73
  constructor(message: string) {
@@ -117,9 +118,44 @@ function fuzzyFind(registry: any, input: string): string | undefined {
117
118
  return (registry.available as string[]).find((id: string) => normalize(id) === norm)
118
119
  }
119
120
 
121
+ /**
122
+ * Try to resolve "helperName.memberName" by searching all registries for the helper,
123
+ * then checking if memberName is a method or getter on it.
124
+ * Returns a 'member' target or null if no match.
125
+ */
126
+ function resolveHelperMember(helperName: string, memberName: string, container: any): ResolvedTarget | null {
127
+ for (const registryName of REGISTRY_NAMES) {
128
+ const reg = container[registryName]
129
+ if (!reg) continue
130
+ const found = fuzzyFind(reg, helperName)
131
+ if (!found) continue
132
+
133
+ const Ctor = reg.lookup(found)
134
+ const introspection = Ctor.introspect?.()
135
+ if (!introspection) continue
136
+
137
+ if (introspection.methods?.[memberName]) {
138
+ return { kind: 'member', registry: registryName, id: found, member: memberName, memberType: 'method' }
139
+ }
140
+ if (introspection.getters?.[memberName]) {
141
+ return { kind: 'member', registry: registryName, id: found, member: memberName, memberType: 'getter' }
142
+ }
143
+
144
+ // If we found the helper but not the member, give a helpful error
145
+ const allMembers = [
146
+ ...Object.keys(introspection.methods || {}).map((m: string) => m + '()'),
147
+ ...Object.keys(introspection.getters || {}),
148
+ ].sort()
149
+ throw new DescribeError(
150
+ `"${memberName}" is not a known method or getter on ${found}.\n\nAvailable members:\n ${allMembers.join(', ')}`
151
+ )
152
+ }
153
+ return null
154
+ }
155
+
120
156
  /**
121
157
  * Parse a single target string into a resolved target.
122
- * Accepts: "container", "features", "features.fs", "fs", etc.
158
+ * Accepts: "container", "features", "features.fs", "fs", "ui.banner", etc.
123
159
  */
124
160
  function resolveTarget(target: string, container: any): ResolvedTarget {
125
161
  const lower = target.toLowerCase()
@@ -153,6 +189,12 @@ function resolveTarget(target: string, container: any): ResolvedTarget {
153
189
  }
154
190
  return { kind: 'helper', registry, id: resolved }
155
191
  }
192
+
193
+ // Not a registry prefix — try "helper.member" (e.g. "ui.banner", "fs.readFile")
194
+ const helperName = prefix!
195
+ const memberName = rest.join('.')
196
+ const memberResult = resolveHelperMember(helperName, memberName, container)
197
+ if (memberResult) return memberResult
156
198
  }
157
199
 
158
200
  // Unqualified name: search all registries (fuzzy)
@@ -546,6 +588,76 @@ function buildHelperSummary(Ctor: any): string {
546
588
  return lines.join('\n')
547
589
  }
548
590
 
591
+ function getMemberData(container: any, registryName: RegistryName, id: string, member: string, memberType: 'method' | 'getter', headingDepth = 1): { json: any; text: string } {
592
+ const registry = container[registryName]
593
+ const Ctor = registry.lookup(id)
594
+ const introspection = Ctor.introspect?.()
595
+ const h = '#'.repeat(headingDepth)
596
+ const hSub = '#'.repeat(headingDepth + 1)
597
+
598
+ if (memberType === 'method') {
599
+ const method = introspection?.methods?.[member] as MethodIntrospection | undefined
600
+ if (!method) return { json: {}, text: `No introspection data for ${id}.${member}()` }
601
+
602
+ const parts: string[] = []
603
+ parts.push(`${h} ${id}.${member}()`)
604
+ parts.push(`> method on **${introspection.className || id}**`)
605
+
606
+ if (method.description) parts.push(method.description)
607
+
608
+ // Parameters
609
+ const paramEntries = Object.entries(method.parameters || {})
610
+ if (paramEntries.length > 0) {
611
+ const paramLines = [`${hSub} Parameters`, '']
612
+ for (const [name, info] of paramEntries) {
613
+ const req = (method.required || []).includes(name) ? ' *(required)*' : ''
614
+ paramLines.push(`- **${name}** \`${info.type}\`${req}${info.description ? ' — ' + info.description : ''}`)
615
+ // Nested properties (e.g. options objects)
616
+ if (info.properties) {
617
+ for (const [propName, propInfo] of Object.entries(info.properties)) {
618
+ paramLines.push(` - **${propName}** \`${propInfo.type}\`${propInfo.description ? ' — ' + propInfo.description : ''}`)
619
+ }
620
+ }
621
+ }
622
+ parts.push(paramLines.join('\n'))
623
+ }
624
+
625
+ // Returns
626
+ if (method.returns && method.returns !== 'void') {
627
+ parts.push(`${hSub} Returns\n\n\`${method.returns}\``)
628
+ }
629
+
630
+ // Examples
631
+ if (method.examples?.length) {
632
+ parts.push(`${hSub} Examples`)
633
+ for (const ex of method.examples) {
634
+ parts.push(`\`\`\`${ex.language || 'typescript'}\n${ex.code}\n\`\`\``)
635
+ }
636
+ }
637
+
638
+ return { json: { [member]: method, _helper: id, _type: 'method' }, text: parts.join('\n\n') }
639
+ }
640
+
641
+ // Getter
642
+ const getter = introspection?.getters?.[member] as GetterIntrospection | undefined
643
+ if (!getter) return { json: {}, text: `No introspection data for ${id}.${member}` }
644
+
645
+ const parts: string[] = []
646
+ parts.push(`${h} ${id}.${member}`)
647
+ parts.push(`> getter on **${introspection.className || id}** — returns \`${getter.returns || 'unknown'}\``)
648
+
649
+ if (getter.description) parts.push(getter.description)
650
+
651
+ if (getter.examples?.length) {
652
+ parts.push(`${hSub} Examples`)
653
+ for (const ex of getter.examples) {
654
+ parts.push(`\`\`\`${ex.language || 'typescript'}\n${ex.code}\n\`\`\``)
655
+ }
656
+ }
657
+
658
+ return { json: { [member]: getter, _helper: id, _type: 'getter' }, text: parts.join('\n\n') }
659
+ }
660
+
549
661
  function getHelperData(container: any, registryName: RegistryName, id: string, sections: (IntrospectionSection | 'description')[], noTitle = false, headingDepth = 1): { json: any; text: string } {
550
662
  const registry = container[registryName]
551
663
  const Ctor = registry.lookup(id)
@@ -628,6 +740,8 @@ export default async function describe(options: z.infer<typeof argsSchema>, cont
628
740
  return getRegistryData(container, item.name, sections, noTitle, headingDepth)
629
741
  case 'helper':
630
742
  return getHelperData(container, item.registry, item.id, sections, noTitle, headingDepth)
743
+ case 'member':
744
+ return getMemberData(container, item.registry, item.id, item.member, item.memberType, headingDepth)
631
745
  }
632
746
  }
633
747
 
@@ -16,6 +16,38 @@ export const argsSchema = CommandOptionsSchema.extend({
16
16
  dontInjectContext: z.boolean().default(false).describe('Skip auto-injecting container context into scripts (run with plain bun instead)'),
17
17
  })
18
18
 
19
+ /**
20
+ * Convert esbuild ESM output imports/exports to CJS require/module.exports
21
+ * so the code can run in a vm context that provides `require`.
22
+ */
23
+ function esmToCjs(code: string): string {
24
+ return code
25
+ // import { a, b } from 'x' → const { a, b } = require('x')
26
+ .replace(/^import\s+\{([^}]+)\}\s+from\s+(['"][^'"]+['"])\s*;?$/gm,
27
+ 'const {$1} = require($2);')
28
+ // import x from 'y' → const x = require('y').default ?? require('y')
29
+ .replace(/^import\s+(\w+)\s+from\s+(['"][^'"]+['"])\s*;?$/gm,
30
+ 'const $1 = require($2).default ?? require($2);')
31
+ // import * as x from 'y' → const x = require('y')
32
+ .replace(/^import\s+\*\s+as\s+(\w+)\s+from\s+(['"][^'"]+['"])\s*;?$/gm,
33
+ 'const $1 = require($2);')
34
+ // import 'y' → require('y')
35
+ .replace(/^import\s+(['"][^'"]+['"])\s*;?$/gm,
36
+ 'require($1);')
37
+ // export default → module.exports.default =
38
+ .replace(/^export\s+default\s+/gm, 'module.exports.default = ')
39
+ // export { ... } → strip (vars already in scope)
40
+ .replace(/^export\s+\{[^}]*\}\s*;?$/gm, '')
41
+ // export const/let/var → const/let/var
42
+ .replace(/^export\s+(const|let|var)\s+/gm, '$1 ')
43
+ }
44
+
45
+ function hasTLA(code: string): boolean {
46
+ // Quick check: contains await outside of async function bodies
47
+ // This is a heuristic — wrapTopLevelAwait does the real work
48
+ return /\bawait\b/.test(code) && !/^\s*\(?\s*async\b/.test(code)
49
+ }
50
+
19
51
  function resolveScript(ref: string, context: ContainerContext): string | null {
20
52
  const container = context.container as any
21
53
  const candidates = [
@@ -196,7 +228,17 @@ async function runScript(scriptPath: string, context: ContainerContext, options:
196
228
  const vm = container.feature('vm')
197
229
  const esbuild = container.feature('esbuild')
198
230
  const raw = container.fs.readFile(scriptPath)
199
- const { code } = esbuild.transformSync(raw, { format: 'cjs' })
231
+
232
+ let code: string
233
+ if (hasTLA(raw)) {
234
+ // TLA is incompatible with CJS format, so transform as ESM (preserves await)
235
+ // then convert import/export statements to require/module.exports for the vm
236
+ const { code: esm } = esbuild.transformSync(raw, { format: 'esm' })
237
+ code = esmToCjs(esm)
238
+ } else {
239
+ const { code: cjs } = esbuild.transformSync(raw, { format: 'cjs' })
240
+ code = cjs
241
+ }
200
242
 
201
243
  const ctx = {
202
244
  require: vm.createRequireFor(scriptPath),
@@ -31,7 +31,7 @@ export class ESBuild extends Feature {
31
31
  return esbuild.transformSync(code, {
32
32
  loader: 'ts',
33
33
  format: 'esm',
34
- target: 'es2020',
34
+ target: 'esnext',
35
35
  sourcemap: false,
36
36
  minify: false,
37
37
  ...options
@@ -48,7 +48,7 @@ export class ESBuild extends Feature {
48
48
  return esbuild.transform(code, {
49
49
  loader: 'ts',
50
50
  format: 'esm',
51
- target: 'es2020',
51
+ target: 'esnext',
52
52
  sourcemap: false,
53
53
  minify: false,
54
54
  ...options
@@ -69,7 +69,7 @@ export class ESBuild extends Feature {
69
69
  bundle: true,
70
70
  platform: 'node',
71
71
  format: 'esm',
72
- target: 'es2020',
72
+ target: 'esnext',
73
73
  write: false,
74
74
  ...options
75
75
  })
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { NodeContainer } from '../src/node/container'
3
+
4
+ /**
5
+ * These tests verify the VM context preservation behavior that `luca run`
6
+ * depends on for markdown execution. When running sequential code blocks
7
+ * in a shared context, variables defined in earlier blocks must be accessible
8
+ * in later blocks — as long as the blocks don't use top-level await (which
9
+ * wraps code in an async IIFE and isolates the scope).
10
+ */
11
+ describe('VM context preservation across sequential runs', () => {
12
+ it('preserves const declarations across runs without await', async () => {
13
+ const c = new NodeContainer()
14
+ const vm = c.feature('vm')
15
+ const shared = vm.createContext({})
16
+
17
+ await vm.run('const x = 42', shared)
18
+ const result = await vm.run('x', shared)
19
+
20
+ expect(result).toBe(42)
21
+ })
22
+
23
+ it('preserves let declarations across runs without await', async () => {
24
+ const c = new NodeContainer()
25
+ const vm = c.feature('vm')
26
+ const shared = vm.createContext({})
27
+
28
+ await vm.run('let count = 0', shared)
29
+ await vm.run('count += 10', shared)
30
+ const result = await vm.run('count', shared)
31
+
32
+ expect(result).toBe(10)
33
+ })
34
+
35
+ it('preserves function declarations across runs', async () => {
36
+ const c = new NodeContainer()
37
+ const vm = c.feature('vm')
38
+ const shared = vm.createContext({})
39
+
40
+ await vm.run('function double(n) { return n * 2 }', shared)
41
+ const result = await vm.run('double(21)', shared)
42
+
43
+ expect(result).toBe(42)
44
+ })
45
+
46
+ it('preserves arrow functions assigned to const', async () => {
47
+ const c = new NodeContainer()
48
+ const vm = c.feature('vm')
49
+ const shared = vm.createContext({})
50
+
51
+ await vm.run('const greet = (name) => `hello ${name}`', shared)
52
+ const result = await vm.run('greet("world")', shared)
53
+
54
+ expect(result).toBe('hello world')
55
+ })
56
+
57
+ it('preserves objects and allows mutation across runs', async () => {
58
+ const c = new NodeContainer()
59
+ const vm = c.feature('vm')
60
+ const shared = vm.createContext({})
61
+
62
+ await vm.run('const data = { items: [] }', shared)
63
+ await vm.run('data.items.push("a", "b")', shared)
64
+ await vm.run('data.items.push("c")', shared)
65
+ const result = await vm.run('data.items', shared)
66
+
67
+ expect(result).toEqual(['a', 'b', 'c'])
68
+ })
69
+
70
+ it('preserves class declarations across runs', async () => {
71
+ const c = new NodeContainer()
72
+ const vm = c.feature('vm')
73
+ const shared = vm.createContext({})
74
+
75
+ await vm.run('class Counter { constructor() { this.n = 0 } inc() { this.n++ } }', shared)
76
+ await vm.run('const counter = new Counter(); counter.inc(); counter.inc()', shared)
77
+ const result = await vm.run('counter.n', shared)
78
+
79
+ expect(result).toBe(2)
80
+ })
81
+
82
+ it('later blocks can compose values from earlier blocks', async () => {
83
+ const c = new NodeContainer()
84
+ const vm = c.feature('vm')
85
+ const shared = vm.createContext({})
86
+
87
+ await vm.run('const firstName = "Jane"', shared)
88
+ await vm.run('const lastName = "Doe"', shared)
89
+ await vm.run('const fullName = `${firstName} ${lastName}`', shared)
90
+ const result = await vm.run('fullName', shared)
91
+
92
+ expect(result).toBe('Jane Doe')
93
+ })
94
+
95
+ it('top-level await blocks lose const/let scope (known limitation)', async () => {
96
+ const c = new NodeContainer()
97
+ const vm = c.feature('vm')
98
+ const shared = vm.createContext({})
99
+
100
+ // This block has await, so it gets wrapped in (async () => { ... })()
101
+ // The const is scoped to that IIFE and doesn't leak to the shared context
102
+ await vm.run('const val = await Promise.resolve(99)', shared)
103
+ const result = await vm.run('typeof val', shared)
104
+
105
+ expect(result).toBe('undefined')
106
+ })
107
+
108
+ it('top-level await blocks can share state via pre-existing context properties', async () => {
109
+ const c = new NodeContainer()
110
+ const vm = c.feature('vm')
111
+ const shared = vm.createContext({ result: null })
112
+
113
+ // Assigning to an existing context property works even inside the IIFE
114
+ await vm.run('result = await Promise.resolve("async value")', shared)
115
+ const value = await vm.run('result', shared)
116
+
117
+ expect(value).toBe('async value')
118
+ })
119
+ })
120
+
121
+ describe('VM wrapTopLevelAwait', () => {
122
+ it('does not wrap code without await', () => {
123
+ const c = new NodeContainer()
124
+ const vm = c.feature('vm')
125
+
126
+ const code = 'const x = 42'
127
+ expect(vm.wrapTopLevelAwait(code)).toBe(code)
128
+ })
129
+
130
+ it('wraps code containing await in an async IIFE', () => {
131
+ const c = new NodeContainer()
132
+ const vm = c.feature('vm')
133
+
134
+ const wrapped = vm.wrapTopLevelAwait('const x = await fetch("http://example.com")')
135
+ expect(wrapped).toContain('async')
136
+ expect(wrapped).toContain('await fetch')
137
+ })
138
+
139
+ it('does not wrap code already in an async function', () => {
140
+ const c = new NodeContainer()
141
+ const vm = c.feature('vm')
142
+
143
+ const code = 'async function go() { await fetch("http://example.com") }'
144
+ expect(vm.wrapTopLevelAwait(code)).toBe(code)
145
+ })
146
+ })
@@ -1 +0,0 @@
1
- {"id":"d587b833-0933-4782-8c20-ad1c28d12c7a","title":"Untitled","model":"gpt-5.2","tags":[],"thread":"assistant:zapldb:2026-03-15","createdAt":"2026-03-15T03:08:58.783Z","updatedAt":"2026-03-15T03:08:58.783Z","messageCount":5,"metadata":{}}
@@ -1 +0,0 @@
1
- {"id":"d587b833-0933-4782-8c20-ad1c28d12c7a","title":"Untitled","model":"gpt-5.2","messages":[{"role":"system","content":"# Luca Assistant Example\n\nYou are currently an example / template \"Assistant\" provided by the Luca framework. ( You'll probably have no idea what that is, don't worry, it doesn't matter ).\n\nYou are what gets scaffolded when a user writes the `luca scaffold assistant` command.\n\nIn luca, an Assistant is backed by a folder which has a few components:\n\n- CORE.md -- this is a markdown file that will get injected into the system prompt of a chat completion call\n- tools.ts -- this file is expected to export functions, and a schemas object whose keys are the names of the functions that get exported, and whose values are zod v4 schemas that describe the parameters\n- hooks.ts -- this file is expexted to export functions, whose names match the events emitted by the luca assistant helper\n\nCurrently, the user is chatting with you from the `luca chat` CLI. \n\nYou should tell them what each of these files is and how to edit them.\n\nIt is also important for them to know that the luca `container` is globally available for them in the context of the `tools.ts` and `hooks.ts` files."},{"role":"user","content":"What should we do?"},{"role":"assistant","content":null,"tool_calls":[{"id":"call_bwGYIZLkPRtuqUMVRAg8ykPy","type":"function","function":{"name":"README","arguments":"{}"}}]},{"role":"tool","tool_call_id":"call_bwGYIZLkPRtuqUMVRAg8ykPy","content":"YO YO"},{"role":"assistant","content":"You’ll typically do three things: customize the assistant’s behavior (CORE.md), add capabilities (tools.ts), and wire up lifecycle events (hooks.ts).\n\n## 1) Edit `CORE.md` (behavior + policy)\n**What it is:** A markdown file whose contents get injected into the **system prompt** at runtime. This is where you define the assistant’s identity, boundaries, style, domain knowledge, do/don’t rules, and any project-specific instructions.\n\n**What to put in it:**\n- Role + purpose (“You are an assistant that helps with X…”)\n- Tone/format preferences (concise, code-first, etc.)\n- Hard constraints (what it must never do, how it handles secrets, etc.)\n- Domain context (your product, APIs, repos, conventions)\n\n**When to edit:** Any time you want the assistant to “think/behave differently” without changing code.\n\n---\n\n## 2) Edit `tools.ts` (add callable functions)\n**What it is:** A TypeScript module that exports:\n- **functions** the model can call as tools (e.g., `searchDocs`, `runQuery`, `readFile`, `createTicket`)\n- a **`schemas` object** mapping tool/function names → **zod v4 schemas** describing parameters\n\n**Why it matters:** Tools are how your assistant safely does real work (fetch data, manipulate files, call APIs) instead of guessing.\n\n**Key points:**\n- Keep tools small and composable.\n- Validate all inputs with zod schemas.\n- Return structured data the model can use.\n\n**Important:** The luca **`container`** is globally available inside `tools.ts`. Use it to access shared services/config (whatever your luca setup provides) without manually importing/constructing everything.\n\n---\n\n## 3) Edit `hooks.ts` (react to events)\n**What it is:** A TypeScript module exporting functions whose names match luca assistant helper **events** (lifecycle hooks). These run automatically when those events fire (e.g., session start, before/after tool call, etc.—depending on what luca emits in your setup).\n\n**Use cases:**\n- Logging / tracing\n- Metrics\n- Tool-call auditing\n- Injecting/refreshing context\n- Guardrails (block certain actions, redact secrets)\n\n**Important:** The luca **`container`** is also globally available in `hooks.ts`.\n\n---\n\n## What to do next (practical plan)\n1) **Open `CORE.md`** and write 10–30 lines that clearly define the assistant’s job and constraints.\n2) **Add 1–2 tools** in `tools.ts` that reflect what you actually want the assistant to be able to do (start minimal).\n3) **Add a basic hook** in `hooks.ts` for logging (so you can see what’s happening and iterate safely).\n4) Run `luca chat` again and test; tighten instructions/schemas based on failures.\n\nIf you tell me what your assistant is supposed to help with (e.g., “triage support tickets”, “write PRs”, “query an internal API”), I can suggest exactly what to put into `CORE.md` and the first tools/hooks to implement."}],"tags":[],"thread":"assistant:zapldb:2026-03-15","createdAt":"2026-03-15T03:08:58.783Z","updatedAt":"2026-03-15T03:08:58.783Z","messageCount":5,"metadata":{}}
@@ -1,2 +0,0 @@
1
-
2
- 0633d48140563c44b78c1c0b9ff1c1fc5d79b689 {"key":"conversation-history:__index__","integrity":"sha512-G7XHWyh5TwD5TE1gmpiXjpQg6bcUbSBKf79bCzBHcpJYFwXSB8AQDauqwn7vVAqq7OM3SvdRBKkyGdTsi/tE5w==","time":1773544138800,"size":40}
@@ -1,2 +0,0 @@
1
-
2
- db975a1cc1e358a00d66dab7a468db4ee63eb62d {"key":"conversation-history:meta:d587b833-0933-4782-8c20-ad1c28d12c7a","integrity":"sha512-2t8dkM5OBCq+sDWhl4MsbWiTQgp0egVr53PrAOT3RaA31QXI2xPd59NrNra4k63bt98PX+nwwT5mXyAFZEcxiw==","time":1773544138799,"size":240}
@@ -1,2 +0,0 @@
1
-
2
- 74e795aeefad56844bf7be3dc434e7e99a145283 {"key":"conversation-history:d587b833-0933-4782-8c20-ad1c28d12c7a","integrity":"sha512-7QTh0MKljC2ymzkhyir/s+pP6+gxxTs468IQGft5mCOrpu1bRhGHPSzSXUItSZVbhSqcMm2g1niJm8HCwpYJAQ==","time":1773544138797,"size":4603}