Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ src/
adapters/ # one file per supported CLI
types.ts # CliAdapter interface all adapters implement
helpers.ts # shared dir-scanning / enable-disable helpers
claude.ts, antigravity.ts, codex.ts, aider.ts, amp.ts, factory.ts
claude.ts, antigravity.ts, codex.ts, copilot.ts, aider.ts, amp.ts, factory.ts
index.ts # adapter registry (getAdapter, getAllAdapters)
scanner.ts # skill discovery
disable.ts # enable/disable logic
Expand Down
32 changes: 26 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![node](https://img.shields.io/node/v/@mingchuno/skillswitch?style=flat-square&color=F3F2EE&labelColor=0B0B0D)](package.json)
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-F3F2EE?style=flat-square&labelColor=0B0B0D)](CONTRIBUTING.md)

Manage AI CLI skills across Claude Code, Antigravity CLI, Codex CLI, Aider, Amp, and Factory Droid — profiles, disable/enable, catalog generation.
Manage AI CLI skills across Claude Code, Antigravity CLI, Codex CLI, GitHub Copilot CLI, Aider, Amp, and Factory Droid — profiles, disable/enable, catalog generation.

## Quick start

Expand All @@ -24,7 +24,7 @@ Running 100+ Claude Code skills? Your context window is leaking. Claude Code inj

## What is Skillswitch?

**Skillswitch is a command-line skill manager for AI coding CLIs** — Claude Code, Antigravity CLI, Codex CLI, Aider, Amp, and Factory Droid. It disables the skills you're not using so they stop consuming your context window, and lets you snapshot enable/disable sets as named profiles you switch between per project.
**Skillswitch is a command-line skill manager for AI coding CLIs** — Claude Code, Antigravity CLI, Codex CLI, GitHub Copilot CLI, Aider, Amp, and Factory Droid. It disables the skills you're not using so they stop consuming your context window, and lets you snapshot enable/disable sets as named profiles you switch between per project.

### Why Skillswitch

Expand All @@ -34,7 +34,7 @@ Claude Code injects the name and description of every installed skill into every

- **vs. deleting skills manually** — deleting is destructive and you lose the skill. Skillswitch moves skills to `.disabled/` and restores them on `enable`. Nothing is lost.
- **vs. editing config by hand** — hand-editing `blocklist.json` or moving files works until you want a *different* set per project. Skillswitch stores profiles and switches the whole set in one command with a `--dry-run` preview.
- **vs. one-CLI tools** — most helpers target Claude Code only. Skillswitch uses the same commands across six CLIs via `--for <cli>`.
- **vs. one-CLI tools** — most helpers target Claude Code only. Skillswitch uses the same commands across seven CLIs via `--for <cli>`.

### When to use Skillswitch

Expand All @@ -49,9 +49,9 @@ Claude Code injects the name and description of every installed skill into every

**Does it send my data anywhere?** No telemetry, no auth, no network. It only reads and updates local files.

**Which CLIs does it support?** Claude Code, Antigravity CLI, Codex CLI, Aider, Amp (Sourcegraph), and Factory Droid. Default target is Claude Code; use `--for <cli>` for the rest.
**Which CLIs does it support?** Claude Code, Antigravity CLI, Codex CLI, GitHub Copilot CLI, Aider, Amp (Sourcegraph), and Factory Droid. Default target is Claude Code; use `--for <cli>` for the rest.

**Profiles support Claude Code and Codex app / CLI.** Catalog and audit support Claude Code only. List, search, status, disable and enable support all listed targets.
**Profiles support Claude Code, Codex app / CLI and GitHub Copilot CLI.** Catalog and audit support Claude Code only. List, search, status, disable and enable support all listed targets.

**How do profiles work?** `skillswitch profile create dev` snapshots your current enabled set. `skillswitch profile use dev` activates it. `profile diff` and `--dry-run` show what changes before it happens.

