@pikku/inspector 0.12.7 → 0.12.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/add/add-ai-agent.js +24 -7
  3. package/dist/add/add-channel.js +2 -2
  4. package/dist/add/add-cli.js +13 -10
  5. package/dist/add/add-file-with-factory.js +22 -5
  6. package/dist/add/add-functions.js +4 -3
  7. package/dist/add/add-http-route.js +1 -0
  8. package/dist/add/add-mcp-prompt.js +4 -0
  9. package/dist/add/add-mcp-resource.js +4 -0
  10. package/dist/add/add-rpc-invocations.js +2 -2
  11. package/dist/add/add-workflow.d.ts +5 -0
  12. package/dist/add/add-workflow.js +20 -2
  13. package/dist/inspector.js +1 -0
  14. package/dist/types.d.ts +1 -0
  15. package/dist/utils/extract-function-name.d.ts +1 -0
  16. package/dist/utils/extract-function-name.js +27 -32
  17. package/dist/utils/extract-node-value.js +6 -1
  18. package/dist/utils/filter-inspector-state.js +211 -8
  19. package/dist/utils/load-addon-functions-meta.js +47 -0
  20. package/dist/utils/post-process.js +63 -0
  21. package/dist/utils/resolve-versions.js +30 -0
  22. package/dist/utils/schema-generator.js +124 -33
  23. package/dist/utils/serialize-inspector-state.d.ts +1 -0
  24. package/dist/utils/serialize-inspector-state.js +2 -0
  25. package/dist/visit.js +1 -1
  26. package/package.json +2 -2
  27. package/src/add/add-ai-agent.ts +25 -10
  28. package/src/add/add-channel.ts +2 -2
  29. package/src/add/add-cli.ts +17 -16
  30. package/src/add/add-file-with-factory.ts +26 -7
  31. package/src/add/add-functions.ts +4 -4
  32. package/src/add/add-http-route.ts +6 -1
  33. package/src/add/add-mcp-prompt.ts +5 -0
  34. package/src/add/add-mcp-resource.ts +5 -0
  35. package/src/add/add-queue-worker.ts +5 -1
  36. package/src/add/add-rpc-invocations.ts +2 -2
  37. package/src/add/add-workflow.ts +22 -2
  38. package/src/inspector.ts +1 -0
  39. package/src/types.ts +1 -0
  40. package/src/utils/extract-function-name.ts +36 -37
  41. package/src/utils/extract-node-value.test.ts +67 -0
  42. package/src/utils/extract-node-value.ts +5 -1
  43. package/src/utils/filter-inspector-state.ts +246 -11
  44. package/src/utils/load-addon-functions-meta.ts +59 -0
  45. package/src/utils/post-process.ts +74 -0
  46. package/src/utils/resolve-versions.test.ts +141 -0
  47. package/src/utils/resolve-versions.ts +37 -0
  48. package/src/utils/schema-generator.ts +191 -41
  49. package/src/utils/serialize-inspector-state.ts +3 -0
  50. package/src/visit.ts +2 -1
  51. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,7 @@
1
1
  import * as ts from 'typescript';
2
2
  import { dirname, join, resolve } from 'path';
3
3
  import { createGenerator, RootlessError } from 'ts-json-schema-generator';
4
- import { tsImport } from 'tsx/esm/api';
4
+ import { register, tsImport } from 'tsx/esm/api';
5
5
  import * as z from 'zod';
6
6
  import { zodToTs, createAuxiliaryTypeStore } from 'zod-to-ts';
7
7
  import { ErrorCode } from '../error-codes.js';
@@ -44,6 +44,9 @@ function primitiveTypeToSchema(typeStr) {
44
44
  if (normalized === 'null') {
45
45
  return { type: 'null' };
46
46
  }
47
+ if (normalized === 'any' || normalized === 'unknown') {
48
+ return {};
49
+ }
47
50
  return null;
48
51
  }
49
52
  // Cached state for schema program reuse across inspect() calls
@@ -89,7 +92,7 @@ function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileCont
89
92
  cachedSchemaProgram = program;
90
93
  return program;
91
94
  }
