@botim/mp-debug-sdk 0.5.2 → 0.6.2

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 CHANGED
@@ -8,15 +8,20 @@ Mini-programs run on user devices in environments you can't easily attach a debu
8
8
 
9
9
  - **Live logs** of `console.*`, `fetch`, `XMLHttpRequest`, and uncaught errors.
10
10
  - **AI-observable sessions** — every event indexed by `(miniProgramId, deviceId, sid)` so resolver agents can pull errors and reproduce bugs without a human in the loop.
11
- - **Safe AI command channel** — agents can `reload`, `dump-state`, `set-feature-flag`, `screenshot`, `ping`, or any custom command you allow. Anything not registered is rejected.
11
+ - **Safe AI command channel** — agents can `reload`, `dump-state`, `set-feature-flag`, `screenshot`, `ping`, `exec` (default-on remote REPL), or any custom command you allow. Anything not registered is rejected.
12
12
  - **Built-in redaction** before the event ever enters the in-memory buffer.
13
13
 
14
14
  ## Install
15
15
 
16
+ > **Heads-up — registry override required in BOTIM repos.**
17
+ > Most BOTIM mini-program repos pin the `@botim` scope to the internal Artifactory mirror (`@botim:registry=https://artifactory.corp.algento.com/artifactory/api/npm/bot-npm/`). This SDK is **published to public npm**, so a plain `npm install @botim/mp-debug-sdk` in those repos will hit Artifactory, not find the package, and 404. Use the public registry explicitly for this one package:
18
+
16
19
  ```bash
17
- npm install @botim/mp-debug-sdk
20
+ npm install @botim/mp-debug-sdk --registry=https://registry.npmjs.org/
18
21
  ```
19
22
 
23
+ If your repo doesn't have an `@botim` scope override (rare), the flag is harmless — npm uses `registry.npmjs.org` by default. **Don't** add `@botim:registry=https://registry.npmjs.org/` to your `.npmrc`: it would shadow the internal Artifactory registry that other `@botim/*` packages (mp-framework, etc.) need.
24
+
20
25
  ## 1. Add the env config files
21
26
 
22
27
  Each environment of your mini-program ships with one config file at the project root, in the standard BOTIM mini-program schema:
@@ -128,7 +133,58 @@ The SDK posts directly to `endpoint` — there's no proxy required. Your relay m
128
133
  CORS_ORIGINS=https://my-mp.example.com,https://staging.example.com
