@pikku/inspector 0.11.0 → 0.11.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/add/add-channel.js +11 -10
  3. package/dist/add/add-file-with-factory.js +10 -10
  4. package/dist/add/add-functions.js +57 -43
  5. package/dist/add/add-http-route.js +5 -4
  6. package/dist/add/add-mcp-prompt.js +6 -5
  7. package/dist/add/add-mcp-resource.js +6 -5
  8. package/dist/add/add-mcp-tool.js +6 -5
  9. package/dist/add/add-middleware.js +1 -1
  10. package/dist/add/add-permission.js +1 -1
  11. package/dist/add/add-queue-worker.js +6 -5
  12. package/dist/add/add-schedule.js +5 -4
  13. package/dist/add/add-workflow.d.ts +1 -1
  14. package/dist/add/add-workflow.js +92 -66
  15. package/dist/error-codes.d.ts +1 -0
  16. package/dist/error-codes.js +1 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +1 -0
  19. package/dist/inspector.js +10 -6
  20. package/dist/types.d.ts +21 -8
  21. package/dist/utils/extract-function-node.d.ts +10 -0
  22. package/dist/utils/extract-function-node.js +38 -0
  23. package/dist/utils/extract-node-value.d.ts +8 -0
  24. package/dist/utils/extract-node-value.js +24 -0
  25. package/dist/utils/extract-service-metadata.d.ts +19 -0
  26. package/dist/utils/extract-service-metadata.js +244 -0
  27. package/dist/utils/get-files-and-methods.d.ts +3 -3
  28. package/dist/utils/get-files-and-methods.js +3 -3
  29. package/dist/utils/get-property-value.d.ts +13 -6
  30. package/dist/utils/get-property-value.js +51 -43
  31. package/dist/utils/post-process.d.ts +9 -0
  32. package/dist/utils/post-process.js +30 -3
  33. package/dist/utils/serialize-inspector-state.d.ts +18 -5
  34. package/dist/utils/serialize-inspector-state.js +12 -10
  35. package/dist/utils/write-service-metadata.d.ts +13 -0
  36. package/dist/utils/write-service-metadata.js +37 -0
  37. package/dist/visit.js +2 -2
  38. package/dist/workflow/extract-simple-workflow.d.ts +15 -0
  39. package/dist/workflow/extract-simple-workflow.js +803 -0
  40. package/dist/workflow/patterns.d.ts +39 -0
  41. package/dist/workflow/patterns.js +138 -0
  42. package/dist/workflow/validation.d.ts +28 -0
  43. package/dist/workflow/validation.js +124 -0
  44. package/package.json +4 -4
  45. package/src/add/add-channel.ts +37 -17
  46. package/src/add/add-file-with-factory.ts +10 -10
  47. package/src/add/add-functions.ts +72 -56
  48. package/src/add/add-http-route.ts +10 -5
  49. package/src/add/add-mcp-prompt.ts +11 -7
  50. package/src/add/add-mcp-resource.ts +11 -7
  51. package/src/add/add-mcp-tool.ts +11 -7
  52. package/src/add/add-middleware.ts +1 -1
  53. package/src/add/add-permission.ts +1 -1
  54. package/src/add/add-queue-worker.ts +11 -12
  55. package/src/add/add-schedule.ts +10 -5
  56. package/src/add/add-workflow.ts +120 -110
  57. package/src/error-codes.ts +1 -0
  58. package/src/index.ts +2 -0
  59. package/src/inspector.ts +16 -6
  60. package/src/types.ts +18 -8
  61. package/src/utils/extract-function-node.ts +58 -0
  62. package/src/utils/extract-node-value.ts +31 -0
  63. package/src/utils/extract-service-metadata.ts +353 -0
  64. package/src/utils/filter-inspector-state.test.ts +3 -3
  65. package/src/utils/filter-utils.test.ts +45 -51
  66. package/src/utils/get-files-and-methods.ts +11 -11
  67. package/src/utils/get-property-value.ts +60 -53
  68. package/src/utils/permissions.test.ts +3 -3
  69. package/src/utils/post-process.ts +56 -3
  70. package/src/utils/serialize-inspector-state.ts +28 -18
  71. package/src/utils/test-data/inspector-state.json +9 -9
  72. package/src/utils/write-service-metadata.ts +51 -0
  73. package/src/visit.ts +3 -3
  74. package/src/workflow/extract-simple-workflow.ts +1035 -0
  75. package/src/workflow/patterns.ts +182 -0
  76. package/src/workflow/validation.ts +153 -0
  77. package/tsconfig.tsbuildinfo +1 -1
