@softr/sdk 0.0.90-alpha.7 → 0.0.91-alpha.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.
Files changed (37) hide show
  1. package/dist/action.d.ts +34 -1
  2. package/dist/action.js +12 -1
  3. package/dist/app-identity.js +25 -0
  4. package/dist/auth/isomorphic.d.ts +3 -3
  5. package/dist/auth/isomorphic.js +2 -0
  6. package/dist/auth/server.js +1 -1
  7. package/dist/core/ai.d.ts +79 -0
  8. package/dist/core/ai.js +64 -0
  9. package/dist/datasource/record.js +3 -12
  10. package/dist/index.d.ts +3 -4
  11. package/dist/index.js +2 -4
  12. package/dist/mcp.d.ts +4 -17
  13. package/dist/mcp.js +18 -61
  14. package/dist/package.js +1 -1
  15. package/dist/server.d.ts +2 -1
  16. package/dist/server.js +2 -1
  17. package/package.json +10 -33
  18. package/skills/softr-sdk/SKILL.md +13 -8
  19. package/skills/softr-sdk/ai/SKILL.md +168 -0
  20. package/skills/softr-sdk/auth/SKILL.md +1 -1
  21. package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
  22. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  23. package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
  24. package/skills/softr-sdk/formatting/SKILL.md +1 -1
  25. package/skills/softr-sdk/integrations/SKILL.md +8 -4
  26. package/skills/softr-sdk/mcp/SKILL.md +1 -16
  27. package/skills/softr-sdk/pwa/SKILL.md +1 -1
  28. package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
  29. package/skills/softr-sdk/record-queries/SKILL.md +1 -1
  30. package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
  31. package/skills/softr-sdk/routes-and-visibility/SKILL.md +4 -4
  32. package/skills/softr-sdk/workflows/SKILL.md +1 -1
  33. package/dist/agent/agent-fns.d.ts +0 -30
  34. package/dist/agent/agent-fns.js +0 -215
  35. package/dist/agent/use-agent-chat.d.ts +0 -106
  36. package/dist/agent/use-agent-chat.js +0 -486
  37. package/skills/softr-sdk/agent-chat/SKILL.md +0 -110
