@agentuity/cli 0.0.112 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@
6
6
  import { join } from 'node:path';
7
7
  import type { Logger, WorkbenchConfig, AnalyticsConfig } from '../../types';
8
8
  import { discoverRoutes } from './vite/route-discovery';
9
+ import { generateWebAnalyticsFile } from './webanalytics-generator';
9
10
 
10
11
  interface GenerateEntryOptions {
11
12
  rootDir: string;
@@ -31,6 +32,14 @@ export async function generateEntryFile(options: GenerateEntryOptions): Promise<
31
32
 
32
33
  logger.trace(`Generating unified entry file (supports both dev and prod modes)...`);
33
34
 
35
+ // Check if analytics is enabled
36
+ const analyticsEnabled = analytics !== false;
37
+
38
+ // Generate web analytics files only if enabled
39
+ if (analyticsEnabled) {
40
+ await generateWebAnalyticsFile({ rootDir, logger, analytics });
41
+ }
42
+
34
43
  // Discover routes to determine which files need to be imported
35
44
  const { routeInfoList } = await discoverRoutes(srcDir, projectId, deploymentId, logger);
36
45
 
@@ -73,10 +82,6 @@ export async function generateEntryFile(options: GenerateEntryOptions): Promise<
73
82
  ` createWorkbenchRouter,`,
74
83
  ` bootstrapRuntimeEnv,`,
75
84
  ` patchBunS3ForStorageDev,`,
76
- ` getOrganizationId,`,
77
- ` getProjectId,`,
78
- ` isDevMode as runtimeIsDevMode,`,
79
- ` createWebSessionMiddleware,`,
80
85
  ];
81
86
 
82
87
  const imports = [
@@ -89,6 +94,10 @@ export async function generateEntryFile(options: GenerateEntryOptions): Promise<
89
94
  ].filter(Boolean);
90
95
 
91
96
  imports.push(`import { type LogLevel } from '@agentuity/core';`);
97
+ if (analyticsEnabled) {
98
+ imports.push(`import { injectAnalytics, registerAnalyticsRoutes } from './webanalytics.js';`);
99
+ imports.push(`import { analyticsConfig } from './analytics-config.js';`);
100
+ }
92
101
 
93
102
  // Generate route mounting code for all discovered routes
94
103
  // Sort route files for deterministic output
