@pikku/inspector 0.7.7 → 0.8.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.7.7",
3
+ "version": "0.8.1",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "test:coverage": "bash run-tests.sh --coverage"
18
18
  },
19
19
  "dependencies": {
20
- "@pikku/core": "^0.7.8",
20
+ "@pikku/core": "^0.8.1",
21
21
  "path-to-regexp": "^8.2.0",
22
22
  "typescript": "^5.6"
23
23
  },
package/run-tests.sh CHANGED
File without changes
@@ -1,14 +1,18 @@
1
1
  import * as ts from 'typescript'
2
2
  import { getPropertyValue } from './get-property-value.js'
3
3
  import { pathToRegexp } from 'path-to-regexp'
4
- import { APIDocs } from '@pikku/core'
4
+ import { APIDocs, PikkuEventTypes } from '@pikku/core'
5
5
  import {
6
6
  extractFunctionName,
7
7
  getPropertyAssignmentInitializer,
8
8
  matchesFilters,
9
9
  } from './utils.js'
10
10
  import type { ChannelMessageMeta, ChannelMeta } from '@pikku/core/channel'
11
- import type { InspectorFilters, InspectorState } from './types.js'
11
+ import type {
12
+ InspectorFilters,
13
+ InspectorState,
14
+ InspectorLogger,
15
+ } from './types.js'
12
16
 
