@pikku/inspector 0.11.1 → 0.12.0

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 (189) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/OPTIMIZATION-PLAN.md +195 -0
  3. package/dist/add/add-ai-agent.d.ts +2 -0
  4. package/dist/add/add-ai-agent.js +314 -0
  5. package/dist/add/add-channel.js +69 -61
  6. package/dist/add/add-cli.js +36 -18
  7. package/dist/add/add-file-with-factory.js +2 -0
  8. package/dist/add/add-functions.js +327 -59
  9. package/dist/add/add-http-route.d.ts +19 -10
  10. package/dist/add/add-http-route.js +153 -44
  11. package/dist/add/add-http-routes.d.ts +5 -0
  12. package/dist/add/add-http-routes.js +159 -0
  13. package/dist/add/add-keyed-wiring.d.ts +12 -0
  14. package/dist/add/add-keyed-wiring.js +97 -0
  15. package/dist/add/add-mcp-prompt.js +14 -9
  16. package/dist/add/add-mcp-resource.js +14 -9
  17. package/dist/add/add-middleware.d.ts +1 -4
  18. package/dist/add/add-middleware.js +364 -79
  19. package/dist/add/add-permission.d.ts +1 -1
  20. package/dist/add/add-permission.js +152 -40
  21. package/dist/add/add-queue-worker.js +18 -12
  22. package/dist/add/add-rpc-invocations.d.ts +3 -0
  23. package/dist/add/add-rpc-invocations.js +65 -25
  24. package/dist/add/add-schedule.js +11 -5
  25. package/dist/add/add-secret.d.ts +3 -0
  26. package/dist/add/add-secret.js +82 -0
  27. package/dist/add/add-trigger.d.ts +2 -0
  28. package/dist/add/add-trigger.js +87 -0
  29. package/dist/add/add-variable.d.ts +1 -0
  30. package/dist/add/add-variable.js +8 -0
  31. package/dist/add/add-workflow-graph.d.ts +7 -0
  32. package/dist/add/add-workflow-graph.js +396 -0
  33. package/dist/add/add-workflow.js +124 -26
  34. package/dist/error-codes.d.ts +16 -1
  35. package/dist/error-codes.js +21 -1
  36. package/dist/index.d.ts +9 -5
  37. package/dist/index.js +5 -2
  38. package/dist/inspector.d.ts +1 -1
  39. package/dist/inspector.js +106 -13
  40. package/dist/schema-generator.d.ts +1 -0
  41. package/dist/schema-generator.js +1 -0
  42. package/dist/types-map.js +10 -1
  43. package/dist/types.d.ts +180 -30
  44. package/dist/utils/compute-required-schemas.d.ts +4 -0
  45. package/dist/utils/compute-required-schemas.js +41 -0
  46. package/dist/utils/contract-hashes.d.ts +35 -0
  47. package/dist/utils/contract-hashes.js +202 -0
  48. package/dist/utils/custom-types-generator.d.ts +9 -0
  49. package/dist/utils/custom-types-generator.js +71 -0
  50. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  51. package/dist/utils/detect-schema-vendor.js +76 -0
  52. package/dist/utils/ensure-function-metadata.d.ts +5 -2
  53. package/dist/utils/ensure-function-metadata.js +220 -6
  54. package/dist/utils/extract-function-name.d.ts +5 -16
  55. package/dist/utils/extract-function-name.js +93 -298
  56. package/dist/utils/extract-services.d.ts +2 -1
  57. package/dist/utils/extract-services.js +25 -1
  58. package/dist/utils/filter-inspector-state.js +107 -23
  59. package/dist/utils/get-property-value.d.ts +8 -2
  60. package/dist/utils/get-property-value.js +33 -4
  61. package/dist/utils/hash.d.ts +2 -0
  62. package/dist/utils/hash.js +23 -0
  63. package/dist/utils/middleware.d.ts +7 -30
  64. package/dist/utils/middleware.js +80 -66
  65. package/dist/utils/permissions.d.ts +2 -2
  66. package/dist/utils/permissions.js +10 -10
  67. package/dist/utils/post-process.d.ts +9 -10
  68. package/dist/utils/post-process.js +231 -24
  69. package/dist/utils/resolve-external-package.d.ts +12 -0
  70. package/dist/utils/resolve-external-package.js +34 -0
  71. package/dist/utils/resolve-function-types.d.ts +6 -0
  72. package/dist/utils/resolve-function-types.js +29 -0
  73. package/dist/utils/resolve-identifier.d.ts +10 -0
  74. package/dist/utils/resolve-identifier.js +36 -0
  75. package/dist/utils/resolve-versions.d.ts +2 -0
  76. package/dist/utils/resolve-versions.js +78 -0
  77. package/dist/utils/schema-generator.d.ts +9 -0
  78. package/dist/utils/schema-generator.js +209 -0
  79. package/dist/utils/serialize-inspector-state.d.ts +73 -13
  80. package/dist/utils/serialize-inspector-state.js +102 -6
  81. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  82. package/dist/utils/serialize-mcp-json.js +99 -0
  83. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  84. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  85. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  86. package/dist/utils/serialize-openapi-json.js +151 -0
  87. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  88. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  89. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  90. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +830 -0
  91. package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
  92. package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +572 -72
  93. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  94. package/dist/utils/workflow/dsl/index.js +7 -0
  95. package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
  96. package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
  97. package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
  98. package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
  99. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  100. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +318 -0
  101. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  102. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  103. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  104. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  105. package/dist/utils/workflow/graph/index.d.ts +8 -0
  106. package/dist/utils/workflow/graph/index.js +8 -0
  107. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +35 -0
  108. package/dist/utils/workflow/graph/serialize-workflow-graph.js +150 -0
  109. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +203 -0
  110. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  111. package/dist/visit.js +13 -2
  112. package/package.json +26 -4
  113. package/src/add/add-ai-agent.ts +468 -0
  114. package/src/add/add-channel.ts +82 -79
  115. package/src/add/add-cli.ts +49 -20
  116. package/src/add/add-file-with-factory.ts +2 -0
  117. package/src/add/add-functions.ts +429 -71
  118. package/src/add/add-http-route.ts +246 -65
  119. package/src/add/add-http-routes.ts +228 -0
  120. package/src/add/add-keyed-wiring.ts +151 -0
  121. package/src/add/add-mcp-prompt.ts +26 -15
  122. package/src/add/add-mcp-resource.ts +27 -15
  123. package/src/add/add-middleware.ts +482 -80
  124. package/src/add/add-permission.ts +199 -40
  125. package/src/add/add-queue-worker.ts +24 -19
  126. package/src/add/add-rpc-invocations.ts +78 -31
  127. package/src/add/add-schedule.ts +16 -11
  128. package/src/add/add-secret.ts +140 -0
  129. package/src/add/add-trigger.ts +154 -0
  130. package/src/add/add-variable.ts +9 -0
  131. package/src/add/add-workflow-graph.ts +522 -0
  132. package/src/add/add-workflow.ts +117 -30
  133. package/src/error-codes.ts +26 -1
  134. package/src/index.ts +27 -8
  135. package/src/inspector.ts +145 -17
  136. package/src/schema-generator.ts +1 -0
  137. package/src/types-map.ts +12 -1
  138. package/src/types.ts +192 -51
  139. package/src/utils/compute-required-schemas.ts +49 -0
  140. package/src/utils/contract-hashes.test.ts +528 -0
  141. package/src/utils/contract-hashes.ts +290 -0
  142. package/src/utils/custom-types-generator.ts +88 -0
  143. package/src/utils/detect-schema-vendor.ts +90 -0
  144. package/src/utils/ensure-function-metadata.ts +324 -7
  145. package/src/utils/extract-function-name.ts +108 -358
  146. package/src/utils/extract-services.ts +35 -2
  147. package/src/utils/filter-inspector-state.test.ts +34 -20
  148. package/src/utils/filter-inspector-state.ts +140 -31
  149. package/src/utils/get-property-value.ts +50 -5
  150. package/src/utils/hash.ts +26 -0
  151. package/src/utils/middleware.test.ts +204 -0
  152. package/src/utils/middleware.ts +129 -67
  153. package/src/utils/permissions.test.ts +35 -12
  154. package/src/utils/permissions.ts +10 -10
  155. package/src/utils/post-process.ts +283 -43
  156. package/src/utils/resolve-external-package.ts +42 -0
  157. package/src/utils/resolve-function-types.ts +42 -0
  158. package/src/utils/resolve-identifier.ts +46 -0
  159. package/src/utils/resolve-versions.test.ts +249 -0
  160. package/src/utils/resolve-versions.ts +105 -0
  161. package/src/utils/schema-generator.ts +329 -0
  162. package/src/utils/serialize-inspector-state.ts +181 -20
  163. package/src/utils/serialize-mcp-json.ts +145 -0
  164. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  165. package/src/utils/serialize-openapi-json.ts +277 -0
  166. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  167. package/src/utils/test-data/inspector-state.json +69 -66
  168. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1104 -0
  169. package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +678 -85
  170. package/src/utils/workflow/dsl/index.ts +11 -0
  171. package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
  172. package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
  173. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +422 -0
  174. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  175. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  176. package/src/utils/workflow/graph/index.ts +11 -0
  177. package/src/utils/workflow/graph/serialize-workflow-graph.ts +216 -0
  178. package/src/utils/workflow/graph/workflow-graph.types.ts +231 -0
  179. package/src/visit.ts +14 -2
  180. package/tsconfig.tsbuildinfo +1 -1
  181. package/dist/add/add-mcp-tool.d.ts +0 -2
  182. package/dist/add/add-mcp-tool.js +0 -81
  183. package/dist/utils/extract-service-metadata.d.ts +0 -19
  184. package/dist/utils/extract-service-metadata.js +0 -244
  185. package/dist/utils/write-service-metadata.d.ts +0 -13
  186. package/dist/utils/write-service-metadata.js +0 -37
  187. package/src/add/add-mcp-tool.ts +0 -141
  188. package/src/utils/extract-service-metadata.ts +0 -353
  189. package/src/utils/write-service-metadata.ts +0 -51
