@soederpop/luca 0.0.23 → 0.0.26
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/AGENTS.md +1 -1
- package/CLAUDE.md +6 -1
- package/assistants/codingAssistant/hooks.ts +0 -1
- package/assistants/lucaExpert/CORE.md +37 -0
- package/assistants/lucaExpert/hooks.ts +9 -0
- package/assistants/lucaExpert/tools.ts +177 -0
- package/commands/build-bootstrap.ts +41 -1
- package/docs/TABLE-OF-CONTENTS.md +0 -1
- package/docs/apis/clients/rest.md +5 -5
- package/docs/apis/features/agi/assistant.md +1 -1
- package/docs/apis/features/agi/conversation-history.md +6 -7
- package/docs/apis/features/agi/conversation.md +1 -1
- package/docs/apis/features/agi/semantic-search.md +1 -1
- package/docs/bootstrap/CLAUDE.md +1 -1
- package/docs/bootstrap/SKILL.md +7 -3
- package/docs/bootstrap/templates/luca-cli.ts +5 -0
- package/docs/mcp/readme.md +1 -1
- package/docs/tutorials/00-bootstrap.md +18 -0
- package/package.json +2 -2
- package/scripts/stamp-build.sh +12 -0
- package/scripts/test-docs-reader.ts +10 -0
- package/src/agi/container.server.ts +8 -5
- package/src/agi/features/assistant.ts +210 -55
- package/src/agi/features/assistants-manager.ts +138 -66
- package/src/agi/features/conversation.ts +46 -14
- package/src/agi/features/docs-reader.ts +166 -0
- package/src/agi/features/openapi.ts +1 -1
- package/src/agi/features/skills-library.ts +257 -313
- package/src/bootstrap/generated.ts +8163 -6
- package/src/cli/build-info.ts +4 -0
- package/src/cli/cli.ts +2 -1
- package/src/command.ts +75 -0
- package/src/commands/bootstrap.ts +16 -1
- package/src/commands/describe.ts +29 -1089
- package/src/commands/eval.ts +6 -1
- package/src/commands/sandbox-mcp.ts +17 -7
- package/src/container-describer.ts +1098 -0
- package/src/container.ts +11 -0
- package/src/helper.ts +56 -2
- package/src/introspection/generated.agi.ts +1684 -799
- package/src/introspection/generated.node.ts +964 -572
- package/src/introspection/generated.web.ts +9 -1
- package/src/node/container.ts +1 -1
- package/src/node/features/content-db.ts +268 -13
- package/src/node/features/fs.ts +18 -0
- package/src/node/features/git.ts +90 -0
- package/src/node/features/grep.ts +1 -1
- package/src/node/features/proc.ts +1 -0
- package/src/node/features/tts.ts +1 -1
- package/src/node/features/vm.ts +48 -0
- package/src/scaffolds/generated.ts +2 -2
- package/src/server.ts +40 -0
- package/src/servers/express.ts +2 -0
- package/src/servers/mcp.ts +1 -0
- package/src/servers/socket.ts +2 -0
- package/assistants/architect/CORE.md +0 -3
- package/assistants/architect/hooks.ts +0 -3
- package/assistants/architect/tools.ts +0 -10
- package/docs/apis/features/agi/skills-library.md +0 -234
- package/docs/reports/assistant-bugs.md +0 -38
- package/docs/reports/attach-pattern-usage.md +0 -18
- package/docs/reports/code-audit-results.md +0 -391
- package/docs/reports/console-hmr-design.md +0 -170
- package/docs/reports/helper-semantic-search.md +0 -72
- package/docs/reports/introspection-audit-tasks.md +0 -378
- package/docs/reports/luca-mcp-improvements.md +0 -128
- package/test-integration/skills-library.test.ts +0 -157
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setBuildTimeData, setContainerBuildTimeData } from './index.js';
|
|
2
2
|
|
|
3
3
|
// Auto-generated introspection registry data
|
|
4
|
-
// Generated at: 2026-03-
|
|
4
|
+
// Generated at: 2026-03-22T20:57:23.113Z
|
|
5
5
|
|
|
6
6
|
setBuildTimeData('features.containerLink', {
|
|
7
7
|
"id": "features.containerLink",
|
|
@@ -982,6 +982,10 @@ setContainerBuildTimeData('Container', {
|
|
|
982
982
|
"description": "Returns a map of enabled feature shortcut IDs to their instances.",
|
|
983
983
|
"returns": "Partial<AvailableInstanceTypes<Features>>"
|
|
984
984
|
},
|
|
985
|
+
"describer": {
|
|
986
|
+
"description": "Lazy-initialized ContainerDescriber for introspecting registries, helpers, and members.",
|
|
987
|
+
"returns": "ContainerDescriber"
|
|
988
|
+
},
|
|
985
989
|
"context": {
|
|
986
990
|
"description": "The Container's context is an object that contains the enabled features, the container itself, and any additional context that has been added to the container. All helper instances that are created by the container will have access to the shared context.",
|
|
987
991
|
"returns": "ContainerContext<Features> & Partial<AvailableInstanceTypes<AvailableFeatures>>"
|
|
@@ -2029,6 +2033,10 @@ export const containerIntrospectionData = [
|
|
|
2029
2033
|
"description": "Returns a map of enabled feature shortcut IDs to their instances.",
|
|
2030
2034
|
"returns": "Partial<AvailableInstanceTypes<Features>>"
|
|
2031
2035
|
},
|
|
2036
|
+
"describer": {
|
|
2037
|
+
"description": "Lazy-initialized ContainerDescriber for introspecting registries, helpers, and members.",
|
|
2038
|
+
"returns": "ContainerDescriber"
|
|
2039
|
+
},
|
|
2032
2040
|
"context": {
|
|
2033
2041
|
"description": "The Container's context is an object that contains the enabled features, the container itself, and any additional context that has been added to the container. All helper instances that are created by the container will have access to the shared context.",
|
|
2034
2042
|
"returns": "ContainerContext<Features> & Partial<AvailableInstanceTypes<AvailableFeatures>>"
|
package/src/node/container.ts
CHANGED
|
@@ -3,8 +3,8 @@ import * as contentbaseExports from 'contentbase'
|
|
|
3
3
|
import { parse, Collection, extractSections, type ModelDefinition } from 'contentbase'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { realpathSync } from 'node:fs'
|
|
7
|
+
import type { GrepOptions } from './grep.js'
|
|
8
8
|
|
|
9
9
|
export const ContentDbStateSchema = FeatureStateSchema.extend({
|
|
10
10
|
loaded: z.boolean().default(false).describe('Whether the content collection has been loaded and parsed'),
|
|
@@ -45,6 +45,71 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
45
45
|
static override eventsSchema = ContentDbEventsSchema
|
|
46
46
|
static { Feature.register(this, 'contentDb') }
|
|
47
47
|
|
|
48
|
+
/** Tools that any assistant can use to progressively explore this collection. */
|
|
49
|
+
static tools: Record<string, { schema: z.ZodType; handler?: Function }> = {
|
|
50
|
+
getCollectionOverview: {
|
|
51
|
+
schema: z.object({}).describe(
|
|
52
|
+
'Get a high-level overview of the document collection: models, document counts, directory tree, and search index status. Call this first to understand what is available.'
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
listDocuments: {
|
|
56
|
+
schema: z.object({
|
|
57
|
+
model: z.string().optional().describe('Filter to documents belonging to this model name'),
|
|
58
|
+
glob: z.string().optional().describe('Glob pattern to filter document path IDs (e.g. "guides/*", "apis/**")'),
|
|
59
|
+
}).describe(
|
|
60
|
+
'List available document IDs in the collection, optionally filtered by model or glob pattern.'
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
readDocument: {
|
|
64
|
+
schema: z.object({
|
|
65
|
+
id: z.string().describe('The document path ID to read (e.g. "guides/intro")'),
|
|
66
|
+
include: z.array(z.string()).optional().describe('Only return these section headings'),
|
|
67
|
+
exclude: z.array(z.string()).optional().describe('Remove these section headings from the output'),
|
|
68
|
+
meta: z.boolean().optional().describe('Include YAML frontmatter in the output'),
|
|
69
|
+
}).describe(
|
|
70
|
+
'Read a single document by its path ID. Use include/exclude to read only specific sections and avoid loading unnecessary content.'
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
readMultipleDocuments: {
|
|
74
|
+
schema: z.object({
|
|
75
|
+
ids: z.array(z.string()).describe('Array of document path IDs to read'),
|
|
76
|
+
include: z.array(z.string()).optional().describe('Only return these section headings from each document'),
|
|
77
|
+
exclude: z.array(z.string()).optional().describe('Remove these section headings from each document'),
|
|
78
|
+
meta: z.boolean().optional().describe('Include YAML frontmatter in the output'),
|
|
79
|
+
}).describe(
|
|
80
|
+
'Read multiple documents at once, concatenated with dividers. Use include/exclude to focus on relevant sections.'
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
queryDocuments: {
|
|
84
|
+
schema: z.object({
|
|
85
|
+
model: z.string().describe('The model name to query (e.g. "Plan", "Task")'),
|
|
86
|
+
where: z.string().optional().describe('Filter conditions as JSON string, e.g. \'{ "meta.status": "approved" }\' or \'{ "meta.priority": { "$gt": 3 } }\''),
|
|
87
|
+
sort: z.string().optional().describe('Sort specification as JSON string, e.g. \'{ "meta.priority": "desc" }\''),
|
|
88
|
+
limit: z.number().optional().describe('Maximum number of results to return'),
|
|
89
|
+
offset: z.number().optional().describe('Number of results to skip'),
|
|
90
|
+
select: z.array(z.string()).optional().describe('Fields to include in output (e.g. ["id", "title", "meta.status"])'),
|
|
91
|
+
}).describe(
|
|
92
|
+
'Query documents by model with MongoDB-style filtering, sorting, and pagination. Returns serialized model instances.'
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
searchContent: {
|
|
96
|
+
schema: z.object({
|
|
97
|
+
pattern: z.string().describe('Regex pattern to search for across all documents'),
|
|
98
|
+
caseSensitive: z.boolean().optional().describe('Whether the search is case-sensitive (default: false)'),
|
|
99
|
+
}).describe(
|
|
100
|
+
'Text/regex search (grep) across all documents in the collection. Returns matching lines with file context.'
|
|
101
|
+
),
|
|
102
|
+
},
|
|
103
|
+
semanticSearch: {
|
|
104
|
+
schema: z.object({
|
|
105
|
+
query: z.string().describe('Natural language search query'),
|
|
106
|
+
limit: z.number().optional().describe('Maximum number of results (default: 10)'),
|
|
107
|
+
}).describe(
|
|
108
|
+
'Semantic search across documents using keyword + vector similarity. Falls back to text search if no search index exists.'
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
48
113
|
override get initialState(): ContentDbState {
|
|
49
114
|
return {
|
|
50
115
|
...super.initialState,
|
|
@@ -54,7 +119,7 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
54
119
|
|
|
55
120
|
/** Whether the content database has been loaded. */
|
|
56
121
|
get isLoaded() {
|
|
57
|
-
return this.state.get('
|
|
122
|
+
return this.state.get('started')
|
|
58
123
|
}
|
|
59
124
|
|
|
60
125
|
_collection?: Collection
|
|
@@ -82,7 +147,7 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
82
147
|
/** Check if contentbase is resolvable via native import from the project root */
|
|
83
148
|
private _canNativeImportContentbase(): boolean {
|
|
84
149
|
const cwd = this.container.cwd
|
|
85
|
-
return
|
|
150
|
+
return this.container.fs.exists(this.container.paths.resolve(cwd, 'node_modules', 'contentbase'))
|
|
86
151
|
}
|
|
87
152
|
|
|
88
153
|
/** Seed the VM with virtual modules so models.ts can import from 'contentbase', 'zod', etc. */
|
|
@@ -122,6 +187,61 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
122
187
|
return this.collection.modelDefinitions.map((d) => d.name)
|
|
123
188
|
}
|
|
124
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Returns the available document ids in the collection
|
|
192
|
+
*/
|
|
193
|
+
get available() : string[] {
|
|
194
|
+
return this.collection.available
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Render a tree view of the collection directory structure.
|
|
199
|
+
* Built with container.fs so it works without the `tree` binary.
|
|
200
|
+
*/
|
|
201
|
+
renderTree(options?: { depth?: number; dirsOnly?: boolean }): string {
|
|
202
|
+
const maxDepth = options?.depth ?? Infinity
|
|
203
|
+
const dirsOnly = options?.dirsOnly ?? false
|
|
204
|
+
const fs = this.container.fs
|
|
205
|
+
const paths = this.container.paths
|
|
206
|
+
const root = this.collectionPath
|
|
207
|
+
const lines: string[] = [paths.basename(root)]
|
|
208
|
+
|
|
209
|
+
const walk = (dir: string, prefix: string, currentDepth: number) => {
|
|
210
|
+
if (currentDepth >= maxDepth) return
|
|
211
|
+
const entries: string[] = fs.readdirSync(dir).sort()
|
|
212
|
+
const filtered = entries.filter((e: string) => {
|
|
213
|
+
if (e.startsWith('.')) return false
|
|
214
|
+
if (dirsOnly) return fs.isDirectory(paths.resolve(dir, e))
|
|
215
|
+
return true
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
filtered.forEach((entry: string, i: number) => {
|
|
219
|
+
const isLast = i === filtered.length - 1
|
|
220
|
+
const connector = isLast ? '└── ' : '├── '
|
|
221
|
+
const fullPath = paths.resolve(dir, entry)
|
|
222
|
+
lines.push(`${prefix}${connector}${entry}`)
|
|
223
|
+
if (fs.isDirectory(fullPath)) {
|
|
224
|
+
walk(fullPath, prefix + (isLast ? ' ' : '│ '), currentDepth + 1)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
walk(root, '', 0)
|
|
230
|
+
return lines.join('\n')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async grep(options: string | GrepOptions) {
|
|
234
|
+
if (typeof options === 'string') {
|
|
235
|
+
options = { pattern: options }
|
|
236
|
+
}
|
|
237
|
+
return this.container.feature('grep').search({
|
|
238
|
+
path: this.collectionPath,
|
|
239
|
+
include: ['**/*.md'],
|
|
240
|
+
exclude: ['.env', 'secrets'],
|
|
241
|
+
...options,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
125
245
|
/**
|
|
126
246
|
* Query documents belonging to a specific model definition.
|
|
127
247
|
*
|
|
@@ -170,7 +290,7 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
170
290
|
}
|
|
171
291
|
|
|
172
292
|
await this.collection.load()
|
|
173
|
-
this.state.set('
|
|
293
|
+
this.state.set('started', true)
|
|
174
294
|
|
|
175
295
|
return this
|
|
176
296
|
}
|
|
@@ -308,6 +428,23 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
308
428
|
async generateModelSummary(options: any) {
|
|
309
429
|
return this.collection.generateModelSummary(options)
|
|
310
430
|
}
|
|
431
|
+
|
|
432
|
+
get modelDefinitionTable() {
|
|
433
|
+
return Object.fromEntries(this.collection.modelDefinitions.map(d => {
|
|
434
|
+
|
|
435
|
+
const prefixPattern = this.container.paths.relative(this.collection.resolve(d.prefix))
|
|
436
|
+
|
|
437
|
+
return [d.name, {
|
|
438
|
+
description: d.name === 'Base' ? 'Any markdown document not matched to a model' : d.description,
|
|
439
|
+
glob: `${prefixPattern}/**/*.md`,
|
|
440
|
+
routePatterns: Array(d.pattern).flatMap(p => p).filter(Boolean).map(p => `${prefixPattern}/${p}`)
|
|
441
|
+
}]
|
|
442
|
+
}))
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
get fileTree() {
|
|
446
|
+
return this.collection.renderFileTree()
|
|
447
|
+
}
|
|
311
448
|
|
|
312
449
|
// ── Search Integration ─────────────────────────────────────────────
|
|
313
450
|
|
|
@@ -315,7 +452,7 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
315
452
|
|
|
316
453
|
/**
|
|
317
454
|
* Lazily initialize the semanticSearch feature, attaching it to the container if needed.
|
|
318
|
-
* The dbPath defaults to
|
|
455
|
+
* The dbPath defaults to `~/.luca/contentbase/{hash}/search.sqlite` where hash is derived from the resolved collection path.
|
|
319
456
|
*/
|
|
320
457
|
private async _getSemanticSearch(options?: { dbPath?: string; embeddingProvider?: string; embeddingModel?: string }) {
|
|
321
458
|
if (this._semanticSearch?.state?.get('dbReady')) return this._semanticSearch
|
|
@@ -326,9 +463,10 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
326
463
|
SemanticSearch.attach(this.container as any)
|
|
327
464
|
}
|
|
328
465
|
|
|
329
|
-
//
|
|
330
|
-
const
|
|
331
|
-
const
|
|
466
|
+
// Store search index in ~/.luca/contentbase/{hash}/ keyed by the real (symlink-resolved) collection path
|
|
467
|
+
const realPath = realpathSync(this.collectionPath)
|
|
468
|
+
const pathHash = this.container.utils.hashObject(realPath).slice(0, 12)
|
|
469
|
+
const dbPath = options?.dbPath ?? this.container.paths.resolve(this.container.os.homedir, '.luca', 'contentbase', pathHash, 'search.sqlite')
|
|
332
470
|
this._semanticSearch = (this.container as any).feature('semanticSearch', {
|
|
333
471
|
dbPath,
|
|
334
472
|
...(options?.embeddingProvider ? { embeddingProvider: options.embeddingProvider } : {}),
|
|
@@ -343,10 +481,14 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
343
481
|
* Check if a search index exists for this collection.
|
|
344
482
|
*/
|
|
345
483
|
private _hasSearchIndex(): boolean {
|
|
346
|
-
const
|
|
347
|
-
|
|
484
|
+
const realPath = realpathSync(this.collectionPath)
|
|
485
|
+
const pathHash = this.container.utils.hashObject(realPath).slice(0, 12)
|
|
486
|
+
const dbDir = this.container.paths.resolve(this.container.os.homedir, '.luca', 'contentbase', pathHash)
|
|
487
|
+
|
|
488
|
+
if (!this.container.fs.exists(dbDir)) return false
|
|
489
|
+
|
|
348
490
|
try {
|
|
349
|
-
const files = readdirSync(dbDir)
|
|
491
|
+
const files = this.container.fs.readdirSync(dbDir)
|
|
350
492
|
return files.some((f: string) => f.startsWith('search.') && f.endsWith('.sqlite'))
|
|
351
493
|
} catch {
|
|
352
494
|
return false
|
|
@@ -525,6 +667,119 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
525
667
|
}
|
|
526
668
|
return Object.fromEntries(queryChains)
|
|
527
669
|
}
|
|
670
|
+
// ── Tool Methods ─────────────────────────────────────────────────
|
|
671
|
+
// These methods are auto-bound as tool handlers by toTools() because
|
|
672
|
+
// their names match the keys in static tools above.
|
|
673
|
+
|
|
674
|
+
/** Returns a high-level overview of the collection. */
|
|
675
|
+
async getCollectionOverview() {
|
|
676
|
+
if (!this.isLoaded) await this.load()
|
|
677
|
+
|
|
678
|
+
const modelCounts: Record<string, number> = {}
|
|
679
|
+
for (const def of this.collection.modelDefinitions) {
|
|
680
|
+
const count = await this.collection.query(def).count()
|
|
681
|
+
modelCounts[def.name] = count
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
rootPath: this.collectionPath,
|
|
686
|
+
totalDocuments: this.available.length,
|
|
687
|
+
models: modelCounts,
|
|
688
|
+
tree: this.renderTree({ depth: 2 }),
|
|
689
|
+
hasSearchIndex: this._hasSearchIndex(),
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** List document IDs, optionally filtered by model or glob. */
|
|
694
|
+
async listDocuments(args: { model?: string; glob?: string }) {
|
|
695
|
+
if (!this.isLoaded) await this.load()
|
|
696
|
+
|
|
697
|
+
let ids = this.available
|
|
698
|
+
|
|
699
|
+
if (args.model) {
|
|
700
|
+
const def = this.models[args.model]
|
|
701
|
+
if (!def) return { error: `Unknown model "${args.model}". Available: ${this.modelNames.join(', ')}` }
|
|
702
|
+
const instances = await this.collection.query(def).fetchAll()
|
|
703
|
+
ids = instances.map((inst: any) => inst.id)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (args.glob) {
|
|
707
|
+
const matched = this.collection.matchPaths(args.glob)
|
|
708
|
+
ids = ids.filter((id: string) => matched.includes(id))
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return ids
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Read a single document with optional section filtering. */
|
|
715
|
+
async readDocument(args: { id: string; include?: string[]; exclude?: string[]; meta?: boolean }) {
|
|
716
|
+
return this.read(args.id, args)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** Read multiple documents with optional section filtering. */
|
|
720
|
+
async readMultipleDocuments(args: { ids: string[]; include?: string[]; exclude?: string[]; meta?: boolean }) {
|
|
721
|
+
return this.readMultiple(args.ids, args)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** Query documents by model with filters, sort, limit. */
|
|
725
|
+
async queryDocuments(args: { model: string; where?: string; sort?: string; limit?: number; offset?: number; select?: string[] }) {
|
|
726
|
+
if (!this.isLoaded) await this.load()
|
|
727
|
+
|
|
728
|
+
const def = this.models[args.model]
|
|
729
|
+
if (!def) return { error: `Unknown model "${args.model}". Available: ${this.modelNames.join(', ')}` }
|
|
730
|
+
|
|
731
|
+
let q = this.collection.query(def)
|
|
732
|
+
|
|
733
|
+
if (args.where) {
|
|
734
|
+
const where: Record<string, any> = typeof args.where === 'string' ? JSON.parse(args.where) : args.where
|
|
735
|
+
for (const [path, value] of Object.entries(where)) {
|
|
736
|
+
q = q.where(path, value)
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (args.sort) {
|
|
740
|
+
const sort: Record<string, string> = typeof args.sort === 'string' ? JSON.parse(args.sort) : args.sort
|
|
741
|
+
for (const [path, dir] of Object.entries(sort)) {
|
|
742
|
+
q = q.sort(path, dir as 'asc' | 'desc')
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (args.limit) q = q.limit(args.limit)
|
|
746
|
+
if (args.offset) q = q.offset(args.offset)
|
|
747
|
+
|
|
748
|
+
const results = await q.fetchAll()
|
|
749
|
+
|
|
750
|
+
return results.map((inst: any) => {
|
|
751
|
+
const json = inst.toJSON()
|
|
752
|
+
if (args.select?.length) {
|
|
753
|
+
const picked: Record<string, any> = {}
|
|
754
|
+
for (const field of args.select) {
|
|
755
|
+
picked[field] = this.container.utils.lodash.get(json, field)
|
|
756
|
+
}
|
|
757
|
+
return picked
|
|
758
|
+
}
|
|
759
|
+
return json
|
|
760
|
+
})
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** Grep/text search across the collection. */
|
|
764
|
+
async searchContent(args: { pattern: string; caseSensitive?: boolean }) {
|
|
765
|
+
return this.grep({
|
|
766
|
+
pattern: args.pattern,
|
|
767
|
+
caseSensitive: args.caseSensitive ?? false,
|
|
768
|
+
} as GrepOptions)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** Hybrid semantic search with graceful fallback to grep. */
|
|
772
|
+
async semanticSearch(args: { query: string; limit?: number }) {
|
|
773
|
+
try {
|
|
774
|
+
return await this.hybridSearch(args.query, { limit: args.limit ?? 10 })
|
|
775
|
+
} catch {
|
|
776
|
+
const grepResults = await this.grep({ pattern: args.query })
|
|
777
|
+
return {
|
|
778
|
+
results: grepResults,
|
|
779
|
+
note: 'No search index available — fell back to text search. Run `cbase embed` to enable semantic search.',
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
528
783
|
}
|
|
529
784
|
|
|
530
|
-
export default ContentDb
|
|
785
|
+
export default ContentDb
|
package/src/node/features/fs.ts
CHANGED
|
@@ -88,6 +88,16 @@ export class FS extends Feature {
|
|
|
88
88
|
return readFileSync(filePath, encoding ?? 'utf-8')
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Synchronously reads a file and returns its contents as a string.
|
|
93
|
+
* added this method because AI Assistants are understandly confused by this deviation from 2000's era node style
|
|
94
|
+
* @alias readFile
|
|
95
|
+
*/
|
|
96
|
+
readFileSync(path: string, encoding?: BufferEncoding | null): string | Buffer {
|
|
97
|
+
return this.readFile(path,encoding)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
91
101
|
/**
|
|
92
102
|
* Asynchronously reads a file and returns its contents as a string.
|
|
93
103
|
*
|
|
@@ -126,6 +136,14 @@ export class FS extends Feature {
|
|
|
126
136
|
readJson(path: string) {
|
|
127
137
|
return JSON.parse(this.readFile(path) as string)
|
|
128
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Read and parse a JSON file synchronously
|
|
142
|
+
* @alias readJson
|
|
143
|
+
*/
|
|
144
|
+
readJsonSync(path: string) {
|
|
145
|
+
return this.readJson(path)
|
|
146
|
+
}
|
|
129
147
|
|
|
130
148
|
/**
|
|
131
149
|
* Asynchronously reads and parses a JSON file.
|
package/src/node/features/git.ts
CHANGED
|
@@ -425,6 +425,96 @@ export class Git extends Feature {
|
|
|
425
425
|
return output
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Extracts a folder (or entire repo) from a remote GitHub repository without cloning.
|
|
430
|
+
*
|
|
431
|
+
* Downloads the repo as a tarball and extracts only the specified subfolder,
|
|
432
|
+
* similar to how degit works. No .git history is included — just the files.
|
|
433
|
+
*
|
|
434
|
+
* Supports shorthand (`user/repo/path`), branch refs (`user/repo/path#branch`),
|
|
435
|
+
* and full GitHub URLs (`https://github.com/user/repo/tree/branch/path`).
|
|
436
|
+
*
|
|
437
|
+
* @param {object} options
|
|
438
|
+
* @param {string} options.source - Repository source in degit-style shorthand
|
|
439
|
+
* @param {string} options.destination - Local path to extract files into
|
|
440
|
+
* @param {string} [options.branch] - Branch, tag, or commit ref (overrides ref in source string)
|
|
441
|
+
* @returns {Promise<{ files: string[], source: { user: string, repo: string, ref: string, subdir: string } }>}
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* ```typescript
|
|
445
|
+
* // Extract a subfolder
|
|
446
|
+
* await git.extractFolder({ source: 'soederpop/luca/src/assistants', destination: './my-assistants' })
|
|
447
|
+
*
|
|
448
|
+
* // Specific branch
|
|
449
|
+
* await git.extractFolder({ source: 'sveltejs/template', destination: './my-app', branch: 'main' })
|
|
450
|
+
*
|
|
451
|
+
* // Full GitHub URL
|
|
452
|
+
* await git.extractFolder({ source: 'https://github.com/user/repo/tree/main/examples', destination: './examples' })
|
|
453
|
+
* ```
|
|
454
|
+
*/
|
|
455
|
+
async extractFolder({ source, destination, branch }: { source: string, destination: string, branch?: string }) {
|
|
456
|
+
const parsed = this._parseRemoteSource(source)
|
|
457
|
+
if (branch) parsed.ref = branch
|
|
458
|
+
|
|
459
|
+
const tarballUrl = `https://github.com/${parsed.user}/${parsed.repo}/archive/${parsed.ref}.tar.gz`
|
|
460
|
+
const stamp = Date.now()
|
|
461
|
+
const fs = this.container.feature('fs')
|
|
462
|
+
const proc = this.container.feature('proc')
|
|
463
|
+
const tmpBase = this.container.paths.resolve(this.container.feature('os').tmpdir, 'luca-degit')
|
|
464
|
+
fs.ensureFolder(tmpBase)
|
|
465
|
+
const tarPath = this.container.paths.resolve(tmpBase, `.degit-${stamp}.tar.gz`)
|
|
466
|
+
const tmpExtract = this.container.paths.resolve(tmpBase, `.degit-extract-${stamp}`)
|
|
467
|
+
const dest = destination.startsWith('/') ? destination : this.container.paths.resolve(destination)
|
|
468
|
+
|
|
469
|
+
const dl = this.container.feature('downloader')
|
|
470
|
+
|
|
471
|
+
await dl.download(tarballUrl, tarPath)
|
|
472
|
+
|
|
473
|
+
// Extract everything, strip the root archive directory (e.g. repo-commitsha/)
|
|
474
|
+
fs.ensureFolder(tmpExtract)
|
|
475
|
+
const extractResult = await proc.execAndCapture(`tar xzf ${tarPath} -C ${tmpExtract} --strip-components=1`)
|
|
476
|
+
if (extractResult.exitCode !== 0) {
|
|
477
|
+
await fs.rmdir(tmpExtract)
|
|
478
|
+
await fs.rm(tarPath)
|
|
479
|
+
throw new Error(`Failed to extract tarball: ${extractResult.stderr}`)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Copy the subfolder (or everything) to destination
|
|
483
|
+
const sourceDir = parsed.subdir ? `${tmpExtract}/${parsed.subdir}` : tmpExtract
|
|
484
|
+
if (fs.existsSync(dest)) await fs.rmdir(dest)
|
|
485
|
+
fs.ensureFolder(dest)
|
|
486
|
+
fs.copy(sourceDir, dest, { overwrite: true })
|
|
487
|
+
|
|
488
|
+
// Cleanup temp files
|
|
489
|
+
await fs.rm(tarPath)
|
|
490
|
+
await fs.rmdir(tmpExtract)
|
|
491
|
+
|
|
492
|
+
const files = fs.readdirSync(dest)
|
|
493
|
+
return { files, source: parsed }
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Parses degit-style source strings into components. */
|
|
497
|
+
private _parseRemoteSource(input: string) {
|
|
498
|
+
let ref = 'HEAD'
|
|
499
|
+
let str = input.replace(/^https?:\/\//, '')
|
|
500
|
+
|
|
501
|
+
// Handle github.com/user/repo/tree/branch/path URLs
|
|
502
|
+
const treeMatch = str.match(/^github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)(?:\/(.+))?$/)
|
|
503
|
+
if (treeMatch) {
|
|
504
|
+
return { user: treeMatch[1], repo: treeMatch[2], ref: treeMatch[3], subdir: treeMatch[4] || '' }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (str.startsWith('github.com/')) str = str.replace('github.com/', '')
|
|
508
|
+
if (str.includes('#')) {
|
|
509
|
+
const parts = str.split('#')
|
|
510
|
+
str = parts[0]
|
|
511
|
+
ref = parts[1]
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const parts = str.split('/')
|
|
515
|
+
return { user: parts[0], repo: parts[1], ref, subdir: parts.slice(2).join('/') }
|
|
516
|
+
}
|
|
517
|
+
|
|
428
518
|
/**
|
|
429
519
|
* Gets the commit history for a set of files or glob patterns.
|
|
430
520
|
*
|
package/src/node/features/tts.ts
CHANGED
|
@@ -70,7 +70,7 @@ export class TTS extends Feature<TTSState, TTSOptions> {
|
|
|
70
70
|
|
|
71
71
|
/** Directory where generated audio files are saved. */
|
|
72
72
|
get outputDir(): string {
|
|
73
|
-
return this.options.outputDir || this.container.paths.
|
|
73
|
+
return this.options.outputDir || this.container.paths.resolve(this.container.feature('os').homedir, '.luca', 'tts-cache')
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/** The 20 preset voice names available in Chatterbox Turbo. */
|
package/src/node/features/vm.ts
CHANGED
|
@@ -247,6 +247,54 @@ export class VM<
|
|
|
247
247
|
return (await script.runInContext(context)) as T
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Execute code and capture all console output as structured JSON.
|
|
252
|
+
*
|
|
253
|
+
* Returns both the execution result and an array of every `console.*` call
|
|
254
|
+
* made during execution, each entry recording the method name and arguments.
|
|
255
|
+
*
|
|
256
|
+
* @param code - The JavaScript code to execute
|
|
257
|
+
* @param ctx - Context variables to make available to the executing code
|
|
258
|
+
* @returns The result, an array of captured console calls, and the context
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```typescript
|
|
262
|
+
* const { result, console: calls } = await vm.runCaptured('console.log("hi"); console.warn("oh"); 42')
|
|
263
|
+
* // result === 42
|
|
264
|
+
* // calls === [{ method: 'log', args: ['hi'] }, { method: 'warn', args: ['oh'] }]
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
async runCaptured<T extends any>(code: string, ctx: any = {}): Promise<{
|
|
268
|
+
result: T
|
|
269
|
+
console: Array<{ method: string, args: any[] }>
|
|
270
|
+
context: vm.Context
|
|
271
|
+
}> {
|
|
272
|
+
const calls: Array<{ method: string, args: any[] }> = []
|
|
273
|
+
const captureConsole: Record<string, (...args: any[]) => void> = {}
|
|
274
|
+
|
|
275
|
+
for (const method of ['log', 'info', 'warn', 'error', 'debug', 'trace', 'dir', 'table'] as const) {
|
|
276
|
+
captureConsole[method] = (...args: any[]) => {
|
|
277
|
+
calls.push({ method, args })
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const isExisting = this.isContext(ctx)
|
|
282
|
+
const context = isExisting ? ctx : this.createContext({ ...ctx, console: captureConsole })
|
|
283
|
+
|
|
284
|
+
// For existing contexts, swap console in for the run and restore after
|
|
285
|
+
const prevConsole = isExisting ? ctx.console : undefined
|
|
286
|
+
if (isExisting) ctx.console = captureConsole
|
|
287
|
+
|
|
288
|
+
const wrapped = this.wrapTopLevelAwait(code)
|
|
289
|
+
const script = this.createScript(wrapped)
|
|
290
|
+
try {
|
|
291
|
+
const result = (await script.runInContext(context)) as T
|
|
292
|
+
return { result, console: calls, context }
|
|
293
|
+
} finally {
|
|
294
|
+
if (isExisting) ctx.console = prevConsole
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
250
298
|
/**
|
|
251
299
|
* Execute JavaScript code synchronously in a controlled environment.
|
|
252
300
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Auto-generated scaffold and MCP readme content
|
|
2
|
-
// Generated at: 2026-03-
|
|
2
|
+
// Generated at: 2026-03-22T20:57:24.026Z
|
|
3
3
|
// Source: docs/scaffolds/*.md, docs/examples/assistant/, and docs/mcp/readme.md
|
|
4
4
|
//
|
|
5
5
|
// Do not edit manually. Run: luca build-scaffolds
|
|
@@ -1669,7 +1669,7 @@ import { Server, servers, ServerStateSchema } from '@soederpop/luca'
|
|
|
1669
1669
|
import { commands, CommandOptionsSchema } from '@soederpop/luca'
|
|
1670
1670
|
\`\`\`
|
|
1671
1671
|
|
|
1672
|
-
Never import from \`fs\`, \`path\`, \`crypto\`, or other Node builtins. Never import third-party packages in consumer code.
|
|
1672
|
+
Never import from \`fs\`, \`path\`, \`crypto\`, or other Node builtins. Never import third-party packages in consumer code. If a container feature wraps the functionality, use it.
|
|
1673
1673
|
|
|
1674
1674
|
## Zod v4
|
|
1675
1675
|
|