From 4e5ee40f6721a5871396af4f61789a1a7f4ceb8d Mon Sep 17 00:00:00 2001 From: nisargpatel7042lva Date: Mon, 21 Sep 2026 17:24:22 +0300 Subject: [PATCH] feat(add): accept bare skill names without --skill Treat identifiers like overview as skills so npx cvmi add server-dev works. Git sources still use owner/repo, URLs, or paths. --- README.md | 4 ++-- src/add.test.ts | 62 ++++++++++++++++++++++++++++++++++++------------- src/add.ts | 13 ++++++++++- src/cli.ts | 2 +- 4 files changed, 61 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index cd348da78..de8d9e37e 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ npx cvmi add # Install a specific skill from the ContextVM repository -npx cvmi add --skill overview +npx cvmi add overview ``` ## Roadmap - [x] `cvmi add` - Install skills with interactive picker -- [x] `cvmi add --skill ` - Install specific skills +- [x] `cvmi add ` - Install specific skills - [x] `cvmi serve` - Expose a server (gateway) - [x] `cvmi use` - Use a server from nostr as stdio (proxy) - [x] `cvmi discover` - Discover announced servers on relays diff --git a/src/add.test.ts b/src/add.test.ts index 03a315ef9..b4dda355f 100644 --- a/src/add.test.ts +++ b/src/add.test.ts @@ -313,58 +313,88 @@ describe('shouldInstallInternalSkills', () => { describe('parseAddOptions', () => { it('should parse --all flag', () => { - const result = parseAddOptions(['source', '--all']); - expect(result.source).toEqual(['source']); + const result = parseAddOptions(['owner/repo', '--all']); + expect(result.source).toEqual(['owner/repo']); expect(result.options.all).toBe(true); }); it('should parse --skill with wildcard', () => { - const result = parseAddOptions(['source', '--skill', '*']); - expect(result.source).toEqual(['source']); + const result = parseAddOptions(['owner/repo', '--skill', '*']); + expect(result.source).toEqual(['owner/repo']); expect(result.options.skill).toEqual(['*']); }); it('should parse --agent with wildcard', () => { - const result = parseAddOptions(['source', '--agent', '*']); - expect(result.source).toEqual(['source']); + const result = parseAddOptions(['owner/repo', '--agent', '*']); + expect(result.source).toEqual(['owner/repo']); expect(result.options.agent).toEqual(['*']); }); it('should parse --skill wildcard with specific agents', () => { - const result = parseAddOptions(['source', '--skill', '*', '--agent', 'claude-code']); - expect(result.source).toEqual(['source']); + const result = parseAddOptions(['owner/repo', '--skill', '*', '--agent', 'claude-code']); + expect(result.source).toEqual(['owner/repo']); expect(result.options.skill).toEqual(['*']); expect(result.options.agent).toEqual(['claude-code']); }); it('should parse --agent wildcard with specific skills', () => { - const result = parseAddOptions(['source', '--agent', '*', '--skill', 'my-skill']); - expect(result.source).toEqual(['source']); + const result = parseAddOptions(['owner/repo', '--agent', '*', '--skill', 'my-skill']); + expect(result.source).toEqual(['owner/repo']); expect(result.options.agent).toEqual(['*']); expect(result.options.skill).toEqual(['my-skill']); }); it('should parse combined flags with wildcards', () => { - const result = parseAddOptions(['source', '-g', '--skill', '*', '-y']); - expect(result.source).toEqual(['source']); + const result = parseAddOptions(['owner/repo', '-g', '--skill', '*', '-y']); + expect(result.source).toEqual(['owner/repo']); expect(result.options.global).toBe(true); expect(result.options.skill).toEqual(['*']); expect(result.options.yes).toBe(true); }); it('should parse --full-depth flag', () => { - const result = parseAddOptions(['source', '--full-depth']); - expect(result.source).toEqual(['source']); + const result = parseAddOptions(['owner/repo', '--full-depth']); + expect(result.source).toEqual(['owner/repo']); expect(result.options.fullDepth).toBe(true); }); it('should parse --full-depth with other flags', () => { - const result = parseAddOptions(['source', '--full-depth', '--list', '-g']); - expect(result.source).toEqual(['source']); + const result = parseAddOptions(['owner/repo', '--full-depth', '--list', '-g']); + expect(result.source).toEqual(['owner/repo']); expect(result.options.fullDepth).toBe(true); expect(result.options.list).toBe(true); expect(result.options.global).toBe(true); }); + + it('should treat a bare skill name as --skill, not a git source', () => { + const result = parseAddOptions(['overview']); + expect(result.source).toEqual([]); + expect(result.options.skill).toEqual(['overview']); + }); + + it('should treat multiple bare skill names as --skill values', () => { + const result = parseAddOptions(['overview', 'server-dev', 'payments']); + expect(result.source).toEqual([]); + expect(result.options.skill).toEqual(['overview', 'server-dev', 'payments']); + }); + + it('should still accept --skill after promoting a bare name', () => { + const result = parseAddOptions(['overview', '--skill', 'payments']); + expect(result.source).toEqual([]); + expect(result.options.skill).toEqual(['payments', 'overview']); + }); + + it('should keep owner/repo tokens as a source', () => { + const result = parseAddOptions(['contextvm/cvmi']); + expect(result.source).toEqual(['contextvm/cvmi']); + expect(result.options.skill).toBeUndefined(); + }); + + it('should keep local paths as a source', () => { + const result = parseAddOptions(['./skills']); + expect(result.source).toEqual(['./skills']); + expect(result.options.skill).toBeUndefined(); + }); }); describe('find-skills prompt with -y flag', () => { diff --git a/src/add.ts b/src/add.ts index 65fd71d69..9f7779fad 100644 --- a/src/add.ts +++ b/src/add.ts @@ -1526,7 +1526,7 @@ export async function runAdd( console.log(); } - p.outro('Use --skill to install specific skills'); + p.outro('Use cvmi add to install specific skills'); await cleanup(tempDir); process.exit(0); } @@ -2074,5 +2074,16 @@ export function parseAddOptions(args: string[]): { source: string[]; options: Ad } } + // Bare identifiers are skill names (`cvmi add overview`), not git sources. + // Real sources use a slash, URL, or path (`owner/repo`, `./skills`, `https://...`). + if (source.length > 0 && source.every(isBareSkillName)) { + options.skill = [...(options.skill ?? []), ...source]; + source.length = 0; + } + return { source, options }; } + +function isBareSkillName(token: string): boolean { + return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(token); +} diff --git a/src/cli.ts b/src/cli.ts index 5879af0bd..dbc1ae2b4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -116,7 +116,7 @@ ${BOLD}Quick start:${RESET} ${BOLD}Examples:${RESET} ${DIM}$${RESET} cvmi add ${DIM}# install embedded ContextVM skills${RESET} - ${DIM}$${RESET} cvmi add --skill overview ${DIM}# install a specific skill${RESET} + ${DIM}$${RESET} cvmi add overview ${DIM}# install a specific skill${RESET} ${DIM}$${RESET} cvmi remove ${DIM}# remove an installed skill${RESET} ${DIM}$${RESET} cvmi serve -- ${DIM}# start gateway, expose an already existing server (stdio or http) over nostr${RESET} ${DIM}$${RESET} cvmi use ${DIM}# connect to remote MCP server, expose it as stdio${RESET}