@agentuity/cli 1.0.47 → 2.0.0-beta.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 (115) hide show
  1. package/dist/cmd/build/app-router-detector.d.ts +2 -5
  2. package/dist/cmd/build/app-router-detector.d.ts.map +1 -1
  3. package/dist/cmd/build/app-router-detector.js +130 -154
  4. package/dist/cmd/build/app-router-detector.js.map +1 -1
  5. package/dist/cmd/build/ids.d.ts +11 -0
  6. package/dist/cmd/build/ids.d.ts.map +1 -0
  7. package/dist/cmd/build/ids.js +18 -0
  8. package/dist/cmd/build/ids.js.map +1 -0
  9. package/dist/cmd/build/vite/agent-discovery.d.ts +8 -4
  10. package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
  11. package/dist/cmd/build/vite/agent-discovery.js +166 -487
  12. package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
  13. package/dist/cmd/build/vite/bun-dev-server.d.ts +10 -16
  14. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
  15. package/dist/cmd/build/vite/bun-dev-server.js +67 -134
  16. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  17. package/dist/cmd/build/vite/docs-generator.d.ts.map +1 -1
  18. package/dist/cmd/build/vite/docs-generator.js +0 -2
  19. package/dist/cmd/build/vite/docs-generator.js.map +1 -1
  20. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  21. package/dist/cmd/build/vite/index.js +0 -36
  22. package/dist/cmd/build/vite/index.js.map +1 -1
  23. package/dist/cmd/build/vite/lifecycle-generator.d.ts +10 -2
  24. package/dist/cmd/build/vite/lifecycle-generator.d.ts.map +1 -1
  25. package/dist/cmd/build/vite/lifecycle-generator.js +302 -23
  26. package/dist/cmd/build/vite/lifecycle-generator.js.map +1 -1
  27. package/dist/cmd/build/vite/route-discovery.d.ts +11 -38
  28. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  29. package/dist/cmd/build/vite/route-discovery.js +97 -177
  30. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  31. package/dist/cmd/build/vite/server-bundler.js +1 -1
  32. package/dist/cmd/build/vite/server-bundler.js.map +1 -1
  33. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  34. package/dist/cmd/build/vite/static-renderer.js +1 -9
  35. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  36. package/dist/cmd/build/vite/vite-asset-server-config.d.ts +6 -3
  37. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  38. package/dist/cmd/build/vite/vite-asset-server-config.js +171 -18
  39. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  40. package/dist/cmd/build/vite/vite-asset-server.d.ts +8 -3
  41. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  42. package/dist/cmd/build/vite/vite-asset-server.js +14 -13
  43. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  44. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  45. package/dist/cmd/build/vite/vite-builder.js +6 -34
  46. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  47. package/dist/cmd/build/vite/ws-proxy.d.ts +53 -0
  48. package/dist/cmd/build/vite/ws-proxy.d.ts.map +1 -0
  49. package/dist/cmd/build/vite/ws-proxy.js +95 -0
  50. package/dist/cmd/build/vite/ws-proxy.js.map +1 -0
  51. package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
  52. package/dist/cmd/build/vite-bundler.js +0 -3
  53. package/dist/cmd/build/vite-bundler.js.map +1 -1
  54. package/dist/cmd/dev/file-watcher.d.ts.map +1 -1
  55. package/dist/cmd/dev/file-watcher.js +2 -8
  56. package/dist/cmd/dev/file-watcher.js.map +1 -1
  57. package/dist/cmd/dev/index.d.ts.map +1 -1
  58. package/dist/cmd/dev/index.js +369 -720
  59. package/dist/cmd/dev/index.js.map +1 -1
  60. package/package.json +6 -8
  61. package/src/cmd/ai/prompt/agent.md +0 -1
  62. package/src/cmd/ai/prompt/api.md +0 -7
  63. package/src/cmd/ai/prompt/web.md +51 -213
  64. package/src/cmd/build/app-router-detector.ts +152 -182
  65. package/src/cmd/build/ids.ts +19 -0
  66. package/src/cmd/build/vite/agent-discovery.ts +208 -679
  67. package/src/cmd/build/vite/bun-dev-server.ts +78 -154
  68. package/src/cmd/build/vite/docs-generator.ts +0 -2
  69. package/src/cmd/build/vite/index.ts +1 -42
  70. package/src/cmd/build/vite/lifecycle-generator.ts +345 -21
  71. package/src/cmd/build/vite/route-discovery.ts +116 -274
  72. package/src/cmd/build/vite/server-bundler.ts +1 -1
  73. package/src/cmd/build/vite/static-renderer.ts +1 -11
  74. package/src/cmd/build/vite/vite-asset-server-config.ts +196 -20
  75. package/src/cmd/build/vite/vite-asset-server.ts +25 -15
  76. package/src/cmd/build/vite/vite-builder.ts +6 -51
  77. package/src/cmd/build/vite/ws-proxy.ts +126 -0
  78. package/src/cmd/build/vite-bundler.ts +0 -4
  79. package/src/cmd/dev/file-watcher.ts +2 -9
  80. package/src/cmd/dev/index.ts +409 -832
  81. package/dist/cmd/build/ast.d.ts +0 -78
  82. package/dist/cmd/build/ast.d.ts.map +0 -1
  83. package/dist/cmd/build/ast.js +0 -2703
  84. package/dist/cmd/build/ast.js.map +0 -1
  85. package/dist/cmd/build/entry-generator.d.ts +0 -25
  86. package/dist/cmd/build/entry-generator.d.ts.map +0 -1
  87. package/dist/cmd/build/entry-generator.js +0 -695
  88. package/dist/cmd/build/entry-generator.js.map +0 -1
  89. package/dist/cmd/build/vite/api-mount-path.d.ts +0 -61
  90. package/dist/cmd/build/vite/api-mount-path.d.ts.map +0 -1
  91. package/dist/cmd/build/vite/api-mount-path.js +0 -83
  92. package/dist/cmd/build/vite/api-mount-path.js.map +0 -1
  93. package/dist/cmd/build/vite/registry-generator.d.ts +0 -19
  94. package/dist/cmd/build/vite/registry-generator.d.ts.map +0 -1
  95. package/dist/cmd/build/vite/registry-generator.js +0 -1108
  96. package/dist/cmd/build/vite/registry-generator.js.map +0 -1
  97. package/dist/cmd/build/webanalytics-generator.d.ts +0 -16
  98. package/dist/cmd/build/webanalytics-generator.d.ts.map +0 -1
  99. package/dist/cmd/build/webanalytics-generator.js +0 -178
  100. package/dist/cmd/build/webanalytics-generator.js.map +0 -1
  101. package/dist/cmd/build/workbench.d.ts +0 -7
  102. package/dist/cmd/build/workbench.d.ts.map +0 -1
  103. package/dist/cmd/build/workbench.js +0 -55
  104. package/dist/cmd/build/workbench.js.map +0 -1
  105. package/dist/utils/route-migration.d.ts +0 -62
  106. package/dist/utils/route-migration.d.ts.map +0 -1
  107. package/dist/utils/route-migration.js +0 -630
  108. package/dist/utils/route-migration.js.map +0 -1
  109. package/src/cmd/build/ast.ts +0 -3529
  110. package/src/cmd/build/entry-generator.ts +0 -760
  111. package/src/cmd/build/vite/api-mount-path.ts +0 -87
  112. package/src/cmd/build/vite/registry-generator.ts +0 -1267
  113. package/src/cmd/build/webanalytics-generator.ts +0 -197
  114. package/src/cmd/build/workbench.ts +0 -58
  115. package/src/utils/route-migration.ts +0 -757
