@absolutejs/artifacts 0.0.1 → 0.0.3

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/dist/index.js CHANGED
@@ -4,6 +4,18 @@ import { Value } from "@sinclair/typebox/value";
4
4
 
5
5
  // src/types.ts
6
6
  var ARTIFACT_STATUSES = ["draft", "published", "archived"];
7
+ var ARTIFACT_EVENT_TYPES = [
8
+ "artifact.archived",
9
+ "artifact.asset_attached",
10
+ "artifact.asset_detached",
11
+ "artifact.created",
12
+ "artifact.generated",
13
+ "artifact.indexing_changed",
14
+ "artifact.published",
15
+ "artifact.restored",
16
+ "artifact.revised",
17
+ "artifact.unpublished"
18
+ ];
7
19
 
8
20
  class ArtifactError extends Error {
9
21
  code;
@@ -31,6 +43,108 @@ var defineArtifactRegistry = (definitions) => ({
31
43
  return content;
32
44
  }
33
45
  });
46
+ // src/generators.ts
47
+ var createArtifactGeneratorRegistry = (initial = []) => {
48
+ const generators = new Map(initial.map((generator) => [generator.kind, generator]));
49
+ return {
50
+ generate: async (service, input) => {
51
+ const generator = generators.get(input.kind);
52
+ if (!generator) {
53
+ throw new ArtifactError("generator_unavailable", `No generator is registered for ${input.kind} artifacts`);
54
+ }
55
+ const result = await generator.generate(input, {
56
+ ownerId: input.ownerId
57
+ });
58
+ const artifact = await service.createBundle(input.ownerId, {
59
+ assets: result.assets,
60
+ content: result.content,
61
+ createdBy: input.createdBy,
62
+ kind: input.kind,
63
+ metadata: {
64
+ ...result.metadata,
65
+ ...result.warnings?.length ? { generationWarnings: result.warnings } : {}
66
+ },
67
+ provenance: result.provenance,
68
+ title: result.title ?? input.title ?? `Generated ${input.kind}`
69
+ });
70
+ return artifact;
71
+ },
72
+ kinds: () => [...generators.keys()],
73
+ register: (generator) => {
74
+ generators.set(generator.kind, generator);
75
+ }
76
+ };
77
+ };
78
+ // src/standardKinds.ts
79
+ import { Type } from "@sinclair/typebox";
80
+ var FileArtifactContentSchema = Type.Object({
81
+ description: Type.Optional(Type.String()),
82
+ instructions: Type.Optional(Type.String()),
83
+ summary: Type.Optional(Type.String())
84
+ });
85
+ var capabilities = [
86
+ "attach",
87
+ "archive",
88
+ "edit",
89
+ "export",
90
+ "preview",
91
+ "refine"
92
+ ];
93
+ var fileKind = (label, acceptedMediaTypes, maxCount = 1) => ({
94
+ assets: { acceptedMediaTypes, maxCount },
95
+ capabilities: [...capabilities],
96
+ content: FileArtifactContentSchema,
97
+ label,
98
+ schemaVersion: 1
99
+ });
100
+ var standardArtifactDefinitions = {
101
+ archive: fileKind("Archive", [
102
+ "application/gzip",
103
+ "application/vnd.rar",
104
+ "application/x-7z-compressed",
105
+ "application/x-bzip2",
106
+ "application/x-tar",
107
+ "application/zip"
108
+ ], 20),
109
+ audio: fileKind("Audio", ["audio/*"]),
110
+ code: fileKind("Code", ["application/json", "application/xml", "text/*"], 100),
111
+ dataset: fileKind("Dataset", [
112
+ "application/json",
113
+ "application/x-ndjson",
114
+ "application/xml",
115
+ "text/csv",
116
+ "text/tab-separated-values",
117
+ "text/plain",
118
+ "text/yaml"
119
+ ], 20),
120
+ document: fileKind("Document", [
121
+ "application/epub+zip",
122
+ "application/msword",
123
+ "application/pdf",
124
+ "application/rtf",
125
+ "application/vnd.oasis.opendocument.text",
126
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
127
+ "text/*"
128
+ ], 20),
129
+ email: fileKind("Email", ["application/mbox", "application/vnd.ms-outlook", "message/*", "text/*"], 100),
130
+ file: fileKind("File", ["*/*"], 100),
131
+ image: fileKind("Image", ["image/*"], 20),
132
+ presentation: fileKind("Presentation", [
133
+ "application/pdf",
134
+ "application/vnd.ms-powerpoint",
135
+ "application/vnd.oasis.opendocument.presentation",
136
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation"
137
+ ], 20),
138
+ spreadsheet: fileKind("Spreadsheet", [
139
+ "application/vnd.ms-excel",
140
+ "application/vnd.oasis.opendocument.spreadsheet",
141
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
142
+ "text/csv",
143
+ "text/tab-separated-values"
144
+ ], 20),
145
+ video: fileKind("Video", ["video/*"])
146
+ };
147
+ var STANDARD_ARTIFACT_KIND_NAMES = Object.keys(standardArtifactDefinitions);
34
148
  // src/renderers.ts