92
- function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, functionMeta, httpWiringsMeta, additionalTypes, additionalProperties = false, schemaLookup) {
95
+ function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, functionMeta, httpWiringsMeta, additionalTypes, additionalProperties = false, generatedZodSchemas) {
93
96
  const schemasSet = new Set(typesMap.customTypes.keys());
94
97
  for (const { inputs, outputs } of Object.values(functionMeta)) {
95
98
  const types = [...(inputs || []), ...(outputs || [])];
@@ -123,6 +126,14 @@ function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, funct
123
126
  schemasSet.add(type);
124
127
  }
125
128
  }
129
+ // Skip ts-json-schema-generator if all schemas are already covered by Zod/primitives.
130
+ // Use generatedZodSchemas (actually converted) rather than schemaLookup (all attempted)
131
+ // so that failed Zod conversions fall through to TS schema generation.
132
+ const uncoveredSchemas = [...schemasSet].filter((s) => !PRIMITIVE_TYPES.has(s) && !generatedZodSchemas?.[s]);
133
+ if (uncoveredSchemas.length === 0) {
134
+ return {};
135
+ }
136
+ logger.debug(`generateTSSchemas needed for ${uncoveredSchemas.length} types: ${uncoveredSchemas.slice(0, 3).join(', ')}${uncoveredSchemas.length > 3 ? '...' : ''}`);
126
137
  const virtualFilePath = join(dirname(resolve(tsconfig)), '__pikku_virtual_types__.ts');
127
138
  const program = createProgramWithVirtualFile(tsconfig, virtualFilePath, customTypesContent);
