artifacty 0.2.0 โ†’ 0.4.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 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:
@@ -174,11 +175,17 @@ artifacty-mcp
174
175
  ```
175
176
 
176
177
  MCP clients can create artifacts with `artifacty_create`. `artifacty_publish` remains as a backwards-compatible alias.
178
+ MCP clients that support resources or prompts can also read `artifacty://recent`,
179
+ `artifacty://artifacts/{id}`, `artifacty://artifacts/{id}/raw{?version}`, and
180
+ `artifacty://schema/v1`, or use prompt templates such as `artifacty_handoff`,
181
+ `artifacty_review`, and `artifacty_release_notes`.
177
182
 
178
183
  Operational commands:
179
184
 
180
185
  ```bash
181
186
  artifacty audit --limit 20
187
+ artifacty index rebuild
188
+ artifacty integrity
182
189
  artifacty backup
183
190
  artifacty export --file ./artifacty-backup.json
184
191
  artifacty import-store --file ./artifacty-backup.json
@@ -200,6 +207,13 @@ ARTIFACTY_HOME=/path/to/shared/store artifacty serve
200
207
 
201
208
  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
209
 
210
+ 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:
211
+
212
+ ```bash
213
+ artifacty index rebuild
214
+ artifacty integrity
215
+ ```
216
+
203
217
  ## API Example
204
218
 
205
219
  Start a protected server in another terminal, or generate a reusable shell token first:
@@ -245,6 +259,13 @@ Browser routes:
245
259
  - `/artifacts/:id/diff`: compare versions.
246
260
  - `/api/audit`: list audit events.
247
261
 
262
+ List APIs support pagination with `limit` and `offset`. Responses keep the top-level `artifacts` array and include `pagination` and `search` metadata:
263
+
264
+ ```bash
265
+ curl -s "http://127.0.0.1:8787/api/artifacts?q=handoff&limit=20&offset=0" \
266
+ -H "x-artifacty-token: $ARTIFACTY_API_TOKEN"
267
+ ```
268
+
248
269
  ## Interface Language
249
270
 
250
271
  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.
@@ -258,6 +279,7 @@ Schema and storage:
258
279
  - Diagram, component, source snippet, analysis report, table, and media assets use `diagram`, `component`, `snippet`, `analysis-report`, `table`, and `asset` artifact types.
259
280
  - Copilot/Cursor examples cover PR reviews, screenshots, demo recordings, and visual evidence bundles.
260
281
  - See [docs/artifact-schema-v1.md](docs/artifact-schema-v1.md).
282
+ - See [docs/mcp-public-api.md](docs/mcp-public-api.md) for MCP tools, resources, prompts, and compatibility notes.
261
283
  - See [docs/sarif-csv-artifact-plan.md](docs/sarif-csv-artifact-plan.md) for the SARIF/CSV output artifact roadmap.
262
284
 
263
285
  ## Security Model
@@ -83,7 +83,21 @@ node src/cli.js check
83
83
  - Cursor: writes project `.cursor/mcp.json` using the Cursor `mcpServers` shape. Pass `--config ~/.cursor/mcp.json` for global Cursor setup.
84
84
  - `--dry-run` returns the generated config without writing it.
85
85
  - `--timeout <ms>` adjusts Codex `startup_timeout_sec` and Gemini `timeout`. It does not change Claude Code startup behavior; set `MCP_TIMEOUT` before launching Claude Code if you need a larger value there.
86
- - `check` starts the local MCP server and verifies required tools through `initialize` and `tools/list`.
86
+ - `check` starts the local MCP server and verifies required tools, resources, and prompts through MCP discovery methods.
87
+
88
+ ## Client Compatibility Matrix
89
+
90
+ | Client | Config shape | Scope | Timeout behavior | Restart requirement | Notes |
91
+ | --- | --- | --- | --- | --- | --- |
92
+ | Claude Code | `.mcp.json` `mcpServers` | project | parent `MCP_TIMEOUT`, default 30s | restart Claude Code | no per-server startup timeout field |
93
+ | Codex | TOML `[mcp_servers.artifacty]` | user or explicit config | `startup_timeout_sec = 30.0` | restart Codex session | Windows cold starts may need the 30s default |
94
+ | Gemini CLI | JSON `mcpServers` | user or project | `timeout: 30000` | reload `/mcp` or restart | `trust` is preserved from installer options |
95
+ | GitHub Copilot in VS Code | `.vscode/mcp.json` `servers` | workspace or user profile | client-managed | reload VS Code MCP server | tools are the primary tested surface |
96
+ | Cursor | `.cursor/mcp.json` `mcpServers` | workspace or global | client-managed | restart/reload Cursor MCP | tools are the primary tested surface |
97
+
98
+ All clients should support MCP tools. Resource and prompt display varies by
99
+ client, so Artifacty's core workflows continue to work through tools even when a
100
+ client does not expose resources or prompts in its UI.
87
101
 
88
102
  Generate a token for protected HTTP routes:
89
103
 
@@ -196,6 +210,25 @@ Then run `/mcp` inside Gemini CLI to confirm that the Artifacty tools are connec
196
210
 
197
211
  Mutating MCP tools scan content for common API keys and private keys before storage. Pass `allowSecrets: true` only for intentional exceptions.
198
212
 
213
+ ## MCP Resources and Prompts
214
+
215
+ Artifacty also exposes read-only MCP resources:
216
+
217
+ - `artifacty://recent`: recent artifacts with pagination and browser URLs.
218
+ - `artifacty://schema/v1`: Artifact schema v1 Markdown.
219
+ - `artifacty://artifacts/{id}`: artifact JSON with metadata, content, and URLs.
220
+ - `artifacty://artifacts/{id}/raw{?version}`: raw latest or versioned content.
221
+
222
+ Prompt templates:
223
+
224
+ - `artifacty_handoff`: continuation handoff.
225
+ - `artifacty_review`: findings-first code review artifact.
226
+ - `artifacty_test_report`: verification evidence.
227
+ - `artifacty_visual_qa`: screenshot/media/browser evidence.
228
+ - `artifacty_release_notes`: release notes with publish evidence and risks.
229
+
230
+ See [mcp-public-api.md](mcp-public-api.md) for the public MCP surface.
231
+
199
232
  ## Importing Agent Artifacts
200
233
 
201
234
  Use `artifacty_import` or the CLI `import` command when the artifact was produced by another agent and needs normalization before sharing.
@@ -0,0 +1,68 @@
1
+ # MCP Public API
2
+
3
+ Artifacty's MCP stdio server is the primary agent-to-agent integration surface.
4
+ The server currently targets MCP protocol `2025-06-18`.
5
+
6
+ ## Capabilities
7
+
8
+ `initialize` advertises:
9
+
10
+ - `tools`: Artifact create, import, list, get, update, archive, restore, audit, and info.
11
+ - `resources`: static and dynamic read-only artifact resources.
12
+ - `prompts`: reusable workflow prompt templates.
13
+
14
+ ## Tools
15
+
16
+ Stable tool names:
17
+
18
+ - `artifacty_create`: create a native Artifacty artifact.
19
+ - `artifacty_publish`: backwards-compatible alias for `artifacty_create`.
20
+ - `artifacty_import`: convert external Claude, Codex, Gemini, Copilot, Cursor, Artifacty, or generic payloads.
21
+ - `artifacty_list`: list artifacts with `query`, `tag`, `sourceAgent`, `includeArchived`, `limit`, and `offset`.
22
+ - `artifacty_get`: read one artifact by `id` and optional `version`.
23
+ - `artifacty_update`: append an immutable version.
24
+ - `artifacty_archive` / `artifacty_restore`: toggle archive state.
25
+ - `artifacty_audit`: list audit events.
26
+ - `artifacty_info`: return local store and browser URL information.
27
+
28
+ Tool schemas use Artifacty schema v1 formats and artifact types. New optional
29
+ properties may be added during 0.x releases; existing names should not be
30
+ renamed without a documented migration.
31
+
32
+ ## Resources
33
+
34
+ Static resources:
35
+
36
+ - `artifacty://recent`: JSON list of recent artifacts with pagination and browser URLs.
37
+ - `artifacty://schema/v1`: Markdown reference for Artifacty schema v1.
38
+
39
+ Resource templates:
40
+
41
+ - `artifacty://artifacts/{id}`: JSON artifact metadata, selected version, content, and URLs.
42
+ - `artifacty://artifacts/{id}/raw{?version}`: raw artifact content for latest or specified version.
43
+
44
+ Resources are read-only and may record an audit `read` event for artifact content.
45
+
46
+ ## Prompts
47
+
48
+ Prompt names:
49
+
50
+ - `artifacty_handoff`
51
+ - `artifacty_review`
52
+ - `artifacty_test_report`
53
+ - `artifacty_visual_qa`
54
+ - `artifacty_release_notes`
55
+
56
+ Each prompt returns one user message that instructs an agent to create or update
57
+ an Artifacty artifact with discoverable `artifactType`, `sourceAgent`, and tags.
58
+ Prompts accept optional context arguments such as `artifactId`, `goal`, `scope`,
59
+ `target`, or `version`.
60
+
61
+ ## Compatibility Notes
62
+
63
+ - The MCP server is local stdio only. Remote MCP auth/OAuth is out of scope.
64
+ - Binary media resources return stored base64 text through MCP; browser `/raw`
65
+ decodes first-class `image` and `video` artifacts into bytes.
66
+ - Clients may display resources and prompts differently. Tools remain the most
67
+ widely supported integration path across Claude, Codex, Gemini, Copilot, and
68
+ Cursor.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifacty",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -37,6 +37,7 @@
37
37
  "docs/artifact-schema-v1.md",
