artifacty 0.1.2 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import path from "node:path";
@@ -17,7 +17,11 @@ export const ARTIFACT_FORMATS = [
17
17
  "code",
18
18
  "svg",
19
19
  "mermaid",
20
- "react"
20
+ "react",
21
+ "sarif",
22
+ "csv",
23
+ "image",
24
+ "video"
21
25
  ];
22
26
  export const ARTIFACT_TYPES = [
23
27
  "document",
@@ -33,6 +37,8 @@ export const ARTIFACT_TYPES = [
33
37
  "diagram",
34
38
  "component",
35
39
  "snippet",
40
+ "analysis-report",
41
+ "table",
36
42
  "unknown"
37
43
  ];
38
44
 
@@ -44,7 +50,11 @@ const FORMAT_TO_EXTENSION = {
44
50
  code: "code",
45
51
  svg: "svg",
46
52
  mermaid: "mmd",
47
- react: "jsx"
53
+ react: "jsx",
54
+ sarif: "sarif",
55
+ csv: "csv",
56
+ image: "image",
57
+ video: "video"
48
58
  };
49
59
 
50
60
  const FORMAT_TO_CONTENT_TYPE = {
@@ -55,7 +65,11 @@ const FORMAT_TO_CONTENT_TYPE = {
55
65
  code: "text/x-source-code; charset=utf-8",
56
66
  svg: "image/svg+xml; charset=utf-8",
57
67
  mermaid: "text/vnd.mermaid; charset=utf-8",
58
- react: "text/jsx; charset=utf-8"
68
+ react: "text/jsx; charset=utf-8",
69
+ sarif: "application/sarif+json; charset=utf-8",
70
+ csv: "text/csv; charset=utf-8",
71
+ image: "application/vnd.artifacty.image+base64; charset=utf-8",
72
+ video: "application/vnd.artifacty.video+base64; charset=utf-8"
59
73
  };
60
74
 
61
75
  export function createStore(options = {}) {
@@ -98,6 +112,7 @@ export async function writeIndex(store, index) {
98
112
  const db = openDatabase(store);
99
113
  try {
100
114
  transaction(db, () => {
115
+ clearSearchIndex(db);
101
116
  db.prepare("DELETE FROM artifact_versions").run();
102
117
  db.prepare("DELETE FROM artifacts").run();
103
118
  for (const artifact of index.artifacts) {
@@ -106,6 +121,7 @@ export async function writeIndex(store, index) {
106
121
  insertVersionRecord(db, artifact.id, version);
107
122
  }
108
123
  }
124
+ rebuildSearchIndexInDb(db, store);
109
125
  });
110
126
  } finally {
111
127
  db.close();
@@ -141,6 +157,7 @@ export async function createArtifact(store = createStore(), input = {}) {
141
157
 
142
158
  insertArtifactRecord(db, artifact);
143
159
  insertVersionRecord(db, id, version);
160
+ upsertSearchIndex(db, artifact, version, normalized.content);
144
161
  insertAuditRecord(db, {
145
162
  action: input.auditAction || "create",
146
163
  artifactId: id,
@@ -195,6 +212,7 @@ export async function updateArtifact(store = createStore(), id, input = {}) {
195
212
  artifact.id
196
213
  );
197
214
  insertVersionRecord(db, artifact.id, version);
215
+ upsertSearchIndex(db, artifact, version, normalized.content);
198
216
  insertAuditRecord(db, {
199
217
  action: input.auditAction || "update",
200
218
  artifactId: artifact.id,
@@ -266,30 +284,316 @@ export async function restoreArtifact(store = createStore(), id, options = {}) {
266
284
  }
267
285
 
268
286
  export async function listArtifacts(store = createStore(), filters = {}) {
269
- const index = await loadIndex(store);
287
+ return (await listArtifactsPage(store, filters)).artifacts;
288
+ }
289
+
290
+ export async function listArtifactsPage(store = createStore(), filters = {}) {
291
+ const db = openDatabase(store);
270
292
  const limit = clampInteger(filters.limit, 1, 200, 50);
271
- const query = normalizeOptionalString(filters.query).toLowerCase();
293
+ const offset = clampInteger(filters.offset, 0, 1_000_000, 0);
294
+ const query = normalizeOptionalString(filters.query);
295
+ const normalizedQuery = query.toLowerCase();
272
296
  const tag = normalizeOptionalString(filters.tag).toLowerCase();
273
297
  const sourceAgent = normalizeOptionalString(filters.sourceAgent).toLowerCase();
274
298
 
275
- return index.artifacts
276
- .filter((artifact) => {
277
- if (!filters.includeArchived && artifact.archivedAt) {
278
- return false;
299
+ try {
300
+ if (query && searchIndexAvailable(db)) {
301
+ const ftsQuery = toFtsQuery(query);
302
+ if (ftsQuery) {
303
+ try {
304
+ const page = listArtifactsPageWithFts(db, {
305
+ ftsQuery,
306
+ tag,
307
+ sourceAgent,
308
+ includeArchived: filters.includeArchived,
309
+ limit,
310
+ offset
311
+ });
312
+ if (page.total > 0) {
313
+ return page;
314
+ }
315
+ } catch {
316
+ // Keep search usable even if the SQLite FTS parser rejects a query.
317
+ }
279
318
  }
280
- if (query && !artifactMatchesQuery(artifact, query)) {
281
- return false;
319
+ }
320
+
321
+ return listArtifactsPageWithSql(db, {
322
+ query: normalizedQuery,
323
+ tag,
324
+ sourceAgent,
325
+ includeArchived: filters.includeArchived,
326
+ limit,
327
+ offset
328
+ });
329
+ } finally {
330
+ db.close();
331
+ }
332
+ }
333
+
334
+ export async function rebuildSearchIndex(store = createStore()) {
335
+ const db = openDatabase(store);
336
+ try {
337
+ if (!searchIndexAvailable(db)) {
338
+ return {
339
+ ok: false,
340
+ fts5: false,
341
+ indexed: 0,
342
+ skipped: [],
343
+ message: "SQLite FTS5 is unavailable; metadata search fallback remains active."
344
+ };
345
+ }
346
+
347
+ return transaction(db, () => rebuildSearchIndexInDb(db, store));
348
+ } finally {
349
+ db.close();
350
+ }
351
+ }
352
+
353
+ export async function checkStoreIntegrity(store = createStore()) {
354
+ const db = openDatabase(store);
355
+ const checkedAt = new Date().toISOString();
356
+ try {
357
+ const artifacts = loadArtifacts(db);
358
+ const referencedPaths = new Set();
359
+ const missingFiles = [];
360
+ const hashMismatches = [];
361
+ const sizeMismatches = [];
362
+ const dbInconsistencies = [];
363
+ let totalBytes = 0;
364
+ let versionCount = 0;
365
+
366
+ for (const artifact of artifacts) {
367
+ if (!artifact.versions.length) {
368
+ dbInconsistencies.push({
369
+ artifactId: artifact.id,
370
+ issue: "artifact has no version rows"
371
+ });
282
372
  }
283
- if (tag && !artifact.tags.some((item) => item.toLowerCase() === tag)) {
284
- return false;
373
+ if (!artifact.versions.some((version) => version.version === artifact.latestVersion)) {
374
+ dbInconsistencies.push({
375
+ artifactId: artifact.id,
376
+ issue: `latest version ${artifact.latestVersion} has no version row`
377
+ });
285
378
  }
286
- if (sourceAgent && artifact.sourceAgent.toLowerCase() !== sourceAgent) {
287
- return false;
379
+
380
+ for (const version of artifact.versions) {
381
+ versionCount += 1;
382
+ const absolutePath = path.resolve(store.home, version.path);
383
+ referencedPaths.add(absolutePath);
384
+ if (!existsSync(absolutePath)) {
385
+ missingFiles.push({
386
+ artifactId: artifact.id,
387
+ version: version.version,
388
+ path: version.path
389
+ });
390
+ continue;
391
+ }
392
+
393
+ const content = readFileSync(absolutePath);
394
+ const actualSize = content.byteLength;
395
+ const actualSha256 = createHash("sha256").update(content).digest("hex");
396
+ totalBytes += actualSize;
397
+
398
+ if (actualSize !== version.sizeBytes) {
399
+ sizeMismatches.push({
400
+ artifactId: artifact.id,
401
+ version: version.version,
402
+ path: version.path,
403
+ expected: version.sizeBytes,
404
+ actual: actualSize
405
+ });
406
+ }
407
+ if (actualSha256 !== version.sha256) {
408
+ hashMismatches.push({
409
+ artifactId: artifact.id,
410
+ version: version.version,
411
+ path: version.path,
412
+ expected: version.sha256,
413
+ actual: actualSha256
414
+ });
415
+ }
288
416
  }
289
- return true;
290
- })
291
- .slice(0, limit)
292
- .map(toArtifactSummary);
417
+ }
418
+
419
+ const orphanFiles = listStoreFiles(store.artifactsDir)
420
+ .filter((filePath) => !referencedPaths.has(filePath))
421
+ .map((filePath) => {
422
+ const stat = statSync(filePath);
423
+ return {
424
+ path: path.relative(store.home, filePath),
425
+ sizeBytes: stat.size
426
+ };
427
+ });
428
+ const orphanBytes = orphanFiles.reduce((sum, file) => sum + file.sizeBytes, 0);
429
+ const ok =
430
+ missingFiles.length === 0 &&
431
+ hashMismatches.length === 0 &&
432
+ sizeMismatches.length === 0 &&
433
+ orphanFiles.length === 0 &&
434
+ dbInconsistencies.length === 0;
435
+
436
+ return {
437
+ ok,
438
+ checkedAt,
439
+ store: store.home,
440
+ artifactCount: artifacts.length,
441
+ versionCount,
442
+ totalBytes,
443
+ orphanBytes,
444
+ missingFiles,
445
+ hashMismatches,
446
+ sizeMismatches,
447
+ orphanFiles,
448
+ dbInconsistencies
449
+ };
450
+ } finally {
451
+ db.close();
452
+ }
453
+ }
454
+
455
+ function listArtifactsPageWithSql(db, filters) {
456
+ const { clauses, params } = artifactWhereClauses(filters);
457
+ if (filters.query) {
458
+ const like = `%${escapeLike(filters.query)}%`;
459
+ clauses.push(`(
460
+ LOWER(id) LIKE ? ESCAPE '\\' OR
461
+ LOWER(title) LIKE ? ESCAPE '\\' OR
462
+ LOWER(source_agent) LIKE ? ESCAPE '\\' OR
463
+ LOWER(tags_json) LIKE ? ESCAPE '\\'
464
+ )`);
465
+ params.push(like, like, like, like);
466
+ }
467
+
468
+ const whereSql = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
469
+ const total = db.prepare(`SELECT COUNT(*) AS total FROM artifacts ${whereSql}`).get(...params).total;
470
+ const rows = db.prepare(`
471
+ SELECT id, title, artifact_type, schema_version, source_agent, tags_json, created_at, updated_at, latest_version, archived_at
472
+ FROM artifacts
473
+ ${whereSql}
474
+ ORDER BY updated_at DESC, created_at DESC
475
+ LIMIT ? OFFSET ?
476
+ `).all(...params, filters.limit, filters.offset);
477
+
478
+ return pagedResult({
479
+ artifacts: rows.map((row) => toArtifactSummary(artifactFromRow(db, row))),
480
+ total,
481
+ limit: filters.limit,
482
+ offset: filters.offset,
483
+ searchBackend: filters.query ? "metadata" : "sqlite"
484
+ });
485
+ }
486
+
487
+ function listArtifactsPageWithFts(db, filters) {
488
+ const { clauses, params } = artifactWhereClauses(filters, "a");
489
+ clauses.unshift("artifact_search MATCH ?");
490
+ params.unshift(filters.ftsQuery);
491
+ const whereSql = `WHERE ${clauses.join(" AND ")}`;
492
+ const total = db.prepare(`
493
+ SELECT COUNT(*) AS total
494
+ FROM artifact_search
495
+ JOIN artifacts a ON a.id = artifact_search.artifact_id
496
+ ${whereSql}
497
+ `).get(...params).total;
498
+ const rows = db.prepare(`
499
+ SELECT
500
+ a.id,
501
+ a.title,
502
+ a.artifact_type,
503
+ a.schema_version,
504
+ a.source_agent,
505
+ a.tags_json,
506
+ a.created_at,
507
+ a.updated_at,
508
+ a.latest_version,
509
+ a.archived_at,
510
+ bm25(artifact_search) AS search_rank,
511
+ snippet(artifact_search, 7, '', '', '...', 24) AS search_snippet
512
+ FROM artifact_search
513
+ JOIN artifacts a ON a.id = artifact_search.artifact_id
514
+ ${whereSql}
515
+ ORDER BY search_rank ASC, a.updated_at DESC, a.created_at DESC
516
+ LIMIT ? OFFSET ?
517
+ `).all(...params, filters.limit, filters.offset);
518
+
519
+ return pagedResult({
520
+ artifacts: rows.map((row) => ({
521
+ ...toArtifactSummary(artifactFromRow(db, row)),
522
+ searchScore: row.search_rank,
523
+ searchSnippet: normalizeWhitespace(row.search_snippet)
524
+ })),
525
+ total,
526
+ limit: filters.limit,
527
+ offset: filters.offset,
528
+ searchBackend: "fts5"
529
+ });
530
+ }
531
+
532
+ function artifactWhereClauses(filters, alias = "") {
533
+ const prefix = alias ? `${alias}.` : "";
534
+ const clauses = [];
535
+ const params = [];
536
+
537
+ if (!filters.includeArchived) {
538
+ clauses.push(`${prefix}archived_at IS NULL`);
539
+ }
540
+ if (filters.tag) {
541
+ clauses.push(`LOWER(${prefix}tags_json) LIKE ? ESCAPE '\\'`);
542
+ params.push(`%"${escapeLike(filters.tag)}"%`);
543
+ }
544
+ if (filters.sourceAgent) {
545
+ clauses.push(`LOWER(${prefix}source_agent) = ?`);
546
+ params.push(filters.sourceAgent);
547
+ }
548
+
549
+ return { clauses, params };
550
+ }
551
+
552
+ function pagedResult({ artifacts, total, limit, offset, searchBackend }) {
553
+ return {
554
+ artifacts,
555
+ total,
556
+ limit,
557
+ offset,
558
+ hasMore: offset + artifacts.length < total,
559
+ nextOffset: offset + artifacts.length < total ? offset + limit : null,
560
+ previousOffset: offset > 0 ? Math.max(0, offset - limit) : null,
561
+ search: {
562
+ backend: searchBackend
563
+ }
564
+ };
565
+ }
566
+
567
+ function artifactFromRow(db, row) {
568
+ return {
569
+ id: row.id,
570
+ title: row.title,
571
+ artifactType: row.artifact_type,
572
+ schemaVersion: row.schema_version,
573
+ sourceAgent: row.source_agent,
574
+ tags: parseJson(row.tags_json, []),
575
+ createdAt: row.created_at,
576
+ updatedAt: row.updated_at,
577
+ archivedAt: row.archived_at,
578
+ latestVersion: row.latest_version,
579
+ versions: loadVersions(db, row.id)
580
+ };
581
+ }
582
+
583
+ function escapeLike(value) {
584
+ return String(value).replace(/[\\%_]/g, (match) => `\\${match}`);
585
+ }
586
+
587
+ function normalizeWhitespace(value) {
588
+ return normalizeOptionalString(value).replace(/\s+/g, " ");
589
+ }
590
+
591
+ function toFtsQuery(value) {
592
+ const tokens = normalizeOptionalString(value).match(/[\p{L}\p{N}_-]+/gu) || [];
593
+ return tokens
594
+ .slice(0, 12)
595
+ .map((token) => `"${token.replaceAll("\"", "\"\"")}"`)
596
+ .join(" AND ");
293
597
  }
294
598
 
295
599
  export async function getArtifact(store = createStore(), id, options = {}) {
@@ -398,6 +702,9 @@ export function normalizeFormat(value = "text") {
398
702
  if (normalized === "jsx" || normalized === "tsx") {
399
703
  return "react";
400
704
  }
705
+ if (normalized === "sarif+json") {
706
+ return "sarif";
707
+ }
401
708
  if (ARTIFACT_FORMATS.includes(normalized)) {
402
709
  return normalized;
403
710
  }
@@ -441,6 +748,7 @@ function openDatabase(store) {
441
748
  `);
442
749
  initializeSchema(db);
443
750
  migrateJsonIndex(db, store);
751
+ syncSearchIndexIfEmpty(db, store);
444
752
  return db;
445
753
  }
446
754
 
@@ -499,6 +807,7 @@ function initializeSchema(db) {
499
807
  ensureColumn(db, "artifacts", "schema_version", "INTEGER NOT NULL DEFAULT 1");
500
808
  ensureColumn(db, "artifacts", "archived_at", "TEXT");
501
809
  db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('store_version', ?)").run(String(STORE_VERSION));
810
+ ensureSearchTable(db);
502
811
  }
503
812
 
504
813
  function migrateJsonIndex(db, store) {
@@ -534,6 +843,135 @@ function migrateJsonIndex(db, store) {
534
843
  });
535
844
  }
536
845
 
846
+ function ensureSearchTable(db) {
847
+ try {
848
+ db.exec(`
849
+ CREATE VIRTUAL TABLE IF NOT EXISTS artifact_search USING fts5(
850
+ artifact_id UNINDEXED,
851
+ title,
852
+ source_agent,
853
+ artifact_type,
854
+ tags,
855
+ format,
856
+ metadata,
857
+ content
858
+ );
859
+ `);
860
+ db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('fts5_enabled', 'true')").run();
861
+ return true;
862
+ } catch (error) {
863
+ db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('fts5_enabled', ?)").run(`false:${error.message}`);
864
+ return false;
865
+ }
866
+ }
867
+
868
+ function searchIndexAvailable(db) {
869
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'artifact_search'").get();
870
+ return Boolean(row) || ensureSearchTable(db);
871
+ }
872
+
873
+ function syncSearchIndexIfEmpty(db, store) {
874
+ if (!searchIndexAvailable(db)) {
875
+ return;
876
+ }
877
+ const artifactCount = db.prepare("SELECT COUNT(*) AS count FROM artifacts").get().count;
878
+ if (artifactCount === 0) {
879
+ return;
880
+ }
881
+ const indexedCount = db.prepare("SELECT COUNT(*) AS count FROM artifact_search").get().count;
882
+ if (indexedCount === 0) {
883
+ transaction(db, () => rebuildSearchIndexInDb(db, store));
884
+ }
885
+ }
886
+
887
+ function clearSearchIndex(db) {
888
+ if (searchIndexAvailable(db)) {
889
+ db.prepare("DELETE FROM artifact_search").run();
890
+ }
891
+ }
892
+
893
+ function rebuildSearchIndexInDb(db, store) {
894
+ if (!searchIndexAvailable(db)) {
895
+ return {
896
+ ok: false,
897
+ fts5: false,
898
+ indexed: 0,
899
+ skipped: []
900
+ };
901
+ }
902
+
903
+ db.prepare("DELETE FROM artifact_search").run();
904
+ const skipped = [];
905
+ let indexed = 0;
906
+ for (const artifact of loadArtifacts(db)) {
907
+ const latest = artifact.versions.find((version) => version.version === artifact.latestVersion);
908
+ if (!latest) {
909
+ skipped.push({ artifactId: artifact.id, reason: "latest version row missing" });
910
+ continue;
911
+ }
912
+ const absolutePath = path.join(store.home, latest.path);
913
+ if (!existsSync(absolutePath)) {
914
+ skipped.push({ artifactId: artifact.id, version: latest.version, reason: "version file missing" });
915
+ continue;
916
+ }
917
+ upsertSearchIndex(db, artifact, latest, readFileSync(absolutePath, "utf8"));
918
+ indexed += 1;
919
+ }
920
+ db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('search_index_built_at', ?)").run(new Date().toISOString());
921
+ return {
922
+ ok: skipped.length === 0,
923
+ fts5: true,
924
+ indexed,
925
+ skipped
926
+ };
927
+ }
928
+
929
+ function upsertSearchIndex(db, artifact, version, content) {
930
+ if (!searchIndexAvailable(db)) {
931
+ return;
932
+ }
933
+ db.prepare("DELETE FROM artifact_search WHERE artifact_id = ?").run(artifact.id);
934
+ db.prepare(`
935
+ INSERT INTO artifact_search (
936
+ artifact_id, title, source_agent, artifact_type, tags, format, metadata, content
937
+ )
938
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
939
+ `).run(
940
+ artifact.id,
941
+ artifact.title,
942
+ artifact.sourceAgent,
943
+ artifact.artifactType,
944
+ artifact.tags.join(" "),
945
+ version.format,
946
+ metadataSearchText(version.metadata),
947
+ content
948
+ );
949
+ }
950
+
951
+ function metadataSearchText(metadata) {
952
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
953
+ return "";
954
+ }
955
+ return JSON.stringify(metadata).slice(0, 64 * 1024);
956
+ }
957
+
958
+ function listStoreFiles(root) {
959
+ if (!existsSync(root)) {
960
+ return [];
961
+ }
962
+
963
+ const files = [];
964
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
965
+ const fullPath = path.join(root, entry.name);
966
+ if (entry.isDirectory()) {
967
+ files.push(...listStoreFiles(fullPath));
968
+ } else if (entry.isFile()) {
969
+ files.push(path.resolve(fullPath));
970
+ }
971
+ }
972
+ return files;
973
+ }
974
+
537
975
  function loadArtifacts(db) {
538
976
  const rows = db.prepare(`
539
977
  SELECT id, title, artifact_type, schema_version, source_agent, tags_json, created_at, updated_at, latest_version, archived_at
@@ -805,9 +1243,29 @@ function inferArtifactType(input) {
805
1243
  if (format === "code") {
806
1244
  return "snippet";
807
1245
  }
1246
+ if (format === "sarif") {
1247
+ return "analysis-report";
1248
+ }
1249
+ if (format === "csv") {
1250
+ return looksLikeAnalysisCsv(input.content) ||
1251
+ /findings?|security|review|scan/i.test(normalizeOptionalString(input.title))
1252
+ ? "analysis-report"
1253
+ : "table";
1254
+ }
1255
+ if (format === "image" || format === "video") {
1256
+ return "asset";
1257
+ }
808
1258
  return "document";
809
1259
  }
810
1260
 
1261
+ function looksLikeAnalysisCsv(content) {
1262
+ const [header = ""] = normalizeOptionalString(content).split(/\r?\n/, 1);
1263
+ const normalized = header.toLowerCase();
1264
+ return normalized.includes("severity") &&
1265
+ (normalized.includes("message") || normalized.includes("description")) &&
1266
+ (normalized.includes("file") || normalized.includes("path") || normalized.includes("rule"));
1267
+ }
1268
+
811
1269
  function normalizeTags(tags) {
812
1270
  if (!Array.isArray(tags)) {
813
1271
  return [];
@@ -834,9 +1292,21 @@ function inferFormat(contentType) {
834
1292
  if (value.includes("vnd.ant.code") || value.includes("source-code")) {
835
1293
  return "code";
836
1294
  }
1295
+ if (value.includes("sarif")) {
1296
+ return "sarif";
1297
+ }
1298
+ if (value.includes("csv")) {
1299
+ return "csv";
1300
+ }
837
1301
  if (value.includes("svg")) {
838
1302
  return "svg";
839
1303
  }
1304
+ if (value.startsWith("image/")) {
1305
+ return "image";
1306
+ }
1307
+ if (value.startsWith("video/")) {
1308
+ return "video";
1309
+ }
840
1310
  if (value.includes("vnd.ant.mermaid") || value.includes("mermaid")) {
841
1311
  return "mermaid";
842
1312
  }
@@ -865,19 +1335,6 @@ function makeArtifactId(title) {
865
1335
  return `${slug}-${randomUUID().slice(0, 8)}`;
866
1336
  }
867
1337
 
868
- function artifactMatchesQuery(artifact, query) {
869
- const haystack = [
870
- artifact.id,
871
- artifact.title,
872
- artifact.sourceAgent,
873
- ...artifact.tags
874
- ]
875
- .join(" ")
876
- .toLowerCase();
877
-
878
- return haystack.includes(query);
879
- }
880
-
881
1338
  function clampInteger(value, min, max, fallback) {
882
1339
  const parsed = Number.parseInt(value, 10);
883
1340
  if (Number.isNaN(parsed)) {
package/src/mcp-server.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  createStore,
9
9
  getArtifact,
10
10
  listAuditEvents,
11
- listArtifacts,
11
+ listArtifactsPage,
12
12
  restoreArtifact,
13
13
  updateArtifact
14
14
  } from "./lib/storage.js";
@@ -86,7 +86,8 @@ const tools = [
86
86
  tag: { type: "string" },
87
87
  sourceAgent: { type: "string" },
88
88
  includeArchived: { type: "boolean" },
89
- limit: { type: "number" }
89
+ limit: { type: "number" },
90
+ offset: { type: "number" }
90
91
  }
91
92
  },
92
93
  annotations: {
@@ -97,13 +98,13 @@ const tools = [
97
98
  {
98
99
  name: "artifacty_import",
99
100
  title: "Import Agent Artifact",
100
- description: "Convert an artifact produced by Claude, Codex, Gemini, or another agent into Artifacty format and save it.",
101
+ description: "Convert an artifact produced by Claude, Codex, Gemini, GitHub Copilot, Cursor, or another agent into Artifacty format and save it.",
101
102
  inputSchema: {
102
103
  type: "object",
103
104
  properties: {
104
105
  agent: {
105
106
  type: "string",
106
- enum: ["auto", "claude", "codex", "gemini", "artifacty", "generic"],
107
+ enum: ["auto", "claude", "codex", "gemini", "copilot", "cursor", "artifacty", "generic"],
107
108
  description: "Original agent family. Use auto when unsure."
108
109
  },
109
110
  title: { type: "string", description: "Optional title override." },
@@ -315,7 +316,7 @@ async function handleRequest(message) {
315
316
  serverInfo: {
316
317
  name: "artifacty",
317
318
  title: "Artifacty",
318
- version: "0.1.0"
319
+ version: "0.3.0"
319
320
  },
320
321
  instructions: "Use Artifacty to create, import, list, read, and update local artifacts that other agents can reuse."
321
322
  };
@@ -346,12 +347,21 @@ async function callTool(name, args) {
346
347
 
347
348
  if (name === "artifacty_list") {
348
349
  const publicBaseUrl = await resolvePublicBaseUrl(store);
349
- const artifacts = await listArtifacts(store, args);
350
+ const page = await listArtifactsPage(store, args);
350
351
  return toolResult({
351
- artifacts: artifacts.map((artifact) => ({
352
+ artifacts: page.artifacts.map((artifact) => ({
352
353
  ...artifact,
353
354
  url: `${publicBaseUrl}/artifacts/${encodeURIComponent(artifact.id)}`
354
- }))
355
+ })),
356
+ pagination: {
357
+ total: page.total,
358
+ limit: page.limit,
359
+ offset: page.offset,
360
+ hasMore: page.hasMore,
361
+ nextOffset: page.nextOffset,
362
+ previousOffset: page.previousOffset
363
+ },
364
+ search: page.search
355
365
  });
356
366
  }
357
367