@unbound-app/debugger-protocol 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controller.d.mts +27 -0
- package/dist/controller.mjs +115 -0
- package/dist/index-DA_F_n3N.d.mts +75 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +45 -0
- package/dist/registry.d.mts +37 -0
- package/dist/registry.mjs +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { o as EvalResult } from "./index-DA_F_n3N.mjs";
|
|
2
|
+
//#region src/controller.d.ts
|
|
3
|
+
declare class ControllerClient {
|
|
4
|
+
private ws;
|
|
5
|
+
private readonly pending;
|
|
6
|
+
private deviceConnected;
|
|
7
|
+
private closed;
|
|
8
|
+
private readonly url;
|
|
9
|
+
constructor(url: string);
|
|
10
|
+
/** @description Opens the socket and keeps it open, reconnecting on drop until {@link close}d. */
|
|
11
|
+
connect(): void;
|
|
12
|
+
/** @description Tears the client down and fails any in-flight evals. Twin of {@link connect}. */
|
|
13
|
+
close(): void;
|
|
14
|
+
/** @description Whether a device is currently connected to the bridge. */
|
|
15
|
+
get isDeviceConnected(): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* @description Sends code to the device via the bridge and resolves with its result. Fails fast
|
|
18
|
+
* if the socket isn't open or the bridge doesn't answer in time.
|
|
19
|
+
* @param code The JavaScript to evaluate on the device.
|
|
20
|
+
* @returns The device's {@link EvalResult}.
|
|
21
|
+
*/
|
|
22
|
+
evaluate(code: string): Promise<EvalResult>;
|
|
23
|
+
private onMessage;
|
|
24
|
+
private onClose;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { ControllerClient };
|
|
@@ -0,0 +1,115 @@
|
|
|
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;
|
|
5
|
+
/** How long to wait between reconnection attempts when the bridge is unreachable. */
|
|
6
|
+
const RECONNECT_DELAY_MS = 1e3;
|
|
7
|
+
var ControllerClient = class {
|
|
8
|
+
ws = null;
|
|
9
|
+
pending = /* @__PURE__ */ new Map();
|
|
10
|
+
deviceConnected = false;
|
|
11
|
+
closed = false;
|
|
12
|
+
url;
|
|
13
|
+
constructor(url) {
|
|
14
|
+
this.url = url;
|
|
15
|
+
}
|
|
16
|
+
/** @description Opens the socket and keeps it open, reconnecting on drop until {@link close}d. */
|
|
17
|
+
connect() {
|
|
18
|
+
if (this.closed) return;
|
|
19
|
+
this.ws = new WebSocket(this.url);
|
|
20
|
+
this.ws.addEventListener("message", (event) => this.onMessage(String(event.data)));
|
|
21
|
+
this.ws.addEventListener("close", () => this.onClose());
|
|
22
|
+
this.ws.addEventListener("error", () => this.ws?.close());
|
|
23
|
+
}
|
|
24
|
+
/** @description Tears the client down and fails any in-flight evals. Twin of {@link connect}. */
|
|
25
|
+
close() {
|
|
26
|
+
this.closed = true;
|
|
27
|
+
this.ws?.close();
|
|
28
|
+
for (const [id, entry] of this.pending) {
|
|
29
|
+
clearTimeout(entry.timer);
|
|
30
|
+
entry.resolve({
|
|
31
|
+
type: "eval-result",
|
|
32
|
+
id,
|
|
33
|
+
ok: false,
|
|
34
|
+
error: "Debugger client shut down.",
|
|
35
|
+
logs: entry.logs
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
this.pending.clear();
|
|
39
|
+
}
|
|
40
|
+
/** @description Whether a device is currently connected to the bridge. */
|
|
41
|
+
get isDeviceConnected() {
|
|
42
|
+
return this.deviceConnected;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* @description Sends code to the device via the bridge and resolves with its result. Fails fast
|
|
46
|
+
* if the socket isn't open or the bridge doesn't answer in time.
|
|
47
|
+
* @param code The JavaScript to evaluate on the device.
|
|
48
|
+
* @returns The device's {@link EvalResult}.
|
|
49
|
+
*/
|
|
50
|
+
evaluate(code) {
|
|
51
|
+
const id = crypto.randomUUID();
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
54
|
+
resolve({
|
|
55
|
+
type: "eval-result",
|
|
56
|
+
id,
|
|
57
|
+
ok: false,
|
|
58
|
+
error: "Not connected to the debugger bridge."
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const logs = [];
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
this.pending.delete(id);
|
|
65
|
+
resolve({
|
|
66
|
+
type: "eval-result",
|
|
67
|
+
id,
|
|
68
|
+
ok: false,
|
|
69
|
+
error: `Evaluation timed out after ${EVAL_TIMEOUT_MS}ms.`,
|
|
70
|
+
logs
|
|
71
|
+
});
|
|
72
|
+
}, EVAL_TIMEOUT_MS);
|
|
73
|
+
this.pending.set(id, {
|
|
74
|
+
resolve,
|
|
75
|
+
timer,
|
|
76
|
+
logs
|
|
77
|
+
});
|
|
78
|
+
this.ws.send(serializeMessage({
|
|
79
|
+
type: "eval",
|
|
80
|
+
id,
|
|
81
|
+
code
|
|
82
|
+
}));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
onMessage(raw) {
|
|
86
|
+
const message = parseMessage(raw);
|
|
87
|
+
if (!message) return;
|
|
88
|
+
if (message.type === "device-status") {
|
|
89
|
+
this.deviceConnected = message.connected;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (message.type === "log") {
|
|
93
|
+
for (const entry of this.pending.values()) entry.logs.push(message);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (message.type === "eval-result") {
|
|
97
|
+
const entry = this.pending.get(message.id);
|
|
98
|
+
if (!entry) return;
|
|
99
|
+
clearTimeout(entry.timer);
|
|
100
|
+
this.pending.delete(message.id);
|
|
101
|
+
entry.resolve({
|
|
102
|
+
...message,
|
|
103
|
+
logs: entry.logs
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
onClose() {
|
|
108
|
+
this.ws = null;
|
|
109
|
+
this.deviceConnected = false;
|
|
110
|
+
if (this.closed) return;
|
|
111
|
+
setTimeout(() => this.connect(), RECONNECT_DELAY_MS);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
//#endregion
|
|
115
|
+
export { ControllerClient };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The wire protocol shared by the bridge, the on-device debugger, and any controller (the terminal
|
|
4
|
+
* REPL or an MCP client). Everything crossing a socket is a JSON-encoded {@link BridgeMessage}.
|
|
5
|
+
*
|
|
6
|
+
* Two planes travel over the single device socket:
|
|
7
|
+
* - control: a controller issues an {@link EvalRequest}; the device answers with the matching
|
|
8
|
+
* {@link EvalResult} (correlated by `id`).
|
|
9
|
+
* - telemetry: the device streams {@link LogMessage}s, broadcast to every controller.
|
|
10
|
+
*
|
|
11
|
+
* Device-safe and dependency-free so it can be bundled into the React Native client.
|
|
12
|
+
*/
|
|
13
|
+
/** A request to evaluate code on the device, sent controller → bridge → device. */
|
|
14
|
+
interface EvalRequest {
|
|
15
|
+
type: 'eval';
|
|
16
|
+
/** Correlation id echoed back on the matching {@link EvalResult}. */
|
|
17
|
+
id: string;
|
|
18
|
+
code: string;
|
|
19
|
+
}
|
|
20
|
+
/** The outcome of an {@link EvalRequest}, sent device → bridge → the one waiting controller. */
|
|
21
|
+
interface EvalResult {
|
|
22
|
+
type: 'eval-result';
|
|
23
|
+
/** The `id` of the {@link EvalRequest} this answers. */
|
|
24
|
+
id: string;
|
|
25
|
+
ok: boolean;
|
|
26
|
+
/** Present when `ok`; the inspected return value. */
|
|
27
|
+
value?: string;
|
|
28
|
+
/** Present when `!ok`; the inspected error. */
|
|
29
|
+
error?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Console output captured on the device for the duration of this eval, so a caller sees the
|
|
32
|
+
* side effects (`console.log`s) the code produced, not just its return value. Populated by the
|
|
33
|
+
* controller that owns the request, not sent by the device.
|
|
34
|
+
*/
|
|
35
|
+
logs?: LogMessage[];
|
|
36
|
+
}
|
|
37
|
+
/** A `console.*` / `nativeLoggingHook` line, streamed device → bridge → all controllers. */
|
|
38
|
+
interface LogMessage {
|
|
39
|
+
type: 'log';
|
|
40
|
+
/** 1 = log/gray, 2 = warn/yellow, 3 = error/red. */
|
|
41
|
+
level: 1 | 2 | 3;
|
|
42
|
+
message: string;
|
|
43
|
+
}
|
|
44
|
+
/** Bridge → controller, once on attach and whenever the device connects or drops. */
|
|
45
|
+
interface DeviceStatus {
|
|
46
|
+
type: 'device-status';
|
|
47
|
+
connected: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** Every framed message on the wire. */
|
|
50
|
+
type BridgeMessage = EvalRequest | EvalResult | LogMessage | DeviceStatus;
|
|
51
|
+
/** The default port the bridge listens on and controllers dial. */
|
|
52
|
+
declare const DEFAULT_BRIDGE_PORT = 9090;
|
|
53
|
+
/** The query marker a controller appends to its URL so the bridge routes it as a controller, not a device. */
|
|
54
|
+
declare const CONTROLLER_ENDPOINT_MARKER = "mcp";
|
|
55
|
+
/**
|
|
56
|
+
* @description Safely parses a raw wire frame into a {@link BridgeMessage}, guarding against invalid
|
|
57
|
+
* JSON and unknown shapes so both ends can trust what they route.
|
|
58
|
+
* @param raw The JSON string received off the socket.
|
|
59
|
+
* @returns The parsed message, or `undefined` if it isn't a recognised frame.
|
|
60
|
+
*/
|
|
61
|
+
declare function parseMessage(raw: string): BridgeMessage | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* @description Serialises a {@link BridgeMessage} into its wire form. Inverse of {@link parseMessage}.
|
|
64
|
+
* @param message The message to frame.
|
|
65
|
+
* @returns The JSON string to send over the socket.
|
|
66
|
+
*/
|
|
67
|
+
declare function serializeMessage(message: BridgeMessage): string;
|
|
68
|
+
declare const _default: {
|
|
69
|
+
DEFAULT_BRIDGE_PORT: number;
|
|
70
|
+
CONTROLLER_ENDPOINT_MARKER: string;
|
|
71
|
+
parseMessage: typeof parseMessage;
|
|
72
|
+
serializeMessage: typeof serializeMessage;
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { EvalRequest as a, _default as c, DeviceStatus as i, parseMessage as l, CONTROLLER_ENDPOINT_MARKER as n, EvalResult as o, DEFAULT_BRIDGE_PORT as r, LogMessage as s, BridgeMessage as t, serializeMessage as u };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as EvalRequest, c as _default, i as DeviceStatus, l as parseMessage, n as CONTROLLER_ENDPOINT_MARKER, o as EvalResult, r as DEFAULT_BRIDGE_PORT, s as LogMessage, t as BridgeMessage, u as serializeMessage } from "./index-DA_F_n3N.mjs";
|
|
2
|
+
export { BridgeMessage, CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT, DeviceStatus, EvalRequest, EvalResult, LogMessage, _default as default, parseMessage, serializeMessage };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/index.ts
|
|
2
|
+
/** The default port the bridge listens on and controllers dial. */
|
|
3
|
+
const DEFAULT_BRIDGE_PORT = 9090;
|
|
4
|
+
/** The query marker a controller appends to its URL so the bridge routes it as a controller, not a device. */
|
|
5
|
+
const CONTROLLER_ENDPOINT_MARKER = "mcp";
|
|
6
|
+
const MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
7
|
+
"eval",
|
|
8
|
+
"eval-result",
|
|
9
|
+
"log",
|
|
10
|
+
"device-status"
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* @description Safely parses a raw wire frame into a {@link BridgeMessage}, guarding against invalid
|
|
14
|
+
* JSON and unknown shapes so both ends can trust what they route.
|
|
15
|
+
* @param raw The JSON string received off the socket.
|
|
16
|
+
* @returns The parsed message, or `undefined` if it isn't a recognised frame.
|
|
17
|
+
*/
|
|
18
|
+
function parseMessage(raw) {
|
|
19
|
+
let value;
|
|
20
|
+
try {
|
|
21
|
+
value = JSON.parse(raw);
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
26
|
+
const type = value.type;
|
|
27
|
+
if (typeof type !== "string" || !MESSAGE_TYPES.has(type)) return void 0;
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* @description Serialises a {@link BridgeMessage} into its wire form. Inverse of {@link parseMessage}.
|
|
32
|
+
* @param message The message to frame.
|
|
33
|
+
* @returns The JSON string to send over the socket.
|
|
34
|
+
*/
|
|
35
|
+
function serializeMessage(message) {
|
|
36
|
+
return JSON.stringify(message);
|
|
37
|
+
}
|
|
38
|
+
var src_default = {
|
|
39
|
+
DEFAULT_BRIDGE_PORT,
|
|
40
|
+
CONTROLLER_ENDPOINT_MARKER: "mcp",
|
|
41
|
+
parseMessage,
|
|
42
|
+
serializeMessage
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { CONTROLLER_ENDPOINT_MARKER, DEFAULT_BRIDGE_PORT, src_default as default, parseMessage, serializeMessage };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ControllerClient } from "./controller.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/registry.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A single command definition is the one source both surfaces derive from: the CLI adapter builds
|
|
6
|
+
* flags and positionals from its schema, the MCP adapter uses the schema as the tool's input schema.
|
|
7
|
+
* Defining a command once keeps the two surfaces structurally in parity.
|
|
8
|
+
*/
|
|
9
|
+
/** The shared runtime a command handler runs against: the bridge controller it evaluates through. */
|
|
10
|
+
type CommandContext = {
|
|
11
|
+
client: ControllerClient;
|
|
12
|
+
};
|
|
13
|
+
/** What a command handler returns: a success flag, human-readable text, and optional structured data. */
|
|
14
|
+
type CommandOutput = {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
text: string;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
};
|
|
19
|
+
/** A command usable from both the CLI and (when opted in) the MCP server. */
|
|
20
|
+
type CommandDefinition<S extends z.ZodObject<z.ZodRawShape>> = {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
/** MCP ⊂ CLI: every MCP action is a CLI command; not every CLI command is an MCP tool. */
|
|
24
|
+
surfaces: {
|
|
25
|
+
cli: true;
|
|
26
|
+
mcp: boolean;
|
|
27
|
+
};
|
|
28
|
+
/** Used directly as the MCP inputSchema; the CLI adapter derives flags from its fields. */
|
|
29
|
+
schema: S;
|
|
30
|
+
/** Which schema fields the CLI presents as positionals; the rest become flags. */
|
|
31
|
+
cli?: {
|
|
32
|
+
positionals?: (keyof z.infer<S>)[];
|
|
33
|
+
};
|
|
34
|
+
handler: (input: z.infer<S>, context: CommandContext) => Promise<CommandOutput>;
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
export { CommandContext, CommandDefinition, CommandOutput };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unbound-app/debugger-protocol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The wire protocol and controller client shared by the debugger bridge, the on-device client, and the CLI.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Mario P.",
|
|
7
|
+
"email": "contact@marioparaschiv.com",
|
|
8
|
+
"url": "https://github.com/marioparaschiv"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"types": "./dist/index.d.mts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.mts",
|
|
18
|
+
"import": "./dist/index.mjs"
|
|
19
|
+
},
|
|
20
|
+
"./controller": {
|
|
21
|
+
"types": "./dist/controller.d.mts",
|
|
22
|
+
"import": "./dist/controller.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./registry": {
|
|
25
|
+
"types": "./dist/registry.d.mts",
|
|
26
|
+
"import": "./dist/registry.mjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsdown"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"zod": "^4.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"tsdown": "^0.22.9"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
}
|
|
41
|
+
}
|