@agentuity/cli 0.1.16 → 0.1.17

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 (185) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +3 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cmd/build/ast.d.ts.map +1 -1
  5. package/dist/cmd/build/ast.js +68 -2
  6. package/dist/cmd/build/ast.js.map +1 -1
  7. package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
  8. package/dist/cmd/build/vite/registry-generator.js +112 -23
  9. package/dist/cmd/build/vite/registry-generator.js.map +1 -1
  10. package/dist/cmd/build/vite/route-discovery.d.ts +4 -0
  11. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  12. package/dist/cmd/build/vite/route-discovery.js +4 -0
  13. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  14. package/dist/cmd/cloud/env/delete.d.ts.map +1 -1
  15. package/dist/cmd/cloud/env/delete.js +8 -2
  16. package/dist/cmd/cloud/env/delete.js.map +1 -1
  17. package/dist/cmd/cloud/env/get.d.ts.map +1 -1
  18. package/dist/cmd/cloud/env/get.js +4 -1
  19. package/dist/cmd/cloud/env/get.js.map +1 -1
  20. package/dist/cmd/cloud/env/import.d.ts.map +1 -1
  21. package/dist/cmd/cloud/env/import.js +5 -8
  22. package/dist/cmd/cloud/env/import.js.map +1 -1
  23. package/dist/cmd/cloud/env/list.d.ts.map +1 -1
  24. package/dist/cmd/cloud/env/list.js +11 -6
  25. package/dist/cmd/cloud/env/list.js.map +1 -1
  26. package/dist/cmd/cloud/env/pull.d.ts.map +1 -1
  27. package/dist/cmd/cloud/env/pull.js.map +1 -1
  28. package/dist/cmd/cloud/env/push.d.ts.map +1 -1
  29. package/dist/cmd/cloud/env/push.js +1 -7
  30. package/dist/cmd/cloud/env/push.js.map +1 -1
  31. package/dist/cmd/cloud/env/set.d.ts.map +1 -1
  32. package/dist/cmd/cloud/env/set.js +4 -1
  33. package/dist/cmd/cloud/env/set.js.map +1 -1
  34. package/dist/cmd/cloud/index.d.ts.map +1 -1
  35. package/dist/cmd/cloud/index.js +2 -0
  36. package/dist/cmd/cloud/index.js.map +1 -1
  37. package/dist/cmd/cloud/queue/ack.d.ts +3 -0
  38. package/dist/cmd/cloud/queue/ack.d.ts.map +1 -0
  39. package/dist/cmd/cloud/queue/ack.js +45 -0
  40. package/dist/cmd/cloud/queue/ack.js.map +1 -0
  41. package/dist/cmd/cloud/queue/create.d.ts +3 -0
  42. package/dist/cmd/cloud/queue/create.d.ts.map +1 -0
  43. package/dist/cmd/cloud/queue/create.js +80 -0
  44. package/dist/cmd/cloud/queue/create.js.map +1 -0
  45. package/dist/cmd/cloud/queue/delete.d.ts +3 -0
  46. package/dist/cmd/cloud/queue/delete.d.ts.map +1 -0
  47. package/dist/cmd/cloud/queue/delete.js +50 -0
  48. package/dist/cmd/cloud/queue/delete.js.map +1 -0
  49. package/dist/cmd/cloud/queue/destinations.d.ts +3 -0
  50. package/dist/cmd/cloud/queue/destinations.d.ts.map +1 -0
  51. package/dist/cmd/cloud/queue/destinations.js +232 -0
  52. package/dist/cmd/cloud/queue/destinations.js.map +1 -0
  53. package/dist/cmd/cloud/queue/dlq.d.ts +3 -0
  54. package/dist/cmd/cloud/queue/dlq.d.ts.map +1 -0
  55. package/dist/cmd/cloud/queue/dlq.js +168 -0
  56. package/dist/cmd/cloud/queue/dlq.js.map +1 -0
  57. package/dist/cmd/cloud/queue/get.d.ts +3 -0
  58. package/dist/cmd/cloud/queue/get.d.ts.map +1 -0
  59. package/dist/cmd/cloud/queue/get.js +130 -0
  60. package/dist/cmd/cloud/queue/get.js.map +1 -0
  61. package/dist/cmd/cloud/queue/index.d.ts +3 -0
  62. package/dist/cmd/cloud/queue/index.d.ts.map +1 -0
  63. package/dist/cmd/cloud/queue/index.js +65 -0
  64. package/dist/cmd/cloud/queue/index.js.map +1 -0
  65. package/dist/cmd/cloud/queue/list.d.ts +3 -0
  66. package/dist/cmd/cloud/queue/list.d.ts.map +1 -0
  67. package/dist/cmd/cloud/queue/list.js +71 -0
  68. package/dist/cmd/cloud/queue/list.js.map +1 -0
  69. package/dist/cmd/cloud/queue/messages.d.ts +3 -0
  70. package/dist/cmd/cloud/queue/messages.d.ts.map +1 -0
  71. package/dist/cmd/cloud/queue/messages.js +137 -0
  72. package/dist/cmd/cloud/queue/messages.js.map +1 -0
  73. package/dist/cmd/cloud/queue/nack.d.ts +3 -0
  74. package/dist/cmd/cloud/queue/nack.d.ts.map +1 -0
  75. package/dist/cmd/cloud/queue/nack.js +45 -0
  76. package/dist/cmd/cloud/queue/nack.js.map +1 -0
  77. package/dist/cmd/cloud/queue/pause.d.ts +3 -0
  78. package/dist/cmd/cloud/queue/pause.d.ts.map +1 -0
  79. package/dist/cmd/cloud/queue/pause.js +36 -0
  80. package/dist/cmd/cloud/queue/pause.js.map +1 -0
  81. package/dist/cmd/cloud/queue/publish.d.ts +3 -0
  82. package/dist/cmd/cloud/queue/publish.d.ts.map +1 -0
  83. package/dist/cmd/cloud/queue/publish.js +76 -0
  84. package/dist/cmd/cloud/queue/publish.js.map +1 -0
  85. package/dist/cmd/cloud/queue/receive.d.ts +3 -0
  86. package/dist/cmd/cloud/queue/receive.d.ts.map +1 -0
  87. package/dist/cmd/cloud/queue/receive.js +67 -0
  88. package/dist/cmd/cloud/queue/receive.js.map +1 -0
  89. package/dist/cmd/cloud/queue/resume.d.ts +3 -0
  90. package/dist/cmd/cloud/queue/resume.d.ts.map +1 -0
  91. package/dist/cmd/cloud/queue/resume.js +35 -0
  92. package/dist/cmd/cloud/queue/resume.js.map +1 -0
  93. package/dist/cmd/cloud/queue/sources.d.ts +3 -0
  94. package/dist/cmd/cloud/queue/sources.d.ts.map +1 -0
  95. package/dist/cmd/cloud/queue/sources.js +290 -0
  96. package/dist/cmd/cloud/queue/sources.js.map +1 -0
  97. package/dist/cmd/cloud/queue/stats.d.ts +3 -0
  98. package/dist/cmd/cloud/queue/stats.d.ts.map +1 -0
  99. package/dist/cmd/cloud/queue/stats.js +239 -0
  100. package/dist/cmd/cloud/queue/stats.js.map +1 -0
  101. package/dist/cmd/cloud/queue/util.d.ts +26 -0
  102. package/dist/cmd/cloud/queue/util.d.ts.map +1 -0
  103. package/dist/cmd/cloud/queue/util.js +19 -0
  104. package/dist/cmd/cloud/queue/util.js.map +1 -0
  105. package/dist/cmd/cloud/sandbox/snapshot/build.d.ts.map +1 -1
  106. package/dist/cmd/cloud/sandbox/snapshot/build.js +122 -28
  107. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  108. package/dist/cmd/cloud/sandbox/snapshot/create.d.ts.map +1 -1
  109. package/dist/cmd/cloud/sandbox/snapshot/create.js +19 -7
  110. package/dist/cmd/cloud/sandbox/snapshot/create.js.map +1 -1
  111. package/dist/cmd/cloud/sandbox/snapshot/get.d.ts.map +1 -1
  112. package/dist/cmd/cloud/sandbox/snapshot/get.js +16 -0
  113. package/dist/cmd/cloud/sandbox/snapshot/get.js.map +1 -1
  114. package/dist/cmd/cloud/sandbox/snapshot/list.d.ts.map +1 -1
  115. package/dist/cmd/cloud/sandbox/snapshot/list.js +4 -0
  116. package/dist/cmd/cloud/sandbox/snapshot/list.js.map +1 -1
  117. package/dist/cmd/cloud/vector/stats.d.ts.map +1 -1
  118. package/dist/cmd/cloud/vector/stats.js +8 -0
  119. package/dist/cmd/cloud/vector/stats.js.map +1 -1
  120. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  121. package/dist/cmd/project/template-flow.js.map +1 -1
  122. package/dist/env-util.d.ts +6 -1
  123. package/dist/env-util.d.ts.map +1 -1
  124. package/dist/env-util.js +16 -2
  125. package/dist/env-util.js.map +1 -1
  126. package/dist/errors.d.ts +4 -2
  127. package/dist/errors.d.ts.map +1 -1
  128. package/dist/errors.js +6 -0
  129. package/dist/errors.js.map +1 -1
  130. package/dist/schema-parser.d.ts.map +1 -1
  131. package/dist/schema-parser.js +2 -2
  132. package/dist/schema-parser.js.map +1 -1
  133. package/dist/tui/box.d.ts +4 -0
  134. package/dist/tui/box.d.ts.map +1 -1
  135. package/dist/tui/box.js +39 -0
  136. package/dist/tui/box.js.map +1 -1
  137. package/dist/tui.d.ts +11 -1
  138. package/dist/tui.d.ts.map +1 -1
  139. package/dist/tui.js +16 -8
  140. package/dist/tui.js.map +1 -1
  141. package/dist/types.d.ts.map +1 -1
  142. package/dist/types.js.map +1 -1
  143. package/package.json +6 -6
  144. package/src/cli.ts +5 -1
  145. package/src/cmd/build/ast.ts +88 -2
  146. package/src/cmd/build/vite/registry-generator.ts +120 -24
  147. package/src/cmd/build/vite/route-discovery.ts +16 -0
  148. package/src/cmd/cloud/env/delete.ts +18 -5
  149. package/src/cmd/cloud/env/get.ts +10 -3
  150. package/src/cmd/cloud/env/import.ts +10 -11
  151. package/src/cmd/cloud/env/list.ts +19 -9
  152. package/src/cmd/cloud/env/org-util.ts +1 -1
  153. package/src/cmd/cloud/env/pull.ts +9 -4
  154. package/src/cmd/cloud/env/push.ts +5 -9
  155. package/src/cmd/cloud/env/set.ts +10 -3
  156. package/src/cmd/cloud/index.ts +2 -0
  157. package/src/cmd/cloud/queue/ack.ts +50 -0
  158. package/src/cmd/cloud/queue/create.ts +91 -0
  159. package/src/cmd/cloud/queue/delete.ts +57 -0
  160. package/src/cmd/cloud/queue/destinations.ts +287 -0
  161. package/src/cmd/cloud/queue/dlq.ts +203 -0
  162. package/src/cmd/cloud/queue/get.ts +158 -0
  163. package/src/cmd/cloud/queue/index.ts +66 -0
  164. package/src/cmd/cloud/queue/list.ts +81 -0
  165. package/src/cmd/cloud/queue/messages.ts +160 -0
  166. package/src/cmd/cloud/queue/nack.ts +50 -0
  167. package/src/cmd/cloud/queue/pause.ts +41 -0
  168. package/src/cmd/cloud/queue/publish.ts +88 -0
  169. package/src/cmd/cloud/queue/receive.ts +76 -0
  170. package/src/cmd/cloud/queue/resume.ts +40 -0
  171. package/src/cmd/cloud/queue/sources.ts +352 -0
  172. package/src/cmd/cloud/queue/stats.ts +297 -0
  173. package/src/cmd/cloud/queue/util.ts +34 -0
  174. package/src/cmd/cloud/sandbox/snapshot/build.ts +146 -29
  175. package/src/cmd/cloud/sandbox/snapshot/create.ts +24 -7
  176. package/src/cmd/cloud/sandbox/snapshot/get.ts +16 -0
  177. package/src/cmd/cloud/sandbox/snapshot/list.ts +4 -0
  178. package/src/cmd/cloud/vector/stats.ts +9 -0
  179. package/src/cmd/project/template-flow.ts +1 -3
  180. package/src/env-util.ts +17 -2
  181. package/src/errors.ts +8 -0
  182. package/src/schema-parser.ts +6 -3
  183. package/src/tui/box.ts +52 -0
  184. package/src/tui.ts +28 -8
  185. package/src/types.ts +0 -1
