@yargote/artifacts-cli 0.1.0-beta.1 → 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 (32) hide show
  1. package/README.md +150 -19
  2. package/package.json +9 -4
  3. package/skills/artifacts-cli/SKILL.md +78 -0
  4. package/skills/artifacts-cli/references/commands.md +240 -0
  5. package/skills/artifacts-cli/references/verify-retained.md +119 -0
  6. package/skills/artifacts-cli/scripts/prepare-retained.mjs +149 -0
  7. package/skills/artifacts-design/SKILL.md +92 -0
  8. package/skills/artifacts-design/THIRD-PARTY-NOTICES.md +155 -0
  9. package/skills/artifacts-design/fonts/OFL.txt +96 -0
  10. package/skills/artifacts-design/fonts/bricolage-grotesque.woff2 +0 -0
  11. package/skills/artifacts-design/fonts/jetbrains-mono.woff2 +0 -0
  12. package/skills/artifacts-design/fonts/newsreader-italic.woff2 +0 -0
  13. package/skills/artifacts-design/fonts/newsreader.woff2 +0 -0
  14. package/skills/artifacts-design/fonts/public-sans-italic.woff2 +0 -0
  15. package/skills/artifacts-design/fonts/public-sans.woff2 +0 -0
  16. package/skills/artifacts-design/references/canvas.md +100 -0
  17. package/skills/artifacts-design/references/catppuccin.md +38 -0
  18. package/skills/artifacts-design/references/interface-checks.md +60 -0
  19. package/skills/artifacts-design/references/local-checks.md +70 -0
  20. package/skills/artifacts-design/references/visual-explanations.md +13 -0
  21. package/skills/artifacts-design/scripts/embed-fonts.mjs +246 -0
  22. package/skills/artifacts-design/scripts/verify-local.mjs +333 -0
  23. package/skills/artifacts-design/templates/starter.html +170 -0
  24. package/src/auth.d.ts +4 -1
  25. package/src/auth.js +184 -45
  26. package/src/credentials.d.ts +2 -0
  27. package/src/credentials.js +22 -0
  28. package/src/index.js +121 -20
  29. package/src/operations.d.ts +68 -38
  30. package/src/operations.js +9 -1
  31. package/src/skills.d.ts +9 -0
  32. package/src/skills.js +181 -0
package/src/index.js CHANGED
@@ -6,28 +6,41 @@ import { homedir } from 'node:os';
6
6
  import { join, resolve } from 'node:path';
7
7
  import { parseArgs } from 'node:util';
8
8
  import { z } from 'zod';
9
- import { createSchema, publishSchema, updateSchema } from '@yargote/artifacts-contracts';
9
+ import { createSchema, listSharesSchema, moveSchema, publishSchema, shareSchema, unshareSchema, updateSchema } from '@yargote/artifacts-contracts';
10
10
  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
+ import { CredentialStore } from "./credentials.js";
13
14
  import { loadOperation, retainOperation, submitOperation } from "./operations.js";