Expand All @@ -72,6 +72,7 @@ pnpm dlx @mingchuno/skillswitch <command>
| Claude Code | `--for claude` (default) | `~/.claude/skills/` + plugins |
| Antigravity CLI | `--for agy` | `~/.gemini/config/skills/` + `~/.gemini/antigravity-cli/skills/` |
| Codex app / CLI | `--for codex` | `~/.agents/skills/` |
| GitHub Copilot CLI | `--for copilot` | `~/.copilot/skills/` |
| Aider | `--for aider` | `~/.aider/skills/` (managed externally) |
| Amp (Sourcegraph) | `--for amp` | `~/.config/amp/skills/` + `~/.agents/skills/` |
| Factory Droid | `--for droid` | `~/.factory/droids/` + `~/.factory/commands/` |
Expand Down Expand Up @@ -111,6 +112,7 @@ skillswitch audit # find duplicates and stale disabled s
# Other CLIs — use --for <cli>
skillswitch --for agy list
skillswitch --for codex disable tdd
skillswitch --for copilot disable tdd
skillswitch --for amp search coding
skillswitch --for droid status
skillswitch --for aider list
Expand Down Expand Up @@ -239,13 +241,31 @@ Skills parked by older versions in `~/.agents/skills/.disabled/` are still liste
as disabled. Restore those folders to `~/.agents/skills/` before enabling them;
Codex operations no longer move folders.

## Copilot profiles

```bash
skillswitch --for copilot profile create full
skillswitch --for copilot disable research --exact
skillswitch --for copilot profile create dev
skillswitch --for copilot profile use full --dry-run
skillswitch --for copilot profile use full
# Run /skills reload in an open Copilot CLI session, or start a new one.
```

Copilot profiles manage personal skill folders in `~/.copilot/skills` by moving
them to `~/.copilot/skills/.disabled/`. Copilot also reads `~/.agents/skills`,
but that root is shared with Codex and Amp, so skillswitch leaves it alone:
those skills are not listed, toggled or saved in Copilot profiles. Repository
(`.github/skills`) and plugin skills are also outside this scope. Every move is
checked before any folder changes; missing profile skills prevent activation.

## How it works

- **Standalone skills** (`~/.claude/skills/*.md`): disabled by moving to `.disabled/` subdirectory — always reversible, nothing deleted
- **Plugin skills** (`~/.claude/plugins/`): disabled by writing to Claude Code's native `blocklist.json`
- **Codex**: path-specific `[[skills.config]]` entries in `~/.codex/config.toml`; shared skill folders stay in place
- **Other CLIs**: skill files moved to `.disabled/` subdirectory inside their skills directory
- **Profiles** stored independently in `~/.claude/skillctl/profiles.json` and `$CODEX_HOME/skillctl/profiles.json` (default `~/.codex/skillctl/profiles.json`)
- **Profiles** stored independently in `~/.claude/skillctl/profiles.json`, `$CODEX_HOME/skillctl/profiles.json` (default `~/.codex/skillctl/profiles.json`) and `~/.copilot/skillctl/profiles.json`

