diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 000000000..c7608d331 --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,8 @@ +# SonarQube Cloud — Automatic Analysis configuration. +# +# Exclude the vendored, generated Freya runtime bundle from analysis. It is a +# prebuilt third-party artifact (esbuild output; see website/tools/freya-vendor), +# not hand-authored source, so findings on it — e.g. Math.random() used to mint a +# fallback tool-use id — are noise rather than defects in this repo's code. +sonar.exclusions=website/friggframework-api/lib/** +sonar.cpd.exclusions=website/friggframework-api/lib/** diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 000000000..48eb63ac9 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,34 @@ +# Netlify configuration for the friggframework.org marketing site. +# +# The site lives in the website/ subdirectory of the Frigg monorepo, so we set +# the base directory to "website". Per Netlify's file-based configuration, all +# other paths in this file are resolved relative to that base directory. +[build] + base = "website" + publish = "." + functions = "friggframework-api" + +# Ship the vendored Freya bundle and the roadmap catalog JSON alongside the +# functions so the "Ask Freya" assistant can load the runtime and retrieve +# ADR / API data at request time. Paths are relative to the base directory. +[functions] + included_files = ["friggframework-api/lib/**", "roadmap/data/*.json"] + +# Proxy the public API host to the deployed serverless functions. +[[redirects]] + from = 'https://api.friggframework.org/*' + to = '/.netlify/functions/:splat' + status = 200 + +# Feedback host now lives on the roadmap subdomain. +[[redirects]] + from = 'https://feedback.friggframework.org/*' + to = 'https://roadmap.friggframework.org/:splat' + status = 301 + force = true + +[[redirects]] + from = 'http://feedback.friggframework.org/*' + to = 'http://roadmap.friggframework.org/:splat' + status = 301 + force = true diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 000000000..e87ffb3ee --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,8 @@ +# Local Netlify state / build output +.netlify + +# Function dependencies +node_modules + +# Local env +.env diff --git a/website/README.md b/website/README.md new file mode 100644 index 000000000..7a403d27a --- /dev/null +++ b/website/README.md @@ -0,0 +1,66 @@ +# friggframework.org marketing site + +The public marketing site for the Frigg Integration Framework +(https://friggframework.org). It is a single, self-contained static page +(inline CSS + vanilla JS, no build step, no framework runtime) with a small set +of Netlify serverless functions for the newsletter / Slack Connect signup flow. + +Previously this site lived in the `lefthook--demo-frigg-application` repo. It +now lives here in the Frigg core monorepo so the site can be iterated on +alongside the framework and deployed from the `next` branch via Netlify. The +page was rebuilt from the original Bootstrap/jQuery version into a modern, +dependency-free page with a light/dark/system theme toggle and messaging around +the current framework story (agents building integrations, integrations exposed +as MCP tools, owning your own stack). + +## Layout + +| Path | What it is | +|------|------------| +| `index.html` | The entire single-page site — inline CSS + JS, no build step | +| `fonts/webfonts/` | Self-hosted woff2 (Bricolage Grotesque, Hanken Grotesk, JetBrains Mono) | +| `assets/img/` | Logo mark + integration icons used in the marquee | +| `friggframework-api/` | Netlify serverless functions (`subscribe`, `submission-created`) | +| `../netlify.toml` | Netlify build config (base = `website`) — lives at the repo root | + +## Design notes + +- **Theme**: tokens are CSS custom properties on `:root`; `prefers-color-scheme` + sets the default and a header toggle stamps `data-theme="light|dark"` (stored + in `localStorage`) which overrides the media query in both directions. +- **No external requests**: fonts are self-hosted; the only third-party scripts + are the existing Google Analytics + PostHog snippets carried over from the + original site. +- **Motion** (weave canvas, terminal typing, scroll reveals) is disabled under + `prefers-reduced-motion`. +- The signup `
` keeps the exact field names (`email`, `slack-invite`, + `update-emails`) and `form-name` that the `submission-created` function reads, + so the Netlify Forms flow is unchanged. + +## Netlify configuration + +The Netlify build config is at the **repo root** (`../netlify.toml`) with +`base = "website"`, so every path in it is relative to this directory. The +functions directory is `friggframework-api/` and the publish directory is this +folder. + +Redirects: + +- `api.friggframework.org/*` → the deployed serverless functions +- `feedback.friggframework.org/*` → `roadmap.friggframework.org` + +## Local development + +From this directory: + +```bash +npm install # installs the functions' deps (node-fetch, dotenv) +netlify dev # serves the static site + functions locally +``` + +The signup functions require these environment variables (set them in the +Netlify UI or a local `.env`, never commit them): + +- `SLACK_TOKEN` — bot token used to invite signups to Slack Connect +- `SLACK_CONNECT_CHANNEL_ID` — target Slack channel +- `WEBHOOK_URL` — Zapier webhook the signup email is forwarded to diff --git a/website/assets/img/42matters-icon.png b/website/assets/img/42matters-icon.png new file mode 100644 index 000000000..03b08e468 Binary files /dev/null and b/website/assets/img/42matters-icon.png differ diff --git a/website/assets/img/activecampaign-icon.jpeg b/website/assets/img/activecampaign-icon.jpeg new file mode 100644 index 000000000..6c04e4234 Binary files /dev/null and b/website/assets/img/activecampaign-icon.jpeg differ diff --git a/website/assets/img/airwallex-icon.png b/website/assets/img/airwallex-icon.png new file mode 100644 index 000000000..0ce7cfe81 Binary files /dev/null and b/website/assets/img/airwallex-icon.png differ diff --git a/website/assets/img/asana-icon.png b/website/assets/img/asana-icon.png new file mode 100644 index 000000000..ea55278e2 Binary files /dev/null and b/website/assets/img/asana-icon.png differ diff --git a/website/assets/img/attentive-icon.png b/website/assets/img/attentive-icon.png new file mode 100644 index 000000000..c91079439 Binary files /dev/null and b/website/assets/img/attentive-icon.png differ diff --git a/website/assets/img/clyde-icon.png b/website/assets/img/clyde-icon.png new file mode 100644 index 000000000..953b22981 Binary files /dev/null and b/website/assets/img/clyde-icon.png differ diff --git a/website/assets/img/clyde-logo.png b/website/assets/img/clyde-logo.png new file mode 100644 index 000000000..04dafc228 Binary files /dev/null and b/website/assets/img/clyde-logo.png differ diff --git a/website/assets/img/connectwise-icon.jpeg b/website/assets/img/connectwise-icon.jpeg new file mode 100644 index 000000000..b53a7d820 Binary files /dev/null and b/website/assets/img/connectwise-icon.jpeg differ diff --git a/website/assets/img/contentful-icon.png b/website/assets/img/contentful-icon.png new file mode 100644 index 000000000..f5a43c168 Binary files /dev/null and b/website/assets/img/contentful-icon.png differ diff --git a/website/assets/img/contentstack-icon.png b/website/assets/img/contentstack-icon.png new file mode 100644 index 000000000..751bf27c0 Binary files /dev/null and b/website/assets/img/contentstack-icon.png differ diff --git a/website/assets/img/crossbeam-icon.jpeg b/website/assets/img/crossbeam-icon.jpeg new file mode 100644 index 000000000..8d792f7e4 Binary files /dev/null and b/website/assets/img/crossbeam-icon.jpeg differ diff --git a/website/assets/img/crossbeam-logo.png b/website/assets/img/crossbeam-logo.png new file mode 100644 index 000000000..39578ef52 Binary files /dev/null and b/website/assets/img/crossbeam-logo.png differ diff --git a/website/assets/img/deel-icon.png b/website/assets/img/deel-icon.png new file mode 100644 index 000000000..cac1edad1 Binary files /dev/null and b/website/assets/img/deel-icon.png differ diff --git a/website/assets/img/fastspring-icon.jpeg b/website/assets/img/fastspring-icon.jpeg new file mode 100644 index 000000000..4903d9d2a Binary files /dev/null and b/website/assets/img/fastspring-icon.jpeg differ diff --git a/website/assets/img/fastspring-logo.png b/website/assets/img/fastspring-logo.png new file mode 100644 index 000000000..2f5c2b17c Binary files /dev/null and b/website/assets/img/fastspring-logo.png differ diff --git a/website/assets/img/freshbooks-icon.png b/website/assets/img/freshbooks-icon.png new file mode 100644 index 000000000..df63b24a9 Binary files /dev/null and b/website/assets/img/freshbooks-icon.png differ diff --git a/website/assets/img/frigg-favicon.svg b/website/assets/img/frigg-favicon.svg new file mode 100644 index 000000000..5e181299f --- /dev/null +++ b/website/assets/img/frigg-favicon.svg @@ -0,0 +1 @@ + diff --git a/website/assets/img/frigg-hero-compressed.png b/website/assets/img/frigg-hero-compressed.png new file mode 100644 index 000000000..737c030a2 Binary files /dev/null and b/website/assets/img/frigg-hero-compressed.png differ diff --git a/website/assets/img/frigg-icon-white.png b/website/assets/img/frigg-icon-white.png new file mode 100644 index 000000000..8ecae0a1c Binary files /dev/null and b/website/assets/img/frigg-icon-white.png differ diff --git a/website/assets/img/frigg-icon.png b/website/assets/img/frigg-icon.png new file mode 100644 index 000000000..fbe6a5a14 Binary files /dev/null and b/website/assets/img/frigg-icon.png differ diff --git a/website/assets/img/frigg-logo.png b/website/assets/img/frigg-logo.png new file mode 100644 index 000000000..59c1f29e1 Binary files /dev/null and b/website/assets/img/frigg-logo.png differ diff --git a/website/assets/img/frigg-logo.svg b/website/assets/img/frigg-logo.svg new file mode 100644 index 000000000..809a6028a --- /dev/null +++ b/website/assets/img/frigg-logo.svg @@ -0,0 +1 @@ +LEFTHOOKBYI \ No newline at end of file diff --git a/website/assets/img/front-icon.jpeg b/website/assets/img/front-icon.jpeg new file mode 100644 index 000000000..a330267d1 Binary files /dev/null and b/website/assets/img/front-icon.jpeg differ diff --git a/website/assets/img/frontify-icon.png b/website/assets/img/frontify-icon.png new file mode 100644 index 000000000..9e11b9ad6 Binary files /dev/null and b/website/assets/img/frontify-icon.png differ diff --git a/website/assets/img/google-calendar-icon.png b/website/assets/img/google-calendar-icon.png new file mode 100644 index 000000000..62c41f06d Binary files /dev/null and b/website/assets/img/google-calendar-icon.png differ diff --git a/website/assets/img/google-drive-icon.png b/website/assets/img/google-drive-icon.png new file mode 100644 index 000000000..ff92eb767 Binary files /dev/null and b/website/assets/img/google-drive-icon.png differ diff --git a/website/assets/img/gorgias-icon.png b/website/assets/img/gorgias-icon.png new file mode 100644 index 000000000..38d10483f Binary files /dev/null and b/website/assets/img/gorgias-icon.png differ diff --git a/website/assets/img/helpscout-icon.png b/website/assets/img/helpscout-icon.png new file mode 100644 index 000000000..2b6901a47 Binary files /dev/null and b/website/assets/img/helpscout-icon.png differ diff --git a/website/assets/img/hubspot-icon.jpeg b/website/assets/img/hubspot-icon.jpeg new file mode 100644 index 000000000..cef12471d Binary files /dev/null and b/website/assets/img/hubspot-icon.jpeg differ diff --git a/website/assets/img/huggg-icon.png b/website/assets/img/huggg-icon.png new file mode 100644 index 000000000..5f1be9cc4 Binary files /dev/null and b/website/assets/img/huggg-icon.png differ diff --git a/website/assets/img/huggg-logo.svg b/website/assets/img/huggg-logo.svg new file mode 100644 index 000000000..aeb100656 --- /dev/null +++ b/website/assets/img/huggg-logo.svg @@ -0,0 +1 @@ + diff --git a/website/assets/img/ironclad-icon.png b/website/assets/img/ironclad-icon.png new file mode 100644 index 000000000..403782222 Binary files /dev/null and b/website/assets/img/ironclad-icon.png differ diff --git a/website/assets/img/linear-icon.svg b/website/assets/img/linear-icon.svg new file mode 100644 index 000000000..dcee7e736 --- /dev/null +++ b/website/assets/img/linear-icon.svg @@ -0,0 +1 @@ + diff --git a/website/assets/img/marketo-icon.jpeg b/website/assets/img/marketo-icon.jpeg new file mode 100644 index 000000000..e10b2eac1 Binary files /dev/null and b/website/assets/img/marketo-icon.jpeg differ diff --git a/website/assets/img/microsoft-sharepoint-icon.png b/website/assets/img/microsoft-sharepoint-icon.png new file mode 100644 index 000000000..bed8a9035 Binary files /dev/null and b/website/assets/img/microsoft-sharepoint-icon.png differ diff --git a/website/assets/img/microsoft-teams-icon.png b/website/assets/img/microsoft-teams-icon.png new file mode 100644 index 000000000..c9c87bb46 Binary files /dev/null and b/website/assets/img/microsoft-teams-icon.png differ diff --git a/website/assets/img/mondaycom-icon.jpeg b/website/assets/img/mondaycom-icon.jpeg new file mode 100644 index 000000000..5651c6ce7 Binary files /dev/null and b/website/assets/img/mondaycom-icon.jpeg differ diff --git a/website/assets/img/netx-icon.png b/website/assets/img/netx-icon.png new file mode 100644 index 000000000..500011b12 Binary files /dev/null and b/website/assets/img/netx-icon.png differ diff --git a/website/assets/img/outreach-icon.jpeg b/website/assets/img/outreach-icon.jpeg new file mode 100644 index 000000000..321175475 Binary files /dev/null and b/website/assets/img/outreach-icon.jpeg differ diff --git a/website/assets/img/personio-icon.png b/website/assets/img/personio-icon.png new file mode 100644 index 000000000..c2ae4918b Binary files /dev/null and b/website/assets/img/personio-icon.png differ diff --git a/website/assets/img/pipedrive-icon.png b/website/assets/img/pipedrive-icon.png new file mode 100644 index 000000000..334768e59 Binary files /dev/null and b/website/assets/img/pipedrive-icon.png differ diff --git a/website/assets/img/quickbooks-icon.svg b/website/assets/img/quickbooks-icon.svg new file mode 100644 index 000000000..2cb159b4c --- /dev/null +++ b/website/assets/img/quickbooks-icon.svg @@ -0,0 +1 @@ +-icon-color diff --git a/website/assets/img/revio-icon.jpeg b/website/assets/img/revio-icon.jpeg new file mode 100644 index 000000000..ea9ecb5d7 Binary files /dev/null and b/website/assets/img/revio-icon.jpeg differ diff --git a/website/assets/img/rollworks-icon.jpeg b/website/assets/img/rollworks-icon.jpeg new file mode 100644 index 000000000..5e80313fe Binary files /dev/null and b/website/assets/img/rollworks-icon.jpeg differ diff --git a/website/assets/img/salesforce-icon.jpeg b/website/assets/img/salesforce-icon.jpeg new file mode 100644 index 000000000..76a18d2cc Binary files /dev/null and b/website/assets/img/salesforce-icon.jpeg differ diff --git a/website/assets/img/salesloft-icon.png b/website/assets/img/salesloft-icon.png new file mode 100644 index 000000000..969afb631 Binary files /dev/null and b/website/assets/img/salesloft-icon.png differ diff --git a/website/assets/img/slack-icon.jpeg b/website/assets/img/slack-icon.jpeg new file mode 100644 index 000000000..49bdb6bd1 Binary files /dev/null and b/website/assets/img/slack-icon.jpeg differ diff --git a/website/assets/img/slack-icon.svg b/website/assets/img/slack-icon.svg new file mode 100644 index 000000000..c37dc5eb4 --- /dev/null +++ b/website/assets/img/slack-icon.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/assets/img/stack-icon.jpeg b/website/assets/img/stack-icon.jpeg new file mode 100644 index 000000000..9384b2720 Binary files /dev/null and b/website/assets/img/stack-icon.jpeg differ diff --git a/website/assets/img/stripe-icon.png b/website/assets/img/stripe-icon.png new file mode 100644 index 000000000..96fcc763d Binary files /dev/null and b/website/assets/img/stripe-icon.png differ diff --git a/website/assets/img/terminus-icon.png b/website/assets/img/terminus-icon.png new file mode 100644 index 000000000..57b3ec42e Binary files /dev/null and b/website/assets/img/terminus-icon.png differ diff --git a/website/assets/img/unbabel-icon.png b/website/assets/img/unbabel-icon.png new file mode 100644 index 000000000..2a8579aea Binary files /dev/null and b/website/assets/img/unbabel-icon.png differ diff --git a/website/assets/img/yotpo-icon.png b/website/assets/img/yotpo-icon.png new file mode 100644 index 000000000..86c5cb5eb Binary files /dev/null and b/website/assets/img/yotpo-icon.png differ diff --git a/website/assets/img/zoho-icon.png b/website/assets/img/zoho-icon.png new file mode 100644 index 000000000..e90cb7784 Binary files /dev/null and b/website/assets/img/zoho-icon.png differ diff --git a/website/assets/img/zoom-icon.png b/website/assets/img/zoom-icon.png new file mode 100644 index 000000000..2919fa616 Binary files /dev/null and b/website/assets/img/zoom-icon.png differ diff --git a/website/fonts/webfonts/bricolage-grotesque.woff2 b/website/fonts/webfonts/bricolage-grotesque.woff2 new file mode 100644 index 000000000..fcc4eb1eb Binary files /dev/null and b/website/fonts/webfonts/bricolage-grotesque.woff2 differ diff --git a/website/fonts/webfonts/hanken-grotesk.woff2 b/website/fonts/webfonts/hanken-grotesk.woff2 new file mode 100644 index 000000000..518e2c462 Binary files /dev/null and b/website/fonts/webfonts/hanken-grotesk.woff2 differ diff --git a/website/fonts/webfonts/jetbrains-mono.woff2 b/website/fonts/webfonts/jetbrains-mono.woff2 new file mode 100644 index 000000000..4d09cda4a Binary files /dev/null and b/website/fonts/webfonts/jetbrains-mono.woff2 differ diff --git a/website/friggframework-api/assistant.js b/website/friggframework-api/assistant.js new file mode 100644 index 000000000..90a88d378 --- /dev/null +++ b/website/friggframework-api/assistant.js @@ -0,0 +1,322 @@ +// Freya — the Frigg site assistant. A small concierge that answers questions about the +// Frigg framework, helps sketch an integration, and points people around the +// roadmap and Left Hook. +// +// Runs on the Netlify AI Gateway: Netlify injects ANTHROPIC_API_KEY and +// ANTHROPIC_BASE_URL at build/deploy time, and the Anthropic SDK's `new +// Anthropic()` picks both up with no extra config. If those are absent (local +// dev without the gateway, or the gateway not enabled), the function returns a +// friendly "assistant is offline" response so the widget degrades gracefully. +// +// Freya seam: Left Hook's companion agent framework (Freya) will eventually +// drive this endpoint with tool use and richer grounding. Everything the model +// needs today flows through `buildSystemPrompt()` and the single messages.create +// call in `answer()`. When Freya lands, swap `answer()` for a Freya session and +// keep this handler's request/response contract unchanged. Look for FREYA-SEAM. + +const { getStore } = require('@netlify/blobs'); + +const CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST,OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', +}; + +// Model is env-overridable so the operator can pick a cost/quality point +// without a code change. Default is Opus for the best answers. For a public, +// high-traffic widget you'll likely want to set ASSISTANT_MODEL to +// claude-haiku-4-5-20251001 (fast + cheap, ideal for short grounded Q&A) or +// claude-sonnet-4-5-20250929 to keep AI Gateway credit burn down. +const MODEL = process.env.ASSISTANT_MODEL || 'claude-opus-4-8'; +const MAX_TOKENS = 900; + +// Rate limiting: a rolling per-IP window backed by Netlify Blobs. Keeps a +// single visitor (or a hot loop) from running up gateway credits. Generous +// enough for real conversation, tight enough to matter. +const RATE_LIMIT_MAX = 12; // requests +const RATE_LIMIT_WINDOW_MS = 60 * 1000; // per minute +const MAX_MESSAGES = 12; // trim conversation history sent to the model +const MAX_CHARS = 4000; // per-message input clamp + +function clientIp(event) { + const xff = event.headers['x-nf-client-connection-ip'] || + event.headers['x-forwarded-for'] || ''; + return (xff.split(',')[0] || 'unknown').trim(); +} + +async function checkRateLimit(ip) { + // Best-effort: if Blobs is unavailable, fail open rather than block chat. + try { + const store = getStore('assistant-rate'); + const key = `rl:${ip}`; + const raw = await store.get(key, { type: 'json' }); + const now = Date.now(); + const hits = (raw && Array.isArray(raw.hits) ? raw.hits : []) + .filter((t) => now - t < RATE_LIMIT_WINDOW_MS); + if (hits.length >= RATE_LIMIT_MAX) { + return { ok: false, retryAfter: Math.ceil(RATE_LIMIT_WINDOW_MS / 1000) }; + } + hits.push(now); + await store.setJSON(key, { hits }); + return { ok: true }; + } catch (e) { + console.log('rate-limit store unavailable, failing open:', e.message); + return { ok: true }; + } +} + +// Condensed, curated grounding. Deliberately not the full 224-API catalog or +// all 27 ADRs — the assistant points people at /roadmap/ for the searchable +// directory rather than reciting it. Keeps the prompt tight and the answers +// honest about where the authoritative lists live. +const KNOWLEDGE = ` +# Frigg framework — quick facts + +Frigg is an open-source, serverless-native framework for building direct/native +integrations. The pitch: stand up an integration in minutes, get to production +in about a day. It is maintained by Left Hook and is at v2.0.0-next (the "next" +pre-release line). GitHub: https://github.com/friggframework/frigg. Docs: +https://docs.friggframework.org. + +## The point of view +An integration is more than moving data between systems. Sometimes an +integration moves no data at all. Frigg exposes integration *primitives* to +developers and their agents: +- Endpoint — spin up an ad hoc HTTP path with a simple route definition. +- Queue — add async/background processing with a quick annotation (AWS SQS). +- Provider-native — Frigg is the active backend for platforms like Attio, + HubSpot, Zapier, Zendesk, and Salesforce, and the API modules ship helpers to + get your platform-specific code right. +- Fenestra — in-app UI experiences. Fenestra is a new spec Left Hook is + introducing to sit alongside the industry specs Frigg already speaks: OpenAPI + (OAS), AsyncAPI, Arazzo, Overlays, JSON Schema, and MCP. + +## What it's built on +- Runtime: Node.js 22+, JavaScript, AWS Lambda. +- Packaging/deploy: Serverless Framework fork (osls) + esbuild, generating + serverless.yml and CloudFormation. +- Database: PostgreSQL OR MongoDB, via Prisma (two schemas ship). +- Encryption: field-level, AWS KMS OR AES-256 (auto-bypassed in dev/test/local). +- Async: AWS SQS queues; EventBridge Scheduler + cron for scheduled jobs. +- Config/secrets: SSM Parameter Store + Secrets Manager. +- HTTP: Express via serverless-http. +- Auth: OAuth2, API-key, and Basic across API modules. +- UI/forms: JSON Schema (JSONForms) with a React/Vite management UI. +- Observability: OpenTelemetry is natively supported (traces + metrics, OTLP + exporters) — vendor-neutral, no lock-in. +- Testing: Jest, nock, in-memory database. +- Architecture: hexagonal / DDD (handlers -> use cases -> repositories). +Where it says OR, that is a real choice the adopter makes, not a default. + +## Cloud + infrastructure-as-code +Cloud-agnostic by design. Today it largely runs on AWS, and adopters have also +deployed to GCP, Azure, and local clouds via container. "Infrastructure as code" +here means the Frigg app definition self-scaffolds its own resources as it needs +them — you define the integrations you want, and the infrastructure generates +from that definition. You own the stack and own your own pipes. + +## Scaffolding an integration (the short version) +1. frigg init my-app — create a new Frigg app. +2. frigg install — pull a pre-built API module (e.g. frigg install + hubspot). frigg search to find one. +3. Write an integration class extending IntegrationBase. Key hooks: authRequest + (OAuth/API-key flow), loadForm/onFormSubmit (dynamic forms), onchange + (watched-field webhooks), processJob (background work). +4. Wire events: USER_ACTION, CRON, QUEUE, WEBHOOK handlers on this.events. +5. frigg start for local dev (hot reload, Docker + DB pre-flight checks). +6. frigg deploy --stage dev|prod. +The frigg CLI also has an authenticator (frigg auth test .) to try OAuth/API-key +flows without deploying anything. + +## The catalog + roadmap +Left Hook tracks a large catalog of APIs across many platforms; a subset already +have built API modules in the api-module-library and the rest are candidates. +For exact counts, categories, which modules are built, and specific ADRs, use the +catalog_stats / search_apis / search_adrs tools — those are authoritative and +current. The full searchable directory plus the roadmap (built from the ADRs) +lives at /roadmap/ on this site, with community voting; point people there rather +than listing everything. To request a new API module, there is a "Request" link +on each candidate that opens a GitHub issue. + +## Roadmap themes (high level; use search_adrs for specifics) +- Agent tooling: Capabilities (typed declarations of what a module/integration + can do, pointing at spec + implementation), Ontology (layered versioned + context compiled into an XML block agents see at session start), Integration + Templates (category base classes you copy into your codebase, ShadCN-style), + Agent Harness (wires ontology + capabilities + plugins + templates into a + coding-agent session), and Evals. +- Extensions & plugins: Plugins are swappable infrastructure (provider, + database, encryption, queue, scheduler); Extensions are optional add-on code + (core, integration, and API-module extensions); Artifacts are provider-side + code Frigg helps generate. +- Also: telemetry/usage tracking (OpenTelemetry), schema + integration-version + migrations, management UI, and infra/deploy work (e.g. SSM offload). + +## Commercial +Frigg is open source. Left Hook also offers a commercial license for teams who +want commercial support and maintenance, plus premium / heavy-duty connectors, +plugins, and extensions. + +## What people use it for +Frigg is agnostic about the use case. Most early adopters build native +integrations for their own end customers. Some run their internal business +process automations on it. The maintainer also runs personal home-lab and home +automation projects on it. If it involves talking to another system, it fits. + +# Left Hook — who's behind Frigg +Left Hook are integration experts for the modern software stack: integration +consulting, development, and automation for businesses of all sizes. They +maintain Frigg. Partnerships include HubSpot (Solutions Partner) and Zapier +(Certified Expert). They serve verticals like legal, insurance, financial +services, healthcare, distribution, community banks, nonprofits, executive +search, and commercial landscaping. Featured work includes Docusign and a 9+ +year partnership with FreshBooks. For commercial help, point people at the +"Talk to Left Hook" contact on this site. +`.trim(); + +function buildSystemPrompt() { + return `You are Freya, the assistant on the Frigg framework website — a +concise, friendly guide to Frigg (an open-source serverless integration +framework maintained by Left Hook). If someone asks your name, you're Freya. + +Your jobs, in one voice: +1. Answer questions about the Frigg framework — what it is, how it works, the + stack, and the concepts. +2. Help someone sketch how they'd build a specific integration in Frigg (which + primitive, which API module, roughly which hooks). Give a short, concrete + starting point, not a full tutorial. +3. Act as a roadmap concierge — help people find APIs, ADRs, and what's planned, + and send them to /roadmap/ for the searchable directory and voting. +4. Share helpful context about Left Hook when it's relevant. + +Rules: +- You have live retrieval tools over the roadmap catalog: catalog_stats (ADR / + API counts and categories), search_adrs (architecture decision records), and + search_apis (the 224-module API catalog, incl. which are already built). For + ANY question about specific ADRs, API modules, catalog counts, or what's built + vs. planned, call the tool and answer from what it returns — do not guess or + recite from memory. Everything else is grounded in the reference below. +- If something isn't covered by a tool or the reference, say so plainly and point + to the docs (https://docs.friggframework.org), the GitHub repo, or /roadmap/ + rather than inventing specifics. +- Never invent API module names, ADR numbers, config keys, or version numbers. + When someone wants the full API list, send them to /roadmap/. +- Keep answers short and scannable. A few sentences or a tight list. This is a + chat widget, not a doc page. Use short code snippets only when they genuinely + help (e.g. frigg install ...). +- Match the site's voice: direct and plain. No hype, no "this changes + everything," go easy on em-dashes. +- You don't have access to a user's account, private data, or the ability to run + commands or deploy. You give guidance and pointers. + +Reference material: +${KNOWLEDGE}`; +} + +function sanitizeMessages(raw) { + if (!Array.isArray(raw)) return []; + return raw + .filter((m) => m && (m.role === 'user' || m.role === 'assistant') && + typeof m.content === 'string' && m.content.trim()) + .slice(-MAX_MESSAGES) + .map((m) => ({ + role: m.role, + content: m.content.slice(0, MAX_CHARS), + })); +} + +// Roadmap retrieval data the Freya tools query at request time. Loaded from the +// committed catalog JSON via static require so Netlify's bundler ships the files; +// if it's ever unavailable the agent still runs and leans on the static prompt. +let ROADMAP_DATA = null; +function loadRoadmapData() { + if (ROADMAP_DATA) return ROADMAP_DATA; + try { + ROADMAP_DATA = { + adrs: require('../roadmap/data/adrs.json'), + apis: require('../roadmap/data/apis.json'), + }; + } catch (e) { + console.log('roadmap data unavailable:', e && e.message ? e.message : e); + ROADMAP_DATA = { adrs: [], apis: {} }; + } + return ROADMAP_DATA; +} + +// FREYA-SEAM: a turn now runs through the vendored Freya runtime — a tool-using +// agent that retrieves ADR / API-catalog facts at request time instead of a +// single grounded Messages call. The (messages) -> string contract, the handler, +// the rate limiter, and the offline path are all unchanged. The self-contained +// bundle lives at ./lib/freya-runtime.mjs (regenerate via website/tools/freya-vendor). +let freyaModule = null; +async function answer(messages) { + // Lazy dynamic import (ESM bundle from CJS) so a load failure never breaks + // the offline path, matching how the SDK was lazily required before. + if (!freyaModule) freyaModule = await import('./lib/freya-runtime.mjs'); + const reply = await freyaModule.runTurn({ + systemPrompt: buildSystemPrompt(), + model: MODEL, + messages, + data: loadRoadmapData(), + }); + return (reply && reply.trim()) || "I didn't catch that. Could you rephrase?"; +} + +const OFFLINE_REPLY = + "The assistant is offline right now. In the meantime: the docs are at " + + "https://docs.friggframework.org, the code is at " + + "https://github.com/friggframework/frigg, and the roadmap and API directory " + + "are at /roadmap/ on this site."; + +exports.handler = async function (event) { + if (event.httpMethod === 'OPTIONS') { + return { statusCode: 200, headers: CORS_HEADERS }; + } + if (event.httpMethod !== 'POST') { + return { + statusCode: 405, + headers: { ...CORS_HEADERS, 'content-type': 'application/json' }, + body: JSON.stringify({ error: 'method not allowed' }), + }; + } + + const json = (status, obj) => ({ + statusCode: status, + headers: { ...CORS_HEADERS, 'content-type': 'application/json' }, + body: JSON.stringify(obj), + }); + + // No gateway configured -> degrade gracefully, don't 500. + if (!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_BASE_URL) { + return json(200, { reply: OFFLINE_REPLY, offline: true }); + } + + let payload; + try { + payload = JSON.parse(event.body || '{}'); + } catch (e) { + return json(400, { error: 'invalid JSON' }); + } + + const messages = sanitizeMessages(payload.messages); + if (!messages.length || messages[messages.length - 1].role !== 'user') { + return json(400, { error: 'expected a non-empty messages array ending with a user turn' }); + } + + const rate = await checkRateLimit(clientIp(event)); + if (!rate.ok) { + return json(429, { + reply: "You're going a little fast for me. Give it a few seconds and try again.", + rateLimited: true, + retryAfter: rate.retryAfter, + }); + } + + try { + const reply = await answer(messages); + return json(200, { reply }); + } catch (e) { + console.log('assistant error:', e && e.message ? e.message : e); + return json(200, { reply: OFFLINE_REPLY, offline: true }); + } +}; diff --git a/website/friggframework-api/lib/freya-runtime.mjs b/website/friggframework-api/lib/freya-runtime.mjs new file mode 100644 index 000000000..22a447ec7 --- /dev/null +++ b/website/friggframework-api/lib/freya-runtime.mjs @@ -0,0 +1,2714 @@ +// GENERATED — vendored Freya runtime. Do not edit by hand. +// Regenerate via website/tools/freya-vendor/build.mjs. + +// ../freya/packages/core/dist/domain/model/Agent.js +function createAgent(config, deploymentId) { + return { + id: config.id, + config, + deploymentId, + createdAt: /* @__PURE__ */ new Date(), + updatedAt: /* @__PURE__ */ new Date() + }; +} + +// ../freya/packages/core/dist/domain/model/Session.js +function createSession(id, agentId, userId, transportId) { + return { + id, + agentId, + userId, + transportId, + messages: [], + status: "active", + turnCount: 0, + createdAt: /* @__PURE__ */ new Date(), + updatedAt: /* @__PURE__ */ new Date(), + metadata: {} + }; +} +function addMessage(session, message) { + return { + ...session, + messages: [...session.messages, message], + turnCount: message.role === "assistant" ? session.turnCount + 1 : session.turnCount, + updatedAt: /* @__PURE__ */ new Date() + }; +} + +// ../freya/packages/core/dist/domain/model/Message.js +function createUserMessage(id, content, transportOrigin) { + return { + id, + role: "user", + content, + timestamp: /* @__PURE__ */ new Date(), + transportOrigin, + metadata: {} + }; +} +function createAssistantMessage(id, content, toolInvocations) { + return { + id, + role: "assistant", + content, + timestamp: /* @__PURE__ */ new Date(), + transportOrigin: "agent", + toolInvocations, + metadata: {} + }; +} + +// ../freya/packages/core/dist/domain/events/DomainEvent.js +function createEvent(id, type, agentId, payload, sessionId) { + return { id, type, timestamp: /* @__PURE__ */ new Date(), agentId, sessionId, payload }; +} + +// ../freya/packages/core/dist/domain/services/ContextBuilderService.js +function buildContext(params) { + const { config, ontology, memories, messages, tools, ontologyRenderer, transport } = params; + const parts = []; + parts.push(config.systemPrompt); + if (transport) { + parts.push(` + +[Channel: ${transport}]`); + } + const ontologyText = ontologyRenderer.render(ontology); + if (ontologyText && ontology.entityTypes.length > 0) { + parts.push("\n---\n"); + parts.push(ontologyText); + } + if (memories.length > 0) { + parts.push("\n---\n# Relevant Context from Memory\n"); + for (const memory of memories) { + parts.push(`[${memory.entityType}] ${memory.content}`); + } + } + const systemPrompt = parts.join("\n"); + const systemTokens = Math.ceil(systemPrompt.length / 4); + const messageTokens = messages.reduce((sum, m) => sum + Math.ceil(m.content.length / 4), 0); + const toolTokens = tools.reduce((sum, t) => sum + Math.ceil(JSON.stringify(t.inputSchema).length / 4), 0); + return { + systemPrompt, + messages, + tools, + tokenEstimate: systemTokens + messageTokens + toolTokens + }; +} + +// ../freya/packages/core/dist/domain/services/ModelPricingService.js +var MODEL_PRICING_PER_1M_TOKENS = { + "claude-sonnet-4-6": [3, 15], + "claude-opus-4-6": [15, 75], + "claude-haiku-4-5-20251001": [0.25, 1.25], + // Bedrock-prefixed deployment-surface ids. Real Bedrock model-id + // strings vary by AWS region and may carry different date suffixes — + // callers should normalize before calling, or unknown ids will return + // 0 (callers can detect via `isKnownPricedModel`). + "anthropic.claude-sonnet-4-6-20251022-v1:0": [3, 15], + "anthropic.claude-opus-4-6-20251022-v1:0": [15, 75], + "anthropic.claude-haiku-4-5-20251001-v1:0": [0.25, 1.25] +}; +var CACHE_WRITE_MULTIPLIER = 1.25; +var CACHE_READ_MULTIPLIER = 0.1; +function estimateCostUSD(modelId, usageOrInputTokens, outputTokens) { + const usage = typeof usageOrInputTokens === "number" ? { inputTokens: usageOrInputTokens, outputTokens: outputTokens ?? 0 } : usageOrInputTokens; + const rates = MODEL_PRICING_PER_1M_TOKENS[modelId]; + if (!rates) + return 0; + const [inputRate, outputRate] = rates; + const safe = (v) => Math.max(0, Number.isFinite(v) ? v : 0); + const safeInput = safe(usage.inputTokens); + const safeOutput = safe(usage.outputTokens); + const safeCacheRead = safe(usage.cacheReadTokens); + const safeCacheWrite = safe(usage.cacheWriteTokens); + const totalUsdPer1M = safeInput * inputRate + safeOutput * outputRate + safeCacheRead * inputRate * CACHE_READ_MULTIPLIER + safeCacheWrite * inputRate * CACHE_WRITE_MULTIPLIER; + return totalUsdPer1M / 1e6; +} + +// ../freya/packages/core/dist/domain/services/TurnBudgetService.js +var LEGACY_INPUT_RATE_PER_1K = 3e-3; +var LEGACY_OUTPUT_RATE_PER_1K = 0.015; +function createBudgetTracker(budget, optionsOrClock = {}) { + const options = "now" in optionsOrClock && typeof optionsOrClock.now === "function" ? { clock: optionsOrClock } : optionsOrClock; + const clock = options.clock ?? { now: () => Date.now() }; + const modelId = options.modelId; + let calls = 0; + let totalTokens = 0; + let totalInputTokens = 0; + let totalOutputTokens = 0; + let totalCacheReadTokens = 0; + let totalCacheWriteTokens = 0; + const startTime = clock.now(); + const sanitize = (v) => Math.max(0, Number.isFinite(v) ? v : 0); + return { + recordCall(usage) { + calls++; + const inputTokens = sanitize(usage.inputTokens); + const outputTokens = sanitize(usage.outputTokens); + const cacheReadTokens = sanitize(usage.cacheReadTokens); + const cacheWriteTokens = sanitize(usage.cacheWriteTokens); + totalInputTokens += inputTokens; + totalOutputTokens += outputTokens; + totalCacheReadTokens += cacheReadTokens; + totalCacheWriteTokens += cacheWriteTokens; + totalTokens += inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens; + }, + isExhausted() { + return this.getStatus().exhausted; + }, + getStatus() { + const elapsedMs = clock.now() - startTime; + const estimatedCostUsd = modelId !== void 0 ? estimateCostUSD(modelId, { + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + cacheReadTokens: totalCacheReadTokens, + cacheWriteTokens: totalCacheWriteTokens + }) : totalInputTokens * LEGACY_INPUT_RATE_PER_1K / 1e3 + totalOutputTokens * LEGACY_OUTPUT_RATE_PER_1K / 1e3; + let exhausted = false; + let exhaustedReason; + if (budget.maxCalls !== void 0 && calls >= budget.maxCalls) { + exhausted = true; + exhaustedReason = `LLM call limit reached (${calls}/${budget.maxCalls})`; + } else if (budget.maxTokens !== void 0 && totalTokens >= budget.maxTokens) { + exhausted = true; + exhaustedReason = `Token limit reached (${totalTokens}/${budget.maxTokens})`; + } else if (budget.maxTimeMs !== void 0 && elapsedMs >= budget.maxTimeMs) { + exhausted = true; + exhaustedReason = `Time limit reached (${elapsedMs}ms/${budget.maxTimeMs}ms)`; + } else if (budget.maxCostUsd !== void 0 && estimatedCostUsd >= budget.maxCostUsd) { + exhausted = true; + exhaustedReason = `Cost limit reached ($${estimatedCostUsd.toFixed(4)}/$${budget.maxCostUsd})`; + } + return { + calls, + tokens: totalTokens, + timeMs: elapsedMs, + estimatedCostUsd, + exhausted, + ...exhaustedReason ? { exhaustedReason } : {} + }; + } + }; +} +function budgetFromMaxTurns(maxTurns) { + return { maxCalls: maxTurns }; +} + +// ../freya/packages/core/dist/domain/services/HooksService.js +var DEFAULT_PRIORITY = 100; +var InMemoryHookRegistry = class { + byPhase = /* @__PURE__ */ new Map(); + register(hook) { + const list = this.byPhase.get(hook.phase) ?? []; + if (list.some((h) => h.name === hook.name)) { + throw new Error(`Hook with name "${hook.name}" is already registered for phase "${hook.phase}"`); + } + list.push(hook); + this.byPhase.set(hook.phase, list); + } + unregister(name) { + for (const [phase, list] of this.byPhase) { + const filtered = list.filter((h) => h.name !== name); + if (filtered.length !== list.length) { + this.byPhase.set(phase, filtered); + } + } + } + hooksFor(phase) { + const list = this.byPhase.get(phase) ?? []; + const sorted = [...list].sort((a, b) => (a.priority ?? DEFAULT_PRIORITY) - (b.priority ?? DEFAULT_PRIORITY)); + return sorted; + } +}; +var HookExecutionError = class extends Error { + hookName; + phase; + cause; + constructor(hookName, phase, cause) { + const message = cause instanceof Error ? cause.message : String(cause); + super(`Hook "${hookName}" failed in phase "${phase}": ${message}`); + this.hookName = hookName; + this.phase = phase; + this.cause = cause; + this.name = "HookExecutionError"; + } +}; +function makeFireHook(deps) { + const annotate = deps.onAnnotation ?? (() => { + }); + return async function fire(phase, initialPayload) { + const hooks = deps.registry.hooksFor(phase); + let payload = initialPayload; + for (const hook of hooks) { + const ctx = { + phase, + agent: deps.agent, + sessionId: deps.sessionId, + turnId: deps.turnId, + userContext: deps.userContext, + payload, + emit: deps.onEvent, + annotate + }; + let outcome; + try { + outcome = await hook.run(ctx); + } catch (err) { + throw new HookExecutionError(hook.name, phase, err); + } + if (outcome.kind === "short_circuit") { + deps.onEvent(createEvent(crypto.randomUUID(), "turn.short_circuited", deps.agent.id, { hookName: hook.name, phase, reason: outcome.reason }, deps.sessionId)); + return { + payload, + shortCircuited: true, + correctionRequested: false, + reason: outcome.reason, + finalResponse: outcome.finalResponse, + hookName: hook.name + }; + } + if (outcome.kind === "request_correction") { + if (phase !== "pre_capture") { + throw new HookExecutionError(hook.name, phase, new Error(`request_correction outcome is only valid from "pre_capture", got "${phase}"`)); + } + return { + payload, + shortCircuited: false, + correctionRequested: true, + correctionPrompt: outcome.correctionPrompt, + hookName: hook.name + }; + } + if (outcome.payload) { + payload = { ...payload, ...outcome.payload }; + } + } + return { + payload, + shortCircuited: false, + correctionRequested: false + }; + }; +} + +// ../freya/packages/core/dist/domain/services/OntologyValidationService.js +function parseResponseForClaims(content, ontology) { + if (ontology.entityTypes.length === 0) + return []; + const claims = []; + for (const entityType of ontology.entityTypes) { + const entityName = entityType.name; + const pattern = new RegExp(`\\b${escapeRegex(entityName)}\\b`, "gi"); + const matches2 = [...content.matchAll(pattern)]; + if (matches2.length === 0) + continue; + for (const match of matches2) { + const matchIndex = match.index; + const windowStart = Math.max(0, matchIndex - 20); + const windowEnd = Math.min(content.length, matchIndex + entityName.length + 200); + const window = content.slice(windowStart, windowEnd); + const properties = extractProperties(window, entityType); + claims.push({ + text: window.trim(), + entityType: entityName, + properties: Object.keys(properties).length > 0 ? properties : void 0 + }); + } + } + return claims; +} +function extractProperties(text, entityType) { + const properties = {}; + for (const prop of entityType.properties) { + const patterns = [ + new RegExp(`\\b${escapeRegex(prop.name)}\\s+(?:is|:|=)\\s+(\\S+)`, "i"), + new RegExp(`\\b${escapeRegex(prop.name)}\\s+(\\S+)`, "i") + ]; + for (const pattern of patterns) { + const match = text.match(pattern); + if (match) { + const value = match[1].replace(/[.,;!?)]+$/, ""); + if (value) { + properties[prop.name] = value; + break; + } + } + } + } + return properties; +} +function validateClaims(claims, ontology) { + const result = { + valid: [], + fixable: [], + friction: [] + }; + for (const claim of claims) { + validateSingleClaim(claim, ontology, result); + } + return result; +} +function validateSingleClaim(claim, ontology, result) { + const entityResolution = resolveEntityType(claim.entityType, ontology); + if (entityResolution.status === "unknown") { + result.friction.push({ + claim, + frictionType: "unknown_entity", + context: `Entity type "${claim.entityType}" is not defined in the ontology. Known types: ${ontology.entityTypes.map((e) => e.name).join(", ")}` + }); + return; + } + if (entityResolution.status === "fixable") { + result.fixable.push({ + claim, + suggestion: `Use "${entityResolution.resolved.name}" instead of "${claim.entityType}"` + }); + return; + } + const resolvedEntity = entityResolution.resolved; + if (!claim.properties || Object.keys(claim.properties).length === 0) { + result.valid.push(claim); + return; + } + let hasIssue = false; + for (const [propName, propValue] of Object.entries(claim.properties)) { + const propResolution = resolveProperty(propName, propValue, resolvedEntity); + if (propResolution.status === "fixable") { + result.fixable.push({ + claim, + suggestion: propResolution.suggestion + }); + hasIssue = true; + break; + } + if (propResolution.status === "unknown_property") { + result.friction.push({ + claim, + frictionType: "unknown_property", + context: `Property "${propName}" does not exist on entity type "${resolvedEntity.name}". Known properties: ${resolvedEntity.properties.map((p) => p.name).join(", ")}`, + propertyName: propName, + availableProperties: resolvedEntity.properties.map((p) => p.name) + }); + hasIssue = true; + continue; + } + if (propResolution.status === "invalid_value") { + const matchedProp = resolvedEntity.properties.find((p) => p.name === propName) ?? resolvedEntity.properties.find((p) => p.name.toLowerCase() === propName.toLowerCase()); + result.friction.push({ + claim, + frictionType: "invalid_value", + context: propResolution.context, + propertyName: propName, + allowedValues: matchedProp?.enumValues ?? [] + }); + hasIssue = true; + continue; + } + } + if (!hasIssue) { + result.valid.push(claim); + } +} +function resolveEntityType(claimedType, ontology) { + if (!claimedType) + return { status: "exact" }; + const exact = ontology.entityTypes.find((e) => e.name === claimedType); + if (exact) + return { status: "exact", resolved: exact }; + const caseMatch = ontology.entityTypes.find((e) => e.name.toLowerCase() === claimedType.toLowerCase()); + if (caseMatch) + return { status: "exact", resolved: caseMatch }; + for (const entity of ontology.entityTypes) { + const claimedLower = claimedType.toLowerCase(); + const entityLower = entity.name.toLowerCase(); + if (claimedLower.includes(entityLower) || entityLower.includes(claimedLower)) { + return { status: "fixable", resolved: entity }; + } + } + for (const entity of ontology.entityTypes) { + if (editDistance(claimedType.toLowerCase(), entity.name.toLowerCase()) <= 2) { + return { status: "fixable", resolved: entity }; + } + } + return { status: "unknown" }; +} +function resolveProperty(propName, propValue, entityType) { + const exact = entityType.properties.find((p) => p.name === propName); + if (exact) { + return validatePropertyValue(exact, propValue, entityType); + } + const caseMatch = entityType.properties.find((p) => p.name.toLowerCase() === propName.toLowerCase()); + if (caseMatch) { + return validatePropertyValue(caseMatch, propValue, entityType); + } + for (const prop of entityType.properties) { + if (editDistance(propName.toLowerCase(), prop.name.toLowerCase()) <= 2) { + return { + status: "fixable", + suggestion: `Use property "${prop.name}" instead of "${propName}" on entity type "${entityType.name}"` + }; + } + } + return { status: "unknown_property" }; +} +function validatePropertyValue(prop, value, entityType) { + if (prop.type === "enum" && prop.enumValues) { + const lowerValue = value.toLowerCase(); + const match = prop.enumValues.find((v) => v.toLowerCase() === lowerValue); + if (!match) { + return { + status: "invalid_value", + context: `Property "${prop.name}" on "${entityType.name}" only allows: ${prop.enumValues.join(", ")}. Got: "${value}"` + }; + } + } + return { status: "valid" }; +} +function editDistance(a, b) { + if (a.length === 0) + return b.length; + if (b.length === 0) + return a.length; + const matrix = []; + for (let i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } + for (let j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + for (let i = 1; i <= b.length; i++) { + for (let j = 1; j <= a.length; j++) { + if (b[i - 1] === a[j - 1]) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, + // substitution + matrix[i][j - 1] + 1, + // insertion + matrix[i - 1][j] + 1 + ); + } + } + } + return matrix[b.length][a.length]; +} +function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +// ../freya/packages/core/dist/domain/services/MemoryCaptureService.js +var DEFAULT_CAPTURE_CONFIDENCE_THRESHOLD = 0.7; +var HEDGE_PATTERN = new RegExp([ + "\\bmight\\b", + "\\bmaybe\\b", + "\\bperhaps\\b", + "\\bpossibly\\b", + "\\bI\\s+think\\b", + "\\bI\\s+believe\\b", + "\\bnot\\s+sure\\b", + "\\bnot\\s+certain\\b", + "\\bprobably\\b", + "\\bunlikely\\b", + "\\bsomewhat\\b", + "\\bsort\\s+of\\b", + "\\bkind\\s+of\\b", + "\\bappears\\s+to\\b", + "\\bseems\\s+to\\b", + "\\bcould\\s+be\\b" +].join("|"), "gi"); +var HEDGE_PENALTY_PER_MATCH = 0.15; +var MAX_HEDGE_PENALTY_MATCHES = 3; +var CONFIDENCE_FLOOR = 0.1; +function countHedgeMarkers(text) { + if (!text) + return 0; + const matches2 = text.match(HEDGE_PATTERN); + if (!matches2) + return 0; + return Math.min(matches2.length, MAX_HEDGE_PENALTY_MATCHES); +} +function scoreClaimConfidence(claim) { + const propCount = claim.properties ? Object.keys(claim.properties).length : 0; + const propertyScore = 0.5 + 0.1 * Math.min(propCount, 5); + const hedgeCount = countHedgeMarkers(claim.text); + const hedgePenalty = HEDGE_PENALTY_PER_MATCH * hedgeCount; + const raw = propertyScore - hedgePenalty; + return Math.min(1, Math.max(CONFIDENCE_FLOOR, raw)); +} +function coerceStructured(claim, entityType) { + const structured = {}; + if (!claim.properties) + return structured; + for (const [k, v] of Object.entries(claim.properties)) { + const prop = entityType.properties.find((p) => p.name === k || p.name.toLowerCase() === k.toLowerCase()); + if (!prop) { + structured[k] = v; + continue; + } + if (prop.type === "number") { + const n = Number(v); + structured[prop.name] = Number.isFinite(n) ? n : v; + } else if (prop.type === "boolean") { + const lv = v.toLowerCase(); + if (lv === "true") + structured[prop.name] = true; + else if (lv === "false") + structured[prop.name] = false; + else + structured[prop.name] = v; + } else { + structured[prop.name] = v; + } + } + return structured; +} +function extractMemoriesFromResponse(params) { + const { response, ontology, agent, sessionId, turnId } = params; + if (ontology.entityTypes.length === 0 || response.length === 0) { + return { toCapture: [], dropped: [] }; + } + const threshold = agent.config.captureConfidenceThreshold ?? DEFAULT_CAPTURE_CONFIDENCE_THRESHOLD; + const claims = parseResponseForClaims(response, ontology); + if (claims.length === 0) { + return { toCapture: [], dropped: [] }; + } + const { valid } = validateClaims(claims, ontology); + const toCapture = []; + const dropped = []; + const dedupKeys = /* @__PURE__ */ new Set(); + for (const claim of valid) { + if (!claim.entityType) + continue; + const entity = ontology.entityTypes.find((e) => e.name === claim.entityType); + if (!entity) + continue; + const confidence = scoreClaimConfidence(claim); + const structured = coerceStructured(claim, entity); + const content = claim.text; + const lookup = pickPredecessorLookup(structured, entity); + const dedupKey = lookup ? `${entity.name}::${lookup.key}::${stringifyLookupValue(lookup.value)}` : `${entity.name}::__no_key__::${content}`; + if (dedupKeys.has(dedupKey)) + continue; + dedupKeys.add(dedupKey); + if (confidence < threshold) { + dropped.push({ + entityType: entity.name, + content, + confidence, + threshold, + reason: "low_confidence", + ...Object.keys(structured).length > 0 ? { structured } : {} + }); + continue; + } + const missingRequired = entity.properties.find((p) => p.required && !(p.name in structured)); + if (missingRequired) { + dropped.push({ + entityType: entity.name, + content, + confidence, + threshold, + reason: "missing_required_property", + ...Object.keys(structured).length > 0 ? { structured } : {} + }); + continue; + } + const source = { + type: "auto_capture", + sessionId, + turnId, + // Auditing handle: include the agent's id so "who captured this" + // is recoverable from `source.author` without joining via agentId. + author: `memory-capture-service:${agent.id}` + }; + toCapture.push({ + id: crypto.randomUUID(), + agentId: agent.id, + scope: "namespace", + scopeId: agent.config.memoryNamespaces[0] ?? "default", + entityType: entity.name, + content, + structured, + confidence, + source, + status: "active", + portable: false, + createdAt: /* @__PURE__ */ new Date(), + version: 1 + }); + } + return { toCapture, dropped }; +} +function pickPredecessorLookup(structured, entityType) { + const candidates = predecessorLookupCandidates(structured, entityType); + return candidates[0] ?? null; +} +function predecessorLookupCandidates(structured, entityType) { + const out = []; + if ("id" in structured && isPrimitive(structured.id)) { + out.push({ key: "id", value: structured.id }); + } + const keys = Object.keys(structured).sort(); + for (const k of keys) { + if (k === "id") + continue; + const v = structured[k]; + if (!isPrimitive(v)) + continue; + if (entityType) { + const prop = entityType.properties.find((p) => p.name === k || p.name.toLowerCase() === k.toLowerCase()); + if (prop && prop.type === "enum") + continue; + } + out.push({ key: k, value: v }); + } + return out; +} +function isPrimitive(v) { + return typeof v === "string" || typeof v === "number" || typeof v === "boolean"; +} +function stringifyLookupValue(v) { + if (typeof v === "string") + return v.toLowerCase(); + return String(v); +} +async function findPredecessor(candidate, memory, ontology) { + const entityType = ontology?.entityTypes.find((e) => e.name === candidate.entityType); + const lookups = predecessorLookupCandidates(candidate.structured, entityType); + if (lookups.length === 0) + return { kind: "none" }; + let entries; + try { + entries = await memory.getByEntityType(candidate.agentId, candidate.entityType); + } catch { + return { kind: "none" }; + } + const sameScope = entries.filter((e) => e.scope === candidate.scope && e.scopeId === candidate.scopeId); + const candidateId = candidate.structured.id; + const candidateHasPrimitiveId = isPrimitive(candidateId); + for (const lookup of lookups) { + const target = stringifyLookupValue(lookup.value); + const matches2 = sameScope.filter((e) => { + if (e.id === candidate.id) + return false; + if (e.status !== "active") + return false; + const v = e.structured[lookup.key]; + if (v === void 0 || !isPrimitive(v) || stringifyLookupValue(v) !== target) + return false; + if (lookup.key !== "id" && candidateHasPrimitiveId) { + const eId = e.structured.id; + if (isPrimitive(eId) && stringifyLookupValue(eId) !== stringifyLookupValue(candidateId)) { + return false; + } + } + return true; + }); + if (matches2.length === 0) + continue; + const sorted = [...matches2].sort((a, b) => { + const dt = b.createdAt.getTime() - a.createdAt.getTime(); + if (dt !== 0) + return dt; + const dv = (b.version ?? 0) - (a.version ?? 0); + if (dv !== 0) + return dv; + return a.id.localeCompare(b.id); + }); + if (sorted.length >= 2) { + return { kind: "ambiguous", entries: sorted }; + } + return { kind: "one", entry: sorted[0] }; + } + return { kind: "none" }; +} +async function captureFromResponse(params) { + const { memory, ...rest } = params; + const { toCapture, dropped } = extractMemoriesFromResponse(rest); + const captured = []; + const errors = []; + const frictionEvents = []; + const availableEntityTypes = rest.ontology.entityTypes.map((e) => e.name); + for (const candidate of toCapture) { + try { + const predecessor = await findPredecessor(candidate, memory, rest.ontology); + await memory.store(candidate); + captured.push(candidate); + switch (predecessor.kind) { + case "none": + break; + case "one": { + try { + await memory.supersede(predecessor.entry.id, candidate.id); + } catch (err) { + errors.push(`supersede(${predecessor.entry.id} \u2192 ${candidate.id}) failed: ${err instanceof Error ? err.message : String(err)}`); + } + break; + } + case "ambiguous": { + const conflictIds = predecessor.entries.map((e) => e.id).sort(); + const eventId = `conflicting_facts:${rest.sessionId}:${rest.agent.id}:${candidate.entityType}:${conflictIds.join(",")}`; + let claimText; + try { + claimText = JSON.stringify({ + entityType: candidate.entityType, + candidate: candidate.structured, + conflictingEntryIds: conflictIds + }); + } catch { + claimText = ""; + } + frictionEvents.push(createEvent(eventId, "ontology.friction", rest.agent.id, { + claim: claimText, + attemptedEntityType: candidate.entityType, + availableEntityTypes, + frictionType: "conflicting_facts", + count: predecessor.entries.length, + // First-class field — consumers shouldn't have to parse + // `claim` (JSON-encoded) to get the conflict set. This + // is what a human reviewer needs to act: WHICH entries + // conflict, not just how many. + conflictingEntryIds: conflictIds + }, rest.sessionId)); + break; + } + } + } catch (err) { + errors.push(`store(${candidate.id}) failed: ${err instanceof Error ? err.message : String(err)}`); + } + } + return { captured, dropped, errors, frictionEvents }; +} + +// ../freya/packages/core/dist/domain/services/AgentSessionService.js +var MAX_CORRECTION_RETRIES_PER_TURN = 2; +async function executeTurn(agent, sessionId, userMessage, deps, budget) { + const events = []; + const allToolResults = []; + let totalInputTokens = 0; + let totalOutputTokens = 0; + let totalCacheReadTokens = 0; + let totalCacheWriteTokens = 0; + let llmCalls = 0; + const tracing = deps.trace === true; + const trace = []; + const turnId = crypto.randomUUID(); + const annotationsBuf = []; + let currentPhase = "init"; + const rawFire = makeFireHook({ + registry: deps.hooks ?? new InMemoryHookRegistry(), + agent, + sessionId, + turnId, + onEvent: (e) => events.push(e), + onAnnotation: (key, value) => annotationsBuf.push({ phase: currentPhase, key, value }) + }); + const fire = async (phase, payload) => { + currentPhase = phase; + try { + return await rawFire(phase, payload); + } catch (err) { + if (!(err instanceof HookExecutionError)) + throw err; + const message = err.message; + annotationsBuf.push({ + phase, + key: "blocking.hook_exception", + value: message + }); + events.push({ + id: crypto.randomUUID(), + type: "turn.annotated", + agentId: agent.id, + sessionId, + timestamp: /* @__PURE__ */ new Date(), + payload: { + key: "blocking.hook_exception", + phase, + error: message + } + }); + return { + payload, + shortCircuited: false, + correctionRequested: false + }; + } + }; + let session = await deps.sessions.get(sessionId); + if (!session) { + session = createSession(sessionId, agent.id, userMessage.metadata.userId ?? "unknown", userMessage.transportOrigin); + } + session = addMessage(session, userMessage); + events.push(createEvent(crypto.randomUUID(), "message.received", agent.id, { messageId: userMessage.id }, sessionId)); + let ontology = await deps.ontologyService.compose(agent.id); + const recallResult = await deps.memory.recall({ + agentId: agent.id, + query: userMessage.content, + limit: 20 + }); + let memories = recallResult.entries; + const effectiveBudget = budget ?? budgetFromMaxTurns(agent.config.maxTurns || 10); + const tracker = createBudgetTracker(effectiveBudget, { modelId: agent.config.modelId }); + const hardCap = Math.max(effectiveBudget.maxCalls != null ? effectiveBudget.maxCalls * 2 : 100, 1); + let totalLLMCalls = 0; + let finalResponse = null; + let budgetExhaustedDuringLoop = false; + let earlyExit = null; + try { + const preTurn = await fire("pre_turn", { userMessage, ontology, memories, session }); + if (preTurn.shortCircuited) { + earlyExit = { finalResponse: preTurn.finalResponse ?? null, reason: preTurn.reason }; + } else { + ontology = preTurn.payload.ontology; + memories = preTurn.payload.memories; + } + const toolDefs = []; + if (!earlyExit) { + for (const scope of agent.config.toolScopes) { + const discovered = await deps.tools.discoverTools(scope); + toolDefs.push(...discovered); + } + } + const MAX_CONTEXT_MESSAGES = 20; + const windowedMessages = session.messages.length > MAX_CONTEXT_MESSAGES ? session.messages.slice(-MAX_CONTEXT_MESSAGES) : session.messages; + let context = !earlyExit ? buildContext({ + config: agent.config, + ontology, + memories, + messages: windowedMessages, + tools: toolDefs, + ontologyRenderer: deps.ontologyRenderer, + transport: userMessage.transportOrigin + }) : { systemPrompt: "", messages: [], tools: [], tokenEstimate: 0 }; + if (!earlyExit) { + const preContext = await fire("pre_context", { + context, + recall: recallResult, + recallQuery: userMessage.content, + availableEntityTypes: ontology.entityTypes.map((e) => e.name) + }); + if (preContext.shortCircuited) { + earlyExit = { finalResponse: preContext.finalResponse ?? null, reason: preContext.reason }; + } else { + context = preContext.payload.context; + } + } + let turnMessages = [...windowedMessages]; + if (tracing) { + trace.push({ step: "start", timestamp: Date.now(), data: { sessionMessages: session.messages.length, windowedMessages: windowedMessages.length, hardCap, budget: effectiveBudget } }); + } + let correctionsUsed = 0; + correctionLoop: while (!earlyExit) { + finalResponse = null; + llmLoop: while (totalLLMCalls < hardCap) { + const preLlm = await fire("pre_llm", { + messages: turnMessages, + systemPrompt: context.systemPrompt, + tools: toolDefs, + callNumber: totalLLMCalls + 1 + }); + if (preLlm.shortCircuited) { + earlyExit = { finalResponse: preLlm.finalResponse ?? null, reason: preLlm.reason }; + break llmLoop; + } + if (tracing) { + trace.push({ step: "llm_call", timestamp: Date.now(), data: { turnMessages: preLlm.payload.messages.length, toolDefs: preLlm.payload.tools.length, callNumber: totalLLMCalls + 1 } }); + } + const llmResponse = await deps.llm.complete({ + model: agent.config.modelId, + systemPrompt: preLlm.payload.systemPrompt, + messages: preLlm.payload.messages, + tools: preLlm.payload.tools + }); + totalLLMCalls++; + llmCalls++; + totalInputTokens += llmResponse.usage.inputTokens; + totalOutputTokens += llmResponse.usage.outputTokens; + totalCacheReadTokens += llmResponse.usage.cacheReadTokens ?? 0; + totalCacheWriteTokens += llmResponse.usage.cacheWriteTokens ?? 0; + tracker.recordCall(llmResponse.usage); + if (tracing) { + trace.push({ step: "llm_response", timestamp: Date.now(), data: { contentLength: llmResponse.content.length, toolCalls: llmResponse.toolCalls.length, stopReason: llmResponse.stopReason, usage: llmResponse.usage } }); + } + const postLlm = await fire("post_llm", { response: llmResponse, callNumber: totalLLMCalls }); + const effectiveResponse = postLlm.payload.response; + if (postLlm.shortCircuited) { + earlyExit = { finalResponse: postLlm.finalResponse ?? null, reason: postLlm.reason }; + finalResponse = effectiveResponse; + break llmLoop; + } + if (effectiveResponse.stopReason === "tool_use" && effectiveResponse.toolCalls.length > 0) { + let toolLoopShortCircuit = false; + for (const call of effectiveResponse.toolCalls) { + const preTool = await fire("pre_tool", { call }); + if (preTool.shortCircuited) { + earlyExit = { finalResponse: preTool.finalResponse ?? null, reason: preTool.reason }; + finalResponse = effectiveResponse; + toolLoopShortCircuit = true; + break; + } + const effectiveCall = preTool.payload.call; + events.push(createEvent(crypto.randomUUID(), "tool.invoked", agent.id, { tool: effectiveCall.toolName }, sessionId)); + const rawResult = await deps.tools.execute(effectiveCall); + const availableEntityTypes = ontology.entityTypes.map((e) => e.name); + const postTool = await fire("post_tool", { + call: effectiveCall, + result: rawResult, + availableEntityTypes + }); + const result2 = postTool.payload.result; + allToolResults.push(result2); + events.push(createEvent(crypto.randomUUID(), "tool.completed", agent.id, { tool: effectiveCall.toolName, status: result2.status }, sessionId)); + const toolUseId = effectiveCall.id; + turnMessages = [ + ...turnMessages, + { + id: crypto.randomUUID(), + role: "assistant", + content: effectiveResponse.content, + timestamp: /* @__PURE__ */ new Date(), + transportOrigin: "agent", + toolInvocations: [{ toolName: effectiveCall.toolName, input: effectiveCall.input, output: result2.output, durationMs: result2.durationMs, status: result2.status }], + metadata: { toolUseId } + }, + { + id: crypto.randomUUID(), + role: "tool", + content: typeof result2.output === "string" ? result2.output : JSON.stringify(result2.output), + timestamp: /* @__PURE__ */ new Date(), + transportOrigin: "tool", + metadata: { toolName: effectiveCall.toolName, callId: toolUseId } + } + ]; + if (postTool.shortCircuited) { + earlyExit = { finalResponse: postTool.finalResponse ?? null, reason: postTool.reason }; + finalResponse = effectiveResponse; + toolLoopShortCircuit = true; + break; + } + } + if (toolLoopShortCircuit) + break llmLoop; + if (tracker.isExhausted()) { + if (tracing) { + trace.push({ step: "budget_check", timestamp: Date.now(), data: { exhausted: true, status: tracker.getStatus() } }); + } + finalResponse = effectiveResponse; + budgetExhaustedDuringLoop = true; + break llmLoop; + } + if (tracing) { + trace.push({ step: "budget_check", timestamp: Date.now(), data: { exhausted: false, status: tracker.getStatus() } }); + } + } else { + finalResponse = effectiveResponse; + break llmLoop; + } + } + if (earlyExit) + break correctionLoop; + const candidateContent = finalResponse?.content ?? (tracker.getStatus().exhausted || budgetExhaustedDuringLoop ? "[Agent budget exhausted]" : "[Agent reached max turns without completing]"); + const candidateMessage = createAssistantMessage(crypto.randomUUID(), candidateContent, allToolResults.map((r) => ({ + toolName: r.toolName, + input: {}, + output: r.output, + durationMs: r.durationMs, + status: r.status + }))); + const preCapture = await fire("pre_capture", { + response: candidateMessage, + toolResults: allToolResults, + ontology + }); + if (preCapture.correctionRequested) { + if (correctionsUsed < MAX_CORRECTION_RETRIES_PER_TURN) { + correctionsUsed++; + turnMessages = [ + ...turnMessages, + candidateMessage, + createUserMessage(crypto.randomUUID(), preCapture.correctionPrompt ?? "Please correct your response.", "system") + ]; + continue correctionLoop; + } + events.push(createEvent(crypto.randomUUID(), "turn.correction_cap_exceeded", agent.id, { + hookName: preCapture.hookName, + correctionsUsed, + cap: MAX_CORRECTION_RETRIES_PER_TURN + }, sessionId)); + } + if (preCapture.shortCircuited) { + earlyExit = { finalResponse: preCapture.finalResponse ?? candidateMessage, reason: preCapture.reason }; + } + break correctionLoop; + } + const budgetStatus = tracker.getStatus(); + const budgetExhausted = budgetExhaustedDuringLoop || budgetStatus.exhausted; + const fallbackContent = budgetExhausted ? "[Agent budget exhausted]" : earlyExit ? `[Agent short-circuited${earlyExit.reason ? `: ${earlyExit.reason}` : ""}]` : "[Agent reached max turns without completing]"; + const builtContent = earlyExit ? fallbackContent : finalResponse?.content ?? fallbackContent; + const responseMessage = earlyExit?.finalResponse ?? createAssistantMessage(crypto.randomUUID(), builtContent, allToolResults.map((r) => ({ + toolName: r.toolName, + input: {}, + output: r.output, + durationMs: r.durationMs, + status: r.status + }))); + session = addMessage(session, responseMessage); + await deps.sessions.save(session); + events.push(createEvent(crypto.randomUUID(), "message.sent", agent.id, { messageId: responseMessage.id }, sessionId)); + let memoriesCaptured = []; + let droppedCandidates = []; + try { + const captureResult = await captureFromResponse({ + response: responseMessage.content, + ontology, + agent, + sessionId, + turnId, + memory: deps.memory + }); + memoriesCaptured = captureResult.captured; + droppedCandidates = captureResult.dropped; + for (const errMsg of captureResult.errors) { + annotationsBuf.push({ + phase: "post_capture", + key: "memory.capture_error", + value: errMsg + }); + } + for (const fe of captureResult.frictionEvents) { + events.push(fe); + } + } catch (err) { + annotationsBuf.push({ + phase: "post_capture", + key: "memory.capture_error", + value: err instanceof Error ? err.message : String(err) + }); + } + const availableEntityTypesAtCapture = ontology.entityTypes.map((e) => e.name); + let recentlyCaptured; + if (memoriesCaptured.length > 0) { + for (const entry of memoriesCaptured) { + try { + const chain = await deps.memory.getSupersedeChain(entry.id); + if (chain.length === 0) + continue; + const propertyNames = Object.keys(entry.structured).sort(); + const nonIdKeys = propertyNames.filter((p) => p !== "id"); + const propertyName = nonIdKeys[0] ?? propertyNames[0]; + if (!propertyName) + continue; + const coverage = chain.filter((p) => propertyName in p.structured).length; + if (coverage < Math.ceil(chain.length / 2)) + continue; + const priorValues = [...chain].reverse().map((prior) => prior.structured[propertyName]); + recentlyCaptured = { + chainAnchor: entry.id, + entityType: entry.entityType, + propertyName, + currentValue: entry.structured[propertyName], + priorValues + }; + break; + } catch { + continue; + } + } + } + const postCapture = await fire("post_capture", { + captured: memoriesCaptured, + availableEntityTypes: availableEntityTypesAtCapture, + droppedCandidates, + ...recentlyCaptured ? { recentlyCaptured } : {} + }); + if (postCapture.shortCircuited && !earlyExit) { + earlyExit = { finalResponse: postCapture.finalResponse ?? null, reason: postCapture.reason }; + } + const estimatedCostUSD = estimateCostUSD(agent.config.modelId, { + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + cacheReadTokens: totalCacheReadTokens, + cacheWriteTokens: totalCacheWriteTokens + }); + if (tracing) { + trace.push({ step: "complete", timestamp: Date.now(), data: { budgetExhausted, responseLength: responseMessage.content.length, totalLLMCalls, hardCap } }); + } + const result = { + response: responseMessage, + session, + memoriesCaptured, + events, + toolResults: allToolResults, + usage: { + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + llmCalls, + estimatedCostUSD + }, + budgetExhausted, + budgetStatus: { + calls: budgetStatus.calls, + tokens: budgetStatus.tokens, + timeMs: budgetStatus.timeMs, + ...budgetStatus.exhaustedReason ? { reason: budgetStatus.exhaustedReason } : {} + }, + ...tracing ? { trace } : {} + }; + await fire("post_turn", { + result, + availableEntityTypes: availableEntityTypesAtCapture + }); + return result; + } catch (err) { + const partialResult = { + response: createAssistantMessage(crypto.randomUUID(), `[Agent error: ${err instanceof Error ? err.message : String(err)}]`, []), + session, + memoriesCaptured: [], + events, + toolResults: allToolResults, + usage: { + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + llmCalls, + // Even on the error path, surface real cost when tokens were + // consumed before the throw — billing observers/cost trackers + // running at post_turn should see the spend that already happened. + estimatedCostUSD: estimateCostUSD(agent.config.modelId, { + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + cacheReadTokens: totalCacheReadTokens, + cacheWriteTokens: totalCacheWriteTokens + }) + } + }; + try { + await fire("post_turn", { + result: partialResult, + availableEntityTypes: ontology.entityTypes.map((e) => e.name) + }); + } catch { + } + throw err; + } +} + +// ../freya/packages/core/dist/domain/services/IdentifierRedactionService.js +var OPAQUE_ID_DEFAULT_MIN = 32; +var OPAQUE_ID_DEFAULT_PATTERN = new RegExp(`\\b[A-Za-z0-9]{${OPAQUE_ID_DEFAULT_MIN},}\\b`, "g"); + +// ../freya/packages/runtime/dist/streaming.js +async function* executeStreamingTurn(agent, sessionId, userMessage, deps, budget, signal) { + const events = []; + const allToolResults = []; + let totalInputTokens = 0; + let totalOutputTokens = 0; + let totalCacheReadTokens = 0; + let totalCacheWriteTokens = 0; + let llmCalls = 0; + const effectiveBudget = budget ?? budgetFromMaxTurns(agent.config.maxTurns || 10); + const tracker = createBudgetTracker(effectiveBudget, { modelId: agent.config.modelId }); + const hardCap = Math.max(effectiveBudget.maxCalls != null ? effectiveBudget.maxCalls * 2 : 100, 1); + let budgetExhaustedDuringLoop = false; + const turnId = crypto.randomUUID(); + const annotations = []; + let currentPhase = "init"; + const rawFire = makeFireHook({ + registry: deps.hooks ?? new InMemoryHookRegistry(), + agent, + sessionId, + turnId, + onEvent: (e) => events.push(e), + // Wire annotations through to a local buffer so callers / tests can see + // them; without this they were silently dropped (the default is a no-op). + onAnnotation: (key, value) => annotations.push({ phase: currentPhase, key, value }) + }); + const fire = async (phase, payload) => { + currentPhase = phase; + try { + return await rawFire(phase, payload); + } catch (err) { + if (!(err instanceof HookExecutionError)) + throw err; + const message = err.message; + annotations.push({ + phase, + key: "streaming.hook_exception", + value: message + }); + events.push(annotationEvent(agent.id, sessionId, "streaming.hook_exception", { + phase, + error: message + })); + return { + payload, + shortCircuited: false, + correctionRequested: false + }; + } + }; + const userId = userMessage.metadata?.userId ?? "unknown"; + let session = await deps.sessions.get(sessionId); + if (!session) { + session = createSession(sessionId, agent.id, userId, userMessage.transportOrigin); + } + session = addMessage(session, userMessage); + events.push(createEvent(crypto.randomUUID(), "message.received", agent.id, { messageId: userMessage.id }, sessionId)); + let ontology = await deps.ontologyService.compose(agent.id); + const recallResult = await deps.memory.recall({ + agentId: agent.id, + query: userMessage.content, + limit: 20 + }); + let memories = recallResult.entries; + let streamingStarted = false; + let earlyExitReason; + let earlyExitResponse = null; + let earlyShortCircuit = false; + try { + const preTurn = await fire("pre_turn", { + userMessage, + ontology, + memories, + session + }); + if (preTurn.shortCircuited) { + earlyExitReason = preTurn.reason; + earlyExitResponse = preTurn.finalResponse ?? null; + yield systemMessage("pre_turn", preTurn.reason, preTurn.finalResponse); + earlyShortCircuit = true; + } else { + ontology = preTurn.payload.ontology; + memories = preTurn.payload.memories; + } + const toolDefs = []; + if (!earlyShortCircuit) { + for (const scope of agent.config.toolScopes) { + const discovered = await deps.tools.discoverTools(scope); + toolDefs.push(...discovered); + } + } + const MAX_CONTEXT_MESSAGES = 20; + const windowedMessages = session.messages.length > MAX_CONTEXT_MESSAGES ? session.messages.slice(-MAX_CONTEXT_MESSAGES) : session.messages; + let context = !earlyShortCircuit ? buildContext({ + config: agent.config, + ontology, + memories, + messages: windowedMessages, + tools: toolDefs, + ontologyRenderer: deps.ontologyRenderer, + transport: userMessage.transportOrigin + }) : { systemPrompt: "", messages: [], tools: [], tokenEstimate: 0 }; + if (!earlyShortCircuit) { + const preContext = await fire("pre_context", { + context, + recall: recallResult, + recallQuery: userMessage.content, + availableEntityTypes: ontology.entityTypes.map((e) => e.name) + }); + if (preContext.shortCircuited) { + earlyExitReason = preContext.reason; + earlyExitResponse = preContext.finalResponse ?? null; + yield systemMessage("pre_context", preContext.reason, preContext.finalResponse); + earlyShortCircuit = true; + } else { + context = preContext.payload.context; + } + } + let currentMessages = [...windowedMessages]; + let lastAssistantContent = ""; + while (!earlyShortCircuit && llmCalls < hardCap) { + const preLlm = await fire("pre_llm", { + messages: currentMessages, + systemPrompt: context.systemPrompt, + tools: toolDefs, + callNumber: llmCalls + 1 + }); + if (preLlm.shortCircuited) { + if (!streamingStarted) { + earlyExitReason = preLlm.reason; + earlyExitResponse = preLlm.finalResponse ?? null; + yield systemMessage("pre_llm", preLlm.reason, preLlm.finalResponse); + earlyShortCircuit = true; + break; + } + events.push(annotationEvent(agent.id, sessionId, "streaming.short_circuit_after_emit", { + phase: "pre_llm", + reason: preLlm.reason + })); + break; + } + let fullContent = ""; + const toolCalls = []; + let chunkUsage = { + inputTokens: 0, + outputTokens: 0 + }; + for await (const chunk of deps.llm.stream({ + model: agent.config.modelId, + systemPrompt: preLlm.payload.systemPrompt, + messages: preLlm.payload.messages, + tools: preLlm.payload.tools, + signal + })) { + if (chunk.type === "text" && chunk.content) { + fullContent += chunk.content; + streamingStarted = true; + yield chunk.content; + } else if (chunk.type === "tool_call" && chunk.toolCall) { + toolCalls.push(chunk.toolCall); + } else if (chunk.type === "done") { + if (chunk.usage) { + chunkUsage = { + inputTokens: chunk.usage.inputTokens, + outputTokens: chunk.usage.outputTokens, + ...chunk.usage.cacheReadTokens !== void 0 && { cacheReadTokens: chunk.usage.cacheReadTokens }, + ...chunk.usage.cacheWriteTokens !== void 0 && { cacheWriteTokens: chunk.usage.cacheWriteTokens } + }; + } + break; + } + } + llmCalls++; + lastAssistantContent = fullContent; + const llmResponse = { + content: fullContent, + toolCalls, + usage: chunkUsage, + stopReason: toolCalls.length > 0 ? "tool_use" : "end_turn" + }; + tracker.recordCall(llmResponse.usage); + const postLlm = await fire("post_llm", { + response: llmResponse, + callNumber: llmCalls + }); + totalInputTokens += llmResponse.usage.inputTokens; + totalOutputTokens += llmResponse.usage.outputTokens; + totalCacheReadTokens += llmResponse.usage.cacheReadTokens ?? 0; + totalCacheWriteTokens += llmResponse.usage.cacheWriteTokens ?? 0; + const effectiveResponse = postLlm.payload.response; + if (postLlm.shortCircuited) { + events.push(annotationEvent(agent.id, sessionId, "streaming.short_circuit_after_emit", { + phase: "post_llm", + reason: postLlm.reason + })); + break; + } + if (effectiveResponse.toolCalls.length === 0) + break; + const availableEntityTypes = ontology.entityTypes.map((e) => e.name); + let toolLoopBreak = false; + for (const call of effectiveResponse.toolCalls) { + const preTool = await fire("pre_tool", { call }); + if (preTool.shortCircuited) { + events.push(annotationEvent(agent.id, sessionId, "streaming.short_circuit_after_emit", { + phase: "pre_tool", + reason: preTool.reason + })); + toolLoopBreak = true; + break; + } + const effectiveCall = preTool.payload.call; + events.push(createEvent(crypto.randomUUID(), "tool.invoked", agent.id, { tool: effectiveCall.toolName }, sessionId)); + const result2 = await deps.tools.execute(effectiveCall); + const postTool = await fire("post_tool", { + call: effectiveCall, + result: result2, + availableEntityTypes + }); + const effectiveResult = postTool.payload.result; + allToolResults.push(effectiveResult); + events.push(createEvent(crypto.randomUUID(), "tool.completed", agent.id, { tool: effectiveCall.toolName, status: effectiveResult.status }, sessionId)); + currentMessages = [ + ...currentMessages, + { + id: crypto.randomUUID(), + role: "assistant", + content: fullContent, + timestamp: /* @__PURE__ */ new Date(), + transportOrigin: "agent", + toolInvocations: [ + { + toolName: effectiveCall.toolName, + input: effectiveCall.input, + output: effectiveResult.output, + durationMs: effectiveResult.durationMs, + status: effectiveResult.status + } + ], + // Thread the original tool-call id so the LLM adapter can emit a + // `tool_use` block whose id matches the `tool_result` below. Without + // this, the Anthropic adapter synthesises two *independent* ids + // (random vs Date.now()) and the provider rejects the continuation + // call with "tool_result ... has no corresponding tool_use block". + // Mirrors the blocking codepath (executeTurn) which sets the same. + metadata: { toolUseId: effectiveCall.id } + }, + { + id: crypto.randomUUID(), + role: "tool", + content: typeof effectiveResult.output === "string" ? effectiveResult.output : JSON.stringify(effectiveResult.output), + timestamp: /* @__PURE__ */ new Date(), + transportOrigin: "tool", + metadata: { toolName: effectiveCall.toolName, callId: effectiveCall.id } + } + ]; + if (postTool.shortCircuited) { + events.push(annotationEvent(agent.id, sessionId, "streaming.short_circuit_after_emit", { + phase: "post_tool", + reason: postTool.reason + })); + toolLoopBreak = true; + break; + } + } + if (toolLoopBreak) + break; + if (tracker.isExhausted()) { + budgetExhaustedDuringLoop = true; + const status = tracker.getStatus(); + events.push(annotationEvent(agent.id, sessionId, "streaming.budget_exhausted", { + reason: status.exhaustedReason, + calls: status.calls + })); + yield ` +[Agent budget exhausted${status.exhaustedReason ? `: ${status.exhaustedReason}` : ""}]`; + break; + } + } + const candidateMessage = earlyShortCircuit ? earlyExitResponse ?? createAssistantMessage(crypto.randomUUID(), `[Agent short-circuited${earlyExitReason ? `: ${earlyExitReason}` : ""}]`, []) : createAssistantMessage(crypto.randomUUID(), budgetExhaustedDuringLoop && lastAssistantContent.trim().length === 0 ? "[Agent budget exhausted]" : lastAssistantContent, allToolResults.map((r) => ({ + toolName: r.toolName, + input: {}, + output: r.output, + durationMs: r.durationMs, + status: r.status + }))); + let responseMessage = candidateMessage; + let memoriesCaptured = []; + let droppedCandidates = []; + if (!earlyShortCircuit) { + const preCapture = await fire("pre_capture", { + response: candidateMessage, + toolResults: allToolResults, + ontology + }); + if (preCapture.correctionRequested) { + events.push(annotationEvent(agent.id, sessionId, "streaming.correction_requested", { + hookName: preCapture.hookName, + correctionPrompt: preCapture.correctionPrompt + })); + } else if (preCapture.shortCircuited) { + events.push(annotationEvent(agent.id, sessionId, "streaming.short_circuit_after_emit", { + phase: "pre_capture", + reason: preCapture.reason + })); + } + responseMessage = preCapture.shortCircuited && preCapture.finalResponse ? preCapture.finalResponse : candidateMessage; + try { + const captureResult = await captureFromResponse({ + response: responseMessage.content, + ontology, + agent, + sessionId, + turnId, + memory: deps.memory + }); + memoriesCaptured = captureResult.captured; + droppedCandidates = captureResult.dropped; + for (const errMsg of captureResult.errors) { + events.push(annotationEvent(agent.id, sessionId, "streaming.memory_capture_error", { + error: errMsg + })); + } + for (const fe of captureResult.frictionEvents) { + events.push(fe); + } + } catch (err) { + events.push(annotationEvent(agent.id, sessionId, "streaming.memory_capture_error", { + error: err instanceof Error ? err.message : String(err) + })); + } + const availableEntityTypesAtCapture = ontology.entityTypes.map((e) => e.name); + let recentlyCaptured; + if (memoriesCaptured.length > 0) { + for (const entry of memoriesCaptured) { + try { + const chain = await deps.memory.getSupersedeChain(entry.id); + if (chain.length === 0) + continue; + const propertyNames = Object.keys(entry.structured).sort(); + const nonIdKeys = propertyNames.filter((p) => p !== "id"); + const propertyName = nonIdKeys[0] ?? propertyNames[0]; + if (!propertyName) + continue; + const coverage = chain.filter((p) => propertyName in p.structured).length; + if (coverage < Math.ceil(chain.length / 2)) + continue; + const priorValues = [...chain].reverse().map((prior) => prior.structured[propertyName]); + recentlyCaptured = { + chainAnchor: entry.id, + entityType: entry.entityType, + propertyName, + currentValue: entry.structured[propertyName], + priorValues + }; + break; + } catch { + continue; + } + } + } + const postCapture = await fire("post_capture", { + captured: memoriesCaptured, + availableEntityTypes: availableEntityTypesAtCapture, + droppedCandidates, + ...recentlyCaptured ? { recentlyCaptured } : {} + }); + if (postCapture.shortCircuited) { + events.push(annotationEvent(agent.id, sessionId, "streaming.short_circuit_after_emit", { + phase: "post_capture", + reason: postCapture.reason + })); + } + } + session = addMessage(session, responseMessage); + await deps.sessions.save(session); + events.push(createEvent(crypto.randomUUID(), "message.sent", agent.id, { messageId: responseMessage.id }, sessionId)); + const finalBudgetStatus = tracker.getStatus(); + const budgetExhausted = budgetExhaustedDuringLoop || finalBudgetStatus.exhausted; + const result = { + response: responseMessage, + session, + memoriesCaptured, + events, + toolResults: allToolResults, + usage: { + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + llmCalls, + // Compute via the shared `estimateCostUSD` helper so blocking + + // streaming produce identical numbers for the same model + tokens. + // Cache tokens flow through the optional usage fields so cached + // agents get accurate cost (cache_read at 0.1× input rate, cache_write + // at 1.25×). + estimatedCostUSD: estimateCostUSD(agent.config.modelId, { + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + cacheReadTokens: totalCacheReadTokens, + cacheWriteTokens: totalCacheWriteTokens + }) + }, + budgetExhausted, + budgetStatus: { + calls: finalBudgetStatus.calls, + tokens: finalBudgetStatus.tokens, + timeMs: finalBudgetStatus.timeMs, + ...finalBudgetStatus.exhaustedReason ? { reason: finalBudgetStatus.exhaustedReason } : {} + } + }; + try { + await fire("post_turn", { + result, + availableEntityTypes: ontology.entityTypes.map((e) => e.name) + }); + } catch { + } + } catch (err) { + const partialBudgetStatus = tracker.getStatus(); + const partialResult = { + response: createAssistantMessage(crypto.randomUUID(), `[Agent error: ${err instanceof Error ? err.message : String(err)}]`, []), + session, + memoriesCaptured: [], + events, + toolResults: allToolResults, + usage: { + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + llmCalls, + // Even on the error path, attribute real cost for tokens already + // consumed before the throw — billing observers / cost-trackers + // at post_turn should see the spend that already happened. + estimatedCostUSD: estimateCostUSD(agent.config.modelId, { + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + cacheReadTokens: totalCacheReadTokens, + cacheWriteTokens: totalCacheWriteTokens + }) + }, + budgetExhausted: budgetExhaustedDuringLoop || partialBudgetStatus.exhausted, + budgetStatus: { + calls: partialBudgetStatus.calls, + tokens: partialBudgetStatus.tokens, + timeMs: partialBudgetStatus.timeMs, + ...partialBudgetStatus.exhaustedReason ? { reason: partialBudgetStatus.exhaustedReason } : {} + } + }; + try { + await fire("post_turn", { + result: partialResult, + availableEntityTypes: ontology.entityTypes.map((e) => e.name) + }); + } catch { + } + throw err; + } +} +function systemMessage(phase, reason, finalResponse) { + if (finalResponse?.content) + return finalResponse.content; + return `[Agent short-circuited at ${phase}${reason ? `: ${reason}` : ""}]`; +} +function annotationEvent(agentId, sessionId, key, data) { + return { + id: crypto.randomUUID(), + type: "turn.annotated", + agentId, + sessionId, + timestamp: /* @__PURE__ */ new Date(), + payload: { key, ...data } + }; +} + +// ../freya/packages/runtime/dist/create-agent-runtime.js +function createAgentRuntime(adapters, agents = /* @__PURE__ */ new Map()) { + const agentMap = new Map(agents); + const ontologyService = { + async compose(agentId) { + const agent = agentMap.get(agentId); + if (!agent) + throw new Error(`Agent not found: ${agentId}`); + const scopes = agent.config.ontologyScopes; + return adapters.ontologyRepo.compose(scopes); + }, + render(ontology) { + return renderOntologySimple(ontology); + }, + async validate(entityType, data) { + const firstAgent = agentMap.values().next().value; + if (!firstAgent) + return true; + const ontology = await adapters.ontologyRepo.compose(firstAgent.config.ontologyScopes); + const result = adapters.ontologyRepo.validateEntry(entityType, data, ontology); + return result.valid; + } + }; + const ontologyRenderer = { + render: renderOntologySimple + }; + const registry = { + async getAgent(agentId) { + return agentMap.get(agentId) ?? null; + }, + async listAgents() { + return Array.from(agentMap.values()); + }, + async registerAgent(config, deploymentId) { + const agent = createAgent(config, deploymentId); + agentMap.set(agent.id, agent); + return agent; + } + }; + return { + async handleMessage({ agentId, sessionId, message, budget }) { + const agent = agentMap.get(agentId); + if (!agent) + throw new Error(`Agent not found: ${agentId}`); + const result = await executeTurn(agent, sessionId, message, { + llm: adapters.llm, + tools: adapters.toolExecutor, + memory: adapters.memory, + sessions: adapters.sessions, + ontologyService, + ontologyRenderer, + transport: adapters.transport ?? noopTransport, + embedding: adapters.embedding + }, budget); + return { + message: result.response, + session: result.session, + memoriesCaptured: result.memoriesCaptured, + delegations: [], + usage: result.usage + }; + }, + handleMessageStream({ agentId, sessionId, message, budget, hooks, signal }) { + const agent = agentMap.get(agentId); + if (!agent) + throw new Error(`Agent not found: ${agentId}`); + return executeStreamingTurn(agent, sessionId, message, { + llm: adapters.llm, + tools: adapters.toolExecutor, + memory: adapters.memory, + sessions: adapters.sessions, + ontologyService, + ontologyRenderer, + embedding: adapters.embedding, + ...hooks ? { hooks } : {} + }, budget, signal); + }, + async startSession({ agentId, userId, transportId }) { + const session = createSession(crypto.randomUUID(), agentId, userId, transportId); + await adapters.sessions.save(session); + return session; + }, + async getAgent(agentId) { + return agentMap.get(agentId) ?? null; + }, + registry + }; +} +function renderOntologySimple(ontology) { + if (ontology.entityTypes.length === 0) + return ""; + const lines = ["# Domain Ontology"]; + for (const entity of ontology.entityTypes) { + const props = entity.properties.map((p) => p.name).join(", "); + const rels = ontology.relationships.filter((r) => r.fromType === entity.name).map((r) => `${r.name}\u2192${r.toType}`).join(", "); + let line = `## ${entity.name}: [${props}]`; + if (rels) + line += ` | ${rels}`; + if (entity.description && entity.description !== entity.name) { + line += ` +${entity.description}`; + } + lines.push(line); + } + return lines.join("\n"); +} +var noopTransport = { + async send() { + }, + async stream() { + } +}; + +// ../freya/packages/llm/dist/adapters/anthropic.js +function toAnthropicMessages(messages) { + const result = []; + for (const m of messages) { + if (m.role === "user") { + result.push({ role: "user", content: m.content }); + } else if (m.role === "assistant") { + if (m.toolInvocations && m.toolInvocations.length > 0) { + const contentBlocks = []; + if (m.content) { + contentBlocks.push({ type: "text", text: m.content }); + } + const toolUseId = m.metadata?.toolUseId || m.toolInvocations[0].toolName + "_" + Math.random().toString(36).slice(2); + for (const tool of m.toolInvocations) { + contentBlocks.push({ + type: "tool_use", + id: toolUseId, + name: tool.toolName, + input: tool.input + }); + } + result.push({ role: "assistant", content: contentBlocks }); + } else { + result.push({ role: "assistant", content: m.content }); + } + } else if (m.role === "tool") { + const toolName = m.metadata?.toolName || "unknown"; + const callId = m.metadata?.callId || toolName + "_" + Date.now(); + result.push({ + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: callId, + content: m.content + } + ] + }); + } + } + return result; +} +function toAnthropicTools(tools) { + return tools.map((t) => ({ + name: t.name, + description: t.description, + input_schema: t.inputSchema + })); +} +var AnthropicLLM = class { + config; + constructor(config) { + this.config = config; + } + async complete(params) { + const body = { + model: params.model || this.config.defaultModel || "claude-sonnet-4-6", + max_tokens: params.maxTokens || this.config.maxTokens || 4096, + system: params.systemPrompt, + messages: toAnthropicMessages(params.messages) + }; + if (params.temperature !== void 0) { + body.temperature = params.temperature; + } + if (params.tools && params.tools.length > 0) { + body.tools = toAnthropicTools(params.tools); + } + const baseUrl = this.config.baseUrl || "https://api.anthropic.com"; + const response = await fetch(`${baseUrl}/v1/messages`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": this.config.apiKey, + "anthropic-version": "2023-06-01" + }, + body: JSON.stringify(body) + }); + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`Anthropic API error: ${response.status} ${errorBody}`); + } + const data = await response.json(); + let content = ""; + const toolCalls = []; + for (const block of data.content || []) { + if (block.type === "text") { + content += block.text; + } else if (block.type === "tool_use") { + toolCalls.push({ + id: block.id, + toolName: block.name, + input: block.input, + timestamp: /* @__PURE__ */ new Date() + }); + } + } + return { + content, + toolCalls, + usage: { + inputTokens: data.usage?.input_tokens || 0, + outputTokens: data.usage?.output_tokens || 0 + }, + stopReason: data.stop_reason === "tool_use" ? "tool_use" : data.stop_reason === "max_tokens" ? "max_tokens" : "end_turn" + }; + } + async *stream(params) { + const body = { + model: params.model || this.config.defaultModel || "claude-sonnet-4-6", + max_tokens: params.maxTokens || this.config.maxTokens || 4096, + system: params.systemPrompt, + messages: toAnthropicMessages(params.messages), + stream: true + }; + if (params.temperature !== void 0) { + body.temperature = params.temperature; + } + if (params.tools && params.tools.length > 0) { + body.tools = toAnthropicTools(params.tools); + } + const baseUrl = this.config.baseUrl || "https://api.anthropic.com"; + const response = await fetch(`${baseUrl}/v1/messages`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": this.config.apiKey, + "anthropic-version": "2023-06-01" + }, + body: JSON.stringify(body), + // Aborting this signal tears down the HTTP request to Anthropic, which + // stops token generation server-side — true cancellation, not just + // closing the consumer's reader. + signal: params.signal + }); + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`Anthropic streaming error: ${response.status} ${errorBody}`); + } + const reader = response.body?.getReader(); + if (!reader) + throw new Error("No response body for streaming"); + const decoder = new TextDecoder(); + let buffer = ""; + let inputTokens = 0; + let outputTokens = 0; + let cacheReadTokens = 0; + let cacheWriteTokens = 0; + let sawUsage = false; + const pendingToolBlocks = /* @__PURE__ */ new Map(); + const sanitizeTokens = (v) => { + if (typeof v !== "number" || !Number.isFinite(v) || v < 0) + return null; + return v; + }; + const doneChunk = () => { + if (!sawUsage) + return { type: "done" }; + const usage = { + inputTokens, + outputTokens + }; + if (cacheReadTokens > 0) + usage.cacheReadTokens = cacheReadTokens; + if (cacheWriteTokens > 0) + usage.cacheWriteTokens = cacheWriteTokens; + return { type: "done", usage }; + }; + while (true) { + const { done, value } = await reader.read(); + if (done) + break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + for (const line of lines) { + if (!line.startsWith("data: ")) + continue; + const data = line.slice(6).trim(); + if (data === "[DONE]") { + yield doneChunk(); + return; + } + try { + const event = JSON.parse(data); + if (event.type === "message_start") { + const u = event.message?.usage; + if (u) { + const inp = sanitizeTokens(u.input_tokens); + const out = sanitizeTokens(u.output_tokens); + const cr = sanitizeTokens(u.cache_read_input_tokens); + const cw = sanitizeTokens(u.cache_creation_input_tokens); + if (inp !== null || out !== null || cr !== null || cw !== null) { + sawUsage = true; + if (inp !== null) + inputTokens = inp; + if (out !== null) + outputTokens = out; + if (cr !== null) + cacheReadTokens = cr; + if (cw !== null) + cacheWriteTokens = cw; + } + } + } else if (event.type === "content_block_delta") { + if (event.delta?.type === "text_delta") { + yield { type: "text", content: event.delta.text }; + } else if (event.delta?.type === "input_json_delta") { + const idx = event.index; + const pending = idx !== void 0 ? pendingToolBlocks.get(idx) : void 0; + if (pending && typeof event.delta.partial_json === "string") { + pending.jsonBuffer += event.delta.partial_json; + } + } + } else if (event.type === "content_block_start") { + if (event.content_block?.type === "tool_use") { + const idx = event.index; + if (idx !== void 0) { + pendingToolBlocks.set(idx, { + id: event.content_block.id, + toolName: event.content_block.name, + jsonBuffer: "" + }); + } + } + } else if (event.type === "content_block_stop") { + const idx = event.index; + const pending = idx !== void 0 ? pendingToolBlocks.get(idx) : void 0; + if (pending) { + let input = {}; + if (pending.jsonBuffer.trim().length > 0) { + try { + const parsed = JSON.parse(pending.jsonBuffer); + if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) { + input = parsed; + } + } catch { + } + } + yield { + type: "tool_call", + toolCall: { + id: pending.id, + toolName: pending.toolName, + input, + timestamp: /* @__PURE__ */ new Date() + } + }; + pendingToolBlocks.delete(idx); + } + } else if (event.type === "message_delta") { + const u = event.usage; + if (u) { + const out = sanitizeTokens(u.output_tokens); + if (out !== null) { + sawUsage = true; + outputTokens = out; + } + } + } else if (event.type === "message_stop") { + yield doneChunk(); + return; + } + } catch { + } + } + } + if (buffer.length > 0) { + for (const line of buffer.split("\n")) { + if (!line.startsWith("data: ")) + continue; + const data = line.slice(6).trim(); + if (data === "[DONE]" || data.length === 0) + continue; + try { + const event = JSON.parse(data); + if (event.type === "message_delta") { + const u = event.usage; + if (u) { + const out = sanitizeTokens(u.output_tokens); + if (out !== null) { + sawUsage = true; + outputTokens = out; + } + } + } + } catch { + } + } + } + yield doneChunk(); + } +}; + +// ../freya/packages/llm/dist/adapters/fake-embedding.js +var FakeEmbedding = class { + callCount = 0; + async embed(text) { + this.callCount++; + const vec = new Array(8).fill(0); + for (let i = 0; i < text.length; i++) { + vec[i % vec.length] += text.charCodeAt(i) / 1e3; + } + const magnitude = Math.sqrt(vec.reduce((s2, v) => s2 + v * v, 0)); + return magnitude > 0 ? vec.map((v) => v / magnitude) : vec; + } + async embedBatch(texts) { + return Promise.all(texts.map((t) => this.embed(t))); + } + getCallCount() { + return this.callCount; + } +}; + +// ../freya/packages/memory/dist/adapters/in-memory-repo.js +var InMemoryMemoryRepository = class { + entries = []; + events = []; + async store(entry) { + this.entries.push(entry); + this.events.push({ + id: crypto.randomUUID(), + entryId: entry.id, + action: "created", + newValue: entry.content, + author: entry.source.author, + timestamp: /* @__PURE__ */ new Date() + }); + } + async recall(params) { + const limit = params.limit ?? 10; + const queryLower = params.query.toLowerCase(); + const entries = this.entries.filter((e) => { + if (e.agentId !== params.agentId) + return false; + if (e.status !== "active") + return false; + if (params.scope && e.scope !== params.scope) + return false; + if (params.scopeId && e.scopeId !== params.scopeId) + return false; + if (params.entityType && e.entityType !== params.entityType) + return false; + return e.content.toLowerCase().includes(queryLower); + }).slice(0, limit); + const textMatchCount = entries.length; + return { + entries, + source: textMatchCount > 0 ? "text" : "none", + vectorCapable: false, + vectorMatchCount: 0, + textMatchCount + }; + } + async supersede(entryId, newEntryId) { + const entry = this.entries.find((e) => e.id === entryId); + if (entry) { + const idx = this.entries.indexOf(entry); + this.entries[idx] = { + ...entry, + status: "superseded", + supersededBy: newEntryId + }; + this.events.push({ + id: crypto.randomUUID(), + entryId, + action: "superseded", + previousValue: entry.content, + author: "system", + timestamp: /* @__PURE__ */ new Date() + }); + } + } + async getEventLog(entryId) { + return this.events.filter((e) => e.entryId === entryId); + } + async getByEntityType(agentId, entityType) { + return this.entries.filter((e) => e.agentId === agentId && e.entityType === entityType && e.status === "active"); + } + /** + * Walk the supersede chain backward from `entryId`. + * + * At each step we find entries whose `supersededBy` points at the current + * node, take the most recent one (by `createdAt` desc — handles the + * unusual case of multiple predecessors pointing at the same successor), + * and continue from there. A visited-set protects against pathological + * cycles (a node whose `supersededBy` ultimately loops back to itself or + * to an ancestor in the walk). + * + * Capped at 50 versions — long-lived facts can rack up a lot of versions, + * and the detector use case only needs "what shapes have we seen recently". + */ + async getSupersedeChain(entryId) { + const CAP = 50; + const chain = []; + const visited = /* @__PURE__ */ new Set(); + let cursor = entryId; + visited.add(cursor); + const anchor = this.entries.find((e) => e.id === entryId); + if (anchor === void 0) + return []; + while (chain.length < CAP) { + const predecessors = this.entries.filter((e) => e.supersededBy === cursor && e.agentId === anchor.agentId && e.scope === anchor.scope && e.scopeId === anchor.scopeId && e.status === "superseded").sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + if (predecessors.length === 0) + break; + const prior = predecessors[0]; + if (visited.has(prior.id)) { + break; + } + visited.add(prior.id); + chain.push(prior); + cursor = prior.id; + } + return chain; + } + // Test helpers + getAll() { + return [...this.entries]; + } + getAllEvents() { + return [...this.events]; + } + clear() { + this.entries = []; + this.events = []; + } +}; + +// ../freya/packages/memory/dist/adapters/in-memory-session-repo.js +var InMemorySessionRepository = class { + sessions = /* @__PURE__ */ new Map(); + async get(id) { + return this.sessions.get(id) ?? null; + } + async save(session) { + this.sessions.set(session.id, session); + } + async findByUser(userId, agentId, limit) { + const matches2 = Array.from(this.sessions.values()).filter((s2) => s2.userId === userId && s2.agentId === agentId).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + return limit ? matches2.slice(0, limit) : matches2; + } + async findByUserLightweight(userId, agentId, limit) { + const matches2 = Array.from(this.sessions.values()).filter((s2) => s2.userId === userId && s2.agentId === agentId).sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); + const limited = limit ? matches2.slice(0, limit) : matches2; + return limited.map((s2) => { + const firstUserMsg = s2.messages.find((m) => m.role === "user"); + const lastMsg = s2.messages.length > 0 ? s2.messages[s2.messages.length - 1] : void 0; + return { + id: s2.id, + agentId: s2.agentId, + userId: s2.userId, + status: s2.status, + turnCount: s2.turnCount, + messageCount: s2.messages.length, + createdAt: s2.createdAt, + updatedAt: s2.updatedAt, + firstMessage: firstUserMsg ? firstUserMsg.content.substring(0, 100) : void 0, + lastMessage: lastMsg ? lastMsg.content.substring(0, 100) : void 0 + }; + }); + } + // Test helpers + clear() { + this.sessions.clear(); + } + getAll() { + return Array.from(this.sessions.values()); + } +}; + +// ../freya/packages/ontology/dist/composer/composer.js +function composeLayers(layers) { + const entityMap = /* @__PURE__ */ new Map(); + const allRelationships = []; + for (const layer of layers) { + for (const entity of layer.entityTypes) { + const existing = entityMap.get(entity.name); + if (existing) { + const existingPropNames = new Set(existing.properties.map((p) => p.name)); + const newProps = entity.properties.filter((p) => !existingPropNames.has(p.name)); + entityMap.set(entity.name, { + ...existing, + properties: [...existing.properties, ...newProps], + description: entity.description || existing.description + }); + } else { + entityMap.set(entity.name, entity); + } + } + allRelationships.push(...layer.relationships); + } + const relationshipMap = /* @__PURE__ */ new Map(); + for (const rel of allRelationships) { + relationshipMap.set(rel.id, rel); + } + return { + layers, + entityTypes: Array.from(entityMap.values()), + relationships: Array.from(relationshipMap.values()), + version: layers.map((l) => `${l.name}@${l.version}`).join("+") + }; +} + +// ../freya/packages/ontology/dist/validator/validator.js +function validateAgainstOntology(entityType, data, ontology) { + const errors = []; + const warnings = []; + const entity = ontology.entityTypes.find((e) => e.name === entityType); + if (!entity) { + return { + valid: false, + errors: [{ field: "entityType", message: `Unknown entity type: ${entityType}`, code: "unknown_entity" }], + warnings: [] + }; + } + for (const prop of entity.properties) { + if (prop.required && !(prop.name in data)) { + errors.push({ + field: prop.name, + message: `Required property missing: ${prop.name}`, + code: "missing_required" + }); + } + } + for (const [key, value] of Object.entries(data)) { + const prop = entity.properties.find((p) => p.name === key); + if (!prop) { + warnings.push(`Property "${key}" not defined in ontology for ${entityType}`); + continue; + } + if (prop.type === "enum" && prop.enumValues && value !== void 0) { + if (!prop.enumValues.includes(String(value))) { + errors.push({ + field: key, + message: `Invalid value "${value}" for enum ${key}. Expected one of: ${prop.enumValues.join(", ")}`, + code: "invalid_enum" + }); + } + } + if (value !== void 0 && value !== null) { + const typeValid = checkType(value, prop.type); + if (!typeValid) { + errors.push({ + field: key, + message: `Expected ${prop.type} for ${key}, got ${typeof value}`, + code: "invalid_type" + }); + } + } + } + return { + valid: errors.length === 0, + errors, + warnings + }; +} +function checkType(value, expectedType) { + switch (expectedType) { + case "string": + case "enum": + return typeof value === "string"; + case "number": + return typeof value === "number"; + case "boolean": + return typeof value === "boolean"; + case "date": + return typeof value === "string" || value instanceof Date; + case "reference": + return typeof value === "string"; + default: + return true; + } +} + +// ../freya/packages/ontology/dist/adapters/in-memory-ontology-repo.js +var InMemoryOntologyRepository = class { + layers = /* @__PURE__ */ new Map(); + async getLayer(id) { + return this.layers.get(id) ?? null; + } + async getLayersByScope(scope) { + return Array.from(this.layers.values()).filter((l) => l.scope === scope); + } + async compose(layerIds) { + const layers = layerIds.map((id) => this.layers.get(id)).filter((l) => l != null); + return composeLayers(layers); + } + validateEntry(entityType, data, ontology) { + const result = validateAgainstOntology(entityType, data, ontology); + return { valid: result.valid, errors: result.errors.map((e) => e.message) }; + } + // Test helpers + addLayer(layer) { + this.layers.set(layer.id, layer); + } + clear() { + this.layers.clear(); + } +}; + +// ../freya/packages/ontology/dist/seed/loader.js +function parseOntologyYaml(id, raw) { + const entityTypes = []; + const relationships = []; + for (const [entityName, entityDef] of Object.entries(raw.entities || {})) { + const properties = []; + if (entityDef.properties) { + for (const prop of entityDef.properties) { + properties.push({ + name: prop, + type: "string", + required: false, + description: "" + }); + } + } + for (const [key, value] of Object.entries(entityDef)) { + if (Array.isArray(value) && key !== "properties" && key !== "belongs_to" && key !== "has_many" && key !== "connects" && value.every((v) => typeof v === "string")) { + properties.push({ + name: key, + type: "enum", + enumValues: value, + required: false, + description: `${key} for ${entityName}` + }); + } + } + entityTypes.push({ + id: `${id}:${entityName}`, + layerId: id, + name: entityName, + properties, + description: entityDef.description || entityName + }); + const belongsTo = entityDef.belongs_to ? Array.isArray(entityDef.belongs_to) ? entityDef.belongs_to : [entityDef.belongs_to] : []; + for (const target of belongsTo) { + relationships.push({ + id: `${id}:${entityName}:belongs_to:${target}`, + layerId: id, + name: "belongs_to", + fromType: entityName, + toType: target, + cardinality: "many_to_many", + description: `${entityName} belongs to ${target}` + }); + } + for (const target of entityDef.has_many || []) { + relationships.push({ + id: `${id}:${entityName}:has_many:${target}`, + layerId: id, + name: "has_many", + fromType: entityName, + toType: target, + cardinality: "one_to_many", + description: `${entityName} has many ${target}` + }); + } + for (const target of entityDef.connects || []) { + relationships.push({ + id: `${id}:${entityName}:connects:${target}`, + layerId: id, + name: "connects", + fromType: entityName, + toType: target, + cardinality: "many_to_many", + description: `${entityName} connects to ${target}` + }); + } + } + return { + id, + name: raw.name, + scope: raw.scope, + version: 1, + entityTypes, + relationships, + createdAt: /* @__PURE__ */ new Date(), + updatedAt: /* @__PURE__ */ new Date() + }; +} + +// website/tools/freya-vendor/entry.mjs +var AGENT_ID = "frigg-web"; +var TRANSPORT = "netlify-web"; +var FRIGG_ONTOLOGY = { + name: "frigg", + scope: "domain", + entities: { + Platform: { + description: "A third-party software product Frigg integrates with (e.g. HubSpot, Salesforce, Attio).", + properties: ["name", "vendor"] + }, + ApiModule: { + description: "A prebuilt Frigg connector for a platform API, installed with `frigg install ` and drawn from the api-module-library.", + properties: ["name", "provider", "authType"], + category: [ + "ai", + "analytics", + "commerce", + "communication", + "crm", + "devtools", + "finance", + "hr", + "marketing", + "other", + "productivity", + "storage", + "support" + ], + complexity: ["Low", "Medium", "High"], + status: ["Active", "Beta", "Planned"], + belongs_to: "Platform" + }, + Integration: { + description: "A running integration a developer builds by extending IntegrationBase, wiring API modules to events (USER_ACTION, CRON, QUEUE, WEBHOOK).", + properties: ["name", "useCase"], + connects: ["ApiModule", "Primitive"] + }, + Primitive: { + description: "A Frigg building block exposed to developers and their agents: an Endpoint, a Queue, a Provider-native backend, or a Fenestra in-app UI experience.", + properties: ["name"], + kind: ["Endpoint", "Queue", "ProviderNative", "Fenestra"] + }, + Capability: { + description: "A typed declaration of what a module or integration can do, pointing at a spec and its implementation (the mcp-tool / agent-tooling surface).", + properties: ["name", "spec"], + belongs_to: "ApiModule" + }, + Adr: { + description: 'A Frigg architecture decision record shaping the roadmap, tracked on the "next" branch and surfaced at /roadmap/.', + properties: ["num", "title", "theme"], + status: ["Accepted", "Proposed", "Superseded", "Draft"] + }, + Visitor: { + description: "A person chatting with the assistant on the site.", + properties: ["name", "stack", "interest"] + } + } +}; +var activeData = { adrs: [], apis: [], categories: [], builtCount: 0 }; +var s = (v) => typeof v === "string" ? v.toLowerCase() : ""; +var matches = (hay, q) => !q || s(hay).includes(s(q)); +var RoadmapTools = class { + async discoverTools(scope) { + if (scope !== "roadmap") return []; + return [ + { + name: "catalog_stats", + description: 'Frigg roadmap catalog summary: number of ADRs, number of API modules, how many are already built, and the list of API categories. Call this first for any "how many / what categories" question.', + inputSchema: { type: "object", properties: {}, additionalProperties: false }, + source: "roadmap", + requiresApproval: false, + permissionScope: "roadmap:read" + }, + { + name: "search_adrs", + description: "Search Frigg architecture decision records (ADRs). Filter by free-text query (matches title/summary/theme) and/or status (e.g. Accepted, Proposed). Returns matching ADRs with number, title, status, theme, one-line summary, and URL.", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Free-text filter over title/summary/theme" }, + status: { type: "string", description: 'Exact status filter, e.g. "Accepted"' } + }, + additionalProperties: false + }, + source: "roadmap", + requiresApproval: false, + permissionScope: "roadmap:read" + }, + { + name: "search_apis", + description: "Search the Frigg API module catalog (224 integrations). Filter by free-text query (matches name/provider/description/tags), category, or built=true to only return modules that already exist in api-module-library. Returns a capped list plus the total match count so you can point people to /roadmap/ for the full set.", + inputSchema: { + type: "object", + properties: { + query: { type: "string" }, + category: { type: "string", description: "One of the catalog categories" }, + built: { type: "boolean", description: "If true, only modules already built" } + }, + additionalProperties: false + }, + source: "roadmap", + requiresApproval: false, + permissionScope: "roadmap:read" + } + ]; + } + async execute(call) { + const start = Date.now(); + const done = (output, status = "success", error) => ({ + callId: call.id, + toolName: call.toolName, + output, + status, + error, + durationMs: Date.now() - start, + timestamp: /* @__PURE__ */ new Date() + }); + try { + const input = call.input || {}; + if (call.toolName === "catalog_stats") { + return done({ + adrCount: activeData.adrs.length, + apiCount: activeData.apis.length, + builtCount: activeData.builtCount, + categories: activeData.categories + }); + } + if (call.toolName === "search_adrs") { + const hits = activeData.adrs.filter( + (a) => (matches(a.title, input.query) || matches(a.summary, input.query) || matches(a.theme, input.query)) && (!input.status || s(a.status) === s(input.status)) + ); + return done({ + total: hits.length, + adrs: hits.slice(0, 12).map((a) => ({ + num: a.num, + title: a.title, + status: a.status, + theme: a.theme, + summary: a.summary, + url: a.url + })) + }); + } + if (call.toolName === "search_apis") { + const hits = activeData.apis.filter( + (a) => (matches(a.name, input.query) || matches(a.provider, input.query) || matches(a.description, input.query) || Array.isArray(a.tags) && a.tags.some((t) => matches(t, input.query))) && (!input.category || s(a.category) === s(input.category)) && (input.built === void 0 || Boolean(a.built) === Boolean(input.built)) + ); + return done({ + total: hits.length, + showing: Math.min(hits.length, 15), + apis: hits.slice(0, 15).map((a) => ({ + slug: a.slug, + name: a.name, + provider: a.provider, + category: a.category, + status: a.status, + complexity: a.complexity, + built: !!a.built, + library: a.library + })) + }); + } + return done(null, "error", `unknown tool: ${call.toolName}`); + } catch (e) { + return done(null, "error", e && e.message ? e.message : String(e)); + } + } +}; +var runtime = null; +var sessionsRepo = null; +var registered = false; +function getRuntime() { + if (runtime) return runtime; + sessionsRepo = new InMemorySessionRepository(); + const apiKey = process.env.ANTHROPIC_API_KEY || ""; + const baseUrl = process.env.ANTHROPIC_BASE_URL || void 0; + runtime = createAgentRuntime({ + llm: new AnthropicLLM({ + apiKey, + baseUrl, + defaultModel: process.env.ASSISTANT_MODEL || "claude-opus-4-8", + maxTokens: 900 + }), + toolExecutor: new RoadmapTools(), + memory: new InMemoryMemoryRepository(), + ontologyRepo: (() => { + const repo = new InMemoryOntologyRepository(); + repo.addLayer(parseOntologyYaml("frigg", FRIGG_ONTOLOGY)); + return repo; + })(), + sessions: sessionsRepo, + embedding: new FakeEmbedding() + }); + return runtime; +} +async function ensureAgent(rt, systemPrompt, model) { + if (registered) return; + await rt.registry.registerAgent( + { + id: AGENT_ID, + name: "Freya", + type: "shared", + systemPrompt, + ontologyScopes: ["frigg"], + memoryNamespaces: ["default"], + toolScopes: ["roadmap"], + routines: [], + delegationTargets: [], + modelId: model || process.env.ASSISTANT_MODEL || "claude-opus-4-8", + maxTurns: 6 + }, + "friggframework-org" + ); + registered = true; +} +async function runTurn({ systemPrompt, model, messages, data }) { + if (data) { + const apis = data.apis || {}; + activeData = { + adrs: data.adrs && data.adrs.adrs || data.adrs || [], + apis: apis.apis || (Array.isArray(apis) ? apis : []), + categories: apis.categories || [], + builtCount: apis.builtCount || 0 + }; + } + const rt = getRuntime(); + await ensureAgent(rt, systemPrompt, model); + const history = messages.slice(0, -1); + const last = messages[messages.length - 1]; + const sessionId = crypto.randomUUID(); + let session = createSession(sessionId, AGENT_ID, "web-visitor", TRANSPORT); + for (const m of history) { + const msg = m.role === "assistant" ? createAssistantMessage(crypto.randomUUID(), m.content) : createUserMessage(crypto.randomUUID(), m.content, TRANSPORT); + session = addMessage(session, msg); + } + await sessionsRepo.save(session); + const result = await rt.handleMessage({ + agentId: AGENT_ID, + sessionId, + message: createUserMessage(crypto.randomUUID(), last.content, TRANSPORT) + }); + return result && result.message && result.message.content || ""; +} +export { + runTurn +}; diff --git a/website/friggframework-api/submission-created.js b/website/friggframework-api/submission-created.js new file mode 100644 index 000000000..ea5e09dc6 --- /dev/null +++ b/website/friggframework-api/submission-created.js @@ -0,0 +1,23 @@ +const fetch = require('node-fetch') +exports.handler = async function (event, context, callback) { + const payload = JSON.parse(event.body).payload + console.log(JSON.parse(event.body)) + console.log(payload) + console.log(payload.data["slack-invite"]) + const email = payload.data.email + const slackInvite = !!payload.data["slack-invite"] + const emailUpdates = !!payload.data["update-emails"] + console.log(email, slackInvite, emailUpdates) + const res = await fetch('https://api.friggframework.org/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json;charset=utf-8' + }, + body: JSON.stringify({email, slackInvite, emailUpdates}) + }) + console.log(res) + return { + statusCode: 200, + body: JSON.stringify({ message: 'processed' }) + } +} diff --git a/website/friggframework-api/subscribe.js b/website/friggframework-api/subscribe.js new file mode 100644 index 000000000..976f9a685 --- /dev/null +++ b/website/friggframework-api/subscribe.js @@ -0,0 +1,82 @@ +const dotenv = require('dotenv'); +const fetch = require('node-fetch'); + +dotenv.config(); + +const CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': + 'Origin, X-Requested-With, Content-Type, Accept', +} + + +exports.handler = async function (event, context, callback) { + if (event.httpMethod === 'OPTIONS') { + return { + statusCode: 200, + headers: CORS_HEADERS, + } + } + const payload = JSON.parse(event.body); + const email = encodeURIComponent(payload.email.trim()); + const SLACK_TOKEN = process.env.SLACK_TOKEN; + const SLACK_CHANNEL = process.env.SLACK_CONNECT_CHANNEL_ID; + const SLACK_INVITE_ENDPOINT = 'https://slack.com/api/conversations.inviteShared'; + const toSlack = `emails=${email}&channel=${SLACK_CHANNEL}`; + let isError = false; + let responseMessage = []; + if (!payload.slackInvite && !payload.emailUpdates) responseMessage.push('No opt-ins provided') + if (payload.slackInvite) { + try { + const res = await fetch(`${SLACK_INVITE_ENDPOINT}?${toSlack}`, + { + headers: + {'Authorization': `Bearer ${SLACK_TOKEN}`} + } + ) + .then((res) => res.json()) + if (!res.ok) { + console.log(res) + isError = true + } else { + responseMessage.push('Invited to Slack') + } + + } catch (e) { + console.log(e) + isError = true + } + } + + // Always send to Zapier + try { + const res = await fetch(process.env.WEBHOOK_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({email: payload.email.trim(), + slackInvite: payload.slackInvite, + emailUpdates: payload.emailUpdates}) + }) + responseMessage.push('Email subscribed') + } catch (e) { + console.log(e) + isError = true + + } + + + + return callback(null, { + statusCode: isError ? 400 : 200, + headers: { + ...CORS_HEADERS, + "content-type": 'application/json' + }, + body: JSON.stringify({ + message: isError ? 'error' : responseMessage.join(', ') + }) + }); + + +} + diff --git a/website/friggframework-api/vote.js b/website/friggframework-api/vote.js new file mode 100644 index 000000000..5efe98802 --- /dev/null +++ b/website/friggframework-api/vote.js @@ -0,0 +1,49 @@ +const { getStore } = require('@netlify/blobs'); + +const CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', +} + +const ID_REGEX = /^(api|adr):[a-z0-9._-]+$/i; + +exports.handler = async function (event, context) { + if (event.httpMethod === 'OPTIONS') { + return { + statusCode: 200, + headers: CORS_HEADERS, + } + } + + try { + const payload = JSON.parse(event.body || '{}'); + const id = payload.id; + + if (!id || !ID_REGEX.test(id)) { + return { + statusCode: 400, + headers: { ...CORS_HEADERS, 'content-type': 'application/json' }, + body: JSON.stringify({ error: 'invalid id' }), + } + } + + const store = getStore('roadmap-votes'); + const cur = await store.get(id, { type: 'text' }); + const next = (parseInt(cur, 10) || 0) + 1; + await store.set(id, String(next)); + + return { + statusCode: 200, + headers: { ...CORS_HEADERS, 'content-type': 'application/json' }, + body: JSON.stringify({ id, count: next }), + } + } catch (e) { + console.log(e); + return { + statusCode: 500, + headers: { ...CORS_HEADERS, 'content-type': 'application/json' }, + body: JSON.stringify({ error: 'vote failed' }), + } + } +} diff --git a/website/friggframework-api/votes.js b/website/friggframework-api/votes.js new file mode 100644 index 000000000..a2d995969 --- /dev/null +++ b/website/friggframework-api/votes.js @@ -0,0 +1,42 @@ +const { getStore } = require('@netlify/blobs'); + +const CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', +} + +exports.handler = async function (event, context) { + if (event.httpMethod === 'OPTIONS') { + return { + statusCode: 200, + headers: CORS_HEADERS, + } + } + + try { + const store = getStore('roadmap-votes'); + const { blobs } = await store.list(); + + const counts = {}; + if (blobs) { + for (const b of blobs) { + const value = await store.get(b.key, { type: 'text' }); + counts[b.key] = parseInt(value, 10) || 0; + } + } + + return { + statusCode: 200, + headers: { ...CORS_HEADERS, 'content-type': 'application/json' }, + body: JSON.stringify(counts), + } + } catch (e) { + console.log(e); + return { + statusCode: 500, + headers: { ...CORS_HEADERS, 'content-type': 'application/json' }, + body: JSON.stringify({ error: 'read failed' }), + } + } +} diff --git a/website/index.html b/website/index.html new file mode 100644 index 000000000..1487af978 --- /dev/null +++ b/website/index.html @@ -0,0 +1,1314 @@ + + + + + + + + + + + + Frigg: integrations woven by agents, owned by you + + + + + + + + + + + + +
+ +
+ + +
+
+
+ Open source · serverless · yours +

Integrations,
woven by agents.
Owned by you.

+

Frigg is the open-source, serverless framework where an agent scaffolds a real, tested integration in an afternoon. Every integration it builds is also an MCP tool your own agents can call.

+ +
+ MIT + commercial + Runs on your cloud + 200+ APIs charted + v2.0.0-next +
+
+ +
+ + +
+
+
+ + +
+
+
+
+
200+APIs charted
+
70Platforms
+
1Framework
+
+

The connectors the community has already charted. Drop one in with a single frigg install.

+
+ +
+
+ + +
+
+
+ A strong opinion +

An integration is more than moving data.

+

Sometimes moving data is the whole job. Sometimes it's none of it. An integration might be an endpoint you expose, a queue you drain, a bit of provider-specific code, or a screen your user actually sees. Frigg treats those as first-class primitives, so you (and your agents) can reach for whatever the job needs.

+
+
+
+ Endpoint +

Need an ad hoc endpoint?

+

Give it a path and you have a route. No side project, no separate service to stand up.

+
router.get('/webhooks/acme', handler)
+
+
+ Queue +

Need a queue?

+

A quick annotation, and the background worker plus the infrastructure behind it come with it.

+
events: { PROCESS_BATCH: { type: 'QUEUE' } }
+
+
+ Provider-native +