@@ -1,1267 +0,0 @@
1
- /**
2
- * Registry Generator
3
- *
4
- * Generates src/generated/registry.ts from discovered agents
5
- */
6
-
7
- import { join, dirname, relative, resolve } from 'node:path';
8
- import { writeFileSync, mkdirSync, existsSync, unlinkSync, readFileSync } from 'node:fs';
9
- import { stat } from 'node:fs/promises';
10
- import { StructuredError } from '@agentuity/core';
11
- import { toCamelCase, toPascalCase } from '../../../utils/string';
12
- import { toForwardSlash } from '../../../utils/normalize-path';
13
- import type { AgentMetadata } from './agent-discovery';
14
- import type { RouteInfo } from './route-discovery';
15
-
16
- /**
17
- * Rebase a relative import path from the route file's location to the generated file's location.
18
- * @param routeFilename - The route file path (e.g., 'api/example/route.ts' or './api/example/route.ts')
19
- * @param schemaImportPath - The import path as written in the route file (e.g., '../../utils/schemas')
20
- * @param srcDir - The src directory path
21
- * @returns The rebased import path relative to src/generated/
22
- */
23
- function rebaseImportPath(routeFilename: string, schemaImportPath: string, srcDir: string): string {
24
- // Non-relative imports (bare modules like '@company/schemas') should be used as-is
25
- if (!schemaImportPath.startsWith('.') && !schemaImportPath.startsWith('/')) {
26
- return schemaImportPath;
27
- }
28
-
29
- // Normalize route filename to get its directory relative to srcDir
30
- let routeDir: string;
31
- const cleanFilename = toForwardSlash(routeFilename);
32
- if (cleanFilename.startsWith('./')) {
33
- routeDir = dirname(join(srcDir, cleanFilename.substring(2)));
34
- } else if (cleanFilename.startsWith('src/')) {
35
- routeDir = dirname(join(srcDir, '..', cleanFilename));
36
- } else {
37
- routeDir = dirname(join(srcDir, cleanFilename));
38
- }
39
-
40
- // Resolve the schema module path from the route file's directory
41
- const resolvedSchemaPath = resolve(routeDir, schemaImportPath);
42
-
43
- // Calculate the relative path from src/generated/ to the resolved schema path
44
- const generatedDir = join(srcDir, 'generated');
45
- let rebasedPath = toForwardSlash(relative(generatedDir, resolvedSchemaPath));
46
-
47
- // Ensure it starts with './' or '../'
48
- if (!rebasedPath.startsWith('.') && !rebasedPath.startsWith('/')) {
49
- rebasedPath = './' + rebasedPath;
50
- }
51
-
52
- return rebasedPath;
53
- }
54
-
55
- const AgentIdentifierCollisionError = StructuredError('AgentIdentifierCollisionError');
56
-
57
- /**
58
- * Regex to strip route parameter characters that produce invalid TypeScript property names:
59
- * - Leading : (path parameters, e.g., :id)
60
- * - Leading * (wildcard routes, e.g., *path)
61
- * - Trailing ?, +, * (optional/one-or-more/wildcard modifiers, e.g., :userId?)
62
- */
63
- const ROUTE_PARAM_CHARS = /^[:*]|[?+*]$/g;
64
-
65
- /**
66
- * Sanitize a route path segment for use as a TypeScript property name.
67
- * Strips route parameter characters and converts to camelCase.
68
- */
69
- function sanitizePathSegment(segment: string): string {
70
- return toCamelCase(segment.replace(ROUTE_PARAM_CHARS, ''));
71
- }
72
-
73
- /**
74
- * Valid unquoted TypeScript/JavaScript property name pattern.
75
- * A property name can be unquoted if it starts with a letter, underscore, or dollar sign,
76
- * and contains only letters, digits, underscores, or dollar signs.
77
- */
78
- const VALID_UNQUOTED_PROPERTY = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
79
-
80
- /**
81
- * Quote a property name for TypeScript object type definitions if it contains
82
- * characters that require quoting (e.g., dots, hyphens, spaces).
83
- */
84
- function quotePropertyName(name: string): string {
85
- return VALID_UNQUOTED_PROPERTY.test(name) ? name : JSON.stringify(name);
86
- }
87
-
88
- /**
89
- * Generate TypeScript type for path parameters.
90
- * Returns 'never' if no path params, or '{ param1: string; param2: string }' format.
91
- */
92
- function generatePathParamsType(pathParams?: string[]): string {
93
- if (!pathParams || pathParams.length === 0) {
94
- return 'never';
95
- }
96
- return `{ ${pathParams.map((p) => `${p}: string`).join('; ')} }`;
97
- }
98
-
99
- /**
100
- * Generate TypeScript tuple type for path parameters (for positional args).
101
- * Returns '[]' if no path params, or '[string, string]' format.
102
- */
103
- function generatePathParamsTupleType(pathParams?: string[]): string {
104
- if (!pathParams || pathParams.length === 0) {
105
- return '[]';
106
- }
107
- return `[${pathParams.map(() => 'string').join(', ')}]`;
108
- }
109
-
110
- /**
111
- * Generate src/generated/registry.ts with agent registry and types
112
- */
113
- export function generateAgentRegistry(srcDir: string, agents: AgentMetadata[]): void {
114
- const generatedDir = join(srcDir, 'generated');
115
- const registryPath = join(generatedDir, 'registry.ts');
116
-
117
- // Sort agents by name for deterministic output
118
- const sortedAgents = [...agents].sort((a, b) => a.name.localeCompare(b.name));
119
-
120
- // Detect naming collisions in generated identifiers
121
- const generatedNames = new Set<string>();
122
- const collisions: string[] = [];
123
-
124
- for (const agent of sortedAgents) {
125
- const camelName = toCamelCase(agent.name);
126
-
127
- if (generatedNames.has(camelName)) {
128
- collisions.push(`Identifier collision detected: "${camelName}" (from "${agent.name}")`);
129
- }
130
- generatedNames.add(camelName);
131
- }
132
-
133
- if (collisions.length > 0) {
134
- throw new AgentIdentifierCollisionError({
135
- message:
136
- `Agent identifier naming collisions detected:\n${collisions.join('\n')}\n\n` +
137
- `This occurs when different agent names produce the same camelCase identifier.\n` +
138
- `Please rename your agents to avoid this collision.`,
139
- });
140
- }
141
-
142
- // Collect eval files that need to be imported for createEval calls to run
143
- // These are eval.ts files in the same directory as agents that have evals
144
- const evalImports: string[] = [];
145
- const seenEvalPaths = new Set<string>();
146
-
147
- for (const agent of sortedAgents) {
148
- if (agent.evals && agent.evals.length > 0) {
149
- // Check if any eval comes from a separate eval.ts file (not the agent file itself)
150
- for (const evalMeta of agent.evals) {
151
- // Skip if eval is defined in the agent file itself
152
- if (evalMeta.filename === agent.filename) continue;
153
-
154
- // Build the relative path for the eval file
155
- let evalRelativePath = toForwardSlash(evalMeta.filename);
156
- if (evalRelativePath.startsWith('./agent/')) {
157
- evalRelativePath = evalRelativePath
158
- .replace(/^\.\/agent\//, '../agent/')
159
- .replace(/\.tsx?$/, '.js');
160
- } else if (evalRelativePath.startsWith('src/agent/')) {
161
- evalRelativePath = evalRelativePath
162
- .replace(/^src\/agent\//, '../agent/')
163
- .replace(/\.tsx?$/, '.js');
164
- } else if (evalRelativePath.includes('/src/agent/')) {
165
- // Handle absolute paths by extracting the relative part
166
- evalRelativePath = evalRelativePath
167
- .replace(/^.*\/src\/agent\//, '../agent/')
168
- .replace(/\.tsx?$/, '.js');
169
- }
170
- // Avoid duplicate imports
171
- if (!seenEvalPaths.has(evalRelativePath)) {
172
- seenEvalPaths.add(evalRelativePath);
173
- evalImports.push(`import '${evalRelativePath}';`);
174
- }
175
- }
176
- }
177
- }
178
-
179
- // Generate imports for all agents
180
- const imports = sortedAgents
181
- .map(({ name, filename }) => {
182
- const camelName = toCamelCase(name);
183
- // Handle both './agent/...' and 'src/agent/...' formats
184
- let relativePath = toForwardSlash(filename);
185
- if (relativePath.startsWith('./agent/')) {
186
- // ./agent/foo.ts -> ../agent/foo.js (use .js extension for TypeScript)
187
- relativePath = relativePath
188
- .replace(/^\.\/agent\//, '../agent/')
189
- .replace(/\.tsx?$/, '.js');
190
- } else if (relativePath.startsWith('src/agent/')) {
191
- // src/agent/foo.ts -> ../agent/foo.js (use .js extension for TypeScript)
192
- relativePath = relativePath
193
- .replace(/^src\/agent\//, '../agent/')
194
- .replace(/\.tsx?$/, '.js');
195
- }
196
- return `import ${camelName} from '${relativePath}';`;
197
- })
198
- .join('\n');
199
-
200
- // Generate schema type exports for all agents
201
- const schemaTypeExports = sortedAgents
202
- .map(({ name, description }) => {
203
- const camelName = toCamelCase(name);
204
- const pascalName = toPascalCase(name);
205
- const descComment = description ? `\n * ${description}` : '';
206
-
207
- const parts = [
208
- '',
209
- `/**`,
210
- ` * Input type for ${name} agent${descComment}`,
211
- ` */`,
212
- `export type ${pascalName}Input = InferInput<typeof ${camelName}['inputSchema']>;`,
213
- '',
214
- `/**`,
215
- ` * Output type for ${name} agent${descComment}`,
216
- ` */`,
217
- `export type ${pascalName}Output = InferOutput<typeof ${camelName}['outputSchema']>;`,
218
- '',
219
- `/**`,
220
- ` * Input schema type for ${name} agent${descComment}`,
221
- ` */`,
222
- `export type ${pascalName}InputSchema = typeof ${camelName}['inputSchema'];`,
223
- '',
224
- `/**`,
225
- ` * Output schema type for ${name} agent${descComment}`,
226
- ` */`,
227
- `export type ${pascalName}OutputSchema = typeof ${camelName}['outputSchema'];`,
228
- '',
229
- `/**`,
230
- ` * Agent type for ${name}${descComment}`,
231
- ` */`,
232
- `export type ${pascalName}Agent = AgentRunner<`,
233
- `\t${pascalName}InputSchema,`,
234
- `\t${pascalName}OutputSchema,`,
235
- `\ttypeof ${camelName}['stream'] extends true ? true : false`,
236
- `>;`,
237
- ];
238
- return parts.join('\n');
239
- })
240
- .join('\n');
241
-
242
- // Generate flat registry structure with JSDoc
243
- const registry = sortedAgents
244
- .map(({ name, description }) => {
245
- const camelName = toCamelCase(name);
246
- const pascalName = toPascalCase(name);
247
- const descComment = description ? `\n\t * ${description}` : '';
248
-
249
- return `\t/**
250
- \t * ${name}${descComment}
251
- \t * @type {${pascalName}Agent}
252
- \t */
253
- \t${camelName},`;
254
- })
255
- .join('\n');
256
-
257
- // Generate flat agent type definitions for AgentRegistry interface augmentation
258
- // Uses the exported Agent types defined above
259
- const runtimeAgentTypes = sortedAgents
260
- .map(({ name }) => {
261
- const camelName = toCamelCase(name);
262
- const pascalName = toPascalCase(name);
263
- return ` ${camelName}: ${pascalName}Agent;`;
264
- })
265
- .join('\n');
266
-
267
- // Build eval imports section (side-effect imports for createEval registration)
268
- const evalImportsSection =
269
- evalImports.length > 0
270
- ? `
271
- // Eval file imports (side-effect imports to register evals via createEval)
272
- ${evalImports.join('\n')}
273
- `
274
- : '';
275
-
276
- const generatedContent = `// @generated
277
- // Auto-generated by Agentuity - DO NOT EDIT
278
- ${imports}
279
- import type { AgentRunner } from '@agentuity/runtime';
280
- import type { InferInput, InferOutput } from '@agentuity/core';
281
- ${evalImportsSection}
282
-
283
- // ============================================================================
284
- // Schema Type Exports
285
- // ============================================================================
286
- ${schemaTypeExports}
287
-
288
- // ============================================================================
289
- // Agent Definitions
290
- // ============================================================================
291
-
292
- /**
293
- * Agent Definitions
294
- *
295
- * Registry of all agents in this application.
296
- * Provides strongly-typed access to agent metadata and runner functions.
297
- *
298
- * @remarks
299
- * This object is auto-generated from your agent files during build.
300
- * Each agent has corresponding Input, Output, and Runner types exported above.
301
- *
302
- * @example
303
- * \`\`\`typescript
304
- * import { AgentDefinitions, SessionBasicInput } from './generated/registry';
305
- *
306
- * // Access agent definition
307
- * const agent = AgentDefinitions.sessionBasic;
308
- *
309
- * // Use typed schema types
310
- * const input: SessionBasicInput = { ... };
311
- * const result = await agent.run(input);
312
- * \`\`\`
313
- */
314
- export const AgentDefinitions = {
315
- ${registry}
316
- } as const;
317
-
318
- // ============================================================================
319
- // Module Augmentation
320
- // ============================================================================
321
-
322
- // Augment @agentuity/runtime types with strongly-typed agents from this project
323
- declare module "@agentuity/runtime" {
324
- // Augment the AgentRegistry interface with project-specific strongly-typed agents
325
- export interface AgentRegistry {
326
- ${runtimeAgentTypes}
327
- }
328
- }
329
-
330
- // FOUND AN ERROR IN THIS FILE?
331
- // Please file an issue at https://github.com/agentuity/sdk/issues
332
- // or if you know the fix please submit a PR!
333
- `;
334
-
335
- const agentsDir = join(srcDir, 'agent');
336
- const legacyTypesPath = join(agentsDir, 'types.generated.d.ts');
337
-
338
- // Ensure src/generated directory exists
339
- if (!existsSync(generatedDir)) {
340
- mkdirSync(generatedDir, { recursive: true });
341
- }
342
-
343
- // Collapse 2+ consecutive empty lines into 1 empty line (3+ \n becomes 2 \n)
344
- const cleanedContent = generatedContent.replace(/\n{3,}/g, '\n\n');
345
-
346
- writeFileSync(registryPath, cleanedContent, 'utf-8');
347
-
348
- // Remove legacy types.generated.d.ts if it exists (legacy cleanup)
349
- if (existsSync(legacyTypesPath)) {
350
- unlinkSync(legacyTypesPath);
351
- }
352
- }
353
-
354
- /**
355
- * Helper function to generate RPC-style nested registry type.
356
- * Converts routes like "POST /api/hello" to nested structure: post.api.hello
357
- */
358
- function generateRPCRegistryType(
359
- apiRoutes: RouteInfo[],
360
- websocketRoutes: RouteInfo[],
361
- sseRoutes: RouteInfo[],
362
- agentImports: Map<string, string>,
363
- _schemaImportAliases: Map<string, Map<string, string>>,
364
- agentMetadataMap: Map<string, AgentMetadata>
365
- ): string {
366
- // Build nested structure from routes
367
- interface NestedNode {
368
- [key: string]: NestedNode | { input: string; output: string; type: string; route: RouteInfo };
369
- }
370
-
371
- const tree: NestedNode = {};
372
-
373
- // Helper to add route to tree
374
- const addRoute = (route: RouteInfo, routeType: 'api' | 'websocket' | 'sse' | 'stream') => {
375
- const method = route.method.toLowerCase();
376
-
377
- // Strip /api prefix from path
378
- let cleanPath = route.path;
379
- if (cleanPath.startsWith('/api/')) {
380
- cleanPath = cleanPath.substring(4); // Remove '/api'
381
- } else if (cleanPath === '/api') {
382
- cleanPath = '/';
383
- }
384
-
385
- const pathParts = cleanPath.split('/').filter(Boolean);
386
-
387
- // Navigate/create tree structure: path segments first, then method
388
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
389
- let current: any = tree;
390
-
391
- // Add path segments - sanitize for valid TypeScript property names
392
- for (let i = 0; i < pathParts.length; i++) {
393
- const rawPart = pathParts[i];
394
- if (!rawPart) {
395
- continue;
396
- }
397
- const part = sanitizePathSegment(rawPart);
398
- // Skip empty segments (e.g., wildcards like '*' that sanitize to '')
399
- if (!part) {
400
- continue;
401
- }
402
- if (!current[part]) {
403
- current[part] = {};
404
- }
405
- current = current[part];
406
- }
407
-
408
- // Determine terminal method name based on route type
409
- // For stream types (websocket, sse, stream), use the type name as the method
410
- // For regular API routes, use the HTTP method
411
- const terminalMethod =
412
- routeType === 'websocket'
413
- ? 'websocket'
414
- : routeType === 'sse'
415
- ? 'eventstream'
416
- : routeType === 'stream'
417
- ? 'stream'
418
- : method;
419
-
420
- // Add method as final level with schema types
421
- const routeKey = `${route.method.toUpperCase()} ${route.path}`;
422
- const safeName = routeKey
423
- .replace(/[^a-zA-Z0-9]/g, '_')
424
- .replace(/^_+|_+$/g, '')
425
- .replace(/_+/g, '_');
426
- const pascalName = toPascalCase(safeName);
427
-
428
- // Only reference type names if route has actual schemas extracted, otherwise use 'never'
429
- // Note: hasValidator may be true (e.g., zValidator('query', ...)) but no schemas extracted
430
- // because only 'json' validators extract input schemas
431
- // Also check if agentVariable exists but import wasn't added (missing agentImportPath)
432
- const hasValidAgentImport = route.agentVariable
433
- ? !!agentImports.get(route.agentVariable)
434
- : false;
435
- const hasSchemas =
436
- route.inputSchemaVariable || route.outputSchemaVariable || hasValidAgentImport;
437
-
438
- current[terminalMethod] = {
439
- input: hasSchemas ? `${pascalName}Input` : 'never',
440
- output: hasSchemas ? `${pascalName}Output` : 'never',
441
- type: `'${routeType}'`,
442
- route,
443
- };
444
- };
445
-
446
- // Add all routes with their types
447
- apiRoutes.forEach((route) => {
448
- const routeType = route.routeType === 'stream' ? 'stream' : 'api';
449
- addRoute(route, routeType);
450
- });
451
- websocketRoutes.forEach((route) => addRoute(route, 'websocket'));
452
- sseRoutes.forEach((route) => addRoute(route, 'sse'));
453
-
454
- // Convert tree to TypeScript type string
455
- function treeToTypeString(node: NestedNode, indent: string = '\t\t'): string {
456
- const lines: string[] = [];
457
-
458
- // Sort entries alphabetically for deterministic output
459
- const sortedEntries = Object.entries(node).sort(([a], [b]) => a.localeCompare(b));
460
- for (const [key, value] of sortedEntries) {
461
- if (
462
- value &&
463
- typeof value === 'object' &&
464
- 'input' in value &&
465
- 'output' in value &&
466
- 'type' in value &&
467
- 'route' in value
468
- ) {
469
- // Leaf node with schema and type - add JSDoc
470
- const route = value.route;
471
- const jsdoc: string[] = [];
472
-
473
- // Access route info from value
474
- const routeInfo = route as RouteInfo;
475
-
476
- // Look up agent metadata
477
- let agentMeta: AgentMetadata | undefined;
478
- if (routeInfo.agentVariable) {
479
- agentMeta = agentMetadataMap.get(routeInfo.agentVariable);
480
- }
481
-
482
- // Build JSDoc comment
483
- jsdoc.push(`${indent}/**`);
484
- jsdoc.push(`${indent} * Route: ${routeInfo.method.toUpperCase()} ${routeInfo.path}`);
485
- if (agentMeta?.name) {
486
- jsdoc.push(`${indent} * @agent ${agentMeta.name}`);
487
- }
488
- if (agentMeta?.description) {
489
- jsdoc.push(`${indent} * @description ${agentMeta.description}`);
490
- }
491
- jsdoc.push(`${indent} */`);
492
- lines.push(...jsdoc);
493
-
494
- const pathParamsType = generatePathParamsType(routeInfo.pathParams);
495
- const pathParamsTupleType = generatePathParamsTupleType(routeInfo.pathParams);
496
- lines.push(
497
- `${indent}${quotePropertyName(key)}: { input: ${value.input}; output: ${value.output}; type: ${value.type}; params: ${pathParamsType}; paramsTuple: ${pathParamsTupleType} };`
498
- );
499
- } else {
500
- // Nested node
501
- lines.push(`${indent}${quotePropertyName(key)}: {`);
502
- lines.push(treeToTypeString(value as NestedNode, indent + '\t'));
503
- lines.push(`${indent}};`);
504
- }
505
- }
506
-
507
- return lines.join('\n');
508
- }
509
-
510
- if (Object.keys(tree).length === 0) {
511
- return '\t\t// No routes discovered';
512
- }
513
-
514
- return treeToTypeString(tree);
515
- }
516
-
517
- /**
518
- * Generate runtime metadata object for RPC routes.
519
- * This allows the client to know route types at runtime.
520
- */
521
- function generateRPCRuntimeMetadata(
522
- apiRoutes: RouteInfo[],
523
- websocketRoutes: RouteInfo[],
524
- sseRoutes: RouteInfo[]
525
- ): string {
526
- interface MetadataNode {
527
- [key: string]: MetadataNode | { type: string; path: string; pathParams?: string[] };
528
- }
529
-
530
- const tree: MetadataNode = {};
531
-
532
- const addRoute = (route: RouteInfo, routeType: string) => {
533
- let cleanPath = route.path;
534
- if (cleanPath.startsWith('/api/')) {
535
- cleanPath = cleanPath.substring(4);
536
- } else if (cleanPath === '/api') {
537
- cleanPath = '/';
538
- }
539
-
540
- const pathParts = cleanPath.split('/').filter(Boolean);
541
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
542
- let current: any = tree;
543
-
544
- // Sanitize path segments for valid property names (must match type generation)
545
- for (const part of pathParts) {
546
- const sanitized = sanitizePathSegment(part);
547
- // Skip empty segments (e.g., wildcards like '*' that sanitize to '')
548
- if (!sanitized) {
549
- continue;
550
- }
551
- if (!current[sanitized]) current[sanitized] = {};
552
- current = current[sanitized];
553
- }
554
-
555
- // Use terminal method name based on route type
556
- const terminalMethod =
557
- routeType === 'websocket'
558
- ? 'websocket'
559
- : routeType === 'sse'
560
- ? 'eventstream'
561
- : routeType === 'stream'
562
- ? 'stream'
563
- : route.method.toLowerCase();
564
-
565
- const metadata: { type: string; path: string; pathParams?: string[] } = {
566
- type: routeType,
567
- path: route.path,
568
- };
569
- if (route.pathParams && route.pathParams.length > 0) {
570
- metadata.pathParams = route.pathParams;
571
- }
572
- current[terminalMethod] = metadata;
573
- };
574
-
575
- apiRoutes.forEach((r) => addRoute(r, r.routeType === 'stream' ? 'stream' : 'api'));
576
- websocketRoutes.forEach((r) => addRoute(r, 'websocket'));
577
- sseRoutes.forEach((r) => addRoute(r, 'sse'));
578
-
579
- // Sort object keys recursively for deterministic output
580
- const sortObject = (obj: MetadataNode): MetadataNode => {
581
- const sorted: MetadataNode = {};
582
- for (const key of Object.keys(obj).sort()) {
583
- const value = obj[key];
584
- if (value === undefined) {
585
- continue;
586
- }
587
- if (typeof value === 'object' && !('type' in value)) {
588
- sorted[key] = sortObject(value as MetadataNode);
589
- } else {
590
- sorted[key] = value;
591
- }
592
- }
593
- return sorted;
594
- };
595
-
596
- return JSON.stringify(sortObject(tree), null, '\t\t');
597
- }
598
-
599
- /**
600
- * Generate RouteRegistry type definitions from discovered routes.
601
- *
602
- * Creates a module augmentation for @agentuity/react that provides
603
- * strongly-typed route keys with input/output schema information.
604
- */
605
- export async function generateRouteRegistry(
606
- srcDir: string,
607
- routes: RouteInfo[],
608
- agents: AgentMetadata[] = []
609
- ): Promise<void> {
610
- const projectRoot = join(srcDir, '..');
611
- const packageJsonPath = join(projectRoot, 'package.json');
612
- let hasReactDependency = false;
613
- let hasFrontendDependency = false;
614
-
615
- try {
616
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
617
- hasReactDependency = !!(
618
- packageJson.dependencies?.['@agentuity/react'] ||
619
- packageJson.devDependencies?.['@agentuity/react']
620
- );
621
- hasFrontendDependency = !!(
622
- packageJson.dependencies?.['@agentuity/frontend'] ||
623
- packageJson.devDependencies?.['@agentuity/frontend']
624
- );
625
- } catch {
626
- // If we can't read package.json, assume no frontend dependencies
627
- }
628
-
629
- const webDir = join(srcDir, 'web');
630
- let hasWebDirectory = false;
631
- try {
632
- const webDirStat = await stat(webDir);
633
- hasWebDirectory = webDirStat.isDirectory();
634
- } catch {
635
- // Directory doesn't exist
636
- }
637
-
638
- const shouldEmitFrontendClient = hasFrontendDependency && !hasReactDependency && hasWebDirectory;
639
-
640
- // Filter routes by type and sort by path for deterministic output
641
- const sortByPath = (a: RouteInfo, b: RouteInfo) => a.path.localeCompare(b.path);
642
- const apiRoutes = routes
643
- .filter((r) => r.routeType === 'api' || r.routeType === 'stream')
644
- .sort(sortByPath);
645
- const websocketRoutes = routes.filter((r) => r.routeType === 'websocket').sort(sortByPath);
646
- const sseRoutes = routes.filter((r) => r.routeType === 'sse').sort(sortByPath);
647
-
648
- const allRoutes = [...apiRoutes, ...websocketRoutes, ...sseRoutes];
649
-
650
- // Create maps for agent metadata lookup
651
- const agentMetadataMap = new Map<string, AgentMetadata>();
652
- const agentNameMap = new Map<string, AgentMetadata>();
653
-
654
- // Map by agent name for easy lookup
655
- agents.forEach((agent) => {
656
- agentNameMap.set(agent.name, agent);
657
- });
658
-
659
- // Map agent import variables to metadata by extracting agent name from import path
660
- allRoutes.forEach((route) => {
661
- if (route.agentVariable && route.agentImportPath) {
662
- // Extract agent name from import path (e.g., "@agent/hello" -> "hello")
663
- const match = route.agentImportPath.match(/@agent[s]?\/([^/]+)/);
664
- if (match) {
665
- const agentName = match[1];
666
- if (agentName) {
667
- const metadata = agentNameMap.get(agentName);
668
- if (metadata) {
669
- agentMetadataMap.set(route.agentVariable, metadata);
670
- }
671
- }
672
- }
673
- }
674
- });
675
-
676
- if (apiRoutes.length === 0 && websocketRoutes.length === 0 && sseRoutes.length === 0) {
677
- // Clean up stale routes.ts from previous builds (issue #924)
678
- // When all API routes are removed, the old file would reference deleted modules
679
- const generatedDir = join(srcDir, 'generated');
680
- const registryPath = join(generatedDir, 'routes.ts');
681
- if (existsSync(registryPath)) {
682
- unlinkSync(registryPath);
683
- }
684
- return;
685
- }
686
-
687
- // Generate imports for agents and schemas
688
- const imports: string[] = [];
689
- const agentImports = new Map<string, string>();
690
- const routeFileImports = new Map<string, Set<string>>();
691
- // Track per-route which import path and schema name to use for alias lookup
692
- const routeSchemaImportInfo = new Map<string, { importPath: string; schemaName: string }>();
693
-
694
- // Collect agent and schema imports from routes with validators or exported schemas
695
- allRoutes.forEach((route) => {
696
- const hasSchemaVars = !!route.inputSchemaVariable || !!route.outputSchemaVariable;
697
- if (!route.hasValidator && !hasSchemaVars && !route.agentVariable) return;
698
-
699
- // Collect agent imports (when using agent.validator())
700
- if (
701
- route.hasValidator &&
702
- route.agentVariable &&
703
- route.agentImportPath &&
704
- !agentImports.has(route.agentVariable)
705
- ) {
706
- let resolvedPath = route.agentImportPath;
707
-
708
- if (resolvedPath.startsWith('@agents/') || resolvedPath.startsWith('@agent/')) {
709
- // Handle both @agents/ and @agent/ aliases -> ../agent/
710
- const suffix = resolvedPath.startsWith('@agents/')
711
- ? resolvedPath.substring('@agents/'.length)
712
- : resolvedPath.substring('@agent/'.length);
713
-
714
- // Convert @agent/hello -> ../agent/hello/index.js
715
- // Convert @agent/hello/agent -> ../agent/hello/agent.js
716
- if (!suffix.includes('/')) {
717
- // Bare module (e.g., @agent/hello) - add /index.js
718
- resolvedPath = `../agent/${suffix}/index.js`;
719
- } else {
720
- // File path (e.g., @agent/hello/agent) - add .js
721
- const finalPath = suffix.endsWith('.js')
722
- ? suffix
723
- : suffix.replace(/\.tsx?$/, '') + '.js';
724
- resolvedPath = `../agent/${finalPath}`;
725
- }
726
- } else if (resolvedPath.startsWith('@api/')) {
727
- // src/generated/ -> src/api/ is ../api/
728
- const suffix = resolvedPath.substring('@api/'.length);
729
- const finalPath = suffix.endsWith('.js')
730
- ? suffix
731
- : suffix.replace(/\.tsx?$/, '') + '.js';
732
- resolvedPath = `../api/${finalPath}`;
733
- } else if (resolvedPath.startsWith('./') || resolvedPath.startsWith('../')) {
734
- // Resolve relative import from route file's directory
735
- // Use schemaSourceFile when available (sub-router routes)
736
- const normalizedFilename = toForwardSlash(route.schemaSourceFile ?? route.filename);
737
- const routeDir = normalizedFilename.substring(0, normalizedFilename.lastIndexOf('/'));
738
- // Join and normalize the path
739
- const joined = `${routeDir}/${resolvedPath}`;
740
- // Normalize by resolving .. and . segments
741
- const normalized = joined
742
- .split('/')
743
- .reduce((acc: string[], segment) => {
744
- if (segment === '..') {
745
- acc.pop();
746
- } else if (segment !== '.' && segment !== '') {
747
- acc.push(segment);
748
- }
749
- return acc;
750
- }, [])
751
- .join('/');
752
- // Remove 'src/' prefix if present (routes are in src/, generated is in src/generated/)
753
- const withoutSrc = normalized.startsWith('src/') ? normalized.substring(4) : normalized;
754
- // Make it relative from src/generated/
755
- resolvedPath = `../${withoutSrc}`;
756
- // Check if this is a directory import (no file extension) vs a file import
757
- // Directory imports like '../agent/translate' should resolve to '../agent/translate/index.js'
758
- // File imports like '../agent/translate/agent' should resolve to '../agent/translate/agent.js'
759
- const hasExtension = /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(resolvedPath);
760
- if (!hasExtension) {
761
- // No extension - check if it's a directory or a file
762
- // Try to resolve the actual path on disk to determine
763
- const absolutePath = join(srcDir, withoutSrc);
764
- const isDirectory =
765
- existsSync(absolutePath) ||
766
- existsSync(join(absolutePath, 'index.ts')) ||
767
- existsSync(join(absolutePath, 'index.tsx'));
768
- const isFile = existsSync(`${absolutePath}.ts`) || existsSync(`${absolutePath}.tsx`);
769
-
770
- if (isDirectory && !isFile) {
771
- // It's a directory import, add /index.js
772
- resolvedPath = `${resolvedPath}/index.js`;
773
- } else {
774
- // It's a file import (or we can't determine), add .js
775
- resolvedPath = `${resolvedPath}.js`;
776
- }
777
- } else {
778
- // Has extension - replace with .js
779
- resolvedPath = resolvedPath.replace(/\.tsx?$/, '.js');
780
- }
781
- }
782
-
783
- const uniqueImportName = route.agentVariable;
784
- imports.push(`import type ${uniqueImportName} from '${resolvedPath}';`);
785
- agentImports.set(route.agentVariable, uniqueImportName);
786
- }
787
-
788
- // Collect schema variable imports
789
- // If the schema is imported from another file, use that file's path (rebased)
790
- // Otherwise fall back to the route file path (for locally defined schemas)
791
- if (route.inputSchemaVariable) {
792
- let importPath: string;
793
- let schemaNameToImport: string;
794
-
795
- if (route.inputSchemaImportPath) {
796
- // Schema is imported - rebase the import path from route file to generated file
797
- // Use schemaSourceFile if available (sub-router routes carry the original file)
798
- const sourceFile = route.schemaSourceFile ?? route.filename;
799
- importPath = rebaseImportPath(sourceFile, route.inputSchemaImportPath, srcDir);
800
- // Use the actual exported name (handles aliased imports like `import { A as B }`)
801
- schemaNameToImport =
802
- route.inputSchemaImportedName === 'default'
803
- ? route.inputSchemaVariable
804
- : (route.inputSchemaImportedName ?? route.inputSchemaVariable);
805
- } else {
806
- // Schema is locally defined - import from the file that defines/exports it
807
- // When route is mounted via .route(), schemaSourceFile points to the sub-router file
808
- const sourceFile = route.schemaSourceFile ?? route.filename;
809
- const filename = toForwardSlash(sourceFile);
810
- const withoutSrc = filename.startsWith('src/') ? filename.substring(4) : filename;
811
- const withoutLeadingDot = withoutSrc.startsWith('./')
812
- ? withoutSrc.substring(2)
813
- : withoutSrc;
814
- importPath = `../${withoutLeadingDot.replace(/\.ts$/, '')}`;
815
- schemaNameToImport = route.inputSchemaVariable;
816
- }
817
-
818
- if (!routeFileImports.has(importPath)) {
819
- routeFileImports.set(importPath, new Set());
820
- }
821
- routeFileImports.get(importPath)!.add(schemaNameToImport);
822
-
823
- // Store the resolved import info for later alias lookup
824
- routeSchemaImportInfo.set(`input:${route.path}:${route.method}`, {
825
- importPath,
826
- schemaName: schemaNameToImport,
827
- });
828
- }
829
-
830
- if (route.outputSchemaVariable) {
831
- let importPath: string;
832
- let schemaNameToImport: string;
833
-
834
- if (route.outputSchemaImportPath) {
835
- // Schema is imported - rebase the import path from route file to generated file
836
- const sourceFile = route.schemaSourceFile ?? route.filename;
837
- importPath = rebaseImportPath(sourceFile, route.outputSchemaImportPath, srcDir);
838
- // Use the actual exported name (handles aliased imports like `import { A as B }`)
839
- schemaNameToImport =
840
- route.outputSchemaImportedName === 'default'
841
- ? route.outputSchemaVariable
842
- : (route.outputSchemaImportedName ?? route.outputSchemaVariable);
843
- } else {
844
- // Schema is locally defined - import from the file that defines/exports it
845
- const sourceFile = route.schemaSourceFile ?? route.filename;
846
- const filename = toForwardSlash(sourceFile);
847
- const withoutSrc = filename.startsWith('src/') ? filename.substring(4) : filename;
848
- const withoutLeadingDot = withoutSrc.startsWith('./')
849
- ? withoutSrc.substring(2)
850
- : withoutSrc;
851
- importPath = `../${withoutLeadingDot.replace(/\.ts$/, '')}`;
852
- schemaNameToImport = route.outputSchemaVariable;
853
- }
854
-
855
- if (!routeFileImports.has(importPath)) {
856
- routeFileImports.set(importPath, new Set());
857
- }
858
- routeFileImports.get(importPath)!.add(schemaNameToImport);
859
-
860
- // Store the resolved import info for later alias lookup
861
- routeSchemaImportInfo.set(`output:${route.path}:${route.method}`, {
862
- importPath,
863
- schemaName: schemaNameToImport,
864
- });
865
- }
866
- });
867
-
868
- // Generate schema imports, only aliasing when names collide across files
869
- const schemaImportAliases = new Map<string, Map<string, string>>(); // importPath -> (schemaName -> alias)
870
-
871
- // First pass: count how many times each schema name appears across all import paths
872
- const globalNameCount = new Map<string, number>();
873
- routeFileImports.forEach((schemas) => {
874
- for (const schemaName of schemas) {
875
- globalNameCount.set(schemaName, (globalNameCount.get(schemaName) ?? 0) + 1);
876
- }
877
- });
878
-
879
- // Track aliases assigned to duplicated names for uniqueness
880
- const duplicateCounters = new Map<string, number>();
881
-
882
- routeFileImports.forEach((schemas, importPath) => {
883
- const aliases = new Map<string, string>();
884
- const importParts: string[] = [];
885
-
886
- for (const schemaName of Array.from(schemas)) {
887
- if ((globalNameCount.get(schemaName) ?? 0) > 1) {
888
- // Name appears in multiple import paths — alias to avoid collision
889
- const counter = duplicateCounters.get(schemaName) ?? 0;
890
- duplicateCounters.set(schemaName, counter + 1);
891
- const alias = `${schemaName}_${counter}`;
892
- aliases.set(schemaName, alias);
893
- importParts.push(`${schemaName} as ${alias}`);
894
- } else {
895
- // Unique name — import directly, no alias needed
896
- aliases.set(schemaName, schemaName);
897
- importParts.push(schemaName);
898
- }
899
- }
900
-
901
- schemaImportAliases.set(importPath, aliases);
902
- imports.push(`import type { ${importParts.join(', ')} } from '${importPath}';`);
903
- });
904
-
905
- const importsStr = imports.length > 0 ? imports.join('\n') + '\n' : '';
906
-
907
- // Add InferInput/InferOutput imports if we have any routes with schemas
908
- const hasSchemas = allRoutes.some(
909
- (r) => r.hasValidator || r.inputSchemaVariable || r.outputSchemaVariable || r.agentVariable
910
- );
911
- const typeImports = hasSchemas
912
- ? `import type { InferInput, InferOutput } from '@agentuity/core';\n`
913
- : '';
914
-
915
- // Generate individual route schema types
916
- const routeSchemaTypes = allRoutes
917
- .filter(
918
- (r) => r.hasValidator || r.inputSchemaVariable || r.outputSchemaVariable || r.agentVariable
919
- )
920
- .map((route) => {
921
- const routeKey = route.method ? `${route.method.toUpperCase()} ${route.path}` : route.path;
922
- const safeName = routeKey
923
- .replace(/[^a-zA-Z0-9]/g, '_')
924
- .replace(/^_+|_+$/g, '')
925
- .replace(/_+/g, '_');
926
- const pascalName = toPascalCase(safeName);
927
-
928
- let inputType = 'never';
929
- let outputType = 'never';
930
- let inputSchemaType = 'never';
931
- let outputSchemaType = 'never';
932
- let agentMeta: AgentMetadata | undefined;
933
-
934
- // Look up agent metadata if available
935
- if (route.agentVariable) {
936
- agentMeta = agentMetadataMap.get(route.agentVariable);
937
- }
938
-
939
- // Only generate agent-based types if the import was successfully added
940
- // (import is only added when hasValidator && agentVariable && agentImportPath are all present)
941
- const importName = route.agentVariable ? agentImports.get(route.agentVariable) : undefined;
942
- if (importName) {
943
- inputType = `InferInput<typeof ${importName}['inputSchema']>`;
944
- outputType = `InferOutput<typeof ${importName}['outputSchema']>`;
945
- inputSchemaType = `typeof ${importName} extends { inputSchema?: infer I } ? I : never`;
946
- outputSchemaType = `typeof ${importName} extends { outputSchema?: infer O } ? O : never`;
947
- } else if (route.inputSchemaVariable || route.outputSchemaVariable) {
948
- // Get the aliased schema names using the stored import info
949
- // (which correctly handles schemas imported from shared files)
950
- const inputInfo = routeSchemaImportInfo.get(`input:${route.path}:${route.method}`);
951
- const outputInfo = routeSchemaImportInfo.get(`output:${route.path}:${route.method}`);
952
-
953
- let inputAlias: string | undefined;
954
- let outputAlias: string | undefined;
955
-
956
- if (inputInfo) {
957
- const aliases = schemaImportAliases.get(inputInfo.importPath);
958
- inputAlias = aliases?.get(inputInfo.schemaName);
959
- }
960
- if (outputInfo) {
961
- const aliases = schemaImportAliases.get(outputInfo.importPath);
962
- outputAlias = aliases?.get(outputInfo.schemaName);
963
- }
964
-
965
- inputType = inputAlias ? `InferInput<typeof ${inputAlias}>` : 'never';
966
- outputType = outputAlias ? `InferOutput<typeof ${outputAlias}>` : 'never';
967
- inputSchemaType = inputAlias ? `typeof ${inputAlias}` : 'never';
968
- outputSchemaType = outputAlias ? `typeof ${outputAlias}` : 'never';
969
- }
970
-
971
- if (inputType === 'never' && outputType === 'never') {
972
- return ''; // Skip routes without schemas
973
- }
974
-
975
- // Build JSDoc with agent description and schema details
976
- const inputJSDoc = ['/**', ` * Input type for route: ${routeKey}`];
977
- if (agentMeta?.description) {
978
- inputJSDoc.push(` * @description ${agentMeta.description}`);
979
- }
980
- if (agentMeta?.inputSchemaCode) {
981
- inputJSDoc.push(` * @schema ${agentMeta.inputSchemaCode}`);
982
- }
983
- inputJSDoc.push(' */');
984
-
985
- const outputJSDoc = ['/**', ` * Output type for route: ${routeKey}`];
986
- if (agentMeta?.description) {
987
- outputJSDoc.push(` * @description ${agentMeta.description}`);
988
- }
989
- if (agentMeta?.outputSchemaCode) {
990
- outputJSDoc.push(` * @schema ${agentMeta.outputSchemaCode}`);
991
- }
992
- outputJSDoc.push(' */');
993
-
994
- const parts = [
995
- '',
996
- ...inputJSDoc,
997
- `export type ${pascalName}Input = ${inputType};`,
998
- '',
999
- ...outputJSDoc,
1000
- `export type ${pascalName}Output = ${outputType};`,
1001
- '',
1002
- `/**`,
1003
- ` * Input schema type for route: ${routeKey}`,
1004
- ` */`,
1005
- `export type ${pascalName}InputSchema = ${inputSchemaType};`,
1006
- '',
1007
- `/**`,
1008
- ` * Output schema type for route: ${routeKey}`,
1009
- ` */`,
1010
- `export type ${pascalName}OutputSchema = ${outputSchemaType};`,
1011
- ];
1012
- return parts.join('\n');
1013
- })
1014
- .filter(Boolean)
1015
- .join('\n');
1016
-
1017
- // Helper to generate route entry - uses exported schema types
1018
- const generateRouteEntry = (route: RouteInfo, pathIncludesMethod = false): string => {
1019
- const routeKey = route.path;
1020
- // For WebSocket/SSE routes, we need to include the method in the type name
1021
- // to match the generated types (which use "POST /api/websocket/echo" as the routeKey)
1022
- // For API routes, the method is already in the path from the caller
1023
- const typeRouteKey = pathIncludesMethod
1024
- ? route.path
1025
- : `${route.method?.toUpperCase()} ${route.path}`;
1026
- const safeName = typeRouteKey
1027
- .replace(/[^a-zA-Z0-9]/g, '_')
1028
- .replace(/^_+|_+$/g, '')
1029
- .replace(/_+/g, '_');
1030
- const pascalName = toPascalCase(safeName);
1031
-
1032
- // Use the exported schema types we generated above
1033
- // Note: agentImports.get() may return undefined if import wasn't added
1034
- const importName = route.agentVariable ? agentImports.get(route.agentVariable) : null;
1035
-
1036
- // Use 'never' types if no schemas were actually extracted
1037
- // Note: hasValidator may be true (e.g., zValidator('query', ...)) but no schemas extracted
1038
- // because only 'json' validators extract input schemas
1039
- // Also check if agentVariable exists but import wasn't added (missing agentImportPath)
1040
- const hasValidAgentImport = route.agentVariable ? !!importName : false;
1041
-
1042
- // Generate pathParams type
1043
- const pathParamsType = generatePathParamsType(route.pathParams);
1044
-
1045
- if (!route.inputSchemaVariable && !route.outputSchemaVariable && !hasValidAgentImport) {
1046
- const streamValue = route.stream === true ? 'true' : 'false';
1047
- return `\t'${routeKey}': {
1048
- \t\tinputSchema: never;
1049
- \t\toutputSchema: never;
1050
- \t\tstream: ${streamValue};
1051
- \t\tparams: ${pathParamsType};
1052
- \t};`;
1053
- }
1054
- const streamValue = importName
1055
- ? `typeof ${importName} extends { stream?: infer S } ? S : false`
1056
- : route.stream === true
1057
- ? 'true'
1058
- : 'false';
1059
-
1060
- return `\t'${routeKey}': {
1061
- \t\tinputSchema: ${pascalName}InputSchema;
1062
- \t\toutputSchema: ${pascalName}OutputSchema;
1063
- \t\tstream: ${streamValue};
1064
- \t\tparams: ${pathParamsType};
1065
- \t};`;
1066
- };
1067
-
1068
- // Generate route entries with METHOD prefix for API routes
1069
- const apiRouteEntries = apiRoutes
1070
- .map((route) => {
1071
- const routeKey = `${route.method.toUpperCase()} ${route.path}`;
1072
- return generateRouteEntry({ ...route, path: routeKey }, true);
1073
- })
1074
- .join('\n');
1075
-
1076
- const websocketRouteEntries = websocketRoutes
1077
- .map((r) => generateRouteEntry(r, false))
1078
- .join('\n');
1079
- const sseRouteEntries = sseRoutes.map((r) => generateRouteEntry(r, false)).join('\n');
1080
-
1081
- // Generate RPC-style nested registry type
1082
- const rpcRegistryType = generateRPCRegistryType(
1083
- apiRoutes,
1084
- websocketRoutes,
1085
- sseRoutes,
1086
- agentImports,
1087
- schemaImportAliases,
1088
- agentMetadataMap
1089
- );
1090
- const rpcRuntimeMetadata = generateRPCRuntimeMetadata(apiRoutes, websocketRoutes, sseRoutes);
1091
-
1092
- const generatedContent = `// @generated
1093
- // Auto-generated by Agentuity - DO NOT EDIT
1094
- ${importsStr}${typeImports}${
1095
- shouldEmitFrontendClient
1096
- ? `
1097
- import { createClient } from '@agentuity/frontend';`
1098
- : ''
1099
- }
1100
- // ============================================================================
1101
- // Route Schema Type Exports
1102
- // ============================================================================
1103
- ${routeSchemaTypes}
1104
-
1105
- // ============================================================================
1106
- // Route Definitions
1107
- // ============================================================================
1108
-
1109
- /**
1110
- * Route Definitions
1111
- *
1112
- * Type-safe route registry for all API routes, WebSocket connections, and SSE endpoints.
1113
- * Used by @agentuity/react and @agentuity/frontend for client-side type-safe routing.
1114
- *
1115
- * @remarks
1116
- * This module augmentation is auto-generated from your route files during build.
1117
- * Individual route Input/Output types are exported above for direct usage.
1118
- *
1119
- * The augmentation targets @agentuity/frontend (the canonical source of registry types).
1120
- * Since @agentuity/react re-exports these types, the augmentation is visible when
1121
- * importing from either package.
1122
- */
1123
- ${
1124
- shouldEmitFrontendClient
1125
- ? `
1126
- /**
1127
- * RPC Route Registry
1128
- *
1129
- * Nested structure for RPC-style client access (e.g., client.hello.post())
1130
- * Used by createClient() from @agentuity/frontend for type-safe RPC calls.
1131
- */
1132
- export interface RPCRouteRegistry {
1133
- ${rpcRegistryType}
1134
- }
1135
- `
1136
- : ''
1137
- }
1138
- declare module '@agentuity/frontend' {
1139
- \t/**
1140
- \t * API Route Registry
1141
- \t *
1142
- \t * Maps route keys (METHOD /path) to their input/output schemas
1143
- \t */
1144
- \texport interface RouteRegistry {
1145
- ${apiRouteEntries}
1146
- \t}
1147
- \t
1148
- \t/**
1149
- \t * WebSocket Route Registry
1150
- \t *
1151
- \t * Maps WebSocket route paths to their schemas
1152
- \t */
1153
- \texport interface WebSocketRouteRegistry {
1154
- ${websocketRouteEntries}
1155
- \t}
1156
- \t
1157
- \t/**
1158
- \t * Server-Sent Events Route Registry
1159
- \t *
1160
- \t * Maps SSE route paths to their schemas
1161
- \t */
1162
- \texport interface SSERouteRegistry {
1163
- ${sseRouteEntries}
1164
- \t}
1165
-
1166
- \t/**
1167
- \t * RPC Route Registry
1168
- \t *
1169
- \t * Nested structure for RPC-style client access (e.g., client.hello.post())
1170
- \t * Used by createClient() from @agentuity/frontend for type-safe RPC calls.
1171
- \t */
1172
- \texport interface RPCRouteRegistry {
1173
- ${rpcRegistryType}
1174
- \t}
1175
- }
1176
- ${
1177
- hasReactDependency
1178
- ? `
1179
- // Backward compatibility: also augment @agentuity/react for older versions
1180
- // that define RouteRegistry locally instead of re-exporting from @agentuity/frontend
1181
- declare module '@agentuity/react' {
1182
- \texport interface RouteRegistry {
1183
- ${apiRouteEntries}
1184
- \t}
1185
- \texport interface WebSocketRouteRegistry {
1186
- ${websocketRouteEntries}
1187
- \t}
1188
- \texport interface SSERouteRegistry {
1189
- ${sseRouteEntries}
1190
- \t}
1191
- \texport interface RPCRouteRegistry {
1192
- ${rpcRegistryType}
1193
- \t}
1194
- }
1195
- `
1196
- : ''
1197
- }
1198
- /**
1199
- * Runtime metadata for RPC routes.
1200
- * Contains route type information for client routing decisions.
1201
- * @internal
1202
- */
1203
- const _rpcRouteMetadata = ${rpcRuntimeMetadata} as const;
1204
-
1205
- // Store metadata globally for createAPIClient() to access
1206
- if (typeof globalThis !== 'undefined') {
1207
- (globalThis as Record<string, unknown>).__rpcRouteMetadata = _rpcRouteMetadata;
1208
- }
1209
- ${
1210
- shouldEmitFrontendClient
1211
- ? `
1212
- /**
1213
- * Create a type-safe API client with optional configuration.
1214
- *
1215
- * This function is only generated when @agentuity/frontend is installed
1216
- * but @agentuity/react is not. For React apps, import createAPIClient
1217
- * from '@agentuity/react' instead.
1218
- *
1219
- * @example
1220
- * \`\`\`typescript
1221
- * import { createAPIClient } from './generated/routes';
1222
- *
1223
- * // Basic usage
1224
- * const api = createAPIClient();
1225
- * const result = await api.hello.post({ name: 'World' });
1226
- *
1227
- * // With custom headers
1228
- * const api = createAPIClient({ headers: { 'X-Custom-Header': 'value' } });
1229
- * await api.hello.post({ name: 'World' });
1230
- * \`\`\`
1231
- */
1232
- export function createAPIClient(options?: Parameters<typeof createClient>[0]): import('@agentuity/frontend').Client<RPCRouteRegistry> {
1233
- return createClient(options || {}, _rpcRouteMetadata) as import('@agentuity/frontend').Client<RPCRouteRegistry>;
1234
- }
1235
- `
1236
- : hasReactDependency
1237
- ? `
1238
- /**
1239
- * Type-safe API client is available from @agentuity/react
1240
- *
1241
- * @example
1242
- * \`\`\`typescript
1243
- * import { createAPIClient } from '@agentuity/react';
1244
- *
1245
- * const api = createAPIClient();
1246
- * const result = await api.hello.post({ name: 'World' });
1247
- * \`\`\`
1248
- */
1249
- `
1250
- : ''
1251
- }
1252
-
1253
- // FOUND AN ERROR IN THIS FILE?
1254
- // Please file an issue at https://github.com/agentuity/sdk/issues
1255
- // or if you know the fix please submit a PR!
1256
- `;
1257
-
1258
- const generatedDir = join(srcDir, 'generated');
1259
- const registryPath = join(generatedDir, 'routes.ts');
1260
-
1261
- mkdirSync(generatedDir, { recursive: true });
1262
-
1263
- // Collapse 2+ consecutive empty lines into 1 empty line (3+ \n becomes 2 \n)
1264
- const cleanedContent = generatedContent.replace(/\n{3,}/g, '\n\n');
1265
-
1266
- writeFileSync(registryPath, cleanedContent, 'utf-8');
1267
- }