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
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,37 @@ firecrawl setup workflows

These install globally across all detected coding editors by default. Use `--agent <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.):
Expand Down
57 changes: 38 additions & 19 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 </dev/tty || answer=""
echo ""

case "$answer" in
[nN]*)
echo " Run 'firecrawl init --skip-install' later to set up."
echo ""
;;
*)
"$firecrawl_bin" init --skip-install
;;
esac
# Interactive terminal — prompt (skip the prompt if the caller already
# told us what to do via forwarded args).
if [ "$has_init_args" -eq 1 ]; then
"$firecrawl_bin" init --skip-install ${init_args[@]+"${init_args[@]}"}
else
echo " Next: authenticate and install AI coding skills."
echo ""
printf " Continue with setup? [Y/n] "
read -r answer </dev/tty || answer=""
echo ""

case "$answer" in
[nN]*)
echo " Run 'firecrawl init --skip-install' later to set up."
echo ""
;;
*)
"$firecrawl_bin" init --skip-install
;;
esac
fi
else
# Non-interactive (piped) — print instructions
echo " Run 'firecrawl init --skip-install' to authenticate and install skills."
echo ""
# Non-interactive (piped). With forwarded args, run a scoped one-shot
# (add --yes so init never blocks on a prompt). Otherwise print instructions.
if [ "$has_init_args" -eq 1 ]; then
"$firecrawl_bin" init --skip-install --yes ${init_args[@]+"${init_args[@]}"}
else
echo " Run 'firecrawl init --skip-install' to authenticate and install skills."
echo " To scope to one harness: bash -s -- --agent <harness> (e.g. openclaw)."
echo ""
fi
fi
}

Expand Down
116 changes: 111 additions & 5 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -131,6 +135,44 @@ async function installSkillRepoQuiet(
): Promise<number | null> {
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 ~/.<agent> 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,
Expand Down Expand Up @@ -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<string[] | null> {
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<string>({
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<number | null> {
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?/);
Expand Down Expand Up @@ -460,14 +546,30 @@ async function stepIntegrations(options: InitOptions): Promise<number | null> {
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) {
case 'skills': {
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(
Expand All @@ -481,7 +583,11 @@ async function stepIntegrations(options: InitOptions): Promise<number | null> {
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(
Expand Down
57 changes: 49 additions & 8 deletions src/commands/skills-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `.<name>/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',
Expand All @@ -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',
Expand All @@ -62,11 +76,6 @@ const AGENTS: AgentConfig[] = [
globalSkillsDir: '.continue/skills',
detectDir: '.continue',
},
{
name: 'augment',
globalSkillsDir: '.augment/skills',
detectDir: '.augment',
},
{
name: 'roo',
globalSkillsDir: '.roo/skills',
Expand All @@ -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',
},
Expand All @@ -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 */
Expand Down Expand Up @@ -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();
Expand Down
Loading