@agentuity/cli 1.0.24 → 1.0.25

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 (147) 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 +1 -4
  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 +1 -3
  30. package/dist/cmd/cloud/email/get.js.map +1 -1
  31. package/dist/cmd/cloud/email/send.d.ts.map +1 -1
  32. package/dist/cmd/cloud/email/send.js +1 -5
  33. package/dist/cmd/cloud/email/send.js.map +1 -1
  34. package/dist/cmd/cloud/email/stats.d.ts.map +1 -1
  35. package/dist/cmd/cloud/email/stats.js.map +1 -1
  36. package/dist/cmd/cloud/email/util.d.ts.map +1 -1
  37. package/dist/cmd/cloud/email/util.js +1 -2
  38. package/dist/cmd/cloud/email/util.js.map +1 -1
  39. package/dist/cmd/cloud/sandbox/snapshot/build.d.ts.map +1 -1
  40. package/dist/cmd/cloud/sandbox/snapshot/build.js +4 -1
  41. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  42. package/dist/cmd/cloud/sandbox/stats.d.ts.map +1 -1
  43. package/dist/cmd/cloud/sandbox/stats.js.map +1 -1
  44. package/dist/cmd/cloud/schedule/delete.d.ts.map +1 -1
  45. package/dist/cmd/cloud/schedule/delete.js +4 -1
  46. package/dist/cmd/cloud/schedule/delete.js.map +1 -1
  47. package/dist/cmd/cloud/schedule/delivery/list.d.ts.map +1 -1
  48. package/dist/cmd/cloud/schedule/delivery/list.js.map +1 -1
  49. package/dist/cmd/cloud/schedule/destination/create.d.ts.map +1 -1
  50. package/dist/cmd/cloud/schedule/destination/create.js +3 -1
  51. package/dist/cmd/cloud/schedule/destination/create.js.map +1 -1
  52. package/dist/cmd/cloud/schedule/destination/index.d.ts.map +1 -1
  53. package/dist/cmd/cloud/schedule/destination/index.js.map +1 -1
  54. package/dist/cmd/cloud/schedule/destination/list.d.ts.map +1 -1
  55. package/dist/cmd/cloud/schedule/destination/list.js.map +1 -1
  56. package/dist/cmd/cloud/schedule/get.d.ts.map +1 -1
  57. package/dist/cmd/cloud/schedule/get.js +4 -1
  58. package/dist/cmd/cloud/schedule/get.js.map +1 -1
  59. package/dist/cmd/cloud/schedule/index.d.ts.map +1 -1
  60. package/dist/cmd/cloud/schedule/index.js +4 -1
  61. package/dist/cmd/cloud/schedule/index.js.map +1 -1
  62. package/dist/cmd/cloud/schedule/list.d.ts.map +1 -1
  63. package/dist/cmd/cloud/schedule/list.js.map +1 -1
  64. package/dist/cmd/cloud/schedule/stats.d.ts.map +1 -1
  65. package/dist/cmd/cloud/schedule/stats.js.map +1 -1
  66. package/dist/cmd/cloud/schedule/util.d.ts.map +1 -1
  67. package/dist/cmd/cloud/schedule/util.js +1 -2
  68. package/dist/cmd/cloud/schedule/util.js.map +1 -1
  69. package/dist/cmd/cloud/services/stats.d.ts.map +1 -1
  70. package/dist/cmd/cloud/services/stats.js.map +1 -1
  71. package/dist/cmd/cloud/stream/index.d.ts.map +1 -1
  72. package/dist/cmd/cloud/stream/index.js +7 -1
  73. package/dist/cmd/cloud/stream/index.js.map +1 -1
  74. package/dist/cmd/cloud/stream/stats.d.ts.map +1 -1
  75. package/dist/cmd/cloud/stream/stats.js.map +1 -1
  76. package/dist/cmd/cloud/task/attachment.d.ts +2 -0
  77. package/dist/cmd/cloud/task/attachment.d.ts.map +1 -0
  78. package/dist/cmd/cloud/task/attachment.js +393 -0
  79. package/dist/cmd/cloud/task/attachment.js.map +1 -0
  80. package/dist/cmd/cloud/task/create.d.ts.map +1 -1
  81. package/dist/cmd/cloud/task/create.js +126 -5
  82. package/dist/cmd/cloud/task/create.js.map +1 -1
  83. package/dist/cmd/cloud/task/get.d.ts.map +1 -1
  84. package/dist/cmd/cloud/task/get.js +29 -11
  85. package/dist/cmd/cloud/task/get.js.map +1 -1
  86. package/dist/cmd/cloud/task/index.d.ts.map +1 -1
  87. package/dist/cmd/cloud/task/index.js +13 -1
  88. package/dist/cmd/cloud/task/index.js.map +1 -1
  89. package/dist/cmd/cloud/task/list.d.ts.map +1 -1
  90. package/dist/cmd/cloud/task/list.js +31 -15
  91. package/dist/cmd/cloud/task/list.js.map +1 -1
  92. package/dist/cmd/cloud/task/stats.js +2 -0
  93. package/dist/cmd/cloud/task/stats.js.map +1 -1
  94. package/dist/cmd/cloud/task/util.d.ts.map +1 -1
  95. package/dist/cmd/cloud/task/util.js +2 -4
  96. package/dist/cmd/cloud/task/util.js.map +1 -1
  97. package/dist/cmd/cloud/webhook/create.d.ts.map +1 -1
  98. package/dist/cmd/cloud/webhook/create.js +6 -1
  99. package/dist/cmd/cloud/webhook/create.js.map +1 -1
  100. package/dist/cmd/cloud/webhook/deliveries.d.ts.map +1 -1
  101. package/dist/cmd/cloud/webhook/deliveries.js +4 -1
  102. package/dist/cmd/cloud/webhook/deliveries.js.map +1 -1
  103. package/dist/cmd/cloud/webhook/destinations.d.ts.map +1 -1
  104. package/dist/cmd/cloud/webhook/destinations.js +4 -5
  105. package/dist/cmd/cloud/webhook/destinations.js.map +1 -1
  106. package/dist/cmd/dev/index.d.ts.map +1 -1
  107. package/dist/cmd/dev/index.js +80 -34
  108. package/dist/cmd/dev/index.js.map +1 -1
  109. package/package.json +6 -6
  110. package/src/cache/index.ts +2 -0
  111. package/src/cache/user-cache.ts +93 -0
  112. package/src/cmd/auth/logout.ts +3 -1
  113. package/src/cmd/build/entry-generator.ts +34 -4
  114. package/src/cmd/build/vite/bun-dev-server.ts +21 -9
  115. package/src/cmd/cloud/db/stats.ts +4 -12
  116. package/src/cmd/cloud/email/create.ts +1 -4
  117. package/src/cmd/cloud/email/destination/delete.ts +5 -1
  118. package/src/cmd/cloud/email/get.ts +1 -3
  119. package/src/cmd/cloud/email/send.ts +1 -5
  120. package/src/cmd/cloud/email/stats.ts +2 -6
  121. package/src/cmd/cloud/email/util.ts +1 -2
  122. package/src/cmd/cloud/sandbox/snapshot/build.ts +25 -6
  123. package/src/cmd/cloud/sandbox/stats.ts +2 -6
  124. package/src/cmd/cloud/schedule/delete.ts +4 -1
  125. package/src/cmd/cloud/schedule/delivery/list.ts +15 -13
  126. package/src/cmd/cloud/schedule/destination/create.ts +11 -3
  127. package/src/cmd/cloud/schedule/destination/index.ts +3 -1
  128. package/src/cmd/cloud/schedule/destination/list.ts +19 -17
  129. package/src/cmd/cloud/schedule/get.ts +25 -20
  130. package/src/cmd/cloud/schedule/index.ts +4 -1
  131. package/src/cmd/cloud/schedule/list.ts +18 -16
  132. package/src/cmd/cloud/schedule/stats.ts +1 -3
  133. package/src/cmd/cloud/schedule/util.ts +1 -2
  134. package/src/cmd/cloud/services/stats.ts +13 -39
  135. package/src/cmd/cloud/stream/index.ts +7 -1
  136. package/src/cmd/cloud/stream/stats.ts +2 -6
  137. package/src/cmd/cloud/task/attachment.ts +432 -0
  138. package/src/cmd/cloud/task/create.ts +131 -5
  139. package/src/cmd/cloud/task/get.ts +30 -12
  140. package/src/cmd/cloud/task/index.ts +13 -1
  141. package/src/cmd/cloud/task/list.ts +31 -15
  142. package/src/cmd/cloud/task/stats.ts +3 -3
  143. package/src/cmd/cloud/task/util.ts +2 -4
  144. package/src/cmd/cloud/webhook/create.ts +6 -1
  145. package/src/cmd/cloud/webhook/deliveries.ts +4 -5
  146. package/src/cmd/cloud/webhook/destinations.ts +4 -5
  147. package/src/cmd/dev/index.ts +91 -48