@@ -1,29 +1,21 @@
1
1
  import * as ts from 'typescript'
2
- import {
3
- getPropertyValue,
4
- getPropertyTags,
5
- } from '../utils/get-property-value.js'
6
- import { PikkuDocs } from '@pikku/core'
7
2
  import { AddWiring, InspectorState } from '../types.js'
8
3
  import { extractFunctionName } from '../utils/extract-function-name.js'
9
- import {
10
- getPropertyAssignmentInitializer,
11
- resolveFunctionDeclaration,
12
- } from '../utils/type-utils.js'
13
- import { resolveMiddleware } from '../utils/middleware.js'
14
- import { extractWireNames } from '../utils/post-process.js'
4
+ import { extractFunctionNode } from '../utils/extract-function-node.js'
15
5
  import { ErrorCode } from '../error-codes.js'
16
6
  import { WorkflowStepMeta } from '@pikku/core/workflow'
17
7
  import {
18
8
  extractStringLiteral,
19
- extractNumberLiteral,
20
- extractPropertyString,
21
9
  isStringLike,
22
10
  isFunctionLike,
11
+ extractDescription,
12
+ extractDuration,
23
13
  } from '../utils/extract-node-value.js'
14
+ import { extractSimpleWorkflow } from '../workflow/extract-simple-workflow.js'
15
+ import { getCommonWireMetaData } from '../utils/get-property-value.js'
24
16
 
25
17
  /**
26
- * Scan for workflow.do() and workflow.sleep() calls to extract workflow steps
18
+ * Scan for workflow.do(), workflow.sleep(), and workflow.cancel() calls to extract workflow steps
27
19
  */
28
20
  function getWorkflowInvocations(
29
21
  node: ts.Node,
@@ -37,7 +29,7 @@ function getWorkflowInvocations(
37
29
  const { name } = node
38
30
 
39
31
  // Check if this is accessing 'do' or 'sleep' property
40
- if (name.text === 'do' || name.text === 'sleep') {
32
+ if (name.text === 'do' || name.text === 'sleep' || name.text === 'cancel') {
41
33
  // Check if the parent is a call expression
42
34
  const parent = node.parent
43
35
  if (ts.isCallExpression(parent) && parent.expression === node) {
@@ -62,7 +54,6 @@ function getWorkflowInvocations(
62
54
  type: 'rpc',
63
55
  stepName,
64
56
  rpcName,
65
- description,
66
57
  })
67
58
  state.rpc.invokedFunctions.add(rpcName)
68
59
  } else if (isFunctionLike(secondArg)) {
@@ -86,6 +77,11 @@ function getWorkflowInvocations(
86
77
  stepName: stepName || '<dynamic>',
87
78
  duration: duration || '<dynamic>',
88
79
  })
80
+ } else if (name.text === 'cancel') {
81
+ // workflow.cancel(reason?)
82
+ steps.push({
83
+ type: 'cancel',
84
+ })
89
85
  }
90
86
  }
91
87
  }
@@ -105,43 +101,10 @@ function getWorkflowInvocations(
105
101
  }
106
102
 
107
103
  /**
108
- * Extract description from options object
109
- */
110
- function extractDescription(
111
- optionsNode: ts.Node | undefined,
112
- checker: ts.TypeChecker
113
- ): string | null {
114
- if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
115
- return null
116
- }
117
- return extractPropertyString(optionsNode, 'description', checker)
118
- }
119
-
120
- /**
121
- * Extract duration value (number or string)
122
- */
123
- function extractDuration(
124
- node: ts.Node,
125
- checker: ts.TypeChecker
126
- ): string | number | null {
127
- const numValue = extractNumberLiteral(node)
128
- if (numValue !== null) {
129
- return numValue
130
- }
131
- return extractStringLiteral(node, checker)
132
- }
133
-
134
- /**
135
- * Inspector for wireWorkflow() calls
104
+ * Inspector for pikkuWorkflow() and pikkuSimpleWorkflow() calls
136
105
  * Detects workflow registration and extracts metadata
137
106
  */
