@pikku/inspector 0.12.2 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/add/add-ai-agent.js +4 -0
  3. package/dist/add/add-approval-description.d.ts +5 -0
  4. package/dist/add/add-approval-description.js +52 -0
  5. package/dist/add/add-channel.js +42 -4
  6. package/dist/add/add-cli.js +73 -13
  7. package/dist/add/add-file-with-factory.js +1 -0
  8. package/dist/add/add-functions.js +22 -3
  9. package/dist/add/add-gateway.js +5 -0
  10. package/dist/add/add-http-route.js +5 -0
  11. package/dist/add/add-mcp-prompt.js +5 -0
  12. package/dist/add/add-mcp-resource.js +5 -0
  13. package/dist/add/add-queue-worker.js +5 -0
  14. package/dist/add/add-schedule.js +5 -0
  15. package/dist/add/add-wire-addon.js +7 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/inspector.js +7 -0
  19. package/dist/types.d.ts +10 -0
  20. package/dist/utils/load-addon-functions-meta.d.ts +12 -0
  21. package/dist/utils/load-addon-functions-meta.js +76 -0
  22. package/dist/utils/post-process.js +26 -0
  23. package/dist/utils/resolve-function-meta.d.ts +11 -0
  24. package/dist/utils/resolve-function-meta.js +17 -0
  25. package/dist/utils/serialize-inspector-state.d.ts +2 -0
  26. package/dist/utils/serialize-inspector-state.js +4 -0
  27. package/dist/utils/serialize-mcp-json.js +13 -7
  28. package/dist/visit.js +2 -0
  29. package/package.json +2 -2
  30. package/src/add/add-ai-agent.ts +6 -0
  31. package/src/add/add-approval-description.ts +76 -0
  32. package/src/add/add-channel.ts +44 -4
  33. package/src/add/add-cli.ts +108 -21
  34. package/src/add/add-file-with-factory.ts +1 -0
  35. package/src/add/add-functions.ts +28 -3
  36. package/src/add/add-gateway.ts +6 -0
  37. package/src/add/add-http-route.ts +6 -0
  38. package/src/add/add-mcp-prompt.ts +6 -0
  39. package/src/add/add-mcp-resource.ts +6 -0
  40. package/src/add/add-queue-worker.ts +6 -0
  41. package/src/add/add-schedule.ts +6 -0
  42. package/src/add/add-wire-addon.ts +8 -0
  43. package/src/index.ts +1 -0
  44. package/src/inspector.ts +12 -0
  45. package/src/types.ts +11 -0
  46. package/src/utils/load-addon-functions-meta.ts +94 -0
  47. package/src/utils/post-process.ts +25 -0
  48. package/src/utils/resolve-function-meta.ts +25 -0
  49. package/src/utils/serialize-inspector-state.ts +6 -0
  50. package/src/utils/serialize-mcp-json.ts +12 -7
  51. package/src/visit.ts +2 -0
  52. package/tsconfig.tsbuildinfo +1 -1
package/dist/types.d.ts CHANGED
@@ -73,6 +73,7 @@ export interface InspectorFunctionState {
73
73
  path: string;
74
74
  exportedName: string;
75
75
  }>;
76
+ approvalDescriptions: Record<string, InspectorApprovalDescriptionDefinition>;
76
77
  }
