@pikku/inspector 0.12.2 → 0.12.4

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 (70) hide show
  1. package/CHANGELOG.md +46 -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-middleware.js +6 -10
  14. package/dist/add/add-permission.js +10 -12
  15. package/dist/add/add-queue-worker.js +5 -0
  16. package/dist/add/add-schedule.js +5 -0
  17. package/dist/add/add-wire-addon.js +7 -0
  18. package/dist/add/add-workflow.js +7 -1
  19. package/dist/error-codes.d.ts +1 -0
  20. package/dist/error-codes.js +2 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/inspector.js +21 -7
  24. package/dist/types.d.ts +12 -0
  25. package/dist/utils/custom-types-generator.js +1 -0
  26. package/dist/utils/load-addon-functions-meta.d.ts +12 -0
  27. package/dist/utils/load-addon-functions-meta.js +76 -0
  28. package/dist/utils/post-process.d.ts +9 -0
  29. package/dist/utils/post-process.js +72 -0
  30. package/dist/utils/resolve-function-meta.d.ts +11 -0
  31. package/dist/utils/resolve-function-meta.js +17 -0
  32. package/dist/utils/schema-generator.js +26 -6
  33. package/dist/utils/serialize-inspector-state.d.ts +2 -0
  34. package/dist/utils/serialize-inspector-state.js +5 -0
  35. package/dist/utils/serialize-mcp-json.js +13 -7
  36. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +1 -0
  37. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +2 -0
  38. package/dist/visit.js +2 -0
  39. package/package.json +4 -3
  40. package/src/add/add-ai-agent.ts +6 -0
  41. package/src/add/add-approval-description.ts +76 -0
  42. package/src/add/add-channel.ts +44 -4
  43. package/src/add/add-cli.ts +108 -21
  44. package/src/add/add-file-with-factory.ts +1 -0
  45. package/src/add/add-functions.ts +28 -3
  46. package/src/add/add-gateway.ts +6 -0
  47. package/src/add/add-http-route.ts +6 -0
  48. package/src/add/add-mcp-prompt.ts +6 -0
  49. package/src/add/add-mcp-resource.ts +6 -0
  50. package/src/add/add-middleware.ts +6 -14
  51. package/src/add/add-permission.ts +10 -16
  52. package/src/add/add-queue-worker.ts +6 -0
  53. package/src/add/add-schedule.ts +6 -0
  54. package/src/add/add-wire-addon.ts +8 -0
  55. package/src/add/add-workflow.ts +11 -1
  56. package/src/error-codes.ts +3 -0
  57. package/src/index.ts +1 -0
  58. package/src/inspector.ts +33 -6
  59. package/src/types.ts +13 -0
  60. package/src/utils/custom-types-generator.ts +1 -0
  61. package/src/utils/load-addon-functions-meta.ts +94 -0
  62. package/src/utils/post-process.ts +84 -0
  63. package/src/utils/resolve-function-meta.ts +25 -0
  64. package/src/utils/schema-generator.ts +38 -10
  65. package/src/utils/serialize-inspector-state.ts +7 -0
  66. package/src/utils/serialize-mcp-json.ts +12 -7
  67. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +1 -0
  68. package/src/utils/workflow/graph/workflow-graph.types.ts +2 -0
  69. package/src/visit.ts +2 -0
  70. package/tsconfig.tsbuildinfo +1 -1
