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 +2 -0
- package/README.md +22 -0
- package/docs/integrations.md +34 -1
- package/docs/mcp-public-api.md +68 -0
- package/package.json +2 -1
- package/src/cli.js +44 -6
- package/src/lib/check.js +29 -2
- 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 +337 -7
- 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:
|
|
@@ -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
|
package/docs/integrations.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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/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
|
-
|
|
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.
|
|
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;
|