From 4edbd695c614ede99b8ecdd16694c41abaa3c2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Tue, 26 May 2026 15:50:11 +0100 Subject: [PATCH 1/2] feat(landing): config-first homepage on a single sales example Consolidate the homepage so every primitive snippet comes from one coherent app (examples/sales) instead of four unrelated ones, and lead with lobu.config.ts. - Pin connector/reaction/skill snippets to sales (was ecommerce/finance/ office-bot); homepage now reads as one story: Salesforce connector -> account/renewal entities -> health watcher -> reaction -> agent + skill. - Reorder sections config-first: lobu.config.ts -> Connectors -> Watchers -> Memory -> Skills. Drop the interactive use-case tab strip from the index; /for/ SEO pages keep their per-use-case snippets. - Add examples/sales/skills/account-brief (lean SKILL.md exercising nixPackages + network.allow + per-domain judge). - Type the Salesforce connector via the SDK generic (ConnectorRuntime / SyncContext); drop the (ctx.checkpoint as any) and json `as any`. - Watchers layout: watcher on the left under the copy, reaction on the right, instead of stacking both (saves vertical space). - Fix trimSkillMarkdown: fold the real judge policy instead of the hardcoded deliveroo text. - Architecture heading -> "Raw events in. Typed memory out. Agents act." - Add a Schedule a call CTA (existing ScheduleDialog) to the bottom CTA. - Trim repeated bullet lists across product sections. --- .../sales/salesforce-pipeline.connector.ts | 11 +- examples/sales/skills/account-brief/SKILL.md | 40 +++++++ .../landing/scripts/gen-landing-snippets.ts | 46 ++++---- .../src/components/ArchitectureDiagram.tsx | 2 +- packages/landing/src/components/CTA.tsx | 11 ++ .../landing/src/components/LandingPage.tsx | 100 ++++++------------ .../src/generated/landing-snippets.json | 20 ++-- 7 files changed, 124 insertions(+), 106 deletions(-) create mode 100644 examples/sales/skills/account-brief/SKILL.md diff --git a/examples/sales/salesforce-pipeline.connector.ts b/examples/sales/salesforce-pipeline.connector.ts index 76b01b72f..d8800c817 100644 --- a/examples/sales/salesforce-pipeline.connector.ts +++ b/examples/sales/salesforce-pipeline.connector.ts @@ -1,7 +1,10 @@ // biome-ignore-all format: stays compact for the landing-page code panel import { ConnectorRuntime, type SyncContext } from "@lobu/connector-sdk"; -export default class SalesforcePipelineConnector extends ConnectorRuntime { +interface Checkpoint { last_modified: string } +interface Opportunity { Id: string; Name: string; StageName: string; LastModifiedDate: string } + +export default class SalesforcePipelineConnector extends ConnectorRuntime { readonly definition = { key: "salesforce-pipeline", name: "Salesforce pipeline", @@ -10,11 +13,11 @@ export default class SalesforcePipelineConnector extends ConnectorRuntime { feeds: { opportunities: { key: "opportunities", name: "Opportunities" } }, }; - async sync(ctx: SyncContext) { - const since = (ctx.checkpoint as any)?.last_modified ?? "2000-01-01T00:00:00Z"; + async sync(ctx: SyncContext) { + const since = ctx.checkpoint?.last_modified ?? "2000-01-01T00:00:00Z"; const q = `SELECT Id,Name,StageName,LastModifiedDate FROM Opportunity WHERE LastModifiedDate > ${since} LIMIT 200`; const r = await fetch(`${ctx.config.instance_url}/services/data/v60.0/query?q=${encodeURIComponent(q)}`); - const records: any[] = (await r.json() as any).records ?? []; + const records = ((await r.json()) as { records?: Opportunity[] }).records ?? []; return { events: records.map((o) => ({ origin_id: o.Id, diff --git a/examples/sales/skills/account-brief/SKILL.md b/examples/sales/skills/account-brief/SKILL.md new file mode 100644 index 000000000..3a406ce2f --- /dev/null +++ b/examples/sales/skills/account-brief/SKILL.md @@ -0,0 +1,40 @@ +--- +name: account-brief +description: Build a pre-renewal brief for a tracked account from its recent public news and announcements. Use before a renewal call or QBR, after the account-health watcher flags a risk. Reading public news is allowed; do not log in, submit forms, or touch any CRM write endpoint. +nixPackages: + - jq +network: + allow: + - .reuters.com + - .apnews.com + judge: + - domain: newsapi.org + judge: news-read + - domain: .newsapi.org + judge: news-read +judges: + news-read: > + Allow GET reads of public news and headlines. Deny logins, posting, + and any account, billing, or write action. Fail closed if unclear. +--- + +# Account brief + +Use this skill when the user asks for a pre-renewal brief on a tracked account, +or when the `account-health-monitor` watcher flags a risk signal. + +## Steps + +1. Resolve the company name from the `organization` entity. +2. Fetch recent headlines and filter to the last 90 days. +3. Summarize anything that moves renewal risk: leadership changes, funding, + layoffs, M&A, or competitive losses. +4. Save a `renewal-risk` entity per material signal, linked to the account with + the `affects` relationship. + +## Rules + +- Read public sources only. Never log in, submit forms, or change account, + billing, or profile data. +- If a source is paywalled or asks for credentials, skip it and note the gap. +- Keep the brief to five bullets or fewer; the rep reads it on a phone. diff --git a/packages/landing/scripts/gen-landing-snippets.ts b/packages/landing/scripts/gen-landing-snippets.ts index 1f6981982..3c69c5c5d 100644 --- a/packages/landing/scripts/gen-landing-snippets.ts +++ b/packages/landing/scripts/gen-landing-snippets.ts @@ -9,15 +9,15 @@ * The landing page shows SOURCE CODE, so we slice the raw `.ts` text into * budget-sized sections; we never import/execute the config. * - * Each primitive section shows ONE canonical pinned example, used as the - * generic fallback when no use case is selected: + * Every primitive section is pinned to the single `sales` example so the + * homepage tells one coherent story (shown config-first on the page): * - * connector -> examples/ecommerce/stripe-charges.connector.ts + * connector -> examples/sales/salesforce-pipeline.connector.ts * memorySchema -> examples/sales/lobu.config.ts (defineEntityType slice) * watcher -> examples/sales/lobu.config.ts (defineWatcher slice) - * reaction -> examples/finance/reconciliation-monitor.reaction.ts + * reaction -> examples/sales/account-health-monitor.reaction.ts * agentConfig -> examples/sales/lobu.config.ts (imports + defineAgent slice) - * skill -> examples/office-bot/.../SKILL.md + * skill -> examples/sales/skills/account-brief/SKILL.md * * Plus a list of every `examples/*\/lobu.config.ts` for BrowseExamplesSection: * @@ -41,21 +41,26 @@ const outFile = resolve(__dirname, "../src/generated/landing-snippets.json"); const CONFIG_FILE = "lobu.config.ts"; +// Every pinned snippet comes from the one `sales` example so the homepage reads +// as a single coherent app (Salesforce connector -> account/renewal entities -> +// health watcher -> reaction -> agent + skill), shown config-first. The +// per-use-case tab data (useCases below) still spans many examples for the +// /for/ SEO pages. const PINNED = { connector: { - slug: "ecommerce", - path: "stripe-charges.connector.ts", + slug: "sales", + path: "salesforce-pipeline.connector.ts", }, agentConfig: { slug: "sales" }, memorySchema: { slug: "sales" }, watcher: { slug: "sales" }, reaction: { - slug: "finance", - path: "reconciliation-monitor.reaction.ts", + slug: "sales", + path: "account-health-monitor.reaction.ts", }, skill: { - slug: "office-bot", - path: "skills/deliveroo-order/SKILL.md", + slug: "sales", + path: "skills/account-brief/SKILL.md", }, } as const; @@ -336,21 +341,16 @@ function trimSkillMarkdown(raw: string): string { continue; } - // A judge policy (`: >` block scalar under judges), keep a short - // folded block scalar (valid YAML that reads naturally) in place of the - // full multi-line policy text. + // A judge policy (`: >` block scalar under judges): keep it as a + // folded block scalar but cap it at the first 2 non-empty lines of the + // real policy text so the snippet stays compact. Authors keep landing + // policies <=2 lines so nothing is cut mid-sentence. const blockScalar = /^(\s*)([\w-]+):\s*[>|][+-]?\s*$/.exec(line); if (blockScalar) { const baseIndent = blockScalar[1].length; const policyName = blockScalar[2]; - const childPad = " ".repeat(baseIndent + 2); out.push(`${" ".repeat(baseIndent)}${policyName}: >`); - out.push( - `${childPad}Allow reads and basket changes. Deny checkout, payment,` - ); - out.push( - `${childPad}saved cards, address, or profile changes. Fail closed if unclear.` - ); + let kept = 0; let j = i + 1; while (j < fm.length) { const child = fm[j]; @@ -360,6 +360,10 @@ function trimSkillMarkdown(raw: string): string { } const childIndent = child.length - child.trimStart().length; if (childIndent <= baseIndent) break; + if (kept < 2) { + out.push(child); + kept++; + } j++; } i = j; diff --git a/packages/landing/src/components/ArchitectureDiagram.tsx b/packages/landing/src/components/ArchitectureDiagram.tsx index 6886977c6..fe1533a8c 100644 --- a/packages/landing/src/components/ArchitectureDiagram.tsx +++ b/packages/landing/src/components/ArchitectureDiagram.tsx @@ -48,7 +48,7 @@ function Header() { class="font-display text-[1.85rem] font-bold leading-[1.1] tracking-tight sm:text-[2.25rem]" style={{ color: "var(--color-page-text)" }} > - Events in. Entities out. Agents on top. + Raw events in. Typed memory out. Agents act.

Read the docs + + + Schedule a call +