129
134
  ```
130
135
 
131
- ## 5. Custom commands (optional)
136
+ ## 5. Remote REPL (`exec`) — default-on
137
+
138
+ Once `enableRemoteDebug` returns, an attached agent can send a JS snippet to the device and read back the value, captured `console.*` output, or thrown error. No host wiring needed.
139
+
140
+ ```bash
141
+ # From any terminal that can reach your relay:
142
+ curl -sX POST "https://debug.botim.dev/v1/mp/<MP_ID>/devices/<DEVICE_ID>/commands" \
143
+ -H 'content-type: application/json' \
144
+ -d '{"name":"exec","args":{"code":"console.log(\"hi\"); return window.location.href"}}'
145
+ ```
146
+
147
+ Result event:
148
+
149
+ ```jsonc
150
+ {
151
+ "type": "command-ack",
152
+ "payload": {
153
+ "command": "exec",
154
+ "ok": true,
155
+ "result": {
156
+ "value": "https://my-mp.example.com/page",
157
+ "logs": [{"method":"log","args":["hi"],"ts": 1735324800123}],
158
+ "durationMs": 2
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ Inside the snippet, `BOT`, `window`, and `document` are bound as locals (with `BOT` resolved best-effort from `window.BOT` or Angular DI; `null` if unavailable). Top-level `await` works. Code is capped at 8 KB and 30 s.
165
+
166
+ **To opt out** (e.g. you ship a non-debug release that still uses the SDK for telemetry only):
167
+
168
+ ```ts
169
+ await enableRemoteDebug({
170
+ // ...,
171
+ builtins: { exec: false },
172
+ });
173
+ ```
174
+
175
+ **To inject extra locals** for your app:
176
+
177
+ ```ts
178
+ await enableRemoteDebug({
179
+ // ...,
180
+ builtins: { exec: { globals: { app: myApp, store: myStore } } },
181
+ });
182
+ // agent can then call: { code: "return store.getState().user" }
183
+ ```
184
+
185
+ > Pre-buffer redaction (headers, JWT/long-token regex) is applied to `result.value` and `result.logs` before they leave the device buffer. The existing prod consent gate (`hostToken` or `userOptIn`) gates whether attach happens at all. See `docs/recipes/exec.md` for deeper notes.
186
+
187
+ ## 6. Custom commands (optional)
132
188
 
133
189
  Any AI agent with the right scope can request a command. The SDK only runs commands registered via `registerCommand`:
134
190
 
@@ -147,7 +203,7 @@ handle.registerCommand('set-locale', async (args) => {
147
203
 
148
204
  Anything not registered gets a `command-rejected` event with reason `unknown-command`.
149
205
 
150
- ## 6. Lifecycle
206
+ ## 7. Lifecycle
151
207
 
152
208
  ```ts
153
209
  await handle.flush(); // force-send buffered events
@@ -186,6 +242,16 @@ Once your build is wired and shipped, see **[`docs/live-debugging.md`](./docs/li
186
242
  | `RemoteDebugHandle.flush()` | Force-flush the in-memory buffer. |
187
243
  | `RemoteDebugHandle.stop()` | Uninstall and drain the queue. |
188
244
 
245
+ ## AI debug skill (Claude Code)
246
+
247
+ A Claude Code skill that teaches AI agents how to wire and consume this SDK lives at `.claude/skills/botim-debug-relay/SKILL.md`. Open Claude Code from this repo and the skill auto-loads — no setup needed. To use it from anywhere on your machine:
248
+
249
+ ```bash
250
+ cp -r .claude/skills/botim-debug-relay ~/.claude/skills/
251
+ ```
252
+
253
+ > **The file in this repo is a mirror.** The canonical copy lives in `botim-debug-relay/.claude/skills/botim-debug-relay/SKILL.md`. **Do not edit the SDK-side copy directly** — your changes will be overwritten by the next sync. Edit in the relay repo, then run `bash bin/sync-skill.sh` from that repo to propagate here.
254
+
189
255
  ## License
190
256
 
191
257
  [ISC](./LICENSE) © BOTIM
package/dist/index.cjs CHANGED
@@ -819,6 +819,10 @@ var CommandRegistry = class {
819
819
  };
820
820
  var MAX_DUMP_BYTES = 64 * 1024;
821
821
  var MAX_SCREENSHOT_BYTES = 1024 * 1024;
822
+ var MAX_EXEC_CODE_BYTES = 8 * 1024;
823
+ var DEFAULT_EXEC_TIMEOUT_MS = 5e3;
824
+ var MIN_EXEC_TIMEOUT_MS = 250;
825
+ var MAX_EXEC_TIMEOUT_MS = 3e4;
822
826
  async function defaultDomScreenshot() {
823
827
  if (typeof document === "undefined" || typeof window === "undefined") {
824
828
  throw new Error(
@@ -933,6 +937,9 @@ function registerBuiltins(registry, hooks = {}) {
933
937
  registry.register("dump-state", makeDumpState(hooks.getState));
934
938
  registry.register("set-feature-flag", makeSetFlag(hooks.setFeatureFlag));
935
939
  registry.register("screenshot", makeScreenshot(hooks.screenshot));
940
+ if (hooks.exec !== false) {
941
+ registry.register("exec", makeExec(hooks.exec ?? {}));
942
+ }
936
943
  }
937
944
  var ping = () => ({ ok: true, ts: Date.now() });
938
945
  function makeReload(reload) {
@@ -995,6 +1002,168 @@ function makeScreenshot(screenshot) {
995
1002
  return { format, data, bytes: data.length };
996
1003
  };
997
1004
  }
1005
+ var cachedBOT = void 0;
1006
+ function getBOT() {
1007
+ if (cachedBOT !== void 0) return cachedBOT;
1008
+ if (typeof window === "undefined") {
1009
+ cachedBOT = null;
1010
+ return null;
1011
+ }
1012
+ try {
1013
+ const w = window;
1014
+ if (w.BOT) {
1015
+ cachedBOT = w.BOT;
1016
+ return cachedBOT;
1017
+ }
1018
+ } catch {
1019
+ }
1020
+ try {
1021
+ const w = window;
1022
+ const ng = w.angular;
1023
+ if (ng?.element && typeof document !== "undefined") {
1024
+ const injector = ng.element(document).injector?.();
1025
+ const bot = injector?.get?.("BOT");
1026
+ if (bot != null) {
1027
+ cachedBOT = bot;
1028
+ return cachedBOT;
1029
+ }
1030
+ }
1031
+ } catch {
1032
+ }
1033
+ return null;
1034
+ }
1035
+ function makeExec(opts) {
1036
+ const extraGlobalNames = Object.keys(opts.globals ?? {});
1037
+ const extraGlobalValues = extraGlobalNames.map((k) => opts.globals[k]);
1038
+ return async (args, ctx) => {
1039
+ if (typeof args.code !== "string" || args.code.length === 0) {
1040
+ throw new Error("args.code (non-empty string) is required");
1041
+ }
1042
+ if (args.code.length > MAX_EXEC_CODE_BYTES) {
1043
+ throw new Error(`args.code ${args.code.length} bytes exceeds limit ${MAX_EXEC_CODE_BYTES}`);
1044
+ }
1045
+ const timeoutMs = clamp(
1046
+ typeof args.timeoutMs === "number" ? args.timeoutMs : DEFAULT_EXEC_TIMEOUT_MS,
1047
+ MIN_EXEC_TIMEOUT_MS,
1048
+ MAX_EXEC_TIMEOUT_MS
1049
+ );
1050
+ const start = Date.now();
1051
+ const logs = [];
1052
+ const consoleMethods = ["log", "info", "warn", "error", "debug"];
1053
+ const originals = {};
1054
+ if (typeof console !== "undefined") {
1055
+ for (const m of consoleMethods) {
1056
+ const orig = console[m];
1057
+ if (typeof orig !== "function") continue;
1058
+ originals[m] = orig;
1059
+ const captured = (...callArgs) => {
1060
+ logs.push({ method: m, args: callArgs.map(execSafeClone), ts: Date.now() });
1061
+ try {
1062
+ orig.call(console, ...callArgs);
1063
+ } catch {
1064
+ }
1065
+ };
1066
+ console[m] = captured;
1067
+ }
1068
+ }
1069
+ let value;
1070
+ let threw = void 0;
1071
+ try {
1072
+ const AsyncFunction = Object.getPrototypeOf(async function() {
1073
+ }).constructor;
1074
+ const localNames = ["BOT", "window", "document", ...extraGlobalNames];
1075
+ const localValues = [
1076
+ getBOT(),
1077
+ typeof window !== "undefined" ? window : void 0,
1078
+ typeof document !== "undefined" ? document : void 0,
1079
+ ...extraGlobalValues
1080
+ ];
1081
+ let fnTry = null;
1082
+ try {
1083
+ fnTry = new AsyncFunction(...localNames, `return (${args.code});`);
1084
+ } catch (e) {
1085
+ if (!(e instanceof SyntaxError)) throw e;
1086
+ }
1087
+ const fn = fnTry ?? new AsyncFunction(...localNames, args.code);
1088
+ let timer = null;
1089
+ let abortReject = null;
1090
+ const onAbort = () => abortReject?.(new Error("exec cancelled"));
1091
+ ctx.signal?.addEventListener?.("abort", onAbort);
1092
+ try {
1093
+ value = await Promise.race([
1094
+ fn(...localValues),
1095
+ new Promise((_, reject) => {
1096
+ timer = setTimeout(
1097
+ () => reject(new Error(`exec exceeded ${timeoutMs}ms`)),
1098
+ timeoutMs
1099
+ );
1100
+ }),
1101
+ new Promise((_, reject) => {
1102
+ abortReject = reject;
1103
+ })
1104
+ ]);
1105
+ } finally {
1106
+ if (timer) clearTimeout(timer);
1107
+ ctx.signal?.removeEventListener?.("abort", onAbort);
1108
+ }
1109
+ } catch (err) {
1110
+ threw = err;
1111
+ } finally {
1112
+ if (typeof console !== "undefined") {
1113
+ for (const m of consoleMethods) {
1114
+ if (originals[m]) {
1115
+ console[m] = originals[m];
1116
+ }
1117
+ }
1118
+ }
1119
+ }
1120
+ if (threw !== void 0) {
1121
+ const e = threw instanceof Error ? threw : new Error(String(threw));
1122
+ const stackTruncated = typeof e.stack === "string" ? e.stack.split("\n").slice(0, 20).join("\n") : void 0;
1123
+ const detail = {
1124
+ name: e.name,
1125
+ message: e.message,
1126
+ stack: stackTruncated,
1127
+ logs,
1128
+ durationMs: Date.now() - start
1129
+ };
1130
+ const wrapped = new Error(JSON.stringify(detail));
1131
+ wrapped.detail = detail;
1132
+ throw wrapped;
1133
+ }
1134
+ return {
1135
+ ok: true,
1136
+ value: execSafeClone(value),
1137
+ logs,
1138
+ durationMs: Date.now() - start
1139
+ };
1140
+ };
1141
+ }
1142
+ function clamp(n, lo, hi) {
1143
+ if (!Number.isFinite(n)) return lo;
1144
+ return Math.min(Math.max(n, lo), hi);
1145
+ }
1146
+ function execSafeClone(v) {
1147
+ try {
1148
+ return JSON.parse(JSON.stringify(v, (_k, val) => {
1149
+ if (typeof val === "function") {
1150
+ return `[Function: ${val.name || "anonymous"}]`;
1151
+ }
1152
+ if (val instanceof Error) {
1153
+ return { name: val.name, message: val.message, stack: val.stack };
1154
+ }
1155
+ if (typeof val === "bigint") return val.toString() + "n";
1156
+ if (typeof val === "undefined") return null;
1157
+ if (val && typeof val === "object" && val.nodeType === 1) {
1158
+ const el = val;
1159
+ return `[${el.tagName ?? "Element"}${el.id ? "#" + el.id : ""}]`;
1160
+ }
1161
+ return val;
1162
+ }));
1163
+ } catch {
1164
+ return { __unserializable: typeof v };
1165
+ }
1166
+ }
998
1167
  function safeStringify(v) {
999
1168
  try {
1000
1169
  const seen = /* @__PURE__ */ new WeakSet();
@@ -1442,5 +1611,6 @@ exports.DEFAULT_MAX_BODY_BYTES = DEFAULT_MAX_BODY_BYTES;
1442
1611
  exports.DEFAULT_REDACT_HEADERS = DEFAULT_REDACT_HEADERS;
1443
1612
  exports.SCHEMA_VERSION = SCHEMA_VERSION;
1444
1613
  exports.enableRemoteDebug = enableRemoteDebug;
1614
+ exports.getBOT = getBOT;
1445
1615
  //# sourceMappingURL=index.cjs.map
1446
1616
  //# sourceMappingURL=index.cjs.map