@agentuity/cli 0.0.54 → 0.0.56

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 (51) hide show
  1. package/dist/cmd/ai/schema/generate.d.ts +3 -0
  2. package/dist/cmd/ai/schema/generate.d.ts.map +1 -0
  3. package/dist/cmd/ai/schema/generate.js +50 -0
  4. package/dist/cmd/ai/schema/generate.js.map +1 -0
  5. package/dist/cmd/ai/schema/index.d.ts.map +1 -1
  6. package/dist/cmd/ai/schema/index.js +2 -1
  7. package/dist/cmd/ai/schema/index.js.map +1 -1
  8. package/dist/cmd/build/ast.d.ts.map +1 -1
  9. package/dist/cmd/build/ast.js +107 -86
  10. package/dist/cmd/build/ast.js.map +1 -1
  11. package/dist/cmd/build/ast.test.js +135 -370
  12. package/dist/cmd/build/ast.test.js.map +1 -1
  13. package/dist/cmd/build/bundler.d.ts.map +1 -1
  14. package/dist/cmd/build/bundler.js +9 -6
  15. package/dist/cmd/build/bundler.js.map +1 -1
  16. package/dist/cmd/build/index.d.ts.map +1 -1
  17. package/dist/cmd/build/index.js +2 -0
  18. package/dist/cmd/build/index.js.map +1 -1
  19. package/dist/cmd/build/plugin.d.ts.map +1 -1
  20. package/dist/cmd/build/plugin.js +10 -4
  21. package/dist/cmd/build/plugin.js.map +1 -1
  22. package/dist/cmd/cloud/session/get.d.ts.map +1 -1
  23. package/dist/cmd/cloud/session/get.js +77 -17
  24. package/dist/cmd/cloud/session/get.js.map +1 -1
  25. package/dist/cmd/dev/index.d.ts.map +1 -1
  26. package/dist/cmd/dev/index.js +32 -14
  27. package/dist/cmd/dev/index.js.map +1 -1
  28. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  29. package/dist/cmd/project/template-flow.js +1 -0
  30. package/dist/cmd/project/template-flow.js.map +1 -1
  31. package/dist/config.d.ts +27 -3
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js +31 -3
  34. package/dist/config.js.map +1 -1
  35. package/dist/types.d.ts +24 -2
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js +4 -75
  38. package/dist/types.js.map +1 -1
  39. package/package.json +3 -3
  40. package/src/cmd/ai/schema/generate.ts +64 -0
  41. package/src/cmd/ai/schema/index.ts +2 -1
  42. package/src/cmd/build/ast.test.ts +157 -549
  43. package/src/cmd/build/ast.ts +121 -94
  44. package/src/cmd/build/bundler.ts +9 -6
  45. package/src/cmd/build/index.ts +2 -0
  46. package/src/cmd/build/plugin.ts +11 -4
  47. package/src/cmd/cloud/session/get.ts +91 -19
  48. package/src/cmd/dev/index.ts +39 -14
  49. package/src/cmd/project/template-flow.ts +1 -0
  50. package/src/config.ts +44 -5
  51. package/src/types.ts +5 -84
@@ -126,7 +126,7 @@ function getAgentId(
126
126
  filename: string,
127
127
  version: string
128
128
  ): string {
129
- return `agent_${hashSHA1(projectId, deploymentId, filename, version)}`;
129
+ return `agentid_${hashSHA1(projectId, deploymentId, filename, version)}`;
130
130
  }
131
131
 
