@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,14 +1,23 @@
1
1
  import * as ts from 'typescript'
2
- import { AddWiring } from '../types.js'
2
+ import { AddWiring, SchemaRef } from '../types.js'
3
+ import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
3
4
  import { TypesMap } from '../types-map.js'
4
- import { extractFunctionName } from '../utils/extract-function-name.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 { FunctionServicesMeta, formatVersionedId } from '@pikku/core'
7
12
  import {
8
13
  getPropertyValue,
9
14
  getCommonWireMetaData,
10
15
  } from '../utils/get-property-value.js'
11
16
  import { resolveMiddleware } from '../utils/middleware.js'
17
+ import { resolvePermissions } from '../utils/permissions.js'
18
+ import { extractWireNames } from '../utils/post-process.js'
19
+ import { ErrorCode } from '../error-codes.js'
20
+ import type { NodeType } from '@pikku/core/node'
12
21
 
13
22
  const isValidVariableName = (name: string) => {
14
23
  const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
@@ -20,7 +29,8 @@ const nullifyTypes = (type: string | null) => {
20
29
  type === 'void' ||
21
30
  type === 'undefined' ||
22
31
  type === 'unknown' ||
23
- type === 'any'
32
+ type === 'any' ||
33
+ type === 'null'
24
34
  ) {
25
35
  return null
26
36
  }
@@ -152,14 +162,18 @@ const resolveUnionTypes = (
152
162
  // Check if it's a union type AND not part of an intersection
153
163
  if (type.isUnion() && !(type.flags & ts.TypeFlags.Intersection)) {
154
164
  for (const t of type.types) {
155
- const name = nullifyTypes(checker.typeToString(t))
165
+ const name = nullifyTypes(
166
+ checker.typeToString(t, undefined, ts.TypeFormatFlags.NoTruncation)
167
+ )
156
168
  if (name) {
157
169
  types.push(t)
158
170
  names.push(name)
159
171
  }
160
172
  }
161
173
  } else {
162
- const name = nullifyTypes(checker.typeToString(type))
174
+ const name = nullifyTypes(
175
+ checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation)
176
+ )
163
177
  if (name) {
164
178
  types.push(type)
165
179
  names.push(name)
@@ -180,8 +194,8 @@ const getNamesAndTypes = (
180
194
  return { names: [], types: [] }
181
195
  }
182
196
 
183
- // 1) Handle an explicit void (or undefined) type up front
184
- if (type.flags & ts.TypeFlags.VoidLike) {
197
+ // 1) Handle an explicit void (or undefined or null) type up front
198
+ if (type.flags & (ts.TypeFlags.VoidLike | ts.TypeFlags.Null)) {
185
199
  return {
186
200
  names: [],
187
201
  types: [],
@@ -199,8 +213,7 @@ const getNamesAndTypes = (
199
213
  const firstName = rawNames[0]
200
214
  if (rawNames.length > 1 || (firstName && !isValidVariableName(firstName))) {
201
215
  const aliasType = rawNames.join(' | ')
202
- const aliasName =
203
- funcName.charAt(0).toUpperCase() + funcName.slice(1) + direction
216
+ const aliasName = funcIdToTypeName(funcName) + direction
204
217
 
205
218
  // record the alias in your TypesMap
206
219
  const references = rawTypes
@@ -246,7 +259,6 @@ const isPrimitiveType = (type: ts.Type): boolean => {
246
259
  ts.TypeFlags.Void |
247
260
  ts.TypeFlags.Undefined |
248
261
  ts.TypeFlags.Null |
249
- ts.TypeFlags.Any |
250
262
  ts.TypeFlags.Unknown |
251
263
  ts.TypeFlags.VoidLike
252
264
 
@@ -280,7 +292,13 @@ function unwrapPromise(checker: ts.TypeChecker, type: ts.Type): ts.Type {
280
292
  * Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
281
293
  * then push into state.functions.meta.
282
294
  */
283
- export const addFunctions: AddWiring = (logger, node, checker, state) => {
295
+ export const addFunctions: AddWiring = (
296
+ logger,
297
+ node,
298
+ checker,
299
+ state,
300
+ options
301
+ ) => {
284
302
  if (!ts.isCallExpression(node)) return
285
303
 
286
304
  const { expression, arguments: args, typeArguments } = node
@@ -303,16 +321,31 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
303
321
 
304
322
  if (args.length === 0) return
305
323
 
306
- const { pikkuFuncName, name, explicitName, exportedName } =
307
- extractFunctionName(node, checker, state.rootDir)
324
+ let { pikkuFuncId, name, explicitName, exportedName } = extractFunctionName(
325
+ node,
326
+ checker,
327
+ state.rootDir
328
+ )
329
+
330
+ if (!pikkuFuncId || pikkuFuncId.startsWith('__temp_')) {
331
+ return
332
+ }
308
333
 
334
+ let title: string | undefined
309
335
  let tags: string[] | undefined
310
336
  let summary: string | undefined
311
337
  let description: string | undefined
312
338
  let errors: string[] | undefined
313
339
  let expose: boolean | undefined
314
- let internal: boolean | undefined
340
+ let remote: boolean | undefined
341
+ let mcp: boolean | undefined
342
+ let requiresApproval: boolean | undefined
343
+ let version: number | undefined
315
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
316
349
 
317
350
  // Extract the function node using shared utility
318
351
  const firstArg = args[0]!
@@ -322,18 +355,168 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
322
355
  isDirectFunction,
323
356
  } = extractFunctionNode(firstArg, checker)
324
357
 
358
+ // Variables to hold schema references if provided
359
+ let inputSchemaRef: SchemaRef | null = null
360
+ let outputSchemaRef: SchemaRef | null = null
361
+
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 => {
368
+ const symbol = checker.getSymbolAtLocation(identifier)
369
+ if (!symbol) return null
370
+
371
+ const decl = symbol.valueDeclaration || symbol.declarations?.[0]
372
+ if (!decl) return null
373
+
374
+ let sourceFile: string
375
+
376
+ // If it's an import specifier, resolve the aliased symbol to get the actual source
377
+ if (ts.isImportSpecifier(decl)) {
378
+ const aliasedSymbol = checker.getAliasedSymbol(symbol)
379
+ if (aliasedSymbol) {
380
+ const aliasedDecl =
381
+ aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]
382
+ if (aliasedDecl) {
383
+ sourceFile = aliasedDecl.getSourceFile().fileName
384
+ } else {
385
+ return null
386
+ }
387
+ } else {
388
+ return null
389
+ }
390
+ } else {
391
+ sourceFile = decl.getSourceFile().fileName
392
+ }
393
+
394
+ const vendor = detectSchemaVendorOrError(
395
+ identifier,
396
+ checker,
397
+ logger,
398
+ context,
399
+ sourceFile
400
+ )
401
+ if (!vendor) return null
402
+
403
+ return {
404
+ variableName: identifier.text,
405
+ sourceFile,
406
+ vendor,
407
+ }
408
+ }
409
+
325
410
  // Extract config properties if using object form
326
411
  if (ts.isObjectLiteralExpression(firstArg)) {
327
412
  objectNode = firstArg
328
413
  const metadata = getCommonWireMetaData(firstArg, 'Function', name, logger)
414
+ if (metadata.disabled) return
415
+ title = metadata.title
329
416
  tags = metadata.tags
330
417
  summary = metadata.summary
331
418
  description = metadata.description
332
419
  errors = metadata.errors
333
420
  expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
334
- internal = getPropertyValue(firstArg, 'internal') as boolean | undefined
421
+ remote = getPropertyValue(firstArg, 'remote') as boolean | undefined
422
+ mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
423
+ requiresApproval = getPropertyValue(firstArg, 'requiresApproval') as
424
+ | boolean
425
+ | undefined
426
+
427
+ const versionRaw = getPropertyValue(firstArg, 'version')
428
+ if (versionRaw !== null && versionRaw !== undefined) {
429
+ const parsed = Number(versionRaw)
430
+ if (Number.isInteger(parsed) && parsed >= 1) {
431
+ version = parsed
432
+ }
433
+ }
434
+
435
+ // Extract node config from nested object
436
+ for (const prop of firstArg.properties) {
437
+ if (
438
+ ts.isPropertyAssignment(prop) &&
439
+ ts.isIdentifier(prop.name) &&
440
+ prop.name.text === 'node' &&
441
+ ts.isObjectLiteralExpression(prop.initializer)
442
+ ) {
443
+ const nodeObj = prop.initializer
444
+ nodeDisplayName = getPropertyValue(nodeObj, 'displayName') as
445
+ | string
446
+ | null
447
+ nodeCategory = getPropertyValue(nodeObj, 'category') as string | null
448
+ nodeType = getPropertyValue(nodeObj, 'type') as NodeType | null
449
+ nodeErrorOutput = getPropertyValue(nodeObj, 'errorOutput') as
450
+ | boolean
451
+ | null
452
+
453
+ if (!nodeDisplayName) {
454
+ logger.critical(
455
+ ErrorCode.MISSING_NAME,
456
+ `Function '${name}' node config is missing the required 'displayName' property.`
457
+ )
458
+ }
459
+ if (!nodeCategory) {
460
+ logger.critical(
461
+ ErrorCode.MISSING_NAME,
462
+ `Function '${name}' node config is missing the required 'category' property.`
463
+ )
464
+ }
465
+ if (!nodeType) {
466
+ logger.critical(
467
+ ErrorCode.MISSING_NAME,
468
+ `Function '${name}' node config is missing the required 'type' property.`
469
+ )
470
+ } else if (!['trigger', 'action', 'end'].includes(nodeType)) {
471
+ logger.critical(
472
+ ErrorCode.INVALID_VALUE,
473
+ `Function '${name}' node config has invalid type '${nodeType}'. Must be 'trigger', 'action', or 'end'.`
474
+ )
475
+ }
476
+ break
477
+ }
478
+ }
479
+
480
+ // Extract schema variable names from input/output properties
481
+ for (const prop of firstArg.properties) {
482
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
483
+ const propName = prop.name.text
484
+ if (propName === 'input' || propName === 'output') {
485
+ if (ts.isIdentifier(prop.initializer)) {
486
+ // Good - it's a variable reference, resolve its actual source file and vendor
487
+ const context = `Function '${name}' ${propName}`
488
+ const ref = resolveSchemaRef(prop.initializer, context)
489
+ if (ref) {
490
+ if (propName === 'input') {
491
+ inputSchemaRef = ref
492
+ } else {
493
+ outputSchemaRef = ref
494
+ }
495
+ }
496
+ } else if (ts.isCallExpression(prop.initializer)) {
497
+ // Bad - it's an inline expression
498
+ const schemaName = `${funcIdToTypeName(name)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`
499
+ logger.critical(
500
+ ErrorCode.INLINE_SCHEMA,
501
+ `Inline schemas are not supported for '${propName}' in '${name}'.\n` +
502
+ ` Extract to an exported variable:\n` +
503
+ ` export const ${schemaName} = ${prop.initializer.getText()}\n` +
504
+ ` Then use: ${propName}: ${schemaName}`
505
+ )
506
+ }
507
+ }
508
+ }
509
+ }
335
510
  }
336
511
 
512
+ if (version !== undefined) {
513
+ const baseName = explicitName || exportedName || pikkuFuncId
514
+ pikkuFuncId = formatVersionedId(baseName, version)
515
+ }
516
+
517
+ const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc'
518
+ const mcpEnabled = mcp || isMCPToolFunc
519
+
337
520
  // Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
338
521
  const handler =
339
522
  resolvedFunc &&
@@ -343,10 +526,11 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
343
526
 
344
527
  // Validate that we got a valid function
345
528
  if (!ts.isArrowFunction(handler) && !ts.isFunctionExpression(handler)) {
346
- logger.error(`• No valid 'func' property found for ${pikkuFuncName}.`)
529
+ logger.error(`• No valid 'func' property found for ${pikkuFuncId}.`)
347
530
  // Create stub metadata to prevent "function not found" errors in wirings
348
- state.functions.meta[pikkuFuncName] = {
349
- pikkuFuncName,
531
+ state.functions.meta[pikkuFuncId] = {
532
+ pikkuFuncId,
533
+ functionType: 'user',
350
534
  name,
351
535
  services: { optimized: false, services: [] },
352
536
  inputSchemaName: null,
@@ -377,50 +561,69 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
377
561
  services.services.push(original)
378
562
  }
379
563
  }
380
- } else if (ts.isIdentifier(firstParam.name)) {
564
+ } else if (
565
+ ts.isIdentifier(firstParam.name) &&
566
+ !firstParam.name.text.startsWith('_')
567
+ ) {
381
568
  services.optimized = false
382
569
  }
383
570
  }
384
571
 
385
- // --- Extract used wires from third parameter ---
386
- const usedWires: string[] = []
387
- const thirdParam = handler.parameters[2]
388
- if (thirdParam && ts.isObjectBindingPattern(thirdParam.name)) {
389
- for (const elem of thirdParam.name.elements) {
390
- const propertyName =
391
- elem.propertyName && ts.isIdentifier(elem.propertyName)
392
- ? elem.propertyName.text
393
- : ts.isIdentifier(elem.name)
394
- ? elem.name.text
395
- : undefined
396
- if (propertyName) {
397
- usedWires.push(propertyName)
398
- }
399
- }
400
- }
572
+ const wires = extractUsedWires(handler, 2)
401
573
 
402
574
  // --- Generics → ts.Type[], unwrapped from Promise ---
403
575
  const genericTypes: ts.Type[] = (typeArguments ?? [])
404
576
  .map((tn) => checker.getTypeFromTypeNode(tn))
405
577
  .map((t) => unwrapPromise(checker, t))
406
578
 
579
+ const capitalizedName = funcIdToTypeName(name)
580
+
407
581
  // --- Input Extraction ---
408
- let { names: inputNames, types: inputTypes } = getNamesAndTypes(
409
- checker,
410
- state.functions.typesMap,
411
- 'Input',
412
- name,
413
- genericTypes[0]
414
- )
415
- // if (inputTypes.length === 0) {
416
- // logger.debug(
417
- // `\x1b[31m• Unknown input type for '${name}', assuming void.\x1b[0m`
418
- // )
419
- // }
582
+ let inputNames: string[] = []
583
+ let inputTypes: ts.Type[] = []
584
+
585
+ if (inputSchemaRef) {
586
+ const schemaName = `${capitalizedName}Input`
587
+ inputNames = [schemaName]
588
+ state.schemaLookup.set(schemaName, inputSchemaRef)
589
+ state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
590
+ } else if (genericTypes.length >= 1 && genericTypes[0]) {
591
+ // Fall back to extracting from generic type arguments
592
+ const result = getNamesAndTypes(
593
+ checker,
594
+ state.functions.typesMap,
595
+ 'Input',
596
+ name,
597
+ genericTypes[0]
598
+ )
599
+ inputNames = result.names
600
+ inputTypes = result.types
601
+ } else {
602
+ // Fall back to extracting from the function's second parameter type
603
+ const secondParam = handler.parameters[1]
604
+ if (secondParam) {
605
+ const paramType = checker.getTypeAtLocation(secondParam)
606
+ const result = getNamesAndTypes(
607
+ checker,
608
+ state.functions.typesMap,
609
+ 'Input',
610
+ pikkuFuncId,
611
+ paramType
612
+ )
613
+ inputNames = result.names
614
+ inputTypes = result.types
615
+ }
616
+ }
420
617
 
421
618
  // --- Output Extraction ---
422
619
  let outputNames: string[] = []
423
- if (genericTypes.length >= 2) {
620
+
621
+ if (outputSchemaRef) {
622
+ const schemaName = `${capitalizedName}Output`
623
+ outputNames = [schemaName]
624
+ state.schemaLookup.set(schemaName, outputSchemaRef)
625
+ state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
626
+ } else if (genericTypes.length >= 2) {
424
627
  outputNames = getNamesAndTypes(
425
628
  checker,
426
629
  state.functions.typesMap,
@@ -437,12 +640,55 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
437
640
  checker,
438
641
  state.functions.typesMap,
439
642
  'Output',
440
- pikkuFuncName,
643
+ pikkuFuncId,
441
644
  unwrapped
442
645
  ).names
443
646
  }
444
647
  }
445
648
 
649
+ const mcpOutputTypes: Record<string, string> = {
650
+ pikkuMCPResourceFunc: 'MCPResourceResponse',
651
+ pikkuMCPToolFunc: 'MCPToolResponse',
652
+ pikkuMCPPromptFunc: 'MCPPromptResponse',
653
+ }
654
+ const mcpOutputType = mcpOutputTypes[expression.text]
655
+ if (mcpOutputType && outputNames[0] !== mcpOutputType) {
656
+ let resolved = false
657
+ const rawSymbol = checker.getSymbolAtLocation(expression)
658
+ const funcSymbol =
659
+ rawSymbol && rawSymbol.flags & ts.SymbolFlags.Alias
660
+ ? checker.getAliasedSymbol(rawSymbol)
661
+ : rawSymbol
662
+ const funcDecls = funcSymbol?.getDeclarations() || []
663
+ for (const funcDecl of funcDecls) {
664
+ if (resolved) break
665
+ const mcpTypeSymbol = checker.resolveName(
666
+ mcpOutputType,
667
+ funcDecl,
668
+ ts.SymbolFlags.Type,
669
+ false
670
+ )
671
+ if (mcpTypeSymbol) {
672
+ const aliased =
673
+ mcpTypeSymbol.flags & ts.SymbolFlags.Alias
674
+ ? checker.getAliasedSymbol(mcpTypeSymbol)
675
+ : mcpTypeSymbol
676
+ const typeDecl = aliased?.getDeclarations()?.[0]
677
+ if (typeDecl) {
678
+ const path = typeDecl.getSourceFile().fileName
679
+ if (!state.functions.typesMap.exists(mcpOutputType, path)) {
680
+ state.functions.typesMap.addType(mcpOutputType, path)
681
+ }
682
+ resolved = true
683
+ }
684
+ }
685
+ }
686
+ if (!resolved) {
687
+ state.functions.typesMap.addCustomType(mcpOutputType, mcpOutputType, [])
688
+ }
689
+ outputNames = [mcpOutputType]
690
+ }
691
+
446
692
  if (inputNames.length > 1) {
447
693
  logger.warn(
448
694
  'More than one input type detected, only the first one will be used as a schema.'
@@ -451,47 +697,150 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
451
697
 
452
698
  // Store the input type for later use
453
699
  if (inputTypes.length > 0) {
454
- state.typesLookup.set(pikkuFuncName, inputTypes)
700
+ state.typesLookup.set(pikkuFuncId, inputTypes)
455
701
  }
456
702
 
457
703
  // --- resolve middleware ---
458
- const middleware = objectNode
704
+ let middleware = objectNode
459
705
  ? resolveMiddleware(state, objectNode, tags, checker)
460
706
  : undefined
461
707
 
462
- state.functions.meta[pikkuFuncName] = {
463
- pikkuFuncName,
708
+ // --- resolve permissions ---
709
+ let permissions = objectNode
710
+ ? resolvePermissions(state, objectNode, tags, checker)
711
+ : undefined
712
+
713
+ if (options.tags?.length) {
714
+ tags = [...new Set([...(tags || []), ...options.tags])]
715
+ const tagEntries = options.tags.map((tag) => ({
716
+ type: 'tag' as const,
717
+ tag,
718
+ }))
719
+ const existingMiddlewareTags = new Set(
720
+ (middleware || [])
721
+ .filter((m) => m.type === 'tag')
722
+ .map((m) => (m as any).tag)
723
+ )
724
+ const newMiddleware = tagEntries.filter(
725
+ (e) => !existingMiddlewareTags.has(e.tag)
726
+ )
727
+ if (newMiddleware.length > 0) {
728
+ middleware = [...(middleware || []), ...newMiddleware]
729
+ }
730
+ const existingPermissionTags = new Set(
731
+ (permissions || [])
732
+ .filter((p) => p.type === 'tag')
733
+ .map((p) => (p as any).tag)
734
+ )
735
+ const newPermissions = tagEntries.filter(
736
+ (e) => !existingPermissionTags.has(e.tag)
737
+ )
738
+ if (newPermissions.length > 0) {
739
+ permissions = [...(permissions || []), ...newPermissions]
740
+ }
741
+ }
742
+
743
+ const sessionless = expression.text !== 'pikkuFunc'
744
+
745
+ state.functions.meta[pikkuFuncId] = {
746
+ pikkuFuncId,
747
+ functionType: 'user',
748
+ funcWrapper: expression.text,
749
+ sessionless,
464
750
  name,
465
751
  services,
466
- usedWires: usedWires.length > 0 ? usedWires : undefined,
752
+ wires: wires.wires.length > 0 || !wires.optimized ? wires : undefined,
467
753
  inputSchemaName: inputNames[0] ?? null,
468
754
  outputSchemaName: outputNames[0] ?? null,
469
755
  inputs: inputNames.filter((n) => n !== 'void') ?? null,
470
756
  outputs: outputNames.filter((n) => n !== 'void') ?? null,
471
757
  expose: expose || undefined,
472
- internal: internal || undefined,
758
+ remote: remote || undefined,
759
+ mcp: mcpEnabled || undefined,
760
+ requiresApproval: requiresApproval || undefined,
761
+ version,
762
+ title,
473
763
  tags: tags || undefined,
474
764
  summary,
475
765
  description,
476
766
  errors,
477
767
  middleware,
768
+ permissions,
478
769
  isDirectFunction,
479
770
  }
480
771
 
481
- // Store function file location for wiring generation
482
- if (exportedName) {
483
- state.functions.files.set(pikkuFuncName, {
484
- path: node.getSourceFile().fileName,
485
- exportedName,
486
- })
772
+ // Populate node metadata if node config is present
773
+ if (nodeDisplayName && nodeCategory && nodeType) {
774
+ state.nodes.files.add(node.getSourceFile().fileName)
775
+ state.nodes.meta[pikkuFuncId] = {
776
+ name: pikkuFuncId,
777
+ displayName: nodeDisplayName,
778
+ category: nodeCategory,
779
+ type: nodeType,
780
+ rpc: pikkuFuncId,
781
+ description,
782
+ errorOutput: nodeErrorOutput ?? false,
783
+ inputSchemaName: inputNames[0] ?? null,
784
+ outputSchemaName: outputNames[0] ?? null,
785
+ tags,
786
+ }
787
+ }
788
+
789
+ if (mcpEnabled) {
790
+ if (!description) {
791
+ logger.critical(
792
+ ErrorCode.MISSING_DESCRIPTION,
793
+ `MCP tool '${name}' is missing a description.`
794
+ )
795
+ return
796
+ }
797
+ state.mcpEndpoints.files.add(node.getSourceFile().fileName)
798
+ state.mcpEndpoints.toolsMeta[name] = {
799
+ pikkuFuncId,
800
+ name,
801
+ title: title || undefined,
802
+ description,
803
+ summary,
804
+ errors,
805
+ tags,
806
+ inputSchema: inputNames[0] ?? null,
807
+ outputSchema: outputNames[0] ?? null,
808
+ middleware,
809
+ permissions,
810
+ }
811
+ state.serviceAggregation.usedFunctions.add(pikkuFuncId)
812
+ extractWireNames(middleware).forEach((n) =>
813
+ state.serviceAggregation.usedMiddleware.add(n)
814
+ )
815
+ extractWireNames(permissions).forEach((n) =>
816
+ state.serviceAggregation.usedPermissions.add(n)
817
+ )
487
818
  }
488
819
 
489
820
  // Workflow functions don't get registered as RPC functions,
490
- // they are their own type handled by add-workdflow
821
+ // they are their own type handled by add-workflow
491
822
  if (expression.text.includes('Workflow')) {
492
823
  return
493
824
  }
494
825
 
826
+ // Trigger and channel connect/disconnect functions are not callable via RPC
827
+ const nonRPCPatterns = [
828
+ /Trigger/i,
829
+ /ChannelConnection/i,
830
+ /ChannelDisconnection/i,
831
+ ]
832
+ if (nonRPCPatterns.some((pattern) => pattern.test(expression.text))) {
833
+ return
834
+ }
835
+
836
+ // Store function file location for wiring generation
837
+ if (exportedName) {
838
+ state.functions.files.set(pikkuFuncId, {
839
+ path: node.getSourceFile().fileName,
840
+ exportedName,
841
+ })
842
+ }
843
+
495
844
  if (exportedName || explicitName) {
496
845
  if (!exportedName) {
497
846
  logger.error(
@@ -500,28 +849,37 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
500
849
  return
501
850
  }
502
851
 
503
- // Mark internal functions as invoked to force bundling
504
- if (internal) {
505
- state.rpc.invokedFunctions.add(pikkuFuncName)
852
+ if (remote) {
853
+ state.rpc.invokedFunctions.add(pikkuFuncId)
506
854
  }
507
855
 
508
856
  if (expose) {
509
- state.rpc.exposedMeta[name] = pikkuFuncName
857
+ state.rpc.exposedMeta[name] = pikkuFuncId
510
858
  state.rpc.exposedFiles.set(name, {
511
859
  path: node.getSourceFile().fileName,
512
860
  exportedName,
513
861
  })
514
862
  // Track exposed RPC function for service aggregation
515
- state.serviceAggregation.usedFunctions.add(pikkuFuncName)
863
+ state.serviceAggregation.usedFunctions.add(pikkuFuncId)
516
864
  }
517
865
 
518
866
  // We add it to internal meta to allow autocomplete for everything
519
- state.rpc.internalMeta[name] = pikkuFuncName
867
+ state.rpc.internalMeta[name] = pikkuFuncId
868
+
869
+ if (version !== undefined) {
870
+ state.rpc.internalMeta[pikkuFuncId] = pikkuFuncId
871
+ state.rpc.invokedFunctions.add(pikkuFuncId)
872
+ }
520
873
 
521
874
  // But we only import the actual function if it's actually invoked to keep
522
875
  // bundle size down
523
- if (state.rpc.invokedFunctions.has(pikkuFuncName) || expose || internal) {
524
- state.rpc.internalFiles.set(pikkuFuncName, {
876
+ if (
877
+ state.rpc.invokedFunctions.has(pikkuFuncId) ||
878
+ expose ||
879
+ remote ||
880
+ mcpEnabled
881
+ ) {
882
+ state.rpc.internalFiles.set(pikkuFuncId, {
525
883
  path: node.getSourceFile().fileName,
526
884
  exportedName,
527
885
  })