138
- export const addWorkflow: AddWiring = (
139
- logger,
140
- node,
141
- checker,
142
- state,
143
- options
144
- ) => {
107
+ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
145
108
  if (!ts.isCallExpression(node)) {
146
109
  return
147
110
  }
@@ -150,8 +113,16 @@ export const addWorkflow: AddWiring = (
150
113
  const firstArg = args[0]
151
114
  const expression = node.expression
152
115
 
153
- // Check if the call is to wireWorkflow
154
- if (!ts.isIdentifier(expression) || expression.text !== 'wireWorkflow') {
116
+ if (!ts.isIdentifier(expression)) {
117
+ return
118
+ }
119
+
120
+ let wrapperType: 'simple' | 'regular' | null = null
121
+ if (expression.text === 'pikkuWorkflowFunc') {
122
+ wrapperType = 'regular'
123
+ } else if (expression.text === 'pikkuSimpleWorkflowFunc') {
124
+ wrapperType = 'simple'
125
+ } else {
155
126
  return
156
127
  }
157
128
 
@@ -159,73 +130,112 @@ export const addWorkflow: AddWiring = (
159
130
  return
160
131
  }
161
132
 
162
- if (ts.isObjectLiteralExpression(firstArg)) {
163
- const obj = firstArg
164
-
165
- const workflowName = getPropertyValue(obj, 'name') as string | null
166
- const description = getPropertyValue(obj, 'description') as
167
- | string
168
- | undefined
169
- const docs = (getPropertyValue(obj, 'docs') as PikkuDocs) || undefined
170
- const tags = getPropertyTags(obj, 'Workflow', workflowName, logger)
171
-
172
- // --- find the referenced function ---
173
- const funcInitializer = getPropertyAssignmentInitializer(
174
- obj,
175
- 'func',
176
- true,
177
- checker
133
+ // Extract workflow name and metadata using same logic as add-functions
134
+ const { pikkuFuncName, name, exportedName } = extractFunctionName(
135
+ node,
136
+ checker,
137
+ state.rootDir
138
+ )
139
+
140
+ const workflowName = exportedName || name
141
+
142
+ if (!workflowName) {
143
+ logger.critical(
144
+ ErrorCode.MISSING_NAME,
145
+ `Could not determine workflow name from export.`
178
146
  )
147
+ return
148
+ }
179
149
 
180
- if (!workflowName) {
181
- logger.critical(
182
- ErrorCode.MISSING_NAME,
183
- `Wasn't able to determine 'name' property for workflow wiring.`
184
- )
185
- return
186
- }
150
+ // Extract the function node (either direct function or from config.func)
151
+ const { funcNode, resolvedFunc } = extractFunctionNode(firstArg, checker)
187
152
 
188
- if (!funcInitializer) {
189
- logger.critical(
190
- ErrorCode.MISSING_FUNC,
191
- `No valid 'func' property for workflow '${workflowName}'.`
192
- )
193
- return
194
- }
153
+ // Extract metadata if using object form
154
+ let tags: string[] | undefined
155
+ let summary: string | undefined
156
+ let description: string | undefined
157
+ let errors: string[] | undefined
195
158
 
196
- const pikkuFuncName = extractFunctionName(
197
- funcInitializer,
198
- checker,
199
- state.rootDir
200
- ).pikkuFuncName
159
+ if (ts.isObjectLiteralExpression(firstArg)) {
160
+ const metadata = getCommonWireMetaData(
161
+ firstArg,
162
+ 'Workflow',
163
+ workflowName,
164
+ logger
165
+ )
166
+ tags = metadata.tags
167
+ summary = metadata.summary
168
+ description = metadata.description
169
+ errors = metadata.errors
170
+ }
201
171
 
202
- // --- resolve middleware ---
203
- const middleware = resolveMiddleware(state, obj, tags, checker)
172
+ // Validate that we got a valid function
173
+ if (
174
+ ts.isObjectLiteralExpression(firstArg) &&
175
+ (!funcNode || funcNode === firstArg)
176
+ ) {
177
+ logger.critical(
178
+ ErrorCode.MISSING_FUNC,
179
+ `No valid 'func' property for workflow '${workflowName}'.`
180
+ )
181
+ return
182
+ }
204
183
 
205
- // --- track used functions/middleware for service aggregation ---
206
- state.serviceAggregation.usedFunctions.add(pikkuFuncName)
207
- extractWireNames(middleware).forEach((name) =>
208
- state.serviceAggregation.usedMiddleware.add(name)
184
+ if (!resolvedFunc) {
185
+ logger.critical(
186
+ ErrorCode.MISSING_FUNC,
187
+ `Could not resolve workflow function for '${workflowName}'.`
209
188
  )
189
+ return
190
+ }
210
191
 
211
- state.workflows.files.add(node.getSourceFile().fileName)
192
+ // Track workflow file for wiring generation
193
+ if (exportedName) {
194
+ state.workflows.files.set(pikkuFuncName, {
195
+ path: node.getSourceFile().fileName,
196
+ exportedName,
197
+ })
198
+ }
212
199
 
213
- // Extract workflow steps from function body
214
- // Resolve the identifier to the actual function declaration
215
- const resolvedFunc = resolveFunctionDeclaration(funcInitializer, checker)
216
- const steps: WorkflowStepMeta[] = []
217
- if (resolvedFunc) {
218
- getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
200
+ let steps: WorkflowStepMeta[] = []
201
+ let simple: boolean | undefined = undefined
202
+
203
+ // Try simple workflow extraction first
204
+ // Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
205
+ const result = extractSimpleWorkflow(node, checker)
206
+
207
+ if (result.status === 'ok' && result.steps) {
208
+ // Simple extraction succeeded
209
+ steps = result.steps
210
+ simple = true
211
+ } else {
212
+ // Simple extraction failed
213
+ if (wrapperType === 'simple') {
214
+ // For pikkuSimpleWorkflowFunc, this is a critical error
215
+ logger.critical(
216
+ ErrorCode.INVALID_SIMPLE_WORKFLOW,
217
+ `Workflow '${workflowName}' uses pikkuSimpleWorkflowFunc but does not conform to simple workflow DSL:\n${result.reason || 'Unknown error'}`
218
+ )
219
+ return
220
+ } else {
221
+ // For pikkuWorkflowFunc, fall back to basic extraction
222
+ logger.debug(
223
+ `Workflow '${workflowName}' could not be extracted as simple workflow: ${result.reason || 'Unknown error'}. Falling back to basic extraction.`
224
+ )
225
+ simple = false
219
226
  }
227
+ }
220
228
 
221
- state.workflows.meta[workflowName] = {
222
- pikkuFuncName,
223
- workflowName,
224
- description,
225
- docs,
226
- tags,
227
- middleware,
228
- steps,
229
- }
229
+ getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
230
+
231
+ state.workflows.meta[workflowName] = {
232
+ pikkuFuncName,
233
+ workflowName,
234
+ steps,
235
+ simple,
236
+ summary,
237
+ description,
238
+ errors,
239
+ tags,
230
240
  }
231
241
  }
@@ -21,6 +21,7 @@ export enum ErrorCode {
21
21
  CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = 'PKU672',
22
22
  DYNAMIC_STEP_NAME = 'PKU529',
23
23
  WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = 'PKU600',
24
+ INVALID_SIMPLE_WORKFLOW = 'PKU641',
24
25
 
25
26
  // Configuration errors
26
27
  CONFIG_TYPE_NOT_FOUND = 'PKU426',
package/src/index.ts CHANGED
@@ -14,3 +14,5 @@ export {
14
14
  } from './utils/serialize-inspector-state.js'
15
15
  export type { SerializableInspectorState } from './utils/serialize-inspector-state.js'
16
16
  export { filterInspectorState } from './utils/filter-inspector-state.js'
17
+ export { writeAllServiceMetadata } from './utils/write-service-metadata.js'
18
+ export type { ServiceMetadata } from './utils/extract-service-metadata.js'
package/src/inspector.ts CHANGED
@@ -5,7 +5,10 @@ import { TypesMap } from './types-map.js'
5
5
  import { InspectorState, InspectorLogger, InspectorOptions } from './types.js'
6
6
  import { getFilesAndMethods } from './utils/get-files-and-methods.js'
7
7
  import { findCommonAncestor } from './utils/find-root-dir.js'
8
- import { aggregateRequiredServices } from './utils/post-process.js'
8
+ import {
9
+ aggregateRequiredServices,
10
+ extractServiceInterfaceMetadata,
11
+ } from './utils/post-process.js'
9
12
 
10
13
  /**
11
14
  * Creates an initial/empty inspector state with all required properties initialized
@@ -16,12 +19,12 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
16
19
  return {
17
20
  rootDir,
18
21
  singletonServicesTypeImportMap: new Map(),
19
- sessionServicesTypeImportMap: new Map(),
22
+ wireServicesTypeImportMap: new Map(),
20
23
  userSessionTypeImportMap: new Map(),
21
24
  configTypeImportMap: new Map(),
22
25
  singletonServicesFactories: new Map(),
23
- sessionServicesFactories: new Map(),
24
- sessionServicesMeta: new Map(),
26
+ wireServicesFactories: new Map(),
27
+ wireServicesMeta: new Map(),
25
28
  configFactories: new Map(),
26
29
  filesAndMethods: {},
27
30
  filesAndMethodsErrors: new Map(),
@@ -60,7 +63,7 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
60
63
  },
61
64
  workflows: {
62
65
  meta: {},
63
- files: new Set(),
66
+ files: new Map(),
64
67
  },
65
68
  rpc: {
66
69
  internalMeta: {},
@@ -96,8 +99,9 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
96
99
  usedMiddleware: new Set(),
97
100
  usedPermissions: new Set(),
98
101
  allSingletonServices: [],
99
- allSessionServices: [],
102
+ allWireServices: [],
100
103
  },
104
+ serviceMetadata: [],
101
105
  }
102
106
  }
103
107
 
@@ -177,6 +181,12 @@ export const inspect = (
177
181
  logger.debug(
178
182
  `Aggregate required services completed in ${(performance.now() - startAggregate).toFixed(2)}ms`
179
183
  )
184
+
185
+ const startServiceMeta = performance.now()
186
+ extractServiceInterfaceMetadata(state, checker)
187
+ logger.debug(
188
+ `Extract service metadata completed in ${(performance.now() - startServiceMeta).toFixed(2)}ms`
189
+ )
180
190
  }
181
191
 
182
192
  return state
package/src/types.ts CHANGED
@@ -120,7 +120,7 @@ export type InspectorOptions = Partial<{
120
120
  configFileType: string
121
121
  userSessionType: string
122
122
  singletonServicesFactoryType: string
123
- sessionServicesFactoryType: string
123
+ wireServicesFactoryType: string
124
124
  }>
125
125
  }>
126
126
 
@@ -147,7 +147,7 @@ export interface InspectorFilesAndMethods {
147
147
  type: string
148
148
  typePath: string
149
149
  }
150
- sessionServicesType?: {
150
+ wireServicesType?: {
151
151
  file: string
152
152
  variable: string
153
153
  type: string
@@ -177,7 +177,7 @@ export interface InspectorFilesAndMethods {
177
177
  type: string
178
178
  typePath: string
179
179
  }
180
- sessionServicesFactory?: {
180
+ wireServicesFactory?: {
181
181
  file: string
182
182
  variable: string
183
183
  type: string
@@ -188,12 +188,12 @@ export interface InspectorFilesAndMethods {
188
188
  export interface InspectorState {
189
189
  rootDir: string // Root directory inferred from source files
190
190
  singletonServicesTypeImportMap: PathToNameAndType
191
- sessionServicesTypeImportMap: PathToNameAndType
191
+ wireServicesTypeImportMap: PathToNameAndType
192
192
  userSessionTypeImportMap: PathToNameAndType
193
193
  configTypeImportMap: PathToNameAndType
194
194
  singletonServicesFactories: PathToNameAndType
195
- sessionServicesFactories: PathToNameAndType
196
- sessionServicesMeta: Map<string, string[]> // variable name -> singleton services consumed
195
+ wireServicesFactories: PathToNameAndType
196
+ wireServicesMeta: Map<string, string[]> // variable name -> singleton services consumed
197
197
  configFactories: PathToNameAndType
198
198
  filesAndMethods: InspectorFilesAndMethods
199
199
  filesAndMethodsErrors: Map<string, PathToNameAndType>
@@ -211,7 +211,7 @@ export interface InspectorState {
211
211
  }
212
212
  workflows: {
213
213
  meta: WorkflowsMeta
214
- files: Set<string>
214
+ files: Map<string, { path: string; exportedName: string }>
215
215
  }
216
216
  rpc: {
217
217
  internalMeta: Record<string, string>
@@ -238,6 +238,16 @@ export interface InspectorState {
238
238
  usedMiddleware: Set<string> // Middleware names used by wired functions
239
239
  usedPermissions: Set<string> // Permission names used by wired functions
240
240
  allSingletonServices: string[] // All services available in SingletonServices type
241
- allSessionServices: string[] // All services available in Services type (excluding SingletonServices)
241
+ allWireServices: string[] // All services available in Services type (excluding SingletonServices)
242
242
  }
243
+ serviceMetadata: Array<{
244
+ name: string
245
+ summary: string
246
+ description: string
247
+ package: string
248
+ path: string
249
+ version: string
250
+ interface: string
251
+ expandedProperties: Record<string, string>
252
+ }>
243
253
  }
@@ -0,0 +1,58 @@
1
+ import * as ts from 'typescript'
2
+ import {
3
+ getPropertyAssignmentInitializer,
4
+ resolveFunctionDeclaration,
5
+ } from './type-utils.js'
6
+
7
+ /**
8
+ * Extracts the actual function node from a pikkuFunc/pikkuWorkflowFunc call
9
+ * Handles both direct function form and config object form { func: ... }
10
+ */
11
+ export function extractFunctionNode(
12
+ firstArg: ts.Expression,
13
+ checker: ts.TypeChecker
14
+ ): {
15
+ funcNode: ts.Node
16
+ resolvedFunc: ts.Node | null
17
+ isDirectFunction: boolean
18
+ } {
19
+ let funcNode: ts.Node = firstArg
20
+ let isDirectFunction = true
21
+
22
+ // Check if first argument is a config object with 'func' property
23
+ if (ts.isObjectLiteralExpression(firstArg)) {
24
+ isDirectFunction = false
25
+ const funcInitializer = getPropertyAssignmentInitializer(
26
+ firstArg,
27
+ 'func',
28
+ true,
29
+ checker
30
+ )
31
+
32
+ if (funcInitializer) {
33
+ funcNode = funcInitializer
34
+ } else {
35
+ // Return the original node if no func property found
36
+ // Caller should handle validation
37
+ funcNode = firstArg
38
+ }
39
+ }
40
+
41
+ // Resolve identifier to get the actual function node
42
+ if (ts.isIdentifier(funcNode)) {
43
+ const symbol = checker.getSymbolAtLocation(funcNode)
44
+ const decl = symbol?.valueDeclaration || symbol?.declarations?.[0]
45
+ if (decl && ts.isVariableDeclaration(decl) && decl.initializer) {
46
+ funcNode = decl.initializer
47
+ }
48
+ }
49
+
50
+ // Resolve function declaration for deeper analysis
51
+ const resolvedFunc = resolveFunctionDeclaration(funcNode, checker)
52
+
53
+ return {
54
+ funcNode,
55
+ resolvedFunc,
56
+ isDirectFunction,
57
+ }
58
+ }
@@ -99,3 +99,34 @@ export function extractPropertyString(
99
99
  }
100
100
  return null
101
101
  }
102
+
103
+ /**
104
+ * Extract description from options object
105
+ */
106
+ export function extractDescription(
107
+ optionsNode: ts.Node | undefined,
108
+ checker: ts.TypeChecker
109
+ ): string | null {
110
+ if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
111
+ return null
112
+ }
113
+ return extractPropertyString(optionsNode, 'description', checker)
114
+ }
115
+
116
+ /**
117
+ * Extract duration value (number or string)
118
+ */
119
+ export function extractDuration(
120
+ node: ts.Node,
121
+ checker: ts.TypeChecker
122
+ ): string | number | null {
123
+ const numValue = extractNumberLiteral(node)
124
+ if (numValue !== null) {
125
+ return numValue
126
+ }
127
+ try {
128
+ return extractStringLiteral(node, checker)
129
+ } catch {
130
+ return null
131
+ }
132
+ }