artifacty 0.2.0 → 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 +2 -0
- package/README.md +17 -0
- package/package.json +1 -1
- package/src/cli.js +44 -6
- package/src/lib/i18n.js +8 -0
- package/src/lib/render.js +88 -2
- package/src/lib/storage.js +438 -30
- package/src/mcp-server.js +16 -6
- package/src/server.js +28 -8
package/AGENTS.md
CHANGED
|
@@ -23,6 +23,8 @@ Artifacty shares LLM artifacts over HTTP, CLI, and MCP.
|
|
|
23
23
|
- `node src/cli.js import --agent claude --file artifact.html`: convert and store an external artifact.
|
|
24
24
|
- `node src/cli.js install claude --dry-run`: preview generated MCP config.
|
|
25
25
|
- `node src/cli.js check`: verify MCP tool discovery.
|
|
26
|
+
- `node src/cli.js index rebuild`: rebuild the optional SQLite FTS5 search index.
|
|
27
|
+
- `node src/cli.js integrity`: verify version files, hashes, and orphaned files.
|
|
26
28
|
|
|
27
29
|
## Coding Style & Naming Conventions
|
|
28
30
|
|
package/README.md
CHANGED
|
@@ -165,6 +165,7 @@ List artifacts:
|
|
|
165
165
|
|
|
166
166
|
```bash
|
|
167
167
|
artifacty list
|
|
168
|
+
artifacty list --query review --limit 20 --offset 20
|
|
168
169
|
```
|
|
169
170
|
|
|
170
171
|
Run the MCP server:
|
|
@@ -179,6 +180,8 @@ Operational commands:
|
|
|
179
180
|
|
|
180
181
|
```bash
|
|
181
182
|
artifacty audit --limit 20
|
|
183
|
+
artifacty index rebuild
|
|
184
|
+
artifacty integrity
|
|
182
185
|
artifacty backup
|
|
183
186
|
artifacty export --file ./artifacty-backup.json
|
|
184
187
|
artifacty import-store --file ./artifacty-backup.json
|
|
@@ -200,6 +203,13 @@ ARTIFACTY_HOME=/path/to/shared/store artifacty serve
|
|
|
200
203
|
|
|
201
204
|
Artifact metadata is stored in `artifacty.sqlite`; artifact content is stored as immutable version files under `artifacts/`. The current browser server URL is written to `server.json` so MCP tools can return the correct links when the default port falls back. Existing `index.json` stores are migrated automatically on first access.
|
|
202
205
|
|
|
206
|
+
Search uses a SQLite FTS5 index when the local Node SQLite build supports it. The index covers the latest version body plus title, tags, source agent, artifact type, format, and metadata summary. If FTS5 is unavailable, Artifacty keeps working with metadata search. Rebuild or check the store when needed:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
artifacty index rebuild
|
|
210
|
+
artifacty integrity
|
|
211
|
+
```
|
|
212
|
+
|
|
203
213
|
## API Example
|
|
204
214
|
|
|
205
215
|
Start a protected server in another terminal, or generate a reusable shell token first:
|
|
@@ -245,6 +255,13 @@ Browser routes:
|
|
|
245
255
|
- `/artifacts/:id/diff`: compare versions.
|
|
246
256
|
- `/api/audit`: list audit events.
|
|
247
257
|
|
|
258
|
+
List APIs support pagination with `limit` and `offset`. Responses keep the top-level `artifacts` array and include `pagination` and `search` metadata:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
curl -s "http://127.0.0.1:8787/api/artifacts?q=handoff&limit=20&offset=0" \
|
|
262
|
+
-H "x-artifacty-token: $ARTIFACTY_API_TOKEN"
|
|
263
|
+
```
|
|
264
|
+
|
|
248
265
|
## Interface Language
|
|
249
266
|
|
|
250
267
|
The browser UI defaults to English. Add `?lang=ko` to any browser route to use Korean, for example `http://127.0.0.1:8787/new?lang=ko`. Forms and in-app links preserve the selected language. Documentation is maintained in English only.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -4,11 +4,13 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import {
|
|
6
6
|
archiveArtifact,
|
|
7
|
+
checkStoreIntegrity,
|
|
7
8
|
createArtifact,
|
|
8
9
|
createStore,
|
|
9
10
|
getArtifact,
|
|
10
11
|
listAuditEvents,
|
|
11
|
-
|
|
12
|
+
listArtifactsPage,
|
|
13
|
+
rebuildSearchIndex,
|
|
12
14
|
restoreArtifact,
|
|
13
15
|
updateArtifact
|
|
14
16
|
} from "./lib/storage.js";
|
|
@@ -200,14 +202,37 @@ async function main() {
|
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
if (command === "list") {
|
|
203
|
-
const
|
|
205
|
+
const page = await listArtifactsPage(store, {
|
|
204
206
|
query: options.query,
|
|
205
207
|
tag: Array.isArray(options.tag) ? options.tag[0] : options.tag,
|
|
206
208
|
sourceAgent: options.source,
|
|
207
209
|
includeArchived: options.includeArchived,
|
|
208
|
-
limit: options.limit
|
|
210
|
+
limit: options.limit,
|
|
211
|
+
offset: options.offset
|
|
212
|
+
});
|
|
213
|
+
printJson({
|
|
214
|
+
artifacts: page.artifacts,
|
|
215
|
+
pagination: paginationJson(page),
|
|
216
|
+
search: page.search
|
|
209
217
|
});
|
|
210
|
-
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if ((command === "index" || command === "search") && options._[0] === "rebuild") {
|
|
222
|
+
const result = await rebuildSearchIndex(store);
|
|
223
|
+
printJson(result);
|
|
224
|
+
if (!result.fts5) {
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (command === "integrity" || command === "check-store") {
|
|
231
|
+
const result = await checkStoreIntegrity(store);
|
|
232
|
+
printJson(result);
|
|
233
|
+
if (!result.ok) {
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
}
|
|
211
236
|
return;
|
|
212
237
|
}
|
|
213
238
|
|
|
@@ -298,7 +323,7 @@ function parseArgs(args) {
|
|
|
298
323
|
|
|
299
324
|
if (key === "tag") {
|
|
300
325
|
options.tag = [...(options.tag || []), value];
|
|
301
|
-
} else if (key === "port" || key === "limit" || key === "version" || key === "schema-version" || key === "timeout" || key === "bytes") {
|
|
326
|
+
} else if (key === "port" || key === "limit" || key === "offset" || key === "version" || key === "schema-version" || key === "timeout" || key === "bytes") {
|
|
302
327
|
options[toCamelCase(key)] = Number(value);
|
|
303
328
|
} else {
|
|
304
329
|
options[toCamelCase(key)] = value;
|
|
@@ -342,6 +367,17 @@ async function withUrls(store, artifact) {
|
|
|
342
367
|
};
|
|
343
368
|
}
|
|
344
369
|
|
|
370
|
+
function paginationJson(page) {
|
|
371
|
+
return {
|
|
372
|
+
total: page.total,
|
|
373
|
+
limit: page.limit,
|
|
374
|
+
offset: page.offset,
|
|
375
|
+
hasMore: page.hasMore,
|
|
376
|
+
nextOffset: page.nextOffset,
|
|
377
|
+
previousOffset: page.previousOffset
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
345
381
|
function requireOption(options, name) {
|
|
346
382
|
if (!options[name]) {
|
|
347
383
|
throw new Error(`Missing required option --${name}`);
|
|
@@ -370,11 +406,13 @@ Usage:
|
|
|
370
406
|
artifacty archive <id>
|
|
371
407
|
artifacty restore <id>
|
|
372
408
|
artifacty audit [--artifact <id>] [--limit 100]
|
|
409
|
+
artifacty index rebuild
|
|
410
|
+
artifacty integrity
|
|
373
411
|
artifacty export --file <path>
|
|
374
412
|
artifacty backup [--file <path>]
|
|
375
413
|
artifacty import-store --file <path>
|
|
376
414
|
artifacty service plist|install|uninstall [--dry-run] [--plist <path>]
|
|
377
|
-
artifacty list [--query text] [--tag tag] [--source agent] [--limit 50] [--include-archived]
|
|
415
|
+
artifacty list [--query text] [--tag tag] [--source agent] [--limit 50] [--offset 0] [--include-archived]
|
|
378
416
|
artifacty show <id> [--version n] [--raw]
|
|
379
417
|
|
|
380
418
|
Environment:
|
package/src/lib/i18n.js
CHANGED
|
@@ -15,7 +15,11 @@ const TRANSLATIONS = {
|
|
|
15
15
|
"language.english": "English",
|
|
16
16
|
"language.korean": "Korean",
|
|
17
17
|
"dashboard.count": "{count} artifacts",
|
|
18
|
+
"dashboard.range": "{start}-{end} of {total} artifacts",
|
|
19
|
+
"dashboard.searchBackend": "{backend} search",
|
|
18
20
|
"dashboard.empty": "No artifacts published yet.",
|
|
21
|
+
"dashboard.previous": "Previous",
|
|
22
|
+
"dashboard.next": "Next",
|
|
19
23
|
"filter.search": "Search",
|
|
20
24
|
"filter.tag": "Tag",
|
|
21
25
|
"filter.source": "Source",
|
|
@@ -69,7 +73,11 @@ const TRANSLATIONS = {
|
|
|
69
73
|
"language.english": "영어",
|
|
70
74
|
"language.korean": "한국어",
|
|
71
75
|
"dashboard.count": "아티팩트 {count}개",
|
|
76
|
+
"dashboard.range": "아티팩트 {total}개 중 {start}-{end}",
|
|
77
|
+
"dashboard.searchBackend": "{backend} 검색",
|
|
72
78
|
"dashboard.empty": "아직 게시된 아티팩트가 없습니다.",
|
|
79
|
+
"dashboard.previous": "이전",
|
|
80
|
+
"dashboard.next": "다음",
|
|
73
81
|
"filter.search": "검색",
|
|
74
82
|
"filter.tag": "태그",
|
|
75
83
|
"filter.source": "소스",
|
package/src/lib/render.js
CHANGED
|
@@ -2,17 +2,26 @@ import { EDITOR_CLIENT_PATH, VIEWER_CLIENT_PATH, editorImportMapJson } from "./e
|
|
|
2
2
|
import { createI18n, DEFAULT_LOCALE, editorMessages, localizedHref, switchLocaleHref } from "./i18n.js";
|
|
3
3
|
import { ARTIFACT_FORMATS, ARTIFACT_TYPES } from "./storage.js";
|
|
4
4
|
|
|
5
|
-
export function renderDashboard({ artifacts, baseUrl, filters = {}, locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
5
|
+
export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination, locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
6
6
|
const view = viewContext(locale, currentPath);
|
|
7
|
+
const total = pagination?.total ?? artifacts.length;
|
|
8
|
+
const start = artifacts.length ? (pagination?.offset ?? 0) + 1 : 0;
|
|
9
|
+
const end = artifacts.length ? (pagination?.offset ?? 0) + artifacts.length : 0;
|
|
10
|
+
const searchBackend = pagination?.search?.backend;
|
|
11
|
+
const pager = renderDashboardPager({ pagination, filters, view });
|
|
7
12
|
const rows = artifacts
|
|
8
13
|
.map((artifact) => {
|
|
9
14
|
const tags = artifact.tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("");
|
|
10
15
|
const status = artifact.archivedAt ? statusBadge("archived") : "";
|
|
16
|
+
const snippet = artifact.searchSnippet
|
|
17
|
+
? `<span class="row-snippet">${escapeHtml(artifact.searchSnippet)}</span>`
|
|
18
|
+
: "";
|
|
11
19
|
return `
|
|
12
20
|
<a class="artifact-row" href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">
|
|
13
21
|
<span class="row-main">
|
|
14
22
|
<strong>${escapeHtml(artifact.title)}</strong>
|
|
15
23
|
<span>${escapeHtml(artifact.id)}</span>
|
|
24
|
+
${snippet}
|
|
16
25
|
</span>
|
|
17
26
|
<span>${escapeHtml(artifact.sourceAgent)}</span>
|
|
18
27
|
${typeBadge(artifact.artifactType || "document")}
|
|
@@ -44,10 +53,12 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, locale = DEF
|
|
|
44
53
|
</header>
|
|
45
54
|
<main class="dashboard">
|
|
46
55
|
<section class="toolbar">
|
|
47
|
-
<span>${view.text("dashboard.
|
|
56
|
+
<span>${view.text("dashboard.range", { start, end, total })}</span>
|
|
57
|
+
${searchBackend ? `<span>${view.text("dashboard.searchBackend", { backend: searchBackend })}</span>` : ""}
|
|
48
58
|
</section>
|
|
49
59
|
<form class="filter-form" method="get" action="/">
|
|
50
60
|
${localeInput(view.locale)}
|
|
61
|
+
${filters.limit ? `<input type="hidden" name="limit" value="${escapeAttribute(filters.limit)}">` : ""}
|
|
51
62
|
<input name="q" value="${escapeAttribute(filters.query || "")}" placeholder="${view.attr("filter.search")}">
|
|
52
63
|
<input name="tag" value="${escapeAttribute(filters.tag || "")}" placeholder="${view.attr("filter.tag")}">
|
|
53
64
|
<input name="sourceAgent" value="${escapeAttribute(filters.sourceAgent || "")}" placeholder="${view.attr("filter.source")}">
|
|
@@ -58,12 +69,52 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, locale = DEF
|
|
|
58
69
|
<section class="artifact-list">
|
|
59
70
|
${rows || `<div class="empty">${view.text("dashboard.empty")}</div>`}
|
|
60
71
|
</section>
|
|
72
|
+
${pager}
|
|
61
73
|
</main>
|
|
62
74
|
`,
|
|
63
75
|
locale: view.locale
|
|
64
76
|
});
|
|
65
77
|
}
|
|
66
78
|
|
|
79
|
+
function renderDashboardPager({ pagination, filters, view }) {
|
|
80
|
+
if (!pagination || pagination.total <= pagination.limit) {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const previous = pagination.previousOffset === null
|
|
85
|
+
? `<span class="pager-disabled">${view.text("dashboard.previous")}</span>`
|
|
86
|
+
: `<a href="${view.href(dashboardPageHref(filters, pagination.previousOffset))}">${view.text("dashboard.previous")}</a>`;
|
|
87
|
+
const next = pagination.nextOffset === null
|
|
88
|
+
? `<span class="pager-disabled">${view.text("dashboard.next")}</span>`
|
|
89
|
+
: `<a href="${view.href(dashboardPageHref(filters, pagination.nextOffset))}">${view.text("dashboard.next")}</a>`;
|
|
90
|
+
|
|
91
|
+
return `<nav class="pager" aria-label="Pagination">${previous}${next}</nav>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function dashboardPageHref(filters, offset) {
|
|
95
|
+
const params = new URLSearchParams();
|
|
96
|
+
if (filters.query) {
|
|
97
|
+
params.set("q", filters.query);
|
|
98
|
+
}
|
|
99
|
+
if (filters.tag) {
|
|
100
|
+
params.set("tag", filters.tag);
|
|
101
|
+
}
|
|
102
|
+
if (filters.sourceAgent) {
|
|
103
|
+
params.set("sourceAgent", filters.sourceAgent);
|
|
104
|
+
}
|
|
105
|
+
if (filters.includeArchived) {
|
|
106
|
+
params.set("includeArchived", "true");
|
|
107
|
+
}
|
|
108
|
+
if (filters.limit) {
|
|
109
|
+
params.set("limit", filters.limit);
|
|
110
|
+
}
|
|
111
|
+
if (offset > 0) {
|
|
112
|
+
params.set("offset", String(offset));
|
|
113
|
+
}
|
|
114
|
+
const query = params.toString();
|
|
115
|
+
return query ? `/?${query}` : "/";
|
|
116
|
+
}
|
|
117
|
+
|
|
67
118
|
export function renderArtifactFormPage({ mode, baseUrl, artifact, version, content, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
68
119
|
const view = viewContext(locale, currentPath);
|
|
69
120
|
const isEdit = mode === "edit";
|
|
@@ -705,6 +756,34 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
705
756
|
background: var(--panel);
|
|
706
757
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
707
758
|
}
|
|
759
|
+
.pager {
|
|
760
|
+
display: flex;
|
|
761
|
+
justify-content: flex-end;
|
|
762
|
+
gap: 8px;
|
|
763
|
+
margin-top: 14px;
|
|
764
|
+
font-family: var(--mono);
|
|
765
|
+
font-size: 12.5px;
|
|
766
|
+
}
|
|
767
|
+
.pager a,
|
|
768
|
+
.pager-disabled {
|
|
769
|
+
display: inline-flex;
|
|
770
|
+
min-height: 34px;
|
|
771
|
+
align-items: center;
|
|
772
|
+
justify-content: center;
|
|
773
|
+
padding: 5px 12px;
|
|
774
|
+
border: 1px solid var(--line-2);
|
|
775
|
+
border-radius: 8px;
|
|
776
|
+
background: var(--panel);
|
|
777
|
+
color: var(--muted);
|
|
778
|
+
}
|
|
779
|
+
.pager a:hover {
|
|
780
|
+
border-color: var(--accent);
|
|
781
|
+
color: var(--text);
|
|
782
|
+
text-decoration: none;
|
|
783
|
+
}
|
|
784
|
+
.pager-disabled {
|
|
785
|
+
opacity: 0.55;
|
|
786
|
+
}
|
|
708
787
|
.editor-form {
|
|
709
788
|
display: grid;
|
|
710
789
|
gap: 16px;
|
|
@@ -889,6 +968,13 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
889
968
|
font-size: 12px;
|
|
890
969
|
color: var(--faint);
|
|
891
970
|
}
|
|
971
|
+
.row-main .row-snippet {
|
|
972
|
+
color: var(--muted);
|
|
973
|
+
white-space: normal;
|
|
974
|
+
overflow: visible;
|
|
975
|
+
text-overflow: clip;
|
|
976
|
+
overflow-wrap: anywhere;
|
|
977
|
+
}
|
|
892
978
|
.artifact-row > span:not(.row-main):not(.tags):not(.badge) {
|
|
893
979
|
font-family: var(--mono);
|
|
894
980
|
font-size: 12.5px;
|
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";
|
|
@@ -112,6 +112,7 @@ export async function writeIndex(store, index) {
|
|
|
112
112
|
const db = openDatabase(store);
|
|
113
113
|
try {
|
|
114
114
|
transaction(db, () => {
|
|
115
|
+
clearSearchIndex(db);
|
|
115
116
|
db.prepare("DELETE FROM artifact_versions").run();
|
|
116
117
|
db.prepare("DELETE FROM artifacts").run();
|
|
117
118
|
for (const artifact of index.artifacts) {
|
|
@@ -120,6 +121,7 @@ export async function writeIndex(store, index) {
|
|
|
120
121
|
insertVersionRecord(db, artifact.id, version);
|
|
121
122
|
}
|
|
122
123
|
}
|
|
124
|
+
rebuildSearchIndexInDb(db, store);
|
|
123
125
|
});
|
|
124
126
|
} finally {
|
|
125
127
|
db.close();
|
|
@@ -155,6 +157,7 @@ export async function createArtifact(store = createStore(), input = {}) {
|
|
|
155
157
|
|
|
156
158
|
insertArtifactRecord(db, artifact);
|
|
157
159
|
insertVersionRecord(db, id, version);
|
|
160
|
+
upsertSearchIndex(db, artifact, version, normalized.content);
|
|
158
161
|
insertAuditRecord(db, {
|
|
159
162
|
action: input.auditAction || "create",
|
|
160
163
|
artifactId: id,
|
|
@@ -209,6 +212,7 @@ export async function updateArtifact(store = createStore(), id, input = {}) {
|
|
|
209
212
|
artifact.id
|
|
210
213
|
);
|
|
211
214
|
insertVersionRecord(db, artifact.id, version);
|
|
215
|
+
upsertSearchIndex(db, artifact, version, normalized.content);
|
|
212
216
|
insertAuditRecord(db, {
|
|
213
217
|
action: input.auditAction || "update",
|
|
214
218
|
artifactId: artifact.id,
|
|
@@ -280,30 +284,316 @@ export async function restoreArtifact(store = createStore(), id, options = {}) {
|
|
|
280
284
|
}
|
|
281
285
|
|
|
282
286
|
export async function listArtifacts(store = createStore(), filters = {}) {
|
|
283
|
-
|
|
287
|
+
return (await listArtifactsPage(store, filters)).artifacts;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function listArtifactsPage(store = createStore(), filters = {}) {
|
|
291
|
+
const db = openDatabase(store);
|
|
284
292
|
const limit = clampInteger(filters.limit, 1, 200, 50);
|
|
285
|
-
const
|
|
293
|
+
const offset = clampInteger(filters.offset, 0, 1_000_000, 0);
|
|
294
|
+
const query = normalizeOptionalString(filters.query);
|
|
295
|
+
const normalizedQuery = query.toLowerCase();
|
|
286
296
|
const tag = normalizeOptionalString(filters.tag).toLowerCase();
|
|
287
297
|
const sourceAgent = normalizeOptionalString(filters.sourceAgent).toLowerCase();
|
|
288
298
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
}
|
|
293
318
|
}
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
});
|
|
296
372
|
}
|
|
297
|
-
if (
|
|
298
|
-
|
|
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
|
+
});
|
|
299
378
|
}
|
|
300
|
-
|
|
301
|
-
|
|
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
|
+
}
|
|
302
416
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
.
|
|
306
|
-
|
|
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 ");
|
|
307
597
|
}
|
|
308
598
|
|
|
309
599
|
export async function getArtifact(store = createStore(), id, options = {}) {
|
|
@@ -458,6 +748,7 @@ function openDatabase(store) {
|
|
|
458
748
|
`);
|
|
459
749
|
initializeSchema(db);
|
|
460
750
|
migrateJsonIndex(db, store);
|
|
751
|
+
syncSearchIndexIfEmpty(db, store);
|
|
461
752
|
return db;
|
|
462
753
|
}
|
|
463
754
|
|
|
@@ -516,6 +807,7 @@ function initializeSchema(db) {
|
|
|
516
807
|
ensureColumn(db, "artifacts", "schema_version", "INTEGER NOT NULL DEFAULT 1");
|
|
517
808
|
ensureColumn(db, "artifacts", "archived_at", "TEXT");
|
|
518
809
|
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('store_version', ?)").run(String(STORE_VERSION));
|
|
810
|
+
ensureSearchTable(db);
|
|
519
811
|
}
|
|
520
812
|
|
|
521
813
|
function migrateJsonIndex(db, store) {
|
|
@@ -551,6 +843,135 @@ function migrateJsonIndex(db, store) {
|
|
|
551
843
|
});
|
|
552
844
|
}
|
|
553
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
|
+
|
|
554
975
|
function loadArtifacts(db) {
|
|
555
976
|
const rows = db.prepare(`
|
|
556
977
|
SELECT id, title, artifact_type, schema_version, source_agent, tags_json, created_at, updated_at, latest_version, archived_at
|
|
@@ -914,19 +1335,6 @@ function makeArtifactId(title) {
|
|
|
914
1335
|
return `${slug}-${randomUUID().slice(0, 8)}`;
|
|
915
1336
|
}
|
|
916
1337
|
|
|
917
|
-
function artifactMatchesQuery(artifact, query) {
|
|
918
|
-
const haystack = [
|
|
919
|
-
artifact.id,
|
|
920
|
-
artifact.title,
|
|
921
|
-
artifact.sourceAgent,
|
|
922
|
-
...artifact.tags
|
|
923
|
-
]
|
|
924
|
-
.join(" ")
|
|
925
|
-
.toLowerCase();
|
|
926
|
-
|
|
927
|
-
return haystack.includes(query);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
1338
|
function clampInteger(value, min, max, fallback) {
|
|
931
1339
|
const parsed = Number.parseInt(value, 10);
|
|
932
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: {
|
|
@@ -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
|
|
package/src/server.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
createArtifact,
|
|
10
10
|
createStore,
|
|
11
11
|
getArtifact,
|
|
12
|
-
|
|
12
|
+
listArtifactsPage,
|
|
13
13
|
listAuditEvents,
|
|
14
14
|
MAX_ARTIFACT_BYTES,
|
|
15
15
|
restoreArtifact,
|
|
@@ -177,15 +177,19 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
177
177
|
query: url.searchParams.get("q") || "",
|
|
178
178
|
tag: url.searchParams.get("tag") || "",
|
|
179
179
|
sourceAgent: url.searchParams.get("sourceAgent") || "",
|
|
180
|
-
includeArchived: url.searchParams.get("includeArchived") === "true"
|
|
180
|
+
includeArchived: url.searchParams.get("includeArchived") === "true",
|
|
181
|
+
limit: url.searchParams.get("limit") || undefined,
|
|
182
|
+
offset: url.searchParams.get("offset") || undefined
|
|
181
183
|
};
|
|
182
|
-
const
|
|
184
|
+
const page = await listArtifactsPage(store, {
|
|
183
185
|
query: filters.query || undefined,
|
|
184
186
|
tag: filters.tag || undefined,
|
|
185
187
|
sourceAgent: filters.sourceAgent || undefined,
|
|
186
|
-
includeArchived: filters.includeArchived
|
|
188
|
+
includeArchived: filters.includeArchived,
|
|
189
|
+
limit: filters.limit,
|
|
190
|
+
offset: filters.offset
|
|
187
191
|
});
|
|
188
|
-
return sendHtml(response, renderDashboard({ artifacts, baseUrl, filters, locale, currentPath }), 200, headOnly);
|
|
192
|
+
return sendHtml(response, renderDashboard({ artifacts: page.artifacts, baseUrl, filters, pagination: page, locale, currentPath }), 200, headOnly);
|
|
189
193
|
}
|
|
190
194
|
|
|
191
195
|
if (method === "GET" && pathname === "/new") {
|
|
@@ -240,14 +244,19 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
if (method === "GET" && pathname === "/api/artifacts") {
|
|
243
|
-
const
|
|
247
|
+
const page = await listArtifactsPage(store, {
|
|
244
248
|
query: url.searchParams.get("q") || undefined,
|
|
245
249
|
tag: url.searchParams.get("tag") || undefined,
|
|
246
250
|
sourceAgent: url.searchParams.get("sourceAgent") || undefined,
|
|
247
251
|
includeArchived: url.searchParams.get("includeArchived") === "true",
|
|
248
|
-
limit: url.searchParams.get("limit") || undefined
|
|
252
|
+
limit: url.searchParams.get("limit") || undefined,
|
|
253
|
+
offset: url.searchParams.get("offset") || undefined
|
|
249
254
|
});
|
|
250
|
-
return sendJson(response, {
|
|
255
|
+
return sendJson(response, {
|
|
256
|
+
artifacts: page.artifacts,
|
|
257
|
+
pagination: paginationJson(page),
|
|
258
|
+
search: page.search
|
|
259
|
+
}, 200, headOnly);
|
|
251
260
|
}
|
|
252
261
|
|
|
253
262
|
if (method === "GET" && pathname === "/api/audit") {
|
|
@@ -442,6 +451,17 @@ export function decorateArtifactUrls(artifact, baseUrl) {
|
|
|
442
451
|
};
|
|
443
452
|
}
|
|
444
453
|
|
|
454
|
+
function paginationJson(page) {
|
|
455
|
+
return {
|
|
456
|
+
total: page.total,
|
|
457
|
+
limit: page.limit,
|
|
458
|
+
offset: page.offset,
|
|
459
|
+
hasMore: page.hasMore,
|
|
460
|
+
nextOffset: page.nextOffset,
|
|
461
|
+
previousOffset: page.previousOffset
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
445
465
|
export async function readJsonBody(request) {
|
|
446
466
|
const raw = await readBody(request, MAX_ARTIFACT_BYTES + 1024);
|
|
447
467
|
if (!raw.trim()) {
|