@@ -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
+ });
@@ -1,9 +1,13 @@
1
+ import { basename, join } from 'path';
1
2
  import { z } from 'zod';
2
3
  import { createCommand } from '../../../types';
3
4
  import * as tui from '../../../tui';
4
5
  import { createStorageAdapter, parseMetadataFlag, cacheTaskId } from './util';
5
6
  import { getCommand } from '../../../command-prefix';
7
+ import { whoami } from '@agentuity/server';
6
8
  import type { TaskPriority, TaskStatus, TaskType } from '@agentuity/core';
9
+ import { getCachedUserInfo, setCachedUserInfo } from '../../../cache';
10
+ import { defaultProfileName } from '../../../config';
7
11
 
8
12
  const TaskCreateResponseSchema = z.object({
9
13
  success: z.boolean().describe('Whether the operation succeeded'),
@@ -15,6 +19,13 @@ const TaskCreateResponseSchema = z.object({
15
19
  priority: z.string().describe('Task priority'),
16
20
  created_at: z.string().describe('Creation timestamp'),
17
21
  }),
22
+ attachment: z
23
+ .object({
24
+ id: z.string().describe('Attachment ID'),
25
+ filename: z.string().describe('Attached filename'),
26
+ })
27
+ .optional()
28
+ .describe('Attached file info (when --file is used)'),
18
29
  durationMs: z.number().describe('Operation duration in milliseconds'),
19
30
  });