package/CHANGELOG.md CHANGED
@@ -1,4 +1,29 @@
1
- ## 0.11.0
1
+ ## 0.12.0
2
+
3
+ ### New Features
4
+
5
+ - AI agent metadata extraction
6
+ - HTTP route groups analysis
7
+ - Trigger and trigger source analysis
8
+ - Secret and variable declaration extraction
9
+ - Workflow graph inspection and DSL extraction
10
+ - Contract hashing for change detection
11
+ - OpenAPI spec generation (moved from CLI)
12
+
13
+ ## 0.11.2
14
+
15
+ ### Patch Changes
16
+
17
+ - db9c7bf: Add workflow graph inspection and DSL extraction
18
+ - Updated dependencies [db9c7bf]
19
+ - @pikku/core@0.11.2
20
+
21
+ ### Features
22
+
23
+ - f35e89da: Add workflow graph inspection and DSL extraction
24
+ - Workflow graph inspection with `add-workflow-graph.ts`
25
+ - DSL workflow extraction utilities (extract, deserialize, validate)
26
+ - DSL to graph conversion for metadata generation
2
27
 
3
28
  ## 0.11.1
4
29
 
@@ -0,0 +1,195 @@
1
+ # Inspector Package Optimization Plan
2
+
3
+ ## Context
4
+
5
+ The inspector (`packages/inspector/src/`) is a static code analyzer built on TypeScript's compiler API. It extracts metadata from source files via a two-pass AST visitor pattern (setup + routes). Profiling reveals that for **every AST node**, all handlers are called sequentially (10 in setup, 16 in routes) even though only 1 ever matches. This creates millions of wasted function calls on large codebases.
6
+
7
+ The inspector already has `performance.now()` instrumentation for 7 phases. We'll use this to measure each optimization independently.
8
+
9
+ ---
10
+
11
+ ## Step 0: Create Benchmark Harness
12
+
13
+ **Goal:** Establish reproducible baseline measurements.
14
+
15
+ **File:** `src/benchmark.ts`
16
+
17
+ **What it does:**
18
+
19
+ - Collects `.ts` files from a target directory (e.g., the external repo's packages)
20
+ - Runs `inspect()` N iterations (default 10) with a warmup run
21
+ - Parses existing `logger.debug` messages to capture per-phase timings
22
+ - Reports median, mean, min, max for each phase + total
23
+ - Outputs JSON for before/after diffing
24
+ - Also benchmarks `filterInspectorState()` separately using the test fixture (`src/utils/test-data/inspector-state.json`)
25
+ - Adds counters for: total AST nodes visited, CallExpression nodes, dispatch hits
26
+
27
+ **Run with:**
28
+
29
+ ```bash
30
+ node --import tsx src/benchmark.ts <target-dir> <iterations>
31
+ ```
32
+
33
+ **How to verify:** Run 3x, confirm <5% variance between runs for each phase.
34
+
35
+ ---
36
+
37
+ ## Step 1: Identifier-Based Dispatch Map in `visit.ts`
38
+
39
+ **File:** `src/visit.ts`
40
+
41
+ **Problem:** Every AST node goes through ALL handlers. In `visitRoutes`, that's 16 function calls per node. Each handler independently checks `isCallExpression` -> `isIdentifier` -> `expression.text === 'xyz'`. For non-CallExpression nodes (the vast majority), all 16 calls are wasted.
42
+
43
+ **Fix:** Check node kind once, extract identifier once, use `Map<string, handler>` for O(1) dispatch.
44
+
45
+ **Dispatch map for `visitRoutes`** (all handlers expect CallExpression + Identifier):
46
+
47
+ | Identifier | Handler |
48
+ | ---------------------- | --------------------- |
49
+ | `wireHTTP` | `addHTTPRoute` |
50
+ | `wireHTTPRoutes` | `addHTTPRoutes` |
51
+ | `wireScheduler` | `addSchedule` |
52
+ | `wireTrigger` | `addTrigger` |
53
+ | `wireTriggerSource` | `addTrigger` |
54
+ | `wireQueueWorker` | `addQueueWorker` |
55
+ | `wireChannel` | `addChannel` |
56
+ | `wireCLI` | `addCLI` |
57
+ | `pikkuCLIRender` | `addCLIRenderers` |
58
+ | `wireMCPResource` | `addMCPResource` |
59
+ | `wireMCPTool` | `addMCPTool` |
60
+ | `wireMCPPrompt` | `addMCPPrompt` |
61
+ | `wireWorkflowGraph` | `addWorkflowGraph` |
62
+ | `wireSecret` | `addSecret` |
63
+ | `wireOAuth2Credential` | `addOAuth2Credential` |
64
+ | `wireVariable` | `addVariable` |
65
+
66
+ Fallback for unmatched identifiers: check `/pikku.*func/i` regex -> `addFunctions`.
67
+
68
+ **Dispatch for `visitSetup`** (mixed node kinds):
69
+
70
+ - `SyntaxKind.CallExpression` + Identifier -> map with: `external`, `workflow`, `workflowStart`, `workflowRun`, `workflowStatus`, `graphStart` -> `addRPCInvocations`; `pikkuMiddleware`, `pikkuMiddlewareFactory`, `addMiddleware`, `addHTTPMiddleware` -> `addMiddleware`; `pikkuPermission`, `pikkuPermissionFactory`, `addPermission`, `addHTTPPermission` -> `addPermission`; `pikkuWorkflowFunc`, `pikkuWorkflowComplexFunc` -> `addWorkflow`
71
+ - `SyntaxKind.CallExpression` + PropertyAccessExpression (`rpc.invoke`) -> `addRPCInvocations`
72
+ - `SyntaxKind.ClassDeclaration` / `InterfaceDeclaration` -> `addFileExtendsCoreType` (4 core type names)
73
+ - `SyntaxKind.VariableDeclaration` -> `addFileWithFactory` (3 factory type names)
74
+ - All other node kinds -> skip directly to recursion
75
+
76
+ **Measure:** Compare "Visit setup phase" and "Visit routes phase" benchmark timings.
77
+
78
+ **Expected improvement:** 30-60% reduction in visit phase times.
79
+
80
+ **Verify:** `bash run-tests.sh` + compare serialized inspector state output before/after.
81
+
82
+ ---
83
+
84
+ ## Step 2: Hoist Regex + Remove Redundant Checks in `add-functions.ts`
85
+
86
+ **File:** `src/add/add-functions.ts`
87
+
88
+ **Problem (lines 290-309):**
89
+
90
+ ```ts
91
+ // Line 301: regex re-created EVERY call (every AST node)
92
+ const pikkuFuncPattern = /pikku.*func/i
93
+ // Line 307: redundant - isIdentifier already checked at line 296,
94
+ // startsWith('pikku') is a subset of the regex
95
+ if (!ts.isIdentifier(expression) || !expression.text.startsWith('pikku'))
96
+ ```
97
+
98
+ **Fix:**
99
+
100
+ 1. Move `const pikkuFuncPattern = /pikku.*func/i` to module scope
101
+ 2. Remove the redundant check at line 307-309
102
+
103
+ **Measure:** "Visit routes phase" timing.
104
+
105
+ **Expected improvement:** 1-5% of visit routes phase.
106
+
107
+ **Verify:** `bash run-tests.sh`
108
+
109
+ ---
110
+
111
+ ## Step 3: Replace `getText()` with `.text` Property Access
112
+
113
+ **Files:** 3-5 files, ~8 safe replacements
114
+
115
+ `node.getText()` reconstructs text by slicing the source string. For `Identifier` nodes, `.text` is a direct property -- zero overhead.
116
+
117
+ **Safe replacements:**
118
+
119
+ - `src/add/add-file-extends-core-type.ts` line 13: `node.name?.getText()` -> `node.name?.text`
120
+ - `src/add/add-file-with-factory.ts` line 24: `node.name.getText()` -> guard with `ts.isIdentifier(node.name) ? node.name.text : node.name.getText()`
121
+ - `src/add/add-channel.ts` line 360: `shorthandDecl.name.getText()` -> `.text` (ImportSpecifier name is always Identifier)
122
+
123
+ **Skip** (not safe or rarely hit): error messages, template literals, TypeNode, QualifiedName.
124
+
125
+ **Measure:** "Visit setup phase" + "Visit routes phase" timings.
126
+
127
+ **Expected improvement:** 1-3% of visit phases.
128
+
129
+ **Verify:** `bash run-tests.sh`
130
+
131
+ ---
132
+
133
+ ## Step 4: Replace `JSON.parse(JSON.stringify(...))` with `structuredClone()`
134
+
135
+ **File:** `src/utils/filter-inspector-state.ts` (lines 198-233)
136
+
137
+ **Problem:** 8 calls to `JSON.parse(JSON.stringify(...))` for deep cloning metadata objects. This serializes to string then parses back -- wasteful.
138
+
139
+ **Fix:** Replace all 8 with `structuredClone()` (available Node 17+, package requires >=18).
140
+
141
+ ```ts
142
+ // Before
143
+ meta: JSON.parse(JSON.stringify(state.http.meta))
144
+ // After
145
+ meta: structuredClone(state.http.meta)
146
+ ```
147
+
148
+ **Measure:** Separate `filterInspectorState` benchmark loop using the 45KB test fixture.
149
+
150
+ **Expected improvement:** 10-30% of filter phase time (not the main inspect path, but important for CLI).
151
+
152
+ **Verify:** `bash run-tests.sh` (filter-inspector-state.test.ts covers this extensively -- 1,440 lines of tests).
153
+
154
+ ---
155
+
156
+ ## Step 5: Cache `node.getSourceFile().fileName` in Hot Handlers
157
+
158
+ **Files:** Primarily `src/add/add-functions.ts`
159
+
160
+ **Problem:** `add-functions.ts` calls `node.getSourceFile().fileName` 4+ times within a single handler invocation for the same node (lines 666, 700, 721, 735). `getSourceFile()` walks the parent chain each time.
161
+
162
+ **Fix:** Extract once at handler entry:
163
+
164
+ ```ts
165
+ const sourceFileName = node.getSourceFile().fileName
166
+ ```
167
+
168
+ Then reuse `sourceFileName` throughout.
169
+
170
+ **Measure:** "Visit routes phase" timing.
171
+
172
+ **Expected improvement:** 1-2%.
173
+
174
+ **Verify:** `bash run-tests.sh`
175
+
176
+ ---
177
+
178
+ ## Summary
179
+
180
+ | Step | Change | File(s) | Expected Impact | Metric |
181
+ | ---- | ----------------- | --------------------------- | -------------------------- | -------------------- |
182
+ | 0 | Benchmark harness | New `benchmark.ts` | Baseline | - |
183
+ | 1 | Dispatch map | `visit.ts` | **30-60%** of visit phases | Visit setup + routes |
184
+ | 2 | Hoist regex | `add-functions.ts` | 1-5% of visit routes | Visit routes |
185
+ | 3 | getText -> .text | 3-5 files | 1-3% of visit phases | Visit setup + routes |
186
+ | 4 | structuredClone | `filter-inspector-state.ts` | 10-30% of filter phase | Filter benchmark |
187
+ | 5 | Cache sourceFile | `add-functions.ts` | 1-2% of visit routes | Visit routes |
188
+
189
+ ## Verification
190
+
191
+ After each step:
192
+
193
+ 1. Run benchmark: `node --import tsx src/benchmark.ts <target> 10`
194
+ 2. Run tests: `cd packages/inspector && bash run-tests.sh`
195
+ 3. Compare serialized inspector state output (before vs after) to confirm identical results
@@ -0,0 +1,2 @@
1
+ import { AddWiring } from '../types.js';
2
+ export declare const addAIAgent: AddWiring;
@@ -0,0 +1,314 @@
1
+ import * as ts from 'typescript';
2
+ import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
3
+ import { extractWireNames } from '../utils/post-process.js';
4
+ import { extractFunctionName, funcIdToTypeName, } from '../utils/extract-function-name.js';
5
+ import { resolveMiddleware, resolveChannelMiddleware, resolveAIMiddleware, } from '../utils/middleware.js';
6
+ import { resolvePermissions } from '../utils/permissions.js';
7
+ import { ErrorCode } from '../error-codes.js';
8
+ import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
9
+ function resolveToolReferences(obj, checker, agentName, logger) {
10
+ const property = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
11
+ ts.isIdentifier(p.name) &&
12
+ p.name.text === 'tools');
13
+ if (!property || !ts.isPropertyAssignment(property)) {
14
+ return null;
15
+ }
16
+ const initializer = property.initializer;
17
+ if (!ts.isArrayLiteralExpression(initializer)) {
18
+ return null;
19
+ }
20
+ const resolved = [];
21
+ for (const element of initializer.elements) {
22
+ if (ts.isStringLiteral(element)) {
23
+ logger.critical(ErrorCode.INVALID_VALUE, `AI agent '${agentName}' tools array contains a string literal '${element.text}'. ` +
24
+ `Use a function reference instead (e.g., import the pikkuFunc variable).`);
25
+ continue;
26
+ }
27
+ if (ts.isCallExpression(element) && ts.isIdentifier(element.expression)) {
28
+ const calleeName = element.expression.text;
29
+ if (calleeName === 'workflow') {
30
+ const [firstArg] = element.arguments;
31
+ if (firstArg && ts.isStringLiteral(firstArg)) {
32
+ resolved.push(`workflow:${firstArg.text}`);
33
+ continue;
34
+ }
35
+ }
36
+ if (calleeName === 'external') {
37
+ const [firstArg] = element.arguments;
38
+ if (firstArg && ts.isStringLiteral(firstArg)) {
39
+ resolved.push(firstArg.text);
40
+ continue;
41
+ }
42
+ }
43
+ }
44
+ if (ts.isIdentifier(element)) {
45
+ const rpcName = resolveIdentifierToRpcName(element, checker);
46
+ if (rpcName) {
47
+ resolved.push(rpcName);
48
+ continue;
49
+ }
50
+ logger.critical(ErrorCode.INVALID_VALUE, `AI agent '${agentName}' tools array contains identifier '${element.text}' ` +
51
+ `that could not be resolved to a pikkuFunc.`);
52
+ }
53
+ }
54
+ return resolved.length > 0 ? resolved : null;
55
+ }
56
+ function resolveAgentReferences(obj, checker, agentName, logger) {
57
+ const property = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
58
+ ts.isIdentifier(p.name) &&
59
+ p.name.text === 'agents');
60
+ if (!property || !ts.isPropertyAssignment(property)) {
61
+ return null;
62
+ }
63
+ const initializer = property.initializer;
64
+ if (!ts.isArrayLiteralExpression(initializer)) {
65
+ return null;
66
+ }
67
+ const resolved = [];
68
+ for (const element of initializer.elements) {
69
+ if (ts.isStringLiteral(element)) {
70
+ logger.critical(ErrorCode.INVALID_VALUE, `AI agent '${agentName}' agents array contains a string literal '${element.text}'. ` +
71
+ `Use an agent reference instead (e.g., import the pikkuAIAgent variable).`);
72
+ continue;
73
+ }
74
+ if (ts.isIdentifier(element)) {
75
+ const name = resolveIdentifierToAgentName(element, checker);
76
+ if (name) {
77
+ resolved.push(name);
78
+ continue;
79
+ }
80
+ logger.critical(ErrorCode.INVALID_VALUE, `AI agent '${agentName}' agents array contains identifier '${element.text}' ` +
81
+ `that could not be resolved to a pikkuAIAgent.`);
82
+ }
83
+ }
84
+ return resolved.length > 0 ? resolved : null;
85
+ }
86
+ function resolveIdentifierToRpcName(identifier, checker) {
87
+ const symbol = checker.getSymbolAtLocation(identifier);
88
+ if (!symbol)
89
+ return null;
90
+ let resolved = symbol;
91
+ if (resolved.flags & ts.SymbolFlags.Alias) {
92
+ resolved = checker.getAliasedSymbol(resolved) ?? resolved;
93
+ }
94
+ const decl = resolved.valueDeclaration ?? resolved.declarations?.[0];
95
+ if (!decl)
96
+ return null;
97
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
98
+ if (ts.isCallExpression(decl.initializer) &&
99
+ ts.isIdentifier(decl.initializer.expression)) {
100
+ const callName = decl.initializer.expression.text;
101
+ if (callName === 'pikkuFunc' ||
102
+ callName === 'pikkuSessionlessFunc' ||
103
+ callName === 'pikkuVoidFunc') {
104
+ const firstArg = decl.initializer.arguments[0];
105
+ 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
+ return prop.initializer.text;
112
+ }
113
+ }
114
+ }
115
+ if (ts.isIdentifier(decl.name)) {
116
+ return decl.name.text;
117
+ }
118
+ }
119
+ }
120
+ }
121
+ return null;
122
+ }
123
+ function resolveIdentifierToAgentName(identifier, checker) {
124
+ const symbol = checker.getSymbolAtLocation(identifier);
125
+ if (!symbol)
126
+ return null;
127
+ let resolved = symbol;
128
+ if (resolved.flags & ts.SymbolFlags.Alias) {
129
+ resolved = checker.getAliasedSymbol(resolved) ?? resolved;
130
+ }
131
+ const decl = resolved.valueDeclaration ?? resolved.declarations?.[0];
132
+ if (!decl)
133
+ return null;
134
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
135
+ if (ts.isCallExpression(decl.initializer) &&
136
+ ts.isIdentifier(decl.initializer.expression) &&
137
+ decl.initializer.expression.text === 'pikkuAIAgent') {
138
+ if (ts.isIdentifier(decl.name)) {
139
+ return decl.name.text;
140
+ }
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+ export const addAIAgent = (logger, node, checker, state, options) => {
146
+ if (!ts.isCallExpression(node)) {
147
+ return;
148
+ }
149
+ const args = node.arguments;
150
+ const firstArg = args[0];
151
+ const expression = node.expression;
152
+ if (!ts.isIdentifier(expression) || expression.text !== 'pikkuAIAgent') {
153
+ return;
154
+ }
155
+ if (!firstArg) {
156
+ return;
157
+ }
158
+ const { exportedName } = extractFunctionName(node, checker, state.rootDir);
159
+ if (ts.isObjectLiteralExpression(firstArg)) {
160
+ const obj = firstArg;
161
+ const nameValue = getPropertyValue(obj, 'name');
162
+ const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'AI agent', nameValue, logger);
163
+ if (disabled)
164
+ return;
165
+ const modelValue = getPropertyValue(obj, 'model');
166
+ const instructionsValue = getPropertyValue(obj, 'instructions');
167
+ const maxStepsValue = getPropertyValue(obj, 'maxSteps');
168
+ const temperatureValue = getPropertyValue(obj, 'temperature');
169
+ const toolChoiceValue = getPropertyValue(obj, 'toolChoice');
170
+ const toolsValue = resolveToolReferences(obj, checker, nameValue || '', logger);
171
+ if (toolsValue) {
172
+ for (const toolName of toolsValue) {
173
+ if (toolName.startsWith('workflow:') || toolName.includes(':'))
174
+ continue;
175
+ const funcFile = state.functions.files.get(toolName);
176
+ if (funcFile && !state.rpc.internalFiles.has(toolName)) {
177
+ state.rpc.internalFiles.set(toolName, funcFile);
178
+ }
179
+ }
180
+ }
181
+ const agentsValue = resolveAgentReferences(obj, checker, nameValue || '', logger);
182
+ if (!nameValue) {
183
+ logger.critical(ErrorCode.MISSING_NAME, "AI agent is missing the required 'name' property.");
184
+ return;
185
+ }
186
+ const agentKey = exportedName || nameValue;
187
+ if (!description) {
188
+ logger.critical(ErrorCode.MISSING_DESCRIPTION, `AI agent '${nameValue}' is missing a description.`);
189
+ return;
190
+ }
191
+ const resolveSchemaRef = (identifier, context) => {
192
+ const symbol = checker.getSymbolAtLocation(identifier);
193
+ if (!symbol)
194
+ return null;
195
+ const decl = symbol.valueDeclaration || symbol.declarations?.[0];
196
+ if (!decl)
197
+ return null;
198
+ let sourceFile;
199
+ if (ts.isImportSpecifier(decl)) {
200
+ const aliasedSymbol = checker.getAliasedSymbol(symbol);
201
+ if (aliasedSymbol) {
202
+ const aliasedDecl = aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0];
203
+ if (aliasedDecl) {
204
+ sourceFile = aliasedDecl.getSourceFile().fileName;
205
+ }
206
+ else {
207
+ return null;
208
+ }
209
+ }
210
+ else {
211
+ return null;
212
+ }
213
+ }
214
+ else {
215
+ sourceFile = decl.getSourceFile().fileName;
216
+ }
217
+ const vendor = detectSchemaVendorOrError(identifier, checker, logger, context, sourceFile);
218
+ if (!vendor)
219
+ return null;
220
+ return {
221
+ variableName: identifier.text,
222
+ sourceFile,
223
+ vendor,
224
+ };
225
+ };
226
+ let inputSchema = null;
227
+ let outputSchema = null;
228
+ let workingMemorySchema = null;
229
+ const capitalizedName = funcIdToTypeName(agentKey);
230
+ for (const prop of obj.properties) {
231
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
232
+ const propName = prop.name.text;
233
+ if (propName === 'input' || propName === 'output') {
234
+ if (ts.isIdentifier(prop.initializer)) {
235
+ const context = `AI agent '${nameValue}' ${propName}`;
236
+ const ref = resolveSchemaRef(prop.initializer, context);
237
+ if (ref) {
238
+ const schemaName = `${capitalizedName}${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
239
+ state.schemaLookup.set(schemaName, ref);
240
+ state.functions.typesMap.addCustomType(schemaName, 'unknown', []);
241
+ if (propName === 'input') {
242
+ inputSchema = schemaName;
243
+ }
244
+ else {
245
+ outputSchema = schemaName;
246
+ }
247
+ }
248
+ }
249
+ else if (ts.isCallExpression(prop.initializer)) {
250
+ const schemaName = `${capitalizedName}${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
251
+ logger.critical(ErrorCode.INLINE_SCHEMA, `Inline schemas are not supported for '${propName}' in AI agent '${nameValue}'.\n` +
252
+ ` Extract to an exported variable:\n` +
253
+ ` export const ${schemaName} = ${prop.initializer.getText()}\n` +
254
+ ` Then use: ${propName}: ${schemaName}`);
255
+ }
256
+ }
257
+ else if (propName === 'memory' &&
258
+ ts.isObjectLiteralExpression(prop.initializer)) {
259
+ for (const memProp of prop.initializer.properties) {
260
+ if (ts.isPropertyAssignment(memProp) &&
261
+ ts.isIdentifier(memProp.name) &&
262
+ memProp.name.text === 'workingMemory' &&
263
+ ts.isIdentifier(memProp.initializer)) {
264
+ const context = `AI agent '${nameValue}' workingMemory`;
265
+ const ref = resolveSchemaRef(memProp.initializer, context);
266
+ if (ref) {
267
+ const schemaName = `${capitalizedName}WorkingMemory`;
268
+ state.schemaLookup.set(schemaName, ref);
269
+ state.functions.typesMap.addCustomType(schemaName, 'unknown', []);
270
+ workingMemorySchema = schemaName;
271
+ }
272
+ }
273
+ }
274
+ }
275
+ }
276
+ }
277
+ const middleware = resolveMiddleware(state, obj, tags, checker);
278
+ const channelMiddleware = resolveChannelMiddleware(state, obj, tags, checker);
279
+ const aiMiddleware = resolveAIMiddleware(state, obj, checker);
280
+ const permissions = resolvePermissions(state, obj, tags, checker);
281
+ state.serviceAggregation.usedFunctions.add(agentKey);
282
+ extractWireNames(middleware).forEach((name) => state.serviceAggregation.usedMiddleware.add(name));
283
+ extractWireNames(permissions).forEach((name) => state.serviceAggregation.usedPermissions.add(name));
284
+ if (exportedName) {
285
+ state.agents.files.set(agentKey, {
286
+ path: node.getSourceFile().fileName,
287
+ exportedName,
288
+ });
289
+ }
290
+ state.agents.agentsMeta[agentKey] = {
291
+ name: nameValue,
292
+ description,
293
+ instructions: instructionsValue || '',
294
+ model: modelValue || '',
295
+ summary,
296
+ errors,
297
+ ...(maxStepsValue !== null && { maxSteps: maxStepsValue }),
298
+ ...(temperatureValue !== null && { temperature: temperatureValue }),
299
+ ...(toolChoiceValue !== null && {
300
+ toolChoice: toolChoiceValue,
301
+ }),
302
+ ...(toolsValue !== null && { tools: toolsValue }),
303
+ ...(agentsValue !== null && { agents: agentsValue }),
304
+ tags,
305
+ inputSchema,
306
+ outputSchema,
307
+ workingMemorySchema,
308
+ middleware,
309
+ channelMiddleware,
310
+ aiMiddleware,
311
+ permissions,
312
+ };
313
+ }
314
+ };