@soederpop/luca 0.0.7 → 0.0.9
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/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/RUNME.md +56 -0
- package/docs/bootstrap/CLAUDE.md +2 -1
- package/docs/bootstrap/SKILL.md +14 -1
- package/docs/bootstrap/templates/runme.md +54 -0
- package/package.json +1 -1
- package/src/bootstrap/generated.ts +56 -1
- package/src/commands/bootstrap.ts +5 -1
- package/src/commands/describe.ts +116 -2
- package/src/commands/run.ts +47 -2
- package/src/node/features/esbuild.ts +3 -3
- package/test/vm-context.test.ts +146 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +0 -1
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +0 -1
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +0 -1
- package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +0 -2
- package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +0 -2
- package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +0 -2
package/AGENTS.md
CHANGED
|
@@ -26,6 +26,7 @@ On the frontend the browser container is perfect for highly reactive, stateful w
|
|
|
26
26
|
- `luca describe diskCache`
|
|
27
27
|
- `luca describe` describe the container itself
|
|
28
28
|
- `luca describe servers` describe which servers are available
|
|
29
|
+
- `luca describe ui.banner` describe a specific method or getter on a helper
|
|
29
30
|
- the arguments to describe are pretty forgiving and permissive
|
|
30
31
|
|
|
31
32
|
**IMPORTANT NOTE** When trying to investigate features, clients, servers, etc, see if these tools can help you first instead of searching for files and reading them that way. If youw ant to understand what they do, vs how theyre actually implemented
|
package/CLAUDE.md
CHANGED
|
@@ -26,6 +26,7 @@ On the frontend the browser container is perfect for highly reactive, stateful w
|
|
|
26
26
|
- `luca describe diskCache`
|
|
27
27
|
- `luca describe` describe the container itself
|
|
28
28
|
- `luca describe servers` describe which servers are available
|
|
29
|
+
- `luca describe ui.banner` describe a specific method or getter on a helper
|
|
29
30
|
- the arguments to describe are pretty forgiving and permissive
|
|
30
31
|
|
|
31
32
|
**IMPORTANT NOTE** When trying to investigate features, clients, servers, etc, see if these tools can help you first instead of searching for files and reading them that way. If youw ant to understand what they do, vs how theyre actually implemented
|
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
|
+
|
package/docs/bootstrap/CLAUDE.md
CHANGED
|
@@ -15,6 +15,7 @@ The `luca` binary is available in the path. Key commands:
|
|
|
15
15
|
- `luca` — list available commands (built-in + project commands)
|
|
16
16
|
- `luca eval "expression"` — evaluate JS with the container in scope
|
|
17
17
|
- `luca describe <name>` — full docs for any feature, client, or server (e.g. `luca describe fs`)
|
|
18
|
+
- `luca describe <name>.<member>` — docs for a specific method or getter (e.g. `luca describe ui.banner`, `luca describe fs.readFile`)
|
|
18
19
|
- `luca describe features` — index of all available features (also: `clients`, `servers`)
|
|
19
20
|
- `luca serve` — start a local server using `endpoints/` folder
|
|
20
21
|
- `luca run script.ts` — run a script with the container
|
|
@@ -28,7 +29,7 @@ The `luca` binary is available in the path. Key commands:
|
|
|
28
29
|
|
|
29
30
|
## Learning the Framework
|
|
30
31
|
|
|
31
|
-
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. This is your first move, always. (See `.claude/skills/luca-framework/SKILL.md` for the full mental model.)
|
|
32
|
+
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.)
|
|
32
33
|
2. **Build** — Run `luca scaffold <type> --tutorial` before creating a new helper. It covers the full guide for that type.
|
|
33
34
|
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.
|
|
34
35
|
4. **Reference** — Browse `.claude/skills/luca-framework/references/api-docs/` for pre-generated API docs
|
package/docs/bootstrap/SKILL.md
CHANGED
|
@@ -36,6 +36,19 @@ luca describe express # full docs for the express server
|
|
|
36
36
|
luca describe git fs proc # multiple helpers in one shot
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
### Drill into a specific method or getter
|
|
40
|
+
|
|
41
|
+
Use dot notation to get docs for a single method or getter on any helper:
|
|
42
|
+
|
|
43
|
+
```shell
|
|
44
|
+
luca describe ui.banner # docs for the banner() method on ui
|
|
45
|
+
luca describe fs.readFile # docs for readFile() on fs
|
|
46
|
+
luca describe ui.colors # docs for the colors getter on ui
|
|
47
|
+
luca describe git.branch # docs for the branch getter on git
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This shows the description, parameters, return type, and examples for just that member. If the member doesn't exist, it lists all available methods and getters on the helper.
|
|
51
|
+
|
|
39
52
|
### Get targeted documentation
|
|
40
53
|
|
|
41
54
|
You can filter to only the sections you need:
|
|
@@ -63,7 +76,7 @@ luca describe --help # full flag reference for describe
|
|
|
63
76
|
luca help scaffold # help for any command
|
|
64
77
|
```
|
|
65
78
|
|
|
66
|
-
**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.
|
|
79
|
+
**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.
|
|
67
80
|
|
|
68
81
|
---
|
|
69
82
|
|
|
@@ -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.
|
|
3
|
+
"version": "0.0.9",
|
|
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-
|
|
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. .
|
|
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')
|
package/src/commands/describe.ts
CHANGED
|
@@ -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
|
|
package/src/commands/run.ts
CHANGED
|
@@ -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 = [
|
|
@@ -141,7 +173,10 @@ async function runMarkdown(scriptPath: string, options: z.infer<typeof argsSchem
|
|
|
141
173
|
continue
|
|
142
174
|
}
|
|
143
175
|
|
|
144
|
-
if (meta && typeof meta === 'string' && meta.toLowerCase().includes('skip'))
|
|
176
|
+
if (meta && typeof meta === 'string' && meta.toLowerCase().includes('skip')) {
|
|
177
|
+
console.log(container.ui.markdown(['```' + lang, value, '```'].join('\n')))
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
145
180
|
|
|
146
181
|
console.log(container.ui.markdown(['```' + lang, value, '```'].join('\n')))
|
|
147
182
|
|
|
@@ -196,7 +231,17 @@ async function runScript(scriptPath: string, context: ContainerContext, options:
|
|
|
196
231
|
const vm = container.feature('vm')
|
|
197
232
|
const esbuild = container.feature('esbuild')
|
|
198
233
|
const raw = container.fs.readFile(scriptPath)
|
|
199
|
-
|
|
234
|
+
|
|
235
|
+
let code: string
|
|
236
|
+
if (hasTLA(raw)) {
|
|
237
|
+
// TLA is incompatible with CJS format, so transform as ESM (preserves await)
|
|
238
|
+
// then convert import/export statements to require/module.exports for the vm
|
|
239
|
+
const { code: esm } = esbuild.transformSync(raw, { format: 'esm' })
|
|
240
|
+
code = esmToCjs(esm)
|
|
241
|
+
} else {
|
|
242
|
+
const { code: cjs } = esbuild.transformSync(raw, { format: 'cjs' })
|
|
243
|
+
code = cjs
|
|
244
|
+
}
|
|
200
245
|
|
|
201
246
|
const ctx = {
|
|
202
247
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
["d587b833-0933-4782-8c20-ad1c28d12c7a"]
|
|
@@ -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":{}}
|