@pikku/inspector 0.12.1 → 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 (53) hide show
  1. package/CHANGELOG.md +23 -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 +44 -4
  6. package/dist/add/add-cli.js +94 -18
  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.d.ts +2 -0
  10. package/dist/add/add-gateway.js +62 -0
  11. package/dist/add/add-http-route.js +5 -0
  12. package/dist/add/add-mcp-prompt.js +5 -0
  13. package/dist/add/add-mcp-resource.js +5 -0
  14. package/dist/add/add-queue-worker.js +5 -0
  15. package/dist/add/add-schedule.js +5 -0
  16. package/dist/add/add-wire-addon.js +7 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1 -0
  19. package/dist/inspector.js +11 -0
  20. package/dist/types.d.ts +15 -0
  21. package/dist/utils/load-addon-functions-meta.d.ts +12 -0
  22. package/dist/utils/load-addon-functions-meta.js +76 -0
  23. package/dist/utils/post-process.js +26 -0
  24. package/dist/utils/resolve-function-meta.d.ts +11 -0
  25. package/dist/utils/resolve-function-meta.js +17 -0
  26. package/dist/utils/serialize-inspector-state.d.ts +6 -0
  27. package/dist/utils/serialize-inspector-state.js +12 -0
  28. package/dist/utils/serialize-mcp-json.js +13 -7
  29. package/dist/visit.js +4 -0
  30. package/package.json +3 -3
  31. package/src/add/add-ai-agent.ts +6 -0
  32. package/src/add/add-approval-description.ts +76 -0
  33. package/src/add/add-channel.ts +47 -11
  34. package/src/add/add-cli.ts +140 -30
  35. package/src/add/add-file-with-factory.ts +1 -0
  36. package/src/add/add-functions.ts +28 -3
  37. package/src/add/add-gateway.ts +101 -0
  38. package/src/add/add-http-route.ts +6 -0
  39. package/src/add/add-mcp-prompt.ts +6 -0
  40. package/src/add/add-mcp-resource.ts +6 -0
  41. package/src/add/add-queue-worker.ts +6 -0
  42. package/src/add/add-schedule.ts +6 -0
  43. package/src/add/add-wire-addon.ts +8 -0
  44. package/src/index.ts +1 -0
  45. package/src/inspector.ts +16 -0
  46. package/src/types.ts +16 -0
  47. package/src/utils/load-addon-functions-meta.ts +94 -0
  48. package/src/utils/post-process.ts +25 -0
  49. package/src/utils/resolve-function-meta.ts +25 -0
  50. package/src/utils/serialize-inspector-state.ts +18 -0
  51. package/src/utils/serialize-mcp-json.ts +12 -7
  52. package/src/visit.ts +4 -0
  53. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 508a796: Fix MCP server not exposing addon tools: resolve namespaced function IDs in MCP runner, load addon schemas after schema generation, and use resolveFunctionMeta for MCP JSON serialization
8
+ - 387b2ee: Add approval description inspection, track packageName on wire metadata, and resolve addon package names in channel/RPC wirings
9
+ - Updated dependencies [387b2ee]
10
+ - Updated dependencies [32ed003]
11
+ - Updated dependencies [7d369f3]
12
+ - Updated dependencies [508a796]
13
+ - Updated dependencies [ffe83af]
14
+ - Updated dependencies [c7ff141]
15
+ - @pikku/core@0.12.3
16
+
17
+ ## 0.12.2
18
+
19
+ ### Patch Changes
20
+
21
+ - 3e04565: chore: update dependencies to latest minor/patch versions
22
+ - Updated dependencies [cc4c9e9]
23
+ - Updated dependencies [3e04565]
24
+ - @pikku/core@0.12.2
25
+
3
26
  ## 0.12.1
4
27
 
5
28
  ### Patch Changes