Moves reject existing destinations, invalid names and symlinked `.disabled/`
directories. Existing copies remain untouched. Corrupt or invalid profile and
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@mingchuno/skillswitch",
"version": "0.5.0",
"description": "Manage AI CLI skills across Claude Code, Antigravity CLI, Codex CLI, Aider, Amp, and Factory Droid — profiles, disable/enable, catalog generation",
"description": "Manage AI CLI skills across Claude Code, Antigravity CLI, Codex CLI, GitHub Copilot CLI, Aider, Amp, and Factory Droid — profiles, disable/enable, catalog generation",
"type": "module",
"bin": {
"skillswitch": "./dist/cli.js"
Expand Down Expand Up @@ -48,6 +48,7 @@
"antigravity",
"agy",
"codex",
"copilot",
"aider",
"amp",
"skills",
Expand Down
61 changes: 61 additions & 0 deletions src/adapters/copilot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// src/adapters/copilot.ts — GitHub Copilot CLI adapter
// Skills: ~/.copilot/skills/<name>/SKILL.md (personal skills)
// Copilot also reads ~/.agents/skills, which Codex and Amp share; that root is left untouched.
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { assertMoveAllowed } from '../moves.js';
import type { ProfileTarget } from '../profile-target.js';
import { skillProfiles } from '../skill-profiles.js';
import { disableDirSkill, enableDirSkill, scanDirSkills } from './helpers.js';
import type { AdapterSkill, CliAdapter } from './types.js';

export class CopilotAdapter implements CliAdapter {
readonly cliName = 'copilot';
readonly displayName = 'GitHub Copilot CLI';
readonly skillsDirs: string[];
readonly profiles: ProfileTarget;

constructor(readonly configDir = path.join(os.homedir(), '.copilot')) {
this.skillsDirs = [path.join(configDir, 'skills')];
this.profiles = skillProfiles({
label: 'Copilot',
directory: configDir,
scope:
'Personal skills in ~/.copilot/skills only. Shared ~/.agents/skills, repo and plugin skills are outside this profile.',
scanSkills: () => this.scanSkills(),
validateSkillChanges: (changes) => this.validateSkillChanges(changes),
setSkillsEnabled: (changes) => this.setSkillsEnabled(changes),
});
}

isInstalled(): boolean {
return fs.existsSync(this.configDir);
}

scanSkills(): AdapterSkill[] {
return scanDirSkills(this.skillsDirs[0]);
}

validateSkillChanges(changes: ReadonlyMap<string, boolean>): void {
for (const [name, enable] of changes) {
assertMoveAllowed(name, this.skillsDirs[0], enable, true);
}
}

setSkillsEnabled(changes: ReadonlyMap<string, boolean>): void {
this.validateSkillChanges(changes);
for (const [name, enable] of changes) {
if (enable) this.enableSkill(name);
else this.disableSkill(name);
}
}

disableSkill(name: string): void {
disableDirSkill(name, this.skillsDirs[0]);
}

enableSkill(name: string): void {
enableDirSkill(name, this.skillsDirs[0]);
}
}
2 changes: 2 additions & 0 deletions src/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AmpAdapter } from './amp.js';
import { AntigravityAdapter } from './antigravity.js';
import { ClaudeAdapter } from './claude.js';
import { CodexAdapter } from './codex.js';
import { CopilotAdapter } from './copilot.js';
import { FactoryAdapter } from './factory.js';
import type { CliAdapter } from './types.js';

