@pikku/inspector 0.12.59 → 0.12.61

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 (132) hide show
  1. package/CHANGELOG.md +373 -0
  2. package/OPTIMIZATION-PLAN.md +19 -19
  3. package/dist/add/add-agent-scorer.d.ts +2 -0
  4. package/dist/add/{add-ai-scorer.js → add-agent-scorer.js} +4 -4
  5. package/dist/add/{add-ai-agent.d.ts → add-agent.d.ts} +1 -1
  6. package/dist/add/{add-ai-agent.js → add-agent.js} +11 -11
  7. package/dist/add/add-auth.js +1 -1
  8. package/dist/add/add-cli.js +5 -0
  9. package/dist/add/add-functions.js +10 -1
  10. package/dist/add/add-http-route.js +18 -18
  11. package/dist/add/add-keyed-wiring.d.ts +2 -0
  12. package/dist/add/add-keyed-wiring.js +3 -1
  13. package/dist/add/add-middleware.js +5 -5
  14. package/dist/add/add-personas.js +3 -0
  15. package/dist/add/add-secret.js +1 -0
  16. package/dist/add/add-variable.js +1 -0
  17. package/dist/add/add-wire-remote-addon.js +2 -1
  18. package/dist/add/add-workflow-graph.js +63 -6
  19. package/dist/error-codes.d.ts +3 -1
  20. package/dist/error-codes.js +3 -1
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.js +1 -0
  23. package/dist/inspector.js +13 -3
  24. package/dist/types.d.ts +10 -5
  25. package/dist/utils/annotate-http-route-auth.js +2 -1
  26. package/dist/utils/check-secret-sinks.js +2 -1
  27. package/dist/utils/compute-required-schemas.d.ts +1 -1
  28. package/dist/utils/contract-hashes.d.ts +2 -1
  29. package/dist/utils/contract-hashes.js +1 -1
  30. package/dist/utils/extract-function-name.js +1 -1
  31. package/dist/utils/extract-services.d.ts +1 -1
  32. package/dist/utils/extract-surface-usage.d.ts +29 -0
  33. package/dist/utils/extract-surface-usage.js +97 -0
  34. package/dist/utils/filter-inspector-state.js +53 -16
  35. package/dist/utils/filter-utils.d.ts +1 -1
  36. package/dist/utils/get-property-value.js +42 -3
  37. package/dist/utils/load-addon-functions-meta.js +31 -0
  38. package/dist/utils/middleware.d.ts +2 -2
  39. package/dist/utils/middleware.js +5 -5
  40. package/dist/utils/permissions.d.ts +1 -1
  41. package/dist/utils/post-process.d.ts +28 -8
  42. package/dist/utils/post-process.js +83 -18
  43. package/dist/utils/resolve-deploy-target.d.ts +1 -1
  44. package/dist/utils/resolve-function-meta.d.ts +1 -1
  45. package/dist/utils/resolve-function-types.d.ts +1 -1
  46. package/dist/utils/resolve-versions.js +1 -1
  47. package/dist/utils/schema-generator.d.ts +1 -1
  48. package/dist/utils/serialize-inspector-state.d.ts +4 -3
  49. package/dist/utils/serialize-inspector-state.js +6 -4
  50. package/dist/utils/serialize-openapi-json.d.ts +1 -1
  51. package/dist/utils/validate-auth-sessionless.js +2 -1
  52. package/dist/utils/workflow/graph/finalize-workflows.js +1 -1
  53. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +6 -1
  54. package/dist/utils/workflow/graph/serialize-workflow-graph.js +26 -2
  55. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +9 -0
  56. package/dist/visit.js +4 -4
  57. package/package.json +10 -10
  58. package/run-tests.sh +0 -0
  59. package/src/add/{add-ai-scorer.test.ts → add-agent-scorer.test.ts} +11 -11
  60. package/src/add/{add-ai-scorer.ts → add-agent-scorer.ts} +4 -4
  61. package/src/add/{add-ai-agent.test.ts → add-agent.test.ts} +1 -1
  62. package/src/add/{add-ai-agent.ts → add-agent.ts} +13 -21
  63. package/src/add/add-auth.ts +2 -2
  64. package/src/add/add-cli.ts +6 -0
  65. package/src/add/add-credential.ts +3 -6
  66. package/src/add/add-file-with-factory.ts +2 -6
  67. package/src/add/add-functions.ts +23 -25
  68. package/src/add/add-http-route-ref.test.ts +149 -0
  69. package/src/add/add-http-route.ts +25 -28
  70. package/src/add/add-keyed-wiring.ts +8 -7
  71. package/src/add/add-middleware.ts +7 -5
  72. package/src/add/add-permission.ts +1 -1
  73. package/src/add/add-personas.test.ts +24 -0
  74. package/src/add/add-personas.ts +3 -1
  75. package/src/add/add-scenario-step.test.ts +35 -0
  76. package/src/add/add-scope.test.ts +4 -4
  77. package/src/add/add-scope.ts +4 -8
  78. package/src/add/add-secret.ts +1 -0
  79. package/src/add/add-system-role.test.ts +8 -8
  80. package/src/add/add-system-role.ts +2 -4
  81. package/src/add/add-variable.ts +1 -0
  82. package/src/add/add-wire-remote-addon.ts +4 -1
  83. package/src/add/add-workflow-graph-foreach.test.ts +174 -0
  84. package/src/add/add-workflow-graph.ts +78 -6
  85. package/src/add/add-workflow-sleep-expression.test.ts +3 -4
  86. package/src/add/addon-contracts.test.ts +1 -1
  87. package/src/add/config-value-metadata.test.ts +94 -0
  88. package/src/add/secret-sink-check.test.ts +4 -1
  89. package/src/error-codes.ts +3 -1
  90. package/src/index.ts +3 -4
  91. package/src/inspector.ts +20 -3
  92. package/src/types.ts +10 -7
  93. package/src/utils/annotate-http-route-auth.test.ts +5 -7
  94. package/src/utils/annotate-http-route-auth.ts +2 -1
  95. package/src/utils/check-secret-sinks.ts +4 -1
  96. package/src/utils/compute-required-schemas.ts +1 -1
  97. package/src/utils/contract-hashes.test.ts +1 -1
  98. package/src/utils/contract-hashes.ts +3 -2
  99. package/src/utils/extract-function-name.ts +1 -1
  100. package/src/utils/extract-services.ts +4 -2
  101. package/src/utils/extract-surface-usage.test.ts +147 -0
  102. package/src/utils/extract-surface-usage.ts +121 -0
  103. package/src/utils/filter-inspector-state.test.ts +138 -12
  104. package/src/utils/filter-inspector-state.ts +82 -21
  105. package/src/utils/filter-utils.ts +1 -1
  106. package/src/utils/get-property-value.ts +43 -5
  107. package/src/utils/load-addon-functions-meta.ts +40 -0
  108. package/src/utils/middleware.test.ts +23 -23
  109. package/src/utils/middleware.ts +6 -6
  110. package/src/utils/permissions.ts +1 -1
  111. package/src/utils/post-process.test.ts +68 -11
  112. package/src/utils/post-process.ts +91 -28
  113. package/src/utils/resolve-deploy-target.ts +1 -1
  114. package/src/utils/resolve-function-meta.ts +1 -1
  115. package/src/utils/resolve-function-types.ts +1 -1
  116. package/src/utils/resolve-versions.ts +1 -1
  117. package/src/utils/schema-generator.ts +2 -1
  118. package/src/utils/serialize-inspector-state.ts +10 -7
  119. package/src/utils/serialize-mcp-json.ts +1 -1
  120. package/src/utils/serialize-openapi-json.ts +1 -1
  121. package/src/utils/validate-auth-sessionless.ts +2 -1
  122. package/src/utils/validate-tags-resolve-to-middleware.test.ts +4 -1
  123. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +4 -1
  124. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
  125. package/src/utils/workflow/graph/finalize-workflows.ts +6 -2
  126. package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
  127. package/src/utils/workflow/graph/workflow-graph.types.ts +10 -0
  128. package/src/visit.ts +4 -4
  129. package/tsconfig.tsbuildinfo +1 -1
  130. package/dist/add/add-ai-scorer.d.ts +0 -2
  131. package/dist/utils/secret-brokers.d.ts +0 -2
  132. package/dist/utils/secret-brokers.js +0 -9
