@pikku/inspector 0.12.2 → 0.12.3

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 (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/add/add-ai-agent.js +4 -0
  3. package/dist/add/add-approval-description.d.ts +5 -0
  4. package/dist/add/add-approval-description.js +52 -0
  5. package/dist/add/add-channel.js +42 -4
  6. package/dist/add/add-cli.js +73 -13
  7. package/dist/add/add-file-with-factory.js +1 -0
  8. package/dist/add/add-functions.js +22 -3
  9. package/dist/add/add-gateway.js +5 -0
  10. package/dist/add/add-http-route.js +5 -0
  11. package/dist/add/add-mcp-prompt.js +5 -0
  12. package/dist/add/add-mcp-resource.js +5 -0
  13. package/dist/add/add-queue-worker.js +5 -0
  14. package/dist/add/add-schedule.js +5 -0
  15. package/dist/add/add-wire-addon.js +7 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/inspector.js +7 -0
  19. package/dist/types.d.ts +10 -0
  20. package/dist/utils/load-addon-functions-meta.d.ts +12 -0
  21. package/dist/utils/load-addon-functions-meta.js +76 -0
  22. package/dist/utils/post-process.js +26 -0
  23. package/dist/utils/resolve-function-meta.d.ts +11 -0
  24. package/dist/utils/resolve-function-meta.js +17 -0
  25. package/dist/utils/serialize-inspector-state.d.ts +2 -0
  26. package/dist/utils/serialize-inspector-state.js +4 -0
  27. package/dist/utils/serialize-mcp-json.js +13 -7
  28. package/dist/visit.js +2 -0
  29. package/package.json +2 -2
  30. package/src/add/add-ai-agent.ts +6 -0
  31. package/src/add/add-approval-description.ts +76 -0
  32. package/src/add/add-channel.ts +44 -4
  33. package/src/add/add-cli.ts +108 -21
  34. package/src/add/add-file-with-factory.ts +1 -0
  35. package/src/add/add-functions.ts +28 -3
  36. package/src/add/add-gateway.ts +6 -0
  37. package/src/add/add-http-route.ts +6 -0
  38. package/src/add/add-mcp-prompt.ts +6 -0
  39. package/src/add/add-mcp-resource.ts +6 -0
  40. package/src/add/add-queue-worker.ts +6 -0
  41. package/src/add/add-schedule.ts +6 -0
  42. package/src/add/add-wire-addon.ts +8 -0
  43. package/src/index.ts +1 -0
  44. package/src/inspector.ts +12 -0
  45. package/src/types.ts +11 -0
  46. package/src/utils/load-addon-functions-meta.ts +94 -0
  47. package/src/utils/post-process.ts +25 -0
  48. package/src/utils/resolve-function-meta.ts +25 -0
  49. package/src/utils/serialize-inspector-state.ts +6 -0
  50. package/src/utils/serialize-mcp-json.ts +12 -7
  51. package/src/visit.ts +2 -0
  52. package/tsconfig.tsbuildinfo +1 -1
@@ -12,9 +12,11 @@ import {
12
12
  makeContextBasedId,
13
13
  } from '../utils/extract-function-name.js'
14
14
  import { resolveMiddleware } from '../utils/middleware.js'
15
+ import { resolveFunctionMeta } from '../utils/resolve-function-meta.js'
15
16
  import { extractWireNames } from '../utils/post-process.js'
16
17
  import { getPropertyValue } from '../utils/get-property-value.js'
17
18
  import { resolveIdentifier } from '../utils/resolve-identifier.js'
19
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
18
20
  import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js'
19
21
 
20
22
  // Track if we've warned about missing Config type to avoid duplicate warnings
@@ -344,15 +346,53 @@ function processCommand(
344
346
  const propName = prop.name.text
345
347
 
346
348
  if (propName === 'func') {
347
- pikkuFuncId = extractFunctionName(
348
- prop.initializer,
349
- typeChecker,
350
- inspectorState.rootDir
351
- ).pikkuFuncId
352
- if (pikkuFuncId.startsWith('__temp_')) {
353
- pikkuFuncId = makeContextBasedId('cli', programName, ...fullPath)
349
+ if (
350
+ ts.isCallExpression(prop.initializer) &&
351
+ ts.isIdentifier(prop.initializer.expression) &&
352
+ prop.initializer.expression.text === 'addon'
353
+ ) {
354
+ const [firstArg] = prop.initializer.arguments
355
+ if (!firstArg || !ts.isStringLiteral(firstArg)) {
356
+ throw new Error(
357
+ `addon() call requires a string literal argument in the form "namespace:funcName"`
358
+ )
359
+ }
360
+ 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
+ )
371
+ }
372
+ meta.pikkuFuncId = pikkuFuncId
373
+ meta.packageName =
374
+ inspectorState.rpc.wireAddonDeclarations.get(addonNamespace)!.package
375
+ } else {
376
+ pikkuFuncId = extractFunctionName(
377
+ prop.initializer,
378
+ typeChecker,
379
+ inspectorState.rootDir
380
+ ).pikkuFuncId
381
+ if (pikkuFuncId.startsWith('__temp_')) {
382
+ pikkuFuncId = makeContextBasedId('cli', programName, ...fullPath)
383
+ }
384
+ meta.pikkuFuncId = pikkuFuncId
385
+ const cliPackageName = ts.isIdentifier(prop.initializer)
386
+ ? resolveAddonName(
387
+ prop.initializer,
388
+ typeChecker,
389
+ inspectorState.rpc.wireAddonDeclarations
390
+ )
391
+ : null
392
+ if (cliPackageName) {
393
+ meta.packageName = cliPackageName
394
+ }
354
395
  }
355
- meta.pikkuFuncId = pikkuFuncId
356
396
  } else if (
357
397
  propName === 'options' &&
358
398
  ts.isObjectLiteralExpression(prop.initializer)
@@ -573,22 +613,31 @@ function processOptions(
573
613
  }
574
614
 
575
615
  // Extract enum values from the function input type if available
576
- // Get the input type if we have a pikkuFuncId
577
- let inputTypes: ts.Type[] | undefined
616
+ let derivedChoices: string[] | null = null
617
+
578
618
  if (pikkuFuncId) {
579
- inputTypes = inspectorState.typesLookup.get(pikkuFuncId)
580
- }
619
+ // 1. Try TypeScript types first (most precise — handles unions, TS enums)
620
+ const inputTypes = inspectorState.typesLookup.get(pikkuFuncId)
621
+ if (inputTypes && inputTypes.length > 0) {
622
+ derivedChoices = extractEnumFromPropertyType(
623
+ inputTypes[0]!,
624
+ optionName,
625
+ typeChecker
626
+ )
627
+ }
581
628
 
582
- let derivedChoices: string[] | null = null
629
+ // 2. Fallback: try JSON schema (works for addon functions)
630
+ if (!derivedChoices) {
631
+ derivedChoices = extractEnumFromJsonSchema(
632
+ inspectorState,
633
+ pikkuFuncId,
634
+ optionName
635
+ )
636
+ }
637
+ }
583
638
 
584
- if (inputTypes && inputTypes.length > 0) {
585
- derivedChoices = extractEnumFromPropertyType(
586
- inputTypes[0]!,
587
- optionName,
588
- typeChecker
589
- )
590
- } else {
591
- // Fallback: try to extract from Config type
639
+ // 3. Last resort: try Config type
640
+ if (!derivedChoices) {
592
641
  derivedChoices = extractEnumFromConfigType(
593
642
  logger,
594
643
  optionName,
@@ -750,6 +799,44 @@ function extractEnumFromConfigType(
750
799
  return extractEnumFromPropertyType(configType, propertyName, typeChecker)
751
800
  }
752
801
 
802
+ /**
803
+ * Extracts enum values from the function's JSON schema.
804
+ * Works for addon functions whose schemas are generated from OpenAPI/Zod.
805
+ */
806
+ function extractEnumFromJsonSchema(
807
+ inspectorState: InspectorState,
808
+ pikkuFuncId: string,
809
+ propertyName: string
810
+ ): string[] | null {
811
+ const fnMeta = resolveFunctionMeta(inspectorState, pikkuFuncId)
812
+ if (!fnMeta?.inputSchemaName) return null
813
+
814
+ const schema = inspectorState.schemas[fnMeta.inputSchemaName] as any
815
+ if (!schema?.properties?.[propertyName]) return null
816
+
817
+ const prop = schema.properties[propertyName]
818
+
819
+ // Direct enum on property
820
+ if (prop.enum && Array.isArray(prop.enum)) {
821
+ const strings = prop.enum.filter((v: unknown) => typeof v === 'string')
822
+ if (strings.length > 0) return strings
823
+ }
824
+
825
+ // Array with enum items (e.g. z.array(z.enum([...])))
826
+ if (
827
+ prop.type === 'array' &&
828
+ prop.items?.enum &&
829
+ Array.isArray(prop.items.enum)
830
+ ) {
831
+ const strings = prop.items.enum.filter(
832
+ (v: unknown) => typeof v === 'string'
833
+ )
834
+ if (strings.length > 0) return strings
835
+ }
836
+
837
+ return null
838
+ }
839
+
753
840
  /**
754
841
  * Gets the property name from a property assignment
755
842
  */
@@ -9,6 +9,7 @@ const wrapperFunctionMap: Record<string, string> = {
9
9
  pikkuServices: 'CreateSingletonServices',
10
10
  pikkuAddonServices: 'CreateSingletonServices',
11
11
  pikkuWireServices: 'CreateWireServices',
12
+ pikkuAddonWireServices: 'CreateWireServices',
12
13
  }
13
14
 
14
15
  export const addFileWithFactory = (
@@ -339,7 +339,8 @@ export const addFunctions: AddWiring = (
339
339
  let remote: boolean | undefined
340
340
  let mcp: boolean | undefined
341
341
  let readonly_: boolean | undefined
342
- let requiresApproval: boolean | undefined
342
+ let approvalRequired: boolean | undefined
343
+ let approvalDescription: string | undefined
343
344
  let version: number | undefined
344
345
  let objectNode: ts.ObjectLiteralExpression | undefined
345
346
  let nodeDisplayName: string | null = null
@@ -421,10 +422,33 @@ export const addFunctions: AddWiring = (
421
422
  remote = getPropertyValue(firstArg, 'remote') as boolean | undefined
422
423
  mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
423
424
  readonly_ = getPropertyValue(firstArg, 'readonly') as boolean | undefined
424
- requiresApproval = getPropertyValue(firstArg, 'requiresApproval') as
425
+ approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
425
426
  | boolean
426
427
  | undefined
427
428
 
429
+ // Extract approvalDescription identifier reference
430
+ for (const prop of firstArg.properties) {
431
+ if (
432
+ ts.isPropertyAssignment(prop) &&
433
+ ts.isIdentifier(prop.name) &&
434
+ prop.name.text === 'approvalDescription' &&
435
+ ts.isIdentifier(prop.initializer)
436
+ ) {
437
+ const { pikkuFuncId: descId } = extractFunctionName(
438
+ prop.initializer,
439
+ checker,
440
+ state.rootDir
441
+ )
442
+ if (descId && !descId.startsWith('__temp_')) {
443
+ approvalDescription = descId
444
+ } else {
445
+ // Try resolving the identifier directly
446
+ approvalDescription = prop.initializer.text
447
+ }
448
+ break
449
+ }
450
+ }
451
+
428
452
  const versionRaw = getPropertyValue(firstArg, 'version')
429
453
  if (versionRaw !== null && versionRaw !== undefined) {
430
454
  const parsed = Number(versionRaw)
@@ -759,7 +783,8 @@ export const addFunctions: AddWiring = (
759
783
  remote: remote || undefined,
760
784
  mcp: mcpEnabled || undefined,
761
785
  readonly: readonly_ || undefined,
762
- requiresApproval: requiresApproval || undefined,
786
+ approvalRequired: approvalRequired || undefined,
787
+ approvalDescription: approvalDescription || undefined,
763
788
  version,
764
789
  title,
765
790
  tags: tags || undefined,
@@ -11,6 +11,7 @@ import {
11
11
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
12
12
  import { resolveMiddleware } from '../utils/middleware.js'
13
13
  import { extractWireNames } from '../utils/post-process.js'
14
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
14
15
  import type { GatewayTransportType } from '@pikku/core/gateway'
15
16
 
16
17
  import { ErrorCode } from '../error-codes.js'
@@ -68,6 +69,10 @@ export const addGateway: AddWiring = (
68
69
  pikkuFuncId = makeContextBasedId('gateway', nameValue)
69
70
  }
70
71
 
72
+ const packageName = ts.isIdentifier(funcInitializer)
73
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
74
+ : null
75
+
71
76
  if (!nameValue || !typeValue) {
72
77
  return
73
78
  }
@@ -82,6 +87,7 @@ export const addGateway: AddWiring = (
82
87
  state.gateways.files.add(node.getSourceFile().fileName)
83
88
  state.gateways.meta[nameValue] = {
84
89
  pikkuFuncId,
90
+ ...(packageName && { packageName }),
85
91
  name: nameValue,
86
92
  type: typeValue,
87
93
  route: routeValue,
@@ -21,6 +21,7 @@ import { ensureFunctionMetadata } from '../utils/ensure-function-metadata.js'
21
21
  import { ErrorCode } from '../error-codes.js'
22
22
  import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js'
23
23
  import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
24
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
24
25
 
25
26
  import type { InspectorLogger } from '../types.js'
26
27
 
@@ -203,6 +204,10 @@ export function registerHTTPRoute({
203
204
  funcName = makeContextBasedId('http', method, fullRoute)
204
205
  }
205
206
 
207
+ const packageName = ts.isIdentifier(funcInitializer)
208
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
209
+ : null
210
+
206
211
  ensureFunctionMetadata(
207
212
  state,
208
213
  funcName,
@@ -320,6 +325,7 @@ export function registerHTTPRoute({
320
325
  state.http.files.add(sourceFile.fileName)
321
326
  state.http.meta[method][fullRoute] = {
322
327
  pikkuFuncId: funcName,
328
+ ...(packageName && { packageName }),
323
329
  route: fullRoute,
324
330
  method: method as HTTPMethod,
325
331
  params: params.length > 0 ? params : undefined,
@@ -13,6 +13,7 @@ import {
13
13
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
14
14
  import { resolveMiddleware } from '../utils/middleware.js'
15
15
  import { resolvePermissions } from '../utils/permissions.js'
16
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
16
17
  import { ErrorCode } from '../error-codes.js'
17
18
 
18
19
  export const addMCPPrompt: AddWiring = (
@@ -72,6 +73,10 @@ export const addMCPPrompt: AddWiring = (
72
73
  pikkuFuncId = makeContextBasedId('mcp', 'prompt', nameValue)
73
74
  }
74
75
 
76
+ const packageName = ts.isIdentifier(funcInitializer)
77
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
78
+ : null
79
+
75
80
  ensureFunctionMetadata(
76
81
  state,
77
82
  pikkuFuncId,
@@ -128,6 +133,7 @@ export const addMCPPrompt: AddWiring = (
128
133
 
129
134
  state.mcpEndpoints.promptsMeta[nameValue] = {
130
135
  pikkuFuncId,
136
+ ...(packageName && { packageName }),
131
137
  name: nameValue,
132
138
  description,
133
139
  summary,
@@ -13,6 +13,7 @@ import {
13
13
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
14
14
  import { resolveMiddleware } from '../utils/middleware.js'
15
15
  import { resolvePermissions } from '../utils/permissions.js'
16
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
16
17
  import { ErrorCode } from '../error-codes.js'
17
18
 
18
19
  export const addMCPResource: AddWiring = (
@@ -81,6 +82,10 @@ export const addMCPResource: AddWiring = (
81
82
  pikkuFuncId = makeContextBasedId('mcp', 'resource', uriValue)
82
83
  }
83
84
 
85
+ const packageName = ts.isIdentifier(funcInitializer)
86
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
87
+ : null
88
+
84
89
  ensureFunctionMetadata(
85
90
  state,
86
91
  pikkuFuncId,
@@ -145,6 +150,7 @@ export const addMCPResource: AddWiring = (
145
150
 
146
151
  state.mcpEndpoints.resourcesMeta[uriValue] = {
147
152
  pikkuFuncId,
153
+ ...(packageName && { packageName }),
148
154
  uri: uriValue,
149
155
  title: titleValue,
150
156
  description,
@@ -11,6 +11,7 @@ import {
11
11
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
12
12
  import { resolveMiddleware } from '../utils/middleware.js'
13
13
  import { extractWireNames } from '../utils/post-process.js'
14
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
14
15
  import { ErrorCode } from '../error-codes.js'
15
16
 
16
17
  export const addQueueWorker: AddWiring = (logger, node, checker, state) => {
@@ -65,6 +66,10 @@ export const addQueueWorker: AddWiring = (logger, node, checker, state) => {
65
66
  pikkuFuncId = makeContextBasedId('queue', name)
66
67
  }
67
68
 
69
+ const packageName = ts.isIdentifier(funcInitializer)
70
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
71
+ : null
72
+
68
73
  if (!name) {
69
74
  logger.critical(
70
75
  ErrorCode.MISSING_QUEUE_NAME,
@@ -85,6 +90,7 @@ export const addQueueWorker: AddWiring = (logger, node, checker, state) => {
85
90
  state.queueWorkers.files.add(node.getSourceFile().fileName)
86
91
  state.queueWorkers.meta[name] = {
87
92
  pikkuFuncId,
93
+ ...(packageName && { packageName }),
88
94
  name,
89
95
  summary,
90
96
  description,
@@ -11,6 +11,7 @@ import {
11
11
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
12
12
  import { resolveMiddleware } from '../utils/middleware.js'
13
13
  import { extractWireNames } from '../utils/post-process.js'
14
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
14
15
 
15
16
  import { ErrorCode } from '../error-codes.js'
16
17
  export const addSchedule: AddWiring = (
@@ -71,6 +72,10 @@ export const addSchedule: AddWiring = (
71
72
  pikkuFuncId = makeContextBasedId('scheduler', nameValue)
72
73
  }
73
74
 
75
+ const packageName = ts.isIdentifier(funcInitializer)
76
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
77
+ : null
78
+
74
79
  if (!nameValue || !scheduleValue) {
75
80
  return
76
81
  }
@@ -87,6 +92,7 @@ export const addSchedule: AddWiring = (
87
92
  state.scheduledTasks.files.add(node.getSourceFile().fileName)
88
93
  state.scheduledTasks.meta[nameValue] = {
89
94
  pikkuFuncId,
95
+ ...(packageName && { packageName }),
90
96
  name: nameValue,
91
97
  schedule: scheduleValue,
92
98
  summary,
@@ -40,6 +40,7 @@ export function addWireAddon(
40
40
  let name: string | undefined
41
41
  let pkg: string | undefined
42
42
  let rpcEndpoint: string | undefined
43
+ let mcp: boolean | undefined
43
44
  let secretOverrides: Record<string, string> | undefined
44
45
  let variableOverrides: Record<string, string> | undefined
45
46
 
@@ -53,6 +54,12 @@ export function addWireAddon(
53
54
  pkg = prop.initializer.text
54
55
  } else if (key === 'rpcEndpoint' && ts.isStringLiteral(prop.initializer)) {
55
56
  rpcEndpoint = prop.initializer.text
57
+ } else if (
58
+ key === 'mcp' &&
59
+ (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
60
+ prop.initializer.kind === ts.SyntaxKind.FalseKeyword)
61
+ ) {
62
+ mcp = prop.initializer.kind === ts.SyntaxKind.TrueKeyword
56
63
  } else if (
57
64
  key === 'secretOverrides' &&
58
65
  ts.isObjectLiteralExpression(prop.initializer)
@@ -72,6 +79,7 @@ export function addWireAddon(
72
79
  state.rpc.wireAddonDeclarations.set(name, {
73
80
  package: pkg,
74
81
  rpcEndpoint,
82
+ mcp,
75
83
  secretOverrides,
76
84
  variableOverrides,
77
85
  })
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ export {
32
32
  deserializeAllDslWorkflows,
33
33
  } from './utils/workflow/dsl/index.js'
34
34
  export { getFilesAndMethods } from './utils/get-files-and-methods.js'
35
+ export { resolveFunctionMeta } from './utils/resolve-function-meta.js'
35
36
  export type {
36
37
  SerializedWorkflowGraph,
37
38
  SerializedWorkflowGraphs,
package/src/inspector.ts CHANGED
@@ -30,6 +30,10 @@ import {
30
30
  finalizeWorkflowWires,
31
31
  } from './utils/workflow/graph/finalize-workflow-wires.js'
32
32
  import { generateAllSchemas } from './utils/schema-generator.js'
33
+ import {
34
+ loadAddonFunctionsMeta,
35
+ loadAddonSchemas,
36
+ } from './utils/load-addon-functions-meta.js'
33
37
  import {
34
38
  computeContractHashes,
35
39
  extractContractsFromMeta,
@@ -63,6 +67,7 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
63
67
  typesMap: new TypesMap(),
64
68
  meta: {},
65
69
  files: new Map(),
70
+ approvalDescriptions: {},
66
71
  },
67
72
  http: {
68
73
  metaInputTypes: new Map(),
@@ -197,6 +202,7 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
197
202
  requiredSchemas: new Set(),
198
203
  openAPISpec: null,
199
204
  diagnostics: [],
205
+ addonFunctions: {},
200
206
  }
201
207
  }
202
208
 
@@ -252,6 +258,9 @@ export const inspect = async (
252
258
  `Visit setup phase completed in ${(performance.now() - startSetup).toFixed(2)}ms`
253
259
  )
254
260
 
261
+ // Load addon function metadata so wirings can reference addon functions
262
+ await loadAddonFunctionsMeta(logger, state)
263
+
255
264
  if (!options.setupOnly) {
256
265
  // Second sweep: add all transports
257
266
  const startRoutes = performance.now()
@@ -280,6 +289,9 @@ export const inspect = async (
280
289
  computeRequiredSchemas(state, options)
281
290
  }
282
291
 
292
+ // Re-load addon schemas (generateAllSchemas replaces state.schemas)
293
+ await loadAddonSchemas(logger, state)
294
+
283
295
  state.manifest.initial = options.manifest ?? null
284
296
  const contracts = extractContractsFromMeta(state.functions.meta)
285
297
  const baseManifest = state.manifest.initial ?? createEmptyManifest()
package/src/types.ts CHANGED
@@ -98,6 +98,7 @@ export interface InspectorFunctionState {
98
98
  typesMap: TypesMap
99
99
  meta: FunctionsMeta
100
100
  files: Map<string, { path: string; exportedName: string }>
101
+ approvalDescriptions: Record<string, InspectorApprovalDescriptionDefinition>
101
102
  }
102
103
 
103
104
  export interface InspectorChannelState {
@@ -140,6 +141,14 @@ export interface InspectorAIMiddlewareState {
140
141
  definitions: Record<string, InspectorMiddlewareDefinition>
141
142
  }
142
143
 
144
+ export interface InspectorApprovalDescriptionDefinition {
145
+ services: FunctionServicesMeta
146
+ wires?: FunctionWiresMeta
147
+ sourceFile: string
148
+ position: number
149
+ exportedName: string | null
150
+ }
151
+
143
152
  export interface InspectorPermissionDefinition {
144
153
  services: FunctionServicesMeta
145
154
  wires?: FunctionWiresMeta
@@ -340,6 +349,7 @@ export interface InspectorState {
340
349
  {
341
350
  package: string
342
351
  rpcEndpoint?: string
352
+ mcp?: boolean
343
353
  secretOverrides?: Record<string, string>
344
354
  variableOverrides?: Record<string, string>
345
355
  }
@@ -409,4 +419,5 @@ export interface InspectorState {
409
419
  requiredSchemas: Set<string>
410
420
  openAPISpec: Record<string, any> | null
411
421
  diagnostics: InspectorDiagnostic[]
422
+ addonFunctions: Record<string, FunctionsMeta> // namespace -> addon's function metadata
412
423
  }
@@ -0,0 +1,94 @@
1
+ import { readFile, readdir } from 'fs/promises'
2
+ import { createRequire } from 'module'
3
+ import { join, dirname } from 'path'
4
+ import type { InspectorState, InspectorLogger } from '../types.js'
5
+
6
+ /**
7
+ * After the setup sweep discovers wireAddon() declarations, load each addon
8
+ * package's function metadata so that wiring handlers (channels, HTTP routes,
9
+ * schedules, etc.) can look up addon function types during the routes sweep.
10
+ */
11
+ export async function loadAddonFunctionsMeta(
12
+ logger: InspectorLogger,
13
+ state: InspectorState
14
+ ): Promise<void> {
15
+ const { wireAddonDeclarations } = state.rpc
16
+ if (wireAddonDeclarations.size === 0) return
17
+
18
+ const require = createRequire(join(state.rootDir, 'package.json'))
19
+
20
+ for (const [namespace, decl] of wireAddonDeclarations) {
21
+ try {
22
+ const metaPath = require.resolve(
23
+ `${decl.package}/.pikku/function/pikku-functions-meta.gen.json`
24
+ )
25
+ const raw = await readFile(metaPath, 'utf-8')
26
+ const meta = JSON.parse(raw)
27
+ state.addonFunctions[namespace] = meta
28
+ logger.debug(
29
+ `Loaded ${Object.keys(meta).length} addon functions for '${namespace}' from ${decl.package}`
30
+ )
31
+
32
+ // If wireAddon has mcp: true, expose addon functions with mcp: true as MCP tools
33
+ if (decl.mcp) {
34
+ for (const [funcName, funcMeta] of Object.entries<any>(meta)) {
35
+ if (funcMeta.mcp) {
36
+ const toolName = `${namespace}:${funcName}`
37
+ state.mcpEndpoints.toolsMeta[toolName] = {
38
+ pikkuFuncId: `${namespace}:${funcName}`,
39
+ name: toolName,
40
+ description: funcMeta.description || funcMeta.title || funcName,
41
+ inputSchema: funcMeta.inputSchemaName ?? null,
42
+ outputSchema: funcMeta.outputSchemaName ?? null,
43
+ tags: funcMeta.tags,
44
+ }
45
+ }
46
+ }
47
+ }
48
+ } catch (error: any) {
49
+ logger.warn(
50
+ `Failed to load addon function metadata for '${namespace}' (${decl.package}): ${error.message}`
51
+ )
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Load addon schemas into state.schemas. Called after generateAllSchemas
58
+ * to ensure addon schemas aren't overwritten.
59
+ */
60
+ export async function loadAddonSchemas(
61
+ logger: InspectorLogger,
62
+ state: InspectorState
63
+ ): Promise<void> {
64
+ const { wireAddonDeclarations } = state.rpc
65
+ if (wireAddonDeclarations.size === 0) return
66
+
67
+ const require = createRequire(join(state.rootDir, 'package.json'))
68
+
69
+ for (const [namespace, decl] of wireAddonDeclarations) {
70
+ try {
71
+ const metaPath = require.resolve(
72
+ `${decl.package}/.pikku/function/pikku-functions-meta.gen.json`
73
+ )
74
+ const schemasDir = join(dirname(metaPath), '..', 'schemas', 'schemas')
75
+ try {
76
+ const schemaFiles = await readdir(schemasDir)
77
+ for (const file of schemaFiles) {
78
+ if (!file.endsWith('.schema.json')) continue
79
+ const schemaName = file.replace('.schema.json', '')
80
+ if (!state.schemas[schemaName]) {
81
+ const schemaRaw = await readFile(join(schemasDir, file), 'utf-8')
82
+ state.schemas[schemaName] = JSON.parse(schemaRaw)
83
+ }
84
+ }
85
+ } catch {
86
+ // No schemas directory — that's fine
87
+ }
88
+ } catch (error: any) {
89
+ logger.warn(
90
+ `Failed to load addon schemas for '${namespace}' (${decl.package}): ${error.message}`
91
+ )
92
+ }
93
+ }
94
+ }
@@ -296,6 +296,31 @@ export function computeResolvedIOTypes(state: InspectorState): void {
296
296
  }
297
297
 
298
298
  state.resolvedIOTypes[pikkuFuncId] = { inputType, outputType }
299
+
300
+ if (meta.inputSchemaName && inputType !== 'null') {
301
+ meta.inputSchemaName = inputType
302
+ }
303
+ if (meta.outputSchemaName && outputType !== 'null') {
304
+ meta.outputSchemaName = outputType
305
+ }
306
+ if (meta.inputs) {
307
+ meta.inputs = meta.inputs.map((name) => {
308
+ try {
309
+ return functions.typesMap.getTypeMeta(name).uniqueName
310
+ } catch {
311
+ return name
312
+ }
313
+ })
314
+ }
315
+ if (meta.outputs) {
316
+ meta.outputs = meta.outputs.map((name) => {
317
+ try {
318
+ return functions.typesMap.getTypeMeta(name).uniqueName
319
+ } catch {
320
+ return name
321
+ }
322
+ })
323
+ }
299
324
  }
300
325
  }
301
326
 
@@ -0,0 +1,25 @@
1
+ import type { FunctionMeta, FunctionsMeta } from '@pikku/core'
2
+
3
+ /**
4
+ * Look up function metadata by pikkuFuncId, checking both local functions
5
+ * and addon functions. Addon functions use namespaced IDs like 'namespace:funcName'.
6
+ */
7
+ export function resolveFunctionMeta(
8
+ state: {
9
+ functions: { meta: FunctionsMeta }
10
+ addonFunctions: Record<string, FunctionsMeta>
11
+ },
12
+ pikkuFuncId: string
13
+ ): FunctionMeta | undefined {
14
+ // Check local functions first
15
+ const local = state.functions.meta[pikkuFuncId]
16
+ if (local) return local
17
+
18
+ // Check addon functions (namespaced like 'swaggerPetstore:addPet')
19
+ const colonIndex = pikkuFuncId.indexOf(':')
20
+ if (colonIndex === -1) return undefined
21
+
22
+ const namespace = pikkuFuncId.substring(0, colonIndex)
23
+ const funcName = pikkuFuncId.substring(colonIndex + 1)
24
+ return state.addonFunctions[namespace]?.[funcName]
25
+ }