artifacty 0.10.6 → 0.10.7

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 CHANGED
@@ -325,6 +325,7 @@ Schema and storage:
325
325
  - Bundle artifacts store multiple files or base64 assets as portable JSON.
326
326
  - Supported formats are `html`, `markdown`, `text`, `json`, `code`, `svg`, `mermaid`, `react`, `sarif`, `csv`, `image`, and `video`.
327
327
  - Native create/import paths infer `html` from HTML documents or fragments when no explicit format is supplied.
328
+ - `sourceAgent` is canonicalized before storage. Aliases such as `claude-code`, `Claude Code`, `github-copilot`, and `gemini-cli` are stored as `claude`, `copilot`, and `gemini`; legacy `unknown` rows are backfilled only when version metadata, audit data, or source-agent tags provide a known agent.
328
329
  - Diagram, component, source snippet, analysis report, table, and media assets use `diagram`, `component`, `snippet`, `analysis-report`, `table`, and `asset` artifact types.
329
330
  - Copilot/Cursor examples cover PR reviews, screenshots, demo recordings, and visual evidence bundles.
330
331
  - See [docs/artifact-schema-v1.md](docs/artifact-schema-v1.md).
@@ -50,6 +50,11 @@ Artifacty store. For authenticated central servers this is the personal token
50
50
  owner's email; `publisherName` and `publisherUserId` are included when the
51
51
  server can map the request to a local user record. Legacy artifacts are
52
52
  best-effort backfilled from the first `create` or `import` audit actor.
53
+ Known source-agent aliases are canonicalized before storage. For example,
54
+ `claude-code`, `Claude Code`, and `anthropic` become `claude`;
55
+ `github-copilot` becomes `copilot`; and `gemini-cli` becomes `gemini`.
56
+ Existing `unknown` rows are upgraded only when version metadata, audit data, or
57
+ source-agent tags provide one of the known agent identities.
53
58
 
54
59
  ## Version Record
55
60
 
@@ -321,7 +321,7 @@ Supported converter inputs:
321
321
  - Gemini: `returnDisplay`, `llmContent`, text blocks, or local markdown/text/json files.
322
322
  - Generic: file extension, content type, HTML doctype/fragments, JSON shape, media data URLs, and markdown headings are used to infer format and title. Generic `text/plain` uploads are upgraded to a more specific format, such as `html`, when filename or content makes that clear.
323
323
 
324
- The converter adds `imported` and source-agent tags, preserves the raw content as an immutable Artifacty version, and records source details under `metadata.artifactyImport`.
324
+ The converter canonicalizes source-agent aliases, adds `imported` and source-agent tags, preserves the raw content as an immutable Artifacty version, and records source details under `metadata.artifactyImport`. For example, `claude-code` and `Claude Code` are stored as `claude`.
325
325
 
326
326
  Fixture examples for Copilot/Cursor PR review, screenshot, demo recording, and
327
327
  visual evidence bundle live under `test/fixtures/` and are covered by converter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifacty",
3
- "version": "0.10.6",
3
+ "version": "0.10.7",
4
4
  "description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -0,0 +1,53 @@
1
+ export const KNOWN_SOURCE_AGENTS = new Set([
2
+ "artifacty",
3
+ "claude",
4
+ "codex",
5
+ "copilot",
6
+ "cursor",
7
+ "gemini",
8
+ "generic",
9
+ "mcp",
10
+ "unknown"
11
+ ]);
12
+
13
+ const SOURCE_AGENT_ALIASES = new Map([
14
+ ["anthropic", "claude"],
15
+ ["claude-ai", "claude"],
16
+ ["claude-code", "claude"],
17
+ ["claude-code-cli", "claude"],
18
+ ["claude_cli", "claude"],
19
+ ["github-copilot", "copilot"],
20
+ ["copilot-chat", "copilot"],
21
+ ["vscode-copilot", "copilot"],
22
+ ["vs-code-copilot", "copilot"],
23
+ ["cursor-ai", "cursor"],
24
+ ["gemini-cli", "gemini"],
25
+ ["google", "gemini"],
26
+ ["openai", "codex"],
27
+ ["openai-codex", "codex"],
28
+ ["codex-cli", "codex"],
29
+ ["chatgpt", "codex"]
30
+ ]);
31
+
32
+ export function normalizeSourceAgent(value, options = {}) {
33
+ const defaultValue = options.defaultValue ?? "unknown";
34
+ const allowAuto = options.allowAuto === true;
35
+ const normalized = String(value ?? "").trim().toLowerCase();
36
+ if (!normalized) {
37
+ return defaultValue;
38
+ }
39
+ if (normalized === "auto") {
40
+ return allowAuto ? "auto" : defaultValue;
41
+ }
42
+ const slug = normalized.replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
43
+ const canonical = SOURCE_AGENT_ALIASES.get(normalized) || SOURCE_AGENT_ALIASES.get(slug) || slug;
44
+ if (KNOWN_SOURCE_AGENTS.has(canonical)) {
45
+ return canonical;
46
+ }
47
+ return canonical || defaultValue;
48
+ }
49
+
50
+ export function isUnknownSourceAgent(value) {
51
+ const normalized = normalizeSourceAgent(value, { defaultValue: "unknown" });
52
+ return normalized === "unknown" || normalized === "generic" || normalized === "auto";
53
+ }
@@ -1,9 +1,9 @@
1
1
  import path from "node:path";
