@mindstudio-ai/remy 0.1.257 → 0.1.259

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.
@@ -9,6 +9,6 @@ First, call `setProjectOnboardingState({ state: "building" })` to transition the
9
9
 
10
10
  Then, write the full spec. Follow the instructions in <spec_authoring_instructions> to write all spec files: app.md, web.md, brand (visual.md, colors.md, typography.md, voice.md), and any others the project needs. Consult the design expert for brand and visual direction. Be thorough: the spec drives everything downstream.
11
11
 
12
- As the final step of spec authoring, call `productVision` to seed the initial roadmap and generate the pitch deck.
12
+ As the final step of spec authoring, dispatch `productVision` with `background: true` to seed the initial roadmap and generate the pitch deck. It runs long and it keeps working after this turn ends, reporting back later as an automated message — so hand it off and move on rather than waiting on it.
13
13
 
14
- When all spec files are written and the roadmap is seeded, end the turn. The build will start automatically.
14
+ When all spec files are written and the roadmap seed is dispatched, end the turn. The build will start automatically.
@@ -4,6 +4,8 @@ Interfaces are projections of the backend contract into different modalities. Th
4
4
 
5
5
  All external service connections (webhook secrets, email addresses) are configured at the project level by the user through the Remy platform. The agent's job is to write the config files and the methods that handle the requests — not to manage API keys, OAuth flows, or service registration.
6
6
 
7
+ `{app-host}`, where it appears in a URL below, means any host the app is served on: its `custom_subdomain` host (e.g. `myapp.madewithremy.com`), a custom domain if one is configured, or the UUID host (`<appId>.madewithremy.com` / `.msagent.ai`).
8
+
7
9
  ## Web Interface
8
10
 
9
11
  A full web application — typically Vite + React, but any framework that produces static output works.
@@ -84,7 +86,7 @@ auth.verifySmsCode(verId, code) // → AppUser (sets session)
84
86
  auth.logout() // clears session