@@ -3,10 +3,23 @@ import type {
3
3
  InspectorFilters,
4
4
  InspectorLogger,
5
5
  } from '../types.js'
6
- import type { PikkuWiringTypes } from '@pikku/core'
7
- import { parseVersionedId } from '@pikku/core'
6
+ import type { PikkuWiringTypes } from '@pikku/core/types'
7
+ import { parseVersionedId } from '@pikku/core/utils'
8
+ import { makeContextBasedId } from './extract-function-name.js'
8
9
  import { aggregateRequiredServices } from './post-process.js'
9
- import { resolveDeployTarget } from './resolve-deploy-target.js'
10
+ import {
11
+ IncompatibleDeployTargetError,
12
+ resolveDeployTarget,
13
+ } from './resolve-deploy-target.js'
14
+
15
+ /**
16
+ * The deploy filter, asked about one function at a time rather than handed over
17
+ * as a set, so that a function's own deploy conflict is raised only if this app
18
+ * turns out to wire it.
19
+ */
20
+ type DeployFilter = {
21
+ has(pikkuFuncId: string): boolean
22
+ }
10
23
 
11
24
  // Module-level Set to track warned groups across multiple filter calls
12
25
  const globalWarnedGroups = new Set<string>()
@@ -97,6 +110,13 @@ function matchesFilters(
97
110
  meta: {
98
111
  type: PikkuWiringTypes
99
112
  name: string
113
+ /**
114
+ * Further ids the `--names` filter may address this wiring by. An HTTP
115
+ * route wired with `ref('ns:fn')` carries the addon's own function id as
116
+ * its name, so its `http:<method>:<route>` wiring id is the only id that
117
+ * names this route rather than every wiring onto that addon function.
118
+ */
119
+ altNames?: string[]
100
120
  tags?: string[]
101
121
  filePath?: string
102
122
  httpRoute?: string
@@ -105,9 +125,10 @@ function matchesFilters(
105
125
  },
106
126
  logger: InspectorLogger,
107
127
  warnedGroups?: Set<string>,
108
- // Set of pikkuFuncIds to keep based on the deploy filter; null when
109
- // the deploy filter is inactive.
110
- keptByDeploy?: Set<string> | null
128
+ // Kept pikkuFuncIds under the deploy filter; null when the filter is
129
+ // inactive. Asked about one id at a time, which is what lets a conflict in an
130
+ // addon's own metadata surface only for a function this app actually wires.
131
+ keptByDeploy?: DeployFilter | null
111
132
  ): boolean {
112
133
  // If no filters, allow everything
113
134
  if (Object.keys(filters).length === 0) return true
@@ -188,10 +209,15 @@ function matchesFilters(
188
209
  }
189
210
  }
190
211
 
191
- const { baseName } = parseVersionedId(meta.name)
212
+ const candidateNames = [meta.name, ...(meta.altNames ?? [])]
192
213
  const matchesNamePattern = (pattern: string) =>
193
- matchesWildcard(meta.name, pattern) ||
194
- (baseName !== meta.name && matchesWildcard(baseName, pattern))
214
+ candidateNames.some((candidate) => {
215
+ const { baseName } = parseVersionedId(candidate)
216
+ return (
217
+ matchesWildcard(candidate, pattern) ||
218
+ (baseName !== candidate && matchesWildcard(baseName, pattern))
219
+ )
220
+ })
195
221
 
196
222
  // Check name include filter (match against both full ID and base name for versioned functions)
197
223
  if (filters.names && filters.names.length > 0) {
@@ -330,7 +356,7 @@ export function filterInspectorState(
330
356
  // Precompute kept-function set for the deploy filter (if active).
331
357
  // resolveDeployTarget throws IncompatibleDeployTargetError when an
332
358
  // explicit deploy: 'serverless' clashes with serverlessIncompatible.
333
- let keptByDeploy: Set<string> | null = null
359
+ let keptByDeploy: DeployFilter | null = null
334
360
  if (
335
361
  (filters.target && filters.target.length > 0) ||
336
362
  (filters.excludeTarget && filters.excludeTarget.length > 0)
@@ -341,17 +367,56 @@ export function filterInspectorState(
341
367
  : null
342
368
  const incompatible = new Set(filters.serverlessIncompatible ?? [])
343
369
  const defaultTarget = filters.defaultTarget ?? 'serverless'
344
- keptByDeploy = new Set<string>()
345
- for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
370
+ const kept = new Set<string>()
371
+ // An addon publishes metadata for everything it exports, so a conflict in a
372
+ // function this app never wires is not this app's build error. Holding it
373
+ // here and raising it from `has` reports it at the one place the app is
374
+ // known to depend on that function.
375
+ const conflicts = new Map<string, IncompatibleDeployTargetError>()
376
+ keptByDeploy = {
377
+ has: (funcId: string) => {
378
+ const conflict = conflicts.get(funcId)
379
+ if (conflict) throw conflict
380
+ return kept.has(funcId)
381
+ },
382
+ }
383
+ const keepByTarget = (
384
+ funcId: string,
385
+ funcMeta: unknown,
386
+ incompatibleServices: Set<string>
387
+ ) => {
346
388
  const target = resolveDeployTarget(
347
389
  funcMeta as any,
348
- incompatible,
390
+ incompatibleServices,
349
391
  funcId,
350
392
  defaultTarget
351
393
  )
352
- if (allowed && !allowed.has(target)) continue
353
- if (excluded && excluded.has(target)) continue
354
- keptByDeploy.add(funcId)
394
+ if (allowed && !allowed.has(target)) return
395
+ if (excluded && excluded.has(target)) return
396
+ kept.add(funcId)
397
+ }
398
+ for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
399
+ keepByTarget(funcId, funcMeta, incompatible)
400
+ }
401
+ // A wiring onto an addon function records the addon's own id, so the
402
+ // addon's published metadata is the only place its deploy target can be
403
+ // read from. Its serverlessIncompatible services are namespace-scoped —
404
+ // a service name means whatever the addon that declared it means.
405
+ for (const [namespace, addonMeta] of Object.entries(
406
+ state.addonFunctions ?? {}
407
+ )) {
408
+ const addonIncompatible = new Set(
409
+ state.addonServerlessIncompatible?.get(namespace) ?? []
410
+ )
411
+ for (const [funcName, funcMeta] of Object.entries(addonMeta)) {
412
+ const funcId = `${namespace}:${funcName}`
413
+ try {
414
+ keepByTarget(funcId, funcMeta, addonIncompatible)
415
+ } catch (e) {
416
+ if (!(e instanceof IncompatibleDeployTargetError)) throw e
417
+ conflicts.set(funcId, e)
418
+ }
419
+ }
355
420
  }
356
421
  }
357
422
 
@@ -459,6 +524,7 @@ export function filterInspectorState(
459
524
  {
460
525
  type: 'http' as PikkuWiringTypes,
461
526
  name: routeMeta.pikkuFuncId, // Use function name, not route
527
+ altNames: [makeContextBasedId('http', method, routeMeta.route)],
462
528
  tags: routeMeta.tags,
463
529
  filePath,
464
530
  httpRoute: routeMeta.route,
@@ -478,11 +544,6 @@ export function filterInspectorState(
478
544
  filteredState.serviceAggregation.usedFunctions.add(
479
545
  routeMeta.pikkuFuncId
480
546
  )
481
- if (routeMeta.refTarget) {
482
- filteredState.serviceAggregation.usedFunctions.add(
483
- routeMeta.refTarget
484
- )
485
- }
486
547
  // For workflow/agent routes, also add the base name
487
548
  // so the workflow/agent definition survives pruning
488
549
  const colonIdx = routeMeta.pikkuFuncId.indexOf(':')
@@ -1,5 +1,5 @@
1
1
  import type { InspectorFilters, InspectorLogger } from '../types.js'
2
- import type { PikkuWiringTypes } from '@pikku/core'
2
+ import type { PikkuWiringTypes } from '@pikku/core/types'
3
3
 
4
4
  /**
5
5
  * Match a value against a pattern with wildcard support
@@ -127,6 +127,46 @@ export const assertStringLiteralProperty = (
127
127
  }
128
128
  }
129
129
 
130
+ /**
131
+ * Fold a node that is a compile-time constant string down to its value.
132
+ *
133
+ * The fallback below this is `getText()`, which returns the node's *source*.
134
+ * For a lone `'…'` that is indistinguishable from the value, so nobody noticed
135
+ * that anything written as an expression comes through raw: a `description`
136
+ * split across lines as `'Fetch the thing ' + 'from upstream'` reaches
137
+ * `infra.json` with the quotes and the `+` still in it, and is rendered that
138
+ * way in the fabric console.
139
+ *
140
+ * Deliberately checker-free, unlike `extractStringLiteral`. Every value this
141
+ * resolves is a literal in the same expression, so no symbol resolution is
142
+ * needed, and `getPropertyValue` is called from a dozen places that have no
143
+ * checker to hand. Anything it cannot fold — an identifier, a template with a
144
+ * substitution — returns `undefined` and takes the old path, so this only ever
145
+ * turns a wrong answer into a right one.
146
+ */
147
+ const foldStaticString = (node: ts.Node): string | undefined => {
148
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
149
+ return node.text
150
+ }
151
+ if (ts.isParenthesizedExpression(node)) {
152
+ return foldStaticString(node.expression)
153
+ }
154
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
155
+ return foldStaticString(node.expression)
156
+ }
157
+ if (
158
+ ts.isBinaryExpression(node) &&
159
+ node.operatorToken.kind === ts.SyntaxKind.PlusToken
160
+ ) {
161
+ const left = foldStaticString(node.left)
162
+ if (left === undefined) return undefined
163
+ const right = foldStaticString(node.right)
164
+ if (right === undefined) return undefined
165
+ return left + right
166
+ }
167
+ return undefined
168
+ }
169
+
130
170
  export const getPropertyValue = (
131
171
  obj: ts.ObjectLiteralExpression,
132
172
  propertyName: string
@@ -174,11 +214,9 @@ export const getPropertyValue = (
174
214
  return Number(initializer.text)
175
215
  }
176
216
 
177
- if (
178
- ts.isStringLiteral(initializer) ||
179
- ts.isNoSubstitutionTemplateLiteral(initializer)
180
- ) {
181
- return initializer.text
217
+ const staticString = foldStaticString(initializer)
218
+ if (staticString !== undefined) {
219
+ return staticString
182
220
  }
183
221
 
184
222
  return initializer.getText()
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'fs'
1
2
  import { readFile, readdir } from 'fs/promises'
2
3
  import { createRequire } from 'module'
3
4
  import { join, dirname } from 'path'
@@ -141,6 +142,44 @@ const resolveAddonMeta = (
141
142
  return null
142
143
  }
143
144
 
145
+ /**
146
+ * Give the addon's input/output type names a declaration file to be imported
147
+ * from, so a wiring that references an addon function can name its contract.
148
+ *
149
+ * Without this the only thing that knows where `GetScenarioArtifactInput` lives
150
+ * is the type checker, and it only knows once the consumer's *own* rpc map has
151
+ * already been generated with the addon's routes in it — which is to say on the
152
+ * second run. A first, cold run resolved `ref('console:getScenarioArtifact')`
153
+ * against a map that did not mention it yet and fell back to the whole
154
+ * `FlattenedRPCMap`, so the same source produced a different http-map depending
155
+ * on whether `.pikku` happened to be there. The addon publishes these types at a
156
+ * fixed subpath; reading them from the metadata makes the first run agree with
157
+ * the second.
158
+ */
159
+ const registerAddonTypes = (
160
+ state: InspectorState,
161
+ metaPath: string,
162
+ meta: Record<string, any>
163
+ ): void => {
164
+ // Derived from the metadata's own location rather than resolved as a package
165
+ // subpath: the addon exports that declaration under a `types`-only condition,
166
+ // which `require.resolve` will not follow.
167
+ const typesPath = join(
168
+ dirname(metaPath),
169
+ '..',
170
+ 'rpc',
171
+ 'pikku-rpc-wirings-map.internal.gen.d.ts'
172
+ )
173
+ if (!existsSync(typesPath)) return
174
+
175
+ for (const funcMeta of Object.values(meta)) {
176
+ for (const name of [funcMeta.inputSchemaName, funcMeta.outputSchemaName]) {
177
+ if (!name || state.functions.typesMap.exists(name, typesPath)) continue
178
+ state.functions.typesMap.addType(name, typesPath)
179
+ }
180
+ }
181
+ }
182
+
144
183
  /**
145
184
  * After the setup sweep discovers wireAddon() declarations, load each addon
146
185
  * package's function metadata so that wiring handlers (channels, HTTP routes,
@@ -177,6 +216,7 @@ export async function loadAddonFunctionsMeta(
177
216
  logger.debug(
178
217
  `Loaded ${Object.keys(meta).length} addon functions for '${namespace}' from ${decl.package}`
179
218
  )
219
+ registerAddonTypes(state, metaPath, meta)
180
220
 
181
221
  // If wireAddon has mcp: true, expose addon functions with mcp: true as MCP tools
182
222
  if (decl.mcp) {
@@ -1,6 +1,6 @@
1
1
  import { describe, test } from 'node:test'
2
2
  import assert from 'node:assert'
3
- import { resolveAIMiddleware } from './middleware.js'
3
+ import { resolveAgentMiddleware } from './middleware.js'
4
4
  import { getInitialInspectorState } from '../inspector.js'
5
5
  import * as ts from 'typescript'
6
6
 
@@ -26,39 +26,39 @@ function createMockChecker(): ts.TypeChecker {
26
26
  return {} as any
27
27
  }
28
28
 
29
- describe('resolveAIMiddleware', () => {
30
- test('should return undefined when no aiMiddleware property exists', () => {
29
+ describe('resolveAgentMiddleware', () => {
30
+ test('should return undefined when no agentMiddleware property exists', () => {
31
31
  const state = getInitialInspectorState('/test')
32
32
  const src = createSourceFile('const x = { name: "test" }')
33
33
  const obj = getObjectLiteral(src)
34
34
  const checker = createMockChecker()
35
35
 
36
- const result = resolveAIMiddleware(state, obj, checker)
36
+ const result = resolveAgentMiddleware(state, obj, checker)
37
37
 
38
38
  assert.strictEqual(result, undefined)
39
39
  })
40
40
 
41
- test('should return undefined when aiMiddleware array is empty', () => {
41
+ test('should return undefined when agentMiddleware array is empty', () => {
42
42
  const state = getInitialInspectorState('/test')
43
- const src = createSourceFile('const x = { aiMiddleware: [] }')
43
+ const src = createSourceFile('const x = { agentMiddleware: [] }')
44
44
  const obj = getObjectLiteral(src)
45
45
  const checker = createMockChecker()
46
46
 
47
- const result = resolveAIMiddleware(state, obj, checker)
47
+ const result = resolveAgentMiddleware(state, obj, checker)
48
48
 
49
49
  assert.strictEqual(result, undefined)
50
50
  })
51
51
 
52
- test('should resolve aiMiddleware identifiers as wire metadata', () => {
52
+ test('should resolve agentMiddleware identifiers as wire metadata', () => {
53
53
  const state = getInitialInspectorState('/test')
54
- state.aiMiddleware.definitions['myAIMW'] = {
54
+ state.agentMiddleware.definitions['myAIMW'] = {
55
55
  services: { optimized: true, services: ['logger'] },
56
56
  sourceFile: '/test/middleware.ts',
57
57
  position: 0,
58
58
  exportedName: 'myAIMW',
59
59
  }
60
60
 
61
- const code = 'const x = { aiMiddleware: [myAIMW] }'
61
+ const code = 'const x = { agentMiddleware: [myAIMW] }'
62
62
  const program = ts.createProgram({
63
63
  rootNames: ['test.ts'],
64
64
  options: { target: ts.ScriptTarget.ESNext },
@@ -77,7 +77,7 @@ describe('resolveAIMiddleware', () => {
77
77
  const sourceFile = program.getSourceFile('test.ts')!
78
78
  const obj = getObjectLiteral(sourceFile)
79
79
 
80
- const result = resolveAIMiddleware(state, obj, checker)
80
+ const result = resolveAgentMiddleware(state, obj, checker)
81
81
 
82
82
  assert.ok(result)
83
83
  assert.strictEqual(result.length, 1)
@@ -88,22 +88,22 @@ describe('resolveAIMiddleware', () => {
88
88
  })
89
89
  })
90
90
 
91
- test('should resolve multiple aiMiddleware entries', () => {
91
+ test('should resolve multiple agentMiddleware entries', () => {
92
92
  const state = getInitialInspectorState('/test')
93
- state.aiMiddleware.definitions['firstMW'] = {
93
+ state.agentMiddleware.definitions['firstMW'] = {
94
94
  services: { optimized: true, services: ['logger'] },
95
95
  sourceFile: '/test/middleware.ts',
96
96
  position: 0,
97
97
  exportedName: 'firstMW',
98
98
  }
99
- state.aiMiddleware.definitions['secondMW'] = {
99
+ state.agentMiddleware.definitions['secondMW'] = {
100
100
  services: { optimized: true, services: [] },
101
101
  sourceFile: '/test/middleware.ts',
102
102
  position: 100,
103
103
  exportedName: 'secondMW',
104
104
  }
105
105
 
106
- const code = 'const x = { aiMiddleware: [firstMW, secondMW] }'
106
+ const code = 'const x = { agentMiddleware: [firstMW, secondMW] }'
107
107
  const program = ts.createProgram({
108
108
  rootNames: ['test.ts'],
109
109
  options: { target: ts.ScriptTarget.ESNext },
@@ -122,7 +122,7 @@ describe('resolveAIMiddleware', () => {
122
122
  const sourceFile = program.getSourceFile('test.ts')!
123
123
  const obj = getObjectLiteral(sourceFile)
124
124
 
125
- const result = resolveAIMiddleware(state, obj, checker)
125
+ const result = resolveAgentMiddleware(state, obj, checker)
126
126
 
127
127
  assert.ok(result)
128
128
  assert.strictEqual(result.length, 2)
@@ -132,16 +132,16 @@ describe('resolveAIMiddleware', () => {
132
132
  assert.strictEqual((result[1] as any).name, 'secondMW')
133
133
  })
134
134
 
135
- test('should mark inline aiMiddleware (null exportedName)', () => {
135
+ test('should mark inline agentMiddleware (null exportedName)', () => {
136
136
  const state = getInitialInspectorState('/test')
137
- state.aiMiddleware.definitions['inlineMW'] = {
137
+ state.agentMiddleware.definitions['inlineMW'] = {
138
138
  services: { optimized: true, services: [] },
139
139
  sourceFile: '/test/middleware.ts',
140
140
  position: 0,
141
141
  exportedName: null,
142
142
  }
143
143
 
144
- const code = 'const x = { aiMiddleware: [inlineMW] }'
144
+ const code = 'const x = { agentMiddleware: [inlineMW] }'
145
145
  const program = ts.createProgram({
146
146
  rootNames: ['test.ts'],
147
147
  options: { target: ts.ScriptTarget.ESNext },
@@ -160,7 +160,7 @@ describe('resolveAIMiddleware', () => {
160
160
  const sourceFile = program.getSourceFile('test.ts')!
161
161
  const obj = getObjectLiteral(sourceFile)
162
162
 
163
- const result = resolveAIMiddleware(state, obj, checker)
163
+ const result = resolveAgentMiddleware(state, obj, checker)
164
164
 
165
165
  assert.ok(result)
166
166
  assert.strictEqual(result.length, 1)
@@ -171,10 +171,10 @@ describe('resolveAIMiddleware', () => {
171
171
  })
172
172
  })
173
173
 
174
- test('should handle aiMiddleware referencing unknown definitions', () => {
174
+ test('should handle agentMiddleware referencing unknown definitions', () => {
175
175
  const state = getInitialInspectorState('/test')
176
176
 
177
- const code = 'const x = { aiMiddleware: [unknownMW] }'
177
+ const code = 'const x = { agentMiddleware: [unknownMW] }'
178
178
  const program = ts.createProgram({
179
179
  rootNames: ['test.ts'],
180
180
  options: { target: ts.ScriptTarget.ESNext },
@@ -193,7 +193,7 @@ describe('resolveAIMiddleware', () => {
193
193
  const sourceFile = program.getSourceFile('test.ts')!
194
194
  const obj = getObjectLiteral(sourceFile)
195
195
 
196
- const result = resolveAIMiddleware(state, obj, checker)
196
+ const result = resolveAgentMiddleware(state, obj, checker)
197
197
 
198
198
  assert.ok(result)
199
199
  assert.strictEqual(result.length, 1)
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript'
2
- import type { MiddlewareMetadata } from '@pikku/core'
2
+ import type { MiddlewareMetadata } from '@pikku/core/middleware'
3
3
  import { extractFunctionName } from './extract-function-name.js'
4
4
  import type { InspectorState } from '../types.js'
5
5
 
@@ -214,24 +214,24 @@ export function resolveHTTPMiddlewareFromObject(
214
214
  )
215
215
  }
216
216
 
217
- function getAIMiddlewareNode(
217
+ function getAgentMiddlewareNode(
218
218
  obj: ts.ObjectLiteralExpression
219
219
  ): ts.Expression | undefined {
220
220
  const prop = obj.properties.find(
221
221
  (p) =>
222
222
  ts.isPropertyAssignment(p) &&
223
223
  ts.isIdentifier(p.name) &&
224
- p.name.text === 'aiMiddleware'
224
+ p.name.text === 'agentMiddleware'
225
225
  ) as ts.PropertyAssignment | undefined
226
226
  return prop?.initializer
227
227
  }
228
228
 
229
- export function resolveAIMiddleware(
229
+ export function resolveAgentMiddleware(
230
230
  state: InspectorState,
231
231
  obj: ts.ObjectLiteralExpression,
232
232
  checker: ts.TypeChecker
233
233
  ): MiddlewareMetadata[] | undefined {
234
- const explicitNode = getAIMiddlewareNode(obj)
234
+ const explicitNode = getAgentMiddlewareNode(obj)
235
235
  if (!explicitNode) return undefined
236
236
 
237
237
  const names = extractMiddlewarePikkuNames(
@@ -240,7 +240,7 @@ export function resolveAIMiddleware(
240
240
  state.rootDir
241
241
  )
242
242
  const resolved: MiddlewareMetadata[] = names.map((name) => {
243
- const def = state.aiMiddleware.definitions[name]
243
+ const def = state.agentMiddleware.definitions[name]
244
244
  return {
245
245
  type: 'wire' as const,
246
246
  name,
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript'
2
- import type { PermissionMetadata } from '@pikku/core'
2
+ import type { PermissionMetadata } from '@pikku/core/function'
3
3
  import { extractFunctionName } from './extract-function-name.js'
4
4
  import type { InspectorState } from '../types.js'
5
5
 
@@ -1,6 +1,6 @@
1
1
  import { strict as assert } from 'assert'
2
2
  import { describe, test } from 'node:test'
3
- import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
3
+ import { mkdirSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { join } from 'node:path'
6
6
  import {
@@ -142,7 +142,7 @@ function makeState(
142
142
 
143
143
  const CONSOLE_PARENT_SERVICES = [
144
144
  'metaService',
145
- 'aiAgentRunner',
145
+ 'agentRunner',
146
146
  'deploymentService',
147
147
  'credentialService',
148
148
  ]
@@ -187,7 +187,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
187
187
  runAgent: {
188
188
  services: {
189
189
  optimized: true,
190
- services: ['aiAgentRunner', 'deploymentService'],
190
+ services: ['agentRunner', 'deploymentService'],
191
191
  },
192
192
  },
193
193
  },
@@ -197,7 +197,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
197
197
  aggregateRequiredServices(state)
198
198
  const required = state.serviceAggregation.requiredServices
199
199
  assert.ok(required.has('metaService'))
200
- assert.ok(!required.has('aiAgentRunner'))
200
+ assert.ok(!required.has('agentRunner'))
201
201
  assert.ok(!required.has('deploymentService'))
202
202
  assert.ok(!required.has('credentialService'))
203
203
  })
@@ -258,7 +258,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
258
258
  const required = state.serviceAggregation.requiredServices
259
259
  assert.ok(required.has('kysely'))
260
260
  assert.ok(!required.has('metaService'))
261
- assert.ok(!required.has('aiAgentRunner'))
261
+ assert.ok(!required.has('agentRunner'))
262
262
  })
263
263
 
264
264
  test('no used addon functions adds no parent services', () => {
@@ -279,7 +279,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
279
279
  aggregateRequiredServices(state)
280
280
  const required = state.serviceAggregation.requiredServices
281
281
  assert.ok(!required.has('metaService'))
282
- assert.ok(!required.has('aiAgentRunner'))
282
+ assert.ok(!required.has('agentRunner'))
283
283
  })
284
284
 
285
285
  test('internal services in addon function meta never force the blanket', () => {
@@ -300,7 +300,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
300
300
  aggregateRequiredServices(state)
301
301
  const required = state.serviceAggregation.requiredServices
302
302
  assert.ok(required.has('metaService'))
303
- assert.ok(!required.has('aiAgentRunner'))
303
+ assert.ok(!required.has('agentRunner'))
304
304
  assert.ok(!required.has('rpc'))
305
305
  })
306
306
 
@@ -345,7 +345,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
345
345
  aggregateRequiredServices(state)
346
346
  const required = state.serviceAggregation.requiredServices
347
347
  assert.ok(required.has('metaService'))
348
- assert.ok(!required.has('aiAgentRunner'))
348
+ assert.ok(!required.has('agentRunner'))
349
349
  })
350
350
 
351
351
  test('multiple used addon functions union their parent services', () => {
@@ -357,7 +357,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
357
357
  services: { optimized: true, services: ['metaService'] },
358
358
  },
359
359
  runAgent: {
360
- services: { optimized: true, services: ['aiAgentRunner'] },
360
+ services: { optimized: true, services: ['agentRunner'] },
361
361
  },
362
362
  },
363
363
  },
@@ -366,7 +366,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
366
366
  aggregateRequiredServices(state)
367
367
  const required = state.serviceAggregation.requiredServices
368
368
  assert.ok(required.has('metaService'))
369
- assert.ok(required.has('aiAgentRunner'))
369
+ assert.ok(required.has('agentRunner'))
370
370
  assert.ok(!required.has('deploymentService'))
371
371
  assert.ok(!required.has('credentialService'))
372
372
  })
@@ -579,10 +579,67 @@ describe('validateRemoteAddonDependencies (wireRemoteAddon must be a devDependen
579
579
  }
580
580
  })
581
581
 
582
- test('ignores a non-remote (wireAddon) declaration', () => {
582
+ test('a non-remote (wireAddon) declaration is judged on resolvability, not package.json', () => {
583
+ // Being listed as a dependency is not what makes an addon usable — it is
584
+ // discovered because it is wired, and it can arrive by workspace link or a
585
+ // local addons/ directory without ever appearing here. So a declared but
586
+ // absent package is still an error.
583
587
  const { logger, criticals } = makeCriticalLogger()
584
588
  const dir = writePkg({ '@addon/local': '1.0.0' }, {})
585
589
  try {
590
+ validateRemoteAddonDependencies(
591
+ logger,
592
+ makeRemoteState(dir, { package: '@addon/local' })
593
+ )
594
+ assert.equal(criticals.length, 1)
595
+ assert.equal(criticals[0]!.code, ErrorCode.ADDON_NOT_INSTALLED)
596
+ } finally {
597
+ rmSync(dir, { recursive: true, force: true })
598
+ }
599
+ })
600
+
601
+ test('a non-remote addon present only in node_modules passes', () => {
602
+ // The inverse, and the case that matters: installed by link, absent from
603
+ // package.json. Reported as missing, this would fail every project whose
604
+ // addon lives in a local addons/ directory.
605
+ const { logger, criticals } = makeCriticalLogger()
606
+ const dir = writePkg({}, {})
607
+ try {
608
+ const pkgDir = join(dir, 'node_modules', '@addon', 'local')
609
+ mkdirSync(pkgDir, { recursive: true })
610
+ writeFileSync(
611
+ join(pkgDir, 'package.json'),
612
+ JSON.stringify({ name: '@addon/local', version: '1.0.0' })
613
+ )
614
+ validateRemoteAddonDependencies(
615
+ logger,
616
+ makeRemoteState(dir, { package: '@addon/local' })
617
+ )
618
+ assert.deepEqual(criticals, [])
619
+ } finally {
620
+ rmSync(dir, { recursive: true, force: true })
621
+ }
622
+ })
623
+
624
+ test('an exports map that refuses package.json is not read as missing', () => {
625
+ // `require.resolve('<pkg>/package.json')` throws ERR_PACKAGE_PATH_NOT_EXPORTED
626
+ // on a package with a restrictive exports map. Reaching the exports map
627
+ // means the package was found, so that must not count as absent.
628
+ const { logger, criticals } = makeCriticalLogger()
629
+ const dir = writePkg({}, {})
630
+ try {
631
+ const pkgDir = join(dir, 'node_modules', '@addon', 'local')
632
+ mkdirSync(pkgDir, { recursive: true })
633
+ writeFileSync(
634
+ join(pkgDir, 'package.json'),
635
+ JSON.stringify({
636
+ name: '@addon/local',
637
+ version: '1.0.0',
638
+ main: 'index.js',
639
+ exports: { '.': './index.js' },
640
+ })
641
+ )
642
+ writeFileSync(join(pkgDir, 'index.js'), 'module.exports = {}\n')
586
643
  validateRemoteAddonDependencies(
587
644
  logger,
588
645
  makeRemoteState(dir, { package: '@addon/local' })