@pikku/inspector 0.12.10 → 0.12.12

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 (63) hide show
  1. package/CHANGELOG.md +21 -9
  2. package/dist/add/add-cli.js +10 -3
  3. package/dist/add/add-credential.js +2 -1
  4. package/dist/add/add-functions.js +99 -5
  5. package/dist/add/add-http-route.js +44 -6
  6. package/dist/add/add-keyed-wiring.js +3 -1
  7. package/dist/add/add-middleware.js +33 -4
  8. package/dist/add/add-permission.js +7 -7
  9. package/dist/add/add-workflow-graph.js +20 -1
  10. package/dist/error-codes.d.ts +2 -0
  11. package/dist/error-codes.js +2 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/inspector.js +2 -5
  15. package/dist/types.d.ts +10 -19
  16. package/dist/utils/extract-function-name.js +6 -0
  17. package/dist/utils/filter-inspector-state.js +187 -59
  18. package/dist/utils/filter-utils.js +13 -5
  19. package/dist/utils/get-property-value.d.ts +10 -0
  20. package/dist/utils/get-property-value.js +30 -0
  21. package/dist/utils/post-process.d.ts +2 -3
  22. package/dist/utils/post-process.js +3 -23
  23. package/dist/utils/resolve-addon-package.d.ts +4 -5
  24. package/dist/utils/resolve-addon-package.js +64 -16
  25. package/dist/utils/resolve-deploy-target.d.ts +28 -0
  26. package/dist/utils/resolve-deploy-target.js +56 -0
  27. package/dist/utils/resolve-versions.js +79 -0
  28. package/dist/utils/schema-generator.js +31 -12
  29. package/dist/utils/validate-auth-sessionless.d.ts +1 -1
  30. package/package.json +2 -2
  31. package/src/add/add-cli.ts +10 -3
  32. package/src/add/add-credential.ts +3 -0
  33. package/src/add/add-functions.test.ts +318 -0
  34. package/src/add/add-functions.ts +164 -6
  35. package/src/add/add-gateway.ts +5 -1
  36. package/src/add/add-http-route.ts +54 -7
  37. package/src/add/add-keyed-wiring.ts +7 -1
  38. package/src/add/add-mcp-prompt.ts +5 -1
  39. package/src/add/add-mcp-resource.ts +5 -1
  40. package/src/add/add-middleware.ts +42 -4
  41. package/src/add/add-permission.ts +7 -7
  42. package/src/add/add-schedule.ts +5 -1
  43. package/src/add/add-workflow-graph.ts +19 -1
  44. package/src/add/wire-name-literal.test.ts +114 -0
  45. package/src/error-codes.ts +2 -0
  46. package/src/index.ts +1 -0
  47. package/src/inspector.ts +1 -5
  48. package/src/types.ts +19 -15
  49. package/src/utils/extract-function-name.ts +8 -0
  50. package/src/utils/filter-inspector-state.test.ts +168 -64
  51. package/src/utils/filter-inspector-state.ts +290 -64
  52. package/src/utils/filter-utils.test.ts +30 -15
  53. package/src/utils/filter-utils.ts +14 -5
  54. package/src/utils/get-property-value.ts +40 -0
  55. package/src/utils/post-process.ts +3 -38
  56. package/src/utils/resolve-addon-package.ts +65 -14
  57. package/src/utils/resolve-deploy-target.test.ts +105 -0
  58. package/src/utils/resolve-deploy-target.ts +63 -0
  59. package/src/utils/resolve-versions.test.ts +108 -0
  60. package/src/utils/resolve-versions.ts +86 -0
  61. package/src/utils/schema-generator.ts +37 -13
  62. package/src/utils/validate-auth-sessionless.ts +1 -1
  63. package/tsconfig.tsbuildinfo +1 -1