@@ -0,0 +1,297 @@
1
+ import { z } from 'zod';
2
+ import { createCommand } from '../../../types';
3
+ import * as tui from '../../../tui';
4
+ import { createQueueAPIClient, getQueueApiOptions } from './util';
5
+ import { getCommand } from '../../../command-prefix';
6
+ import {
7
+ getOrgAnalytics,
8
+ getQueueAnalytics,
9
+ streamOrgAnalytics,
10
+ streamQueueAnalytics,
11
+ type OrgAnalytics,
12
+ type QueueAnalytics,
13
+ type SSEStatsEvent,
14
+ } from '@agentuity/server';
15
+
16
+ const StatsResponseSchema = z.union([
17
+ z.object({ type: z.literal('org'), analytics: z.unknown() }),
18
+ z.object({ type: z.literal('queue'), analytics: z.unknown() }),
19
+ z.object({ type: z.literal('stream'), events: z.array(z.unknown()) }),
20
+ ]);
21
+
22
+ function formatNumber(n: number): string {
23
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
24
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
25
+ return String(n);
26
+ }
27
+
28
+ function formatPercent(n: number): string {
29
+ return `${n.toFixed(2)}%`;
30
+ }
31
+
32
+ function formatLatency(ms: number): string {
33
+ if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
34
+ return `${Math.round(ms)}ms`;
35
+ }
36
+
37
+ function formatDuration(start: string, end: string): string {
38
+ const startDate = new Date(start);
39
+ const endDate = new Date(end);
40
+ const hours = Math.round((endDate.getTime() - startDate.getTime()) / 3600000);
41
+ if (hours >= 24) return `${Math.round(hours / 24)}d`;
42
+ return `${hours}h`;
43
+ }
44
+
45
+ function displayOrgAnalytics(analytics: OrgAnalytics): void {
46
+ const { summary, queues, period } = analytics;
47
+
48
+ tui.info(`Organization Analytics (${formatDuration(period.start, period.end)})`);
49
+ tui.newline();
50
+ console.log(tui.colorPrimary('Summary:'));
51
+ console.log(` ${tui.muted('Total Queues:')} ${summary.total_queues}`);
52
+ console.log(
53
+ ` ${tui.muted('Published:')} ${formatNumber(summary.total_messages_published)}`
54
+ );
55
+ console.log(
56
+ ` ${tui.muted('Delivered:')} ${formatNumber(summary.total_messages_delivered)}`
57
+ );
58
+ console.log(
59
+ ` ${tui.muted('Acknowledged:')} ${formatNumber(summary.total_messages_acknowledged)}`
60
+ );
61
+ console.log(` ${tui.muted('DLQ Messages:')} ${formatNumber(summary.total_dlq_messages)}`);
62
+ console.log(` ${tui.muted('Avg Latency:')} ${formatLatency(summary.avg_latency_ms)}`);
63
+ console.log(` ${tui.muted('P95 Latency:')} ${formatLatency(summary.p95_latency_ms)}`);
64
+ console.log(` ${tui.muted('Error Rate:')} ${formatPercent(summary.error_rate_percent)}`);
65
+
66
+ if (queues.length > 0) {
67
+ tui.newline();
68
+ console.log(tui.colorPrimary('Queues:'));
69
+ const tableData = queues.map((q) => ({
70
+ Name: q.name,
71
+ Type: q.queue_type,
72
+ Published: formatNumber(q.messages_published),
73
+ Delivered: formatNumber(q.messages_delivered),
74
+ Backlog: formatNumber(q.backlog),
75
+ DLQ: formatNumber(q.dlq_count),
76
+ 'Avg Latency': formatLatency(q.avg_latency_ms),
77
+ 'Error %': formatPercent(q.error_rate_percent),
78
+ }));
79
+ tui.table(tableData);
80
+ }
81
+ }
82
+
83
+ function displayQueueAnalytics(analytics: QueueAnalytics): void {
84
+ const { queue_name, queue_type, period, current, period_stats, latency, consumer_latency } =
85
+ analytics;
86
+
87
+ tui.info(`Queue: ${queue_name} (${queue_type})`);
88
+ console.log(tui.colorWarning(`Period: ${formatDuration(period.start, period.end)}`));
89
+ tui.newline();
90
+
91
+ console.log(tui.colorPrimary('Current State:'));
92
+ console.log(` ${tui.muted('Backlog:')} ${formatNumber(current.backlog)}`);
93
+ console.log(` ${tui.muted('In-Flight:')} ${formatNumber(current.messages_in_flight)}`);
94
+ console.log(` ${tui.muted('DLQ:')} ${formatNumber(current.dlq_count)}`);
95
+ console.log(` ${tui.muted('Consumers:')} ${current.active_consumers}`);
96
+ if (current.oldest_message_age_seconds != null) {
97
+ console.log(` ${tui.muted('Oldest Msg Age:')} ${current.oldest_message_age_seconds}s`);
98
+ }
99
+
100
+ tui.newline();
101
+ console.log(tui.colorPrimary('Period Stats:'));
102
+ console.log(
103
+ ` ${tui.muted('Published:')} ${formatNumber(period_stats.messages_published)}`
104
+ );
105
+ console.log(
106
+ ` ${tui.muted('Delivered:')} ${formatNumber(period_stats.messages_delivered)}`
107
+ );
108
+ console.log(
109
+ ` ${tui.muted('Acknowledged:')} ${formatNumber(period_stats.messages_acknowledged)}`
110
+ );
111
+ console.log(
112
+ ` ${tui.muted('Failed:')} ${formatNumber(period_stats.messages_failed)}`
113
+ );
114
+ console.log(
115
+ ` ${tui.muted('Replayed:')} ${formatNumber(period_stats.messages_replayed)}`
116
+ );
117
+ console.log(
118
+ ` ${tui.muted('Bytes Published:')} ${tui.formatBytes(period_stats.bytes_published)}`
119
+ );
120
+ console.log(
121
+ ` ${tui.muted('Delivery Attempts:')} ${formatNumber(period_stats.delivery_attempts)}`
122
+ );
123
+ console.log(` ${tui.muted('Retries:')} ${formatNumber(period_stats.retry_count)}`);
124
+
125
+ tui.newline();
126
+ console.log(tui.colorPrimary('Delivery Latency:'));
127
+ console.log(` ${tui.muted('Average:')} ${formatLatency(latency.avg_ms)}`);
128
+ if (latency.p50_ms != null)
129
+ console.log(` ${tui.muted('Median (P50):')} ${formatLatency(latency.p50_ms)}`);
130
+ if (latency.p95_ms != null)
131
+ console.log(` ${tui.muted('P95:')} ${formatLatency(latency.p95_ms)}`);
132
+ if (latency.p99_ms != null)
133
+ console.log(` ${tui.muted('P99:')} ${formatLatency(latency.p99_ms)}`);
134
+ if (latency.max_ms != null)
135
+ console.log(` ${tui.muted('Max:')} ${formatLatency(latency.max_ms)}`);
136
+
137
+ tui.newline();
138
+ console.log(tui.colorPrimary('Consumer Latency:'));
139
+ console.log(` ${tui.muted('Average:')} ${formatLatency(consumer_latency.avg_ms)}`);
140
+ if (consumer_latency.p50_ms != null)
141
+ console.log(` ${tui.muted('Median (P50):')} ${formatLatency(consumer_latency.p50_ms)}`);
142
+ if (consumer_latency.p95_ms != null)
143
+ console.log(` ${tui.muted('P95:')} ${formatLatency(consumer_latency.p95_ms)}`);
144
+ if (consumer_latency.p99_ms != null)
145
+ console.log(` ${tui.muted('P99:')} ${formatLatency(consumer_latency.p99_ms)}`);
146
+
147
+ if (analytics.destinations && analytics.destinations.length > 0) {
148
+ tui.newline();
149
+ tui.info('Destinations:');
150
+ const destData = analytics.destinations.map((d) => {
151
+ const total = d.success_count + d.failure_count;
152
+ const errorRate = total > 0 ? (d.failure_count / total) * 100 : 0;
153
+ return {
154
+ ID: d.id.slice(0, 12) + '...',
155
+ URL: d.url.length > 40 ? d.url.slice(0, 37) + '...' : d.url,
156
+ Success: formatNumber(d.success_count),
157
+ Failed: formatNumber(d.failure_count),
158
+ 'Avg Response': d.avg_response_time_ms ? formatLatency(d.avg_response_time_ms) : '-',
159
+ 'Error %': formatPercent(errorRate),
160
+ };
161
+ });
162
+ tui.table(destData);
163
+ }
164
+ }
165
+
166
+ function displayStreamEvent(event: SSEStatsEvent, queueName?: string): void {
167
+ const time = new Date(event.timestamp).toLocaleTimeString();
168
+ const prefix = queueName ? `[${queueName}]` : '[org]';
169
+
170
+ process.stdout.write('\x1b[2K\r');
171
+ process.stdout.write(
172
+ `${tui.colorMuted(time)} ${prefix} ` +
173
+ `Backlog: ${tui.colorInfo(formatNumber(event.backlog))} | ` +
174
+ `In-Flight: ${tui.colorInfo(formatNumber(event.messages_in_flight))} | ` +
175
+ `Throughput: ${tui.colorSuccess(formatNumber(event.throughput_1m))}/min | ` +
176
+ `Latency: ${formatLatency(event.avg_latency_ms)} | ` +
177
+ `Errors: ${event.error_rate_1m > 0 ? tui.colorError(String(event.error_rate_1m)) : '0'}/min`
178
+ );
179
+ }
180
+
181
+ export const statsSubcommand = createCommand({
182
+ name: 'stats',
183
+ description: 'View queue analytics and statistics',
184
+ tags: ['read-only', 'requires-auth'],
185
+ requires: { auth: true, org: true },
186
+ examples: [
187
+ {
188
+ command: getCommand('cloud queue stats'),
189
+ description: 'View org-level analytics for all queues',
190
+ },
191
+ {
192
+ command: getCommand('cloud queue stats --name my-queue'),
193
+ description: 'View detailed analytics for a specific queue',
194
+ },
195
+ {
196
+ command: getCommand('cloud queue stats --live'),
197
+ description: 'Stream real-time stats (Ctrl+C to exit)',
198
+ },
199
+ {
200
+ command: getCommand('cloud queue stats --name my-queue --live --interval 10'),
201
+ description: 'Stream queue stats every 10 seconds',
202
+ },
203
+ ],
204
+ schema: {
205
+ args: z.object({
206
+ name: z.string().optional().describe('Queue name (omit for org-level stats)'),
207
+ }),
208
+ options: z.object({
209
+ live: z.boolean().default(false).describe('Stream real-time stats'),
210
+ interval: z.number().default(5).describe('Refresh interval in seconds (for --live)'),
211
+ start: z.string().optional().describe('Start time (ISO 8601)'),
212
+ end: z.string().optional().describe('End time (ISO 8601)'),
213
+ granularity: z.enum(['minute', 'hour', 'day']).optional().describe('Time granularity'),
214
+ }),
215
+ response: StatsResponseSchema,
216
+ },
217
+ idempotent: true,
218
+
219
+ async handler(ctx) {
220
+ const { args, opts, options } = ctx;
221
+ const client = await createQueueAPIClient(ctx);
222
+ const apiOptions = getQueueApiOptions(ctx);
223
+
224
+ const analyticsOptions = {
225
+ ...apiOptions,
226
+ start: opts.start,
227
+ end: opts.end,
228
+ granularity: opts.granularity,
229
+ };
230
+
231
+ if (opts.live) {
232
+ const events: SSEStatsEvent[] = [];
233
+
234
+ tui.info(
235
+ `Streaming stats every ${opts.interval}s... ${tui.colorMuted('(Ctrl+C to exit)')}`
236
+ );
237
+ tui.info('');
238
+
239
+ const handleInterrupt = () => {
240
+ tui.info('');
241
+ tui.info('Stream stopped.');
242
+ process.exit(0);
243
+ };
244
+ process.on('SIGINT', handleInterrupt);
245
+
246
+ try {
247
+ if (args.name) {
248
+ const stream = streamQueueAnalytics(client, args.name, {
249
+ interval: opts.interval,
250
+ orgId: apiOptions?.orgId,
251
+ });
252
+ for await (const event of stream) {
253
+ events.push(event);
254
+ if (!options.json) {
255
+ displayStreamEvent(event, args.name);
256
+ }
257
+ }
258
+ } else {
259
+ const stream = streamOrgAnalytics(client, {
260
+ interval: opts.interval,
261
+ orgId: apiOptions?.orgId,
262
+ });
263
+ for await (const event of stream) {
264
+ events.push(event);
265
+ if (!options.json) {
266
+ displayStreamEvent(event);
267
+ }
268
+ }
269
+ }
270
+ } finally {
271
+ process.off('SIGINT', handleInterrupt);
272
+ }
273
+
274
+ return { type: 'stream' as const, events };
275
+ }
276
+
277
+ if (args.name) {
278
+ const analytics = await getQueueAnalytics(client, args.name, analyticsOptions);
279
+
280
+ if (!options.json) {
281
+ displayQueueAnalytics(analytics);
282
+ }
283
+
284
+ return { type: 'queue' as const, analytics };
285
+ }
286
+
287
+ const analytics = await getOrgAnalytics(client, analyticsOptions);
288
+
289
+ if (!options.json) {
290
+ displayOrgAnalytics(analytics);
291
+ }
292
+
293
+ return { type: 'org' as const, analytics };
294
+ },
295
+ });
296
+
297
+ export default statsSubcommand;
@@ -0,0 +1,34 @@
1
+ import type { Logger } from '@agentuity/core';
2
+ import { APIClient, type QueueApiOptions } from '@agentuity/server';
3
+ import { getGlobalCatalystAPIClient } from '../../../config';
4
+ import type { AuthData, Config, GlobalOptions } from '../../../types';
5
+
6
+ /**
7
+ * Context required for queue API operations.
8
+ */
9
+ export interface QueueContext {
10
+ logger: Logger;
11
+ auth: AuthData;
12
+ config: Config | null;
13
+ options: GlobalOptions;
14
+ orgId?: string;
15
+ }
16
+
17
+ /**
18
+ * Creates an API client for queue operations.
19
+ *
20
+ * Queues are global resources that don't require a project context.
21
+ * Uses the global Catalyst API client with user authentication.
22
+ */
23
+ export async function createQueueAPIClient(ctx: QueueContext): Promise<APIClient> {
24
+ return getGlobalCatalystAPIClient(ctx.logger, ctx.auth, ctx.config?.name);
25
+ }
26
+
27
+ /**
28
+ * Creates QueueApiOptions from the CLI context.
29
+ * Prioritizes explicit orgId on context, then falls back to global --org-id option.
30
+ */
31
+ export function getQueueApiOptions(ctx: QueueContext): QueueApiOptions | undefined {
32
+ const orgId = ctx.orgId ?? ctx.options.orgId;
33
+ return orgId ? { orgId } : undefined;
34
+ }
@@ -5,10 +5,12 @@ import { YAML } from 'bun';
5
5
  import * as tar from 'tar';
