@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 5866b66: Add critical error (PKU490) when Zod schemas and wiring calls (wireHTTPRoutes, addPermission, addHTTPMiddleware) coexist in the same file. The CLI uses tsImport to extract Zod schemas at runtime, which executes all top-level code — wiring side-effects crash in this context because pikku state metadata doesn't exist. Schemas and wirings must be in separate files.
8
+ - e412b4d: Optimize CLI codegen performance: 12x faster `pikku all`
9
+
10
+ - Reuse schemas across re-inspections (skip redundant `ts-json-schema-generator` runs)
11
+ - Cache TS schemas to disk (`.pikku/schema-cache.json`) for cross-run reuse
12
+ - Pass `oldProgram` to `ts.createProgram` for incremental TS compilation
13
+ - Cache parsed tsconfig in schema generator between runs
14
+ - Auto-include direct `addPermission`/`addHTTPMiddleware` in bootstrap via side-effect imports
15
+ - Skip `pikkuAuth()` errors when nested inside `addPermission`/`addHTTPPermission`
16
+
17
+ - Updated dependencies [e412b4d]
18
+ - Updated dependencies [53dc8c8]
19
+ - Updated dependencies [0a1cc51]
20
+ - Updated dependencies [0a1cc51]
21
+ - Updated dependencies [0a1cc51]
22
+ - Updated dependencies [0a1cc51]
23
+ - Updated dependencies [0a1cc51]
24
+ - Updated dependencies [0a1cc51]
25
+ - Updated dependencies [0a1cc51]
26
+ - Updated dependencies [0a1cc51]
27
+ - Updated dependencies [0a1cc51]
28
+ - Updated dependencies [8b9b2e9]
29
+ - Updated dependencies [8b9b2e9]
30
+ - Updated dependencies [b973d44]
31
+ - Updated dependencies [8b9b2e9]
32
+ - Updated dependencies [8b9b2e9]
33
+ - @pikku/core@0.12.9
34
+
35
+ ## 0.12.3
36
+
37
+ ### Patch Changes
38
+
39
+ - 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
40
+ - 387b2ee: Add approval description inspection, track packageName on wire metadata, and resolve addon package names in channel/RPC wirings
41
+ - Updated dependencies [387b2ee]
42
+ - Updated dependencies [32ed003]
43
+ - Updated dependencies [7d369f3]
44
+ - Updated dependencies [508a796]
45
+ - Updated dependencies [ffe83af]
46
+ - Updated dependencies [c7ff141]
47
+ - @pikku/core@0.12.3
48
+
3
49
  ## 0.12.2
4
50
 
5
51
  ### 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
  }
@@ -382,8 +398,12 @@ export const addChannel = (logger, node, checker, state, options) => {
382
398
  logger.critical(ErrorCode.FUNCTION_METADATA_NOT_FOUND, `No function metadata found for onMessage handler '${msgFuncId}'`);
383
399
  return;
384
400
  }
401
+ const msgPackageName = ts.isIdentifier(onMsgProp)
402
+ ? resolveAddonName(onMsgProp, checker, state.rpc.wireAddonDeclarations)
403
+ : null;
385
404
  message = {
386
405
  pikkuFuncId: msgFuncId,
406
+ ...(msgPackageName && { packageName: msgPackageName }),
387
407
  };
388
408
  }
389
409
  // nested message-routes
