@soederpop/luca 0.0.25 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soederpop/luca",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "website": "https://luca.soederpop.com",
5
5
  "description": "lightweight universal conversational architecture AKA Le Ultimate Component Architecture AKA Last Universal Common Ancestor, part AI part Human",
6
6
  "author": "jon soeder aka the people's champ <jon@soederpop.com>",
@@ -278,28 +278,30 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
278
278
  * .use(container.feature('git'))
279
279
  * ```
280
280
  */
281
- use(fnOrHelper: ((assistant: this) => void | Promise<void>) | { toTools: () => { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> } }): this {
281
+ use(fnOrHelper: ((assistant: this) => void | Promise<void>) | { toTools: () => { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> } } | { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> }): this {
282
282
  if (typeof fnOrHelper === 'function') {
283
283
  const result = fnOrHelper(this)
284
284
  if (result && typeof (result as any).then === 'function') {
285
285
  const pending = this.state.get('pendingPlugins') as Promise<void>[]
286
286
  this.state.set('pendingPlugins', [...pending, result as Promise<void>])
287
287
  }
288
- } else if (fnOrHelper && typeof fnOrHelper.toTools === 'function') {
289
- const { schemas, handlers } = fnOrHelper.toTools()
290
- for (const name of Object.keys(schemas)) {
291
- if(typeof handlers[name] === 'function') {
292
- this.addTool(
293
- name,
294
- handlers[name] as any,
295
- schemas[name],
296
- )
297
- }
298
- }
288
+ } else if (fnOrHelper && typeof (fnOrHelper as any).toTools === 'function') {
289
+ this._registerTools((fnOrHelper as any).toTools())
290
+ } else if (fnOrHelper && 'schemas' in fnOrHelper && 'handlers' in fnOrHelper) {
291
+ this._registerTools(fnOrHelper as { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> })
299
292
  }
300
293
  return this
301
294
  }
302
295
 
296
+ /** Register tools from a `{ schemas, handlers }` object. */
297
+ private _registerTools({ schemas, handlers }: { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> }) {
298
+ for (const name of Object.keys(schemas)) {
299
+ if (typeof handlers[name] === 'function') {
300
+ this.addTool(name, handlers[name] as any, schemas[name])
301
+ }
302
+ }
303
+ }
304
+
303
305
  /**
304
306
  * Add a tool to this assistant. The tool name is derived from the
305
307
  * handler's function name.
@@ -108,15 +108,39 @@ export class DocsReader extends Feature<DocsReaderState, DocsReaderOptions> {
108
108
 
109
109
  assistant?: Assistant
110
110
 
111
+
112
+ private generateSpecificCollectionExplainer() {
113
+ const { contentDb } = this
114
+ const fileTree = contentDb.fileTree
115
+ const modelDefinitionTable = contentDb.modelDefinitionTable
116
+ const modelNames = contentDb.modelNames
117
+
118
+ const domainTermSummary = this.container.feature('ui').endent(`
119
+ ## Domain Specific Terms
120
+
121
+ When the user is referring to one of the following nouns: ${modelNames.join(', ')} they are likely
122
+ referencing one of the documents defined by the following content models:
123
+
124
+ ${Object.entries(modelDefinitionTable).map(([name, { description, glob, routePatterns }]) => `
125
+ - **${name}**: ${description}
126
+ Glob: ${glob}
127
+ Route Patterns: ${routePatterns.join(', ')}
128
+ `).join('\n')}
129
+ `)
130
+
131
+ return domainTermSummary
132
+ }
133
+
111
134
  /** Start the docs reader by loading the contentDb and wiring its tools into an assistant. */
112
135
  async start(): Promise<DocsReader> {
113
136
  if (this.isStarted) return this
114
137
 
115
138
  const contentDb = this.contentDb
116
139
  if (!contentDb.isLoaded) await contentDb.load()
140
+
117
141
 
118
142
  this.assistant = this.container.feature('assistant', {
119
- systemPrompt: CONTENT_DB_SYSTEM_PROMPT,
143
+ systemPrompt: [CONTENT_DB_SYSTEM_PROMPT, this.generateSpecificCollectionExplainer()].filter(Boolean).join('\n\n'),
120
144
  model: this.options.model,
121
145
  local: this.options.local,
122
146
  }).use(contentDb)
@@ -1,5 +1,5 @@
1
1
  // Auto-generated bootstrap content
2
- // Generated at: 2026-03-22T06:53:18.884Z
2
+ // Generated at: 2026-03-22T20:57:24.866Z
3
3
  // Source: docs/bootstrap/*.md, docs/bootstrap/templates/*, docs/examples/*.md, docs/tutorials/*.md
4
4
  //
5
5
  // Do not edit manually. Run: luca build-bootstrap
@@ -1,4 +1,4 @@
1
1
  // Generated at compile time — do not edit manually
2
- export const BUILD_SHA = '33ebc12'
2
+ export const BUILD_SHA = '4f7677c'
3
3
  export const BUILD_BRANCH = 'main'
4
- export const BUILD_DATE = '2026-03-22T06:53:18Z'
4
+ export const BUILD_DATE = '2026-03-22T20:57:24Z'
package/src/command.ts CHANGED
@@ -266,6 +266,81 @@ export class CommandsRegistry extends Registry<Command<any>> {
266
266
  override scope = 'commands'
267
267
  override baseClass = Command as any
268
268
 
269
+ /**
270
+ * Convert all registered commands into a `{ schemas, handlers }` object
271
+ * compatible with `assistant.use()`.
272
+ *
273
+ * Each command becomes a tool whose parameters come from the command's
274
+ * `argsSchema` (with internal CLI fields stripped) and whose handler
275
+ * dispatches the command headlessly, returning `{ exitCode, stdout, stderr }`.
276
+ *
277
+ * @param container - The container used to instantiate and dispatch commands
278
+ * @param options - Optional filter/transform options
279
+ * @param options.include - Only include these command names (default: all)
280
+ * @param options.exclude - Exclude these command names (default: none)
281
+ * @param options.prefix - Prefix tool names (e.g. 'luca_' → 'luca_eval')
282
+ */
283
+ toTools(
284
+ container: Container<any> & CommandsInterface,
285
+ options?: { include?: string[], exclude?: string[], prefix?: string },
286
+ ): { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> } {
287
+ const schemas: Record<string, z.ZodType> = {}
288
+ const handlers: Record<string, Function> = {}
289
+ const prefix = options?.prefix ?? ''
290
+ const includeSet = options?.include ? new Set(options.include) : null
291
+ const excludeSet = new Set(options?.exclude ?? [])
292
+
293
+ // Internal fields inherited from HelperOptionsSchema / CommandOptionsSchema
294
+ const internalFields = ['_', 'dispatchSource', 'name', '_cacheKey']
295
+
296
+ for (const name of this.available) {
297
+ if (excludeSet.has(name)) continue
298
+ if (includeSet && !includeSet.has(name)) continue
299
+
300
+ const Cmd = this.lookup(name) as typeof Command
301
+ const rawSchema = Cmd.argsSchema
302
+ const description = Cmd.commandDescription || Cmd.description || name
303
+
304
+ // Build a clean schema by stripping internal CLI fields from the argsSchema.
305
+ // If the schema is a ZodObject we can use .omit(), otherwise create a
306
+ // virtual passthrough schema so the tool still flows through.
307
+ let toolSchema: z.ZodType
308
+ try {
309
+ const shape = typeof (rawSchema as any)?._def?.shape === 'function'
310
+ ? (rawSchema as any)._def.shape()
311
+ : (rawSchema as any)?._def?.shape
312
+
313
+ if (shape) {
314
+ // Build a new object schema omitting internal fields
315
+ const cleanShape: Record<string, z.ZodType> = {}
316
+ for (const [key, val] of Object.entries(shape)) {
317
+ if (internalFields.includes(key)) continue
318
+ cleanShape[key] = val as z.ZodType
319
+ }
320
+
321
+ toolSchema = Object.keys(cleanShape).length > 0
322
+ ? z.object(cleanShape).describe(description)
323
+ : z.object({}).describe(description)
324
+ } else {
325
+ // Not a ZodObject — wrap as passthrough
326
+ toolSchema = z.object({}).describe(description)
327
+ }
328
+ } catch {
329
+ toolSchema = z.object({}).describe(description)
330
+ }
331
+
332
+ const toolName = `${prefix}${name}`
333
+ schemas[toolName] = toolSchema
334
+ handlers[toolName] = async (args: Record<string, any>) => {
335
+ const cmd = container.command(name as any)
336
+ const result = await cmd.dispatch(args ?? {}, 'headless')
337
+ return result ?? { exitCode: 0, stdout: '', stderr: '' }
338
+ }
339
+ }
340
+
341
+ return { schemas, handlers }
342
+ }
343
+
269
344
  /**
270
345
  * Internal: register a command from a handler function.
271
346
  * Used by built-in commands. External commands should use the module export pattern.