Thanks to visit codestin.com
Credit goes to github.com

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
10 changes: 8 additions & 2 deletions docs/src/content/docs/docs/getting-started/quick-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,19 @@ allagents update

## Add Individual Skills

Install a specific skill from a GitHub repo without adding the full plugin:
Pass `owner/repo` directly to install all skills from that repo — no flags required:

```bash
allagents skill add ReScienceLab/opc-skills
```

To install only a specific skill from a multi-skill repo:

```bash
allagents skill add reddit --from ReScienceLab/opc-skills
```

Or pass a GitHub URL directly:
Or point at a specific skill file with a full URL:

```bash
allagents skill add https://github.com/owner/repo/tree/main/skills/my-skill
Expand Down
48 changes: 38 additions & 10 deletions docs/src/content/docs/docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -195,30 +195,58 @@ After disabling, the skill is added to `disabledSkills` in workspace.yaml and sy

### skill add

Add a skill from a plugin, or re-enable a previously disabled skill. The `<skill>` argument can be a skill name or a GitHub URL pointing to a skill.
Add skills from a GitHub repo, re-enable a previously disabled skill, or add a skill from a specific plugin. The positional argument is interpreted by context:

| Flag | Description |
|------|-------------|
| `-f, --from <source>` | Plugin source (GitHub URL, `owner/repo`, or `plugin@marketplace`) to install if the skill is not already available |
| `--skill <names>` | Comma-separated skill names to install when the positional is a plugin source |
| `--all` | Install every skill from the source |
| `--list` | List available skills at the source without installing |
| `-p, --plugin <plugin>` | Plugin name (required if skill exists in multiple plugins) |
| `-s, --scope <scope>` | Scope: `project` (default) or `user` |

#### Adding a skill from a new plugin
#### Skill-first: install all skills from a repo

Use `--from` to specify the plugin source when the skill isn't already installed:
Pass `owner/repo` directly with no flags — all skills in that repo are installed automatically. This mirrors how `npx skills add` treats a package as a transport for its skills, not a named entity:

```bash
# Install all skills from a repo (standalone skill or multi-skill bundle)
allagents skill add ReScienceLab/opc-skills
allagents skill add https://github.com/owner/repo
allagents skill add gh:owner/repo
```

This works for:
- **Standalone skill repos** — a single `SKILL.md` at the root (no `skills/` subdirectory)
- **Multi-skill bundles** — repos with a `skills/` directory containing multiple skill files

Use `--list` first to preview what's available before installing:

```bash
allagents skill add ReScienceLab/opc-skills --list
```

#### Installing a specific named skill

To install only one skill from a multi-skill repo:

```bash
# Add a specific skill from a GitHub repo
allagents skill add reddit --from ReScienceLab/opc-skills
allagents skill add ReScienceLab/opc-skills --skill reddit
```

# Or pass a GitHub URL directly — the skill name is extracted from the URL path
allagents skill add https://github.com/owner/repo/tree/main/skills/my-skill
To install multiple by name:

```bash
allagents skill add ReScienceLab/opc-skills --skill reddit,terraform
```

**Source formats for `--from`:**
- GitHub shorthand: `owner/repo`
- GitHub URL: `https://github.com/owner/repo`
- Marketplace: `plugin@marketplace`
To install a skill from a specific file URL:

```bash
allagents skill add https://github.com/owner/repo/tree/main/skills/my-skill
```

#### Re-enabling a disabled skill