2
2
  import { createHash } from "node:crypto";
3
+ import { normalizeSourceAgent } from "./agents.js";
3
4
  import { ARTIFACT_TYPES, contentTypeForFormat, normalizeFormat } from "./storage.js";
4
5
 
5
6
  const CONTINUATION_AGENTS = new Set(["codex", "copilot", "cursor"]);
6
- const KNOWN_AGENTS = new Set(["auto", "artifacty", "claude", "codex", "copilot", "cursor", "gemini", "generic"]);
7
7
 
8
8
  export function convertAgentArtifact(input = {}) {
9
9
  const originalAgent = normalizeAgent(input.agent || input.sourceAgent || input.source_agent || "auto");
@@ -28,8 +28,11 @@ export function convertAgentArtifact(input = {}) {
28
28
  if (media) {
29
29
  content = media.content;
30
30
  }
31
- const sourceAgent = optionalString(input.sourceAgent || input.source_agent || decoded.sourceAgent) ||
32
- (originalAgent === "auto" ? detectAgent(parsed, fileName) : originalAgent);
31
+ const explicitSourceAgent = optionalString(input.sourceAgent || input.source_agent || decoded.sourceAgent);
32
+ const detectedSourceAgent = originalAgent === "auto" ? detectAgent(parsed, fileName) : originalAgent;
33
+ const sourceAgent = explicitSourceAgent
34
+ ? normalizeAgent(explicitSourceAgent)
35
+ : normalizeAgent(detectedSourceAgent || "unknown");
33
36
 
34
37
  const title =
35
38
  optionalString(input.title) ||
@@ -1387,29 +1390,10 @@ function cleanTitle(value) {
1387
1390
  }
1388
1391
 
1389
1392
  function normalizeAgent(value) {
1390
- const normalized = optionalString(value).toLowerCase();
1391
- if (!normalized) {
1392
- return "auto";
1393
- }
1394
- if (normalized === "claude-code" || normalized === "anthropic") {
1395
- return "claude";
1396
- }
1397
- if (normalized === "gemini-cli" || normalized === "google") {
1398
- return "gemini";
1399
- }
1400
- if (normalized === "github-copilot" || normalized === "copilot-chat" || normalized === "vscode-copilot" || normalized === "vs-code-copilot") {
1401
- return "copilot";
1402
- }
1403
- if (normalized === "cursor-ai") {
1404
- return "cursor";
1405
- }
1406
- if (normalized === "openai" || normalized === "chatgpt") {
1407
- return "codex";
1408
- }
1409
- if (KNOWN_AGENTS.has(normalized)) {
1410
- return normalized;
1411
- }
1412
- return normalized.replace(/[^a-z0-9-]+/g, "-") || "generic";
1393
+ return normalizeSourceAgent(value, {
1394
+ defaultValue: "auto",
1395
+ allowAuto: true
1396
+ });
1413
1397
  }
1414
1398
 
1415
1399
  function normalizeTags(tags) {
@@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { DatabaseSync } from "node:sqlite";
7
+ import { isUnknownSourceAgent, normalizeSourceAgent } from "./agents.js";
7
8
  import { assertNoSecrets, securityConfig } from "./security.js";
8
9
 
9
10
  export const STORE_VERSION = 4;
@@ -182,7 +183,11 @@ export async function createArtifact(store = createStore(), input = {}) {
182
183
  export async function updateArtifact(store = createStore(), id, input = {}) {
183
184
  const secretScan = assertNoSecrets(input, securityConfig());
184
185
  input = withSecretScan(input, secretScan);
185
- const normalized = normalizeArtifactInput(input, { requireContent: true, requireTitle: false });
186
+ const normalized = normalizeArtifactInput(input, {
187
+ requireContent: true,
188
+ requireTitle: false,
189
+ defaultSourceAgent: ""
190
+ });
186
191
  const db = openDatabase(store);
187
192
 
188
193
  try {
@@ -252,7 +257,11 @@ export async function updateArtifact(store = createStore(), id, input = {}) {
252
257
  export async function replaceArtifactVersion(store = createStore(), id, versionNumber, input = {}) {
253
258
  const secretScan = assertNoSecrets(input, securityConfig());
254
259
  input = withSecretScan(input, secretScan);
255
- const normalized = normalizeArtifactInput(input, { requireContent: true, requireTitle: false });
260
+ const normalized = normalizeArtifactInput(input, {
261
+ requireContent: true,
262
+ requireTitle: false,
263
+ defaultSourceAgent: ""
264
+ });
256
265
  const targetVersion = Number(versionNumber);
257
266
  if (!Number.isInteger(targetVersion) || targetVersion < 1) {
258
267
  throw Object.assign(new Error(`Invalid artifact version: ${versionNumber}`), {
@@ -467,7 +476,7 @@ export async function listArtifactsPage(store = createStore(), filters = {}) {
467
476
  const query = normalizeOptionalString(filters.query);
468
477
  const normalizedQuery = query.toLowerCase();
469
478
  const tag = normalizeOptionalString(filters.tag).toLowerCase();
470
- const sourceAgent = normalizeOptionalString(filters.sourceAgent).toLowerCase();
479
+ const sourceAgent = normalizeSourceAgent(filters.sourceAgent, { defaultValue: "" }).toLowerCase();
471
480
 
472
481
  try {
473
482
  if (query && searchIndexAvailable(db)) {
@@ -1250,6 +1259,7 @@ function openDatabase(store) {
1250
1259
  `);
1251
1260
  initializeSchema(db);
1252
1261
  migrateJsonIndex(db, store);
1262
+ normalizeStoredSourceAgents(db, store);
1253
1263
  syncSearchIndexIfEmpty(db, store);
1254
1264
  return db;
1255
1265
  }
@@ -1492,6 +1502,123 @@ function migrateJsonIndex(db, store) {
1492
1502
  });
1493
1503
  }
1494
1504
 
1505
+ const INFERABLE_SOURCE_AGENTS = new Set(["claude", "codex", "copilot", "cursor", "gemini"]);
1506
+
1507
+ function normalizeStoredSourceAgents(db, store) {
1508
+ const rows = db.prepare(`
1509
+ SELECT id, source_agent, tags_json
1510
+ FROM artifacts
1511
+ `).all();
1512
+ const updateArtifact = db.prepare("UPDATE artifacts SET source_agent = ?, tags_json = ? WHERE id = ?");
1513
+ let changed = false;
1514
+
1515
+ transaction(db, () => {
1516
+ for (const row of rows) {
1517
+ const normalized = normalizeSourceAgent(row.source_agent, { defaultValue: "unknown" });
1518
+ const inferred = isUnknownSourceAgent(normalized)
1519
+ ? inferStoredSourceAgent(db, row)
1520
+ : normalized;
1521
+ const nextSourceAgent = inferred || normalized;
1522
+ const nextTags = normalizeStoredSourceTags(row.tags_json, nextSourceAgent);
1523
+ if (nextSourceAgent !== row.source_agent || nextTags !== row.tags_json) {
1524
+ updateArtifact.run(nextSourceAgent, nextTags, row.id);
1525
+ changed = true;
1526
+ }
1527
+ }
1528
+
1529
+ const auditRows = db.prepare(`
1530
+ SELECT id, source_agent
1531
+ FROM audit_log
1532
+ WHERE source_agent IS NOT NULL
1533
+ AND TRIM(source_agent) != ''
1534
+ `).all();
1535
+ const updateAudit = db.prepare("UPDATE audit_log SET source_agent = ? WHERE id = ?");
1536
+ for (const row of auditRows) {
1537
+ const normalized = normalizeSourceAgent(row.source_agent, { defaultValue: "" });
1538
+ if (normalized && normalized !== row.source_agent) {
1539
+ updateAudit.run(normalized, row.id);
1540
+ changed = true;
1541
+ }
1542
+ }
1543
+ });
1544
+
1545
+ if (changed) {
1546
+ rebuildSearchIndexInDb(db, store);
1547
+ }
1548
+ }
1549
+
1550
+ function inferStoredSourceAgent(db, artifactRow) {
1551
+ const tagCandidate = firstInferableSourceAgent(parseJson(artifactRow.tags_json, []));
1552
+ if (tagCandidate) {
1553
+ return tagCandidate;
1554
+ }
1555
+
1556
+ const versionRows = db.prepare(`
1557
+ SELECT metadata_json
1558
+ FROM artifact_versions
1559
+ WHERE artifact_id = ?
1560
+ ORDER BY version DESC
1561
+ `).all(artifactRow.id);
1562
+ for (const row of versionRows) {
1563
+ const metadata = parseJson(row.metadata_json, {});
1564
+ const candidate = firstInferableSourceAgent([
1565
+ metadata.sourceAgent,
1566
+ metadata.source_agent,
1567
+ metadata.agent,
1568
+ metadata.artifactyImport?.sourceAgent,
1569
+ metadata.artifactyImport?.originalAgent
1570
+ ]);
1571
+ if (candidate) {
1572
+ return candidate;
1573
+ }
1574
+ }
1575
+
1576
+ const auditRows = db.prepare(`
1577
+ SELECT source_agent
1578
+ FROM audit_log
1579
+ WHERE artifact_id = ?
1580
+ AND source_agent IS NOT NULL
1581
+ AND TRIM(source_agent) != ''
1582
+ ORDER BY created_at DESC
1583
+ `).all(artifactRow.id);
1584
+ return firstInferableSourceAgent(auditRows.map((row) => row.source_agent));
1585
+ }
1586
+
1587
+ function normalizeStoredSourceTags(tagsJson, sourceAgent) {
1588
+ const tags = parseJson(tagsJson, []);
1589
+ if (!Array.isArray(tags)) {
1590
+ return JSON.stringify([]);
1591
+ }
1592
+ const normalizedTags = [];
1593
+ for (const tag of tags) {
1594
+ const trimmed = normalizeOptionalString(tag);
1595
+ if (!trimmed) {
1596
+ continue;
1597
+ }
1598
+ const canonical = inferableSourceAgent(trimmed);
1599
+ const next = canonical || (trimmed === "unknown" && INFERABLE_SOURCE_AGENTS.has(sourceAgent) ? sourceAgent : trimmed);
1600
+ if (!normalizedTags.includes(next)) {
1601
+ normalizedTags.push(next);
1602
+ }
1603
+ }
1604
+ return JSON.stringify(normalizedTags);
1605
+ }
1606
+
1607
+ function firstInferableSourceAgent(values) {
1608
+ for (const value of values || []) {
1609
+ const candidate = inferableSourceAgent(value);
1610
+ if (candidate) {
1611
+ return candidate;
1612
+ }
1613
+ }
1614
+ return "";
1615
+ }
1616
+
1617
+ function inferableSourceAgent(value) {
1618
+ const normalized = normalizeSourceAgent(value, { defaultValue: "" });
1619
+ return INFERABLE_SOURCE_AGENTS.has(normalized) ? normalized : "";
1620
+ }
1621
+
1495
1622
  function ensureSearchTable(db) {
1496
1623
  try {
1497
1624
  const existing = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'artifact_search'").get();
@@ -1715,7 +1842,7 @@ function insertArtifactRecord(db, artifact) {
1715
1842
  artifact.title,
1716
1843
  normalizeArtifactType(artifact.artifactType || artifact.artifact_type || "document"),
1717
1844
  normalizeSchemaVersion(artifact.schemaVersion || artifact.schema_version),
1718
- artifact.sourceAgent || artifact.source_agent || "unknown",
1845
+ normalizeSourceAgent(artifact.sourceAgent || artifact.source_agent, { defaultValue: "unknown" }),
1719
1846
  normalizeOptionalString(artifact.publisherId || artifact.publisher_id) || null,
1720
1847
  normalizeOptionalString(artifact.publisherName || artifact.publisher_name) || null,
1721
1848
  normalizeOptionalString(artifact.publisherUserId || artifact.publisher_user_id) || null,
@@ -1925,7 +2052,9 @@ function normalizeArtifactInput(input, options) {
1925
2052
  contentType: normalizeOptionalString(input.contentType),
1926
2053
  artifactType: normalizeArtifactType(input.artifactType || input.artifact_type || inferArtifactType(input)),
1927
2054
  schemaVersion: normalizeSchemaVersion(input.schemaVersion || input.schema_version),
1928
- sourceAgent: normalizeOptionalString(input.sourceAgent || input.source_agent || input.agent) || "unknown",
2055
+ sourceAgent: normalizeSourceAgent(input.sourceAgent || input.source_agent || input.agent, {
2056
+ defaultValue: options.defaultSourceAgent ?? "unknown"
2057
+ }),
1929
2058
  tags: normalizeTags(input.tags),
1930
2059
  metadata: normalizeMetadata(input.metadata)
1931
2060
  };