@pikku/inspector 0.11.2 → 0.12.1

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 (211) hide show
  1. package/CHANGELOG.md +36 -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 +81 -61
  6. package/dist/add/add-cli.d.ts +1 -1
  7. package/dist/add/add-cli.js +42 -19
  8. package/dist/add/add-file-extends-core-type.d.ts +1 -1
  9. package/dist/add/add-file-with-config.d.ts +1 -1
  10. package/dist/add/add-file-with-factory.d.ts +1 -1
  11. package/dist/add/add-file-with-factory.js +2 -0
  12. package/dist/add/add-functions.d.ts +1 -1
  13. package/dist/add/add-functions.js +256 -82
  14. package/dist/add/add-http-route.d.ts +20 -10
  15. package/dist/add/add-http-route.js +156 -66
  16. package/dist/add/add-http-routes.d.ts +5 -0
  17. package/dist/add/add-http-routes.js +160 -0
  18. package/dist/add/add-keyed-wiring.d.ts +12 -0
  19. package/dist/add/add-keyed-wiring.js +97 -0
  20. package/dist/add/add-mcp-prompt.d.ts +1 -1
  21. package/dist/add/add-mcp-prompt.js +14 -9
  22. package/dist/add/add-mcp-resource.d.ts +1 -1
  23. package/dist/add/add-mcp-resource.js +14 -9
  24. package/dist/add/add-middleware.d.ts +1 -4
  25. package/dist/add/add-middleware.js +364 -79
  26. package/dist/add/add-permission.d.ts +1 -1
  27. package/dist/add/add-permission.js +152 -40
  28. package/dist/add/add-queue-worker.d.ts +1 -1
  29. package/dist/add/add-queue-worker.js +18 -12
  30. package/dist/add/add-rpc-invocations.d.ts +3 -3
  31. package/dist/add/add-rpc-invocations.js +24 -10
  32. package/dist/add/add-schedule.d.ts +1 -1
  33. package/dist/add/add-schedule.js +11 -5
  34. package/dist/add/add-secret.d.ts +3 -0
  35. package/dist/add/add-secret.js +82 -0
  36. package/dist/add/add-trigger.d.ts +2 -0
  37. package/dist/add/add-trigger.js +87 -0
  38. package/dist/add/add-variable.d.ts +1 -0
  39. package/dist/add/add-variable.js +8 -0
  40. package/dist/add/add-wire-addon.d.ts +7 -0
  41. package/dist/add/add-wire-addon.js +70 -0
  42. package/dist/add/add-workflow-graph.d.ts +3 -2
  43. package/dist/add/add-workflow-graph.js +143 -406
  44. package/dist/add/add-workflow.d.ts +1 -1
  45. package/dist/add/add-workflow.js +6 -4
  46. package/dist/error-codes.d.ts +15 -1
  47. package/dist/error-codes.js +20 -1
  48. package/dist/index.d.ts +9 -8
  49. package/dist/index.js +5 -4
  50. package/dist/inspector.d.ts +2 -2
  51. package/dist/inspector.js +95 -15
  52. package/dist/schema-generator.d.ts +1 -0
  53. package/dist/schema-generator.js +1 -0
  54. package/dist/types-map.js +10 -1
  55. package/dist/types.d.ts +180 -50
  56. package/dist/utils/compute-required-schemas.d.ts +4 -0
  57. package/dist/utils/compute-required-schemas.js +40 -0
  58. package/dist/utils/contract-hashes.d.ts +52 -0
  59. package/dist/utils/contract-hashes.js +269 -0
  60. package/dist/utils/custom-types-generator.d.ts +9 -0
  61. package/dist/utils/custom-types-generator.js +71 -0
  62. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  63. package/dist/utils/detect-schema-vendor.js +76 -0
  64. package/dist/utils/does-type-extend-core-type.d.ts +1 -1
  65. package/dist/utils/ensure-function-metadata.d.ts +6 -3
  66. package/dist/utils/ensure-function-metadata.js +220 -6
  67. package/dist/utils/extract-function-name.d.ts +5 -16
  68. package/dist/utils/extract-function-name.js +86 -291
  69. package/dist/utils/extract-services.d.ts +2 -1
  70. package/dist/utils/extract-services.js +25 -1
  71. package/dist/utils/filter-inspector-state.d.ts +1 -1
  72. package/dist/utils/filter-inspector-state.js +107 -23
  73. package/dist/utils/filter-utils.d.ts +2 -2
  74. package/dist/utils/get-files-and-methods.d.ts +1 -1
  75. package/dist/utils/get-property-value.d.ts +6 -1
  76. package/dist/utils/get-property-value.js +28 -3
  77. package/dist/utils/hash.d.ts +2 -0
  78. package/dist/utils/hash.js +23 -0
  79. package/dist/utils/middleware.d.ts +9 -32
  80. package/dist/utils/middleware.js +80 -66
  81. package/dist/utils/permissions.d.ts +4 -4
  82. package/dist/utils/permissions.js +10 -10
  83. package/dist/utils/post-process.d.ts +11 -11
  84. package/dist/utils/post-process.js +247 -24
  85. package/dist/utils/resolve-addon-package.d.ts +16 -0
  86. package/dist/utils/resolve-addon-package.js +34 -0
  87. package/dist/utils/resolve-function-types.d.ts +6 -0
  88. package/dist/utils/resolve-function-types.js +29 -0
  89. package/dist/utils/resolve-identifier.d.ts +10 -0
  90. package/dist/utils/resolve-identifier.js +36 -0
  91. package/dist/utils/resolve-versions.d.ts +2 -0
  92. package/dist/utils/resolve-versions.js +78 -0
  93. package/dist/utils/schema-generator.d.ts +9 -0
  94. package/dist/utils/schema-generator.js +209 -0
  95. package/dist/utils/serialize-inspector-state.d.ts +70 -23
  96. package/dist/utils/serialize-inspector-state.js +98 -22
  97. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  98. package/dist/utils/serialize-mcp-json.js +99 -0
  99. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  100. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  101. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  102. package/dist/utils/serialize-openapi-json.js +151 -0
  103. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  104. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  105. package/dist/utils/validate-auth-sessionless.d.ts +3 -0
  106. package/dist/utils/validate-auth-sessionless.js +14 -0
  107. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +34 -102
  108. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +1 -1
  109. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +23 -4
  110. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +12 -10
  111. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  112. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  113. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  114. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  115. package/dist/utils/workflow/graph/index.d.ts +2 -0
  116. package/dist/utils/workflow/graph/index.js +2 -0
  117. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +0 -8
  118. package/dist/utils/workflow/graph/serialize-workflow-graph.js +1 -3
  119. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +53 -79
  120. package/dist/utils/workflow/graph/workflow-graph.types.js +1 -1
  121. package/dist/visit.d.ts +1 -1
  122. package/dist/visit.js +13 -6
  123. package/package.json +14 -4
  124. package/src/add/add-ai-agent.ts +468 -0
  125. package/src/add/add-channel.ts +103 -79
  126. package/src/add/add-cli.ts +68 -24
  127. package/src/add/add-file-extends-core-type.ts +1 -1
  128. package/src/add/add-file-with-config.ts +1 -1
  129. package/src/add/add-file-with-factory.ts +3 -1
  130. package/src/add/add-functions.ts +349 -103
  131. package/src/add/add-http-route.ts +263 -89
  132. package/src/add/add-http-routes.ts +229 -0
  133. package/src/add/add-keyed-wiring.ts +151 -0
  134. package/src/add/add-mcp-prompt.ts +27 -16
  135. package/src/add/add-mcp-resource.ts +28 -16
  136. package/src/add/add-middleware.ts +482 -80
  137. package/src/add/add-permission.ts +199 -40
  138. package/src/add/add-queue-worker.ts +25 -20
  139. package/src/add/add-rpc-invocations.ts +28 -11
  140. package/src/add/add-schedule.ts +17 -12
  141. package/src/add/add-secret.ts +140 -0
  142. package/src/add/add-trigger.ts +154 -0
  143. package/src/add/add-variable.ts +9 -0
  144. package/src/add/add-wire-addon.ts +80 -0
  145. package/src/add/add-workflow-graph.ts +180 -522
  146. package/src/add/add-workflow.ts +7 -6
  147. package/src/error-codes.ts +25 -1
  148. package/src/index.ts +23 -13
  149. package/src/inspector.ts +139 -19
  150. package/src/schema-generator.ts +1 -0
  151. package/src/types-map.ts +12 -1
  152. package/src/types.ts +199 -69
  153. package/src/utils/compute-required-schemas.ts +48 -0
  154. package/src/utils/contract-hashes.test.ts +553 -0
  155. package/src/utils/contract-hashes.ts +386 -0
  156. package/src/utils/custom-types-generator.ts +88 -0
  157. package/src/utils/detect-schema-vendor.ts +90 -0
  158. package/src/utils/does-type-extend-core-type.ts +1 -1
  159. package/src/utils/ensure-function-metadata.ts +325 -8
  160. package/src/utils/extract-function-name.ts +101 -351
  161. package/src/utils/extract-services.ts +35 -2
  162. package/src/utils/filter-inspector-state.test.ts +37 -25
  163. package/src/utils/filter-inspector-state.ts +146 -32
  164. package/src/utils/filter-utils.test.ts +1 -1
  165. package/src/utils/filter-utils.ts +2 -2
  166. package/src/utils/get-files-and-methods.ts +1 -1
  167. package/src/utils/get-property-value.ts +42 -4
  168. package/src/utils/hash.ts +26 -0
  169. package/src/utils/middleware.test.ts +204 -0
  170. package/src/utils/middleware.ts +131 -69
  171. package/src/utils/permissions.test.ts +35 -12
  172. package/src/utils/permissions.ts +12 -12
  173. package/src/utils/post-process.ts +306 -44
  174. package/src/utils/resolve-addon-package.ts +49 -0
  175. package/src/utils/resolve-function-types.ts +42 -0
  176. package/src/utils/resolve-identifier.ts +46 -0
  177. package/src/utils/resolve-versions.test.ts +249 -0
  178. package/src/utils/resolve-versions.ts +105 -0
  179. package/src/utils/schema-generator.ts +329 -0
  180. package/src/utils/serialize-inspector-state.ts +184 -43
  181. package/src/utils/serialize-mcp-json.ts +145 -0
  182. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  183. package/src/utils/serialize-openapi-json.ts +277 -0
  184. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  185. package/src/utils/test-data/inspector-state.json +69 -66
  186. package/src/utils/validate-auth-sessionless.ts +29 -0
  187. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +43 -119
  188. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +26 -6
  189. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +17 -10
  190. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  191. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  192. package/src/utils/workflow/graph/index.ts +5 -0
  193. package/src/utils/workflow/graph/serialize-workflow-graph.ts +1 -8
  194. package/src/utils/workflow/graph/workflow-graph.types.ts +29 -78
  195. package/src/visit.ts +19 -7
  196. package/tsconfig.tsbuildinfo +1 -1
  197. package/dist/add/add-forge-credential.d.ts +0 -8
  198. package/dist/add/add-forge-credential.js +0 -77
  199. package/dist/add/add-forge-node.d.ts +0 -7
  200. package/dist/add/add-forge-node.js +0 -77
  201. package/dist/add/add-mcp-tool.d.ts +0 -2
  202. package/dist/add/add-mcp-tool.js +0 -81
  203. package/dist/utils/extract-service-metadata.d.ts +0 -19
  204. package/dist/utils/extract-service-metadata.js +0 -244
  205. package/dist/utils/write-service-metadata.d.ts +0 -13
  206. package/dist/utils/write-service-metadata.js +0 -37
  207. package/src/add/add-forge-credential.ts +0 -119
  208. package/src/add/add-forge-node.ts +0 -132
  209. package/src/add/add-mcp-tool.ts +0 -141
  210. package/src/utils/extract-service-metadata.ts +0 -353
  211. package/src/utils/write-service-metadata.ts +0 -51
@@ -1,16 +1,24 @@
1
1
  import * as ts from 'typescript'
2
- import { AddWiring } from '../types.js'
3
- import { TypesMap } from '../types-map.js'
4
- import { extractFunctionName } from '../utils/extract-function-name.js'
2
+ import type { AddWiring, SchemaRef } from '../types.js'
3
+ import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
4
+ import type { TypesMap } from '../types-map.js'
5
+ import {
6
+ extractFunctionName,
7
+ funcIdToTypeName,
8
+ } from '../utils/extract-function-name.js'
5
9
  import { extractFunctionNode } from '../utils/extract-function-node.js'
6
- import { FunctionServicesMeta } from '@pikku/core'
10
+ import { extractUsedWires } from '../utils/extract-services.js'
11
+ import type { FunctionServicesMeta } from '@pikku/core'
12
+ import { formatVersionedId } from '@pikku/core'
7
13
  import {
8
14
  getPropertyValue,
9
15
  getCommonWireMetaData,
10
16
  } from '../utils/get-property-value.js'
11
17
  import { resolveMiddleware } from '../utils/middleware.js'
12
18
  import { resolvePermissions } from '../utils/permissions.js'
19
+ import { extractWireNames } from '../utils/post-process.js'
13
20
  import { ErrorCode } from '../error-codes.js'
