@pikku/inspector 0.11.1 → 0.12.0

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 (189) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/OPTIMIZATION-PLAN.md +195 -0
  3. package/dist/add/add-ai-agent.d.ts +2 -0
  4. package/dist/add/add-ai-agent.js +314 -0
  5. package/dist/add/add-channel.js +69 -61
  6. package/dist/add/add-cli.js +36 -18
  7. package/dist/add/add-file-with-factory.js +2 -0
  8. package/dist/add/add-functions.js +327 -59
  9. package/dist/add/add-http-route.d.ts +19 -10
  10. package/dist/add/add-http-route.js +153 -44
  11. package/dist/add/add-http-routes.d.ts +5 -0
  12. package/dist/add/add-http-routes.js +159 -0
  13. package/dist/add/add-keyed-wiring.d.ts +12 -0
  14. package/dist/add/add-keyed-wiring.js +97 -0
  15. package/dist/add/add-mcp-prompt.js +14 -9
  16. package/dist/add/add-mcp-resource.js +14 -9
  17. package/dist/add/add-middleware.d.ts +1 -4
  18. package/dist/add/add-middleware.js +364 -79
  19. package/dist/add/add-permission.d.ts +1 -1
  20. package/dist/add/add-permission.js +152 -40
  21. package/dist/add/add-queue-worker.js +18 -12
  22. package/dist/add/add-rpc-invocations.d.ts +3 -0
  23. package/dist/add/add-rpc-invocations.js +65 -25
  24. package/dist/add/add-schedule.js +11 -5
  25. package/dist/add/add-secret.d.ts +3 -0
  26. package/dist/add/add-secret.js +82 -0
  27. package/dist/add/add-trigger.d.ts +2 -0
  28. package/dist/add/add-trigger.js +87 -0
  29. package/dist/add/add-variable.d.ts +1 -0
  30. package/dist/add/add-variable.js +8 -0
  31. package/dist/add/add-workflow-graph.d.ts +7 -0
  32. package/dist/add/add-workflow-graph.js +396 -0
  33. package/dist/add/add-workflow.js +124 -26
  34. package/dist/error-codes.d.ts +16 -1
  35. package/dist/error-codes.js +21 -1
  36. package/dist/index.d.ts +9 -5
  37. package/dist/index.js +5 -2
  38. package/dist/inspector.d.ts +1 -1
  39. package/dist/inspector.js +106 -13
  40. package/dist/schema-generator.d.ts +1 -0
  41. package/dist/schema-generator.js +1 -0
  42. package/dist/types-map.js +10 -1
  43. package/dist/types.d.ts +180 -30
  44. package/dist/utils/compute-required-schemas.d.ts +4 -0
  45. package/dist/utils/compute-required-schemas.js +41 -0
  46. package/dist/utils/contract-hashes.d.ts +35 -0
  47. package/dist/utils/contract-hashes.js +202 -0
  48. package/dist/utils/custom-types-generator.d.ts +9 -0
  49. package/dist/utils/custom-types-generator.js +71 -0
  50. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  51. package/dist/utils/detect-schema-vendor.js +76 -0
  52. package/dist/utils/ensure-function-metadata.d.ts +5 -2
  53. package/dist/utils/ensure-function-metadata.js +220 -6
  54. package/dist/utils/extract-function-name.d.ts +5 -16
  55. package/dist/utils/extract-function-name.js +93 -298
  56. package/dist/utils/extract-services.d.ts +2 -1
  57. package/dist/utils/extract-services.js +25 -1
  58. package/dist/utils/filter-inspector-state.js +107 -23
  59. package/dist/utils/get-property-value.d.ts +8 -2
  60. package/dist/utils/get-property-value.js +33 -4
  61. package/dist/utils/hash.d.ts +2 -0
  62. package/dist/utils/hash.js +23 -0
  63. package/dist/utils/middleware.d.ts +7 -30
  64. package/dist/utils/middleware.js +80 -66
  65. package/dist/utils/permissions.d.ts +2 -2
  66. package/dist/utils/permissions.js +10 -10
  67. package/dist/utils/post-process.d.ts +9 -10
  68. package/dist/utils/post-process.js +231 -24
  69. package/dist/utils/resolve-external-package.d.ts +12 -0
  70. package/dist/utils/resolve-external-package.js +34 -0
  71. package/dist/utils/resolve-function-types.d.ts +6 -0
  72. package/dist/utils/resolve-function-types.js +29 -0
  73. package/dist/utils/resolve-identifier.d.ts +10 -0
  74. package/dist/utils/resolve-identifier.js +36 -0
  75. package/dist/utils/resolve-versions.d.ts +2 -0
  76. package/dist/utils/resolve-versions.js +78 -0
  77. package/dist/utils/schema-generator.d.ts +9 -0
  78. package/dist/utils/schema-generator.js +209 -0
  79. package/dist/utils/serialize-inspector-state.d.ts +73 -13
  80. package/dist/utils/serialize-inspector-state.js +102 -6
  81. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  82. package/dist/utils/serialize-mcp-json.js +99 -0
  83. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  84. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  85. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  86. package/dist/utils/serialize-openapi-json.js +151 -0
  87. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  88. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  89. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  90. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +830 -0
  91. package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
  92. package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +572 -72
  93. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  94. package/dist/utils/workflow/dsl/index.js +7 -0
  95. package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
  96. package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
  97. package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
  98. package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
  99. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  100. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +318 -0
  101. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  102. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  103. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  104. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  105. package/dist/utils/workflow/graph/index.d.ts +8 -0
  106. package/dist/utils/workflow/graph/index.js +8 -0
  107. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +35 -0
  108. package/dist/utils/workflow/graph/serialize-workflow-graph.js +150 -0
  109. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +203 -0
  110. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  111. package/dist/visit.js +13 -2
  112. package/package.json +26 -4
  113. package/src/add/add-ai-agent.ts +468 -0
  114. package/src/add/add-channel.ts +82 -79
  115. package/src/add/add-cli.ts +49 -20
  116. package/src/add/add-file-with-factory.ts +2 -0
  117. package/src/add/add-functions.ts +429 -71
  118. package/src/add/add-http-route.ts +246 -65
  119. package/src/add/add-http-routes.ts +228 -0
  120. package/src/add/add-keyed-wiring.ts +151 -0
  121. package/src/add/add-mcp-prompt.ts +26 -15
  122. package/src/add/add-mcp-resource.ts +27 -15
  123. package/src/add/add-middleware.ts +482 -80
  124. package/src/add/add-permission.ts +199 -40
  125. package/src/add/add-queue-worker.ts +24 -19
  126. package/src/add/add-rpc-invocations.ts +78 -31
  127. package/src/add/add-schedule.ts +16 -11
  128. package/src/add/add-secret.ts +140 -0
  129. package/src/add/add-trigger.ts +154 -0
  130. package/src/add/add-variable.ts +9 -0
  131. package/src/add/add-workflow-graph.ts +522 -0
  132. package/src/add/add-workflow.ts +117 -30
  133. package/src/error-codes.ts +26 -1
  134. package/src/index.ts +27 -8
  135. package/src/inspector.ts +145 -17
  136. package/src/schema-generator.ts +1 -0
  137. package/src/types-map.ts +12 -1
  138. package/src/types.ts +192 -51
  139. package/src/utils/compute-required-schemas.ts +49 -0
  140. package/src/utils/contract-hashes.test.ts +528 -0
  141. package/src/utils/contract-hashes.ts +290 -0
  142. package/src/utils/custom-types-generator.ts +88 -0
  143. package/src/utils/detect-schema-vendor.ts +90 -0
  144. package/src/utils/ensure-function-metadata.ts +324 -7
  145. package/src/utils/extract-function-name.ts +108 -358
  146. package/src/utils/extract-services.ts +35 -2
  147. package/src/utils/filter-inspector-state.test.ts +34 -20
  148. package/src/utils/filter-inspector-state.ts +140 -31
  149. package/src/utils/get-property-value.ts +50 -5
  150. package/src/utils/hash.ts +26 -0
  151. package/src/utils/middleware.test.ts +204 -0
  152. package/src/utils/middleware.ts +129 -67
  153. package/src/utils/permissions.test.ts +35 -12
  154. package/src/utils/permissions.ts +10 -10
  155. package/src/utils/post-process.ts +283 -43
  156. package/src/utils/resolve-external-package.ts +42 -0
  157. package/src/utils/resolve-function-types.ts +42 -0
  158. package/src/utils/resolve-identifier.ts +46 -0
  159. package/src/utils/resolve-versions.test.ts +249 -0
  160. package/src/utils/resolve-versions.ts +105 -0
  161. package/src/utils/schema-generator.ts +329 -0
  162. package/src/utils/serialize-inspector-state.ts +181 -20
  163. package/src/utils/serialize-mcp-json.ts +145 -0
  164. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  165. package/src/utils/serialize-openapi-json.ts +277 -0
  166. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  167. package/src/utils/test-data/inspector-state.json +69 -66
  168. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1104 -0
  169. package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +678 -85
  170. package/src/utils/workflow/dsl/index.ts +11 -0
  171. package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
  172. package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
  173. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +422 -0
  174. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  175. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  176. package/src/utils/workflow/graph/index.ts +11 -0
  177. package/src/utils/workflow/graph/serialize-workflow-graph.ts +216 -0
  178. package/src/utils/workflow/graph/workflow-graph.types.ts +231 -0
  179. package/src/visit.ts +14 -2
  180. package/tsconfig.tsbuildinfo +1 -1
  181. package/dist/add/add-mcp-tool.d.ts +0 -2
  182. package/dist/add/add-mcp-tool.js +0 -81
  183. package/dist/utils/extract-service-metadata.d.ts +0 -19
  184. package/dist/utils/extract-service-metadata.js +0 -244
  185. package/dist/utils/write-service-metadata.d.ts +0 -13
  186. package/dist/utils/write-service-metadata.js +0 -37
  187. package/src/add/add-mcp-tool.ts +0 -141
  188. package/src/utils/extract-service-metadata.ts +0 -353
  189. package/src/utils/write-service-metadata.ts +0 -51