15
+ import { runSkillsCommand } from "./skills.js";
14
16
  const help = `Usage: artifacts COMMAND [OPTIONS]
15
17
 
16
18
  Global options:
17
- --server RESOURCE Canonical OAuth resource, or ARTIFACTS_SERVER
19
+ --server RESOURCE Exact REST OAuth resource, or ARTIFACTS_SERVER
20
+ Defaults to the last successful login
18
21
  --state-dir DIR Private local state, or ARTIFACTS_STATE_DIR
19
22
  Default: ~/.local/state/artifacts
20
23
  --help Print this reference
21
24
  --version Print the installed package version
22
25
 
23
26
  Commands:
27
+ skills [list]
28
+ skills get NAME [--full]
29
+ skills get --all [--full]
30
+ skills path [NAME]
31
+ skills install [NAME] [--agent claude] [--global] [--force]
24
32
  login [--no-browser]
25
33
  logout
26
34
  whoami
27
- create --snapshot FILE [--request-id ID] [--response full|compact]
35
+ create --snapshot FILE [--project NAME] [--slug SLUG] [--request-id ID]
36
+ [--response full|compact]
28
37
  create --command FILE
29
- list [--limit N] [--cursor CURSOR]
30
- read ID [--revision REV] [--response full|compact] [--out FILE]
38
+ list [--limit N] [--cursor CURSOR] [--project NAME]
39
+ projects
40
+ search QUERY [--project NAME] [--limit N] [--cursor CURSOR]
41
+ read ID [--revision REV] [--response full|compact] [--out FILE [--quiet]]
42
+ read --slug SLUG [--project NAME] [--revision REV] [--response full|compact]
43
+ [--out FILE [--quiet]]
31
44
  update ID --snapshot FILE --expected-revision REV [--request-id ID]
32
45
  [--response full|compact]
33
46
  update [ID] --command FILE
@@ -36,26 +49,63 @@ Commands:
36
49
  publish ID --revision REV --expected-version N [--request-id ID]
37
50
  publish [ID] --command FILE
38
51
  export ID --revision REV --out FILE
52
+ move ID [--project NAME | --no-project] [--slug SLUG | --no-slug]
53
+ [--request-id ID]
54
+ share ID --email EMAIL [--notify] [--request-id ID]
55
+ shares ID
56
+ unshare ID SHARE_ID [--request-id ID]
39
57
  retry REQUEST_ID
40
58
 
41
59
  Mutation command files contain the complete create, update, or publish JSON.
42
60
  Mutation request IDs default to a random UUID. Commands are retained before
43
61
  submission. Retry sends the exact retained command for the same server and owner.
44
- Read --out saves the complete artifact JSON. Export --out saves the ZIP.
62
+ Projects group artifacts by name without regard to letter case. A project
63
+ exists while it contains an artifact. Move changes the project or the slug of
64
+ an artifact without creating a revision. It needs at least one of its four
65
+ options. --no-project removes the artifact from its project, and --no-slug
66
+ clears its slug. An option left out keeps that value, so a move to another
67
+ project keeps the slug.
68
+ A slug such as concepts/rag names an artifact within its project, or within
69
+ the unfiled artifacts without --project. Read --slug finds it without its ID.
70
+ Create or move fails with slug_conflict when the project already uses the slug.
71
+ Search finds owned Markdown and text artifacts whose title, description, or
72
+ content contains every term of QUERY, ignoring letter case and accents. Quote a
73
+ QUERY with spaces. Results are ordered by relevance with a **bold** snippet.
74
+ Read --out saves the returned artifact JSON. Export --out saves the ZIP.
75
+ Read --quiet requires --out and suppresses successful JSON stdout.
45
76
  Output files are created exclusively and never overwrite an existing path.
46
77
  JSON results go to stdout. Diagnostics and recovery guidance go to stderr.
78
+ Login accepts an MCP URL when its metadata advertises artifacts_rest_resource.
79
+ Split MCP and REST resources require this relation. Login remembers the verified
80
+ REST URL for later commands. --server takes precedence over ARTIFACTS_SERVER,
81
+ which takes precedence over the saved URL. Ordinary overrides do not replace
82
+ the saved URL. Logout keeps it for a later login.
83
+ Login checks <resource>/v1/identity before browser
84
+ authorization. Legacy /mcp
85
+ resources work when their REST API is at /mcp/v1. If the check fails, confirm
86
+ the configured REST URL with your service administrator.
87
+ Login errors report the phase, method, endpoint, and HTTP status when received.
88
+ Network failures have no HTTP status. Login verifies identity again after OAuth.
47
89
  ARTIFACTS_ACCESS_TOKEN supplies a token for automation without local login.
90
+ Skills commands work offline without a server, login, or private state.
91
+ They print plain text. Global server and state options are accepted and ignored.
92
+ Skills install defaults to artifacts-cli under .agents/skills in the current
93
+ directory. --agent claude uses .claude/skills. --global uses your home directory.
94
+ The installed discovery file loads instructions through artifacts skills get.
95
+ Identical files stay unchanged. Differing files require --force.
48
96
  `;
