Skip to content

fix(firecrawl): unwrap double-data response + Slack DM on approvals - #20

Merged
sebtsang merged 16 commits into
mainfrom
claude/distracted-bohr-d5ff81
May 10, 2026
Merged

sebtsang merged 16 commits into
mainfrom
claude/distracted-bohr-d5ff81

Conversation

@sebtsang

Copy link
Copy Markdown
Owner

Summary

Two related fixes that came out of getting the Composio docs → blog pipeline working end-to-end:

  • Firecrawl integration: was silently returning empty markdown on every successful scrape and failing with code 1810 on tool execution. Both root causes fixed.
  • Slack approval DMs: the existing `sendApprovalDM` helper was wired but never called. Now fires from `raiseApproval` so the founder is pinged in Slack the moment an approval is needed.

What changed

Firecrawl

  • `extractMarkdown` (in lib/personas/researcher/fetch.ts and lib/personas/researcher/company-fetch.ts) now walks up to 3 levels of nested `data` keys. Composio wraps Firecrawl's response, which itself wraps the result, so markdown lives at `r.data.data.markdown` — the previous extractor only unwrapped one level and silently returned undefined on every successful scrape.
  • New scripts/connect-firecrawl.ts creates a properly user-bound Firecrawl connection. Required because API_KEY toolkits don't go through OAuth Connect Link, and connections created via the Composio dashboard playground end up without a userId binding (causes `tools.execute({ userId: "default" })` to return code 1810 "No connected account found").
  • IntegrationFetchError message now points at that script when the failure is `not_connected`.

Slack approval DMs

  • New optional env var `GMAESTRO_SLACK_CHANNEL` (channel id, channel name, or user id for DM). When set, every `raiseApproval` fires a fire-and-forget Slack DM with the approval summary + a link to the dashboard's approval page.
  • Fixed `sendApprovalDM` missing `dangerouslySkipVersionCheck` — Composio was rejecting the SLACK_SEND_MESSAGE call with "Toolkit version not specified".
  • New scripts/connect-slack-channel.ts helps the founder list Slack channels, persist the chosen target to `~/.gmaestro/.env`, and send a verification DM.

Mock-mode polish

  • `fetchResearcherBundleForInput` short-circuits when `GMAESTRO_MOCK_PERSONAS=1` so demo runs complete in ~3s instead of waiting on Firecrawl + Reddit + Perplexity calls the mock persona would have ignored anyway.

Diagnostics

  • Underscore-prefixed scripts (`_test-firecrawl.ts`, `_fix-firecrawl.ts`, `_list-firecrawl-conns.ts`, `_verify-firecrawl-extract.ts`, `_check-slack.ts`, `_slack-tools.ts`) added during debugging. Kept for future Composio integration triage; not user-facing.

Why

Before this PR, every content-pipeline run failed at the researcher step because Firecrawl returned empty markdown that downstream personas couldn't write from. The pipeline cascaded garbage and reported "workflow finished without producing a draft" 5+ minutes in. With these fixes, a real run produced a 2,140-char Firecrawl scrape of the Composio docs URL, and the dispatcher progressed through researcher → strategist → ContentOutline approval (verified live).

Test plan

  • `pnpm typecheck` clean
  • Live Firecrawl scrape via researcher persona returned ~2.1 KB markdown from `docs.composio.dev/toolkits/firecrawl`
  • Run progressed past previously-blocked researcher step; ContentOutline approval reached
  • Slack test DM landed in target channel
  • Mock-mode run (`GMAESTRO_MOCK_PERSONAS=1`) completes end-to-end in ~3s

Notes for reviewer

  • The diagnostic scripts could be deleted later if they become noise — keeping them small and namespaced under `_` prefix.
  • Slack interactivity (Approve/Deny buttons in the DM) is not in this PR; the DM contains a link to the dashboard's approval page. Adding interactive Block Kit buttons needs a public webhook URL + signing secret, which is out of scope for the demo.
  • Writer persona currently fails on a separate JSON-parse issue (Haiku output truncation) that's not addressed here.

🤖 Generated with Claude Code

