@cosmoops/connect-sdk 0.15.0 → 0.16.0
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/CHANGELOG.md +69 -0
- package/README.md +2 -1
- package/dist/cjs/client/components/ai-screens.js +73 -0
- package/dist/cjs/client/components/ai-widgets.js +37 -0
- package/dist/cjs/client/components/ask-ai.js +31 -5
- package/dist/cjs/client/index.js +5 -2
- package/dist/cjs/index.js +6 -1
- package/dist/cjs/mcp/index.js +90 -0
- package/dist/cjs/mcp/tools.js +369 -0
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/handler.js +7 -0
- package/dist/cjs/server/resources/ai.js +8 -1
- package/dist/cjs/store-schema.js +66 -0
- package/dist/esm/client/components/ai-screens.js +69 -0
- package/dist/esm/client/components/ai-widgets.js +37 -1
- package/dist/esm/client/components/ask-ai.js +32 -6
- package/dist/esm/client/index.js +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/mcp/index.js +85 -0
- package/dist/esm/mcp/tools.js +366 -0
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/handler.js +7 -0
- package/dist/esm/server/resources/ai.js +8 -1
- package/dist/esm/store-schema.js +62 -0
- package/dist/types/client/components/ai-screens.d.ts +25 -0
- package/dist/types/client/components/ai-widgets.d.ts +34 -1
- package/dist/types/client/components/ask-ai.d.ts +47 -1
- package/dist/types/client/index.d.ts +2 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/mcp/index.d.ts +74 -0
- package/dist/types/mcp/tools.d.ts +107 -0
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/resources/ai.d.ts +11 -1
- package/dist/types/store-schema.d.ts +52 -0
- package/dist/types/types.d.ts +50 -1
- package/package.json +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,75 @@
|
|
|
3
3
|
`@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
|
|
4
4
|
versions may carry breaking changes** and patch versions never do.
|
|
5
5
|
|
|
6
|
+
## 0.16.0
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`@cosmoops/connect-sdk/mcp` - your store declaration, as MCP tools.**
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createStoreToolset } from "@cosmoops/connect-sdk/mcp";
|
|
14
|
+
|
|
15
|
+
const toolset = createStoreToolset({
|
|
16
|
+
collections: STORE_SCHEMA, // the array you already seed to Connect
|
|
17
|
+
data: connect.data,
|
|
18
|
+
workspaceId: session.workspaceId,
|
|
19
|
+
asUser: session.sessionToken, // every call runs as this user
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
server.setRequestHandler(ListToolsRequestSchema, () => toolset.listTools());
|
|
23
|
+
server.setRequestHandler(CallToolRequestSchema, (request) =>
|
|
24
|
+
toolset.callTool(request.params.name, request.params.arguments ?? {}),
|
|
25
|
+
);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`Application.storeSchema` already says what each collection holds, which
|
|
29
|
+
fields may be filtered on, which query shapes the database can serve, and
|
|
30
|
+
whether it refuses everything but a create. Writing tools over that by hand
|
|
31
|
+
means keeping a second copy of all of it, and the copy is wrong the first time
|
|
32
|
+
a filter is added to a screen. So they are derived: adding a queryable field
|
|
33
|
+
and re-seeding widens the tools on the next boot, with no tool code to touch.
|
|
34
|
+
|
|
35
|
+
Three read tools per collection (`_get`, `_list`, `_aggregate`), and writes
|
|
36
|
+
only if you ask - `writes: "create" | "all"`, defaulting to `"none"`. An
|
|
37
|
+
`appendOnly` collection never derives an update or a delete, because Connect
|
|
38
|
+
would refuse them anyway and a tool that can only fail is worse than no tool.
|
|
39
|
+
|
|
40
|
+
Each tool's description carries the collection's queryable fields and its
|
|
41
|
+
declared composite indexes, and an unservable query is refused locally with
|
|
42
|
+
the index it would need - so a model adapts instead of spending a turn
|
|
43
|
+
discovering the shape.
|
|
44
|
+
|
|
45
|
+
**Every call carries an end user's session token.** Connect resolves it to a
|
|
46
|
+
member and checks the collection's declared `readPermission`/`writePermission`
|
|
47
|
+
against that member's live roles, so a toolset never reaches further than the
|
|
48
|
+
person it runs for, and a permission revoked in the console is gone from the
|
|
49
|
+
next call. Build one per request from `getConnectSession()`, or pass a
|
|
50
|
+
function for `asUser`.
|
|
51
|
+
|
|
52
|
+
There is deliberately **no scan tool**. `data.scan` reads across every
|
|
53
|
+
workspace and for that reason takes no `asUser` - deriving one would mean that
|
|
54
|
+
single call escalating to your API key while every tool beside it ran as the
|
|
55
|
+
user. Cross-tenant reads belong behind their own gate.
|
|
56
|
+
|
|
57
|
+
Server-only, like `/server` and `/next`: a browser import resolves to the
|
|
58
|
+
guard stub and fails the build. **No new dependency** - `listTools()` and
|
|
59
|
+
`callTool()` answer in the shapes the MCP protocol defines, so the official
|
|
60
|
+
MCP SDK stays yours to install and version.
|
|
61
|
+
|
|
62
|
+
**Pass `include`.** Measured on the reference applications, deriving over
|
|
63
|
+
every collection yields 45 read tools for club and 21 for CRM - whole-catalogue
|
|
64
|
+
numbers, and a model offered them chooses worse than one offered a handful.
|
|
65
|
+
Name the collections the surface at hand is about. Where a collection declares
|
|
66
|
+
more index shapes than are worth listing (club's ledger declares around
|
|
67
|
+
forty), the description states the rule and leaves the specifics to the
|
|
68
|
+
refusal, which names the exact index a rejected query needed.
|
|
69
|
+
|
|
70
|
+
- **`StoreCollectionDeclaration`, `describeQueryRejection` and
|
|
71
|
+
`storeIndexCovers`** on the root entry. The declaration type is the shape
|
|
72
|
+
Connect stores on `Application.storeSchema`, so an app deriving its schema
|
|
73
|
+
from its own query matrix can stop declaring a local copy of it.
|
|
74
|
+
|
|
6
75
|
## 0.15.0
|
|
7
76
|
|
|
8
77
|
Your application's own database stops being a toy. Real queries, real
|
package/README.md
CHANGED
|
@@ -12,13 +12,14 @@ npm install @cosmoops/connect-sdk
|
|
|
12
12
|
npm install streamdown # optional — Ask AI's markdown renderer, see step 4
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Five entry points, and the split is a security boundary, not a preference:
|
|
16
16
|
|
|
17
17
|
| Import | Runs in | Holds the API key |
|
|
18
18
|
| --- | --- | --- |
|
|
19
19
|
| `@cosmoops/connect-sdk/server` | your backend | **yes** |
|
|
20
20
|
| `@cosmoops/connect-sdk/client` | the browser | never |
|
|
21
21
|
| `@cosmoops/connect-sdk/next` | your backend | **yes** (Next.js plug-and-play) |
|
|
22
|
+
| `@cosmoops/connect-sdk/mcp` | your backend | **yes** (store, as MCP tools) |
|
|
22
23
|
| `@cosmoops/connect-sdk` | anywhere | types + `ConnectError` only |
|
|
23
24
|
|
|
24
25
|
The browser half never sees a credential. It calls your own origin, and
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.matchConnectScreen = matchConnectScreen;
|
|
4
|
+
exports.isLinkableConnectScreen = isLinkableConnectScreen;
|
|
5
|
+
/**
|
|
6
|
+
* Matching a live path against the screens your application declared.
|
|
7
|
+
*
|
|
8
|
+
* The same rule Connect uses for its own routes, and it has to be here too
|
|
9
|
+
* because only this side knows what your app's routes are: a dynamic segment
|
|
10
|
+
* matches exactly one path segment, and where several patterns fit, the most
|
|
11
|
+
* *literal* wins - so `/deals/new` resolves to itself rather than to
|
|
12
|
+
* `/deals/[dealId]` merely because that also fits.
|
|
13
|
+
*
|
|
14
|
+
* Plain functions with no React and no imports beyond the type, so the panel,
|
|
15
|
+
* the action links and any host that wants to reuse the matching can share one
|
|
16
|
+
* implementation.
|
|
17
|
+
*/
|
|
18
|
+
/** Trims the query string, the hash and any trailing slash. */
|
|
19
|
+
function normalise(path) {
|
|
20
|
+
const withoutQuery = path.split(/[?#]/)[0] ?? "";
|
|
21
|
+
return withoutQuery.length > 1 ? withoutQuery.replace(/\/+$/, "") : withoutQuery;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The declared screen a live path is on, or `null` if none matches.
|
|
25
|
+
*
|
|
26
|
+
* `null` is a normal outcome, not an error: an application need only describe
|
|
27
|
+
* the screens it wants the assistant to know about, and an undescribed one
|
|
28
|
+
* simply costs the assistant its orientation rather than costing the user an
|
|
29
|
+
* answer.
|
|
30
|
+
*/
|
|
31
|
+
function matchConnectScreen(screens, path) {
|
|
32
|
+
const segments = normalise(path).split("/").filter(Boolean);
|
|
33
|
+
let best = null;
|
|
34
|
+
for (const screen of screens) {
|
|
35
|
+
const pattern = normalise(screen.path).split("/").filter(Boolean);
|
|
36
|
+
if (pattern.length !== segments.length)
|
|
37
|
+
continue;
|
|
38
|
+
let literals = 0;
|
|
39
|
+
let matches = true;
|
|
40
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
41
|
+
const part = pattern[i];
|
|
42
|
+
if (part.startsWith("[") && part.endsWith("]"))
|
|
43
|
+
continue;
|
|
44
|
+
if (part !== segments[i]) {
|
|
45
|
+
matches = false;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
literals++;
|
|
49
|
+
}
|
|
50
|
+
if (matches && (!best || literals > best.literals))
|
|
51
|
+
best = { screen, literals };
|
|
52
|
+
}
|
|
53
|
+
return best?.screen ?? null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether an href the model produced may be rendered as a link.
|
|
57
|
+
*
|
|
58
|
+
* Same reasoning as Connect's own `showActions` check, and the same conclusion:
|
|
59
|
+
* an assistant that can emit an arbitrary URL is a redirect the model can be
|
|
60
|
+
* talked into, and retrieved content shares a context window with its
|
|
61
|
+
* tool-calling authority. So a link is drawn only when it resolves to a screen
|
|
62
|
+
* *your application declared* - which makes an external destination structurally
|
|
63
|
+
* impossible, since no route pattern has a scheme or a host.
|
|
64
|
+
*
|
|
65
|
+
* The protocol-relative form is refused separately: `//evil.example.com` has no
|
|
66
|
+
* scheme for a path matcher to trip over, and would otherwise reach an anchor
|
|
67
|
+
* as an absolute destination.
|
|
68
|
+
*/
|
|
69
|
+
function isLinkableConnectScreen(screens, href) {
|
|
70
|
+
if (!href.startsWith("/") || href.startsWith("//"))
|
|
71
|
+
return false;
|
|
72
|
+
return matchConnectScreen(screens, href) !== null;
|
|
73
|
+
}
|
|
@@ -8,10 +8,12 @@ exports.AiTimeline = AiTimeline;
|
|
|
8
8
|
exports.AiDetails = AiDetails;
|
|
9
9
|
exports.AiSteps = AiSteps;
|
|
10
10
|
exports.AiSources = AiSources;
|
|
11
|
+
exports.AiActions = AiActions;
|
|
11
12
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
12
13
|
const react_1 = require("react");
|
|
13
14
|
const react_2 = require("@heroui/react");
|
|
14
15
|
const lucide_react_1 = require("lucide-react");
|
|
16
|
+
const ai_screens_js_1 = require("./ai-screens.js");
|
|
15
17
|
const ai_format_js_1 = require("./ai-format.js");
|
|
16
18
|
/**
|
|
17
19
|
* What Ask AI draws.
|
|
@@ -230,3 +232,38 @@ function AiSources({ sources }) {
|
|
|
230
232
|
return ((0, jsx_runtime_1.jsx)("li", { children: source.uri ? ((0, jsx_runtime_1.jsxs)("a", { href: source.uri, target: "_blank", rel: "noopener noreferrer", className: "flex gap-1.5 rounded-sm hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", children: [label, (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "mt-0.5 size-3 shrink-0 text-muted", "aria-hidden": true })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "flex gap-1.5", children: label })) }, `${source.uri ?? source.title}-${index}`));
|
|
231
233
|
}) })] }));
|
|
232
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Where to go next, as links into *your* application.
|
|
237
|
+
*
|
|
238
|
+
* ## Why links and not navigation
|
|
239
|
+
*
|
|
240
|
+
* The obvious version of "the assistant can act" moves the user. It is also the
|
|
241
|
+
* version that breaks the property the whole design rests on: nothing happens
|
|
242
|
+
* without a click. Retrieved content shares a context window with the model's
|
|
243
|
+
* tool-calling authority, so an auto-navigate is a redirect primitive that
|
|
244
|
+
* content can reach. A link cannot be, and loses nothing - the user was going
|
|
245
|
+
* to click something anyway.
|
|
246
|
+
*
|
|
247
|
+
* ## Why every href is checked against your declared screens
|
|
248
|
+
*
|
|
249
|
+
* The model can emit any string. Only your application knows which strings are
|
|
250
|
+
* real routes, which is why `screens` is a prop rather than something Connect
|
|
251
|
+
* could supply. A link that does not resolve is *dropped* rather than rendered
|
|
252
|
+
* broken - the prose around it still stands, so a missing link reads as terse
|
|
253
|
+
* rather than wrong.
|
|
254
|
+
*
|
|
255
|
+
* `renderLink` is how this stays router-agnostic. Given one, a link is your
|
|
256
|
+
* framework's - a Next `<Link>`, a TanStack `<Link>` - and navigation is a
|
|
257
|
+
* client transition. Without one it falls back to an anchor, which works
|
|
258
|
+
* everywhere and costs a document load.
|
|
259
|
+
*/
|
|
260
|
+
function AiActions({ title, actions, screens, renderLink, }) {
|
|
261
|
+
const resolved = actions.filter((action) => (0, ai_screens_js_1.isLinkableConnectScreen)(screens, action.href));
|
|
262
|
+
if (resolved.length === 0)
|
|
263
|
+
return null;
|
|
264
|
+
const className = "group flex items-center gap-2 rounded-lg border border-separator bg-surface px-3 py-2 no-underline transition-colors hover:border-accent";
|
|
265
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "my-3", children: [title ? (0, jsx_runtime_1.jsx)("p", { className: "mb-1.5 text-xs font-medium text-muted", children: title }) : null, (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-1.5", children: resolved.map((action) => {
|
|
266
|
+
const body = ((0, jsx_runtime_1.jsxs)("span", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("span", { className: "block truncate text-xs font-medium text-foreground", children: action.label }), action.detail ? ((0, jsx_runtime_1.jsx)("span", { className: "block truncate text-[11px] text-muted", children: action.detail })) : null] }));
|
|
267
|
+
return ((0, jsx_runtime_1.jsx)(react_1.Fragment, { children: renderLink ? (renderLink({ href: action.href, className, children: body })) : ((0, jsx_runtime_1.jsx)("a", { href: action.href, className: className, children: body })) }, action.href));
|
|
268
|
+
}) })] }));
|
|
269
|
+
}
|
|
@@ -8,6 +8,8 @@ const react_2 = require("@heroui/react");
|
|
|
8
8
|
const lucide_react_1 = require("lucide-react");
|
|
9
9
|
const streamdown_1 = require("streamdown");
|
|
10
10
|
const provider_js_1 = require("../provider.js");
|
|
11
|
+
const use_active_path_js_1 = require("../use-active-path.js");
|
|
12
|
+
const ai_screens_js_1 = require("./ai-screens.js");
|
|
11
13
|
const ai_widgets_js_1 = require("./ai-widgets.js");
|
|
12
14
|
/**
|
|
13
15
|
* The panel's open width. The *inner* pane always carries it, so the content
|
|
@@ -66,7 +68,7 @@ function ThinkingDisclosure({ text, isStreaming }) {
|
|
|
66
68
|
*/
|
|
67
69
|
const isArray = (value) => Array.isArray(value) && value.length > 0;
|
|
68
70
|
const field = (input, key) => typeof input === "object" && input !== null ? input[key] : undefined;
|
|
69
|
-
function renderTool(name, input, renderMarkdown) {
|
|
71
|
+
function renderTool(name, input, renderMarkdown, screens, renderLink) {
|
|
70
72
|
switch (name) {
|
|
71
73
|
case "showTable":
|
|
72
74
|
return isArray(field(input, "columns")) && Array.isArray(field(input, "rows")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiTable, { ...input })) : null;
|
|
@@ -82,6 +84,12 @@ function renderTool(name, input, renderMarkdown) {
|
|
|
82
84
|
return isArray(field(input, "steps")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSteps, { ...input, renderDetail: renderMarkdown })) : null;
|
|
83
85
|
case "showSources":
|
|
84
86
|
return isArray(field(input, "sources")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSources, { ...input }) : null;
|
|
87
|
+
case "showActions":
|
|
88
|
+
// Every href is checked against the declared screens before anything is
|
|
89
|
+
// drawn - see `AiActions`. With none declared, nothing resolves and the
|
|
90
|
+
// tool renders nothing, which is the correct outcome rather than a
|
|
91
|
+
// collection of dead links.
|
|
92
|
+
return isArray(field(input, "actions")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiActions, { ...input, screens: screens, renderLink: renderLink })) : null;
|
|
85
93
|
default:
|
|
86
94
|
return null;
|
|
87
95
|
}
|
|
@@ -223,7 +231,7 @@ function appendText(blocks, text) {
|
|
|
223
231
|
* Neither is something the browser can discover, so both surface as a message
|
|
224
232
|
* in the panel rather than as a hidden button.
|
|
225
233
|
*/
|
|
226
|
-
function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placeholder = "Ask a question...", hint = "Ask a question about your workspace - answers are grounded in what your team has indexed.", defaultOpen = false, markdownPlugins, }) {
|
|
234
|
+
function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placeholder = "Ask a question...", hint = "Ask a question about your workspace - answers are grounded in what your team has indexed.", defaultOpen = false, markdownPlugins, screens = [], activePath, renderLink, }) {
|
|
227
235
|
const { session, isLoading, streamResource } = (0, provider_js_1.useConnect)();
|
|
228
236
|
const [isOpen, setIsOpen] = (0, react_1.useState)(defaultOpen);
|
|
229
237
|
const [turns, setTurns] = (0, react_1.useState)([]);
|
|
@@ -242,6 +250,16 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
|
|
|
242
250
|
// A conversation left mid-stream should stop costing tokens the moment the
|
|
243
251
|
// panel goes away.
|
|
244
252
|
(0, react_1.useEffect)(() => () => abortRef.current?.abort(), []);
|
|
253
|
+
/**
|
|
254
|
+
* The declared screen the user is on, if any.
|
|
255
|
+
*
|
|
256
|
+
* Drives the opening chips here and travels with each message so the answer
|
|
257
|
+
* is written from where they are standing. `null` whenever nothing matches -
|
|
258
|
+
* an application need only describe the screens it wants the assistant to
|
|
259
|
+
* know about.
|
|
260
|
+
*/
|
|
261
|
+
const resolvedPath = (0, use_active_path_js_1.useActivePath)(activePath);
|
|
262
|
+
const currentScreen = (0, react_1.useMemo)(() => (resolvedPath ? (0, ai_screens_js_1.matchConnectScreen)(screens, resolvedPath) : null), [screens, resolvedPath]);
|
|
245
263
|
const renderMarkdown = (0, react_1.useCallback)((text, isStreaming = false) => ((0, jsx_runtime_1.jsx)(streamdown_1.Streamdown, { className: "connect-ai-body", mode: isStreaming ? "streaming" : "static", isAnimating: isStreaming, animated: ANIMATED, caret: isStreaming ? "block" : undefined, plugins: markdownPlugins,
|
|
246
264
|
// Retrieved documents cite real URLs; an interstitial on every one of
|
|
247
265
|
// them turns a citation into a chore. The renderer still sanitises the
|
|
@@ -286,7 +304,15 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
|
|
|
286
304
|
try {
|
|
287
305
|
// Relative to the provider's own `basePath` - `streamResource` prefixes
|
|
288
306
|
// it, the same way every other bridge call is addressed.
|
|
289
|
-
const response = await streamResource("/ai/chat", {
|
|
307
|
+
const response = await streamResource("/ai/chat", {
|
|
308
|
+
messages: history,
|
|
309
|
+
// What the assistant is told about where the user is, and what your
|
|
310
|
+
// application's screens are. Both are prose: Connect uses them to
|
|
311
|
+
// write a better answer and to validate action links, never to
|
|
312
|
+
// decide what anyone may do. See `ConnectScreen`.
|
|
313
|
+
...(resolvedPath ? { screenContext: { pathname: resolvedPath } } : {}),
|
|
314
|
+
...(screens.length > 0 ? { screens } : {}),
|
|
315
|
+
}, { signal: controller.signal });
|
|
290
316
|
// The assistant's turn is appended only once the stream is live, so a
|
|
291
317
|
// failed request leaves an error under the question rather than an
|
|
292
318
|
// empty bubble that never fills.
|
|
@@ -333,7 +359,7 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
|
|
|
333
359
|
// The user's own turn is shown as they typed it - running a
|
|
334
360
|
// question that happens to start with `#` through the markdown
|
|
335
361
|
// renderer would turn it into a heading.
|
|
336
|
-
(0, jsx_runtime_1.jsx)("div", { children: turn.role === "user" ? ((0, jsx_runtime_1.jsx)("p", { className: "whitespace-pre-wrap", children: block.text })) : (renderMarkdown(block.text, isLive && index === lastTextIndex)) }, index)) : block.input === null ? ((0, jsx_runtime_1.jsx)(ToolSkeleton, {}, block.id)) : ((0, jsx_runtime_1.jsx)("div", { children: renderTool(block.name, block.input, renderMarkdown) }, block.id))), isLive && lastTextIndex === -1 && turn.blocks.length === 0 && (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] }, turn.id));
|
|
362
|
+
(0, jsx_runtime_1.jsx)("div", { children: turn.role === "user" ? ((0, jsx_runtime_1.jsx)("p", { className: "whitespace-pre-wrap", children: block.text })) : (renderMarkdown(block.text, isLive && index === lastTextIndex)) }, index)) : block.input === null ? ((0, jsx_runtime_1.jsx)(ToolSkeleton, {}, block.id)) : ((0, jsx_runtime_1.jsx)("div", { children: renderTool(block.name, block.input, renderMarkdown, screens, renderLink) }, block.id))), isLive && lastTextIndex === -1 && turn.blocks.length === 0 && (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] }, turn.id));
|
|
337
363
|
}), [turns, isAnswering, lastTurn?.id, assistantName, renderMarkdown]);
|
|
338
364
|
// Signed out there is no workspace to ground an answer in, and the bridge
|
|
339
365
|
// would answer 401 - so there is nothing to offer, not even a button.
|
|
@@ -347,7 +373,7 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
|
|
|
347
373
|
setInput("");
|
|
348
374
|
void send(text);
|
|
349
375
|
};
|
|
350
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isOpen && ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "primary", "aria-label": title, onPress: () => setIsOpen(true), className: "fixed bottom-4 end-4 z-30 size-12 rounded-full shadow-lg sm:bottom-6 sm:end-6", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)("div", { className: `fixed inset-0 z-40 bg-backdrop transition-opacity lg:hidden ${isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`, onClick: () => setIsOpen(false), "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("aside", { "aria-hidden": !isOpen, className: `fixed inset-y-0 end-0 z-50 flex flex-col border-s border-border bg-surface shadow-xl transition-transform duration-300 ease-in-out ${panelShellWidth(isOpen)} ${isOpen ? "translate-x-0" : "translate-x-full rtl:-translate-x-full"} lg:static lg:z-auto lg:translate-x-0 lg:shadow-none lg:transition-[width]`, children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-full shrink-0 flex-col ${PANEL_WIDTH}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 border-b border-border px-4 py-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 shrink-0 text-accent" }), (0, jsx_runtime_1.jsx)("span", { className: "flex-1 text-sm font-semibold", children: title }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Close", onPress: () => setIsOpen(false), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "size-4" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [turns.length === 0 && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: hint }), rendered, isBusy && lastTurn?.role === "user" && ((0, jsx_runtime_1.jsxs)("div", { className: "rounded-md border border-border bg-accent/5 p-2.5 text-sm", children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-1 text-[11px] font-semibold tracking-wide text-muted uppercase", children: assistantName }), (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] })), error && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-danger", children: error }), (0, jsx_runtime_1.jsx)("div", { ref: bottomRef })] }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex gap-2 border-t border-border p-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", className: "flex-1", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { className: "sr-only", children: placeholder }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: placeholder, value: input, onChange: (event) => setInput(event.currentTarget.value), disabled: isBusy })] }), isBusy ? ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", isIconOnly: true, variant: "secondary", "aria-label": "Stop generating", onPress: stop, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Square, { className: "size-3.5" }) })) : ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", isIconOnly: true, variant: "primary", "aria-label": "Send", isDisabled: !input.trim(), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Send, { className: "size-3.5" }) }))] })] }) })] }));
|
|
376
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isOpen && ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "primary", "aria-label": title, onPress: () => setIsOpen(true), className: "fixed bottom-4 end-4 z-30 size-12 rounded-full shadow-lg sm:bottom-6 sm:end-6", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)("div", { className: `fixed inset-0 z-40 bg-backdrop transition-opacity lg:hidden ${isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`, onClick: () => setIsOpen(false), "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("aside", { "aria-hidden": !isOpen, className: `fixed inset-y-0 end-0 z-50 flex flex-col border-s border-border bg-surface shadow-xl transition-transform duration-300 ease-in-out ${panelShellWidth(isOpen)} ${isOpen ? "translate-x-0" : "translate-x-full rtl:-translate-x-full"} lg:static lg:z-auto lg:translate-x-0 lg:shadow-none lg:transition-[width]`, children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-full shrink-0 flex-col ${PANEL_WIDTH}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 border-b border-border px-4 py-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 shrink-0 text-accent" }), (0, jsx_runtime_1.jsx)("span", { className: "flex-1 text-sm font-semibold", children: title }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Close", onPress: () => setIsOpen(false), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "size-4" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [turns.length === 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "space-y-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: hint }), currentScreen?.suggestions?.length ? ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap gap-1.5", children: currentScreen.suggestions.map((suggestion) => ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "secondary", className: "h-auto rounded-full px-2.5 py-1 text-start text-xs whitespace-normal", isDisabled: isBusy, onPress: () => void send(suggestion), children: suggestion }, suggestion))) })) : null] })), rendered, isBusy && lastTurn?.role === "user" && ((0, jsx_runtime_1.jsxs)("div", { className: "rounded-md border border-border bg-accent/5 p-2.5 text-sm", children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-1 text-[11px] font-semibold tracking-wide text-muted uppercase", children: assistantName }), (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] })), error && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-danger", children: error }), (0, jsx_runtime_1.jsx)("div", { ref: bottomRef })] }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex gap-2 border-t border-border p-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", className: "flex-1", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { className: "sr-only", children: placeholder }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: placeholder, value: input, onChange: (event) => setInput(event.currentTarget.value), disabled: isBusy })] }), isBusy ? ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", isIconOnly: true, variant: "secondary", "aria-label": "Stop generating", onPress: stop, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Square, { className: "size-3.5" }) })) : ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", isIconOnly: true, variant: "primary", "aria-label": "Send", isDisabled: !input.trim(), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Send, { className: "size-3.5" }) }))] })] }) })] }));
|
|
351
377
|
}
|
|
352
378
|
/** Hoisted for the same reference-equality reason as `ANIMATED` - Streamdown compares it by identity. */
|
|
353
379
|
const LINK_SAFETY = { enabled: false };
|
package/dist/cjs/client/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.normalizePath = exports.flattenNav = exports.normalizeNav = exports.useActivePath = exports.ConnectSideNav = exports.ConnectSidebarNav = exports.RAIL_EXPANDED = exports.RAIL_COLLAPSED = exports.sidebarRowClass = exports.useConnectShell = exports.ConnectShellProvider = exports.ConnectSidebarRow = exports.ConnectSidebarFooter = exports.ConnectSidebarBody = exports.ConnectSidebarHeader = exports.ConnectSidebar = exports.AppShell = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectProfileModal = exports.ConnectUserButton = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.ConnectGrievanceNotice = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectCompanyProfile = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectPlatformAccess = exports.useConnectAudience = exports.useConnectWorkspaceType = exports.useConnectUser = exports.useConnect = exports.ConnectProvider = void 0;
|
|
5
|
-
exports.
|
|
6
|
-
exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = void 0;
|
|
5
|
+
exports.annualSaving = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectFilterTabs = exports.ConnectCursorPagination = exports.ConnectConfirmDialog = exports.ConnectLinkButton = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.ConnectPlatformGate = exports.ConnectWorkspaceTypeGate = exports.useConnectFeature = exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.AI_TONE_DOT = exports.AI_TONE_CHIP = exports.AI_EMPTY_VALUE = exports.aiStatusTone = exports.isNumericAiFormat = exports.formatAiValue = exports.AiSources = exports.AiSteps = exports.AiDetails = exports.AiTimeline = exports.AiMetrics = exports.AiChart = exports.AiTable = exports.ConnectAskAi = exports.ConnectAppsGrid = exports.isLinkableConnectScreen = exports.matchConnectScreen = exports.filterNavForAudience = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = void 0;
|
|
6
|
+
exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.ACTION_LABEL = exports.discountLabel = void 0;
|
|
7
7
|
/**
|
|
8
8
|
* Browser half of `@cosmoops/connect-sdk` - React components and hooks.
|
|
9
9
|
*
|
|
@@ -114,6 +114,9 @@ Object.defineProperty(exports, "resolveActivePath", { enumerable: true, get: fun
|
|
|
114
114
|
Object.defineProperty(exports, "isNavItemActive", { enumerable: true, get: function () { return nav_js_1.isNavItemActive; } });
|
|
115
115
|
Object.defineProperty(exports, "gateExemptPaths", { enumerable: true, get: function () { return nav_js_1.gateExemptPaths; } });
|
|
116
116
|
Object.defineProperty(exports, "filterNavForAudience", { enumerable: true, get: function () { return nav_js_1.filterNavForAudience; } });
|
|
117
|
+
var ai_screens_js_1 = require("./components/ai-screens.js");
|
|
118
|
+
Object.defineProperty(exports, "matchConnectScreen", { enumerable: true, get: function () { return ai_screens_js_1.matchConnectScreen; } });
|
|
119
|
+
Object.defineProperty(exports, "isLinkableConnectScreen", { enumerable: true, get: function () { return ai_screens_js_1.isLinkableConnectScreen; } });
|
|
117
120
|
var apps_grid_js_1 = require("./components/apps-grid.js");
|
|
118
121
|
Object.defineProperty(exports, "ConnectAppsGrid", { enumerable: true, get: function () { return apps_grid_js_1.ConnectAppsGrid; } });
|
|
119
122
|
// `AppShell askAi` mounts this; exported for an app that wants the panel
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.unchangedSince = exports.storeIndexSignature = exports.storeIndexFor = exports.serverTimestamp = exports.increment = exports.documentExists = exports.deleteField = exports.arrayUnion = exports.arrayRemove = exports.normalizePlanFeatures = exports.blockingBlockers = exports.isConnectError = exports.ConnectError = void 0;
|
|
3
|
+
exports.storeIndexCovers = exports.describeQueryRejection = exports.unchangedSince = exports.storeIndexSignature = exports.storeIndexFor = exports.serverTimestamp = exports.increment = exports.documentExists = exports.deleteField = exports.arrayUnion = exports.arrayRemove = exports.normalizePlanFeatures = exports.blockingBlockers = exports.isConnectError = exports.ConnectError = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* `@cosmoops/connect-sdk` - the official client for the CosmoOps Connect
|
|
6
6
|
* **application-partner API** (`/api/v1/application/*`).
|
|
@@ -13,6 +13,8 @@ exports.unchangedSince = exports.storeIndexSignature = exports.storeIndexFor = e
|
|
|
13
13
|
* Holds the API key. Never import this from browser code.
|
|
14
14
|
* - `@cosmoops/connect-sdk/client` - React components and hooks. Talks to your
|
|
15
15
|
* own origin only.
|
|
16
|
+
* - `@cosmoops/connect-sdk/mcp` - your store's declaration, derived into MCP
|
|
17
|
+
* tools. Holds the API key; never import this from browser code.
|
|
16
18
|
*/
|
|
17
19
|
var errors_js_1 = require("./errors.js");
|
|
18
20
|
Object.defineProperty(exports, "ConnectError", { enumerable: true, get: function () { return errors_js_1.ConnectError; } });
|
|
@@ -30,3 +32,6 @@ Object.defineProperty(exports, "serverTimestamp", { enumerable: true, get: funct
|
|
|
30
32
|
Object.defineProperty(exports, "storeIndexFor", { enumerable: true, get: function () { return store_js_1.storeIndexFor; } });
|
|
31
33
|
Object.defineProperty(exports, "storeIndexSignature", { enumerable: true, get: function () { return store_js_1.storeIndexSignature; } });
|
|
32
34
|
Object.defineProperty(exports, "unchangedSince", { enumerable: true, get: function () { return store_js_1.unchangedSince; } });
|
|
35
|
+
var store_schema_js_1 = require("./store-schema.js");
|
|
36
|
+
Object.defineProperty(exports, "describeQueryRejection", { enumerable: true, get: function () { return store_schema_js_1.describeQueryRejection; } });
|
|
37
|
+
Object.defineProperty(exports, "storeIndexCovers", { enumerable: true, get: function () { return store_schema_js_1.storeIndexCovers; } });
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deriveStoreTools = void 0;
|
|
4
|
+
exports.createStoreToolset = createStoreToolset;
|
|
5
|
+
/**
|
|
6
|
+
* `@cosmoops/connect-sdk/mcp` - your application's store, as an MCP server.
|
|
7
|
+
*
|
|
8
|
+
* Server-only: every call goes out through `ConnectClient`, which holds the API
|
|
9
|
+
* key. The `browser` export condition resolves this entry to a stub, so an
|
|
10
|
+
* import from client code fails the build rather than shipping the key.
|
|
11
|
+
*
|
|
12
|
+
* ## Zero dependencies, on purpose
|
|
13
|
+
*
|
|
14
|
+
* This entry does not import `@modelcontextprotocol/sdk`, and the package takes
|
|
15
|
+
* no dependency on it. `listTools()` and `callTool()` answer in the shapes the
|
|
16
|
+
* MCP protocol defines, so wiring them to the official server is four lines and
|
|
17
|
+
* a transport you choose:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const toolset = createStoreToolset({
|
|
21
|
+
* collections: STORE_SCHEMA,
|
|
22
|
+
* data: connect.data,
|
|
23
|
+
* workspaceId: session.workspaceId,
|
|
24
|
+
* asUser: session.sessionToken,
|
|
25
|
+
* })
|
|
26
|
+
*
|
|
27
|
+
* server.setRequestHandler(ListToolsRequestSchema, () => toolset.listTools())
|
|
28
|
+
* server.setRequestHandler(CallToolRequestSchema, (request) =>
|
|
29
|
+
* toolset.callTool(request.params.name, request.params.arguments ?? {}),
|
|
30
|
+
* )
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* The alternative - depending on the MCP SDK so this file could return an
|
|
34
|
+
* `McpServer` - would put a version of it in every install of this package,
|
|
35
|
+
* including the ones that never touch MCP, and pin partners to whichever
|
|
36
|
+
* version shipped here. The protocol shapes are stable and small; the coupling
|
|
37
|
+
* would not be.
|
|
38
|
+
*
|
|
39
|
+
* ## One toolset per session
|
|
40
|
+
*
|
|
41
|
+
* `asUser` is an end user's Connect session token, and Connect checks each
|
|
42
|
+
* collection's declared permission against that member's live roles. A toolset
|
|
43
|
+
* is therefore scoped to one signed-in user - build it per request from
|
|
44
|
+
* `getConnectSession()`, or pass a function for `asUser` if the surrounding
|
|
45
|
+
* server outlives a session.
|
|
46
|
+
*/
|
|
47
|
+
var tools_js_1 = require("./tools.js");
|
|
48
|
+
Object.defineProperty(exports, "deriveStoreTools", { enumerable: true, get: function () { return tools_js_1.deriveStoreTools; } });
|
|
49
|
+
const tools_js_2 = require("./tools.js");
|
|
50
|
+
/**
|
|
51
|
+
* A refusal is a result, not a thrown error.
|
|
52
|
+
*
|
|
53
|
+
* MCP draws that distinction deliberately: a protocol error is a fault the
|
|
54
|
+
* model cannot act on, while `isError: true` is a tool result the model reads
|
|
55
|
+
* and can respond to. An unservable query and a permission Connect refused are
|
|
56
|
+
* both things a model should see and adapt to - naming a field it may not
|
|
57
|
+
* filter on, or telling the user it cannot reach that data - so they come back
|
|
58
|
+
* as content rather than as a transport failure.
|
|
59
|
+
*/
|
|
60
|
+
function asResult(text, isError = false) {
|
|
61
|
+
return { content: [{ type: "text", text }], isError };
|
|
62
|
+
}
|
|
63
|
+
function createStoreToolset(options) {
|
|
64
|
+
const tools = (0, tools_js_2.deriveStoreTools)(options);
|
|
65
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
66
|
+
return {
|
|
67
|
+
tools,
|
|
68
|
+
listTools: () => ({
|
|
69
|
+
tools: tools.map((tool) => ({
|
|
70
|
+
name: tool.name,
|
|
71
|
+
title: tool.title,
|
|
72
|
+
description: tool.description,
|
|
73
|
+
inputSchema: tool.inputSchema,
|
|
74
|
+
annotations: tool.annotations,
|
|
75
|
+
})),
|
|
76
|
+
}),
|
|
77
|
+
async callTool(name, args) {
|
|
78
|
+
const tool = byName.get(name);
|
|
79
|
+
if (!tool) {
|
|
80
|
+
return asResult(`No such tool: "${name}". Available: ${[...byName.keys()].join(", ")}.`, true);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return asResult(JSON.stringify(await tool.execute(args), null, 2));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return asResult(error instanceof Error ? error.message : String(error), true);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|