@soederpop/luca 0.0.31 → 0.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +241 -36
  2. package/bun.lock +24 -5
  3. package/commands/build-python-bridge.ts +43 -0
  4. package/docs/apis/clients/rest.md +7 -7
  5. package/docs/apis/clients/websocket.md +23 -10
  6. package/docs/apis/features/agi/assistant.md +155 -8
  7. package/docs/apis/features/agi/assistants-manager.md +90 -22
  8. package/docs/apis/features/agi/auto-assistant.md +377 -0
  9. package/docs/apis/features/agi/browser-use.md +802 -0
  10. package/docs/apis/features/agi/claude-code.md +6 -1
  11. package/docs/apis/features/agi/conversation-history.md +7 -6
  12. package/docs/apis/features/agi/conversation.md +111 -38
  13. package/docs/apis/features/agi/docs-reader.md +35 -57
  14. package/docs/apis/features/agi/file-tools.md +163 -0
  15. package/docs/apis/features/agi/openapi.md +2 -2
  16. package/docs/apis/features/agi/skills-library.md +227 -0
  17. package/docs/apis/features/node/content-db.md +125 -4
  18. package/docs/apis/features/node/disk-cache.md +11 -11
  19. package/docs/apis/features/node/downloader.md +1 -1
  20. package/docs/apis/features/node/file-manager.md +15 -15
  21. package/docs/apis/features/node/fs.md +78 -21
  22. package/docs/apis/features/node/git.md +50 -10
  23. package/docs/apis/features/node/google-calendar.md +3 -0
  24. package/docs/apis/features/node/google-docs.md +10 -1
  25. package/docs/apis/features/node/google-drive.md +3 -0
  26. package/docs/apis/features/node/google-mail.md +214 -0
  27. package/docs/apis/features/node/google-sheets.md +3 -0
  28. package/docs/apis/features/node/ink.md +10 -10
  29. package/docs/apis/features/node/ipc-socket.md +83 -93
  30. package/docs/apis/features/node/networking.md +5 -5
  31. package/docs/apis/features/node/os.md +7 -7
  32. package/docs/apis/features/node/package-finder.md +14 -14
  33. package/docs/apis/features/node/proc.md +2 -1
  34. package/docs/apis/features/node/process-manager.md +70 -3
  35. package/docs/apis/features/node/python.md +265 -9
  36. package/docs/apis/features/node/redis.md +380 -0
  37. package/docs/apis/features/node/ui.md +13 -13
  38. package/docs/apis/servers/express.md +35 -7
  39. package/docs/apis/servers/mcp.md +3 -3
  40. package/docs/apis/servers/websocket.md +51 -8
  41. package/docs/bootstrap/CLAUDE.md +1 -1
  42. package/docs/bootstrap/SKILL.md +93 -7
  43. package/docs/examples/feature-as-tool-provider.md +143 -0
  44. package/docs/examples/python.md +42 -1
  45. package/docs/introspection.md +15 -5
  46. package/docs/tutorials/00-bootstrap.md +3 -3
  47. package/docs/tutorials/02-container.md +2 -2
  48. package/docs/tutorials/10-creating-features.md +5 -0
  49. package/docs/tutorials/13-introspection.md +12 -2
  50. package/docs/tutorials/19-python-sessions.md +401 -0
  51. package/package.json +8 -4
  52. package/src/agi/container.server.ts +8 -0
  53. package/src/agi/features/assistant.ts +19 -0
  54. package/src/agi/features/autonomous-assistant.ts +435 -0
  55. package/src/agi/features/conversation.ts +58 -6
  56. package/src/agi/features/file-tools.ts +286 -0
  57. package/src/agi/features/luca-coder.ts +643 -0
  58. package/src/bootstrap/generated.ts +705 -17
  59. package/src/cli/build-info.ts +2 -2
  60. package/src/cli/cli.ts +22 -13
  61. package/src/commands/bootstrap.ts +49 -6
  62. package/src/commands/code.ts +369 -0
  63. package/src/commands/describe.ts +7 -2
  64. package/src/commands/index.ts +1 -0
  65. package/src/commands/sandbox-mcp.ts +7 -7
  66. package/src/commands/save-api-docs.ts +1 -1
  67. package/src/container-describer.ts +4 -4
  68. package/src/container.ts +10 -19
  69. package/src/helper.ts +24 -33
  70. package/src/introspection/generated.agi.ts +2499 -63
  71. package/src/introspection/generated.node.ts +1625 -688
  72. package/src/introspection/generated.web.ts +15 -57
  73. package/src/node/container.ts +5 -0
  74. package/src/node/features/figlet-fonts.ts +597 -0
  75. package/src/node/features/fs.ts +3 -9
  76. package/src/node/features/helpers.ts +20 -0
  77. package/src/node/features/python.ts +429 -16
  78. package/src/node/features/redis.ts +446 -0
  79. package/src/node/features/ui.ts +4 -11
  80. package/src/python/bridge.py +220 -0
  81. package/src/python/generated.ts +227 -0
  82. package/src/scaffolds/generated.ts +1 -1
  83. package/test/python-session.test.ts +105 -0
  84. package/assistants/lucaExpert/CORE.md +0 -37
  85. package/assistants/lucaExpert/hooks.ts +0 -9
  86. package/assistants/lucaExpert/tools.ts +0 -177