@@ -206,126 +215,6 @@ if (isDevelopment() && process.env.VITE_PORT) {
206
215
  // See: https://github.com/oven-sh/bun/issues/20183
207
216
  const getEnv = (key: string) => process.env[key];
208
217
  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 {}
329
218
  `;
330
219
 
331
220
  // Web routes (runtime mode detection)
@@ -353,8 +242,12 @@ if (isDevelopment()) {
353
242
  .replace(/src="\\.\\//g, 'src="/src/web/')
354
243
  .replace(/href="\\.\\//g, 'href="/src/web/');
355
244
 
356
- // Inject analytics config and script (session/thread read from cookies by beacon)
357
- html = injectAnalytics(html);
245
+ ${
246
+ analyticsEnabled
247
+ ? ` // Inject analytics config and script (session/thread read from cookies by beacon)
248
+ html = injectAnalytics(html, analyticsConfig);`
249
+ : ''
250
+ }
358
251
 
359
252
  return new Response(html, {
360
253
  status: res.status,
@@ -399,9 +292,13 @@ if (isDevelopment()) {
399
292
  if (!baseIndexHtml) {
400
293
  return c.text('Production build incomplete', 500);
401
294
  }
402
- // Inject analytics config and script (session/thread loaded via session.js)
403
- const html = injectAnalytics(baseIndexHtml);
404
- return c.html(html);
295
+ ${
296
+ analyticsEnabled
297
+ ? ` // Inject analytics config and script (session/thread loaded via session.js)
298
+ const html = injectAnalytics(baseIndexHtml, analyticsConfig);
299
+ return c.html(html);`
300
+ : ` return c.html(baseIndexHtml);`
301
+ }
405
302
  };
406
303
 
407
304
  app.get('/', prodHtmlHandler);
@@ -553,8 +450,6 @@ ${imports.join('\n')}
553
450
 
554
451
  ${modeDetection}
555
452
 
556
- ${analyticsHelper}
557
-
558
453
  // Step 0: Bootstrap runtime environment (load profile-specific .env files)
559
454
  // Only in development - production env vars are injected by platform
560
455
  // This must happen BEFORE any imports that depend on environment variables
@@ -589,11 +484,17 @@ app.use('*', createBaseMiddleware({
589
484
  meter: otel.meter,
590
485
  }));
591
486
 
592
- app.use('/_agentuity/*', createCorsMiddleware());
487
+ app.use('/_agentuity/workbench/*', createCorsMiddleware());
593
488
  app.use('/api/*', createCorsMiddleware());
594
489
 
595
490
  // Critical: otelMiddleware creates session/thread/waitUntilHandler
596
- app.use('/_agentuity/*', createOtelMiddleware());
491
+ // Only apply to routes that need full session tracking:
492
+ // - /api/* routes (agent/API invocations)
493
+ // - /_agentuity/workbench/* routes (workbench API)
494
+ // Explicitly excluded (no session tracking, no Catalyst events):
495
+ // - /_agentuity/webanalytics/* (web analytics - uses lightweight cookie-only middleware)
496
+ // - /_agentuity/health, /_agentuity/ready, /_agentuity/idle (health checks)
497
+ app.use('/_agentuity/workbench/*', createOtelMiddleware());
597
498
  app.use('/api/*', createOtelMiddleware());
598
499
 
599
500
  // Critical: agentMiddleware sets up agent context
@@ -626,8 +527,12 @@ await sessionProvider.initialize(appState);
626
527
 
627
528
  ${healthRoutes}
628
529
 
629
- // Register analytics routes (if enabled)
630
- registerAnalyticsRoutes(app);
530
+ ${
531
+ analyticsEnabled
532
+ ? `// Register analytics routes
533
+ registerAnalyticsRoutes(app);`
534
+ : ''
535
+ }
631
536
 
632
537
  ${assetProxyRoutes}
633
538
  ${apiMount}
@@ -112,7 +112,7 @@ export function generateAgentRegistry(srcDir: string, agents: AgentMetadata[]):
112
112
  .replace(/^.*\/src\/agent\//, '../agent/')
113
113
  .replace(/\.tsx?$/, '.js');
114
114
  }
115
- // Avoid duplicate imports
115
+ // Avoid duplicate imports
116
116
  if (!seenEvalPaths.has(evalRelativePath)) {
117
117
  seenEvalPaths.add(evalRelativePath);
118
118
  evalImports.push(`import '${evalRelativePath}';`);
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Web analytics beacon code generator
3
+ * Generates src/generated/webanalytics.ts with the analytics beacon script
4
+ */
5
+
6
+ import { join } from 'node:path';
7
+ import type { Logger, AnalyticsConfig } from '../../types';
8
+
9
+ interface GenerateWebAnalyticsOptions {
10
+ rootDir: string;
11
+ logger: Logger;
12
+ analytics?: boolean | AnalyticsConfig;
13
+ }
14
+
15
+ /**
16
+ * Generate the web analytics files (webanalytics.ts and analytics-config.ts)
17
+ */
18
+ export async function generateWebAnalyticsFile(
19
+ options: GenerateWebAnalyticsOptions
20
+ ): Promise<void> {
21
+ const { rootDir, logger, analytics } = options;
22
+
23
+ const srcDir = join(rootDir, 'src');
24
+ const generatedDir = join(srcDir, 'generated');
25
+ const analyticsPath = join(generatedDir, 'webanalytics.ts');
26
+ const configPath = join(generatedDir, 'analytics-config.ts');
27
+
28
+ logger.trace(`Generating web analytics files...`);
29
+
30
+ const analyticsEnabled = analytics !== false;
31
+ const analyticsConfig: AnalyticsConfig = typeof analytics === 'object' ? analytics : {};
32
+
33
+ // Generate the analytics config file with resolved values
34
+ const configCode = generateAnalyticsConfigCode(analyticsEnabled, analyticsConfig);
35
+ await Bun.write(configPath, configCode);
36
+
37
+ // Generate the webanalytics file
38
+ const code = analyticsEnabled ? getEnabledAnalyticsCode() : getDisabledAnalyticsCode();
39
+ await Bun.write(analyticsPath, code);
40
+
41
+ logger.trace(`Generated web analytics files at %s`, generatedDir);
42
+ }
43
+
44
+ function generateAnalyticsConfigCode(enabled: boolean, config: AnalyticsConfig): string {
45
+ return `// @generated
46
+ // Auto-generated by Agentuity
47
+ // DO NOT EDIT - This file is regenerated on every build
48
+
49
+ export interface AnalyticsConfig {
50
+ enabled: boolean;
51
+ requireConsent: boolean;
52
+ trackClicks: boolean;
53
+ trackScroll: boolean;
54
+ trackOutboundLinks: boolean;
55
+ trackForms: boolean;
56
+ trackWebVitals: boolean;
57
+ trackErrors: boolean;
58
+ trackSPANavigation: boolean;
59
+ sampleRate: number;
60
+ excludePatterns: string[];
61
+ globalProperties: Record<string, unknown>;
62
+ }
63
+
64
+ export const analyticsConfig: AnalyticsConfig = {
65
+ enabled: ${enabled && config.enabled !== false},
66
+ requireConsent: ${config.requireConsent ?? false},
67
+ trackClicks: ${config.trackClicks ?? true},
68
+ trackScroll: ${config.trackScroll ?? true},
69
+ trackOutboundLinks: ${config.trackOutboundLinks ?? true},
70
+ trackForms: ${config.trackForms ?? false},
71
+ trackWebVitals: ${config.trackWebVitals ?? true},
72
+ trackErrors: ${config.trackErrors ?? true},
73
+ trackSPANavigation: ${config.trackSPANavigation ?? true},
74
+ sampleRate: ${config.sampleRate ?? 1},
75
+ excludePatterns: ${JSON.stringify(config.excludePatterns ?? [])},
76
+ globalProperties: ${JSON.stringify(config.globalProperties ?? {})},
77
+ };
78
+ `;
79
+ }
80
+
81
+ function getDisabledAnalyticsCode(): string {
82
+ return `// @generated
83
+ // Auto-generated by Agentuity
84
+ // DO NOT EDIT - This file is regenerated on every build
85
+
86
+ import { createRouter } from '@agentuity/runtime';
87
+ import type { AnalyticsConfig } from './analytics-config';
88
+
89
+ // Analytics disabled
90
+ export function injectAnalytics(html: string, _config: AnalyticsConfig): string {
91
+ return html;
92
+ }
93
+
94
+ export function registerAnalyticsRoutes(_app: ReturnType<typeof createRouter>): void {}
95
+ `;
96
+ }
97
+
98
+ function getEnabledAnalyticsCode(): string {
99
+ return `// @generated
100
+ // Auto-generated by Agentuity
101
+ // DO NOT EDIT - This file is regenerated on every build
102
+
103
+ import type { Context } from 'hono';
104
+ import {
105
+ createRouter,
106
+ createWebSessionMiddleware,
107
+ getOrganizationId,
108
+ getProjectId,
109
+ isDevMode as runtimeIsDevMode,
110
+ } from '@agentuity/runtime';
111
+ import type { AnalyticsConfig } from './analytics-config';
112
+
113
+ // Inject analytics config and script into HTML
114
+ // Note: Only static config is injected (org, project, devmode, tracking options)
115
+ // Session and thread IDs are read from cookies by the beacon script
116
+ export function injectAnalytics(html: string, analyticsConfig: AnalyticsConfig): string {
117
+ if (!analyticsConfig.enabled) return html;
118
+
119
+ const orgId = getOrganizationId() || '';
120
+ const projectId = getProjectId() || '';
121
+ const isDevmode = runtimeIsDevMode();
122
+
123
+ // Only include static config - session/thread come from cookies
124
+ const pageConfig = {
125
+ ...analyticsConfig,
126
+ orgId,
127
+ projectId,
128
+ isDevmode,
129
+ };
130
+
131
+ const configScript = \`<script>window.__AGENTUITY_ANALYTICS__=\${JSON.stringify(pageConfig)};</script>\`;
132
+ // Session script sets cookies and window.__AGENTUITY_SESSION__ (dynamic, not cached)
133
+ const sessionScript = '<script src="/_agentuity/webanalytics/session.js" async></script>';
134
+ // Beacon script reads from __AGENTUITY_SESSION__ and sends events (static, cached)
135
+ const beaconScript = '<script src="/_agentuity/webanalytics/analytics.js" async></script>';
136
+ const injection = configScript + sessionScript + beaconScript;
137
+
138
+ // Inject before </head> or at start of <body>
139
+ if (html.includes('</head>')) {
140
+ return html.replace('</head>', injection + '</head>');
141
+ }
142
+ if (html.includes('<body')) {
143
+ return html.replace(/<body([^>]*)>/, \`<body$1>\${injection}\`);
144
+ }
145
+ return injection + html;
146
+ }
147
+
148
+ // The beacon script - minified for production
149
+ export const BEACON_SCRIPT = \`(function(){
150
+ var w=window,d=document,c=w.__AGENTUITY_ANALYTICS__;
151
+ if(!c||!c.enabled)return;
152
+ var q=[],t=null,sr=false,E='/_agentuity/webanalytics/collect',geo=null;
153
+ function id(){return crypto.randomUUID?crypto.randomUUID():Date.now()+'-'+Math.random().toString(36).substr(2,9)}
154
+ 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}
155
+ 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){}
156
+ function getSession(){return w.__AGENTUITY_SESSION__}
157
+ 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)}
158
+ 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(){})}
159
+ function flush(){if(sr){doFlush(getSession())}else{waitForSession(function(s){sr=true;doFlush(s)})}}
160
+ 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))}
161
+ 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)}
162
+ w.addEventListener('visibilitychange',function(){d.visibilityState==='hidden'&&flush()});
163
+ w.addEventListener('pagehide',flush);
164
+ 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)}
165
+ if(c.trackErrors){w.addEventListener('error',function(e){var ev=base('error');ev.event_name='js_error';ev.event_data=JSON.stringify({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=JSON.stringify({message:e.reason instanceof Error?e.reason.message:String(e.reason)});queue(ev)})}
166
+ 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)}
167
+ 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})}
168
+ 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)}}
169
+ d.readyState==='complete'?pv():w.addEventListener('load',pv);
170
+ w.agentuityAnalytics={track:function(n,p){var e=base('custom');e.event_name=n;if(p)e.event_data=JSON.stringify(p);queue(e)},flush:flush};
171
+ })()\`;
172
+
173
+ // Serve analytics routes
174
+ export function registerAnalyticsRoutes(app: ReturnType<typeof createRouter>): void {
175
+ // Dynamic thread config script - sets cookie and returns thread ID
176
+ // Web analytics only tracks thread ID, not session ID (to avoid polluting sessions table)
177
+ // This endpoint is NOT cached - it generates unique data per request
178
+ app.get('/_agentuity/webanalytics/session.js', createWebSessionMiddleware(), async (c: Context) => {
179
+ // Read from context (cookies aren't readable until the next request)
180
+ const threadId = c.get('_webThreadId') || '';
181
+
182
+ // Use JSON.stringify to safely escape threadId and prevent XSS/injection
183
+ const sessionData = JSON.stringify({ threadId });
184
+ const sessionScript = \`window.__AGENTUITY_SESSION__=\${sessionData};\`;
185
+
186
+ return new Response(sessionScript, {
187
+ headers: {
188
+ 'Content-Type': 'application/javascript; charset=utf-8',
189
+ 'Cache-Control': 'no-store, no-cache, must-revalidate',
190
+ },
191
+ });
192
+ });
193
+
194
+ // Static beacon script - can be cached
195
+ app.get('/_agentuity/webanalytics/analytics.js', async (c: Context) => {
196
+ return new Response(BEACON_SCRIPT, {
197
+ headers: {
198
+ 'Content-Type': 'application/javascript; charset=utf-8',
199
+ 'Cache-Control': 'public, max-age=3600',
200
+ },
201
+ });
202
+ });
203
+ }
204
+ `;
205
+ }
@@ -370,25 +370,7 @@ export const deploySubcommand = createSubcommand({
370
370
  }
371
371
  },
372
372
  },