@@ -167,6 +167,7 @@ export const addAIAgent = (logger, node, checker, state, options) => {
167
167
  const maxStepsValue = getPropertyValue(obj, 'maxSteps');
168
168
  const temperatureValue = getPropertyValue(obj, 'temperature');
169
169
  const toolChoiceValue = getPropertyValue(obj, 'toolChoice');
170
+ const dynamicWorkflowsValue = getPropertyValue(obj, 'dynamicWorkflows');
170
171
  const toolsValue = resolveToolReferences(obj, checker, nameValue || '', logger);
171
172
  if (toolsValue) {
172
173
  for (const toolName of toolsValue) {
@@ -301,6 +302,9 @@ export const addAIAgent = (logger, node, checker, state, options) => {
301
302
  }),
302
303
  ...(toolsValue !== null && { tools: toolsValue }),
303
304
  ...(agentsValue !== null && { agents: agentsValue }),
305
+ ...(dynamicWorkflowsValue !== null && {
306
+ dynamicWorkflows: dynamicWorkflowsValue,
307
+ }),
304
308
  tags,
305
309
  inputSchema,
306
310
  outputSchema,
@@ -0,0 +1,5 @@
1
+ import type { AddWiring } from '../types.js';
2
+ /**
3
+ * Inspect pikkuApprovalDescription() calls and extract metadata
4
+ */
5
+ export declare const addApprovalDescription: AddWiring;
@@ -0,0 +1,52 @@
1
+ import * as ts from 'typescript';
2
+ import { extractFunctionName } from '../utils/extract-function-name.js';
3
+ import { extractServicesFromFunction, extractUsedWires, } from '../utils/extract-services.js';
4
+ /**
5
+ * Inspect pikkuApprovalDescription() calls and extract metadata
6
+ */
7
+ export const addApprovalDescription = (logger, node, checker, state) => {
8
+ if (!ts.isCallExpression(node))
9
+ return;
10
+ const { expression, arguments: args } = node;
11
+ if (!ts.isIdentifier(expression))
12
+ return;
13
+ if (expression.text !== 'pikkuApprovalDescription')
14
+ return;
15
+ const arg = args[0];
16
+ if (!arg)
17
+ return;
18
+ let actualHandler;
19
+ if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
20
+ actualHandler = arg;
21
+ }
22
+ else {
23
+ logger.error(`• Handler for pikkuApprovalDescription is not a function.`);
24
+ return;
25
+ }
26
+ const services = extractServicesFromFunction(actualHandler);
27
+ const wires = extractUsedWires(actualHandler, 1);
28
+ let { pikkuFuncId, exportedName } = extractFunctionName(node, checker, state.rootDir);
29
+ if (pikkuFuncId.startsWith('__temp_')) {
30
+ if (ts.isVariableDeclaration(node.parent) &&
31
+ ts.isIdentifier(node.parent.name)) {
32
+ pikkuFuncId = node.parent.name.text;
33
+ }
34
+ else if (ts.isPropertyAssignment(node.parent) &&
35
+ ts.isIdentifier(node.parent.name)) {
36
+ pikkuFuncId = node.parent.name.text;
37
+ }
38
+ else {
39
+ logger.error(`• pikkuApprovalDescription() must be assigned to a variable or object property. ` +
40
+ `Extract it to a const: const myApproval = pikkuApprovalDescription(...)`);
41
+ return;
42
+ }
43
+ }
44
+ state.functions.approvalDescriptions[pikkuFuncId] = {
45
+ services,
46
+ wires: wires.wires.length > 0 || !wires.optimized ? wires : undefined,
47
+ sourceFile: node.getSourceFile().fileName,
48
+ position: node.getStart(),
49
+ exportedName,
50
+ };
51
+ logger.debug(`• Found approval description '${pikkuFuncId}' with services: ${services.services.join(', ')}`);
52
+ };
@@ -7,6 +7,8 @@ import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
7
7
  import { resolveMiddleware, resolveChannelMiddleware, } from '../utils/middleware.js';
8
8
  import { extractWireNames } from '../utils/post-process.js';
9
9
  import { resolveIdentifier } from '../utils/resolve-identifier.js';
10
+ import { resolveFunctionMeta } from '../utils/resolve-function-meta.js';
11
+ import { resolveAddonName } from '../utils/resolve-addon-package.js';
10
12
  import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js';
11
13
  /**
12
14
  * Safely get the "initializer" expression of a property-like AST node:
@@ -63,6 +65,13 @@ function getHandlerNameFromExpression(expr, checker, rootDir) {
63
65
  }
64
66
  // Handle call expressions
65
67
  if (ts.isCallExpression(expr)) {
68
+ // Handle addon('namespace:funcName') calls
69
+ if (ts.isIdentifier(expr.expression) && expr.expression.text === 'addon') {
70
+ const [firstArg] = expr.arguments;
71
+ if (firstArg && ts.isStringLiteral(firstArg)) {
72
+ return firstArg.text;
73
+ }
74
+ }
66
75
  const { pikkuFuncId } = extractFunctionName(expr, checker, rootDir);
67
76
  return pikkuFuncId;
68
77
  }
@@ -315,9 +324,9 @@ export function addMessagesRoutes(logger, obj, state, checker) {
315
324
  logger.error(`Could not resolve handler for message route '${routeKey}'`);
316
325
  continue;
317
326
  }
318
- const fnMeta = state.functions.meta[handlerName];
327
+ const fnMeta = resolveFunctionMeta(state, handlerName);
319
328
  if (!fnMeta) {
320
- logger.critical(ErrorCode.FUNCTION_METADATA_NOT_FOUND, `No function metadata found for handler '${handlerName}'`);
329
+ logger.critical(ErrorCode.FUNCTION_METADATA_NOT_FOUND, `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.`);
321
330
  continue;
322
331
  }
323
332
  // Resolve middleware and permissions for this route
@@ -328,8 +337,15 @@ export function addMessagesRoutes(logger, obj, state, checker) {
328
337
  const routeMiddleware = ts.isObjectLiteralExpression(init)
329
338
  ? resolveMiddleware(state, init, routeTags, checker)
330
339
  : undefined;
340
+ // Resolve package name for addon functions (e.g. 'swaggerPetstore:addPet')
341
+ const colonIdx = handlerName.indexOf(':');
342
+ const addonNs = colonIdx !== -1 ? handlerName.substring(0, colonIdx) : null;
343
+ const packageName = addonNs
344
+ ? state.rpc.wireAddonDeclarations.get(addonNs)?.package
345
+ : undefined;
331
346
  result[channelKey][routeKey] = {
332
347
  pikkuFuncId: handlerName,
348
+ packageName,
333
349
  middleware: routeMiddleware,
334
350
  };
335
351
  }
@@ -366,6 +382,7 @@ export const addChannel = (logger, node, checker, state, options) => {
366
382
  if (disabled)
367
383
  return;
368
384
  const query = getPropertyValue(obj, 'query');
385
+ const binary = getPropertyValue(obj, 'binary');
369
386
  const connect = getPropertyAssignmentInitializer(obj, 'onConnect', true, checker);
370
387
  const disconnect = getPropertyAssignmentInitializer(obj, 'onDisconnect', true, checker);
371
388
  // default onMessage handler
@@ -381,8 +398,12 @@ export const addChannel = (logger, node, checker, state, options) => {
381
398
  logger.critical(ErrorCode.FUNCTION_METADATA_NOT_FOUND, `No function metadata found for onMessage handler '${msgFuncId}'`);
382
399
  return;
383
400
  }
401
+ const msgPackageName = ts.isIdentifier(onMsgProp)
402
+ ? resolveAddonName(onMsgProp, checker, state.rpc.wireAddonDeclarations)
403
+ : null;
384
404
  message = {
385
405
  pikkuFuncId: msgFuncId,
406
+ ...(msgPackageName && { packageName: msgPackageName }),
386
407
  };
387
408
  }
388
409
  // nested message-routes
@@ -393,19 +414,27 @@ export const addChannel = (logger, node, checker, state, options) => {
393
414
  // --- track used functions/middleware for service aggregation ---
394
415
  // Track connect/disconnect/message handlers
395
416
  let connectFuncId;
417
+ let connectPackageName = null;
396
418
  if (connect) {
397
419
  const extracted = extractFunctionName(connect, checker, state.rootDir);
398
420
  connectFuncId = extracted.pikkuFuncId.startsWith('__temp_')
399
421
  ? makeContextBasedId('channel', name, 'connect')
400
422
  : extracted.pikkuFuncId;
423
+ connectPackageName = ts.isIdentifier(connect)
424
+ ? resolveAddonName(connect, checker, state.rpc.wireAddonDeclarations)
425
+ : null;
401
426
  state.serviceAggregation.usedFunctions.add(connectFuncId);
402
427
  }
403
428
  let disconnectFuncId;
429
+ let disconnectPackageName = null;
404
430
  if (disconnect) {
405
431
  const extracted = extractFunctionName(disconnect, checker, state.rootDir);
406
432
  disconnectFuncId = extracted.pikkuFuncId.startsWith('__temp_')
407
433
  ? makeContextBasedId('channel', name, 'disconnect')
408
434
  : extracted.pikkuFuncId;
435
+ disconnectPackageName = ts.isIdentifier(disconnect)
436
+ ? resolveAddonName(disconnect, checker, state.rpc.wireAddonDeclarations)
437
+ : null;
409
438
  state.serviceAggregation.usedFunctions.add(disconnectFuncId);
410
439
  }
411
440
  if (message) {
@@ -435,10 +464,21 @@ export const addChannel = (logger, node, checker, state, options) => {
435
464
  input: null,
436
465
  params: params.length ? params : undefined,
437
466
  query: query?.length ? query : undefined,
438
- connect: connectFuncId ? { pikkuFuncId: connectFuncId } : null,
439
- disconnect: disconnectFuncId ? { pikkuFuncId: disconnectFuncId } : null,
467
+ connect: connectFuncId
468
+ ? {
469
+ pikkuFuncId: connectFuncId,
470
+ ...(connectPackageName && { packageName: connectPackageName }),
471
+ }
472
+ : null,
473
+ disconnect: disconnectFuncId
474
+ ? {
475
+ pikkuFuncId: disconnectFuncId,
476
+ ...(disconnectPackageName && { packageName: disconnectPackageName }),
477
+ }
478
+ : null,
440
479
  message,
441
480
  messageWirings,
481
+ binary: binary === undefined ? undefined : binary,
442
482
  summary,
443
483
  description,
444
484
  errors,
@@ -1,9 +1,11 @@
1
1
  import ts from 'typescript';
2
- import { extractFunctionName } from '../utils/extract-function-name.js';
2
+ import { extractFunctionName, makeContextBasedId, } from '../utils/extract-function-name.js';
3
3
  import { resolveMiddleware } from '../utils/middleware.js';
4
+ import { resolveFunctionMeta } from '../utils/resolve-function-meta.js';
4
5
  import { extractWireNames } from '../utils/post-process.js';
5
6
  import { getPropertyValue } from '../utils/get-property-value.js';
6
7
  import { resolveIdentifier } from '../utils/resolve-identifier.js';
8
+ import { resolveAddonName } from '../utils/resolve-addon-package.js';
7
9
  import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js';
8
10
  // Track if we've warned about missing Config type to avoid duplicate warnings
9
11
  const configTypeWarningShown = new Set();
@@ -95,10 +97,14 @@ function processCLIConfig(logger, node, sourceFile, typeChecker, inspectorState,
95
97
  programMeta.options = processOptions(logger, prop.initializer, typeChecker, inspectorState, options);
96
98
  }
97
99
  break;
98
- case 'render':
99
- // Extract the actual renderer function name
100
- programMeta.defaultRenderName = extractFunctionName(prop.initializer, typeChecker, inspectorState.rootDir).pikkuFuncId;
100
+ case 'render': {
101
+ let renderFuncId = extractFunctionName(prop.initializer, typeChecker, inspectorState.rootDir).pikkuFuncId;
102
+ if (renderFuncId.startsWith('__temp_')) {
103
+ renderFuncId = makeContextBasedId('cli-render', programName);
104
+ }
105
+ programMeta.defaultRenderName = renderFuncId;
101
106
  break;
107
+ }
102
108
  }
103
109
  }
104
110
  return { programName, programMeta };
@@ -156,8 +162,12 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
156
162
  if (ts.isIdentifier(node) ||
157
163
  ts.isArrowFunction(node) ||
158
164
  ts.isFunctionExpression(node)) {
165
+ let pikkuFuncId = extractFunctionName(node, typeChecker, inspectorState.rootDir).pikkuFuncId;
166
+ if (pikkuFuncId.startsWith('__temp_')) {
167
+ pikkuFuncId = makeContextBasedId('cli', programName, ...fullPath);
168
+ }
159
169
  return {
160
- pikkuFuncId: extractFunctionName(node, typeChecker, inspectorState.rootDir).pikkuFuncId,
170
+ pikkuFuncId,
161
171
  positionals: [],
162
172
  options: {},
163
173
  };
@@ -194,8 +204,38 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
194
204
  continue;
195
205
  const propName = prop.name.text;
196
206
  if (propName === 'func') {
197
- pikkuFuncId = extractFunctionName(prop.initializer, typeChecker, inspectorState.rootDir).pikkuFuncId;
198
- meta.pikkuFuncId = pikkuFuncId;
207
+ if (ts.isCallExpression(prop.initializer) &&
208
+ ts.isIdentifier(prop.initializer.expression) &&
209
+ prop.initializer.expression.text === 'addon') {
210
+ const [firstArg] = prop.initializer.arguments;
211
+ if (!firstArg || !ts.isStringLiteral(firstArg)) {
212
+ throw new Error(`addon() call requires a string literal argument in the form "namespace:funcName"`);
213
+ }
214
+ pikkuFuncId = firstArg.text;
215
+ const addonNamespace = pikkuFuncId.split(':')[0];
216
+ if (!addonNamespace || !pikkuFuncId.includes(':')) {
217
+ throw new Error(`Malformed addon function ID "${pikkuFuncId}": expected "namespace:funcName" format`);
218
+ }
219
+ if (!inspectorState.rpc.wireAddonDeclarations.has(addonNamespace)) {
220
+ throw new Error(`Unknown addon namespace "${addonNamespace}" in "${pikkuFuncId}": no matching wireAddonDeclarations entry found`);
221
+ }
222
+ meta.pikkuFuncId = pikkuFuncId;
223
+ meta.packageName =
224
+ inspectorState.rpc.wireAddonDeclarations.get(addonNamespace).package;
225
+ }
226
+ else {
227
+ pikkuFuncId = extractFunctionName(prop.initializer, typeChecker, inspectorState.rootDir).pikkuFuncId;
228
+ if (pikkuFuncId.startsWith('__temp_')) {
229
+ pikkuFuncId = makeContextBasedId('cli', programName, ...fullPath);
230
+ }
231
+ meta.pikkuFuncId = pikkuFuncId;
232
+ const cliPackageName = ts.isIdentifier(prop.initializer)
233
+ ? resolveAddonName(prop.initializer, typeChecker, inspectorState.rpc.wireAddonDeclarations)
234
+ : null;
235
+ if (cliPackageName) {
236
+ meta.packageName = cliPackageName;
237
+ }
238
+ }
199
239
  }
200
240
  else if (propName === 'options' &&
201
241
  ts.isObjectLiteralExpression(prop.initializer)) {
@@ -238,9 +278,14 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
238
278
  case 'func':
239
279
  // Already handled in first pass
240
280
  break;
241
- case 'render':
242
- meta.renderName = extractFunctionName(prop.initializer, typeChecker, inspectorState.rootDir).pikkuFuncId;
281
+ case 'render': {
282
+ let renderFuncId = extractFunctionName(prop.initializer, typeChecker, inspectorState.rootDir).pikkuFuncId;
283
+ if (renderFuncId.startsWith('__temp_')) {
284
+ renderFuncId = makeContextBasedId('cli-render', programName, ...fullPath);
285
+ }
286
+ meta.renderName = renderFuncId;
243
287
  break;
288
+ }
244
289
  case 'options':
245
290
  // Process with pikkuFuncId from first pass
246
291
  if (optionsNode) {
@@ -341,17 +386,20 @@ function processOptions(logger, node, typeChecker, inspectorState, inspectorOpti
341
386
  }
342
387
  }
343
388
  // Extract enum values from the function input type if available
344
- // Get the input type if we have a pikkuFuncId
345
- let inputTypes;
346
- if (pikkuFuncId) {
347
- inputTypes = inspectorState.typesLookup.get(pikkuFuncId);
348
- }
349
389
  let derivedChoices = null;
350
- if (inputTypes && inputTypes.length > 0) {
351
- derivedChoices = extractEnumFromPropertyType(inputTypes[0], optionName, typeChecker);
390
+ if (pikkuFuncId) {
391
+ // 1. Try TypeScript types first (most precise — handles unions, TS enums)
392
+ const inputTypes = inspectorState.typesLookup.get(pikkuFuncId);
393
+ if (inputTypes && inputTypes.length > 0) {
394
+ derivedChoices = extractEnumFromPropertyType(inputTypes[0], optionName, typeChecker);
395
+ }
396
+ // 2. Fallback: try JSON schema (works for addon functions)
397
+ if (!derivedChoices) {
398
+ derivedChoices = extractEnumFromJsonSchema(inspectorState, pikkuFuncId, optionName);
399
+ }
352
400
  }
353
- else {
354
- // Fallback: try to extract from Config type
401
+ // 3. Last resort: try Config type
402
+ if (!derivedChoices) {
355
403
  derivedChoices = extractEnumFromConfigType(logger, optionName, typeChecker, inspectorState, inspectorOptions);
356
404
  }
357
405
  // Validate and set choices
@@ -472,6 +520,34 @@ function extractEnumFromConfigType(logger, propertyName, typeChecker, inspectorS
472
520
  // Extract enum from the property
473
521
  return extractEnumFromPropertyType(configType, propertyName, typeChecker);
474
522
  }
523
+ /**
524
+ * Extracts enum values from the function's JSON schema.
525
+ * Works for addon functions whose schemas are generated from OpenAPI/Zod.
526
+ */
527
+ function extractEnumFromJsonSchema(inspectorState, pikkuFuncId, propertyName) {
528
+ const fnMeta = resolveFunctionMeta(inspectorState, pikkuFuncId);
529
+ if (!fnMeta?.inputSchemaName)
530
+ return null;
531
+ const schema = inspectorState.schemas[fnMeta.inputSchemaName];
532
+ if (!schema?.properties?.[propertyName])
533
+ return null;
534
+ const prop = schema.properties[propertyName];
535
+ // Direct enum on property
536
+ if (prop.enum && Array.isArray(prop.enum)) {
537
+ const strings = prop.enum.filter((v) => typeof v === 'string');
538
+ if (strings.length > 0)
539
+ return strings;
540
+ }
541
+ // Array with enum items (e.g. z.array(z.enum([...])))
542
+ if (prop.type === 'array' &&
543
+ prop.items?.enum &&
544
+ Array.isArray(prop.items.enum)) {
545
+ const strings = prop.items.enum.filter((v) => typeof v === 'string');
546
+ if (strings.length > 0)
547
+ return strings;
548
+ }
549
+ return null;
550
+ }
475
551
  /**
476
552
  * Gets the property name from a property assignment
477
553
  */
@@ -7,6 +7,7 @@ const wrapperFunctionMap = {
7
7
  pikkuServices: 'CreateSingletonServices',
8
8
  pikkuAddonServices: 'CreateSingletonServices',
9
9
  pikkuWireServices: 'CreateWireServices',
10
+ pikkuAddonWireServices: 'CreateWireServices',
10
11
  };
11
12
  export const addFileWithFactory = (node, checker, methods = new Map(), expectedTypeName, state) => {
12
13
  if (ts.isVariableDeclaration(node)) {
@@ -245,7 +245,8 @@ export const addFunctions = (logger, node, checker, state, options) => {
245
245
  let remote;
246
246
  let mcp;
247
247
  let readonly_;
248
- let requiresApproval;
248
+ let approvalRequired;
249
+ let approvalDescription;
249
250
  let version;
250
251
  let objectNode;
251
252
  let nodeDisplayName = null;
@@ -311,7 +312,24 @@ export const addFunctions = (logger, node, checker, state, options) => {
311
312
  remote = getPropertyValue(firstArg, 'remote');
312
313
  mcp = getPropertyValue(firstArg, 'mcp');
313
314
  readonly_ = getPropertyValue(firstArg, 'readonly');
314
- requiresApproval = getPropertyValue(firstArg, 'requiresApproval');
315
+ approvalRequired = getPropertyValue(firstArg, 'approvalRequired');
316
+ // Extract approvalDescription identifier reference
317
+ for (const prop of firstArg.properties) {
318
+ if (ts.isPropertyAssignment(prop) &&
319
+ ts.isIdentifier(prop.name) &&
320
+ prop.name.text === 'approvalDescription' &&
321
+ ts.isIdentifier(prop.initializer)) {
322
+ const { pikkuFuncId: descId } = extractFunctionName(prop.initializer, checker, state.rootDir);
323
+ if (descId && !descId.startsWith('__temp_')) {
324
+ approvalDescription = descId;
325
+ }
326
+ else {
327
+ // Try resolving the identifier directly
328
+ approvalDescription = prop.initializer.text;
329
+ }
330
+ break;
331
+ }
332
+ }
315
333
  const versionRaw = getPropertyValue(firstArg, 'version');
316
334
  if (versionRaw !== null && versionRaw !== undefined) {
317
335
  const parsed = Number(versionRaw);
@@ -565,7 +583,8 @@ export const addFunctions = (logger, node, checker, state, options) => {
565
583
  remote: remote || undefined,
566
584
  mcp: mcpEnabled || undefined,
567
585
  readonly: readonly_ || undefined,
568
- requiresApproval: requiresApproval || undefined,
586
+ approvalRequired: approvalRequired || undefined,
587
+ approvalDescription: approvalDescription || undefined,
569
588
  version,
570
589
  title,
571
590
  tags: tags || undefined,
@@ -0,0 +1,2 @@
1
+ import type { AddWiring } from '../types.js';
2
+ export declare const addGateway: AddWiring;
@@ -0,0 +1,62 @@
1
+ import * as ts from 'typescript';
2
+ import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
3
+ import { extractFunctionName, makeContextBasedId, } from '../utils/extract-function-name.js';
4
+ import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
5
+ import { resolveMiddleware } from '../utils/middleware.js';
6
+ import { extractWireNames } from '../utils/post-process.js';
7
+ import { resolveAddonName } from '../utils/resolve-addon-package.js';
8
+ import { ErrorCode } from '../error-codes.js';
9
+ export const addGateway = (logger, node, checker, state, _options) => {
10
+ if (!ts.isCallExpression(node)) {
11
+ return;
12
+ }
13
+ const args = node.arguments;
14
+ const firstArg = args[0];
15
+ const expression = node.expression;
16
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireGateway') {
17
+ return;
18
+ }
19
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) {
20
+ return;
21
+ }
22
+ const obj = firstArg;
23
+ const nameValue = getPropertyValue(obj, 'name');
24
+ const typeValue = getPropertyValue(obj, 'type');
25
+ const routeValue = getPropertyValue(obj, 'route');
26
+ const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Gateway', nameValue, logger);
27
+ if (disabled)
28
+ return;
29
+ const funcInitializer = getPropertyAssignmentInitializer(obj, 'func', true, checker);
30
+ if (!funcInitializer) {
31
+ logger.critical(ErrorCode.MISSING_FUNC, `No valid 'func' property for gateway '${nameValue}'.`);
32
+ return;
33
+ }
34
+ const extracted = extractFunctionName(funcInitializer, checker, state.rootDir);
35
+ let pikkuFuncId = extracted.pikkuFuncId;
36
+ if (pikkuFuncId.startsWith('__temp_') && nameValue) {
37
+ pikkuFuncId = makeContextBasedId('gateway', nameValue);
38
+ }
39
+ const packageName = ts.isIdentifier(funcInitializer)
40
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
41
+ : null;
42
+ if (!nameValue || !typeValue) {
43
+ return;
44
+ }
45
+ const middleware = resolveMiddleware(state, obj, tags, checker);
46
+ state.serviceAggregation.usedFunctions.add(pikkuFuncId);
47
+ extractWireNames(middleware).forEach((name) => state.serviceAggregation.usedMiddleware.add(name));
48
+ state.gateways.files.add(node.getSourceFile().fileName);
49
+ state.gateways.meta[nameValue] = {
50
+ pikkuFuncId,
51
+ ...(packageName && { packageName }),
52
+ name: nameValue,
53
+ type: typeValue,
54
+ route: routeValue,
55
+ gateway: true,
56
+ summary,
57
+ description,
58
+ errors,
59
+ tags,
60
+ middleware,
61
+ };
62
+ };
@@ -10,6 +10,7 @@ import { ensureFunctionMetadata } from '../utils/ensure-function-metadata.js';
10
10
  import { ErrorCode } from '../error-codes.js';
11
11
  import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js';
12
12
  import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
13
+ import { resolveAddonName } from '../utils/resolve-addon-package.js';
13
14
  /**
14
15
  * Extract header schema reference from headers property
15
16
  */
@@ -112,6 +113,9 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
112
113
  if (funcName.startsWith('__temp_')) {
113
114
  funcName = makeContextBasedId('http', method, fullRoute);
114
115
  }
116
+ const packageName = ts.isIdentifier(funcInitializer)
117
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
118
+ : null;
115
119
  ensureFunctionMetadata(state, funcName, fullRoute, funcInitializer, checker, extracted.isHelper);
116
120
  // Lookup existing function metadata
117
121
  const fnMeta = state.functions.meta[funcName];
@@ -168,6 +172,7 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
168
172
  state.http.files.add(sourceFile.fileName);
169
173
  state.http.meta[method][fullRoute] = {
170
174
  pikkuFuncId: funcName,
175
+ ...(packageName && { packageName }),
171
176
  route: fullRoute,
172
177
  method: method,
173
178
  params: params.length > 0 ? params : undefined,
@@ -6,6 +6,7 @@ import { extractFunctionName, makeContextBasedId, } from '../utils/extract-funct
6
6
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
7
7
  import { resolveMiddleware } from '../utils/middleware.js';
8
8
  import { resolvePermissions } from '../utils/permissions.js';
9
+ import { resolveAddonName } from '../utils/resolve-addon-package.js';
9
10
  import { ErrorCode } from '../error-codes.js';
10
11
  export const addMCPPrompt = (logger, node, checker, state, options) => {
11
12
  if (!ts.isCallExpression(node)) {
@@ -37,6 +38,9 @@ export const addMCPPrompt = (logger, node, checker, state, options) => {
37
38
  if (pikkuFuncId.startsWith('__temp_') && nameValue) {
38
39
  pikkuFuncId = makeContextBasedId('mcp', 'prompt', nameValue);
39
40
  }
41
+ const packageName = ts.isIdentifier(funcInitializer)
42
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
43
+ : null;
40
44
  ensureFunctionMetadata(state, pikkuFuncId, nameValue || undefined, funcInitializer, checker, extracted.isHelper);
41
45
  if (!nameValue) {
42
46
  logger.critical(ErrorCode.MISSING_NAME, "MCP prompt is missing the required 'name' property.");
@@ -65,6 +69,7 @@ export const addMCPPrompt = (logger, node, checker, state, options) => {
65
69
  state.mcpEndpoints.files.add(node.getSourceFile().fileName);
66
70
  state.mcpEndpoints.promptsMeta[nameValue] = {
67
71
  pikkuFuncId,
72
+ ...(packageName && { packageName }),
68
73
  name: nameValue,
69
74
  description,
70
75
  summary,
@@ -6,6 +6,7 @@ import { extractFunctionName, makeContextBasedId, } from '../utils/extract-funct
6
6
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
7
7
  import { resolveMiddleware } from '../utils/middleware.js';
8
8
  import { resolvePermissions } from '../utils/permissions.js';
9
+ import { resolveAddonName } from '../utils/resolve-addon-package.js';
9
10
  import { ErrorCode } from '../error-codes.js';
10
11
  export const addMCPResource = (logger, node, checker, state, options) => {
11
12
  if (!ts.isCallExpression(node)) {
@@ -42,6 +43,9 @@ export const addMCPResource = (logger, node, checker, state, options) => {
42
43
  if (pikkuFuncId.startsWith('__temp_') && uriValue) {
43
44
  pikkuFuncId = makeContextBasedId('mcp', 'resource', uriValue);
44
45
  }
46
+ const packageName = ts.isIdentifier(funcInitializer)
47
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
48
+ : null;
45
49
  ensureFunctionMetadata(state, pikkuFuncId, uriValue || undefined, funcInitializer, checker, extracted.isHelper);
46
50
  if (!uriValue) {
47
51
  logger.critical(ErrorCode.MISSING_URI, "MCP resource is missing the required 'uri' property.");
@@ -74,6 +78,7 @@ export const addMCPResource = (logger, node, checker, state, options) => {
74
78
  state.mcpEndpoints.files.add(node.getSourceFile().fileName);
75
79
  state.mcpEndpoints.resourcesMeta[uriValue] = {
76
80
  pikkuFuncId,
81
+ ...(packageName && { packageName }),
77
82
  uri: uriValue,
78
83
  title: titleValue,
79
84
  description,
@@ -4,6 +4,7 @@ import { extractFunctionName, makeContextBasedId, } from '../utils/extract-funct
4
4
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
5
5
  import { resolveMiddleware } from '../utils/middleware.js';
6
6
  import { extractWireNames } from '../utils/post-process.js';
7
+ import { resolveAddonName } from '../utils/resolve-addon-package.js';
7
8
  import { ErrorCode } from '../error-codes.js';
8
9
  export const addQueueWorker = (logger, node, checker, state) => {
9
10
  if (!ts.isCallExpression(node)) {
@@ -36,6 +37,9 @@ export const addQueueWorker = (logger, node, checker, state) => {
36
37
  if (pikkuFuncId.startsWith('__temp_') && name) {
37
38
  pikkuFuncId = makeContextBasedId('queue', name);
38
39
  }
40
+ const packageName = ts.isIdentifier(funcInitializer)
41
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
42
+ : null;
39
43
  if (!name) {
40
44
  logger.critical(ErrorCode.MISSING_QUEUE_NAME, `No 'name' provided for queue processor function '${pikkuFuncId}'.`);
41
45
  return;
@@ -48,6 +52,7 @@ export const addQueueWorker = (logger, node, checker, state) => {
48
52
  state.queueWorkers.files.add(node.getSourceFile().fileName);
49
53
  state.queueWorkers.meta[name] = {
50
54
  pikkuFuncId,
55
+ ...(packageName && { packageName }),
51
56
  name,
52
57
  summary,
53
58
  description,
@@ -4,6 +4,7 @@ import { extractFunctionName, makeContextBasedId, } from '../utils/extract-funct
4
4
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
5
5
  import { resolveMiddleware } from '../utils/middleware.js';
6
6
  import { extractWireNames } from '../utils/post-process.js';
7
+ import { resolveAddonName } from '../utils/resolve-addon-package.js';
7
8
  import { ErrorCode } from '../error-codes.js';
8
9
  export const addSchedule = (logger, node, checker, state, options) => {
9
10
  if (!ts.isCallExpression(node)) {
@@ -36,6 +37,9 @@ export const addSchedule = (logger, node, checker, state, options) => {
36
37
  if (pikkuFuncId.startsWith('__temp_') && nameValue) {
37
38
  pikkuFuncId = makeContextBasedId('scheduler', nameValue);
38
39
  }
40
+ const packageName = ts.isIdentifier(funcInitializer)
41
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
42
+ : null;
39
43
  if (!nameValue || !scheduleValue) {
40
44
  return;
41
45
  }
@@ -47,6 +51,7 @@ export const addSchedule = (logger, node, checker, state, options) => {
47
51
  state.scheduledTasks.files.add(node.getSourceFile().fileName);
48
52
  state.scheduledTasks.meta[nameValue] = {
49
53
  pikkuFuncId,
54
+ ...(packageName && { packageName }),
50
55
  name: nameValue,
51
56
  schedule: scheduleValue,
52
57
  summary,