@soederpop/luca 0.0.29 → 0.0.31

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 (50) hide show
  1. package/commands/try-all-challenges.ts +1 -1
  2. package/docs/TABLE-OF-CONTENTS.md +0 -3
  3. package/docs/tutorials/20-browser-esm.md +234 -0
  4. package/package.json +1 -1
  5. package/src/agi/container.server.ts +4 -0
  6. package/src/agi/features/assistant.ts +120 -3
  7. package/src/agi/features/browser-use.ts +623 -0
  8. package/src/bootstrap/generated.ts +236 -308
  9. package/src/cli/build-info.ts +2 -2
  10. package/src/clients/rest.ts +7 -7
  11. package/src/command.ts +20 -1
  12. package/src/commands/chat.ts +22 -0
  13. package/src/commands/describe.ts +67 -2
  14. package/src/commands/prompt.ts +23 -3
  15. package/src/commands/serve.ts +27 -0
  16. package/src/container.ts +411 -113
  17. package/src/endpoint.ts +6 -0
  18. package/src/helper.ts +226 -5
  19. package/src/introspection/generated.agi.ts +16089 -10021
  20. package/src/introspection/generated.node.ts +5102 -2077
  21. package/src/introspection/generated.web.ts +379 -291
  22. package/src/introspection/index.ts +7 -0
  23. package/src/introspection/scan.ts +224 -7
  24. package/src/node/container.ts +31 -10
  25. package/src/node/features/content-db.ts +7 -7
  26. package/src/node/features/disk-cache.ts +11 -11
  27. package/src/node/features/esbuild.ts +3 -3
  28. package/src/node/features/file-manager.ts +15 -15
  29. package/src/node/features/fs.ts +23 -22
  30. package/src/node/features/git.ts +10 -10
  31. package/src/node/features/helpers.ts +5 -2
  32. package/src/node/features/ink.ts +13 -13
  33. package/src/node/features/ipc-socket.ts +8 -8
  34. package/src/node/features/networking.ts +3 -3
  35. package/src/node/features/os.ts +7 -7
  36. package/src/node/features/package-finder.ts +15 -15
  37. package/src/node/features/proc.ts +1 -1
  38. package/src/node/features/ui.ts +13 -13
  39. package/src/node/features/vm.ts +4 -4
  40. package/src/scaffolds/generated.ts +1 -1
  41. package/src/servers/express.ts +24 -6
  42. package/src/servers/mcp.ts +4 -4
  43. package/src/servers/socket.ts +6 -6
  44. package/docs/apis/features/node/window-manager.md +0 -445
  45. package/docs/examples/window-manager-layouts.md +0 -180
  46. package/docs/examples/window-manager.md +0 -125
  47. package/docs/window-manager-fix.md +0 -249
  48. package/scripts/test-window-manager-lifecycle.ts +0 -86
  49. package/scripts/test-window-manager.ts +0 -43
  50. package/src/node/features/window-manager.ts +0 -1603
@@ -20,6 +20,9 @@ export const argsSchema = CommandOptionsSchema.extend({
20
20
  clear: z.boolean().optional().describe('Clear the conversation history for the resolved history mode and exit'),
21
21
  prependPrompt: z.string().optional().describe('Text or path to a markdown file to prepend to the system prompt'),
22
22
  appendPrompt: z.string().optional().describe('Text or path to a markdown file to append to the system prompt'),
23
+ use: z.union([z.string(), z.array(z.string())]).optional().describe('Feature(s) to inject into the assistant via .use(). Supports options: --use "contentDb:rootPath=/tmp;lazy=true"'),
24
+ forbidTool: z.union([z.string(), z.array(z.string())]).optional().describe('Tool name patterns to exclude (supports * glob). Can be specified multiple times.'),
25
+ allowTool: z.union([z.string(), z.array(z.string())]).optional().describe('Tool name patterns to allow (strict allowlist, supports * glob). Can be specified multiple times.'),
23
26
  })
