@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/README.md +138 -3
- package/dist/index.js +598 -59
- package/dist/manifest.js +18 -0
- package/dist/manifest.json +50 -0
- package/dist/rag.js +76 -0
- package/dist/src/generators.d.ts +34 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/manifest.d.ts +25 -1
- package/dist/src/rag.d.ts +46 -0
- package/dist/src/registry.d.ts +6 -0
- package/dist/src/service.d.ts +31 -3
- package/dist/src/standardKinds.d.ts +157 -0
- package/dist/src/store.d.ts +33 -3
- package/dist/src/types.d.ts +81 -2
- package/package.json +18 -4
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
|
-
|
|
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
|
-
|
|
272
|
+
const archived = {
|
|
80
273
|
...current,
|
|
81
274
|
revision: current.revision + 1,
|
|
82
275
|
status: "archived",
|
|
83
276
|
updatedAt: now()
|
|
84
|
-
}
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
92
|
-
const
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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:${
|
|
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
|
-
|
|
449
|
+
const published = {
|
|
130
450
|
...current,
|
|
131
451
|
publication,
|
|
132
452
|
revision: current.revision + 1,
|
|
133
453
|
status: "published",
|
|
134
454
|
updatedAt: publishedAt
|
|
135
|
-
}
|
|
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
|
|
158
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
218
|
-
content:
|
|
219
|
-
kind:
|
|
220
|
-
title:
|
|
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:
|
|
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:
|
|
246
|
-
kind:
|
|
247
|
-
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
|
|
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:
|
|
262
|
-
artifactId:
|
|
263
|
-
|
|
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:
|
|
281
|
-
artifactId:
|
|
282
|
-
content:
|
|
283
|
-
expectedRevision:
|
|
284
|
-
title:
|
|
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
|
};
|