diff --git a/README.md b/README.md index 45d9271f7c..09b6077998 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/package.json b/package.json index 6ecdc6d0bb..4d753d6b5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firecrawl-cli", - "version": "1.26.4", + "version": "1.26.5", "publishConfig": { "tag": "latest" }, diff --git a/src/__tests__/commands/setup.test.ts b/src/__tests__/commands/setup.test.ts index 530062c681..036a7bc2dd 100644 --- a/src/__tests__/commands/setup.test.ts +++ b/src/__tests__/commands/setup.test.ts @@ -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'; @@ -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 }); + 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}` ); }); @@ -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}` ); }); @@ -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' ); }); @@ -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', @@ -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'); }); @@ -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(); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 0b444a4ee2..f8d27bf439 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -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, @@ -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': @@ -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); @@ -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}:`, diff --git a/src/commands/skills-install.ts b/src/commands/skills-install.ts index 5bec03b056..d43dd1903f 100644 --- a/src/commands/skills-install.ts +++ b/src/commands/skills-install.ts @@ -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',