@remotedraw/cli 0.2.1 → 0.3.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 +36 -6
- package/dist/cli.d.ts +17 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +261 -64
- package/dist/cloud.d.ts +1 -1
- package/dist/cloud.d.ts.map +1 -1
- package/dist/generated/agent-skill.d.ts +1 -1
- package/dist/generated/agent-skill.d.ts.map +1 -1
- package/dist/generated/agent-skill.js +1 -1
- package/dist/index.js +1 -1
- package/dist/locales/en.d.ts +27 -12
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/en.js +27 -12
- package/dist/locales/index.d.ts +28 -13
- package/dist/locales/index.d.ts.map +1 -1
- package/dist/locales/nl.d.ts.map +1 -1
- package/dist/locales/nl.js +26 -11
- package/dist/packageManagers.d.ts +75 -0
- package/dist/packageManagers.d.ts.map +1 -0
- package/dist/packageManagers.js +146 -0
- package/dist/scan.d.ts +77 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +563 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/update.d.ts +11 -5
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +55 -28
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const AGENT_SKILL_MARKDOWN = "---\nname: remotedraw\ndescription: Add RemoteDraw phone input to customer apps with the RemoteDraw CLI, public API, React SDK, raw HTTP, or customer-owned iOS sender flow.\n---\n\n# RemoteDraw Agent Skill\n\nUse this skill when a user asks to create, initialize, debug, or review a RemoteDraw integration.\n\n## Decision Flow\n\n1. Identify the receiver surface: web app, desktop app, iOS app, or headless/backend workflow.\n2. Identify the sender surface: RemoteDraw iOS app, embedded web sender, customer-owned iOS sender, or raw/headless sender.\n3. Pick the SDK path:\n - React SDK: web receiver and optional embedded web sender.\n - Plain JavaScript/raw HTTP: non-React web, desktop, backend, or custom clients.\n - Swift: customer-owned iOS sender apps.\n4. Pick a supported starter: `sketch` for a free-form receiver surface, or\n `screenMarkup` for annotations over a shared screen \u2014 both render through the\n `RemoteDrawReceiver` foundation, with the surrounding UI owned by the\n integrating app. Configure the target kind and descriptor directly for\n photos, PDFs, maps, bounded fields, and other custom surfaces. A bounded\n field is a target you describe yourself \u2014 `inputMapping: \"surface\"` plus a\n `coordinateSpace` \u2014 not a preset RemoteDraw ships.\n\n## Endpoint, Key, and Packages\n\nRemoteDraw is hosted. There is nothing for the customer to run or self-host.\n\n- API base URL: `https://api.remotedraw.com`\n- Public docs: `https://docs.remotedraw.com/docs`\n- API keys are created in the console at\n `https://dashboard.remotedraw.com/api/keys` and belong in `.env.local` as\n `REMOTEDRAW_API_KEY`, alongside\n `REMOTEDRAW_API_BASE_URL=https://api.remotedraw.com`.\n\nPublished npm packages \u2014 install only what the chosen path needs:\n\n| Package | Install | Use it for |\n| --- | --- | --- |\n| `@remotedraw/cli` | `npm install -g @remotedraw/cli` (or `npx @remotedraw/cli@latest`) | Scaffolding, doctor, and test sessions. |\n| `@remotedraw/react` | `npm install @remotedraw/react` | React receiver, pairing, and headless sender. Peers on `react`/`react-dom` >= 18. |\n| `@remotedraw/svelte` | `npm install @remotedraw/svelte` | Svelte receiver store over the framework-free client. |\n| `@remotedraw/client` | `npm install @remotedraw/client` | Framework-free receiver/sender/API clients for any JS runtime. |\n| `@remotedraw/protocol` | `npm install @remotedraw/protocol` | Shared schemas, types, and limits for raw-HTTP integrations. |\n| `@remotedraw/geometry` | `npm install @remotedraw/geometry` | Normalized stroke geometry, shape assist, hit testing, export. |\n\n`@remotedraw/react` already depends on `client`, `protocol`, and `geometry`, so\ndo not add those separately for a React app.\n\nFor customer-owned iOS senders, prefer the published SwiftPM package \u2014 it is the\nsame implementation the first-party RemoteDraw app runs:\n\n```swift\n.package(url: \"https://github.com/AxioSOzo/remotedraw-swift.git\", from: \"0.1.0\")\n.product(name: \"RemoteDrawSenderKit\", package: \"remotedraw-swift\")\n```\n\n`package:` is the repository basename, not the module name. `remotedraw init\n--sdk swift` remains available and writes one self-contained\n`RemoteDrawIntegration.swift` against the public HTTP routes; use it only when\nadding a SwiftPM dependency is not an option.\n\n## CLI First\n\nRead the machine-readable option catalog before choosing a plan:\n\n```sh\nremotedraw options --format json\n```\n\nInitialize a project with the closest supported path:\n\n```sh\nremotedraw init --target web --sender remotedraw-ios --sdk react --preset sketch\nremotedraw init --target web --sender embedded-web --sdk react --preset sketch\nremotedraw init --target desktop --sender remotedraw-ios --sdk js --preset sketch\nremotedraw init --target ios --sender own-ios --sdk swift --preset sketch\n```\n\nBy default, `new` and `init` create the dashboard project and a project-scoped\ndevelopment API key, then write `REMOTEDRAW_API_BASE_URL`,\n`REMOTEDRAW_PROJECT_ID`, and `REMOTEDRAW_API_KEY` to a gitignored `.env.local`.\nUse `--offline` only when cloud setup is intentionally out of scope.\n\n`--sender own-ios` requires `--sdk swift`; every other combination of\n`--target`, `--sender`, and `--sdk` is accepted. Invalid combinations fail with\n`INVALID_COMBINATION` rather than guessing.\n\nAgents must use explicit non-interactive dry runs before changing a project:\n\n```sh\nremotedraw init --non-interactive --offline --dry-run --format json --target web --sender remotedraw-ios --sdk react --preset sketch --package-manager npm\n# Inspect plan, files, defaultsApplied, and warnings before applying.\nremotedraw init --non-interactive --offline --format json --target web --sender remotedraw-ios --sdk react --preset sketch --package-manager npm\nremotedraw doctor --format json\nremotedraw create-input --preset sketch --json\n```\n\nDo not use the interactive wizard, synthesize arrow-key input, or scrape\nhuman-formatted output. If receiver, sender, or preset intent is ambiguous, ask\nthe user instead of guessing. Never pass `--force` unless overwrite scope was\nexplicitly approved.\n\n## Security Rules\n\n- Keep `rd_sk_...` API keys in trusted backend secrets only.\n- Keep the account-level `rd_cli_...` credential in the user config directory; never copy it into a project. Use `REMOTEDRAW_CLI_TOKEN` only as an explicitly managed CI secret.\n- Never place API keys in browser bundles, mobile clients, screenshots, logs, or generated examples.\n- Public clients should receive only `joinUrl`, `joinToken`, `receiverToken`, or `senderToken` values scoped to the session.\n- Production QR codes should use HTTPS `joinUrl` values. Do not make the custom scheme the primary QR target.\n\n## API Contract\n\n- Backend creates sessions with `POST /v1/sessions`.\n- Receiver clients read `POST /v1/receiver/session`, `/drawings`, `/drafts`, and `/senders` with a receiver token.\n- Sender clients join with `POST /v1/join`, stream mutable drafts to `/v1/sender/draft`, commit durable strokes to `/v1/sender/commit`, and finish with `/v1/sender/submit`.\n- Custom senders should throttle draft updates, coalesce to the latest pending preview, and commit one durable stroke on pointer-up with a stable `clientStrokeId`.\n\n## AI Actions\n\nReach for AI when the product needs something _from_ the finished drawing:\na generated image, a description, or structured data to branch on. Backend only\n(`aiActions:*` scopes on an `rd_sk_...` key). Never wire it to a commit,\nsubmit, or presence event \u2014 AI runs only on an explicit `POST /v1/ai-actions`\ncall the user asked for. Run it after the user is done; the route accepts\n`active` and `ended` sessions.\n\nMinimal request per outcome (`POST /v1/ai-actions`, plus optional\n`quality: \"fast\" | \"balanced\" | \"max\"`, default `balanced`):\n\n```jsonc\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\" } // image back in the response\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\", \"deliver\": [\"result\", \"board\"] } // and onto the board\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"Describe this drawing.\" } // text back\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"...\", \"text\": { \"schema\": { /* JSON Schema */ } } } // typed JSON\n```\n\nThe response is asynchronous: `create` returns `status: \"queued\"`. Poll\n`POST /v1/ai-actions/get` until `status` is `succeeded`, `failed`, or\n`canceled`, or use the helper on the API client\n(`createHttpRemoteDrawApiClient` from `@remotedraw/client`, re-exported by\n`@remotedraw/react`). This client holds the `rd_sk_` key, so it only ever runs\non the backend:\n\n```ts\nconst action = await client.createAiAction({\n sessionId,\n request: \"image\",\n prompt,\n});\nconst finished = await client.waitForAiAction({ aiActionId: action.id });\n// finished.result: generatedImageUrl | generatedText | generatedData | boardDrawingIds\n```\n\nThere is no completion webhook. Results are RemoteDraw-hosted asset URLs, not\nprovider URLs. See https://docs.remotedraw.com/docs/api#ai for the full\nrequest, credit, and legacy-mapping tables.\n\n## Verification\n\nAfter changes, verify against the customer's project \u2014 never assume RemoteDraw's\nown repo scripts exist here.\n\n```sh\nremotedraw doctor # config, SDK deps, REMOTEDRAW_* env\nremotedraw create-input --execute # open a real session, print the join URL\n```\n\nThen run whatever type check and test command the project already defines (for\nexample `npm run typecheck` and `npm test`). Do not invent script names, and do\nnot run `bun run test:api`, `bun run typecheck`, or `bun run ios:kit:test` \u2014\nthose are RemoteDraw's internal monorepo scripts and will not exist in a\ncustomer project.";
|
|
1
|
+
export declare const AGENT_SKILL_MARKDOWN = "---\nname: remotedraw\ndescription: Integrate RemoteDraw phone drawing into a customer's product \u2014 a phone becomes the pen for a screen the customer already owns. Covers the two-device model, which flows are valid, the ready-made components (React/Svelte/JS receiver, hosted web sender, RemoteDrawSenderKit iOS sender), per-surface recipes, and how to judge whether a proposed use case fits at all.\n---\n\n# RemoteDraw Agent Skill\n\nUse this skill when a user asks what RemoteDraw is, whether it fits their app,\nor to create, debug, or review a RemoteDraw integration.\n\nRead the whole file before proposing a design. The first two sections decide\nwhether the use case is possible; everything after decides how it is built.\n\n## The model: two devices, always\n\nRemoteDraw is not a drawing library. It is a wire between **two devices**.\n\n- **The receiver is the paper.** A screen someone is looking at \u2014 a laptop, a\n desktop, a large display, a kiosk, a tablet on a desk \u2014 showing *your*\n product. Your app renders whatever is being drawn on: the map, the photo, the\n PDF page, the form, the whiteboard. RemoteDraw renders none of that content.\n It paints ink on top of it.\n- **The sender is the pen.** A phone. It supplies a hand, pressure, tilt and a\n stroke. It does **not** supply the content: the iOS SDK opens no camera and no\n file picker (it needs no `Info.plist` entries at all). On a bounded surface\n the phone's pad *is* the surface; on a large one the phone is a viewport\n moving over it.\n- **RemoteDraw is the wire.** A hosted session carries strokes from the pen to\n the paper in real time and stores them. Nothing runs on the customer's\n infrastructure except the session-creation call.\n\nThe one question that decides whether RemoteDraw fits:\n\n> **What is being drawn on, and which screen is it already displayed on?**\n\nIf the answer is \"a screen the user is looking at, and they wish they could\ndraw on it with their hand\" \u2014 that is RemoteDraw. If the answer is \"the phone's\nown screen\", it is not: a phone drawing on its own content and uploading the\nresult is a camera-and-canvas feature you build with PencilKit or a `<canvas>`,\nand RemoteDraw would only add a round trip.\n\nThree consequences, because they are the mistakes integrators actually make:\n\n1. **The phone never supplies the picture.** \"The tenant photographs the leak\n and circles it\" is *not* a RemoteDraw flow \u2014 there is no second screen. The\n RemoteDraw version of that job: the photo is already open in your web app on\n the office desktop, and the person at that desk circles the leak with their\n phone instead of a mouse.\n2. **The receiver already exists.** RemoteDraw goes onto a screen your product\n already has. It does not get its own page unless the user asks for a demo.\n3. **Ink is coordinates, not pixels.** Strokes arrive in normalized board space\n (`0..1`, remapped through the sender's `device.aspectRatio`). Your app\n decides what board space *means* \u2014 a pixel, a page, a field, a coordinate on\n Earth. RemoteDraw never sees your content.\n\n## Which flows are valid\n\nRead the row for the **sender** (who holds the phone) and the column for the\n**receiver** (the screen showing the content).\n\n| Sender (the pen) | Receiver (the paper) | Valid? | Notes |\n| --- | --- | --- | --- |\n| iPhone \u2014 RemoteDraw app, your app via `RemoteDrawSenderKit`, or hosted `/join` in Safari | Desktop / laptop browser | **Yes \u2014 the canonical flow** | Everything below is written for it. |\n| iPhone (any of the three) | Large display, TV, projector, kiosk browser | **Yes** | Size `target.coordinateSpace` to the display. |\n| iPhone (any of the three) | Desktop app \u2014 Electron, macOS, Windows \u2014 via `@remotedraw/client` or raw HTTP | **Yes** | No React needed; poll `/v1/receiver/*` or use the realtime source. |\n| Android phone \u2014 hosted `/join` in Chrome | Any of the above | **Yes** | There is no native Android SDK. The hosted join page *is* the Android sender and it is full-featured. |\n| iPhone / Android | iPad or tablet browser, as a **second** device someone else is looking at | **Yes** | Two devices, two people. |\n| Headless script, test, or agent \u2014 raw `POST /v1/join` \u2192 `/v1/sender/draft` \u2192 `/v1/sender/commit` | Any receiver | **Yes, for verification only** | Never ship a hand-rolled sender to users. |\n| Any phone | **The same phone** \u2014 one device shows the content and draws on it | **No** | There is no second screen. Use PencilKit / `<canvas>`. RemoteDraw adds a network hop and nothing else. |\n| A phone that must first **capture** the content \u2014 photograph or scan it | *(anything)* | **No** | The iOS sender opens no camera and needs no `Info.plist` entries. The content must already be on the receiver. |\n| Desktop mouse or trackpad as the pen | *(anything)* | **No \u2014 does not exist** | There is no desktop sender. The macOS trackpad sender is unbuilt research. Do not promise it. |\n| Phone \u2192 phone, two different people, two different devices | Phone browser as receiver | *Technically yes, rarely right* | A phone browser is a browser. But if both people hold phones, ask why the drawing is not simply in one app. |\n\n**The three nevers of the model.**\n\n1. **Never same-device.** If sender and receiver would be one phone, stop and\n say so. Propose the non-RemoteDraw alternative.\n2. **Never make the phone the source of content.** The receiver supplies what\n is drawn on. (One honest exception: the hosted `/join` pad has a file-attach\n tool that can put a file on the board. It is a hosted-sender capability, not\n a way to make a same-device flow valid, and it cannot currently be disabled.)\n3. **Never promise a sender RemoteDraw does not ship.** iOS (native SDK) and\n any mobile browser (hosted `/join`) are the senders. Nothing else exists.\n\n## Never do these\n\n- **Never write a custom canvas.** Not a `UIViewRepresentable` drawing view, not\n a `<canvas>` sender pad, not a hand-rolled SVG receiver, not a raw\n `URLSession`/`fetch` draft loop. Cadence, point budgets, the packed-point\n codec, sequence healing, token refresh and presence are protocol, and the SDKs\n implement them. Go headless on the SDK's own primitives if you must.\n- **Never put the iOS surface in a small pad or a draggable sheet.** It is\n full screen (`.remoteDrawSurface` / `RemoteDrawTakeover`). A sheet is\n acceptable only if `RemoteDrawSurface` fills it edge to edge and the sheet\n cannot be dragged mid-stroke. There is no small-canvas option.\n- **Never create a session on mount or on page load.** `POST /v1/sessions` is\n billable and not idempotent, and React StrictMode fires mount effects twice.\n Create it on the server (route handler, loader, server action) or on explicit\n user intent, and guard with a ref if it must be a client effect.\n- **Never let an `rd_sk_\u2026` key reach a client.** Not browser bundles, not Swift,\n not app bundles, screenshots, logs, or generated examples. The phone never\n calls `/v1/sessions/direct-sender`; your backend does.\n- **Never poll by hand when a component or store exists.** `RemoteDrawProvider`\n / `createReceiverStore` already do one-in-flight polling plus an optional\n realtime push source.\n- **Never promise Bluetooth or Wi-Fi pairing.** The API advertises\n `bluetooth` and `localNetwork` as pairing methods and the iOS/Android apps do\n *advertise* on those radios, but **nothing scans, browses, or connects\n anywhere in the product**. They are not implemented. Do not present them as\n options; do not build UI around them. QR and direct sender are the real ones.\n- **Never promise a native Android SDK, a desktop sender, per-tenant Universal\n Links, e-signature compliance, or streaming inside `RemoteDrawSenderKit`.**\n See \"What does not exist yet\".\n- **Never delete or replace a host-app feature on your own initiative.** Build\n beside it.\n\n## The decision tree\n\n**1. What is being drawn on? \u2192 `target.kind` + `inputMapping`.**\n\n`target.kind` accepts `whiteboard`, `paper`, `canvas`, `svg`, `map`, `tldraw`,\n`field`, `image`, `pdf`, `screen`, `custom`. It selects the background the\nreceiver defaults to, the tool policy, and deposit defaults.\n`target.inputMapping` is `surface` (the phone's whole pad *is* the target \u2014 use\nfor bounded targets like a signature field) or `viewport` (the phone is a\nmovable window over a larger board \u2014 the default when omitted).\n\n| Kind | Host renders | `inputMapping` | Sender | Status |\n| --- | --- | --- | --- | --- |\n| `whiteboard` / `paper` | nothing \u2014 RemoteDraw's own ground | `viewport` (or `surface`) | hosted `/join` or SenderKit | shipped |\n| `image` (photo, screenshot) | the `<img>`, as `background` | `surface` (whole photo) or `viewport` (zoomable) | either | shipped |\n| `pdf` | your PDF renderer, one page at a time, as `background` or behind a `transparent` receiver | `viewport` | either | shipped; RemoteDraw renders no PDFs |\n| `field` (signature, initials) | the form, with a baseline as SVG `children` | **`surface`** \u2014 mandatory | either | shipped; **not** an e-signature product |\n| `screen` | a screenshot, or a live stream you publish | `viewport` | either | shipped; live view is experimental |\n| `map` | your map (Mapbox / MapLibre / Leaflet / Google) | `viewport` | hosted `/join` or SenderKit | shipped \u2014 use `RemoteDrawMapReceiver` |\n| `custom` | anything else you own | `viewport` | hosted `/join` (streaming) or SenderKit (ink only) | shipped |\n\n**2. Which stack renders the receiver? \u2192 the composition.**\n\n| Host | Use | Not |\n| --- | --- | --- |\n| React / Next / Remix | `@remotedraw/react`: `RemoteDrawProvider` + `RemoteDrawReceiver` (or `RemoteDrawMapReceiver`) + `PairingCode`/`RemoteDrawConnect`/`RemoteDrawLaunchButton` + `RemoteDrawSessionControls` | A hand-rolled SVG or polling loop |\n| Svelte / SvelteKit | `@remotedraw/svelte`: `createRemoteDrawReceiver` store + `createDirectSender` store. **No components ship** \u2014 you write all the markup, including ink rendering | Assuming React's components exist here |\n| Anything else with JS (Electron, Vue, vanilla) | `@remotedraw/client`: `createHttpReceiverClient`, `createReceiverStore`, `createRealtimeReceiverSource` | \u2014 |\n| No JS at all | Raw HTTP `POST /v1/receiver/*` with the receiver token | \u2014 |\n\n**3. Who holds the phone? \u2192 the sender path.**\n\n| Situation | Path |\n| --- | --- |\n| Anyone who can scan; no phone app in the product | **Hosted `/join`.** Render `joinUrl` as a QR with `PairingCode`. Zero sender code. The RemoteDraw iOS app opens the same HTTPS link through Universal Links; every other phone gets the web pad. |\n| The product has a first-party iOS app the user already installed *and* it is the RemoteDraw app | Same QR. Universal Links open it. |\n| The product has **its own** iOS app and the user is signed in | **Direct sender.** Backend calls `POST /v1/sessions/direct-sender` with `launchUrlTemplate: \"yourapp://draw?senderToken={senderToken}\"`; the browser shows `RemoteDrawLaunchButton`; the app receives the URL in `onOpenURL` and presents `.remoteDrawSurface(isPresented:senderToken:)`. Keep the QR as the fallback \u2014 the button reveals one automatically. |\n| A QR that opens the **customer's own** app | **Not possible.** The Universal-Link association file has one hard-coded appID and the QR host is a RemoteDraw deployment setting. Use the direct sender instead. |\n| Headless / tests | `POST /v1/join` \u2192 `/v1/sender/draft` \u2192 `/v1/sender/commit` |\n\n## Component catalogue\n\nEverything below is a real export, read from source. Anything not listed does\nnot exist. Grouped by decision: the board \u2192 what is behind it \u2192 pairing \u2192\nsession state \u2192 AI \u2192 clients \u2192 iOS.\n\n### `@remotedraw/react`\n\n**`RemoteDrawProvider`** \u2014 receiver session state, polling, and actions.\nRequires a `receiver` client to do anything.\nProps: `children`, `session`/`sessionId`/`joinUrl`/`joinUrls`/`joinTokenUse`/`pairing`/`receiverToken`,\n`receiver` (a `ReceiverClient` \u2014 **without it the store is inert and silent**),\n`createSession`, `createSessionRequest` (default `{ target: { kind: \"custom\" } }`),\n`autoCreate` (default: true when `createSession` and no `session`),\n`pollIntervalMs` (`1000`), `draftPollIntervalMs` (`250`), `source` (`null`),\n`fallbackToPolling` (`true`), `initialDrawings`/`initialDrafts`/`initialSenders` (`[]`),\n`onError` (`(error: Error) => void` \u2014 **wire it; it is the only signal for a dead credential**).\nContext: `session, sessionId, joinUrl, joinUrls, joinTokenUse, pairing, receiverToken, credentials, drawings, drafts, senders, loading, creating, error, transport, create, refetch, ingest, undo, clear`.\n*Limits:* `error` is a plain `Error`; narrow with `instanceof RemoteDrawHttpError`\nfor `.status`/`.code`/`.shouldReJoin`.\n\n**`RemoteDrawReceiver`** \u2014 the receiver foundation. Paints committed ink, live\ndrafts, sender pointers and connected phones over a configurable background, and\nprescribes nothing around it.\nProps: `drawings`/`drafts`/`senders` (fall back to the provider), `background`\n(default: the session target's surface inside a provider, else `\"whiteboard\"`;\naccepts a surface name, `\"transparent\"`, any CSS background string, or any\nReactNode), `pointers` (`true`), `phones` (`true`), `pointerColor` (`#1f7a8c`),\n`strokeColor` (`#151512`), `draftColor` (`#1f7a8c`), `strokeWidth` (`6`),\n`coordinateAspectRatio` (default: the session's `coordinateSpace`, else `1`;\nalso sets CSS `aspect-ratio`), `preserveAspectRatio` (`\"none\"`),\n**`projectPoint`** (`(point) => {x,y} | null` in **CSS pixels from the\nreceiver's top-left** \u2014 the seam for a camera; return `null` to drop a point),\n**`space`** (the same seam in the receiver's own viewBox coordinates; `projectPoint`\nwins when both are given), `animate` (`true`), `children` (SVG overlay in\n`0..surfaceWidth \u00D7 0..1000`), `className`/`style`/`svgProps`/`aria-label`.\n*Defaults that surprise:* it renders immediately with no empty state; `phones`\nand `pointers` are on.\n*Limits:* without `projectPoint`/`space` it stretches board space across its own\nelement \u2014 correct for a fixed board, silently wrong over a live map. Use\n`RemoteDrawMapReceiver` there.\n*Never:* stack it on a pannable map without a projection.\n\n**`RemoteDrawMapReceiver`** \u2014 `RemoteDrawReceiver` wired to a map the host owns.\nRenders a transparent ground (your map is the ground) plus the\nboard\u2192geography\u2192pixel projection. Takes every `RemoteDrawReceiver` prop except\n`projectPoint`/`space`, plus:\n`bounds` (the board's `target.coordinateSpace.bounds`; defaults to the session's\nown inside a provider \u2014 usually pass nothing), `mapBounds` (the map's current\nvisible bounds; **exact only for a north-up, unpitched camera**),\n`projectLngLat` (`(lng, lat) => {x,y} | null` \u2014 normally\n`map.project([lng, lat])`; exact under rotation and pitch, and wins over\n`mapBounds`).\n*Limits:* with neither `projectLngLat` nor `mapBounds` it renders unprojected\nand warns once in development. Leave `preserveAspectRatio` at `\"none\"`.\n*Never:* recompute the projection identity on every render \u2014 memoize it and bump\nit on the map's `move` event.\n\n**`RemoteDrawPhoneProjection`** \u2014 one connected phone drawn in place, as an SVG\n`<g>` for a host with its own `<svg>`. `layout` (required, from\n`phoneProjectionLayouts(senders)`), `space` (default 1000\u00D71000), `color`,\n`model` (`\"auto\"`), `className`. `RemoteDrawReceiver` already renders these.\n\n**`PairingCode`** \u2014 the pairing component: the scannable code with live status,\nhover-to-copy, and subtle branding.\nProps: `joinUrl` (default: the provider's, falling back to `joinUrls.web`),\n`size` (`200`, or `\"fill\"`), `direction` (`\"paper\"`, one of 14 \u2014 TEMPORARY),\n`treatment` (`\"fluid\"`, one of 11 \u2014 TEMPORARY), `accentColor` (`#1f7a8c`),\n`inset`, `tile` (`true`), `logo` (the RemoteDraw mark; `false` for none, a\nstring for a URL), `logoPlacement` (`\"plate\"`), `title`/`description`,\n`showStatus` (`\"auto\"`), `joinMode` (the session's `joinTokenUse`),\n`showJoinMode`/`showLink` (`false`), `copyOnHover` (`true`), `onCopy`,\n`onConnected`/`onConnectedDismiss`, `labels`, `alt`, `placement` (`\"inline\"` +\nfour corners), `position`/`offset`/`zIndex` (`\"absolute\"`/`12`/`20`),\n`className`/`style`/`codeClassName`/`codeStyle`.\n*There is no `card`, `variant`, `showBrand`, `brand`, or `status` prop.* The\ncard is always drawn (`tile`, on by default) and extends to hold `title` /\n`description`; status is **inferred** (connected \u2192 error \u2192 expired \u2192 ready \u2192\nidle) and cannot be passed.\n*Limits:* always encodes the HTTPS `joinUrl`. The animated optical field that\n`variant=\"aurora\"` once selected is now the separate EXPERIMENTAL\n`AuroraPairingField`, decodable only by the RemoteDraw app's own scanner.\n\n**`PairingDevices`** \u2014 the list UI for pairing methods that resolve to a device\n(`bluetooth | localNetwork | accountPresence | direct`). `method` (required),\n`devices` (`[]`), `onSelectDevice`, `joinUrl`, `showCodeFallback` (`true`),\n`status`, `size` (`168`), `emptyLabel`, `actions`, `accentColor`,\n`autoHideOnConnected` (`true`), `connectedHideDelayMs` (`1150`), `direction`,\n`logo`, `onCopy`, `className`/`style`.\n*Limits:* **purely presentational \u2014 it discovers nothing.** You supply `devices`\nand `onSelectDevice` from your own backend. There is no Bluetooth, no Bonjour,\nand no customer-reachable account-presence route.\n*Never:* present it as \"nearby device pairing\" to a customer. It is chrome.\n\n**`RemoteDrawConnect`** \u2014 a compact trigger button that opens the code or the\ndevice list in a popover, for pairing exactly at the field, margin, or toolbar\nthat needs it. Everything from `PairingCodeProps` except placement/position/\noffset/zIndex/className/style/size/tile, plus `size` (`168`), `open`,\n`defaultOpen` (`false`), `onOpenChange`, `placement` (`\"bottom\"`), `method`,\n`devices`, `onSelectDevice`, `trigger`, `triggerLabel` (`\"Pair phone\"`),\n`showTriggerLabel` (`false`), `triggerClassName`/`triggerStyle`/`triggerDisabled`,\n`openOnHover` (`true`), `panel*`/`popover*` class and style.\n*Use it instead of hand-rolling a \"connect phone\" button.*\n\n**`RemoteDrawLaunchButton`** \u2014 \"open on my phone\" for a user whose **own** app is\nthe sender. Mints a scoped sender through your backend, opens the deep link, and\nreveals a QR when the app never comes back.\nProps: `connect` (required \u2014 your own backend endpoint; may return the raw\n`connectSender` response, a create-session response carrying `senderConnection`,\nor just `{ launchUrl }`; resolving `null` means \"no direct sender for this user\"),\n`handoffTimeoutMs` (`DIRECT_SENDER_HANDOFF_TIMEOUT_MS` = `12_000`),\n`showQrFallback` (`true`), `fallback`, `pairingProps`, `labels`, `accentColor`\n(`#1f7a8c`), `disabled` (`false`), `autoLaunch` (`true`), `openUrl` (default\nassigns `window.location.href`), `onError`, `onStatusChange` (transitions only),\n`children` (node or `(state) => node`), `className`/`style`/`buttonClassName`/\n`buttonStyle`/`aria-label`.\n*Why it is a component and not an `onClick`:* a custom scheme nothing has\nregistered fails **silently** \u2014 no error, no navigation, no event. The timeout\nwith no sender on the board is the only detector.\n\n**`useDirectSender(options)`** \u2014 the hook the button is a thin default over.\nOptions: `connect` (required), `autoLaunch`, `openUrl`, `onError`,\n`onStatusChange`. Returns `{ status, launchUrl, senderToken, senderId,\nconnection, error, connect(), launch(), reset() }`.\n`status`: `idle | connecting | ready | drawing | submitted | expired | error`.\n`error` is a `RemoteDrawLaunchError` (an `Error`) with\n`kind: \"connect-failed\" | \"no-launch-url\" | \"launch-blocked\"` and the original\nrejection on `cause`.\n*Limits:* phases after `ready` are read from `RemoteDrawProvider`; outside one\nit can never advance past `ready`.\n\n**`RemoteDrawSessionControls`** \u2014 the one session-state surface: a collected\nstatus bar (or card, via `title`) telling the session's story \u2014 waiting for a\nphone, connected, drawing, submitted, with the sender's device name \u2014 plus\nundo/clear. `actions` (`[\"undo\",\"clear\"]`), `confirmClear` (`true`), `title`,\n`submission`, `labels`, `undoLabel`/`clearLabel`/`confirmClearLabel`,\n`metadataKeys`/`metadataLabels`/`formatMetadataValue` (keys render humanized,\nnever raw), `className`/`style`.\n*Limits:* every failure renders as one string, \"Connection problem\".\n\n**`AiImage` / `AiText`** \u2014 render a finished `AiAction`. `AiImage`: `action`,\n`direction` (`\"plate\"`, 6 options), `actions` (`\"hover\"`, 4),\n`standardActions` (`[\"download\",\"copy\"]`, plus `\"open\"`), `customActions`,\n`theme` (`\"auto\"`), `aspectRatio`, `radius` (`14`), `fit` (`\"cover\"`),\n`fileName`, `labels`, `onRetry`, `placeholder`, `imageAlt`, `className`/`style`.\n`AiText`: `action`, `direction` (`\"note\"`, 5), `actions` (`\"bar\"`),\n`standardActions`, `customActions`, `theme`, `radius`, `maxWidth` (`\"60ch\"`),\n`labels`, `onRetry`, `placeholder`, `className`/`style`.\n*Limits:* neither ever shows the model, tier, latency, credit cost, or the\nprovider's error string. A schema run renders nothing \u2014 `result.generatedData`\nis for your code.\n\n**`useVisualContextPublisher`** (experimental) \u2014 the **publish** half of live\nview: sends the receiver's pixels to the phone drawing on it. Has its own\n`onError`.\n\n**`RemoteDrawStreamView`** \u2014 the **consume** half, for a sender pad you host\nyourself: the receiver's stream as a ground, your pad as its `children`.\nProps: `senderToken`, `signaling` (a `VisualContextSignalingClient` \u2014 build it\nwith `createRealtimeVisualContextSignalingClient` for push, or\n`createHttpVisualContextSignalingClient` for the polled `/v1/.../visual-context/*`\nroutes), `enabled` (gate it on `viewReceiverContext` + `visualContext.enabled` +\na reported phone projection), `iceServers` (from the session's\n`visualContext.iceServers` \u2014 without them a phone on cellular connects to\nnothing), `pollIntervalMs`, `onStatus`, `onError`, plus `fit` (`\"contain\"`),\n`fadeMs`, `posterStyle`, `className`/`style`/`aria-label`, `children`.\n`useRemoteDrawStream(options)` is the same thing headless, returning\n`{ mediaStream, status, streamStatus, stream, error, markStreamLive }` with\n`status` one of `idle | connecting | streaming | unsupported | failed | closed`.\n*Limits:* `children` are deliberately **not** gated on the stream \u2014 a pad that\nonly appears once pixels arrive never appears on the networks where WebRTC\ncannot connect, and drawing must keep working there.\n\n**`VisualContextVideoLayer`** \u2014 the raw `<video>` for a `MediaStream` you\nproduce yourself. `mediaStream`, `status`, `fit` (`\"contain\"`), `fadeMs`\n(`VISUAL_CONTEXT_FADE_MS` = `220`), `posterStyle`, `onLiveChange`,\n`className`/`style`/`aria-label`. Hand over on `onLiveChange(true)`, not on\nhaving a stream. `RemoteDrawStreamView` wires this for you.\n\n**Hooks:** `useRemoteDraw` (throws outside the provider),\n`useRemoteDrawSession`, `useReceiverData`, `usePairingUrl`,\n`useRemoteDrawPointers`, `useReceiverStrokes`.\n\n**Low-level / rarely right:** `InkCanvas` (WebGL2 ink substrate \u2014 the receiver\ndrives it), `RemoteDrawMark`, `AuroraPairingField` (EXPERIMENTAL),\n`FreehandFilmGroup`/`freehandStrokePaths`, the element-selection helpers.\n\n**Deliberately not exported:** a web sender component. Hosted `/join` is the web\nsender. Custom in-page pads are built headless on `createHttpSenderClient`.\n`@remotedraw/react/next` is a separate, unfinished v2 entry point \u2014 do not mix\nit into a normal integration.\n\n### `@remotedraw/svelte`\n\n`createRemoteDrawReceiver(options)` \u2192 `{ subscribe, create, refetch, ingest,\nundo, clear, setSession, configure, start, stop }`.\n`createDirectSender({ connect, receiver, autoLaunch, openUrl, onError })` \u2192\n`{ subscribe, connect, launch, reset, stop }`, the same state machine React's\n`useDirectSender` binds. Plus `export * from \"@remotedraw/client\"`.\n**No components ship.** A Svelte integrator writes the pairing UI, the ink\nrendering and the session UI themselves.\n\n### `@remotedraw/client` (framework-free)\n\n- `createHttpRemoteDrawApiClient(baseUrl, { apiKey })` \u2014 **server-only.**\n `createSession`, `getSession`, `listSessions`, `issueJoinToken`,\n `connectSender`, `endSession`, `createAiAction`, `getAiAction`,\n `cancelAiAction`, `waitForAiAction`.\n- `createSessionWithHttpApi(baseUrl, options)` \u2014 server-only convenience.\n- `createHttpReceiverClient(baseUrl)` \u2014 the seven `/v1/receiver/*` calls.\n Credentials go in the body, not a header.\n- `createHttpSenderClient(baseUrl, { packPoints })` \u2014 14 sender calls.\n- `createReceiverStore(options)` \u2014 the headless receiver state machine\n `RemoteDrawProvider` and the Svelte store both bind.\n- `createRealtimeReceiverSource({ driver })` + `createConvexRealtimeDriver({ client })`\n \u2014 push transport over the hosted realtime endpoint. `convex` is never imported\n by the package; you hand it a two-method driver.\n- `RemoteDrawHttpError` \u2014 `status`, `code`, `upgradeUrl`, `body`, and the\n getters `isAuthenticationFailure`, `isPermissionFailure`, `isSessionOver`,\n `shouldReJoin`.\n- `joinTokenFromInput`, `joinUrlForOrigin`, `nativeJoinUrlFromSession`.\n- `createPacedDraftQueue` / `createLatestOnlyQueue` / `draftPointsForTransport`\n / `retryIdempotentRequest` \u2014 the 32 ms latest-only draft gate a custom sender\n must use instead of POSTing every pointer event.\n- `createDirectSenderController`, `resolveDirectSenderStatus`,\n `directSenderConnectionFromResult` \u2014 the shared direct-sender rules.\n- `createRealtimeVisualContextSignalingClient` /\n `createHttpVisualContextSignalingClient` \u2014 where live-view signals travel\n (realtime push, or the polled public `/v1/.../visual-context/*` routes).\n\n### `@remotedraw/geometry`\n\nStroke/shape helpers (`buildNormalizedStroke`, `recognizeStroke`,\n`simplifyNormalizedPoints`, hit-testing, transforms, `drawingsToSvg`), **and the\nmap board transform**, re-exported by `@remotedraw/react`:\n`boardPointFromLngLat`, `lngLatFromBoardPoint`, `longitudeFromBoardX`,\n`latitudeFromBoardY`, `mercatorYFromLatitude`, `latitudeFromMercatorY`,\n`boardViewportFromMapBounds`, `mapBoundsFromBoardViewport`,\n`mapBoardPointToScreen`, `mapBoardPointFromScreen`, `screenPointFromBoardPoint`,\n`surfacePointFromBoardPoint`, `padMapBounds`, `MAX_MERCATOR_LATITUDE`.\nUse these rather than reimplementing the projection \u2014 `x` is linear in\nlongitude, `y` is linear in **Web Mercator**, and a version that is linear in\nlatitude puts ink kilometres away.\n\n### `RemoteDrawSenderKit` (SwiftPM, iOS 17+)\n\n`https://github.com/AxioSOzo/remotedraw-swift.git`, product\n`RemoteDrawSenderKit`. Zero external dependencies. No `Info.plist` entries.\n\n- `RemoteDraw.shared` \u2014 **zero-config**: it installs production defaults the\n first time anything reads it. `RemoteDraw.configure(_:)` in `App.init` is\n *optional* and only overrides `apiBaseURL`, `device`, `tokenProvider`,\n `urlSession`, `onClientAdvisory`. `try RemoteDraw.requireConfigured()` throws\n `RemoteDrawError.notConfigured` if you want the strict behaviour back.\n (This used to `preconditionFailure` from inside the modifier's `.task` \u2014 a\n crash on the user's tap. It no longer does.)\n- `.remoteDrawSurface(isPresented:senderToken:appearance:strings:exit:onOutcome:)`\n \u2014 the whole integration, a full-screen cover with the board, an exit and an\n outcome. A second overload adds `background:` \u2014 a `@ViewBuilder` handed a\n `RemoteDrawGroundContext` (`session`, `mapBounds`, `phoneProjection`, `size`,\n `reportViewport`) for your own cartography or ground. A ground that **moves**\n must call `reportViewport(_:)`; a static one calls nothing.\n- `RemoteDrawTakeover` \u2014 the same board plus scene-phase wiring and exit, for\n your own cover / navigation push / `UIHostingController`.\n- `RemoteDrawSurface` \u2014 the board as a plain `View`, for a host that fills its\n presentation with it edge to edge.\n- `RemoteDrawSenderSession` \u2014 the headless core (`begin`/`append`/`end`,\n `undo`, `clear`, `submit(metadata:)`, `edit`, `updateProjection`, `leave`,\n published `phase`, `strokes`, `live`, `lastError`), with\n `RemoteDrawInkCanvas` + `RemoteDrawStrokeCapture` when you own the screen.\n- `RemoteDrawBoardCanvas`, `RemoteDrawMapBoardGround`, `RemoteDrawMapGeometry`,\n `RemoteDrawAppearance`, `RemoteDrawStrings`, `RemoteDrawExit`,\n `RemoteDrawError` (13 cases with `shouldReJoin` / `isRetriable`).\n- **Map boards are built in.** A `kind: \"map\"` session with\n `coordinateSpace.bounds` draws the geography through MapKit, using the same\n transform as `@remotedraw/geometry`. The built-in map is deliberately not\n pannable; supply your own through `background:` if it should be.\n- **Streaming boards are not.** A session with `senderIntegrationMode:\n \"streaming\"` or a receiver publishing `visualContext.enabled` cannot be drawn\n by this SDK \u2014 no WebRTC, no video, no `WKWebView`. It reports\n `.unsupportedSurface(_:)` carrying `hostedSenderURL`; present that in a\n `WKWebView` and streaming works today.\n\n`RemoteDrawOutcome` is a **closed** enum:\n\n| Case | Meaning | Do |\n| --- | --- | --- |\n| `.submitted(RemoteDrawReceipt)` | Drawing submitted. | Record it. Call `session.submit(metadata:)` yourself for the server's own ids \u2014 a submit from the SDK's controls reports a placeholder. |\n| `.left` | The person left; ink is on the board. | Nothing. |\n| `.expired` | The board finished or timed out. **Terminal.** | Create a new session. |\n| `.credentialLost(RemoteDrawError?)` | The token died; the board did not. **Recoverable.** | Mint a fresh `rd_send_` and present again. |\n| `.unsupportedSurface(RemoteDrawUnsupportedSurface)` | The board wants a renderer this SDK lacks. The cover **stays up** showing the reason. | Open `hostedSenderURL` in a `WKWebView`. |\n| `.failed(RemoteDrawError)` | Anything else. | Read `error.shouldReJoin` / `error.isRetriable`. |\n\n**There is no `.revoked`.** A revoked, unknown and malformed token all answer\n`invalid_sender_token` on purpose; only a genuine expiry is distinguishable, and\nthat travels in the error on `.credentialLost`.\n\nNote: `RemoteDrawKit` is a *different*, internal macOS-only package that some\nolder docs still name. Customers use `RemoteDrawSenderKit`.\n\n## Recipes, one per surface\n\nAll React snippets assume the session came from your backend and are wrapped in:\n\n```tsx\nconst receiver = createHttpReceiverClient(\"https://api.remotedraw.com\");\n\n<RemoteDrawProvider\n session={session.session}\n receiverToken={session.receiverToken}\n joinUrl={session.joinUrl}\n joinUrls={session.joinUrls}\n receiver={receiver}\n onError={(error) => reportToYourLogger(error)}\n>\n {/* the recipe */}\n</RemoteDrawProvider>\n```\n\n### Whiteboard / freeform sketch\n\n```ts\n// Backend\ntarget: { kind: \"whiteboard\", label: \"Session notes\" }\n```\n\n```tsx\n<PairingCode title=\"Scan to draw\" />\n<RemoteDrawReceiver />\n<RemoteDrawSessionControls title=\"Sender workflow\" />\n```\n\n### Photo / screenshot annotation (`image`)\n\nThe photo is **already on the receiver**. The phone never takes it.\n\n```ts\ntarget: {\n kind: \"image\",\n inputMapping: \"surface\", // the pad is the whole photo\n coordinateSpace: { width: 1600, height: 900 },\n metadata: { label: \"Inspection photo\", imageId: \"img_123\" },\n}\n```\n\n```tsx\n<div style={{ position: \"relative\" }}>\n <RemoteDrawReceiver\n coordinateAspectRatio={16 / 9}\n background={<img src={photoUrl} alt=\"\" style={{ width: \"100%\", height: \"100%\", objectFit: \"contain\" }} />}\n />\n <PairingCode placement=\"top-right\" size={112} />\n</div>\n```\n\n*Limit:* RemoteDraw stores no images. Your app owns the photo and the link\nbetween it and the drawings.\n\n### PDF page\n\n```ts\ntarget: { kind: \"pdf\", inputMapping: \"viewport\",\n metadata: { documentId: \"doc_9\", page: 3 } }\n```\n\n```tsx\n<div style={{ position: \"relative\" }}>\n <YourPdfPage page={3} />\n <RemoteDrawReceiver\n background=\"transparent\"\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\" }}\n />\n</div>\n```\n\n*Limit:* RemoteDraw renders no PDFs and knows nothing about pages. One session\nper page, or carry the page number in `target.metadata` and re-create.\n\n### Signature / bounded field\n\n```ts\ntarget: {\n kind: \"field\",\n inputMapping: \"surface\", // mandatory \u2014 the pad IS the field\n coordinateSpace: { width: 1600, height: 500 },\n metadata: { label: \"Customer signature\", fieldId: \"sig_1\" },\n}\n```\n\n```tsx\n<RemoteDrawConnect method=\"qr\" triggerLabel=\"Sign with your phone\" showTriggerLabel />\n<RemoteDrawReceiver coordinateAspectRatio={16 / 5} strokeWidth={7}\n style={{ border: \"1px solid #ddd8cf\", borderRadius: 12 }}>\n <line x1=\"140\" y1=\"760\" x2=\"3060\" y2=\"760\" stroke=\"#d8d8d8\" strokeWidth=\"4\" />\n</RemoteDrawReceiver>\n```\n\n*Limit:* this is markup transport, **not** e-signature compliance. No identity\nproofing, no intent-to-sign ceremony, no tamper-evident audit package, no\ncertificates. Say so if the user asks for a legal signature.\n\n### Map\n\nThe board's `coordinateSpace.bounds` is a **hard geographic fence, fixed for the\nlife of the session** \u2014 no route changes it. Size it larger than the camera you\nopen on. The phone pans *inside* it.\n\n```ts\nimport { padMapBounds } from \"@remotedraw/geometry\";\n\ntarget: {\n kind: \"map\",\n inputMapping: \"viewport\",\n coordinateSpace: {\n width: 1600, height: 1310, // the fence's Mercator aspect\n ...padMapBounds(currentCameraBounds, 1), // 3x the camera\n },\n}\n```\n\n```tsx\nconst [camera, setCamera] = useState(0);\nuseEffect(() => {\n const onMove = () => setCamera((n) => n + 1);\n map.on(\"move\", onMove);\n return () => map.off(\"move\", onMove);\n}, [map]);\nconst projectLngLat = useCallback(\n (lng: number, lat: number) => map.project([lng, lat]), // CSS px in the container\n [map, camera],\n);\n\n<div style={{ position: \"relative\" }}>\n <div ref={mapContainer} style={{ position: \"absolute\", inset: 0 }} />\n <RemoteDrawMapReceiver\n projectLngLat={projectLngLat}\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\" }}\n />\n</div>\n```\n\n*Limits:* `mapBounds={map.getBounds()}` is the no-callback alternative but is\nexact only for a north-up, unpitched camera. Omitting `bounds` at session\ncreation does not mean \"the customer's map\" \u2014 it means RemoteDraw's own default\nregion. `senderIntegrationMode` stays `\"native\"` on a map board: streaming\nrequires a `phoneProjection` that a native map sender never sends.\n\n### Screen / live view\n\n```ts\ntarget: { kind: \"screen\", inputMapping: \"viewport\" }\ncapabilities: [..., \"viewReceiverContext\"]\nvisualContext: { enabled: true }\n```\n\nThe receiver publishes with `useVisualContextPublisher`. Three consumers, in\norder of how little you write:\n\n1. **The hosted `/join` pad** \u2014 consumes the stream automatically. Zero code.\n2. **Your own web pad** \u2014 `RemoteDrawStreamView` with your pad as its\n `children`, plus a signaling client.\n3. **`RemoteDrawSenderKit`** \u2014 *cannot* consume it. It returns\n `.unsupportedSurface(_:)` carrying a `hostedSenderURL`; present that in a\n `WKWebView` and streaming works today.\n\nLive view is experimental: build so that a session whose stream never starts is\nstill a working session \u2014 the phone keeps drawing, it simply does not see the\nreceiver's pixels.\n\n### iOS sender \u2014 the complete Swift\n\n```swift\n// Package.swift / Xcode \u2192 Add Package\n.package(url: \"https://github.com/AxioSOzo/remotedraw-swift.git\", from: \"0.1.0\")\n.product(name: \"RemoteDrawSenderKit\", package: \"remotedraw-swift\")\n\nimport RemoteDrawSenderKit\n\n// Optional. RemoteDraw.shared installs production defaults on first use, so\n// this line exists only to OVERRIDE something.\n@main struct MyApp: App {\n init() { RemoteDraw.configure(.init(tokenProvider: mintSenderToken)) }\n var body: some Scene { WindowGroup { RootView() } }\n}\n\n// Wherever drawing starts. `token` is the rd_send_ string your backend minted\n// with POST /v1/sessions/direct-sender. A QR's rd_join_ works too, but spending\n// one revokes every other sender on that session.\nButton(\"Draw\") { drawing = true }\n .remoteDrawSurface(isPresented: $drawing, senderToken: token) { outcome in\n switch outcome {\n case .submitted(let receipt): record(receipt)\n case .left: dismissBanner()\n case .expired: refreshSession() // terminal\n case .credentialLost: refreshToken() // recoverable\n case .unsupportedSurface(let it): openHostedPad(it.hostedSenderURL)\n case .failed(let error): report(error) // do not swallow\n }\n }\n```\n\nReceiving the launch URL (the other half of the direct sender \u2014 nothing else\ndocuments it):\n\n```swift\n// Info.plist: CFBundleURLTypes \u2192 your scheme, e.g. \"yourapp\"\n.onOpenURL { url in\n guard url.scheme == \"yourapp\",\n let token = URLComponents(url: url, resolvingAgainstBaseURL: false)?\n .queryItems?.first(where: { $0.name == \"senderToken\" })?.value\n else { return }\n senderToken = token\n drawing = true\n}\n```\n\nRules:\n\n- The surface is **full screen**. Not a small pad, not a draggable sheet.\n- The user can always leave; `RemoteDrawExit` only decides whether leaving with\n unsubmitted ink asks first. The host always gets an outcome.\n- Do not write a `UIViewRepresentable` canvas, a draft loop, or an HTTP client.\n Go headless on `RemoteDrawSenderSession` + `RemoteDrawInkCanvas` +\n `RemoteDrawStrokeCapture` if you own the screen \u2014 never raw `URLSession`.\n- Customers do not ship a separate RemoteDraw app; their app *is* the sender.\n\n## How to suggest use cases\n\n**What the product is good at**, as a sentence to pattern-match against:\n\n> A person is at a screen. The thing they need to mark is already on that\n> screen. A mouse is the wrong instrument for the mark \u2014 because it is\n> handwriting, a circle around a defect, a diagram, a signature, or a gesture\n> over a map \u2014 and their phone is in their pocket.\n\n**Ask these four before proposing anything:**\n\n1. Which screen in your product already shows the thing to be marked, and what\n device is that screen on?\n2. Is the person in front of it holding a phone at the same time?\n3. What does the mark mean afterwards \u2014 saved to which record, shown where?\n4. Anyone who scans, or a signed-in user of your own app? (QR vs direct sender.)\n\n**Good vs bad, for a property-management SaaS:**\n\n- \u2705 Property manager reviews an inspection photo on the office desktop and\n circles the damage with their phone. *Two devices; content already on the\n receiver.*\n- \u2705 Tenant signs the handover report on the manager's laptop screen using their\n own phone as the pen. *This is the flow that replaces a stylus.*\n- \u2705 Planner marks a route on the dispatch map on the wall display.\n- \u2705 Support agent circles the broken control on a customer's shared screen.\n- \u274C Tenant photographs a leak on their phone and circles it. *One device,\n phone-supplied content. **Not RemoteDraw.*** Say so and propose PencilKit.\n- \u274C An in-app sketch pad in the mobile app. *One device.*\n- \u274C Field engineer marks up a PDF on their iPad in the van. *One device \u2014\n unless a second screen is genuinely present.*\n- \u274C \"Pair over Bluetooth when the phone is nearby.\" *Not implemented.*\n\n**The disqualifier:** if you cannot name two devices and say which one already\ndisplays the content, you do not have a use case yet \u2014 ask.\n\n## The flow \u2014 in this order\n\n1. **Explain before touching anything.** If the user is asking what RemoteDraw\n can do, answer from this file and the docs. Do not install, scaffold, or\n create sessions to answer a question.\n2. **Ask for approval before installing.** Name exactly what you want to add\n (`@remotedraw/cli`, `@remotedraw/react`, a Swift package, a dashboard\n project + key) and why, then wait. This includes `remotedraw init` without\n `--offline`, which provisions a billable project and key. Install with the\n project's own package manager \u2014 the scan below reports it, and a lockfile\n the project did not ask for is a mess a human has to clean up:\n `npm install -g @remotedraw/cli`, `pnpm add -g @remotedraw/cli`,\n `bun add -g @remotedraw/cli`, or \u2014 Yarn Berry has no global install \u2014\n `yarn dlx @remotedraw/cli`. Same for the SDKs: `npm install`, `pnpm add`,\n `yarn add`, or `bun add`.\n3. **Scan the codebase.** `remotedraw scan --format json` (or, before the CLI\n is installed, `npx @remotedraw/cli@latest scan --format json` \u2014 `pnpm dlx`,\n `yarn dlx`, or `bunx @remotedraw/cli` for those managers) reports the\n web/server/iOS projects, the project's package manager and its install/add\n commands, where an `rd_sk_` key may live, any RemoteDraw wiring already\n present, the integration options that fit, and the product questions to ask.\n Read it; verify its `evidence` where it matters.\n4. **Ask the product questions** (the four above, plus the scan's own). Do not\n invent answers; a one-page \"drawing lab\" is only right when the user says a\n demo is what they want.\n5. **Propose one plan, then build all of it.** Receiver, session creation,\n sender, and the exit/submit path \u2014 an integration is not done when ink\n appears once on a test page. Build beside existing features.\n6. **Verify by using it.** Run `remotedraw doctor --format json`, open a real\n session, draw from a phone (or `create-input --execute` + the hosted join\n URL), and confirm ink lands on the receiver. On iOS, run it on a device or\n simulator and look at the screen; a compiling canvas is not a working one.\n\nSession creation (`POST /v1/sessions`) needs the `rd_sk_` key and therefore runs\nonly where the scan found server-side code: a Convex action, a Next route\nhandler, an Express/Hono route, a serverless function. If the scan found none,\nask where the backend is. Never scaffold `createRemoteDrawSession.ts` into a\nVite/Next client tree \u2014 `--target web` in `remotedraw init` still writes it\nunder `src/`; move it, or scaffold into a scratch directory and copy only what\nbelongs.\n\n## CLI\n\n```sh\nremotedraw options --format json # the option catalog\nremotedraw scan --format json # read the codebase first (step 3)\nremotedraw init --non-interactive --offline --dry-run --format json \\\n --path apps/web --target web --sender remotedraw-ios --sdk react --preset sketch --package-manager npm\nremotedraw doctor --format json\nremotedraw create-input --preset sketch --json\nremotedraw create-input --execute # a real session + join URL, needs a key\nremotedraw agent --print-skill # this file, no install needed\n```\n\n`--sender own-ios` requires `--sdk swift`; other invalid combinations fail with\n`INVALID_COMBINATION`. By default `init`/`new` also create a dashboard project\nand a project-scoped development key in `.env.local` \u2014 that is the step that\nneeds approval (step 2); `--offline` writes files only. `--preset mapMarkup`\nemits an explicit `coordinateSpace.bounds`; replace the example region with the\ncustomer's. Never pass `--force` unless the user approved overwriting. Do not\ndrive the interactive wizard or scrape human-formatted output; every command has\n`--format json`.\n\n## Security and tenancy\n\n- `rd_sk_\u2026` keys: backend secrets only. Never in browser bundles, Swift, app\n bundles, screenshots, logs, or generated examples.\n- The account-level `rd_cli_\u2026` credential stays in the user config directory;\n never copy it into a project. `REMOTEDRAW_CLI_TOKEN` is for CI secrets only.\n- Public clients receive only `joinUrl`, `joinToken`, `receiverToken`, or\n `senderToken`, each scoped to one session. Production QR codes use the HTTPS\n `joinUrl`, not the custom scheme.\n- One key serves every customer of the product, so a session id is not a\n capability. Create sessions with `externalId: \"<product>:<tenant>\"`, and check\n it (`POST /v1/sessions/get`) before attaching a sender or ending a session on\n a tenant's behalf.\n\n## Gotchas the SDKs hide and hand-written code hits\n\n- `POST /v1/sessions` is billable and not idempotent. React StrictMode runs\n mount effects twice in development: guard with a ref, or create the session in\n a server action / loader. Store the `receiverToken` if the receiver outlives a\n page load \u2014 it is the only credential that reads a session's ink.\n- Timestamps are integer milliseconds. `occurredAt` and point `t` values are\n accepted with a fraction (floored) but a hand-written client should send\n integers.\n- Committed points come back in board space, remapped through\n `device.aspectRatio`; send the aspect ratio of the pad the finger touches.\n- `RemoteDrawReceiver` defaults `phones` and `pointers` to on, and renders\n immediately with no empty state. Pass `phones={false}` for a plain surface;\n drive your own empty state from `useReceiverData().senders`.\n- `PairingCode` hides the join URL text unless `showLink`.\n- `joinTokenExpiresAt` is earlier than the session's `expiresAt`: the QR dies\n first, the board stays live.\n- Wire `RemoteDrawProvider`'s `onError` (and `useVisualContextPublisher`'s).\n Without it a dead credential is silent and the board simply stops updating.\n- The hosted `/join` pad respects the joined capability list; a custom sender\n must too. It also has a **file-attach tool** available on any session granting\n `draw` or `point`, which cannot currently be turned off.\n\n## API contract\n\n- Backend: `POST /v1/sessions` (create), `/v1/sessions/get`, `/v1/sessions/end`,\n `/v1/sessions/direct-sender` (mint `rd_send_` for your own app),\n `/v1/sessions/join-token` (a fresh QR).\n- Receiver: `POST /v1/receiver/session`, `/drawings`, `/drafts`, `/senders`\n with the receiver token, or the realtime source in the SDKs.\n- Sender: `POST /v1/join` (spends a join token; revokes other senders), then\n `/v1/sender/draft` (latest-only preview, throttle to ~32 ms),\n `/v1/sender/commit` (one durable stroke per pointer-up with a stable\n `clientStrokeId`), `/v1/sender/submit`.\n- Capabilities: `draw`, `point`, `undo`, `clear`, `moveViewport`,\n `viewExisting`, `viewReceiverContext`. Omitting `capabilities` grants the\n first six. Reissued join tokens may narrow but never widen.\n\n## AI actions\n\nReach for AI when the product needs something _from_ the finished drawing: a\ngenerated image, a description, or structured data to branch on. Backend only\n(`aiActions:*` scopes on an `rd_sk_...` key). Never wire it to a commit, submit,\nor presence event \u2014 AI runs only on an explicit `POST /v1/ai-actions` call the\nuser asked for. Run it after the user is done; the route accepts `active` and\n`ended` sessions.\n\nMinimal request per outcome (`POST /v1/ai-actions`, plus optional\n`quality: \"fast\" | \"balanced\" | \"max\"`, default `balanced`):\n\n```jsonc\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\" } // image back in the response\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\", \"deliver\": [\"result\", \"board\"] } // and onto the board\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"Describe this drawing.\" } // text back\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"...\", \"text\": { \"schema\": { /* JSON Schema */ } } } // typed JSON\n```\n\nThe response is asynchronous: `create` returns `status: \"queued\"`. Poll\n`POST /v1/ai-actions/get` until `status` is `succeeded`, `failed`, or\n`canceled`, or use `createAiAction` + `waitForAiAction` on\n`createHttpRemoteDrawApiClient` from `@remotedraw/client` (re-exported by\n`@remotedraw/react`) \u2014 backend only, it holds the key. There is no completion\nwebhook. Render results with `AiImage` / `AiText`. See\nhttps://docs.remotedraw.com/docs/api#ai.\n\n## What does not exist yet\n\nVerified against source, 2026-08-30. Read this *before* designing, so you never\npromise any of it.\n\n- **No Bluetooth or Wi-Fi pairing.** The radios advertise; nothing scans or\n browses, on any platform. `PairingDevices` is presentational chrome.\n- **No QR into a customer's own app.** The Universal-Link association file has\n one hard-coded appID and the QR host is a RemoteDraw deployment setting. Use\n the direct sender.\n- **No native Android SDK.** Hosted `/join` in a mobile browser is the Android\n sender, and it is full-featured.\n- **No desktop or trackpad sender.** Unbuilt research.\n- **No streaming consumer in `RemoteDrawSenderKit`.** It reports\n `.unsupportedSurface(_:)` with a `hostedSenderURL` to open in a `WKWebView`.\n (A customer's own *web* pad can consume a stream \u2014 `RemoteDrawStreamView`.\n It is the native SDK that cannot.)\n- **No Svelte components.** A receiver store and a direct-sender store only.\n- **No web sender component.** Deliberate: hosted `/join` is the web sender.\n- **No API-key route that reads a session's ink.** Lose the `receiverToken` and\n the session is unreadable while still billable.\n- **No `clientSessionId` idempotency on `POST /v1/sessions`.**\n- **No completion webhook for AI actions** \u2014 poll `POST /v1/ai-actions/get`.\n- **No e-signature compliance.** No identity proofing, intent-to-sign ceremony,\n tamper-evident audit package, or certificate handling.\n- **No content rendering of any kind.** No PDF renderer, no image storage, no\n document pipeline, no auth, no billing UI. The host renders; RemoteDraw inks.\n\n## Verification\n\nAfter changes, verify against the customer's project \u2014 never assume RemoteDraw's\nown repo scripts exist here.\n\n```sh\nremotedraw doctor # config, SDK deps, REMOTEDRAW_* env\nremotedraw create-input --execute # open a real session, print the join URL\n```\n\nThen run whatever type check and test command the project already defines (for\nexample `npm run typecheck` and `npm test`). Do not invent script names, and do\nnot run `bun run test:api`, `bun run typecheck`, or `bun run ios:kit:test` \u2014\nthose are RemoteDraw's internal monorepo scripts and will not exist in a\ncustomer project.\n\n## Reference\n\n- API: `https://api.remotedraw.com` \u00B7 Docs: `https://docs.remotedraw.com/docs`\n (agent summary: `https://docs.remotedraw.com/llms.txt`) \u00B7 Keys:\n `https://dashboard.remotedraw.com/api/keys`\n- Packages: `@remotedraw/cli`, `@remotedraw/react`, `@remotedraw/svelte`,\n `@remotedraw/client`, `@remotedraw/protocol`, `@remotedraw/geometry`, and the\n SwiftPM package `https://github.com/AxioSOzo/remotedraw-swift.git` (product\n `RemoteDrawSenderKit`).";
|
|
2
2
|
//# sourceMappingURL=agent-skill.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-skill.d.ts","sourceRoot":"","sources":["../../src/generated/agent-skill.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"agent-skill.d.ts","sourceRoot":"","sources":["../../src/generated/agent-skill.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,oBAAoB,yolDAA06jD,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// GENERATED FILE — do not edit.
|
|
2
2
|
// Source: docs/agents/remotedraw/SKILL.md
|
|
3
3
|
// Regenerate: bun run --cwd packages/cli generate:skill
|
|
4
|
-
export const AGENT_SKILL_MARKDOWN = "---\nname: remotedraw\ndescription: Add RemoteDraw phone input to customer apps with the RemoteDraw CLI, public API, React SDK, raw HTTP, or customer-owned iOS sender flow.\n---\n\n# RemoteDraw Agent Skill\n\nUse this skill when a user asks to create, initialize, debug, or review a RemoteDraw integration.\n\n## Decision Flow\n\n1. Identify the receiver surface: web app, desktop app, iOS app, or headless/backend workflow.\n2. Identify the sender surface: RemoteDraw iOS app, embedded web sender, customer-owned iOS sender, or raw/headless sender.\n3. Pick the SDK path:\n - React SDK: web receiver and optional embedded web sender.\n - Plain JavaScript/raw HTTP: non-React web, desktop, backend, or custom clients.\n - Swift: customer-owned iOS sender apps.\n4. Pick a supported starter: `sketch` for a free-form receiver surface, or\n `screenMarkup` for annotations over a shared screen — both render through the\n `RemoteDrawReceiver` foundation, with the surrounding UI owned by the\n integrating app. Configure the target kind and descriptor directly for\n photos, PDFs, maps, bounded fields, and other custom surfaces. A bounded\n field is a target you describe yourself — `inputMapping: \"surface\"` plus a\n `coordinateSpace` — not a preset RemoteDraw ships.\n\n## Endpoint, Key, and Packages\n\nRemoteDraw is hosted. There is nothing for the customer to run or self-host.\n\n- API base URL: `https://api.remotedraw.com`\n- Public docs: `https://docs.remotedraw.com/docs`\n- API keys are created in the console at\n `https://dashboard.remotedraw.com/api/keys` and belong in `.env.local` as\n `REMOTEDRAW_API_KEY`, alongside\n `REMOTEDRAW_API_BASE_URL=https://api.remotedraw.com`.\n\nPublished npm packages — install only what the chosen path needs:\n\n| Package | Install | Use it for |\n| --- | --- | --- |\n| `@remotedraw/cli` | `npm install -g @remotedraw/cli` (or `npx @remotedraw/cli@latest`) | Scaffolding, doctor, and test sessions. |\n| `@remotedraw/react` | `npm install @remotedraw/react` | React receiver, pairing, and headless sender. Peers on `react`/`react-dom` >= 18. |\n| `@remotedraw/svelte` | `npm install @remotedraw/svelte` | Svelte receiver store over the framework-free client. |\n| `@remotedraw/client` | `npm install @remotedraw/client` | Framework-free receiver/sender/API clients for any JS runtime. |\n| `@remotedraw/protocol` | `npm install @remotedraw/protocol` | Shared schemas, types, and limits for raw-HTTP integrations. |\n| `@remotedraw/geometry` | `npm install @remotedraw/geometry` | Normalized stroke geometry, shape assist, hit testing, export. |\n\n`@remotedraw/react` already depends on `client`, `protocol`, and `geometry`, so\ndo not add those separately for a React app.\n\nFor customer-owned iOS senders, prefer the published SwiftPM package — it is the\nsame implementation the first-party RemoteDraw app runs:\n\n```swift\n.package(url: \"https://github.com/AxioSOzo/remotedraw-swift.git\", from: \"0.1.0\")\n.product(name: \"RemoteDrawSenderKit\", package: \"remotedraw-swift\")\n```\n\n`package:` is the repository basename, not the module name. `remotedraw init\n--sdk swift` remains available and writes one self-contained\n`RemoteDrawIntegration.swift` against the public HTTP routes; use it only when\nadding a SwiftPM dependency is not an option.\n\n## CLI First\n\nRead the machine-readable option catalog before choosing a plan:\n\n```sh\nremotedraw options --format json\n```\n\nInitialize a project with the closest supported path:\n\n```sh\nremotedraw init --target web --sender remotedraw-ios --sdk react --preset sketch\nremotedraw init --target web --sender embedded-web --sdk react --preset sketch\nremotedraw init --target desktop --sender remotedraw-ios --sdk js --preset sketch\nremotedraw init --target ios --sender own-ios --sdk swift --preset sketch\n```\n\nBy default, `new` and `init` create the dashboard project and a project-scoped\ndevelopment API key, then write `REMOTEDRAW_API_BASE_URL`,\n`REMOTEDRAW_PROJECT_ID`, and `REMOTEDRAW_API_KEY` to a gitignored `.env.local`.\nUse `--offline` only when cloud setup is intentionally out of scope.\n\n`--sender own-ios` requires `--sdk swift`; every other combination of\n`--target`, `--sender`, and `--sdk` is accepted. Invalid combinations fail with\n`INVALID_COMBINATION` rather than guessing.\n\nAgents must use explicit non-interactive dry runs before changing a project:\n\n```sh\nremotedraw init --non-interactive --offline --dry-run --format json --target web --sender remotedraw-ios --sdk react --preset sketch --package-manager npm\n# Inspect plan, files, defaultsApplied, and warnings before applying.\nremotedraw init --non-interactive --offline --format json --target web --sender remotedraw-ios --sdk react --preset sketch --package-manager npm\nremotedraw doctor --format json\nremotedraw create-input --preset sketch --json\n```\n\nDo not use the interactive wizard, synthesize arrow-key input, or scrape\nhuman-formatted output. If receiver, sender, or preset intent is ambiguous, ask\nthe user instead of guessing. Never pass `--force` unless overwrite scope was\nexplicitly approved.\n\n## Security Rules\n\n- Keep `rd_sk_...` API keys in trusted backend secrets only.\n- Keep the account-level `rd_cli_...` credential in the user config directory; never copy it into a project. Use `REMOTEDRAW_CLI_TOKEN` only as an explicitly managed CI secret.\n- Never place API keys in browser bundles, mobile clients, screenshots, logs, or generated examples.\n- Public clients should receive only `joinUrl`, `joinToken`, `receiverToken`, or `senderToken` values scoped to the session.\n- Production QR codes should use HTTPS `joinUrl` values. Do not make the custom scheme the primary QR target.\n\n## API Contract\n\n- Backend creates sessions with `POST /v1/sessions`.\n- Receiver clients read `POST /v1/receiver/session`, `/drawings`, `/drafts`, and `/senders` with a receiver token.\n- Sender clients join with `POST /v1/join`, stream mutable drafts to `/v1/sender/draft`, commit durable strokes to `/v1/sender/commit`, and finish with `/v1/sender/submit`.\n- Custom senders should throttle draft updates, coalesce to the latest pending preview, and commit one durable stroke on pointer-up with a stable `clientStrokeId`.\n\n## AI Actions\n\nReach for AI when the product needs something _from_ the finished drawing:\na generated image, a description, or structured data to branch on. Backend only\n(`aiActions:*` scopes on an `rd_sk_...` key). Never wire it to a commit,\nsubmit, or presence event — AI runs only on an explicit `POST /v1/ai-actions`\ncall the user asked for. Run it after the user is done; the route accepts\n`active` and `ended` sessions.\n\nMinimal request per outcome (`POST /v1/ai-actions`, plus optional\n`quality: \"fast\" | \"balanced\" | \"max\"`, default `balanced`):\n\n```jsonc\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\" } // image back in the response\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\", \"deliver\": [\"result\", \"board\"] } // and onto the board\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"Describe this drawing.\" } // text back\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"...\", \"text\": { \"schema\": { /* JSON Schema */ } } } // typed JSON\n```\n\nThe response is asynchronous: `create` returns `status: \"queued\"`. Poll\n`POST /v1/ai-actions/get` until `status` is `succeeded`, `failed`, or\n`canceled`, or use the helper on the API client\n(`createHttpRemoteDrawApiClient` from `@remotedraw/client`, re-exported by\n`@remotedraw/react`). This client holds the `rd_sk_` key, so it only ever runs\non the backend:\n\n```ts\nconst action = await client.createAiAction({\n sessionId,\n request: \"image\",\n prompt,\n});\nconst finished = await client.waitForAiAction({ aiActionId: action.id });\n// finished.result: generatedImageUrl | generatedText | generatedData | boardDrawingIds\n```\n\nThere is no completion webhook. Results are RemoteDraw-hosted asset URLs, not\nprovider URLs. See https://docs.remotedraw.com/docs/api#ai for the full\nrequest, credit, and legacy-mapping tables.\n\n## Verification\n\nAfter changes, verify against the customer's project — never assume RemoteDraw's\nown repo scripts exist here.\n\n```sh\nremotedraw doctor # config, SDK deps, REMOTEDRAW_* env\nremotedraw create-input --execute # open a real session, print the join URL\n```\n\nThen run whatever type check and test command the project already defines (for\nexample `npm run typecheck` and `npm test`). Do not invent script names, and do\nnot run `bun run test:api`, `bun run typecheck`, or `bun run ios:kit:test` —\nthose are RemoteDraw's internal monorepo scripts and will not exist in a\ncustomer project.";
|
|
4
|
+
export const AGENT_SKILL_MARKDOWN = "---\nname: remotedraw\ndescription: Integrate RemoteDraw phone drawing into a customer's product — a phone becomes the pen for a screen the customer already owns. Covers the two-device model, which flows are valid, the ready-made components (React/Svelte/JS receiver, hosted web sender, RemoteDrawSenderKit iOS sender), per-surface recipes, and how to judge whether a proposed use case fits at all.\n---\n\n# RemoteDraw Agent Skill\n\nUse this skill when a user asks what RemoteDraw is, whether it fits their app,\nor to create, debug, or review a RemoteDraw integration.\n\nRead the whole file before proposing a design. The first two sections decide\nwhether the use case is possible; everything after decides how it is built.\n\n## The model: two devices, always\n\nRemoteDraw is not a drawing library. It is a wire between **two devices**.\n\n- **The receiver is the paper.** A screen someone is looking at — a laptop, a\n desktop, a large display, a kiosk, a tablet on a desk — showing *your*\n product. Your app renders whatever is being drawn on: the map, the photo, the\n PDF page, the form, the whiteboard. RemoteDraw renders none of that content.\n It paints ink on top of it.\n- **The sender is the pen.** A phone. It supplies a hand, pressure, tilt and a\n stroke. It does **not** supply the content: the iOS SDK opens no camera and no\n file picker (it needs no `Info.plist` entries at all). On a bounded surface\n the phone's pad *is* the surface; on a large one the phone is a viewport\n moving over it.\n- **RemoteDraw is the wire.** A hosted session carries strokes from the pen to\n the paper in real time and stores them. Nothing runs on the customer's\n infrastructure except the session-creation call.\n\nThe one question that decides whether RemoteDraw fits:\n\n> **What is being drawn on, and which screen is it already displayed on?**\n\nIf the answer is \"a screen the user is looking at, and they wish they could\ndraw on it with their hand\" — that is RemoteDraw. If the answer is \"the phone's\nown screen\", it is not: a phone drawing on its own content and uploading the\nresult is a camera-and-canvas feature you build with PencilKit or a `<canvas>`,\nand RemoteDraw would only add a round trip.\n\nThree consequences, because they are the mistakes integrators actually make:\n\n1. **The phone never supplies the picture.** \"The tenant photographs the leak\n and circles it\" is *not* a RemoteDraw flow — there is no second screen. The\n RemoteDraw version of that job: the photo is already open in your web app on\n the office desktop, and the person at that desk circles the leak with their\n phone instead of a mouse.\n2. **The receiver already exists.** RemoteDraw goes onto a screen your product\n already has. It does not get its own page unless the user asks for a demo.\n3. **Ink is coordinates, not pixels.** Strokes arrive in normalized board space\n (`0..1`, remapped through the sender's `device.aspectRatio`). Your app\n decides what board space *means* — a pixel, a page, a field, a coordinate on\n Earth. RemoteDraw never sees your content.\n\n## Which flows are valid\n\nRead the row for the **sender** (who holds the phone) and the column for the\n**receiver** (the screen showing the content).\n\n| Sender (the pen) | Receiver (the paper) | Valid? | Notes |\n| --- | --- | --- | --- |\n| iPhone — RemoteDraw app, your app via `RemoteDrawSenderKit`, or hosted `/join` in Safari | Desktop / laptop browser | **Yes — the canonical flow** | Everything below is written for it. |\n| iPhone (any of the three) | Large display, TV, projector, kiosk browser | **Yes** | Size `target.coordinateSpace` to the display. |\n| iPhone (any of the three) | Desktop app — Electron, macOS, Windows — via `@remotedraw/client` or raw HTTP | **Yes** | No React needed; poll `/v1/receiver/*` or use the realtime source. |\n| Android phone — hosted `/join` in Chrome | Any of the above | **Yes** | There is no native Android SDK. The hosted join page *is* the Android sender and it is full-featured. |\n| iPhone / Android | iPad or tablet browser, as a **second** device someone else is looking at | **Yes** | Two devices, two people. |\n| Headless script, test, or agent — raw `POST /v1/join` → `/v1/sender/draft` → `/v1/sender/commit` | Any receiver | **Yes, for verification only** | Never ship a hand-rolled sender to users. |\n| Any phone | **The same phone** — one device shows the content and draws on it | **No** | There is no second screen. Use PencilKit / `<canvas>`. RemoteDraw adds a network hop and nothing else. |\n| A phone that must first **capture** the content — photograph or scan it | *(anything)* | **No** | The iOS sender opens no camera and needs no `Info.plist` entries. The content must already be on the receiver. |\n| Desktop mouse or trackpad as the pen | *(anything)* | **No — does not exist** | There is no desktop sender. The macOS trackpad sender is unbuilt research. Do not promise it. |\n| Phone → phone, two different people, two different devices | Phone browser as receiver | *Technically yes, rarely right* | A phone browser is a browser. But if both people hold phones, ask why the drawing is not simply in one app. |\n\n**The three nevers of the model.**\n\n1. **Never same-device.** If sender and receiver would be one phone, stop and\n say so. Propose the non-RemoteDraw alternative.\n2. **Never make the phone the source of content.** The receiver supplies what\n is drawn on. (One honest exception: the hosted `/join` pad has a file-attach\n tool that can put a file on the board. It is a hosted-sender capability, not\n a way to make a same-device flow valid, and it cannot currently be disabled.)\n3. **Never promise a sender RemoteDraw does not ship.** iOS (native SDK) and\n any mobile browser (hosted `/join`) are the senders. Nothing else exists.\n\n## Never do these\n\n- **Never write a custom canvas.** Not a `UIViewRepresentable` drawing view, not\n a `<canvas>` sender pad, not a hand-rolled SVG receiver, not a raw\n `URLSession`/`fetch` draft loop. Cadence, point budgets, the packed-point\n codec, sequence healing, token refresh and presence are protocol, and the SDKs\n implement them. Go headless on the SDK's own primitives if you must.\n- **Never put the iOS surface in a small pad or a draggable sheet.** It is\n full screen (`.remoteDrawSurface` / `RemoteDrawTakeover`). A sheet is\n acceptable only if `RemoteDrawSurface` fills it edge to edge and the sheet\n cannot be dragged mid-stroke. There is no small-canvas option.\n- **Never create a session on mount or on page load.** `POST /v1/sessions` is\n billable and not idempotent, and React StrictMode fires mount effects twice.\n Create it on the server (route handler, loader, server action) or on explicit\n user intent, and guard with a ref if it must be a client effect.\n- **Never let an `rd_sk_…` key reach a client.** Not browser bundles, not Swift,\n not app bundles, screenshots, logs, or generated examples. The phone never\n calls `/v1/sessions/direct-sender`; your backend does.\n- **Never poll by hand when a component or store exists.** `RemoteDrawProvider`\n / `createReceiverStore` already do one-in-flight polling plus an optional\n realtime push source.\n- **Never promise Bluetooth or Wi-Fi pairing.** The API advertises\n `bluetooth` and `localNetwork` as pairing methods and the iOS/Android apps do\n *advertise* on those radios, but **nothing scans, browses, or connects\n anywhere in the product**. They are not implemented. Do not present them as\n options; do not build UI around them. QR and direct sender are the real ones.\n- **Never promise a native Android SDK, a desktop sender, per-tenant Universal\n Links, e-signature compliance, or streaming inside `RemoteDrawSenderKit`.**\n See \"What does not exist yet\".\n- **Never delete or replace a host-app feature on your own initiative.** Build\n beside it.\n\n## The decision tree\n\n**1. What is being drawn on? → `target.kind` + `inputMapping`.**\n\n`target.kind` accepts `whiteboard`, `paper`, `canvas`, `svg`, `map`, `tldraw`,\n`field`, `image`, `pdf`, `screen`, `custom`. It selects the background the\nreceiver defaults to, the tool policy, and deposit defaults.\n`target.inputMapping` is `surface` (the phone's whole pad *is* the target — use\nfor bounded targets like a signature field) or `viewport` (the phone is a\nmovable window over a larger board — the default when omitted).\n\n| Kind | Host renders | `inputMapping` | Sender | Status |\n| --- | --- | --- | --- | --- |\n| `whiteboard` / `paper` | nothing — RemoteDraw's own ground | `viewport` (or `surface`) | hosted `/join` or SenderKit | shipped |\n| `image` (photo, screenshot) | the `<img>`, as `background` | `surface` (whole photo) or `viewport` (zoomable) | either | shipped |\n| `pdf` | your PDF renderer, one page at a time, as `background` or behind a `transparent` receiver | `viewport` | either | shipped; RemoteDraw renders no PDFs |\n| `field` (signature, initials) | the form, with a baseline as SVG `children` | **`surface`** — mandatory | either | shipped; **not** an e-signature product |\n| `screen` | a screenshot, or a live stream you publish | `viewport` | either | shipped; live view is experimental |\n| `map` | your map (Mapbox / MapLibre / Leaflet / Google) | `viewport` | hosted `/join` or SenderKit | shipped — use `RemoteDrawMapReceiver` |\n| `custom` | anything else you own | `viewport` | hosted `/join` (streaming) or SenderKit (ink only) | shipped |\n\n**2. Which stack renders the receiver? → the composition.**\n\n| Host | Use | Not |\n| --- | --- | --- |\n| React / Next / Remix | `@remotedraw/react`: `RemoteDrawProvider` + `RemoteDrawReceiver` (or `RemoteDrawMapReceiver`) + `PairingCode`/`RemoteDrawConnect`/`RemoteDrawLaunchButton` + `RemoteDrawSessionControls` | A hand-rolled SVG or polling loop |\n| Svelte / SvelteKit | `@remotedraw/svelte`: `createRemoteDrawReceiver` store + `createDirectSender` store. **No components ship** — you write all the markup, including ink rendering | Assuming React's components exist here |\n| Anything else with JS (Electron, Vue, vanilla) | `@remotedraw/client`: `createHttpReceiverClient`, `createReceiverStore`, `createRealtimeReceiverSource` | — |\n| No JS at all | Raw HTTP `POST /v1/receiver/*` with the receiver token | — |\n\n**3. Who holds the phone? → the sender path.**\n\n| Situation | Path |\n| --- | --- |\n| Anyone who can scan; no phone app in the product | **Hosted `/join`.** Render `joinUrl` as a QR with `PairingCode`. Zero sender code. The RemoteDraw iOS app opens the same HTTPS link through Universal Links; every other phone gets the web pad. |\n| The product has a first-party iOS app the user already installed *and* it is the RemoteDraw app | Same QR. Universal Links open it. |\n| The product has **its own** iOS app and the user is signed in | **Direct sender.** Backend calls `POST /v1/sessions/direct-sender` with `launchUrlTemplate: \"yourapp://draw?senderToken={senderToken}\"`; the browser shows `RemoteDrawLaunchButton`; the app receives the URL in `onOpenURL` and presents `.remoteDrawSurface(isPresented:senderToken:)`. Keep the QR as the fallback — the button reveals one automatically. |\n| A QR that opens the **customer's own** app | **Not possible.** The Universal-Link association file has one hard-coded appID and the QR host is a RemoteDraw deployment setting. Use the direct sender instead. |\n| Headless / tests | `POST /v1/join` → `/v1/sender/draft` → `/v1/sender/commit` |\n\n## Component catalogue\n\nEverything below is a real export, read from source. Anything not listed does\nnot exist. Grouped by decision: the board → what is behind it → pairing →\nsession state → AI → clients → iOS.\n\n### `@remotedraw/react`\n\n**`RemoteDrawProvider`** — receiver session state, polling, and actions.\nRequires a `receiver` client to do anything.\nProps: `children`, `session`/`sessionId`/`joinUrl`/`joinUrls`/`joinTokenUse`/`pairing`/`receiverToken`,\n`receiver` (a `ReceiverClient` — **without it the store is inert and silent**),\n`createSession`, `createSessionRequest` (default `{ target: { kind: \"custom\" } }`),\n`autoCreate` (default: true when `createSession` and no `session`),\n`pollIntervalMs` (`1000`), `draftPollIntervalMs` (`250`), `source` (`null`),\n`fallbackToPolling` (`true`), `initialDrawings`/`initialDrafts`/`initialSenders` (`[]`),\n`onError` (`(error: Error) => void` — **wire it; it is the only signal for a dead credential**).\nContext: `session, sessionId, joinUrl, joinUrls, joinTokenUse, pairing, receiverToken, credentials, drawings, drafts, senders, loading, creating, error, transport, create, refetch, ingest, undo, clear`.\n*Limits:* `error` is a plain `Error`; narrow with `instanceof RemoteDrawHttpError`\nfor `.status`/`.code`/`.shouldReJoin`.\n\n**`RemoteDrawReceiver`** — the receiver foundation. Paints committed ink, live\ndrafts, sender pointers and connected phones over a configurable background, and\nprescribes nothing around it.\nProps: `drawings`/`drafts`/`senders` (fall back to the provider), `background`\n(default: the session target's surface inside a provider, else `\"whiteboard\"`;\naccepts a surface name, `\"transparent\"`, any CSS background string, or any\nReactNode), `pointers` (`true`), `phones` (`true`), `pointerColor` (`#1f7a8c`),\n`strokeColor` (`#151512`), `draftColor` (`#1f7a8c`), `strokeWidth` (`6`),\n`coordinateAspectRatio` (default: the session's `coordinateSpace`, else `1`;\nalso sets CSS `aspect-ratio`), `preserveAspectRatio` (`\"none\"`),\n**`projectPoint`** (`(point) => {x,y} | null` in **CSS pixels from the\nreceiver's top-left** — the seam for a camera; return `null` to drop a point),\n**`space`** (the same seam in the receiver's own viewBox coordinates; `projectPoint`\nwins when both are given), `animate` (`true`), `children` (SVG overlay in\n`0..surfaceWidth × 0..1000`), `className`/`style`/`svgProps`/`aria-label`.\n*Defaults that surprise:* it renders immediately with no empty state; `phones`\nand `pointers` are on.\n*Limits:* without `projectPoint`/`space` it stretches board space across its own\nelement — correct for a fixed board, silently wrong over a live map. Use\n`RemoteDrawMapReceiver` there.\n*Never:* stack it on a pannable map without a projection.\n\n**`RemoteDrawMapReceiver`** — `RemoteDrawReceiver` wired to a map the host owns.\nRenders a transparent ground (your map is the ground) plus the\nboard→geography→pixel projection. Takes every `RemoteDrawReceiver` prop except\n`projectPoint`/`space`, plus:\n`bounds` (the board's `target.coordinateSpace.bounds`; defaults to the session's\nown inside a provider — usually pass nothing), `mapBounds` (the map's current\nvisible bounds; **exact only for a north-up, unpitched camera**),\n`projectLngLat` (`(lng, lat) => {x,y} | null` — normally\n`map.project([lng, lat])`; exact under rotation and pitch, and wins over\n`mapBounds`).\n*Limits:* with neither `projectLngLat` nor `mapBounds` it renders unprojected\nand warns once in development. Leave `preserveAspectRatio` at `\"none\"`.\n*Never:* recompute the projection identity on every render — memoize it and bump\nit on the map's `move` event.\n\n**`RemoteDrawPhoneProjection`** — one connected phone drawn in place, as an SVG\n`<g>` for a host with its own `<svg>`. `layout` (required, from\n`phoneProjectionLayouts(senders)`), `space` (default 1000×1000), `color`,\n`model` (`\"auto\"`), `className`. `RemoteDrawReceiver` already renders these.\n\n**`PairingCode`** — the pairing component: the scannable code with live status,\nhover-to-copy, and subtle branding.\nProps: `joinUrl` (default: the provider's, falling back to `joinUrls.web`),\n`size` (`200`, or `\"fill\"`), `direction` (`\"paper\"`, one of 14 — TEMPORARY),\n`treatment` (`\"fluid\"`, one of 11 — TEMPORARY), `accentColor` (`#1f7a8c`),\n`inset`, `tile` (`true`), `logo` (the RemoteDraw mark; `false` for none, a\nstring for a URL), `logoPlacement` (`\"plate\"`), `title`/`description`,\n`showStatus` (`\"auto\"`), `joinMode` (the session's `joinTokenUse`),\n`showJoinMode`/`showLink` (`false`), `copyOnHover` (`true`), `onCopy`,\n`onConnected`/`onConnectedDismiss`, `labels`, `alt`, `placement` (`\"inline\"` +\nfour corners), `position`/`offset`/`zIndex` (`\"absolute\"`/`12`/`20`),\n`className`/`style`/`codeClassName`/`codeStyle`.\n*There is no `card`, `variant`, `showBrand`, `brand`, or `status` prop.* The\ncard is always drawn (`tile`, on by default) and extends to hold `title` /\n`description`; status is **inferred** (connected → error → expired → ready →\nidle) and cannot be passed.\n*Limits:* always encodes the HTTPS `joinUrl`. The animated optical field that\n`variant=\"aurora\"` once selected is now the separate EXPERIMENTAL\n`AuroraPairingField`, decodable only by the RemoteDraw app's own scanner.\n\n**`PairingDevices`** — the list UI for pairing methods that resolve to a device\n(`bluetooth | localNetwork | accountPresence | direct`). `method` (required),\n`devices` (`[]`), `onSelectDevice`, `joinUrl`, `showCodeFallback` (`true`),\n`status`, `size` (`168`), `emptyLabel`, `actions`, `accentColor`,\n`autoHideOnConnected` (`true`), `connectedHideDelayMs` (`1150`), `direction`,\n`logo`, `onCopy`, `className`/`style`.\n*Limits:* **purely presentational — it discovers nothing.** You supply `devices`\nand `onSelectDevice` from your own backend. There is no Bluetooth, no Bonjour,\nand no customer-reachable account-presence route.\n*Never:* present it as \"nearby device pairing\" to a customer. It is chrome.\n\n**`RemoteDrawConnect`** — a compact trigger button that opens the code or the\ndevice list in a popover, for pairing exactly at the field, margin, or toolbar\nthat needs it. Everything from `PairingCodeProps` except placement/position/\noffset/zIndex/className/style/size/tile, plus `size` (`168`), `open`,\n`defaultOpen` (`false`), `onOpenChange`, `placement` (`\"bottom\"`), `method`,\n`devices`, `onSelectDevice`, `trigger`, `triggerLabel` (`\"Pair phone\"`),\n`showTriggerLabel` (`false`), `triggerClassName`/`triggerStyle`/`triggerDisabled`,\n`openOnHover` (`true`), `panel*`/`popover*` class and style.\n*Use it instead of hand-rolling a \"connect phone\" button.*\n\n**`RemoteDrawLaunchButton`** — \"open on my phone\" for a user whose **own** app is\nthe sender. Mints a scoped sender through your backend, opens the deep link, and\nreveals a QR when the app never comes back.\nProps: `connect` (required — your own backend endpoint; may return the raw\n`connectSender` response, a create-session response carrying `senderConnection`,\nor just `{ launchUrl }`; resolving `null` means \"no direct sender for this user\"),\n`handoffTimeoutMs` (`DIRECT_SENDER_HANDOFF_TIMEOUT_MS` = `12_000`),\n`showQrFallback` (`true`), `fallback`, `pairingProps`, `labels`, `accentColor`\n(`#1f7a8c`), `disabled` (`false`), `autoLaunch` (`true`), `openUrl` (default\nassigns `window.location.href`), `onError`, `onStatusChange` (transitions only),\n`children` (node or `(state) => node`), `className`/`style`/`buttonClassName`/\n`buttonStyle`/`aria-label`.\n*Why it is a component and not an `onClick`:* a custom scheme nothing has\nregistered fails **silently** — no error, no navigation, no event. The timeout\nwith no sender on the board is the only detector.\n\n**`useDirectSender(options)`** — the hook the button is a thin default over.\nOptions: `connect` (required), `autoLaunch`, `openUrl`, `onError`,\n`onStatusChange`. Returns `{ status, launchUrl, senderToken, senderId,\nconnection, error, connect(), launch(), reset() }`.\n`status`: `idle | connecting | ready | drawing | submitted | expired | error`.\n`error` is a `RemoteDrawLaunchError` (an `Error`) with\n`kind: \"connect-failed\" | \"no-launch-url\" | \"launch-blocked\"` and the original\nrejection on `cause`.\n*Limits:* phases after `ready` are read from `RemoteDrawProvider`; outside one\nit can never advance past `ready`.\n\n**`RemoteDrawSessionControls`** — the one session-state surface: a collected\nstatus bar (or card, via `title`) telling the session's story — waiting for a\nphone, connected, drawing, submitted, with the sender's device name — plus\nundo/clear. `actions` (`[\"undo\",\"clear\"]`), `confirmClear` (`true`), `title`,\n`submission`, `labels`, `undoLabel`/`clearLabel`/`confirmClearLabel`,\n`metadataKeys`/`metadataLabels`/`formatMetadataValue` (keys render humanized,\nnever raw), `className`/`style`.\n*Limits:* every failure renders as one string, \"Connection problem\".\n\n**`AiImage` / `AiText`** — render a finished `AiAction`. `AiImage`: `action`,\n`direction` (`\"plate\"`, 6 options), `actions` (`\"hover\"`, 4),\n`standardActions` (`[\"download\",\"copy\"]`, plus `\"open\"`), `customActions`,\n`theme` (`\"auto\"`), `aspectRatio`, `radius` (`14`), `fit` (`\"cover\"`),\n`fileName`, `labels`, `onRetry`, `placeholder`, `imageAlt`, `className`/`style`.\n`AiText`: `action`, `direction` (`\"note\"`, 5), `actions` (`\"bar\"`),\n`standardActions`, `customActions`, `theme`, `radius`, `maxWidth` (`\"60ch\"`),\n`labels`, `onRetry`, `placeholder`, `className`/`style`.\n*Limits:* neither ever shows the model, tier, latency, credit cost, or the\nprovider's error string. A schema run renders nothing — `result.generatedData`\nis for your code.\n\n**`useVisualContextPublisher`** (experimental) — the **publish** half of live\nview: sends the receiver's pixels to the phone drawing on it. Has its own\n`onError`.\n\n**`RemoteDrawStreamView`** — the **consume** half, for a sender pad you host\nyourself: the receiver's stream as a ground, your pad as its `children`.\nProps: `senderToken`, `signaling` (a `VisualContextSignalingClient` — build it\nwith `createRealtimeVisualContextSignalingClient` for push, or\n`createHttpVisualContextSignalingClient` for the polled `/v1/.../visual-context/*`\nroutes), `enabled` (gate it on `viewReceiverContext` + `visualContext.enabled` +\na reported phone projection), `iceServers` (from the session's\n`visualContext.iceServers` — without them a phone on cellular connects to\nnothing), `pollIntervalMs`, `onStatus`, `onError`, plus `fit` (`\"contain\"`),\n`fadeMs`, `posterStyle`, `className`/`style`/`aria-label`, `children`.\n`useRemoteDrawStream(options)` is the same thing headless, returning\n`{ mediaStream, status, streamStatus, stream, error, markStreamLive }` with\n`status` one of `idle | connecting | streaming | unsupported | failed | closed`.\n*Limits:* `children` are deliberately **not** gated on the stream — a pad that\nonly appears once pixels arrive never appears on the networks where WebRTC\ncannot connect, and drawing must keep working there.\n\n**`VisualContextVideoLayer`** — the raw `<video>` for a `MediaStream` you\nproduce yourself. `mediaStream`, `status`, `fit` (`\"contain\"`), `fadeMs`\n(`VISUAL_CONTEXT_FADE_MS` = `220`), `posterStyle`, `onLiveChange`,\n`className`/`style`/`aria-label`. Hand over on `onLiveChange(true)`, not on\nhaving a stream. `RemoteDrawStreamView` wires this for you.\n\n**Hooks:** `useRemoteDraw` (throws outside the provider),\n`useRemoteDrawSession`, `useReceiverData`, `usePairingUrl`,\n`useRemoteDrawPointers`, `useReceiverStrokes`.\n\n**Low-level / rarely right:** `InkCanvas` (WebGL2 ink substrate — the receiver\ndrives it), `RemoteDrawMark`, `AuroraPairingField` (EXPERIMENTAL),\n`FreehandFilmGroup`/`freehandStrokePaths`, the element-selection helpers.\n\n**Deliberately not exported:** a web sender component. Hosted `/join` is the web\nsender. Custom in-page pads are built headless on `createHttpSenderClient`.\n`@remotedraw/react/next` is a separate, unfinished v2 entry point — do not mix\nit into a normal integration.\n\n### `@remotedraw/svelte`\n\n`createRemoteDrawReceiver(options)` → `{ subscribe, create, refetch, ingest,\nundo, clear, setSession, configure, start, stop }`.\n`createDirectSender({ connect, receiver, autoLaunch, openUrl, onError })` →\n`{ subscribe, connect, launch, reset, stop }`, the same state machine React's\n`useDirectSender` binds. Plus `export * from \"@remotedraw/client\"`.\n**No components ship.** A Svelte integrator writes the pairing UI, the ink\nrendering and the session UI themselves.\n\n### `@remotedraw/client` (framework-free)\n\n- `createHttpRemoteDrawApiClient(baseUrl, { apiKey })` — **server-only.**\n `createSession`, `getSession`, `listSessions`, `issueJoinToken`,\n `connectSender`, `endSession`, `createAiAction`, `getAiAction`,\n `cancelAiAction`, `waitForAiAction`.\n- `createSessionWithHttpApi(baseUrl, options)` — server-only convenience.\n- `createHttpReceiverClient(baseUrl)` — the seven `/v1/receiver/*` calls.\n Credentials go in the body, not a header.\n- `createHttpSenderClient(baseUrl, { packPoints })` — 14 sender calls.\n- `createReceiverStore(options)` — the headless receiver state machine\n `RemoteDrawProvider` and the Svelte store both bind.\n- `createRealtimeReceiverSource({ driver })` + `createConvexRealtimeDriver({ client })`\n — push transport over the hosted realtime endpoint. `convex` is never imported\n by the package; you hand it a two-method driver.\n- `RemoteDrawHttpError` — `status`, `code`, `upgradeUrl`, `body`, and the\n getters `isAuthenticationFailure`, `isPermissionFailure`, `isSessionOver`,\n `shouldReJoin`.\n- `joinTokenFromInput`, `joinUrlForOrigin`, `nativeJoinUrlFromSession`.\n- `createPacedDraftQueue` / `createLatestOnlyQueue` / `draftPointsForTransport`\n / `retryIdempotentRequest` — the 32 ms latest-only draft gate a custom sender\n must use instead of POSTing every pointer event.\n- `createDirectSenderController`, `resolveDirectSenderStatus`,\n `directSenderConnectionFromResult` — the shared direct-sender rules.\n- `createRealtimeVisualContextSignalingClient` /\n `createHttpVisualContextSignalingClient` — where live-view signals travel\n (realtime push, or the polled public `/v1/.../visual-context/*` routes).\n\n### `@remotedraw/geometry`\n\nStroke/shape helpers (`buildNormalizedStroke`, `recognizeStroke`,\n`simplifyNormalizedPoints`, hit-testing, transforms, `drawingsToSvg`), **and the\nmap board transform**, re-exported by `@remotedraw/react`:\n`boardPointFromLngLat`, `lngLatFromBoardPoint`, `longitudeFromBoardX`,\n`latitudeFromBoardY`, `mercatorYFromLatitude`, `latitudeFromMercatorY`,\n`boardViewportFromMapBounds`, `mapBoundsFromBoardViewport`,\n`mapBoardPointToScreen`, `mapBoardPointFromScreen`, `screenPointFromBoardPoint`,\n`surfacePointFromBoardPoint`, `padMapBounds`, `MAX_MERCATOR_LATITUDE`.\nUse these rather than reimplementing the projection — `x` is linear in\nlongitude, `y` is linear in **Web Mercator**, and a version that is linear in\nlatitude puts ink kilometres away.\n\n### `RemoteDrawSenderKit` (SwiftPM, iOS 17+)\n\n`https://github.com/AxioSOzo/remotedraw-swift.git`, product\n`RemoteDrawSenderKit`. Zero external dependencies. No `Info.plist` entries.\n\n- `RemoteDraw.shared` — **zero-config**: it installs production defaults the\n first time anything reads it. `RemoteDraw.configure(_:)` in `App.init` is\n *optional* and only overrides `apiBaseURL`, `device`, `tokenProvider`,\n `urlSession`, `onClientAdvisory`. `try RemoteDraw.requireConfigured()` throws\n `RemoteDrawError.notConfigured` if you want the strict behaviour back.\n (This used to `preconditionFailure` from inside the modifier's `.task` — a\n crash on the user's tap. It no longer does.)\n- `.remoteDrawSurface(isPresented:senderToken:appearance:strings:exit:onOutcome:)`\n — the whole integration, a full-screen cover with the board, an exit and an\n outcome. A second overload adds `background:` — a `@ViewBuilder` handed a\n `RemoteDrawGroundContext` (`session`, `mapBounds`, `phoneProjection`, `size`,\n `reportViewport`) for your own cartography or ground. A ground that **moves**\n must call `reportViewport(_:)`; a static one calls nothing.\n- `RemoteDrawTakeover` — the same board plus scene-phase wiring and exit, for\n your own cover / navigation push / `UIHostingController`.\n- `RemoteDrawSurface` — the board as a plain `View`, for a host that fills its\n presentation with it edge to edge.\n- `RemoteDrawSenderSession` — the headless core (`begin`/`append`/`end`,\n `undo`, `clear`, `submit(metadata:)`, `edit`, `updateProjection`, `leave`,\n published `phase`, `strokes`, `live`, `lastError`), with\n `RemoteDrawInkCanvas` + `RemoteDrawStrokeCapture` when you own the screen.\n- `RemoteDrawBoardCanvas`, `RemoteDrawMapBoardGround`, `RemoteDrawMapGeometry`,\n `RemoteDrawAppearance`, `RemoteDrawStrings`, `RemoteDrawExit`,\n `RemoteDrawError` (13 cases with `shouldReJoin` / `isRetriable`).\n- **Map boards are built in.** A `kind: \"map\"` session with\n `coordinateSpace.bounds` draws the geography through MapKit, using the same\n transform as `@remotedraw/geometry`. The built-in map is deliberately not\n pannable; supply your own through `background:` if it should be.\n- **Streaming boards are not.** A session with `senderIntegrationMode:\n \"streaming\"` or a receiver publishing `visualContext.enabled` cannot be drawn\n by this SDK — no WebRTC, no video, no `WKWebView`. It reports\n `.unsupportedSurface(_:)` carrying `hostedSenderURL`; present that in a\n `WKWebView` and streaming works today.\n\n`RemoteDrawOutcome` is a **closed** enum:\n\n| Case | Meaning | Do |\n| --- | --- | --- |\n| `.submitted(RemoteDrawReceipt)` | Drawing submitted. | Record it. Call `session.submit(metadata:)` yourself for the server's own ids — a submit from the SDK's controls reports a placeholder. |\n| `.left` | The person left; ink is on the board. | Nothing. |\n| `.expired` | The board finished or timed out. **Terminal.** | Create a new session. |\n| `.credentialLost(RemoteDrawError?)` | The token died; the board did not. **Recoverable.** | Mint a fresh `rd_send_` and present again. |\n| `.unsupportedSurface(RemoteDrawUnsupportedSurface)` | The board wants a renderer this SDK lacks. The cover **stays up** showing the reason. | Open `hostedSenderURL` in a `WKWebView`. |\n| `.failed(RemoteDrawError)` | Anything else. | Read `error.shouldReJoin` / `error.isRetriable`. |\n\n**There is no `.revoked`.** A revoked, unknown and malformed token all answer\n`invalid_sender_token` on purpose; only a genuine expiry is distinguishable, and\nthat travels in the error on `.credentialLost`.\n\nNote: `RemoteDrawKit` is a *different*, internal macOS-only package that some\nolder docs still name. Customers use `RemoteDrawSenderKit`.\n\n## Recipes, one per surface\n\nAll React snippets assume the session came from your backend and are wrapped in:\n\n```tsx\nconst receiver = createHttpReceiverClient(\"https://api.remotedraw.com\");\n\n<RemoteDrawProvider\n session={session.session}\n receiverToken={session.receiverToken}\n joinUrl={session.joinUrl}\n joinUrls={session.joinUrls}\n receiver={receiver}\n onError={(error) => reportToYourLogger(error)}\n>\n {/* the recipe */}\n</RemoteDrawProvider>\n```\n\n### Whiteboard / freeform sketch\n\n```ts\n// Backend\ntarget: { kind: \"whiteboard\", label: \"Session notes\" }\n```\n\n```tsx\n<PairingCode title=\"Scan to draw\" />\n<RemoteDrawReceiver />\n<RemoteDrawSessionControls title=\"Sender workflow\" />\n```\n\n### Photo / screenshot annotation (`image`)\n\nThe photo is **already on the receiver**. The phone never takes it.\n\n```ts\ntarget: {\n kind: \"image\",\n inputMapping: \"surface\", // the pad is the whole photo\n coordinateSpace: { width: 1600, height: 900 },\n metadata: { label: \"Inspection photo\", imageId: \"img_123\" },\n}\n```\n\n```tsx\n<div style={{ position: \"relative\" }}>\n <RemoteDrawReceiver\n coordinateAspectRatio={16 / 9}\n background={<img src={photoUrl} alt=\"\" style={{ width: \"100%\", height: \"100%\", objectFit: \"contain\" }} />}\n />\n <PairingCode placement=\"top-right\" size={112} />\n</div>\n```\n\n*Limit:* RemoteDraw stores no images. Your app owns the photo and the link\nbetween it and the drawings.\n\n### PDF page\n\n```ts\ntarget: { kind: \"pdf\", inputMapping: \"viewport\",\n metadata: { documentId: \"doc_9\", page: 3 } }\n```\n\n```tsx\n<div style={{ position: \"relative\" }}>\n <YourPdfPage page={3} />\n <RemoteDrawReceiver\n background=\"transparent\"\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\" }}\n />\n</div>\n```\n\n*Limit:* RemoteDraw renders no PDFs and knows nothing about pages. One session\nper page, or carry the page number in `target.metadata` and re-create.\n\n### Signature / bounded field\n\n```ts\ntarget: {\n kind: \"field\",\n inputMapping: \"surface\", // mandatory — the pad IS the field\n coordinateSpace: { width: 1600, height: 500 },\n metadata: { label: \"Customer signature\", fieldId: \"sig_1\" },\n}\n```\n\n```tsx\n<RemoteDrawConnect method=\"qr\" triggerLabel=\"Sign with your phone\" showTriggerLabel />\n<RemoteDrawReceiver coordinateAspectRatio={16 / 5} strokeWidth={7}\n style={{ border: \"1px solid #ddd8cf\", borderRadius: 12 }}>\n <line x1=\"140\" y1=\"760\" x2=\"3060\" y2=\"760\" stroke=\"#d8d8d8\" strokeWidth=\"4\" />\n</RemoteDrawReceiver>\n```\n\n*Limit:* this is markup transport, **not** e-signature compliance. No identity\nproofing, no intent-to-sign ceremony, no tamper-evident audit package, no\ncertificates. Say so if the user asks for a legal signature.\n\n### Map\n\nThe board's `coordinateSpace.bounds` is a **hard geographic fence, fixed for the\nlife of the session** — no route changes it. Size it larger than the camera you\nopen on. The phone pans *inside* it.\n\n```ts\nimport { padMapBounds } from \"@remotedraw/geometry\";\n\ntarget: {\n kind: \"map\",\n inputMapping: \"viewport\",\n coordinateSpace: {\n width: 1600, height: 1310, // the fence's Mercator aspect\n ...padMapBounds(currentCameraBounds, 1), // 3x the camera\n },\n}\n```\n\n```tsx\nconst [camera, setCamera] = useState(0);\nuseEffect(() => {\n const onMove = () => setCamera((n) => n + 1);\n map.on(\"move\", onMove);\n return () => map.off(\"move\", onMove);\n}, [map]);\nconst projectLngLat = useCallback(\n (lng: number, lat: number) => map.project([lng, lat]), // CSS px in the container\n [map, camera],\n);\n\n<div style={{ position: \"relative\" }}>\n <div ref={mapContainer} style={{ position: \"absolute\", inset: 0 }} />\n <RemoteDrawMapReceiver\n projectLngLat={projectLngLat}\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\" }}\n />\n</div>\n```\n\n*Limits:* `mapBounds={map.getBounds()}` is the no-callback alternative but is\nexact only for a north-up, unpitched camera. Omitting `bounds` at session\ncreation does not mean \"the customer's map\" — it means RemoteDraw's own default\nregion. `senderIntegrationMode` stays `\"native\"` on a map board: streaming\nrequires a `phoneProjection` that a native map sender never sends.\n\n### Screen / live view\n\n```ts\ntarget: { kind: \"screen\", inputMapping: \"viewport\" }\ncapabilities: [..., \"viewReceiverContext\"]\nvisualContext: { enabled: true }\n```\n\nThe receiver publishes with `useVisualContextPublisher`. Three consumers, in\norder of how little you write:\n\n1. **The hosted `/join` pad** — consumes the stream automatically. Zero code.\n2. **Your own web pad** — `RemoteDrawStreamView` with your pad as its\n `children`, plus a signaling client.\n3. **`RemoteDrawSenderKit`** — *cannot* consume it. It returns\n `.unsupportedSurface(_:)` carrying a `hostedSenderURL`; present that in a\n `WKWebView` and streaming works today.\n\nLive view is experimental: build so that a session whose stream never starts is\nstill a working session — the phone keeps drawing, it simply does not see the\nreceiver's pixels.\n\n### iOS sender — the complete Swift\n\n```swift\n// Package.swift / Xcode → Add Package\n.package(url: \"https://github.com/AxioSOzo/remotedraw-swift.git\", from: \"0.1.0\")\n.product(name: \"RemoteDrawSenderKit\", package: \"remotedraw-swift\")\n\nimport RemoteDrawSenderKit\n\n// Optional. RemoteDraw.shared installs production defaults on first use, so\n// this line exists only to OVERRIDE something.\n@main struct MyApp: App {\n init() { RemoteDraw.configure(.init(tokenProvider: mintSenderToken)) }\n var body: some Scene { WindowGroup { RootView() } }\n}\n\n// Wherever drawing starts. `token` is the rd_send_ string your backend minted\n// with POST /v1/sessions/direct-sender. A QR's rd_join_ works too, but spending\n// one revokes every other sender on that session.\nButton(\"Draw\") { drawing = true }\n .remoteDrawSurface(isPresented: $drawing, senderToken: token) { outcome in\n switch outcome {\n case .submitted(let receipt): record(receipt)\n case .left: dismissBanner()\n case .expired: refreshSession() // terminal\n case .credentialLost: refreshToken() // recoverable\n case .unsupportedSurface(let it): openHostedPad(it.hostedSenderURL)\n case .failed(let error): report(error) // do not swallow\n }\n }\n```\n\nReceiving the launch URL (the other half of the direct sender — nothing else\ndocuments it):\n\n```swift\n// Info.plist: CFBundleURLTypes → your scheme, e.g. \"yourapp\"\n.onOpenURL { url in\n guard url.scheme == \"yourapp\",\n let token = URLComponents(url: url, resolvingAgainstBaseURL: false)?\n .queryItems?.first(where: { $0.name == \"senderToken\" })?.value\n else { return }\n senderToken = token\n drawing = true\n}\n```\n\nRules:\n\n- The surface is **full screen**. Not a small pad, not a draggable sheet.\n- The user can always leave; `RemoteDrawExit` only decides whether leaving with\n unsubmitted ink asks first. The host always gets an outcome.\n- Do not write a `UIViewRepresentable` canvas, a draft loop, or an HTTP client.\n Go headless on `RemoteDrawSenderSession` + `RemoteDrawInkCanvas` +\n `RemoteDrawStrokeCapture` if you own the screen — never raw `URLSession`.\n- Customers do not ship a separate RemoteDraw app; their app *is* the sender.\n\n## How to suggest use cases\n\n**What the product is good at**, as a sentence to pattern-match against:\n\n> A person is at a screen. The thing they need to mark is already on that\n> screen. A mouse is the wrong instrument for the mark — because it is\n> handwriting, a circle around a defect, a diagram, a signature, or a gesture\n> over a map — and their phone is in their pocket.\n\n**Ask these four before proposing anything:**\n\n1. Which screen in your product already shows the thing to be marked, and what\n device is that screen on?\n2. Is the person in front of it holding a phone at the same time?\n3. What does the mark mean afterwards — saved to which record, shown where?\n4. Anyone who scans, or a signed-in user of your own app? (QR vs direct sender.)\n\n**Good vs bad, for a property-management SaaS:**\n\n- ✅ Property manager reviews an inspection photo on the office desktop and\n circles the damage with their phone. *Two devices; content already on the\n receiver.*\n- ✅ Tenant signs the handover report on the manager's laptop screen using their\n own phone as the pen. *This is the flow that replaces a stylus.*\n- ✅ Planner marks a route on the dispatch map on the wall display.\n- ✅ Support agent circles the broken control on a customer's shared screen.\n- ❌ Tenant photographs a leak on their phone and circles it. *One device,\n phone-supplied content. **Not RemoteDraw.*** Say so and propose PencilKit.\n- ❌ An in-app sketch pad in the mobile app. *One device.*\n- ❌ Field engineer marks up a PDF on their iPad in the van. *One device —\n unless a second screen is genuinely present.*\n- ❌ \"Pair over Bluetooth when the phone is nearby.\" *Not implemented.*\n\n**The disqualifier:** if you cannot name two devices and say which one already\ndisplays the content, you do not have a use case yet — ask.\n\n## The flow — in this order\n\n1. **Explain before touching anything.** If the user is asking what RemoteDraw\n can do, answer from this file and the docs. Do not install, scaffold, or\n create sessions to answer a question.\n2. **Ask for approval before installing.** Name exactly what you want to add\n (`@remotedraw/cli`, `@remotedraw/react`, a Swift package, a dashboard\n project + key) and why, then wait. This includes `remotedraw init` without\n `--offline`, which provisions a billable project and key. Install with the\n project's own package manager — the scan below reports it, and a lockfile\n the project did not ask for is a mess a human has to clean up:\n `npm install -g @remotedraw/cli`, `pnpm add -g @remotedraw/cli`,\n `bun add -g @remotedraw/cli`, or — Yarn Berry has no global install —\n `yarn dlx @remotedraw/cli`. Same for the SDKs: `npm install`, `pnpm add`,\n `yarn add`, or `bun add`.\n3. **Scan the codebase.** `remotedraw scan --format json` (or, before the CLI\n is installed, `npx @remotedraw/cli@latest scan --format json` — `pnpm dlx`,\n `yarn dlx`, or `bunx @remotedraw/cli` for those managers) reports the\n web/server/iOS projects, the project's package manager and its install/add\n commands, where an `rd_sk_` key may live, any RemoteDraw wiring already\n present, the integration options that fit, and the product questions to ask.\n Read it; verify its `evidence` where it matters.\n4. **Ask the product questions** (the four above, plus the scan's own). Do not\n invent answers; a one-page \"drawing lab\" is only right when the user says a\n demo is what they want.\n5. **Propose one plan, then build all of it.** Receiver, session creation,\n sender, and the exit/submit path — an integration is not done when ink\n appears once on a test page. Build beside existing features.\n6. **Verify by using it.** Run `remotedraw doctor --format json`, open a real\n session, draw from a phone (or `create-input --execute` + the hosted join\n URL), and confirm ink lands on the receiver. On iOS, run it on a device or\n simulator and look at the screen; a compiling canvas is not a working one.\n\nSession creation (`POST /v1/sessions`) needs the `rd_sk_` key and therefore runs\nonly where the scan found server-side code: a Convex action, a Next route\nhandler, an Express/Hono route, a serverless function. If the scan found none,\nask where the backend is. Never scaffold `createRemoteDrawSession.ts` into a\nVite/Next client tree — `--target web` in `remotedraw init` still writes it\nunder `src/`; move it, or scaffold into a scratch directory and copy only what\nbelongs.\n\n## CLI\n\n```sh\nremotedraw options --format json # the option catalog\nremotedraw scan --format json # read the codebase first (step 3)\nremotedraw init --non-interactive --offline --dry-run --format json \\\n --path apps/web --target web --sender remotedraw-ios --sdk react --preset sketch --package-manager npm\nremotedraw doctor --format json\nremotedraw create-input --preset sketch --json\nremotedraw create-input --execute # a real session + join URL, needs a key\nremotedraw agent --print-skill # this file, no install needed\n```\n\n`--sender own-ios` requires `--sdk swift`; other invalid combinations fail with\n`INVALID_COMBINATION`. By default `init`/`new` also create a dashboard project\nand a project-scoped development key in `.env.local` — that is the step that\nneeds approval (step 2); `--offline` writes files only. `--preset mapMarkup`\nemits an explicit `coordinateSpace.bounds`; replace the example region with the\ncustomer's. Never pass `--force` unless the user approved overwriting. Do not\ndrive the interactive wizard or scrape human-formatted output; every command has\n`--format json`.\n\n## Security and tenancy\n\n- `rd_sk_…` keys: backend secrets only. Never in browser bundles, Swift, app\n bundles, screenshots, logs, or generated examples.\n- The account-level `rd_cli_…` credential stays in the user config directory;\n never copy it into a project. `REMOTEDRAW_CLI_TOKEN` is for CI secrets only.\n- Public clients receive only `joinUrl`, `joinToken`, `receiverToken`, or\n `senderToken`, each scoped to one session. Production QR codes use the HTTPS\n `joinUrl`, not the custom scheme.\n- One key serves every customer of the product, so a session id is not a\n capability. Create sessions with `externalId: \"<product>:<tenant>\"`, and check\n it (`POST /v1/sessions/get`) before attaching a sender or ending a session on\n a tenant's behalf.\n\n## Gotchas the SDKs hide and hand-written code hits\n\n- `POST /v1/sessions` is billable and not idempotent. React StrictMode runs\n mount effects twice in development: guard with a ref, or create the session in\n a server action / loader. Store the `receiverToken` if the receiver outlives a\n page load — it is the only credential that reads a session's ink.\n- Timestamps are integer milliseconds. `occurredAt` and point `t` values are\n accepted with a fraction (floored) but a hand-written client should send\n integers.\n- Committed points come back in board space, remapped through\n `device.aspectRatio`; send the aspect ratio of the pad the finger touches.\n- `RemoteDrawReceiver` defaults `phones` and `pointers` to on, and renders\n immediately with no empty state. Pass `phones={false}` for a plain surface;\n drive your own empty state from `useReceiverData().senders`.\n- `PairingCode` hides the join URL text unless `showLink`.\n- `joinTokenExpiresAt` is earlier than the session's `expiresAt`: the QR dies\n first, the board stays live.\n- Wire `RemoteDrawProvider`'s `onError` (and `useVisualContextPublisher`'s).\n Without it a dead credential is silent and the board simply stops updating.\n- The hosted `/join` pad respects the joined capability list; a custom sender\n must too. It also has a **file-attach tool** available on any session granting\n `draw` or `point`, which cannot currently be turned off.\n\n## API contract\n\n- Backend: `POST /v1/sessions` (create), `/v1/sessions/get`, `/v1/sessions/end`,\n `/v1/sessions/direct-sender` (mint `rd_send_` for your own app),\n `/v1/sessions/join-token` (a fresh QR).\n- Receiver: `POST /v1/receiver/session`, `/drawings`, `/drafts`, `/senders`\n with the receiver token, or the realtime source in the SDKs.\n- Sender: `POST /v1/join` (spends a join token; revokes other senders), then\n `/v1/sender/draft` (latest-only preview, throttle to ~32 ms),\n `/v1/sender/commit` (one durable stroke per pointer-up with a stable\n `clientStrokeId`), `/v1/sender/submit`.\n- Capabilities: `draw`, `point`, `undo`, `clear`, `moveViewport`,\n `viewExisting`, `viewReceiverContext`. Omitting `capabilities` grants the\n first six. Reissued join tokens may narrow but never widen.\n\n## AI actions\n\nReach for AI when the product needs something _from_ the finished drawing: a\ngenerated image, a description, or structured data to branch on. Backend only\n(`aiActions:*` scopes on an `rd_sk_...` key). Never wire it to a commit, submit,\nor presence event — AI runs only on an explicit `POST /v1/ai-actions` call the\nuser asked for. Run it after the user is done; the route accepts `active` and\n`ended` sessions.\n\nMinimal request per outcome (`POST /v1/ai-actions`, plus optional\n`quality: \"fast\" | \"balanced\" | \"max\"`, default `balanced`):\n\n```jsonc\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\" } // image back in the response\n{ \"sessionId\": \"...\", \"request\": \"image\", \"prompt\": \"...\", \"deliver\": [\"result\", \"board\"] } // and onto the board\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"Describe this drawing.\" } // text back\n{ \"sessionId\": \"...\", \"request\": \"text\", \"prompt\": \"...\", \"text\": { \"schema\": { /* JSON Schema */ } } } // typed JSON\n```\n\nThe response is asynchronous: `create` returns `status: \"queued\"`. Poll\n`POST /v1/ai-actions/get` until `status` is `succeeded`, `failed`, or\n`canceled`, or use `createAiAction` + `waitForAiAction` on\n`createHttpRemoteDrawApiClient` from `@remotedraw/client` (re-exported by\n`@remotedraw/react`) — backend only, it holds the key. There is no completion\nwebhook. Render results with `AiImage` / `AiText`. See\nhttps://docs.remotedraw.com/docs/api#ai.\n\n## What does not exist yet\n\nVerified against source, 2026-08-30. Read this *before* designing, so you never\npromise any of it.\n\n- **No Bluetooth or Wi-Fi pairing.** The radios advertise; nothing scans or\n browses, on any platform. `PairingDevices` is presentational chrome.\n- **No QR into a customer's own app.** The Universal-Link association file has\n one hard-coded appID and the QR host is a RemoteDraw deployment setting. Use\n the direct sender.\n- **No native Android SDK.** Hosted `/join` in a mobile browser is the Android\n sender, and it is full-featured.\n- **No desktop or trackpad sender.** Unbuilt research.\n- **No streaming consumer in `RemoteDrawSenderKit`.** It reports\n `.unsupportedSurface(_:)` with a `hostedSenderURL` to open in a `WKWebView`.\n (A customer's own *web* pad can consume a stream — `RemoteDrawStreamView`.\n It is the native SDK that cannot.)\n- **No Svelte components.** A receiver store and a direct-sender store only.\n- **No web sender component.** Deliberate: hosted `/join` is the web sender.\n- **No API-key route that reads a session's ink.** Lose the `receiverToken` and\n the session is unreadable while still billable.\n- **No `clientSessionId` idempotency on `POST /v1/sessions`.**\n- **No completion webhook for AI actions** — poll `POST /v1/ai-actions/get`.\n- **No e-signature compliance.** No identity proofing, intent-to-sign ceremony,\n tamper-evident audit package, or certificate handling.\n- **No content rendering of any kind.** No PDF renderer, no image storage, no\n document pipeline, no auth, no billing UI. The host renders; RemoteDraw inks.\n\n## Verification\n\nAfter changes, verify against the customer's project — never assume RemoteDraw's\nown repo scripts exist here.\n\n```sh\nremotedraw doctor # config, SDK deps, REMOTEDRAW_* env\nremotedraw create-input --execute # open a real session, print the join URL\n```\n\nThen run whatever type check and test command the project already defines (for\nexample `npm run typecheck` and `npm test`). Do not invent script names, and do\nnot run `bun run test:api`, `bun run typecheck`, or `bun run ios:kit:test` —\nthose are RemoteDraw's internal monorepo scripts and will not exist in a\ncustomer project.\n\n## Reference\n\n- API: `https://api.remotedraw.com` · Docs: `https://docs.remotedraw.com/docs`\n (agent summary: `https://docs.remotedraw.com/llms.txt`) · Keys:\n `https://dashboard.remotedraw.com/api/keys`\n- Packages: `@remotedraw/cli`, `@remotedraw/react`, `@remotedraw/svelte`,\n `@remotedraw/client`, `@remotedraw/protocol`, `@remotedraw/geometry`, and the\n SwiftPM package `https://github.com/AxioSOzo/remotedraw-swift.git` (product\n `RemoteDrawSenderKit`).";
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ if (process.stderr.isTTY && process.argv[2] !== "update") {
|
|
|
31
31
|
try {
|
|
32
32
|
const check = await checkForUpdate(runtime, CLI_VERSION);
|
|
33
33
|
if (check?.updateAvailable) {
|
|
34
|
-
const method = detectInstallMethod(runtime.binPath ?? "");
|
|
34
|
+
const method = detectInstallMethod(runtime.binPath ?? "", runtime.env);
|
|
35
35
|
process.stderr.write(`\n${updateNotice(check, method)}\n`);
|
|
36
36
|
}
|
|
37
37
|
}
|
package/dist/locales/en.d.ts
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export declare const en: {
|
|
14
14
|
readonly "language.word": "Language";
|
|
15
|
-
readonly "language.prompt.helper": "Sets the language of the RemoteDraw CLI. Generated code and files stay in English.";
|
|
16
15
|
readonly "language.saved": "Language set to {language}. Change it any time with: remotedraw language";
|
|
17
16
|
readonly "language.current": "Language: {language} ({locale})";
|
|
18
17
|
readonly "language.source.flag": "Set for this run by --language.";
|
|
@@ -48,6 +47,24 @@ export declare const en: {
|
|
|
48
47
|
readonly "help.command.createInput": "Create or print a test input request payload.";
|
|
49
48
|
readonly "help.command.examples": "List or install example projects.";
|
|
50
49
|
readonly "help.command.agent": "Print or install the RemoteDraw agent skill.";
|
|
50
|
+
readonly "help.command.scan": "Read a codebase and propose where RemoteDraw fits.";
|
|
51
|
+
readonly "help.command.project": "Create a dashboard project and API key without scaffolding.";
|
|
52
|
+
readonly "project.help.usage": " remotedraw project create --name <name> [--key-name <name>] [--env <dir>] [--format json]";
|
|
53
|
+
readonly "project.help.body": "Creates a dashboard project and a project-scoped development API key. Nothing is scaffolded; use it when the integration already has its files.";
|
|
54
|
+
readonly "project.help.env": "Pass --env <dir> to write REMOTEDRAW_API_BASE_URL, REMOTEDRAW_PROJECT_ID, and REMOTEDRAW_API_KEY into <dir>/.env.local instead of printing the key.";
|
|
55
|
+
readonly "project.help.auth": "Needs a signed-in CLI (remotedraw login) or REMOTEDRAW_CLI_TOKEN.";
|
|
56
|
+
readonly "project.error.unknownSubcommand": "Unknown project subcommand: {subcommand}. Expected create.";
|
|
57
|
+
readonly "project.error.nameRequired": "--name is required for project create.";
|
|
58
|
+
readonly "project.created": "Created project {name} ({id}).";
|
|
59
|
+
readonly "project.keyWritten": "Wrote the API key to {path}.";
|
|
60
|
+
readonly "project.keyOnce": "API key (shown once): {key}";
|
|
61
|
+
readonly "project.keyRule": "Keep rd_sk_... keys in backend secrets only; never in browser bundles, mobile apps, or source control.";
|
|
62
|
+
readonly "cloud.warning.clientTree": "src/remotedraw/createRemoteDrawSession.ts reads REMOTEDRAW_API_KEY and must run server-side. Move it to your API route, server action, Convex action, or serverless function before use; a browser bundle must never import it.";
|
|
63
|
+
readonly "scan.help.usage": " remotedraw scan [--path <dir>] [--format json]";
|
|
64
|
+
readonly "scan.help.body": "Detects web, server, and iOS projects, existing RemoteDraw wiring, and where an rd_sk_ key may live; then lists integration options and the product questions to ask the user before building.";
|
|
65
|
+
readonly "scan.help.privacy": "Reads manifests and file names only. Reports env variable names, never values.";
|
|
66
|
+
readonly "scan.help.json": "Pass --format json for the machine-readable report agents should read.";
|
|
67
|
+
readonly "scan.error.missingPath": "Path does not exist: {path}";
|
|
51
68
|
readonly "help.startHere": "Start here:";
|
|
52
69
|
readonly "guide.title": "RemoteDraw integration guide";
|
|
53
70
|
readonly "guide.intro": "Choose the surface your customer already has:";
|
|
@@ -149,22 +166,22 @@ export declare const en: {
|
|
|
149
166
|
readonly "choice.preset.sketch.summary": "Normalized input for your own receiver surface.";
|
|
150
167
|
readonly "choice.preset.sketch.explanation": "Choose this when your product owns the photo, map, PDF, canvas, or other receiver UI. RemoteDraw supplies the input primitives without prescribing a component design.";
|
|
151
168
|
readonly "choice.preset.sketch.docs": "Open sketch docs";
|
|
152
|
-
readonly "choice.preset.photoMarkup.summary": "
|
|
153
|
-
readonly "choice.preset.photoMarkup.explanation": "
|
|
169
|
+
readonly "choice.preset.photoMarkup.summary": "Draw on a photo your app is already showing on a screen.";
|
|
170
|
+
readonly "choice.preset.photoMarkup.explanation": "Your receiver renders the photo — on the desktop, laptop, or display someone is looking at — and a phone marks it up from across the room. The phone never takes or uploads the picture; your app supplies it and keeps the link with the drawings. Use this for reviewing an inspection photo at a desk, giving feedback on imagery, or annotating a screenshot.";
|
|
154
171
|
readonly "choice.preset.photoMarkup.docs": "Open photo markup docs";
|
|
155
|
-
readonly "choice.preset.pdfMarkup.summary": "
|
|
156
|
-
readonly "choice.preset.pdfMarkup.explanation": "Use this for document review
|
|
172
|
+
readonly "choice.preset.pdfMarkup.summary": "Draw on a PDF page your app is already showing on a screen.";
|
|
173
|
+
readonly "choice.preset.pdfMarkup.explanation": "Use this for document review at a desk: your receiver renders the right page and a phone marks it. RemoteDraw renders no PDFs — your app owns the page and maps RemoteDraw coordinates onto that fixed view.";
|
|
157
174
|
readonly "choice.preset.pdfMarkup.docs": "Open PDF markup docs";
|
|
158
|
-
readonly "choice.preset.mapMarkup.summary": "
|
|
159
|
-
readonly "choice.preset.mapMarkup.explanation": "Use this for routes, locations, or spatial feedback. Your app owns the map and
|
|
175
|
+
readonly "choice.preset.mapMarkup.summary": "Draw routes and directions on the map your app is showing.";
|
|
176
|
+
readonly "choice.preset.mapMarkup.explanation": "Use this for routes, locations, or spatial feedback marked from a phone onto a map on a desktop or wall display. Your app owns the map and the camera; RemoteDraw carries the input in geographic board space. The board's coordinateSpace.bounds is a fixed fence — size it larger than the camera you open on.";
|
|
160
177
|
readonly "choice.preset.mapMarkup.docs": "Open map markup docs";
|
|
161
178
|
readonly "choice.preset.screenMarkup.label": "Screen annotation";
|
|
162
179
|
readonly "choice.preset.screenMarkup.hint": "Mark up a screen or window your receiver shares.";
|
|
163
180
|
readonly "choice.preset.screenMarkup.summary": "Annotations on a screen or application view.";
|
|
164
|
-
readonly "choice.preset.screenMarkup.explanation": "Use this for support, demos, and UI feedback. Your receiver supplies the screen image the phone marks are projected onto.";
|
|
181
|
+
readonly "choice.preset.screenMarkup.explanation": "Use this for support, demos, and UI feedback: the screen being marked is the receiver's, and the phone is the pen. Your receiver supplies the screen image the phone marks are projected onto.";
|
|
165
182
|
readonly "choice.preset.screenMarkup.docs": "Open screen markup docs";
|
|
166
183
|
readonly "choice.preset.designReview.summary": "Focused visual feedback on a design.";
|
|
167
|
-
readonly "choice.preset.designReview.explanation": "Use this for design reviews with arrows, shapes, and freehand lines. Your product owns comments, versions, and decisions.";
|
|
184
|
+
readonly "choice.preset.designReview.explanation": "Use this for design reviews with arrows, shapes, and freehand lines drawn from a phone onto the design open on a reviewer's screen. Your product owns comments, versions, and decisions.";
|
|
168
185
|
readonly "choice.preset.designReview.docs": "Open design review docs";
|
|
169
186
|
readonly "choice.preset.pointer.summary": "Live pointing without persistent ink.";
|
|
170
187
|
readonly "choice.preset.pointer.explanation": "Use this for presentations and guidance where the phone acts as a pointer and movement matters more than stored ink.";
|
|
@@ -197,7 +214,6 @@ export declare const en: {
|
|
|
197
214
|
readonly "wizard.control.back": "Esc back";
|
|
198
215
|
readonly "wizard.control.quitCtrlC": "Ctrl+C quit";
|
|
199
216
|
readonly "wizard.control.quitQ": "q quit";
|
|
200
|
-
readonly "wizard.control.move": "↑/↓ move";
|
|
201
217
|
readonly "wizard.control.steps": "←/→ steps";
|
|
202
218
|
readonly "wizard.control.choose": "↑/↓ choose";
|
|
203
219
|
readonly "wizard.control.docs": "d open docs";
|
|
@@ -215,7 +231,6 @@ export declare const en: {
|
|
|
215
231
|
readonly "wizard.tagline.setup": "phone → canvas · project setup";
|
|
216
232
|
readonly "wizard.tagline": "phone → canvas";
|
|
217
233
|
readonly "wizard.docs.openFailed": "Could not open the documentation. Use {url}";
|
|
218
|
-
readonly "wizard.docs.button": "[ {label} ↗ ]";
|
|
219
234
|
readonly "new.help.usage.bare": " remotedraw new";
|
|
220
235
|
readonly "new.help.usage.named": " remotedraw new --app-name <name> [options]";
|
|
221
236
|
readonly "new.help.interactive": "Run without options for the interactive setup flow.";
|
|
@@ -283,7 +298,7 @@ export declare const en: {
|
|
|
283
298
|
readonly "doctor.sdk.ok": "{package} is listed in package.json dependencies.";
|
|
284
299
|
readonly "doctor.sdk.missing": "Add {package}@{range} to dependencies or rerun remotedraw init.";
|
|
285
300
|
readonly "doctor.apiKey.ok": "Server-side API key shape looks correct.";
|
|
286
|
-
readonly "doctor.apiKey.missing": "
|
|
301
|
+
readonly "doctor.apiKey.missing": "No rd_sk_... key found in this project's environment (REMOTEDRAW_API_KEY). When you add one, keep it in backend secrets only.";
|
|
287
302
|
readonly "doctor.label.deployment": "deployment";
|
|
288
303
|
readonly "doctor.label.deploymentReachable": "deployment reachable";
|
|
289
304
|
readonly "doctor.label.apiKeyAccepted": "API key accepted";
|
package/dist/locales/en.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,EAAE
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4lBL,CAAC"}
|
package/dist/locales/en.js
CHANGED
|
@@ -16,7 +16,6 @@ export const en = {
|
|
|
16
16
|
// installed locale into its prompt, so the question reads no matter which
|
|
17
17
|
// language the user speaks.
|
|
18
18
|
"language.word": "Language",
|
|
19
|
-
"language.prompt.helper": "Sets the language of the RemoteDraw CLI. Generated code and files stay in English.",
|
|
20
19
|
"language.saved": "Language set to {language}. Change it any time with: remotedraw language",
|
|
21
20
|
"language.current": "Language: {language} ({locale})",
|
|
22
21
|
"language.source.flag": "Set for this run by --language.",
|
|
@@ -54,6 +53,24 @@ export const en = {
|
|
|
54
53
|
"help.command.createInput": "Create or print a test input request payload.",
|
|
55
54
|
"help.command.examples": "List or install example projects.",
|
|
56
55
|
"help.command.agent": "Print or install the RemoteDraw agent skill.",
|
|
56
|
+
"help.command.scan": "Read a codebase and propose where RemoteDraw fits.",
|
|
57
|
+
"help.command.project": "Create a dashboard project and API key without scaffolding.",
|
|
58
|
+
"project.help.usage": " remotedraw project create --name <name> [--key-name <name>] [--env <dir>] [--format json]",
|
|
59
|
+
"project.help.body": "Creates a dashboard project and a project-scoped development API key. Nothing is scaffolded; use it when the integration already has its files.",
|
|
60
|
+
"project.help.env": "Pass --env <dir> to write REMOTEDRAW_API_BASE_URL, REMOTEDRAW_PROJECT_ID, and REMOTEDRAW_API_KEY into <dir>/.env.local instead of printing the key.",
|
|
61
|
+
"project.help.auth": "Needs a signed-in CLI (remotedraw login) or REMOTEDRAW_CLI_TOKEN.",
|
|
62
|
+
"project.error.unknownSubcommand": "Unknown project subcommand: {subcommand}. Expected create.",
|
|
63
|
+
"project.error.nameRequired": "--name is required for project create.",
|
|
64
|
+
"project.created": "Created project {name} ({id}).",
|
|
65
|
+
"project.keyWritten": "Wrote the API key to {path}.",
|
|
66
|
+
"project.keyOnce": "API key (shown once): {key}",
|
|
67
|
+
"project.keyRule": "Keep rd_sk_... keys in backend secrets only; never in browser bundles, mobile apps, or source control.",
|
|
68
|
+
"cloud.warning.clientTree": "src/remotedraw/createRemoteDrawSession.ts reads REMOTEDRAW_API_KEY and must run server-side. Move it to your API route, server action, Convex action, or serverless function before use; a browser bundle must never import it.",
|
|
69
|
+
"scan.help.usage": " remotedraw scan [--path <dir>] [--format json]",
|
|
70
|
+
"scan.help.body": "Detects web, server, and iOS projects, existing RemoteDraw wiring, and where an rd_sk_ key may live; then lists integration options and the product questions to ask the user before building.",
|
|
71
|
+
"scan.help.privacy": "Reads manifests and file names only. Reports env variable names, never values.",
|
|
72
|
+
"scan.help.json": "Pass --format json for the machine-readable report agents should read.",
|
|
73
|
+
"scan.error.missingPath": "Path does not exist: {path}",
|
|
57
74
|
"help.startHere": "Start here:",
|
|
58
75
|
// ── guide ─────────────────────────────────────────────────────────────
|
|
59
76
|
"guide.title": "RemoteDraw integration guide",
|
|
@@ -162,22 +179,22 @@ export const en = {
|
|
|
162
179
|
"choice.preset.sketch.summary": "Normalized input for your own receiver surface.",
|
|
163
180
|
"choice.preset.sketch.explanation": "Choose this when your product owns the photo, map, PDF, canvas, or other receiver UI. RemoteDraw supplies the input primitives without prescribing a component design.",
|
|
164
181
|
"choice.preset.sketch.docs": "Open sketch docs",
|
|
165
|
-
"choice.preset.photoMarkup.summary": "
|
|
166
|
-
"choice.preset.photoMarkup.explanation": "
|
|
182
|
+
"choice.preset.photoMarkup.summary": "Draw on a photo your app is already showing on a screen.",
|
|
183
|
+
"choice.preset.photoMarkup.explanation": "Your receiver renders the photo — on the desktop, laptop, or display someone is looking at — and a phone marks it up from across the room. The phone never takes or uploads the picture; your app supplies it and keeps the link with the drawings. Use this for reviewing an inspection photo at a desk, giving feedback on imagery, or annotating a screenshot.",
|
|
167
184
|
"choice.preset.photoMarkup.docs": "Open photo markup docs",
|
|
168
|
-
"choice.preset.pdfMarkup.summary": "
|
|
169
|
-
"choice.preset.pdfMarkup.explanation": "Use this for document review
|
|
185
|
+
"choice.preset.pdfMarkup.summary": "Draw on a PDF page your app is already showing on a screen.",
|
|
186
|
+
"choice.preset.pdfMarkup.explanation": "Use this for document review at a desk: your receiver renders the right page and a phone marks it. RemoteDraw renders no PDFs — your app owns the page and maps RemoteDraw coordinates onto that fixed view.",
|
|
170
187
|
"choice.preset.pdfMarkup.docs": "Open PDF markup docs",
|
|
171
|
-
"choice.preset.mapMarkup.summary": "
|
|
172
|
-
"choice.preset.mapMarkup.explanation": "Use this for routes, locations, or spatial feedback. Your app owns the map and
|
|
188
|
+
"choice.preset.mapMarkup.summary": "Draw routes and directions on the map your app is showing.",
|
|
189
|
+
"choice.preset.mapMarkup.explanation": "Use this for routes, locations, or spatial feedback marked from a phone onto a map on a desktop or wall display. Your app owns the map and the camera; RemoteDraw carries the input in geographic board space. The board's coordinateSpace.bounds is a fixed fence — size it larger than the camera you open on.",
|
|
173
190
|
"choice.preset.mapMarkup.docs": "Open map markup docs",
|
|
174
191
|
"choice.preset.screenMarkup.label": "Screen annotation",
|
|
175
192
|
"choice.preset.screenMarkup.hint": "Mark up a screen or window your receiver shares.",
|
|
176
193
|
"choice.preset.screenMarkup.summary": "Annotations on a screen or application view.",
|
|
177
|
-
"choice.preset.screenMarkup.explanation": "Use this for support, demos, and UI feedback. Your receiver supplies the screen image the phone marks are projected onto.",
|
|
194
|
+
"choice.preset.screenMarkup.explanation": "Use this for support, demos, and UI feedback: the screen being marked is the receiver's, and the phone is the pen. Your receiver supplies the screen image the phone marks are projected onto.",
|
|
178
195
|
"choice.preset.screenMarkup.docs": "Open screen markup docs",
|
|
179
196
|
"choice.preset.designReview.summary": "Focused visual feedback on a design.",
|
|
180
|
-
"choice.preset.designReview.explanation": "Use this for design reviews with arrows, shapes, and freehand lines. Your product owns comments, versions, and decisions.",
|
|
197
|
+
"choice.preset.designReview.explanation": "Use this for design reviews with arrows, shapes, and freehand lines drawn from a phone onto the design open on a reviewer's screen. Your product owns comments, versions, and decisions.",
|
|
181
198
|
"choice.preset.designReview.docs": "Open design review docs",
|
|
182
199
|
"choice.preset.pointer.summary": "Live pointing without persistent ink.",
|
|
183
200
|
"choice.preset.pointer.explanation": "Use this for presentations and guidance where the phone acts as a pointer and movement matters more than stored ink.",
|
|
@@ -212,7 +229,6 @@ export const en = {
|
|
|
212
229
|
"wizard.control.back": "Esc back",
|
|
213
230
|
"wizard.control.quitCtrlC": "Ctrl+C quit",
|
|
214
231
|
"wizard.control.quitQ": "q quit",
|
|
215
|
-
"wizard.control.move": "↑/↓ move",
|
|
216
232
|
"wizard.control.steps": "←/→ steps",
|
|
217
233
|
"wizard.control.choose": "↑/↓ choose",
|
|
218
234
|
"wizard.control.docs": "d open docs",
|
|
@@ -230,7 +246,6 @@ export const en = {
|
|
|
230
246
|
"wizard.tagline.setup": "phone → canvas · project setup",
|
|
231
247
|
"wizard.tagline": "phone → canvas",
|
|
232
248
|
"wizard.docs.openFailed": "Could not open the documentation. Use {url}",
|
|
233
|
-
"wizard.docs.button": "[ {label} ↗ ]",
|
|
234
249
|
// ── new / init ────────────────────────────────────────────────────────
|
|
235
250
|
"new.help.usage.bare": " remotedraw new",
|
|
236
251
|
"new.help.usage.named": " remotedraw new --app-name <name> [options]",
|
|
@@ -302,7 +317,7 @@ export const en = {
|
|
|
302
317
|
"doctor.sdk.ok": "{package} is listed in package.json dependencies.",
|
|
303
318
|
"doctor.sdk.missing": "Add {package}@{range} to dependencies or rerun remotedraw init.",
|
|
304
319
|
"doctor.apiKey.ok": "Server-side API key shape looks correct.",
|
|
305
|
-
"doctor.apiKey.missing": "
|
|
320
|
+
"doctor.apiKey.missing": "No rd_sk_... key found in this project's environment (REMOTEDRAW_API_KEY). When you add one, keep it in backend secrets only.",
|
|
306
321
|
"doctor.label.deployment": "deployment",
|
|
307
322
|
"doctor.label.deploymentReachable": "deployment reachable",
|
|
308
323
|
"doctor.label.apiKeyAccepted": "API key accepted",
|