Building inside a provider's world?

+

Attio, HubSpot, Zapier, Zendesk, Salesforce, and the rest. Frigg runs happily as the backend behind them, and the modules ship with helpers so your platform-specific code is built right.

+
this.hubspot.helpers.card(spec)
+
+
+ In-app UI · Fenestra +

Need a screen your users see?

+

Describe the in-app experience in a spec instead of hand-rolling it. Fenestra is the piece we're adding for integration UI, and it gets easier with every extension we ship.

+
fenestra: { view: 'settings', schema }
+
+
+
+ Speaks the specs your tools already know +
+ OpenAPIAsyncAPIArazzoOverlaysJSON SchemaMCPFenestra new +
+

Fenestra is an emerging spec we're introducing for in-app integration UI. It sits alongside the standards, it doesn't try to replace any of them.

+
+
+
+ + +
+
+
+ What changed +

Building the integration is now the agent's job. Deciding which to build is still yours.

+

Frigg has always been about not rebuilding the wheel for every integration. With an agent doing the wiring, the work moves to the parts that need judgment: what to connect, what to skip, and what the integration should actually do. The same structure that lets an agent build an integration also lets your agents use it.

+
+ +
+
+ Agents build it +

Point an agent at two systems. Get a tested integration.

+

Not boilerplate you copy. Run frigg init, then the agent harness grounds the model in your app's own conventions, queries the capabilities that already exist, scaffolds from templates, and validates its plan before it writes a line. You finish the last mile (mapping and business logic) instead of starting from a blank file.