13
17
  /**
14
18
  * Safely get the “initializer” expression of a property-like AST node:
@@ -363,7 +367,8 @@ export function addChannel(
363
367
  node: ts.Node,
364
368
  checker: ts.TypeChecker,
365
369
  state: InspectorState,
366
- filters: InspectorFilters
370
+ filters: InspectorFilters,
371
+ logger: InspectorLogger
367
372
  ) {
368
373
  if (!ts.isCallExpression(node)) return
369
374
  const { expression, arguments: args } = node
@@ -391,7 +396,17 @@ export function addChannel(
391
396
  const tags = getPropertyValue(obj, 'tags') as string[] | undefined
392
397
  const query = getPropertyValue(obj, 'query') as string[] | []
393
398
 
394
- if (!matchesFilters(filters, { tags }, { type: 'channel', name })) return
399
+ const filePath = node.getSourceFile().fileName
400
+
401
+ if (
402
+ !matchesFilters(
403
+ filters,
404
+ { tags },
405
+ { type: PikkuEventTypes.channel, name, filePath },
406
+ logger
407
+ )
408
+ )
409
+ return
395
410
 
396
411
  const connect = getPropertyAssignmentInitializer(
397
412
  obj,
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript'
2
- import { InspectorState, InspectorFilters } from './types.js'
2
+ import { InspectorState, InspectorFilters, InspectorLogger } from './types.js'
3
3
  import { TypesMap } from './types-map.js'
4
4
  import {
5
5
  extractFunctionName,
@@ -231,7 +231,8 @@ export function addFunctions(
231
231
  node: ts.Node,
232
232
  checker: ts.TypeChecker,
233
233
  state: InspectorState,
234
- filters: InspectorFilters
234
+ filters: InspectorFilters,
235
+ logger: InspectorLogger
235
236
  ) {
236
237
  if (!ts.isCallExpression(node)) return
237
238
 
@@ -305,10 +306,7 @@ export function addFunctions(
305
306
  services.services.push(original)
306
307
  }
307
308
  }
308
- } else if (
309
- ts.isIdentifier(firstParam.name) &&
310
- !firstParam.name.text.startsWith('_')
311
- ) {
309
+ } else if (ts.isIdentifier(firstParam.name)) {
312
310
  services.optimized = false
313
311
  }
314
312
  }
@@ -2,13 +2,13 @@ import * as ts from 'typescript'
2
2
  import { getPropertyValue } from './get-property-value.js'
3
3
  import { pathToRegexp } from 'path-to-regexp'
4
4
  import { HTTPMethod } from '@pikku/core/http'
5
- import { APIDocs } from '@pikku/core'
5
+ import { APIDocs, PikkuEventTypes } from '@pikku/core'
6
6
  import {
7
7
  extractFunctionName,
8
8
  getPropertyAssignmentInitializer,
9
9
  matchesFilters,
10
10
  } from './utils.js'
11
- import { InspectorState, InspectorFilters } from './types.js'
11
+ import { InspectorState, InspectorFilters, InspectorLogger } from './types.js'
12
12
 
13
13
  /**
14
14
  * Populate metaInputTypes for a given route based on method, input type,
@@ -43,7 +43,8 @@ export const addHTTPRoute = (
43
43
  node: ts.Node,
44
44
  checker: ts.TypeChecker,
45
45
  state: InspectorState,
46
- filters: InspectorFilters
46
+ filters: InspectorFilters,
47
+ logger: InspectorLogger
47
48
  ) => {
48
49
  // only look at calls
49
50
  if (!ts.isCallExpression(node)) return
@@ -69,7 +70,16 @@ export const addHTTPRoute = (
69
70
  const tags = (getPropertyValue(obj, 'tags') as string[]) || undefined
70
71
  const query = (getPropertyValue(obj, 'query') as string[]) || []
71
72
 
72
- if (!matchesFilters(filters, { tags }, { type: 'http', name: route })) {
73
+ const filePath = node.getSourceFile().fileName
74
+
75
+ if (
76
+ !matchesFilters(
77
+ filters,
78
+ { tags },
79
+ { type: PikkuEventTypes.http, name: route, filePath },
80
+ logger
81
+ )
82
+ ) {
73
83
  return
74
84
  }
75
85
 
@@ -0,0 +1,104 @@
1
+ import * as ts from 'typescript'
2
+ import { getPropertyValue } from './get-property-value.js'
3
+ import { PikkuEventTypes } from '@pikku/core'
4
+ import { InspectorFilters, InspectorState, InspectorLogger } from './types.js'
5
+ import {
6
+ extractFunctionName,
7
+ getPropertyAssignmentInitializer,
8
+ matchesFilters,
9
+ } from './utils.js'
10
+
11
+ export const addMCPPrompt = (
12
+ node: ts.Node,
13
+ checker: ts.TypeChecker,
14
+ state: InspectorState,
15
+ filters: InspectorFilters,
16
+ logger: InspectorLogger
17
+ ) => {
18
+ if (!ts.isCallExpression(node)) {
19
+ return
20
+ }
21
+
22
+ const args = node.arguments
23
+ const firstArg = args[0]
24
+ const expression = node.expression
25
+
26
+ // Check if the call is to addMCPPrompt
27
+ if (!ts.isIdentifier(expression) || expression.text !== 'addMCPPrompt') {
28
+ return
29
+ }
30
+
31
+ if (!firstArg) {
32
+ return
33
+ }
34
+
35
+ if (ts.isObjectLiteralExpression(firstArg)) {
36
+ const obj = firstArg
37
+
38
+ const nameValue = getPropertyValue(obj, 'name') as string | null
39
+ const descriptionValue = getPropertyValue(obj, 'description') as
40
+ | string
41
+ | null
42
+ const tags = (getPropertyValue(obj, 'tags') as string[]) || undefined
43
+
44
+ const funcInitializer = getPropertyAssignmentInitializer(
45
+ obj,
46
+ 'func',
47
+ true,
48
+ checker
49
+ )
50
+ if (!funcInitializer) {
51
+ console.error(`• No valid 'func' property for MCP prompt '${nameValue}'.`)
52
+ return
53
+ }
54
+
55
+ const pikkuFuncName = extractFunctionName(
56
+ funcInitializer,
57
+ checker
58
+ ).pikkuFuncName
59
+
60
+ if (!nameValue) {
61
+ console.error(`• MCP prompt is missing the required 'name' property.`)
62
+ return
63
+ }
64
+
65
+ if (!descriptionValue) {
66
+ console.error(`• MCP prompt '${nameValue}' is missing a description.`)
67
+ return
68
+ }
69
+
70
+ const filePath = node.getSourceFile().fileName
71
+
72
+ if (
73
+ !matchesFilters(
74
+ filters,
75
+ { tags },
76
+ { type: PikkuEventTypes.mcp, name: nameValue, filePath },
77
+ logger
78
+ )
79
+ ) {
80
+ return
81
+ }
82
+
83
+ // lookup existing function metadata
84
+ const fnMeta = state.functions.meta[pikkuFuncName]
85
+ if (!fnMeta) {
86
+ console.error(`• No function metadata found for '${pikkuFuncName}'.`)
87
+ return
88
+ }
89
+ const inputSchema = fnMeta.inputs?.[0] || null
90
+ const outputSchema = fnMeta.outputs?.[0] || null
91
+
92
+ state.mcpEndpoints.files.add(node.getSourceFile().fileName)
93
+
94
+ state.mcpEndpoints.promptsMeta[nameValue] = {
95
+ pikkuFuncName,
96
+ name: nameValue,
97
+ description: descriptionValue,
98
+ tags,
99
+ inputSchema,
100
+ outputSchema,
101
+ arguments: [], // Will be populated by CLI during serialization
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,116 @@
1
+ import * as ts from 'typescript'
2
+ import { getPropertyValue } from './get-property-value.js'
3
+ import { PikkuEventTypes } from '@pikku/core'
4
+ import { InspectorFilters, InspectorState, InspectorLogger } from './types.js'
5
+ import {
6
+ extractFunctionName,
7
+ getPropertyAssignmentInitializer,
8
+ matchesFilters,
9
+ } from './utils.js'
10
+
11
+ export const addMCPResource = (
12
+ node: ts.Node,
13
+ checker: ts.TypeChecker,
14
+ state: InspectorState,
15
+ filters: InspectorFilters,
16
+ logger: InspectorLogger
17
+ ) => {
18
+ if (!ts.isCallExpression(node)) {
19
+ return
20
+ }
21
+
22
+ const args = node.arguments
23
+ const firstArg = args[0]
24
+ const expression = node.expression
25
+
26
+ // Check if the call is to addMCPResource
27
+ if (!ts.isIdentifier(expression) || expression.text !== 'addMCPResource') {
28
+ return
29
+ }
30
+
31
+ if (!firstArg) {
32
+ return
33
+ }
34
+
35
+ if (ts.isObjectLiteralExpression(firstArg)) {
36
+ const obj = firstArg
37
+
38
+ const uriValue = getPropertyValue(obj, 'uri') as string | null
39
+ const titleValue = getPropertyValue(obj, 'title') as string | null
40
+ const descriptionValue = getPropertyValue(obj, 'description') as
41
+ | string
42
+ | null
43
+ const streamingValue = getPropertyValue(obj, 'streaming') as boolean | null
44
+ const tags = (getPropertyValue(obj, 'tags') as string[]) || undefined
45
+
46
+ const funcInitializer = getPropertyAssignmentInitializer(
47
+ obj,
48
+ 'func',
49
+ true,
50
+ checker
51
+ )
52
+ if (!funcInitializer) {
53
+ console.error(
54
+ `• No valid 'func' property for MCP resource '${uriValue}'.`
55
+ )
56
+ return
57
+ }
58
+
59
+ const pikkuFuncName = extractFunctionName(
60
+ funcInitializer,
61
+ checker
62
+ ).pikkuFuncName
63
+
64
+ if (!uriValue) {
65
+ console.error(`• MCP resource is missing the required 'uri' property.`)
66
+ return
67
+ }
68
+
69
+ if (!titleValue) {
70
+ console.error(
71
+ `• MCP resource '${uriValue}' is missing the required 'title' property.`
72
+ )
73
+ return
74
+ }
75
+
76
+ if (!descriptionValue) {
77
+ console.error(`• MCP resource '${uriValue}' is missing a description.`)
78
+ return
79
+ }
80
+
81
+ const filePath = node.getSourceFile().fileName
82
+
83
+ if (
84
+ !matchesFilters(
85
+ filters,
86
+ { tags },
87
+ { type: PikkuEventTypes.mcp, name: uriValue, filePath },
88
+ logger
89
+ )
90
+ ) {
91
+ return
92
+ }
93
+
94
+ // lookup existing function metadata
95
+ const fnMeta = state.functions.meta[pikkuFuncName]
96
+ if (!fnMeta) {
97
+ console.error(`• No function metadata found for '${pikkuFuncName}'.`)
98
+ return
99
+ }
100
+ const inputSchema = fnMeta.inputs?.[0] || null
101
+ const outputSchema = fnMeta.outputs?.[0] || null
102
+
103
+ state.mcpEndpoints.files.add(node.getSourceFile().fileName)
104
+
105
+ state.mcpEndpoints.resourcesMeta[uriValue] = {
106
+ pikkuFuncName,
107
+ uri: uriValue,
108
+ title: titleValue,
109
+ description: descriptionValue,
110
+ ...(streamingValue !== null && { streaming: streamingValue }),
111
+ tags,
112
+ inputSchema,
113
+ outputSchema,
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,107 @@
1
+ import * as ts from 'typescript'
2
+ import { getPropertyValue } from './get-property-value.js'
3
+ import { PikkuEventTypes } from '@pikku/core'
4
+ import { InspectorFilters, InspectorState, InspectorLogger } from './types.js'
5
+ import {
6
+ extractFunctionName,
7
+ getPropertyAssignmentInitializer,
8
+ matchesFilters,
9
+ } from './utils.js'
10
+
11
+ export const addMCPTool = (
12
+ node: ts.Node,
13
+ checker: ts.TypeChecker,
14
+ state: InspectorState,
15
+ filters: InspectorFilters,
16
+ logger: InspectorLogger
17
+ ) => {
18
+ if (!ts.isCallExpression(node)) {
19
+ return
20
+ }
21
+
22
+ const args = node.arguments
23
+ const firstArg = args[0]
24
+ const expression = node.expression
25
+
26
+ // Check if the call is to addMCPTool
27
+ if (!ts.isIdentifier(expression) || expression.text !== 'addMCPTool') {
28
+ return
29
+ }
30
+
31
+ if (!firstArg) {
32
+ return
33
+ }
34
+
35
+ if (ts.isObjectLiteralExpression(firstArg)) {
36
+ const obj = firstArg
37
+
38
+ const nameValue = getPropertyValue(obj, 'name') as string | null
39
+ const titleValue = getPropertyValue(obj, 'title') as string | null
40
+ const descriptionValue = getPropertyValue(obj, 'description') as
41
+ | string
42
+ | null
43
+ const streamingValue = getPropertyValue(obj, 'streaming') as boolean | null
44
+ const tags = (getPropertyValue(obj, 'tags') as string[]) || undefined
45
+
46
+ const funcInitializer = getPropertyAssignmentInitializer(
47
+ obj,
48
+ 'func',
49
+ true,
50
+ checker
51
+ )
52
+ if (!funcInitializer) {
53
+ console.error(`• No valid 'func' property for MCP tool '${nameValue}'.`)
54
+ return
55
+ }
56
+
57
+ const pikkuFuncName = extractFunctionName(
58
+ funcInitializer,
59
+ checker
60
+ ).pikkuFuncName
61
+
62
+ if (!nameValue) {
63
+ console.error(`• MCP tool is missing the required 'name' property.`)
64
+ return
65
+ }
66
+
67
+ if (!descriptionValue) {
68
+ console.error(`• MCP tool '${nameValue}' is missing a description.`)
69
+ return
70
+ }
71
+
72
+ const filePath = node.getSourceFile().fileName
73
+
74
+ if (
75
+ !matchesFilters(
76
+ filters,
77
+ { tags },
78
+ { type: PikkuEventTypes.mcp, name: nameValue, filePath },
79
+ logger
80
+ )
81
+ ) {
82
+ return
83
+ }
84
+
85
+ // lookup existing function metadata
86
+ const fnMeta = state.functions.meta[pikkuFuncName]
87
+ if (!fnMeta) {
88
+ console.error(`• No function metadata found for '${pikkuFuncName}'.`)
89
+ return
90
+ }
91
+ const inputSchema = fnMeta.inputs?.[0] || null
92
+ const outputSchema = fnMeta.outputs?.[0] || null
93
+
94
+ state.mcpEndpoints.files.add(node.getSourceFile().fileName)
95
+
96
+ state.mcpEndpoints.toolsMeta[nameValue] = {
97
+ pikkuFuncName,
98
+ name: nameValue,
99
+ title: titleValue || undefined,
100
+ description: descriptionValue,
101
+ ...(streamingValue !== null && { streaming: streamingValue }),
102
+ tags,
103
+ inputSchema,
104
+ outputSchema,
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,92 @@
1
+ import * as ts from 'typescript'
2
+ import { getPropertyValue } from './get-property-value.js'
3
+ import { APIDocs, PikkuEventTypes } from '@pikku/core'
4
+ import { InspectorFilters, InspectorState, InspectorLogger } from './types.js'
5
+ import {
6
+ extractFunctionName,
7
+ getPropertyAssignmentInitializer,
8
+ matchesFilters,
9
+ } from './utils.js'
10
+
11
+ export const addQueueWorker = (
12
+ node: ts.Node,
13
+ checker: ts.TypeChecker,
14
+ state: InspectorState,
15
+ filters: InspectorFilters,
16
+ logger: InspectorLogger
17
+ ) => {
18
+ if (!ts.isCallExpression(node)) {
19
+ return
20
+ }
21
+
22
+ const args = node.arguments
23
+ const firstArg = args[0]
24
+ const expression = node.expression
25
+
26
+ // Check if the call is to addQueueWorker
27
+ if (!ts.isIdentifier(expression) || expression.text !== 'addQueueWorker') {
28
+ return
29
+ }
30
+
31
+ if (!firstArg) {
32
+ return
33
+ }
34
+
35
+ if (ts.isObjectLiteralExpression(firstArg)) {
36
+ const obj = firstArg
37
+
38
+ const queueName = getPropertyValue(obj, 'queueName') as string | null
39
+ const docs = (getPropertyValue(obj, 'docs') as APIDocs) || undefined
40
+ const tags = (getPropertyValue(obj, 'tags') as string[]) || undefined
41
+
42
+ // --- find the referenced function ---
43
+ const funcInitializer = getPropertyAssignmentInitializer(
44
+ obj,
45
+ 'func',
46
+ true,
47
+ checker
48
+ )
49
+ if (!funcInitializer) {
50
+ console.error(
51
+ `• No valid 'func' property for queue processor '${queueName}'.`
52
+ )
53
+ return
54
+ }
55
+
56
+ const pikkuFuncName = extractFunctionName(
57
+ funcInitializer,
58
+ checker
59
+ ).pikkuFuncName
60
+
61
+ if (!queueName) {
62
+ console.error(
63
+ `• No 'queueName' provided for queue processor function '${pikkuFuncName}'.`
64
+ )
65
+ return
66
+ }
67
+
68
+ const filePath = node.getSourceFile().fileName
69
+
70
+ if (
71
+ !matchesFilters(
72
+ filters,
73
+ { tags },
74
+ { type: PikkuEventTypes.queue, name: queueName, filePath },
75
+ logger
76
+ )
77
+ ) {
78
+ console.info(
79
+ `• Skipping queue processor '${pikkuFuncName}' for queue '${queueName}' due to filter mismatch.`
80
+ )
81
+ return
82
+ }
83
+
84
+ state.queueWorkers.files.add(node.getSourceFile().fileName)
85
+ state.queueWorkers.meta[queueName] = {
86
+ pikkuFuncName,
87
+ queueName,
88
+ docs,
89
+ tags,
90
+ }
91
+ }
92
+ }
@@ -1,14 +1,19 @@
1
1
  import * as ts from 'typescript'
2
2
  import { getPropertyValue } from './get-property-value.js'
3
- import { APIDocs } from '@pikku/core'
4
- import { InspectorFilters, InspectorState } from './types.js'
5
- import { extractFunctionName, matchesFilters } from './utils.js'
3
+ import { APIDocs, PikkuEventTypes } from '@pikku/core'
4
+ import { InspectorFilters, InspectorState, InspectorLogger } from './types.js'
5
+ import {
6
+ extractFunctionName,
7
+ getPropertyAssignmentInitializer,
8
+ matchesFilters,
9
+ } from './utils.js'
6
10
 
7
11
  export const addSchedule = (
8
12
  node: ts.Node,
9
13
  checker: ts.TypeChecker,
10
14
  state: InspectorState,
11
- filters: InspectorFilters
15
+ filters: InspectorFilters,
16
+ logger: InspectorLogger
12
17
  ) => {
13
18
  if (!ts.isCallExpression(node)) {
14
19
  return
@@ -35,22 +40,21 @@ export const addSchedule = (
35
40
  const docs = (getPropertyValue(obj, 'docs') as APIDocs) || undefined
36
41
  const tags = (getPropertyValue(obj, 'tags') as string[]) || undefined
37
42
 
38
- // --- find the referenced function ---
39
- const funcProp = obj.properties.find(
40
- (p) =>
41
- ts.isPropertyAssignment(p) &&
42
- ts.isIdentifier(p.name) &&
43
- p.name.text === 'func'
44
- ) as ts.PropertyAssignment | undefined
45
-
46
- if (!funcProp || !ts.isIdentifier(funcProp.initializer)) {
43
+ const funcInitializer = getPropertyAssignmentInitializer(
44
+ obj,
45
+ 'func',
46
+ true,
47
+ checker
48
+ )
49
+ if (!funcInitializer) {
47
50
  console.error(
48
51
  `• No valid 'func' property for scheduled task '${nameValue}'.`
49
52
  )
50
53
  return
51
54
  }
55
+
52
56
  const pikkuFuncName = extractFunctionName(
53
- funcProp.initializer,
57
+ funcInitializer,
54
58
  checker
55
59
  ).pikkuFuncName
56
60
 
@@ -58,8 +62,15 @@ export const addSchedule = (
58
62
  return
59
63
  }
60
64
 
65
+ const filePath = node.getSourceFile().fileName
66
+
61
67
  if (
62
- !matchesFilters(filters, { tags }, { type: 'schedule', name: nameValue })
68
+ !matchesFilters(
69
+ filters,
70
+ { tags },
71
+ { type: PikkuEventTypes.scheduler, name: nameValue, filePath },
72
+ logger
73
+ )
63
74
  ) {
64
75
  return
65
76
  }
package/src/inspector.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  InspectorState,
6
6
  InspectorHTTPState,
7
7
  InspectorFilters,
8
+ InspectorLogger,
8
9
  } from './types.js'
9
10
 
10
11
  export const normalizeHTTPTypes = (
@@ -14,6 +15,7 @@ export const normalizeHTTPTypes = (
14
15
  }
15
16
 
16
17
  export const inspect = (
18
+ logger: InspectorLogger,
17
19
  routeFiles: string[],
18
20
  filters: InspectorFilters
19
21
  ): InspectorState => {
@@ -49,22 +51,32 @@ export const inspect = (
49
51
  meta: {},
50
52
  files: new Set(),
51
53
  },
54
+ queueWorkers: {
55
+ meta: {},
56
+ files: new Set(),
57
+ },
52
58
  rpc: {
53
59
  meta: {},
54
60
  },
61
+ mcpEndpoints: {
62
+ resourcesMeta: {},
63
+ toolsMeta: {},
64
+ promptsMeta: {},
65
+ files: new Set(),
66
+ },
55
67
  }
56
68
 
57
69
  // First sweep: add all functions
58
70
  for (const sourceFile of sourceFiles) {
59
71
  ts.forEachChild(sourceFile, (child) =>
60
- visitSetup(checker, child, state, filters)
72
+ visitSetup(checker, child, state, filters, logger)
61
73
  )
62
74
  }
63
75
 
64
76
  // Second sweep: add all transports
65
77
  for (const sourceFile of sourceFiles) {
66
78
  ts.forEachChild(sourceFile, (child) =>
67
- visitRoutes(checker, child, state, filters)
79
+ visitRoutes(checker, child, state, filters, logger)
68
80
  )
69
81
  }
70
82