@softr/sdk 0.0.67 → 0.0.68-alpha.1
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/dist/core/integration.d.ts +7 -0
- package/dist/core/integration.js +10 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/package.js +1 -1
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +5 -1
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/formatting/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +58 -5
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/record-queries/SKILL.md +1 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
|
@@ -7,6 +7,13 @@ interface IntegrationFetchInit {
|
|
|
7
7
|
headers?: Record<string, string>;
|
|
8
8
|
body?: string;
|
|
9
9
|
signal?: AbortSignal;
|
|
10
|
+
/**
|
|
11
|
+
* Relay the vendor's answer as it is produced instead of waiting for all of it — for any endpoint
|
|
12
|
+
* that streams, such as an LLM completion requested with `"stream": true`. The returned `Response`
|
|
13
|
+
* body is then the vendor's own stream, readable chunk by chunk. A vendor that answers in one piece
|
|
14
|
+
* still works.
|
|
15
|
+
*/
|
|
16
|
+
stream?: boolean;
|
|
10
17
|
}
|
|
11
18
|
declare function integrationCreateFetcher(action: IntegrationAction): (init?: IntegrationFetchInit) => Promise<Response>;
|
|
12
19
|
declare const integration: {
|
package/dist/core/integration.js
CHANGED
|
@@ -3,15 +3,16 @@ import { readSessionToken } from "../auth/session.js";
|
|
|
3
3
|
import { dsUrl, refererHeader } from "../datasource/record.js";
|
|
4
4
|
|
|
5
5
|
//#region src/core/integration.ts
|
|
6
|
-
|
|
6
|
+
const EVENT_STREAM = "text/event-stream";
|
|
7
|
+
async function integrationFetch(action, { params, query, headers = {}, body, signal, stream = false } = {}) {
|
|
7
8
|
if (signal?.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
8
9
|
const payload = {
|
|
9
10
|
method: action.resource.method,
|
|
10
11
|
url: buildUrl(action.resource.url, params, query),
|
|
11
|
-
headers
|
|
12
|
+
headers: stream ? withAcceptEventStream(headers) : headers
|
|
12
13
|
};
|
|
13
14
|
if (body !== void 0) payload.body = body;
|
|
14
|
-
return fetch(`${dsUrl(action.id)}/proxy`, {
|
|
15
|
+
return fetch(`${dsUrl(action.id)}/${stream ? "proxy/stream" : "proxy"}`, {
|
|
15
16
|
method: "POST",
|
|
16
17
|
headers: {
|
|
17
18
|
"Content-Type": "application/json",
|
|
@@ -30,6 +31,12 @@ function buildUrl(template, params, query) {
|
|
|
30
31
|
if (query !== void 0 && Object.keys(query).length > 0) url += `?${new URLSearchParams(query).toString()}`;
|
|
31
32
|
return url;
|
|
32
33
|
}
|
|
34
|
+
function withAcceptEventStream(headers) {
|
|
35
|
+
return Object.keys(headers).some((name) => name.toLowerCase() === "accept") ? headers : {
|
|
36
|
+
...headers,
|
|
37
|
+
Accept: EVENT_STREAM
|
|
38
|
+
};
|
|
39
|
+
}
|
|
33
40
|
function integrationCreateFetcher(action) {
|
|
34
41
|
return (init) => integrationFetch(action, init);
|
|
35
42
|
}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatasourceAction } from "./action.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as _tanstack_react_start0 from "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/mcp.d.ts
|
|
5
5
|
|
|
@@ -31,6 +31,6 @@ type SoftrMcpOptions = {
|
|
|
31
31
|
* ],
|
|
32
32
|
* }))
|
|
33
33
|
*/
|
|
34
|
-
declare function softrMcpMiddleware(options?: SoftrMcpOptions):
|
|
34
|
+
declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
35
35
|
//#endregion
|
|
36
36
|
export { softrMcpMiddleware };
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: core
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.68-alpha.1
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
# Softr SDK core
|
|
@@ -48,6 +48,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
48
48
|
| Trigger a Softr workflow | [workflows/SKILL.md](./workflows/SKILL.md) |
|
|
49
49
|
| Sign a visitor in, out, or up | [auth/SKILL.md](./auth/SKILL.md) |
|
|
50
50
|
| Call a connected third-party API (Slack, Stripe, Gmail, …) | [integrations/SKILL.md](./integrations/SKILL.md) |
|
|
51
|
+
| Show an AI answer as it is written (a chat, an assistant, a live summary) | [integrations/SKILL.md](./integrations/SKILL.md), section "Streaming a vendor answer" |
|
|
51
52
|
| Open the app to AI assistants over MCP | [mcp/SKILL.md](./mcp/SKILL.md) |
|
|
52
53
|
| Let visitors install the app on their home screen | [pwa/SKILL.md](./pwa/SKILL.md) |
|
|
53
54
|
| Show a date, a time or a number | [formatting/SKILL.md](./formatting/SKILL.md) |
|
|
@@ -82,6 +83,9 @@ Wiring a login, signup, or sign-out?
|
|
|
82
83
|
Calling a connected third-party API (Slack, Stripe, Gmail, Google Calendar, …)?
|
|
83
84
|
load softr-sdk/integrations
|
|
84
85
|
|
|
86
|
+
Showing an AI answer as it is written: a chat, an assistant, a summary that types itself out?
|
|
87
|
+
load softr-sdk/integrations and stream the call: its "Streaming a vendor answer" section
|
|
88
|
+
|
|
85
89
|
Opening the app to an AI assistant over MCP?
|
|
86
90
|
load softr-sdk/mcp
|
|
87
91
|
|
|
@@ -19,7 +19,7 @@ description: >-
|
|
|
19
19
|
metadata:
|
|
20
20
|
type: sub-skill
|
|
21
21
|
library: softr-sdk
|
|
22
|
-
library_version: 0.0.
|
|
22
|
+
library_version: 0.0.68-alpha.1
|
|
23
23
|
---
|
|
24
24
|
|
|
25
25
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
|
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.68-alpha.1
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.68-alpha.1
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk` and run in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
|
|
@@ -5,12 +5,12 @@ description: >-
|
|
|
5
5
|
defineIntegration from @softr/sdk declares the action with its integrationId,
|
|
6
6
|
the one HTTP method and endpoint url it allows, and permissions;
|
|
7
7
|
integration.createFetcher from @softr/sdk/server binds it and returns a
|
|
8
|
-
fetcher function you call with optional params, query, headers, and
|
|
9
|
-
Check res.ok before res.json().
|
|
8
|
+
fetcher function you call with optional params, query, headers, body, and
|
|
9
|
+
stream for a vendor answer that arrives in pieces. Check res.ok before res.json().
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.68-alpha.1
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -129,9 +129,15 @@ Do not add an `Authorization` key to `headers` — Softr injects the integration
|
|
|
129
129
|
|
|
130
130
|
## Calling an AI model
|
|
131
131
|
|
|
132
|
+
**Two shapes, decided by who reads the answer.** An answer the user reads as it is written — a chat, an
|
|
133
|
+
assistant, a long summary shown on the page — streams: pass `stream: true` and relay the body, see
|
|
134
|
+
[Streaming a vendor answer](#streaming-a-vendor-answer) below. An answer the server reads — JSON the page
|
|
135
|
+
lays out, text saved to a field — is buffered: the call below, then `res.json()`. A chat is never buffered:
|
|
136
|
+
the user would watch a blank panel until the last token.
|
|
137
|
+
|
|
132
138
|
A button on one record that hands its fields to the model and shows the answer is the `ai-action` pattern:
|
|
133
|
-
it carries
|
|
134
|
-
a field the model fills on create. An AI integration is called like any other. For `ANTHROPIC_AI`, the action is `POST` on
|
|
139
|
+
it carries the buffered call and the model id, so write the call by hand only for another shape — a batch,
|
|
140
|
+
a field the model fills on create, or a chat, which streams. An AI integration is called like any other. For `ANTHROPIC_AI`, the action is `POST` on
|
|
135
141
|
`https://api.anthropic.com/v1/messages`, the request carries the `anthropic-version` header and a body of
|
|
136
142
|
`model`, `max_tokens`, `system` and `messages`, and the answer's text is in `content[].text`:
|
|
137
143
|
|
|
@@ -164,6 +170,49 @@ const text = body.content.flatMap((block) => (block.type === 'text' && block.tex
|
|
|
164
170
|
the button is pressed. `claude-opus-5-5` is the current default; `claude-sonnet-5-5` is the cheaper one for a
|
|
165
171
|
short summary.
|
|
166
172
|
|
|
173
|
+
## Streaming a vendor answer
|
|
174
|
+
|
|
175
|
+
Some vendor endpoints can send their answer in pieces while they produce it — LLM completions token by token, long exports, live event feeds. The fetcher relays such an answer as it arrives when you pass `stream: true`; the response body is then the vendor's own stream, unchanged.
|
|
176
|
+
|
|
177
|
+
Every connected LLM streams its completion. Ask the vendor for it in the request **and** pass `stream: true` to the fetcher; one without the other gains nothing:
|
|
178
|
+
|
|
179
|
+
| integrationType | Endpoint | How to ask for the stream |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| ANTHROPIC_AI | `POST /v1/messages` | `"stream": true` in the body |
|
|
182
|
+
| OPEN_AI | `POST /v1/chat/completions` or `POST /v1/responses` | `"stream": true` in the body |
|
|
183
|
+
| GEMINI_AI | `POST /v1beta/models/{model}:streamGenerateContent` with `query: { alt: 'sse' }` | the endpoint itself; `generateContent` never streams, so the action's `url` must name the streaming one |
|
|
184
|
+
| MISTRAL_AI | `POST /v1/chat/completions` | `"stream": true` in the body |
|
|
185
|
+
| DEEP_SEEK_AI | `POST /chat/completions` | `"stream": true` in the body |
|
|
186
|
+
| COHERE | `POST /v2/chat` | `"stream": true` in the body |
|
|
187
|
+
|
|
188
|
+
For any other vendor, read that endpoint's docs: it streams when they offer a `stream` flag, an `Accept: text/event-stream` variant, or a chunked response to read incrementally. Most REST calls answer in one piece; call those without `stream` and `res.json()` works.
|
|
189
|
+
|
|
190
|
+
Hand a streamed body to the browser as a `Response` from the server function and read it there. The example below streams a completion through the `askClaude` action declared above; the same shape applies to any vendor endpoint that streams.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const streamAnswer = createServerFn({ method: 'POST' })
|
|
194
|
+
.validator((data: { prompt: string }) => data)
|
|
195
|
+
.handler(async ({ data }) => {
|
|
196
|
+
const claude = integration.createFetcher(askClaude)
|
|
197
|
+
const res = await claude({
|
|
198
|
+
stream: true,
|
|
199
|
+
headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' },
|
|
200
|
+
body: JSON.stringify({
|
|
201
|
+
model: 'claude-opus-5-5',
|
|
202
|
+
max_tokens: 1024,
|
|
203
|
+
stream: true,
|
|
204
|
+
messages: [{ role: 'user', content: data.prompt }],
|
|
205
|
+
}),
|
|
206
|
+
})
|
|
207
|
+
if (!res.ok) throw new Error(`AI error: ${res.status} ${await res.text()}`)
|
|
208
|
+
return new Response(res.body, { headers: { 'Content-Type': 'text/event-stream' } })
|
|
209
|
+
})
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
On the client, call the server function's URL with `fetch` and read `response.body` with a `ReadableStream` reader; each chunk arrives in the vendor's own format, for an event stream one or more `event:`/`data:` frames. Use `stream: true` only together with a request that asks the vendor to stream: without it the vendor answers in one piece, which still works but gains nothing.
|
|
213
|
+
|
|
214
|
+
`stream: true` is not available for REST_API integrations; those always answer in one piece.
|
|
215
|
+
|
|
167
216
|
## Base URLs by integration type
|
|
168
217
|
|
|
169
218
|
Match the `integrationType` from the attached integrations section to find the base URL:
|
|
@@ -235,3 +284,7 @@ If the API the user needs is not in the table above, call it directly from a ser
|
|
|
235
284
|
**Not checking `res.ok`.** The fetcher returns the proxied response as-is. A 4xx or 5xx from the third-party API is not thrown — call `res.ok` or `res.status` before calling `res.json()`.
|
|
236
285
|
|
|
237
286
|
**Reusing one action for a different endpoint.** An action authorizes one `method` and one endpoint `url`. A different endpoint belongs to a different action — declare one for it, with its own audience.
|
|
287
|
+
|
|
288
|
+
**Buffering an answer the user watches being written.** A chat or assistant called without `stream: true` shows nothing until the last token arrives, and a long answer then hits the proxy's read timeout. Pass `stream: true`, ask the vendor to stream in the request, and return `res.body` in a `Response`.
|
|
289
|
+
|
|
290
|
+
**Reading a streamed body with `res.json()`.** With `stream: true` the body is an event stream, not one JSON document. Return `res.body` in a `Response` or read it with a stream reader; `res.json()` waits for the end and then fails to parse.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.68-alpha.1
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `softrMcpMiddleware` lives on `@softr/sdk` and is registered once, in `src/start.ts`. The actions you hand it are the same `defineQuery` / `defineMutation` values the app's own pages use — declared wherever they already live, not duplicated for MCP.
|
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: sub-skill
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.
|
|
14
|
+
library_version: 0.0.68-alpha.1
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -13,7 +13,7 @@ description: >-
|
|
|
13
13
|
metadata:
|
|
14
14
|
type: sub-skill
|
|
15
15
|
library: softr-sdk
|
|
16
|
-
library_version: 0.0.
|
|
16
|
+
library_version: 0.0.68-alpha.1
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. The same holds for any helper that calls `ds`: declare it inside the handler. A function at module level that calls `ds` keeps the server-only import in the browser bundle, and every page stops loading until it moves. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -13,7 +13,7 @@ description: >-
|
|
|
13
13
|
metadata:
|
|
14
14
|
type: sub-skill
|
|
15
15
|
library: softr-sdk
|
|
16
|
-
library_version: 0.0.
|
|
16
|
+
library_version: 0.0.68-alpha.1
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
**Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.68-alpha.1
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere.
|