21
+ import type { NodeType } from '@pikku/core/node'
14
22
 
15
23
  const isValidVariableName = (name: string) => {
16
24
  const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
@@ -22,7 +30,8 @@ const nullifyTypes = (type: string | null) => {
22
30
  type === 'void' ||
23
31
  type === 'undefined' ||
24
32
  type === 'unknown' ||
25
- type === 'any'
33
+ type === 'any' ||
34
+ type === 'null'
26
35
  ) {
27
36
  return null
28
37
  }
@@ -186,8 +195,8 @@ const getNamesAndTypes = (
186
195
  return { names: [], types: [] }
187
196
  }
188
197
 
189
- // 1) Handle an explicit void (or undefined) type up front
190
- if (type.flags & ts.TypeFlags.VoidLike) {
198
+ // 1) Handle an explicit void (or undefined or null) type up front
199
+ if (type.flags & (ts.TypeFlags.VoidLike | ts.TypeFlags.Null)) {
191
200
  return {
192
201
  names: [],
193
202
  types: [],
@@ -205,13 +214,12 @@ const getNamesAndTypes = (
205
214
  const firstName = rawNames[0]
206
215
  if (rawNames.length > 1 || (firstName && !isValidVariableName(firstName))) {
207
216
  const aliasType = rawNames.join(' | ')
208
- const aliasName =
209
- funcName.charAt(0).toUpperCase() + funcName.slice(1) + direction
217
+ const aliasName = funcIdToTypeName(funcName) + direction
210
218
 
211
219
  // record the alias in your TypesMap
212
- const references = rawTypes
213
- .map((t) => resolveTypeImports(t, typesMap, true, checker))
214
- .flat()
220
+ const references = rawTypes.flatMap((t) =>
221
+ resolveTypeImports(t, typesMap, true, checker)
222
+ )
215
223
 
216
224
  typesMap.addCustomType(aliasName, aliasType, references)
217
225
 
@@ -222,19 +230,17 @@ const getNamesAndTypes = (
222
230
  }
223
231
 
224
232
  // 4) Single, valid name → inline it
225
- const uniqueNames = rawNames
226
- .map((name, i) => {
227
- const t = rawTypes[i]
228
- if (!t) {
229
- throw new Error(`Expected type for name "${name}" in ${funcName}`)
230
- }
231
- if (isPrimitiveType(t)) {
232
- return name
233
- }
234
- // non-primitive: import/alias it inline
235
- return resolveTypeImports(t, typesMap, false, checker)
236
- })
237
- .flat()
233
+ const uniqueNames = rawNames.flatMap((name, i) => {
234
+ const t = rawTypes[i]
235
+ if (!t) {
236
+ throw new Error(`Expected type for name "${name}" in ${funcName}`)
237
+ }
238
+ if (isPrimitiveType(t)) {
239
+ return name
240
+ }
241
+ // non-primitive: import/alias it inline
242
+ return resolveTypeImports(t, typesMap, false, checker)
243
+ })
238
244
 
239
245
  return {
240
246
  names: uniqueNames,
@@ -252,7 +258,6 @@ const isPrimitiveType = (type: ts.Type): boolean => {
252
258
  ts.TypeFlags.Void |
253
259
  ts.TypeFlags.Undefined |
254
260
  ts.TypeFlags.Null |
255
- ts.TypeFlags.Any |
256
261
  ts.TypeFlags.Unknown |
257
262
  ts.TypeFlags.VoidLike
258
263
 
@@ -286,7 +291,13 @@ function unwrapPromise(checker: ts.TypeChecker, type: ts.Type): ts.Type {
286
291
  * Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
287
292
  * then push into state.functions.meta.
288
293
  */
289
- export const addFunctions: AddWiring = (logger, node, checker, state) => {
294
+ export const addFunctions: AddWiring = (
295
+ logger,
296
+ node,
297
+ checker,
298
+ state,
299
+ options
300
+ ) => {
290
301
  if (!ts.isCallExpression(node)) return
291
302
 
292
303
  const { expression, arguments: args, typeArguments } = node
@@ -309,8 +320,15 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
309
320
 
310
321
  if (args.length === 0) return
311
322
 
312
- const { pikkuFuncName, name, explicitName, exportedName } =
313
- extractFunctionName(node, checker, state.rootDir)
323
+ let { pikkuFuncId, name, explicitName, exportedName } = extractFunctionName(
324
+ node,
325
+ checker,
326
+ state.rootDir
327
+ )
328
+
329
+ if (!pikkuFuncId || pikkuFuncId.startsWith('__temp_')) {
330
+ return
331
+ }
314
332
 
315
333
  let title: string | undefined
316
334
  let tags: string[] | undefined
@@ -318,8 +336,16 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
318
336
  let description: string | undefined
319
337
  let errors: string[] | undefined
320
338
  let expose: boolean | undefined
321
- let internal: boolean | undefined
339
+ let remote: boolean | undefined
340
+ let mcp: boolean | undefined
341
+ let readonly_: boolean | undefined
342
+ let requiresApproval: boolean | undefined
343
+ let version: number | undefined
322
344
  let objectNode: ts.ObjectLiteralExpression | undefined
345
+ let nodeDisplayName: string | null = null
346
+ let nodeCategory: string | null = null
347
+ let nodeType: NodeType | null = null
348
+ let nodeErrorOutput: boolean | null = null
323
349
 
324
350
  // Extract the function node using shared utility
325
351
  const firstArg = args[0]!
@@ -329,22 +355,24 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
329
355
  isDirectFunction,
330
356
  } = extractFunctionNode(firstArg, checker)
331
357
 
332
- // Variables to hold zod schema references if provided
333
- let inputZodSchemaRef: { variableName: string; sourceFile: string } | null =
334
- null
335
- let outputZodSchemaRef: { variableName: string; sourceFile: string } | null =
336
- null
358
+ // Variables to hold schema references if provided
359
+ let inputSchemaRef: SchemaRef | null = null
360
+ let outputSchemaRef: SchemaRef | null = null
337
361
 
338
- // Helper to resolve schema identifier to its actual source file
339
- const resolveSchemaSourceFile = (
340
- identifier: ts.Identifier
341
- ): { variableName: string; sourceFile: string } | null => {
362
+ // Helper to resolve schema identifier to its actual source file and detect vendor.
363
+ // Logs a fatal error and returns null if vendor cannot be determined.
364
+ const resolveSchemaRef = (
365
+ identifier: ts.Identifier,
366
+ context: string
367
+ ): SchemaRef | null => {
342
368
  const symbol = checker.getSymbolAtLocation(identifier)
343
369
  if (!symbol) return null
344
370
 
345
371
  const decl = symbol.valueDeclaration || symbol.declarations?.[0]
346
372
  if (!decl) return null
347
373
 
374
+ let sourceFile: string
375
+
348
376
  // If it's an import specifier, resolve the aliased symbol to get the actual source
349
377
  if (ts.isImportSpecifier(decl)) {
350
378
  const aliasedSymbol = checker.getAliasedSymbol(symbol)
@@ -352,18 +380,30 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
352
380
  const aliasedDecl =
353
381
  aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]
354
382
  if (aliasedDecl) {
355
- return {
356
- variableName: identifier.text,
357
- sourceFile: aliasedDecl.getSourceFile().fileName,
358
- }
383
+ sourceFile = aliasedDecl.getSourceFile().fileName
384
+ } else {
385
+ return null
359
386
  }
387
+ } else {
388
+ return null
360
389
  }
390
+ } else {
391
+ sourceFile = decl.getSourceFile().fileName
361
392
  }
362
393
 
363
- // Not an import - use the current source file
394
+ const vendor = detectSchemaVendorOrError(
395
+ identifier,
396
+ checker,
397
+ logger,
398
+ context,
399
+ sourceFile
400
+ )
401
+ if (!vendor) return null
402
+
364
403
  return {
365
404
  variableName: identifier.text,
366
- sourceFile: decl.getSourceFile().fileName,
405
+ sourceFile,
406
+ vendor,
367
407
  }
368
408
  }
369
409
 
@@ -371,35 +411,95 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
371
411
  if (ts.isObjectLiteralExpression(firstArg)) {
372
412
  objectNode = firstArg
373
413
  const metadata = getCommonWireMetaData(firstArg, 'Function', name, logger)
414
+ if (metadata.disabled) return
374
415
  title = metadata.title
375
416
  tags = metadata.tags
376
417
  summary = metadata.summary
377
418
  description = metadata.description
378
419
  errors = metadata.errors
379
420
  expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
380
- internal = getPropertyValue(firstArg, 'internal') as boolean | undefined
421
+ remote = getPropertyValue(firstArg, 'remote') as boolean | undefined
422
+ mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
423
+ readonly_ = getPropertyValue(firstArg, 'readonly') as boolean | undefined
424
+ requiresApproval = getPropertyValue(firstArg, 'requiresApproval') as
425
+ | boolean
426
+ | undefined
427
+
428
+ const versionRaw = getPropertyValue(firstArg, 'version')
429
+ if (versionRaw !== null && versionRaw !== undefined) {
430
+ const parsed = Number(versionRaw)
431
+ if (Number.isInteger(parsed) && parsed >= 1) {
432
+ version = parsed
433
+ }
434
+ }
381
435
 
382
- // Extract zod schema variable names from input/output properties
436
+ // Extract node config from nested object
437
+ for (const prop of firstArg.properties) {
438
+ if (
439
+ ts.isPropertyAssignment(prop) &&
440
+ ts.isIdentifier(prop.name) &&
441
+ prop.name.text === 'node' &&
442
+ ts.isObjectLiteralExpression(prop.initializer)
443
+ ) {
444
+ const nodeObj = prop.initializer
445
+ nodeDisplayName = getPropertyValue(nodeObj, 'displayName') as
446
+ | string
447
+ | null
448
+ nodeCategory = getPropertyValue(nodeObj, 'category') as string | null
449
+ nodeType = getPropertyValue(nodeObj, 'type') as NodeType | null
450
+ nodeErrorOutput = getPropertyValue(nodeObj, 'errorOutput') as
451
+ | boolean
452
+ | null
453
+
454
+ if (!nodeDisplayName) {
455
+ logger.critical(
456
+ ErrorCode.MISSING_NAME,
457
+ `Function '${name}' node config is missing the required 'displayName' property.`
458
+ )
459
+ }
460
+ if (!nodeCategory) {
461
+ logger.critical(
462
+ ErrorCode.MISSING_NAME,
463
+ `Function '${name}' node config is missing the required 'category' property.`
464
+ )
465
+ }
466
+ if (!nodeType) {
467
+ logger.critical(
468
+ ErrorCode.MISSING_NAME,
469
+ `Function '${name}' node config is missing the required 'type' property.`
470
+ )
471
+ } else if (!['trigger', 'action', 'end'].includes(nodeType)) {
472
+ logger.critical(
473
+ ErrorCode.INVALID_VALUE,
474
+ `Function '${name}' node config has invalid type '${nodeType}'. Must be 'trigger', 'action', or 'end'.`
475
+ )
476
+ }
477
+ break
478
+ }
479
+ }
480
+
481
+ // Extract schema variable names from input/output properties
383
482
  for (const prop of firstArg.properties) {
384
483
  if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
385
484
  const propName = prop.name.text
386
485
  if (propName === 'input' || propName === 'output') {
387
486
  if (ts.isIdentifier(prop.initializer)) {
388
- // Good - it's a variable reference, resolve its actual source file
389
- const ref = resolveSchemaSourceFile(prop.initializer)
487
+ // Good - it's a variable reference, resolve its actual source file and vendor
488
+ const context = `Function '${name}' ${propName}`
489
+ const ref = resolveSchemaRef(prop.initializer, context)
390
490
  if (ref) {
391
491
  if (propName === 'input') {
392
- inputZodSchemaRef = ref
492
+ inputSchemaRef = ref
393
493
  } else {
394
- outputZodSchemaRef = ref
494
+ outputSchemaRef = ref
395
495
  }
396
496
  }
397
497
  } else if (ts.isCallExpression(prop.initializer)) {
398
498
  // Bad - it's an inline expression
399
- const schemaName = `${name.charAt(0).toUpperCase() + name.slice(1)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`
499
+ const schemaName = `${funcIdToTypeName(name)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`
400
500
  logger.critical(
401
- ErrorCode.INLINE_ZOD_SCHEMA,
402
- `Inline Zod schemas are not supported for '${propName}' in '${name}'.\n` +
501
+ ErrorCode.INLINE_SCHEMA,
502
+ `Inline schemas are not supported for '${propName}' in '${name}'.\n` +
403
503
  ` Extract to an exported variable:\n` +
404
504
  ` export const ${schemaName} = ${prop.initializer.getText()}\n` +
405
505
  ` Then use: ${propName}: ${schemaName}`
@@ -410,6 +510,14 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
410
510
  }
411
511
  }
412
512
 
513
+ if (version !== undefined) {
514
+ const baseName = explicitName || exportedName || pikkuFuncId
515
+ pikkuFuncId = formatVersionedId(baseName, version)
516
+ }
517
+
518
+ const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc'
519
+ const mcpEnabled = mcp || isMCPToolFunc
520
+
413
521
  // Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
414
522
  const handler =
415
523
  resolvedFunc &&
@@ -419,10 +527,11 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
419
527
 
420
528
  // Validate that we got a valid function
421
529
  if (!ts.isArrowFunction(handler) && !ts.isFunctionExpression(handler)) {
422
- logger.error(`• No valid 'func' property found for ${pikkuFuncName}.`)
530
+ logger.error(`• No valid 'func' property found for ${pikkuFuncId}.`)
423
531
  // Create stub metadata to prevent "function not found" errors in wirings
424
- state.functions.meta[pikkuFuncName] = {
425
- pikkuFuncName,
532
+ state.functions.meta[pikkuFuncId] = {
533
+ pikkuFuncId,
534
+ functionType: 'user',
426
535
  name,
427
536
  services: { optimized: false, services: [] },
428
537
  inputSchemaName: null,
@@ -453,43 +562,31 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
453
562
  services.services.push(original)
454
563
  }
455
564
  }
456
- } else if (ts.isIdentifier(firstParam.name)) {
565
+ } else if (
566
+ ts.isIdentifier(firstParam.name) &&
567
+ !firstParam.name.text.startsWith('_')
568
+ ) {
457
569
  services.optimized = false
458
570
  }
459
571
  }
460
572
 
461
- // --- Extract used wires from third parameter ---
462
- const usedWires: string[] = []
463
- const thirdParam = handler.parameters[2]
464
- if (thirdParam && ts.isObjectBindingPattern(thirdParam.name)) {
465
- for (const elem of thirdParam.name.elements) {
466
- const propertyName =
467
- elem.propertyName && ts.isIdentifier(elem.propertyName)
468
- ? elem.propertyName.text
469
- : ts.isIdentifier(elem.name)
470
- ? elem.name.text
471
- : undefined
472
- if (propertyName) {
473
- usedWires.push(propertyName)
474
- }
475
- }
476
- }
573
+ const wires = extractUsedWires(handler, 2)
477
574
 
478
575
  // --- Generics → ts.Type[], unwrapped from Promise ---
479
576
  const genericTypes: ts.Type[] = (typeArguments ?? [])
480
577
  .map((tn) => checker.getTypeFromTypeNode(tn))
481
578
  .map((t) => unwrapPromise(checker, t))
482
579
 
483
- const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1)
580
+ const capitalizedName = funcIdToTypeName(name)
484
581
 
485
582
  // --- Input Extraction ---
486
583
  let inputNames: string[] = []
487
584
  let inputTypes: ts.Type[] = []
488
585
 
489
- if (inputZodSchemaRef) {
586
+ if (inputSchemaRef) {
490
587
  const schemaName = `${capitalizedName}Input`
491
588
  inputNames = [schemaName]
492
- state.zodLookup.set(schemaName, inputZodSchemaRef)
589
+ state.schemaLookup.set(schemaName, inputSchemaRef)
493
590
  state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
494
591
  } else if (genericTypes.length >= 1 && genericTypes[0]) {
495
592
  // Fall back to extracting from generic type arguments
@@ -511,7 +608,7 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
511
608
  checker,
512
609
  state.functions.typesMap,
513
610
  'Input',
514
- pikkuFuncName,
611
+ pikkuFuncId,
515
612
  paramType
516
613
  )
517
614
  inputNames = result.names
@@ -522,10 +619,10 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
522
619
  // --- Output Extraction ---
523
620
  let outputNames: string[] = []
524
621
 
525
- if (outputZodSchemaRef) {
622
+ if (outputSchemaRef) {
526
623
  const schemaName = `${capitalizedName}Output`
527
624
  outputNames = [schemaName]
528
- state.zodLookup.set(schemaName, outputZodSchemaRef)
625
+ state.schemaLookup.set(schemaName, outputSchemaRef)
529
626
  state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
530
627
  } else if (genericTypes.length >= 2) {
531
628
  outputNames = getNamesAndTypes(
@@ -544,12 +641,55 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
544
641
  checker,
545
642
  state.functions.typesMap,
546
643
  'Output',
547
- pikkuFuncName,
644
+ pikkuFuncId,
548
645
  unwrapped
549
646
  ).names
550
647
  }
551
648
  }
552
649
 
650
+ const mcpOutputTypes: Record<string, string> = {
651
+ pikkuMCPResourceFunc: 'MCPResourceResponse',
652
+ pikkuMCPToolFunc: 'MCPToolResponse',
653
+ pikkuMCPPromptFunc: 'MCPPromptResponse',
654
+ }
655
+ const mcpOutputType = mcpOutputTypes[expression.text]
656
+ if (mcpOutputType && outputNames[0] !== mcpOutputType) {
657
+ let resolved = false
658
+ const rawSymbol = checker.getSymbolAtLocation(expression)
659
+ const funcSymbol =
660
+ rawSymbol && rawSymbol.flags & ts.SymbolFlags.Alias
661
+ ? checker.getAliasedSymbol(rawSymbol)
662
+ : rawSymbol
663
+ const funcDecls = funcSymbol?.getDeclarations() || []
664
+ for (const funcDecl of funcDecls) {
665
+ if (resolved) break
666
+ const mcpTypeSymbol = checker.resolveName(
667
+ mcpOutputType,
668
+ funcDecl,
669
+ ts.SymbolFlags.Type,
670
+ false
671
+ )
672
+ if (mcpTypeSymbol) {
673
+ const aliased =
674
+ mcpTypeSymbol.flags & ts.SymbolFlags.Alias
675
+ ? checker.getAliasedSymbol(mcpTypeSymbol)
676
+ : mcpTypeSymbol
677
+ const typeDecl = aliased?.getDeclarations()?.[0]
678
+ if (typeDecl) {
679
+ const path = typeDecl.getSourceFile().fileName
680
+ if (!state.functions.typesMap.exists(mcpOutputType, path)) {
681
+ state.functions.typesMap.addType(mcpOutputType, path)
682
+ }
683
+ resolved = true
684
+ }
685
+ }
686
+ }
687
+ if (!resolved) {
688
+ state.functions.typesMap.addCustomType(mcpOutputType, mcpOutputType, [])
689
+ }
690
+ outputNames = [mcpOutputType]
691
+ }
692
+
553
693
  if (inputNames.length > 1) {
554
694
  logger.warn(
555
695
  'More than one input type detected, only the first one will be used as a schema.'
@@ -558,30 +698,69 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
558
698
 
559
699
  // Store the input type for later use
560
700
  if (inputTypes.length > 0) {
561
- state.typesLookup.set(pikkuFuncName, inputTypes)
701
+ state.typesLookup.set(pikkuFuncId, inputTypes)
562
702
  }
563
703
 
564
704
  // --- resolve middleware ---
565
- const middleware = objectNode
705
+ let middleware = objectNode
566
706
  ? resolveMiddleware(state, objectNode, tags, checker)
567
707
  : undefined
568
708
 
569
709
  // --- resolve permissions ---
570
- const permissions = objectNode
710
+ let permissions = objectNode
571
711
  ? resolvePermissions(state, objectNode, tags, checker)
572
712
  : undefined
573
713
 
574
- state.functions.meta[pikkuFuncName] = {
575
- pikkuFuncName,
714
+ if (options.tags?.length) {
715
+ tags = [...new Set([...(tags || []), ...options.tags])]
716
+ const tagEntries = options.tags.map((tag) => ({
717
+ type: 'tag' as const,
718
+ tag,
719
+ }))
720
+ const existingMiddlewareTags = new Set(
721
+ (middleware || [])
722
+ .filter((m) => m.type === 'tag')
723
+ .map((m) => (m as any).tag)
724
+ )
725
+ const newMiddleware = tagEntries.filter(
726
+ (e) => !existingMiddlewareTags.has(e.tag)
727
+ )
728
+ if (newMiddleware.length > 0) {
729
+ middleware = [...(middleware || []), ...newMiddleware]
730
+ }
731
+ const existingPermissionTags = new Set(
732
+ (permissions || [])
733
+ .filter((p) => p.type === 'tag')
734
+ .map((p) => (p as any).tag)
735
+ )
736
+ const newPermissions = tagEntries.filter(
737
+ (e) => !existingPermissionTags.has(e.tag)
738
+ )
739
+ if (newPermissions.length > 0) {
740
+ permissions = [...(permissions || []), ...newPermissions]
741
+ }
742
+ }
743
+
744
+ const sessionless = expression.text !== 'pikkuFunc'
745
+
746
+ state.functions.meta[pikkuFuncId] = {
747
+ pikkuFuncId,
748
+ functionType: 'user',
749
+ funcWrapper: expression.text,
750
+ sessionless,
576
751
  name,
577
752
  services,
578
- usedWires: usedWires.length > 0 ? usedWires : undefined,
753
+ wires: wires.wires.length > 0 || !wires.optimized ? wires : undefined,
579
754
  inputSchemaName: inputNames[0] ?? null,
580
755
  outputSchemaName: outputNames[0] ?? null,
581
756
  inputs: inputNames.filter((n) => n !== 'void') ?? null,
582
757
  outputs: outputNames.filter((n) => n !== 'void') ?? null,
583
758
  expose: expose || undefined,
584
- internal: internal || undefined,
759
+ remote: remote || undefined,
760
+ mcp: mcpEnabled || undefined,
761
+ readonly: readonly_ || undefined,
762
+ requiresApproval: requiresApproval || undefined,
763
+ version,
585
764
  title,
586
765
  tags: tags || undefined,
587
766
  summary,
@@ -592,20 +771,78 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
592
771
  isDirectFunction,
593
772
  }
594
773
 
595
- // Store function file location for wiring generation
596
- if (exportedName) {
597
- state.functions.files.set(pikkuFuncName, {
598
- path: node.getSourceFile().fileName,
599
- exportedName,
600
- })
774
+ // Populate node metadata if node config is present
775
+ if (nodeDisplayName && nodeCategory && nodeType) {
776
+ state.nodes.files.add(node.getSourceFile().fileName)
777
+ state.nodes.meta[pikkuFuncId] = {
778
+ name: pikkuFuncId,
779
+ displayName: nodeDisplayName,
780
+ category: nodeCategory,
781
+ type: nodeType,
782
+ rpc: pikkuFuncId,
783
+ description,
784
+ errorOutput: nodeErrorOutput ?? false,
785
+ inputSchemaName: inputNames[0] ?? null,
786
+ outputSchemaName: outputNames[0] ?? null,
787
+ tags,
788
+ }
789
+ }
790
+
791
+ if (mcpEnabled) {
792
+ if (!description) {
793
+ logger.critical(
794
+ ErrorCode.MISSING_DESCRIPTION,
795
+ `MCP tool '${name}' is missing a description.`
796
+ )
797
+ return
798
+ }
799
+ state.mcpEndpoints.files.add(node.getSourceFile().fileName)
800
+ state.mcpEndpoints.toolsMeta[name] = {
801
+ pikkuFuncId,
802
+ name,
803
+ title: title || undefined,
804
+ description,
805
+ summary,
806
+ errors,
807
+ tags,
808
+ inputSchema: inputNames[0] ?? null,
809
+ outputSchema: outputNames[0] ?? null,
810
+ middleware,
811
+ permissions,
812
+ }
813
+ state.serviceAggregation.usedFunctions.add(pikkuFuncId)
814
+ extractWireNames(middleware).forEach((n) =>
815
+ state.serviceAggregation.usedMiddleware.add(n)
816
+ )
817
+ extractWireNames(permissions).forEach((n) =>
818
+ state.serviceAggregation.usedPermissions.add(n)
819
+ )
601
820
  }
602
821
 
603
822
  // Workflow functions don't get registered as RPC functions,
604
- // they are their own type handled by add-workdflow
823
+ // they are their own type handled by add-workflow
605
824
  if (expression.text.includes('Workflow')) {
606
825
  return
607
826
  }
608
827
 
828
+ // Trigger and channel connect/disconnect functions are not callable via RPC
829
+ const nonRPCPatterns = [
830
+ /Trigger/i,
831
+ /ChannelConnection/i,
832
+ /ChannelDisconnection/i,
833
+ ]
834
+ if (nonRPCPatterns.some((pattern) => pattern.test(expression.text))) {
835
+ return
836
+ }
837
+
838
+ // Store function file location for wiring generation
839
+ if (exportedName) {
840
+ state.functions.files.set(pikkuFuncId, {
841
+ path: node.getSourceFile().fileName,
842
+ exportedName,
843
+ })
844
+ }
845
+
609
846
  if (exportedName || explicitName) {
610
847
  if (!exportedName) {
611
848
  logger.error(
@@ -614,28 +851,37 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
614
851
  return
615
852
  }
616
853
 
617
- // Mark internal functions as invoked to force bundling
618
- if (internal) {
619
- state.rpc.invokedFunctions.add(pikkuFuncName)
854
+ if (remote) {
855
+ state.rpc.invokedFunctions.add(pikkuFuncId)
620
856
  }
621
857
 
622
858
  if (expose) {
623
- state.rpc.exposedMeta[name] = pikkuFuncName
859
+ state.rpc.exposedMeta[name] = pikkuFuncId
624
860
  state.rpc.exposedFiles.set(name, {
625
861
  path: node.getSourceFile().fileName,
626
862
  exportedName,
627
863
  })
628
864
  // Track exposed RPC function for service aggregation
629
- state.serviceAggregation.usedFunctions.add(pikkuFuncName)
865
+ state.serviceAggregation.usedFunctions.add(pikkuFuncId)
630
866
  }
631
867
 
632
868
  // We add it to internal meta to allow autocomplete for everything
633
- state.rpc.internalMeta[name] = pikkuFuncName
869
+ state.rpc.internalMeta[name] = pikkuFuncId
870
+
871
+ if (version !== undefined) {
872
+ state.rpc.internalMeta[pikkuFuncId] = pikkuFuncId
873
+ state.rpc.invokedFunctions.add(pikkuFuncId)
874
+ }
634
875
 
635
876
  // But we only import the actual function if it's actually invoked to keep
636
877
  // bundle size down
637
- if (state.rpc.invokedFunctions.has(pikkuFuncName) || expose || internal) {
638
- state.rpc.internalFiles.set(pikkuFuncName, {
878
+ if (
879
+ state.rpc.invokedFunctions.has(pikkuFuncId) ||
880
+ expose ||
881
+ remote ||
882
+ mcpEnabled
883
+ ) {
884
+ state.rpc.internalFiles.set(pikkuFuncId, {
639
885
  path: node.getSourceFile().fileName,
640
886
  exportedName,
641
887
  })