@opencode/browse 0.0.6-dev.27 → 0.0.6-dev.32

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