373
- {
374
- label: 'Create Deployment',
375
- run: async () => {
376
- if (useExistingDeployment) {
377
- return stepSkipped('using pre-created deployment');
378
- }
379
- try {
380
- deployment = await projectDeploymentCreate(
381
- apiClient,
382
- project.projectId,
383
- project.deployment
384
- );
385
- return stepSuccess();
386
- } catch (ex) {
387
- const _ex = ex as { message?: string };
388
- return stepError(_ex.message ?? String(_ex), ex as Error);
389
- }
390
- },
391
- },
373
+
392
374
  {
393
375
  label: 'Build, Verify and Package',
394
376
  run: async () => {
@@ -59,10 +59,9 @@ export function isRunningFromExecutable(): boolean {
59
59
  const scriptPath = process.argv[1] || '';
60
60
 
61
61
  // Check if running from compiled binary (uses Bun's virtual filesystem)
62
- // When compiled with `bun build --compile`, the path is in the virtual /$bunfs/root/ directory
63
- const isCompiledBinary = process.argv[0] === 'bun' && scriptPath.startsWith('/$bunfs/root/');
64
-
65
- if (isCompiledBinary) {
62
+ // When compiled with `bun build --compile`, the script path is in the virtual /$bunfs/root/ directory
63
+ // Note: process.argv[0] is the executable path (e.g., /usr/local/bin/agentuity), not 'bun'
64
+ if (scriptPath.startsWith('/$bunfs/root/')) {
66
65
  return true;
67
66
  }
68
67
 
package/src/tui.ts CHANGED
@@ -67,14 +67,16 @@ export const ICONS = {
67
67
  } as const;
68
68
 
69
69
  /**
70
- * Check if we should treat stdout as a TTY (real TTY or FORCE_COLOR set by fork wrapper)
71
- * Returns false in CI environments since CI terminals don't support cursor control sequences
70
+ * Check if we should treat the terminal as TTY-like for interactive output
71
+ * (real TTY on stdout or stderr, or FORCE_COLOR set by fork wrapper).
72
+ * Returns false in CI environments since CI terminals often don't support
73
+ * cursor control sequences reliably.
72
74
  */
73
75
  export function isTTYLike(): boolean {
74
76
  if (process.env.CI) {
75
77
  return false;
76
78
  }
77
- return process.stdout.isTTY || process.env.FORCE_COLOR === '1';
79
+ return !!process.stdout.isTTY || !!process.stderr.isTTY || process.env.FORCE_COLOR === '1';
78
80
  }
79
81
 
80
82
  /**
@@ -1120,8 +1122,9 @@ export async function spinner<T>(
1120
1122
  const outputOptions = getOutputOptions();
1121
1123
  const noProgress = outputOptions ? shouldDisableProgress(outputOptions) : false;
1122
1124
 
1123
- // If no TTY or progress disabled, just execute the callback without animation
1124
- if (!process.stderr.isTTY || noProgress) {
1125
+ // If no interactive TTY-like environment or progress disabled, just execute
1126
+ // the callback without animation
1127
+ if (!isTTYLike() || noProgress) {
1125
1128
  try {
1126
1129
  const result =
1127
1130
  options.type === 'progress'
@@ -77,11 +77,39 @@ interface SourceContext {
77
77
  before: string | null;
78
78
  beforeLineNum: number;
79
79
  current: string;
80
+ currentOriginal: string;
80
81
  after: string | null;
81
82
  afterLineNum: number;
82
83
  total: number;
83
84
  }
84
85
 
86
+ const TAB_WIDTH = 4;
87
+
88
+ /**
89
+ * Convert tabs to spaces for consistent width calculation and display.
90
+ * Terminals render tabs with variable width (typically 8 spaces), but
91
+ * Bun.stringWidth() returns 0 for tabs, causing alignment issues.
92
+ */
93
+ function expandTabs(str: string, tabWidth = TAB_WIDTH): string {
94
+ return str.replace(/\t/g, ' '.repeat(tabWidth));
95
+ }
96
+
97
+ /**
98
+ * Convert a column position from the original source (with tabs) to the
99
+ * expanded position (with tabs converted to spaces).
100
+ */
101
+ function expandColumn(originalLine: string, col: number, tabWidth = TAB_WIDTH): number {
102
+ let expandedCol = 0;
103
+ for (let i = 0; i < col && i < originalLine.length; i++) {
104
+ if (originalLine[i] === '\t') {
105
+ expandedCol += tabWidth;
106
+ } else {
107
+ expandedCol += 1;
108
+ }
109
+ }
110
+ return expandedCol;
111
+ }
112
+
85
113
  /**
86
114
  * Read source lines with context (line before, current, line after)
87
115
  */
@@ -100,15 +128,19 @@ async function getSourceContext(
100
128
  return null;
101
129
  }
102
130
 
103
- const current = lines[lineNumber - 1];
104
- const before = lineNumber > 1 ? lines[lineNumber - 2] : null;
105
- const after = lineNumber < lines.length ? lines[lineNumber] : null;
131
+ const currentOriginal = lines[lineNumber - 1];
132
+ const current = expandTabs(currentOriginal);
133
+ const beforeRaw = lineNumber > 1 ? lines[lineNumber - 2] : null;
134
+ const afterRaw = lineNumber < lines.length ? lines[lineNumber] : null;
135
+ const before = beforeRaw !== null && beforeRaw.trim() !== '' ? expandTabs(beforeRaw) : null;
136
+ const after = afterRaw !== null && afterRaw.trim() !== '' ? expandTabs(afterRaw) : null;
106
137
 
107
138
  return {
108
- before: before !== null && before.trim() !== '' ? before : null,
139
+ before,
109
140
  beforeLineNum: lineNumber - 1,
110
141
  current,
111
- after: after !== null && after.trim() !== '' ? after : null,
142
+ currentOriginal,
143
+ after,
112
144
  afterLineNum: lineNumber + 1,
113
145
  total: lines.length,
114
146
  };
@@ -196,9 +228,13 @@ async function prepareError(
196
228
  codeLines.push({ content: errorLineContent, rawWidth: getDisplayWidth(errorLineContent) });
197
229
 
198
230
  // Error pointer line with carets
199
- const col = Math.max(0, error.col - 1);
231
+ // Convert the original column (which may include tabs) to the expanded column position
232
+ const originalCol = Math.max(0, error.col - 1);
233
+ const expandedCol = expandColumn(context.currentOriginal, originalCol);
234
+
235
+ // Find identifier length in the expanded (displayed) content
200
236
  let underlineLength = 1;
201
- const restOfLine = context.current.slice(col);
237
+ const restOfLine = context.current.slice(expandedCol);
202
238
  const identifierMatch = restOfLine.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*/);
203
239
  if (identifierMatch) {
204
240
  underlineLength = identifierMatch[0].length;
@@ -210,7 +246,7 @@ async function prepareError(
210
246
  }
211
247
 
212
248
  const maxCaretWidth = maxAvailableWidth - linePrefix;
213
- const caretStart = Math.min(col, maxCaretWidth - 1);
249
+ const caretStart = Math.min(expandedCol, maxCaretWidth - 1);
214
250
  const caretLen = Math.min(underlineLength, maxCaretWidth - caretStart);
215
251
  const carets = caretLen > 0 ? '^'.repeat(caretLen) : '^';
216
252
  const caretPadding = ' '.repeat(caretStart);