artifact-cafe 0.0.7 → 0.0.9

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 +1408 -427
  2. package/package.json +9 -9
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ function printBanner() {
30
30
  console.log();
31
31
  }
32
32
 
33
- // src/comments.ts
33
+ // src/comment.ts
34
34
  import path3 from "path";
35
35
 
36
36
  // ../shared/src/account.ts
@@ -58,6 +58,9 @@ var apiKeyListResponseSchema = z.object({
58
58
  var revokeApiKeyResponseSchema = z.object({
59
59
  revoked: z.literal(true)
60
60
  });
61
+ var notificationPrefsSchema = z.object({
62
+ commentEmailsEnabled: z.boolean()
63
+ });
61
64
  var meResponseSchema = z.object({
62
65
  // Best-effort profile field from our own user table; null when unknown.
63
66
  email: z.string().nullable(),
@@ -81,106 +84,260 @@ var artifactListResponseSchema = z.object({
81
84
  });
82
85
 
83
86
  // ../shared/src/comments.ts
87
+ import { z as z3 } from "zod";
88
+
89
+ // ../shared/src/publish.ts
84
90
  import { z as z2 } from "zod";
85
- var elementSnapshotSchema = z2.object({
86
- classes: z2.string().max(256).optional(),
87
- id: z2.string().max(256).optional(),
88
- tag: z2.string().min(1).max(128),
89
- text: z2.string().max(200).optional()
91
+ function isSafeArtifactPath(p) {
92
+ if (p.length === 0 || p.length > 1024) {
93
+ return false;
94
+ }
95
+ if (p.includes("\\") || p.includes("\0")) {
96
+ return false;
97
+ }
98
+ if (p.startsWith("/") || p.startsWith("~")) {
99
+ return false;
100
+ }
101
+ return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
102
+ }
103
+ var manifestFileSchema = z2.object({
104
+ hash: z2.string().regex(/^sha256_[a-f0-9]{64}$/),
105
+ mimeType: z2.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
106
+ path: z2.string().min(1).max(1024).refine(isSafeArtifactPath, {
107
+ message: "path must be a safe relative path"
108
+ }),
109
+ size: z2.number().int().positive()
110
+ });
111
+ var publishSourceSchema = z2.object({
112
+ agent: z2.string().optional(),
113
+ runId: z2.string().optional(),
114
+ type: z2.literal("cli")
115
+ });
116
+ var createPublishSessionSchema = z2.object({
117
+ artifactId: z2.string().nullable(),
118
+ entryPath: z2.string().optional(),
119
+ files: z2.array(manifestFileSchema).min(1).max(1e3),
120
+ // SPEC-2 §6.4 — workspace placement is available only to authenticated
121
+ // members; the API enforces membership and folder ownership. folderId
122
+ // without workspaceId resolves against the account's personal workspace.
123
+ folderId: z2.string().min(1).optional(),
124
+ source: publishSourceSchema,
125
+ title: z2.string().max(200).optional(),
126
+ workspaceId: z2.string().min(1).optional()
127
+ });
128
+ var publishSessionResponseSchema = z2.object({
129
+ artifactId: z2.string(),
130
+ expiresAt: z2.string(),
131
+ filesToUpload: z2.array(
132
+ z2.object({
133
+ method: z2.literal("PUT"),
134
+ path: z2.string(),
135
+ uploadUrl: z2.string().url()
136
+ })
137
+ ),
138
+ publishSessionId: z2.string(),
139
+ skippedFiles: z2.array(z2.string()),
140
+ versionId: z2.string()
141
+ });
142
+ var finalizeResponseSchema = z2.object({
143
+ artifactId: z2.string(),
144
+ // API-key publishes create the artifact already claimed (SPEC §5.4) —
145
+ // the CLI uses this to tell "claim URL pending" from "already yours".
146
+ claimed: z2.boolean(),
147
+ // Returned only on first publish of a new artifact (one-time).
148
+ claimUrl: z2.string().url().optional(),
149
+ publishToken: z2.string().optional(),
150
+ url: z2.string().url(),
151
+ versionId: z2.string(),
152
+ versionNumber: z2.number().int().positive()
153
+ });
154
+ var artifactResponseSchema = z2.object({
155
+ claimed: z2.boolean(),
156
+ currentVersion: z2.object({
157
+ entryUrl: z2.string().url(),
158
+ id: z2.string(),
159
+ versionNumber: z2.number().int().positive()
160
+ }).nullable(),
161
+ // SPEC-2 §6.4 — placement + visibility surface on the artifact.
162
+ folderId: z2.string().nullable(),
163
+ id: z2.string(),
164
+ slug: z2.string(),
165
+ title: z2.string(),
166
+ visibility: z2.enum(["public", "unlisted", "workspace"]),
167
+ workspaceId: z2.string().nullable()
90
168
  });
91
- var pointAnchorSchema = z2.object({
169
+
170
+ // ../shared/src/comments.ts
171
+ var anchorPageSchema = z3.string().max(1024).refine(isSafeArtifactPath, {
172
+ message: "page must be a safe relative path"
173
+ }).optional();
174
+ var elementSnapshotSchema = z3.object({
175
+ classes: z3.string().max(256).optional(),
176
+ id: z3.string().max(256).optional(),
177
+ tag: z3.string().min(1).max(128),
178
+ text: z3.string().max(200).optional()
179
+ });
180
+ var pointAnchorSchema = z3.object({
92
181
  element: elementSnapshotSchema.optional(),
93
182
  // elementPath is the primary anchor; x/y are offsets within the
94
183
  // element's box, normalized 0..1 (SPEC.md §8.1).
95
- elementPath: z2.string().max(2048),
96
- type: z2.literal("point"),
97
- viewportHeight: z2.number().int().positive().max(1e5),
98
- viewportWidth: z2.number().int().positive().max(1e5),
99
- x: z2.number().min(0).max(1),
100
- y: z2.number().min(0).max(1)
101
- });
102
- var textAnchorSchema = z2.object({
103
- elementPath: z2.string().max(2048),
104
- endOffset: z2.number().int().nonnegative().max(1e8),
105
- prefix: z2.string().max(200).optional(),
106
- quote: z2.string().min(1).max(1e3),
107
- startOffset: z2.number().int().nonnegative().max(1e8),
108
- suffix: z2.string().max(200).optional(),
109
- type: z2.literal("text")
110
- });
111
- var anchorSchema = z2.discriminatedUnion("type", [
184
+ elementPath: z3.string().max(2048),
185
+ page: anchorPageSchema,
186
+ type: z3.literal("point"),
187
+ viewportHeight: z3.number().int().positive().max(1e5),
188
+ viewportWidth: z3.number().int().positive().max(1e5),
189
+ x: z3.number().min(0).max(1),
190
+ y: z3.number().min(0).max(1)
191
+ });
192
+ var textAnchorSchema = z3.object({
193
+ elementPath: z3.string().max(2048),
194
+ endOffset: z3.number().int().nonnegative().max(1e8),
195
+ page: anchorPageSchema,
196
+ prefix: z3.string().max(200).optional(),
197
+ quote: z3.string().min(1).max(1e3),
198
+ startOffset: z3.number().int().nonnegative().max(1e8),
199
+ suffix: z3.string().max(200).optional(),
200
+ type: z3.literal("text")
201
+ });
202
+ var pageAnchorSchema = z3.object({
203
+ page: anchorPageSchema,
204
+ type: z3.literal("page")
205
+ });
206
+ var anchorSchema = z3.discriminatedUnion("type", [
112
207
  pointAnchorSchema,
113
- textAnchorSchema
208
+ textAnchorSchema,
209
+ pageAnchorSchema
114
210
  ]);
115
- var guestSchema = z2.object({
116
- displayName: z2.string().min(1).max(64),
117
- guestId: z2.string().min(16)
211
+ var displayNameSchema = z3.string().trim().min(1).max(64);
212
+ var guestSchema = z3.object({
213
+ displayName: displayNameSchema,
214
+ guestId: z3.string().min(16)
118
215
  });
119
- var createCommentThreadSchema = z2.object({
216
+ var viaLabelSchema = z3.string().trim().min(1).max(64);
217
+ var createCommentThreadSchema = z3.object({
120
218
  anchor: anchorSchema,
121
- body: z2.string().min(1).max(1e4),
219
+ body: z3.string().min(1).max(1e4),
122
220
  guest: guestSchema.optional(),
123
- versionId: z2.string()
221
+ versionId: z3.string(),
222
+ via: viaLabelSchema.optional()
223
+ });
224
+ var createCommentMessageSchema = z3.object({
225
+ body: z3.string().min(1).max(1e4),
226
+ guest: guestSchema.optional(),
227
+ via: viaLabelSchema.optional()
124
228
  });
125
- var createCommentMessageSchema = z2.object({
126
- body: z2.string().min(1).max(1e4),
229
+ var editCommentMessageSchema = z3.object({
230
+ body: z3.string().min(1).max(1e4),
127
231
  guest: guestSchema.optional()
128
232
  });
129
- var threadActionSchema = z2.object({
233
+ var threadActionSchema = z3.object({
130
234
  guest: guestSchema.optional()
131
235
  });
132
- var commentAuthorSchema = z2.object({
133
- avatarUrl: z2.string().nullable(),
134
- displayName: z2.string(),
135
- guestId: z2.string().nullable(),
136
- isViewer: z2.boolean(),
137
- kind: z2.enum(["member", "guest"])
236
+ var REACTION_EMOJI = [
237
+ "\u{1F44D}",
238
+ "\u{1F44E}",
239
+ "\u2764\uFE0F",
240
+ "\u{1F389}",
241
+ "\u{1F4A1}",
242
+ "\u{1F440}",
243
+ "\u{1F680}",
244
+ "\u2615"
245
+ ];
246
+ var reactionEmojiSchema = z3.enum(REACTION_EMOJI);
247
+ var toggleReactionSchema = z3.object({
248
+ emoji: reactionEmojiSchema,
249
+ guest: guestSchema.optional()
138
250
  });
139
- var commentMessageSchema = z2.object({
251
+ var commentAuthorSchema = z3.object({
252
+ avatarUrl: z3.string().nullable(),
253
+ displayName: z3.string(),
254
+ guestId: z3.string().nullable(),
255
+ isViewer: z3.boolean(),
256
+ kind: z3.enum(["member", "guest"]),
257
+ // Optional "via" attribution (§6.4). Null for plain comments; a bounded
258
+ // label the shell appends after the display name.
259
+ via: z3.string().nullable()
260
+ });
261
+ var messageReactionSchema = z3.object({
262
+ emoji: z3.string(),
263
+ reactors: z3.array(commentAuthorSchema)
264
+ });
265
+ var commentMessageSchema = z3.object({
140
266
  author: commentAuthorSchema,
141
- body: z2.string(),
142
- createdAt: z2.string(),
143
- id: z2.string()
144
- });
145
- var commentThreadSchema = z2.object({
267
+ // Empty string for a deleted message — the body is stripped server-side
268
+ // (§6.7.1 tombstone), never sent to the client and hidden there.
269
+ body: z3.string(),
270
+ createdAt: z3.string(),
271
+ // Soft-deleted (§6.7.1). A tombstone keeps thread structure and replies;
272
+ // the shell renders "Comment deleted" instead of the (absent) body.
273
+ deleted: z3.boolean(),
274
+ // Last edit time (§6.7.1), or null if never edited — drives the "edited"
275
+ // badge. No edit history is kept.
276
+ editedAt: z3.string().nullable(),
277
+ id: z3.string(),
278
+ // Defaulted, not required: a new CLI parsing an older server's
279
+ // response must not fail on the missing key.
280
+ reactions: z3.array(messageReactionSchema).default([])
281
+ });
282
+ var commentThreadSchema = z3.object({
146
283
  anchor: anchorSchema,
147
- artifactId: z2.string(),
148
- createdAt: z2.string(),
284
+ artifactId: z3.string(),
285
+ createdAt: z3.string(),
149
286
  createdBy: commentAuthorSchema,
150
- id: z2.string(),
151
- messages: z2.array(commentMessageSchema),
152
- status: z2.enum(["open", "resolved"]),
153
- versionId: z2.string()
154
- });
155
- var commentThreadListSchema = z2.object({
156
- threads: z2.array(commentThreadSchema)
157
- });
158
- var commentExportQuerySchema = z2.object({
159
- status: z2.enum(["open", "resolved", "all"]).default("open"),
160
- version: z2.enum(["current", "all"]).default("current")
161
- });
162
- var commentExportReplySchema = z2.object({
163
- author: z2.string(),
164
- authorKind: z2.enum(["member", "guest"]),
165
- body: z2.string(),
166
- createdAt: z2.string()
167
- });
168
- var commentExportThreadSchema = z2.object({
287
+ id: z3.string(),
288
+ messages: z3.array(commentMessageSchema),
289
+ status: z3.enum(["open", "resolved"]),
290
+ versionId: z3.string()
291
+ });
292
+ var commentThreadListSchema = z3.object({
293
+ threads: z3.array(commentThreadSchema)
294
+ });
295
+ var commentExportQuerySchema = z3.object({
296
+ status: z3.enum(["open", "resolved", "all"]).default("open"),
297
+ version: z3.enum(["current", "all"]).default("current")
298
+ });
299
+ var exportReactionSchema = z3.object({
300
+ count: z3.number().int().positive(),
301
+ emoji: z3.string()
302
+ });
303
+ var commentExportReplySchema = z3.object({
304
+ author: z3.string(),
305
+ authorKind: z3.enum(["member", "guest"]),
306
+ body: z3.string(),
307
+ createdAt: z3.string(),
308
+ // Soft-delete tombstone (§6.7.1): body is "" when true.
309
+ deleted: z3.boolean(),
310
+ editedAt: z3.string().nullable(),
311
+ // The message id — what `edit`/`delete` target (§5.1).
312
+ id: z3.string(),
313
+ reactions: z3.array(exportReactionSchema).optional()
314
+ });
315
+ var commentExportThreadSchema = z3.object({
169
316
  anchor: anchorSchema,
170
- author: z2.string(),
171
- authorKind: z2.enum(["member", "guest"]),
172
- body: z2.string(),
173
- createdAt: z2.string(),
174
- id: z2.string(),
175
- replies: z2.array(commentExportReplySchema),
176
- status: z2.enum(["open", "resolved"]),
177
- versionNumber: z2.number().int().positive()
178
- });
179
- var commentExportSchema = z2.object({
180
- artifactId: z2.string(),
181
- comments: z2.array(commentExportThreadSchema),
317
+ author: z3.string(),
318
+ authorKind: z3.enum(["member", "guest"]),
319
+ body: z3.string(),
320
+ createdAt: z3.string(),
321
+ deleted: z3.boolean(),
322
+ editedAt: z3.string().nullable(),
323
+ id: z3.string(),
324
+ // The opening message's id (distinct from the thread `id`) — `edit`/
325
+ // `delete` operate on messages, so the CLI needs it to target the first
326
+ // comment, not just the replies.
327
+ messageId: z3.string(),
328
+ reactions: z3.array(exportReactionSchema).optional(),
329
+ replies: z3.array(commentExportReplySchema),
330
+ status: z3.enum(["open", "resolved"]),
331
+ versionNumber: z3.number().int().positive()
332
+ });
333
+ var commentExportSchema = z3.object({
334
+ artifactId: z3.string(),
335
+ comments: z3.array(commentExportThreadSchema),
182
336
  // The artifact's current version (null before the first finalize).
183
- versionNumber: z2.number().int().positive().nullable()
337
+ versionNumber: z3.number().int().positive().nullable()
338
+ });
339
+ var commentReadResponseSchema = z3.object({
340
+ lastSeenAt: z3.string()
184
341
  });
185
342
  function elementLabel(el) {
186
343
  if (el.id) {
@@ -191,145 +348,163 @@ function elementLabel(el) {
191
348
  }
192
349
 
193
350
  // ../shared/src/errors.ts
194
- import { z as z3 } from "zod";
195
- var apiErrorSchema = z3.object({
196
- code: z3.string(),
197
- docsUrl: z3.string().url(),
198
- message: z3.string(),
199
- retryAfter: z3.number().int().positive().optional()
351
+ import { z as z4 } from "zod";
352
+ var apiErrorSchema = z4.object({
353
+ code: z4.string(),
354
+ docsUrl: z4.string().url(),
355
+ message: z4.string(),
356
+ retryAfter: z4.number().int().positive().optional()
357
+ });
358
+
359
+ // ../shared/src/feedback.ts
360
+ import { z as z5 } from "zod";
361
+ var feedbackRequestSchema = z5.object({
362
+ email: z5.string().trim().toLowerCase().email().optional().or(z5.literal("")),
363
+ message: z5.string().trim().min(1).max(4e3)
364
+ });
365
+ var feedbackResponseSchema = z5.object({
366
+ sent: z5.literal(true)
200
367
  });
201
368
 
202
369
  // ../shared/src/me.ts
203
- import { z as z4 } from "zod";
204
- var ownArtifactSchema = z4.object({
205
- createdAt: z4.string(),
206
- currentVersionNumber: z4.number().nullable(),
370
+ import { z as z6 } from "zod";
371
+ var ownArtifactSchema = z6.object({
372
+ createdAt: z6.string(),
373
+ currentVersionNumber: z6.number().nullable(),
207
374
  // Owned artifacts normally never expire; non-null only while an
208
375
  // expiry is still set (e.g. an unclaimed publish surfaced here later).
209
- expiresAt: z4.string().nullable(),
210
- // SPEC-2 §8.4 — the containing folder's name for workspace-owned
211
- // artifacts; always null for personal artifacts.
212
- folderName: z4.string().nullable(),
376
+ expiresAt: z6.string().nullable(),
377
+ // SPEC-2 §8.4 — the containing folder for workspace-owned artifacts;
378
+ // both null for personal artifacts. `folderId` drives the dashboard's
379
+ // move control and folder filter; `folderName` is the display label.
380
+ folderId: z6.string().nullable(),
381
+ folderName: z6.string().nullable(),
213
382
  // Whether a publish token is still active — revocable from the
214
383
  // dashboard (§6.18); the token itself is never returned.
215
- hasPublishToken: z4.boolean(),
216
- id: z4.string(),
217
- openThreads: z4.number(),
218
- protected: z4.boolean(),
219
- slug: z4.string(),
220
- title: z4.string(),
221
- totalViews: z4.number(),
222
- updatedAt: z4.string(),
223
- url: z4.string(),
224
- versionCount: z4.number()
225
- });
226
- var ownArtifactsResponseSchema = z4.object({
227
- artifacts: z4.array(ownArtifactSchema)
228
- });
229
- var revokePublishTokenResponseSchema = z4.object({
230
- revoked: z4.boolean()
384
+ hasPublishToken: z6.boolean(),
385
+ id: z6.string(),
386
+ openThreads: z6.number(),
387
+ protected: z6.boolean(),
388
+ slug: z6.string(),
389
+ title: z6.string(),
390
+ totalViews: z6.number(),
391
+ // SPEC §6.20 — messages by others newer than the viewer's last-seen
392
+ // watermark. 0 for signed-out/API-key-less contexts.
393
+ unreadComments: z6.number(),
394
+ updatedAt: z6.string(),
395
+ url: z6.string(),
396
+ versionCount: z6.number()
397
+ });
398
+ var ownArtifactsResponseSchema = z6.object({
399
+ artifacts: z6.array(ownArtifactSchema)
400
+ });
401
+ var revokePublishTokenResponseSchema = z6.object({
402
+ revoked: z6.boolean()
231
403
  });
232
404
 
233
405
  // ../shared/src/owner.ts
234
- import { z as z6 } from "zod";
406
+ import { z as z8 } from "zod";
235
407
 
236
408
  // ../shared/src/workspaces.ts
237
- import { z as z5 } from "zod";
409
+ import { z as z7 } from "zod";
238
410
  var WORKSPACE_HANDLE_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])$/;
239
- var workspaceRoleSchema = z5.enum(["owner", "admin", "member"]);
240
- var workspaceInviteRoleSchema = z5.enum(["admin", "member"]);
241
- var workspaceHandleSchema = z5.string().min(3).max(32).regex(WORKSPACE_HANDLE_PATTERN, {
411
+ var workspaceRoleSchema = z7.enum(["owner", "admin", "member"]);
412
+ var workspaceInviteRoleSchema = z7.enum(["admin", "member"]);
413
+ var workspaceHandleSchema = z7.string().min(3).max(32).regex(WORKSPACE_HANDLE_PATTERN, {
242
414
  message: "handle must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
243
415
  });
244
- var createWorkspaceSchema = z5.object({
416
+ var createWorkspaceSchema = z7.object({
245
417
  handle: workspaceHandleSchema.optional(),
246
- name: z5.string().trim().min(1).max(100)
418
+ name: z7.string().trim().min(1).max(100)
247
419
  });
248
- var updateWorkspaceSchema = z5.object({
420
+ var updateWorkspaceSchema = z7.object({
249
421
  handle: workspaceHandleSchema.optional(),
250
- name: z5.string().trim().min(1).max(100).optional()
422
+ name: z7.string().trim().min(1).max(100).optional()
251
423
  }).refine((body) => body.handle !== void 0 || body.name !== void 0, {
252
424
  message: "provide at least one of name or handle"
253
425
  });
254
- var workspaceSchema = z5.object({
255
- createdAt: z5.string(),
256
- handle: z5.string(),
257
- id: z5.string(),
258
- name: z5.string(),
426
+ var workspaceSchema = z7.object({
427
+ createdAt: z7.string(),
428
+ handle: z7.string(),
429
+ id: z7.string(),
430
+ // Auto-provisioned solo workspace, one per user: non-deletable, takes
431
+ // no members, renameable like any workspace.
432
+ isPersonal: z7.boolean(),
433
+ name: z7.string(),
259
434
  role: workspaceRoleSchema.nullable(),
260
- updatedAt: z5.string()
435
+ updatedAt: z7.string()
261
436
  });
262
- var workspaceListResponseSchema = z5.object({
263
- workspaces: z5.array(workspaceSchema)
437
+ var workspaceListResponseSchema = z7.object({
438
+ workspaces: z7.array(workspaceSchema)
264
439
  });
265
- var deleteWorkspaceResponseSchema = z5.object({
266
- deleted: z5.literal(true)
440
+ var deleteWorkspaceResponseSchema = z7.object({
441
+ deleted: z7.literal(true)
267
442
  });
268
- var workspaceMemberSchema = z5.object({
269
- createdAt: z5.string(),
270
- email: z5.string().email(),
271
- image: z5.string().nullable(),
272
- name: z5.string(),
443
+ var workspaceMemberSchema = z7.object({
444
+ createdAt: z7.string(),
445
+ email: z7.string().email(),
446
+ image: z7.string().nullable(),
447
+ name: z7.string(),
273
448
  role: workspaceRoleSchema,
274
- userId: z5.string()
449
+ userId: z7.string()
275
450
  });
276
- var workspaceMemberListResponseSchema = z5.object({
277
- members: z5.array(workspaceMemberSchema)
451
+ var workspaceMemberListResponseSchema = z7.object({
452
+ members: z7.array(workspaceMemberSchema)
278
453
  });
279
- var updateWorkspaceMemberSchema = z5.object({
454
+ var updateWorkspaceMemberSchema = z7.object({
280
455
  role: workspaceRoleSchema
281
456
  });
282
- var removeWorkspaceMemberResponseSchema = z5.object({
283
- removed: z5.literal(true)
457
+ var removeWorkspaceMemberResponseSchema = z7.object({
458
+ removed: z7.literal(true)
284
459
  });
285
- var createWorkspaceInviteSchema = z5.object({
286
- email: z5.string().trim().toLowerCase().email().optional(),
287
- expiresAt: z5.string().datetime(),
288
- maxUses: z5.number().int().positive().optional(),
460
+ var createWorkspaceInviteSchema = z7.object({
461
+ email: z7.string().trim().toLowerCase().email().optional(),
462
+ expiresAt: z7.string().datetime(),
463
+ maxUses: z7.number().int().positive().optional(),
289
464
  role: workspaceInviteRoleSchema
290
465
  });
291
- var workspaceInviteSchema = z5.object({
292
- createdAt: z5.string(),
293
- email: z5.string().email().nullable(),
294
- expiresAt: z5.string(),
295
- id: z5.string(),
296
- maxUses: z5.number().int().positive().nullable(),
466
+ var workspaceInviteSchema = z7.object({
467
+ createdAt: z7.string(),
468
+ email: z7.string().email().nullable(),
469
+ expiresAt: z7.string(),
470
+ id: z7.string(),
471
+ maxUses: z7.number().int().positive().nullable(),
297
472
  role: workspaceInviteRoleSchema,
298
- tokenPrefix: z5.string(),
299
- useCount: z5.number().int().nonnegative()
473
+ tokenPrefix: z7.string(),
474
+ useCount: z7.number().int().nonnegative()
300
475
  });
301
476
  var createdWorkspaceInviteSchema = workspaceInviteSchema.extend({
302
- url: z5.string().url()
477
+ url: z7.string().url()
303
478
  });
304
- var workspaceInviteListResponseSchema = z5.object({
305
- invites: z5.array(workspaceInviteSchema)
479
+ var workspaceInviteListResponseSchema = z7.object({
480
+ invites: z7.array(workspaceInviteSchema)
306
481
  });
307
- var revokeWorkspaceInviteResponseSchema = z5.object({
308
- revoked: z5.literal(true)
482
+ var revokeWorkspaceInviteResponseSchema = z7.object({
483
+ revoked: z7.literal(true)
309
484
  });
310
- var acceptWorkspaceInviteSchema = z5.object({
311
- token: z5.string().min(1)
485
+ var acceptWorkspaceInviteSchema = z7.object({
486
+ token: z7.string().min(1)
312
487
  });
313
- var acceptWorkspaceInviteResponseSchema = z5.object({
488
+ var acceptWorkspaceInviteResponseSchema = z7.object({
314
489
  role: workspaceRoleSchema,
315
490
  workspace: workspaceSchema
316
491
  });
317
- var folderVisibilitySchema = z5.enum(["public", "workspace"]);
318
- var artifactVisibilitySchema = z5.enum([
492
+ var folderVisibilitySchema = z7.enum(["public", "workspace"]);
493
+ var artifactVisibilitySchema = z7.enum([
319
494
  "public",
320
495
  "unlisted",
321
496
  "workspace"
322
497
  ]);
323
- var createFolderSchema = z5.object({
324
- name: z5.string().trim().min(1).max(100),
498
+ var createFolderSchema = z7.object({
499
+ name: z7.string().trim().min(1).max(100),
325
500
  visibility: folderVisibilitySchema
326
501
  });
327
- var updateFolderSchema = z5.object({
502
+ var updateFolderSchema = z7.object({
328
503
  // With `visibility`: bulk-update the folder's artifacts to match
329
504
  // (SPEC-2 §3.4 "Changing a folder's visibility offers a bulk update").
330
- cascade: z5.boolean().optional(),
331
- name: z5.string().trim().min(1).max(100).optional(),
332
- sortOrder: z5.number().int().min(0).optional(),
505
+ cascade: z7.boolean().optional(),
506
+ name: z7.string().trim().min(1).max(100).optional(),
507
+ sortOrder: z7.number().int().min(0).optional(),
333
508
  visibility: folderVisibilitySchema.optional()
334
509
  }).refine(
335
510
  (body) => body.name !== void 0 || body.visibility !== void 0 || body.sortOrder !== void 0,
@@ -337,248 +512,168 @@ var updateFolderSchema = z5.object({
337
512
  ).refine((body) => !body.cascade || body.visibility !== void 0, {
338
513
  message: "cascade requires visibility"
339
514
  });
340
- var folderSchema = z5.object({
341
- createdAt: z5.string(),
342
- id: z5.string(),
343
- name: z5.string(),
515
+ var folderSchema = z7.object({
516
+ createdAt: z7.string(),
517
+ id: z7.string(),
518
+ name: z7.string(),
344
519
  // Permalink segment for the public folder page (/w/{handle}/f/{slug}).
345
520
  // Server-derived from the name; renames regenerate it.
346
- slug: z5.string(),
347
- sortOrder: z5.number().int(),
348
- updatedAt: z5.string(),
521
+ slug: z7.string(),
522
+ sortOrder: z7.number().int(),
523
+ updatedAt: z7.string(),
349
524
  visibility: folderVisibilitySchema,
350
- workspaceId: z5.string()
351
- });
352
- var folderListResponseSchema = z5.object({
353
- folders: z5.array(folderSchema)
354
- });
355
- var deleteFolderResponseSchema = z5.object({
356
- deleted: z5.literal(true)
357
- });
358
- var workspaceArtifactSchema = z5.object({
359
- createdAt: z5.string(),
360
- currentVersionNumber: z5.number().int().nullable(),
361
- folderId: z5.string().nullable(),
362
- id: z5.string(),
363
- openCommentCount: z5.number().int().nonnegative(),
364
- slug: z5.string(),
365
- title: z5.string(),
366
- updatedAt: z5.string(),
367
- url: z5.string().url(),
368
- versionCount: z5.number().int().nonnegative(),
525
+ workspaceId: z7.string()
526
+ });
527
+ var folderListResponseSchema = z7.object({
528
+ folders: z7.array(folderSchema)
529
+ });
530
+ var deleteFolderResponseSchema = z7.object({
531
+ deleted: z7.literal(true)
532
+ });
533
+ var workspaceArtifactSchema = z7.object({
534
+ createdAt: z7.string(),
535
+ currentVersionNumber: z7.number().int().nullable(),
536
+ folderId: z7.string().nullable(),
537
+ id: z7.string(),
538
+ openCommentCount: z7.number().int().nonnegative(),
539
+ slug: z7.string(),
540
+ title: z7.string(),
541
+ updatedAt: z7.string(),
542
+ url: z7.string().url(),
543
+ versionCount: z7.number().int().nonnegative(),
369
544
  visibility: artifactVisibilitySchema
370
545
  });
371
- var workspaceArtifactListResponseSchema = z5.object({
372
- artifacts: z5.array(workspaceArtifactSchema)
546
+ var workspaceArtifactListResponseSchema = z7.object({
547
+ artifacts: z7.array(workspaceArtifactSchema)
373
548
  });
374
549
 
375
550
  // ../shared/src/owner.ts
376
551
  var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
377
- var claimResponseSchema = z6.object({
378
- artifactId: z6.string(),
379
- slug: z6.string(),
380
- title: z6.string(),
381
- url: z6.string().url()
552
+ var claimResponseSchema = z8.object({
553
+ artifactId: z8.string(),
554
+ slug: z8.string(),
555
+ title: z8.string(),
556
+ url: z8.string().url()
382
557
  });
383
- var updateArtifactSchema = z6.object({
558
+ var updateArtifactSchema = z8.object({
384
559
  // SPEC-2 §6.4 — placement + visibility. `folderId: null` clears the
385
560
  // folder; `workspaceId: null` moves the artifact out of its workspace
386
561
  // (§5.5 ownership transfer).
387
- folderId: z6.string().min(1).nullable().optional(),
388
- slug: z6.string().min(3).max(63).regex(SLUG_PATTERN, {
562
+ folderId: z8.string().min(1).nullable().optional(),
563
+ slug: z8.string().min(3).max(63).regex(SLUG_PATTERN, {
389
564
  message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
390
565
  }).optional(),
391
- title: z6.string().min(1).max(200).optional(),
566
+ title: z8.string().min(1).max(200).optional(),
392
567
  visibility: artifactVisibilitySchema.optional(),
393
- workspaceId: z6.string().min(1).nullable().optional()
568
+ workspaceId: z8.string().min(1).nullable().optional()
394
569
  }).refine(
395
570
  (body) => body.title !== void 0 || body.slug !== void 0 || body.visibility !== void 0 || body.folderId !== void 0 || body.workspaceId !== void 0,
396
571
  {
397
572
  message: "provide at least one of title, slug, visibility, folderId, workspaceId"
398
573
  }
399
574
  );
400
- var deleteArtifactResponseSchema = z6.object({
401
- deleted: z6.literal(true)
575
+ var deleteArtifactResponseSchema = z8.object({
576
+ deleted: z8.literal(true)
402
577
  });
403
578
 
404
579
  // ../shared/src/presence.ts
405
- import { z as z7 } from "zod";
580
+ import { z as z9 } from "zod";
406
581
  var AGENT_PRESENCE = {
407
582
  heartbeatSeconds: 15,
408
583
  shellPollSeconds: 15,
409
584
  ttlSeconds: 45
410
585
  };
411
- var agentHeartbeatResponseSchema = z7.object({
412
- listening: z7.literal(true),
413
- ttlSeconds: z7.number().int().positive()
586
+ var agentHeartbeatResponseSchema = z9.object({
587
+ listening: z9.literal(true),
588
+ ttlSeconds: z9.number().int().positive()
414
589
  });
415
- var agentPresenceSchema = z7.object({
416
- listening: z7.boolean(),
417
- versionNumber: z7.number().int().positive().nullable()
590
+ var agentPresenceSchema = z9.object({
591
+ listening: z9.boolean(),
592
+ versionNumber: z9.number().int().positive().nullable()
418
593
  });
419
594
 
420
595
  // ../shared/src/protection.ts
421
- import { z as z8 } from "zod";
596
+ import { z as z10 } from "zod";
422
597
  var PASSWORD_MIN = 6;
423
598
  var PASSWORD_MAX = 128;
424
- var gatePasswordSchema = z8.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
425
- var setProtectionSchema = z8.object({
599
+ var gatePasswordSchema = z10.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
600
+ var setProtectionSchema = z10.object({
426
601
  password: gatePasswordSchema.nullable()
427
602
  });
428
- var protectionResponseSchema = z8.object({
429
- protected: z8.boolean()
430
- });
431
- var unlockSchema = z8.object({
432
- password: z8.string().min(1).max(1024)
433
- });
434
- var unlockResponseSchema = z8.object({
435
- unlocked: z8.literal(true)
436
- });
437
-
438
- // ../shared/src/publish.ts
439
- import { z as z9 } from "zod";
440
- function isSafeArtifactPath(p) {
441
- if (p.length === 0 || p.length > 1024) {
442
- return false;
443
- }
444
- if (p.includes("\\") || p.includes("\0")) {
445
- return false;
446
- }
447
- if (p.startsWith("/") || p.startsWith("~")) {
448
- return false;
449
- }
450
- return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
451
- }
452
- var manifestFileSchema = z9.object({
453
- hash: z9.string().regex(/^sha256_[a-f0-9]{64}$/),
454
- mimeType: z9.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
455
- path: z9.string().min(1).max(1024).refine(isSafeArtifactPath, {
456
- message: "path must be a safe relative path"
457
- }),
458
- size: z9.number().int().positive()
459
- });
460
- var publishSourceSchema = z9.object({
461
- agent: z9.string().optional(),
462
- runId: z9.string().optional(),
463
- type: z9.literal("cli")
603
+ var protectionResponseSchema = z10.object({
604
+ protected: z10.boolean()
464
605
  });
465
- var createPublishSessionSchema = z9.object({
466
- artifactId: z9.string().nullable(),
467
- entryPath: z9.string().optional(),
468
- files: z9.array(manifestFileSchema).min(1).max(1e3),
469
- // SPEC-2 §6.4 — workspace placement is available only to authenticated
470
- // members; the API enforces membership and folder ownership.
471
- folderId: z9.string().min(1).optional(),
472
- source: publishSourceSchema,
473
- title: z9.string().max(200).optional(),
474
- workspaceId: z9.string().min(1).optional()
475
- });
476
- var publishSessionResponseSchema = z9.object({
477
- artifactId: z9.string(),
478
- expiresAt: z9.string(),
479
- filesToUpload: z9.array(
480
- z9.object({
481
- method: z9.literal("PUT"),
482
- path: z9.string(),
483
- uploadUrl: z9.string().url()
484
- })
485
- ),
486
- publishSessionId: z9.string(),
487
- skippedFiles: z9.array(z9.string()),
488
- versionId: z9.string()
606
+ var unlockSchema = z10.object({
607
+ password: z10.string().min(1).max(1024)
489
608
  });
490
- var finalizeResponseSchema = z9.object({
491
- artifactId: z9.string(),
492
- // API-key publishes create the artifact already claimed (SPEC §5.4) —
493
- // the CLI uses this to tell "claim URL pending" from "already yours".
494
- claimed: z9.boolean(),
495
- // Returned only on first publish of a new artifact (one-time).
496
- claimUrl: z9.string().url().optional(),
497
- publishToken: z9.string().optional(),
498
- url: z9.string().url(),
499
- versionId: z9.string(),
500
- versionNumber: z9.number().int().positive()
501
- });
502
- var artifactResponseSchema = z9.object({
503
- claimed: z9.boolean(),
504
- currentVersion: z9.object({
505
- entryUrl: z9.string().url(),
506
- id: z9.string(),
507
- versionNumber: z9.number().int().positive()
508
- }).nullable(),
509
- // SPEC-2 §6.4 — placement + visibility surface on the artifact.
510
- folderId: z9.string().nullable(),
511
- id: z9.string(),
512
- slug: z9.string(),
513
- title: z9.string(),
514
- visibility: z9.enum(["public", "unlisted", "workspace"]),
515
- workspaceId: z9.string().nullable()
609
+ var unlockResponseSchema = z10.object({
610
+ unlocked: z10.literal(true)
516
611
  });
517
612
 
518
613
  // ../shared/src/report.ts
519
- import { z as z10 } from "zod";
520
- var reportRequestSchema = z10.object({
521
- reason: z10.string().max(1e3).optional()
614
+ import { z as z11 } from "zod";
615
+ var reportRequestSchema = z11.object({
616
+ reason: z11.string().max(1e3).optional()
522
617
  });
523
- var reportResponseSchema = z10.object({
524
- reported: z10.literal(true)
618
+ var reportResponseSchema = z11.object({
619
+ reported: z11.literal(true)
525
620
  });
526
621
 
527
622
  // ../shared/src/stats.ts
528
- import { z as z11 } from "zod";
529
- var avgViewSecondsSchema = z11.number().int().nonnegative().nullable();
530
- var versionViewStatsSchema = z11.object({
623
+ import { z as z12 } from "zod";
624
+ var avgViewSecondsSchema = z12.number().int().nonnegative().nullable();
625
+ var versionViewStatsSchema = z12.object({
531
626
  avgViewSeconds: avgViewSecondsSchema,
532
627
  // Views from inside the review shell's iframe vs. direct visits to
533
628
  // the render URL. views = embeddedViews + directViews.
534
- directViews: z11.number().int().nonnegative(),
535
- embeddedViews: z11.number().int().nonnegative(),
629
+ directViews: z12.number().int().nonnegative(),
630
+ embeddedViews: z12.number().int().nonnegative(),
536
631
  // Distinct hashed viewer IPs across the version's lifetime.
537
- uniqueViewers: z11.number().int().nonnegative(),
538
- versionNumber: z11.number().int().positive(),
539
- views: z11.number().int().nonnegative()
632
+ uniqueViewers: z12.number().int().nonnegative(),
633
+ versionNumber: z12.number().int().positive(),
634
+ views: z12.number().int().nonnegative()
540
635
  });
541
- var dailyViewsSchema = z11.object({
636
+ var dailyViewsSchema = z12.object({
542
637
  avgViewSeconds: avgViewSecondsSchema,
543
638
  // UTC day, YYYY-MM-DD.
544
- day: z11.string(),
545
- uniqueViewers: z11.number().int().nonnegative(),
546
- views: z11.number().int().nonnegative()
639
+ day: z12.string(),
640
+ uniqueViewers: z12.number().int().nonnegative(),
641
+ views: z12.number().int().nonnegative()
547
642
  });
548
- var artifactStatsResponseSchema = z11.object({
643
+ var artifactStatsResponseSchema = z12.object({
549
644
  avgViewSeconds: avgViewSecondsSchema,
550
- daily: z11.array(dailyViewsSchema),
645
+ daily: z12.array(dailyViewsSchema),
551
646
  // Distinct hashed viewer IPs across all versions and days.
552
- totalUniqueViewers: z11.number().int().nonnegative(),
553
- totalViews: z11.number().int().nonnegative(),
554
- versions: z11.array(versionViewStatsSchema)
647
+ totalUniqueViewers: z12.number().int().nonnegative(),
648
+ totalViews: z12.number().int().nonnegative(),
649
+ versions: z12.array(versionViewStatsSchema)
555
650
  });
556
651
 
557
652
  // ../shared/src/versions.ts
558
- import { z as z12 } from "zod";
559
- var versionSourceSchema = z12.object({
560
- agentName: z12.string().nullable(),
561
- type: z12.string()
653
+ import { z as z13 } from "zod";
654
+ var versionSourceSchema = z13.object({
655
+ agentName: z13.string().nullable(),
656
+ type: z13.string()
562
657
  });
563
- var versionChangesSchema = z12.object({
564
- added: z12.array(z12.string()),
565
- changed: z12.array(z12.string()),
566
- removed: z12.array(z12.string())
658
+ var versionChangesSchema = z13.object({
659
+ added: z13.array(z13.string()),
660
+ changed: z13.array(z13.string()),
661
+ removed: z13.array(z13.string())
567
662
  });
568
- var versionSummarySchema = z12.object({
663
+ var versionSummarySchema = z13.object({
569
664
  // Absent for the first version — there is no previous version to diff against.
570
665
  changes: versionChangesSchema.nullable(),
571
- createdAt: z12.string(),
572
- entryUrl: z12.string().url(),
573
- fileCount: z12.number().int().nonnegative(),
574
- id: z12.string(),
575
- isCurrent: z12.boolean(),
666
+ createdAt: z13.string(),
667
+ entryUrl: z13.string().url(),
668
+ fileCount: z13.number().int().nonnegative(),
669
+ id: z13.string(),
670
+ isCurrent: z13.boolean(),
576
671
  source: versionSourceSchema,
577
- totalSizeBytes: z12.number().int().nonnegative(),
578
- versionNumber: z12.number().int().positive()
672
+ totalSizeBytes: z13.number().int().nonnegative(),
673
+ versionNumber: z13.number().int().positive()
579
674
  });
580
- var versionListResponseSchema = z12.object({
581
- versions: z12.array(versionSummarySchema)
675
+ var versionListResponseSchema = z13.object({
676
+ versions: z13.array(versionSummarySchema)
582
677
  });
583
678
 
584
679
  // src/api.ts
@@ -648,6 +743,36 @@ function createApiClient(apiUrl2, publishToken) {
648
743
  (d) => publishSessionResponseSchema.parse(d)
649
744
  );
650
745
  },
746
+ // SPEC §6.4 — open a new comment thread as the account this client's
747
+ // key belongs to (agent-originated review, §5.1 `comment`). The create
748
+ // route rejects a bare publish token (agents respond, accounts
749
+ // originate), so this must carry an account credential.
750
+ async createThread(artifactId, body) {
751
+ return request(
752
+ `${base}/api/v1/artifacts/${artifactId}/comments`,
753
+ { body: JSON.stringify(body), headers, method: "POST" },
754
+ (d) => commentThreadSchema.parse(d)
755
+ );
756
+ },
757
+ // SPEC §6.7.1 — soft-delete a message you authored. Authorized by this
758
+ // client's credential (account key, or the publish token for an agent
759
+ // message); a guest deletes from the browser, not the CLI.
760
+ async deleteMessage(messageId) {
761
+ await request(
762
+ `${base}/api/v1/comment-messages/${messageId}`,
763
+ { body: "{}", headers, method: "DELETE" },
764
+ () => void 0
765
+ );
766
+ },
767
+ // SPEC §6.7.1 — edit a message you authored (new body + "edited" badge).
768
+ // Same credential rule as delete; returns the updated thread.
769
+ async editMessage(messageId, body) {
770
+ return request(
771
+ `${base}/api/v1/comment-messages/${messageId}`,
772
+ { body: JSON.stringify({ body }), headers, method: "PATCH" },
773
+ (d) => commentThreadSchema.parse(d)
774
+ );
775
+ },
651
776
  async exportComments(artifactId, query) {
652
777
  const params = new URLSearchParams({
653
778
  status: query.status,
@@ -655,7 +780,7 @@ function createApiClient(apiUrl2, publishToken) {
655
780
  });
656
781
  return request(
657
782
  `${base}/api/v1/artifacts/${artifactId}/comments/export?${params}`,
658
- { method: "GET" },
783
+ { headers, method: "GET" },
659
784
  (d) => commentExportSchema.parse(d)
660
785
  );
661
786
  },
@@ -669,7 +794,7 @@ function createApiClient(apiUrl2, publishToken) {
669
794
  async getArtifact(artifactId) {
670
795
  return request(
671
796
  `${base}/api/v1/artifacts/${artifactId}`,
672
- { method: "GET" },
797
+ { headers, method: "GET" },
673
798
  (d) => artifactResponseSchema.parse(d)
674
799
  );
675
800
  },
@@ -705,6 +830,34 @@ function createApiClient(apiUrl2, publishToken) {
705
830
  (d) => meResponseSchema.parse(d)
706
831
  );
707
832
  },
833
+ // SPEC §6.7 — reopen a resolved thread. Same auth as resolve.
834
+ async reopenThread(threadId) {
835
+ return request(
836
+ `${base}/api/v1/comment-threads/${threadId}/reopen`,
837
+ { body: "{}", headers, method: "POST" },
838
+ (d) => commentThreadSchema.parse(d)
839
+ );
840
+ },
841
+ // SPEC §6.5 — post a reply into a thread. Authorized by this client's
842
+ // credential: the artifact's publish token (the reply renders as the
843
+ // agent) or the owner's account key (renders as the owner). No guest
844
+ // payload — the credential is the identity.
845
+ async replyThread(threadId, body) {
846
+ return request(
847
+ `${base}/api/v1/comment-threads/${threadId}/messages`,
848
+ { body: JSON.stringify({ body }), headers, method: "POST" },
849
+ (d) => commentThreadSchema.parse(d)
850
+ );
851
+ },
852
+ // SPEC §6.6 — mark a thread resolved. Authorized by this client's
853
+ // credential: the artifact's publish token, or the owner's account key.
854
+ async resolveThread(threadId) {
855
+ return request(
856
+ `${base}/api/v1/comment-threads/${threadId}/resolve`,
857
+ { body: "{}", headers, method: "POST" },
858
+ (d) => commentThreadSchema.parse(d)
859
+ );
860
+ },
708
861
  // SPEC §6.16 — set a password (string) or clear it (null). Authorized
709
862
  // by the publish token this client was created with.
710
863
  async setProtection(artifactId, password) {
@@ -741,17 +894,17 @@ import {
741
894
  writeFileSync as writeFileSync2
742
895
  } from "fs";
743
896
  import path2 from "path";
744
- import { z as z14 } from "zod";
897
+ import { z as z15 } from "zod";
745
898
 
746
899
  // src/credentials.ts
747
900
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
748
901
  import { homedir } from "os";
749
902
  import path from "path";
750
- import { z as z13 } from "zod";
903
+ import { z as z14 } from "zod";
751
904
  var DEFAULT_API_URL = "https://artifact.cafe";
752
- var credentialsSchema = z13.object({
753
- keys: z13.record(z13.string(), z13.string()),
754
- version: z13.literal(1)
905
+ var credentialsSchema = z14.object({
906
+ keys: z14.record(z14.string(), z14.string()),
907
+ version: z14.literal(1)
755
908
  });
756
909
  function credentialsPath() {
757
910
  return path.join(homedir(), ".artifactcafe", "credentials");
@@ -801,22 +954,28 @@ function resolveApiKey(apiUrl2, flag) {
801
954
  }
802
955
 
803
956
  // src/config.ts
804
- var configSchema = z14.object({
805
- apiUrl: z14.string(),
806
- artifactId: z14.string(),
807
- folder: z14.string().optional(),
808
- folderId: z14.string().optional(),
957
+ var configSchema = z15.object({
958
+ apiUrl: z15.string(),
959
+ artifactId: z15.string(),
960
+ folder: z15.string().optional(),
961
+ folderId: z15.string().optional(),
809
962
  // Optional: a folder linked by `link` to an artifact you own carries no
810
963
  // token — owner actions authorize via your stored login instead. A first
811
964
  // publish still writes the anonymous-mode token here.
812
- publishToken: z14.string().optional(),
813
- slug: z14.string(),
814
- url: z14.string(),
815
- workspace: z14.string().optional(),
816
- workspaceId: z14.string().optional()
817
- });
965
+ publishToken: z15.string().optional(),
966
+ slug: z15.string(),
967
+ url: z15.string(),
968
+ workspace: z15.string().optional(),
969
+ workspaceId: z15.string().optional(),
970
+ workspaceKind: z15.enum(["personal", "workspace"]).optional()
971
+ });
972
+ var projectConfigSchema = z15.object({
973
+ folder: z15.string().trim().min(1).optional(),
974
+ workspace: z15.string().trim().min(1)
975
+ }).strict();
818
976
  var CONFIG_DIR = ".artifactcafe";
819
977
  var CONFIG_FILE = "config.json";
978
+ var PROJECT_CONFIG_FILE = "artifact-cafe.json";
820
979
  function readConfig(dir) {
821
980
  const file = path2.join(dir, CONFIG_DIR, CONFIG_FILE);
822
981
  if (!existsSync2(file)) {
@@ -838,6 +997,48 @@ function writeConfig(dir, config) {
838
997
  );
839
998
  ensureGitignored(dir);
840
999
  }
1000
+ function readProjectConfig(dir) {
1001
+ let current = path2.resolve(dir);
1002
+ while (true) {
1003
+ const file = path2.join(current, PROJECT_CONFIG_FILE);
1004
+ if (existsSync2(file)) {
1005
+ try {
1006
+ return projectConfigSchema.parse(
1007
+ JSON.parse(readFileSync2(file, "utf8"))
1008
+ );
1009
+ } catch (error) {
1010
+ const message = error instanceof z15.ZodError ? error.issues[0]?.message ?? "invalid project target" : "invalid JSON";
1011
+ throw new Error(`Invalid ${file}: ${message}.`);
1012
+ }
1013
+ }
1014
+ if (existsSync2(path2.join(current, ".git"))) {
1015
+ return null;
1016
+ }
1017
+ const parent = path2.dirname(current);
1018
+ if (parent === current) {
1019
+ return null;
1020
+ }
1021
+ current = parent;
1022
+ }
1023
+ }
1024
+ function writeProjectConfig(dir, config) {
1025
+ let root = path2.resolve(dir);
1026
+ while (!existsSync2(path2.join(root, ".git"))) {
1027
+ const parent = path2.dirname(root);
1028
+ if (parent === root) {
1029
+ root = path2.resolve(dir);
1030
+ break;
1031
+ }
1032
+ root = parent;
1033
+ }
1034
+ const file = path2.join(root, PROJECT_CONFIG_FILE);
1035
+ writeFileSync2(
1036
+ file,
1037
+ `${JSON.stringify(projectConfigSchema.parse(config), null, 2)}
1038
+ `
1039
+ );
1040
+ return file;
1041
+ }
841
1042
  function ensureGitignored(dir) {
842
1043
  const gitignore = path2.join(dir, ".gitignore");
843
1044
  if (!existsSync2(gitignore)) {
@@ -876,7 +1077,107 @@ function resolveArtifactTarget(dir, opts = {}) {
876
1077
  return { apiUrl: apiUrl2, artifactId, credential, url };
877
1078
  }
878
1079
 
1080
+ // src/comment.ts
1081
+ async function readStdin() {
1082
+ if (process.stdin.isTTY) {
1083
+ return "";
1084
+ }
1085
+ const chunks = [];
1086
+ for await (const chunk of process.stdin) {
1087
+ chunks.push(chunk);
1088
+ }
1089
+ return Buffer.concat(chunks).toString("utf8");
1090
+ }
1091
+ function buildAnchor(opts) {
1092
+ if (opts.quote !== void 0) {
1093
+ const quote = opts.quote.trim();
1094
+ if (!quote) {
1095
+ throw new Error(
1096
+ "--quote can't be empty. Omit it entirely for a page-level comment."
1097
+ );
1098
+ }
1099
+ const anchor2 = {
1100
+ elementPath: "body",
1101
+ endOffset: 0,
1102
+ prefix: opts.prefix ?? "",
1103
+ quote,
1104
+ startOffset: 0,
1105
+ suffix: opts.suffix ?? "",
1106
+ type: "text"
1107
+ };
1108
+ if (opts.page) {
1109
+ anchor2.page = opts.page;
1110
+ }
1111
+ return anchor2;
1112
+ }
1113
+ if (opts.prefix || opts.suffix) {
1114
+ throw new Error("--prefix and --suffix only apply together with --quote.");
1115
+ }
1116
+ const anchor = { type: "page" };
1117
+ if (opts.page) {
1118
+ anchor.page = opts.page;
1119
+ }
1120
+ return anchor;
1121
+ }
1122
+ async function comment(opts) {
1123
+ const body = (opts.body ?? await readStdin()).trim();
1124
+ if (!body) {
1125
+ throw new Error(
1126
+ 'Nothing to post. Pass the comment with --body "\u2026", or pipe it on stdin.'
1127
+ );
1128
+ }
1129
+ const anchor = buildAnchor(opts);
1130
+ const dir = path3.resolve(opts.dir);
1131
+ const target = resolveArtifactTarget(dir, {
1132
+ apiKey: opts.apiKey,
1133
+ artifact: opts.artifact
1134
+ });
1135
+ const credential = resolveApiKey(target.apiUrl, opts.apiKey) ?? target.credential;
1136
+ const client = createApiClient(target.apiUrl, credential);
1137
+ const artifact = await client.getArtifact(target.artifactId);
1138
+ if (!artifact.currentVersion) {
1139
+ throw new Error(
1140
+ "This artifact has no published version yet \u2014 publish before commenting."
1141
+ );
1142
+ }
1143
+ let thread;
1144
+ try {
1145
+ thread = await client.createThread(target.artifactId, {
1146
+ anchor,
1147
+ body,
1148
+ versionId: artifact.currentVersion.id,
1149
+ via: opts.via
1150
+ });
1151
+ } catch (err) {
1152
+ if (err instanceof ApiRequestError && err.status === 401) {
1153
+ throw new Error(
1154
+ "Publish tokens can't open threads. Run `artifact-cafe login` to comment as your account."
1155
+ );
1156
+ }
1157
+ throw err;
1158
+ }
1159
+ if (opts.json) {
1160
+ console.log(
1161
+ JSON.stringify(
1162
+ {
1163
+ anchorType: thread.anchor.type,
1164
+ id: thread.id,
1165
+ messageId: thread.messages[0]?.id,
1166
+ versionId: thread.versionId
1167
+ },
1168
+ null,
1169
+ 2
1170
+ )
1171
+ );
1172
+ return;
1173
+ }
1174
+ const url = target.url ?? reviewUrl(target.apiUrl, artifact.slug);
1175
+ const where = anchor.type === "text" ? ` on \u201C${anchor.quote}\u201D` : " on the page";
1176
+ console.log(`\u2713 Opened thread ${thread.id}${where} \u2014 ${url}`);
1177
+ }
1178
+
879
1179
  // src/comments.ts
1180
+ import path4 from "path";
880
1181
  var STATUSES = ["open", "resolved", "all"];
881
1182
  var VERSIONS = ["current", "all"];
882
1183
  var pollSeconds = () => Number(process.env.AC_WAIT_POLL_SECONDS ?? 5);
@@ -895,7 +1196,7 @@ async function comments(opts) {
895
1196
  }
896
1197
  const status = opts.status;
897
1198
  const version = opts.version;
898
- const dir = path3.resolve(opts.dir);
1199
+ const dir = path4.resolve(opts.dir);
899
1200
  const target = resolveArtifactTarget(dir, {
900
1201
  apiKey: opts.apiKey,
901
1202
  artifact: opts.artifact
@@ -1054,21 +1355,30 @@ function printHuman(data, url, status, version) {
1054
1355
  function printThread(t) {
1055
1356
  const marker = t.status === "resolved" ? "\u2713" : "\u25CF";
1056
1357
  const date = t.createdAt.slice(0, 10);
1358
+ const edited = t.editedAt && !t.deleted ? " \xB7 edited" : "";
1057
1359
  console.log(
1058
- `${marker} ${t.author} \xB7 v${t.versionNumber} \xB7 ${date} \xB7 ${t.id}`
1360
+ `${marker} ${t.author} \xB7 v${t.versionNumber} \xB7 ${date} \xB7 ${t.id}${edited}`
1059
1361
  );
1060
1362
  if (t.anchor.type === "text") {
1061
1363
  console.log(` \u21B3 "${t.anchor.quote}"`);
1062
- } else if (t.anchor.element) {
1364
+ } else if (t.anchor.type === "point") {
1063
1365
  const el = t.anchor.element;
1064
- const text = el.text ? ` "${el.text}"` : "";
1065
- console.log(` \u21B3 at ${elementLabel(el)}${text} \u2014 ${t.anchor.elementPath}`);
1366
+ if (el) {
1367
+ const text = el.text ? ` "${el.text}"` : "";
1368
+ console.log(
1369
+ ` \u21B3 at ${elementLabel(el)}${text} \u2014 ${t.anchor.elementPath}`
1370
+ );
1371
+ } else {
1372
+ console.log(` \u21B3 at ${t.anchor.elementPath}`);
1373
+ }
1066
1374
  } else {
1067
- console.log(` \u21B3 at ${t.anchor.elementPath}`);
1375
+ console.log(t.anchor.page ? ` \u21B3 on ${t.anchor.page}` : " \u21B3 page comment");
1068
1376
  }
1069
- console.log(indent(t.body, " "));
1070
- for (const reply of t.replies) {
1071
- console.log(indent(`${reply.author}: ${reply.body}`, " "));
1377
+ console.log(indent(t.deleted ? "[deleted]" : t.body, " "));
1378
+ for (const reply2 of t.replies) {
1379
+ const replyEdited = reply2.editedAt && !reply2.deleted ? " (edited)" : "";
1380
+ const replyBody = reply2.deleted ? "[deleted]" : reply2.body;
1381
+ console.log(indent(`${reply2.author}: ${replyBody}${replyEdited}`, " "));
1072
1382
  }
1073
1383
  console.log("");
1074
1384
  }
@@ -1077,19 +1387,20 @@ function indent(text, pad) {
1077
1387
  }
1078
1388
 
1079
1389
  // src/dashboard.ts
1080
- import path6 from "path";
1390
+ import path7 from "path";
1081
1391
 
1082
1392
  // src/publish.ts
1083
1393
  import { exec } from "child_process";
1084
1394
  import { readFileSync as readFileSync4 } from "fs";
1085
- import path5 from "path";
1395
+ import path6 from "path";
1396
+ import { createInterface } from "readline";
1086
1397
 
1087
1398
  // src/manifest.ts
1088
1399
  import { createHash } from "crypto";
1089
1400
  import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
1090
- import path4 from "path";
1401
+ import path5 from "path";
1091
1402
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".artifactcafe"]);
1092
- var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
1403
+ var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store", "artifact-cafe.json"]);
1093
1404
  function isIgnoredFile(name) {
1094
1405
  if (IGNORED_FILES.has(name)) {
1095
1406
  return true;
@@ -1136,7 +1447,7 @@ var MIME_TYPES = {
1136
1447
  ".xml": "application/xml"
1137
1448
  };
1138
1449
  function mimeFor(filePath) {
1139
- return MIME_TYPES[path4.extname(filePath).toLowerCase()] ?? "application/octet-stream";
1450
+ return MIME_TYPES[path5.extname(filePath).toLowerCase()] ?? "application/octet-stream";
1140
1451
  }
1141
1452
  function scanDirectory(dir) {
1142
1453
  const files = [];
@@ -1144,7 +1455,7 @@ function scanDirectory(dir) {
1144
1455
  function walk(current, relPrefix) {
1145
1456
  for (const entry of readdirSync(current, { withFileTypes: true })) {
1146
1457
  const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
1147
- const abs = path4.join(current, entry.name);
1458
+ const abs = path5.join(current, entry.name);
1148
1459
  if (entry.isSymbolicLink()) {
1149
1460
  skipped.push(rel);
1150
1461
  continue;
@@ -1210,7 +1521,7 @@ function resolveEntry(files, explicit) {
1210
1521
  // src/publish.ts
1211
1522
  var UPLOAD_CONCURRENCY = 6;
1212
1523
  async function publish(opts) {
1213
- const dir = path5.resolve(opts.dir);
1524
+ const dir = path6.resolve(opts.dir);
1214
1525
  const config = readConfig(dir);
1215
1526
  if (opts.password !== void 0) {
1216
1527
  const parsed = gatePasswordSchema.safeParse(opts.password);
@@ -1222,7 +1533,13 @@ async function publish(opts) {
1222
1533
  const token = (artifactId !== null && artifactId === config?.artifactId ? config.publishToken : void 0) ?? process.env.ARTIFACT_CAFE_TOKEN;
1223
1534
  const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
1224
1535
  const apiKey = resolveApiKey(apiUrl2, opts.apiKey);
1225
- const placement = await resolvePlacement({ apiKey, apiUrl: apiUrl2, config, opts });
1536
+ const placement = await resolvePlacement({
1537
+ apiKey,
1538
+ apiUrl: apiUrl2,
1539
+ config,
1540
+ isNewArtifact: artifactId === null,
1541
+ opts
1542
+ });
1226
1543
  const { files, skipped } = scanDirectory(dir);
1227
1544
  if (files.length === 0) {
1228
1545
  throw new Error(`Nothing to publish in ${dir} (after ignore rules).`);
@@ -1247,7 +1564,7 @@ async function publish(opts) {
1247
1564
  type: "cli"
1248
1565
  },
1249
1566
  title: opts.title,
1250
- ...placement.workspaceId ? { workspaceId: placement.workspaceId } : {}
1567
+ ...placement.publishWorkspaceId ? { workspaceId: placement.publishWorkspaceId } : {}
1251
1568
  };
1252
1569
  let credential = apiKey ?? token;
1253
1570
  let client = createApiClient(apiUrl2, credential);
@@ -1279,8 +1596,9 @@ async function publish(opts) {
1279
1596
  );
1280
1597
  });
1281
1598
  const result = await client.finalize(session.publishSessionId);
1599
+ const rememberedProjectFile = placement.rememberProjectDefault ? writeProjectConfig(dir, placement.rememberProjectDefault) : null;
1282
1600
  const existingToken = config?.artifactId === result.artifactId ? config.publishToken : void 0;
1283
- const shouldWriteConfig = result.publishToken !== void 0 || config?.artifactId === result.artifactId || opts.workspace !== void 0 || opts.folder !== void 0;
1601
+ const shouldWriteConfig = result.publishToken !== void 0 || config?.artifactId === result.artifactId || opts.workspace !== void 0 || opts.folder !== void 0 || placement.workspaceId !== void 0;
1284
1602
  if (shouldWriteConfig) {
1285
1603
  writeConfig(dir, {
1286
1604
  apiUrl: apiUrl2,
@@ -1291,7 +1609,8 @@ async function publish(opts) {
1291
1609
  slug: slugFromUrl(result.url),
1292
1610
  url: result.url,
1293
1611
  ...placement.workspace ? { workspace: placement.workspace } : {},
1294
- ...placement.workspaceId ? { workspaceId: placement.workspaceId } : {}
1612
+ ...placement.workspaceId ? { workspaceId: placement.workspaceId } : {},
1613
+ ...placement.destination === "personal" || placement.destination === "workspace" ? { workspaceKind: placement.destination } : {}
1295
1614
  });
1296
1615
  }
1297
1616
  if (opts.password !== void 0) {
@@ -1307,6 +1626,8 @@ async function publish(opts) {
1307
1626
  {
1308
1627
  artifactId: result.artifactId,
1309
1628
  claimed: result.claimed,
1629
+ destination: placement.destination,
1630
+ destinationSource: placement.destinationSource,
1310
1631
  folder: placement.folder ?? null,
1311
1632
  folderId: placement.folderId ?? null,
1312
1633
  url: result.url,
@@ -1325,6 +1646,12 @@ async function publish(opts) {
1325
1646
  );
1326
1647
  } else if (result.versionNumber === 1) {
1327
1648
  console.log("\n\u2713 Published to artifact.cafe\n");
1649
+ console.log(`Destination: ${placement.destinationLabel}
1650
+ `);
1651
+ if (rememberedProjectFile) {
1652
+ console.log(`Project default: ${rememberedProjectFile}
1653
+ `);
1654
+ }
1328
1655
  console.log(`Review URL:
1329
1656
  ${result.url}
1330
1657
  `);
@@ -1344,6 +1671,12 @@ ${result.claimUrl}
1344
1671
  console.log(`
1345
1672
  \u2713 Published v${result.versionNumber}
1346
1673
  `);
1674
+ console.log(`Destination: ${placement.destinationLabel}
1675
+ `);
1676
+ if (rememberedProjectFile) {
1677
+ console.log(`Project default: ${rememberedProjectFile}
1678
+ `);
1679
+ }
1347
1680
  console.log(`${result.url}
1348
1681
  `);
1349
1682
  if (opts.password !== void 0) {
@@ -1355,75 +1688,181 @@ ${result.claimUrl}
1355
1688
  }
1356
1689
  }
1357
1690
  async function resolvePlacement(params) {
1358
- const { apiKey, apiUrl: apiUrl2, config, opts } = params;
1691
+ const { apiKey, apiUrl: apiUrl2, config, isNewArtifact, opts } = params;
1359
1692
  const hasPlacementFlag = opts.workspace !== void 0 || opts.folder !== void 0;
1360
1693
  if (!hasPlacementFlag && config?.workspaceId) {
1361
- if (!apiKey) {
1694
+ const isPersonal = config.workspaceKind === "personal";
1695
+ if (!(apiKey || isPersonal)) {
1362
1696
  throw new Error(
1363
1697
  `Publishing into workspace ${config.workspace ?? config.workspaceId} needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
1364
1698
  );
1365
1699
  }
1366
1700
  return {
1701
+ destination: isPersonal ? "personal" : "workspace",
1702
+ destinationLabel: isPersonal ? "Personal" : config.workspace ?? config.workspaceId,
1703
+ destinationSource: "artifact-config",
1367
1704
  ...config.folder ? { folder: config.folder } : {},
1368
1705
  ...config.folderId ? { folderId: config.folderId } : {},
1369
1706
  ...config.workspace ? { workspace: config.workspace } : {},
1707
+ ...apiKey ? { publishWorkspaceId: config.workspaceId } : {},
1370
1708
  workspaceId: config.workspaceId
1371
1709
  };
1372
1710
  }
1373
- if (!hasPlacementFlag) {
1374
- return {};
1711
+ if (!(hasPlacementFlag || isNewArtifact)) {
1712
+ return {
1713
+ destination: "existing-artifact",
1714
+ destinationLabel: "Existing artifact",
1715
+ destinationSource: "artifact-config"
1716
+ };
1375
1717
  }
1376
- if (!apiKey) {
1718
+ const projectConfig = isNewArtifact ? readProjectConfig(opts.dir) : null;
1719
+ const wantsWorkspace = opts.workspace !== void 0 || opts.folder !== void 0 || projectConfig !== null;
1720
+ if (wantsWorkspace && !apiKey) {
1377
1721
  throw new Error(
1378
1722
  "Publishing into a workspace needs an account \u2014 run `artifact-cafe login` first."
1379
1723
  );
1380
1724
  }
1381
- const workspaceHandle = opts.workspace ?? config?.workspace;
1382
- if (!workspaceHandle) {
1725
+ if (!(hasPlacementFlag || projectConfig)) {
1726
+ if (!apiKey) {
1727
+ return {
1728
+ destination: "anonymous",
1729
+ destinationLabel: "Anonymous",
1730
+ destinationSource: "anonymous-default"
1731
+ };
1732
+ }
1733
+ if (shouldPromptForWorkspace(opts)) {
1734
+ const data2 = await createApiClient(apiUrl2, apiKey).listWorkspaces();
1735
+ if (data2.workspaces.length > 1) {
1736
+ const choice = await pickWorkspace(data2.workspaces);
1737
+ return {
1738
+ destination: choice.workspace.isPersonal ? "personal" : "workspace",
1739
+ destinationLabel: choice.workspace.isPersonal ? "Personal" : choice.workspace.name,
1740
+ destinationSource: "interactive",
1741
+ publishWorkspaceId: choice.workspace.id,
1742
+ ...choice.remember ? {
1743
+ rememberProjectDefault: {
1744
+ workspace: choice.workspace.handle
1745
+ }
1746
+ } : {},
1747
+ workspace: choice.workspace.handle,
1748
+ workspaceId: choice.workspace.id
1749
+ };
1750
+ }
1751
+ }
1752
+ return {
1753
+ destination: "personal",
1754
+ destinationLabel: "Personal",
1755
+ destinationSource: "personal-default"
1756
+ };
1757
+ }
1758
+ if (!isNewArtifact && opts.folder !== void 0 && opts.workspace === void 0 && !(config?.workspace || config?.workspaceId)) {
1383
1759
  throw new Error("--folder requires --workspace or a stored workspace.");
1384
1760
  }
1385
1761
  const client = createApiClient(apiUrl2, apiKey);
1386
- let workspaceId;
1387
- if (opts.workspace === void 0 && config?.workspaceId) {
1388
- ({ workspaceId } = config);
1389
- } else {
1390
- const data2 = await client.listWorkspaces();
1391
- const workspace = data2.workspaces.find(
1762
+ const data = await client.listWorkspaces();
1763
+ const workspaceHandle = opts.workspace ?? config?.workspace ?? projectConfig?.workspace;
1764
+ let workspace;
1765
+ if (workspaceHandle) {
1766
+ workspace = data.workspaces.find(
1392
1767
  (candidate) => candidate.handle === workspaceHandle
1393
1768
  );
1394
- if (!workspace) {
1395
- throw new Error(
1396
- `No workspace with handle "${workspaceHandle}" belongs to this account.`
1397
- );
1398
- }
1399
- workspaceId = workspace.id;
1769
+ } else if (config?.workspaceId) {
1770
+ workspace = data.workspaces.find(
1771
+ (candidate) => candidate.id === config.workspaceId
1772
+ );
1773
+ } else {
1774
+ workspace = data.workspaces.find((candidate) => candidate.isPersonal);
1775
+ }
1776
+ if (!workspace) {
1777
+ const target = workspaceHandle ? `workspace with handle "${workspaceHandle}"` : "personal workspace";
1778
+ throw new Error(`No ${target} belongs to this account.`);
1400
1779
  }
1401
- const folderName = opts.workspace !== void 0 && opts.folder === void 0 ? void 0 : opts.folder ?? config?.folder;
1780
+ const folderName = opts.workspace !== void 0 && opts.folder === void 0 ? void 0 : opts.folder ?? projectConfig?.folder;
1781
+ const destinationSource = hasPlacementFlag ? "explicit" : "project-default";
1402
1782
  if (!folderName) {
1403
- return { workspace: workspaceHandle, workspaceId };
1783
+ return {
1784
+ destination: workspace.isPersonal ? "personal" : "workspace",
1785
+ destinationLabel: workspace.isPersonal ? "Personal" : workspace.name,
1786
+ destinationSource,
1787
+ publishWorkspaceId: workspace.id,
1788
+ workspace: workspace.handle,
1789
+ workspaceId: workspace.id
1790
+ };
1404
1791
  }
1405
- const data = await client.listFolders(workspaceId);
1406
- const matches = data.folders.filter(
1792
+ const folderData = await client.listFolders(workspace.id);
1793
+ const matches = folderData.folders.filter(
1407
1794
  (candidate) => candidate.name === folderName || candidate.id === folderName
1408
1795
  );
1409
1796
  if (matches.length === 0) {
1410
1797
  throw new Error(
1411
- `No folder named "${folderName}" exists in workspace ${workspaceHandle}.`
1798
+ `No folder named "${folderName}" exists in workspace ${workspace.handle}.`
1412
1799
  );
1413
1800
  }
1414
1801
  if (matches.length > 1) {
1415
1802
  throw new Error(
1416
- `More than one folder is named "${folderName}" in workspace ${workspaceHandle}; pass the folder id instead.`
1803
+ `More than one folder is named "${folderName}" in workspace ${workspace.handle}; pass the folder id instead.`
1417
1804
  );
1418
1805
  }
1419
1806
  const folder = matches[0];
1420
1807
  return {
1808
+ destination: workspace.isPersonal ? "personal" : "workspace",
1809
+ destinationLabel: workspace.isPersonal ? "Personal" : workspace.name,
1810
+ destinationSource,
1421
1811
  folder: folder.name,
1422
1812
  folderId: folder.id,
1423
- workspace: workspaceHandle,
1424
- workspaceId
1813
+ publishWorkspaceId: workspace.id,
1814
+ workspace: workspace.handle,
1815
+ workspaceId: workspace.id
1425
1816
  };
1426
1817
  }
1818
+ function shouldPromptForWorkspace(opts) {
1819
+ if (opts.json || process.env.ARTIFACT_CAFE_AGENT) {
1820
+ return false;
1821
+ }
1822
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
1823
+ }
1824
+ function pickWorkspace(workspaces3) {
1825
+ const choices = [...workspaces3].sort((a, b) => {
1826
+ if (a.isPersonal !== b.isPersonal) {
1827
+ return a.isPersonal ? -1 : 1;
1828
+ }
1829
+ return a.name.localeCompare(b.name);
1830
+ });
1831
+ console.log("Where should this artifact live?\n");
1832
+ choices.forEach((workspace, index) => {
1833
+ const label = workspace.isPersonal ? "Personal" : workspace.name;
1834
+ const handle = workspace.isPersonal ? "" : ` \xB7 ${workspace.handle}`;
1835
+ console.log(` ${String(index + 1).padStart(2)}. ${label}${handle}`);
1836
+ });
1837
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1838
+ return new Promise((resolve2, reject) => {
1839
+ rl.question(`
1840
+ Publish where? [1-${choices.length}]: `, (answer) => {
1841
+ const index = Number(answer.trim()) - 1;
1842
+ const workspace = choices[index];
1843
+ if (!(Number.isInteger(index) && workspace)) {
1844
+ rl.close();
1845
+ reject(new Error(`Enter a number between 1 and ${choices.length}.`));
1846
+ return;
1847
+ }
1848
+ if (workspace.isPersonal) {
1849
+ rl.close();
1850
+ resolve2({ remember: false, workspace });
1851
+ return;
1852
+ }
1853
+ rl.question(
1854
+ `Remember ${workspace.name} for new artifacts in this project? [y/N]: `,
1855
+ (rememberAnswer) => {
1856
+ rl.close();
1857
+ resolve2({
1858
+ remember: /^y(?:es)?$/i.test(rememberAnswer.trim()),
1859
+ workspace
1860
+ });
1861
+ }
1862
+ );
1863
+ });
1864
+ });
1865
+ }
1427
1866
  async function inBatches(items, concurrency, fn) {
1428
1867
  let next = 0;
1429
1868
  const workers = Array.from(
@@ -1460,7 +1899,7 @@ function openInBrowser(url) {
1460
1899
 
1461
1900
  // src/dashboard.ts
1462
1901
  function openDashboard(opts) {
1463
- const base = process.env.ARTIFACT_CAFE_URL ?? readConfig(path6.resolve(opts.dir))?.apiUrl ?? DEFAULT_API_URL;
1902
+ const base = process.env.ARTIFACT_CAFE_URL ?? readConfig(path7.resolve(opts.dir))?.apiUrl ?? DEFAULT_API_URL;
1464
1903
  const url = `${base.replace(/\/$/, "")}/dashboard`;
1465
1904
  console.log(url);
1466
1905
  if (process.stdout.isTTY) {
@@ -1486,7 +1925,12 @@ function printCliError(err, json) {
1486
1925
  if (err.retryAfter) {
1487
1926
  console.error(` Retry in ${err.retryAfter}s.`);
1488
1927
  }
1489
- }
1928
+ if (err.code === "password_required") {
1929
+ console.error(
1930
+ " Run `artifact-cafe login` as the owner, or run from the published folder (its publish token unlocks it)."
1931
+ );
1932
+ }
1933
+ }
1490
1934
  return;
1491
1935
  }
1492
1936
  const message = err instanceof Error ? err.message : String(err);
@@ -1544,8 +1988,8 @@ function printFolder(folder) {
1544
1988
  }
1545
1989
 
1546
1990
  // src/link.ts
1547
- import path7 from "path";
1548
- import { createInterface } from "readline";
1991
+ import path8 from "path";
1992
+ import { createInterface as createInterface2 } from "readline";
1549
1993
  async function link(opts) {
1550
1994
  const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
1551
1995
  /\/$/,
@@ -1587,7 +2031,7 @@ async function link(opts) {
1587
2031
  }
1588
2032
  chosen = await pickArtifact(artifacts);
1589
2033
  }
1590
- const dir = path7.resolve(opts.dir);
2034
+ const dir = path8.resolve(opts.dir);
1591
2035
  writeConfig(dir, {
1592
2036
  apiUrl: apiUrl2,
1593
2037
  artifactId: chosen.id,
@@ -1624,8 +2068,8 @@ function pickArtifact(artifacts) {
1624
2068
  ` ${String(i + 1).padStart(2)}. ${a.title} \xB7 ${v} \xB7 ${a.url}`
1625
2069
  );
1626
2070
  });
1627
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1628
- return new Promise((resolve, reject) => {
2071
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
2072
+ return new Promise((resolve2, reject) => {
1629
2073
  rl.question(`
1630
2074
  Link which? [1-${artifacts.length}]: `, (answer) => {
1631
2075
  rl.close();
@@ -1634,7 +2078,7 @@ Link which? [1-${artifacts.length}]: `, (answer) => {
1634
2078
  reject(new Error(`Enter a number between 1 and ${artifacts.length}.`));
1635
2079
  return;
1636
2080
  }
1637
- resolve(artifacts[n - 1]);
2081
+ resolve2(artifacts[n - 1]);
1638
2082
  });
1639
2083
  });
1640
2084
  }
@@ -1696,13 +2140,13 @@ function relativeTime(iso) {
1696
2140
  }
1697
2141
 
1698
2142
  // src/login.ts
1699
- import { createInterface as createInterface2 } from "readline";
2143
+ import { createInterface as createInterface3 } from "readline";
1700
2144
  function apiUrl() {
1701
2145
  return (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
1702
2146
  }
1703
2147
  function promptSecret(question) {
1704
- return new Promise((resolve) => {
1705
- const rl = createInterface2({
2148
+ return new Promise((resolve2) => {
2149
+ const rl = createInterface3({
1706
2150
  input: process.stdin,
1707
2151
  output: process.stdout
1708
2152
  });
@@ -1716,7 +2160,7 @@ function promptSecret(question) {
1716
2160
  rl.question(question, (answer) => {
1717
2161
  rl.close();
1718
2162
  process.stdout.write("\n");
1719
- resolve(answer);
2163
+ resolve2(answer);
1720
2164
  });
1721
2165
  muted = true;
1722
2166
  });
@@ -1809,10 +2253,69 @@ function logout(opts) {
1809
2253
  );
1810
2254
  }
1811
2255
 
2256
+ // src/message.ts
2257
+ import path9 from "path";
2258
+ async function readStdin2() {
2259
+ if (process.stdin.isTTY) {
2260
+ return "";
2261
+ }
2262
+ const chunks = [];
2263
+ for await (const chunk of process.stdin) {
2264
+ chunks.push(chunk);
2265
+ }
2266
+ return Buffer.concat(chunks).toString("utf8");
2267
+ }
2268
+ async function editMessage(opts) {
2269
+ const messageId = opts.message?.trim();
2270
+ if (!messageId) {
2271
+ throw new Error(
2272
+ "Pass the message id to edit \u2014 get it from `artifact-cafe comments --json`."
2273
+ );
2274
+ }
2275
+ const body = (opts.body ?? await readStdin2()).trim();
2276
+ if (!body) {
2277
+ throw new Error(
2278
+ 'Nothing to save. Pass the new text with --body "\u2026", or pipe it on stdin.'
2279
+ );
2280
+ }
2281
+ const dir = path9.resolve(opts.dir);
2282
+ const target = resolveArtifactTarget(dir, {
2283
+ apiKey: opts.apiKey,
2284
+ artifact: opts.artifact
2285
+ });
2286
+ const client = createApiClient(target.apiUrl, target.credential);
2287
+ const thread = await client.editMessage(messageId, body);
2288
+ if (opts.json) {
2289
+ console.log(JSON.stringify({ id: thread.id, messageId }, null, 2));
2290
+ return;
2291
+ }
2292
+ console.log(`\u2713 Edited ${messageId}.`);
2293
+ }
2294
+ async function deleteMessage(opts) {
2295
+ const messageId = opts.message?.trim();
2296
+ if (!messageId) {
2297
+ throw new Error(
2298
+ "Pass the message id to delete \u2014 get it from `artifact-cafe comments --json`."
2299
+ );
2300
+ }
2301
+ const dir = path9.resolve(opts.dir);
2302
+ const target = resolveArtifactTarget(dir, {
2303
+ apiKey: opts.apiKey,
2304
+ artifact: opts.artifact
2305
+ });
2306
+ const client = createApiClient(target.apiUrl, target.credential);
2307
+ await client.deleteMessage(messageId);
2308
+ if (opts.json) {
2309
+ console.log(JSON.stringify({ deleted: true, messageId }, null, 2));
2310
+ return;
2311
+ }
2312
+ console.log(`\u2713 Deleted ${messageId}.`);
2313
+ }
2314
+
1812
2315
  // src/open.ts
1813
- import path8 from "path";
2316
+ import path10 from "path";
1814
2317
  async function openArtifact(opts) {
1815
- const dir = path8.resolve(opts.dir);
2318
+ const dir = path10.resolve(opts.dir);
1816
2319
  const target = resolveArtifactTarget(dir, {
1817
2320
  apiKey: opts.apiKey,
1818
2321
  artifact: opts.artifact
@@ -1830,7 +2333,7 @@ async function openArtifact(opts) {
1830
2333
  }
1831
2334
 
1832
2335
  // src/protect.ts
1833
- import path9 from "path";
2336
+ import path11 from "path";
1834
2337
  async function protect(opts) {
1835
2338
  if (opts.off && opts.password !== void 0) {
1836
2339
  throw new Error("Pass either --password or --off, not both.");
@@ -1847,7 +2350,7 @@ async function protect(opts) {
1847
2350
  throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
1848
2351
  }
1849
2352
  }
1850
- const dir = path9.resolve(opts.dir);
2353
+ const dir = path11.resolve(opts.dir);
1851
2354
  const target = resolveArtifactTarget(dir, {
1852
2355
  apiKey: opts.apiKey,
1853
2356
  artifact: opts.artifact
@@ -1871,6 +2374,234 @@ async function protect(opts) {
1871
2374
  );
1872
2375
  }
1873
2376
 
2377
+ // src/reply.ts
2378
+ import path12 from "path";
2379
+ async function readStdin3() {
2380
+ if (process.stdin.isTTY) {
2381
+ return "";
2382
+ }
2383
+ const chunks = [];
2384
+ for await (const chunk of process.stdin) {
2385
+ chunks.push(chunk);
2386
+ }
2387
+ return Buffer.concat(chunks).toString("utf8");
2388
+ }
2389
+ async function replyToThread(opts) {
2390
+ const threadId = opts.thread?.trim();
2391
+ if (!threadId) {
2392
+ throw new Error(
2393
+ "Pass the thread id to reply to \u2014 get it from `artifact-cafe comments --json`."
2394
+ );
2395
+ }
2396
+ const body = (opts.body ?? await readStdin3()).trim();
2397
+ if (!body) {
2398
+ throw new Error(
2399
+ 'Nothing to post. Pass the reply with --body "\u2026", or pipe it on stdin.'
2400
+ );
2401
+ }
2402
+ const dir = path12.resolve(opts.dir);
2403
+ const target = resolveArtifactTarget(dir, {
2404
+ apiKey: opts.apiKey,
2405
+ artifact: opts.artifact
2406
+ });
2407
+ const client = createApiClient(target.apiUrl, target.credential);
2408
+ let thread = await client.replyThread(threadId, body);
2409
+ if (opts.resolve) {
2410
+ thread = await client.resolveThread(threadId);
2411
+ }
2412
+ if (opts.json) {
2413
+ console.log(
2414
+ JSON.stringify(
2415
+ {
2416
+ id: thread.id,
2417
+ messageCount: thread.messages.length,
2418
+ status: thread.status
2419
+ },
2420
+ null,
2421
+ 2
2422
+ )
2423
+ );
2424
+ return;
2425
+ }
2426
+ const closed = opts.resolve ? " and resolved it" : "";
2427
+ console.log(`\u2713 Replied to ${thread.id}${closed} \u2014 now ${thread.status}.`);
2428
+ }
2429
+
2430
+ // src/resolve.ts
2431
+ import path13 from "path";
2432
+ async function changeThreadStatus(opts) {
2433
+ const threadId = opts.thread?.trim();
2434
+ if (!threadId) {
2435
+ throw new Error(
2436
+ `Pass the thread id to ${opts.action} \u2014 get it from \`artifact-cafe comments --json\`.`
2437
+ );
2438
+ }
2439
+ const dir = path13.resolve(opts.dir);
2440
+ const target = resolveArtifactTarget(dir, {
2441
+ apiKey: opts.apiKey,
2442
+ artifact: opts.artifact
2443
+ });
2444
+ const client = createApiClient(target.apiUrl, target.credential);
2445
+ const thread = opts.action === "resolve" ? await client.resolveThread(threadId) : await client.reopenThread(threadId);
2446
+ if (opts.json) {
2447
+ console.log(
2448
+ JSON.stringify({ id: thread.id, status: thread.status }, null, 2)
2449
+ );
2450
+ return;
2451
+ }
2452
+ const verb = opts.action === "resolve" ? "Resolved" : "Reopened";
2453
+ console.log(`\u2713 ${verb} ${thread.id} \u2014 now ${thread.status}.`);
2454
+ }
2455
+
2456
+ // src/update.ts
2457
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
2458
+ import { createRequire } from "module";
2459
+ import { homedir as homedir2 } from "os";
2460
+ import path14 from "path";
2461
+ import { z as z16 } from "zod";
2462
+ var REGISTRY_LATEST_URL = "https://registry.npmjs.org/artifact-cafe/latest";
2463
+ var SKILL_SOURCE_URL = "https://raw.githubusercontent.com/Artifact-Cafe/skill/main/artifact-cafe/SKILL.md";
2464
+ var SKILL_UPDATE_COMMAND = "npx skills add artifact-cafe/skill --skill artifact-cafe -g";
2465
+ var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
2466
+ var FETCH_TIMEOUT_MS = 2e3;
2467
+ var SKILL_VERSION_PATTERN = /\*\*Skill version:\s*(\d+\.\d+\.\d+)\*\*/;
2468
+ var cacheSchema = z16.object({
2469
+ checkedAt: z16.number(),
2470
+ cliLatest: z16.string().nullable(),
2471
+ skillLatest: z16.string().nullable(),
2472
+ version: z16.literal(1)
2473
+ });
2474
+ function cliVersion() {
2475
+ const pkg = createRequire(import.meta.url)("../package.json");
2476
+ return pkg.version;
2477
+ }
2478
+ function cachePath() {
2479
+ return path14.join(homedir2(), ".artifactcafe", "update-check.json");
2480
+ }
2481
+ function readCache() {
2482
+ try {
2483
+ return cacheSchema.parse(JSON.parse(readFileSync5(cachePath(), "utf8")));
2484
+ } catch {
2485
+ return null;
2486
+ }
2487
+ }
2488
+ function writeCache(cache) {
2489
+ try {
2490
+ mkdirSync3(path14.dirname(cachePath()), { mode: 448, recursive: true });
2491
+ writeFileSync3(cachePath(), `${JSON.stringify(cache, null, 2)}
2492
+ `);
2493
+ } catch {
2494
+ }
2495
+ }
2496
+ function isNewerVersion(candidate, current) {
2497
+ const parse = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
2498
+ const a = parse(candidate);
2499
+ const b = parse(current);
2500
+ for (let i = 0; i < 3; i++) {
2501
+ if ((a[i] ?? 0) !== (b[i] ?? 0)) {
2502
+ return (a[i] ?? 0) > (b[i] ?? 0);
2503
+ }
2504
+ }
2505
+ return false;
2506
+ }
2507
+ async function fetchJson(url) {
2508
+ const res = await fetch(url, {
2509
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2510
+ });
2511
+ if (!res.ok) {
2512
+ throw new Error(`GET ${url} \u2192 ${res.status}`);
2513
+ }
2514
+ return res.json();
2515
+ }
2516
+ async function fetchText(url) {
2517
+ const res = await fetch(url, {
2518
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2519
+ });
2520
+ if (!res.ok) {
2521
+ throw new Error(`GET ${url} \u2192 ${res.status}`);
2522
+ }
2523
+ return res.text();
2524
+ }
2525
+ async function fetchLatestVersions(previous) {
2526
+ const [cli, skill] = await Promise.all([
2527
+ fetchJson(REGISTRY_LATEST_URL).then((body) => z16.object({ version: z16.string() }).parse(body).version).catch(() => null),
2528
+ fetchText(SKILL_SOURCE_URL).then((body) => SKILL_VERSION_PATTERN.exec(body)?.[1] ?? null).catch(() => null)
2529
+ ]);
2530
+ return {
2531
+ checkedAt: Date.now(),
2532
+ cliLatest: cli ?? previous?.cliLatest ?? null,
2533
+ skillLatest: skill ?? previous?.skillLatest ?? null,
2534
+ version: 1
2535
+ };
2536
+ }
2537
+ function installedSkillVersions() {
2538
+ const roots = [homedir2(), process.cwd()];
2539
+ const dirs = [".agents", ".claude"];
2540
+ const versions = [];
2541
+ for (const root of roots) {
2542
+ for (const dir of dirs) {
2543
+ const file = path14.join(root, dir, "skills", "artifact-cafe", "SKILL.md");
2544
+ if (!existsSync3(file)) {
2545
+ continue;
2546
+ }
2547
+ try {
2548
+ const found = SKILL_VERSION_PATTERN.exec(readFileSync5(file, "utf8"));
2549
+ if (found?.[1]) {
2550
+ versions.push(found[1]);
2551
+ }
2552
+ } catch {
2553
+ }
2554
+ }
2555
+ }
2556
+ return versions;
2557
+ }
2558
+ function updateChecksDisabled() {
2559
+ return Boolean(
2560
+ process.env.ARTIFACT_CAFE_NO_UPDATE_CHECK ?? process.env.NO_UPDATE_NOTIFIER ?? process.env.CI
2561
+ );
2562
+ }
2563
+ function startUpdateCheck() {
2564
+ if (updateChecksDisabled()) {
2565
+ return () => Promise.resolve();
2566
+ }
2567
+ const pending = (async () => {
2568
+ const cached = readCache();
2569
+ if (cached && Date.now() - cached.checkedAt < CHECK_TTL_MS) {
2570
+ return cached;
2571
+ }
2572
+ const fresh = await fetchLatestVersions(cached);
2573
+ writeCache(fresh);
2574
+ return fresh;
2575
+ })().catch(() => null);
2576
+ return async () => {
2577
+ const latest = await pending;
2578
+ if (!latest) {
2579
+ return;
2580
+ }
2581
+ const notices = [];
2582
+ const current = cliVersion();
2583
+ if (latest.cliLatest && isNewerVersion(latest.cliLatest, current)) {
2584
+ notices.push(
2585
+ `[artifact-cafe] CLI update available: ${current} \u2192 ${latest.cliLatest}. Run commands via \`npx artifact-cafe@latest\`; update a global install with \`npm i -g artifact-cafe@latest\`.`
2586
+ );
2587
+ }
2588
+ if (latest.skillLatest) {
2589
+ const stale = installedSkillVersions().filter(
2590
+ (version) => isNewerVersion(latest.skillLatest, version)
2591
+ ).sort((a, b) => isNewerVersion(a, b) ? 1 : -1);
2592
+ if (stale.length > 0) {
2593
+ notices.push(
2594
+ `[artifact-cafe] Skill update available: ${stale[0]} \u2192 ${latest.skillLatest}. Update: \`${SKILL_UPDATE_COMMAND}\` (drop -g for a repo-local copy), then re-read SKILL.md.`
2595
+ );
2596
+ }
2597
+ }
2598
+ if (notices.length > 0) {
2599
+ console.error(`
2600
+ ${notices.join("\n")}`);
2601
+ }
2602
+ };
2603
+ }
2604
+
1874
2605
  // src/workspaces.ts
1875
2606
  async function workspaces(opts) {
1876
2607
  const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
@@ -2292,6 +3023,247 @@ var protect2 = defineCommand({
2292
3023
  }
2293
3024
  }
2294
3025
  });
3026
+ var threadStatusCommand = (action) => defineCommand({
3027
+ args: {
3028
+ "api-key": {
3029
+ description: "Authorize with an account API key (default: stored login)",
3030
+ type: "string"
3031
+ },
3032
+ artifact: {
3033
+ description: "Act on an artifact ID instead of the linked folder",
3034
+ type: "string"
3035
+ },
3036
+ dir: {
3037
+ default: ".",
3038
+ description: "Linked directory to authorize from (default: .)",
3039
+ type: "string"
3040
+ },
3041
+ json: { description: "Machine-readable output", type: "boolean" },
3042
+ thread: {
3043
+ description: "Thread id (from `comments --json`)",
3044
+ required: true,
3045
+ type: "positional"
3046
+ }
3047
+ },
3048
+ meta: {
3049
+ description: action === "resolve" ? "Mark a comment thread resolved" : "Reopen a resolved comment thread",
3050
+ name: action
3051
+ },
3052
+ async run({ args }) {
3053
+ try {
3054
+ await changeThreadStatus({
3055
+ action,
3056
+ apiKey: args["api-key"],
3057
+ artifact: args.artifact,
3058
+ dir: args.dir,
3059
+ json: Boolean(args.json),
3060
+ thread: args.thread
3061
+ });
3062
+ } catch (err) {
3063
+ printCliError(err, Boolean(args.json));
3064
+ process.exit(1);
3065
+ }
3066
+ }
3067
+ });
3068
+ var resolve = threadStatusCommand("resolve");
3069
+ var reopen = threadStatusCommand("reopen");
3070
+ var reply = defineCommand({
3071
+ args: {
3072
+ "api-key": {
3073
+ description: "Authorize with an account API key (default: stored login)",
3074
+ type: "string"
3075
+ },
3076
+ artifact: {
3077
+ description: "Act on an artifact ID instead of the linked folder",
3078
+ type: "string"
3079
+ },
3080
+ body: {
3081
+ description: "Reply text (omit to read from stdin)",
3082
+ type: "string"
3083
+ },
3084
+ dir: {
3085
+ default: ".",
3086
+ description: "Linked directory to authorize from (default: .)",
3087
+ type: "string"
3088
+ },
3089
+ json: { description: "Machine-readable output", type: "boolean" },
3090
+ resolve: {
3091
+ description: "Resolve the thread after replying",
3092
+ type: "boolean"
3093
+ },
3094
+ thread: {
3095
+ description: "Thread id (from `comments --json`)",
3096
+ required: true,
3097
+ type: "positional"
3098
+ }
3099
+ },
3100
+ meta: {
3101
+ description: "Reply to a comment thread (as the agent or owner)",
3102
+ name: "reply"
3103
+ },
3104
+ async run({ args }) {
3105
+ try {
3106
+ await replyToThread({
3107
+ apiKey: args["api-key"],
3108
+ artifact: args.artifact,
3109
+ body: args.body,
3110
+ dir: args.dir,
3111
+ json: Boolean(args.json),
3112
+ resolve: Boolean(args.resolve),
3113
+ thread: args.thread
3114
+ });
3115
+ } catch (err) {
3116
+ printCliError(err, Boolean(args.json));
3117
+ process.exit(1);
3118
+ }
3119
+ }
3120
+ });
3121
+ var comment2 = defineCommand({
3122
+ args: {
3123
+ "api-key": {
3124
+ description: "Authorize with an account API key (default: stored login)",
3125
+ type: "string"
3126
+ },
3127
+ artifact: {
3128
+ description: "Act on an artifact ID instead of the linked folder",
3129
+ type: "string"
3130
+ },
3131
+ body: {
3132
+ description: "Comment text (omit to read from stdin)",
3133
+ type: "string"
3134
+ },
3135
+ dir: {
3136
+ default: ".",
3137
+ description: "Published/linked directory",
3138
+ type: "positional"
3139
+ },
3140
+ json: { description: "Machine-readable output", type: "boolean" },
3141
+ page: {
3142
+ description: "Scope to one document (e.g. pages/notes.html)",
3143
+ type: "string"
3144
+ },
3145
+ prefix: {
3146
+ description: "Text just before --quote (disambiguates repeats)",
3147
+ type: "string"
3148
+ },
3149
+ quote: {
3150
+ description: "Anchor to this text (omit for a page-level comment)",
3151
+ type: "string"
3152
+ },
3153
+ suffix: {
3154
+ description: "Text just after --quote (disambiguates repeats)",
3155
+ type: "string"
3156
+ },
3157
+ via: {
3158
+ description: 'Attribution label, e.g. "Claude Code"',
3159
+ type: "string"
3160
+ }
3161
+ },
3162
+ meta: {
3163
+ description: "Open a comment thread (as your account)",
3164
+ name: "comment"
3165
+ },
3166
+ async run({ args }) {
3167
+ try {
3168
+ await comment({
3169
+ apiKey: args["api-key"],
3170
+ artifact: args.artifact,
3171
+ body: args.body,
3172
+ dir: args.dir,
3173
+ json: Boolean(args.json),
3174
+ page: args.page,
3175
+ prefix: args.prefix,
3176
+ quote: args.quote,
3177
+ suffix: args.suffix,
3178
+ via: args.via
3179
+ });
3180
+ } catch (err) {
3181
+ printCliError(err, Boolean(args.json));
3182
+ process.exit(1);
3183
+ }
3184
+ }
3185
+ });
3186
+ var edit = defineCommand({
3187
+ args: {
3188
+ "api-key": {
3189
+ description: "Authorize with an account API key (default: stored login)",
3190
+ type: "string"
3191
+ },
3192
+ artifact: {
3193
+ description: "Act on an artifact ID instead of the linked folder",
3194
+ type: "string"
3195
+ },
3196
+ body: {
3197
+ description: "New comment text (omit to read from stdin)",
3198
+ type: "string"
3199
+ },
3200
+ dir: {
3201
+ default: ".",
3202
+ description: "Linked directory to authorize from (default: .)",
3203
+ type: "string"
3204
+ },
3205
+ json: { description: "Machine-readable output", type: "boolean" },
3206
+ message: {
3207
+ description: "Message id (from `comments --json`)",
3208
+ required: true,
3209
+ type: "positional"
3210
+ }
3211
+ },
3212
+ meta: { description: "Edit a comment you authored", name: "edit" },
3213
+ async run({ args }) {
3214
+ try {
3215
+ await editMessage({
3216
+ apiKey: args["api-key"],
3217
+ artifact: args.artifact,
3218
+ body: args.body,
3219
+ dir: args.dir,
3220
+ json: Boolean(args.json),
3221
+ message: args.message
3222
+ });
3223
+ } catch (err) {
3224
+ printCliError(err, Boolean(args.json));
3225
+ process.exit(1);
3226
+ }
3227
+ }
3228
+ });
3229
+ var del = defineCommand({
3230
+ args: {
3231
+ "api-key": {
3232
+ description: "Authorize with an account API key (default: stored login)",
3233
+ type: "string"
3234
+ },
3235
+ artifact: {
3236
+ description: "Act on an artifact ID instead of the linked folder",
3237
+ type: "string"
3238
+ },
3239
+ dir: {
3240
+ default: ".",
3241
+ description: "Linked directory to authorize from (default: .)",
3242
+ type: "string"
3243
+ },
3244
+ json: { description: "Machine-readable output", type: "boolean" },
3245
+ message: {
3246
+ description: "Message id (from `comments --json`)",
3247
+ required: true,
3248
+ type: "positional"
3249
+ }
3250
+ },
3251
+ meta: { description: "Delete a comment you authored", name: "delete" },
3252
+ async run({ args }) {
3253
+ try {
3254
+ await deleteMessage({
3255
+ apiKey: args["api-key"],
3256
+ artifact: args.artifact,
3257
+ dir: args.dir,
3258
+ json: Boolean(args.json),
3259
+ message: args.message
3260
+ });
3261
+ } catch (err) {
3262
+ printCliError(err, Boolean(args.json));
3263
+ process.exit(1);
3264
+ }
3265
+ }
3266
+ });
2295
3267
  var open = defineCommand({
2296
3268
  args: {
2297
3269
  "api-key": {
@@ -2346,7 +3318,8 @@ var dashboard = defineCommand({
2346
3318
  var main = defineCommand({
2347
3319
  meta: {
2348
3320
  description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
2349
- name: "artifact-cafe"
3321
+ name: "artifact-cafe",
3322
+ version: cliVersion()
2350
3323
  },
2351
3324
  // Bare `artifact-cafe` (no subcommand) → brand banner + usage. citty
2352
3325
  // still calls this parent run *after* a matched subcommand, so bail when
@@ -2360,8 +3333,11 @@ var main = defineCommand({
2360
3333
  await showUsage(cmd);
2361
3334
  },
2362
3335
  subCommands: {
3336
+ comment: comment2,
2363
3337
  comments: comments2,
2364
3338
  dashboard,
3339
+ delete: del,
3340
+ edit,
2365
3341
  folders: folders2,
2366
3342
  link: link2,
2367
3343
  list: list2,
@@ -2371,8 +3347,13 @@ var main = defineCommand({
2371
3347
  open,
2372
3348
  protect: protect2,
2373
3349
  publish: publish2,
3350
+ reopen,
3351
+ reply,
3352
+ resolve,
2374
3353
  whoami: whoami2,
2375
3354
  workspaces: workspaces2
2376
3355
  }
2377
3356
  });
2378
- runMain(main);
3357
+ var finishUpdateCheck = startUpdateCheck();
3358
+ await runMain(main);
3359
+ await finishUpdateCheck();