artifacty 0.10.6 → 0.10.8
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 +5 -0
- package/docs/artifact-schema-v1.md +5 -0
- package/docs/central-team-deployment-design.md +13 -0
- package/docs/integrations.md +4 -2
- package/package.json +1 -1
- package/src/lib/agents.js +53 -0
- package/src/lib/converters.js +10 -26
- package/src/lib/service.js +2 -0
- package/src/lib/storage.js +157 -5
package/README.md
CHANGED
|
@@ -92,6 +92,10 @@ artifacty serve --host 10.0.0.50 --share-mode team --api-token "$ARTIFACTY_BOOTS
|
|
|
92
92
|
artifacty install all --mcp-url http://10.0.0.50:8787/mcp --api-token "$ARTIFACTY_PERSONAL_TOKEN"
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
When running the central server as a Linux `systemctl --user` service, enable
|
|
96
|
+
lingering for the service account so Artifacty stays up after logout:
|
|
97
|
+
`sudo loginctl enable-linger $USER`.
|
|
98
|
+
|
|
95
99
|
Administrators can create users individually at `/admin/users` or import them
|
|
96
100
|
from CSV. Use `email,name,role,password,password_reset_required` headers. If
|
|
97
101
|
`password` is empty, Artifacty generates a temporary password, shows it once in
|
|
@@ -325,6 +329,7 @@ Schema and storage:
|
|
|
325
329
|
- Bundle artifacts store multiple files or base64 assets as portable JSON.
|
|
326
330
|
- Supported formats are `html`, `markdown`, `text`, `json`, `code`, `svg`, `mermaid`, `react`, `sarif`, `csv`, `image`, and `video`.
|
|
327
331
|
- Native create/import paths infer `html` from HTML documents or fragments when no explicit format is supplied.
|
|
332
|
+
- `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
333
|
- Diagram, component, source snippet, analysis report, table, and media assets use `diagram`, `component`, `snippet`, `analysis-report`, `table`, and `asset` artifact types.
|
|
329
334
|
- Copilot/Cursor examples cover PR reviews, screenshots, demo recordings, and visual evidence bundles.
|
|
330
335
|
- 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
|
|
|
@@ -134,6 +134,19 @@ proxy, keep `ARTIFACTY_ENABLE_REACT_RENDERER` disabled unless the team trusts
|
|
|
134
134
|
all artifact authors, and store `ARTIFACTY_HOME` on local server disk with
|
|
135
135
|
regular backups.
|
|
136
136
|
|
|
137
|
+
For Linux user-service deployments, enable systemd lingering for the service
|
|
138
|
+
account:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
sudo loginctl enable-linger artifacty
|
|
142
|
+
loginctl show-user artifacty -p Linger
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Use the actual service account name. Without lingering, `systemctl --user`
|
|
146
|
+
services are tied to the user's login manager and can stop after logout, making
|
|
147
|
+
the central dashboard and `/mcp` endpoint unavailable until that user logs in
|
|
148
|
+
again. A system-level unit is another valid option for central servers.
|
|
149
|
+
|
|
137
150
|
The central server exposes `/mcp` only when explicitly enabled. This keeps the
|
|
138
151
|
local browser/API server behavior unchanged while making team MCP exposure an
|
|
139
152
|
intentional operating mode.
|
package/docs/integrations.md
CHANGED
|
@@ -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
|
|
@@ -393,9 +393,11 @@ The generated service runs `src/server.js` with explicit `--host` and `--home` a
|
|
|
393
393
|
Activation is still delegated to the OS service manager:
|
|
394
394
|
|
|
395
395
|
- macOS: `launchctl load ~/Library/LaunchAgents/com.artifacty.server.plist`
|
|
396
|
-
- Linux: `systemctl --user enable --now com.artifacty.server.service`
|
|
396
|
+
- Linux: `sudo loginctl enable-linger $USER`, then `systemctl --user enable --now com.artifacty.server.service`
|
|
397
397
|
- Windows: run the generated PowerShell script, which registers and starts the `ArtifactyServer` scheduled task.
|
|
398
398
|
|
|
399
|
+
On Linux central servers, user services stop when the user's systemd manager exits unless lingering is enabled. Verify durable operation with `loginctl show-user $USER -p Linger`; it should report `Linger=yes`. Without lingering, Artifacty may be unavailable after logout and appear to recover only after the user logs back in.
|
|
400
|
+
|
|
399
401
|
For background services, prefer a stable `ARTIFACTY_API_TOKEN` or `--api-token` value. `serve --generate-token` is intended for temporary interactive sessions; the parent CLI returns the generated token in JSON.
|
|
400
402
|
|
|
401
403
|
## Backup and Audit
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/lib/converters.js
CHANGED
|
@@ -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
|
|
32
|
-
|
|
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
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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) {
|
package/src/lib/service.js
CHANGED
|
@@ -184,9 +184,11 @@ function definitionForPlatform(platform, options = {}, renderOnly) {
|
|
|
184
184
|
path: servicePath(platform, options),
|
|
185
185
|
content: createSystemdUserUnit(options),
|
|
186
186
|
nextSteps: [
|
|
187
|
+
"sudo loginctl enable-linger $USER",
|
|
187
188
|
"systemctl --user daemon-reload",
|
|
188
189
|
`systemctl --user enable --now ${path.basename(servicePath(platform, options))}`,
|
|
189
190
|
`systemctl --user status ${path.basename(servicePath(platform, options))}`,
|
|
191
|
+
"loginctl show-user $USER -p Linger",
|
|
190
192
|
`journalctl --user -u ${path.basename(servicePath(platform, options))} -f`,
|
|
191
193
|
`systemctl --user disable --now ${path.basename(servicePath(platform, options))}`
|
|
192
194
|
],
|
package/src/lib/storage.js
CHANGED
|
@@ -4,11 +4,13 @@ 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;
|
|
10
11
|
export const ARTIFACT_SCHEMA_VERSION = 1;
|
|
11
12
|
export const MAX_ARTIFACT_BYTES = 16 * 1024 * 1024;
|
|
13
|
+
const SOURCE_AGENT_NORMALIZATION_VERSION = "1";
|
|
12
14
|
export const USER_ROLES = ["admin", "user"];
|
|
13
15
|
export const ARTIFACT_FORMATS = [
|
|
14
16
|
"html",
|
|
@@ -122,6 +124,11 @@ export async function writeIndex(store, index) {
|
|
|
122
124
|
insertVersionRecord(db, artifact.id, version);
|
|
123
125
|
}
|
|
124
126
|
}
|
|
127
|
+
normalizeStoredSourceAgents(db, store, {
|
|
128
|
+
force: true,
|
|
129
|
+
inTransaction: true,
|
|
130
|
+
rebuildSearch: false
|
|
131
|
+
});
|
|
125
132
|
rebuildSearchIndexInDb(db, store);
|
|
126
133
|
});
|
|
127
134
|
} finally {
|
|
@@ -182,7 +189,11 @@ export async function createArtifact(store = createStore(), input = {}) {
|
|
|
182
189
|
export async function updateArtifact(store = createStore(), id, input = {}) {
|
|
183
190
|
const secretScan = assertNoSecrets(input, securityConfig());
|
|
184
191
|
input = withSecretScan(input, secretScan);
|
|
185
|
-
const normalized = normalizeArtifactInput(input, {
|
|
192
|
+
const normalized = normalizeArtifactInput(input, {
|
|
193
|
+
requireContent: true,
|
|
194
|
+
requireTitle: false,
|
|
195
|
+
defaultSourceAgent: ""
|
|
196
|
+
});
|
|
186
197
|
const db = openDatabase(store);
|
|
187
198
|
|
|
188
199
|
try {
|
|
@@ -252,7 +263,11 @@ export async function updateArtifact(store = createStore(), id, input = {}) {
|
|
|
252
263
|
export async function replaceArtifactVersion(store = createStore(), id, versionNumber, input = {}) {
|
|
253
264
|
const secretScan = assertNoSecrets(input, securityConfig());
|
|
254
265
|
input = withSecretScan(input, secretScan);
|
|
255
|
-
const normalized = normalizeArtifactInput(input, {
|
|
266
|
+
const normalized = normalizeArtifactInput(input, {
|
|
267
|
+
requireContent: true,
|
|
268
|
+
requireTitle: false,
|
|
269
|
+
defaultSourceAgent: ""
|
|
270
|
+
});
|
|
256
271
|
const targetVersion = Number(versionNumber);
|
|
257
272
|
if (!Number.isInteger(targetVersion) || targetVersion < 1) {
|
|
258
273
|
throw Object.assign(new Error(`Invalid artifact version: ${versionNumber}`), {
|
|
@@ -467,7 +482,7 @@ export async function listArtifactsPage(store = createStore(), filters = {}) {
|
|
|
467
482
|
const query = normalizeOptionalString(filters.query);
|
|
468
483
|
const normalizedQuery = query.toLowerCase();
|
|
469
484
|
const tag = normalizeOptionalString(filters.tag).toLowerCase();
|
|
470
|
-
const sourceAgent =
|
|
485
|
+
const sourceAgent = normalizeSourceAgent(filters.sourceAgent, { defaultValue: "" }).toLowerCase();
|
|
471
486
|
|
|
472
487
|
try {
|
|
473
488
|
if (query && searchIndexAvailable(db)) {
|
|
@@ -1250,6 +1265,7 @@ function openDatabase(store) {
|
|
|
1250
1265
|
`);
|
|
1251
1266
|
initializeSchema(db);
|
|
1252
1267
|
migrateJsonIndex(db, store);
|
|
1268
|
+
normalizeStoredSourceAgents(db, store);
|
|
1253
1269
|
syncSearchIndexIfEmpty(db, store);
|
|
1254
1270
|
return db;
|
|
1255
1271
|
}
|
|
@@ -1492,6 +1508,140 @@ function migrateJsonIndex(db, store) {
|
|
|
1492
1508
|
});
|
|
1493
1509
|
}
|
|
1494
1510
|
|
|
1511
|
+
function metaValue(db, key) {
|
|
1512
|
+
return db.prepare("SELECT value FROM meta WHERE key = ?").get(key)?.value || "";
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const INFERABLE_SOURCE_AGENTS = new Set(["claude", "codex", "copilot", "cursor", "gemini"]);
|
|
1516
|
+
|
|
1517
|
+
function normalizeStoredSourceAgents(db, store, options = {}) {
|
|
1518
|
+
if (!options.force && metaValue(db, "source_agent_normalized_version") === SOURCE_AGENT_NORMALIZATION_VERSION) {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const run = () => normalizeStoredSourceAgentsInDb(db, store, options);
|
|
1523
|
+
if (options.inTransaction) {
|
|
1524
|
+
run();
|
|
1525
|
+
} else {
|
|
1526
|
+
transaction(db, run);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function normalizeStoredSourceAgentsInDb(db, store, options = {}) {
|
|
1531
|
+
const rows = db.prepare(`
|
|
1532
|
+
SELECT id, source_agent, tags_json
|
|
1533
|
+
FROM artifacts
|
|
1534
|
+
`).all();
|
|
1535
|
+
const updateArtifact = db.prepare("UPDATE artifacts SET source_agent = ?, tags_json = ? WHERE id = ?");
|
|
1536
|
+
let changed = false;
|
|
1537
|
+
|
|
1538
|
+
for (const row of rows) {
|
|
1539
|
+
const normalized = normalizeSourceAgent(row.source_agent, { defaultValue: "unknown" });
|
|
1540
|
+
const inferred = isUnknownSourceAgent(normalized)
|
|
1541
|
+
? inferStoredSourceAgent(db, row)
|
|
1542
|
+
: normalized;
|
|
1543
|
+
const nextSourceAgent = inferred || normalized;
|
|
1544
|
+
const nextTags = normalizeStoredSourceTags(row.tags_json, nextSourceAgent);
|
|
1545
|
+
if (nextSourceAgent !== row.source_agent || nextTags !== row.tags_json) {
|
|
1546
|
+
updateArtifact.run(nextSourceAgent, nextTags, row.id);
|
|
1547
|
+
changed = true;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
const auditRows = db.prepare(`
|
|
1552
|
+
SELECT id, source_agent
|
|
1553
|
+
FROM audit_log
|
|
1554
|
+
WHERE source_agent IS NOT NULL
|
|
1555
|
+
AND TRIM(source_agent) != ''
|
|
1556
|
+
`).all();
|
|
1557
|
+
const updateAudit = db.prepare("UPDATE audit_log SET source_agent = ? WHERE id = ?");
|
|
1558
|
+
for (const row of auditRows) {
|
|
1559
|
+
const normalized = normalizeSourceAgent(row.source_agent, { defaultValue: "" });
|
|
1560
|
+
if (normalized && normalized !== row.source_agent) {
|
|
1561
|
+
updateAudit.run(normalized, row.id);
|
|
1562
|
+
changed = true;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('source_agent_normalized_version', ?)").run(SOURCE_AGENT_NORMALIZATION_VERSION);
|
|
1567
|
+
|
|
1568
|
+
if (changed && options.rebuildSearch !== false) {
|
|
1569
|
+
rebuildSearchIndexInDb(db, store);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function inferStoredSourceAgent(db, artifactRow) {
|
|
1574
|
+
const tagCandidate = firstInferableSourceAgent(parseJson(artifactRow.tags_json, []));
|
|
1575
|
+
if (tagCandidate) {
|
|
1576
|
+
return tagCandidate;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const versionRows = db.prepare(`
|
|
1580
|
+
SELECT metadata_json
|
|
1581
|
+
FROM artifact_versions
|
|
1582
|
+
WHERE artifact_id = ?
|
|
1583
|
+
ORDER BY version DESC
|
|
1584
|
+
`).all(artifactRow.id);
|
|
1585
|
+
for (const row of versionRows) {
|
|
1586
|
+
const metadata = parseJson(row.metadata_json, {});
|
|
1587
|
+
const candidate = firstInferableSourceAgent([
|
|
1588
|
+
metadata.sourceAgent,
|
|
1589
|
+
metadata.source_agent,
|
|
1590
|
+
metadata.agent,
|
|
1591
|
+
metadata.artifactyImport?.sourceAgent,
|
|
1592
|
+
metadata.artifactyImport?.originalAgent
|
|
1593
|
+
]);
|
|
1594
|
+
if (candidate) {
|
|
1595
|
+
return candidate;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
const auditRows = db.prepare(`
|
|
1600
|
+
SELECT source_agent
|
|
1601
|
+
FROM audit_log
|
|
1602
|
+
WHERE artifact_id = ?
|
|
1603
|
+
AND source_agent IS NOT NULL
|
|
1604
|
+
AND TRIM(source_agent) != ''
|
|
1605
|
+
ORDER BY created_at DESC
|
|
1606
|
+
`).all(artifactRow.id);
|
|
1607
|
+
return firstInferableSourceAgent(auditRows.map((row) => row.source_agent));
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function normalizeStoredSourceTags(tagsJson, sourceAgent) {
|
|
1611
|
+
const tags = parseJson(tagsJson, []);
|
|
1612
|
+
if (!Array.isArray(tags)) {
|
|
1613
|
+
return JSON.stringify([]);
|
|
1614
|
+
}
|
|
1615
|
+
const normalizedTags = [];
|
|
1616
|
+
for (const tag of tags) {
|
|
1617
|
+
const trimmed = normalizeOptionalString(tag);
|
|
1618
|
+
if (!trimmed) {
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
const canonical = inferableSourceAgent(trimmed);
|
|
1622
|
+
const next = canonical || (trimmed === "unknown" && INFERABLE_SOURCE_AGENTS.has(sourceAgent) ? sourceAgent : trimmed);
|
|
1623
|
+
if (!normalizedTags.includes(next)) {
|
|
1624
|
+
normalizedTags.push(next);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return JSON.stringify(normalizedTags);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function firstInferableSourceAgent(values) {
|
|
1631
|
+
for (const value of values || []) {
|
|
1632
|
+
const candidate = inferableSourceAgent(value);
|
|
1633
|
+
if (candidate) {
|
|
1634
|
+
return candidate;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return "";
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function inferableSourceAgent(value) {
|
|
1641
|
+
const normalized = normalizeSourceAgent(value, { defaultValue: "" });
|
|
1642
|
+
return INFERABLE_SOURCE_AGENTS.has(normalized) ? normalized : "";
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1495
1645
|
function ensureSearchTable(db) {
|
|
1496
1646
|
try {
|
|
1497
1647
|
const existing = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'artifact_search'").get();
|
|
@@ -1715,7 +1865,7 @@ function insertArtifactRecord(db, artifact) {
|
|
|
1715
1865
|
artifact.title,
|
|
1716
1866
|
normalizeArtifactType(artifact.artifactType || artifact.artifact_type || "document"),
|
|
1717
1867
|
normalizeSchemaVersion(artifact.schemaVersion || artifact.schema_version),
|
|
1718
|
-
artifact.sourceAgent || artifact.source_agent
|
|
1868
|
+
normalizeSourceAgent(artifact.sourceAgent || artifact.source_agent, { defaultValue: "unknown" }),
|
|
1719
1869
|
normalizeOptionalString(artifact.publisherId || artifact.publisher_id) || null,
|
|
1720
1870
|
normalizeOptionalString(artifact.publisherName || artifact.publisher_name) || null,
|
|
1721
1871
|
normalizeOptionalString(artifact.publisherUserId || artifact.publisher_user_id) || null,
|
|
@@ -1925,7 +2075,9 @@ function normalizeArtifactInput(input, options) {
|
|
|
1925
2075
|
contentType: normalizeOptionalString(input.contentType),
|
|
1926
2076
|
artifactType: normalizeArtifactType(input.artifactType || input.artifact_type || inferArtifactType(input)),
|
|
1927
2077
|
schemaVersion: normalizeSchemaVersion(input.schemaVersion || input.schema_version),
|
|
1928
|
-
sourceAgent:
|
|
2078
|
+
sourceAgent: normalizeSourceAgent(input.sourceAgent || input.source_agent || input.agent, {
|
|
2079
|
+
defaultValue: options.defaultSourceAgent ?? "unknown"
|
|
2080
|
+
}),
|
|
1929
2081
|
tags: normalizeTags(input.tags),
|
|
1930
2082
|
metadata: normalizeMetadata(input.metadata)
|
|
1931
2083
|
};
|