@@ -61,7 +61,8 @@ export const matchesFilters = (
61
61
  if (
62
62
  (!filters.names || filters.names.length === 0) &&
63
63
  (!filters.tags || filters.tags.length === 0) &&
64
- (!filters.types || filters.types.length === 0) &&
64
+ (!filters.wires || filters.wires.length === 0) &&
65
+ (!filters.excludeWires || filters.excludeWires.length === 0) &&
65
66
  (!filters.directories || filters.directories.length === 0) &&
66
67
  (!filters.httpRoutes || filters.httpRoutes.length === 0) &&
67
68
  (!filters.httpMethods || filters.httpMethods.length === 0)
@@ -69,10 +70,18 @@ export const matchesFilters = (
69
70
  return true
70
71
  }
71
72
 
72
- // Check type filter
73
- if (filters.types && filters.types.length > 0) {
74
- if (!filters.types.includes(meta.type)) {
75
- logger.debug(`⒡ Filtered by type: ${meta.type}:${meta.name}`)
73
+ // Check wire include filter
74
+ if (filters.wires && filters.wires.length > 0) {
75
+ if (!filters.wires.includes(meta.type)) {
76
+ logger.debug(`⒡ Filtered by wire include: ${meta.type}:${meta.name}`)
77
+ return false
78
+ }
79
+ }
80
+
81
+ // Check wire exclude filter
82
+ if (filters.excludeWires && filters.excludeWires.length > 0) {
83
+ if (filters.excludeWires.includes(meta.type)) {
84
+ logger.debug(`⒡ Filtered by wire exclude: ${meta.type}:${meta.name}`)
76
85
  return false
77
86
  }
78
87
  }
@@ -27,6 +27,44 @@ export const getArrayPropertyValue = (
27
27
  return null
28
28
  }
29
29
 
30
+ /**
31
+ * Wiring identity fields (`name`, `secretId`, `variableId`, …) are read
32
+ * STATICALLY from source — a const or variable reference is keyed by its
33
+ * identifier text, not its runtime value, so the wiring is silently skipped at
34
+ * runtime (`metadata not found`). If the named property exists but is not an
35
+ * inline literal, raise a fatal diagnostic so the build fails instead.
36
+ */
37
+ export const assertStringLiteralProperty = (
38
+ obj: ts.ObjectLiteralExpression,
39
+ propertyName: string,
40
+ wiringType: string,
41
+ logger?: { critical: (code: ErrorCode, message: string) => void }
42
+ ): void => {
43
+ const property = obj.properties.find(
44
+ (p) =>
45
+ ts.isPropertyAssignment(p) &&
46
+ ts.isIdentifier(p.name) &&
47
+ p.name.text === propertyName
48
+ )
49
+ if (!property || !ts.isPropertyAssignment(property)) {
50
+ return
51
+ }
52
+ const init = property.initializer
53
+ const isStaticLiteral =
54
+ ts.isStringLiteral(init) ||
55
+ ts.isNoSubstitutionTemplateLiteral(init) ||
56
+ ts.isNumericLiteral(init)
57
+ if (isStaticLiteral) {
58
+ return
59
+ }
60
+ const errorMsg = `${wiringType} has a non-literal '${propertyName}': \`${init.getText()}\`. Wiring identity fields must be inline string literals — the inspector reads them statically from source, so a const or variable reference is keyed by its identifier text and the wiring is silently skipped at runtime. Inline the literal instead, e.g. ${propertyName}: 'my-wiring-name'.`
61
+ if (logger) {
62
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, errorMsg)
63
+ } else {
64
+ console.error(errorMsg)
65
+ }
66
+ }
67
+
30
68
  export const getPropertyValue = (
31
69
  obj: ts.ObjectLiteralExpression,
32
70
  propertyName: string
@@ -117,6 +155,8 @@ export const getCommonWireMetaData = (
117
155
  errors?: string[]
118
156
  } = {}
119
157
 
158
+ assertStringLiteralProperty(obj, 'name', wiringType, logger)
159
+
120
160
  obj.properties.forEach((prop) => {
121
161
  if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
122
162
  const propName = prop.name.text
@@ -2,7 +2,6 @@ import type {
2
2
  InspectorState,
3
3
  InspectorLogger,
4
4
  InspectorOptions,
5
- InspectorModelConfig,
6
5
  MiddlewareGroupMeta,
7
6
  InspectorDiagnostic,
8
7
  } from '../types.js'
@@ -538,11 +537,8 @@ export function computeRequiredSchemas(
538
537
 
539
538
  export function validateAgentModels(
540
539
  logger: InspectorLogger,
541
- state: InspectorState | Omit<InspectorState, 'typesLookup'>,
542
- modelConfig?: InspectorModelConfig
540
+ state: InspectorState | Omit<InspectorState, 'typesLookup'>
543
541
  ): void {
544
- const aliases = modelConfig?.models ?? {}
545
-
546
542
  for (const [, meta] of Object.entries(state.agents.agentsMeta)) {
547
543
  const model = meta.model
548
544
  if (!model) {
@@ -552,41 +548,10 @@ export function validateAgentModels(
552
548
  )
553
549
  continue
554
550
  }
555
- if (model.includes('/')) continue
556
- if (!aliases[model]) {
557
- const available = Object.keys(aliases)
558
- logger.critical(
559
- ErrorCode.INVALID_MODEL,
560
- `AI agent '${meta.name}' uses model alias '${model}' which is not defined in pikku.config.json models. ` +
561
- `Available aliases: ${available.join(', ') || 'none'}`
562
- )
563
- }
564
- }
565
- }
566
-
567
- export function validateAgentOverrides(
568
- logger: InspectorLogger,
569
- state: InspectorState | Omit<InspectorState, 'typesLookup'>,
570
- modelConfig?: InspectorModelConfig
571
- ): void {
572
- const overrides = modelConfig?.agentOverrides ?? {}
573
- const aliases = modelConfig?.models ?? {}
574
- const agentNames = new Set(
575
- Object.values(state.agents.agentsMeta).map((m) => m.name)
576
- )
577
-
578
- for (const [agentName, override] of Object.entries(overrides)) {
579
- if (!agentNames.has(agentName)) {
580
- logger.warn(`agentOverrides references unknown agent '${agentName}'`)
581
- }
582
- if (
583
- override.model &&
584
- !override.model.includes('/') &&
585
- !aliases[override.model]
586
- ) {
551
+ if (!model.includes('/')) {
587
552
  logger.critical(
588
553
  ErrorCode.INVALID_MODEL,
589
- `agentOverrides['${agentName}'].model uses alias '${override.model}' which is not defined in models.`
554
+ `AI agent '${meta.name}' uses model '${model}', which must be provider-qualified as '<provider>/<model>' (e.g. 'openai/gpt-4').`
590
555
  )
591
556
  }
592
557
  }
@@ -1,13 +1,43 @@
1
1
  import * as ts from 'typescript'
2
+ import { existsSync, readFileSync } from 'fs'
3
+ import { dirname, join, parse } from 'path'
4
+
5
+ const packageNameCache = new Map<string, string | null>()
6
+
7
+ const findPackageNameForFile = (filePath: string): string | null => {
8
+ if (packageNameCache.has(filePath)) {
9
+ return packageNameCache.get(filePath)!
10
+ }
11
+ const root = parse(filePath).root
12
+ let dir = dirname(filePath)
13
+ while (dir && dir !== root) {
14
+ const pkgPath = join(dir, 'package.json')
15
+ if (existsSync(pkgPath)) {
16
+ try {
17
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
18
+ const name = typeof pkg.name === 'string' ? pkg.name : null
19
+ packageNameCache.set(filePath, name)
20
+ return name
21
+ } catch {
22
+ packageNameCache.set(filePath, null)
23
+ return null
24
+ }
25
+ }
26
+ const parent = dirname(dir)
27
+ if (parent === dir) break
28
+ dir = parent
29
+ }
30
+ packageNameCache.set(filePath, null)
31
+ return null
32
+ }
2
33
 
3
34
  /**
4
35
  * Resolve the addon package name from an imported identifier.
5
36
  * Checks if the identifier's import module specifier matches any
6
- * configured addon package.
7
- *
8
- * This is a general utility any wire handler that processes a `func`
9
- * property can use it to detect when the function comes from an
10
- * addon package.
37
+ * configured addon package — and if the import is relative (because
38
+ * the identifier resolves to a source file inside the addon package
39
+ * itself), walks up to the nearest package.json to obtain the real
40
+ * package name.
11
41
  */
12
42
  export const resolveAddonName = (
13
43
  identifier: ts.Identifier,
@@ -31,18 +61,39 @@ export const resolveAddonName = (
31
61
  if (!sym) return null
32
62
 
33
63
  const decl = sym.declarations?.[0]
34
- if (!decl || !ts.isImportSpecifier(decl)) return null
64
+ if (!decl) return null
65
+
66
+ let candidatePackage: string | null = null
35
67
 
36
- // ImportSpecifier -> NamedImports -> ImportClause -> ImportDeclaration
37
- const importDecl = decl.parent?.parent?.parent
38
- if (!importDecl || !ts.isImportDeclaration(importDecl)) return null
39
- if (!ts.isStringLiteral(importDecl.moduleSpecifier)) return null
68
+ if (ts.isImportSpecifier(decl)) {
69
+ // ImportSpecifier -> NamedImports -> ImportClause -> ImportDeclaration
70
+ const importDecl = decl.parent?.parent?.parent
71
+ if (
72
+ importDecl &&
73
+ ts.isImportDeclaration(importDecl) &&
74
+ ts.isStringLiteral(importDecl.moduleSpecifier)
75
+ ) {
76
+ candidatePackage = importDecl.moduleSpecifier.text
77
+ }
78
+ }
40
79
 
41
- const moduleSpecifier = importDecl.moduleSpecifier.text
80
+ // Bare package import path
81
+ if (candidatePackage && !candidatePackage.startsWith('.')) {
82
+ for (const addonDecl of wireAddonDeclarations.values()) {
83
+ if (addonDecl.package === candidatePackage) return addonDecl.package
84
+ }
85
+ }
42
86
 
43
- for (const addonDecl of wireAddonDeclarations.values()) {
44
- if (addonDecl.package === moduleSpecifier) {
45
- return addonDecl.package
87
+ // Fall back to package.json lookup based on the declaration's source file.
88
+ // This catches the case where the identifier resolves into an addon
89
+ // package's own internal source (relative import inside that package).
90
+ const declFile = decl.getSourceFile()?.fileName
91
+ if (declFile) {
92
+ const pkgName = findPackageNameForFile(declFile)
93
+ if (pkgName) {
94
+ for (const addonDecl of wireAddonDeclarations.values()) {
95
+ if (addonDecl.package === pkgName) return addonDecl.package
96
+ }
46
97
  }
47
98
  }
48
99
 
@@ -0,0 +1,105 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import {
4
+ IncompatibleDeployTargetError,
5
+ resolveDeployTarget,
6
+ } from './resolve-deploy-target.js'
7
+
8
+ describe('resolveDeployTarget', () => {
9
+ test('default → serverless', () => {
10
+ assert.strictEqual(resolveDeployTarget({}, new Set()), 'serverless')
11
+ })
12
+
13
+ test('explicit deploy: server → server', () => {
14
+ assert.strictEqual(
15
+ resolveDeployTarget({ deploy: 'server' }, new Set()),
16
+ 'server'
17
+ )
18
+ })
19
+
20
+ test('explicit deploy: serverless with no incompatible svc → serverless', () => {
21
+ assert.strictEqual(
22
+ resolveDeployTarget(
23
+ { deploy: 'serverless', services: { services: ['kysely'] } as any },
24
+ new Set(['fs'])
25
+ ),
26
+ 'serverless'
27
+ )
28
+ })
29
+
30
+ test('serverlessIncompatible service forces server even without deploy flag', () => {
31
+ assert.strictEqual(
32
+ resolveDeployTarget(
33
+ { services: { services: ['metaService'] } as any },
34
+ new Set(['metaService'])
35
+ ),
36
+ 'server'
37
+ )
38
+ })
39
+
40
+ test('serverlessIncompatible takes precedence over explicit deploy: server', () => {
41
+ assert.strictEqual(
42
+ resolveDeployTarget(
43
+ {
44
+ deploy: 'server',
45
+ services: { services: ['metaService'] } as any,
46
+ },
47
+ new Set(['metaService'])
48
+ ),
49
+ 'server'
50
+ )
51
+ })
52
+
53
+ test('explicit deploy: serverless + incompatible svc → throws', () => {
54
+ assert.throws(
55
+ () =>
56
+ resolveDeployTarget(
57
+ {
58
+ deploy: 'serverless',
59
+ services: { services: ['metaService', 'unrelated'] } as any,
60
+ },
61
+ new Set(['metaService']),
62
+ 'myFunction'
63
+ ),
64
+ (err: unknown) => {
65
+ assert.ok(err instanceof IncompatibleDeployTargetError)
66
+ assert.strictEqual(err.functionName, 'myFunction')
67
+ assert.deepStrictEqual(err.incompatibleServices, ['metaService'])
68
+ assert.match(err.message, /serverless-incompatible/)
69
+ assert.match(err.message, /myFunction/)
70
+ return true
71
+ }
72
+ )
73
+ })
74
+
75
+ test('multiple incompatible services are all reported', () => {
76
+ assert.throws(
77
+ () =>
78
+ resolveDeployTarget(
79
+ {
80
+ deploy: 'serverless',
81
+ services: {
82
+ services: ['metaService', 'localContent'],
83
+ } as any,
84
+ },
85
+ new Set(['metaService', 'localContent']),
86
+ 'multiSvc'
87
+ ),
88
+ (err: unknown) => {
89
+ assert.ok(err instanceof IncompatibleDeployTargetError)
90
+ assert.deepStrictEqual(err.incompatibleServices, [
91
+ 'metaService',
92
+ 'localContent',
93
+ ])
94
+ return true
95
+ }
96
+ )
97
+ })
98
+
99
+ test('handles funcMeta with no services field', () => {
100
+ assert.strictEqual(
101
+ resolveDeployTarget({ deploy: 'server' }, new Set(['metaService'])),
102
+ 'server'
103
+ )
104
+ })
105
+ })
@@ -0,0 +1,63 @@
1
+ import type { FunctionMeta } from '@pikku/core'
2
+
3
+ /**
4
+ * Thrown when a function's explicit `deploy: 'serverless'` conflicts
5
+ * with one of its services being declared `serverlessIncompatible`.
6
+ * The user has to either remove the explicit flag (let it auto-resolve
7
+ * to 'server'), or set `deploy: 'server'` explicitly.
8
+ */
9
+ export class IncompatibleDeployTargetError extends Error {
10
+ constructor(
11
+ public readonly functionName: string,
12
+ public readonly incompatibleServices: string[]
13
+ ) {
14
+ super(
15
+ `Function '${functionName}' is declared deploy: 'serverless' but uses ` +
16
+ `serverless-incompatible service(s) [${incompatibleServices.join(', ')}]. ` +
17
+ `Either remove deploy: 'serverless' (will auto-resolve to 'server'), ` +
18
+ `or set deploy: 'server' explicitly.`
19
+ )
20
+ this.name = 'IncompatibleDeployTargetError'
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Determine the effective deploy target for a function.
26
+ *
27
+ * Resolution order:
28
+ * 1. If any of the function's services is in `serverlessIncompatible`:
29
+ * - throw if the function explicitly declares `deploy: 'serverless'`
30
+ * - otherwise target is 'server'
31
+ * 2. Explicit `funcMeta.deploy: 'serverless' | 'server'`
32
+ * 3. Default 'serverless'
33
+ *
34
+ * Used both by the per-unit deploy analyzer (when bucketing functions
35
+ * into deployment units) and by `filterInspectorState` (when
36
+ * `pikku all --deploy <target>` is used to emit a target-scoped set
37
+ * of gen files).
38
+ */
39
+ export function resolveDeployTarget(
40
+ funcMeta: Pick<FunctionMeta, 'deploy' | 'services'>,
41
+ serverlessIncompatible: Set<string>,
42
+ functionName = '<unknown>'
43
+ ): 'serverless' | 'server' {
44
+ // Service compatibility wins over the explicit flag — a serverless
45
+ // bundle of a function that needs (e.g.) node:fs would crash at runtime.
46
+ const incompatibleHits: string[] = []
47
+ if (funcMeta.services?.services) {
48
+ for (const svc of funcMeta.services.services) {
49
+ if (serverlessIncompatible.has(svc)) incompatibleHits.push(svc)
50
+ }
51
+ }
52
+
53
+ if (incompatibleHits.length > 0) {
54
+ if (funcMeta.deploy === 'serverless') {
55
+ throw new IncompatibleDeployTargetError(functionName, incompatibleHits)
56
+ }
57
+ return 'server'
58
+ }
59
+
60
+ if (funcMeta.deploy === 'server') return 'server'
61
+ if (funcMeta.deploy === 'serverless') return 'serverless'
62
+ return 'serverless'
63
+ }
@@ -387,4 +387,112 @@ describe('resolveLatestVersions', () => {
387
387
  assert.strictEqual(state.rpc.internalFiles.has('createUser'), false)
388
388
  assert.ok(state.rpc.internalFiles.has('createUser@v2'))
389
389
  })
390
+
391
+ test('rewrites CLI, channel, scheduler, queue and MCP funcId references when an unversioned function gets an implicit version', () => {
392
+ const state = makeState({
393
+ 'listCards@v1': { pikkuFuncId: 'listCards@v1', version: 1 },
394
+ listCards: { pikkuFuncId: 'listCards' },
395
+ })
396
+ // CLI command (+ nested subcommand) referencing the unversioned function
397
+ state.cli = {
398
+ meta: {
399
+ programs: {
400
+ kanban: {
401
+ name: 'kanban',
402
+ commands: {
403
+ list: { pikkuFuncId: 'listCards' },
404
+ cards: {
405
+ pikkuFuncId: 'listCards',
406
+ subcommands: { all: { pikkuFuncId: 'listCards' } },
407
+ },
408
+ },
409
+ },
410
+ },
411
+ renderers: {},
412
+ },
413
+ } as any
414
+ // Channel slots + action-routed message wiring
415
+ state.channels = {
416
+ meta: {
417
+ cli: {
418
+ name: 'cli',
419
+ route: '/cli/kanban',
420
+ input: null,
421
+ connect: null,
422
+ disconnect: null,
423
+ message: { pikkuFuncId: 'listCards' },
424
+ messageWirings: {
425
+ command: { list: { pikkuFuncId: 'listCards' } },
426
+ },
427
+ },
428
+ },
429
+ files: new Set(),
430
+ } as any
431
+ state.scheduledTasks = {
432
+ meta: {
433
+ tick: { name: 'tick', schedule: '* * * * *', pikkuFuncId: 'listCards' },
434
+ },
435
+ files: new Set(),
436
+ } as any
437
+ state.queueWorkers = {
438
+ meta: { worker: { name: 'worker', pikkuFuncId: 'listCards' } },
439
+ files: new Set(),
440
+ } as any
441
+ state.mcpEndpoints = {
442
+ resourcesMeta: { res: { pikkuFuncId: 'listCards' } },
443
+ toolsMeta: { tool: { pikkuFuncId: 'listCards' } },
444
+ promptsMeta: { prompt: { pikkuFuncId: 'listCards' } },
445
+ files: new Set(),
446
+ } as any
447
+ state.triggers = {
448
+ meta: { cardCreated: { name: 'cardCreated' } },
449
+ sourceMeta: {
450
+ cardCreated: { name: 'cardCreated', pikkuFuncId: 'listCards' },
451
+ },
452
+ files: new Set(),
453
+ } as any
454
+ const { logger } = makeLogger()
455
+
456
+ resolveLatestVersions(state, logger)
457
+
458
+ // unversioned listCards becomes the implicit latest (v2)
459
+ assert.ok(state.functions.meta['listCards@v2'])
460
+ const program = (state as any).cli.meta.programs.kanban
461
+ assert.strictEqual(program.commands.list.pikkuFuncId, 'listCards@v2')
462
+ assert.strictEqual(program.commands.cards.pikkuFuncId, 'listCards@v2')
463
+ assert.strictEqual(
464
+ program.commands.cards.subcommands.all.pikkuFuncId,
465
+ 'listCards@v2'
466
+ )
467
+ const channel = (state as any).channels.meta.cli
468
+ assert.strictEqual(channel.message.pikkuFuncId, 'listCards@v2')
469
+ assert.strictEqual(
470
+ channel.messageWirings.command.list.pikkuFuncId,
471
+ 'listCards@v2'
472
+ )
473
+ assert.strictEqual(
474
+ (state as any).scheduledTasks.meta.tick.pikkuFuncId,
475
+ 'listCards@v2'
476
+ )
477
+ assert.strictEqual(
478
+ (state as any).queueWorkers.meta.worker.pikkuFuncId,
479
+ 'listCards@v2'
480
+ )
481
+ assert.strictEqual(
482
+ (state as any).mcpEndpoints.resourcesMeta.res.pikkuFuncId,
483
+ 'listCards@v2'
484
+ )
485
+ assert.strictEqual(
486
+ (state as any).mcpEndpoints.toolsMeta.tool.pikkuFuncId,
487
+ 'listCards@v2'
488
+ )
489
+ assert.strictEqual(
490
+ (state as any).mcpEndpoints.promptsMeta.prompt.pikkuFuncId,
491
+ 'listCards@v2'
492
+ )
493
+ assert.strictEqual(
494
+ (state as any).triggers.sourceMeta.cardCreated.pikkuFuncId,
495
+ 'listCards@v2'
496
+ )
497
+ })
390
498
  })
@@ -1,4 +1,5 @@
1
1
  import { parseVersionedId, formatVersionedId } from '@pikku/core'
2
+ import type { CLICommandMeta } from '@pikku/core/cli'
2
3
  import type { InspectorState, InspectorLogger } from '../types.js'
3
4
  import { ErrorCode } from '../error-codes.js'
4
5
 
@@ -130,6 +131,7 @@ function updateWiringReferences(
130
131
  oldId: string,
131
132
  newId: string
132
133
  ): void {
134
+ // HTTP routes
133
135
  if (state.http) {
134
136
  for (const methods of Object.values(state.http.meta)) {
135
137
  for (const meta of Object.values(methods)) {
@@ -139,4 +141,88 @@ function updateWiringReferences(
139
141
  }
140
142
  }
141
143
  }
144
+
145
+ // Channels: connect/disconnect/message slots + action-routed message wirings
146
+ if (state.channels) {
147
+ for (const channel of Object.values(state.channels.meta)) {
148
+ for (const slot of [
149
+ channel.connect,
150
+ channel.disconnect,
151
+ channel.message,
152
+ ]) {
153
+ if (slot && slot.pikkuFuncId === oldId) {
154
+ slot.pikkuFuncId = newId
155
+ }
156
+ }
157
+ for (const routes of Object.values(channel.messageWirings)) {
158
+ for (const message of Object.values(routes)) {
159
+ if (message.pikkuFuncId === oldId) {
160
+ message.pikkuFuncId = newId
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ // CLI programs: commands and nested subcommands. This also covers
168
+ // CLI-over-channel generation, which reads command funcIds from this meta.
169
+ if (state.cli) {
170
+ const updateCommands = (commands: Record<string, CLICommandMeta>): void => {
171
+ for (const command of Object.values(commands)) {
172
+ if (command.pikkuFuncId === oldId) {
173
+ command.pikkuFuncId = newId
174
+ }
175
+ if (command.subcommands) {
176
+ updateCommands(command.subcommands)
177
+ }
178
+ }
179
+ }
180
+ for (const program of Object.values(state.cli.meta.programs)) {
181
+ updateCommands(program.commands)
182
+ }
183
+ }
184
+
185
+ // Scheduled tasks
186
+ if (state.scheduledTasks) {
187
+ for (const task of Object.values(state.scheduledTasks.meta)) {
188
+ if (task.pikkuFuncId === oldId) {
189
+ task.pikkuFuncId = newId
190
+ }
191
+ }
192
+ }
193
+
194
+ // Queue workers
195
+ if (state.queueWorkers) {
196
+ for (const worker of Object.values(state.queueWorkers.meta)) {
197
+ if (worker.pikkuFuncId === oldId) {
198
+ worker.pikkuFuncId = newId
199
+ }
200
+ }
201
+ }
202
+
203
+ // Trigger sources (TriggerSourceMeta carries the handler's pikkuFuncId).
204
+ // Gateways/workflows/agents reference functions by bare rpc name and are
205
+ // resolved at runtime via state.rpc.internalMeta, so they need no rewrite here.
206
+ if (state.triggers) {
207
+ for (const source of Object.values(state.triggers.sourceMeta)) {
208
+ if (source.pikkuFuncId === oldId) {
209
+ source.pikkuFuncId = newId
210
+ }
211
+ }
212
+ }
213
+
214
+ // MCP resources, tools, and prompts
215
+ if (state.mcpEndpoints) {
216
+ for (const collection of [
217
+ state.mcpEndpoints.resourcesMeta,
218
+ state.mcpEndpoints.toolsMeta,
219
+ state.mcpEndpoints.promptsMeta,
220
+ ]) {
221
+ for (const endpoint of Object.values(collection)) {
222
+ if (endpoint.pikkuFuncId === oldId) {
223
+ endpoint.pikkuFuncId = newId
224
+ }
225
+ }
226
+ }
227
+ }
142
228
  }