sebtsang and others added 16 commits May 10, 2026 00:57
Pivots the project from a sales/CS/RevOps GTM team to an AI content team
optimizing for both traditional SEO and Generative Engine Optimization
(GEO — citation by ChatGPT/Perplexity/Claude/Gemini/Google AI Overviews).

Architecture is unchanged: Conductor → Department Heads → Specialists,
Pattern B universal (deterministic Composio fetches → pure-LLM synth),
post-approval deterministic dispatch.

Persona roster (10 total): 5 Content + 2 Distribution + 3 Insight
- Content: researcher / strategist / writer / geo-editor / formatter
- Distribution: pipeline-reporter / slack-digest
- Insight: feedback-tagger / theme-synthesizer / linear-filer

Key pieces:
- lib/personas/researcher/fetch.ts — Pattern B over Reddit/X/Firecrawl/Perplexity
- lib/dispatch/providers.ts — ChannelVariant providers for 7 publish targets
  (GitHub PR, WordPress, Ghost, Notion, Reddit, LinkedIn, X). 4 confirmed
  Composio slugs, WordPress/Ghost TBD, X is BYO dev account.
- lib/ui/components/approval-card.tsx — BlogDraft preview with channels
  checkbox group (founder picks destinations at approval time, dispatcher
  fans out one Formatter call per ticked target).
- lib/shared/auth-configs.ts — Reddit/Twitter/WordPress entries flagged
  TBD pending Foundation registration.

Replaces:
- 6 GTM sales personas (researcher/qualifier/strategist/writer/scheduler/
  brief-writer) — researcher/strategist/writer kept with content-domain
  re-prompts, qualifier/scheduler/brief-writer dropped.
- CS + RevOps departments — replaced with Distribution.
- OutreachDraft/CustomDeal/ActivationNudge/CRMUpdate artifact types —
  replaced with TopicResearchBrief/ContentOutline/BlogDraft/ChannelVariant/
  PublishedArtifact.

Verified:
- pnpm typecheck + pnpm build green
- Mock-mode dashboard renders DAG + BlogDraft approval gate with channels
  picker, no runtime errors
- Live LLM harness: 7/10 personas pass on real Anthropic/Claude OAuth.
  Researcher produces "AI Cold Email Is Dead: Why Founders Who Publish
  Are Winning" as the recommended angle.

Out of scope (next iteration):
- BlogDraft → Formatter fanout dispatch handler in api/approvals/[id]
- WordPress/Ghost Composio slug verification via _probe-mcp-tools
- Strategist prompt budget (occasional 120s timeouts)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merges origin/main into the content pivot branch. The pivot's persona
roster (10 content personas, 3 departments) wins over main's parallel
additive blogs roster (which kept GTM + 6 new content personas alongside).

