@opencode/browse 0.0.3-dev.2 → 0.0.6-dev.17

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.
@@ -0,0 +1,1725 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ extensionProtocolVersion,
4
+ extensionProtocolAdvice,
5
+ dataRoot,
6
+ fromCheckout,
7
+ cliCommand,
8
+ devExtension,
9
+ installCommand,
10
+ autoUpdateStatus
11
+ } from "./main-g5mke6rn.mjs";
12
+ import {
13
+ defaultPort2,
14
+ isValidSessionId2,
15
+ portConfig,
16
+ extensionOriginsConfig,
17
+ endpointForPort,
18
+ RelayUnreachable,
19
+ RelayRejected,
20
+ Service,
21
+ layerFetch,
22
+ shouldWaitForExtensionReconnect,
23
+ relayBuildProblem,
24
+ ensureRelay,
25
+ restartRelay,
26
+ extensionReconnectWaitMs,
27
+ ensureExtensionConnected,
28
+ stoppedRelayStatus,
29
+ statusCollections
30
+ } from "./main-3qpkwaa7.mjs";
31
+ import {
32
+ opencodeBrowserVersion2,
33
+ opencodeBrowserBuildId2
34
+ } from "./main-y7c1atbf.mjs";
35
+ import {
36
+ addSecret,
37
+ listSecrets,
38
+ removeSecret,
39
+ readClient,
40
+ socketPath,
41
+ connect,
42
+ pair,
43
+ unpair,
44
+ settingKeys,
45
+ formatSettings
46
+ } from "./main-e4stqtf2.mjs";
47
+ import {
48
+ defaultJournalBaseDir,
49
+ readJournalEntries,
50
+ formatJournalEntry
51
+ } from "./main-kvmxefp1.mjs";
52
+ import {
53
+ updateJson2,
54
+ formatUpdateNotice2,
55
+ updateNotice2
56
+ } from "./main-4p2p9eey.mjs";
57
+ import"./main-4bht27ry.mjs";
58
+
59
+ // src/cli.ts
60
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
61
+ import { Config, Console, Effect as Effect3, FileSystem as FileSystem3, Layer as Layer2, Option as Option2, Predicate as Predicate2, Result, Schema as Schema3 } from "effect";
62
+ import { Argument, CliError, Command, Flag } from "effect/cli";
63
+ import path from "node:path";
64
+ import process from "node:process";
65
+ import { fileURLToPath, pathToFileURL } from "node:url";
66
+
67
+ // src/cli-output.ts
68
+ import { writeSync } from "node:fs";
69
+ var ExitCode = { ok: 0, failure: 1, usage: 2, auth: 3, host: 4 };
70
+ var failure = (command, code, message, options = {}) => ({
71
+ ok: false,
72
+ command,
73
+ checkedAt: new Date().toISOString(),
74
+ error: {
75
+ code,
76
+ message,
77
+ ...options.hint ? { hint: options.hint } : {},
78
+ exitCode: options.exitCode ?? ExitCode.failure
79
+ }
80
+ });
81
+ var takeJsonFlag = (argv) => ({
82
+ json: argv.includes("--json"),
83
+ argv: argv.filter((arg) => arg !== "--json")
84
+ });
85
+ var pause = new Int32Array(new SharedArrayBuffer(4));
86
+ var writeStdout = (text, fd = 1) => {
87
+ const buffer = Buffer.from(text);
88
+ let offset = 0;
89
+ while (offset < buffer.length) {
90
+ try {
91
+ offset += writeSync(fd, buffer, offset, buffer.length - offset);
92
+ } catch (error) {
93
+ const code = error.code;
94
+ if (code === "EPIPE")
95
+ return;
96
+ if (code !== "EAGAIN")
97
+ throw error;
98
+ Atomics.wait(pause, 0, 0, 2);
99
+ }
100
+ }
101
+ };
102
+ var printJson = (value) => writeStdout(`${JSON.stringify(value, null, 2)}
103
+ `);
104
+
105
+ // src/doctor.ts
106
+ import { Effect as Effect2, FileSystem as FileSystem2, Path as Path2, Schema as Schema2 } from "effect";
107
+
108
+ // src/session-store.ts
109
+ import { Context, Effect, FileSystem, Layer, Option, Path, Predicate, Schema } from "effect";
110
+ class SessionStoreError extends Schema.TaggedError()("SessionStore.SessionStoreError", {
111
+ message: Schema.String,
112
+ operation: Schema.String,
113
+ cause: Schema.Defect()
114
+ }) {
115
+ }
116
+ var StoreFile = Schema.Struct({
117
+ id: Schema.optionalKey(Schema.String),
118
+ endpoints: Schema.optionalKey(Schema.Record(Schema.String, Schema.Struct({
119
+ id: Schema.String
120
+ })))
121
+ });
122
+
123
+ class Service2 extends Context.Service()("opencode-browser-cli/SessionStore") {
124
+ }
125
+ var defaultFilePath = () => {
126
+ return `${dataRoot()}/session.json`;
127
+ };
128
+ var make = Effect.fn("SessionStore.make")(function* (options) {
129
+ const fs = yield* FileSystem.FileSystem;
130
+ const path = yield* Path.Path;
131
+ const filePath = options?.filePath ?? defaultFilePath();
132
+ const port = yield* portConfig.pipe(Effect.mapError((cause) => new SessionStoreError({
133
+ message: `Invalid OPENCODE_BROWSER_PORT configuration: ${cause.message}`,
134
+ operation: "configure",
135
+ cause
136
+ })));
137
+ const endpoint = options?.endpoint ?? endpointForPort(port);
138
+ const defaultEndpoint = endpointForPort(defaultPort2);
139
+ const storeError = (operation) => (cause) => new SessionStoreError({
140
+ message: `Could not ${operation} the current session file at ${filePath}`,
141
+ operation,
142
+ cause
143
+ });
144
+ const readStore = fs.readFileString(filePath).pipe(Effect.map(Option.some), Effect.catchTag("PlatformError", (error) => Predicate.isTagged(error.reason, "NotFound") ? Effect.succeed(Option.none()) : Effect.fail(error)), Effect.mapError(storeError("read")), Effect.flatMap(Option.match({
145
+ onNone: () => Effect.succeed({}),
146
+ onSome: (text) => Effect.try({
147
+ try: () => JSON.parse(text),
148
+ catch: (cause) => cause
149
+ }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(StoreFile)), Effect.mapError(storeError("decode")))
150
+ })));
151
+ const currentEntries = (store) => {
152
+ const entries = { ...store.endpoints ?? {} };
153
+ if (store.id && entries[defaultEndpoint] === undefined) {
154
+ entries[defaultEndpoint] = { id: store.id };
155
+ }
156
+ return entries;
157
+ };
158
+ const writeStore = (entries) => fs.makeDirectory(path.dirname(filePath), { recursive: true, mode: 448 }).pipe(Effect.andThen(fs.writeFileString(filePath, `${JSON.stringify({ endpoints: entries }, null, 2)}
159
+ `, { mode: 384 })), Effect.andThen(fs.chmod(filePath, 384)), Effect.mapError(storeError("write")));
160
+ const read = readStore.pipe(Effect.map((store) => currentEntries(store)[endpoint]?.id));
161
+ const write = (id) => readStore.pipe(Effect.flatMap((store) => {
162
+ const entries = currentEntries(store);
163
+ entries[endpoint] = { id };
164
+ return writeStore(entries);
165
+ }));
166
+ const clear = readStore.pipe(Effect.flatMap((store) => {
167
+ const entries = currentEntries(store);
168
+ delete entries[endpoint];
169
+ return writeStore(entries);
170
+ }));
171
+ return Service2.of({ endpoint, filePath, read, write, clear });
172
+ });
173
+ var layer = Layer.effect(Service2, make());
174
+
175
+ // src/doctor.ts
176
+ var PackageMetadata = Schema2.Struct({
177
+ name: Schema2.String,
178
+ version: Schema2.String,
179
+ bin: Schema2.optionalKey(Schema2.Record(Schema2.String, Schema2.String))
180
+ });
181
+ function extensionManifestVersion(version) {
182
+ const match = /^(\d+\.\d+\.\d+)(?:-dev\.(\d+)(?:\.\d+)?)?/.exec(version);
183
+ if (!match)
184
+ return version;
185
+ return match[2] ? `${match[1]}.${match[2]}` : match[1];
186
+ }
187
+ var probe = (effect) => effect.pipe(Effect2.match({
188
+ onFailure: (error) => ({ ok: false, error: errorMessage(error) }),
189
+ onSuccess: (value) => ({ ok: true, value })
190
+ }));
191
+ var errorMessage = (error) => {
192
+ if (error instanceof Error) {
193
+ return error.message;
194
+ }
195
+ return String(error);
196
+ };
197
+ var createDoctorReport = Effect2.fn("Doctor.createReport")(function* (options) {
198
+ const relay = yield* Service;
199
+ const store = yield* Service2;
200
+ const fs = yield* FileSystem2.FileSystem;
201
+ const path = yield* Path2.Path;
202
+ const readJsonFile = (relativePath, schema) => fs.readFileString(path.join(options.packageRoot, relativePath)).pipe(Effect2.mapError((cause) => new Error(`read ${relativePath}: ${cause.reason.message}`)), Effect2.flatMap((text) => Effect2.try({
203
+ try: () => JSON.parse(text),
204
+ catch: () => new Error(`parse ${relativePath}: invalid JSON`)
205
+ })), Effect2.flatMap((value) => Schema2.decodeUnknownEffect(schema)(value).pipe(Effect2.mapError((cause) => new Error(`decode ${relativePath}: ${cause.message}`)))));
206
+ const readPackageInfo = readJsonFile("package.json", PackageMetadata).pipe(Effect2.map((metadata) => ({
207
+ name: metadata.name,
208
+ version: metadata.version,
209
+ bin: {
210
+ opencodeBrowser: metadata.bin?.["opencode-browser"] ?? null
211
+ }
212
+ })));
213
+ const fileExists = (relativePath) => fs.exists(path.join(options.packageRoot, relativePath)).pipe(Effect2.orElseSucceed(() => false));
214
+ const [packageResult, distCliExists, distMcpExists, currentResult] = yield* Effect2.all([
215
+ probe(readPackageInfo),
216
+ fileExists("dist/cli.mjs"),
217
+ fileExists("dist/cli.mjs"),
218
+ probe(store.read)
219
+ ]);
220
+ const expectedExtensionVersion = packageResult.ok ? { ok: true, value: extensionManifestVersion(packageResult.value.version) } : packageResult;
221
+ const relayResult = yield* probe(relay.version);
222
+ const relayBuildMatches = relayResult.ok ? relayResult.value.buildId ? relayBuildProblem(relayResult.value, opencodeBrowserBuildId2) === undefined : null : null;
223
+ const [extensionResult, targetsResult, sessionsResult] = relayResult.ok ? yield* Effect2.all([
224
+ probe(relay.extensionStatus),
225
+ probe(relay.targets),
226
+ probe(relay.sessions)
227
+ ]) : [
228
+ { ok: false, error: relayResult.error },
229
+ { ok: false, error: relayResult.error },
230
+ { ok: false, error: relayResult.error }
231
+ ];
232
+ const expectedVersion = expectedExtensionVersion.ok ? expectedExtensionVersion.value : null;
233
+ const extensionVersion = extensionResult.ok ? extensionResult.value.version : null;
234
+ const extensionVersionMatches = extensionResult.ok && extensionVersion && expectedVersion ? extensionVersion === expectedVersion : null;
235
+ const current = currentResult.ok ? currentResult.value ?? null : null;
236
+ const targets = targetsResult.ok ? targetsResult.value : [];
237
+ const sessions = sessionsResult.ok ? sessionsResult.value : [];
238
+ const staleCurrent = Boolean(current && sessionsResult.ok && !sessions.some((session) => {
239
+ return session.id === current;
240
+ }));
241
+ const relayOwnedTargets = targets.filter((target) => {
242
+ return target.owner === "relay";
243
+ });
244
+ const unhealthyTargets = targets.filter((target) => {
245
+ return target.crashed === true || target.url.startsWith("chrome-error://");
246
+ });
247
+ const possibleLeakedSessions = sessions.filter((session) => {
248
+ return session.connected && session.id !== current;
249
+ });
250
+ const artifacts = [
251
+ { path: "dist/cli.mjs", exists: distCliExists && distMcpExists }
252
+ ];
253
+ const checks = buildDoctorChecks({
254
+ packageResult,
255
+ relayResult,
256
+ extensionResult,
257
+ expectedExtensionVersion,
258
+ artifacts,
259
+ currentResult,
260
+ staleCurrent,
261
+ relayOwnedTargets,
262
+ unhealthyTargets,
263
+ possibleLeakedSessions,
264
+ targetsResult,
265
+ sessionsResult,
266
+ update: options.update ?? null
267
+ });
268
+ const report = {
269
+ status: summarizeCheckStatus(checks),
270
+ endpoint: relay.endpoint,
271
+ cli: { version: opencodeBrowserVersion2, buildId: opencodeBrowserBuildId2 },
272
+ package: {
273
+ path: path.join(options.packageRoot, "package.json"),
274
+ name: packageResult.ok ? packageResult.value.name : null,
275
+ version: packageResult.ok ? packageResult.value.version : null,
276
+ bin: {
277
+ opencodeBrowser: packageResult.ok ? Boolean(packageResult.value.bin.opencodeBrowser) : false
278
+ },
279
+ error: packageResult.ok ? null : packageResult.error
280
+ },
281
+ relay: {
282
+ reachable: relayResult.ok,
283
+ version: relayResult.ok ? relayResult.value.version : null,
284
+ buildId: relayResult.ok ? relayResult.value.buildId ?? null : null,
285
+ buildMatches: relayBuildMatches,
286
+ error: relayResult.ok ? null : relayResult.error
287
+ },
288
+ extension: {
289
+ connected: extensionResult.ok ? extensionResult.value.connected : null,
290
+ version: extensionVersion,
291
+ expectedVersion,
292
+ versionMatches: extensionVersionMatches,
293
+ protocolVersion: extensionResult.ok ? extensionResult.value.protocolVersion ?? null : null,
294
+ expectedProtocolVersion: extensionProtocolVersion,
295
+ protocolCompatible: extensionResult.ok ? extensionResult.value.protocolCompatible ?? null : null,
296
+ protocolLegacy: extensionResult.ok ? extensionResult.value.protocolLegacy ?? null : null,
297
+ rejectedConnections: extensionResult.ok ? extensionResult.value.rejectedConnections ?? null : null,
298
+ error: extensionResult.ok ? null : extensionResult.error
299
+ },
300
+ targets: {
301
+ active: extensionResult.ok ? extensionResult.value.activeTargets : null,
302
+ child: extensionResult.ok ? extensionResult.value.childTargets ?? null : null,
303
+ relayOwned: relayOwnedTargets,
304
+ unhealthy: unhealthyTargets,
305
+ all: targets,
306
+ error: targetsResult.ok ? null : targetsResult.error
307
+ },
308
+ sessions: {
309
+ current,
310
+ staleCurrent,
311
+ possibleLeaked: possibleLeakedSessions,
312
+ all: sessions,
313
+ error: sessionsResult.ok ? currentResult.ok ? null : currentResult.error : sessionsResult.error
314
+ },
315
+ artifacts,
316
+ update: options.update ?? null,
317
+ checks,
318
+ recommendations: buildDoctorRecommendations({
319
+ update: options.update ?? null,
320
+ relayResult,
321
+ extensionResult,
322
+ artifacts,
323
+ staleCurrent,
324
+ current,
325
+ unhealthyTargets
326
+ })
327
+ };
328
+ return report;
329
+ });
330
+ function buildDoctorChecks(options) {
331
+ const packageBinChecks = options.packageResult.ok ? [
332
+ {
333
+ id: "bin-opencode-browser",
334
+ label: "opencode-browser bin",
335
+ status: options.packageResult.value.bin.opencodeBrowser ? "ok" : "fail",
336
+ message: options.packageResult.value.bin.opencodeBrowser ?? "missing from package.json bin"
337
+ }
338
+ ] : [];
339
+ const artifactChecks = options.artifacts.map((artifact) => {
340
+ return {
341
+ id: `artifact-${artifact.path}`,
342
+ label: artifact.path,
343
+ status: artifact.exists ? "ok" : "warn",
344
+ message: artifact.exists ? artifact.version ? `exists (${artifact.version})` : "exists" : "missing"
345
+ };
346
+ });
347
+ return [
348
+ {
349
+ id: "package-metadata",
350
+ label: "package metadata",
351
+ status: options.packageResult.ok ? "ok" : "fail",
352
+ message: options.packageResult.ok ? `${options.packageResult.value.name} ${options.packageResult.value.version}` : options.packageResult.error
353
+ },
354
+ ...packageBinChecks,
355
+ {
356
+ id: "relay-http",
357
+ label: "relay HTTP endpoint",
358
+ status: options.relayResult.ok ? "ok" : "fail",
359
+ message: options.relayResult.ok ? `reachable (${options.relayResult.value.version})` : options.relayResult.error
360
+ },
361
+ relayBuildCheck({ relayResult: options.relayResult, cliBuildId: opencodeBrowserBuildId2 }),
362
+ {
363
+ id: "extension-connected",
364
+ label: "extension connection",
365
+ status: options.extensionResult.ok && options.extensionResult.value.connected ? "ok" : "fail",
366
+ message: options.extensionResult.ok ? options.extensionResult.value.connected ? `connected${options.extensionResult.value.version ? ` (${options.extensionResult.value.version})` : ""}` : "disconnected" : options.extensionResult.error
367
+ },
368
+ extensionVersionCheck({
369
+ extensionResult: options.extensionResult,
370
+ expectedExtensionVersion: options.expectedExtensionVersion
371
+ }),
372
+ extensionProtocolCheck(options.extensionResult),
373
+ ...options.extensionResult.ok && options.extensionResult.value.rejectedConnections !== undefined ? [{
374
+ id: "extension-connection-conflicts",
375
+ label: "competing browser connections",
376
+ status: options.extensionResult.value.rejectedConnections > 0 ? "warn" : "ok",
377
+ message: options.extensionResult.value.rejectedConnections > 0 ? `${options.extensionResult.value.rejectedConnections} connection attempt(s) rejected; the active browser connection was preserved` : "none observed on this connection"
378
+ }] : [],
379
+ ...options.extensionResult.ok && options.extensionResult.value.pairingState ? [{
380
+ id: "extension-pairing",
381
+ label: "extension pairing",
382
+ status: options.extensionResult.value.connected ? "ok" : "warn",
383
+ message: options.extensionResult.value.pairingState
384
+ }] : options.extensionResult.ok && options.extensionResult.value.pairingFailures !== undefined ? [{
385
+ id: "extension-pairing",
386
+ label: "extension pairing",
387
+ status: options.extensionResult.value.pairingFailures > 0 && !options.extensionResult.value.connected ? "warn" : "ok",
388
+ message: options.extensionResult.value.pairingFailures > 0 ? `${options.extensionResult.value.pairingFailures} connection(s) failed pairing${options.extensionResult.value.connected ? "; a paired extension is connected" : `; update the extension, or run \`${cliCommand("pair")}\` and enter the code on its status page`}` : "no failed pairing attempts"
389
+ }] : [],
390
+ ...options.extensionResult.ok && options.extensionResult.value.processFaults !== undefined ? [{
391
+ id: "relay-process-faults",
392
+ label: "relay Playwright faults",
393
+ status: options.extensionResult.value.processFaults.suppressed > 0 ? "warn" : "ok",
394
+ message: options.extensionResult.value.processFaults.suppressed > 0 ? `${options.extensionResult.value.processFaults.suppressed} uncaught Playwright fault(s) caught and survived${options.extensionResult.value.processFaults.lastMessage ? `; last: ${options.extensionResult.value.processFaults.lastMessage}` : ""}` : "none since the relay started"
395
+ }] : [],
396
+ {
397
+ id: "targets-readable",
398
+ label: "targets readable",
399
+ status: options.targetsResult.ok ? "ok" : "fail",
400
+ message: options.targetsResult.ok ? `${options.targetsResult.value.length} active root target(s)` : options.targetsResult.error
401
+ },
402
+ unhealthyTargetsCheck({
403
+ targetsResult: options.targetsResult,
404
+ unhealthyTargets: options.unhealthyTargets
405
+ }),
406
+ {
407
+ id: "sessions-readable",
408
+ label: "sessions readable",
409
+ status: options.sessionsResult.ok ? "ok" : "fail",
410
+ message: options.sessionsResult.ok ? `${options.sessionsResult.value.length} session(s)` : options.sessionsResult.error
411
+ },
412
+ {
413
+ id: "current-session-file",
414
+ label: "current session file",
415
+ status: options.currentResult.ok ? "ok" : "warn",
416
+ message: options.currentResult.ok ? options.currentResult.value ?? "none" : options.currentResult.error
417
+ },
418
+ {
419
+ id: "current-session-stale",
420
+ label: "current session membership",
421
+ status: options.staleCurrent ? "warn" : "ok",
422
+ message: options.staleCurrent ? "current session is not present in relay sessions" : "current session is valid or unset"
423
+ },
424
+ {
425
+ id: "relay-owned-targets",
426
+ label: "relay-owned active targets",
427
+ status: "ok",
428
+ message: options.relayOwnedTargets.length ? `${options.relayOwnedTargets.length} persistent relay-owned target(s)` : "none"
429
+ },
430
+ {
431
+ id: "possible-leaked-sessions",
432
+ label: "connected non-current sessions",
433
+ status: "ok",
434
+ message: options.possibleLeakedSessions.length ? `${options.possibleLeakedSessions.length} connected non-current session(s)` : "none"
435
+ },
436
+ ...artifactChecks,
437
+ ...options.update ? [{
438
+ id: "update",
439
+ label: "update",
440
+ status: "ok",
441
+ message: `available: ${options.update.summary}`
442
+ }] : []
443
+ ];
444
+ }
445
+ var makeDoctorCheck = (id, label) => (status, message) => ({
446
+ id,
447
+ label,
448
+ status,
449
+ message
450
+ });
451
+ function unhealthyTargetsCheck(options) {
452
+ const check = makeDoctorCheck("unhealthy-targets", "crashed or browser-error targets");
453
+ if (!options.targetsResult.ok) {
454
+ return check("warn", `target health unknown: ${options.targetsResult.error}`);
455
+ }
456
+ return check(options.unhealthyTargets.length ? "warn" : "ok", options.unhealthyTargets.length ? `${options.unhealthyTargets.length} unhealthy target(s)` : "none");
457
+ }
458
+ function relayBuildCheck(options) {
459
+ const check = makeDoctorCheck("relay-build", "relay build");
460
+ if (!options.relayResult.ok) {
461
+ return check("warn", "relay unreachable; cannot compare builds");
462
+ }
463
+ const relayBuildId = options.relayResult.value.buildId;
464
+ if (!relayBuildId) {
465
+ return check("warn", "running relay does not report a build id");
466
+ }
467
+ const matches = relayBuildId === options.cliBuildId;
468
+ return check(matches ? "ok" : "warn", matches ? `matches CLI build (${options.cliBuildId})` : `runtime ${relayBuildId} does not match CLI ${options.cliBuildId}`);
469
+ }
470
+ function extensionVersionCheck(options) {
471
+ const check = makeDoctorCheck("extension-version", "extension version");
472
+ if (!options.extensionResult.ok) {
473
+ return check("warn", options.extensionResult.error);
474
+ }
475
+ if (!options.extensionResult.value.connected) {
476
+ return check("warn", "extension disconnected; cannot compare runtime version");
477
+ }
478
+ if (!options.expectedExtensionVersion.ok) {
479
+ return check("warn", `could not read this CLI's version: ${options.expectedExtensionVersion.error}`);
480
+ }
481
+ const runtimeVersion = options.extensionResult.value.version;
482
+ if (!runtimeVersion) {
483
+ return check("warn", "extension did not report a version");
484
+ }
485
+ return check("ok", runtimeVersion === options.expectedExtensionVersion.value ? `matches this CLI (${options.expectedExtensionVersion.value})` : `extension ${runtimeVersion} differs from this CLI's ${options.expectedExtensionVersion.value}; protocol compatibility determines support`);
486
+ }
487
+ function extensionProtocolCheck(extensionResult) {
488
+ const check = makeDoctorCheck("extension-protocol", "extension protocol");
489
+ if (!extensionResult.ok) {
490
+ return check("warn", extensionResult.error);
491
+ }
492
+ const protocolVersion = extensionResult.value.protocolVersion;
493
+ const protocolCompatible = extensionResult.value.protocolCompatible;
494
+ if (protocolCompatible === false) {
495
+ return check("fail", `runtime ${protocolVersion ?? "unknown"} is incompatible with relay ${extensionProtocolVersion}`);
496
+ }
497
+ if (extensionResult.value.protocolLegacy === true) {
498
+ return check("warn", `legacy extension does not report its protocol; relay infers ${protocolVersion ?? "unknown"}`);
499
+ }
500
+ if (protocolVersion === undefined || protocolVersion === null) {
501
+ return check("warn", "extension protocol is unknown");
502
+ }
503
+ return check("ok", `runtime ${protocolVersion} is compatible with relay ${extensionProtocolVersion}`);
504
+ }
505
+ function buildDoctorRecommendations(options) {
506
+ const relayRecommendations = options.relayResult.ok ? [] : [
507
+ `Run a relay-backed command to start the detached relay automatically; use \`${cliCommand("serve")}\` only for foreground debugging.`
508
+ ];
509
+ const relayBuildRecommendations = options.relayResult.ok ? [relayBuildProblem(options.relayResult.value)].filter((message) => message !== undefined) : [];
510
+ const extensionRecommendations = options.relayResult.ok && options.extensionResult.ok && !options.extensionResult.value.connected ? options.extensionResult.value.nextStep ? [options.extensionResult.value.nextStep] : options.extensionResult.value.protocolCompatible === false ? [extensionProtocolAdvice(options.extensionResult.value.protocolVersion, { install: installCommand() })] : [fromCheckout() || devExtension() ? `Run \`${installCommand()}\` and load the extension it points to (Load unpacked on your browser's extensions page).` : `Run \`${installCommand()}\`, and install Browse from the Chrome Web Store page it opens.`] : [];
511
+ const artifactRecommendations = options.artifacts.some((artifact) => {
512
+ return !artifact.exists;
513
+ }) ? [`Rebuild or reinstall the missing files with \`${installCommand()}\`.`] : [];
514
+ const connectionConflictRecommendations = options.extensionResult.ok && (options.extensionResult.value.rejectedConnections ?? 0) > 0 ? ["Keep Browse enabled in one browser/profile. To switch, disable it in the current browser before connecting the other; new sessions do not change the active browser."] : [];
515
+ const staleSessionRecommendations = options.staleCurrent && options.current ? [
516
+ `Current session ${options.current} is stale; run \`${cliCommand("session new")}\` or \`${cliCommand("session use <id>")}\` after the relay is running.`
517
+ ] : [];
518
+ const unhealthyTargetRecommendations = options.unhealthyTargets.length ? [
519
+ "A target is crashed or showing a browser error page. Run the owning session once to trigger relay-owned recovery, or reset/re-adopt a user-owned tab."
520
+ ] : [];
521
+ return [
522
+ ...options.update ? [options.update.notice] : [],
523
+ ...relayRecommendations,
524
+ ...relayBuildRecommendations,
525
+ ...extensionRecommendations,
526
+ ...connectionConflictRecommendations,
527
+ ...artifactRecommendations,
528
+ ...staleSessionRecommendations,
529
+ ...unhealthyTargetRecommendations
530
+ ];
531
+ }
532
+ function summarizeCheckStatus(checks) {
533
+ if (checks.some((check) => {
534
+ return check.status === "fail";
535
+ })) {
536
+ return "fail";
537
+ }
538
+ if (checks.some((check) => {
539
+ return check.status === "warn";
540
+ })) {
541
+ return "warn";
542
+ }
543
+ return "ok";
544
+ }
545
+ function formatDoctorReport(report) {
546
+ const lines = [
547
+ "Browse doctor",
548
+ `Status: ${report.status}`,
549
+ `Endpoint: ${report.endpoint}`,
550
+ `CLI: ${report.cli.version} (${report.cli.buildId})`,
551
+ `Package: ${report.package.name && report.package.version ? `${report.package.name} ${report.package.version}` : report.package.error ?? "unknown"}`,
552
+ `Relay: ${report.relay.reachable ? `reachable (${report.relay.version ?? "unknown"}, ${report.relay.buildId ?? "unknown build"})` : `unreachable (${report.relay.error ?? "unknown error"})`}`,
553
+ `Extension: ${formatExtensionSummary(report)}`,
554
+ `Targets: active=${formatNullableNumber(report.targets.active)} child=${formatNullableNumber(report.targets.child)} relay-owned=${report.targets.relayOwned.length} unhealthy=${report.targets.unhealthy.length}`,
555
+ `Sessions: current=${report.sessions.current ?? "none"} total=${report.sessions.all.length} connected=${report.sessions.all.filter((session) => {
556
+ return session.connected;
557
+ }).length}`,
558
+ "",
559
+ "Checks:",
560
+ ...report.checks.map((check) => {
561
+ return `[${check.status}] ${check.label}: ${check.message}`;
562
+ })
563
+ ];
564
+ const targetLines = report.targets.relayOwned.map((target) => {
565
+ return `- ${formatTargetSummary(target)}`;
566
+ });
567
+ const unhealthyTargetLines = report.targets.unhealthy.map((target) => {
568
+ return `- ${formatTargetSummary(target)}`;
569
+ });
570
+ const sessionLines = report.sessions.possibleLeaked.map((session) => {
571
+ return `- ${session.id} ${session.pageUrl ?? "no page yet"}`;
572
+ });
573
+ const details = [
574
+ ...targetLines.length ? ["", "Relay-owned targets:", ...targetLines] : [],
575
+ ...unhealthyTargetLines.length ? ["", "Unhealthy targets:", ...unhealthyTargetLines] : [],
576
+ ...sessionLines.length ? ["", "Connected non-current sessions:", ...sessionLines] : [],
577
+ ...report.recommendations.length ? ["", "Next steps:", ...report.recommendations.map((item) => {
578
+ return `- ${item}`;
579
+ })] : []
580
+ ];
581
+ return [...lines, ...details].join(`
582
+ `);
583
+ }
584
+ function formatExtensionSummary(report) {
585
+ if (report.extension.connected === null) {
586
+ return `unknown (${report.extension.error ?? "relay unreachable"})`;
587
+ }
588
+ const version = report.extension.version ? ` (${report.extension.version})` : "";
589
+ const protocol = report.extension.protocolVersion === null ? "" : `, protocol ${report.extension.protocolVersion} ${report.extension.protocolCompatible === false ? "incompatible" : "compatible"}`;
590
+ const match = report.extension.versionMatches === null ? "" : report.extension.versionMatches ? ", matches the CLI's version" : ", differs from the CLI's version";
591
+ return `${report.extension.connected ? "connected" : "disconnected"}${version}${protocol}${match}`;
592
+ }
593
+ function formatNullableNumber(value) {
594
+ return value === null ? "unknown" : String(value);
595
+ }
596
+ function formatTargetSummary(target, options = {}) {
597
+ const tab = target.tabId === undefined ? "" : ` tab=${target.tabId}`;
598
+ const session = options.includeSession && target.opencodeBrowserSessionId ? ` session=${target.opencodeBrowserSessionId}` : "";
599
+ const owner = target.owner ? ` owner=${target.owner}` : "";
600
+ const health = `${target.crashed ? " crashed=true" : ""}${target.protectedUi ? " protected-ui=true" : ""}`;
601
+ return `${target.type} ${target.id}${tab}${session}${owner}${health} ${target.url || "about:blank"}`;
602
+ }
603
+
604
+ // src/cli.ts
605
+ var packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
606
+ var sessionIdConfig = Config.option(Config.String("OPENCODE_BROWSER_SESSION"));
607
+ var agentSessionIdConfig = Config.option(Config.String("OPENCODE_SESSION_ID"));
608
+ var targetUrlConfig = Config.option(Config.String("OPENCODE_BROWSER_TARGET_URL"));
609
+ var targetIndexConfig = Config.option(Config.Int("OPENCODE_BROWSER_TARGET_INDEX"));
610
+ var encodedCliOperandsMarker = "bc-cli-operands:v1";
611
+ var encodedCliOperandPrefix = "bc-cli-operand:";
612
+ var jsonFlag = Flag.Boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Print machine-readable JSON"));
613
+ var sessionFlag = (description) => {
614
+ const flag = Flag.String("session").pipe(Flag.optional, Flag.withAlias("s"));
615
+ return description ? flag.pipe(Flag.withDescription(description)) : flag;
616
+ };
617
+ var tabIdFlag = (description) => {
618
+ const flag = Flag.Int("tab-id").pipe(Flag.optional);
619
+ return description ? flag.pipe(Flag.withDescription(description)) : flag;
620
+ };
621
+ var failExit = Effect3.sync(() => {
622
+ process.exitCode = ExitCode.failure;
623
+ });
624
+ var exitWith = (code) => Effect3.sync(() => {
625
+ process.exitCode = code;
626
+ });
627
+ var hostErrorTags = new Set(["RelayClient.RelayUnreachable", "RelayLifecycle.RelayStartFailed", "RelayLifecycle.ExtensionDisconnected"]);
628
+ function exitCodeForError(error) {
629
+ if (error instanceof RelayRejected && error.code === "browser-unavailable")
630
+ return ExitCode.host;
631
+ const tag = Predicate2.isObject(error) && Predicate2.hasProperty(error, "_tag") ? error._tag : undefined;
632
+ return Predicate2.isString(tag) && hostErrorTags.has(tag) ? ExitCode.host : ExitCode.failure;
633
+ }
634
+ function hoistJsonFlag(args) {
635
+ const delimiter = args.indexOf("--");
636
+ const { json, argv } = takeJsonFlag(delimiter === -1 ? args : args.slice(0, delimiter));
637
+ if (!json)
638
+ return { json, argv: args };
639
+ return { json, argv: [...argv, "--json", ...delimiter === -1 ? [] : args.slice(delimiter)] };
640
+ }
641
+ var stderrLine = (message) => Effect3.sync(() => {
642
+ process.stderr.write(`opencode-browser: ${message.split(`
643
+ `)[0].slice(0, 300)}
644
+ `);
645
+ });
646
+ var emitJson = (value) => Effect3.sync(() => printJson(value));
647
+ var printOr = (json, value, text) => json ? emitJson(value) : Console.log(text);
648
+ function agentDefaultSessionId(agentSessionId) {
649
+ const id = agentSessionId ? `oc-${agentSessionId.toLowerCase()}` : undefined;
650
+ return id && isValidSessionId2(id) ? id : undefined;
651
+ }
652
+ function resolveExplicitSessionSelector(options) {
653
+ if (options.positional && options.flag) {
654
+ throw new Error("Use either a positional session id or --session, not both");
655
+ }
656
+ return options.flag ?? options.positional ?? options.environment;
657
+ }
658
+ function formatRecordingQuality(quality) {
659
+ if (!quality)
660
+ return "Capture quality: unavailable (tab capture or older relay).";
661
+ return [
662
+ `Capture: ${quality.width}×${quality.height}, output ${quality.frameRate} fps; source surface ${quality.sourceWidth ?? "?"}×${quality.sourceHeight ?? "?"} CSS px`,
663
+ `Source frames: ${quality.sourceFrameCount} received (${quality.achievedSourceFrameRate.toFixed(1)}/s), ${quality.encodedSourceFrameCount} retained (${quality.achievedEncodedSourceFrameRate.toFixed(1)}/s), ${quality.coalescedFrameCount} coalesced, ${quality.droppedFrameCount} dropped`,
664
+ quality.screenshotFallback ? "WARNING: no compositor frames arrived; this video holds a single stop-time screenshot." : "Source counts are compositor events, not a measurement of distinct motion. Inspect the video before sharing."
665
+ ].join(`
666
+ `);
667
+ }
668
+ function normalizeCliArguments(args) {
669
+ const delimiter = args.indexOf("--");
670
+ const secretsRun = args.findIndex((argument, index) => argument === "secrets" && args[index + 1] === "run");
671
+ if (delimiter === -1 || secretsRun === -1 || secretsRun > delimiter)
672
+ return args;
673
+ return [
674
+ ...args.slice(0, delimiter),
675
+ encodedCliOperandsMarker,
676
+ ...args.slice(delimiter + 1).map((operand) => `${encodedCliOperandPrefix}${encodeURIComponent(operand)}`)
677
+ ];
678
+ }
679
+ function decodeCliOperands(operands) {
680
+ if (operands[0] !== encodedCliOperandsMarker)
681
+ return operands;
682
+ return operands.slice(1).map((operand) => {
683
+ if (!operand.startsWith(encodedCliOperandPrefix))
684
+ throw new Error("Invalid encoded secrets run operand");
685
+ return decodeURIComponent(operand.slice(encodedCliOperandPrefix.length));
686
+ });
687
+ }
688
+ var readExecuteFile = Effect3.fnUntraced(function* (filePath) {
689
+ const fs = yield* FileSystem3.FileSystem;
690
+ return yield* fs.readFileString(path.resolve(filePath)).pipe(Effect3.mapError((cause) => new Error(`read execute file ${filePath}: ${cause.reason.message}`, { cause })));
691
+ });
692
+ var ensureCliRelay = Effect3.fnUntraced(function* () {
693
+ const relay = yield* Service;
694
+ const readiness = yield* ensureRelay({ relay });
695
+ if (readiness.started) {
696
+ yield* Console.error(`Started Browse relay at ${relay.endpoint}`);
697
+ }
698
+ if (readiness.buildProblem) {
699
+ return yield* Effect3.fail(new Error(readiness.buildProblem));
700
+ }
701
+ return readiness;
702
+ });
703
+ var ensureCliRelayAndExtension = Effect3.fnUntraced(function* (sessionId) {
704
+ const relay = yield* Service;
705
+ const readiness = yield* ensureCliRelay();
706
+ yield* ensureExtensionConnected({
707
+ relay,
708
+ sessionId,
709
+ waitForReconnect: shouldWaitForExtensionReconnect(readiness),
710
+ onWait: Console.error(`Waiting up to ${extensionReconnectWaitMs / 1000}s for the Browse extension to reconnect`)
711
+ });
712
+ });
713
+ var resolveSelectedSessionId = Effect3.fnUntraced(function* (explicitSessionId) {
714
+ const store = yield* Service2;
715
+ const sessionId = explicitSessionId ?? (yield* store.read);
716
+ if (!sessionId) {
717
+ return yield* Effect3.fail(new Error("No session provided and no current Browse session exists"));
718
+ }
719
+ return sessionId;
720
+ });
721
+ var resolvePositionalOrFlagSessionId = Effect3.fnUntraced(function* (id, session) {
722
+ return yield* resolveSelectedSessionId(resolveExplicitSessionSelector({
723
+ positional: Option2.getOrUndefined(id),
724
+ flag: Option2.getOrUndefined(session),
725
+ environment: Option2.getOrUndefined(yield* sessionIdConfig)
726
+ }));
727
+ });
728
+ var ensureSessionExists = Effect3.fnUntraced(function* (id) {
729
+ const relay = yield* Service;
730
+ yield* ensureCliRelay();
731
+ const sessions = yield* relay.sessions;
732
+ const exists = sessions.some((session) => {
733
+ return session.id === id;
734
+ });
735
+ if (!exists) {
736
+ return yield* Effect3.fail(new Error(`Session not found: ${id}`));
737
+ }
738
+ });
739
+ var recordingTarget = Effect3.fnUntraced(function* (options) {
740
+ const sessionId = Option2.getOrUndefined(options.session);
741
+ const tabId = Option2.getOrUndefined(options.tabId);
742
+ if (sessionId && tabId !== undefined) {
743
+ return yield* Effect3.fail(new Error("Use only one recording target selector: --session or --tab-id"));
744
+ }
745
+ return {
746
+ ...sessionId ? { sessionId } : {},
747
+ ...tabId === undefined ? {} : { tabId }
748
+ };
749
+ });
750
+ var parseRecordingModeOption = Effect3.fnUntraced(function* (value) {
751
+ if (value === undefined) {
752
+ return;
753
+ }
754
+ if (value === "auto" || value === "tab-capture" || value === "cdp") {
755
+ return value;
756
+ }
757
+ return yield* Effect3.fail(new Error("Recording mode must be auto, tab-capture, or cdp"));
758
+ });
759
+ function formatExecuteLogs(logs) {
760
+ return [
761
+ "Console logs:",
762
+ ...logs.map((log) => {
763
+ const location = log.location?.url ? ` ${log.location.url}:${log.location.lineNumber}:${log.location.columnNumber}` : "";
764
+ return `[${log.source}:${log.type}]${location} ${log.text}`;
765
+ })
766
+ ].join(`
767
+ `);
768
+ }
769
+ function formatAftermath(aftermath) {
770
+ const parts = [];
771
+ if (aftermath.startUrl !== aftermath.endUrl) {
772
+ parts.push(`Page: ${aftermath.startUrl ?? "none"} -> ${aftermath.endUrl ?? "none"}`);
773
+ }
774
+ if (aftermath.navigations.length > 1) {
775
+ parts.push(`navigations=${aftermath.navigations.length}`);
776
+ }
777
+ if (aftermath.pageErrorCount > 0) {
778
+ parts.push(`pageErrors=${aftermath.pageErrorCount}`);
779
+ }
780
+ if (aftermath.handoffs > 0) {
781
+ parts.push(`handoffs=${aftermath.handoffs}`);
782
+ }
783
+ return parts.length > 0 ? parts.join(" ") : null;
784
+ }
785
+ var ScriptErrorEnvelope = Schema3.TaggedStruct("ScriptError", {
786
+ message: Schema3.String
787
+ });
788
+ function executeJsonEnvelope(result) {
789
+ const hasStructuredValue = Object.hasOwn(result, "value") && result.value !== undefined;
790
+ return {
791
+ ok: !result.isError,
792
+ isError: result.isError,
793
+ text: result.text,
794
+ value: hasStructuredValue ? result.value : null,
795
+ valueUnavailable: !hasStructuredValue,
796
+ ...result.isError ? { error: ScriptErrorEnvelope.make({ message: result.text }) } : {},
797
+ logs: result.logs,
798
+ warnings: result.warnings ?? [],
799
+ ...result.diagnostic ? { diagnostic: result.diagnostic } : {},
800
+ ...result.aftermath ? { aftermath: result.aftermath } : {},
801
+ session: result.session
802
+ };
803
+ }
804
+ function formatSessionContinuation(sessionId) {
805
+ return `Session: ${sessionId}. Continue with --session ${sessionId}.`;
806
+ }
807
+ function errorJsonEnvelope(error) {
808
+ const tag = Predicate2.isObject(error) && Predicate2.hasProperty(error, "_tag") && Predicate2.isString(error._tag) ? error._tag : "Error";
809
+ const message = error instanceof Error ? error.message : String(error);
810
+ const errorValue = Object.assign({ message }, { _tag: tag });
811
+ return { ok: false, isError: true, text: message, value: null, valueUnavailable: true, error: errorValue, logs: [], warnings: [] };
812
+ }
813
+ var serve = Command.make("serve", {}, Effect3.fn("Cli.serve")(function* () {
814
+ const [{ startRelay }, { parseAdditionalExtensionOrigins }] = yield* Effect3.promise(() => Promise.all([import("./relay-ns525pms.mjs"), import("./relay-helpers-pjcbxs7p.mjs")]));
815
+ const port = yield* portConfig;
816
+ const additionalExtensionOrigins = parseAdditionalExtensionOrigins(yield* extensionOriginsConfig);
817
+ yield* Effect3.scoped(Effect3.gen(function* () {
818
+ const relay = yield* startRelay({ port, additionalExtensionOrigins });
819
+ yield* Console.log(`Browse relay listening at ${relay.url}`);
820
+ yield* Console.log(`Browse connects automatically while the browser runs. Run \`${cliCommand("status")}\` to check.`);
821
+ yield* Effect3.never;
822
+ }));
823
+ })).pipe(Command.withDescription("Start the local Browse relay"));
824
+ var relayRestart = Command.make("restart", {
825
+ force: Flag.Boolean("force").pipe(Flag.withDefault(false), Flag.withDescription("End network captures and recordings (discarding them) and disconnect raw CDP clients instead of refusing to restart"))
826
+ }, Effect3.fn("Cli.relayRestart")(function* ({ force }) {
827
+ const relay = yield* Service;
828
+ yield* Console.error("Restarting the relay leaves browser tabs open but resets in-memory JavaScript state.");
829
+ const readiness = yield* restartRelay({ relay, clientKind: "cli", force });
830
+ yield* Console.log(`${readiness.started ? "Started" : "Reused concurrent replacement"} Browse relay at ${relay.endpoint} (${readiness.version.buildId})`);
831
+ })).pipe(Command.withDescription("Explicitly drain and restart the managed relay using this installation"));
832
+ var relay = Command.make("relay").pipe(Command.withDescription("Manage the local relay lifecycle"), Command.withSubcommands([relayRestart]));
833
+ var execute = Command.make("execute", {
834
+ code: Argument.String("code").pipe(Argument.variadic({ min: 0 })),
835
+ file: Flag.String("file").pipe(Flag.optional, Flag.withDescription("Read execute code from a file")),
836
+ session: sessionFlag("Continue an existing Browse session; defaults to the OpenCode agent's session"),
837
+ rm: Flag.Boolean("rm").pipe(Flag.withDefault(false), Flag.withDescription("Use a fresh one-shot session and close its tabs afterward")),
838
+ timeoutMs: Flag.Int("timeout-ms").pipe(Flag.optional, Flag.withDescription("Overall script timeout in milliseconds (paused during handoff); defaults to 60000, 0 disables")),
839
+ targetUrl: Flag.String("target-url").pipe(Flag.optional, Flag.withDescription("Use the attached page whose URL contains this text")),
840
+ targetIndex: Flag.Int("target-index").pipe(Flag.optional, Flag.withDescription("Use the attached page at this zero-based index")),
841
+ json: Flag.Boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Print a machine-readable result envelope: { ok, isError, text, value, valueUnavailable, error?, logs, warnings, diagnostic?, aftermath, session }"))
842
+ }, Effect3.fn("Cli.execute")(function* ({ code, file, session, rm, timeoutMs, targetUrl, targetIndex, json }) {
843
+ const run = Effect3.gen(function* () {
844
+ const relay = yield* Service;
845
+ const filePath = Option2.getOrUndefined(file);
846
+ if (code.length > 0 && filePath) {
847
+ return yield* Effect3.fail(new Error("Use either positional code or --file, not both"));
848
+ }
849
+ if (code.length === 0 && !filePath) {
850
+ return yield* Effect3.fail(new Error("Execute requires positional code or --file <path>"));
851
+ }
852
+ const timeoutMsValue = Option2.getOrUndefined(timeoutMs);
853
+ if (timeoutMsValue !== undefined && timeoutMsValue < 0) {
854
+ return yield* Effect3.fail(new Error("Timeout must be a non-negative integer"));
855
+ }
856
+ const executeCode = filePath ? yield* readExecuteFile(filePath) : code.join(" ");
857
+ const explicitSessionId = Option2.getOrUndefined(session) ?? Option2.getOrUndefined(yield* sessionIdConfig);
858
+ if (rm && explicitSessionId) {
859
+ return yield* Effect3.fail(new Error("Use --rm without --session or OPENCODE_BROWSER_SESSION"));
860
+ }
861
+ const agentWorkSessionId = agentDefaultSessionId(Option2.getOrUndefined(yield* agentSessionIdConfig));
862
+ const workSessionId = explicitSessionId ?? (rm ? undefined : agentWorkSessionId);
863
+ yield* ensureCliRelayAndExtension(workSessionId);
864
+ const targetUrlValue = Option2.getOrUndefined(targetUrl) ?? Option2.getOrUndefined(yield* targetUrlConfig);
865
+ const targetIndexValue = Option2.getOrUndefined(targetIndex) ?? Option2.getOrUndefined(yield* targetIndexConfig);
866
+ if (targetIndexValue !== undefined && targetIndexValue < 0) {
867
+ return yield* Effect3.fail(new Error("Target index must be a non-negative integer"));
868
+ }
869
+ if (targetUrlValue && targetIndexValue !== undefined) {
870
+ return yield* Effect3.fail(new Error("Use only one target selector: --target-url/OPENCODE_BROWSER_TARGET_URL or --target-index/OPENCODE_BROWSER_TARGET_INDEX"));
871
+ }
872
+ const result = yield* relay.execute({
873
+ ...workSessionId ? { sessionId: workSessionId } : {},
874
+ code: executeCode,
875
+ createIfMissing: !explicitSessionId || explicitSessionId === agentWorkSessionId,
876
+ ...rm ? { removeAfterExecute: true } : {},
877
+ ...timeoutMsValue === undefined ? {} : { timeoutMs: timeoutMsValue },
878
+ ...targetUrlValue || targetIndexValue !== undefined ? {
879
+ targetSelection: {
880
+ ...targetUrlValue ? { urlIncludes: targetUrlValue } : {},
881
+ ...targetIndexValue !== undefined ? { index: targetIndexValue } : {}
882
+ }
883
+ } : {}
884
+ });
885
+ if (!explicitSessionId && !rm) {
886
+ yield* Console.error(formatSessionContinuation(result.session.id));
887
+ }
888
+ return result;
889
+ });
890
+ if (json) {
891
+ const outcome = yield* Effect3.result(run);
892
+ const envelope = Result.isFailure(outcome) ? errorJsonEnvelope(outcome.failure) : executeJsonEnvelope(outcome.success);
893
+ yield* emitJson(envelope);
894
+ if (!envelope.ok) {
895
+ yield* stderrLine(envelope.error?.message ?? envelope.text);
896
+ yield* exitWith(Result.isFailure(outcome) ? exitCodeForError(outcome.failure) : ExitCode.failure);
897
+ }
898
+ return;
899
+ }
900
+ const outcome = yield* Effect3.result(run);
901
+ if (Result.isFailure(outcome)) {
902
+ yield* Console.error(outcome.failure.message);
903
+ yield* exitWith(exitCodeForError(outcome.failure));
904
+ return;
905
+ }
906
+ const result = outcome.success;
907
+ const print = result.isError ? Console.error : Console.log;
908
+ yield* print(result.text);
909
+ if (result.logs.length > 0) {
910
+ yield* print(formatExecuteLogs(result.logs));
911
+ }
912
+ yield* Effect3.forEach(result.warnings ?? [], (warning) => print(`Warning: ${warning}`));
913
+ if (result.diagnostic) {
914
+ yield* print(`Diagnostic: ${result.diagnostic}`);
915
+ }
916
+ const aftermath = result.aftermath ? formatAftermath(result.aftermath) : null;
917
+ if (aftermath) {
918
+ yield* print(aftermath);
919
+ }
920
+ if (result.isError) {
921
+ yield* failExit;
922
+ }
923
+ })).pipe(Command.withDescription("Execute Playwright code against the attached browser"));
924
+ var sessionNew = Command.make("new", {
925
+ name: Argument.String("name").pipe(Argument.optional, Argument.withDescription("Optional lowercase session id")),
926
+ readOnly: Flag.Boolean("read-only").pipe(Flag.withDefault(false), Flag.withDescription("Create a read-only session: the relay rejects input-dispatching CDP so scripts can inspect but not click or type")),
927
+ allowOrigin: Flag.String("allow-origin").pipe(Flag.atLeast(0), Flag.withDescription("Only let this session's tabs load these hosts or origins (repeatable or comma-separated): example.com, *.example.com, https://app.example.com, http://localhost:3000"))
928
+ }, Effect3.fn("Cli.sessionNew")(function* ({ name, readOnly, allowOrigin }) {
929
+ const relay = yield* Service;
930
+ yield* ensureCliRelay();
931
+ const store = yield* Service2;
932
+ const result = yield* relay.sessionNew(Option2.getOrUndefined(name), {
933
+ ...readOnly ? { readOnly: true } : {},
934
+ ...allowOrigin.length > 0 ? { allowedOrigins: allowOrigin } : {}
935
+ });
936
+ yield* store.write(result.id);
937
+ yield* Console.log(result.id);
938
+ })).pipe(Command.withDescription("Create a Browse session and make it current"));
939
+ var sessionList = Command.make("list", {
940
+ json: jsonFlag
941
+ }, Effect3.fn("Cli.sessionList")(function* ({ json }) {
942
+ const relay = yield* Service;
943
+ yield* ensureCliRelay();
944
+ const store = yield* Service2;
945
+ const sessions = yield* relay.sessions;
946
+ const current = yield* store.read;
947
+ if (json) {
948
+ yield* emitJson({ current: current ?? undefined, sessions });
949
+ return;
950
+ }
951
+ if (sessions.length === 0) {
952
+ yield* Console.log("No sessions");
953
+ return;
954
+ }
955
+ yield* Effect3.forEach(sessions, (item) => {
956
+ const marker = item.id === current ? "*" : " ";
957
+ const page = item.pageUrl ?? "no page yet";
958
+ const keys = item.stateKeys.length ? ` state=${item.stateKeys.join(",")}` : "";
959
+ const readOnly = item.readOnly ? " read-only" : "";
960
+ const allowed = item.allowedOrigins ? ` allow=${item.allowedOrigins.join(",")}` : "";
961
+ return Console.log(`${marker} ${item.id} ${page}${keys}${readOnly}${allowed}`);
962
+ });
963
+ })).pipe(Command.withDescription("List Browse sessions"));
964
+ var sessionCurrent = Command.make("current", {}, Effect3.fn("Cli.sessionCurrent")(function* () {
965
+ const store = yield* Service2;
966
+ const current = yield* store.read;
967
+ yield* Console.log(current ?? "none");
968
+ })).pipe(Command.withDescription("Print the current default session"));
969
+ var sessionUse = Command.make("use", {
970
+ id: Argument.String("id")
971
+ }, Effect3.fn("Cli.sessionUse")(function* ({ id }) {
972
+ const store = yield* Service2;
973
+ yield* ensureSessionExists(id);
974
+ yield* store.write(id);
975
+ yield* Console.log(id);
976
+ })).pipe(Command.withDescription("Set the current default session"));
977
+ var sessionReset = Command.make("reset", {
978
+ id: Argument.String("id").pipe(Argument.optional),
979
+ session: sessionFlag("Reset this Browse session id")
980
+ }, Effect3.fn("Cli.sessionReset")(function* ({ id, session }) {
981
+ const relay = yield* Service;
982
+ const sessionId = yield* resolvePositionalOrFlagSessionId(id, session);
983
+ yield* ensureCliRelay();
984
+ const reset = yield* relay.sessionReset(sessionId);
985
+ if (reset.stopped)
986
+ yield* Console.error(`Stopped first: ${describeStop(reset.stopped)}`);
987
+ yield* Console.log(reset.session.id);
988
+ })).pipe(Command.withDescription("Reset a Browse session state and page"));
989
+ var sessionAdopt = Command.make("adopt", {
990
+ session: sessionFlag("Adopt into this Browse session, creating it when it does not exist yet; omit to create a fresh readable id"),
991
+ targetUrl: Flag.String("target-url").pipe(Flag.optional, Flag.withDescription("Adopt the attached page whose URL contains this text")),
992
+ targetIndex: Flag.Int("target-index").pipe(Flag.optional, Flag.withDescription("Adopt the attached page at this zero-based target index")),
993
+ tabId: tabIdFlag("Adopt the user's shared tab with this Chrome tab id (the tab= value in status)")
994
+ }, Effect3.fn("Cli.sessionAdopt")(function* ({ session, targetUrl, targetIndex, tabId }) {
995
+ const relay = yield* Service;
996
+ const explicitSessionId = Option2.getOrUndefined(session) ?? Option2.getOrUndefined(yield* sessionIdConfig);
997
+ yield* ensureCliRelayAndExtension(explicitSessionId);
998
+ const targetUrlValue = Option2.getOrUndefined(targetUrl);
999
+ const targetIndexValue = Option2.getOrUndefined(targetIndex);
1000
+ if (targetIndexValue !== undefined && targetIndexValue < 0) {
1001
+ return yield* Effect3.fail(new Error("Target index must be a non-negative integer"));
1002
+ }
1003
+ const tabIdValue = Option2.getOrUndefined(tabId);
1004
+ if ([targetUrlValue, targetIndexValue, tabIdValue].filter((value) => value !== undefined).length > 1) {
1005
+ return yield* Effect3.fail(new Error("Use only one target selector: --target-url, --target-index, or --tab-id"));
1006
+ }
1007
+ const targetSelection = targetUrlValue ? { urlIncludes: targetUrlValue } : targetIndexValue !== undefined ? { index: targetIndexValue } : undefined;
1008
+ const result = yield* relay.sessionAdopt({
1009
+ ...explicitSessionId ? { sessionId: explicitSessionId } : {},
1010
+ createIfMissing: true,
1011
+ ...targetSelection ? { targetSelection } : {},
1012
+ ...tabIdValue === undefined ? {} : { tabId: tabIdValue }
1013
+ });
1014
+ yield* Console.log(`${result.session.created ? "Created and adopted" : "Adopted"} session '${result.session.id}' default page: ${result.adoptedUrl}`);
1015
+ if (result.session.created) {
1016
+ yield* Console.error(formatSessionContinuation(result.session.id));
1017
+ }
1018
+ })).pipe(Command.withDescription("Make an attached tab the session's default page"));
1019
+ function describeStop(stopped) {
1020
+ return [
1021
+ stopped.execute ? "a running execute" : undefined,
1022
+ stopped.handoff ? "a pending handoff" : undefined
1023
+ ].filter(Boolean).join(" and ") + (stopped.forced ? " (the script did not stop in time and was abandoned with its state)" : "");
1024
+ }
1025
+ var sessionDelete = Command.make("delete", {
1026
+ id: Argument.String("id").pipe(Argument.optional),
1027
+ session: sessionFlag("Delete this Browse session id")
1028
+ }, Effect3.fn("Cli.sessionDelete")(function* ({ id, session }) {
1029
+ const relay = yield* Service;
1030
+ const store = yield* Service2;
1031
+ const sessionId = yield* resolvePositionalOrFlagSessionId(id, session);
1032
+ yield* ensureCliRelay();
1033
+ const deleted = yield* relay.sessionDelete(sessionId);
1034
+ if (deleted.stopped)
1035
+ yield* Console.error(`Stopped first: ${describeStop(deleted.stopped)}`);
1036
+ const current = yield* store.read;
1037
+ if (current === sessionId) {
1038
+ yield* store.clear;
1039
+ }
1040
+ yield* Console.log(sessionId);
1041
+ })).pipe(Command.withDescription("Delete a Browse session"));
1042
+ var sessionAllow = Command.make("allow", {
1043
+ session: sessionFlag(),
1044
+ origins: Argument.String("origins").pipe(Argument.variadic({ min: 0 }), Argument.withDescription("Hosts or origins the session's tabs may load: example.com, *.example.com, https://app.example.com, http://localhost:3000")),
1045
+ clear: Flag.Boolean("clear").pipe(Flag.withDefault(false), Flag.withDescription("Remove the allowlist: the session may load any site again")),
1046
+ json: jsonFlag
1047
+ }, Effect3.fn("Cli.sessionAllow")(function* ({ session, origins, clear, json }) {
1048
+ const relay = yield* Service;
1049
+ yield* ensureCliRelay();
1050
+ if (!clear && origins.length === 0) {
1051
+ return yield* Effect3.fail(new Error("session allow needs origins, or --clear to remove the allowlist"));
1052
+ }
1053
+ const id = yield* resolvePositionalOrFlagSessionId(Option2.none(), session);
1054
+ const result = yield* relay.sessionAllowlist(id, clear ? [] : origins);
1055
+ if (json) {
1056
+ yield* emitJson(result);
1057
+ return;
1058
+ }
1059
+ yield* Console.log(result.allowedOrigins ? `${result.id} may load: ${result.allowedOrigins.join(", ")}` : `${result.id} may load any site`);
1060
+ })).pipe(Command.withDescription("Set the domain allowlist of a session (the current one by default)"));
1061
+ var session = Command.make("session").pipe(Command.withDescription("Manage Browse sessions"), Command.withSubcommands([sessionNew, sessionList, sessionCurrent, sessionUse, sessionReset, sessionAdopt, sessionDelete, sessionAllow]));
1062
+ var status = Command.make("status", {
1063
+ json: jsonFlag
1064
+ }, Effect3.fn("Cli.status")(function* ({ json }) {
1065
+ const relay = yield* Service;
1066
+ const store = yield* Service2;
1067
+ const [relayResult, update] = yield* Effect3.all([Effect3.result(relay.version), Effect3.promise(() => updateNotice2())], { concurrency: 2 });
1068
+ const updateLine = update ? formatUpdateNotice2(update) : undefined;
1069
+ if (Result.isFailure(relayResult)) {
1070
+ if (!(relayResult.failure instanceof RelayUnreachable)) {
1071
+ if (json) {
1072
+ yield* emitJson({
1073
+ endpoint: relay.endpoint,
1074
+ relay: { running: false, error: relayResult.failure.message },
1075
+ extension: null,
1076
+ sessions: [],
1077
+ targets: [],
1078
+ update: updateJson2(update)
1079
+ });
1080
+ }
1081
+ yield* stderrLine(`Relay status failed: ${relayResult.failure.message}`);
1082
+ yield* exitWith(ExitCode.host);
1083
+ return;
1084
+ }
1085
+ const stopped = stoppedRelayStatus(relay.endpoint);
1086
+ if (json) {
1087
+ yield* emitJson({ ...stopped, update: updateJson2(update), autoUpdate: autoUpdateStatus() });
1088
+ yield* stderrLine(`relay is not running at ${relay.endpoint}`);
1089
+ } else {
1090
+ yield* Console.log(`Relay: stopped (${relay.endpoint})`);
1091
+ yield* Console.log(`Run \`${cliCommand("execute")}\` (or any relay-backed command) to start it automatically.`);
1092
+ if (updateLine)
1093
+ yield* Console.log(updateLine);
1094
+ }
1095
+ yield* exitWith(ExitCode.host);
1096
+ return;
1097
+ }
1098
+ const version = relayResult.success;
1099
+ const buildProblem = relayBuildProblem(version);
1100
+ const [extensionStatus, current] = yield* Effect3.all([relay.extensionStatus, store.read]);
1101
+ const collections = statusCollections(extensionStatus);
1102
+ const [sessions, targets] = collections ? [collections.sessions, collections.targets] : yield* Effect3.all([relay.sessions, relay.targets]);
1103
+ if (json) {
1104
+ yield* emitJson({
1105
+ endpoint: relay.endpoint,
1106
+ relay: { running: true, version: version.version, buildId: version.buildId ?? null, stale: buildProblem !== undefined },
1107
+ extension: extensionStatus,
1108
+ currentSession: current ?? null,
1109
+ sessions,
1110
+ targets,
1111
+ update: updateJson2(update),
1112
+ autoUpdate: autoUpdateStatus()
1113
+ });
1114
+ if (buildProblem) {
1115
+ yield* stderrLine(buildProblem);
1116
+ yield* failExit;
1117
+ }
1118
+ return;
1119
+ }
1120
+ if (updateLine)
1121
+ yield* Console.log(updateLine);
1122
+ yield* Console.log(`Relay: ${relay.endpoint} (${version.version})`);
1123
+ if (buildProblem) {
1124
+ yield* Console.log(`Warning: ${buildProblem}`);
1125
+ }
1126
+ yield* Console.log(`Extension: ${extensionStatus.connected ? "connected" : "disconnected"}${extensionStatus.version ? ` (${extensionStatus.version})` : ""}`);
1127
+ if ((extensionStatus.rejectedConnections ?? 0) > 0) {
1128
+ yield* Console.log(`Warning: ${extensionStatus.rejectedConnections} competing browser/profile connection attempt(s) rejected; the active connection was preserved. Use Browse in one browser/profile at a time.`);
1129
+ }
1130
+ if (extensionStatus.nextStep)
1131
+ yield* Console.log(extensionStatus.nextStep);
1132
+ else if ((extensionStatus.pairingFailures ?? 0) > 0 && !extensionStatus.connected) {
1133
+ yield* Console.log(`Warning: ${extensionStatus.pairingFailures} extension connection(s) failed pairing. Update the extension, or run \`${cliCommand("pair")}\` and enter the code on its status page.`);
1134
+ }
1135
+ if ((extensionStatus.processFaults?.suppressed ?? 0) > 0) {
1136
+ const faults = extensionStatus.processFaults;
1137
+ yield* Console.log(`Warning: the relay survived ${faults.suppressed} Playwright fault(s)${faults.lastAt ? `, last at ${faults.lastAt}` : ""}${faults.lastMessage ? `: ${faults.lastMessage}` : ""}. See the relay log.`);
1138
+ }
1139
+ if (extensionStatus.protocolVersion !== undefined && extensionStatus.protocolVersion !== null) {
1140
+ const compatibility = extensionStatus.protocolCompatible === false ? "incompatible" : "compatible";
1141
+ const legacy = extensionStatus.protocolLegacy === true ? ", inferred from legacy hello" : "";
1142
+ yield* Console.log(`Extension protocol: ${extensionStatus.protocolVersion} (${compatibility}${legacy})`);
1143
+ if (extensionStatus.protocolCompatible === false && !extensionStatus.nextStep)
1144
+ yield* Console.log(extensionProtocolAdvice(extensionStatus.protocolVersion, { install: installCommand() }));
1145
+ }
1146
+ yield* Console.log(`Active targets: ${extensionStatus.activeTargets}`);
1147
+ if (extensionStatus.childTargets !== undefined) {
1148
+ yield* Console.log(`Child targets: ${extensionStatus.childTargets}`);
1149
+ }
1150
+ if (extensionStatus.cdpClients !== undefined) {
1151
+ yield* Console.log(`CDP clients: ${extensionStatus.cdpClients}`);
1152
+ }
1153
+ yield* Console.log(`Current session: ${current ?? "none"}`);
1154
+ if (sessions.length === 0) {
1155
+ yield* Console.log("Sessions: none");
1156
+ } else {
1157
+ yield* Console.log("Sessions:");
1158
+ const activeSessions = sessions.filter((item) => item.id === current || item.connected || item.pageUrl !== null || item.stateKeys.length > 0 || item.readOnly);
1159
+ const idleSessions = sessions.filter((item) => !activeSessions.includes(item));
1160
+ const shownSessions = activeSessions.length > 0 ? activeSessions : idleSessions.slice(0, 5);
1161
+ const omittedIdleCount = activeSessions.length > 0 ? idleSessions.length : Math.max(0, idleSessions.length - shownSessions.length);
1162
+ yield* Effect3.forEach(shownSessions, (item) => {
1163
+ const marker = item.id === current ? "*" : " ";
1164
+ return Console.log(`${marker} ${item.id} ${item.pageUrl ?? "no page yet"}`);
1165
+ });
1166
+ if (omittedIdleCount > 0) {
1167
+ yield* Console.log(` (${omittedIdleCount} idle session(s) with no page omitted; run ${cliCommand("session list")} to view all)`);
1168
+ }
1169
+ }
1170
+ if (targets.length === 0) {
1171
+ yield* Console.log("Targets: none");
1172
+ } else {
1173
+ yield* Console.log("Targets:");
1174
+ yield* Effect3.forEach(targets, (target, index) => {
1175
+ return Console.log(`- [${index}] ${formatTargetSummary(target, { includeSession: true })}`);
1176
+ });
1177
+ }
1178
+ if (buildProblem) {
1179
+ yield* failExit;
1180
+ }
1181
+ })).pipe(Command.withDescription("Show relay, extension, and target status"));
1182
+ var recordingStart = Command.make("start", {
1183
+ outputPath: Argument.String("output-path").pipe(Argument.withDescription("Path to write the recording artifact; tabCapture requires .webm, CDP accepts .webm or .mp4")),
1184
+ session: sessionFlag("Record the page for this Browse or CDP session id"),
1185
+ tabId: tabIdFlag("Record this attached Chrome tab id"),
1186
+ mode: Flag.String("mode").pipe(Flag.optional, Flag.withDescription("Recording mode: auto, tab-capture, or cdp. auto uses CDP for relay-owned tabs and tabCapture for user-owned tabs")),
1187
+ audio: Flag.Boolean("audio").pipe(Flag.withDefault(false), Flag.withDescription("Include tab audio")),
1188
+ frameRate: Flag.Int("frame-rate").pipe(Flag.optional, Flag.withDescription("Output frame rate, integer 1..60; defaults to 30 for tab-capture and 60 for CDP")),
1189
+ maxDurationMs: Flag.Int("max-duration-ms").pipe(Flag.optional, Flag.withDescription("Auto-stop guard in milliseconds, defaults to 900000")),
1190
+ cursor: Flag.Boolean("cursor").pipe(Flag.withDefault(false), Flag.withDescription("Draw the ghost cursor in the recording (off by default: only the page is recorded)")),
1191
+ json: jsonFlag
1192
+ }, Effect3.fn("Cli.recordingStart")(function* ({ outputPath, session, tabId, mode, audio, frameRate, maxDurationMs, cursor, json }) {
1193
+ if (Option2.isSome(frameRate) && (frameRate.value < 1 || frameRate.value > 60)) {
1194
+ return yield* Effect3.fail(new Error("Recording frameRate must be an integer from 1 to 60"));
1195
+ }
1196
+ const relay = yield* Service;
1197
+ yield* ensureCliRelayAndExtension();
1198
+ const target = yield* recordingTarget({ session, tabId });
1199
+ const modeValue = yield* parseRecordingModeOption(Option2.getOrUndefined(mode));
1200
+ const resolvedOutputPath = path.resolve(outputPath);
1201
+ const result = yield* relay.recordingStart({
1202
+ ...target,
1203
+ outputPath: resolvedOutputPath,
1204
+ ...modeValue === undefined ? {} : { mode: modeValue },
1205
+ audio,
1206
+ ...Option2.isSome(frameRate) ? { frameRate: frameRate.value } : {},
1207
+ ...Option2.isSome(maxDurationMs) ? { maxDurationMs: maxDurationMs.value } : {},
1208
+ ...cursor ? { cursor } : {}
1209
+ });
1210
+ if (!result.success) {
1211
+ return yield* Effect3.fail(new Error(result.error ?? "Failed to start recording"));
1212
+ }
1213
+ yield* printOr(json, result, `Recording started: ${result.path ?? resolvedOutputPath} tab=${result.tabId ?? "unknown"} mode=${result.mode ?? "tab-capture"} artifact=${result.artifactType ?? "webm"} mime=${result.mimeType ?? "video/webm"} fps=${result.frameRate ?? "unknown"}`);
1214
+ })).pipe(Command.withDescription("Start recording an attached tab"));
1215
+ var recordingStop = Command.make("stop", {
1216
+ session: sessionFlag("Stop recording for this CDP session id"),
1217
+ tabId: tabIdFlag("Stop recording for this Chrome tab id"),
1218
+ json: jsonFlag
1219
+ }, Effect3.fn("Cli.recordingStop")(function* ({ session, tabId, json }) {
1220
+ const relay = yield* Service;
1221
+ yield* ensureCliRelay();
1222
+ const target = yield* recordingTarget({ session, tabId });
1223
+ const result = yield* relay.recordingStop(target);
1224
+ if (!result.success) {
1225
+ return yield* Effect3.fail(new Error(result.error ?? "Failed to stop recording"));
1226
+ }
1227
+ if (json)
1228
+ return yield* emitJson(result);
1229
+ const frames = result.frameCount === undefined ? "" : `, frames=${result.frameCount}`;
1230
+ yield* Console.log(`Recording saved: ${result.path ?? "unknown"} (${result.size ?? 0} bytes, ${result.duration ?? 0}ms, mode=${result.mode ?? "tab-capture"}, artifact=${result.artifactType ?? "webm"}${frames})`);
1231
+ yield* Console.log(formatRecordingQuality(result.quality));
1232
+ })).pipe(Command.withDescription("Stop recording and write the artifact"));
1233
+ var recordingStatus = Command.make("status", {
1234
+ session: sessionFlag("Check recording for this CDP session id"),
1235
+ tabId: tabIdFlag("Check recording for this Chrome tab id"),
1236
+ json: jsonFlag
1237
+ }, Effect3.fn("Cli.recordingStatus")(function* ({ session, tabId, json }) {
1238
+ const relay = yield* Service;
1239
+ yield* ensureCliRelay();
1240
+ const target = yield* recordingTarget({ session, tabId });
1241
+ const result = yield* relay.recordingStatus(target);
1242
+ if (json) {
1243
+ yield* emitJson(result);
1244
+ return;
1245
+ }
1246
+ if (!result.isRecording) {
1247
+ yield* Console.log("Recording: inactive");
1248
+ return;
1249
+ }
1250
+ const frames = result.frameCount === undefined ? "" : ` frameCount=${result.frameCount}`;
1251
+ yield* Console.log(`Recording: active tab=${result.tabId ?? "unknown"} mode=${result.mode ?? "tab-capture"} artifact=${result.artifactType ?? "webm"} path=${result.path ?? "unknown"} size=${result.size ?? 0}${frames} startedAt=${result.startedAt ?? "unknown"}`);
1252
+ yield* Console.log(formatRecordingQuality(result.quality));
1253
+ })).pipe(Command.withDescription("Check current recording status"));
1254
+ var recordingCancel = Command.make("cancel", {
1255
+ session: sessionFlag("Cancel recording for this CDP session id"),
1256
+ tabId: tabIdFlag("Cancel recording for this Chrome tab id")
1257
+ }, Effect3.fn("Cli.recordingCancel")(function* ({ session, tabId }) {
1258
+ const relay = yield* Service;
1259
+ yield* ensureCliRelay();
1260
+ const target = yield* recordingTarget({ session, tabId });
1261
+ const result = yield* relay.recordingCancel(target);
1262
+ if (!result.success) {
1263
+ return yield* Effect3.fail(new Error(result.error ?? "Failed to cancel recording"));
1264
+ }
1265
+ yield* Console.log("Recording cancelled");
1266
+ })).pipe(Command.withDescription("Cancel recording without writing a file"));
1267
+ var recording = Command.make("recording").pipe(Command.withDescription("Record an attached tab to WebM or MP4"), Command.withSubcommands([recordingStart, recordingStop, recordingStatus, recordingCancel]));
1268
+ var flightRecorderStart = Command.make("start", {
1269
+ session: sessionFlag("Buffer the tab owned by this session"),
1270
+ tabId: tabIdFlag("Buffer this attached Chrome tab id"),
1271
+ retentionMs: Flag.Int("retention-ms").pipe(Flag.optional, Flag.withDescription("Ring-buffer duration in milliseconds (1000..120000; default 60000)")),
1272
+ frameRate: Flag.Int("frame-rate").pipe(Flag.optional, Flag.withDescription("Output frame rate from 1 to 60")),
1273
+ cursor: Flag.Boolean("cursor").pipe(Flag.withDefault(false), Flag.withDescription("Draw the ghost cursor in the buffered video (off by default)")),
1274
+ json: jsonFlag
1275
+ }, Effect3.fn("Cli.flightRecorderStart")(function* ({ session, tabId, retentionMs, frameRate, cursor, json }) {
1276
+ const relay = yield* Service;
1277
+ yield* ensureCliRelayAndExtension();
1278
+ const target = yield* recordingTarget({ session, tabId });
1279
+ const result = yield* relay.flightRecorderStart({
1280
+ ...target,
1281
+ ...Option2.isSome(retentionMs) ? { retentionMs: retentionMs.value } : {},
1282
+ ...Option2.isSome(frameRate) ? { frameRate: frameRate.value } : {},
1283
+ ...cursor ? { cursor } : {}
1284
+ });
1285
+ yield* printOr(json, result, `Flight recorder buffering tab ${result.tabId}; retention=${result.retentionMs}ms`);
1286
+ })).pipe(Command.withDescription("Start a rolling in-memory video buffer"));
1287
+ var flightRecorderStatus = Command.make("status", {
1288
+ session: sessionFlag(),
1289
+ tabId: tabIdFlag(),
1290
+ json: jsonFlag
1291
+ }, Effect3.fn("Cli.flightRecorderStatus")(function* ({ session, tabId, json }) {
1292
+ const relay = yield* Service;
1293
+ yield* ensureCliRelay();
1294
+ const result = yield* relay.flightRecorderStatus(yield* recordingTarget({ session, tabId }));
1295
+ if (json)
1296
+ return yield* emitJson(result);
1297
+ if (!result.active)
1298
+ return yield* Console.log("Flight recorder inactive");
1299
+ yield* Console.log(`Flight recorder tab=${result.tabId} frames=${result.bufferedFrames} retained=${result.retainedDurationMs}ms bytes=${result.bufferedBytes}`);
1300
+ })).pipe(Command.withDescription("Show rolling flight-recorder status"));
1301
+ var flightRecorderSaveLast = Command.make("save-last", {
1302
+ outputPath: Argument.String("output-path").pipe(Argument.withDescription("Fresh .webm or .mp4 artifact path")),
1303
+ session: sessionFlag(),
1304
+ tabId: tabIdFlag(),
1305
+ durationMs: Flag.Int("duration-ms").pipe(Flag.optional, Flag.withDescription("How much recent history to save; defaults to 30 seconds")),
1306
+ json: jsonFlag
1307
+ }, Effect3.fn("Cli.flightRecorderSaveLast")(function* ({ outputPath, session, tabId, durationMs, json }) {
1308
+ const relay = yield* Service;
1309
+ yield* ensureCliRelay();
1310
+ const result = yield* relay.flightRecorderSaveLast({
1311
+ ...yield* recordingTarget({ session, tabId }),
1312
+ outputPath: path.resolve(outputPath),
1313
+ ...Option2.isSome(durationMs) ? { durationMs: durationMs.value } : {}
1314
+ });
1315
+ yield* printOr(json, result, `Saved ${result.durationMs}ms flight recorder clip (${result.frameCount} frames) to ${result.path}`);
1316
+ })).pipe(Command.withDescription("Save the most recent buffered video without stopping the recorder"));
1317
+ var flightRecorderCancel = Command.make("cancel", {
1318
+ session: sessionFlag(),
1319
+ tabId: tabIdFlag()
1320
+ }, Effect3.fn("Cli.flightRecorderCancel")(function* ({ session, tabId }) {
1321
+ const relay = yield* Service;
1322
+ yield* ensureCliRelay();
1323
+ const result = yield* relay.flightRecorderCancel(yield* recordingTarget({ session, tabId }));
1324
+ yield* Console.log(result.cancelled ? "Flight recorder stopped" : "No active flight recorder");
1325
+ })).pipe(Command.withDescription("Stop and discard the rolling video buffer"));
1326
+ var flightRecorder = Command.make("flight-recorder").pipe(Command.withDescription("Keep and save a rolling buffer of recent browser video"), Command.withSubcommands([flightRecorderStart, flightRecorderStatus, flightRecorderSaveLast, flightRecorderCancel]));
1327
+ var networkSession = Effect3.fnUntraced(function* (session) {
1328
+ return yield* resolveSelectedSessionId(Option2.getOrUndefined(session) ?? Option2.getOrUndefined(yield* sessionIdConfig));
1329
+ });
1330
+ var parseNetworkContent = Effect3.fnUntraced(function* (content) {
1331
+ const value = Option2.getOrUndefined(content);
1332
+ if (value === undefined || value === "embed" || value === "omit")
1333
+ return value;
1334
+ return yield* Effect3.fail(new Error("Network content must be embed or omit"));
1335
+ });
1336
+ function formatNetworkStatus(status) {
1337
+ if (!status.active)
1338
+ return status.ended ? `Network capture: inactive (the last one ended because ${status.ended})` : "Network capture: inactive";
1339
+ return `Network capture: active entries=${status.entryCount} responses=${status.responseCount} failures=${status.failureCount} bodyBytes=${status.capturedBodyBytes} truncated=${status.truncatedBodyCount} dropped=${status.droppedEntryCount} startedAt=${status.startedAt ?? "unknown"}${status.endsAt ? ` endsAt=${status.endsAt}` : ""}`;
1340
+ }
1341
+ function formatNetworkResult(result) {
1342
+ const output = result.outputPath ? ` output=${result.outputPath}` : "";
1343
+ const profile = result.authProfile ? ` secrets=${result.authProfile.name}(${result.authProfile.slotCount})` : "";
1344
+ return `Network capture stopped: entries=${result.entryCount} responses=${result.responseCount} failures=${result.failureCount} bodyBytes=${result.capturedBodyBytes} truncated=${result.truncatedBodyCount} dropped=${result.droppedEntryCount}${output}${profile}`;
1345
+ }
1346
+ var networkStart = Command.make("start", {
1347
+ session: sessionFlag("Capture the default page for this Browse session"),
1348
+ urlFilter: Flag.String("url").pipe(Flag.optional, Flag.withDescription("Capture only requests whose URL contains this text")),
1349
+ resourceTypes: Flag.String("resource-type").pipe(Flag.atMost(50), Flag.withDescription("Capture this Playwright resource type; repeat for multiple types")),
1350
+ content: Flag.String("content").pipe(Flag.optional, Flag.withDescription("Response and request body mode: embed (default) or omit")),
1351
+ maxBodyBytes: Flag.Int("max-body-bytes").pipe(Flag.optional, Flag.withDescription("Maximum captured bytes per body, defaults to 1000000")),
1352
+ maxTotalBodyBytes: Flag.Int("max-total-body-bytes").pipe(Flag.optional, Flag.withDescription("Maximum captured body bytes for the whole capture, defaults to 25000000")),
1353
+ maxEntries: Flag.Int("max-entries").pipe(Flag.optional, Flag.withDescription("Maximum request entries, defaults to 1000")),
1354
+ maxDuration: Flag.Int("max-duration-ms").pipe(Flag.optional, Flag.withDescription("End the capture by itself (discarding it) after this long, defaults to 1800000 (30 minutes)")),
1355
+ json: jsonFlag
1356
+ }, Effect3.fn("Cli.networkStart")(function* ({ session, urlFilter, resourceTypes, content, maxBodyBytes, maxTotalBodyBytes, maxEntries, maxDuration, json }) {
1357
+ const relay = yield* Service;
1358
+ yield* ensureCliRelayAndExtension();
1359
+ const sessionId = yield* networkSession(session);
1360
+ const contentValue = yield* parseNetworkContent(content);
1361
+ const result = yield* relay.networkStart({
1362
+ sessionId,
1363
+ ...Option2.isSome(urlFilter) ? { urlFilter: urlFilter.value } : {},
1364
+ ...resourceTypes.length === 0 ? {} : { resourceTypes },
1365
+ ...contentValue === undefined ? {} : { content: contentValue },
1366
+ ...Option2.isSome(maxBodyBytes) ? { maxBodyBytes: maxBodyBytes.value } : {},
1367
+ ...Option2.isSome(maxTotalBodyBytes) ? { maxTotalBodyBytes: maxTotalBodyBytes.value } : {},
1368
+ ...Option2.isSome(maxEntries) ? { maxEntries: maxEntries.value } : {},
1369
+ ...Option2.isSome(maxDuration) ? { maxDurationMs: maxDuration.value } : {}
1370
+ });
1371
+ yield* printOr(json, result, formatNetworkStatus(result));
1372
+ })).pipe(Command.withDescription("Start session-scoped network capture"));
1373
+ var networkStatus = Command.make("status", {
1374
+ session: sessionFlag(),
1375
+ json: jsonFlag
1376
+ }, Effect3.fn("Cli.networkStatus")(function* ({ session, json }) {
1377
+ const relay = yield* Service;
1378
+ const sessionId = yield* networkSession(session);
1379
+ yield* ensureCliRelay();
1380
+ const result = yield* relay.networkStatus({ sessionId });
1381
+ yield* printOr(json, result, formatNetworkStatus(result));
1382
+ })).pipe(Command.withDescription("Show session-scoped network capture status"));
1383
+ var networkStop = Command.make("stop", {
1384
+ session: sessionFlag(),
1385
+ output: Flag.String("output").pipe(Flag.optional, Flag.withAlias("o"), Flag.withDescription("Write a credential-redacted HAR artifact to this path")),
1386
+ secrets: Flag.String("secrets").pipe(Flag.optional, Flag.withDescription("Store captured credentials under this reusable profile name")),
1387
+ json: jsonFlag
1388
+ }, Effect3.fn("Cli.networkStop")(function* ({ session, output, secrets, json }) {
1389
+ const relay = yield* Service;
1390
+ const outputValue = Option2.getOrUndefined(output);
1391
+ const secretsValue = Option2.getOrUndefined(secrets);
1392
+ if (!outputValue && !secretsValue) {
1393
+ return yield* Effect3.fail(new Error("network stop requires --output, --secrets, or both"));
1394
+ }
1395
+ const sessionId = yield* networkSession(session);
1396
+ yield* ensureCliRelay();
1397
+ const result = yield* relay.networkStop({
1398
+ sessionId,
1399
+ ...outputValue ? { outputPath: path.resolve(outputValue) } : {},
1400
+ ...secretsValue ? { secrets: secretsValue } : {}
1401
+ });
1402
+ yield* printOr(json, result, formatNetworkResult(result));
1403
+ })).pipe(Command.withDescription("Stop capture and write a redacted artifact or reusable secret profile"));
1404
+ var networkCancel = Command.make("cancel", { session: sessionFlag() }, Effect3.fn("Cli.networkCancel")(function* ({ session }) {
1405
+ const relay = yield* Service;
1406
+ const sessionId = yield* networkSession(session);
1407
+ yield* ensureCliRelay();
1408
+ const result = yield* relay.networkCancel({ sessionId });
1409
+ yield* Console.log(result.cancelled ? "Network capture cancelled" : "Network capture was not active");
1410
+ })).pipe(Command.withDescription("Cancel capture without writing an artifact"));
1411
+ var network = Command.make("network").pipe(Command.withDescription("Capture authenticated network exchanges for direct client derivation"), Command.withSubcommands([networkStart, networkStatus, networkStop, networkCancel]));
1412
+ var secretsStatus = Command.make("status", {
1413
+ name: Argument.String("name"),
1414
+ json: jsonFlag
1415
+ }, Effect3.fn("Cli.secretsStatus")(function* ({ name, json }) {
1416
+ const relay = yield* Service;
1417
+ yield* ensureCliRelay();
1418
+ const result = yield* relay.authStatus({ name });
1419
+ if (json) {
1420
+ yield* emitJson(result);
1421
+ return;
1422
+ }
1423
+ yield* Console.log(`Secrets profile ${result.name}: slots=${result.slotCount} updatedAt=${result.updatedAt}`);
1424
+ yield* Effect3.forEach(result.slots, (slot) => Console.log(`- ${slot.ref} sources=${slot.sources.join(",")} expired=${slot.expired}${slot.expiresAt ? ` expiresAt=${slot.expiresAt}` : ""}`));
1425
+ })).pipe(Command.withDescription("Show secret profile metadata without revealing values"));
1426
+ var secretsRefresh = Command.make("refresh", {
1427
+ name: Argument.String("name"),
1428
+ session: sessionFlag(),
1429
+ urlFilter: Flag.String("url").pipe(Flag.optional, Flag.withDescription("Observe credentials only on matching request URLs")),
1430
+ timeoutMs: Flag.Int("timeout-ms").pipe(Flag.optional, Flag.withDescription("Page reload timeout, defaults to 30000")),
1431
+ json: jsonFlag
1432
+ }, Effect3.fn("Cli.secretsRefresh")(function* ({ name, session, urlFilter, timeoutMs, json }) {
1433
+ const relay = yield* Service;
1434
+ yield* ensureCliRelayAndExtension();
1435
+ const sessionId = yield* networkSession(session);
1436
+ const result = yield* relay.authRefresh({
1437
+ sessionId,
1438
+ name,
1439
+ ...Option2.isSome(urlFilter) ? { urlFilter: urlFilter.value } : {},
1440
+ ...Option2.isSome(timeoutMs) ? { timeoutMs: timeoutMs.value } : {}
1441
+ });
1442
+ yield* printOr(json, result, `Secrets profile ${name} refreshed: observed=${result.observedSecretRefs.length} changed=${result.updatedSecretRefs.length}`);
1443
+ })).pipe(Command.withDescription("Reload a session page and refresh a profile while preserving stable references"));
1444
+ var secretsRun = Command.make("run", {
1445
+ name: Argument.String("name"),
1446
+ command: Argument.String("command").pipe(Argument.variadic({ min: 1 })),
1447
+ cwd: Flag.String("cwd").pipe(Flag.optional, Flag.withDescription("Child process working directory")),
1448
+ timeoutMs: Flag.Int("timeout-ms").pipe(Flag.optional, Flag.withDescription("Child timeout in milliseconds, defaults to 120000"))
1449
+ }, Effect3.fn("Cli.secretsRun")(function* ({ name, command, cwd, timeoutMs }) {
1450
+ const relay = yield* Service;
1451
+ yield* ensureCliRelay();
1452
+ const [executable, ...args] = decodeCliOperands(command);
1453
+ if (!executable)
1454
+ return yield* Effect3.fail(new Error("secrets run requires a command after --"));
1455
+ const result = yield* relay.authRun({
1456
+ name,
1457
+ command: executable,
1458
+ args,
1459
+ cwd: path.resolve(Option2.getOrElse(cwd, () => process.cwd())),
1460
+ ...Option2.isSome(timeoutMs) ? { timeoutMs: timeoutMs.value } : {}
1461
+ });
1462
+ yield* Effect3.sync(() => {
1463
+ if (result.stdout)
1464
+ process.stdout.write(result.stdout);
1465
+ if (result.stderr)
1466
+ process.stderr.write(result.stderr);
1467
+ if (result.stdoutTruncated || result.stderrTruncated)
1468
+ process.stderr.write(`
1469
+ Browse truncated child output.
1470
+ `);
1471
+ if (result.exitCode !== 0)
1472
+ process.exitCode = result.exitCode;
1473
+ });
1474
+ })).pipe(Command.withDescription("Run a command with profile values injected as BC_SECRET_* environment variables"));
1475
+ function readSecretValue(prompt) {
1476
+ const input = process.stdin;
1477
+ if (!input.isTTY) {
1478
+ return new Promise((resolve, reject) => {
1479
+ const chunks = [];
1480
+ input.on("data", (chunk) => chunks.push(chunk));
1481
+ input.once("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1482
+ input.once("error", reject);
1483
+ });
1484
+ }
1485
+ return new Promise((resolve, reject) => {
1486
+ process.stderr.write(prompt);
1487
+ let value = "";
1488
+ const finish = (result) => {
1489
+ input.setRawMode(false);
1490
+ input.pause();
1491
+ input.off("data", onData);
1492
+ process.stderr.write(`
1493
+ `);
1494
+ if (result instanceof Error)
1495
+ reject(result);
1496
+ else
1497
+ resolve(result);
1498
+ };
1499
+ const onData = (chunk) => {
1500
+ for (const char of chunk.toString("utf8")) {
1501
+ if (char === "\r" || char === `
1502
+ `)
1503
+ return finish(value);
1504
+ if (char === "\x03")
1505
+ return finish(new Error("Cancelled"));
1506
+ if (char === "" || char === "\b")
1507
+ value = value.slice(0, -1);
1508
+ else
1509
+ value += char;
1510
+ }
1511
+ };
1512
+ input.setRawMode(true);
1513
+ input.resume();
1514
+ input.on("data", onData);
1515
+ });
1516
+ }
1517
+ var secretsAdd = Command.make("add", {
1518
+ name: Argument.String("name").pipe(Argument.withDescription('Secret name agents use: secrets.type(locator, "name")')),
1519
+ origin: Flag.String("origin").pipe(Flag.atLeast(1), Flag.withDescription("Origin the secret may be typed on (repeatable): https://example.com, *.example.com (https), http://localhost:3000")),
1520
+ totp: Flag.Boolean("totp").pipe(Flag.withDefault(false), Flag.withDescription("The value is a TOTP seed (base32 or otpauth:// URI); typing it types the current code. Storing the seed beside the password makes 2FA one factor")),
1521
+ json: jsonFlag
1522
+ }, Effect3.fn("Cli.secretsAdd")(function* ({ name, origin, totp, json }) {
1523
+ const value = yield* Effect3.tryPromise({
1524
+ try: () => readSecretValue(totp ? `TOTP seed for ${name} (hidden): ` : `Value for ${name} (hidden): `),
1525
+ catch: (cause) => cause instanceof Error ? cause : new Error("Could not read the secret value")
1526
+ });
1527
+ const result = yield* addSecret({ name, value, allowedOrigins: origin, kind: totp ? "totp" : "password" });
1528
+ if (json) {
1529
+ yield* emitJson(result);
1530
+ return;
1531
+ }
1532
+ yield* Console.log(`Saved ${result.kind} secret ${result.name}; it can be typed on ${result.allowedOrigins.join(", ")}`);
1533
+ if (totp)
1534
+ yield* Console.error("Note: with the TOTP seed stored here, anyone who can use these secrets has both factors. Prefer handing 2FA to a person when you can.");
1535
+ })).pipe(Command.withDescription("Store a secret agents can type only on its origins (value from stdin or a hidden prompt)"));
1536
+ var secretsList = Command.make("list", { json: jsonFlag }, Effect3.fn("Cli.secretsList")(function* ({ json }) {
1537
+ const secrets = yield* listSecrets();
1538
+ if (json) {
1539
+ yield* emitJson({ secrets });
1540
+ return;
1541
+ }
1542
+ if (secrets.length === 0) {
1543
+ yield* Console.log("No site secrets");
1544
+ return;
1545
+ }
1546
+ yield* Effect3.forEach(secrets, (secret) => Console.log(`${secret.name} ${secret.kind} ${secret.allowedOrigins.join(",")}`));
1547
+ })).pipe(Command.withDescription("List site secrets (names, kinds and origins, never values)"));
1548
+ var secretsRemove = Command.make("remove", { name: Argument.String("name") }, Effect3.fn("Cli.secretsRemove")(function* ({ name }) {
1549
+ const removed = yield* removeSecret(name);
1550
+ yield* Console.log(removed ? `Removed ${name}` : `No secret named ${name}`);
1551
+ })).pipe(Command.withDescription("Delete a site secret"));
1552
+ var secrets = Command.make("secrets").pipe(Command.withDescription("Site secrets agents can type without seeing them, and captured credential profiles"), Command.withSubcommands([secretsAdd, secretsList, secretsRemove, secretsStatus, secretsRefresh, secretsRun]));
1553
+ var deadboltConnect = Command.make("connect", { json: jsonFlag }, Effect3.fn("Cli.deadboltConnect")(function* ({ json }) {
1554
+ const paired = yield* Effect3.tryPromise({
1555
+ try: () => pair(),
1556
+ catch: (cause) => cause instanceof Error ? cause : new Error("Could not connect to Deadbolt")
1557
+ });
1558
+ if (json) {
1559
+ yield* emitJson({ connected: true, id: paired.id });
1560
+ return;
1561
+ }
1562
+ yield* Console.log(`Connected Browse to Deadbolt as client ${paired.id}.`);
1563
+ yield* Console.log("Agents can use only the logins you allow, in Deadbolt: deadbolt agents allow <site> (narrow with --login, --fields, --expires), or deadbolt agents allow --all for every login");
1564
+ })).pipe(Command.withDescription("Pair Browse with Deadbolt (runs `deadbolt agents add-client browse`, which asks you to confirm) and store its client token (0600)"));
1565
+ var deadboltDisconnect = Command.make("disconnect", {}, Effect3.fn("Cli.deadboltDisconnect")(function* () {
1566
+ const result = yield* Effect3.try({
1567
+ try: () => unpair(),
1568
+ catch: (cause) => cause instanceof Error ? cause : new Error("Could not disconnect from Deadbolt")
1569
+ });
1570
+ if (!result.removed) {
1571
+ yield* Console.log("Browse isn't connected to Deadbolt");
1572
+ return;
1573
+ }
1574
+ yield* Console.log(result.revoked ? "Disconnected: the client is removed in Deadbolt and its token deleted here." : "Deleted the client token here. Deadbolt didn't confirm removing the client: check `deadbolt agents clients`.");
1575
+ })).pipe(Command.withDescription("Remove Browse's Deadbolt client (in Deadbolt and here)"));
1576
+ var deadboltStatus = Command.make("status", { json: jsonFlag }, Effect3.fn("Cli.deadboltStatus")(function* ({ json }) {
1577
+ const client = readClient();
1578
+ const socket = socketPath();
1579
+ const status = client ? yield* Effect3.promise(() => connect({ client, socket }).status().then((value) => ({ reachable: true, ...value }), (error) => ({ reachable: false, error: error instanceof Error ? error.message : String(error) }))) : undefined;
1580
+ const report = { connected: client !== undefined, ...client ? { id: client.id } : {}, socket, ...status ? { deadbolt: status } : {} };
1581
+ if (json) {
1582
+ yield* emitJson(report);
1583
+ return;
1584
+ }
1585
+ if (!client) {
1586
+ yield* Console.log("Not connected to Deadbolt. Run `opencode-browser deadbolt connect`.");
1587
+ return;
1588
+ }
1589
+ yield* Console.log(`Connected as Deadbolt client ${client.id}; socket ${socket}`);
1590
+ if (status && status.reachable)
1591
+ yield* Console.log(`Deadbolt ${status.version ?? ""} (protocol ${status.protocol}) is ${status.unlocked ? "unlocked" : "locked: agent sign-ins are paused until you unlock it"}`);
1592
+ else if (status)
1593
+ yield* Console.log(`Deadbolt isn't reachable: ${status.error}`);
1594
+ })).pipe(Command.withDescription("Show whether Browse is paired with Deadbolt and whether Deadbolt is reachable and unlocked"));
1595
+ var deadbolt = Command.make("deadbolt").pipe(Command.withDescription("Let agents sign in with the Deadbolt logins you grant them, without seeing any value"), Command.withSubcommands([deadboltConnect, deadboltDisconnect, deadboltStatus]));
1596
+ var journal = Command.make("journal", {
1597
+ session: sessionFlag("Show the journal for this Browse session id"),
1598
+ limit: Flag.Int("limit").pipe(Flag.optional, Flag.withDescription("Number of most recent entries to show, defaults to 20")),
1599
+ json: jsonFlag
1600
+ }, Effect3.fn("Cli.journal")(function* ({ session, limit, json }) {
1601
+ const sessionId = yield* networkSession(session);
1602
+ const entries = yield* Effect3.tryPromise({
1603
+ try: () => readJournalEntries({ baseDir: defaultJournalBaseDir(), sessionId, limit: Option2.getOrUndefined(limit) ?? 20 }),
1604
+ catch: (cause) => new Error(`read session journal for ${sessionId}`, { cause })
1605
+ });
1606
+ if (json) {
1607
+ yield* emitJson({ session: sessionId, entries });
1608
+ return;
1609
+ }
1610
+ if (entries.length === 0) {
1611
+ yield* Console.log(`No journal entries for session ${sessionId}`);
1612
+ return;
1613
+ }
1614
+ yield* Console.log(`Journal for ${sessionId} (last ${entries.length}):`);
1615
+ yield* Effect3.forEach(entries, (entry) => {
1616
+ return Console.log(formatJournalEntry(entry));
1617
+ });
1618
+ })).pipe(Command.withDescription("Show what agents did in a Browse session"));
1619
+ var doctor = Command.make("doctor", {
1620
+ json: jsonFlag
1621
+ }, Effect3.fn("Cli.doctor")(function* ({ json }) {
1622
+ const update = yield* Effect3.promise(() => updateNotice2());
1623
+ const report = yield* createDoctorReport({ packageRoot, update: updateJson2(update) });
1624
+ if (json) {
1625
+ yield* emitJson(report);
1626
+ } else {
1627
+ yield* Console.log(formatDoctorReport(report));
1628
+ }
1629
+ if (report.status === "fail") {
1630
+ yield* failExit;
1631
+ }
1632
+ })).pipe(Command.withDescription("Diagnose the local Browse install and runtime"));
1633
+ var skill = Command.make("skill", {}, Effect3.fn("Cli.skill")(function* () {
1634
+ const fs = yield* FileSystem3.FileSystem;
1635
+ const text = yield* fs.readFileString(path.join(packageRoot, "skills", "browse", "SKILL.md")).pipe(Effect3.mapError((cause) => new Error("read opencode-browser skill", { cause })));
1636
+ yield* Console.log(text.trimEnd());
1637
+ })).pipe(Command.withDescription("Print the Browse agent skill text"));
1638
+ var tabsCleanup = Command.make("cleanup", {
1639
+ idleMinutes: Flag.Int("idle-minutes").pipe(Flag.optional, Flag.withDescription("Close agent tabs idle at least this long; 0 closes every eligible agent tab (default: the extension's setting, 10; still 10 when it is set to never)")),
1640
+ json: jsonFlag
1641
+ }, Effect3.fn("Cli.tabsCleanup")(function* ({ idleMinutes, json }) {
1642
+ const relay = yield* Service;
1643
+ yield* ensureCliRelay();
1644
+ const result = yield* relay.tabsCleanup(Option2.getOrUndefined(idleMinutes));
1645
+ if (json)
1646
+ return yield* emitJson(result);
1647
+ if (!result.closed.length)
1648
+ return yield* Console.log("No idle agent tabs to close.");
1649
+ for (const tab of result.closed)
1650
+ yield* Console.log(`Closed ${tab.title} (idle ${tab.idleMinutes}m)`);
1651
+ })).pipe(Command.withDescription("Close tabs agents opened that nobody has used for a while"));
1652
+ var tabs = Command.make("tabs").pipe(Command.withDescription("Manage the tabs agents open"), Command.withSubcommands([tabsCleanup]));
1653
+ var printSettings = (settings) => Effect3.forEach(formatSettings(settings), (line) => Console.log(line), { discard: true });
1654
+ var settingsGet = Command.make("get", { json: jsonFlag }, Effect3.fn("Cli.settingsGet")(function* ({ json }) {
1655
+ const relay = yield* Service;
1656
+ yield* ensureCliRelay();
1657
+ const result = yield* relay.settingsGet;
1658
+ if (json)
1659
+ return yield* emitJson(result);
1660
+ yield* printSettings(result.settings);
1661
+ yield* Console.log(result.note);
1662
+ })).pipe(Command.withDescription("Show each setting's value, the value in effect, and any relay environment variable overriding it"));
1663
+ var settingsSet = Command.make("set", {
1664
+ assignments: Argument.String("key=value").pipe(Argument.variadic({ min: 1 }), Argument.withDescription(`Settings to change: ${settingKeys.join(", ")}`)),
1665
+ json: jsonFlag
1666
+ }, Effect3.fn("Cli.settingsSet")(function* ({ assignments, json }) {
1667
+ const changes = {};
1668
+ for (const assignment of assignments) {
1669
+ const index = assignment.indexOf("=");
1670
+ if (index <= 0)
1671
+ return yield* Effect3.fail(new Error(`Expected key=value, got "${assignment}". Settings: ${settingKeys.join(", ")}.`));
1672
+ changes[assignment.slice(0, index)] = assignment.slice(index + 1);
1673
+ }
1674
+ const relay = yield* Service;
1675
+ yield* ensureCliRelay();
1676
+ const result = yield* relay.settingsSet(changes);
1677
+ if (json)
1678
+ return yield* emitJson(result);
1679
+ yield* Console.log(result.message);
1680
+ yield* printSettings(result.settings);
1681
+ })).pipe(Command.withDescription("Change settings (validated strictly; the extension's options page updates live)"));
1682
+ var settings = Command.make("settings").pipe(Command.withDescription("Read or change the user's settings (the extension's options page). Security switches are environment-only"), Command.withSubcommands([settingsGet, settingsSet]));
1683
+ var mcp = Command.make("mcp", {}, Effect3.fn("Cli.mcp")(function* () {
1684
+ const { runMcpServer } = yield* Effect3.promise(() => import("./mcp-2smdsn8z.mjs"));
1685
+ yield* runMcpServer;
1686
+ })).pipe(Command.withDescription("Run the Browse MCP server over stdio"));
1687
+ var opencodeBrowser = Command.make("opencode-browser").pipe(Command.withDescription("Control the user's existing browser through the Browse extension"), Command.withSubcommands([serve, relay, execute, session, tabs, settings, status, network, secrets, deadbolt, recording, flightRecorder, journal, doctor, skill, mcp]));
1688
+ var mainLayer = Layer2.mergeAll(layerFetch, layer).pipe(Layer2.provideMerge(NodeServices.layer));
1689
+ function runCli(rawArgs = process.argv.slice(2)) {
1690
+ const cliArgs = hoistJsonFlag(rawArgs);
1691
+ const subcommand = cliArgs.argv.find((arg) => !arg.startsWith("-"));
1692
+ const commandName = subcommand ? `opencode-browser ${subcommand}` : "opencode-browser";
1693
+ const reportUsageError = (error) => Effect3.gen(function* () {
1694
+ const errors = error._tag === "ShowHelp" ? error.errors : [error];
1695
+ if (errors.length === 0)
1696
+ return;
1697
+ const message = errors.map((e) => e.message).join("; ");
1698
+ if (cliArgs.json) {
1699
+ yield* emitJson(failure(commandName, "usage", message, { exitCode: ExitCode.usage }));
1700
+ yield* stderrLine(message);
1701
+ }
1702
+ yield* exitWith(ExitCode.usage);
1703
+ });
1704
+ const reportFailure = (error) => Effect3.gen(function* () {
1705
+ const message = error instanceof Error ? error.message : String(error);
1706
+ const exitCode = exitCodeForError(error);
1707
+ if (cliArgs.json) {
1708
+ yield* emitJson(failure(commandName, exitCode === ExitCode.host ? "relay_unavailable" : "error", message, { exitCode }));
1709
+ yield* stderrLine(message);
1710
+ } else {
1711
+ yield* Console.error(message);
1712
+ }
1713
+ yield* exitWith(exitCode);
1714
+ });
1715
+ const silent = () => {};
1716
+ const askedForHelp = cliArgs.argv.some((arg) => arg === "--help" || arg === "-h");
1717
+ const quietJson = cliArgs.json && !askedForHelp;
1718
+ Command.runWith(opencodeBrowser, { version: opencodeBrowserVersion2, renderErrors: !quietJson })(normalizeCliArguments(cliArgs.argv)).pipe(quietJson ? Effect3.provideService(Console.Console, { ...globalThis.console, log: silent }) : (effect) => effect, Effect3.catchIf(CliError.isCliError, reportUsageError), Effect3.catch(reportFailure), Effect3.provide(mainLayer), NodeRuntime.runMain);
1719
+ }
1720
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
1721
+ runCli();
1722
+ }
1723
+ export {
1724
+ runCli
1725
+ };