@agent-nexus/csreg 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@agent-nexus/csreg",
3
+ "version": "0.1.0",
4
+ "description": "CLI for Claude Skills Registry",
5
+ "type": "module",
6
+ "bin": {
7
+ "csreg": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup",
11
+ "dev": "tsx src/index.ts",
12
+ "test": "vitest run"
13
+ },
14
+ "dependencies": {
15
+ "commander": "^13.0.0",
16
+ "chalk": "^5.4.0",
17
+ "ora": "^8.0.0",
18
+ "yaml": "^2.7.0",
19
+ "semver": "^7.7.0",
20
+ "tar": "^7.4.0",
21
+ "inquirer": "^12.0.0",
22
+ "cli-table3": "^0.6.0",
23
+ "glob": "^11.0.0"
24
+ },
25
+ "publishConfig": {
26
+ "access": "restricted"
27
+ },
28
+ "devDependencies": {
29
+ "tsup": "^8.0.0",
30
+ "tsx": "^4.21.0",
31
+ "vitest": "^4.0.0",
32
+ "@types/semver": "^7.5.0",
33
+ "@types/inquirer": "^9.0.0",
34
+ "typescript": "^5.0.0"
35
+ }
36
+ }
@@ -0,0 +1,145 @@
1
+ import { getApiUrl, getAuthToken } from './config.js';
2
+ import { CliError } from './lib/errors.js';
3
+
4
+ interface Rfc7807Error {
5
+ type?: string;
6
+ title?: string;
7
+ detail?: string;
8
+ status?: number;
9
+ }
10
+
11
+ interface RequestOptions {
12
+ body?: unknown;
13
+ headers?: Record<string, string>;
14
+ query?: Record<string, string>;
15
+ }
16
+
17
+ const MAX_RETRIES = 3;
18
+ const BASE_DELAY_MS = 500;
19
+
20
+ export class ApiClient {
21
+ private baseUrl: string;
22
+ private token: string | undefined;
23
+
24
+ constructor() {
25
+ this.baseUrl = getApiUrl();
26
+ this.token = getAuthToken();
27
+ }
28
+
29
+ private buildUrl(path: string, query?: Record<string, string>): string {
30
+ const url = new URL(path, this.baseUrl);
31
+ if (query) {
32
+ for (const [key, value] of Object.entries(query)) {
33
+ url.searchParams.set(key, value);
34
+ }
35
+ }
36
+ return url.toString();
37
+ }
38
+
39
+ private buildHeaders(extra?: Record<string, string>): Record<string, string> {
40
+ const headers: Record<string, string> = {
41
+ 'Content-Type': 'application/json',
42
+ 'Accept': 'application/json',
43
+ ...extra,
44
+ };
45
+ if (this.token) {
46
+ headers['Authorization'] = `Bearer ${this.token}`;
47
+ }
48
+ return headers;
49
+ }
50
+
51
+ private async handleResponse<T>(response: Response): Promise<T> {
52
+ if (response.ok) {
53
+ if (response.status === 204) {
54
+ return undefined as T;
55
+ }
56
+ return response.json() as Promise<T>;
57
+ }
58
+
59
+ let errorBody: Rfc7807Error | undefined;
60
+ try {
61
+ errorBody = await response.json() as Rfc7807Error;
62
+ } catch {
63
+ // response body was not JSON
64
+ }
65
+
66
+ const detail = errorBody?.detail ?? response.statusText;
67
+ const title = errorBody?.title ?? `HTTP ${response.status}`;
68
+ const suggestions: string[] = [];
69
+
70
+ if (response.status === 401) {
71
+ suggestions.push('Run `csreg login` to authenticate.');
72
+ } else if (response.status === 403) {
73
+ suggestions.push('You may not have permission for this action.');
74
+ } else if (response.status === 404) {
75
+ suggestions.push('Check that the skill name and scope are correct.');
76
+ }
77
+
78
+ throw new CliError(`${title}: ${detail}`, suggestions);
79
+ }
80
+
81
+ private async requestWithRetry<T>(
82
+ method: string,
83
+ path: string,
84
+ opts?: RequestOptions,
85
+ ): Promise<T> {
86
+ const url = this.buildUrl(path, opts?.query);
87
+ const headers = this.buildHeaders(opts?.headers);
88
+
89
+ let lastError: unknown;
90
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
91
+ try {
92
+ const response = await fetch(url, {
93
+ method,
94
+ headers,
95
+ body: opts?.body !== undefined ? JSON.stringify(opts.body) : undefined,
96
+ });
97
+
98
+ if (response.status >= 500 && attempt < MAX_RETRIES - 1) {
99
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt);
100
+ await new Promise(resolve => setTimeout(resolve, delay));
101
+ continue;
102
+ }
103
+
104
+ return await this.handleResponse<T>(response);
105
+ } catch (error) {
106
+ lastError = error;
107
+ if (error instanceof CliError) {
108
+ throw error;
109
+ }
110
+ if (attempt < MAX_RETRIES - 1) {
111
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt);
112
+ await new Promise(resolve => setTimeout(resolve, delay));
113
+ continue;
114
+ }
115
+ }
116
+ }
117
+
118
+ throw lastError instanceof CliError
119
+ ? lastError
120
+ : new CliError(`Request failed: ${String(lastError)}`, [
121
+ 'Check your internet connection.',
122
+ 'The API server may be unavailable.',
123
+ ]);
124
+ }
125
+
126
+ async get<T>(path: string, opts?: RequestOptions): Promise<T> {
127
+ return this.requestWithRetry<T>('GET', path, opts);
128
+ }
129
+
130
+ async post<T>(path: string, opts?: RequestOptions): Promise<T> {
131
+ return this.requestWithRetry<T>('POST', path, opts);
132
+ }
133
+
134
+ async put<T>(path: string, opts?: RequestOptions): Promise<T> {
135
+ return this.requestWithRetry<T>('PUT', path, opts);
136
+ }
137
+
138
+ async patch<T>(path: string, opts?: RequestOptions): Promise<T> {
139
+ return this.requestWithRetry<T>('PATCH', path, opts);
140
+ }
141
+
142
+ async delete<T>(path: string, opts?: RequestOptions): Promise<T> {
143
+ return this.requestWithRetry<T>('DELETE', path, opts);
144
+ }
145
+ }
@@ -0,0 +1,71 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { ApiClient } from '../api-client.js';
4
+ import { handleError, CliError } from '../lib/errors.js';
5
+
6
+ interface SkillInfo {
7
+ name: string;
8
+ scope: string;
9
+ description?: string;
10
+ type: string;
11
+ latestVersion: string;
12
+ totalDownloads: number;
13
+ tags: string[];
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ author?: { displayName: string; email: string };
17
+ }
18
+
19
+ export const infoCommand = new Command('info')
20
+ .description('Display details about a skill')
21
+ .argument('<ref>', 'Skill reference (scope/name)')
22
+ .action(async (ref: string) => {
23
+ try {
24
+ const cleaned = ref.startsWith('@') ? ref.slice(1) : ref;
25
+ const slashIndex = cleaned.indexOf('/');
26
+ if (slashIndex < 0) {
27
+ throw new CliError(`Invalid skill reference: ${ref}`, [
28
+ 'Use the format: @scope/name',
29
+ ]);
30
+ }
31
+
32
+ const scope = cleaned.slice(0, slashIndex);
33
+ const name = cleaned.slice(slashIndex + 1);
34
+
35
+ const client = new ApiClient();
36
+ const skill = await client.get<SkillInfo>(`/api/v1/skills/${scope}/${name}`);
37
+
38
+ console.log('');
39
+ console.log(chalk.bold(`${skill.scope}/${skill.name}`) + chalk.dim(` @ ${skill.latestVersion}`));
40
+ console.log('');
41
+
42
+ if (skill.description) {
43
+ console.log(skill.description);
44
+ console.log('');
45
+ }
46
+
47
+ const fields: [string, string][] = [
48
+ ['Type', skill.type],
49
+ ['Latest Version', skill.latestVersion],
50
+ ['Downloads', String(skill.totalDownloads)],
51
+ ['Created', new Date(skill.createdAt).toLocaleDateString()],
52
+ ['Updated', new Date(skill.updatedAt).toLocaleDateString()],
53
+ ];
54
+
55
+ if (skill.author) {
56
+ fields.push(['Author', `${skill.author.displayName} (${skill.author.email})`]);
57
+ }
58
+
59
+ if (skill.tags.length > 0) {
60
+ fields.push(['Tags', skill.tags.join(', ')]);
61
+ }
62
+
63
+ const maxLabel = Math.max(...fields.map(([label]) => label.length));
64
+ for (const [label, value] of fields) {
65
+ console.log(` ${chalk.cyan(label.padEnd(maxLabel + 2))}${value}`);
66
+ }
67
+ console.log('');
68
+ } catch (err) {
69
+ handleError(err);
70
+ }
71
+ });
@@ -0,0 +1,112 @@
1
+ import { Command } from 'commander';
2
+ import { input, confirm } from '@inquirer/prompts';
3
+ import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { success, info } from '../lib/output.js';
6
+ import { handleError, CliError } from '../lib/errors.js';
7
+
8
+ export const initCommand = new Command('init')
9
+ .description('Initialize a new skill project')
10
+ .argument('[dir]', 'Directory to initialize the skill in')
11
+ .action(async (dir?: string) => {
12
+ try {
13
+ const name = await input({
14
+ message: 'Skill name:',
15
+ validate: (value: string) => {
16
+ if (!value.trim()) return 'Name is required.';
17
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(value.trim())) {
18
+ return 'Must be lowercase alphanumeric with hyphens (e.g., my-skill).';
19
+ }
20
+ if (value.trim().length > 64) {
21
+ return 'Name must be at most 64 characters.';
22
+ }
23
+ if (value.includes('--')) {
24
+ return 'Name must not contain consecutive hyphens (--).';
25
+ }
26
+ return true;
27
+ },
28
+ });
29
+
30
+ const description = await input({
31
+ message: 'Description (what this skill does and when to use it):',
32
+ validate: (value: string) => {
33
+ if (!value.trim()) return 'Description is required — Claude uses it to decide when to invoke the skill.';
34
+ return true;
35
+ },
36
+ });
37
+
38
+ const scope = await input({
39
+ message: 'Scope (your team/username, for registry publishing):',
40
+ validate: (value: string) => {
41
+ if (!value.trim()) return 'Scope is required for publishing.';
42
+ return true;
43
+ },
44
+ });
45
+
46
+ const userInvocable = await confirm({
47
+ message: 'User-invocable (show in /slash command menu)?',
48
+ default: true,
49
+ });
50
+
51
+ const targetDir = resolve(dir ?? name.trim());
52
+
53
+ if (existsSync(targetDir)) {
54
+ throw new CliError(`Directory already exists: ${targetDir}`, [
55
+ 'Choose a different name or directory.',
56
+ ]);
57
+ }
58
+
59
+ mkdirSync(targetDir, { recursive: true });
60
+
61
+ // Build SKILL.md with YAML frontmatter
62
+ const frontmatterLines = [
63
+ '---',
64
+ `name: ${name.trim()}`,
65
+ `description: ${description.trim()}`,
66
+ `version: "0.1.0"`,
67
+ `scope: ${scope.trim()}`,
68
+ ];
69
+
70
+ if (!userInvocable) {
71
+ frontmatterLines.push('user-invocable: false');
72
+ }
73
+
74
+ frontmatterLines.push('---');
75
+
76
+ const skillContent = `${frontmatterLines.join('\n')}
77
+
78
+ # ${name.trim()}
79
+
80
+ ${description.trim()}
81
+
82
+ ## Instructions
83
+
84
+ Add your skill instructions here. This is the prompt that Claude will follow
85
+ when this skill is invoked.
86
+
87
+ You can use \`$ARGUMENTS\` to reference arguments passed by the user.
88
+ For example: \`/my-skill some-argument\` makes \`$ARGUMENTS\` = "some-argument".
89
+ `;
90
+
91
+ writeFileSync(
92
+ join(targetDir, 'SKILL.md'),
93
+ skillContent,
94
+ 'utf-8',
95
+ );
96
+
97
+ console.log('');
98
+ success(`Created skill "${scope.trim()}/${name.trim()}" in ${targetDir}`);
99
+ info('Files created:');
100
+ console.log(' - SKILL.md');
101
+ console.log('');
102
+ info('Next steps:');
103
+ console.log(' 1. Edit SKILL.md with your skill instructions');
104
+ console.log(' 2. Run `csreg validate` to check your skill');
105
+ console.log(' 3. Run `csreg push` to publish to the registry');
106
+ console.log('');
107
+ info('To use locally without publishing:');
108
+ console.log(` cp -r ${targetDir} .claude/skills/${name.trim()}`);
109
+ } catch (err) {
110
+ handleError(err);
111
+ }
112
+ });
@@ -0,0 +1,43 @@
1
+ import { Command } from 'commander';
2
+ import { input } from '@inquirer/prompts';
3
+ import { setConfig, getApiUrl } from '../config.js';
4
+ import { ApiClient } from '../api-client.js';
5
+ import { success, info } from '../lib/output.js';
6
+ import { handleError } from '../lib/errors.js';
7
+
8
+ export const loginCommand = new Command('login')
9
+ .description('Authenticate with the Skills Registry')
10
+ .option('--token <token>', 'Provide auth token directly')
11
+ .action(async (opts: { token?: string }) => {
12
+ try {
13
+ let token = opts.token;
14
+
15
+ if (!token) {
16
+ const apiUrl = getApiUrl();
17
+ info(`Open this URL to authenticate:`);
18
+ console.log(` ${apiUrl}/cli/auth`);
19
+ console.log('');
20
+
21
+ token = await input({
22
+ message: 'Paste your auth token:',
23
+ validate: (value: string) => {
24
+ if (!value.trim()) return 'Token is required.';
25
+ return true;
26
+ },
27
+ });
28
+ token = token.trim();
29
+ }
30
+
31
+ setConfig({ token });
32
+
33
+ // Verify the token by calling whoami
34
+ const client = new ApiClient();
35
+ const user = await client.get<{ email: string; displayName: string }>(
36
+ '/api/v1/users/me',
37
+ );
38
+
39
+ success(`Logged in as ${user.displayName} (${user.email})`);
40
+ } catch (err) {
41
+ handleError(err);
42
+ }
43
+ });
@@ -0,0 +1,15 @@
1
+ import { Command } from 'commander';
2
+ import { setConfig } from '../config.js';
3
+ import { success } from '../lib/output.js';
4
+ import { handleError } from '../lib/errors.js';
5
+
6
+ export const logoutCommand = new Command('logout')
7
+ .description('Remove stored authentication credentials')
8
+ .action(async () => {
9
+ try {
10
+ setConfig({ token: undefined });
11
+ success('Logged out successfully.');
12
+ } catch (err) {
13
+ handleError(err);
14
+ }
15
+ });
@@ -0,0 +1,40 @@
1
+ import { Command } from 'commander';
2
+ import { resolve } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { runValidation } from './validate.js';
5
+ import { pack as archivePack } from '../lib/archive.js';
6
+ import { success, info } from '../lib/output.js';
7
+ import { handleError, CliError } from '../lib/errors.js';
8
+
9
+ export const packCommand = new Command('pack')
10
+ .description('Pack a skill into a tarball')
11
+ .argument('[dir]', 'Skill directory', '.')
12
+ .action(async (dir: string) => {
13
+ try {
14
+ const resolved = resolve(dir);
15
+ if (!existsSync(resolved)) {
16
+ throw new CliError(`Directory not found: ${resolved}`);
17
+ }
18
+
19
+ // Validate first
20
+ const validation = runValidation(resolved);
21
+ if (!validation.valid) {
22
+ for (const e of validation.errors) {
23
+ console.error(e);
24
+ }
25
+ throw new CliError('Validation failed. Fix errors before packing.', [
26
+ 'Run `csreg validate` for details.',
27
+ ]);
28
+ }
29
+
30
+ const result = await archivePack(resolved);
31
+
32
+ console.log('');
33
+ success('Skill packed successfully.');
34
+ info(`Archive: ${result.path}`);
35
+ info(`Size: ${(result.size / 1024).toFixed(1)}KB`);
36
+ info(`SHA-256: ${result.sha256}`);
37
+ } catch (err) {
38
+ handleError(err);
39
+ }
40
+ });