oathbound 0.14.0 → 0.15.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/cli.ts DELETED
@@ -1,617 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import { createClient } from '@supabase/supabase-js';
4
- import { createHash } from 'node:crypto';
5
- import { execFileSync } from 'node:child_process';
6
- import {
7
- writeFileSync, readFileSync, unlinkSync, existsSync, mkdirSync,
8
- } from 'node:fs';
9
- import { join, basename } from 'node:path';
10
- import { tmpdir } from 'node:os';
11
- import { intro, outro, select, confirm, cancel, isCancel } from '@clack/prompts';
12
-
13
- import { BRAND, TEAL, GREEN, RED, YELLOW, DIM, BOLD, RESET, usage, agentUsage, fail, spinner } from './ui';
14
- import {
15
- stripJsoncComments, writeOathboundConfig, mergeClaudeSettings,
16
- type EnforcementLevel, type MergeResult,
17
- } from './config';
18
- import { checkForUpdate, isNewer } from './update';
19
- import { isValidSemver, compareSemver } from './semver';
20
- import { verify, verifyCheck, findSkillsDir } from './verify';
21
- import { login, logout, whoami } from './auth';
22
- import { push } from './push';
23
- import { search, parseSearchArgs } from './search';
24
- import { agentPush } from './agent-push';
25
- import { agentSearch, parseAgentSearchArgs } from './agent-search';
26
-
27
- // Re-exports for tests
28
- export { stripJsoncComments, writeOathboundConfig, mergeClaudeSettings, type MergeResult } from './config';
29
- export { isNewer } from './update';
30
- export { installDevDependency, type InstallResult, setup, addPrepareScript, type PrepareResult, addTrustedDependency, type TrustedDepResult };
31
-
32
- const VERSION = '0.14.0';
33
-
34
- // --- Supabase ---
35
- const SUPABASE_URL = 'https://mjnfqagwuewhgwbtrdgs.supabase.co';
36
- const SUPABASE_ANON_KEY = 'sb_publishable_T-rk0azNRqAMLLGCyadyhQ_ulk9685n';
37
- const API_BASE = process.env.OATHBOUND_API_URL ?? 'https://www.oathbound.ai';
38
-
39
- // --- Types ---
40
- interface SkillRow {
41
- id: string;
42
- name: string;
43
- namespace: string;
44
- version: string;
45
- tar_hash: string;
46
- storage_path: string;
47
- }
48
-
49
- function parseSkillArg(arg: string): { namespace: string; name: string; version: string | null } | null {
50
- const slash = arg.indexOf('/');
51
- if (slash < 1 || slash === arg.length - 1) return null;
52
- const afterSlash = arg.slice(slash + 1);
53
- const atIdx = afterSlash.indexOf('@');
54
- if (atIdx === -1) {
55
- return { namespace: arg.slice(0, slash), name: afterSlash, version: null };
56
- }
57
- const name = afterSlash.slice(0, atIdx);
58
- if (!name) return null;
59
- const vStr = afterSlash.slice(atIdx + 1);
60
- if (!isValidSemver(vStr)) return null;
61
- return { namespace: arg.slice(0, slash), name, version: vStr };
62
- }
63
-
64
- // --- Package manager detection ---
65
- type PackageManager = 'bun' | 'pnpm' | 'yarn' | 'npm';
66
-
67
- function detectPackageManager(): PackageManager {
68
- if (existsSync(join(process.cwd(), 'bun.lockb')) || existsSync(join(process.cwd(), 'bun.lock'))) return 'bun';
69
- if (existsSync(join(process.cwd(), 'pnpm-lock.yaml'))) return 'pnpm';
70
- if (existsSync(join(process.cwd(), 'yarn.lock'))) return 'yarn';
71
- return 'npm';
72
- }
73
-
74
- type InstallResult = 'installed' | 'skipped' | 'failed' | 'no-package-json';
75
-
76
- function installDevDependency(): InstallResult {
77
- const pkgPath = join(process.cwd(), 'package.json');
78
- if (!existsSync(pkgPath)) return 'no-package-json';
79
-
80
- try {
81
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
82
- if (pkg.devDependencies?.oathbound || pkg.dependencies?.oathbound) return 'skipped';
83
- } catch {
84
- // Malformed package.json — proceed with install attempt, let the package manager deal with it
85
- }
86
-
87
- const pm = detectPackageManager();
88
- const cmds: Record<PackageManager, [string, string[]]> = {
89
- bun: ['bun', ['add', '--dev', 'oathbound']],
90
- pnpm: ['pnpm', ['add', '--save-dev', 'oathbound']],
91
- yarn: ['yarn', ['add', '--dev', 'oathbound']],
92
- npm: ['npm', ['install', '--save-dev', 'oathbound']],
93
- };
94
-
95
- const [bin, args] = cmds[pm];
96
- try {
97
- execFileSync(bin, args, { stdio: 'pipe', cwd: process.cwd() });
98
- return 'installed';
99
- } catch {
100
- return 'failed';
101
- }
102
- }
103
-
104
- // --- Setup command (non-interactive, idempotent, runs via prepare hook) ---
105
- function setup(): void {
106
- if (!existsSync(join(process.cwd(), '.oathbound.jsonc'))) return;
107
- const result = mergeClaudeSettings();
108
- if (result === 'malformed') {
109
- process.stderr.write('oathbound setup: .claude/settings.json is malformed — hooks not installed\n');
110
- process.exit(1);
111
- }
112
- }
113
-
114
- type TrustedDepResult = 'added' | 'skipped';
115
-
116
- function addTrustedDependency(): TrustedDepResult {
117
- const pkgPath = join(process.cwd(), 'package.json');
118
- if (!existsSync(pkgPath)) return 'skipped';
119
-
120
- let pkg: Record<string, unknown>;
121
- try {
122
- pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
123
- } catch {
124
- return 'skipped';
125
- }
126
-
127
- const trusted = Array.isArray(pkg.trustedDependencies) ? pkg.trustedDependencies as string[] : [];
128
- if (trusted.includes('oathbound')) return 'skipped';
129
-
130
- pkg.trustedDependencies = [...trusted, 'oathbound'];
131
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
132
- return 'added';
133
- }
134
-
135
- type PrepareResult = 'added' | 'appended' | 'skipped';
136
-
137
- function addPrepareScript(): PrepareResult {
138
- const pkgPath = join(process.cwd(), 'package.json');
139
- if (!existsSync(pkgPath)) return 'skipped';
140
-
141
- let pkg: Record<string, unknown>;
142
- try {
143
- pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
144
- } catch {
145
- return 'skipped'; // malformed package.json — let the package manager deal with it
146
- }
147
-
148
- const prepare = (pkg.scripts as Record<string, string> | undefined)?.prepare ?? '';
149
- if (prepare.includes('oathbound setup')) return 'skipped';
150
-
151
- const newPrepare = prepare ? `${prepare} && oathbound setup` : 'oathbound setup';
152
- pkg.scripts = { ...(pkg.scripts as Record<string, string> ?? {}), prepare: newPrepare };
153
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
154
- return prepare ? 'appended' : 'added';
155
- }
156
-
157
- // --- Init command ---
158
- async function init(): Promise<void> {
159
- intro(BRAND);
160
-
161
- const enforcement = await select({
162
- message: 'Choose an enforcement level:',
163
- options: [
164
- { value: 'warn', label: 'Warn', hint: 'Report unverified skills but allow them' },
165
- { value: 'registered', label: 'Registered', hint: 'Block unregistered skills' },
166
- { value: 'audited', label: 'Audited', hint: 'Block skills without a passed audit' },
167
- ],
168
- });
169
-
170
- if (isCancel(enforcement)) {
171
- cancel('Setup cancelled.');
172
- process.exit(0);
173
- }
174
-
175
- const level = enforcement as EnforcementLevel;
176
-
177
- // Install as devDependency
178
- let installResult = installDevDependency();
179
-
180
- if (installResult === 'no-package-json') {
181
- const shouldCreate = await confirm({
182
- message: 'No package.json found. Create a minimal one?',
183
- });
184
-
185
- if (isCancel(shouldCreate) || !shouldCreate) {
186
- cancel('Please run `npx oathbound init` inside of the folder where you want to run Claude Code. Oathbound currently needs an NPM package in order to run.');
187
- process.exit(1);
188
- }
189
-
190
- const dirName = basename(process.cwd())
191
- .toLowerCase()
192
- .replace(/[^a-z0-9._-]/g, '-')
193
- .replace(/^[._]+/, '')
194
- .replace(/-+/g, '-')
195
- || 'project';
196
- writeFileSync(
197
- join(process.cwd(), 'package.json'),
198
- JSON.stringify({
199
- name: dirName,
200
- private: true,
201
- scripts: { prepare: 'oathbound setup' },
202
- }, null, 2) + '\n',
203
- );
204
- process.stderr.write(`${GREEN} ✓ Created package.json${RESET}\n`);
205
- installResult = installDevDependency();
206
- }
207
-
208
- switch (installResult) {
209
- case 'installed':
210
- process.stderr.write(`${GREEN} ✓ Added oathbound to devDependencies${RESET}\n`);
211
- break;
212
- case 'skipped':
213
- process.stderr.write(`${DIM} oathbound already in dependencies — skipped${RESET}\n`);
214
- break;
215
- case 'failed':
216
- process.stderr.write(`${YELLOW} ⚠ Failed to add oathbound to devDependencies — install manually${RESET}\n`);
217
- break;
218
- case 'no-package-json':
219
- process.stderr.write(`${RED} ✗ package.json was created but could not be found — something went wrong${RESET}\n`);
220
- process.exit(1);
221
- }
222
-
223
- // For bun/pnpm: add trustedDependencies so postinstall runs
224
- const pm = detectPackageManager();
225
- if (pm === 'bun' || pm === 'pnpm') {
226
- const trustResult = addTrustedDependency();
227
- if (trustResult === 'added') {
228
- process.stderr.write(`${GREEN} ✓ Added oathbound to trustedDependencies (required by ${pm})${RESET}\n`);
229
- }
230
- }
231
-
232
- // Add prepare script to package.json
233
- const prepareResult = addPrepareScript();
234
- if (prepareResult === 'added' || prepareResult === 'appended') {
235
- process.stderr.write(`${GREEN} ✓ Added prepare hook to package.json${RESET}\n`);
236
- }
237
-
238
- // Write .oathbound.jsonc
239
- const configWritten = writeOathboundConfig(level);
240
- if (configWritten) {
241
- process.stderr.write(`${GREEN} ✓ Created .oathbound.jsonc${RESET}\n`);
242
- } else {
243
- process.stderr.write(`${DIM} .oathbound.jsonc already exists — skipped${RESET}\n`);
244
- }
245
-
246
- // Merge hooks into .claude/settings.json
247
- const mergeResult = mergeClaudeSettings();
248
- switch (mergeResult) {
249
- case 'created':
250
- process.stderr.write(`${GREEN} ✓ Created .claude/settings.json with hooks${RESET}\n`);
251
- break;
252
- case 'merged':
253
- process.stderr.write(`${GREEN} ✓ Added hooks to .claude/settings.json${RESET}\n`);
254
- break;
255
- case 'skipped':
256
- process.stderr.write(`${DIM} .claude/settings.json already has oathbound hooks — skipped${RESET}\n`);
257
- break;
258
- case 'malformed':
259
- process.stderr.write(`${RED} ✗ .claude/settings.json is malformed JSON — skipped${RESET}\n`);
260
- process.stderr.write(`${RED} Please fix the file manually and re-run oathbound init${RESET}\n`);
261
- break;
262
- }
263
-
264
- outro(`🎉 Oath Bound set up complete!`);
265
- }
266
-
267
- // --- Pull command ---
268
- async function pull(skillArg: string): Promise<void> {
269
- const parsed = parseSkillArg(skillArg);
270
- if (!parsed) usage();
271
- const { namespace, name, version } = parsed;
272
- const fullName = `${namespace}/${name}`;
273
-
274
- console.log(`\n${BRAND} ${TEAL}↓ Pulling ${fullName}${version ? `@${version}` : ''}...${RESET}`);
275
-
276
- const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
277
-
278
- // 1. Query for the skill
279
- let skill: SkillRow;
280
-
281
- if (version !== null) {
282
- const { data, error } = await supabase
283
- .from('skills')
284
- .select('id, name, namespace, version, tar_hash, storage_path')
285
- .eq('namespace', namespace)
286
- .eq('name', name)
287
- .eq('version', version)
288
- .single<SkillRow>();
289
-
290
- if (error || !data) {
291
- fail(`Skill not found: ${fullName}@${version}`);
292
- }
293
- skill = data;
294
- } else {
295
- // Fetch all versions, pick highest via semver comparison
296
- const { data, error } = await supabase
297
- .from('skills')
298
- .select('id, name, namespace, version, tar_hash, storage_path')
299
- .eq('namespace', namespace)
300
- .eq('name', name);
301
-
302
- if (error || !data || data.length === 0) {
303
- fail(`Skill not found: ${fullName}`);
304
- }
305
- skill = (data as SkillRow[]).sort((a, b) => compareSemver(a.version, b.version)).at(-1)!;
306
- }
307
-
308
- // 2. Download the tar from storage
309
- const { data: blob, error: downloadError } = await supabase
310
- .storage
311
- .from('skills')
312
- .download(skill.storage_path);
313
-
314
- if (downloadError || !blob) {
315
- fail('Download failed', downloadError?.message ?? 'Unknown storage error');
316
- }
317
-
318
- const buffer = Buffer.from(await blob.arrayBuffer());
319
- const tarFile = join(tmpdir(), `oathbound-${name}-${Date.now()}.tar.gz`);
320
-
321
- // 3. Hash and verify
322
- const verifySpinner = spinner('Verifying...');
323
- const hash = createHash('sha256').update(buffer).digest('hex');
324
- verifySpinner.stop();
325
-
326
- console.log(`${DIM} tar hash: ${hash}${RESET}`);
327
-
328
- if (hash !== skill.tar_hash) {
329
- console.log(`${RED} expected: ${skill.tar_hash}${RESET}`);
330
- fail('Verification failed', `Downloaded file does not match expected hash for ${fullName}`);
331
- }
332
-
333
- // 4. Find target directory and extract
334
- let skillsDir = findSkillsDir();
335
- if (!skillsDir.endsWith('.claude/skills') && !skillsDir.includes('.claude/skills')) {
336
- // findSkillsDir() fell back to cwd — create .claude/skills instead of extracting into project root
337
- skillsDir = join(process.cwd(), '.claude', 'skills');
338
- mkdirSync(skillsDir, { recursive: true });
339
- console.log(`${DIM} Created ${skillsDir}${RESET}`);
340
- }
341
- writeFileSync(tarFile, buffer);
342
- try {
343
- execFileSync('tar', ['-xf', tarFile, '-C', skillsDir], { stdio: 'pipe' });
344
- } catch (e: unknown) {
345
- unlinkSync(tarFile);
346
- const msg = e instanceof Error ? e.message : 'Unknown error';
347
- fail('Extraction failed', msg);
348
- }
349
- unlinkSync(tarFile);
350
-
351
- // 5. Record download (non-fatal)
352
- try {
353
- const trackRes = await fetch(`${API_BASE}/api/downloads`, {
354
- method: 'POST',
355
- headers: { 'Content-Type': 'application/json' },
356
- body: JSON.stringify({ skill_id: skill.id, version: skill.version }),
357
- });
358
- if (!trackRes.ok) {
359
- process.stderr.write(`${DIM} [warn] download tracking failed (${trackRes.status})${RESET}\n`);
360
- }
361
- } catch {
362
- // Network error — non-fatal
363
- }
364
-
365
- // 6. Success
366
- console.log(`${BOLD}${GREEN} ✓ Skill verified${RESET}`);
367
- console.log(`${DIM} ${fullName} v${skill.version}${RESET}`);
368
- console.log(`${DIM} → ${join(skillsDir, name)}${RESET}`);
369
- }
370
-
371
- // --- Agent types ---
372
- interface AgentRow {
373
- id: string;
374
- name: string;
375
- namespace: string;
376
- version: string;
377
- content_hash: string;
378
- storage_path: string;
379
- config: Record<string, unknown> | null;
380
- }
381
-
382
- // --- Agent pull ---
383
- async function agentPull(agentArg: string): Promise<void> {
384
- const parsed = parseSkillArg(agentArg); // Same namespace/name[@version] format
385
- if (!parsed) usage();
386
- const { namespace, name, version } = parsed;
387
- const fullName = `${namespace}/${name}`;
388
-
389
- console.log(`\n${BRAND} ${TEAL}↓ Pulling agent ${fullName}${version ? `@${version}` : ''}...${RESET}`);
390
-
391
- const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
392
-
393
- // Query for the agent
394
- let agent: AgentRow;
395
-
396
- if (version !== null) {
397
- const { data, error } = await supabase
398
- .from('agents')
399
- .select('id, name, namespace, version, content_hash, storage_path, config')
400
- .eq('namespace', namespace)
401
- .eq('name', name)
402
- .eq('version', version)
403
- .single<AgentRow>();
404
-
405
- if (error || !data) {
406
- fail(`Agent not found: ${fullName}@${version}`);
407
- }
408
- agent = data;
409
- } else {
410
- const { data, error } = await supabase
411
- .from('agents')
412
- .select('id, name, namespace, version, content_hash, storage_path, config')
413
- .eq('namespace', namespace)
414
- .eq('name', name);
415
-
416
- if (error || !data || data.length === 0) {
417
- fail(`Agent not found: ${fullName}`);
418
- }
419
- agent = (data as AgentRow[]).sort((a, b) => compareSemver(a.version, b.version)).at(-1)!;
420
- }
421
-
422
- // Download from storage
423
- const { data: blob, error: downloadError } = await supabase
424
- .storage
425
- .from('agents')
426
- .download(agent.storage_path);
427
-
428
- if (downloadError || !blob) {
429
- fail('Download failed', downloadError?.message ?? 'Unknown storage error');
430
- }
431
-
432
- const content = await blob.text();
433
-
434
- // Verify content hash
435
- const verifySpinner = spinner('Verifying...');
436
- const hash = createHash('sha256').update(content).digest('hex');
437
- verifySpinner.stop();
438
-
439
- console.log(`${DIM} content hash: ${hash}${RESET}`);
440
-
441
- if (hash !== agent.content_hash) {
442
- console.log(`${RED} expected: ${agent.content_hash}${RESET}`);
443
- fail('Verification failed', `Downloaded file does not match expected hash for ${fullName}`);
444
- }
445
-
446
- // Validate name has no path traversal characters
447
- if (name.includes('/') || name.includes('\\') || name.includes('..')) {
448
- fail('Invalid agent name', `Name "${name}" contains path traversal characters`);
449
- }
450
-
451
- // Ensure .claude/agents/ directory exists
452
- const agentsDir = join(process.cwd(), '.claude', 'agents');
453
- mkdirSync(agentsDir, { recursive: true });
454
-
455
- // Validate resolved path stays within agentsDir
456
- const targetPath = join(agentsDir, `${name}.md`);
457
- if (!targetPath.startsWith(agentsDir)) {
458
- fail('Invalid agent name', `Resolved path escapes agents directory`);
459
- }
460
-
461
- // Warn and confirm if hooks/mcpServers are present
462
- const config = agent.config;
463
- let hasDangerous = false;
464
- if (config?.hooks) {
465
- console.log(`\n${YELLOW}${BOLD} ⚠ This agent defines hooks (arbitrary command execution):${RESET}`);
466
- console.log(`${DIM}${JSON.stringify(config.hooks, null, 2)}${RESET}\n`);
467
- hasDangerous = true;
468
- }
469
- if (config?.mcpServers) {
470
- console.log(`\n${YELLOW}${BOLD} ⚠ This agent defines MCP servers (external connections):${RESET}`);
471
- console.log(`${DIM}${JSON.stringify(config.mcpServers, null, 2)}${RESET}\n`);
472
- hasDangerous = true;
473
- }
474
- if (hasDangerous) {
475
- const answer = await confirm({
476
- message: 'This agent contains security-sensitive configuration. Install anyway?',
477
- });
478
- if (isCancel(answer) || !answer) {
479
- fail('Aborted', 'Agent not installed');
480
- }
481
- }
482
-
483
- // Write agent file
484
- writeFileSync(targetPath, content);
485
-
486
- // Record download (non-fatal)
487
- try {
488
- const trackRes = await fetch(`${API_BASE}/api/downloads`, {
489
- method: 'POST',
490
- headers: { 'Content-Type': 'application/json' },
491
- body: JSON.stringify({ agent_id: agent.id, version: agent.version }),
492
- });
493
- if (!trackRes.ok) {
494
- process.stderr.write(`${DIM} [warn] download tracking failed (${trackRes.status})${RESET}\n`);
495
- }
496
- } catch {
497
- // Network error — non-fatal
498
- }
499
-
500
- console.log(`${BOLD}${GREEN} ✓ Agent verified${RESET}`);
501
- console.log(`${DIM} ${fullName} v${agent.version}${RESET}`);
502
- console.log(`${DIM} → ${targetPath}${RESET}`);
503
- }
504
-
505
- // --- Agent subcommand router ---
506
- async function handleAgent(agentArgs: string[]): Promise<void> {
507
- const agentSub = agentArgs[0];
508
-
509
- if (!agentSub || agentSub === '--help' || agentSub === '-h') {
510
- agentUsage(agentSub ? 0 : 1);
511
- }
512
-
513
- if (agentSub === 'push') {
514
- const pushArgs = agentArgs.slice(1);
515
- const isPrivate = pushArgs.includes('--private');
516
- const pushPath = pushArgs.find(a => !a.startsWith('--'));
517
- await agentPush(pushPath, { private: isPrivate });
518
- } else if (agentSub === 'pull' || agentSub === 'install' || agentSub === 'i') {
519
- const target = agentArgs[1];
520
- if (!target) {
521
- fail('Missing agent name', 'Usage: oathbound agent pull <namespace/name[@version]>');
522
- }
523
- await agentPull(target);
524
- } else if (agentSub === 'search' || agentSub === 'list' || agentSub === 'ls') {
525
- const searchOpts = parseAgentSearchArgs(agentArgs.slice(1));
526
- await agentSearch(searchOpts);
527
- } else {
528
- agentUsage();
529
- }
530
- }
531
-
532
- // --- Entry ---
533
- if (!import.meta.main) {
534
- // Module imported for testing — skip CLI entry
535
- } else {
536
- const args = Bun.argv.slice(2);
537
- const subcommand = args[0];
538
-
539
- if (subcommand === '--help' || subcommand === '-h') {
540
- usage(0);
541
- }
542
-
543
- // Fire-and-forget auto-update on every command except verify (hooks must be fast)
544
- if (subcommand !== 'verify' && subcommand !== 'setup') {
545
- const updatePromise = checkForUpdate(VERSION).catch(() => {});
546
-
547
- if (subcommand === '--version' || subcommand === '-v') {
548
- // Wait for update check so the user sees the notification
549
- await updatePromise;
550
- console.log(`oathbound ${VERSION}`);
551
- process.exit(0);
552
- }
553
- }
554
-
555
- if (subcommand === 'init') {
556
- init().catch((err: unknown) => {
557
- const msg = err instanceof Error ? err.message : 'Unknown error';
558
- fail('Init failed', msg);
559
- });
560
- } else if (subcommand === 'setup') {
561
- setup();
562
- } else if (subcommand === 'verify') {
563
- const isCheck = args.includes('--check');
564
- const run = isCheck ? verifyCheck : () => verify(SUPABASE_URL, SUPABASE_ANON_KEY);
565
- run().catch((err: unknown) => {
566
- const msg = err instanceof Error ? err.message : 'Unknown error';
567
- process.stderr.write(`oathbound verify: ${msg}\n`);
568
- process.exit(1);
569
- });
570
- } else if (subcommand === 'login') {
571
- login().catch((err: unknown) => {
572
- const msg = err instanceof Error ? err.message : 'Unknown error';
573
- fail('Login failed', msg);
574
- });
575
- } else if (subcommand === 'logout') {
576
- logout().catch((err: unknown) => {
577
- const msg = err instanceof Error ? err.message : 'Unknown error';
578
- fail('Logout failed', msg);
579
- });
580
- } else if (subcommand === 'whoami') {
581
- whoami().catch((err: unknown) => {
582
- const msg = err instanceof Error ? err.message : 'Unknown error';
583
- fail('Failed', msg);
584
- });
585
- } else if (subcommand === 'push') {
586
- const pushArgs = args.slice(1);
587
- const isPrivate = pushArgs.includes('--private');
588
- const pushPath = pushArgs.find(a => !a.startsWith('--'));
589
- push(pushPath, { private: isPrivate }).catch((err: unknown) => {
590
- const msg = err instanceof Error ? err.message : 'Unknown error';
591
- fail('Push failed', msg);
592
- });
593
- } else if (subcommand === 'search' || subcommand === 'list' || subcommand === 'ls') {
594
- const searchOpts = parseSearchArgs(args.slice(1));
595
- search(searchOpts).catch((err: unknown) => {
596
- const msg = err instanceof Error ? err.message : 'Unknown error';
597
- fail('Search failed', msg);
598
- });
599
- } else if (subcommand === 'agent') {
600
- handleAgent(args.slice(1)).catch((err: unknown) => {
601
- const msg = err instanceof Error ? err.message : 'Unknown error';
602
- fail('Agent command failed', msg);
603
- });
604
- } else {
605
- const PULL_ALIASES = new Set(['pull', 'i', 'install']);
606
- const skillArg = args[1];
607
-
608
- if (!subcommand || !PULL_ALIASES.has(subcommand) || !skillArg) {
609
- usage();
610
- }
611
-
612
- pull(skillArg).catch((err: unknown) => {
613
- const msg = err instanceof Error ? err.message : 'Unknown error';
614
- fail('Unexpected error', msg);
615
- });
616
- }
617
- } // end if (import.meta.main)