artifacty 0.10.8 → 0.11.0
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.
- package/CLAUDE.md +1 -1
- package/README.md +260 -5
- package/docs/artifact-schema-v1.md +187 -1
- package/docs/central-team-deployment-design.md +3 -1
- package/docs/integrations.md +9 -1
- package/docs/mcp-public-api.md +103 -9
- package/docs/release-checklist.md +1 -1
- package/docs/roadmap-design.md +977 -0
- package/docs/sarif-csv-artifact-plan.md +42 -7
- package/docs/threat-model.md +217 -2
- package/package.json +2 -1
- package/src/cli.js +550 -43
- package/src/client/viewer.js +377 -0
- package/src/lib/backup.js +326 -15
- package/src/lib/converters.js +99 -5
- package/src/lib/csv.js +51 -0
- package/src/lib/diff.js +656 -0
- package/src/lib/doctor.js +88 -2
- package/src/lib/embeddings.js +407 -0
- package/src/lib/events.js +183 -0
- package/src/lib/i18n.js +290 -2
- package/src/lib/listing.js +46 -0
- package/src/lib/openapi.js +614 -0
- package/src/lib/render.js +1604 -160
- package/src/lib/retention-form.js +31 -0
- package/src/lib/retention.js +499 -0
- package/src/lib/sarif-csv-export.js +290 -0
- package/src/lib/schemas.js +461 -0
- package/src/lib/security.js +226 -1
- package/src/lib/sse.js +52 -0
- package/src/lib/storage.js +2935 -305
- package/src/lib/webhooks.js +356 -0
- package/src/mcp-server.js +756 -25
- package/src/server.js +1529 -98
package/src/cli.js
CHANGED
|
@@ -3,6 +3,8 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { generateToken } from "./lib/token.js";
|
|
6
|
+
import { parseSseFrames } from "./lib/sse.js";
|
|
7
|
+
import { groupArtifactIds, paginationJson } from "./lib/listing.js";
|
|
6
8
|
|
|
7
9
|
const PACKAGE_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
8
10
|
|
|
@@ -31,17 +33,31 @@ async function main() {
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const {
|
|
36
|
+
addComment,
|
|
37
|
+
addRelation,
|
|
34
38
|
archiveArtifact,
|
|
35
39
|
checkStoreIntegrity,
|
|
36
40
|
createArtifact,
|
|
41
|
+
createSavedView,
|
|
37
42
|
createStore,
|
|
43
|
+
deleteSavedView,
|
|
38
44
|
getArtifact,
|
|
39
45
|
importUsersFromCsv,
|
|
40
46
|
listAuditEvents,
|
|
41
47
|
listArtifactsPage,
|
|
48
|
+
listComments,
|
|
49
|
+
listRelations,
|
|
50
|
+
listSavedViews,
|
|
51
|
+
rebuildEmbeddingIndex,
|
|
42
52
|
rebuildSearchIndex,
|
|
53
|
+
removeRelation,
|
|
54
|
+
resolveComment,
|
|
55
|
+
resolveSavedView,
|
|
43
56
|
restoreArtifact,
|
|
44
|
-
|
|
57
|
+
setArtifactVisibility,
|
|
58
|
+
setReviewStatus,
|
|
59
|
+
updateArtifact,
|
|
60
|
+
VersionConflictError
|
|
45
61
|
} = await import("./lib/storage.js");
|
|
46
62
|
const { exportStore, importStore, defaultBackupPath } = await import("./lib/backup.js");
|
|
47
63
|
const { convertAgentArtifact } = await import("./lib/converters.js");
|
|
@@ -50,6 +66,8 @@ async function main() {
|
|
|
50
66
|
const { serviceCommand } = await import("./lib/service.js");
|
|
51
67
|
const { backgroundStatus, startBackgroundServer, stopBackgroundServer } = await import("./lib/background.js");
|
|
52
68
|
const { runDoctor } = await import("./lib/doctor.js");
|
|
69
|
+
const { getRetentionPolicy, normalizeRetentionPolicy, runRetention, setRetentionPolicy } = await import("./lib/retention.js");
|
|
70
|
+
const { parseArchiveAfterDaysPairs } = await import("./lib/retention-form.js");
|
|
53
71
|
const { startServer } = await import("./server.js");
|
|
54
72
|
const store = createStore({ home: options.home });
|
|
55
73
|
|
|
@@ -146,6 +164,7 @@ async function main() {
|
|
|
146
164
|
tags: options.tag || [],
|
|
147
165
|
metadata: options.metadata ? JSON.parse(options.metadata) : {},
|
|
148
166
|
allowSecrets: options.allowSecrets,
|
|
167
|
+
visibility: options.visibility,
|
|
149
168
|
audit: cliAuditContext()
|
|
150
169
|
});
|
|
151
170
|
printJson(await withUrls(store, artifact));
|
|
@@ -223,43 +242,146 @@ async function main() {
|
|
|
223
242
|
throw new Error("update requires an artifact id");
|
|
224
243
|
}
|
|
225
244
|
const content = await readContent(options);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
245
|
+
try {
|
|
246
|
+
const artifact = await updateArtifact(store, id, {
|
|
247
|
+
title: options.title,
|
|
248
|
+
content,
|
|
249
|
+
format: options.format,
|
|
250
|
+
artifactType: options.artifactType,
|
|
251
|
+
schemaVersion: options.schemaVersion,
|
|
252
|
+
sourceAgent: options.source || "cli",
|
|
253
|
+
tags: options.tag || [],
|
|
254
|
+
metadata: options.metadata ? JSON.parse(options.metadata) : {},
|
|
255
|
+
allowSecrets: options.allowSecrets,
|
|
256
|
+
expectedVersion: options.expectedVersion,
|
|
257
|
+
audit: cliAuditContext()
|
|
258
|
+
});
|
|
259
|
+
const finalArtifact = options.visibility
|
|
260
|
+
? await setArtifactVisibility(store, id, options.visibility, { audit: cliAuditContext() })
|
|
261
|
+
: artifact;
|
|
262
|
+
printJson(await withUrls(store, finalArtifact));
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (error instanceof VersionConflictError) {
|
|
265
|
+
printJson({
|
|
266
|
+
error: error.message,
|
|
267
|
+
code: error.code,
|
|
268
|
+
latestVersion: error.latestVersion
|
|
269
|
+
});
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
239
275
|
return;
|
|
240
276
|
}
|
|
241
277
|
|
|
242
278
|
if (command === "list") {
|
|
243
|
-
|
|
279
|
+
let baseFilters = {};
|
|
280
|
+
if (options.view) {
|
|
281
|
+
const view = await resolveSavedView(store, options.view, {});
|
|
282
|
+
if (view) {
|
|
283
|
+
baseFilters = view.filters;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const explicit = {
|
|
244
287
|
query: options.query,
|
|
245
288
|
tag: Array.isArray(options.tag) ? options.tag[0] : options.tag,
|
|
246
289
|
sourceAgent: options.source,
|
|
247
|
-
|
|
290
|
+
artifactType: options.type,
|
|
291
|
+
publisher: options.publisher,
|
|
292
|
+
createdAfter: options.createdAfter,
|
|
293
|
+
createdBefore: options.createdBefore,
|
|
294
|
+
reviewStatus: options.reviewStatus,
|
|
295
|
+
relatedTo: options.relatedTo,
|
|
296
|
+
relation: options.relation,
|
|
297
|
+
mode: options.mode,
|
|
298
|
+
includeArchived: options.includeArchived
|
|
299
|
+
};
|
|
300
|
+
const filters = { ...baseFilters };
|
|
301
|
+
for (const [key, value] of Object.entries(explicit)) {
|
|
302
|
+
if (value !== undefined) {
|
|
303
|
+
filters[key] = value;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const page = await listArtifactsPage(store, {
|
|
307
|
+
...filters,
|
|
248
308
|
limit: options.limit,
|
|
249
309
|
offset: options.offset
|
|
250
310
|
});
|
|
251
|
-
|
|
311
|
+
const result = {
|
|
252
312
|
artifacts: page.artifacts,
|
|
253
313
|
pagination: paginationJson(page),
|
|
254
314
|
search: page.search
|
|
255
|
-
}
|
|
315
|
+
};
|
|
316
|
+
if (options.groupBy) {
|
|
317
|
+
result.groups = groupArtifactIds(page.artifacts, options.groupBy);
|
|
318
|
+
}
|
|
319
|
+
printJson(result);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (command === "views") {
|
|
324
|
+
const subcommand = options._[0];
|
|
325
|
+
|
|
326
|
+
if (subcommand === "save") {
|
|
327
|
+
const name = options._[1];
|
|
328
|
+
if (!name) {
|
|
329
|
+
throw new Error("views save requires a name");
|
|
330
|
+
}
|
|
331
|
+
const filterSource = {
|
|
332
|
+
query: options.query,
|
|
333
|
+
tag: Array.isArray(options.tag) ? options.tag[0] : options.tag,
|
|
334
|
+
sourceAgent: options.source,
|
|
335
|
+
artifactType: options.type,
|
|
336
|
+
publisher: options.publisher,
|
|
337
|
+
createdAfter: options.createdAfter,
|
|
338
|
+
createdBefore: options.createdBefore,
|
|
339
|
+
reviewStatus: options.reviewStatus,
|
|
340
|
+
relatedTo: options.relatedTo,
|
|
341
|
+
relation: options.relation,
|
|
342
|
+
mode: options.mode
|
|
343
|
+
};
|
|
344
|
+
const filters = {};
|
|
345
|
+
for (const [key, value] of Object.entries(filterSource)) {
|
|
346
|
+
if (value !== undefined) {
|
|
347
|
+
filters[key] = value;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (options.includeArchived) {
|
|
351
|
+
filters.includeArchived = true;
|
|
352
|
+
}
|
|
353
|
+
const view = await createSavedView(store, {
|
|
354
|
+
name,
|
|
355
|
+
filters,
|
|
356
|
+
shared: Boolean(options.shared),
|
|
357
|
+
audit: cliAuditContext()
|
|
358
|
+
});
|
|
359
|
+
printJson(view);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (subcommand === "delete") {
|
|
364
|
+
const id = options._[1];
|
|
365
|
+
if (!id) {
|
|
366
|
+
throw new Error("views delete requires a view id");
|
|
367
|
+
}
|
|
368
|
+
const view = await deleteSavedView(store, id, { audit: cliAuditContext() });
|
|
369
|
+
printJson(view);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const views = await listSavedViews(store, {});
|
|
374
|
+
printJson({ views });
|
|
256
375
|
return;
|
|
257
376
|
}
|
|
258
377
|
|
|
259
378
|
if ((command === "index" || command === "search") && options._[0] === "rebuild") {
|
|
260
379
|
const result = await rebuildSearchIndex(store);
|
|
380
|
+
if (options.embeddings) {
|
|
381
|
+
result.embeddings = await rebuildEmbeddingIndex(store);
|
|
382
|
+
}
|
|
261
383
|
printJson(result);
|
|
262
|
-
if (!result.fts5) {
|
|
384
|
+
if (!result.fts5 || (options.embeddings && result.embeddings.configured && !result.embeddings.ok)) {
|
|
263
385
|
process.exitCode = 1;
|
|
264
386
|
}
|
|
265
387
|
return;
|
|
@@ -274,6 +396,46 @@ async function main() {
|
|
|
274
396
|
return;
|
|
275
397
|
}
|
|
276
398
|
|
|
399
|
+
if (command === "retention") {
|
|
400
|
+
const subcommand = options._[0];
|
|
401
|
+
|
|
402
|
+
if (subcommand === "show") {
|
|
403
|
+
printJson(await getRetentionPolicy(store));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (subcommand === "set") {
|
|
408
|
+
const current = await getRetentionPolicy(store);
|
|
409
|
+
const byType = parseArchiveAfterDaysPairs(options.archiveAfterDaysFor || [], {
|
|
410
|
+
base: current.archiveAfterDays.byType,
|
|
411
|
+
onInvalid: (entry) => {
|
|
412
|
+
throw new Error(`--archive-after-days-for expects type=days, got: ${entry}`);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
const next = normalizeRetentionPolicy({
|
|
416
|
+
archiveAfterDays: {
|
|
417
|
+
default: options.archiveAfterDays !== undefined ? options.archiveAfterDays : current.archiveAfterDays.default,
|
|
418
|
+
byType
|
|
419
|
+
},
|
|
420
|
+
purgeArchivedAfterDays: options.purgeArchivedAfterDays !== undefined ? options.purgeArchivedAfterDays : current.purgeArchivedAfterDays,
|
|
421
|
+
auditRetentionDays: options.auditRetentionDays !== undefined ? options.auditRetentionDays : current.auditRetentionDays,
|
|
422
|
+
eventRetentionRows: options.eventRetentionRows !== undefined ? options.eventRetentionRows : current.eventRetentionRows,
|
|
423
|
+
keepTags: options.keepTag !== undefined ? options.keepTag : current.keepTags
|
|
424
|
+
});
|
|
425
|
+
const saved = await setRetentionPolicy(store, next, { audit: cliAuditContext() });
|
|
426
|
+
printJson(saved);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (subcommand === "run") {
|
|
431
|
+
const result = await runRetention(store, { dryRun: Boolean(options.dryRun), allowPurge: options.allowPurge });
|
|
432
|
+
printJson(result);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
throw new Error("retention requires a subcommand: show, set, or run");
|
|
437
|
+
}
|
|
438
|
+
|
|
277
439
|
if (command === "archive" || command === "restore") {
|
|
278
440
|
const id = options._[0];
|
|
279
441
|
if (!id) {
|
|
@@ -286,6 +448,17 @@ async function main() {
|
|
|
286
448
|
return;
|
|
287
449
|
}
|
|
288
450
|
|
|
451
|
+
if (command === "visibility") {
|
|
452
|
+
const id = options._[0];
|
|
453
|
+
const visibility = options._[1];
|
|
454
|
+
if (!id || !visibility) {
|
|
455
|
+
throw new Error("visibility requires an artifact id and a value: private or team");
|
|
456
|
+
}
|
|
457
|
+
const artifact = await setArtifactVisibility(store, id, visibility, { audit: cliAuditContext() });
|
|
458
|
+
printJson(await withUrls(store, artifact));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
289
462
|
if (command === "audit") {
|
|
290
463
|
const events = await listAuditEvents(store, {
|
|
291
464
|
artifactId: options.artifact,
|
|
@@ -297,12 +470,15 @@ async function main() {
|
|
|
297
470
|
|
|
298
471
|
if (command === "export" || command === "backup") {
|
|
299
472
|
const file = command === "backup" ? options.file || defaultBackupPath(store) : requireOption(options, "file");
|
|
300
|
-
printJson(await exportStore(store, file));
|
|
473
|
+
printJson(await exportStore(store, file, { scope: options.full ? "full" : "artifacts" }));
|
|
301
474
|
return;
|
|
302
475
|
}
|
|
303
476
|
|
|
304
477
|
if (command === "import-store") {
|
|
305
|
-
printJson(await importStore(store, requireOption(options, "file")
|
|
478
|
+
printJson(await importStore(store, requireOption(options, "file"), {
|
|
479
|
+
confirm: options.confirm,
|
|
480
|
+
forceUsers: Boolean(options.forceUsers)
|
|
481
|
+
}));
|
|
306
482
|
return;
|
|
307
483
|
}
|
|
308
484
|
|
|
@@ -350,13 +526,304 @@ async function main() {
|
|
|
350
526
|
process.stdout.write(artifact.content);
|
|
351
527
|
return;
|
|
352
528
|
}
|
|
529
|
+
if (options.relations) {
|
|
530
|
+
printJson(artifact.relations);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
353
533
|
printJson(await withUrls(store, artifact));
|
|
354
534
|
return;
|
|
355
535
|
}
|
|
356
536
|
|
|
537
|
+
if (command === "link" || command === "unlink") {
|
|
538
|
+
const [fromId, relation, toId] = options._;
|
|
539
|
+
if (!fromId || !relation || !toId) {
|
|
540
|
+
throw new Error(`${command} requires: <from> <relation> <to>`);
|
|
541
|
+
}
|
|
542
|
+
const result = command === "link"
|
|
543
|
+
? await addRelation(store, { fromId, toId, relation, audit: cliAuditContext() })
|
|
544
|
+
: await removeRelation(store, { fromId, toId, relation, audit: cliAuditContext() });
|
|
545
|
+
printJson(result);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (command === "relations") {
|
|
550
|
+
const id = options._[0];
|
|
551
|
+
if (!id) {
|
|
552
|
+
throw new Error("relations requires an artifact id");
|
|
553
|
+
}
|
|
554
|
+
printJson(await listRelations(store, id, {
|
|
555
|
+
direction: options.direction,
|
|
556
|
+
relation: options.relation
|
|
557
|
+
}));
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (command === "comment") {
|
|
562
|
+
const id = options._[0];
|
|
563
|
+
if (!id) {
|
|
564
|
+
throw new Error("comment requires an artifact id");
|
|
565
|
+
}
|
|
566
|
+
if (!options.body) {
|
|
567
|
+
throw new Error("comment requires --body <text>");
|
|
568
|
+
}
|
|
569
|
+
const comment = await addComment(store, id, {
|
|
570
|
+
version: options.version,
|
|
571
|
+
body: options.body,
|
|
572
|
+
parentId: options.parent,
|
|
573
|
+
anchor: Number.isFinite(options.line) ? { line: options.line } : undefined,
|
|
574
|
+
sourceAgent: "cli",
|
|
575
|
+
audit: cliAuditContext()
|
|
576
|
+
});
|
|
577
|
+
printJson(comment);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (command === "comments") {
|
|
582
|
+
const id = options._[0];
|
|
583
|
+
if (!id) {
|
|
584
|
+
throw new Error("comments requires an artifact id");
|
|
585
|
+
}
|
|
586
|
+
printJson(await listComments(store, id, {
|
|
587
|
+
version: options.version,
|
|
588
|
+
status: options.status
|
|
589
|
+
}));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (command === "resolve-comment") {
|
|
594
|
+
const [id, commentId] = options._;
|
|
595
|
+
if (!id || !commentId) {
|
|
596
|
+
throw new Error("resolve-comment requires: <id> <commentId>");
|
|
597
|
+
}
|
|
598
|
+
printJson(await resolveComment(store, id, commentId, { audit: cliAuditContext() }));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (command === "review-status") {
|
|
603
|
+
const [id, status] = options._;
|
|
604
|
+
if (!id || !status) {
|
|
605
|
+
throw new Error("review-status requires: <id> none|pending|changes-requested|approved");
|
|
606
|
+
}
|
|
607
|
+
printJson(await withUrls(store, await setReviewStatus(store, id, status, { audit: cliAuditContext() })));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (command === "diff") {
|
|
612
|
+
const id = options._[0];
|
|
613
|
+
if (!id) {
|
|
614
|
+
throw new Error("diff requires an artifact id");
|
|
615
|
+
}
|
|
616
|
+
const { createStructuredDiff, createLineDiff, diffFormatFor, renderUnifiedDiffText, resolveDiffView } = await import("./lib/diff.js");
|
|
617
|
+
const latest = await getArtifact(store, id);
|
|
618
|
+
const defaultFrom = Math.max(1, latest.latestVersion - 1);
|
|
619
|
+
const fromNumber = Number.isFinite(options.from) ? options.from : defaultFrom;
|
|
620
|
+
const toNumber = Number.isFinite(options.to) ? options.to : latest.latestVersion;
|
|
621
|
+
const from = await getArtifact(store, id, { version: fromNumber });
|
|
622
|
+
const to = await getArtifact(store, id, { version: toNumber });
|
|
623
|
+
const diffFormat = diffFormatFor(latest.artifactType, to.version.format);
|
|
624
|
+
const useStructured = options.structured ||
|
|
625
|
+
resolveDiffView({ artifactType: latest.artifactType, format: to.version.format, requestedView: undefined }) === "structured";
|
|
626
|
+
|
|
627
|
+
if (options.json) {
|
|
628
|
+
if (useStructured) {
|
|
629
|
+
printJson({
|
|
630
|
+
id: latest.id,
|
|
631
|
+
from: from.version.version,
|
|
632
|
+
to: to.version.version,
|
|
633
|
+
view: "structured",
|
|
634
|
+
format: diffFormat,
|
|
635
|
+
structuredDiff: createStructuredDiff(from.content, to.content, { format: diffFormat })
|
|
636
|
+
});
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
printJson({
|
|
640
|
+
id: latest.id,
|
|
641
|
+
from: from.version.version,
|
|
642
|
+
to: to.version.version,
|
|
643
|
+
view: "lines",
|
|
644
|
+
format: diffFormat,
|
|
645
|
+
diffRows: createLineDiff(from.content, to.content)
|
|
646
|
+
});
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (useStructured) {
|
|
651
|
+
const structuredDiff = createStructuredDiff(from.content, to.content, { format: diffFormat });
|
|
652
|
+
process.stdout.write(`${renderUnifiedDiffText(structuredDiff)}\n`);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const rows = createLineDiff(from.content, to.content);
|
|
657
|
+
for (const row of rows) {
|
|
658
|
+
if (row.type === "same") {
|
|
659
|
+
process.stdout.write(` ${row.text}\n`);
|
|
660
|
+
} else if (row.type === "added") {
|
|
661
|
+
process.stdout.write(`+ ${row.text}\n`);
|
|
662
|
+
} else {
|
|
663
|
+
process.stdout.write(`- ${row.text}\n`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (command === "watch") {
|
|
670
|
+
return runWatchCommand(store, options);
|
|
671
|
+
}
|
|
672
|
+
|
|
357
673
|
throw new Error(`Unknown command: ${command}`);
|
|
358
674
|
}
|
|
359
675
|
|
|
676
|
+
// `artifacty watch` connects to a running server's SSE endpoint
|
|
677
|
+
// (/api/events; see src/server.js handleEventStream) and prints one line
|
|
678
|
+
// per matching event, optionally piping each event to --exec. Reconnects
|
|
679
|
+
// with Last-Event-ID on drop so a brief server restart does not lose
|
|
680
|
+
// events. --once resolves after the first match, making this usable as a
|
|
681
|
+
// shell-friendly, no-dependency alternative to the artifacty_wait MCP tool.
|
|
682
|
+
async function runWatchCommand(store, options) {
|
|
683
|
+
const { resolvePublicBaseUrl } = await import("./lib/server-state.js");
|
|
684
|
+
const baseUrl = await resolvePublicBaseUrl(store, { url: options.url });
|
|
685
|
+
const token = options.apiToken || process.env.ARTIFACTY_API_TOKEN || "";
|
|
686
|
+
const filter = {};
|
|
687
|
+
if (options.tag) filter.tag = Array.isArray(options.tag) ? options.tag[options.tag.length - 1] : options.tag;
|
|
688
|
+
if (options.artifact) filter.artifactId = options.artifact;
|
|
689
|
+
if (options.type) filter.type = options.type;
|
|
690
|
+
|
|
691
|
+
let lastEventId = null;
|
|
692
|
+
// SIGINT exits immediately (process.exit is synchronous-enough that the
|
|
693
|
+
// while loop below never observes another iteration), so there is no
|
|
694
|
+
// "stopped" state for the loop condition to check.
|
|
695
|
+
process.on("SIGINT", () => {
|
|
696
|
+
process.exit(0);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
for (;;) {
|
|
700
|
+
const matched = await watchOnce({ baseUrl, token, filter, lastEventId, options });
|
|
701
|
+
if (matched.stop) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (matched.lastEventId) {
|
|
705
|
+
lastEventId = matched.lastEventId;
|
|
706
|
+
}
|
|
707
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function watchOnce({ baseUrl, token, filter, lastEventId, options }) {
|
|
712
|
+
const url = new URL("/api/events", baseUrl);
|
|
713
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
714
|
+
url.searchParams.set(key, value);
|
|
715
|
+
}
|
|
716
|
+
const headers = { accept: "text/event-stream" };
|
|
717
|
+
if (token) {
|
|
718
|
+
headers.authorization = `Bearer ${token}`;
|
|
719
|
+
}
|
|
720
|
+
if (lastEventId) {
|
|
721
|
+
headers["last-event-id"] = String(lastEventId);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
let response;
|
|
725
|
+
try {
|
|
726
|
+
response = await fetch(url, { headers });
|
|
727
|
+
} catch (error) {
|
|
728
|
+
process.stderr.write(`artifacty watch: connection failed: ${error.message}\n`);
|
|
729
|
+
return {};
|
|
730
|
+
}
|
|
731
|
+
if (!response.ok || !response.body) {
|
|
732
|
+
process.stderr.write(`artifacty watch: server returned ${response.status}\n`);
|
|
733
|
+
return {};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const reader = response.body.getReader();
|
|
737
|
+
const decoder = new TextDecoder();
|
|
738
|
+
let buffer = "";
|
|
739
|
+
let seenLastEventId = lastEventId;
|
|
740
|
+
let readyPrinted = false;
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
while (true) {
|
|
744
|
+
const { value, done } = await reader.read();
|
|
745
|
+
if (done) {
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
buffer += decoder.decode(value, { stream: true });
|
|
749
|
+
// handleEventStream (src/server.js) writes ": connected\n\n" as the
|
|
750
|
+
// very first frame once the SSE subscription is actually live, before
|
|
751
|
+
// any replayed or new events. A caller scripting against `watch`
|
|
752
|
+
// (tests included) needs a deterministic point to know the stream is
|
|
753
|
+
// subscribed rather than sleeping and hoping; print the ready marker
|
|
754
|
+
// once we've seen it, before parsing frames as events (parseSseFrames
|
|
755
|
+
// filters comment-only frames like this one out anyway).
|
|
756
|
+
if (!readyPrinted && buffer.includes(": connected\n\n")) {
|
|
757
|
+
readyPrinted = true;
|
|
758
|
+
printWatchReady(options);
|
|
759
|
+
}
|
|
760
|
+
const { frames, rest } = parseSseFrames(buffer);
|
|
761
|
+
buffer = rest;
|
|
762
|
+
for (const parsed of frames) {
|
|
763
|
+
if (parsed.id) {
|
|
764
|
+
seenLastEventId = parsed.id;
|
|
765
|
+
}
|
|
766
|
+
const event = JSON.parse(parsed.data);
|
|
767
|
+
await emitWatchEvent(event, options);
|
|
768
|
+
if (options.once) {
|
|
769
|
+
await reader.cancel().catch(() => {});
|
|
770
|
+
return { stop: true };
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
} catch (error) {
|
|
775
|
+
process.stderr.write(`artifacty watch: stream error: ${error.message}\n`);
|
|
776
|
+
}
|
|
777
|
+
return { lastEventId: seenLastEventId };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function printWatchReady(options) {
|
|
781
|
+
if (options.json) {
|
|
782
|
+
process.stderr.write(`${JSON.stringify({ ready: true })}\n`);
|
|
783
|
+
} else {
|
|
784
|
+
process.stderr.write("# connected\n");
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async function emitWatchEvent(event, options) {
|
|
789
|
+
if (options.json) {
|
|
790
|
+
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
791
|
+
} else {
|
|
792
|
+
process.stdout.write(`${event.type} ${event.artifactId || ""} v${event.version ?? ""}\n`);
|
|
793
|
+
}
|
|
794
|
+
if (options.exec) {
|
|
795
|
+
await execWatchCommand(options.exec, event);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async function execWatchCommand(command, event) {
|
|
800
|
+
const { spawn } = await import("node:child_process");
|
|
801
|
+
await new Promise((resolve) => {
|
|
802
|
+
const child = spawn(command, {
|
|
803
|
+
shell: true,
|
|
804
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
805
|
+
env: {
|
|
806
|
+
...process.env,
|
|
807
|
+
ARTIFACTY_EVENT_TYPE: event.type || "",
|
|
808
|
+
ARTIFACTY_EVENT_ARTIFACT_ID: event.artifactId || "",
|
|
809
|
+
ARTIFACTY_EVENT_VERSION: event.version !== undefined && event.version !== null ? String(event.version) : ""
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
child.stdin.write(`${JSON.stringify(event)}\n`);
|
|
813
|
+
child.stdin.end();
|
|
814
|
+
child.on("error", (error) => {
|
|
815
|
+
process.stderr.write(`artifacty watch: --exec failed: ${error.message}\n`);
|
|
816
|
+
resolve();
|
|
817
|
+
});
|
|
818
|
+
child.on("exit", (code) => {
|
|
819
|
+
if (code !== 0) {
|
|
820
|
+
process.stderr.write(`artifacty watch: --exec exited with code ${code}\n`);
|
|
821
|
+
}
|
|
822
|
+
resolve();
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
360
827
|
function parseArgs(args) {
|
|
361
828
|
const options = {};
|
|
362
829
|
const positional = [];
|
|
@@ -369,7 +836,7 @@ function parseArgs(args) {
|
|
|
369
836
|
}
|
|
370
837
|
|
|
371
838
|
const key = arg.slice(2);
|
|
372
|
-
if (key === "raw" || key === "dry-run" || key === "trust" || key === "include-archived" || key === "allow-secrets" || key === "generate-token" || key === "detach" || key === "foreground" || key === "force" || key === "skip-mcp" || key === "mcp-http" || key === "no-password-reset") {
|
|
839
|
+
if (key === "raw" || key === "dry-run" || key === "trust" || key === "include-archived" || key === "allow-secrets" || key === "generate-token" || key === "detach" || key === "foreground" || key === "force" || key === "skip-mcp" || key === "mcp-http" || key === "no-password-reset" || key === "relations" || key === "structured" || key === "json" || key === "once" || key === "full" || key === "force-users" || key === "allow-purge" || key === "shared" || key === "embeddings") {
|
|
373
840
|
options[toCamelCase(key)] = true;
|
|
374
841
|
continue;
|
|
375
842
|
}
|
|
@@ -381,7 +848,11 @@ function parseArgs(args) {
|
|
|
381
848
|
|
|
382
849
|
if (key === "tag") {
|
|
383
850
|
options.tag = [...(options.tag || []), value];
|
|
384
|
-
} else if (key === "
|
|
851
|
+
} else if (key === "keep-tag") {
|
|
852
|
+
options.keepTag = [...(options.keepTag || []), value];
|
|
853
|
+
} else if (key === "archive-after-days-for") {
|
|
854
|
+
options.archiveAfterDaysFor = [...(options.archiveAfterDaysFor || []), value];
|
|
855
|
+
} else if (key === "port" || key === "limit" || key === "offset" || key === "version" || key === "schema-version" || key === "timeout" || key === "bytes" || key === "expected-version" || key === "from" || key === "to" || key === "archive-after-days" || key === "purge-archived-after-days" || key === "audit-retention-days" || key === "event-retention-rows" || key === "line") {
|
|
385
856
|
options[toCamelCase(key)] = Number(value);
|
|
386
857
|
} else {
|
|
387
858
|
options[toCamelCase(key)] = value;
|
|
@@ -426,17 +897,6 @@ async function withUrls(store, artifact) {
|
|
|
426
897
|
};
|
|
427
898
|
}
|
|
428
899
|
|
|
429
|
-
function paginationJson(page) {
|
|
430
|
-
return {
|
|
431
|
-
total: page.total,
|
|
432
|
-
limit: page.limit,
|
|
433
|
-
offset: page.offset,
|
|
434
|
-
hasMore: page.hasMore,
|
|
435
|
-
nextOffset: page.nextOffset,
|
|
436
|
-
previousOffset: page.previousOffset
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
900
|
function requireOption(options, name) {
|
|
441
901
|
if (!options[name]) {
|
|
442
902
|
throw new Error(`Missing required option --${name}`);
|
|
@@ -465,32 +925,79 @@ Usage:
|
|
|
465
925
|
artifacty status [--home ~/.artifacty]
|
|
466
926
|
artifacty stop [--home ~/.artifacty] [--timeout 30000] [--force]
|
|
467
927
|
artifacty doctor [--home ~/.artifacty] [--skip-mcp] [--timeout 5000]
|
|
468
|
-
artifacty publish --title <title> (--file <path> | --content <text>) [--format html|markdown|text|json|code|svg|mermaid|react] [--source agent] [--tag tag]
|
|
469
|
-
artifacty import --agent claude|codex|gemini|copilot|cursor|auto (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react] [--tag tag]
|
|
928
|
+
artifacty publish --title <title> (--file <path> | --content <text>) [--format html|markdown|text|json|code|svg|mermaid|react|notebook] [--source agent] [--tag tag] [--visibility private|team]
|
|
929
|
+
artifacty import --agent claude|codex|gemini|copilot|cursor|auto (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react|notebook] [--tag tag]
|
|
470
930
|
artifacty install claude|codex|gemini|copilot|cursor|all [--dry-run] [--config <path>] [--server-path <path>] [--url http://127.0.0.1:8787] [--mcp-url http://127.0.0.1:8787/mcp] [--api-token token] [--transport local|bridge] [--timeout 30000]
|
|
471
931
|
artifacty check [--server-path <path>] [--timeout 5000]
|
|
472
|
-
artifacty update <id> (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react]
|
|
932
|
+
artifacty update <id> (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react|notebook] [--expected-version N] [--visibility private|team]
|
|
473
933
|
artifacty archive <id>
|
|
474
934
|
artifacty restore <id>
|
|
935
|
+
artifacty visibility <id> private|team
|
|
475
936
|
artifacty audit [--artifact <id>] [--limit 100]
|
|
476
|
-
artifacty index rebuild
|
|
937
|
+
artifacty index rebuild [--embeddings]
|
|
477
938
|
artifacty integrity
|
|
478
|
-
artifacty
|
|
479
|
-
artifacty
|
|
480
|
-
artifacty
|
|
939
|
+
artifacty retention show
|
|
940
|
+
artifacty retention set [--archive-after-days N] [--archive-after-days-for type=N] [--purge-archived-after-days N] [--audit-retention-days N] [--event-retention-rows N] [--keep-tag tag]
|
|
941
|
+
artifacty retention run [--dry-run] [--allow-purge]
|
|
942
|
+
artifacty export --file <path> [--full]
|
|
943
|
+
artifacty backup [--file <path>] [--full]
|
|
944
|
+
artifacty import-store --file <path> [--confirm replace-all] [--force-users]
|
|
481
945
|
artifacty users import --file <path.csv> [--no-password-reset]
|
|
482
946
|
artifacty service plist|unit|task|install|uninstall [--platform macos|linux|windows] [--dry-run] [--path <path>] [--mcp-http]
|
|
483
|
-
artifacty list [--query text] [--tag tag] [--source agent] [--limit 50] [--offset 0] [--include-archived]
|
|
484
|
-
artifacty
|
|
947
|
+
artifacty list [--query text] [--tag tag] [--source agent] [--type <artifactType>] [--publisher <id>] [--created-after <iso-date>] [--created-before <iso-date>] [--review-status none|pending|changes-requested|approved] [--related-to <id>] [--relation derived-from|supersedes|reviews|references|part-of] [--mode keyword|semantic|hybrid] [--view <name|id>] [--group-by artifactType|sourceAgent|day] [--limit 50] [--offset 0] [--include-archived]
|
|
948
|
+
artifacty views
|
|
949
|
+
artifacty views save <name> [--shared] [--query text] [--tag tag] [--source agent] [--type <artifactType>] [--publisher <id>] [--created-after <iso-date>] [--created-before <iso-date>] [--review-status none|pending|changes-requested|approved] [--related-to <id>] [--relation derived-from|supersedes|reviews|references|part-of] [--include-archived]
|
|
950
|
+
artifacty views delete <id>
|
|
951
|
+
artifacty show <id> [--version n] [--raw] [--relations]
|
|
952
|
+
artifacty link <from> <relation> <to>
|
|
953
|
+
artifacty unlink <from> <relation> <to>
|
|
954
|
+
artifacty relations <id> [--direction out|in|both] [--relation derived-from|supersedes|reviews|references|part-of]
|
|
955
|
+
artifacty comment <id> --body "..." [--version n] [--line n] [--parent commentId]
|
|
956
|
+
artifacty comments <id> [--version n] [--status open|resolved]
|
|
957
|
+
artifacty resolve-comment <id> <commentId>
|
|
958
|
+
artifacty review-status <id> none|pending|changes-requested|approved
|
|
959
|
+
artifacty diff <id> [--from N] [--to M] [--structured] [--json]
|
|
960
|
+
artifacty watch [--tag tag] [--artifact <id>] [--type artifact.updated] [--json] [--once] [--exec "<cmd>"]
|
|
485
961
|
|
|
486
962
|
Environment:
|
|
487
963
|
ARTIFACTY_HOME Storage directory. Defaults to ~/.artifacty
|
|
488
964
|
ARTIFACTY_URL Public URL override. Otherwise CLI/MCP read the last running server URL
|
|
965
|
+
ARTIFACTY_HOST Bind host for the HTTP server. Defaults to 127.0.0.1
|
|
966
|
+
ARTIFACTY_PORT Bind port for the HTTP server. Defaults to 8787
|
|
489
967
|
ARTIFACTY_MCP_URL Central MCP HTTP endpoint used by bridge mode
|
|
490
968
|
ARTIFACTY_MCP_MODE local or bridge. bridge forwards stdio MCP to ARTIFACTY_MCP_URL
|
|
969
|
+
ARTIFACTY_MCP_HTTP Set true to also expose MCP over HTTP on the running server
|
|
970
|
+
ARTIFACTY_MCP_TIMEOUT_MS Bridge-mode MCP HTTP request timeout in ms. Defaults to 30000
|
|
491
971
|
ARTIFACTY_API_TOKEN Required token for HTTP API and LAN mode
|
|
972
|
+
ARTIFACTY_LOCALE Default UI locale (e.g. en, ko) when a request does not specify one
|
|
973
|
+
ARTIFACTY_EMBEDDINGS_URL openai-compatible embeddings endpoint base URL, enables semantic/hybrid search
|
|
974
|
+
ARTIFACTY_EMBEDDINGS_MODEL Embeddings model name (defaults per provider)
|
|
975
|
+
ARTIFACTY_EMBEDDINGS_API_KEY API key for the openai-compatible provider, never logged or stored
|
|
976
|
+
ARTIFACTY_EMBEDDINGS_COMMAND Local command that reads JSON lines on stdin and writes vectors on stdout
|
|
977
|
+
ARTIFACTY_EMBEDDINGS_MAX_CHARS Max content characters embedded per artifact (default 8000)
|
|
978
|
+
ARTIFACTY_EMBEDDINGS_TIMEOUT_MS Timeout in ms for the local embeddings command. Defaults to 30000
|
|
979
|
+
ARTIFACTY_EMBEDDINGS_MAX_CANDIDATES Max embedding rows scored per semantic/hybrid search. Defaults to 20000
|
|
980
|
+
ARTIFACTY_EMBEDDINGS_SYNC Set true to index embeddings synchronously (tests only)
|
|
492
981
|
ARTIFACTY_SHARE_MODE Use lan or team before binding outside localhost
|
|
982
|
+
ARTIFACTY_TEAM_WRITE Set owner to restrict writes on team-visibility artifacts to the owner or an admin
|
|
493
983
|
ARTIFACTY_ALLOW_SECRETS Set true only to intentionally store detected secrets
|
|
984
|
+
ARTIFACTY_ENABLE_REACT_RENDERER Set true to execute React artifacts in a sandboxed frame instead of source-only
|
|
985
|
+
ARTIFACTY_RETENTION_INTERVAL_MS Background retention sweep interval in ms. Defaults to 3600000 (1 hour)
|
|
986
|
+
ARTIFACTY_RETENTION_ALLOW_PURGE Set true to allow retention sweeps to hard-delete archived artifacts
|
|
987
|
+
ARTIFACTY_WEBHOOK_TIMEOUT_MS Per-attempt webhook delivery timeout in ms. Defaults to 10000
|
|
988
|
+
ARTIFACTY_WEBHOOK_ALLOW_PRIVATE Set true to allow webhook URLs that resolve to private/loopback addresses
|
|
989
|
+
ARTIFACTY_EVENT_POLL_MS SSE poll interval in ms for new events. Defaults to 1000
|
|
990
|
+
ARTIFACTY_EVENT_HISTORY Max in-memory event history retained for replay. Defaults to 10000
|
|
991
|
+
ARTIFACTY_SSE_MAX_CLIENTS Max concurrent SSE connections. Defaults to 64
|
|
992
|
+
ARTIFACTY_MAX_WAITS Max concurrent artifacty_wait MCP long-polls. Defaults to 64
|
|
993
|
+
ARTIFACTY_MAX_DIFF_ENTRIES Max diff entries computed per artifacty diff. Defaults to 5000
|
|
994
|
+
ARTIFACTY_MAX_INLINE_DIAGRAMS Max inline Mermaid diagrams rendered per Markdown artifact. Defaults to 20
|
|
995
|
+
ARTIFACTY_MAX_COMMENTS_PER_ARTIFACT Max comments retained per artifact. Defaults to 2000
|
|
996
|
+
ARTIFACTY_MAX_SAVED_VIEWS_PER_USER Max saved views retained per user. Defaults to 100
|
|
997
|
+
ARTIFACTY_RATE_LIMIT always or off. Overrides the default (disabled on loopback, enabled otherwise)
|
|
998
|
+
ARTIFACTY_RATE_WRITE_PER_MIN Rate limit for mutating routes. Defaults to 120/min
|
|
999
|
+
ARTIFACTY_RATE_AUTH_PER_MIN Rate limit for /login per address. Defaults to 10/min
|
|
1000
|
+
ARTIFACTY_RATE_SEARCH_PER_MIN Rate limit for GET /api/artifacts?q= searches. Defaults to 300/min
|
|
494
1001
|
`);
|
|
495
1002
|
}
|
|
496
1003
|
|