package/dist/action.d.ts CHANGED
@@ -234,5 +234,38 @@ type IntegrationDefinition<TId extends string> = {
234
234
  * const res = await listEvents({ query: { timeMin: now, singleEvents: "true" } })
235
235
  */
236
236
  declare function defineIntegration<const TId extends string>(definition: IntegrationDefinition<TId>): IntegrationAction<TId>;
237
+ type AiResource = {
238
+ type: "SOFTR_AI";
239
+ model: string;
240
+ };
241
+ type AiAction<TId extends string = string> = Action<AiResource> & {
242
+ id: TId;
243
+ };
244
+ type AiDefinition<TId extends string> = {
245
+ id: TId;
246
+ model: string;
247
+ permissions: ActionPermissions;
248
+ };
249
+ /**
250
+ * Declares an AI action: a model call run by Softr on its own provider keys and billed to the
251
+ * workspace's AI credits, so nothing has to be connected. `model` is the id of a model from
252
+ * Softr's AI catalog, chosen when the app is built: read the catalog with the `ai_list_models`
253
+ * tool and copy the id exactly. An id outside the catalog is refused when the action is saved.
254
+ *
255
+ * Every call spends the workspace's credits, so keep the audience to signed-in users unless a
256
+ * public page really needs the call.
257
+ *
258
+ * Pass the returned object to `ai.generate()` or `ai.stream()` at runtime.
259
+ *
260
+ * @example
261
+ * const summarizeDeal = defineAiAction({
262
+ * id: "summarizeDeal",
263
+ * model: "anthropic-claude-sonnet-5-5",
264
+ * permissions: { userGroupTypes: ["LOGGED_IN_USERS"] },
265
+ * })
266
+ *
267
+ * const answer = await ai.generate(summarizeDeal, { system: "...", prompt: facts })
268
+ */
269
+ declare function defineAiAction<const TId extends string>(definition: AiDefinition<TId>): AiAction<TId>;
237
270
  //#endregion
238
- export { ActionData, ActionPermissions, ActionRecord, ActionShape, DatasourceAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineIntegration, defineMutation, defineQuery, defineWorkflow };
271
+ export { ActionData, ActionPermissions, ActionRecord, ActionShape, AiAction, DatasourceAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow };
package/dist/action.js CHANGED
@@ -56,6 +56,17 @@ function isDatasourceAction(value) {
56
56
  const resource = candidate.resource;
57
57
  return typeof candidate.id === "string" && typeof resource === "object" && resource !== null && resource.type === "DATASOURCE";
58
58
  }
59
+ function defineAiAction(definition) {
60
+ const { id, model, permissions } = definition;
61
+ return {
62
+ id,
63
+ resource: {
64
+ type: "SOFTR_AI",
65
+ model
66
+ },
67
+ permissions
68
+ };
69
+ }
59
70
 
60
71
  //#endregion
61
- export { defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
72
+ export { defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
@@ -0,0 +1,25 @@
1
+ import { createMiddleware } from "@tanstack/react-start";
2
+
3
+ //#region src/app-identity.ts
4
+ /**
5
+ * Tells support which Softr app answered, in an `x-softr-app-id` response
6
+ * header.
7
+ */
8
+ function appIdentityMiddleware() {
9
+ return createMiddleware({ type: "request" }).server(async ({ next }) => {
10
+ const result = await next();
11
+ const applicationId = process.env.SOFTR_APPLICATION_ID;
12
+ if (!applicationId) return result;
13
+ try {
14
+ result.response.headers.set("x-softr-app-id", applicationId);
15
+ return result;
16
+ } catch {
17
+ const response = new Response(result.response.body, result.response);
18
+ response.headers.set("x-softr-app-id", applicationId);
19
+ return response;
20
+ }
21
+ });
22
+ }
23
+
24
+ //#endregion
25
+ export { appIdentityMiddleware };
@@ -2,7 +2,7 @@ import { User } from "./user.js";
2
2
  import { Formatting } from "../formatting.js";
3
3
  import { Permissions } from "./permissions.js";
4
4
  import { SoftrContext } from "./context.js";
5
- import * as react3 from "react";
5
+ import * as react0 from "react";
6
6
  import { ReactNode } from "react";
7
7
  import * as _tanstack_react_start0 from "@tanstack/react-start";
8
8
 
@@ -22,7 +22,7 @@ import * as _tanstack_react_start0 from "@tanstack/react-start";
22
22
  * requestMiddleware: [softrAuthMiddleware()],
23
23
  * }))
24
24
  */
