artifact-cafe 0.0.8 → 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 +1049 -433
  2. package/package.json +1 -1
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,107 +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()
90
141
  });
91
- var pointAnchorSchema = z2.object({
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()
168
+ });
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 displayNameSchema = z2.string().trim().min(1).max(64);
116
- var guestSchema = z2.object({
211
+ var displayNameSchema = z3.string().trim().min(1).max(64);
212
+ var guestSchema = z3.object({
117
213
  displayName: displayNameSchema,
118
- guestId: z2.string().min(16)
214
+ guestId: z3.string().min(16)
119
215
  });
120
- var createCommentThreadSchema = z2.object({
216
+ var viaLabelSchema = z3.string().trim().min(1).max(64);
217
+ var createCommentThreadSchema = z3.object({
121
218
  anchor: anchorSchema,
122
- body: z2.string().min(1).max(1e4),
219
+ body: z3.string().min(1).max(1e4),
123
220
  guest: guestSchema.optional(),
124
- versionId: z2.string()
221
+ versionId: z3.string(),
222
+ via: viaLabelSchema.optional()
125
223
  });
126
- var createCommentMessageSchema = z2.object({
127
- body: z2.string().min(1).max(1e4),
224
+ var createCommentMessageSchema = z3.object({
225
+ body: z3.string().min(1).max(1e4),
226
+ guest: guestSchema.optional(),
227
+ via: viaLabelSchema.optional()
228
+ });
229
+ var editCommentMessageSchema = z3.object({
230
+ body: z3.string().min(1).max(1e4),
128
231
  guest: guestSchema.optional()
129
232
  });
130
- var threadActionSchema = z2.object({
233
+ var threadActionSchema = z3.object({
131
234
  guest: guestSchema.optional()
132
235
  });
133
- var commentAuthorSchema = z2.object({
134
- avatarUrl: z2.string().nullable(),
135
- displayName: z2.string(),
136
- guestId: z2.string().nullable(),
137
- isViewer: z2.boolean(),
138
- 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()
139
250
  });
140
- 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({
141
266
  author: commentAuthorSchema,
142
- body: z2.string(),
143
- createdAt: z2.string(),
144
- id: z2.string()
145
- });
146
- 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({
147
283
  anchor: anchorSchema,
148
- artifactId: z2.string(),
149
- createdAt: z2.string(),
284
+ artifactId: z3.string(),
285
+ createdAt: z3.string(),
150
286
  createdBy: commentAuthorSchema,
151
- id: z2.string(),
152
- messages: z2.array(commentMessageSchema),
153
- status: z2.enum(["open", "resolved"]),
154
- versionId: z2.string()
155
- });
156
- var commentThreadListSchema = z2.object({
157
- threads: z2.array(commentThreadSchema)
158
- });
159
- var commentExportQuerySchema = z2.object({
160
- status: z2.enum(["open", "resolved", "all"]).default("open"),
161
- version: z2.enum(["current", "all"]).default("current")
162
- });
163
- var commentExportReplySchema = z2.object({
164
- author: z2.string(),
165
- authorKind: z2.enum(["member", "guest"]),
166
- body: z2.string(),
167
- createdAt: z2.string()
168
- });
169
- 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({
170
316
  anchor: anchorSchema,
171
- author: z2.string(),
172
- authorKind: z2.enum(["member", "guest"]),
173
- body: z2.string(),
174
- createdAt: z2.string(),
175
- id: z2.string(),
176
- replies: z2.array(commentExportReplySchema),
177
- status: z2.enum(["open", "resolved"]),
178
- versionNumber: z2.number().int().positive()
179
- });
180
- var commentExportSchema = z2.object({
181
- artifactId: z2.string(),
182
- 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),
183
336
  // The artifact's current version (null before the first finalize).
184
- versionNumber: z2.number().int().positive().nullable()
337
+ versionNumber: z3.number().int().positive().nullable()
338
+ });
339
+ var commentReadResponseSchema = z3.object({
340
+ lastSeenAt: z3.string()
185
341
  });
186
342
  function elementLabel(el) {
187
343
  if (el.id) {
@@ -192,147 +348,163 @@ function elementLabel(el) {
192
348
  }
193
349
 
194
350
  // ../shared/src/errors.ts
195
- import { z as z3 } from "zod";
196
- var apiErrorSchema = z3.object({
197
- code: z3.string(),
198
- docsUrl: z3.string().url(),
199
- message: z3.string(),
200
- 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)
201
367
  });
202
368
 
203
369
  // ../shared/src/me.ts
204
- import { z as z4 } from "zod";
205
- var ownArtifactSchema = z4.object({
206
- createdAt: z4.string(),
207
- 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(),
208
374
  // Owned artifacts normally never expire; non-null only while an
209
375
  // expiry is still set (e.g. an unclaimed publish surfaced here later).
210
- expiresAt: z4.string().nullable(),
376
+ expiresAt: z6.string().nullable(),
211
377
  // SPEC-2 §8.4 — the containing folder for workspace-owned artifacts;
212
378
  // both null for personal artifacts. `folderId` drives the dashboard's
213
379
  // move control and folder filter; `folderName` is the display label.
214
- folderId: z4.string().nullable(),
215
- folderName: z4.string().nullable(),
380
+ folderId: z6.string().nullable(),
381
+ folderName: z6.string().nullable(),
216
382
  // Whether a publish token is still active — revocable from the
217
383
  // dashboard (§6.18); the token itself is never returned.
218
- hasPublishToken: z4.boolean(),
219
- id: z4.string(),
220
- openThreads: z4.number(),
221
- protected: z4.boolean(),
222
- slug: z4.string(),
223
- title: z4.string(),
224
- totalViews: z4.number(),
225
- updatedAt: z4.string(),
226
- url: z4.string(),
227
- versionCount: z4.number()
228
- });
229
- var ownArtifactsResponseSchema = z4.object({
230
- artifacts: z4.array(ownArtifactSchema)
231
- });
232
- var revokePublishTokenResponseSchema = z4.object({
233
- 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()
234
403
  });
235
404
 
236
405
  // ../shared/src/owner.ts
237
- import { z as z6 } from "zod";
406
+ import { z as z8 } from "zod";
238
407
 
239
408
  // ../shared/src/workspaces.ts
240
- import { z as z5 } from "zod";
409
+ import { z as z7 } from "zod";
241
410
  var WORKSPACE_HANDLE_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])$/;
242
- var workspaceRoleSchema = z5.enum(["owner", "admin", "member"]);
243
- var workspaceInviteRoleSchema = z5.enum(["admin", "member"]);
244
- var workspaceHandleSchema = z5.string().min(3).max(32).regex(WORKSPACE_HANDLE_PATTERN, {
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, {
245
414
  message: "handle must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
246
415
  });
247
- var createWorkspaceSchema = z5.object({
416
+ var createWorkspaceSchema = z7.object({
248
417
  handle: workspaceHandleSchema.optional(),
249
- name: z5.string().trim().min(1).max(100)
418
+ name: z7.string().trim().min(1).max(100)
250
419
  });
251
- var updateWorkspaceSchema = z5.object({
420
+ var updateWorkspaceSchema = z7.object({
252
421
  handle: workspaceHandleSchema.optional(),
253
- name: z5.string().trim().min(1).max(100).optional()
422
+ name: z7.string().trim().min(1).max(100).optional()
254
423
  }).refine((body) => body.handle !== void 0 || body.name !== void 0, {
255
424
  message: "provide at least one of name or handle"
256
425
  });
257
- var workspaceSchema = z5.object({
258
- createdAt: z5.string(),
259
- handle: z5.string(),
260
- id: z5.string(),
261
- 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(),
262
434
  role: workspaceRoleSchema.nullable(),
263
- updatedAt: z5.string()
435
+ updatedAt: z7.string()
264
436
  });
265
- var workspaceListResponseSchema = z5.object({
266
- workspaces: z5.array(workspaceSchema)
437
+ var workspaceListResponseSchema = z7.object({
438
+ workspaces: z7.array(workspaceSchema)
267
439
  });
268
- var deleteWorkspaceResponseSchema = z5.object({
269
- deleted: z5.literal(true)
440
+ var deleteWorkspaceResponseSchema = z7.object({
441
+ deleted: z7.literal(true)
270
442
  });
271
- var workspaceMemberSchema = z5.object({
272
- createdAt: z5.string(),
273
- email: z5.string().email(),
274
- image: z5.string().nullable(),
275
- 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(),
276
448
  role: workspaceRoleSchema,
277
- userId: z5.string()
449
+ userId: z7.string()
278
450
  });
279
- var workspaceMemberListResponseSchema = z5.object({
280
- members: z5.array(workspaceMemberSchema)
451
+ var workspaceMemberListResponseSchema = z7.object({
452
+ members: z7.array(workspaceMemberSchema)
281
453
  });
282
- var updateWorkspaceMemberSchema = z5.object({
454
+ var updateWorkspaceMemberSchema = z7.object({
283
455
  role: workspaceRoleSchema
284
456
  });
285
- var removeWorkspaceMemberResponseSchema = z5.object({
286
- removed: z5.literal(true)
457
+ var removeWorkspaceMemberResponseSchema = z7.object({
458
+ removed: z7.literal(true)
287
459
  });
288
- var createWorkspaceInviteSchema = z5.object({
289
- email: z5.string().trim().toLowerCase().email().optional(),
290
- expiresAt: z5.string().datetime(),
291
- maxUses: z5.number().int().positive().optional(),
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(),
292
464
  role: workspaceInviteRoleSchema
293
465
  });
294
- var workspaceInviteSchema = z5.object({
295
- createdAt: z5.string(),
296
- email: z5.string().email().nullable(),
297
- expiresAt: z5.string(),
298
- id: z5.string(),
299
- maxUses: z5.number().int().positive().nullable(),
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(),
300
472
  role: workspaceInviteRoleSchema,
301
- tokenPrefix: z5.string(),
302
- useCount: z5.number().int().nonnegative()
473
+ tokenPrefix: z7.string(),
474
+ useCount: z7.number().int().nonnegative()
303
475
  });
304
476
  var createdWorkspaceInviteSchema = workspaceInviteSchema.extend({
305
- url: z5.string().url()
477
+ url: z7.string().url()
306
478
  });
307
- var workspaceInviteListResponseSchema = z5.object({
308
- invites: z5.array(workspaceInviteSchema)
479
+ var workspaceInviteListResponseSchema = z7.object({
480
+ invites: z7.array(workspaceInviteSchema)
309
481
  });
310
- var revokeWorkspaceInviteResponseSchema = z5.object({
311
- revoked: z5.literal(true)
482
+ var revokeWorkspaceInviteResponseSchema = z7.object({
483
+ revoked: z7.literal(true)
312
484
  });
313
- var acceptWorkspaceInviteSchema = z5.object({
314
- token: z5.string().min(1)
485
+ var acceptWorkspaceInviteSchema = z7.object({
486
+ token: z7.string().min(1)
315
487
  });
316
- var acceptWorkspaceInviteResponseSchema = z5.object({
488
+ var acceptWorkspaceInviteResponseSchema = z7.object({
317
489
  role: workspaceRoleSchema,
318
490
  workspace: workspaceSchema
319
491
  });
320
- var folderVisibilitySchema = z5.enum(["public", "workspace"]);
321
- var artifactVisibilitySchema = z5.enum([
492
+ var folderVisibilitySchema = z7.enum(["public", "workspace"]);
493
+ var artifactVisibilitySchema = z7.enum([
322
494
  "public",
323
495
  "unlisted",
324
496
  "workspace"
325
497
  ]);
326
- var createFolderSchema = z5.object({
327
- name: z5.string().trim().min(1).max(100),
498
+ var createFolderSchema = z7.object({
499
+ name: z7.string().trim().min(1).max(100),
328
500
  visibility: folderVisibilitySchema
329
501
  });
330
- var updateFolderSchema = z5.object({
502
+ var updateFolderSchema = z7.object({
331
503
  // With `visibility`: bulk-update the folder's artifacts to match
332
504
  // (SPEC-2 §3.4 "Changing a folder's visibility offers a bulk update").
333
- cascade: z5.boolean().optional(),
334
- name: z5.string().trim().min(1).max(100).optional(),
335
- sortOrder: z5.number().int().min(0).optional(),
505
+ cascade: z7.boolean().optional(),
506
+ name: z7.string().trim().min(1).max(100).optional(),
507
+ sortOrder: z7.number().int().min(0).optional(),
336
508
  visibility: folderVisibilitySchema.optional()
337
509
  }).refine(
338
510
  (body) => body.name !== void 0 || body.visibility !== void 0 || body.sortOrder !== void 0,
@@ -340,248 +512,168 @@ var updateFolderSchema = z5.object({
340
512
  ).refine((body) => !body.cascade || body.visibility !== void 0, {
341
513
  message: "cascade requires visibility"
342
514
  });
343
- var folderSchema = z5.object({
344
- createdAt: z5.string(),
345
- id: z5.string(),
346
- name: z5.string(),
515
+ var folderSchema = z7.object({
516
+ createdAt: z7.string(),
517
+ id: z7.string(),
518
+ name: z7.string(),
347
519
  // Permalink segment for the public folder page (/w/{handle}/f/{slug}).
348
520
  // Server-derived from the name; renames regenerate it.
349
- slug: z5.string(),
350
- sortOrder: z5.number().int(),
351
- updatedAt: z5.string(),
521
+ slug: z7.string(),
522
+ sortOrder: z7.number().int(),
523
+ updatedAt: z7.string(),
352
524
  visibility: folderVisibilitySchema,
353
- workspaceId: z5.string()
354
- });
355
- var folderListResponseSchema = z5.object({
356
- folders: z5.array(folderSchema)
357
- });
358
- var deleteFolderResponseSchema = z5.object({
359
- deleted: z5.literal(true)
360
- });
361
- var workspaceArtifactSchema = z5.object({
362
- createdAt: z5.string(),
363
- currentVersionNumber: z5.number().int().nullable(),
364
- folderId: z5.string().nullable(),
365
- id: z5.string(),
366
- openCommentCount: z5.number().int().nonnegative(),
367
- slug: z5.string(),
368
- title: z5.string(),
369
- updatedAt: z5.string(),
370
- url: z5.string().url(),
371
- 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(),
372
544
  visibility: artifactVisibilitySchema
373
545
  });
374
- var workspaceArtifactListResponseSchema = z5.object({
375
- artifacts: z5.array(workspaceArtifactSchema)
546
+ var workspaceArtifactListResponseSchema = z7.object({
547
+ artifacts: z7.array(workspaceArtifactSchema)
376
548
  });
377
549
 
378
550
  // ../shared/src/owner.ts
379
551
  var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$/;
380
- var claimResponseSchema = z6.object({
381
- artifactId: z6.string(),
382
- slug: z6.string(),
383
- title: z6.string(),
384
- url: z6.string().url()
552
+ var claimResponseSchema = z8.object({
553
+ artifactId: z8.string(),
554
+ slug: z8.string(),
555
+ title: z8.string(),
556
+ url: z8.string().url()
385
557
  });
386
- var updateArtifactSchema = z6.object({
558
+ var updateArtifactSchema = z8.object({
387
559
  // SPEC-2 §6.4 — placement + visibility. `folderId: null` clears the
388
560
  // folder; `workspaceId: null` moves the artifact out of its workspace
389
561
  // (§5.5 ownership transfer).
390
- folderId: z6.string().min(1).nullable().optional(),
391
- 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, {
392
564
  message: "slug must be lowercase letters, digits, and hyphens, with no leading/trailing hyphen"
393
565
  }).optional(),
394
- title: z6.string().min(1).max(200).optional(),
566
+ title: z8.string().min(1).max(200).optional(),
395
567
  visibility: artifactVisibilitySchema.optional(),
396
- workspaceId: z6.string().min(1).nullable().optional()
568
+ workspaceId: z8.string().min(1).nullable().optional()
397
569
  }).refine(
398
570
  (body) => body.title !== void 0 || body.slug !== void 0 || body.visibility !== void 0 || body.folderId !== void 0 || body.workspaceId !== void 0,
399
571
  {
400
572
  message: "provide at least one of title, slug, visibility, folderId, workspaceId"
401
573
  }
402
574
  );
403
- var deleteArtifactResponseSchema = z6.object({
404
- deleted: z6.literal(true)
575
+ var deleteArtifactResponseSchema = z8.object({
576
+ deleted: z8.literal(true)
405
577
  });
406
578
 
407
579
  // ../shared/src/presence.ts
408
- import { z as z7 } from "zod";
580
+ import { z as z9 } from "zod";
409
581
  var AGENT_PRESENCE = {
410
582
  heartbeatSeconds: 15,
411
583
  shellPollSeconds: 15,
412
584
  ttlSeconds: 45
413
585
  };
414
- var agentHeartbeatResponseSchema = z7.object({
415
- listening: z7.literal(true),
416
- ttlSeconds: z7.number().int().positive()
586
+ var agentHeartbeatResponseSchema = z9.object({
587
+ listening: z9.literal(true),
588
+ ttlSeconds: z9.number().int().positive()
417
589
  });
418
- var agentPresenceSchema = z7.object({
419
- listening: z7.boolean(),
420
- versionNumber: z7.number().int().positive().nullable()
590
+ var agentPresenceSchema = z9.object({
591
+ listening: z9.boolean(),
592
+ versionNumber: z9.number().int().positive().nullable()
421
593
  });
422
594
 
423
595
  // ../shared/src/protection.ts
424
- import { z as z8 } from "zod";
596
+ import { z as z10 } from "zod";
425
597
  var PASSWORD_MIN = 6;
426
598
  var PASSWORD_MAX = 128;
427
- var gatePasswordSchema = z8.string().min(PASSWORD_MIN, `Password must be at least ${PASSWORD_MIN} characters.`).max(PASSWORD_MAX, `Password must be at most ${PASSWORD_MAX} characters.`);
428
- var setProtectionSchema = z8.object({
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({
429
601
  password: gatePasswordSchema.nullable()
430
602
  });
431
- var protectionResponseSchema = z8.object({
432
- protected: z8.boolean()
433
- });
434
- var unlockSchema = z8.object({
435
- password: z8.string().min(1).max(1024)
436
- });
437
- var unlockResponseSchema = z8.object({
438
- unlocked: z8.literal(true)
603
+ var protectionResponseSchema = z10.object({
604
+ protected: z10.boolean()
439
605
  });
440
-
441
- // ../shared/src/publish.ts
442
- import { z as z9 } from "zod";
443
- function isSafeArtifactPath(p) {
444
- if (p.length === 0 || p.length > 1024) {
445
- return false;
446
- }
447
- if (p.includes("\\") || p.includes("\0")) {
448
- return false;
449
- }
450
- if (p.startsWith("/") || p.startsWith("~")) {
451
- return false;
452
- }
453
- return p.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
454
- }
455
- var manifestFileSchema = z9.object({
456
- hash: z9.string().regex(/^sha256_[a-f0-9]{64}$/),
457
- mimeType: z9.string().regex(/^[-\w.+]+\/[-\w.+]+$/, { message: "invalid MIME type" }),
458
- path: z9.string().min(1).max(1024).refine(isSafeArtifactPath, {
459
- message: "path must be a safe relative path"
460
- }),
461
- size: z9.number().int().positive()
462
- });
463
- var publishSourceSchema = z9.object({
464
- agent: z9.string().optional(),
465
- runId: z9.string().optional(),
466
- type: z9.literal("cli")
606
+ var unlockSchema = z10.object({
607
+ password: z10.string().min(1).max(1024)
467
608
  });
468
- var createPublishSessionSchema = z9.object({
469
- artifactId: z9.string().nullable(),
470
- entryPath: z9.string().optional(),
471
- files: z9.array(manifestFileSchema).min(1).max(1e3),
472
- // SPEC-2 §6.4 — workspace placement is available only to authenticated
473
- // members; the API enforces membership and folder ownership.
474
- folderId: z9.string().min(1).optional(),
475
- source: publishSourceSchema,
476
- title: z9.string().max(200).optional(),
477
- workspaceId: z9.string().min(1).optional()
478
- });
479
- var publishSessionResponseSchema = z9.object({
480
- artifactId: z9.string(),
481
- expiresAt: z9.string(),
482
- filesToUpload: z9.array(
483
- z9.object({
484
- method: z9.literal("PUT"),
485
- path: z9.string(),
486
- uploadUrl: z9.string().url()
487
- })
488
- ),
489
- publishSessionId: z9.string(),
490
- skippedFiles: z9.array(z9.string()),
491
- versionId: z9.string()
492
- });
493
- var finalizeResponseSchema = z9.object({
494
- artifactId: z9.string(),
495
- // API-key publishes create the artifact already claimed (SPEC §5.4) —
496
- // the CLI uses this to tell "claim URL pending" from "already yours".
497
- claimed: z9.boolean(),
498
- // Returned only on first publish of a new artifact (one-time).
499
- claimUrl: z9.string().url().optional(),
500
- publishToken: z9.string().optional(),
501
- url: z9.string().url(),
502
- versionId: z9.string(),
503
- versionNumber: z9.number().int().positive()
504
- });
505
- var artifactResponseSchema = z9.object({
506
- claimed: z9.boolean(),
507
- currentVersion: z9.object({
508
- entryUrl: z9.string().url(),
509
- id: z9.string(),
510
- versionNumber: z9.number().int().positive()
511
- }).nullable(),
512
- // SPEC-2 §6.4 — placement + visibility surface on the artifact.
513
- folderId: z9.string().nullable(),
514
- id: z9.string(),
515
- slug: z9.string(),
516
- title: z9.string(),
517
- visibility: z9.enum(["public", "unlisted", "workspace"]),
518
- workspaceId: z9.string().nullable()
609
+ var unlockResponseSchema = z10.object({
610
+ unlocked: z10.literal(true)
519
611
  });
520
612
 
521
613
  // ../shared/src/report.ts
522
- import { z as z10 } from "zod";
523
- var reportRequestSchema = z10.object({
524
- 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()
525
617
  });
526
- var reportResponseSchema = z10.object({
527
- reported: z10.literal(true)
618
+ var reportResponseSchema = z11.object({
619
+ reported: z11.literal(true)
528
620
  });
529
621
 
530
622
  // ../shared/src/stats.ts
531
- import { z as z11 } from "zod";
532
- var avgViewSecondsSchema = z11.number().int().nonnegative().nullable();
533
- var versionViewStatsSchema = z11.object({
623
+ import { z as z12 } from "zod";
624
+ var avgViewSecondsSchema = z12.number().int().nonnegative().nullable();
625
+ var versionViewStatsSchema = z12.object({
534
626
  avgViewSeconds: avgViewSecondsSchema,
535
627
  // Views from inside the review shell's iframe vs. direct visits to
536
628
  // the render URL. views = embeddedViews + directViews.
537
- directViews: z11.number().int().nonnegative(),
538
- embeddedViews: z11.number().int().nonnegative(),
629
+ directViews: z12.number().int().nonnegative(),
630
+ embeddedViews: z12.number().int().nonnegative(),
539
631
  // Distinct hashed viewer IPs across the version's lifetime.
540
- uniqueViewers: z11.number().int().nonnegative(),
541
- versionNumber: z11.number().int().positive(),
542
- views: z11.number().int().nonnegative()
632
+ uniqueViewers: z12.number().int().nonnegative(),
633
+ versionNumber: z12.number().int().positive(),
634
+ views: z12.number().int().nonnegative()
543
635
  });
544
- var dailyViewsSchema = z11.object({
636
+ var dailyViewsSchema = z12.object({
545
637
  avgViewSeconds: avgViewSecondsSchema,
546
638
  // UTC day, YYYY-MM-DD.
547
- day: z11.string(),
548
- uniqueViewers: z11.number().int().nonnegative(),
549
- views: z11.number().int().nonnegative()
639
+ day: z12.string(),
640
+ uniqueViewers: z12.number().int().nonnegative(),
641
+ views: z12.number().int().nonnegative()
550
642
  });
551
- var artifactStatsResponseSchema = z11.object({
643
+ var artifactStatsResponseSchema = z12.object({
552
644
  avgViewSeconds: avgViewSecondsSchema,
553
- daily: z11.array(dailyViewsSchema),
645
+ daily: z12.array(dailyViewsSchema),
554
646
  // Distinct hashed viewer IPs across all versions and days.
555
- totalUniqueViewers: z11.number().int().nonnegative(),
556
- totalViews: z11.number().int().nonnegative(),
557
- versions: z11.array(versionViewStatsSchema)
647
+ totalUniqueViewers: z12.number().int().nonnegative(),
648
+ totalViews: z12.number().int().nonnegative(),
649
+ versions: z12.array(versionViewStatsSchema)
558
650
  });
559
651
 
560
652
  // ../shared/src/versions.ts
561
- import { z as z12 } from "zod";
562
- var versionSourceSchema = z12.object({
563
- agentName: z12.string().nullable(),
564
- type: z12.string()
653
+ import { z as z13 } from "zod";
654
+ var versionSourceSchema = z13.object({
655
+ agentName: z13.string().nullable(),
656
+ type: z13.string()
565
657
  });
566
- var versionChangesSchema = z12.object({
567
- added: z12.array(z12.string()),
568
- changed: z12.array(z12.string()),
569
- 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())
570
662
  });
571
- var versionSummarySchema = z12.object({
663
+ var versionSummarySchema = z13.object({
572
664
  // Absent for the first version — there is no previous version to diff against.
573
665
  changes: versionChangesSchema.nullable(),
574
- createdAt: z12.string(),
575
- entryUrl: z12.string().url(),
576
- fileCount: z12.number().int().nonnegative(),
577
- id: z12.string(),
578
- 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(),
579
671
  source: versionSourceSchema,
580
- totalSizeBytes: z12.number().int().nonnegative(),
581
- versionNumber: z12.number().int().positive()
672
+ totalSizeBytes: z13.number().int().nonnegative(),
673
+ versionNumber: z13.number().int().positive()
582
674
  });
583
- var versionListResponseSchema = z12.object({
584
- versions: z12.array(versionSummarySchema)
675
+ var versionListResponseSchema = z13.object({
676
+ versions: z13.array(versionSummarySchema)
585
677
  });
586
678
 
587
679
  // src/api.ts
@@ -651,6 +743,36 @@ function createApiClient(apiUrl2, publishToken) {
651
743
  (d) => publishSessionResponseSchema.parse(d)
652
744
  );
653
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
+ },
654
776
  async exportComments(artifactId, query) {
655
777
  const params = new URLSearchParams({
656
778
  status: query.status,
@@ -772,17 +894,17 @@ import {
772
894
  writeFileSync as writeFileSync2
773
895
  } from "fs";
774
896
  import path2 from "path";
775
- import { z as z14 } from "zod";
897
+ import { z as z15 } from "zod";
776
898
 
777
899
  // src/credentials.ts
778
900
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
779
901
  import { homedir } from "os";
780
902
  import path from "path";
781
- import { z as z13 } from "zod";
903
+ import { z as z14 } from "zod";
782
904
  var DEFAULT_API_URL = "https://artifact.cafe";
783
- var credentialsSchema = z13.object({
784
- keys: z13.record(z13.string(), z13.string()),
785
- version: z13.literal(1)
905
+ var credentialsSchema = z14.object({
906
+ keys: z14.record(z14.string(), z14.string()),
907
+ version: z14.literal(1)
786
908
  });
787
909
  function credentialsPath() {
788
910
  return path.join(homedir(), ".artifactcafe", "credentials");
@@ -832,22 +954,28 @@ function resolveApiKey(apiUrl2, flag) {
832
954
  }
833
955
 
834
956
  // src/config.ts
835
- var configSchema = z14.object({
836
- apiUrl: z14.string(),
837
- artifactId: z14.string(),
838
- folder: z14.string().optional(),
839
- 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(),
840
962
  // Optional: a folder linked by `link` to an artifact you own carries no
841
963
  // token — owner actions authorize via your stored login instead. A first
842
964
  // publish still writes the anonymous-mode token here.
843
- publishToken: z14.string().optional(),
844
- slug: z14.string(),
845
- url: z14.string(),
846
- workspace: z14.string().optional(),
847
- workspaceId: z14.string().optional()
848
- });
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();
849
976
  var CONFIG_DIR = ".artifactcafe";
850
977
  var CONFIG_FILE = "config.json";
978
+ var PROJECT_CONFIG_FILE = "artifact-cafe.json";
851
979
  function readConfig(dir) {
852
980
  const file = path2.join(dir, CONFIG_DIR, CONFIG_FILE);
853
981
  if (!existsSync2(file)) {
@@ -869,6 +997,48 @@ function writeConfig(dir, config) {
869
997
  );
870
998
  ensureGitignored(dir);
871
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
+ }
872
1042
  function ensureGitignored(dir) {
873
1043
  const gitignore = path2.join(dir, ".gitignore");
874
1044
  if (!existsSync2(gitignore)) {
@@ -907,7 +1077,107 @@ function resolveArtifactTarget(dir, opts = {}) {
907
1077
  return { apiUrl: apiUrl2, artifactId, credential, url };
908
1078
  }
909
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
+
910
1179
  // src/comments.ts
1180
+ import path4 from "path";
911
1181
  var STATUSES = ["open", "resolved", "all"];
912
1182
  var VERSIONS = ["current", "all"];
913
1183
  var pollSeconds = () => Number(process.env.AC_WAIT_POLL_SECONDS ?? 5);
@@ -926,7 +1196,7 @@ async function comments(opts) {
926
1196
  }
927
1197
  const status = opts.status;
928
1198
  const version = opts.version;
929
- const dir = path3.resolve(opts.dir);
1199
+ const dir = path4.resolve(opts.dir);
930
1200
  const target = resolveArtifactTarget(dir, {
931
1201
  apiKey: opts.apiKey,
932
1202
  artifact: opts.artifact
@@ -1085,21 +1355,30 @@ function printHuman(data, url, status, version) {
1085
1355
  function printThread(t) {
1086
1356
  const marker = t.status === "resolved" ? "\u2713" : "\u25CF";
1087
1357
  const date = t.createdAt.slice(0, 10);
1358
+ const edited = t.editedAt && !t.deleted ? " \xB7 edited" : "";
1088
1359
  console.log(
1089
- `${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}`
1090
1361
  );
1091
1362
  if (t.anchor.type === "text") {
1092
1363
  console.log(` \u21B3 "${t.anchor.quote}"`);
1093
- } else if (t.anchor.element) {
1364
+ } else if (t.anchor.type === "point") {
1094
1365
  const el = t.anchor.element;
1095
- const text = el.text ? ` "${el.text}"` : "";
1096
- 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
+ }
1097
1374
  } else {
1098
- console.log(` \u21B3 at ${t.anchor.elementPath}`);
1375
+ console.log(t.anchor.page ? ` \u21B3 on ${t.anchor.page}` : " \u21B3 page comment");
1099
1376
  }
1100
- console.log(indent(t.body, " "));
1377
+ console.log(indent(t.deleted ? "[deleted]" : t.body, " "));
1101
1378
  for (const reply2 of t.replies) {
1102
- console.log(indent(`${reply2.author}: ${reply2.body}`, " "));
1379
+ const replyEdited = reply2.editedAt && !reply2.deleted ? " (edited)" : "";
1380
+ const replyBody = reply2.deleted ? "[deleted]" : reply2.body;
1381
+ console.log(indent(`${reply2.author}: ${replyBody}${replyEdited}`, " "));
1103
1382
  }
1104
1383
  console.log("");
1105
1384
  }
@@ -1108,19 +1387,20 @@ function indent(text, pad) {
1108
1387
  }
1109
1388
 
1110
1389
  // src/dashboard.ts
1111
- import path6 from "path";
1390
+ import path7 from "path";
1112
1391
 
1113
1392
  // src/publish.ts
1114
1393
  import { exec } from "child_process";
1115
1394
  import { readFileSync as readFileSync4 } from "fs";
1116
- import path5 from "path";
1395
+ import path6 from "path";
1396
+ import { createInterface } from "readline";
1117
1397
 
1118
1398
  // src/manifest.ts
1119
1399
  import { createHash } from "crypto";
1120
1400
  import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
1121
- import path4 from "path";
1401
+ import path5 from "path";
1122
1402
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".artifactcafe"]);
1123
- var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
1403
+ var IGNORED_FILES = /* @__PURE__ */ new Set([".DS_Store", "artifact-cafe.json"]);
1124
1404
  function isIgnoredFile(name) {
1125
1405
  if (IGNORED_FILES.has(name)) {
1126
1406
  return true;
@@ -1167,7 +1447,7 @@ var MIME_TYPES = {
1167
1447
  ".xml": "application/xml"
1168
1448
  };
1169
1449
  function mimeFor(filePath) {
1170
- return MIME_TYPES[path4.extname(filePath).toLowerCase()] ?? "application/octet-stream";
1450
+ return MIME_TYPES[path5.extname(filePath).toLowerCase()] ?? "application/octet-stream";
1171
1451
  }
1172
1452
  function scanDirectory(dir) {
1173
1453
  const files = [];
@@ -1175,7 +1455,7 @@ function scanDirectory(dir) {
1175
1455
  function walk(current, relPrefix) {
1176
1456
  for (const entry of readdirSync(current, { withFileTypes: true })) {
1177
1457
  const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
1178
- const abs = path4.join(current, entry.name);
1458
+ const abs = path5.join(current, entry.name);
1179
1459
  if (entry.isSymbolicLink()) {
1180
1460
  skipped.push(rel);
1181
1461
  continue;
@@ -1241,7 +1521,7 @@ function resolveEntry(files, explicit) {
1241
1521
  // src/publish.ts
1242
1522
  var UPLOAD_CONCURRENCY = 6;
1243
1523
  async function publish(opts) {
1244
- const dir = path5.resolve(opts.dir);
1524
+ const dir = path6.resolve(opts.dir);
1245
1525
  const config = readConfig(dir);
1246
1526
  if (opts.password !== void 0) {
1247
1527
  const parsed = gatePasswordSchema.safeParse(opts.password);
@@ -1253,7 +1533,13 @@ async function publish(opts) {
1253
1533
  const token = (artifactId !== null && artifactId === config?.artifactId ? config.publishToken : void 0) ?? process.env.ARTIFACT_CAFE_TOKEN;
1254
1534
  const apiUrl2 = process.env.ARTIFACT_CAFE_URL ?? config?.apiUrl ?? DEFAULT_API_URL;
1255
1535
  const apiKey = resolveApiKey(apiUrl2, opts.apiKey);
1256
- 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
+ });
1257
1543
  const { files, skipped } = scanDirectory(dir);
1258
1544
  if (files.length === 0) {
1259
1545
  throw new Error(`Nothing to publish in ${dir} (after ignore rules).`);
@@ -1278,7 +1564,7 @@ async function publish(opts) {
1278
1564
  type: "cli"
1279
1565
  },
1280
1566
  title: opts.title,
1281
- ...placement.workspaceId ? { workspaceId: placement.workspaceId } : {}
1567
+ ...placement.publishWorkspaceId ? { workspaceId: placement.publishWorkspaceId } : {}
1282
1568
  };
1283
1569
  let credential = apiKey ?? token;
1284
1570
  let client = createApiClient(apiUrl2, credential);
@@ -1310,8 +1596,9 @@ async function publish(opts) {
1310
1596
  );
1311
1597
  });
1312
1598
  const result = await client.finalize(session.publishSessionId);
1599
+ const rememberedProjectFile = placement.rememberProjectDefault ? writeProjectConfig(dir, placement.rememberProjectDefault) : null;
1313
1600
  const existingToken = config?.artifactId === result.artifactId ? config.publishToken : void 0;
1314
- const shouldWriteConfig = result.publishToken !== void 0 || config?.artifactId === result.artifactId || opts.workspace !== void 0 || opts.folder !== void 0;
1601
+ const shouldWriteConfig = result.publishToken !== void 0 || config?.artifactId === result.artifactId || opts.workspace !== void 0 || opts.folder !== void 0 || placement.workspaceId !== void 0;
1315
1602
  if (shouldWriteConfig) {
1316
1603
  writeConfig(dir, {
1317
1604
  apiUrl: apiUrl2,
@@ -1322,7 +1609,8 @@ async function publish(opts) {
1322
1609
  slug: slugFromUrl(result.url),
1323
1610
  url: result.url,
1324
1611
  ...placement.workspace ? { workspace: placement.workspace } : {},
1325
- ...placement.workspaceId ? { workspaceId: placement.workspaceId } : {}
1612
+ ...placement.workspaceId ? { workspaceId: placement.workspaceId } : {},
1613
+ ...placement.destination === "personal" || placement.destination === "workspace" ? { workspaceKind: placement.destination } : {}
1326
1614
  });
1327
1615
  }
1328
1616
  if (opts.password !== void 0) {
@@ -1338,6 +1626,8 @@ async function publish(opts) {
1338
1626
  {
1339
1627
  artifactId: result.artifactId,
1340
1628
  claimed: result.claimed,
1629
+ destination: placement.destination,
1630
+ destinationSource: placement.destinationSource,
1341
1631
  folder: placement.folder ?? null,
1342
1632
  folderId: placement.folderId ?? null,
1343
1633
  url: result.url,
@@ -1356,6 +1646,12 @@ async function publish(opts) {
1356
1646
  );
1357
1647
  } else if (result.versionNumber === 1) {
1358
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
+ }
1359
1655
  console.log(`Review URL:
1360
1656
  ${result.url}
1361
1657
  `);
@@ -1375,6 +1671,12 @@ ${result.claimUrl}
1375
1671
  console.log(`
1376
1672
  \u2713 Published v${result.versionNumber}
1377
1673
  `);
1674
+ console.log(`Destination: ${placement.destinationLabel}
1675
+ `);
1676
+ if (rememberedProjectFile) {
1677
+ console.log(`Project default: ${rememberedProjectFile}
1678
+ `);
1679
+ }
1378
1680
  console.log(`${result.url}
1379
1681
  `);
1380
1682
  if (opts.password !== void 0) {
@@ -1386,75 +1688,181 @@ ${result.claimUrl}
1386
1688
  }
1387
1689
  }
1388
1690
  async function resolvePlacement(params) {
1389
- const { apiKey, apiUrl: apiUrl2, config, opts } = params;
1691
+ const { apiKey, apiUrl: apiUrl2, config, isNewArtifact, opts } = params;
1390
1692
  const hasPlacementFlag = opts.workspace !== void 0 || opts.folder !== void 0;
1391
1693
  if (!hasPlacementFlag && config?.workspaceId) {
1392
- if (!apiKey) {
1694
+ const isPersonal = config.workspaceKind === "personal";
1695
+ if (!(apiKey || isPersonal)) {
1393
1696
  throw new Error(
1394
1697
  `Publishing into workspace ${config.workspace ?? config.workspaceId} needs an account \u2014 run \`artifact-cafe login\` first (no API key for ${apiOrigin(apiUrl2)}).`
1395
1698
  );
1396
1699
  }
1397
1700
  return {
1701
+ destination: isPersonal ? "personal" : "workspace",
1702
+ destinationLabel: isPersonal ? "Personal" : config.workspace ?? config.workspaceId,
1703
+ destinationSource: "artifact-config",
1398
1704
  ...config.folder ? { folder: config.folder } : {},
1399
1705
  ...config.folderId ? { folderId: config.folderId } : {},
1400
1706
  ...config.workspace ? { workspace: config.workspace } : {},
1707
+ ...apiKey ? { publishWorkspaceId: config.workspaceId } : {},
1401
1708
  workspaceId: config.workspaceId
1402
1709
  };
1403
1710
  }
1404
- if (!hasPlacementFlag) {
1405
- return {};
1711
+ if (!(hasPlacementFlag || isNewArtifact)) {
1712
+ return {
1713
+ destination: "existing-artifact",
1714
+ destinationLabel: "Existing artifact",
1715
+ destinationSource: "artifact-config"
1716
+ };
1406
1717
  }
1407
- 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) {
1408
1721
  throw new Error(
1409
1722
  "Publishing into a workspace needs an account \u2014 run `artifact-cafe login` first."
1410
1723
  );
1411
1724
  }
1412
- const workspaceHandle = opts.workspace ?? config?.workspace;
1413
- 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)) {
1414
1759
  throw new Error("--folder requires --workspace or a stored workspace.");
1415
1760
  }
1416
1761
  const client = createApiClient(apiUrl2, apiKey);
1417
- let workspaceId;
1418
- if (opts.workspace === void 0 && config?.workspaceId) {
1419
- ({ workspaceId } = config);
1420
- } else {
1421
- const data2 = await client.listWorkspaces();
1422
- const workspace = data2.workspaces.find(
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(
1423
1767
  (candidate) => candidate.handle === workspaceHandle
1424
1768
  );
1425
- if (!workspace) {
1426
- throw new Error(
1427
- `No workspace with handle "${workspaceHandle}" belongs to this account.`
1428
- );
1429
- }
1430
- 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.`);
1431
1779
  }
1432
- 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";
1433
1782
  if (!folderName) {
1434
- 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
+ };
1435
1791
  }
1436
- const data = await client.listFolders(workspaceId);
1437
- const matches = data.folders.filter(
1792
+ const folderData = await client.listFolders(workspace.id);
1793
+ const matches = folderData.folders.filter(
1438
1794
  (candidate) => candidate.name === folderName || candidate.id === folderName
1439
1795
  );
1440
1796
  if (matches.length === 0) {
1441
1797
  throw new Error(
1442
- `No folder named "${folderName}" exists in workspace ${workspaceHandle}.`
1798
+ `No folder named "${folderName}" exists in workspace ${workspace.handle}.`
1443
1799
  );
1444
1800
  }
1445
1801
  if (matches.length > 1) {
1446
1802
  throw new Error(
1447
- `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.`
1448
1804
  );
1449
1805
  }
1450
1806
  const folder = matches[0];
1451
1807
  return {
1808
+ destination: workspace.isPersonal ? "personal" : "workspace",
1809
+ destinationLabel: workspace.isPersonal ? "Personal" : workspace.name,
1810
+ destinationSource,
1452
1811
  folder: folder.name,
1453
1812
  folderId: folder.id,
1454
- workspace: workspaceHandle,
1455
- workspaceId
1813
+ publishWorkspaceId: workspace.id,
1814
+ workspace: workspace.handle,
1815
+ workspaceId: workspace.id
1456
1816
  };
1457
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
+ }
1458
1866
  async function inBatches(items, concurrency, fn) {
1459
1867
  let next = 0;
1460
1868
  const workers = Array.from(
@@ -1491,7 +1899,7 @@ function openInBrowser(url) {
1491
1899
 
1492
1900
  // src/dashboard.ts
1493
1901
  function openDashboard(opts) {
1494
- 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;
1495
1903
  const url = `${base.replace(/\/$/, "")}/dashboard`;
1496
1904
  console.log(url);
1497
1905
  if (process.stdout.isTTY) {
@@ -1580,8 +1988,8 @@ function printFolder(folder) {
1580
1988
  }
1581
1989
 
1582
1990
  // src/link.ts
1583
- import path7 from "path";
1584
- import { createInterface } from "readline";
1991
+ import path8 from "path";
1992
+ import { createInterface as createInterface2 } from "readline";
1585
1993
  async function link(opts) {
1586
1994
  const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
1587
1995
  /\/$/,
@@ -1623,7 +2031,7 @@ async function link(opts) {
1623
2031
  }
1624
2032
  chosen = await pickArtifact(artifacts);
1625
2033
  }
1626
- const dir = path7.resolve(opts.dir);
2034
+ const dir = path8.resolve(opts.dir);
1627
2035
  writeConfig(dir, {
1628
2036
  apiUrl: apiUrl2,
1629
2037
  artifactId: chosen.id,
@@ -1660,7 +2068,7 @@ function pickArtifact(artifacts) {
1660
2068
  ` ${String(i + 1).padStart(2)}. ${a.title} \xB7 ${v} \xB7 ${a.url}`
1661
2069
  );
1662
2070
  });
1663
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2071
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1664
2072
  return new Promise((resolve2, reject) => {
1665
2073
  rl.question(`
1666
2074
  Link which? [1-${artifacts.length}]: `, (answer) => {
@@ -1732,13 +2140,13 @@ function relativeTime(iso) {
1732
2140
  }
1733
2141
 
1734
2142
  // src/login.ts
1735
- import { createInterface as createInterface2 } from "readline";
2143
+ import { createInterface as createInterface3 } from "readline";
1736
2144
  function apiUrl() {
1737
2145
  return (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
1738
2146
  }
1739
2147
  function promptSecret(question) {
1740
2148
  return new Promise((resolve2) => {
1741
- const rl = createInterface2({
2149
+ const rl = createInterface3({
1742
2150
  input: process.stdin,
1743
2151
  output: process.stdout
1744
2152
  });
@@ -1845,10 +2253,69 @@ function logout(opts) {
1845
2253
  );
1846
2254
  }
1847
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
+
1848
2315
  // src/open.ts
1849
- import path8 from "path";
2316
+ import path10 from "path";
1850
2317
  async function openArtifact(opts) {
1851
- const dir = path8.resolve(opts.dir);
2318
+ const dir = path10.resolve(opts.dir);
1852
2319
  const target = resolveArtifactTarget(dir, {
1853
2320
  apiKey: opts.apiKey,
1854
2321
  artifact: opts.artifact
@@ -1866,7 +2333,7 @@ async function openArtifact(opts) {
1866
2333
  }
1867
2334
 
1868
2335
  // src/protect.ts
1869
- import path9 from "path";
2336
+ import path11 from "path";
1870
2337
  async function protect(opts) {
1871
2338
  if (opts.off && opts.password !== void 0) {
1872
2339
  throw new Error("Pass either --password or --off, not both.");
@@ -1883,7 +2350,7 @@ async function protect(opts) {
1883
2350
  throw new Error(parsed.error.issues[0]?.message ?? "Invalid password.");
1884
2351
  }
1885
2352
  }
1886
- const dir = path9.resolve(opts.dir);
2353
+ const dir = path11.resolve(opts.dir);
1887
2354
  const target = resolveArtifactTarget(dir, {
1888
2355
  apiKey: opts.apiKey,
1889
2356
  artifact: opts.artifact
@@ -1908,8 +2375,8 @@ async function protect(opts) {
1908
2375
  }
1909
2376
 
1910
2377
  // src/reply.ts
1911
- import path10 from "path";
1912
- async function readStdin() {
2378
+ import path12 from "path";
2379
+ async function readStdin3() {
1913
2380
  if (process.stdin.isTTY) {
1914
2381
  return "";
1915
2382
  }
@@ -1926,13 +2393,13 @@ async function replyToThread(opts) {
1926
2393
  "Pass the thread id to reply to \u2014 get it from `artifact-cafe comments --json`."
1927
2394
  );
1928
2395
  }
1929
- const body = (opts.body ?? await readStdin()).trim();
2396
+ const body = (opts.body ?? await readStdin3()).trim();
1930
2397
  if (!body) {
1931
2398
  throw new Error(
1932
2399
  'Nothing to post. Pass the reply with --body "\u2026", or pipe it on stdin.'
1933
2400
  );
1934
2401
  }
1935
- const dir = path10.resolve(opts.dir);
2402
+ const dir = path12.resolve(opts.dir);
1936
2403
  const target = resolveArtifactTarget(dir, {
1937
2404
  apiKey: opts.apiKey,
1938
2405
  artifact: opts.artifact
@@ -1961,7 +2428,7 @@ async function replyToThread(opts) {
1961
2428
  }
1962
2429
 
1963
2430
  // src/resolve.ts
1964
- import path11 from "path";
2431
+ import path13 from "path";
1965
2432
  async function changeThreadStatus(opts) {
1966
2433
  const threadId = opts.thread?.trim();
1967
2434
  if (!threadId) {
@@ -1969,7 +2436,7 @@ async function changeThreadStatus(opts) {
1969
2436
  `Pass the thread id to ${opts.action} \u2014 get it from \`artifact-cafe comments --json\`.`
1970
2437
  );
1971
2438
  }
1972
- const dir = path11.resolve(opts.dir);
2439
+ const dir = path13.resolve(opts.dir);
1973
2440
  const target = resolveArtifactTarget(dir, {
1974
2441
  apiKey: opts.apiKey,
1975
2442
  artifact: opts.artifact
@@ -1990,26 +2457,26 @@ async function changeThreadStatus(opts) {
1990
2457
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1991
2458
  import { createRequire } from "module";
1992
2459
  import { homedir as homedir2 } from "os";
1993
- import path12 from "path";
1994
- import { z as z15 } from "zod";
2460
+ import path14 from "path";
2461
+ import { z as z16 } from "zod";
1995
2462
  var REGISTRY_LATEST_URL = "https://registry.npmjs.org/artifact-cafe/latest";
1996
2463
  var SKILL_SOURCE_URL = "https://raw.githubusercontent.com/Artifact-Cafe/skill/main/artifact-cafe/SKILL.md";
1997
2464
  var SKILL_UPDATE_COMMAND = "npx skills add artifact-cafe/skill --skill artifact-cafe -g";
1998
2465
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
1999
2466
  var FETCH_TIMEOUT_MS = 2e3;
2000
2467
  var SKILL_VERSION_PATTERN = /\*\*Skill version:\s*(\d+\.\d+\.\d+)\*\*/;
2001
- var cacheSchema = z15.object({
2002
- checkedAt: z15.number(),
2003
- cliLatest: z15.string().nullable(),
2004
- skillLatest: z15.string().nullable(),
2005
- version: z15.literal(1)
2468
+ var cacheSchema = z16.object({
2469
+ checkedAt: z16.number(),
2470
+ cliLatest: z16.string().nullable(),
2471
+ skillLatest: z16.string().nullable(),
2472
+ version: z16.literal(1)
2006
2473
  });
2007
2474
  function cliVersion() {
2008
2475
  const pkg = createRequire(import.meta.url)("../package.json");
2009
2476
  return pkg.version;
2010
2477
  }
2011
2478
  function cachePath() {
2012
- return path12.join(homedir2(), ".artifactcafe", "update-check.json");
2479
+ return path14.join(homedir2(), ".artifactcafe", "update-check.json");
2013
2480
  }
2014
2481
  function readCache() {
2015
2482
  try {
@@ -2020,7 +2487,7 @@ function readCache() {
2020
2487
  }
2021
2488
  function writeCache(cache) {
2022
2489
  try {
2023
- mkdirSync3(path12.dirname(cachePath()), { mode: 448, recursive: true });
2490
+ mkdirSync3(path14.dirname(cachePath()), { mode: 448, recursive: true });
2024
2491
  writeFileSync3(cachePath(), `${JSON.stringify(cache, null, 2)}
2025
2492
  `);
2026
2493
  } catch {
@@ -2057,7 +2524,7 @@ async function fetchText(url) {
2057
2524
  }
2058
2525
  async function fetchLatestVersions(previous) {
2059
2526
  const [cli, skill] = await Promise.all([
2060
- fetchJson(REGISTRY_LATEST_URL).then((body) => z15.object({ version: z15.string() }).parse(body).version).catch(() => null),
2527
+ fetchJson(REGISTRY_LATEST_URL).then((body) => z16.object({ version: z16.string() }).parse(body).version).catch(() => null),
2061
2528
  fetchText(SKILL_SOURCE_URL).then((body) => SKILL_VERSION_PATTERN.exec(body)?.[1] ?? null).catch(() => null)
2062
2529
  ]);
2063
2530
  return {
@@ -2073,7 +2540,7 @@ function installedSkillVersions() {
2073
2540
  const versions = [];
2074
2541
  for (const root of roots) {
2075
2542
  for (const dir of dirs) {
2076
- const file = path12.join(root, dir, "skills", "artifact-cafe", "SKILL.md");
2543
+ const file = path14.join(root, dir, "skills", "artifact-cafe", "SKILL.md");
2077
2544
  if (!existsSync3(file)) {
2078
2545
  continue;
2079
2546
  }
@@ -2651,6 +3118,152 @@ var reply = defineCommand({
2651
3118
  }
2652
3119
  }
2653
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
+ });
2654
3267
  var open = defineCommand({
2655
3268
  args: {
2656
3269
  "api-key": {
@@ -2720,8 +3333,11 @@ var main = defineCommand({
2720
3333
  await showUsage(cmd);
2721
3334
  },
2722
3335
  subCommands: {
3336
+ comment: comment2,
2723
3337
  comments: comments2,
2724
3338
  dashboard,
3339
+ delete: del,
3340
+ edit,
2725
3341
  folders: folders2,
2726
3342
  link: link2,
2727
3343
  list: list2,