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 package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "firecrawl-cli",
"version": "1.19.22",
"version": "1.19.23",
"description": "Command-line interface for Firecrawl. Scrape, crawl, and extract data from any website directly from your terminal.",
"main": "dist/index.js",
"bin": {
Expand Down
31 changes: 30 additions & 1 deletion src/__tests__/commands/monitor.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest';
import { buildCreateBody } from '../../commands/monitor';
import {
buildCreateBody,
hasCreateFlagPayload,
hasUpdateFlagPayload,
} from '../../commands/monitor';

describe('monitor command helpers', () => {
describe('buildCreateBody', () => {
Expand Down Expand Up @@ -91,4 +95,29 @@ describe('monitor command helpers', () => {
});
});
});

describe('stdin payload policy', () => {
it('does not implicitly read stdin when create flags can build a payload', () => {
expect(
hasCreateFlagPayload({
name: 'LLM releases',
goal: 'Track new model launches',
schedule: 'hourly',
queries: ['OpenAI release'],
})
).toBe(true);
});

it('still allows implicit stdin for create when no payload flags are present', () => {
expect(hasCreateFlagPayload({ timezone: 'UTC' })).toBe(false);
});

it('does not implicitly read stdin when update flags can build a payload', () => {
expect(hasUpdateFlagPayload({ state: 'paused' })).toBe(true);
});

it('still allows implicit stdin for update when no update flags are present', () => {
expect(hasUpdateFlagPayload({})).toBe(false);
});
});
});
80 changes: 75 additions & 5 deletions src/commands/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,18 @@ function emit(
* Read a JSON payload from a positional arg or piped stdin.
*
* - `file` is a path to a .json file, or `-` to read stdin explicitly.
* - If `file` is omitted and stdin is a pipe, stdin is used.
* - If `file` is omitted and stdin is a pipe, stdin is used only when the
* caller did not provide enough flags to build a payload.
* - Returns `undefined` when no source is provided — caller falls back to flags.
*/
async function readJsonPayload(file?: string): Promise<unknown | undefined> {
if (file === '-' || (!file && !process.stdin.isTTY)) {
async function readJsonPayload(
file?: string,
options: { allowImplicitStdin?: boolean } = {}
): Promise<unknown | undefined> {
if (
file === '-' ||
(!file && options.allowImplicitStdin === true && !process.stdin.isTTY)
) {
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
const raw = Buffer.concat(chunks).toString('utf-8').trim();
Expand All @@ -114,6 +121,65 @@ async function readJsonPayload(file?: string): Promise<unknown | undefined> {
return undefined;
}

export function hasCreateFlagPayload(opts: {
name?: string;
goal?: string;
cron?: string;
schedule?: string;
timezone?: string;
page?: string;
scrapeUrls?: string[];
crawlUrl?: string;
queries?: string[];
searchWindow?: string;
maxResults?: number;
includeDomains?: string[];
excludeDomains?: string[];
webhookUrl?: string;
webhookEvents?: string[];
email?: string[];
retentionDays?: number;
}): boolean {
return Boolean(
opts.name ||
opts.goal ||
opts.cron ||
opts.schedule ||
opts.page ||
(opts.scrapeUrls && opts.scrapeUrls.length > 0) ||
opts.crawlUrl ||
(opts.queries && opts.queries.length > 0) ||
opts.searchWindow ||
opts.maxResults !== undefined ||
(opts.includeDomains && opts.includeDomains.length > 0) ||
(opts.excludeDomains && opts.excludeDomains.length > 0) ||
opts.webhookUrl ||
(opts.webhookEvents && opts.webhookEvents.length > 0) ||
(opts.email && opts.email.length > 0) ||
opts.retentionDays !== undefined
);
}

export function hasUpdateFlagPayload(opts: {
name?: string;
goal?: string;
cron?: string;
schedule?: string;
timezone?: string;
state?: string;
retentionDays?: number;
}): boolean {
return Boolean(
opts.name ||
opts.goal ||
opts.cron ||
opts.schedule ||
opts.timezone ||
opts.state ||
opts.retentionDays !== undefined
);
}

function parseCommaList(value: string): string[] {
return value
.split(',')
Expand Down Expand Up @@ -340,7 +406,9 @@ whole web each check and alerts on new results matching your --goal (required wi
)
).action(async (file: string | undefined, options) => {
try {
const fromJson = await readJsonPayload(file);
const fromJson = await readJsonPayload(file, {
allowImplicitStdin: !hasCreateFlagPayload(options),
});
const body =
fromJson ??
buildCreateBody({
Expand Down Expand Up @@ -437,7 +505,9 @@ whole web each check and alerts on new results matching your --goal (required wi
.option('--retention-days <n>', 'Snapshot retention window', parseInt)
).action(async (monitorId: string, file: string | undefined, options) => {
try {
const fromJson = await readJsonPayload(file);
const fromJson = await readJsonPayload(file, {
allowImplicitStdin: !hasUpdateFlagPayload(options),
});
let body: Record<string, unknown>;
if (fromJson) {
body = fromJson as Record<string, unknown>;
Expand Down
Loading