@@ -301,6 +301,7 @@ export function convertDslToGraph(workflowName, meta) {
301
301
  source,
302
302
  description: meta.description,
303
303
  tags: meta.tags,
304
+ inline: meta.inline,
304
305
  context: meta.context,
305
306
  nodes: nodesRecord,
306
307
  entryNodeIds,
@@ -141,6 +141,8 @@ export interface SerializedWorkflowGraph {
141
141
  description?: string;
142
142
  /** Tags for organization */
143
143
  tags?: string[];
144
+ /** If true, workflow always executes inline without queues */
145
+ inline?: boolean;
144
146
  /** Workflow context/state variables (from Zod schema) */
145
147
  context?: WorkflowContext;
146
148
  /** Serialized nodes */
package/dist/visit.js CHANGED
@@ -21,6 +21,7 @@ import { addSecret, addOAuth2Credential } from './add/add-secret.js';
21
21
  import { addVariable } from './add/add-variable.js';
22
22
  import { addWorkflowGraph } from './add/add-workflow-graph.js';
23
23
  import { addAIAgent } from './add/add-ai-agent.js';
24
+ import { addApprovalDescription } from './add/add-approval-description.js';
24
25
  export const visitSetup = (logger, checker, node, state, options) => {
25
26
  addFileExtendsCoreType(node, checker, state.singletonServicesTypeImportMap, 'CoreSingletonServices', state);
26
27
  addFileExtendsCoreType(node, checker, state.wireServicesTypeImportMap, 'CoreServices', state);
@@ -33,6 +34,7 @@ export const visitSetup = (logger, checker, node, state, options) => {
33
34
  addWireAddon(node, state, logger);
34
35
  addMiddleware(logger, node, checker, state, options);
35
36
  addPermission(logger, node, checker, state, options);
37
+ addApprovalDescription(logger, node, checker, state, options);
36
38
  addWorkflow(logger, node, checker, state, options);
37
39
  ts.forEachChild(node, (child) => visitSetup(logger, checker, child, state, options));
38
40
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -30,11 +30,12 @@
30
30
  "release": "yarn build && npm test",
31
31
  "test": "bash run-tests.sh",
32
32
  "test:watch": "bash run-tests.sh --watch",
33
- "test:coverage": "bash run-tests.sh --coverage"
33
+ "test:coverage": "bash run-tests.sh --coverage",
34
+ "prepublishOnly": "yarn build"
34
35
  },
35
36
  "dependencies": {
36
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
37
- "@pikku/core": "^0.12.2",
38
+ "@pikku/core": "^0.12.9",
38
39
  "path-to-regexp": "^8.3.0",
39
40
  "ts-json-schema-generator": "^2.5.0",
40
41
  "tsx": "^4.21.0",
@@ -268,6 +268,9 @@ export const addAIAgent: AddWiring = (
268
268
  | number
269
269
  | null
270
270
  const toolChoiceValue = getPropertyValue(obj, 'toolChoice') as string | null
271
+ const dynamicWorkflowsValue = getPropertyValue(obj, 'dynamicWorkflows') as
272
+ | string
273
+ | null
271
274
  const toolsValue = resolveToolReferences(
272
275
  obj,
273
276
  checker,
@@ -455,6 +458,9 @@ export const addAIAgent: AddWiring = (
455
458
  }),
456
459
  ...(toolsValue !== null && { tools: toolsValue }),
457
460
  ...(agentsValue !== null && { agents: agentsValue }),
461
+ ...(dynamicWorkflowsValue !== null && {
462
+ dynamicWorkflows: dynamicWorkflowsValue as 'read' | 'always' | 'ask',
463
+ }),
458
464
  tags,
459
465
  inputSchema,
460
466
  outputSchema,
@@ -0,0 +1,76 @@
1
+ import * as ts from 'typescript'
2
+ import type { AddWiring } from '../types.js'
3
+ import { extractFunctionName } from '../utils/extract-function-name.js'
4
+ import {
5
+ extractServicesFromFunction,
6
+ extractUsedWires,
7
+ } from '../utils/extract-services.js'
8
+
9
+ /**
10
+ * Inspect pikkuApprovalDescription() calls and extract metadata
11
+ */
12
+ export const addApprovalDescription: AddWiring = (
13
+ logger,
14
+ node,
15
+ checker,
16
+ state
17
+ ) => {
18
+ if (!ts.isCallExpression(node)) return
19
+
20
+ const { expression, arguments: args } = node
21
+
22
+ if (!ts.isIdentifier(expression)) return
23
+ if (expression.text !== 'pikkuApprovalDescription') return
24
+
25
+ const arg = args[0]
26
+ if (!arg) return
27
+
28
+ let actualHandler: ts.ArrowFunction | ts.FunctionExpression
29
+
30
+ if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
31
+ actualHandler = arg
32
+ } else {
33
+ logger.error(`• Handler for pikkuApprovalDescription is not a function.`)
34
+ return
35
+ }
36
+
37
+ const services = extractServicesFromFunction(actualHandler)
38
+ const wires = extractUsedWires(actualHandler, 1)
39
+ let { pikkuFuncId, exportedName } = extractFunctionName(
40
+ node,
41
+ checker,
42
+ state.rootDir
43
+ )
44
+
45
+ if (pikkuFuncId.startsWith('__temp_')) {
46
+ if (
47
+ ts.isVariableDeclaration(node.parent) &&
48
+ ts.isIdentifier(node.parent.name)
49
+ ) {
50
+ pikkuFuncId = node.parent.name.text
51
+ } else if (
52
+ ts.isPropertyAssignment(node.parent) &&
53
+ ts.isIdentifier(node.parent.name)
54
+ ) {
55
+ pikkuFuncId = node.parent.name.text
56
+ } else {
57
+ logger.error(
58
+ `• pikkuApprovalDescription() must be assigned to a variable or object property. ` +
59
+ `Extract it to a const: const myApproval = pikkuApprovalDescription(...)`
60
+ )
61
+ return
62
+ }
63
+ }
64
+
65
+ state.functions.approvalDescriptions[pikkuFuncId] = {
66
+ services,
67
+ wires: wires.wires.length > 0 || !wires.optimized ? wires : undefined,
68
+ sourceFile: node.getSourceFile().fileName,
69
+ position: node.getStart(),
70
+ exportedName,
71
+ }
72
+
73
+ logger.debug(
74
+ `• Found approval description '${pikkuFuncId}' with services: ${services.services.join(', ')}`
75
+ )
76
+ }
@@ -18,6 +18,8 @@ import {
18
18
  } from '../utils/middleware.js'
19
19
  import { extractWireNames } from '../utils/post-process.js'
20
20
  import { resolveIdentifier } from '../utils/resolve-identifier.js'
21
+ import { resolveFunctionMeta } from '../utils/resolve-function-meta.js'
22
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
21
23
  import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js'
22
24
 
23
25
  /**
@@ -92,6 +94,13 @@ function getHandlerNameFromExpression(
92
94
 
93
95
  // Handle call expressions
94
96
  if (ts.isCallExpression(expr)) {
97
+ // Handle addon('namespace:funcName') calls
98
+ if (ts.isIdentifier(expr.expression) && expr.expression.text === 'addon') {
99
+ const [firstArg] = expr.arguments
100
+ if (firstArg && ts.isStringLiteral(firstArg)) {
101
+ return firstArg.text
102
+ }
103
+ }
95
104
  const { pikkuFuncId } = extractFunctionName(expr, checker, rootDir)
96
105
  return pikkuFuncId
97
106
  }
@@ -460,11 +469,11 @@ export function addMessagesRoutes(
460
469
  continue
461
470
  }
462
471
 
463
- const fnMeta = state.functions.meta[handlerName]
472
+ const fnMeta = resolveFunctionMeta(state, handlerName)
464
473
  if (!fnMeta) {
465
474
  logger.critical(
466
475
  ErrorCode.FUNCTION_METADATA_NOT_FOUND,
467
- `No function metadata found for handler '${handlerName}'`
476
+ `No function metadata found for channel handler '${handlerName}' on route '${routeKey}'. If this is an inline function, it must be exported for the inspector to discover it.`
468
477
  )
469
478
  continue
470
479
  }
@@ -478,8 +487,17 @@ export function addMessagesRoutes(
478
487
  ? resolveMiddleware(state, init, routeTags, checker)
479
488
  : undefined
480
489
 
490
+ // Resolve package name for addon functions (e.g. 'swaggerPetstore:addPet')
491
+ const colonIdx = handlerName.indexOf(':')
492
+ const addonNs =
493
+ colonIdx !== -1 ? handlerName.substring(0, colonIdx) : null
494
+ const packageName = addonNs
495
+ ? state.rpc.wireAddonDeclarations.get(addonNs)?.package
496
+ : undefined
497
+
481
498
  result[channelKey]![routeKey] = {
482
499
  pikkuFuncId: handlerName,
500
+ packageName,
483
501
  middleware: routeMiddleware,
484
502
  }
485
503
  }
@@ -564,8 +582,12 @@ export const addChannel: AddWiring = (
564
582
  )
565
583
  return
566
584
  }
585
+ const msgPackageName = ts.isIdentifier(onMsgProp)
586
+ ? resolveAddonName(onMsgProp, checker, state.rpc.wireAddonDeclarations)
587
+ : null
567
588
  message = {
568
589
  pikkuFuncId: msgFuncId,
590
+ ...(msgPackageName && { packageName: msgPackageName }),
569
591
  }
570
592
  }
571
593
 
@@ -579,15 +601,20 @@ export const addChannel: AddWiring = (
579
601
  // --- track used functions/middleware for service aggregation ---
580
602
  // Track connect/disconnect/message handlers
581
603
  let connectFuncId: string | undefined
604
+ let connectPackageName: string | null = null
582
605
  if (connect) {
583
606
  const extracted = extractFunctionName(connect, checker, state.rootDir)
584
607
  connectFuncId = extracted.pikkuFuncId.startsWith('__temp_')
585
608
  ? makeContextBasedId('channel', name, 'connect')
586
609
  : extracted.pikkuFuncId
610
+ connectPackageName = ts.isIdentifier(connect)
611
+ ? resolveAddonName(connect, checker, state.rpc.wireAddonDeclarations)
612
+ : null
587
613
  state.serviceAggregation.usedFunctions.add(connectFuncId)
588
614
  }
589
615
 
590
616
  let disconnectFuncId: string | undefined
617
+ let disconnectPackageName: string | null = null
591
618
  if (disconnect) {
592
619
  const extracted = extractFunctionName(
593
620
  disconnect as any,
@@ -597,6 +624,9 @@ export const addChannel: AddWiring = (
597
624
  disconnectFuncId = extracted.pikkuFuncId.startsWith('__temp_')
598
625
  ? makeContextBasedId('channel', name, 'disconnect')
599
626
  : extracted.pikkuFuncId
627
+ disconnectPackageName = ts.isIdentifier(disconnect)
628
+ ? resolveAddonName(disconnect, checker, state.rpc.wireAddonDeclarations)
629
+ : null
600
630
  state.serviceAggregation.usedFunctions.add(disconnectFuncId)
601
631
  }
602
632
 
@@ -634,8 +664,18 @@ export const addChannel: AddWiring = (
634
664
  input: null,
635
665
  params: params.length ? params : undefined,
636
666
  query: query?.length ? query : undefined,
637
- connect: connectFuncId ? { pikkuFuncId: connectFuncId } : null,
638
- disconnect: disconnectFuncId ? { pikkuFuncId: disconnectFuncId } : null,
667
+ connect: connectFuncId
668
+ ? {
669
+ pikkuFuncId: connectFuncId,
670
+ ...(connectPackageName && { packageName: connectPackageName }),
671
+ }
672
+ : null,
673
+ disconnect: disconnectFuncId
674
+ ? {
675
+ pikkuFuncId: disconnectFuncId,
676
+ ...(disconnectPackageName && { packageName: disconnectPackageName }),
677
+ }
678
+ : null,
639
679
  message,
640
680
  messageWirings,
641
681
  binary: binary === undefined ? undefined : binary,
@@ -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,
@@ -274,13 +274,10 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
274
274
  state.rootDir
275
275
  )
276
276
 
277
- if (refs.length === 0) {
278
- logger.warn(`• addMiddleware('${tag}', ...) has empty middleware array`)
279
- return
280
- }
281
-
282
277
  const definitionIds = refs.map((r) => r.definitionId)
283
- renameTempDefinitions(state, definitionIds, 'tag', tag)
278
+ if (definitionIds.length > 0) {
279
+ renameTempDefinitions(state, definitionIds, 'tag', tag)
280
+ }
284
281
 
285
282
  const sourceFile = node.getSourceFile().fileName
286
283
  const instanceIds: string[] = []
@@ -384,15 +381,10 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
384
381
  state.rootDir
385
382
  )
386
383
 
387
- if (refs.length === 0) {
388
- logger.warn(
389
- `• addHTTPMiddleware('${pattern}', ...) has empty middleware array`
390
- )
391
- return
392
- }
393
-
394
384
  const definitionIds = refs.map((r) => r.definitionId)
395
- renameTempDefinitions(state, definitionIds, 'http', pattern)
385
+ if (definitionIds.length > 0) {
386
+ renameTempDefinitions(state, definitionIds, 'http', pattern)
387
+ }
396
388
 
397
389
  const sourceFile = node.getSourceFile().fileName
398
390
  const instanceIds: string[] = []