@unbound-app/debugger-protocol 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,17 @@
1
- import { l as PluginPushResult, o as EvalResult } from "./index-DgRewGZa.mjs";
1
+ import { l as EvalResult, o as DeviceInfo, p as PluginPushResult } from "./index-Ctl4b3sx.mjs";
2
2
  import { AddonManifest } from "@unbound-app/types";
3
- //#region src/controller.d.ts
4
3
  type PluginPush = {
5
4
  addonId: string;
6
5
  bundle: string;
7
6
  manifest: AddonManifest;
7
+ bytecode?: Record<string, string>;
8
8
  };
9
- declare class ControllerClient {
9
+ export declare class ControllerClient {
10
10
  private ws;
11
11
  private readonly pending;
12
12
  private readonly pendingPushes;
13
13
  private deviceConnected;
14
+ private deviceInfo;
14
15
  private closed;
15
16
  private readonly url;
16
17
  constructor(url: string);
@@ -20,13 +21,16 @@ declare class ControllerClient {
20
21
  close(): void;
21
22
  /** @description Whether a device is currently connected to the bridge. */
22
23
  get isDeviceConnected(): boolean;
24
+ /** @description What the connected device reported about itself in its hello, once it has said hello. */
25
+ get device(): DeviceInfo | undefined;
23
26
  /**
24
27
  * @description Sends code to the device via the bridge and resolves with its result. Fails fast
25
28
  * if the socket isn't open or the bridge doesn't answer in time.
26
29
  * @param code The JavaScript to evaluate on the device.
30
+ * @param timeout How long the bridge waits for the device, in milliseconds; the bridge's default when omitted.
27
31
  * @returns The device's {@link EvalResult}.
28
32
  */
29
- evaluate(code: string): Promise<EvalResult>;
33
+ evaluate(code: string, timeout?: number): Promise<EvalResult>;
30
34
  /**
31
35
  * @description Pushes a freshly built plugin to the device via the bridge and resolves with the
32
36
  * device's reload outcome. Fails fast if the socket isn't open or the bridge doesn't answer in time.
@@ -37,5 +41,4 @@ declare class ControllerClient {
37
41
  private onMessage;
38
42
  private onClose;
39
43
  }
40
- //#endregion
41
- export { ControllerClient };
44
+ export {};
@@ -1,7 +1,6 @@
1
- import { parseMessage, serializeMessage } from "./index.mjs";
2
- //#region src/controller.ts
3
- /** How long the client waits for a result before failing an eval (must exceed the bridge's own). */
4
- const EVAL_TIMEOUT_MS = 15e3;
1
+ import { MAX_EVAL_TIMEOUT_MS, parseMessage, serializeMessage } from "./index.mjs";
2
+ // src/controller.ts
3
+ const EVAL_RESULT_GRACE_MS = 5e3;
5
4
  const PLUGIN_PUSH_TIMEOUT_MS = 75e3;
6
5
  /** How long to wait between reconnection attempts when the bridge is unreachable. */
7
6
  const RECONNECT_DELAY_MS = 1e3;
@@ -10,6 +9,7 @@ var ControllerClient = class {
10
9
  pending = /* @__PURE__ */ new Map();
11
10
  pendingPushes = /* @__PURE__ */ new Map();
12
11
  deviceConnected = false;
12
+ deviceInfo;
13
13
  closed = false;
14
14
  url;
15
15
  constructor(url) {
@@ -53,13 +53,18 @@ var ControllerClient = class {
53
53
  get isDeviceConnected() {
54
54
  return this.deviceConnected;
55
55
  }
56
+ /** @description What the connected device reported about itself in its hello, once it has said hello. */
57
+ get device() {
58
+ return this.deviceInfo;
59
+ }
56
60
  /**
57
61
  * @description Sends code to the device via the bridge and resolves with its result. Fails fast
58
62
  * if the socket isn't open or the bridge doesn't answer in time.
59
63
  * @param code The JavaScript to evaluate on the device.
64
+ * @param timeout How long the bridge waits for the device, in milliseconds; the bridge's default when omitted.
60
65
  * @returns The device's {@link EvalResult}.
61
66
  */
62
- evaluate(code) {
67
+ evaluate(code, timeout) {
63
68
  const id = crypto.randomUUID();
64
69
  return new Promise((resolve) => {
65
70
  if (this.ws?.readyState !== WebSocket.OPEN) {
@@ -72,25 +77,31 @@ var ControllerClient = class {
72
77
  return;
73
78
  }
74
79
  const logs = [];
80
+ const limit = Math.min(timeout ?? 1e4, MAX_EVAL_TIMEOUT_MS);
75
81
  const timer = setTimeout(() => {
76
82
  this.pending.delete(id);
77
83
  resolve({
78
84
  type: "eval-result",
79
85
  id,
80
86
  ok: false,
81
- error: `Evaluation timed out after ${EVAL_TIMEOUT_MS}ms.`,
87
+ error: `Evaluation timed out after ${limit}ms.`,
82
88
  logs
83
89
  });
84
- }, EVAL_TIMEOUT_MS);
90
+ }, limit + EVAL_RESULT_GRACE_MS);
85
91
  this.pending.set(id, {
86
92
  resolve,
87
93
  timer,
88
94
  logs
89
95
  });
90
- this.ws.send(serializeMessage({
96
+ this.ws.send(serializeMessage(timeout === void 0 ? {
91
97
  type: "eval",
92
98
  id,
93
99
  code
100
+ } : {
101
+ type: "eval",
102
+ id,
103
+ code,
104
+ timeout
94
105
  }));
95
106
  });
96
107
  }
@@ -137,6 +148,7 @@ var ControllerClient = class {
137
148
  if (!message) return;
138
149
  if (message.type === "device-status") {
139
150
  this.deviceConnected = message.connected;
151
+ this.deviceInfo = message.connected ? message.device : void 0;
140
152
  return;
141
153
  }
142
154
  if (message.type === "log") {
@@ -168,5 +180,4 @@ var ControllerClient = class {
168
180
  setTimeout(() => this.connect(), RECONNECT_DELAY_MS);
169
181
  }
170
182
  };
171
- //#endregion
172
183
  export { ControllerClient };
@@ -1,5 +1,4 @@
1
1
  import { AddonManifest } from "@unbound-app/types";
2
- //#region src/index.d.ts
3
2
  /**
4
3
  * The wire protocol shared by the bridge, the device and controllers: every frame is a JSON-encoded
5
4
  * {@link BridgeMessage}. Dependency-free at runtime so the React Native client can bundle it.
@@ -10,6 +9,7 @@ type EvalRequest = {
10
9
  /** Correlation id echoed back on the matching {@link EvalResult}. */
11
10
  id: string;
12
11
  code: string;
12
+ timeout?: number;
13
13
  };
14
14
  /** The outcome of an {@link EvalRequest}, sent device → bridge → the one waiting controller. */
15
15
  type EvalResult = {
@@ -38,6 +38,8 @@ type PluginPushRequest = {
38
38
  addonId: string;
39
39
  bundle: string;
40
40
  manifest: AddonManifest;
41
+ /** The bundle compiled to Hermes bytecode, base64-encoded, by bytecode version; its digests are in `manifest.bytecode`. */
42
+ bytecode?: Record<string, string>;
41
43
  };
42
44
  /** The outcome of a {@link PluginPushRequest}, sent device → bridge → the one waiting controller. */
43
45
  type PluginPushResult = {
@@ -55,15 +57,31 @@ type LogMessage = {
55
57
  level: 1 | 2 | 3;
56
58
  message: string;
57
59
  };
58
- /** Bridge → controller, once on attach and whenever the device connects or drops. */
60
+ /** What a device reports about itself when it connects. */
61
+ type DeviceInfo = {
62
+ platform: string;
63
+ discordVersion: string;
64
+ loaderVersion: string;
65
+ clientVersion: string;
66
+ };
67
+ /** Device → bridge, once its socket opens, so the bridge can say which device is attached. */
68
+ type DeviceHello = {
69
+ type: 'hello';
70
+ device: DeviceInfo;
71
+ };
72
+ /** Bridge → controller, once on attach and whenever the device connects, drops or says hello. */
59
73
  type DeviceStatus = {
60
74
  type: 'device-status';
61
75
  connected: boolean;
76
+ /** Present once the connected device has said hello. */
77
+ device?: DeviceInfo;
62
78
  };
63
79
  /** Every framed message on the wire. */
64
- type BridgeMessage = EvalRequest | EvalResult | PluginPushRequest | PluginPushResult | LogMessage | DeviceStatus;
80
+ type BridgeMessage = EvalRequest | EvalResult | PluginPushRequest | PluginPushResult | LogMessage | DeviceHello | DeviceStatus;
65
81
  /** The default port the bridge listens on and controllers dial. */
66
82
  declare const DEFAULT_BRIDGE_PORT = 9090;
83
+ declare const DEFAULT_EVAL_TIMEOUT_MS = 10000;
84
+ declare const MAX_EVAL_TIMEOUT_MS = 86400000;
67
85
  /** The query marker a controller appends to its URL so the bridge routes it as a controller, not a device. */
68
86
  declare const CONTROLLER_ENDPOINT_MARKER = "mcp";
69
87
  /**
@@ -81,9 +99,10 @@ declare function parseMessage(raw: string): BridgeMessage | undefined;
81
99
  declare function serializeMessage(message: BridgeMessage): string;
82
100
  declare const _default: {
83
101
  DEFAULT_BRIDGE_PORT: number;
102
+ DEFAULT_EVAL_TIMEOUT_MS: number;
103
+ MAX_EVAL_TIMEOUT_MS: number;
84
104
  CONTROLLER_ENDPOINT_MARKER: string;
85
105
  parseMessage: typeof parseMessage;
86
106
  serializeMessage: typeof serializeMessage;
87
107
  };
88
- //#endregion
89
- export { EvalRequest as a, PluginPushRequest as c, parseMessage as d, serializeMessage as f, DeviceStatus as i, PluginPushResult as l, CONTROLLER_ENDPOINT_MARKER as n, EvalResult as o, DEFAULT_BRIDGE_PORT as r, LogMessage as s, BridgeMessage as t, _default as u };
108
+ export { DeviceHello as a, EvalRequest as c, MAX_EVAL_TIMEOUT_MS as d, PluginPushRequest as f, serializeMessage as g, parseMessage as h, DEFAULT_EVAL_TIMEOUT_MS as i, EvalResult as l, _default as m, CONTROLLER_ENDPOINT_MARKER as n, DeviceInfo as o, PluginPushResult as p, DEFAULT_BRIDGE_PORT as r, DeviceStatus as s, BridgeMessage as t, LogMessage as u };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as EvalRequest, c as PluginPushRequest, d as parseMessage, f as serializeMessage, i as DeviceStatus, l as PluginPushResult, n as CONTROLLER_ENDPOINT_MARKER, o as EvalResult, r as DEFAULT_BRIDGE_PORT, s as LogMessage, t as BridgeMessage, u as _default } from "./index-DgRewGZa.mjs";
2
- export { BridgeMessage, CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT, DeviceStatus, EvalRequest, EvalResult, LogMessage, PluginPushRequest, PluginPushResult, _default as default, parseMessage, serializeMessage };
1
+ import { a as DeviceHello, c as EvalRequest, d as MAX_EVAL_TIMEOUT_MS, f as PluginPushRequest, g as serializeMessage, h as parseMessage, i as DEFAULT_EVAL_TIMEOUT_MS, l as EvalResult, m as _default, n as CONTROLLER_ENDPOINT_MARKER, o as DeviceInfo, p as PluginPushResult, r as DEFAULT_BRIDGE_PORT, s as DeviceStatus, t as BridgeMessage, u as LogMessage } from "./index-Ctl4b3sx.mjs";
2
+ export { BridgeMessage, CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT, DEFAULT_EVAL_TIMEOUT_MS, DeviceHello, DeviceInfo, DeviceStatus, EvalRequest, EvalResult, LogMessage, MAX_EVAL_TIMEOUT_MS, PluginPushRequest, PluginPushResult, _default as default, parseMessage, serializeMessage };
package/dist/index.mjs CHANGED
@@ -1,6 +1,8 @@
1
- //#region src/index.ts
1
+ // src/index.ts
2
2
  /** The default port the bridge listens on and controllers dial. */
3
3
  const DEFAULT_BRIDGE_PORT = 9090;
4
+ const DEFAULT_EVAL_TIMEOUT_MS = 1e4;
5
+ const MAX_EVAL_TIMEOUT_MS = 864e5;
4
6
  /** The query marker a controller appends to its URL so the bridge routes it as a controller, not a device. */
5
7
  const CONTROLLER_ENDPOINT_MARKER = "mcp";
6
8
  const MESSAGE_TYPES = /* @__PURE__ */ new Set([
@@ -9,6 +11,7 @@ const MESSAGE_TYPES = /* @__PURE__ */ new Set([
9
11
  "plugin-push",
10
12
  "plugin-push-result",
11
13
  "log",
14
+ "hello",
12
15
  "device-status"
13
16
  ]);
14
17
  /**
@@ -26,8 +29,27 @@ function parseMessage(raw) {
26
29
  }
27
30
  return isBridgeMessage(value) ? value : void 0;
28
31
  }
32
+ function hasRequiredFields(frame) {
33
+ if (typeof frame !== "object" || frame === null || !("type" in frame)) return false;
34
+ switch (frame.type) {
35
+ case "log": return "message" in frame && typeof frame.message === "string" && "level" in frame && (frame.level === 1 || frame.level === 2 || frame.level === 3);
36
+ case "eval": return "id" in frame && typeof frame.id === "string" && "code" in frame && typeof frame.code === "string";
37
+ case "eval-result":
38
+ case "plugin-push-result": return "id" in frame && typeof frame.id === "string" && "ok" in frame && typeof frame.ok === "boolean";
39
+ case "plugin-push": return "id" in frame && typeof frame.id === "string" && "addonId" in frame && typeof frame.addonId === "string" && "bundle" in frame && typeof frame.bundle === "string" && "manifest" in frame && typeof frame.manifest === "object" && frame.manifest !== null && (!("bytecode" in frame) || frame.bytecode === void 0 || isStringRecord(frame.bytecode));
40
+ case "hello": return "device" in frame && isDeviceInfo(frame.device);
41
+ case "device-status": return "connected" in frame && typeof frame.connected === "boolean" && (!("device" in frame) || frame.device === void 0 || isDeviceInfo(frame.device));
42
+ default: return false;
43
+ }
44
+ }
45
+ function isDeviceInfo(value) {
46
+ return typeof value === "object" && value !== null && "platform" in value && typeof value.platform === "string" && "discordVersion" in value && typeof value.discordVersion === "string" && "loaderVersion" in value && typeof value.loaderVersion === "string" && "clientVersion" in value && typeof value.clientVersion === "string";
47
+ }
48
+ function isStringRecord(value) {
49
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
50
+ }
29
51
  function isBridgeMessage(value) {
30
- return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string" && MESSAGE_TYPES.has(value.type);
52
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string" && MESSAGE_TYPES.has(value.type) && hasRequiredFields(value);
31
53
  }
32
54
  /**
33
55
  * @description Serialises a {@link BridgeMessage} into its wire form. Inverse of {@link parseMessage}.
@@ -39,9 +61,10 @@ function serializeMessage(message) {
39
61
  }
40
62
  var src_default = {
41
63
  DEFAULT_BRIDGE_PORT,
64
+ DEFAULT_EVAL_TIMEOUT_MS,
65
+ MAX_EVAL_TIMEOUT_MS,
42
66
  CONTROLLER_ENDPOINT_MARKER: "mcp",
43
67
  parseMessage,
44
68
  serializeMessage
45
69
  };
46
- //#endregion
47
- export { CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT, src_default as default, parseMessage, serializeMessage };
70
+ export { CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT, DEFAULT_EVAL_TIMEOUT_MS, MAX_EVAL_TIMEOUT_MS, src_default as default, parseMessage, serializeMessage };
@@ -1,16 +1,15 @@
1
1
  import { ControllerClient } from "./controller.mjs";
2
2
  import { z } from "zod";
3
- //#region src/registry.d.ts
4
3
  /**
5
4
  * One command definition feeds both surfaces: the CLI builds flags and positionals from its schema,
6
5
  * and the MCP server uses the schema as the tool's input schema.
7
6
  */
8
7
  /** The shared runtime a command handler runs against: the bridge controller it evaluates through. */
9
- type CommandContext = {
8
+ export type CommandContext = {
10
9
  client: ControllerClient;
11
10
  };
12
11
  /** What a command handler returns: a success flag, human-readable text, and optional structured data. */
13
- type CommandOutput = {
12
+ export type CommandOutput = {
14
13
  ok: boolean;
15
14
  text: string;
16
15
  data?: unknown;
@@ -19,7 +18,7 @@ type CommandOutput = {
19
18
  * Every command is on the CLI: at the root (`ubd eval`), or under `ubd mcp` (`ubd mcp open`) for
20
19
  * agent-facing tools, which must then be MCP tools too.
21
20
  */
22
- type CommandSurfaces = {
21
+ export type CommandSurfaces = {
23
22
  cli: 'root';
24
23
  mcp: boolean;
25
24
  } | {
@@ -27,7 +26,7 @@ type CommandSurfaces = {
27
26
  mcp: true;
28
27
  };
29
28
  /** A command usable from both the CLI and (when opted in) the MCP server. */
30
- type CommandDefinition<S extends z.ZodObject<z.ZodRawShape>> = {
29
+ export type CommandDefinition<S extends z.ZodObject<z.ZodRawShape>> = {
31
30
  name: string;
32
31
  description: string;
33
32
  surfaces: CommandSurfaces;
@@ -38,6 +37,4 @@ type CommandDefinition<S extends z.ZodObject<z.ZodRawShape>> = {
38
37
  positionals?: (keyof z.infer<S>)[];
39
38
  };
40
39
  handler: (input: z.infer<S>, context: CommandContext) => Promise<CommandOutput>;
41
- };
42
- //#endregion
43
- export { CommandContext, CommandDefinition, CommandOutput, CommandSurfaces };
40
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unbound-app/debugger-protocol",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "The wire protocol and controller client shared by the debugger bridge, the on-device client, and the CLI.",
5
5
  "author": {
6
6
  "name": "Mario P.",