@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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { Feature } from '@soederpop/luca/feature'
|
|
4
|
+
import type { FS } from '../../node/features/fs.js'
|
|
5
|
+
import type { Grep, GrepMatch } from '../../node/features/grep.js'
|
|
6
|
+
import type { Helper } from '../../helper.js'
|
|
7
|
+
|
|
8
|
+
declare module '@soederpop/luca/feature' {
|
|
9
|
+
interface AvailableFeatures {
|
|
10
|
+
fileTools: typeof FileTools
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const FileToolsStateSchema = FeatureStateSchema.extend({})
|
|
15
|
+
export const FileToolsOptionsSchema = FeatureOptionsSchema.extend({})
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Curated file-system and code-search tools for AI assistants.
|
|
19
|
+
*
|
|
20
|
+
* Wraps the container's `fs` and `grep` features into a focused tool surface
|
|
21
|
+
* modeled on the tools that coding assistants (Claude Code, Cursor, etc.) rely on:
|
|
22
|
+
* read, write, edit, list, search, find, stat, mkdir, move, copy, delete.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const fileTools = container.feature('fileTools')
|
|
27
|
+
* assistant.use(fileTools)
|
|
28
|
+
* // or selectively:
|
|
29
|
+
* assistant.use(fileTools.toTools({ only: ['readFile', 'searchFiles', 'listDirectory'] }))
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @extends Feature
|
|
33
|
+
*/
|
|
34
|
+
export class FileTools extends Feature {
|
|
35
|
+
static override shortcut = 'features.fileTools' as const
|
|
36
|
+
static override stateSchema = FileToolsStateSchema
|
|
37
|
+
static override optionsSchema = FileToolsOptionsSchema
|
|
38
|
+
|
|
39
|
+
static { Feature.register(this, 'fileTools') }
|
|
40
|
+
|
|
41
|
+
static tools: Record<string, { schema: z.ZodType; description?: string }> = {
|
|
42
|
+
readFile: {
|
|
43
|
+
description: 'Read the contents of a file. Returns the text content. Use offset/limit to read portions of large files.',
|
|
44
|
+
schema: z.object({
|
|
45
|
+
path: z.string().describe('File path relative to the project root'),
|
|
46
|
+
offset: z.number().optional().describe('Line number to start reading from (1-based)'),
|
|
47
|
+
limit: z.number().optional().describe('Maximum number of lines to read'),
|
|
48
|
+
}).describe('Read the contents of a file. Returns the text content. Use offset/limit to read portions of large files.'),
|
|
49
|
+
},
|
|
50
|
+
writeFile: {
|
|
51
|
+
description: 'Create a new file or overwrite an existing file with the given content. Prefer editFile for modifying existing files.',
|
|
52
|
+
schema: z.object({
|
|
53
|
+
path: z.string().describe('File path relative to the project root'),
|
|
54
|
+
content: z.string().describe('The full content to write'),
|
|
55
|
+
}).describe('Create a new file or overwrite an existing file with the given content. Prefer editFile for modifying existing files.'),
|
|
56
|
+
},
|
|
57
|
+
editFile: {
|
|
58
|
+
description: 'Make a surgical edit to a file by replacing an exact string match. The oldString must appear exactly once in the file (unless replaceAll is true). This is the preferred way to modify existing files.',
|
|
59
|
+
schema: z.object({
|
|
60
|
+
path: z.string().describe('File path relative to the project root'),
|
|
61
|
+
oldString: z.string().describe('The exact text to find and replace'),
|
|
62
|
+
newString: z.string().describe('The replacement text'),
|
|
63
|
+
replaceAll: z.boolean().optional().describe('Replace all occurrences instead of requiring uniqueness (default: false)'),
|
|
64
|
+
}).describe('Make a surgical edit to a file by replacing an exact string match. The oldString must appear exactly once in the file (unless replaceAll is true).'),
|
|
65
|
+
},
|
|
66
|
+
listDirectory: {
|
|
67
|
+
description: 'List files and directories at a path. Returns arrays of file and directory names.',
|
|
68
|
+
schema: z.object({
|
|
69
|
+
path: z.string().optional().describe('Directory path relative to project root (defaults to ".")'),
|
|
70
|
+
recursive: z.boolean().optional().describe('Whether to list recursively (default: false)'),
|
|
71
|
+
include: z.string().optional().describe('Glob pattern to filter results (e.g. "*.ts")'),
|
|
72
|
+
exclude: z.string().optional().describe('Glob pattern to exclude (e.g. "node_modules")'),
|
|
73
|
+
}).describe('List files and directories at a path. Returns arrays of file and directory names.'),
|
|
74
|
+
},
|
|
75
|
+
searchFiles: {
|
|
76
|
+
description: 'Search file contents for a pattern using ripgrep. Returns structured matches with file, line number, and content.',
|
|
77
|
+
schema: z.object({
|
|
78
|
+
pattern: z.string().describe('Search pattern (regex supported)'),
|
|
79
|
+
path: z.string().optional().describe('Directory to search in (defaults to project root)'),
|
|
80
|
+
include: z.string().optional().describe('Glob pattern to filter files (e.g. "*.ts")'),
|
|
81
|
+
exclude: z.string().optional().describe('Glob pattern to exclude (e.g. "node_modules")'),
|
|
82
|
+
ignoreCase: z.boolean().optional().describe('Case insensitive search'),
|
|
83
|
+
maxResults: z.number().optional().describe('Maximum number of results to return'),
|
|
84
|
+
}).describe('Search file contents for a pattern using ripgrep. Returns structured matches with file, line number, and content.'),
|
|
85
|
+
},
|
|
86
|
+
findFiles: {
|
|
87
|
+
description: 'Find files by name/glob pattern. Returns matching file paths.',
|
|
88
|
+
schema: z.object({
|
|
89
|
+
pattern: z.string().describe('Glob pattern to match (e.g. "**/*.test.ts", "src/**/*.tsx")'),
|
|
90
|
+
path: z.string().optional().describe('Directory to search from (defaults to project root)'),
|
|
91
|
+
exclude: z.string().optional().describe('Glob pattern to exclude'),
|
|
92
|
+
}).describe('Find files by name/glob pattern. Returns matching file paths.'),
|
|
93
|
+
},
|
|
94
|
+
fileInfo: {
|
|
95
|
+
description: 'Get information about a file or directory: whether it exists, its type (file/directory), size, and modification time.',
|
|
96
|
+
schema: z.object({
|
|
97
|
+
path: z.string().describe('File path relative to the project root'),
|
|
98
|
+
}).describe('Get information about a file or directory: whether it exists, its type, size, and modification time.'),
|
|
99
|
+
},
|
|
100
|
+
createDirectory: {
|
|
101
|
+
description: 'Create a directory and all parent directories if they do not exist.',
|
|
102
|
+
schema: z.object({
|
|
103
|
+
path: z.string().describe('Directory path relative to the project root'),
|
|
104
|
+
}).describe('Create a directory and all parent directories if they do not exist.'),
|
|
105
|
+
},
|
|
106
|
+
moveFile: {
|
|
107
|
+
description: 'Move or rename a file or directory.',
|
|
108
|
+
schema: z.object({
|
|
109
|
+
source: z.string().describe('Source path relative to the project root'),
|
|
110
|
+
destination: z.string().describe('Destination path relative to the project root'),
|
|
111
|
+
}).describe('Move or rename a file or directory.'),
|
|
112
|
+
},
|
|
113
|
+
copyFile: {
|
|
114
|
+
description: 'Copy a file or directory (recursive for directories).',
|
|
115
|
+
schema: z.object({
|
|
116
|
+
source: z.string().describe('Source path relative to the project root'),
|
|
117
|
+
destination: z.string().describe('Destination path relative to the project root'),
|
|
118
|
+
}).describe('Copy a file or directory (recursive for directories).'),
|
|
119
|
+
},
|
|
120
|
+
deleteFile: {
|
|
121
|
+
description: 'Delete a file. Does not delete directories — use with care.',
|
|
122
|
+
schema: z.object({
|
|
123
|
+
path: z.string().describe('File path relative to the project root'),
|
|
124
|
+
}).describe('Delete a file. Does not delete directories — use with care.'),
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private get fs(): FS {
|
|
129
|
+
return this.container.feature('fs') as unknown as FS
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private get grep(): Grep {
|
|
133
|
+
return this.container.feature('grep') as unknown as Grep
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// -------------------------------------------------------------------------
|
|
137
|
+
// Tool implementations — each matches a static tools key by name
|
|
138
|
+
// -------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
async readFile(args: { path: string; offset?: number; limit?: number }): Promise<string> {
|
|
141
|
+
const content = await this.fs.readFileAsync(args.path) as string
|
|
142
|
+
|
|
143
|
+
if (args.offset || args.limit) {
|
|
144
|
+
const lines = content.split('\n')
|
|
145
|
+
const start = Math.max(0, (args.offset || 1) - 1)
|
|
146
|
+
const end = args.limit ? start + args.limit : lines.length
|
|
147
|
+
return lines.slice(start, end).map((line, i) => `${start + i + 1}\t${line}`).join('\n')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return content
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async writeFile(args: { path: string; content: string }): Promise<string> {
|
|
154
|
+
await this.fs.ensureFolderAsync(args.path.includes('/') ? args.path.split('/').slice(0, -1).join('/') : '.')
|
|
155
|
+
await this.fs.writeFileAsync(args.path, args.content)
|
|
156
|
+
return `Wrote ${args.content.length} bytes to ${args.path}`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async editFile(args: { path: string; oldString: string; newString: string; replaceAll?: boolean }): Promise<string> {
|
|
160
|
+
const content = await this.fs.readFileAsync(args.path) as string
|
|
161
|
+
|
|
162
|
+
if (args.replaceAll) {
|
|
163
|
+
const updated = content.split(args.oldString).join(args.newString)
|
|
164
|
+
const count = (content.split(args.oldString).length - 1)
|
|
165
|
+
if (count === 0) return `Error: "${args.oldString}" not found in ${args.path}`
|
|
166
|
+
await this.fs.writeFileAsync(args.path, updated)
|
|
167
|
+
return `Replaced ${count} occurrence(s) in ${args.path}`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const idx = content.indexOf(args.oldString)
|
|
171
|
+
if (idx === -1) return `Error: "${args.oldString}" not found in ${args.path}`
|
|
172
|
+
|
|
173
|
+
const lastIdx = content.lastIndexOf(args.oldString)
|
|
174
|
+
if (idx !== lastIdx) {
|
|
175
|
+
const count = content.split(args.oldString).length - 1
|
|
176
|
+
return `Error: "${args.oldString}" appears ${count} times in ${args.path}. Use replaceAll or provide a more specific string.`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const updated = content.slice(0, idx) + args.newString + content.slice(idx + args.oldString.length)
|
|
180
|
+
await this.fs.writeFileAsync(args.path, updated)
|
|
181
|
+
return `Edited ${args.path}`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async listDirectory(args: { path?: string; recursive?: boolean; include?: string; exclude?: string }): Promise<string> {
|
|
185
|
+
const dir = args.path || '.'
|
|
186
|
+
const result = await this.fs.walkAsync(dir, {
|
|
187
|
+
files: true,
|
|
188
|
+
directories: true,
|
|
189
|
+
relative: true,
|
|
190
|
+
include: args.include ? [args.include] : undefined,
|
|
191
|
+
exclude: args.exclude ? [args.exclude] : ['node_modules', '.git'],
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// For non-recursive, filter to top-level only
|
|
195
|
+
if (!args.recursive) {
|
|
196
|
+
result.files = result.files.filter(f => !f.includes('/'))
|
|
197
|
+
result.directories = result.directories.filter(d => !d.includes('/'))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return JSON.stringify({ files: result.files, directories: result.directories })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async searchFiles(args: { pattern: string; path?: string; include?: string; exclude?: string; ignoreCase?: boolean; maxResults?: number }): Promise<string> {
|
|
204
|
+
const results: GrepMatch[] = await this.grep.search({
|
|
205
|
+
pattern: args.pattern,
|
|
206
|
+
path: args.path,
|
|
207
|
+
include: args.include,
|
|
208
|
+
exclude: args.exclude || 'node_modules',
|
|
209
|
+
ignoreCase: args.ignoreCase,
|
|
210
|
+
maxResults: args.maxResults || 50,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
return JSON.stringify(results.map(r => ({
|
|
214
|
+
file: r.file,
|
|
215
|
+
line: r.line,
|
|
216
|
+
content: r.content,
|
|
217
|
+
})))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async findFiles(args: { pattern: string; path?: string; exclude?: string }): Promise<string> {
|
|
221
|
+
const dir = args.path || '.'
|
|
222
|
+
const result = await this.fs.walkAsync(dir, {
|
|
223
|
+
files: true,
|
|
224
|
+
directories: false,
|
|
225
|
+
relative: true,
|
|
226
|
+
include: [args.pattern],
|
|
227
|
+
exclude: args.exclude ? [args.exclude, 'node_modules', '.git'] : ['node_modules', '.git'],
|
|
228
|
+
})
|
|
229
|
+
return JSON.stringify(result.files)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async fileInfo(args: { path: string }): Promise<string> {
|
|
233
|
+
const exists = await this.fs.existsAsync(args.path)
|
|
234
|
+
if (!exists) return JSON.stringify({ exists: false })
|
|
235
|
+
|
|
236
|
+
const stat = await this.fs.statAsync(args.path)
|
|
237
|
+
return JSON.stringify({
|
|
238
|
+
exists: true,
|
|
239
|
+
isFile: stat.isFile(),
|
|
240
|
+
isDirectory: stat.isDirectory(),
|
|
241
|
+
size: stat.size,
|
|
242
|
+
modified: stat.mtime.toISOString(),
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async createDirectory(args: { path: string }): Promise<string> {
|
|
247
|
+
await this.fs.ensureFolderAsync(args.path)
|
|
248
|
+
return `Created ${args.path}`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async moveFile(args: { source: string; destination: string }): Promise<string> {
|
|
252
|
+
await this.fs.moveAsync(args.source, args.destination)
|
|
253
|
+
return `Moved ${args.source} → ${args.destination}`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async copyFile(args: { source: string; destination: string }): Promise<string> {
|
|
257
|
+
await this.fs.copyAsync(args.source, args.destination)
|
|
258
|
+
return `Copied ${args.source} → ${args.destination}`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async deleteFile(args: { path: string }): Promise<string> {
|
|
262
|
+
const isDir = await this.fs.isDirectoryAsync(args.path)
|
|
263
|
+
if (isDir) return `Error: "${args.path}" is a directory. Use deleteFile only for files.`
|
|
264
|
+
await this.fs.rm(args.path)
|
|
265
|
+
return `Deleted ${args.path}`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* When an assistant uses fileTools, inject system prompt guidance
|
|
270
|
+
* about how to use the tools effectively.
|
|
271
|
+
*/
|
|
272
|
+
override setupToolsConsumer(consumer: Helper) {
|
|
273
|
+
if (typeof (consumer as any).addSystemPromptExtension === 'function') {
|
|
274
|
+
(consumer as any).addSystemPromptExtension('fileTools', [
|
|
275
|
+
'## File Tools',
|
|
276
|
+
'- All file paths are relative to the project root unless they start with /',
|
|
277
|
+
'- Use `searchFiles` to understand code before modifying it',
|
|
278
|
+
'- Use `editFile` for surgical changes to existing files — prefer it over `writeFile`',
|
|
279
|
+
'- Use `listDirectory` to explore before assuming paths exist',
|
|
280
|
+
'- Use `readFile` with offset/limit for large files instead of reading the entire file',
|
|
281
|
+
].join('\n'))
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export default FileTools
|