77
78
  export interface InspectorChannelState {
78
79
  meta: ChannelsMeta;
@@ -108,6 +109,13 @@ export interface InspectorChannelMiddlewareState {
108
109
  export interface InspectorAIMiddlewareState {
109
110
  definitions: Record<string, InspectorMiddlewareDefinition>;
110
111
  }
112
+ export interface InspectorApprovalDescriptionDefinition {
113
+ services: FunctionServicesMeta;
114
+ wires?: FunctionWiresMeta;
115
+ sourceFile: string;
116
+ position: number;
117
+ exportedName: string | null;
118
+ }
111
119
  export interface InspectorPermissionDefinition {
112
120
  services: FunctionServicesMeta;
113
121
  wires?: FunctionWiresMeta;
@@ -309,6 +317,7 @@ export interface InspectorState {
309
317
  wireAddonDeclarations: Map<string, {
310
318
  package: string;
311
319
  rpcEndpoint?: string;
320
+ mcp?: boolean;
312
321
  secretOverrides?: Record<string, string>;
313
322
  variableOverrides?: Record<string, string>;
314
323
  }>;
@@ -383,4 +392,5 @@ export interface InspectorState {
383
392
  requiredSchemas: Set<string>;
384
393
  openAPISpec: Record<string, any> | null;
385
394
  diagnostics: InspectorDiagnostic[];
395
+ addonFunctions: Record<string, FunctionsMeta>;
386
396
  }
@@ -0,0 +1,12 @@
1
+ import type { InspectorState, InspectorLogger } from '../types.js';
2
+ /**
3
+ * After the setup sweep discovers wireAddon() declarations, load each addon
4
+ * package's function metadata so that wiring handlers (channels, HTTP routes,
5
+ * schedules, etc.) can look up addon function types during the routes sweep.
6
+ */
7
+ export declare function loadAddonFunctionsMeta(logger: InspectorLogger, state: InspectorState): Promise<void>;
8
+ /**
9
+ * Load addon schemas into state.schemas. Called after generateAllSchemas
10
+ * to ensure addon schemas aren't overwritten.
11
+ */
12
+ export declare function loadAddonSchemas(logger: InspectorLogger, state: InspectorState): Promise<void>;
@@ -0,0 +1,76 @@
1
+ import { readFile, readdir } from 'fs/promises';
2
+ import { createRequire } from 'module';
3
+ import { join, dirname } from 'path';
4
+ /**
5
+ * After the setup sweep discovers wireAddon() declarations, load each addon
6
+ * package's function metadata so that wiring handlers (channels, HTTP routes,
7
+ * schedules, etc.) can look up addon function types during the routes sweep.
8
+ */
9
+ export async function loadAddonFunctionsMeta(logger, state) {
10
+ const { wireAddonDeclarations } = state.rpc;
11
+ if (wireAddonDeclarations.size === 0)
12
+ return;
13
+ const require = createRequire(join(state.rootDir, 'package.json'));
14
+ for (const [namespace, decl] of wireAddonDeclarations) {
15
+ try {
16
+ const metaPath = require.resolve(`${decl.package}/.pikku/function/pikku-functions-meta.gen.json`);
17
+ const raw = await readFile(metaPath, 'utf-8');
18
+ const meta = JSON.parse(raw);
19
+ state.addonFunctions[namespace] = meta;
20
+ logger.debug(`Loaded ${Object.keys(meta).length} addon functions for '${namespace}' from ${decl.package}`);
21
+ // If wireAddon has mcp: true, expose addon functions with mcp: true as MCP tools
22
+ if (decl.mcp) {
23
+ for (const [funcName, funcMeta] of Object.entries(meta)) {
24
+ if (funcMeta.mcp) {
25
+ const toolName = `${namespace}:${funcName}`;
26
+ state.mcpEndpoints.toolsMeta[toolName] = {
27
+ pikkuFuncId: `${namespace}:${funcName}`,
28
+ name: toolName,
29
+ description: funcMeta.description || funcMeta.title || funcName,
30
+ inputSchema: funcMeta.inputSchemaName ?? null,
31
+ outputSchema: funcMeta.outputSchemaName ?? null,
32
+ tags: funcMeta.tags,
33
+ };
34
+ }
35
+ }
36
+ }
37
+ }
38
+ catch (error) {
39
+ logger.warn(`Failed to load addon function metadata for '${namespace}' (${decl.package}): ${error.message}`);
40
+ }
41
+ }
42
+ }
43
+ /**
44
+ * Load addon schemas into state.schemas. Called after generateAllSchemas
45
+ * to ensure addon schemas aren't overwritten.
46
+ */
47
+ export async function loadAddonSchemas(logger, state) {
48
+ const { wireAddonDeclarations } = state.rpc;
49
+ if (wireAddonDeclarations.size === 0)
50
+ return;
51
+ const require = createRequire(join(state.rootDir, 'package.json'));
52
+ for (const [namespace, decl] of wireAddonDeclarations) {
53
+ try {
54
+ const metaPath = require.resolve(`${decl.package}/.pikku/function/pikku-functions-meta.gen.json`);
55
+ const schemasDir = join(dirname(metaPath), '..', 'schemas', 'schemas');
56
+ try {
57
+ const schemaFiles = await readdir(schemasDir);
58
+ for (const file of schemaFiles) {
59
+ if (!file.endsWith('.schema.json'))
60
+ continue;
61
+ const schemaName = file.replace('.schema.json', '');
62
+ if (!state.schemas[schemaName]) {
63
+ const schemaRaw = await readFile(join(schemasDir, file), 'utf-8');
64
+ state.schemas[schemaName] = JSON.parse(schemaRaw);
65
+ }
66
+ }
67
+ }
68
+ catch {
69
+ // No schemas directory — that's fine
70
+ }
71
+ }
72
+ catch (error) {
73
+ logger.warn(`Failed to load addon schemas for '${namespace}' (${decl.package}): ${error.message}`);
74
+ }
75
+ }
76
+ }
@@ -217,6 +217,32 @@ export function computeResolvedIOTypes(state) {
217
217
  }
218
218
  }
219
219
  state.resolvedIOTypes[pikkuFuncId] = { inputType, outputType };
220
+ if (meta.inputSchemaName && inputType !== 'null') {
221
+ meta.inputSchemaName = inputType;
222
+ }
223
+ if (meta.outputSchemaName && outputType !== 'null') {
224
+ meta.outputSchemaName = outputType;
225
+ }
226
+ if (meta.inputs) {
227
+ meta.inputs = meta.inputs.map((name) => {
228
+ try {
229
+ return functions.typesMap.getTypeMeta(name).uniqueName;
230
+ }
231
+ catch {
232
+ return name;
233
+ }
234
+ });
235
+ }
236
+ if (meta.outputs) {
237
+ meta.outputs = meta.outputs.map((name) => {
238
+ try {
239
+ return functions.typesMap.getTypeMeta(name).uniqueName;
240
+ }
241
+ catch {
242
+ return name;
243
+ }
244
+ });
245
+ }
220
246
  }
221
247
  }
222
248
  const serializeGroupMap = (groupMap) => {
@@ -0,0 +1,11 @@
1
+ import type { FunctionMeta, FunctionsMeta } from '@pikku/core';
2
+ /**
3
+ * Look up function metadata by pikkuFuncId, checking both local functions
4
+ * and addon functions. Addon functions use namespaced IDs like 'namespace:funcName'.
5
+ */
6
+ export declare function resolveFunctionMeta(state: {
7
+ functions: {
8
+ meta: FunctionsMeta;
9
+ };
10
+ addonFunctions: Record<string, FunctionsMeta>;
11
+ }, pikkuFuncId: string): FunctionMeta | undefined;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Look up function metadata by pikkuFuncId, checking both local functions
3
+ * and addon functions. Addon functions use namespaced IDs like 'namespace:funcName'.
4
+ */
5
+ export function resolveFunctionMeta(state, pikkuFuncId) {
6
+ // Check local functions first
7
+ const local = state.functions.meta[pikkuFuncId];
8
+ if (local)
9
+ return local;
10
+ // Check addon functions (namespaced like 'swaggerPetstore:addPet')
11
+ const colonIndex = pikkuFuncId.indexOf(':');
12
+ if (colonIndex === -1)
13
+ return undefined;
14
+ const namespace = pikkuFuncId.substring(0, colonIndex);
15
+ const funcName = pikkuFuncId.substring(colonIndex + 1);
16
+ return state.addonFunctions[namespace]?.[funcName];
17
+ }
@@ -100,6 +100,7 @@ export interface SerializableInspectorState {
100
100
  path: string;
101
101
  exportedName: string;
102
102
  }]>;
103
+ approvalDescriptions: InspectorState['functions']['approvalDescriptions'];
103
104
  };
