@agentuity/cli 1.0.24 → 1.0.26

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 (152) hide show
  1. package/dist/cache/index.d.ts +1 -0
  2. package/dist/cache/index.d.ts.map +1 -1
  3. package/dist/cache/index.js +1 -0
  4. package/dist/cache/index.js.map +1 -1
  5. package/dist/cache/user-cache.d.ts +20 -0
  6. package/dist/cache/user-cache.d.ts.map +1 -0
  7. package/dist/cache/user-cache.js +79 -0
  8. package/dist/cache/user-cache.js.map +1 -0
  9. package/dist/cmd/auth/logout.d.ts.map +1 -1
  10. package/dist/cmd/auth/logout.js +3 -1
  11. package/dist/cmd/auth/logout.js.map +1 -1
  12. package/dist/cmd/build/entry-generator.d.ts +4 -0
  13. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  14. package/dist/cmd/build/entry-generator.js +18 -3
  15. package/dist/cmd/build/entry-generator.js.map +1 -1
  16. package/dist/cmd/build/vite/bun-dev-server.d.ts +1 -0
  17. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
  18. package/dist/cmd/build/vite/bun-dev-server.js +11 -9
  19. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  20. package/dist/cmd/cloud/db/stats.d.ts.map +1 -1
  21. package/dist/cmd/cloud/db/stats.js.map +1 -1
  22. package/dist/cmd/cloud/email/create.d.ts.map +1 -1
  23. package/dist/cmd/cloud/email/create.js +2 -7
  24. package/dist/cmd/cloud/email/create.js.map +1 -1
  25. package/dist/cmd/cloud/email/destination/delete.d.ts.map +1 -1
  26. package/dist/cmd/cloud/email/destination/delete.js +5 -1
  27. package/dist/cmd/cloud/email/destination/delete.js.map +1 -1
  28. package/dist/cmd/cloud/email/get.d.ts.map +1 -1
  29. package/dist/cmd/cloud/email/get.js +30 -7
  30. package/dist/cmd/cloud/email/get.js.map +1 -1
  31. package/dist/cmd/cloud/email/list.d.ts.map +1 -1
  32. package/dist/cmd/cloud/email/list.js +0 -6
  33. package/dist/cmd/cloud/email/list.js.map +1 -1
  34. package/dist/cmd/cloud/email/send.d.ts.map +1 -1
  35. package/dist/cmd/cloud/email/send.js +1 -5
  36. package/dist/cmd/cloud/email/send.js.map +1 -1
  37. package/dist/cmd/cloud/email/stats.d.ts.map +1 -1
  38. package/dist/cmd/cloud/email/stats.js.map +1 -1
  39. package/dist/cmd/cloud/email/util.d.ts +0 -1
  40. package/dist/cmd/cloud/email/util.d.ts.map +1 -1
  41. package/dist/cmd/cloud/email/util.js +1 -3
  42. package/dist/cmd/cloud/email/util.js.map +1 -1
  43. package/dist/cmd/cloud/sandbox/snapshot/build.d.ts.map +1 -1
  44. package/dist/cmd/cloud/sandbox/snapshot/build.js +4 -1
  45. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  46. package/dist/cmd/cloud/sandbox/stats.d.ts.map +1 -1
  47. package/dist/cmd/cloud/sandbox/stats.js.map +1 -1
  48. package/dist/cmd/cloud/schedule/delete.d.ts.map +1 -1
  49. package/dist/cmd/cloud/schedule/delete.js +4 -1
  50. package/dist/cmd/cloud/schedule/delete.js.map +1 -1
  51. package/dist/cmd/cloud/schedule/delivery/list.d.ts.map +1 -1
  52. package/dist/cmd/cloud/schedule/delivery/list.js.map +1 -1
  53. package/dist/cmd/cloud/schedule/destination/create.d.ts.map +1 -1
  54. package/dist/cmd/cloud/schedule/destination/create.js +3 -1
  55. package/dist/cmd/cloud/schedule/destination/create.js.map +1 -1
  56. package/dist/cmd/cloud/schedule/destination/index.d.ts.map +1 -1
  57. package/dist/cmd/cloud/schedule/destination/index.js.map +1 -1
  58. package/dist/cmd/cloud/schedule/destination/list.d.ts.map +1 -1
  59. package/dist/cmd/cloud/schedule/destination/list.js.map +1 -1
  60. package/dist/cmd/cloud/schedule/get.d.ts.map +1 -1
  61. package/dist/cmd/cloud/schedule/get.js +4 -1
  62. package/dist/cmd/cloud/schedule/get.js.map +1 -1
  63. package/dist/cmd/cloud/schedule/index.d.ts.map +1 -1
  64. package/dist/cmd/cloud/schedule/index.js +4 -1
  65. package/dist/cmd/cloud/schedule/index.js.map +1 -1
  66. package/dist/cmd/cloud/schedule/list.d.ts.map +1 -1
  67. package/dist/cmd/cloud/schedule/list.js.map +1 -1
  68. package/dist/cmd/cloud/schedule/stats.d.ts.map +1 -1
  69. package/dist/cmd/cloud/schedule/stats.js.map +1 -1
  70. package/dist/cmd/cloud/schedule/util.d.ts.map +1 -1
  71. package/dist/cmd/cloud/schedule/util.js +1 -2
  72. package/dist/cmd/cloud/schedule/util.js.map +1 -1
  73. package/dist/cmd/cloud/services/stats.d.ts.map +1 -1
  74. package/dist/cmd/cloud/services/stats.js.map +1 -1
  75. package/dist/cmd/cloud/stream/index.d.ts.map +1 -1
  76. package/dist/cmd/cloud/stream/index.js +7 -1
  77. package/dist/cmd/cloud/stream/index.js.map +1 -1
  78. package/dist/cmd/cloud/stream/stats.d.ts.map +1 -1
  79. package/dist/cmd/cloud/stream/stats.js.map +1 -1
  80. package/dist/cmd/cloud/task/attachment.d.ts +2 -0
  81. package/dist/cmd/cloud/task/attachment.d.ts.map +1 -0
  82. package/dist/cmd/cloud/task/attachment.js +393 -0
  83. package/dist/cmd/cloud/task/attachment.js.map +1 -0
  84. package/dist/cmd/cloud/task/create.d.ts.map +1 -1
  85. package/dist/cmd/cloud/task/create.js +126 -5
  86. package/dist/cmd/cloud/task/create.js.map +1 -1
  87. package/dist/cmd/cloud/task/get.d.ts.map +1 -1
  88. package/dist/cmd/cloud/task/get.js +29 -11
  89. package/dist/cmd/cloud/task/get.js.map +1 -1
  90. package/dist/cmd/cloud/task/index.d.ts.map +1 -1
  91. package/dist/cmd/cloud/task/index.js +13 -1
  92. package/dist/cmd/cloud/task/index.js.map +1 -1
  93. package/dist/cmd/cloud/task/list.d.ts.map +1 -1
  94. package/dist/cmd/cloud/task/list.js +31 -15
  95. package/dist/cmd/cloud/task/list.js.map +1 -1
  96. package/dist/cmd/cloud/task/stats.js +2 -0
  97. package/dist/cmd/cloud/task/stats.js.map +1 -1
  98. package/dist/cmd/cloud/task/util.d.ts.map +1 -1
  99. package/dist/cmd/cloud/task/util.js +2 -4
  100. package/dist/cmd/cloud/task/util.js.map +1 -1
  101. package/dist/cmd/cloud/webhook/create.d.ts.map +1 -1
  102. package/dist/cmd/cloud/webhook/create.js +6 -1
  103. package/dist/cmd/cloud/webhook/create.js.map +1 -1
  104. package/dist/cmd/cloud/webhook/deliveries.d.ts.map +1 -1
  105. package/dist/cmd/cloud/webhook/deliveries.js +4 -1
  106. package/dist/cmd/cloud/webhook/deliveries.js.map +1 -1
  107. package/dist/cmd/cloud/webhook/destinations.d.ts.map +1 -1
  108. package/dist/cmd/cloud/webhook/destinations.js +4 -5
  109. package/dist/cmd/cloud/webhook/destinations.js.map +1 -1
  110. package/dist/cmd/dev/index.d.ts.map +1 -1
  111. package/dist/cmd/dev/index.js +80 -34
  112. package/dist/cmd/dev/index.js.map +1 -1
  113. package/package.json +6 -6
  114. package/src/cache/index.ts +2 -0
  115. package/src/cache/user-cache.ts +93 -0
  116. package/src/cmd/auth/logout.ts +3 -1
  117. package/src/cmd/build/entry-generator.ts +34 -4
  118. package/src/cmd/build/vite/bun-dev-server.ts +21 -9
  119. package/src/cmd/cloud/db/stats.ts +4 -12
  120. package/src/cmd/cloud/email/create.ts +2 -7
  121. package/src/cmd/cloud/email/destination/delete.ts +5 -1
  122. package/src/cmd/cloud/email/get.ts +42 -7
  123. package/src/cmd/cloud/email/list.ts +0 -6
  124. package/src/cmd/cloud/email/send.ts +1 -5
  125. package/src/cmd/cloud/email/stats.ts +2 -6
  126. package/src/cmd/cloud/email/util.ts +1 -3
  127. package/src/cmd/cloud/sandbox/snapshot/build.ts +25 -6
  128. package/src/cmd/cloud/sandbox/stats.ts +2 -6
  129. package/src/cmd/cloud/schedule/delete.ts +4 -1
  130. package/src/cmd/cloud/schedule/delivery/list.ts +15 -13
  131. package/src/cmd/cloud/schedule/destination/create.ts +11 -3
  132. package/src/cmd/cloud/schedule/destination/index.ts +3 -1
  133. package/src/cmd/cloud/schedule/destination/list.ts +19 -17
  134. package/src/cmd/cloud/schedule/get.ts +25 -20
  135. package/src/cmd/cloud/schedule/index.ts +4 -1
  136. package/src/cmd/cloud/schedule/list.ts +18 -16
  137. package/src/cmd/cloud/schedule/stats.ts +1 -3
  138. package/src/cmd/cloud/schedule/util.ts +1 -2
  139. package/src/cmd/cloud/services/stats.ts +13 -39
  140. package/src/cmd/cloud/stream/index.ts +7 -1
  141. package/src/cmd/cloud/stream/stats.ts +2 -6
  142. package/src/cmd/cloud/task/attachment.ts +432 -0
  143. package/src/cmd/cloud/task/create.ts +131 -5
  144. package/src/cmd/cloud/task/get.ts +30 -12
  145. package/src/cmd/cloud/task/index.ts +13 -1
  146. package/src/cmd/cloud/task/list.ts +31 -15
  147. package/src/cmd/cloud/task/stats.ts +3 -3
  148. package/src/cmd/cloud/task/util.ts +2 -4
  149. package/src/cmd/cloud/webhook/create.ts +6 -1
  150. package/src/cmd/cloud/webhook/deliveries.ts +4 -5
  151. package/src/cmd/cloud/webhook/destinations.ts +4 -5
  152. package/src/cmd/dev/index.ts +91 -48