Expand Down
177 changes: 156 additions & 21 deletions src/cli/commands/plugin-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,12 +747,16 @@ async function installSkillViaMarketplace(opts: {

/**
* Decide whether the positional argument to `skill add` is a plugin source
* (npx-skills shape) or a skill name (legacy shape).
* (npx-skills shape), an auto-install source, or a skill name (legacy shape).
*
* The positional is interpreted as a source only when it looks like a GitHub
* spec AND the user supplied an explicit selector (--skill, --list, --all).
* Without a selector we fall through to the legacy resolveSkillFromUrl path,
* which preserves the deep-URL form `skill add <url-with-subpath>`.
* shape: 'source' — GitHub spec + explicit selector (--skill/--list/--all)
* shape: 'source-auto' — GitHub spec, no selector, no subpath: treat the repo
* as a collection of skills and install all of them. This
* mirrors `npx skills add owner/repo` where the repo is
* just a transport for its skills, not a named entity.
* shape: 'skill-name' — everything else, including deep-URL forms with a
* subpath (e.g. owner/repo/skills/foo) which are handled
* by the legacy resolveSkillFromUrl path.
*/
export function classifySkillAddPositional(
positional: string | undefined,
Expand All @@ -762,7 +766,8 @@ export function classifySkillAddPositional(
):
| { shape: 'none' }
| { shape: 'skill-name' }
| { shape: 'source'; skills: string[] } {
| { shape: 'source'; skills: string[] }
| { shape: 'source-auto' } {
if (!positional) return { shape: 'none' };
const skills = skillFlag
? skillFlag
Expand All @@ -771,8 +776,11 @@ export function classifySkillAddPositional(
.filter(Boolean)
: [];
const hasSelector = skills.length > 0 || list || all;
if (isGitHubUrl(positional) && hasSelector) {
return { shape: 'source', skills };
if (isGitHubUrl(positional)) {
if (hasSelector) return { shape: 'source', skills };
// No subpath → the repo is a standalone skill or skill bundle; auto-install all.
// Subpath → legacy deep-URL form handled by resolveSkillFromUrl.
if (!parseGitHubUrl(positional)?.subpath) return { shape: 'source-auto' };
}
return { shape: 'skill-name' };
}
Expand Down Expand Up @@ -1093,6 +1101,118 @@ async function discoverSkillsFromSource(
return { success: true, skills, isMarketplace: false };
}

/**
* Skill-first interactive install for `skill add owner/repo` (source-auto).
*
* When connected to a TTY, discovers skills from the source and shows an
* interactive multiselect with all skills pre-selected — the user can deselect
* any they don't want before confirming. Falls back to install-all in non-TTY
* or JSON mode, and for marketplace sources (which have a more complex picker
* model that isn't worth duplicating here).
*/
async function selectAndInstallSkillsFromSource(opts: {
from: string;
isUser: boolean;
workspacePath: string;
}): Promise<
| { success: true; installed: Array<{ pluginName: string; skills: string[] }>; syncResult: SyncResult }
| { success: false; error: string }
| { success: 'cancelled' }
> {
const { from, isUser, workspacePath } = opts;
const isTTY = process.stdout.isTTY && process.stdin.isTTY;

// Non-interactive path: install everything silently
if (!isTTY || isJsonMode()) {
return installAllSkillsFromSource(opts);
}

// Discover available skills (fetches the repo)
const discovered = await discoverSkillsFromSource(from);
if (!discovered.success) return { success: false, error: discovered.error };
if (discovered.skills.length === 0) {
return { success: false, error: `No skills found in '${from}'.` };
}

// Marketplace repos or single-skill repos: skip the picker
if (discovered.isMarketplace || discovered.skills.length === 1) {
return installAllSkillsFromSource(opts);
}

// Multiple skills on a direct repo: show a multiselect with all pre-selected
const p = await import('@clack/prompts');
const allNames = discovered.skills.map((s) => s.name);
const options = discovered.skills.map((s) => ({
label: s.name,
value: s.name,
...(s.description ? { hint: s.description } : {}),
}));

const selected = await p.autocompleteMultiselect({
message: `Select skills to install from ${chalk.bold(from)}`,
options,
initialValues: allNames,
placeholder: 'Type to filter · Space to toggle · Enter to confirm',
required: false,
});

if (p.isCancel(selected) || (selected as string[]).length === 0) {
return { success: 'cancelled' };
}

const selectedNames = selected as string[];

// All skills selected: use the efficient bulk path
if (selectedNames.length === allNames.length) {
return installAllSkillsFromSource(opts);
}

// Subset selected: configure allowlist with just those names, then sync once
const parsed = isGitHubUrl(from) ? parseGitHubUrl(from) : null;
const fetchResult = await fetchPlugin(from, {
...(parsed?.branch && { branch: parsed.branch }),
});
if (!fetchResult.success) {
return {
success: false,
error: `Failed to fetch '${from}': ${fetchResult.error ?? 'Unknown error'}`,
};
}

const existingEnabled = await getEnabledSkillsForGitHubSource(from, workspacePath);
const desiredSkills = [...existingEnabled];
for (const name of selectedNames) {
if (!desiredSkills.includes(name)) desiredSkills.push(name);
}

const updateResult = isUser
? await upsertUserGitHubPluginSourceAllowlist(from, desiredSkills)
: await upsertGitHubPluginSourceAllowlist(from, desiredSkills, workspacePath);

if (!updateResult.success) {
return {
success: false,
error: `Failed to configure skill allowlist: ${updateResult.error ?? 'Unknown error'}`,
};
}

const pluginName = extractPrimaryPluginName(updateResult.normalizedPlugin ?? from);
console.log(
`✓ Enabled ${selectedNames.length} skill(s) from ${pluginName}: ${selectedNames.join(', ')}`,
);

const syncResult = isUser
? await syncUserWorkspace()
: await syncWorkspace(workspacePath);
if (!syncResult.success) return { success: false, error: 'Sync failed' };

return {
success: true,
installed: [{ pluginName, skills: desiredSkills }],
syncResult,
};
}

/**
* Install all skills from a --from source. Mirrors installSkillFromSource but
* enables every discovered skill rather than a single named one.
Expand Down Expand Up @@ -1459,18 +1579,18 @@ const addCmd = command({
all,
}) => {
try {
// npx-skills shape: positional is the source, --skill/--list/--all picks
// what to install. Triggered only when the positional looks like a GitHub
// source AND a selector is provided, so the legacy deep-URL form
// (positional with implicit subpath selector) keeps working.
// Classify the positional argument so we know which code path to take.
const classified = classifySkillAddPositional(
skillArg,
skillFlag,
list,
all,
);
let skillsFromFlag: string[] = [];
if (classified.shape === 'source') {
// source-auto: bare owner/repo (no subpath, no selector) — install all skills
// from the repo as if --all were passed. Mirrors `npx skills add owner/repo`.
let autoAll = false;
if (classified.shape === 'source' || classified.shape === 'source-auto') {
if (fromArg) {
const error =
'Cannot use --from when the positional argument is already a plugin source.';
Expand All @@ -1483,7 +1603,11 @@ const addCmd = command({
}
fromArg = skillArg;
skillArg = undefined;
skillsFromFlag = classified.skills;
if (classified.shape === 'source') {
skillsFromFlag = classified.skills;
} else {
autoAll = true;
}
} else if (skillFlag) {
const error =
'--skill requires the positional argument to be a plugin source (e.g., `skill add owner/repo --skill foo`). To install a known skill, pass the skill name as the positional.';
Expand Down Expand Up @@ -1612,8 +1736,8 @@ const addCmd = command({
return;
}

// --all: bulk install
if (all) {
// --all or auto-install (bare owner/repo with no selector): bulk install
if (all || autoAll) {
if (!fromArg) {
const error = '--all requires --from to specify a plugin source.';
if (isJsonMode()) {
Expand All @@ -1637,11 +1761,22 @@ const addCmd = command({
const isUserAll = scope === 'user';
const workspacePathAll = isUserAll ? getHomeDir() : process.cwd();

const installResult = await installAllSkillsFromSource({
from: fromArg,
isUser: isUserAll,
workspacePath: workspacePathAll,
});
// source-auto: interactive picker on TTY; --all: direct bulk install
const installResult = autoAll
? await selectAndInstallSkillsFromSource({
from: fromArg,
isUser: isUserAll,
workspacePath: workspacePathAll,
})
: await installAllSkillsFromSource({
from: fromArg,
isUser: isUserAll,
workspacePath: workspacePathAll,
});

if (installResult.success === 'cancelled') {
return;
}

if (!installResult.success) {
if (isJsonMode()) {
Expand Down
6 changes: 5 additions & 1 deletion src/cli/metadata/plugin-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export const skillsAddMeta: AgentCommandMeta = {
whenToUse:
'To add a skill from a GitHub repo or marketplace plugin, or to re-enable a skill that was previously disabled',
examples: [
'allagents skill add ReScienceLab/opc-skills',
'allagents skill add reddit --from ReScienceLab/opc-skills',
'allagents skill add NousResearch/hermes-agent --skill llm-wiki',
'allagents skill add NousResearch/hermes-agent --skill llm-wiki,dogfood',
Expand All @@ -147,7 +148,10 @@ export const skillsAddMeta: AgentCommandMeta = {
type: 'string',
required: false,
description:
'Either a skill name (paired with --from) or a plugin source (owner/repo, gh:..., or a GitHub URL — paired with --skill, --list, or --all).',
'A skill name (re-enable an installed skill, or pair with --from to install from a source); ' +
'a plugin source without subpath (owner/repo, gh:owner/repo, or https://github.com/owner/repo — ' +
'installs all skills from that repo without needing any flags); ' +
'or a deep GitHub URL with a subpath pointing to a specific skill file.',
},
],
options: [
Expand Down
29 changes: 27 additions & 2 deletions tests/unit/cli/skill-add-classify-positional.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,33 @@ describe('classifySkillAddPositional', () => {
});
});

it('falls back to skill-name for source-shaped positional with no selector (legacy deep-URL form)', () => {
// `resolveSkillFromUrl` handles URL/owner-repo without selectors as legacy
// skill-first: bare owner/repo (no selector, no subpath) → auto-install all skills
it('auto-installs all skills for bare owner/repo with no selector', () => {
expect(classifySkillAddPositional('ReScienceLab/opc-skills', undefined, false, false)).toEqual({
shape: 'source-auto',
});
});

it('auto-installs all skills for bare https://github.com/owner/repo with no selector', () => {
expect(
classifySkillAddPositional('https://github.com/owner/repo', undefined, false, false),
).toEqual({ shape: 'source-auto' });
});

it('auto-installs all skills for gh: shorthand with no selector', () => {
expect(classifySkillAddPositional('gh:owner/repo', undefined, false, false)).toEqual({
shape: 'source-auto',
});
});

it('falls back to skill-name for owner/repo with subpath (legacy deep-URL form)', () => {
// Subpath → handled by resolveSkillFromUrl as a specific skill install
expect(
classifySkillAddPositional('owner/repo/skills/my-skill', undefined, false, false),
).toEqual({ shape: 'skill-name' });
});

it('falls back to skill-name for full GitHub URL with subpath (legacy deep-URL form)', () => {
expect(
classifySkillAddPositional(
'https://github.com/owner/repo/blob/main/skills/foo',
Expand Down