104
105
  http: {
105
106
  metaInputTypes: Array<[
@@ -253,6 +254,7 @@ export interface SerializableInspectorState {
253
254
  requiredSchemas: string[];
254
255
  openAPISpec: Record<string, any> | null;
255
256
  diagnostics: InspectorDiagnostic[];
257
+ addonFunctions: InspectorState['addonFunctions'];
256
258
  }
257
259
  /**
258
260
  * Serializes InspectorState to a JSON-compatible format
@@ -32,6 +32,7 @@ export function serializeInspectorState(state) {
32
32
  typesMap: serializeTypesMap(state.functions.typesMap),
33
33
  meta: state.functions.meta,
34
34
  files: Array.from(state.functions.files.entries()),
35
+ approvalDescriptions: state.functions.approvalDescriptions,
35
36
  },
36
37
  http: {
37
38
  metaInputTypes: Array.from(state.http.metaInputTypes.entries()),
@@ -137,6 +138,7 @@ export function serializeInspectorState(state) {
137
138
  requiredSchemas: Array.from(state.requiredSchemas),
138
139
  openAPISpec: state.openAPISpec,
139
140
  diagnostics: state.diagnostics,
141
+ addonFunctions: state.addonFunctions,
140
142
  };
141
143
  }
142
144
  /**
@@ -173,6 +175,7 @@ export function deserializeInspectorState(data) {
173
175
  typesMap: deserializeTypesMap(data.functions.typesMap),
174
176
  meta: data.functions.meta,
175
177
  files: new Map(data.functions.files),
178
+ approvalDescriptions: data.functions.approvalDescriptions || {},
176
179
  },
177
180
  http: {
178
181
  metaInputTypes: new Map(data.http.metaInputTypes),
@@ -288,5 +291,6 @@ export function deserializeInspectorState(data) {
288
291
  requiredSchemas: new Set(data.requiredSchemas || []),
289
292
  openAPISpec: data.openAPISpec || null,
290
293
  diagnostics: data.diagnostics || [],
294
+ addonFunctions: data.addonFunctions || {},
291
295
  };
292
296
  }
@@ -1,6 +1,7 @@
1
+ import { resolveFunctionMeta } from './resolve-function-meta.js';
1
2
  export const serializeMCPJson = (logger, state) => {
2
3
  const { mcpEndpoints, functions, schemas } = state;
3
- const { meta: functionsMeta, typesMap } = functions;
4
+ const { typesMap } = functions;
4
5
  const { resourcesMeta, toolsMeta, promptsMeta } = mcpEndpoints;
5
6
  const tools = [];
6
7
  const resources = [];
@@ -19,9 +20,14 @@ export const serializeMCPJson = (logger, state) => {
19
20
  ].includes(typeName)) {
20
21
  return undefined;
21
22
  }
22
- const uniqueName = typesMap.getUniqueName(typeName);
23
- if (!uniqueName) {
24
- return undefined;
23
+ // Try local typesMap first, fall back to direct schema lookup (for addon types)
24
+ let uniqueName;
25
+ try {
26
+ uniqueName = typesMap.getUniqueName(typeName);
27
+ }
28
+ catch {
29
+ // Type not in local typesMap — try direct schema lookup (addon schemas)
30
+ uniqueName = typeName;
25
31
  }
26
32
  const schema = schemas[uniqueName];
27
33
  if (!schema) {
@@ -31,7 +37,7 @@ export const serializeMCPJson = (logger, state) => {
31
37
  return schema;
32
38
  };
33
39
  for (const [name, endpointMeta] of Object.entries(resourcesMeta)) {
34
- const functionMeta = functionsMeta[endpointMeta.pikkuFuncId];
40
+ const functionMeta = resolveFunctionMeta(state, endpointMeta.pikkuFuncId);
35
41
  if (!functionMeta) {
36
42
  logger.warn(`Function ${endpointMeta.pikkuFuncId} not found in functionsMeta. Skipping resource ${name}.`);
37
43
  continue;
@@ -50,7 +56,7 @@ export const serializeMCPJson = (logger, state) => {
50
56
  });
51
57
  }
52
58
  for (const [name, endpointMeta] of Object.entries(toolsMeta)) {
53
- const functionMeta = functionsMeta[endpointMeta.pikkuFuncId];
59
+ const functionMeta = resolveFunctionMeta(state, endpointMeta.pikkuFuncId);
54
60
  if (!functionMeta) {
55
61
  logger.warn(`Function ${endpointMeta.pikkuFuncId} not found in functionsMeta. Skipping tool ${name}.`);
56
62
  continue;
@@ -68,7 +74,7 @@ export const serializeMCPJson = (logger, state) => {
68
74
  });
69
75
  }
70
76
  for (const [name, endpointMeta] of Object.entries(promptsMeta)) {
71
- const functionMeta = functionsMeta[endpointMeta.pikkuFuncId];
77
+ const functionMeta = resolveFunctionMeta(state, endpointMeta.pikkuFuncId);
72
78
  if (!functionMeta) {
73
79
  logger.warn(`Function ${endpointMeta.pikkuFuncId} not found in functionsMeta. Skipping prompt ${name}.`);
74
80
  continue;
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.3",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
37
- "@pikku/core": "^0.12.2",
37
+ "@pikku/core": "^0.12.3",
38
38
  "path-to-regexp": "^8.3.0",
39
39
  "ts-json-schema-generator": "^2.5.0",
40
40
  "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,