6
6
  import { createCommand } from '../../../../types';
7
7
  import * as tui from '../../../../tui';
8
+ import { ErrorCode } from '../../../../errors';
8
9
  import { getCommand } from '../../../../command-prefix';
9
10
  import {
10
11
  snapshotBuildInit,
11
12
  snapshotBuildFinalize,
13
+ snapshotUpload,
12
14
  SnapshotBuildFileSchema,
13
15
  } from '@agentuity/server';
14
16
  import type { SnapshotFileInfo } from '@agentuity/server';
@@ -35,8 +37,13 @@ const SnapshotBuildResponseSchema = z.object({
35
37
  .record(z.string(), z.string())
36
38
  .optional()
37
39
  .describe('User-defined metadata key-value pairs'),
40
+ error: z.string().optional().describe('Error message if build failed'),
41
+ malwareDetected: z.boolean().optional().describe('True if malware was detected'),
42
+ virusName: z.string().optional().describe('Name of detected virus'),
38
43
  });
39
44
 
45
+ const MALWARE_REGEX = /malware detected \(([^)]+)\)/i;
46
+
40
47
  interface FileEntry {
41
48
  path: string;
42
49
  absolutePath: string;
@@ -229,6 +236,33 @@ async function createTarGzArchive(
229
236
  );
230
237
  }