85
87
  ```
86
88
 
87
- For apps with an agent interface, the SDK also provides `createAgentChatClient()` for thread management and streaming chat. See the "Building Agent Interfaces" section for usage details.
89
+ For apps with an agent interface, the SDK also provides `createAgentChatClient()` for thread management and streaming chat. Load the `agentInterfaces` skill for its usage — thread APIs, streaming callbacks, and attachments are all there.
88
90
 
89
91
  The project uses `"jsx": "react-jsx"` (automatic JSX transform) — do not `import React from 'react'`. Only import the specific hooks and types you need (e.g., `import { useState, useEffect } from 'react'`).
90
92
 
@@ -138,496 +140,49 @@ await prerender.invalidate(['/u/abc']); // omit arg to purge all
138
140
 
139
141
  REST endpoints for external consumers — other services, mobile apps, integrations. This is separate from the web frontend's internal RPC (`@mindstudio-ai/interface` calls `/_/methods` directly and does not use the API interface). The API interface lives at `/_/api/` and exposes only the methods you choose to route.
140
142
 
141
- Use it for receiving webhooks (Stripe, Twilio), sync endpoints for other services, a public REST API, batch tools — anything where something outside the app's own frontend needs to call a method over HTTP.
142
-
143
- ### Spec: `src/interfaces/api.md`
144
-
145
- The human-readable spec. Frontmatter declares the API name and description; the body maps methods to REST routes using MSFM.
146
-
147
- ```yaml
148
- ---
149
- name: Vendor Management API
150
- description: API for managing vendors and purchase orders.
151
- type: interface/api
152
- ---
153
- ```
154
-
155
- Routes are declared as `VERB /path → methodExportName` under resource headings, with annotations for params and descriptions:
156
-
157
- ```markdown
158
- ## Vendors
159
-
160
- ### List vendors
161
- GET /vendors → listVendors
162
- ~~~
163
- Returns all vendors, optionally filtered by status.
164
- query: status (string, optional) — filter by vendor status
165
- ~~~
166
-
167
- ### Create vendor
168
- POST /vendors → submitVendorRequest
169
- ~~~
170
- Submit a new vendor for approval.
171
- body: name (string, required) — vendor name
172
- contactEmail (string, required) — billing contact
173
- ~~~
174
-
175
- ### Delete vendor
176
- DELETE /vendors/:vendorId → deleteVendor
177
- ~~~
178
- path: vendorId (string, required) — the vendor's unique identifier
179
- ~~~
180
- ```
181
-
182
- ### Compiled Output: `dist/interfaces/api/api.json`
183
-
184
- ```json
185
- {
186
- "api": {
187
- "name": "Vendor Management API",
188
- "description": "API for managing vendors and purchase orders.",
189
- "routes": [
190
- {
191
- "method": "GET",
192
- "path": "/vendors",
193
- "handler": "list-vendors",
194
- "summary": "List vendors",
195
- "description": "Returns all vendors, optionally filtered by status.",
196
- "tag": "Vendors",
197
- "params": {
198
- "query": {
199
- "status": { "type": "string", "required": false, "description": "Filter by vendor status" }
200
- }
201
- }
202
- },
203
- {
204
- "method": "POST",
205
- "path": "/vendors",
206
- "handler": "submit-vendor-request",
207
- "summary": "Create vendor",
208
- "description": "Submit a new vendor for approval.",
209
- "tag": "Vendors",
210
- "params": {
211
- "body": {
212
- "name": { "type": "string", "required": true, "description": "Vendor name" },
213
- "contactEmail": { "type": "string", "required": true, "description": "Billing contact" }
214
- }
215
- }
216
- },
217
- {
218
- "method": "DELETE",
219
- "path": "/vendors/:vendorId",
220
- "handler": "delete-vendor",
221
- "summary": "Delete vendor",
222
- "description": "Permanently remove a vendor.",
223
- "tag": "Vendors",
224
- "params": {
225
- "path": {
226
- "vendorId": { "type": "string", "required": true, "description": "The vendor's unique identifier" }
227
- }
228
- }
229
- }
230
- ]
231
- }
232
- }
233
- ```
234
-
235
- | Field | Description |
236
- |-------|-------------|
237
- | `name` | API display name (used in generated OpenAPI spec) |
238
- | `description` | API description |
239
- | `routes[].method` | HTTP method: `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
240
- | `routes[].path` | URL path with `:param` placeholders for path params |
241
- | `routes[].handler` | Method `id` from the manifest (kebab-case) |
242
- | `routes[].summary` | Short description for the endpoint |
243
- | `routes[].description` | Longer description |
244
- | `routes[].tag` | Resource grouping (becomes a tag in OpenAPI) |
245
- | `routes[].params` | Parameter declarations: `path`, `query`, and/or `body` objects |
143
+ Use it for sync endpoints for other services, a public REST API, batch tools — anything where something outside the app's own frontend needs to call a method over HTTP. It's also the interface that hands a method the raw HTTP request (`input._request`).
246
144
 
247
- ### Platform Behavior
248
-
249
- Routes are mounted at `/_/api{path}` (e.g. `DELETE /_/api/vendors/abc123`).
250
-
251
- - **Path params** are extracted and merged into the method's input: `/:vendorId` → `{ vendorId: "abc123" }`
252
- - **Query params** are merged into input for GET requests: `?status=approved` → `{ status: "approved" }`
253
- - **Request body** for POST/PUT/PATCH is the input directly (no `{ input: {...} }` wrapper)
254
- - **Response** is the method output directly (no `{ output: {...} }` wrapper)
255
- - **Auth** via `Authorization: Bearer sk_...` (API key resolves to a user with full RBAC)
256
- - **Streaming**: `Accept: text/event-stream` header returns SSE chunks
257
- - **Raw request context**: Every API method receives `input._request` with `{ method, headers, rawBody }`. `rawBody` is the original unparsed body as a UTF-8 string — critical for webhook signature verification (Stripe, GitHub, Shopify). For most methods you don't need `_request` at all.
258
-
259
- ### Manifest
260
-
261
- ```json
262
- { "type": "api", "path": "dist/interfaces/api/api.json" }
263
- ```
145
+ **Load the `restApi` skill** before authoring `src/interfaces/api.md` or writing the config — the route spec format, param declarations, auth, and platform behaviour are all there.
264
146
 