24
27
 
25
28
  export default async function chat(options: z.infer<typeof argsSchema>, context: ContainerContext) {
@@ -72,6 +75,8 @@ export default async function chat(options: z.infer<typeof argsSchema>, context:
72
75
  const createOptions: Record<string, any> = { historyMode, injectTimestamps: true }
73
76
  if (options.model) createOptions.model = options.model
74
77
  if (options.local) createOptions.local = options.local
78
+ if (options.forbidTool) createOptions.forbidTools = Array.isArray(options.forbidTool) ? options.forbidTool : [options.forbidTool]
79
+ if (options.allowTool) createOptions.allowTools = Array.isArray(options.allowTool) ? options.allowTool : [options.allowTool]
75
80
 
76
81
  // Resolve --prepend-prompt / --append-prompt: if it's an existing file, read it; if it ends in .md but doesn't exist, error
77
82
  const fs = container.feature('fs')
@@ -176,6 +181,23 @@ export default async function chat(options: z.infer<typeof argsSchema>, context:
176
181
  process.stdout.write('\n')
177
182
  })
178
183
 
184
+ // --use: inject features into the assistant
185
+ if (options.use) {
186
+ const items = Array.isArray(options.use) ? options.use : [options.use]
187
+ for (const item of items) {
188
+ const [namepart, optStr] = item.split(':')
189
+ const featureOpts: Record<string, any> = { enable: true }
190
+ if (optStr) {
191
+ for (const pair of optStr.split(';')) {
192
+ const [k, v] = pair.split('=')
193
+ if (k) featureOpts[k.trim()] = v?.trim() === 'true' ? true : v?.trim() === 'false' ? false : v?.trim()
194
+ }
195
+ }
196
+ const feature = container.feature(namepart.trim(), featureOpts)
197
+ assistant.use(feature)
198
+ }
199
+ }
200
+
179
201
  // Start the assistant (loads history if applicable)
180
202
  await assistant.start()
181
203
 
@@ -2,11 +2,12 @@ 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 { HelperIntrospection } from '../introspection/index.js'
5
+ import type { HelperIntrospection, IntrospectionSection } from '../introspection/index.js'
6
6
  import { __INTROSPECTION__, __BROWSER_INTROSPECTION__ } from '../introspection/index.js'
7
7
  import { features } from '../feature.js'
8
8
  import { ContainerDescriber } from '../container-describer.js'
9
9
  import type { BrowserFeatureData } from '../container-describer.js'
10
+ import { presentIntrospectionAsTypeScript } from '../helper.js'
10
11
 
11
12
  declare module '../command.js' {
12
13
  interface AvailableCommands {
@@ -16,6 +17,8 @@ declare module '../command.js' {
16
17
 
17
18
  export const argsSchema = CommandOptionsSchema.extend({
18
19
  json: z.boolean().default(false).describe('Output introspection data as JSON instead of markdown'),
20
+ typescript: z.boolean().default(false).describe('Output introspection data as TypeScript interface declarations'),
21
+ ts: z.boolean().default(false).describe('Alias for --typescript'),
19
22
  pretty: z.boolean().default(false).describe('Render markdown with terminal styling via ui.markdown'),
20
23
  title: z.boolean().default(true).describe('Include the title header in the output (use --no-title to omit)'),
21
24
  // Clean section flags (can be combined: --description --usage)
@@ -156,7 +159,12 @@ export default async function describe(options: z.infer<typeof argsSchema>, cont
156
159
  platform: options.platform as any,
157
160
  })
158
161
 
159
- if (options.json) {
162
+ const wantsTypeScript = options.typescript || options.ts
163
+
164
+ if (wantsTypeScript) {
165
+ const output = renderResultAsTypeScript(result, targets, describer, sections)
166
+ console.log(output)
167
+ } else if (options.json) {
160
168
  console.log(JSON.stringify(result.json, null, 2))
161
169
  } else if (options.pretty) {
162
170
  const ui = container.feature('ui')
@@ -166,6 +174,63 @@ export default async function describe(options: z.infer<typeof argsSchema>, cont
166
174
  }
167
175
  }
168
176
 
177
+ /**
178
+ * Renders the describe result as TypeScript interface declarations.
179
+ * Handles single helpers, arrays of helpers (registry describes), and the container.
180
+ */
181
+ function renderResultAsTypeScript(result: { json: any; text: string }, targets: string[], describer: ContainerDescriber, sections: (IntrospectionSection | 'description')[]): string {
182
+ const json = result.json
183
+ const section = sections.length === 1 && sections[0] !== 'description' ? sections[0] as IntrospectionSection : undefined
184
+
185
+ // Single helper introspection object (has shortcut = full data, or id = filtered data)
186
+ if (json && (json.shortcut || json.id)) {
187
+ // If sections were applied, the JSON is partial — get full data and pass section to renderer
188
+ const fullData = json.shortcut ? json : __INTROSPECTION__.get(json.id) || json
189
+ return presentIntrospectionAsTypeScript(fullData, section)
190
+ }
191
+
192
+ // Container introspection (has className, registries, factories)
193
+ if (json && json.className && json.registries) {
194
+ const container = (describer as any).container
195
+ return container.inspectAsType()
196
+ }
197
+
198
+ // Array of results (e.g. from registry describe or multiple targets)
199
+ if (Array.isArray(json)) {
200
+ const interfaces = json
201
+ .filter((item: any) => item && (item.shortcut || item.id))
202
+ .map((item: any) => {
203
+ const fullData = item.shortcut ? item : __INTROSPECTION__.get(item.id) || item
204
+ return presentIntrospectionAsTypeScript(fullData, section)
205
+ })
206
+ return interfaces.join('\n\n')
207
+ }
208
+
209
+ // Object keyed by helper id (registry describe format — has _shared and per-helper summaries)
210
+ if (json && typeof json === 'object' && !json._helper && json._shared) {
211
+ const ids = Object.keys(json).filter(k => k !== '_shared')
212
+ const interfaces = ids
213
+ .map((id: string) => {
214
+ // Try qualified key first (e.g. "clients.rest"), then scan the introspection map
215
+ for (const prefix of ['features', 'clients', 'servers', 'commands', 'endpoints']) {
216
+ const data = __INTROSPECTION__.get(`${prefix}.${id}`)
217
+ if (data) return presentIntrospectionAsTypeScript(data, section)
218
+ }
219
+ return null
220
+ })
221
+ .filter(Boolean)
222
+ if (interfaces.length > 0) return interfaces.join('\n\n')
223
+ }
224
+
225
+ // Member-level result (has _helper and _type) — render the full helper interface
226
+ if (json && json._helper) {
227
+ const fullData = __INTROSPECTION__.get(json._helper) || __INTROSPECTION__.get(`features.${json._helper}`)
228
+ if (fullData) return presentIntrospectionAsTypeScript(fullData)
229
+ }
230
+
231
+ return result.text
232
+ }
233
+
169
234
  commands.registerHandler('describe', {
170
235
  description: 'Describe the container, registries, or individual helpers',
171
236
  argsSchema,
@@ -12,7 +12,8 @@ declare module '../command.js' {
12
12
 
13
13
  export const argsSchema = CommandOptionsSchema.extend({
14
14
  model: z.string().optional().describe('Override the LLM model (assistant mode only)'),
15
- 'preserve-frontmatter': z.boolean().default(false).describe('Keep YAML frontmatter in the prompt instead of stripping it before sending to the agent.'),
15
+ 'include-frontmatter': z.boolean().default(false).describe('Keep YAML frontmatter in the prompt instead of stripping it before sending to the agent.'),
16
+ 'skip-eval': z.boolean().default(false).describe('Skip execution of fenced code blocks in the prompt file'),
16
17
  'permission-mode': z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).default('acceptEdits').describe('Permission mode for CLI agents (default: acceptEdits)'),
17
18
  'in-folder': z.string().optional().describe('Run the CLI agent in this directory (resolved via container.paths)'),
18
19
  'out-file': z.string().optional().describe('Save session output as a markdown file'),
@@ -790,12 +791,31 @@ async function preparePrompt(
790
791
  }
791
792
 
792
793
  let promptContent: string
793
- if (options['preserve-frontmatter']) {
794
- promptContent = content
794
+ if (options['skip-eval']) {
795
+ // Strip frontmatter but don't execute code blocks
796
+ if (content.startsWith('---')) {
797
+ const fmEnd = content.indexOf('\n---', 3)
798
+ if (fmEnd !== -1) {
799
+ promptContent = content.slice(fmEnd + 4).trimStart()
800
+ } else {
801
+ promptContent = content
802
+ }
803
+ } else {
804
+ promptContent = content
805
+ }
795
806
  } else {
796
807
  promptContent = await executePromptFile(resolvedPath, container, resolvedInputs)
797
808
  }
798
809
 
810
+ // Re-attach frontmatter if requested
811
+ if (options['include-frontmatter'] && content.startsWith('---')) {
812
+ const fmEnd = content.indexOf('\n---', 3)
813
+ if (fmEnd !== -1) {
814
+ const frontmatter = content.slice(0, fmEnd + 4)
815
+ promptContent = frontmatter + '\n' + promptContent
816
+ }
817
+ }
818
+
799
819
  // Substitute {{key}} placeholders with resolved input values
800
820
  if (Object.keys(resolvedInputs).length) {
801
821
  promptContent = substituteInputs(promptContent, resolvedInputs)
@@ -19,6 +19,7 @@ export const argsSchema = CommandOptionsSchema.extend({
19
19
  force: z.boolean().default(false).describe('Kill any process currently using the target port'),
20
20
  anyPort: z.boolean().default(false).describe('Find an available port starting above 3000'),
21
21
  open: z.boolean().default(true).describe('Open the server URL in Google Chrome'),
22
+ watch: z.boolean().default(false).describe('Watch endpoint files and hot-reload handlers on change'),
22
23
  })
23
24
 
24
25
  export default async function serve(options: z.infer<typeof argsSchema>, context: ContainerContext) {
@@ -157,6 +158,32 @@ export default async function serve(options: z.infer<typeof argsSchema>, context
157
158
  }
158
159
  }
159
160
 
161
+ if (options.watch && endpointsDir) {
162
+ const fm = container.feature('fileManager')
163
+ await fm.watch({ paths: [endpointsDir] })
164
+
165
+ fm.on('file:change', async ({ type, path: filePath }: { type: string; path: string }) => {
166
+ if (!filePath.endsWith('.ts')) return
167
+
168
+ if (type === 'change') {
169
+ try {
170
+ const ep = await expressServer.reloadEndpoint(filePath)
171
+ if (ep) {
172
+ console.log(`[watch] Reloaded ${ep.methods.map((m: string) => m.toUpperCase()).join(',')} ${ep.path}`)
173
+ }
174
+ } catch (err: any) {
175
+ console.error(`[watch] Failed to reload ${filePath}: ${err.message}`)
176
+ }
177
+ } else if (type === 'add') {
178
+ console.log(`[watch] New file detected: ${filePath} (restart to mount)`)
179
+ } else if (type === 'delete') {
180
+ console.log(`[watch] File deleted: ${filePath} (restart to unmount)`)
181
+ }
182
+ })
183
+
184
+ console.log(`\n[watch] Watching ${endpointsDir} for changes`)
185
+ }
186
+
160
187
  console.log()
161
188
  }
162
189