35
149
  var createArtifactRendererRegistry = (initial = []) => {
36
150
  const key = (kind, format) => `${kind}:${format}`;
@@ -55,84 +169,327 @@ var requireCapability = (artifact, capability) => {
55
169
  throw new ArtifactError("unsupported_capability", `${artifact.kind} artifacts do not support ${capability}`);
56
170
  }
57
171
  };
172
+ var mediaTypeMatches = (accepted, actual) => {
173
+ if (accepted === "*/*" || accepted === actual)
174
+ return true;
175
+ if (!accepted.endsWith("/*"))
176
+ return false;
177
+ return actual.startsWith(accepted.slice(0, -1));
178
+ };
58
179
  var createArtifactService = (options) => {
59
180
  const now = () => (options.clock ?? (() => new Date))().toISOString();
60
181
  const idFactory = options.idFactory ?? (() => crypto.randomUUID());
182
+ const eventIdFactory = options.eventIdFactory ?? (() => crypto.randomUUID());
183
+ const event = (artifact, type, payload) => ({
184
+ artifactId: artifact.id,
185
+ createdAt: now(),
186
+ id: eventIdFactory(),
187
+ ownerId: artifact.ownerId,
188
+ payload,
189
+ revision: artifact.revision,
190
+ type
191
+ });
192
+ const validateAssets = (kind, assets) => {
193
+ const policy = options.registry.definitions[kind]?.assets;
194
+ if (!policy) {
195
+ if (assets.length > 0) {
196
+ throw new ArtifactError("invalid_content", `${kind} artifacts do not accept file assets`);
197
+ }
198
+ return assets;
199
+ }
200
+ if (policy.maxCount !== undefined && assets.length > policy.maxCount) {
201
+ throw new ArtifactError("invalid_content", `${kind} artifacts accept at most ${policy.maxCount} file assets`);
202
+ }
203
+ const rejected = assets.find((asset) => policy.acceptedMediaTypes?.length && !policy.acceptedMediaTypes.some((accepted) => mediaTypeMatches(accepted, asset.mediaType)));
204
+ if (rejected) {
205
+ throw new ArtifactError("invalid_content", `${rejected.mediaType} is not accepted by ${kind} artifacts`);
206
+ }
207
+ return assets;
208
+ };
209
+ const validateAssetInputs = (kind, inputs, currentCount = 0) => {
210
+ const policy = options.registry.definitions[kind]?.assets;
211
+ if (!policy && inputs.length > 0) {
212
+ throw new ArtifactError("invalid_content", `${kind} artifacts do not accept file assets`);
213
+ }
214
+ if (policy?.maxCount !== undefined && currentCount + inputs.length > policy.maxCount) {
215
+ throw new ArtifactError("invalid_content", `${kind} artifacts accept at most ${policy.maxCount} file assets`);
216
+ }
217
+ const rejected = inputs.find((input) => policy?.acceptedMediaTypes?.length && !policy.acceptedMediaTypes.some((accepted) => mediaTypeMatches(accepted, input.mediaType)));
218
+ if (rejected) {
219
+ throw new ArtifactError("invalid_content", `${rejected.mediaType} is not accepted by ${kind} artifacts`);
220
+ }
221
+ };
222
+ const buildRecord = (ownerId, input, assets) => {
223
+ const definition = options.registry.definitions[input.kind];
224
+ if (!definition) {
225
+ throw new ArtifactError("unknown_kind", `Unknown artifact kind: ${input.kind}`);
226
+ }
227
+ const timestamp = now();
228
+ const artifact = {
229
+ assets: validateAssets(input.kind, assets),
230
+ capabilities: definition.capabilities ?? ["archive", "edit", "preview"],
231
+ content: options.registry.parse(input.kind, input.content),
232
+ createdAt: timestamp,
233
+ createdBy: input.createdBy,
234
+ id: idFactory(),
235
+ kind: input.kind,
236
+ metadata: input.metadata ?? {},
237
+ ownerId,
238
+ provenance: input.provenance,
239
+ revision: 1,
240
+ schemaVersion: definition.schemaVersion ?? 1,
241
+ status: "draft",
242
+ title: input.title.trim(),
243
+ updatedAt: timestamp
244
+ };
245
+ return artifact;
246
+ };
61
247
  const get = async (ownerId, artifactId) => {
62
248
  const artifact = await options.store.get(ownerId, artifactId);
63
- if (!artifact) {
249
+ if (!artifact)
64
250
  throw new ArtifactError("not_found", "Artifact not found");
65
- }
66
251
  return artifact;
67
252
  };
68
- const saveRevision = async (artifact, expectedRevision) => {
69
- const saved = await options.store.save(artifact, expectedRevision);
253
+ const saveRevision = async (artifact, expectedRevision, type, payload) => {
254
+ const saved = await options.store.save(artifact, expectedRevision, [
255
+ event(artifact, type, payload)
256
+ ]);
70
257
  if (!saved) {
71
258
  throw new ArtifactError("conflict", "Artifact changed since it was opened; reload before saving");
72
259
  }
73
260
  return artifact;
74
261
  };
75
- return {
262
+ const requireAssetTransactions = () => {
263
+ if (!options.assetStore?.stage) {
264
+ throw new ArtifactError("asset_transaction_unavailable", "The configured artifact asset store does not support atomic bundles");
265
+ }
266
+ return options.assetStore;
267
+ };
268
+ const service = {
76
269
  archive: async (ownerId, artifactId) => {
77
270
  const current = await get(ownerId, artifactId);
78
271
  requireCapability(current, "archive");
79
- return saveRevision({
272
+ const archived = {
80
273
  ...current,
81
274
  revision: current.revision + 1,
82
275
  status: "archived",
83
276
  updatedAt: now()
84
- }, current.revision);
277
+ };
278
+ return saveRevision(archived, current.revision, "artifact.archived");
279
+ },
280
+ attach: async (ownerId, artifactId, input, expectedRevision) => {
281
+ const current = await get(ownerId, artifactId);
282
+ requireCapability(current, "attach");
283
+ validateAssetInputs(current.kind, [input], current.assets.length);
284
+ if (!options.assetStore) {
285
+ throw new ArtifactError("asset_store_unavailable", "No artifact asset store is configured");
286
+ }
287
+ const reference = await options.assetStore.write(input, {
288
+ artifact: current,
289
+ idempotencyKey: `artifact:${current.id}:asset:${current.revision + 1}`
290
+ });
291
+ return saveRevision({
292
+ ...current,
293
+ assets: validateAssets(current.kind, [...current.assets, reference]),
294
+ revision: current.revision + 1,
295
+ updatedAt: now()
296
+ }, expectedRevision ?? current.revision, "artifact.asset_attached", { assetIds: [reference.id] });
297
+ },
298
+ attachBundle: async (ownerId, artifactId, inputs, expectedRevision) => {
299
+ const current = await get(ownerId, artifactId);
300
+ requireCapability(current, "attach");
301
+ validateAssetInputs(current.kind, inputs, current.assets.length);
302
+ const assetStore = requireAssetTransactions();
303
+ const transaction = await assetStore.stage(inputs, {
304
+ artifact: current,
305
+ idempotencyKey: `artifact:${current.id}:bundle:${current.revision + 1}`
306
+ });
307
+ const assets = validateAssets(current.kind, [
308
+ ...current.assets,
309
+ ...transaction.references
310
+ ]);
311
+ const revised = {
312
+ ...current,
313
+ assets,
314
+ revision: current.revision + 1,
315
+ updatedAt: now()
316
+ };
317
+ try {
318
+ await transaction.commit();
319
+ return await saveRevision(revised, expectedRevision ?? current.revision, "artifact.asset_attached", { assetIds: transaction.references.map((asset) => asset.id) });
320
+ } catch (error) {
321
+ await transaction.rollback();
322
+ throw error;
323
+ }
324
+ },
325
+ collectAssetGarbage: async (input) => {
326
+ if (!options.assetStore) {
327
+ throw new ArtifactError("asset_store_unavailable", "No artifact asset store is configured");
328
+ }
329
+ const referenced = new Set(await options.store.listReferencedAssetIds());
330
+ const cutoff = Date.now() - (input.minimumAgeMs ?? 0);
331
+ const candidates = await options.assetStore.listCandidates();
332
+ const deleted = [];
333
+ const retained = [];
334
+ for (const candidate of candidates) {
335
+ if (referenced.has(candidate.reference.id) || new Date(candidate.createdAt).getTime() > cutoff) {
336
+ retained.push(candidate.reference);
337
+ } else {
338
+ deleted.push(candidate.reference);
339
+ if (!input.dryRun)
340
+ await options.assetStore.delete(candidate.reference);
341
+ }
342
+ }
343
+ return { deleted, retained };
85
344
  },
86
345
  create: async (ownerId, input) => {
87
- const definition = options.registry.definitions[input.kind];
88
- if (!definition) {
89
- throw new ArtifactError("unknown_kind", `Unknown artifact kind: ${input.kind}`);
346
+ const artifact = buildRecord(ownerId, input, input.assets ?? []);
347
+ await options.store.create(artifact, [
348
+ event(artifact, "artifact.created")
349
+ ]);
350
+ return artifact;
351
+ },
352
+ createBundle: async (ownerId, input) => {
353
+ const { assets: assetInputs = [], ...createInput } = input;
354
+ validateAssetInputs(input.kind, assetInputs);
355
+ if (assetInputs.length === 0) {
356
+ return service.create(ownerId, createInput);
90
357
  }
91
- const content = options.registry.parse(input.kind, input.content);
92
- const timestamp = now();
358
+ const assetStore = requireAssetTransactions();
359
+ const provisional = buildRecord(ownerId, createInput, []);
360
+ const transaction = await assetStore.stage(assetInputs, {
361
+ artifact: provisional,
362
+ idempotencyKey: `artifact:${provisional.id}:bundle:1`
363
+ });
93
364
  const artifact = {
94
- capabilities: definition.capabilities ?? ["archive", "edit", "preview"],
95
- content,
96
- createdAt: timestamp,
97
- createdBy: input.createdBy,
98
- id: idFactory(),
99
- kind: input.kind,
100
- metadata: input.metadata ?? {},
101
- ownerId,
102
- provenance: input.provenance,
103
- revision: 1,
104
- schemaVersion: definition.schemaVersion ?? 1,
105
- status: "draft",
106
- title: input.title.trim(),
107
- updatedAt: timestamp
365
+ ...provisional,
366
+ assets: validateAssets(input.kind, transaction.references)
108
367
  };
109
- await options.store.create(artifact);
110
- return artifact;
368
+ try {
369
+ await transaction.commit();
370
+ await options.store.create(artifact, [
371
+ event(artifact, "artifact.created"),
372
+ event(artifact, "artifact.generated", {
373
+ assetIds: transaction.references.map((asset) => asset.id)
374
+ })
375
+ ]);
376
+ return artifact;
377
+ } catch (error) {
378
+ await transaction.rollback();
379
+ throw error;
380
+ }
381
+ },
382
+ detach: async (ownerId, artifactId, assetId, expectedRevision) => {
383
+ const current = await get(ownerId, artifactId);
384
+ requireCapability(current, "attach");
385
+ const assets = current.assets.filter((asset) => asset.id !== assetId);
386
+ if (assets.length === current.assets.length) {
387
+ throw new ArtifactError("not_found", "Artifact asset not found");
388
+ }
389
+ return saveRevision({
390
+ ...current,
391
+ assets,
392
+ revision: current.revision + 1,
393
+ updatedAt: now()
394
+ }, expectedRevision ?? current.revision, "artifact.asset_detached", { assetId });
111
395
  },
112
396
  get,
397
+ getIndexingState: (ownerId, artifactId) => options.store.getIndexingState(ownerId, artifactId),
398
+ getRevision: async (ownerId, artifactId, revision) => {
399
+ const snapshot = await options.store.getRevision(ownerId, artifactId, revision);
400
+ if (!snapshot) {
401
+ throw new ArtifactError("not_found", "Artifact revision not found");
402
+ }
403
+ return snapshot;
404
+ },
113
405
  list: (ownerId, query) => options.store.list(ownerId, query),
114
- publish: async (ownerId, artifactId) => {
406
+ listEvents: (query) => options.store.listEvents(query),
407
+ listRevisions: (ownerId, artifactId) => options.store.listRevisions(ownerId, artifactId),
408
+ markEventProcessed: (eventId, processedAt = now()) => options.store.markEventProcessed(eventId, processedAt),
409
+ markIndexing: async (ownerId, artifactId, input) => {
410
+ const artifact = await get(ownerId, artifactId);
411
+ const state = {
412
+ artifactId,
413
+ documentIds: input.documentIds ?? [],
414
+ error: input.error,
415
+ indexedAt: input.status === "indexed" ? now() : undefined,
416
+ revision: input.revision,
417
+ status: input.status,
418
+ updatedAt: now()
419
+ };
420
+ await options.store.putIndexingState(ownerId, state, [
421
+ event(artifact, "artifact.indexing_changed", {
422
+ indexingRevision: state.revision,
423
+ indexingStatus: state.status
424
+ })
425
+ ]);
426
+ return state;
427
+ },
428
+ publish: async (ownerId, artifactId, input = {}) => {
115
429
  const current = await get(ownerId, artifactId);
116
430
  requireCapability(current, "publish");
117
431
  if (!options.publisher) {
118
432
  throw new ArtifactError("publisher_unavailable", "No artifact publisher is configured");
119
433
  }
434
+ const mode = input.mode ?? "pinned";
435
+ const publishedRevision = current.revision;
120
436
  const result = await options.publisher.publish(current, {
121
- idempotencyKey: `artifact:${current.id}:publish:${current.revision + 1}`
437
+ idempotencyKey: `artifact:${current.id}:publish:${publishedRevision}:${mode}`,
438
+ mode,
439
+ revision: publishedRevision
122
440
  });
123
441
  const publishedAt = now();
124
442
  const publication = {
125
443
  id: result.id,
444
+ mode,
126
445
  publishedAt,
446
+ revision: publishedRevision,
127
447
  url: result.url
128
448
  };
129
- return saveRevision({
449
+ const published = {
130
450
  ...current,
131
451
  publication,
132
452
  revision: current.revision + 1,
133
453
  status: "published",
134
454
  updatedAt: publishedAt
135
- }, current.revision);
455
+ };
456
+ return saveRevision(published, current.revision, "artifact.published", {
457
+ mode,
458
+ publishedRevision
459
+ });
460
+ },
461
+ readAsset: async (ownerId, artifactId, assetId) => {
462
+ const artifact = await get(ownerId, artifactId);
463
+ const asset = artifact.assets.find((candidate) => candidate.id === assetId);
464
+ if (!asset)
465
+ throw new ArtifactError("not_found", "Artifact asset not found");
466
+ if (!options.assetStore) {
467
+ throw new ArtifactError("asset_store_unavailable", "No artifact asset store is configured");
468
+ }
469
+ return {
470
+ asset,
471
+ data: await options.assetStore.read(asset, { artifact })
472
+ };
473
+ },
474
+ restore: async (ownerId, artifactId, revision, expectedRevision) => {
475
+ const current = await get(ownerId, artifactId);
476
+ requireCapability(current, "edit");
477
+ const snapshot = await options.store.getRevision(ownerId, artifactId, revision);
478
+ if (!snapshot) {
479
+ throw new ArtifactError("not_found", "Artifact revision not found");
480
+ }
481
+ return saveRevision({
482
+ ...current,
483
+ assets: validateAssets(current.kind, snapshot.assets),
484
+ content: options.registry.parse(current.kind, snapshot.content),
485
+ metadata: snapshot.metadata,
486
+ publication: undefined,
487
+ provenance: snapshot.provenance,
488
+ revision: current.revision + 1,
489
+ status: "draft",
490
+ title: snapshot.title,
491
+ updatedAt: now()
492
+ }, expectedRevision ?? current.revision, "artifact.restored", { restoredRevision: revision });
136
493
  },
137
494
  unpublish: async (ownerId, artifactId) => {
138
495
  const current = await get(ownerId, artifactId);
@@ -149,51 +506,198 @@ var createArtifactService = (options) => {
149
506
  revision: current.revision + 1,
150
507
  status: "draft",
151
508
  updatedAt: now()
152
- }, current.revision);
509
+ }, current.revision, "artifact.unpublished");
153
510
  },
154
511
  update: async (ownerId, artifactId, input) => {
155
512
  const current = await get(ownerId, artifactId);
156
513
  requireCapability(current, "edit");
157
- const expectedRevision = input.expectedRevision ?? current.revision;
158
- const content = input.content === undefined ? current.content : options.registry.parse(current.kind, input.content);
514
+ const nextRevision = current.revision + 1;
515
+ const publication = current.publication?.mode === "live" ? { ...current.publication, revision: nextRevision } : current.publication;
159
516
  return saveRevision({
160
517
  ...current,
161
- content,
518
+ assets: input.assets === undefined ? current.assets : validateAssets(current.kind, input.assets),
519
+ content: input.content === undefined ? current.content : options.registry.parse(current.kind, input.content),
162
520
  metadata: input.metadata ?? current.metadata,
163
- revision: current.revision + 1,
521
+ publication,
522
+ revision: nextRevision,
164
523
  title: input.title?.trim() || current.title,
165
524
  updatedAt: now()
166
- }, expectedRevision);
525
+ }, input.expectedRevision ?? current.revision, "artifact.revised");
167
526
  }
168
527
  };