265
147
  ## Platform-Triggered Interfaces
266
148
 
267
149
  Cron, Webhook, and Email interfaces are invoked by the platform, not by a user session. Methods called through these interfaces run with `auth.roles: ['system']`. Use `auth.requireRole('system')` to restrict a method to platform triggers only.
268
150
 
269
- ## Cron
270
-
271
- Scheduled method execution.
151
+ Each has its own skill carrying the config shape, the input the method receives, and the platform's behaviour: `scheduledJobs`, `webhooks`, `inboundEmail`.
272
152
 
273
- ### Config (`interface.json`)
153
+ ## Cron
274
154
 
275
- ```json
276
- {
277
- "cron": {
278
- "jobs": [
279
- {
280
- "schedule": "0 9 * * 5",
281
- "method": "process-weekly-payments",
282
- "description": "Process approved invoices every Friday at 9am"
283
- },
284
- {
285
- "schedule": "*/30 * * * *",
286
- "method": "sync-vendor-status",
287
- "description": "Sync vendor statuses every 30 minutes"
288
- }
289
- ]
290
- }
291
- }
292
- ```
155
+ Scheduled method execution — a method plus a cron expression, synced to the platform on deploy.
293
156
 
294
- Standard cron expression format. Jobs are synced to the platform on deploy.
157
+ **Load the `scheduledJobs` skill** before adding one.
295
158
 
296
159
  ## Webhook
297
160
 
298
161
  Inbound HTTP endpoints that invoke a method directly and synchronously — the caller waits for the method to finish. Use for receiving webhooks from external services (Stripe, GitHub, Shopify, Slack, Twilio). Direct inbound webhooks with signature verification work natively; do **not** build confirmation-token or polling workarounds.
299
162
 
300
- ### Config (`interface.json`)
163
+ Routing is by a secret in the URL rather than an auth header, which is what makes it the right fit for provider callbacks — they can't send a bearer token. The API interface is the alternative when the caller can.
301
164
 
302
- The top-level key must match the interface type (`webhook`):
303
-
304
- ```json
305
- {
306
- "webhook": {
307
- "endpoints": [
308
- {
309
- "method": "handle-payment-webhook",
310
- "secret": "whsec_pick_a_long_random_token",
311
- "description": "Stripe events"
312
- }
313
- ]
314
- }
315
- }
316
- ```
317
-
318
- - `method` — the id of a method in `methods[]` to invoke.
319
- - `secret` — a developer-chosen opaque token that is **both the routing key and the access guard**. It is stable across deploys (compilation is a passthrough — redeploying never rotates it), so a URL you register with Stripe/GitHub stays valid. Generate one long random value per endpoint and keep it constant.
320
- - Declare multiple endpoints if needed; each `secret` maps to one method.
321
-
322
- ### Endpoint URL
323
-
324
- Register this with the external service: `https://{app-host}/_/webhook/{secret}` — `{app-host}` is any host the app is served on: its `custom_subdomain` host (e.g. `myapp.madewithremy.com`), a custom domain if configured, or the UUID host (`<appId>.madewithremy.com` / `.msagent.ai`). All HTTP verbs are accepted.
325
-
326
- ### Input
327
-
328
- The method receives:
329
-
330
- ```ts
331
- {
332
- method: string; // HTTP method
333
- headers: Record<string, string>; // request headers
334
- query: Record<string, string>; // query params
335
- body: any; // parsed JSON / form body
336
- rawBody: string; // exact raw request bytes (UTF-8), pre-parse
337
- }
338
- ```
339
-
340
- For signature verification **always use `rawBody`, never `body`** — providers (Stripe, GitHub, Shopify, Slack) HMAC the raw payload, and a re-serialized `body` will not match. E.g. `stripe.webhooks.constructEvent(input.rawBody, input.headers['stripe-signature'], endpointSecret)`. `rawBody` is populated for `application/json` and `application/x-www-form-urlencoded` bodies (what these providers send).
341
-
342
- ### Response
343
-
344
- Whatever the method returns as output is sent back to the caller as JSON; if it returns no output, the platform responds `204`. A wrong/unknown secret returns `401`; an app with no live release returns `404`.
165
+ **Load the `webhooks` skill** before adding one — the secret semantics, endpoint URL, input shape, and signature verification are all there.
345
166
 
