@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.
- package/README.md +241 -36
- package/bun.lock +24 -5
- package/commands/build-python-bridge.ts +43 -0
- package/docs/apis/clients/rest.md +7 -7
- package/docs/apis/clients/websocket.md +23 -10
- package/docs/apis/features/agi/assistant.md +155 -8
- package/docs/apis/features/agi/assistants-manager.md +90 -22
- package/docs/apis/features/agi/auto-assistant.md +377 -0
- package/docs/apis/features/agi/browser-use.md +802 -0
- package/docs/apis/features/agi/claude-code.md +6 -1
- package/docs/apis/features/agi/conversation-history.md +7 -6
- package/docs/apis/features/agi/conversation.md +111 -38
- package/docs/apis/features/agi/docs-reader.md +35 -57
- package/docs/apis/features/agi/file-tools.md +163 -0
- package/docs/apis/features/agi/openapi.md +2 -2
- package/docs/apis/features/agi/skills-library.md +227 -0
- package/docs/apis/features/node/content-db.md +125 -4
- package/docs/apis/features/node/disk-cache.md +11 -11
- package/docs/apis/features/node/downloader.md +1 -1
- package/docs/apis/features/node/file-manager.md +15 -15
- package/docs/apis/features/node/fs.md +78 -21
- package/docs/apis/features/node/git.md +50 -10
- package/docs/apis/features/node/google-calendar.md +3 -0
- package/docs/apis/features/node/google-docs.md +10 -1
- package/docs/apis/features/node/google-drive.md +3 -0
- package/docs/apis/features/node/google-mail.md +214 -0
- package/docs/apis/features/node/google-sheets.md +3 -0
- package/docs/apis/features/node/ink.md +10 -10
- package/docs/apis/features/node/ipc-socket.md +83 -93
- package/docs/apis/features/node/networking.md +5 -5
- package/docs/apis/features/node/os.md +7 -7
- package/docs/apis/features/node/package-finder.md +14 -14
- package/docs/apis/features/node/proc.md +2 -1
- package/docs/apis/features/node/process-manager.md +70 -3
- package/docs/apis/features/node/python.md +265 -9
- package/docs/apis/features/node/redis.md +380 -0
- package/docs/apis/features/node/ui.md +13 -13
- package/docs/apis/servers/express.md +35 -7
- package/docs/apis/servers/mcp.md +3 -3
- package/docs/apis/servers/websocket.md +51 -8
- package/docs/bootstrap/CLAUDE.md +1 -1
- package/docs/bootstrap/SKILL.md +93 -7
- package/docs/examples/feature-as-tool-provider.md +143 -0
- package/docs/examples/python.md +42 -1
- package/docs/introspection.md +15 -5
- package/docs/tutorials/00-bootstrap.md +3 -3
- package/docs/tutorials/02-container.md +2 -2
- package/docs/tutorials/10-creating-features.md +5 -0
- package/docs/tutorials/13-introspection.md +12 -2
- package/docs/tutorials/19-python-sessions.md +401 -0
- package/package.json +8 -4
- package/src/agi/container.server.ts +8 -0
- package/src/agi/features/assistant.ts +19 -0
- package/src/agi/features/autonomous-assistant.ts +435 -0
- package/src/agi/features/conversation.ts +58 -6
- package/src/agi/features/file-tools.ts +286 -0
- package/src/agi/features/luca-coder.ts +643 -0
- package/src/bootstrap/generated.ts +705 -17
- package/src/cli/build-info.ts +2 -2
- package/src/cli/cli.ts +22 -13
- package/src/commands/bootstrap.ts +49 -6
- package/src/commands/code.ts +369 -0
- package/src/commands/describe.ts +7 -2
- package/src/commands/index.ts +1 -0
- package/src/commands/sandbox-mcp.ts +7 -7
- package/src/commands/save-api-docs.ts +1 -1
- package/src/container-describer.ts +4 -4
- package/src/container.ts +10 -19
- package/src/helper.ts +24 -33
- package/src/introspection/generated.agi.ts +2499 -63
- package/src/introspection/generated.node.ts +1625 -688
- package/src/introspection/generated.web.ts +15 -57
- package/src/node/container.ts +5 -0
- package/src/node/features/figlet-fonts.ts +597 -0
- package/src/node/features/fs.ts +3 -9
- package/src/node/features/helpers.ts +20 -0
- package/src/node/features/python.ts +429 -16
- package/src/node/features/redis.ts +446 -0
- package/src/node/features/ui.ts +4 -11
- package/src/python/bridge.py +220 -0
- package/src/python/generated.ts +227 -0
- package/src/scaffolds/generated.ts +1 -1
- package/test/python-session.test.ts +105 -0
- package/assistants/lucaExpert/CORE.md +0 -37
- package/assistants/lucaExpert/hooks.ts +0 -9
- package/assistants/lucaExpert/tools.ts +0 -177
package/src/cli/build-info.ts
CHANGED
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
|
-
|
|
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
|
|
55
|
-
done = t('
|
|
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
|
|
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
|
-
|
|
185
|
-
const dir = join(homedir(), '.luca', 'commands')
|
|
189
|
+
const DISCOVERABLE_TYPES = ['features', 'clients', 'servers', 'commands', 'selectors'] as const
|
|
186
190
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
+
})
|
package/src/commands/describe.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
200
|
+
return container.introspectAsType()
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
// Array of results (e.g. from registry describe or multiple targets)
|
package/src/commands/index.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
344
|
-
'container.
|
|
345
|
-
'container.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
535
|
-
return { json: data, text: container.
|
|
534
|
+
const data = container.introspect()
|
|
535
|
+
return { json: data, text: container.introspectAsText(undefined, headingDepth) }
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
-
const data = container.
|
|
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.
|
|
555
|
+
textParts.push(container.introspectAsText(section, headingDepth))
|
|
556
556
|
jsonResult[section] = data[section]
|
|
557
557
|
}
|
|
558
558
|
|