20
31
 
@@ -23,7 +34,8 @@ export const createSubcommand = createCommand({
23
34
  aliases: ['new', 'add'],
24
35
  description: 'Create a new task',
25
36
  tags: ['mutating', 'slow', 'requires-auth'],
26
- requires: { auth: true },
37
+ requires: { auth: true, apiClient: true },
38
+ optional: { project: true },
27
39
  examples: [
28
40
  {
29
41
  command: getCommand('cloud task create "Fix login bug" --type bug --created-id agent_001'),
@@ -48,19 +60,34 @@ export const createSubcommand = createCommand({
48
60
  }),
49
61
  options: z.object({
50
62
  type: z.enum(['epic', 'feature', 'enhancement', 'bug', 'task']).describe('the task type'),
51
- createdId: z.string().min(1).describe('the ID of the creator (agent or user)'),
63
+ createdId: z
64
+ .string()
65
+ .min(1)
66
+ .optional()
67
+ .describe('the ID of the creator (agent or user, defaults to authenticated user)'),
68
+ createdName: z
69
+ .string()
70
+ .min(1)
71
+ .optional()
72
+ .describe('the display name of the creator (used with --created-id)'),
73
+ projectId: z.string().optional().describe('project ID to associate with the task'),
74
+ projectName: z
75
+ .string()
76
+ .optional()
77
+ .describe('project display name (used with --project-id)'),
52
78
  description: z.string().optional().describe('task description'),
53
79
  priority: z
54
80
  .enum(['high', 'medium', 'low', 'none'])
55
81
  .optional()
56
82
  .describe('task priority (default: none)'),
57
83
  status: z
58
- .enum(['open', 'in_progress', 'closed'])
84
+ .enum(['open', 'in_progress', 'closed', 'done', 'cancelled'])
59
85
  .optional()
60
86
  .describe('initial task status (default: open)'),
61
87
  parentId: z.string().optional().describe('parent task ID for subtasks'),
62
88
  assignedId: z.string().optional().describe('ID of the assigned agent or user'),
63
89
  metadata: z.string().optional().describe('JSON metadata object'),
90
+ file: z.string().optional().describe('file path to attach to the task'),
64
91
  }),
65
92
  response: TaskCreateResponseSchema,
66
93
  },
@@ -72,10 +99,65 @@ export const createSubcommand = createCommand({
72
99
 
73
100
  const metadata = parseMetadataFlag(opts.metadata);
74
101
 
102
+ // Resolve creator info
103
+ const createdId = opts.createdId ?? ctx.auth.userId;
104
+ let creator: { id: string; name: string } | undefined;
105
+ if (opts.createdId && opts.createdName) {
106
+ // Explicit creator with name
107
+ creator = { id: opts.createdId, name: opts.createdName };
108
+ } else if (!opts.createdId) {
109
+ // Using auth userId — check cache first, then fall back to whoami API call
110
+ const profileName = ctx.config?.name ?? defaultProfileName;
111
+ const cached = getCachedUserInfo(profileName);
112
+ if (cached) {
113
+ const name = [cached.firstName, cached.lastName].filter(Boolean).join(' ');
114
+ if (name) {
115
+ creator = { id: createdId, name };
116
+ }
117
+ } else {
118
+ // Fetch from API and cache
119
+ try {
120
+ const user = await whoami(ctx.apiClient);
121
+ const name = [user.firstName, user.lastName].filter(Boolean).join(' ');
122
+ if (name) {
123
+ creator = { id: createdId, name };
124
+ }
125
+ setCachedUserInfo(profileName, createdId, user.firstName, user.lastName);
126
+ } catch {
127
+ // Fall back to no creator EntityRef — task DB will use id as name
128
+ }
129
+ }
130
+ }
131
+
132
+ // Resolve project info
133
+ let project: { id: string; name: string } | undefined;
134
+ if (opts.projectId) {
135
+ // Explicit project via flags
136
+ project = { id: opts.projectId, name: opts.projectName ?? opts.projectId };
137
+ } else if (ctx.project?.projectId) {
138
+ // Auto-detect from project context — read name from package.json
139
+ let projectName = ctx.project.projectId;
140
+ try {
141
+ const pkgPath = join(ctx.projectDir, 'package.json');
142
+ const pkgFile = Bun.file(pkgPath);
143
+ if (await pkgFile.exists()) {
144
+ const pkg = await pkgFile.json();
145
+ if (pkg.name) {
146
+ projectName = pkg.name;
147
+ }
148
+ }
149
+ } catch {
150
+ // Fall back to projectId as name
151
+ }
152
+ project = { id: ctx.project.projectId, name: projectName };
153
+ }
154
+
75
155
  const task = await storage.create({
76
156
  title: args.title,
77
157
  type: opts.type as TaskType,
78
- created_id: opts.createdId,
158
+ created_id: createdId,
159
+ creator,
160
+ project,
79
161
  description: opts.description,
80
162
  priority: opts.priority as TaskPriority,
81
163
  status: opts.status as TaskStatus,
@@ -84,9 +166,44 @@ export const createSubcommand = createCommand({
84
166
  metadata,
85
167
  });
86
168
 
87
- const durationMs = Date.now() - started;
88
169
  await cacheTaskId(ctx, task.id);
89
170
 
171
+ // Handle --file attachment
172
+ let attachmentInfo: { id: string; filename: string } | undefined;
173
+ if (opts.file) {
174
+ const file = Bun.file(opts.file);
175
+ if (!(await file.exists())) {
176
+ tui.fatal(`File not found: ${opts.file}`);
177
+ }
178
+
179
+ const filename = basename(opts.file);
180
+ const contentType = file.type || 'application/octet-stream';
181
+ const size = file.size;
182
+
183
+ const presign = await storage.uploadAttachment(task.id, {
184
+ filename,
185
+ content_type: contentType,
186
+ size,
187
+ });
188
+
189
+ const uploadResponse = await fetch(presign.presigned_url, {
190
+ method: 'PUT',
191
+ body: file.stream(),
192
+ headers: {
193
+ 'Content-Type': contentType,
194
+ },
195
+ duplex: 'half',
196
+ });
197
+ if (!uploadResponse.ok) {
198
+ tui.fatal(`Attachment upload failed: ${uploadResponse.statusText}`);
199
+ }
200
+
201
+ const attachment = await storage.confirmAttachment(presign.attachment.id);
202
+ attachmentInfo = { id: attachment.id, filename: attachment.filename };
203
+ }
204
+
205
+ const durationMs = Date.now() - started;
206
+
90
207
  if (!options.json) {
91
208
  tui.success(`Task created: ${tui.bold(task.id)}`);
92
209
 
@@ -102,6 +219,14 @@ export const createSubcommand = createCommand({
102
219
  tableData['Description'] = task.description;
103
220
  }
104
221
 
222
+ if (project) {
223
+ tableData['Project'] = project.name;
224
+ }
225
+
226
+ if (attachmentInfo) {
227
+ tableData['Attachment'] = `${attachmentInfo.filename} (${attachmentInfo.id})`;
228
+ }
229
+
105
230
  tui.table([tableData], Object.keys(tableData), { layout: 'vertical', padStart: ' ' });
106
231
  }
107
232
 
@@ -115,6 +240,7 @@ export const createSubcommand = createCommand({
115
240
  priority: task.priority,
116
241
  created_at: task.created_at,
117
242
  },
243
+ ...(attachmentInfo ? { attachment: attachmentInfo } : {}),
118
244
  durationMs,
119
245
  };
120
246
  },