49
97
  class UsageError extends Error {
50
98
  }
51
99
  const commandOptions = {
52
100
  login: ['no-browser'], logout: [], whoami: [],
53
- create: ['snapshot', 'request-id', 'response', 'command'],
54
- list: ['limit', 'cursor'], read: ['revision', 'response', 'out'],
101
+ create: ['snapshot', 'project', 'slug', 'request-id', 'response', 'command'],
102
+ list: ['limit', 'cursor', 'project'], projects: [], search: ['project', 'limit', 'cursor'], read: ['slug', 'project', 'revision', 'response', 'out', 'quiet'],
55
103
  update: ['snapshot', 'expected-revision', 'request-id', 'response', 'command'],
56
104
  edit: ['changes', 'request-id'], history: ['limit', 'before-sequence', 'summary'],
57
105
  publish: ['revision', 'expected-version', 'request-id', 'command'],
58
- export: ['revision', 'out'], retry: [],
106
+ share: ['email', 'notify', 'request-id'], shares: [], unshare: ['request-id'],
107
+ export: ['revision', 'out'], move: ['project', 'no-project', 'slug', 'no-slug', 'request-id'], retry: [],
108
+ skills: ['full', 'all', 'agent', 'global', 'force'],
59
109
  };
60
110
  async function jsonFile(path) {
61
111
  const file = await open(path, constants.O_RDONLY);
@@ -88,7 +138,10 @@ async function main() {
88
138
  changes: { type: 'string' }, 'request-id': { type: 'string' }, response: { type: 'string' },
89
139
  revision: { type: 'string' }, 'expected-revision': { type: 'string' }, 'expected-version': { type: 'string' },
90
140
  limit: { type: 'string' }, cursor: { type: 'string' }, 'before-sequence': { type: 'string' },
91
- summary: { type: 'boolean' }, out: { type: 'string' },
141
+ summary: { type: 'boolean' }, out: { type: 'string' }, quiet: { type: 'boolean' },
142
+ full: { type: 'boolean' }, all: { type: 'boolean' }, agent: { type: 'string' },
143
+ global: { type: 'boolean' }, force: { type: 'boolean' },
144
+ project: { type: 'string' }, 'no-project': { type: 'boolean' }, slug: { type: 'string' }, 'no-slug': { type: 'boolean' }, email: { type: 'string' }, notify: { type: 'boolean' },
92
145
  },
93
146
  });
94
147
  const seen = new Set();
