@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
@@ -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,
@@ -32,6 +32,7 @@ export function addWireAddon(node, state, logger) {
32
32
  let name;
33
33
  let pkg;
34
34
  let rpcEndpoint;
35
+ let mcp;
35
36
  let secretOverrides;
36
37
  let variableOverrides;
37
38
  for (const prop of firstArg.properties) {
@@ -47,6 +48,11 @@ export function addWireAddon(node, state, logger) {
47
48
  else if (key === 'rpcEndpoint' && ts.isStringLiteral(prop.initializer)) {
48
49
  rpcEndpoint = prop.initializer.text;
49
50
  }
51
+ else if (key === 'mcp' &&
52
+ (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
53
+ prop.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
54
+ mcp = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
55
+ }
50
56
  else if (key === 'secretOverrides' &&
51
57
  ts.isObjectLiteralExpression(prop.initializer)) {
52
58
  secretOverrides = parseStringRecord(prop.initializer);
@@ -62,6 +68,7 @@ export function addWireAddon(node, state, logger) {
62
68
  state.rpc.wireAddonDeclarations.set(name, {
63
69
  package: pkg,
64
70
  rpcEndpoint,
71
+ mcp,
65
72
  secretOverrides,
66
73
  variableOverrides,
67
74
  });
@@ -3,7 +3,7 @@ import { extractFunctionName } from '../utils/extract-function-name.js';
3
3
  import { extractFunctionNode } from '../utils/extract-function-node.js';
4
4
  import { ErrorCode } from '../error-codes.js';
5
5
  import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
6
- import { getCommonWireMetaData } from '../utils/get-property-value.js';
6
+ import { getCommonWireMetaData, getPropertyValue, } from '../utils/get-property-value.js';
7
7
  import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js';
8
8
  /**
9
9
  * Recursively check if any step has inline type (non-serializable)
@@ -182,6 +182,7 @@ export const addWorkflow = (logger, node, checker, state) => {
182
182
  let summary;
183
183
  let description;
184
184
  let errors;
185
+ let inline;
185
186
  if (ts.isObjectLiteralExpression(firstArg)) {
186
187
  const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger);
187
188
  if (metadata.disabled)
@@ -190,6 +191,10 @@ export const addWorkflow = (logger, node, checker, state) => {
190
191
  summary = metadata.summary;
191
192
  description = metadata.description;
192
193
  errors = metadata.errors;
194
+ const inlineProp = getPropertyValue(firstArg, 'inline');
195
+ if (inlineProp === true) {
196
+ inline = true;
197
+ }
193
198
  }
194
199
  // Validate that we got a valid function
195
200
  if (ts.isObjectLiteralExpression(firstArg) &&
@@ -272,5 +277,6 @@ export const addWorkflow = (logger, node, checker, state) => {
272
277
  description,
273
278
  errors,
274
279
  tags,
280
+ inline,
275
281
  };
276
282
  };
@@ -49,6 +49,7 @@ export declare enum ErrorCode {
49
49
  MANIFEST_INTEGRITY_ERROR = "PKU865",
50
50
  MISSING_MODEL = "PKU145",
51
51
  INVALID_MODEL = "PKU146",
52
+ SCHEMA_AND_WIRING_COLOCATED = "PKU490",
52
53
  SERVICES_NOT_DESTRUCTURED = "PKU410",
53
54
  WIRES_NOT_DESTRUCTURED = "PKU411",
54
55
  WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901"
@@ -58,6 +58,8 @@ export var ErrorCode;
58
58
  // Model configuration errors
59
59
  ErrorCode["MISSING_MODEL"] = "PKU145";
60
60
  ErrorCode["INVALID_MODEL"] = "PKU146";
61
+ // File structure errors
62
+ ErrorCode["SCHEMA_AND_WIRING_COLOCATED"] = "PKU490";
61
63
  // Optimization diagnostics
62
64
  ErrorCode["SERVICES_NOT_DESTRUCTURED"] = "PKU410";
63
65
  ErrorCode["WIRES_NOT_DESTRUCTURED"] = "PKU411";
package/dist/index.d.ts CHANGED
@@ -13,4 +13,5 @@ export { serializeMCPJson } from './utils/serialize-mcp-json.js';
13
13
  export type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js';
14
14
  export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/index.js';
15
15
  export { getFilesAndMethods } from './utils/get-files-and-methods.js';
16
+ export { resolveFunctionMeta } from './utils/resolve-function-meta.js';
16
17
  export type { SerializedWorkflowGraph, SerializedWorkflowGraphs, } from './utils/workflow/graph/index.js';
package/dist/index.js CHANGED
@@ -7,3 +7,4 @@ export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes
7
7
  export { serializeMCPJson } from './utils/serialize-mcp-json.js';
8
8
  export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/index.js';
9
9
  export { getFilesAndMethods } from './utils/get-files-and-methods.js';
10
+ export { resolveFunctionMeta } from './utils/resolve-function-meta.js';
package/dist/inspector.js CHANGED
@@ -4,13 +4,14 @@ import { visitSetup, visitRoutes } from './visit.js';
4
4
  import { TypesMap } from './types-map.js';
5
5
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
6
6
  import { findCommonAncestor } from './utils/find-root-dir.js';
7
- import { aggregateRequiredServices, validateAgentModels, validateAgentOverrides, validateSecretOverrides, validateVariableOverrides, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, } from './utils/post-process.js';
7
+ import { aggregateRequiredServices, validateAgentModels, validateAgentOverrides, validateSecretOverrides, validateVariableOverrides, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, } from './utils/post-process.js';
8
8
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
9
9
  import { pikkuState } from '@pikku/core/internal';
10
10
  import { resolveLatestVersions } from './utils/resolve-versions.js';
11
11
  import { finalizeWorkflows } from './utils/workflow/graph/finalize-workflows.js';
12
12
  import { finalizeWorkflowHelperTypes, finalizeWorkflowWires, } from './utils/workflow/graph/finalize-workflow-wires.js';
13
13
  import { generateAllSchemas } from './utils/schema-generator.js';
14
+ import { loadAddonFunctionsMeta, loadAddonSchemas, } from './utils/load-addon-functions-meta.js';
14
15
  import { computeContractHashes, extractContractsFromMeta, updateManifest, createEmptyManifest, validateContracts, } from './utils/contract-hashes.js';
15
16
  /**
16
17
  * Creates an initial/empty inspector state with all required properties initialized
@@ -37,6 +38,7 @@ export function getInitialInspectorState(rootDir) {
37
38
  typesMap: new TypesMap(),
38
39
  meta: {},
39
40
  files: new Map(),
41
+ approvalDescriptions: {},
40
42
  },
41
43
  http: {
42
44
  metaInputTypes: new Map(),
@@ -171,11 +173,12 @@ export function getInitialInspectorState(rootDir) {
171
173
  requiredSchemas: new Set(),
172
174
  openAPISpec: null,
173
175
  diagnostics: [],
176
+ addonFunctions: {},
177
+ program: null,
174
178
  };
175
179
  }
176
180
  export const inspect = async (logger, routeFiles, options = {}) => {
177
- const startProgram = performance.now();
178
- const program = ts.createProgram(routeFiles, {
181
+ const compilerOptions = {
179
182
  target: ts.ScriptTarget.ESNext,
180
183
  module: ts.ModuleKind.Node16,
181
184
  skipLibCheck: true,
@@ -184,8 +187,11 @@ export const inspect = async (logger, routeFiles, options = {}) => {
184
187
  types: [],
185
188
  allowJs: false,
186
189
  checkJs: false,
187
- });
188
- logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(2)}ms`);
190
+ };
191
+ const startProgram = performance.now();
192
+ const program = ts.createProgram(routeFiles, compilerOptions, undefined, // host
193
+ options.oldProgram);
194
+ logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${routeFiles.length} files${options.oldProgram ? ', incremental' : ''})`);
189
195
  const startChecker = performance.now();
190
196
  const checker = program.getTypeChecker();
191
197
  logger.debug(`Got type checker in ${(performance.now() - startChecker).toFixed(2)}ms`);
@@ -204,20 +210,26 @@ export const inspect = async (logger, routeFiles, options = {}) => {
204
210
  for (const sourceFile of sourceFiles) {
205
211
  ts.forEachChild(sourceFile, (child) => visitSetup(logger, checker, child, state, options));
206
212
  }
207
- logger.debug(`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(2)}ms`);
213
+ logger.debug(`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(0)}ms`);
214
+ // Load addon function metadata so wirings can reference addon functions
215
+ await loadAddonFunctionsMeta(logger, state);
208
216
  if (!options.setupOnly) {
209
217
  // Second sweep: add all transports
210
218
  const startRoutes = performance.now();
211
219
  for (const sourceFile of sourceFiles) {
212
220
  ts.forEachChild(sourceFile, (child) => visitRoutes(logger, checker, child, state, options));
213
221
  }
214
- logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(2)}ms`);
222
+ logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`);
215
223
  resolveLatestVersions(state, logger);
216
224
  if (options.schemaConfig) {
225
+ const startSchemas = performance.now();
217
226
  state.schemas = await generateAllSchemas(logger, options.schemaConfig, state);
227
+ logger.debug(`generateAllSchemas took ${(performance.now() - startSchemas).toFixed(0)}ms`);
218
228
  computeContractHashes(state.schemas, state.functions.typesMap, state.functions.meta);
219
229
  computeRequiredSchemas(state, options);
220
230
  }
231
+ // Re-load addon schemas (generateAllSchemas replaces state.schemas)
232
+ await loadAddonSchemas(logger, state);
221
233
  state.manifest.initial = options.manifest ?? null;
222
234
  const contracts = extractContractsFromMeta(state.functions.meta);
223
235
  const baseManifest = state.manifest.initial ?? createEmptyManifest();
@@ -241,6 +253,7 @@ export const inspect = async (logger, routeFiles, options = {}) => {
241
253
  computeMiddlewareGroupsMeta(state);
242
254
  computePermissionsGroupsMeta(state);
243
255
  computeDiagnostics(state);
256
+ validateSchemaWiringSeparation(logger, state);
244
257
  if (options.openAPI) {
245
258
  state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
246
259
  }
@@ -249,5 +262,6 @@ export const inspect = async (logger, routeFiles, options = {}) => {
249
262
  validateSecretOverrides(logger, state);
250
263
  validateVariableOverrides(logger, state);
251
264
  }
265
+ state.program = program;
252
266
  return state;
253
267
  };
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;
@@ -185,6 +193,7 @@ export type InspectorOptions = Partial<{
185
193
  tags: string[];
186
194
  manifest: VersionManifest;
187
195
  modelConfig: InspectorModelConfig;
196
+ oldProgram: ts.Program | undefined;
188
197
  }>;
189
198
  export interface InspectorLogger {
190
199
  info: (message: string) => void;
@@ -309,6 +318,7 @@ export interface InspectorState {
309
318
  wireAddonDeclarations: Map<string, {
310
319
  package: string;
311
320
  rpcEndpoint?: string;
321
+ mcp?: boolean;
312
322
  secretOverrides?: Record<string, string>;
313
323
  variableOverrides?: Record<string, string>;
314
324
  }>;
@@ -383,4 +393,6 @@ export interface InspectorState {
383
393
  requiredSchemas: Set<string>;
384
394
  openAPISpec: Record<string, any> | null;
385
395
  diagnostics: InspectorDiagnostic[];
396
+ addonFunctions: Record<string, FunctionsMeta>;
397
+ program: ts.Program | null;
386
398
  }
@@ -9,6 +9,7 @@ export function sanitizeTypeName(name) {
9
9
  }
10
10
  export function generateCustomTypes(typesMap, requiredTypes) {
11
11
  const typeDeclarations = Array.from(typesMap.customTypes.entries())
12
+ .sort(([a], [b]) => a.localeCompare(b))
12
13
  .filter(([_name, { type }]) => {
13
14
  const hasUndefinedGeneric = /\b(Name|In|Out|Key)\b/.test(type) && /\[.*\]/.test(type);
14
15
  return !hasUndefinedGeneric;
@@ -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
+ }
@@ -22,4 +22,13 @@ export declare function computePermissionsGroupsMeta(state: InspectorState): voi
22
22
  export declare function computeRequiredSchemas(state: InspectorState, options: InspectorOptions): void;
23
23
  export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, modelConfig?: InspectorModelConfig): void;
24
24
  export declare function validateAgentOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, modelConfig?: InspectorModelConfig): void;
25
+ /**
26
+ * Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
27
+ * addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
28
+ *
29
+ * The CLI uses tsImport to extract Zod schemas at runtime, which executes
30
+ * all top-level code in the file. Wiring calls crash during this process
31
+ * because the pikku state metadata doesn't exist in the CLI context.
32
+ */
33
+ export declare function validateSchemaWiringSeparation(logger: InspectorLogger, state: InspectorState): void;
25
34
  export declare function computeDiagnostics(state: InspectorState): void;
@@ -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) => {
@@ -352,6 +378,52 @@ export function validateAgentOverrides(logger, state, modelConfig) {
352
378
  }
353
379
  }
354
380
  }
381
+ /**
382
+ * Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
383
+ * addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
384
+ *
385
+ * The CLI uses tsImport to extract Zod schemas at runtime, which executes
386
+ * all top-level code in the file. Wiring calls crash during this process
387
+ * because the pikku state metadata doesn't exist in the CLI context.
388
+ */
389
+ export function validateSchemaWiringSeparation(logger, state) {
390
+ // Collect files that contain schemas
391
+ const schemaFiles = new Set();
392
+ for (const ref of state.schemaLookup.values()) {
393
+ schemaFiles.add(ref.sourceFile);
394
+ }
395
+ // Collect files that contain wiring side-effects
396
+ const wiringFiles = new Set();
397
+ // HTTP route wirings
398
+ for (const file of state.http.files) {
399
+ wiringFiles.add(file);
400
+ }
401
+ // Permission wirings (addPermission calls)
402
+ for (const meta of state.permissions.tagPermissions.values()) {
403
+ wiringFiles.add(meta.sourceFile);
404
+ }
405
+ for (const meta of state.http.routePermissions.values()) {
406
+ wiringFiles.add(meta.sourceFile);
407
+ }
408
+ // Middleware wirings (addHTTPMiddleware calls)
409
+ for (const meta of state.http.routeMiddleware.values()) {
410
+ wiringFiles.add(meta.sourceFile);
411
+ }
412
+ for (const meta of state.middleware.tagMiddleware.values()) {
413
+ wiringFiles.add(meta.sourceFile);
414
+ }
415
+ // Check for overlap
416
+ for (const file of schemaFiles) {
417
+ if (wiringFiles.has(file)) {
418
+ const schemas = Array.from(state.schemaLookup.entries())
419
+ .filter(([, ref]) => ref.sourceFile === file)
420
+ .map(([name]) => name);
421
+ logger.critical(ErrorCode.SCHEMA_AND_WIRING_COLOCATED, `File '${file}' contains both Zod schemas (${schemas.join(', ')}) and wiring calls (wireHTTPRoutes, addPermission, etc.). ` +
422
+ `These must be in separate files because the CLI imports schema files at runtime, which triggers wiring side-effects that crash without server context. ` +
423
+ `Move the route/wiring definitions to a dedicated wiring file.`);
424
+ }
425
+ }
426
+ }
355
427
  export function computeDiagnostics(state) {
356
428
  const diagnostics = [];
357
429
  for (const [id, meta] of Object.entries(state.functions.meta)) {
@@ -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
+ }
@@ -46,14 +46,25 @@ function primitiveTypeToSchema(typeStr) {
46
46
  }
47
47
  return null;
48
48
  }
49
+ // Cached state for schema program reuse across inspect() calls
50
+ let cachedSchemaProgram;
51
+ let cachedParsedConfig;
52
+ let cachedTsconfigPath;
53
+ let cachedCustomTypesContent;
54
+ let cachedTSSchemas;
49
55
  function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileContent) {
50
56
  const configPath = resolve(tsconfig);
51
- const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
52
- const basePath = dirname(configPath);
53
- const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, basePath);
57
+ // Cache the parsed tsconfig — it doesn't change between runs
58
+ if (!cachedParsedConfig || cachedTsconfigPath !== configPath) {
59
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
60
+ const basePath = dirname(configPath);
61
+ cachedParsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, basePath);
62
+ cachedTsconfigPath = configPath;
63
+ cachedSchemaProgram = undefined;
64
+ }
54
65
  const resolvedVirtualPath = resolve(virtualFilePath);
55
- const fileNames = [...parsedConfig.fileNames, resolvedVirtualPath];
56
- const defaultHost = ts.createCompilerHost(parsedConfig.options);
66
+ const fileNames = [...cachedParsedConfig.fileNames, resolvedVirtualPath];
67
+ const defaultHost = ts.createCompilerHost(cachedParsedConfig.options);
57
68
  const customHost = {
58
69
  ...defaultHost,
59
70
  getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) {
@@ -73,7 +84,10 @@ function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileCont
73
84
  return defaultHost.readFile(fileName);
74
85
  },
75
86
  };
76
- return ts.createProgram(fileNames, parsedConfig.options, customHost);
87
+ const program = ts.createProgram(fileNames, cachedParsedConfig.options, customHost, cachedSchemaProgram // reuse previous program for incremental compilation
88
+ );
89
+ cachedSchemaProgram = program;
90
+ return program;
77
91
  }
