@soederpop/luca 0.1.0 → 0.1.2
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/assistants/codingAssistant/hooks.ts +7 -0
- package/bun.lock +172 -7
- package/package.json +3 -3
- package/scripts/test-assistant-hooks.ts +13 -0
- package/src/agi/features/skills-library.ts +7 -0
- package/src/bootstrap/generated.ts +1 -1
- package/src/cli/build-info.ts +2 -2
- package/src/introspection/generated.agi.ts +27 -1
- package/src/introspection/generated.node.ts +1270 -1270
- package/src/introspection/generated.web.ts +1 -1
- package/src/node/features/figlet-fonts.ts +4 -1
- package/src/node/features/transpiler.ts +34 -9
- package/src/node/features/vm.ts +3 -2
- package/src/python/generated.ts +1 -1
- package/src/scaffolds/generated.ts +1 -1
- package/test/assistant.test.ts +72 -0
- package/test/vm-loadmodule.test.ts +213 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setBuildTimeData, setContainerBuildTimeData } from './index.js';
|
|
2
2
|
|
|
3
3
|
// Auto-generated introspection registry data
|
|
4
|
-
// Generated at: 2026-03-
|
|
4
|
+
// Generated at: 2026-03-30T06:52:54.962Z
|
|
5
5
|
|
|
6
6
|
setBuildTimeData('features.containerLink', {
|
|
7
7
|
"id": "features.containerLink",
|
|
@@ -294,7 +294,10 @@ import f292 from "figlet/importable-fonts/Whimsy.js";
|
|
|
294
294
|
import f293 from "figlet/importable-fonts/Wow.js";
|
|
295
295
|
import f294 from "figlet/importable-fonts/miniwi.js";
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
// Use the browser (core) subpath — filesystem-free, works in compiled binaries.
|
|
298
|
+
// figlet 1.11.0 replaced lib/ with dist/ and added an exports map; "./browser" is the stable subpath.
|
|
299
|
+
// @ts-ignore — figlet/browser exists at runtime via the package exports map
|
|
300
|
+
import figlet from "figlet/browser";
|
|
298
301
|
|
|
299
302
|
// Register all fonts with figlet (filesystem-free, works in compiled binaries)
|
|
300
303
|
figlet.parseFont("1Row", f0);
|
|
@@ -18,7 +18,9 @@ export interface TransformResult {
|
|
|
18
18
|
* so the code can run in a vm context that provides `require`.
|
|
19
19
|
*/
|
|
20
20
|
function esmToCjs(code: string): string {
|
|
21
|
-
|
|
21
|
+
const exportedNames: string[] = []
|
|
22
|
+
|
|
23
|
+
let result = code
|
|
22
24
|
// import Foo, { bar, baz } from 'x' → const Foo = require('x').default ?? require('x'); const { bar, baz } = require('x')
|
|
23
25
|
.replace(/^import\s+(\w+)\s*,\s*\{([^}]+)\}\s+from\s+(['"][^'"]+['"])\s*;?$/gm,
|
|
24
26
|
'const $1 = require($3).default ?? require($3); const {$2} = require($3);')
|
|
@@ -39,14 +41,37 @@ function esmToCjs(code: string): string {
|
|
|
39
41
|
// export { a, b } from 'x' → Object.assign(module.exports, require('x')) (re-exports)
|
|
40
42
|
.replace(/^export\s+\{[^}]*\}\s+from\s+(['"][^'"]+['"])\s*;?$/gm,
|
|
41
43
|
'Object.assign(module.exports, require($1));')
|
|
42
|
-
// export {
|
|
43
|
-
.replace(/^export\s+\{[^}]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
// export { a, b as c } → exports.a = a; exports.c = b;
|
|
45
|
+
.replace(/^export\s+\{([^}]*)\}\s*;?$/gm, (_match, body: string) => {
|
|
46
|
+
return body.split(',').map(s => {
|
|
47
|
+
const parts = s.trim().split(/\s+as\s+/)
|
|
48
|
+
const local = parts[0].trim()
|
|
49
|
+
const exported = (parts[1] || parts[0]).trim()
|
|
50
|
+
return local ? `exports['${exported}'] = ${local};` : ''
|
|
51
|
+
}).filter(Boolean).join(' ')
|
|
52
|
+
})
|
|
53
|
+
// export const/let/var NAME → const/let/var NAME (track for deferred export)
|
|
54
|
+
.replace(/^export\s+(const|let|var)\s+(\w+)/gm, (_match, decl: string, name: string) => {
|
|
55
|
+
exportedNames.push(name)
|
|
56
|
+
return `${decl} ${name}`
|
|
57
|
+
})
|
|
58
|
+
// export function NAME / export class NAME → function/class NAME (track for deferred export)
|
|
59
|
+
.replace(/^export\s+(function|class)\s+(\w+)/gm, (_match, type: string, name: string) => {
|
|
60
|
+
exportedNames.push(name)
|
|
61
|
+
return `${type} ${name}`
|
|
62
|
+
})
|
|
63
|
+
// export async function NAME → async function NAME (track for deferred export)
|
|
64
|
+
.replace(/^export\s+(async\s+function)\s+(\w+)/gm, (_match, type: string, name: string) => {
|
|
65
|
+
exportedNames.push(name)
|
|
66
|
+
return `${type} ${name}`
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Append exports for all tracked named exports
|
|
70
|
+
if (exportedNames.length > 0) {
|
|
71
|
+
result += '\n' + exportedNames.map(n => `exports['${n}'] = ${n};`).join('\n')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result
|
|
50
75
|
}
|
|
51
76
|
|
|
52
77
|
/**
|
package/src/node/features/vm.ts
CHANGED
|
@@ -392,10 +392,11 @@ export class VM<
|
|
|
392
392
|
const raw = fs.readFile(filePath)
|
|
393
393
|
const { code } = this.container.feature('transpiler').transformSync(raw, { format: 'cjs' })
|
|
394
394
|
|
|
395
|
+
const sharedExports = {}
|
|
395
396
|
const { context } = this.performSync(code, {
|
|
396
397
|
require: this.createRequireFor(filePath),
|
|
397
|
-
exports:
|
|
398
|
-
module: { exports:
|
|
398
|
+
exports: sharedExports,
|
|
399
|
+
module: { exports: sharedExports },
|
|
399
400
|
console,
|
|
400
401
|
setTimeout,
|
|
401
402
|
setInterval,
|
package/src/python/generated.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Auto-generated scaffold and MCP readme content
|
|
2
|
-
// Generated at: 2026-03-
|
|
2
|
+
// Generated at: 2026-03-30T06:52:56.013Z
|
|
3
3
|
// Source: docs/scaffolds/*.md, docs/examples/assistant/, and docs/mcp/readme.md
|
|
4
4
|
//
|
|
5
5
|
// Do not edit manually. Run: luca build-scaffolds
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'bun:test'
|
|
2
|
+
import { AGIContainer } from '../src/agi/container.server'
|
|
3
|
+
|
|
4
|
+
describe('Assistant', () => {
|
|
5
|
+
let container: AGIContainer
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
container = new AGIContainer()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('codingAssistant', () => {
|
|
12
|
+
it('loads a non-empty system prompt from CORE.md', () => {
|
|
13
|
+
const assistant = container.feature('assistant', { folder: 'assistants/codingAssistant' })
|
|
14
|
+
expect(assistant.systemPrompt.length).toBeGreaterThan(0)
|
|
15
|
+
expect(assistant.systemPrompt).toContain('coding assistant')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('loads tools from tools.ts via the VM', () => {
|
|
19
|
+
const assistant = container.feature('assistant', { folder: 'assistants/codingAssistant' })
|
|
20
|
+
const tools = assistant.availableTools
|
|
21
|
+
expect(tools).toContain('rg')
|
|
22
|
+
expect(tools).toContain('ls')
|
|
23
|
+
expect(tools).toContain('cat')
|
|
24
|
+
expect(tools).toContain('pwd')
|
|
25
|
+
expect(tools.length).toBeGreaterThan(0)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('tools have descriptions and parameter schemas', () => {
|
|
29
|
+
const assistant = container.feature('assistant', { folder: 'assistants/codingAssistant' })
|
|
30
|
+
const { rg, ls, cat } = assistant.tools
|
|
31
|
+
expect(rg.description.length).toBeGreaterThan(0)
|
|
32
|
+
expect(rg.parameters.type).toBe('object')
|
|
33
|
+
expect(rg.parameters.properties).toHaveProperty('args')
|
|
34
|
+
expect(ls.parameters.properties).toHaveProperty('args')
|
|
35
|
+
expect(cat.parameters.properties).toHaveProperty('args')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('loads hooks from hooks.ts via the VM', () => {
|
|
39
|
+
const assistant = container.feature('assistant', { folder: 'assistants/codingAssistant' })
|
|
40
|
+
const hooks = assistant.state.get('hooks') as Record<string, Function>
|
|
41
|
+
expect(hooks).toBeDefined()
|
|
42
|
+
expect(typeof hooks.started).toBe('function')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('hooks fire when the assistant starts', async () => {
|
|
46
|
+
const assistant = container.feature('assistant', {
|
|
47
|
+
folder: 'assistants/codingAssistant',
|
|
48
|
+
local: true,
|
|
49
|
+
model: 'qwen/qwen3-8b',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// bindHooksToEvents emits 'hookFired' with the event name each time a hook runs
|
|
53
|
+
const fired: string[] = []
|
|
54
|
+
assistant.on('hookFired', (eventName: string) => { fired.push(eventName) })
|
|
55
|
+
|
|
56
|
+
await assistant.start()
|
|
57
|
+
expect(fired).toContain('started')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('tools are wired into the conversation after start', async () => {
|
|
61
|
+
const assistant = container.feature('assistant', {
|
|
62
|
+
folder: 'assistants/codingAssistant',
|
|
63
|
+
local: true,
|
|
64
|
+
model: 'qwen/qwen3-8b',
|
|
65
|
+
})
|
|
66
|
+
await assistant.start()
|
|
67
|
+
const convTools = assistant.conversation.tools
|
|
68
|
+
expect(Object.keys(convTools)).toContain('rg')
|
|
69
|
+
expect(Object.keys(convTools)).toContain('ls')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { NodeContainer } from '../src/node/container'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for the vm.loadModule pipeline: TypeScript source → esmToCjs → performSync → exports.
|
|
6
|
+
*
|
|
7
|
+
* These tests exercise the transpiler+VM execution path in isolation by running
|
|
8
|
+
* TypeScript code strings directly through transpiler.transformSync + vm.performSync,
|
|
9
|
+
* exactly as loadModule does internally.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function runModule(c: NodeContainer, ts: string, ctx: Record<string, any> = {}): Record<string, any> {
|
|
13
|
+
const transpiler = c.feature('transpiler')
|
|
14
|
+
const vm = c.feature('vm')
|
|
15
|
+
const { code } = transpiler.transformSync(ts, { format: 'cjs' })
|
|
16
|
+
const sharedExports = {}
|
|
17
|
+
const { context } = vm.performSync(code, {
|
|
18
|
+
require: (id: string) => require(id),
|
|
19
|
+
exports: sharedExports,
|
|
20
|
+
module: { exports: sharedExports },
|
|
21
|
+
console,
|
|
22
|
+
...ctx,
|
|
23
|
+
})
|
|
24
|
+
return context.module?.exports || context.exports || {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('vm.loadModule pipeline', () => {
|
|
28
|
+
describe('export const / let / var', () => {
|
|
29
|
+
it('exports a const', () => {
|
|
30
|
+
const c = new NodeContainer()
|
|
31
|
+
const exports = runModule(c, `export const x = 42`)
|
|
32
|
+
expect(exports.x).toBe(42)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('exports a let', () => {
|
|
36
|
+
const c = new NodeContainer()
|
|
37
|
+
const exports = runModule(c, `export let name = 'hello'`)
|
|
38
|
+
expect(exports.name).toBe('hello')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('exports multiple consts', () => {
|
|
42
|
+
const c = new NodeContainer()
|
|
43
|
+
const exports = runModule(c, `
|
|
44
|
+
export const a = 1
|
|
45
|
+
export const b = 2
|
|
46
|
+
export const c = 3
|
|
47
|
+
`)
|
|
48
|
+
expect(exports.a).toBe(1)
|
|
49
|
+
expect(exports.b).toBe(2)
|
|
50
|
+
expect(exports.c).toBe(3)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('exports a const object', () => {
|
|
54
|
+
const c = new NodeContainer()
|
|
55
|
+
const exports = runModule(c, `
|
|
56
|
+
export const schemas = { rg: 'search', ls: 'list' }
|
|
57
|
+
`)
|
|
58
|
+
expect(exports.schemas).toEqual({ rg: 'search', ls: 'list' })
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('export function', () => {
|
|
63
|
+
it('exports a named function', () => {
|
|
64
|
+
const c = new NodeContainer()
|
|
65
|
+
const exports = runModule(c, `
|
|
66
|
+
export function greet(name: string): string {
|
|
67
|
+
return 'hello ' + name
|
|
68
|
+
}
|
|
69
|
+
`)
|
|
70
|
+
expect(typeof exports.greet).toBe('function')
|
|
71
|
+
expect(exports.greet('world')).toBe('hello world')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('exports an async function', () => {
|
|
75
|
+
const c = new NodeContainer()
|
|
76
|
+
const exports = runModule(c, `
|
|
77
|
+
export async function fetchData(): Promise<string> {
|
|
78
|
+
return 'data'
|
|
79
|
+
}
|
|
80
|
+
`)
|
|
81
|
+
expect(typeof exports.fetchData).toBe('function')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('exports multiple functions', () => {
|
|
85
|
+
const c = new NodeContainer()
|
|
86
|
+
const exports = runModule(c, `
|
|
87
|
+
export function add(a: number, b: number) { return a + b }
|
|
88
|
+
export function multiply(a: number, b: number) { return a * b }
|
|
89
|
+
`)
|
|
90
|
+
expect(exports.add(2, 3)).toBe(5)
|
|
91
|
+
expect(exports.multiply(2, 3)).toBe(6)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('export { ... }', () => {
|
|
96
|
+
it('exports from a named export block', () => {
|
|
97
|
+
const c = new NodeContainer()
|
|
98
|
+
const exports = runModule(c, `
|
|
99
|
+
const foo = 'foo'
|
|
100
|
+
const bar = 42
|
|
101
|
+
export { foo, bar }
|
|
102
|
+
`)
|
|
103
|
+
expect(exports.foo).toBe('foo')
|
|
104
|
+
expect(exports.bar).toBe(42)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('exports with renaming', () => {
|
|
108
|
+
const c = new NodeContainer()
|
|
109
|
+
const exports = runModule(c, `
|
|
110
|
+
const internal = 'value'
|
|
111
|
+
export { internal as external }
|
|
112
|
+
`)
|
|
113
|
+
expect(exports.external).toBe('value')
|
|
114
|
+
expect(exports.internal).toBeUndefined()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('export default', () => {
|
|
119
|
+
it('exports a default value', () => {
|
|
120
|
+
const c = new NodeContainer()
|
|
121
|
+
const exports = runModule(c, `export default 99`)
|
|
122
|
+
expect(exports.default).toBe(99)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('exports a default object', () => {
|
|
126
|
+
const c = new NodeContainer()
|
|
127
|
+
const exports = runModule(c, `export default { key: 'value' }`)
|
|
128
|
+
expect(exports.default).toEqual({ key: 'value' })
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('TypeScript features', () => {
|
|
133
|
+
it('strips type annotations', () => {
|
|
134
|
+
const c = new NodeContainer()
|
|
135
|
+
const exports = runModule(c, `
|
|
136
|
+
export function identity<T>(value: T): T {
|
|
137
|
+
return value
|
|
138
|
+
}
|
|
139
|
+
`)
|
|
140
|
+
expect(exports.identity('test')).toBe('test')
|
|
141
|
+
expect(exports.identity(42)).toBe(42)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('strips import type statements', () => {
|
|
145
|
+
const c = new NodeContainer()
|
|
146
|
+
// import type should be stripped entirely — no runtime require
|
|
147
|
+
const exports = runModule(c, `
|
|
148
|
+
import type { SomeType } from 'some-module'
|
|
149
|
+
export const x = 1
|
|
150
|
+
`)
|
|
151
|
+
expect(exports.x).toBe(1)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('strips declare global blocks', () => {
|
|
155
|
+
const c = new NodeContainer()
|
|
156
|
+
const exports = runModule(c, `
|
|
157
|
+
declare global {
|
|
158
|
+
var container: any
|
|
159
|
+
}
|
|
160
|
+
export const answer = 42
|
|
161
|
+
`)
|
|
162
|
+
expect(exports.answer).toBe(42)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('context injection', () => {
|
|
167
|
+
it('injected context variables are accessible in module code', () => {
|
|
168
|
+
const c = new NodeContainer()
|
|
169
|
+
const exports = runModule(c, `
|
|
170
|
+
export function getGreeting(): string {
|
|
171
|
+
return greeting + ' world'
|
|
172
|
+
}
|
|
173
|
+
`, { greeting: 'hello' })
|
|
174
|
+
expect(exports.getGreeting()).toBe('hello world')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('injected functions are callable from exported functions', () => {
|
|
178
|
+
const c = new NodeContainer()
|
|
179
|
+
const log: string[] = []
|
|
180
|
+
const exports = runModule(c, `
|
|
181
|
+
export function run() {
|
|
182
|
+
record('called')
|
|
183
|
+
}
|
|
184
|
+
`, { record: (s: string) => log.push(s) })
|
|
185
|
+
exports.run()
|
|
186
|
+
expect(log).toEqual(['called'])
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('mixed exports (tools.ts pattern)', () => {
|
|
191
|
+
it('handles the schemas + named functions pattern used by assistant tools', () => {
|
|
192
|
+
const c = new NodeContainer()
|
|
193
|
+
const exports = runModule(c, `
|
|
194
|
+
export const schemas = {
|
|
195
|
+
echo: { description: 'echo a value', properties: { text: {} } }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function echo({ text }: { text: string }): string {
|
|
199
|
+
return text
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function asyncOp(): Promise<string> {
|
|
203
|
+
return 'done'
|
|
204
|
+
}
|
|
205
|
+
`)
|
|
206
|
+
expect(exports.schemas).toBeDefined()
|
|
207
|
+
expect(exports.schemas.echo.description).toBe('echo a value')
|
|
208
|
+
expect(typeof exports.echo).toBe('function')
|
|
209
|
+
expect(exports.echo({ text: 'hi' })).toBe('hi')
|
|
210
|
+
expect(typeof exports.asyncOp).toBe('function')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
})
|