@agent-native/dispatch 0.13.9 → 0.13.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -28
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
- package/dist/server/lib/dispatch-integrations.js +39 -0
- package/dist/server/lib/dispatch-integrations.js.map +1 -1
- package/dist/server/plugins/integrations.d.ts.map +1 -1
- package/dist/server/plugins/integrations.js +12 -1
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +2 -2
- package/src/routes/pages/$appId.tsx +6 -6
- package/src/server/lib/dispatch-integrations.spec.ts +92 -1
- package/src/server/lib/dispatch-integrations.ts +48 -0
- package/src/server/plugins/integrations.ts +13 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Fork a working app and let the agent evolve it. **You can customize everything.*
|
|
|
45
45
|
|
|
46
46
|
<a href="https://agent-native.com/templates/clips"><img src="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F189ebd9b2f2b4f0ead3b33138d4e4c10?format=webp&width=800" alt="Clips app" width="100%" /></a>
|
|
47
47
|
|
|
48
|
-
**Agent-Native Loom
|
|
48
|
+
**Agent-Native Loom**
|
|
49
49
|
|
|
50
50
|
Record your screen with auto-transcripts and captured browser debug logs, share a link, and let an agent read the transcript, see timestamped frames, and fix the bug.
|
|
51
51
|
|
|
@@ -67,7 +67,7 @@ Install `/visual-plan` and `/visual-recap` so your coding agent can plan before
|
|
|
67
67
|
|
|
68
68
|
<a href="https://agent-native.com/templates/design"><img src="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2Fe2c86908c2fa4f119ee4aa90b4823944?format=webp&width=800" alt="Design app" width="100%" /></a>
|
|
69
69
|
|
|
70
|
-
**Agent-Native
|
|
70
|
+
**Agent-Native Figma**
|
|
71
71
|
|
|
72
72
|
Generate interactive HTML prototypes, compare variants, refine controls, and export the result.
|
|
73
73
|
|
|
@@ -80,40 +80,27 @@ Generate interactive HTML prototypes, compare variants, refine controls, and exp
|
|
|
80
80
|
|
|
81
81
|
<a href="https://agent-native.com/templates/content"><img src="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F89bcfc6106304bfbaf8ec8a7ccd721eb?format=webp&width=800" alt="Content app" width="100%" /></a>
|
|
82
82
|
|
|
83
|
-
**
|
|
83
|
+
**Agent-Native Notion/Obsidian**
|
|
84
84
|
|
|
85
85
|
Edit local Markdown/MDX files, generate rich interactive custom blocks, and draft, rewrite, or publish with an agent.
|
|
86
86
|
|
|
87
87
|
</td>
|
|
88
88
|
<td width="33%" align="center" valign="top">
|
|
89
89
|
|
|
90
|
-
**Slides**
|
|
91
|
-
|
|
92
|
-
<a href="https://agent-native.com/templates/slides"><img src="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F2c09b451d40c4a74a89a38d69170c2d8?format=webp&width=800" alt="Slides app" width="100%" /></a>
|
|
93
|
-
|
|
94
|
-
**Agent-Native Google Slides, Pitch**
|
|
95
|
-
|
|
96
|
-
Generate and edit React-based presentations via prompt or point-and-click.
|
|
97
|
-
|
|
98
|
-
</td>
|
|
99
|
-
<td width="33%" align="center" valign="top">
|
|
100
|
-
|
|
101
90
|
**Analytics**
|
|
102
91
|
|
|
103
92
|
<a href="https://agent-native.com/templates/analytics"><img src="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F4933a80cc3134d7e874631f688be828a?format=webp&width=800" alt="Analytics app" width="100%" /></a>
|
|
104
93
|
|
|
105
|
-
**Agent-Native Amplitude,
|
|
94
|
+
**Agent-Native Amplitude, Fullstory**
|
|
106
95
|
|
|
107
96
|
Connect analytics data sources, prompt for real charts, and build reusable dashboards.
|
|
108
97
|
|
|
109
98
|
</td>
|
|
110
|
-
|
|
111
|
-
<tr>
|
|
112
|
-
<td colspan="3" align="center" valign="top">
|
|
99
|
+
<td width="33%" align="center" valign="top">
|
|
113
100
|
|
|
114
101
|
**Chat**
|
|
115
102
|
|
|
116
|
-
<a href="https://agent-native.com/templates/chat"><img src="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F6b36dc596fca4799815fa34c31e1c406?format=webp&width=800" alt="Chat app" width="
|
|
103
|
+
<a href="https://agent-native.com/templates/chat"><img src="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F6b36dc596fca4799815fa34c31e1c406?format=webp&width=800" alt="Chat app" width="100%" /></a>
|
|
117
104
|
|
|
118
105
|
**A minimal ChatGPT-style app for your own agent**
|
|
119
106
|
|
|
@@ -123,9 +110,7 @@ Chat-first app scaffold with durable threads, actions, auth, live sync, and a cl
|
|
|
123
110
|
</tr>
|
|
124
111
|
</table>
|
|
125
112
|
|
|
126
|
-
**
|
|
127
|
-
|
|
128
|
-
View the full app gallery at **[agent-native.com/apps](https://agent-native.com/apps)**.
|
|
113
|
+
View the full app gallery at **[agent-native.com/apps](https://agent-native.com/apps)**, or build from scratch with the **[framework guide](https://agent-native.com/docs/getting-started)**.
|
|
129
114
|
|
|
130
115
|
## Quick Start
|
|
131
116
|
|
|
@@ -138,12 +123,6 @@ pnpm install
|
|
|
138
123
|
pnpm dev
|
|
139
124
|
```
|
|
140
125
|
|
|
141
|
-
`create` first asks how you want to start:
|
|
142
|
-
|
|
143
|
-
- **Full app(s)**: clone one or more complete apps into a workspace. Pick Mail + Calendar + Forms and you get all three wired up and sharing auth.
|
|
144
|
-
- **Chat**: a single app with a minimal chat UI and the browser shell already wired, the simplest way to get a UI.
|
|
145
|
-
- **Headless**: a single action-first app with no UI shell. The CLI walks you through calling your first action and agent, and you can add a UI later.
|
|
146
|
-
|
|
147
126
|
Prefer flags? `create my-app --template mail`, `--headless`, or `--standalone` skip the prompt.
|
|
148
127
|
|
|
149
128
|
See the full [getting started docs](https://agent-native.com/docs).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"$appId.js","sourceRoot":"","sources":["../../../src/routes/pages/$appId.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useActionQuery, appPath, useT } from \"@agent-native/core/client\";\nimport {\n IconArrowLeft,\n IconArrowUpRight,\n IconClockHour4,\n} from \"@tabler/icons-react\";\nimport { useEffect, useMemo } from \"react\";\nimport {\n Link,\n Navigate,\n redirect,\n useParams,\n type ClientLoaderFunctionArgs,\n type LoaderFunctionArgs,\n} from \"react-router\";\n\nimport { DispatchShell } from \"@/components/dispatch-shell\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport { resolveServerCatchAllTarget } from \"@/lib/catch-all-target\";\nimport {\n workspaceAppHref,\n type WorkspaceAppSummary,\n} from \"@/lib/workspace-apps\";\n\nexport function meta() {\n return [{ title: \"Workspace app - Dispatch\" }];\n}\n\n/**\n * Catch-all for `/dispatch/<segment>` paths that don't match an explicit\n * Dispatch route. When `<segment>` is the id of a workspace app sibling\n * (e.g. `/dispatch/todo` after Builder.io routes a \"navigate to /todo\"\n * call through Dispatch's mount point), bounce to the absolute `/<appId>`\n * so the user lands on the actual app instead of a 404 inside Dispatch.\n *\n * Server-side redirect: we resolve the workspace app manifest via the\n * shared `loadWorkspaceAppsManifest()` helper, which checks the\n * `AGENT_NATIVE_WORKSPACE_APPS_JSON` env var, then the\n * `.agent-native/workspace-apps.json` file written by `workspace-deploy.ts`,\n * then a live filesystem scan of `apps/` for local dev. We then throw\n * `redirect(\"/<appId>\")`. React Router 7 does not prepend the basename to\n * absolute paths returned from a loader, so the redirect escapes Dispatch's\n * `/dispatch` mount cleanly.\n *\n * Why a catch-all instead of fixing the agent prompt: Builder.io currently\n * resolves \"navigate to /todo\" relative to Dispatch's mount, sending the\n * user to /dispatch/todo. The same wrong path then gets captured as the\n * OAuth callbackURL, so Google sign-in completes back at /dispatch/todo\n * and looks broken. This route fixes both the post-creation navigation\n * and the OAuth round-trip from a single place.\n *\n * Built-in template fallback: when no workspace manifest is available\n * (framework dev with each template on its own port, hosted dispatch with\n * no sibling apps), redirect to the matching first-party template's deploy\n * URL — `http://localhost:<devPort>` in dev, `https://<id>.agent-native.com`\n * in production. Without this, a user visiting `/forms` on dispatch is\n * forced to sign in (auth guard) and then lands on this route's \"Page not\n * found\" pane after the post-login reload.\n *\n * `appId === \"dispatch\"` short-circuit: when the segment matches Dispatch\n * itself (e.g. `/dispatch/dispatch`), we go straight to the overview rather\n * than chaining through `/dispatch` (which polled `useActionQuery` re-fired\n * `window.location.assign` against and looped forever in production).\n */\nfunction dispatchSelfRedirect(appId: string | undefined): string | null {\n if (appId === \"dispatch\") return appPath(\"/overview\");\n return null;\n}\n\nexport async function loader({ params }: LoaderFunctionArgs) {\n const appId = params.appId;\n if (!appId) return null;\n const selfTarget = dispatchSelfRedirect(appId);\n if (selfTarget) throw redirect(selfTarget);\n const target = await resolveServerCatchAllTarget(appId);\n if (target) throw redirect(target);\n return null;\n}\n\nexport async function clientLoader({\n params,\n serverLoader,\n}: ClientLoaderFunctionArgs) {\n const selfTarget = dispatchSelfRedirect(params.appId);\n if (selfTarget) throw redirect(selfTarget);\n // Defer to the server loader so the built-in template fallback runs on\n // SPA navigations too (e.g. clicking a `/<template-id>` link inside\n // dispatch). Without this the client side would only check the workspace\n // apps query, which never lists the static first-party templates and so\n // the user would land on the \"Page not found\" pane.\n return serverLoader();\n}\n\nexport default function WorkspaceAppCatchAllRoute() {\n const t = useT();\n const { appId } = useParams();\n const { data: apps = [], isLoading } = useActionQuery(\n \"list-workspace-apps\",\n { includeAgentCards: false },\n { refetchInterval: 2_000 },\n );\n const app = useMemo(\n () =>\n (apps as WorkspaceAppSummary[]).find((item) => item.id === appId) ?? null,\n [appId, apps],\n );\n const href = app ? workspaceAppHref(app) : null;\n const isSelfReference = appId === \"dispatch\";\n\n useEffect(() => {\n if (isSelfReference) return;\n if (!app || app.status === \"pending\" || !href) return;\n window.location.assign(href);\n }, [app, href, isSelfReference]);\n\n if (isSelfReference) {\n return <Navigate to={appPath(\"/overview\")} replace />;\n }\n\n if ((isLoading && !app) || (app && app.status !== \"pending\" && href)) {\n return (\n <div className=\"flex h-screen w-full items-center justify-center\">\n <Spinner className=\"size-8\" />\n </div>\n );\n }\n\n return (\n <DispatchShell\n title={app?.name || t(\"dispatch.pages.pageNotFound\")}\n description={t(\"dispatch.pages.pageNotFoundDescription\")}\n >\n <div className=\"max-w-2xl rounded-lg border bg-card p-5\">\n <Button asChild size=\"sm\" variant=\"ghost\" className=\"-ml-2 mb-4\">\n <Link to={appPath(\"/overview\")}>\n <IconArrowLeft size={15} className=\"mr-1.5\" />\n {t(\"dispatch.nav.overview\")}\n </Link>\n </Button>\n\n {app?.status === \"pending\" ? (\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"text-base font-semibold text-foreground\">\n {app.name}\n </h2>\n <Badge\n variant=\"outline\"\n className=\"gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300\"\n >\n <IconClockHour4 size={12} />\n {t(\"dispatch.pages.building\")}\n </Badge>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"dispatch.pages.appBuildingPrefix\")}{\" \"}\n <span className=\"font-mono text-foreground\">{app.path}</span>{\" \"}\n {t(\"dispatch.pages.appBuildingSuffix\")}\n </p>\n {app.branchName ? (\n <p className=\"text-xs text-muted-foreground\">\n {t(\"dispatch.pages.branch\", { branch: app.branchName })}\n </p>\n ) : null}\n {app.builderUrl ? (\n <Button asChild>\n <a href={app.builderUrl} target=\"_blank\" rel=\"noreferrer\">\n {t(\"dispatch.pages.openBuilderBranch\")}\n <IconArrowUpRight size={15} className=\"ml-1.5\" />\n </a>\n </Button>\n ) : null}\n </div>\n ) : (\n <div className=\"space-y-3\">\n <h2 className=\"text-base font-semibold text-foreground\">\n {t(\"dispatch.pages.pageNotFound\")}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n <span className=\"font-mono text-foreground\">/{appId}</span> isn't\n {t(\"dispatch.pages.notDispatchOrWorkspaceApp\")}\n </p>\n <Button asChild>\n <Link to={appPath(\"/apps\")}>\n {t(\"dispatch.pages.browseApps\")}\n </Link>\n </Button>\n </div>\n )}\n </div>\n </DispatchShell>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"$appId.js","sourceRoot":"","sources":["../../../src/routes/pages/$appId.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useActionQuery, appPath, useT } from \"@agent-native/core/client\";\nimport {\n IconArrowLeft,\n IconArrowUpRight,\n IconClockHour4,\n} from \"@tabler/icons-react\";\nimport { useEffect, useMemo } from \"react\";\nimport {\n Link,\n Navigate,\n redirect,\n useParams,\n type ClientLoaderFunctionArgs,\n type LoaderFunctionArgs,\n} from \"react-router\";\n\nimport { DispatchShell } from \"../../components/dispatch-shell\";\nimport { Badge } from \"../../components/ui/badge\";\nimport { Button } from \"../../components/ui/button\";\nimport { Spinner } from \"../../components/ui/spinner\";\nimport { resolveServerCatchAllTarget } from \"../../lib/catch-all-target\";\nimport {\n workspaceAppHref,\n type WorkspaceAppSummary,\n} from \"../../lib/workspace-apps\";\n\nexport function meta() {\n return [{ title: \"Workspace app - Dispatch\" }];\n}\n\n/**\n * Catch-all for `/dispatch/<segment>` paths that don't match an explicit\n * Dispatch route. When `<segment>` is the id of a workspace app sibling\n * (e.g. `/dispatch/todo` after Builder.io routes a \"navigate to /todo\"\n * call through Dispatch's mount point), bounce to the absolute `/<appId>`\n * so the user lands on the actual app instead of a 404 inside Dispatch.\n *\n * Server-side redirect: we resolve the workspace app manifest via the\n * shared `loadWorkspaceAppsManifest()` helper, which checks the\n * `AGENT_NATIVE_WORKSPACE_APPS_JSON` env var, then the\n * `.agent-native/workspace-apps.json` file written by `workspace-deploy.ts`,\n * then a live filesystem scan of `apps/` for local dev. We then throw\n * `redirect(\"/<appId>\")`. React Router 7 does not prepend the basename to\n * absolute paths returned from a loader, so the redirect escapes Dispatch's\n * `/dispatch` mount cleanly.\n *\n * Why a catch-all instead of fixing the agent prompt: Builder.io currently\n * resolves \"navigate to /todo\" relative to Dispatch's mount, sending the\n * user to /dispatch/todo. The same wrong path then gets captured as the\n * OAuth callbackURL, so Google sign-in completes back at /dispatch/todo\n * and looks broken. This route fixes both the post-creation navigation\n * and the OAuth round-trip from a single place.\n *\n * Built-in template fallback: when no workspace manifest is available\n * (framework dev with each template on its own port, hosted dispatch with\n * no sibling apps), redirect to the matching first-party template's deploy\n * URL — `http://localhost:<devPort>` in dev, `https://<id>.agent-native.com`\n * in production. Without this, a user visiting `/forms` on dispatch is\n * forced to sign in (auth guard) and then lands on this route's \"Page not\n * found\" pane after the post-login reload.\n *\n * `appId === \"dispatch\"` short-circuit: when the segment matches Dispatch\n * itself (e.g. `/dispatch/dispatch`), we go straight to the overview rather\n * than chaining through `/dispatch` (which polled `useActionQuery` re-fired\n * `window.location.assign` against and looped forever in production).\n */\nfunction dispatchSelfRedirect(appId: string | undefined): string | null {\n if (appId === \"dispatch\") return appPath(\"/overview\");\n return null;\n}\n\nexport async function loader({ params }: LoaderFunctionArgs) {\n const appId = params.appId;\n if (!appId) return null;\n const selfTarget = dispatchSelfRedirect(appId);\n if (selfTarget) throw redirect(selfTarget);\n const target = await resolveServerCatchAllTarget(appId);\n if (target) throw redirect(target);\n return null;\n}\n\nexport async function clientLoader({\n params,\n serverLoader,\n}: ClientLoaderFunctionArgs) {\n const selfTarget = dispatchSelfRedirect(params.appId);\n if (selfTarget) throw redirect(selfTarget);\n // Defer to the server loader so the built-in template fallback runs on\n // SPA navigations too (e.g. clicking a `/<template-id>` link inside\n // dispatch). Without this the client side would only check the workspace\n // apps query, which never lists the static first-party templates and so\n // the user would land on the \"Page not found\" pane.\n return serverLoader();\n}\n\nexport default function WorkspaceAppCatchAllRoute() {\n const t = useT();\n const { appId } = useParams();\n const { data: apps = [], isLoading } = useActionQuery(\n \"list-workspace-apps\",\n { includeAgentCards: false },\n { refetchInterval: 2_000 },\n );\n const app = useMemo(\n () =>\n (apps as WorkspaceAppSummary[]).find((item) => item.id === appId) ?? null,\n [appId, apps],\n );\n const href = app ? workspaceAppHref(app) : null;\n const isSelfReference = appId === \"dispatch\";\n\n useEffect(() => {\n if (isSelfReference) return;\n if (!app || app.status === \"pending\" || !href) return;\n window.location.assign(href);\n }, [app, href, isSelfReference]);\n\n if (isSelfReference) {\n return <Navigate to={appPath(\"/overview\")} replace />;\n }\n\n if ((isLoading && !app) || (app && app.status !== \"pending\" && href)) {\n return (\n <div className=\"flex h-screen w-full items-center justify-center\">\n <Spinner className=\"size-8\" />\n </div>\n );\n }\n\n return (\n <DispatchShell\n title={app?.name || t(\"dispatch.pages.pageNotFound\")}\n description={t(\"dispatch.pages.pageNotFoundDescription\")}\n >\n <div className=\"max-w-2xl rounded-lg border bg-card p-5\">\n <Button asChild size=\"sm\" variant=\"ghost\" className=\"-ml-2 mb-4\">\n <Link to={appPath(\"/overview\")}>\n <IconArrowLeft size={15} className=\"mr-1.5\" />\n {t(\"dispatch.nav.overview\")}\n </Link>\n </Button>\n\n {app?.status === \"pending\" ? (\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"text-base font-semibold text-foreground\">\n {app.name}\n </h2>\n <Badge\n variant=\"outline\"\n className=\"gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300\"\n >\n <IconClockHour4 size={12} />\n {t(\"dispatch.pages.building\")}\n </Badge>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"dispatch.pages.appBuildingPrefix\")}{\" \"}\n <span className=\"font-mono text-foreground\">{app.path}</span>{\" \"}\n {t(\"dispatch.pages.appBuildingSuffix\")}\n </p>\n {app.branchName ? (\n <p className=\"text-xs text-muted-foreground\">\n {t(\"dispatch.pages.branch\", { branch: app.branchName })}\n </p>\n ) : null}\n {app.builderUrl ? (\n <Button asChild>\n <a href={app.builderUrl} target=\"_blank\" rel=\"noreferrer\">\n {t(\"dispatch.pages.openBuilderBranch\")}\n <IconArrowUpRight size={15} className=\"ml-1.5\" />\n </a>\n </Button>\n ) : null}\n </div>\n ) : (\n <div className=\"space-y-3\">\n <h2 className=\"text-base font-semibold text-foreground\">\n {t(\"dispatch.pages.pageNotFound\")}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n <span className=\"font-mono text-foreground\">/{appId}</span> isn't\n {t(\"dispatch.pages.notDispatchOrWorkspaceApp\")}\n </p>\n <Button asChild>\n <Link to={appPath(\"/apps\")}>\n {t(\"dispatch.pages.browseApps\")}\n </Link>\n </Button>\n </div>\n )}\n </div>\n </DispatchShell>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch-integrations.d.ts","sourceRoot":"","sources":["../../../src/server/lib/dispatch-integrations.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dispatch-integrations.d.ts","sourceRoot":"","sources":["../../../src/server/lib/dispatch-integrations.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EAChB,MAAM,2BAA2B,CAAC;AAsBnC,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,eAAe,GACxB,MAAM,GAAG,IAAI,CAmBf;AA4ID,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,eAAe,GACxB,OAAO,CAAC,MAAM,CAAC,CA8DjB;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAA;CAAE,CAAC,CA0CxE"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { resolveOrgIdForEmail } from "@agent-native/core/org";
|
|
3
|
+
import { withConfiguredAppBasePath } from "@agent-native/core/server";
|
|
3
4
|
import { handleRemoteCodeCommand } from "./dispatch-remote-commands.js";
|
|
4
5
|
import { consumeLinkToken, resolveLinkedOwner } from "./dispatch-store.js";
|
|
5
6
|
const slackProfileCache = new Map();
|
|
@@ -53,6 +54,35 @@ function configuredDefaultOwnerForIncoming(incoming) {
|
|
|
53
54
|
return null;
|
|
54
55
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : null;
|
|
55
56
|
}
|
|
57
|
+
function platformRequiresExplicitLink(incoming) {
|
|
58
|
+
// Telegram does not provide a verified email address. Require an explicit
|
|
59
|
+
// identity link before it can act as a Builder/Agent-Native user.
|
|
60
|
+
return incoming.platform === "telegram";
|
|
61
|
+
}
|
|
62
|
+
function configuredDispatchIdentitiesUrl() {
|
|
63
|
+
const raw = process.env.WORKSPACE_GATEWAY_URL ||
|
|
64
|
+
process.env.APP_URL ||
|
|
65
|
+
process.env.URL ||
|
|
66
|
+
process.env.DEPLOY_URL ||
|
|
67
|
+
process.env.BETTER_AUTH_URL ||
|
|
68
|
+
"";
|
|
69
|
+
if (!raw.trim())
|
|
70
|
+
return null;
|
|
71
|
+
const base = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
72
|
+
try {
|
|
73
|
+
return new URL("identities", `${withConfiguredAppBasePath(base).replace(/\/+$/, "")}/`).toString();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function formatTelegramLinkRequiredMessage() {
|
|
80
|
+
const identitiesUrl = configuredDispatchIdentitiesUrl();
|
|
81
|
+
const linkStep = identitiesUrl
|
|
82
|
+
? `Tap ${identitiesUrl}, create a Telegram link token, then send \`/link <token>\` here.`
|
|
83
|
+
: "Open Dispatch while signed in, create a Telegram link token, then send `/link <token>` here.";
|
|
84
|
+
return `Telegram is connected, but this Telegram account is not linked to an Agent-Native user yet. ${linkStep} After that I can use your Builder.io org and connected apps.`;
|
|
85
|
+
}
|
|
56
86
|
async function resolveSlackSenderProfile(incoming) {
|
|
57
87
|
if (incoming.platform !== "slack")
|
|
58
88
|
return { email: null, name: null };
|
|
@@ -179,6 +209,15 @@ export async function beforeDispatchProcess(incoming, adapter) {
|
|
|
179
209
|
const commandText = contextString(incoming.platformContext.rawText) || trimmed;
|
|
180
210
|
const match = commandText.match(/^\/link(?:@\w+)?\s+([a-zA-Z0-9_-]+)$/);
|
|
181
211
|
if (!match) {
|
|
212
|
+
if (platformRequiresExplicitLink(incoming)) {
|
|
213
|
+
const owner = await resolveLinkedOwner(incoming.platform, identityKeyForIncoming(incoming), { allowAnyOrgFallback: true });
|
|
214
|
+
if (!owner) {
|
|
215
|
+
return {
|
|
216
|
+
handled: true,
|
|
217
|
+
responseText: formatTelegramLinkRequiredMessage(),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
182
221
|
return handleRemoteCodeCommand(incoming, adapter, {
|
|
183
222
|
resolveOwner: () => resolveDispatchOwner(incoming),
|
|
184
223
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch-integrations.js","sourceRoot":"","sources":["../../../src/server/lib/dispatch-integrations.ts"],"names":[],"mappings":"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","sourcesContent":["import crypto from \"node:crypto\";\n\nimport { resolveOrgIdForEmail } from \"@agent-native/core/org\";\nimport type {\n IncomingMessage,\n PlatformAdapter,\n} from \"@agent-native/core/server\";\n\nimport { handleRemoteCodeCommand } from \"./dispatch-remote-commands.js\";\nimport { consumeLinkToken, resolveLinkedOwner } from \"./dispatch-store.js\";\n\ntype SlackSenderProfile = {\n email: string | null;\n name: string | null;\n};\n\nconst slackProfileCache = new Map<\n string,\n { profile: SlackSenderProfile; expiresAt: number }\n>();\nconst SLACK_PROFILE_CACHE_TTL = 10 * 60 * 1000;\n\nfunction contextString(value: unknown): string | null {\n if (typeof value === \"string\" && value.trim()) return value.trim();\n if (typeof value === \"number\" && Number.isFinite(value)) return String(value);\n return null;\n}\n\nexport function identityKeyForIncoming(\n incoming: IncomingMessage,\n): string | null {\n const senderId = contextString(incoming.senderId);\n if (!senderId) return null;\n\n if (incoming.platform === \"slack\") {\n const teamId = contextString(incoming.platformContext.teamId);\n return teamId ? `${teamId}:${senderId}` : senderId;\n }\n\n if (incoming.platform === \"whatsapp\") {\n const phoneNumberId = contextString(incoming.platformContext.phoneNumberId);\n return phoneNumberId ? `${phoneNumberId}:${senderId}` : senderId;\n }\n\n if (incoming.platform === \"email\") {\n return senderId.toLowerCase();\n }\n\n return senderId;\n}\n\nfunction fallbackOwnerForIncoming(incoming: IncomingMessage): string {\n const tenant =\n contextString(incoming.platformContext.teamId) ||\n contextString(incoming.platformContext.phoneNumberId) ||\n contextString(incoming.platformContext.chatId) ||\n contextString(incoming.platformContext.from) ||\n incoming.externalThreadId;\n const raw = `${incoming.platform}:${tenant}:${incoming.senderId || \"\"}`;\n const hash = crypto\n .createHash(\"sha256\")\n .update(raw)\n .digest(\"hex\")\n .slice(0, 16);\n return `dispatch+${hash}@integration.local`;\n}\n\nfunction configuredDefaultOwnerForIncoming(\n incoming: IncomingMessage,\n): string | null {\n // This is intentionally Slack-only: a deployment-wide default owner grants\n // that Slack workspace access to the owner's connected agents and org\n // credentials, so other platforms should opt in with explicit identity links.\n if (incoming.platform !== \"slack\") return null;\n const email = process.env.DISPATCH_DEFAULT_OWNER_EMAIL?.trim();\n if (!email) return null;\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) ? email : null;\n}\n\nasync function resolveSlackSenderProfile(\n incoming: IncomingMessage,\n): Promise<SlackSenderProfile> {\n if (incoming.platform !== \"slack\") return { email: null, name: null };\n const token = process.env.SLACK_BOT_TOKEN;\n const userId = contextString(incoming.senderId);\n const teamId = contextString(incoming.platformContext.teamId);\n if (!token || !userId) return { email: null, name: null };\n\n // Slack user IDs are scoped per workspace, so without a teamId we can't\n // safely cache: two installs of the bot in different workspaces could\n // share user-id strings and collide on a single \"default\" key. Skip the\n // cache (and lookup on every request) when teamId is missing.\n const cacheKey = teamId ? `${teamId}:${userId}` : null;\n if (cacheKey) {\n const cached = slackProfileCache.get(cacheKey);\n if (cached && cached.expiresAt > Date.now()) return cached.profile;\n }\n\n try {\n const params = new URLSearchParams({ user: userId });\n const res = await fetch(`https://slack.com/api/users.info?${params}`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n const data = (await res.json()) as {\n ok?: boolean;\n user?: {\n real_name?: string;\n name?: string;\n profile?: {\n email?: string;\n real_name?: string;\n display_name?: string;\n };\n };\n };\n const profile = data.ok\n ? {\n email: data.user?.profile?.email?.trim().toLowerCase() || null,\n name:\n data.user?.profile?.real_name?.trim() ||\n data.user?.profile?.display_name?.trim() ||\n data.user?.real_name?.trim() ||\n data.user?.name?.trim() ||\n null,\n }\n : { email: null, name: null };\n if (cacheKey) {\n slackProfileCache.set(cacheKey, {\n profile,\n expiresAt: Date.now() + SLACK_PROFILE_CACHE_TTL,\n });\n }\n return profile;\n } catch {\n return { email: null, name: null };\n }\n}\n\nasync function resolveSlackOwnerFromVerifiedEmail(\n incoming: IncomingMessage,\n): Promise<string | null> {\n const profile = await resolveSlackSenderProfile(incoming);\n if (!profile.email) return null;\n\n incoming.senderEmail = profile.email;\n incoming.platformContext.senderEmail = profile.email;\n if (profile.name) {\n incoming.senderName = profile.name;\n incoming.platformContext.senderName = profile.name;\n }\n\n const orgId = await resolveOrgIdForEmail(profile.email);\n return orgId ? profile.email : null;\n}\n\nexport async function resolveDispatchOwner(\n incoming: IncomingMessage,\n): Promise<string> {\n try {\n const externalUserId = identityKeyForIncoming(incoming);\n\n // Webhooks do not have the browser request's org context, so allow a safe\n // cross-org fallback when the linked platform identity maps to one owner.\n const owner = await resolveLinkedOwner(incoming.platform, externalUserId, {\n allowAnyOrgFallback: true,\n });\n if (owner) return owner;\n\n // For email, the sender's `From:` address is attacker-settable: SMTP lets\n // anyone claim any From, and our inbound webhook secret only authenticates\n // the provider→app hop, not the original sender. So we must NOT grant a\n // real user's identity (their API keys, org secrets, personal\n // instructions, ownable data) off the bare From. Mirror the Slack gate:\n // only return the sender email as the acting owner when BOTH\n // (a) the message is DKIM/SPF-verified for the From domain, AND\n // (b) that email maps to a real org member.\n // Otherwise fall through to the synthetic, credential-less fallback owner.\n // (A linked identity, handled by resolveLinkedOwner above, remains an\n // always-allowed way to bind an address regardless of verification.)\n //\n // Escape hatch: set DISPATCH_TRUST_UNVERIFIED_EMAIL_SENDER=1 to restore\n // the legacy \"trust the From header\" behavior. OFF by default; only use\n // this if you fully control the inbound mail path and accept that a\n // spoofed From can act as any org member. See FINDING 3 (inbound-email\n // impersonation) in the webhook security audit.\n if (\n incoming.platform === \"email\" &&\n incoming.senderId &&\n incoming.senderId.includes(\"@\")\n ) {\n if (process.env.DISPATCH_TRUST_UNVERIFIED_EMAIL_SENDER === \"1\") {\n return incoming.senderId;\n }\n if (incoming.senderVerified) {\n const orgId = await resolveOrgIdForEmail(incoming.senderId);\n if (orgId) return incoming.senderId;\n }\n // Unverified or not an org member — do not impersonate. Fall through to\n // the synthetic fallback owner below.\n }\n\n // Slack gives us a user id in the event payload. Resolve it to a verified\n // workspace email and use that user's own org context when they are an\n // Agent-Native member, so artifacts created via @agent-native are visible\n // when they open the target app.\n if (incoming.platform === \"slack\") {\n const slackOwner = await resolveSlackOwnerFromVerifiedEmail(incoming);\n if (slackOwner) return slackOwner;\n }\n\n const defaultOwner = configuredDefaultOwnerForIncoming(incoming);\n if (defaultOwner) return defaultOwner;\n\n return fallbackOwnerForIncoming(incoming);\n } catch {\n const defaultOwner = configuredDefaultOwnerForIncoming(incoming);\n if (defaultOwner) return defaultOwner;\n return fallbackOwnerForIncoming(incoming);\n }\n}\n\nexport async function beforeDispatchProcess(\n incoming: IncomingMessage,\n adapter: PlatformAdapter,\n): Promise<{ handled: true; responseText?: string } | { handled: false }> {\n const trimmed = incoming.text.trim();\n const commandText =\n contextString(incoming.platformContext.rawText) || trimmed;\n const match = commandText.match(/^\\/link(?:@\\w+)?\\s+([a-zA-Z0-9_-]+)$/);\n if (!match) {\n return handleRemoteCodeCommand(incoming, adapter, {\n resolveOwner: () => resolveDispatchOwner(incoming),\n });\n }\n\n try {\n const owner = await consumeLinkToken({\n platform: incoming.platform,\n token: match[1],\n externalUserId: identityKeyForIncoming(incoming),\n externalUserName: incoming.senderName || null,\n });\n return {\n handled: true,\n responseText: `Linked successfully. Future ${incoming.platform} messages will use ${owner}'s personal dispatch context.`,\n };\n } catch (error) {\n return {\n handled: true,\n responseText:\n error instanceof Error ? error.message : \"Failed to link this account.\",\n };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dispatch-integrations.js","sourceRoot":"","sources":["../../../src/server/lib/dispatch-integrations.ts"],"names":[],"mappings":"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","sourcesContent":["import crypto from \"node:crypto\";\n\nimport { resolveOrgIdForEmail } from \"@agent-native/core/org\";\nimport { withConfiguredAppBasePath } from \"@agent-native/core/server\";\nimport type {\n IncomingMessage,\n PlatformAdapter,\n} from \"@agent-native/core/server\";\n\nimport { handleRemoteCodeCommand } from \"./dispatch-remote-commands.js\";\nimport { consumeLinkToken, resolveLinkedOwner } from \"./dispatch-store.js\";\n\ntype SlackSenderProfile = {\n email: string | null;\n name: string | null;\n};\n\nconst slackProfileCache = new Map<\n string,\n { profile: SlackSenderProfile; expiresAt: number }\n>();\nconst SLACK_PROFILE_CACHE_TTL = 10 * 60 * 1000;\n\nfunction contextString(value: unknown): string | null {\n if (typeof value === \"string\" && value.trim()) return value.trim();\n if (typeof value === \"number\" && Number.isFinite(value)) return String(value);\n return null;\n}\n\nexport function identityKeyForIncoming(\n incoming: IncomingMessage,\n): string | null {\n const senderId = contextString(incoming.senderId);\n if (!senderId) return null;\n\n if (incoming.platform === \"slack\") {\n const teamId = contextString(incoming.platformContext.teamId);\n return teamId ? `${teamId}:${senderId}` : senderId;\n }\n\n if (incoming.platform === \"whatsapp\") {\n const phoneNumberId = contextString(incoming.platformContext.phoneNumberId);\n return phoneNumberId ? `${phoneNumberId}:${senderId}` : senderId;\n }\n\n if (incoming.platform === \"email\") {\n return senderId.toLowerCase();\n }\n\n return senderId;\n}\n\nfunction fallbackOwnerForIncoming(incoming: IncomingMessage): string {\n const tenant =\n contextString(incoming.platformContext.teamId) ||\n contextString(incoming.platformContext.phoneNumberId) ||\n contextString(incoming.platformContext.chatId) ||\n contextString(incoming.platformContext.from) ||\n incoming.externalThreadId;\n const raw = `${incoming.platform}:${tenant}:${incoming.senderId || \"\"}`;\n const hash = crypto\n .createHash(\"sha256\")\n .update(raw)\n .digest(\"hex\")\n .slice(0, 16);\n return `dispatch+${hash}@integration.local`;\n}\n\nfunction configuredDefaultOwnerForIncoming(\n incoming: IncomingMessage,\n): string | null {\n // This is intentionally Slack-only: a deployment-wide default owner grants\n // that Slack workspace access to the owner's connected agents and org\n // credentials, so other platforms should opt in with explicit identity links.\n if (incoming.platform !== \"slack\") return null;\n const email = process.env.DISPATCH_DEFAULT_OWNER_EMAIL?.trim();\n if (!email) return null;\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) ? email : null;\n}\n\nfunction platformRequiresExplicitLink(incoming: IncomingMessage): boolean {\n // Telegram does not provide a verified email address. Require an explicit\n // identity link before it can act as a Builder/Agent-Native user.\n return incoming.platform === \"telegram\";\n}\n\nfunction configuredDispatchIdentitiesUrl(): string | null {\n const raw =\n process.env.WORKSPACE_GATEWAY_URL ||\n process.env.APP_URL ||\n process.env.URL ||\n process.env.DEPLOY_URL ||\n process.env.BETTER_AUTH_URL ||\n \"\";\n if (!raw.trim()) return null;\n const base = /^https?:\\/\\//i.test(raw) ? raw : `https://${raw}`;\n try {\n return new URL(\n \"identities\",\n `${withConfiguredAppBasePath(base).replace(/\\/+$/, \"\")}/`,\n ).toString();\n } catch {\n return null;\n }\n}\n\nfunction formatTelegramLinkRequiredMessage(): string {\n const identitiesUrl = configuredDispatchIdentitiesUrl();\n const linkStep = identitiesUrl\n ? `Tap ${identitiesUrl}, create a Telegram link token, then send \\`/link <token>\\` here.`\n : \"Open Dispatch while signed in, create a Telegram link token, then send `/link <token>` here.\";\n return `Telegram is connected, but this Telegram account is not linked to an Agent-Native user yet. ${linkStep} After that I can use your Builder.io org and connected apps.`;\n}\n\nasync function resolveSlackSenderProfile(\n incoming: IncomingMessage,\n): Promise<SlackSenderProfile> {\n if (incoming.platform !== \"slack\") return { email: null, name: null };\n const token = process.env.SLACK_BOT_TOKEN;\n const userId = contextString(incoming.senderId);\n const teamId = contextString(incoming.platformContext.teamId);\n if (!token || !userId) return { email: null, name: null };\n\n // Slack user IDs are scoped per workspace, so without a teamId we can't\n // safely cache: two installs of the bot in different workspaces could\n // share user-id strings and collide on a single \"default\" key. Skip the\n // cache (and lookup on every request) when teamId is missing.\n const cacheKey = teamId ? `${teamId}:${userId}` : null;\n if (cacheKey) {\n const cached = slackProfileCache.get(cacheKey);\n if (cached && cached.expiresAt > Date.now()) return cached.profile;\n }\n\n try {\n const params = new URLSearchParams({ user: userId });\n const res = await fetch(`https://slack.com/api/users.info?${params}`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n const data = (await res.json()) as {\n ok?: boolean;\n user?: {\n real_name?: string;\n name?: string;\n profile?: {\n email?: string;\n real_name?: string;\n display_name?: string;\n };\n };\n };\n const profile = data.ok\n ? {\n email: data.user?.profile?.email?.trim().toLowerCase() || null,\n name:\n data.user?.profile?.real_name?.trim() ||\n data.user?.profile?.display_name?.trim() ||\n data.user?.real_name?.trim() ||\n data.user?.name?.trim() ||\n null,\n }\n : { email: null, name: null };\n if (cacheKey) {\n slackProfileCache.set(cacheKey, {\n profile,\n expiresAt: Date.now() + SLACK_PROFILE_CACHE_TTL,\n });\n }\n return profile;\n } catch {\n return { email: null, name: null };\n }\n}\n\nasync function resolveSlackOwnerFromVerifiedEmail(\n incoming: IncomingMessage,\n): Promise<string | null> {\n const profile = await resolveSlackSenderProfile(incoming);\n if (!profile.email) return null;\n\n incoming.senderEmail = profile.email;\n incoming.platformContext.senderEmail = profile.email;\n if (profile.name) {\n incoming.senderName = profile.name;\n incoming.platformContext.senderName = profile.name;\n }\n\n const orgId = await resolveOrgIdForEmail(profile.email);\n return orgId ? profile.email : null;\n}\n\nexport async function resolveDispatchOwner(\n incoming: IncomingMessage,\n): Promise<string> {\n try {\n const externalUserId = identityKeyForIncoming(incoming);\n\n // Webhooks do not have the browser request's org context, so allow a safe\n // cross-org fallback when the linked platform identity maps to one owner.\n const owner = await resolveLinkedOwner(incoming.platform, externalUserId, {\n allowAnyOrgFallback: true,\n });\n if (owner) return owner;\n\n // For email, the sender's `From:` address is attacker-settable: SMTP lets\n // anyone claim any From, and our inbound webhook secret only authenticates\n // the provider→app hop, not the original sender. So we must NOT grant a\n // real user's identity (their API keys, org secrets, personal\n // instructions, ownable data) off the bare From. Mirror the Slack gate:\n // only return the sender email as the acting owner when BOTH\n // (a) the message is DKIM/SPF-verified for the From domain, AND\n // (b) that email maps to a real org member.\n // Otherwise fall through to the synthetic, credential-less fallback owner.\n // (A linked identity, handled by resolveLinkedOwner above, remains an\n // always-allowed way to bind an address regardless of verification.)\n //\n // Escape hatch: set DISPATCH_TRUST_UNVERIFIED_EMAIL_SENDER=1 to restore\n // the legacy \"trust the From header\" behavior. OFF by default; only use\n // this if you fully control the inbound mail path and accept that a\n // spoofed From can act as any org member. See FINDING 3 (inbound-email\n // impersonation) in the webhook security audit.\n if (\n incoming.platform === \"email\" &&\n incoming.senderId &&\n incoming.senderId.includes(\"@\")\n ) {\n if (process.env.DISPATCH_TRUST_UNVERIFIED_EMAIL_SENDER === \"1\") {\n return incoming.senderId;\n }\n if (incoming.senderVerified) {\n const orgId = await resolveOrgIdForEmail(incoming.senderId);\n if (orgId) return incoming.senderId;\n }\n // Unverified or not an org member — do not impersonate. Fall through to\n // the synthetic fallback owner below.\n }\n\n // Slack gives us a user id in the event payload. Resolve it to a verified\n // workspace email and use that user's own org context when they are an\n // Agent-Native member, so artifacts created via @agent-native are visible\n // when they open the target app.\n if (incoming.platform === \"slack\") {\n const slackOwner = await resolveSlackOwnerFromVerifiedEmail(incoming);\n if (slackOwner) return slackOwner;\n }\n\n const defaultOwner = configuredDefaultOwnerForIncoming(incoming);\n if (defaultOwner) return defaultOwner;\n\n return fallbackOwnerForIncoming(incoming);\n } catch {\n const defaultOwner = configuredDefaultOwnerForIncoming(incoming);\n if (defaultOwner) return defaultOwner;\n return fallbackOwnerForIncoming(incoming);\n }\n}\n\nexport async function beforeDispatchProcess(\n incoming: IncomingMessage,\n adapter: PlatformAdapter,\n): Promise<{ handled: true; responseText?: string } | { handled: false }> {\n const trimmed = incoming.text.trim();\n const commandText =\n contextString(incoming.platformContext.rawText) || trimmed;\n const match = commandText.match(/^\\/link(?:@\\w+)?\\s+([a-zA-Z0-9_-]+)$/);\n if (!match) {\n if (platformRequiresExplicitLink(incoming)) {\n const owner = await resolveLinkedOwner(\n incoming.platform,\n identityKeyForIncoming(incoming),\n { allowAnyOrgFallback: true },\n );\n if (!owner) {\n return {\n handled: true,\n responseText: formatTelegramLinkRequiredMessage(),\n };\n }\n }\n return handleRemoteCodeCommand(incoming, adapter, {\n resolveOwner: () => resolveDispatchOwner(incoming),\n });\n }\n\n try {\n const owner = await consumeLinkToken({\n platform: incoming.platform,\n token: match[1],\n externalUserId: identityKeyForIncoming(incoming),\n externalUserName: incoming.senderName || null,\n });\n return {\n handled: true,\n responseText: `Linked successfully. Future ${incoming.platform} messages will use ${owner}'s personal dispatch context.`,\n };\n } catch (error) {\n return {\n handled: true,\n responseText:\n error instanceof Error ? error.message : \"Failed to link this account.\",\n };\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integrations.d.ts","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"integrations.d.ts","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"AA8CA;;;;GAIG;AACH,QAAA,MAAM,0BAA0B,aAAoB,GAAG,kBAuBtD,CAAC;eAEa,0BAA0B"}
|
|
@@ -2,6 +2,16 @@ import { createIntegrationsPlugin } from "@agent-native/core/server";
|
|
|
2
2
|
import { dispatchActions } from "../../actions/index.js";
|
|
3
3
|
import { getDispatchConfig } from "../index.js";
|
|
4
4
|
import { beforeDispatchProcess, resolveDispatchOwner, } from "../lib/dispatch-integrations.js";
|
|
5
|
+
const dispatchIntegrationActions = {
|
|
6
|
+
...dispatchActions,
|
|
7
|
+
// Messaging integrations should use the core call-agent tool for cross-app
|
|
8
|
+
// delegation because it queues A2A continuations when serverless budgets are
|
|
9
|
+
// tight. The MCP-facing ask_app action is still available outside this path.
|
|
10
|
+
ask_app: {
|
|
11
|
+
...dispatchActions.ask_app,
|
|
12
|
+
agentTool: false,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
5
15
|
const DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for this workspace, responding via a messaging platform integration (Slack, Telegram, email, etc.).
|
|
6
16
|
|
|
7
17
|
Default posture:
|
|
@@ -16,6 +26,7 @@ Default posture:
|
|
|
16
26
|
|
|
17
27
|
When a user asks for something:
|
|
18
28
|
- If it belongs to analytics, content, slides, clips, assets, etc., delegate via call-agent — do not re-implement the domain logic in dispatch.
|
|
29
|
+
- In messaging integrations, use call-agent for cross-app delegation; do not use ask_app.
|
|
19
30
|
- After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.
|
|
20
31
|
- Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.
|
|
21
32
|
- If the user asks to create, build, make, scaffold, or generate an "agent" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.
|
|
@@ -41,7 +52,7 @@ const dispatchIntegrationsPlugin = async (nitroApp) => {
|
|
|
41
52
|
: DISPATCH_INTEGRATION_SYSTEM_PROMPT;
|
|
42
53
|
const plugin = createIntegrationsPlugin({
|
|
43
54
|
appId: "dispatch",
|
|
44
|
-
actions:
|
|
55
|
+
actions: dispatchIntegrationActions,
|
|
45
56
|
resolveOwner: resolveDispatchOwner,
|
|
46
57
|
beforeProcess: beforeDispatchProcess,
|
|
47
58
|
systemPrompt,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integrations.js","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAEzC,MAAM,kCAAkC,GAAG
|
|
1
|
+
{"version":3,"file":"integrations.js","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAEzC,MAAM,0BAA0B,GAAG;IACjC,GAAG,eAAe;IAClB,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,OAAO,EAAE;QACP,GAAG,eAAe,CAAC,OAAO;QAC1B,SAAS,EAAE,KAAK;KACjB;CACF,CAAC;AAEF,MAAM,kCAAkC,GAAG;;;;;;;;;;;;;;;;;;;;;;;;4FAwBiD,CAAC;AAE7F;;;;GAIG;AACH,MAAM,0BAA0B,GAAG,KAAK,EAAE,QAAa,EAAE,EAAE;IACzD,MAAM,EAAE,YAAY,GAAG,EAAE,EAAE,GAAG,iBAAiB,EAAE,CAAC;IAClD,MAAM,cAAc,GAAG,YAAY,CAAC,YAAY,CAAC;IACjD,MAAM,YAAY,GAChB,OAAO,cAAc,KAAK,QAAQ;QAChC,CAAC,CAAC,cAAc;QAChB,CAAC,CAAC,OAAO,cAAc,KAAK,UAAU;YACpC,CAAC,CAAC,cAAc,CAAC,kCAAkC,CAAC;YACpD,CAAC,CAAC,kCAAkC,CAAC;IAE3C,MAAM,MAAM,GAAG,wBAAwB,CAAC;QACtC,KAAK,EAAE,UAAU;QACjB,OAAO,EAAE,0BAA0B;QACnC,YAAY,EAAE,oBAAoB;QAClC,aAAa,EAAE,qBAAqB;QACpC,YAAY;QACZ,wDAAwD;QACxD,yEAAyE;QACzE,+DAA+D;QAC/D,6EAA6E;KAC9E,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC,CAAC;AAEF,eAAe,0BAA0B,CAAC","sourcesContent":["import { createIntegrationsPlugin } from \"@agent-native/core/server\";\n\nimport { dispatchActions } from \"../../actions/index.js\";\nimport { getDispatchConfig } from \"../index.js\";\nimport {\n beforeDispatchProcess,\n resolveDispatchOwner,\n} from \"../lib/dispatch-integrations.js\";\n\nconst dispatchIntegrationActions = {\n ...dispatchActions,\n // Messaging integrations should use the core call-agent tool for cross-app\n // delegation because it queues A2A continuations when serverless budgets are\n // tight. The MCP-facing ask_app action is still available outside this path.\n ask_app: {\n ...dispatchActions.ask_app,\n agentTool: false,\n },\n};\n\nconst DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for this workspace, responding via a messaging platform integration (Slack, Telegram, email, etc.).\n\nDefault posture:\n- Treat Slack, Telegram, and email as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job. Apps you can delegate to include slides (decks/presentations), analytics (data/dashboards), content (docs/articles), forms (form builder), clips (screen recordings), design (visual designs), and assets (brand libraries plus generated images/videos).\n- Use the available-apps prompt context first, then list-connected-agents when you need fresh details, to see what agents are available before assuming a request must be handled locally.\n- When asked whether workspace apps expose agent cards or A2A endpoints, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.\n- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Reply in the originating thread unless the user explicitly asks you to send to a saved destination.\n\nWhen a user asks for something:\n- If it belongs to analytics, content, slides, clips, assets, etc., delegate via call-agent — do not re-implement the domain logic in dispatch.\n- In messaging integrations, use call-agent for cross-app delegation; do not use ask_app.\n- After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.\n- Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.\n- If the user asks to create, build, make, scaffold, or generate an \"agent\" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.\n- If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized fork/copy.\n- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible \"Chat\", \"Starter\", \"Blank app\", or \"New app\" UI behind.\n- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode \"builder\", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode \"local-agent\", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode \"coming-soon\", say this requires a code change and they can edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like; do not send them to Builder org/beta settings. If it returns mode \"builder-unavailable\", report the action's message exactly enough to preserve the missing identity/credential detail.\n- For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.\n- Keep responses concise and operational — messaging platforms have character limits.\n- Use markdown sparingly (bold and lists are fine, avoid complex formatting).\n- If a task requires many steps, summarize what you did rather than streaming every detail.`;\n\n/**\n * Defer plugin construction until the Nitro plugin actually fires so the\n * config-aware system prompt resolves AFTER `setupDispatch(config)` has\n * stamped the active config (plugin module load order is not guaranteed).\n */\nconst dispatchIntegrationsPlugin = async (nitroApp: any) => {\n const { integrations = {} } = getDispatchConfig();\n const promptOverride = integrations.systemPrompt;\n const systemPrompt =\n typeof promptOverride === \"string\"\n ? promptOverride\n : typeof promptOverride === \"function\"\n ? promptOverride(DISPATCH_INTEGRATION_SYSTEM_PROMPT)\n : DISPATCH_INTEGRATION_SYSTEM_PROMPT;\n\n const plugin = createIntegrationsPlugin({\n appId: \"dispatch\",\n actions: dispatchIntegrationActions,\n resolveOwner: resolveDispatchOwner,\n beforeProcess: beforeDispatchProcess,\n systemPrompt,\n // Inherit the framework default (claude-sonnet-4-6 from\n // packages/core/src/integrations/plugin.ts). Haiku was tried for latency\n // but hallucinated URLs/IDs after delegated call-agent results\n // (e.g. inventing `https://slides.workspace.com/deck/builder-io-deck-2024`).\n });\n\n return plugin(nitroApp);\n};\n\nexport default dispatchIntegrationsPlugin;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/dispatch",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.10",
|
|
4
4
|
"description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"typescript": "^6.0.3",
|
|
95
95
|
"vite": "8.1.0",
|
|
96
96
|
"vitest": "^4.1.5",
|
|
97
|
-
"@agent-native/core": "0.
|
|
97
|
+
"@agent-native/core": "0.91.0"
|
|
98
98
|
},
|
|
99
99
|
"peerDependencies": {
|
|
100
100
|
"@agent-native/core": ">=0.8.0",
|
|
@@ -14,15 +14,15 @@ import {
|
|
|
14
14
|
type LoaderFunctionArgs,
|
|
15
15
|
} from "react-router";
|
|
16
16
|
|
|
17
|
-
import { DispatchShell } from "
|
|
18
|
-
import { Badge } from "
|
|
19
|
-
import { Button } from "
|
|
20
|
-
import { Spinner } from "
|
|
21
|
-
import { resolveServerCatchAllTarget } from "
|
|
17
|
+
import { DispatchShell } from "../../components/dispatch-shell";
|
|
18
|
+
import { Badge } from "../../components/ui/badge";
|
|
19
|
+
import { Button } from "../../components/ui/button";
|
|
20
|
+
import { Spinner } from "../../components/ui/spinner";
|
|
21
|
+
import { resolveServerCatchAllTarget } from "../../lib/catch-all-target";
|
|
22
22
|
import {
|
|
23
23
|
workspaceAppHref,
|
|
24
24
|
type WorkspaceAppSummary,
|
|
25
|
-
} from "
|
|
25
|
+
} from "../../lib/workspace-apps";
|
|
26
26
|
|
|
27
27
|
export function meta() {
|
|
28
28
|
return [{ title: "Workspace app - Dispatch" }];
|
|
@@ -15,9 +15,13 @@ vi.mock("@agent-native/core/org", () => ({
|
|
|
15
15
|
resolveOrgIdForEmail: mocks.resolveOrgIdForEmail,
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
IncomingMessage,
|
|
20
|
+
PlatformAdapter,
|
|
21
|
+
} from "@agent-native/core/server";
|
|
19
22
|
|
|
20
23
|
import {
|
|
24
|
+
beforeDispatchProcess,
|
|
21
25
|
identityKeyForIncoming,
|
|
22
26
|
resolveDispatchOwner,
|
|
23
27
|
} from "./dispatch-integrations.js";
|
|
@@ -54,6 +58,38 @@ function emailIncoming(
|
|
|
54
58
|
};
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
function telegramIncoming(
|
|
62
|
+
overrides: Partial<IncomingMessage> = {},
|
|
63
|
+
): IncomingMessage {
|
|
64
|
+
return {
|
|
65
|
+
platform: "telegram",
|
|
66
|
+
externalThreadId: "12345",
|
|
67
|
+
text: "ask analytics about traffic",
|
|
68
|
+
senderId: "777",
|
|
69
|
+
senderName: "Steve",
|
|
70
|
+
platformContext: { chatId: 12345, fromId: 777, rawText: "hello" },
|
|
71
|
+
timestamp: 1,
|
|
72
|
+
...overrides,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const noopAdapter: PlatformAdapter = {
|
|
77
|
+
platform: "telegram",
|
|
78
|
+
label: "Telegram",
|
|
79
|
+
getRequiredEnvKeys: () => [],
|
|
80
|
+
handleVerification: async () => ({ handled: false }),
|
|
81
|
+
verifyWebhook: async () => true,
|
|
82
|
+
parseIncomingMessage: async () => null,
|
|
83
|
+
sendResponse: async () => {},
|
|
84
|
+
formatAgentResponse: (text: string) => ({ text, platformContext: {} }),
|
|
85
|
+
getStatus: async () => ({
|
|
86
|
+
platform: "telegram",
|
|
87
|
+
label: "Telegram",
|
|
88
|
+
enabled: true,
|
|
89
|
+
configured: true,
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
|
|
57
93
|
beforeEach(() => {
|
|
58
94
|
mocks.resolveLinkedOwner.mockResolvedValue(null);
|
|
59
95
|
mocks.consumeLinkToken.mockResolvedValue("owner@example.test");
|
|
@@ -75,6 +111,10 @@ describe("identityKeyForIncoming", () => {
|
|
|
75
111
|
it("scopes Slack identities by team", () => {
|
|
76
112
|
expect(identityKeyForIncoming(slackIncoming())).toBe("T123:U123");
|
|
77
113
|
});
|
|
114
|
+
|
|
115
|
+
it("uses Telegram sender ids as link identities", () => {
|
|
116
|
+
expect(identityKeyForIncoming(telegramIncoming())).toBe("777");
|
|
117
|
+
});
|
|
78
118
|
});
|
|
79
119
|
|
|
80
120
|
describe("resolveDispatchOwner", () => {
|
|
@@ -184,3 +224,54 @@ describe("resolveDispatchOwner", () => {
|
|
|
184
224
|
).resolves.toBe("victim@member.test");
|
|
185
225
|
});
|
|
186
226
|
});
|
|
227
|
+
|
|
228
|
+
describe("beforeDispatchProcess", () => {
|
|
229
|
+
it("asks unlinked Telegram users to link before using org context", async () => {
|
|
230
|
+
vi.stubEnv("APP_URL", "https://dispatch.agent-native.test");
|
|
231
|
+
|
|
232
|
+
const result = await beforeDispatchProcess(telegramIncoming(), noopAdapter);
|
|
233
|
+
|
|
234
|
+
expect(result).toEqual({
|
|
235
|
+
handled: true,
|
|
236
|
+
responseText:
|
|
237
|
+
"Telegram is connected, but this Telegram account is not linked to an Agent-Native user yet. Tap https://dispatch.agent-native.test/identities, create a Telegram link token, then send `/link <token>` here. After that I can use your Builder.io org and connected apps.",
|
|
238
|
+
});
|
|
239
|
+
expect(mocks.resolveLinkedOwner).toHaveBeenCalledWith("telegram", "777", {
|
|
240
|
+
allowAnyOrgFallback: true,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("lets linked Telegram users proceed to normal agent processing", async () => {
|
|
245
|
+
mocks.resolveLinkedOwner.mockResolvedValueOnce("steve@builder.io");
|
|
246
|
+
|
|
247
|
+
await expect(
|
|
248
|
+
beforeDispatchProcess(telegramIncoming(), noopAdapter),
|
|
249
|
+
).resolves.toEqual({ handled: false });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("still consumes Telegram link commands before enforcing the link gate", async () => {
|
|
253
|
+
const result = await beforeDispatchProcess(
|
|
254
|
+
telegramIncoming({
|
|
255
|
+
text: "token-123",
|
|
256
|
+
platformContext: {
|
|
257
|
+
chatId: 12345,
|
|
258
|
+
fromId: 777,
|
|
259
|
+
rawText: "/link token-123",
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
noopAdapter,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
expect(result).toEqual({
|
|
266
|
+
handled: true,
|
|
267
|
+
responseText:
|
|
268
|
+
"Linked successfully. Future telegram messages will use owner@example.test's personal dispatch context.",
|
|
269
|
+
});
|
|
270
|
+
expect(mocks.consumeLinkToken).toHaveBeenCalledWith({
|
|
271
|
+
platform: "telegram",
|
|
272
|
+
token: "token-123",
|
|
273
|
+
externalUserId: "777",
|
|
274
|
+
externalUserName: "Steve",
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { resolveOrgIdForEmail } from "@agent-native/core/org";
|
|
4
|
+
import { withConfiguredAppBasePath } from "@agent-native/core/server";
|
|
4
5
|
import type {
|
|
5
6
|
IncomingMessage,
|
|
6
7
|
PlatformAdapter,
|
|
@@ -77,6 +78,40 @@ function configuredDefaultOwnerForIncoming(
|
|
|
77
78
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : null;
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
function platformRequiresExplicitLink(incoming: IncomingMessage): boolean {
|
|
82
|
+
// Telegram does not provide a verified email address. Require an explicit
|
|
83
|
+
// identity link before it can act as a Builder/Agent-Native user.
|
|
84
|
+
return incoming.platform === "telegram";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function configuredDispatchIdentitiesUrl(): string | null {
|
|
88
|
+
const raw =
|
|
89
|
+
process.env.WORKSPACE_GATEWAY_URL ||
|
|
90
|
+
process.env.APP_URL ||
|
|
91
|
+
process.env.URL ||
|
|
92
|
+
process.env.DEPLOY_URL ||
|
|
93
|
+
process.env.BETTER_AUTH_URL ||
|
|
94
|
+
"";
|
|
95
|
+
if (!raw.trim()) return null;
|
|
96
|
+
const base = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
97
|
+
try {
|
|
98
|
+
return new URL(
|
|
99
|
+
"identities",
|
|
100
|
+
`${withConfiguredAppBasePath(base).replace(/\/+$/, "")}/`,
|
|
101
|
+
).toString();
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatTelegramLinkRequiredMessage(): string {
|
|
108
|
+
const identitiesUrl = configuredDispatchIdentitiesUrl();
|
|
109
|
+
const linkStep = identitiesUrl
|
|
110
|
+
? `Tap ${identitiesUrl}, create a Telegram link token, then send \`/link <token>\` here.`
|
|
111
|
+
: "Open Dispatch while signed in, create a Telegram link token, then send `/link <token>` here.";
|
|
112
|
+
return `Telegram is connected, but this Telegram account is not linked to an Agent-Native user yet. ${linkStep} After that I can use your Builder.io org and connected apps.`;
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
async function resolveSlackSenderProfile(
|
|
81
116
|
incoming: IncomingMessage,
|
|
82
117
|
): Promise<SlackSenderProfile> {
|
|
@@ -228,6 +263,19 @@ export async function beforeDispatchProcess(
|
|
|
228
263
|
contextString(incoming.platformContext.rawText) || trimmed;
|
|
229
264
|
const match = commandText.match(/^\/link(?:@\w+)?\s+([a-zA-Z0-9_-]+)$/);
|
|
230
265
|
if (!match) {
|
|
266
|
+
if (platformRequiresExplicitLink(incoming)) {
|
|
267
|
+
const owner = await resolveLinkedOwner(
|
|
268
|
+
incoming.platform,
|
|
269
|
+
identityKeyForIncoming(incoming),
|
|
270
|
+
{ allowAnyOrgFallback: true },
|
|
271
|
+
);
|
|
272
|
+
if (!owner) {
|
|
273
|
+
return {
|
|
274
|
+
handled: true,
|
|
275
|
+
responseText: formatTelegramLinkRequiredMessage(),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
231
279
|
return handleRemoteCodeCommand(incoming, adapter, {
|
|
232
280
|
resolveOwner: () => resolveDispatchOwner(incoming),
|
|
233
281
|
});
|
|
@@ -7,6 +7,17 @@ import {
|
|
|
7
7
|
resolveDispatchOwner,
|
|
8
8
|
} from "../lib/dispatch-integrations.js";
|
|
9
9
|
|
|
10
|
+
const dispatchIntegrationActions = {
|
|
11
|
+
...dispatchActions,
|
|
12
|
+
// Messaging integrations should use the core call-agent tool for cross-app
|
|
13
|
+
// delegation because it queues A2A continuations when serverless budgets are
|
|
14
|
+
// tight. The MCP-facing ask_app action is still available outside this path.
|
|
15
|
+
ask_app: {
|
|
16
|
+
...dispatchActions.ask_app,
|
|
17
|
+
agentTool: false,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
10
21
|
const DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for this workspace, responding via a messaging platform integration (Slack, Telegram, email, etc.).
|
|
11
22
|
|
|
12
23
|
Default posture:
|
|
@@ -21,6 +32,7 @@ Default posture:
|
|
|
21
32
|
|
|
22
33
|
When a user asks for something:
|
|
23
34
|
- If it belongs to analytics, content, slides, clips, assets, etc., delegate via call-agent — do not re-implement the domain logic in dispatch.
|
|
35
|
+
- In messaging integrations, use call-agent for cross-app delegation; do not use ask_app.
|
|
24
36
|
- After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.
|
|
25
37
|
- Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.
|
|
26
38
|
- If the user asks to create, build, make, scaffold, or generate an "agent" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.
|
|
@@ -49,7 +61,7 @@ const dispatchIntegrationsPlugin = async (nitroApp: any) => {
|
|
|
49
61
|
|
|
50
62
|
const plugin = createIntegrationsPlugin({
|
|
51
63
|
appId: "dispatch",
|
|
52
|
-
actions:
|
|
64
|
+
actions: dispatchIntegrationActions,
|
|
53
65
|
resolveOwner: resolveDispatchOwner,
|
|
54
66
|
beforeProcess: beforeDispatchProcess,
|
|
55
67
|
systemPrompt,
|