Expand All @@ -17,6 +18,7 @@ const ADAPTERS: Record<string, (claudeDir?: string) => CliAdapter> = {
claude: (claudeDir) => new ClaudeAdapter(claudeDir ?? path.join(os.homedir(), '.claude')),
agy: () => new AntigravityAdapter(),
codex: () => new CodexAdapter(),
copilot: () => new CopilotAdapter(),
droid: () => new FactoryAdapter(),
amp: () => new AmpAdapter(),
aider: () => new AiderAdapter(),
Expand Down
2 changes: 1 addition & 1 deletion src/cli/inspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
const { duplicates, disabledStandalone, disabledPlugins } = report;

if (opts.json) {
terminal.write(JSON.stringify(report, null, 2) + '\n');

Check notice on line 34 in src/cli/inspection.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.

Check notice on line 34 in src/cli/inspection.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.
return;
}

Expand Down Expand Up @@ -70,7 +70,7 @@
installed: a.isInstalled(),
}));
if (opts.json) {
terminal.write(JSON.stringify(results, null, 2) + '\n');

Check notice on line 73 in src/cli/inspection.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.

Check notice on line 73 in src/cli/inspection.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.
return;
}
terminal.log('\nDetected CLIs:');
Expand All @@ -86,7 +86,7 @@
function requireClaude(context: CommandContext): void {
if (context.target === 'claude') return;
terminal.writeError(
'Error: catalog and audit support only Claude; profiles support Claude and Codex.\n',
'Error: catalog and audit support only Claude; profiles support Claude, Codex and Copilot.\n',
);
terminal.exit(1);
}
Expand Down
2 changes: 2 additions & 0 deletions src/cli/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@
terminal.log(`Activated "${name}".`);
if (context.target === 'codex')
terminal.log('Restart Codex app / CLI to apply configuration changes.');
if (context.target === 'copilot')
terminal.log('Run /skills reload in Copilot CLI to apply changes.');
if (result.enabled.length) terminal.log(` Enabled : ${result.enabled.join(', ')}`);
if (result.disabled.length) terminal.log(` Disabled: ${result.disabled.join(', ')}`);
if (result.pluginsBlocked.length)
Expand All @@ -103,11 +105,11 @@
if (!opts.json) showScope(context);
if (opts.json) {
terminal.write(
JSON.stringify(
{ active: store.active, previous: store.previous, profiles: store.profiles },
null,
2,
) + '\n',

Check notice on line 112 in src/cli/profiles.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.

Check notice on line 112 in src/cli/profiles.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.
);
return;
}
Expand Down Expand Up @@ -210,7 +212,7 @@
files.writeText(opts.out, json);
terminal.log(`Profile "${name}" exported to ${opts.out}`);
} else {
terminal.write(json + '\n');

Check notice on line 215 in src/cli/profiles.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.

Check notice on line 215 in src/cli/profiles.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.
}
} catch (err: unknown) {
terminal.error(`Error: ${(err as Error).message}`);
Expand Down
3 changes: 3 additions & 0 deletions src/cli/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
let skills = adapter.scanSkills();
if (statusFilter) skills = skills.filter((s) => s.status === statusFilter);
if (opts.json) {
terminal.write(JSON.stringify(skills, null, 2) + '\n');

Check notice on line 28 in src/cli/skills.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.

Check notice on line 28 in src/cli/skills.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.
return;
}
terminal.log(`\n${adapter.displayName} skills (${skills.length}):`);
Expand All @@ -46,7 +46,7 @@
const pp = filter
? plugins.filter((p) => p.status === (filter === 'active' ? 'active' : 'disabled'))
: plugins;
terminal.write(JSON.stringify({ standalone: ss, plugins: pp }, null, 2) + '\n');

Check notice on line 49 in src/cli/skills.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.

Check notice on line 49 in src/cli/skills.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.
return;
}

Expand All @@ -71,7 +71,7 @@
const adapter = adapters.get(cliTarget);
const matches = searchSkills(adapter.scanSkills(), query, opts.status);
if (opts.json) {
terminal.write(JSON.stringify(matches, null, 2) + '\n');

Check notice on line 74 in src/cli/skills.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.

Check notice on line 74 in src/cli/skills.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.
return;
}
if (!matches.length) {
Expand All @@ -95,7 +95,7 @@
opts.status,
);
if (opts.json) {
terminal.write(JSON.stringify(matches, null, 2) + '\n');

Check notice on line 98 in src/cli/skills.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.

Check notice on line 98 in src/cli/skills.ts

View workflow job for this annotation

GitHub Actions / verify

lint/style/useTemplate

Template literals are preferred over string concatenation.
return;
}
if (!matches.length) {
Expand Down Expand Up @@ -207,6 +207,9 @@
if (context.target === 'codex' && !opts.dryRun && !opts.quiet) {
terminal.log('Restart Codex app / CLI to apply configuration changes.');
}
if (context.target === 'copilot' && !opts.dryRun && !opts.quiet) {
terminal.log('Run /skills reload in Copilot CLI to apply changes.');
}
}