346
167
  ## Email
347
168
 
348
- Inbound email triggers. Each app has one email-handler method; the platform routes all inbound mail destined for the app — across any of its address tiers — to that method.
349
-
350
- ### Address tiers
351
-
352
- Three tiers, all delivered to the same handler method. The new tiers are catchall (no localpart registration); the legacy tier is specific-localpart and frozen for new apps.
353
-
354
- | Tier | Address | How it's set up |
355
- |---|---|---|
356
- | Platform subdomain (default) | `*@<custom_subdomain>.madewithremy.com` | Automatic the moment the app has a `custom_subdomain` set. Every address on that subdomain delivers to the handler. |
357
- | Custom domain | `*@<their-domain>` | The user adds a domain in the dashboard's email-domains settings and points one MX record at `mx.msagent.ai`. Not something the agent provisions. |
358
- | Legacy `mindstudio-hooks.com` | `<name>@mindstudio-hooks.com` | Existing apps only — frozen for new apps. Don't recommend it; treat as read-only history. |
359
-
360
- Because the new tiers are catchall, `to` carries an arbitrary localpart. Methods that need to branch on it should read `input.to` (e.g. `if (input.to.startsWith('support@')) ...`).
169
+ Inbound email triggers. Each app has one email-handler method; the platform routes all inbound mail destined for the app — across any of its address tiers — to that method. Addresses on the app's subdomain are catchall, so per-purpose addresses (`support@`, `receipts@`) work without registering anything.
361
170
 
