artifact-cafe 0.0.15 → 0.0.17

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 +1544 -616
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -7,24 +7,17 @@ import { defineCommand, runMain, showUsage } from "citty";
7
7
  var AMBER = "\x1B[38;2;214;138;57m";
8
8
  var BOLD = "\x1B[1m";
9
9
  var RESET = "\x1B[0m";
10
- var MUG = [
11
- " \u2596",
12
- " \u2598 \u259D",
13
- " \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E",
14
- " \u2502 \u251C\u2500\u256E",
15
- " \u2502 \u2502 \u2502",
16
- " \u2502 \u251C\u2500\u256F",
17
- " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F",
18
- " \u203E\u203E\u203E\u203E\u203E\u203E\u203E\u203E"
19
- ];
10
+ var TICK_TOP = " \u250C \u2510";
11
+ var GLYPH = " \xE3";
12
+ var TICK_BOTTOM = " \u2514 \u2518";
20
13
  var WORDMARK = " artifact.cafe";
21
14
  function printBanner() {
22
15
  const color = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
23
16
  const paint = (line, codes) => color ? `${codes}${line}${RESET}` : line;
24
17
  console.log();
25
- for (const line of MUG) {
26
- console.log(paint(line, AMBER));
27
- }
18
+ console.log(paint(TICK_TOP, AMBER));
19
+ console.log(paint(GLYPH, BOLD));
20
+ console.log(paint(TICK_BOTTOM, AMBER));
28
21
  console.log();
29
22
  console.log(paint(WORDMARK, AMBER + BOLD));
30
23
  console.log();
@@ -69,8 +62,10 @@ var meResponseSchema = z.object({
69
62
  userId: z.string()
70
63
  });
71
64
  var artifactListItemSchema = z.object({
65
+ autoDeleteAt: z.string().datetime().nullable().optional(),
72
66
  createdAt: z.string(),
73
67
  currentVersionNumber: z.number().int().positive().nullable(),
68
+ expiresAt: z.string().datetime().nullable().optional(),
74
69
  id: z.string(),
75
70
  openCommentCount: z.number().int().nonnegative(),
76
71
  slug: z.string(),
@@ -181,7 +176,7 @@ var workspaceAvatarResponseSchema = z3.object({
181
176
  });
182
177
 
183
178
  // ../shared/src/comments.ts
184
- import { z as z7 } from "zod";
179
+ import { z as z8 } from "zod";
185
180
 
186
181
  // ../shared/src/emoji.ts
187
182
  import { z as z4 } from "zod";
@@ -193,15 +188,38 @@ function canonicalizeEmoji(s) {
193
188
  var iconEmojiSchema = z4.string().max(64).transform(canonicalizeEmoji).refine((s) => SINGLE_EMOJI.test(s), { message: "Must be a single emoji." }).nullable().describe("A single emoji shown as the icon, or null for the default.");
194
189
 
195
190
  // ../shared/src/publish.ts
196
- import { z as z6 } from "zod";
191
+ import { z as z7 } from "zod";
197
192
 
198
- // ../shared/src/review-mode.ts
193
+ // ../shared/src/expiration.ts
199
194
  import { z as z5 } from "zod";
200
- var reviewModeSchema = z5.enum(["open", "readonly", "off"]);
201
- var setReviewModeSchema = z5.object({
195
+ var MAX_EXPIRATION_SECONDS = 2147483647;
196
+ var expirationSecondsSchema = z5.number().int().positive().max(MAX_EXPIRATION_SECONDS);
197
+ var setExpirationSchema = z5.object({
198
+ expiresInSeconds: expirationSecondsSchema.nullable()
199
+ });
200
+ var expirationResponseSchema = z5.object({
201
+ autoDeleteAt: z5.string().datetime().nullable(),
202
+ expiresAt: z5.string().datetime().nullable()
203
+ });
204
+ function parseArtifactDuration(value) {
205
+ const match = /^([1-9]\d*)([mhd])$/.exec(value);
206
+ const units = { d: 86400, h: 3600, m: 60 };
207
+ const seconds = match ? Number(match[1]) * (units[match[2]] ?? 0) : 0;
208
+ if (!expirationSecondsSchema.safeParse(seconds).success) {
209
+ throw new Error(
210
+ `Duration must be a positive whole number followed by m, h, or d (for example 30m, 1h, 7d), up to ${MAX_EXPIRATION_SECONDS} seconds.`
211
+ );
212
+ }
213
+ return seconds;
214
+ }
215
+
216
+ // ../shared/src/review-mode.ts
217
+ import { z as z6 } from "zod";
218
+ var reviewModeSchema = z6.enum(["open", "readonly", "off"]);
219
+ var setReviewModeSchema = z6.object({
202
220
  mode: reviewModeSchema
203
221
  });
204
- var reviewModeResponseSchema = z5.object({
222
+ var reviewModeResponseSchema = z6.object({
205
223
  mode: reviewModeSchema
206
224
  });
207
225
 
@@ -220,274 +238,300 @@ function isSafeArtifactPath(p) {
220
238
  }
221
239
  var CARD_DESCRIPTION_MAX = 300;
222
240
  var CARD_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
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, {
241
+ var manifestFileSchema = z7.object({
242
+ hash: z7.string().regex(/^sha256_[a-f0-9]{64}$/),
243
+ mimeType: z7.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
244
+ path: z7.string().min(1).max(1024).refine(isSafeArtifactPath, {
227
245
  message: "path must be a safe relative path"
228
246
  }),
229
- size: z6.number().int().positive()
247
+ size: z7.number().int().positive()
230
248
  });
231
- var publishSourceSchema = z6.object({
232
- agent: z6.string().optional(),
233
- runId: z6.string().optional(),
234
- type: z6.literal("cli")
249
+ var publishSourceSchema = z7.object({
250
+ agent: z7.string().optional(),
251
+ runId: z7.string().optional(),
252
+ type: z7.literal("cli")
235
253
  });
236
- var createPublishSessionSchema = z6.object({
237
- artifactId: z6.string().nullable(),
254
+ var createPublishSessionSchema = z7.object({
255
+ artifactId: z7.string().nullable(),
238
256
  // One-line share-card summary (`--description`). Overrides whatever the
239
257
  // entry HTML declares; leave it off and the API reads the document's own
240
258
  // `og:description`/`description` at finalize.
241
- description: z6.string().max(CARD_DESCRIPTION_MAX).optional(),
242
- entryPath: z6.string().optional(),
243
- files: z6.array(manifestFileSchema).min(1).max(1e3),
259
+ description: z7.string().max(CARD_DESCRIPTION_MAX).optional(),
260
+ entryPath: z7.string().optional(),
261
+ expiresInSeconds: expirationSecondsSchema.optional(),
262
+ files: z7.array(manifestFileSchema).min(1).max(1e3),
244
263
  // SPEC-2 §6.4 — workspace placement is available only to authenticated
245
264
  // members; the API enforces membership and folder ownership. folderId
246
265
  // without workspaceId resolves against the account's personal workspace.
247
- folderId: z6.string().min(1).optional(),
266
+ folderId: z7.string().min(1).optional(),
248
267
  // Bullet-point changelog for this version, authored by the publisher.
249
268
  // Optional on every publish (incl. v1); stored immutably on the version.
250
- notes: z6.string().max(4e3).optional(),
269
+ notes: z7.string().max(4e3).optional(),
251
270
  source: publishSourceSchema,
252
- title: z6.string().max(200).optional(),
253
- workspaceId: z6.string().min(1).optional()
254
- });
255
- var publishSessionResponseSchema = z6.object({
256
- artifactId: z6.string(),
257
- expiresAt: z6.string(),
258
- filesToUpload: z6.array(
259
- z6.object({
260
- method: z6.literal("PUT"),
261
- path: z6.string(),
262
- uploadUrl: z6.string().url()
271
+ title: z7.string().max(200).optional(),
272
+ workspaceId: z7.string().min(1).optional()
273
+ });
274
+ var publishSessionResponseSchema = z7.object({
275
+ artifactId: z7.string(),
276
+ expiresAt: z7.string(),
277
+ expiresInSeconds: expirationSecondsSchema.nullable().optional(),
278
+ filesToUpload: z7.array(
279
+ z7.object({
280
+ method: z7.literal("PUT"),
281
+ path: z7.string(),
282
+ uploadUrl: z7.string().url()
263
283
  })
264
284
  ),
265
- publishSessionId: z6.string(),
266
- skippedFiles: z6.array(z6.string()),
267
- versionId: z6.string()
285
+ publishSessionId: z7.string(),
286
+ skippedFiles: z7.array(z7.string()),
287
+ versionId: z7.string()
268
288
  });
269
- var finalizeResponseSchema = z6.object({
270
- artifactId: z6.string(),
289
+ var finalizeResponseSchema = z7.object({
290
+ artifactId: z7.string(),
291
+ autoDeleteAt: z7.string().datetime().nullable().optional(),
271
292
  // API-key publishes create the artifact already claimed (SPEC §5.4) —
272
293
  // the CLI uses this to tell "claim URL pending" from "already yours".
273
- claimed: z6.boolean(),
294
+ claimed: z7.boolean(),
274
295
  // Returned only on first publish of a new artifact (one-time).
275
- claimUrl: z6.string().url().optional(),
276
- publishToken: z6.string().optional(),
277
- url: z6.string().url(),
278
- versionId: z6.string(),
279
- versionNumber: z6.number().int().positive(),
296
+ claimUrl: z7.string().url().optional(),
297
+ expiresAt: z7.string().datetime().nullable().optional(),
298
+ publishToken: z7.string().optional(),
299
+ url: z7.string().url(),
300
+ versionId: z7.string(),
301
+ versionNumber: z7.number().int().positive(),
280
302
  // SPEC-2 §5.2 — who can open the review URL. A publish into a folder
281
303
  // inherits that folder's visibility, so this is the only place the CLI
282
304
  // learns that a link it just printed is members-only. Optional so a new
283
305
  // CLI against an older API degrades to "unknown" instead of failing the
284
306
  // publish over a field that only shapes the closing message.
285
- visibility: z6.enum(["public", "unlisted", "workspace", "private"]).optional(),
286
- workspaceId: z6.string().nullable().optional()
287
- });
288
- var artifactResponseSchema = z6.object({
289
- claimed: z6.boolean(),
290
- currentVersion: z6.object({
291
- entryUrl: z6.string().url(),
292
- id: z6.string(),
293
- versionNumber: z6.number().int().positive()
307
+ visibility: z7.enum(["public", "unlisted", "workspace", "private"]).optional(),
308
+ workspaceId: z7.string().nullable().optional()
309
+ });
310
+ var artifactResponseSchema = z7.object({
311
+ autoDeleteAt: z7.string().datetime().nullable().optional(),
312
+ claimed: z7.boolean(),
313
+ currentVersion: z7.object({
314
+ entryUrl: z7.string().url(),
315
+ id: z7.string(),
316
+ versionNumber: z7.number().int().positive()
294
317
  }).nullable(),
295
318
  // Custom icon, or null to render the default document glyph.
296
- emoji: z6.string().nullable(),
319
+ emoji: z7.string().nullable(),
320
+ expiresAt: z7.string().datetime().nullable().optional(),
297
321
  // SPEC-2 §6.4 — placement + visibility surface on the artifact.
298
- folderId: z6.string().nullable(),
299
- id: z6.string(),
322
+ folderId: z7.string().nullable(),
323
+ id: z7.string(),
300
324
  // SPEC-2 §5.8 — what a viewer may do once in (open / readonly / off).
301
325
  reviewMode: reviewModeSchema,
302
- slug: z6.string(),
303
- title: z6.string(),
304
- visibility: z6.enum(["public", "unlisted", "workspace", "private"]),
305
- workspaceId: z6.string().nullable()
326
+ slug: z7.string(),
327
+ title: z7.string(),
328
+ visibility: z7.enum(["public", "unlisted", "workspace", "private"]),
329
+ workspaceId: z7.string().nullable()
306
330
  });
331
+ function resolvePublishEntry(files, explicit) {
332
+ if (explicit !== void 0) {
333
+ if (!files.some((file) => file.path === explicit)) {
334
+ throw new Error(`Entry file "${explicit}" is not in the manifest.`);
335
+ }
336
+ return explicit;
337
+ }
338
+ const htmlFiles = files.filter(
339
+ (file) => file.mimeType === "text/html" && !file.path.includes("/")
340
+ );
341
+ if (htmlFiles.some((file) => file.path === "index.html")) {
342
+ return "index.html";
343
+ }
344
+ if (htmlFiles.length > 1) {
345
+ throw new Error(
346
+ `Multiple HTML files found (${htmlFiles.map((file) => file.path).join(", ")}). Specify an entry file.`
347
+ );
348
+ }
349
+ return htmlFiles[0]?.path;
350
+ }
307
351
 
308
352
  // ../shared/src/comments.ts
309
- var anchorPageSchema = z7.string().max(1024).refine(isSafeArtifactPath, {
353
+ var anchorPageSchema = z8.string().max(1024).refine(isSafeArtifactPath, {
310
354
  message: "page must be a safe relative path"
311
355
  }).optional();
312
- var elementSnapshotSchema = z7.object({
313
- classes: z7.string().max(256).optional(),
314
- id: z7.string().max(256).optional(),
315
- tag: z7.string().min(1).max(128),
316
- text: z7.string().max(200).optional()
356
+ var elementSnapshotSchema = z8.object({
357
+ classes: z8.string().max(256).optional(),
358
+ id: z8.string().max(256).optional(),
359
+ tag: z8.string().min(1).max(128),
360
+ text: z8.string().max(200).optional()
317
361
  });
318
- var pointAnchorSchema = z7.object({
362
+ var pointAnchorSchema = z8.object({
319
363
  element: elementSnapshotSchema.optional(),
320
364
  // elementPath is the primary anchor; x/y are offsets within the
321
365
  // element's box, normalized 0..1 (SPEC.md §8.1).
322
- elementPath: z7.string().max(2048),
366
+ elementPath: z8.string().max(2048),
323
367
  page: anchorPageSchema,
324
- type: z7.literal("point"),
325
- viewportHeight: z7.number().int().positive().max(1e5),
326
- viewportWidth: z7.number().int().positive().max(1e5),
327
- x: z7.number().min(0).max(1),
328
- y: z7.number().min(0).max(1)
329
- });
330
- var textAnchorSchema = z7.object({
331
- elementPath: z7.string().max(2048),
332
- endOffset: z7.number().int().nonnegative().max(1e8),
368
+ type: z8.literal("point"),
369
+ viewportHeight: z8.number().int().positive().max(1e5),
370
+ viewportWidth: z8.number().int().positive().max(1e5),
371
+ x: z8.number().min(0).max(1),
372
+ y: z8.number().min(0).max(1)
373
+ });
374
+ var textAnchorSchema = z8.object({
375
+ elementPath: z8.string().max(2048),
376
+ endOffset: z8.number().int().nonnegative().max(1e8),
333
377
  page: anchorPageSchema,
334
- prefix: z7.string().max(200).optional(),
335
- quote: z7.string().min(1).max(1e3),
336
- startOffset: z7.number().int().nonnegative().max(1e8),
337
- suffix: z7.string().max(200).optional(),
338
- type: z7.literal("text")
378
+ prefix: z8.string().max(200).optional(),
379
+ quote: z8.string().min(1).max(1e3),
380
+ startOffset: z8.number().int().nonnegative().max(1e8),
381
+ suffix: z8.string().max(200).optional(),
382
+ type: z8.literal("text")
339
383
  });
340
- var pageAnchorSchema = z7.object({
384
+ var pageAnchorSchema = z8.object({
341
385
  page: anchorPageSchema,
342
- type: z7.literal("page")
386
+ type: z8.literal("page")
343
387
  });
344
- var anchorSchema = z7.discriminatedUnion("type", [
388
+ var anchorSchema = z8.discriminatedUnion("type", [
345
389
  pointAnchorSchema,
346
390
  textAnchorSchema,
347
391
  pageAnchorSchema
348
392
  ]);
349
- var displayNameSchema = z7.string().trim().min(1).max(64);
350
- var guestSchema = z7.object({
393
+ var displayNameSchema = z8.string().trim().min(1).max(64);
394
+ var guestSchema = z8.object({
351
395
  displayName: displayNameSchema,
352
- guestId: z7.string().min(16)
396
+ guestId: z8.string().min(16)
353
397
  });
354
- var viaLabelSchema = z7.string().trim().min(1).max(64);
355
- var createCommentThreadSchema = z7.object({
398
+ var viaLabelSchema = z8.string().trim().min(1).max(64);
399
+ var createCommentThreadSchema = z8.object({
356
400
  anchor: anchorSchema,
357
- body: z7.string().min(1).max(1e4),
401
+ body: z8.string().min(1).max(1e4),
358
402
  guest: guestSchema.optional(),
359
- versionId: z7.string(),
403
+ versionId: z8.string(),
360
404
  via: viaLabelSchema.optional()
361
405
  });
362
- var createCommentMessageSchema = z7.object({
363
- body: z7.string().min(1).max(1e4),
406
+ var createCommentMessageSchema = z8.object({
407
+ body: z8.string().min(1).max(1e4),
364
408
  guest: guestSchema.optional(),
365
409
  via: viaLabelSchema.optional()
366
410
  });
367
- var editCommentMessageSchema = z7.object({
368
- body: z7.string().min(1).max(1e4),
411
+ var editCommentMessageSchema = z8.object({
412
+ body: z8.string().min(1).max(1e4),
369
413
  guest: guestSchema.optional()
370
414
  });
371
- var threadActionSchema = z7.object({
415
+ var threadActionSchema = z8.object({
372
416
  guest: guestSchema.optional()
373
417
  });
374
- var reactionEmojiSchema = z7.string().max(64).transform(canonicalizeEmoji).refine((s) => SINGLE_EMOJI.test(s), { message: "Must be a single emoji." }).describe("A single emoji, e.g. \u{1F44D}");
375
- var toggleReactionSchema = z7.object({
418
+ var reactionEmojiSchema = z8.string().max(64).transform(canonicalizeEmoji).refine((s) => SINGLE_EMOJI.test(s), { message: "Must be a single emoji." }).describe("A single emoji, e.g. \u{1F44D}");
419
+ var toggleReactionSchema = z8.object({
376
420
  emoji: reactionEmojiSchema,
377
421
  guest: guestSchema.optional()
378
422
  });
379
- var commentAgentSchema = z7.object({
380
- id: z7.string().nullable(),
381
- label: z7.string()
423
+ var commentAgentSchema = z8.object({
424
+ id: z8.string().nullable(),
425
+ label: z8.string()
382
426
  });
383
- var commentAuthorSchema = z7.object({
427
+ var commentAuthorSchema = z8.object({
384
428
  agent: commentAgentSchema.nullable().default(null),
385
- avatarUrl: z7.string().nullable(),
386
- displayName: z7.string(),
387
- guestId: z7.string().nullable(),
388
- isViewer: z7.boolean(),
389
- kind: z7.enum(["member", "guest"]),
429
+ avatarUrl: z8.string().nullable(),
430
+ displayName: z8.string(),
431
+ guestId: z8.string().nullable(),
432
+ isViewer: z8.boolean(),
433
+ kind: z8.enum(["member", "guest"]),
390
434
  // Optional "via" attribution (§6.4). Null for plain comments; a bounded
391
435
  // label the shell appends after the display name. Defaulted for the same
392
436
  // reason as `agent`: a server predating §6.4 omits the key entirely.
393
- via: z7.string().nullable().default(null)
437
+ via: z8.string().nullable().default(null)
394
438
  });
395
- var messageReactionSchema = z7.object({
396
- emoji: z7.string(),
397
- reactors: z7.array(commentAuthorSchema)
439
+ var messageReactionSchema = z8.object({
440
+ emoji: z8.string(),
441
+ reactors: z8.array(commentAuthorSchema)
398
442
  });
399
- var commentMessageSchema = z7.object({
443
+ var commentMessageSchema = z8.object({
400
444
  author: commentAuthorSchema,
401
445
  // Empty string for a deleted message — the body is stripped server-side
402
446
  // (§6.7.1 tombstone), never sent to the client and hidden there.
403
- body: z7.string(),
404
- createdAt: z7.string(),
447
+ body: z8.string(),
448
+ createdAt: z8.string(),
405
449
  // Soft-deleted (§6.7.1). A tombstone keeps thread structure and replies;
406
450
  // the shell renders "Comment deleted" instead of the (absent) body.
407
- deleted: z7.boolean(),
451
+ deleted: z8.boolean(),
408
452
  // Last edit time (§6.7.1), or null if never edited — drives the "edited"
409
453
  // badge. No edit history is kept.
410
- editedAt: z7.string().nullable(),
411
- id: z7.string(),
454
+ editedAt: z8.string().nullable(),
455
+ id: z8.string(),
412
456
  // Defaulted, not required: a new CLI parsing an older server's
413
457
  // response must not fail on the missing key.
414
- reactions: z7.array(messageReactionSchema).default([])
458
+ reactions: z8.array(messageReactionSchema).default([])
415
459
  });
416
- var threadActorSchema = z7.object({
417
- displayName: z7.string(),
418
- kind: z7.enum(["member", "guest", "agent"])
460
+ var threadActorSchema = z8.object({
461
+ displayName: z8.string(),
462
+ kind: z8.enum(["member", "guest", "agent"])
419
463
  });
420
- var commentThreadSchema = z7.object({
464
+ var commentThreadSchema = z8.object({
421
465
  // §6.6.1 — set while open to signal "seen, working on it"; cleared on
422
466
  // reopen, kept (but superseded in display) once resolved. Defaulted so
423
467
  // clients parsing an older server's response don't fail.
424
- acknowledgedAt: z7.string().nullable().default(null),
468
+ acknowledgedAt: z8.string().nullable().default(null),
425
469
  acknowledgedBy: threadActorSchema.nullable().default(null),
426
470
  anchor: anchorSchema,
427
- artifactId: z7.string(),
428
- createdAt: z7.string(),
471
+ artifactId: z8.string(),
472
+ createdAt: z8.string(),
429
473
  createdBy: commentAuthorSchema,
430
- id: z7.string(),
431
- messages: z7.array(commentMessageSchema),
474
+ id: z8.string(),
475
+ messages: z8.array(commentMessageSchema),
432
476
  // Null on threads resolved before actor recording existed, and always
433
477
  // null while open.
434
478
  resolvedBy: threadActorSchema.nullable().default(null),
435
- status: z7.enum(["open", "resolved"]),
436
- versionId: z7.string()
479
+ status: z8.enum(["open", "resolved"]),
480
+ versionId: z8.string()
437
481
  });
438
- var commentThreadListSchema = z7.object({
439
- threads: z7.array(commentThreadSchema)
482
+ var commentThreadListSchema = z8.object({
483
+ threads: z8.array(commentThreadSchema)
440
484
  });
441
- var commentExportQuerySchema = z7.object({
442
- status: z7.enum(["open", "resolved", "all"]).default("open"),
443
- version: z7.enum(["current", "all"]).default("current")
485
+ var commentExportQuerySchema = z8.object({
486
+ status: z8.enum(["open", "resolved", "all"]).default("open"),
487
+ version: z8.enum(["current", "all"]).default("current")
444
488
  });
445
- var exportReactionSchema = z7.object({
446
- count: z7.number().int().positive(),
447
- emoji: z7.string()
489
+ var exportReactionSchema = z8.object({
490
+ count: z8.number().int().positive(),
491
+ emoji: z8.string()
448
492
  });
449
- var commentExportReplySchema = z7.object({
450
- author: z7.string(),
451
- authorKind: z7.enum(["member", "guest"]),
452
- body: z7.string(),
453
- createdAt: z7.string(),
493
+ var commentExportReplySchema = z8.object({
494
+ author: z8.string(),
495
+ authorKind: z8.enum(["member", "guest"]),
496
+ body: z8.string(),
497
+ createdAt: z8.string(),
454
498
  // Soft-delete tombstone (§6.7.1): body is "" when true.
455
- deleted: z7.boolean(),
456
- editedAt: z7.string().nullable(),
499
+ deleted: z8.boolean(),
500
+ editedAt: z8.string().nullable(),
457
501
  // The message id — what `edit`/`delete` target (§5.1).
458
- id: z7.string(),
459
- reactions: z7.array(exportReactionSchema).optional()
502
+ id: z8.string(),
503
+ reactions: z8.array(exportReactionSchema).optional()
460
504
  });
461
- var commentExportThreadSchema = z7.object({
505
+ var commentExportThreadSchema = z8.object({
462
506
  // §6.6.1 — lets a polling agent skip threads it already acknowledged
463
507
  // (acknowledged threads stay in status=open exports on purpose).
464
- acknowledgedAt: z7.string().nullable().default(null),
508
+ acknowledgedAt: z8.string().nullable().default(null),
465
509
  anchor: anchorSchema,
466
- author: z7.string(),
467
- authorKind: z7.enum(["member", "guest"]),
468
- body: z7.string(),
469
- createdAt: z7.string(),
470
- deleted: z7.boolean(),
471
- editedAt: z7.string().nullable(),
472
- id: z7.string(),
510
+ author: z8.string(),
511
+ authorKind: z8.enum(["member", "guest"]),
512
+ body: z8.string(),
513
+ createdAt: z8.string(),
514
+ deleted: z8.boolean(),
515
+ editedAt: z8.string().nullable(),
516
+ id: z8.string(),
473
517
  // The opening message's id (distinct from the thread `id`) — `edit`/
474
518
  // `delete` operate on messages, so the CLI needs it to target the first
475
519
  // comment, not just the replies.
476
- messageId: z7.string(),
477
- reactions: z7.array(exportReactionSchema).optional(),
478
- replies: z7.array(commentExportReplySchema),
520
+ messageId: z8.string(),
521
+ reactions: z8.array(exportReactionSchema).optional(),
522
+ replies: z8.array(commentExportReplySchema),
479
523
  resolvedBy: threadActorSchema.nullable().default(null),
480
- status: z7.enum(["open", "resolved"]),
481
- versionNumber: z7.number().int().positive()
524
+ status: z8.enum(["open", "resolved"]),
525
+ versionNumber: z8.number().int().positive()
482
526
  });
483
- var commentExportSchema = z7.object({
484
- artifactId: z7.string(),
485
- comments: z7.array(commentExportThreadSchema),
527
+ var commentExportSchema = z8.object({
528
+ artifactId: z8.string(),
529
+ comments: z8.array(commentExportThreadSchema),
486
530
  // The artifact's current version (null before the first finalize).
487
- versionNumber: z7.number().int().positive().nullable()
531
+ versionNumber: z8.number().int().positive().nullable()
488
532
  });
489
- var commentReadResponseSchema = z7.object({
490
- lastSeenAt: z7.string()
533
+ var commentReadResponseSchema = z8.object({
534
+ lastSeenAt: z8.string()
491
535
  });
492
536
  function elementLabel(el) {
493
537
  if (el.id) {
@@ -497,245 +541,579 @@ function elementLabel(el) {
497
541
  return classes.length ? `<${el.tag}.${classes.join(".")}>` : `<${el.tag}>`;
498
542
  }
499
543
 
544
+ // ../shared/src/data.ts
545
+ var DATA_MANIFEST_PATH = ".artifact/data.json";
546
+ var DATA_NAME_RE = /^[a-z][a-z0-9_]*$/;
547
+ var DATA_NAME_MAX = 64;
548
+ var DATA_RESERVED_FIELDS = /* @__PURE__ */ new Set([
549
+ "id",
550
+ "created_at",
551
+ "updated_at",
552
+ "created_by",
553
+ "mine"
554
+ ]);
555
+ var DATA_LIMITS = {
556
+ artifactRateWindowSeconds: 3600,
557
+ artifactReadHourlyLimit: 1e4,
558
+ artifactWriteHourlyLimit: 1e3,
559
+ defaultReadRate: "120/minute/ip",
560
+ defaultWriteRate: "10/minute/ip",
561
+ maxCollections: 10,
562
+ maxFieldsPerCollection: 50,
563
+ /** Owner-plane batch insert: records per request. */
564
+ maxInsertBatch: 100,
565
+ maxListLimit: 200,
566
+ maxManifestBytes: 64 * 1024,
567
+ maxRecordBytes: 16 * 1024,
568
+ maxRecordsPerArtifact: 1e4
569
+ };
570
+ var DATA_ACCESS_DEFAULTS = {
571
+ delete: "owner",
572
+ insert: "owner",
573
+ read: "public",
574
+ update: "owner"
575
+ };
576
+ var DATA_RATE_LIMIT_RE = /^([1-9]\d{0,5})\/(hour|minute)\/ip$/;
577
+ var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
578
+ function validateFieldValue(name, field, value, errors) {
579
+ switch (field.type) {
580
+ case "string": {
581
+ if (typeof value !== "string") {
582
+ errors.push(`${name}: expected a string`);
583
+ return value;
584
+ }
585
+ const s = field.trim ? value.trim() : value;
586
+ if (field.maxLength !== void 0 && s.length > field.maxLength) {
587
+ errors.push(`${name}: longer than maxLength ${field.maxLength}`);
588
+ }
589
+ return s;
590
+ }
591
+ case "number":
592
+ case "integer": {
593
+ if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
594
+ errors.push(`${name}: expected a ${field.type}`);
595
+ return value;
596
+ }
597
+ if (field.type === "integer" && !Number.isInteger(value)) {
598
+ errors.push(`${name}: expected an integer`);
599
+ }
600
+ if (field.minimum !== void 0 && value < field.minimum) {
601
+ errors.push(`${name}: below minimum ${field.minimum}`);
602
+ }
603
+ if (field.maximum !== void 0 && value > field.maximum) {
604
+ errors.push(`${name}: above maximum ${field.maximum}`);
605
+ }
606
+ return value;
607
+ }
608
+ case "boolean": {
609
+ if (typeof value !== "boolean") {
610
+ errors.push(`${name}: expected a boolean`);
611
+ }
612
+ return value;
613
+ }
614
+ case "datetime": {
615
+ if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
616
+ errors.push(`${name}: expected an ISO 8601 datetime string`);
617
+ }
618
+ return value;
619
+ }
620
+ case "url": {
621
+ if (typeof value !== "string" || !canParseUrl(value)) {
622
+ errors.push(`${name}: expected a valid URL`);
623
+ }
624
+ return value;
625
+ }
626
+ case "email": {
627
+ if (typeof value !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
628
+ errors.push(`${name}: expected an email address`);
629
+ }
630
+ return value;
631
+ }
632
+ case "array": {
633
+ if (!Array.isArray(value)) {
634
+ errors.push(`${name}: expected an array`);
635
+ return value;
636
+ }
637
+ if (field.maxLength !== void 0 && value.length > field.maxLength) {
638
+ errors.push(`${name}: more than maxLength ${field.maxLength} items`);
639
+ }
640
+ return value;
641
+ }
642
+ case "object": {
643
+ if (!isPlainObject(value)) {
644
+ errors.push(`${name}: expected an object`);
645
+ }
646
+ return value;
647
+ }
648
+ default: {
649
+ errors.push(`${name}: unknown field type`);
650
+ return value;
651
+ }
652
+ }
653
+ }
654
+ function canParseUrl(value) {
655
+ try {
656
+ return Boolean(new URL(value));
657
+ } catch {
658
+ return false;
659
+ }
660
+ }
661
+ function validateRecordInput(collection, input, opts = {}) {
662
+ if (!isPlainObject(input)) {
663
+ return { errors: ["record body must be a JSON object"], ok: false };
664
+ }
665
+ const errors = [];
666
+ const data2 = {};
667
+ for (const key of Object.keys(input)) {
668
+ if (DATA_RESERVED_FIELDS.has(key)) {
669
+ errors.push(`${key}: reserved field (server-stamped)`);
670
+ continue;
671
+ }
672
+ const field = collection.fields[key];
673
+ if (!(Object.hasOwn(collection.fields, key) && field)) {
674
+ errors.push(`${key}: not a declared field`);
675
+ continue;
676
+ }
677
+ const value = input[key];
678
+ if (value === null || value === void 0) {
679
+ if (!field.required) {
680
+ data2[key] = null;
681
+ } else if (opts.partial) {
682
+ errors.push(`${key}: required (cannot be null)`);
683
+ }
684
+ continue;
685
+ }
686
+ data2[key] = validateFieldValue(key, field, value, errors);
687
+ }
688
+ if (opts.partial) {
689
+ if (Object.keys(data2).length === 0 && errors.length === 0) {
690
+ errors.push("no fields to update");
691
+ }
692
+ } else {
693
+ for (const [name, field] of Object.entries(collection.fields)) {
694
+ const present = data2[name] !== void 0 && data2[name] !== null;
695
+ if (present) {
696
+ continue;
697
+ }
698
+ if (field.default !== void 0) {
699
+ data2[name] = field.default;
700
+ } else if (field.required) {
701
+ errors.push(`${name}: required`);
702
+ }
703
+ }
704
+ }
705
+ if (errors.length > 0) {
706
+ return { errors, ok: false };
707
+ }
708
+ return { data: data2, ok: true };
709
+ }
710
+
711
+ // ../shared/src/data-api.ts
712
+ import { z as z9 } from "zod";
713
+ var dataRecordSchema = z9.object({
714
+ created_at: z9.string(),
715
+ created_by: z9.string().nullable(),
716
+ id: z9.string(),
717
+ updated_at: z9.string()
718
+ }).catchall(z9.unknown());
719
+ var dataPublicRecordSchema = z9.object({
720
+ created_at: z9.string(),
721
+ created_by: z9.never().optional(),
722
+ id: z9.string(),
723
+ mine: z9.boolean(),
724
+ updated_at: z9.string()
725
+ }).catchall(z9.unknown());
726
+ var dataPublicListResponseSchema = z9.object({
727
+ nextCursor: z9.string().nullable(),
728
+ records: z9.array(dataPublicRecordSchema)
729
+ });
730
+ var dataListResponseSchema = z9.object({
731
+ nextCursor: z9.string().nullable(),
732
+ records: z9.array(dataRecordSchema)
733
+ });
734
+ var dataRecordResponseSchema = z9.object({ record: dataRecordSchema });
735
+ var dataBatchInsertResponseSchema = z9.object({
736
+ records: z9.array(dataRecordSchema)
737
+ });
738
+ var dataCollectionSummarySchema = z9.object({
739
+ access: z9.record(z9.string(), z9.string()).optional(),
740
+ count: z9.number().int().nonnegative().optional(),
741
+ name: z9.string(),
742
+ rateLimit: z9.string().optional()
743
+ });
744
+ var dataSummaryResponseSchema = z9.object({
745
+ collections: z9.array(dataCollectionSummarySchema),
746
+ enabled: z9.boolean()
747
+ });
748
+ var dataClearResponseSchema = z9.object({
749
+ cleared: z9.number().int().nonnegative()
750
+ });
751
+
752
+ // ../shared/src/data-manifest.ts
753
+ import { z as z10 } from "zod";
754
+ var nameSchema = z10.string().max(DATA_NAME_MAX).regex(DATA_NAME_RE, "must match ^[a-z][a-z0-9_]*$");
755
+ var fieldTypeSchema = z10.enum([
756
+ "string",
757
+ "number",
758
+ "integer",
759
+ "boolean",
760
+ "datetime",
761
+ "url",
762
+ "email",
763
+ "array",
764
+ "object"
765
+ ]);
766
+ var fieldSchema = z10.strictObject({
767
+ default: z10.unknown().optional(),
768
+ maximum: z10.number().optional(),
769
+ maxLength: z10.number().int().positive().optional(),
770
+ minimum: z10.number().optional(),
771
+ required: z10.boolean().optional(),
772
+ trim: z10.boolean().optional(),
773
+ type: fieldTypeSchema
774
+ }).superRefine((field, ctx) => {
775
+ if (field.default === void 0) {
776
+ return;
777
+ }
778
+ if (!defaultMatchesType(field)) {
779
+ ctx.addIssue({
780
+ code: "custom",
781
+ message: `default value does not match type "${field.type}"`,
782
+ path: ["default"]
783
+ });
784
+ }
785
+ });
786
+ function defaultMatchesType(field) {
787
+ if (field.required && field.default === null) {
788
+ return false;
789
+ }
790
+ return validateRecordInput(
791
+ {
792
+ access: DATA_ACCESS_DEFAULTS,
793
+ fields: { value: { ...field, default: void 0 } }
794
+ },
795
+ { value: field.default }
796
+ ).ok;
797
+ }
798
+ var accessLevelSchema = z10.enum(["public", "creator", "owner", "none"]);
799
+ var accessSchema = z10.strictObject({
800
+ delete: accessLevelSchema.default("owner"),
801
+ insert: accessLevelSchema.default("owner"),
802
+ read: accessLevelSchema.default("public"),
803
+ update: accessLevelSchema.default("owner")
804
+ }).prefault({});
805
+ var collectionSchema = z10.strictObject({
806
+ access: accessSchema,
807
+ fields: z10.record(nameSchema, fieldSchema),
808
+ publicMutation: z10.literal("open").optional(),
809
+ rateLimit: z10.string().regex(DATA_RATE_LIMIT_RE, 'expected "N/hour/ip" or "N/minute/ip"').optional()
810
+ }).superRefine((collection, ctx) => {
811
+ const fieldNames = Object.keys(collection.fields);
812
+ if (fieldNames.length > DATA_LIMITS.maxFieldsPerCollection) {
813
+ ctx.addIssue({
814
+ code: "custom",
815
+ message: `at most ${DATA_LIMITS.maxFieldsPerCollection} fields per collection`,
816
+ path: ["fields"]
817
+ });
818
+ }
819
+ for (const name of fieldNames) {
820
+ if (DATA_RESERVED_FIELDS.has(name)) {
821
+ ctx.addIssue({
822
+ code: "custom",
823
+ message: `"${name}" is reserved (server-stamped)`,
824
+ path: ["fields", name]
825
+ });
826
+ }
827
+ }
828
+ for (const action of ["update", "delete"]) {
829
+ if (collection.access[action] === "public" && collection.publicMutation !== "open") {
830
+ ctx.addIssue({
831
+ code: "custom",
832
+ message: `access.${action} may only be "public" alongside "publicMutation": "open"`,
833
+ path: ["access", action]
834
+ });
835
+ }
836
+ }
837
+ });
838
+ var dataManifestSchema = z10.strictObject({
839
+ collections: z10.record(nameSchema, collectionSchema)
840
+ }).superRefine((manifest, ctx) => {
841
+ if (Object.keys(manifest.collections).length > DATA_LIMITS.maxCollections) {
842
+ ctx.addIssue({
843
+ code: "custom",
844
+ message: `at most ${DATA_LIMITS.maxCollections} collections`,
845
+ path: ["collections"]
846
+ });
847
+ }
848
+ });
849
+ function parseDataManifest(text) {
850
+ if (new TextEncoder().encode(text).length > DATA_LIMITS.maxManifestBytes) {
851
+ return {
852
+ errors: [`manifest larger than ${DATA_LIMITS.maxManifestBytes} bytes`],
853
+ ok: false
854
+ };
855
+ }
856
+ let raw;
857
+ try {
858
+ raw = JSON.parse(text);
859
+ } catch (err) {
860
+ return {
861
+ errors: [`not valid JSON: ${err instanceof Error ? err.message : err}`],
862
+ ok: false
863
+ };
864
+ }
865
+ const parsed = dataManifestSchema.safeParse(raw);
866
+ if (!parsed.success) {
867
+ return {
868
+ errors: parsed.error.issues.map(
869
+ (issue) => issue.path.length ? `${issue.path.join(".")}: ${issue.message}` : issue.message
870
+ ),
871
+ ok: false
872
+ };
873
+ }
874
+ return { config: parsed.data, ok: true };
875
+ }
876
+
500
877
  // ../shared/src/errors.ts
501
- import { z as z8 } from "zod";
502
- var apiErrorSchema = z8.object({
503
- code: z8.string(),
504
- docsUrl: z8.string().url(),
505
- message: z8.string(),
506
- retryAfter: z8.number().int().positive().optional()
878
+ import { z as z11 } from "zod";
879
+ var apiErrorSchema = z11.object({
880
+ code: z11.string(),
881
+ docsUrl: z11.string().url(),
882
+ message: z11.string(),
883
+ retryAfter: z11.number().int().positive().optional(),
884
+ scope: z11.enum(["ip", "artifact"]).optional()
507
885
  });
508
886
 
509
887
  // ../shared/src/feedback.ts
510
- import { z as z9 } from "zod";
511
- var feedbackRequestSchema = z9.object({
512
- email: z9.string().trim().toLowerCase().email().optional().or(z9.literal("")),
513
- message: z9.string().trim().min(1).max(4e3)
888
+ import { z as z12 } from "zod";
889
+ var feedbackRequestSchema = z12.object({
890
+ email: z12.string().trim().toLowerCase().email().optional().or(z12.literal("")),
891
+ message: z12.string().trim().min(1).max(4e3)
514
892
  });
515
- var feedbackResponseSchema = z9.object({
516
- sent: z9.literal(true)
893
+ var feedbackResponseSchema = z12.object({
894
+ sent: z12.literal(true)
517
895
  });
518
896
 
519
897
  // ../shared/src/grants.ts
520
- import { z as z10 } from "zod";
521
- var grantRoleSchema = z10.enum(["viewer"]);
522
- var createGrantSchema = z10.object({
523
- email: z10.string().trim().toLowerCase().email().optional(),
898
+ import { z as z13 } from "zod";
899
+ var grantRoleSchema = z13.enum(["viewer"]);
900
+ var createGrantSchema = z13.object({
901
+ email: z13.string().trim().toLowerCase().email().optional(),
524
902
  role: grantRoleSchema.default("viewer"),
525
- userId: z10.string().min(1).optional()
903
+ userId: z13.string().min(1).optional()
526
904
  }).refine((body) => Boolean(body.email) !== Boolean(body.userId), {
527
905
  message: "provide exactly one of email or userId"
528
906
  });
529
- var grantSchema = z10.object({
530
- createdAt: z10.string(),
531
- email: z10.string().email().nullable(),
532
- expiresAt: z10.string().nullable(),
533
- id: z10.string(),
907
+ var grantSchema = z13.object({
908
+ createdAt: z13.string(),
909
+ email: z13.string().email().nullable(),
910
+ expiresAt: z13.string().nullable(),
911
+ id: z13.string(),
534
912
  // The subject's avatar once they have an account; null otherwise.
535
- image: z10.string().nullable(),
913
+ image: z13.string().nullable(),
536
914
  // Present on the invited account after redemption / when granting an
537
915
  // existing user; null for a still-pending email invite.
538
- name: z10.string().nullable(),
539
- redeemedAt: z10.string().nullable(),
916
+ name: z13.string().nullable(),
917
+ redeemedAt: z13.string().nullable(),
540
918
  role: grantRoleSchema,
541
919
  // Shown as "· pending" beside the role until the link is redeemed.
542
- status: z10.enum(["pending", "active"]),
920
+ status: z13.enum(["pending", "active"]),
543
921
  // Display fragment of the live invitation token, or null once redeemed.
544
- tokenPrefix: z10.string().nullable(),
545
- userId: z10.string().nullable()
922
+ tokenPrefix: z13.string().nullable(),
923
+ userId: z13.string().nullable()
546
924
  });
547
925
  var createdGrantSchema = grantSchema.extend({
548
- url: z10.string().url()
926
+ url: z13.string().url()
549
927
  });
550
- var grantListResponseSchema = z10.object({
551
- grants: z10.array(grantSchema),
928
+ var grantListResponseSchema = z13.object({
929
+ grants: z13.array(grantSchema),
552
930
  // The artifact's current general-access value, so the share surface can
553
931
  // render the "General access" line without a second read.
554
- visibility: z10.enum(["public", "unlisted", "workspace", "private"]),
932
+ visibility: z13.enum(["public", "unlisted", "workspace", "private"]),
555
933
  // The owning workspace's logo, so the share modal can draw the workspace
556
934
  // itself rather than a generic glyph. Null when none was uploaded.
557
- workspaceLogo: z10.string().nullable().optional(),
935
+ workspaceLogo: z13.string().nullable().optional(),
558
936
  // The owning workspace's display name, so "People with access" can name the
559
937
  // audience `workspace` visibility actually admits ("All Acme members") rather
560
938
  // than leaving an empty list to imply nobody. Null for a workspace-less
561
939
  // artifact; optional so an older server still parses.
562
- workspaceName: z10.string().nullable().optional()
940
+ workspaceName: z13.string().nullable().optional()
563
941
  });
564
- var revokeGrantResponseSchema = z10.object({
565
- revoked: z10.literal(true)
942
+ var revokeGrantResponseSchema = z13.object({
943
+ revoked: z13.literal(true)
566
944
  });
567
- var acceptGrantSchema = z10.object({
568
- token: z10.string().min(1)
945
+ var acceptGrantSchema = z13.object({
946
+ token: z13.string().min(1)
569
947
  });
570
- var acceptGrantResponseSchema = z10.object({
571
- artifactId: z10.string(),
948
+ var acceptGrantResponseSchema = z13.object({
949
+ artifactId: z13.string(),
572
950
  // The review shell URL to land on, carrying ?invited=1 for the banner.
573
- artifactUrl: z10.string().url(),
574
- slug: z10.string(),
575
- title: z10.string()
576
- });
577
- var workspaceGuestSchema = z10.object({
578
- artifacts: z10.array(
579
- z10.object({
580
- artifactId: z10.string(),
581
- grantId: z10.string(),
582
- slug: z10.string(),
583
- status: z10.enum(["pending", "active"]),
584
- title: z10.string()
951
+ artifactUrl: z13.string().url(),
952
+ slug: z13.string(),
953
+ title: z13.string()
954
+ });
955
+ var workspaceGuestSchema = z13.object({
956
+ artifacts: z13.array(
957
+ z13.object({
958
+ artifactId: z13.string(),
959
+ grantId: z13.string(),
960
+ slug: z13.string(),
961
+ status: z13.enum(["pending", "active"]),
962
+ title: z13.string()
585
963
  })
586
964
  ),
587
- email: z10.string().nullable(),
588
- image: z10.string().nullable(),
589
- name: z10.string().nullable(),
590
- userId: z10.string().nullable()
591
- });
592
- var workspaceGuestListResponseSchema = z10.object({
593
- guests: z10.array(workspaceGuestSchema)
594
- });
595
- var sharedWithMeArtifactSchema = z10.object({
596
- artifactId: z10.string(),
597
- slug: z10.string(),
598
- title: z10.string(),
599
- url: z10.string().url(),
965
+ email: z13.string().nullable(),
966
+ image: z13.string().nullable(),
967
+ name: z13.string().nullable(),
968
+ userId: z13.string().nullable()
969
+ });
970
+ var workspaceGuestListResponseSchema = z13.object({
971
+ guests: z13.array(workspaceGuestSchema)
972
+ });
973
+ var sharedWithMeArtifactSchema = z13.object({
974
+ artifactId: z13.string(),
975
+ slug: z13.string(),
976
+ title: z13.string(),
977
+ url: z13.string().url(),
600
978
  // Who shared it, for the "Shared with me" list.
601
- workspaceName: z10.string()
979
+ workspaceName: z13.string()
602
980
  });
603
- var sharedWithMeListResponseSchema = z10.object({
604
- artifacts: z10.array(sharedWithMeArtifactSchema)
981
+ var sharedWithMeListResponseSchema = z13.object({
982
+ artifacts: z13.array(sharedWithMeArtifactSchema)
605
983
  });
606
984
 
607
985
  // ../shared/src/html-meta.ts
608
986
  var HEAD_SCAN_CHARS = 64 * 1024;
609
987
 
610
988
  // ../shared/src/me.ts
611
- import { z as z12 } from "zod";
989
+ import { z as z15 } from "zod";
612
990
 
613
991
  // ../shared/src/workspaces.ts
614
- import { z as z11 } from "zod";
992
+ import { z as z14 } from "zod";
615
993
  var WORKSPACE_HANDLE_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])$/;
616
- var workspaceHandleUnavailableReasonSchema = z11.enum([
994
+ var workspaceHandleUnavailableReasonSchema = z14.enum([
617
995
  "held",
618
996
  "invalid",
619
997
  "reserved",
620
998
  "taken"
621
999
  ]);
622
- var workspaceHandleAvailabilitySchema = z11.object({
623
- available: z11.boolean(),
624
- handle: z11.string(),
1000
+ var workspaceHandleAvailabilitySchema = z14.object({
1001
+ available: z14.boolean(),
1002
+ handle: z14.string(),
625
1003
  // Human-readable sentence for the field hint; null when available.
626
- message: z11.string().nullable(),
1004
+ message: z14.string().nullable(),
627
1005
  reason: workspaceHandleUnavailableReasonSchema.nullable()
628
1006
  });
629
- var workspaceRoleSchema = z11.enum(["owner", "admin", "member"]);
630
- var workspaceInviteRoleSchema = z11.enum(["admin", "member"]);
631
- var workspaceHandleSchema = z11.string().min(3).max(32).regex(WORKSPACE_HANDLE_PATTERN, {
1007
+ var workspaceRoleSchema = z14.enum(["owner", "admin", "member"]);
1008
+ var workspaceInviteRoleSchema = z14.enum(["admin", "member"]);
1009
+ var workspaceHandleSchema = z14.string().min(3).max(32).regex(WORKSPACE_HANDLE_PATTERN, {
632
1010
  message: "handle must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
633
1011
  });
634
- var createWorkspaceSchema = z11.object({
1012
+ var createWorkspaceSchema = z14.object({
635
1013
  handle: workspaceHandleSchema.optional(),
636
- name: z11.string().trim().min(1).max(100)
1014
+ name: z14.string().trim().min(1).max(100)
637
1015
  });
638
- var updateWorkspaceSchema = z11.object({
1016
+ var updateWorkspaceSchema = z14.object({
639
1017
  handle: workspaceHandleSchema.optional(),
640
- name: z11.string().trim().min(1).max(100).optional()
1018
+ name: z14.string().trim().min(1).max(100).optional()
641
1019
  }).refine((body) => body.handle !== void 0 || body.name !== void 0, {
642
1020
  message: "provide at least one of name or handle"
643
1021
  });
644
- var workspaceSchema = z11.object({
645
- createdAt: z11.string(),
646
- handle: z11.string(),
1022
+ var workspaceSchema = z14.object({
1023
+ createdAt: z14.string(),
1024
+ handle: z14.string(),
647
1025
  // ISO timestamp when `{handle}.artifact.cafe` was provisioned, or null.
648
1026
  // Set by the one-time, self-serve enable flow (§4.4); never auto at
649
1027
  // create, and always null for personal workspaces.
650
- hostnameProvisionedAt: z11.string().nullable(),
651
- id: z11.string(),
1028
+ hostnameProvisionedAt: z14.string().nullable(),
1029
+ id: z14.string(),
652
1030
  // Auto-provisioned solo workspace, one per user: non-deletable, takes
653
1031
  // no members, renameable like any workspace.
654
- isPersonal: z11.boolean(),
1032
+ isPersonal: z14.boolean(),
655
1033
  // Uploaded logo URL, or null for the initials fallback. Set through the
656
1034
  // dedicated /workspaces/:id/avatar route, never the generic update.
657
- logo: z11.string().nullable(),
658
- name: z11.string(),
1035
+ logo: z14.string().nullable(),
1036
+ name: z14.string(),
659
1037
  role: workspaceRoleSchema.nullable(),
660
- updatedAt: z11.string()
1038
+ updatedAt: z14.string()
661
1039
  });
662
- var workspaceListResponseSchema = z11.object({
663
- workspaces: z11.array(workspaceSchema)
1040
+ var workspaceListResponseSchema = z14.object({
1041
+ workspaces: z14.array(workspaceSchema)
664
1042
  });
665
- var deleteWorkspaceResponseSchema = z11.object({
666
- deleted: z11.literal(true)
1043
+ var deleteWorkspaceResponseSchema = z14.object({
1044
+ deleted: z14.literal(true)
667
1045
  });
668
- var workspaceMemberSchema = z11.object({
669
- createdAt: z11.string(),
670
- email: z11.string().email(),
671
- image: z11.string().nullable(),
672
- name: z11.string(),
1046
+ var workspaceMemberSchema = z14.object({
1047
+ createdAt: z14.string(),
1048
+ email: z14.string().email(),
1049
+ image: z14.string().nullable(),
1050
+ name: z14.string(),
673
1051
  role: workspaceRoleSchema,
674
- userId: z11.string()
1052
+ userId: z14.string()
675
1053
  });
676
- var workspaceMemberListResponseSchema = z11.object({
677
- members: z11.array(workspaceMemberSchema)
1054
+ var workspaceMemberListResponseSchema = z14.object({
1055
+ members: z14.array(workspaceMemberSchema)
678
1056
  });
679
- var updateWorkspaceMemberSchema = z11.object({
1057
+ var updateWorkspaceMemberSchema = z14.object({
680
1058
  role: workspaceRoleSchema
681
1059
  });
682
- var removeWorkspaceMemberResponseSchema = z11.object({
683
- removed: z11.literal(true)
1060
+ var removeWorkspaceMemberResponseSchema = z14.object({
1061
+ removed: z14.literal(true)
684
1062
  });
685
- var createWorkspaceInviteSchema = z11.object({
686
- email: z11.string().trim().toLowerCase().email().optional(),
687
- expiresAt: z11.string().datetime(),
688
- maxUses: z11.number().int().positive().optional(),
1063
+ var createWorkspaceInviteSchema = z14.object({
1064
+ email: z14.string().trim().toLowerCase().email().optional(),
1065
+ expiresAt: z14.string().datetime(),
1066
+ maxUses: z14.number().int().positive().optional(),
689
1067
  role: workspaceInviteRoleSchema
690
1068
  });
691
- var workspaceInviteSchema = z11.object({
692
- createdAt: z11.string(),
693
- email: z11.string().email().nullable(),
694
- expiresAt: z11.string(),
695
- id: z11.string(),
696
- maxUses: z11.number().int().positive().nullable(),
1069
+ var workspaceInviteSchema = z14.object({
1070
+ createdAt: z14.string(),
1071
+ email: z14.string().email().nullable(),
1072
+ expiresAt: z14.string(),
1073
+ id: z14.string(),
1074
+ maxUses: z14.number().int().positive().nullable(),
697
1075
  role: workspaceInviteRoleSchema,
698
- tokenPrefix: z11.string(),
699
- useCount: z11.number().int().nonnegative()
1076
+ tokenPrefix: z14.string(),
1077
+ useCount: z14.number().int().nonnegative()
700
1078
  });
701
1079
  var createdWorkspaceInviteSchema = workspaceInviteSchema.extend({
702
- url: z11.string().url()
1080
+ url: z14.string().url()
703
1081
  });
704
- var workspaceInviteListResponseSchema = z11.object({
705
- invites: z11.array(workspaceInviteSchema)
1082
+ var workspaceInviteListResponseSchema = z14.object({
1083
+ invites: z14.array(workspaceInviteSchema)
706
1084
  });
707
- var revokeWorkspaceInviteResponseSchema = z11.object({
708
- revoked: z11.literal(true)
1085
+ var revokeWorkspaceInviteResponseSchema = z14.object({
1086
+ revoked: z14.literal(true)
709
1087
  });
710
- var acceptWorkspaceInviteSchema = z11.object({
711
- token: z11.string().min(1)
1088
+ var acceptWorkspaceInviteSchema = z14.object({
1089
+ token: z14.string().min(1)
712
1090
  });
713
- var acceptWorkspaceInviteResponseSchema = z11.object({
1091
+ var acceptWorkspaceInviteResponseSchema = z14.object({
714
1092
  role: workspaceRoleSchema,
715
1093
  workspace: workspaceSchema
716
1094
  });
717
- var folderVisibilitySchema = z11.enum(["public", "workspace"]);
718
- var artifactVisibilitySchema = z11.enum([
1095
+ var folderVisibilitySchema = z14.enum(["public", "workspace"]);
1096
+ var artifactVisibilitySchema = z14.enum([
719
1097
  "public",
720
1098
  "unlisted",
721
1099
  "workspace",
722
1100
  "private"
723
1101
  ]);
724
- var createFolderSchema = z11.object({
1102
+ var createFolderSchema = z14.object({
725
1103
  // Optional custom icon; omitted (or null) leaves the default glyph.
726
1104
  emoji: iconEmojiSchema.optional(),
727
- name: z11.string().trim().min(1).max(100),
1105
+ name: z14.string().trim().min(1).max(100),
728
1106
  visibility: folderVisibilitySchema
729
1107
  });
730
- var updateFolderSchema = z11.object({
1108
+ var updateFolderSchema = z14.object({
731
1109
  // With `visibility`: bulk-update the folder's artifacts to match
732
1110
  // (SPEC-2 §3.4 "Changing a folder's visibility offers a bulk update").
733
- cascade: z11.boolean().optional(),
1111
+ cascade: z14.boolean().optional(),
734
1112
  // Explicit null clears the icon back to the default glyph, so this is
735
1113
  // a field where "sent as null" and "omitted" mean different things.
736
1114
  emoji: iconEmojiSchema.optional(),
737
- name: z11.string().trim().min(1).max(100).optional(),
738
- sortOrder: z11.number().int().min(0).optional(),
1115
+ name: z14.string().trim().min(1).max(100).optional(),
1116
+ sortOrder: z14.number().int().min(0).optional(),
739
1117
  visibility: folderVisibilitySchema.optional()
740
1118
  }).refine(
741
1119
  (body) => body.name !== void 0 || body.visibility !== void 0 || body.sortOrder !== void 0 || body.emoji !== void 0,
@@ -743,93 +1121,95 @@ var updateFolderSchema = z11.object({
743
1121
  ).refine((body) => !body.cascade || body.visibility !== void 0, {
744
1122
  message: "cascade requires visibility"
745
1123
  });
746
- var folderSchema = z11.object({
747
- createdAt: z11.string(),
1124
+ var folderSchema = z14.object({
1125
+ createdAt: z14.string(),
748
1126
  // Custom icon, or null to render the default folder glyph.
749
- emoji: z11.string().nullable(),
750
- id: z11.string(),
751
- name: z11.string(),
1127
+ emoji: z14.string().nullable(),
1128
+ id: z14.string(),
1129
+ name: z14.string(),
752
1130
  // Permalink segment for the public folder page (/w/{handle}/f/{slug}).
753
1131
  // Server-derived from the name; renames regenerate it.
754
- slug: z11.string(),
755
- sortOrder: z11.number().int(),
756
- updatedAt: z11.string(),
1132
+ slug: z14.string(),
1133
+ sortOrder: z14.number().int(),
1134
+ updatedAt: z14.string(),
757
1135
  visibility: folderVisibilitySchema,
758
- workspaceId: z11.string()
1136
+ workspaceId: z14.string()
759
1137
  });
760
- var folderListResponseSchema = z11.object({
761
- folders: z11.array(folderSchema)
1138
+ var folderListResponseSchema = z14.object({
1139
+ folders: z14.array(folderSchema)
762
1140
  });
763
- var deleteFolderResponseSchema = z11.object({
764
- deleted: z11.literal(true)
1141
+ var deleteFolderResponseSchema = z14.object({
1142
+ deleted: z14.literal(true)
765
1143
  });
766
- var workspaceArtifactSchema = z11.object({
767
- createdAt: z11.string(),
768
- currentVersionNumber: z11.number().int().nullable(),
1144
+ var workspaceArtifactSchema = z14.object({
1145
+ autoDeleteAt: z14.string().datetime().nullable().optional(),
1146
+ createdAt: z14.string(),
1147
+ currentVersionNumber: z14.number().int().nullable(),
769
1148
  // Custom icon, or null to render the default document glyph.
770
- emoji: z11.string().nullable(),
771
- folderId: z11.string().nullable(),
772
- id: z11.string(),
773
- openCommentCount: z11.number().int().nonnegative(),
1149
+ emoji: z14.string().nullable(),
1150
+ expiresAt: z14.string().datetime().nullable().optional(),
1151
+ folderId: z14.string().nullable(),
1152
+ id: z14.string(),
1153
+ openCommentCount: z14.number().int().nonnegative(),
774
1154
  reviewMode: reviewModeSchema,
775
- slug: z11.string(),
776
- title: z11.string(),
777
- updatedAt: z11.string(),
778
- url: z11.string().url(),
779
- versionCount: z11.number().int().nonnegative(),
1155
+ slug: z14.string(),
1156
+ title: z14.string(),
1157
+ updatedAt: z14.string(),
1158
+ url: z14.string().url(),
1159
+ versionCount: z14.number().int().nonnegative(),
780
1160
  visibility: artifactVisibilitySchema
781
1161
  });
782
- var workspaceArtifactListResponseSchema = z11.object({
783
- artifacts: z11.array(workspaceArtifactSchema)
1162
+ var workspaceArtifactListResponseSchema = z14.object({
1163
+ artifacts: z14.array(workspaceArtifactSchema)
784
1164
  });
785
- var workspaceArtifactSearchResultSchema = z11.object({
786
- id: z11.string(),
787
- slug: z11.string(),
788
- title: z11.string()
1165
+ var workspaceArtifactSearchResultSchema = z14.object({
1166
+ id: z14.string(),
1167
+ slug: z14.string(),
1168
+ title: z14.string()
789
1169
  });
790
- var workspaceArtifactSearchResponseSchema = z11.object({
791
- results: z11.array(workspaceArtifactSearchResultSchema)
1170
+ var workspaceArtifactSearchResponseSchema = z14.object({
1171
+ results: z14.array(workspaceArtifactSearchResultSchema)
792
1172
  });
793
1173
 
794
1174
  // ../shared/src/me.ts
795
- var personSchema = z12.object({
796
- image: z12.string().nullable(),
797
- name: z12.string()
1175
+ var personSchema = z15.object({
1176
+ image: z15.string().nullable(),
1177
+ name: z15.string()
798
1178
  });
799
- var ownArtifactSchema = z12.object({
1179
+ var ownArtifactSchema = z15.object({
1180
+ autoDeleteAt: z15.string().datetime().nullable().optional(),
800
1181
  // Everyone else who has published a version, oldest first. Any member may
801
1182
  // publish a new one (SPEC-2 §5.1), so an artifact's byline is often more
802
1183
  // than one person and the card stacks their faces rather than crediting
803
1184
  // whoever shipped last. Excludes `publishedBy`; empty on a solo artifact.
804
- coPublishers: z12.array(personSchema),
805
- createdAt: z12.string(),
1185
+ coPublishers: z15.array(personSchema),
1186
+ createdAt: z15.string(),
806
1187
  // SPEC-2 §5.1 — who *made* the artifact: the author of its first
807
1188
  // finalized version, the same row `viewerCanManage` is decided on. Not
808
1189
  // the same person as `publishedBy` once an agent ships v2, which is why
809
1190
  // the dashboard filters on both. Null for a token-only publish.
810
- createdBy: z12.object({ image: z12.string().nullable(), name: z12.string() }).nullable(),
811
- currentVersionNumber: z12.number().nullable(),
1191
+ createdBy: z15.object({ image: z15.string().nullable(), name: z15.string() }).nullable(),
1192
+ currentVersionNumber: z15.number().nullable(),
812
1193
  // Custom icon, or null to render the default document glyph.
813
- emoji: z12.string().nullable(),
814
- // Owned artifacts normally never expire; non-null only while an
815
- // expiry is still set (e.g. an unclaimed publish surfaced here later).
816
- expiresAt: z12.string().nullable(),
1194
+ emoji: z15.string().nullable(),
1195
+ // Effective expiry: explicit temporary lifetime or anonymous retention.
1196
+ expiresAt: z15.string().nullable(),
817
1197
  // SPEC-2 §8.4 — the containing folder for workspace-owned artifacts;
818
1198
  // both null for personal artifacts. `folderId` drives the dashboard's
819
1199
  // move control and folder filter; `folderName` is the display label,
820
1200
  // `folderEmoji` its icon (null = the default folder glyph).
821
- folderEmoji: z12.string().nullable(),
822
- folderId: z12.string().nullable(),
823
- folderName: z12.string().nullable(),
1201
+ folderEmoji: z15.string().nullable(),
1202
+ folderId: z15.string().nullable(),
1203
+ folderName: z15.string().nullable(),
824
1204
  // Whether a publish token is still active — revocable from the
825
1205
  // dashboard (§6.18); the token itself is never returned.
826
- hasPublishToken: z12.boolean(),
1206
+ hasPublishToken: z15.boolean(),
827
1207
  // Owner preference: hide the version list from reviewers in the review
828
1208
  // shell. Managers still see it.
829
- hideVersions: z12.boolean(),
830
- id: z12.string(),
831
- openThreads: z12.number(),
832
- protected: z12.boolean(),
1209
+ hideVersions: z15.boolean(),
1210
+ id: z15.string(),
1211
+ openThreads: z15.number(),
1212
+ protected: z15.boolean(),
833
1213
  // Who published the current version (falling back to the claiming
834
1214
  // owner) — the dashboard shows a name and avatar per artifact so a
835
1215
  // shared workspace reads as people's work, not anonymous rows. Null
@@ -838,44 +1218,44 @@ var ownArtifactSchema = z12.object({
838
1218
  // SPEC-2 §5.8 — the review dial, so the dashboard's settings dialog can
839
1219
  // render current state without a second fetch.
840
1220
  reviewMode: reviewModeSchema,
841
- slug: z12.string(),
842
- title: z12.string(),
843
- totalViews: z12.number(),
1221
+ slug: z15.string(),
1222
+ title: z15.string(),
1223
+ totalViews: z15.number(),
844
1224
  // SPEC §6.20 — messages by others newer than the viewer's last-seen
845
1225
  // watermark. 0 for signed-out/API-key-less contexts.
846
- unreadComments: z12.number(),
847
- updatedAt: z12.string(),
848
- url: z12.string(),
849
- versionCount: z12.number(),
1226
+ unreadComments: z15.number(),
1227
+ updatedAt: z15.string(),
1228
+ url: z15.string(),
1229
+ versionCount: z15.number(),
850
1230
  // SPEC-2 §5.1 — whether the caller clears the manage bar for this
851
1231
  // artifact (workspace admin, or the person who first published it).
852
1232
  // Only a manager's folder move inherits the folder's visibility, so the
853
1233
  // dashboard warns exactly the people the change can actually affect, and
854
1234
  // only a manager is offered the Share entry (§5.7).
855
- viewerCanManage: z12.boolean(),
1235
+ viewerCanManage: z15.boolean(),
856
1236
  // SPEC-2 §5.2 — the artifact's own visibility, the single enforcement
857
1237
  // source. The dashboard needs it to tell whether a folder move would
858
1238
  // widen exposure before it sends the PATCH (§3.4), and to render the
859
1239
  // audience glyph — one per level — on rows and cards (§5.7).
860
1240
  visibility: artifactVisibilitySchema
861
1241
  });
862
- var ownArtifactsResponseSchema = z12.object({
863
- artifacts: z12.array(ownArtifactSchema)
1242
+ var ownArtifactsResponseSchema = z15.object({
1243
+ artifacts: z15.array(ownArtifactSchema)
864
1244
  });
865
- var revokePublishTokenResponseSchema = z12.object({
866
- revoked: z12.boolean()
1245
+ var revokePublishTokenResponseSchema = z15.object({
1246
+ revoked: z15.boolean()
867
1247
  });
868
1248
 
869
1249
  // ../shared/src/owner.ts
870
- import { z as z13 } from "zod";
1250
+ import { z as z16 } from "zod";
871
1251
  var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
872
- var claimResponseSchema = z13.object({
873
- artifactId: z13.string(),
874
- slug: z13.string(),
875
- title: z13.string(),
876
- url: z13.string().url()
1252
+ var claimResponseSchema = z16.object({
1253
+ artifactId: z16.string(),
1254
+ slug: z16.string(),
1255
+ title: z16.string(),
1256
+ url: z16.string().url()
877
1257
  });
878
- var updateArtifactSchema = z13.object({
1258
+ var updateArtifactSchema = z16.object({
879
1259
  // Custom icon; explicit null clears it back to the default glyph.
880
1260
  // Part of what the artifact *is* (like the title), so it takes the
881
1261
  // same manage rights as title/slug — not the member-level folder move.
@@ -883,159 +1263,159 @@ var updateArtifactSchema = z13.object({
883
1263
  // SPEC-2 §6.4 — placement + visibility. `folderId: null` clears the
884
1264
  // folder; `workspaceId: null` moves the artifact out of its workspace
885
1265
  // (§5.5 ownership transfer).
886
- folderId: z13.string().min(1).nullable().optional(),
1266
+ folderId: z16.string().min(1).nullable().optional(),
887
1267
  // Keep the version list out of the review shell. Presentation only:
888
1268
  // versions stay immutable, and a `/vN/` URL still resolves.
889
- hideVersions: z13.boolean().optional(),
890
- slug: z13.string().min(3).max(63).regex(SLUG_PATTERN, {
1269
+ hideVersions: z16.boolean().optional(),
1270
+ slug: z16.string().min(3).max(63).regex(SLUG_PATTERN, {
891
1271
  message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
892
1272
  }).optional(),
893
- title: z13.string().min(1).max(200).optional(),
1273
+ title: z16.string().min(1).max(200).optional(),
894
1274
  visibility: artifactVisibilitySchema.optional(),
895
- workspaceId: z13.string().min(1).nullable().optional()
1275
+ workspaceId: z16.string().min(1).nullable().optional()
896
1276
  }).refine(
897
1277
  (body) => body.title !== void 0 || body.slug !== void 0 || body.visibility !== void 0 || body.folderId !== void 0 || body.workspaceId !== void 0 || body.emoji !== void 0 || body.hideVersions !== void 0,
898
1278
  {
899
1279
  message: "provide at least one of title, slug, visibility, folderId, workspaceId, emoji, hideVersions"
900
1280
  }
901
1281
  );
902
- var deleteArtifactResponseSchema = z13.object({
903
- deleted: z13.literal(true)
1282
+ var deleteArtifactResponseSchema = z16.object({
1283
+ deleted: z16.literal(true)
904
1284
  });
905
1285
 
906
1286
  // ../shared/src/presence.ts
907
- import { z as z14 } from "zod";
1287
+ import { z as z17 } from "zod";
908
1288
  var AGENT_STATES = ["listening", "editing", "publishing"];
909
- var agentStateSchema = z14.enum(AGENT_STATES);
1289
+ var agentStateSchema = z17.enum(AGENT_STATES);
910
1290
  var AGENT_PRESENCE = {
911
1291
  editingTtlSeconds: 300,
912
1292
  heartbeatSeconds: 15,
913
1293
  shellPollSeconds: 15,
914
1294
  ttlSeconds: 45
915
1295
  };
916
- var agentHeartbeatBodySchema = z14.object({
1296
+ var agentHeartbeatBodySchema = z17.object({
917
1297
  state: agentStateSchema.optional()
918
1298
  });
919
- var agentHeartbeatResponseSchema = z14.object({
920
- listening: z14.literal(true),
1299
+ var agentHeartbeatResponseSchema = z17.object({
1300
+ listening: z17.literal(true),
921
1301
  state: agentStateSchema.optional(),
922
- ttlSeconds: z14.number().int().positive()
1302
+ ttlSeconds: z17.number().int().positive()
923
1303
  });
924
- var agentPresenceSchema = z14.object({
925
- agent: z14.object({ id: z14.string().nullable(), label: z14.string() }).nullable().default(null),
926
- listening: z14.boolean(),
1304
+ var agentPresenceSchema = z17.object({
1305
+ agent: z17.object({ id: z17.string().nullable(), label: z17.string() }).nullable().default(null),
1306
+ listening: z17.boolean(),
927
1307
  state: agentStateSchema.nullable().optional(),
928
- versionNumber: z14.number().int().positive().nullable()
1308
+ versionNumber: z17.number().int().positive().nullable()
929
1309
  });
930
1310
 
931
1311
  // ../shared/src/protection.ts
932
- import { z as z15 } from "zod";
1312
+ import { z as z18 } from "zod";
933
1313
  var PASSWORD_MIN = 6;
934
1314
  var PASSWORD_MAX = 128;
935
- var gatePasswordSchema = z15.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
936
- var setProtectionSchema = z15.object({
1315
+ var gatePasswordSchema = z18.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
1316
+ var setProtectionSchema = z18.object({
937
1317
  password: gatePasswordSchema.nullable()
938
1318
  });
939
- var protectionResponseSchema = z15.object({
940
- protected: z15.boolean()
1319
+ var protectionResponseSchema = z18.object({
1320
+ protected: z18.boolean()
941
1321
  });
942
- var unlockSchema = z15.object({
943
- password: z15.string().min(1).max(1024)
1322
+ var unlockSchema = z18.object({
1323
+ password: z18.string().min(1).max(1024)
944
1324
  });
945
- var unlockResponseSchema = z15.object({
946
- unlocked: z15.literal(true)
1325
+ var unlockResponseSchema = z18.object({
1326
+ unlocked: z18.literal(true)
947
1327
  });
948
1328
 
949
1329
  // ../shared/src/report.ts
950
- import { z as z16 } from "zod";
951
- var reportRequestSchema = z16.object({
952
- reason: z16.string().max(1e3).optional()
1330
+ import { z as z19 } from "zod";
1331
+ var reportRequestSchema = z19.object({
1332
+ reason: z19.string().max(1e3).optional()
953
1333
  });
954
- var reportResponseSchema = z16.object({
955
- reported: z16.literal(true)
1334
+ var reportResponseSchema = z19.object({
1335
+ reported: z19.literal(true)
956
1336
  });
957
1337
 
958
1338
  // ../shared/src/reviewers.ts
959
- import { z as z17 } from "zod";
960
- var artifactReviewerSchema = z17.object({
961
- avatarUrl: z17.string().nullable(),
962
- displayName: z17.string(),
1339
+ import { z as z20 } from "zod";
1340
+ var artifactReviewerSchema = z20.object({
1341
+ avatarUrl: z20.string().nullable(),
1342
+ displayName: z20.string(),
963
1343
  // Exactly one of userId/guestId is set, per `kind`.
964
- guestId: z17.string().nullable(),
1344
+ guestId: z20.string().nullable(),
965
1345
  // "account", not commentAuthorSchema's "member" — the whole point here is
966
1346
  // that these people need not be workspace members. A guest can be named in
967
1347
  // a comment but never notified: guest_identities carries a display name and
968
1348
  // no email, so there is nowhere to send anything.
969
- kind: z17.enum(["account", "guest"]),
970
- userId: z17.string().nullable()
1349
+ kind: z20.enum(["account", "guest"]),
1350
+ userId: z20.string().nullable()
971
1351
  });
972
- var artifactReviewerListResponseSchema = z17.object({
973
- reviewers: z17.array(artifactReviewerSchema)
1352
+ var artifactReviewerListResponseSchema = z20.object({
1353
+ reviewers: z20.array(artifactReviewerSchema)
974
1354
  });
975
1355
 
976
1356
  // ../shared/src/stats.ts
977
- import { z as z18 } from "zod";
978
- var avgViewSecondsSchema = z18.number().int().nonnegative().nullable();
979
- var versionViewStatsSchema = z18.object({
1357
+ import { z as z21 } from "zod";
1358
+ var avgViewSecondsSchema = z21.number().int().nonnegative().nullable();
1359
+ var versionViewStatsSchema = z21.object({
980
1360
  avgViewSeconds: avgViewSecondsSchema,
981
1361
  // Views from inside the review shell's iframe vs. direct visits to
982
1362
  // the render URL. views = embeddedViews + directViews.
983
- directViews: z18.number().int().nonnegative(),
984
- embeddedViews: z18.number().int().nonnegative(),
1363
+ directViews: z21.number().int().nonnegative(),
1364
+ embeddedViews: z21.number().int().nonnegative(),
985
1365
  // Distinct hashed viewer IPs across the version's lifetime.
986
- uniqueViewers: z18.number().int().nonnegative(),
987
- versionNumber: z18.number().int().positive(),
988
- views: z18.number().int().nonnegative()
1366
+ uniqueViewers: z21.number().int().nonnegative(),
1367
+ versionNumber: z21.number().int().positive(),
1368
+ views: z21.number().int().nonnegative()
989
1369
  });
990
- var dailyViewsSchema = z18.object({
1370
+ var dailyViewsSchema = z21.object({
991
1371
  avgViewSeconds: avgViewSecondsSchema,
992
1372
  // UTC day, YYYY-MM-DD.
993
- day: z18.string(),
994
- uniqueViewers: z18.number().int().nonnegative(),
995
- views: z18.number().int().nonnegative()
1373
+ day: z21.string(),
1374
+ uniqueViewers: z21.number().int().nonnegative(),
1375
+ views: z21.number().int().nonnegative()
996
1376
  });
997
- var artifactStatsResponseSchema = z18.object({
1377
+ var artifactStatsResponseSchema = z21.object({
998
1378
  avgViewSeconds: avgViewSecondsSchema,
999
- daily: z18.array(dailyViewsSchema),
1379
+ daily: z21.array(dailyViewsSchema),
1000
1380
  // Distinct hashed viewer IPs across all versions and days.
1001
- totalUniqueViewers: z18.number().int().nonnegative(),
1002
- totalViews: z18.number().int().nonnegative(),
1003
- versions: z18.array(versionViewStatsSchema)
1381
+ totalUniqueViewers: z21.number().int().nonnegative(),
1382
+ totalViews: z21.number().int().nonnegative(),
1383
+ versions: z21.array(versionViewStatsSchema)
1004
1384
  });
1005
1385
 
1006
1386
  // ../shared/src/versions.ts
1007
- import { z as z19 } from "zod";
1008
- var versionSourceSchema = z19.object({
1009
- agentName: z19.string().nullable(),
1010
- type: z19.string()
1387
+ import { z as z22 } from "zod";
1388
+ var versionSourceSchema = z22.object({
1389
+ agentName: z22.string().nullable(),
1390
+ type: z22.string()
1011
1391
  });
1012
- var versionChangesSchema = z19.object({
1013
- added: z19.array(z19.string()),
1014
- changed: z19.array(z19.string()),
1015
- removed: z19.array(z19.string())
1392
+ var versionChangesSchema = z22.object({
1393
+ added: z22.array(z22.string()),
1394
+ changed: z22.array(z22.string()),
1395
+ removed: z22.array(z22.string())
1016
1396
  });
1017
- var versionSummarySchema = z19.object({
1397
+ var versionSummarySchema = z22.object({
1018
1398
  // Absent for the first version — there is no previous version to diff against.
1019
1399
  changes: versionChangesSchema.nullable(),
1020
- createdAt: z19.string(),
1021
- entryUrl: z19.string().url(),
1022
- fileCount: z19.number().int().nonnegative(),
1023
- id: z19.string(),
1024
- isCurrent: z19.boolean(),
1400
+ createdAt: z22.string(),
1401
+ entryUrl: z22.string().url(),
1402
+ fileCount: z22.number().int().nonnegative(),
1403
+ id: z22.string(),
1404
+ isCurrent: z22.boolean(),
1025
1405
  // Author-supplied bullet-point changelog; null when the publish omitted it.
1026
- notes: z19.string().nullable(),
1406
+ notes: z22.string().nullable(),
1027
1407
  // The account that published this version, with their own avatar where
1028
1408
  // they've set one. Any workspace member can publish a new version
1029
1409
  // (SPEC-2 §5.1), so "which teammate" outranks "which agent" — the agent
1030
1410
  // is the tool, this is the person. Null for token-only publishes, which
1031
1411
  // carry no account.
1032
- publishedBy: z19.object({ image: z19.string().nullable(), name: z19.string() }).nullable(),
1412
+ publishedBy: z22.object({ image: z22.string().nullable(), name: z22.string() }).nullable(),
1033
1413
  source: versionSourceSchema,
1034
- totalSizeBytes: z19.number().int().nonnegative(),
1035
- versionNumber: z19.number().int().positive()
1414
+ totalSizeBytes: z22.number().int().nonnegative(),
1415
+ versionNumber: z22.number().int().positive()
1036
1416
  });
1037
- var versionListResponseSchema = z19.object({
1038
- versions: z19.array(versionSummarySchema)
1417
+ var versionListResponseSchema = z22.object({
1418
+ versions: z22.array(versionSummarySchema)
1039
1419
  });
1040
1420
 
1041
1421
  // src/agent-identity.ts
@@ -1120,6 +1500,13 @@ function createApiClient(apiUrl2, publishToken) {
1120
1500
  (d) => agentHeartbeatResponseSchema.parse(d)
1121
1501
  );
1122
1502
  },
1503
+ async clearData(artifactId, collection) {
1504
+ return request(
1505
+ `${base}/api/v1/artifacts/${artifactId}/data/${encodeURIComponent(collection)}`,
1506
+ { headers, method: "DELETE" },
1507
+ (d) => dataClearResponseSchema.parse(d)
1508
+ );
1509
+ },
1123
1510
  // SPEC-2 §6.3 — create a folder in a workspace. Account key required
1124
1511
  // (owner/admin; in a personal workspace that's always the user).
1125
1512
  async createFolder(workspaceId, body) {
@@ -1202,6 +1589,28 @@ function createApiClient(apiUrl2, publishToken) {
1202
1589
  (d) => artifactResponseSchema.parse(d)
1203
1590
  );
1204
1591
  },
1592
+ async getDataSummary(artifactId) {
1593
+ return request(
1594
+ `${base}/api/v1/artifacts/${artifactId}/data`,
1595
+ { headers, method: "GET" },
1596
+ (d) => dataSummaryResponseSchema.parse(d)
1597
+ );
1598
+ },
1599
+ async insertData(artifactId, collection, fields) {
1600
+ return request(
1601
+ `${base}/api/v1/artifacts/${artifactId}/data/${encodeURIComponent(collection)}`,
1602
+ { body: JSON.stringify(fields), headers, method: "POST" },
1603
+ (d) => dataRecordResponseSchema.parse(d).record
1604
+ );
1605
+ },
1606
+ /** Owner-plane batch: up to DATA_LIMITS.maxInsertBatch, all-or-nothing. */
1607
+ async insertDataBatch(artifactId, collection, records) {
1608
+ return request(
1609
+ `${base}/api/v1/artifacts/${artifactId}/data/${encodeURIComponent(collection)}`,
1610
+ { body: JSON.stringify(records), headers, method: "POST" },
1611
+ (d) => dataBatchInsertResponseSchema.parse(d)
1612
+ );
1613
+ },
1205
1614
  // SPEC §6.18 — everything owned by the account this client's API
1206
1615
  // key (or session) belongs to.
1207
1616
  async listArtifacts() {
@@ -1211,6 +1620,21 @@ function createApiClient(apiUrl2, publishToken) {
1211
1620
  (d) => artifactListResponseSchema.parse(d)
1212
1621
  );
1213
1622
  },
1623
+ async listData(artifactId, collection, query = {}) {
1624
+ const params = new URLSearchParams();
1625
+ if (query.limit !== void 0) {
1626
+ params.set("limit", String(query.limit));
1627
+ }
1628
+ if (query.cursor !== void 0) {
1629
+ params.set("cursor", query.cursor);
1630
+ }
1631
+ const qs = params.toString();
1632
+ return request(
1633
+ `${base}/api/v1/artifacts/${artifactId}/data/${encodeURIComponent(collection)}${qs ? `?${qs}` : ""}`,
1634
+ { headers, method: "GET" },
1635
+ (d) => dataListResponseSchema.parse(d)
1636
+ );
1637
+ },
1214
1638
  async listFolders(workspaceId) {
1215
1639
  return request(
1216
1640
  `${base}/api/v1/workspaces/${workspaceId}/folders`,
@@ -1290,6 +1714,13 @@ function createApiClient(apiUrl2, publishToken) {
1290
1714
  () => void 0
1291
1715
  );
1292
1716
  },
1717
+ async setExpiration(artifactId, expiresInSeconds) {
1718
+ return request(
1719
+ `${base}/api/v1/artifacts/${artifactId}/expiration`,
1720
+ { body: JSON.stringify({ expiresInSeconds }), headers, method: "PUT" },
1721
+ (d) => expirationResponseSchema.parse(d)
1722
+ );
1723
+ },
1293
1724
  // SPEC §6.16 — set a password (string) or clear it (null). Authorized
1294
1725
  // by the publish token this client was created with.
1295
1726
  async setProtection(artifactId, password) {
@@ -1346,17 +1777,17 @@ import {
1346
1777
  writeFileSync as writeFileSync2
1347
1778
  } from "fs";
1348
1779
  import path2 from "path";
1349
- import { z as z21 } from "zod";
1780
+ import { z as z24 } from "zod";
1350
1781
 
1351
1782
  // src/credentials.ts
1352
1783
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
1353
1784
  import { homedir } from "os";
1354
1785
  import path from "path";
1355
- import { z as z20 } from "zod";
1786
+ import { z as z23 } from "zod";
1356
1787
  var DEFAULT_API_URL = "https://artifact.cafe";
1357
- var credentialsSchema = z20.object({
1358
- keys: z20.record(z20.string(), z20.string()),
1359
- version: z20.literal(1)
1788
+ var credentialsSchema = z23.object({
1789
+ keys: z23.record(z23.string(), z23.string()),
1790
+ version: z23.literal(1)
1360
1791
  });
1361
1792
  function credentialsPath() {
1362
1793
  return path.join(homedir(), ".artifactcafe", "credentials");
@@ -1406,24 +1837,24 @@ function resolveApiKey(apiUrl2, flag) {
1406
1837
  }
1407
1838
 
1408
1839
  // src/config.ts
1409
- var configSchema = z21.object({
1410
- apiUrl: z21.string(),
1411
- artifactId: z21.string(),
1412
- folder: z21.string().optional(),
1413
- folderId: z21.string().optional(),
1840
+ var configSchema = z24.object({
1841
+ apiUrl: z24.string(),
1842
+ artifactId: z24.string(),
1843
+ folder: z24.string().optional(),
1844
+ folderId: z24.string().optional(),
1414
1845
  // Optional: a folder linked by `link` to an artifact you own carries no
1415
1846
  // token — owner actions authorize via your stored login instead. A first
1416
1847
  // publish still writes the anonymous-mode token here.
1417
- publishToken: z21.string().optional(),
1418
- slug: z21.string(),
1419
- url: z21.string(),
1420
- workspace: z21.string().optional(),
1421
- workspaceId: z21.string().optional(),
1422
- workspaceKind: z21.enum(["personal", "workspace"]).optional()
1423
- });
1424
- var projectConfigSchema = z21.object({
1425
- folder: z21.string().trim().min(1).optional(),
1426
- workspace: z21.string().trim().min(1)
1848
+ publishToken: z24.string().optional(),
1849
+ slug: z24.string(),
1850
+ url: z24.string(),
1851
+ workspace: z24.string().optional(),
1852
+ workspaceId: z24.string().optional(),
1853
+ workspaceKind: z24.enum(["personal", "workspace"]).optional()
1854
+ });
1855
+ var projectConfigSchema = z24.object({
1856
+ folder: z24.string().trim().min(1).optional(),
1857
+ workspace: z24.string().trim().min(1)
1427
1858
  }).strict();
1428
1859
  var CONFIG_DIR = ".artifactcafe";
1429
1860
  var CONFIG_FILE = "config.json";
@@ -1459,7 +1890,7 @@ function readProjectConfig(dir) {
1459
1890
  JSON.parse(readFileSync2(file, "utf8"))
1460
1891
  );
1461
1892
  } catch (error) {
1462
- const message = error instanceof z21.ZodError ? error.issues[0]?.message ?? "invalid project target" : "invalid JSON";
1893
+ const message = error instanceof z24.ZodError ? error.issues[0]?.message ?? "invalid project target" : "invalid JSON";
1463
1894
  throw new Error(`Invalid ${file}: ${message}.`);
1464
1895
  }
1465
1896
  }
@@ -1669,27 +2100,27 @@ async function comments(opts) {
1669
2100
  });
1670
2101
  return;
1671
2102
  }
1672
- const data = await client.exportComments(target.artifactId, {
2103
+ const data2 = await client.exportComments(target.artifactId, {
1673
2104
  status: status2,
1674
2105
  version
1675
2106
  });
1676
2107
  if (opts.json) {
1677
- console.log(JSON.stringify(data, null, 2));
2108
+ console.log(JSON.stringify(data2, null, 2));
1678
2109
  return;
1679
2110
  }
1680
- printHuman(data, url, status2, version);
2111
+ printHuman(data2, url, status2, version);
1681
2112
  }
1682
2113
  async function deriveReviewUrl(client, target) {
1683
2114
  const artifact = await client.getArtifact(target.artifactId);
1684
2115
  return reviewUrl(target.apiUrl, artifact.slug);
1685
2116
  }
1686
2117
  var sleep = (s) => new Promise((r) => setTimeout(r, s * 1e3));
1687
- function messageCounts(data) {
1688
- return new Map(data.comments.map((t) => [t.id, 1 + t.replies.length]));
2118
+ function messageCounts(data2) {
2119
+ return new Map(data2.comments.map((t) => [t.id, 1 + t.replies.length]));
1689
2120
  }
1690
- function diffThreads(baseline, data) {
2121
+ function diffThreads(baseline, data2) {
1691
2122
  const changed = [];
1692
- for (const t of data.comments) {
2123
+ for (const t of data2.comments) {
1693
2124
  const before = baseline.get(t.id);
1694
2125
  if (before === void 0) {
1695
2126
  changed.push({ ...t, change: "new" });
@@ -1732,8 +2163,8 @@ async function waitForFeedback(client, config, opts) {
1732
2163
  while (Date.now() < deadline) {
1733
2164
  await sleep(Math.min(pollSeconds(), (deadline - Date.now()) / 1e3));
1734
2165
  await heartbeat();
1735
- const data = await client.exportComments(config.artifactId, query);
1736
- if (diffThreads(baseline, data).length === 0) {
2166
+ const data2 = await client.exportComments(config.artifactId, query);
2167
+ if (diffThreads(baseline, data2).length === 0) {
1737
2168
  continue;
1738
2169
  }
1739
2170
  await sleep(settleSeconds());
@@ -1766,7 +2197,7 @@ async function waitForFeedback(client, config, opts) {
1766
2197
  }
1767
2198
  process.exit(WAIT_TIMEOUT_EXIT);
1768
2199
  }
1769
- function printWaitResult(config, data, changed, json) {
2200
+ function printWaitResult(config, data2, changed, json) {
1770
2201
  if (json) {
1771
2202
  console.log(
1772
2203
  JSON.stringify(
@@ -1774,7 +2205,7 @@ function printWaitResult(config, data, changed, json) {
1774
2205
  artifactId: config.artifactId,
1775
2206
  threads: changed,
1776
2207
  timedOut: false,
1777
- versionNumber: data.versionNumber
2208
+ versionNumber: data2.versionNumber
1778
2209
  },
1779
2210
  null,
1780
2211
  2
@@ -1793,22 +2224,22 @@ ${changed.length} thread${plural} with new feedback \u2014 ${config.url}
1793
2224
  printThread(thread);
1794
2225
  }
1795
2226
  }
1796
- function printHuman(data, url, status2, version) {
2227
+ function printHuman(data2, url, status2, version) {
1797
2228
  const statusLabel = status2 === "all" ? "" : `${status2} `;
1798
2229
  let scope = "across all versions";
1799
2230
  if (version === "current") {
1800
- scope = data.versionNumber === null ? "on the current version" : `on v${data.versionNumber}`;
2231
+ scope = data2.versionNumber === null ? "on the current version" : `on v${data2.versionNumber}`;
1801
2232
  }
1802
- if (data.comments.length === 0) {
2233
+ if (data2.comments.length === 0) {
1803
2234
  console.log(`No ${statusLabel}comments ${scope}.`);
1804
2235
  return;
1805
2236
  }
1806
- const plural = data.comments.length === 1 ? "" : "s";
2237
+ const plural = data2.comments.length === 1 ? "" : "s";
1807
2238
  console.log(
1808
- `${data.comments.length} ${statusLabel}thread${plural} ${scope} \u2014 ${url}
2239
+ `${data2.comments.length} ${statusLabel}thread${plural} ${scope} \u2014 ${url}
1809
2240
  `
1810
2241
  );
1811
- for (const thread of data.comments) {
2242
+ for (const thread of data2.comments) {
1812
2243
  printThread(thread);
1813
2244
  }
1814
2245
  }
@@ -1853,18 +2284,230 @@ function indent(text, pad) {
1853
2284
  }
1854
2285
 
1855
2286
  // src/dashboard.ts
1856
- import path7 from "path";
2287
+ import path8 from "path";
1857
2288
 
1858
2289
  // src/publish.ts
1859
2290
  import { exec } from "child_process";
1860
- import { readFileSync as readFileSync4 } from "fs";
1861
- import path6 from "path";
2291
+ import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
2292
+ import path7 from "path";
1862
2293
  import { createInterface } from "readline";
1863
2294
 
2295
+ // src/data.ts
2296
+ import { readFileSync as readFileSync3 } from "fs";
2297
+ import path5 from "path";
2298
+ function inspectDataManifest(files) {
2299
+ const info = { announcements: [] };
2300
+ const manifestFile = files.find((f) => f.path === DATA_MANIFEST_PATH);
2301
+ if (manifestFile) {
2302
+ const parsed = parseDataManifest(
2303
+ readFileSync3(manifestFile.absPath, "utf8")
2304
+ );
2305
+ if (!parsed.ok) {
2306
+ throw new Error(
2307
+ `${DATA_MANIFEST_PATH} is invalid:
2308
+ ${(parsed.errors ?? []).map((e) => ` - ${e}`).join("\n")}`
2309
+ );
2310
+ }
2311
+ info.config = parsed.config;
2312
+ for (const [name, collection] of Object.entries(
2313
+ parsed.config?.collections ?? {}
2314
+ )) {
2315
+ const bits = [
2316
+ `read: ${collection.access.read}`,
2317
+ `insert: ${collection.access.insert}`
2318
+ ];
2319
+ if (collection.access.update !== "owner") {
2320
+ bits.push(`update: ${collection.access.update}`);
2321
+ }
2322
+ if (collection.access.delete !== "owner") {
2323
+ bits.push(`delete: ${collection.access.delete}`);
2324
+ }
2325
+ if (collection.rateLimit) {
2326
+ bits.push(collection.rateLimit);
2327
+ }
2328
+ info.announcements.push(`\u26C1 Data enabled: ${name} (${bits.join(", ")})`);
2329
+ }
2330
+ if (info.announcements.length === 0) {
2331
+ info.announcements.push(
2332
+ "\u26C1 Data disabled (empty collections in .artifact/data.json)"
2333
+ );
2334
+ }
2335
+ }
2336
+ return info;
2337
+ }
2338
+ function requireLinkedArtifact(dir) {
2339
+ const resolved = path5.resolve(dir);
2340
+ const config = readConfig(resolved);
2341
+ if (!config) {
2342
+ throw new Error(
2343
+ `No artifact linked in ${resolved} \u2014 run \`artifact-cafe publish\` there first (.artifactcafe/config.json not found).`
2344
+ );
2345
+ }
2346
+ return {
2347
+ apiUrl: process.env.ARTIFACT_CAFE_URL ?? config.apiUrl,
2348
+ artifactId: config.artifactId,
2349
+ publishToken: resolveApiKey(process.env.ARTIFACT_CAFE_URL ?? config.apiUrl) ?? config.publishToken ?? process.env.ARTIFACT_CAFE_TOKEN,
2350
+ url: config.url
2351
+ };
2352
+ }
2353
+ async function dataLs(opts) {
2354
+ const linked = requireLinkedArtifact(opts.dir);
2355
+ const client = createApiClient(linked.apiUrl, linked.publishToken);
2356
+ const records = [];
2357
+ let cursor;
2358
+ do {
2359
+ const page = await client.listData(linked.artifactId, opts.collection, {
2360
+ cursor,
2361
+ limit: 200
2362
+ });
2363
+ records.push(...page.records);
2364
+ cursor = page.nextCursor ?? void 0;
2365
+ } while (cursor && (opts.limit === void 0 || records.length < opts.limit));
2366
+ const shown = opts.limit === void 0 ? records : records.slice(0, opts.limit);
2367
+ if (opts.json) {
2368
+ console.log(
2369
+ JSON.stringify({ collection: opts.collection, records: shown }, null, 2)
2370
+ );
2371
+ return;
2372
+ }
2373
+ if (shown.length === 0) {
2374
+ console.log(`No records in "${opts.collection}".`);
2375
+ return;
2376
+ }
2377
+ const plural = shown.length === 1 ? "" : "s";
2378
+ console.log(
2379
+ `${shown.length} record${plural} in "${opts.collection}" \u2014 ${linked.url}
2380
+ `
2381
+ );
2382
+ for (const record of shown) {
2383
+ const { created_at, created_by, id, updated_at: _u, ...fields } = record;
2384
+ const author = created_by ?? "anonymous";
2385
+ console.log(`\u25CF ${id} \xB7 ${String(created_at).slice(0, 10)} \xB7 ${author}`);
2386
+ console.log(` ${JSON.stringify(fields)}`);
2387
+ }
2388
+ }
2389
+ var RECORD_SHAPE_HINT = `a JSON object or array of objects, e.g. '{"name":"Ada"}'`;
2390
+ function isRecord(value) {
2391
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2392
+ }
2393
+ async function dataInsert(opts) {
2394
+ if (opts.record === void 0 === (opts.file === void 0)) {
2395
+ throw new Error("Pass exactly one of --record '<json>' or --file <path>.");
2396
+ }
2397
+ const text = opts.record ?? readFileSync3(opts.file, "utf8");
2398
+ let parsed;
2399
+ try {
2400
+ parsed = JSON.parse(text);
2401
+ } catch {
2402
+ throw new Error(`Records must be ${RECORD_SHAPE_HINT}.`);
2403
+ }
2404
+ const linked = requireLinkedArtifact(opts.dir);
2405
+ const client = createApiClient(linked.apiUrl, linked.publishToken);
2406
+ if (isRecord(parsed)) {
2407
+ const record = await client.insertData(
2408
+ linked.artifactId,
2409
+ opts.collection,
2410
+ parsed
2411
+ );
2412
+ if (opts.json) {
2413
+ console.log(JSON.stringify({ collection: opts.collection, record }));
2414
+ return;
2415
+ }
2416
+ console.log(`Inserted ${record.id} into "${opts.collection}".`);
2417
+ return;
2418
+ }
2419
+ if (!(Array.isArray(parsed) && parsed.length > 0 && parsed.every(isRecord))) {
2420
+ throw new Error(`Records must be ${RECORD_SHAPE_HINT}.`);
2421
+ }
2422
+ const records = [];
2423
+ for (let i = 0; i < parsed.length; i += DATA_LIMITS.maxInsertBatch) {
2424
+ try {
2425
+ const page = await client.insertDataBatch(
2426
+ linked.artifactId,
2427
+ opts.collection,
2428
+ parsed.slice(i, i + DATA_LIMITS.maxInsertBatch)
2429
+ );
2430
+ records.push(...page.records);
2431
+ } catch (err) {
2432
+ if (records.length === 0) {
2433
+ throw err;
2434
+ }
2435
+ const note = ` (the first ${records.length} records were inserted; records ${i}+ may not have been)`;
2436
+ if (err instanceof ApiRequestError) {
2437
+ throw new ApiRequestError(
2438
+ err.status,
2439
+ err.code,
2440
+ `${err.message}${note}`,
2441
+ err.retryAfter
2442
+ );
2443
+ }
2444
+ throw new Error(
2445
+ `${err instanceof Error ? err.message : String(err)}${note}`
2446
+ );
2447
+ }
2448
+ }
2449
+ if (opts.json) {
2450
+ console.log(JSON.stringify({ collection: opts.collection, records }));
2451
+ return;
2452
+ }
2453
+ const plural = records.length === 1 ? "" : "s";
2454
+ console.log(
2455
+ `Inserted ${records.length} record${plural} into "${opts.collection}".`
2456
+ );
2457
+ }
2458
+ async function dataExport(opts) {
2459
+ const linked = requireLinkedArtifact(opts.dir);
2460
+ const client = createApiClient(linked.apiUrl, linked.publishToken);
2461
+ const summary = await client.getDataSummary(linked.artifactId);
2462
+ const collections = {};
2463
+ for (const { name } of summary.collections) {
2464
+ const records = [];
2465
+ let cursor;
2466
+ do {
2467
+ const page = await client.listData(linked.artifactId, name, {
2468
+ cursor,
2469
+ limit: 200
2470
+ });
2471
+ records.push(...page.records);
2472
+ cursor = page.nextCursor ?? void 0;
2473
+ } while (cursor);
2474
+ collections[name] = records;
2475
+ }
2476
+ console.log(
2477
+ JSON.stringify(
2478
+ {
2479
+ artifactId: linked.artifactId,
2480
+ collections,
2481
+ enabled: summary.enabled,
2482
+ url: linked.url
2483
+ },
2484
+ null,
2485
+ 2
2486
+ )
2487
+ );
2488
+ }
2489
+ async function dataClear(opts) {
2490
+ if (!opts.yes) {
2491
+ throw new Error(
2492
+ `Clearing "${opts.collection}" deletes every record in it. Re-run with --yes to confirm.`
2493
+ );
2494
+ }
2495
+ const linked = requireLinkedArtifact(opts.dir);
2496
+ const client = createApiClient(linked.apiUrl, linked.publishToken);
2497
+ const result = await client.clearData(linked.artifactId, opts.collection);
2498
+ if (opts.json) {
2499
+ console.log(
2500
+ JSON.stringify({ cleared: result.cleared, collection: opts.collection })
2501
+ );
2502
+ return;
2503
+ }
2504
+ console.log(`Cleared ${result.cleared} record(s) from "${opts.collection}".`);
2505
+ }
2506
+
1864
2507
  // src/manifest.ts
1865
2508
  import { createHash } from "crypto";
1866
- import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
1867
- import path5 from "path";
2509
+ import { lstatSync, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
2510
+ import path6 from "path";
1868
2511
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".artifactcafe"]);
1869
2512
  var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store", "artifact-cafe.json"]);
1870
2513
  function isIgnoredFile(name) {
@@ -1885,6 +2528,7 @@ function isIgnoredFile(name) {
1885
2528
  var MIME_TYPES = {
1886
2529
  ".avif": "image/avif",
1887
2530
  ".css": "text/css",
2531
+ ".csv": "text/csv",
1888
2532
  ".gif": "image/gif",
1889
2533
  ".htm": "text/html",
1890
2534
  ".html": "text/html",
@@ -1894,6 +2538,7 @@ var MIME_TYPES = {
1894
2538
  ".js": "text/javascript",
1895
2539
  ".json": "application/json",
1896
2540
  ".map": "application/json",
2541
+ ".markdown": "text/markdown",
1897
2542
  ".md": "text/markdown",
1898
2543
  ".mjs": "text/javascript",
1899
2544
  ".mp3": "audio/mpeg",
@@ -1913,7 +2558,32 @@ var MIME_TYPES = {
1913
2558
  ".xml": "application/xml"
1914
2559
  };
1915
2560
  function mimeFor(filePath) {
1916
- return MIME_TYPES[path5.extname(filePath).toLowerCase()] ?? "application/octet-stream";
2561
+ return MIME_TYPES[path6.extname(filePath).toLowerCase()] ?? "application/octet-stream";
2562
+ }
2563
+ function localFile(absPath, rel) {
2564
+ if (isIgnoredFile(path6.basename(rel)) || !isSafeArtifactPath(rel)) {
2565
+ return null;
2566
+ }
2567
+ const { size } = statSync(absPath);
2568
+ if (size === 0) {
2569
+ return null;
2570
+ }
2571
+ return {
2572
+ absPath,
2573
+ hash: `sha256_${createHash("sha256").update(readFileSync4(absPath)).digest("hex")}`,
2574
+ mimeType: mimeFor(rel),
2575
+ path: rel,
2576
+ size
2577
+ };
2578
+ }
2579
+ function scanPath(target) {
2580
+ const stat = lstatSync(target);
2581
+ if (stat.isDirectory()) {
2582
+ return scanDirectory(target);
2583
+ }
2584
+ const rel = path6.basename(target);
2585
+ const file = stat.isFile() ? localFile(target, rel) : null;
2586
+ return file ? { files: [file], skipped: [] } : { files: [], skipped: [rel] };
1917
2587
  }
1918
2588
  function scanDirectory(dir) {
1919
2589
  const files = [];
@@ -1921,7 +2591,7 @@ function scanDirectory(dir) {
1921
2591
  function walk(current, relPrefix) {
1922
2592
  for (const entry of readdirSync(current, { withFileTypes: true })) {
1923
2593
  const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
1924
- const abs = path5.join(current, entry.name);
2594
+ const abs = path6.join(current, entry.name);
1925
2595
  if (entry.isSymbolicLink()) {
1926
2596
  skipped.push(rel);
1927
2597
  continue;
@@ -1937,23 +2607,12 @@ function scanDirectory(dir) {
1937
2607
  if (!entry.isFile()) {
1938
2608
  continue;
1939
2609
  }
1940
- if (isIgnoredFile(entry.name) || !isSafeArtifactPath(rel)) {
2610
+ const file = localFile(abs, rel);
2611
+ if (!file) {
1941
2612
  skipped.push(rel);
1942
2613
  continue;
1943
2614
  }
1944
- const { size } = statSync(abs);
1945
- if (size === 0) {
1946
- skipped.push(rel);
1947
- continue;
1948
- }
1949
- const content = readFileSync3(abs);
1950
- files.push({
1951
- absPath: abs,
1952
- hash: `sha256_${createHash("sha256").update(content).digest("hex")}`,
1953
- mimeType: mimeFor(rel),
1954
- path: rel,
1955
- size
1956
- });
2615
+ files.push(file);
1957
2616
  }
1958
2617
  }
1959
2618
  walk(dir, "");
@@ -1961,27 +2620,37 @@ function scanDirectory(dir) {
1961
2620
  return { files, skipped };
1962
2621
  }
1963
2622
  function resolveEntry(files, explicit) {
1964
- if (explicit) {
1965
- if (!files.some((f) => f.path === explicit)) {
1966
- throw new Error(
1967
- `Entry file "${explicit}" not found in the publish folder.`
1968
- );
2623
+ return resolvePublishEntry(files, explicit);
2624
+ }
2625
+
2626
+ // src/spinner.ts
2627
+ var FRAMES = ["\xE3", "\xE4", "\xE5"];
2628
+ var INTERVAL_MS = 420;
2629
+ var AMBER2 = "\x1B[38;2;214;138;57m";
2630
+ var RESET2 = "\x1B[0m";
2631
+ var CLEAR_LINE = "\r\x1B[2K";
2632
+ function startBrewing(label, enabled) {
2633
+ const stream = process.stderr;
2634
+ if (!(enabled && stream.isTTY) || process.env.CI) {
2635
+ return { stop: () => void 0 };
2636
+ }
2637
+ const color = !process.env.NO_COLOR;
2638
+ let frame = 0;
2639
+ const draw = () => {
2640
+ const glyph = FRAMES[frame % FRAMES.length];
2641
+ stream.write(
2642
+ `${CLEAR_LINE} ${color ? `${AMBER2}${glyph}${RESET2}` : glyph} ${label}`
2643
+ );
2644
+ frame += 1;
2645
+ };
2646
+ draw();
2647
+ const timer = setInterval(draw, INTERVAL_MS);
2648
+ return {
2649
+ stop() {
2650
+ clearInterval(timer);
2651
+ stream.write(CLEAR_LINE);
1969
2652
  }
1970
- return explicit;
1971
- }
1972
- if (files.some((f) => f.path === "index.html")) {
1973
- return "index.html";
1974
- }
1975
- const htmlFiles = files.filter(
1976
- (f) => f.path.endsWith(".html") && !f.path.includes("/")
1977
- );
1978
- const [onlyHtmlFile] = htmlFiles;
1979
- if (onlyHtmlFile && htmlFiles.length === 1) {
1980
- return onlyHtmlFile.path;
1981
- }
1982
- throw new Error(
1983
- htmlFiles.length === 0 ? "No entry file found. Add an index.html or pass --entry <file>." : `Multiple HTML files found (${htmlFiles.map((f) => f.path).join(", ")}). Pass --entry <file>.`
1984
- );
2653
+ };
1985
2654
  }
1986
2655
 
1987
2656
  // src/publish.ts
@@ -1992,8 +2661,10 @@ var REVIEW_LABEL = {
1992
2661
  readonly: "read-only \u2014 existing comments only"
1993
2662
  };
1994
2663
  async function publish(opts) {
1995
- const dir = path6.resolve(opts.dir);
2664
+ const target = path7.resolve(opts.dir);
2665
+ const dir = statSync2(target).isFile() ? path7.dirname(target) : target;
1996
2666
  const config = readConfig(dir);
2667
+ const expiresInSeconds = opts.expiresIn === void 0 ? void 0 : parseArtifactDuration(opts.expiresIn);
1997
2668
  if (opts.password !== void 0) {
1998
2669
  const parsed = gatePasswordSchema.safeParse(opts.password);
1999
2670
  if (!parsed.success) {
@@ -2018,13 +2689,14 @@ async function publish(opts) {
2018
2689
  apiUrl: apiUrl2,
2019
2690
  config,
2020
2691
  isNewArtifact: artifactId === null,
2021
- opts
2692
+ opts: { ...opts, dir }
2022
2693
  });
2023
- const { files, skipped } = scanDirectory(dir);
2694
+ const { files, skipped } = scanPath(target);
2024
2695
  if (files.length === 0) {
2025
- throw new Error(`Nothing to publish in ${dir} (after ignore rules).`);
2696
+ throw new Error(`Nothing to publish at ${target} (after ignore rules).`);
2026
2697
  }
2027
2698
  const entryPath = resolveEntry(files, opts.entry);
2699
+ const dataInfo = inspectDataManifest(files);
2028
2700
  const log = (msg) => {
2029
2701
  if (!opts.json) {
2030
2702
  console.log(msg);
@@ -2035,6 +2707,7 @@ async function publish(opts) {
2035
2707
  }
2036
2708
  const sessionBody = {
2037
2709
  artifactId,
2710
+ ...expiresInSeconds === void 0 ? {} : { expiresInSeconds },
2038
2711
  // Overrides the entry HTML's own meta description on the share card.
2039
2712
  // Omitted entirely when absent, so the document keeps speaking for itself.
2040
2713
  ...opts.description?.trim() ? { description: opts.description.trim() } : {},
@@ -2069,11 +2742,17 @@ async function publish(opts) {
2069
2742
  client = createApiClient(apiUrl2, credential);
2070
2743
  session = await client.createSession(sessionBody);
2071
2744
  }
2745
+ if (expiresInSeconds !== void 0 && session.expiresInSeconds !== expiresInSeconds) {
2746
+ throw new Error(
2747
+ "This API does not support temporary publishing. Nothing was uploaded or finalized; upgrade the API before using --expires-in."
2748
+ );
2749
+ }
2072
2750
  const byPath = new Map(files.map((f) => [f.path, f]));
2073
2751
  log(
2074
2752
  ` uploading ${session.filesToUpload.length} file(s)` + (session.skippedFiles.length ? `, ${session.skippedFiles.length} unchanged` : "")
2075
2753
  );
2076
2754
  const presence = startPublishingPresence(client, session.artifactId);
2755
+ const brewing = startBrewing("brewing\u2026", !opts.json);
2077
2756
  try {
2078
2757
  await inBatches(
2079
2758
  session.filesToUpload,
@@ -2085,15 +2764,23 @@ async function publish(opts) {
2085
2764
  }
2086
2765
  await uploadFile(
2087
2766
  upload.uploadUrl,
2088
- readFileSync4(file.absPath),
2767
+ readFileSync5(file.absPath),
2089
2768
  file.mimeType
2090
2769
  );
2091
2770
  }
2092
2771
  );
2772
+ } catch (error) {
2773
+ brewing.stop();
2774
+ throw error;
2093
2775
  } finally {
2094
2776
  presence.stop();
2095
2777
  }
2096
- const result = await client.finalize(session.publishSessionId);
2778
+ let result;
2779
+ try {
2780
+ result = await client.finalize(session.publishSessionId);
2781
+ } finally {
2782
+ brewing.stop();
2783
+ }
2097
2784
  if (usingAgentToken && result.workspaceId) {
2098
2785
  placement.workspaceId = result.workspaceId;
2099
2786
  }
@@ -2133,6 +2820,9 @@ async function publish(opts) {
2133
2820
  JSON.stringify(
2134
2821
  {
2135
2822
  artifactId: result.artifactId,
2823
+ autoDeleteAt: result.autoDeleteAt ?? null,
2824
+ expiresAt: result.expiresAt ?? null,
2825
+ ...dataInfo.config ? { data: dataInfo.config } : {},
2136
2826
  claimed: result.claimed,
2137
2827
  destination: placement.destination,
2138
2828
  destinationSource: placement.destinationSource,
@@ -2176,7 +2866,7 @@ ${result.url}
2176
2866
  ${result.claimUrl}
2177
2867
  `
2178
2868
  );
2179
- } else if (result.claimed) {
2869
+ } else if (result.claimed && !result.expiresAt) {
2180
2870
  console.log("Claimed by your account \u2014 no expiry.\n");
2181
2871
  }
2182
2872
  if (opts.password !== void 0) {
@@ -2211,6 +2901,17 @@ ${result.claimUrl}
2211
2901
  `);
2212
2902
  }
2213
2903
  }
2904
+ if (!opts.json) {
2905
+ if (result.expiresAt) {
2906
+ console.log(
2907
+ `${result.autoDeleteAt ? "Temporary artifact" : "Anonymous artifact"} \u2014 expires at ${result.expiresAt}. All versions, comments, files, and artifact data will be automatically deleted.
2908
+ `
2909
+ );
2910
+ }
2911
+ for (const announcement of dataInfo.announcements) {
2912
+ console.log(announcement);
2913
+ }
2914
+ }
2214
2915
  if (opts.open && !opts.json && process.stdout.isTTY) {
2215
2916
  openInBrowser(result.url);
2216
2917
  }
@@ -2280,9 +2981,9 @@ async function resolvePlacement(params) {
2280
2981
  };
2281
2982
  }
2282
2983
  if (shouldPromptForWorkspace(opts)) {
2283
- const data2 = await createApiClient(apiUrl2, apiKey).listWorkspaces();
2284
- if (data2.workspaces.length > 1) {
2285
- const choice = await pickWorkspace(data2.workspaces);
2984
+ const data3 = await createApiClient(apiUrl2, apiKey).listWorkspaces();
2985
+ if (data3.workspaces.length > 1) {
2986
+ const choice = await pickWorkspace(data3.workspaces);
2286
2987
  return {
2287
2988
  destination: choice.workspace.isPersonal ? "personal" : "workspace",
2288
2989
  destinationLabel: choice.workspace.isPersonal ? "Personal" : choice.workspace.name,
@@ -2308,19 +3009,19 @@ async function resolvePlacement(params) {
2308
3009
  throw new Error("--folder requires --workspace or a stored workspace.");
2309
3010
  }
2310
3011
  const client = createApiClient(apiUrl2, apiKey);
2311
- const data = await client.listWorkspaces();
3012
+ const data2 = await client.listWorkspaces();
2312
3013
  const workspaceHandle = opts.workspace ?? config?.workspace ?? projectConfig?.workspace;
2313
3014
  let workspace;
2314
3015
  if (workspaceHandle) {
2315
- workspace = data.workspaces.find(
3016
+ workspace = data2.workspaces.find(
2316
3017
  (candidate) => candidate.handle === workspaceHandle
2317
3018
  );
2318
3019
  } else if (config?.workspaceId) {
2319
- workspace = data.workspaces.find(
3020
+ workspace = data2.workspaces.find(
2320
3021
  (candidate) => candidate.id === config.workspaceId
2321
3022
  );
2322
3023
  } else {
2323
- workspace = data.workspaces.find((candidate) => candidate.isPersonal);
3024
+ workspace = data2.workspaces.find((candidate) => candidate.isPersonal);
2324
3025
  }
2325
3026
  if (!workspace) {
2326
3027
  const target = workspaceHandle ? `workspace with handle "${workspaceHandle}"` : "personal workspace";
@@ -2458,7 +3159,7 @@ function openInBrowser(url) {
2458
3159
 
2459
3160
  // src/dashboard.ts
2460
3161
  function openDashboard(opts) {
2461
- const base = process.env.ARTIFACT_CAFE_URL ?? readConfig(path7.resolve(opts.dir))?.apiUrl ?? DEFAULT_API_URL;
3162
+ const base = process.env.ARTIFACT_CAFE_URL ?? readConfig(path8.resolve(opts.dir))?.apiUrl ?? DEFAULT_API_URL;
2462
3163
  const url = `${base.replace(/\/$/, "")}/dashboard`;
2463
3164
  console.log(url);
2464
3165
  if (process.stdout.isTTY) {
@@ -2500,6 +3201,35 @@ function printCliError(err, json) {
2500
3201
  }
2501
3202
  }
2502
3203
 
3204
+ // src/expiration.ts
3205
+ import path9 from "path";
3206
+ async function expiration(opts) {
3207
+ if (opts.duration !== void 0 === opts.keep) {
3208
+ throw new Error("Provide exactly one of --in <duration> or --keep.");
3209
+ }
3210
+ const seconds = opts.keep ? null : parseArtifactDuration(opts.duration);
3211
+ const target = resolveArtifactTarget(path9.resolve(opts.dir), opts);
3212
+ const result = await createApiClient(
3213
+ target.apiUrl,
3214
+ target.credential
3215
+ ).setExpiration(target.artifactId, seconds);
3216
+ if (opts.json) {
3217
+ console.log(JSON.stringify(result, null, 2));
3218
+ return;
3219
+ }
3220
+ if (result.autoDeleteAt) {
3221
+ console.log(`Temporary artifact \u2014 expires at ${result.expiresAt}.`);
3222
+ } else if (result.expiresAt) {
3223
+ console.log(
3224
+ `Temporary deadline cleared. Anonymous retention still expires at ${result.expiresAt}; claim the artifact to keep it.`
3225
+ );
3226
+ } else {
3227
+ console.log(
3228
+ "Artifact kept \u2014 no automatic expiry. Its link, versions, and comments are preserved."
3229
+ );
3230
+ }
3231
+ }
3232
+
2503
3233
  // src/folders.ts
2504
3234
  async function folders(opts) {
2505
3235
  if (opts.action !== void 0 && opts.action !== "create") {
@@ -2544,21 +3274,21 @@ async function folders(opts) {
2544
3274
  await createFolder(client, workspace, opts);
2545
3275
  return;
2546
3276
  }
2547
- const data = await client.listFolders(workspace.id);
3277
+ const data2 = await client.listFolders(workspace.id);
2548
3278
  if (opts.json) {
2549
- console.log(JSON.stringify(data, null, 2));
3279
+ console.log(JSON.stringify(data2, null, 2));
2550
3280
  return;
2551
3281
  }
2552
- if (data.folders.length === 0) {
3282
+ if (data2.folders.length === 0) {
2553
3283
  console.log(`No folders in workspace ${workspace.handle}.`);
2554
3284
  return;
2555
3285
  }
2556
- const plural = data.folders.length === 1 ? "" : "s";
3286
+ const plural = data2.folders.length === 1 ? "" : "s";
2557
3287
  console.log(
2558
- `${data.folders.length} folder${plural} in ${workspace.handle}
3288
+ `${data2.folders.length} folder${plural} in ${workspace.handle}
2559
3289
  `
2560
3290
  );
2561
- for (const folder of data.folders) {
3291
+ for (const folder of data2.folders) {
2562
3292
  printFolder(folder);
2563
3293
  }
2564
3294
  }
@@ -2596,7 +3326,7 @@ function printFolder(folder) {
2596
3326
  }
2597
3327
 
2598
3328
  // src/link.ts
2599
- import path8 from "path";
3329
+ import path10 from "path";
2600
3330
  import { createInterface as createInterface2 } from "readline";
2601
3331
 
2602
3332
  // src/list.ts
@@ -2653,28 +3383,28 @@ async function list(opts) {
2653
3383
  await listWorkspace(client, opts);
2654
3384
  return;
2655
3385
  }
2656
- const [data, groups] = await Promise.all([
3386
+ const [data2, groups] = await Promise.all([
2657
3387
  client.listArtifacts(),
2658
3388
  teamGroups(client)
2659
3389
  ]);
2660
3390
  if (opts.json) {
2661
3391
  console.log(
2662
- JSON.stringify({ artifacts: data.artifacts, workspaces: groups }, null, 2)
3392
+ JSON.stringify({ artifacts: data2.artifacts, workspaces: groups }, null, 2)
2663
3393
  );
2664
3394
  return;
2665
3395
  }
2666
- const total = data.artifacts.length + groups.reduce((sum, group) => sum + group.artifacts.length, 0);
3396
+ const total = data2.artifacts.length + groups.reduce((sum, group) => sum + group.artifacts.length, 0);
2667
3397
  if (total === 0) {
2668
3398
  console.log(
2669
3399
  "No artifacts you can publish to yet \u2014 publish while logged in, or claim one via its claim link."
2670
3400
  );
2671
3401
  return;
2672
3402
  }
2673
- if (data.artifacts.length > 0) {
2674
- const plural = data.artifacts.length === 1 ? "" : "s";
2675
- console.log(`${data.artifacts.length} artifact${plural}
3403
+ if (data2.artifacts.length > 0) {
3404
+ const plural = data2.artifacts.length === 1 ? "" : "s";
3405
+ console.log(`${data2.artifacts.length} artifact${plural}
2676
3406
  `);
2677
- for (const artifact of data.artifacts) {
3407
+ for (const artifact of data2.artifacts) {
2678
3408
  printArtifact(artifact);
2679
3409
  }
2680
3410
  }
@@ -2825,7 +3555,7 @@ async function link(opts) {
2825
3555
  }
2826
3556
  chosen = await pickArtifact(artifacts);
2827
3557
  }
2828
- const dir = path8.resolve(opts.dir);
3558
+ const dir = path10.resolve(opts.dir);
2829
3559
  writeConfig(dir, {
2830
3560
  apiUrl: apiUrl2,
2831
3561
  artifactId: chosen.id,
@@ -2993,7 +3723,7 @@ function logout(opts) {
2993
3723
  }
2994
3724
 
2995
3725
  // src/message.ts
2996
- import path9 from "path";
3726
+ import path11 from "path";
2997
3727
  async function readStdin2() {
2998
3728
  if (process.stdin.isTTY) {
2999
3729
  return "";
@@ -3017,7 +3747,7 @@ async function editMessage(opts) {
3017
3747
  'Nothing to save. Pass the new text with --body "\u2026", or pipe it on stdin.'
3018
3748
  );
3019
3749
  }
3020
- const dir = path9.resolve(opts.dir);
3750
+ const dir = path11.resolve(opts.dir);
3021
3751
  const target = resolveArtifactTarget(dir, {
3022
3752
  apiKey: opts.apiKey,
3023
3753
  artifact: opts.artifact
@@ -3037,7 +3767,7 @@ async function deleteMessage(opts) {
3037
3767
  "Pass the message id to delete \u2014 get it from `artifact-cafe comments --json`."
3038
3768
  );
3039
3769
  }
3040
- const dir = path9.resolve(opts.dir);
3770
+ const dir = path11.resolve(opts.dir);
3041
3771
  const target = resolveArtifactTarget(dir, {
3042
3772
  apiKey: opts.apiKey,
3043
3773
  artifact: opts.artifact
@@ -3052,12 +3782,12 @@ async function deleteMessage(opts) {
3052
3782
  }
3053
3783
 
3054
3784
  // src/move.ts
3055
- import path10 from "path";
3785
+ import path12 from "path";
3056
3786
  async function move(opts) {
3057
3787
  if (Boolean(opts.folder) === Boolean(opts.unfiled)) {
3058
3788
  throw new Error("Pass exactly one of --folder <name-or-id> or --unfiled.");
3059
3789
  }
3060
- const dir = path10.resolve(opts.dir);
3790
+ const dir = path12.resolve(opts.dir);
3061
3791
  const target = resolveArtifactTarget(dir, {
3062
3792
  apiKey: opts.apiKey,
3063
3793
  artifact: opts.artifact
@@ -3078,8 +3808,8 @@ async function move(opts) {
3078
3808
  let folderId = null;
3079
3809
  let folderName = null;
3080
3810
  if (opts.folder) {
3081
- const data = await client.listFolders(artifact.workspaceId);
3082
- const matches = data.folders.filter(
3811
+ const data2 = await client.listFolders(artifact.workspaceId);
3812
+ const matches = data2.folders.filter(
3083
3813
  (candidate) => candidate.name === opts.folder || candidate.id === opts.folder
3084
3814
  );
3085
3815
  if (matches.length === 0) {
@@ -3147,9 +3877,9 @@ async function move(opts) {
3147
3877
  }
3148
3878
 
3149
3879
  // src/open.ts
3150
- import path11 from "path";
3880
+ import path13 from "path";
3151
3881
  async function openArtifact(opts) {
3152
- const dir = path11.resolve(opts.dir);
3882
+ const dir = path13.resolve(opts.dir);
3153
3883
  const target = resolveArtifactTarget(dir, {
3154
3884
  apiKey: opts.apiKey,
3155
3885
  artifact: opts.artifact
@@ -3167,7 +3897,7 @@ async function openArtifact(opts) {
3167
3897
  }
3168
3898
 
3169
3899
  // src/protect.ts
3170
- import path12 from "path";
3900
+ import path14 from "path";
3171
3901
  async function protect(opts) {
3172
3902
  if (opts.off && opts.password !== void 0) {
3173
3903
  throw new Error("Pass either --password or --off, not both.");
@@ -3184,7 +3914,7 @@ async function protect(opts) {
3184
3914
  throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
3185
3915
  }
3186
3916
  }
3187
- const dir = path12.resolve(opts.dir);
3917
+ const dir = path14.resolve(opts.dir);
3188
3918
  const target = resolveArtifactTarget(dir, {
3189
3919
  apiKey: opts.apiKey,
3190
3920
  artifact: opts.artifact
@@ -3209,7 +3939,7 @@ async function protect(opts) {
3209
3939
  }
3210
3940
 
3211
3941
  // src/reply.ts
3212
- import path13 from "path";
3942
+ import path15 from "path";
3213
3943
  async function readStdin3() {
3214
3944
  if (process.stdin.isTTY) {
3215
3945
  return "";
@@ -3233,7 +3963,7 @@ async function replyToThread(opts) {
3233
3963
  'Nothing to post. Pass the reply with --body "\u2026", or pipe it on stdin.'
3234
3964
  );
3235
3965
  }
3236
- const dir = path13.resolve(opts.dir);
3966
+ const dir = path15.resolve(opts.dir);
3237
3967
  const target = resolveArtifactTarget(dir, {
3238
3968
  apiKey: opts.apiKey,
3239
3969
  artifact: opts.artifact
@@ -3262,7 +3992,7 @@ async function replyToThread(opts) {
3262
3992
  }
3263
3993
 
3264
3994
  // src/resolve.ts
3265
- import path14 from "path";
3995
+ import path16 from "path";
3266
3996
  async function changeThreadStatus(opts) {
3267
3997
  const threadId = opts.thread?.trim();
3268
3998
  if (!threadId) {
@@ -3270,7 +4000,7 @@ async function changeThreadStatus(opts) {
3270
4000
  `Pass the thread id to ${opts.action} \u2014 get it from \`artifact-cafe comments --json\`.`
3271
4001
  );
3272
4002
  }
3273
- const dir = path14.resolve(opts.dir);
4003
+ const dir = path16.resolve(opts.dir);
3274
4004
  const target = resolveArtifactTarget(dir, {
3275
4005
  apiKey: opts.apiKey,
3276
4006
  artifact: opts.artifact
@@ -3307,7 +4037,7 @@ async function changeThreadStatus(opts) {
3307
4037
  }
3308
4038
 
3309
4039
  // src/review.ts
3310
- import path15 from "path";
4040
+ import path17 from "path";
3311
4041
  var OUTCOME = {
3312
4042
  off: "Plain artifact \u2014 the review tools are hidden entirely",
3313
4043
  open: "Open for review \u2014 anyone who can open it can comment",
@@ -3321,7 +4051,7 @@ async function review(opts) {
3321
4051
  );
3322
4052
  }
3323
4053
  const mode = parsed.data;
3324
- const dir = path15.resolve(opts.dir);
4054
+ const dir = path17.resolve(opts.dir);
3325
4055
  const target = resolveArtifactTarget(dir, {
3326
4056
  apiKey: opts.apiKey,
3327
4057
  artifact: opts.artifact
@@ -3343,10 +4073,10 @@ async function review(opts) {
3343
4073
  }
3344
4074
 
3345
4075
  // src/share.ts
3346
- import path16 from "path";
4076
+ import path18 from "path";
3347
4077
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3348
4078
  async function share(opts) {
3349
- const dir = path16.resolve(opts.dir);
4079
+ const dir = path18.resolve(opts.dir);
3350
4080
  const target = resolveArtifactTarget(dir, {
3351
4081
  apiKey: opts.apiKey,
3352
4082
  artifact: opts.artifact
@@ -3401,7 +4131,7 @@ General access: ${visibility}`);
3401
4131
  console.log("");
3402
4132
  }
3403
4133
  async function unshare(opts) {
3404
- const dir = path16.resolve(opts.dir);
4134
+ const dir = path18.resolve(opts.dir);
3405
4135
  const target = resolveArtifactTarget(dir, {
3406
4136
  apiKey: opts.apiKey,
3407
4137
  artifact: opts.artifact
@@ -3424,7 +4154,7 @@ async function unshare(opts) {
3424
4154
  }
3425
4155
 
3426
4156
  // src/status.ts
3427
- import path17 from "path";
4157
+ import path19 from "path";
3428
4158
  async function status(opts) {
3429
4159
  if (!AGENT_STATES.includes(opts.state)) {
3430
4160
  throw new Error(
@@ -3432,7 +4162,7 @@ async function status(opts) {
3432
4162
  );
3433
4163
  }
3434
4164
  const state = opts.state;
3435
- const dir = path17.resolve(opts.dir);
4165
+ const dir = path19.resolve(opts.dir);
3436
4166
  const target = resolveArtifactTarget(dir, {
3437
4167
  apiKey: opts.apiKey,
3438
4168
  artifact: opts.artifact
@@ -3458,40 +4188,40 @@ async function status(opts) {
3458
4188
  }
3459
4189
 
3460
4190
  // src/update.ts
3461
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
4191
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
3462
4192
  import { createRequire } from "module";
3463
4193
  import { homedir as homedir2 } from "os";
3464
- import path18 from "path";
3465
- import { z as z22 } from "zod";
4194
+ import path20 from "path";
4195
+ import { z as z25 } from "zod";
3466
4196
  var REGISTRY_LATEST_URL = "https://registry.npmjs.org/artifact-cafe/latest";
3467
4197
  var SKILL_SOURCE_URL = "https://raw.githubusercontent.com/Artifact-Cafe/skill/main/artifact-cafe/SKILL.md";
3468
4198
  var SKILL_UPDATE_COMMAND = "npx skills add artifact-cafe/skill --skill artifact-cafe -g";
3469
4199
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
3470
4200
  var FETCH_TIMEOUT_MS = 2e3;
3471
4201
  var SKILL_VERSION_PATTERN = /\*\*Skill version:\s*(\d+\.\d+\.\d+)\*\*/;
3472
- var cacheSchema = z22.object({
3473
- checkedAt: z22.number(),
3474
- cliLatest: z22.string().nullable(),
3475
- skillLatest: z22.string().nullable(),
3476
- version: z22.literal(1)
4202
+ var cacheSchema = z25.object({
4203
+ checkedAt: z25.number(),
4204
+ cliLatest: z25.string().nullable(),
4205
+ skillLatest: z25.string().nullable(),
4206
+ version: z25.literal(1)
3477
4207
  });
3478
4208
  function cliVersion() {
3479
4209
  const pkg = createRequire(import.meta.url)("../package.json");
3480
4210
  return pkg.version;
3481
4211
  }
3482
4212
  function cachePath() {
3483
- return path18.join(homedir2(), ".artifactcafe", "update-check.json");
4213
+ return path20.join(homedir2(), ".artifactcafe", "update-check.json");
3484
4214
  }
3485
4215
  function readCache() {
3486
4216
  try {
3487
- return cacheSchema.parse(JSON.parse(readFileSync5(cachePath(), "utf8")));
4217
+ return cacheSchema.parse(JSON.parse(readFileSync6(cachePath(), "utf8")));
3488
4218
  } catch {
3489
4219
  return null;
3490
4220
  }
3491
4221
  }
3492
4222
  function writeCache(cache) {
3493
4223
  try {
3494
- mkdirSync3(path18.dirname(cachePath()), { mode: 448, recursive: true });
4224
+ mkdirSync3(path20.dirname(cachePath()), { mode: 448, recursive: true });
3495
4225
  writeFileSync3(cachePath(), `${JSON.stringify(cache, null, 2)}
3496
4226
  `);
3497
4227
  } catch {
@@ -3528,7 +4258,7 @@ async function fetchText(url) {
3528
4258
  }
3529
4259
  async function fetchLatestVersions(previous) {
3530
4260
  const [cli, skill] = await Promise.all([
3531
- fetchJson(REGISTRY_LATEST_URL).then((body) => z22.object({ version: z22.string() }).parse(body).version).catch(() => null),
4261
+ fetchJson(REGISTRY_LATEST_URL).then((body) => z25.object({ version: z25.string() }).parse(body).version).catch(() => null),
3532
4262
  fetchText(SKILL_SOURCE_URL).then((body) => SKILL_VERSION_PATTERN.exec(body)?.[1] ?? null).catch(() => null)
3533
4263
  ]);
3534
4264
  return {
@@ -3544,12 +4274,12 @@ function installedSkillVersions() {
3544
4274
  const versions = [];
3545
4275
  for (const root of roots) {
3546
4276
  for (const dir of dirs) {
3547
- const file = path18.join(root, dir, "skills", "artifact-cafe", "SKILL.md");
4277
+ const file = path20.join(root, dir, "skills", "artifact-cafe", "SKILL.md");
3548
4278
  if (!existsSync3(file)) {
3549
4279
  continue;
3550
4280
  }
3551
4281
  try {
3552
- const found = SKILL_VERSION_PATTERN.exec(readFileSync5(file, "utf8"));
4282
+ const found = SKILL_VERSION_PATTERN.exec(readFileSync6(file, "utf8"));
3553
4283
  if (found?.[1]) {
3554
4284
  versions.push(found[1]);
3555
4285
  }
@@ -3618,19 +4348,19 @@ async function workspaces(opts) {
3618
4348
  `Listing workspaces needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
3619
4349
  );
3620
4350
  }
3621
- const data = await createApiClient(apiUrl2, key).listWorkspaces();
4351
+ const data2 = await createApiClient(apiUrl2, key).listWorkspaces();
3622
4352
  if (opts.json) {
3623
- console.log(JSON.stringify(data, null, 2));
4353
+ console.log(JSON.stringify(data2, null, 2));
3624
4354
  return;
3625
4355
  }
3626
- if (data.workspaces.length === 0) {
4356
+ if (data2.workspaces.length === 0) {
3627
4357
  console.log("No workspaces yet \u2014 create one from the dashboard.");
3628
4358
  return;
3629
4359
  }
3630
- const plural = data.workspaces.length === 1 ? "" : "s";
3631
- console.log(`${data.workspaces.length} workspace${plural}
4360
+ const plural = data2.workspaces.length === 1 ? "" : "s";
4361
+ console.log(`${data2.workspaces.length} workspace${plural}
3632
4362
  `);
3633
- for (const workspace of data.workspaces) {
4363
+ for (const workspace of data2.workspaces) {
3634
4364
  printWorkspace(workspace);
3635
4365
  }
3636
4366
  }
@@ -3672,10 +4402,14 @@ var publish2 = defineCommand({
3672
4402
  },
3673
4403
  dir: {
3674
4404
  default: ".",
3675
- description: "Directory to publish",
4405
+ description: "File or directory to publish",
3676
4406
  type: "positional"
3677
4407
  },
3678
4408
  entry: { description: "Entry file (default: auto-detect)", type: "string" },
4409
+ "expires-in": {
4410
+ description: "Temporary lifetime: 30m, 1h, 7d (ordinary republishes preserve it)",
4411
+ type: "string"
4412
+ },
3679
4413
  folder: {
3680
4414
  description: "Publish into this workspace folder name (or id)",
3681
4415
  type: "string"
@@ -3700,7 +4434,10 @@ var publish2 = defineCommand({
3700
4434
  type: "string"
3701
4435
  }
3702
4436
  },
3703
- meta: { description: "Publish a folder as an artifact", name: "publish" },
4437
+ meta: {
4438
+ description: "Publish a file or folder as an artifact",
4439
+ name: "publish"
4440
+ },
3704
4441
  async run({ args }) {
3705
4442
  try {
3706
4443
  await publish({
@@ -3709,6 +4446,7 @@ var publish2 = defineCommand({
3709
4446
  description: readStringArg(process.argv.slice(2), "--description"),
3710
4447
  dir: args.dir,
3711
4448
  entry: args.entry,
4449
+ expiresIn: args["expires-in"],
3712
4450
  folder: args.folder,
3713
4451
  json: Boolean(args.json),
3714
4452
  notes: readStringArg(process.argv.slice(2), "--notes"),
@@ -3964,6 +4702,48 @@ var link2 = defineCommand({
3964
4702
  }
3965
4703
  }
3966
4704
  });
4705
+ var expiration2 = defineCommand({
4706
+ args: {
4707
+ "api-key": {
4708
+ description: "Account or agent token (default: env or login)",
4709
+ type: "string"
4710
+ },
4711
+ artifact: {
4712
+ description: "Artifact ID instead of the linked folder",
4713
+ type: "string"
4714
+ },
4715
+ dir: {
4716
+ default: ".",
4717
+ description: "Published directory",
4718
+ type: "positional"
4719
+ },
4720
+ in: { description: "Reset lifetime from now: 30m, 1h, 7d", type: "string" },
4721
+ json: { description: "Machine-readable output", type: "boolean" },
4722
+ keep: {
4723
+ description: "Clear the explicit temporary deadline",
4724
+ type: "boolean"
4725
+ }
4726
+ },
4727
+ meta: {
4728
+ description: "Extend a temporary artifact or keep it",
4729
+ name: "expiration"
4730
+ },
4731
+ async run({ args }) {
4732
+ try {
4733
+ await expiration({
4734
+ apiKey: args["api-key"],
4735
+ artifact: args.artifact,
4736
+ dir: args.dir,
4737
+ duration: args.in,
4738
+ json: Boolean(args.json),
4739
+ keep: Boolean(args.keep)
4740
+ });
4741
+ } catch (err) {
4742
+ printCliError(err, Boolean(args.json));
4743
+ process.exitCode = 1;
4744
+ }
4745
+ }
4746
+ });
3967
4747
  var comments2 = defineCommand({
3968
4748
  args: {
3969
4749
  "api-key": {
@@ -4599,6 +5379,152 @@ var dashboard = defineCommand({
4599
5379
  }
4600
5380
  }
4601
5381
  });
5382
+ var dataLsCommand = defineCommand({
5383
+ args: {
5384
+ collection: {
5385
+ description: "Collection name",
5386
+ required: true,
5387
+ type: "positional"
5388
+ },
5389
+ dir: {
5390
+ default: ".",
5391
+ description: "Published directory",
5392
+ type: "positional"
5393
+ },
5394
+ json: { description: "Machine-readable output", type: "boolean" },
5395
+ limit: { description: "Max records to show", type: "string" }
5396
+ },
5397
+ meta: {
5398
+ description: "List records in a collection",
5399
+ name: "ls"
5400
+ },
5401
+ async run({ args }) {
5402
+ try {
5403
+ await dataLs({
5404
+ collection: args.collection,
5405
+ dir: args.dir,
5406
+ json: Boolean(args.json),
5407
+ limit: args.limit === void 0 ? void 0 : Number(args.limit)
5408
+ });
5409
+ } catch (err) {
5410
+ printCliError(err, Boolean(args.json));
5411
+ process.exit(1);
5412
+ }
5413
+ }
5414
+ });
5415
+ var dataInsertCommand = defineCommand({
5416
+ args: {
5417
+ collection: {
5418
+ description: "Collection name",
5419
+ required: true,
5420
+ type: "positional"
5421
+ },
5422
+ dir: {
5423
+ default: ".",
5424
+ description: "Published directory",
5425
+ type: "positional"
5426
+ },
5427
+ file: {
5428
+ description: "Read the record(s) from a JSON file instead of --record",
5429
+ type: "string"
5430
+ },
5431
+ json: { description: "Machine-readable output", type: "boolean" },
5432
+ record: {
5433
+ description: "A JSON object, or an array of objects for a batch",
5434
+ type: "string"
5435
+ }
5436
+ },
5437
+ meta: {
5438
+ description: "Insert one or many records into a collection",
5439
+ name: "insert"
5440
+ },
5441
+ async run({ args }) {
5442
+ try {
5443
+ await dataInsert({
5444
+ collection: args.collection,
5445
+ dir: args.dir,
5446
+ file: args.file,
5447
+ json: Boolean(args.json),
5448
+ record: args.record
5449
+ });
5450
+ } catch (err) {
5451
+ printCliError(err, Boolean(args.json));
5452
+ process.exit(1);
5453
+ }
5454
+ }
5455
+ });
5456
+ var dataExportCommand = defineCommand({
5457
+ args: {
5458
+ dir: {
5459
+ default: ".",
5460
+ description: "Published directory",
5461
+ type: "positional"
5462
+ },
5463
+ // Output is always JSON; accepted so --json works uniformly across
5464
+ // every command (SPEC-2 milestone 20 → CLI integration).
5465
+ json: {
5466
+ description: "Machine-readable output (always on)",
5467
+ type: "boolean"
5468
+ }
5469
+ },
5470
+ meta: {
5471
+ description: "Export every collection's records as JSON",
5472
+ name: "export"
5473
+ },
5474
+ async run({ args }) {
5475
+ try {
5476
+ await dataExport({ dir: args.dir });
5477
+ } catch (err) {
5478
+ printCliError(err, true);
5479
+ process.exit(1);
5480
+ }
5481
+ }
5482
+ });
5483
+ var dataClearCommand = defineCommand({
5484
+ args: {
5485
+ collection: {
5486
+ description: "Collection name",
5487
+ required: true,
5488
+ type: "positional"
5489
+ },
5490
+ dir: {
5491
+ default: ".",
5492
+ description: "Published directory",
5493
+ type: "positional"
5494
+ },
5495
+ json: { description: "Machine-readable output", type: "boolean" },
5496
+ yes: { description: "Confirm deleting every record", type: "boolean" }
5497
+ },
5498
+ meta: {
5499
+ description: "Delete every record in a collection",
5500
+ name: "clear"
5501
+ },
5502
+ async run({ args }) {
5503
+ try {
5504
+ await dataClear({
5505
+ collection: args.collection,
5506
+ dir: args.dir,
5507
+ json: Boolean(args.json),
5508
+ yes: Boolean(args.yes)
5509
+ });
5510
+ } catch (err) {
5511
+ printCliError(err, Boolean(args.json));
5512
+ process.exit(1);
5513
+ }
5514
+ }
5515
+ });
5516
+ var data = defineCommand({
5517
+ meta: {
5518
+ description: "Read, insert, or clear the artifact's shared records",
5519
+ name: "data"
5520
+ },
5521
+ subCommands: {
5522
+ clear: dataClearCommand,
5523
+ export: dataExportCommand,
5524
+ insert: dataInsertCommand,
5525
+ ls: dataLsCommand
5526
+ }
5527
+ });
4602
5528
  var main = defineCommand({
4603
5529
  meta: {
4604
5530
  description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
@@ -4621,8 +5547,10 @@ var main = defineCommand({
4621
5547
  comment: comment2,
4622
5548
  comments: comments2,
4623
5549
  dashboard,
5550
+ data,
4624
5551
  delete: del,
4625
5552
  edit,
5553
+ expiration: expiration2,
4626
5554
  folders: folders2,
4627
5555
  link: link2,
4628
5556
  list: list2,