@agentuity/cli 0.0.99 → 0.0.101

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 (107) hide show
  1. package/AGENTS.md +1 -1
  2. package/dist/api.d.ts +1 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +1 -1
  5. package/dist/api.js.map +1 -1
  6. package/dist/auth.d.ts.map +1 -1
  7. package/dist/auth.js +5 -0
  8. package/dist/auth.js.map +1 -1
  9. package/dist/cmd/build/ast.d.ts +2 -1
  10. package/dist/cmd/build/ast.d.ts.map +1 -1
  11. package/dist/cmd/build/ast.js +135 -47
  12. package/dist/cmd/build/ast.js.map +1 -1
  13. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  14. package/dist/cmd/build/entry-generator.js +220 -188
  15. package/dist/cmd/build/entry-generator.js.map +1 -1
  16. package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
  17. package/dist/cmd/build/vite/agent-discovery.js +103 -45
  18. package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
  19. package/dist/cmd/build/vite/bun-dev-server.js +1 -1
  20. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  21. package/dist/cmd/build/vite/docs-generator.d.ts +13 -0
  22. package/dist/cmd/build/vite/docs-generator.d.ts.map +1 -0
  23. package/dist/cmd/build/vite/docs-generator.js +81 -0
  24. package/dist/cmd/build/vite/docs-generator.js.map +1 -0
  25. package/dist/cmd/build/vite/index.d.ts +3 -4
  26. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  27. package/dist/cmd/build/vite/index.js +9 -8
  28. package/dist/cmd/build/vite/index.js.map +1 -1
  29. package/dist/cmd/build/vite/lifecycle-generator.d.ts +1 -1
  30. package/dist/cmd/build/vite/lifecycle-generator.d.ts.map +1 -1
  31. package/dist/cmd/build/vite/lifecycle-generator.js +19 -5
  32. package/dist/cmd/build/vite/lifecycle-generator.js.map +1 -1
  33. package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -1
  34. package/dist/cmd/build/vite/metadata-generator.js +145 -0
  35. package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
  36. package/dist/cmd/build/vite/registry-generator.d.ts +3 -3
  37. package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
  38. package/dist/cmd/build/vite/registry-generator.js +627 -103
  39. package/dist/cmd/build/vite/registry-generator.js.map +1 -1
  40. package/dist/cmd/build/vite/route-discovery.d.ts +4 -0
  41. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  42. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  43. package/dist/cmd/build/vite/server-bundler.d.ts.map +1 -1
  44. package/dist/cmd/build/vite/server-bundler.js +48 -1
  45. package/dist/cmd/build/vite/server-bundler.js.map +1 -1
  46. package/dist/cmd/build/vite/vite-builder.d.ts +1 -1
  47. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  48. package/dist/cmd/build/vite/vite-builder.js +30 -21
  49. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  50. package/dist/cmd/build/vite-bundler.js +6 -6
  51. package/dist/cmd/build/vite-bundler.js.map +1 -1
  52. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  53. package/dist/cmd/cloud/deploy.js +11 -5
  54. package/dist/cmd/cloud/deploy.js.map +1 -1
  55. package/dist/cmd/dev/file-watcher.d.ts.map +1 -1
  56. package/dist/cmd/dev/file-watcher.js +33 -1
  57. package/dist/cmd/dev/file-watcher.js.map +1 -1
  58. package/dist/cmd/dev/index.d.ts.map +1 -1
  59. package/dist/cmd/dev/index.js +102 -21
  60. package/dist/cmd/dev/index.js.map +1 -1
  61. package/dist/cmd/dev/sync.d.ts.map +1 -1
  62. package/dist/cmd/dev/sync.js +19 -3
  63. package/dist/cmd/dev/sync.js.map +1 -1
  64. package/dist/cmd/project/create.d.ts.map +1 -1
  65. package/dist/cmd/project/create.js +8 -2
  66. package/dist/cmd/project/create.js.map +1 -1
  67. package/dist/config.d.ts.map +1 -1
  68. package/dist/config.js +8 -0
  69. package/dist/config.js.map +1 -1
  70. package/dist/index.d.ts +0 -1
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +0 -1
  73. package/dist/index.js.map +1 -1
  74. package/package.json +5 -8
  75. package/src/api.ts +1 -1
  76. package/src/auth.ts +6 -0
  77. package/src/cmd/build/ast.ts +161 -48
  78. package/src/cmd/build/entry-generator.ts +225 -190
  79. package/src/cmd/build/vite/agent-discovery.ts +151 -58
  80. package/src/cmd/build/vite/bun-dev-server.ts +1 -1
  81. package/src/cmd/build/vite/docs-generator.ts +87 -0
  82. package/src/cmd/build/vite/index.ts +9 -8
  83. package/src/cmd/build/vite/lifecycle-generator.ts +19 -5
  84. package/src/cmd/build/vite/metadata-generator.ts +178 -0
  85. package/src/cmd/build/vite/registry-generator.ts +727 -108
  86. package/src/cmd/build/vite/route-discovery.ts +4 -0
  87. package/src/cmd/build/vite/server-bundler.ts +56 -1
  88. package/src/cmd/build/vite/vite-builder.ts +46 -33
  89. package/src/cmd/build/vite-bundler.ts +6 -6
  90. package/src/cmd/cloud/deploy.ts +15 -5
  91. package/src/cmd/dev/file-watcher.ts +37 -1
  92. package/src/cmd/dev/index.ts +141 -30
  93. package/src/cmd/dev/sync.ts +41 -6
  94. package/src/cmd/project/create.ts +13 -3
  95. package/src/config.ts +9 -0
  96. package/src/index.ts +0 -5
  97. package/src/runtime-bootstrap.md +1 -1
  98. package/dist/cmd/build/vite/patch-plugin.d.ts +0 -21
  99. package/dist/cmd/build/vite/patch-plugin.d.ts.map +0 -1
  100. package/dist/cmd/build/vite/patch-plugin.js +0 -70
  101. package/dist/cmd/build/vite/patch-plugin.js.map +0 -1
  102. package/dist/runtime-bootstrap.d.ts +0 -56
  103. package/dist/runtime-bootstrap.d.ts.map +0 -1
  104. package/dist/runtime-bootstrap.js +0 -95
  105. package/dist/runtime-bootstrap.js.map +0 -1
  106. package/src/cmd/build/vite/patch-plugin.ts +0 -88
  107. package/src/runtime-bootstrap.ts +0 -131