Resolved in favor of THIS branch:
- PersonaId / Department / ApprovalArtifactType / FanoutSource unions
- 10-persona registry, scopes, prompts (no GTM personas, no Sunny's 6)
- 3-department persona-meta + DAG layout
- Channels-checkbox UX on the BlogDraft approval card
- Connection-meta POPULAR list (publishing-first ordering)

Absorbed from main (additive, complementary):
- CompanyContext system (drizzle/migrations/0003_company_context.sql,
  lib/state/company-context.ts, lib/state/gtm-metrics.ts,
  lib/orchestrator/context-synth.ts, app/api/context/{route,refresh,
  mock/context}/route.ts, lib/ui/components/{company-context-card,
  edit-company-context-dialog}.tsx, lib/ui/hooks/use-company-context.ts)
- ICPProfile / GtmObjective / GtmMetric / CompanyContext types
- CompanyContextSchema + CompanyContextInputSchema
- companyContext Drizzle table
- README/INSTALL updates from main

CompanyContext threading into content personas is the next iteration —
the persona prompts already accept an optional `companyProfile` field
(see lib/state/workflows.ts:421-470). Slice-map for the 5 content personas:
- researcher: companyName, competitors, sourceUrl
- strategist: oneLiner, icp, positioning, valueProps, competitors, voiceTone
- writer: oneLiner, productDescription, valueProps, voiceTone (+ samples)
- geo-editor: oneLiner, valueProps, productDescription
- formatter: companyName, oneLiner, sourceUrl

Verified:
- pnpm typecheck green
- pnpm build green
- All conflict markers resolved

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PITCH.md is now the north-star file. Everything else (slide deck, demo
script, marketing copy, README) derives from it. If a teammate is unsure
where to point a sentence, this is north.

Locked positioning:

  Hero:    "Docs are for parsers. Blogs are for people. We turn one into
           the other — for devtools founders whose engineering velocity
           outruns their marketing."
  Tagline: "One commit. Every channel. All your buyers."
  ICP:     Series A devtools companies whose docs ship daily and blogs
           ship quarterly. Resend, Linear, Vercel, Stripe-shape.
  Theme:   "One for All" — literally restates one approval / all
           destinations / one source / all audiences / one prompt /
           the whole team executing.

Why this niche won the debate (research-driven):
- Anthropic-judged hackathons reward narrow + theatrical demos
  (lawyer beat 500 devs at Cerebral Valley with permit-processing app)
- Profound's $1B Series C owns GEO observability narrative — we don't
  fight there. We're generation, not measurement.
- Jasper/Copy.ai/Writer own broad "AI content" — we don't fight there.
- White space: Notra/PersonaBox/Docsie are adjacent; nobody has
  multi-persona + multi-channel + founder-in-loop.
- Mintlify ($45M Series B, $500M val), Stainless ($25M Series A)
  publicly naming docs-as-marketing-surface — category being established.
- Devtools = highest-WTP B2B niche; $4–12K/mo agency budgets prove buyer
  pays.

Pivot history (locked here for future Claude sessions to find):
- 2026-05-08 — initial: AI GTM team (sales / CS / RevOps)
- 2026-05-09 — pivoted to AI content team (blog / GEO / multi-channel)
- 2026-05-10 — narrowed to devtools docs → multi-channel content. Locked.

CLAUDE.md updated to point Claude Code sessions at PITCH.md as the
first file to read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the freeform-prompt textarea with a structured 3-input form
that locks the workflow to: company URL (voice + product context) +
docs URL (topic source) + destination (single output target). The
freeform path is preserved for backward compat (the persona harness
+ legacy callers still send a `prompt` string).

Why: the founder doesn't have to articulate the workflow — they hand
us the inputs and the team takes over. Voice samples are now
auto-extracted from the company's existing blog instead of pasted
manually at setup. Single destination = single approval gate (no
multi-channel fanout for v1).

Architecture
------------
Pre-fetch (Pattern B, deterministic TS):
  fetchCompanyContextBundle(companyUrl)
    Firecrawl scrape /, /blog (3 most recent posts), compute
    VoiceFingerprint via 10 mechanical rules:
      1. sentence length distribution (mean + stdev)
      2. pronoun mode (we / i / neutral)
      3. hook pattern (anomaly / contrarian / stat-led / announcement)
      4. heading style (topical / question / named-concept)
      5. code block frequency
      6. opinion marker density
      7. banned vocabulary scan (leverage / empower / unlock / etc.)
      8. closing pattern (single-line-punch / wrapping-up / cta-only)
      9. stat density
      10. words-per-section ratio
  fetchDocBundle(docsUrl)
    Firecrawl scrape, return markdown.

Persona pipeline:
  Researcher → Strategist → Writer → GEO-Editor → Formatter
  Single-track. No fanout. Destination locked from form submit.

Approval flow:
  One combined gate after Formatter (formatted-for-destination preview).

Persona prompt updates (research-driven from Composio / Inngest /
Linear / Stripe / Resend / Polar blog analysis):
  - Word count target: 1,800–2,200 for blog-html (devtools mode);
    250 for Reddit; 5–10 tweets for X
  - 3 rhetorical moves locked: failure-mode-first / stat-anchored /
    contrarian-or-anomaly opening
  - Voice rules locked: pronoun mode never mixed; banned marketing
    verbs (leverage/empower/unlock/seamless/robust); aggressive
    sentence-length variation when source company does it
  - Per-destination shape: H2 count, code block count, closing pattern

New files
---------
  lib/personas/researcher/company-fetch.ts
  lib/ui/components/run-input-form.tsx

Modified
--------
  lib/shared/types.ts        — Destination, RunWorkflowInput,
                               VoiceFingerprint, ALL_DESTINATIONS
  lib/shared/schemas.ts      — RunWorkflowRequestSchema accepts new
                               shape; backward-compat with `prompt`
  lib/personas/registry.ts   — researcher input accepts companyUrl /
                               docsUrl / destination; same for
                               strategist, writer, geo-editor
  lib/personas/prompts/researcher.md   — reasons over companyBundle +
                                         docBundle, passes voice
                                         fingerprint downstream
  lib/personas/prompts/strategist.md   — encodes word-count + section-
                                         count targets per destination
                                         + 3 rhetorical moves
  lib/personas/prompts/writer.md       — voice rules locked from
                                         fingerprint; per-destination
                                         output shape
  lib/personas/prompts/formatter.md    — Reddit + X format rules
                                         tightened from research
  lib/state/workflows.ts     — fetchResearcherBundleForInput dispatches
                               to new dual-bundle fetch when companyUrl
                               + docsUrl present; injectItemContext
                               splices runInputs into every persona
                               input; parseRunInputsFromPrompt
                               recovers structured fields from the
                               synthesized prompt string
  app/api/runs/route.ts      — buildStructuredPrompt synthesizes a
                               Conductor-readable prompt from the form
                               payload; legacy email-extraction path
                               only fires when `prompt` is present
  app/(dashboard)/page.tsx   — swap PromptInput for RunInputForm

Verified: pnpm typecheck + pnpm build green; form renders with all 3
inputs + destination radio in mock mode.

Out of scope (roadmap):
  - Multi-channel fanout from a single approval (architecture stays;
    UI is single-destination)
  - Visual theme extraction (colors, fonts) from company URL
  - Outline-then-draft two-stage approval (collapsed to one)
  - Founder-paste voice samples (replaced by auto-extraction)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…formatter

Three fixes from a real-LLM smoke that revealed the workflow could complete
"successfully" with no draft produced and no approval raised — the run
showed state="done" but the founder had nothing to review.

Fix 1: Fail loud on Firecrawl failures
  When the researcher's Pattern B fetch can't reach the docs URL, throw
  IntegrationFetchError with a founder-readable message ("Connect Firecrawl
  on /connections, then retry"). Previously the synthesizer ran on an empty
  docBundle, produced garbage, and the cascade swallowed the failure under
  triggerRule: "all_done". Company-URL fetch failures stay soft (degraded
  voice fingerprint, log warn, continue) since the doc content is the
  load-bearing input.

Fix 2: Single-destination formatter materialization
  The Conductor emits a generic `formatter` task with `fanoutOver: "channels"`
  for multi-channel fanout. WorkContext returns empty channels[] for v1 so
  expandPlan materialized ZERO formatter tasks, meaning the BlogDraft never
  got formatted or surfaced for approval. Now: when the run has a structured
  destination (3-input form path), expandPlan rewrites the formatter template
  as a single non-fanout task with target = destinationToToolkit(destination)
  (blog-html→github, reddit→reddit, x-thread→twitter). Multi-channel fanout
  stays in the architecture for future use.

Fix 3: Approval-gate guard + IntegrationFetchError surfacing
  After the workflow loop, if the plan included an approval-triggering
  persona (geo-editor or formatter) but no approval row was created in
  approval_requests, mark the run failed with a clear reason instead of
  reporting "done" with nothing to review. Also catch IntegrationFetchError
  at the top of runWorkflow and route it through markRunFailed without
  re-throwing — the founder sees the actionable message in the run details.

Adds:
  scripts/_inspect-runs.ts — debug helper to dump recent runs, approvals,
  and event streams from the local DB for triage.

Verified:
  pnpm typecheck + pnpm build green
  Dashboard renders with no new errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Teammates pulling the branch no longer need to manually edit
~/.gmaestro/auth-configs.json — getAuthConfigId() falls back to this
static map. Each person still needs to connect their own Firecrawl API
key on the Composio side; this just tells the app which auth config to
look up at runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ration

The previous 120s ceiling was tuned for the GTM-era short outputs (cold
emails, scheduler payloads). The content pivot's writer + geo-editor +
formatter each generate or edit 1,800–2,200 word blog posts on Sonnet 4.6,
which routinely lands at 90–150s for the writer alone, plus 60–120s for
geo-editing and 30–90s for formatter.

Symptom: a real-LLM run on docs.composio.dev/toolkits/firecrawl finished
with state=failed and the new approval-gate-guard message ("Workflow
finished without producing a draft to approve"). Activity events showed
exact 120s gaps between writer→geo-editor→formatter persona_started
events — Promise.race kills each task right at the budget and the
all_done cascade lets pipeline-reporter / slack-digest keep going.

300s gives long-form personas room to land while still failing fast on
a genuinely hung model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighter generation budget + faster demo. Three places updated:

- strategist.md: word count table, estimatedWordCount example, section
  count math, blog-html override
- writer.md: word count rule, blog-html shape header
- run-input-form.tsx: destination card hint

reddit (250w) and x-thread (5–10 tweets) unchanged — already short.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 4-turn cap was tuned for GTM-era personas (1 tool call + 1 synth
turn). The content pivot's writer + geo-editor + formatter were hitting
error_max_turns: writer especially, since drafting ~1,000 words
sometimes requires the SDK to inject extra rounds for long completions
or extended thinking — even without tools.

Symptom from a real-LLM run on docs.composio.dev:
  writer: error_max_turns: Reached maximum number of turns (4)
  geo-editor + formatter: Unterminated string in JSON (truncated mid-output)

12 gives the SDK headroom while still failing fast on a looping model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous writer prompt treated the docs as side context and the
outline as load-bearing. Real failure mode in production: writer
summarized the doc instead of translating it, output read as AI slop.

Rewrite leads with the killer framing — "translate, don't summarize"
— and reorders inputs by importance: docBundle.markdown FIRST (the
source material to internalize and re-explain), outline SECOND (the
skeleton), voiceFingerprint THIRD (the voice contract).

Added Translation rules (5 specific moves):
  - Open with what changed, not what the doc IS
  - Replace doc-style enumeration with narrative
  - Show why the reader cares before the API
  - Inline code only when load-bearing
  - No hedge words ("might", "could", "may help")

Tightened failure handling:
  - Doc bundle empty → refuse to draft, explicit [DOC FETCH FAILED]
    placeholder pointing the founder to /connections (don't fabricate
    a blog from nothing)
  - Outline empty → fall back to doc-driven structure (still write)
  - voiceFingerprint empty → default founder tone

Voice + rhetorical move sections kept (research-backed); just trimmed
and made more declarative.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Composio's docs (and most modern devtools docs — Mintlify, Docusaurus,
Vercel-hosted) are JS-rendered SPAs. Firecrawl's basic scrape was
returning the JS shell, not the rendered content — researcher saw
status: not_found and the diagnostic surface bailed the run.

Two args added to every FIRECRAWL_SCRAPE call:
  - waitFor: 2500ms — give the page time to hydrate before extraction
  - onlyMainContent: true — strip nav / footer / sidebar boilerplate
    so the markdown is the actual article body

Per-fetch timeout bumped 8s/12s → 25s to accommodate the wait + scrape
+ network round-trip without flapping.

Symptom this resolves:
  researcher: Couldn't fetch the docs URL via Firecrawl (status: not_found)
  on https://docs.composio.dev/toolkits/firecrawl

Should now read the rendered Mintlify page cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cuts to writer wall time:

1. Writer model dropped sonnet 4.6 → haiku 4.5. Sonnet was taking 5+ min
   on 1k-word drafts and blowing past the 300s timeout. Haiku is ~3×
   faster; quality on long-form is weaker but usable when the strategist
   delivers a concrete outline (which our prompts enforce).

2. The synthesized Conductor prompt in app/api/runs/route.ts still said
   "~2,000 words" even though strategist + writer + form UI were all
   updated to 1,000. Synced — the LLM now sees the consistent ~1,000
   word target end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-editor/formatter

Switch back to default triggerRule:all_success so downstream personas
SKIP cleanly when an upstream fails, instead of running on empty input
for 5+ minutes producing [MISSING UPSTREAM DATA] placeholders.

Symptom: when researcher's Firecrawl fetch fails (Mintlify SPA, .md
URL not found, etc.), strategist correctly skips on all_success — but
writer/geo-editor/formatter were marked all_done and ran anyway. Each
spent its full 300s timeout budget trying to fabricate output from
nothing, then formatter died on truncated JSON. The user's read of
the dashboard: "writer running before researcher, strategist skipped
entirely" — symptom of researcher's silent failure (no persona_started
emitted from runtime when Pattern B throws) plus the all_done cascade
making downstream still pop on the activity feed.

The all_done cascade was a GTM-era hack ("ship at least a draft if
research failed"). Now redundant — IntegrationFetchError already loud-
fails Firecrawl issues at the dispatcher level, and the approval-gate
guard catches empty-output runs at the workflow level. No need to
silently cascade.

triggerRule:all_done now reserved for end-of-run reporter personas
(pipeline-reporter, slack-digest in distribution-mgr) which legitimately
should fire regardless of upstream content success.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Composio's docs (https://docs.composio.dev/toolkits/firecrawl) are
Vercel-hosted Next.js client-side-rendered. The SSR'd HTML is just the
JS shell; actual content renders post-hydration. 2.5s wasn't enough —
Firecrawl returned shell HTML, length under our 100-char floor, status
read as not_found.

Per-fetch timeout bumped 25s → 40s to accommodate.

5s covers most slow-hydrating SPAs without making the dispatcher feel
sluggish. If specific docs sites still return shell HTML, bump again
or switch to a server-rendered URL for the demo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ng script

Two issues blocking the docs-scrape persona:

1. Composio wraps Firecrawl's response, which itself wraps the result, so
   markdown lives at r.data.data.markdown — the extractor only walked one
   level and silently returned nothing on every successful scrape. Walk up to
   3 levels in both lib/personas/researcher/fetch.ts and company-fetch.ts.

2. API_KEY toolkits like Firecrawl don't go through the OAuth Connect Link
   flow, so connections created via Composio's dashboard playground end up
   without a userId binding. tools.execute({ userId: "default" }) then
   returns code 1810. Added scripts/connect-firecrawl.ts which uses
   connectedAccounts.initiate() with config.val.generic_api_key to create a
   properly-bound connection. Updated the IntegrationFetchError hint to
   point at the script when status === "not_connected".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When raiseApproval inserts a pending approval, we now fire-and-forget a Slack
DM via the existing sendApprovalDM() helper (which was wired but never
called). Surfaces approval-needed events on Slack with a deep link to the
dashboard's approval page — the founder can drive the workflow from either
surface.

Wiring:

- New optional env var GMAESTRO_SLACK_CHANNEL controls the target channel/
  user ID. Unset = no Slack notifications, dashboard remains the only
  approval surface.
- sendApprovalDM was missing dangerouslySkipVersionCheck, so Composio
  was rejecting the SLACK_SEND_MESSAGE call with "Toolkit version not
  specified". Fixed.
- New scripts/connect-slack-channel.ts helps the founder list their Slack
  channels, write the chosen target into ~/.gmaestro/.env, and send a test
  DM to verify the end-to-end path.

Mock-mode polish: fetchResearcherBundleForInput now short-circuits when
GMAESTRO_MOCK_PERSONAS=1, so a mock-mode demo run completes in ~3s instead
of hanging on Firecrawl + Reddit + Perplexity calls that the mock persona
would have ignored anyway.

Diagnostic scripts (underscore-prefixed, not user-facing) added during the
Firecrawl debugging session — kept for future Composio integration triage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sebtsang
sebtsang merged commit bf96cea into main May 10, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant