@remotedraw/cli 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/dist/cli.d.ts +17 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +79 -23
- 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 +8 -8
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/en.js +8 -8
- package/dist/locales/index.d.ts +8 -8
- package/dist/locales/nl.d.ts.map +1 -1
- package/dist/locales/nl.js +7 -7
- 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 +19 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +22 -3
- 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
package/README.md
CHANGED
|
@@ -7,17 +7,25 @@ for your chosen stack.
|
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
|
-
RemoteDraw requires Node.js 20 or newer.
|
|
10
|
+
RemoteDraw requires Node.js 20 or newer. Use whichever package manager your
|
|
11
|
+
project already uses — the CLI reads the project's lockfile and `packageManager`
|
|
12
|
+
field, and prints its own next steps in that manager's commands.
|
|
11
13
|
|
|
12
14
|
```sh
|
|
13
|
-
npm install
|
|
15
|
+
npm install -g @remotedraw/cli # npm
|
|
16
|
+
pnpm add -g @remotedraw/cli # pnpm
|
|
17
|
+
bun add -g @remotedraw/cli # bun
|
|
14
18
|
remotedraw --version
|
|
15
19
|
```
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
Yarn Berry has no global install, so run it straight from the registry — which
|
|
22
|
+
also works everywhere else, when you would rather not install anything:
|
|
18
23
|
|
|
19
24
|
```sh
|
|
20
|
-
|
|
25
|
+
yarn dlx @remotedraw/cli # Yarn
|
|
26
|
+
npx @remotedraw/cli@latest # npm
|
|
27
|
+
pnpm dlx @remotedraw/cli # pnpm
|
|
28
|
+
bunx @remotedraw/cli # bun
|
|
21
29
|
```
|
|
22
30
|
|
|
23
31
|
## Start here
|
|
@@ -159,8 +167,10 @@ remotedraw update # install the latest published version
|
|
|
159
167
|
remotedraw update --check # report it without installing
|
|
160
168
|
```
|
|
161
169
|
|
|
162
|
-
`update` detects whether the binary came from npm, pnpm, yarn, or bun and
|
|
163
|
-
|
|
170
|
+
`update` detects whether the binary came from npm, pnpm, yarn, or bun — and
|
|
171
|
+
whether it was run through `npx`/`pnpm dlx`/`yarn dlx`/`bunx`, which fetch the
|
|
172
|
+
latest release every time and so have nothing to update — then runs that
|
|
173
|
+
manager's own command. Other commands check the registry at most once a day and
|
|
164
174
|
print a one-line notice when a newer version exists; that notice only appears on
|
|
165
175
|
an interactive terminal, and `REMOTEDRAW_CLI_NO_UPDATE_CHECK=1`, `CI`, or
|
|
166
176
|
`NO_UPDATE_NOTIFIER` disable it.
|
package/dist/cli.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { type WizardDefinition, type WizardTerminal } from "./setup-wizard.js";
|
|
|
2
2
|
import { type CloudRuntime } from "./cloud.js";
|
|
3
3
|
import { type UpdateRuntime } from "./update.js";
|
|
4
4
|
export declare const CLI_VERSION: string;
|
|
5
|
-
export declare const REMOTEDRAW_SDK_RANGE = "^0.
|
|
6
|
-
export declare const REMOTEDRAW_CLI_RANGE = "^0.
|
|
5
|
+
export declare const REMOTEDRAW_SDK_RANGE = "^0.3.0";
|
|
6
|
+
export declare const REMOTEDRAW_CLI_RANGE = "^0.3.0";
|
|
7
7
|
export type CliResult = {
|
|
8
8
|
exitCode: number;
|
|
9
9
|
stdout: string;
|
|
@@ -64,7 +64,9 @@ export declare function createRemoteDrawProject(plan: IntegrationPlan, outputDir
|
|
|
64
64
|
dryRun?: boolean;
|
|
65
65
|
force?: boolean;
|
|
66
66
|
}): Promise<string[]>;
|
|
67
|
-
export declare function projectSetupDefinition(
|
|
67
|
+
export declare function projectSetupDefinition(detected?: {
|
|
68
|
+
packageManager?: PackageManagerChoice;
|
|
69
|
+
}): WizardDefinition;
|
|
68
70
|
type PayloadOptions = {
|
|
69
71
|
preset: PresetChoice;
|
|
70
72
|
label: string;
|
|
@@ -76,8 +78,6 @@ export declare function createSessionPayload(options: PayloadOptions): {
|
|
|
76
78
|
externalId: string;
|
|
77
79
|
markupPreset: "approval" | "sketch" | "photoMarkup" | "pdfMarkup" | "mapMarkup" | "screenMarkup" | "designReview" | "pointer";
|
|
78
80
|
target: {
|
|
79
|
-
kind: string;
|
|
80
|
-
label: string;
|
|
81
81
|
metadata: {
|
|
82
82
|
descriptor: {
|
|
83
83
|
version: number;
|
|
@@ -107,6 +107,18 @@ export declare function createSessionPayload(options: PayloadOptions): {
|
|
|
107
107
|
};
|
|
108
108
|
};
|
|
109
109
|
};
|
|
110
|
+
coordinateSpace?: {
|
|
111
|
+
width: number;
|
|
112
|
+
height: number;
|
|
113
|
+
bounds: {
|
|
114
|
+
minX: number;
|
|
115
|
+
minY: number;
|
|
116
|
+
maxX: number;
|
|
117
|
+
maxY: number;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
kind: string;
|
|
121
|
+
label: string;
|
|
110
122
|
};
|
|
111
123
|
};
|
|
112
124
|
/**
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,gBAAgB,EAGrB,KAAK,cAAc,EAEpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAaL,KAAK,YAAY,EAElB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,gBAAgB,EAGrB,KAAK,cAAc,EAEpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAaL,KAAK,YAAY,EAElB,MAAM,YAAY,CAAC;AA0BpB,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAMrB,eAAO,MAAM,WAAW,QAGT,CAAC;AAQhB,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAC7C,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAE7C,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAIF,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IACpD,KAAK,EAAE,CAAC,CAAC;IACT,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,IAAI;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,CAAC,CAAC;IACjB,OAAO,EAAE,SAAS,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,EAAE,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CAC3E,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,YAAY,GACnC,aAAa,GAAG;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,cAAc,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CAC7C,CAAC;AAEJ,eAAO,MAAM,aAAa,gDAAiD,CAAC;AAC5E,eAAO,MAAM,aAAa,oEAKhB,CAAC;AACX,eAAO,MAAM,UAAU,yDAMb,CAAC;AACX,QAAA,MAAM,aAAa,qHAST,CAAC;AAOX,QAAA,MAAM,qBAAqB,yCAAoB,CAAC;AAEhD,KAAK,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AACnD,KAAK,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AACnD,KAAK,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAC7C,KAAK,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,KAAK,oBAAoB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnE,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,oBAAoB,CAAC;CACtC,CAAC;AAaF,wBAAgB,iBAAiB,IAAI,UAAU,CAgC9C;AAED,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EACvC,MAAM,GAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAoB,sCASnD;AAsND,wBAAsB,MAAM,CAC1B,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,UAAgC,GACxC,OAAO,CAAC,SAAS,CAAC,CAuGpB;AAuhBD,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,eAAe,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,UAAU,EACnB,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,qBAOpD;AA0pDD,wBAAgB,sBAAsB,CACpC,QAAQ,GAAE;IAAE,cAAc,CAAC,EAAE,oBAAoB,CAAA;CAAO,GACvD,gBAAgB,CAsDlB;AAypCD,KAAK,cAAc,GAAG;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,CAAC;AAuCF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB3D;AA+FD;;;;;GAKG;AACH,wBAAgB,kBAAkB,WAEjC"}
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createWizardTerminal, runSetupWizard, } from "./setup-wizard.js";
|
|
|
11
11
|
import { assertEnvCanAcceptProvisioning, currentCliAccount, DEFAULT_REMOTEDRAW_API_BASE_URL, globalConfigPath, loginToRemoteDraw, logoutFromRemoteDraw, provisionRemoteDrawProject, updateEnvContents, readStoredLanguage, remoteDrawApiBaseUrl, writeStoredLanguage, writeProvisionedProjectSetup, } from "./cloud.js";
|
|
12
12
|
import { LANGUAGE_ENV_VARS, SOURCE_LOCALE, extractLanguageFlag, languageChoices, languagePromptMessage, localeEndonym, normalizeLocale, resolveLocale, setActiveLocale, supportedLocaleList, SUPPORTED_LOCALES, systemLocaleHint, t, } from "./i18n.js";
|
|
13
13
|
import { CliError, cliError } from "./errors.js";
|
|
14
|
+
import { detectPackageManager, packageManagerCommands, packageManagerIds, } from "./packageManagers.js";
|
|
14
15
|
import { AGENT_SKILL_MARKDOWN } from "./generated/agent-skill.js";
|
|
15
16
|
import { reportCliCrash } from "./telemetry.js";
|
|
16
17
|
import { checkForUpdate, CLI_PACKAGE_NAME, detectInstallMethod, installMethodDisplay, runInstall, } from "./update.js";
|
|
@@ -24,8 +25,8 @@ export const CLI_VERSION = typeof packageMetadata.version === "string"
|
|
|
24
25
|
// private, and would silently accept a future breaking major once they were
|
|
25
26
|
// not. tests/generated-manifest.test.ts cross-checks every range below against
|
|
26
27
|
// the workspace manifests.
|
|
27
|
-
export const REMOTEDRAW_SDK_RANGE = "^0.
|
|
28
|
-
export const REMOTEDRAW_CLI_RANGE = "^0.
|
|
28
|
+
export const REMOTEDRAW_SDK_RANGE = "^0.3.0";
|
|
29
|
+
export const REMOTEDRAW_CLI_RANGE = "^0.3.0";
|
|
29
30
|
export const targetChoices = ["web", "desktop", "ios", "headless"];
|
|
30
31
|
export const senderChoices = [
|
|
31
32
|
"remotedraw-ios",
|
|
@@ -56,7 +57,7 @@ const exampleChoices = [
|
|
|
56
57
|
"raw-http",
|
|
57
58
|
"ios-owned-sender",
|
|
58
59
|
];
|
|
59
|
-
const packageManagerChoices =
|
|
60
|
+
const packageManagerChoices = packageManagerIds;
|
|
60
61
|
export function createNodeRuntime() {
|
|
61
62
|
const cwd = process.cwd();
|
|
62
63
|
const env = cliEnvironmentForWorkspace(cwd, process.env);
|
|
@@ -397,7 +398,9 @@ async function setupWizardCommand(runtime, askLanguage) {
|
|
|
397
398
|
await loginToRemoteDraw(runtime, apiBaseUrl);
|
|
398
399
|
}
|
|
399
400
|
const result = await runSetupWizard({
|
|
400
|
-
definition: projectSetupDefinition(
|
|
401
|
+
definition: projectSetupDefinition({
|
|
402
|
+
packageManager: await projectPackageManager(runtime.cwd, runtime),
|
|
403
|
+
}),
|
|
401
404
|
terminal,
|
|
402
405
|
execute: async (values) => {
|
|
403
406
|
const plan = planFromWizardValues(values);
|
|
@@ -763,6 +766,9 @@ async function newCommand(args, runtime, askLanguage = false) {
|
|
|
763
766
|
: planFromArgs(parsed, {
|
|
764
767
|
appNameRequired: true,
|
|
765
768
|
defaultAppName: "RemoteDraw app",
|
|
769
|
+
defaults: {
|
|
770
|
+
packageManager: await projectPackageManager(runtime.cwd, runtime),
|
|
771
|
+
},
|
|
766
772
|
});
|
|
767
773
|
const outputDir = resolveOutputDir(runtime.cwd, parsed, plan.slug);
|
|
768
774
|
const dryRun = parsed.booleans.has("dry-run");
|
|
@@ -816,6 +822,9 @@ async function initCommand(args, runtime) {
|
|
|
816
822
|
const plan = planFromArgs(parsed, {
|
|
817
823
|
appNameRequired: false,
|
|
818
824
|
defaultAppName,
|
|
825
|
+
defaults: {
|
|
826
|
+
packageManager: await projectPackageManager(outputDir, runtime),
|
|
827
|
+
},
|
|
819
828
|
});
|
|
820
829
|
const dryRun = parsed.booleans.has("dry-run");
|
|
821
830
|
const force = parsed.booleans.has("force");
|
|
@@ -958,7 +967,7 @@ async function updateCommand(args, runtime) {
|
|
|
958
967
|
return ok(updateHelpText());
|
|
959
968
|
const format = outputFormat(parsed);
|
|
960
969
|
const checkOnly = parsed.booleans.has("check");
|
|
961
|
-
const method = detectInstallMethod(runtime.binPath ?? "");
|
|
970
|
+
const method = detectInstallMethod(runtime.binPath ?? "", runtime.env);
|
|
962
971
|
const command = installMethodDisplay(method);
|
|
963
972
|
// `remotedraw update` is an explicit request, so it ignores the cache and the
|
|
964
973
|
// ambient opt-outs that only silence the passive notice.
|
|
@@ -1268,7 +1277,10 @@ async function examplesCommand(args, runtime) {
|
|
|
1268
1277
|
const plan = planFromArgs(parsed, {
|
|
1269
1278
|
appNameRequired: false,
|
|
1270
1279
|
defaultAppName: readString(parsed, "app-name") ?? titleCase(installedExample),
|
|
1271
|
-
defaults
|
|
1280
|
+
defaults: {
|
|
1281
|
+
packageManager: await projectPackageManager(runtime.cwd, runtime),
|
|
1282
|
+
...defaults,
|
|
1283
|
+
},
|
|
1272
1284
|
});
|
|
1273
1285
|
const files = newProjectFiles(plan);
|
|
1274
1286
|
const dryRun = parsed.booleans.has("dry-run");
|
|
@@ -1640,10 +1652,12 @@ function shouldPromptForNewProject(parsed, runtime) {
|
|
|
1640
1652
|
}
|
|
1641
1653
|
async function promptForNewProject(parsed, runtime) {
|
|
1642
1654
|
const prompts = runtime.prompts;
|
|
1655
|
+
const detectedPackageManager = await projectPackageManager(runtime.cwd, runtime);
|
|
1643
1656
|
if (!prompts) {
|
|
1644
1657
|
return planFromArgs(parsed, {
|
|
1645
1658
|
appNameRequired: true,
|
|
1646
1659
|
defaultAppName: "RemoteDraw app",
|
|
1660
|
+
defaults: { packageManager: detectedPackageManager },
|
|
1647
1661
|
});
|
|
1648
1662
|
}
|
|
1649
1663
|
await prompts.intro?.(t("prompt.intro.setup"));
|
|
@@ -1676,7 +1690,7 @@ async function promptForNewProject(parsed, runtime) {
|
|
|
1676
1690
|
const packageManager = readString(parsed, "package-manager") == null
|
|
1677
1691
|
? await prompts.select({
|
|
1678
1692
|
message: t("field.packageManager"),
|
|
1679
|
-
defaultValue:
|
|
1693
|
+
defaultValue: detectedPackageManager,
|
|
1680
1694
|
choices: packageManagerPromptChoices(),
|
|
1681
1695
|
})
|
|
1682
1696
|
: choice(readString(parsed, "package-manager"), packageManagerChoices, "package-manager");
|
|
@@ -2131,7 +2145,7 @@ function agentOptionCatalog() {
|
|
|
2131
2145
|
},
|
|
2132
2146
|
};
|
|
2133
2147
|
}
|
|
2134
|
-
export function projectSetupDefinition() {
|
|
2148
|
+
export function projectSetupDefinition(detected = {}) {
|
|
2135
2149
|
return {
|
|
2136
2150
|
defaults: {
|
|
2137
2151
|
appName: "RemoteDraw App",
|
|
@@ -2139,7 +2153,8 @@ export function projectSetupDefinition() {
|
|
|
2139
2153
|
sender: "remotedraw-ios",
|
|
2140
2154
|
sdk: "react",
|
|
2141
2155
|
preset: "sketch",
|
|
2142
|
-
|
|
2156
|
+
// npm only when the project has not already answered this itself.
|
|
2157
|
+
packageManager: detected.packageManager ?? "npm",
|
|
2143
2158
|
},
|
|
2144
2159
|
fields: projectSetupFields(),
|
|
2145
2160
|
normalize(values, changed) {
|
|
@@ -2622,6 +2637,7 @@ function reactBackendSessionTs(plan) {
|
|
|
2622
2637
|
"const apiBaseUrl = process.env.REMOTEDRAW_API_BASE_URL;",
|
|
2623
2638
|
"const apiKey = process.env.REMOTEDRAW_API_KEY;",
|
|
2624
2639
|
"",
|
|
2640
|
+
...mapBoundsNote(plan.preset),
|
|
2625
2641
|
"export function createRemoteDrawSessionRequest(): CreateSessionRequest {",
|
|
2626
2642
|
" return " +
|
|
2627
2643
|
JSON.stringify(createSessionPayload({
|
|
@@ -2814,6 +2830,7 @@ function svelteBackendSessionTs(plan) {
|
|
|
2814
2830
|
"const apiBaseUrl = process.env.REMOTEDRAW_API_BASE_URL;",
|
|
2815
2831
|
"const apiKey = process.env.REMOTEDRAW_API_KEY;",
|
|
2816
2832
|
"",
|
|
2833
|
+
...mapBoundsNote(plan.preset),
|
|
2817
2834
|
"export function createRemoteDrawSessionRequest(): CreateSessionRequest {",
|
|
2818
2835
|
" return " +
|
|
2819
2836
|
JSON.stringify(createSessionPayload({
|
|
@@ -2901,6 +2918,7 @@ function rawHttpSessionTs(plan) {
|
|
|
2901
2918
|
"const apiBaseUrl = process.env.REMOTEDRAW_API_BASE_URL;",
|
|
2902
2919
|
"const apiKey = process.env.REMOTEDRAW_API_KEY;",
|
|
2903
2920
|
"",
|
|
2921
|
+
...mapBoundsNote(plan.preset),
|
|
2904
2922
|
`export function createRemoteDrawSessionRequest()${typed ? ": CreateSessionRequest" : ""} {`,
|
|
2905
2923
|
" return " +
|
|
2906
2924
|
JSON.stringify(createSessionPayload({
|
|
@@ -3188,6 +3206,42 @@ function reactViteConfig() {
|
|
|
3188
3206
|
"",
|
|
3189
3207
|
].join("\n");
|
|
3190
3208
|
}
|
|
3209
|
+
/**
|
|
3210
|
+
* The example fence a map preset ships with.
|
|
3211
|
+
*
|
|
3212
|
+
* A map session's `coordinateSpace.bounds` is what every point on the wire is
|
|
3213
|
+
* normalized against — `x` linear in longitude, `y` linear in Web Mercator — and
|
|
3214
|
+
* it cannot be changed once the session exists. Omitting it does not mean "the
|
|
3215
|
+
* customer's map": it means the server's own default region, which is how
|
|
3216
|
+
* `remotedraw init --preset mapMarkup` used to produce a phone showing New York
|
|
3217
|
+
* whatever the customer's map was pointed at.
|
|
3218
|
+
*
|
|
3219
|
+
* So the preset states one, explicitly and visibly, with the note above it in
|
|
3220
|
+
* the generated file. `width`/`height` are that region's Mercator aspect.
|
|
3221
|
+
*/
|
|
3222
|
+
const MAP_PRESET_COORDINATE_SPACE = {
|
|
3223
|
+
width: 1600,
|
|
3224
|
+
height: 1310,
|
|
3225
|
+
bounds: { minX: 4.85, minY: 52.35, maxX: 4.95, maxY: 52.4 },
|
|
3226
|
+
};
|
|
3227
|
+
/**
|
|
3228
|
+
* The comment that has to travel with the bounds. A map board's fence is the
|
|
3229
|
+
* one decision this scaffold cannot walk back for the customer.
|
|
3230
|
+
*/
|
|
3231
|
+
function mapBoundsNote(preset) {
|
|
3232
|
+
if (PRESETS[preset].kind !== "map")
|
|
3233
|
+
return [];
|
|
3234
|
+
return [
|
|
3235
|
+
"// `target.coordinateSpace.bounds` is this board's geographic fence: every",
|
|
3236
|
+
"// point on the wire is normalized against it (x linear in longitude, y",
|
|
3237
|
+
"// linear in Web Mercator). It is FIXED for the life of the session — no",
|
|
3238
|
+
"// route changes it — so size it LARGER than the camera you open on, or a",
|
|
3239
|
+
"// sender that pans is drawing at the edge of the world. Replace the example",
|
|
3240
|
+
"// region below with yours; padMapBounds(camera, 1) from @remotedraw/geometry",
|
|
3241
|
+
"// grows a camera into a fence. width/height are that fence's Mercator aspect.",
|
|
3242
|
+
"",
|
|
3243
|
+
];
|
|
3244
|
+
}
|
|
3191
3245
|
export function createSessionPayload(options) {
|
|
3192
3246
|
const descriptor = descriptorForPreset(options.preset, options.label);
|
|
3193
3247
|
return {
|
|
@@ -3196,6 +3250,9 @@ export function createSessionPayload(options) {
|
|
|
3196
3250
|
target: {
|
|
3197
3251
|
kind: PRESETS[options.preset].kind,
|
|
3198
3252
|
label: options.label,
|
|
3253
|
+
...(PRESETS[options.preset].kind === "map"
|
|
3254
|
+
? { coordinateSpace: MAP_PRESET_COORDINATE_SPACE }
|
|
3255
|
+
: {}),
|
|
3199
3256
|
metadata: {
|
|
3200
3257
|
descriptor,
|
|
3201
3258
|
},
|
|
@@ -3480,22 +3537,21 @@ function statusLine(state, label, message) {
|
|
|
3480
3537
|
return `[${state}] ${label}: ${message}`;
|
|
3481
3538
|
}
|
|
3482
3539
|
function installCommand(packageManager) {
|
|
3483
|
-
|
|
3484
|
-
return "npm install";
|
|
3485
|
-
if (packageManager === "pnpm")
|
|
3486
|
-
return "pnpm install";
|
|
3487
|
-
if (packageManager === "yarn")
|
|
3488
|
-
return "yarn install";
|
|
3489
|
-
return "bun install";
|
|
3540
|
+
return packageManagerCommands[packageManager].install;
|
|
3490
3541
|
}
|
|
3491
3542
|
function devCommandForPackageManager(packageManager) {
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3543
|
+
return `${packageManagerCommands[packageManager].run} dev`;
|
|
3544
|
+
}
|
|
3545
|
+
/**
|
|
3546
|
+
* The manager the project already uses, not the one we happen to prefer.
|
|
3547
|
+
* Scaffolding into a pnpm repo and then printing `npm install` is how a second
|
|
3548
|
+
* lockfile gets born.
|
|
3549
|
+
*/
|
|
3550
|
+
async function projectPackageManager(dir, runtime) {
|
|
3551
|
+
const detected = await detectPackageManager(dir, runtime, {
|
|
3552
|
+
pathModule: path,
|
|
3553
|
+
});
|
|
3554
|
+
return detected.manager;
|
|
3499
3555
|
}
|
|
3500
3556
|
function titleCase(value) {
|
|
3501
3557
|
return value
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const AGENT_SKILL_MARKDOWN = "---\nname: remotedraw\ndescription: Integrate RemoteDraw phone drawing into a customer's product \u2014 scan the codebase, ask the product questions, get approval before installing, then wire the receiver and the sender with the SDK's own components (React/Svelte/JS receiver, hosted web sender, RemoteDrawSenderKit full-screen iOS sender).\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\n## What RemoteDraw is, in four sentences\n\nA phone becomes a pen for a screen. Your **backend** creates a **session** with\nan `rd_sk_` API key; your **receiver** (web, desktop, or any client) shows the\nsession's ink using a `rd_recv_` token; a **sender** (the hosted `/join` page\nthat any QR opens, the RemoteDraw iOS app, or your own iOS app through\n`RemoteDrawSenderKit`) draws with a `rd_send_` token. Strokes stream as drafts\nwhile the finger is down and commit on release; nothing runs on the customer's\ninfrastructure except the session-creation call.\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`,\n and the SwiftPM package `https://github.com/AxioSOzo/remotedraw-swift.git`\n (product `RemoteDrawSenderKit`).\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 (`npm install -g @remotedraw/cli`, `@remotedraw/react`, a Swift package, a\n dashboard project + key) and why, then wait. This includes `remotedraw init`\n without `--offline`, which provisions a billable project and key.\n3. **Scan the codebase.** `remotedraw scan --format json` (or\n `npx @remotedraw/cli@latest scan --format json` before the CLI is installed)\n reports the web/server/iOS projects, where an `rd_sk_` key may live, any\n RemoteDraw wiring already present, the integration options that fit, and\n the product questions to ask. Read it; verify its `evidence` where it\n matters.\n4. **Ask the product questions.** They come back in the scan's `questions`\n array. The ones that decide the build: what is drawn on, which existing\n screen the receiver goes on, who holds the phone (signed-in user \u2192 direct\n sender, anyone \u2192 QR), what Submit does, and demo vs. real feature. 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; never delete\n or replace a host-app feature on your own initiative.\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\n## Choosing the pieces\n\nReceiver (the screen that shows ink):\n\n| Host | Use | Not |\n| --- | --- | --- |\n| React / Next / Remix | `@remotedraw/react`: `RemoteDrawProvider`, `RemoteDrawReceiver`, `PairingCode`, `RemoteDrawSessionControls` | A hand-rolled SVG or polling loop |\n| Svelte / SvelteKit | `@remotedraw/svelte` receiver store | \u2014 |\n| Anything else with JS | `@remotedraw/client`: `createHttpReceiverClient`, `createRealtimeReceiverSource` | \u2014 |\n| No JS at all | Raw HTTP `POST /v1/receiver/*` with the receiver token | \u2014 |\n\nSender (the phone):\n\n| Situation | Use | Not |\n| --- | --- | --- |\n| No phone app in the product | **Hosted `/join` page.** Render `joinUrl` as a QR (`PairingCode`). Full-screen, polished, zero sender code; the RemoteDraw iOS app opens the same link. | An in-page web sender (`--sender embedded-web`) unless the user explicitly wants drawing inside their own web page |\n| The product has its own iOS app | **`RemoteDrawSenderKit`** full-screen surface (below). Backend mints `rd_send_` via `POST /v1/sessions/direct-sender` for a signed-in user; QR join stays as the fallback. | A custom canvas, a raw-HTTP Swift client, or `remotedraw init --sdk swift` when adding a SwiftPM dependency is possible |\n| Headless / tests | `POST /v1/join` \u2192 `/v1/sender/draft` \u2192 `/v1/sender/commit` | \u2014 |\n\nSession creation (`POST /v1/sessions`) needs the `rd_sk_` key and therefore\nruns only 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## iOS: the bar\n\nThe reference is the RemoteDraw app's own drawing screen, and the SDK ships it.\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// Once, at app start.\nRemoteDraw.configure(.init(apiBaseURL: .production))\n\n// Wherever drawing starts. `token` is the rd_send_ string your backend\n// minted with POST /v1/sessions/direct-sender (a QR's rd_join_ works too).\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: break\n case .expired: refreshSession()\n case .failed(let error): report(error)\n }\n }\n```\n\nRules:\n\n- The drawing surface is **full screen**: `.remoteDrawSurface` (a\n `fullScreenCover`) or `RemoteDrawTakeover` inside your own cover or\n `UIHostingController`. A sheet is acceptable only if `RemoteDrawSurface`\n fills it edge to edge; the surface then owns the gestures and the sheet must\n not be draggable while drawing. There is no small-canvas option.\n- The user can always leave \u2014 the surface has its own exit; `RemoteDrawExit`\n only decides whether leaving with unsubmitted ink asks first \u2014 and the host\n gets an outcome. Submit, undo, clear, instruments, and paper are the SDK's.\n- Do not write a `UIViewRepresentable` canvas, a draft loop, or an HTTP client:\n cadence, point budgets, the packed-point codec, sequence healing, token\n refresh, and presence are protocol, and `RemoteDrawSenderSession` already\n implements them. If you must go headless, compose `RemoteDrawInkCanvas` +\n `RemoteDrawStrokeCapture` on that session \u2014 never raw `URLSession`.\n- `RemoteDraw.configure` is the only setup; the SDK needs no `Info.plist` keys.\n- Customers do not ship a separate RemoteDraw app; their app *is* the sender.\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\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. Never pass `--force`\nunless the user approved overwriting. Do not drive the interactive wizard or\nscrape human-formatted output; every command has `--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\n check it (`POST /v1/sessions/get`) before attaching a sender or ending a\n session on 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\n in a server action / loader. Store the `receiverToken` if the receiver\n outlives a 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. A connected\n phone that enters viewport mode is drawn on the board as a phone outline; pass\n `phones={false}` for a plain surface where that is not wanted.\n- `PairingCode` hides the join URL text unless `showLink`; pass it when a human\n needs the link without a camera.\n- `joinTokenExpiresAt` is earlier than the session's `expiresAt`: the QR dies\n first, the board stays live.\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\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 `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. See https://docs.remotedraw.com/docs/api#ai.\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**),\n`refreshJoinToken` (`({ sessionId }) => Promise<result | null>` \u2014 the QR re-arm.\nThe join token lives 10 minutes; the session usually outlives it. Point this at\na backend route that calls `POST /v1/sessions/join-token` and return the\nresponse as-is; when the token dies on a live, unpaired session the provider\nswaps in the fresh code and every pairing component follows. Without it a stale\nQR shows \"Link expired\" until the host recreates the whole session \u2014 wire it in\nany integration where a board can sit unpaired for more than 10 minutes).\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 \"expired\" status is real and\nterminal unless the provider has `refreshJoinToken` wired \u2014 the component shows\nthe death of the 10-minute join token but cannot mint a replacement itself. The\nanimated optical field that `variant=\"aurora\"` once selected is now the separate\nEXPERIMENTAL `AuroraPairingField`, decodable only by the RemoteDraw app's own\nscanner.\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,o4mDAAsplD,CAAC"}
|