@yargote/artifacts-cli 0.0.0-stage → 0.1.0-beta.10

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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +309 -2
  3. package/package.json +42 -4
  4. package/skills/artifacts-cli/SKILL.md +78 -0
  5. package/skills/artifacts-cli/references/commands.md +240 -0
  6. package/skills/artifacts-cli/references/verify-retained.md +119 -0
  7. package/skills/artifacts-cli/scripts/prepare-retained.mjs +149 -0
  8. package/skills/artifacts-design/SKILL.md +92 -0
  9. package/skills/artifacts-design/THIRD-PARTY-NOTICES.md +155 -0
  10. package/skills/artifacts-design/fonts/OFL.txt +96 -0
  11. package/skills/artifacts-design/fonts/bricolage-grotesque.woff2 +0 -0
  12. package/skills/artifacts-design/fonts/jetbrains-mono.woff2 +0 -0
  13. package/skills/artifacts-design/fonts/newsreader-italic.woff2 +0 -0
  14. package/skills/artifacts-design/fonts/newsreader.woff2 +0 -0
  15. package/skills/artifacts-design/fonts/public-sans-italic.woff2 +0 -0
  16. package/skills/artifacts-design/fonts/public-sans.woff2 +0 -0
  17. package/skills/artifacts-design/references/canvas.md +100 -0
  18. package/skills/artifacts-design/references/catppuccin.md +38 -0
  19. package/skills/artifacts-design/references/interface-checks.md +60 -0
  20. package/skills/artifacts-design/references/local-checks.md +70 -0
  21. package/skills/artifacts-design/references/visual-explanations.md +13 -0
  22. package/skills/artifacts-design/scripts/embed-fonts.mjs +246 -0
  23. package/skills/artifacts-design/scripts/verify-local.mjs +333 -0
  24. package/skills/artifacts-design/templates/starter.html +170 -0
  25. package/src/auth.d.ts +21 -0
  26. package/src/auth.js +415 -0
  27. package/src/credentials.d.ts +39 -0
  28. package/src/credentials.js +116 -0
  29. package/src/index.d.ts +2 -0
  30. package/src/index.js +343 -0
  31. package/src/operations.d.ts +174 -0
  32. package/src/operations.js +98 -0
  33. package/src/skills.d.ts +9 -0
  34. package/src/skills.js +181 -0
@@ -0,0 +1,240 @@
1
+ # Command workflow
2
+
3
+ Check the installed `artifacts --help` before use. These examples use the canonical resource selected for the task. `ARTIFACT_ID` and `REVISION_ID` stand for IDs observed in actual command output.
4
+
5
+ ## Inspect and install bundled skills
6
+
7
+ When installed help lists `skills`, use these commands without a server or login.
8
+
9
+ ```sh
10
+ artifacts skills
11
+ artifacts skills get artifacts-cli --full
12
+ artifacts skills get --all --full
13
+ artifacts skills path artifacts-cli
14
+ artifacts skills install
15
+ artifacts skills install --agent claude --global
16
+ ```
17
+
18
+ `skills` and `skills list` list names and descriptions. Plain `skills get artifacts-cli` returns the complete `SKILL.md`. `--full` includes references and templates with relative file paths. `skills path` without a name returns the catalog directory.
19
+
20
+ Installation defaults to `artifacts-cli` in `.agents/skills` under the current directory. `--agent claude` selects `.claude/skills`. `--global` selects the user's home directory. Existing agent directories do not affect the default. The discovery file instructs the agent to load the installed CLI's complete instructions with `artifacts skills get artifacts-cli --full`. Installation reports the absolute path, leaves identical files unchanged, and requires `--force` to replace differing files.
21
+
22
+ The published `0.1.0-beta.1` package predates skills commands. Do not assume support without checking help.
23
+
24
+ ## Authenticate
25
+
26
+ ```sh
27
+ artifacts login --server https://artifacts.example.test/mcp
28
+ artifacts whoami
29
+ ```
30
+
31
+ Replace the example with the selected resource before executing it. Service commands select `--server RESOURCE` before `ARTIFACTS_SERVER`, then the connection saved by a successful identity-verified login. Ordinary overrides do not replace the saved connection. A later successful login replaces it with the verified REST resource. A failed login to another server preserves the previous connection and its credentials. Check installed help for saved-connection support.
32
+
33
+ `--state-dir DIR` or `ARTIFACTS_STATE_DIR` selects an isolated private state directory. For automation, `ARTIFACTS_ACCESS_TOKEN` supplies an already authorized bearer token. Do not echo it or put it in an argument.
34
+
35
+ Use `--no-browser` when the environment cannot open a browser automatically. Open the printed authorization URL and keep the CLI running for its loopback callback. A remote machine's loopback callback must be reachable by the browser used for login. Do not replace the callback with an arbitrary public listener.
36
+
37
+ `logout` clears local credentials and attempts refresh-token revocation. It keeps the saved connection, so a later `artifacts login` can reuse it. Copied access JWTs can remain valid until expiry. The existing account page's global session revocation stops access across CLI, REST, MCP, and panel. Local logout does not claim to perform that global action.
38
+
39
+ ## Create a complete snapshot
40
+
41
+ Save this JSON as `snapshot.json`, replacing its content with the requested artifact.
42
+
43
+ ```json
44
+ {
45
+ "metadata": {
46
+ "title": "Project note",
47
+ "description": "A retained note",
48
+ "custom": { "source": "requested content" }
49
+ },
50
+ "content": {
51
+ "kind": "html",
52
+ "text": "<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><title>Project note</title><main><h1>Project note</h1><p>Retained content.</p></main></html>"
53
+ },
54
+ "files": [
55
+ {
56
+ "path": "notes.txt",
57
+ "mediaType": "text/plain",
58
+ "encoding": "utf8",
59
+ "data": "Retained attachment.\n"
60
+ }
61
+ ]
62
+ }
63
+ ```
64
+
65
+ `metadata.description` and `files` are required even when empty. Content kind is `html`, `markdown`, `text`, or `design`. Binary files use `encoding: "base64"` and canonical padded base64. Paths are unique normalized relative paths. Do not use empty, dot, parent, absolute, backslash, percent, or colon path segments. A file cannot also be another file's directory.
66
+
67
+ ```sh
68
+ artifacts create --snapshot snapshot.json
69
+ artifacts list --limit 25
70
+ artifacts read ARTIFACT_ID --out saved.json --quiet
71
+ ```
72
+
73
+ Keep the request ID printed for the mutation. Successful JSON goes to stdout; diagnostics and recovery instructions go to stderr. Exit zero indicates command success. A nonzero exit must be inspected before claiming completion.
74
+
75
+ For `read`, `--out FILE --quiet` saves the returned response without printing successful JSON. It requires a nonempty output path. It preserves the selected response mode, so use the default full response for snapshot verification. Errors still go to stderr and exit nonzero. Without `--quiet`, `read --out FILE` also prints the response to stdout.
76
+
77
+ Check installed help for `--quiet` support before running these examples. Older versions, including `0.1.0-beta.1`, can suppress stdout with `artifacts read ARTIFACT_ID --out saved.json > /dev/null`. This redirection preserves stderr and the command's exit status. Use the same fallback for the revision reads below.
78
+
79
+ ## Prepare a design canvas
80
+
81
+ Read the design skill's `references/canvas.md` through `artifacts skills get artifacts-design --full`. Keep the index in `my-canvas/canvas.json` and complete UTF-8 board documents in `my-canvas/boards/*.html`.
82
+
83
+ ```sh
84
+ artifacts skills path artifacts-cli
85
+ node <returned-skill-directory>/scripts/prepare-retained.mjs ./my-canvas > snapshot.json
86
+ artifacts create --snapshot snapshot.json
87
+ ```
88
+
89
+ The positional form accepts exactly one directory. It preserves the index text and board HTML, sorts snapshot files by path, validates the complete `design` snapshot with the installed contracts, and rejects missing or orphan boards. It uses the directory basename as the title and an empty description. Edit the emitted metadata before saving when needed. Keep the script under the path returned by `skills path` so its package dependencies resolve. All board data, embedded fonts, index text, and metadata share the 1 MiB budget.
90
+
91
+ The existing named-option form of `prepare-retained.mjs` builds a private retained-verification config from saved receipts and an independent read. It does not prepare a content snapshot. The two input forms cannot be combined.
92
+
93
+ ## Organize projects
94
+
95
+ Check that installed help lists `projects` and `move` before using them.
96
+
97
+ ```sh
98
+ artifacts create --snapshot snapshot.json --project "Client onboarding"
99
+ artifacts projects
100
+ artifacts list --project "client onboarding"
101
+ artifacts move ARTIFACT_ID --project Archive
102
+ artifacts move ARTIFACT_ID --no-project
103
+ ```
104
+
105
+ A project name contains 1 to 80 characters after trimming and NFC normalization, without control characters. A name that matches an existing project regardless of letter case joins it and keeps its stored spelling. Every read and list item includes `project`, a name or null. `projects` prints `{ "items": [{ "name": "Client onboarding", "artifactCount": 1 }] }`, ordered by name without regard to letter case. A project exists only while it contains an artifact.
106
+
107
+ `move` takes at least one of `--project NAME`, `--no-project`, `--slug SLUG`, or `--no-slug`, and at most one from each pair. It prints `{ "artifactId": "...", "project": "Archive" }`, or `null` for the project after `--no-project`. An option left out keeps that value. It creates no revision and leaves the publication unchanged. Moving to the current project succeeds without change. A lost move response is recovered with `retry REQUEST_ID` like any other retained mutation.
108
+
109
+ ## Name pages with slugs
110
+
111
+ Check that installed help lists `--slug` before using it.
112
+
113
+ ```sh
114
+ artifacts create --snapshot snapshot.json --project "Mi wiki" --slug concepts/rag
115
+ artifacts read --project "mi wiki" --slug concepts/rag
116
+ artifacts read --slug index
117
+ artifacts move ARTIFACT_ID --slug concepts/retrieval
118
+ artifacts move ARTIFACT_ID --no-slug
119
+ ```
120
+
121
+ A slug contains 1 to 160 characters of lowercase ASCII kebab-case segments separated by `/`, such as `concepts/rag` or `index`. It is never normalized, and a trailing `.md` is rejected with a hint to remove it. A slug is unique within the owner's project, matched by the project's stored spelling. Artifacts without a project share one unfiled namespace, which `read --slug` without `--project` reads. Every read and list item includes `slug`, a name or null.
122
+
123
+ `read` takes exactly one of an ID or `--slug`. `--project` applies only with `--slug`. A used slug fails `create` with `slug_conflict`, status 409, and the holder's `artifactId` in `details`. A locator with no artifact fails with `not_found`. `move ID --slug SLUG` renames a page in its project, and `move ID --no-slug` clears the slug. A move to another project keeps the slug unless `--slug` or `--no-slug` changes it. A move fails with `slug_conflict` and changes nothing when the target project already uses the target slug. A slug change creates no revision.
124
+
125
+ ## Search pages
126
+
127
+ Check that installed help lists `search` before using it.
128
+
129
+ ```sh
130
+ artifacts search envigado
131
+ artifacts search "tarde envigado" --project "mi wiki"
132
+ artifacts search envigado --limit 5 --cursor 5
133
+ ```
134
+
135
+ `search QUERY` finds owned Markdown and text artifacts whose title, description, or content at the head revision contains every term. Quote a query with spaces. The query is plain text of 1 to 256 characters. Every whitespace-separated term must match, letter case and accents are ignored, and operators or punctuation in the query are treated as text, so input such as `"foo" OR NEAR(` never fails with a syntax error. HTML and design artifacts, older revisions, and artifacts shared with the caller are not searched. `--project` matches regardless of letter case. `--limit` is 1 to 25 and defaults to 10. Items are `{ artifactId, project, slug, title, snippet, rank }`, ordered by `rank` and then `artifactId`. `rank` is the FTS5 `bm25` score, where lower is more relevant, and `snippet` is an excerpt with each match wrapped in `**`. Pass `nextCursor` as `--cursor` for the next page; `null` means there are no more results.
136
+
137
+ ## Edit without losing retained data
138
+
139
+ For a title change, save `changes.json` with only the requested field.
140
+
141
+ ```json
142
+ { "metadata": { "title": "Reviewed project note" } }
143
+ ```
144
+
145
+ ```sh
146
+ artifacts edit ARTIFACT_ID --changes changes.json
147
+ artifacts read ARTIFACT_ID --out current.json --quiet
148
+ ```
149
+
150
+ For a complete replacement, read the current head, preserve untouched data in `next-snapshot.json`, and use that observed revision.
151
+
152
+ ```sh
153
+ artifacts update ARTIFACT_ID --snapshot next-snapshot.json --expected-revision REVISION_ID
154
+ ```
155
+
156
+ A complete command file can preserve a caller-owned request ID, snapshot, and expected revision. Use `artifacts create --command create.json`, `artifacts update --command update.json`, or `artifacts publish --command publish.json`. Do not add `response: "full"` to an existing command that omitted it. Optional response modes affect exact mutation identity.
157
+
158
+ Command files contain the command itself, without the CLI's retained-operation wrapper.
159
+
160
+ | Command | Required JSON fields |
161
+ | --- | --- |
162
+ | `create` | `requestId`, `snapshot` |
163
+ | `update` | `requestId`, `artifactId`, `expectedRevisionId`, `snapshot` |
164
+ | `publish` | `requestId`, `artifactId`, `revisionId`, `expectedPublicationVersion` |
165
+
166
+ `requestId` is a caller-chosen string of 8 to 128 characters. Create and update also accept optional `response` with value `full` or `compact`. Create also accepts an optional `project` name and an optional `slug`. This complete update example replaces a snapshot. Substitute the observed IDs and preserve all required content and attachments in its `snapshot`.
167
+
168
+ ```json
169
+ {
170
+ "requestId": "reviewed-update-001",
171
+ "artifactId": "OBSERVED_ARTIFACT_ID",
172
+ "expectedRevisionId": "OBSERVED_HEAD_REVISION_ID",
173
+ "snapshot": {
174
+ "metadata": { "title": "Reviewed project note", "description": "A retained note" },
175
+ "content": { "kind": "text", "text": "Reviewed content." },
176
+ "files": []
177
+ }
178
+ }
179
+ ```
180
+
181
+ Create uses the same `requestId` and complete `snapshot` fields without artifact or expected-revision IDs. A complete publication command has this shape. Replace zero with the observed publication version when already published.
182
+
183
+ ```json
184
+ {
185
+ "requestId": "reviewed-publish-001",
186
+ "artifactId": "OBSERVED_ARTIFACT_ID",
187
+ "revisionId": "SELECTED_REVISION_ID",
188
+ "expectedPublicationVersion": 0
189
+ }
190
+ ```
191
+
192
+ ## Inspect history and publish
193
+
194
+ ```sh
195
+ artifacts history ARTIFACT_ID --summary
196
+ artifacts read ARTIFACT_ID --revision REVISION_ID --out historical.json --quiet
197
+ artifacts publish ARTIFACT_ID --revision REVISION_ID --expected-version 0
198
+ ```
199
+
200
+ The publication example applies only to an unpublished artifact. For an existing publication, replace zero with the `publication.version` observed in a current read. After publishing, read again and compare current publication with the returned receipt.
201
+
202
+ History returns `nextBeforeSequence`; pass it as `--before-sequence N` to continue. List returns `nextCursor`; pass it as `--cursor CURSOR`. Do not substitute an offset or assume that one page contains everything.
203
+
204
+ ## Share publication access
205
+
206
+ Check installed help for these commands. They are available from `0.1.0-beta.6`.
207
+
208
+ ```sh
209
+ artifacts share ARTIFACT_ID --email ana@example.com --request-id share-ana-001
210
+ artifacts shares ARTIFACT_ID
211
+ artifacts unshare ARTIFACT_ID SHARE_ID --request-id unshare-ana-001
212
+ artifacts retry share-ana-001
213
+ ```
214
+
215
+ Share requires the owner's `artifacts:publish` scope, trims and lowercases the email, and prints a share with `shareId`, `artifactId`, `kind: "email"`, `value`, and `createdAt`. `shares` requires the same scope and prints `{ "items": [...] }` with current grants without retaining a mutation. Use an observed `shareId` to unshare. Unshare requires the same scope. Unshare and its successful retry exit zero with empty stdout.
216
+
217
+ The publication admits the owner and signed-in users whose verified email matches a current share. Guests see the published revision and attachments. History, export, editing, and project moves remain owner-only. Shares before first publication wait for publication. Add `--notify` to request one invitation email. Invited people can sign in with an email code when the server enables email.
218
+
219
+ Share and unshare retain complete normalized commands. `retry REQUEST_ID` replays the original operation. A historical share replay does not regrant revoked access, and an old unshare replay leaves a deliberate new share intact. Verify current grants with `shares ARTIFACT_ID` after a replay.
220
+
221
+ Share with `--notify` to request one email attempt. Access succeeds when mail is disabled or the owner has used the daily allowance. The JSON result adds `notification.status` as `requested` or `skipped`, with `disabled` or `rate_limited` as the skip reason. `requested` does not confirm delivery. A crash after commit can lose the message. An exact retry preserves the captured flag and never sends a second email. Use a new deliberate command to request another attempt for existing access.
222
+
223
+ Owners can reserve 100 invitations per UTC day. Revoke, deletion, and failed delivery do not refund reservations. New access is limited to 100 people per artifact and 1000 grants per owner. Existing access remains listable and revocable.
224
+
225
+ ## Verify the exact saved revision
226
+
227
+ Follow [the retained revision recipe](verify-retained.md) after successful authoring and requested publication. Retain each successful write stdout in a private receipt file. The recipe extracts its IDs, performs a full independent explicit-revision read, and prepares the existing verifier config without authoring or account mutations. It includes an installed-browser path and identifies the additional capabilities for complete verification.
228
+
229
+ ## Retry and export
230
+
231
+ ```sh
232
+ artifacts retry REQUEST_ID
233
+ artifacts export ARTIFACT_ID --revision REVISION_ID --out artifact.zip
234
+ ```
235
+
236
+ `retry` loads the saved complete operation. It does not prepare a new edit or change expected versions. Keep the record after an uncertain result. The server checks current authority before replaying an original receipt.
237
+
238
+ `read --out` and `export --out` create files exclusively. Use a new destination if one already exists. Do not delete an existing output merely to rerun an example.
239
+
240
+ The archive contains `artifact.json`, `content.html`, `content.md`, or `content.txt`, and attachments under `files/`. Inspect the manifest's artifact and revision IDs, content, and required attachment bytes. Exporting the same retained revision through REST, MCP, or the panel must yield identical ZIP bytes.
@@ -0,0 +1,119 @@
1
+ # Verify the saved revision in the panel
2
+
3
+ Use the successful create, update, or edit result and an independent full read to select the exact saved revision. Use an authorized browser session to inspect its real panel and, when requested, its protected publication. Preparation alone does not prove persistence or browser behavior.
4
+
5
+ ## Retain the authoring results
6
+
7
+ Before the authorized write, use `umask 077` and save successful stdout to a new private receipt file. For example, add `> write-result.json` to the create or update command you already intend to run. Keep stderr and the retained request ID for recovery. Continue only after exit zero. Do not repeat a write to obtain a receipt after an uncertain response. Follow [retry recovery](commands.md#retry-and-export).
8
+
9
+ If publication was requested, also retain the successful publish stdout as `publish-result.json`. Finish that authorized publication before the independent read below. Do not publish, share, or revoke access merely to verify an artifact.
10
+
11
+ ## Prepare the exact selection
12
+
13
+ Use Node 24+ and a CLI whose help lists `skills path` and `read --quiet`. For older read output, use the [stdout fallback](commands.md#create-a-complete-snapshot). The installed helper requires only Node built-ins. It makes no service request and does not open a browser.
14
+
15
+ Set `observed_panel_origin` to the panel origin observed in the authorized browser or supplied service configuration. Do not derive it from `ARTIFACTS_SERVER` or the publication host. Replace the example origin before execution.
16
+
17
+ ```sh
18
+ umask 077
19
+ observed_panel_origin=https://panel.example.test
20
+ skill_directory=$(artifacts skills path artifacts-cli)
21
+ artifact_id=$(node -e '
22
+ try {
23
+ const id = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).artifactId;
24
+ if (typeof id !== "string" || id.length === 0) throw new Error();
25
+ process.stdout.write(id);
26
+ } catch {
27
+ console.error("Cannot extract the artifact ID from the write receipt");
28
+ process.exitCode = 1;
29
+ }
30
+ ' write-result.json)
31
+ revision_id=$(node -e '
32
+ try {
33
+ const id = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).revision.id;
34
+ if (typeof id !== "string" || id.length === 0) throw new Error();
35
+ process.stdout.write(id);
36
+ } catch {
37
+ console.error("Cannot extract the revision ID from the write receipt");
38
+ process.exitCode = 1;
39
+ }
40
+ ' write-result.json)
41
+ artifacts read "$artifact_id" --revision "$revision_id" --response full --out saved-read.json --quiet
42
+ ```
43
+
44
+ Stop if any extraction or read fails. A compact write receipt can select a revision. The independent read must contain the complete snapshot. Compare its complete metadata, content, and decoded attachment bytes with the intended snapshot. For an update, check the saved parent against the head used to prepare that update.
45
+
46
+ Save `checks.json` with the requested viewport sizes and meaningful expectations for this document. Replace the example selectors and values with intended content behavior. Expectations run in order inside the content iframe after a fresh load at each viewport.
47
+
48
+ ```json
49
+ {
50
+ "viewports": [{ "width": 1440, "height": 900 }, { "width": 390, "height": 844 }],
51
+ "expectations": [
52
+ { "kind": "text", "selector": "#total", "text": "5.00" },
53
+ { "kind": "click", "selector": "#change" },
54
+ { "kind": "text", "selector": "#total", "text": "28.40" }
55
+ ],
56
+ "timeoutMs": 10000
57
+ }
58
+ ```
59
+
60
+ Use `text` for exact rendered text, `visible` for visibility, `count` for an element count, or `attribute` for an exact attribute value. Use `name` and `value` with `attribute`. Interactions also support `fill` with `value`, `check` with `checked`, and `select` with a `values` array. Do not use arbitrary JavaScript or target panel controls in these expectations.
61
+
62
+ Prepare a new private config with a future evidence directory that does not yet exist.
63
+
64
+ ```sh
65
+ node "$skill_directory/scripts/prepare-retained.mjs" \
66
+ --receipt write-result.json --read saved-read.json \
67
+ --panel-origin "$observed_panel_origin" --checks checks.json \
68
+ --evidence "$PWD/new-retained-evidence" --out retained-config.json
69
+ ```
70
+
71
+ When publication was requested, add `--publication-receipt publish-result.json` to that invocation. The helper compares its artifact, revision, version, and URL with the current publication in the saved read. A different publication or concurrent change fails preparation. Report the difference. Do not replace expected versions or publish again to make verification pass.
72
+
73
+ The helper derives all selected IDs and URLs from these inputs. It accepts a newer draft head while keeping the saved revision pinned and reports `headChanged: true`. It emits `status: "prepared"`, never a verification pass. Without a publication receipt, it omits publication coverage even when another revision is published. Existing output files are preserved.
74
+
75
+ ## Inspect with an authorized browser
76
+
77
+ This route works with the installed CLI, its bundled files, and browser-control capabilities. It requires no repository checkout. Use a user-supplied session or complete the normal sign-in flow. CLI OAuth login does not supply panel cookies. Keep browser authentication inside the browser. Never extract credentials to construct a Playwright context or state file.
78
+
79
+ After sign-in setup, perform these read-only checks on the selected artifact. Do not create revisions, publish, share, log out, or revoke sessions. Normal protected-viewer admission can consume its one-use grant through the existing GET handoff.
80
+
81
+ 1. Read `retained-config.json` privately and open its exact `panelUrl`. Compare the artifact, revision, digest, and publication version with `expected`. Current panels expose `data-artifact-id`, `data-revision-id`, `data-revision-sha256`, and `data-publication-version`. Version zero means no publication. Report a newer head separately. Never switch the selected revision to the head.
82
+ 2. Establish the actual browser version and available frame, response, console, download, and viewport controls. Record missing capabilities. A browser name, load event, or empty console does not establish that content loaded.
83
+ 3. Inspect the actual preview iframe source, pinned revision, sandbox, referrer policy, and dimensions. Wait for a distinctive expected element within the configured deadline. Exercise each authorized content control and record the resulting visible state. Repeat at each available requested viewport and check keyboard interactions and overflow where relevant.
84
+ 4. When response inspection or native DevTools is available, inspect the iframe request. Record completion, status, content type, CSP, connection allowlist, cache policy, and DNS policy. Distinguish interactive content, inert fallback, and error responses. A pending request has no final response evidence. A top-level preview navigation deliberately returns 403 and cannot replace this iframe check.
85
+ 5. When the config includes `publicationUrl`, open that separate protected viewer through normal admission. Compare its selected revision, digest, and version with `expected`. Observe the real content and repeat the authorized interactions. Open `Archivos` and compare the complete attachment inventory and revision-bound links. Check protected download bytes when that capability is available. Missing attachment or response evidence remains unavailable.
86
+ 6. After publication interactions, reread the same explicit revision with the quoted IDs into a new private file. Compare its current `publication.revisionId`, `version`, and `url` with the saved read and publish receipt. Reload the selected panel and compare its publication markers again. Report a changed publication as a concurrent change. Do not report protected-publication success for a different selected revision.
87
+ 7. If the frame is blank, distinguish admission, pending transport, policy, browser compatibility, content execution, and browser-control failures from observed evidence. If debugger attachment stalls it, disconnect through normal browser controls, reload the same pinned page, and repeat the interaction with native controls. Preserve authentication and containment. Attribute a debugger failure only when that comparison demonstrates it.
88
+ 8. Save a sanitized report and private screenshots. Include selected IDs, digest, browser version, viewport sizes, actions, resulting visible states, observed response policies, and unavailable checks. Exclude cookies, tokens, viewer grants, callback queries, raw authentication requests, and private content from diagnostic logs.
89
+
90
+ This is partial browser verification. Do not return the complete verifier's pass verdict. Missing exact bytes, ZIP, policy, attachment, console, or viewport evidence must remain explicit.
91
+
92
+ ## Run the existing complete verifier when available
93
+
94
+ The CLI package does not install Playwright, `tsx`, Chrome, or the repository's verifier and rendering-policy modules. Complete automated verification needs an operator-provided installation of that existing verifier with its dependencies and actual supported Chrome. It also needs an explicitly supplied authenticated Playwright `BrowserContext` or owner-private storage-state file prepared through an approved sign-in flow.
95
+
96
+ For repository operators, run this command from the repository root. The paths in this command are repository capabilities, not paths inside an installed CLI package.
97
+
98
+ ```sh
99
+ node --import tsx .agents/skills/verify-artifacts/scripts/verify-retained.ts \
100
+ --config /private/path/retained-config.json \
101
+ --storage-state /private/path/browser-state.json
102
+ ```
103
+
104
+ Pass the prepared config unchanged. The existing verifier validates the full schema and canonical snapshot hash. It checks pinned responses, markers, policies, interactions, ZIP, attachment bytes, viewports, and publication state before and after viewer checks. It writes private evidence and fails on mismatches or concurrent publication changes. Use its callable `verifyRetainedArtifact(context, config)` when an authorized context is supplied. Do not build another viewer or weaken sandbox policies to establish deployed behavior.
105
+
106
+ If this runtime or authenticated input is unavailable, use the partial route above and name that limit. If browser access is unavailable too, report only the persistence checks actually completed.
107
+
108
+ ## Deliver the observed outcomes
109
+
110
+ Report each outcome separately, with its evidence and missing checks.
111
+
112
+ | Outcome | Evidence needed |
113
+ | --- | --- |
114
+ | Persistence | Full independent explicit-revision read and complete intended-snapshot comparison. Name any exact export check separately. |
115
+ | Local rendering | Observed local document layout and interactions. This does not prove hosted rendering. |
116
+ | Panel rendering | Exact real panel selection, pinned iframe, visible content, and observed interaction. Name missing response, policy, byte, console, or viewport checks. |
117
+ | Protected publication | Separate authorized viewer admission for the selected published revision, visible content, interactions, and attachment checks. Report concurrent changes and unavailable checks. |
118
+
119
+ Use `passed`, `failed`, `unavailable`, or `not requested` for each outcome as the evidence permits. Keep session identifiers and private evidence private. A successful helper invocation establishes only input preparation.
@@ -0,0 +1,149 @@
1
+ import { lstat, readFile, readdir, writeFile } from 'node:fs/promises';
2
+ import { basename, join, resolve } from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ function requireValue(condition, message) {
7
+ if (!condition) throw new Error(message);
8
+ }
9
+
10
+ function object(value) {
11
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
12
+ }
13
+
14
+ function prepareRetainedSelection({ receipt, expected, checks, panelOrigin, evidenceDirectory, publicationReceipt }) {
15
+ requireValue(object(receipt) && object(receipt.revision), 'Write receipt must identify a saved revision');
16
+ requireValue(
17
+ typeof receipt.artifactId === 'string' && receipt.artifactId.length > 0 &&
18
+ typeof receipt.revision.id === 'string' && receipt.revision.id.length > 0 &&
19
+ /^[a-f0-9]{64}$/.test(receipt.revision.sha256),
20
+ 'Write receipt must contain artifact, revision, and SHA-256',
21
+ );
22
+ const snapshot = expected?.revision?.snapshot;
23
+ requireValue(
24
+ object(expected) && object(snapshot) && object(snapshot.metadata) &&
25
+ typeof snapshot.metadata.title === 'string' && typeof snapshot.metadata.description === 'string' &&
26
+ object(snapshot.content) && ['html', 'markdown', 'text', 'design'].includes(snapshot.content.kind) &&
27
+ typeof snapshot.content.text === 'string' && Array.isArray(snapshot.files),
28
+ 'Independent read must contain the full snapshot, not a compact response',
29
+ );
30
+ requireValue(expected.artifactId === receipt.artifactId && expected.revision.id === receipt.revision.id && expected.revision.sha256 === receipt.revision.sha256, 'Independent read differs from the saved write receipt');
31
+ requireValue(typeof expected.headRevisionId === 'string' && expected.headRevisionId.length > 0, 'Independent read must identify its current draft head');
32
+ requireValue(object(checks) && Object.keys(checks).every((key) => ['viewports', 'expectations', 'timeoutMs'].includes(key)) && Array.isArray(checks.viewports) && checks.viewports.length > 0 && Array.isArray(checks.expectations), 'Checks must contain viewports and expectations, with optional timeoutMs');
33
+ let panel;
34
+ try { panel = new URL(panelOrigin); } catch { throw new Error('Panel origin must be an observed HTTP origin'); }
35
+ requireValue(['http:', 'https:'].includes(panel.protocol) && !panel.username && !panel.password && panel.pathname === '/' && !panel.search && !panel.hash, 'Panel origin must be an observed HTTP origin without a path, credentials, query, or fragment');
36
+ panel.pathname = `/artifacts/${encodeURIComponent(expected.artifactId)}`;
37
+ panel.searchParams.set('revisionId', expected.revision.id);
38
+ let publicationUrl;
39
+ if (publicationReceipt !== undefined) {
40
+ const current = expected.publication;
41
+ requireValue(
42
+ object(publicationReceipt) && object(current) &&
43
+ publicationReceipt.artifactId === expected.artifactId &&
44
+ publicationReceipt.revisionId === expected.revision.id && publicationReceipt.revisionId === current.revisionId &&
45
+ Number.isInteger(publicationReceipt.version) && publicationReceipt.version > 0 && publicationReceipt.version === current.version &&
46
+ typeof publicationReceipt.url === 'string' && publicationReceipt.url === current.url,
47
+ 'Publication differs from the requested receipt or selected revision; report the changed or different publication',
48
+ );
49
+ let publication;
50
+ try { publication = new URL(current.url); } catch { throw new Error('Publication receipt must contain a stable HTTP URL'); }
51
+ requireValue(['http:', 'https:'].includes(publication.protocol) && !publication.username && !publication.password && !publication.search && !publication.hash && publication.pathname === `/a/${encodeURIComponent(expected.artifactId)}`, 'Publication receipt must contain the selected artifact stable HTTP URL');
52
+ publicationUrl = publication.href;
53
+ }
54
+ return {
55
+ expected,
56
+ panelUrl: panel.href,
57
+ ...(publicationUrl === undefined ? {} : { publicationUrl }),
58
+ ...checks,
59
+ evidenceDirectory: resolve(evidenceDirectory),
60
+ };
61
+ }
62
+
63
+ async function canvasContracts() {
64
+ const name = '@yargote/artifacts-contracts';
65
+ let path;
66
+ try { path = createRequire(import.meta.url).resolve(name); }
67
+ catch (error) {
68
+ if (error.code !== 'MODULE_NOT_FOUND') throw error;
69
+ path = createRequire(new URL('../../../apps/cli/package.json', import.meta.url)).resolve(name);
70
+ }
71
+ return import(pathToFileURL(path).href);
72
+ }
73
+
74
+ async function prepareCanvasDirectory(input) {
75
+ const directory = resolve(input);
76
+ requireValue((await lstat(directory)).isDirectory(), 'Canvas input must be a directory');
77
+ const { canvasIndexSchema, snapshotSchema } = await canvasContracts();
78
+ const indexFile = join(directory, 'canvas.json');
79
+ const indexInfo = await lstat(indexFile);
80
+ requireValue(indexInfo.isFile(), 'canvas.json must be a regular file');
81
+ requireValue(indexInfo.size <= 1_048_576, 'canvas.json exceeds the shared 1 MiB revision budget');
82
+ const indexBytes = await readFile(indexFile);
83
+ const text = indexBytes.toString('utf8');
84
+ requireValue(Buffer.from(text, 'utf8').equals(indexBytes), 'canvas.json must contain valid UTF-8');
85
+ const index = canvasIndexSchema.parse(JSON.parse(text));
86
+ const boardDirectory = join(directory, 'boards');
87
+ requireValue((await lstat(boardDirectory)).isDirectory(), 'boards must be a directory, not a symbolic link');
88
+ const entries = await readdir(boardDirectory, { withFileTypes: true });
89
+ for (const entry of entries) {
90
+ const path = `boards/${entry.name}`;
91
+ requireValue(entry.isFile(), `Canvas board ${path} must be a regular file`);
92
+ requireValue(Object.hasOwn(index.boards, path), `Orphan canvas board ${path} is absent from canvas.json`);
93
+ }
94
+ const files = [];
95
+ for (const path of [...index.order].sort()) {
96
+ const filename = join(directory, path);
97
+ let info;
98
+ try { info = await lstat(filename); }
99
+ catch (error) {
100
+ if (error.code === 'ENOENT') throw new Error(`Canvas board ${path} has no matching file`);
101
+ throw error;
102
+ }
103
+ requireValue(info.isFile(), `Canvas board ${path} must be a regular file`);
104
+ requireValue(info.size <= 1_048_576, `Canvas board ${path} exceeds the shared 1 MiB revision budget`);
105
+ const bytes = await readFile(filename);
106
+ const data = bytes.toString('utf8');
107
+ requireValue(Buffer.from(data, 'utf8').equals(bytes), `Canvas board ${path} must contain valid UTF-8`);
108
+ files.push({ path, mediaType: 'text/html', encoding: 'utf8', data });
109
+ }
110
+ return snapshotSchema.parse({
111
+ metadata: { title: basename(directory), description: '' },
112
+ content: { kind: 'design', text },
113
+ files,
114
+ });
115
+ }
116
+
117
+ const required = ['--receipt', '--read', '--panel-origin', '--checks', '--evidence', '--out'];
118
+ try {
119
+ const args = process.argv.slice(2);
120
+ if (args.length === 1 && !args[0].startsWith('--')) {
121
+ console.log(JSON.stringify(await prepareCanvasDirectory(args[0]), null, 2));
122
+ } else {
123
+ const options = new Map();
124
+ requireValue(args.length % 2 === 0, 'Use named options with one value each');
125
+ for (let index = 0; index < args.length; index += 2) {
126
+ const name = args[index];
127
+ requireValue([...required, '--publication-receipt'].includes(name) && !options.has(name) && typeof args[index + 1] === 'string' && args[index + 1].length > 0, 'Use each supported option once with a nonempty value');
128
+ options.set(name, args[index + 1]);
129
+ }
130
+ requireValue(required.every((name) => options.has(name)), 'Required options are --receipt --read --panel-origin --checks --evidence --out');
131
+ const json = async (name) => {
132
+ try { return JSON.parse(await readFile(options.get(name), 'utf8')); }
133
+ catch { throw new Error(`Cannot read valid JSON for ${name}`); }
134
+ };
135
+ const receipt = await json('--receipt');
136
+ const expected = await json('--read');
137
+ const checks = await json('--checks');
138
+ const publication = options.has('--publication-receipt') ? await json('--publication-receipt') : undefined;
139
+ const config = prepareRetainedSelection({ receipt, expected, checks, panelOrigin: options.get('--panel-origin'), evidenceDirectory: options.get('--evidence'), publicationReceipt: publication });
140
+ try { await writeFile(options.get('--out'), `${JSON.stringify(config, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); }
141
+ catch (error) { throw new Error(error?.code === 'EEXIST' ? 'Config output already exists; choose a new output path' : 'Cannot create the private config output'); }
142
+ const headChanged = expected.headRevisionId !== expected.revision.id;
143
+ console.log(JSON.stringify({ status: 'prepared', headChanged, publication: publication === undefined ? 'not-requested' : 'requested' }));
144
+ if (headChanged) console.error('Current draft head differs from the selected saved revision. The selection remains pinned.');
145
+ }
146
+ } catch (error) {
147
+ console.error(error.message);
148
+ process.exitCode = 1;
149
+ }
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: artifacts-design
3
+ description: Design HTML documents and multi-board design canvases of a retained Artifact with embedded fonts, light and dark Catppuccin tokens, purposeful layout, and rendered checks. Use before writing or restyling HTML or design boards for the artifacts or artifacts-cli skills.
4
+ ---
5
+
6
+ # Design HTML artifacts
7
+
8
+ The calling skill owns the snapshot, size limit, and save workflow. This skill owns how the document looks and behaves. Follow the user's visual direction first. When revising content only, keep an existing artifact's established style. When the user asks to redesign or improve the look, start from these defaults.
9
+
10
+ The document renders in a sandbox that blocks every network request. Fonts, images, and icons load only as `data:` URIs. External stylesheets, Google Fonts links, and CDN scripts silently fail. Inline all CSS and JavaScript.
11
+
12
+ For multiple boards, read [canvas authoring](references/canvas.md) before writing the index or HTML. It defines the board files, pages, geometry, shared font budget, and complete snapshot workflow.
13
+
14
+ ## Choose the composition
15
+
16
+ Before writing HTML, identify the reader's task, the facts and assets available, and what the reader should understand or do first. Use supplied facts. Label illustrative inputs and assumptions, and leave unavailable evidence absent.
17
+
18
+ Choose the element that carries the answer: a comparison, a diagram, a sequence, a chart, or a result beside its inputs. Give it the most space and put its supporting text nearby. If the same composition would fit an unrelated topic unchanged, use the subject's actual relationships or quantities to make the structure more specific. A report can be dense, an explanation can use large scenes, and a calculator can fit on one screen. Do not impose a hero, a section count, or a word budget.
19
+
20
+ ## Use the starter's tokens
21
+
22
+ Copy the `<style>` block of [the starter](templates/starter.html) into the new document. It defines the color, font, and spacing tokens and shared components: contextual text and labels, lede, summary strip, pills, expandable rows, panel, note, table, filter bar, and footer. Delete components the page does not use. Derive every new color and type decision from its tokens.
23
+
24
+ The starter supplies a vocabulary, not a layout. Compose the body for the reader's task:
25
+
26
+ | Purpose | Composition |
27
+ | --- | --- |
28
+ | Report or guide | A conclusion first, a reading column near 68 characters, and evidence beside the claim it supports. |
29
+ | Inventory, ledger, or status | A summary strip, a filter bar with a live count, then one `.row` per record. Its summary shows the identifier, title, and state; its detail holds everything else. Set `--cols` on `.rows` to match the summary cells. Rows stack to one column below 720 px. |
30
+ | Compare options | Aligned criteria in a table, consistent scales, and the tradeoff stated in words. |
31
+ | Explore data | A dominant chart or table sized to carry the page, with filters beside it. |
32
+ | Calculate or simulate | Labeled inputs beside the result they change, with units and a realistic starting example. |
33
+ | Explain a concept | A sequence of large diagrams with short captions; read [visual explanations](references/visual-explanations.md). |
34
+
35
+ Open in a complete state with the user's real data. Never ship an empty shell or placeholder text.
36
+
37
+ ## Typography
38
+
39
+ Use the bundled faces through the starter's tokens:
40
+
41
+ - `--f-display` is Bricolage Grotesque. Use it for `h1` to `h3` and large figures only.
42
+ - `--f-body` is Public Sans for running text, controls, labels, and table headers.
43
+ - `--f-mono` is JetBrains Mono for code and identifiers. Use it for measurements when it helps the reader read or align them.
44
+ - Newsreader is an optional reading face. For a long essay or editorial guide, set `--f-body` to `"Newsreader",Charter,Georgia,serif` and keep Public Sans for controls.
45
+
46
+ Start with `h1` at `clamp(28px,4vw,40px)` and body at 15 to 16 px with a 1.55 line height. Set labels in the body face at 12 to 13 px, in sentence case without added tracking. Adjust the scale for the document's main visual and reading density. Keep `text-wrap: balance` on headings and `font-variant-numeric: tabular-nums` wherever digits align.
47
+
48
+ After the HTML file is complete, embed the fonts it names:
49
+
50
+ ```sh
51
+ node <skill-directory>/scripts/embed-fonts.mjs page.html
52
+ ```
53
+
54
+ For the CLI, `<skill-directory>` is the output of `artifacts skills path artifacts-design`. The script rewrites the file in place and is safe to rerun after edits. It selects families from CSS `font-family` and `font` declarations in style elements and inline styles, including referenced custom properties. Comments, content text, and JavaScript strings do not select fonts. It adds italic faces for CSS `font-style: italic`, italic `font` shorthand, or real `<em>`, `<i>`, and `<cite>` elements. It refuses to write a page above 1 MiB. Three families add about 135 KB. Newsreader adds about 80 to 165 KB. Do not name another family unless you embed its `woff2` yourself, because an unembedded face silently falls back. Without Node, keep the fallback stacks and report that the fonts were not embedded.
55
+
56
+ ## Color
57
+
58
+ The starter follows the viewer's system theme. Light uses Catppuccin Latte and dark uses Mocha, with Blue as the only accent. [Catppuccin roles](references/catppuccin.md) lists every token and the extra chart colors.
59
+
60
+ - Define each color once as a token on bare `:root` and redefine only the tokens in both dark blocks. Never write a literal color in a component rule.
61
+ - Keep `body` on `var(--page)`. Use `--surface` for the objects you set apart, and `--sunken` for table headers and hover.
62
+ - Show state with soft pills: a `*-soft` background and `*-ink` text. Do not fill badges with saturated pastels. Map every state to neutral, accent, ok, warn, or bad. Do not add a hue for an extra state; distinguish it with its label.
63
+ - Use the accent for links, focus, the selected state, and one primary action. Status colors carry meaning only.
64
+ - When the user names a single dark look, set the Mocha values on bare `:root`, keep `color-scheme: dark`, and drop the media blocks.
65
+
66
+ ## Layout and components
67
+
68
+ - Center one `.wrap` column at `--width` with `max(16px, env(safe-area-inset-*))` side gutters. Space siblings with grid or flex `gap`.
69
+ - Use borders and fills by role. A summary strip, a table, and one panel can each earn a surface. Running text and headings sit on the page.
70
+ - Use an eyebrow only for source or scope information the heading does not already give. A label names a field, and numbering appears only for real sequences.
71
+ - Encode attention in form. A row that needs action gets a pill or a muted closed state, not only a different word.
72
+ - Put wide code and diagrams in their own `overflow-x: auto` container. Use a `<table>` only for short records that stay readable when scrolled; use expandable rows for records with long text. The page itself never scrolls sideways at 375 px.
73
+ - Draw diagrams in inline SVG with token colors and labels inside the view box. For icons, use inline Tabler outline SVGs with `currentColor`.
74
+ - Respect `prefers-reduced-motion`. Use motion only to explain a change of state.
75
+
76
+ ## Copy
77
+
78
+ Give the `<title>` a specific name of two to four words, or the name the user already uses for the document. Write labels from the reader's side, in the user's language. State units next to numbers. Put the source and date of the data in the footer.
79
+
80
+ ## Check the rendered page
81
+
82
+ Run the local helper after embedding fonts and before saving:
83
+
84
+ ```sh
85
+ node <skill-directory>/scripts/verify-local.mjs page.html --evidence ./page-evidence
86
+ ```
87
+
88
+ For the CLI, resolve `<skill-directory>` with `artifacts skills path artifacts-design`. Read [local checks](references/local-checks.md) for existing runtime requirements, optional declarative control expectations, browser selection, and report fields. The helper never installs a dependency, changes the HTML, saves, publishes, or requires service credentials. Its report proves **renderizado local** and leaves visual review required. It does not verify the retained revision or authenticated publication.
89
+
90
+ Run the [interface checks](references/interface-checks.md) before saving. They distinguish required checks from visual judgment. Render the finished page at 375 px and 1280 px in both light and dark when browser tools are available. Fix observed failures and repeat the affected checks. Report checks you could not run.
91
+
92
+ The license notices for bundled fonts and adapted guidance are in [third-party notices](THIRD-PARTY-NOTICES.md).