@@ -33,8 +33,12 @@ export interface RouteInfo {
33
33
  routeType: 'api' | 'sms' | 'email' | 'cron' | 'websocket' | 'sse' | 'stream';
34
34
  agentVariable?: string;
35
35
  agentImportPath?: string;
36
+ agentName?: string;
37
+ agentDescription?: string;
36
38
  inputSchemaVariable?: string;
37
39
  outputSchemaVariable?: string;
40
+ inputSchemaCode?: string;
41
+ outputSchemaCode?: string;
38
42
  stream?: boolean;
39
43
  }
40
44
 
@@ -6,6 +6,8 @@
6
6
  import { join } from 'node:path';
7
7
  import { readdir, stat } from 'node:fs/promises';
8
8
  import type { Logger } from '../../../types';
9
+ import type { BunPlugin } from 'bun';
10
+ import { generatePatches, applyPatch } from '../patch';
9
11
 
10
12
  export interface ServerBundleOptions {
11
13
  rootDir: string;
@@ -22,7 +24,7 @@ export async function installExternalsAndBuild(options: ServerBundleOptions): Pr
22
24
 
23
25
  logger.debug('[server-bundler] Starting server bundle process');
24
26
 
25
- const entryPath = join(rootDir, '.agentuity/app.generated.ts');
27
+ const entryPath = join(rootDir, 'src/generated/app.ts');
26
28
  const outDir = join(rootDir, '.agentuity');
27
29
 
28
30
  logger.debug(`[server-bundler] Entry: ${entryPath}, OutDir: ${outDir}`);
@@ -202,6 +204,39 @@ export async function installExternalsAndBuild(options: ServerBundleOptions): Pr
202
204
  logger.debug('Building server with Bun.build...');
203
205
  logger.debug(`External packages (${external.length}): ${external.join(', ')}`);
204
206
 
207
+ // Create Bun plugin to apply LLM patches during bundling
208
+ const patches = generatePatches();
209
+ logger.debug(`Loaded ${patches.size} patch(es) for LLM providers`);
210
+
211
+ const patchPlugin: BunPlugin = {
212
+ name: 'agentuity:patch',
213
+ setup(build) {
214
+ for (const [, patch] of patches) {
215
+ let modulePath = join('node_modules', patch.module, '.*');
216
+ if (patch.filename) {
217
+ modulePath = join('node_modules', patch.module, patch.filename + '.*');
218
+ }
219
+ build.onLoad(
220
+ {
221
+ filter: new RegExp(modulePath),
222
+ namespace: 'file',
223
+ },
224
+ async (args) => {
225
+ if (build.config.target !== 'bun') {
226
+ return;
227
+ }
228
+ logger.trace(`Applying patch to: ${args.path}`);
229
+ const [contents, loader] = await applyPatch(args.path, patch);
230
+ return {
231
+ contents,
232
+ loader,
233
+ };
234
+ }
235
+ );
236
+ }
237
+ },
238
+ };
239
+
205
240
  const buildConfig = {
206
241
  entrypoints: [entryPath],
207
242
  outdir: outDir, // Output to .agentuity/ directly (not .agentuity/server/)
@@ -211,7 +246,12 @@ export async function installExternalsAndBuild(options: ServerBundleOptions): Pr
211
246
  minify: !dev,
212
247
  sourcemap: (dev ? 'inline' : 'external') as 'inline' | 'external',
213
248
  external,
249
+ // CRITICAL: Disable environment variable inlining for server builds
250
+ // Server code must read process.env at RUNTIME, not have values baked in at build time
251
+ // Without this, NODE_ENV and other env vars get inlined as string literals
252
+ env: 'disable' as const,
214
253
  define: userDefine, // Include custom define values from agentuity.config.ts
254
+ plugins: [patchPlugin],
215
255
  naming: {
216
256
  entry: 'app.js', // Output as app.js (not app.generated.js)
217
257
  },
@@ -221,6 +261,12 @@ export async function installExternalsAndBuild(options: ServerBundleOptions): Pr
221
261
  `Bun.build config: ${JSON.stringify({ ...buildConfig, external: `[${external.length} packages]` }, null, 2)}`
222
262
  );
223
263
 
264
+ // WORKAROUND: Temporarily delete NODE_ENV to prevent Bun.build from inlining it
265
+ // See: https://github.com/oven-sh/bun/issues/20183
266
+ // Even with env: 'disable', Bun.build still inlines NODE_ENV at build time
267
+ const originalNodeEnv = process.env.NODE_ENV;
268
+ delete process.env.NODE_ENV;
269
+
224
270
  // Verify entry point exists before building
225
271
  if (!(await Bun.file(entryPath).exists())) {
226
272
  throw new Error(`Entry point not found: ${entryPath}`);
@@ -232,6 +278,10 @@ export async function installExternalsAndBuild(options: ServerBundleOptions): Pr
232
278
  try {
233
279
  result = await Bun.build(buildConfig);
234
280
  } catch (error: unknown) {
281
+ // Restore NODE_ENV after build attempt
282
+ if (originalNodeEnv !== undefined) {
283
+ process.env.NODE_ENV = originalNodeEnv;
284
+ }
235
285
  logger.error('Bun.build threw an exception');
236
286
 
237
287
  // Handle AggregateError with build/resolve messages
@@ -250,6 +300,11 @@ export async function installExternalsAndBuild(options: ServerBundleOptions): Pr
250
300
  throw error;
251
301
  }
252
302
 
303
+ // Restore NODE_ENV after successful build
304
+ if (originalNodeEnv !== undefined) {
305
+ process.env.NODE_ENV = originalNodeEnv;
306
+ }
307
+
253
308
  if (!result.success) {
254
309
  logger.error('Bun.build failed for server');
255
310
  logger.error(
@@ -7,7 +7,6 @@
7
7
  import { join } from 'node:path';
8
8
  import type { InlineConfig } from 'vite';
9
9
  import type { Logger } from '../../../types';
10
- import { patchPlugin } from './patch-plugin';
11
10
  import { browserEnvPlugin } from './browser-env-plugin';
12
11
 
13
12
  export interface ViteBuildOptions {
@@ -26,7 +25,7 @@ export interface ViteBuildOptions {
26
25
 
27
26
  /**
28
27
  * Run a Vite build for the specified mode
29
- * Generates vite config in .agentuity/.vite/ and uses it
28
+ * Uses inline Vite config (customizable via agentuity.config.ts)
30
29
  */
31
30
  export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
32
31
  const { rootDir, mode, dev = false, projectId = '', deploymentId = '', logger } = options;
@@ -36,7 +35,17 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
36
35
  // For server mode, use Bun.build (preserves process.env at runtime)
37
36
  if (mode === 'server') {
38
37
  try {
39
- // First, generate the entry file
38
+ const srcDir = join(rootDir, 'src');
39
+
40
+ // Generate documentation files (if they don't exist)
41
+ const { generateDocumentation } = await import('./docs-generator');
42
+ await generateDocumentation(srcDir, logger);
43
+
44
+ // Generate lifecycle types (if setup() exists)
45
+ const { generateLifecycleTypes } = await import('./lifecycle-generator');
46
+ await generateLifecycleTypes(rootDir, srcDir, logger);
47
+
48
+ // Then, generate the entry file
40
49
  const { generateEntryFile } = await import('../entry-generator');
41
50
  await generateEntryFile({
42
51
  rootDir,
@@ -46,7 +55,7 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
46
55
  mode: dev ? 'dev' : 'prod',
47
56
  });
48
57
 
49
- // Then, build with Bun.build
58
+ // Finally, build with Bun.build
50
59
  const { installExternalsAndBuild } = await import('./server-bundler');
51
60
  await installExternalsAndBuild({
52
61
  rootDir,
@@ -76,7 +85,7 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
76
85
  const { workbenchEnabled = false, workbenchRoute = '/workbench' } = options;
77
86
 
78
87
  // Load custom user plugins from agentuity.config.ts if it exists
79
- const plugins = [react(), browserEnvPlugin(), patchPlugin({ logger, dev })];
88
+ const plugins = [react(), browserEnvPlugin()];
80
89
  const { loadAgentuityConfig } = await import('./config-loader');
81
90
  const userConfig = await loadAgentuityConfig(rootDir, logger);
82
91
  const userPlugins = userConfig?.plugins || [];
@@ -147,7 +156,7 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
147
156
  viteConfig = {
148
157
  root: join(rootDir, '.agentuity/workbench-src'), // Use generated workbench source
149
158
  base, // All workbench assets are under the configured route
150
- plugins: [react(), patchPlugin({ logger, dev })],
159
+ plugins: [react()],
151
160
  envPrefix: ['VITE_', 'AGENTUITY_PUBLIC_', 'PUBLIC_'],
152
161
  define: {
153
162
  // Merge user-defined constants
@@ -214,10 +223,36 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
214
223
  await generateWorkbenchFiles(rootDir, projectId, workbenchConfig, logger);
215
224
  }
216
225
 
226
+ // 1. Discover agents and routes BEFORE builds
227
+ logger.debug('Discovering agents and routes...');
228
+ const { generateAgentRegistry, generateRouteRegistry } = await import('./registry-generator');
229
+ const { discoverAgents } = await import('./agent-discovery');
230
+ const { discoverRoutes } = await import('./route-discovery');
231
+
232
+ const srcDir = join(rootDir, 'src');
233
+ const agentMetadata = await discoverAgents(
234
+ srcDir,
235
+ projectId,
236
+ options.deploymentId || '',
237
+ logger
238
+ );
239
+ const { routes, routeInfoList } = await discoverRoutes(
240
+ srcDir,
241
+ projectId,
242
+ options.deploymentId || '',
243
+ logger
244
+ );
245
+
246
+ // Generate agent and route registries for type augmentation BEFORE builds
247
+ // (TypeScript needs these files to exist during type checking)
248
+ generateAgentRegistry(srcDir, agentMetadata);
249
+ generateRouteRegistry(srcDir, routeInfoList);
250
+ logger.debug('Agent and route registries generated');
251
+
217
252
  // Check if web frontend exists
218
253
  const hasWebFrontend = await Bun.file(join(rootDir, 'src', 'web', 'index.html')).exists();
219
254
 
220
- // 1. Build client (only if web frontend exists)
255
+ // 2. Build client (only if web frontend exists)
221
256
  if (hasWebFrontend) {
222
257
  logger.debug('Building client assets...');
223
258
  try {
@@ -238,7 +273,7 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
238
273
  logger.debug('Skipping client build - no src/web/index.html found');
239
274
  }
240
275
 
241
- // 2. Build workbench (if enabled in config)
276
+ // 3. Build workbench (if enabled in config)
242
277
  if (workbenchConfig.enabled) {
243
278
  logger.debug('Building workbench assets...');
244
279
  try {
@@ -257,7 +292,7 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
257
292
  }
258
293
  }
259
294
 
260
- // 3. Build server
295
+ // 4. Build server
261
296
  logger.debug('Building server...');
262
297
  try {
263
298
  const started = Date.now();
@@ -269,31 +304,9 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
269
304
  throw error;
270
305
  }
271
306
 
272
- // 4. Generate registry and metadata (after all builds complete)
273
- logger.debug('Generating agent registry and metadata...');
307
+ // 5. Generate metadata (after all builds complete)
308
+ logger.debug('Generating metadata...');
274
309
  const { generateMetadata, writeMetadataFile } = await import('./metadata-generator');
275
- const { generateAgentRegistry, generateRouteRegistry } = await import('./registry-generator');
276
- const { discoverAgents } = await import('./agent-discovery');
277
- const { discoverRoutes } = await import('./route-discovery');
278
-
279
- const srcDir = join(rootDir, 'src');
280
- const agentMetadata = await discoverAgents(
281
- srcDir,
282
- projectId,
283
- options.deploymentId || '',
284
- logger
285
- );
286
- const { routes, routeInfoList } = await discoverRoutes(
287
- srcDir,
288
- projectId,
289
- options.deploymentId || '',
290
- logger
291
- );
292
-
293
- // Generate agent and route registries for type augmentation
294
- generateAgentRegistry(srcDir, agentMetadata);
295
- generateRouteRegistry(srcDir, routeInfoList);
296
- logger.debug('Agent and route registries generated');
297
310
 
298
311
  // Generate metadata
299
312
  const metadata = await generateMetadata({
@@ -75,8 +75,8 @@ export async function viteBundle(options: ViteBundleOptions): Promise<{ output:
75
75
  }
76
76
 
77
77
  try {
78
- // Run all Vite builds (client -> workbench -> server)
79
- logger.debug('Starting Vite builds...');
78
+ // Run all builds (client -> workbench -> server)
79
+ logger.debug('Starting builds...');
80
80
 
81
81
  const result = await runAllBuilds({
82
82
  rootDir,
@@ -90,16 +90,16 @@ export async function viteBundle(options: ViteBundleOptions): Promise<{ output:
90
90
  });
91
91
 
92
92
  if (result.client.included) {
93
- output.push(tui.muted(`✓ Client Built in ${result.client.duration}ms`));
93
+ output.push(tui.muted(`✓ Client built in ${result.client.duration}ms`));
94
94
  }
95
95
  if (result.workbench.included) {
96
- output.push(tui.muted(`✓ Workbench Built in ${result.workbench.duration}ms`));
96
+ output.push(tui.muted(`✓ Workbench built in ${result.workbench.duration}ms`));
97
97
  }
98
98
  if (result.server.included) {
99
- output.push(tui.muted(`✓ Server Built in ${result.server.duration}ms`));
99
+ output.push(tui.muted(`✓ Server built in ${result.server.duration}ms`));
100
100
  }
101
101
 
102
- logger.debug('Vite builds complete');
102
+ logger.debug('All builds complete');
103
103
 
104
104
  return { output };
105
105
  } catch (error) {
@@ -6,8 +6,9 @@ import { tmpdir } from 'node:os';
6
6
  import { StructuredError } from '@agentuity/core';
7
7
  import { isRunningFromExecutable } from '../upgrade';
8
8
  import { createSubcommand } from '../../types';
9
+ import { getUserAgent } from '../../api';
9
10
  import * as tui from '../../tui';
10
- import { saveProjectDir, getDefaultConfigDir } from '../../config';
11
+ import { saveProjectDir, getDefaultConfigDir, loadProjectSDKKey } from '../../config';
11
12
  import {
12
13
  runSteps,
13
14
  stepSuccess,
@@ -106,6 +107,15 @@ export const deploySubcommand = createSubcommand({
106
107
  let statusResult: DeploymentStatusResult | undefined;
107
108
  const logs: string[] = [];
108
109
 
110
+ const sdkKey = await loadProjectSDKKey(ctx.logger, ctx.projectDir);
111
+
112
+ // Ensure SDK key is present before proceeding
113
+ if (!sdkKey) {
114
+ ctx.logger.fatal(
115
+ 'SDK key not found. Run "agentuity auth login" to authenticate or set AGENTUITY_SDK_KEY environment variable.'
116
+ );
117
+ }
118
+
109
119
  try {
110
120
  await saveProjectDir(projectDir);
111
121
 
@@ -241,10 +251,6 @@ export const deploySubcommand = createSubcommand({
241
251
  const deploymentZip = join(tmpdir(), `${deployment.id}.zip`);
242
252
  await zipDir(join(projectDir, '.agentuity'), deploymentZip, {
243
253
  filter: (_filename: string, relative: string) => {
244
- // Exclude Vite-specific build artifacts
245
- if (relative.endsWith('.generated.ts')) {
246
- return false;
247
- }
248
254
  if (relative.startsWith('.vite/')) {
249
255
  return false;
250
256
  }
@@ -444,6 +450,10 @@ export const deploySubcommand = createSubcommand({
444
450
  logger.debug('fetching stream: %s/%s', streamsUrl, streamId);
445
451
  const resp = await fetch(`${streamsUrl}/${streamId}`, {
446
452
  signal: logStreamController.signal,
453
+ headers: {
454
+ Authorization: `Bearer ${sdkKey}`,
455
+ 'User-Agent': getUserAgent(),
456
+ },
447
457
  });
448
458
  if (!resp.ok || !resp.body) {
449
459
  ctx.logger.trace(
@@ -5,9 +5,10 @@
5
5
  * Handles both backend (API, agents, lib) and generates restart signals.
6
6
  */
7
7
 
8
- import { watch, type FSWatcher } from 'node:fs';
8
+ import { watch, type FSWatcher, statSync, readdirSync } from 'node:fs';
9
9
  import { resolve } from 'node:path';
10
10
  import type { Logger } from '../../types';
11
+ import { createAgentTemplates, createAPITemplates } from './templates';
11
12
 
12
13
  export interface FileWatcherOptions {
13
14
  rootDir: string;
@@ -127,6 +128,41 @@ export function createFileWatcher(options: FileWatcherOptions): FileWatcherManag
127
128
  return;
128
129
  }
129
130
 
131
+ // Check if an empty directory was created in src/agent/ or src/api/
132
+ // This helps with developer experience by auto-scaffolding template files
133
+ if (changedFile && eventType === 'rename') {
134
+ try {
135
+ const absPath = resolve(watchDir, changedFile);
136
+ // Normalize the path for comparison (use forward slashes)
137
+ const normalizedPath = changedFile.replace(/\\/g, '/');
138
+
139
+ // Check if it's a directory and empty
140
+ const stats = statSync(absPath);
141
+ if (stats.isDirectory()) {
142
+ const contents = readdirSync(absPath);
143
+ if (contents.length === 0) {
144
+ // Check if this is an agent or API directory
145
+ if (
146
+ normalizedPath.startsWith('src/agent/') ||
147
+ normalizedPath.includes('/src/agent/')
148
+ ) {
149
+ logger.debug('Agent directory created: %s', changedFile);
150
+ createAgentTemplates(absPath);
151
+ } else if (
152
+ normalizedPath.startsWith('src/api/') ||
153
+ normalizedPath.includes('/src/api/')
154
+ ) {
155
+ logger.debug('API directory created: %s', changedFile);
156
+ createAPITemplates(absPath);
157
+ }
158
+ }
159
+ }
160
+ } catch (error) {
161
+ // File might have been deleted or doesn't exist yet - this is normal
162
+ logger.trace('Unable to check directory for template creation: %s', error);
163
+ }
164
+ }
165
+
130
166
  logger.debug('File changed (%s): %s', eventType, changedFile || watchDir);
131
167
  onRestart();
132
168
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { resolve, join } from 'node:path';
3
3
  import { existsSync } from 'node:fs';
4
- import { internalExit } from '@agentuity/runtime';
4
+ import { getServiceUrls } from '@agentuity/server';
5
5
  import { createCommand } from '../../types';
6
6
  import { startBunDevServer } from '../build/vite/bun-dev-server';
7
7
  import { startViteAssetServer } from '../build/vite/vite-asset-server';
@@ -99,6 +99,13 @@ export const command = createCommand({
99
99
 
100
100
  const interactive = !shouldDisableInteractive(opts.interactive);
101
101
 
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ let originalExit = (globalThis as any).AGENTUITY_PROCESS_EXIT;
104
+
105
+ if (!originalExit) {
106
+ originalExit = process.exit.bind(process);
107
+ }
108
+
102
109
  for (const filename of mustHaves) {
103
110
  if (!existsSync(filename)) {
104
111
  missing.push(filename);
@@ -110,17 +117,22 @@ export const command = createCommand({
110
117
  for (const filename of missing) {
111
118
  tui.bullet(`Missing ${filename}`);
112
119
  }
113
- internalExit(1);
120
+ originalExit(1);
114
121
  }
115
122
 
116
123
  // Setup devmode and gravity (if using public URL)
117
124
  const useMockService = process.env.DEVMODE_SYNC_SERVICE_MOCK === 'true';
118
125
  const apiClient = auth ? new APIClient(getAPIBaseURL(config), logger, config) : null;
119
- createDevmodeSyncService({
120
- logger,
121
- apiClient,
122
- mock: useMockService,
123
- });
126
+ const syncService = apiClient
127
+ ? createDevmodeSyncService({
128
+ logger,
129
+ apiClient,
130
+ mock: useMockService,
131
+ })
132
+ : null;
133
+
134
+ // Track previous metadata for sync diffing
135
+ let previousMetadata: Awaited<ReturnType<typeof import('../build/vite/metadata-generator').generateMetadata>> | undefined;
124
136
 
125
137
  let devmode: DevmodeResponse | undefined;
126
138
  let gravityBin: string | undefined;
@@ -224,8 +236,8 @@ export const command = createCommand({
224
236
 
225
237
  // Start Vite asset server ONCE before restart loop
226
238
  // Vite handles frontend HMR independently and stays running across backend restarts
227
- let vitePort: number;
228
239
  let viteServer: ServerLike | null = null;
240
+ let vitePort: number;
229
241
 
230
242
  try {
231
243
  logger.debug('Starting Vite asset server...');
@@ -241,13 +253,15 @@ export const command = createCommand({
241
253
  );
242
254
  } catch (error) {
243
255
  tui.error(`Failed to start Vite asset server: ${error}`);
244
- internalExit(1);
256
+ originalExit(1);
257
+ return;
245
258
  }
246
259
 
247
260
  // Restart loop - allows BACKEND server to restart on file changes
248
261
  // Vite stays running and handles frontend changes via HMR
249
262
  let shouldRestart = false;
250
263
  let gravityProcess: ProcessLike | null = null;
264
+ let stdinListenerRegistered = false; // Track if stdin listener is already registered
251
265
 
252
266
  const restartServer = () => {
253
267
  shouldRestart = true;
@@ -293,11 +307,22 @@ export const command = createCommand({
293
307
  }
294
308
  }
295
309
 
296
- internalExit(0);
310
+ originalExit(0);
297
311
  };
298
312
 
299
- process.on('SIGINT', cleanup);
300
- process.on('SIGTERM', cleanup);
313
+ // SIGINT/SIGTERM: coordinate shutdown between bundle and dev resources
314
+ let devShutdownHandled = false;
315
+ process.on('SIGINT', async () => {
316
+ if (devShutdownHandled) return;
317
+ devShutdownHandled = true;
318
+ // The bundle handles its own shutdown, we clean up dev resources
319
+ await cleanup();
320
+ });
321
+ process.on('SIGTERM', async () => {
322
+ if (devShutdownHandled) return;
323
+ devShutdownHandled = true;
324
+ await cleanup();
325
+ });
301
326
 
302
327
  // Ensure Vite and gravity are always killed on exit (even if cleanup is bypassed)
303
328
  process.on('exit', () => {
@@ -329,23 +354,76 @@ export const command = createCommand({
329
354
  fileWatcher.pause();
330
355
 
331
356
  try {
332
- // Generate entry file for Vite before starting dev server
357
+ // Generate entry file and bundle for dev server (with LLM patches)
333
358
  await tui.spinner({
334
- message: 'Generating entry file',
359
+ message: 'Building dev bundle',
335
360
  callback: async () => {
336
- const { generateEntryFile } = await import('../build/entry-generator');
337
- await generateEntryFile({
338
- rootDir,
339
- projectId: project?.projectId ?? '',
340
- deploymentId,
341
- logger,
342
- mode: 'dev',
343
- });
361
+ const { generateEntryFile } = await import('../build/entry-generator');
362
+ await generateEntryFile({
363
+ rootDir,
364
+ projectId: project?.projectId ?? '',
365
+ deploymentId,
366
+ logger,
367
+ mode: 'dev',
368
+ });
369
+
370
+ // Bundle the app with LLM patches (dev mode = no minification)
371
+ const { installExternalsAndBuild } = await import('../build/vite/server-bundler');
372
+ await installExternalsAndBuild({
373
+ rootDir,
374
+ dev: true, // DevMode: no minification, inline sourcemaps
375
+ logger,
376
+ });
377
+
378
+ // Generate metadata file (needed for eval ID lookup at runtime)
379
+ const { discoverAgents } = await import('../build/vite/agent-discovery');
380
+ const { discoverRoutes } = await import('../build/vite/route-discovery');
381
+ const { generateMetadata, writeMetadataFile } = await import(
382
+ '../build/vite/metadata-generator'
383
+ );
384
+
385
+ const srcDir = join(rootDir, 'src');
386
+ const agents = await discoverAgents(
387
+ srcDir,
388
+ project?.projectId ?? '',
389
+ deploymentId,
390
+ logger
391
+ );
392
+ const { routes } = await discoverRoutes(
393
+ srcDir,
394
+ project?.projectId ?? '',
395
+ deploymentId,
396
+ logger
397
+ );
398
+
399
+ const metadata = await generateMetadata({
400
+ rootDir,
401
+ projectId: project?.projectId ?? '',
402
+ orgId: project?.orgId ?? '',
403
+ deploymentId,
404
+ agents,
405
+ routes,
406
+ dev: true,
407
+ logger,
408
+ });
409
+
410
+ writeMetadataFile(rootDir, metadata, true, logger);
411
+
412
+ // Sync metadata with backend (creates agents and evals in the database)
413
+ if (syncService && project?.projectId) {
414
+ await syncService.sync(
415
+ metadata,
416
+ previousMetadata,
417
+ project.projectId,
418
+ deploymentId
419
+ );
420
+ previousMetadata = metadata;
421
+ }
344
422
  },
345
423
  clearOnSuccess: true,
346
424
  });
347
425
  } catch (error) {
348
- tui.error(`Failed to generate entry file: ${error}`);
426
+ tui.error(`Failed to build dev bundle: ${error}`);
349
427
  tui.warn('Waiting for file changes to retry...');
350
428
 
351
429
  // Resume watcher to detect changes for retry
@@ -364,6 +442,34 @@ export const command = createCommand({
364
442
  }
365
443
 
366
444
  try {
445
+ // Set environment variables for LLM provider patches BEFORE starting server
446
+ // These must be set so the bundled patches can route LLM calls through AI Gateway
447
+ const serviceUrls = getServiceUrls(project?.region);
448
+
449
+ process.env.AGENTUITY_SDK_DEV_MODE = 'true';
450
+ process.env.AGENTUITY_ENV = 'development';
451
+ process.env.NODE_ENV = 'development';
452
+ if (project?.region) {
453
+ process.env.AGENTUITY_REGION = project.region;
454
+ }
455
+ process.env.PORT = String(opts.port);
456
+ process.env.AGENTUITY_PORT = process.env.PORT;
457
+
458
+ if (project) {
459
+ process.env.AGENTUITY_TRANSPORT_URL = serviceUrls.catalyst;
460
+ process.env.AGENTUITY_CATALYST_URL = serviceUrls.catalyst;
461
+ process.env.AGENTUITY_VECTOR_URL = serviceUrls.vector;
462
+ process.env.AGENTUITY_KEYVALUE_URL = serviceUrls.keyvalue;
463
+ process.env.AGENTUITY_STREAM_URL = serviceUrls.stream;
464
+ process.env.AGENTUITY_CLOUD_ORG_ID = project.orgId;
465
+ process.env.AGENTUITY_CLOUD_PROJECT_ID = project.projectId;
466
+ }
467
+
468
+ // Set Vite port for asset proxying in bundled app
469
+ process.env.VITE_PORT = String(vitePort);
470
+
471
+ logger.debug('Set VITE_PORT=%s for asset proxying', process.env.VITE_PORT);
472
+
367
473
  // Start Bun dev server (Vite already running, just start backend)
368
474
  await startBunDevServer({
369
475
  rootDir,
@@ -375,8 +481,6 @@ export const command = createCommand({
375
481
  vitePort, // Pass port of already-running Vite server
376
482
  });
377
483
 
378
- // Note: Bun server runs in-process, no separate app process needed
379
-
380
484
  // Wait for app.ts to finish loading (Vite is ready but app may still be initializing)
381
485
  // Give it 2 seconds to ensure app initialization completes
382
486
  await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -454,8 +558,14 @@ export const command = createCommand({
454
558
  // TODO: Integrate sync service with Vite's buildStart/buildEnd hooks
455
559
  // The sync service will be called when metadata changes are detected
456
560
 
457
- // Handle keyboard shortcuts
458
- if (interactive && process.stdin.isTTY && process.stdout.isTTY) {
561
+ // Handle keyboard shortcuts - only register listener once
562
+ if (
563
+ interactive &&
564
+ process.stdin.isTTY &&
565
+ process.stdout.isTTY &&
566
+ !stdinListenerRegistered
567
+ ) {
568
+ stdinListenerRegistered = true;
459
569
  process.stdin.setRawMode(true);
460
570
  process.stdin.resume();
461
571
  process.stdin.setEncoding('utf8');
@@ -470,9 +580,10 @@ export const command = createCommand({
470
580
  process.stdin.on('data', (data) => {
471
581
  const key = data.toString();
472
582
 
473
- // Handle Ctrl+C
583
+ // Handle Ctrl+C - send SIGINT to trigger graceful shutdown
474
584
  if (key === '\u0003') {
475
- internalExit(0);
585
+ process.kill(process.pid, 'SIGINT');
586
+ return;
476
587
  }
477
588
 
478
589
  switch (key) {
@@ -489,7 +600,7 @@ export const command = createCommand({
489
600
  });
490
601
  break;
491
602
  case 'q':
492
- internalExit(0);
603
+ originalExit(0);
493
604
  break;
494
605
  default:
495
606
  process.stdout.write(data);