128
139
  const generator = createGenerator({
@@ -142,7 +153,7 @@ function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, funct
142
153
  if (PRIMITIVE_TYPES.has(schema)) {
143
154
  return;
144
155
  }
145
- if (schemaLookup?.has(schema)) {
156
+ if (generatedZodSchemas?.[schema]) {
146
157
  return;
147
158
  }
148
159
  try {
@@ -165,53 +176,133 @@ function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, funct
165
176
  });
166
177
  return schemas;
167
178
  }
179
+ /**
180
+ * Import all source files in parallel using tsx's register() API.
181
+ *
182
+ * tsx's register() sets up the TypeScript loader once, then all subsequent
183
+ * import() calls reuse that loader. This is dramatically faster than calling
184
+ * tsImport() per-file because tsImport() sets up and tears down a fresh
185
+ * compilation context for each call (~170ms each).
186
+ *
187
+ * With register() + parallel import():
188
+ * - 71 files: ~350ms total
189
+ * - vs tsImport loop: ~12,000ms (71 * 170ms)
190
+ *
191
+ * Falls back to serial tsImport() per-file if register() is unavailable.
192
+ */
193
+ async function batchImportWithRegister(logger, sourceFiles) {
194
+ if (sourceFiles.length === 0)
195
+ return new Map();
196
+ let unregister;
197
+ try {
198
+ unregister = register();
199
+ const modules = new Map();
200
+ const results = await Promise.allSettled(sourceFiles.map(async (srcPath) => {
201
+ const mod = await import(srcPath);
202
+ modules.set(srcPath, mod);
203
+ }));
204
+ const failures = results.filter((r) => r.status === 'rejected');
205
+ if (failures.length > 0) {
206
+ logger.debug(`${failures.length}/${sourceFiles.length} files failed to import via register()`);
207
+ }
208
+ return modules;
209
+ }
210
+ catch (e) {
211
+ logger.debug(`tsx register() batch import failed: ${e.message}`);
212
+ return null;
213
+ }
214
+ finally {
215
+ unregister?.();
216
+ }
217
+ }
218
+ function processZodSchema(schemaName, zodSchema, schemas, typesMap, auxiliaryTypeStore, printer, fakeSourceFile, logger) {
219
+ const schema = z.toJSONSchema(zodSchema, {
220
+ unrepresentable: 'any',
221
+ override: ({ zodSchema, jsonSchema }) => {
222
+ if (zodSchema._zod?.def?.type === 'date') {
223
+ ;
224
+ jsonSchema.type = 'string';
225
+ jsonSchema.format = 'date-time';
226
+ }
227
+ },
228
+ });
229
+ if (schema.required && schema.properties) {
230
+ schema.required = schema.required.filter((fieldName) => {
231
+ const prop = schema.properties[fieldName];
232
+ return prop && prop.default === undefined;
233
+ });
234
+ if (schema.required.length === 0) {
235
+ delete schema.required;
236
+ }
237
+ }
238
+ const { node: tsType } = zodToTs(zodSchema, { auxiliaryTypeStore });
239
+ const typeText = printer.printNode(ts.EmitHint.Unspecified, tsType, fakeSourceFile);
240
+ typesMap.addCustomType(schemaName, typeText, []);
241
+ schemas[schemaName] = schema;
242
+ logger.debug(`• Generated schema from Zod: ${schemaName}`);
243
+ }
168
244
  async function generateZodSchemas(logger, schemaLookup, typesMap) {
169
245
  const schemas = {};
170
246
  const auxiliaryTypeStore = createAuxiliaryTypeStore();
171
247
  const printer = ts.createPrinter();
172
248
  const fakeSourceFile = ts.createSourceFile('zod-types.ts', '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS);
249
+ // Validate all schemas are zod (or unspecified vendor)
173
250
  for (const [schemaName, ref] of schemaLookup.entries()) {
174
251
  if (ref.vendor && ref.vendor !== 'zod') {
175
252
  throw new Error(`Schema '${schemaName}' uses ${ref.vendor} which is not yet supported for JSON Schema generation. ` +
176
253
  `Currently only Zod schemas can be converted to JSON Schema. ` +
177
254
  `Please use Zod or contribute support for ${ref.vendor}.`);
178
255
  }
179
- try {
180
- const module = await tsImport(ref.sourceFile, import.meta.url);
181
- const zodSchema = module[ref.variableName];
256
+ }
257
+ // Collect unique source files and batch-import them in parallel
258
+ const uniqueSourceFiles = [
259
+ ...new Set([...schemaLookup.values()].map((ref) => ref.sourceFile)),
260
+ ];
261
+ console.log(`[TIMING] Zod schemas: ${schemaLookup.size} schemas from ${uniqueSourceFiles.length} files`);
262
+ const importStart = performance.now();
263
+ const importedModules = await batchImportWithRegister(logger, uniqueSourceFiles);
264
+ console.log(`[TIMING] Batch import: ${(performance.now() - importStart).toFixed(0)}ms`);
265
+ const processStart = performance.now();
266
+ // Track schemas that need per-file tsImport fallback
267
+ const fallbackSchemas = [];
268
+ for (const [schemaName, ref] of schemaLookup.entries()) {
269
+ const mod = importedModules?.get(ref.sourceFile);
270
+ if (mod) {
271
+ const zodSchema = mod[ref.variableName];
182
272
  if (!zodSchema) {
183
- logger.warn(`Could not find exported schema '${ref.variableName}' in ${ref.sourceFile} for ${schemaName}. Available exports: ${Object.keys(module).join(', ')}`);
273
+ logger.warn(`Could not find exported schema '${ref.variableName}' in ${ref.sourceFile} for ${schemaName}. Available exports: ${Object.keys(mod).join(', ')}`);
184
274
  continue;
185
275
  }
186
- const schema = z.toJSONSchema(zodSchema, {
187
- unrepresentable: 'any',
188
- override: ({ zodSchema, jsonSchema }) => {
189
- if (zodSchema._zod?.def?.type === 'date') {
190
- ;
191
- jsonSchema.type = 'string';
192
- jsonSchema.format = 'date-time';
193
- }
194
- },
195
- });
196
- if (schema.required && schema.properties) {
197
- schema.required = schema.required.filter((fieldName) => {
198
- const prop = schema.properties[fieldName];
199
- return prop && prop.default === undefined;
200
- });
201
- if (schema.required.length === 0) {
202
- delete schema.required;
203
- }
276
+ try {
277
+ processZodSchema(schemaName, zodSchema, schemas, typesMap, auxiliaryTypeStore, printer, fakeSourceFile, logger);
278
+ }
279
+ catch (e) {
280
+ logger.warn(`Could not convert Zod schema '${schemaName}': ${e instanceof Error ? e.message : e}`);
204
281
  }
205
- schemas[schemaName] = schema;
206
- const { node: tsType } = zodToTs(zodSchema, { auxiliaryTypeStore });
207
- const typeText = printer.printNode(ts.EmitHint.Unspecified, tsType, fakeSourceFile);
208
- typesMap.addCustomType(schemaName, typeText, []);
209
- logger.debug(`• Generated schema from Zod: ${schemaName}`);
210
282
  }
211
- catch (e) {
212
- logger.warn(`Could not convert Zod schema '${schemaName}': ${e instanceof Error ? e.message : e}`);
283
+ else {
284
+ fallbackSchemas.push([schemaName, ref]);
285
+ }
286
+ }
287
+ // Fallback: use tsImport for any schemas that batch import couldn't handle
288
+ if (fallbackSchemas.length > 0) {
289
+ logger.debug(`Falling back to tsImport for ${fallbackSchemas.length} schema(s)`);
290
+ for (const [schemaName, ref] of fallbackSchemas) {
291
+ try {
292
+ const module = await tsImport(ref.sourceFile, import.meta.url);
293
+ const zodSchema = module[ref.variableName];
294
+ if (!zodSchema) {
295
+ logger.warn(`Could not find exported schema '${ref.variableName}' in ${ref.sourceFile} for ${schemaName}. Available exports: ${Object.keys(module).join(', ')}`);
296
+ continue;
297
+ }
298
+ processZodSchema(schemaName, zodSchema, schemas, typesMap, auxiliaryTypeStore, printer, fakeSourceFile, logger);
299
+ }
300
+ catch (e) {
301
+ logger.warn(`Could not convert Zod schema '${schemaName}': ${e instanceof Error ? e.message : e}`);
302
+ }
213
303
  }
214
304
  }
305
+ console.log(`[TIMING] Process schemas: ${(performance.now() - processStart).toFixed(0)}ms (${Object.keys(schemas).length} generated)`);
215
306
  return schemas;
216
307
  }
217
308
  export async function generateAllSchemas(logger, config, state) {
@@ -222,7 +313,7 @@ export async function generateAllSchemas(logger, config, state) {
222
313
  logger.debug('Reusing cached TS schemas (types unchanged)');
223
314
  return { ...cachedTSSchemas, ...zodSchemas };
224
315
  }
225
- const tsSchemas = generateTSSchemas(logger, config.tsconfig, customTypesContent, state.functions.typesMap, state.functions.meta, state.http.meta, config.schemasFromTypes, config.schema?.additionalProperties, state.schemaLookup);
316
+ const tsSchemas = generateTSSchemas(logger, config.tsconfig, customTypesContent, state.functions.typesMap, state.functions.meta, state.http.meta, config.schemasFromTypes, config.schema?.additionalProperties, zodSchemas);
226
317
  cachedCustomTypesContent = customTypesContent;
227
318
  cachedTSSchemas = tsSchemas;
228
319
  return { ...tsSchemas, ...zodSchemas };
@@ -55,6 +55,7 @@ export interface SerializableInspectorState {
55
55
  }[]
56
56
  ]>;
57
57
  wireServicesMeta: Array<[string, string[]]>;
58
+ addonRequiredParentServices: string[];
58
59
  configFactories: Array<[
59
60
  string,
60
61
  {
@@ -23,6 +23,7 @@ export function serializeInspectorState(state) {
23
23
  singletonServicesFactories: Array.from(state.singletonServicesFactories.entries()),
24
24
  wireServicesFactories: Array.from(state.wireServicesFactories.entries()),
25
25
  wireServicesMeta: Array.from(state.wireServicesMeta.entries()),
26
+ addonRequiredParentServices: state.addonRequiredParentServices,
26
27
  configFactories: Array.from(state.configFactories.entries()),
27
28
  filesAndMethods: state.filesAndMethods,
28
29
  filesAndMethodsErrors: Array.from(state.filesAndMethodsErrors.entries()).map(([key, mapValue]) => [key, Array.from(mapValue.entries())]),
@@ -167,6 +168,7 @@ export function deserializeInspectorState(data) {
167
168
  singletonServicesFactories: new Map(data.singletonServicesFactories),
168
169
  wireServicesFactories: new Map(data.wireServicesFactories),
169
170
  wireServicesMeta: new Map(data.wireServicesMeta),
171
+ addonRequiredParentServices: data.addonRequiredParentServices || [],
170
172
  configFactories: new Map(data.configFactories),
171
173
  filesAndMethods: data.filesAndMethods,
172
174
  filesAndMethodsErrors: new Map(data.filesAndMethodsErrors.map(([key, entries]) => [
package/dist/visit.js CHANGED
@@ -28,7 +28,7 @@ export const visitSetup = (logger, checker, node, state, options) => {
28
28
  addFileExtendsCoreType(node, checker, state.wireServicesTypeImportMap, 'CoreServices', state);
29
29
  addFileExtendsCoreType(node, checker, state.userSessionTypeImportMap, 'CoreUserSession', state);
30
30
  addFileExtendsCoreType(node, checker, state.configTypeImportMap, 'CoreConfig', state);
31
- addFileWithFactory(node, checker, state.singletonServicesFactories, 'CreateSingletonServices');
31
+ addFileWithFactory(node, checker, state.singletonServicesFactories, 'CreateSingletonServices', state);
32
32
  addFileWithFactory(node, checker, state.wireServicesFactories, 'CreateWireServices', state);
33
33
  addFileWithFactory(node, checker, state.configFactories, 'CreateConfig');
34
34
  addRPCInvocations(node, state, logger);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.7",
3
+ "version": "0.12.9",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
- "@pikku/core": "^0.12.13",
38
+ "@pikku/core": "^0.12.16",
39
39
  "path-to-regexp": "^8.3.0",
40
40
  "ts-json-schema-generator": "^2.5.0",
41
41
  "tsx": "^4.21.0",
@@ -63,7 +63,7 @@ function resolveToolReferences(
63
63
  }
64
64
  }
65
65
 
66
- if (calleeName === 'addon') {
66
+ if (calleeName === 'ref') {
67
67
  const [firstArg] = element.arguments
68
68
  if (firstArg && ts.isStringLiteral(firstArg)) {
69
69
  resolved.push(firstArg.text)
@@ -258,19 +258,17 @@ export const addAIAgent: AddWiring = (
258
258
 
259
259
  const modelValue = getPropertyValue(obj, 'model') as string | null
260
260
 
261
- const instructionsValue = getPropertyValue(obj, 'instructions') as
261
+ const roleValue = getPropertyValue(obj, 'role') as string | null
262
+ const personalityValue = getPropertyValue(obj, 'personality') as
262
263
  | string
263
- | string[]
264
264
  | null
265
+ const goalValue = getPropertyValue(obj, 'goal') as string | null
265
266
 
266
267
  const maxStepsValue = getPropertyValue(obj, 'maxSteps') as number | null
267
268
  const temperatureValue = getPropertyValue(obj, 'temperature') as
268
269
  | number
269
270
  | null
270
271
  const toolChoiceValue = getPropertyValue(obj, 'toolChoice') as string | null
271
- const dynamicWorkflowsValue = getPropertyValue(obj, 'dynamicWorkflows') as
272
- | string
273
- | null
274
272
  const toolsValue = resolveToolReferences(
275
273
  obj,
276
274
  checker,
@@ -447,10 +445,14 @@ export const addAIAgent: AddWiring = (
447
445
  state.agents.agentsMeta[agentKey] = {
448
446
  name: nameValue,
449
447
  description,
450
- instructions: instructionsValue || '',
448
+ role: roleValue || undefined,
449
+ personality: personalityValue || undefined,
450
+ goal: goalValue || '',
451
451
  model: modelValue || '',
452
452
  summary,
453
453
  errors,
454
+ sourceFile: node.getSourceFile().fileName,
455
+ exportedName: exportedName || undefined,
454
456
  ...(maxStepsValue !== null && { maxSteps: maxStepsValue }),
455
457
  ...(temperatureValue !== null && { temperature: temperatureValue }),
456
458
  ...(toolChoiceValue !== null && {
@@ -458,9 +460,6 @@ export const addAIAgent: AddWiring = (
458
460
  }),
459
461
  ...(toolsValue !== null && { tools: toolsValue }),
460
462
  ...(agentsValue !== null && { agents: agentsValue }),
461
- ...(dynamicWorkflowsValue !== null && {
462
- dynamicWorkflows: dynamicWorkflowsValue as 'read' | 'always' | 'ask',
463
- }),
464
463
  tags,
465
464
  inputSchema,
466
465
  outputSchema,
@@ -470,5 +469,21 @@ export const addAIAgent: AddWiring = (
470
469
  aiMiddleware,
471
470
  permissions,
472
471
  }
472
+
473
+ // AI agent functions require platform services that aren't visible
474
+ // through parameter destructuring
475
+ const funcMeta = state.functions.meta[agentKey]
476
+ if (funcMeta?.services) {
477
+ for (const svc of [
478
+ 'aiStorage',
479
+ 'aiRunState',
480
+ 'agentRunService',
481
+ 'aiAgentRunner',
482
+ ]) {
483
+ if (!funcMeta.services.services.includes(svc)) {
484
+ funcMeta.services.services.push(svc)
485
+ }
486
+ }
487
+ }
473
488
  }
474
489
  }
@@ -94,8 +94,8 @@ function getHandlerNameFromExpression(
94
94
 
95
95
  // Handle call expressions
96
96
  if (ts.isCallExpression(expr)) {
97
- // Handle addon('namespace:funcName') calls
98
- if (ts.isIdentifier(expr.expression) && expr.expression.text === 'addon') {
97
+ // Handle ref('name') calls
98
+ if (ts.isIdentifier(expr.expression) && expr.expression.text === 'ref') {
99
99
  const [firstArg] = expr.arguments
100
100
  if (firstArg && ts.isStringLiteral(firstArg)) {
101
101
  return firstArg.text
@@ -349,29 +349,30 @@ function processCommand(
349
349
  if (
350
350
  ts.isCallExpression(prop.initializer) &&
351
351
  ts.isIdentifier(prop.initializer.expression) &&
352
- prop.initializer.expression.text === 'addon'
352
+ prop.initializer.expression.text === 'ref'
353
353
  ) {
354
354
  const [firstArg] = prop.initializer.arguments
355
355
  if (!firstArg || !ts.isStringLiteral(firstArg)) {
356
- throw new Error(
357
- `addon() call requires a string literal argument in the form "namespace:funcName"`
358
- )
356
+ throw new Error(`ref() call requires a string literal argument`)
359
357
  }
360
358
  pikkuFuncId = firstArg.text
361
- const addonNamespace = pikkuFuncId.split(':')[0]
362
- if (!addonNamespace || !pikkuFuncId.includes(':')) {
363
- throw new Error(
364
- `Malformed addon function ID "${pikkuFuncId}": expected "namespace:funcName" format`
365
- )
366
- }
367
- if (!inspectorState.rpc.wireAddonDeclarations.has(addonNamespace)) {
368
- throw new Error(
369
- `Unknown addon namespace "${addonNamespace}" in "${pikkuFuncId}": no matching wireAddonDeclarations entry found`
370
- )
359
+ const addonNamespace = pikkuFuncId.includes(':')
360
+ ? pikkuFuncId.split(':')[0]
361
+ : null
362
+ if (addonNamespace) {
363
+ if (!inspectorState.rpc.wireAddonDeclarations.has(addonNamespace)) {
364
+ throw new Error(
365
+ `Unknown addon namespace "${addonNamespace}" in "${pikkuFuncId}": no matching wireAddonDeclarations entry found`
366
+ )
367
+ }
371
368
  }
372
369
  meta.pikkuFuncId = pikkuFuncId
373
- meta.packageName =
374
- inspectorState.rpc.wireAddonDeclarations.get(addonNamespace)!.package
370
+ if (addonNamespace) {
371
+ meta.packageName =
372
+ inspectorState.rpc.wireAddonDeclarations.get(
373
+ addonNamespace
374
+ )!.package
375
+ }
375
376
  } else {
376
377
  pikkuFuncId = extractFunctionName(
377
378
  prop.initializer,
@@ -57,12 +57,7 @@ export const addFileWithFactory = (
57
57
  })
58
58
  methods.set(fileName, variables)
59
59
 
60
- // Extract singleton services for CreateWireServices factories
61
- if (
62
- expectedTypeName === 'CreateWireServices' &&
63
- state &&
64
- callExpression.arguments.length > 0
65
- ) {
60
+ if (state && callExpression.arguments.length > 0) {
66
61
  const firstArg = callExpression.arguments[0]
67
62
  let functionNode:
68
63
  | ts.ArrowFunction
@@ -75,10 +70,34 @@ export const addFileWithFactory = (
75
70
  functionNode = firstArg
76
71
  }
77
72
 
78
- if (functionNode) {
73
+ // Extract singleton services for CreateWireServices factories
74
+ if (expectedTypeName === 'CreateWireServices' && functionNode) {
79
75
  const servicesMeta = extractServicesFromFunction(functionNode)
80
76
  state.wireServicesMeta.set(variableName, servicesMeta.services)
81
77
  }
78
+
79
+ // Extract existing services an addon needs from the parent
80
+ // (second parameter of pikkuAddonServices callback)
81
+ if (
82
+ wrapperFunctionName === 'pikkuAddonServices' &&
83
+ functionNode &&
84
+ functionNode.parameters.length >= 2
85
+ ) {
86
+ const secondParam = functionNode.parameters[1]
87
+ if (secondParam && ts.isObjectBindingPattern(secondParam.name)) {
88
+ for (const elem of secondParam.name.elements) {
89
+ const name =
90
+ elem.propertyName && ts.isIdentifier(elem.propertyName)
91
+ ? elem.propertyName.text
92
+ : ts.isIdentifier(elem.name)
93
+ ? elem.name.text
94
+ : undefined
95
+ if (name) {
96
+ state.addonRequiredParentServices.push(name)
97
+ }
98
+ }
99
+ }
100
+ }
82
101
  }
83
102
 
84
103
  return // Early return since we found a match
@@ -794,6 +794,8 @@ export const addFunctions: AddWiring = (
794
794
  middleware,
795
795
  permissions,
796
796
  isDirectFunction,
797
+ sourceFile: node.getSourceFile().fileName,
798
+ exportedName: exportedName || undefined,
797
799
  }
798
800
 
799
801
  // Populate node metadata if node config is present
@@ -815,18 +817,16 @@ export const addFunctions: AddWiring = (
815
817
 
816
818
  if (mcpEnabled) {
817
819
  if (!description) {
818
- logger.critical(
819
- ErrorCode.MISSING_DESCRIPTION,
820
+ logger.warn(
820
821
  `MCP tool '${name}' is missing a description.`
821
822
  )
822
- return
823
823
  }
824
824
  state.mcpEndpoints.files.add(node.getSourceFile().fileName)
825
825
  state.mcpEndpoints.toolsMeta[name] = {
826
826
  pikkuFuncId,
827
827
  name,
828
828
  title: title || undefined,
829
- description,
829
+ description: description || undefined,
830
830
  summary,
831
831
  errors,
832
832
  tags,
@@ -205,7 +205,11 @@ export function registerHTTPRoute({
205
205
  }
206
206
 
207
207
  const packageName = ts.isIdentifier(funcInitializer)
208
- ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
208
+ ? resolveAddonName(
209
+ funcInitializer,
210
+ checker,
211
+ state.rpc.wireAddonDeclarations
212
+ )
209
213
  : null
210
214
 
211
215
  ensureFunctionMetadata(
@@ -327,6 +331,7 @@ export function registerHTTPRoute({
327
331
  pikkuFuncId: funcName,
328
332
  ...(packageName && { packageName }),
329
333
  route: fullRoute,
334
+ sourceFile: sourceFile.fileName,
330
335
  method: method as HTTPMethod,
331
336
  params: params.length > 0 ? params : undefined,
332
337
  query: query.length > 0 ? query : undefined,
@@ -114,6 +114,11 @@ export const addMCPPrompt: AddWiring = (
114
114
  const inputSchema = fnMeta.inputs?.[0] || null
115
115
  const outputSchema = fnMeta.outputs?.[0] || null
116
116
 
117
+ if (!fnMeta.outputSchemaName) {
118
+ fnMeta.outputSchemaName = 'MCPPromptResponse'
119
+ fnMeta.outputs = ['MCPPromptResponse']
120
+ }
121
+
117
122
  // --- resolve middleware ---
118
123
  const middleware = resolveMiddleware(state, obj, tags, checker)
119
124
 
@@ -131,6 +131,11 @@ export const addMCPResource: AddWiring = (
131
131
  const inputSchema = fnMeta.inputs?.[0] || null
132
132
  const outputSchema = fnMeta.outputs?.[0] || null
133
133
 
134
+ if (!fnMeta.outputSchemaName) {
135
+ fnMeta.outputSchemaName = 'MCPResourceResponse'
136
+ fnMeta.outputs = ['MCPResourceResponse']
137
+ }
138
+
134
139
  // --- resolve middleware ---
135
140
  const middleware = resolveMiddleware(state, obj, tags, checker)
136
141
 
@@ -67,7 +67,11 @@ export const addQueueWorker: AddWiring = (logger, node, checker, state) => {
67
67
  }
68
68
 
69
69
  const packageName = ts.isIdentifier(funcInitializer)
70
- ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
70
+ ? resolveAddonName(
71
+ funcInitializer,
72
+ checker,
73
+ state.rpc.wireAddonDeclarations
74
+ )
71
75
  : null
72
76
 
73
77
  if (!name) {
@@ -27,8 +27,8 @@ export function addRPCInvocations(
27
27
  if (ts.isCallExpression(node)) {
28
28
  const { expression, arguments: args } = node
29
29
 
30
- // Check for addon('namespace:function') calls
31
- if (ts.isIdentifier(expression) && expression.text === 'addon') {
30
+ // Check for ref('name') calls
31
+ if (ts.isIdentifier(expression) && expression.text === 'ref') {
32
32
  const [firstArg] = args
33
33
  if (firstArg && ts.isStringLiteral(firstArg)) {
34
34
  const functionRef = firstArg.text
@@ -46,7 +46,7 @@ function hasInlineSteps(steps: WorkflowStepMeta[]): boolean {
46
46
  /**
47
47
  * Recursively collect all RPC names from workflow steps
48
48
  */
49
- function collectInvokedRPCs(
49
+ export function collectInvokedRPCs(
50
50
  steps: WorkflowStepMeta[],
51
51
  rpcs: Set<string>
52
52
  ): void {
@@ -97,7 +97,7 @@ function getWorkflowInvocations(
97
97
  const stepNameArg = args[0]
98
98
  const secondArg = args[1]
99
99
  const optionsArg =
100
- args.length >= 3 ? args[args.length - 1] : undefined
100
+ args.length >= 4 ? args[args.length - 1] : undefined
101
101
 
102
102
  const stepName = extractStringLiteral(stepNameArg, checker)
103
103
  const description =
@@ -210,6 +210,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
210
210
  let description: string | undefined
211
211
  let errors: string[] | undefined
212
212
  let inline: boolean | undefined
213
+ let expose: boolean | undefined
213
214
 
214
215
  if (ts.isObjectLiteralExpression(firstArg)) {
215
216
  const metadata = getCommonWireMetaData(
@@ -228,6 +229,8 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
228
229
  if (inlineProp === true) {
229
230
  inline = true
230
231
  }
232
+
233
+ expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
231
234
  }
232
235
 
233
236
  // Validate that we got a valid function
@@ -334,5 +337,22 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
334
337
  errors,
335
338
  tags,
336
339
  inline,
340
+ expose,
341
+ }
342
+
343
+ // Workflow functions require platform services that aren't visible
344
+ // through parameter destructuring (they're accessed via workflow.do/sleep)
345
+ const funcMeta = state.functions.meta[pikkuFuncId]
346
+ if (funcMeta?.services) {
347
+ for (const svc of [
348
+ 'workflowService',
349
+ 'workflowRunService',
350
+ 'schedulerService',
351
+ 'queueService',
352
+ ]) {
353
+ if (!funcMeta.services.services.includes(svc)) {
354
+ funcMeta.services.services.push(svc)
355
+ }
356
+ }
337
357
  }
338
358
  }
package/src/inspector.ts CHANGED
@@ -59,6 +59,7 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
59
59
  singletonServicesFactories: new Map(),
60
60
  wireServicesFactories: new Map(),
61
61
  wireServicesMeta: new Map(),
62
+ addonRequiredParentServices: [],
62
63
  configFactories: new Map(),
63
64
  filesAndMethods: {},
64
65
  filesAndMethodsErrors: new Map(),
package/src/types.ts CHANGED
@@ -306,6 +306,7 @@ export interface InspectorState {
306
306
  singletonServicesFactories: PathToNameAndType
307
307
  wireServicesFactories: PathToNameAndType
308
308
  wireServicesMeta: Map<string, string[]> // variable name -> singleton services consumed
309
+ addonRequiredParentServices: string[] // services an addon needs from the parent (extracted from pikkuAddonServices 2nd param)
309
310
  configFactories: PathToNameAndType
310
311
  filesAndMethods: InspectorFilesAndMethods
311
312
  filesAndMethodsErrors: Map<string, PathToNameAndType>