528
+ return service;
169
529
  };
170
530
  // src/store.ts
531
+ import { createHash } from "crypto";
171
532
  var clone = (value) => structuredClone(value);
533
+ var createMemoryArtifactAssetStore = () => {
534
+ const bytes = new Map;
535
+ const references = new Map;
536
+ const idempotency = new Map;
537
+ return {
538
+ delete: async (reference) => {
539
+ bytes.delete(reference.id);
540
+ references.delete(reference.id);
541
+ },
542
+ listCandidates: async () => [...references.values()].map((reference) => ({
543
+ createdAt: reference.createdAt,
544
+ reference: clone(reference)
545
+ })),
546
+ read: async (reference) => {
547
+ const data = bytes.get(reference.id);
548
+ if (!data)
549
+ throw new Error(`Artifact asset not found: ${reference.id}`);
550
+ return clone(data);
551
+ },
552
+ write: async (input, context) => {
553
+ const existingId = idempotency.get(context.idempotencyKey);
554
+ if (existingId)
555
+ return clone(references.get(existingId));
556
+ const id = crypto.randomUUID();
557
+ const reference = {
558
+ checksum: {
559
+ algorithm: "sha256",
560
+ value: createHash("sha256").update(input.data).digest("hex")
561
+ },
562
+ createdAt: new Date().toISOString(),
563
+ id,
564
+ mediaType: input.mediaType,
565
+ metadata: input.metadata,
566
+ name: input.name,
567
+ role: input.role ?? "attachment",
568
+ size: input.data.byteLength,
569
+ uri: `memory://${id}`
570
+ };
571
+ bytes.set(id, clone(input.data));
572
+ references.set(id, clone(reference));
573
+ idempotency.set(context.idempotencyKey, id);
574
+ return reference;
575
+ },
576
+ stage: async (inputs, context) => {
577
+ const staged = inputs.map((input, index) => {
578
+ const key = `${context.idempotencyKey}:${index}`;
579
+ const existingId = idempotency.get(key);
580
+ if (existingId) {
581
+ return {
582
+ data: bytes.get(existingId),
583
+ key,
584
+ reference: references.get(existingId)
585
+ };
586
+ }
587
+ const id = crypto.randomUUID();
588
+ const reference = {
589
+ checksum: {
590
+ algorithm: "sha256",
591
+ value: createHash("sha256").update(input.data).digest("hex")
592
+ },
593
+ createdAt: new Date().toISOString(),
594
+ id,
595
+ mediaType: input.mediaType,
596
+ metadata: input.metadata,
597
+ name: input.name,
598
+ role: input.role ?? "attachment",
599
+ size: input.data.byteLength,
600
+ uri: `memory://${id}`
601
+ };
602
+ return { data: clone(input.data), key, reference };
603
+ });
604
+ return {
605
+ commit: async () => {
606
+ for (const item of staged) {
607
+ bytes.set(item.reference.id, clone(item.data));
608
+ references.set(item.reference.id, clone(item.reference));
609
+ idempotency.set(item.key, item.reference.id);
610
+ }
611
+ },
612
+ references: staged.map((item) => clone(item.reference)),
613
+ rollback: async () => {
614
+ for (const item of staged) {
615
+ bytes.delete(item.reference.id);
616
+ references.delete(item.reference.id);
617
+ idempotency.delete(item.key);
618
+ }
619
+ }
620
+ };
621
+ }
622
+ };
623
+ };
172
624
  var createMemoryArtifactStore = (initial = []) => {
173
625
  const records = new Map(initial.map((record) => [record.id, clone(record)]));
626
+ const revisions = new Map;
627
+ const events = new Map;
628
+ const indexing = new Map;
629
+ for (const record of initial)
630
+ revisions.set(record.id, [clone(record)]);
174
631
  return {
175
- create: async (record) => {
632
+ create: async (record, newEvents = []) => {
176
633
  if (records.has(record.id))
177
634
  throw new Error(`Duplicate artifact id: ${record.id}`);
178
635
  records.set(record.id, clone(record));
636
+ revisions.set(record.id, [clone(record)]);
637
+ for (const event of newEvents)
638
+ events.set(event.id, clone(event));
179
639
  },
180
640
  get: async (ownerId, artifactId) => {
181
641
  const record = records.get(artifactId);
182
642
  return record?.ownerId === ownerId ? clone(record) : null;
183
643
  },
644
+ getIndexingState: async (ownerId, artifactId) => {
645
+ const record = records.get(artifactId);
646
+ if (record?.ownerId !== ownerId)
647
+ return null;
648
+ return clone(indexing.get(artifactId) ?? null);
649
+ },
650
+ getRevision: async (ownerId, artifactId, revision) => {
651
+ const current = records.get(artifactId);
652
+ if (current?.ownerId !== ownerId)
653
+ return null;
654
+ return clone(revisions.get(artifactId)?.find((candidate) => candidate.revision === revision) ?? null);
655
+ },
184
656
  list: async (ownerId, query = {}) => [...records.values()].filter((record) => record.ownerId === ownerId && (!query.kind || record.kind === query.kind) && (!query.status || record.status === query.status)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, query.limit ?? Number.POSITIVE_INFINITY).map(clone),
185
- save: async (record, expectedRevision) => {
657
+ listRevisions: async (ownerId, artifactId) => {
658
+ const current = records.get(artifactId);
659
+ if (current?.ownerId !== ownerId)
660
+ return [];
661
+ return (revisions.get(artifactId) ?? []).toSorted((left, right) => right.revision - left.revision).map(clone);
662
+ },
663
+ listEvents: async (query = {}) => [...events.values()].filter((event) => (query.processed === undefined || Boolean(event.processedAt) === query.processed) && (!query.type || event.type === query.type)).sort((left, right) => left.createdAt.localeCompare(right.createdAt)).slice(0, query.limit ?? Number.POSITIVE_INFINITY).map(clone),
664
+ listReferencedAssetIds: async () => [
665
+ ...new Set([...revisions.values()].flat().flatMap((revision) => revision.assets.map((asset) => asset.id)))
666
+ ],
667
+ markEventProcessed: async (eventId, processedAt) => {
668
+ const event = events.get(eventId);
669
+ if (!event)
670
+ return false;
671
+ events.set(eventId, { ...event, processedAt });
672
+ return true;
673
+ },
674
+ putIndexingState: async (ownerId, state, newEvents = []) => {
675
+ const record = records.get(state.artifactId);
676
+ if (record?.ownerId !== ownerId) {
677
+ throw new Error("Artifact not found");
678
+ }
679
+ indexing.set(state.artifactId, clone(state));
680
+ for (const event of newEvents)
681
+ events.set(event.id, clone(event));
682
+ },
683
+ save: async (record, expectedRevision, newEvents = []) => {
186
684
  const current = records.get(record.id);
187
685
  if (!current || current.ownerId !== record.ownerId || current.revision !== expectedRevision) {
188
686
  return false;
189
687
  }
190
688
  records.set(record.id, clone(record));
689
+ revisions.set(record.id, [
690
+ ...revisions.get(record.id) ?? [],
691
+ clone(record)
692
+ ]);
693
+ for (const event of newEvents)
694
+ events.set(event.id, clone(event));
191
695
  return true;
192
696
  }
193
697
  };
194
698
  };
195
699
  // src/tools.ts
196
- import { Type } from "@sinclair/typebox";
700
+ import { Type as Type2 } from "@sinclair/typebox";
197
701
  var record = (input) => input && typeof input === "object" && !Array.isArray(input) ? input : {};
198
702
  var stringValue = (input, key) => typeof input[key] === "string" ? input[key] : undefined;
199
703
  var createArtifactTools = (options) => ({
@@ -214,10 +718,10 @@ var createArtifactTools = (options) => ({
214
718
  });
215
719
  return JSON.stringify(artifact);
216
720
  },
217
- input: Type.Object({
218
- content: Type.Unknown(),
219
- kind: Type.String({ minLength: 1 }),
220
- title: Type.String({ minLength: 1 })
721
+ input: Type2.Object({
722
+ content: Type2.Unknown(),
723
+ kind: Type2.String({ minLength: 1 }),
724
+ title: Type2.String({ minLength: 1 })
221
725
  })
222
726
  },
223
727
  artifact_get: {
@@ -229,7 +733,7 @@ var createArtifactTools = (options) => ({
229
733
  return "Provide artifactId.";
230
734
  return JSON.stringify(await options.service.get(options.ownerId, artifactId));
231
735
  },
232
- input: Type.Object({ artifactId: Type.String({ minLength: 1 }) })
736
+ input: Type2.Object({ artifactId: Type2.String({ minLength: 1 }) })
233
737
  },
234
738
  artifact_list: {
235
739
  annotations: { readOnlyHint: true },
@@ -242,11 +746,22 @@ var createArtifactTools = (options) => ({
242
746
  status: ARTIFACT_STATUSES.find((candidate) => candidate === status)
243
747
  }));
244
748
  },
245
- input: Type.Object({
246
- kind: Type.Optional(Type.String()),
247
- status: Type.Optional(Type.Union(ARTIFACT_STATUSES.map((status) => Type.Literal(status))))
749
+ input: Type2.Object({
750
+ kind: Type2.Optional(Type2.String()),
751
+ status: Type2.Optional(Type2.Union(ARTIFACT_STATUSES.map((status) => Type2.Literal(status))))
248
752
  })
249
753
  },
754
+ artifact_history: {
755
+ annotations: { readOnlyHint: true },
756
+ description: "List the immutable revisions of one owned artifact.",
757
+ handler: async (raw) => {
758
+ const artifactId = stringValue(record(raw), "artifactId");
759
+ if (!artifactId)
760
+ return "Provide artifactId.";
761
+ return JSON.stringify(await options.service.listRevisions(options.ownerId, artifactId));
762
+ },
763
+ input: Type2.Object({ artifactId: Type2.String({ minLength: 1 }) })
764
+ },
250
765
  artifact_publish: {
251
766
  description: "Publish or unpublish an artifact. Hosts should expose this tool only when the user explicitly controls public access.",
252
767
  handler: async (raw) => {
@@ -255,12 +770,31 @@ var createArtifactTools = (options) => ({
255
770
  if (!artifactId || typeof input.published !== "boolean") {
256
771
  return "Provide artifactId and published.";
257
772
  }
258
- const artifact = input.published ? await options.service.publish(options.ownerId, artifactId) : await options.service.unpublish(options.ownerId, artifactId);
773
+ const artifact = input.published ? await options.service.publish(options.ownerId, artifactId, {
774
+ mode: input.mode === "live" ? "live" : "pinned"
775
+ }) : await options.service.unpublish(options.ownerId, artifactId);
259
776
  return JSON.stringify(artifact);
260
777
  },
261
- input: Type.Object({
262
- artifactId: Type.String({ minLength: 1 }),
263
- published: Type.Boolean()
778
+ input: Type2.Object({
779
+ artifactId: Type2.String({ minLength: 1 }),
780
+ mode: Type2.Optional(Type2.Union([Type2.Literal("live"), Type2.Literal("pinned")])),
781
+ published: Type2.Boolean()
782
+ })
783
+ },
784
+ artifact_restore: {
785
+ description: "Restore an immutable artifact revision as a new private draft revision.",
786
+ handler: async (raw) => {
787
+ const input = record(raw);
788
+ const artifactId = stringValue(input, "artifactId");
789
+ if (!artifactId || typeof input.revision !== "number") {
790
+ return "Provide artifactId and revision.";
791
+ }
792
+ return JSON.stringify(await options.service.restore(options.ownerId, artifactId, input.revision, typeof input.expectedRevision === "number" ? input.expectedRevision : undefined));
793
+ },
794
+ input: Type2.Object({
795
+ artifactId: Type2.String({ minLength: 1 }),
796
+ expectedRevision: Type2.Optional(Type2.Integer({ minimum: 1 })),
797
+ revision: Type2.Integer({ minimum: 1 })
264
798
  })
265
799
  },
266
800
  artifact_update: {
@@ -277,20 +811,25 @@ var createArtifactTools = (options) => ({
277
811
  });
278
812
  return JSON.stringify(artifact);
279
813
  },
280
- input: Type.Object({
281
- artifactId: Type.String({ minLength: 1 }),
282
- content: Type.Optional(Type.Unknown()),
283
- expectedRevision: Type.Optional(Type.Integer({ minimum: 1 })),
284
- title: Type.Optional(Type.String({ minLength: 1 }))
814
+ input: Type2.Object({
815
+ artifactId: Type2.String({ minLength: 1 }),
816
+ content: Type2.Optional(Type2.Unknown()),
817
+ expectedRevision: Type2.Optional(Type2.Integer({ minimum: 1 })),
818
+ title: Type2.Optional(Type2.String({ minLength: 1 }))
285
819
  })
286
820
  }
287
821
  });
288
822
  export {
823
+ standardArtifactDefinitions,
289
824
  defineArtifactRegistry,
290
825
  createMemoryArtifactStore,
826
+ createMemoryArtifactAssetStore,
291
827
  createArtifactTools,
292
828
  createArtifactService,
293
829
  createArtifactRendererRegistry,
830
+ createArtifactGeneratorRegistry,
831
+ STANDARD_ARTIFACT_KIND_NAMES,
294
832
  ArtifactError,
295
- ARTIFACT_STATUSES
833
+ ARTIFACT_STATUSES,
834
+ ARTIFACT_EVENT_TYPES
296
835
  };