@@ -1,4 +1,4 @@
1
1
  // Generated at compile time — do not edit manually
2
- export const BUILD_SHA = 'ff23ed7'
2
+ export const BUILD_SHA = 'd3e8b52'
3
3
  export const BUILD_BRANCH = 'main'
4
- export const BUILD_DATE = '2026-03-24T09:08:07Z'
4
+ export const BUILD_DATE = '2026-03-26T03:30:23Z'
package/src/cli/cli.ts CHANGED
@@ -37,9 +37,15 @@ async function main() {
37
37
  // helpers.discoverAll() which registers project commands early
38
38
  const builtinCommands = new Set(container.commands.available as string[])
39
39
 
40
+ // Load global CLI module (~/.luca/luca.cli.ts) before project-level —
41
+ // lets users set up global helpers, discovery roots, etc.
42
+ let done = t('loadGlobalCliModule')
43
+ await loadCliModule(join(homedir(), '.luca', 'luca.cli.ts'))
44
+ done()
45
+
40
46
  // Load project-level CLI module (luca.cli.ts) for container customization
41
- let done = t('loadCliModule')
42
- await loadCliModule()
47
+ done = t('loadCliModule')
48
+ await loadCliModule(container.paths.resolve('luca.cli.ts'))
43
49
  done()
44
50
 
45
51
  // Discover project-local commands (commands/ or src/commands/)
@@ -51,10 +57,10 @@ async function main() {
51
57
  const afterProject = new Set(container.commands.available as string[])
52
58
  const projectCommands = new Set([...afterProject].filter((n) => !builtinCommands.has(n)))
53
59
 
54
- // Discover user-level commands (~/.luca/commands/)
55
- done = t('discoverUserCommands')
60
+ // Discover user-level helpers (~/.luca/{features,clients,servers,commands,selectors}/)
61
+ done = t('discoverUserHelpers')
56
62
  if (discovery !== 'disable' && discovery !== 'no-home') {
57
- await discoverUserCommands()
63
+ await discoverUserHelpers()
58
64
  }
59
65
  done()
60
66
  const afterUser = new Set(container.commands.available as string[])
@@ -135,8 +141,7 @@ function resolveScript(ref: string, container: any) {
135
141
  return null
136
142
  }
137
143
 
138
- async function loadCliModule() {
139
- const modulePath = container.paths.resolve('luca.cli.ts')
144
+ async function loadCliModule(modulePath: string) {
140
145
  if (!container.fs.exists(modulePath)) return
141
146
 
142
147
  // Use the helpers feature to load the module — it handles the native import
@@ -181,13 +186,17 @@ async function loadProjectIntrospection() {
181
186
  }
182
187
  }
183
188
 
184
- async function discoverUserCommands() {
185
- const dir = join(homedir(), '.luca', 'commands')
189
+ const DISCOVERABLE_TYPES = ['features', 'clients', 'servers', 'commands', 'selectors'] as const
186
190
 
187
- if (container.fs.exists(dir)) {
188
- // Route through helpers for consistent dedup and VM/native handling
189
- const helpers = container.feature('helpers') as any
190
- await helpers.discover('commands', { directory: dir })
191
+ async function discoverUserHelpers() {
192
+ const lucaHome = join(homedir(), '.luca')
193
+ const helpers = container.feature('helpers') as any
194
+
195
+ for (const type of DISCOVERABLE_TYPES) {
196
+ const dir = join(lucaHome, type)
197
+ if (container.fs.exists(dir)) {
198
+ await helpers.discover(type, { directory: dir })
199
+ }
191
200
  }
192
201
  }
193
202
 
@@ -3,7 +3,6 @@ import { commands } from '../command.js'
3
3
  import { CommandOptionsSchema } from '../schemas/base.js'
4
4
  import type { ContainerContext } from '../container.js'
5
5
  import { bootstrapFiles, bootstrapTemplates, bootstrapExamples, bootstrapTutorials } from '../bootstrap/generated.js'
6
- import { apiDocs } from './save-api-docs.js'
7
6
  import { generateScaffold } from '../scaffolds/template.js'
8
7
 
9
8
  declare module '../command.js' {
@@ -14,16 +13,34 @@ declare module '../command.js' {
14
13
 
15
14
  export const argsSchema = CommandOptionsSchema.extend({
16
15
  output: z.string().default('.').describe('Output folder path (defaults to cwd)'),
16
+ 'update-skill': z.boolean().default(false).describe('Only update .claude/skills/luca-framework in the current project'),
17
17
  })
18
18
 
19
19
  async function bootstrap(options: z.infer<typeof argsSchema>, context: ContainerContext) {
20
20
  const { container } = context
21
21
  const args = container.argv._ as string[]
22
- const outputDir = container.paths.resolve(args[1] || options.output)
23
22
  const fs = container.feature('fs')
24
23
  const ui = container.feature('ui')
25
24
  const proc = container.feature('proc')
26
25
 
26
+ // ── --update-skill: refresh skill files in the current project ──
27
+ if (options['update-skill']) {
28
+ return await updateSkill(container, fs, ui)
29
+ }
30
+
31
+ // Require an explicit target — don't silently bootstrap into cwd
32
+ let target = args[1] || (options.output !== '.' ? options.output : '')
33
+
34
+ if (!target) {
35
+ const answer = await ui.askQuestion('Project name (folder to create):')
36
+ target = answer?.question?.trim()
37
+ if (!target) {
38
+ ui.print.red('\n No project name given, aborting.\n')
39
+ return
40
+ }
41
+ }
42
+
43
+ const outputDir = container.paths.resolve(target)
27
44
  await fs.ensureFolder(outputDir)
28
45
  const mkPath = (...segments: string[]) => container.paths.resolve(outputDir, ...segments)
29
46
 
@@ -48,10 +65,6 @@ async function bootstrap(options: z.infer<typeof argsSchema>, context: Container
48
65
  await fs.ensureFolder(skillDir)
49
66
  await writeFile(fs, ui, container.paths.resolve(skillDir, 'SKILL.md'), bootstrapFiles['SKILL'] || '', '.claude/skills/luca-framework/SKILL.md')
50
67
 
51
- ui.print.cyan(' Generating API docs...')
52
- const apiDocsPath = container.paths.resolve(skillDir, 'references', 'api-docs')
53
- await apiDocs({ _: [], outputPath: apiDocsPath }, context)
54
-
55
68
  // ── 3b. examples and tutorials ─────────────────────────────────
56
69
  const examplesDir = container.paths.resolve(skillDir, 'references', 'examples')
57
70
  await fs.ensureFolder(examplesDir)
@@ -146,6 +159,36 @@ async function bootstrap(options: z.infer<typeof argsSchema>, context: Container
146
159
 
147
160
  // ── Helpers ──────────────────────────────────────────────────────────
148
161
 
162
+ async function updateSkill(container: any, fs: any, ui: any) {
163
+ const skillDir = container.paths.resolve('.claude', 'skills', 'luca-framework')
164
+
165
+ // Wipe existing skill directory so stale files don't linger
166
+ if (fs.exists(skillDir)) {
167
+ await fs.rmdir(skillDir)
168
+ }
169
+
170
+ await fs.ensureFolder(skillDir)
171
+ await writeFile(fs, ui, container.paths.resolve(skillDir, 'SKILL.md'), bootstrapFiles['SKILL'] || '', '.claude/skills/luca-framework/SKILL.md')
172
+
173
+ const examplesDir = container.paths.resolve(skillDir, 'references', 'examples')
174
+ await fs.ensureFolder(examplesDir)
175
+ for (const [filename, content] of Object.entries(bootstrapExamples)) {
176
+ await fs.writeFileAsync(container.paths.resolve(examplesDir, filename), content)
177
+ }
178
+ ui.print.cyan(` Writing ${Object.keys(bootstrapExamples).length} example docs...`)
179
+
180
+ const tutorialsDir = container.paths.resolve(skillDir, 'references', 'tutorials')
181
+ await fs.ensureFolder(tutorialsDir)
182
+ for (const [filename, content] of Object.entries(bootstrapTutorials)) {
183
+ await fs.writeFileAsync(container.paths.resolve(tutorialsDir, filename), content)
184
+ }
185
+ ui.print.cyan(` Writing ${Object.keys(bootstrapTutorials).length} tutorial docs...`)
186
+
187
+ ui.print('')
188
+ ui.print.green(' ✓ Skill updated!')
189
+ ui.print('')
190
+ }
191
+
149
192
  async function writeFile(fs: any, ui: any, path: string, content: string, label: string) {
150
193
  ui.print.cyan(` Writing ${label}...`)
151
194
  await fs.writeFileAsync(path, content)
@@ -0,0 +1,369 @@
1
+ import { z } from 'zod'
2
+ import * as readline from 'readline'
3
+ import { commands } from '../command.js'
4
+ import { CommandOptionsSchema } from '../schemas/base.js'
5
+ import type { ContainerContext } from '../container.js'
6
+ import type { AGIContainer, AGIFeatures } from '../agi/container.server.js'
7
+
8
+ declare module '../command.js' {
9
+ interface AvailableCommands {
10
+ code: ReturnType<typeof commands.registerHandler>
11
+ }
12
+ }
13
+
14
+ export const argsSchema = CommandOptionsSchema.extend({
15
+ model: z.string().optional().describe('Override the LLM model'),
16
+ local: z.boolean().default(false).describe('Use a local API server'),
17
+ prompt: z.string().optional().describe('Path to a markdown file or inline text for the system prompt'),
18
+ allowAll: z.boolean().default(false).describe('Start with all permissions set to allow (fully autonomous)'),
19
+ denyWrites: z.boolean().default(false).describe('Deny all write/delete/move operations'),
20
+ skills: z.string().optional().describe('Comma-separated list of additional skill names to load'),
21
+ })
22
+
23
+ export const positionals = ['prompt']
24
+
25
+ export default async function code(options: z.infer<typeof argsSchema>, context: ContainerContext<AGIFeatures>) {
26
+ const container = context.container as AGIContainer
27
+ const ui = container.feature('ui')
28
+ const colors = ui.colors
29
+ const fs = container.feature('fs')
30
+
31
+ // ── Resolve system prompt ──────────────────────────────────────────────
32
+ // The lucaCoder feature has a solid default system prompt. Only override
33
+ // if the user explicitly provides one via --prompt.
34
+ let systemPrompt: string | undefined
35
+
36
+ if (options.prompt) {
37
+ const resolved = container.paths.resolve(options.prompt)
38
+ if (fs.exists(resolved)) {
39
+ systemPrompt = fs.readFile(resolved)
40
+ } else if (!options.prompt.endsWith('.md')) {
41
+ systemPrompt = options.prompt
42
+ } else {
43
+ console.error(colors.red(`File not found: ${resolved}`))
44
+ process.exit(1)
45
+ }
46
+ }
47
+
48
+ // ── Permission profile ─────────────────────────────────────────────────
49
+ const readTools = ['readFile', 'searchFiles', 'findFiles', 'listDirectory', 'fileInfo']
50
+ const writeTools = ['writeFile', 'editFile', 'createDirectory', 'moveFile', 'copyFile', 'bash']
51
+ const dangerTools = ['deleteFile']
52
+ const skillTools = ['searchAvailableSkills', 'loadSkill', 'askSkillBasedQuestion']
53
+
54
+ const permissions: Record<string, 'allow' | 'ask' | 'deny'> = {}
55
+
56
+ // Skill tools are always allowed — they're read-only discovery operations
57
+ for (const t of skillTools) permissions[t] = 'allow'
58
+
59
+ if (options.allowAll) {
60
+ for (const t of [...readTools, ...writeTools, ...dangerTools]) permissions[t] = 'allow'
61
+ } else if (options.denyWrites) {
62
+ for (const t of readTools) permissions[t] = 'allow'
63
+ for (const t of writeTools) permissions[t] = 'deny'
64
+ for (const t of dangerTools) permissions[t] = 'deny'
65
+ } else {
66
+ // Default: reads are free, writes need approval, danger is denied
67
+ for (const t of readTools) permissions[t] = 'allow'
68
+ for (const t of writeTools) permissions[t] = 'ask'
69
+ for (const t of dangerTools) permissions[t] = 'deny'
70
+ }
71
+
72
+ // ── Parse extra skills ────────────────────────────────────────────────
73
+ const extraSkills = options.skills ? options.skills.split(',').map((s: string) => s.trim()).filter(Boolean) : undefined
74
+
75
+ // ── Create the luca coder ──────────────────────────────────────────────
76
+ const coder = container.feature('lucaCoder', {
77
+ tools: ['fileTools'],
78
+ permissions,
79
+ defaultPermission: 'ask',
80
+ systemPrompt,
81
+ model: options.model,
82
+ local: options.local,
83
+ skills: extraSkills,
84
+ })
85
+
86
+ // ── UI setup ───────────────────────────────────────────────────────────
87
+ const ink = container.feature('ink', { enable: true })
88
+ await ink.loadModules()
89
+ const React = ink.React
90
+ const { Text } = ink.components
91
+ const inkModule = await import('ink')
92
+
93
+ let responseBuffer = ''
94
+ let inkInstance: any = null
95
+
96
+ function mdElement(content: string) {
97
+ const rendered = content ? String(ui.markdown(content)).trimEnd() : ''
98
+ return React.createElement(Text, null, rendered)
99
+ }
100
+
101
+ // Wire events on coder before starting (it forwards from inner assistant)
102
+ coder.on('chunk', (text: string) => {
103
+ responseBuffer += text
104
+ if (!inkInstance) {
105
+ process.stdout.write('\n')
106
+ inkInstance = inkModule.render(mdElement(responseBuffer), { patchConsole: false })
107
+ } else {
108
+ inkInstance.rerender(mdElement(responseBuffer))
109
+ }
110
+ })
111
+
112
+ coder.on('toolCall', (toolName: string, args: any) => {
113
+ if (inkInstance) { inkInstance.unmount(); inkInstance = null }
114
+ responseBuffer = ''
115
+ const argsStr = JSON.stringify(args).slice(0, 120)
116
+ process.stdout.write(colors.dim(`\n ⟳ ${toolName}`) + colors.dim(`(${argsStr})\n`))
117
+ })
118
+
119
+ coder.on('toolResult', (toolName: string, result: any) => {
120
+ const preview = typeof result === 'string' ? result.slice(0, 100) : JSON.stringify(result).slice(0, 100)
121
+ process.stdout.write(colors.green(` ✓ ${toolName}`) + colors.dim(` → ${preview}${preview.length >= 100 ? '…' : ''}\n`))
122
+ })
123
+
124
+ coder.on('toolError', (toolName: string, error: any) => {
125
+ const msg = error?.message || String(error)
126
+ process.stdout.write(colors.red(` ✗ ${toolName}: ${msg}\n`))
127
+ })
128
+
129
+ coder.on('response', () => {
130
+ if (inkInstance) { inkInstance.unmount(); inkInstance = null }
131
+ responseBuffer = ''
132
+ process.stdout.write('\n')
133
+ })
134
+
135
+ // Start the coder (creates inner assistant, registers bash tool, stacks fileTools, loads skills)
136
+ await coder.start()
137
+
138
+ // ── Permission request handler ─────────────────────────────────────────
139
+ coder.on('permissionRequest', ({ id, toolName, args }: { id: string; toolName: string; args: Record<string, any> }) => {
140
+ if (inkInstance) { inkInstance.unmount(); inkInstance = null }
141
+
142
+ const argsPreview = Object.entries(args)
143
+ .map(([k, v]) => {
144
+ const val = typeof v === 'string'
145
+ ? (v.length > 60 ? v.slice(0, 57) + '...' : v)
146
+ : JSON.stringify(v)
147
+ return ` ${colors.dim(k)}: ${val}`
148
+ })
149
+ .join('\n')
150
+
151
+ process.stdout.write('\n')
152
+ process.stdout.write(colors.yellow(` ⚡ Permission required: ${colors.bold(toolName)}\n`))
153
+ if (argsPreview) process.stdout.write(argsPreview + '\n')
154
+ process.stdout.write(colors.dim(`\n [y] approve [n] deny [a] allow all future ${toolName} calls\n`))
155
+
156
+ promptApproval(id, toolName)
157
+ })
158
+
159
+ coder.on('toolBlocked', (toolName: string, reason: string) => {
160
+ process.stdout.write(colors.red(` ✗ ${toolName} blocked (${reason})\n`))
161
+ })
162
+
163
+ // ── Readline ───────────────────────────────────────────────────────────
164
+ let rl = readline.createInterface({
165
+ input: process.stdin,
166
+ output: process.stdout,
167
+ })
168
+ let rlClosed = false
169
+ rl.on('close', () => { rlClosed = true })
170
+
171
+ function ensureRl() {
172
+ if (rlClosed) {
173
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout })
174
+ rlClosed = false
175
+ rl.on('close', () => { rlClosed = true })
176
+ }
177
+ }
178
+
179
+ function prompt(): Promise<string> {
180
+ return new Promise((resolve) => {
181
+ ensureRl()
182
+ rl.question(colors.cyan('\n code > '), (answer: string) => resolve(answer.trim()))
183
+ })
184
+ }
185
+
186
+ function promptApproval(id: string, toolName: string) {
187
+ ensureRl()
188
+ rl.question(colors.yellow(' > '), (answer: string) => {
189
+ const a = answer.trim().toLowerCase()
190
+ if (a === 'y' || a === 'yes') {
191
+ coder.approve(id)
192
+ } else if (a === 'a' || a === 'always') {
193
+ coder.permitTool(toolName)
194
+ coder.approve(id)
195
+ process.stdout.write(colors.green(` ✓ ${toolName} will be auto-approved from now on\n`))
196
+ } else {
197
+ coder.deny(id)
198
+ }
199
+ })
200
+ }
201
+
202
+ // ── Piped stdin mode ──────────────────────────────────────────────────
203
+ // When stdin is piped (e.g. `cat prompt.md | luca code`), read all of
204
+ // stdin as the initial prompt, run it, print the result, and exit.
205
+ if (!process.stdin.isTTY) {
206
+ const chunks: Buffer[] = []
207
+ for await (const chunk of process.stdin) {
208
+ chunks.push(chunk as Buffer)
209
+ }
210
+ const pipedInput = Buffer.concat(chunks).toString('utf-8').trim()
211
+
212
+ if (!pipedInput) {
213
+ console.error(colors.red(' No input received from stdin'))
214
+ process.exit(1)
215
+ }
216
+
217
+ console.log(colors.dim(` Piped input: ${pipedInput.length} chars`))
218
+ await coder.ask(pipedInput)
219
+ process.exit(0)
220
+ }
221
+
222
+ // ── Banner ─────────────────────────────────────────────────────────────
223
+ console.log()
224
+ console.log(ui.banner('CODE', { font: 'Small', colors: ['cyan', 'blue'] }))
225
+
226
+ const toolCount = Object.keys(coder.tools).length
227
+ const allowCount = Object.values(permissions).filter(v => v === 'allow').length
228
+ const askCount = Object.values(permissions).filter(v => v === 'ask').length
229
+ const denyCount = Object.values(permissions).filter(v => v === 'deny').length
230
+ const loadedSkills = coder.state.get('loadedSkills') as string[]
231
+
232
+ console.log(colors.dim(` ${toolCount} tools loaded`))
233
+ console.log(
234
+ colors.green(` ${allowCount} allow`) + colors.dim(' · ') +
235
+ colors.yellow(`${askCount} ask`) + colors.dim(' · ') +
236
+ colors.red(`${denyCount} deny`)
237
+ )
238
+ if (loadedSkills.length) {
239
+ console.log(colors.dim(` Skills: ${loadedSkills.join(', ')}`))
240
+ }
241
+ console.log()
242
+ console.log(colors.dim(' Commands: .exit .perms .skills .allow <tool> .deny <tool> .gate <tool> .allow-all /console'))
243
+ console.log()
244
+
245
+ // ── Main loop ──────────────────────────────────────────────────────────
246
+ while (true) {
247
+ const input = await prompt()
248
+ if (!input) continue
249
+
250
+ // Meta commands
251
+ if (input === '.exit') break
252
+
253
+ if (input === '.perms') {
254
+ const perms = coder.permissions
255
+ const def = coder.state.get('defaultPermission')
256
+ console.log()
257
+ console.log(colors.dim(` Default: ${def}`))
258
+ for (const [name, level] of Object.entries(perms).sort()) {
259
+ const color = level === 'allow' ? colors.green : level === 'deny' ? colors.red : colors.yellow
260
+ console.log(` ${color(level.padEnd(5))} ${name}`)
261
+ }
262
+ console.log()
263
+ continue
264
+ }
265
+
266
+ if (input === '.skills') {
267
+ const skillsLib = container.feature('skillsLibrary')
268
+ if (!skillsLib.isStarted) await skillsLib.start()
269
+ const available = skillsLib.list()
270
+ const loaded = coder.state.get('loadedSkills') as string[]
271
+ console.log()
272
+ if (available.length === 0) {
273
+ console.log(colors.dim(' No skills found'))
274
+ } else {
275
+ for (const skill of available) {
276
+ const isLoaded = loaded.includes(skill.name)
277
+ const marker = isLoaded ? colors.green('●') : colors.dim('○')
278
+ console.log(` ${marker} ${colors.bold(skill.name)} ${colors.dim('—')} ${colors.dim(skill.description || '')}`)
279
+ }
280
+ console.log()
281
+ console.log(colors.dim(` ${loaded.length} loaded into context, ${available.length} total available`))
282
+ }
283
+ console.log()
284
+ continue
285
+ }
286
+
287
+ if (input.startsWith('.allow-all')) {
288
+ for (const t of Object.keys(coder.tools)) {
289
+ coder.permitTool(t)
290
+ }
291
+ console.log(colors.green(' All tools set to allow'))
292
+ continue
293
+ }
294
+
295
+ if (input.startsWith('.allow ')) {
296
+ const tool = input.slice(7).trim()
297
+ coder.permitTool(tool)
298
+ console.log(colors.green(` ${tool} → allow`))
299
+ continue
300
+ }
301
+
302
+ if (input.startsWith('.deny ')) {
303
+ const tool = input.slice(6).trim()
304
+ coder.blockTool(tool)
305
+ console.log(colors.red(` ${tool} → deny`))
306
+ continue
307
+ }
308
+
309
+ if (input.startsWith('.gate ')) {
310
+ const tool = input.slice(6).trim()
311
+ coder.gateTool(tool)
312
+ console.log(colors.yellow(` ${tool} → ask`))
313
+ continue
314
+ }
315
+
316
+ if (input === '/console') {
317
+ // Pause readline so the REPL can own stdin
318
+ rl.close()
319
+
320
+ const featureContext: Record<string, any> = {}
321
+ for (const fname of container.features.available) {
322
+ try { featureContext[fname] = container.feature(fname) } catch {}
323
+ }
324
+
325
+ const replPrompt = ui.colors.magenta('console') + ui.colors.dim(' > ')
326
+ const repl = container.feature('repl', { prompt: replPrompt })
327
+
328
+ console.log()
329
+ console.log(colors.dim(' Dropping into console. The coder is available as `coder`.'))
330
+ console.log(colors.dim(' Type .exit to return to code.'))
331
+ console.log()
332
+
333
+ await repl.start({
334
+ context: {
335
+ ...featureContext,
336
+ coder,
337
+ console,
338
+ setTimeout, setInterval, clearTimeout, clearInterval,
339
+ fetch,
340
+ },
341
+ })
342
+
343
+ // Wait for the REPL to close
344
+ await new Promise<void>((resolve) => {
345
+ repl._rl!.on('close', resolve)
346
+ })
347
+
348
+ // Resume readline
349
+ console.log()
350
+ console.log(colors.dim(' Back in code mode.'))
351
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout })
352
+ rlClosed = false
353
+ rl.on('close', () => { rlClosed = true })
354
+ continue
355
+ }
356
+
357
+ // Ask the coder
358
+ await coder.ask(input)
359
+ }
360
+
361
+ rl.close()
362
+ console.log()
363
+ }
364
+
365
+ commands.registerHandler('code', {
366
+ description: 'Autonomous coding assistant with file tools, bash, and permission gating',
367
+ argsSchema,
368
+ handler: code,
369
+ })
@@ -163,7 +163,12 @@ export default async function describe(options: z.infer<typeof argsSchema>, cont
163
163
 
164
164
  if (wantsTypeScript) {
165
165
  const output = renderResultAsTypeScript(result, targets, describer, sections)
166
- console.log(output)
166
+ if (options.pretty) {
167
+ const ui = container.feature('ui')
168
+ console.log(ui.markdown('```ts\n' + output + '\n```'))
169
+ } else {
170
+ console.log(output)
171
+ }
167
172
  } else if (options.json) {
168
173
  console.log(JSON.stringify(result.json, null, 2))
169
174
  } else if (options.pretty) {
@@ -192,7 +197,7 @@ function renderResultAsTypeScript(result: { json: any; text: string }, targets:
192
197
  // Container introspection (has className, registries, factories)
193
198
  if (json && json.className && json.registries) {
194
199
  const container = (describer as any).container
195
- return container.inspectAsType()
200
+ return container.introspectAsType()
196
201
  }
197
202
 
198
203
  // Array of results (e.g. from registry describe or multiple targets)
@@ -16,3 +16,4 @@ import './introspect.js'
16
16
  import './save-api-docs.js'
17
17
  import './bootstrap.js'
18
18
  import './select.js'
19
+ import './code.js'
@@ -140,7 +140,7 @@ export default async function mcpSandbox(options: z.infer<typeof argsSchema>, co
140
140
  ' container.commands.available — list available command names',
141
141
  ' container.features.describe(n) — get docs for a feature by name',
142
142
  ' container.clients.describe(n) — get docs for a client by name',
143
- ' container.inspectAsText() — full container introspection',
143
+ ' container.introspectAsText() — full container introspection',
144
144
  ' fs.readFile(path) — read a file',
145
145
  ' fs.readdir(dir) — list directory contents',
146
146
  ' proc.exec(cmd) — run a shell command',
@@ -208,7 +208,7 @@ export default async function mcpSandbox(options: z.infer<typeof argsSchema>, co
208
208
  .describe('Optional section to filter to. Omit for full overview.'),
209
209
  }),
210
210
  handler: (args) => {
211
- return container.inspectAsText(args.section)
211
+ return container.introspectAsText(args.section)
212
212
  },
213
213
  })
214
214
 
@@ -340,9 +340,9 @@ export default async function mcpSandbox(options: z.infer<typeof argsSchema>, co
340
340
  'container.features.describe("fs") // Docs for the fs feature',
341
341
  'container.features.describe("vm") // Docs for the vm feature',
342
342
  'container.clients.describe("rest") // Docs for the rest client',
343
- 'container.inspectAsText() // Full container introspection',
344
- 'container.inspectAsText("methods") // Just the methods section',
345
- 'container.inspectAsText("state") // Just the state section',
343
+ 'container.introspectAsText() // Full container introspection',
344
+ 'container.introspectAsText("methods") // Just the methods section',
345
+ 'container.introspectAsText("state") // Just the state section',
346
346
  '```',
347
347
  '',
348
348
  '### Using features directly',
@@ -375,7 +375,7 @@ export default async function mcpSandbox(options: z.infer<typeof argsSchema>, co
375
375
  mcpServer.prompt('introspect', {
376
376
  description: 'Get full introspection of the Luca container — all registries, state, methods, events, and environment info.',
377
377
  handler: async () => {
378
- const text = container.inspectAsText()
378
+ const text = container.introspectAsText()
379
379
  return [{
380
380
  role: 'user' as const,
381
381
  content: `Here is the full container introspection:\n\n${text}`,
@@ -388,7 +388,7 @@ export default async function mcpSandbox(options: z.infer<typeof argsSchema>, co
388
388
  name: 'Container Info',
389
389
  description: 'Full introspection of the running Luca container',
390
390
  mimeType: 'text/markdown',
391
- handler: () => container.inspectAsText(),
391
+ handler: () => container.introspectAsText(),
392
392
  })
393
393
 
394
394
  // --- Resource: feature list ---
@@ -24,7 +24,7 @@ export async function apiDocs(options: z.infer<typeof argsSchema>, context: Cont
24
24
 
25
25
  const mkPath = (...args) => container.paths.resolve(outputFolder, ...args)
26
26
 
27
- const result = await container.fs.writeFileAsync(mkPath('agi-container.md'), container.inspectAsText())
27
+ const result = await container.fs.writeFileAsync(mkPath('agi-container.md'), container.introspectAsText())
28
28
 
29
29
  for(let reg of ['features','clients','servers']) {
30
30
  const helperIds = container[reg].available
@@ -531,11 +531,11 @@ export class ContainerDescriber {
531
531
  const container = this.container
532
532
 
533
533
  if (sections.length === 0) {
534
- const data = container.inspect()
535
- return { json: data, text: container.inspectAsText(undefined, headingDepth) }
534
+ const data = container.introspect()
535
+ return { json: data, text: container.introspectAsText(undefined, headingDepth) }
536
536
  }
537
537
 
538
- const data = container.inspect()
538
+ const data = container.introspect()
539
539
  const introspectionSections = sections.filter((s): s is IntrospectionSection => s !== 'description')
540
540
  const textParts: string[] = []
541
541
  const jsonResult: Record<string, any> = {}
@@ -552,7 +552,7 @@ export class ContainerDescriber {
552
552
  }
553
553
 
554
554
  for (const section of introspectionSections) {
555
- textParts.push(container.inspectAsText(section, headingDepth))
555
+ textParts.push(container.introspectAsText(section, headingDepth))
556
556
  jsonResult[section] = data[section]
557
557
  }
558
558