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
77 changes: 76 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,41 @@ Installed skill philips-hue to ~/.claude/skills/philips-hue

Even better, once this convention becomes commonplace, agents will by default do all these before they even run the tool, so when you ask it to "install hue-cli", it will know to run `--skill list` the same way a human would run `--help` after downloading a program, and install the necessary skills themselves without being asked to.

## Quick setup — add skillflag to your CLI

Copy the prompt below and paste it into your coding agent. It will add skillflag support to your project.

Currently TypeScript/Node only. [Open an issue](https://github.com/osolmaz/skillflag/issues) if you'd like support for another language.

```text
Add skillflag to this project so the CLI can bundle and expose agent skills.

1. Install the skillflag library:
npm install skillflag

2. Create a skill directory at skills/<skill-id>/SKILL.md with a YAML
frontmatter (name, description) and markdown instructions for the agent.

3. In the CLI entrypoint, intercept --skill and delegate to skillflag:

import { findSkillsRoot, maybeHandleSkillflag } from "skillflag";

await maybeHandleSkillflag(process.argv, {
skillsRoot: findSkillsRoot(import.meta.url),
});

4. Verify it works:
<tool> --skill list
<tool> --skill show <id>
<tool> --skill export <id> | npx skillflag install

5. For the full integration guide:
https://raw.githubusercontent.com/osolmaz/skillflag/main/docs/INTEGRATION.md

6. For the skillflag specification:
https://raw.githubusercontent.com/osolmaz/skillflag/main/docs/SKILLFLAG_SPEC.md
```

## Install (optional)

```bash
Expand All @@ -56,14 +91,36 @@ Any CLI that implements the skillflag convention can be used like this:
```bash
# list skills the tool can export
<tool> --skill list
# show a single skills metadata
# show a single skill's metadata
<tool> --skill show <id>
# install into Codex user skills
<tool> --skill export <id> | npx skillflag install --agent codex
# install into Claude project skills
<tool> --skill export <id> | npx skillflag install --agent claude --scope repo
```

### Interactive mode

When `--agent` or `--scope` is omitted and a TTY is available, `skill-install` launches an interactive wizard:

```bash
# pipe a skill and let the wizard guide you
<tool> --skill export <id> | npx skillflag install
```

The wizard lets you pick agents and scopes with arrow keys and space to select, then confirms before installing. This works even when stdin is piped.

### Multi-target install

`--agent` and `--scope` flags accept a single value each.
To install to multiple agents/scopes in one run, use the interactive wizard:

```bash
<tool> --skill export <id> | npx skillflag install
```

In the wizard, select multiple entries with space, then confirm the matrix install.

## Add skillflag to your CLI

1. Add the library and ship your skill directory in the package.
Expand All @@ -84,6 +141,24 @@ await maybeHandleSkillflag(process.argv, {

See the full guide in [docs/INTEGRATION.md](docs/INTEGRATION.md).

## Supported agents

codex, claude, portable, vscode, copilot, amp, goose, opencode, factory, cursor

## Supported scopes

| Scope | Description |
| ------ | ------------------------------------------------------------- |
| `repo` | Install into the current repository (e.g. `.codex/skills/`) |
| `user` | Install into the user's home config (e.g. `~/.codex/skills/`) |
| `cwd` | Install relative to the current working directory |

## Help

```bash
skill-install --help
```

## Bundled skill

This repo ships a single bundled skill at `skills/skillflag/` that documents both `skillflag` and `skill-install`.
4 changes: 3 additions & 1 deletion docs/SKILLFLAG_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,9 @@ skill-install [PATH ...]
[--legacy] # only where a legacy target exists (vscode/copilot -> .claude/skills)
```

Implementations **MAY** accept repeated and/or comma-separated `--agent` and `--scope` flags, and apply an install matrix across selected sources and targets.
The reference implementation accepts only one value for `--agent` and one value for `--scope`.
Repeated flags and comma-separated values are rejected.
Multi-target installs remain available through the interactive wizard (multi-select).

### 2.2 Required flags

Expand Down
58 changes: 35 additions & 23 deletions src/install/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,23 +135,25 @@ const scopeList = SCOPES.join(", ");

const usageLines = [
"Usage:",
" skill-install [PATH ...] [--agent <agent>[,<agent>...]] [--scope <scope>[,<scope>...]] [--force]",
" skill-install [PATH ...] [--agent <agent>] [--scope <scope>] [--force]",
"",
"Input:",
" PATH ... Skill directory path(s) containing SKILL.md.",
" stdin tar stream If PATH is omitted, reads a tar bundle from stdin.",
"",
"Options:",
" --agent <values> Target agent(s); repeat or use comma-separated values.",
" --agent <value> Target agent (single value).",
` Supported agents: ${agentList}`,
" --scope <values> Target scope(s); repeat or use comma-separated values.",
" --scope <value> Target scope (single value).",
` Supported scopes: ${scopeList}`,
" --force Overwrite destination if it already exists.",
" -h, --help Show this help.",
"",
"Behavior:",
" If --agent or --scope is missing and an interactive TTY is available,",
" the installer launches a wizard to collect missing values.",
" CLI flags accept only one --agent and one --scope.",
" Use the wizard to select multiple agents/scopes.",
];

const usageText = usageLines.join("\n");
Expand Down Expand Up @@ -192,51 +194,61 @@ const scopeDescriptions: Record<Scope, string> = {
cwd: "Install relative to the current working directory.",
};

function parseScopeValues(value: string | undefined): string[] {
function parseScopeValue(value: string | undefined): string {
if (!value || value.startsWith("-")) {
throw new InstallError("Missing value for --scope.");
}
const scopes = value
.split(",")
.map((item) => item.trim())
.filter((item) => item.length > 0);
if (scopes.length === 0) {
const scope = value.trim();
if (scope.length === 0) {
throw new InstallError("Missing value for --scope.");
}
return scopes;
if (scope.includes(",")) {
throw new InstallError(
"Only one value is allowed for --scope. Comma-separated values are not supported.",
);
}
return scope;
}

function parseAgentValues(value: string | undefined): string[] {
function parseAgentValue(value: string | undefined): string {
if (!value || value.startsWith("-")) {
throw new InstallError("Missing value for --agent.");
}
const agents = value
.split(",")
.map((item) => item.trim())
.filter((item) => item.length > 0);
if (agents.length === 0) {
const agent = value.trim();
if (agent.length === 0) {
throw new InstallError("Missing value for --agent.");
}
return agents;
if (agent.includes(",")) {
throw new InstallError(
"Only one value is allowed for --agent. Comma-separated values are not supported.",
);
}
return agent;
}

function parseArgs(args: string[]): ParsedArgs {
const rest = [...args];
const inputPaths: string[] = [];
const agents: string[] = [];
const scopes: string[] = [];
let agentValue: string | undefined;
let scopeValue: string | undefined;
let force = false;
let help = false;

for (let i = 0; i < rest.length; i += 1) {
const arg = rest[i];
if (arg === "--agent") {
agents.push(...parseAgentValues(rest[i + 1]));
if (agentValue !== undefined) {
throw new InstallError("Only one --agent flag is allowed.");
}
agentValue = parseAgentValue(rest[i + 1]);
i += 1;
continue;
}
if (arg === "--scope") {
scopes.push(...parseScopeValues(rest[i + 1]));
if (scopeValue !== undefined) {
throw new InstallError("Only one --scope flag is allowed.");
}
scopeValue = parseScopeValue(rest[i + 1]);
i += 1;
continue;
}
Expand All @@ -256,8 +268,8 @@ function parseArgs(args: string[]): ParsedArgs {

return {
inputPaths,
agents: uniqueValues(agents),
scopes: uniqueValues(scopes),
agents: agentValue ? [agentValue] : [],
scopes: scopeValue ? [scopeValue] : [],
force,
help,
};
Expand Down
8 changes: 4 additions & 4 deletions src/skillflag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export type SkillflagPromptApi = {

const usageLines = [
"Usage:",
" --skill install [<id> ...] [--agent <agent>[,<agent>...]] [--agent <agent>[,<agent>...]] [--scope <scope>[,<scope>...]] [--scope <scope>[,<scope>...]] [--force]",
" --skill install [<id> ...] [--agent <agent>] [--scope <scope>] [--force]",
" --skill list [--json]",
" --skill export <id>",
" --skill show <id>",
Expand All @@ -75,9 +75,9 @@ export const SKILLFLAG_HELP_TEXT = [
"Export a skill bundle:",
" tool --skill export <id>",
"",
"Install a skill bundle into one or more agents:",
" tool --skill install [<id> ...]",
" tool --skill export <id> | skill-install --agent <agent> [--agent <agent>] --scope <scope> [--scope <scope>]",
"Install a skill bundle:",
" tool --skill install [<id> ...] [--agent <agent>] [--scope <scope>]",
" tool --skill export <id> | skill-install --agent <agent> --scope <scope>",
"",
"For full details, read docs/SKILLFLAG_SPEC.md.",
].join("\n");
Expand Down
Loading