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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ detected harnesses (all selected by default) so you can pick a subset.

The init command installs the **CLI skills** by default and offers the **workflow skills** as optional extras, into AI coding agents (Cursor, Claude Code, Windsurf, etc.):

- **CLI skills** — teach agents how to use the Firecrawl CLI for live web work (search, scrape, interact, map, crawl, agent). Installed by default.
- **CLI skills** — teach agents how to use the Firecrawl CLI for live web work (search, scrape, interact, map, crawl, agent) and Alexandria data providers. Installed by default.
- **Workflow skills** — teach agents how to produce Firecrawl-powered deliverables such as research briefs, SEO audits, QA reports, lead lists, knowledge bases, and design-system extraction. Interactive multi-select during init.

All skill families live in the [`firecrawl/skills`](https://github.com/firecrawl/skills) catalog — including the **build skills** for integrating Firecrawl into application code:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "firecrawl-cli",
"version": "1.26.4",
"version": "1.26.5",
"publishConfig": {
"tag": "latest"
},
Expand Down
119 changes: 55 additions & 64 deletions src/__tests__/commands/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ import {
const cliSkillFlags = `--skill ${CLI_SKILLS.join(' ')}`;
const buildSkillFlags = `--skill ${BUILD_SKILLS.join(' ')}`;
const workflowSkillFlags = `--skill ${WORKFLOW_SKILLS.join(' ')}`;

function expectSkillsInstall(command: string) {
const [bin, ...args] = command.split(' ');
expect(execFileSync).toHaveBeenCalledWith(
bin,
args,
expect.objectContaining({ stdio: 'inherit' })
);
}
import { configureWebDefaults } from '../../utils/web-defaults';
import { getApiKey } from '../../utils/config';
import { browserLogin, isAuthenticated } from '../../utils/auth';
Expand Down Expand Up @@ -88,60 +97,47 @@ describe('handleSetupCommand', () => {
it('installs the CLI skills from the catalog globally across all detected agents by default', async () => {
await handleSetupCommand('skills', {});

expect(execSync).toHaveBeenCalledWith(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${cliSkillFlags}`,
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${cliSkillFlags}`
);
});

it('copies only the bundled beta skills for explicit beta setup', async () => {
await handleSetupCommand('alexandria', { agent: 'claude-code', yes: true });
it('ships the Alexandria skill in the core set', () => {
expect(CLI_SKILLS).toContain('firecrawl-alexandria');
});

it('passes --agent to npx as one argument, never through a shell', async () => {
const agent = 'cursor; touch /tmp/pwned';
await handleSetupCommand('core', { agent });
Comment thread
capy-ai[bot] marked this conversation as resolved.

expect(execFileSync).toHaveBeenCalledWith(
'npx',
[
'-y',
'skills',
'add',
path.resolve('beta-skills'),
'--full-depth',
'--global',
'--yes',
'--agent',
'claude-code',
'--skill',
'firecrawl-alexandria',
'firecrawl-agent',
'--copy',
],
expect.arrayContaining(['--agent', agent]),
expect.objectContaining({ stdio: 'inherit' })
);
expect(execSync).not.toHaveBeenCalled();
});

it('installs the CLI skills globally for a specific agent without using --all', async () => {
await handleSetupCommand('skills', { agent: 'cursor' });

expect(execSync).toHaveBeenCalledWith(
`npx -y skills add firecrawl/skills --full-depth --global --yes --agent cursor ${cliSkillFlags}`,
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
`npx -y skills add firecrawl/skills --full-depth --global --yes --agent cursor ${cliSkillFlags}`
);
});

it('treats "core" as the canonical name for the CLI skill set', async () => {
await handleSetupCommand('core', {});

expect(execSync).toHaveBeenCalledWith(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${cliSkillFlags}`,
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${cliSkillFlags}`
);
});

it('installs the build skills from the catalog as their own group', async () => {
await handleSetupCommand('build', {});

expect(execSync).toHaveBeenCalledWith(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${buildSkillFlags}`,
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${buildSkillFlags}`
);
});

Expand Down Expand Up @@ -179,44 +175,39 @@ describe('handleSetupCommand', () => {
it('installs a single catalog skill by exact name', async () => {
await handleSetupCommand('firecrawl-developer-index', {});

expect(execSync).toHaveBeenCalledWith(
'npx -y skills add firecrawl/skills --full-depth --global --yes --skill firecrawl-developer-index',
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
'npx -y skills add firecrawl/skills --full-depth --global --yes --skill firecrawl-developer-index'
);
});

it('resolves bare skill names by adding the firecrawl- prefix', async () => {
await handleSetupCommand('developer-index', {});

expect(execSync).toHaveBeenCalledWith(
'npx -y skills add firecrawl/skills --full-depth --global --yes --skill firecrawl-developer-index',
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
'npx -y skills add firecrawl/skills --full-depth --global --yes --skill firecrawl-developer-index'
);
});

it('prefers the build group over the firecrawl-build skill for bare "build"', async () => {
await handleSetupCommand('build', {});

expect(execSync).toHaveBeenCalledWith(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${buildSkillFlags}`,
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${buildSkillFlags}`
);

vi.mocked(execSync).mockClear();
vi.mocked(execFileSync).mockClear();
await handleSetupCommand('firecrawl-build', {});

expect(execSync).toHaveBeenCalledWith(
'npx -y skills add firecrawl/skills --full-depth --global --yes --skill firecrawl-build',
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
'npx -y skills add firecrawl/skills --full-depth --global --yes --skill firecrawl-build'
);
});

it('installs workflow skills from the catalog as a separate setup option', async () => {
await handleSetupCommand('workflows', {});

expect(execSync).toHaveBeenCalledWith(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${workflowSkillFlags}`,
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${workflowSkillFlags}`
);
});

Expand All @@ -227,17 +218,14 @@ describe('handleSetupCommand', () => {
ALL_SKILL_REPOS
);

expect(execSync).toHaveBeenCalledWith(
'npx -y skills add firecrawl/cli --full-depth --global --yes --agent codex',
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
'npx -y skills add firecrawl/cli --full-depth --global --yes --agent codex'
);
expect(execSync).toHaveBeenCalledWith(
'npx -y skills add firecrawl/skills --full-depth --global --yes --agent codex',
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
'npx -y skills add firecrawl/skills --full-depth --global --yes --agent codex'
);
expect(execSync).toHaveBeenCalledWith(
'npx -y skills add firecrawl/firecrawl-workflows --full-depth --global --yes --agent codex',
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
'npx -y skills add firecrawl/firecrawl-workflows --full-depth --global --yes --agent codex'
);
});

Expand All @@ -255,9 +243,8 @@ describe('handleSetupCommand', () => {

await handleSetupCommand(undefined, { yes: true });

expect(execSync).toHaveBeenCalledWith(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${cliSkillFlags}`,
expect.objectContaining({ stdio: 'inherit' })
expectSkillsInstall(
`npx -y skills add firecrawl/skills --full-depth --global --yes ${cliSkillFlags}`
);
expect(execFileSync).toHaveBeenCalledWith(
'npx',
Expand Down Expand Up @@ -815,7 +802,11 @@ describe('handleSetupCommand', () => {

const mcpCall = vi
.mocked(execFileSync)
.mock.calls.find(([command]) => command === 'npx');
.mock.calls.find(
([command, args]) =>
command === 'npx' && !(args as string[]).includes('skills')
);
expect(mcpCall).toBeDefined();
expect(mcpCall?.[1]).not.toContain('--global');
});

Expand Down Expand Up @@ -984,13 +975,13 @@ describe('handleSetupCommand', () => {
await handleSetupCommand('skills', {});

const allCalls = (
execSync as unknown as {
mock: { calls: [string, { env?: NodeJS.ProcessEnv }][] };
execFileSync as unknown as {
mock: { calls: [string, string[], { env?: NodeJS.ProcessEnv }][] };
}
).mock.calls;
const installCalls = allCalls.filter(([cmd]) =>
cmd.includes('skills add')
);
const installCalls = allCalls
.filter(([, args]) => args.join(' ').includes('skills add'))
.map(([, , opts]) => [undefined, opts] as const);
expect(installCalls.length).toBe(1);
for (const [, opts] of installCalls) {
expect(opts.env).toBeDefined();
Expand Down
35 changes: 10 additions & 25 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Installs firecrawl skill files and MCP server into AI coding agents
*/

import { execFileSync, execSync } from 'child_process';
import { execFileSync } from 'child_process';
import {
chmodSync,
existsSync,
Expand Down Expand Up @@ -302,26 +302,6 @@ export async function handleSetupCommand(
}

switch (subcommand) {
case 'alexandria': {
if (options.nativeSkills || options.project) {
throw new Error(
'Alexandria beta skill setup requires npm and global scope.'
);
}
const args = buildSkillsInstallArgs({
repo: path.resolve(__dirname, '../../beta-skills'),
skills: ['firecrawl-alexandria', 'firecrawl-agent'],
agent: options.agent,
includeNpxYes: true,
});
// Copy out of the npm cache so the installed skill survives cache cleanup.
runClientCommand('npx', [...args.slice(1), '--copy'], {
stdio: 'inherit',
env: cleanNpmEnv(),
});
await offerSkillsAuth(options);
break;
}
// `skills` is the historical name for the core set; keep it as an alias.
case 'skills':
case 'core':
Expand Down Expand Up @@ -636,11 +616,13 @@ async function installSkills(
skills: selection.skills,
});

const cmd = args.join(' ');
console.log(`Running: ${cmd}\n`);
console.log(`Running: ${args.join(' ')}\n`);

try {
execSync(cmd, { stdio: 'inherit', env: cleanNpmEnv() });
runClientCommand(args[0], args.slice(1), {
stdio: 'inherit',
env: cleanNpmEnv(),
});
continue;
} catch {
process.exit(1);
Expand All @@ -649,7 +631,10 @@ async function installSkills(

// Fallback: native install (no npx/Node required)
try {
await installSkillsNative(repo, { skills: selection.skills });
await installSkillsNative(repo, {
agent: options.agent,
skills: selection.skills,
});
} catch (error) {
console.error(
`Failed to install skills from ${repo}:`,
Expand Down
1 change: 1 addition & 0 deletions src/commands/skills-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const CLI_SKILLS = [
'firecrawl-monitor',
'firecrawl-parse',
'firecrawl-download',
'firecrawl-alexandria',
// Index skills: teach the `firecrawl research` / `firecrawl developer`
// CLI commands, so they ship with the CLI set.
'firecrawl-research-index',
Expand Down
Loading