artifact-cafe 0.0.5 → 0.0.8

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 +993 -145
  2. package/package.json +9 -9
package/dist/index.js CHANGED
@@ -112,8 +112,9 @@ var anchorSchema = z2.discriminatedUnion("type", [
112
112
  pointAnchorSchema,
113
113
  textAnchorSchema
114
114
  ]);
115
+ var displayNameSchema = z2.string().trim().min(1).max(64);
115
116
  var guestSchema = z2.object({
116
- displayName: z2.string().min(1).max(64),
117
+ displayName: displayNameSchema,
117
118
  guestId: z2.string().min(16)
118
119
  });
119
120
  var createCommentThreadSchema = z2.object({
@@ -130,8 +131,11 @@ var threadActionSchema = z2.object({
130
131
  guest: guestSchema.optional()
131
132
  });
132
133
  var commentAuthorSchema = z2.object({
134
+ avatarUrl: z2.string().nullable(),
133
135
  displayName: z2.string(),
134
- guestId: z2.string().nullable()
136
+ guestId: z2.string().nullable(),
137
+ isViewer: z2.boolean(),
138
+ kind: z2.enum(["member", "guest"])
135
139
  });
136
140
  var commentMessageSchema = z2.object({
137
141
  author: commentAuthorSchema,
@@ -158,12 +162,14 @@ var commentExportQuerySchema = z2.object({
158
162
  });
159
163
  var commentExportReplySchema = z2.object({
160
164
  author: z2.string(),
165
+ authorKind: z2.enum(["member", "guest"]),
161
166
  body: z2.string(),
162
167
  createdAt: z2.string()
163
168
  });
164
169
  var commentExportThreadSchema = z2.object({
165
170
  anchor: anchorSchema,
166
171
  author: z2.string(),
172
+ authorKind: z2.enum(["member", "guest"]),
167
173
  body: z2.string(),
168
174
  createdAt: z2.string(),
169
175
  id: z2.string(),
@@ -199,6 +205,17 @@ import { z as z4 } from "zod";
199
205
  var ownArtifactSchema = z4.object({
200
206
  createdAt: z4.string(),
201
207
  currentVersionNumber: z4.number().nullable(),
208
+ // Owned artifacts normally never expire; non-null only while an
209
+ // expiry is still set (e.g. an unclaimed publish surfaced here later).
210
+ expiresAt: z4.string().nullable(),
211
+ // SPEC-2 §8.4 — the containing folder for workspace-owned artifacts;
212
+ // both null for personal artifacts. `folderId` drives the dashboard's
213
+ // move control and folder filter; `folderName` is the display label.
214
+ folderId: z4.string().nullable(),
215
+ folderName: z4.string().nullable(),
216
+ // Whether a publish token is still active — revocable from the
217
+ // dashboard (§6.18); the token itself is never returned.
218
+ hasPublishToken: z4.boolean(),
202
219
  id: z4.string(),
203
220
  openThreads: z4.number(),
204
221
  protected: z4.boolean(),
@@ -212,64 +229,217 @@ var ownArtifactSchema = z4.object({
212
229
  var ownArtifactsResponseSchema = z4.object({
213
230
  artifacts: z4.array(ownArtifactSchema)
214
231
  });
232
+ var revokePublishTokenResponseSchema = z4.object({
233
+ revoked: z4.boolean()
234
+ });
215
235
 
216
236
  // ../shared/src/owner.ts
237
+ import { z as z6 } from "zod";
238
+
239
+ // ../shared/src/workspaces.ts
217
240
  import { z as z5 } from "zod";
218
- var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
219
- var claimResponseSchema = z5.object({
220
- artifactId: z5.string(),
241
+ var WORKSPACE_HANDLE_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])$/;
242
+ var workspaceRoleSchema = z5.enum(["owner", "admin", "member"]);
243
+ var workspaceInviteRoleSchema = z5.enum(["admin", "member"]);
244
+ var workspaceHandleSchema = z5.string().min(3).max(32).regex(WORKSPACE_HANDLE_PATTERN, {
245
+ message: "handle must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
246
+ });
247
+ var createWorkspaceSchema = z5.object({
248
+ handle: workspaceHandleSchema.optional(),
249
+ name: z5.string().trim().min(1).max(100)
250
+ });
251
+ var updateWorkspaceSchema = z5.object({
252
+ handle: workspaceHandleSchema.optional(),
253
+ name: z5.string().trim().min(1).max(100).optional()
254
+ }).refine((body) => body.handle !== void 0 || body.name !== void 0, {
255
+ message: "provide at least one of name or handle"
256
+ });
257
+ var workspaceSchema = z5.object({
258
+ createdAt: z5.string(),
259
+ handle: z5.string(),
260
+ id: z5.string(),
261
+ name: z5.string(),
262
+ role: workspaceRoleSchema.nullable(),
263
+ updatedAt: z5.string()
264
+ });
265
+ var workspaceListResponseSchema = z5.object({
266
+ workspaces: z5.array(workspaceSchema)
267
+ });
268
+ var deleteWorkspaceResponseSchema = z5.object({
269
+ deleted: z5.literal(true)
270
+ });
271
+ var workspaceMemberSchema = z5.object({
272
+ createdAt: z5.string(),
273
+ email: z5.string().email(),
274
+ image: z5.string().nullable(),
275
+ name: z5.string(),
276
+ role: workspaceRoleSchema,
277
+ userId: z5.string()
278
+ });
279
+ var workspaceMemberListResponseSchema = z5.object({
280
+ members: z5.array(workspaceMemberSchema)
281
+ });
282
+ var updateWorkspaceMemberSchema = z5.object({
283
+ role: workspaceRoleSchema
284
+ });
285
+ var removeWorkspaceMemberResponseSchema = z5.object({
286
+ removed: z5.literal(true)
287
+ });
288
+ var createWorkspaceInviteSchema = z5.object({
289
+ email: z5.string().trim().toLowerCase().email().optional(),
290
+ expiresAt: z5.string().datetime(),
291
+ maxUses: z5.number().int().positive().optional(),
292
+ role: workspaceInviteRoleSchema
293
+ });
294
+ var workspaceInviteSchema = z5.object({
295
+ createdAt: z5.string(),
296
+ email: z5.string().email().nullable(),
297
+ expiresAt: z5.string(),
298
+ id: z5.string(),
299
+ maxUses: z5.number().int().positive().nullable(),
300
+ role: workspaceInviteRoleSchema,
301
+ tokenPrefix: z5.string(),
302
+ useCount: z5.number().int().nonnegative()
303
+ });
304
+ var createdWorkspaceInviteSchema = workspaceInviteSchema.extend({
305
+ url: z5.string().url()
306
+ });
307
+ var workspaceInviteListResponseSchema = z5.object({
308
+ invites: z5.array(workspaceInviteSchema)
309
+ });
310
+ var revokeWorkspaceInviteResponseSchema = z5.object({
311
+ revoked: z5.literal(true)
312
+ });
313
+ var acceptWorkspaceInviteSchema = z5.object({
314
+ token: z5.string().min(1)
315
+ });
316
+ var acceptWorkspaceInviteResponseSchema = z5.object({
317
+ role: workspaceRoleSchema,
318
+ workspace: workspaceSchema
319
+ });
320
+ var folderVisibilitySchema = z5.enum(["public", "workspace"]);
321
+ var artifactVisibilitySchema = z5.enum([
322
+ "public",
323
+ "unlisted",
324
+ "workspace"
325
+ ]);
326
+ var createFolderSchema = z5.object({
327
+ name: z5.string().trim().min(1).max(100),
328
+ visibility: folderVisibilitySchema
329
+ });
330
+ var updateFolderSchema = z5.object({
331
+ // With `visibility`: bulk-update the folder's artifacts to match
332
+ // (SPEC-2 §3.4 "Changing a folder's visibility offers a bulk update").
333
+ cascade: z5.boolean().optional(),
334
+ name: z5.string().trim().min(1).max(100).optional(),
335
+ sortOrder: z5.number().int().min(0).optional(),
336
+ visibility: folderVisibilitySchema.optional()
337
+ }).refine(
338
+ (body) => body.name !== void 0 || body.visibility !== void 0 || body.sortOrder !== void 0,
339
+ { message: "provide at least one of name, visibility, sortOrder" }
340
+ ).refine((body) => !body.cascade || body.visibility !== void 0, {
341
+ message: "cascade requires visibility"
342
+ });
343
+ var folderSchema = z5.object({
344
+ createdAt: z5.string(),
345
+ id: z5.string(),
346
+ name: z5.string(),
347
+ // Permalink segment for the public folder page (/w/{handle}/f/{slug}).
348
+ // Server-derived from the name; renames regenerate it.
349
+ slug: z5.string(),
350
+ sortOrder: z5.number().int(),
351
+ updatedAt: z5.string(),
352
+ visibility: folderVisibilitySchema,
353
+ workspaceId: z5.string()
354
+ });
355
+ var folderListResponseSchema = z5.object({
356
+ folders: z5.array(folderSchema)
357
+ });
358
+ var deleteFolderResponseSchema = z5.object({
359
+ deleted: z5.literal(true)
360
+ });
361
+ var workspaceArtifactSchema = z5.object({
362
+ createdAt: z5.string(),
363
+ currentVersionNumber: z5.number().int().nullable(),
364
+ folderId: z5.string().nullable(),
365
+ id: z5.string(),
366
+ openCommentCount: z5.number().int().nonnegative(),
221
367
  slug: z5.string(),
222
368
  title: z5.string(),
223
- url: z5.string().url()
369
+ updatedAt: z5.string(),
370
+ url: z5.string().url(),
371
+ versionCount: z5.number().int().nonnegative(),
372
+ visibility: artifactVisibilitySchema
373
+ });
374
+ var workspaceArtifactListResponseSchema = z5.object({
375
+ artifacts: z5.array(workspaceArtifactSchema)
224
376
  });
225
- var updateArtifactSchema = z5.object({
226
- slug: z5.string().min(3).max(63).regex(SLUG_PATTERN, {
377
+
378
+ // ../shared/src/owner.ts
379
+ var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
380
+ var claimResponseSchema = z6.object({
381
+ artifactId: z6.string(),
382
+ slug: z6.string(),
383
+ title: z6.string(),
384
+ url: z6.string().url()
385
+ });
386
+ var updateArtifactSchema = z6.object({
387
+ // SPEC-2 §6.4 — placement + visibility. `folderId: null` clears the
388
+ // folder; `workspaceId: null` moves the artifact out of its workspace
389
+ // (§5.5 ownership transfer).
390
+ folderId: z6.string().min(1).nullable().optional(),
391
+ slug: z6.string().min(3).max(63).regex(SLUG_PATTERN, {
227
392
  message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
228
393
  }).optional(),
229
- title: z5.string().min(1).max(200).optional()
230
- }).refine((body) => body.title !== void 0 || body.slug !== void 0, {
231
- message: "provide at least one of title, slug"
232
- });
233
- var deleteArtifactResponseSchema = z5.object({
234
- deleted: z5.literal(true)
394
+ title: z6.string().min(1).max(200).optional(),
395
+ visibility: artifactVisibilitySchema.optional(),
396
+ workspaceId: z6.string().min(1).nullable().optional()
397
+ }).refine(
398
+ (body) => body.title !== void 0 || body.slug !== void 0 || body.visibility !== void 0 || body.folderId !== void 0 || body.workspaceId !== void 0,
399
+ {
400
+ message: "provide at least one of title, slug, visibility, folderId, workspaceId"
401
+ }
402
+ );
403
+ var deleteArtifactResponseSchema = z6.object({
404
+ deleted: z6.literal(true)
235
405
  });
236
406
 
237
407
  // ../shared/src/presence.ts
238
- import { z as z6 } from "zod";
408
+ import { z as z7 } from "zod";
239
409
  var AGENT_PRESENCE = {
240
410
  heartbeatSeconds: 15,
241
411
  shellPollSeconds: 15,
242
412
  ttlSeconds: 45
243
413
  };
244
- var agentHeartbeatResponseSchema = z6.object({
245
- listening: z6.literal(true),
246
- ttlSeconds: z6.number().int().positive()
414
+ var agentHeartbeatResponseSchema = z7.object({
415
+ listening: z7.literal(true),
416
+ ttlSeconds: z7.number().int().positive()
247
417
  });
248
- var agentPresenceSchema = z6.object({
249
- listening: z6.boolean(),
250
- versionNumber: z6.number().int().positive().nullable()
418
+ var agentPresenceSchema = z7.object({
419
+ listening: z7.boolean(),
420
+ versionNumber: z7.number().int().positive().nullable()
251
421
  });
252
422
 
253
423
  // ../shared/src/protection.ts
254
- import { z as z7 } from "zod";
424
+ import { z as z8 } from "zod";
255
425
  var PASSWORD_MIN = 6;
256
426
  var PASSWORD_MAX = 128;
257
- 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.`);
258
- var setProtectionSchema = z7.object({
427
+ var gatePasswordSchema = z8.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
428
+ var setProtectionSchema = z8.object({
259
429
  password: gatePasswordSchema.nullable()
260
430
  });
261
- var protectionResponseSchema = z7.object({
262
- protected: z7.boolean()
431
+ var protectionResponseSchema = z8.object({
432
+ protected: z8.boolean()
263
433
  });
264
- var unlockSchema = z7.object({
265
- password: z7.string().min(1).max(1024)
434
+ var unlockSchema = z8.object({
435
+ password: z8.string().min(1).max(1024)
266
436
  });
267
- var unlockResponseSchema = z7.object({
268
- unlocked: z7.literal(true)
437
+ var unlockResponseSchema = z8.object({
438
+ unlocked: z8.literal(true)
269
439
  });
270
440
 
271
441
  // ../shared/src/publish.ts
272
- import { z as z8 } from "zod";
442
+ import { z as z9 } from "zod";
273
443
  function isSafeArtifactPath(p) {
274
444
  if (p.length === 0 || p.length > 1024) {
275
445
  return false;
@@ -282,128 +452,136 @@ function isSafeArtifactPath(p) {
282
452
  }
283
453
  return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
284
454
  }
285
- var manifestFileSchema = z8.object({
286
- hash: z8.string().regex(/^sha256_[a-f0-9]{64}$/),
287
- mimeType: z8.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
288
- path: z8.string().min(1).max(1024).refine(isSafeArtifactPath, {
455
+ var manifestFileSchema = z9.object({
456
+ hash: z9.string().regex(/^sha256_[a-f0-9]{64}$/),
457
+ mimeType: z9.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
458
+ path: z9.string().min(1).max(1024).refine(isSafeArtifactPath, {
289
459
  message: "path must be a safe relative path"
290
460
  }),
291
- size: z8.number().int().positive()
461
+ size: z9.number().int().positive()
292
462
  });
293
- var publishSourceSchema = z8.object({
294
- agent: z8.string().optional(),
295
- runId: z8.string().optional(),
296
- type: z8.literal("cli")
463
+ var publishSourceSchema = z9.object({
464
+ agent: z9.string().optional(),
465
+ runId: z9.string().optional(),
466
+ type: z9.literal("cli")
297
467
  });
298
- var createPublishSessionSchema = z8.object({
299
- artifactId: z8.string().nullable(),
300
- entryPath: z8.string().optional(),
301
- files: z8.array(manifestFileSchema).min(1).max(1e3),
468
+ var createPublishSessionSchema = z9.object({
469
+ artifactId: z9.string().nullable(),
470
+ entryPath: z9.string().optional(),
471
+ files: z9.array(manifestFileSchema).min(1).max(1e3),
472
+ // SPEC-2 §6.4 — workspace placement is available only to authenticated
473
+ // members; the API enforces membership and folder ownership.
474
+ folderId: z9.string().min(1).optional(),
302
475
  source: publishSourceSchema,
303
- title: z8.string().max(200).optional()
304
- });
305
- var publishSessionResponseSchema = z8.object({
306
- artifactId: z8.string(),
307
- expiresAt: z8.string(),
308
- filesToUpload: z8.array(
309
- z8.object({
310
- method: z8.literal("PUT"),
311
- path: z8.string(),
312
- uploadUrl: z8.string().url()
476
+ title: z9.string().max(200).optional(),
477
+ workspaceId: z9.string().min(1).optional()
478
+ });
479
+ var publishSessionResponseSchema = z9.object({
480
+ artifactId: z9.string(),
481
+ expiresAt: z9.string(),
482
+ filesToUpload: z9.array(
483
+ z9.object({
484
+ method: z9.literal("PUT"),
485
+ path: z9.string(),
486
+ uploadUrl: z9.string().url()
313
487
  })
314
488
  ),
315
- publishSessionId: z8.string(),
316
- skippedFiles: z8.array(z8.string()),
317
- versionId: z8.string()
489
+ publishSessionId: z9.string(),
490
+ skippedFiles: z9.array(z9.string()),
491
+ versionId: z9.string()
318
492
  });
319
- var finalizeResponseSchema = z8.object({
320
- artifactId: z8.string(),
493
+ var finalizeResponseSchema = z9.object({
494
+ artifactId: z9.string(),
321
495
  // API-key publishes create the artifact already claimed (SPEC §5.4) —
322
496
  // the CLI uses this to tell "claim URL pending" from "already yours".
323
- claimed: z8.boolean(),
497
+ claimed: z9.boolean(),
324
498
  // Returned only on first publish of a new artifact (one-time).
325
- claimUrl: z8.string().url().optional(),
326
- publishToken: z8.string().optional(),
327
- url: z8.string().url(),
328
- versionId: z8.string(),
329
- versionNumber: z8.number().int().positive()
330
- });
331
- var artifactResponseSchema = z8.object({
332
- claimed: z8.boolean(),
333
- currentVersion: z8.object({
334
- entryUrl: z8.string().url(),
335
- id: z8.string(),
336
- versionNumber: z8.number().int().positive()
499
+ claimUrl: z9.string().url().optional(),
500
+ publishToken: z9.string().optional(),
501
+ url: z9.string().url(),
502
+ versionId: z9.string(),
503
+ versionNumber: z9.number().int().positive()
504
+ });
505
+ var artifactResponseSchema = z9.object({
506
+ claimed: z9.boolean(),
507
+ currentVersion: z9.object({
508
+ entryUrl: z9.string().url(),
509
+ id: z9.string(),
510
+ versionNumber: z9.number().int().positive()
337
511
  }).nullable(),
338
- id: z8.string(),
339
- slug: z8.string(),
340
- title: z8.string()
512
+ // SPEC-2 §6.4 — placement + visibility surface on the artifact.
513
+ folderId: z9.string().nullable(),
514
+ id: z9.string(),
515
+ slug: z9.string(),
516
+ title: z9.string(),
517
+ visibility: z9.enum(["public", "unlisted", "workspace"]),
518
+ workspaceId: z9.string().nullable()
341
519
  });
342
520
 
343
521
  // ../shared/src/report.ts
344
- import { z as z9 } from "zod";
345
- var reportRequestSchema = z9.object({
346
- reason: z9.string().max(1e3).optional()
522
+ import { z as z10 } from "zod";
523
+ var reportRequestSchema = z10.object({
524
+ reason: z10.string().max(1e3).optional()
347
525
  });
348
- var reportResponseSchema = z9.object({
349
- reported: z9.literal(true)
526
+ var reportResponseSchema = z10.object({
527
+ reported: z10.literal(true)
350
528
  });
351
529
 
352
530
  // ../shared/src/stats.ts
353
- import { z as z10 } from "zod";
354
- var avgViewSecondsSchema = z10.number().int().nonnegative().nullable();
355
- var versionViewStatsSchema = z10.object({
531
+ import { z as z11 } from "zod";
532
+ var avgViewSecondsSchema = z11.number().int().nonnegative().nullable();
533
+ var versionViewStatsSchema = z11.object({
356
534
  avgViewSeconds: avgViewSecondsSchema,
357
535
  // Views from inside the review shell's iframe vs. direct visits to
358
536
  // the render URL. views = embeddedViews + directViews.
359
- directViews: z10.number().int().nonnegative(),
360
- embeddedViews: z10.number().int().nonnegative(),
537
+ directViews: z11.number().int().nonnegative(),
538
+ embeddedViews: z11.number().int().nonnegative(),
361
539
  // Distinct hashed viewer IPs across the version's lifetime.
362
- uniqueViewers: z10.number().int().nonnegative(),
363
- versionNumber: z10.number().int().positive(),
364
- views: z10.number().int().nonnegative()
540
+ uniqueViewers: z11.number().int().nonnegative(),
541
+ versionNumber: z11.number().int().positive(),
542
+ views: z11.number().int().nonnegative()
365
543
  });
366
- var dailyViewsSchema = z10.object({
544
+ var dailyViewsSchema = z11.object({
367
545
  avgViewSeconds: avgViewSecondsSchema,
368
546
  // UTC day, YYYY-MM-DD.
369
- day: z10.string(),
370
- uniqueViewers: z10.number().int().nonnegative(),
371
- views: z10.number().int().nonnegative()
547
+ day: z11.string(),
548
+ uniqueViewers: z11.number().int().nonnegative(),
549
+ views: z11.number().int().nonnegative()
372
550
  });
373
- var artifactStatsResponseSchema = z10.object({
551
+ var artifactStatsResponseSchema = z11.object({
374
552
  avgViewSeconds: avgViewSecondsSchema,
375
- daily: z10.array(dailyViewsSchema),
553
+ daily: z11.array(dailyViewsSchema),
376
554
  // Distinct hashed viewer IPs across all versions and days.
377
- totalUniqueViewers: z10.number().int().nonnegative(),
378
- totalViews: z10.number().int().nonnegative(),
379
- versions: z10.array(versionViewStatsSchema)
555
+ totalUniqueViewers: z11.number().int().nonnegative(),
556
+ totalViews: z11.number().int().nonnegative(),
557
+ versions: z11.array(versionViewStatsSchema)
380
558
  });
381
559
 
382
560
  // ../shared/src/versions.ts
383
- import { z as z11 } from "zod";
384
- var versionSourceSchema = z11.object({
385
- agentName: z11.string().nullable(),
386
- type: z11.string()
561
+ import { z as z12 } from "zod";
562
+ var versionSourceSchema = z12.object({
563
+ agentName: z12.string().nullable(),
564
+ type: z12.string()
387
565
  });
388
- var versionChangesSchema = z11.object({
389
- added: z11.array(z11.string()),
390
- changed: z11.array(z11.string()),
391
- removed: z11.array(z11.string())
566
+ var versionChangesSchema = z12.object({
567
+ added: z12.array(z12.string()),
568
+ changed: z12.array(z12.string()),
569
+ removed: z12.array(z12.string())
392
570
  });
393
- var versionSummarySchema = z11.object({
571
+ var versionSummarySchema = z12.object({
394
572
  // Absent for the first version — there is no previous version to diff against.
395
573
  changes: versionChangesSchema.nullable(),
396
- createdAt: z11.string(),
397
- entryUrl: z11.string().url(),
398
- fileCount: z11.number().int().nonnegative(),
399
- id: z11.string(),
400
- isCurrent: z11.boolean(),
574
+ createdAt: z12.string(),
575
+ entryUrl: z12.string().url(),
576
+ fileCount: z12.number().int().nonnegative(),
577
+ id: z12.string(),
578
+ isCurrent: z12.boolean(),
401
579
  source: versionSourceSchema,
402
- totalSizeBytes: z11.number().int().nonnegative(),
403
- versionNumber: z11.number().int().positive()
580
+ totalSizeBytes: z12.number().int().nonnegative(),
581
+ versionNumber: z12.number().int().positive()
404
582
  });
405
- var versionListResponseSchema = z11.object({
406
- versions: z11.array(versionSummarySchema)
583
+ var versionListResponseSchema = z12.object({
584
+ versions: z12.array(versionSummarySchema)
407
585
  });
408
586
 
409
587
  // src/api.ts
@@ -480,7 +658,7 @@ function createApiClient(apiUrl2, publishToken) {
480
658
  });
481
659
  return request(
482
660
  `${base}/api/v1/artifacts/${artifactId}/comments/export?${params}`,
483
- { method: "GET" },
661
+ { headers, method: "GET" },
484
662
  (d) => commentExportSchema.parse(d)
485
663
  );
486
664
  },
@@ -494,7 +672,7 @@ function createApiClient(apiUrl2, publishToken) {
494
672
  async getArtifact(artifactId) {
495
673
  return request(
496
674
  `${base}/api/v1/artifacts/${artifactId}`,
497
- { method: "GET" },
675
+ { headers, method: "GET" },
498
676
  (d) => artifactResponseSchema.parse(d)
499
677
  );
500
678
  },
@@ -507,6 +685,20 @@ function createApiClient(apiUrl2, publishToken) {
507
685
  (d) => artifactListResponseSchema.parse(d)
508
686
  );
509
687
  },
688
+ async listFolders(workspaceId) {
689
+ return request(
690
+ `${base}/api/v1/workspaces/${workspaceId}/folders`,
691
+ { headers, method: "GET" },
692
+ (d) => folderListResponseSchema.parse(d)
693
+ );
694
+ },
695
+ async listWorkspaces() {
696
+ return request(
697
+ `${base}/api/v1/workspaces`,
698
+ { headers, method: "GET" },
699
+ (d) => workspaceListResponseSchema.parse(d)
700
+ );
701
+ },
510
702
  // SPEC §6.14 — who the presented credential belongs to. Used by
511
703
  // `login` to verify a pasted key and by `whoami`.
512
704
  async me() {
@@ -516,6 +708,34 @@ function createApiClient(apiUrl2, publishToken) {
516
708
  (d) => meResponseSchema.parse(d)
517
709
  );
518
710
  },
711
+ // SPEC §6.7 — reopen a resolved thread. Same auth as resolve.
712
+ async reopenThread(threadId) {
713
+ return request(
714
+ `${base}/api/v1/comment-threads/${threadId}/reopen`,
715
+ { body: "{}", headers, method: "POST" },
716
+ (d) => commentThreadSchema.parse(d)
717
+ );
718
+ },
719
+ // SPEC §6.5 — post a reply into a thread. Authorized by this client's
720
+ // credential: the artifact's publish token (the reply renders as the
721
+ // agent) or the owner's account key (renders as the owner). No guest
722
+ // payload — the credential is the identity.
723
+ async replyThread(threadId, body) {
724
+ return request(
725
+ `${base}/api/v1/comment-threads/${threadId}/messages`,
726
+ { body: JSON.stringify({ body }), headers, method: "POST" },
727
+ (d) => commentThreadSchema.parse(d)
728
+ );
729
+ },
730
+ // SPEC §6.6 — mark a thread resolved. Authorized by this client's
731
+ // credential: the artifact's publish token, or the owner's account key.
732
+ async resolveThread(threadId) {
733
+ return request(
734
+ `${base}/api/v1/comment-threads/${threadId}/resolve`,
735
+ { body: "{}", headers, method: "POST" },
736
+ (d) => commentThreadSchema.parse(d)
737
+ );
738
+ },
519
739
  // SPEC §6.16 — set a password (string) or clear it (null). Authorized
520
740
  // by the publish token this client was created with.
521
741
  async setProtection(artifactId, password) {
@@ -552,17 +772,17 @@ import {
552
772
  writeFileSync as writeFileSync2
553
773
  } from "fs";
554
774
  import path2 from "path";
555
- import { z as z13 } from "zod";
775
+ import { z as z14 } from "zod";
556
776
 
557
777
  // src/credentials.ts
558
778
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
559
779
  import { homedir } from "os";
560
780
  import path from "path";
561
- import { z as z12 } from "zod";
781
+ import { z as z13 } from "zod";
562
782
  var DEFAULT_API_URL = "https://artifact.cafe";
563
- var credentialsSchema = z12.object({
564
- keys: z12.record(z12.string(), z12.string()),
565
- version: z12.literal(1)
783
+ var credentialsSchema = z13.object({
784
+ keys: z13.record(z13.string(), z13.string()),
785
+ version: z13.literal(1)
566
786
  });
567
787
  function credentialsPath() {
568
788
  return path.join(homedir(), ".artifactcafe", "credentials");
@@ -612,15 +832,19 @@ function resolveApiKey(apiUrl2, flag) {
612
832
  }
613
833
 
614
834
  // src/config.ts
615
- var configSchema = z13.object({
616
- apiUrl: z13.string(),
617
- artifactId: z13.string(),
835
+ var configSchema = z14.object({
836
+ apiUrl: z14.string(),
837
+ artifactId: z14.string(),
838
+ folder: z14.string().optional(),
839
+ folderId: z14.string().optional(),
618
840
  // Optional: a folder linked by `link` to an artifact you own carries no
619
841
  // token — owner actions authorize via your stored login instead. A first
620
842
  // publish still writes the anonymous-mode token here.
621
- publishToken: z13.string().optional(),
622
- slug: z13.string(),
623
- url: z13.string()
843
+ publishToken: z14.string().optional(),
844
+ slug: z14.string(),
845
+ url: z14.string(),
846
+ workspace: z14.string().optional(),
847
+ workspaceId: z14.string().optional()
624
848
  });
625
849
  var CONFIG_DIR = ".artifactcafe";
626
850
  var CONFIG_FILE = "config.json";
@@ -874,8 +1098,8 @@ function printThread(t) {
874
1098
  console.log(` \u21B3 at ${t.anchor.elementPath}`);
875
1099
  }
876
1100
  console.log(indent(t.body, " "));
877
- for (const reply of t.replies) {
878
- console.log(indent(`${reply.author}: ${reply.body}`, " "));
1101
+ for (const reply2 of t.replies) {
1102
+ console.log(indent(`${reply2.author}: ${reply2.body}`, " "));
879
1103
  }
880
1104
  console.log("");
881
1105
  }
@@ -1029,6 +1253,7 @@ async function publish(opts) {
1029
1253
  const token = (artifactId !== null && artifactId === config?.artifactId ? config.publishToken : void 0) ?? process.env.ARTIFACT_CAFE_TOKEN;
1030
1254
  const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
1031
1255
  const apiKey = resolveApiKey(apiUrl2, opts.apiKey);
1256
+ const placement = await resolvePlacement({ apiKey, apiUrl: apiUrl2, config, opts });
1032
1257
  const { files, skipped } = scanDirectory(dir);
1033
1258
  if (files.length === 0) {
1034
1259
  throw new Error(`Nothing to publish in ${dir} (after ignore rules).`);
@@ -1046,12 +1271,14 @@ async function publish(opts) {
1046
1271
  artifactId,
1047
1272
  entryPath,
1048
1273
  files: files.map(({ absPath: _absPath, ...manifest }) => manifest),
1274
+ ...placement.folderId ? { folderId: placement.folderId } : {},
1049
1275
  source: {
1050
1276
  agent: process.env.ARTIFACT_CAFE_AGENT,
1051
1277
  runId: process.env.ARTIFACT_CAFE_RUN_ID,
1052
1278
  type: "cli"
1053
1279
  },
1054
- title: opts.title
1280
+ title: opts.title,
1281
+ ...placement.workspaceId ? { workspaceId: placement.workspaceId } : {}
1055
1282
  };
1056
1283
  let credential = apiKey ?? token;
1057
1284
  let client = createApiClient(apiUrl2, credential);
@@ -1083,13 +1310,19 @@ async function publish(opts) {
1083
1310
  );
1084
1311
  });
1085
1312
  const result = await client.finalize(session.publishSessionId);
1086
- if (result.publishToken) {
1313
+ const existingToken = config?.artifactId === result.artifactId ? config.publishToken : void 0;
1314
+ const shouldWriteConfig = result.publishToken !== void 0 || config?.artifactId === result.artifactId || opts.workspace !== void 0 || opts.folder !== void 0;
1315
+ if (shouldWriteConfig) {
1087
1316
  writeConfig(dir, {
1088
1317
  apiUrl: apiUrl2,
1089
1318
  artifactId: result.artifactId,
1090
- publishToken: result.publishToken,
1319
+ ...placement.folder ? { folder: placement.folder } : {},
1320
+ ...placement.folderId ? { folderId: placement.folderId } : {},
1321
+ ...result.publishToken ?? existingToken ? { publishToken: result.publishToken ?? existingToken } : {},
1091
1322
  slug: slugFromUrl(result.url),
1092
- url: result.url
1323
+ url: result.url,
1324
+ ...placement.workspace ? { workspace: placement.workspace } : {},
1325
+ ...placement.workspaceId ? { workspaceId: placement.workspaceId } : {}
1093
1326
  });
1094
1327
  }
1095
1328
  if (opts.password !== void 0) {
@@ -1105,8 +1338,12 @@ async function publish(opts) {
1105
1338
  {
1106
1339
  artifactId: result.artifactId,
1107
1340
  claimed: result.claimed,
1341
+ folder: placement.folder ?? null,
1342
+ folderId: placement.folderId ?? null,
1108
1343
  url: result.url,
1109
1344
  versionNumber: result.versionNumber,
1345
+ workspace: placement.workspace ?? null,
1346
+ workspaceId: placement.workspaceId ?? null,
1110
1347
  ...result.claimUrl ? { claimUrl: result.claimUrl } : {},
1111
1348
  ...opts.password === void 0 ? {} : { protected: true },
1112
1349
  filesUploaded: session.filesToUpload.map((f) => f.path),
@@ -1148,6 +1385,76 @@ ${result.claimUrl}
1148
1385
  openInBrowser(result.url);
1149
1386
  }
1150
1387
  }
1388
+ async function resolvePlacement(params) {
1389
+ const { apiKey, apiUrl: apiUrl2, config, opts } = params;
1390
+ const hasPlacementFlag = opts.workspace !== void 0 || opts.folder !== void 0;
1391
+ if (!hasPlacementFlag && config?.workspaceId) {
1392
+ if (!apiKey) {
1393
+ throw new Error(
1394
+ `Publishing into workspace ${config.workspace ?? config.workspaceId} needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
1395
+ );
1396
+ }
1397
+ return {
1398
+ ...config.folder ? { folder: config.folder } : {},
1399
+ ...config.folderId ? { folderId: config.folderId } : {},
1400
+ ...config.workspace ? { workspace: config.workspace } : {},
1401
+ workspaceId: config.workspaceId
1402
+ };
1403
+ }
1404
+ if (!hasPlacementFlag) {
1405
+ return {};
1406
+ }
1407
+ if (!apiKey) {
1408
+ throw new Error(
1409
+ "Publishing into a workspace needs an account \u2014 run `artifact-cafe login` first."
1410
+ );
1411
+ }
1412
+ const workspaceHandle = opts.workspace ?? config?.workspace;
1413
+ if (!workspaceHandle) {
1414
+ throw new Error("--folder requires --workspace or a stored workspace.");
1415
+ }
1416
+ const client = createApiClient(apiUrl2, apiKey);
1417
+ let workspaceId;
1418
+ if (opts.workspace === void 0 && config?.workspaceId) {
1419
+ ({ workspaceId } = config);
1420
+ } else {
1421
+ const data2 = await client.listWorkspaces();
1422
+ const workspace = data2.workspaces.find(
1423
+ (candidate) => candidate.handle === workspaceHandle
1424
+ );
1425
+ if (!workspace) {
1426
+ throw new Error(
1427
+ `No workspace with handle "${workspaceHandle}" belongs to this account.`
1428
+ );
1429
+ }
1430
+ workspaceId = workspace.id;
1431
+ }
1432
+ const folderName = opts.workspace !== void 0 && opts.folder === void 0 ? void 0 : opts.folder ?? config?.folder;
1433
+ if (!folderName) {
1434
+ return { workspace: workspaceHandle, workspaceId };
1435
+ }
1436
+ const data = await client.listFolders(workspaceId);
1437
+ const matches = data.folders.filter(
1438
+ (candidate) => candidate.name === folderName || candidate.id === folderName
1439
+ );
1440
+ if (matches.length === 0) {
1441
+ throw new Error(
1442
+ `No folder named "${folderName}" exists in workspace ${workspaceHandle}.`
1443
+ );
1444
+ }
1445
+ if (matches.length > 1) {
1446
+ throw new Error(
1447
+ `More than one folder is named "${folderName}" in workspace ${workspaceHandle}; pass the folder id instead.`
1448
+ );
1449
+ }
1450
+ const folder = matches[0];
1451
+ return {
1452
+ folder: folder.name,
1453
+ folderId: folder.id,
1454
+ workspace: workspaceHandle,
1455
+ workspaceId
1456
+ };
1457
+ }
1151
1458
  async function inBatches(items, concurrency, fn) {
1152
1459
  let next = 0;
1153
1460
  const workers = Array.from(
@@ -1210,6 +1517,11 @@ function printCliError(err, json) {
1210
1517
  if (err.retryAfter) {
1211
1518
  console.error(` Retry in ${err.retryAfter}s.`);
1212
1519
  }
1520
+ if (err.code === "password_required") {
1521
+ console.error(
1522
+ " Run `artifact-cafe login` as the owner, or run from the published folder (its publish token unlocks it)."
1523
+ );
1524
+ }
1213
1525
  }
1214
1526
  return;
1215
1527
  }
@@ -1221,6 +1533,52 @@ function printCliError(err, json) {
1221
1533
  }
1222
1534
  }
1223
1535
 
1536
+ // src/folders.ts
1537
+ async function folders(opts) {
1538
+ const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
1539
+ /\/$/,
1540
+ ""
1541
+ );
1542
+ const key = resolveApiKey(apiUrl2, opts.apiKey);
1543
+ if (!key) {
1544
+ throw new Error(
1545
+ `Listing folders needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
1546
+ );
1547
+ }
1548
+ const client = createApiClient(apiUrl2, key);
1549
+ const workspaceData = await client.listWorkspaces();
1550
+ const workspace = workspaceData.workspaces.find(
1551
+ (candidate) => candidate.handle === opts.workspace
1552
+ );
1553
+ if (!workspace) {
1554
+ throw new Error(
1555
+ `No workspace with handle "${opts.workspace}" belongs to this account.`
1556
+ );
1557
+ }
1558
+ const data = await client.listFolders(workspace.id);
1559
+ if (opts.json) {
1560
+ console.log(JSON.stringify(data, null, 2));
1561
+ return;
1562
+ }
1563
+ if (data.folders.length === 0) {
1564
+ console.log(`No folders in workspace ${workspace.handle}.`);
1565
+ return;
1566
+ }
1567
+ const plural = data.folders.length === 1 ? "" : "s";
1568
+ console.log(
1569
+ `${data.folders.length} folder${plural} in ${workspace.handle}
1570
+ `
1571
+ );
1572
+ for (const folder of data.folders) {
1573
+ printFolder(folder);
1574
+ }
1575
+ }
1576
+ function printFolder(folder) {
1577
+ console.log(`${folder.name} (${folder.visibility})`);
1578
+ console.log(` ${folder.id}
1579
+ `);
1580
+ }
1581
+
1224
1582
  // src/link.ts
1225
1583
  import path7 from "path";
1226
1584
  import { createInterface } from "readline";
@@ -1303,7 +1661,7 @@ function pickArtifact(artifacts) {
1303
1661
  );
1304
1662
  });
1305
1663
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1306
- return new Promise((resolve, reject) => {
1664
+ return new Promise((resolve2, reject) => {
1307
1665
  rl.question(`
1308
1666
  Link which? [1-${artifacts.length}]: `, (answer) => {
1309
1667
  rl.close();
@@ -1312,7 +1670,7 @@ Link which? [1-${artifacts.length}]: `, (answer) => {
1312
1670
  reject(new Error(`Enter a number between 1 and ${artifacts.length}.`));
1313
1671
  return;
1314
1672
  }
1315
- resolve(artifacts[n - 1]);
1673
+ resolve2(artifacts[n - 1]);
1316
1674
  });
1317
1675
  });
1318
1676
  }
@@ -1379,7 +1737,7 @@ function apiUrl() {
1379
1737
  return (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
1380
1738
  }
1381
1739
  function promptSecret(question) {
1382
- return new Promise((resolve) => {
1740
+ return new Promise((resolve2) => {
1383
1741
  const rl = createInterface2({
1384
1742
  input: process.stdin,
1385
1743
  output: process.stdout
@@ -1394,14 +1752,14 @@ function promptSecret(question) {
1394
1752
  rl.question(question, (answer) => {
1395
1753
  rl.close();
1396
1754
  process.stdout.write("\n");
1397
- resolve(answer);
1755
+ resolve2(answer);
1398
1756
  });
1399
1757
  muted = true;
1400
1758
  });
1401
1759
  }
1402
1760
  async function login(opts) {
1403
1761
  const base = apiUrl();
1404
- const keysUrl = `${base}/cli`;
1762
+ const keysUrl = `${base}/dashboard?view=api-keys`;
1405
1763
  let key = opts.apiKey?.trim();
1406
1764
  if (!key) {
1407
1765
  if (!opts.json) {
@@ -1483,7 +1841,7 @@ function logout(opts) {
1483
1841
  }
1484
1842
  console.log(`\u2713 Logged out of ${apiOrigin(base)} on this machine.`);
1485
1843
  console.log(
1486
- `The key itself is still active \u2014 revoke it at ${base}/cli if this machine is shared.`
1844
+ `The key itself is still active \u2014 revoke it at ${base}/dashboard?view=api-keys if this machine is shared.`
1487
1845
  );
1488
1846
  }
1489
1847
 
@@ -1549,6 +1907,268 @@ async function protect(opts) {
1549
1907
  );
1550
1908
  }
1551
1909
 
1910
+ // src/reply.ts
1911
+ import path10 from "path";
1912
+ async function readStdin() {
1913
+ if (process.stdin.isTTY) {
1914
+ return "";
1915
+ }
1916
+ const chunks = [];
1917
+ for await (const chunk of process.stdin) {
1918
+ chunks.push(chunk);
1919
+ }
1920
+ return Buffer.concat(chunks).toString("utf8");
1921
+ }
1922
+ async function replyToThread(opts) {
1923
+ const threadId = opts.thread?.trim();
1924
+ if (!threadId) {
1925
+ throw new Error(
1926
+ "Pass the thread id to reply to \u2014 get it from `artifact-cafe comments --json`."
1927
+ );
1928
+ }
1929
+ const body = (opts.body ?? await readStdin()).trim();
1930
+ if (!body) {
1931
+ throw new Error(
1932
+ 'Nothing to post. Pass the reply with --body "\u2026", or pipe it on stdin.'
1933
+ );
1934
+ }
1935
+ const dir = path10.resolve(opts.dir);
1936
+ const target = resolveArtifactTarget(dir, {
1937
+ apiKey: opts.apiKey,
1938
+ artifact: opts.artifact
1939
+ });
1940
+ const client = createApiClient(target.apiUrl, target.credential);
1941
+ let thread = await client.replyThread(threadId, body);
1942
+ if (opts.resolve) {
1943
+ thread = await client.resolveThread(threadId);
1944
+ }
1945
+ if (opts.json) {
1946
+ console.log(
1947
+ JSON.stringify(
1948
+ {
1949
+ id: thread.id,
1950
+ messageCount: thread.messages.length,
1951
+ status: thread.status
1952
+ },
1953
+ null,
1954
+ 2
1955
+ )
1956
+ );
1957
+ return;
1958
+ }
1959
+ const closed = opts.resolve ? " and resolved it" : "";
1960
+ console.log(`\u2713 Replied to ${thread.id}${closed} \u2014 now ${thread.status}.`);
1961
+ }
1962
+
1963
+ // src/resolve.ts
1964
+ import path11 from "path";
1965
+ async function changeThreadStatus(opts) {
1966
+ const threadId = opts.thread?.trim();
1967
+ if (!threadId) {
1968
+ throw new Error(
1969
+ `Pass the thread id to ${opts.action} \u2014 get it from \`artifact-cafe comments --json\`.`
1970
+ );
1971
+ }
1972
+ const dir = path11.resolve(opts.dir);
1973
+ const target = resolveArtifactTarget(dir, {
1974
+ apiKey: opts.apiKey,
1975
+ artifact: opts.artifact
1976
+ });
1977
+ const client = createApiClient(target.apiUrl, target.credential);
1978
+ const thread = opts.action === "resolve" ? await client.resolveThread(threadId) : await client.reopenThread(threadId);
1979
+ if (opts.json) {
1980
+ console.log(
1981
+ JSON.stringify({ id: thread.id, status: thread.status }, null, 2)
1982
+ );
1983
+ return;
1984
+ }
1985
+ const verb = opts.action === "resolve" ? "Resolved" : "Reopened";
1986
+ console.log(`\u2713 ${verb} ${thread.id} \u2014 now ${thread.status}.`);
1987
+ }
1988
+
1989
+ // src/update.ts
1990
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1991
+ import { createRequire } from "module";
1992
+ import { homedir as homedir2 } from "os";
1993
+ import path12 from "path";
1994
+ import { z as z15 } from "zod";
1995
+ var REGISTRY_LATEST_URL = "https://registry.npmjs.org/artifact-cafe/latest";
1996
+ var SKILL_SOURCE_URL = "https://raw.githubusercontent.com/Artifact-Cafe/skill/main/artifact-cafe/SKILL.md";
1997
+ var SKILL_UPDATE_COMMAND = "npx skills add artifact-cafe/skill --skill artifact-cafe -g";
1998
+ var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
1999
+ var FETCH_TIMEOUT_MS = 2e3;
2000
+ var SKILL_VERSION_PATTERN = /\*\*Skill version:\s*(\d+\.\d+\.\d+)\*\*/;
2001
+ var cacheSchema = z15.object({
2002
+ checkedAt: z15.number(),
2003
+ cliLatest: z15.string().nullable(),
2004
+ skillLatest: z15.string().nullable(),
2005
+ version: z15.literal(1)
2006
+ });
2007
+ function cliVersion() {
2008
+ const pkg = createRequire(import.meta.url)("../package.json");
2009
+ return pkg.version;
2010
+ }
2011
+ function cachePath() {
2012
+ return path12.join(homedir2(), ".artifactcafe", "update-check.json");
2013
+ }
2014
+ function readCache() {
2015
+ try {
2016
+ return cacheSchema.parse(JSON.parse(readFileSync5(cachePath(), "utf8")));
2017
+ } catch {
2018
+ return null;
2019
+ }
2020
+ }
2021
+ function writeCache(cache) {
2022
+ try {
2023
+ mkdirSync3(path12.dirname(cachePath()), { mode: 448, recursive: true });
2024
+ writeFileSync3(cachePath(), `${JSON.stringify(cache, null, 2)}
2025
+ `);
2026
+ } catch {
2027
+ }
2028
+ }
2029
+ function isNewerVersion(candidate, current) {
2030
+ const parse = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
2031
+ const a = parse(candidate);
2032
+ const b = parse(current);
2033
+ for (let i = 0; i < 3; i++) {
2034
+ if ((a[i] ?? 0) !== (b[i] ?? 0)) {
2035
+ return (a[i] ?? 0) > (b[i] ?? 0);
2036
+ }
2037
+ }
2038
+ return false;
2039
+ }
2040
+ async function fetchJson(url) {
2041
+ const res = await fetch(url, {
2042
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2043
+ });
2044
+ if (!res.ok) {
2045
+ throw new Error(`GET ${url} \u2192 ${res.status}`);
2046
+ }
2047
+ return res.json();
2048
+ }
2049
+ async function fetchText(url) {
2050
+ const res = await fetch(url, {
2051
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2052
+ });
2053
+ if (!res.ok) {
2054
+ throw new Error(`GET ${url} \u2192 ${res.status}`);
2055
+ }
2056
+ return res.text();
2057
+ }
2058
+ async function fetchLatestVersions(previous) {
2059
+ const [cli, skill] = await Promise.all([
2060
+ fetchJson(REGISTRY_LATEST_URL).then((body) => z15.object({ version: z15.string() }).parse(body).version).catch(() => null),
2061
+ fetchText(SKILL_SOURCE_URL).then((body) => SKILL_VERSION_PATTERN.exec(body)?.[1] ?? null).catch(() => null)
2062
+ ]);
2063
+ return {
2064
+ checkedAt: Date.now(),
2065
+ cliLatest: cli ?? previous?.cliLatest ?? null,
2066
+ skillLatest: skill ?? previous?.skillLatest ?? null,
2067
+ version: 1
2068
+ };
2069
+ }
2070
+ function installedSkillVersions() {
2071
+ const roots = [homedir2(), process.cwd()];
2072
+ const dirs = [".agents", ".claude"];
2073
+ const versions = [];
2074
+ for (const root of roots) {
2075
+ for (const dir of dirs) {
2076
+ const file = path12.join(root, dir, "skills", "artifact-cafe", "SKILL.md");
2077
+ if (!existsSync3(file)) {
2078
+ continue;
2079
+ }
2080
+ try {
2081
+ const found = SKILL_VERSION_PATTERN.exec(readFileSync5(file, "utf8"));
2082
+ if (found?.[1]) {
2083
+ versions.push(found[1]);
2084
+ }
2085
+ } catch {
2086
+ }
2087
+ }
2088
+ }
2089
+ return versions;
2090
+ }
2091
+ function updateChecksDisabled() {
2092
+ return Boolean(
2093
+ process.env.ARTIFACT_CAFE_NO_UPDATE_CHECK ?? process.env.NO_UPDATE_NOTIFIER ?? process.env.CI
2094
+ );
2095
+ }
2096
+ function startUpdateCheck() {
2097
+ if (updateChecksDisabled()) {
2098
+ return () => Promise.resolve();
2099
+ }
2100
+ const pending = (async () => {
2101
+ const cached = readCache();
2102
+ if (cached && Date.now() - cached.checkedAt < CHECK_TTL_MS) {
2103
+ return cached;
2104
+ }
2105
+ const fresh = await fetchLatestVersions(cached);
2106
+ writeCache(fresh);
2107
+ return fresh;
2108
+ })().catch(() => null);
2109
+ return async () => {
2110
+ const latest = await pending;
2111
+ if (!latest) {
2112
+ return;
2113
+ }
2114
+ const notices = [];
2115
+ const current = cliVersion();
2116
+ if (latest.cliLatest && isNewerVersion(latest.cliLatest, current)) {
2117
+ notices.push(
2118
+ `[artifact-cafe] CLI update available: ${current} \u2192 ${latest.cliLatest}. Run commands via \`npx artifact-cafe@latest\`; update a global install with \`npm i -g artifact-cafe@latest\`.`
2119
+ );
2120
+ }
2121
+ if (latest.skillLatest) {
2122
+ const stale = installedSkillVersions().filter(
2123
+ (version) => isNewerVersion(latest.skillLatest, version)
2124
+ ).sort((a, b) => isNewerVersion(a, b) ? 1 : -1);
2125
+ if (stale.length > 0) {
2126
+ notices.push(
2127
+ `[artifact-cafe] Skill update available: ${stale[0]} \u2192 ${latest.skillLatest}. Update: \`${SKILL_UPDATE_COMMAND}\` (drop -g for a repo-local copy), then re-read SKILL.md.`
2128
+ );
2129
+ }
2130
+ }
2131
+ if (notices.length > 0) {
2132
+ console.error(`
2133
+ ${notices.join("\n")}`);
2134
+ }
2135
+ };
2136
+ }
2137
+
2138
+ // src/workspaces.ts
2139
+ async function workspaces(opts) {
2140
+ const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
2141
+ /\/$/,
2142
+ ""
2143
+ );
2144
+ const key = resolveApiKey(apiUrl2, opts.apiKey);
2145
+ if (!key) {
2146
+ throw new Error(
2147
+ `Listing workspaces needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
2148
+ );
2149
+ }
2150
+ const data = await createApiClient(apiUrl2, key).listWorkspaces();
2151
+ if (opts.json) {
2152
+ console.log(JSON.stringify(data, null, 2));
2153
+ return;
2154
+ }
2155
+ if (data.workspaces.length === 0) {
2156
+ console.log("No workspaces yet \u2014 create one from the dashboard.");
2157
+ return;
2158
+ }
2159
+ const plural = data.workspaces.length === 1 ? "" : "s";
2160
+ console.log(`${data.workspaces.length} workspace${plural}
2161
+ `);
2162
+ for (const workspace of data.workspaces) {
2163
+ printWorkspace(workspace);
2164
+ }
2165
+ }
2166
+ function printWorkspace(workspace) {
2167
+ console.log(`${workspace.name} (${workspace.role ?? "non-member"})`);
2168
+ console.log(` ${workspace.handle} \xB7 ${workspace.id}
2169
+ `);
2170
+ }
2171
+
1552
2172
  // src/index.ts
1553
2173
  var publish2 = defineCommand({
1554
2174
  args: {
@@ -1566,13 +2186,21 @@ var publish2 = defineCommand({
1566
2186
  type: "positional"
1567
2187
  },
1568
2188
  entry: { description: "Entry file (default: auto-detect)", type: "string" },
2189
+ folder: {
2190
+ description: "Publish into this workspace folder name (or id)",
2191
+ type: "string"
2192
+ },
1569
2193
  json: { description: "Machine-readable output", type: "boolean" },
1570
2194
  "no-open": { description: "Don't open the review URL", type: "boolean" },
1571
2195
  password: {
1572
2196
  description: "Password-protect the artifact (min 6 chars)",
1573
2197
  type: "string"
1574
2198
  },
1575
- title: { description: "Artifact title", type: "string" }
2199
+ title: { description: "Artifact title", type: "string" },
2200
+ workspace: {
2201
+ description: "Publish into this workspace handle",
2202
+ type: "string"
2203
+ }
1576
2204
  },
1577
2205
  meta: { description: "Publish a folder as an artifact", name: "publish" },
1578
2206
  async run({ args }) {
@@ -1582,10 +2210,12 @@ var publish2 = defineCommand({
1582
2210
  artifact: args.artifact,
1583
2211
  dir: args.dir,
1584
2212
  entry: args.entry,
2213
+ folder: args.folder,
1585
2214
  json: Boolean(args.json),
1586
2215
  open: !args["no-open"],
1587
2216
  password: args.password,
1588
- title: args.title
2217
+ title: args.title,
2218
+ workspace: args.workspace
1589
2219
  });
1590
2220
  } catch (err) {
1591
2221
  printCliError(err, Boolean(args.json));
@@ -1669,6 +2299,60 @@ var list2 = defineCommand({
1669
2299
  }
1670
2300
  }
1671
2301
  });
2302
+ var folders2 = defineCommand({
2303
+ args: {
2304
+ "api-key": {
2305
+ description: "List with this API key (default: stored login)",
2306
+ type: "string"
2307
+ },
2308
+ json: { description: "Machine-readable output", type: "boolean" },
2309
+ workspace: {
2310
+ description: "Workspace handle whose folders to list",
2311
+ required: true,
2312
+ type: "string"
2313
+ }
2314
+ },
2315
+ meta: {
2316
+ description: "List folders in a workspace",
2317
+ name: "folders"
2318
+ },
2319
+ async run({ args }) {
2320
+ try {
2321
+ await folders({
2322
+ apiKey: args["api-key"],
2323
+ json: Boolean(args.json),
2324
+ workspace: args.workspace
2325
+ });
2326
+ } catch (err) {
2327
+ printCliError(err, Boolean(args.json));
2328
+ process.exit(1);
2329
+ }
2330
+ }
2331
+ });
2332
+ var workspaces2 = defineCommand({
2333
+ args: {
2334
+ "api-key": {
2335
+ description: "List with this API key (default: stored login)",
2336
+ type: "string"
2337
+ },
2338
+ json: { description: "Machine-readable output", type: "boolean" }
2339
+ },
2340
+ meta: {
2341
+ description: "List the workspaces your account belongs to",
2342
+ name: "workspaces"
2343
+ },
2344
+ async run({ args }) {
2345
+ try {
2346
+ await workspaces({
2347
+ apiKey: args["api-key"],
2348
+ json: Boolean(args.json)
2349
+ });
2350
+ } catch (err) {
2351
+ printCliError(err, Boolean(args.json));
2352
+ process.exit(1);
2353
+ }
2354
+ }
2355
+ });
1672
2356
  var link2 = defineCommand({
1673
2357
  args: {
1674
2358
  "api-key": {
@@ -1773,6 +2457,66 @@ var comments2 = defineCommand({
1773
2457
  }
1774
2458
  }
1775
2459
  });
2460
+ var listen = defineCommand({
2461
+ args: {
2462
+ "api-key": {
2463
+ description: "Authorize with an account API key (default: stored login)",
2464
+ type: "string"
2465
+ },
2466
+ artifact: {
2467
+ description: "Act on an artifact ID instead of the linked folder",
2468
+ type: "string"
2469
+ },
2470
+ dir: {
2471
+ default: ".",
2472
+ description: "Published directory",
2473
+ type: "positional"
2474
+ },
2475
+ json: { description: "Machine-readable output", type: "boolean" },
2476
+ status: {
2477
+ default: "open",
2478
+ description: "Filter: open | resolved | all",
2479
+ type: "string"
2480
+ },
2481
+ timeout: {
2482
+ default: "900",
2483
+ description: "Give up after this many seconds (exit 2)",
2484
+ type: "string"
2485
+ },
2486
+ version: {
2487
+ default: "current",
2488
+ description: "Filter: current | all",
2489
+ type: "string"
2490
+ }
2491
+ },
2492
+ meta: {
2493
+ description: "Wait for new reviewer feedback (alias for comments --wait)",
2494
+ name: "listen"
2495
+ },
2496
+ async run({ args }) {
2497
+ try {
2498
+ const timeout = Number(args.timeout);
2499
+ if (!Number.isFinite(timeout) || timeout <= 0) {
2500
+ throw new Error(
2501
+ `--timeout must be a positive number of seconds (got "${args.timeout}").`
2502
+ );
2503
+ }
2504
+ await comments({
2505
+ apiKey: args["api-key"],
2506
+ artifact: args.artifact,
2507
+ dir: args.dir,
2508
+ json: Boolean(args.json),
2509
+ status: args.status,
2510
+ timeout,
2511
+ version: args.version,
2512
+ wait: true
2513
+ });
2514
+ } catch (err) {
2515
+ printCliError(err, Boolean(args.json));
2516
+ process.exit(1);
2517
+ }
2518
+ }
2519
+ });
1776
2520
  var protect2 = defineCommand({
1777
2521
  args: {
1778
2522
  "api-key": {
@@ -1812,6 +2556,101 @@ var protect2 = defineCommand({
1812
2556
  }
1813
2557
  }
1814
2558
  });
2559
+ var threadStatusCommand = (action) => defineCommand({
2560
+ args: {
2561
+ "api-key": {
2562
+ description: "Authorize with an account API key (default: stored login)",
2563
+ type: "string"
2564
+ },
2565
+ artifact: {
2566
+ description: "Act on an artifact ID instead of the linked folder",
2567
+ type: "string"
2568
+ },
2569
+ dir: {
2570
+ default: ".",
2571
+ description: "Linked directory to authorize from (default: .)",
2572
+ type: "string"
2573
+ },
2574
+ json: { description: "Machine-readable output", type: "boolean" },
2575
+ thread: {
2576
+ description: "Thread id (from `comments --json`)",
2577
+ required: true,
2578
+ type: "positional"
2579
+ }
2580
+ },
2581
+ meta: {
2582
+ description: action === "resolve" ? "Mark a comment thread resolved" : "Reopen a resolved comment thread",
2583
+ name: action
2584
+ },
2585
+ async run({ args }) {
2586
+ try {
2587
+ await changeThreadStatus({
2588
+ action,
2589
+ apiKey: args["api-key"],
2590
+ artifact: args.artifact,
2591
+ dir: args.dir,
2592
+ json: Boolean(args.json),
2593
+ thread: args.thread
2594
+ });
2595
+ } catch (err) {
2596
+ printCliError(err, Boolean(args.json));
2597
+ process.exit(1);
2598
+ }
2599
+ }
2600
+ });
2601
+ var resolve = threadStatusCommand("resolve");
2602
+ var reopen = threadStatusCommand("reopen");
2603
+ var reply = defineCommand({
2604
+ args: {
2605
+ "api-key": {
2606
+ description: "Authorize with an account API key (default: stored login)",
2607
+ type: "string"
2608
+ },
2609
+ artifact: {
2610
+ description: "Act on an artifact ID instead of the linked folder",
2611
+ type: "string"
2612
+ },
2613
+ body: {
2614
+ description: "Reply text (omit to read from stdin)",
2615
+ type: "string"
2616
+ },
2617
+ dir: {
2618
+ default: ".",
2619
+ description: "Linked directory to authorize from (default: .)",
2620
+ type: "string"
2621
+ },
2622
+ json: { description: "Machine-readable output", type: "boolean" },
2623
+ resolve: {
2624
+ description: "Resolve the thread after replying",
2625
+ type: "boolean"
2626
+ },
2627
+ thread: {
2628
+ description: "Thread id (from `comments --json`)",
2629
+ required: true,
2630
+ type: "positional"
2631
+ }
2632
+ },
2633
+ meta: {
2634
+ description: "Reply to a comment thread (as the agent or owner)",
2635
+ name: "reply"
2636
+ },
2637
+ async run({ args }) {
2638
+ try {
2639
+ await replyToThread({
2640
+ apiKey: args["api-key"],
2641
+ artifact: args.artifact,
2642
+ body: args.body,
2643
+ dir: args.dir,
2644
+ json: Boolean(args.json),
2645
+ resolve: Boolean(args.resolve),
2646
+ thread: args.thread
2647
+ });
2648
+ } catch (err) {
2649
+ printCliError(err, Boolean(args.json));
2650
+ process.exit(1);
2651
+ }
2652
+ }
2653
+ });
1815
2654
  var open = defineCommand({
1816
2655
  args: {
1817
2656
  "api-key": {
@@ -1866,7 +2705,8 @@ var dashboard = defineCommand({
1866
2705
  var main = defineCommand({
1867
2706
  meta: {
1868
2707
  description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
1869
- name: "artifact-cafe"
2708
+ name: "artifact-cafe",
2709
+ version: cliVersion()
1870
2710
  },
1871
2711
  // Bare `artifact-cafe` (no subcommand) → brand banner + usage. citty
1872
2712
  // still calls this parent run *after* a matched subcommand, so bail when
@@ -1882,14 +2722,22 @@ var main = defineCommand({
1882
2722
  subCommands: {
1883
2723
  comments: comments2,
1884
2724
  dashboard,
2725
+ folders: folders2,
1885
2726
  link: link2,
1886
2727
  list: list2,
2728
+ listen,
1887
2729
  login: login2,
1888
2730
  logout: logout2,
1889
2731
  open,
1890
2732
  protect: protect2,
1891
2733
  publish: publish2,
1892
- whoami: whoami2
2734
+ reopen,
2735
+ reply,
2736
+ resolve,
2737
+ whoami: whoami2,
2738
+ workspaces: workspaces2
1893
2739
  }
1894
2740
  });
1895
- runMain(main);
2741
+ var finishUpdateCheck = startUpdateCheck();
2742
+ await runMain(main);
2743
+ await finishUpdateCheck();