diff --git a/examples/agent-community/connectors/discourse-posts.connector.ts b/examples/agent-community/discourse-posts.connector.ts similarity index 100% rename from examples/agent-community/connectors/discourse-posts.connector.ts rename to examples/agent-community/discourse-posts.connector.ts diff --git a/examples/agent-community/lobu.config.ts b/examples/agent-community/lobu.config.ts index 133535768..d0a459d36 100644 --- a/examples/agent-community/lobu.config.ts +++ b/examples/agent-community/lobu.config.ts @@ -1,4 +1,5 @@ import { + connectorFromFile, defineAgent, defineConfig, defineEntityType, @@ -146,7 +147,7 @@ const opportunityMatcher = defineWatcher({ notification: { priority: "normal" }, tags: ["community", "matching"], minCooldownSeconds: 300, - reaction: "./models/reactions/opportunity-matcher.reaction.ts", + reaction: "./opportunity-matcher.reaction.ts", prompt: "Monitor connected profiles, newsletters, websites, and member updates for new launches, posts, hiring signals, funding news, and project changes. Identify which members are likely to care, explain why, and queue approved intro or outreach drafts.\n", extractionSchema: { @@ -172,6 +173,7 @@ const opportunityMatcher = defineWatcher({ }); export default defineConfig({ + connectors: [connectorFromFile("./discourse-posts.connector.ts")], org: "agent-community", orgName: "Agent Community", orgDescription: diff --git a/examples/agent-community/models/reactions/opportunity-matcher.reaction.ts b/examples/agent-community/opportunity-matcher.reaction.ts similarity index 100% rename from examples/agent-community/models/reactions/opportunity-matcher.reaction.ts rename to examples/agent-community/opportunity-matcher.reaction.ts diff --git a/examples/agent-community/tsconfig.json b/examples/agent-community/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/agent-community/tsconfig.json +++ b/examples/agent-community/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/examples/atlas/models/reactions/catalog-staleness-checker.reaction.ts b/examples/atlas/catalog-staleness-checker.reaction.ts similarity index 100% rename from examples/atlas/models/reactions/catalog-staleness-checker.reaction.ts rename to examples/atlas/catalog-staleness-checker.reaction.ts diff --git a/examples/atlas/lobu.config.ts b/examples/atlas/lobu.config.ts index d2afb03d5..f2a33acee 100644 --- a/examples/atlas/lobu.config.ts +++ b/examples/atlas/lobu.config.ts @@ -204,7 +204,7 @@ const catalogStalenessChecker = defineWatcher({ notification: { priority: "low" }, tags: ["atlas", "reference", "weekly"], minCooldownSeconds: 3600, - reaction: "./models/reactions/catalog-staleness-checker.reaction.ts", + reaction: "./catalog-staleness-checker.reaction.ts", prompt: 'Sweep the atlas reference catalog for entries that haven\'t been\nupdated in 90+ days. List the stalest 10 across cities, countries,\nindustries, technologies, and universities. Suggest a re-verification\naction for each (e.g. "country/PL: confirm population from latest census").\n', extractionSchema: { diff --git a/examples/atlas/tsconfig.json b/examples/atlas/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/atlas/tsconfig.json +++ b/examples/atlas/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/examples/delivery/lobu.config.ts b/examples/delivery/lobu.config.ts index 5c3b9f942..965ba263b 100644 --- a/examples/delivery/lobu.config.ts +++ b/examples/delivery/lobu.config.ts @@ -1,4 +1,5 @@ import { + connectorFromFile, defineAgent, defineConfig, defineEntityType, @@ -175,6 +176,7 @@ const phoenixRolloutTracker = defineWatcher({ }); export default defineConfig({ + connectors: [connectorFromFile("./shopify-orders.connector.ts")], org: "delivery", orgName: "Delivery", orgDescription: diff --git a/examples/delivery/connectors/shopify-orders.connector.ts b/examples/delivery/shopify-orders.connector.ts similarity index 100% rename from examples/delivery/connectors/shopify-orders.connector.ts rename to examples/delivery/shopify-orders.connector.ts diff --git a/examples/delivery/tsconfig.json b/examples/delivery/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/delivery/tsconfig.json +++ b/examples/delivery/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/examples/ecommerce/lobu.config.ts b/examples/ecommerce/lobu.config.ts index 4f66e55bc..abda1f812 100644 --- a/examples/ecommerce/lobu.config.ts +++ b/examples/ecommerce/lobu.config.ts @@ -1,4 +1,5 @@ import { + connectorFromFile, defineAgent, defineConfig, defineEntityType, @@ -186,6 +187,7 @@ const customerActivityTracker = defineWatcher({ }); export default defineConfig({ + connectors: [connectorFromFile("./stripe-charges.connector.ts")], org: "ecommerce", orgName: "Ecommerce", orgDescription: diff --git a/examples/ecommerce/connectors/stripe-charges.connector.ts b/examples/ecommerce/stripe-charges.connector.ts similarity index 100% rename from examples/ecommerce/connectors/stripe-charges.connector.ts rename to examples/ecommerce/stripe-charges.connector.ts diff --git a/examples/ecommerce/tsconfig.json b/examples/ecommerce/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/ecommerce/tsconfig.json +++ b/examples/ecommerce/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/examples/finance/lobu.config.ts b/examples/finance/lobu.config.ts index 3278f4956..16eeeff7a 100644 --- a/examples/finance/lobu.config.ts +++ b/examples/finance/lobu.config.ts @@ -1,4 +1,5 @@ import { + connectorFromFile, defineAgent, defineConfig, defineEntityType, @@ -171,7 +172,7 @@ const reconciliationMonitor = defineWatcher({ notification: { priority: "high", channel: "both" }, tags: ["finance", "reconciliation", "daily"], minCooldownSeconds: 3600, - reaction: "./models/reactions/reconciliation-monitor.reaction.ts", + reaction: "./reconciliation-monitor.reaction.ts", prompt: "Check accounts for unreconciled transactions, new variances, and approaching reporting deadlines. Lead with exceptions that need review.\n", extractionSchema: { @@ -187,6 +188,7 @@ const reconciliationMonitor = defineWatcher({ }); export default defineConfig({ + connectors: [connectorFromFile("./quickbooks-transactions.connector.ts")], org: "finance", orgName: "Finance", orgDescription: diff --git a/examples/finance/connectors/quickbooks-transactions.connector.ts b/examples/finance/quickbooks-transactions.connector.ts similarity index 100% rename from examples/finance/connectors/quickbooks-transactions.connector.ts rename to examples/finance/quickbooks-transactions.connector.ts diff --git a/examples/finance/models/reactions/reconciliation-monitor.reaction.ts b/examples/finance/reconciliation-monitor.reaction.ts similarity index 100% rename from examples/finance/models/reactions/reconciliation-monitor.reaction.ts rename to examples/finance/reconciliation-monitor.reaction.ts diff --git a/examples/finance/tsconfig.json b/examples/finance/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/finance/tsconfig.json +++ b/examples/finance/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/examples/leadership/connectors/linear-cycles.connector.ts b/examples/leadership/linear-cycles.connector.ts similarity index 100% rename from examples/leadership/connectors/linear-cycles.connector.ts rename to examples/leadership/linear-cycles.connector.ts diff --git a/examples/leadership/lobu.config.ts b/examples/leadership/lobu.config.ts index fa277dfb2..248152779 100644 --- a/examples/leadership/lobu.config.ts +++ b/examples/leadership/lobu.config.ts @@ -1,4 +1,5 @@ import { + connectorFromFile, defineAgent, defineConfig, defineEntityType, @@ -205,6 +206,7 @@ const boardActionTracker = defineWatcher({ }); export default defineConfig({ + connectors: [connectorFromFile("./linear-cycles.connector.ts")], org: "leadership", orgName: "Leadership", orgDescription: diff --git a/examples/leadership/tsconfig.json b/examples/leadership/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/leadership/tsconfig.json +++ b/examples/leadership/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/examples/legal/connectors/docusign-envelopes.connector.ts b/examples/legal/docusign-envelopes.connector.ts similarity index 100% rename from examples/legal/connectors/docusign-envelopes.connector.ts rename to examples/legal/docusign-envelopes.connector.ts diff --git a/examples/legal/lobu.config.ts b/examples/legal/lobu.config.ts index e3c2ed685..8116a4d05 100644 --- a/examples/legal/lobu.config.ts +++ b/examples/legal/lobu.config.ts @@ -1,4 +1,5 @@ import { + connectorFromFile, defineAgent, defineConfig, defineEntityType, @@ -196,6 +197,7 @@ const contractReviewTracker = defineWatcher({ }); export default defineConfig({ + connectors: [connectorFromFile("./docusign-envelopes.connector.ts")], org: "legal-review", orgName: "Legal", orgDescription: diff --git a/examples/legal/tsconfig.json b/examples/legal/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/legal/tsconfig.json +++ b/examples/legal/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/examples/lobu-crm/README.md b/examples/lobu-crm/README.md index eced160a8..17e4a4266 100644 --- a/examples/lobu-crm/README.md +++ b/examples/lobu-crm/README.md @@ -8,17 +8,14 @@ Use this as a starting point for new projects. It shows every Lobu concept in on ``` lobu-crm/ ├── lobu.config.ts # Agent, entities, relationships, watchers, connections, auth profiles -├── connectors/ -│ └── funnel-form.connector.ts # Custom connector implementation -├── models/ -│ └── reactions/ -│ ├── inbound-triage.reaction.ts # Runs after watcher extraction -│ └── funnel-digest.reaction.ts # Runs after watcher extraction +├── funnel-form.connector.ts # Custom connector (connectorFromFile) +├── inbound-triage.reaction.ts # Runs after watcher extraction +├── funnel-digest.reaction.ts # Runs after watcher extraction └── agents/crm/ ├── SOUL.md # Agent personality ├── IDENTITY.md # Agent identity ├── USER.md # User context - └── skills/crm-ops/SKILL.md # Agent skill + └── skills/crm-ops/SKILL.md # Agent skill (skillFromFile) ``` The built-in GitHub, X, Hacker News, and website connections are declared inline in @@ -29,5 +26,5 @@ The built-in GitHub, X, Hacker News, and website connections are declared inline | File | What it shows | |------|--------------| | `lobu.config.ts` | Agent config, providers, network allowlist, entity + relationship + watcher definitions, connections, auth profiles | -| `connectors/funnel-form.connector.ts` | Custom connector with typed checkpoint + config | -| `models/reactions/inbound-triage.reaction.ts` | Reaction script with typed `ReactionClient` | +| `funnel-form.connector.ts` | Custom connector with typed checkpoint + config (listed via `connectorFromFile`) | +| `inbound-triage.reaction.ts` | Reaction script with typed `ReactionClient` | diff --git a/examples/lobu-crm/models/reactions/funnel-digest.reaction.ts b/examples/lobu-crm/funnel-digest.reaction.ts similarity index 100% rename from examples/lobu-crm/models/reactions/funnel-digest.reaction.ts rename to examples/lobu-crm/funnel-digest.reaction.ts diff --git a/examples/lobu-crm/connectors/funnel-form.connector.ts b/examples/lobu-crm/funnel-form.connector.ts similarity index 100% rename from examples/lobu-crm/connectors/funnel-form.connector.ts rename to examples/lobu-crm/funnel-form.connector.ts diff --git a/examples/lobu-crm/models/reactions/inbound-triage.reaction.ts b/examples/lobu-crm/inbound-triage.reaction.ts similarity index 100% rename from examples/lobu-crm/models/reactions/inbound-triage.reaction.ts rename to examples/lobu-crm/inbound-triage.reaction.ts diff --git a/examples/lobu-crm/lobu.config.ts b/examples/lobu-crm/lobu.config.ts index 8fe47ceea..1d57ba4d6 100644 --- a/examples/lobu-crm/lobu.config.ts +++ b/examples/lobu-crm/lobu.config.ts @@ -1,4 +1,5 @@ import { + connectorFromFile, defineAgent, defineAuthProfile, defineConfig, @@ -154,7 +155,7 @@ const funnel_digestWatcher = defineWatcher({ notification: { channel: "both", priority: "high" }, minCooldownSeconds: 3600, tags: ["crm", "weekly"], - reaction: "./models/reactions/funnel-digest.reaction.ts", + reaction: "./funnel-digest.reaction.ts", prompt: 'Produce the weekly funnel digest and post it to Slack. Keep it short.\n\n1. The single recommended action for the week, on the first line. Pick the\n move that does the most to get pilot #1 closer (almost always: follow up\n with the warmest lead in "conversation", or progress whichever pilot\n conversation is furthest along).\n2. Funnel snapshot: count of `lead` entities per stage; what moved since the\n last digest (new leads, stage changes, new/updated `pilot` entities).\n3. Top-of-funnel since last digest: new GitHub stars, X mentions/replies,\n HN/PH activity.\n4. Stale: any lead in `conversation` with no `lead:interaction` in 7+ days —\n list them for follow-up.\n5. One gap callout if there is one (e.g. "18 new stars, 0 became leads —\n is inbound-triage catching the right signal?").\n\nTone: a checklist a busy founder reads in 30 seconds. End on the next action,\nnot the status. Remember: the metric that matters is customer conversations\nthis week — if that number is below 3, say so plainly.\n', extractionSchema: { @@ -200,7 +201,7 @@ const inbound_triageWatcher = defineWatcher({ notification: { priority: "normal" }, minCooldownSeconds: 300, tags: ["crm", "triage"], - reaction: "./models/reactions/inbound-triage.reaction.ts", + reaction: "./inbound-triage.reaction.ts", prompt: 'Look for new top-of-funnel signals since the last run, across the connectors\nin this org:\n - GitHub: new stargazers on lobu-ai/lobu; new issues / issue comments /\n PR comments — especially anything with deployment, self-host, multi-tenant,\n "how do I", or evaluation language.\n - X: new @-mentions of Lobu, replies to Burak\'s Lobu threads, quote-tweets.\n - Hacker News / Product Hunt: new comments or posts mentioning Lobu or OpenClaw.\n\nFor each signal that looks like a real person (not a bot, not a casual star):\n 1. search_memory for an existing `lead` (match github handle / x handle / email).\n 2. If none, create a `lead` entity at the lowest stage the evidence supports\n (a bare star → "signal"; a deployment-flavored issue comment or a\n "how do I deploy this for my team" mention → "trial" or "conversation"),\n with source set to where it came from, and entity_ids linking to the\n source event. Then save a `lead:created` event.\n 3. If a lead exists, enrich it (add the handle, bump the stage if the new\n signal warrants it, update last_touch) and save a `lead:interaction` or\n `lead:stage_changed` event as appropriate.\n\nThen post to Slack: the new/updated leads, ranked by closeness-to-a-paying-pilot,\neach with a one-line recommended next action (e.g. "reply on the issue and offer\na 20-min call"). If nothing notable, post nothing — don\'t manufacture noise.\n', extractionSchema: { @@ -378,6 +379,7 @@ const x_mentionsConn = defineConnection({ }); export default defineConfig({ + connectors: [connectorFromFile("./funnel-form.connector.ts")], org: "lobu-crm", orgName: "Lobu CRM", orgDescription: diff --git a/examples/lobu-crm/tsconfig.json b/examples/lobu-crm/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/lobu-crm/tsconfig.json +++ b/examples/lobu-crm/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/examples/market/connectors/exa-news-feed.connector.ts b/examples/market/exa-news-feed.connector.ts similarity index 100% rename from examples/market/connectors/exa-news-feed.connector.ts rename to examples/market/exa-news-feed.connector.ts diff --git a/examples/market/models/reactions/founder-activity-tracker.reaction.ts b/examples/market/founder-activity-tracker.reaction.ts similarity index 100% rename from examples/market/models/reactions/founder-activity-tracker.reaction.ts rename to examples/market/founder-activity-tracker.reaction.ts diff --git a/examples/market/lobu.config.ts b/examples/market/lobu.config.ts index 6f491f66e..e327fa1e8 100644 --- a/examples/market/lobu.config.ts +++ b/examples/market/lobu.config.ts @@ -1,4 +1,5 @@ import { + connectorFromFile, defineAgent, defineConfig, defineEntityType, @@ -456,7 +457,7 @@ const founderActivityTracker = defineWatcher({ notification: { priority: "normal" }, tags: ["vc", "founders", "daily"], minCooldownSeconds: 600, - reaction: "./models/reactions/founder-activity-tracker.reaction.ts", + reaction: "./founder-activity-tracker.reaction.ts", prompt: "You are a venture capital analyst tracking the public activity of startup founders in your portfolio.\n\n## Founders\n{{#each entities}}\n- {{name}} ({{entity_type}}, ID: {{id}})\n{{/each}}\n\n## Recent Founder Activity\n{{#if sources.founder_posts}}\n{{sources.founder_posts}}\n{{/if}}\n\n---\n\nProduce a structured founder activity report:\n1. **Executive Summary**: 2-3 sentence overview of founder activity and signals.\n2. **Per-Founder Analysis**: For each active founder, summarize their messaging themes, engagement level, and signals about company direction.\n3. **Cross-Portfolio Patterns**: Themes multiple founders discuss.\n4. **Notable Signals**: Flag potential announcements, strategic shifts, or concerns.\n\nBe specific and cite actual tweets/posts as evidence.\n", sources: { @@ -582,6 +583,7 @@ const opportunityMatcher = defineWatcher({ }); export default defineConfig({ + connectors: [connectorFromFile("./exa-news-feed.connector.ts")], org: "market", orgName: "Market", orgDescription: diff --git a/examples/market/tsconfig.json b/examples/market/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/market/tsconfig.json +++ b/examples/market/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/examples/office-bot/tsconfig.json b/examples/office-bot/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/office-bot/tsconfig.json +++ b/examples/office-bot/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/examples/personal-finance/tsconfig.json b/examples/personal-finance/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/personal-finance/tsconfig.json +++ b/examples/personal-finance/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/examples/sales/models/reactions/account-health-monitor.reaction.ts b/examples/sales/account-health-monitor.reaction.ts similarity index 100% rename from examples/sales/models/reactions/account-health-monitor.reaction.ts rename to examples/sales/account-health-monitor.reaction.ts diff --git a/examples/sales/lobu.config.ts b/examples/sales/lobu.config.ts index 24c96552e..c32db565b 100644 --- a/examples/sales/lobu.config.ts +++ b/examples/sales/lobu.config.ts @@ -1,4 +1,5 @@ import { + connectorFromFile, defineAgent, defineConfig, defineEntityType, @@ -183,7 +184,7 @@ const accountHealthMonitor = defineWatcher({ notification: { priority: "high", channel: "both" }, tags: ["sales", "health", "renewals"], minCooldownSeconds: 1800, - reaction: "./models/reactions/account-health-monitor.reaction.ts", + reaction: "./account-health-monitor.reaction.ts", prompt: "Poll CRM data for tracked accounts. Track expansion progress, risk level changes, and renewal timeline.\n", extractionSchema: { @@ -204,6 +205,7 @@ const accountHealthMonitor = defineWatcher({ }); export default defineConfig({ + connectors: [connectorFromFile("./salesforce-pipeline.connector.ts")], org: "sales", orgName: "Sales", orgDescription: diff --git a/examples/sales/connectors/salesforce-pipeline.connector.ts b/examples/sales/salesforce-pipeline.connector.ts similarity index 100% rename from examples/sales/connectors/salesforce-pipeline.connector.ts rename to examples/sales/salesforce-pipeline.connector.ts diff --git a/examples/sales/tsconfig.json b/examples/sales/tsconfig.json index 168612855..a706088ce 100644 --- a/examples/sales/tsconfig.json +++ b/examples/sales/tsconfig.json @@ -10,8 +10,7 @@ "include": [ "lobu.config.ts", "agents/**/*.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", - "models/**/*.ts" + "**/*.connector.ts", + "**/*.reaction.ts" ] } diff --git a/packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts b/packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts index d8162e957..d9fc10b3a 100644 --- a/packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts +++ b/packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts @@ -55,11 +55,10 @@ describe("loadDesiredStateFromConfig", () => { ); }); - test("ships local connectors/*.connector.ts source referenced by a connection", async () => { + test("ships a connectorFromFile source referenced by a connection", async () => { dir = mkdtempSync(join(import.meta.dir, "connector-")); - mkdirSync(join(dir, "connectors")); writeFileSync( - join(dir, "connectors", "weather.connector.ts"), + join(dir, "weather.connector.ts"), [ `import { defineConnector } from "@lobu/connector-sdk/define-connector";`, `export default defineConnector({`, @@ -72,9 +71,10 @@ describe("loadDesiredStateFromConfig", () => { writeFileSync( join(dir, "lobu.config.ts"), [ - `import { defineAgent, defineConfig, defineConnection } from "@lobu/cli/config";`, + `import { connectorFromFile, defineAgent, defineConfig, defineConnection } from "@lobu/cli/config";`, `export default defineConfig({`, ` agents: [defineAgent({ id: "crm" })],`, + ` connectors: [connectorFromFile("./weather.connector.ts")],`, ` connections: [defineConnection({ slug: "weather", connector: "weather" })],`, `});`, ``, @@ -85,7 +85,7 @@ describe("loadDesiredStateFromConfig", () => { expect(state.connectors.definitions).toHaveLength(1); const def = state.connectors.definitions[0]; expect(def?.key).toBeNull(); - expect(def?.sourceFile).toBe("connectors/weather.connector.ts"); + expect(def?.sourceFile).toBe("weather.connector.ts"); expect(def?.sourcePath).toContain("weather.connector.ts"); expect(def?.sourceCode).toContain("defineConnector"); // The connection references the connector by key; the server resolves the @@ -93,11 +93,10 @@ describe("loadDesiredStateFromConfig", () => { expect(state.connectors.connections[0]?.connector).toBe("weather"); }); - test("--only agents skips local connector definitions", async () => { + test("--only agents skips connector definitions", async () => { dir = mkdtempSync(join(import.meta.dir, "only-")); - mkdirSync(join(dir, "connectors")); writeFileSync( - join(dir, "connectors", "weather.connector.ts"), + join(dir, "weather.connector.ts"), [ `import { defineConnector } from "@lobu/connector-sdk/define-connector";`, `export default defineConnector({ key: "weather", feeds: { current: { sync: async () => [] } } });`, @@ -107,8 +106,11 @@ describe("loadDesiredStateFromConfig", () => { writeFileSync( join(dir, "lobu.config.ts"), [ - `import { defineAgent, defineConfig } from "@lobu/cli/config";`, - `export default defineConfig({ agents: [defineAgent({ id: "crm" })] });`, + `import { connectorFromFile, defineAgent, defineConfig } from "@lobu/cli/config";`, + `export default defineConfig({`, + ` agents: [defineAgent({ id: "crm" })],`, + ` connectors: [connectorFromFile("./weather.connector.ts")],`, + `});`, ``, ].join("\n") ); @@ -120,41 +122,69 @@ describe("loadDesiredStateFromConfig", () => { expect(state.connectors.definitions).toHaveLength(0); }); - test("discovers multiple .connector.ts files sorted, ignoring non-matching files and subdirs", async () => { + test("connector definitions are sorted by sourceFile", async () => { dir = mkdtempSync(join(import.meta.dir, "multi-")); - mkdirSync(join(dir, "connectors")); - const connectorSrc = `import { defineConnector } from "@lobu/connector-sdk/define-connector";\nexport default defineConnector({ key: "x", feeds: {} });\n`; - // Out-of-order on disk; result must be sorted by sourceFile. - writeFileSync(join(dir, "connectors", "beta.connector.ts"), connectorSrc); - writeFileSync(join(dir, "connectors", "alpha.connector.ts"), connectorSrc); - // Non-matching files are ignored. - writeFileSync( - join(dir, "connectors", "helper.ts"), - `export const x = 1;\n` - ); - writeFileSync(join(dir, "connectors", "README.md"), `# connectors\n`); - // Nested .connector.ts is ignored (scan is non-recursive). - mkdirSync(join(dir, "connectors", "nested")); - writeFileSync( - join(dir, "connectors", "nested", "deep.connector.ts"), - connectorSrc - ); + const src = `import { defineConnector } from "@lobu/connector-sdk/define-connector";\nexport default defineConnector({ key: "x", feeds: {} });\n`; + writeFileSync(join(dir, "beta.connector.ts"), src); + writeFileSync(join(dir, "alpha.connector.ts"), src); writeFileSync( join(dir, "lobu.config.ts"), [ - `import { defineAgent, defineConfig } from "@lobu/cli/config";`, - `export default defineConfig({ agents: [defineAgent({ id: "crm" })] });`, + `import { connectorFromFile, defineAgent, defineConfig } from "@lobu/cli/config";`, + `export default defineConfig({`, + ` agents: [defineAgent({ id: "crm" })],`, + ` connectors: [`, + ` connectorFromFile("./beta.connector.ts"),`, + ` connectorFromFile("./alpha.connector.ts"),`, + ` ],`, + `});`, ``, ].join("\n") ); const { state } = await loadDesiredStateFromConfig({ cwd: dir }); expect(state.connectors.definitions.map((d) => d.sourceFile)).toEqual([ - "connectors/alpha.connector.ts", - "connectors/beta.connector.ts", + "alpha.connector.ts", + "beta.connector.ts", ]); }); + test("connectorFromFile with a missing file fails clearly", async () => { + dir = mkdtempSync(join(import.meta.dir, "missingconn-")); + writeFileSync( + join(dir, "lobu.config.ts"), + [ + `import { connectorFromFile, defineAgent, defineConfig } from "@lobu/cli/config";`, + `export default defineConfig({`, + ` agents: [defineAgent({ id: "crm" })],`, + ` connectors: [connectorFromFile("./nope.connector.ts")],`, + `});`, + ``, + ].join("\n") + ); + await expect(loadDesiredStateFromConfig({ cwd: dir })).rejects.toThrow( + /connectorFromFile.*does not exist/ + ); + }); + + test("connectorFromFile rejects a path escaping the config dir", async () => { + dir = mkdtempSync(join(import.meta.dir, "escconn-")); + writeFileSync( + join(dir, "lobu.config.ts"), + [ + `import { connectorFromFile, defineAgent, defineConfig } from "@lobu/cli/config";`, + `export default defineConfig({`, + ` agents: [defineAgent({ id: "crm" })],`, + ` connectors: [connectorFromFile("../evil.connector.ts")],`, + `});`, + ``, + ].join("\n") + ); + await expect(loadDesiredStateFromConfig({ cwd: dir })).rejects.toThrow( + /must not contain `\.\.`|resolves outside/ + ); + }); + test("loads agent-dir markdown + a file skill, merging skill network/nix", async () => { dir = mkdtempSync(join(import.meta.dir, "agentdir-")); const agentDir = join(dir, "agents", "crm"); diff --git a/packages/cli/src/commands/_lib/apply/desired-state.ts b/packages/cli/src/commands/_lib/apply/desired-state.ts index 24f4534b3..3b45d950a 100644 --- a/packages/cli/src/commands/_lib/apply/desired-state.ts +++ b/packages/cli/src/commands/_lib/apply/desired-state.ts @@ -1,5 +1,5 @@ import { existsSync, readFileSync } from "node:fs"; -import { readdir, readFile, stat } from "node:fs/promises"; +import { readFile } from "node:fs/promises"; import { basename, isAbsolute, join, relative, resolve, sep } from "node:path"; import { pathToFileURL } from "node:url"; import type { @@ -10,7 +10,7 @@ import type { import type { AgentSettings } from "@lobu/core"; import Ajv from "ajv"; import addFormats from "ajv-formats"; -import type { Project, Skill } from "../../../config/index.js"; +import type { ConnectorSource, Project, Skill } from "../../../config/index.js"; import { ValidationError } from "../../memory/_lib/errors.js"; import { mapProjectToDesiredState, @@ -215,8 +215,8 @@ export interface DesiredState { /** Watchers declared via `defineWatcher`. */ watchers: DesiredWatcher[]; /** - * Connectors: local `*.connector.ts` definitions (discovered under - * `./connectors`), `defineConnection`s, and `defineAuthProfile`s. + * Connectors: local `*.connector.ts` definitions (declared via + * `connectorFromFile`), `defineConnection`s, and `defineAuthProfile`s. */ connectors: { definitions: DesiredConnectorDefinition[]; @@ -707,60 +707,74 @@ export interface LoadDesiredStateOptions { } /** - * Discover local connector definitions for the TypeScript config path. + * Resolve the project's explicit `connectors: [connectorFromFile(...)]` list + * into connector definitions to compile + ship. Replaces directory + * auto-discovery: only listed connectors are uploaded. Paths are relative to + * the config dir and guarded (no absolute, `..`, or backslash escapes), + * mirroring `resolveReactionScript`. * - * A `lobu.config.ts` references connectors by key (or via the class returned by - * `defineConnector`); the source the server compiles lives in - * `./connectors/*.connector.ts`. We ship each file's source with `key: null` — - * the server compiles it and resolves the real key, the same contract the YAML - * loader used for auto-discovered `.connector.ts` files. `apply-cmd` then - * compiles each `sourcePath` on the CLI (where the project's node_modules is - * available) and uploads it via `install_connector`. - * - * We intentionally do NOT compile/instantiate the connector here to resolve its - * key eagerly: that would force a full esbuild + module load (and installed - * project deps, and any module-load side effects) on every load — including - * `--dry-run` — for no benefit, since the server is the source of truth for the - * compiled key. The cost is deferred to post-confirmation install in apply-cmd. - * - * Caveat (shared with YAML auto-discovery, see `locallyDeclaredConnectorKeys`): - * because the shipped key is `null`, a connection's config is validated against - * the *fresh* catalog only after install, and a connection that references a + * Each source ships with `key: null`; `apply-cmd` compiles each `sourcePath` on + * the CLI (where the project's node_modules is available) and the server + * resolves the real key. We intentionally do NOT compile/instantiate here to + * resolve the key eagerly — that would force a full esbuild + module load on + * every load (including `--dry-run`) for no benefit, since the server is the + * source of truth for the compiled key. A connection that references a * connector by a bare *string* key relies on that string matching the file's - * compiled `definition.key`. Reference the connector by its `defineConnector` - * class instead (`connector: myConnector`) to make that match exact — the - * mapper resolves the key from `definition.key`, so a typo can't silently bind - * the connection to a different (bundled/remote) connector. + * compiled `definition.key`; reference it by its `defineConnector` class + * (`connector: myConnector`) to make that match exact. */ -async function discoverLocalConnectorDefinitions( +function resolveConnectorSources( + sources: ConnectorSource[], cwd: string -): Promise { - const dirPath = resolve(cwd, "connectors"); - let entries: string[]; - try { - entries = (await readdir(dirPath)).sort(); - } catch { - // No `./connectors` dir — a project may declare no local connectors. - return []; - } - +): DesiredConnectorDefinition[] { + const baseDir = resolve(cwd); const defs: DesiredConnectorDefinition[] = []; - for (const entry of entries) { - if (!entry.endsWith(".connector.ts")) continue; - const entryPath = join(dirPath, entry); - let entryStat; + for (const src of sources) { + const rel = src.path.trim(); + if (!rel) { + throw new ValidationError( + "connectorFromFile() requires a path to a `*.connector.ts` file" + ); + } + if (rel.startsWith("/") || rel.includes("\\")) { + throw new ValidationError( + `connectorFromFile(${JSON.stringify(rel)}) must be a relative POSIX path (./foo.connector.ts) — absolute paths and backslashes are not allowed` + ); + } + if (rel.split("/").some((seg) => seg === "..")) { + throw new ValidationError( + `connectorFromFile(${JSON.stringify(rel)}) must not contain \`..\` segments — keep the connector under the config directory` + ); + } + if (!rel.endsWith(".ts")) { + throw new ValidationError( + `connectorFromFile(${JSON.stringify(rel)}) must point at a \`.ts\` file` + ); + } + const abs = resolve(baseDir, rel); + const relPath = relative(baseDir, abs); + if ( + relPath === ".." || + relPath.startsWith(`..${sep}`) || + isAbsolute(relPath) + ) { + throw new ValidationError( + `connectorFromFile(${JSON.stringify(rel)}) resolves outside the config directory (${abs})` + ); + } + let sourceCode: string; try { - entryStat = await stat(entryPath); + sourceCode = readFileSync(abs, "utf-8"); } catch { - continue; + throw new ValidationError( + `connectorFromFile(${JSON.stringify(rel)}) does not exist (resolved to ${abs})` + ); } - if (!entryStat.isFile()) continue; - const sourceCode = await readFile(entryPath, "utf-8"); defs.push({ key: null, - sourcePath: entryPath, + sourcePath: abs, sourceCode, - sourceFile: `connectors/${entry}`, + sourceFile: rel.replace(/^\.\//, ""), }); } return defs.sort((a, b) => a.sourceFile.localeCompare(b.sourceFile)); @@ -935,7 +949,8 @@ export async function loadDesiredStateFromConfig( // `--only agents|memory` skips connectors (matching the mapper), so don't // ship local connector source for those runs either. if (!opts.only) { - state.connectors.definitions = await discoverLocalConnectorDefinitions( + state.connectors.definitions = resolveConnectorSources( + typedProject.connectors ?? [], opts.cwd ); } diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index a56c4841f..160830e2e 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -212,9 +212,9 @@ export async function scaffoldProjectPackaging( }, include: [ "lobu.config.ts", - "connectors/**/*.ts", - "reactions/**/*.ts", "agents/**/*.ts", + "**/*.connector.ts", + "**/*.reaction.ts", ], }, null, diff --git a/packages/cli/src/config/define.ts b/packages/cli/src/config/define.ts index dffc1ca65..ac6f9e054 100644 --- a/packages/cli/src/config/define.ts +++ b/packages/cli/src/config/define.ts @@ -112,6 +112,24 @@ export function defineConnection(config: Omit): Connection { return { ...config, kind: "connection" }; } +/** + * A local connector source file to compile and ship at `lobu apply`. Built with + * {@link connectorFromFile} and listed in {@link Project.connectors}. This is + * explicit — only listed connectors are compiled and uploaded; there is no + * `./connectors` directory auto-discovery. Connections reference the connector + * by key (or its `defineConnector` class), independent of this list. + */ +export interface ConnectorSource { + readonly kind: "connectorSource"; + /** Path to a `*.connector.ts`, relative to the config file. */ + path: string; +} + +/** Reference a local connector source file to compile + ship at apply time. */ +export function connectorFromFile(path: string): ConnectorSource { + return { kind: "connectorSource", path }; +} + // --------------------------------------------------------------------------- // Watchers (reaction handlers are wired in a later slice) // --------------------------------------------------------------------------- @@ -402,6 +420,12 @@ export interface Project { connections?: Connection[]; authProfiles?: AuthProfile[]; watchers?: Watcher[]; + /** + * Local connector source files (`*.connector.ts`) to compile and ship, + * built with {@link connectorFromFile}. Explicit list, no `./connectors` + * auto-discovery; only listed connectors are uploaded. + */ + connectors?: ConnectorSource[]; } export function defineConfig(config: Omit): Project { diff --git a/packages/landing/scripts/gen-landing-snippets.ts b/packages/landing/scripts/gen-landing-snippets.ts index b89a097a3..199fb70f7 100644 --- a/packages/landing/scripts/gen-landing-snippets.ts +++ b/packages/landing/scripts/gen-landing-snippets.ts @@ -12,10 +12,10 @@ * Each primitive section shows ONE canonical pinned example, used as the * generic fallback when no use case is selected: * - * connector -> examples/ecommerce/connectors/stripe-charges.connector.ts + * connector -> examples/ecommerce/stripe-charges.connector.ts * memorySchema -> examples/sales/lobu.config.ts (defineEntityType slice) * watcher -> examples/sales/lobu.config.ts (defineWatcher slice) - * reaction -> examples/finance/models/reactions/reconciliation-monitor.reaction.ts + * reaction -> examples/finance/reconciliation-monitor.reaction.ts * agentConfig -> examples/sales/lobu.config.ts (imports + defineAgent slice) * skill -> examples/office-bot/.../SKILL.md * @@ -44,14 +44,14 @@ const CONFIG_FILE = "lobu.config.ts"; const PINNED = { connector: { slug: "ecommerce", - path: "connectors/stripe-charges.connector.ts", + path: "stripe-charges.connector.ts", }, agentConfig: { slug: "sales" }, memorySchema: { slug: "sales" }, watcher: { slug: "sales" }, reaction: { slug: "finance", - path: "models/reactions/reconciliation-monitor.reaction.ts", + path: "reconciliation-monitor.reaction.ts", }, skill: { slug: "office-bot", @@ -94,7 +94,7 @@ type LandingSnippets = { /** Slugs that get per-use-case connector / memory / watcher snippets. The id * equals the example directory name. Each dir has exactly one - * connectors/*.connector.ts and a lobu.config.ts. */ + * *.connector.ts and a lobu.config.ts. */ const USE_CASE_SLUGS = [ "legal", "finance", @@ -399,12 +399,10 @@ function listExamples(): ExampleEntry[] { } function findConnectorFile(slug: string): { rel: string } { - const connectorsDir = resolve(examplesDir, slug, "connectors"); - const file = readdirSync(connectorsDir).find((f) => - f.endsWith(".connector.ts") - ); - if (!file) throw new Error(`No *.connector.ts in ${connectorsDir}`); - return { rel: `connectors/${file}` }; + const exampleDir = resolve(examplesDir, slug); + const file = readdirSync(exampleDir).find((f) => f.endsWith(".connector.ts")); + if (!file) throw new Error(`No *.connector.ts in ${exampleDir}`); + return { rel: file }; } function buildUseCases(): Record { diff --git a/packages/landing/src/components/LandingPage.tsx b/packages/landing/src/components/LandingPage.tsx index d6e34cfde..852fe5e6d 100644 --- a/packages/landing/src/components/LandingPage.tsx +++ b/packages/landing/src/components/LandingPage.tsx @@ -47,7 +47,7 @@ const SETUP_PROMPT = `I want to build a Lobu agent. 2. Walk me through the skill's onboarding interview (it asks what the agent should do, who uses it, where data comes from, where I'll talk to it, what should run on a schedule). Pause at every real decision and ask me, don't fake credentials, don't guess. -3. Scaffold the project per my answers (lobu.config.ts, connectors/, models/reactions/), boot it locally, send a test message via the chosen channel, and show me the memory event that was written. +3. Scaffold the project per my answers (lobu.config.ts plus any connector, reaction, and skill files it references), boot it locally, send a test message via the chosen channel, and show me the memory event that was written. Lobu is an open-source event-sourced backend for AI agents: connectors emit events, memory keeps the structured record, agents react in real time and dream on cron. Repo: https://github.com/lobu-ai/lobu. Docs: https://lobu.ai/docs/`; @@ -998,7 +998,7 @@ function RunAnywhereSection() { style={{ color: "var(--color-page-text-muted)" }} > Same lobu.config.ts +{" "} - connectors/ +{" "} + *.connector.ts +{" "} agents/. One command to boot embedded; Docker + Helm for self-hosting; Lobu Cloud when you don't want to run it yourself. diff --git a/packages/landing/src/content/docs/getting-started/connector-sdk.md b/packages/landing/src/content/docs/getting-started/connector-sdk.md index a4f5ba1e0..e4886b5a1 100644 --- a/packages/landing/src/content/docs/getting-started/connector-sdk.md +++ b/packages/landing/src/content/docs/getting-started/connector-sdk.md @@ -152,7 +152,18 @@ A few things to notice: - **The PAT is a `lobu_secret_` placeholder at runtime.** The gateway's secret proxy swaps it for the real value when the outbound HTTPS request leaves the worker, so the secret never lives in the worker's memory. - **Pagination via the `since` query param.** The GitHub `Link` header is the alternative for cursor-style paging when you need to walk a stable, ordered list; `since` is simpler when the source already gives you a monotonic timestamp. -Drop this file at `connectors/github-issues.connector.ts` in your Lobu project. `lobu apply` ships the source to the gateway, which compiles and registers it; from there each `feeds.` entry shows up as something a user can create a connection for in the admin UI. +Save this file in your Lobu project (e.g. `github-issues.connector.ts` next to `lobu.config.ts`) and list it in your config: + +```ts +import { connectorFromFile, defineConfig } from "@lobu/cli/config"; + +export default defineConfig({ + connectors: [connectorFromFile("./github-issues.connector.ts")], + // ...agents, connections, etc. +}); +``` + +`lobu apply` ships the source to the gateway, which compiles and registers it; from there each `feeds.` entry shows up as something a user can create a connection for in the admin UI. ## Concepts @@ -305,18 +316,17 @@ Rules of thumb: ## Where the file lives -In your Lobu project, drop `*.connector.ts` files under `connectors/`: +A `*.connector.ts` file can live anywhere in your Lobu project; reference each one explicitly with `connectorFromFile` in `defineConfig({ connectors })`: ``` my-agent/ -├── lobu.config.ts -├── connectors/ -│ ├── github-issues.connector.ts -│ └── stripe-charges.connector.ts +├── lobu.config.ts # connectors: [connectorFromFile("./github-issues.connector.ts")] +├── github-issues.connector.ts +├── stripe-charges.connector.ts └── agents/my-agent/... ``` -`lobu apply` discovers, type-checks, and ships them. Update the `version` field whenever the event shape changes so the gateway forces a fresh checkpoint. +`lobu apply` type-checks and ships only the listed connectors (there is no `./connectors` auto-discovery). Update the `version` field whenever the event shape changes so the gateway forces a fresh checkpoint. ## Dependencies @@ -354,8 +364,8 @@ The rule of thumb: **npm is bundled (compile-time), native is nix (run-time).** ## See it in production -- [`examples/ecommerce/connectors/stripe-charges.connector.ts`](https://github.com/lobu-ai/lobu/blob/main/examples/ecommerce/connectors/stripe-charges.connector.ts) — REST API, `env_keys` auth, timestamp checkpoint. -- [`examples/lobu-crm/connectors/funnel-form.connector.ts`](https://github.com/lobu-ai/lobu/blob/main/examples/lobu-crm/connectors/funnel-form.connector.ts) — small custom HTTP API, ID-set dedupe. +- [`examples/ecommerce/stripe-charges.connector.ts`](https://github.com/lobu-ai/lobu/blob/main/examples/ecommerce/stripe-charges.connector.ts) — REST API, `env_keys` auth, timestamp checkpoint. +- [`examples/lobu-crm/funnel-form.connector.ts`](https://github.com/lobu-ai/lobu/blob/main/examples/lobu-crm/funnel-form.connector.ts) — small custom HTTP API, ID-set dedupe. ## See also diff --git a/packages/landing/src/content/docs/getting-started/index.mdx b/packages/landing/src/content/docs/getting-started/index.mdx index c13ea1214..ebd9c615f 100644 --- a/packages/landing/src/content/docs/getting-started/index.mdx +++ b/packages/landing/src/content/docs/getting-started/index.mdx @@ -41,23 +41,25 @@ my-agent/ │ ├── IDENTITY.md # who the agent is │ ├── SOUL.md # instructions, rules, workflows │ ├── USER.md # per-user context and preferences -│ ├── skills/ # skills scoped to this agent only +│ ├── skills/ # SKILL.md files, referenced via skillFromFile │ └── evals/ │ └── promptfooconfig.yaml # test cases for agent quality (promptfoo) -├── connectors/ # custom *.connector.ts (optional) -├── data/ # local runtime data; memory seeds when enabled -└── skills/ # skills shared across all agents +├── my-feed.connector.ts # custom connector, referenced via connectorFromFile +├── my-watcher.reaction.ts # watcher reaction, referenced via reaction: +└── data/ # local runtime data; memory seeds when enabled ``` +Connectors, reaction scripts, and skill files are referenced explicitly from `lobu.config.ts` (`connectorFromFile` / `defineWatcher({ reaction })` / `skillFromFile`), so they can live anywhere — next to the config for small agents, or in folders as you grow. There is no directory auto-discovery. + The memory schema (entity types, relationship types, watchers) lives directly in `lobu.config.ts` via `defineEntityType` / `defineRelationshipType` / `defineWatcher`. | Path | Docs | |------|------| | `lobu.config.ts` | [lobu.config.ts reference](/reference/lobu-config/) | | `agents/*/IDENTITY.md`, `SOUL.md`, `USER.md` | [Agent Workspace](/guides/agent-prompts/) | -| `agents/*/skills/`, `skills/` | [SKILL.md reference](/reference/skill-md/), [Skills](/getting-started/skills/) | +| `*/SKILL.md` (via `skillFromFile`) | [SKILL.md reference](/reference/skill-md/), [Skills](/getting-started/skills/) | | `agents/*/evals/` | [Evaluations](/guides/evals/) | -| `connectors/` | [Connector SDK](/getting-started/connector-sdk/) | +| `*.connector.ts` (via `connectorFromFile`) | [Connector SDK](/getting-started/connector-sdk/) | | `.env` | [CLI reference](/reference/cli/) | ## Develop your agent diff --git a/packages/landing/src/content/docs/getting-started/reaction-sdk.md b/packages/landing/src/content/docs/getting-started/reaction-sdk.md index b6fbc7c9f..91d573fdc 100644 --- a/packages/landing/src/content/docs/getting-started/reaction-sdk.md +++ b/packages/landing/src/content/docs/getting-started/reaction-sdk.md @@ -159,7 +159,7 @@ If you don't want a reaction, omit the `reaction` field. The watcher's extractio ## See it in production -- [`examples/sales/models/reactions/account-health-monitor.reaction.ts`](https://github.com/lobu-ai/lobu/blob/main/examples/sales/models/reactions/account-health-monitor.reaction.ts) — filters worsening risk transitions out of a watcher's account-changes extraction and persists each one as a typed `health_change` event. +- [`examples/sales/account-health-monitor.reaction.ts`](https://github.com/lobu-ai/lobu/blob/main/examples/sales/account-health-monitor.reaction.ts) — filters worsening risk transitions out of a watcher's account-changes extraction and persists each one as a typed `health_change` event. ## See also diff --git a/packages/landing/src/content/docs/reference/cli.md b/packages/landing/src/content/docs/reference/cli.md index c217b17b8..1b374892f 100644 --- a/packages/landing/src/content/docs/reference/cli.md +++ b/packages/landing/src/content/docs/reference/cli.md @@ -33,9 +33,9 @@ Generates: - `lobu.config.ts`: the TypeScript project entrypoint (`defineConfig` from `@lobu/cli/config`) - `package.json` + `tsconfig.json`: declare `@lobu/cli` / `@lobu/connector-sdk` and give the editor type resolution - `.env` — local environment variables (API keys, optional external `DATABASE_URL`) -- `agents/{name}/` — `IDENTITY.md`, `SOUL.md`, `USER.md`, local skills, and evals -- `skills/` — shared local skills directory -- `connectors/`: custom `*.connector.ts` files +- `agents/{name}/` — `IDENTITY.md`, `SOUL.md`, `USER.md`, skill files, and evals +- `*.connector.ts` — custom connectors, referenced from `lobu.config.ts` via `connectorFromFile` +- `*.reaction.ts` — watcher reaction scripts, referenced via `defineWatcher({ reaction })` - `AGENTS.md`, `TESTING.md`, `README.md`, `.gitignore` Interactive prompts guide you through provider, platform, network access policy, gateway port, public URL, and memory configuration. Local runs use bundled PGlite by default; set `DATABASE_URL` when you want to use external Postgres with pgvector. diff --git a/packages/landing/src/content/docs/reference/lobu-config.md b/packages/landing/src/content/docs/reference/lobu-config.md index 4029e915b..23cd14635 100644 --- a/packages/landing/src/content/docs/reference/lobu-config.md +++ b/packages/landing/src/content/docs/reference/lobu-config.md @@ -185,6 +185,7 @@ The default export of `lobu.config.ts`. | `connections` | `Connection[]` | no | Connections (from `defineConnection`) | | `authProfiles` | `AuthProfile[]` | no | Auth profiles (from `defineAuthProfile`) | | `watchers` | `Watcher[]` | no | Watchers (from `defineWatcher`) | +| `connectors` | `ConnectorSource[]` | no | Local connector source files to compile + ship (from `connectorFromFile`). Explicit list, no `./connectors` auto-discovery | Connections, the memory schema, and watchers are declared at the project level (in `defineConfig`), not inside `defineAgent`. A watcher names its owning agent through its own `agent` field. diff --git a/packages/landing/src/content/docs/reference/reaction-sdk.md b/packages/landing/src/content/docs/reference/reaction-sdk.md index dad17d9ae..a19620f43 100644 --- a/packages/landing/src/content/docs/reference/reaction-sdk.md +++ b/packages/landing/src/content/docs/reference/reaction-sdk.md @@ -140,7 +140,7 @@ When you need to call a third-party API that an installed connector already auth ## Lifecycle 1. **Watcher window closes.** The watcher's prompt + `extraction_schema` runs against the events in the window; the extracted JSON is validated. -2. **Lobu looks for a paired reaction.** Filename match: a watcher with slug `account-health-monitor` pairs with `models/reactions/account-health-monitor.reaction.ts`. If no file exists, the run ends here. +2. **Lobu runs the watcher's reaction.** The watcher's `reaction` script (the `.ts` file referenced by `defineWatcher({ reaction: "./account-health-monitor.reaction.ts" })`) runs. If the watcher declares no `reaction`, the run ends here. 3. **Sandbox boots the reaction.** Isolated worker, network restricted by the agent's `WORKER_ALLOWED_DOMAINS`, stdout/stderr captured into the run record, hard timeout. 4. **Reaction runs.** Any `client.knowledge.save` calls append events; outbound `fetch` calls go through the worker HTTP proxy. 5. **Result lands.** Success or failure is recorded on the watcher run; partial side effects (events already saved before a throw) stay in place — they're real events in the durable log. diff --git a/packages/landing/src/generated/landing-snippets.json b/packages/landing/src/generated/landing-snippets.json index 2a4176fb2..bead74382 100644 --- a/packages/landing/src/generated/landing-snippets.json +++ b/packages/landing/src/generated/landing-snippets.json @@ -1,8 +1,8 @@ { "connector": { "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type ConnectorDefinition, type EventEnvelope, type SyncContext, type SyncResult } from \"@lobu/connector-sdk\";\n\ninterface StripeCharge { id: string; amount: number; currency: string; created: number; refunded: boolean }\ninterface Checkpoint { last_created: number }\n\nexport default class StripeChargesConnector extends ConnectorRuntime {\n readonly definition: ConnectorDefinition = {\n key: \"stripe-charges\",\n name: \"Stripe charges\",\n version: \"1.0.0\",\n // Stripe secret key collected per connection; exposed to sync() as ctx.config.secret_key.\n authSchema: { methods: [{ type: \"env_keys\", fields: [{ key: \"secret_key\", label: \"Stripe secret key\", secret: true, required: true }] }] },\n feeds: { charges: { key: \"charges\", name: \"Charges\" } },\n };\n\n async sync(ctx: SyncContext): Promise {\n const cursor = ((ctx.checkpoint ?? {}) as Partial).last_created ?? 0;\n const secretKey = String(ctx.config.secret_key ?? \"\");\n const r = await fetch(`https://api.stripe.com/v1/charges?limit=100&created[gt]=${cursor}`, {\n headers: { Authorization: `Bearer ${secretKey}` },\n });\n if (!r.ok) throw new Error(`Stripe ${r.status}: ${await r.text()}`);\n const data = (((await r.json()) as { data?: StripeCharge[] }).data ?? []).sort((a, b) => a.created - b.created);\n const events: EventEnvelope[] = data.map((c) => ({\n origin_id: c.refunded ? `${c.id}:refund` : c.id,\n origin_type: c.refunded ? \"charge_refunded\" : \"charge_succeeded\",\n title: `${c.refunded ? \"Refund\" : \"Charge\"} — ${(c.amount / 100).toFixed(2)} ${c.currency.toUpperCase()}`,\n payload_text: `${c.refunded ? \"Refund\" : \"Charge\"} of ${(c.amount / 100).toFixed(2)} ${c.currency.toUpperCase()} (stripe id ${c.id})`,\n source_url: `https://dashboard.stripe.com/payments/${c.id}`,\n occurred_at: new Date(c.created * 1000),\n }));\n return { events, checkpoint: { last_created: data.at(-1)?.created ?? cursor } satisfies Checkpoint };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", - "path": "connectors/stripe-charges.connector.ts", - "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/ecommerce/connectors/stripe-charges.connector.ts", + "path": "stripe-charges.connector.ts", + "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/ecommerce/stripe-charges.connector.ts", "language": "typescript" }, "memorySchema": { @@ -12,19 +12,19 @@ "language": "typescript" }, "watcher": { - "code": "const accountHealthMonitor = defineWatcher({\n agent: sales,\n slug: \"account-health-monitor\",\n name: \"Account health monitor\",\n schedule: \"0 */12 * * *\",\n notification: { priority: \"high\", channel: \"both\" },\n tags: [\"sales\", \"health\", \"renewals\"],\n minCooldownSeconds: 1800,\n reaction: \"./models/reactions/account-health-monitor.reaction.ts\",\n prompt:\n \"Poll CRM data for tracked accounts. Track expansion progress, risk level changes, and renewal timeline.\\n\",\n extractionSchema: {\n type: \"object\",\n required: [\n \"risk_level\",\n \"expansion_status\",\n \"renewal_blockers\",\n \"activity_delta\",\n ],\n properties: {\n risk_level: { type: \"string\" },\n expansion_status: { type: \"string\" },\n renewal_blockers: { type: \"array\", items: { type: \"string\" } },\n activity_delta: { type: \"string\" },\n },\n },\n});", + "code": "const accountHealthMonitor = defineWatcher({\n agent: sales,\n slug: \"account-health-monitor\",\n name: \"Account health monitor\",\n schedule: \"0 */12 * * *\",\n notification: { priority: \"high\", channel: \"both\" },\n tags: [\"sales\", \"health\", \"renewals\"],\n minCooldownSeconds: 1800,\n reaction: \"./account-health-monitor.reaction.ts\",\n prompt:\n \"Poll CRM data for tracked accounts. Track expansion progress, risk level changes, and renewal timeline.\\n\",\n extractionSchema: {\n type: \"object\",\n required: [\n \"risk_level\",\n \"expansion_status\",\n \"renewal_blockers\",\n \"activity_delta\",\n ],\n properties: {\n risk_level: { type: \"string\" },\n expansion_status: { type: \"string\" },\n renewal_blockers: { type: \"array\", items: { type: \"string\" } },\n activity_delta: { type: \"string\" },\n },\n },\n});", "path": "lobu.config.ts", "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/sales/lobu.config.ts", "language": "typescript" }, "reaction": { - "code": "/**\n * Reaction for the `reconciliation-monitor` watcher.\n *\n * Persists variance events when unreconciled transactions or new anomalies\n * are detected during the daily reconciliation pass.\n */\nimport type { ReactionClient, ReactionContext } from \"@lobu/connector-sdk\";\n\ninterface ReconciliationData {\n unreconciled_count: number;\n new_variances: string[];\n approaching_deadlines: string[];\n payment_risks?: string[];\n}\n\nexport default async (\n ctx: ReactionContext,\n client: ReactionClient\n): Promise => {\n const data = ctx.extracted_data as ReconciliationData;\n\n const hasIssues =\n data.unreconciled_count > 0 ||\n (data.new_variances?.length ?? 0) > 0 ||\n (data.approaching_deadlines?.length ?? 0) > 0;\n\n if (!hasIssues) return;\n\n const parts: string[] = [];\n if (data.unreconciled_count > 0) {\n parts.push(`${data.unreconciled_count} unreconciled transactions`);\n }\n if (data.new_variances?.length) {\n parts.push(`Variances: ${data.new_variances.join(\"; \")}`);\n }\n if (data.approaching_deadlines?.length) {\n parts.push(`Deadlines: ${data.approaching_deadlines.join(\"; \")}`);\n }\n\n await client.knowledge.save({\n entity_ids: ctx.entities.map((e) => e.id),\n content: parts.join(\"\\n\"),\n semantic_type: \"reconciliation_alert\",\n metadata: {\n window_id: ctx.window.id,\n unreconciled_count: data.unreconciled_count,\n variance_count: data.new_variances?.length ?? 0,\n },\n });\n};", - "path": "models/reactions/reconciliation-monitor.reaction.ts", - "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/finance/models/reactions/reconciliation-monitor.reaction.ts", + "code": "/**\n * Reaction for the `reconciliation-monitor` watcher.\n *\n * Persists variance events when unreconciled transactions or new anomalies\n * are detected during the daily reconciliation pass.\n */\nimport type { ReactionClient, ReactionContext } from \"@lobu/connector-sdk\";\n\ninterface ReconciliationData {\n unreconciled_count: number;\n new_variances: string[];\n approaching_deadlines: string[];\n payment_risks?: string[];\n}\n\nexport default async (\n ctx: ReactionContext,\n client: ReactionClient\n): Promise => {\n const data = ctx.extracted_data as unknown as ReconciliationData;\n\n const hasIssues =\n data.unreconciled_count > 0 ||\n (data.new_variances?.length ?? 0) > 0 ||\n (data.approaching_deadlines?.length ?? 0) > 0;\n\n if (!hasIssues) return;\n\n const parts: string[] = [];\n if (data.unreconciled_count > 0) {\n parts.push(`${data.unreconciled_count} unreconciled transactions`);\n }\n if (data.new_variances?.length) {\n parts.push(`Variances: ${data.new_variances.join(\"; \")}`);\n }\n if (data.approaching_deadlines?.length) {\n parts.push(`Deadlines: ${data.approaching_deadlines.join(\"; \")}`);\n }\n\n await client.knowledge.save({\n entity_ids: ctx.entities.map((e) => e.id),\n content: parts.join(\"\\n\"),\n semantic_type: \"reconciliation_alert\",\n metadata: {\n window_id: ctx.window.id,\n unreconciled_count: data.unreconciled_count,\n variance_count: data.new_variances?.length ?? 0,\n },\n });\n};", + "path": "reconciliation-monitor.reaction.ts", + "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/finance/reconciliation-monitor.reaction.ts", "language": "typescript" }, "agentConfig": { - "code": "import {\n defineAgent,\n defineConfig,\n defineEntityType,\n defineRelationshipType,\n defineWatcher,\n secret,\n} from \"@lobu/cli/config\";\n\nconst sales = defineAgent({\n id: \"sales\",\n name: \"sales\",\n description:\n \"Help revenue teams track account health, rollout progress, and renewal signals\",\n dir: \"./agents/sales\",\n providers: [\n {\n id: \"anthropic\",\n model: \"claude/sonnet-4-5\",\n key: secret(\"ANTHROPIC_API_KEY\"),\n },\n ],\n network: {\n allowed: [\n \"github.com\",\n \".github.com\",\n \".githubusercontent.com\",\n \"registry.npmjs.org\",\n \".npmjs.org\",\n ],\n },\n});", + "code": "import {\n connectorFromFile,\n defineAgent,\n defineConfig,\n defineEntityType,\n defineRelationshipType,\n defineWatcher,\n secret,\n} from \"@lobu/cli/config\";\n\nconst sales = defineAgent({\n id: \"sales\",\n name: \"sales\",\n description:\n \"Help revenue teams track account health, rollout progress, and renewal signals\",\n dir: \"./agents/sales\",\n providers: [\n {\n id: \"anthropic\",\n model: \"claude/sonnet-4-5\",\n key: secret(\"ANTHROPIC_API_KEY\"),\n },\n ],\n network: {\n allowed: [\n \"github.com\",\n \".github.com\",\n \".githubusercontent.com\",\n \"registry.npmjs.org\",\n \".npmjs.org\",\n ],\n },\n});", "path": "lobu.config.ts", "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/sales/lobu.config.ts", "language": "typescript" @@ -112,9 +112,9 @@ "useCases": { "legal": { "connector": { - "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class DocuSignEnvelopesConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"docusign-envelopes\",\n name: \"DocuSign envelopes\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"oauth\" as const, provider: \"docusign\" }] },\n feeds: { envelopes: { key: \"envelopes\", name: \"Envelope status changes\" } },\n };\n\n async sync(ctx: SyncContext) {\n const since = (ctx.checkpoint as any)?.last_status_changed ?? \"2000-01-01T00:00:00Z\";\n const base = String(ctx.config.base_path ?? \"https://www.docusign.net/restapi\").replace(/\\/$/, \"\");\n const r = await fetch(`${base}/v2.1/accounts/${ctx.config.account_id}/envelopes?from_date=${encodeURIComponent(since)}&count=100`);\n const envelopes: any[] = ((await r.json() as any).envelopes ?? []).sort((a: any, b: any) => new Date(a.statusChangedDateTime).getTime() - new Date(b.statusChangedDateTime).getTime());\n return {\n events: envelopes.map((e) => ({\n origin_id: `${e.envelopeId}:${e.status}`,\n origin_type: \"envelope_status_changed\",\n title: `${e.emailSubject ?? e.envelopeId} → ${e.status}`,\n occurred_at: new Date(e.statusChangedDateTime),\n })),\n checkpoint: { last_status_changed: envelopes.at(-1)?.statusChangedDateTime ?? since },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", - "path": "connectors/docusign-envelopes.connector.ts", - "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/legal/connectors/docusign-envelopes.connector.ts", + "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class DocuSignEnvelopesConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"docusign-envelopes\",\n name: \"DocuSign envelopes\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"oauth\" as const, provider: \"docusign\", requiredScopes: [\"signature\"] }] },\n feeds: { envelopes: { key: \"envelopes\", name: \"Envelope status changes\" } },\n };\n\n async sync(ctx: SyncContext) {\n const since = (ctx.checkpoint as any)?.last_status_changed ?? \"2000-01-01T00:00:00Z\";\n const base = String(ctx.config.base_path ?? \"https://www.docusign.net/restapi\").replace(/\\/$/, \"\");\n const r = await fetch(`${base}/v2.1/accounts/${ctx.config.account_id}/envelopes?from_date=${encodeURIComponent(since)}&count=100`);\n const envelopes: any[] = ((await r.json() as any).envelopes ?? []).sort((a: any, b: any) => new Date(a.statusChangedDateTime).getTime() - new Date(b.statusChangedDateTime).getTime());\n return {\n events: envelopes.map((e) => ({\n origin_id: `${e.envelopeId}:${e.status}`,\n origin_type: \"envelope_status_changed\",\n title: `${e.emailSubject ?? e.envelopeId} → ${e.status}`,\n payload_text: `Envelope ${e.emailSubject ?? e.envelopeId} is now ${e.status}`,\n occurred_at: new Date(e.statusChangedDateTime),\n })),\n checkpoint: { last_status_changed: envelopes.at(-1)?.statusChangedDateTime ?? since },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", + "path": "docusign-envelopes.connector.ts", + "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/legal/docusign-envelopes.connector.ts", "language": "typescript" }, "memorySchema": { @@ -132,9 +132,9 @@ }, "finance": { "connector": { - "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class QuickBooksTransactionsConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"quickbooks-transactions\",\n name: \"QuickBooks transactions\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"oauth\" as const, provider: \"intuit\" }] },\n feeds: { transactions: { key: \"transactions\", name: \"Posted transactions\" } },\n };\n\n async sync(ctx: SyncContext) {\n const since = (ctx.checkpoint as any)?.last_txn_date ?? \"1970-01-01\";\n const q = `SELECT * FROM Transaction WHERE TxnDate > '${since}' ORDERBY TxnDate ASC MAXRESULTS 500`;\n const r = await fetch(`https://quickbooks.api.intuit.com/v3/company/${ctx.config.realm_id}/query?query=${encodeURIComponent(q)}`);\n const txns: any[] = (await r.json() as any).QueryResponse?.Transaction ?? [];\n return {\n events: txns.map((t) => ({\n origin_id: t.Id,\n origin_type: \"transaction_posted\",\n title: `${t.AccountRef?.name ?? \"Bank\"} — $${t.Amount.toFixed(2)}`,\n occurred_at: new Date(`${t.TxnDate}T00:00:00Z`),\n })),\n checkpoint: { last_txn_date: txns.at(-1)?.TxnDate ?? since },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", - "path": "connectors/quickbooks-transactions.connector.ts", - "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/finance/connectors/quickbooks-transactions.connector.ts", + "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class QuickBooksTransactionsConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"quickbooks-transactions\",\n name: \"QuickBooks transactions\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"oauth\" as const, provider: \"intuit\", requiredScopes: [\"com.intuit.quickbooks.accounting\"] }] },\n feeds: { transactions: { key: \"transactions\", name: \"Posted transactions\" } },\n };\n\n async sync(ctx: SyncContext) {\n const since = (ctx.checkpoint as any)?.last_txn_date ?? \"1970-01-01\";\n const q = `SELECT * FROM Transaction WHERE TxnDate > '${since}' ORDERBY TxnDate ASC MAXRESULTS 500`;\n const r = await fetch(`https://quickbooks.api.intuit.com/v3/company/${ctx.config.realm_id}/query?query=${encodeURIComponent(q)}`);\n const txns: any[] = (await r.json() as any).QueryResponse?.Transaction ?? [];\n return {\n events: txns.map((t) => ({\n origin_id: t.Id,\n origin_type: \"transaction_posted\",\n title: `${t.AccountRef?.name ?? \"Bank\"} — $${t.Amount.toFixed(2)}`,\n payload_text: `${t.AccountRef?.name ?? \"Bank\"} transaction for $${t.Amount.toFixed(2)} on ${t.TxnDate}`,\n occurred_at: new Date(`${t.TxnDate}T00:00:00Z`),\n })),\n checkpoint: { last_txn_date: txns.at(-1)?.TxnDate ?? since },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", + "path": "quickbooks-transactions.connector.ts", + "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/finance/quickbooks-transactions.connector.ts", "language": "typescript" }, "memorySchema": { @@ -144,7 +144,7 @@ "language": "typescript" }, "watcher": { - "code": "const reconciliationMonitor = defineWatcher({\n agent: finance,\n slug: \"reconciliation-monitor\",\n name: \"Reconciliation monitor\",\n schedule: \"0 6 * * 1-5\",\n notification: { priority: \"high\", channel: \"both\" },\n tags: [\"finance\", \"reconciliation\", \"daily\"],\n minCooldownSeconds: 3600,\n reaction: \"./models/reactions/reconciliation-monitor.reaction.ts\",\n prompt:\n \"Check accounts for unreconciled transactions, new variances, and approaching reporting deadlines. Lead with exceptions that need review.\\n\",\n extractionSchema: {\n type: \"object\",\n required: [\"unreconciled_count\", \"new_variances\", \"approaching_deadlines\"],\n properties: {\n unreconciled_count: { type: \"integer\" },\n new_variances: { type: \"array\", items: { type: \"string\" } },\n approaching_deadlines: { type: \"array\", items: { type: \"string\" } },\n payment_risks: { type: \"array\", items: { type: \"string\" } },\n },\n },\n});", + "code": "const reconciliationMonitor = defineWatcher({\n agent: finance,\n slug: \"reconciliation-monitor\",\n name: \"Reconciliation monitor\",\n schedule: \"0 6 * * 1-5\",\n notification: { priority: \"high\", channel: \"both\" },\n tags: [\"finance\", \"reconciliation\", \"daily\"],\n minCooldownSeconds: 3600,\n reaction: \"./reconciliation-monitor.reaction.ts\",\n prompt:\n \"Check accounts for unreconciled transactions, new variances, and approaching reporting deadlines. Lead with exceptions that need review.\\n\",\n extractionSchema: {\n type: \"object\",\n required: [\"unreconciled_count\", \"new_variances\", \"approaching_deadlines\"],\n properties: {\n unreconciled_count: { type: \"integer\" },\n new_variances: { type: \"array\", items: { type: \"string\" } },\n approaching_deadlines: { type: \"array\", items: { type: \"string\" } },\n payment_risks: { type: \"array\", items: { type: \"string\" } },\n },\n },\n});", "path": "lobu.config.ts", "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/finance/lobu.config.ts", "language": "typescript" @@ -152,9 +152,9 @@ }, "sales": { "connector": { - "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class SalesforcePipelineConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"salesforce-pipeline\",\n name: \"Salesforce pipeline\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"oauth\" as const, provider: \"salesforce\" }] },\n feeds: { opportunities: { key: \"opportunities\", name: \"Opportunities\" } },\n };\n\n async sync(ctx: SyncContext) {\n const since = (ctx.checkpoint as any)?.last_modified ?? \"2000-01-01T00:00:00Z\";\n const q = `SELECT Id,Name,StageName,LastModifiedDate FROM Opportunity WHERE LastModifiedDate > ${since} LIMIT 200`;\n const r = await fetch(`${ctx.config.instance_url}/services/data/v60.0/query?q=${encodeURIComponent(q)}`);\n const records: any[] = (await r.json() as any).records ?? [];\n return {\n events: records.map((o) => ({\n origin_id: o.Id,\n origin_type: \"opportunity_updated\",\n title: `${o.Name} → ${o.StageName}`,\n occurred_at: new Date(o.LastModifiedDate),\n })),\n checkpoint: { last_modified: records.at(-1)?.LastModifiedDate ?? since },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", - "path": "connectors/salesforce-pipeline.connector.ts", - "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/sales/connectors/salesforce-pipeline.connector.ts", + "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class SalesforcePipelineConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"salesforce-pipeline\",\n name: \"Salesforce pipeline\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"oauth\" as const, provider: \"salesforce\", requiredScopes: [\"api\", \"refresh_token\"] }] },\n feeds: { opportunities: { key: \"opportunities\", name: \"Opportunities\" } },\n };\n\n async sync(ctx: SyncContext) {\n const since = (ctx.checkpoint as any)?.last_modified ?? \"2000-01-01T00:00:00Z\";\n const q = `SELECT Id,Name,StageName,LastModifiedDate FROM Opportunity WHERE LastModifiedDate > ${since} LIMIT 200`;\n const r = await fetch(`${ctx.config.instance_url}/services/data/v60.0/query?q=${encodeURIComponent(q)}`);\n const records: any[] = (await r.json() as any).records ?? [];\n return {\n events: records.map((o) => ({\n origin_id: o.Id,\n origin_type: \"opportunity_updated\",\n title: `${o.Name} → ${o.StageName}`,\n payload_text: `${o.Name} moved to ${o.StageName}`,\n occurred_at: new Date(o.LastModifiedDate),\n })),\n checkpoint: { last_modified: records.at(-1)?.LastModifiedDate ?? since },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", + "path": "salesforce-pipeline.connector.ts", + "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/sales/salesforce-pipeline.connector.ts", "language": "typescript" }, "memorySchema": { @@ -164,7 +164,7 @@ "language": "typescript" }, "watcher": { - "code": "const accountHealthMonitor = defineWatcher({\n agent: sales,\n slug: \"account-health-monitor\",\n name: \"Account health monitor\",\n schedule: \"0 */12 * * *\",\n notification: { priority: \"high\", channel: \"both\" },\n tags: [\"sales\", \"health\", \"renewals\"],\n minCooldownSeconds: 1800,\n reaction: \"./models/reactions/account-health-monitor.reaction.ts\",\n prompt:\n \"Poll CRM data for tracked accounts. Track expansion progress, risk level changes, and renewal timeline.\\n\",\n extractionSchema: {\n type: \"object\",\n required: [\n \"risk_level\",\n \"expansion_status\",\n \"renewal_blockers\",\n \"activity_delta\",\n ],\n properties: {\n risk_level: { type: \"string\" },\n expansion_status: { type: \"string\" },\n renewal_blockers: { type: \"array\", items: { type: \"string\" } },\n activity_delta: { type: \"string\" },\n },\n },\n});", + "code": "const accountHealthMonitor = defineWatcher({\n agent: sales,\n slug: \"account-health-monitor\",\n name: \"Account health monitor\",\n schedule: \"0 */12 * * *\",\n notification: { priority: \"high\", channel: \"both\" },\n tags: [\"sales\", \"health\", \"renewals\"],\n minCooldownSeconds: 1800,\n reaction: \"./account-health-monitor.reaction.ts\",\n prompt:\n \"Poll CRM data for tracked accounts. Track expansion progress, risk level changes, and renewal timeline.\\n\",\n extractionSchema: {\n type: \"object\",\n required: [\n \"risk_level\",\n \"expansion_status\",\n \"renewal_blockers\",\n \"activity_delta\",\n ],\n properties: {\n risk_level: { type: \"string\" },\n expansion_status: { type: \"string\" },\n renewal_blockers: { type: \"array\", items: { type: \"string\" } },\n activity_delta: { type: \"string\" },\n },\n },\n});", "path": "lobu.config.ts", "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/sales/lobu.config.ts", "language": "typescript" @@ -172,9 +172,9 @@ }, "delivery": { "connector": { - "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class ShopifyOrdersConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"shopify-orders\",\n name: \"Shopify orders\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"env\" as const, fields: [{ name: \"access_token\" }] }] },\n feeds: { orders: { key: \"orders\", name: \"Order updates\" } },\n };\n\n async sync(ctx: SyncContext) {\n const since = (ctx.checkpoint as any)?.updated_at_min ?? \"2000-01-01T00:00:00Z\";\n const r = await fetch(`https://${ctx.config.shop}/admin/api/2024-10/orders.json?status=any&updated_at_min=${encodeURIComponent(since)}&limit=100`);\n const orders: any[] = ((await r.json() as any).orders ?? []).sort((a: any, b: any) => new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime());\n return {\n events: orders.map((o) => ({\n origin_id: `${o.id}:${o.updated_at}`,\n origin_type: \"order_updated\",\n title: `Order ${o.name} — ${o.fulfillment_status ?? \"unfulfilled\"}`,\n source_url: `https://${ctx.config.shop}/admin/orders/${o.id}`,\n occurred_at: new Date(o.updated_at),\n })),\n checkpoint: { updated_at_min: orders.at(-1)?.updated_at ?? since },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", - "path": "connectors/shopify-orders.connector.ts", - "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/delivery/connectors/shopify-orders.connector.ts", + "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class ShopifyOrdersConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"shopify-orders\",\n name: \"Shopify orders\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"env_keys\" as const, fields: [{ key: \"access_token\", secret: true }] }] },\n feeds: { orders: { key: \"orders\", name: \"Order updates\" } },\n };\n\n async sync(ctx: SyncContext) {\n const since = (ctx.checkpoint as any)?.updated_at_min ?? \"2000-01-01T00:00:00Z\";\n const r = await fetch(`https://${ctx.config.shop}/admin/api/2024-10/orders.json?status=any&updated_at_min=${encodeURIComponent(since)}&limit=100`);\n const orders: any[] = ((await r.json() as any).orders ?? []).sort((a: any, b: any) => new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime());\n return {\n events: orders.map((o) => ({\n origin_id: `${o.id}:${o.updated_at}`,\n origin_type: \"order_updated\",\n title: `Order ${o.name} — ${o.fulfillment_status ?? \"unfulfilled\"}`,\n payload_text: `Order ${o.name} is ${o.fulfillment_status ?? \"unfulfilled\"}`,\n source_url: `https://${ctx.config.shop}/admin/orders/${o.id}`,\n occurred_at: new Date(o.updated_at),\n })),\n checkpoint: { updated_at_min: orders.at(-1)?.updated_at ?? since },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", + "path": "shopify-orders.connector.ts", + "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/delivery/shopify-orders.connector.ts", "language": "typescript" }, "memorySchema": { @@ -192,9 +192,9 @@ }, "market": { "connector": { - "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class ExaNewsFeedConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"exa-news-feed\",\n name: \"Exa news feed\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"env\" as const, fields: [{ name: \"api_key\" }] }] },\n feeds: { articles: { key: \"articles\", name: \"Articles\" } },\n };\n\n async sync(ctx: SyncContext) {\n const seen = new Set((ctx.checkpoint as any)?.seen_ids ?? []);\n const r = await fetch(\"https://api.exa.ai/search\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify({ query: ctx.config.query, numResults: ctx.config.num_results ?? 20 }) });\n const fresh: any[] = ((await r.json() as any).results ?? []).filter((x: any) => x.id && !seen.has(x.id));\n return {\n events: fresh.map((x) => ({\n origin_id: x.id,\n origin_type: \"article_published\",\n title: x.title ?? x.url,\n author_name: x.author,\n source_url: x.url,\n occurred_at: x.publishedDate ? new Date(x.publishedDate) : new Date(),\n })),\n checkpoint: { seen_ids: [...seen, ...fresh.map((x) => x.id)].slice(-1000) },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", - "path": "connectors/exa-news-feed.connector.ts", - "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/market/connectors/exa-news-feed.connector.ts", + "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class ExaNewsFeedConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"exa-news-feed\",\n name: \"Exa news feed\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"env_keys\" as const, fields: [{ key: \"api_key\", secret: true }] }] },\n feeds: { articles: { key: \"articles\", name: \"Articles\" } },\n };\n\n async sync(ctx: SyncContext) {\n const seen = new Set((ctx.checkpoint as any)?.seen_ids ?? []);\n const r = await fetch(\"https://api.exa.ai/search\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify({ query: ctx.config.query, numResults: ctx.config.num_results ?? 20 }) });\n const fresh: any[] = ((await r.json() as any).results ?? []).filter((x: any) => x.id && !seen.has(x.id));\n return {\n events: fresh.map((x) => ({\n origin_id: x.id,\n origin_type: \"article_published\",\n title: x.title ?? x.url,\n payload_text: x.text ?? x.title ?? x.url,\n author_name: x.author,\n source_url: x.url,\n occurred_at: x.publishedDate ? new Date(x.publishedDate) : new Date(),\n })),\n checkpoint: { seen_ids: [...seen, ...fresh.map((x) => x.id)].slice(-1000) },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", + "path": "exa-news-feed.connector.ts", + "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/market/exa-news-feed.connector.ts", "language": "typescript" }, "memorySchema": { @@ -204,7 +204,7 @@ "language": "typescript" }, "watcher": { - "code": "const founderActivityTracker = defineWatcher({\n agent: vcTracking,\n slug: \"founder-activity-tracker\",\n name: \"Founder Activity Tracker\",\n schedule: \"0 10 * * *\",\n notification: { priority: \"normal\" },\n tags: [\"vc\", \"founders\", \"daily\"],\n minCooldownSeconds: 600,\n reaction: \"./models/reactions/founder-activity-tracker.reaction.ts\",\n prompt:\n \"You are a venture capital analyst tracking the public activity of startup founders in your portfolio.\\n\\n## Founders\\n{{#each entities}}\\n- {{name}} ({{entity_type}}, ID: {{id}})\\n{{/each}}\\n\\n## Recent Founder Activity\\n{{#if sources.founder_posts}}\\n{{sources.founder_posts}}\\n{{/if}}\\n\\n---\\n\\nProduce a structured founder activity report:\\n1. **Executive Summary**: 2-3 sentence overview of founder activity and signals.\\n2. **Per-Founder Analysis**: For each active founder, summarize their messaging themes, engagement level, and signals about company direction.\\n3. **Cross-Portfolio Patterns**: Themes multiple founders discuss.\\n4. **Notable Signals**: Flag potential announcements, strategic shifts, or concerns.\\n\\nBe specific and cite actual tweets/posts as evidence.\\n\",\n sources: {\n founder_posts:\n \"SELECT id, title, payload_text, author_name, source_url, occurred_at, score, origin_type, connector_key FROM events WHERE connector_key IN ('x') AND origin_type IN ('tweet', 'reply') ORDER BY occurred_at DESC LIMIT 300\\n\",\n },\n reactionsGuidance:\n \"When a founder signals hiring activity, fundraising, or pivots, flag for the investment team.\\nTrack founders going quiet as a potential concern.\\nAlert on any public statements about competitors or market conditions.\\n\",\n extractionSchema: {\n type: \"object\",\n required: [\"summary\", \"founders\", \"notable_signals\"],\n properties: {\n summary: { type: \"string\" },\n founders: {\n type: \"array\",\n items: {\n type: \"object\",\n required: [\"name\", \"company\", \"activity_level\", \"themes\"],\n properties: {\n name: { type: \"string\" },\n company: { type: \"string\" },\n activity_level: {\n type: \"string\",\n enum: [\"high\", \"medium\", \"low\", \"inactive\"],\n },\n themes: { type: \"array\", items: { type: \"string\" } },\n sentiment: {\n type: \"string\",\n enum: [\"bullish\", \"neutral\", \"cautious\", \"concerned\"],\n },\n signals: { type: \"array\", items: { type: \"string\" } },\n notable_posts: { type: \"array\", items: { type: \"string\" } },\n },\n },\n },\n cross_patterns: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n theme: { type: \"string\" },\n founders_involved: { type: \"array\", items: { type: \"string\" } },\n },\n },\n },\n notable_signals: {\n type: \"array\",\n items: {\n type: \"object\",\n required: [\"signal\", \"founder\", \"impact\"],\n properties: {\n signal: { type: \"string\" },\n founder: { type: \"string\" },\n impact: { type: \"string\", enum: [\"high\", \"medium\", \"low\"] },\n },\n },\n },\n },\n },\n});", + "code": "const founderActivityTracker = defineWatcher({\n agent: vcTracking,\n slug: \"founder-activity-tracker\",\n name: \"Founder Activity Tracker\",\n schedule: \"0 10 * * *\",\n notification: { priority: \"normal\" },\n tags: [\"vc\", \"founders\", \"daily\"],\n minCooldownSeconds: 600,\n reaction: \"./founder-activity-tracker.reaction.ts\",\n prompt:\n \"You are a venture capital analyst tracking the public activity of startup founders in your portfolio.\\n\\n## Founders\\n{{#each entities}}\\n- {{name}} ({{entity_type}}, ID: {{id}})\\n{{/each}}\\n\\n## Recent Founder Activity\\n{{#if sources.founder_posts}}\\n{{sources.founder_posts}}\\n{{/if}}\\n\\n---\\n\\nProduce a structured founder activity report:\\n1. **Executive Summary**: 2-3 sentence overview of founder activity and signals.\\n2. **Per-Founder Analysis**: For each active founder, summarize their messaging themes, engagement level, and signals about company direction.\\n3. **Cross-Portfolio Patterns**: Themes multiple founders discuss.\\n4. **Notable Signals**: Flag potential announcements, strategic shifts, or concerns.\\n\\nBe specific and cite actual tweets/posts as evidence.\\n\",\n sources: {\n founder_posts:\n \"SELECT id, title, payload_text, author_name, source_url, occurred_at, score, origin_type, connector_key FROM events WHERE connector_key IN ('x') AND origin_type IN ('tweet', 'reply') ORDER BY occurred_at DESC LIMIT 300\\n\",\n },\n reactionsGuidance:\n \"When a founder signals hiring activity, fundraising, or pivots, flag for the investment team.\\nTrack founders going quiet as a potential concern.\\nAlert on any public statements about competitors or market conditions.\\n\",\n extractionSchema: {\n type: \"object\",\n required: [\"summary\", \"founders\", \"notable_signals\"],\n properties: {\n summary: { type: \"string\" },\n founders: {\n type: \"array\",\n items: {\n type: \"object\",\n required: [\"name\", \"company\", \"activity_level\", \"themes\"],\n properties: {\n name: { type: \"string\" },\n company: { type: \"string\" },\n activity_level: {\n type: \"string\",\n enum: [\"high\", \"medium\", \"low\", \"inactive\"],\n },\n themes: { type: \"array\", items: { type: \"string\" } },\n sentiment: {\n type: \"string\",\n enum: [\"bullish\", \"neutral\", \"cautious\", \"concerned\"],\n },\n signals: { type: \"array\", items: { type: \"string\" } },\n notable_posts: { type: \"array\", items: { type: \"string\" } },\n },\n },\n },\n cross_patterns: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n theme: { type: \"string\" },\n founders_involved: { type: \"array\", items: { type: \"string\" } },\n },\n },\n },\n notable_signals: {\n type: \"array\",\n items: {\n type: \"object\",\n required: [\"signal\", \"founder\", \"impact\"],\n properties: {\n signal: { type: \"string\" },\n founder: { type: \"string\" },\n impact: { type: \"string\", enum: [\"high\", \"medium\", \"low\"] },\n },\n },\n },\n },\n },\n});", "path": "lobu.config.ts", "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/market/lobu.config.ts", "language": "typescript" @@ -212,9 +212,9 @@ }, "agent-community": { "connector": { - "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class DiscoursePostsConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"discourse-posts\",\n name: \"Discourse posts\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"env\" as const, fields: [{ name: \"api_key\" }] }] },\n feeds: { posts: { key: \"posts\", name: \"Forum posts\" } },\n };\n\n async sync(ctx: SyncContext) {\n const cursor = (ctx.checkpoint as any)?.last_post_id ?? 0;\n const r = await fetch(`${ctx.config.base_url}/posts.json?before=${cursor + 50}`);\n const posts: any[] = ((await r.json() as any).latest_posts ?? []).filter((p: any) => p.id > cursor).sort((a: any, b: any) => a.id - b.id);\n return {\n events: posts.map((p) => ({\n origin_id: String(p.id),\n origin_type: \"post_created\",\n title: p.topic_title ?? `Post by ${p.username}`,\n author_name: p.username,\n source_url: `${ctx.config.base_url}/t/${p.topic_slug}/${p.topic_id}/${p.id}`,\n occurred_at: new Date(p.created_at),\n })),\n checkpoint: { last_post_id: posts.at(-1)?.id ?? cursor },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", - "path": "connectors/discourse-posts.connector.ts", - "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/agent-community/connectors/discourse-posts.connector.ts", + "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nexport default class DiscoursePostsConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"discourse-posts\",\n name: \"Discourse posts\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"env_keys\" as const, fields: [{ key: \"api_key\", secret: true }] }] },\n feeds: { posts: { key: \"posts\", name: \"Forum posts\" } },\n };\n\n async sync(ctx: SyncContext) {\n const cursor = (ctx.checkpoint as any)?.last_post_id ?? 0;\n const r = await fetch(`${ctx.config.base_url}/posts.json?before=${cursor + 50}`);\n const posts: any[] = ((await r.json() as any).latest_posts ?? []).filter((p: any) => p.id > cursor).sort((a: any, b: any) => a.id - b.id);\n return {\n events: posts.map((p) => ({\n origin_id: String(p.id),\n origin_type: \"post_created\",\n title: p.topic_title ?? `Post by ${p.username}`,\n payload_text: p.raw ?? p.cooked ?? `Post by ${p.username}`,\n author_name: p.username,\n source_url: `${ctx.config.base_url}/t/${p.topic_slug}/${p.topic_id}/${p.id}`,\n occurred_at: new Date(p.created_at),\n })),\n checkpoint: { last_post_id: posts.at(-1)?.id ?? cursor },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", + "path": "discourse-posts.connector.ts", + "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/agent-community/discourse-posts.connector.ts", "language": "typescript" }, "memorySchema": { @@ -224,7 +224,7 @@ "language": "typescript" }, "watcher": { - "code": "const opportunityMatcher = defineWatcher({\n agent: agentCommunity,\n slug: \"opportunity-matcher\",\n name: \"Opportunity matcher\",\n schedule: \"0 */12 * * *\",\n notification: { priority: \"normal\" },\n tags: [\"community\", \"matching\"],\n minCooldownSeconds: 300,\n reaction: \"./models/reactions/opportunity-matcher.reaction.ts\",\n prompt:\n \"Monitor connected profiles, newsletters, websites, and member updates for new launches, posts, hiring signals, funding news, and project changes. Identify which members are likely to care, explain why, and queue approved intro or outreach drafts.\\n\",\n extractionSchema: {\n type: \"object\",\n required: [\"signals\"],\n properties: {\n signals: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n type: { type: \"string\" },\n source: { type: \"string\" },\n related_topics: { type: \"array\", items: { type: \"string\" } },\n interested_members: { type: \"array\", items: { type: \"string\" } },\n reason: { type: \"string\" },\n suggested_action: { type: \"string\" },\n },\n },\n },\n },\n },\n});", + "code": "const opportunityMatcher = defineWatcher({\n agent: agentCommunity,\n slug: \"opportunity-matcher\",\n name: \"Opportunity matcher\",\n schedule: \"0 */12 * * *\",\n notification: { priority: \"normal\" },\n tags: [\"community\", \"matching\"],\n minCooldownSeconds: 300,\n reaction: \"./opportunity-matcher.reaction.ts\",\n prompt:\n \"Monitor connected profiles, newsletters, websites, and member updates for new launches, posts, hiring signals, funding news, and project changes. Identify which members are likely to care, explain why, and queue approved intro or outreach drafts.\\n\",\n extractionSchema: {\n type: \"object\",\n required: [\"signals\"],\n properties: {\n signals: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n type: { type: \"string\" },\n source: { type: \"string\" },\n related_topics: { type: \"array\", items: { type: \"string\" } },\n interested_members: { type: \"array\", items: { type: \"string\" } },\n reason: { type: \"string\" },\n suggested_action: { type: \"string\" },\n },\n },\n },\n },\n },\n});", "path": "lobu.config.ts", "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/agent-community/lobu.config.ts", "language": "typescript" @@ -233,8 +233,8 @@ "ecommerce": { "connector": { "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type ConnectorDefinition, type EventEnvelope, type SyncContext, type SyncResult } from \"@lobu/connector-sdk\";\n\ninterface StripeCharge { id: string; amount: number; currency: string; created: number; refunded: boolean }\ninterface Checkpoint { last_created: number }\n\nexport default class StripeChargesConnector extends ConnectorRuntime {\n readonly definition: ConnectorDefinition = {\n key: \"stripe-charges\",\n name: \"Stripe charges\",\n version: \"1.0.0\",\n // Stripe secret key collected per connection; exposed to sync() as ctx.config.secret_key.\n authSchema: { methods: [{ type: \"env_keys\", fields: [{ key: \"secret_key\", label: \"Stripe secret key\", secret: true, required: true }] }] },\n feeds: { charges: { key: \"charges\", name: \"Charges\" } },\n };\n\n async sync(ctx: SyncContext): Promise {\n const cursor = ((ctx.checkpoint ?? {}) as Partial).last_created ?? 0;\n const secretKey = String(ctx.config.secret_key ?? \"\");\n const r = await fetch(`https://api.stripe.com/v1/charges?limit=100&created[gt]=${cursor}`, {\n headers: { Authorization: `Bearer ${secretKey}` },\n });\n if (!r.ok) throw new Error(`Stripe ${r.status}: ${await r.text()}`);\n const data = (((await r.json()) as { data?: StripeCharge[] }).data ?? []).sort((a, b) => a.created - b.created);\n const events: EventEnvelope[] = data.map((c) => ({\n origin_id: c.refunded ? `${c.id}:refund` : c.id,\n origin_type: c.refunded ? \"charge_refunded\" : \"charge_succeeded\",\n title: `${c.refunded ? \"Refund\" : \"Charge\"} — ${(c.amount / 100).toFixed(2)} ${c.currency.toUpperCase()}`,\n payload_text: `${c.refunded ? \"Refund\" : \"Charge\"} of ${(c.amount / 100).toFixed(2)} ${c.currency.toUpperCase()} (stripe id ${c.id})`,\n source_url: `https://dashboard.stripe.com/payments/${c.id}`,\n occurred_at: new Date(c.created * 1000),\n }));\n return { events, checkpoint: { last_created: data.at(-1)?.created ?? cursor } satisfies Checkpoint };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", - "path": "connectors/stripe-charges.connector.ts", - "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/ecommerce/connectors/stripe-charges.connector.ts", + "path": "stripe-charges.connector.ts", + "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/ecommerce/stripe-charges.connector.ts", "language": "typescript" }, "memorySchema": { @@ -252,9 +252,9 @@ }, "leadership": { "connector": { - "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nconst QUERY = `query($t:ID!,$a:DateTimeOrDuration!){issues(first:100,filter:{team:{id:{eq:$t}},updatedAt:{gt:$a},cycle:{isActive:{eq:true}}},orderBy:updatedAt){nodes{id identifier title url updatedAt state{name}}}}`;\n\nexport default class LinearCyclesConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"linear-cycles\",\n name: \"Linear cycles\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"oauth\" as const, provider: \"linear\" }] },\n feeds: { cycle_issues: { key: \"cycle_issues\", name: \"Cycle issues\" } },\n };\n\n async sync(ctx: SyncContext) {\n const since = (ctx.checkpoint as any)?.updated_at ?? \"2000-01-01T00:00:00Z\";\n const r = await fetch(\"https://api.linear.app/graphql\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify({ query: QUERY, variables: { t: ctx.config.team_id, a: since } }) });\n const issues: any[] = ((await r.json()) as any).data?.issues?.nodes ?? [];\n return {\n events: issues.map((i) => ({\n origin_id: `${i.id}:${i.state.name}`,\n origin_type: \"issue_state_changed\",\n title: `${i.identifier} ${i.title} → ${i.state.name}`,\n source_url: i.url,\n occurred_at: new Date(i.updatedAt),\n })),\n checkpoint: { updated_at: issues.at(-1)?.updatedAt ?? since },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", - "path": "connectors/linear-cycles.connector.ts", - "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/leadership/connectors/linear-cycles.connector.ts", + "code": "// biome-ignore-all format: stays compact for the landing-page code panel\nimport { ConnectorRuntime, type SyncContext } from \"@lobu/connector-sdk\";\n\nconst QUERY = `query($t:ID!,$a:DateTimeOrDuration!){issues(first:100,filter:{team:{id:{eq:$t}},updatedAt:{gt:$a},cycle:{isActive:{eq:true}}},orderBy:updatedAt){nodes{id identifier title url updatedAt state{name}}}}`;\n\nexport default class LinearCyclesConnector extends ConnectorRuntime {\n readonly definition = {\n key: \"linear-cycles\",\n name: \"Linear cycles\",\n version: \"1.0.0\",\n authSchema: { methods: [{ type: \"oauth\" as const, provider: \"linear\", requiredScopes: [\"read\"] }] },\n feeds: { cycle_issues: { key: \"cycle_issues\", name: \"Cycle issues\" } },\n };\n\n async sync(ctx: SyncContext) {\n const since = (ctx.checkpoint as any)?.updated_at ?? \"2000-01-01T00:00:00Z\";\n const r = await fetch(\"https://api.linear.app/graphql\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify({ query: QUERY, variables: { t: ctx.config.team_id, a: since } }) });\n const issues: any[] = ((await r.json()) as any).data?.issues?.nodes ?? [];\n return {\n events: issues.map((i) => ({\n origin_id: `${i.id}:${i.state.name}`,\n origin_type: \"issue_state_changed\",\n title: `${i.identifier} ${i.title} → ${i.state.name}`,\n payload_text: `${i.identifier} ${i.title} is now ${i.state.name}`,\n source_url: i.url,\n occurred_at: new Date(i.updatedAt),\n })),\n checkpoint: { updated_at: issues.at(-1)?.updatedAt ?? since },\n };\n }\n\n async execute() {\n return { success: false, error: \"no actions\" };\n }\n}", + "path": "linear-cycles.connector.ts", + "githubUrl": "https://github.com/lobu-ai/lobu/blob/main/examples/leadership/linear-cycles.connector.ts", "language": "typescript" }, "memorySchema": { diff --git a/skills/lobu/SKILL.md b/skills/lobu/SKILL.md index 73482a7c3..09f7be7b9 100644 --- a/skills/lobu/SKILL.md +++ b/skills/lobu/SKILL.md @@ -49,9 +49,9 @@ The CLI generates the directory layout, including `lobu.config.ts`, `package.jso - **`lobu.config.ts`** — set the agent name + description from question 1 on `defineAgent`; add the chosen provider with `providers: [{ id, model, key: secret("X_API_KEY") }]`; set `org` / `orgName` in `defineConfig` from a slug of the user's choice. - **`.env`** — fill in `DATABASE_URL` and the provider API key from Phase 1. - **Entity types** — declare the entity types from question 3 with `defineEntityType({ key, name, properties })` and list them in `defineConfig({ entities: [...] })`. Each property is a JSON Schema fragment; add `"x-table-label"` / `"x-table-column": true` to surface a column in the admin UI. -- **`connectors/.connector.ts`** — only if the source from question 4 is not a bundled connector. Model it on `examples/lobu-crm/connectors/funnel-form.connector.ts` in the lobu repo. +- **`.connector.ts`** — only if the source from question 4 is not a bundled connector. Model it on `examples/lobu-crm/funnel-form.connector.ts` in the lobu repo, then list it with `connectorFromFile("./.connector.ts")` in `defineConfig({ connectors: [...] })`. - **Watchers** — add one watcher with `defineWatcher({ agent, slug, prompt, extractionSchema, schedule? })` and list it in `defineConfig({ watchers: [...] })`. Use the cron `schedule` from question 6 if the user wants one. -- **`reactions/.reaction.ts`** — only if the watcher needs to call actions after extracting (post to Slack, update an entity, etc.). Point the watcher at it with `reaction: "./reactions/.reaction.ts"`. Default path (no `reaction`) just writes the extracted data to memory. +- **`.reaction.ts`** — only if the watcher needs to call actions after extracting (post to Slack, update an entity, etc.). Point the watcher at it with `reaction: "./.reaction.ts"`. Default (no `reaction`) just writes the extracted data to memory. Then boot: