@yargote/artifacts-cli 0.1.0-beta.1 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -7
- package/package.json +8 -4
- package/skills/artifacts-cli/SKILL.md +56 -0
- package/skills/artifacts-cli/references/commands.md +156 -0
- package/src/index.js +26 -5
- package/src/skills.d.ts +9 -0
- package/src/skills.js +166 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Artifacts CLI reference
|
|
2
2
|
|
|
3
|
-
The `artifacts` Node CLI reads and writes retained artifacts through REST. It prints JSON results to stdout and diagnostics to stderr. Node 24 or later is required. Its private credential database uses Node's experimental built-in SQLite API.
|
|
3
|
+
The `artifacts` Node CLI reads and writes retained artifacts through REST. It prints JSON results to stdout and diagnostics to stderr. Offline skills commands print plain text. Node 24 or later is required. Its private credential database uses Node's experimental built-in SQLite API.
|
|
4
4
|
|
|
5
5
|
The `@yargote/artifacts-cli` package exposes the `artifacts` binary as compiled ESM JavaScript. Its client and contracts dependencies install automatically. Repository development uses `pnpm artifacts` with the same command syntax.
|
|
6
6
|
|
|
@@ -11,9 +11,9 @@ Use Node 24 or later. The beta targets macOS and Linux. Windows has not been ver
|
|
|
11
11
|
Choose one persistent installation:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
npm install --global @yargote/artifacts-cli@0.1.0-beta.
|
|
14
|
+
npm install --global @yargote/artifacts-cli@0.1.0-beta.2
|
|
15
15
|
# Or, with pnpm and an existing global bin directory on PATH:
|
|
16
|
-
pnpm add --global @yargote/artifacts-cli@0.1.0-beta.
|
|
16
|
+
pnpm add --global @yargote/artifacts-cli@0.1.0-beta.2
|
|
17
17
|
artifacts --version
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -22,8 +22,8 @@ If pnpm reports that no global bin directory exists, run `pnpm setup` and reopen
|
|
|
22
22
|
For one-off execution, pin the same version:
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
|
-
npx --yes @yargote/artifacts-cli@0.1.0-beta.
|
|
26
|
-
pnpm dlx @yargote/artifacts-cli@0.1.0-beta.
|
|
25
|
+
npx --yes @yargote/artifacts-cli@0.1.0-beta.2 --help
|
|
26
|
+
pnpm dlx @yargote/artifacts-cli@0.1.0-beta.2 --help
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
`@beta` follows future prereleases. Exact versions remain reproducible. No stable `latest` release is part of this beta. An organization's pnpm minimum release age or trust policy can delay a new release. Follow that policy rather than disabling it globally.
|
|
@@ -80,6 +80,8 @@ Run `artifacts logout` before uninstalling if you want to clear credentials. Uni
|
|
|
80
80
|
|
|
81
81
|
## Server and local state
|
|
82
82
|
|
|
83
|
+
Skills, help, and version commands work without a server or login. Skills commands do not read credentials or create private state. They accept and ignore global `--server` and `--state-dir` values, including their environment defaults.
|
|
84
|
+
|
|
83
85
|
`--server RESOURCE` selects the exact canonical OAuth resource. `ARTIFACTS_SERVER` supplies the same value. A resource ending in `/mcp` uses REST under `/mcp/v1`. The CLI has no production default.
|
|
84
86
|
|
|
85
87
|
`--state-dir DIR` overrides `ARTIFACTS_STATE_DIR`. The default is `~/.local/state/artifacts`. The state directory must be private and owned by the current user. Credentials are separate from operation files. Profiles bind credentials to the exact resource. Retained operation paths hash the resource and request ID, so request IDs cannot select filesystem paths.
|
|
@@ -109,7 +111,7 @@ artifacts whoami
|
|
|
109
111
|
artifacts create --snapshot FILE [--request-id ID] [--response full|compact]
|
|
110
112
|
artifacts create --command FILE
|
|
111
113
|
artifacts list [--limit N] [--cursor CURSOR]
|
|
112
|
-
artifacts read ID [--revision REV] [--response full|compact] [--out FILE]
|
|
114
|
+
artifacts read ID [--revision REV] [--response full|compact] [--out FILE [--quiet]]
|
|
113
115
|
artifacts update ID --snapshot FILE --expected-revision REV [--request-id ID]
|
|
114
116
|
[--response full|compact]
|
|
115
117
|
artifacts update [ID] --command FILE
|
|
@@ -119,10 +121,42 @@ artifacts publish ID --revision REV --expected-version N [--request-id ID]
|
|
|
119
121
|
artifacts publish [ID] --command FILE
|
|
120
122
|
artifacts export ID --revision REV --out FILE
|
|
121
123
|
artifacts retry REQUEST_ID
|
|
124
|
+
artifacts skills [list]
|
|
125
|
+
artifacts skills get NAME [--full]
|
|
126
|
+
artifacts skills get --all [--full]
|
|
127
|
+
artifacts skills path [NAME]
|
|
128
|
+
artifacts skills install [NAME] [--agent claude] [--global] [--force]
|
|
122
129
|
```
|
|
123
130
|
|
|
124
131
|
Global `--server RESOURCE` and `--state-dir DIR` options can appear before or after the command. Options cannot be repeated. Unknown options and options for another command fail before submission.
|
|
125
132
|
|
|
133
|
+
## Bundled skills
|
|
134
|
+
|
|
135
|
+
Check `artifacts --help` for `skills` support. The published `0.1.0-beta.1` package predates this command group. These commands describe the current source and packages built from it.
|
|
136
|
+
|
|
137
|
+
`artifacts skills` and `artifacts skills list` print each bundled skill's name and description, separated by a tab. The initial catalog contains `artifacts-cli`. Its complete source directory is bundled with the CLI, so the instructions match the installed version and work offline.
|
|
138
|
+
|
|
139
|
+
`artifacts skills get artifacts-cli` prints the complete `SKILL.md` without headers or changes to its bytes. `--full` includes every bundled reference and template. Each file starts with a catalog-relative marker such as `=== artifacts-cli/references/commands.md ===`. `artifacts skills get --all` prints every `SKILL.md` with these markers. Add `--full` to include every skill's support files.
|
|
140
|
+
|
|
141
|
+
`artifacts skills path` prints the absolute bundled catalog directory. Add a skill name to print that skill's directory. These paths belong to the CLI package and do not depend on the current directory.
|
|
142
|
+
|
|
143
|
+
Install a discovery skill for an agent with one of these commands.
|
|
144
|
+
|
|
145
|
+
| Command | Destination |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `artifacts skills install` | `./.agents/skills/artifacts-cli/SKILL.md` |
|
|
148
|
+
| `artifacts skills install --agent claude` | `./.claude/skills/artifacts-cli/SKILL.md` |
|
|
149
|
+
| `artifacts skills install --global` | `~/.agents/skills/artifacts-cli/SKILL.md` |
|
|
150
|
+
| `artifacts skills install --agent claude --global` | `~/.claude/skills/artifacts-cli/SKILL.md` |
|
|
151
|
+
|
|
152
|
+
The optional name defaults to `artifacts-cli`. The default destination always uses `.agents`, even if `.claude` already exists. Only explicit `--agent claude` selects `.claude`. Local paths resolve from the current directory. Global paths resolve from the user's home directory.
|
|
153
|
+
|
|
154
|
+
The command prints the absolute destination. It leaves identical files unchanged, including their modification time. A differing file requires `--force`. Forced installation replaces only `SKILL.md` and preserves neighboring files. A directory or symbolic link at `SKILL.md` is rejected.
|
|
155
|
+
|
|
156
|
+
The discovery file instructs the agent to run `artifacts skills get artifacts-cli --full` and follow the returned instructions. It contains no copied workflow or fixed package path. After a CLI upgrade, the same discovery command loads that installed version's skill.
|
|
157
|
+
|
|
158
|
+
## Artifact input and output
|
|
159
|
+
|
|
126
160
|
Create and update snapshot files contain the complete domain snapshot.
|
|
127
161
|
|
|
128
162
|
```json
|
|
@@ -133,7 +167,11 @@ Create and update snapshot files contain the complete domain snapshot.
|
|
|
133
167
|
}
|
|
134
168
|
```
|
|
135
169
|
|
|
136
|
-
`read --out FILE` saves the returned artifact JSON and also prints it to stdout.
|
|
170
|
+
`read --out FILE` saves the returned artifact JSON and also prints it to stdout. Add `--quiet` to suppress successful stdout while saving the same bytes for the selected response mode. `--quiet` is available only for `read` and requires a nonempty `--out FILE`. Errors still go to stderr and exit with code `1`.
|
|
171
|
+
|
|
172
|
+
Check installed help for `--quiet` support. Version `0.1.0-beta.1` predates this option. With that version, `artifacts read ARTIFACT_ID --out saved.json > /dev/null` suppresses stdout while preserving stderr and the exit status.
|
|
173
|
+
|
|
174
|
+
A default full read contains the complete snapshot under `revision.snapshot`. `export --out FILE` saves the exact ZIP and prints its filename, local path, media type, SHA-256, and byte count. Output files use exclusive creation and never overwrite an existing path.
|
|
137
175
|
|
|
138
176
|
History defaults to complete revisions. `--summary` returns revision metadata and attachment summaries. `--before-sequence` continues a page. `list --cursor` continues an artifact list.
|
|
139
177
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yargote/artifacts-cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Command-line client for the Artifacts REST API.",
|
|
@@ -23,14 +23,18 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"src",
|
|
25
25
|
"README.md",
|
|
26
|
-
"LICENSE"
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"skills"
|
|
27
28
|
],
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@yargote/artifacts-contracts": "0.1.0-beta.
|
|
30
|
-
"@yargote/artifacts-client": "0.1.0-beta.
|
|
30
|
+
"@yargote/artifacts-contracts": "0.1.0-beta.2",
|
|
31
|
+
"@yargote/artifacts-client": "0.1.0-beta.2",
|
|
31
32
|
"openid-client": "6.8.8",
|
|
32
33
|
"zod": "4.6.5"
|
|
33
34
|
},
|
|
35
|
+
"artifactsSkills": [
|
|
36
|
+
"artifacts-cli"
|
|
37
|
+
],
|
|
34
38
|
"publishConfig": {
|
|
35
39
|
"access": "public",
|
|
36
40
|
"tag": "beta",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: artifacts-cli
|
|
3
|
+
description: Create, revise, inspect, publish, and export retained Artifacts through the artifacts CLI and REST API. Use when the user chooses the CLI or a terminal workflow for Artifacts. Exclude generic build artifacts and unrelated local files.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Manage retained Artifacts with the CLI
|
|
7
|
+
|
|
8
|
+
Use the installed `artifacts` executable. Run `artifacts --help` and read [the command workflow](references/commands.md) before the first operation. With bundled skills support, `artifacts skills get artifacts-cli --full` returns this skill and its complete command reference. If the executable is unavailable, report that prerequisite. Do not substitute database writes or invent CLI results.
|
|
9
|
+
|
|
10
|
+
## Select the account and resource
|
|
11
|
+
|
|
12
|
+
Use the resource selected by the user or the existing `ARTIFACTS_SERVER` environment value. Pass `--server RESOURCE` or set `ARTIFACTS_SERVER` for service commands. Skills, help, and version commands work offline without a server or login. Login does not set an implicit default. A resource is the canonical OAuth URL, such as `https://artifacts.example.test/mcp`. REST routes are below that resource at `/v1`. Do not infer a production connection from an example or silently switch to a local server.
|
|
13
|
+
|
|
14
|
+
Run `artifacts whoami` before the first mutation when the current identity is not established. Keep the selected resource and owner consistent throughout one operation. If login is needed, use `artifacts login --server RESOURCE`. The user authenticates in the browser. Do not collect their password or read another application's credentials. Never put tokens in command arguments, operation files, logs, or the final reply.
|
|
15
|
+
|
|
16
|
+
## Prepare retained content
|
|
17
|
+
|
|
18
|
+
A revision is a complete snapshot with `metadata`, `content`, and `files`. The command reference contains its JSON shape. Use the requested title, language, content, and attachments. Do not include unrelated files or credentials.
|
|
19
|
+
|
|
20
|
+
Use self-contained HTML for interactive content. Inline styles and scripts and embed required assets as data URLs. External requests, CDN dependencies, relative executable assets, forms, browser storage, and multi-file websites are unsupported. Attachments are downloads, not importable website resources. Markdown and text display inertly.
|
|
21
|
+
|
|
22
|
+
Keep the whole revision within 1 MiB of decoded content, metadata, attachment paths, media types, and attachment bytes. At most 32 attachments are accepted. Base64 changes wire size, not the decoded limit. If required content exceeds a limit, explain the constraint without silently removing it.
|
|
23
|
+
|
|
24
|
+
Finish requested visual and interaction checks before saving when browser tools are available. A successful CLI write proves persistence, not visual quality or browser compatibility. Interactive HTML follows the server's Chrome policy; do not promise it in every browser.
|
|
25
|
+
|
|
26
|
+
## Create or revise
|
|
27
|
+
|
|
28
|
+
Create a draft when the user asks to save new content. Use a complete snapshot file and retain the CLI's request ID and operation record until the result is verified.
|
|
29
|
+
|
|
30
|
+
Resolve an existing artifact by its supplied ID, an observed publication URL, or a paginated list. If several results match, ask which one the user means. Read its current full snapshot before editing. Preserve metadata, custom JSON, content fields, and attachments the user did not ask to change.
|
|
31
|
+
|
|
32
|
+
Use `edit` for partial changes that preserve omitted fields. Use `update` for a complete next snapshot with the observed `headRevisionId` as its expected revision. Changing the attachment list replaces that inventory; an empty list removes all attachments. An update creates a retained revision and does not change the published revision.
|
|
33
|
+
|
|
34
|
+
## Publish deliberately
|
|
35
|
+
|
|
36
|
+
Publish when the user requests publication, a shareable artifact URL, or a change to the live artifact. Use the exact saved revision selected by the user or returned by the edit. Read the current publication version before preparing publication. Use zero only when the artifact is unpublished.
|
|
37
|
+
|
|
38
|
+
A published URL still requires its owner's Artifacts login. Publishing a historical revision leaves the draft head unchanged. Report the returned protected URL only after the publication succeeds and its current state is verified.
|
|
39
|
+
|
|
40
|
+
## Recover an uncertain write
|
|
41
|
+
|
|
42
|
+
The CLI retains complete mutations before sending them. After a connection failure or uncertain result, use `retry` with that retained request ID once. Keep the same resource, owner, request ID, response mode, snapshot, and expected versions. Do not rerun a convenience edit or generate a new ID to repeat an operation whose outcome is unknown.
|
|
43
|
+
|
|
44
|
+
If the retry remains unavailable, report the uncertainty and retain the recovery record. A conflict stops the failed operation. Read the current state and report the concurrent change. Do not replace expected versions to force a write. A changed operation needs a deliberate new command and request ID.
|
|
45
|
+
|
|
46
|
+
A `request_conflict` means the ID was used with different arguments. Do not describe it as success. Authentication, validation, missing-artifact, and service failures likewise remain failures.
|
|
47
|
+
|
|
48
|
+
## Verify and deliver
|
|
49
|
+
|
|
50
|
+
After create or update, read the explicit returned revision. Use `read ARTIFACT_ID --revision REVISION_ID --out FILE --quiet` to retain the full response without printing its snapshot. Check that the installed `--help` supports `--quiet` first. For older versions, use the command workflow's stdout redirection fallback. Compare the saved complete metadata, content, and decoded attachment bytes with the intended snapshot. For an update, check that its parent is the head used for preparation. Compact responses cannot verify omitted content.
|
|
51
|
+
|
|
52
|
+
After publication, read current publication and compare revision, version, and URL with the committed result. A replayed receipt describes the original commit; another writer may have changed the current publication afterward. Report that difference.
|
|
53
|
+
|
|
54
|
+
Follow list and history cursors when the request needs more than the first page. Export an explicit retained revision to the requested destination. The CLI checks the archive digest. Confirm that the file exists and inspect its manifest and relevant content before linking it.
|
|
55
|
+
|
|
56
|
+
Report the title, saved revision, and draft or published state in the user's language. Link the actual export or returned protected publication URL. Distinguish persistence from any visual or browser checks that did not run. Keep local credential and session identifiers private.
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
export ARTIFACTS_SERVER=https://artifacts.example.test/mcp
|
|
28
|
+
artifacts login
|
|
29
|
+
artifacts whoami
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Replace the example with the selected resource before executing it. Service commands read `ARTIFACTS_SERVER` or an explicit `--server RESOURCE`. Login does not save a default resource. `--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.
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
`logout` clears local credentials and attempts refresh-token revocation. 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.
|
|
37
|
+
|
|
38
|
+
## Create a complete snapshot
|
|
39
|
+
|
|
40
|
+
Save this JSON as `snapshot.json`, replacing its content with the requested artifact.
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"metadata": {
|
|
45
|
+
"title": "Project note",
|
|
46
|
+
"description": "A retained note",
|
|
47
|
+
"custom": { "source": "requested content" }
|
|
48
|
+
},
|
|
49
|
+
"content": {
|
|
50
|
+
"kind": "html",
|
|
51
|
+
"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>"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
{
|
|
55
|
+
"path": "notes.txt",
|
|
56
|
+
"mediaType": "text/plain",
|
|
57
|
+
"encoding": "utf8",
|
|
58
|
+
"data": "Retained attachment.\n"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`metadata.description` and `files` are required even when empty. Content kind is `html`, `markdown`, or `text`. 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.
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
artifacts create --snapshot snapshot.json
|
|
68
|
+
artifacts list --limit 25
|
|
69
|
+
artifacts read ARTIFACT_ID --out saved.json --quiet
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
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.
|
|
77
|
+
|
|
78
|
+
## Edit without losing retained data
|
|
79
|
+
|
|
80
|
+
For a title change, save `changes.json` with only the requested field.
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{ "metadata": { "title": "Reviewed project note" } }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
artifacts edit ARTIFACT_ID --changes changes.json
|
|
88
|
+
artifacts read ARTIFACT_ID --out current.json --quiet
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For a complete replacement, read the current head, preserve untouched data in `next-snapshot.json`, and use that observed revision.
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
artifacts update ARTIFACT_ID --snapshot next-snapshot.json --expected-revision REVISION_ID
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
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.
|
|
98
|
+
|
|
99
|
+
Command files contain the command itself, without the CLI's retained-operation wrapper.
|
|
100
|
+
|
|
101
|
+
| Command | Required JSON fields |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `create` | `requestId`, `snapshot` |
|
|
104
|
+
| `update` | `requestId`, `artifactId`, `expectedRevisionId`, `snapshot` |
|
|
105
|
+
| `publish` | `requestId`, `artifactId`, `revisionId`, `expectedPublicationVersion` |
|
|
106
|
+
|
|
107
|
+
`requestId` is a caller-chosen string of 8 to 128 characters. Create and update also accept optional `response` with value `full` or `compact`. This complete update example replaces a snapshot. Substitute the observed IDs and preserve all required content and attachments in its `snapshot`.
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"requestId": "reviewed-update-001",
|
|
112
|
+
"artifactId": "OBSERVED_ARTIFACT_ID",
|
|
113
|
+
"expectedRevisionId": "OBSERVED_HEAD_REVISION_ID",
|
|
114
|
+
"snapshot": {
|
|
115
|
+
"metadata": { "title": "Reviewed project note", "description": "A retained note" },
|
|
116
|
+
"content": { "kind": "text", "text": "Reviewed content." },
|
|
117
|
+
"files": []
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"requestId": "reviewed-publish-001",
|
|
127
|
+
"artifactId": "OBSERVED_ARTIFACT_ID",
|
|
128
|
+
"revisionId": "SELECTED_REVISION_ID",
|
|
129
|
+
"expectedPublicationVersion": 0
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Inspect history and publish
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
artifacts history ARTIFACT_ID --summary
|
|
137
|
+
artifacts read ARTIFACT_ID --revision REVISION_ID --out historical.json --quiet
|
|
138
|
+
artifacts publish ARTIFACT_ID --revision REVISION_ID --expected-version 0
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
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.
|
|
142
|
+
|
|
143
|
+
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.
|
|
144
|
+
|
|
145
|
+
## Retry and export
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
artifacts retry REQUEST_ID
|
|
149
|
+
artifacts export ARTIFACT_ID --revision REVISION_ID --out artifact.zip
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`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.
|
|
153
|
+
|
|
154
|
+
`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.
|
|
155
|
+
|
|
156
|
+
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.
|
package/src/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { canonicalResource } from '@yargote/artifacts-contracts/http';
|
|
|
11
11
|
import { createArtifactHttpClient, HttpError, prepareArtifactEdit } from '@yargote/artifacts-client';
|
|
12
12
|
import { accessToken, login, logout } from "./auth.js";
|
|
13
13
|
import { loadOperation, retainOperation, submitOperation } from "./operations.js";
|
|
14
|
+
import { runSkillsCommand } from "./skills.js";
|
|
14
15
|
const help = `Usage: artifacts COMMAND [OPTIONS]
|
|
15
16
|
|
|
16
17
|
Global options:
|
|
@@ -21,13 +22,18 @@ Global options:
|
|
|
21
22
|
--version Print the installed package version
|
|
22
23
|
|
|
23
24
|
Commands:
|
|
25
|
+
skills [list]
|
|
26
|
+
skills get NAME [--full]
|
|
27
|
+
skills get --all [--full]
|
|
28
|
+
skills path [NAME]
|
|
29
|
+
skills install [NAME] [--agent claude] [--global] [--force]
|
|
24
30
|
login [--no-browser]
|
|
25
31
|
logout
|
|
26
32
|
whoami
|
|
27
33
|
create --snapshot FILE [--request-id ID] [--response full|compact]
|
|
28
34
|
create --command FILE
|
|
29
35
|
list [--limit N] [--cursor CURSOR]
|
|
30
|
-
read ID [--revision REV] [--response full|compact] [--out FILE]
|
|
36
|
+
read ID [--revision REV] [--response full|compact] [--out FILE [--quiet]]
|
|
31
37
|
update ID --snapshot FILE --expected-revision REV [--request-id ID]
|
|
32
38
|
[--response full|compact]
|
|
33
39
|
update [ID] --command FILE
|
|
@@ -41,21 +47,29 @@ Commands:
|
|
|
41
47
|
Mutation command files contain the complete create, update, or publish JSON.
|
|
42
48
|
Mutation request IDs default to a random UUID. Commands are retained before
|
|
43
49
|
submission. Retry sends the exact retained command for the same server and owner.
|
|
44
|
-
Read --out saves the
|
|
50
|
+
Read --out saves the returned artifact JSON. Export --out saves the ZIP.
|
|
51
|
+
Read --quiet requires --out and suppresses successful JSON stdout.
|
|
45
52
|
Output files are created exclusively and never overwrite an existing path.
|
|
46
53
|
JSON results go to stdout. Diagnostics and recovery guidance go to stderr.
|
|
47
54
|
ARTIFACTS_ACCESS_TOKEN supplies a token for automation without local login.
|
|
55
|
+
Skills commands work offline without a server, login, or private state.
|
|
56
|
+
They print plain text. Global server and state options are accepted and ignored.
|
|
57
|
+
Skills install defaults to artifacts-cli under .agents/skills in the current
|
|
58
|
+
directory. --agent claude uses .claude/skills. --global uses your home directory.
|
|
59
|
+
The installed discovery file loads instructions through artifacts skills get.
|
|
60
|
+
Identical files stay unchanged. Differing files require --force.
|
|
48
61
|
`;
|
|
49
62
|
class UsageError extends Error {
|
|
50
63
|
}
|
|
51
64
|
const commandOptions = {
|
|
52
65
|
login: ['no-browser'], logout: [], whoami: [],
|
|
53
66
|
create: ['snapshot', 'request-id', 'response', 'command'],
|
|
54
|
-
list: ['limit', 'cursor'], read: ['revision', 'response', 'out'],
|
|
67
|
+
list: ['limit', 'cursor'], read: ['revision', 'response', 'out', 'quiet'],
|
|
55
68
|
update: ['snapshot', 'expected-revision', 'request-id', 'response', 'command'],
|
|
56
69
|
edit: ['changes', 'request-id'], history: ['limit', 'before-sequence', 'summary'],
|
|
57
70
|
publish: ['revision', 'expected-version', 'request-id', 'command'],
|
|
58
71
|
export: ['revision', 'out'], retry: [],
|
|
72
|
+
skills: ['full', 'all', 'agent', 'global', 'force'],
|
|
59
73
|
};
|
|
60
74
|
async function jsonFile(path) {
|
|
61
75
|
const file = await open(path, constants.O_RDONLY);
|
|
@@ -88,7 +102,9 @@ async function main() {
|
|
|
88
102
|
changes: { type: 'string' }, 'request-id': { type: 'string' }, response: { type: 'string' },
|
|
89
103
|
revision: { type: 'string' }, 'expected-revision': { type: 'string' }, 'expected-version': { type: 'string' },
|
|
90
104
|
limit: { type: 'string' }, cursor: { type: 'string' }, 'before-sequence': { type: 'string' },
|
|
91
|
-
summary: { type: 'boolean' }, out: { type: 'string' },
|
|
105
|
+
summary: { type: 'boolean' }, out: { type: 'string' }, quiet: { type: 'boolean' },
|
|
106
|
+
full: { type: 'boolean' }, all: { type: 'boolean' }, agent: { type: 'string' },
|
|
107
|
+
global: { type: 'boolean' }, force: { type: 'boolean' },
|
|
92
108
|
},
|
|
93
109
|
});
|
|
94
110
|
const seen = new Set();
|
|
@@ -114,6 +130,8 @@ async function main() {
|
|
|
114
130
|
for (const option of seen)
|
|
115
131
|
if (!['server', 'state-dir', 'help', ...commandOptions[command]].includes(option))
|
|
116
132
|
throw new UsageError(`Option --${option} is not supported by ${command}`);
|
|
133
|
+
if (command === 'skills')
|
|
134
|
+
return runSkillsCommand(arguments_, values, seen);
|
|
117
135
|
const resourceValue = values.server ?? process.env.ARTIFACTS_SERVER;
|
|
118
136
|
if (!resourceValue)
|
|
119
137
|
throw new UsageError('Specify --server RESOURCE or ARTIFACTS_SERVER. No production server is selected by default.');
|
|
@@ -145,6 +163,9 @@ async function main() {
|
|
|
145
163
|
const response = values.response;
|
|
146
164
|
if (response !== undefined && response !== 'full' && response !== 'compact')
|
|
147
165
|
throw new UsageError('--response requires full or compact');
|
|
166
|
+
const quiet = values.quiet ?? false;
|
|
167
|
+
if (quiet)
|
|
168
|
+
required('out');
|
|
148
169
|
let commandToken;
|
|
149
170
|
const client = createArtifactHttpClient({ resource, accessToken: () => commandToken ??= accessToken({ resource, stateDir }) });
|
|
150
171
|
async function mutate(mutation) {
|
|
@@ -191,7 +212,7 @@ async function main() {
|
|
|
191
212
|
const result = await client.read({ artifactId: argument(), ...(values.revision === undefined ? {} : { revisionId: values.revision }), ...(response === undefined ? {} : { response }) });
|
|
192
213
|
if (values.out)
|
|
193
214
|
await writeOutput(values.out, `${JSON.stringify(result, null, 2)}\n`);
|
|
194
|
-
return result;
|
|
215
|
+
return quiet ? undefined : result;
|
|
195
216
|
}
|
|
196
217
|
case 'update': {
|
|
197
218
|
if (values.command)
|
package/src/skills.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface SkillsOptions {
|
|
2
|
+
full?: boolean;
|
|
3
|
+
all?: boolean;
|
|
4
|
+
agent?: string;
|
|
5
|
+
global?: boolean;
|
|
6
|
+
force?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function runSkillsCommand(positionals: string[], options: SkillsOptions, seen: ReadonlySet<string>): Promise<void>;
|
|
9
|
+
export {};
|
package/src/skills.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, open, readFile, readdir, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
const catalogDirectory = resolve(fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? '../../../skills/' : '../skills/', import.meta.url)));
|
|
8
|
+
function parseCommand(positionals, options, seen) {
|
|
9
|
+
const [kind = 'list', ...arguments_] = positionals;
|
|
10
|
+
const allowed = { list: [], get: ['all', 'full'], path: [], install: ['agent', 'global', 'force'] };
|
|
11
|
+
if (!Object.hasOwn(allowed, kind))
|
|
12
|
+
throw new TypeError(`Unknown skills command ${kind}. Use artifacts --help for the reference.`);
|
|
13
|
+
for (const option of seen) {
|
|
14
|
+
if (!['server', 'state-dir', ...allowed[kind]].includes(option))
|
|
15
|
+
throw new TypeError(`Option --${option} is not supported by skills ${kind}`);
|
|
16
|
+
}
|
|
17
|
+
switch (kind) {
|
|
18
|
+
case 'list':
|
|
19
|
+
if (arguments_.length)
|
|
20
|
+
throw new TypeError('skills list does not accept positional arguments');
|
|
21
|
+
return { kind };
|
|
22
|
+
case 'get':
|
|
23
|
+
if (arguments_.length > 1 || (options.all ? arguments_.length !== 0 : arguments_.length !== 1))
|
|
24
|
+
throw new TypeError('skills get requires either one skill name or --all');
|
|
25
|
+
return { kind, selection: options.all ? { all: true } : { name: arguments_[0] }, full: options.full ?? false };
|
|
26
|
+
case 'path':
|
|
27
|
+
if (arguments_.length > 1)
|
|
28
|
+
throw new TypeError('skills path accepts zero or one skill name');
|
|
29
|
+
return { kind, name: arguments_[0] };
|
|
30
|
+
case 'install':
|
|
31
|
+
if (arguments_.length > 1)
|
|
32
|
+
throw new TypeError('skills install accepts zero or one skill name');
|
|
33
|
+
if (options.agent !== undefined && options.agent !== 'claude')
|
|
34
|
+
throw new TypeError('--agent requires claude. Omit --agent to install under .agents.');
|
|
35
|
+
return { kind, name: arguments_[0] ?? 'artifacts-cli', agent: options.agent === 'claude' ? 'claude' : 'agents', global: options.global ?? false, force: options.force ?? false };
|
|
36
|
+
default:
|
|
37
|
+
throw new TypeError(`Unknown skills command ${kind}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function loadCatalog() {
|
|
41
|
+
const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
42
|
+
const names = manifest.artifactsSkills;
|
|
43
|
+
if (!Array.isArray(names) || names.some((name) => typeof name !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) || new Set(names).size !== names.length)
|
|
44
|
+
throw new Error('Invalid bundled skills declaration');
|
|
45
|
+
return Promise.all(names.toSorted().map(async (name) => {
|
|
46
|
+
const directory = join(catalogDirectory, name);
|
|
47
|
+
const skillText = await readFile(join(directory, 'SKILL.md'));
|
|
48
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(skillText.toString('utf8'))?.[1];
|
|
49
|
+
const declaredName = frontmatter && /^name: (.+)$/m.exec(frontmatter)?.[1];
|
|
50
|
+
const description = frontmatter && /^description: (.+)$/m.exec(frontmatter)?.[1];
|
|
51
|
+
if (declaredName !== name || !description)
|
|
52
|
+
throw new Error(`Invalid bundled skill frontmatter at ${join(directory, 'SKILL.md')}`);
|
|
53
|
+
return { name, description, directory, skillText };
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
async function fullFiles(directory, prefix = '') {
|
|
57
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
58
|
+
const files = [];
|
|
59
|
+
for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name, 'en'))) {
|
|
60
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
61
|
+
if (entry.isDirectory())
|
|
62
|
+
files.push(...await fullFiles(join(directory, entry.name), relative));
|
|
63
|
+
else if (entry.isFile())
|
|
64
|
+
files.push(relative);
|
|
65
|
+
else
|
|
66
|
+
throw new Error(`Bundled skill contains a non-regular file at ${join(directory, entry.name)}`);
|
|
67
|
+
}
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
70
|
+
async function labeledContent(skills, full) {
|
|
71
|
+
const chunks = [];
|
|
72
|
+
for (const skill of skills) {
|
|
73
|
+
const files = full ? ['SKILL.md', ...(await fullFiles(skill.directory)).filter((path) => path !== 'SKILL.md')] : ['SKILL.md'];
|
|
74
|
+
for (const path of files) {
|
|
75
|
+
const content = path === 'SKILL.md' ? skill.skillText : await readFile(join(skill.directory, path));
|
|
76
|
+
chunks.push(Buffer.from(`=== ${skill.name}/${path} ===\n`), content);
|
|
77
|
+
if (content.at(-1) !== 10)
|
|
78
|
+
chunks.push(Buffer.from('\n'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return Buffer.concat(chunks);
|
|
82
|
+
}
|
|
83
|
+
function discoveryText(skill) {
|
|
84
|
+
return `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\nBefore using this skill, run \`artifacts skills get ${skill.name} --full\`.\nLoad and follow the complete instructions, references, and templates bundled with the installed CLI version.\nRelative paths refer to the bundled directory reported by \`artifacts skills path ${skill.name}\`.\n`;
|
|
85
|
+
}
|
|
86
|
+
async function installSkill(skill, command) {
|
|
87
|
+
const destination = resolve(command.global ? homedir() : process.cwd(), command.agent === 'claude' ? '.claude' : '.agents', 'skills', skill.name, 'SKILL.md');
|
|
88
|
+
const content = Buffer.from(discoveryText(skill));
|
|
89
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
90
|
+
try {
|
|
91
|
+
const file = await open(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o644);
|
|
92
|
+
try {
|
|
93
|
+
await file.writeFile(content);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
await file.close();
|
|
97
|
+
}
|
|
98
|
+
return destination;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST'))
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
const existing = await lstat(destination);
|
|
105
|
+
if (!existing.isFile())
|
|
106
|
+
throw new TypeError(`Cannot install ${destination}. The destination must be a regular file, not a directory or symbolic link.`);
|
|
107
|
+
const file = await open(destination, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
108
|
+
let previous;
|
|
109
|
+
try {
|
|
110
|
+
if (!(await file.stat()).isFile())
|
|
111
|
+
throw new TypeError(`Cannot install ${destination}. The destination must be a regular file.`);
|
|
112
|
+
previous = await file.readFile();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await file.close();
|
|
116
|
+
}
|
|
117
|
+
if (previous.equals(content))
|
|
118
|
+
return destination;
|
|
119
|
+
if (!command.force)
|
|
120
|
+
throw new TypeError(`Cannot replace differing file ${destination}. Use --force to replace it.`);
|
|
121
|
+
const temporary = join(dirname(destination), `.SKILL.md.${randomUUID()}.tmp`);
|
|
122
|
+
try {
|
|
123
|
+
const replacement = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o644);
|
|
124
|
+
try {
|
|
125
|
+
await replacement.writeFile(content);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
await replacement.close();
|
|
129
|
+
}
|
|
130
|
+
const current = await lstat(destination);
|
|
131
|
+
if (!current.isFile())
|
|
132
|
+
throw new TypeError(`Cannot install ${destination}. The destination must be a regular file, not a directory or symbolic link.`);
|
|
133
|
+
if (current.ino !== existing.ino || current.dev !== existing.dev || current.mtimeMs !== existing.mtimeMs || current.ctimeMs !== existing.ctimeMs)
|
|
134
|
+
throw new TypeError(`Cannot replace ${destination}. The destination changed during installation. Run the command again.`);
|
|
135
|
+
await rename(temporary, destination);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
await rm(temporary, { force: true });
|
|
139
|
+
}
|
|
140
|
+
return destination;
|
|
141
|
+
}
|
|
142
|
+
export async function runSkillsCommand(positionals, options, seen) {
|
|
143
|
+
const command = parseCommand(positionals, options, seen);
|
|
144
|
+
const skills = await loadCatalog();
|
|
145
|
+
function named(name) {
|
|
146
|
+
const skill = skills.find((entry) => entry.name === name);
|
|
147
|
+
if (!skill)
|
|
148
|
+
throw new TypeError(`Unknown bundled skill ${name}. Use artifacts skills list to inspect available skills.`);
|
|
149
|
+
return skill;
|
|
150
|
+
}
|
|
151
|
+
switch (command.kind) {
|
|
152
|
+
case 'list':
|
|
153
|
+
process.stdout.write(skills.map((skill) => `${skill.name}\t${skill.description}\n`).join(''));
|
|
154
|
+
return;
|
|
155
|
+
case 'get': {
|
|
156
|
+
const selection = 'all' in command.selection ? skills : [named(command.selection.name)];
|
|
157
|
+
process.stdout.write(!command.full && 'name' in command.selection ? selection[0].skillText : await labeledContent(selection, command.full));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
case 'path':
|
|
161
|
+
process.stdout.write(`${command.name === undefined ? catalogDirectory : named(command.name).directory}\n`);
|
|
162
|
+
return;
|
|
163
|
+
case 'install':
|
|
164
|
+
process.stdout.write(`${await installSkill(named(command.name), command)}\n`);
|
|
165
|
+
}
|
|
166
|
+
}
|