@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.
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +1 -0
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/user-cache.d.ts +20 -0
- package/dist/cache/user-cache.d.ts.map +1 -0
- package/dist/cache/user-cache.js +79 -0
- package/dist/cache/user-cache.js.map +1 -0
- package/dist/cmd/auth/logout.d.ts.map +1 -1
- package/dist/cmd/auth/logout.js +3 -1
- package/dist/cmd/auth/logout.js.map +1 -1
- package/dist/cmd/build/entry-generator.d.ts +4 -0
- package/dist/cmd/build/entry-generator.d.ts.map +1 -1
- package/dist/cmd/build/entry-generator.js +18 -3
- package/dist/cmd/build/entry-generator.js.map +1 -1
- package/dist/cmd/build/vite/bun-dev-server.d.ts +1 -0
- package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
- package/dist/cmd/build/vite/bun-dev-server.js +11 -9
- package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
- package/dist/cmd/cloud/db/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/db/stats.js.map +1 -1
- package/dist/cmd/cloud/email/create.d.ts.map +1 -1
- package/dist/cmd/cloud/email/create.js +1 -4
- package/dist/cmd/cloud/email/create.js.map +1 -1
- package/dist/cmd/cloud/email/destination/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/email/destination/delete.js +5 -1
- package/dist/cmd/cloud/email/destination/delete.js.map +1 -1
- package/dist/cmd/cloud/email/get.d.ts.map +1 -1
- package/dist/cmd/cloud/email/get.js +1 -3
- package/dist/cmd/cloud/email/get.js.map +1 -1
- package/dist/cmd/cloud/email/send.d.ts.map +1 -1
- package/dist/cmd/cloud/email/send.js +1 -5
- package/dist/cmd/cloud/email/send.js.map +1 -1
- package/dist/cmd/cloud/email/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/email/stats.js.map +1 -1
- package/dist/cmd/cloud/email/util.d.ts.map +1 -1
- package/dist/cmd/cloud/email/util.js +1 -2
- package/dist/cmd/cloud/email/util.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/build.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/build.js +4 -1
- package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
- package/dist/cmd/cloud/sandbox/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/stats.js.map +1 -1
- package/dist/cmd/cloud/schedule/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/delete.js +4 -1
- package/dist/cmd/cloud/schedule/delete.js.map +1 -1
- package/dist/cmd/cloud/schedule/delivery/list.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/delivery/list.js.map +1 -1
- package/dist/cmd/cloud/schedule/destination/create.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/destination/create.js +3 -1
- package/dist/cmd/cloud/schedule/destination/create.js.map +1 -1
- package/dist/cmd/cloud/schedule/destination/index.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/destination/index.js.map +1 -1
- package/dist/cmd/cloud/schedule/destination/list.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/destination/list.js.map +1 -1
- package/dist/cmd/cloud/schedule/get.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/get.js +4 -1
- package/dist/cmd/cloud/schedule/get.js.map +1 -1
- package/dist/cmd/cloud/schedule/index.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/index.js +4 -1
- package/dist/cmd/cloud/schedule/index.js.map +1 -1
- package/dist/cmd/cloud/schedule/list.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/list.js.map +1 -1
- package/dist/cmd/cloud/schedule/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/stats.js.map +1 -1
- package/dist/cmd/cloud/schedule/util.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/util.js +1 -2
- package/dist/cmd/cloud/schedule/util.js.map +1 -1
- package/dist/cmd/cloud/services/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/services/stats.js.map +1 -1
- package/dist/cmd/cloud/stream/index.d.ts.map +1 -1
- package/dist/cmd/cloud/stream/index.js +7 -1
- package/dist/cmd/cloud/stream/index.js.map +1 -1
- package/dist/cmd/cloud/stream/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/stream/stats.js.map +1 -1
- package/dist/cmd/cloud/task/attachment.d.ts +2 -0
- package/dist/cmd/cloud/task/attachment.d.ts.map +1 -0
- package/dist/cmd/cloud/task/attachment.js +393 -0
- package/dist/cmd/cloud/task/attachment.js.map +1 -0
- package/dist/cmd/cloud/task/create.d.ts.map +1 -1
- package/dist/cmd/cloud/task/create.js +126 -5
- package/dist/cmd/cloud/task/create.js.map +1 -1
- package/dist/cmd/cloud/task/get.d.ts.map +1 -1
- package/dist/cmd/cloud/task/get.js +29 -11
- package/dist/cmd/cloud/task/get.js.map +1 -1
- package/dist/cmd/cloud/task/index.d.ts.map +1 -1
- package/dist/cmd/cloud/task/index.js +13 -1
- package/dist/cmd/cloud/task/index.js.map +1 -1
- package/dist/cmd/cloud/task/list.d.ts.map +1 -1
- package/dist/cmd/cloud/task/list.js +31 -15
- package/dist/cmd/cloud/task/list.js.map +1 -1
- package/dist/cmd/cloud/task/stats.js +2 -0
- package/dist/cmd/cloud/task/stats.js.map +1 -1
- package/dist/cmd/cloud/task/util.d.ts.map +1 -1
- package/dist/cmd/cloud/task/util.js +2 -4
- package/dist/cmd/cloud/task/util.js.map +1 -1
- package/dist/cmd/cloud/webhook/create.d.ts.map +1 -1
- package/dist/cmd/cloud/webhook/create.js +6 -1
- package/dist/cmd/cloud/webhook/create.js.map +1 -1
- package/dist/cmd/cloud/webhook/deliveries.d.ts.map +1 -1
- package/dist/cmd/cloud/webhook/deliveries.js +4 -1
- package/dist/cmd/cloud/webhook/deliveries.js.map +1 -1
- package/dist/cmd/cloud/webhook/destinations.d.ts.map +1 -1
- package/dist/cmd/cloud/webhook/destinations.js +4 -5
- package/dist/cmd/cloud/webhook/destinations.js.map +1 -1
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/index.js +80 -34
- package/dist/cmd/dev/index.js.map +1 -1
- package/package.json +6 -6
- package/src/cache/index.ts +2 -0
- package/src/cache/user-cache.ts +93 -0
- package/src/cmd/auth/logout.ts +3 -1
- package/src/cmd/build/entry-generator.ts +34 -4
- package/src/cmd/build/vite/bun-dev-server.ts +21 -9
- package/src/cmd/cloud/db/stats.ts +4 -12
- package/src/cmd/cloud/email/create.ts +1 -4
- package/src/cmd/cloud/email/destination/delete.ts +5 -1
- package/src/cmd/cloud/email/get.ts +1 -3
- package/src/cmd/cloud/email/send.ts +1 -5
- package/src/cmd/cloud/email/stats.ts +2 -6
- package/src/cmd/cloud/email/util.ts +1 -2
- package/src/cmd/cloud/sandbox/snapshot/build.ts +25 -6
- package/src/cmd/cloud/sandbox/stats.ts +2 -6
- package/src/cmd/cloud/schedule/delete.ts +4 -1
- package/src/cmd/cloud/schedule/delivery/list.ts +15 -13
- package/src/cmd/cloud/schedule/destination/create.ts +11 -3
- package/src/cmd/cloud/schedule/destination/index.ts +3 -1
- package/src/cmd/cloud/schedule/destination/list.ts +19 -17
- package/src/cmd/cloud/schedule/get.ts +25 -20
- package/src/cmd/cloud/schedule/index.ts +4 -1
- package/src/cmd/cloud/schedule/list.ts +18 -16
- package/src/cmd/cloud/schedule/stats.ts +1 -3
- package/src/cmd/cloud/schedule/util.ts +1 -2
- package/src/cmd/cloud/services/stats.ts +13 -39
- package/src/cmd/cloud/stream/index.ts +7 -1
- package/src/cmd/cloud/stream/stats.ts +2 -6
- package/src/cmd/cloud/task/attachment.ts +432 -0
- package/src/cmd/cloud/task/create.ts +131 -5
- package/src/cmd/cloud/task/get.ts +30 -12
- package/src/cmd/cloud/task/index.ts +13 -1
- package/src/cmd/cloud/task/list.ts +31 -15
- package/src/cmd/cloud/task/stats.ts +3 -3
- package/src/cmd/cloud/task/util.ts +2 -4
- package/src/cmd/cloud/webhook/create.ts +6 -1
- package/src/cmd/cloud/webhook/deliveries.ts +4 -5
- package/src/cmd/cloud/webhook/destinations.ts +4 -5
- 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
|
|
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:
|
|
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
|
},
|