362
- A verified custom domain (and the app's `madewithremy.com` subdomain) also **sends** outbound mail, not just receives `sendEmail` picks the app's own-brand sender automatically, configured in the dashboard's **Email** settings.
363
-
364
- ### Config (`interface.json`)
365
-
366
- ```json
367
- {
368
- "email": {
369
- "method": "handle-inbound-email",
370
- "approvedSenders": ["billing@vendor.com", "*@trusted-partner.com"]
371
- }
372
- }
373
- ```
374
-
375
- `approvedSenders` is optional. When set, only senders matching an exact address or `*@domain.com` wildcard reach the method; everything else is rejected by the platform with `400 invalid_sender` before the method runs (silently — the sender isn't bounced). Matching is case-insensitive. The same list applies uniformly across all three address tiers.
376
-
377
- ### Input shape
378
-
379
- ```ts
380
- {
381
- to: string; // full recipient address; localpart is arbitrary on catchall tiers
382
- from: string; // bare sender address, extracted from "Name <a@b>" form
383
- fromName: string | null; // sender display name, or null
384
- subject: string; // 'No Subject' if missing
385
- message: string; // plain-text body, falls back to HTML if text is missing; 'No Body' if neither was sent
386
- html: string; // HTML body, or '' when text-only
387
- attachments: string[]; // CDN URLs — already uploaded by the platform
388
- messageId: string | null; // this email's Message-ID, angle-bracketed (<id@host>)
389
- inReplyTo: string | null; // Message-ID this email is replying to, if any
390
- references: string[]; // prior Message-IDs in the thread (angle-bracketed); [] if none
391
- replyTo: string | null; // Reply-To address — reply here, not `from`, when set
392
- cc: string[]; // Cc recipient addresses
393
- date: string | null; // original send time, ISO-8601
394
- }
395
- ```
396
-
397
- To reply in-thread, feed these into `sendEmail`: set `inReplyTo` to the incoming `messageId` and `references` to `[...references, messageId]`. Send to `replyTo` when it's set, otherwise `from`. `sendEmail` returns `{ recipients, cc, bcc, from }` (who it sent to + the sender used); it does not return the sent message's own `Message-ID`, so thread off *inbound* mail, not off messages you sent.
398
-
399
- ### Attachments and size limits
400
-
401
- `attachments[]` is an array of CDN URLs — the platform has already received and uploaded the files. Fetch them server-side via the URL when you need the bytes; pass them through as URLs to UI or downstream services.
402
-
403
- Max inbound message size is 25 MB total (including all attachments). Oversized messages are rejected by the platform before the method runs.
404
-
405
- ### Auth
406
-
407
- Methods invoked through this interface run with `auth.roles: ['system']` (see the system-roles section above). They have no user session and can't impersonate. Use `auth.requireRole('system')` to gate methods that should only be reachable via email.
171
+ **Load the `inboundEmail` skill** before writing the handler address tiers, `approvedSenders`, the input shape, in-thread replies, and attachments are all there.
408
172
 
409
173
  ## MCP (Model Context Protocol)
410
174
 
411
175
  Expose the app to *external* AI agents — Claude Desktop, Cursor, other people's agents, anything that speaks MCP. Unlike the agent interface (which *is* an agent — its own LLM, personality, and chat UI), MCP has no model of its own; it's the app projected as an MCP server for an outside AI to drive.
412
176
 
413
- It supports the full MCP surface:
414
- - **Tools** — methods the agent can call (rich descriptions + machine-readable annotations).
415
- - **Resources** — read-only app data the agent can pull into context, addressable by URI.
416
- - **Prompts** — reusable, parameterized prompt templates the server offers.
417
- - **Instructions** — server-level guidance shown to the calling agent (the toolset's "system prompt").
418
-
419
- The platform hosts the server, handles auth like the API interface (optional — keyed or anonymous), and derives every tool's input schema from the method contract. Because the consumer is an external agent with no knowledge of your app, **the descriptions are the product** — load the `mcpInterfaces` skill for how to write them.
420
-
421
- ### Spec: `src/interfaces/mcp.md`
422
-
423
- Frontmatter declares the server. The body's intro prose becomes the server `instructions`; `## Tools`, `## Resources`, and `## Prompts` sections declare the rest.
424
-
425
- ```yaml
426
- ---
427
- name: Vendor Management
428
- description: Tools and data for managing vendors and purchase orders.
429
- type: interface/mcp
430
- ---
431
- ```
432
-
433
- ```markdown
434
- This server manages vendors and purchase orders. Read a vendor before updating it; submitted
435
- requests go through approval before they become active.
436
-
437
- ## Tools
438
-
439
- ### Submit a vendor request
440
- method: submit-vendor-request
441
- ~~~
442
- Submit a new vendor for approval. Use when the caller wants to add a vendor.
443
- Do NOT use to modify an existing vendor — that's update-vendor.
444
- - name: the vendor's legal name
445
- - contactEmail: billing contact; required for approval routing
446
- Returns the created vendor's id and its initial "pending" status.
447
- ~~~
448
-
449
- ### List vendors
450
- method: list-vendors
451
- annotations: readOnly
452
- ~~~
453
- List all vendors, newest first. Read-only.
454
- ~~~
455
-
456
- ## Resources
457
-
458
- - list-vendors → app://vendors — "Vendors" — all vendors (application/json)
459
- - get-vendor → app://vendors/{id} — "Vendor" — a single vendor by id (application/json)
460
-
461
- ## Prompts
462
-
463
- ### draft_vendor_email
464
- description: Draft an outreach email to a vendor.
465
- arguments: vendorId (required) — the vendor to contact
466
- ~~~
467
- Write a warm outreach email to vendor {{vendorId}} introducing our procurement process.
468
- ~~~
469
- ```
470
-
471
- Don't hand-author input schemas — the platform derives them. For a resource template, `{param}` in the URI maps to the backing method's input.
472
-
473
- ### Compiled Output: `dist/interfaces/mcp/`
474
-
475
- ```
476
- dist/interfaces/mcp/
477
- ├── interface.json ← config the platform reads
478
- ├── instructions.md ← server-level guidance (returned in `initialize`)
479
- ├── tools/
480
- │ ├── submitVendorRequest.md ← rich description, one per tool
481
- │ └── listVendors.md
482
- └── prompts/
483
- └── draftVendorEmail.md ← prompt template body, one per prompt
484
- ```
485
-
486
- Resources carry inline metadata only — no per-resource file.
487
-
488
- ### Config (`interface.json`)
489
-
490
- ```json
491
- {
492
- "mcp": {
493
- "name": "Vendor Management",
494
- "description": "Tools and data for managing vendors and purchase orders.",
495
- "instructions": "instructions.md",
496
- "tools": [
497
- {
498
- "method": "submit-vendor-request",
499
- "name": "submit_vendor_request",
500
- "title": "Submit Vendor Request",
501
- "description": "tools/submitVendorRequest.md",
502
- "annotations": { "readOnly": false, "destructive": false, "idempotent": false, "openWorld": false }
503
- },
504
- {
505
- "method": "list-vendors",
506
- "title": "List Vendors",
507
- "description": "tools/listVendors.md",
508
- "annotations": { "readOnly": true }
509
- }
510
- ],
511
- "resources": [
512
- { "method": "list-vendors", "uri": "app://vendors", "name": "Vendors", "description": "All vendors.", "mimeType": "application/json" },
513
- { "method": "get-vendor", "uriTemplate": "app://vendors/{id}", "name": "Vendor", "description": "A single vendor by id.", "mimeType": "application/json" }
514
- ],
515
- "prompts": [
516
- {
517
- "name": "draft_vendor_email",
518
- "title": "Draft vendor email",
519
- "description": "Draft an outreach email to a vendor.",
520
- "arguments": [ { "name": "vendorId", "description": "The vendor to contact", "required": true } ],
521
- "template": "prompts/draftVendorEmail.md"
522
- }
523
- ]
524
- }
525
- }
526
- ```
527
-
528
- | Field | Description |
529
- |-------|-------------|
530
- | `name`, `description` | Server display name + registry metadata (not shown to the calling agent) |
531
- | `instructions` | Relative path to the server-level guidance returned in `initialize` |
532
- | `tools[].method` | Method `id` from the manifest (kebab-case) |
533
- | `tools[].name` | Tool name exposed to clients. Optional — defaults to the method `id`. Must match `[a-zA-Z0-9_-]` and be unique within the server |
534
- | `tools[].title` | Optional human-friendly display name |
535
- | `tools[].description` | Relative path to the tool's markdown description |
536
- | `tools[].annotations` | Optional client hints (auto-call vs. confirm): `readOnly`, `destructive`, `idempotent`, `openWorld` — map to MCP's `readOnlyHint` etc. |
537
- | `resources[].method` | The read method invoked when the resource is read |
538
- | `resources[].uri` / `uriTemplate` | A static URI, or a template whose `{param}` maps to the method's input |
539
- | `resources[].name`, `description`, `mimeType` | Resource metadata |
540
- | `prompts[].name`, `title`, `description` | Prompt identity + metadata |
541
- | `prompts[].arguments` | `[{ name, description?, required? }]` |
542
- | `prompts[].template` | Relative path to the template body (`{{arg}}` placeholders) |
543
-
544
- There is no `inputSchema` field — the platform derives each tool's schema from the method's input contract.
545
-
546
- ### Platform Behavior
547
-
548
- - The platform hosts the MCP server and exposes it to external clients. Clients connect at `POST https://{app-host}/_/mcp`.
549
- - **Auth is optional**, identical to the API interface: a `Bearer` key resolves to a user with full RBAC; with no key, calls run anonymously (no user, no roles). The method is the boundary — gate sensitive tools with `auth.requireRole`/`requireUser`; a public (keyless) server exposes only the un-gated tools.
550
- - Input schemas are derived automatically from each method's input contract.
551
- - `tools/list` is static; access is enforced per-method at call time (a gated tool is listed but rejects an unauthorized call).
552
- - A resource read invokes the backing method (template `{param}`s come from the URI) and returns its output as the resource contents.
553
- - `prompts/get` fills the template with the provided arguments.
554
- - `instructions` is returned in the `initialize` response.
555
-
556
- ### Manifest
177
+ It supports the full MCP surface: tools (methods the agent can call), resources (read-only app data addressable by URI), prompts (parameterized templates), and instructions (server-level guidance for the whole toolset). The platform hosts the server, handles auth, and derives every tool's input schema from the method contract.
557
178
 
558
- ```json
559
- { "type": "mcp", "path": "dist/interfaces/mcp/interface.json" }
560
- ```
179
+ **Load the `mcpInterfaces` skill** before authoring `src/interfaces/mcp.md`. Because the consumer is an external agent with no knowledge of your app, the descriptions are the product — the skill carries both how to write them and the full config contract.
561
180
 
562
181
  ## Agent (Conversational Interface)
563
182
 
564
- A conversational interface where an LLM has access to the app's methods as tools. Unlike MCP (which exposes methods for external agents), the agent interface IS the agent — it has its own personality, system prompt, and model config, and orchestrates tool calls against the app's methods internally.
565
-
566
- This section is the wiring. Load the `agentInterfaces` skill before authoring the spec body or building the chat UI — what belongs in the agent's system prompt, how to write its tool descriptions, and the `createAgentChatClient()` frontend API are all there.
567
-
568
- ### Spec: `src/interfaces/agent.md`
569
-
570
- The human-readable spec. Frontmatter contains structured fields; the prose body is the behavioral spec — voice, personality, capabilities, rules — written in MSFM.
571
-
572
- ```yaml
573
- ---
574
- name: Todo Assistant
575
- model: {"model": "claude-4-5-haiku", "temperature": 0.5, "maxResponseTokens": 16000}
576
- description: Conversational agent that helps users manage their to-do list.
577
- ---
578
- ```
579
-
580
- Frontmatter fields:
581
- - `name` — agent display name
582
- - `model` — JSON string with `model` (MindStudio model ID), `temperature`, `maxResponseTokens`, and optional `config` (model-specific settings like `reasoning`, `tools`, etc.). Use `askMindStudioSdk` to look up available model IDs and their config options when setting the model ID. The user's UI will have a nice visual picker to allow them to change it later, so only validate model when you're setting - otherwise assume this value to be correct if it changes.
583
- - `description` — one-liner for agent card/listing
584
-
585
- The prose body contains sections like Voice & Personality, Capabilities, Behavior — whatever structure serves the agent's character. This is compiled into the system prompt and tool descriptions.
586
-
587
- ### Compiled Output: `dist/interfaces/agent/`
183
+ A conversational interface where an LLM has access to the app's methods as tools. Unlike MCP (which exposes methods for external agents), the agent interface IS the agent — it has its own personality, system prompt, and model config, and orchestrates tool calls against the app's methods internally. Chat runs as the authenticated user, so every tool call carries that user's roles.
588
184
 
589
- ```
590
- dist/interfaces/agent/
591
- ├── agent.json ← config the platform reads
592
- ├── system.md ← compiled system prompt
593
- └── tools/
594
- ├── createTodo.md ← rich tool description per method
595
- ├── listTodos.md
596
- └── ...
597
- ```
598
-
599
- ### Config (`agent.json`)
600
-
601
- ```json
602
- {
603
- "agent": {
604
- "model": "claude-4-5-haiku",
605
- "temperature": 0.5,
606
- "maxTokens": 16000,
607
- "systemPrompt": "system.md",
608
- "tools": [
609
- { "method": "create-todo", "description": "tools/createTodo.md" },
610
- { "method": "list-todos", "description": "tools/listTodos.md" }
611
- ],
612
- "webInterfacePath": "/chat"
613
- }
614
- }
615
- ```
616
-
617
- | Field | Description |
618
- |-------|-------------|
619
- | `model` | MindStudio model ID (e.g. `claude-4-5-haiku`, `claude-5-sonnet`) |
620
- | `temperature` | Model temperature |
621
- | `maxTokens` | Max response tokens |
622
- | `systemPrompt` | Relative path to the compiled system prompt markdown file |
623
- | `tools` | Array of tool entries — `method` references a method `id` from the manifest, `description` is a relative path to a markdown file with rich tool docs (when to use, examples, edge cases, parameter guidance) |
624
- | `webInterfacePath` | Optional. If the app has a web interface with a chat page, this path tells the IDE where to show the preview. Otherwise the agent is accessed via API. |
625
-
626
- ### Manifest Declaration
627
-
628
- ```json
629
- { "type": "agent", "path": "dist/interfaces/agent/agent.json" }
630
- ```
185
+ **Load the `agentInterfaces` skill** before authoring `src/interfaces/agent.md` or building the chat UI — the spec frontmatter, compiled output, `agent.json`, and the entire frontend surface are all there.
631
186
 
632
187
  ## Manifest Declaration
633
188
 
@@ -637,7 +192,7 @@ Each interface is declared in `mindstudio.json`:
637
192
  {
638
193
  "interfaces": [
639
194
  { "type": "web", "path": "dist/interfaces/web/web.json" },
640
- { "type": "api" },
195
+ { "type": "api", "path": "dist/interfaces/api/api.json" },
641
196
  { "type": "cron", "path": "dist/interfaces/cron/interface.json" },
642
197
  { "type": "webhook", "path": "dist/interfaces/webhook/interface.json" },
643
198
  { "type": "email", "path": "dist/interfaces/email/interface.json" },
@@ -647,4 +202,4 @@ Each interface is declared in `mindstudio.json`:
647
202
  }
648
203
  ```
649
204
 
650
- Some interfaces (like `api`) work without a config file just declaring the type is enough. Others need a config for command mappings, schedules, etc. Set `"enabled": false` to skip an interface during build.
205
+ An interface with nothing to configure can be declared with just its type; the rest point at a compiled config file. Set `"enabled": false` to skip an interface during build.
@@ -87,15 +87,9 @@ await mindstudio.sendEmail({
87
87
  body: content, // markdown or HTML, auto-detected; bodyType overrides. cc/bcc/replyTo/attachments also supported
88
88
  });
89
89
 
90
- // Reply in-thread to an inbound email (fields come from the email interface's input)
91
- await mindstudio.sendEmail({
92
- to: input.replyTo ?? input.from,
93
- subject: `Re: ${input.subject}`,
94
- body: reply,
95
- inReplyTo: input.messageId ?? undefined,
96
- references: input.messageId ? [...input.references, input.messageId] : input.references,
97
- cc: input.cc, // reply-all
98
- });
90
+ // Replying to inbound mail is the `inboundEmail` skill's job — load it before writing
91
+ // a handler. It has the full input shape and the threading rules; getting the headers
92
+ // wrong sends a reply that starts a new conversation instead of continuing one.
99
93
 
100
94
  // Store a file → returns a stable URL (define the store at module scope; see Files & Storage)
101
95
  const { url } = await Reports.put(buffer, { contentType: 'application/pdf', filename: 'report.pdf' });
@@ -323,7 +317,9 @@ input._request: {
323
317
  }
324
318
  ```
325
319
 
326
- `rawBody` preserves the exact bytes the client sent — whitespace, key ordering, encoding. Use it for webhook signature verification:
320
+ `rawBody` preserves the exact bytes the client sent — whitespace, key ordering, encoding. Use it for signature verification.
321
+
322
+ **There are two inbound HTTP interfaces and they expose the raw body differently.** This one is the API interface, at `input._request.rawBody`. The Webhook interface puts it at top-level `input.rawBody` and routes by a secret in the URL instead of a bearer token, which usually suits provider callbacks better since a provider can't send one. Load the `webhooks` skill before choosing — the example below is the API-interface shape and won't work unchanged in a webhook handler.
327
323
 
328
324
  ```typescript
329
325
  export async function stripeWebhook(input: {