function matchingPlugins(context: CommandContext, name: string, opts: MutationOptions) {
Expand Down
88 changes: 9 additions & 79 deletions src/codex-profiles.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,16 @@
import type { CodexAdapter } from './adapters/codex.js';
import type { ProfileTarget } from './profile-target.js';
import { importProfile, readProfileStore, recordProfileActivation } from './profiles.js';

function skillChanges(diff: { toEnable: string[]; toDisable: string[] }): Map<string, boolean> {
return new Map([
...diff.toEnable.map((skill) => [skill, true] as const),
...diff.toDisable.map((skill) => [skill, false] as const),
]);
}
import { skillProfiles } from './skill-profiles.js';

export function codexProfiles(adapter: CodexAdapter): ProfileTarget {
const directory = adapter.configDir;
function getProfile(name: string) {
const profile = readProfileStore(directory).profiles[name];
if (!profile) throw new Error(`Profile "${name}" does not exist`);
if (profile.plugins.length) throw new Error('Codex profiles do not support plugins');
return profile;
}

function validate(name: string) {
const profile = getProfile(name);
const inventory = new Set(adapter.scanSkills().map((skill) => skill.name));
return {
validSkills: profile.skills.filter((skill) => inventory.has(skill)),
ghostSkills: profile.skills.filter((skill) => !inventory.has(skill)),
};
}

function diff(name: string) {
const profile = getProfile(name);
const selected = new Set(profile.skills);
const inventory = adapter.scanSkills();
adapter.assertProfileSelection(selected);
const missing = profile.skills.filter(
(skill) => !inventory.some((entry) => entry.name === skill),
);
if (missing.length) throw new Error(`Profile has missing skills: ${missing.join(', ')}`);
const changes = {
toEnable: inventory
.filter((skill) => selected.has(skill.name) && skill.status === 'disabled')
.map((skill) => skill.name),
toDisable: inventory
.filter((skill) => !selected.has(skill.name) && skill.status === 'active')
.map((skill) => skill.name),
toBlock: [],
toUnblock: [],
};
adapter.validateSkillChanges(skillChanges(changes));
return changes;
}

return {
directory,
return skillProfiles({
label: 'Codex',
directory: adapter.configDir,
scope:
'User skills in ~/.agents/skills only; applies to Codex app and CLI. Repo, plugin and system skills are outside this profile.',
snapshot: () => ({
skills: adapter
.scanSkills()
.filter((skill) => skill.status === 'active')
.map((skill) => skill.name),
plugins: [],
}),
diff,
validate,
async activate(name) {
const changes = diff(name);
adapter.setSkillsEnabled(skillChanges(changes));
recordProfileActivation(name, directory);
return { enabled: changes.toEnable, disabled: changes.toDisable, pluginsBlocked: [] };
},
import(json) {
const profile: unknown = JSON.parse(json);
if (
profile !== null &&
typeof profile === 'object' &&
'plugins' in profile &&
Array.isArray(profile.plugins) &&
profile.plugins.length
) {
throw new Error('Codex profiles do not support plugins');
}
return importProfile(json, directory);
},
};
scanSkills: () => adapter.scanSkills(),
assertProfileSelection: (selected) => adapter.assertProfileSelection(selected),
validateSkillChanges: (changes) => adapter.validateSkillChanges(changes),
setSkillsEnabled: (changes) => adapter.setSkillsEnabled(changes),
});
}
5 changes: 3 additions & 2 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function createProgram(handlers: CommandHandlers, config: CommandConfig):
program
.name('skillswitch')
.description(
`Manage AI CLI skills — Claude Code, Antigravity CLI, Codex CLI, Factory Droid, Amp, Aider`,
`Manage AI CLI skills — Claude Code, Antigravity CLI, Codex CLI, GitHub Copilot CLI, Factory Droid, Amp, Aider`,
)
.version('0.5.0') // x-release-please-version
.option('--claude-dir <path>', 'Override the Claude config directory (default: ~/.claude)')
Expand All @@ -33,7 +33,8 @@ export function createProgram(handlers: CommandHandlers, config: CommandConfig):
if (
actionCommand.name() === 'catalog' ||
actionCommand.name() === 'audit' ||
(actionCommand.parent?.name() === 'profile' && context().target !== 'codex')
(actionCommand.parent?.name() === 'profile' &&
!['codex', 'copilot'].includes(context().target))
) {
handlers.inspection.requireClaude(context());
}
Expand Down
2 changes: 1 addition & 1 deletion src/profile-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface ProfileTarget {
export function profileTarget(adapter: CliAdapter, claudeDir: string): ProfileTarget {
if (adapter.profiles) return adapter.profiles;
if (adapter.cliName !== 'claude')
throw new Error('Profiles currently support only Claude and Codex.');
throw new Error('Profiles currently support only Claude, Codex and Copilot.');
return {
directory: claudeDir,
snapshot: () => ({
Expand Down
Loading
Loading