@@ -114,11 +167,22 @@ async function main() {
114
167
  for (const option of seen)
115
168
  if (!['server', 'state-dir', 'help', ...commandOptions[command]].includes(option))
116
169
  throw new UsageError(`Option --${option} is not supported by ${command}`);
117
- const resourceValue = values.server ?? process.env.ARTIFACTS_SERVER;
118
- if (!resourceValue)
119
- throw new UsageError('Specify --server RESOURCE or ARTIFACTS_SERVER. No production server is selected by default.');
120
- const resource = canonicalResource(resourceValue);
170
+ if (command === 'skills')
171
+ return runSkillsCommand(arguments_, values, seen);
121
172
  const stateDir = resolve(values['state-dir'] ?? process.env.ARTIFACTS_STATE_DIR ?? join(homedir(), '.local', 'state', 'artifacts'));
173
+ let resourceValue = values.server ?? process.env.ARTIFACTS_SERVER;
174
+ if (resourceValue === undefined) {
175
+ const store = await CredentialStore.open(stateDir);
176
+ try {
177
+ resourceValue = await store.exclusive(() => store.readSelectedResource());
178
+ }
179
+ finally {
180
+ store.close();
181
+ }
182
+ }
183
+ if (resourceValue === undefined)
184
+ throw new UsageError('No server is saved. Run artifacts login --server RESOURCE or set ARTIFACTS_SERVER.');
185
+ const resource = canonicalResource(resourceValue);
122
186
  function argument(required = true) {
123
187
  if (arguments_.length > 1 || (required && arguments_.length !== 1))
124
188
  throw new UsageError(`${command} requires ${required ? 'one' : 'zero or one'} positional argument`);
@@ -142,9 +206,17 @@ async function main() {
142
206
  throw new UsageError(`--${name} requires a nonnegative safe integer`);
143
207
  return Number(value);
144
208
  }
209
+ function cleared(name, value, clear) {
210
+ if (value !== undefined && clear)
211
+ throw new UsageError(`Specify at most one of --${name} or --no-${name}`);
212
+ return clear ? null : value;
213
+ }
145
214
  const response = values.response;
146
215
  if (response !== undefined && response !== 'full' && response !== 'compact')
147
216
  throw new UsageError('--response requires full or compact');
217
+ const quiet = values.quiet ?? false;
218
+ if (quiet)
219
+ required('out');
148
220
  let commandToken;
149
221
  const client = createArtifactHttpClient({ resource, accessToken: () => commandToken ??= accessToken({ resource, stateDir }) });
150
222
  async function mutate(mutation) {
@@ -171,7 +243,12 @@ async function main() {
171
243
  switch (command) {
172
244
  case 'login':
173
245
  none();
174
- return login({ resource, stateDir, noBrowser: values['no-browser'] ?? false, onAuthorizationUrl: (url) => { process.stderr.write(`Authorize this login in your browser:\n${url}\n`); } });
246
+ {
247
+ const result = await login({ resource, stateDir, noBrowser: values['no-browser'] ?? false, onAuthorizationUrl: (url) => { process.stderr.write(`Authorize this login in your browser:\n${url}\n`); } });
248
+ if (result.resource !== resource)
249
+ process.stderr.write(`REST resource: ${result.resource}\nThis verified REST URL is saved for subsequent commands.\n`);
250
+ return result.identity;
251
+ }
175
252
  case 'logout':
176
253
  none();
177
254
  return logout({ resource, stateDir });
@@ -182,16 +259,25 @@ async function main() {
182
259
  if (values.command)
183
260
  return mutate(await commandFile('create'));
184
261
  none();
185
- return mutate({ kind: 'create', command: createSchema.parse({ requestId: values['request-id'] ?? randomUUID(), snapshot: await jsonFile(required('snapshot')), ...(response === undefined ? {} : { response }) }) });
262
+ return mutate({ kind: 'create', command: createSchema.parse({ requestId: values['request-id'] ?? randomUUID(), snapshot: await jsonFile(required('snapshot')), ...(values.project === undefined ? {} : { project: values.project }), ...(values.slug === undefined ? {} : { slug: values.slug }), ...(response === undefined ? {} : { response }) }) });
186
263
  }
187
264
  case 'list':
188
265
  none();
189
- return client.list({ ...(values.limit === undefined ? {} : { limit: integer('limit') }), ...(values.cursor === undefined ? {} : { cursor: values.cursor }) });
266
+ return client.list({ ...(values.limit === undefined ? {} : { limit: integer('limit') }), ...(values.cursor === undefined ? {} : { cursor: values.cursor }), ...(values.project === undefined ? {} : { project: values.project }) });
267
+ case 'projects':
268
+ none();
269
+ return client.projects();
270
+ case 'search': return client.search({ query: argument(), ...(values.project === undefined ? {} : { project: values.project }), ...(values.limit === undefined ? {} : { limit: integer('limit') }), ...(values.cursor === undefined ? {} : { cursor: values.cursor }) });
190
271
  case 'read': {
191
- const result = await client.read({ artifactId: argument(), ...(values.revision === undefined ? {} : { revisionId: values.revision }), ...(response === undefined ? {} : { response }) });
272
+ if (values.slug === undefined && values.project !== undefined)
273
+ throw new UsageError('read --project requires --slug');
274
+ if ((values.slug === undefined) !== (arguments_.length === 1) || arguments_.length > 1)
275
+ throw new UsageError('read requires exactly one of an artifact ID or --slug SLUG');
276
+ const target = values.slug === undefined ? { artifactId: arguments_[0] } : { slug: values.slug, ...(values.project === undefined ? {} : { project: values.project }) };
277
+ const result = await client.read({ ...target, ...(values.revision === undefined ? {} : { revisionId: values.revision }), ...(response === undefined ? {} : { response }) });
192
278
  if (values.out)
193
279
  await writeOutput(values.out, `${JSON.stringify(result, null, 2)}\n`);
194
- return result;
280
+ return quiet ? undefined : result;
195
281
  }
196
282
  case 'update': {
197
283
  if (values.command)
@@ -217,11 +303,26 @@ async function main() {
217
303
  await writeOutput(output, archive.bytes);
218
304
  return { filename: archive.filename, path: resolve(output), mediaType: archive.mediaType, sha256: archive.sha256, bytes: archive.bytes.byteLength };
219
305
  }
306
+ case 'move': {
307
+ const artifactId = argument();
308
+ const project = cleared('project', values.project, values['no-project']);
309
+ const slug = cleared('slug', values.slug, values['no-slug']);
310
+ if (project === undefined && slug === undefined)
311
+ throw new UsageError('move requires --project NAME, --no-project, --slug SLUG, or --no-slug');
312
+ return mutate({ kind: 'move', command: moveSchema.parse({ artifactId, requestId: values['request-id'] ?? randomUUID(), ...(project === undefined ? {} : { project }), ...(slug === undefined ? {} : { slug }) }) });
313
+ }
314
+ case 'share': return mutate({ kind: 'share', command: shareSchema.parse({ artifactId: argument(), requestId: values['request-id'] ?? randomUUID(), email: required('email'), ...(values.notify === undefined ? {} : { notify: values.notify }) }) });
315
+ case 'shares': return client.listShares(listSharesSchema.parse({ artifactId: argument() }));
316
+ case 'unshare': {
317
+ if (arguments_.length !== 2)
318
+ throw new UsageError('unshare requires two positional arguments');
319
+ return mutate({ kind: 'unshare', command: unshareSchema.parse({ artifactId: arguments_[0], shareId: arguments_[1], requestId: values['request-id'] ?? randomUUID() }) });
320
+ }
220
321
  case 'retry': {
221
322
  const requestId = argument();
222
323
  const owner = await client.identity();
223
324
  const operation = await loadOperation(stateDir, requestId, resource, owner.userId);
224
- process.stderr.write(`Replaying retained ${operation.kind} operation ${requestId}. The result is its retained receipt and may differ from current state.\n`);
325
+ process.stderr.write(`Replaying retained ${operation.kind} operation ${requestId}. Replay confirms the original operation and does not establish current state.\n`);
225
326
  return submitOperation(client, operation);
226
327
  }
227
328
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { createSchema, publishSchema, updateSchema } from '@yargote/artifacts-contracts';
2
+ import { createSchema, moveSchema, publishSchema, shareSchema, unshareSchema, updateSchema } from '@yargote/artifacts-contracts';
3
3
  import type { ArtifactHttpClient } from '@yargote/artifacts-client';
4
4
  export declare const operationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
5
5
  kind: z.ZodLiteral<"create">;
@@ -16,6 +16,7 @@ export declare const operationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
16
16
  text: "text";
17
17
  html: "html";
18
18
  markdown: "markdown";
19
+ design: "design";
19
20
  }>;
20
21
  text: z.ZodString;
21
22
  }, z.core.$strict>;
@@ -29,6 +30,8 @@ export declare const operationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
29
30
  data: z.ZodString;
30
31
  }, z.core.$strict>>;
31
32
  }, z.core.$strict>;
33
+ project: z.ZodOptional<z.ZodString>;
34
+ slug: z.ZodOptional<z.ZodString>;
32
35
  response: z.ZodOptional<z.ZodEnum<{
33
36
  full: "full";
34
37
  compact: "compact";
@@ -54,6 +57,7 @@ export declare const operationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
54
57
  text: "text";
55
58
  html: "html";
56
59
  markdown: "markdown";
60
+ design: "design";
57
61
  }>;
58
62
  text: z.ZodString;
59
63
  }, z.core.$strict>;
@@ -86,6 +90,39 @@ export declare const operationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
86
90
  version: z.ZodLiteral<1>;
87
91
  resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
88
92
  ownerId: z.ZodString;
93
+ }, z.core.$strict>, z.ZodObject<{
94
+ kind: z.ZodLiteral<"move">;
95
+ command: z.ZodObject<{
96
+ requestId: z.ZodString;
97
+ artifactId: z.ZodString;
98
+ project: z.ZodOptional<z.ZodNullable<z.ZodString>>;
99
+ slug: z.ZodOptional<z.ZodNullable<z.ZodString>>;
100
+ }, z.core.$strict>;
101
+ version: z.ZodLiteral<1>;
102
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
103
+ ownerId: z.ZodString;
104
+ }, z.core.$strict>, z.ZodObject<{
105
+ kind: z.ZodLiteral<"share">;
106
+ command: z.ZodObject<{
107
+ requestId: z.ZodString;
108
+ artifactId: z.ZodString;
109
+ email: z.ZodPipe<z.ZodString, z.ZodEmail>;
110
+ notify: z.ZodOptional<z.ZodBoolean>;
111
+ requireNew: z.ZodOptional<z.ZodLiteral<true>>;
112
+ }, z.core.$strict>;
113
+ version: z.ZodLiteral<1>;
114
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
115
+ ownerId: z.ZodString;
116
+ }, z.core.$strict>, z.ZodObject<{
117
+ kind: z.ZodLiteral<"unshare">;
118
+ command: z.ZodObject<{
119
+ requestId: z.ZodString;
120
+ artifactId: z.ZodString;
121
+ shareId: z.ZodString;
122
+ }, z.core.$strict>;
123
+ version: z.ZodLiteral<1>;
124
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
125
+ ownerId: z.ZodString;
89
126
  }, z.core.$strict>], "kind">;