78
92
  function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, functionMeta, httpWiringsMeta, additionalTypes, additionalProperties = false, schemaLookup) {
79
93
  const schemasSet = new Set(typesMap.customTypes.keys());
@@ -204,6 +218,12 @@ export async function generateAllSchemas(logger, config, state) {
204
218
  const zodSchemas = await generateZodSchemas(logger, state.schemaLookup, state.functions.typesMap);
205
219
  const requiredTypes = new Set();
206
220
  const customTypesContent = generateCustomTypes(state.functions.typesMap, requiredTypes);
221
+ if (cachedTSSchemas && cachedCustomTypesContent === customTypesContent) {
222
+ logger.debug('Reusing cached TS schemas (types unchanged)');
223
+ return { ...cachedTSSchemas, ...zodSchemas };
224
+ }
207
225
  const tsSchemas = generateTSSchemas(logger, config.tsconfig, customTypesContent, state.functions.typesMap, state.functions.meta, state.http.meta, config.schemasFromTypes, config.schema?.additionalProperties, state.schemaLookup);
226
+ cachedCustomTypesContent = customTypesContent;
227
+ cachedTSSchemas = tsSchemas;
208
228
  return { ...tsSchemas, ...zodSchemas };
209
229
  }
@@ -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,7 @@ 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 || {},
295
+ program: null,
291
296
  };
292
297
  }
@@ -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;