artifact-cafe 0.0.2 → 0.0.4

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 (2) hide show
  1. package/dist/index.js +869 -223
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,102 +6,149 @@ import { defineCommand, runMain } from "citty";
6
6
  // src/comments.ts
7
7
  import path2 from "path";
8
8
 
9
- // ../shared/src/comments.ts
9
+ // ../shared/src/account.ts
10
10
  import { z } from "zod";
11
- var elementSnapshotSchema = z.object({
12
- classes: z.string().max(256).optional(),
13
- id: z.string().max(256).optional(),
14
- tag: z.string().min(1).max(128),
15
- text: z.string().max(200).optional()
11
+ var createApiKeySchema = z.object({
12
+ name: z.string().trim().min(1).max(60).optional()
13
+ });
14
+ var createdApiKeySchema = z.object({
15
+ createdAt: z.string(),
16
+ id: z.string(),
17
+ key: z.string(),
18
+ keyPrefix: z.string(),
19
+ name: z.string()
20
+ });
21
+ var apiKeySummarySchema = z.object({
22
+ createdAt: z.string(),
23
+ id: z.string(),
24
+ keyPrefix: z.string(),
25
+ lastUsedAt: z.string().nullable(),
26
+ name: z.string()
27
+ });
28
+ var apiKeyListResponseSchema = z.object({
29
+ keys: z.array(apiKeySummarySchema)
30
+ });
31
+ var revokeApiKeyResponseSchema = z.object({
32
+ revoked: z.literal(true)
16
33
  });
17
- var pointAnchorSchema = z.object({
34
+ var meResponseSchema = z.object({
35
+ // Best-effort profile field from our own user table; null when unknown.
36
+ email: z.string().nullable(),
37
+ // Present when the request was authenticated with an API key.
38
+ keyName: z.string().optional(),
39
+ userId: z.string()
40
+ });
41
+ var artifactListItemSchema = z.object({
42
+ createdAt: z.string(),
43
+ currentVersionNumber: z.number().int().positive().nullable(),
44
+ id: z.string(),
45
+ openCommentCount: z.number().int().nonnegative(),
46
+ slug: z.string(),
47
+ title: z.string(),
48
+ updatedAt: z.string(),
49
+ url: z.string().url(),
50
+ versionCount: z.number().int().nonnegative()
51
+ });
52
+ var artifactListResponseSchema = z.object({
53
+ artifacts: z.array(artifactListItemSchema)
54
+ });
55
+
56
+ // ../shared/src/comments.ts
57
+ import { z as z2 } from "zod";
58
+ var elementSnapshotSchema = z2.object({
59
+ classes: z2.string().max(256).optional(),
60
+ id: z2.string().max(256).optional(),
61
+ tag: z2.string().min(1).max(128),
62
+ text: z2.string().max(200).optional()
63
+ });
64
+ var pointAnchorSchema = z2.object({
18
65
  element: elementSnapshotSchema.optional(),
19
66
  // elementPath is the primary anchor; x/y are offsets within the
20
67
  // element's box, normalized 0..1 (SPEC.md §8.1).
21
- elementPath: z.string().max(2048),
22
- type: z.literal("point"),
23
- viewportHeight: z.number().int().positive().max(1e5),
24
- viewportWidth: z.number().int().positive().max(1e5),
25
- x: z.number().min(0).max(1),
26
- y: z.number().min(0).max(1)
27
- });
28
- var textAnchorSchema = z.object({
29
- elementPath: z.string().max(2048),
30
- endOffset: z.number().int().nonnegative().max(1e8),
31
- prefix: z.string().max(200).optional(),
32
- quote: z.string().min(1).max(1e3),
33
- startOffset: z.number().int().nonnegative().max(1e8),
34
- suffix: z.string().max(200).optional(),
35
- type: z.literal("text")
36
- });
37
- var anchorSchema = z.discriminatedUnion("type", [
68
+ elementPath: z2.string().max(2048),
69
+ type: z2.literal("point"),
70
+ viewportHeight: z2.number().int().positive().max(1e5),
71
+ viewportWidth: z2.number().int().positive().max(1e5),
72
+ x: z2.number().min(0).max(1),
73
+ y: z2.number().min(0).max(1)
74
+ });
75
+ var textAnchorSchema = z2.object({
76
+ elementPath: z2.string().max(2048),
77
+ endOffset: z2.number().int().nonnegative().max(1e8),
78
+ prefix: z2.string().max(200).optional(),
79
+ quote: z2.string().min(1).max(1e3),
80
+ startOffset: z2.number().int().nonnegative().max(1e8),
81
+ suffix: z2.string().max(200).optional(),
82
+ type: z2.literal("text")
83
+ });
84
+ var anchorSchema = z2.discriminatedUnion("type", [
38
85
  pointAnchorSchema,
39
86
  textAnchorSchema
40
87
  ]);
41
- var guestSchema = z.object({
42
- displayName: z.string().min(1).max(64),
43
- guestId: z.string().min(16)
88
+ var guestSchema = z2.object({
89
+ displayName: z2.string().min(1).max(64),
90
+ guestId: z2.string().min(16)
44
91
  });
45
- var createCommentThreadSchema = z.object({
92
+ var createCommentThreadSchema = z2.object({
46
93
  anchor: anchorSchema,
47
- body: z.string().min(1).max(1e4),
94
+ body: z2.string().min(1).max(1e4),
48
95
  guest: guestSchema.optional(),
49
- versionId: z.string()
96
+ versionId: z2.string()
50
97
  });
51
- var createCommentMessageSchema = z.object({
52
- body: z.string().min(1).max(1e4),
98
+ var createCommentMessageSchema = z2.object({
99
+ body: z2.string().min(1).max(1e4),
53
100
  guest: guestSchema.optional()
54
101
  });
55
- var threadActionSchema = z.object({
102
+ var threadActionSchema = z2.object({
56
103
  guest: guestSchema.optional()
57
104
  });
58
- var commentAuthorSchema = z.object({
59
- displayName: z.string(),
60
- guestId: z.string().nullable()
105
+ var commentAuthorSchema = z2.object({
106
+ displayName: z2.string(),
107
+ guestId: z2.string().nullable()
61
108
  });
62
- var commentMessageSchema = z.object({
109
+ var commentMessageSchema = z2.object({
63
110
  author: commentAuthorSchema,
64
- body: z.string(),
65
- createdAt: z.string(),
66
- id: z.string()
111
+ body: z2.string(),
112
+ createdAt: z2.string(),
113
+ id: z2.string()
67
114
  });
68
- var commentThreadSchema = z.object({
115
+ var commentThreadSchema = z2.object({
69
116
  anchor: anchorSchema,
70
- artifactId: z.string(),
71
- createdAt: z.string(),
117
+ artifactId: z2.string(),
118
+ createdAt: z2.string(),
72
119
  createdBy: commentAuthorSchema,
73
- id: z.string(),
74
- messages: z.array(commentMessageSchema),
75
- status: z.enum(["open", "resolved"]),
76
- versionId: z.string()
120
+ id: z2.string(),
121
+ messages: z2.array(commentMessageSchema),
122
+ status: z2.enum(["open", "resolved"]),
123
+ versionId: z2.string()
77
124
  });
78
- var commentThreadListSchema = z.object({
79
- threads: z.array(commentThreadSchema)
125
+ var commentThreadListSchema = z2.object({
126
+ threads: z2.array(commentThreadSchema)
80
127
  });
81
- var commentExportQuerySchema = z.object({
82
- status: z.enum(["open", "resolved", "all"]).default("open"),
83
- version: z.enum(["current", "all"]).default("current")
128
+ var commentExportQuerySchema = z2.object({
129
+ status: z2.enum(["open", "resolved", "all"]).default("open"),
130
+ version: z2.enum(["current", "all"]).default("current")
84
131
  });
85
- var commentExportReplySchema = z.object({
86
- author: z.string(),
87
- body: z.string(),
88
- createdAt: z.string()
132
+ var commentExportReplySchema = z2.object({
133
+ author: z2.string(),
134
+ body: z2.string(),
135
+ createdAt: z2.string()
89
136
  });
90
- var commentExportThreadSchema = z.object({
137
+ var commentExportThreadSchema = z2.object({
91
138
  anchor: anchorSchema,
92
- author: z.string(),
93
- body: z.string(),
94
- createdAt: z.string(),
95
- id: z.string(),
96
- replies: z.array(commentExportReplySchema),
97
- status: z.enum(["open", "resolved"]),
98
- versionNumber: z.number().int().positive()
139
+ author: z2.string(),
140
+ body: z2.string(),
141
+ createdAt: z2.string(),
142
+ id: z2.string(),
143
+ replies: z2.array(commentExportReplySchema),
144
+ status: z2.enum(["open", "resolved"]),
145
+ versionNumber: z2.number().int().positive()
99
146
  });
100
- var commentExportSchema = z.object({
101
- artifactId: z.string(),
102
- comments: z.array(commentExportThreadSchema),
147
+ var commentExportSchema = z2.object({
148
+ artifactId: z2.string(),
149
+ comments: z2.array(commentExportThreadSchema),
103
150
  // The artifact's current version (null before the first finalize).
104
- versionNumber: z.number().int().positive().nullable()
151
+ versionNumber: z2.number().int().positive().nullable()
105
152
  });
106
153
  function elementLabel(el) {
107
154
  if (el.id) {
@@ -112,55 +159,90 @@ function elementLabel(el) {
112
159
  }
113
160
 
114
161
  // ../shared/src/errors.ts
115
- import { z as z2 } from "zod";
116
- var apiErrorSchema = z2.object({
117
- code: z2.string(),
118
- docsUrl: z2.string().url(),
119
- message: z2.string(),
120
- retryAfter: z2.number().int().positive().optional()
162
+ import { z as z3 } from "zod";
163
+ var apiErrorSchema = z3.object({
164
+ code: z3.string(),
165
+ docsUrl: z3.string().url(),
166
+ message: z3.string(),
167
+ retryAfter: z3.number().int().positive().optional()
168
+ });
169
+
170
+ // ../shared/src/me.ts
171
+ import { z as z4 } from "zod";
172
+ var ownArtifactSchema = z4.object({
173
+ createdAt: z4.string(),
174
+ currentVersionNumber: z4.number().nullable(),
175
+ id: z4.string(),
176
+ openThreads: z4.number(),
177
+ protected: z4.boolean(),
178
+ slug: z4.string(),
179
+ title: z4.string(),
180
+ totalViews: z4.number(),
181
+ updatedAt: z4.string(),
182
+ url: z4.string(),
183
+ versionCount: z4.number()
184
+ });
185
+ var ownArtifactsResponseSchema = z4.object({
186
+ artifacts: z4.array(ownArtifactSchema)
121
187
  });
122
188
 
123
189
  // ../shared/src/owner.ts
124
- import { z as z3 } from "zod";
190
+ import { z as z5 } from "zod";
125
191
  var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
126
- var claimResponseSchema = z3.object({
127
- artifactId: z3.string(),
128
- slug: z3.string(),
129
- title: z3.string(),
130
- url: z3.string().url()
131
- });
132
- var updateArtifactSchema = z3.object({
133
- slug: z3.string().min(3).max(63).regex(SLUG_PATTERN, {
192
+ var claimResponseSchema = z5.object({
193
+ artifactId: z5.string(),
194
+ slug: z5.string(),
195
+ title: z5.string(),
196
+ url: z5.string().url()
197
+ });
198
+ var updateArtifactSchema = z5.object({
199
+ slug: z5.string().min(3).max(63).regex(SLUG_PATTERN, {
134
200
  message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
135
201
  }).optional(),
136
- title: z3.string().min(1).max(200).optional()
202
+ title: z5.string().min(1).max(200).optional()
137
203
  }).refine((body) => body.title !== void 0 || body.slug !== void 0, {
138
204
  message: "provide at least one of title, slug"
139
205
  });
140
- var deleteArtifactResponseSchema = z3.object({
141
- deleted: z3.literal(true)
206
+ var deleteArtifactResponseSchema = z5.object({
207
+ deleted: z5.literal(true)
208
+ });
209
+
210
+ // ../shared/src/presence.ts
211
+ import { z as z6 } from "zod";
212
+ var AGENT_PRESENCE = {
213
+ heartbeatSeconds: 15,
214
+ shellPollSeconds: 15,
215
+ ttlSeconds: 45
216
+ };
217
+ var agentHeartbeatResponseSchema = z6.object({
218
+ listening: z6.literal(true),
219
+ ttlSeconds: z6.number().int().positive()
220
+ });
221
+ var agentPresenceSchema = z6.object({
222
+ listening: z6.boolean(),
223
+ versionNumber: z6.number().int().positive().nullable()
142
224
  });
143
225
 
144
226
  // ../shared/src/protection.ts
145
- import { z as z4 } from "zod";
227
+ import { z as z7 } from "zod";
146
228
  var PASSWORD_MIN = 6;
147
229
  var PASSWORD_MAX = 128;
148
- var gatePasswordSchema = z4.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
149
- var setProtectionSchema = z4.object({
230
+ var gatePasswordSchema = z7.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
231
+ var setProtectionSchema = z7.object({
150
232
  password: gatePasswordSchema.nullable()
151
233
  });
152
- var protectionResponseSchema = z4.object({
153
- protected: z4.boolean()
234
+ var protectionResponseSchema = z7.object({
235
+ protected: z7.boolean()
154
236
  });
155
- var unlockSchema = z4.object({
156
- password: z4.string().min(1).max(1024)
237
+ var unlockSchema = z7.object({
238
+ password: z7.string().min(1).max(1024)
157
239
  });
158
- var unlockResponseSchema = z4.object({
159
- unlocked: z4.literal(true)
240
+ var unlockResponseSchema = z7.object({
241
+ unlocked: z7.literal(true)
160
242
  });
161
243
 
162
244
  // ../shared/src/publish.ts
163
- import { z as z5 } from "zod";
245
+ import { z as z8 } from "zod";
164
246
  function isSafeArtifactPath(p) {
165
247
  if (p.length === 0 || p.length > 1024) {
166
248
  return false;
@@ -173,95 +255,128 @@ function isSafeArtifactPath(p) {
173
255
  }
174
256
  return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
175
257
  }
176
- var manifestFileSchema = z5.object({
177
- hash: z5.string().regex(/^sha256_[a-f0-9]{64}$/),
178
- mimeType: z5.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
179
- path: z5.string().min(1).max(1024).refine(isSafeArtifactPath, {
258
+ var manifestFileSchema = z8.object({
259
+ hash: z8.string().regex(/^sha256_[a-f0-9]{64}$/),
260
+ mimeType: z8.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
261
+ path: z8.string().min(1).max(1024).refine(isSafeArtifactPath, {
180
262
  message: "path must be a safe relative path"
181
263
  }),
182
- size: z5.number().int().positive()
264
+ size: z8.number().int().positive()
183
265
  });
184
- var publishSourceSchema = z5.object({
185
- agent: z5.string().optional(),
186
- runId: z5.string().optional(),
187
- type: z5.literal("cli")
266
+ var publishSourceSchema = z8.object({
267
+ agent: z8.string().optional(),
268
+ runId: z8.string().optional(),
269
+ type: z8.literal("cli")
188
270
  });
189
- var createPublishSessionSchema = z5.object({
190
- artifactId: z5.string().nullable(),
191
- entryPath: z5.string().optional(),
192
- files: z5.array(manifestFileSchema).min(1).max(1e3),
271
+ var createPublishSessionSchema = z8.object({
272
+ artifactId: z8.string().nullable(),
273
+ entryPath: z8.string().optional(),
274
+ files: z8.array(manifestFileSchema).min(1).max(1e3),
193
275
  source: publishSourceSchema,
194
- title: z5.string().max(200).optional()
276
+ title: z8.string().max(200).optional()
195
277
  });
196
- var publishSessionResponseSchema = z5.object({
197
- artifactId: z5.string(),
198
- expiresAt: z5.string(),
199
- filesToUpload: z5.array(
200
- z5.object({
201
- method: z5.literal("PUT"),
202
- path: z5.string(),
203
- uploadUrl: z5.string().url()
278
+ var publishSessionResponseSchema = z8.object({
279
+ artifactId: z8.string(),
280
+ expiresAt: z8.string(),
281
+ filesToUpload: z8.array(
282
+ z8.object({
283
+ method: z8.literal("PUT"),
284
+ path: z8.string(),
285
+ uploadUrl: z8.string().url()
204
286
  })
205
287
  ),
206
- publishSessionId: z5.string(),
207
- skippedFiles: z5.array(z5.string()),
208
- versionId: z5.string()
288
+ publishSessionId: z8.string(),
289
+ skippedFiles: z8.array(z8.string()),
290
+ versionId: z8.string()
209
291
  });
210
- var finalizeResponseSchema = z5.object({
211
- artifactId: z5.string(),
292
+ var finalizeResponseSchema = z8.object({
293
+ artifactId: z8.string(),
294
+ // API-key publishes create the artifact already claimed (SPEC §5.4) —
295
+ // the CLI uses this to tell "claim URL pending" from "already yours".
296
+ claimed: z8.boolean(),
212
297
  // Returned only on first publish of a new artifact (one-time).
213
- claimUrl: z5.string().url().optional(),
214
- publishToken: z5.string().optional(),
215
- url: z5.string().url(),
216
- versionId: z5.string(),
217
- versionNumber: z5.number().int().positive()
218
- });
219
- var artifactResponseSchema = z5.object({
220
- claimed: z5.boolean(),
221
- currentVersion: z5.object({
222
- entryUrl: z5.string().url(),
223
- id: z5.string(),
224
- versionNumber: z5.number().int().positive()
298
+ claimUrl: z8.string().url().optional(),
299
+ publishToken: z8.string().optional(),
300
+ url: z8.string().url(),
301
+ versionId: z8.string(),
302
+ versionNumber: z8.number().int().positive()
303
+ });
304
+ var artifactResponseSchema = z8.object({
305
+ claimed: z8.boolean(),
306
+ currentVersion: z8.object({
307
+ entryUrl: z8.string().url(),
308
+ id: z8.string(),
309
+ versionNumber: z8.number().int().positive()
225
310
  }).nullable(),
226
- id: z5.string(),
227
- slug: z5.string(),
228
- title: z5.string()
311
+ id: z8.string(),
312
+ slug: z8.string(),
313
+ title: z8.string()
229
314
  });
230
315
 
231
316
  // ../shared/src/report.ts
232
- import { z as z6 } from "zod";
233
- var reportRequestSchema = z6.object({
234
- reason: z6.string().max(1e3).optional()
317
+ import { z as z9 } from "zod";
318
+ var reportRequestSchema = z9.object({
319
+ reason: z9.string().max(1e3).optional()
320
+ });
321
+ var reportResponseSchema = z9.object({
322
+ reported: z9.literal(true)
323
+ });
324
+
325
+ // ../shared/src/stats.ts
326
+ import { z as z10 } from "zod";
327
+ var avgViewSecondsSchema = z10.number().int().nonnegative().nullable();
328
+ var versionViewStatsSchema = z10.object({
329
+ avgViewSeconds: avgViewSecondsSchema,
330
+ // Views from inside the review shell's iframe vs. direct visits to
331
+ // the render URL. views = embeddedViews + directViews.
332
+ directViews: z10.number().int().nonnegative(),
333
+ embeddedViews: z10.number().int().nonnegative(),
334
+ // Distinct hashed viewer IPs across the version's lifetime.
335
+ uniqueViewers: z10.number().int().nonnegative(),
336
+ versionNumber: z10.number().int().positive(),
337
+ views: z10.number().int().nonnegative()
338
+ });
339
+ var dailyViewsSchema = z10.object({
340
+ avgViewSeconds: avgViewSecondsSchema,
341
+ // UTC day, YYYY-MM-DD.
342
+ day: z10.string(),
343
+ uniqueViewers: z10.number().int().nonnegative(),
344
+ views: z10.number().int().nonnegative()
235
345
  });
236
- var reportResponseSchema = z6.object({
237
- reported: z6.literal(true)
346
+ var artifactStatsResponseSchema = z10.object({
347
+ avgViewSeconds: avgViewSecondsSchema,
348
+ daily: z10.array(dailyViewsSchema),
349
+ // Distinct hashed viewer IPs across all versions and days.
350
+ totalUniqueViewers: z10.number().int().nonnegative(),
351
+ totalViews: z10.number().int().nonnegative(),
352
+ versions: z10.array(versionViewStatsSchema)
238
353
  });
239
354
 
240
355
  // ../shared/src/versions.ts
241
- import { z as z7 } from "zod";
242
- var versionSourceSchema = z7.object({
243
- agentName: z7.string().nullable(),
244
- type: z7.string()
356
+ import { z as z11 } from "zod";
357
+ var versionSourceSchema = z11.object({
358
+ agentName: z11.string().nullable(),
359
+ type: z11.string()
245
360
  });
246
- var versionChangesSchema = z7.object({
247
- added: z7.array(z7.string()),
248
- changed: z7.array(z7.string()),
249
- removed: z7.array(z7.string())
361
+ var versionChangesSchema = z11.object({
362
+ added: z11.array(z11.string()),
363
+ changed: z11.array(z11.string()),
364
+ removed: z11.array(z11.string())
250
365
  });
251
- var versionSummarySchema = z7.object({
366
+ var versionSummarySchema = z11.object({
252
367
  // Absent for the first version — there is no previous version to diff against.
253
368
  changes: versionChangesSchema.nullable(),
254
- createdAt: z7.string(),
255
- entryUrl: z7.string().url(),
256
- fileCount: z7.number().int().nonnegative(),
257
- id: z7.string(),
258
- isCurrent: z7.boolean(),
369
+ createdAt: z11.string(),
370
+ entryUrl: z11.string().url(),
371
+ fileCount: z11.number().int().nonnegative(),
372
+ id: z11.string(),
373
+ isCurrent: z11.boolean(),
259
374
  source: versionSourceSchema,
260
- totalSizeBytes: z7.number().int().nonnegative(),
261
- versionNumber: z7.number().int().positive()
375
+ totalSizeBytes: z11.number().int().nonnegative(),
376
+ versionNumber: z11.number().int().positive()
262
377
  });
263
- var versionListResponseSchema = z7.object({
264
- versions: z7.array(versionSummarySchema)
378
+ var versionListResponseSchema = z11.object({
379
+ versions: z11.array(versionSummarySchema)
265
380
  });
266
381
 
267
382
  // src/api.ts
@@ -306,8 +421,8 @@ async function request(url, init, parse) {
306
421
  }
307
422
  return parse(body);
308
423
  }
309
- function createApiClient(apiUrl, publishToken) {
310
- const base = apiUrl.replace(/\/$/, "");
424
+ function createApiClient(apiUrl2, publishToken) {
425
+ const base = apiUrl2.replace(/\/$/, "");
311
426
  const headers = {
312
427
  "content-type": "application/json"
313
428
  };
@@ -315,6 +430,15 @@ function createApiClient(apiUrl, publishToken) {
315
430
  headers.authorization = `Bearer ${publishToken}`;
316
431
  }
317
432
  return {
433
+ // SPEC §6.19 — mark the agent as actively waiting for feedback.
434
+ // Authorized by the publish token this client was created with.
435
+ async agentHeartbeat(artifactId) {
436
+ return request(
437
+ `${base}/api/v1/artifacts/${artifactId}/agent-presence`,
438
+ { headers, method: "POST" },
439
+ (d) => agentHeartbeatResponseSchema.parse(d)
440
+ );
441
+ },
318
442
  async createSession(body) {
319
443
  return request(
320
444
  `${base}/api/v1/publish`,
@@ -347,6 +471,24 @@ function createApiClient(apiUrl, publishToken) {
347
471
  (d) => artifactResponseSchema.parse(d)
348
472
  );
349
473
  },
474
+ // SPEC §6.18 — everything owned by the account this client's API
475
+ // key (or session) belongs to.
476
+ async listArtifacts() {
477
+ return request(
478
+ `${base}/api/v1/artifacts`,
479
+ { headers, method: "GET" },
480
+ (d) => artifactListResponseSchema.parse(d)
481
+ );
482
+ },
483
+ // SPEC §6.14 — who the presented credential belongs to. Used by
484
+ // `login` to verify a pasted key and by `whoami`.
485
+ async me() {
486
+ return request(
487
+ `${base}/api/v1/me`,
488
+ { headers, method: "GET" },
489
+ (d) => meResponseSchema.parse(d)
490
+ );
491
+ },
350
492
  // SPEC §6.16 — set a password (string) or clear it (null). Authorized
351
493
  // by the publish token this client was created with.
352
494
  async setProtection(artifactId, password) {
@@ -383,13 +525,13 @@ import {
383
525
  writeFileSync
384
526
  } from "fs";
385
527
  import path from "path";
386
- import { z as z8 } from "zod";
387
- var configSchema = z8.object({
388
- apiUrl: z8.string(),
389
- artifactId: z8.string(),
390
- publishToken: z8.string(),
391
- slug: z8.string(),
392
- url: z8.string()
528
+ import { z as z12 } from "zod";
529
+ var configSchema = z12.object({
530
+ apiUrl: z12.string(),
531
+ artifactId: z12.string(),
532
+ publishToken: z12.string(),
533
+ slug: z12.string(),
534
+ url: z12.string()
393
535
  });
394
536
  var CONFIG_DIR = ".artifactcafe";
395
537
  var CONFIG_FILE = "config.json";
@@ -438,6 +580,9 @@ ${CONFIG_DIR}/
438
580
  // src/comments.ts
439
581
  var STATUSES = ["open", "resolved", "all"];
440
582
  var VERSIONS = ["current", "all"];
583
+ var pollSeconds = () => Number(process.env.AC_WAIT_POLL_SECONDS ?? 5);
584
+ var settleSeconds = () => Number(process.env.AC_WAIT_SETTLE_SECONDS ?? 20);
585
+ var WAIT_TIMEOUT_EXIT = 2;
441
586
  async function comments(opts) {
442
587
  if (!STATUSES.includes(opts.status)) {
443
588
  throw new Error(
@@ -458,8 +603,17 @@ async function comments(opts) {
458
603
  `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
459
604
  );
460
605
  }
461
- const apiUrl = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
462
- const client = createApiClient(apiUrl);
606
+ const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
607
+ const client = createApiClient(apiUrl2, config.publishToken);
608
+ if (opts.wait) {
609
+ await waitForFeedback(client, config, {
610
+ json: opts.json,
611
+ status,
612
+ timeoutSeconds: opts.timeout,
613
+ version
614
+ });
615
+ return;
616
+ }
463
617
  const data = await client.exportComments(config.artifactId, {
464
618
  status,
465
619
  version
@@ -470,6 +624,112 @@ async function comments(opts) {
470
624
  }
471
625
  printHuman(data, config.url, status, version);
472
626
  }
627
+ var sleep = (s) => new Promise((r) => setTimeout(r, s * 1e3));
628
+ function messageCounts(data) {
629
+ return new Map(data.comments.map((t) => [t.id, 1 + t.replies.length]));
630
+ }
631
+ function diffThreads(baseline, data) {
632
+ const changed = [];
633
+ for (const t of data.comments) {
634
+ const before = baseline.get(t.id);
635
+ if (before === void 0) {
636
+ changed.push({ ...t, change: "new" });
637
+ } else if (1 + t.replies.length > before) {
638
+ changed.push({ ...t, change: "updated" });
639
+ }
640
+ }
641
+ return changed;
642
+ }
643
+ async function waitForFeedback(client, config, opts) {
644
+ const query = { status: opts.status, version: opts.version };
645
+ const baselineExport = await client.exportComments(config.artifactId, query);
646
+ const baseline = messageCounts(baselineExport);
647
+ if (!opts.json) {
648
+ const mins = Math.round(opts.timeoutSeconds / 60);
649
+ console.log(
650
+ `Waiting for feedback on ${config.url} \u2014 times out after ${mins}m. Ctrl-C to stop.`
651
+ );
652
+ }
653
+ const deadline = Date.now() + opts.timeoutSeconds * 1e3;
654
+ let heartbeatWarned = false;
655
+ let lastHeartbeat = 0;
656
+ const heartbeat = async () => {
657
+ if (Date.now() - lastHeartbeat < AGENT_PRESENCE.heartbeatSeconds * 1e3) {
658
+ return;
659
+ }
660
+ lastHeartbeat = Date.now();
661
+ try {
662
+ await client.agentHeartbeat(config.artifactId);
663
+ } catch (err) {
664
+ if (!(opts.json || heartbeatWarned)) {
665
+ heartbeatWarned = true;
666
+ console.error(
667
+ `(presence heartbeat failed \u2014 reviewers won't see "Agent is listening": ${err instanceof Error ? err.message : err})`
668
+ );
669
+ }
670
+ }
671
+ };
672
+ await heartbeat();
673
+ while (Date.now() < deadline) {
674
+ await sleep(Math.min(pollSeconds(), (deadline - Date.now()) / 1e3));
675
+ await heartbeat();
676
+ const data = await client.exportComments(config.artifactId, query);
677
+ if (diffThreads(baseline, data).length === 0) {
678
+ continue;
679
+ }
680
+ await sleep(settleSeconds());
681
+ const settled = await client.exportComments(config.artifactId, query);
682
+ const changed = diffThreads(baseline, settled);
683
+ printWaitResult(config, settled, changed, opts.json);
684
+ return;
685
+ }
686
+ if (opts.json) {
687
+ console.log(
688
+ JSON.stringify(
689
+ {
690
+ artifactId: config.artifactId,
691
+ threads: [],
692
+ timedOut: true,
693
+ versionNumber: baselineExport.versionNumber
694
+ },
695
+ null,
696
+ 2
697
+ )
698
+ );
699
+ } else {
700
+ console.log(
701
+ `No new feedback after ${opts.timeoutSeconds}s. Run the command again to keep waiting.`
702
+ );
703
+ }
704
+ process.exit(WAIT_TIMEOUT_EXIT);
705
+ }
706
+ function printWaitResult(config, data, changed, json) {
707
+ if (json) {
708
+ console.log(
709
+ JSON.stringify(
710
+ {
711
+ artifactId: config.artifactId,
712
+ threads: changed,
713
+ timedOut: false,
714
+ versionNumber: data.versionNumber
715
+ },
716
+ null,
717
+ 2
718
+ )
719
+ );
720
+ return;
721
+ }
722
+ const plural = changed.length === 1 ? "" : "s";
723
+ console.log(
724
+ `
725
+ ${changed.length} thread${plural} with new feedback \u2014 ${config.url}
726
+ `
727
+ );
728
+ for (const thread of changed) {
729
+ console.log(thread.change === "new" ? "NEW" : "UPDATED");
730
+ printThread(thread);
731
+ }
732
+ }
473
733
  function printHuman(data, url, status, version) {
474
734
  const statusLabel = status === "all" ? "" : `${status} `;
475
735
  let scope = "across all versions";
@@ -514,47 +774,75 @@ function indent(text, pad) {
514
774
  return text.split("\n").map((line) => pad + line).join("\n");
515
775
  }
516
776
 
517
- // src/errors.ts
518
- function printCliError(err, json) {
519
- if (err instanceof ApiRequestError) {
520
- if (json) {
521
- console.log(
522
- JSON.stringify({
523
- error: {
524
- code: err.code,
525
- message: err.message,
526
- retryAfter: err.retryAfter
527
- }
528
- })
529
- );
530
- } else {
531
- console.error(`\u2717 ${err.message} (${err.code})`);
532
- if (err.retryAfter) {
533
- console.error(` Retry in ${err.retryAfter}s.`);
534
- }
535
- }
536
- return;
777
+ // src/dashboard.ts
778
+ import path6 from "path";
779
+
780
+ // src/credentials.ts
781
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
782
+ import { homedir } from "os";
783
+ import path3 from "path";
784
+ import { z as z13 } from "zod";
785
+ var DEFAULT_API_URL = "https://artifact.cafe";
786
+ var credentialsSchema = z13.object({
787
+ keys: z13.record(z13.string(), z13.string()),
788
+ version: z13.literal(1)
789
+ });
790
+ function credentialsPath() {
791
+ return path3.join(homedir(), ".artifactcafe", "credentials");
792
+ }
793
+ function apiOrigin(apiUrl2) {
794
+ return new URL(apiUrl2).origin;
795
+ }
796
+ function readCredentials() {
797
+ const file = credentialsPath();
798
+ if (!existsSync2(file)) {
799
+ return { keys: {}, version: 1 };
537
800
  }
538
- const message = err instanceof Error ? err.message : String(err);
539
- if (json) {
540
- console.log(JSON.stringify({ error: { code: "cli_error", message } }));
541
- } else {
542
- console.error(`\u2717 ${message}`);
801
+ try {
802
+ return credentialsSchema.parse(JSON.parse(readFileSync2(file, "utf8")));
803
+ } catch {
804
+ return { keys: {}, version: 1 };
543
805
  }
544
806
  }
545
-
546
- // src/open.ts
547
- import path5 from "path";
807
+ function writeCredentials(credentials) {
808
+ const file = credentialsPath();
809
+ mkdirSync2(path3.dirname(file), { mode: 448, recursive: true });
810
+ writeFileSync2(file, `${JSON.stringify(credentials, null, 2)}
811
+ `, {
812
+ mode: 384
813
+ });
814
+ }
815
+ function storedApiKey(apiUrl2) {
816
+ return readCredentials().keys[apiOrigin(apiUrl2)] ?? null;
817
+ }
818
+ function storeApiKey(apiUrl2, key) {
819
+ const credentials = readCredentials();
820
+ credentials.keys[apiOrigin(apiUrl2)] = key;
821
+ writeCredentials(credentials);
822
+ }
823
+ function removeApiKey(apiUrl2) {
824
+ const credentials = readCredentials();
825
+ const origin = apiOrigin(apiUrl2);
826
+ if (!(origin in credentials.keys)) {
827
+ return false;
828
+ }
829
+ delete credentials.keys[origin];
830
+ writeCredentials(credentials);
831
+ return true;
832
+ }
833
+ function resolveApiKey(apiUrl2, flag) {
834
+ return flag ?? process.env.ARTIFACT_CAFE_API_KEY ?? storedApiKey(apiUrl2) ?? void 0;
835
+ }
548
836
 
549
837
  // src/publish.ts
550
838
  import { exec } from "child_process";
551
- import { readFileSync as readFileSync3 } from "fs";
552
- import path4 from "path";
839
+ import { readFileSync as readFileSync4 } from "fs";
840
+ import path5 from "path";
553
841
 
554
842
  // src/manifest.ts
555
843
  import { createHash } from "crypto";
556
- import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
557
- import path3 from "path";
844
+ import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
845
+ import path4 from "path";
558
846
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".artifactcafe"]);
559
847
  var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
560
848
  function isIgnoredFile(name) {
@@ -603,7 +891,7 @@ var MIME_TYPES = {
603
891
  ".xml": "application/xml"
604
892
  };
605
893
  function mimeFor(filePath) {
606
- return MIME_TYPES[path3.extname(filePath).toLowerCase()] ?? "application/octet-stream";
894
+ return MIME_TYPES[path4.extname(filePath).toLowerCase()] ?? "application/octet-stream";
607
895
  }
608
896
  function scanDirectory(dir) {
609
897
  const files = [];
@@ -611,7 +899,7 @@ function scanDirectory(dir) {
611
899
  function walk(current, relPrefix) {
612
900
  for (const entry of readdirSync(current, { withFileTypes: true })) {
613
901
  const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
614
- const abs = path3.join(current, entry.name);
902
+ const abs = path4.join(current, entry.name);
615
903
  if (entry.isSymbolicLink()) {
616
904
  skipped.push(rel);
617
905
  continue;
@@ -636,7 +924,7 @@ function scanDirectory(dir) {
636
924
  skipped.push(rel);
637
925
  continue;
638
926
  }
639
- const content = readFileSync2(abs);
927
+ const content = readFileSync3(abs);
640
928
  files.push({
641
929
  absPath: abs,
642
930
  hash: `sha256_${createHash("sha256").update(content).digest("hex")}`,
@@ -675,10 +963,9 @@ function resolveEntry(files, explicit) {
675
963
  }
676
964
 
677
965
  // src/publish.ts
678
- var DEFAULT_API_URL = "https://artifact.cafe";
679
966
  var UPLOAD_CONCURRENCY = 6;
680
967
  async function publish(opts) {
681
- const dir = path4.resolve(opts.dir);
968
+ const dir = path5.resolve(opts.dir);
682
969
  const config = readConfig(dir);
683
970
  if (opts.password !== void 0) {
684
971
  const parsed = gatePasswordSchema.safeParse(opts.password);
@@ -688,7 +975,8 @@ async function publish(opts) {
688
975
  }
689
976
  const artifactId = opts.artifact ?? config?.artifactId ?? null;
690
977
  const token = (artifactId !== null && artifactId === config?.artifactId ? config.publishToken : void 0) ?? process.env.ARTIFACT_CAFE_TOKEN;
691
- const apiUrl = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
978
+ const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
979
+ const apiKey = resolveApiKey(apiUrl2, opts.apiKey);
692
980
  const { files, skipped } = scanDirectory(dir);
693
981
  if (files.length === 0) {
694
982
  throw new Error(`Nothing to publish in ${dir} (after ignore rules).`);
@@ -702,8 +990,7 @@ async function publish(opts) {
702
990
  if (skipped.length) {
703
991
  log(` ignoring ${skipped.length} item(s): ${preview(skipped)}`);
704
992
  }
705
- const client = createApiClient(apiUrl, token);
706
- const session = await client.createSession({
993
+ const sessionBody = {
707
994
  artifactId,
708
995
  entryPath,
709
996
  files: files.map(({ absPath: _absPath, ...manifest }) => manifest),
@@ -713,7 +1000,21 @@ async function publish(opts) {
713
1000
  type: "cli"
714
1001
  },
715
1002
  title: opts.title
716
- });
1003
+ };
1004
+ let credential = apiKey ?? token;
1005
+ let client = createApiClient(apiUrl2, credential);
1006
+ let session;
1007
+ try {
1008
+ session = await client.createSession(sessionBody);
1009
+ } catch (err) {
1010
+ const keyRefused = apiKey !== void 0 && token !== void 0 && apiKey !== token && artifactId !== null && err instanceof ApiRequestError && (err.status === 401 || err.status === 403);
1011
+ if (!keyRefused) {
1012
+ throw err;
1013
+ }
1014
+ credential = token;
1015
+ client = createApiClient(apiUrl2, credential);
1016
+ session = await client.createSession(sessionBody);
1017
+ }
717
1018
  const byPath = new Map(files.map((f) => [f.path, f]));
718
1019
  log(
719
1020
  ` uploading ${session.filesToUpload.length} file(s)` + (session.skippedFiles.length ? `, ${session.skippedFiles.length} unchanged` : "")
@@ -725,14 +1026,14 @@ async function publish(opts) {
725
1026
  }
726
1027
  await uploadFile(
727
1028
  upload.uploadUrl,
728
- readFileSync3(file.absPath),
1029
+ readFileSync4(file.absPath),
729
1030
  file.mimeType
730
1031
  );
731
1032
  });
732
1033
  const result = await client.finalize(session.publishSessionId);
733
1034
  if (result.publishToken) {
734
1035
  writeConfig(dir, {
735
- apiUrl,
1036
+ apiUrl: apiUrl2,
736
1037
  artifactId: result.artifactId,
737
1038
  publishToken: result.publishToken,
738
1039
  slug: slugFromUrl(result.url),
@@ -740,7 +1041,10 @@ async function publish(opts) {
740
1041
  });
741
1042
  }
742
1043
  if (opts.password !== void 0) {
743
- const protectClient = createApiClient(apiUrl, token ?? result.publishToken);
1044
+ const protectClient = createApiClient(
1045
+ apiUrl2,
1046
+ credential ?? result.publishToken
1047
+ );
744
1048
  await protectClient.setProtection(result.artifactId, opts.password);
745
1049
  }
746
1050
  if (opts.json) {
@@ -748,6 +1052,7 @@ async function publish(opts) {
748
1052
  JSON.stringify(
749
1053
  {
750
1054
  artifactId: result.artifactId,
1055
+ claimed: result.claimed,
751
1056
  url: result.url,
752
1057
  versionNumber: result.versionNumber,
753
1058
  ...result.claimUrl ? { claimUrl: result.claimUrl } : {},
@@ -771,6 +1076,8 @@ ${result.url}
771
1076
  ${result.claimUrl}
772
1077
  `
773
1078
  );
1079
+ } else if (result.claimed) {
1080
+ console.log("Claimed by your account \u2014 no expiry.\n");
774
1081
  }
775
1082
  if (opts.password !== void 0) {
776
1083
  console.log("Password protection: on\n");
@@ -823,9 +1130,219 @@ function openInBrowser(url) {
823
1130
  });
824
1131
  }
825
1132
 
1133
+ // src/dashboard.ts
1134
+ function openDashboard(opts) {
1135
+ const base = process.env.ARTIFACT_CAFE_URL ?? readConfig(path6.resolve(opts.dir))?.apiUrl ?? DEFAULT_API_URL;
1136
+ const url = `${base.replace(/\/$/, "")}/dashboard`;
1137
+ console.log(url);
1138
+ if (process.stdout.isTTY) {
1139
+ openInBrowser(url);
1140
+ }
1141
+ }
1142
+
1143
+ // src/errors.ts
1144
+ function printCliError(err, json) {
1145
+ if (err instanceof ApiRequestError) {
1146
+ if (json) {
1147
+ console.log(
1148
+ JSON.stringify({
1149
+ error: {
1150
+ code: err.code,
1151
+ message: err.message,
1152
+ retryAfter: err.retryAfter
1153
+ }
1154
+ })
1155
+ );
1156
+ } else {
1157
+ console.error(`\u2717 ${err.message} (${err.code})`);
1158
+ if (err.retryAfter) {
1159
+ console.error(` Retry in ${err.retryAfter}s.`);
1160
+ }
1161
+ }
1162
+ return;
1163
+ }
1164
+ const message = err instanceof Error ? err.message : String(err);
1165
+ if (json) {
1166
+ console.log(JSON.stringify({ error: { code: "cli_error", message } }));
1167
+ } else {
1168
+ console.error(`\u2717 ${message}`);
1169
+ }
1170
+ }
1171
+
1172
+ // src/list.ts
1173
+ async function list(opts) {
1174
+ const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
1175
+ /\/$/,
1176
+ ""
1177
+ );
1178
+ const key = resolveApiKey(apiUrl2, opts.apiKey);
1179
+ if (!key) {
1180
+ throw new Error(
1181
+ `Listing your artifacts needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
1182
+ );
1183
+ }
1184
+ const data = await createApiClient(apiUrl2, key).listArtifacts();
1185
+ if (opts.json) {
1186
+ console.log(JSON.stringify(data, null, 2));
1187
+ return;
1188
+ }
1189
+ if (data.artifacts.length === 0) {
1190
+ console.log(
1191
+ "No artifacts in your account yet \u2014 publish while logged in, or claim one via its claim link."
1192
+ );
1193
+ return;
1194
+ }
1195
+ const plural = data.artifacts.length === 1 ? "" : "s";
1196
+ console.log(`${data.artifacts.length} artifact${plural}
1197
+ `);
1198
+ for (const artifact of data.artifacts) {
1199
+ printArtifact(artifact);
1200
+ }
1201
+ }
1202
+ function printArtifact(a) {
1203
+ const version = a.currentVersionNumber === null ? "no version" : `v${a.currentVersionNumber}`;
1204
+ const open2 = a.openCommentCount === 0 ? "no open comments" : `${a.openCommentCount} open comment${a.openCommentCount === 1 ? "" : "s"}`;
1205
+ const marker = a.openCommentCount > 0 ? "\u25CF" : "\u25CB";
1206
+ console.log(`${marker} ${a.title}`);
1207
+ console.log(` ${version} \xB7 ${open2} \xB7 updated ${relativeTime(a.updatedAt)}`);
1208
+ console.log(` ${a.url}
1209
+ `);
1210
+ }
1211
+ function relativeTime(iso) {
1212
+ const seconds = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1e3);
1213
+ if (seconds < 60) {
1214
+ return "just now";
1215
+ }
1216
+ if (seconds < 3600) {
1217
+ return `${Math.floor(seconds / 60)}m ago`;
1218
+ }
1219
+ if (seconds < 86400) {
1220
+ return `${Math.floor(seconds / 3600)}h ago`;
1221
+ }
1222
+ if (seconds < 30 * 86400) {
1223
+ return `${Math.floor(seconds / 86400)}d ago`;
1224
+ }
1225
+ return iso.slice(0, 10);
1226
+ }
1227
+
1228
+ // src/login.ts
1229
+ import { createInterface } from "readline";
1230
+ function apiUrl() {
1231
+ return (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
1232
+ }
1233
+ function promptSecret(question) {
1234
+ return new Promise((resolve) => {
1235
+ const rl = createInterface({
1236
+ input: process.stdin,
1237
+ output: process.stdout
1238
+ });
1239
+ let muted = false;
1240
+ const raw = rl;
1241
+ raw._writeToOutput = (s) => {
1242
+ if (!muted) {
1243
+ process.stdout.write(s);
1244
+ }
1245
+ };
1246
+ rl.question(question, (answer) => {
1247
+ rl.close();
1248
+ process.stdout.write("\n");
1249
+ resolve(answer);
1250
+ });
1251
+ muted = true;
1252
+ });
1253
+ }
1254
+ async function login(opts) {
1255
+ const base = apiUrl();
1256
+ const keysUrl = `${base}/cli`;
1257
+ let key = opts.apiKey?.trim();
1258
+ if (!key) {
1259
+ if (!opts.json) {
1260
+ console.log(`
1261
+ Opening ${keysUrl}`);
1262
+ console.log("Create a CLI key there, then paste it below.\n");
1263
+ }
1264
+ if (process.stdout.isTTY) {
1265
+ openInBrowser(keysUrl);
1266
+ }
1267
+ key = (await promptSecret("API key: ")).trim();
1268
+ }
1269
+ if (!key) {
1270
+ throw new Error("No key entered \u2014 nothing was saved.");
1271
+ }
1272
+ if (!key.startsWith("ack_")) {
1273
+ throw new Error(
1274
+ `That doesn't look like an artifact.cafe API key (they start with "ack_").`
1275
+ );
1276
+ }
1277
+ const me = await createApiClient(base, key).me();
1278
+ storeApiKey(base, key);
1279
+ const who = me.email ?? me.userId;
1280
+ if (opts.json) {
1281
+ console.log(
1282
+ JSON.stringify(
1283
+ { apiUrl: base, email: me.email, loggedIn: true, userId: me.userId },
1284
+ null,
1285
+ 2
1286
+ )
1287
+ );
1288
+ return;
1289
+ }
1290
+ console.log(`
1291
+ \u2713 Logged in to ${apiOrigin(base)} as ${who}`);
1292
+ console.log(
1293
+ "New publishes are claimed by your account automatically. Try `artifact-cafe list`.\n"
1294
+ );
1295
+ }
1296
+ async function whoami(opts) {
1297
+ const base = apiUrl();
1298
+ const key = resolveApiKey(base);
1299
+ if (!key) {
1300
+ throw new Error(
1301
+ `Not logged in to ${apiOrigin(base)} \u2014 run \`artifact-cafe login\` first.`
1302
+ );
1303
+ }
1304
+ const me = await createApiClient(base, key).me();
1305
+ if (opts.json) {
1306
+ console.log(
1307
+ JSON.stringify(
1308
+ {
1309
+ apiUrl: base,
1310
+ email: me.email,
1311
+ keyName: me.keyName ?? null,
1312
+ userId: me.userId
1313
+ },
1314
+ null,
1315
+ 2
1316
+ )
1317
+ );
1318
+ return;
1319
+ }
1320
+ const keyLabel = me.keyName ? ` (key: ${me.keyName})` : "";
1321
+ console.log(
1322
+ `Logged in to ${apiOrigin(base)} as ${me.email ?? me.userId}${keyLabel}`
1323
+ );
1324
+ }
1325
+ function logout(opts) {
1326
+ const base = apiUrl();
1327
+ const removed = removeApiKey(base);
1328
+ if (opts.json) {
1329
+ console.log(JSON.stringify({ apiUrl: base, loggedOut: removed }, null, 2));
1330
+ return;
1331
+ }
1332
+ if (!removed) {
1333
+ console.log(`No stored key for ${apiOrigin(base)} \u2014 already logged out.`);
1334
+ return;
1335
+ }
1336
+ console.log(`\u2713 Logged out of ${apiOrigin(base)} on this machine.`);
1337
+ console.log(
1338
+ `The key itself is still active \u2014 revoke it at ${base}/cli if this machine is shared.`
1339
+ );
1340
+ }
1341
+
826
1342
  // src/open.ts
1343
+ import path7 from "path";
827
1344
  function openArtifact(opts) {
828
- const dir = path5.resolve(opts.dir);
1345
+ const dir = path7.resolve(opts.dir);
829
1346
  const config = readConfig(dir);
830
1347
  if (!config) {
831
1348
  throw new Error(
@@ -839,7 +1356,7 @@ function openArtifact(opts) {
839
1356
  }
840
1357
 
841
1358
  // src/protect.ts
842
- import path6 from "path";
1359
+ import path8 from "path";
843
1360
  async function protect(opts) {
844
1361
  if (opts.off && opts.password !== void 0) {
845
1362
  throw new Error("Pass either --password or --off, not both.");
@@ -856,16 +1373,16 @@ async function protect(opts) {
856
1373
  throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
857
1374
  }
858
1375
  }
859
- const dir = path6.resolve(opts.dir);
1376
+ const dir = path8.resolve(opts.dir);
860
1377
  const config = readConfig(dir);
861
1378
  if (!config) {
862
1379
  throw new Error(
863
1380
  `No artifact linked in ${dir} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
864
1381
  );
865
1382
  }
866
- const apiUrl = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
1383
+ const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
867
1384
  const token = config.publishToken ?? process.env.ARTIFACT_CAFE_TOKEN;
868
- const client = createApiClient(apiUrl, token);
1385
+ const client = createApiClient(apiUrl2, token);
869
1386
  const res = await client.setProtection(config.artifactId, password);
870
1387
  if (opts.json) {
871
1388
  console.log(JSON.stringify({ protected: res.protected }, null, 2));
@@ -883,6 +1400,10 @@ async function protect(opts) {
883
1400
  // src/index.ts
884
1401
  var publish2 = defineCommand({
885
1402
  args: {
1403
+ "api-key": {
1404
+ description: "Publish with an account API key (default: stored login)",
1405
+ type: "string"
1406
+ },
886
1407
  artifact: {
887
1408
  description: "Publish to an existing artifact ID",
888
1409
  type: "string"
@@ -905,6 +1426,7 @@ var publish2 = defineCommand({
905
1426
  async run({ args }) {
906
1427
  try {
907
1428
  await publish({
1429
+ apiKey: args["api-key"],
908
1430
  artifact: args.artifact,
909
1431
  dir: args.dir,
910
1432
  entry: args.entry,
@@ -919,6 +1441,82 @@ var publish2 = defineCommand({
919
1441
  }
920
1442
  }
921
1443
  });
1444
+ var login2 = defineCommand({
1445
+ args: {
1446
+ "api-key": {
1447
+ description: "Store this key without the browser prompt (agents, CI)",
1448
+ type: "string"
1449
+ },
1450
+ json: { description: "Machine-readable output", type: "boolean" }
1451
+ },
1452
+ meta: {
1453
+ description: "Log in \u2014 mint an API key in the browser and store it",
1454
+ name: "login"
1455
+ },
1456
+ async run({ args }) {
1457
+ try {
1458
+ await login({ apiKey: args["api-key"], json: Boolean(args.json) });
1459
+ } catch (err) {
1460
+ printCliError(err, Boolean(args.json));
1461
+ process.exit(1);
1462
+ }
1463
+ }
1464
+ });
1465
+ var logout2 = defineCommand({
1466
+ args: {
1467
+ json: { description: "Machine-readable output", type: "boolean" }
1468
+ },
1469
+ meta: {
1470
+ description: "Remove the stored API key for this machine",
1471
+ name: "logout"
1472
+ },
1473
+ run({ args }) {
1474
+ try {
1475
+ logout({ json: Boolean(args.json) });
1476
+ } catch (err) {
1477
+ printCliError(err, Boolean(args.json));
1478
+ process.exit(1);
1479
+ }
1480
+ }
1481
+ });
1482
+ var whoami2 = defineCommand({
1483
+ args: {
1484
+ json: { description: "Machine-readable output", type: "boolean" }
1485
+ },
1486
+ meta: {
1487
+ description: "Show which account the stored API key belongs to",
1488
+ name: "whoami"
1489
+ },
1490
+ async run({ args }) {
1491
+ try {
1492
+ await whoami({ json: Boolean(args.json) });
1493
+ } catch (err) {
1494
+ printCliError(err, Boolean(args.json));
1495
+ process.exit(1);
1496
+ }
1497
+ }
1498
+ });
1499
+ var list2 = defineCommand({
1500
+ args: {
1501
+ "api-key": {
1502
+ description: "List with this API key (default: stored login)",
1503
+ type: "string"
1504
+ },
1505
+ json: { description: "Machine-readable output", type: "boolean" }
1506
+ },
1507
+ meta: {
1508
+ description: "List the artifacts your account owns",
1509
+ name: "list"
1510
+ },
1511
+ async run({ args }) {
1512
+ try {
1513
+ await list({ apiKey: args["api-key"], json: Boolean(args.json) });
1514
+ } catch (err) {
1515
+ printCliError(err, Boolean(args.json));
1516
+ process.exit(1);
1517
+ }
1518
+ }
1519
+ });
922
1520
  var comments2 = defineCommand({
923
1521
  args: {
924
1522
  dir: {
@@ -932,10 +1530,19 @@ var comments2 = defineCommand({
932
1530
  description: "Filter: open | resolved | all",
933
1531
  type: "string"
934
1532
  },
1533
+ timeout: {
1534
+ default: "900",
1535
+ description: "With --wait: give up after this many seconds (exit 2)",
1536
+ type: "string"
1537
+ },
935
1538
  version: {
936
1539
  default: "current",
937
1540
  description: "Filter: current | all",
938
1541
  type: "string"
1542
+ },
1543
+ wait: {
1544
+ description: "Block until a reviewer leaves new feedback",
1545
+ type: "boolean"
939
1546
  }
940
1547
  },
941
1548
  meta: {
@@ -944,11 +1551,19 @@ var comments2 = defineCommand({
944
1551
  },
945
1552
  async run({ args }) {
946
1553
  try {
1554
+ const timeout = Number(args.timeout);
1555
+ if (!Number.isFinite(timeout) || timeout <= 0) {
1556
+ throw new Error(
1557
+ `--timeout must be a positive number of seconds (got "${args.timeout}").`
1558
+ );
1559
+ }
947
1560
  await comments({
948
1561
  dir: args.dir,
949
1562
  json: Boolean(args.json),
950
1563
  status: args.status,
951
- version: args.version
1564
+ timeout,
1565
+ version: args.version,
1566
+ wait: Boolean(args.wait)
952
1567
  });
953
1568
  } catch (err) {
954
1569
  printCliError(err, Boolean(args.json));
@@ -1003,11 +1618,42 @@ var open = defineCommand({
1003
1618
  }
1004
1619
  }
1005
1620
  });
1621
+ var dashboard = defineCommand({
1622
+ args: {
1623
+ dir: {
1624
+ default: ".",
1625
+ description: "Directory whose linked instance to open (optional)",
1626
+ type: "positional"
1627
+ }
1628
+ },
1629
+ meta: {
1630
+ description: "Open your personal dashboard in the browser",
1631
+ name: "dashboard"
1632
+ },
1633
+ run({ args }) {
1634
+ try {
1635
+ openDashboard({ dir: args.dir });
1636
+ } catch (err) {
1637
+ printCliError(err, false);
1638
+ process.exit(1);
1639
+ }
1640
+ }
1641
+ });
1006
1642
  var main = defineCommand({
1007
1643
  meta: {
1008
1644
  description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
1009
1645
  name: "artifact-cafe"
1010
1646
  },
1011
- subCommands: { comments: comments2, open, protect: protect2, publish: publish2 }
1647
+ subCommands: {
1648
+ comments: comments2,
1649
+ dashboard,
1650
+ list: list2,
1651
+ login: login2,
1652
+ logout: logout2,
1653
+ open,
1654
+ protect: protect2,
1655
+ publish: publish2,
1656
+ whoami: whoami2
1657
+ }
1012
1658
  });
1013
1659
  runMain(main);