90
127
  export type PreparedOperation = z.infer<typeof operationSchema>;
91
128
  export type Mutation = {
@@ -97,48 +134,41 @@ export type Mutation = {
97
134
  } | {
98
135
  kind: 'publish';
99
136
  command: z.input<typeof publishSchema>;
137
+ } | {
138
+ kind: 'move';
139
+ command: z.input<typeof moveSchema>;
140
+ } | {
141
+ kind: 'share';
142
+ command: z.input<typeof shareSchema>;
143
+ } | {
144
+ kind: 'unshare';
145
+ command: z.input<typeof unshareSchema>;
100
146
  };
101
147
  export declare function operationPath(stateDir: string, resource: string, requestId: string): string;
102
148
  export declare function retainOperation(stateDir: string, resource: string, ownerId: string, mutation: Mutation): Promise<PreparedOperation>;
103
149
  export declare function loadOperation(stateDir: string, requestId: string, resource: string, ownerId: string): Promise<PreparedOperation>;
104
- export declare function submitOperation(client: ArtifactHttpClient, operation: PreparedOperation): Promise<{
150
+ export declare function submitOperation(client: ArtifactHttpClient, operation: PreparedOperation): Promise<void> | Promise<{
151
+ shareId: string;
152
+ artifactId: string;
153
+ kind: "email";
154
+ value: string;
155
+ createdAt: string;
156
+ notification?: {
157
+ status: "requested";
158
+ } | {
159
+ status: "skipped";
160
+ reason: "disabled";
161
+ } | {
162
+ status: "skipped";
163
+ reason: "rate_limited";
164
+ retryAt: string;
165
+ } | undefined;
166
+ }> | Promise<{
167
+ artifactId: string;
168
+ project: string | null;
169
+ }> | Promise<{
105
170
  revisionId: string;
106
171
  version: number;
107
172
  url: string;
108
173
  artifactId: string;
109
- }> | Promise<{
110
- artifactId: string;
111
- ownerId: string;
112
- headRevisionId: string;
113
- publication: {
114
- revisionId: string;
115
- version: number;
116
- url: string;
117
- } | null;
118
- revision: {
119
- id: string;
120
- parentRevisionId: string | null;
121
- sequence: number;
122
- authorUserId: string;
123
- authorClientId: string;
124
- createdAt: string;
125
- sha256: string;
126
- snapshot: {
127
- metadata: {
128
- title: string;
129
- description: string;
130
- custom?: z.core.util.JSONType | undefined;
131
- };
132
- content: {
133
- kind: "text" | "html" | "markdown";
134
- text: string;
135
- };
136
- files: {
137
- path: string;
138
- mediaType: string;
139
- encoding: "utf8" | "base64";
140
- data: string;
141
- }[];
142
- };
143
- };
144
- } | import("@yargote/artifacts-client").CompactArtifactRead>;
174
+ }>;
package/src/operations.js CHANGED
@@ -3,7 +3,7 @@ import { lstat, mkdir, open } from 'node:fs/promises';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { join } from 'node:path';
5
5
  import { z } from 'zod';
6
- import { createSchema, publishSchema, updateSchema } from '@yargote/artifacts-contracts';
6
+ import { createSchema, moveSchema, publishSchema, shareSchema, unshareSchema, updateSchema } from '@yargote/artifacts-contracts';
7
7
  import { canonicalResource } from '@yargote/artifacts-contracts/http';
8
8
  const binding = {
9
9
  version: z.literal(1),
@@ -14,6 +14,9 @@ export const operationSchema = z.discriminatedUnion('kind', [
14
14
  z.strictObject({ ...binding, kind: z.literal('create'), command: createSchema }),
15
15
  z.strictObject({ ...binding, kind: z.literal('update'), command: updateSchema }),
16
16
  z.strictObject({ ...binding, kind: z.literal('publish'), command: publishSchema }),
17
+ z.strictObject({ ...binding, kind: z.literal('move'), command: moveSchema }),
18
+ z.strictObject({ ...binding, kind: z.literal('share'), command: shareSchema }),
19
+ z.strictObject({ ...binding, kind: z.literal('unshare'), command: unshareSchema }),
17
20
  ]);
18
21
  export function operationPath(stateDir, resource, requestId) {
19
22
  createSchema.shape.requestId.parse(requestId);
@@ -86,5 +89,10 @@ export function submitOperation(client, operation) {
86
89
  case 'create': return client.create(operation.command);
87
90
  case 'update': return client.update(operation.command);
88
91
  case 'publish': return client.publish(operation.command);
92
+ case 'move': return client.move(operation.command);
93
+ case 'share': return client.share(operation.command);
94
+ case 'unshare': return client.unshare(operation.command);
89
95
  }
96
+ const exhaustive = operation;
97
+ throw new Error(`Unsupported retained operation: ${exhaustive}`);
90
98
  }
@@ -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,181 @@
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
+ function isText(content) {
71
+ if (content.includes(0))
72
+ return false;
73
+ try {
74
+ new TextDecoder('utf-8', { fatal: true }).decode(content);
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ async function labeledContent(skills, full) {
82
+ const chunks = [];
83
+ for (const skill of skills) {
84
+ const files = full ? ['SKILL.md', ...(await fullFiles(skill.directory)).filter((path) => path !== 'SKILL.md')] : ['SKILL.md'];
85
+ for (const path of files) {
86
+ const content = path === 'SKILL.md' ? skill.skillText : await readFile(join(skill.directory, path));
87
+ if (!isText(content)) {
88
+ chunks.push(Buffer.from(`=== ${skill.name}/${path} (binary, ${content.length} bytes) ===\n`));
89
+ continue;
90
+ }
91
+ chunks.push(Buffer.from(`=== ${skill.name}/${path} ===\n`), content);
92
+ if (content.at(-1) !== 10)
93
+ chunks.push(Buffer.from('\n'));
94
+ }
95
+ }
96
+ return Buffer.concat(chunks);
97
+ }
98
+ function discoveryText(skill) {
99
+ 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`;
100
+ }
101
+ async function installSkill(skill, command) {
102
+ const destination = resolve(command.global ? homedir() : process.cwd(), command.agent === 'claude' ? '.claude' : '.agents', 'skills', skill.name, 'SKILL.md');
103
+ const content = Buffer.from(discoveryText(skill));
104
+ await mkdir(dirname(destination), { recursive: true });
105
+ try {
106
+ const file = await open(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o644);
107
+ try {
108
+ await file.writeFile(content);
109
+ }
110
+ finally {
111
+ await file.close();
112
+ }
113
+ return destination;
114
+ }
115
+ catch (error) {
116
+ if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST'))
117
+ throw error;
118
+ }
119
+ const existing = await lstat(destination);
120
+ if (!existing.isFile())
121
+ throw new TypeError(`Cannot install ${destination}. The destination must be a regular file, not a directory or symbolic link.`);
122
+ const file = await open(destination, constants.O_RDONLY | constants.O_NOFOLLOW);
123
+ let previous;
124
+ try {
125
+ if (!(await file.stat()).isFile())
126
+ throw new TypeError(`Cannot install ${destination}. The destination must be a regular file.`);
127
+ previous = await file.readFile();
128
+ }
129
+ finally {
130
+ await file.close();
131
+ }
132
+ if (previous.equals(content))
133
+ return destination;
134
+ if (!command.force)
135
+ throw new TypeError(`Cannot replace differing file ${destination}. Use --force to replace it.`);
136
+ const temporary = join(dirname(destination), `.SKILL.md.${randomUUID()}.tmp`);
137
+ try {
138
+ const replacement = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o644);
139
+ try {
140
+ await replacement.writeFile(content);
141
+ }
142
+ finally {
143
+ await replacement.close();
144
+ }
145
+ const current = await lstat(destination);
146
+ if (!current.isFile())
147
+ throw new TypeError(`Cannot install ${destination}. The destination must be a regular file, not a directory or symbolic link.`);
148
+ if (current.ino !== existing.ino || current.dev !== existing.dev || current.mtimeMs !== existing.mtimeMs || current.ctimeMs !== existing.ctimeMs)
149
+ throw new TypeError(`Cannot replace ${destination}. The destination changed during installation. Run the command again.`);
150
+ await rename(temporary, destination);
151
+ }
152
+ finally {
153
+ await rm(temporary, { force: true });
154
+ }
155
+ return destination;
156
+ }
157
+ export async function runSkillsCommand(positionals, options, seen) {
158
+ const command = parseCommand(positionals, options, seen);
159
+ const skills = await loadCatalog();
160
+ function named(name) {
161
+ const skill = skills.find((entry) => entry.name === name);
162
+ if (!skill)
163
+ throw new TypeError(`Unknown bundled skill ${name}. Use artifacts skills list to inspect available skills.`);
164
+ return skill;
165
+ }
166
+ switch (command.kind) {
167
+ case 'list':
168
+ process.stdout.write(skills.map((skill) => `${skill.name}\t${skill.description}\n`).join(''));
169
+ return;
170
+ case 'get': {
171
+ const selection = 'all' in command.selection ? skills : [named(command.selection.name)];
172
+ process.stdout.write(!command.full && 'name' in command.selection ? selection[0].skillText : await labeledContent(selection, command.full));
173
+ return;
174
+ }
175
+ case 'path':
176
+ process.stdout.write(`${command.name === undefined ? catalogDirectory : named(command.name).directory}\n`);
177
+ return;
178
+ case 'install':
179
+ process.stdout.write(`${await installSkill(named(command.name), command)}\n`);
180
+ }
181
+ }