@pikku/inspector 0.12.7 → 0.12.8

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 (38) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/add/add-ai-agent.js +24 -7
  3. package/dist/add/add-channel.js +2 -2
  4. package/dist/add/add-cli.js +13 -10
  5. package/dist/add/add-file-with-factory.js +22 -5
  6. package/dist/add/add-functions.js +2 -0
  7. package/dist/add/add-http-route.js +1 -0
  8. package/dist/add/add-rpc-invocations.js +2 -2
  9. package/dist/add/add-workflow.d.ts +5 -0
  10. package/dist/add/add-workflow.js +19 -1
  11. package/dist/inspector.js +1 -0
  12. package/dist/types.d.ts +1 -0
  13. package/dist/utils/filter-inspector-state.js +204 -5
  14. package/dist/utils/load-addon-functions-meta.js +47 -0
  15. package/dist/utils/post-process.js +63 -0
  16. package/dist/utils/schema-generator.js +124 -33
  17. package/dist/utils/serialize-inspector-state.d.ts +1 -0
  18. package/dist/utils/serialize-inspector-state.js +2 -0
  19. package/dist/visit.js +1 -1
  20. package/package.json +2 -2
  21. package/src/add/add-ai-agent.ts +25 -10
  22. package/src/add/add-channel.ts +2 -2
  23. package/src/add/add-cli.ts +17 -16
  24. package/src/add/add-file-with-factory.ts +26 -7
  25. package/src/add/add-functions.ts +2 -0
  26. package/src/add/add-http-route.ts +6 -1
  27. package/src/add/add-queue-worker.ts +5 -1
  28. package/src/add/add-rpc-invocations.ts +2 -2
  29. package/src/add/add-workflow.ts +21 -1
  30. package/src/inspector.ts +1 -0
  31. package/src/types.ts +1 -0
  32. package/src/utils/filter-inspector-state.ts +239 -8
  33. package/src/utils/load-addon-functions-meta.ts +59 -0
  34. package/src/utils/post-process.ts +74 -0
  35. package/src/utils/schema-generator.ts +191 -41
  36. package/src/utils/serialize-inspector-state.ts +3 -0
  37. package/src/visit.ts +2 -1
  38. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 624097e: Add deploy pipeline with provider-agnostic architecture
8
+
9
+ - Add MetaService with explicit typed API, absorb WiringService reads
10
+ - Add deployment service, traceId propagation, scoped logger
11
+ - Rewrite analyzer: one function = one worker, gateways dispatch via RPC
12
+ - Add Cloudflare deploy provider with plan/apply commands
13
+ - Add per-unit filtered codegen for deploy pipeline
14
+ - Skip missing metadata in wiring registration for deploy units
15
+ - Fix schema coercion crash when schema has no properties
16
+ - Fix E2E codegen: double-pass resolves cross-package Zod type imports
17
+
18
+ - Updated dependencies [9e8605f]
19
+ - Updated dependencies [624097e]
20
+ - Updated dependencies [7ab3243]
21
+ - @pikku/core@0.12.15
22
+
3
23
  ## 0.12.7
4
24
 
5
25
  ### Patch Changes
@@ -33,7 +33,7 @@ function resolveToolReferences(obj, checker, agentName, logger) {
33
33
  continue;
34
34
  }
35
35
  }