+
+ +
+ Own your pipes +

Cloud-agnostic. Your account, your data, your pipes.

+

Frigg deploys to your cloud, not a vendor's. It runs primarily on AWS today, and adopters have shipped it to GCP, Azure, and their own hardware via containers. Serverless means it costs next to nothing at rest and scales from three records a day to millions an hour on the same code. Customer data never transits someone else's servers, and there's no per-connection tax to renegotiate later.

+
+ +
+
+ Integrations as MCP tools +

Every integration is also a tool your agents can call.

+

Frigg describes what each integration does as typed capabilities like crm.contact.sync and notifications.slack.send. Flip the mcp-tool surface on and your agents call those integrations directly, right alongside raw MCP tools that are plain API requests. One capability graph, exposed to humans over GET /api/capabilities and to agents as tools.

+
+ +
+ +
+ Wisdom of the crowd +

Inherit what everyone already learned about the API.

+

Auth flows, pagination quirks, webhook signatures, the rate limit that isn't in the docs. Every API the community charts becomes a module anyone can install, so you start from what other people already worked out instead of re-reading the same reference page a hundred developers read before you.

+
+ +
+ Infra writes itself +

Define the integrations. The infrastructure generates itself.

+

Frigg is infrastructure-as-code. The app definition self-scaffolds the resources it needs (queues, functions, VPC-private networking, field-level encryption with your own KMS keys, OAuth2 wiring) as the integrations call for them. You describe what you want to integrate, and the cloud footprint is generated from that, so you're never hand-writing the same infra decisions twice.

+
+
+
+
+ + +
+
+
+ From zero to production +

An afternoon, not a quarter.

+

The original vision was "spin one up in minutes, push to production in a day." With an agent doing the wiring, a first integration realistically lands in a few hours. Here's the actual path.

+
+
+
+
scaffold00:00
+

Initialize the app

+

frigg init lays down the serverless app structure: handlers, infra-as-code, and the definition your integrations plug into.

+
+
+
install00:05
+

Drop in a module

+

frigg install hubspot pulls a charted API module, with auth, endpoints, and webhooks already mapped by the community.

+
+
+
weave00:30
+

Let the agent wire it

+

The agent queries capabilities, copies a template, scaffolds tests, and validates its plan. You review and fill in the business logic.

+
+
+
deploy~few hrs
+

Ship to your cloud

+

frigg deploy stands it up on your own AWS account, VPC-private and encrypted, scaling on demand.

+
+
+
+
+ + +
+
+
+ For the geeks in the room +

What it's built on.

+

No magic, just a considered stack. Where you see an orange or, that's a real choice Frigg hands you instead of deciding for you.

+
+
+
Runtime
Node.js 22+JavaScript
+
Compute
AWS Lambdaserverless, scale to zero
+
Deploy
Serverless Framework (osls)esbuild
+
Infra as code
generated serverless.ymlCloudFormation
+
Data store
PostgreSQLorMongoDBvia Prisma ORM
+
Encryption
AWS KMSorAES-256field-level, your keys
+
Queues
AWS SQSrate limiting, fan-out
+
Scheduling
EventBridge Schedulercron events
+
Config
SSM Parameter StoreSecrets Manager
+
HTTP
Expressvia serverless-http
+
Auth
OAuth2API keyBasic
+
Forms & UI
JSON Schema · JSONFormsReact admin UI
+
Cloud
AWS todayorGCPAzurecontainer
+
Testing
Jestnockin-memory DB
+
Observability
OpenTelemetrynative traces + metrics, OTLP export
+
Monorepo
npm workspacesLernaNx
+
Architecture
hexagonal / DDDhandlers → use cases → repositories
+
+
+
+ + +
+
+
+ However you run it +

Frigg doesn't care what you point it at.

+

It runs on infrastructure you own and stays out of your way about what you build on it. The use case is yours to pick. Here's where adopters have actually taken it.

+
+
+
+ Most adopters +

Product integrations for your customers

+

Native, in-product integrations your end users authorize themselves. The CRM sync, the storage connector, the webhook that keeps two SaaS tools in step.

+
// productized · multi-tenant
+
+
+ Some adopters +

Internal business-process automation

+

The plumbing behind your own operations. Moving records between the tools your team already runs, on a schedule or a trigger, without a per-task automation bill.

+
// internal · back-office
+
+
+ Our founder +

Home lab & personal automation

+

Frigg runs the same way for a weekend home-lab project as it does in production. Self and home automation, personal dashboards, whatever you feel like wiring together.

+
// homelab · personal
+
+
+
+
+ + +
+
+
+ Easy to adopt +

Low commitment. High ceiling.

+

Frigg is a framework you add to a Node backend, not a platform you migrate onto. Try it on one integration this week.

+
+
+
+
    +
  • Start on one integration. No rip-and-replace. Prove it on the connector your customers are asking for right now.
  • +
  • It's a library, not a lock-in. Frigg drops into an existing Node.js backend and reads like code you'd have written. Open source under MIT, every line auditable.
  • +
  • Bring your own cloud. Deploy to the AWS account you already have. No new vendor bill, no data leaving your perimeter.
  • +
  • Charted or custom. Install a community module, or let an agent chart a brand-new API. Either way you keep the module.
  • +
  • No seat math. Pricing isn't per-connection or per-seat; it's whatever your own serverless compute costs, which is close to nothing until traffic shows up.
  • +
+
+
+
+ Get started +

One command to scaffold your first app:

+
+
+ $ frigg init my-integrations + +
+ +
+
+
+
+ + +
+
+
+
+ Open core +

Free and MIT-licensed. Commercial when you want a hand.

+

The framework is open source. Use it, fork it, ship it, forever. Left Hook (the team that builds Frigg) also offers a commercial license for teams who want more than the community can give.

+
    +
  • Commercial support and maintenance, with SLAs
  • +
  • Premium, heavy-duty connectors, plugins & extensions
  • +
  • Hands-on help standing up and scaling your integration program
  • +
+
+
+
+ + + + + Left Hook +
+ Talk to Left Hook + + +

The team that builds Frigg, and builds integrations for a living.

+
+
+
+
+ + +
+
+
+ Community +

Build in the open, with people who ship integrations for a living.

+

Join the Frigg Slack Connect channel for help, war stories, and early looks at the agent tooling. Or jump straight into GitHub Discussions.

+

+ + + GitHub Discussions + +

+
+ +
+
+ + + + + + + + + + + + + + + + + + + diff --git a/website/package-lock.json b/website/package-lock.json new file mode 100644 index 000000000..a540e1e72 --- /dev/null +++ b/website/package-lock.json @@ -0,0 +1,96 @@ +{ + "name": "friggframework.org", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "friggframework.org", + "version": "1.0.0", + "dependencies": { + "dotenv": "^16.0.1", + "node-fetch": "^2.6.7" + } + }, + "node_modules/dotenv": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", + "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + }, + "dependencies": { + "dotenv": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", + "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==" + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 000000000..26ec06351 --- /dev/null +++ b/website/package.json @@ -0,0 +1,14 @@ +{ + "name": "friggframework.org", + "version": "1.0.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.71.0", + "@netlify/blobs": "^8.1.0", + "node-fetch": "^2.6.7", + "dotenv": "^16.0.1" + }, + "scripts": { + "functions": "NODE_OPTIONS=--inspect netlify functions:serve", + "netlify dev": "NODE_OPTIONS=--inspect netlify dev" + } +} diff --git a/website/roadmap/data/adrs.json b/website/roadmap/data/adrs.json new file mode 100644 index 000000000..4bdca9551 --- /dev/null +++ b/website/roadmap/data/adrs.json @@ -0,0 +1,275 @@ +{ + "count": 27, + "adrs": [ + { + "num": 1, + "id": "001", + "title": "Use Vite + React for Management UI", + "status": "Accepted", + "date": "2025-01-25", + "theme": "Developer experience", + "summary": "Uses Vite and React for the local management UI so it can reuse existing Frigg UI components and stay lightweight, with no Electron wrapper.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/001-use-vite-for-management-ui.md" + }, + { + "num": 2, + "id": "002", + "title": "No Database for Local Development Tools", + "status": "Accepted", + "date": "2025-01-25", + "theme": "Developer experience", + "summary": "The management GUI keeps no database or persistent storage; all state is runtime memory read from project files, so each session starts fresh.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/002-no-database-for-local-dev.md" + }, + { + "num": 3, + "id": "003", + "title": "Runtime State Only for Management GUI", + "status": "Accepted", + "date": "2025-01-25", + "theme": "Developer experience", + "summary": "The management GUI uses a minimal local-only security model: no auth, no stored credentials, no encryption, localhost CORS, and read-only file access.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/003-runtime-state-only.md" + }, + { + "num": 4, + "id": "004", + "title": "Project Structure Migration Tool", + "status": "Proposed", + "date": "2025-01-25", + "theme": "Developer experience", + "summary": "Defines frigg migrate, a build-time CLI that rewrites an app's project structure from create-frigg-app to the frigg init layout while preserving custom code.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/004-migration-tool-design.md" + }, + { + "num": 5, + "id": "005", + "title": "Admin Script Runner Service", + "status": "Accepted", + "date": "2025-12-10", + "theme": "Developer experience", + "summary": "Adds an Admin Script Runner so adopters register admin operations in the app definition and run them sync or scheduled behind a dedicated admin key.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/005-admin-script-runner.md" + }, + { + "num": 6, + "id": "006", + "title": "Integration Router v2", + "status": "Accepted", + "date": "2025-12-14", + "theme": "Developer experience", + "summary": "Restructures the integration API into a v2 router with consistent naming, credential management, and proxy endpoints, dropping the duplicated modules routes.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/006-integration-router-v2.md" + }, + { + "num": 7, + "id": "007", + "title": "Management UI Architecture", + "status": "Accepted", + "date": "2025-12-14", + "theme": "Developer experience", + "summary": "The management UI runs as a separate Express server that proxies to running Frigg apps, keeping admin API keys off the browser across local and remote.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/007-management-ui-architecture.md" + }, + { + "num": 8, + "id": "008", + "title": "Frigg CLI Start Command", + "status": "Accepted", + "date": "2025-12-14", + "theme": "Infra & deploy", + "summary": "frigg start runs pre-flight checks for Docker, the database, env files, and Prisma, offering to fix problems so local startup just works.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/008-frigg-cli-start-command.md" + }, + { + "num": 9, + "id": "009", + "title": "E2E Test Package", + "status": "Accepted", + "date": "2025-12-15", + "theme": "Developer experience", + "summary": "Adds a self-contained e2e package that spins up a real Express server and in-memory MongoDB to test full integration lifecycles over HTTP.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/009-e2e-test-package.md" + }, + { + "num": 10, + "id": "010", + "title": "Reporting as an Admin Operation", + "status": "Accepted", + "date": "2026-07-03", + "theme": "Observability & ops", + "summary": "Treats reporting as an admin operation so reports register in the app definition and run on the shared script-runner instead of a hardcoded core endpoint.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/010-reporting-as-admin-operation.md" + }, + { + "num": 11, + "id": "011", + "title": "Integration Telemetry, Eventing & Feature-Usage Tracking", + "status": "Accepted", + "date": "2026-07-03", + "theme": "Observability & ops", + "summary": "Adopts OpenTelemetry as a vendor-neutral telemetry layer with auto-instrumentation and a durable per-integration usage store that feeds reports.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/011-integration-telemetry-and-usage-tracking.md" + }, + { + "num": 12, + "id": "012", + "title": "Database Schema Migrations", + "status": "Proposed", + "date": "2026-07-04", + "theme": "Data & migrations", + "summary": "Runs Prisma schema migrations from a VPC-internal Lambda triggered by CI/CD, tracking status in S3 so it needs no dependency on the tables it creates.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/012-database-schema-migrations.md" + }, + { + "num": 13, + "id": "013", + "title": "Integration Version Migrations", + "status": "Proposed", + "date": "2026-07-04", + "theme": "Data & migrations", + "summary": "Makes integration version migration a first-class admin operation that idempotently transforms persisted records, config, and mappings across versions.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/013-integration-version-migrations.md" + }, + { + "num": 14, + "id": "014", + "title": "One Numbered ADR Register", + "status": "Accepted", + "date": "2026-07-04", + "theme": "Developer experience", + "summary": "Consolidates Frigg's two ADR sets into one numbered register under docs/architecture-decisions with a single structure, header, and index.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/014-consolidate-adr-register.md" + }, + { + "num": 15, + "id": "015", + "title": "Extensions Taxonomy", + "status": "Proposed", + "date": "2026-06-09", + "theme": "Extensions & plugins", + "summary": "Defines Extensions as optional add-on code split into core, integration, and API-module types, distinct from required infrastructure Plugins.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/015-extensions-taxonomy.md" + }, + { + "num": 16, + "id": "016", + "title": "Plugins", + "status": "Proposed", + "date": "2026-06-09", + "theme": "Extensions & plugins", + "summary": "Defines Plugins as swappable infrastructure packages (provider, database, encryption, queue, scheduler) so core depends on interfaces, not bundled deps.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/016-plugins.md" + }, + { + "num": 17, + "id": "017", + "title": "Core Extensions", + "status": "Proposed", + "date": "2026-06-09", + "theme": "Extensions & plugins", + "summary": "Core Extensions add optional app-level functionality like alerting, monitoring, and a Slack admin bot, declared on appDefinition.extensions.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/017-core-extensions.md" + }, + { + "num": 18, + "id": "018", + "title": "Integration Extensions", + "status": "Implemented", + "date": "2026-06-09", + "theme": "Extensions & plugins", + "summary": "Integration Extensions let an API module or shared library ship a bundle of routes, events, queues, and workers that an integration binds declaratively.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/018-integration-extensions.md" + }, + { + "num": 19, + "id": "019", + "title": "API Module Extensions", + "status": "Proposed", + "date": "2026-06-09", + "theme": "Extensions & plugins", + "summary": "API Module Extensions are the producer side: an API module exports reusable extension bundles under extensions.{name} with provider quirks built in.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/019-api-module-extensions.md" + }, + { + "num": 20, + "id": "020", + "title": "Capabilities", + "status": "Proposed", + "date": "2026-06-09", + "theme": "Agent tooling", + "summary": "Capabilities are typed declarations on a Definition that name what a module, integration, or app can do and point at its spec and implementation.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/020-capabilities.md" + }, + { + "num": 21, + "id": "021", + "title": "Ontology", + "status": "Proposed", + "date": "2026-06-09", + "theme": "Agent tooling", + "summary": "The Ontology is layered, versioned context (universal, framework, vendor, instance) that compiles into an XML block agents see at session start.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/021-ontology.md" + }, + { + "num": 22, + "id": "022", + "title": "Artifacts", + "status": "Proposed", + "date": "2026-06-09", + "theme": "Extensions & plugins", + "summary": "Artifacts are code or config an API module helps generate that runs outside Frigg on the provider's platform, with Frigg serving the runtime bridge.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/022-artifacts.md" + }, + { + "num": 23, + "id": "023", + "title": "Integration Templates", + "status": "Proposed", + "date": "2026-06-09", + "theme": "Agent tooling", + "summary": "Integration Templates are category base classes an adopter copies into their codebase, ShadCN-style, with sync and mapping pre-built and owned by them.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/023-integration-templates.md" + }, + { + "num": 24, + "id": "024", + "title": "Global Entities", + "status": "Proposed", + "date": "2024-12-18", + "theme": "Data & migrations", + "summary": "Global Entities let integrations use shared app-owner service accounts instead of per-user credentials, and need the currently missing schema fields.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/024-global-entities.md" + }, + { + "num": 25, + "id": "025", + "title": "Agent Harness", + "status": "Proposed", + "date": "2026-06-09", + "theme": "Agent tooling", + "summary": "The Agent Harness is a package wiring ontology, capabilities, plugins, and templates into Claude Code's session so agents ground and validate Frigg work.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/025-agent-harness.md" + }, + { + "num": 26, + "id": "026", + "title": "Evals", + "status": "Proposed", + "date": "2026-06-09", + "theme": "Agent tooling", + "summary": "Builds an 8-condition factorial eval to measure whether capabilities, ontology, and the harness each improve agent accuracy on Frigg tasks.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/026-evals.md" + }, + { + "num": 27, + "id": "027", + "title": "SSM Parameter Offload and Per-Function Environment Scoping", + "status": "Accepted", + "date": "2026-07-10", + "theme": "Infra & deploy", + "summary": "Offloads large secrets to SSM and scopes env vars per function so Lambdas stay under the 4KB environment limit as the integration count grows.", + "url": "https://github.com/friggframework/frigg/blob/next/docs/architecture-decisions/027-ssm-parameter-offload-and-env-scoping.md" + } + ] +} diff --git a/website/roadmap/data/apis.json b/website/roadmap/data/apis.json new file mode 100644 index 000000000..55f6b68db --- /dev/null +++ b/website/roadmap/data/apis.json @@ -0,0 +1 @@ +{"generatedFrom":"left-hook-catalog","count":224,"categories":["ai","analytics","commerce","communication","crm","devtools","finance","hr","marketing","other","productivity","storage","support"],"apis":[{"slug":"act-on","name":"Act-On","provider":"Act-On","category":"marketing","description":"Marketing automation platform for B2B companies with lead generation, email marketing, and analytics.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["marketing","automation","email","b2b"],"logo":"logos/act-on.jpg","built":false,"library":null},{"slug":"activecampaign","name":"ActiveCampaign","provider":"ActiveCampaign","category":"crm","description":"Email marketing automation platform with CRM, marketing automation, and customer experience tools.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","email","marketing","automation"],"logo":"logos/activecampaign.jpg","library":"updating","built":true},{"slug":"acuity-scheduling","name":"Acuity Scheduling","provider":"Squarespace","category":"productivity","description":"Online appointment scheduling software for businesses with client self-booking and calendar sync.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["scheduling","appointments","booking","calendar"],"logo":"logos/acuity-scheduling.jpg","built":false,"library":null},{"slug":"marketo-engage","name":"Adobe Marketo Engage","provider":"Adobe","category":"marketing","description":"B2B marketing automation platform (formerly Marketo) for lead management, campaigns, and revenue attribution.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["marketing","automation","b2b","lead-management"],"logo":"logos/marketo-engage.jpg","built":true,"library":"updating"},{"slug":"adobe-sign","name":"Adobe Sign","provider":"Adobe","category":"productivity","description":"Electronic signature and document workflow solution for signing agreements and automating approvals.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["esignature","documents","workflows","agreements"],"logo":"logos/adobe-sign.jpg","built":false,"library":null},{"slug":"adp","name":"ADP","provider":"ADP","category":"hr","description":"Human capital management platform with payroll, HR, talent management, and benefits administration APIs.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["hr","payroll","hcm","benefits"],"logo":"logos/adp.jpg","built":false,"library":null},{"slug":"agile-crm","name":"Agile CRM","provider":"Agile CRM","category":"crm","description":"All-in-one CRM with sales, marketing, and service automation for small to medium businesses.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","sales","marketing","automation"],"logo":"logos/agile-crm.jpg","built":false,"library":null},{"slug":"airtable","name":"Airtable","provider":"Airtable","category":"productivity","description":"Cloud-based spreadsheet-database hybrid with flexible API for building collaborative applications.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["database","spreadsheet","collaboration","no-code"],"logo":"logos/airtable.jpg","built":false,"library":null},{"slug":"airwallex","name":"Airwallex","provider":"Airwallex","category":"finance","description":"Global payments and financial infrastructure platform for cross-border transactions and multi-currency accounts.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["payments","international","fintech","multi-currency"],"logo":"logos/airwallex.jpg","library":"updating","built":true},{"slug":"alchemer","name":"Alchemer","provider":"Alchemer","category":"marketing","description":"Survey and feedback platform (formerly SurveyGizmo) for customer insights and market research.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["surveys","feedback","research","analytics"],"logo":"logos/alchemer.jpg","built":false,"library":null},{"slug":"api-gateway","name":"Amazon API Gateway","provider":"Amazon Web Services","category":"devtools","description":"Fully managed service to create, publish, maintain, monitor, and secure APIs at any scale.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["api","gateway","rest","websocket","serverless"],"logo":null,"built":false,"library":null},{"slug":"sns","name":"Amazon SNS","provider":"Amazon Web Services","category":"communication","description":"Simple Notification Service for pub/sub messaging and mobile push notifications.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["messaging","notifications","pubsub","sms"],"logo":null,"built":false,"library":null},{"slug":"sqs","name":"Amazon SQS","provider":"Amazon Web Services","category":"devtools","description":"Simple Queue Service for fully managed message queuing for microservices and distributed systems.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["queue","messaging","distributed","async"],"logo":null,"built":false,"library":null},{"slug":"ams360","name":"AMS360","provider":"Vertafore","category":"finance","description":"Agency management system API for insurance agencies with policy management, accounting, and client data integration.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["insurance","agency-management","policy","accounting"],"logo":"logos/ams360.jpg","built":false,"library":null},{"slug":"anthropic-claude","name":"Anthropic Claude","provider":"Anthropic","category":"ai","description":"Claude AI models and the Model Context Protocol (MCP). Build AI assistants, MCP servers, Claude connectors, and tool-use integrations.","status":"Active","complexity":"Medium","experience":"Advanced","featured":true,"tags":["AI","LLM","MCP","agents","tool-use","connectors"],"logo":null,"built":false,"library":null},{"slug":"applied-epic","name":"Applied Epic","provider":"Applied Systems","category":"finance","description":"Insurance agency management platform API with policy lifecycle management, claims processing, and client data integration.","status":"Active","complexity":"High","experience":"Intermediate","featured":false,"tags":["insurance","agency-management","policy","claims"],"logo":"logos/applied-epic.jpg","built":false,"library":null},{"slug":"applied-rater","name":"Applied Rater","provider":"Applied Systems","category":"finance","description":"Insurance rating engine API for comparative quoting, real-time rate comparison, and policy pricing across carriers.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["insurance","rating","quoting","pricing"],"logo":"logos/applied-rater.jpg","built":false,"library":null},{"slug":"asana","name":"Asana","provider":"Asana","category":"productivity","description":"Work management platform for organizing and tracking team projects, tasks, and workflows.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["project-management","tasks","collaboration","workflows"],"logo":"logos/asana.jpg","library":"ready","built":true},{"slug":"assemblyai","name":"AssemblyAI","provider":"AssemblyAI","category":"ai","description":"REST API for speech-to-text transcription with speaker diarization, sentiment analysis, and real-time streaming.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["ai","speech-to-text","transcription","audio"],"logo":"logos/assemblyai.jpg","built":false,"library":null},{"slug":"athenahealth","name":"Athenahealth","provider":"Athenahealth","category":"other","description":"athenaClinicals API for electronic health records, patient scheduling, clinical documentation, and practice management.","status":"Active","complexity":"High","experience":"Intermediate","featured":false,"tags":["healthcare","ehr","clinical","patient-management"],"logo":"logos/athenahealth.jpg","built":false,"library":null},{"slug":"attentive","name":"Attentive","provider":"Attentive","category":"marketing","description":"SMS marketing API for subscriber management, campaigns, segmentation, and personalized messaging.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["sms","marketing","ecommerce","messaging"],"logo":"logos/attentive.jpg","library":"updating","built":true},{"slug":"authorize-net","name":"Authorize.Net","provider":"Visa","category":"finance","description":"Payment gateway for accepting credit card and electronic check payments online and in-person.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["payments","gateway","credit-cards","ecommerce"],"logo":"logos/authorize-net.jpg","built":false,"library":null},{"slug":"autotask-psa","name":"Autotask PSA","provider":"Datto","category":"productivity","description":"Professional services automation platform (acquired by Datto) for MSPs with ticketing, billing, and project management.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["psa","msp","ticketing","project-management"],"logo":"logos/autotask-psa.jpg","built":false,"library":null},{"slug":"availity","name":"Availity","provider":"Availity","category":"other","description":"Essentials API for healthcare revenue cycle management with eligibility verification, claims processing, and payer connectivity.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["healthcare","insurance","claims","eligibility"],"logo":"logos/availity.jpg","built":false,"library":null},{"slug":"account-management","name":"AWS Account Management","provider":"Amazon Web Services","category":"devtools","description":"APIs for managing AWS accounts, contacts, billing, and organizational settings.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["account","billing","organization","management"],"logo":"logos/account-management.jpg","built":false,"library":null},{"slug":"cloudwatch","name":"AWS CloudWatch","provider":"Amazon Web Services","category":"devtools","description":"Monitoring and observability service for AWS resources and applications with logs, metrics, and alarms.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["monitoring","logs","metrics","observability"],"logo":null,"built":false,"library":null},{"slug":"dynamodb","name":"AWS DynamoDB","provider":"Amazon Web Services","category":"storage","description":"Fully managed NoSQL database service with fast and predictable performance at any scale.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["database","nosql","serverless","key-value"],"logo":null,"built":false,"library":null},{"slug":"elasticache","name":"AWS ElastiCache","provider":"Amazon Web Services","category":"storage","description":"Fully managed in-memory caching service supporting Redis and Memcached for sub-millisecond latency.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["cache","redis","memcached","performance"],"logo":null,"built":false,"library":null},{"slug":"kms","name":"AWS KMS","provider":"Amazon Web Services","category":"devtools","description":"Key Management Service for creating and controlling encryption keys to secure your data.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["security","encryption","keys","compliance"],"logo":null,"built":false,"library":null},{"slug":"lambda","name":"AWS Lambda","provider":"Amazon Web Services","category":"devtools","description":"Serverless compute service that runs code in response to events without provisioning or managing servers.","status":"Active","complexity":"Low","experience":"Expert","featured":false,"tags":["serverless","compute","functions","event-driven"],"logo":null,"built":false,"library":null},{"slug":"s3","name":"AWS S3","provider":"Amazon Web Services","category":"storage","description":"Simple Storage Service for scalable object storage with industry-leading durability and availability.","status":"Active","complexity":"Low","experience":"Expert","featured":false,"tags":["storage","object-storage","files","backup"],"logo":null,"built":false,"library":null},{"slug":"bamboohr","name":"BambooHR","provider":"BambooHR","category":"hr","description":"HR software for small and medium businesses with employee data, time tracking, and performance management.","status":"Active","complexity":"Low","experience":"Intermediate","featured":true,"tags":["hr","employee","time-tracking","performance"],"logo":"logos/bamboohr.jpg","built":false,"library":null},{"slug":"benchmark-email","name":"Benchmark Email","provider":"Benchmark","category":"marketing","description":"Email marketing platform with drag-and-drop editor, automation, and subscriber management.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["email","marketing","automation","newsletters"],"logo":"logos/benchmark-email.jpg","built":false,"library":null},{"slug":"bitbucket","name":"Bitbucket","provider":"Atlassian","category":"devtools","description":"Git-based source code repository hosting service with CI/CD pipelines and code collaboration features.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["git","source-control","ci-cd","devops"],"logo":"logos/bitbucket.jpg","built":false,"library":null},{"slug":"bloomerang","name":"Bloomerang","provider":"Bloomerang","category":"crm","description":"REST API for donor management and nonprofit CRM with fundraising, engagement tracking, and donation processing.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["nonprofit","donor-management","fundraising","crm"],"logo":"logos/bloomerang.jpg","built":false,"library":null},{"slug":"boomset","name":"Boomset","provider":"Boomset","category":"marketing","description":"Event management and registration platform for check-in, badge printing, and attendee engagement.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["events","registration","check-in","badges"],"logo":"logos/boomset.jpg","built":false,"library":null},{"slug":"box","name":"Box","provider":"Box","category":"storage","description":"Enterprise cloud content management platform for secure file sharing, collaboration, and workflow automation.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["storage","files","collaboration","enterprise"],"logo":"logos/box.jpg","built":false,"library":null},{"slug":"bulksms","name":"BulkSMS","provider":"BulkSMS","category":"communication","description":"SMS messaging platform for sending bulk text messages and mobile notifications globally.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["sms","messaging","mobile","notifications"],"logo":"logos/bulksms.jpg","built":false,"library":null},{"slug":"bullhorn","name":"Bullhorn","provider":"Bullhorn","category":"crm","description":"REST API for staffing and recruiting CRM with candidate management, job order tracking, and placement automation.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["recruiting","staffing","crm","hr"],"logo":"logos/bullhorn.jpg","built":false,"library":null},{"slug":"calendly","name":"Calendly","provider":"Calendly","category":"productivity","description":"Scheduling automation platform for booking meetings and appointments without the back-and-forth.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["scheduling","meetings","appointments","calendar"],"logo":"logos/calendly.jpg","built":false,"library":null},{"slug":"campaign-monitor","name":"Campaign Monitor","provider":"Campaign Monitor","category":"marketing","description":"Email marketing and automation platform for creating, sending, and tracking email campaigns.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["email","marketing","campaigns","automation"],"logo":"logos/campaign-monitor.jpg","built":false,"library":null},{"slug":"canva","name":"Canva","provider":"Canva","category":"marketing","description":"Design platform API for creating graphics, accessing templates, and integrating design workflows into applications.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["design","graphics","templates","creative"],"logo":"logos/canva.jpg","built":false,"library":null},{"slug":"capsule","name":"Capsule","provider":"Capsule","category":"crm","description":"Simple yet powerful CRM for managing contacts, sales opportunities, and customer relationships.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["crm","contacts","sales","relationships"],"logo":"logos/capsule.jpg","built":false,"library":null},{"slug":"chargify","name":"Chargify","provider":"Maxio","category":"finance","description":"Subscription billing platform (now part of Maxio) for recurring revenue and subscription management.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["billing","subscriptions","recurring","revenue"],"logo":"logos/chargify.jpg","built":false,"library":null},{"slug":"webex","name":"Cisco Webex","provider":"Cisco","category":"communication","description":"Enterprise video conferencing and team collaboration platform with messaging and meeting APIs.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["video","conferencing","collaboration","meetings"],"logo":"logos/webex.jpg","built":false,"library":null},{"slug":"clearbit","name":"Clearbit","provider":"Clearbit","category":"analytics","description":"Enrichment API for company and contact data with real-time lookups, lead scoring, and business intelligence.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["enrichment","data","lead-scoring","intelligence"],"logo":"logos/clearbit.jpg","built":false,"library":null},{"slug":"clientsuccess","name":"ClientSuccess","provider":"ClientSuccess","category":"crm","description":"Customer success management platform for tracking health scores, renewals, and customer engagement.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["customer-success","retention","health-scores","renewals"],"logo":"logos/clientsuccess.jpg","built":false,"library":null},{"slug":"clio","name":"Clio","provider":"Clio","category":"productivity","description":"Legal practice management software for case management, billing, and client communications.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["legal","practice-management","billing","case-management"],"logo":"logos/clio.jpg","built":false,"library":null},{"slug":"close","name":"Close","provider":"Close","category":"crm","description":"Sales CRM built for inside sales teams with built-in calling, email, and sales automation.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","sales","calling","automation"],"logo":"logos/close.jpg","built":false,"library":null},{"slug":"coda","name":"Coda","provider":"Coda","category":"productivity","description":"All-in-one collaborative workspace combining documents, spreadsheets, and apps with automation.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["documents","collaboration","automation","no-code"],"logo":"logos/coda.jpg","built":false,"library":null},{"slug":"confluence","name":"Confluence","provider":"Atlassian","category":"productivity","description":"Team workspace and wiki platform for creating, organizing, and sharing documentation and knowledge.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["wiki","documentation","collaboration","knowledge-base"],"logo":"logos/confluence.jpg","built":false,"library":null},{"slug":"connectwise","name":"ConnectWise","provider":"ConnectWise","category":"productivity","description":"Business management platform for MSPs and technology solution providers with PSA and RMM tools.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["psa","msp","rmm","business-management"],"logo":"logos/connectwise.jpg","library":"ready","built":true},{"slug":"constant-contact","name":"Constant Contact","provider":"Constant Contact","category":"marketing","description":"Email marketing platform for small businesses with campaigns, automation, and contact management.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["email","marketing","small-business","automation"],"logo":"logos/constant-contact.jpg","built":false,"library":null},{"slug":"contentful","name":"Contentful","provider":"Contentful","category":"devtools","description":"Composable content platform API for content models, entries, assets, and localization.","status":"Active","complexity":"Medium","experience":"Expert","featured":false,"tags":["cms","headless","content","composable"],"logo":"logos/contentful.jpg","library":"ready","built":true},{"slug":"contentstack","name":"Contentstack","provider":"Contentstack","category":"devtools","description":"Headless CMS API for managing content models, entries, assets, and publishing workflows.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["cms","headless","content","api-first"],"logo":"logos/contentstack.jpg","library":"ready","built":true},{"slug":"copper","name":"Copper","provider":"Copper","category":"crm","description":"Google Workspace-native CRM for managing relationships, deals, and workflows directly within Gmail.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","google-workspace","sales","relationships"],"logo":"logos/copper.jpg","built":false,"library":null},{"slug":"crelate","name":"Crelate","provider":"Crelate","category":"crm","description":"REST API for talent relationship management with recruiting, candidate tracking, and staffing workflow automation.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["recruiting","talent-management","crm","staffing"],"logo":"logos/crelate.jpg","built":false,"library":null},{"slug":"crossbeam","name":"Crossbeam","provider":"Crossbeam","category":"analytics","description":"Partner ecosystem platform for securely comparing customer data and identifying co-selling opportunities.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["partnerships","ecosystem","analytics","co-selling"],"logo":"logos/crossbeam.jpg","library":"ready","built":true},{"slug":"cvent","name":"Cvent","provider":"Cvent","category":"marketing","description":"Event management platform for planning, marketing, and analyzing meetings, conferences, and events.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["events","conferences","registration","marketing"],"logo":"logos/cvent.jpg","built":false,"library":null},{"slug":"deel","name":"Deel","provider":"Deel","category":"hr","description":"Global payroll API for managing international contractors, employees, and compliance across 150+ countries.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["payroll","hr","global","compliance"],"logo":"logos/deel.jpg","library":"ready","built":true},{"slug":"dialpad","name":"Dialpad","provider":"Dialpad","category":"communication","description":"REST API for cloud communications with voice, video, messaging, and AI-powered call analytics.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["communication","voice","telephony","ai"],"logo":"logos/dialpad.jpg","built":false,"library":null},{"slug":"dispatch","name":"Dispatch","provider":"Dispatch","category":"productivity","description":"Field service management platform for scheduling, dispatching, and tracking service technicians.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["field-service","dispatch","scheduling","technicians"],"logo":"logos/dispatch.jpg","built":false,"library":null},{"slug":"docusign","name":"Docusign","provider":"Docusign","category":"productivity","description":"Electronic signature platform for sending, signing, and managing digital agreements and contracts.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["esignature","documents","contracts","agreements"],"logo":"logos/docusign.jpg","built":false,"library":null},{"slug":"drchrono","name":"DrChrono","provider":"DrChrono","category":"other","description":"EHR API for electronic health records with patient charting, medical billing, scheduling, and clinical data management.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["healthcare","ehr","medical-billing","scheduling"],"logo":"logos/drchrono.jpg","built":false,"library":null},{"slug":"drift","name":"Drift","provider":"Drift","category":"support","description":"Conversational marketing and sales platform with live chat, chatbots, and customer engagement tools.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["chat","conversational","sales","marketing"],"logo":"logos/drift.jpg","built":false,"library":null},{"slug":"dropbox","name":"Dropbox","provider":"Dropbox","category":"storage","description":"Cloud storage and file synchronization service for storing, sharing, and collaborating on files.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["storage","files","sync","collaboration"],"logo":"logos/dropbox.jpg","built":false,"library":null},{"slug":"dropbox-sign","name":"Dropbox Sign","provider":"Dropbox","category":"productivity","description":"eSign API for electronic signatures with document workflows, template management, and embedded signing experiences.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["esignature","documents","workflow","signing"],"logo":"logos/dropbox-sign.jpg","built":false,"library":null},{"slug":"drupal","name":"Drupal","provider":"Drupal","category":"other","description":"Open-source content management system with RESTful API for building websites and web applications.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["cms","content","websites","open-source"],"logo":"logos/drupal.jpg","built":false,"library":null},{"slug":"encompass","name":"Encompass","provider":"ICE Mortgage Technology","category":"finance","description":"Loan Origination API for mortgage processing with loan management, compliance automation, and document generation.","status":"Active","complexity":"High","experience":"Intermediate","featured":false,"tags":["mortgage","lending","loan-origination","compliance"],"logo":"logos/encompass.jpg","built":false,"library":null},{"slug":"epic","name":"Epic","provider":"Epic","category":"other","description":"FHIR/HL7 API for electronic health records with clinical data exchange, patient access, and healthcare interoperability.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["healthcare","ehr","fhir","hl7"],"logo":"logos/epic.jpg","built":false,"library":null},{"slug":"etsy","name":"Etsy","provider":"Etsy","category":"commerce","description":"Marketplace platform API for handmade and vintage items with listings, orders, and shop management.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["marketplace","ecommerce","handmade","vintage"],"logo":"logos/etsy.jpg","built":false,"library":null},{"slug":"eventbrite","name":"Eventbrite","provider":"Eventbrite","category":"marketing","description":"Event management and ticketing platform for creating, promoting, and managing events.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["events","ticketing","registration","marketing"],"logo":"logos/eventbrite.jpg","built":false,"library":null},{"slug":"facebook-graph","name":"Facebook","provider":"Meta","category":"marketing","description":"Graph API for Facebook platform integration with pages, ads, user data, and social media management.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["social-media","advertising","marketing","graph-api"],"logo":null,"built":false,"library":null},{"slug":"fidelity","name":"Fidelity","provider":"Fidelity Investments","category":"finance","description":"Institutional API for investment management with portfolio data, trade execution, and account administration.","status":"Active","complexity":"High","experience":"Intermediate","featured":false,"tags":["finance","investments","portfolio","institutional"],"logo":"logos/fidelity.jpg","built":false,"library":null},{"slug":"fireflies-ai","name":"Fireflies.ai","provider":"Fireflies.ai","category":"ai","description":"GraphQL API for AI meeting transcription with conversation intelligence, note-taking, and meeting analytics.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["ai","transcription","meetings","analytics"],"logo":"logos/fireflies-ai.jpg","built":false,"library":null},{"slug":"fis","name":"FIS","provider":"FIS","category":"finance","description":"Payment Processing API for financial services with transaction management, merchant services, and banking infrastructure.","status":"Active","complexity":"High","experience":"Intermediate","featured":false,"tags":["payments","banking","fintech","processing"],"logo":"logos/fis.jpg","built":false,"library":null},{"slug":"fiserv","name":"Fiserv","provider":"Fiserv","category":"finance","description":"Banking API for financial technology with payment processing, account management, and digital banking solutions.","status":"Active","complexity":"High","experience":"Intermediate","featured":false,"tags":["banking","payments","fintech","digital-banking"],"logo":"logos/fiserv.jpg","built":false,"library":null},{"slug":"fishbowl","name":"Fishbowl","provider":"Fishbowl","category":"commerce","description":"REST API for inventory management with warehouse operations, manufacturing, order fulfillment, and asset tracking.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["inventory","warehouse","manufacturing","commerce"],"logo":"logos/fishbowl.jpg","built":false,"library":null},{"slug":"formsite","name":"Formsite","provider":"Formsite","category":"productivity","description":"Online form builder for creating surveys, registration forms, and order forms with data collection.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["forms","surveys","data-collection","registration"],"logo":"logos/formsite.jpg","built":false,"library":null},{"slug":"formstack","name":"Formstack","provider":"Formstack","category":"productivity","description":"Online form builder and workflow automation platform for data collection, signatures, and document generation.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["forms","workflow","automation","data-collection"],"logo":"logos/formstack.jpg","built":false,"library":null},{"slug":"freshbooks","name":"FreshBooks","provider":"FreshBooks","category":"finance","description":"Cloud accounting software for small businesses with invoicing, expenses, and time tracking.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["accounting","invoicing","expenses","small-business"],"logo":"logos/freshbooks.jpg","library":"updating","built":true},{"slug":"freshdesk","name":"Freshdesk","provider":"Freshworks","category":"support","description":"Cloud-based customer support software with ticketing, knowledge base, and multichannel support.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["support","helpdesk","ticketing","customer-service"],"logo":"logos/freshdesk.jpg","built":false,"library":null},{"slug":"freshsales","name":"Freshsales","provider":"Freshworks","category":"crm","description":"Sales CRM with AI-powered lead scoring, built-in phone, email, and sales pipeline management.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","sales","leads","pipeline"],"logo":"logos/freshsales.jpg","built":false,"library":null},{"slug":"front","name":"FrontApp","provider":"Front","category":"support","description":"Collaborative inbox for teams to manage shared email, messages, and customer communications.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["email","inbox","collaboration","support"],"logo":"logos/front.jpg","library":"updating","built":true},{"slug":"frontify","name":"Frontify","provider":"Frontify","category":"marketing","description":"Brand management API for digital asset management, brand guidelines, and creative collaboration.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["brand","dam","design","assets"],"logo":"logos/frontify.jpg","library":"updating","built":true},{"slug":"github","name":"GitHub","provider":"GitHub","category":"devtools","description":"Development platform for version control, code hosting, CI/CD, and collaborative software development.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["git","source-control","ci-cd","devops"],"logo":"logos/github.jpg","built":false,"library":null},{"slug":"gmail","name":"Gmail API","provider":"Google","category":"communication","description":"Flexible RESTful API for reading, sending, and organizing email with full Gmail functionality.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["email","messaging","workspace","communication"],"logo":"logos/gmail.jpg","built":false,"library":null},{"slug":"analytics","name":"Google Analytics","provider":"Google","category":"analytics","description":"Web analytics platform (GA4) for measuring website traffic, user behavior, and conversion tracking.","status":"Active","complexity":"Medium","experience":"Expert","featured":false,"tags":["analytics","tracking","metrics","reporting"],"logo":"logos/analytics.jpg","built":false,"library":null},{"slug":"google-calendar","name":"Google Calendar API","provider":"Google","category":"productivity","description":"RESTful API for creating, managing, and syncing calendar events across Google Workspace.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["calendar","scheduling","events","workspace"],"logo":"logos/google-calendar.jpg","library":"ready","built":true},{"slug":"google-chat","name":"Google Chat API","provider":"Google","category":"communication","description":"Team collaboration API for sending messages, creating spaces, and building Chat apps for Workspace.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["chat","messaging","collaboration","workspace"],"logo":"logos/google-chat.jpg","built":false,"library":null},{"slug":"cloud-platform","name":"Google Cloud Platform APIs","provider":"Google","category":"devtools","description":"Unified API access to 200+ Google Cloud services including compute, storage, AI, and networking.","status":"Active","complexity":"High","experience":"Expert","featured":false,"tags":["cloud","infrastructure","compute","ai"],"logo":"logos/cloud-platform.jpg","built":false,"library":null},{"slug":"contacts","name":"Google Contacts API","provider":"Google","category":"crm","description":"People API for managing contact information, groups, and profiles across Google services.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["contacts","crm","people","workspace"],"logo":"logos/contacts.jpg","built":false,"library":null},{"slug":"docs","name":"Google Docs API","provider":"Google","category":"productivity","description":"Create, read, and update Google Docs documents programmatically with rich text formatting.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["documents","word-processing","productivity","workspace"],"logo":"logos/docs.jpg","built":false,"library":null},{"slug":"google-drive","name":"Google Drive API","provider":"Google","category":"storage","description":"Cloud storage API for uploading, organizing, and sharing files across Google Workspace.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["storage","files","cloud","workspace"],"logo":"logos/google-drive.jpg","library":"ready","built":true},{"slug":"maps","name":"Google Maps API","provider":"Google","category":"devtools","description":"Comprehensive mapping platform with geocoding, directions, places, and interactive map visualization.","status":"Active","complexity":"Medium","experience":"Expert","featured":false,"tags":["maps","location","geocoding","directions"],"logo":"logos/maps.jpg","built":false,"library":null},{"slug":"sheets","name":"Google Sheets API","provider":"Google","category":"productivity","description":"Spreadsheet API for reading, writing, and formatting data in Google Sheets programmatically.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["spreadsheet","data","productivity","workspace"],"logo":"logos/sheets.jpg","built":false,"library":null},{"slug":"gorgias","name":"Gorgias","provider":"Gorgias","category":"support","description":"E-commerce focused helpdesk with ticket management, automation, and deep Shopify integration.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["support","ecommerce","helpdesk","automation"],"logo":"logos/gorgias.jpg","library":"updating","built":true},{"slug":"goto","name":"GoTo","provider":"GoTo","category":"communication","description":"Business communication suite with video meetings, webinars, phone, and remote support solutions.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["video","meetings","webinars","communication"],"logo":"logos/goto.jpg","built":false,"library":null},{"slug":"gravity-forms","name":"Gravity Forms","provider":"Gravity Forms","category":"productivity","description":"WordPress form builder plugin with advanced fields, conditional logic, and extensive integration options.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["forms","wordpress","data-collection","automation"],"logo":"logos/gravity-forms.jpg","built":false,"library":null},{"slug":"greenhouse","name":"Greenhouse","provider":"Greenhouse","category":"hr","description":"Applicant tracking and recruiting software with structured hiring workflows and candidate management.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["hr","recruiting","ats","hiring"],"logo":"logos/greenhouse.jpg","built":false,"library":null},{"slug":"guesty","name":"Guesty","provider":"Guesty","category":"commerce","description":"Property management platform for short-term rentals with channel management and guest communication.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["property-management","rentals","hospitality","booking"],"logo":"logos/guesty.jpg","built":false,"library":null},{"slug":"gusto","name":"Gusto","provider":"Gusto","category":"hr","description":"All-in-one people platform for payroll, benefits, and HR management for small businesses.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["hr","payroll","benefits","small-business"],"logo":"logos/gusto.jpg","built":false,"library":null},{"slug":"help-scout","name":"Help Scout","provider":"Help Scout","category":"support","description":"Customer service platform with shared inbox, knowledge base, and live chat for support teams.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["support","helpdesk","inbox","knowledge-base"],"logo":"logos/help-scout.jpg","library":"ready","built":true},{"slug":"hootsuite","name":"Hootsuite","provider":"Hootsuite","category":"marketing","description":"Social media management platform for scheduling, publishing, and analyzing social content.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["social-media","scheduling","marketing","analytics"],"logo":"logos/hootsuite.jpg","built":false,"library":null},{"slug":"hubspot","name":"HubSpot CRM","provider":"HubSpot","category":"crm","description":"Complete CRM platform with contacts, deals, companies, and marketing automation.","status":"Active","complexity":"Medium","experience":"Expert","featured":true,"tags":["crm","marketing","sales","automation"],"logo":"logos/hubspot.jpg","library":"ready","built":true},{"slug":"icims","name":"iCIMS","provider":"iCIMS","category":"hr","description":"Enterprise talent acquisition platform with applicant tracking, onboarding, and recruitment marketing.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["hr","recruiting","ats","enterprise"],"logo":"logos/icims.jpg","built":false,"library":null},{"slug":"icontact","name":"iContact","provider":"iContact","category":"marketing","description":"Email marketing platform for creating campaigns, managing contacts, and tracking engagement.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["email","marketing","campaigns","contacts"],"logo":"logos/icontact.jpg","built":false,"library":null},{"slug":"ideascale","name":"IdeaScale","provider":"IdeaScale","category":"support","description":"Innovation management platform for collecting, evaluating, and implementing ideas from customers and employees.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["innovation","ideas","feedback","crowdsourcing"],"logo":"logos/ideascale.jpg","built":false,"library":null},{"slug":"ifttt","name":"IFTTT","provider":"IFTTT","category":"devtools","description":"Automation platform for connecting apps and devices with simple conditional statements and applets.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["automation","integration","iot","applets"],"logo":"logos/ifttt.jpg","built":false,"library":null},{"slug":"insightly","name":"Insightly","provider":"Insightly","category":"crm","description":"CRM and project management platform for small to mid-sized businesses with relationship linking.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","project-management","contacts","relationships"],"logo":"logos/insightly.jpg","built":false,"library":null},{"slug":"intercom","name":"Intercom","provider":"Intercom","category":"support","description":"Customer messaging platform with live chat, support ticketing, and customer engagement automation.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["support","messaging","chat","engagement"],"logo":"logos/intercom.jpg","built":false,"library":null},{"slug":"ironclad","name":"Ironclad","provider":"Ironclad","category":"productivity","description":"Contract lifecycle management API for creating, negotiating, and managing contracts programmatically.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["contracts","legal","clm","workflow"],"logo":"logos/ironclad.jpg","library":"ready","built":true},{"slug":"ivans","name":"IVANS","provider":"IVANS","category":"finance","description":"Insurance Exchange API for policy download, eDocs delivery, and real-time connectivity between agencies and carriers.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["insurance","policy","exchange","carrier"],"logo":"logos/ivans.jpg","built":false,"library":null},{"slug":"jack-henry","name":"Jack Henry","provider":"Jack Henry","category":"finance","description":"Open Banking API for financial institutions with account management, payment processing, and digital banking services.","status":"Active","complexity":"High","experience":"Intermediate","featured":false,"tags":["banking","fintech","payments","open-banking"],"logo":"logos/jack-henry.jpg","built":false,"library":null},{"slug":"jira","name":"Jira","provider":"Atlassian","category":"productivity","description":"Issue tracking and project management platform for agile software development teams.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["project-management","agile","issue-tracking","devops"],"logo":"logos/jira.jpg","built":false,"library":null},{"slug":"jumio","name":"Jumio","provider":"Jumio","category":"other","description":"Identity Verification API for KYC compliance with document verification, biometric authentication, and fraud prevention.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["identity","verification","kyc","compliance"],"logo":"logos/jumio.jpg","built":false,"library":null},{"slug":"keap","name":"Keap","provider":"Keap","category":"marketing","description":"CRM and marketing automation (formerly InfusionSoft) for small businesses with sales and email tools.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","marketing","automation","small-business"],"logo":"logos/keap.jpg","built":false,"library":null},{"slug":"knack","name":"Knack","provider":"Knack","category":"devtools","description":"No-code database and application builder for creating custom business apps without programming.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["no-code","database","apps","business"],"logo":"logos/knack.jpg","built":false,"library":null},{"slug":"lawpay","name":"LawPay","provider":"LawPay","category":"finance","description":"Payment API for legal professionals with trust accounting, client billing, and compliant payment processing.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["legal","payments","billing","trust-accounting"],"logo":"logos/lawpay.jpg","built":false,"library":null},{"slug":"less-annoying-crm","name":"Less Annoying CRM","provider":"Less Annoying CRM","category":"crm","description":"Simple CRM designed specifically for small businesses with contact and pipeline management.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["crm","small-business","contacts","simple"],"logo":"logos/less-annoying-crm.jpg","built":false,"library":null},{"slug":"lever","name":"Lever","provider":"Lever","category":"hr","description":"Talent acquisition suite combining ATS and CRM for sourcing, nurturing, and hiring candidates.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["hr","recruiting","ats","crm"],"logo":"logos/lever.jpg","built":false,"library":null},{"slug":"linkedin","name":"LinkedIn","provider":"Microsoft","category":"marketing","description":"Marketing and Talent API for professional networking with campaign management, talent acquisition, and company page integration.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["social-media","recruiting","marketing","talent"],"logo":"logos/linkedin.jpg","built":false,"library":null},{"slug":"lumapps","name":"LumApps","provider":"LumApps","category":"communication","description":"Employee experience platform with intranet, communication tools, and social collaboration features.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["intranet","collaboration","employee","communication"],"logo":"logos/lumapps.jpg","built":false,"library":null},{"slug":"lumar","name":"Lumar","provider":"Lumar","category":"analytics","description":"SEO crawler and site auditing platform (formerly DeepCrawl) for technical SEO analysis and monitoring.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["seo","crawler","analytics","site-audit"],"logo":"logos/lumar.jpg","built":false,"library":null},{"slug":"mailchimp","name":"Mailchimp","provider":"Intuit","category":"marketing","description":"Email marketing platform (acquired by Intuit) with campaigns, automations, and audience management.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["email","marketing","campaigns","automation"],"logo":"logos/mailchimp.jpg","built":false,"library":null},{"slug":"mailchimp-transactional","name":"Mailchimp Transactional","provider":"Intuit","category":"marketing","description":"Transactional email service (formerly Mandrill) for triggered emails and notifications.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["email","transactional","notifications","templates"],"logo":"logos/mailchimp-transactional.jpg","built":false,"library":null},{"slug":"dynamics-365","name":"Microsoft Dynamics 365","provider":"Microsoft","category":"crm","description":"Enterprise CRM and ERP platform with sales, customer service, finance, and operations modules.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["crm","erp","enterprise","sales"],"logo":"logos/dynamics-365.jpg","built":false,"library":null},{"slug":"microsoft-graph","name":"Microsoft Graph","provider":"Microsoft","category":"productivity","description":"Unified API for Microsoft 365, including Teams, Outlook, OneDrive, and SharePoint.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["productivity","enterprise","office365","teams"],"logo":"logos/microsoft-graph.jpg","built":false,"library":null},{"slug":"onedrive","name":"Microsoft OneDrive","provider":"Microsoft","category":"storage","description":"Cloud storage service for storing, syncing, and sharing files across Microsoft 365.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["storage","files","cloud","office365"],"logo":"logos/onedrive.jpg","built":false,"library":null},{"slug":"outlook","name":"Microsoft Outlook","provider":"Microsoft","category":"communication","description":"Email and calendar service with full API access for mail, events, and contact management.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["email","calendar","office365","communication"],"logo":"logos/outlook.jpg","built":false,"library":null},{"slug":"microsoft-teams","name":"Microsoft Teams","provider":"Microsoft","category":"communication","description":"Collaboration platform with chat, meetings, calls, and app integration for Microsoft 365.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["collaboration","chat","meetings","office365"],"logo":"logos/microsoft-teams.jpg","library":"updating","built":true},{"slug":"mitel","name":"Mitel","provider":"Mitel","category":"communication","description":"Enterprise business communications with cloud PBX, contact center, and unified communications APIs.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["phone","pbx","contact-center","enterprise"],"logo":"logos/mitel.jpg","built":false,"library":null},{"slug":"modern-treasury","name":"Modern Treasury","provider":"Modern Treasury","category":"finance","description":"Payment operations platform for automating money movement with ledgers, reconciliation, and workflows.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["payments","treasury","ledger","reconciliation"],"logo":"logos/modern-treasury.jpg","built":false,"library":null},{"slug":"monday","name":"Monday.com","provider":"monday.com","category":"productivity","description":"Work operating system for managing projects, workflows, and team collaboration with customizable boards.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["project-management","workflows","collaboration","graphql"],"logo":"logos/monday.jpg","library":"updating","built":true},{"slug":"mycase","name":"MyCase","provider":"MyCase","category":"productivity","description":"REST API for legal practice management with case tracking, time billing, document management, and client communication.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["legal","case-management","billing","practice-management"],"logo":"logos/mycase.jpg","built":false,"library":null},{"slug":"ncino","name":"nCino","provider":"nCino","category":"finance","description":"Banking API for cloud-based loan origination, client onboarding, and financial institution workflow automation.","status":"Active","complexity":"High","experience":"Intermediate","featured":false,"tags":["banking","lending","loan-origination","fintech"],"logo":"logos/ncino.jpg","built":false,"library":null},{"slug":"netsuite","name":"NetSuite","provider":"Oracle","category":"finance","description":"Cloud-based ERP platform for financials, inventory, CRM, and e-commerce business management.","status":"Active","complexity":"High","experience":"Advanced","featured":true,"tags":["erp","accounting","inventory","enterprise"],"logo":"logos/netsuite.jpg","built":false,"library":null},{"slug":"nimble","name":"Nimble","provider":"Nimble","category":"crm","description":"Social CRM that combines contacts, communication history, and social insights in one platform.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","social","contacts","relationships"],"logo":"logos/nimble.jpg","built":false,"library":null},{"slug":"notion","name":"Notion","provider":"Notion","category":"productivity","description":"All-in-one workspace for notes, docs, wikis, and project management with powerful API integration.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["productivity","notes","wiki","collaboration"],"logo":"logos/notion.jpg","built":false,"library":null},{"slug":"nutshell","name":"Nutshell","provider":"Nutshell","category":"crm","description":"CRM and sales automation platform for small businesses with pipeline management and email integration.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","sales","pipeline","small-business"],"logo":"logos/nutshell.jpg","built":false,"library":null},{"slug":"odoo","name":"Odoo","provider":"Odoo","category":"finance","description":"Open-source ERP and business management suite with accounting, CRM, inventory, and HR modules.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["erp","accounting","open-source","business"],"logo":"logos/odoo.jpg","built":false,"library":null},{"slug":"openai","name":"OpenAI","provider":"OpenAI","category":"ai","description":"ChatGPT, GPT models, and the Apps SDK. Build GPT Actions, ChatGPT integrations, and AI-powered connectors for the GPT Store and App Directory.","status":"Active","complexity":"Medium","experience":"Advanced","featured":true,"tags":["AI","ChatGPT","GPT","agents","connectors","apps-sdk"],"logo":"logos/openai.jpg","built":false,"library":null},{"slug":"optimizely","name":"Optimizely","provider":"Optimizely","category":"devtools","description":"Digital experience platform for A/B testing, feature flags, and personalization experimentation.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["ab-testing","experimentation","feature-flags","personalization"],"logo":"logos/optimizely.jpg","built":false,"library":null},{"slug":"orion","name":"Orion","provider":"Orion Advisor Solutions","category":"finance","description":"Portfolio API for wealth management with portfolio accounting, performance reporting, and trading integration.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["finance","wealth-management","portfolio","advisor"],"logo":"logos/orion.jpg","built":false,"library":null},{"slug":"otter-ai","name":"Otter.ai","provider":"Otter.ai","category":"ai","description":"Transcription API for AI-powered meeting notes with real-time transcription, speaker identification, and summary generation.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["ai","transcription","meetings","notes"],"logo":"logos/otter-ai.jpg","built":false,"library":null},{"slug":"outreach","name":"Outreach.io","provider":"Outreach","category":"crm","description":"Sales engagement platform for automating and optimizing sales workflows and prospect outreach.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["sales","engagement","automation","outreach"],"logo":"logos/outreach.jpg","library":"updating","built":true},{"slug":"pandadoc","name":"PandaDoc","provider":"PandaDoc","category":"productivity","description":"Document API for proposal and contract automation with template management, e-signatures, and workflow tracking.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["documents","proposals","contracts","esignature"],"logo":"logos/pandadoc.jpg","built":false,"library":null},{"slug":"paylocity","name":"Paylocity","provider":"Paylocity","category":"hr","description":"Cloud-based payroll and HR platform with workforce management, talent, and benefits administration.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["hr","payroll","workforce","benefits"],"logo":"logos/paylocity.jpg","built":false,"library":null},{"slug":"personio","name":"Personio","provider":"Personio","category":"hr","description":"All-in-one HR software for SMBs with recruiting, onboarding, payroll, and absence management.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["hr","recruiting","payroll","smb"],"logo":"logos/personio.jpg","library":"updating","built":true},{"slug":"pipedrive","name":"Pipedrive","provider":"Pipedrive","category":"crm","description":"Sales CRM and pipeline management tool designed for small to medium-sized sales teams.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["crm","sales","pipeline","deals"],"logo":"logos/pipedrive.jpg","library":"updating","built":true},{"slug":"postman","name":"Postman","provider":"Postman","category":"devtools","description":"API development platform for building, testing, documenting, and monitoring APIs collaboratively.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["api","testing","documentation","collaboration"],"logo":"logos/postman.jpg","built":false,"library":null},{"slug":"power-automate","name":"Power Automate","provider":"Microsoft","category":"devtools","description":"Low-code workflow automation platform for creating automated processes across Microsoft 365 and third-party apps.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["automation","workflow","low-code","integration"],"logo":"logos/power-automate.jpg","built":false,"library":null},{"slug":"practice-panther","name":"PracticePanther","provider":"PracticePanther","category":"productivity","description":"REST API for legal practice management with case management, time tracking, billing, and client portal integration.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["legal","practice-management","billing","case-management"],"logo":"logos/practice-panther.jpg","built":false,"library":null},{"slug":"processmaker","name":"ProcessMaker","provider":"ProcessMaker","category":"devtools","description":"Business process management and workflow automation platform with visual process designer.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["bpm","workflow","automation","process"],"logo":"logos/processmaker.jpg","built":false,"library":null},{"slug":"quick-base","name":"Quick Base","provider":"Quick Base","category":"devtools","description":"Low-code platform for building custom business applications and workflow automation without coding.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["low-code","apps","database","workflow"],"logo":"logos/quick-base.jpg","built":false,"library":null},{"slug":"quickbooks-desktop","name":"QuickBooks Desktop","provider":"Intuit","category":"finance","description":"Desktop accounting software API for enterprise financial management, payroll, and inventory.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["accounting","finance","desktop","enterprise"],"logo":"logos/quickbooks-desktop.jpg","built":false,"library":null},{"slug":"quickbooks-online","name":"QuickBooks Online","provider":"Intuit","category":"finance","description":"Cloud accounting platform for invoicing, expenses, payroll, and financial reporting.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":true,"tags":["accounting","invoicing","payroll","finance"],"logo":"logos/quickbooks-online.jpg","built":true,"library":"updating"},{"slug":"raisers-edge","name":"Raiser's Edge","provider":"Blackbaud","category":"crm","description":"SKY API for nonprofit fundraising with donor management, gift processing, constituent tracking, and campaign analytics.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["nonprofit","fundraising","donor-management","crm"],"logo":"logos/raisers-edge.jpg","built":false,"library":null},{"slug":"recharge","name":"Recharge","provider":"Recharge","category":"commerce","description":"Subscription payments platform for e-commerce with recurring billing and subscription management.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["subscriptions","ecommerce","billing","recurring"],"logo":"logos/recharge.jpg","built":false,"library":null},{"slug":"recurly","name":"Recurly","provider":"Recurly","category":"finance","description":"Subscription management and billing platform for recurring revenue businesses.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["subscriptions","billing","recurring","revenue"],"logo":"logos/recurly.jpg","built":false,"library":null},{"slug":"redtail","name":"Redtail","provider":"Redtail Technology","category":"crm","description":"CRM API for financial advisors with contact management, workflow automation, and client relationship tracking.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","financial-advisor","wealth-management","contact-management"],"logo":"logos/redtail.jpg","built":false,"library":null},{"slug":"resource-guru","name":"Resource Guru","provider":"Resource Guru","category":"productivity","description":"Resource scheduling and management software for teams with availability tracking and booking.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["scheduling","resources","booking","availability"],"logo":"logos/resource-guru.jpg","built":false,"library":null},{"slug":"ringcentral","name":"RingCentral","provider":"RingCentral","category":"communication","description":"Cloud business phone system with voice, video, messaging, and collaboration features.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["phone","voice","video","communication"],"logo":"logos/ringcentral.jpg","built":false,"library":null},{"slug":"ringcentral-events","name":"RingCentral Events","provider":"RingCentral","category":"communication","description":"Virtual event platform (acquired from Hopin) for hosting hybrid and virtual events, conferences, and webinars.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["events","virtual","webinars","hybrid"],"logo":"logos/ringcentral-events.jpg","built":false,"library":null},{"slug":"rollworks","name":"RollWorks","provider":"NextRoll","category":"analytics","description":"Account-based marketing platform for B2B advertising, targeting, and account engagement.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["abm","advertising","b2b","targeting"],"logo":"logos/rollworks.jpg","library":"updating","built":true},{"slug":"account-engagement","name":"Salesforce Account Engagement","provider":"Salesforce","category":"marketing","description":"B2B marketing automation (formerly Pardot) integrated with Salesforce CRM for lead nurturing and campaigns.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["marketing","automation","b2b","lead-nurturing"],"logo":"logos/account-engagement.jpg","built":false,"library":null},{"slug":"salesforce","name":"Salesforce APIs","provider":"Salesforce","category":"crm","description":"World's leading CRM platform with extensive REST and SOAP APIs for sales, service, and marketing.","status":"Active","complexity":"High","experience":"Expert","featured":true,"tags":["crm","enterprise","sales","apex"],"logo":"logos/salesforce.jpg","library":"ready","built":true},{"slug":"salesloft","name":"SalesLoft","provider":"SalesLoft","category":"crm","description":"Sales engagement platform for revenue intelligence, cadence automation, and buyer engagement.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["sales","engagement","automation","revenue"],"logo":"logos/salesloft.jpg","library":"updating","built":true},{"slug":"schwab","name":"Schwab","provider":"Charles Schwab","category":"finance","description":"Advisor API for investment management with account data, trading, portfolio analytics, and custodial services.","status":"Active","complexity":"High","experience":"Intermediate","featured":false,"tags":["finance","investments","advisor","custodial"],"logo":"logos/schwab.jpg","built":false,"library":null},{"slug":"scrive","name":"Scrive","provider":"Scrive","category":"productivity","description":"Electronic signature platform for secure document signing with identity verification and workflows.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["esignature","documents","identity","workflows"],"logo":"logos/scrive.jpg","built":false,"library":null},{"slug":"sendgrid","name":"SendGrid","provider":"Twilio","category":"marketing","description":"Email delivery platform (acquired by Twilio) for transactional and marketing emails at scale.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["email","transactional","marketing","delivery"],"logo":"logos/sendgrid.jpg","built":false,"library":null},{"slug":"servicebridge","name":"ServiceBridge","provider":"ServiceBridge","category":"productivity","description":"Field service management software for work orders, scheduling, dispatching, and invoicing.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["field-service","work-orders","scheduling","invoicing"],"logo":"logos/servicebridge.jpg","built":false,"library":null},{"slug":"setmore","name":"Setmore","provider":"Setmore","category":"productivity","description":"Free online appointment scheduling software with calendar sync and customer booking pages.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["scheduling","appointments","booking","calendar"],"logo":"logos/setmore.jpg","built":false,"library":null},{"slug":"sharefile","name":"ShareFile","provider":"Citrix","category":"storage","description":"Secure file sharing and storage platform for business document management and collaboration.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["storage","files","sharing","enterprise"],"logo":"logos/sharefile.jpg","built":false,"library":null},{"slug":"sharepoint","name":"SharePoint","provider":"Microsoft","category":"productivity","description":"Enterprise content management API for document libraries, sites, lists, and collaboration features.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["documents","collaboration","intranet","microsoft"],"logo":"logos/sharepoint.jpg","built":true,"library":"updating"},{"slug":"shippingeasy","name":"ShippingEasy","provider":"Auctane","category":"commerce","description":"Shipping software for e-commerce with label printing, order management, and carrier rate comparison.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["shipping","ecommerce","logistics","labels"],"logo":"logos/shippingeasy.jpg","built":false,"library":null},{"slug":"shipstation","name":"ShipStation","provider":"Auctane","category":"commerce","description":"Multi-carrier shipping software for e-commerce with order management and fulfillment automation.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["shipping","ecommerce","fulfillment","multi-carrier"],"logo":"logos/shipstation.jpg","built":false,"library":null},{"slug":"shopify","name":"Shopify Admin API","provider":"Shopify","category":"commerce","description":"E-commerce platform API for managing products, orders, customers, and inventory.","status":"Active","complexity":"Medium","experience":"Expert","featured":true,"tags":["ecommerce","retail","inventory","graphql"],"logo":"logos/shopify.jpg","built":false,"library":null},{"slug":"signority","name":"Signority","provider":"Signority","category":"productivity","description":"Electronic signature solution for document signing, workflow automation, and compliance management.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["esignature","documents","compliance","workflows"],"logo":"logos/signority.jpg","built":false,"library":null},{"slug":"simpletexting","name":"SimpleTexting","provider":"SimpleTexting","category":"communication","description":"SMS marketing and messaging platform for mass texting, two-way conversations, and automated campaigns.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["sms","messaging","marketing","automation"],"logo":"logos/simpletexting.jpg","built":false,"library":null},{"slug":"slack","name":"Slack Web API","provider":"Slack","category":"communication","description":"Team communication platform with messaging, channels, and workflow automation.","status":"Active","complexity":"Low","experience":"Expert","featured":true,"tags":["communication","collaboration","messaging","bots"],"logo":"logos/slack.jpg","library":"updating","built":true},{"slug":"socure","name":"Socure","provider":"Socure","category":"other","description":"ID+ API for digital identity verification with fraud prevention, KYC compliance, and real-time risk assessment.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["identity","verification","fraud-prevention","kyc"],"logo":"logos/socure.jpg","built":false,"library":null},{"slug":"splunk-on-call","name":"Splunk On-Call","provider":"Splunk","category":"devtools","description":"Incident management platform (formerly VictorOps) for on-call scheduling, alerting, and incident response.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["incident-management","on-call","alerting","devops"],"logo":"logos/splunk-on-call.jpg","built":false,"library":null},{"slug":"square","name":"Square","provider":"Block","category":"commerce","description":"Commerce platform with APIs for payments, point of sale, invoicing, and business management.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["payments","pos","commerce","invoicing"],"logo":"logos/square.jpg","built":false,"library":null},{"slug":"stack-exchange","name":"Stack Exchange","provider":"Stack Exchange","category":"devtools","description":"Q&A platform API for accessing Stack Overflow and other Stack Exchange network sites data.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["q-and-a","developer","community","knowledge"],"logo":"logos/stack-exchange.jpg","built":false,"library":null},{"slug":"streak-crm","name":"Streak CRM","provider":"Streak","category":"crm","description":"CRM built directly inside Gmail for managing sales pipelines, contacts, and workflows from your inbox.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["crm","gmail","sales","pipelines"],"logo":"logos/streak-crm.jpg","built":false,"library":null},{"slug":"stripe","name":"Stripe API","provider":"Stripe","category":"finance","description":"Payment processing platform with comprehensive APIs for payments, subscriptions, and billing.","status":"Active","complexity":"Medium","experience":"Expert","featured":true,"tags":["payments","billing","subscriptions","fintech"],"logo":"logos/stripe.jpg","library":"ready","built":true},{"slug":"sugarcrm","name":"SugarCRM","provider":"SugarCRM","category":"crm","description":"Customer relationship management platform for sales automation, marketing, and customer service.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","sales","marketing","automation"],"logo":"logos/sugarcrm.jpg","built":false,"library":null},{"slug":"tamarac","name":"Tamarac","provider":"Envestnet","category":"finance","description":"Advisor API for portfolio management with rebalancing, performance reporting, and client portal integration.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["finance","portfolio-management","advisor","rebalancing"],"logo":"logos/tamarac.jpg","built":false,"library":null},{"slug":"teamwork","name":"Teamwork","provider":"Teamwork","category":"productivity","description":"Project management and team collaboration platform with tasks, time tracking, and resource management.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["project-management","tasks","time-tracking","collaboration"],"logo":"logos/teamwork.jpg","built":false,"library":null},{"slug":"terminus","name":"Terminus","provider":"DemandScience","category":"analytics","description":"Account-based marketing platform for multi-channel campaigns and account engagement analytics.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["abm","marketing","analytics","engagement"],"logo":"logos/terminus.jpg","library":"updating","built":true},{"slug":"timesolv","name":"TimeSolv","provider":"TimeSolv","category":"productivity","description":"REST API for legal time tracking and billing with expense management, invoicing, and project-based accounting.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["legal","time-tracking","billing","invoicing"],"logo":"logos/timesolv.jpg","built":false,"library":null},{"slug":"tipalti","name":"Tipalti","provider":"Tipalti","category":"finance","description":"Global payables automation platform for supplier payments, tax compliance, and AP workflows.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["payments","payables","finance","automation"],"logo":"logos/tipalti.jpg","built":false,"library":null},{"slug":"totango","name":"Totango","provider":"Totango","category":"crm","description":"Customer success platform for monitoring customer health, engagement, and retention analytics.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["customer-success","retention","analytics","engagement"],"logo":"logos/totango.jpg","built":false,"library":null},{"slug":"touchplan","name":"Touchplan","provider":"Touchplan","category":"productivity","description":"Construction project planning platform for lean scheduling, pull planning, and team coordination.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["construction","scheduling","planning","lean"],"logo":"logos/touchplan.jpg","built":false,"library":null},{"slug":"trello","name":"Trello","provider":"Atlassian","category":"productivity","description":"Project management API for boards, lists, cards, and Power-Up integrations.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["project-management","kanban","collaboration","atlassian"],"logo":"logos/trello.jpg","built":false,"library":null},{"slug":"trolley","name":"Trolley","provider":"Trolley","category":"finance","description":"Global payouts platform (formerly PaymentRails) for mass payments and recipient management.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["payouts","payments","mass-payments","international"],"logo":"logos/trolley.jpg","built":false,"library":null},{"slug":"twilio","name":"Twilio","provider":"Twilio","category":"communication","description":"Cloud communications platform for building SMS, voice, video, and authentication into applications.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["sms","voice","video","communication"],"logo":"logos/twilio.jpg","built":false,"library":null},{"slug":"typeform","name":"Typeform","provider":"Typeform","category":"productivity","description":"Create and Responses API for interactive forms with survey creation, response collection, and webhook-based data pipelines.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["forms","surveys","data-collection","automation"],"logo":"logos/typeform.jpg","built":false,"library":null},{"slug":"ukg","name":"UKG","provider":"UKG","category":"hr","description":"Enterprise HR and workforce management platform with time tracking, payroll, and talent solutions.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["hr","workforce","payroll","enterprise"],"logo":"logos/ukg.jpg","built":false,"library":null},{"slug":"veem","name":"Veem","provider":"Veem","category":"finance","description":"Global business payments platform for sending and receiving international payments with tracking.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["payments","international","b2b","transfers"],"logo":"logos/veem.jpg","built":false,"library":null},{"slug":"virtuous","name":"Virtuous","provider":"Virtuous","category":"crm","description":"Nonprofit CRM platform for donor management, fundraising, and responsive giving experiences.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["nonprofit","crm","fundraising","donors"],"logo":"logos/virtuous.jpg","built":false,"library":null},{"slug":"wealthbox","name":"Wealthbox","provider":"Wealthbox","category":"crm","description":"REST API for financial advisor CRM with contact management, workflow automation, and wealth management integration.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["crm","financial-advisor","wealth-management","contacts"],"logo":"logos/wealthbox.jpg","built":false,"library":null},{"slug":"webflow","name":"Webflow","provider":"Webflow","category":"other","description":"Visual web design and CMS platform with APIs for managing sites, content, and e-commerce.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["cms","websites","design","ecommerce"],"logo":"logos/webflow.jpg","built":false,"library":null},{"slug":"whispir","name":"Whispir","provider":"Whispir","category":"communication","description":"Intelligent communications platform for multi-channel messaging across SMS, email, voice, and social.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["communication","sms","multi-channel","messaging"],"logo":"logos/whispir.jpg","built":false,"library":null},{"slug":"wistia","name":"Wistia","provider":"Wistia","category":"storage","description":"Video hosting platform for businesses with analytics, customization, and marketing integrations.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["video","hosting","analytics","marketing"],"logo":"logos/wistia.jpg","built":false,"library":null},{"slug":"woocommerce","name":"WooCommerce","provider":"Automattic","category":"commerce","description":"Open-source e-commerce platform for WordPress with REST API for products, orders, and customers.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["ecommerce","wordpress","products","orders"],"logo":"logos/woocommerce.jpg","built":false,"library":null},{"slug":"wordpress","name":"WordPress","provider":"WordPress","category":"other","description":"Open-source content management system with REST API for creating and managing websites and blogs.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["cms","websites","blogs","content"],"logo":"logos/wordpress.jpg","built":false,"library":null},{"slug":"workato","name":"Workato","provider":"Workato","category":"devtools","description":"Enterprise automation platform for integrating apps, automating workflows, and building intelligent bots.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["automation","integration","workflow","enterprise"],"logo":"logos/workato.jpg","built":false,"library":null},{"slug":"workday","name":"Workday","provider":"Workday","category":"hr","description":"Enterprise cloud applications for finance and HR with workforce management, analytics, and planning.","status":"Active","complexity":"High","experience":"Expert","featured":false,"tags":["hr","finance","enterprise","hcm"],"logo":"logos/workday.jpg","built":false,"library":null},{"slug":"workzone","name":"Workzone","provider":"Workzone","category":"productivity","description":"Project management software for marketing teams with task management, proofing, and resource planning.","status":"Active","complexity":"Low","experience":"Intermediate","featured":false,"tags":["project-management","marketing","tasks","proofing"],"logo":"logos/workzone.jpg","built":false,"library":null},{"slug":"wrike","name":"Wrike","provider":"Wrike","category":"productivity","description":"Project management and collaboration platform with task tracking, workflows, and team communication.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["project-management","tasks","collaboration","workflows"],"logo":"logos/wrike.jpg","built":false,"library":null},{"slug":"wufoo","name":"Wufoo","provider":"SurveyMonkey","category":"productivity","description":"Form builder (acquired by SurveyMonkey) for online data collection, surveys, and registrations.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["forms","surveys","data-collection","registration"],"logo":"logos/wufoo.jpg","built":false,"library":null},{"slug":"xero","name":"Xero","provider":"Xero","category":"finance","description":"Cloud accounting software for small businesses with invoicing, bank reconciliation, and reporting.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["accounting","invoicing","finance","small-business"],"logo":"logos/xero.jpg","built":false,"library":null},{"slug":"xola","name":"Xola","provider":"Xola","category":"commerce","description":"Booking and reservation software for tours, activities, and attractions with channel distribution.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["booking","reservations","tours","activities"],"logo":"logos/xola.jpg","built":false,"library":null},{"slug":"yotpo","name":"Yotpo","provider":"Yotpo","category":"marketing","description":"Ecommerce marketing API for reviews, ratings, loyalty programs, and user-generated content.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["reviews","loyalty","ecommerce","ugc"],"logo":"logos/yotpo.jpg","library":"updating","built":true},{"slug":"zapier","name":"Zapier","provider":"Zapier","category":"devtools","description":"Workflow automation platform for connecting apps and automating tasks with no-code integrations.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["automation","integration","no-code","workflow"],"logo":"logos/zapier.jpg","built":false,"library":null},{"slug":"zendesk","name":"Zendesk","provider":"Zendesk","category":"support","description":"Customer service platform with ticketing, knowledge base, and multichannel support capabilities.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":true,"tags":["support","helpdesk","ticketing","customer-service"],"logo":"logos/zendesk.jpg","built":false,"library":null},{"slug":"zendesk-sell","name":"Zendesk Sell","provider":"Zendesk","category":"crm","description":"Sales CRM platform (formerly Base CRM) for pipeline management, forecasting, and sales automation.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","sales","pipeline","forecasting"],"logo":"logos/zendesk-sell.jpg","built":false,"library":null},{"slug":"zerobounce","name":"ZeroBounce","provider":"ZeroBounce","category":"marketing","description":"Email validation and verification service for cleaning email lists and improving deliverability.","status":"Active","complexity":"Low","experience":"Beginner","featured":false,"tags":["email","validation","verification","deliverability"],"logo":"logos/zerobounce.jpg","built":false,"library":null},{"slug":"zoho-crm","name":"Zoho CRM","provider":"Zoho","category":"crm","description":"Cloud-based CRM platform with sales automation, marketing, and customer support features.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["crm","sales","marketing","automation"],"logo":"logos/zoho-crm.jpg","library":"ready","built":true},{"slug":"zoom","name":"Zoom","provider":"Zoom","category":"communication","description":"Video conferencing platform with APIs for meetings, webinars, chat, and phone services.","status":"Active","complexity":"Medium","experience":"Intermediate","featured":false,"tags":["video","conferencing","meetings","webinars"],"logo":"logos/zoom.jpg","library":"ready","built":true},{"slug":"zoominfo","name":"ZoomInfo","provider":"ZoomInfo","category":"analytics","description":"Enrich and Search API for B2B contact and company intelligence with prospecting, enrichment, and intent data.","status":"Active","complexity":"Medium","experience":"Advanced","featured":false,"tags":["b2b","data-enrichment","prospecting","intelligence"],"logo":"logos/zoominfo.jpg","built":false,"library":null},{"slug":"zuora","name":"Zuora","provider":"Zuora","category":"finance","description":"Subscription management platform for enterprise billing, revenue recognition, and subscriber analytics.","status":"Active","complexity":"High","experience":"Advanced","featured":false,"tags":["subscriptions","billing","enterprise","revenue"],"logo":"logos/zuora.jpg","built":false,"library":null}],"builtCount":35} \ No newline at end of file diff --git a/website/roadmap/index.html b/website/roadmap/index.html new file mode 100644 index 000000000..09812248e --- /dev/null +++ b/website/roadmap/index.html @@ -0,0 +1,540 @@ + + + + + + + Frigg Roadmap — framework direction & the API module wishlist + + + + + + + + + + +
+ +
+ +
+
+ Roadmap +

Where Frigg is headed, and what you help pick next.

+

Two things live here. The framework roadmap comes straight from our architecture decision records on the next branch, so it's the real plan, not a marketing wishlist. The API module directory is every connector in our catalog. Vote for the ones you want, and open a thread on GitHub to make the case.

+
+ Vote counts are live (Netlify). + One vote per item, per browser. + Discuss & request on GitHub. +
+
+
+ + +
+
+
+
+ From the ADRs +

Framework roadmap

+

Architecture decisions that are shipped, in progress, or proposed. Each links to the full ADR on GitHub.

+
+ loading… +
+
+ +
+ + + +
+ +
+
Loading roadmap…
+
+
+ + +
+
+
+
+ API module wishlist +

The connector directory

+

Every API in our catalog is fair game for the module library. Vote for the ones you want built next, and open a request on GitHub to make the case.

+
+ loading… +
+
+ + + +
+ + + +
+
+
Loading the catalog…
+
+
+ + + + + + + + + + + + diff --git a/website/roadmap/logos/account-engagement.jpg b/website/roadmap/logos/account-engagement.jpg new file mode 100644 index 000000000..dfd4f7d52 Binary files /dev/null and b/website/roadmap/logos/account-engagement.jpg differ diff --git a/website/roadmap/logos/account-management.jpg b/website/roadmap/logos/account-management.jpg new file mode 100644 index 000000000..6c1ae6ce1 Binary files /dev/null and b/website/roadmap/logos/account-management.jpg differ diff --git a/website/roadmap/logos/act-on.jpg b/website/roadmap/logos/act-on.jpg new file mode 100644 index 000000000..0956f0b83 Binary files /dev/null and b/website/roadmap/logos/act-on.jpg differ diff --git a/website/roadmap/logos/activecampaign.jpg b/website/roadmap/logos/activecampaign.jpg new file mode 100644 index 000000000..60d85239a Binary files /dev/null and b/website/roadmap/logos/activecampaign.jpg differ diff --git a/website/roadmap/logos/acuity-scheduling.jpg b/website/roadmap/logos/acuity-scheduling.jpg new file mode 100644 index 000000000..f1061dae7 Binary files /dev/null and b/website/roadmap/logos/acuity-scheduling.jpg differ diff --git a/website/roadmap/logos/adobe-sign.jpg b/website/roadmap/logos/adobe-sign.jpg new file mode 100644 index 000000000..72c6547dc Binary files /dev/null and b/website/roadmap/logos/adobe-sign.jpg differ diff --git a/website/roadmap/logos/adp.jpg b/website/roadmap/logos/adp.jpg new file mode 100644 index 000000000..d00943ab1 Binary files /dev/null and b/website/roadmap/logos/adp.jpg differ diff --git a/website/roadmap/logos/agile-crm.jpg b/website/roadmap/logos/agile-crm.jpg new file mode 100644 index 000000000..da25e8d6c Binary files /dev/null and b/website/roadmap/logos/agile-crm.jpg differ diff --git a/website/roadmap/logos/airtable.jpg b/website/roadmap/logos/airtable.jpg new file mode 100644 index 000000000..715e348c6 Binary files /dev/null and b/website/roadmap/logos/airtable.jpg differ diff --git a/website/roadmap/logos/airwallex.jpg b/website/roadmap/logos/airwallex.jpg new file mode 100644 index 000000000..ff9cfd214 Binary files /dev/null and b/website/roadmap/logos/airwallex.jpg differ diff --git a/website/roadmap/logos/alchemer.jpg b/website/roadmap/logos/alchemer.jpg new file mode 100644 index 000000000..5fb89146a Binary files /dev/null and b/website/roadmap/logos/alchemer.jpg differ diff --git a/website/roadmap/logos/ams360.jpg b/website/roadmap/logos/ams360.jpg new file mode 100644 index 000000000..98c94da4b Binary files /dev/null and b/website/roadmap/logos/ams360.jpg differ diff --git a/website/roadmap/logos/analytics.jpg b/website/roadmap/logos/analytics.jpg new file mode 100644 index 000000000..bc1643069 Binary files /dev/null and b/website/roadmap/logos/analytics.jpg differ diff --git a/website/roadmap/logos/applied-epic.jpg b/website/roadmap/logos/applied-epic.jpg new file mode 100644 index 000000000..90f8be632 Binary files /dev/null and b/website/roadmap/logos/applied-epic.jpg differ diff --git a/website/roadmap/logos/applied-rater.jpg b/website/roadmap/logos/applied-rater.jpg new file mode 100644 index 000000000..90f8be632 Binary files /dev/null and b/website/roadmap/logos/applied-rater.jpg differ diff --git a/website/roadmap/logos/asana.jpg b/website/roadmap/logos/asana.jpg new file mode 100644 index 000000000..9807729e7 Binary files /dev/null and b/website/roadmap/logos/asana.jpg differ diff --git a/website/roadmap/logos/assemblyai.jpg b/website/roadmap/logos/assemblyai.jpg new file mode 100644 index 000000000..af6010c24 Binary files /dev/null and b/website/roadmap/logos/assemblyai.jpg differ diff --git a/website/roadmap/logos/athenahealth.jpg b/website/roadmap/logos/athenahealth.jpg new file mode 100644 index 000000000..9819fd42d Binary files /dev/null and b/website/roadmap/logos/athenahealth.jpg differ diff --git a/website/roadmap/logos/attentive.jpg b/website/roadmap/logos/attentive.jpg new file mode 100644 index 000000000..1cc107410 Binary files /dev/null and b/website/roadmap/logos/attentive.jpg differ diff --git a/website/roadmap/logos/authorize-net.jpg b/website/roadmap/logos/authorize-net.jpg new file mode 100644 index 000000000..4733b8a47 Binary files /dev/null and b/website/roadmap/logos/authorize-net.jpg differ diff --git a/website/roadmap/logos/autotask-psa.jpg b/website/roadmap/logos/autotask-psa.jpg new file mode 100644 index 000000000..77f89b9a7 Binary files /dev/null and b/website/roadmap/logos/autotask-psa.jpg differ diff --git a/website/roadmap/logos/availity.jpg b/website/roadmap/logos/availity.jpg new file mode 100644 index 000000000..f04ef452f Binary files /dev/null and b/website/roadmap/logos/availity.jpg differ diff --git a/website/roadmap/logos/bamboohr.jpg b/website/roadmap/logos/bamboohr.jpg new file mode 100644 index 000000000..2998daf4e Binary files /dev/null and b/website/roadmap/logos/bamboohr.jpg differ diff --git a/website/roadmap/logos/benchmark-email.jpg b/website/roadmap/logos/benchmark-email.jpg new file mode 100644 index 000000000..9cadd548b Binary files /dev/null and b/website/roadmap/logos/benchmark-email.jpg differ diff --git a/website/roadmap/logos/bitbucket.jpg b/website/roadmap/logos/bitbucket.jpg new file mode 100644 index 000000000..3f6551bc2 Binary files /dev/null and b/website/roadmap/logos/bitbucket.jpg differ diff --git a/website/roadmap/logos/bloomerang.jpg b/website/roadmap/logos/bloomerang.jpg new file mode 100644 index 000000000..d0ccef4c3 Binary files /dev/null and b/website/roadmap/logos/bloomerang.jpg differ diff --git a/website/roadmap/logos/boomset.jpg b/website/roadmap/logos/boomset.jpg new file mode 100644 index 000000000..9e020ceb0 Binary files /dev/null and b/website/roadmap/logos/boomset.jpg differ diff --git a/website/roadmap/logos/box.jpg b/website/roadmap/logos/box.jpg new file mode 100644 index 000000000..aef0be2b9 Binary files /dev/null and b/website/roadmap/logos/box.jpg differ diff --git a/website/roadmap/logos/bulksms.jpg b/website/roadmap/logos/bulksms.jpg new file mode 100644 index 000000000..0df933b81 Binary files /dev/null and b/website/roadmap/logos/bulksms.jpg differ diff --git a/website/roadmap/logos/bullhorn.jpg b/website/roadmap/logos/bullhorn.jpg new file mode 100644 index 000000000..fc80a3278 Binary files /dev/null and b/website/roadmap/logos/bullhorn.jpg differ diff --git a/website/roadmap/logos/calendly.jpg b/website/roadmap/logos/calendly.jpg new file mode 100644 index 000000000..1056930fa Binary files /dev/null and b/website/roadmap/logos/calendly.jpg differ diff --git a/website/roadmap/logos/campaign-monitor.jpg b/website/roadmap/logos/campaign-monitor.jpg new file mode 100644 index 000000000..3db34ad6a Binary files /dev/null and b/website/roadmap/logos/campaign-monitor.jpg differ diff --git a/website/roadmap/logos/canva.jpg b/website/roadmap/logos/canva.jpg new file mode 100644 index 000000000..bef703c66 Binary files /dev/null and b/website/roadmap/logos/canva.jpg differ diff --git a/website/roadmap/logos/capsule.jpg b/website/roadmap/logos/capsule.jpg new file mode 100644 index 000000000..90aed9af6 Binary files /dev/null and b/website/roadmap/logos/capsule.jpg differ diff --git a/website/roadmap/logos/chargify.jpg b/website/roadmap/logos/chargify.jpg new file mode 100644 index 000000000..b150c13d5 Binary files /dev/null and b/website/roadmap/logos/chargify.jpg differ diff --git a/website/roadmap/logos/clearbit.jpg b/website/roadmap/logos/clearbit.jpg new file mode 100644 index 000000000..6e9e4e529 Binary files /dev/null and b/website/roadmap/logos/clearbit.jpg differ diff --git a/website/roadmap/logos/clientsuccess.jpg b/website/roadmap/logos/clientsuccess.jpg new file mode 100644 index 000000000..23aa9a7b8 Binary files /dev/null and b/website/roadmap/logos/clientsuccess.jpg differ diff --git a/website/roadmap/logos/clio.jpg b/website/roadmap/logos/clio.jpg new file mode 100644 index 000000000..3267a3a75 Binary files /dev/null and b/website/roadmap/logos/clio.jpg differ diff --git a/website/roadmap/logos/close.jpg b/website/roadmap/logos/close.jpg new file mode 100644 index 000000000..157f02583 Binary files /dev/null and b/website/roadmap/logos/close.jpg differ diff --git a/website/roadmap/logos/cloud-platform.jpg b/website/roadmap/logos/cloud-platform.jpg new file mode 100644 index 000000000..bc1643069 Binary files /dev/null and b/website/roadmap/logos/cloud-platform.jpg differ diff --git a/website/roadmap/logos/coda.jpg b/website/roadmap/logos/coda.jpg new file mode 100644 index 000000000..74eb8dd79 Binary files /dev/null and b/website/roadmap/logos/coda.jpg differ diff --git a/website/roadmap/logos/confluence.jpg b/website/roadmap/logos/confluence.jpg new file mode 100644 index 000000000..f74a21172 Binary files /dev/null and b/website/roadmap/logos/confluence.jpg differ diff --git a/website/roadmap/logos/connectwise.jpg b/website/roadmap/logos/connectwise.jpg new file mode 100644 index 000000000..f72b54a2a Binary files /dev/null and b/website/roadmap/logos/connectwise.jpg differ diff --git a/website/roadmap/logos/constant-contact.jpg b/website/roadmap/logos/constant-contact.jpg new file mode 100644 index 000000000..ff519c935 Binary files /dev/null and b/website/roadmap/logos/constant-contact.jpg differ diff --git a/website/roadmap/logos/contacts.jpg b/website/roadmap/logos/contacts.jpg new file mode 100644 index 000000000..bc1643069 Binary files /dev/null and b/website/roadmap/logos/contacts.jpg differ diff --git a/website/roadmap/logos/contentful.jpg b/website/roadmap/logos/contentful.jpg new file mode 100644 index 000000000..4e827e19c Binary files /dev/null and b/website/roadmap/logos/contentful.jpg differ diff --git a/website/roadmap/logos/contentstack.jpg b/website/roadmap/logos/contentstack.jpg new file mode 100644 index 000000000..e1831fb5d Binary files /dev/null and b/website/roadmap/logos/contentstack.jpg differ diff --git a/website/roadmap/logos/copper.jpg b/website/roadmap/logos/copper.jpg new file mode 100644 index 000000000..54c349377 Binary files /dev/null and b/website/roadmap/logos/copper.jpg differ diff --git a/website/roadmap/logos/crelate.jpg b/website/roadmap/logos/crelate.jpg new file mode 100644 index 000000000..bbd81cd97 Binary files /dev/null and b/website/roadmap/logos/crelate.jpg differ diff --git a/website/roadmap/logos/crossbeam.jpg b/website/roadmap/logos/crossbeam.jpg new file mode 100644 index 000000000..d564ad502 Binary files /dev/null and b/website/roadmap/logos/crossbeam.jpg differ diff --git a/website/roadmap/logos/cvent.jpg b/website/roadmap/logos/cvent.jpg new file mode 100644 index 000000000..070133462 Binary files /dev/null and b/website/roadmap/logos/cvent.jpg differ diff --git a/website/roadmap/logos/deel.jpg b/website/roadmap/logos/deel.jpg new file mode 100644 index 000000000..d79d6a284 Binary files /dev/null and b/website/roadmap/logos/deel.jpg differ diff --git a/website/roadmap/logos/dialpad.jpg b/website/roadmap/logos/dialpad.jpg new file mode 100644 index 000000000..26a08eec7 Binary files /dev/null and b/website/roadmap/logos/dialpad.jpg differ diff --git a/website/roadmap/logos/dispatch.jpg b/website/roadmap/logos/dispatch.jpg new file mode 100644 index 000000000..5332359f0 Binary files /dev/null and b/website/roadmap/logos/dispatch.jpg differ diff --git a/website/roadmap/logos/docs.jpg b/website/roadmap/logos/docs.jpg new file mode 100644 index 000000000..bc1643069 Binary files /dev/null and b/website/roadmap/logos/docs.jpg differ diff --git a/website/roadmap/logos/docusign.jpg b/website/roadmap/logos/docusign.jpg new file mode 100644 index 000000000..23f6a27cd Binary files /dev/null and b/website/roadmap/logos/docusign.jpg differ diff --git a/website/roadmap/logos/drchrono.jpg b/website/roadmap/logos/drchrono.jpg new file mode 100644 index 000000000..7b1f64b05 Binary files /dev/null and b/website/roadmap/logos/drchrono.jpg differ diff --git a/website/roadmap/logos/drift.jpg b/website/roadmap/logos/drift.jpg new file mode 100644 index 000000000..dc0ae0543 Binary files /dev/null and b/website/roadmap/logos/drift.jpg differ diff --git a/website/roadmap/logos/dropbox-sign.jpg b/website/roadmap/logos/dropbox-sign.jpg new file mode 100644 index 000000000..c1b80f06c Binary files /dev/null and b/website/roadmap/logos/dropbox-sign.jpg differ diff --git a/website/roadmap/logos/dropbox.jpg b/website/roadmap/logos/dropbox.jpg new file mode 100644 index 000000000..95ca4c41f Binary files /dev/null and b/website/roadmap/logos/dropbox.jpg differ diff --git a/website/roadmap/logos/drupal.jpg b/website/roadmap/logos/drupal.jpg new file mode 100644 index 000000000..91bce38ee Binary files /dev/null and b/website/roadmap/logos/drupal.jpg differ diff --git a/website/roadmap/logos/dynamics-365.jpg b/website/roadmap/logos/dynamics-365.jpg new file mode 100644 index 000000000..b8fae0084 Binary files /dev/null and b/website/roadmap/logos/dynamics-365.jpg differ diff --git a/website/roadmap/logos/encompass.jpg b/website/roadmap/logos/encompass.jpg new file mode 100644 index 000000000..80c5ba18b Binary files /dev/null and b/website/roadmap/logos/encompass.jpg differ diff --git a/website/roadmap/logos/epic.jpg b/website/roadmap/logos/epic.jpg new file mode 100644 index 000000000..28abbd878 Binary files /dev/null and b/website/roadmap/logos/epic.jpg differ diff --git a/website/roadmap/logos/etsy.jpg b/website/roadmap/logos/etsy.jpg new file mode 100644 index 000000000..d8af444c8 Binary files /dev/null and b/website/roadmap/logos/etsy.jpg differ diff --git a/website/roadmap/logos/eventbrite.jpg b/website/roadmap/logos/eventbrite.jpg new file mode 100644 index 000000000..378157ee4 Binary files /dev/null and b/website/roadmap/logos/eventbrite.jpg differ diff --git a/website/roadmap/logos/fidelity.jpg b/website/roadmap/logos/fidelity.jpg new file mode 100644 index 000000000..a1a4c0117 Binary files /dev/null and b/website/roadmap/logos/fidelity.jpg differ diff --git a/website/roadmap/logos/fireflies-ai.jpg b/website/roadmap/logos/fireflies-ai.jpg new file mode 100644 index 000000000..4c3677843 Binary files /dev/null and b/website/roadmap/logos/fireflies-ai.jpg differ diff --git a/website/roadmap/logos/fis.jpg b/website/roadmap/logos/fis.jpg new file mode 100644 index 000000000..e86b60823 Binary files /dev/null and b/website/roadmap/logos/fis.jpg differ diff --git a/website/roadmap/logos/fiserv.jpg b/website/roadmap/logos/fiserv.jpg new file mode 100644 index 000000000..1cca2bcc9 Binary files /dev/null and b/website/roadmap/logos/fiserv.jpg differ diff --git a/website/roadmap/logos/fishbowl.jpg b/website/roadmap/logos/fishbowl.jpg new file mode 100644 index 000000000..5080e71cd Binary files /dev/null and b/website/roadmap/logos/fishbowl.jpg differ diff --git a/website/roadmap/logos/formsite.jpg b/website/roadmap/logos/formsite.jpg new file mode 100644 index 000000000..8bcbe796d Binary files /dev/null and b/website/roadmap/logos/formsite.jpg differ diff --git a/website/roadmap/logos/formstack.jpg b/website/roadmap/logos/formstack.jpg new file mode 100644 index 000000000..cba0cd8de Binary files /dev/null and b/website/roadmap/logos/formstack.jpg differ diff --git a/website/roadmap/logos/freshbooks.jpg b/website/roadmap/logos/freshbooks.jpg new file mode 100644 index 000000000..f1f5619aa Binary files /dev/null and b/website/roadmap/logos/freshbooks.jpg differ diff --git a/website/roadmap/logos/freshdesk.jpg b/website/roadmap/logos/freshdesk.jpg new file mode 100644 index 000000000..cf3c473d5 Binary files /dev/null and b/website/roadmap/logos/freshdesk.jpg differ diff --git a/website/roadmap/logos/freshsales.jpg b/website/roadmap/logos/freshsales.jpg new file mode 100644 index 000000000..cf3c473d5 Binary files /dev/null and b/website/roadmap/logos/freshsales.jpg differ diff --git a/website/roadmap/logos/front.jpg b/website/roadmap/logos/front.jpg new file mode 100644 index 000000000..44629aa8f Binary files /dev/null and b/website/roadmap/logos/front.jpg differ diff --git a/website/roadmap/logos/frontify.jpg b/website/roadmap/logos/frontify.jpg new file mode 100644 index 000000000..5ca38fa83 Binary files /dev/null and b/website/roadmap/logos/frontify.jpg differ diff --git a/website/roadmap/logos/github.jpg b/website/roadmap/logos/github.jpg new file mode 100644 index 000000000..3a41df346 Binary files /dev/null and b/website/roadmap/logos/github.jpg differ diff --git a/website/roadmap/logos/gmail.jpg b/website/roadmap/logos/gmail.jpg new file mode 100644 index 000000000..bc1643069 Binary files /dev/null and b/website/roadmap/logos/gmail.jpg differ diff --git a/website/roadmap/logos/google-calendar.jpg b/website/roadmap/logos/google-calendar.jpg new file mode 100644 index 000000000..bc1643069 Binary files /dev/null and b/website/roadmap/logos/google-calendar.jpg differ diff --git a/website/roadmap/logos/google-chat.jpg b/website/roadmap/logos/google-chat.jpg new file mode 100644 index 000000000..bc1643069 Binary files /dev/null and b/website/roadmap/logos/google-chat.jpg differ diff --git a/website/roadmap/logos/google-drive.jpg b/website/roadmap/logos/google-drive.jpg new file mode 100644 index 000000000..bc1643069 Binary files /dev/null and b/website/roadmap/logos/google-drive.jpg differ diff --git a/website/roadmap/logos/gorgias.jpg b/website/roadmap/logos/gorgias.jpg new file mode 100644 index 000000000..6dc3c611f Binary files /dev/null and b/website/roadmap/logos/gorgias.jpg differ diff --git a/website/roadmap/logos/goto.jpg b/website/roadmap/logos/goto.jpg new file mode 100644 index 000000000..c77bfeff4 Binary files /dev/null and b/website/roadmap/logos/goto.jpg differ diff --git a/website/roadmap/logos/gravity-forms.jpg b/website/roadmap/logos/gravity-forms.jpg new file mode 100644 index 000000000..87ede3434 Binary files /dev/null and b/website/roadmap/logos/gravity-forms.jpg differ diff --git a/website/roadmap/logos/greenhouse.jpg b/website/roadmap/logos/greenhouse.jpg new file mode 100644 index 000000000..83891ccbc Binary files /dev/null and b/website/roadmap/logos/greenhouse.jpg differ diff --git a/website/roadmap/logos/guesty.jpg b/website/roadmap/logos/guesty.jpg new file mode 100644 index 000000000..457e7adb4 Binary files /dev/null and b/website/roadmap/logos/guesty.jpg differ diff --git a/website/roadmap/logos/gusto.jpg b/website/roadmap/logos/gusto.jpg new file mode 100644 index 000000000..4a3606e8b Binary files /dev/null and b/website/roadmap/logos/gusto.jpg differ diff --git a/website/roadmap/logos/help-scout.jpg b/website/roadmap/logos/help-scout.jpg new file mode 100644 index 000000000..6bb7c0ef5 Binary files /dev/null and b/website/roadmap/logos/help-scout.jpg differ diff --git a/website/roadmap/logos/hootsuite.jpg b/website/roadmap/logos/hootsuite.jpg new file mode 100644 index 000000000..7784d0e72 Binary files /dev/null and b/website/roadmap/logos/hootsuite.jpg differ diff --git a/website/roadmap/logos/hubspot.jpg b/website/roadmap/logos/hubspot.jpg new file mode 100644 index 000000000..d0e55d9a3 Binary files /dev/null and b/website/roadmap/logos/hubspot.jpg differ diff --git a/website/roadmap/logos/icims.jpg b/website/roadmap/logos/icims.jpg new file mode 100644 index 000000000..e6b942f52 Binary files /dev/null and b/website/roadmap/logos/icims.jpg differ diff --git a/website/roadmap/logos/icontact.jpg b/website/roadmap/logos/icontact.jpg new file mode 100644 index 000000000..815da8e5b Binary files /dev/null and b/website/roadmap/logos/icontact.jpg differ diff --git a/website/roadmap/logos/ideascale.jpg b/website/roadmap/logos/ideascale.jpg new file mode 100644 index 000000000..3479c38fa Binary files /dev/null and b/website/roadmap/logos/ideascale.jpg differ diff --git a/website/roadmap/logos/ifttt.jpg b/website/roadmap/logos/ifttt.jpg new file mode 100644 index 000000000..66ba9c7c5 Binary files /dev/null and b/website/roadmap/logos/ifttt.jpg differ diff --git a/website/roadmap/logos/insightly.jpg b/website/roadmap/logos/insightly.jpg new file mode 100644 index 000000000..53d4b190b Binary files /dev/null and b/website/roadmap/logos/insightly.jpg differ diff --git a/website/roadmap/logos/intercom.jpg b/website/roadmap/logos/intercom.jpg new file mode 100644 index 000000000..e4bef0a5e Binary files /dev/null and b/website/roadmap/logos/intercom.jpg differ diff --git a/website/roadmap/logos/ironclad.jpg b/website/roadmap/logos/ironclad.jpg new file mode 100644 index 000000000..47f5cd03d Binary files /dev/null and b/website/roadmap/logos/ironclad.jpg differ diff --git a/website/roadmap/logos/ivans.jpg b/website/roadmap/logos/ivans.jpg new file mode 100644 index 000000000..81b809766 Binary files /dev/null and b/website/roadmap/logos/ivans.jpg differ diff --git a/website/roadmap/logos/jack-henry.jpg b/website/roadmap/logos/jack-henry.jpg new file mode 100644 index 000000000..6c9bf2ad3 Binary files /dev/null and b/website/roadmap/logos/jack-henry.jpg differ diff --git a/website/roadmap/logos/jira.jpg b/website/roadmap/logos/jira.jpg new file mode 100644 index 000000000..f74a21172 Binary files /dev/null and b/website/roadmap/logos/jira.jpg differ diff --git a/website/roadmap/logos/jumio.jpg b/website/roadmap/logos/jumio.jpg new file mode 100644 index 000000000..128376913 Binary files /dev/null and b/website/roadmap/logos/jumio.jpg differ diff --git a/website/roadmap/logos/keap.jpg b/website/roadmap/logos/keap.jpg new file mode 100644 index 000000000..08a3aa070 Binary files /dev/null and b/website/roadmap/logos/keap.jpg differ diff --git a/website/roadmap/logos/knack.jpg b/website/roadmap/logos/knack.jpg new file mode 100644 index 000000000..d87cf1516 Binary files /dev/null and b/website/roadmap/logos/knack.jpg differ diff --git a/website/roadmap/logos/lawpay.jpg b/website/roadmap/logos/lawpay.jpg new file mode 100644 index 000000000..cba46394d Binary files /dev/null and b/website/roadmap/logos/lawpay.jpg differ diff --git a/website/roadmap/logos/less-annoying-crm.jpg b/website/roadmap/logos/less-annoying-crm.jpg new file mode 100644 index 000000000..ac6ce7acb Binary files /dev/null and b/website/roadmap/logos/less-annoying-crm.jpg differ diff --git a/website/roadmap/logos/lever.jpg b/website/roadmap/logos/lever.jpg new file mode 100644 index 000000000..5aea995e9 Binary files /dev/null and b/website/roadmap/logos/lever.jpg differ diff --git a/website/roadmap/logos/linkedin.jpg b/website/roadmap/logos/linkedin.jpg new file mode 100644 index 000000000..2d6902ba8 Binary files /dev/null and b/website/roadmap/logos/linkedin.jpg differ diff --git a/website/roadmap/logos/lumapps.jpg b/website/roadmap/logos/lumapps.jpg new file mode 100644 index 000000000..eb80edd38 Binary files /dev/null and b/website/roadmap/logos/lumapps.jpg differ diff --git a/website/roadmap/logos/lumar.jpg b/website/roadmap/logos/lumar.jpg new file mode 100644 index 000000000..2ddacb950 Binary files /dev/null and b/website/roadmap/logos/lumar.jpg differ diff --git a/website/roadmap/logos/mailchimp-transactional.jpg b/website/roadmap/logos/mailchimp-transactional.jpg new file mode 100644 index 000000000..22ab23730 Binary files /dev/null and b/website/roadmap/logos/mailchimp-transactional.jpg differ diff --git a/website/roadmap/logos/mailchimp.jpg b/website/roadmap/logos/mailchimp.jpg new file mode 100644 index 000000000..22ab23730 Binary files /dev/null and b/website/roadmap/logos/mailchimp.jpg differ diff --git a/website/roadmap/logos/maps.jpg b/website/roadmap/logos/maps.jpg new file mode 100644 index 000000000..bc1643069 Binary files /dev/null and b/website/roadmap/logos/maps.jpg differ diff --git a/website/roadmap/logos/marketo-engage.jpg b/website/roadmap/logos/marketo-engage.jpg new file mode 100644 index 000000000..72c6547dc Binary files /dev/null and b/website/roadmap/logos/marketo-engage.jpg differ diff --git a/website/roadmap/logos/microsoft-graph.jpg b/website/roadmap/logos/microsoft-graph.jpg new file mode 100644 index 000000000..b8fae0084 Binary files /dev/null and b/website/roadmap/logos/microsoft-graph.jpg differ diff --git a/website/roadmap/logos/microsoft-teams.jpg b/website/roadmap/logos/microsoft-teams.jpg new file mode 100644 index 000000000..b8fae0084 Binary files /dev/null and b/website/roadmap/logos/microsoft-teams.jpg differ diff --git a/website/roadmap/logos/mitel.jpg b/website/roadmap/logos/mitel.jpg new file mode 100644 index 000000000..bfd588d0a Binary files /dev/null and b/website/roadmap/logos/mitel.jpg differ diff --git a/website/roadmap/logos/modern-treasury.jpg b/website/roadmap/logos/modern-treasury.jpg new file mode 100644 index 000000000..c52661b66 Binary files /dev/null and b/website/roadmap/logos/modern-treasury.jpg differ diff --git a/website/roadmap/logos/monday.jpg b/website/roadmap/logos/monday.jpg new file mode 100644 index 000000000..bf794a1db Binary files /dev/null and b/website/roadmap/logos/monday.jpg differ diff --git a/website/roadmap/logos/mycase.jpg b/website/roadmap/logos/mycase.jpg new file mode 100644 index 000000000..a1ec2a172 Binary files /dev/null and b/website/roadmap/logos/mycase.jpg differ diff --git a/website/roadmap/logos/ncino.jpg b/website/roadmap/logos/ncino.jpg new file mode 100644 index 000000000..f538a2e3d Binary files /dev/null and b/website/roadmap/logos/ncino.jpg differ diff --git a/website/roadmap/logos/netsuite.jpg b/website/roadmap/logos/netsuite.jpg new file mode 100644 index 000000000..fabd6b779 Binary files /dev/null and b/website/roadmap/logos/netsuite.jpg differ diff --git a/website/roadmap/logos/nimble.jpg b/website/roadmap/logos/nimble.jpg new file mode 100644 index 000000000..9a5bf21ab Binary files /dev/null and b/website/roadmap/logos/nimble.jpg differ diff --git a/website/roadmap/logos/notion.jpg b/website/roadmap/logos/notion.jpg new file mode 100644 index 000000000..be34a62f4 Binary files /dev/null and b/website/roadmap/logos/notion.jpg differ diff --git a/website/roadmap/logos/nutshell.jpg b/website/roadmap/logos/nutshell.jpg new file mode 100644 index 000000000..418cedcac Binary files /dev/null and b/website/roadmap/logos/nutshell.jpg differ diff --git a/website/roadmap/logos/odoo.jpg b/website/roadmap/logos/odoo.jpg new file mode 100644 index 000000000..0070391ea Binary files /dev/null and b/website/roadmap/logos/odoo.jpg differ diff --git a/website/roadmap/logos/onedrive.jpg b/website/roadmap/logos/onedrive.jpg new file mode 100644 index 000000000..b8fae0084 Binary files /dev/null and b/website/roadmap/logos/onedrive.jpg differ diff --git a/website/roadmap/logos/openai.jpg b/website/roadmap/logos/openai.jpg new file mode 100644 index 000000000..5190cce76 Binary files /dev/null and b/website/roadmap/logos/openai.jpg differ diff --git a/website/roadmap/logos/optimizely.jpg b/website/roadmap/logos/optimizely.jpg new file mode 100644 index 000000000..f2b13729a Binary files /dev/null and b/website/roadmap/logos/optimizely.jpg differ diff --git a/website/roadmap/logos/orion.jpg b/website/roadmap/logos/orion.jpg new file mode 100644 index 000000000..9b6ff8e01 Binary files /dev/null and b/website/roadmap/logos/orion.jpg differ diff --git a/website/roadmap/logos/otter-ai.jpg b/website/roadmap/logos/otter-ai.jpg new file mode 100644 index 000000000..435a21cc1 Binary files /dev/null and b/website/roadmap/logos/otter-ai.jpg differ diff --git a/website/roadmap/logos/outlook.jpg b/website/roadmap/logos/outlook.jpg new file mode 100644 index 000000000..b8fae0084 Binary files /dev/null and b/website/roadmap/logos/outlook.jpg differ diff --git a/website/roadmap/logos/outreach.jpg b/website/roadmap/logos/outreach.jpg new file mode 100644 index 000000000..fe4612faf Binary files /dev/null and b/website/roadmap/logos/outreach.jpg differ diff --git a/website/roadmap/logos/pandadoc.jpg b/website/roadmap/logos/pandadoc.jpg new file mode 100644 index 000000000..78c767454 Binary files /dev/null and b/website/roadmap/logos/pandadoc.jpg differ diff --git a/website/roadmap/logos/paylocity.jpg b/website/roadmap/logos/paylocity.jpg new file mode 100644 index 000000000..c74849a6d Binary files /dev/null and b/website/roadmap/logos/paylocity.jpg differ diff --git a/website/roadmap/logos/personio.jpg b/website/roadmap/logos/personio.jpg new file mode 100644 index 000000000..d28b9482a Binary files /dev/null and b/website/roadmap/logos/personio.jpg differ diff --git a/website/roadmap/logos/pipedrive.jpg b/website/roadmap/logos/pipedrive.jpg new file mode 100644 index 000000000..4948c02a9 Binary files /dev/null and b/website/roadmap/logos/pipedrive.jpg differ diff --git a/website/roadmap/logos/postman.jpg b/website/roadmap/logos/postman.jpg new file mode 100644 index 000000000..056e0cffc Binary files /dev/null and b/website/roadmap/logos/postman.jpg differ diff --git a/website/roadmap/logos/power-automate.jpg b/website/roadmap/logos/power-automate.jpg new file mode 100644 index 000000000..b8fae0084 Binary files /dev/null and b/website/roadmap/logos/power-automate.jpg differ diff --git a/website/roadmap/logos/practice-panther.jpg b/website/roadmap/logos/practice-panther.jpg new file mode 100644 index 000000000..3bc475903 Binary files /dev/null and b/website/roadmap/logos/practice-panther.jpg differ diff --git a/website/roadmap/logos/processmaker.jpg b/website/roadmap/logos/processmaker.jpg new file mode 100644 index 000000000..70988ac1e Binary files /dev/null and b/website/roadmap/logos/processmaker.jpg differ diff --git a/website/roadmap/logos/quick-base.jpg b/website/roadmap/logos/quick-base.jpg new file mode 100644 index 000000000..0d64d6c22 Binary files /dev/null and b/website/roadmap/logos/quick-base.jpg differ diff --git a/website/roadmap/logos/quickbooks-desktop.jpg b/website/roadmap/logos/quickbooks-desktop.jpg new file mode 100644 index 000000000..e5ff767bf Binary files /dev/null and b/website/roadmap/logos/quickbooks-desktop.jpg differ diff --git a/website/roadmap/logos/quickbooks-online.jpg b/website/roadmap/logos/quickbooks-online.jpg new file mode 100644 index 000000000..e5ff767bf Binary files /dev/null and b/website/roadmap/logos/quickbooks-online.jpg differ diff --git a/website/roadmap/logos/raisers-edge.jpg b/website/roadmap/logos/raisers-edge.jpg new file mode 100644 index 000000000..898f8c466 Binary files /dev/null and b/website/roadmap/logos/raisers-edge.jpg differ diff --git a/website/roadmap/logos/recharge.jpg b/website/roadmap/logos/recharge.jpg new file mode 100644 index 000000000..f78713cdd Binary files /dev/null and b/website/roadmap/logos/recharge.jpg differ diff --git a/website/roadmap/logos/recurly.jpg b/website/roadmap/logos/recurly.jpg new file mode 100644 index 000000000..2490c9074 Binary files /dev/null and b/website/roadmap/logos/recurly.jpg differ diff --git a/website/roadmap/logos/redtail.jpg b/website/roadmap/logos/redtail.jpg new file mode 100644 index 000000000..b418dfa62 Binary files /dev/null and b/website/roadmap/logos/redtail.jpg differ diff --git a/website/roadmap/logos/resource-guru.jpg b/website/roadmap/logos/resource-guru.jpg new file mode 100644 index 000000000..08e896390 Binary files /dev/null and b/website/roadmap/logos/resource-guru.jpg differ diff --git a/website/roadmap/logos/ringcentral-events.jpg b/website/roadmap/logos/ringcentral-events.jpg new file mode 100644 index 000000000..c7f02cf39 Binary files /dev/null and b/website/roadmap/logos/ringcentral-events.jpg differ diff --git a/website/roadmap/logos/ringcentral.jpg b/website/roadmap/logos/ringcentral.jpg new file mode 100644 index 000000000..c7f02cf39 Binary files /dev/null and b/website/roadmap/logos/ringcentral.jpg differ diff --git a/website/roadmap/logos/rollworks.jpg b/website/roadmap/logos/rollworks.jpg new file mode 100644 index 000000000..3ee6a77ff Binary files /dev/null and b/website/roadmap/logos/rollworks.jpg differ diff --git a/website/roadmap/logos/salesforce.jpg b/website/roadmap/logos/salesforce.jpg new file mode 100644 index 000000000..dfd4f7d52 Binary files /dev/null and b/website/roadmap/logos/salesforce.jpg differ diff --git a/website/roadmap/logos/salesloft.jpg b/website/roadmap/logos/salesloft.jpg new file mode 100644 index 000000000..dc0ae0543 Binary files /dev/null and b/website/roadmap/logos/salesloft.jpg differ diff --git a/website/roadmap/logos/schwab.jpg b/website/roadmap/logos/schwab.jpg new file mode 100644 index 000000000..6eef38aac Binary files /dev/null and b/website/roadmap/logos/schwab.jpg differ diff --git a/website/roadmap/logos/scrive.jpg b/website/roadmap/logos/scrive.jpg new file mode 100644 index 000000000..4f2ed79eb Binary files /dev/null and b/website/roadmap/logos/scrive.jpg differ diff --git a/website/roadmap/logos/sendgrid.jpg b/website/roadmap/logos/sendgrid.jpg new file mode 100644 index 000000000..719d67b3d Binary files /dev/null and b/website/roadmap/logos/sendgrid.jpg differ diff --git a/website/roadmap/logos/servicebridge.jpg b/website/roadmap/logos/servicebridge.jpg new file mode 100644 index 000000000..2056a0607 Binary files /dev/null and b/website/roadmap/logos/servicebridge.jpg differ diff --git a/website/roadmap/logos/setmore.jpg b/website/roadmap/logos/setmore.jpg new file mode 100644 index 000000000..f25420cc0 Binary files /dev/null and b/website/roadmap/logos/setmore.jpg differ diff --git a/website/roadmap/logos/sharefile.jpg b/website/roadmap/logos/sharefile.jpg new file mode 100644 index 000000000..3f833e9df Binary files /dev/null and b/website/roadmap/logos/sharefile.jpg differ diff --git a/website/roadmap/logos/sharepoint.jpg b/website/roadmap/logos/sharepoint.jpg new file mode 100644 index 000000000..b8fae0084 Binary files /dev/null and b/website/roadmap/logos/sharepoint.jpg differ diff --git a/website/roadmap/logos/sheets.jpg b/website/roadmap/logos/sheets.jpg new file mode 100644 index 000000000..bc1643069 Binary files /dev/null and b/website/roadmap/logos/sheets.jpg differ diff --git a/website/roadmap/logos/shippingeasy.jpg b/website/roadmap/logos/shippingeasy.jpg new file mode 100644 index 000000000..ce1fee5c5 Binary files /dev/null and b/website/roadmap/logos/shippingeasy.jpg differ diff --git a/website/roadmap/logos/shipstation.jpg b/website/roadmap/logos/shipstation.jpg new file mode 100644 index 000000000..67f6d6b39 Binary files /dev/null and b/website/roadmap/logos/shipstation.jpg differ diff --git a/website/roadmap/logos/shopify.jpg b/website/roadmap/logos/shopify.jpg new file mode 100644 index 000000000..dcdd7a378 Binary files /dev/null and b/website/roadmap/logos/shopify.jpg differ diff --git a/website/roadmap/logos/signority.jpg b/website/roadmap/logos/signority.jpg new file mode 100644 index 000000000..22dc3a3d6 Binary files /dev/null and b/website/roadmap/logos/signority.jpg differ diff --git a/website/roadmap/logos/simpletexting.jpg b/website/roadmap/logos/simpletexting.jpg new file mode 100644 index 000000000..b07a280e0 Binary files /dev/null and b/website/roadmap/logos/simpletexting.jpg differ diff --git a/website/roadmap/logos/slack.jpg b/website/roadmap/logos/slack.jpg new file mode 100644 index 000000000..cb930def6 Binary files /dev/null and b/website/roadmap/logos/slack.jpg differ diff --git a/website/roadmap/logos/socure.jpg b/website/roadmap/logos/socure.jpg new file mode 100644 index 000000000..586a26771 Binary files /dev/null and b/website/roadmap/logos/socure.jpg differ diff --git a/website/roadmap/logos/splunk-on-call.jpg b/website/roadmap/logos/splunk-on-call.jpg new file mode 100644 index 000000000..b07cc908b Binary files /dev/null and b/website/roadmap/logos/splunk-on-call.jpg differ diff --git a/website/roadmap/logos/square.jpg b/website/roadmap/logos/square.jpg new file mode 100644 index 000000000..c48a77ee9 Binary files /dev/null and b/website/roadmap/logos/square.jpg differ diff --git a/website/roadmap/logos/stack-exchange.jpg b/website/roadmap/logos/stack-exchange.jpg new file mode 100644 index 000000000..fb97e3308 Binary files /dev/null and b/website/roadmap/logos/stack-exchange.jpg differ diff --git a/website/roadmap/logos/streak-crm.jpg b/website/roadmap/logos/streak-crm.jpg new file mode 100644 index 000000000..99ce7f396 Binary files /dev/null and b/website/roadmap/logos/streak-crm.jpg differ diff --git a/website/roadmap/logos/stripe.jpg b/website/roadmap/logos/stripe.jpg new file mode 100644 index 000000000..fa47783cb Binary files /dev/null and b/website/roadmap/logos/stripe.jpg differ diff --git a/website/roadmap/logos/sugarcrm.jpg b/website/roadmap/logos/sugarcrm.jpg new file mode 100644 index 000000000..60ffac0a6 Binary files /dev/null and b/website/roadmap/logos/sugarcrm.jpg differ diff --git a/website/roadmap/logos/tamarac.jpg b/website/roadmap/logos/tamarac.jpg new file mode 100644 index 000000000..36cc087ee Binary files /dev/null and b/website/roadmap/logos/tamarac.jpg differ diff --git a/website/roadmap/logos/teamwork.jpg b/website/roadmap/logos/teamwork.jpg new file mode 100644 index 000000000..0b55b4d82 Binary files /dev/null and b/website/roadmap/logos/teamwork.jpg differ diff --git a/website/roadmap/logos/terminus.jpg b/website/roadmap/logos/terminus.jpg new file mode 100644 index 000000000..b463329f2 Binary files /dev/null and b/website/roadmap/logos/terminus.jpg differ diff --git a/website/roadmap/logos/timesolv.jpg b/website/roadmap/logos/timesolv.jpg new file mode 100644 index 000000000..45cb8ce4b Binary files /dev/null and b/website/roadmap/logos/timesolv.jpg differ diff --git a/website/roadmap/logos/tipalti.jpg b/website/roadmap/logos/tipalti.jpg new file mode 100644 index 000000000..bd8c56b6d Binary files /dev/null and b/website/roadmap/logos/tipalti.jpg differ diff --git a/website/roadmap/logos/totango.jpg b/website/roadmap/logos/totango.jpg new file mode 100644 index 000000000..20edbd8c5 Binary files /dev/null and b/website/roadmap/logos/totango.jpg differ diff --git a/website/roadmap/logos/touchplan.jpg b/website/roadmap/logos/touchplan.jpg new file mode 100644 index 000000000..3d0a2d5ae Binary files /dev/null and b/website/roadmap/logos/touchplan.jpg differ diff --git a/website/roadmap/logos/trello.jpg b/website/roadmap/logos/trello.jpg new file mode 100644 index 000000000..859f434f5 Binary files /dev/null and b/website/roadmap/logos/trello.jpg differ diff --git a/website/roadmap/logos/trolley.jpg b/website/roadmap/logos/trolley.jpg new file mode 100644 index 000000000..18c6ecde0 Binary files /dev/null and b/website/roadmap/logos/trolley.jpg differ diff --git a/website/roadmap/logos/twilio.jpg b/website/roadmap/logos/twilio.jpg new file mode 100644 index 000000000..c6dfcbd9f Binary files /dev/null and b/website/roadmap/logos/twilio.jpg differ diff --git a/website/roadmap/logos/typeform.jpg b/website/roadmap/logos/typeform.jpg new file mode 100644 index 000000000..1bd2346ed Binary files /dev/null and b/website/roadmap/logos/typeform.jpg differ diff --git a/website/roadmap/logos/ukg.jpg b/website/roadmap/logos/ukg.jpg new file mode 100644 index 000000000..88530fac7 Binary files /dev/null and b/website/roadmap/logos/ukg.jpg differ diff --git a/website/roadmap/logos/veem.jpg b/website/roadmap/logos/veem.jpg new file mode 100644 index 000000000..1b2126819 Binary files /dev/null and b/website/roadmap/logos/veem.jpg differ diff --git a/website/roadmap/logos/virtuous.jpg b/website/roadmap/logos/virtuous.jpg new file mode 100644 index 000000000..c412251e2 Binary files /dev/null and b/website/roadmap/logos/virtuous.jpg differ diff --git a/website/roadmap/logos/wealthbox.jpg b/website/roadmap/logos/wealthbox.jpg new file mode 100644 index 000000000..4437e7928 Binary files /dev/null and b/website/roadmap/logos/wealthbox.jpg differ diff --git a/website/roadmap/logos/webex.jpg b/website/roadmap/logos/webex.jpg new file mode 100644 index 000000000..fd7737193 Binary files /dev/null and b/website/roadmap/logos/webex.jpg differ diff --git a/website/roadmap/logos/webflow.jpg b/website/roadmap/logos/webflow.jpg new file mode 100644 index 000000000..beb5dbfac Binary files /dev/null and b/website/roadmap/logos/webflow.jpg differ diff --git a/website/roadmap/logos/whispir.jpg b/website/roadmap/logos/whispir.jpg new file mode 100644 index 000000000..30b238ffe Binary files /dev/null and b/website/roadmap/logos/whispir.jpg differ diff --git a/website/roadmap/logos/wistia.jpg b/website/roadmap/logos/wistia.jpg new file mode 100644 index 000000000..f4a9d38ee Binary files /dev/null and b/website/roadmap/logos/wistia.jpg differ diff --git a/website/roadmap/logos/woocommerce.jpg b/website/roadmap/logos/woocommerce.jpg new file mode 100644 index 000000000..2416209b9 Binary files /dev/null and b/website/roadmap/logos/woocommerce.jpg differ diff --git a/website/roadmap/logos/wordpress.jpg b/website/roadmap/logos/wordpress.jpg new file mode 100644 index 000000000..5567a8b19 Binary files /dev/null and b/website/roadmap/logos/wordpress.jpg differ diff --git a/website/roadmap/logos/workato.jpg b/website/roadmap/logos/workato.jpg new file mode 100644 index 000000000..1790783bb Binary files /dev/null and b/website/roadmap/logos/workato.jpg differ diff --git a/website/roadmap/logos/workday.jpg b/website/roadmap/logos/workday.jpg new file mode 100644 index 000000000..1fdc1baf6 Binary files /dev/null and b/website/roadmap/logos/workday.jpg differ diff --git a/website/roadmap/logos/workzone.jpg b/website/roadmap/logos/workzone.jpg new file mode 100644 index 000000000..393bcb306 Binary files /dev/null and b/website/roadmap/logos/workzone.jpg differ diff --git a/website/roadmap/logos/wrike.jpg b/website/roadmap/logos/wrike.jpg new file mode 100644 index 000000000..94b77f317 Binary files /dev/null and b/website/roadmap/logos/wrike.jpg differ diff --git a/website/roadmap/logos/wufoo.jpg b/website/roadmap/logos/wufoo.jpg new file mode 100644 index 000000000..33a3ae436 Binary files /dev/null and b/website/roadmap/logos/wufoo.jpg differ diff --git a/website/roadmap/logos/xero.jpg b/website/roadmap/logos/xero.jpg new file mode 100644 index 000000000..e6cb48c62 Binary files /dev/null and b/website/roadmap/logos/xero.jpg differ diff --git a/website/roadmap/logos/xola.jpg b/website/roadmap/logos/xola.jpg new file mode 100644 index 000000000..743303e68 Binary files /dev/null and b/website/roadmap/logos/xola.jpg differ diff --git a/website/roadmap/logos/yotpo.jpg b/website/roadmap/logos/yotpo.jpg new file mode 100644 index 000000000..81a7ca9de Binary files /dev/null and b/website/roadmap/logos/yotpo.jpg differ diff --git a/website/roadmap/logos/zapier.jpg b/website/roadmap/logos/zapier.jpg new file mode 100644 index 000000000..81a458a98 Binary files /dev/null and b/website/roadmap/logos/zapier.jpg differ diff --git a/website/roadmap/logos/zendesk-sell.jpg b/website/roadmap/logos/zendesk-sell.jpg new file mode 100644 index 000000000..7afc986e3 Binary files /dev/null and b/website/roadmap/logos/zendesk-sell.jpg differ diff --git a/website/roadmap/logos/zendesk.jpg b/website/roadmap/logos/zendesk.jpg new file mode 100644 index 000000000..7afc986e3 Binary files /dev/null and b/website/roadmap/logos/zendesk.jpg differ diff --git a/website/roadmap/logos/zerobounce.jpg b/website/roadmap/logos/zerobounce.jpg new file mode 100644 index 000000000..8c724402c Binary files /dev/null and b/website/roadmap/logos/zerobounce.jpg differ diff --git a/website/roadmap/logos/zoho-crm.jpg b/website/roadmap/logos/zoho-crm.jpg new file mode 100644 index 000000000..6946acbb3 Binary files /dev/null and b/website/roadmap/logos/zoho-crm.jpg differ diff --git a/website/roadmap/logos/zoom.jpg b/website/roadmap/logos/zoom.jpg new file mode 100644 index 000000000..fa25ba1ae Binary files /dev/null and b/website/roadmap/logos/zoom.jpg differ diff --git a/website/roadmap/logos/zoominfo.jpg b/website/roadmap/logos/zoominfo.jpg new file mode 100644 index 000000000..27f78c45e Binary files /dev/null and b/website/roadmap/logos/zoominfo.jpg differ diff --git a/website/roadmap/logos/zuora.jpg b/website/roadmap/logos/zuora.jpg new file mode 100644 index 000000000..1c54a8076 Binary files /dev/null and b/website/roadmap/logos/zuora.jpg differ diff --git a/website/tools/freya-vendor/README.md b/website/tools/freya-vendor/README.md new file mode 100644 index 000000000..f9ae0afe2 --- /dev/null +++ b/website/tools/freya-vendor/README.md @@ -0,0 +1,55 @@ +# Vendored Freya runtime + +The "Ask Freya" assistant (`friggframework-api/assistant.js`) runs on the +[Freya agent framework](https://github.com/lefthookhq/freya). Freya is an ESM, +pnpm-workspace monorepo of `@freyaframework/*` packages that is **not published +to npm**, so we vendor a prebuilt, self-contained bundle into the site instead of +depending on it at install time. + +## What's here + +| File | Role | +|---|---| +| `entry.mjs` | The bundling seed. Wires a minimal Freya runtime (Anthropic LLM + in-memory adapters + a small roadmap-retrieval tool surface) and exports `runTurn({ systemPrompt, model, messages, data }) → string`. | +| `build.mjs` | esbuild driver. Bundles `entry.mjs` + everything it imports from `@freyaframework/*` into the committed artifact below. | +| `../../friggframework-api/lib/freya-runtime.mjs` | **Generated, committed.** The self-contained bundle the Netlify function dynamic-imports. Do not edit by hand. | + +## Regenerating the bundle + +You need a built Freya checkout: + +```bash +git clone https://github.com/lefthookhq/freya +cd freya && pnpm install && pnpm build +``` + +Then, from this repo: + +```bash +FREYA_DIR=/path/to/freya node website/tools/freya-vendor/build.mjs +``` + +`FREYA_DIR` defaults to a sibling `../freya` next to the frigg checkout. Commit the +regenerated `friggframework-api/lib/freya-runtime.mjs`. Re-run whenever the pinned +Freya version should move. + +## Design notes + +- **Minimal graph.** `entry.mjs` deep-imports the specific adapters it needs + (`AnthropicLLM`, the in-memory memory/session/ontology repos, `FakeEmbedding`) + rather than the package barrels, keeping the transformers-js embedding adapter + and the Postgres adapters out of the bundle. The only optional deps left + external — `@huggingface/transformers`, `pg`, `onnxruntime-node`, `sharp` — are + reached only through lazy `import()`s that this assistant never triggers. +- **Frigg-domain ontology.** `entry.mjs` defines a small typed ontology (Platform, + ApiModule, Integration, Primitive, Capability, Adr, Visitor) that Freya renders + into the system prompt each turn, so the agent reasons in Frigg's vocabulary. + It's prompt-only grounding today; with the in-memory (ephemeral) store, post-turn + memory capture is deterministic and effectively discarded on cold start. When the + site gets a durable memory store (Freya's Supabase/Postgres adapters), the same + typed entities make capture meaningful. +- **Stateless per request.** In-memory repos reset on cold start; the widget sends + full history each call, so `runTurn` seeds a fresh session from that history and + runs the final user turn against it. +- **The seam.** `assistant.js` calls `runTurn` at the `FREYA-SEAM`; the HTTP + contract, rate limiter, and offline path are unchanged from the pre-Freya version. diff --git a/website/tools/freya-vendor/build.mjs b/website/tools/freya-vendor/build.mjs new file mode 100644 index 000000000..76cb72dee --- /dev/null +++ b/website/tools/freya-vendor/build.mjs @@ -0,0 +1,96 @@ +// Bundle the vendored Freya runtime for the "Ask Freya" Netlify function. +// +// Freya (@freyaframework/*) is an ESM, workspace-only monorepo that is not +// published to npm, so we vendor a prebuilt, self-contained bundle into the site +// rather than depend on it at install time. This script produces: +// +// website/friggframework-api/lib/freya-runtime.mjs +// +// from ./entry.mjs, inlining everything reachable from @freyaframework/{core, +// runtime, llm, memory, ontology}. The only optional deps left external are the +// ones Freya loads lazily and this assistant never exercises (transformers-js +// embeddings, Postgres) — they are referenced behind dynamic imports that never +// fire here. +// +// Usage: +// FREYA_DIR=/path/to/freya node website/tools/freya-vendor/build.mjs +// +// FREYA_DIR must point at a built Freya checkout (run `pnpm install && pnpm build` +// there first). Defaults to a sibling ../freya next to the frigg repo. Re-run this +// whenever the vendored Freya version needs to move; commit the regenerated +// freya-runtime.mjs. +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import fs from 'node:fs'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const websiteRoot = path.resolve(here, '..', '..'); // website/ +const repoRoot = path.resolve(websiteRoot, '..'); // frigg/ + +const FREYA_DIR = process.env.FREYA_DIR || path.resolve(repoRoot, '..', 'freya'); +const freyaModules = path.join(FREYA_DIR, 'node_modules'); + +if (!fs.existsSync(path.join(FREYA_DIR, 'packages', 'runtime', 'dist', 'create-agent-runtime.js'))) { + console.error( + `Freya build output not found under ${FREYA_DIR}.\n` + + `Set FREYA_DIR to a checkout where "pnpm install && pnpm build" has run.`, + ); + process.exit(1); +} + +// Use the esbuild that ships in the Freya checkout so this needs no extra install. +// pnpm keeps it in the nested store rather than hoisting it, so resolve it there +// if a plain top-level require misses. +const require = createRequire(path.join(freyaModules, 'noop.js')); +function loadEsbuild() { + try { + return require('esbuild'); + } catch { + const pnpmDir = path.join(freyaModules, '.pnpm'); + const entry = fs + .readdirSync(pnpmDir) + .filter((d) => d.startsWith('esbuild@')) + .map((d) => path.join(pnpmDir, d, 'node_modules', 'esbuild')) + .find((p) => fs.existsSync(p)); + if (!entry) throw new Error('esbuild not found in Freya node_modules'); + return require(entry); + } +} +const esbuild = loadEsbuild(); + +// pnpm doesn't link the workspace packages into the root node_modules (the root +// doesn't depend on them), and the packages import each other by bare +// @freyaframework/* specifier. Materialize a scoped symlink tree so both our +// imports and the internal cross-package imports resolve via nodePaths. +const scopeDir = path.join(freyaModules, '@freyaframework'); +fs.mkdirSync(scopeDir, { recursive: true }); +for (const pkg of fs.readdirSync(path.join(FREYA_DIR, 'packages'))) { + const target = path.join(FREYA_DIR, 'packages', pkg); + if (!fs.existsSync(path.join(target, 'package.json'))) continue; + const link = path.join(scopeDir, pkg); + if (!fs.existsSync(link)) fs.symlinkSync(target, link, 'dir'); +} + +const outfile = path.join(websiteRoot, 'friggframework-api', 'lib', 'freya-runtime.mjs'); + +await esbuild.build({ + entryPoints: [path.join(here, 'entry.mjs')], + outfile, + bundle: true, + platform: 'node', + format: 'esm', + target: 'node18', + // Resolve bare @freyaframework/* specifiers against the Freya checkout. + nodePaths: [freyaModules], + // Lazily-imported optional deps the assistant path never touches. + external: ['@huggingface/transformers', 'pg', 'onnxruntime-node', 'sharp'], + legalComments: 'none', + banner: { + js: '// GENERATED — vendored Freya runtime. Do not edit by hand.\n' + + '// Regenerate via website/tools/freya-vendor/build.mjs.', + }, +}); + +const bytes = fs.statSync(outfile).size; +console.log(`Wrote ${path.relative(repoRoot, outfile)} (${(bytes / 1024).toFixed(1)} KB)`); diff --git a/website/tools/freya-vendor/entry.mjs b/website/tools/freya-vendor/entry.mjs new file mode 100644 index 000000000..6bac20114 --- /dev/null +++ b/website/tools/freya-vendor/entry.mjs @@ -0,0 +1,325 @@ +// Vendored Freya entry — the bundling seed for the on-site "Ask Freya" assistant. +// +// esbuild bundles this module (and everything it imports from @freyaframework/*) +// into website/friggframework-api/lib/freya-runtime.mjs , a single self-contained +// ESM file the Netlify function loads. See ./build.mjs and ./README.md. +// +// Why deep imports instead of the package barrels: the @freyaframework/llm barrel +// re-exports the transformers-js embedding adapter, and the memory barrel carries +// the Postgres adapters. We need none of that here. Importing the specific adapter +// files keeps those (and their heavy optional deps) out of the bundle graph. The +// only optional dep that remains referenced is @huggingface/transformers, and only +// via a lazy `await import()` inside an embedding path we never call — it is marked +// external in build.mjs and never loads at runtime. +import { createAgentRuntime } from '@freyaframework/runtime/dist/create-agent-runtime.js'; +import { AnthropicLLM } from '@freyaframework/llm/dist/adapters/anthropic.js'; +import { FakeEmbedding } from '@freyaframework/llm/dist/adapters/fake-embedding.js'; +import { InMemoryMemoryRepository } from '@freyaframework/memory/dist/adapters/in-memory-repo.js'; +import { InMemorySessionRepository } from '@freyaframework/memory/dist/adapters/in-memory-session-repo.js'; +import { InMemoryOntologyRepository } from '@freyaframework/ontology/dist/adapters/in-memory-ontology-repo.js'; +import { parseOntologyYaml } from '@freyaframework/ontology/dist/seed/loader.js'; +import { + createUserMessage, + createAssistantMessage, + createSession, + addMessage, +} from '@freyaframework/core'; + +const AGENT_ID = 'frigg-web'; +const TRANSPORT = 'netlify-web'; + +// Frigg-domain ontology. Rendered into the system prompt each turn as a typed +// domain model, so the agent reasons in Frigg's real vocabulary (and its enum +// values line up with the search tools' category/status/complexity args). This is +// prompt-only grounding today; when the site gets a durable memory store, the same +// typed entities make Freya's memory capture meaningful (a returning visitor's +// stack/interests become typed, queryable memories). +const FRIGG_ONTOLOGY = { + name: 'frigg', + scope: 'domain', + entities: { + Platform: { + description: 'A third-party software product Frigg integrates with (e.g. HubSpot, Salesforce, Attio).', + properties: ['name', 'vendor'], + }, + ApiModule: { + description: + 'A prebuilt Frigg connector for a platform API, installed with `frigg install ` and drawn from the api-module-library.', + properties: ['name', 'provider', 'authType'], + category: [ + 'ai', 'analytics', 'commerce', 'communication', 'crm', 'devtools', + 'finance', 'hr', 'marketing', 'other', 'productivity', 'storage', 'support', + ], + complexity: ['Low', 'Medium', 'High'], + status: ['Active', 'Beta', 'Planned'], + belongs_to: 'Platform', + }, + Integration: { + description: + 'A running integration a developer builds by extending IntegrationBase, wiring API modules to events (USER_ACTION, CRON, QUEUE, WEBHOOK).', + properties: ['name', 'useCase'], + connects: ['ApiModule', 'Primitive'], + }, + Primitive: { + description: + 'A Frigg building block exposed to developers and their agents: an Endpoint, a Queue, a Provider-native backend, or a Fenestra in-app UI experience.', + properties: ['name'], + kind: ['Endpoint', 'Queue', 'ProviderNative', 'Fenestra'], + }, + Capability: { + description: + 'A typed declaration of what a module or integration can do, pointing at a spec and its implementation (the mcp-tool / agent-tooling surface).', + properties: ['name', 'spec'], + belongs_to: 'ApiModule', + }, + Adr: { + description: + 'A Frigg architecture decision record shaping the roadmap, tracked on the "next" branch and surfaced at /roadmap/.', + properties: ['num', 'title', 'theme'], + status: ['Accepted', 'Proposed', 'Superseded', 'Draft'], + }, + Visitor: { + description: 'A person chatting with the assistant on the site.', + properties: ['name', 'stack', 'interest'], + }, + }, +}; + +// Retrieval data for the current request, set at the top of each runTurn call. +// A warm Netlify instance handles one request at a time, so a module-level holder +// is safe; the tool executor reads whatever the latest turn supplied. +let activeData = { adrs: [], apis: [], categories: [], builtCount: 0 }; + +const s = (v) => (typeof v === 'string' ? v.toLowerCase() : ''); +const matches = (hay, q) => !q || s(hay).includes(s(q)); + +// A small, honest data-retrieval tool surface over the roadmap catalog. The +// agent calls these instead of reciting from a static blob, so answers track +// the committed ADR / API data rather than the prompt. +class RoadmapTools { + async discoverTools(scope) { + if (scope !== 'roadmap') return []; + return [ + { + name: 'catalog_stats', + description: + 'Frigg roadmap catalog summary: number of ADRs, number of API modules, ' + + 'how many are already built, and the list of API categories. Call this ' + + 'first for any "how many / what categories" question.', + inputSchema: { type: 'object', properties: {}, additionalProperties: false }, + source: 'roadmap', + requiresApproval: false, + permissionScope: 'roadmap:read', + }, + { + name: 'search_adrs', + description: + 'Search Frigg architecture decision records (ADRs). Filter by free-text ' + + 'query (matches title/summary/theme) and/or status (e.g. Accepted, Proposed). ' + + 'Returns matching ADRs with number, title, status, theme, one-line summary, and URL.', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Free-text filter over title/summary/theme' }, + status: { type: 'string', description: 'Exact status filter, e.g. "Accepted"' }, + }, + additionalProperties: false, + }, + source: 'roadmap', + requiresApproval: false, + permissionScope: 'roadmap:read', + }, + { + name: 'search_apis', + description: + 'Search the Frigg API module catalog (224 integrations). Filter by free-text ' + + 'query (matches name/provider/description/tags), category, or built=true to only ' + + 'return modules that already exist in api-module-library. Returns a capped list ' + + 'plus the total match count so you can point people to /roadmap/ for the full set.', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string' }, + category: { type: 'string', description: 'One of the catalog categories' }, + built: { type: 'boolean', description: 'If true, only modules already built' }, + }, + additionalProperties: false, + }, + source: 'roadmap', + requiresApproval: false, + permissionScope: 'roadmap:read', + }, + ]; + } + + async execute(call) { + const start = Date.now(); + const done = (output, status = 'success', error) => ({ + callId: call.id, + toolName: call.toolName, + output, + status, + error, + durationMs: Date.now() - start, + timestamp: new Date(), + }); + try { + const input = call.input || {}; + if (call.toolName === 'catalog_stats') { + return done({ + adrCount: activeData.adrs.length, + apiCount: activeData.apis.length, + builtCount: activeData.builtCount, + categories: activeData.categories, + }); + } + if (call.toolName === 'search_adrs') { + const hits = activeData.adrs.filter( + (a) => + (matches(a.title, input.query) || + matches(a.summary, input.query) || + matches(a.theme, input.query)) && + (!input.status || s(a.status) === s(input.status)), + ); + return done({ + total: hits.length, + adrs: hits.slice(0, 12).map((a) => ({ + num: a.num, + title: a.title, + status: a.status, + theme: a.theme, + summary: a.summary, + url: a.url, + })), + }); + } + if (call.toolName === 'search_apis') { + const hits = activeData.apis.filter( + (a) => + (matches(a.name, input.query) || + matches(a.provider, input.query) || + matches(a.description, input.query) || + (Array.isArray(a.tags) && a.tags.some((t) => matches(t, input.query)))) && + (!input.category || s(a.category) === s(input.category)) && + (input.built === undefined || Boolean(a.built) === Boolean(input.built)), + ); + return done({ + total: hits.length, + showing: Math.min(hits.length, 15), + apis: hits.slice(0, 15).map((a) => ({ + slug: a.slug, + name: a.name, + provider: a.provider, + category: a.category, + status: a.status, + complexity: a.complexity, + built: !!a.built, + library: a.library, + })), + }); + } + return done(null, 'error', `unknown tool: ${call.toolName}`); + } catch (e) { + return done(null, 'error', e && e.message ? e.message : String(e)); + } + } +} + +let runtime = null; +let sessionsRepo = null; +let registered = false; + +function getRuntime() { + if (runtime) return runtime; + sessionsRepo = new InMemorySessionRepository(); + const apiKey = process.env.ANTHROPIC_API_KEY || ''; + const baseUrl = process.env.ANTHROPIC_BASE_URL || undefined; + runtime = createAgentRuntime({ + llm: new AnthropicLLM({ + apiKey, + baseUrl, + defaultModel: process.env.ASSISTANT_MODEL || 'claude-opus-4-8', + maxTokens: 900, + }), + toolExecutor: new RoadmapTools(), + memory: new InMemoryMemoryRepository(), + ontologyRepo: (() => { + const repo = new InMemoryOntologyRepository(); + repo.addLayer(parseOntologyYaml('frigg', FRIGG_ONTOLOGY)); + return repo; + })(), + sessions: sessionsRepo, + embedding: new FakeEmbedding(), + }); + return runtime; +} + +async function ensureAgent(rt, systemPrompt, model) { + if (registered) return; + await rt.registry.registerAgent( + { + id: AGENT_ID, + name: 'Freya', + type: 'shared', + systemPrompt, + ontologyScopes: ['frigg'], + memoryNamespaces: ['default'], + toolScopes: ['roadmap'], + routines: [], + delegationTargets: [], + modelId: model || process.env.ASSISTANT_MODEL || 'claude-opus-4-8', + maxTurns: 6, + }, + 'friggframework-org', + ); + registered = true; +} + +/** + * Drive one grounded, tool-using Freya turn. + * + * @param {object} opts + * @param {string} opts.systemPrompt Static voice/rules core for the agent. + * @param {string=} opts.model Model id (defaults to ASSISTANT_MODEL / opus). + * @param {Array<{role:'user'|'assistant',content:string}>} opts.messages + * Full conversation; the last entry must be the user turn. + * @param {object=} opts.data { adrs:[], apis:{apis:[],categories:[],builtCount} }. + * @returns {Promise} The assistant reply text. + */ +export async function runTurn({ systemPrompt, model, messages, data }) { + if (data) { + const apis = data.apis || {}; + activeData = { + adrs: (data.adrs && data.adrs.adrs) || data.adrs || [], + apis: apis.apis || (Array.isArray(apis) ? apis : []), + categories: apis.categories || [], + builtCount: apis.builtCount || 0, + }; + } + + const rt = getRuntime(); + await ensureAgent(rt, systemPrompt, model); + + const history = messages.slice(0, -1); + const last = messages[messages.length - 1]; + + // Seed a fresh session with prior turns so each request is self-contained + // (in-memory repos reset on cold start; the widget always sends full history). + const sessionId = crypto.randomUUID(); + let session = createSession(sessionId, AGENT_ID, 'web-visitor', TRANSPORT); + for (const m of history) { + const msg = + m.role === 'assistant' + ? createAssistantMessage(crypto.randomUUID(), m.content) + : createUserMessage(crypto.randomUUID(), m.content, TRANSPORT); + session = addMessage(session, msg); + } + await sessionsRepo.save(session); + + const result = await rt.handleMessage({ + agentId: AGENT_ID, + sessionId, + message: createUserMessage(crypto.randomUUID(), last.content, TRANSPORT), + }); + return (result && result.message && result.message.content) || ''; +}