@wenrwa/marketplace-cli 0.4.4 → 0.4.6

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.
Files changed (116) hide show
  1. package/dist/__tests__/bridge-activity-parser.test.d.ts +1 -0
  2. package/dist/__tests__/bridge-activity-parser.test.js +0 -0
  3. package/dist/__tests__/bridge-activity-tap.test.d.ts +1 -0
  4. package/dist/__tests__/bridge-activity-tap.test.js +178 -0
  5. package/dist/__tests__/bridge-api-client.test.d.ts +1 -0
  6. package/dist/__tests__/bridge-api-client.test.js +69 -0
  7. package/dist/__tests__/bridge-browser-driver.test.d.ts +1 -0
  8. package/dist/__tests__/bridge-browser-driver.test.js +379 -0
  9. package/dist/__tests__/bridge-browser-frames.test.d.ts +1 -0
  10. package/dist/__tests__/bridge-browser-frames.test.js +166 -0
  11. package/dist/__tests__/bridge-browser-idle.test.d.ts +1 -0
  12. package/dist/__tests__/bridge-browser-idle.test.js +259 -0
  13. package/dist/__tests__/bridge-browser-relay.test.d.ts +1 -0
  14. package/dist/__tests__/bridge-browser-relay.test.js +670 -0
  15. package/dist/__tests__/bridge-bundle-installer.test.d.ts +1 -0
  16. package/dist/__tests__/bridge-bundle-installer.test.js +126 -0
  17. package/dist/__tests__/bridge-cdp-client.test.d.ts +1 -0
  18. package/dist/__tests__/bridge-cdp-client.test.js +133 -0
  19. package/dist/__tests__/bridge-chromium-launcher.test.d.ts +1 -0
  20. package/dist/__tests__/bridge-chromium-launcher.test.js +202 -0
  21. package/dist/__tests__/bridge-command-browser-options.test.d.ts +1 -0
  22. package/dist/__tests__/bridge-command-browser-options.test.js +37 -0
  23. package/dist/__tests__/bridge-config-mcp.test.js +4 -0
  24. package/dist/__tests__/bridge-context-manifest-frame.test.d.ts +1 -0
  25. package/dist/__tests__/bridge-context-manifest-frame.test.js +125 -0
  26. package/dist/__tests__/bridge-context-writer.test.d.ts +1 -0
  27. package/dist/__tests__/bridge-context-writer.test.js +161 -0
  28. package/dist/__tests__/bridge-device-registration.test.d.ts +1 -0
  29. package/dist/__tests__/bridge-device-registration.test.js +33 -0
  30. package/dist/__tests__/bridge-folder-picker-runner.test.d.ts +1 -0
  31. package/dist/__tests__/bridge-folder-picker-runner.test.js +37 -0
  32. package/dist/__tests__/bridge-folder-picker.test.d.ts +1 -0
  33. package/dist/__tests__/bridge-folder-picker.test.js +66 -0
  34. package/dist/__tests__/bridge-folder-registry.test.d.ts +1 -0
  35. package/dist/__tests__/bridge-folder-registry.test.js +107 -0
  36. package/dist/__tests__/bridge-frame-pacer.test.d.ts +1 -0
  37. package/dist/__tests__/bridge-frame-pacer.test.js +102 -0
  38. package/dist/__tests__/bridge-install-bundle-frame.test.d.ts +1 -0
  39. package/dist/__tests__/bridge-install-bundle-frame.test.js +107 -0
  40. package/dist/__tests__/bridge-local-add-core.test.d.ts +1 -0
  41. package/dist/__tests__/bridge-local-add-core.test.js +48 -0
  42. package/dist/__tests__/bridge-local-path-validation-fs.test.d.ts +1 -0
  43. package/dist/__tests__/bridge-local-path-validation-fs.test.js +88 -0
  44. package/dist/__tests__/bridge-local-path-validation.test.d.ts +1 -0
  45. package/dist/__tests__/bridge-local-path-validation.test.js +45 -0
  46. package/dist/__tests__/bridge-login-status.test.d.ts +1 -0
  47. package/dist/__tests__/bridge-login-status.test.js +60 -0
  48. package/dist/__tests__/bridge-relay-client.test.d.ts +1 -0
  49. package/dist/__tests__/bridge-relay-client.test.js +283 -0
  50. package/dist/__tests__/bridge-relay-mode.test.d.ts +1 -0
  51. package/dist/__tests__/bridge-relay-mode.test.js +33 -0
  52. package/dist/__tests__/bridge-relay-pucc14.test.d.ts +1 -0
  53. package/dist/__tests__/bridge-relay-pucc14.test.js +230 -0
  54. package/dist/__tests__/bridge-server-local-endpoints.test.d.ts +1 -0
  55. package/dist/__tests__/bridge-server-local-endpoints.test.js +197 -0
  56. package/dist/__tests__/bridge-server.test.js +22 -0
  57. package/dist/__tests__/bridge-workspace-multirepo.test.d.ts +1 -0
  58. package/dist/__tests__/bridge-workspace-multirepo.test.js +317 -0
  59. package/dist/__tests__/devices.test.d.ts +1 -0
  60. package/dist/__tests__/devices.test.js +66 -0
  61. package/dist/__tests__/local.test.d.ts +1 -0
  62. package/dist/__tests__/local.test.js +147 -0
  63. package/dist/bridge/activityParser.d.ts +53 -0
  64. package/dist/bridge/activityParser.js +202 -0
  65. package/dist/bridge/apiClient.d.ts +56 -0
  66. package/dist/bridge/apiClient.js +50 -0
  67. package/dist/bridge/browserDriver.d.ts +98 -0
  68. package/dist/bridge/browserDriver.js +265 -0
  69. package/dist/bridge/browserFrames.d.ts +182 -0
  70. package/dist/bridge/browserFrames.js +179 -0
  71. package/dist/bridge/browserIdle.d.ts +75 -0
  72. package/dist/bridge/browserIdle.js +120 -0
  73. package/dist/bridge/bundleInstaller.d.ts +31 -0
  74. package/dist/bridge/bundleInstaller.js +127 -0
  75. package/dist/bridge/cdpClient.d.ts +47 -0
  76. package/dist/bridge/cdpClient.js +102 -0
  77. package/dist/bridge/chromiumLauncher.d.ts +45 -0
  78. package/dist/bridge/chromiumLauncher.js +113 -0
  79. package/dist/bridge/config.d.ts +3 -0
  80. package/dist/bridge/contextWriter.d.ts +31 -0
  81. package/dist/bridge/contextWriter.js +139 -0
  82. package/dist/bridge/deviceRegistration.d.ts +28 -0
  83. package/dist/bridge/deviceRegistration.js +19 -0
  84. package/dist/bridge/folderPicker.d.ts +34 -0
  85. package/dist/bridge/folderPicker.js +36 -0
  86. package/dist/bridge/folderPickerRunner.d.ts +3 -0
  87. package/dist/bridge/folderPickerRunner.js +25 -0
  88. package/dist/bridge/folderRegistry.d.ts +19 -0
  89. package/dist/bridge/folderRegistry.js +100 -0
  90. package/dist/bridge/framePacer.d.ts +61 -0
  91. package/dist/bridge/framePacer.js +79 -0
  92. package/dist/bridge/localAdd.d.ts +28 -0
  93. package/dist/bridge/localAdd.js +67 -0
  94. package/dist/bridge/localPathValidation.d.ts +20 -0
  95. package/dist/bridge/localPathValidation.js +58 -0
  96. package/dist/bridge/localPathValidationFs.d.ts +2 -0
  97. package/dist/bridge/localPathValidationFs.js +98 -0
  98. package/dist/bridge/loginStatus.d.ts +38 -0
  99. package/dist/bridge/loginStatus.js +56 -0
  100. package/dist/bridge/realPty.js +6 -2
  101. package/dist/bridge/relayClient.d.ts +166 -0
  102. package/dist/bridge/relayClient.js +517 -0
  103. package/dist/bridge/repoCheckout.d.ts +31 -0
  104. package/dist/bridge/repoCheckout.js +18 -0
  105. package/dist/bridge/server.d.ts +33 -0
  106. package/dist/bridge/server.js +98 -10
  107. package/dist/bridge/sessionManager.d.ts +18 -0
  108. package/dist/bridge/sessionManager.js +15 -1
  109. package/dist/commands/bridge.d.ts +21 -0
  110. package/dist/commands/bridge.js +194 -1
  111. package/dist/commands/devices.d.ts +9 -0
  112. package/dist/commands/devices.js +53 -0
  113. package/dist/commands/local.d.ts +9 -0
  114. package/dist/commands/local.js +141 -0
  115. package/dist/index.js +7 -1
  116. package/package.json +1 -1
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ /**
3
+ * pucc-int-03 (ADR-0032) — Claude Code PTY activity parser (the PRODUCER).
4
+ *
5
+ * A Claude Code agent is a real PTY session the platform never drives
6
+ * (ADR-0028). To give it the same file-targeted drones/holograms a credits
7
+ * agent gets, the bridge OBSERVES its PTY output (read-only) and extracts the
8
+ * tool-use it renders — Edit/Write/Read/Bash targets + a short "thinking"
9
+ * summary — so the arena can swarm the files it touches.
10
+ *
11
+ * This module is PURE and defensive by contract:
12
+ * - Input is raw PTY output: ANSI escapes, \r cursor redraws, and tool lines
13
+ * split arbitrarily across chunks. It buffers partial lines across `push`.
14
+ * - A parse MISS yields NOTHING (empty array). It NEVER throws — every code
15
+ * path that could fail is caught and degraded to "no signal", because a
16
+ * parser bug must never break the terminal it is only watching.
17
+ * - The recognized markers are a SMALL, VERSIONED set (see TOOL_VERBS): if
18
+ * Claude Code's output format drifts, this table is the one thing to bump.
19
+ *
20
+ * Recognized shape (best-effort): Claude Code renders a tool call as a bulleted
21
+ * line — `● Edit(src/foo.ts)`, `⏺ Read(package.json)`, `● Bash(npm test)`,
22
+ * `⏺ Update(src/x.ts)` — a bullet glyph, the Tool name, and a parenthesized
23
+ * arg. For file tools the arg's first token is the (repo-relative) path; for
24
+ * Bash it is a command (no path is extracted — the consumer degrades an empty
25
+ * `filePaths` to an ambient pulse). Every recognized tool also yields a
26
+ * human "thinking" phrase ("Editing foo.ts", "Running npm test").
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.ActivityParser = void 0;
30
+ /**
31
+ * The VERSIONED marker table. Tool → gerund used in the "thinking" phrase.
32
+ * FILE_TOOLS is the subset whose arg is a path we extract into `filePaths`.
33
+ * Bash is recognized (for a thinking cue) but yields no path.
34
+ */
35
+ const TOOL_VERBS = {
36
+ Read: 'Reading',
37
+ Edit: 'Editing',
38
+ Write: 'Writing',
39
+ Update: 'Updating',
40
+ Create: 'Creating',
41
+ MultiEdit: 'Editing',
42
+ NotebookEdit: 'Editing',
43
+ Bash: 'Running',
44
+ };
45
+ const FILE_TOOLS = new Set(['Read', 'Edit', 'Write', 'Update', 'Create', 'MultiEdit', 'NotebookEdit']);
46
+ /** Bullet glyphs Claude Code (and similar TUIs) prefix a tool line with. */
47
+ const BULLET = '[\\u25cf\\u23fa\\u23f5\\u25b6\\u25b7\\u2022*>]'; // ● ⏺ ⏵ ▶ ▷ • * >
48
+ /**
49
+ * `<bullet> <Tool>(<arg>` — arg captured up to the first ')' or end-of-line
50
+ * (Claude Code truncates long args with `…` and NO closing paren, so a missing
51
+ * ')' must still parse). Operates on a single already-newline-split, ANSI-free
52
+ * line, so `.` never crosses lines.
53
+ */
54
+ const TOOL_LINE = new RegExp('^' + BULLET + '\\s+([A-Za-z][A-Za-z0-9_]*)\\(([^)]*)\\)?');
55
+ /**
56
+ * ANSI/VT control-sequence matchers, built from \\u001b (ESC) / \\u0007 (BEL)
57
+ * escapes so this source carries NO literal control bytes (which an editor can
58
+ * silently corrupt). Stateless with /g; String.replace resets lastIndex.
59
+ */
60
+ const _ESC = "\\u001b";
61
+ const _BEL = "\\u0007";
62
+ // OSC: ESC ] ... (BEL | ESC \\)
63
+ const OSC = new RegExp(_ESC + '\\][\\s\\S]*?(?:' + _BEL + '|' + _ESC + '\\\\)', 'g');
64
+ // CSI + other ESC-introduced sequences (SGR colors, cursor moves, ...): ESC,
65
+ // optional intro chars, optional numeric params, a final byte in @-~.
66
+ const CSI = new RegExp(_ESC + '[[\\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[@-~]', 'g');
67
+ // Any remaining lone ESC + C0 control chars EXCEPT tab(09) newline(0a) cr(0d).
68
+ const STRAY = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
69
+ /** Strip ANSI/VT control sequences to leave printable text. */
70
+ function stripAnsi(input) {
71
+ return input.replace(OSC, "").replace(CSI, "").replace(STRAY, "");
72
+ }
73
+ /**
74
+ * Reduce a raw line to its final RENDERED content. A TUI redraws in place with
75
+ * carriage returns: "old\rnew" displays "new". Keep only the text after the
76
+ * last \r so repeated redraws collapse to one signal.
77
+ */
78
+ function finalRendered(line) {
79
+ const cr = line.lastIndexOf('\r');
80
+ return cr >= 0 ? line.slice(cr + 1) : line;
81
+ }
82
+ /** Extract a plausible repo-relative path from a file-tool arg, or null. */
83
+ function extractPath(arg) {
84
+ // Take the first comma-separated segment (Claude Code appends `, offset:…`).
85
+ let s = arg.split(',')[0].trim();
86
+ // Drop an optional `file_path:` / `path:` label if present.
87
+ const labelled = s.match(/^(?:file_path|path|notebook_path)\s*:\s*(.+)$/i);
88
+ if (labelled)
89
+ s = labelled[1].trim();
90
+ // Strip a single pair of surrounding quotes.
91
+ s = s.replace(/^["'`](.*)["'`]$/, '$1').trim();
92
+ // Strip a trailing truncation ellipsis Claude Code adds to long args.
93
+ s = s.replace(/(?:…|\.\.\.)+$/, '').trim();
94
+ // Strip a leading "./".
95
+ s = s.replace(/^\.\//, '');
96
+ if (!s || s.length > 512)
97
+ return null;
98
+ // Reject obvious non-paths (nothing path-like left after cleanup).
99
+ if (!/[A-Za-z0-9._/\\-]/.test(s))
100
+ return null;
101
+ return s;
102
+ }
103
+ function basename(p) {
104
+ const parts = p.split(/[\\/]/);
105
+ return parts[parts.length - 1] || p;
106
+ }
107
+ /** Truncate a thinking phrase to a bounded, single-line length. */
108
+ function boundThinking(s) {
109
+ const oneLine = s.replace(/\s+/g, ' ').trim();
110
+ return oneLine.length > 80 ? oneLine.slice(0, 79) + '…' : oneLine;
111
+ }
112
+ /** Best-effort short summary of a Bash arg for the thinking cue. */
113
+ function extractCommandSummary(arg) {
114
+ const s = arg
115
+ .replace(/(?:…|\.\.\.)+$/, '')
116
+ .trim()
117
+ .replace(/^["'`](.*)["'`]$/, '$1')
118
+ .trim();
119
+ return s || null;
120
+ }
121
+ const DEFAULT_MAX_LINE = 16384;
122
+ class ActivityParser {
123
+ buf = '';
124
+ maxLineLength;
125
+ /** Suppress consecutive identical signals (TUI re-renders the same line). */
126
+ lastKey = '';
127
+ constructor(opts = {}) {
128
+ this.maxLineLength = opts.maxLineLength ?? DEFAULT_MAX_LINE;
129
+ }
130
+ /**
131
+ * Feed a chunk of PTY output. Returns every NEW tool-use signal completed by
132
+ * this chunk (usually 0 or 1). Never throws: any internal failure degrades
133
+ * to an empty result so the caller's PTY forwarding is never disturbed.
134
+ */
135
+ push(chunk) {
136
+ try {
137
+ return this.pushUnsafe(chunk);
138
+ }
139
+ catch {
140
+ // A parser bug must never break the terminal it only observes. Reset the
141
+ // buffer so one bad state can't wedge all future parsing.
142
+ this.buf = '';
143
+ return [];
144
+ }
145
+ }
146
+ pushUnsafe(chunk) {
147
+ if (typeof chunk !== 'string' || chunk.length === 0)
148
+ return [];
149
+ this.buf += chunk;
150
+ // Normalize CRLF line endings to LF so a terminal's trailing `\r\n` is a
151
+ // line break, not mistaken for a mid-line redraw. A lone trailing `\r`
152
+ // (chunk boundary) stays buffered — the next chunk's `\n` completes it.
153
+ this.buf = this.buf.replace(/\r\n/g, '\n');
154
+ // Bounded buffer: if a single line runs away without a newline (binary
155
+ // spew), keep only the tail so a future newline can still complete a line.
156
+ if (this.buf.length > this.maxLineLength && !this.buf.includes('\n')) {
157
+ this.buf = this.buf.slice(-this.maxLineLength);
158
+ }
159
+ const nl = this.buf.lastIndexOf('\n');
160
+ if (nl < 0)
161
+ return []; // no complete line yet — keep buffering
162
+ const complete = this.buf.slice(0, nl);
163
+ this.buf = this.buf.slice(nl + 1);
164
+ const out = [];
165
+ for (const rawLine of complete.split('\n')) {
166
+ const signal = this.parseLine(rawLine);
167
+ if (!signal)
168
+ continue;
169
+ const key = JSON.stringify(signal);
170
+ if (key === this.lastKey)
171
+ continue; // dedup rapid repeats
172
+ this.lastKey = key;
173
+ out.push(signal);
174
+ }
175
+ return out;
176
+ }
177
+ parseLine(rawLine) {
178
+ const line = stripAnsi(finalRendered(rawLine)).trim();
179
+ if (!line)
180
+ return null;
181
+ const m = TOOL_LINE.exec(line);
182
+ if (!m)
183
+ return null;
184
+ const tool = m[1];
185
+ const verb = TOOL_VERBS[tool];
186
+ if (!verb)
187
+ return null; // unknown tool → no signal (fewer false events)
188
+ const arg = m[2] ?? '';
189
+ if (FILE_TOOLS.has(tool)) {
190
+ const path = extractPath(arg);
191
+ if (path) {
192
+ return { filePaths: [path], thinking: boundThinking(verb + ' ' + basename(path)) };
193
+ }
194
+ // Recognized file tool but no usable path → thinking-only (ambient pulse).
195
+ return { filePaths: [], thinking: boundThinking(verb) };
196
+ }
197
+ // Bash (and any future non-file tool): a thinking cue, no path.
198
+ const cmd = extractCommandSummary(arg);
199
+ return { filePaths: [], thinking: boundThinking(cmd ? verb + ' ' + cmd : verb) };
200
+ }
201
+ }
202
+ exports.ActivityParser = ActivityParser;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * lws-03 — bridge REST client. The daemon talks to the backend AS ITSELF
3
+ * (Authorization: Bearer <Connect Token>), distinct from utils/client.ts
4
+ * (the interactive CLI's X-Wallet-Pubkey login flow). Used for device
5
+ * pairing ("Connect this machine") and local-workspace creation/deletion.
6
+ * fetchImpl is injectable so callers/tests never hit the network.
7
+ */
8
+ export type FetchImpl = (url: string, init: {
9
+ method: string;
10
+ headers: Record<string, string>;
11
+ body?: string;
12
+ }) => Promise<{
13
+ ok: boolean;
14
+ status: number;
15
+ json: () => Promise<any>;
16
+ }>;
17
+ export interface BridgeApiOpts {
18
+ apiUrl: string;
19
+ connectToken: string;
20
+ fetchImpl?: FetchImpl;
21
+ }
22
+ export declare class BridgeApiError extends Error {
23
+ code: string;
24
+ status: number;
25
+ constructor(message: string, code: string, status: number);
26
+ }
27
+ export interface DeviceRecord {
28
+ id: string;
29
+ name: string;
30
+ createdAt?: string;
31
+ revokedAt?: string | null;
32
+ }
33
+ export declare function registerDevice(opts: BridgeApiOpts, name: string): Promise<{
34
+ success: boolean;
35
+ device: DeviceRecord;
36
+ }>;
37
+ export declare function listDevices(opts: BridgeApiOpts): Promise<{
38
+ success: boolean;
39
+ devices: DeviceRecord[];
40
+ }>;
41
+ export declare function revokeDevice(opts: BridgeApiOpts, deviceId: string): Promise<{
42
+ success: boolean;
43
+ }>;
44
+ export interface WorkspaceRecord {
45
+ id: string;
46
+ name: string;
47
+ }
48
+ export declare function createLocalWorkspace(opts: BridgeApiOpts, params: {
49
+ name: string;
50
+ }): Promise<{
51
+ success: boolean;
52
+ workspace: WorkspaceRecord;
53
+ }>;
54
+ export declare function deleteWorkspace(opts: BridgeApiOpts, workspaceId: string): Promise<{
55
+ success: boolean;
56
+ }>;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BridgeApiError = void 0;
4
+ exports.registerDevice = registerDevice;
5
+ exports.listDevices = listDevices;
6
+ exports.revokeDevice = revokeDevice;
7
+ exports.createLocalWorkspace = createLocalWorkspace;
8
+ exports.deleteWorkspace = deleteWorkspace;
9
+ class BridgeApiError extends Error {
10
+ code;
11
+ status;
12
+ constructor(message, code, status) {
13
+ super(message);
14
+ this.code = code;
15
+ this.status = status;
16
+ this.name = 'BridgeApiError';
17
+ }
18
+ }
19
+ exports.BridgeApiError = BridgeApiError;
20
+ async function bridgeRequest(opts, method, path, body) {
21
+ const f = opts.fetchImpl ?? fetch;
22
+ const res = await f(`${opts.apiUrl}${path}`, {
23
+ method,
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ Authorization: `Bearer ${opts.connectToken}`,
27
+ },
28
+ body: body !== undefined ? JSON.stringify(body) : undefined,
29
+ });
30
+ const json = await res.json();
31
+ if (!res.ok || json?.success === false) {
32
+ throw new BridgeApiError(json?.error?.message || `HTTP ${res.status}`, json?.error?.code || 'UNKNOWN', res.status);
33
+ }
34
+ return json;
35
+ }
36
+ function registerDevice(opts, name) {
37
+ return bridgeRequest(opts, 'POST', '/me/devices', { name });
38
+ }
39
+ function listDevices(opts) {
40
+ return bridgeRequest(opts, 'GET', '/me/devices');
41
+ }
42
+ function revokeDevice(opts, deviceId) {
43
+ return bridgeRequest(opts, 'DELETE', `/me/devices/${deviceId}`);
44
+ }
45
+ function createLocalWorkspace(opts, params) {
46
+ return bridgeRequest(opts, 'POST', '/workspaces', { name: params.name, kind: 'local' });
47
+ }
48
+ function deleteWorkspace(opts, workspaceId) {
49
+ return bridgeRequest(opts, 'DELETE', `/workspaces/${workspaceId}`);
50
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * cbs-02 — bridge CDP browser driver.
3
+ *
4
+ * (docs/prd/cloud-browser-session.md, issues/cbs-02-bridge-cdp-driver.md)
5
+ * The deep module inside the in-container bridge that owns one headless
6
+ * Chromium tab: spawn (child_process, NOT node-pty/SessionManager — this
7
+ * must never touch the PTY session manager or spawn a shell), a minimal
8
+ * hand-rolled CDP client (cdpClient.ts) driving `Page.startScreencast` for
9
+ * output and `Input.dispatch*`/`insertText` for input, screencast frames
10
+ * piped through the cbs-01 keep-newest pacer (framePacer.ts), and
11
+ * navigation/lifecycle state surfaced as plain callbacks. All CDP mechanics
12
+ * (target attach, session ids, event wiring) are hidden behind this
13
+ * interface — relayClient.ts (or any future caller) only sees start/
14
+ * navigate/input/back/forward/reload/onFrame/onState/ackFrame/stop.
15
+ */
16
+ import { ChromiumLaunchHandle } from './chromiumLauncher';
17
+ import { CdpTransportLike } from './cdpClient';
18
+ import { BrowserInputEvent, BrowserSessionState } from './browserFrames';
19
+ /** container -> pane screencast frame, minus the wire envelope (`t`,
20
+ * `sessionId`) — the driver doesn't know the relay session id; the caller
21
+ * (relayClient) stamps those on when forwarding. */
22
+ export interface BrowserDriverFramePayload {
23
+ seq: number;
24
+ data: string;
25
+ w: number;
26
+ h: number;
27
+ }
28
+ /** container -> pane lifecycle/navigation state, minus the wire envelope. */
29
+ export interface BrowserDriverStatePayload {
30
+ state: BrowserSessionState;
31
+ url?: string;
32
+ title?: string;
33
+ reason?: string;
34
+ }
35
+ export interface BrowserDriver {
36
+ /** Launches Chromium (if not already running), attaches to a page
37
+ * target, starts the screencast, and navigates to `url`. */
38
+ start(url: string): Promise<void>;
39
+ /** Navigates the already-open tab to `url` (also what a `browser-open`
40
+ * on an already-open session should call — see relayClient wiring). */
41
+ navigate(url: string): Promise<void>;
42
+ /** Dispatches one input event. Never dropped/paced (unlike frames). */
43
+ input(event: BrowserInputEvent): void;
44
+ back(): Promise<void>;
45
+ forward(): Promise<void>;
46
+ reload(): Promise<void>;
47
+ /** Registers the (single) frame consumer. Frames are keep-newest paced —
48
+ * see ackFrame(). */
49
+ onFrame(cb: (frame: BrowserDriverFramePayload) => void): void;
50
+ /** Registers the (single) lifecycle/navigation state consumer. */
51
+ onState(cb: (state: BrowserDriverStatePayload) => void): void;
52
+ /** Consumer contract for the cbs-01 pacer (framePacer.ts): call once a
53
+ * delivered frame has been handed off downstream. The wire protocol
54
+ * (browserFrames.ts, v1) has no pane-side ack frame yet, so relayClient's
55
+ * interim policy is to call this right after forwarding — true end-to-end
56
+ * backpressure to the pane is a later slice (see PRD module 5). */
57
+ ackFrame(): void;
58
+ /** Kills the Chromium process and tears down the CDP transport, emitting a
59
+ * terminal 'closed' state. Safe to call more than once and safe to call
60
+ * before start() resolves. */
61
+ stop(): void;
62
+ /** cbs-06: same physical teardown as stop() (kills Chromium, tears down
63
+ * CDP — no orphaned process either way), but emits a terminal 'asleep'
64
+ * state instead of 'closed' — intentional and resumable (a later
65
+ * browser-open on this sessionId gets a FRESH driver instance per the
66
+ * relayClient "replace, not navigate" policy; this driver instance is
67
+ * done either way). `reason` rides straight onto the emitted
68
+ * browser-state (default 'sleep'; relayClient passes 'idle-timeout' for
69
+ * the 15-minute auto-close path — see browserFrames.ts's BrowserStateFrame
70
+ * doc comment for the reason-string convention). Safe to call more than
71
+ * once, safe to call before start() resolves, and safe to call after
72
+ * stop() (or vice versa) — only the FIRST of stop()/sleep() wins; the
73
+ * second is a no-op (exactly one terminal browser-state frame per driver
74
+ * lifetime). */
75
+ sleep(reason?: string): void;
76
+ }
77
+ export interface BrowserDriverOpts {
78
+ chromiumPath?: string;
79
+ userDataDir?: string;
80
+ /** Window size AND `Page.startScreencast` maxWidth/maxHeight. Default 1280x800. */
81
+ width?: number;
82
+ height?: number;
83
+ /** JPEG quality for the screencast. Default 60 (PRD: "JPEG quality tuned
84
+ * for text legibility"). */
85
+ quality?: number;
86
+ /** Injectable for tests: defaults to chromiumLauncher.launchChromium. */
87
+ launchChromiumFn?: (opts: {
88
+ chromiumPath?: string;
89
+ userDataDir?: string;
90
+ width?: number;
91
+ height?: number;
92
+ }) => ChromiumLaunchHandle;
93
+ /** Injectable for tests: defaults to a `ws`-backed transport connected to
94
+ * the Chromium DevTools websocket URL. Tests inject an in-process
95
+ * MockCdpEndpoint transport here to avoid a real socket/process. */
96
+ wsFactory?: (url: string) => CdpTransportLike;
97
+ }
98
+ export declare function createBrowserDriver(opts?: BrowserDriverOpts): BrowserDriver;
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createBrowserDriver = createBrowserDriver;
4
+ /**
5
+ * cbs-02 — bridge CDP browser driver.
6
+ *
7
+ * (docs/prd/cloud-browser-session.md, issues/cbs-02-bridge-cdp-driver.md)
8
+ * The deep module inside the in-container bridge that owns one headless
9
+ * Chromium tab: spawn (child_process, NOT node-pty/SessionManager — this
10
+ * must never touch the PTY session manager or spawn a shell), a minimal
11
+ * hand-rolled CDP client (cdpClient.ts) driving `Page.startScreencast` for
12
+ * output and `Input.dispatch*`/`insertText` for input, screencast frames
13
+ * piped through the cbs-01 keep-newest pacer (framePacer.ts), and
14
+ * navigation/lifecycle state surfaced as plain callbacks. All CDP mechanics
15
+ * (target attach, session ids, event wiring) are hidden behind this
16
+ * interface — relayClient.ts (or any future caller) only sees start/
17
+ * navigate/input/back/forward/reload/onFrame/onState/ackFrame/stop.
18
+ */
19
+ const chromiumLauncher_1 = require("./chromiumLauncher");
20
+ const cdpClient_1 = require("./cdpClient");
21
+ const framePacer_1 = require("./framePacer");
22
+ /** CDP's Input.dispatch*Event `modifiers` bitmask: Alt=1, Ctrl=2, Meta=4,
23
+ * Shift=8 (per the CDP spec) — abstracted away from callers by
24
+ * browserFrames.ts's plain BrowserInputModifiers booleans. */
25
+ function modifiersBitmask(m) {
26
+ let bits = 0;
27
+ if (m?.alt)
28
+ bits |= 1;
29
+ if (m?.ctrl)
30
+ bits |= 2;
31
+ if (m?.meta)
32
+ bits |= 4;
33
+ if (m?.shift)
34
+ bits |= 8;
35
+ return bits;
36
+ }
37
+ /** Default real transport: wraps a `ws` WebSocket connected to the
38
+ * Chromium DevTools endpoint. Never exercised by tests (they inject
39
+ * wsFactory) — the real-Chromium round is a manual smoke test at
40
+ * image-build time per the PRD's Testing Decisions. */
41
+ function createDefaultTransport(url) {
42
+ // Lazy require: keeps `ws` off the load path for every CLI command that
43
+ // never touches the browser driver (mirrors realPty.ts's lazy node-pty require).
44
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
45
+ const { WebSocket } = require('ws');
46
+ const socket = new WebSocket(url);
47
+ return {
48
+ send: (data) => {
49
+ if (socket.readyState === WebSocket.OPEN)
50
+ socket.send(data);
51
+ },
52
+ close: () => socket.close(),
53
+ on: (event, cb) => {
54
+ if (event === 'message') {
55
+ socket.on('message', (raw) => cb(String(raw)));
56
+ }
57
+ else if (event === 'close') {
58
+ socket.on('close', (code, reason) => cb({ code, reason: reason ? String(reason) : undefined }));
59
+ }
60
+ else {
61
+ socket.on(event, cb);
62
+ }
63
+ },
64
+ };
65
+ }
66
+ function createBrowserDriver(opts = {}) {
67
+ const chromiumPath = opts.chromiumPath;
68
+ const userDataDir = opts.userDataDir;
69
+ const width = opts.width ?? 1280;
70
+ const height = opts.height ?? 800;
71
+ const quality = opts.quality ?? 60;
72
+ const launchChromiumFn = opts.launchChromiumFn ?? chromiumLauncher_1.launchChromium;
73
+ const wsFactory = opts.wsFactory ?? createDefaultTransport;
74
+ let cdp = null;
75
+ let childProc = null;
76
+ let pageTargetId = null;
77
+ let targetSessionId = null;
78
+ let seq = 0;
79
+ let closed = false;
80
+ let stopped = false;
81
+ let frameCb = null;
82
+ let stateCb = null;
83
+ const pacer = (0, framePacer_1.createFramePacer)({
84
+ onDeliver: (f) => frameCb?.(f),
85
+ });
86
+ const emitState = (s) => {
87
+ stateCb?.(s);
88
+ };
89
+ // cbs-06: `closed` guards the TERMINAL STATE EMISSION (fires at most once
90
+ // per driver lifetime, whichever of 'closed'/'asleep' gets there first —
91
+ // a crash-exit racing an explicit stop()/sleep() must never double-emit).
92
+ // `stopped` guards the PHYSICAL TEARDOWN (kill the process, close CDP) —
93
+ // separate from `closed` because a genuine Chromium exit event can arrive
94
+ // AFTER an explicit stop()/sleep() already tore things down; teardown()
95
+ // must not try to kill an already-dead process a second time.
96
+ const emitTerminal = (state, reason) => {
97
+ if (closed)
98
+ return;
99
+ closed = true;
100
+ pacer.close();
101
+ emitState({ state, reason });
102
+ };
103
+ const handleClosed = (reason) => {
104
+ emitTerminal('closed', reason);
105
+ };
106
+ const teardown = () => {
107
+ if (stopped)
108
+ return;
109
+ stopped = true;
110
+ try {
111
+ cdp?.close();
112
+ }
113
+ catch {
114
+ /* best-effort teardown */
115
+ }
116
+ try {
117
+ childProc?.kill();
118
+ }
119
+ catch {
120
+ /* best-effort teardown */
121
+ }
122
+ };
123
+ async function start(url) {
124
+ emitState({ state: 'opening' });
125
+ const { proc, wsUrl } = launchChromiumFn({ chromiumPath, userDataDir, width, height });
126
+ childProc = proc;
127
+ proc.on('exit', (code, signal) => {
128
+ handleClosed(`chromium-exit code=${code ?? 'null'} signal=${signal ?? 'null'}`);
129
+ });
130
+ proc.on('error', (err) => {
131
+ handleClosed(`chromium-error ${err instanceof Error ? err.message : String(err)}`);
132
+ });
133
+ const browserWsUrl = await wsUrl;
134
+ const transport = wsFactory(browserWsUrl);
135
+ const client = (0, cdpClient_1.createCdpClient)(transport);
136
+ cdp = client;
137
+ client.onClose((info) => {
138
+ handleClosed(info?.reason ? `cdp-closed:${info.reason}` : 'cdp-closed');
139
+ });
140
+ client.onEvent('Target.targetInfoChanged', (params) => {
141
+ const info = params.targetInfo;
142
+ if (!info || !pageTargetId || info.targetId !== pageTargetId)
143
+ return;
144
+ emitState({ state: 'open', url: info.url, title: info.title });
145
+ });
146
+ client.onEvent('Page.screencastFrame', (params) => {
147
+ const frameAckId = params.sessionId;
148
+ // Ack CDP immediately regardless of pacer/consumer state — this is
149
+ // CDP's own flow-control contract (an un-acked screencast stalls),
150
+ // independent of the keep-newest policy governing onFrame delivery.
151
+ if (targetSessionId) {
152
+ void client.send('Page.screencastFrameAck', { sessionId: frameAckId }, targetSessionId).catch(() => { });
153
+ }
154
+ seq += 1;
155
+ const metadata = params.metadata ?? {};
156
+ pacer.push({
157
+ seq,
158
+ data: params.data,
159
+ w: metadata.deviceWidth ?? width,
160
+ h: metadata.deviceHeight ?? height,
161
+ });
162
+ });
163
+ const { targetInfos } = await client.send('Target.getTargets');
164
+ const existingPage = targetInfos.find((t) => t.type === 'page');
165
+ if (existingPage) {
166
+ pageTargetId = existingPage.targetId;
167
+ }
168
+ else {
169
+ const created = await client.send('Target.createTarget', { url: 'about:blank' });
170
+ pageTargetId = created.targetId;
171
+ }
172
+ const attached = await client.send('Target.attachToTarget', {
173
+ targetId: pageTargetId,
174
+ flatten: true,
175
+ });
176
+ targetSessionId = attached.sessionId;
177
+ await client.send('Page.enable', {}, targetSessionId);
178
+ await client.send('Page.startScreencast', { format: 'jpeg', quality, maxWidth: width, maxHeight: height }, targetSessionId);
179
+ await navigate(url);
180
+ }
181
+ async function navigate(url) {
182
+ if (!cdp || !targetSessionId)
183
+ return;
184
+ await cdp.send('Page.navigate', { url }, targetSessionId);
185
+ }
186
+ async function goHistory(delta) {
187
+ if (!cdp || !targetSessionId)
188
+ return;
189
+ const hist = await cdp.send('Page.getNavigationHistory', {}, targetSessionId);
190
+ const entry = hist.entries[hist.currentIndex + delta];
191
+ if (!entry)
192
+ return;
193
+ await cdp.send('Page.navigateToHistoryEntry', { entryId: entry.id }, targetSessionId);
194
+ }
195
+ async function reload() {
196
+ if (!cdp || !targetSessionId)
197
+ return;
198
+ await cdp.send('Page.reload', {}, targetSessionId);
199
+ }
200
+ function input(event) {
201
+ if (!cdp || !targetSessionId)
202
+ return;
203
+ const client = cdp;
204
+ const sid = targetSessionId;
205
+ switch (event.kind) {
206
+ case 'mouse': {
207
+ const type = event.type === 'down' ? 'mousePressed' : event.type === 'up' ? 'mouseReleased' : 'mouseMoved';
208
+ void client
209
+ .send('Input.dispatchMouseEvent', {
210
+ type,
211
+ x: event.x,
212
+ y: event.y,
213
+ button: event.button ?? 'none',
214
+ clickCount: event.type === 'move' ? 0 : 1,
215
+ modifiers: modifiersBitmask(event.modifiers),
216
+ }, sid)
217
+ .catch(() => { });
218
+ break;
219
+ }
220
+ case 'wheel':
221
+ void client
222
+ .send('Input.dispatchMouseEvent', { type: 'mouseWheel', x: event.x, y: event.y, deltaX: event.deltaX, deltaY: event.deltaY }, sid)
223
+ .catch(() => { });
224
+ break;
225
+ case 'key':
226
+ void client
227
+ .send('Input.dispatchKeyEvent', {
228
+ type: event.type === 'down' ? 'keyDown' : 'keyUp',
229
+ key: event.key,
230
+ code: event.code,
231
+ modifiers: modifiersBitmask(event.modifiers),
232
+ }, sid)
233
+ .catch(() => { });
234
+ break;
235
+ case 'insertText':
236
+ void client.send('Input.insertText', { text: event.text }, sid).catch(() => { });
237
+ break;
238
+ }
239
+ }
240
+ function stop() {
241
+ teardown();
242
+ emitTerminal('closed', 'stopped');
243
+ }
244
+ function sleep(reason = 'sleep') {
245
+ teardown();
246
+ emitTerminal('asleep', reason);
247
+ }
248
+ return {
249
+ start,
250
+ navigate,
251
+ input,
252
+ back: () => goHistory(-1),
253
+ forward: () => goHistory(1),
254
+ reload,
255
+ onFrame: (cb) => {
256
+ frameCb = cb;
257
+ },
258
+ onState: (cb) => {
259
+ stateCb = cb;
260
+ },
261
+ ackFrame: () => pacer.ack(),
262
+ stop,
263
+ sleep,
264
+ };
265
+ }