From e766ad6d8ef9adf1307e40b07bc7e5e7e222f0de Mon Sep 17 00:00:00 2001 From: hmishra2250 Date: Sun, 12 Jul 2026 02:26:11 +0530 Subject: [PATCH] revert: remove managed MCP router cards --- README.md | 23 --- package.json | 2 +- src/__tests__/commands/launch.test.ts | 46 ----- src/__tests__/commands/setup.test.ts | 41 ----- src/__tests__/utils/router-card.test.ts | 173 ------------------- src/commands/launch.ts | 34 +--- src/commands/setup.ts | 30 ---- src/index.ts | 16 -- src/utils/router-card.ts | 215 ------------------------ 9 files changed, 7 insertions(+), 573 deletions(-) delete mode 100644 src/__tests__/utils/router-card.test.ts delete mode 100644 src/utils/router-card.ts diff --git a/README.md b/README.md index e726ceb3b..9d31fe2a8 100644 --- a/README.md +++ b/README.md @@ -81,29 +81,6 @@ To install the Firecrawl MCP server into your editors (Cursor, Claude Code, VS C firecrawl setup mcp ``` -To co-deliver concise routing guidance into a project's harness-native context, -select one agent and opt in to the router card: - -```bash -firecrawl setup mcp --agent claude-code --router-card --project ./my-project -firecrawl setup mcp --agent codex --router-card --project ./my-project -``` - -The CLI writes an auth-neutral, versioned managed block to `CLAUDE.md` for -Claude Code, `AGENTS.md` for Codex/OpenCode/Hermes/OpenClaw, or -`.cursor/rules/firecrawl.mdc` for Cursor/VS Code. Existing project guidance is -preserved. The card is written only after MCP configuration succeeds; the MCP -server itself never writes project files. - -`firecrawl launch ` co-delivers the router card by default after MCP is -configured and launches the agent in the selected project. Opt out explicitly -when needed: - -```bash -firecrawl launch codex --project ./my-project -firecrawl launch claude --project ./my-project --no-router-card -``` - To make Firecrawl the default web provider for supported AI agents: ```bash diff --git a/package.json b/package.json index 173bb4c78..083d30cd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firecrawl-cli", - "version": "1.19.25", + "version": "1.19.26", "description": "Command-line interface for Firecrawl. Scrape, crawl, and extract data from any website directly from your terminal.", "main": "dist/index.js", "bin": { diff --git a/src/__tests__/commands/launch.test.ts b/src/__tests__/commands/launch.test.ts index e5c780a54..ecfaa448b 100644 --- a/src/__tests__/commands/launch.test.ts +++ b/src/__tests__/commands/launch.test.ts @@ -9,7 +9,6 @@ import { installSkillsForAgent, } from '../../commands/setup'; import { ALL_SKILL_REPOS } from '../../commands/skills-install'; -import { installRouterCard } from '../../utils/router-card'; vi.mock('child_process', () => ({ spawnSync: vi.fn(), @@ -26,15 +25,6 @@ vi.mock('../../commands/setup', () => ({ installSkillsForAgent: vi.fn(async () => undefined), })); -vi.mock('../../utils/router-card', () => ({ - installRouterCard: vi.fn(() => ({ - path: '/project/AGENTS.md', - changed: true, - version: 1, - sha256: 'df867193a6fe011342fce14b770e497cf667ca755e396bb16bbb52c513627951', - })), -})); - describe('handleLaunchCommand', () => { const originalIsTty = process.stdin.isTTY; @@ -88,7 +78,6 @@ describe('handleLaunchCommand', () => { ALL_SKILL_REPOS ); expect(spawnSync).not.toHaveBeenCalled(); - expect(installRouterCard).toHaveBeenCalledWith('claude', process.cwd()); }); it('supports setup and config as install-mode aliases', async () => { @@ -237,7 +226,6 @@ describe('handleLaunchCommand', () => { await handleLaunchCommand('opencode', { skipMcp: true }); expect(installMcp).not.toHaveBeenCalled(); - expect(installRouterCard).not.toHaveBeenCalled(); expect(installSkillsForAgent).toHaveBeenCalledWith( 'opencode', { @@ -253,45 +241,11 @@ describe('handleLaunchCommand', () => { }); }); - it('writes the harness-native router card by default in a selected project', async () => { - await handleLaunchCommand('opencode', { - project: '/tmp/example-project', - }); - - expect(installRouterCard).toHaveBeenCalledWith( - 'opencode', - '/tmp/example-project' - ); - expect(spawnSync).toHaveBeenNthCalledWith( - 2, - 'opencode', - [], - expect.objectContaining({ cwd: '/tmp/example-project' }) - ); - }); - - it('supports explicitly disabling router-card delivery', async () => { - await handleLaunchCommand('codex', { routerCard: false }); - - expect(installRouterCard).not.toHaveBeenCalled(); - }); - - it('never writes a router card when MCP configuration fails', async () => { - vi.mocked(installMcp).mockRejectedValueOnce(new Error('MCP failed')); - - await expect(handleLaunchCommand('codex')).rejects.toThrow('MCP failed'); - - expect(installRouterCard).not.toHaveBeenCalled(); - expect(installSkillsForAgent).not.toHaveBeenCalled(); - expect(spawnSync).not.toHaveBeenCalled(); - }); - it('can skip skills for a launch target that normally supports them', async () => { await handleLaunchCommand('opencode', { skipMcp: true, skipSkills: true }); expect(installMcp).not.toHaveBeenCalled(); expect(installSkillsForAgent).not.toHaveBeenCalled(); - expect(installRouterCard).not.toHaveBeenCalled(); }); it('configures Hermes MCP and skills, then launches Hermes Agent', async () => { diff --git a/src/__tests__/commands/setup.test.ts b/src/__tests__/commands/setup.test.ts index 0ec815bf3..6c989be73 100644 --- a/src/__tests__/commands/setup.test.ts +++ b/src/__tests__/commands/setup.test.ts @@ -186,47 +186,6 @@ describe('handleSetupCommand', () => { ); }); - it('co-delivers an auth-neutral router card when explicitly requested', async () => { - const root = mkdtempSync(path.join(os.tmpdir(), 'firecrawl-router-setup-')); - try { - await handleSetupCommand('mcp', { - agent: 'claude-code', - global: true, - yes: true, - routerCard: true, - project: root, - }); - - const card = readFileSync(path.join(root, 'CLAUDE.md'), 'utf8'); - expect(card).toContain('firecrawl-router-card:version=1'); - expect(card).not.toContain('fc-test-key'); - expect(card).not.toContain('mcp.firecrawl.dev'); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it('requires a single selected harness for MCP router-card delivery', async () => { - await expect( - handleSetupCommand('mcp', { routerCard: true, project: process.cwd() }) - ).rejects.toThrow('requires one explicit --agent'); - await expect( - handleSetupCommand('mcp', { - agent: 'all', - routerCard: true, - project: process.cwd(), - }) - ).rejects.toThrow('requires one explicit --agent'); - await expect( - handleSetupCommand('mcp', { - agent: 'unknown-harness', - routerCard: true, - project: process.cwd(), - }) - ).rejects.toThrow('not supported'); - expect(execSync).not.toHaveBeenCalled(); - }); - it('normalizes launch aliases when reinstalling MCP after auth changes', async () => { await handleSetupCommand('mcp', { agent: 'codex-app', diff --git a/src/__tests__/utils/router-card.test.ts b/src/__tests__/utils/router-card.test.ts deleted file mode 100644 index 0bc204e4a..000000000 --- a/src/__tests__/utils/router-card.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { - chmodSync, - lstatSync, - mkdtempSync, - mkdirSync, - readFileSync, - readdirSync, - rmSync, - symlinkSync, - writeFileSync, -} from 'fs'; -import os from 'os'; -import path from 'path'; -import { afterEach, describe, expect, it } from 'vitest'; -import { - installRouterCard, - resolveRouterCardContext, - ROUTER_CARD, - ROUTER_CARD_SHA256, - ROUTER_CARD_VERSION, - routerCardPath, -} from '../../utils/router-card'; - -const projects: string[] = []; - -function project(): string { - const directory = mkdtempSync(path.join(os.tmpdir(), 'firecrawl-router-')); - projects.push(directory); - return directory; -} - -afterEach(() => { - while (projects.length > 0) { - rmSync(projects.pop()!, { recursive: true, force: true }); - } -}); - -describe('router card delivery', () => { - it.each([ - ['claude', 'CLAUDE.md'], - ['claude-code', 'CLAUDE.md'], - ['codex', 'AGENTS.md'], - ['opencode', 'AGENTS.md'], - ['hermes', 'AGENTS.md'], - ['openclaw', 'AGENTS.md'], - ['vscode', path.join('.cursor', 'rules', 'firecrawl.mdc')], - ['cursor', path.join('.cursor', 'rules', 'firecrawl.mdc')], - ])('writes %s guidance to its native project context', (agent, relative) => { - const root = project(); - const result = installRouterCard(agent, root); - - expect(result).toEqual({ - path: path.join(root, relative), - changed: true, - version: ROUTER_CARD_VERSION, - sha256: ROUTER_CARD_SHA256, - }); - const content = readFileSync(result.path, 'utf8'); - if (relative.endsWith('.mdc')) { - expect(content).toBe( - `---\ndescription: Route public web discovery and retrieval through Firecrawl\nalwaysApply: true\n---\n\n${ROUTER_CARD}\n` - ); - } else { - expect(content).toBe(`${ROUTER_CARD}\n`); - } - }); - - it('preserves unrelated content and updates the managed block in place', () => { - const root = project(); - const target = path.join(root, 'AGENTS.md'); - writeFileSync( - target, - `before\n\n\nold\n\n\nafter\n` - ); - - installRouterCard('codex', root); - - expect(readFileSync(target, 'utf8')).toBe( - `before\n\n${ROUTER_CARD}\n\nafter\n` - ); - }); - - it('is byte-for-byte idempotent once current', () => { - const root = project(); - const first = installRouterCard('codex', root); - const before = readFileSync(first.path, 'utf8'); - - const second = installRouterCard('codex', root); - - expect(second.changed).toBe(false); - expect(readFileSync(first.path, 'utf8')).toBe(before); - }); - - it('preserves permissions while atomically replacing an existing file', () => { - const root = project(); - const target = path.join(root, 'AGENTS.md'); - writeFileSync(target, 'project rules\n'); - chmodSync(target, 0o600); - - installRouterCard('codex', root); - - expect(lstatSync(target).mode & 0o777).toBe(0o600); - expect(readdirSync(root).filter((name) => name.endsWith('.tmp'))).toEqual( - [] - ); - }); - - it.each([ - '\nmissing end\n', - '\n', - '\n\n', - `${ROUTER_CARD}\n${ROUTER_CARD}\n`, - `\n${ROUTER_CARD}\n`, - ])('refuses malformed or duplicate marker state', (content) => { - const root = project(); - const target = path.join(root, 'AGENTS.md'); - writeFileSync(target, content); - - expect(() => installRouterCard('codex', root)).toThrow( - 'malformed or duplicate' - ); - expect(readFileSync(target, 'utf8')).toBe(content); - }); - - it('refuses a symlink destination without changing its referent', () => { - const root = project(); - const referent = path.join(root, 'real.md'); - writeFileSync(referent, 'do not touch\n'); - symlinkSync(referent, path.join(root, 'AGENTS.md')); - - expect(() => installRouterCard('codex', root)).toThrow('symlink'); - expect(readFileSync(referent, 'utf8')).toBe('do not touch\n'); - }); - - it('refuses a broken symlink destination', () => { - const root = project(); - symlinkSync(path.join(root, 'missing.md'), path.join(root, 'AGENTS.md')); - - expect(() => installRouterCard('codex', root)).toThrow('symlink'); - }); - - it('refuses a symlinked context directory', () => { - const root = project(); - const elsewhere = path.join(root, 'elsewhere'); - mkdirSync(elsewhere); - symlinkSync(elsewhere, path.join(root, '.cursor')); - - expect(() => installRouterCard('cursor', root)).toThrow('symlink'); - expect(readdirSync(elsewhere)).toEqual([]); - }); - - it('contains routing only and no credentials or hosted MCP URL', () => { - expect(ROUTER_CARD).toContain('firecrawl_search'); - expect(ROUTER_CARD).toContain('firecrawl_scrape'); - expect(ROUTER_CARD).toContain( - 'Respect explicit requests to stay offline, avoid web lookup, or use another named tool.' - ); - expect(ROUTER_CARD).not.toMatch(/api[_ -]?key|fc-[a-z0-9]|mcp\.firecrawl/i); - }); - - it('rejects unknown agents and missing project directories', () => { - expect(() => resolveRouterCardContext('unknown')).toThrow('not supported'); - expect(() => - installRouterCard('codex', path.join(project(), 'missing')) - ).toThrow('not a directory'); - }); - - it('resolves paths without writing files', () => { - const root = project(); - expect(routerCardPath(root, 'claude')).toBe(path.join(root, 'CLAUDE.md')); - expect(readdirSync(root)).toEqual([]); - }); -}); diff --git a/src/commands/launch.ts b/src/commands/launch.ts index 93843006b..ee1f11e51 100644 --- a/src/commands/launch.ts +++ b/src/commands/launch.ts @@ -10,7 +10,6 @@ import { installSkillsForAgent, } from './setup'; import { ALL_SKILL_REPOS } from './skills-install'; -import { installRouterCard } from '../utils/router-card'; export interface LaunchOptions { config?: boolean; @@ -20,8 +19,6 @@ export interface LaunchOptions { yes?: boolean; skipMcp?: boolean; skipSkills?: boolean; - routerCard?: boolean; - project?: string; } interface LaunchTarget { @@ -33,10 +30,7 @@ interface LaunchTarget { command: string; args?: string[]; supportsExtraArgs?: boolean; - fallbackCommand?: ( - project: string - ) => { command: string; args: string[] } | null; - routerCardAgent: string; + fallbackCommand?: () => { command: string; args: string[] } | null; } type LaunchSetupMode = 'both' | 'mcp' | 'skills'; @@ -44,7 +38,6 @@ type LaunchSetupMode = 'both' | 'mcp' | 'skills'; const TARGETS: LaunchTarget[] = [ { aliases: ['claude', 'claude-code'], - routerCardAgent: 'claude', displayName: 'Claude Code', mcpAgent: 'claude-code', skillsAgent: 'claude-code', @@ -58,22 +51,20 @@ const TARGETS: LaunchTarget[] = [ }, { aliases: ['code', 'vscode', 'vs-code'], - routerCardAgent: 'vscode', displayName: 'VS Code', mcpAgent: 'vscode', command: 'code', args: ['.'], - fallbackCommand: (project) => { + fallbackCommand: () => { if (process.platform !== 'darwin') return null; return { command: 'open', - args: ['-a', 'Visual Studio Code', project], + args: ['-a', 'Visual Studio Code', process.cwd()], }; }, }, { aliases: ['codex'], - routerCardAgent: 'codex', displayName: 'Codex', mcpAgent: 'codex', skillsAgent: 'codex', @@ -81,7 +72,6 @@ const TARGETS: LaunchTarget[] = [ }, { aliases: ['codex-app', 'codex-desktop', 'codex-gui'], - routerCardAgent: 'codex', displayName: 'Codex App', mcpAgent: 'codex', skillsAgent: 'codex', @@ -98,7 +88,6 @@ const TARGETS: LaunchTarget[] = [ }, { aliases: ['opencode', 'open-code'], - routerCardAgent: 'opencode', displayName: 'OpenCode', mcpAgent: 'opencode', skillsAgent: 'opencode', @@ -106,7 +95,6 @@ const TARGETS: LaunchTarget[] = [ }, { aliases: ['hermes', 'hermes-agent'], - routerCardAgent: 'hermes', displayName: 'Hermes Agent', mcpInstaller: installHermesMcp, skillsAgent: 'hermes-agent', @@ -114,7 +102,6 @@ const TARGETS: LaunchTarget[] = [ }, { aliases: ['openclaw'], - routerCardAgent: 'openclaw', displayName: 'OpenClaw', mcpInstaller: installOpenClawMcp, skillsAgent: 'openclaw', @@ -214,8 +201,7 @@ function commandExists(command: string): boolean { function resolveLaunchCommand( target: LaunchTarget, - extraArgs: string[], - project: string + extraArgs: string[] ): { command: string; args: string[] } { if (extraArgs.length > 0 && target.supportsExtraArgs === false) { throw new Error(`${target.displayName} does not accept extra arguments.`); @@ -228,7 +214,7 @@ function resolveLaunchCommand( }; } - const fallback = target.fallbackCommand?.(project); + const fallback = target.fallbackCommand?.(); if (fallback) { return { command: fallback.command, @@ -261,7 +247,6 @@ export async function handleLaunchCommand( const targetSupportsMcp = Boolean(target.mcpInstaller || target.mcpAgent); const targetSupportsSkills = Boolean(target.skillsAgent); - const project = path.resolve(options.project ?? process.cwd()); let installMcpForTarget = targetSupportsMcp && !options.skipMcp; let installSkillsForTarget = targetSupportsSkills && !options.skipSkills; @@ -287,12 +272,6 @@ export async function handleLaunchCommand( quiet: true, }); } - if (options.routerCard !== false) { - const result = installRouterCard(target.routerCardAgent, project); - console.log( - `Firecrawl router card ${result.changed ? 'installed' : 'already current'} at ${result.path} (sha256:${result.sha256}).` - ); - } } if (target.skillsAgent && installSkillsForTarget) { @@ -314,11 +293,10 @@ export async function handleLaunchCommand( return; } - const launch = resolveLaunchCommand(target, extraArgs, project); + const launch = resolveLaunchCommand(target, extraArgs); const result = spawnSync(launch.command, launch.args, { stdio: 'inherit', env: process.env, - cwd: project, }); if (result.error) { diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 754344316..25eac9450 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -22,10 +22,6 @@ import { WEB_AGENTS, type WebAgent, } from '../utils/web-defaults'; -import { - installRouterCard, - resolveRouterCardContext, -} from '../utils/router-card'; export type SetupSubcommand = 'skills' | 'workflows' | 'mcp' | 'defaults'; @@ -47,10 +43,6 @@ export interface SetupOptions { nativeSkills?: boolean; /** Render compact skill install output. */ quiet?: boolean; - /** Co-deliver project routing guidance with the MCP configuration. */ - routerCard?: boolean; - /** Project directory that receives harness-native routing guidance. */ - project?: string; } const green = '\x1b[32m'; @@ -374,25 +366,12 @@ export async function installSkillsForAgent( export async function installMcp(options: SetupOptions): Promise { const resolvedAgent = resolveMcpAgent(options.agent); - if ( - options.routerCard && - (!options.agent || resolvedAgent.kind === 'all-launchers') - ) { - throw new Error( - '--router-card requires one explicit --agent so the CLI can select the harness-native project context file.' - ); - } - if (options.routerCard && options.agent) { - resolveRouterCardContext(options.agent); - } if (resolvedAgent.kind === 'hermes') { await installHermesMcp(); - installRequestedRouterCard(options); return; } if (resolvedAgent.kind === 'openclaw') { await installOpenClawMcp(); - installRequestedRouterCard(options); return; } if (resolvedAgent.kind === 'all-launchers') { @@ -406,15 +385,6 @@ export async function installMcp(options: SetupOptions): Promise { } await installAddMcp(options, resolvedAgent); - installRequestedRouterCard(options); -} - -function installRequestedRouterCard(options: SetupOptions): void { - if (!options.routerCard || !options.agent) return; - const result = installRouterCard(options.agent, options.project); - console.log( - ` ${green}✓${reset} Firecrawl router card ${result.changed ? 'installed' : 'already current'} at ${result.path} (sha256:${result.sha256})` - ); } async function installAddMcp( diff --git a/src/index.ts b/src/index.ts index 6b6dd9b42..8949f0732 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2191,14 +2191,6 @@ program '--undo', 'Undo setup defaults by re-enabling native web tools where supported' ) - .option( - '--router-card', - 'Write versioned Firecrawl routing guidance to the selected agent project context' - ) - .option( - '--project ', - 'Project directory for the router card (default: current directory)' - ) .action(async (subcommand: SetupSubcommand, options) => { await handleSetupCommand(subcommand, options); }); @@ -2238,14 +2230,6 @@ program .option('--config', 'Alias for --install') .option('--skip-mcp', 'Launch without installing or updating Firecrawl MCP') .option('--skip-skills', 'Launch without installing Firecrawl skills') - .option( - '--no-router-card', - 'Launch without writing Firecrawl routing guidance to the project context' - ) - .option( - '--project ', - 'Project directory for router guidance and launched-agent working directory' - ) .option( '-g, --global', 'Install Firecrawl MCP globally for the selected agent', diff --git a/src/utils/router-card.ts b/src/utils/router-card.ts deleted file mode 100644 index 95099d484..000000000 --- a/src/utils/router-card.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { - closeSync, - existsSync, - fsyncSync, - lstatSync, - mkdirSync, - openSync, - readFileSync, - renameSync, - rmSync, - statSync, - writeFileSync, -} from 'fs'; -import path from 'path'; -import { createHash, randomUUID } from 'crypto'; - -const START_MARKER = ''; -const END_MARKER = ''; - -export const ROUTER_CARD_VERSION = 1; - -export const ROUTER_CARD = `${START_MARKER} - -## Firecrawl web routing - -Use Firecrawl for public-web discovery and retrieval: -- Search the web with \`firecrawl_search\`. -- Read a page with \`firecrawl_scrape\`. -- Discover or traverse a site with \`firecrawl_map\` or \`firecrawl_crawl\`. - -Prefer Firecrawl over generic browsing tools when the task needs public web data. -Respect explicit requests to stay offline, avoid web lookup, or use another named tool. -${END_MARKER}`; - -const CURSOR_FRONTMATTER = `--- -description: Route public web discovery and retrieval through Firecrawl -alwaysApply: true ----`; - -export type RouterCardContext = 'claude' | 'agents' | 'cursor'; - -export interface RouterCardResult { - path: string; - changed: boolean; - version: number; - sha256: string; -} - -export const ROUTER_CARD_SHA256 = createHash('sha256') - .update(ROUTER_CARD, 'utf8') - .digest('hex'); - -const CLAUDE_ALIASES = new Set(['claude', 'claude-code']); -const AGENTS_ALIASES = new Set([ - 'codex', - 'codex-app', - 'codex-desktop', - 'codex-gui', - 'opencode', - 'open-code', - 'hermes', - 'hermes-agent', - 'openclaw', -]); -const CURSOR_ALIASES = new Set(['code', 'vscode', 'vs-code', 'cursor']); - -export function resolveRouterCardContext(agent: string): RouterCardContext { - const normalized = agent.trim().toLowerCase(); - if (CLAUDE_ALIASES.has(normalized)) return 'claude'; - if (AGENTS_ALIASES.has(normalized)) return 'agents'; - if (CURSOR_ALIASES.has(normalized)) return 'cursor'; - throw new Error( - `Router cards are not supported for agent "${agent}". Supported agents: claude, codex, opencode, hermes, openclaw, vscode, cursor.` - ); -} - -export function routerCardPath( - projectPath: string, - context: RouterCardContext -): string { - const project = path.resolve(projectPath); - switch (context) { - case 'claude': - return path.join(project, 'CLAUDE.md'); - case 'agents': - return path.join(project, 'AGENTS.md'); - case 'cursor': - return path.join(project, '.cursor', 'rules', 'firecrawl.mdc'); - } -} - -function assertNotSymlink(candidate: string, label: string): void { - try { - if (lstatSync(candidate).isSymbolicLink()) { - throw new Error( - `Refusing to write router card through symlink: ${label}` - ); - } - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; - } -} - -function assertSafeDestination(project: string, destination: string): void { - if (!existsSync(project) || !statSync(project).isDirectory()) { - throw new Error(`Router card project is not a directory: ${project}`); - } - assertNotSymlink(project, project); - - let current = path.dirname(destination); - while (current !== project) { - assertNotSymlink(current, current); - const parent = path.dirname(current); - if (parent === current) break; - current = parent; - } - assertNotSymlink(destination, destination); -} - -function markerCount(content: string, marker: string): number { - return content.split(marker).length - 1; -} - -function updateManagedBlock(existing: string): string { - const starts = markerCount(existing, START_MARKER); - const ends = markerCount(existing, END_MARKER); - const markerLike = - existing.match(//g) ?? - []; - - if ( - starts > 1 || - ends > 1 || - starts !== ends || - markerLike.length !== starts + ends - ) { - throw new Error( - 'Refusing to update malformed or duplicate Firecrawl router-card markers.' - ); - } - - if (starts === 1) { - const start = existing.indexOf(START_MARKER); - const end = existing.indexOf(END_MARKER); - if (end < start) { - throw new Error( - 'Refusing to update malformed or duplicate Firecrawl router-card markers.' - ); - } - return `${existing.slice(0, start)}${ROUTER_CARD}${existing.slice( - end + END_MARKER.length - )}`; - } - - if (!existing) return `${ROUTER_CARD}\n`; - const separator = existing.endsWith('\n\n') - ? '' - : existing.endsWith('\n') - ? '\n' - : '\n\n'; - return `${existing}${separator}${ROUTER_CARD}\n`; -} - -function atomicWrite(destination: string, content: string, mode: number): void { - mkdirSync(path.dirname(destination), { recursive: true }); - const temporary = path.join( - path.dirname(destination), - `.${path.basename(destination)}.${process.pid}.${randomUUID()}.tmp` - ); - let descriptor: number | undefined; - try { - descriptor = openSync(temporary, 'wx', mode); - writeFileSync(descriptor, content, 'utf8'); - fsyncSync(descriptor); - closeSync(descriptor); - descriptor = undefined; - renameSync(temporary, destination); - } finally { - if (descriptor !== undefined) closeSync(descriptor); - rmSync(temporary, { force: true }); - } -} - -export function installRouterCard( - agent: string, - projectPath: string = process.cwd() -): RouterCardResult { - const project = path.resolve(projectPath); - const context = resolveRouterCardContext(agent); - const destination = routerCardPath(project, context); - assertSafeDestination(project, destination); - - const exists = existsSync(destination); - const existing = exists ? readFileSync(destination, 'utf8') : ''; - const contextBase = - !exists && context === 'cursor' ? `${CURSOR_FRONTMATTER}\n\n` : existing; - const updated = updateManagedBlock(contextBase); - if (updated === existing) { - return { - path: destination, - changed: false, - version: ROUTER_CARD_VERSION, - sha256: ROUTER_CARD_SHA256, - }; - } - - const mode = exists ? statSync(destination).mode & 0o777 : 0o644; - atomicWrite(destination, updated, mode); - return { - path: destination, - changed: true, - version: ROUTER_CARD_VERSION, - sha256: ROUTER_CARD_SHA256, - }; -}