Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` - Install specific skills
- [x] `cvmi add <skill>` - 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
Expand Down
62 changes: 46 additions & 16 deletions src/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
13 changes: 12 additions & 1 deletion src/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1526,7 +1526,7 @@ export async function runAdd(
console.log();
}

p.outro('Use --skill <name> to install specific skills');
p.outro('Use cvmi add <name> to install specific skills');
await cleanup(tempDir);
process.exit(0);
}
Expand Down Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <skill> ${DIM}# remove an installed skill${RESET}
${DIM}$${RESET} cvmi serve -- <command-or-url> ${DIM}# start gateway, expose an already existing server (stdio or http) over nostr${RESET}
${DIM}$${RESET} cvmi use <server-pubkey> ${DIM}# connect to remote MCP server, expose it as stdio${RESET}
Expand Down
Loading