Skip to content

Commit 304c07f

Browse files
Know a run will fail before it spends, and one finding refused on its merits (#16)
The remaining seven audit findings. Six fixed; F-07 investigated and DECLINED, which is the part worth reading. F-04 THE VENDOR CLI IS NOW A CONTRACT, CHECKED BEFORE SPENDING The Firecrawl CLI is the one dependency no lockfile pins: installed globally, changeable under a working install, and every adapter test drives a stub - so a payload-shape change is exactly what the suite cannot see. The place it would otherwise surface is mid-collection, after the credits are gone. cliCompatibility() returns three answers, not two: supported the major matches what the adapter was written against (1.23.3) untested no version, or one that will not parse. PROCEEDS - a CLI that prints its version differently is not evidence that scraping is broken, and refusing would strand a working install unsupported a different MAJOR. Refuses in runResearch BEFORE the first request, dry run included, and names --transport http-keyless F-05 AN EXPLICIT SEARCH PROVIDER IS CHECKED FOR READINESS Automatic selection only reaches SerpAPI when a key exists. An explicit choice validated the NAME and not the readiness, so `--search-transport serpapi` with no key was accepted and failed later, after a plan was built. My first fix threw from selectSearch and broke three precedence tests - and they were right: selectSearch is also how --dry-run and doctor ask what WOULD be used, and those must describe a missing key rather than crash. The unreadiness now travels with the selection and runResearch refuses on it, so the report and the refusal happen in the right places. F-07 DECLINED, AND THE REASON MATTERS MORE THAN THE FIX The concern is real: pidAlive() asks whether a process with that NUMBER exists, not whether it holds this lock, so a reused pid can make a dead lock read as held forever. I implemented the proposed bound - one hour - and it broke a test named F13 that backdates a live lock SIX hours and requires it to survive. ADR-0025 decided this deliberately: age-based eviction was tried, and it evicted the CORRECT holder, because a collection legitimately holds the section across many slow requests. Any bound short enough to catch a reused pid is short enough to interleave two collectors, which is the worse failure. So liveness stays authoritative and pid reuse is made DIAGNOSABLE instead: the timeout message names the possibility, prints the lock's age, and gives the platform-correct command to check whether that pid is a research-kit run. That is the audit's own alternative remedy, and the one an operator can act on. F-09 A CREDENTIALED INTEGRATION WORKFLOW, BUDGETED .github/workflows/live-collection.yml. Manual dispatch only - no push, no schedule, no fork trigger. Refuses max_pages outside 1..10 in the job itself, reports the CLI version to the summary before collecting, collects into a scratch project under RUNNER_TEMP, asserts the repository is unchanged afterwards, and uploads only the ledger's SHAPE - op, status, transport, completeness - never the captured pages. F-10 CI READS DATA, NOT PROSE selftest.mjs writes RESEARCH_KIT_RESULT_FILE when asked: passed, failures, unsupported, blocking, exit, seconds, files, node, platform. The summary step parses it, and ABSENCE is now its own signal - "the runner produced no result file: it crashed before reporting" is a different fact from a failing suite, and scraping stdout could not tell them apart. F-11 PREREQUISITES ARE CHECKED, NOT DOCUMENTED lib/runtime.mjs: checkNode (22+), checkGit, checkPython (3.12+, reading BOTH streams because Python 2 printed its version to stderr), and requireRuntime exiting 3 - distinct from a product failure. Per-command rather than one gate: Python is used by the conformance runners and nothing else, and a command that reads the corpus should not refuse over a language it never calls. F-12 A CAPABILITY MAY NOT BE CLAIMED WITHOUT AN ENTRYPOINT The dashboard was specified and deliberately aborted, and its notes survive in the dated record - correctly. The risk is that a future reader adds a command table row from them. A test now refuses any CURRENT document naming dashboard, web UI or live view unless a bin/ entrypoint backs it. History may discuss them freely. VERIFIED node research-kit/bin/selftest.mjs -> 642 passed, 0 failed, exit 0 (630 + 12) node research-kit/bin/preflight.mjs -> PASS, 0 blocking, 0 warnings The result file was checked by writing one and reading it back. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent b6e0d25 commit 304c07f

12 files changed

Lines changed: 639 additions & 9 deletions
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
name: live-collection
2+
3+
# THE ONLY WORKFLOW THAT SPENDS MONEY, AND THE ONLY ONE THAT TOUCHES A VENDOR.
4+
#
5+
# `offline-suite` deliberately cannot reach the network, which is right: a reviewer or a
6+
# fork must be able to re-run every check without being given a key. But that means the
7+
# real network boundary is represented entirely by stubs, and the suite stays green while
8+
# real collection fails on a changed payload, a proxy, a platform TLS quirk, a 429, or a
9+
# partial response.
10+
#
11+
# This exists to exercise that boundary on purpose, with the cost visible and bounded.
12+
#
13+
# It is:
14+
# manual only no push, no pull_request, no schedule - nothing runs it by
15+
# accident, and a fork cannot trigger it
16+
# budgeted `max_pages` is an input with a small default, and the run
17+
# reports the credits it actually spent
18+
# credential-scoped one secret, read from the environment, never written to disk
19+
# corpus-isolated it collects into a scratch project under RUNNER_TEMP and
20+
# commits nothing. The repository's own corpus is evidence for
21+
# its own unknowns, not a place to put test captures
22+
# redacted the key never reaches a log line; the adapter refuses a query
23+
# containing it before transmit, and `command()` renders
24+
# pre-redacted
25+
#
26+
# WHEN TO RUN IT
27+
# - before trusting a Firecrawl CLI version this adapter has not seen
28+
# - after changing anything under lib/firecrawl.mjs, lib/http-transport.mjs,
29+
# lib/serpapi.mjs or lib/collect.mjs
30+
# - when a real collection fails in a way the offline suite cannot reproduce
31+
#
32+
# WHAT A FAILURE HERE MEANS
33+
# The offline suite passing and this failing is the interesting case, and the expected
34+
# one: it means the stubs and the vendor have diverged. Read the version report first.
35+
36+
on:
37+
workflow_dispatch:
38+
inputs:
39+
max_pages:
40+
description: 'Pages to collect (each costs at least one credit)'
41+
required: true
42+
default: '2'
43+
transport:
44+
description: 'Which fetch route to exercise'
45+
required: true
46+
default: 'firecrawl-cli'
47+
type: choice
48+
options: [firecrawl-cli, http-keyless]
49+
50+
permissions:
51+
contents: read
52+
53+
jobs:
54+
live:
55+
name: live
56+
runs-on: ubuntu-latest
57+
timeout-minutes: 15
58+
# A budget nobody can raise by editing an input: the job itself refuses a large run.
59+
environment: live-collection
60+
defaults:
61+
run:
62+
shell: bash
63+
steps:
64+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4, resolved 2026-09-21
65+
66+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4, resolved 2026-09-21
67+
with:
68+
node-version: '22'
69+
70+
- name: refuse an unbounded run
71+
run: |
72+
pages='${{ inputs.max_pages }}'
73+
case "$pages" in (''|*[!0-9]*) echo "max_pages must be a whole number"; exit 1;; esac
74+
if [ "$pages" -lt 1 ] || [ "$pages" -gt 10 ]; then
75+
echo "max_pages=$pages is outside 1..10."
76+
echo "This workflow spends real credits; a large run belongs on a machine where"
77+
echo "somebody is watching the meter, not in CI."
78+
exit 1
79+
fi
80+
echo "budget: $pages page(s)"
81+
82+
- name: install the vendor CLI, and report the version this run used
83+
if: inputs.transport == 'firecrawl-cli'
84+
run: |
85+
npm install -g @mendable/firecrawl-cli >/dev/null 2>&1 || npm install -g firecrawl >/dev/null 2>&1 || true
86+
installed=$(firecrawl --version 2>/dev/null || echo 'not installed')
87+
echo "firecrawl CLI: $installed"
88+
# The version is the first thing to read when this workflow fails, so it goes in
89+
# the summary rather than only in the log.
90+
echo "| firecrawl CLI | \`$installed\` |" >> "$GITHUB_STEP_SUMMARY"
91+
node -e '
92+
import("./research-kit/lib/firecrawl.mjs").then((m) => {
93+
const c = m.cliCompatibility();
94+
console.log(`${c.level}: ${c.detail}`);
95+
if (!c.supported) { console.error(c.remedy); process.exit(1); }
96+
});
97+
'
98+
99+
- name: the key is present, and is never printed
100+
env:
101+
FIRECRAWL_API_KEY: ${{ secrets.FIRECRAWL_API_KEY }}
102+
run: |
103+
if [ -z "${FIRECRAWL_API_KEY:-}" ] && [ '${{ inputs.transport }}' = 'firecrawl-cli' ]; then
104+
echo "FIRECRAWL_API_KEY is not set for this environment."
105+
echo "Add it under Settings > Environments > live-collection, or run with transport=http-keyless."
106+
exit 1
107+
fi
108+
echo "credential present: ${FIRECRAWL_API_KEY:+yes}${FIRECRAWL_API_KEY:-no (keyless route)}"
109+
110+
- name: collect into a scratch project, never the repository's corpus
111+
env:
112+
FIRECRAWL_API_KEY: ${{ secrets.FIRECRAWL_API_KEY }}
113+
RESEARCH_KIT_TRANSPORT: ${{ inputs.transport }}
114+
run: |
115+
set -e
116+
kit="$PWD/research-kit"
117+
scratch="$RUNNER_TEMP/live-project"
118+
mkdir -p "$scratch"
119+
cd "$scratch"
120+
121+
# A scratch project, so nothing this run collects can reach the repository's
122+
# hash-chained ledger. That ledger proves the corpus answers ITS OWN questions;
123+
# a CI capture in it would be evidence of nothing.
124+
node "$kit/bin/new-project.mjs" . --topic "live transport check"
125+
126+
node "$kit/bin/doctor.mjs" || true
127+
node "$kit/bin/research.mjs" --dry-run --depth quick
128+
node "$kit/bin/research.mjs" --depth quick --max-scrapes '${{ inputs.max_pages }}'
129+
130+
echo "--- what it cost ---"
131+
node "$kit/bin/research.mjs" --status || true
132+
133+
- name: the corpus that arrived verifies
134+
env:
135+
RESEARCH_KIT_TRANSPORT: ${{ inputs.transport }}
136+
run: |
137+
cd "$RUNNER_TEMP/live-project"
138+
# The point of collecting at all: a real capture must pass the same checks a
139+
# synthetic one does - chain verifies, every cited capture on disk, bodies
140+
# unmodified.
141+
node "$GITHUB_WORKSPACE/research-kit/bin/handoff.mjs" || true
142+
node "$GITHUB_WORKSPACE/research-kit/bin/preflight.mjs" --json > preflight.json || true
143+
node -e '
144+
const r = JSON.parse(require("fs").readFileSync("preflight.json", "utf8"));
145+
console.log(`preflight: ${r.status ?? r.verdict}`);
146+
const chain = (r.checks ?? []).find((c) => /chain|ledger/.test(c.name ?? ""));
147+
if (chain) console.log(` ${chain.name}: ${chain.status}`);
148+
'
149+
150+
- name: the repository is unchanged
151+
run: |
152+
# The guarantee this workflow rests on. If a live run ever wrote into the
153+
# checkout, every later claim about corpus provenance would be suspect.
154+
if [ -n "$(git status --porcelain)" ]; then
155+
echo "a live collection modified the repository:"
156+
git status --porcelain
157+
exit 1
158+
fi
159+
echo "repository untouched"
160+
161+
- name: sanitized diagnostics
162+
if: always()
163+
run: |
164+
cd "$RUNNER_TEMP/live-project" || exit 0
165+
# Deliberately NOT the captures: they are someone else's pages, and this is a
166+
# public log. Only the shape of what happened.
167+
node -e '
168+
const fs = require("fs");
169+
const led = "research/raw/.fetches.jsonl";
170+
if (!fs.existsSync(led)) { console.log("no ledger written"); process.exit(0); }
171+
const rows = fs.readFileSync(led, "utf8").trim().split("\n").filter(Boolean).map(JSON.parse);
172+
for (const r of rows) {
173+
console.log([r.op, r.statusCode ?? "-", r.transport ?? "-", r.completeness ?? "-", (r.url ?? "").slice(0, 60)].join(" "));
174+
}
175+
' >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || true

‎.github/workflows/offline-suite.yml‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,10 @@ jobs:
112112
echo "working tree is LF"
113113
114114
- name: selftest
115-
run: |
116-
set -o pipefail
117-
node research-kit/bin/selftest.mjs | tee "$RUNNER_TEMP/suite.txt"
115+
env:
116+
# Structured result, so the summary below reads DATA rather than scraping prose.
117+
RESEARCH_KIT_RESULT_FILE: ${{ runner.temp }}/suite-result.json
118+
run: node research-kit/bin/selftest.mjs
118119

119120
# Publish the count where a reviewer can read it WITHOUT running anything.
120121
#
@@ -126,7 +127,20 @@ jobs:
126127
- name: publish the count
127128
if: always()
128129
run: |
129-
line=$(grep -E '^[0-9]+ passed, [0-9]+ failed' "$RUNNER_TEMP/suite.txt" || echo 'suite produced no count')
130+
result="$RUNNER_TEMP/suite-result.json"
131+
if [ -f "$result" ]; then
132+
line=$(node -e '
133+
const r = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
134+
const bits = [`${r.passed} passed`, `${r.failures} failed`];
135+
if (r.unsupported) bits.push(`${r.unsupported} unsupported`);
136+
process.stdout.write(`${bits.join(", ")} in ${r.seconds}s across ${r.files} files (node ${r.node})`);
137+
' "$result")
138+
else
139+
# No file means the runner never reached its own reporting - a startup error,
140+
# an import-time throw, an OOM kill. That is a DIFFERENT fact from a failing
141+
# suite, and scraping stdout could not tell the two apart.
142+
line='the runner produced no result file: it crashed before reporting'
143+
fi
130144
echo "| ${{ matrix.os }} | \`$line\` |" >> "$GITHUB_STEP_SUMMARY"
131145
132146
- name: preflight

‎docs/ARCHITECTURE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ variable (`CLAUDE_SETTINGS_PATH`) without ever honouring it. Owns the machine ro
146146
| `handoff.mjs` | The **arrival question**: `verifyHandoff(root)` reports a ledger that is absent or empty, every capture an evidence row names that is not on disk, and a chain that does not verify — each named. Read-only by design: the machine that asks cannot collect the missing bytes. The report also carries `lineEndings`, the captures whose body hash fails *only* because git rewrote CRLF into them on checkout, and **`handoffRemedy(report)` picks the remedy from the cause** rather than printing one text for every way a corpus can look broken (ADR-0020): `HANDOFF_REMEDY` when something did not travel (the collector must push `research/raw/`, dotfiles included), `lineEndingRemedy` when everything travelled and this machine rewrote it (pin with `.gitattributes`, re-check out, touch no collector and spend no credits), both when a report holds both. Consumed by `bin/handoff.mjs` and doctor (blocker on `role=builder`, silent on a collector; doctor passes the snapshot it already read, so the question is composed once for both). | ADR-0011, ADR-0020 |
147147
| `bundle.mjs` | The **archive this project arrived as**, and how far the tree has moved from it. `BUNDLE_INDEX.md` lists 80 SHA-256 digests taken at the 2026-09-17 handover; until ADR-0028 **nothing had ever read them**, which is how ADR-0022 could treat them as a constraint for three days without noticing the constraint was already violated. `verifyBundle(root)` sorts every entry into unchanged / drifted / unexpected / missing, where "drifted" means a file in `EXPECTED_TO_DRIFT` — the corpus, the map, the ADR index, the manifest itself, and (from 2026-09-20) `CONTEXT.md` and the root `README.md`, which are documentation ABOUT the code and go stale the moment it moves, each of which moves because one of this project's own rules *requires* it to. The list is deliberately narrow: exempting a planning document would leave the report unable to say anything, and a test asserts none is exempted. Hashes **bytes**, not decoded text. Reports; never fails a build — a changed file is a fact about history, and the only thing that makes it a problem is nobody knowing. | ADR-0028 |
148148
| `git-config.mjs`, `config.mjs` | Compatibility shims → `machine.mjs`. | ADR-0002 |
149+
| `runtime.mjs` | The host prerequisites a command actually needs: `checkNode` (22+), `checkGit`, `checkPython` (3.12+, reading either stream because Python 2 printed its version to stderr), and `requireRuntime({ node, git, python })` which exits **3** — distinct from a product failure — naming the missing piece and how to install it. Deliberately per-command rather than one gate that demands everything: Python is used by the cross-language conformance runners and nothing else, and a command that only reads the corpus has no business refusing to run because a language it never calls is absent. | — |
149150
| `scaffold.mjs` | Project shape: `LAYOUT` (12 entries), the **four** `GATE_MARKERS` (pinned by ADR-0001 and its test), template rendering, placeholder validation, hook executability. `.gitattributes` is a LAYOUT entry but **not** a gate marker: a project without it still gates, it just breaks on a Windows builder (ADR-0020). | ADR-0001, ADR-0020 |
150151
| `installer.mjs` | Installs the git hook (records/restores prior `hooksPath`) and the edit-time gate (backs up, repairs known corruption, refuses unfamiliar states). The hook was renamed (A10.4): the installer recognises a registration left at a retired name, replaces it rather than adding a second, and reports the repair — `retiredRepairNote`. | ADR-0012 |
151152
| `render.mjs` | Terminal table rendering for CLI output. | — |
@@ -412,7 +413,7 @@ Counted against the filesystem, not estimated:
412413

413414
| | |
414415
|---|---|
415-
| `lib/` modules | **41** (37, plus the 4 extracted into `lib/release/`) — every one has a row above |
416+
| `lib/` modules | **42** (37, plus the 4 extracted into `lib/release/`) — every one has a row above |
416417
| `bin/` entrypoints | **25**, of which **4 are Python**: 3 conformance runners and the module they share |
417418
| `schemas/` | **11** |
418419
| `conformance/` vector packets | **3** |

‎research-kit/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ node research-kit/bin/selftest.mjs # all of it
188188
node research-kit/bin/selftest.mjs gate hook # just these files
189189
```
190190

191-
630 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
191+
642 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
192192
the assertions settled (ADR-0021), and each test is raced against a watchdog
193193
(`RESEARCH_KIT_TEST_TIMEOUT`, default 60s).
194194

‎research-kit/bin/selftest.mjs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,32 @@ for (const file of files) {
4444
}
4545

4646
const { failures, passed, unsupported, blocking } = await runPending();
47+
48+
/**
49+
* Write the result as DATA, when asked.
50+
*
51+
* CI used to scrape this program's stdout for `N passed, M failed`, which cannot tell a
52+
* failing suite from a suite that died before printing anything - a startup error, an
53+
* import-time throw, an OOM kill and a truncated log all render as "suite produced no
54+
* count". A reviewer then sees a generic message and has to go hunting for the real
55+
* diagnostic.
56+
*
57+
* Absence of this file is itself the signal: if the runner crashed before emission there
58+
* is nothing to read, which is distinguishable from a suite that ran and failed.
59+
*/
60+
function writeResultFile(code) {
61+
const target = process.env.RESEARCH_KIT_RESULT_FILE;
62+
if (!target) return;
63+
try {
64+
fs.writeFileSync(target, `${JSON.stringify({
65+
passed, failures, unsupported: unsupported.length, blocking, exit: code,
66+
seconds: Number(((Date.now() - started) / 1000).toFixed(1)),
67+
files: files.length, node: process.versions.node, platform: process.platform,
68+
}, null, 2)}
69+
`, 'utf8');
70+
} catch { /* a report that cannot be written must not fail the run it reports on */ }
71+
}
72+
4773
const seconds = ((Date.now() - started) / 1000).toFixed(1);
4874

4975
process.stdout.write(`\n${passed} passed, ${failures} failed${unsupported.length ? `, ${unsupported.length} unsupported` : ''} in ${seconds}s (watchdog ${TEST_TIMEOUT}ms/test)\n`);
@@ -55,6 +81,7 @@ if (unsupported.length) {
5581
for (const entry of unsupported) process.stdout.write(` ${entry.code} ${entry.label}\n ${entry.reason}\n`);
5682
}
5783
if (blocking) {
84+
writeResultFile(1);
5885
process.stdout.write(`\nA red suite stops work. cwd: ${process.cwd()}\n`);
5986
process.exit(1);
6087
}
@@ -78,10 +105,12 @@ if (!positional.length) {
78105
`\nresearch-kit/README.md claims ${claim[1]} tests; this run has ${actual}.\n`
79106
+ `A stale count is the first claim a reader checks, and the cheapest one to lose trust over.\n`
80107
+ `Fix: change "${claim[1]} tests, offline" to "${actual} tests, offline" in research-kit/README.md\n`);
108+
writeResultFile(1);
81109
process.exit(1);
82110
}
83111
} catch { /* no README (a scaffolded or partial copy) - nothing to keep honest */ }
84112
}
85113

114+
writeResultFile(0);
86115
process.stdout.write('all tests passed\n');
87116
process.exit(0);

‎research-kit/lib/firecrawl.mjs‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,4 +339,60 @@ export function cliVersion({ execFn = exec, ...opts } = {}) {
339339
return result.ok ? result.stdout.trim() : null;
340340
}
341341

342+
/**
343+
* The version of the Firecrawl CLI this adapter was written and tested against.
344+
*
345+
* The CLI is the one dependency outside this repository's control: no lockfile pins it,
346+
* it is installed globally, and it can change under a working install. This adapter
347+
* expects specific flags, a specific JSON shape, and a specific `--status` rendering, so
348+
* a major-version bump is a payload contract change the tests cannot see - every one of
349+
* them drives a stub.
350+
*/
351+
export const TESTED_CLI_VERSION = '1.23.3';
352+
export const SUPPORTED_CLI_MAJOR = 1;
353+
354+
/**
355+
* What this adapter will do with the installed CLI, and why.
356+
*
357+
* Three answers, deliberately, because "compatible or not" is the wrong shape:
358+
*
359+
* supported the major matches what the adapter was written against
360+
* untested a version could not be read, or does not parse. NOT refused - a CLI that
361+
* prints its version differently is not evidence that scraping is broken,
362+
* and refusing on it would strand a working install
363+
* unsupported a DIFFERENT major. Semver says the payload contract may have changed, and
364+
* the adapter cannot tell a changed shape from an empty result
365+
*
366+
* Only `unsupported` stops a run, and it stops it BEFORE anything is spent. Discovering
367+
* an incompatibility after the credits are gone is the failure worth engineering against.
368+
*/
369+
export function cliCompatibility(version = cliVersion()) {
370+
if (!version) {
371+
return { level: 'untested', version: null, supported: true,
372+
detail: 'the Firecrawl CLI did not report a version; proceeding, since that is not evidence of a broken CLI' };
373+
}
374+
const match = String(version).trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
375+
if (!match) {
376+
return { level: 'untested', version, supported: true,
377+
detail: `could not parse "${version}" as a version; proceeding, and this adapter was tested against ${TESTED_CLI_VERSION}` };
378+
}
379+
const major = Number(match[1]);
380+
if (major !== SUPPORTED_CLI_MAJOR) {
381+
return {
382+
level: 'unsupported', version, supported: false, major,
383+
detail: `Firecrawl CLI ${version} is major ${major}; this adapter was written against `
384+
+ `${TESTED_CLI_VERSION} and expects major ${SUPPORTED_CLI_MAJOR}. A major bump may change the `
385+
+ 'payload shape, the flags, or the status output, and the adapter cannot tell a changed '
386+
+ 'shape from an empty result - so this refuses BEFORE spending credits.',
387+
remedy: `install a ${SUPPORTED_CLI_MAJOR}.x CLI, or use --transport http-keyless, which has no vendor CLI in the route`,
388+
};
389+
}
390+
return {
391+
level: 'supported', version, supported: true, major,
392+
detail: version === TESTED_CLI_VERSION
393+
? `Firecrawl CLI ${version}, the version this adapter is tested against`
394+
: `Firecrawl CLI ${version}; tested against ${TESTED_CLI_VERSION}, same major`,
395+
};
396+
}
397+
342398
export default { name, scrape, search, map, command, status, runScrape };

0 commit comments

Comments
 (0)