artifact-cafe 0.0.3 → 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 +418 -206
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -167,47 +167,82 @@ var apiErrorSchema = z3.object({
167
167
  retryAfter: z3.number().int().positive().optional()
168
168
  });
169
169
 
170
- // ../shared/src/owner.ts
170
+ // ../shared/src/me.ts
171
171
  import { z as z4 } from "zod";
172
- var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
173
- var claimResponseSchema = z4.object({
174
- artifactId: z4.string(),
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(),
175
178
  slug: z4.string(),
176
179
  title: z4.string(),
177
- url: z4.string().url()
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)
178
187
  });
179
- var updateArtifactSchema = z4.object({
180
- slug: z4.string().min(3).max(63).regex(SLUG_PATTERN, {
188
+
189
+ // ../shared/src/owner.ts
190
+ import { z as z5 } from "zod";
191
+ var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
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, {
181
200
  message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
182
201
  }).optional(),
183
- title: z4.string().min(1).max(200).optional()
202
+ title: z5.string().min(1).max(200).optional()
184
203
  }).refine((body) => body.title !== void 0 || body.slug !== void 0, {
185
204
  message: "provide at least one of title, slug"
186
205
  });
187
- var deleteArtifactResponseSchema = z4.object({
188
- deleted: z4.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()
189
224
  });
190
225
 
191
226
  // ../shared/src/protection.ts
192
- import { z as z5 } from "zod";
227
+ import { z as z7 } from "zod";
193
228
  var PASSWORD_MIN = 6;
194
229
  var PASSWORD_MAX = 128;
195
- var gatePasswordSchema = z5.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
196
- var setProtectionSchema = z5.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({
197
232
  password: gatePasswordSchema.nullable()
198
233
  });
199
- var protectionResponseSchema = z5.object({
200
- protected: z5.boolean()
234
+ var protectionResponseSchema = z7.object({
235
+ protected: z7.boolean()
201
236
  });
202
- var unlockSchema = z5.object({
203
- password: z5.string().min(1).max(1024)
237
+ var unlockSchema = z7.object({
238
+ password: z7.string().min(1).max(1024)
204
239
  });
205
- var unlockResponseSchema = z5.object({
206
- unlocked: z5.literal(true)
240
+ var unlockResponseSchema = z7.object({
241
+ unlocked: z7.literal(true)
207
242
  });
208
243
 
209
244
  // ../shared/src/publish.ts
210
- import { z as z6 } from "zod";
245
+ import { z as z8 } from "zod";
211
246
  function isSafeArtifactPath(p) {
212
247
  if (p.length === 0 || p.length > 1024) {
213
248
  return false;
@@ -220,128 +255,128 @@ function isSafeArtifactPath(p) {
220
255
  }
221
256
  return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
222
257
  }
223
- var manifestFileSchema = z6.object({
224
- hash: z6.string().regex(/^sha256_[a-f0-9]{64}$/),
225
- mimeType: z6.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
226
- path: z6.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, {
227
262
  message: "path must be a safe relative path"
228
263
  }),
229
- size: z6.number().int().positive()
264
+ size: z8.number().int().positive()
230
265
  });
231
- var publishSourceSchema = z6.object({
232
- agent: z6.string().optional(),
233
- runId: z6.string().optional(),
234
- type: z6.literal("cli")
266
+ var publishSourceSchema = z8.object({
267
+ agent: z8.string().optional(),
268
+ runId: z8.string().optional(),
269
+ type: z8.literal("cli")
235
270
  });
236
- var createPublishSessionSchema = z6.object({
237
- artifactId: z6.string().nullable(),
238
- entryPath: z6.string().optional(),
239
- files: z6.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),
240
275
  source: publishSourceSchema,
241
- title: z6.string().max(200).optional()
276
+ title: z8.string().max(200).optional()
242
277
  });
243
- var publishSessionResponseSchema = z6.object({
244
- artifactId: z6.string(),
245
- expiresAt: z6.string(),
246
- filesToUpload: z6.array(
247
- z6.object({
248
- method: z6.literal("PUT"),
249
- path: z6.string(),
250
- uploadUrl: z6.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()
251
286
  })
252
287
  ),
253
- publishSessionId: z6.string(),
254
- skippedFiles: z6.array(z6.string()),
255
- versionId: z6.string()
288
+ publishSessionId: z8.string(),
289
+ skippedFiles: z8.array(z8.string()),
290
+ versionId: z8.string()
256
291
  });
257
- var finalizeResponseSchema = z6.object({
258
- artifactId: z6.string(),
292
+ var finalizeResponseSchema = z8.object({
293
+ artifactId: z8.string(),
259
294
  // API-key publishes create the artifact already claimed (SPEC §5.4) —
260
295
  // the CLI uses this to tell "claim URL pending" from "already yours".
261
- claimed: z6.boolean(),
296
+ claimed: z8.boolean(),
262
297
  // Returned only on first publish of a new artifact (one-time).
263
- claimUrl: z6.string().url().optional(),
264
- publishToken: z6.string().optional(),
265
- url: z6.string().url(),
266
- versionId: z6.string(),
267
- versionNumber: z6.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()
268
303
  });
269
- var artifactResponseSchema = z6.object({
270
- claimed: z6.boolean(),
271
- currentVersion: z6.object({
272
- entryUrl: z6.string().url(),
273
- id: z6.string(),
274
- versionNumber: z6.number().int().positive()
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()
275
310
  }).nullable(),
276
- id: z6.string(),
277
- slug: z6.string(),
278
- title: z6.string()
311
+ id: z8.string(),
312
+ slug: z8.string(),
313
+ title: z8.string()
279
314
  });
280
315
 
281
316
  // ../shared/src/report.ts
282
- import { z as z7 } from "zod";
283
- var reportRequestSchema = z7.object({
284
- reason: z7.string().max(1e3).optional()
317
+ import { z as z9 } from "zod";
318
+ var reportRequestSchema = z9.object({
319
+ reason: z9.string().max(1e3).optional()
285
320
  });
286
- var reportResponseSchema = z7.object({
287
- reported: z7.literal(true)
321
+ var reportResponseSchema = z9.object({
322
+ reported: z9.literal(true)
288
323
  });
289
324
 
290
325
  // ../shared/src/stats.ts
291
- import { z as z8 } from "zod";
292
- var avgViewSecondsSchema = z8.number().int().nonnegative().nullable();
293
- var versionViewStatsSchema = z8.object({
326
+ import { z as z10 } from "zod";
327
+ var avgViewSecondsSchema = z10.number().int().nonnegative().nullable();
328
+ var versionViewStatsSchema = z10.object({
294
329
  avgViewSeconds: avgViewSecondsSchema,
295
330
  // Views from inside the review shell's iframe vs. direct visits to
296
331
  // the render URL. views = embeddedViews + directViews.
297
- directViews: z8.number().int().nonnegative(),
298
- embeddedViews: z8.number().int().nonnegative(),
332
+ directViews: z10.number().int().nonnegative(),
333
+ embeddedViews: z10.number().int().nonnegative(),
299
334
  // Distinct hashed viewer IPs across the version's lifetime.
300
- uniqueViewers: z8.number().int().nonnegative(),
301
- versionNumber: z8.number().int().positive(),
302
- views: z8.number().int().nonnegative()
335
+ uniqueViewers: z10.number().int().nonnegative(),
336
+ versionNumber: z10.number().int().positive(),
337
+ views: z10.number().int().nonnegative()
303
338
  });
304
- var dailyViewsSchema = z8.object({
339
+ var dailyViewsSchema = z10.object({
305
340
  avgViewSeconds: avgViewSecondsSchema,
306
341
  // UTC day, YYYY-MM-DD.
307
- day: z8.string(),
308
- uniqueViewers: z8.number().int().nonnegative(),
309
- views: z8.number().int().nonnegative()
342
+ day: z10.string(),
343
+ uniqueViewers: z10.number().int().nonnegative(),
344
+ views: z10.number().int().nonnegative()
310
345
  });
311
- var artifactStatsResponseSchema = z8.object({
346
+ var artifactStatsResponseSchema = z10.object({
312
347
  avgViewSeconds: avgViewSecondsSchema,
313
- daily: z8.array(dailyViewsSchema),
348
+ daily: z10.array(dailyViewsSchema),
314
349
  // Distinct hashed viewer IPs across all versions and days.
315
- totalUniqueViewers: z8.number().int().nonnegative(),
316
- totalViews: z8.number().int().nonnegative(),
317
- versions: z8.array(versionViewStatsSchema)
350
+ totalUniqueViewers: z10.number().int().nonnegative(),
351
+ totalViews: z10.number().int().nonnegative(),
352
+ versions: z10.array(versionViewStatsSchema)
318
353
  });
319
354
 
320
355
  // ../shared/src/versions.ts
321
- import { z as z9 } from "zod";
322
- var versionSourceSchema = z9.object({
323
- agentName: z9.string().nullable(),
324
- type: z9.string()
356
+ import { z as z11 } from "zod";
357
+ var versionSourceSchema = z11.object({
358
+ agentName: z11.string().nullable(),
359
+ type: z11.string()
325
360
  });
326
- var versionChangesSchema = z9.object({
327
- added: z9.array(z9.string()),
328
- changed: z9.array(z9.string()),
329
- removed: z9.array(z9.string())
361
+ var versionChangesSchema = z11.object({
362
+ added: z11.array(z11.string()),
363
+ changed: z11.array(z11.string()),
364
+ removed: z11.array(z11.string())
330
365
  });
331
- var versionSummarySchema = z9.object({
366
+ var versionSummarySchema = z11.object({
332
367
  // Absent for the first version — there is no previous version to diff against.
333
368
  changes: versionChangesSchema.nullable(),
334
- createdAt: z9.string(),
335
- entryUrl: z9.string().url(),
336
- fileCount: z9.number().int().nonnegative(),
337
- id: z9.string(),
338
- isCurrent: z9.boolean(),
369
+ createdAt: z11.string(),
370
+ entryUrl: z11.string().url(),
371
+ fileCount: z11.number().int().nonnegative(),
372
+ id: z11.string(),
373
+ isCurrent: z11.boolean(),
339
374
  source: versionSourceSchema,
340
- totalSizeBytes: z9.number().int().nonnegative(),
341
- versionNumber: z9.number().int().positive()
375
+ totalSizeBytes: z11.number().int().nonnegative(),
376
+ versionNumber: z11.number().int().positive()
342
377
  });
343
- var versionListResponseSchema = z9.object({
344
- versions: z9.array(versionSummarySchema)
378
+ var versionListResponseSchema = z11.object({
379
+ versions: z11.array(versionSummarySchema)
345
380
  });
346
381
 
347
382
  // src/api.ts
@@ -395,6 +430,15 @@ function createApiClient(apiUrl2, publishToken) {
395
430
  headers.authorization = `Bearer ${publishToken}`;
396
431
  }
397
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
+ },
398
442
  async createSession(body) {
399
443
  return request(
400
444
  `${base}/api/v1/publish`,
@@ -481,13 +525,13 @@ import {
481
525
  writeFileSync
482
526
  } from "fs";
483
527
  import path from "path";
484
- import { z as z10 } from "zod";
485
- var configSchema = z10.object({
486
- apiUrl: z10.string(),
487
- artifactId: z10.string(),
488
- publishToken: z10.string(),
489
- slug: z10.string(),
490
- url: z10.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()
491
535
  });
492
536
  var CONFIG_DIR = ".artifactcafe";
493
537
  var CONFIG_FILE = "config.json";
@@ -536,6 +580,9 @@ ${CONFIG_DIR}/
536
580
  // src/comments.ts
537
581
  var STATUSES = ["open", "resolved", "all"];
538
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;
539
586
  async function comments(opts) {
540
587
  if (!STATUSES.includes(opts.status)) {
541
588
  throw new Error(
@@ -557,7 +604,16 @@ async function comments(opts) {
557
604
  );
558
605
  }
559
606
  const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config.apiUrl;
560
- const client = createApiClient(apiUrl2);
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
+ }
561
617
  const data = await client.exportComments(config.artifactId, {
562
618
  status,
563
619
  version
@@ -568,6 +624,112 @@ async function comments(opts) {
568
624
  }
569
625
  printHuman(data, config.url, status, version);
570
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
+ }
571
733
  function printHuman(data, url, status, version) {
572
734
  const statusLabel = status === "all" ? "" : `${status} `;
573
735
  let scope = "across all versions";
@@ -612,44 +774,18 @@ function indent(text, pad) {
612
774
  return text.split("\n").map((line) => pad + line).join("\n");
613
775
  }
614
776
 
615
- // src/errors.ts
616
- function printCliError(err, json) {
617
- if (err instanceof ApiRequestError) {
618
- if (json) {
619
- console.log(
620
- JSON.stringify({
621
- error: {
622
- code: err.code,
623
- message: err.message,
624
- retryAfter: err.retryAfter
625
- }
626
- })
627
- );
628
- } else {
629
- console.error(`\u2717 ${err.message} (${err.code})`);
630
- if (err.retryAfter) {
631
- console.error(` Retry in ${err.retryAfter}s.`);
632
- }
633
- }
634
- return;
635
- }
636
- const message = err instanceof Error ? err.message : String(err);
637
- if (json) {
638
- console.log(JSON.stringify({ error: { code: "cli_error", message } }));
639
- } else {
640
- console.error(`\u2717 ${message}`);
641
- }
642
- }
777
+ // src/dashboard.ts
778
+ import path6 from "path";
643
779
 
644
780
  // src/credentials.ts
645
781
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
646
782
  import { homedir } from "os";
647
783
  import path3 from "path";
648
- import { z as z11 } from "zod";
784
+ import { z as z13 } from "zod";
649
785
  var DEFAULT_API_URL = "https://artifact.cafe";
650
- var credentialsSchema = z11.object({
651
- keys: z11.record(z11.string(), z11.string()),
652
- version: z11.literal(1)
786
+ var credentialsSchema = z13.object({
787
+ keys: z13.record(z13.string(), z13.string()),
788
+ version: z13.literal(1)
653
789
  });
654
790
  function credentialsPath() {
655
791
  return path3.join(homedir(), ".artifactcafe", "credentials");
@@ -698,65 +834,6 @@ function resolveApiKey(apiUrl2, flag) {
698
834
  return flag ?? process.env.ARTIFACT_CAFE_API_KEY ?? storedApiKey(apiUrl2) ?? void 0;
699
835
  }
700
836
 
701
- // src/list.ts
702
- async function list(opts) {
703
- const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
704
- /\/$/,
705
- ""
706
- );
707
- const key = resolveApiKey(apiUrl2, opts.apiKey);
708
- if (!key) {
709
- throw new Error(
710
- `Listing your artifacts needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
711
- );
712
- }
713
- const data = await createApiClient(apiUrl2, key).listArtifacts();
714
- if (opts.json) {
715
- console.log(JSON.stringify(data, null, 2));
716
- return;
717
- }
718
- if (data.artifacts.length === 0) {
719
- console.log(
720
- "No artifacts in your account yet \u2014 publish while logged in, or claim one via its claim link."
721
- );
722
- return;
723
- }
724
- const plural = data.artifacts.length === 1 ? "" : "s";
725
- console.log(`${data.artifacts.length} artifact${plural}
726
- `);
727
- for (const artifact of data.artifacts) {
728
- printArtifact(artifact);
729
- }
730
- }
731
- function printArtifact(a) {
732
- const version = a.currentVersionNumber === null ? "no version" : `v${a.currentVersionNumber}`;
733
- const open2 = a.openCommentCount === 0 ? "no open comments" : `${a.openCommentCount} open comment${a.openCommentCount === 1 ? "" : "s"}`;
734
- const marker = a.openCommentCount > 0 ? "\u25CF" : "\u25CB";
735
- console.log(`${marker} ${a.title}`);
736
- console.log(` ${version} \xB7 ${open2} \xB7 updated ${relativeTime(a.updatedAt)}`);
737
- console.log(` ${a.url}
738
- `);
739
- }
740
- function relativeTime(iso) {
741
- const seconds = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1e3);
742
- if (seconds < 60) {
743
- return "just now";
744
- }
745
- if (seconds < 3600) {
746
- return `${Math.floor(seconds / 60)}m ago`;
747
- }
748
- if (seconds < 86400) {
749
- return `${Math.floor(seconds / 3600)}h ago`;
750
- }
751
- if (seconds < 30 * 86400) {
752
- return `${Math.floor(seconds / 86400)}d ago`;
753
- }
754
- return iso.slice(0, 10);
755
- }
756
-
757
- // src/login.ts
758
- import { createInterface } from "readline";
759
-
760
837
  // src/publish.ts
761
838
  import { exec } from "child_process";
762
839
  import { readFileSync as readFileSync4 } from "fs";
@@ -1053,7 +1130,103 @@ function openInBrowser(url) {
1053
1130
  });
1054
1131
  }
1055
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
+
1056
1228
  // src/login.ts
1229
+ import { createInterface } from "readline";
1057
1230
  function apiUrl() {
1058
1231
  return (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
1059
1232
  }
@@ -1167,9 +1340,9 @@ function logout(opts) {
1167
1340
  }
1168
1341
 
1169
1342
  // src/open.ts
1170
- import path6 from "path";
1343
+ import path7 from "path";
1171
1344
  function openArtifact(opts) {
1172
- const dir = path6.resolve(opts.dir);
1345
+ const dir = path7.resolve(opts.dir);
1173
1346
  const config = readConfig(dir);
1174
1347
  if (!config) {
1175
1348
  throw new Error(
@@ -1183,7 +1356,7 @@ function openArtifact(opts) {
1183
1356
  }
1184
1357
 
1185
1358
  // src/protect.ts
1186
- import path7 from "path";
1359
+ import path8 from "path";
1187
1360
  async function protect(opts) {
1188
1361
  if (opts.off && opts.password !== void 0) {
1189
1362
  throw new Error("Pass either --password or --off, not both.");
@@ -1200,7 +1373,7 @@ async function protect(opts) {
1200
1373
  throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
1201
1374
  }
1202
1375
  }
1203
- const dir = path7.resolve(opts.dir);
1376
+ const dir = path8.resolve(opts.dir);
1204
1377
  const config = readConfig(dir);
1205
1378
  if (!config) {
1206
1379
  throw new Error(
@@ -1357,10 +1530,19 @@ var comments2 = defineCommand({
1357
1530
  description: "Filter: open | resolved | all",
1358
1531
  type: "string"
1359
1532
  },
1533
+ timeout: {
1534
+ default: "900",
1535
+ description: "With --wait: give up after this many seconds (exit 2)",
1536
+ type: "string"
1537
+ },
1360
1538
  version: {
1361
1539
  default: "current",
1362
1540
  description: "Filter: current | all",
1363
1541
  type: "string"
1542
+ },
1543
+ wait: {
1544
+ description: "Block until a reviewer leaves new feedback",
1545
+ type: "boolean"
1364
1546
  }
1365
1547
  },
1366
1548
  meta: {
@@ -1369,11 +1551,19 @@ var comments2 = defineCommand({
1369
1551
  },
1370
1552
  async run({ args }) {
1371
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
+ }
1372
1560
  await comments({
1373
1561
  dir: args.dir,
1374
1562
  json: Boolean(args.json),
1375
1563
  status: args.status,
1376
- version: args.version
1564
+ timeout,
1565
+ version: args.version,
1566
+ wait: Boolean(args.wait)
1377
1567
  });
1378
1568
  } catch (err) {
1379
1569
  printCliError(err, Boolean(args.json));
@@ -1428,6 +1618,27 @@ var open = defineCommand({
1428
1618
  }
1429
1619
  }
1430
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
+ });
1431
1642
  var main = defineCommand({
1432
1643
  meta: {
1433
1644
  description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
@@ -1435,6 +1646,7 @@ var main = defineCommand({
1435
1646
  },
1436
1647
  subCommands: {
1437
1648
  comments: comments2,
1649
+ dashboard,
1438
1650
  list: list2,
1439
1651
  login: login2,
1440
1652
  logout: logout2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifact-cafe",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Publish AI-generated artifacts for review on artifact.cafe",
5
5
  "type": "module",
6
6
  "bin": {