36
- if (calleeName === 'addon') {
36
+ if (calleeName === 'ref') {
37
37
  const [firstArg] = element.arguments;
38
38
  if (firstArg && ts.isStringLiteral(firstArg)) {
39
39
  resolved.push(firstArg.text);
@@ -163,11 +163,12 @@ export const addAIAgent = (logger, node, checker, state, options) => {
163
163
  if (disabled)
164
164
  return;
165
165
  const modelValue = getPropertyValue(obj, 'model');
166
- const instructionsValue = getPropertyValue(obj, 'instructions');
166
+ const roleValue = getPropertyValue(obj, 'role');
167
+ const personalityValue = getPropertyValue(obj, 'personality');
168
+ const goalValue = getPropertyValue(obj, 'goal');
167
169
  const maxStepsValue = getPropertyValue(obj, 'maxSteps');
168
170
  const temperatureValue = getPropertyValue(obj, 'temperature');
169
171
  const toolChoiceValue = getPropertyValue(obj, 'toolChoice');
170
- const dynamicWorkflowsValue = getPropertyValue(obj, 'dynamicWorkflows');
171
172
  const toolsValue = resolveToolReferences(obj, checker, nameValue || '', logger);
172
173
  if (toolsValue) {
173
174
  for (const toolName of toolsValue) {
@@ -291,10 +292,14 @@ export const addAIAgent = (logger, node, checker, state, options) => {
291
292
  state.agents.agentsMeta[agentKey] = {
292
293
  name: nameValue,
293
294
  description,
294
- instructions: instructionsValue || '',
295
+ role: roleValue || undefined,
296
+ personality: personalityValue || undefined,
297
+ goal: goalValue || '',
295
298
  model: modelValue || '',
296
299
  summary,
297
300
  errors,
301
+ sourceFile: node.getSourceFile().fileName,
302
+ exportedName: exportedName || undefined,
298
303
  ...(maxStepsValue !== null && { maxSteps: maxStepsValue }),
299
304
  ...(temperatureValue !== null && { temperature: temperatureValue }),
300
305
  ...(toolChoiceValue !== null && {
@@ -302,9 +307,6 @@ export const addAIAgent = (logger, node, checker, state, options) => {
302
307
  }),
303
308
  ...(toolsValue !== null && { tools: toolsValue }),
304
309
  ...(agentsValue !== null && { agents: agentsValue }),
305
- ...(dynamicWorkflowsValue !== null && {
306
- dynamicWorkflows: dynamicWorkflowsValue,
307
- }),
308
310
  tags,
309
311
  inputSchema,
310
312
  outputSchema,
@@ -314,5 +316,20 @@ export const addAIAgent = (logger, node, checker, state, options) => {
314
316
  aiMiddleware,
315
317
  permissions,
316
318
  };
319
+ // AI agent functions require platform services that aren't visible
320
+ // through parameter destructuring
321
+ const funcMeta = state.functions.meta[agentKey];
322
+ if (funcMeta?.services) {
323
+ for (const svc of [
324
+ 'aiStorage',
325
+ 'aiRunState',
326
+ 'agentRunService',
327
+ 'aiAgentRunner',
328
+ ]) {
329
+ if (!funcMeta.services.services.includes(svc)) {
330
+ funcMeta.services.services.push(svc);
331
+ }
332
+ }
333
+ }
317
334
  }
318
335
  };
@@ -65,8 +65,8 @@ function getHandlerNameFromExpression(expr, checker, rootDir) {
65
65
  }
66
66
  // Handle call expressions
67
67
  if (ts.isCallExpression(expr)) {
68
- // Handle addon('namespace:funcName') calls
69
- if (ts.isIdentifier(expr.expression) && expr.expression.text === 'addon') {
68
+ // Handle ref('name') calls
69
+ if (ts.isIdentifier(expr.expression) && expr.expression.text === 'ref') {
70
70
  const [firstArg] = expr.arguments;
71
71
  if (firstArg && ts.isStringLiteral(firstArg)) {
72
72
  return firstArg.text;
@@ -206,22 +206,25 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
206
206
  if (propName === 'func') {
207
207
  if (ts.isCallExpression(prop.initializer) &&
208
208
  ts.isIdentifier(prop.initializer.expression) &&
209
- prop.initializer.expression.text === 'addon') {
209
+ prop.initializer.expression.text === 'ref') {
210
210
  const [firstArg] = prop.initializer.arguments;
211
211
  if (!firstArg || !ts.isStringLiteral(firstArg)) {
212
- throw new Error(`addon() call requires a string literal argument in the form "namespace:funcName"`);
212
+ throw new Error(`ref() call requires a string literal argument`);
213
213
  }
214
214
  pikkuFuncId = firstArg.text;
215
- const addonNamespace = pikkuFuncId.split(':')[0];
216
- if (!addonNamespace || !pikkuFuncId.includes(':')) {
217
- throw new Error(`Malformed addon function ID "${pikkuFuncId}": expected "namespace:funcName" format`);
218
- }
219
- if (!inspectorState.rpc.wireAddonDeclarations.has(addonNamespace)) {
220
- throw new Error(`Unknown addon namespace "${addonNamespace}" in "${pikkuFuncId}": no matching wireAddonDeclarations entry found`);
215
+ const addonNamespace = pikkuFuncId.includes(':')
216
+ ? pikkuFuncId.split(':')[0]
217
+ : null;
218
+ if (addonNamespace) {
219
+ if (!inspectorState.rpc.wireAddonDeclarations.has(addonNamespace)) {
220
+ throw new Error(`Unknown addon namespace "${addonNamespace}" in "${pikkuFuncId}": no matching wireAddonDeclarations entry found`);
221
+ }
221
222
  }
222
223
  meta.pikkuFuncId = pikkuFuncId;
223
- meta.packageName =
224
- inspectorState.rpc.wireAddonDeclarations.get(addonNamespace).package;
224
+ if (addonNamespace) {
225
+ meta.packageName =
226
+ inspectorState.rpc.wireAddonDeclarations.get(addonNamespace).package;
227
+ }
225
228
  }
226
229
  else {
227
230
  pikkuFuncId = extractFunctionName(prop.initializer, typeChecker, inspectorState.rootDir).pikkuFuncId;
@@ -40,10 +40,7 @@ export const addFileWithFactory = (node, checker, methods = new Map(), expectedT
40
40
  typePath: typeDeclarationPath,
41
41
  });
42
42
  methods.set(fileName, variables);
43
- // Extract singleton services for CreateWireServices factories
44
- if (expectedTypeName === 'CreateWireServices' &&
45
- state &&
46
- callExpression.arguments.length > 0) {
43
+ if (state && callExpression.arguments.length > 0) {
47
44
  const firstArg = callExpression.arguments[0];
48
45
  let functionNode;
49
46
  if (ts.isArrowFunction(firstArg)) {
@@ -52,10 +49,30 @@ export const addFileWithFactory = (node, checker, methods = new Map(), expectedT
52
49
  else if (ts.isFunctionExpression(firstArg)) {
53
50
  functionNode = firstArg;
54
51
  }
55
- if (functionNode) {
52
+ // Extract singleton services for CreateWireServices factories
53
+ if (expectedTypeName === 'CreateWireServices' && functionNode) {
56
54
  const servicesMeta = extractServicesFromFunction(functionNode);
57
55
  state.wireServicesMeta.set(variableName, servicesMeta.services);
58
56
  }
57
+ // Extract existing services an addon needs from the parent
58
+ // (second parameter of pikkuAddonServices callback)
59
+ if (wrapperFunctionName === 'pikkuAddonServices' &&
60
+ functionNode &&
61
+ functionNode.parameters.length >= 2) {
62
+ const secondParam = functionNode.parameters[1];
63
+ if (secondParam && ts.isObjectBindingPattern(secondParam.name)) {
64
+ for (const elem of secondParam.name.elements) {
65
+ const name = elem.propertyName && ts.isIdentifier(elem.propertyName)
66
+ ? elem.propertyName.text
67
+ : ts.isIdentifier(elem.name)
68
+ ? elem.name.text
69
+ : undefined;
70
+ if (name) {
71
+ state.addonRequiredParentServices.push(name);
72
+ }
73
+ }
74
+ }
75
+ }
59
76
  }
60
77
  return; // Early return since we found a match
61
78
  }
@@ -594,6 +594,8 @@ export const addFunctions = (logger, node, checker, state, options) => {
594
594
  middleware,
595
595
  permissions,
596
596
  isDirectFunction,
597
+ sourceFile: node.getSourceFile().fileName,
598
+ exportedName: exportedName || undefined,
597
599
  };
598
600
  // Populate node metadata if node config is present
599
601
  if (nodeDisplayName && nodeCategory && nodeType) {
@@ -174,6 +174,7 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
174
174
  pikkuFuncId: funcName,
175
175
  ...(packageName && { packageName }),
176
176
  route: fullRoute,
177
+ sourceFile: sourceFile.fileName,
177
178
  method: method,
178
179
  params: params.length > 0 ? params : undefined,
179
180
  query: query.length > 0 ? query : undefined,
@@ -19,8 +19,8 @@ export function addRPCInvocations(node, state, logger) {
19
19
  // Look for call expressions: addon('ext:hello') or rpc.invoke('...')
20
20
  if (ts.isCallExpression(node)) {
21
21
  const { expression, arguments: args } = node;
22
- // Check for addon('namespace:function') calls
23
- if (ts.isIdentifier(expression) && expression.text === 'addon') {
22
+ // Check for ref('name') calls
23
+ if (ts.isIdentifier(expression) && expression.text === 'ref') {
24
24
  const [firstArg] = args;
25
25
  if (firstArg && ts.isStringLiteral(firstArg)) {
26
26
  const functionRef = firstArg.text;
@@ -1,4 +1,9 @@
1
1
  import type { AddWiring } from '../types.js';
2
+ import type { WorkflowStepMeta } from '@pikku/core/workflow';
3
+ /**
4
+ * Recursively collect all RPC names from workflow steps
5
+ */
6
+ export declare function collectInvokedRPCs(steps: WorkflowStepMeta[], rpcs: Set<string>): void;
2
7
  /**
3
8
  * Inspector for pikkuWorkflow() and pikkuSimpleWorkflow() calls
4
9
  * Detects workflow registration and extracts metadata
@@ -43,7 +43,7 @@ function hasInlineSteps(steps) {
43
43
  /**
44
44
  * Recursively collect all RPC names from workflow steps
45
45
  */
46
- function collectInvokedRPCs(steps, rpcs) {
46
+ export function collectInvokedRPCs(steps, rpcs) {
47
47
  for (const step of steps) {
48
48
  if (step.type === 'rpc' && step.rpcName) {
49
49
  rpcs.add(step.rpcName);
@@ -183,6 +183,7 @@ export const addWorkflow = (logger, node, checker, state) => {
183
183
  let description;
184
184
  let errors;
185
185
  let inline;
186
+ let expose;
186
187
  if (ts.isObjectLiteralExpression(firstArg)) {
187
188
  const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger);
188
189
  if (metadata.disabled)
@@ -195,6 +196,7 @@ export const addWorkflow = (logger, node, checker, state) => {
195
196
  if (inlineProp === true) {
196
197
  inline = true;
197
198
  }
199
+ expose = getPropertyValue(firstArg, 'expose');
198
200
  }
199
201
  // Validate that we got a valid function
200
202
  if (ts.isObjectLiteralExpression(firstArg) &&
@@ -278,5 +280,21 @@ export const addWorkflow = (logger, node, checker, state) => {
278
280
  errors,
279
281
  tags,
280
282
  inline,
283
+ expose,
281
284
  };
285
+ // Workflow functions require platform services that aren't visible
286
+ // through parameter destructuring (they're accessed via workflow.do/sleep)
287
+ const funcMeta = state.functions.meta[pikkuFuncId];
288
+ if (funcMeta?.services) {
289
+ for (const svc of [
290
+ 'workflowService',
291
+ 'workflowRunService',
292
+ 'schedulerService',
293
+ 'queueService',
294
+ ]) {
295
+ if (!funcMeta.services.services.includes(svc)) {
296
+ funcMeta.services.services.push(svc);
297
+ }
298
+ }
299
+ }
282
300
  };
package/dist/inspector.js CHANGED
@@ -28,6 +28,7 @@ export function getInitialInspectorState(rootDir) {
28
28
  singletonServicesFactories: new Map(),
29
29
  wireServicesFactories: new Map(),
30
30
  wireServicesMeta: new Map(),
31
+ addonRequiredParentServices: [],
31
32
  configFactories: new Map(),
32
33
  filesAndMethods: {},
33
34
  filesAndMethodsErrors: new Map(),
package/dist/types.d.ts CHANGED
@@ -264,6 +264,7 @@ export interface InspectorState {
264
264
  singletonServicesFactories: PathToNameAndType;
265
265
  wireServicesFactories: PathToNameAndType;
266
266
  wireServicesMeta: Map<string, string[]>;
267
+ addonRequiredParentServices: string[];
267
268
  configFactories: PathToNameAndType;
268
269
  filesAndMethods: InspectorFilesAndMethods;
269
270
  filesAndMethodsErrors: Map<string, PathToNameAndType>;
@@ -139,6 +139,10 @@ export function filterInspectorState(state, filters, logger) {
139
139
  (!filters.httpMethods || filters.httpMethods.length === 0))) {
140
140
  return state;
141
141
  }
142
+ // Snapshot the original workflow graph meta before filtering prunes it
143
+ const originalGraphMeta = {
144
+ ...(state.workflows?.graphMeta ?? {}),
145
+ };
142
146
  // Create a shallow copy with new Maps/Sets to avoid mutating the original
143
147
  const filteredState = {
144
148
  ...state,
@@ -149,11 +153,21 @@ export function filterInspectorState(state, filters, logger) {
149
153
  usedMiddleware: new Set(),
150
154
  usedPermissions: new Set(),
151
155
  },
156
+ functions: {
157
+ ...state.functions,
158
+ meta: JSON.parse(JSON.stringify(state.functions.meta)), // Deep clone to avoid mutating original
159
+ files: new Map(state.functions.files),
160
+ },
152
161
  http: {
153
162
  ...state.http,
154
163
  meta: JSON.parse(JSON.stringify(state.http.meta)), // Deep clone metadata
155
164
  files: new Set(), // Will be repopulated with filtered files
156
165
  },
166
+ workflows: {
167
+ ...state.workflows,
168
+ graphMeta: JSON.parse(JSON.stringify(state.workflows?.graphMeta ?? {})),
169
+ meta: JSON.parse(JSON.stringify(state.workflows?.meta ?? {})),
170
+ },
157
171
  channels: {
158
172
  ...state.channels,
159
173
  meta: JSON.parse(JSON.stringify(state.channels.meta)),
@@ -186,6 +200,14 @@ export function filterInspectorState(state, filters, logger) {
186
200
  agentsMeta: JSON.parse(JSON.stringify(state.agents?.agentsMeta ?? {})),
187
201
  files: new Map(),
188
202
  },
203
+ rpc: {
204
+ ...state.rpc,
205
+ internalMeta: { ...state.rpc.internalMeta }, // Clone to avoid mutating original
206
+ internalFiles: new Map(state.rpc.internalFiles),
207
+ exposedMeta: { ...state.rpc.exposedMeta },
208
+ exposedFiles: new Map(state.rpc.exposedFiles),
209
+ invokedFunctions: new Set(state.rpc.invokedFunctions),
210
+ },
189
211
  cli: {
190
212
  ...state.cli,
191
213
  meta: JSON.parse(JSON.stringify(state.cli.meta)),
@@ -216,16 +238,33 @@ export function filterInspectorState(state, filters, logger) {
216
238
  // Track used functions/middleware/permissions
217
239
  if (routeMeta.pikkuFuncId) {
218
240
  filteredState.serviceAggregation.usedFunctions.add(routeMeta.pikkuFuncId);
241
+ // For workflow/agent routes, also add the base name
242
+ // so the workflow/agent definition survives pruning
243
+ const colonIdx = routeMeta.pikkuFuncId.indexOf(':');
244
+ if (colonIdx !== -1) {
245
+ filteredState.serviceAggregation.usedFunctions.add(routeMeta.pikkuFuncId.slice(colonIdx + 1));
246
+ }
219
247
  }
220
248
  extractWireNames(routeMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
221
249
  extractWireNames(routeMeta.permissions).forEach((name) => filteredState.serviceAggregation.usedPermissions.add(name));
222
250
  }
223
251
  }
224
252
  }
225
- // Repopulate http.files if any routes remain
226
- const hasHttpRoutes = Object.values(filteredState.http.meta).some((routes) => Object.keys(routes).length > 0);
227
- if (hasHttpRoutes) {
228
- filteredState.http.files = new Set(state.http.files);
253
+ // Repopulate http.files with only files that have surviving routes
254
+ for (const method of Object.keys(filteredState.http.meta)) {
255
+ const routes = filteredState.http.meta[method];
256
+ for (const routeMeta of Object.values(routes)) {
257
+ if (routeMeta.sourceFile) {
258
+ filteredState.http.files.add(routeMeta.sourceFile);
259
+ }
260
+ }
261
+ }
262
+ // Fallback: if no sourceFile info available but routes exist, include all files
263
+ if (filteredState.http.files.size === 0) {
264
+ const hasHttpRoutes = Object.values(filteredState.http.meta).some((routes) => Object.keys(routes).length > 0);
265
+ if (hasHttpRoutes) {
266
+ filteredState.http.files = new Set(state.http.files);
267
+ }
229
268
  }
230
269
  // Filter channels
231
270
  for (const name of Object.keys(filteredState.channels.meta)) {
@@ -239,9 +278,30 @@ export function filterInspectorState(state, filters, logger) {
239
278
  delete filteredState.channels.meta[name];
240
279
  }
241
280
  else {
242
- if (channelMeta.pikkuFuncId) {
281
+ // Add all functions referenced by this channel
282
+ if ('pikkuFuncId' in channelMeta && channelMeta.pikkuFuncId) {
243
283
  filteredState.serviceAggregation.usedFunctions.add(channelMeta.pikkuFuncId);
244
284
  }
285
+ if (channelMeta.connect?.pikkuFuncId) {
286
+ filteredState.serviceAggregation.usedFunctions.add(channelMeta.connect.pikkuFuncId);
287
+ }
288
+ if (channelMeta.disconnect?.pikkuFuncId) {
289
+ filteredState.serviceAggregation.usedFunctions.add(channelMeta.disconnect.pikkuFuncId);
290
+ }
291
+ if (channelMeta.message?.pikkuFuncId) {
292
+ filteredState.serviceAggregation.usedFunctions.add(channelMeta.message.pikkuFuncId);
293
+ }
294
+ if (channelMeta.messageWirings) {
295
+ for (const groupKey of Object.keys(channelMeta.messageWirings)) {
296
+ const commands = channelMeta.messageWirings[groupKey];
297
+ for (const cmdKey of Object.keys(commands)) {
298
+ const wiring = commands[cmdKey];
299
+ if (wiring.pikkuFuncId) {
300
+ filteredState.serviceAggregation.usedFunctions.add(wiring.pikkuFuncId);
301
+ }
302
+ }
303
+ }
304
+ }
245
305
  extractWireNames(channelMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
246
306
  extractWireNames(channelMeta.permissions).forEach((name) => filteredState.serviceAggregation.usedPermissions.add(name));
247
307
  }
@@ -307,6 +367,10 @@ export function filterInspectorState(state, filters, logger) {
307
367
  else {
308
368
  if (workerMeta.pikkuFuncId) {
309
369
  filteredState.serviceAggregation.usedFunctions.add(workerMeta.pikkuFuncId);
370
+ const colonIdx = workerMeta.pikkuFuncId.indexOf(':');
371
+ if (colonIdx !== -1) {
372
+ filteredState.serviceAggregation.usedFunctions.add(workerMeta.pikkuFuncId.slice(colonIdx + 1));
373
+ }
310
374
  }
311
375
  extractWireNames(workerMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
312
376
  }
@@ -444,6 +508,30 @@ export function filterInspectorState(state, filters, logger) {
444
508
  if (hasCliPrograms || hasCliRenderers) {
445
509
  filteredState.cli.files = new Set(state.cli.files);
446
510
  }
511
+ // Direct function filtering: functions that match the names/tags/directories
512
+ // filters should be included even if no wiring (HTTP, scheduler, etc.) references them.
513
+ // This ensures standalone RPC-callable functions survive filtering.
514
+ // Only run when function-level filters are active — httpRoutes/httpMethods work
515
+ // through the HTTP wiring pass which already adds the right functions.
516
+ const hasFunctionLevelFilters = (filters.names && filters.names.length > 0) ||
517
+ (filters.tags && filters.tags.length > 0) ||
518
+ (filters.directories && filters.directories.length > 0);
519
+ for (const funcId of Object.keys(filteredState.functions.meta)) {
520
+ if (!hasFunctionLevelFilters)
521
+ break;
522
+ const funcMeta = filteredState.functions.meta[funcId];
523
+ const funcFile = filteredState.functions.files.get(funcId);
524
+ const filePath = funcFile?.path;
525
+ const matches = matchesFilters(filters, {
526
+ type: 'rpc',
527
+ name: funcId,
528
+ tags: funcMeta.tags,
529
+ filePath,
530
+ }, logger);
531
+ if (matches) {
532
+ filteredState.serviceAggregation.usedFunctions.add(funcId);
533
+ }
534
+ }
447
535
  // Post-filter version expansion: include all versions of matched functions
448
536
  const includedBaseNames = new Set();
449
537
  for (const funcId of filteredState.serviceAggregation.usedFunctions) {
@@ -458,6 +546,117 @@ export function filterInspectorState(state, filters, logger) {
458
546
  }
459
547
  }
460
548
  }
549
+ // Prune functions.meta and functions.files to only include used functions
550
+ if (filteredState.serviceAggregation.usedFunctions.size > 0) {
551
+ for (const funcId of Object.keys(filteredState.functions.meta)) {
552
+ if (!filteredState.serviceAggregation.usedFunctions.has(funcId)) {
553
+ delete filteredState.functions.meta[funcId];
554
+ filteredState.functions.files.delete(funcId);
555
+ }
556
+ }
557
+ // Prune channels whose functions were filtered out
558
+ for (const name of Object.keys(filteredState.channels.meta)) {
559
+ const channelMeta = filteredState.channels.meta[name];
560
+ // Check if any of the channel's functions are in the used set
561
+ const channelFuncIds = [];
562
+ if (channelMeta.connect?.pikkuFuncId)
563
+ channelFuncIds.push(channelMeta.connect.pikkuFuncId);
564
+ if (channelMeta.disconnect?.pikkuFuncId)
565
+ channelFuncIds.push(channelMeta.disconnect.pikkuFuncId);
566
+ if (channelMeta.message?.pikkuFuncId)
567
+ channelFuncIds.push(channelMeta.message.pikkuFuncId);
568
+ if (channelMeta.messageWirings) {
569
+ for (const groupKey of Object.keys(channelMeta.messageWirings)) {
570
+ const commands = channelMeta.messageWirings[groupKey];
571
+ for (const cmdKey of Object.keys(commands)) {
572
+ const wiring = commands[cmdKey];
573
+ if (wiring.pikkuFuncId)
574
+ channelFuncIds.push(wiring.pikkuFuncId);
575
+ }
576
+ }
577
+ }
578
+ const hasUsedFunc = channelFuncIds.some((id) => filteredState.serviceAggregation.usedFunctions.has(id));
579
+ if (channelFuncIds.length > 0 && !hasUsedFunc) {
580
+ delete filteredState.channels.meta[name];
581
+ }
582
+ }
583
+ // Prune workflow graphs whose function was filtered out
584
+ const workflowKeys = new Set([
585
+ ...Object.keys(filteredState.workflows.graphMeta),
586
+ ...Object.keys(filteredState.workflows.meta),
587
+ ]);
588
+ for (const name of workflowKeys) {
589
+ const graphMeta = filteredState.workflows.graphMeta[name];
590
+ const workflowMeta = filteredState.workflows.meta[name];
591
+ // Check both graphMeta.pikkuFuncId and meta.pikkuFuncId
592
+ const pikkuFuncId = graphMeta?.pikkuFuncId ?? workflowMeta?.pikkuFuncId;
593
+ if (pikkuFuncId &&
594
+ !filteredState.serviceAggregation.usedFunctions.has(pikkuFuncId)) {
595
+ delete filteredState.workflows.graphMeta[name];
596
+ delete filteredState.workflows.meta[name];
597
+ }
598
+ else if (!pikkuFuncId) {
599
+ // No function ID found — prune it
600
+ delete filteredState.workflows.graphMeta[name];
601
+ delete filteredState.workflows.meta[name];
602
+ }
603
+ }
604
+ // Prune RPC meta to only include entries whose target function survived
605
+ const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta));
606
+ for (const key of Object.keys(filteredState.rpc.internalMeta)) {
607
+ const targetFuncId = filteredState.rpc.internalMeta[key];
608
+ if (!survivingFuncIds.has(targetFuncId) && !survivingFuncIds.has(key)) {
609
+ delete filteredState.rpc.internalMeta[key];
610
+ filteredState.rpc.internalFiles.delete(key);
611
+ }
612
+ }
613
+ for (const key of Object.keys(filteredState.rpc.exposedMeta)) {
614
+ const targetFuncId = filteredState.rpc.exposedMeta[key];
615
+ if (!survivingFuncIds.has(targetFuncId) && !survivingFuncIds.has(key)) {
616
+ delete filteredState.rpc.exposedMeta[key];
617
+ filteredState.rpc.exposedFiles.delete(key);
618
+ }
619
+ }
620
+ // Prune invokedFunctions to match surviving functions
621
+ for (const funcId of filteredState.rpc.invokedFunctions) {
622
+ if (!survivingFuncIds.has(funcId)) {
623
+ filteredState.rpc.invokedFunctions.delete(funcId);
624
+ }
625
+ }
626
+ }
627
+ // Recompute requiredSchemas based on pruned functions.meta
628
+ if (filteredState.serviceAggregation.usedFunctions.size > 0) {
629
+ const prunedSchemas = new Set();
630
+ for (const funcMeta of Object.values(filteredState.functions.meta)) {
631
+ if (funcMeta.inputs?.[0])
632
+ prunedSchemas.add(funcMeta.inputs[0]);
633
+ if (funcMeta.outputs?.[0])
634
+ prunedSchemas.add(funcMeta.outputs[0]);
635
+ }
636
+ filteredState.requiredSchemas = prunedSchemas;
637
+ }
638
+ // If any surviving function is a non-inline workflow step, the unit needs
639
+ // workflowService + queueService even though the function doesn't use them.
640
+ // Check the ORIGINAL graph meta (before filtering pruned it).
641
+ const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta));
642
+ // Use the snapshot taken before filtering
643
+ for (const graph of Object.values(originalGraphMeta)) {
644
+ if (!graph.nodes)
645
+ continue;
646
+ for (const node of Object.values(graph.nodes)) {
647
+ if (!('rpcName' in node) || !node.rpcName)
648
+ continue;
649
+ const rpcName = node.rpcName;
650
+ if (!survivingFuncIds.has(rpcName))
651
+ continue;
652
+ const isInline = node.options?.async !== true &&
653
+ graph.inline === true;
654
+ if (!isInline) {
655
+ filteredState.serviceAggregation.requiredServices.add('workflowService');
656
+ filteredState.serviceAggregation.requiredServices.add('queueService');
657
+ }
658
+ }
659
+ }
461
660
  // Recalculate requiredServices based on filtered functions/middleware/permissions
462
661
  // Need to cast to InspectorState temporarily for aggregateRequiredServices
463
662
  const stateForAggregation = filteredState;
@@ -34,6 +34,53 @@ export async function loadAddonFunctionsMeta(logger, state) {
34
34
  }
35
35
  }
36
36
  }
37
+ // Load addon secrets meta
38
+ try {
39
+ const secretsMetaPath = require.resolve(`${decl.package}/.pikku/secrets/pikku-secrets-meta.gen.json`);
40
+ const secretsRaw = await readFile(secretsMetaPath, 'utf-8');
41
+ const secretsMeta = JSON.parse(secretsRaw);
42
+ for (const [key, def] of Object.entries(secretsMeta)) {
43
+ const existing = state.secrets.definitions.find((d) => d.name === key);
44
+ if (!existing) {
45
+ state.secrets.definitions.push(def);
46
+ logger.debug(`Loaded addon secret '${key}' from ${decl.package}`);
47
+ }
48
+ }
49
+ }
50
+ catch {
51
+ // No secrets meta — that's fine
52
+ }
53
+ // Load addon variables meta
54
+ try {
55
+ const variablesMetaPath = require.resolve(`${decl.package}/.pikku/variables/pikku-variables-meta.gen.json`);
56
+ const variablesRaw = await readFile(variablesMetaPath, 'utf-8');
57
+ const variablesMeta = JSON.parse(variablesRaw);
58
+ for (const [key, def] of Object.entries(variablesMeta)) {
59
+ const existing = state.variables.definitions.find((d) => d.name === key);
60
+ if (!existing) {
61
+ state.variables.definitions.push(def);
62
+ logger.debug(`Loaded addon variable '${key}' from ${decl.package}`);
63
+ }
64
+ }
65
+ }
66
+ catch {
67
+ // No variables meta — that's fine
68
+ }
69
+ // Load addon required parent services from pikku-services.gen
70
+ try {
71
+ const servicesGenPath = require.resolve(`${decl.package}/.pikku/pikku-services.gen.js`);
72
+ const servicesModule = await import(servicesGenPath);
73
+ if (servicesModule.requiredParentServices &&
74
+ Array.isArray(servicesModule.requiredParentServices)) {
75
+ for (const service of servicesModule.requiredParentServices) {
76
+ state.addonRequiredParentServices.push(service);
77
+ }
78
+ logger.debug(`Loaded ${servicesModule.requiredParentServices.length} required parent services for '${namespace}' from ${decl.package}`);
79
+ }
80
+ }
81
+ catch {
82
+ // No services gen — addon may not have requiredParentServices
83
+ }
37
84
  }
38
85
  catch (error) {
39
86
  logger.warn(`Failed to load addon function metadata for '${namespace}' (${decl.package}): ${error.message}`);