@@ -49,22 +49,24 @@ export const listSubcommand = createCommand({
49
49
  tui.info('No schedules found');
50
50
  } else {
51
51
  tui.table(
52
- result.schedules.map((item: {
53
- id: string;
54
- created_at: string;
55
- updated_at: string;
56
- created_by: string;
57
- name: string;
58
- description: string | null;
59
- expression: string;
60
- due_date: string;
61
- }) => ({
62
- Name: item.name,
63
- ID: item.id,
64
- Expression: item.expression,
65
- 'Next Due': item.due_date,
66
- Created: new Date(item.created_at).toLocaleString(),
67
- })),
52
+ result.schedules.map(
53
+ (item: {
54
+ id: string;
55
+ created_at: string;
56
+ updated_at: string;
57
+ created_by: string;
58
+ name: string;
59
+ description: string | null;
60
+ expression: string;
61
+ due_date: string;
62
+ }) => ({
63
+ Name: item.name,
64
+ ID: item.id,
65
+ Expression: item.expression,
66
+ 'Next Due': item.due_date,
67
+ Created: new Date(item.created_at).toLocaleString(),
68
+ })
69
+ ),
68
70
  ['Name', 'ID', 'Expression', 'Next Due', 'Created']
69
71
  );
70
72
  }
@@ -19,9 +19,7 @@ function displayStats(data: ServiceStatsData): void {
19
19
  }
20
20
  tui.header('Schedule Statistics');
21
21
  tui.newline();
22
- console.log(
23
- ` ${tui.muted('Schedules:')} ${formatNumber(svc.scheduleCount)}`
24
- );
22
+ console.log(` ${tui.muted('Schedules:')} ${formatNumber(svc.scheduleCount)}`);
25
23
  console.log(
26
24
  ` ${tui.muted('Deliveries:')} ${formatNumber(svc.totalDeliveries)} (${svc.successDeliveries} ok, ${svc.failedDeliveries} failed)`
27
25
  );
@@ -14,8 +14,7 @@ export interface ScheduleContext {
14
14
 
15
15
  export async function createScheduleAdapter(ctx: ScheduleContext) {
16
16
  const orgId =
17
- ctx.options.orgId ??
18
- (process.env.AGENTUITY_CLOUD_ORG_ID || ctx.config?.preferences?.orgId);
17
+ ctx.options.orgId ?? (process.env.AGENTUITY_CLOUD_ORG_ID || ctx.config?.preferences?.orgId);
19
18
  if (!orgId) {
20
19
  tui.fatal('Organization ID is required. Use --org-id flag or set AGENTUITY_CLOUD_ORG_ID.');
21
20
  }
@@ -34,9 +34,7 @@ function displayServiceStats(data: ServiceStatsData): void {
34
34
  console.log(
35
35
  ` ${tui.muted('Namespaces:')} ${formatNumber(services.keyvalue.namespaceCount)}`
36
36
  );
37
- console.log(
38
- ` ${tui.muted('Keys:')} ${formatNumber(services.keyvalue.keyCount)}`
39
- );
37
+ console.log(` ${tui.muted('Keys:')} ${formatNumber(services.keyvalue.keyCount)}`);
40
38
  console.log(
41
39
  ` ${tui.muted('Total Size:')} ${tui.formatBytes(services.keyvalue.totalSizeBytes)}`
42
40
  );
@@ -61,15 +59,11 @@ function displayServiceStats(data: ServiceStatsData): void {
61
59
  hasData = true;
62
60
  tui.newline();
63
61
  console.log(tui.colorPrimary('Queue:'));
64
- console.log(
65
- ` ${tui.muted('Queues:')} ${formatNumber(services.queue.queueCount)}`
66
- );
62
+ console.log(` ${tui.muted('Queues:')} ${formatNumber(services.queue.queueCount)}`);
67
63
  console.log(
68
64
  ` ${tui.muted('Total Messages:')} ${formatNumber(services.queue.totalMessages)}`
69
65
  );
70
- console.log(
71
- ` ${tui.muted('DLQ Messages:')} ${formatNumber(services.queue.totalDlq)}`
72
- );
66
+ console.log(` ${tui.muted('DLQ Messages:')} ${formatNumber(services.queue.totalDlq)}`);
73
67
  }
74
68
 
75
69
  if (services.stream) {
@@ -92,13 +86,9 @@ function displayServiceStats(data: ServiceStatsData): void {
92
86
  console.log(
93
87
  ` ${tui.muted('Active:')} ${formatNumber(sb.totalActive)} (${sb.running} running, ${sb.idle} idle, ${sb.creating} creating)`
94
88
  );
95
- console.log(
96
- ` ${tui.muted('Executions:')} ${formatNumber(sb.totalExecutions)}`
97
- );
89
+ console.log(` ${tui.muted('Executions:')} ${formatNumber(sb.totalExecutions)}`);
98
90
  console.log(` ${tui.muted('CPU Time:')} ${formatLatency(sb.totalCpuTimeMs)}`);
99
- console.log(
100
- ` ${tui.muted('Memory:')} ${tui.formatBytes(sb.totalMemoryByteSec)}`
101
- );
91
+ console.log(` ${tui.muted('Memory:')} ${tui.formatBytes(sb.totalMemoryByteSec)}`);
102
92
  console.log(
103
93
  ` ${tui.muted('Network Out:')} ${tui.formatBytes(sb.totalNetworkEgressBytes)}`
104
94
  );
@@ -109,12 +99,8 @@ function displayServiceStats(data: ServiceStatsData): void {
109
99
  const em = services.email;
110
100
  tui.newline();
111
101
  console.log(tui.colorPrimary('Email:'));
112
- console.log(
113
- ` ${tui.muted('Addresses:')} ${formatNumber(em.addressCount)}`
114
- );
115
- console.log(
116
- ` ${tui.muted('Inbound:')} ${formatNumber(em.inboundCount)}`
117
- );
102
+ console.log(` ${tui.muted('Addresses:')} ${formatNumber(em.addressCount)}`);
103
+ console.log(` ${tui.muted('Inbound:')} ${formatNumber(em.inboundCount)}`);
118
104
  console.log(
119
105
  ` ${tui.muted('Outbound:')} ${formatNumber(em.outboundCount)} (${em.outboundSuccess} ok, ${em.outboundFailed} failed)`
120
106
  );
@@ -127,9 +113,7 @@ function displayServiceStats(data: ServiceStatsData): void {
127
113
  console.log(tui.colorPrimary('Task:'));
128
114
  console.log(` ${tui.muted('Total:')} ${formatNumber(tk.total)}`);
129
115
  console.log(` ${tui.muted('Open:')} ${formatNumber(tk.open)}`);
130
- console.log(
131
- ` ${tui.muted('In Progress:')} ${formatNumber(tk.inProgress)}`
132
- );
116
+ console.log(` ${tui.muted('In Progress:')} ${formatNumber(tk.inProgress)}`);
133
117
  console.log(` ${tui.muted('Closed:')} ${formatNumber(tk.closed)}`);
134
118
  }
135
119
 
@@ -138,9 +122,7 @@ function displayServiceStats(data: ServiceStatsData): void {
138
122
  const sc = services.schedule;
139
123
  tui.newline();
140
124
  console.log(tui.colorPrimary('Schedule:'));
141
- console.log(
142
- ` ${tui.muted('Schedules:')} ${formatNumber(sc.scheduleCount)}`
143
- );
125
+ console.log(` ${tui.muted('Schedules:')} ${formatNumber(sc.scheduleCount)}`);
144
126
  console.log(
145
127
  ` ${tui.muted('Deliveries:')} ${formatNumber(sc.totalDeliveries)} (${sc.successDeliveries} ok, ${sc.failedDeliveries} failed)`
146
128
  );
@@ -151,18 +133,10 @@ function displayServiceStats(data: ServiceStatsData): void {
151
133
  const db = services.database;
152
134
  tui.newline();
153
135
  console.log(tui.colorPrimary('Database:'));
154
- console.log(
155
- ` ${tui.muted('Databases:')} ${formatNumber(db.databaseCount)}`
156
- );
157
- console.log(
158
- ` ${tui.muted('Tables:')} ${formatNumber(db.totalTableCount)}`
159
- );
160
- console.log(
161
- ` ${tui.muted('Records:')} ${formatNumber(db.totalRecordCount)}`
162
- );
163
- console.log(
164
- ` ${tui.muted('Total Size:')} ${tui.formatBytes(db.totalSizeBytes)}`
165
- );
136
+ console.log(` ${tui.muted('Databases:')} ${formatNumber(db.databaseCount)}`);
137
+ console.log(` ${tui.muted('Tables:')} ${formatNumber(db.totalTableCount)}`);
138
+ console.log(` ${tui.muted('Records:')} ${formatNumber(db.totalRecordCount)}`);
139
+ console.log(` ${tui.muted('Total Size:')} ${tui.formatBytes(db.totalSizeBytes)}`);
166
140
  }
167
141
 
168
142
  if (!hasData) {
@@ -23,7 +23,13 @@ export const streamCommand = createCommand({
23
23
  { command: getCommand('cloud stream list'), description: 'List all streams' },
24
24
  { command: getCommand('cloud stream get <id>'), description: 'Get stream details' },
25
25
  ],
26
- subcommands: [createSubcommand, listSubcommand, getSubcommand, deleteSubcommand, statsSubcommand],
26
+ subcommands: [
27
+ createSubcommand,
28
+ listSubcommand,
29
+ getSubcommand,
30
+ deleteSubcommand,
31
+ statsSubcommand,
32
+ ],
27
33
  });
28
34
 
29
35
  export default streamCommand;
@@ -19,12 +19,8 @@ function displayStats(data: ServiceStatsData): void {
19
19
  }
20
20
  tui.header('Stream Statistics');
21
21
  tui.newline();
22
- console.log(
23
- ` ${tui.muted('Streams:')} ${formatNumber(svc.streamCount)}`
24
- );
25
- console.log(
26
- ` ${tui.muted('Total Size:')} ${tui.formatBytes(svc.totalSizeBytes)}`
27
- );
22
+ console.log(` ${tui.muted('Streams:')} ${formatNumber(svc.streamCount)}`);
23
+ console.log(` ${tui.muted('Total Size:')} ${tui.formatBytes(svc.totalSizeBytes)}`);
28
24
  }
29
25
 
30
26
  export const statsSubcommand = createCommand({
@@ -0,0 +1,432 @@
1
+ import { basename, join } from 'path';
2
+ import { stat as fsStat } from 'node:fs/promises';
3
+ import { z } from 'zod';
4
+ import { createCommand } from '../../../types';
5
+ import * as tui from '../../../tui';
6
+ import { createStorageAdapter } from './util';
7
+ import { getCommand } from '../../../command-prefix';
8
+ import type { Attachment } from '@agentuity/core';
9
+
10
+ function formatBytes(bytes: number | undefined): string {
11
+ if (bytes === undefined || bytes === null) return '—';
12
+ if (bytes < 1024) return `${bytes} B`;
13
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
14
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
15
+ }
16
+
17
+ function truncate(s: string, max: number): string {
18
+ if (s.length <= max) return s;
19
+ return `${s.slice(0, max - 1)}…`;
20
+ }
21
+
22
+ // ── Upload ──────────────────────────────────────────────────────────────
23
+
24
+ const uploadSubcommand = createCommand({
25
+ name: 'upload',
26
+ aliases: ['up', 'put'],
27
+ description: 'Upload a file attachment to a task',
28
+ tags: ['mutating', 'slow', 'requires-auth'],
29
+ requires: { auth: true },
30
+ examples: [
31
+ {
32
+ command: getCommand('cloud task attachment upload task_abc123 ./report.pdf'),
33
+ description: 'Upload a file to a task',
34
+ },
35
+ ],
36
+ schema: {
37
+ args: z.object({
38
+ taskId: z.string().min(1).describe('the task ID to attach the file to'),
39
+ file: z.string().min(1).describe('local file path to upload'),
40
+ }),
41
+ response: z.object({
42
+ success: z.boolean().describe('Whether the operation succeeded'),
43
+ attachment: z.object({
44
+ id: z.string().describe('Attachment ID'),
45
+ filename: z.string().describe('Filename'),
46
+ content_type: z.string().optional().describe('Content type'),
47
+ size: z.number().optional().describe('File size in bytes'),
48
+ }),
49
+ durationMs: z.number().describe('Operation duration in milliseconds'),
50
+ }),
51
+ },
52
+
53
+ async handler(ctx) {
54
+ const { args, options } = ctx;
55
+ const started = Date.now();
56
+ const storage = await createStorageAdapter(ctx);
57
+
58
+ const file = Bun.file(args.file);
59
+ if (!(await file.exists())) {
60
+ tui.fatal(`File not found: ${args.file}`);
61
+ }
62
+
63
+ const filename = basename(args.file);
64
+ const contentType = file.type || 'application/octet-stream';
65
+ const size = file.size;
66
+
67
+ // Step 1: Get presigned upload URL
68
+ const presign = await tui.spinner({
69
+ message: 'Requesting upload URL',
70
+ clearOnSuccess: true,
71
+ callback: async () => {
72
+ return storage.uploadAttachment(args.taskId, {
73
+ filename,
74
+ content_type: contentType,
75
+ size,
76
+ });
77
+ },
78
+ });
79
+
80
+ // Step 2: Upload file to presigned URL
81
+ await tui.spinner({
82
+ message: `Uploading ${filename}`,
83
+ clearOnSuccess: true,
84
+ callback: async () => {
85
+ const response = await fetch(presign.presigned_url, {
86
+ method: 'PUT',
87
+ body: file.stream(),
88
+ headers: {
89
+ 'Content-Type': contentType,
90
+ },
91
+ duplex: 'half',
92
+ });
93
+ if (!response.ok) {
94
+ tui.fatal(`Upload failed: ${response.statusText}`);
95
+ }
96
+ },
97
+ });
98
+
99
+ // Step 3: Confirm the upload
100
+ const attachment = await tui.spinner({
101
+ message: 'Confirming upload',
102
+ clearOnSuccess: true,
103
+ callback: async () => {
104
+ return storage.confirmAttachment(presign.attachment.id);
105
+ },
106
+ });
107
+
108
+ const durationMs = Date.now() - started;
109
+
110
+ if (!options.json) {
111
+ tui.success(`Attachment uploaded: ${tui.bold(attachment.id)}`);
112
+
113
+ const tableData: Record<string, string> = {
114
+ ID: attachment.id,
115
+ Filename: attachment.filename,
116
+ 'Content Type': attachment.content_type ?? '—',
117
+ Size: formatBytes(attachment.size),
118
+ Task: attachment.task_id,
119
+ Created: new Date(attachment.created_at).toLocaleString(),
120
+ };
121
+
122
+ tui.table([tableData], Object.keys(tableData), { layout: 'vertical', padStart: ' ' });
123
+ }
124
+
125
+ return {
126
+ success: true,
127
+ attachment: {
128
+ id: attachment.id,
129
+ filename: attachment.filename,
130
+ content_type: attachment.content_type,
131
+ size: attachment.size,
132
+ },
133
+ durationMs,
134
+ };
135
+ },
136
+ });
137
+
138
+ // ── List ────────────────────────────────────────────────────────────────
139
+
140
+ const listAttachmentsSubcommand = createCommand({
141
+ name: 'list',
142
+ aliases: ['ls'],
143
+ description: 'List attachments for a task',
144
+ tags: ['read-only', 'slow', 'requires-auth'],
145
+ idempotent: true,
146
+ requires: { auth: true },
147
+ examples: [
148
+ {
149
+ command: getCommand('cloud task attachment list task_abc123'),
150
+ description: 'List all attachments for a task',
151
+ },
152
+ ],
153
+ schema: {
154
+ args: z.object({
155
+ taskId: z.string().min(1).describe('the task ID to list attachments for'),
156
+ }),
157
+ response: z.object({
158
+ success: z.boolean().describe('Whether the operation succeeded'),
159
+ attachments: z.array(
160
+ z.object({
161
+ id: z.string(),
162
+ filename: z.string(),
163
+ content_type: z.string().optional(),
164
+ size: z.number().optional(),
165
+ created_at: z.string(),
166
+ })
167
+ ),
168
+ total: z.number().describe('Total number of attachments'),
169
+ durationMs: z.number().describe('Operation duration in milliseconds'),
170
+ }),
171
+ },
172
+
173
+ async handler(ctx) {
174
+ const { args, options } = ctx;
175
+ const started = Date.now();
176
+ const storage = await createStorageAdapter(ctx);
177
+
178
+ const result = await storage.listAttachments(args.taskId);
179
+ const durationMs = Date.now() - started;
180
+
181
+ if (!options.json) {
182
+ if (result.attachments.length === 0) {
183
+ tui.info('No attachments found');
184
+ } else {
185
+ const tableData = result.attachments.map((att: Attachment) => ({
186
+ ID: tui.muted(truncate(att.id, 28)),
187
+ Filename: truncate(att.filename, 40),
188
+ 'Content Type': att.content_type ?? tui.muted('—'),
189
+ Size: formatBytes(att.size),
190
+ Created: new Date(att.created_at).toLocaleDateString(),
191
+ }));
192
+
193
+ tui.table(tableData, [
194
+ { name: 'ID', alignment: 'left' },
195
+ { name: 'Filename', alignment: 'left' },
196
+ { name: 'Content Type', alignment: 'left' },
197
+ { name: 'Size', alignment: 'right' },
198
+ { name: 'Created', alignment: 'left' },
199
+ ]);
200
+
201
+ tui.info(
202
+ `${result.total} ${tui.plural(result.total, 'attachment', 'attachments')} (${durationMs.toFixed(1)}ms)`
203
+ );
204
+ }
205
+ }
206
+
207
+ return {
208
+ success: true,
209
+ attachments: result.attachments.map((att: Attachment) => ({
210
+ id: att.id,
211
+ filename: att.filename,
212
+ content_type: att.content_type,
213
+ size: att.size,
214
+ created_at: att.created_at,
215
+ })),
216
+ total: result.total,
217
+ durationMs,
218
+ };
219
+ },
220
+ });
221
+
222
+ // ── Download ────────────────────────────────────────────────────────────
223
+
224
+ const downloadSubcommand = createCommand({
225
+ name: 'download',
226
+ aliases: ['dl', 'get'],
227
+ description: 'Download a task attachment',
228
+ tags: ['read-only', 'slow', 'requires-auth'],
229
+ requires: { auth: true },
230
+ examples: [
231
+ {
232
+ command: getCommand('cloud task attachment download att_abc123'),
233
+ description: 'Download an attachment to the current directory',
234
+ },
235
+ {
236
+ command: getCommand('cloud task attachment download att_abc123 --output ./downloads/'),
237
+ description: 'Download an attachment to a specific directory',
238
+ },
239
+ ],
240
+ schema: {
241
+ args: z.object({
242
+ attachmentId: z.string().min(1).describe('the attachment ID to download'),
243
+ }),
244
+ options: z.object({
245
+ output: z
246
+ .string()
247
+ .optional()
248
+ .describe('output file path or directory (defaults to current directory)'),
249
+ }),
250
+ response: z.object({
251
+ success: z.boolean().describe('Whether the operation succeeded'),
252
+ path: z.string().describe('Path where the file was saved'),
253
+ size: z.number().describe('Downloaded file size in bytes'),
254
+ durationMs: z.number().describe('Operation duration in milliseconds'),
255
+ }),
256
+ },
257
+
258
+ async handler(ctx) {
259
+ const { args, opts, options } = ctx;
260
+ const started = Date.now();
261
+ const storage = await createStorageAdapter(ctx);
262
+
263
+ // Step 1: Get presigned download URL
264
+ const presign = await tui.spinner({
265
+ message: 'Requesting download URL',
266
+ clearOnSuccess: true,
267
+ callback: async () => {
268
+ return storage.downloadAttachment(args.attachmentId);
269
+ },
270
+ });
271
+
272
+ // Step 2: Download the file
273
+ const response = await tui.spinner({
274
+ message: 'Downloading',
275
+ clearOnSuccess: true,
276
+ callback: async () => {
277
+ const res = await fetch(presign.presigned_url);
278
+ if (!res.ok) {
279
+ tui.fatal(`Download failed: ${res.statusText}`);
280
+ }
281
+ return res;
282
+ },
283
+ });
284
+
285
+ // Determine output path
286
+ // Extract filename from Content-Disposition header or URL
287
+ let filename = 'attachment';
288
+ const disposition = response.headers.get('content-disposition');
289
+ if (disposition) {
290
+ const match = disposition.match(/filename[*]?=(?:UTF-8''|"?)([^";]+)/i);
291
+ if (match?.[1]) {
292
+ filename = decodeURIComponent(match[1].replace(/"/g, ''));
293
+ }
294
+ } else {
295
+ // Try to extract filename from the presigned URL path
296
+ const urlPath = new URL(presign.presigned_url).pathname;
297
+ const urlFilename = basename(urlPath);
298
+ if (urlFilename && urlFilename !== '/') {
299
+ filename = decodeURIComponent(urlFilename);
300
+ }
301
+ }
302
+
303
+ // Sanitize filename against path traversal
304
+ filename = filename.replace(/\0/g, ''); // strip null bytes
305
+ filename = filename.replace(/[/\\]/g, '_'); // replace path separators
306
+ filename = filename.replace(/^\.+/, ''); // strip leading dots
307
+ if (!filename || filename === '.' || filename === '..') {
308
+ filename = 'attachment';
309
+ }
310
+
311
+ let outputPath: string;
312
+ if (opts.output) {
313
+ try {
314
+ const stats = await fsStat(opts.output);
315
+ if (stats.isDirectory()) {
316
+ outputPath = join(opts.output, filename);
317
+ } else {
318
+ // It's an existing file — use it directly
319
+ outputPath = opts.output;
320
+ }
321
+ } catch {
322
+ // Path doesn't exist — treat as target file path
323
+ outputPath = opts.output;
324
+ }
325
+ } else {
326
+ outputPath = join(process.cwd(), filename);
327
+ }
328
+
329
+ // Step 3: Write file to disk
330
+ const size = await tui.spinner({
331
+ message: `Saving to ${outputPath}`,
332
+ clearOnSuccess: true,
333
+ callback: async () => {
334
+ const bytes = await Bun.write(outputPath, response);
335
+ return bytes;
336
+ },
337
+ });
338
+
339
+ const durationMs = Date.now() - started;
340
+
341
+ if (!options.json) {
342
+ tui.success(`Downloaded to ${tui.bold(outputPath)} (${formatBytes(size)})`);
343
+ }
344
+
345
+ return {
346
+ success: true,
347
+ path: outputPath,
348
+ size,
349
+ durationMs,
350
+ };
351
+ },
352
+ });
353
+
354
+ // ── Delete ──────────────────────────────────────────────────────────────
355
+
356
+ const deleteAttachmentSubcommand = createCommand({
357
+ name: 'delete',
358
+ aliases: ['rm', 'remove'],
359
+ description: 'Delete a task attachment',
360
+ tags: ['mutating', 'slow', 'requires-auth'],
361
+ requires: { auth: true },
362
+ examples: [
363
+ {
364
+ command: getCommand('cloud task attachment delete att_abc123'),
365
+ description: 'Delete an attachment',
366
+ },
367
+ ],
368
+ schema: {
369
+ args: z.object({
370
+ attachmentId: z.string().min(1).describe('the attachment ID to delete'),
371
+ }),
372
+ response: z.object({
373
+ success: z.boolean().describe('Whether the operation succeeded'),
374
+ attachmentId: z.string().describe('Deleted attachment ID'),
375
+ durationMs: z.number().describe('Operation duration in milliseconds'),
376
+ }),
377
+ },
378
+
379
+ async handler(ctx) {
380
+ const { args, options } = ctx;
381
+ const started = Date.now();
382
+ const storage = await createStorageAdapter(ctx);
383
+
384
+ await storage.deleteAttachment(args.attachmentId);
385
+
386
+ const durationMs = Date.now() - started;
387
+
388
+ if (!options.json) {
389
+ tui.success(`Attachment deleted: ${tui.bold(args.attachmentId)}`);
390
+ }
391
+
392
+ return {
393
+ success: true,
394
+ attachmentId: args.attachmentId,
395
+ durationMs,
396
+ };
397
+ },
398
+ });
399
+
400
+ // ── Parent command ──────────────────────────────────────────────────────
401
+
402
+ export const attachmentSubcommand = createCommand({
403
+ name: 'attachment',
404
+ aliases: ['attach', 'att'],
405
+ description: 'Manage task attachments',
406
+ tags: ['requires-auth'],
407
+ requires: { auth: true },
408
+ examples: [
409
+ {
410
+ command: getCommand('cloud task attachment upload task_abc123 ./report.pdf'),
411
+ description: 'Upload a file to a task',
412
+ },
413
+ {
414
+ command: getCommand('cloud task attachment list task_abc123'),
415
+ description: 'List task attachments',
416
+ },
417
+ {
418
+ command: getCommand('cloud task attachment download att_abc123'),
419
+ description: 'Download an attachment',
420
+ },
421
+ {
422
+ command: getCommand('cloud task attachment delete att_abc123'),
423
+ description: 'Delete an attachment',
424
+ },
425
+ ],
426
+ subcommands: [
427
+ uploadSubcommand,
428
+ listAttachmentsSubcommand,
429
+ downloadSubcommand,
430
+ deleteAttachmentSubcommand,
431
+ ],
432
+ });