diff --git a/README.md b/README.md index 26bbd37496..9d31fe2a86 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,37 @@ firecrawl setup workflows These install globally across all detected coding editors by default. Use `--agent ` to scope either command to one editor. +#### Scope setup to a single harness + +By default skills route to every detected harness. To install and route to only +one harness in a single command: + +```bash +# One-shot from the install script (non-interactive, no TTY needed) +curl -fsSL https://firecrawl.dev/install.sh | bash -s -- --agent openclaw + +# Or, once the CLI is installed +firecrawl init --agent openclaw # only OpenClaw +firecrawl setup skills --agent openclaw +firecrawl setup workflows --agent openclaw +``` + +Any args after `bash -s --` are forwarded to `firecrawl init`, so you can add +things like `--skip-auth` for a skills-only, no-login setup. When you run +`firecrawl init` interactively without `--agent`, it shows a checkbox of your +detected harnesses (all selected by default) so you can pick a subset. + +| Harness | `--agent` value | +| ------------ | --------------- | +| Claude Code | `claude-code` | +| Codex | `codex` | +| Cursor | `cursor` | +| Windsurf | `windsurf` | +| OpenCode | `opencode` | +| OpenClaw | `openclaw` | +| OpenHands | `openhands` | +| Hermes Agent | `hermes-agent` | + ### Agent skills The init command installs all Firecrawl agent skill segments into AI coding agents (Cursor, Claude Code, Windsurf, etc.): diff --git a/scripts/install.sh b/scripts/install.sh index 046e29d410..f77ca80164 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -208,28 +208,47 @@ main() { # Resolve the binary path (may need updated PATH) local firecrawl_bin="$install_dir/firecrawl" + # Any extra args passed to the installer are forwarded to `firecrawl init`. + # This lets callers scope setup to a single harness in one non-interactive + # command, e.g.: + # curl -fsSL https://firecrawl.dev/install.sh | bash -s -- --agent openclaw + local -a init_args=("$@") + local has_init_args=0 + [ ${#init_args[@]} -gt 0 ] && has_init_args=1 + # Offer to continue with setup (login, skills, integrations) if [ -t 0 ] && [ -t 1 ]; then - # Interactive terminal — prompt - echo " Next: authenticate and install AI coding skills." - echo "" - printf " Continue with setup? [Y/n] " - read -r answer (e.g. openclaw)." + echo "" + fi fi } diff --git a/src/commands/init.ts b/src/commands/init.ts index 24a2dfd566..bbc74885ca 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -15,7 +15,11 @@ import { SKILL_REPOS, WORKFLOW_SKILL_REPOS, } from './skills-install'; -import { hasNpx, installSkillsNative } from './skills-native'; +import { + hasNpx, + installSkillsNative, + detectInstalledAgentNames, +} from './skills-native'; import { configureWebDefaults, WEB_AGENTS, @@ -131,6 +135,44 @@ async function installSkillRepoQuiet( ): Promise { const label = skillRepoLabel(repo); + // Prefer the native installer when we can detect installed harnesses. + // It creates symlinks to ~/.agents/skills/ (single source of truth) and + // only touches agents whose ~/. dirs actually exist — unlike + // `npx skills add --all` which sprays to every agent npx knows about. + const detected = detectInstalledAgentNames(); + const useNative = + // Explicit --agent → always native (scoped + symlinked) + options.agent || + // No --all flag → native with detected agents only + !options.all || + // --all but nothing detected locally → native will detect + symlink + detected.length > 0; + + if (useNative) { + try { + const result = await installSkillsNative(repo, { + agent: options.agent, + quiet: true, + }); + const suffix = ` ${dim}(${result.skillCount})${reset}`; + const linked = + result.linkedAgents.length > 0 + ? ` ${dim}→ ${result.linkedAgents.join(', ')}${reset}` + : ''; + console.log(` ${green}✓${reset} ${label}${suffix}${linked}`); + return result.skillCount; + } catch (err) { + console.log(` ${dim}✗${reset} ${label}`); + const msg = err instanceof Error ? err.message : 'Unknown error'; + console.error(` ${dim}${msg}${reset}`); + + // Fall through to npx if native failed and npx is available + if (!hasNpx()) throw err; + console.log(` ${dim}Retrying with npx...${reset}`); + } + } + + // Fallback: npx skills add (copies files, may spray to all agents) if (hasNpx()) { const args = buildSkillsInstallArgs({ repo, @@ -183,11 +225,55 @@ async function installSkillRepoQuiet( } } - // No npx — fall back to native installer. It prints its own status. - await installSkillsNative(repo); + // No npx and native already failed — nothing left to try return null; } +/** + * Prompt for which harnesses should receive the skills. Returns an explicit + * subset, or `null` to mean "all detected agents" (the broad `--all` path, + * which also covers agents npx knows about but that aren't detected locally). + */ +async function pickHarnesses(): Promise { + const detected = detectInstalledAgentNames(); + // Nothing detected locally — fall back to the broad install. + if (detected.length === 0) return null; + + const { checkbox } = await import('@inquirer/prompts'); + const selected = await checkbox({ + message: 'Which agents/harnesses should get the skills?', + choices: detected.map((name) => ({ name, value: name, checked: true })), + }); + + // Empty selection or "all of them" both map to the broad --all install. + if (selected.length === 0 || selected.length === detected.length) return null; + return selected; +} + +/** + * Install one skill repo across the chosen harnesses. `agents === null` lets + * the native installer detect and symlink to every installed harness in one + * pass; otherwise the repo is linked into each named agent individually. + * The skill count is reported once (the same skills are symlinked to each + * agent), so a multi-agent install does not inflate the total. + */ +async function installRepoAcrossAgents( + repo: string, + options: InitOptions, + agents: string[] | null +): Promise { + if (!agents) { + return installSkillRepoQuiet(repo, options); + } + + let count: number | null = null; + for (const agent of agents) { + const c = await installSkillRepoQuiet(repo, { ...options, agent }); + if (count == null) count = c; + } + return count; +} + /** Parse "Found N skills" or "Installed N skills" from npx skills output. */ function parseSkillCount(output: string): number | null { const match = output.match(/(?:Found|Installed)\s+(\d+)\s+skills?/); @@ -460,6 +546,18 @@ async function stepIntegrations(options: InitOptions): Promise { return null; } + // If skills/workflows are being installed, let the user route them to a + // subset of harnesses. An explicit --agent flag wins and skips the prompt; + // otherwise the default is every detected harness (null → --all). + const installsSkills = + integrations.includes('skills') || integrations.includes('workflows'); + const targetAgents: string[] | null = + installsSkills && !options.agent + ? await pickHarnesses() + : options.agent + ? [options.agent] + : null; + let totalSkills: number | null = null; for (const integration of integrations) { switch (integration) { @@ -467,7 +565,11 @@ async function stepIntegrations(options: InitOptions): Promise { console.log(`\n Installing skills...`); for (const repo of SKILL_REPOS) { try { - const count = await installSkillRepoQuiet(repo, options); + const count = await installRepoAcrossAgents( + repo, + options, + targetAgents + ); if (count != null) totalSkills = (totalSkills ?? 0) + count; } catch { console.error( @@ -481,7 +583,11 @@ async function stepIntegrations(options: InitOptions): Promise { console.log(`\n Installing workflow skills...`); for (const repo of WORKFLOW_SKILL_REPOS) { try { - const count = await installSkillRepoQuiet(repo, options); + const count = await installRepoAcrossAgents( + repo, + options, + targetAgents + ); if (count != null) totalSkills = (totalSkills ?? 0) + count; } catch { console.error( diff --git a/src/commands/skills-native.ts b/src/commands/skills-native.ts index a9ad9ba17c..c899d17e02 100644 --- a/src/commands/skills-native.ts +++ b/src/commands/skills-native.ts @@ -36,6 +36,19 @@ export interface NativeSkillsInstallResult { linkedAgents: string[]; } +/** + * Per-harness global skills directories. + * + * These MUST match the authoritative registry used by the `npx skills` tool + * (antfu/skills-cli, src/agents.ts) so the native fallback path (no npx) writes + * to the exact same place npx would. Several dirs are NOT `./skills` + * (windsurf, opencode, github-copilot), so relying on the synthesized fallback + * would install to the wrong directory. Paths are relative to HOME. + * + * openclaw/openhands/hermes-agent are Firecrawl launch targets; openclaw and + * openhands are verified (OpenClaw docs / antfu registry), hermes-agent mirrors + * its `~/.hermes` config home. + */ const AGENTS: AgentConfig[] = [ { name: 'claude-code', @@ -48,9 +61,10 @@ const AGENTS: AgentConfig[] = [ detectDir: '.cursor', }, { + // Windsurf stores skills under Codeium, NOT `.windsurf/skills`. name: 'windsurf', - globalSkillsDir: '.windsurf/skills', - detectDir: '.windsurf', + globalSkillsDir: '.codeium/windsurf/skills', + detectDir: '.codeium/windsurf', }, { name: 'codex', @@ -62,11 +76,6 @@ const AGENTS: AgentConfig[] = [ globalSkillsDir: '.continue/skills', detectDir: '.continue', }, - { - name: 'augment', - globalSkillsDir: '.augment/skills', - detectDir: '.augment', - }, { name: 'roo', globalSkillsDir: '.roo/skills', @@ -78,7 +87,8 @@ const AGENTS: AgentConfig[] = [ detectDir: '.gemini', }, { - name: 'copilot', + // antfu's agent id is 'github-copilot'; global skills live under `.copilot`. + name: 'github-copilot', globalSkillsDir: '.copilot/skills', detectDir: '.copilot', }, @@ -87,6 +97,29 @@ const AGENTS: AgentConfig[] = [ globalSkillsDir: '.factory/skills', detectDir: '.factory', }, + { + // OpenCode stores global skills under `.config/opencode`, NOT `.opencode`. + name: 'opencode', + globalSkillsDir: '.config/opencode/skills', + detectDir: '.config/opencode', + }, + { + name: 'openclaw', + globalSkillsDir: '.openclaw/skills', + detectDir: '.openclaw', + }, + { + name: 'openhands', + globalSkillsDir: '.openhands/skills', + detectDir: '.openhands', + }, + { + // Launch target's skillsAgent is 'hermes-agent'; its config home is + // `~/.hermes`, so skills live in `.hermes/skills` (not `.hermes-agent`). + name: 'hermes-agent', + globalSkillsDir: '.hermes/skills', + detectDir: '.hermes', + }, ]; /** Canonical directory for skill files — single source of truth */ @@ -262,6 +295,14 @@ function hashDir(dir: string): string { return hash.digest('hex'); } +/** + * Names of the agents/harnesses whose config directories currently exist under + * HOME. Used to populate the interactive harness picker in `firecrawl init`. + */ +export function detectInstalledAgentNames(): string[] { + return detectInstalledAgents().map((agent) => agent.name); +} + /** Detect which agents are installed by checking for their config directories */ function detectInstalledAgents(): AgentConfig[] { const home = os.homedir();