@agentuity/cli 0.0.111 → 0.1.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 (107) hide show
  1. package/bin/cli.ts +4 -0
  2. package/dist/agents-docs.d.ts +5 -4
  3. package/dist/agents-docs.d.ts.map +1 -1
  4. package/dist/agents-docs.js +28 -8
  5. package/dist/agents-docs.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +12 -2
  8. package/dist/cli.js.map +1 -1
  9. package/dist/cmd/auth/apikey.d.ts +2 -0
  10. package/dist/cmd/auth/apikey.d.ts.map +1 -0
  11. package/dist/cmd/auth/apikey.js +31 -0
  12. package/dist/cmd/auth/apikey.js.map +1 -0
  13. package/dist/cmd/auth/index.d.ts.map +1 -1
  14. package/dist/cmd/auth/index.js +9 -1
  15. package/dist/cmd/auth/index.js.map +1 -1
  16. package/dist/cmd/build/ast.d.ts.map +1 -1
  17. package/dist/cmd/build/ast.js +103 -2
  18. package/dist/cmd/build/ast.js.map +1 -1
  19. package/dist/cmd/build/entry-generator.d.ts +2 -1
  20. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  21. package/dist/cmd/build/entry-generator.js +152 -9
  22. package/dist/cmd/build/entry-generator.js.map +1 -1
  23. package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
  24. package/dist/cmd/build/vite/agent-discovery.js +4 -3
  25. package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
  26. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  27. package/dist/cmd/build/vite/index.js +2 -1
  28. package/dist/cmd/build/vite/index.js.map +1 -1
  29. package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
  30. package/dist/cmd/build/vite/registry-generator.js +45 -0
  31. package/dist/cmd/build/vite/registry-generator.js.map +1 -1
  32. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  33. package/dist/cmd/build/vite/vite-builder.js +2 -1
  34. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  35. package/dist/cmd/cloud/deploy-fork.d.ts +32 -0
  36. package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -0
  37. package/dist/cmd/cloud/deploy-fork.js +258 -0
  38. package/dist/cmd/cloud/deploy-fork.js.map +1 -0
  39. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  40. package/dist/cmd/cloud/deploy.js +62 -3
  41. package/dist/cmd/cloud/deploy.js.map +1 -1
  42. package/dist/cmd/cloud/sandbox/get.d.ts.map +1 -1
  43. package/dist/cmd/cloud/sandbox/get.js +19 -0
  44. package/dist/cmd/cloud/sandbox/get.js.map +1 -1
  45. package/dist/cmd/cloud/ssh.d.ts.map +1 -1
  46. package/dist/cmd/cloud/ssh.js +9 -3
  47. package/dist/cmd/cloud/ssh.js.map +1 -1
  48. package/dist/cmd/dev/index.d.ts.map +1 -1
  49. package/dist/cmd/dev/index.js +18 -12
  50. package/dist/cmd/dev/index.js.map +1 -1
  51. package/dist/cmd/upgrade/index.d.ts.map +1 -1
  52. package/dist/cmd/upgrade/index.js +3 -3
  53. package/dist/cmd/upgrade/index.js.map +1 -1
  54. package/dist/config.js +1 -1
  55. package/dist/config.js.map +1 -1
  56. package/dist/log-collector.d.ts +30 -0
  57. package/dist/log-collector.d.ts.map +1 -0
  58. package/dist/log-collector.js +74 -0
  59. package/dist/log-collector.js.map +1 -0
  60. package/dist/output.d.ts.map +1 -1
  61. package/dist/output.js +2 -1
  62. package/dist/output.js.map +1 -1
  63. package/dist/steps.d.ts.map +1 -1
  64. package/dist/steps.js +48 -3
  65. package/dist/steps.js.map +1 -1
  66. package/dist/tui/box.d.ts.map +1 -1
  67. package/dist/tui/box.js +1 -6
  68. package/dist/tui/box.js.map +1 -1
  69. package/dist/tui/symbols.d.ts.map +1 -1
  70. package/dist/tui/symbols.js +4 -0
  71. package/dist/tui/symbols.js.map +1 -1
  72. package/dist/tui.d.ts +21 -12
  73. package/dist/tui.d.ts.map +1 -1
  74. package/dist/tui.js +74 -25
  75. package/dist/tui.js.map +1 -1
  76. package/dist/types.d.ts +71 -0
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js.map +1 -1
  79. package/dist/typescript-errors.d.ts.map +1 -1
  80. package/dist/typescript-errors.js +42 -10
  81. package/dist/typescript-errors.js.map +1 -1
  82. package/package.json +5 -5
  83. package/src/agents-docs.ts +42 -8
  84. package/src/cli.ts +12 -2
  85. package/src/cmd/auth/apikey.ts +36 -0
  86. package/src/cmd/auth/index.ts +9 -1
  87. package/src/cmd/build/ast.ts +120 -2
  88. package/src/cmd/build/entry-generator.ts +157 -10
  89. package/src/cmd/build/vite/agent-discovery.ts +4 -1
  90. package/src/cmd/build/vite/index.ts +2 -1
  91. package/src/cmd/build/vite/registry-generator.ts +47 -0
  92. package/src/cmd/build/vite/vite-builder.ts +2 -1
  93. package/src/cmd/cloud/deploy-fork.ts +296 -0
  94. package/src/cmd/cloud/deploy.ts +70 -3
  95. package/src/cmd/cloud/sandbox/get.ts +17 -0
  96. package/src/cmd/cloud/ssh.ts +13 -3
  97. package/src/cmd/dev/index.ts +18 -13
  98. package/src/cmd/upgrade/index.ts +3 -4
  99. package/src/config.ts +1 -1
  100. package/src/log-collector.ts +77 -0
  101. package/src/output.ts +2 -1
  102. package/src/steps.ts +52 -4
  103. package/src/tui/box.ts +1 -7
  104. package/src/tui/symbols.ts +5 -0
  105. package/src/tui.ts +77 -25
  106. package/src/types.ts +85 -0
  107. package/src/typescript-errors.ts +46 -9
@@ -1,5 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
+ import { mkdir } from 'node:fs/promises';
3
4
  import { generateLLMPrompt as generateCLIPrompt } from './cmd/ai/prompt/llm';
4
5
  import { generateLLMPrompt as generateAgentPrompt } from './cmd/ai/prompt/agent';
5
6
  import { generateLLMPrompt as generateWebPrompt } from './cmd/ai/prompt/web';
@@ -13,14 +14,23 @@ interface WriteAgentsDocsOptions {
13
14
  onlyIfMissing?: boolean;
14
15
  }
15
16
 
17
+ /**
18
+ * Generate the reference file content that points to .agents/
19
+ */
20
+ function generateReferenceContent(name: string): string {
21
+ return `See [.agents/agentuity/sdk/${name}/AGENTS.md](../../.agents/agentuity/sdk/${name}/AGENTS.md) for Agentuity ${name} development guidelines.
22
+ `;
23
+ }
24
+
16
25
  /**
17
26
  * Writes AGENTS.md documentation files to the appropriate locations in a project.
18
27
  *
19
28
  * This function generates and writes AGENTS.md files to:
20
- * - node_modules/@agentuity/cli/AGENTS.md (CLI documentation)
21
- * - src/agent/AGENTS.md (Agent development documentation)
22
- * - src/api/AGENTS.md (API development documentation)
23
- * - src/web/AGENTS.md (Web development documentation)
29
+ * - node_modules/@agentuity/cli/AGENTS.md (CLI documentation - full content)
30
+ * - .agents/agentuity/sdk/[type]/AGENTS.md (Full content for agent, api, web)
31
+ * - src/agent/AGENTS.md (Reference file pointing to .agents/)
32
+ * - src/api/AGENTS.md (Reference file pointing to .agents/)
33
+ * - src/web/AGENTS.md (Reference file pointing to .agents/)
24
34
  *
25
35
  * @param rootDir - The root directory of the project
26
36
  * @param options - Options for controlling write behavior
@@ -38,6 +48,13 @@ export async function writeAgentsDocs(
38
48
  await Bun.write(filePath, content);
39
49
  };
40
50
 
51
+ const ensureWithCreate = async (dir: string, fileName: string, content: string) => {
52
+ await mkdir(dir, { recursive: true });
53
+ const filePath = join(dir, fileName);
54
+ if (onlyIfMissing && existsSync(filePath)) return;
55
+ await Bun.write(filePath, content);
56
+ };
57
+
41
58
  // Generate and write AGENTS.md for the CLI (in node_modules)
42
59
  await ensure(
43
60
  join(rootDir, 'node_modules', '@agentuity', 'cli'),
@@ -45,8 +62,25 @@ export async function writeAgentsDocs(
45
62
  generateCLIPrompt()
46
63
  );
47
64
 
48
- // Generate and write AGENTS.md for each of the main source folders
49
- await ensure(join(rootDir, 'src', 'agent'), 'AGENTS.md', generateAgentPrompt());
50
- await ensure(join(rootDir, 'src', 'api'), 'AGENTS.md', generateAPIPrompt());
51
- await ensure(join(rootDir, 'src', 'web'), 'AGENTS.md', generateWebPrompt());
65
+ // Write full content to .agents/agentuity/sdk/[type]/AGENTS.md
66
+ await ensureWithCreate(
67
+ join(rootDir, '.agents', 'agentuity', 'sdk', 'agent'),
68
+ 'AGENTS.md',
69
+ generateAgentPrompt()
70
+ );
71
+ await ensureWithCreate(
72
+ join(rootDir, '.agents', 'agentuity', 'sdk', 'api'),
73
+ 'AGENTS.md',
74
+ generateAPIPrompt()
75
+ );
76
+ await ensureWithCreate(
77
+ join(rootDir, '.agents', 'agentuity', 'sdk', 'web'),
78
+ 'AGENTS.md',
79
+ generateWebPrompt()
80
+ );
81
+
82
+ // Write reference files to src/[type]/AGENTS.md (pointing to .agents/)
83
+ await ensure(join(rootDir, 'src', 'agent'), 'AGENTS.md', generateReferenceContent('agent'));
84
+ await ensure(join(rootDir, 'src', 'api'), 'AGENTS.md', generateReferenceContent('api'));
85
+ await ensure(join(rootDir, 'src', 'web'), 'AGENTS.md', generateReferenceContent('web'));
52
86
  }
package/src/cli.ts CHANGED
@@ -914,8 +914,18 @@ async function registerSubcommand(
914
914
  if (opt.hasDefault) {
915
915
  const defaultValue =
916
916
  typeof opt.defaultValue === 'function' ? opt.defaultValue() : opt.defaultValue;
917
- cmd.option(`--no-${flag}`, desc);
918
- cmd.option(flagSpec, desc, defaultValue);
917
+ // For boolean flags with default true, only show the --no-* flag in help
918
+ // since that's the only actionable flag users need to know about.
919
+ // The positive flag is hidden but still functional.
920
+ const baseDesc = desc.replace(/\s*\(use\s+--no-\S+\s+to\s+skip\)/i, '').trim();
921
+ const negatedDesc = baseDesc.toLowerCase().startsWith('run ')
922
+ ? `Skip ${baseDesc.slice(4)}`
923
+ : `Do not ${baseDesc.charAt(0).toLowerCase()}${baseDesc.slice(1)}`;
924
+ cmd.option(`--no-${flag}`, negatedDesc);
925
+ const positiveOption = cmd.createOption(flagSpec, baseDesc);
926
+ positiveOption.default(defaultValue);
927
+ positiveOption.hideHelp();
928
+ cmd.addOption(positiveOption);
919
929
  } else {
920
930
  cmd.option(flagSpec, desc);
921
931
  }
@@ -0,0 +1,36 @@
1
+ import { createSubcommand } from '../../types';
2
+ import { getCommand } from '../../command-prefix';
3
+ import { z } from 'zod';
4
+
5
+ const ApikeyResponseSchema = z.object({
6
+ apiKey: z.string().describe('The API key for the authenticated user'),
7
+ });
8
+
9
+ export const apikeyCommand = createSubcommand({
10
+ name: 'apikey',
11
+ description: 'Display the API key for the currently authenticated user',
12
+ tags: ['read-only', 'fast', 'requires-auth'],
13
+ requires: { auth: true },
14
+ idempotent: true,
15
+ schema: {
16
+ response: ApikeyResponseSchema,
17
+ },
18
+ examples: [
19
+ { command: getCommand('auth apikey'), description: 'Print the API key' },
20
+ { command: getCommand('--json auth apikey'), description: 'Output API key in JSON format' },
21
+ ],
22
+
23
+ async handler(ctx) {
24
+ const { auth, options } = ctx;
25
+
26
+ const result = {
27
+ apiKey: auth.apiKey,
28
+ };
29
+
30
+ if (!options.json) {
31
+ console.log(result.apiKey);
32
+ }
33
+
34
+ return result;
35
+ },
36
+ });
@@ -1,4 +1,5 @@
1
1
  import { createCommand } from '../../types';
2
+ import { apikeyCommand } from './apikey';
2
3
  import { loginCommand } from './login';
3
4
  import { logoutCommand } from './logout';
4
5
  import { signupCommand } from './signup';
@@ -14,5 +15,12 @@ export const command = createCommand({
14
15
  { command: getCommand('auth login'), description: 'Login to your account' },
15
16
  { command: getCommand('auth whoami'), description: 'Show current user info' },
16
17
  ],
17
- subcommands: [loginCommand, logoutCommand, signupCommand, whoamiCommand, sshSubcommand],
18
+ subcommands: [
19
+ apikeyCommand,
20
+ loginCommand,
21
+ logoutCommand,
22
+ signupCommand,
23
+ whoamiCommand,
24
+ sshSubcommand,
25
+ ],
18
26
  });
@@ -967,9 +967,19 @@ function hasValidatorCall(args: unknown[]): ValidatorInfo {
967
967
  if (callExpr.callee.type === 'Identifier') {
968
968
  const identifier = callExpr.callee as ASTNodeIdentifier;
969
969
  if (identifier.name === 'validator') {
970
- // Try to extract schema variables from validator({ input, output })
970
+ // Try to extract schema variables from validator({ input, output, stream })
971
971
  const schemas = extractValidatorSchemas(callExpr);
972
- return { hasValidator: true, ...schemas };
972
+ // Return if we found any schema variables OR a stream flag
973
+ if (
974
+ schemas.inputSchemaVariable ||
975
+ schemas.outputSchemaVariable ||
976
+ schemas.stream !== undefined
977
+ ) {
978
+ return { hasValidator: true, ...schemas };
979
+ }
980
+ // Try Hono validator('json', callback) pattern
981
+ const honoSchemas = extractHonoValidatorSchema(callExpr);
982
+ return { hasValidator: true, ...honoSchemas };
973
983
  }
974
984
  // Check for zValidator('json', schema)
975
985
  if (identifier.name === 'zValidator') {
@@ -1129,6 +1139,114 @@ function extractZValidatorSchema(callExpr: ASTCallExpression): {
1129
1139
  return result;
1130
1140
  }
1131
1141
 
1142
+ /**
1143
+ * Extract schema from Hono validator('json', callback) pattern
1144
+ * Example: validator('json', (value, c) => { const result = mySchema['~standard'].validate(value); ... })
1145
+ * Searches the callback function body for schema.validate() or schema['~standard'].validate() calls
1146
+ */
1147
+ function extractHonoValidatorSchema(callExpr: ASTCallExpression): {
1148
+ inputSchemaVariable?: string;
1149
+ } {
1150
+ const result: { inputSchemaVariable?: string } = {};
1151
+
1152
+ // Hono validator requires at least 2 arguments: validator(target, callback)
1153
+ if (!callExpr.arguments || callExpr.arguments.length < 2) {
1154
+ return result;
1155
+ }
1156
+
1157
+ // First argument should be 'json' literal (only extract for JSON validation)
1158
+ const targetArg = callExpr.arguments[0] as ASTNode;
1159
+ if (targetArg.type === 'Literal') {
1160
+ const targetValue = (targetArg as ASTLiteral).value;
1161
+ if (typeof targetValue === 'string' && targetValue !== 'json') {
1162
+ return result;
1163
+ }
1164
+ } else {
1165
+ return result;
1166
+ }
1167
+
1168
+ // Second argument should be a function (arrow or regular)
1169
+ const callbackArg = callExpr.arguments[1] as ASTNode;
1170
+ if (
1171
+ callbackArg.type !== 'ArrowFunctionExpression' &&
1172
+ callbackArg.type !== 'FunctionExpression'
1173
+ ) {
1174
+ return result;
1175
+ }
1176
+
1177
+ // Get the function body
1178
+ const funcExpr = callbackArg as {
1179
+ body?: ASTNode;
1180
+ };
1181
+
1182
+ if (!funcExpr.body) {
1183
+ return result;
1184
+ }
1185
+
1186
+ // Search the function body for schema.validate() or schema['~standard'].validate() calls
1187
+ const schemaVar = findSchemaValidateCall(funcExpr.body);
1188
+ if (schemaVar) {
1189
+ result.inputSchemaVariable = schemaVar;
1190
+ }
1191
+
1192
+ return result;
1193
+ }
1194
+
1195
+ /**
1196
+ * Recursively search AST for schema.validate() or schema['~standard'].validate() calls
1197
+ * Returns the schema variable name if found
1198
+ */
1199
+ function findSchemaValidateCall(node: ASTNode): string | undefined {
1200
+ if (!node || typeof node !== 'object') return undefined;
1201
+
1202
+ // Check if this is a CallExpression with .validate()
1203
+ if (node.type === 'CallExpression') {
1204
+ const callExpr = node as ASTCallExpression;
1205
+
1206
+ // Check for schema['~standard'].validate(value) pattern
1207
+ // AST: CallExpression -> MemberExpression(validate) -> MemberExpression(['~standard']) -> Identifier(schema)
1208
+ if (callExpr.callee.type === 'MemberExpression') {
1209
+ const member = callExpr.callee as ASTMemberExpression;
1210
+ const propName =
1211
+ member.property.type === 'Identifier'
1212
+ ? (member.property as ASTNodeIdentifier).name
1213
+ : undefined;
1214
+
1215
+ if (propName === 'validate') {
1216
+ // Check if the object is schema['~standard'] or just schema
1217
+ if (member.object.type === 'MemberExpression') {
1218
+ // schema['~standard'].validate() pattern
1219
+ const innerMember = member.object as ASTMemberExpression;
1220
+ if (innerMember.object.type === 'Identifier') {
1221
+ return (innerMember.object as ASTNodeIdentifier).name;
1222
+ }
1223
+ } else if (member.object.type === 'Identifier') {
1224
+ // schema.validate() pattern
1225
+ return (member.object as ASTNodeIdentifier).name;
1226
+ }
1227
+ }
1228
+ }
1229
+ }
1230
+
1231
+ // Recursively search child nodes
1232
+ for (const key of Object.keys(node)) {
1233
+ const value = (node as unknown as Record<string, unknown>)[key];
1234
+ if (Array.isArray(value)) {
1235
+ for (const item of value) {
1236
+ if (item && typeof item === 'object') {
1237
+ const found = findSchemaValidateCall(item as ASTNode);
1238
+ if (found) return found;
1239
+ }
1240
+ }
1241
+ } else if (value && typeof value === 'object') {
1242
+ const found = findSchemaValidateCall(value as ASTNode);
1243
+ if (found) return found;
1244
+ }
1245
+ }
1246
+
1247
+ return undefined;
1248
+ }
1249
+
1132
1250
  export async function parseRoute(
1133
1251
  rootDir: string,
1134
1252
  filename: string,
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { join } from 'node:path';
7
- import type { Logger, WorkbenchConfig } from '../../types';
7
+ import type { Logger, WorkbenchConfig, AnalyticsConfig } from '../../types';
8
8
  import { discoverRoutes } from './vite/route-discovery';
9
9
 
10
10
  interface GenerateEntryOptions {
@@ -14,6 +14,7 @@ interface GenerateEntryOptions {
14
14
  logger: Logger;
15
15
  mode: 'dev' | 'prod';
16
16
  workbench?: WorkbenchConfig;
17
+ analytics?: boolean | AnalyticsConfig;
17
18
  vitePort?: number; // Port of Vite asset server (dev mode only)
18
19
  }
19
20
 
@@ -21,7 +22,8 @@ interface GenerateEntryOptions {
21
22
  * Generate entry file with clean Vite-native architecture
22
23
  */
23
24
  export async function generateEntryFile(options: GenerateEntryOptions): Promise<void> {
24
- const { rootDir, projectId, deploymentId, logger, mode, workbench, vitePort } = options;
25
+ const { rootDir, projectId, deploymentId, logger, mode, workbench, analytics, vitePort } =
26
+ options;
25
27
 
26
28
  const srcDir = join(rootDir, 'src');
27
29
  const generatedDir = join(srcDir, 'generated');
@@ -70,7 +72,11 @@ export async function generateEntryFile(options: GenerateEntryOptions): Promise<
70
72
  ` loadBuildMetadata,`,
71
73
  ` createWorkbenchRouter,`,
72
74
  ` bootstrapRuntimeEnv,`,
73
- ` patchBunS3ForStorageDev,`,
75
+ ` patchBunS3ForStorageDev,`,
76
+ ` getOrganizationId,`,
77
+ ` getProjectId,`,
78
+ ` isDevMode as runtimeIsDevMode,`,
79
+ ` createWebSessionMiddleware,`,
74
80
  ];
75
81
 
76
82
  const imports = [
@@ -200,6 +206,126 @@ if (isDevelopment() && process.env.VITE_PORT) {
200
206
  // See: https://github.com/oven-sh/bun/issues/20183
201
207
  const getEnv = (key: string) => process.env[key];
202
208
  const isDevelopment = () => getEnv('NODE' + '_' + 'ENV') !== 'production';
209
+ `;
210
+
211
+ // Generate analytics config and injection helper
212
+ const analyticsEnabled = analytics !== false;
213
+ const analyticsConfig: AnalyticsConfig = typeof analytics === 'object' ? analytics : {};
214
+
215
+ const analyticsHelper = analyticsEnabled
216
+ ? `
217
+ // Analytics configuration - edit agentuity.config.ts to configure
218
+ const analyticsConfig = {
219
+ enabled: ${analyticsConfig.enabled !== false},
220
+ requireConsent: ${analyticsConfig.requireConsent ?? false},
221
+ trackClicks: ${analyticsConfig.trackClicks ?? true},
222
+ trackScroll: ${analyticsConfig.trackScroll ?? true},
223
+ trackOutboundLinks: ${analyticsConfig.trackOutboundLinks ?? true},
224
+ trackForms: ${analyticsConfig.trackForms ?? false},
225
+ trackWebVitals: ${analyticsConfig.trackWebVitals ?? true},
226
+ trackErrors: ${analyticsConfig.trackErrors ?? true},
227
+ trackSPANavigation: ${analyticsConfig.trackSPANavigation ?? true},
228
+ sampleRate: ${analyticsConfig.sampleRate ?? 1},
229
+ excludePatterns: ${JSON.stringify(analyticsConfig.excludePatterns ?? [])},
230
+ globalProperties: ${JSON.stringify(analyticsConfig.globalProperties ?? {})},
231
+ };
232
+
233
+ // Inject analytics config and script into HTML
234
+ // Note: Only static config is injected (org, project, devmode, tracking options)
235
+ // Session and thread IDs are read from cookies by the beacon script
236
+ function injectAnalytics(html: string): string {
237
+ if (!analyticsConfig.enabled) return html;
238
+
239
+ const orgId = getOrganizationId() || '';
240
+ const projectId = getProjectId() || '';
241
+ const isDevmode = runtimeIsDevMode();
242
+
243
+ // Only include static config - session/thread come from cookies
244
+ const pageConfig = {
245
+ ...analyticsConfig,
246
+ orgId,
247
+ projectId,
248
+ isDevmode,
249
+ };
250
+
251
+ const configScript = \`<script>window.__AGENTUITY_ANALYTICS__=\${JSON.stringify(pageConfig)};</script>\`;
252
+ // Session script sets cookies and window.__AGENTUITY_SESSION__ (dynamic, not cached)
253
+ const sessionScript = '<script src="/_agentuity/webanalytics/session.js" async></script>';
254
+ // Beacon script reads from __AGENTUITY_SESSION__ and sends events (static, cached)
255
+ const beaconScript = '<script src="/_agentuity/webanalytics/analytics.js" async></script>';
256
+ const injection = configScript + sessionScript + beaconScript;
257
+
258
+ // Inject before </head> or at start of <body>
259
+ if (html.includes('</head>')) {
260
+ return html.replace('</head>', injection + '</head>');
261
+ }
262
+ if (html.includes('<body')) {
263
+ return html.replace(/<body([^>]*)>/, \`<body$1>\${injection}\`);
264
+ }
265
+ return injection + html;
266
+ }
267
+
268
+ // Serve analytics routes
269
+ function registerAnalyticsRoutes(app: ReturnType<typeof createRouter>): void {
270
+ // Dynamic session config script - sets cookies and returns session/thread IDs
271
+ // This endpoint is NOT cached - it generates unique session data per request
272
+ app.get('/_agentuity/webanalytics/session.js', createWebSessionMiddleware(), async (c: Context) => {
273
+ const sessionId = c.get('sessionId') || '';
274
+ const thread = c.get('thread');
275
+ const threadId = thread?.id || '';
276
+
277
+ const sessionScript = \`window.__AGENTUITY_SESSION__={sessionId:"\${sessionId}",threadId:"\${threadId}"};\`;
278
+
279
+ return new Response(sessionScript, {
280
+ headers: {
281
+ 'Content-Type': 'application/javascript; charset=utf-8',
282
+ 'Cache-Control': 'no-store, no-cache, must-revalidate',
283
+ },
284
+ });
285
+ });
286
+
287
+ // Static beacon script - can be cached
288
+ app.get('/_agentuity/webanalytics/analytics.js', async (c: Context) => {
289
+ // Beacon waits for window.__AGENTUITY_SESSION__ before sending events
290
+ const beaconScript = \`(function(){
291
+ var w=window,d=document,c=w.__AGENTUITY_ANALYTICS__;
292
+ if(!c||!c.enabled)return;
293
+ var q=[],t=null,sr=false,E='/_agentuity/webanalytics/collect',geo=null;
294
+ function id(){return crypto.randomUUID?crypto.randomUUID():Date.now()+'-'+Math.random().toString(36).substr(2,9)}
295
+ function base(type){var e={id:id(),timestamp:Date.now(),timezone_offset:new Date().getTimezoneOffset(),event_type:type,url:location.href,path:location.pathname,referrer:d.referrer||'',title:d.title||'',screen_width:screen.width||0,screen_height:screen.height||0,viewport_width:innerWidth||0,viewport_height:innerHeight||0,device_pixel_ratio:devicePixelRatio||1,user_agent:navigator.userAgent||'',language:navigator.language||''};if(geo){e.country=geo.country||'';e.region=geo.region||'';e.city=geo.city||'';e.timezone=geo.timezone||''}return e}
296
+ fetch('https://agentuity.sh/location').then(function(r){return r.json()}).then(function(g){geo=g;try{sessionStorage.setItem('agentuity_geo',JSON.stringify(g))}catch(e){}}).catch(function(){try{var cached=sessionStorage.getItem('agentuity_geo');if(cached)geo=JSON.parse(cached)}catch(e){}});try{var cached=sessionStorage.getItem('agentuity_geo');if(cached)geo=JSON.parse(cached)}catch(e){}
297
+ function getSession(){return w.__AGENTUITY_SESSION__}
298
+ function waitForSession(cb){var s=getSession();if(s){cb(s);return}var attempts=0,maxAttempts=50;var iv=setInterval(function(){s=getSession();if(s||++attempts>=maxAttempts){clearInterval(iv);cb(s)}},100)}
299
+ function doFlush(s){if(!q.length)return;var events=q.splice(0);if(c.isDevmode){console.debug('[Agentuity Analytics]',events);return}var sid=s?s.sessionId:'',tid=s?s.threadId:'';var p={org_id:c.orgId,project_id:c.projectId,session_id:sid,thread_id:tid,visitor_id:localStorage.getItem('agentuity_vid')||'vid_'+id(),events:events};try{localStorage.setItem('agentuity_vid',p.visitor_id)}catch(e){}navigator.sendBeacon?navigator.sendBeacon(E,JSON.stringify(p)):fetch(E,{method:'POST',body:JSON.stringify(p),keepalive:true}).catch(function(){})}
300
+ function flush(){if(sr){doFlush(getSession())}else{waitForSession(function(s){sr=true;doFlush(s)})}}
301
+ function queue(e){if(c.sampleRate<1&&Math.random()>c.sampleRate)return;q.push(e);q.length>=10?flush():t||(t=setTimeout(function(){t=null;flush()},5000))}
302
+ function pv(){var e=base('pageview');if(performance.getEntriesByType){var n=performance.getEntriesByType('navigation')[0];if(n){e.load_time=Math.round(n.loadEventEnd-n.startTime);e.dom_ready=Math.round(n.domContentLoadedEventEnd-n.startTime);e.ttfb=Math.round(n.responseStart-n.requestStart)}}queue(e)}
303
+ w.addEventListener('visibilitychange',function(){d.visibilityState==='hidden'&&flush()});
304
+ w.addEventListener('pagehide',flush);
305
+ if(c.trackSPANavigation){var op=history.pushState,or=history.replaceState,cp=location.pathname;function ch(){var np=location.pathname;np!==cp&&(cp=np,pv())}history.pushState=function(){op.apply(this,arguments);ch()};history.replaceState=function(){or.apply(this,arguments);ch()};w.addEventListener('popstate',ch)}
306
+ if(c.trackErrors){w.addEventListener('error',function(e){var ev=base('error');ev.event_name='js_error';ev.event_data={message:e.message||'Unknown',filename:e.filename||'',lineno:e.lineno||0};queue(ev)});w.addEventListener('unhandledrejection',function(e){var ev=base('error');ev.event_name='unhandled_rejection';ev.event_data={message:e.reason instanceof Error?e.reason.message:String(e.reason)};queue(ev)})}
307
+ if(c.trackClicks){d.addEventListener('click',function(e){var t=e.target;if(!t)return;var a=t.closest('[data-analytics]');if(!a)return;var ev=base('click');ev.event_name=a.getAttribute('data-analytics');queue(ev)},true)}
308
+ if(c.trackScroll){var ms=new Set(),mx=0;function gs(){var st=w.scrollY||d.documentElement.scrollTop,sh=d.documentElement.scrollHeight-d.documentElement.clientHeight;return sh<=0?100:Math.min(100,Math.round(st/sh*100))}w.addEventListener('scroll',function(){var dp=gs();if(dp>mx)mx=dp;[25,50,75,100].forEach(function(m){if(dp>=m&&!ms.has(m)){ms.add(m);var ev=base('scroll');ev.event_name='scroll_'+m;ev.scroll_depth=m;queue(ev)}})},{passive:true})}
309
+ if(c.trackWebVitals!==false&&typeof PerformanceObserver!=='undefined'){var wvLcp=0,wvCls=0,wvInp=0,wvPath=location.pathname,wvSent={};function wvReset(){wvLcp=0;wvCls=0;wvInp=0;wvSent={}}function wvSend(){var p=wvPath;if(wvLcp>0&&!wvSent.lcp){wvSent.lcp=1;var ev=base('web_vital');ev.event_name='lcp';ev.lcp=Math.round(wvLcp);ev.path=p;queue(ev)}if(!wvSent.cls){wvSent.cls=1;var ev=base('web_vital');ev.event_name='cls';ev.cls=Math.round(wvCls*1000)/1000;ev.path=p;queue(ev)}if(wvInp>0&&!wvSent.inp){wvSent.inp=1;var ev=base('web_vital');ev.event_name='inp';ev.inp=Math.round(wvInp);ev.path=p;queue(ev)}flush()}try{var fcpObs=new PerformanceObserver(function(l){l.getEntries().forEach(function(e){if(e.name==='first-contentful-paint'){var ev=base('web_vital');ev.event_name='fcp';ev.fcp=Math.round(e.startTime);queue(ev);flush();fcpObs.disconnect()}})});fcpObs.observe({type:'paint',buffered:true})}catch(e){}try{new PerformanceObserver(function(l){var entries=l.getEntries();if(entries.length)wvLcp=entries[entries.length-1].startTime}).observe({type:'largest-contentful-paint',buffered:true})}catch(e){}try{new PerformanceObserver(function(l){l.getEntries().forEach(function(e){if(!e.hadRecentInput&&e.value)wvCls+=e.value})}).observe({type:'layout-shift',buffered:true})}catch(e){}try{new PerformanceObserver(function(l){l.getEntries().forEach(function(e){if(e.duration&&e.duration>wvInp)wvInp=e.duration})}).observe({type:'event',buffered:true})}catch(e){}d.addEventListener('visibilitychange',function(){if(d.visibilityState==='hidden')wvSend()});w.addEventListener('pagehide',wvSend);if(c.trackSPANavigation){var wvOp=history.pushState,wvOr=history.replaceState;function wvNav(){var np=location.pathname;if(np!==wvPath){wvSend();wvPath=np;wvReset()}}history.pushState=function(){wvOp.apply(this,arguments);wvNav()};history.replaceState=function(){wvOr.apply(this,arguments);wvNav()};w.addEventListener('popstate',wvNav)}}
310
+ d.readyState==='complete'?pv():w.addEventListener('load',pv);
311
+ w.agentuityAnalytics={track:function(n,p){var e=base('custom');e.event_name=n;if(p)e.event_data=p;queue(e)},flush:flush};
312
+ })();\`;
313
+
314
+ return new Response(beaconScript, {
315
+ headers: {
316
+ 'Content-Type': 'application/javascript; charset=utf-8',
317
+ 'Cache-Control': 'public, max-age=3600',
318
+ },
319
+ });
320
+ });
321
+ }
322
+ `
323
+ : `
324
+ // Analytics disabled
325
+ function injectAnalytics(html: string): string {
326
+ return html;
327
+ }
328
+ function registerAnalyticsRoutes(_app: ReturnType<typeof createRouter>): void {}
203
329
  `;
204
330
 
205
331
  // Web routes (runtime mode detection)
@@ -207,6 +333,9 @@ const isDevelopment = () => getEnv('NODE' + '_' + 'ENV') !== 'production';
207
333
  if (hasWebFrontend) {
208
334
  webRoutes = `
209
335
  // Web routes - Runtime mode detection (dev proxies to Vite, prod serves static)
336
+ // Note: Session/thread cookies are set by /_agentuity/webanalytics/session.js (loaded via script tag)
337
+ // This keeps the HTML response static and cacheable
338
+
210
339
  if (isDevelopment()) {
211
340
  // Development mode: Proxy HTML from Vite to enable React Fast Refresh
212
341
  const VITE_ASSET_PORT = parseInt(process.env.VITE_PORT || '${vitePort || 5173}', 10);
@@ -219,12 +348,15 @@ if (isDevelopment()) {
219
348
  const res = await fetch(viteUrl, { signal: AbortSignal.timeout(10000) });
220
349
 
221
350
  // Get HTML text and transform relative paths to absolute
222
- const html = await res.text();
223
- const transformedHtml = html
351
+ let html = await res.text();
352
+ html = html
224
353
  .replace(/src="\\.\\//g, 'src="/src/web/')
225
354
  .replace(/href="\\.\\//g, 'href="/src/web/');
226
355
 
227
- return new Response(transformedHtml, {
356
+ // Inject analytics config and script (session/thread read from cookies by beacon)
357
+ html = injectAnalytics(html);
358
+
359
+ return new Response(html, {
228
360
  status: res.status,
229
361
  headers: res.headers,
230
362
  });
@@ -255,15 +387,24 @@ if (isDevelopment()) {
255
387
  } else {
256
388
  // Production mode: Serve static files from bundled output
257
389
  const indexHtmlPath = import.meta.dir + '/client/index.html';
258
- const indexHtml = existsSync(indexHtmlPath)
390
+ const baseIndexHtml = existsSync(indexHtmlPath)
259
391
  ? readFileSync(indexHtmlPath, 'utf-8')
260
392
  : '';
261
393
 
262
- if (!indexHtml) {
394
+ if (!baseIndexHtml) {
263
395
  otel.logger.warn('Production HTML not found at %s', indexHtmlPath);
264
396
  }
397
+
398
+ const prodHtmlHandler = (c: Context) => {
399
+ if (!baseIndexHtml) {
400
+ return c.text('Production build incomplete', 500);
401
+ }
402
+ // Inject analytics config and script (session/thread loaded via session.js)
403
+ const html = injectAnalytics(baseIndexHtml);
404
+ return c.html(html);
405
+ };
265
406
 
266
- app.get('/', (c: Context) => indexHtml ? c.html(indexHtml) : c.text('Production build incomplete', 500));
407
+ app.get('/', prodHtmlHandler);
267
408
 
268
409
  // Serve static assets from /assets/* (Vite bundled output)
269
410
  app.use('/assets/*', serveStatic({ root: import.meta.dir + '/client' }));
@@ -285,7 +426,7 @@ if (isDevelopment()) {
285
426
  if (/\\.[a-zA-Z0-9]+$/.test(path)) {
286
427
  return c.notFound();
287
428
  }
288
- return c.html(indexHtml);
429
+ return prodHtmlHandler(c);
289
430
  });
290
431
  }
291
432
  `;
@@ -412,6 +553,8 @@ ${imports.join('\n')}
412
553
 
413
554
  ${modeDetection}
414
555
 
556
+ ${analyticsHelper}
557
+
415
558
  // Step 0: Bootstrap runtime environment (load profile-specific .env files)
416
559
  // Only in development - production env vars are injected by platform
417
560
  // This must happen BEFORE any imports that depend on environment variables
@@ -482,6 +625,10 @@ await sessionProvider.initialize(appState);
482
625
  // Step 6: Mount routes (AFTER middleware is applied)
483
626
 
484
627
  ${healthRoutes}
628
+
629
+ // Register analytics routes (if enabled)
630
+ registerAnalyticsRoutes(app);
631
+
485
632
  ${assetProxyRoutes}
486
633
  ${apiMount}
487
634
  ${workbenchApiMount}
@@ -311,6 +311,7 @@ function extractAgentMetadata(
311
311
  */
312
312
  async function extractEvalMetadata(
313
313
  evalsPath: string,
314
+ relativeEvalsPath: string,
314
315
  agentId: string,
315
316
  projectId: string,
316
317
  deploymentId: string,
@@ -325,7 +326,7 @@ async function extractEvalMetadata(
325
326
  const evalsSource = await evalsFile.text();
326
327
  return extractEvalsFromSource(
327
328
  evalsSource,
328
- evalsPath,
329
+ relativeEvalsPath,
329
330
  agentId,
330
331
  projectId,
331
332
  deploymentId,
@@ -569,8 +570,10 @@ export async function discoverAgents(
569
570
  // 2. Check for evals in separate eval.ts file in same directory
570
571
  const agentDir = dirname(filePath);
571
572
  const evalsPath = join(agentDir, 'eval.ts');
573
+ const relativeEvalsPath = relative(rootDir, evalsPath);
572
574
  const evalsInSeparateFile = await extractEvalMetadata(
573
575
  evalsPath,
576
+ relativeEvalsPath,
574
577
  agentMetadata.agentId,
575
578
  projectId,
576
579
  deploymentId,
@@ -96,7 +96,7 @@ export function agentuityPlugin(options: AgentuityPluginOptions): Plugin {
96
96
  const lifecycleResult = await generateLifecycleTypes(rootDir, srcDir, logger);
97
97
  logger.debug(`[vite-plugin] generateLifecycleTypes returned: ${lifecycleResult}`);
98
98
 
99
- // Generate entry file (pass workbench config for route mounting)
99
+ // Generate entry file (pass workbench and analytics config)
100
100
  await generateEntryFile({
101
101
  rootDir,
102
102
  projectId,
@@ -104,6 +104,7 @@ export function agentuityPlugin(options: AgentuityPluginOptions): Plugin {
104
104
  logger,
105
105
  mode: dev ? 'dev' : 'prod',
106
106
  workbench: workbenchConfig.enabled ? workbenchConfig : undefined,
107
+ analytics: config?.analytics,
107
108
  });
108
109
 
109
110
  logger.trace('buildStart: Discovery complete');
@@ -84,6 +84,43 @@ export function generateAgentRegistry(srcDir: string, agents: AgentMetadata[]):
84
84
  });
85
85
  }
86
86
 
87
+ // Collect eval files that need to be imported for createEval calls to run
88
+ // These are eval.ts files in the same directory as agents that have evals
89
+ const evalImports: string[] = [];
90
+ const seenEvalPaths = new Set<string>();
91
+
92
+ for (const agent of sortedAgents) {
93
+ if (agent.evals && agent.evals.length > 0) {
94
+ // Check if any eval comes from a separate eval.ts file (not the agent file itself)
95
+ for (const evalMeta of agent.evals) {
96
+ // Skip if eval is defined in the agent file itself
97
+ if (evalMeta.filename === agent.filename) continue;
98
+
99
+ // Build the relative path for the eval file
100
+ let evalRelativePath = evalMeta.filename;
101
+ if (evalRelativePath.startsWith('./agent/')) {
102
+ evalRelativePath = evalRelativePath
103
+ .replace(/^\.\/agent\//, '../agent/')
104
+ .replace(/\.tsx?$/, '.js');
105
+ } else if (evalRelativePath.startsWith('src/agent/')) {
106
+ evalRelativePath = evalRelativePath
107
+ .replace(/^src\/agent\//, '../agent/')
108
+ .replace(/\.tsx?$/, '.js');
109
+ } else if (evalRelativePath.includes('/src/agent/')) {
110
+ // Handle absolute paths by extracting the relative part
111
+ evalRelativePath = evalRelativePath
112
+ .replace(/^.*\/src\/agent\//, '../agent/')
113
+ .replace(/\.tsx?$/, '.js');
114
+ }
115
+ // Avoid duplicate imports
116
+ if (!seenEvalPaths.has(evalRelativePath)) {
117
+ seenEvalPaths.add(evalRelativePath);
118
+ evalImports.push(`import '${evalRelativePath}';`);
119
+ }
120
+ }
121
+ }
122
+ }
123
+
87
124
  // Generate imports for all agents
88
125
  const imports = sortedAgents
89
126
  .map(({ name, filename }) => {
@@ -172,11 +209,21 @@ export function generateAgentRegistry(srcDir: string, agents: AgentMetadata[]):
172
209
  })
173
210
  .join('\n');
174
211
 
212
+ // Build eval imports section (side-effect imports for createEval registration)
213
+ const evalImportsSection =
214
+ evalImports.length > 0
215
+ ? `
216
+ // Eval file imports (side-effect imports to register evals via createEval)
217
+ ${evalImports.join('\n')}
218
+ `
219
+ : '';
220
+
175
221
  const generatedContent = `// @generated
176
222
  // Auto-generated by Agentuity - DO NOT EDIT
177
223
  ${imports}
178
224
  import type { AgentRunner } from '@agentuity/runtime';
179
225
  import type { InferInput, InferOutput } from '@agentuity/core';
226
+ ${evalImportsSection}
180
227
 
181
228
  // ============================================================================
182
229
  // Schema Type Exports