@@ -394,19 +414,27 @@ export const addChannel = (logger, node, checker, state, options) => {
394
414
  // --- track used functions/middleware for service aggregation ---
395
415
  // Track connect/disconnect/message handlers
396
416
  let connectFuncId;
417
+ let connectPackageName = null;
397
418
  if (connect) {
398
419
  const extracted = extractFunctionName(connect, checker, state.rootDir);
399
420
  connectFuncId = extracted.pikkuFuncId.startsWith('__temp_')
400
421
  ? makeContextBasedId('channel', name, 'connect')
401
422
  : extracted.pikkuFuncId;
423
+ connectPackageName = ts.isIdentifier(connect)
424
+ ? resolveAddonName(connect, checker, state.rpc.wireAddonDeclarations)
425
+ : null;
402
426
  state.serviceAggregation.usedFunctions.add(connectFuncId);
403
427
  }
404
428
  let disconnectFuncId;
429
+ let disconnectPackageName = null;
405
430
  if (disconnect) {
406
431
  const extracted = extractFunctionName(disconnect, checker, state.rootDir);
407
432
  disconnectFuncId = extracted.pikkuFuncId.startsWith('__temp_')
408
433
  ? makeContextBasedId('channel', name, 'disconnect')
409
434
  : extracted.pikkuFuncId;
435
+ disconnectPackageName = ts.isIdentifier(disconnect)
436
+ ? resolveAddonName(disconnect, checker, state.rpc.wireAddonDeclarations)
437
+ : null;
410
438
  state.serviceAggregation.usedFunctions.add(disconnectFuncId);
411
439
  }
412
440
  if (message) {
@@ -436,8 +464,18 @@ export const addChannel = (logger, node, checker, state, options) => {
436
464
  input: null,
437
465
  params: params.length ? params : undefined,
438
466
  query: query?.length ? query : undefined,
439
- connect: connectFuncId ? { pikkuFuncId: connectFuncId } : null,
440
- 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,
441
479
  message,
442
480
  messageWirings,
443
481
  binary: binary === undefined ? undefined : binary,
@@ -1,9 +1,11 @@
1
1
  import ts from 'typescript';
2
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();
@@ -202,11 +204,38 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
202
204
  continue;
203
205
  const propName = prop.name.text;
204
206
  if (propName === 'func') {
205
- pikkuFuncId = extractFunctionName(prop.initializer, typeChecker, inspectorState.rootDir).pikkuFuncId;
206
- if (pikkuFuncId.startsWith('__temp_')) {
207
- pikkuFuncId = makeContextBasedId('cli', programName, ...fullPath);
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
+ }
208
238
  }
209
- meta.pikkuFuncId = pikkuFuncId;
210
239
  }
211
240
  else if (propName === 'options' &&
212
241
  ts.isObjectLiteralExpression(prop.initializer)) {
@@ -357,17 +386,20 @@ function processOptions(logger, node, typeChecker, inspectorState, inspectorOpti
357
386
  }
358
387
  }
359
388
  // Extract enum values from the function input type if available
360
- // Get the input type if we have a pikkuFuncId
361
- let inputTypes;
362
- if (pikkuFuncId) {
363
- inputTypes = inspectorState.typesLookup.get(pikkuFuncId);
364
- }
365
389
  let derivedChoices = null;
366
- if (inputTypes && inputTypes.length > 0) {
367
- 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
+ }
368
400
  }
369
- else {
370
- // Fallback: try to extract from Config type
401
+ // 3. Last resort: try Config type
402
+ if (!derivedChoices) {
371
403
  derivedChoices = extractEnumFromConfigType(logger, optionName, typeChecker, inspectorState, inspectorOptions);
372
404
  }
373
405
  // Validate and set choices
@@ -488,6 +520,34 @@ function extractEnumFromConfigType(logger, propertyName, typeChecker, inspectorS
488
520
  // Extract enum from the property
489
521
  return extractEnumFromPropertyType(configType, propertyName, typeChecker);
490
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
+ }
491
551
  /**
492
552
  * Gets the property name from a property assignment
493
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,
@@ -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 addGateway = (logger, node, checker, state, _options) => {
9
10
  if (!ts.isCallExpression(node)) {
@@ -35,6 +36,9 @@ export const addGateway = (logger, node, checker, state, _options) => {
35
36
  if (pikkuFuncId.startsWith('__temp_') && nameValue) {
36
37
  pikkuFuncId = makeContextBasedId('gateway', nameValue);
37
38
  }
39
+ const packageName = ts.isIdentifier(funcInitializer)
40
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
41
+ : null;
38
42
  if (!nameValue || !typeValue) {
39
43
  return;
40
44
  }
@@ -44,6 +48,7 @@ export const addGateway = (logger, node, checker, state, _options) => {
44
48
  state.gateways.files.add(node.getSourceFile().fileName);
45
49
  state.gateways.meta[nameValue] = {
46
50
  pikkuFuncId,
51
+ ...(packageName && { packageName }),
47
52
  name: nameValue,
48
53
  type: typeValue,
49
54
  route: routeValue,
@@ -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,
@@ -189,12 +189,10 @@ export const addMiddleware = (logger, node, checker, state) => {
189
189
  return;
190
190
  }
191
191
  const refs = extractMiddlewareRefs(middlewareArrayArg, checker, state.rootDir);
192
- if (refs.length === 0) {
193
- logger.warn(`• addMiddleware('${tag}', ...) has empty middleware array`);
194
- return;
195
- }
196
192
  const definitionIds = refs.map((r) => r.definitionId);
197
- renameTempDefinitions(state, definitionIds, 'tag', tag);
193
+ if (definitionIds.length > 0) {
194
+ renameTempDefinitions(state, definitionIds, 'tag', tag);
195
+ }
198
196
  const sourceFile = node.getSourceFile().fileName;
199
197
  const instanceIds = [];
200
198
  for (let i = 0; i < refs.length; i++) {
@@ -273,12 +271,10 @@ export const addMiddleware = (logger, node, checker, state) => {
273
271
  return;
274
272
  }
275
273
  const refs = extractMiddlewareRefs(middlewareArrayArg, checker, state.rootDir);
276
- if (refs.length === 0) {
277
- logger.warn(`• addHTTPMiddleware('${pattern}', ...) has empty middleware array`);
278
- return;
279
- }
280
274
  const definitionIds = refs.map((r) => r.definitionId);
281
- renameTempDefinitions(state, definitionIds, 'http', pattern);
275
+ if (definitionIds.length > 0) {
276
+ renameTempDefinitions(state, definitionIds, 'http', pattern);
277
+ }
282
278
  const sourceFile = node.getSourceFile().fileName;
283
279
  const instanceIds = [];
284
280
  for (let i = 0; i < refs.length; i++) {
@@ -21,12 +21,14 @@ function renameTempDefinitions(state, definitionIds, groupType, groupKey) {
21
21
  definitionIds[idx] = newId;
22
22
  }
23
23
  }
24
- function isInsidePermissionFactory(node) {
24
+ function isInsidePermissionContainer(node) {
25
25
  let current = node.parent;
26
26
  while (current) {
27
27
  if (ts.isCallExpression(current) &&
28
28
  ts.isIdentifier(current.expression) &&
29
- current.expression.text === 'pikkuPermissionFactory') {
29
+ (current.expression.text === 'pikkuPermissionFactory' ||
30
+ current.expression.text === 'addPermission' ||
31
+ current.expression.text === 'addHTTPPermission')) {
30
32
  return true;
31
33
  }
32
34
  current = current.parent;
@@ -47,7 +49,7 @@ export const addPermission = (logger, node, checker, state) => {
47
49
  // Handle pikkuPermission(...) - individual permission function definition
48
50
  if (expression.text === 'pikkuPermission') {
49
51
  // Skip if nested inside pikkuPermissionFactory — the factory handler extracts services itself
50
- if (isInsidePermissionFactory(node))
52
+ if (isInsidePermissionContainer(node))
51
53
  return;
52
54
  const arg = args[0];
53
55
  if (!arg)
@@ -110,7 +112,7 @@ export const addPermission = (logger, node, checker, state) => {
110
112
  return;
111
113
  }
112
114
  if (expression.text === 'pikkuAuth') {
113
- if (isInsidePermissionFactory(node))
115
+ if (isInsidePermissionContainer(node))
114
116
  return;
115
117
  const arg = args[0];
116
118
  if (!arg)
@@ -269,11 +271,9 @@ export const addPermission = (logger, node, checker, state) => {
269
271
  }
270
272
  // Extract permission pikkuFuncIds from array
271
273
  const permissionNames = extractPermissionPikkuNames(permissionsArrayArg, checker, state.rootDir);
272
- if (permissionNames.length === 0) {
273
- logger.warn(`• addPermission('${tag}', ...) has empty permissions array`);
274
- return;
274
+ if (permissionNames.length > 0) {
275
+ renameTempDefinitions(state, permissionNames, 'tag', tag);
275
276
  }
276
- renameTempDefinitions(state, permissionNames, 'tag', tag);
277
277
  const allServices = new Set();
278
278
  for (const permissionName of permissionNames) {
279
279
  const permissionMeta = state.permissions.definitions[permissionName];
@@ -348,11 +348,9 @@ export const addPermission = (logger, node, checker, state) => {
348
348
  }
349
349
  // Extract permission pikkuFuncIds from array
350
350
  const permissionNames = extractPermissionPikkuNames(permissionsArrayArg, checker, state.rootDir);
351
- if (permissionNames.length === 0) {
352
- logger.warn(`• addHTTPPermission('${pattern}', ...) has empty permissions array`);
353
- return;
351
+ if (permissionNames.length > 0) {
352
+ renameTempDefinitions(state, permissionNames, 'http', pattern);
354
353
  }
355
- renameTempDefinitions(state, permissionNames, 'http', pattern);
356
354
  const allServices = new Set();
357
355
  for (const permissionName of permissionNames) {
358
356
  const permissionMeta = state.permissions.definitions[permissionName];
@@ -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,