@mindstudio-ai/interface 0.1.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/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # @mindstudio-ai/interface
2
+
3
+ Frontend SDK for [MindStudio](https://mindstudio.ai) v2 app web interfaces.
4
+
5
+ Typed RPC to backend methods, file uploads, agent chat, and user context — all from the browser. Zero dependencies.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @mindstudio-ai/interface
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```tsx
16
+ import { createClient, platform, auth } from '@mindstudio-ai/interface';
17
+
18
+ const api = createClient();
19
+
20
+ function App() {
21
+ const [dashboard, setDashboard] = useState(null);
22
+
23
+ useEffect(() => {
24
+ api.getDashboard().then(setDashboard);
25
+ }, []);
26
+
27
+ const handleUpload = async (file: File) => {
28
+ const url = await platform.uploadFile(file);
29
+ // use the CDN url...
30
+ };
31
+
32
+ return (
33
+ <div>
34
+ <p>Welcome, {auth.name}</p>
35
+ <img src={auth.profilePictureUrl} />
36
+ <input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
37
+ </div>
38
+ );
39
+ }
40
+ ```
41
+
42
+ ## API
43
+
44
+ ### `createClient<T>()`
45
+
46
+ Returns a typed RPC client. Each method maps to a backend route:
47
+
48
+ ```ts
49
+ const api = createClient();
50
+
51
+ const result = await api.submitVendorRequest({ name: 'Acme' });
52
+ const dashboard = await api.getDashboard();
53
+ ```
54
+
55
+ For type safety, pass an interface matching your backend routes:
56
+
57
+ ```ts
58
+ import type { SubmitVendorInput } from '../../backend/src/submitVendorRequest';
59
+
60
+ interface AppRoutes {
61
+ submitVendorRequest(input: SubmitVendorInput): Promise<{ vendorId: string }>;
62
+ getDashboard(): Promise<DashboardData>;
63
+ }
64
+
65
+ const api = createClient<AppRoutes>();
66
+ ```
67
+
68
+ ### `platform.uploadFile(file)`
69
+
70
+ Upload a file to the MindStudio CDN. Returns a public CDN URL.
71
+
72
+ ```ts
73
+ const url = await platform.uploadFile(file);
74
+ ```
75
+
76
+ ### `auth`
77
+
78
+ Current user's identity (read-only, synchronous):
79
+
80
+ ```ts
81
+ auth.userId // "uuid"
82
+ auth.name // "Sean"
83
+ auth.email // "sean@example.com"
84
+ auth.profilePictureUrl // "https://..." or null
85
+ ```
86
+
87
+ For display purposes only. Role checking and permissions are handled by backend routes using `@mindstudio-ai/agent`.
88
+
89
+ ### `createAgentChatClient()`
90
+
91
+ Stateless client for thread-based conversations with AI agents. The agent runs server-side with access to your app's methods as tools.
92
+
93
+ #### Thread management
94
+
95
+ ```ts
96
+ import { createAgentChatClient } from '@mindstudio-ai/interface';
97
+
98
+ const chat = createAgentChatClient();
99
+
100
+ const thread = await chat.createThread();
101
+ const { threads, nextCursor } = await chat.listThreads();
102
+ const full = await chat.getThread(thread.id);
103
+ await chat.updateThread(thread.id, 'New title');
104
+ await chat.deleteThread(thread.id);
105
+
106
+ // Paginate
107
+ const page2 = await chat.listThreads(nextCursor);
108
+ ```
109
+
110
+ #### Sending messages
111
+
112
+ `sendMessage` streams the agent's response via SSE. Named callbacks handle common events; the catch-all `onEvent` receives everything as a discriminated union.
113
+
114
+ ```tsx
115
+ function ChatInput({ threadId }: { threadId: string }) {
116
+ const [text, setText] = useState('');
117
+ const [thinking, setThinking] = useState('');
118
+ const [tools, setTools] = useState<Map<string, string>>(new Map());
119
+
120
+ const send = (content: string) => {
121
+ const response = chat.sendMessage(threadId, content, {
122
+ // Text deltas — append, don't replace
123
+ onText: (delta) => setText((prev) => prev + delta),
124
+
125
+ // Extended thinking (also deltas)
126
+ onThinking: (delta) => setThinking((prev) => prev + delta),
127
+ onThinkingComplete: (thinking, signature) => setThinking(''),
128
+
129
+ // Tool execution
130
+ onToolCallStart: (id, name) =>
131
+ setTools((m) => new Map(m).set(id, `Running ${name}...`)),
132
+ onToolCallResult: (id, output) =>
133
+ setTools((m) => new Map(m).set(id, JSON.stringify(output))),
134
+
135
+ // Errors
136
+ onError: (error) => console.error('Stream error:', error),
137
+
138
+ // Catch-all for logging or low-level events (tool_use, tool_input_delta)
139
+ onEvent: (event) => console.log(event.type, event),
140
+ });
141
+
142
+ // Resolves when stream completes
143
+ response.then(({ stopReason, usage }) => {
144
+ console.log(`Done: ${stopReason}, tokens: ${usage.inputTokens}+${usage.outputTokens}`);
145
+ });
146
+
147
+ // Cancel mid-stream
148
+ // response.abort();
149
+ };
150
+ }
151
+ ```
152
+
153
+ #### Abort support
154
+
155
+ `sendMessage` returns an `AbortablePromise` — a standard Promise with an `.abort()` method. You can also pass an `AbortSignal` via the callbacks:
156
+
157
+ ```ts
158
+ const controller = new AbortController();
159
+
160
+ const response = chat.sendMessage(threadId, content, {
161
+ onText: (delta) => setText((prev) => prev + delta),
162
+ signal: controller.signal,
163
+ });
164
+
165
+ // Either works:
166
+ response.abort();
167
+ controller.abort();
168
+ ```
169
+
170
+ #### Attachments
171
+
172
+ Send images or documents alongside a message. Upload files first via `platform.uploadFile()`, then pass the CDN URLs:
173
+
174
+ ```ts
175
+ const url = await platform.uploadFile(file);
176
+
177
+ chat.sendMessage(threadId, "What's in this document?", {
178
+ onText: (delta) => setText((prev) => prev + delta),
179
+ }, {
180
+ attachments: [url],
181
+ });
182
+ ```
183
+
184
+ - **Images** (`i.mscdn.ai`): Sent to the model as vision input (one image per message)
185
+ - **Documents** (`f.mscdn.ai`): Text extracted server-side and included in context
186
+
187
+ Attachments are preserved in thread history — when you load a thread via `getThread()`, user messages include their original `attachments` array.
188
+
189
+ #### SSE event types
190
+
191
+ All events are available via the `onEvent` catch-all as the `AgentChatEvent` discriminated union:
192
+
193
+ | Event | Fields | Named callback |
194
+ |-------|--------|----------------|
195
+ | `text` | `text` (delta) | `onText` |
196
+ | `thinking` | `text` (delta) | `onThinking` |
197
+ | `thinking_complete` | `thinking`, `signature` | `onThinkingComplete` |
198
+ | `tool_call_start` | `id`, `name` | `onToolCallStart` |
199
+ | `tool_call_result` | `id`, `output` | `onToolCallResult` |
200
+ | `error` | `error` | `onError` |
201
+ | `tool_use` | `id`, `name`, `input` | `onEvent` only |
202
+ | `tool_input_delta` | `id`, `name`, `delta` | `onEvent` only |
203
+ | `done` | `stopReason`, `usage` | resolves the Promise |
204
+
205
+ ## Error handling
206
+
207
+ ```ts
208
+ import { MindStudioInterfaceError } from '@mindstudio-ai/interface';
209
+
210
+ try {
211
+ await api.submitVendorRequest({ name: '' });
212
+ } catch (err) {
213
+ if (err instanceof MindStudioInterfaceError) {
214
+ console.error(err.message); // human-readable
215
+ console.error(err.code); // 'route_error', 'forbidden', etc.
216
+ console.error(err.status); // HTTP status
217
+ }
218
+ }
219
+ ```
220
+
221
+ ## How it works
222
+
223
+ The MindStudio platform injects `window.__MINDSTUDIO__` into the page before your code runs. This contains the session token, user info, and method registry. The SDK reads this automatically — no configuration needed.
224
+
225
+ All API calls use same-origin `/_/` paths (e.g. `/_/methods/{id}/invoke`, `/_/agent/threads`). The platform proxy resolves the app from the subdomain — no cross-origin requests or app IDs in URLs. This works identically in production and local dev.
226
+
227
+ ## License
228
+
229
+ MIT
@@ -0,0 +1,668 @@
1
+ /**
2
+ * Method RPC client — typed function calls to backend methods.
3
+ *
4
+ * `createClient()` returns a Proxy where every property access creates
5
+ * an async function that invokes the corresponding backend method via HTTP.
6
+ * The method name matches the method's `export` field in `app.json`.
7
+ *
8
+ * ## How it works
9
+ *
10
+ * ```
11
+ * api.submitVendorRequest({ name: 'Acme' })
12
+ * │
13
+ * ├─ Look up method ID: config.methods["submitVendorRequest"]
14
+ * │ → "submit-vendor-request"
15
+ * │
16
+ * ├─ POST /_/methods/submit-vendor-request/invoke
17
+ * │ Authorization: Bearer {session token}
18
+ * │ Body: { input: { name: "Acme" } }
19
+ * │
20
+ * └─ Returns response.output (or throws on error)
21
+ * ```
22
+ *
23
+ * The method registry (export name → method ID mapping) is embedded in
24
+ * `window.__MINDSTUDIO__.methods` at page load time — no extra HTTP call
25
+ * needed to discover methods.
26
+ *
27
+ * ## Streaming
28
+ *
29
+ * Methods can optionally stream LLM token output via SSE. Pass
30
+ * `{ stream: true }` as the second argument along with an `onToken`
31
+ * callback to receive incremental updates:
32
+ *
33
+ * ```ts
34
+ * const result = await api.submitVendorRequest({ name: 'Acme' }, {
35
+ * stream: true,
36
+ * onToken: (text) => setResponseText(text),
37
+ * });
38
+ * ```
39
+ *
40
+ * **Important:** The `text` value passed to `onToken` is the accumulated
41
+ * response so far (not a delta). Replace your display content each time —
42
+ * do not append. See {@link InvokeOptions.onToken} for details.
43
+ *
44
+ * ## Type safety
45
+ *
46
+ * Since the frontend and backend live in the same project, you can import
47
+ * types directly:
48
+ *
49
+ * ```ts
50
+ * import type { SubmitVendorInput, SubmitVendorOutput }
51
+ * from '../../backend/src/submitVendorRequest';
52
+ *
53
+ * interface AppMethods {
54
+ * submitVendorRequest(input: SubmitVendorInput): Promise<SubmitVendorOutput>;
55
+ * getDashboard(): Promise<GetDashboardOutput>;
56
+ * }
57
+ *
58
+ * const api = createClient<AppMethods>();
59
+ * const result = await api.submitVendorRequest({ name: 'Acme' }); // fully typed
60
+ * ```
61
+ *
62
+ * If no type parameter is provided, methods accept `any` input and return `any`.
63
+ */
64
+ /**
65
+ * Options for method invocation. Pass as the optional second argument to
66
+ * any method on the client returned by {@link createClient}.
67
+ *
68
+ * When `stream` is `true`, the SDK sends `{ stream: true }` in the request
69
+ * body and parses the response as an SSE (Server-Sent Events) stream.
70
+ * Three event types are handled:
71
+ *
72
+ * | SSE `type` | Payload field | SDK action |
73
+ * |------------|---------------|------------|
74
+ * | `token` | `text` | Calls {@link onToken} with accumulated text |
75
+ * | `error` | `error` | Calls {@link onStreamError} with the message |
76
+ * | `done` | `output` | Captures as the method's return value |
77
+ *
78
+ * The method's `Promise` resolves with the final `output` from the `done`
79
+ * event — the same shape you'd get without streaming.
80
+ */
81
+ interface InvokeOptions {
82
+ /**
83
+ * Enable SSE streaming for LLM token output.
84
+ *
85
+ * When `true`, the request body includes `{ stream: true }` and the
86
+ * response is parsed as an SSE stream instead of a single JSON payload.
87
+ * The method still returns a `Promise` that resolves with the final
88
+ * output once the stream completes.
89
+ *
90
+ * @default false
91
+ */
92
+ stream?: boolean;
93
+ /**
94
+ * Called with the accumulated response text each time a new token arrives.
95
+ *
96
+ * **Important:** `text` is the full response so far, not a delta/chunk.
97
+ * Your frontend should **replace** (not append to) its display content
98
+ * with this value each time the callback fires.
99
+ *
100
+ * This design avoids edge cases where the platform sanitizes or
101
+ * reformats JSON mid-stream, causing earlier portions of the text to
102
+ * change between tokens.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * const result = await api.generate({ prompt: 'Hello' }, {
107
+ * stream: true,
108
+ * onToken: (text) => {
109
+ * // Replace — do NOT append
110
+ * responseElement.textContent = text;
111
+ * },
112
+ * });
113
+ * ```
114
+ */
115
+ onToken?: (text: string) => void;
116
+ /**
117
+ * Called if a stream-level error occurs.
118
+ *
119
+ * Stream errors are non-fatal at the protocol level — the method may
120
+ * still complete and resolve with a final output. Use this callback for
121
+ * logging or showing a transient warning, not for control flow.
122
+ *
123
+ * If the stream ends without a `done` event, the method rejects with a
124
+ * `MindStudioInterfaceError` (code `stream_incomplete`) regardless of
125
+ * whether `onStreamError` was provided.
126
+ */
127
+ onStreamError?: (error: string) => void;
128
+ }
129
+ /**
130
+ * Default client type when no generic parameter is provided.
131
+ * Every property returns an async function accepting optional input
132
+ * and optional {@link InvokeOptions} for streaming.
133
+ */
134
+ type DefaultMethodClient = Record<string, (input?: Record<string, unknown>, options?: InvokeOptions) => Promise<unknown>>;
135
+ /**
136
+ * Create a typed RPC client for calling backend methods.
137
+ *
138
+ * @typeParam T - Optional interface mapping method names to their signatures.
139
+ * If omitted, all methods accept `any` and return `Promise<any>`.
140
+ *
141
+ * @returns A Proxy object where each property is an async function that
142
+ * invokes the corresponding backend method.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * import { createClient } from '@mindstudio-ai/interface';
147
+ *
148
+ * const api = createClient();
149
+ *
150
+ * // Each method maps to a backend method export
151
+ * const result = await api.submitVendorRequest({ name: 'Acme' });
152
+ * const dashboard = await api.getDashboard();
153
+ *
154
+ * // Stream LLM output — onToken receives accumulated text (replace, don't append)
155
+ * const streamed = await api.submitVendorRequest({ name: 'Acme' }, {
156
+ * stream: true,
157
+ * onToken: (text) => setResponseText(text),
158
+ * });
159
+ * // `streamed` is the same final output you'd get without streaming
160
+ * ```
161
+ */
162
+ declare function createClient<T = DefaultMethodClient>(): T;
163
+
164
+ /**
165
+ * Agent chat client — thread-based conversations with AI agents.
166
+ *
167
+ * `createAgentChatClient()` returns an object with methods for thread
168
+ * lifecycle (create, list, get, rename, delete) and message streaming.
169
+ * The agent runs server-side with access to the app's methods as tools.
170
+ *
171
+ * ## How it works
172
+ *
173
+ * ```
174
+ * chat.sendMessage(threadId, 'Help me prep for my review', {
175
+ * onText: (delta) => setAssistantMessage((prev) => prev + delta),
176
+ * })
177
+ * │
178
+ * ├─ POST /_/agent/threads/{threadId}/messages
179
+ * │ Authorization: Bearer {session token}
180
+ * │ Body: { content: "Help me prep for my review" }
181
+ * │
182
+ * ├─ Response: SSE stream
183
+ * │ data: {"type":"text","text":"I'll create...","ts":1711...}
184
+ * │ data: {"type":"tool_call_start","id":"call_abc","name":"create-todo","ts":1711...}
185
+ * │ data: {"type":"tool_call_result","id":"call_abc","output":{...},"ts":1711...}
186
+ * │ data: {"type":"text","text":"Done — added it.","ts":1711...}
187
+ * │ data: {"type":"done","stopReason":"end_turn","usage":{...},"ts":1711...}
188
+ * │
189
+ * └─ Promise resolves with { stopReason, usage }
190
+ * ```
191
+ *
192
+ * ## Stateless design
193
+ *
194
+ * This client is a thin wrapper over HTTP endpoints. It does not manage
195
+ * local state — your React/framework layer owns the thread list, message
196
+ * array, and UI state. Each method is an independent request.
197
+ *
198
+ * ## Streaming callbacks
199
+ *
200
+ * `sendMessage` accepts named callbacks for common events (onText,
201
+ * onThinking, onToolCallStart, etc.) plus a catch-all `onEvent` that
202
+ * receives every SSE event as a discriminated union. Both fire for the
203
+ * same event — use named callbacks for convenience, `onEvent` for
204
+ * logging or handling low-level events like `tool_use` and
205
+ * `tool_input_delta`.
206
+ *
207
+ * @example
208
+ * ```ts
209
+ * import { createAgentChatClient } from '@mindstudio-ai/interface';
210
+ *
211
+ * const chat = createAgentChatClient();
212
+ *
213
+ * // Thread lifecycle
214
+ * const thread = await chat.createThread();
215
+ * const { threads } = await chat.listThreads();
216
+ * const full = await chat.getThread(thread.id);
217
+ *
218
+ * // Send a message with streaming
219
+ * const response = chat.sendMessage(thread.id, 'Hello!', {
220
+ * onText: (delta) => setMessage((prev) => prev + delta),
221
+ * onToolCallStart: (id, name) => showToolSpinner(name),
222
+ * onToolCallResult: (id, output) => showToolResult(output),
223
+ * });
224
+ *
225
+ * const { stopReason, usage } = await response;
226
+ *
227
+ * // Abort mid-stream
228
+ * response.abort();
229
+ * ```
230
+ */
231
+ /** Thread summary returned from create and list endpoints. */
232
+ interface ThreadSummary {
233
+ /** Thread ID (UUID). */
234
+ id: string;
235
+ /** User-editable title, auto-generated on first message. */
236
+ title: string | null;
237
+ /** ISO 8601 creation timestamp. */
238
+ dateCreated: string;
239
+ /** ISO 8601 last-updated timestamp. */
240
+ dateUpdated: string;
241
+ }
242
+ /** Full thread with message history, returned from get endpoint. */
243
+ interface Thread {
244
+ /** Thread ID (UUID). */
245
+ id: string;
246
+ /** User-editable title. */
247
+ title: string | null;
248
+ /** Full conversation history. */
249
+ messages: Message[];
250
+ /** ISO 8601 creation timestamp. */
251
+ dateCreated: string;
252
+ /** ISO 8601 last-updated timestamp. */
253
+ dateUpdated: string;
254
+ }
255
+ /** A tool call requested by the assistant. */
256
+ interface ToolCall {
257
+ /** Tool call ID, used to match results. */
258
+ id: string;
259
+ /** Method name. */
260
+ name: string;
261
+ /** Input arguments. */
262
+ input: Record<string, unknown>;
263
+ }
264
+ /** A single message in a thread's conversation history. */
265
+ interface Message {
266
+ /** `"user"` for human messages, `"assistant"` for agent responses. */
267
+ role: 'user' | 'assistant';
268
+ /** Message text content. For tool results, this is JSON-stringified output. */
269
+ content: string;
270
+ /**
271
+ * MindStudio CDN URLs attached to this message (user messages only).
272
+ *
273
+ * Images (`i.mscdn.ai`) are sent to the model as vision input.
274
+ * Documents (`f.mscdn.ai`, `files.mindstudio-cdn.com`) have their text
275
+ * extracted server-side and included in the model context. The original
276
+ * URLs are preserved in thread history for the frontend to render.
277
+ */
278
+ attachments?: string[];
279
+ /** Tool calls the assistant requested (assistant messages only). */
280
+ toolCalls?: ToolCall[];
281
+ /** ID of the tool call this message is a result for (tool result messages only). */
282
+ toolCallId?: string;
283
+ /** Whether this tool result represents an error. */
284
+ isToolError?: boolean;
285
+ }
286
+ /** Paginated thread list response. */
287
+ interface ThreadListPage {
288
+ /** Threads for this page. */
289
+ threads: ThreadSummary[];
290
+ /** Cursor for the next page, or `null` if this is the last page. */
291
+ nextCursor: string | null;
292
+ }
293
+ /** Assistant text delta — append to your display string, do not replace. */
294
+ interface AgentTextEvent {
295
+ type: 'text';
296
+ text: string;
297
+ ts: number;
298
+ }
299
+ /** Model thinking text (extended thinking). */
300
+ interface AgentThinkingEvent {
301
+ type: 'thinking';
302
+ text: string;
303
+ ts: number;
304
+ }
305
+ /** Thinking complete with full text and signature. */
306
+ interface AgentThinkingCompleteEvent {
307
+ type: 'thinking_complete';
308
+ thinking: string;
309
+ signature: string;
310
+ ts: number;
311
+ }
312
+ /** Model requested a tool call (includes parsed input). */
313
+ interface AgentToolUseEvent {
314
+ type: 'tool_use';
315
+ id: string;
316
+ name: string;
317
+ input: Record<string, unknown>;
318
+ ts: number;
319
+ }
320
+ /** Streaming delta for tool call input JSON. */
321
+ interface AgentToolInputDeltaEvent {
322
+ type: 'tool_input_delta';
323
+ id: string;
324
+ name: string;
325
+ delta: string;
326
+ ts: number;
327
+ }
328
+ /** Agent began executing a tool call. */
329
+ interface AgentToolCallStartEvent {
330
+ type: 'tool_call_start';
331
+ id: string;
332
+ name: string;
333
+ ts: number;
334
+ }
335
+ /** Tool call returned a result. */
336
+ interface AgentToolCallResultEvent {
337
+ type: 'tool_call_result';
338
+ id: string;
339
+ output: unknown;
340
+ ts: number;
341
+ }
342
+ /** Stream complete. */
343
+ interface AgentDoneEvent {
344
+ type: 'done';
345
+ stopReason: string;
346
+ usage: {
347
+ inputTokens: number;
348
+ outputTokens: number;
349
+ };
350
+ ts: number;
351
+ }
352
+ /** Stream-level error. */
353
+ interface AgentErrorEvent {
354
+ type: 'error';
355
+ error: string;
356
+ ts: number;
357
+ }
358
+ /** Discriminated union of all SSE events from the agent chat stream. */
359
+ type AgentChatEvent = AgentTextEvent | AgentThinkingEvent | AgentThinkingCompleteEvent | AgentToolUseEvent | AgentToolInputDeltaEvent | AgentToolCallStartEvent | AgentToolCallResultEvent | AgentDoneEvent | AgentErrorEvent;
360
+ /**
361
+ * Callbacks for `sendMessage` streaming.
362
+ *
363
+ * Named callbacks fire for common events. The catch-all `onEvent` fires
364
+ * for every event including those with named callbacks — use it for
365
+ * logging or handling low-level events like `tool_use` and
366
+ * `tool_input_delta`.
367
+ */
368
+ interface SendMessageCallbacks {
369
+ /**
370
+ * Called with each text delta as the assistant responds.
371
+ *
372
+ * **Important:** `text` is a delta (new chunk), not the accumulated
373
+ * response. Append it to your display string — do not replace.
374
+ *
375
+ * @example
376
+ * ```ts
377
+ * onText: (delta) => setMessage((prev) => prev + delta)
378
+ * ```
379
+ */
380
+ onText?: (text: string) => void;
381
+ /** Called with model thinking text (extended thinking). */
382
+ onThinking?: (text: string) => void;
383
+ /** Called when thinking is complete. */
384
+ onThinkingComplete?: (thinking: string, signature: string) => void;
385
+ /** Called when a tool call begins executing. */
386
+ onToolCallStart?: (id: string, name: string) => void;
387
+ /** Called when a tool call produces a result. */
388
+ onToolCallResult?: (id: string, output: unknown) => void;
389
+ /** Called on a stream-level error event. */
390
+ onError?: (error: string) => void;
391
+ /** Called for every SSE event (including ones with named callbacks). */
392
+ onEvent?: (event: AgentChatEvent) => void;
393
+ /** AbortSignal to cancel the stream. */
394
+ signal?: AbortSignal;
395
+ }
396
+ /** Options for `sendMessage`. */
397
+ interface SendMessageOptions {
398
+ /**
399
+ * MindStudio CDN URLs to attach to the message.
400
+ *
401
+ * Images (`i.mscdn.ai`) are sent to the model as vision input (one per message).
402
+ * Documents (`f.mscdn.ai`, `files.mindstudio-cdn.com`) have their text
403
+ * extracted server-side and included in context.
404
+ *
405
+ * Upload files first via `platform.uploadFile()`.
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * const url = await platform.uploadFile(file);
410
+ * chat.sendMessage(threadId, 'What is this?', callbacks, {
411
+ * attachments: [url],
412
+ * });
413
+ * ```
414
+ */
415
+ attachments?: string[];
416
+ }
417
+ /** Resolved value of `sendMessage` — extracted from the `done` event. */
418
+ interface SendMessageResult {
419
+ /** Why the agent stopped (`"end_turn"`, `"max_tokens"`, etc.). */
420
+ stopReason: string;
421
+ /** Token usage for the full agent loop. */
422
+ usage: {
423
+ inputTokens: number;
424
+ outputTokens: number;
425
+ };
426
+ }
427
+ /** A Promise with an `abort()` method for cancelling the stream. */
428
+ type AbortablePromise<T> = Promise<T> & {
429
+ /** Cancel the in-flight stream. The promise rejects with `AbortError`. */
430
+ abort: () => void;
431
+ };
432
+ /** Agent chat client returned by {@link createAgentChatClient}. */
433
+ interface AgentChatClient {
434
+ /** Create a new conversation thread. */
435
+ createThread(): Promise<ThreadSummary>;
436
+ /** List threads with cursor-based pagination (50 per page). */
437
+ listThreads(cursor?: string): Promise<ThreadListPage>;
438
+ /** Get a thread with its full message history. */
439
+ getThread(threadId: string): Promise<Thread>;
440
+ /** Update a thread's title. */
441
+ updateThread(threadId: string, title: string): Promise<void>;
442
+ /** Delete a thread (soft delete). */
443
+ deleteThread(threadId: string): Promise<void>;
444
+ /**
445
+ * Send a message and stream the agent's response.
446
+ *
447
+ * Returns an {@link AbortablePromise} that resolves with
448
+ * `{ stopReason, usage }` when the stream completes. Call `.abort()`
449
+ * on the returned promise to cancel mid-stream.
450
+ *
451
+ * @param options.attachments - MindStudio CDN URLs to attach.
452
+ * Images (`i.mscdn.ai`) are sent as vision input.
453
+ * Documents (`f.mscdn.ai`) have text extracted into context.
454
+ * Upload files first via `platform.uploadFile()`.
455
+ */
456
+ sendMessage(threadId: string, content: string, callbacks?: SendMessageCallbacks, options?: SendMessageOptions): AbortablePromise<SendMessageResult>;
457
+ }
458
+ /**
459
+ * Create an agent chat client for thread-based conversations.
460
+ *
461
+ * The client is stateless — each method is an independent HTTP request.
462
+ * Your application layer owns thread state, message arrays, and UI updates.
463
+ *
464
+ * @returns An {@link AgentChatClient} with methods for thread CRUD and
465
+ * message streaming.
466
+ *
467
+ * @example
468
+ * ```ts
469
+ * import { createAgentChatClient } from '@mindstudio-ai/interface';
470
+ *
471
+ * const chat = createAgentChatClient();
472
+ *
473
+ * const thread = await chat.createThread();
474
+ *
475
+ * const response = chat.sendMessage(thread.id, 'Hello!', {
476
+ * onText: (delta) => setAssistantMessage((prev) => prev + delta),
477
+ * onToolCallStart: (id, name) => showToolSpinner(name),
478
+ * onToolCallResult: (id, output) => showToolResult(output),
479
+ * });
480
+ *
481
+ * const { stopReason, usage } = await response;
482
+ * ```
483
+ */
484
+ declare function createAgentChatClient(): AgentChatClient;
485
+
486
+ /**
487
+ * Platform actions — file uploads via presigned S3 POST.
488
+ *
489
+ * Uses the session token from `window.__MINDSTUDIO__` to request a
490
+ * presigned upload URL from the platform, then uploads directly to S3.
491
+ * Returns the public CDN URL.
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * import { platform } from '@mindstudio-ai/interface';
496
+ *
497
+ * const file = document.querySelector('input[type=file]').files[0];
498
+ * const url = await platform.uploadFile(file);
499
+ *
500
+ * // With progress and abort
501
+ * const controller = new AbortController();
502
+ * const url = await platform.uploadFile(file, {
503
+ * onProgress: (fraction) => console.log(`${Math.round(fraction * 100)}%`),
504
+ * signal: controller.signal,
505
+ * });
506
+ * ```
507
+ */
508
+ /**
509
+ * Options for `platform.uploadFile()`.
510
+ */
511
+ interface UploadFileOptions {
512
+ /** Called with upload progress as a fraction from 0 to 1. */
513
+ onProgress?: (fraction: number) => void;
514
+ /** AbortSignal to cancel the upload. */
515
+ signal?: AbortSignal;
516
+ }
517
+ /**
518
+ * The platform namespace — file upload actions.
519
+ */
520
+ declare const platform: {
521
+ /**
522
+ * Upload a file to the MindStudio CDN.
523
+ *
524
+ * Requests a presigned upload URL from the platform, then uploads
525
+ * the file directly to S3. Returns the public CDN URL.
526
+ *
527
+ * @param file - The File to upload
528
+ * @param options - Optional progress callback and abort signal
529
+ * @returns CDN URL of the uploaded file
530
+ *
531
+ * @example
532
+ * ```ts
533
+ * const url = await platform.uploadFile(file);
534
+ *
535
+ * // With progress
536
+ * const url = await platform.uploadFile(file, {
537
+ * onProgress: (f) => setProgress(f),
538
+ * });
539
+ *
540
+ * // With abort
541
+ * const controller = new AbortController();
542
+ * const url = await platform.uploadFile(file, {
543
+ * signal: controller.signal,
544
+ * });
545
+ * // controller.abort() to cancel
546
+ * ```
547
+ */
548
+ uploadFile(file: File, options?: UploadFileOptions): Promise<string>;
549
+ };
550
+
551
+ /**
552
+ * Auth context — current user's identity for display purposes.
553
+ *
554
+ * Provides synchronous access to the current user's name, email, and
555
+ * profile picture. Hydrated from the bootstrap globals injected by the
556
+ * platform — no HTTP call needed.
557
+ *
558
+ * This is for **display only**. Role checking, permission enforcement,
559
+ * and user lookups for other users are backend concerns — the frontend
560
+ * calls backend methods that handle those via `@mindstudio-ai/agent`'s
561
+ * `auth` namespace.
562
+ *
563
+ * @example
564
+ * ```ts
565
+ * import { auth } from '@mindstudio-ai/interface';
566
+ *
567
+ * return (
568
+ * <div>
569
+ * <p>Welcome, {auth.name}</p>
570
+ * <img src={auth.profilePictureUrl} />
571
+ * </div>
572
+ * );
573
+ * ```
574
+ */
575
+ /**
576
+ * User identity context. All properties are read-only and synchronous.
577
+ */
578
+ interface AuthContext {
579
+ /** Current user's ID (UUID). */
580
+ readonly userId: string;
581
+ /** Current user's display name. */
582
+ readonly name: string;
583
+ /** Current user's email address. */
584
+ readonly email: string;
585
+ /** Current user's profile picture URL, or null if not set. */
586
+ readonly profilePictureUrl: string | null;
587
+ }
588
+ /**
589
+ * Lazy auth proxy — reads from bootstrap globals on first property access.
590
+ *
591
+ * Using a Proxy lets us export `auth` as a module-level constant while
592
+ * deferring the `getConfig()` call until the first property is accessed.
593
+ * This avoids throwing during import if the page hasn't finished loading
594
+ * the bootstrap script yet.
595
+ */
596
+ declare const auth: AuthContext;
597
+
598
+ /**
599
+ * Error class for the @mindstudio-ai/interface SDK.
600
+ *
601
+ * Thrown by `createClient()` method calls on HTTP errors, by `platform`
602
+ * actions on failures, and by the config reader when bootstrap globals
603
+ * are missing.
604
+ */
605
+ declare class MindStudioInterfaceError extends Error {
606
+ /** Machine-readable error code (e.g. 'method_not_found', 'not_initialized'). */
607
+ readonly code: string;
608
+ /** HTTP status code, when the error originated from an API response. */
609
+ readonly status?: number;
610
+ constructor(message: string, code: string, status?: number);
611
+ }
612
+
613
+ /**
614
+ * Type definitions for the @mindstudio-ai/interface SDK.
615
+ *
616
+ * These types describe the bootstrap globals injected by the platform,
617
+ * the user context, and options for platform actions.
618
+ */
619
+ /**
620
+ * Bootstrap configuration injected into the page by the MindStudio
621
+ * platform before the app's JavaScript runs.
622
+ *
623
+ * Available at `window.__MINDSTUDIO__`. The SDK reads this automatically —
624
+ * app code should use `createClient()`, `auth`, and `platform` instead
625
+ * of accessing this directly.
626
+ *
627
+ * Injection mechanism: The Cloudflare Worker on `*.static.mscdn.ai`
628
+ * intercepts requests for `index.html`, injects a `<script>` tag with
629
+ * the session context, and returns the modified HTML.
630
+ */
631
+ interface BootstrapConfig {
632
+ /** Short-lived session token scoped to this app + user. */
633
+ token: string;
634
+ /** App ID (optional — not needed for API calls, resolved from subdomain). */
635
+ appId?: string;
636
+ /** Current release ID. */
637
+ releaseId: string;
638
+ /** API base URL (optional — SDK uses same-origin `/_/` paths). */
639
+ apiBaseUrl?: string;
640
+ /** Resolved user fragment — display info for the current user. */
641
+ user: BootstrapUser;
642
+ /**
643
+ * Method registry mapping export names to method IDs.
644
+ * Embedded at injection time from the release manifest.
645
+ *
646
+ * @example
647
+ * ```
648
+ * { "submitVendorRequest": "submit-vendor-request", "getDashboard": "get-dashboard" }
649
+ * ```
650
+ */
651
+ methods: Record<string, string>;
652
+ }
653
+ /**
654
+ * Resolved user fragment. Includes display info so apps can render
655
+ * personalized UI immediately without an extra API call.
656
+ */
657
+ interface BootstrapUser {
658
+ /** User ID (UUID). */
659
+ id: string;
660
+ /** Display name. */
661
+ name: string;
662
+ /** Email address. */
663
+ email: string;
664
+ /** Profile picture URL, if set. */
665
+ profilePictureUrl?: string;
666
+ }
667
+
668
+ export { type AbortablePromise, type AgentChatClient, type AgentChatEvent, type AuthContext, type BootstrapConfig, type BootstrapUser, type InvokeOptions, type Message, MindStudioInterfaceError, type SendMessageCallbacks, type SendMessageOptions, type SendMessageResult, type Thread, type ThreadListPage, type ThreadSummary, type UploadFileOptions, auth, createAgentChatClient, createClient, platform };
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ var s=class extends Error{code;status;constructor(t,n,i){super(t),this.name="MindStudioInterfaceError",this.code=n,this.status=i}};var k;function h(){if(k)return k;let e=globalThis.__MINDSTUDIO__;if(!e)throw new s("@mindstudio-ai/interface requires the MindStudio platform context. This page must be loaded inside MindStudio (window.__MINDSTUDIO__ is missing).","not_initialized");if(!e.token||!e.releaseId||!e.user||!e.methods){let t=["token","releaseId","user","methods"].filter(n=>!e[n]).join(", ");throw new s(`MindStudio platform context is incomplete. Missing: ${t}`,"invalid_config")}return k=e,k}function v(){return new Proxy({},{get(e,t){return async(n,i)=>{let a=h(),o=a.methods[t];if(!o)throw new s(`Method "${t}" not found. Available methods: ${Object.keys(a.methods).join(", ")||"(none)"}`,"method_not_found");let r=`/_/methods/${o}/invoke`,l=i?.stream===!0,d=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a.token}`},body:JSON.stringify({input:n??{},...l&&{stream:!0}})});if(!d.ok){let p=`Method "${t}" failed: ${d.status} ${d.statusText}`,T="method_error";try{let g=await d.json();g.error&&(p=g.error),g.code&&(T=g.code)}catch{}throw new s(p,T,d.status)}if(!l)return(await d.json()).output;let w=d.body.getReader(),u=new TextDecoder,f="",y;for(;;){let{done:p,value:T}=await w.read();if(p)break;f+=u.decode(T,{stream:!0});let g=f.split(`
2
+ `);f=g.pop()??"";for(let b of g){if(!b.startsWith("data: "))continue;let m=b.slice(6);try{let c=JSON.parse(m);c.type==="token"&&i?.onToken&&c.text?i.onToken(c.text):c.type==="error"&&c.error?i?.onStreamError&&i.onStreamError(c.error):c.type==="done"&&(y=c.output)}catch{}}}if(y===void 0)throw new s("Stream ended without a done event","stream_incomplete");return y}}})}var S="/_/agent";async function x(e,t,n){let a={Authorization:`Bearer ${h().token}`};n!==void 0&&(a["Content-Type"]="application/json");let o=await fetch(`${S}${e}`,{method:t,headers:a,...n!==void 0&&{body:JSON.stringify(n)}});if(!o.ok){let r=`Agent chat request failed: ${o.status} ${o.statusText}`,l="agent_chat_error";try{let d=await o.json();d.error&&(r=d.error),d.code&&(l=d.code)}catch{}throw new s(r,l,o.status)}return await o.json()}function A(e,t){switch(t.onEvent?.(e),e.type){case"text":t.onText?.(e.text);break;case"thinking":t.onThinking?.(e.text);break;case"thinking_complete":t.onThinkingComplete?.(e.thinking,e.signature);break;case"tool_call_start":t.onToolCallStart?.(e.id,e.name);break;case"tool_call_result":t.onToolCallResult?.(e.id,e.output);break;case"error":t.onError?.(e.error);break}}function _(){return{createThread(){return x("/threads","POST")},listThreads(e){let t=e?`?cursor=${encodeURIComponent(e)}`:"";return x(`/threads${t}`,"GET")},getThread(e){return x(`/threads/${e}`,"GET")},async updateThread(e,t){await x(`/threads/${e}`,"PATCH",{title:t})},async deleteThread(e){await x(`/threads/${e}`,"DELETE")},sendMessage(e,t,n,i){let a=new AbortController,o=n??{};o.signal&&(o.signal.aborted?a.abort():o.signal.addEventListener("abort",()=>a.abort(),{once:!0}));let l=(async()=>{let d=h(),w=`${S}/threads/${e}/messages`,u=await fetch(w,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${d.token}`},body:JSON.stringify({content:t,...i?.attachments?.length&&{attachments:i.attachments}}),signal:a.signal});if(!u.ok){let g=`Send message failed: ${u.status} ${u.statusText}`,b="agent_chat_error";try{let m=await u.json();m.error&&(g=m.error),m.code&&(b=m.code)}catch{}throw new s(g,b,u.status)}let f=u.body.getReader(),y=new TextDecoder,p="",T;for(;;){let{done:g,value:b}=await f.read();if(g)break;p+=y.decode(b,{stream:!0});let m=p.split(`
3
+ `);p=m.pop()??"";for(let c of m){if(!c.startsWith("data: "))continue;let E=c.slice(6);try{let C=JSON.parse(E);C.type==="done"&&(T={stopReason:C.stopReason,usage:C.usage}),A(C,o)}catch{}}}if(!T)throw new s("Stream ended without a done event","stream_incomplete");return T})();return l.abort=()=>a.abort(),l}}}var M={async uploadFile(e,t){let n=h(),{onProgress:i,signal:a}=t??{};a?.throwIfAborted();let r=await fetch("/_/generate-upload-request",{method:"POST",headers:{Authorization:`Bearer ${n.token}`,"Content-Type":"application/json"},body:JSON.stringify({filename:e.name,contentType:e.type||"application/octet-stream"}),signal:a});if(!r.ok)throw new s(`Upload presign failed: ${r.status} ${r.statusText}`,"upload_error",r.status);let{uploadUrl:l,uploadFields:d,publicUrl:w}=await r.json(),u=new FormData;for(let[y,p]of Object.entries(d))u.append(y,p);if(u.append("file",e),i)return await P(l,u,i,a),w;let f=await fetch(l,{method:"POST",body:u,signal:a});if(!f.ok)throw new s(`File upload failed: ${f.status} ${f.statusText}`,"upload_error",f.status);return w}};function P(e,t,n,i){return new Promise((a,o)=>{let r=new XMLHttpRequest;if(r.upload.addEventListener("progress",l=>{l.lengthComputable&&n(l.loaded/l.total)}),r.addEventListener("load",()=>{r.status>=200&&r.status<300?(n(1),a()):o(new s(`File upload failed: ${r.status} ${r.statusText}`,"upload_error",r.status))}),r.addEventListener("error",()=>{o(new s("File upload failed: network error","upload_error"))}),r.addEventListener("abort",()=>{o(new DOMException("Upload aborted","AbortError"))}),i){if(i.aborted){o(new DOMException("Upload aborted","AbortError"));return}i.addEventListener("abort",()=>r.abort(),{once:!0})}r.open("POST",e),r.send(t)})}var O=new Proxy({},{get(e,t){let{user:n}=h();switch(t){case"userId":return n.id;case"name":return n.name;case"email":return n.email;case"profilePictureUrl":return n.profilePictureUrl??null;default:return}}});export{s as MindStudioInterfaceError,O as auth,_ as createAgentChatClient,v as createClient,M as platform};
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/config.ts","../src/client.ts","../src/agent-chat.ts","../src/platform.ts","../src/auth.ts"],"sourcesContent":["/**\n * Error class for the @mindstudio-ai/interface SDK.\n *\n * Thrown by `createClient()` method calls on HTTP errors, by `platform`\n * actions on failures, and by the config reader when bootstrap globals\n * are missing.\n */\nexport class MindStudioInterfaceError extends Error {\n /** Machine-readable error code (e.g. 'method_not_found', 'not_initialized'). */\n readonly code: string;\n\n /** HTTP status code, when the error originated from an API response. */\n readonly status?: number;\n\n constructor(message: string, code: string, status?: number) {\n super(message);\n this.name = 'MindStudioInterfaceError';\n this.code = code;\n this.status = status;\n }\n}\n","/**\n * Bootstrap configuration reader.\n *\n * Reads and validates `window.__MINDSTUDIO__` — the globals injected by\n * the platform before the app's JavaScript runs. The result is cached\n * after first read.\n *\n * All other modules in the SDK call `getConfig()` internally. App code\n * should never need to call this directly — use `createClient()`, `auth`,\n * and `platform` instead.\n */\n\nimport { MindStudioInterfaceError } from './errors.js';\nimport type { BootstrapConfig } from './types.js';\n\nlet _config: BootstrapConfig | undefined;\n\n/**\n * Read and validate the bootstrap configuration from `window.__MINDSTUDIO__`.\n *\n * Caches the result after first successful read. Throws if the globals\n * are missing or incomplete — this means the page isn't running inside\n * the MindStudio platform.\n *\n * @returns The validated bootstrap config\n * @throws {MindStudioInterfaceError} if `window.__MINDSTUDIO__` is missing or invalid\n */\nexport function getConfig(): BootstrapConfig {\n if (_config) {\n return _config;\n }\n\n const raw = (globalThis as Record<string, unknown>).__MINDSTUDIO__ as\n | Partial<BootstrapConfig>\n | undefined;\n\n if (!raw) {\n throw new MindStudioInterfaceError(\n '@mindstudio-ai/interface requires the MindStudio platform context. ' +\n 'This page must be loaded inside MindStudio (window.__MINDSTUDIO__ is missing).',\n 'not_initialized',\n );\n }\n\n // Validate required fields\n if (!raw.token || !raw.releaseId || !raw.user || !raw.methods) {\n const missing = ['token', 'releaseId', 'user', 'methods']\n .filter((k) => !raw[k as keyof BootstrapConfig])\n .join(', ');\n\n throw new MindStudioInterfaceError(\n `MindStudio platform context is incomplete. Missing: ${missing}`,\n 'invalid_config',\n );\n }\n\n _config = raw as BootstrapConfig;\n return _config;\n}\n","/**\n * Method RPC client — typed function calls to backend methods.\n *\n * `createClient()` returns a Proxy where every property access creates\n * an async function that invokes the corresponding backend method via HTTP.\n * The method name matches the method's `export` field in `app.json`.\n *\n * ## How it works\n *\n * ```\n * api.submitVendorRequest({ name: 'Acme' })\n * │\n * ├─ Look up method ID: config.methods[\"submitVendorRequest\"]\n * │ → \"submit-vendor-request\"\n * │\n * ├─ POST /_/methods/submit-vendor-request/invoke\n * │ Authorization: Bearer {session token}\n * │ Body: { input: { name: \"Acme\" } }\n * │\n * └─ Returns response.output (or throws on error)\n * ```\n *\n * The method registry (export name → method ID mapping) is embedded in\n * `window.__MINDSTUDIO__.methods` at page load time — no extra HTTP call\n * needed to discover methods.\n *\n * ## Streaming\n *\n * Methods can optionally stream LLM token output via SSE. Pass\n * `{ stream: true }` as the second argument along with an `onToken`\n * callback to receive incremental updates:\n *\n * ```ts\n * const result = await api.submitVendorRequest({ name: 'Acme' }, {\n * stream: true,\n * onToken: (text) => setResponseText(text),\n * });\n * ```\n *\n * **Important:** The `text` value passed to `onToken` is the accumulated\n * response so far (not a delta). Replace your display content each time —\n * do not append. See {@link InvokeOptions.onToken} for details.\n *\n * ## Type safety\n *\n * Since the frontend and backend live in the same project, you can import\n * types directly:\n *\n * ```ts\n * import type { SubmitVendorInput, SubmitVendorOutput }\n * from '../../backend/src/submitVendorRequest';\n *\n * interface AppMethods {\n * submitVendorRequest(input: SubmitVendorInput): Promise<SubmitVendorOutput>;\n * getDashboard(): Promise<GetDashboardOutput>;\n * }\n *\n * const api = createClient<AppMethods>();\n * const result = await api.submitVendorRequest({ name: 'Acme' }); // fully typed\n * ```\n *\n * If no type parameter is provided, methods accept `any` input and return `any`.\n */\n\nimport { getConfig } from './config.js';\nimport { MindStudioInterfaceError } from './errors.js';\n\n/**\n * Options for method invocation. Pass as the optional second argument to\n * any method on the client returned by {@link createClient}.\n *\n * When `stream` is `true`, the SDK sends `{ stream: true }` in the request\n * body and parses the response as an SSE (Server-Sent Events) stream.\n * Three event types are handled:\n *\n * | SSE `type` | Payload field | SDK action |\n * |------------|---------------|------------|\n * | `token` | `text` | Calls {@link onToken} with accumulated text |\n * | `error` | `error` | Calls {@link onStreamError} with the message |\n * | `done` | `output` | Captures as the method's return value |\n *\n * The method's `Promise` resolves with the final `output` from the `done`\n * event — the same shape you'd get without streaming.\n */\nexport interface InvokeOptions {\n /**\n * Enable SSE streaming for LLM token output.\n *\n * When `true`, the request body includes `{ stream: true }` and the\n * response is parsed as an SSE stream instead of a single JSON payload.\n * The method still returns a `Promise` that resolves with the final\n * output once the stream completes.\n *\n * @default false\n */\n stream?: boolean;\n\n /**\n * Called with the accumulated response text each time a new token arrives.\n *\n * **Important:** `text` is the full response so far, not a delta/chunk.\n * Your frontend should **replace** (not append to) its display content\n * with this value each time the callback fires.\n *\n * This design avoids edge cases where the platform sanitizes or\n * reformats JSON mid-stream, causing earlier portions of the text to\n * change between tokens.\n *\n * @example\n * ```ts\n * const result = await api.generate({ prompt: 'Hello' }, {\n * stream: true,\n * onToken: (text) => {\n * // Replace — do NOT append\n * responseElement.textContent = text;\n * },\n * });\n * ```\n */\n onToken?: (text: string) => void;\n\n /**\n * Called if a stream-level error occurs.\n *\n * Stream errors are non-fatal at the protocol level — the method may\n * still complete and resolve with a final output. Use this callback for\n * logging or showing a transient warning, not for control flow.\n *\n * If the stream ends without a `done` event, the method rejects with a\n * `MindStudioInterfaceError` (code `stream_incomplete`) regardless of\n * whether `onStreamError` was provided.\n */\n onStreamError?: (error: string) => void;\n}\n\n/**\n * Default client type when no generic parameter is provided.\n * Every property returns an async function accepting optional input\n * and optional {@link InvokeOptions} for streaming.\n */\ntype DefaultMethodClient = Record<\n string,\n (input?: Record<string, unknown>, options?: InvokeOptions) => Promise<unknown>\n>;\n\n/**\n * Create a typed RPC client for calling backend methods.\n *\n * @typeParam T - Optional interface mapping method names to their signatures.\n * If omitted, all methods accept `any` and return `Promise<any>`.\n *\n * @returns A Proxy object where each property is an async function that\n * invokes the corresponding backend method.\n *\n * @example\n * ```ts\n * import { createClient } from '@mindstudio-ai/interface';\n *\n * const api = createClient();\n *\n * // Each method maps to a backend method export\n * const result = await api.submitVendorRequest({ name: 'Acme' });\n * const dashboard = await api.getDashboard();\n *\n * // Stream LLM output — onToken receives accumulated text (replace, don't append)\n * const streamed = await api.submitVendorRequest({ name: 'Acme' }, {\n * stream: true,\n * onToken: (text) => setResponseText(text),\n * });\n * // `streamed` is the same final output you'd get without streaming\n * ```\n */\nexport function createClient<T = DefaultMethodClient>(): T {\n return new Proxy({} as Record<string, unknown> as T & object, {\n get(_, methodName: string) {\n return async (input?: unknown, options?: InvokeOptions) => {\n const config = getConfig();\n\n const methodId = config.methods[methodName];\n if (!methodId) {\n throw new MindStudioInterfaceError(\n `Method \"${methodName}\" not found. Available methods: ${Object.keys(config.methods).join(', ') || '(none)'}`,\n 'method_not_found',\n );\n }\n\n const url = `/_/methods/${methodId}/invoke`;\n const wantsStream = options?.stream === true;\n\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.token}`,\n },\n body: JSON.stringify({\n input: input ?? {},\n ...(wantsStream && { stream: true }),\n }),\n });\n\n if (!res.ok) {\n let errorMessage = `Method \"${methodName}\" failed: ${res.status} ${res.statusText}`;\n let errorCode = 'method_error';\n try {\n const body = (await res.json()) as {\n error?: string;\n code?: string;\n };\n if (body.error) {\n errorMessage = body.error;\n }\n if (body.code) {\n errorCode = body.code;\n }\n } catch {\n // Response wasn't JSON — use the default message\n }\n throw new MindStudioInterfaceError(\n errorMessage,\n errorCode,\n res.status,\n );\n }\n\n // Non-streaming: unchanged behavior\n if (!wantsStream) {\n const body = (await res.json()) as { output?: unknown };\n return body.output;\n }\n\n // Streaming: parse SSE events from response body\n const reader = res.body!.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let finalOutput: unknown;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) {\n continue;\n }\n const json = line.slice(6);\n try {\n const event = JSON.parse(json) as {\n type: string;\n text?: string;\n error?: string;\n output?: unknown;\n };\n\n if (event.type === 'token' && options?.onToken && event.text) {\n options.onToken(event.text);\n } else if (event.type === 'error' && event.error) {\n if (options?.onStreamError) {\n options.onStreamError(event.error);\n }\n } else if (event.type === 'done') {\n finalOutput = event.output;\n }\n } catch {\n // Skip malformed SSE lines\n }\n }\n }\n\n if (finalOutput === undefined) {\n throw new MindStudioInterfaceError(\n 'Stream ended without a done event',\n 'stream_incomplete',\n );\n }\n\n return finalOutput;\n };\n },\n });\n}\n","/**\n * Agent chat client — thread-based conversations with AI agents.\n *\n * `createAgentChatClient()` returns an object with methods for thread\n * lifecycle (create, list, get, rename, delete) and message streaming.\n * The agent runs server-side with access to the app's methods as tools.\n *\n * ## How it works\n *\n * ```\n * chat.sendMessage(threadId, 'Help me prep for my review', {\n * onText: (delta) => setAssistantMessage((prev) => prev + delta),\n * })\n * │\n * ├─ POST /_/agent/threads/{threadId}/messages\n * │ Authorization: Bearer {session token}\n * │ Body: { content: \"Help me prep for my review\" }\n * │\n * ├─ Response: SSE stream\n * │ data: {\"type\":\"text\",\"text\":\"I'll create...\",\"ts\":1711...}\n * │ data: {\"type\":\"tool_call_start\",\"id\":\"call_abc\",\"name\":\"create-todo\",\"ts\":1711...}\n * │ data: {\"type\":\"tool_call_result\",\"id\":\"call_abc\",\"output\":{...},\"ts\":1711...}\n * │ data: {\"type\":\"text\",\"text\":\"Done — added it.\",\"ts\":1711...}\n * │ data: {\"type\":\"done\",\"stopReason\":\"end_turn\",\"usage\":{...},\"ts\":1711...}\n * │\n * └─ Promise resolves with { stopReason, usage }\n * ```\n *\n * ## Stateless design\n *\n * This client is a thin wrapper over HTTP endpoints. It does not manage\n * local state — your React/framework layer owns the thread list, message\n * array, and UI state. Each method is an independent request.\n *\n * ## Streaming callbacks\n *\n * `sendMessage` accepts named callbacks for common events (onText,\n * onThinking, onToolCallStart, etc.) plus a catch-all `onEvent` that\n * receives every SSE event as a discriminated union. Both fire for the\n * same event — use named callbacks for convenience, `onEvent` for\n * logging or handling low-level events like `tool_use` and\n * `tool_input_delta`.\n *\n * @example\n * ```ts\n * import { createAgentChatClient } from '@mindstudio-ai/interface';\n *\n * const chat = createAgentChatClient();\n *\n * // Thread lifecycle\n * const thread = await chat.createThread();\n * const { threads } = await chat.listThreads();\n * const full = await chat.getThread(thread.id);\n *\n * // Send a message with streaming\n * const response = chat.sendMessage(thread.id, 'Hello!', {\n * onText: (delta) => setMessage((prev) => prev + delta),\n * onToolCallStart: (id, name) => showToolSpinner(name),\n * onToolCallResult: (id, output) => showToolResult(output),\n * });\n *\n * const { stopReason, usage } = await response;\n *\n * // Abort mid-stream\n * response.abort();\n * ```\n */\n\nimport { getConfig } from './config.js';\nimport { MindStudioInterfaceError } from './errors.js';\n\n// ---------------------------------------------------------------------------\n// Thread types\n// ---------------------------------------------------------------------------\n\n/** Thread summary returned from create and list endpoints. */\nexport interface ThreadSummary {\n /** Thread ID (UUID). */\n id: string;\n /** User-editable title, auto-generated on first message. */\n title: string | null;\n /** ISO 8601 creation timestamp. */\n dateCreated: string;\n /** ISO 8601 last-updated timestamp. */\n dateUpdated: string;\n}\n\n/** Full thread with message history, returned from get endpoint. */\nexport interface Thread {\n /** Thread ID (UUID). */\n id: string;\n /** User-editable title. */\n title: string | null;\n /** Full conversation history. */\n messages: Message[];\n /** ISO 8601 creation timestamp. */\n dateCreated: string;\n /** ISO 8601 last-updated timestamp. */\n dateUpdated: string;\n}\n\n/** A tool call requested by the assistant. */\nexport interface ToolCall {\n /** Tool call ID, used to match results. */\n id: string;\n /** Method name. */\n name: string;\n /** Input arguments. */\n input: Record<string, unknown>;\n}\n\n/** A single message in a thread's conversation history. */\nexport interface Message {\n /** `\"user\"` for human messages, `\"assistant\"` for agent responses. */\n role: 'user' | 'assistant';\n /** Message text content. For tool results, this is JSON-stringified output. */\n content: string;\n /**\n * MindStudio CDN URLs attached to this message (user messages only).\n *\n * Images (`i.mscdn.ai`) are sent to the model as vision input.\n * Documents (`f.mscdn.ai`, `files.mindstudio-cdn.com`) have their text\n * extracted server-side and included in the model context. The original\n * URLs are preserved in thread history for the frontend to render.\n */\n attachments?: string[];\n /** Tool calls the assistant requested (assistant messages only). */\n toolCalls?: ToolCall[];\n /** ID of the tool call this message is a result for (tool result messages only). */\n toolCallId?: string;\n /** Whether this tool result represents an error. */\n isToolError?: boolean;\n}\n\n/** Paginated thread list response. */\nexport interface ThreadListPage {\n /** Threads for this page. */\n threads: ThreadSummary[];\n /** Cursor for the next page, or `null` if this is the last page. */\n nextCursor: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// SSE event types\n// ---------------------------------------------------------------------------\n\n/** Assistant text delta — append to your display string, do not replace. */\nexport interface AgentTextEvent {\n type: 'text';\n text: string;\n ts: number;\n}\n\n/** Model thinking text (extended thinking). */\nexport interface AgentThinkingEvent {\n type: 'thinking';\n text: string;\n ts: number;\n}\n\n/** Thinking complete with full text and signature. */\nexport interface AgentThinkingCompleteEvent {\n type: 'thinking_complete';\n thinking: string;\n signature: string;\n ts: number;\n}\n\n/** Model requested a tool call (includes parsed input). */\nexport interface AgentToolUseEvent {\n type: 'tool_use';\n id: string;\n name: string;\n input: Record<string, unknown>;\n ts: number;\n}\n\n/** Streaming delta for tool call input JSON. */\nexport interface AgentToolInputDeltaEvent {\n type: 'tool_input_delta';\n id: string;\n name: string;\n delta: string;\n ts: number;\n}\n\n/** Agent began executing a tool call. */\nexport interface AgentToolCallStartEvent {\n type: 'tool_call_start';\n id: string;\n name: string;\n ts: number;\n}\n\n/** Tool call returned a result. */\nexport interface AgentToolCallResultEvent {\n type: 'tool_call_result';\n id: string;\n output: unknown;\n ts: number;\n}\n\n/** Stream complete. */\nexport interface AgentDoneEvent {\n type: 'done';\n stopReason: string;\n usage: { inputTokens: number; outputTokens: number };\n ts: number;\n}\n\n/** Stream-level error. */\nexport interface AgentErrorEvent {\n type: 'error';\n error: string;\n ts: number;\n}\n\n/** Discriminated union of all SSE events from the agent chat stream. */\nexport type AgentChatEvent =\n | AgentTextEvent\n | AgentThinkingEvent\n | AgentThinkingCompleteEvent\n | AgentToolUseEvent\n | AgentToolInputDeltaEvent\n | AgentToolCallStartEvent\n | AgentToolCallResultEvent\n | AgentDoneEvent\n | AgentErrorEvent;\n\n// ---------------------------------------------------------------------------\n// Callbacks and result types\n// ---------------------------------------------------------------------------\n\n/**\n * Callbacks for `sendMessage` streaming.\n *\n * Named callbacks fire for common events. The catch-all `onEvent` fires\n * for every event including those with named callbacks — use it for\n * logging or handling low-level events like `tool_use` and\n * `tool_input_delta`.\n */\nexport interface SendMessageCallbacks {\n /**\n * Called with each text delta as the assistant responds.\n *\n * **Important:** `text` is a delta (new chunk), not the accumulated\n * response. Append it to your display string — do not replace.\n *\n * @example\n * ```ts\n * onText: (delta) => setMessage((prev) => prev + delta)\n * ```\n */\n onText?: (text: string) => void;\n\n /** Called with model thinking text (extended thinking). */\n onThinking?: (text: string) => void;\n\n /** Called when thinking is complete. */\n onThinkingComplete?: (thinking: string, signature: string) => void;\n\n /** Called when a tool call begins executing. */\n onToolCallStart?: (id: string, name: string) => void;\n\n /** Called when a tool call produces a result. */\n onToolCallResult?: (id: string, output: unknown) => void;\n\n /** Called on a stream-level error event. */\n onError?: (error: string) => void;\n\n /** Called for every SSE event (including ones with named callbacks). */\n onEvent?: (event: AgentChatEvent) => void;\n\n /** AbortSignal to cancel the stream. */\n signal?: AbortSignal;\n}\n\n/** Options for `sendMessage`. */\nexport interface SendMessageOptions {\n /**\n * MindStudio CDN URLs to attach to the message.\n *\n * Images (`i.mscdn.ai`) are sent to the model as vision input (one per message).\n * Documents (`f.mscdn.ai`, `files.mindstudio-cdn.com`) have their text\n * extracted server-side and included in context.\n *\n * Upload files first via `platform.uploadFile()`.\n *\n * @example\n * ```ts\n * const url = await platform.uploadFile(file);\n * chat.sendMessage(threadId, 'What is this?', callbacks, {\n * attachments: [url],\n * });\n * ```\n */\n attachments?: string[];\n}\n\n/** Resolved value of `sendMessage` — extracted from the `done` event. */\nexport interface SendMessageResult {\n /** Why the agent stopped (`\"end_turn\"`, `\"max_tokens\"`, etc.). */\n stopReason: string;\n /** Token usage for the full agent loop. */\n usage: { inputTokens: number; outputTokens: number };\n}\n\n/** A Promise with an `abort()` method for cancelling the stream. */\nexport type AbortablePromise<T> = Promise<T> & {\n /** Cancel the in-flight stream. The promise rejects with `AbortError`. */\n abort: () => void;\n};\n\n// ---------------------------------------------------------------------------\n// Client interface\n// ---------------------------------------------------------------------------\n\n/** Agent chat client returned by {@link createAgentChatClient}. */\nexport interface AgentChatClient {\n /** Create a new conversation thread. */\n createThread(): Promise<ThreadSummary>;\n\n /** List threads with cursor-based pagination (50 per page). */\n listThreads(cursor?: string): Promise<ThreadListPage>;\n\n /** Get a thread with its full message history. */\n getThread(threadId: string): Promise<Thread>;\n\n /** Update a thread's title. */\n updateThread(threadId: string, title: string): Promise<void>;\n\n /** Delete a thread (soft delete). */\n deleteThread(threadId: string): Promise<void>;\n\n /**\n * Send a message and stream the agent's response.\n *\n * Returns an {@link AbortablePromise} that resolves with\n * `{ stopReason, usage }` when the stream completes. Call `.abort()`\n * on the returned promise to cancel mid-stream.\n *\n * @param options.attachments - MindStudio CDN URLs to attach.\n * Images (`i.mscdn.ai`) are sent as vision input.\n * Documents (`f.mscdn.ai`) have text extracted into context.\n * Upload files first via `platform.uploadFile()`.\n */\n sendMessage(\n threadId: string,\n content: string,\n callbacks?: SendMessageCallbacks,\n options?: SendMessageOptions,\n ): AbortablePromise<SendMessageResult>;\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\nconst AGENT_BASE = '/_/agent';\n\nasync function request<T>(\n path: string,\n method: string,\n body?: unknown,\n): Promise<T> {\n const config = getConfig();\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.token}`,\n };\n\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const res = await fetch(`${AGENT_BASE}${path}`, {\n method,\n headers,\n ...(body !== undefined && { body: JSON.stringify(body) }),\n });\n\n if (!res.ok) {\n let errorMessage = `Agent chat request failed: ${res.status} ${res.statusText}`;\n let errorCode = 'agent_chat_error';\n try {\n const err = (await res.json()) as { error?: string; code?: string };\n if (err.error) {\n errorMessage = err.error;\n }\n if (err.code) {\n errorCode = err.code;\n }\n } catch {\n // Response wasn't JSON — use the default message\n }\n throw new MindStudioInterfaceError(errorMessage, errorCode, res.status);\n }\n\n return (await res.json()) as T;\n}\n\nfunction dispatchEvent(\n event: AgentChatEvent,\n callbacks: SendMessageCallbacks,\n): void {\n // Catch-all fires for every event\n callbacks.onEvent?.(event);\n\n // Named callbacks\n switch (event.type) {\n case 'text':\n callbacks.onText?.(event.text);\n break;\n case 'thinking':\n callbacks.onThinking?.(event.text);\n break;\n case 'thinking_complete':\n callbacks.onThinkingComplete?.(event.thinking, event.signature);\n break;\n case 'tool_call_start':\n callbacks.onToolCallStart?.(event.id, event.name);\n break;\n case 'tool_call_result':\n callbacks.onToolCallResult?.(event.id, event.output);\n break;\n case 'error':\n callbacks.onError?.(event.error);\n break;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create an agent chat client for thread-based conversations.\n *\n * The client is stateless — each method is an independent HTTP request.\n * Your application layer owns thread state, message arrays, and UI updates.\n *\n * @returns An {@link AgentChatClient} with methods for thread CRUD and\n * message streaming.\n *\n * @example\n * ```ts\n * import { createAgentChatClient } from '@mindstudio-ai/interface';\n *\n * const chat = createAgentChatClient();\n *\n * const thread = await chat.createThread();\n *\n * const response = chat.sendMessage(thread.id, 'Hello!', {\n * onText: (delta) => setAssistantMessage((prev) => prev + delta),\n * onToolCallStart: (id, name) => showToolSpinner(name),\n * onToolCallResult: (id, output) => showToolResult(output),\n * });\n *\n * const { stopReason, usage } = await response;\n * ```\n */\nexport function createAgentChatClient(): AgentChatClient {\n return {\n createThread() {\n return request<ThreadSummary>('/threads', 'POST');\n },\n\n listThreads(cursor?: string) {\n const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : '';\n return request<ThreadListPage>(`/threads${query}`, 'GET');\n },\n\n getThread(threadId: string) {\n return request<Thread>(`/threads/${threadId}`, 'GET');\n },\n\n async updateThread(threadId: string, title: string) {\n await request(`/threads/${threadId}`, 'PATCH', { title });\n },\n\n async deleteThread(threadId: string) {\n await request(`/threads/${threadId}`, 'DELETE');\n },\n\n sendMessage(\n threadId: string,\n content: string,\n callbacks?: SendMessageCallbacks,\n options?: SendMessageOptions,\n ): AbortablePromise<SendMessageResult> {\n const controller = new AbortController();\n const cb = callbacks ?? {};\n\n // Wire caller's signal to our internal controller\n if (cb.signal) {\n if (cb.signal.aborted) {\n controller.abort();\n } else {\n cb.signal.addEventListener('abort', () => controller.abort(), {\n once: true,\n });\n }\n }\n\n const promise = (async (): Promise<SendMessageResult> => {\n const config = getConfig();\n const url = `${AGENT_BASE}/threads/${threadId}/messages`;\n\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.token}`,\n },\n body: JSON.stringify({\n content,\n ...(options?.attachments?.length && {\n attachments: options.attachments,\n }),\n }),\n signal: controller.signal,\n });\n\n if (!res.ok) {\n let errorMessage = `Send message failed: ${res.status} ${res.statusText}`;\n let errorCode = 'agent_chat_error';\n try {\n const err = (await res.json()) as {\n error?: string;\n code?: string;\n };\n if (err.error) {\n errorMessage = err.error;\n }\n if (err.code) {\n errorCode = err.code;\n }\n } catch {\n // Response wasn't JSON — use the default message\n }\n throw new MindStudioInterfaceError(\n errorMessage,\n errorCode,\n res.status,\n );\n }\n\n const reader = res.body!.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let result: SendMessageResult | undefined;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) {\n continue;\n }\n const json = line.slice(6);\n try {\n const event = JSON.parse(json) as AgentChatEvent;\n\n if (event.type === 'done') {\n result = {\n stopReason: event.stopReason,\n usage: event.usage,\n };\n }\n\n dispatchEvent(event, cb);\n } catch {\n // Skip malformed SSE lines\n }\n }\n }\n\n if (!result) {\n throw new MindStudioInterfaceError(\n 'Stream ended without a done event',\n 'stream_incomplete',\n );\n }\n\n return result;\n })();\n\n const abortable = promise as AbortablePromise<SendMessageResult>;\n abortable.abort = () => controller.abort();\n return abortable;\n },\n };\n}\n","/**\n * Platform actions — file uploads via presigned S3 POST.\n *\n * Uses the session token from `window.__MINDSTUDIO__` to request a\n * presigned upload URL from the platform, then uploads directly to S3.\n * Returns the public CDN URL.\n *\n * @example\n * ```ts\n * import { platform } from '@mindstudio-ai/interface';\n *\n * const file = document.querySelector('input[type=file]').files[0];\n * const url = await platform.uploadFile(file);\n *\n * // With progress and abort\n * const controller = new AbortController();\n * const url = await platform.uploadFile(file, {\n * onProgress: (fraction) => console.log(`${Math.round(fraction * 100)}%`),\n * signal: controller.signal,\n * });\n * ```\n */\n\nimport { getConfig } from './config.js';\nimport { MindStudioInterfaceError } from './errors.js';\n\n/**\n * Options for `platform.uploadFile()`.\n */\nexport interface UploadFileOptions {\n /** Called with upload progress as a fraction from 0 to 1. */\n onProgress?: (fraction: number) => void;\n\n /** AbortSignal to cancel the upload. */\n signal?: AbortSignal;\n}\n\n/**\n * The platform namespace — file upload actions.\n */\nexport const platform = {\n /**\n * Upload a file to the MindStudio CDN.\n *\n * Requests a presigned upload URL from the platform, then uploads\n * the file directly to S3. Returns the public CDN URL.\n *\n * @param file - The File to upload\n * @param options - Optional progress callback and abort signal\n * @returns CDN URL of the uploaded file\n *\n * @example\n * ```ts\n * const url = await platform.uploadFile(file);\n *\n * // With progress\n * const url = await platform.uploadFile(file, {\n * onProgress: (f) => setProgress(f),\n * });\n *\n * // With abort\n * const controller = new AbortController();\n * const url = await platform.uploadFile(file, {\n * signal: controller.signal,\n * });\n * // controller.abort() to cancel\n * ```\n */\n async uploadFile(file: File, options?: UploadFileOptions): Promise<string> {\n const config = getConfig();\n const { onProgress, signal } = options ?? {};\n\n // Check if already aborted\n signal?.throwIfAborted();\n\n // Step 1: Get presigned upload URL\n const presignUrl = '/_/generate-upload-request';\n const presignRes = await fetch(presignUrl, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${config.token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n filename: file.name,\n contentType: file.type || 'application/octet-stream',\n }),\n signal,\n });\n\n if (!presignRes.ok) {\n throw new MindStudioInterfaceError(\n `Upload presign failed: ${presignRes.status} ${presignRes.statusText}`,\n 'upload_error',\n presignRes.status,\n );\n }\n\n const { uploadUrl, uploadFields, publicUrl } =\n (await presignRes.json()) as {\n uploadUrl: string;\n uploadFields: Record<string, string>;\n publicUrl: string;\n };\n\n // Step 2: Upload directly to S3\n const formData = new FormData();\n for (const [key, value] of Object.entries(uploadFields)) {\n formData.append(key, value);\n }\n formData.append('file', file); // must be last\n\n // Use XHR for upload progress (fetch doesn't support it)\n if (onProgress) {\n await xhrUpload(uploadUrl, formData, onProgress, signal);\n return publicUrl;\n }\n\n const uploadRes = await fetch(uploadUrl, {\n method: 'POST',\n body: formData,\n signal,\n });\n\n if (!uploadRes.ok) {\n throw new MindStudioInterfaceError(\n `File upload failed: ${uploadRes.status} ${uploadRes.statusText}`,\n 'upload_error',\n uploadRes.status,\n );\n }\n\n return publicUrl;\n },\n};\n\n/**\n * Upload via XMLHttpRequest to get progress events.\n * Falls back from fetch because fetch upload streams don't support progress.\n */\nfunction xhrUpload(\n url: string,\n formData: FormData,\n onProgress: (fraction: number) => void,\n signal?: AbortSignal,\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n\n xhr.upload.addEventListener('progress', (e) => {\n if (e.lengthComputable) {\n onProgress(e.loaded / e.total);\n }\n });\n\n xhr.addEventListener('load', () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n onProgress(1);\n resolve();\n } else {\n reject(\n new MindStudioInterfaceError(\n `File upload failed: ${xhr.status} ${xhr.statusText}`,\n 'upload_error',\n xhr.status,\n ),\n );\n }\n });\n\n xhr.addEventListener('error', () => {\n reject(\n new MindStudioInterfaceError(\n 'File upload failed: network error',\n 'upload_error',\n ),\n );\n });\n\n xhr.addEventListener('abort', () => {\n reject(new DOMException('Upload aborted', 'AbortError'));\n });\n\n // Wire up abort signal\n if (signal) {\n if (signal.aborted) {\n reject(new DOMException('Upload aborted', 'AbortError'));\n return;\n }\n signal.addEventListener('abort', () => xhr.abort(), { once: true });\n }\n\n xhr.open('POST', url);\n xhr.send(formData);\n });\n}\n","/**\n * Auth context — current user's identity for display purposes.\n *\n * Provides synchronous access to the current user's name, email, and\n * profile picture. Hydrated from the bootstrap globals injected by the\n * platform — no HTTP call needed.\n *\n * This is for **display only**. Role checking, permission enforcement,\n * and user lookups for other users are backend concerns — the frontend\n * calls backend methods that handle those via `@mindstudio-ai/agent`'s\n * `auth` namespace.\n *\n * @example\n * ```ts\n * import { auth } from '@mindstudio-ai/interface';\n *\n * return (\n * <div>\n * <p>Welcome, {auth.name}</p>\n * <img src={auth.profilePictureUrl} />\n * </div>\n * );\n * ```\n */\n\nimport { getConfig } from './config.js';\n\n/**\n * User identity context. All properties are read-only and synchronous.\n */\nexport interface AuthContext {\n /** Current user's ID (UUID). */\n readonly userId: string;\n\n /** Current user's display name. */\n readonly name: string;\n\n /** Current user's email address. */\n readonly email: string;\n\n /** Current user's profile picture URL, or null if not set. */\n readonly profilePictureUrl: string | null;\n}\n\n/**\n * Lazy auth proxy — reads from bootstrap globals on first property access.\n *\n * Using a Proxy lets us export `auth` as a module-level constant while\n * deferring the `getConfig()` call until the first property is accessed.\n * This avoids throwing during import if the page hasn't finished loading\n * the bootstrap script yet.\n */\nexport const auth: AuthContext = new Proxy({} as AuthContext, {\n get(_, prop: string) {\n const { user } = getConfig();\n\n switch (prop) {\n case 'userId':\n return user.id;\n case 'name':\n return user.name;\n case 'email':\n return user.email;\n case 'profilePictureUrl':\n return user.profilePictureUrl ?? null;\n default:\n return undefined;\n }\n },\n});\n"],"mappings":"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","names":["MindStudioInterfaceError","message","code","status","_config","getConfig","raw","MindStudioInterfaceError","missing","k","createClient","_","methodName","input","options","config","getConfig","methodId","MindStudioInterfaceError","url","wantsStream","res","errorMessage","errorCode","body","reader","decoder","buffer","finalOutput","done","value","lines","line","json","event","AGENT_BASE","request","path","method","body","headers","getConfig","res","errorMessage","errorCode","err","MindStudioInterfaceError","dispatchEvent","event","callbacks","createAgentChatClient","cursor","query","threadId","title","content","options","controller","cb","abortable","config","url","reader","decoder","buffer","result","done","value","lines","line","json","platform","file","options","config","getConfig","onProgress","signal","presignRes","MindStudioInterfaceError","uploadUrl","uploadFields","publicUrl","formData","key","value","xhrUpload","uploadRes","url","resolve","reject","xhr","e","auth","_","prop","user","getConfig"]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@mindstudio-ai/interface",
3
+ "version": "0.1.0",
4
+ "description": "Frontend SDK for MindStudio v2 app interfaces",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "typecheck": "tsc --noEmit",
22
+ "prepare": "husky && tsup",
23
+ "prepublishOnly": "tsup",
24
+ "lint:fix": "prettier --write ./src"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "keywords": [
30
+ "mindstudio",
31
+ "interface",
32
+ "frontend",
33
+ "sdk"
34
+ ],
35
+ "homepage": "https://github.com/mindstudio-ai/mindstudio-interface#readme",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/mindstudio-ai/mindstudio-interface.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/mindstudio-ai/mindstudio-interface/issues"
42
+ },
43
+ "author": "MindStudio <support@mindstudio.ai> (https://mindstudio.ai)",
44
+ "license": "MIT",
45
+ "sideEffects": false,
46
+ "devDependencies": {
47
+ "husky": "^9.1.7",
48
+ "lint-staged": "^16.3.2",
49
+ "prettier": "^3.8.1",
50
+ "prettier-plugin-curly": "^0.4.1",
51
+ "tsup": "^8",
52
+ "typescript": "^5.5"
53
+ },
54
+ "lint-staged": {
55
+ "**/*": "prettier --write --ignore-unknown"
56
+ }
57
+ }