25
- declare function softrAuthMiddleware(): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, readonly [_tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>, _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>, _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>, _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>], {
25
+ declare function softrAuthMiddleware(): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, readonly [_tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>, _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>, _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>, _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>, _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>], {
26
26
  user: User | null;
27
27
  permissions: Permissions;
28
28
  formatting: Formatting;
@@ -104,7 +104,7 @@ declare function AuthSettingsProvider({
104
104
  }: {
105
105
  value: AuthOptions;
106
106
  children?: ReactNode;
107
- }): react3.JSX.Element;
107
+ }): react0.JSX.Element;
108
108
  /**
109
109
  * Which ways in this app offers. Requires an {@link AuthSettingsProvider}
110
110
  * above it.
@@ -10,6 +10,7 @@ import { bootErrorMiddleware } from "../boot-error.js";
10
10
  import { pwaMiddleware } from "../pwa.js";
11
11
  import { faviconMiddleware } from "../favicon.js";
12
12
  import { embeddingMiddleware } from "../embedding.js";
13
+ import { appIdentityMiddleware } from "../app-identity.js";
13
14
  import { createContext, useContext } from "react";
14
15
  import { createMiddleware, createServerFn, getGlobalStartContext } from "@tanstack/react-start";
15
16
  import { getRequestHeader } from "@tanstack/react-start/server";
@@ -34,6 +35,7 @@ import { jsx } from "react/jsx-runtime";
34
35
  function softrAuthMiddleware() {
35
36
  return createMiddleware({ type: "request" }).middleware([
36
37
  embeddingMiddleware(),
38
+ appIdentityMiddleware(),
37
39
  bootErrorMiddleware(),
38
40
  pwaMiddleware(),
39
41
  faviconMiddleware()
@@ -1,5 +1,4 @@
1
1
  import { settings } from "./methods/settings.js";
2
- import { currentUserRow } from "./current-user-row.js";
3
2
  import { confirmAuthenticatorSetup, removeAuthenticator, resetAuthenticator, sendAuthenticatorResetCode, skipAuthenticatorSetup, startAuthenticatorSetup, verifyAuthenticator } from "./methods/authenticator.js";
4
3
  import { sendSignInCode, signInWithCode } from "./methods/code.js";
5
4
  import { signInWithMagicLink } from "./methods/magicLink.js";
@@ -8,6 +7,7 @@ import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./
8
7
  import { confirmSignUp, signUp } from "./methods/signup.js";
9
8
  import { signInWithGoogle } from "./methods/social.js";
10
9
  import { removeAvatar, updateProfile } from "./methods/profile.js";
10
+ import { currentUserRow } from "./current-user-row.js";
11
11
  import "@tanstack/react-start/server-only";
12
12
 
13
13
  //#region src/auth/server.ts
@@ -0,0 +1,79 @@
1
+ import { AiAction } from "../action.js";
2
+
3
+ //#region src/core/ai.d.ts
4
+ /** A document or image the model reads, by the URL the app holds it under: one of its own uploads, as a rule. */
5
+ interface AiFile {
6
+ /** An https URL Softr may fetch. */
7
+ url: string;
8
+ /** The file's type when the URL does not say, such as `application/pdf`. */
9
+ mimeType?: string;
10
+ }
11
+ /** One earlier turn of a conversation. */
12
+ interface AiMessage {
13
+ role: 'user' | 'assistant';
14
+ content: string;
15
+ }
16
+ interface AiGenerateInit {
17
+ /** What the model is and the rules it answers under. */
18
+ system?: string;
19
+ /** The earlier turns of a conversation, oldest first, before the prompt. Leave out for a one-shot call. */
20
+ messages?: ReadonlyArray<AiMessage>;
21
+ /** What it is asked, with the facts it is given. */
22
+ prompt: string;
23
+ /**
24
+ * A JSON schema the answer has to match. With it, the model is asked for JSON only and the
25
+ * answer comes back parsed in `json` as well as written in `text`.
26
+ */
27
+ schema?: Record<string, unknown>;
28
+ /** Documents and images the model reads alongside the prompt. */
29
+ files?: ReadonlyArray<AiFile>;
30
+ /** A cap on the answer's length, in tokens; Softr caps it further. */
31
+ maxOutputTokens?: number;
32
+ /**
33
+ * How hard a reasoning model thinks before it answers. A model that does not reason ignores it;
34
+ * the catalog says which do. Left out, the provider's default applies.
35
+ */
36
+ reasoning?: 'low' | 'medium' | 'high';
37
+ /**
38
+ * How varied the answer is, 0 to 1: 0 for an extraction, higher for a draft. Left out, the provider's default
39
+ * applies. A model whose provider no longer takes one ignores it.
40
+ */
41
+ temperature?: number;
42
+ signal?: AbortSignal;
43
+ }
44
+ interface AiUsage {
45
+ inputTokens: number;
46
+ outputTokens: number;
47
+ }
48
+ interface AiAnswer {
49
+ /** The answer as the model wrote it. */
50
+ text: string;
51
+ /** The answer parsed as JSON when `schema` was given and the model kept to it; `null` otherwise. */
52
+ json: unknown;
53
+ usage: AiUsage;
54
+ /** The model that answered. */
55
+ model: string;
56
+ }
57
+ /**
58
+ * Thrown when the model call was refused or failed. `status` is the HTTP status Softr answered
59
+ * with; 402 means the workspace has no AI credits left.
60
+ */
61
+ declare class AiError extends Error {
62
+ readonly status: number;
63
+ constructor(status: number, message: string);
64
+ }
65
+ /** Runs the action once and answers when the model is done. */
66
+ declare function aiGenerate(action: AiAction, init: AiGenerateInit): Promise<AiAnswer>;
67
+ /**
68
+ * Runs the action and answers while the model writes. The returned `Response` body is a stream of
69
+ * Server-Sent Events, one JSON object per `data:` line: `{ "text": "..." }` for each piece of the
70
+ * answer, then `{ "done": true, "usage": {...}, "model": "..." }`, or `{ "error": "..." }` in its
71
+ * place. Hand it to the browser as a `Response` with `Content-Type: text/event-stream`.
72
+ */
73
+ declare function aiStream(action: AiAction, init: AiGenerateInit): Promise<Response>;
74
+ declare const ai: {
75
+ generate: typeof aiGenerate;
76
+ stream: typeof aiStream;
77
+ };
78
+ //#endregion
79
+ export { AiAnswer, AiError, AiFile, AiGenerateInit, AiMessage, AiUsage, ai };
@@ -0,0 +1,64 @@
1
+ import { fetch } from "../fetch.js";
2
+ import { readSessionToken } from "../auth/session.js";
3
+ import { describeFailure, dsUrl, refererHeader } from "../datasource/record.js";
4
+
5
+ //#region src/core/ai.ts
6
+ /**
7
+ * Thrown when the model call was refused or failed. `status` is the HTTP status Softr answered
8
+ * with; 402 means the workspace has no AI credits left.
9
+ */
10
+ var AiError = class extends Error {
11
+ status;
12
+ constructor(status, message) {
13
+ super(message);
14
+ this.name = "AiError";
15
+ this.status = status;
16
+ }
17
+ };
18
+ function body(init) {
19
+ return JSON.stringify({
20
+ system: init.system,
21
+ messages: init.messages,
22
+ prompt: init.prompt,
23
+ schema: init.schema,
24
+ files: init.files,
25
+ maxOutputTokens: init.maxOutputTokens,
26
+ reasoning: init.reasoning,
27
+ temperature: init.temperature
28
+ });
29
+ }
30
+ async function post(action, path, init) {
31
+ if (init.signal?.aborted) throw init.signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
32
+ const res = await fetch(`${dsUrl(action.id)}/${path}`, {
33
+ method: "POST",
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ "Authorization": `Bearer ${readSessionToken()}`,
37
+ ...refererHeader()
38
+ },
39
+ body: body(init),
40
+ signal: init.signal
41
+ });
42
+ if (!res.ok) throw new AiError(res.status, `AI call "${action.id}" failed: ${await describeFailure(res)}`);
43
+ return res;
44
+ }
45
+ /** Runs the action once and answers when the model is done. */
46
+ async function aiGenerate(action, init) {
47
+ return await (await post(action, "softr-ai/generate", init)).json();
48
+ }
49
+ /**
50
+ * Runs the action and answers while the model writes. The returned `Response` body is a stream of
51
+ * Server-Sent Events, one JSON object per `data:` line: `{ "text": "..." }` for each piece of the
52
+ * answer, then `{ "done": true, "usage": {...}, "model": "..." }`, or `{ "error": "..." }` in its
53
+ * place. Hand it to the browser as a `Response` with `Content-Type: text/event-stream`.
54
+ */
55
+ async function aiStream(action, init) {
56
+ return post(action, "softr-ai/generate/stream", init);
57
+ }
58
+ const ai = {
59
+ generate: aiGenerate,
60
+ stream: aiStream
61
+ };
62
+
63
+ //#endregion
64
+ export { AiError, ai };
@@ -30,15 +30,6 @@ function refererHeader() {
30
30
  const referer = getRequestHeader("referer");
31
31
  return { Referer: referer !== void 0 && URL.parse(referer)?.origin === page.origin ? referer : page.href };
32
32
  }
33
- /** A datasource call the service refused. `status` is its HTTP status. */
34
- var DatasourceError = class extends Error {
35
- status;
36
- constructor(message, status) {
37
- super(message);
38
- this.name = "DatasourceError";
39
- this.status = status;
40
- }
41
- };
42
33
  async function describeFailure(res) {
43
34
  const body = await res.json().catch(() => null);
44
35
  const message = typeof body === "object" && body !== null && "message" in body ? body.message : null;
@@ -244,7 +235,7 @@ async function updateRecord(action, options) {
244
235
  },
245
236
  body: JSON.stringify({ fields: rawFields })
246
237
  });
247
- if (!res.ok) throw new DatasourceError(`datasource update failed for action "${actionId}": ${await describeFailure(res)}`, res.status);
238
+ if (!res.ok) throw new Error(`datasource update failed for action "${actionId}": ${await describeFailure(res)}`);
248
239
  const record = await res.json();
249
240
  return {
250
241
  id: record.id,
@@ -266,7 +257,7 @@ async function deleteRecord(action, options) {
266
257
  ...refererHeader()
267
258
  }
268
259
  });
269
- if (!res.ok) throw new DatasourceError(`datasource delete failed for action "${actionId}": ${await describeFailure(res)}`, res.status);
260
+ if (!res.ok) throw new Error(`datasource delete failed for action "${actionId}": ${await describeFailure(res)}`);
270
261
  }
271
262
  function resolveTarget(subject, operation) {
272
263
  const declaredBy = operation === "read" ? "defineQuery" : "defineMutation";
@@ -290,4 +281,4 @@ function dealiasData(data, select) {
290
281
  }
291
282
 
292
283
  //#endregion
293
- export { DatasourceError, createRecord, deleteRecord, describeFailure, dsUrl, getRecord, listRecords, refererHeader, resolveTarget, updateRecord };
284
+ export { createRecord, deleteRecord, describeFailure, dsUrl, getRecord, listRecords, refererHeader, resolveTarget, updateRecord };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { SoftrUserGroupId, SoftrUserGroups, SoftrUserProperties, User } from "./auth/user.js";
2
- import { ActionPermissions, SoftrDatasources, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
2
+ import { ActionPermissions, SoftrDatasources, defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
3
3
  import { FileAttachment } from "./core/file-upload.js";
4
4
  import { FileItem, useFileUpload } from "./core/use-file-upload.js";
5
+ import { AiAnswer, AiFile, AiGenerateInit, AiMessage, AiUsage } from "./core/ai.js";
5
6
  import { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting } from "./formatting.js";
6
7
  import { SoftrContext } from "./auth/context.js";
7
8
  import { AuthSettings } from "./auth/platform.js";
@@ -16,7 +17,5 @@ import { useOverrides } from "./core/use-overrides.js";
16
17
  import { useUserTimezone } from "./timezone.js";
17
18
  import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
18
19
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
19
- import { Agent, AgentChatSummary, listAgents } from "./agent/agent-fns.js";
20
- import { AgentBlock, AgentChat, AgentChatOptions, AgentMessage, AgentQuestion, AgentTodo, AgentToolCall, useAgentChat } from "./agent/use-agent-chat.js";
21
20
  import { Audience } from "./route/audience.js";
22
- export { type ActionPermissions, type Agent, type AgentBlock, type AgentChat, type AgentChatOptions, type AgentChatSummary, type AgentMessage, type AgentQuestion, type AgentTodo, type AgentToolCall, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type SoftrUserProperties, type User, type WeekDay, assertRouteAccess, checkAccess, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, listAgents, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAgentChat, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
21
+ export { type ActionPermissions, type AiAnswer, type AiFile, type AiGenerateInit, type AiMessage, type AiUsage, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type SoftrUserProperties, type User, type WeekDay, assertRouteAccess, checkAccess, defineAiAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
1
+ import { defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
2
2
  import { useFileUpload } from "./core/use-file-upload.js";
3
3
  import { checkAccess, defineVisibility, isAuthorized } from "./route/visibility.js";
4
4
  import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./route/route.js";
@@ -12,8 +12,6 @@ import { fieldShape, formatDuration, formatField, formatValue, registerSoftrFiel
12
12
  import { useOverrides } from "./core/use-overrides.js";
13
13
  import { useUserTimezone } from "./timezone.js";
14
14
  import { DefaultErrorOverlay } from "./default-error-overlay.js";
15
- import { listAgents } from "./agent/agent-fns.js";
16
- import { useAgentChat } from "./agent/use-agent-chat.js";
17
15
  import { Audience } from "./route/audience.js";
18
16
 
19
- export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, listAgents, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAgentChat, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
17
+ export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineAiAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
package/dist/mcp.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { DatasourceAction, ReadShape } from "./action.js";
2
- import * as _tanstack_react_start6 from "@tanstack/react-start";
1
+ import { DatasourceAction } from "./action.js";
2
+ import * as _tanstack_react_start0 from "@tanstack/react-start";
3
3
 
4
4
  //#region src/mcp.d.ts
5
5
 
6
- type SoftrMcpOptions<TUsers extends ReadShape = ReadShape> = {
6
+ type SoftrMcpOptions = {
7
7
  /**
8
8
  * The actions to expose as tools, from `defineQuery` and `defineMutation`.
9
9
  *
@@ -12,19 +12,6 @@ type SoftrMcpOptions<TUsers extends ReadShape = ReadShape> = {
12
12
  * decide who may run the ones that are listed.
13
13
  */
14
14
  actions?: Array<DatasourceAction>;
15
- /**
16
- * Fills the owner of a person's record from who is calling, the way the app's
17
- * pages do with `auth.currentUserRow`. The field is left out of the tool, so
18
- * the assistant can't leave it wrong or name someone else.
19
- */
20
- owner?: SoftrMcpOwner<TUsers>;
21
- };
22
- type SoftrMcpOwner<TUsers extends ReadShape = ReadShape> = {
23
- /** The people table, and its email field, to find the caller's row in. */
24
- users: DatasourceAction<TUsers>;
25
- email: keyof TUsers["fields"] & string;
26
- /** Listed create or update action id to the field that links to the caller's row. */
27
- fields: Record<string, string>;
28
15
  };
29
16
  /**
30
17
  * Serves the MCP endpoint and its OAuth flow from the app's own origin.
@@ -44,6 +31,6 @@ type SoftrMcpOwner<TUsers extends ReadShape = ReadShape> = {
44
31
  * ],
45
32
  * }))
46
33
  */
47
- declare function softrMcpMiddleware<TUsers extends ReadShape>(options?: SoftrMcpOptions<TUsers>): _tanstack_react_start6.RequestMiddlewareAfterServer<{}, undefined, undefined>;
34
+ declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>;
48
35
  //#endregion
49
36
  export { softrMcpMiddleware };
package/dist/mcp.js CHANGED
@@ -5,10 +5,8 @@ import { softrConfig } from "./softr-config.js";
5
5
  import { sessionConfig } from "./auth/config.js";
6
6
  import { readSessionToken, withSessionToken } from "./auth/session.js";
7
7
  import { isDatasourceAction } from "./action.js";
8
- import { DatasourceError } from "./datasource/record.js";
9
8
  import { makePlatform } from "./auth/platform.js";
10
9
  import { ds } from "./datasource/datasource.js";
11
- import { currentUserRow } from "./auth/current-user-row.js";
12
10
  import { z } from "zod";
13
11
  import { createMiddleware } from "@tanstack/react-start";
14
12
 
@@ -50,14 +48,6 @@ function softrMcpMiddleware(options = {}) {
50
48
  assert(!actions.has(value.id), `two actions share the id "${value.id}"`);
51
49
  actions.set(value.id, value);
52
50
  }
53
- const owner = options.owner;
54
- for (const [actionId, field] of Object.entries(owner?.fields ?? {})) {
55
- const action = actions.get(actionId);
56
- assert(action, `owner names "${actionId}", which is not a listed action`);
57
- const { operation, fields } = action.resource;
58
- assert(operation === "create" || operation === "update", `owner fills "${actionId}", which is a ${operation}`);
59
- assert(field in (fields ?? {}), `"${actionId}" has no field "${field}" for the owner`);
60
- }
61
51
  return createMiddleware({ type: "request" }).server(async ({ next, request }) => {
62
52
  const url = new URL(request.url);
63
53
  const origin = url.origin;
@@ -81,7 +71,7 @@ function softrMcpMiddleware(options = {}) {
81
71
  });
82
72
  case AUTHORIZE_PATH: return await authorize(request, login);
83
73
  case TOKEN_PATH: return await issueToken(request);
84
- case MCP_PATH: return await rpc(request, origin, actions, owner);
74
+ case MCP_PATH: return await rpc(request, origin, actions);
85
75
  default: return await next();
86
76
  }
87
77
  });
@@ -254,8 +244,7 @@ const WHOAMI = {
254
244
  type: "object",
255
245
  properties: {},
256
246
  additionalProperties: false
257
- },
258
- annotations: { readOnlyHint: true }
247
+ }
259
248
  };
260
249
  /** How many records a read returns when the caller does not say. */
261
250
  const DEFAULT_LIMIT = 20;
@@ -268,16 +257,14 @@ const MAX_LIMIT = 100;
268
257
  * that only ever reads one datasource exposes one read, even though its
269
258
  * credentials would allow more.
270
259
  */
271
- function describeTool(action, ownerField) {
260
+ function describeTool(action) {
272
261
  const { operation, fields } = action.resource;
273
262
  const names = Object.keys(fields ?? {});
274
- const writable = names.filter((name) => name !== ownerField);
275
- const fieldProps = Object.fromEntries(writable.map((name) => [name, { description: `The "${name}" field.` }]));
263
+ const fieldProps = Object.fromEntries(names.map((name) => [name, { description: `The "${name}" field.` }]));
276
264
  switch (operation) {
277
265
  case "read": return {
278
266
  name: action.id,
279
267
  description: `Read records from "${action.id}". Returns the fields ${names.join(", ")}. Only records the signed-in user may see come back.`,
280
- annotations: { readOnlyHint: true },
281
268
  inputSchema: {
282
269
  type: "object",
283
270
  properties: {
@@ -298,7 +285,7 @@ function describeTool(action, ownerField) {
298
285
  };
299
286
  case "create": return {
300
287
  name: action.id,
301
- description: `Create a record with "${action.id}". Writable fields: ${writable.join(", ")}.`,
288
+ description: `Create a record with "${action.id}". Writable fields: ${names.join(", ")}.`,
302
289
  inputSchema: {
303
290
  type: "object",
304
291
  properties: fieldProps,
@@ -307,7 +294,7 @@ function describeTool(action, ownerField) {
307
294
  };
308
295
  case "update": return {
309
296
  name: action.id,
310
- description: `Update a record with "${action.id}". Writable fields: ${writable.join(", ")}.`,
297
+ description: `Update a record with "${action.id}". Writable fields: ${names.join(", ")}.`,
311
298
  inputSchema: {
312
299
  type: "object",
313
300
  properties: {
@@ -343,7 +330,7 @@ function describeTool(action, ownerField) {
343
330
  * visitor's user groups exactly as it would a click in the app. Nothing here
344
331
  * decides what is allowed.
345
332
  */
346
- async function runTool(action, args, owner) {
333
+ async function runTool(action, args) {
347
334
  const typed = action;
348
335
  switch (action.resource.operation) {
349
336
  case "read": {
@@ -355,21 +342,13 @@ async function runTool(action, args, owner) {
355
342
  nextOffset: result$1.nextOffset
356
343
  };
357
344
  }
358
- case "create": {
359
- const data = writableFields(action, args, owner?.field);
360
- if (owner !== null) {
361
- const row = await owner.row();
362
- if (row === null) throw new Error("The visitor has no row in the people table, so this record can't be created for them.");
363
- data[owner.field] = { id: row.id };
364
- }
365
- return await ds.create(typed, { data });
366
- }
345
+ case "create": return await ds.create(typed, { data: writableFields(action, args) });
367
346
  case "update": {
368
347
  const { id,...data } = args;
369
348
  assert(typeof id === "string" && id !== "", "update needs a record id");
370
349
  return await ds.update(typed, {
371
350
  id,
372
- data: writableFields(action, data, owner?.field)
351
+ data: writableFields(action, data)
373
352
  });
374
353
  }
375
354
  case "delete": {
@@ -380,19 +359,8 @@ async function runTool(action, args, owner) {
380
359
  }
381
360
  }
382
361
  }
383
- function ownerFieldOf(owner, actionId) {
384
- return owner !== void 0 && Object.hasOwn(owner.fields, actionId) ? owner.fields[actionId] : void 0;
385
- }
386
- function ownerFill(owner, actionId, sessionToken) {
387
- const field = ownerFieldOf(owner, actionId);
388
- if (owner === void 0 || field === void 0) return null;
389
- return {
390
- field,
391
- row: async () => currentUserRow(await resolveUser(sessionToken), owner.users, owner.email)
392
- };
393
- }
394
362
  /**
395
- * Only the fields the action declared, less the owner it fills itself.
363
+ * Only the fields the action declared.
396
364
  *
397
365
  * `additionalProperties: false` in the tool schema is a hint to the model, not
398
366
  * a check — a client can send whatever it likes. An unlisted key reaches the
@@ -400,18 +368,9 @@ function ownerFill(owner, actionId, sessionToken) {
400
368
  * straight through. So the action's field list gets enforced here, or it isn't
401
369
  * a boundary at all.
402
370
  */
403
- function writableFields(action, args, ownerField) {
371
+ function writableFields(action, args) {
404
372
  const allowed = action.resource.fields ?? {};
405
- return Object.fromEntries(Object.entries(args).filter(([name]) => name in allowed && name !== ownerField));
406
- }
407
- /**
408
- * The error as the assistant should read it. A row rule that keeps the caller
409
- * from changing a record answers 404, the same as a deleted record, so a bare
410
- * "not found" would leave the assistant guessing.
411
- */
412
- function describeError(error) {
413
- if (error instanceof DatasourceError && error.status === 404) return "Refused: this record doesn't exist, or the visitor's user group may not change it. If it still shows in a read, it is a permission. Tell the visitor they don't have access to this action, and that the app's builder can grant it.";
414
- return error instanceof Error ? error.message : String(error);
373
+ return Object.fromEntries(Object.entries(args).filter(([name]) => name in allowed));
415
374
  }
416
375
  function clampLimit(raw) {
417
376
  if (typeof raw !== "number" || !Number.isInteger(raw)) return DEFAULT_LIMIT;
@@ -421,15 +380,13 @@ function clampOffset(raw) {
421
380
  if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) return null;
422
381
  return raw;
423
382
  }
424
- async function rpc(request, origin, actions, owner) {
383
+ async function rpc(request, origin, actions) {
425
384
  if (request.method !== "POST") return methodNotAllowed("POST");
426
385
  const token = bearerToken(request);
427
386
  if (token === null) return unauthorized(origin);
428
387
  const session = await platformSession(token);
429
- let sessionToken;
430
- if (session.ok) sessionToken = session.sessionToken;
431
- else if (session.refused && await resolveUser(token) !== null) sessionToken = token;
432
- else return session.refused ? unauthorized(origin) : unavailable();
388
+ if (!session.ok) return session.refused ? unauthorized(origin) : unavailable();
389
+ const { sessionToken } = session;
433
390
  const body = await readJson(request);
434
391
  const message = z.object({
435
392
  id: z.union([z.string(), z.number()]).optional(),
@@ -458,7 +415,7 @@ async function rpc(request, origin, actions, owner) {
458
415
  }
459
416
  });
460
417
  }
461
- case "tools/list": return result(id, { tools: [WHOAMI, ...[...actions.values()].map((action) => describeTool(action, ownerFieldOf(owner, action.id)))] });
418
+ case "tools/list": return result(id, { tools: [WHOAMI, ...[...actions.values()].map(describeTool)] });
462
419
  case "tools/call": {
463
420
  const name = params?.name;
464
421
  const args = typeof params?.arguments === "object" && params.arguments !== null ? params.arguments : {};
@@ -470,13 +427,13 @@ async function rpc(request, origin, actions, owner) {
470
427
  const action = typeof name === "string" ? actions.get(name) : void 0;
471
428
  if (action === void 0) return failure(id, -32602, `Unknown tool: ${String(name)}`);
472
429
  try {
473
- return result(id, toolResult(await withSessionToken(sessionToken, () => runTool(action, args, ownerFill(owner, action.id, sessionToken)))));
430
+ return result(id, toolResult(await withSessionToken(sessionToken, () => runTool(action, args))));
474
431
  } catch (error) {
475
432
  return result(id, {
476
433
  isError: true,
477
434
  content: [{
478
435
  type: "text",
479
- text: describeError(error)
436
+ text: error instanceof Error ? error.message : String(error)
480
437
  }]
481
438
  });
482
439
  }
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.90-alpha.7";
2
+ var version = "0.0.91-alpha.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/server.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { upload } from "./core/file-upload.js";
2
+ import { AiError, ai } from "./core/ai.js";
2
3
  import { auth } from "./auth/server.js";
3
4
  import { FieldChoice, RecordOption, RecordOptions } from "./core/options.js";
4
5
  import { ds } from "./datasource/datasource.js";
5
6
  import { workflow } from "./core/workflow.js";
6
7
  import { integration } from "./core/integration.js";
7
8
  import "@tanstack/react-start/server-only";
8
- export { type FieldChoice, type RecordOption, type RecordOptions, auth, ds, integration, upload, workflow };
9
+ export { AiError, type FieldChoice, type RecordOption, type RecordOptions, ai, auth, ds, integration, upload, workflow };
package/dist/server.js CHANGED
@@ -3,6 +3,7 @@ import { ds } from "./datasource/datasource.js";
3
3
  import { auth } from "./auth/server.js";
4
4
  import { workflow } from "./core/workflow.js";
5
5
  import { integration } from "./core/integration.js";
6
+ import { AiError, ai } from "./core/ai.js";
6
7
  import "@tanstack/react-start/server-only";
7
8
 
8
- export { auth, ds, integration, upload, workflow };
9
+ export { AiError, ai, auth, ds, integration, upload, workflow };