local-browser-bridge 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.
Files changed (92) hide show
  1. package/README.md +724 -0
  2. package/dist/package.json +61 -0
  3. package/dist/src/browser/chrome.d.ts +19 -0
  4. package/dist/src/browser/chrome.js +778 -0
  5. package/dist/src/browser/index.d.ts +3 -0
  6. package/dist/src/browser/index.js +25 -0
  7. package/dist/src/browser/safari.d.ts +41 -0
  8. package/dist/src/browser/safari.js +827 -0
  9. package/dist/src/browser-attach-ux-helper.d.ts +39 -0
  10. package/dist/src/browser-attach-ux-helper.js +157 -0
  11. package/dist/src/capabilities.d.ts +3 -0
  12. package/dist/src/capabilities.js +182 -0
  13. package/dist/src/chrome-relay-error-helper.d.ts +19 -0
  14. package/dist/src/chrome-relay-error-helper.js +78 -0
  15. package/dist/src/chrome-relay-helper-cli.d.ts +2 -0
  16. package/dist/src/chrome-relay-helper-cli.js +97 -0
  17. package/dist/src/chrome-relay-helper.d.ts +29 -0
  18. package/dist/src/chrome-relay-helper.js +151 -0
  19. package/dist/src/chrome-relay-state.d.ts +23 -0
  20. package/dist/src/chrome-relay-state.js +108 -0
  21. package/dist/src/claude-code.d.ts +20 -0
  22. package/dist/src/claude-code.js +66 -0
  23. package/dist/src/cli-reference-adapter.d.ts +13 -0
  24. package/dist/src/cli-reference-adapter.js +48 -0
  25. package/dist/src/cli.d.ts +3 -0
  26. package/dist/src/cli.js +200 -0
  27. package/dist/src/codex.d.ts +17 -0
  28. package/dist/src/codex.js +25 -0
  29. package/dist/src/connection-ux.d.ts +61 -0
  30. package/dist/src/connection-ux.js +256 -0
  31. package/dist/src/errors.d.ts +12 -0
  32. package/dist/src/errors.js +58 -0
  33. package/dist/src/http-reference-adapter.d.ts +34 -0
  34. package/dist/src/http-reference-adapter.js +61 -0
  35. package/dist/src/http.d.ts +3 -0
  36. package/dist/src/http.js +161 -0
  37. package/dist/src/index.d.ts +17 -0
  38. package/dist/src/index.js +43 -0
  39. package/dist/src/mcp-stdio.d.ts +2 -0
  40. package/dist/src/mcp-stdio.js +10 -0
  41. package/dist/src/mcp.d.ts +25 -0
  42. package/dist/src/mcp.js +483 -0
  43. package/dist/src/reference-adapter.d.ts +32 -0
  44. package/dist/src/reference-adapter.js +42 -0
  45. package/dist/src/service/attach-service.d.ts +28 -0
  46. package/dist/src/service/attach-service.js +272 -0
  47. package/dist/src/session-metadata.d.ts +4 -0
  48. package/dist/src/session-metadata.js +88 -0
  49. package/dist/src/store/session-store.d.ts +14 -0
  50. package/dist/src/store/session-store.js +52 -0
  51. package/dist/src/target.d.ts +9 -0
  52. package/dist/src/target.js +61 -0
  53. package/dist/src/types.d.ts +397 -0
  54. package/dist/src/types.js +2 -0
  55. package/dist/tests/attach-service.test.d.ts +1 -0
  56. package/dist/tests/attach-service.test.js +1367 -0
  57. package/dist/tests/browser-attach-ux-helper.test.d.ts +1 -0
  58. package/dist/tests/browser-attach-ux-helper.test.js +139 -0
  59. package/dist/tests/chrome-relay-error-helper.test.d.ts +1 -0
  60. package/dist/tests/chrome-relay-error-helper.test.js +67 -0
  61. package/dist/tests/chrome-relay-helper.test.d.ts +1 -0
  62. package/dist/tests/chrome-relay-helper.test.js +142 -0
  63. package/dist/tests/chrome-relay-state-schema.test.d.ts +1 -0
  64. package/dist/tests/chrome-relay-state-schema.test.js +96 -0
  65. package/dist/tests/claude-code-wrapper.test.d.ts +1 -0
  66. package/dist/tests/claude-code-wrapper.test.js +170 -0
  67. package/dist/tests/codex.test.d.ts +1 -0
  68. package/dist/tests/codex.test.js +210 -0
  69. package/dist/tests/demo-client-smoke.test.d.ts +1 -0
  70. package/dist/tests/demo-client-smoke.test.js +405 -0
  71. package/dist/tests/docs-fixtures.test.d.ts +1 -0
  72. package/dist/tests/docs-fixtures.test.js +255 -0
  73. package/dist/tests/doctor-connect-wrapper.test.d.ts +1 -0
  74. package/dist/tests/doctor-connect-wrapper.test.js +62 -0
  75. package/dist/tests/fixtures/doctor-connect-cli-stub.d.ts +1 -0
  76. package/dist/tests/fixtures/doctor-connect-cli-stub.js +93 -0
  77. package/dist/tests/fixtures/public-root-cli-stub.d.ts +210 -0
  78. package/dist/tests/fixtures/public-root-cli-stub.js +143 -0
  79. package/dist/tests/fixtures/public-root-consumer.js +67 -0
  80. package/dist/tests/mcp.test.d.ts +1 -0
  81. package/dist/tests/mcp.test.js +345 -0
  82. package/dist/tests/public-consumer-helpers.test.d.ts +1 -0
  83. package/dist/tests/public-consumer-helpers.test.js +33 -0
  84. package/dist/tests/public-package-git-consumption.test.d.ts +1 -0
  85. package/dist/tests/public-package-git-consumption.test.js +56 -0
  86. package/dist/tests/public-root-consumer-smoke.test.d.ts +1 -0
  87. package/dist/tests/public-root-consumer-smoke.test.js +214 -0
  88. package/dist/tests/reference-adapter.test.d.ts +1 -0
  89. package/dist/tests/reference-adapter.test.js +220 -0
  90. package/dist/tests/transport-reference-adapters.test.d.ts +1 -0
  91. package/dist/tests/transport-reference-adapters.test.js +214 -0
  92. package/package.json +61 -0
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CHROME_RELAY_STATE_PATH_ENV = void 0;
4
+ exports.getDefaultChromeRelayStateOutputPath = getDefaultChromeRelayStateOutputPath;
5
+ exports.getHomeChromeRelayStateOutputPath = getHomeChromeRelayStateOutputPath;
6
+ exports.resolveChromeRelayStateOutputPath = resolveChromeRelayStateOutputPath;
7
+ exports.buildChromeRelayFixtureState = buildChromeRelayFixtureState;
8
+ exports.writeChromeRelayStateSnapshot = writeChromeRelayStateSnapshot;
9
+ exports.writeChromeRelayFixtureState = writeChromeRelayFixtureState;
10
+ const promises_1 = require("node:fs/promises");
11
+ const node_os_1 = require("node:os");
12
+ const node_path_1 = require("node:path");
13
+ const errors_1 = require("./errors");
14
+ const chrome_relay_state_1 = require("./chrome-relay-state");
15
+ exports.CHROME_RELAY_STATE_PATH_ENV = "LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH";
16
+ const DEFAULT_OUTPUT_PATH = ".local-browser-bridge/chrome-relay-state.json";
17
+ function nowIso() {
18
+ return new Date().toISOString();
19
+ }
20
+ function oneHourFromNowIso() {
21
+ return new Date(Date.now() + 60 * 60 * 1000).toISOString();
22
+ }
23
+ function oneMinuteAgoIso() {
24
+ return new Date(Date.now() - 60 * 1000).toISOString();
25
+ }
26
+ function resolveSharedTab(options) {
27
+ return {
28
+ id: options.tabId ?? "tab-123",
29
+ title: options.title ?? "Relay Example",
30
+ url: options.url ?? "https://example.com/shared"
31
+ };
32
+ }
33
+ function getDefaultChromeRelayStateOutputPath() {
34
+ return (0, node_path_1.resolve)(process.cwd(), DEFAULT_OUTPUT_PATH);
35
+ }
36
+ function getHomeChromeRelayStateOutputPath() {
37
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), DEFAULT_OUTPUT_PATH);
38
+ }
39
+ function resolveChromeRelayStateOutputPath(explicitPath) {
40
+ const trimmedExplicit = explicitPath?.trim();
41
+ if (trimmedExplicit) {
42
+ return (0, node_path_1.resolve)(trimmedExplicit);
43
+ }
44
+ const configured = process.env[exports.CHROME_RELAY_STATE_PATH_ENV]?.trim();
45
+ if (configured) {
46
+ return (0, node_path_1.resolve)(configured);
47
+ }
48
+ return getDefaultChromeRelayStateOutputPath();
49
+ }
50
+ function buildChromeRelayFixtureState(options) {
51
+ const version = options.version?.trim() || "1.1.0";
52
+ const updatedAt = options.updatedAt?.trim() || nowIso();
53
+ if (options.flow === "extension-missing") {
54
+ return {
55
+ version,
56
+ updatedAt,
57
+ extensionInstalled: false
58
+ };
59
+ }
60
+ if (options.flow === "disconnected") {
61
+ return {
62
+ version,
63
+ updatedAt,
64
+ extensionInstalled: true,
65
+ connected: false
66
+ };
67
+ }
68
+ if (options.flow === "click-required") {
69
+ return {
70
+ version,
71
+ updatedAt,
72
+ extensionInstalled: true,
73
+ connected: true,
74
+ userGestureRequired: true
75
+ };
76
+ }
77
+ if (options.flow === "share-required") {
78
+ return {
79
+ version,
80
+ updatedAt,
81
+ extensionInstalled: true,
82
+ connected: true,
83
+ userGestureRequired: false,
84
+ shareRequired: true
85
+ };
86
+ }
87
+ if (options.flow === "clear-shared-tab") {
88
+ return {
89
+ version,
90
+ updatedAt,
91
+ extensionInstalled: true,
92
+ connected: true,
93
+ userGestureRequired: false,
94
+ shareRequired: false,
95
+ sharedTab: null
96
+ };
97
+ }
98
+ if (options.flow === "shared-tab") {
99
+ return {
100
+ version,
101
+ updatedAt,
102
+ extensionInstalled: true,
103
+ connected: true,
104
+ userGestureRequired: false,
105
+ shareRequired: false,
106
+ resumable: options.resumable ?? true,
107
+ resumeRequiresUserGesture: options.resumeRequiresUserGesture ?? false,
108
+ expiresAt: options.expiresAt?.trim() || oneHourFromNowIso(),
109
+ sharedTab: resolveSharedTab(options)
110
+ };
111
+ }
112
+ return {
113
+ version,
114
+ updatedAt,
115
+ extensionInstalled: true,
116
+ connected: true,
117
+ userGestureRequired: false,
118
+ shareRequired: false,
119
+ resumable: options.resumable ?? false,
120
+ resumeRequiresUserGesture: options.resumeRequiresUserGesture ?? true,
121
+ expiresAt: options.expiresAt?.trim() || oneMinuteAgoIso(),
122
+ sharedTab: resolveSharedTab(options)
123
+ };
124
+ }
125
+ async function writeChromeRelayStateSnapshot(state, options) {
126
+ const validation = (0, chrome_relay_state_1.validateChromeRelayState)(state);
127
+ if (!validation.ok || !validation.probe) {
128
+ throw new errors_1.AppError(`Refusing to write invalid chrome relay state: ${validation.errors.join("; ")}`, 400, "invalid_relay_state");
129
+ }
130
+ const outputPath = resolveChromeRelayStateOutputPath(options?.outputPath);
131
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(outputPath), { recursive: true });
132
+ const tempPath = `${outputPath}.${process.pid}.${Date.now()}.tmp`;
133
+ try {
134
+ await (0, promises_1.writeFile)(tempPath, `${JSON.stringify(validation.probe, null, 2)}\n`, "utf8");
135
+ await (0, promises_1.rename)(tempPath, outputPath);
136
+ }
137
+ catch (error) {
138
+ await (0, promises_1.rm)(tempPath, { force: true }).catch(() => undefined);
139
+ throw error;
140
+ }
141
+ return outputPath;
142
+ }
143
+ async function writeChromeRelayFixtureState(options) {
144
+ const state = buildChromeRelayFixtureState(options);
145
+ const path = await writeChromeRelayStateSnapshot(state, { outputPath: options.outputPath });
146
+ return {
147
+ flow: options.flow,
148
+ path,
149
+ state
150
+ };
151
+ }
@@ -0,0 +1,23 @@
1
+ export interface ChromeRelayStateTabProbe {
2
+ id?: string;
3
+ url?: string;
4
+ title?: string;
5
+ }
6
+ export interface ChromeRelayStateProbe {
7
+ version?: string;
8
+ updatedAt?: string;
9
+ extensionInstalled?: boolean;
10
+ connected?: boolean;
11
+ userGestureRequired?: boolean;
12
+ shareRequired?: boolean;
13
+ resumable?: boolean;
14
+ expiresAt?: string;
15
+ resumeRequiresUserGesture?: boolean;
16
+ sharedTab?: ChromeRelayStateTabProbe | null;
17
+ }
18
+ export interface ChromeRelayStateValidationResult {
19
+ ok: boolean;
20
+ probe?: ChromeRelayStateProbe;
21
+ errors: string[];
22
+ }
23
+ export declare function validateChromeRelayState(raw: unknown): ChromeRelayStateValidationResult;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateChromeRelayState = validateChromeRelayState;
4
+ function isRecord(value) {
5
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+ function isNonEmptyString(value) {
8
+ return typeof value === "string" && value.trim().length > 0;
9
+ }
10
+ function isOptionalBoolean(value) {
11
+ return value === undefined || typeof value === "boolean";
12
+ }
13
+ function isOptionalTimestamp(value) {
14
+ return value === undefined || (isNonEmptyString(value) && !Number.isNaN(Date.parse(value)));
15
+ }
16
+ function validateChromeRelayState(raw) {
17
+ if (!isRecord(raw)) {
18
+ return { ok: false, errors: ["relay state must be a JSON object"] };
19
+ }
20
+ const errors = [];
21
+ const sharedTabRaw = raw.sharedTab;
22
+ let sharedTab;
23
+ if (sharedTabRaw === null) {
24
+ sharedTab = null;
25
+ }
26
+ else if (sharedTabRaw === undefined) {
27
+ sharedTab = undefined;
28
+ }
29
+ else if (isRecord(sharedTabRaw)) {
30
+ const id = isNonEmptyString(sharedTabRaw.id) ? sharedTabRaw.id : undefined;
31
+ const url = isNonEmptyString(sharedTabRaw.url) ? sharedTabRaw.url : undefined;
32
+ const title = isNonEmptyString(sharedTabRaw.title) ? sharedTabRaw.title : undefined;
33
+ if ((sharedTabRaw.id !== undefined && id === undefined) || (sharedTabRaw.url !== undefined && url === undefined) || (sharedTabRaw.title !== undefined && title === undefined)) {
34
+ errors.push("sharedTab fields must be non-empty strings when present");
35
+ }
36
+ if (!id && !url && !title) {
37
+ errors.push("sharedTab must include at least one of id, url, or title");
38
+ }
39
+ sharedTab = { id, url, title };
40
+ }
41
+ else {
42
+ errors.push("sharedTab must be an object or null");
43
+ }
44
+ if (raw.version !== undefined && !isNonEmptyString(raw.version)) {
45
+ errors.push("version must be a non-empty string when present");
46
+ }
47
+ if (!isOptionalTimestamp(raw.updatedAt)) {
48
+ errors.push("updatedAt must be an ISO 8601 timestamp when present");
49
+ }
50
+ if (!isOptionalTimestamp(raw.expiresAt)) {
51
+ errors.push("expiresAt must be an ISO 8601 timestamp when present");
52
+ }
53
+ for (const key of ["extensionInstalled", "connected", "userGestureRequired", "shareRequired", "resumable", "resumeRequiresUserGesture"]) {
54
+ if (!isOptionalBoolean(raw[key])) {
55
+ errors.push(`${key} must be a boolean when present`);
56
+ }
57
+ }
58
+ const extensionInstalled = typeof raw.extensionInstalled === "boolean" ? raw.extensionInstalled : undefined;
59
+ const connected = typeof raw.connected === "boolean" ? raw.connected : undefined;
60
+ const userGestureRequired = typeof raw.userGestureRequired === "boolean" ? raw.userGestureRequired : undefined;
61
+ const shareRequired = typeof raw.shareRequired === "boolean" ? raw.shareRequired : undefined;
62
+ const resumable = typeof raw.resumable === "boolean" ? raw.resumable : undefined;
63
+ const resumeRequiresUserGesture = typeof raw.resumeRequiresUserGesture === "boolean" ? raw.resumeRequiresUserGesture : undefined;
64
+ const version = isNonEmptyString(raw.version) ? raw.version : undefined;
65
+ const updatedAt = isNonEmptyString(raw.updatedAt) ? raw.updatedAt : undefined;
66
+ const expiresAt = isNonEmptyString(raw.expiresAt) ? raw.expiresAt : undefined;
67
+ const hasSharedTabObject = sharedTab !== undefined && sharedTab !== null;
68
+ if (extensionInstalled === false && connected === true) {
69
+ errors.push("extensionInstalled=false cannot be combined with connected=true");
70
+ }
71
+ if (extensionInstalled === false && hasSharedTabObject) {
72
+ errors.push("extensionInstalled=false cannot be combined with sharedTab");
73
+ }
74
+ if (connected === false && hasSharedTabObject) {
75
+ errors.push("connected=false cannot be combined with sharedTab");
76
+ }
77
+ if (connected === false && shareRequired === true) {
78
+ errors.push("connected=false cannot be combined with shareRequired=true");
79
+ }
80
+ if (connected === false && userGestureRequired === true) {
81
+ errors.push("connected=false cannot be combined with userGestureRequired=true");
82
+ }
83
+ if (userGestureRequired === true && hasSharedTabObject) {
84
+ errors.push("userGestureRequired=true cannot be combined with sharedTab");
85
+ }
86
+ if (shareRequired === true && hasSharedTabObject) {
87
+ errors.push("shareRequired=true cannot be combined with sharedTab");
88
+ }
89
+ if (errors.length > 0) {
90
+ return { ok: false, errors };
91
+ }
92
+ return {
93
+ ok: true,
94
+ errors: [],
95
+ probe: {
96
+ version,
97
+ updatedAt,
98
+ extensionInstalled,
99
+ connected,
100
+ userGestureRequired,
101
+ shareRequired,
102
+ resumable,
103
+ expiresAt,
104
+ resumeRequiresUserGesture,
105
+ sharedTab
106
+ }
107
+ };
108
+ }
@@ -0,0 +1,20 @@
1
+ import { type BridgeAdapter, type BridgeConnectionResult, type BridgeRoute, type BridgeSessionResult } from "./reference-adapter";
2
+ import { type BrowserAttachUxInterpretation } from "./browser-attach-ux-helper";
3
+ import type { BridgeCapabilitiesContract, BrowserDiagnostics } from "./types";
4
+ export type ClaudeCodeRouteName = "safari" | "chrome-direct" | "chrome-relay";
5
+ export interface ClaudeCodeRouteInput {
6
+ route: ClaudeCodeRouteName;
7
+ sessionId?: string;
8
+ }
9
+ export interface ClaudeCodePreparedRoute<TCapabilities = BridgeCapabilitiesContract, TResult extends BridgeSessionResult = BridgeSessionResult> {
10
+ blocked: boolean;
11
+ capabilities: TCapabilities;
12
+ diagnostics: BrowserDiagnostics;
13
+ operation: "attach" | "resumeSession";
14
+ route: BridgeRoute;
15
+ routeUx: BrowserAttachUxInterpretation;
16
+ prompt?: string;
17
+ connection?: BridgeConnectionResult<TCapabilities, TResult>;
18
+ }
19
+ export declare function normalizeClaudeCodeRoute(input: ClaudeCodeRouteInput): BridgeRoute;
20
+ export declare function prepareClaudeCodeRoute<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends BridgeSessionResult = BridgeSessionResult>(adapter: BridgeAdapter<TCapabilities, TAttachResult, TResumeResult>, input: ClaudeCodeRouteInput): Promise<ClaudeCodePreparedRoute<TCapabilities, TAttachResult | TResumeResult>>;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeClaudeCodeRoute = normalizeClaudeCodeRoute;
4
+ exports.prepareClaudeCodeRoute = prepareClaudeCodeRoute;
5
+ const reference_adapter_1 = require("./reference-adapter");
6
+ const browser_attach_ux_helper_1 = require("./browser-attach-ux-helper");
7
+ function toBrowser(route) {
8
+ return route === "safari" ? "safari" : "chrome";
9
+ }
10
+ function toAttachMode(route) {
11
+ return route === "chrome-relay" ? "relay" : "direct";
12
+ }
13
+ function normalizeClaudeCodeRoute(input) {
14
+ const browser = toBrowser(input.route);
15
+ const attachMode = toAttachMode(input.route);
16
+ return input.sessionId ? { browser, attachMode, sessionId: input.sessionId } : { browser, attachMode };
17
+ }
18
+ async function prepareClaudeCodeRoute(adapter, input) {
19
+ const route = normalizeClaudeCodeRoute(input);
20
+ const capabilities = await adapter.getCapabilities();
21
+ const diagnostics = await adapter.getDiagnostics(route.browser);
22
+ const operation = "sessionId" in route ? "resumeSession" : "attach";
23
+ const routeUx = (0, browser_attach_ux_helper_1.interpretBrowserAttachUxFromDiagnostics)({
24
+ browser: route.browser,
25
+ attachMode: route.attachMode ?? "direct",
26
+ diagnostics,
27
+ operation
28
+ });
29
+ if (routeUx.state === "blocked") {
30
+ return {
31
+ blocked: true,
32
+ capabilities,
33
+ diagnostics,
34
+ operation,
35
+ route,
36
+ routeUx,
37
+ prompt: routeUx.prompt
38
+ };
39
+ }
40
+ const result = "sessionId" in route ? await adapter.resume(route.sessionId) : await adapter.attach(route);
41
+ const session = (0, reference_adapter_1.sessionFromBridgeResult)(result);
42
+ const sessionUx = (0, browser_attach_ux_helper_1.interpretBrowserAttachUxFromSession)({
43
+ session,
44
+ operation
45
+ });
46
+ const connection = {
47
+ capabilities,
48
+ diagnostics,
49
+ operation,
50
+ route,
51
+ routeUx,
52
+ result,
53
+ session,
54
+ sessionUx
55
+ };
56
+ return {
57
+ blocked: false,
58
+ capabilities,
59
+ diagnostics,
60
+ operation,
61
+ route,
62
+ routeUx,
63
+ prompt: connection.sessionUx.prompt ?? connection.routeUx.prompt,
64
+ connection
65
+ };
66
+ }
@@ -0,0 +1,13 @@
1
+ import { type BridgeAdapter, type BridgeSessionResult } from "./reference-adapter";
2
+ import type { BridgeCapabilitiesContract, ResumedSession } from "./types";
3
+ export interface CliBridgeCommand {
4
+ args: string[];
5
+ }
6
+ export interface CliBridgeCommandResult {
7
+ stdout: string;
8
+ }
9
+ export type CliBridgeExecutor = (command: CliBridgeCommand) => Promise<CliBridgeCommandResult>;
10
+ export interface CreateCliBridgeAdapterOptions<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends ResumedSession = ResumedSession> {
11
+ execute: CliBridgeExecutor;
12
+ }
13
+ export declare function createCliBridgeAdapter<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends ResumedSession = ResumedSession>(options: CreateCliBridgeAdapterOptions<TCapabilities, TAttachResult, TResumeResult>): BridgeAdapter<TCapabilities, TAttachResult, TResumeResult>;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCliBridgeAdapter = createCliBridgeAdapter;
4
+ const errors_1 = require("./errors");
5
+ const reference_adapter_1 = require("./reference-adapter");
6
+ function parseJsonEnvelope(stdout, context) {
7
+ try {
8
+ return JSON.parse(stdout);
9
+ }
10
+ catch (error) {
11
+ throw new errors_1.AppError(`Expected ${context} command to return valid JSON.`, 500, "invalid_transport_response", { cause: error instanceof Error ? error.message : String(error) });
12
+ }
13
+ }
14
+ function requireEnvelopeField(envelope, key, context) {
15
+ const value = envelope[key];
16
+ if (value === undefined || value === null) {
17
+ throw new errors_1.AppError(`Expected ${context} command output to include ${String(key)}.`, 500, "invalid_transport_response");
18
+ }
19
+ return value;
20
+ }
21
+ async function runCommand(execute, args, context) {
22
+ const result = await execute({ args });
23
+ return parseJsonEnvelope(result.stdout, context);
24
+ }
25
+ function createCliBridgeAdapter(options) {
26
+ return (0, reference_adapter_1.createBridgeAdapter)({
27
+ async getCapabilities() {
28
+ const envelope = await runCommand(options.execute, ["capabilities"], "capabilities");
29
+ return requireEnvelopeField(envelope, "capabilities", "capabilities");
30
+ },
31
+ async getDiagnostics(browser) {
32
+ const envelope = await runCommand(options.execute, ["diagnostics", "--browser", browser], "diagnostics");
33
+ return requireEnvelopeField(envelope, "diagnostics", "diagnostics");
34
+ },
35
+ async attach(args) {
36
+ const commandArgs = ["attach", "--browser", args.browser];
37
+ if (args.attachMode) {
38
+ commandArgs.push("--attach-mode", args.attachMode);
39
+ }
40
+ const envelope = await runCommand(options.execute, commandArgs, "attach");
41
+ return requireEnvelopeField(envelope, "session", "attach");
42
+ },
43
+ async resume(sessionId) {
44
+ const envelope = await runCommand(options.execute, ["resume", "--id", sessionId], "resumeSession");
45
+ return requireEnvelopeField(envelope, "resumedSession", "resumeSession");
46
+ }
47
+ });
48
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { AttachService } from "./service/attach-service";
3
+ export declare function runCli(args: string[], service?: AttachService): Promise<void>;
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.runCli = runCli;
5
+ const http_1 = require("./http");
6
+ const browser_1 = require("./browser");
7
+ const attach_service_1 = require("./service/attach-service");
8
+ const errors_1 = require("./errors");
9
+ const target_1 = require("./target");
10
+ const connection_ux_1 = require("./connection-ux");
11
+ function printUsage() {
12
+ process.stdout.write([
13
+ "Usage:",
14
+ " local-browser-bridge front-tab [--browser safari|chrome]",
15
+ " local-browser-bridge tab [--browser safari|chrome] (--window-index 1 --tab-index 2 | --signature <signature>)",
16
+ " local-browser-bridge tabs [--browser safari|chrome]",
17
+ " local-browser-bridge attach [--browser safari|chrome] [--attach-mode direct|relay] [--window-index 1 --tab-index 2 | --signature <signature>]",
18
+ " local-browser-bridge activate [--browser safari|chrome] [--window-index 1 --tab-index 2 | --signature <signature>]",
19
+ " local-browser-bridge navigate [--browser safari|chrome] [--window-index 1 --tab-index 2 | --signature <signature>] --url <url>",
20
+ " local-browser-bridge screenshot [--browser safari|chrome] [--window-index 1 --tab-index 2 | --signature <signature>] [--output <path>]",
21
+ " local-browser-bridge capabilities [--browser safari|chrome]",
22
+ " local-browser-bridge diagnostics [--browser safari|chrome]",
23
+ " local-browser-bridge doctor --route safari|chrome-direct|chrome-relay [--session-id <session-id>]",
24
+ " local-browser-bridge connect --route safari|chrome-direct|chrome-relay [--session-id <session-id>]",
25
+ " local-browser-bridge sessions",
26
+ " local-browser-bridge session --id <session-id>",
27
+ " local-browser-bridge resume --id <session-id>",
28
+ " local-browser-bridge session-activate --id <session-id>",
29
+ " local-browser-bridge session-navigate --id <session-id> --url <url>",
30
+ " local-browser-bridge session-screenshot --id <session-id> [--output <path>]",
31
+ " local-browser-bridge serve [--host 127.0.0.1] [--port 3000]",
32
+ "",
33
+ "Safari is the primary adapter. Chrome/Chromium is descriptor/stub-only in this phase.",
34
+ "Compatibility: the safari-attach-tool binary name still works as an alias."
35
+ ].join("\n") + "\n");
36
+ }
37
+ function readFlag(args, name, fallback) {
38
+ const index = args.indexOf(name);
39
+ if (index === -1) {
40
+ return fallback;
41
+ }
42
+ return args[index + 1] ?? fallback;
43
+ }
44
+ function readTarget(args) {
45
+ return (0, target_1.buildTabTarget)({
46
+ windowIndex: readFlag(args, "--window-index"),
47
+ tabIndex: readFlag(args, "--tab-index"),
48
+ signature: readFlag(args, "--signature"),
49
+ url: readFlag(args, "--url"),
50
+ title: readFlag(args, "--title")
51
+ });
52
+ }
53
+ function readOutputPath(args) {
54
+ return readFlag(args, "--output");
55
+ }
56
+ function readAttachMode(args) {
57
+ const value = readFlag(args, "--attach-mode")?.trim();
58
+ if (!value) {
59
+ return undefined;
60
+ }
61
+ if (value === "direct" || value === "relay") {
62
+ return value;
63
+ }
64
+ throw new errors_1.AppError("--attach-mode must be direct or relay.", 400, "invalid_attach_mode");
65
+ }
66
+ function readRequiredFlag(args, name, code) {
67
+ const value = readFlag(args, name)?.trim();
68
+ if (!value) {
69
+ throw new errors_1.AppError(`${name} is required.`, 400, code);
70
+ }
71
+ return value;
72
+ }
73
+ function readRequiredRoute(args) {
74
+ return (0, connection_ux_1.normalizeConnectionRouteName)(readRequiredFlag(args, "--route", "missing_route"));
75
+ }
76
+ function writeJson(payload) {
77
+ (0, errors_1.writeJsonLine)(process.stdout, payload);
78
+ }
79
+ async function runCli(args, service = new attach_service_1.AttachService()) {
80
+ const command = args[0];
81
+ if (!command || command === "--help" || command === "-h") {
82
+ printUsage();
83
+ return;
84
+ }
85
+ if (command === "front-tab") {
86
+ const browser = (0, browser_1.normalizeBrowser)(readFlag(args, "--browser"));
87
+ writeJson({ frontTab: await service.inspectFrontTab(browser) });
88
+ return;
89
+ }
90
+ if (command === "tab") {
91
+ const browser = (0, browser_1.normalizeBrowser)(readFlag(args, "--browser"));
92
+ writeJson({ tab: await service.inspectTab(browser, readTarget(args)) });
93
+ return;
94
+ }
95
+ if (command === "tabs") {
96
+ const browser = (0, browser_1.normalizeBrowser)(readFlag(args, "--browser"));
97
+ writeJson({ tabs: await service.listTabs(browser) });
98
+ return;
99
+ }
100
+ if (command === "attach") {
101
+ const browser = (0, browser_1.normalizeBrowser)(readFlag(args, "--browser"));
102
+ writeJson({
103
+ session: await service.attach(browser, {
104
+ target: readTarget(args),
105
+ attach: { mode: readAttachMode(args) }
106
+ })
107
+ });
108
+ return;
109
+ }
110
+ if (command === "activate") {
111
+ const browser = (0, browser_1.normalizeBrowser)(readFlag(args, "--browser"));
112
+ writeJson({ activation: await service.activate(browser, readTarget(args)) });
113
+ return;
114
+ }
115
+ if (command === "navigate") {
116
+ const browser = (0, browser_1.normalizeBrowser)(readFlag(args, "--browser"));
117
+ const url = readRequiredFlag(args, "--url", "missing_url");
118
+ writeJson({ navigation: await service.navigate(browser, readTarget(args), { url }) });
119
+ return;
120
+ }
121
+ if (command === "screenshot") {
122
+ const browser = (0, browser_1.normalizeBrowser)(readFlag(args, "--browser"));
123
+ writeJson({ screenshot: await service.screenshot(browser, readTarget(args), { outputPath: readOutputPath(args) }) });
124
+ return;
125
+ }
126
+ if (command === "capabilities") {
127
+ const browserFlag = readFlag(args, "--browser");
128
+ const browser = browserFlag ? (0, browser_1.normalizeBrowser)(browserFlag) : undefined;
129
+ writeJson({ capabilities: service.getCapabilities(browser) });
130
+ return;
131
+ }
132
+ if (command === "diagnostics") {
133
+ const browser = (0, browser_1.normalizeBrowser)(readFlag(args, "--browser"));
134
+ writeJson({ diagnostics: await service.diagnostics(browser) });
135
+ return;
136
+ }
137
+ if (command === "doctor") {
138
+ writeJson(await (0, connection_ux_1.doctorConnectionRoute)(service, {
139
+ route: readRequiredRoute(args),
140
+ sessionId: readFlag(args, "--session-id")
141
+ }));
142
+ return;
143
+ }
144
+ if (command === "connect") {
145
+ writeJson(await (0, connection_ux_1.connectConnectionRoute)(service, {
146
+ route: readRequiredRoute(args),
147
+ sessionId: readFlag(args, "--session-id")
148
+ }));
149
+ return;
150
+ }
151
+ if (command === "sessions") {
152
+ writeJson({ sessions: await service.listSessions() });
153
+ return;
154
+ }
155
+ if (command === "session") {
156
+ const id = readRequiredFlag(args, "--id", "missing_id");
157
+ writeJson({ session: await service.getSession(id) });
158
+ return;
159
+ }
160
+ if (command === "resume") {
161
+ const id = readRequiredFlag(args, "--id", "missing_id");
162
+ writeJson({ resumedSession: await service.resumeSession(id) });
163
+ return;
164
+ }
165
+ if (command === "session-activate") {
166
+ const id = readRequiredFlag(args, "--id", "missing_id");
167
+ writeJson({ sessionActivation: await service.activateSession(id) });
168
+ return;
169
+ }
170
+ if (command === "session-navigate") {
171
+ const id = readRequiredFlag(args, "--id", "missing_id");
172
+ const url = readRequiredFlag(args, "--url", "missing_url");
173
+ writeJson({ sessionNavigation: await service.navigateSession(id, { url }) });
174
+ return;
175
+ }
176
+ if (command === "session-screenshot") {
177
+ const id = readRequiredFlag(args, "--id", "missing_id");
178
+ writeJson({ sessionScreenshot: await service.screenshotSession(id, { outputPath: readOutputPath(args) }) });
179
+ return;
180
+ }
181
+ if (command === "serve") {
182
+ const host = readFlag(args, "--host", "127.0.0.1") ?? "127.0.0.1";
183
+ const port = Number(readFlag(args, "--port", "3000") ?? "3000");
184
+ const server = (0, http_1.createApiServer)(service);
185
+ await new Promise((resolve) => server.listen(port, host, resolve));
186
+ process.stdout.write(`Server listening on http://${host}:${port}\n`);
187
+ return;
188
+ }
189
+ throw new errors_1.AppError(`Unknown command: ${command}`, 400, "unknown_command");
190
+ }
191
+ async function main() {
192
+ await runCli(process.argv.slice(2));
193
+ }
194
+ if (require.main === module) {
195
+ main().catch((error) => {
196
+ const { payload } = (0, errors_1.toErrorPayload)(error);
197
+ (0, errors_1.writeJsonLine)(process.stderr, payload);
198
+ process.exitCode = 1;
199
+ });
200
+ }
@@ -0,0 +1,17 @@
1
+ import { type BridgeConnectionResult, type BridgeSessionResult } from "./reference-adapter";
2
+ import { type CliBridgeExecutor, type CreateCliBridgeAdapterOptions } from "./cli-reference-adapter";
3
+ import { type CreateHttpBridgeAdapterOptions, type HttpBridgeExecutor } from "./http-reference-adapter";
4
+ import type { BridgeCapabilitiesContract, ResumedSession } from "./types";
5
+ export type CodexRouteName = "safari" | "chrome-direct" | "chrome-relay";
6
+ export interface CodexRouteInput {
7
+ route: CodexRouteName;
8
+ sessionId?: string;
9
+ }
10
+ export interface ConnectCodexViaCliOptions<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends ResumedSession = ResumedSession> extends CodexRouteInput, CreateCliBridgeAdapterOptions<TCapabilities, TAttachResult, TResumeResult> {
11
+ }
12
+ export interface ConnectCodexViaHttpOptions<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends ResumedSession = ResumedSession> extends CodexRouteInput, CreateHttpBridgeAdapterOptions<TCapabilities, TAttachResult, TResumeResult> {
13
+ }
14
+ export declare function normalizeCodexRoute(route: CodexRouteName, sessionId?: string): import("./reference-adapter").BridgeRoute;
15
+ export declare function connectCodexViaCli<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends ResumedSession = ResumedSession>(options: ConnectCodexViaCliOptions<TCapabilities, TAttachResult, TResumeResult>): Promise<BridgeConnectionResult<TCapabilities, TAttachResult | TResumeResult>>;
16
+ export declare function connectCodexViaHttp<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends ResumedSession = ResumedSession>(options: ConnectCodexViaHttpOptions<TCapabilities, TAttachResult, TResumeResult>): Promise<BridgeConnectionResult<TCapabilities, TAttachResult | TResumeResult>>;
17
+ export type { CliBridgeExecutor, HttpBridgeExecutor };