@nimblebrain/mpak 0.0.1 → 0.0.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.
Files changed (94) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/CLAUDE.md +73 -34
  3. package/README.md +222 -57
  4. package/dist/commands/search.d.ts +12 -0
  5. package/dist/commands/search.d.ts.map +1 -0
  6. package/dist/commands/search.js +144 -0
  7. package/dist/commands/search.js.map +1 -0
  8. package/dist/commands/skills/index.d.ts +8 -0
  9. package/dist/commands/skills/index.d.ts.map +1 -0
  10. package/dist/commands/skills/index.js +8 -0
  11. package/dist/commands/skills/index.js.map +1 -0
  12. package/dist/commands/skills/install.d.ts +9 -0
  13. package/dist/commands/skills/install.d.ts.map +1 -0
  14. package/dist/commands/skills/install.js +110 -0
  15. package/dist/commands/skills/install.js.map +1 -0
  16. package/dist/commands/skills/list.d.ts +8 -0
  17. package/dist/commands/skills/list.d.ts.map +1 -0
  18. package/dist/commands/skills/list.js +89 -0
  19. package/dist/commands/skills/list.js.map +1 -0
  20. package/dist/commands/skills/pack.d.ts +22 -0
  21. package/dist/commands/skills/pack.d.ts.map +1 -0
  22. package/dist/commands/skills/pack.js +116 -0
  23. package/dist/commands/skills/pack.js.map +1 -0
  24. package/dist/commands/skills/pull.d.ts +9 -0
  25. package/dist/commands/skills/pull.d.ts.map +1 -0
  26. package/dist/commands/skills/pull.js +68 -0
  27. package/dist/commands/skills/pull.js.map +1 -0
  28. package/dist/commands/skills/search.d.ts +14 -0
  29. package/dist/commands/skills/search.d.ts.map +1 -0
  30. package/dist/commands/skills/search.js +53 -0
  31. package/dist/commands/skills/search.js.map +1 -0
  32. package/dist/commands/skills/show.d.ts +8 -0
  33. package/dist/commands/skills/show.d.ts.map +1 -0
  34. package/dist/commands/skills/show.js +64 -0
  35. package/dist/commands/skills/show.js.map +1 -0
  36. package/dist/commands/skills/validate.d.ts +25 -0
  37. package/dist/commands/skills/validate.d.ts.map +1 -0
  38. package/dist/commands/skills/validate.js +191 -0
  39. package/dist/commands/skills/validate.js.map +1 -0
  40. package/dist/lib/api/skills-client.d.ts +30 -0
  41. package/dist/lib/api/skills-client.d.ts.map +1 -0
  42. package/dist/lib/api/skills-client.js +110 -0
  43. package/dist/lib/api/skills-client.js.map +1 -0
  44. package/dist/program.d.ts +5 -1
  45. package/dist/program.d.ts.map +1 -1
  46. package/dist/program.js +98 -33
  47. package/dist/program.js.map +1 -1
  48. package/dist/schemas/generated/api-responses.d.ts +541 -0
  49. package/dist/schemas/generated/api-responses.d.ts.map +1 -0
  50. package/dist/schemas/generated/api-responses.js +313 -0
  51. package/dist/schemas/generated/api-responses.js.map +1 -0
  52. package/dist/schemas/generated/auth.d.ts +18 -0
  53. package/dist/schemas/generated/auth.d.ts.map +1 -0
  54. package/dist/schemas/generated/auth.js +18 -0
  55. package/dist/schemas/generated/auth.js.map +1 -0
  56. package/dist/schemas/generated/index.d.ts +5 -0
  57. package/dist/schemas/generated/index.d.ts.map +1 -0
  58. package/dist/schemas/generated/index.js +6 -0
  59. package/dist/schemas/generated/index.js.map +1 -0
  60. package/dist/schemas/generated/package.d.ts +43 -0
  61. package/dist/schemas/generated/package.d.ts.map +1 -0
  62. package/dist/schemas/generated/package.js +20 -0
  63. package/dist/schemas/generated/package.js.map +1 -0
  64. package/dist/schemas/generated/skill.d.ts +381 -0
  65. package/dist/schemas/generated/skill.d.ts.map +1 -0
  66. package/dist/schemas/generated/skill.js +216 -0
  67. package/dist/schemas/generated/skill.js.map +1 -0
  68. package/dist/utils/config-manager.d.ts +13 -1
  69. package/dist/utils/config-manager.d.ts.map +1 -1
  70. package/dist/utils/config-manager.js +76 -11
  71. package/dist/utils/config-manager.js.map +1 -1
  72. package/package.json +6 -2
  73. package/src/commands/search.ts +191 -0
  74. package/src/commands/skills/index.ts +7 -0
  75. package/src/commands/skills/install.ts +129 -0
  76. package/src/commands/skills/list.ts +116 -0
  77. package/src/commands/skills/pack.test.ts +260 -0
  78. package/src/commands/skills/pack.ts +145 -0
  79. package/src/commands/skills/pull.ts +88 -0
  80. package/src/commands/skills/search.ts +73 -0
  81. package/src/commands/skills/show.ts +72 -0
  82. package/src/commands/skills/validate.test.ts +466 -0
  83. package/src/commands/skills/validate.ts +227 -0
  84. package/src/lib/api/skills-client.ts +148 -0
  85. package/src/program.test.ts +1 -3
  86. package/src/program.ts +125 -35
  87. package/src/schemas/config.v1.schema.json +37 -0
  88. package/src/schemas/generated/api-responses.ts +386 -0
  89. package/src/schemas/generated/auth.ts +21 -0
  90. package/src/schemas/generated/index.ts +5 -0
  91. package/src/schemas/generated/package.ts +29 -0
  92. package/src/schemas/generated/skill.ts +271 -0
  93. package/src/utils/config-manager.test.ts +182 -2
  94. package/src/utils/config-manager.ts +126 -12
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Skills API client for mpak registry
3
+ */
4
+
5
+ import type {
6
+ SkillSearchResponse,
7
+ SkillDetail,
8
+ SkillDownloadInfo,
9
+ } from '../../schemas/generated/skill.js';
10
+
11
+ const DEFAULT_REGISTRY_URL = 'https://api.mpak.dev';
12
+
13
+ function getRegistryUrl(): string {
14
+ return process.env.MPAK_REGISTRY_URL || DEFAULT_REGISTRY_URL;
15
+ }
16
+
17
+ /**
18
+ * Parse a scoped name into scope and name parts
19
+ * Handles both @scope/name and scope/name formats
20
+ */
21
+ function parseScopedName(name: string): { scope: string; skillName: string } {
22
+ const normalizedName = name.startsWith('@') ? name.slice(1) : name;
23
+ const [scope, skillName] = normalizedName.split('/');
24
+ if (!scope || !skillName) {
25
+ throw new Error(`Invalid skill name format: ${name}. Expected @scope/name or scope/name`);
26
+ }
27
+ return { scope, skillName };
28
+ }
29
+
30
+ export interface SkillSearchOptions {
31
+ q?: string;
32
+ tags?: string;
33
+ category?: string;
34
+ surface?: string;
35
+ sort?: 'downloads' | 'recent' | 'name';
36
+ limit?: number;
37
+ offset?: number;
38
+ }
39
+
40
+ /**
41
+ * Search for skills in the registry
42
+ */
43
+ export async function searchSkills(options: SkillSearchOptions): Promise<SkillSearchResponse> {
44
+ const baseUrl = getRegistryUrl();
45
+ const params = new URLSearchParams();
46
+
47
+ if (options.q) params.set('q', options.q);
48
+ if (options.tags) params.set('tags', options.tags);
49
+ if (options.category) params.set('category', options.category);
50
+ if (options.surface) params.set('surface', options.surface);
51
+ if (options.sort) params.set('sort', options.sort);
52
+ if (options.limit) params.set('limit', options.limit.toString());
53
+ if (options.offset) params.set('offset', options.offset.toString());
54
+
55
+ const url = `${baseUrl}/v1/skills/search?${params.toString()}`;
56
+
57
+ const response = await fetch(url, {
58
+ headers: { Accept: 'application/json' },
59
+ });
60
+
61
+ if (!response.ok) {
62
+ const text = await response.text();
63
+ throw new Error(`Search failed (${response.status}): ${text}`);
64
+ }
65
+
66
+ return response.json() as Promise<SkillSearchResponse>;
67
+ }
68
+
69
+ /**
70
+ * Get skill details from the registry
71
+ */
72
+ export async function getSkillDetails(name: string): Promise<SkillDetail> {
73
+ const baseUrl = getRegistryUrl();
74
+ const { scope, skillName } = parseScopedName(name);
75
+
76
+ // Server expects: /v1/skills/@scope/name
77
+ const url = `${baseUrl}/v1/skills/@${scope}/${skillName}`;
78
+
79
+ const response = await fetch(url, {
80
+ headers: { Accept: 'application/json' },
81
+ });
82
+
83
+ if (!response.ok) {
84
+ if (response.status === 404) {
85
+ throw new Error(`Skill not found: ${name}`);
86
+ }
87
+ const text = await response.text();
88
+ throw new Error(`Failed to get skill details (${response.status}): ${text}`);
89
+ }
90
+
91
+ return response.json() as Promise<SkillDetail>;
92
+ }
93
+
94
+ /**
95
+ * Get download info for a skill
96
+ */
97
+ export async function getSkillDownloadInfo(
98
+ name: string,
99
+ version?: string
100
+ ): Promise<SkillDownloadInfo> {
101
+ const baseUrl = getRegistryUrl();
102
+ const { scope, skillName } = parseScopedName(name);
103
+
104
+ // Server expects: /v1/skills/@scope/name/download or /v1/skills/@scope/name/versions/x.y.z/download
105
+ const versionPath = version ? `/versions/${version}` : '';
106
+ const url = `${baseUrl}/v1/skills/@${scope}/${skillName}${versionPath}/download`;
107
+
108
+ const response = await fetch(url, {
109
+ headers: { Accept: 'application/json' },
110
+ });
111
+
112
+ if (!response.ok) {
113
+ if (response.status === 404) {
114
+ throw new Error(`Skill not found: ${name}${version ? `@${version}` : ''}`);
115
+ }
116
+ const text = await response.text();
117
+ throw new Error(`Failed to get download info (${response.status}): ${text}`);
118
+ }
119
+
120
+ return response.json() as Promise<SkillDownloadInfo>;
121
+ }
122
+
123
+ /**
124
+ * Download a skill bundle
125
+ */
126
+ export async function downloadSkillBundle(
127
+ downloadUrl: string,
128
+ expectedSha256?: string
129
+ ): Promise<Buffer> {
130
+ const response = await fetch(downloadUrl);
131
+
132
+ if (!response.ok) {
133
+ throw new Error(`Download failed (${response.status})`);
134
+ }
135
+
136
+ const buffer = Buffer.from(await response.arrayBuffer());
137
+
138
+ // Verify SHA256 if provided
139
+ if (expectedSha256) {
140
+ const { createHash } = await import('crypto');
141
+ const hash = createHash('sha256').update(buffer).digest('hex');
142
+ if (hash !== expectedSha256) {
143
+ throw new Error(`SHA256 mismatch: expected ${expectedSha256}, got ${hash}`);
144
+ }
145
+ }
146
+
147
+ return buffer;
148
+ }
@@ -9,9 +9,7 @@ describe('createProgram', () => {
9
9
 
10
10
  it('should have a description', () => {
11
11
  const program = createProgram();
12
- expect(program.description()).toBe(
13
- 'CLI for downloading MCPB bundles from the package directory'
14
- );
12
+ expect(program.description()).toBe('CLI for MCP bundles and Agent Skills');
15
13
  });
16
14
 
17
15
  it('should have version option', () => {
package/src/program.ts CHANGED
@@ -1,26 +1,66 @@
1
1
  import { Command } from 'commander';
2
2
  import { getVersion } from './utils/version.js';
3
+ import { handleUnifiedSearch } from './commands/search.js';
3
4
  import { handleSearch } from './commands/packages/search.js';
4
5
  import { handleShow } from './commands/packages/show.js';
5
6
  import { handlePull } from './commands/packages/pull.js';
6
7
  import { handleRun } from './commands/packages/run.js';
7
- import { handleConfigSet, handleConfigGet, handleConfigList, handleConfigClear } from './commands/config.js';
8
+ import {
9
+ handleConfigSet,
10
+ handleConfigGet,
11
+ handleConfigList,
12
+ handleConfigClear,
13
+ } from './commands/config.js';
14
+ import {
15
+ handleSkillValidate,
16
+ handleSkillPack,
17
+ handleSkillSearch,
18
+ handleSkillShow,
19
+ handleSkillPull,
20
+ handleSkillInstall,
21
+ handleSkillList,
22
+ } from './commands/skills/index.js';
8
23
 
9
24
  /**
10
25
  * Creates and configures the CLI program
11
26
  *
12
- * MVP: Unauthenticated commands only (v1 API)
27
+ * Command structure:
28
+ * - mpak search <query> - Unified search (bundles + skills)
29
+ * - mpak bundle <command> - MCP bundle commands
30
+ * - mpak skill <command> - Agent skill commands
31
+ * - mpak config <command> - Configuration commands
13
32
  */
14
33
  export function createProgram(): Command {
15
34
  const program = new Command();
16
35
 
17
36
  program
18
37
  .name('mpak')
19
- .description('CLI for downloading MCPB bundles from the package directory')
38
+ .description('CLI for MCP bundles and Agent Skills')
20
39
  .version(getVersion(), '-v, --version', 'Output the current version');
21
40
 
22
- // Search command
41
+ // ==========================================================================
42
+ // Unified search (bundles + skills)
43
+ // ==========================================================================
44
+
23
45
  program
46
+ .command('search <query>')
47
+ .description('Search bundles and skills')
48
+ .option('--type <type>', 'Filter by type (bundle, skill)')
49
+ .option('--sort <field>', 'Sort by: downloads, recent, name')
50
+ .option('--limit <number>', 'Limit results', parseInt)
51
+ .option('--offset <number>', 'Pagination offset', parseInt)
52
+ .option('--json', 'Output as JSON')
53
+ .action(async (query, options) => {
54
+ await handleUnifiedSearch(query, options);
55
+ });
56
+
57
+ // ==========================================================================
58
+ // Bundle namespace (MCP bundles)
59
+ // ==========================================================================
60
+
61
+ const bundle = program.command('bundle').description('MCP bundle commands');
62
+
63
+ bundle
24
64
  .command('search <query>')
25
65
  .description('Search public bundles')
26
66
  .option('--type <type>', 'Filter by server type (node, python, binary)')
@@ -32,8 +72,7 @@ export function createProgram(): Command {
32
72
  await handleSearch(query, options);
33
73
  });
34
74
 
35
- // Show command
36
- program
75
+ bundle
37
76
  .command('show <package>')
38
77
  .description('Show detailed information about a bundle')
39
78
  .option('--json', 'Output as JSON')
@@ -41,19 +80,9 @@ export function createProgram(): Command {
41
80
  await handleShow(packageName, options);
42
81
  });
43
82
 
44
- // Info command (alias for show)
45
- program
46
- .command('info <package>')
47
- .description('Show detailed information about a bundle (alias for show)')
48
- .option('--json', 'Output as JSON')
49
- .action(async (packageName, options) => {
50
- await handleShow(packageName, options);
51
- });
52
-
53
- // Pull command
54
- program
83
+ bundle
55
84
  .command('pull <package>')
56
- .description('Download a bundle from the registry (e.g., @scope/name or @scope/name@1.0.0)')
85
+ .description('Download a bundle from the registry')
57
86
  .option('-o, --output <path>', 'Output file path')
58
87
  .option('--os <os>', 'Target OS (darwin, linux, win32)')
59
88
  .option('--arch <arch>', 'Target architecture (x64, arm64)')
@@ -62,35 +91,96 @@ export function createProgram(): Command {
62
91
  await handlePull(packageSpec, options);
63
92
  });
64
93
 
65
- // Install command (alias for pull)
66
- program
67
- .command('install <package>')
68
- .description('Download a bundle from the registry (alias for pull)')
69
- .option('-o, --output <path>', 'Output file path')
70
- .option('--os <os>', 'Target OS (darwin, linux, win32)')
71
- .option('--arch <arch>', 'Target architecture (x64, arm64)')
72
- .option('--json', 'Output download info as JSON')
73
- .action(async (packageSpec, options) => {
74
- await handlePull(packageSpec, options);
75
- });
76
-
77
- // Run command
78
- program
94
+ bundle
79
95
  .command('run <package>')
80
- .description('Run an MCP server from the registry (e.g., @scope/name or @scope/name@1.0.0)')
96
+ .description('Run an MCP server from the registry')
81
97
  .option('--update', 'Force re-download even if cached')
82
98
  .action(async (packageSpec, options) => {
83
99
  await handleRun(packageSpec, options);
84
100
  });
85
101
 
86
- // Config command group
102
+ // ==========================================================================
103
+ // Skill namespace (Agent Skills)
104
+ // ==========================================================================
105
+
106
+ const skill = program.command('skill').description('Agent skill commands');
107
+
108
+ skill
109
+ .command('validate <path>')
110
+ .description('Validate a skill directory against the Agent Skills spec')
111
+ .option('--json', 'Output as JSON')
112
+ .action(async (path, options) => {
113
+ await handleSkillValidate(path, options);
114
+ });
115
+
116
+ skill
117
+ .command('pack <path>')
118
+ .description('Create a .skill bundle from a skill directory')
119
+ .option('-o, --output <path>', 'Output file path')
120
+ .option('--json', 'Output as JSON')
121
+ .action(async (path, options) => {
122
+ await handleSkillPack(path, options);
123
+ });
124
+
125
+ skill
126
+ .command('search <query>')
127
+ .description('Search skills in the registry')
128
+ .option('--tags <tags>', 'Filter by tags (comma-separated)')
129
+ .option('--category <category>', 'Filter by category')
130
+ .option('--surface <surface>', 'Filter by surface (claude-code, claude-api, claude-ai)')
131
+ .option('--sort <field>', 'Sort by: downloads, recent, name')
132
+ .option('--limit <number>', 'Limit results', parseInt)
133
+ .option('--offset <number>', 'Pagination offset', parseInt)
134
+ .option('--json', 'Output as JSON')
135
+ .action(async (query, options) => {
136
+ await handleSkillSearch(query, options);
137
+ });
138
+
139
+ skill
140
+ .command('show <name>')
141
+ .description('Show detailed information about a skill')
142
+ .option('--json', 'Output as JSON')
143
+ .action(async (name, options) => {
144
+ await handleSkillShow(name, options);
145
+ });
146
+
147
+ skill
148
+ .command('pull <name>')
149
+ .description('Download a .skill bundle from the registry')
150
+ .option('-o, --output <path>', 'Output file path')
151
+ .option('--json', 'Output as JSON')
152
+ .action(async (name, options) => {
153
+ await handleSkillPull(name, options);
154
+ });
155
+
156
+ skill
157
+ .command('install <name>')
158
+ .description('Install a skill to ~/.claude/skills/')
159
+ .option('--force', 'Overwrite existing installation')
160
+ .option('--json', 'Output as JSON')
161
+ .action(async (name, options) => {
162
+ await handleSkillInstall(name, options);
163
+ });
164
+
165
+ skill
166
+ .command('list')
167
+ .description('List installed skills')
168
+ .option('--json', 'Output as JSON')
169
+ .action(async (options) => {
170
+ await handleSkillList(options);
171
+ });
172
+
173
+ // ==========================================================================
174
+ // Config commands (shared for bundles and skills)
175
+ // ==========================================================================
176
+
87
177
  const configCmd = program
88
178
  .command('config')
89
179
  .description('Manage per-package configuration values');
90
180
 
91
181
  configCmd
92
182
  .command('set <package> <key=value...>')
93
- .description('Set config value(s) for a package (e.g., mpak config set @scope/name api_key=xxx)')
183
+ .description('Set config value(s) for a package')
94
184
  .action(async (packageName, keyValuePairs) => {
95
185
  await handleConfigSet(packageName, keyValuePairs);
96
186
  });
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://mpak.dev/schemas/config.v1.json",
4
+ "title": "mpak CLI Configuration v1",
5
+ "description": "Configuration file for the mpak CLI stored at ~/.mpak/config.json",
6
+ "type": "object",
7
+ "required": ["version", "lastUpdated"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "version": {
11
+ "type": "string",
12
+ "const": "1.0.0",
13
+ "description": "Configuration schema version"
14
+ },
15
+ "lastUpdated": {
16
+ "type": "string",
17
+ "format": "date-time",
18
+ "description": "ISO 8601 timestamp of last configuration update"
19
+ },
20
+ "registryUrl": {
21
+ "type": "string",
22
+ "format": "uri",
23
+ "description": "Custom registry URL (overrides default https://api.mpak.dev)"
24
+ },
25
+ "packages": {
26
+ "type": "object",
27
+ "description": "Per-package configuration values (user_config)",
28
+ "additionalProperties": {
29
+ "type": "object",
30
+ "description": "Configuration key-value pairs for a specific package",
31
+ "additionalProperties": {
32
+ "type": "string"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }