@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 +70 -4
- package/dist/index.cjs +170 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +170 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
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.
|
|
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
|
-
##
|
|
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
|