@@ -1,8 +1,14 @@
1
1
  import * as ts from 'typescript';
2
- import { extractFunctionName } from '../utils/extract-function-name.js';
2
+ import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
3
+ import { extractFunctionName, funcIdToTypeName, } from '../utils/extract-function-name.js';
3
4
  import { extractFunctionNode } from '../utils/extract-function-node.js';
5
+ import { extractUsedWires } from '../utils/extract-services.js';
6
+ import { formatVersionedId } from '@pikku/core';
4
7
  import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
5
8
  import { resolveMiddleware } from '../utils/middleware.js';
9
+ import { resolvePermissions } from '../utils/permissions.js';
10
+ import { extractWireNames } from '../utils/post-process.js';
11
+ import { ErrorCode } from '../error-codes.js';
6
12
  const isValidVariableName = (name) => {
7
13
  const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
8
14
  return regex.test(name);
@@ -11,7 +17,8 @@ const nullifyTypes = (type) => {
11
17
  if (type === 'void' ||
12
18
  type === 'undefined' ||
13
19
  type === 'unknown' ||
14
- type === 'any') {
20
+ type === 'any' ||
21
+ type === 'null') {
15
22
  return null;
16
23
  }
17
24
  return type;
@@ -109,7 +116,7 @@ const resolveUnionTypes = (checker, type) => {
109
116
  // Check if it's a union type AND not part of an intersection
110
117
  if (type.isUnion() && !(type.flags & ts.TypeFlags.Intersection)) {
111
118
  for (const t of type.types) {
112
- const name = nullifyTypes(checker.typeToString(t));
119
+ const name = nullifyTypes(checker.typeToString(t, undefined, ts.TypeFormatFlags.NoTruncation));
113
120
  if (name) {
114
121
  types.push(t);
115
122
  names.push(name);
@@ -117,7 +124,7 @@ const resolveUnionTypes = (checker, type) => {
117
124
  }
118
125
  }
119
126
  else {
120
- const name = nullifyTypes(checker.typeToString(type));
127
+ const name = nullifyTypes(checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation));
121
128
  if (name) {
122
129
  types.push(type);
123
130
  names.push(name);
@@ -129,8 +136,8 @@ const getNamesAndTypes = (checker, typesMap, direction, funcName, type) => {
129
136
  if (!type) {
130
137
  return { names: [], types: [] };
131
138
  }
132
- // 1) Handle an explicit void (or undefined) type up front
133
- if (type.flags & ts.TypeFlags.VoidLike) {
139
+ // 1) Handle an explicit void (or undefined or null) type up front
140
+ if (type.flags & (ts.TypeFlags.VoidLike | ts.TypeFlags.Null)) {
134
141
  return {
135
142
  names: [],
136
143
  types: [],
@@ -145,7 +152,7 @@ const getNamesAndTypes = (checker, typesMap, direction, funcName, type) => {
145
152
  const firstName = rawNames[0];
146
153
  if (rawNames.length > 1 || (firstName && !isValidVariableName(firstName))) {
147
154
  const aliasType = rawNames.join(' | ');
148
- const aliasName = funcName.charAt(0).toUpperCase() + funcName.slice(1) + direction;
155
+ const aliasName = funcIdToTypeName(funcName) + direction;
149
156
  // record the alias in your TypesMap
150
157
  const references = rawTypes
151
158
  .map((t) => resolveTypeImports(t, typesMap, true, checker))
@@ -184,7 +191,6 @@ const isPrimitiveType = (type) => {
184
191
  ts.TypeFlags.Void |
185
192
  ts.TypeFlags.Undefined |
186
193
  ts.TypeFlags.Null |
187
- ts.TypeFlags.Any |
188
194
  ts.TypeFlags.Unknown |
189
195
  ts.TypeFlags.VoidLike;
190
196
  return (type.flags & primitiveFlags) !== 0;
@@ -211,7 +217,7 @@ function unwrapPromise(checker, type) {
211
217
  * Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
212
218
  * then push into state.functions.meta.
213
219
  */
214
- export const addFunctions = (logger, node, checker, state) => {
220
+ export const addFunctions = (logger, node, checker, state, options) => {
215
221
  if (!ts.isCallExpression(node))
216
222
  return;
217
223
  const { expression, arguments: args, typeArguments } = node;
@@ -230,28 +236,153 @@ export const addFunctions = (logger, node, checker, state) => {
230
236
  }
231
237
  if (args.length === 0)
232
238
  return;
233
- const { pikkuFuncName, name, explicitName, exportedName } = extractFunctionName(node, checker, state.rootDir);
239
+ let { pikkuFuncId, name, explicitName, exportedName } = extractFunctionName(node, checker, state.rootDir);
240
+ if (!pikkuFuncId || pikkuFuncId.startsWith('__temp_')) {
241
+ return;
242
+ }
243
+ let title;
234
244
  let tags;
235
245
  let summary;
236
246
  let description;
237
247
  let errors;
238
248
  let expose;
239
- let internal;
249
+ let remote;
250
+ let mcp;
251
+ let requiresApproval;
252
+ let version;
240
253
  let objectNode;
254
+ let nodeDisplayName = null;
255
+ let nodeCategory = null;
256
+ let nodeType = null;
257
+ let nodeErrorOutput = null;
241
258
  // Extract the function node using shared utility
242
259
  const firstArg = args[0];
243
260
  const { funcNode: handlerNode, resolvedFunc, isDirectFunction, } = extractFunctionNode(firstArg, checker);
261
+ // Variables to hold schema references if provided
262
+ let inputSchemaRef = null;
263
+ let outputSchemaRef = null;
264
+ // Helper to resolve schema identifier to its actual source file and detect vendor.
265
+ // Logs a fatal error and returns null if vendor cannot be determined.
266
+ const resolveSchemaRef = (identifier, context) => {
267
+ const symbol = checker.getSymbolAtLocation(identifier);
268
+ if (!symbol)
269
+ return null;
270
+ const decl = symbol.valueDeclaration || symbol.declarations?.[0];
271
+ if (!decl)
272
+ return null;
273
+ let sourceFile;
274
+ // If it's an import specifier, resolve the aliased symbol to get the actual source
275
+ if (ts.isImportSpecifier(decl)) {
276
+ const aliasedSymbol = checker.getAliasedSymbol(symbol);
277
+ if (aliasedSymbol) {
278
+ const aliasedDecl = aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0];
279
+ if (aliasedDecl) {
280
+ sourceFile = aliasedDecl.getSourceFile().fileName;
281
+ }
282
+ else {
283
+ return null;
284
+ }
285
+ }
286
+ else {
287
+ return null;
288
+ }
289
+ }
290
+ else {
291
+ sourceFile = decl.getSourceFile().fileName;
292
+ }
293
+ const vendor = detectSchemaVendorOrError(identifier, checker, logger, context, sourceFile);
294
+ if (!vendor)
295
+ return null;
296
+ return {
297
+ variableName: identifier.text,
298
+ sourceFile,
299
+ vendor,
300
+ };
301
+ };
244
302
  // Extract config properties if using object form
245
303
  if (ts.isObjectLiteralExpression(firstArg)) {
246
304
  objectNode = firstArg;
247
305
  const metadata = getCommonWireMetaData(firstArg, 'Function', name, logger);
306
+ if (metadata.disabled)
307
+ return;
308
+ title = metadata.title;
248
309
  tags = metadata.tags;
249
310
  summary = metadata.summary;
250
311
  description = metadata.description;
251
312
  errors = metadata.errors;
252
313
  expose = getPropertyValue(firstArg, 'expose');
253
- internal = getPropertyValue(firstArg, 'internal');
314
+ remote = getPropertyValue(firstArg, 'remote');
315
+ mcp = getPropertyValue(firstArg, 'mcp');
316
+ requiresApproval = getPropertyValue(firstArg, 'requiresApproval');
317
+ const versionRaw = getPropertyValue(firstArg, 'version');
318
+ if (versionRaw !== null && versionRaw !== undefined) {
319
+ const parsed = Number(versionRaw);
320
+ if (Number.isInteger(parsed) && parsed >= 1) {
321
+ version = parsed;
322
+ }
323
+ }
324
+ // Extract node config from nested object
325
+ for (const prop of firstArg.properties) {
326
+ if (ts.isPropertyAssignment(prop) &&
327
+ ts.isIdentifier(prop.name) &&
328
+ prop.name.text === 'node' &&
329
+ ts.isObjectLiteralExpression(prop.initializer)) {
330
+ const nodeObj = prop.initializer;
331
+ nodeDisplayName = getPropertyValue(nodeObj, 'displayName');
332
+ nodeCategory = getPropertyValue(nodeObj, 'category');
333
+ nodeType = getPropertyValue(nodeObj, 'type');
334
+ nodeErrorOutput = getPropertyValue(nodeObj, 'errorOutput');
335
+ if (!nodeDisplayName) {
336
+ logger.critical(ErrorCode.MISSING_NAME, `Function '${name}' node config is missing the required 'displayName' property.`);
337
+ }
338
+ if (!nodeCategory) {
339
+ logger.critical(ErrorCode.MISSING_NAME, `Function '${name}' node config is missing the required 'category' property.`);
340
+ }
341
+ if (!nodeType) {
342
+ logger.critical(ErrorCode.MISSING_NAME, `Function '${name}' node config is missing the required 'type' property.`);
343
+ }
344
+ else if (!['trigger', 'action', 'end'].includes(nodeType)) {
345
+ logger.critical(ErrorCode.INVALID_VALUE, `Function '${name}' node config has invalid type '${nodeType}'. Must be 'trigger', 'action', or 'end'.`);
346
+ }
347
+ break;
348
+ }
349
+ }
350
+ // Extract schema variable names from input/output properties
351
+ for (const prop of firstArg.properties) {
352
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
353
+ const propName = prop.name.text;
354
+ if (propName === 'input' || propName === 'output') {
355
+ if (ts.isIdentifier(prop.initializer)) {
356
+ // Good - it's a variable reference, resolve its actual source file and vendor
357
+ const context = `Function '${name}' ${propName}`;
358
+ const ref = resolveSchemaRef(prop.initializer, context);
359
+ if (ref) {
360
+ if (propName === 'input') {
361
+ inputSchemaRef = ref;
362
+ }
363
+ else {
364
+ outputSchemaRef = ref;
365
+ }
366
+ }
367
+ }
368
+ else if (ts.isCallExpression(prop.initializer)) {
369
+ // Bad - it's an inline expression
370
+ const schemaName = `${funcIdToTypeName(name)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
371
+ logger.critical(ErrorCode.INLINE_SCHEMA, `Inline schemas are not supported for '${propName}' in '${name}'.\n` +
372
+ ` Extract to an exported variable:\n` +
373
+ ` export const ${schemaName} = ${prop.initializer.getText()}\n` +
374
+ ` Then use: ${propName}: ${schemaName}`);
375
+ }
376
+ }
377
+ }
378
+ }
379
+ }
380
+ if (version !== undefined) {
381
+ const baseName = explicitName || exportedName || pikkuFuncId;
382
+ pikkuFuncId = formatVersionedId(baseName, version);
254
383
  }
384
+ const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc';
385
+ const mcpEnabled = mcp || isMCPToolFunc;
255
386
  // Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
256
387
  const handler = resolvedFunc &&
257
388
  (ts.isArrowFunction(resolvedFunc) || ts.isFunctionExpression(resolvedFunc))
@@ -259,10 +390,11 @@ export const addFunctions = (logger, node, checker, state) => {
259
390
  : handlerNode;
260
391
  // Validate that we got a valid function
261
392
  if (!ts.isArrowFunction(handler) && !ts.isFunctionExpression(handler)) {
262
- logger.error(`• No valid 'func' property found for ${pikkuFuncName}.`);
393
+ logger.error(`• No valid 'func' property found for ${pikkuFuncId}.`);
263
394
  // Create stub metadata to prevent "function not found" errors in wirings
264
- state.functions.meta[pikkuFuncName] = {
265
- pikkuFuncName,
395
+ state.functions.meta[pikkuFuncId] = {
396
+ pikkuFuncId,
397
+ functionType: 'user',
266
398
  name,
267
399
  services: { optimized: false, services: [] },
268
400
  inputSchemaName: null,
@@ -291,39 +423,51 @@ export const addFunctions = (logger, node, checker, state) => {
291
423
  }
292
424
  }
293
425
  }
294
- else if (ts.isIdentifier(firstParam.name)) {
426
+ else if (ts.isIdentifier(firstParam.name) &&
427
+ !firstParam.name.text.startsWith('_')) {
295
428
  services.optimized = false;
296
429
  }
297
430
  }
298
- // --- Extract used wires from third parameter ---
299
- const usedWires = [];
300
- const thirdParam = handler.parameters[2];
301
- if (thirdParam && ts.isObjectBindingPattern(thirdParam.name)) {
302
- for (const elem of thirdParam.name.elements) {
303
- const propertyName = elem.propertyName && ts.isIdentifier(elem.propertyName)
304
- ? elem.propertyName.text
305
- : ts.isIdentifier(elem.name)
306
- ? elem.name.text
307
- : undefined;
308
- if (propertyName) {
309
- usedWires.push(propertyName);
310
- }
311
- }
312
- }
431
+ const wires = extractUsedWires(handler, 2);
313
432
  // --- Generics → ts.Type[], unwrapped from Promise ---
314
433
  const genericTypes = (typeArguments ?? [])
315
434
  .map((tn) => checker.getTypeFromTypeNode(tn))
316
435
  .map((t) => unwrapPromise(checker, t));
436
+ const capitalizedName = funcIdToTypeName(name);
317
437
  // --- Input Extraction ---
318
- let { names: inputNames, types: inputTypes } = getNamesAndTypes(checker, state.functions.typesMap, 'Input', name, genericTypes[0]);
319
- // if (inputTypes.length === 0) {
320
- // logger.debug(
321
- // `\x1b[31m• Unknown input type for '${name}', assuming void.\x1b[0m`
322
- // )
323
- // }
438
+ let inputNames = [];
439
+ let inputTypes = [];
440
+ if (inputSchemaRef) {
441
+ const schemaName = `${capitalizedName}Input`;
442
+ inputNames = [schemaName];
443
+ state.schemaLookup.set(schemaName, inputSchemaRef);
444
+ state.functions.typesMap.addCustomType(schemaName, 'unknown', []);
445
+ }
446
+ else if (genericTypes.length >= 1 && genericTypes[0]) {
447
+ // Fall back to extracting from generic type arguments
448
+ const result = getNamesAndTypes(checker, state.functions.typesMap, 'Input', name, genericTypes[0]);
449
+ inputNames = result.names;
450
+ inputTypes = result.types;
451
+ }
452
+ else {
453
+ // Fall back to extracting from the function's second parameter type
454
+ const secondParam = handler.parameters[1];
455
+ if (secondParam) {
456
+ const paramType = checker.getTypeAtLocation(secondParam);
457
+ const result = getNamesAndTypes(checker, state.functions.typesMap, 'Input', pikkuFuncId, paramType);
458
+ inputNames = result.names;
459
+ inputTypes = result.types;
460
+ }
461
+ }
324
462
  // --- Output Extraction ---
325
463
  let outputNames = [];
326
- if (genericTypes.length >= 2) {
464
+ if (outputSchemaRef) {
465
+ const schemaName = `${capitalizedName}Output`;
466
+ outputNames = [schemaName];
467
+ state.schemaLookup.set(schemaName, outputSchemaRef);
468
+ state.functions.typesMap.addCustomType(schemaName, 'unknown', []);
469
+ }
470
+ else if (genericTypes.length >= 2) {
327
471
  outputNames = getNamesAndTypes(checker, state.functions.typesMap, 'Output', name, genericTypes[1]).names;
328
472
  }
329
473
  else {
@@ -331,74 +475,198 @@ export const addFunctions = (logger, node, checker, state) => {
331
475
  if (sig) {
332
476
  const rawRet = checker.getReturnTypeOfSignature(sig);
333
477
  const unwrapped = unwrapPromise(checker, rawRet);
334
- outputNames = getNamesAndTypes(checker, state.functions.typesMap, 'Output', pikkuFuncName, unwrapped).names;
478
+ outputNames = getNamesAndTypes(checker, state.functions.typesMap, 'Output', pikkuFuncId, unwrapped).names;
479
+ }
480
+ }
481
+ const mcpOutputTypes = {
482
+ pikkuMCPResourceFunc: 'MCPResourceResponse',
483
+ pikkuMCPToolFunc: 'MCPToolResponse',
484
+ pikkuMCPPromptFunc: 'MCPPromptResponse',
485
+ };
486
+ const mcpOutputType = mcpOutputTypes[expression.text];
487
+ if (mcpOutputType && outputNames[0] !== mcpOutputType) {
488
+ let resolved = false;
489
+ const rawSymbol = checker.getSymbolAtLocation(expression);
490
+ const funcSymbol = rawSymbol && rawSymbol.flags & ts.SymbolFlags.Alias
491
+ ? checker.getAliasedSymbol(rawSymbol)
492
+ : rawSymbol;
493
+ const funcDecls = funcSymbol?.getDeclarations() || [];
494
+ for (const funcDecl of funcDecls) {
495
+ if (resolved)
496
+ break;
497
+ const mcpTypeSymbol = checker.resolveName(mcpOutputType, funcDecl, ts.SymbolFlags.Type, false);
498
+ if (mcpTypeSymbol) {
499
+ const aliased = mcpTypeSymbol.flags & ts.SymbolFlags.Alias
500
+ ? checker.getAliasedSymbol(mcpTypeSymbol)
501
+ : mcpTypeSymbol;
502
+ const typeDecl = aliased?.getDeclarations()?.[0];
503
+ if (typeDecl) {
504
+ const path = typeDecl.getSourceFile().fileName;
505
+ if (!state.functions.typesMap.exists(mcpOutputType, path)) {
506
+ state.functions.typesMap.addType(mcpOutputType, path);
507
+ }
508
+ resolved = true;
509
+ }
510
+ }
511
+ }
512
+ if (!resolved) {
513
+ state.functions.typesMap.addCustomType(mcpOutputType, mcpOutputType, []);
335
514
  }
515
+ outputNames = [mcpOutputType];
336
516
  }
337
517
  if (inputNames.length > 1) {
338
518
  logger.warn('More than one input type detected, only the first one will be used as a schema.');
339
519
  }
340
520
  // Store the input type for later use
341
521
  if (inputTypes.length > 0) {
342
- state.typesLookup.set(pikkuFuncName, inputTypes);
522
+ state.typesLookup.set(pikkuFuncId, inputTypes);
343
523
  }
344
524
  // --- resolve middleware ---
345
- const middleware = objectNode
525
+ let middleware = objectNode
346
526
  ? resolveMiddleware(state, objectNode, tags, checker)
347
527
  : undefined;
348
- state.functions.meta[pikkuFuncName] = {
349
- pikkuFuncName,
528
+ // --- resolve permissions ---
529
+ let permissions = objectNode
530
+ ? resolvePermissions(state, objectNode, tags, checker)
531
+ : undefined;
532
+ if (options.tags?.length) {
533
+ tags = [...new Set([...(tags || []), ...options.tags])];
534
+ const tagEntries = options.tags.map((tag) => ({
535
+ type: 'tag',
536
+ tag,
537
+ }));
538
+ const existingMiddlewareTags = new Set((middleware || [])
539
+ .filter((m) => m.type === 'tag')
540
+ .map((m) => m.tag));
541
+ const newMiddleware = tagEntries.filter((e) => !existingMiddlewareTags.has(e.tag));
542
+ if (newMiddleware.length > 0) {
543
+ middleware = [...(middleware || []), ...newMiddleware];
544
+ }
545
+ const existingPermissionTags = new Set((permissions || [])
546
+ .filter((p) => p.type === 'tag')
547
+ .map((p) => p.tag));
548
+ const newPermissions = tagEntries.filter((e) => !existingPermissionTags.has(e.tag));
549
+ if (newPermissions.length > 0) {
550
+ permissions = [...(permissions || []), ...newPermissions];
551
+ }
552
+ }
553
+ const sessionless = expression.text !== 'pikkuFunc';
554
+ state.functions.meta[pikkuFuncId] = {
555
+ pikkuFuncId,
556
+ functionType: 'user',
557
+ funcWrapper: expression.text,
558
+ sessionless,
350
559
  name,
351
560
  services,
352
- usedWires: usedWires.length > 0 ? usedWires : undefined,
561
+ wires: wires.wires.length > 0 || !wires.optimized ? wires : undefined,
353
562
  inputSchemaName: inputNames[0] ?? null,
354
563
  outputSchemaName: outputNames[0] ?? null,
355
564
  inputs: inputNames.filter((n) => n !== 'void') ?? null,
356
565
  outputs: outputNames.filter((n) => n !== 'void') ?? null,
357
566
  expose: expose || undefined,
358
- internal: internal || undefined,
567
+ remote: remote || undefined,
568
+ mcp: mcpEnabled || undefined,
569
+ requiresApproval: requiresApproval || undefined,
570
+ version,
571
+ title,
359
572
  tags: tags || undefined,
360
573
  summary,
361
574
  description,
362
575
  errors,
363
576
  middleware,
577
+ permissions,
364
578
  isDirectFunction,
365
579
  };
580
+ // Populate node metadata if node config is present
581
+ if (nodeDisplayName && nodeCategory && nodeType) {
582
+ state.nodes.files.add(node.getSourceFile().fileName);
583
+ state.nodes.meta[pikkuFuncId] = {
584
+ name: pikkuFuncId,
585
+ displayName: nodeDisplayName,
586
+ category: nodeCategory,
587
+ type: nodeType,
588
+ rpc: pikkuFuncId,
589
+ description,
590
+ errorOutput: nodeErrorOutput ?? false,
591
+ inputSchemaName: inputNames[0] ?? null,
592
+ outputSchemaName: outputNames[0] ?? null,
593
+ tags,
594
+ };
595
+ }
596
+ if (mcpEnabled) {
597
+ if (!description) {
598
+ logger.critical(ErrorCode.MISSING_DESCRIPTION, `MCP tool '${name}' is missing a description.`);
599
+ return;
600
+ }
601
+ state.mcpEndpoints.files.add(node.getSourceFile().fileName);
602
+ state.mcpEndpoints.toolsMeta[name] = {
603
+ pikkuFuncId,
604
+ name,
605
+ title: title || undefined,
606
+ description,
607
+ summary,
608
+ errors,
609
+ tags,
610
+ inputSchema: inputNames[0] ?? null,
611
+ outputSchema: outputNames[0] ?? null,
612
+ middleware,
613
+ permissions,
614
+ };
615
+ state.serviceAggregation.usedFunctions.add(pikkuFuncId);
616
+ extractWireNames(middleware).forEach((n) => state.serviceAggregation.usedMiddleware.add(n));
617
+ extractWireNames(permissions).forEach((n) => state.serviceAggregation.usedPermissions.add(n));
618
+ }
619
+ // Workflow functions don't get registered as RPC functions,
620
+ // they are their own type handled by add-workflow
621
+ if (expression.text.includes('Workflow')) {
622
+ return;
623
+ }
624
+ // Trigger and channel connect/disconnect functions are not callable via RPC
625
+ const nonRPCPatterns = [
626
+ /Trigger/i,
627
+ /ChannelConnection/i,
628
+ /ChannelDisconnection/i,
629
+ ];
630
+ if (nonRPCPatterns.some((pattern) => pattern.test(expression.text))) {
631
+ return;
632
+ }
366
633
  // Store function file location for wiring generation
367
634
  if (exportedName) {
368
- state.functions.files.set(pikkuFuncName, {
635
+ state.functions.files.set(pikkuFuncId, {
369
636
  path: node.getSourceFile().fileName,
370
637
  exportedName,
371
638
  });
372
639
  }
373
- // Workflow functions don't get registered as RPC functions,
374
- // they are their own type handled by add-workdflow
375
- if (expression.text.includes('Workflow')) {
376
- return;
377
- }
378
640
  if (exportedName || explicitName) {
379
641
  if (!exportedName) {
380
642
  logger.error(`• Function with explicit name '${name}' is not exported, this is not allowed.`);
381
643
  return;
382
644
  }
383
- // Mark internal functions as invoked to force bundling
384
- if (internal) {
385
- state.rpc.invokedFunctions.add(pikkuFuncName);
645
+ if (remote) {
646
+ state.rpc.invokedFunctions.add(pikkuFuncId);
386
647
  }
387
648
  if (expose) {
388
- state.rpc.exposedMeta[name] = pikkuFuncName;
649
+ state.rpc.exposedMeta[name] = pikkuFuncId;
389
650
  state.rpc.exposedFiles.set(name, {
390
651
  path: node.getSourceFile().fileName,
391
652
  exportedName,
392
653
  });
393
654
  // Track exposed RPC function for service aggregation
394
- state.serviceAggregation.usedFunctions.add(pikkuFuncName);
655
+ state.serviceAggregation.usedFunctions.add(pikkuFuncId);
395
656
  }
396
657
  // We add it to internal meta to allow autocomplete for everything
397
- state.rpc.internalMeta[name] = pikkuFuncName;
658
+ state.rpc.internalMeta[name] = pikkuFuncId;
659
+ if (version !== undefined) {
660
+ state.rpc.internalMeta[pikkuFuncId] = pikkuFuncId;
661
+ state.rpc.invokedFunctions.add(pikkuFuncId);
662
+ }
398
663
  // But we only import the actual function if it's actually invoked to keep
399
664
  // bundle size down
400
- if (state.rpc.invokedFunctions.has(pikkuFuncName) || expose || internal) {
401
- state.rpc.internalFiles.set(pikkuFuncName, {
665
+ if (state.rpc.invokedFunctions.has(pikkuFuncId) ||
666
+ expose ||
667
+ remote ||
668
+ mcpEnabled) {
669
+ state.rpc.internalFiles.set(pikkuFuncId, {
402
670
  path: node.getSourceFile().fileName,
403
671
  exportedName,
404
672
  });
@@ -1,15 +1,24 @@
1
- import { AddWiring } from '../types.js';
1
+ import * as ts from 'typescript';
2
+ import { AddWiring, InspectorState } from '../types.js';
3
+ import type { InspectorLogger } from '../types.js';
2
4
  /**
3
- * Populate metaInputTypes for a given route based on method, input type,
4
- * query and params. Returns undefined (we only mutate metaTypes).
5
+ * Parameters for registering an HTTP route
5
6
  */
6
- export declare const getInputTypes: (metaTypes: Map<string, {
7
- query?: string[];
8
- params?: string[];
9
- body?: string[];
10
- }>, methodType: string, inputType: string | null, queryValues: string[], paramsValues: string[]) => undefined;
7
+ export interface RegisterHTTPRouteParams {
8
+ obj: ts.ObjectLiteralExpression;
9
+ state: InspectorState;
10
+ checker: ts.TypeChecker;
11
+ logger: InspectorLogger;
12
+ sourceFile: ts.SourceFile;
13
+ basePath?: string;
14
+ inheritedTags?: string[];
15
+ }
11
16
  /**
12
- * Simplified wireHTTP: re-uses function metadata from state.functions.meta
13
- * instead of re-inferring types here.
17
+ * Shared function to register an HTTP route in the inspector state.
18
+ * Used by both wireHTTP and wireHTTPRoutes.
19
+ */
20
+ export declare function registerHTTPRoute({ obj, state, checker, logger, sourceFile, basePath, inheritedTags, }: RegisterHTTPRouteParams): void;
21
+ /**
22
+ * Process wireHTTP calls
14
23
  */
15
24
  export declare const addHTTPRoute: AddWiring;