231
238
 
239
+ function createProgressStream(
240
+ file: ReturnType<typeof Bun.file>,
241
+ totalSize: number,
242
+ onProgress: (percent: number) => void
243
+ ): ReadableStream<Uint8Array> {
244
+ let bytesRead = 0;
245
+ const reader = file.stream().getReader();
246
+
247
+ return new ReadableStream<Uint8Array>({
248
+ async pull(controller) {
249
+ const { done, value } = await reader.read();
250
+ if (done) {
251
+ controller.close();
252
+ onProgress(100);
253
+ return;
254
+ }
255
+ bytesRead += value.byteLength;
256
+ const percent = Math.min(99, Math.floor((bytesRead / totalSize) * 100));
257
+ onProgress(percent);
258
+ controller.enqueue(value);
259
+ },
260
+ cancel() {
261
+ reader.cancel();
262
+ },
263
+ });
264
+ }
265
+
232
266
  async function generateContentHash(params: {
233
267
  runtime: string;
234
268
  description?: string;
@@ -254,7 +288,8 @@ async function generateContentHash(params: {
254
288
  const sortedFiles = [...params.files].sort((a, b) => a.path.localeCompare(b.path));
255
289
  for (const file of sortedFiles) {
256
290
  const contentHash = params.fileHashes.get(file.path) ?? '';
257
- hash.update(`file:${file.path}:${file.size}:${contentHash}\n`);
291
+ const mode = file.mode.toString(8).padStart(4, '0');
292
+ hash.update(`file:${file.path}:${file.size}:${contentHash}:${mode}:${file.contentType}\n`);
258
293
  }
259
294
  }
260
295
 
@@ -315,6 +350,10 @@ export const buildSubcommand = createCommand({
315
350
  description: z.string().optional().describe('Snapshot description (overrides build file)'),
316
351
  metadata: z.array(z.string()).optional().describe('Metadata key-value pairs (KEY=VALUE)'),
317
352
  force: z.boolean().optional().describe('Force rebuild even if content is unchanged'),
353
+ public: z
354
+ .boolean()
355
+ .optional()
356
+ .describe('Make snapshot public (enables virus scanning, no encryption)'),
318
357
  }),
319
358
  response: SnapshotBuildResponseSchema,
320
359
  },
@@ -457,21 +496,35 @@ export const buildSubcommand = createCommand({
457
496
  files = await resolveFileGlobs(directory, buildConfig.files);
458
497
  }
459
498
 
460
- const fileList: SnapshotFileInfo[] = Array.from(files.values()).map((f) => ({
461
- path: f.path,
462
- size: f.size,
463
- }));
464
- const totalSize = fileList.reduce((sum, f) => sum + f.size, 0);
465
-
466
- const fileHashes = new Map<string, string>();
499
+ const fileMetadata = new Map<string, { sha256: string; contentType: string; mode: number }>();
467
500
  for (const file of files.values()) {
468
501
  const fullPath = join(directory, file.path);
469
502
  const bunFile = Bun.file(fullPath);
470
503
  const content = await bunFile.arrayBuffer();
471
504
  const hash = createHash('sha256').update(Buffer.from(content)).digest('hex');
472
- fileHashes.set(file.path, hash);
505
+ const contentType = bunFile.type || 'application/octet-stream';
506
+ const stat = statSync(fullPath);
507
+ const mode = stat.mode & 0o7777; // Extract permission bits only
508
+ fileMetadata.set(file.path, { sha256: hash, contentType, mode });
509
+ }
510
+
511
+ const fileHashes = new Map<string, string>();
512
+ for (const [path, meta] of fileMetadata) {
513
+ fileHashes.set(path, meta.sha256);
473
514
  }
474
515
 
516
+ const fileList: SnapshotFileInfo[] = Array.from(files.values()).map((f) => {
517
+ const meta = fileMetadata.get(f.path);
518
+ return {
519
+ path: f.path,
520
+ size: f.size,
521
+ sha256: meta?.sha256 ?? '',
522
+ contentType: meta?.contentType ?? 'application/octet-stream',
523
+ mode: meta?.mode ?? 0o644,
524
+ };
525
+ });
526
+ const totalSize = fileList.reduce((sum, f) => sum + f.size, 0);
527
+
475
528
  const contentHash = await generateContentHash({
476
529
  runtime: buildConfig.runtime,
477
530
  description: finalDescription,
@@ -565,6 +618,8 @@ export const buildSubcommand = createCommand({
565
618
 
566
619
  const client = getCatalystAPIClient(logger, auth, region);
567
620
 
621
+ const isPublic = opts.public === true;
622
+
568
623
  const initResult = await tui.spinner({
569
624
  message: 'Initializing snapshot build...',
570
625
  clearOnSuccess: true,
@@ -576,7 +631,8 @@ export const buildSubcommand = createCommand({
576
631
  description: finalDescription,
577
632
  contentHash,
578
633
  force: opts.force,
579
- encrypt: true,
634
+ encrypt: !isPublic,
635
+ public: isPublic,
580
636
  orgId,
581
637
  });
582
638
  },
@@ -611,7 +667,7 @@ export const buildSubcommand = createCommand({
611
667
  };
612
668
  }
613
669
 
614
- // Encrypt the archive if public key is provided
670
+ // Encrypt the archive if public key is provided (private snapshots only)
615
671
  let uploadPath = archivePath;
616
672
  let uploadSize = archiveSize;
617
673
 
@@ -646,28 +702,89 @@ export const buildSubcommand = createCommand({
646
702
  uploadSize = Bun.file(encryptedPath).size;
647
703
  }
648
704
 
649
- await tui.spinner({
650
- message: 'Uploading snapshot...',
651
- type: 'progress',
652
- clearOnSuccess: true,
653
- callback: async (updateProgress) => {
654
- const uploadFile = Bun.file(uploadPath);
655
- const response = await fetch(initResult.uploadUrl!, {
656
- method: 'PUT',
657
- headers: {
658
- 'Content-Type': 'application/gzip',
659
- 'Content-Length': String(uploadSize),
705
+ if (initResult.uploadUrl) {
706
+ // Private snapshot: upload directly to S3
707
+ // Use Bun.file() directly as body - Bun sets Content-Length automatically from file size
708
+ await tui.spinner({
709
+ message: 'Uploading snapshot...',
710
+ clearOnSuccess: true,
711
+ callback: async () => {
712
+ const uploadFile = Bun.file(uploadPath);
713
+ const response = await fetch(initResult.uploadUrl!, {
714
+ method: 'PUT',
715
+ headers: {
716
+ 'Content-Type': 'application/gzip',
717
+ },
718
+ body: uploadFile,
719
+ });
720
+
721
+ if (!response.ok) {
722
+ throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
723
+ }
724
+ },
725
+ });
726
+ } else {
727
+ // Public snapshot: upload via Catalyst (with virus scanning)
728
+ try {
729
+ await tui.spinner({
730
+ message: 'Uploading and scanning snapshot...',
731
+ type: 'progress',
732
+ clearOnSuccess: true,
733
+ clearOnError: true,
734
+ callback: async (updateProgress) => {
735
+ const uploadFile = Bun.file(uploadPath);
736
+ const progressStream = createProgressStream(uploadFile, uploadSize, updateProgress);
737
+ await snapshotUpload(client, {
738
+ snapshotId: initResult.snapshotId!,
739
+ body: progressStream,
740
+ contentLength: uploadSize,
741
+ orgId,
742
+ });
660
743
  },
661
- body: uploadFile,
662
744
  });
663
-
664
- if (!response.ok) {
665
- throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
745
+ } catch (err) {
746
+ const errorMessage = err instanceof Error ? err.message : String(err);
747
+ const malwareMatch = MALWARE_REGEX.exec(errorMessage);
748
+
749
+ if (malwareMatch) {
750
+ const virusName = malwareMatch[1];
751
+
752
+ if (options.json) {
753
+ console.log(
754
+ JSON.stringify(
755
+ {
756
+ snapshotId: '',
757
+ name: finalName ?? '',
758
+ tag: opts.tag,
759
+ runtime: buildConfig.runtime,
760
+ sizeBytes: totalSize,
761
+ fileCount: fileList.length,
762
+ createdAt: new Date().toISOString(),
763
+ error: errorMessage,
764
+ malwareDetected: true,
765
+ virusName,
766
+ },
767
+ null,
768
+ 2
769
+ )
770
+ );
771
+ process.exit(ErrorCode.MALWARE_DETECTED);
772
+ }
773
+
774
+ console.log('');
775
+ tui.errorBox(
776
+ 'Malware Detected',
777
+ `Your snapshot was rejected because it contains malware.\n\nVirus: ${virusName}\n\nPlease remove the infected files and try again.`
778
+ );
779
+ tui.fatal(
780
+ 'Snapshot build failed due to malware detection',
781
+ ErrorCode.MALWARE_DETECTED
782
+ );
666
783
  }
667
784
 
668
- updateProgress(100);
669
- },
670
- });
785
+ throw err;
786
+ }
787
+ }
671
788
 
672
789
  const snapshot = await tui.spinner({
673
790
  message: 'Finalizing snapshot...',
@@ -39,6 +39,10 @@ export const createSubcommand = createCommand({
39
39
  ),
40
40
  description: 'Create a named snapshot with description',
41
41
  },
42
+ {
43
+ command: getCommand('cloud sandbox snapshot create sbx_abc123 --public'),
44
+ description: 'Create a public snapshot',
45
+ },
42
46
  ],
43
47
  schema: {
44
48
  args: z.object({
@@ -51,6 +55,7 @@ export const createSubcommand = createCommand({
51
55
  .describe('Display name for the snapshot (letters, numbers, underscores, dashes only)'),
52
56
  description: z.string().optional().describe('Description of the snapshot'),
53
57
  tag: z.string().optional().describe('Tag for the snapshot (defaults to "latest")'),
58
+ public: z.boolean().optional().default(false).describe('Make the snapshot publicly accessible'),
54
59
  }),
55
60
  response: SnapshotCreateResponseSchema,
56
61
  },
@@ -86,17 +91,29 @@ export const createSubcommand = createCommand({
86
91
  name: opts.name,
87
92
  description: opts.description,
88
93
  tag: opts.tag,
94
+ public: opts.public,
89
95
  orgId,
90
96
  });
91
97
 
92
98
  if (!options.json) {
93
- tui.success(`created snapshot ${tui.bold(snapshot.snapshotId)}`);
94
- tui.info(`Name: ${snapshot.name}`);
95
- if (snapshot.description) {
96
- tui.info(`Description: ${snapshot.description}`);
97
- }
98
- tui.info(`Size: ${tui.formatBytes(snapshot.sizeBytes)}, Files: ${snapshot.fileCount}`);
99
- tui.info(`Tag: ${snapshot.tag ?? 'latest'}`);
99
+ tui.success(`Created snapshot ${tui.bold(snapshot.snapshotId)}`);
100
+ console.log('');
101
+
102
+ tui.table(
103
+ [
104
+ {
105
+ Name: snapshot.name,
106
+ Description: snapshot.description ?? '-',
107
+ Tag: snapshot.tag ?? 'latest',
108
+ Size: tui.formatBytes(snapshot.sizeBytes),
109
+ Files: snapshot.fileCount.toFixed(),
110
+ Visibility: snapshot.public ? 'public' : 'private',
111
+ Created: snapshot.createdAt,
112
+ },
113
+ ],
114
+ ['Name', 'Description', 'Tag', 'Size', 'Files', 'Visibility', 'Created'],
115
+ { layout: 'vertical', padStart: ' ' }
116
+ );
100
117
  }
101
118
 
102
119
  return {
@@ -10,6 +10,9 @@ import { getGlobalCatalystAPIClient } from '../../../../config';
10
10
  const SnapshotFileSchema = z.object({
11
11
  path: z.string(),
12
12
  size: z.number(),
13
+ sha256: z.string(),
14
+ contentType: z.string(),
15
+ mode: z.number(),
13
16
  });
14
17
 
15
18
  const SandboxInfoSchema = z.object({
@@ -22,10 +25,14 @@ const SandboxInfoSchema = z.object({
22
25
  const SnapshotGetResponseSchema = z.object({
23
26
  snapshotId: z.string().describe('Snapshot ID'),
24
27
  name: z.string().describe('Snapshot name'),
28
+ fullName: z.string().optional().describe('Full name with org slug (@slug/name:tag)'),
25
29
  tag: z.string().nullable().optional().describe('Snapshot tag'),
26
30
  sizeBytes: z.number().describe('Snapshot size in bytes'),
27
31
  fileCount: z.number().describe('Number of files'),
28
32
  parentSnapshotId: z.string().nullable().optional().describe('Parent snapshot ID'),
33
+ public: z.boolean().optional().describe('Whether snapshot is publicly accessible'),
34
+ orgName: z.string().optional().describe('Organization name (for public snapshots)'),
35
+ orgSlug: z.string().optional().describe('Organization slug (for public snapshots)'),
29
36
  createdAt: z.string().describe('Creation timestamp'),
30
37
  downloadUrl: z.string().optional().describe('Presigned download URL'),
31
38
  files: z.array(SnapshotFileSchema).nullable().optional().describe('Files in snapshot'),
@@ -87,6 +94,15 @@ export const getSubcommand = createCommand({
87
94
  }
88
95
  tableData['Size'] = tui.formatBytes(snapshot.sizeBytes);
89
96
  tableData['Files'] = snapshot.fileCount;
97
+ if (snapshot.public) {
98
+ tableData['Public'] = 'Yes';
99
+ if (snapshot.fullName) {
100
+ tableData['Full Name'] = snapshot.fullName;
101
+ }
102
+ if (snapshot.orgName) {
103
+ tableData['Publisher'] = snapshot.orgName;
104
+ }
105
+ }
90
106
  tableData['Created'] = snapshot.createdAt;
91
107
  if (snapshot.parentSnapshotId) {
92
108
  tableData['Parent'] = snapshot.parentSnapshotId;