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.
- package/AGENTS.md +4 -2
- package/README.md +34 -8
- package/docs/artifact-schema-v1.md +27 -5
- package/docs/integrations.md +21 -4
- package/docs/sarif-csv-artifact-plan.md +44 -65
- package/package.json +1 -1
- package/src/cli.js +62 -10
- package/src/client/editor.js +76 -4
- package/src/lib/converters.js +429 -44
- package/src/lib/i18n.js +8 -0
- package/src/lib/installer.js +58 -4
- package/src/lib/render.js +492 -4
- package/src/lib/storage.js +490 -33
- package/src/mcp-server.js +18 -8
- package/src/server.js +69 -10
package/src/lib/storage.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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 (
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
.
|
|
292
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|