@pikku/inspector 0.12.8 → 0.12.9

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.9
4
+
5
+ ### Patch Changes
6
+
7
+ - 2ac6468: Fix workflow inspector crash when workflow.do() data object has a 'description' property
8
+ - fbcf5b9: Add version awareness to RPC handler: versioned functions now appear in the exposed RPC type map (e.g. `getData@v1`, `getData@v2`), enabling type-safe `rpc.invoke('getData@v1', data)` calls. Tree-shaking respects specific version filters without pulling in all versions. HTTP wirings correctly resolve versioned function IDs.
9
+ - Updated dependencies [fbcf5b9]
10
+ - @pikku/core@0.12.16
11
+
3
12
  ## 0.12.8
4
13
 
5
14
  ### Patch Changes
@@ -615,15 +615,14 @@ export const addFunctions = (logger, node, checker, state, options) => {
615
615
  }
616
616
  if (mcpEnabled) {
617
617
  if (!description) {
618
- logger.critical(ErrorCode.MISSING_DESCRIPTION, `MCP tool '${name}' is missing a description.`);
619
- return;
618
+ logger.warn(`MCP tool '${name}' is missing a description.`);
620
619
  }
621
620
  state.mcpEndpoints.files.add(node.getSourceFile().fileName);
622
621
  state.mcpEndpoints.toolsMeta[name] = {
623
622
  pikkuFuncId,
624
623
  name,
625
624
  title: title || undefined,
626
- description,
625
+ description: description || undefined,
627
626
  summary,
628
627
  errors,
629
628
  tags,
@@ -58,6 +58,10 @@ export const addMCPPrompt = (logger, node, checker, state, options) => {
58
58
  }
59
59
  const inputSchema = fnMeta.inputs?.[0] || null;
60
60
  const outputSchema = fnMeta.outputs?.[0] || null;
61
+ if (!fnMeta.outputSchemaName) {
62
+ fnMeta.outputSchemaName = 'MCPPromptResponse';
63
+ fnMeta.outputs = ['MCPPromptResponse'];
64
+ }
61
65
  // --- resolve middleware ---
62
66
  const middleware = resolveMiddleware(state, obj, tags, checker);
63
67
  // --- resolve permissions ---
@@ -67,6 +67,10 @@ export const addMCPResource = (logger, node, checker, state, options) => {
67
67
  }
68
68
  const inputSchema = fnMeta.inputs?.[0] || null;
69
69
  const outputSchema = fnMeta.outputs?.[0] || null;
70
+ if (!fnMeta.outputSchemaName) {
71
+ fnMeta.outputSchemaName = 'MCPResourceResponse';
72
+ fnMeta.outputs = ['MCPResourceResponse'];
73
+ }
70
74
  // --- resolve middleware ---
71
75
  const middleware = resolveMiddleware(state, obj, tags, checker);
72
76
  // --- resolve permissions ---
@@ -88,7 +88,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
88
88
  // workflow.do(stepName, rpcName|fn, data?, options?)
89
89
  const stepNameArg = args[0];
90
90
  const secondArg = args[1];
91
- const optionsArg = args.length >= 3 ? args[args.length - 1] : undefined;
91
+ const optionsArg = args.length >= 4 ? args[args.length - 1] : undefined;
92
92
  const stepName = extractStringLiteral(stepNameArg, checker);
93
93
  const description = extractDescription(optionsArg, checker) ?? undefined;
94
94
  // Determine form by checking 2nd argument type
@@ -6,6 +6,7 @@ export type ExtractedFunctionName = {
6
6
  exportedName: string | null;
7
7
  propertyName: string | null;
8
8
  isHelper: boolean;
9
+ version: number | null;
9
10
  };
10
11
  export declare function makeContextBasedId(wiringType: string, ...segments: string[]): string;
11
12
  export declare function funcIdToTypeName(id: string): string;
@@ -1,5 +1,6 @@
1
1
  import * as ts from 'typescript';
2
2
  import { randomUUID } from 'crypto';
3
+ import { formatVersionedId } from '@pikku/core';
3
4
  export function makeContextBasedId(wiringType, ...segments) {
4
5
  return [wiringType, ...segments].join(':');
5
6
  }
@@ -20,6 +21,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
20
21
  propertyName: null,
21
22
  explicitName: null,
22
23
  isHelper: false,
24
+ version: null,
23
25
  };
24
26
  const workflowHelpers = new Set([
25
27
  'workflow',
@@ -103,16 +105,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
103
105
  // Check for object with 'name' property in first argument
104
106
  const firstArg = args[0];
105
107
  if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
106
- for (const prop of firstArg.properties) {
107
- if (ts.isPropertyAssignment(prop) &&
108
- ts.isIdentifier(prop.name) &&
109
- prop.name.text === 'override' &&
110
- ts.isStringLiteral(prop.initializer)) {
111
- // Priority 1: Object with override property
112
- result.explicitName = prop.initializer.text;
113
- break;
114
- }
115
- }
108
+ extractOverrideAndVersion(firstArg, result);
116
109
  }
117
110
  // Special handling for pikkuSessionlessFunc pattern - use the arrow function directly
118
111
  if (expression.text.startsWith('pikku')) {
@@ -274,19 +267,9 @@ export function extractFunctionName(callExpr, checker, rootDir) {
274
267
  if (ts.isCallExpression(decl.initializer) &&
275
268
  ts.isIdentifier(decl.initializer.expression) &&
276
269
  decl.initializer.expression.text.startsWith('pikku')) {
277
- // Check for object with 'override' property in first argument
278
270
  const firstArg = decl.initializer.arguments[0];
279
271
  if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
280
- for (const prop of firstArg.properties) {
281
- if (ts.isPropertyAssignment(prop) &&
282
- ts.isIdentifier(prop.name) &&
283
- prop.name.text === 'override' &&
284
- ts.isStringLiteral(prop.initializer)) {
285
- // Priority 1: Object with override property
286
- result.explicitName = prop.initializer.text;
287
- break;
288
- }
289
- }
272
+ extractOverrideAndVersion(firstArg, result);
290
273
  }
291
274
  if (decl.initializer.expression.text.startsWith('pikku')) {
292
275
  if (firstArg &&
@@ -340,17 +323,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
340
323
  else if (ts.isCallExpression(callExpr)) {
341
324
  const firstArg = callExpr.arguments[0];
342
325
  if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
343
- for (const prop of firstArg.properties) {
344
- if (ts.isPropertyAssignment(prop) &&
345
- ts.isIdentifier(prop.name) &&
346
- prop.name.text === 'override' &&
347
- ts.isStringLiteral(prop.initializer) &&
348
- !result.explicitName // Only set if not already set
349
- ) {
350
- result.explicitName = prop.initializer.text;
351
- break;
352
- }
353
- }
326
+ extractOverrideAndVersion(firstArg, result);
354
327
  }
355
328
  }
356
329
  // Apply name priority logic
@@ -364,6 +337,9 @@ export function extractFunctionName(callExpr, checker, rootDir) {
364
337
  else {
365
338
  result.pikkuFuncId = `__temp_${randomUUID()}`;
366
339
  }
340
+ if (result.version !== null) {
341
+ result.pikkuFuncId = formatVersionedId(result.pikkuFuncId, result.version);
342
+ }
367
343
  return result;
368
344
  }
369
345
  /**
@@ -420,3 +396,22 @@ export function isNamedExport(declaration, checker) {
420
396
  }
421
397
  return false;
422
398
  }
399
+ function extractOverrideAndVersion(objLiteral, result) {
400
+ for (const prop of objLiteral.properties) {
401
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
402
+ if (prop.name.text === 'override' &&
403
+ ts.isStringLiteral(prop.initializer) &&
404
+ !result.explicitName) {
405
+ result.explicitName = prop.initializer.text;
406
+ }
407
+ else if (prop.name.text === 'version' &&
408
+ ts.isNumericLiteral(prop.initializer) &&
409
+ result.version === null) {
410
+ const parsed = Number(prop.initializer.text);
411
+ if (Number.isInteger(parsed) && parsed >= 1) {
412
+ result.version = parsed;
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
@@ -84,7 +84,12 @@ export function extractDescription(optionsNode, checker) {
84
84
  if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
85
85
  return null;
86
86
  }
87
- return extractPropertyString(optionsNode, 'description', checker);
87
+ try {
88
+ return extractPropertyString(optionsNode, 'description', checker);
89
+ }
90
+ catch {
91
+ return null;
92
+ }
88
93
  }
89
94
  /**
90
95
  * Extract duration value (number or string)
@@ -532,11 +532,15 @@ export function filterInspectorState(state, filters, logger) {
532
532
  filteredState.serviceAggregation.usedFunctions.add(funcId);
533
533
  }
534
534
  }
535
- // Post-filter version expansion: include all versions of matched functions
535
+ // Post-filter version expansion: when an unversioned base name is matched,
536
+ // include all its versions. Specific version matches (e.g. analyzeData@v1)
537
+ // do NOT expand to include other versions.
536
538
  const includedBaseNames = new Set();
537
539
  for (const funcId of filteredState.serviceAggregation.usedFunctions) {
538
- const { baseName } = parseVersionedId(funcId);
539
- includedBaseNames.add(baseName);
540
+ const { baseName, version } = parseVersionedId(funcId);
541
+ if (version === null) {
542
+ includedBaseNames.add(baseName);
543
+ }
540
544
  }
541
545
  if (includedBaseNames.size > 0) {
542
546
  for (const funcId of Object.keys(state.functions.meta)) {
@@ -66,13 +66,43 @@ export function resolveLatestVersions(state, logger) {
66
66
  if (state.rpc.exposedMeta[baseName] === oldId) {
67
67
  state.rpc.exposedMeta[baseName] = newId;
68
68
  }
69
+ updateWiringReferences(state, oldId, newId);
69
70
  }
70
71
  else {
71
72
  const latest = group.explicit.reduce((a, b) => a.version > b.version ? a : b);
72
73
  state.rpc.internalMeta[baseName] = latest.id;
73
74
  }
75
+ if (state.rpc.exposedMeta[baseName]) {
76
+ const latestId = state.rpc.internalMeta[baseName];
77
+ state.rpc.exposedMeta[baseName] = latestId;
78
+ for (const entry of group.explicit) {
79
+ state.rpc.exposedMeta[entry.id] = entry.id;
80
+ const fileEntry = state.rpc.internalFiles.get(entry.id);
81
+ if (fileEntry) {
82
+ state.rpc.exposedFiles.set(entry.id, fileEntry);
83
+ }
84
+ }
85
+ if (group.unversioned) {
86
+ state.rpc.exposedMeta[latestId] = latestId;
87
+ const fileEntry = state.rpc.internalFiles.get(latestId);
88
+ if (fileEntry) {
89
+ state.rpc.exposedFiles.set(latestId, fileEntry);
90
+ }
91
+ }
92
+ }
74
93
  for (const entry of group.explicit) {
75
94
  state.rpc.invokedFunctions.add(entry.id);
76
95
  }
77
96
  }
78
97
  }
98
+ function updateWiringReferences(state, oldId, newId) {
99
+ if (state.http) {
100
+ for (const methods of Object.values(state.http.meta)) {
101
+ for (const meta of Object.values(methods)) {
102
+ if (meta.pikkuFuncId === oldId) {
103
+ meta.pikkuFuncId = newId;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.8",
3
+ "version": "0.12.9",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
- "@pikku/core": "^0.12.15",
38
+ "@pikku/core": "^0.12.16",
39
39
  "path-to-regexp": "^8.3.0",
40
40
  "ts-json-schema-generator": "^2.5.0",
41
41
  "tsx": "^4.21.0",
@@ -817,18 +817,16 @@ export const addFunctions: AddWiring = (
817
817
 
818
818
  if (mcpEnabled) {
819
819
  if (!description) {
820
- logger.critical(
821
- ErrorCode.MISSING_DESCRIPTION,
820
+ logger.warn(
822
821
  `MCP tool '${name}' is missing a description.`
823
822
  )
824
- return
825
823
  }
826
824
  state.mcpEndpoints.files.add(node.getSourceFile().fileName)
827
825
  state.mcpEndpoints.toolsMeta[name] = {
828
826
  pikkuFuncId,
829
827
  name,
830
828
  title: title || undefined,
831
- description,
829
+ description: description || undefined,
832
830
  summary,
833
831
  errors,
834
832
  tags,
@@ -114,6 +114,11 @@ export const addMCPPrompt: AddWiring = (
114
114
  const inputSchema = fnMeta.inputs?.[0] || null
115
115
  const outputSchema = fnMeta.outputs?.[0] || null
116
116
 
117
+ if (!fnMeta.outputSchemaName) {
118
+ fnMeta.outputSchemaName = 'MCPPromptResponse'
119
+ fnMeta.outputs = ['MCPPromptResponse']
120
+ }
121
+
117
122
  // --- resolve middleware ---
118
123
  const middleware = resolveMiddleware(state, obj, tags, checker)
119
124
 
@@ -131,6 +131,11 @@ export const addMCPResource: AddWiring = (
131
131
  const inputSchema = fnMeta.inputs?.[0] || null
132
132
  const outputSchema = fnMeta.outputs?.[0] || null
133
133
 
134
+ if (!fnMeta.outputSchemaName) {
135
+ fnMeta.outputSchemaName = 'MCPResourceResponse'
136
+ fnMeta.outputs = ['MCPResourceResponse']
137
+ }
138
+
134
139
  // --- resolve middleware ---
135
140
  const middleware = resolveMiddleware(state, obj, tags, checker)
136
141
 
@@ -97,7 +97,7 @@ function getWorkflowInvocations(
97
97
  const stepNameArg = args[0]
98
98
  const secondArg = args[1]
99
99
  const optionsArg =
100
- args.length >= 3 ? args[args.length - 1] : undefined
100
+ args.length >= 4 ? args[args.length - 1] : undefined
101
101
 
102
102
  const stepName = extractStringLiteral(stepNameArg, checker)
103
103
  const description =
@@ -1,5 +1,6 @@
1
1
  import * as ts from 'typescript'
2
2
  import { randomUUID } from 'crypto'
3
+ import { formatVersionedId } from '@pikku/core'
3
4
 
4
5
  export type ExtractedFunctionName = {
5
6
  pikkuFuncId: string
@@ -8,6 +9,7 @@ export type ExtractedFunctionName = {
8
9
  exportedName: string | null
9
10
  propertyName: string | null
10
11
  isHelper: boolean
12
+ version: number | null
11
13
  }
12
14
 
13
15
  export function makeContextBasedId(
@@ -40,6 +42,7 @@ export function extractFunctionName(
40
42
  propertyName: null,
41
43
  explicitName: null,
42
44
  isHelper: false,
45
+ version: null,
43
46
  }
44
47
 
45
48
  const workflowHelpers = new Set([
@@ -143,18 +146,7 @@ export function extractFunctionName(
143
146
  // Check for object with 'name' property in first argument
144
147
  const firstArg = args[0]
145
148
  if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
146
- for (const prop of firstArg.properties) {
147
- if (
148
- ts.isPropertyAssignment(prop) &&
149
- ts.isIdentifier(prop.name) &&
150
- prop.name.text === 'override' &&
151
- ts.isStringLiteral(prop.initializer)
152
- ) {
153
- // Priority 1: Object with override property
154
- result.explicitName = prop.initializer.text
155
- break
156
- }
157
- }
149
+ extractOverrideAndVersion(firstArg, result)
158
150
  }
159
151
 
160
152
  // Special handling for pikkuSessionlessFunc pattern - use the arrow function directly
@@ -367,21 +359,9 @@ export function extractFunctionName(
367
359
  ts.isIdentifier(decl.initializer.expression) &&
368
360
  decl.initializer.expression.text.startsWith('pikku')
369
361
  ) {
370
- // Check for object with 'override' property in first argument
371
362
  const firstArg = decl.initializer.arguments[0]
372
363
  if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
373
- for (const prop of firstArg.properties) {
374
- if (
375
- ts.isPropertyAssignment(prop) &&
376
- ts.isIdentifier(prop.name) &&
377
- prop.name.text === 'override' &&
378
- ts.isStringLiteral(prop.initializer)
379
- ) {
380
- // Priority 1: Object with override property
381
- result.explicitName = prop.initializer.text
382
- break
383
- }
384
- }
364
+ extractOverrideAndVersion(firstArg, result)
385
365
  }
386
366
 
387
367
  if (decl.initializer.expression.text.startsWith('pikku')) {
@@ -448,18 +428,7 @@ export function extractFunctionName(
448
428
  else if (ts.isCallExpression(callExpr)) {
449
429
  const firstArg = callExpr.arguments[0]
450
430
  if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
451
- for (const prop of firstArg.properties) {
452
- if (
453
- ts.isPropertyAssignment(prop) &&
454
- ts.isIdentifier(prop.name) &&
455
- prop.name.text === 'override' &&
456
- ts.isStringLiteral(prop.initializer) &&
457
- !result.explicitName // Only set if not already set
458
- ) {
459
- result.explicitName = prop.initializer.text
460
- break
461
- }
462
- }
431
+ extractOverrideAndVersion(firstArg, result)
463
432
  }
464
433
  }
465
434
 
@@ -474,6 +443,10 @@ export function extractFunctionName(
474
443
  result.pikkuFuncId = `__temp_${randomUUID()}`
475
444
  }
476
445
 
446
+ if (result.version !== null) {
447
+ result.pikkuFuncId = formatVersionedId(result.pikkuFuncId, result.version)
448
+ }
449
+
477
450
  return result
478
451
  }
479
452
 
@@ -539,3 +512,29 @@ export function isNamedExport(
539
512
 
540
513
  return false
541
514
  }
515
+
516
+ function extractOverrideAndVersion(
517
+ objLiteral: ts.ObjectLiteralExpression,
518
+ result: ExtractedFunctionName
519
+ ): void {
520
+ for (const prop of objLiteral.properties) {
521
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
522
+ if (
523
+ prop.name.text === 'override' &&
524
+ ts.isStringLiteral(prop.initializer) &&
525
+ !result.explicitName
526
+ ) {
527
+ result.explicitName = prop.initializer.text
528
+ } else if (
529
+ prop.name.text === 'version' &&
530
+ ts.isNumericLiteral(prop.initializer) &&
531
+ result.version === null
532
+ ) {
533
+ const parsed = Number(prop.initializer.text)
534
+ if (Number.isInteger(parsed) && parsed >= 1) {
535
+ result.version = parsed
536
+ }
537
+ }
538
+ }
539
+ }
540
+ }
@@ -0,0 +1,67 @@
1
+ import { test, describe } from 'node:test'
2
+ import { strict as assert } from 'node:assert'
3
+ import * as ts from 'typescript'
4
+ import { extractDescription } from './extract-node-value'
5
+
6
+ const createChecker = (source: string) => {
7
+ const sourceFile = ts.createSourceFile(
8
+ 'test.ts',
9
+ source,
10
+ ts.ScriptTarget.Latest,
11
+ true,
12
+ ts.ScriptKind.TS
13
+ )
14
+ const host = ts.createCompilerHost({})
15
+ const originalGetSourceFile = host.getSourceFile
16
+ host.getSourceFile = (fileName, target) => {
17
+ if (fileName === 'test.ts') return sourceFile
18
+ return originalGetSourceFile.call(host, fileName, target)
19
+ }
20
+ const program = ts.createProgram(['test.ts'], {}, host)
21
+ return { checker: program.getTypeChecker(), sourceFile }
22
+ }
23
+
24
+ const findObjectLiteral = (
25
+ node: ts.Node
26
+ ): ts.ObjectLiteralExpression | undefined => {
27
+ if (ts.isObjectLiteralExpression(node)) return node
28
+ let result: ts.ObjectLiteralExpression | undefined
29
+ ts.forEachChild(node, (child) => {
30
+ if (!result) result = findObjectLiteral(child)
31
+ })
32
+ return result
33
+ }
34
+
35
+ describe('extractDescription', () => {
36
+ test('returns null when node is undefined', () => {
37
+ const { checker } = createChecker('')
38
+ assert.equal(extractDescription(undefined, checker), null)
39
+ })
40
+
41
+ test('extracts string literal description', () => {
42
+ const { checker, sourceFile } = createChecker(
43
+ `const opts = { description: 'my step' }`
44
+ )
45
+ const obj = findObjectLiteral(sourceFile)!
46
+ assert.equal(extractDescription(obj, checker), 'my step')
47
+ })
48
+
49
+ test('returns null for non-literal description value without crashing', () => {
50
+ const { checker, sourceFile } = createChecker(
51
+ `const name = 'test'; const data = { description: name + ' addon' }`
52
+ )
53
+ const objs: ts.ObjectLiteralExpression[] = []
54
+ const visit = (node: ts.Node) => {
55
+ if (ts.isObjectLiteralExpression(node)) objs.push(node)
56
+ ts.forEachChild(node, visit)
57
+ }
58
+ ts.forEachChild(sourceFile, visit)
59
+ const dataObj = objs[objs.length - 1]!
60
+ assert.equal(extractDescription(dataObj, checker), null)
61
+ })
62
+
63
+ test('returns null for non-object node', () => {
64
+ const { checker, sourceFile } = createChecker(`const x = 42`)
65
+ assert.equal(extractDescription(sourceFile, checker), null)
66
+ })
67
+ })
@@ -110,7 +110,11 @@ export function extractDescription(
110
110
  if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
111
111
  return null
112
112
  }
113
- return extractPropertyString(optionsNode, 'description', checker)
113
+ try {
114
+ return extractPropertyString(optionsNode, 'description', checker)
115
+ } catch {
116
+ return null
117
+ }
114
118
  }
115
119
 
116
120
  /**
@@ -730,11 +730,15 @@ export function filterInspectorState(
730
730
  }
731
731
  }
732
732
 
733
- // Post-filter version expansion: include all versions of matched functions
733
+ // Post-filter version expansion: when an unversioned base name is matched,
734
+ // include all its versions. Specific version matches (e.g. analyzeData@v1)
735
+ // do NOT expand to include other versions.
734
736
  const includedBaseNames = new Set<string>()
735
737
  for (const funcId of filteredState.serviceAggregation.usedFunctions) {
736
- const { baseName } = parseVersionedId(funcId)
737
- includedBaseNames.add(baseName)
738
+ const { baseName, version } = parseVersionedId(funcId)
739
+ if (version === null) {
740
+ includedBaseNames.add(baseName)
741
+ }
738
742
  }
739
743
  if (includedBaseNames.size > 0) {
740
744
  for (const funcId of Object.keys(state.functions.meta)) {