132
132
  function getEvalId(
@@ -152,7 +152,7 @@ function generateRouteId(
152
152
  }
153
153
 
154
154
  function generateStableAgentId(projectId: string, name: string): string {
155
- return `agentid_${hashSHA1(projectId, name)}`.substring(0, 64);
155
+ return `agent_${hashSHA1(projectId, name)}`.substring(0, 64);
156
156
  }
157
157
 
158
158
  function generateStableEvalId(projectId: string, agentId: string, name: string): string {
@@ -164,7 +164,7 @@ type AcornParseResultType = ReturnType<typeof acornLoose.parse>;
164
164
  function augmentAgentMetadataNode(
165
165
  projectId: string,
166
166
  id: string,
167
- name: string,
167
+ identifier: string,
168
168
  rel: string,
169
169
  version: string,
170
170
  ast: AcornParseResultType,
@@ -178,7 +178,8 @@ function augmentAgentMetadataNode(
178
178
  `missing required metadata.name in ${filename}${location}. This Agent should have a unique and human readable name for this project.`
179
179
  );
180
180
  }
181
- if (metadata.has('identifier') && name !== metadata.get('identifier')) {
181
+ const name = metadata.get('name')!;
182
+ if (metadata.has('identifier') && identifier !== metadata.get('identifier')) {
182
183
  const location = ast.loc?.start ? ` on line ${ast.loc.start}` : '';
183
184
  throw new Error(
184
185
  `metadata.identifier (${metadata.get('identifier')}) in ${filename}${location} is mismatched (${name}). This is an internal error.`
@@ -188,7 +189,7 @@ function augmentAgentMetadataNode(
188
189
  const description = descriptionNode ? (descriptionNode as ASTLiteral).value : '';
189
190
  const agentId = generateStableAgentId(projectId, name);
190
191
  metadata.set('version', version);
191
- metadata.set('identifier', name);
192
+ metadata.set('identifier', identifier);
192
193
  metadata.set('filename', rel);
193
194
  metadata.set('id', id);
194
195
  metadata.set('agentId', agentId);
@@ -789,111 +790,137 @@ export async function parseRoute(
789
790
  const routes: RouteDefinition = [];
790
791
  const routePrefix = filename.includes('src/agents') ? '/agent' : '/api';
791
792
 
792
- for (const body of ast.body) {
793
- if (body.type === 'ExpressionStatement') {
794
- const statement = body as ASTExpressionStatement;
795
- const callee = statement.expression.callee;
796
- if (callee.object.type === 'Identifier') {
797
- const identifier = callee.object as ASTNodeIdentifier;
798
- if (identifier.name === variableName) {
799
- let method = (callee.property as ASTNodeIdentifier).name;
800
- let type = 'api';
801
- const action = statement.expression.arguments[0];
802
- let suffix = '';
803
- let config: Record<string, unknown> | undefined;
804
- switch (method) {
805
- case 'get':
806
- case 'put':
807
- case 'post':
808
- case 'patch':
809
- case 'delete': {
810
- const theaction = action as ASTLiteral;
811
- if (theaction.type === 'Literal') {
812
- suffix = theaction.value;
793
+ try {
794
+ for (const body of ast.body) {
795
+ if (body.type === 'ExpressionStatement') {
796
+ const statement = body as ASTExpressionStatement;
797
+
798
+ // Validate that the expression is a call expression (e.g. function call)
799
+ if (statement.expression.type !== 'CallExpression') {
800
+ continue;
801
+ }
802
+
803
+ const callee = statement.expression.callee;
804
+
805
+ // Validate that the callee is a member expression (e.g. object.method())
806
+ // This handles cases like 'console.log()' or 'router.get()'
807
+ // direct function calls like 'myFunc()' have type 'Identifier' and will be skipped
808
+ if (callee.type !== 'MemberExpression') {
809
+ continue;
810
+ }
811
+
812
+ if (callee.object.type === 'Identifier' && statement.expression.arguments?.length > 0) {
813
+ const identifier = callee.object as ASTNodeIdentifier;
814
+ if (identifier.name === variableName) {
815
+ let method = (callee.property as ASTNodeIdentifier).name;
816
+ let type = 'api';
817
+ const action = statement.expression.arguments[0];
818
+ let suffix = '';
819
+ let config: Record<string, unknown> | undefined;
820
+ switch (method) {
821
+ case 'get':
822
+ case 'put':
823
+ case 'post':
824
+ case 'patch':
825
+ case 'delete': {
826
+ if (action && (action as ASTLiteral).type === 'Literal') {
827
+ suffix = (action as ASTLiteral).value;
828
+ } else {
829
+ throw new Error(
830
+ `unsupported HTTP method ${method} in ${filename} at line ${body.start}`
831
+ );
832
+ }
813
833
  break;
814
834
  }
815
- break;
816
- }
817
- case 'stream':
818
- case 'sse':
819
- case 'websocket': {
820
- type = method;
821
- method = 'post';
822
- const theaction = action as ASTLiteral;
823
- if (theaction.type === 'Literal') {
824
- suffix = theaction.value;
835
+ case 'stream':
836
+ case 'sse':
837
+ case 'websocket': {
838
+ type = method;
839
+ method = 'post';
840
+ const theaction = action as ASTLiteral;
841
+ if (theaction.type === 'Literal') {
842
+ suffix = theaction.value;
843
+ break;
844
+ }
825
845
  break;
826
846
  }
827
- break;
828
- }
829
- case 'sms': {
830
- type = method;
831
- method = 'post';
832
- const theaction = action as ASTObjectExpression;
833
- if (theaction.type === 'ObjectExpression') {
834
- config = {};
835
- theaction.properties.forEach((p) => {
836
- if (p.value.type === 'Literal') {
837
- const literal = p.value as ASTLiteral;
838
- config![p.key.name] = literal.value;
847
+ case 'sms': {
848
+ type = method;
849
+ method = 'post';
850
+ const theaction = action as ASTObjectExpression;
851
+ if (theaction.type === 'ObjectExpression') {
852
+ config = {};
853
+ theaction.properties.forEach((p) => {
854
+ if (p.value.type === 'Literal') {
855
+ const literal = p.value as ASTLiteral;
856
+ config![p.key.name] = literal.value;
857
+ }
858
+ });
859
+ const number = theaction.properties.find((p) => p.key.name === 'number');
860
+ if (number && number.value.type === 'Literal') {
861
+ const phoneNumber = number.value as ASTLiteral;
862
+ suffix = hash(phoneNumber.value);
863
+ break;
839
864
  }
840
- });
841
- const number = theaction.properties.find((p) => p.key.name === 'number');
842
- if (number && number.value.type === 'Literal') {
843
- const phoneNumber = number.value as ASTLiteral;
844
- suffix = hash(phoneNumber.value);
845
- break;
846
865
  }
866
+ break;
847
867
  }
848
- break;
849
- }
850
- case 'email': {
851
- type = method;
852
- method = 'post';
853
- const theaction = action as ASTLiteral;
854
- if (theaction.type === 'Literal') {
855
- const email = theaction.value;
856
- suffix = hash(email);
868
+ case 'email': {
869
+ type = method;
870
+ method = 'post';
871
+ const theaction = action as ASTLiteral;
872
+ if (theaction.type === 'Literal') {
873
+ const email = theaction.value;
874
+ suffix = hash(email);
875
+ break;
876
+ }
857
877
  break;
858
878
  }
859
- break;
860
- }
861
- case 'cron': {
862
- type = method;
863
- method = 'post';
864
- const theaction = action as ASTLiteral;
865
- if (theaction.type === 'Literal') {
866
- const number = theaction.value;
867
- suffix = hash(number);
879
+ case 'cron': {
880
+ type = method;
881
+ method = 'post';
882
+ const theaction = action as ASTLiteral;
883
+ if (theaction.type === 'Literal') {
884
+ const number = theaction.value;
885
+ suffix = hash(number);
886
+ break;
887
+ }
868
888
  break;
869
889
  }
870
- break;
890
+ default: {
891
+ throw new Error(
892
+ `unsupported router method ${method} in ${filename} at line ${body.start}`
893
+ );
894
+ }
871
895
  }
896
+ const thepath = `${routePrefix}/${routeName}/${suffix}`
897
+ .replaceAll(/\/{2,}/g, '/')
898
+ .replaceAll(/\/$/g, '');
899
+ const id = generateRouteId(
900
+ projectId,
901
+ deploymentId,
902
+ type,
903
+ method,
904
+ rel,
905
+ thepath,
906
+ version
907
+ );
908
+ routes.push({
909
+ id,
910
+ method: method as 'get' | 'post' | 'put' | 'delete' | 'patch',
911
+ type: type as 'api' | 'sms' | 'email' | 'cron',
912
+ filename: rel,
913
+ path: thepath,
914
+ version,
915
+ config,
916
+ });
872
917
  }
873
- const thepath = `${routePrefix}/${routeName}/${suffix}`
874
- .replaceAll(/\/{2,}/g, '/')
875
- .replaceAll(/\/$/g, '');
876
- const id = generateRouteId(
877
- projectId,
878
- deploymentId,
879
- type,
880
- method,
881
- rel,
882
- thepath,
883
- version
884
- );
885
- routes.push({
886
- id,
887
- method: method as 'get' | 'post' | 'put' | 'delete' | 'patch',
888
- type: type as 'api' | 'sms' | 'email' | 'cron',
889
- filename: rel,
890
- path: thepath,
891
- version,
892
- config,
893
- });
894
918
  }
895
919
  }
896
920
  }
921
+ } catch (error) {
922
+ const err = error instanceof Error ? error : new Error(String(error));
923
+ throw new Error(`Failed to parse route file ${filename}: ${err.message}`);
897
924
  }
898
925
  return routes;
899
926
  }
@@ -50,7 +50,7 @@ export async function bundle({
50
50
  }
51
51
  const files = await getFilesRecursively(dir);
52
52
  for (const filename of files) {
53
- if (/\.[jt]s?$/.test(filename)) {
53
+ if (/\.[jt]s?$/.test(filename) && !filename.includes('.generated.')) {
54
54
  appEntrypoints.push(filename);
55
55
  }
56
56
  }
@@ -358,13 +358,16 @@ export async function bundle({
358
358
  .map((s) => s.trim())
359
359
  .filter(Boolean);
360
360
  }
361
- const branch = $`git branch --show-current`.nothrow().quiet();
362
- if (branch) {
363
- const _branch = await branch.text();
364
- if (_branch) {
365
- buildmetadata.deployment.git.branch = _branch.trim();
361
+ let branch = process.env.GITHUB_HEAD_REF;
362
+ if (!branch) {
363
+ const branchText = $`git branch --show-current`.nothrow().quiet();
364
+ if (branchText) {
365
+ branch = await branchText.text();
366
366
  }
367
367
  }
368
+ if (branch) {
369
+ buildmetadata.deployment.git.branch = branch.trim();
370
+ }
368
371
  const commit = $`git rev-parse HEAD`.nothrow().quiet();
369
372
  if (commit) {
370
373
  const sha = await commit.text();
@@ -39,6 +39,8 @@ export const command = createCommand({
39
39
  rootDir: projectDir,
40
40
  dev: opts.dev || false,
41
41
  project,
42
+ orgId: project?.orgId,
43
+ projectId: project?.projectId,
42
44
  });
43
45
 
44
46
  // Run TypeScript type checking after registry generation (skip in dev mode)
@@ -493,7 +493,6 @@ const AgentuityBundler: BunPlugin = {
493
493
  projectId,
494
494
  deploymentId
495
495
  );
496
- routeDefinitions = [...routeDefinitions, ...definitions];
497
496
 
498
497
  let agentDetail: Record<string, string> = {};
499
498
 
@@ -504,6 +503,7 @@ const AgentuityBundler: BunPlugin = {
504
503
  }
505
504
  agentDetail = {
506
505
  name,
506
+ id: md.get('id')!,
507
507
  path: `.${agent}`,
508
508
  filename: md.get('filename')!,
509
509
  identifier: md.get('identifier')!,
@@ -514,8 +514,14 @@ const AgentuityBundler: BunPlugin = {
514
514
  agentDetail.parent = parentName;
515
515
  }
516
516
  agentInfo.push(agentDetail);
517
+ for (const def of definitions) {
518
+ def.agentIds = [agentDetail.agentId, agentDetail.id];
519
+ }
517
520
  }
518
521
 
522
+ // do this after handling the agent association (if any)
523
+ routeDefinitions = [...routeDefinitions, ...definitions];
524
+
519
525
  let buffer = `await (async() => {
520
526
  const { createAgentMiddleware, getRouter, registerAgent } = await import('@agentuity/runtime');
521
527
  const router = getRouter()!;
@@ -599,9 +605,7 @@ if (route !== '/workbench') {
599
605
  // 1. Evals are already imported when agents are registered (see line 421-422)
600
606
  // 2. The registry is for type definitions only, not runtime execution
601
607
  // 3. Importing it causes bundler resolution issues since it's generated during build
602
- if (agentInfo.length > 0) {
603
- generateAgentRegistry(srcDir, agentInfo);
604
- }
608
+ generateAgentRegistry(srcDir, agentInfo);
605
609
 
606
610
  // create the workbench routes
607
611
  inserts.push(`await (async() => {
@@ -659,6 +663,9 @@ if (route !== '/workbench') {
659
663
  if (!v.has('name')) {
660
664
  throw new Error('agent metadata is missing expected name property');
661
665
  }
666
+ if (!v.has('agentId')) {
667
+ throw new Error('agent metadata is missing expected agentId property');
668
+ }
662
669
 
663
670
  const parentName = v.get('parent');
664
671
  if (parentName) {
@@ -1,11 +1,29 @@
1
1
  import { z } from 'zod';
2
2
  import { createSubcommand } from '../../../types';
3
3
  import * as tui from '../../../tui';
4
- import { sessionGet } from '@agentuity/server';
4
+ import { sessionGet, type SpanNode } from '@agentuity/server';
5
5
  import { getCommand } from '../../../command-prefix';
6
6
  import { ErrorCode } from '../../../errors';
7
7
  import { getCatalystAPIClient } from '../../../config';
8
8
 
9
+ const SpanNodeSchema: z.ZodType<SpanNode> = z.lazy(() =>
10
+ z.object({
11
+ id: z.string().describe('Span ID'),
12
+ duration: z.number().describe('Duration in milliseconds'),
13
+ operation: z.string().describe('Operation name'),
14
+ attributes: z.record(z.string(), z.unknown()).describe('Span attributes'),
15
+ children: z.array(SpanNodeSchema).optional().describe('Child spans'),
16
+ })
17
+ );
18
+
19
+ const RouteInfoSchema = z
20
+ .object({
21
+ id: z.string().describe('Route ID'),
22
+ method: z.string().describe('HTTP method'),
23
+ path: z.string().describe('Route path'),
24
+ })
25
+ .nullable();
26
+
9
27
  const SessionGetResponseSchema = z.object({
10
28
  id: z.string().describe('Session ID'),
11
29
  created_at: z.string().describe('Creation timestamp'),
@@ -26,8 +44,15 @@ const SessionGetResponseSchema = z.object({
26
44
  url: z.string().describe('Request URL'),
27
45
  route_id: z.string().describe('Route ID'),
28
46
  thread_id: z.string().describe('Thread ID'),
29
- agentNames: z.array(z.string()).describe('Agent names'),
30
- evalRuns: z
47
+ agents: z
48
+ .array(
49
+ z.object({
50
+ name: z.string(),
51
+ identifier: z.string(),
52
+ })
53
+ )
54
+ .describe('Agents'),
55
+ eval_runs: z
31
56
  .array(
32
57
  z.object({
33
58
  id: z.string(),
@@ -40,8 +65,41 @@ const SessionGetResponseSchema = z.object({
40
65
  })
41
66
  )
42
67
  .describe('Eval runs'),
68
+ timeline: SpanNodeSchema.nullable().optional().describe('Session timeline'),
69
+ route: RouteInfoSchema.optional().describe('Route information'),
43
70
  });
44
71
 
72
+ function formatDuration(ms: number): string {
73
+ if (ms < 1) {
74
+ return `${(ms * 1000).toFixed(0)}µs`;
75
+ }
76
+ if (ms < 1000) {
77
+ return `${ms.toFixed(1)}ms`;
78
+ }
79
+ return `${(ms / 1000).toFixed(2)}s`;
80
+ }
81
+
82
+ function printTimeline(node: SpanNode, prefix: string, isLast = true): void {
83
+ const connector = isLast ? '└── ' : '├── ';
84
+ const duration = tui.muted(`(${formatDuration(node.duration)})`);
85
+ let extra = '';
86
+ if (node.operation.startsWith('agentuity.')) {
87
+ if ('name' in node.attributes && 'key' in node.attributes) {
88
+ extra = tui.colorSuccess(`${node.attributes.name} ${node.attributes.key}`) + ' ';
89
+ }
90
+ }
91
+ if (node.operation.startsWith('HTTP ') && 'http.url' in node.attributes) {
92
+ extra = `${tui.colorSuccess(node.attributes['http.url'] as string)} `;
93
+ }
94
+ console.log(`${prefix}${connector}${node.operation} ${extra}${duration}`);
95
+
96
+ const childPrefix = prefix + (isLast ? ' ' : '│ ');
97
+ const children = node.children ?? [];
98
+ children.forEach((child, index) => {
99
+ printTimeline(child, childPrefix, index === children.length - 1);
100
+ });
101
+ }
102
+
45
103
  export const getSubcommand = createSubcommand({
46
104
  name: 'get',
47
105
  description: 'Get details about a specific session',
@@ -83,8 +141,8 @@ export const getSubcommand = createSubcommand({
83
141
  url: session.url,
84
142
  route_id: session.route_id,
85
143
  thread_id: session.thread_id,
86
- agentNames: enriched.agentNames,
87
- evalRuns: enriched.evalRuns.map((run) => ({
144
+ agents: enriched.agents,
145
+ eval_runs: enriched.evalRuns.map((run) => ({
88
146
  id: run.id,
89
147
  eval_id: run.eval_id,
90
148
  created_at: run.created_at,
@@ -93,6 +151,8 @@ export const getSubcommand = createSubcommand({
93
151
  error: run.error,
94
152
  result: run.result,
95
153
  })),
154
+ timeline: enriched.timeline,
155
+ route: enriched.route,
96
156
  };
97
157
 
98
158
  if (options.json) {
@@ -100,17 +160,14 @@ export const getSubcommand = createSubcommand({
100
160
  return result;
101
161
  }
102
162
 
103
- tui.banner(`Session ${session.id}`, `Status: ${session.success ? 'Success' : 'Failed'}`);
104
-
105
163
  console.log(tui.bold('ID: ') + session.id);
106
164
  console.log(tui.bold('Project: ') + session.project_id);
107
165
  console.log(tui.bold('Deployment: ') + (session.deployment_id || '-'));
108
- console.log(tui.bold('Created: ') + new Date(session.created_at).toLocaleString());
109
166
  console.log(tui.bold('Start: ') + new Date(session.start_time).toLocaleString());
110
167
  if (session.end_time) {
111
168
  console.log(tui.bold('End: ') + new Date(session.end_time).toLocaleString());
112
169
  }
113
- if (session.duration) {
170
+ if (session.duration && session.end_time) {
114
171
  console.log(
115
172
  tui.bold('Duration: ') + `${(session.duration / 1_000_000).toFixed(0)}ms`
116
173
  );
@@ -118,23 +175,32 @@ export const getSubcommand = createSubcommand({
118
175
  console.log(tui.bold('Method: ') + session.method);
119
176
  console.log(tui.bold('URL: ') + tui.link(session.url, session.url));
120
177
  console.log(tui.bold('Trigger: ') + session.trigger);
121
- console.log(tui.bold('Environment: ') + session.env);
178
+ if (session.env !== 'production') {
179
+ console.log(tui.bold('Environment: ') + session.env);
180
+ }
122
181
  console.log(tui.bold('Dev Mode: ') + (session.devmode ? 'Yes' : 'No'));
123
- console.log(tui.bold('Success: ') + (session.success ? '✓' : '✗'));
182
+ console.log(
183
+ tui.bold('Success: ') +
184
+ (session.success ? tui.colorSuccess('✓') : tui.colorError('✗'))
185
+ );
124
186
  console.log(tui.bold('Pending: ') + (session.pending ? 'Yes' : 'No'));
125
187
  if (session.error) {
126
188
  console.log(tui.bold('Error: ') + tui.error(session.error));
127
189
  }
128
- if (enriched.agentNames.length > 0) {
129
- const agentDisplay = enriched.agentNames
130
- .map((name, idx) => {
131
- const agentId = session.agent_ids[idx];
132
- return `${name} ${tui.muted(`(${agentId})`)}`;
133
- })
190
+ if (enriched.agents.length > 0) {
191
+ const agentDisplay = enriched.agents
192
+ .map((agent) => `${agent.name} ${tui.muted(`(${agent.identifier})`)}`)
134
193
  .join(', ');
135
194
  console.log(tui.bold('Agents: ') + agentDisplay);
136
195
  }
137
- console.log(tui.bold('Route ID: ') + session.route_id);
196
+ if (enriched.route) {
197
+ console.log(
198
+ tui.bold('Route: ') +
199
+ `${enriched.route.method.toUpperCase()} ${enriched.route.path} ${tui.muted(`(${enriched.route.id})`)}`
200
+ );
201
+ } else {
202
+ console.log(tui.bold('Route ID: ') + session.route_id);
203
+ }
138
204
  console.log(tui.bold('Thread ID: ') + session.thread_id);
139
205
 
140
206
  if (enriched.evalRuns.length > 0) {
@@ -143,7 +209,7 @@ export const getSubcommand = createSubcommand({
143
209
  const evalTableData = enriched.evalRuns.map((run) => ({
144
210
  ID: run.id,
145
211
  'Eval ID': run.eval_id,
146
- Success: run.success ? '✓' : '✗',
212
+ Success: run.success ? tui.colorSuccess('✓') : tui.colorError('✗'),
147
213
  Pending: run.pending ? '⏳' : '✓',
148
214
  Error: run.error || 'No',
149
215
  Created: new Date(run.created_at).toLocaleString(),
@@ -159,6 +225,12 @@ export const getSubcommand = createSubcommand({
159
225
  ]);
160
226
  }
161
227
 
228
+ if (result.timeline) {
229
+ console.log('');
230
+ console.log(tui.bold('Timeline:'));
231
+ printTimeline(result.timeline, '');
232
+ }
233
+
162
234
  return result;
163
235
  } catch (ex) {
164
236
  tui.fatal(
@@ -236,6 +236,7 @@ export const command = createCommand({
236
236
  let showInitialReadyMessage = true;
237
237
  let serverStartTime = 0;
238
238
  let gravityClient: Bun.Subprocess | undefined;
239
+ let initialStartupComplete = false;
239
240
 
240
241
  if (gravityBin && devmode && project) {
241
242
  const sdkKey = await loadProjectSDKKey(rootDir);
@@ -259,7 +260,7 @@ export const command = createCommand({
259
260
  '--url',
260
261
  config?.overrides?.gravity_url ?? 'grpc://devmode.agentuity.com',
261
262
  '--log-level',
262
- 'error',
263
+ process.env.AGENTUITY_GRAVITY_LOG_LEVEL ?? 'error',
263
264
  ],
264
265
  {
265
266
  cwd: rootDir,
@@ -412,17 +413,10 @@ export const command = createCommand({
412
413
  logger.trace('Initial server start');
413
414
  }
414
415
  logger.trace('Starting typecheck and build...');
415
- await Promise.all([
416
- tui.runCommand({
417
- command: 'tsc',
418
- cmd: ['bunx', 'tsc', '--noEmit'],
419
- cwd: rootDir,
420
- clearOnSuccess: true,
421
- truncate: false,
422
- maxLinesOutput: 2,
423
- maxLinesOnFailure: 15,
424
- }),
425
- tui.spinner('Building project', async () => {
416
+ await tui.spinner({
417
+ message: 'Building project',
418
+ clearOnSuccess: true,
419
+ callback: async () => {
426
420
  try {
427
421
  logger.trace('Bundle starting...');
428
422
  building = true;
@@ -436,13 +430,26 @@ export const command = createCommand({
436
430
  building = false;
437
431
  buildCompletedAt = Date.now();
438
432
  logger.trace('Bundle completed successfully');
433
+ logger.trace('tsc starting...');
434
+ await tui.runCommand({
435
+ command: 'tsc',
436
+ cmd: ['bunx', 'tsc', '--noEmit'],
437
+ cwd: rootDir,
438
+ clearOnSuccess: true,
439
+ truncate: false,
440
+ maxLinesOutput: 2,
441
+ maxLinesOnFailure: 15,
442
+ });
443
+ logger.trace('tsc completed successfully');
439
444
  } catch (error) {
440
445
  building = false;
441
446
  logger.trace('Bundle failed: %s', error);
442
447
  failure('Build failed');
448
+ return;
443
449
  }
444
- }),
445
- ]);
450
+ },
451
+ });
452
+
446
453
  logger.trace('Typecheck and build completed');
447
454
 
448
455
  if (failed) {
@@ -541,8 +548,14 @@ export const command = createCommand({
541
548
 
542
549
  if (showInitialReadyMessage) {
543
550
  showInitialReadyMessage = false;
551
+ // Clear any lingering spinner/command output - clear everything below cursor
552
+ process.stderr.write('\x1B[J'); // Clear from cursor to end of screen
553
+ process.stdout.write('\x1B[J'); // Clear from cursor to end of screen
544
554
  logger.info('DevMode ready 🚀');
545
555
  logger.trace('Initial ready message logged');
556
+ // Mark initial startup complete immediately to prevent watcher restarts
557
+ initialStartupComplete = true;
558
+ logger.trace('Initial startup complete, file watcher restarts now enabled');
546
559
  }
547
560
 
548
561
  logger.trace('Attaching exit handler to dev server process...');
@@ -634,6 +647,7 @@ export const command = createCommand({
634
647
  logger.trace('Starting initial build and server');
635
648
  await restart();
636
649
  logger.trace('Initial restart completed, setting up watchers');
650
+ logger.trace('initialStartupComplete is now: %s', initialStartupComplete);
637
651
 
638
652
  // Setup keyboard shortcuts (only if we have a TTY)
639
653
  if (canDoInput) {
@@ -743,6 +757,17 @@ export const command = createCommand({
743
757
  const watcher = watch(watchDir, { recursive: true }, (eventType, changedFile) => {
744
758
  const absPath = changedFile ? resolve(watchDir, changedFile) : watchDir;
745
759
 
760
+ // Ignore file changes during initial startup to prevent spurious restarts
761
+ if (!initialStartupComplete) {
762
+ logger.trace(
763
+ 'File change ignored (initial startup): %s (event: %s, file: %s)',
764
+ watchDir,
765
+ eventType,
766
+ changedFile
767
+ );
768
+ return;
769
+ }
770
+
746
771
  // Ignore file changes during active build to prevent loops
747
772
  if (building) {
748
773
  logger.trace(
@@ -325,6 +325,7 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
325
325
  deployment: {
326
326
  resources: resourceConfig,
327
327
  },
328
+ region: region ?? 'usc',
328
329
  });
329
330
  },
330
331
  });