38
38
  "docs/assets/artifacty.png",
39
39
  "docs/integrations.md",
40
+ "docs/mcp-public-api.md",
40
41
  "docs/network-sharing.md",
41
42
  "docs/release-checklist.md",
42
43
  "docs/sarif-csv-artifact-plan.md",
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
- listArtifacts,
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 artifacts = await listArtifacts(store, {
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
- printJson({ artifacts });
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/check.js CHANGED
@@ -14,10 +14,25 @@ export const REQUIRED_MCP_TOOLS = [
14
14
  "artifacty_info"
15
15
  ];
16
16
 
17
+ export const REQUIRED_MCP_RESOURCES = [
18
+ "artifacty://recent",
19
+ "artifacty://schema/v1"
20
+ ];
21
+
22
+ export const REQUIRED_MCP_PROMPTS = [
23
+ "artifacty_handoff",
24
+ "artifacty_review",
25
+ "artifacty_test_report",
26
+ "artifacty_visual_qa",
27
+ "artifacty_release_notes"
28
+ ];
29
+
17
30
  export async function checkMcpTools(options = {}) {
18
31
  const projectDir = path.resolve(options.projectDir || process.cwd());
19
32
  const serverPath = path.resolve(options.serverPath || path.join(projectDir, "src", "mcp-server.js"));
20
33
  const requiredTools = options.requiredTools || REQUIRED_MCP_TOOLS;
34
+ const requiredResources = options.requiredResources || REQUIRED_MCP_RESOURCES;
35
+ const requiredPrompts = options.requiredPrompts || REQUIRED_MCP_PROMPTS;
21
36
  const timeoutMs = Number(options.timeout || 5000);
22
37
  const client = spawnMcpClient({
23
38
  serverPath,
@@ -39,14 +54,26 @@ export async function checkMcpTools(options = {}) {
39
54
  const listed = await client.request("tools/list", {});
40
55
  const toolNames = (listed.tools || []).map((tool) => tool.name).sort();
41
56
  const missingTools = requiredTools.filter((tool) => !toolNames.includes(tool));
57
+ const listedResources = await client.request("resources/list", {});
58
+ const resourceUris = (listedResources.resources || []).map((resource) => resource.uri).sort();
59
+ const missingResources = requiredResources.filter((resource) => !resourceUris.includes(resource));
60
+ const listedPrompts = await client.request("prompts/list", {});
61
+ const promptNames = (listedPrompts.prompts || []).map((prompt) => prompt.name).sort();
62
+ const missingPrompts = requiredPrompts.filter((prompt) => !promptNames.includes(prompt));
42
63
 
43
64
  return {
44
- ok: missingTools.length === 0,
65
+ ok: missingTools.length === 0 && missingResources.length === 0 && missingPrompts.length === 0,
45
66
  serverPath,
46
67
  protocolVersion: initialized.protocolVersion,
47
68
  toolCount: toolNames.length,
69
+ resourceCount: resourceUris.length,
70
+ promptCount: promptNames.length,
48
71
  tools: toolNames,
49
- missingTools
72
+ resources: resourceUris,
73
+ prompts: promptNames,
74
+ missingTools,
75
+ missingResources,
76
+ missingPrompts
50
77
  };
51
78
  } finally {
52
79
  client.close();
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.count", { count: artifacts.length })}</span>
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;