@opencode/browse 0.0.6-dev.17 → 0.0.6-dev.19

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.
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- extensionProtocolVersion,
4
- extensionProtocolAdvice,
5
3
  dataRoot,
6
4
  fromCheckout,
7
5
  cliCommand,
8
6
  devExtension,
9
7
  installCommand,
10
- autoUpdateStatus
11
- } from "./main-g5mke6rn.mjs";
8
+ autoUpdateStatus,
9
+ extensionProtocolVersion,
10
+ extensionProtocolAdvice
11
+ } from "./main-yd50d95g.mjs";
12
12
  import {
13
13
  defaultPort2,
14
14
  isValidSessionId2,
@@ -27,44 +27,45 @@ import {
27
27
  ensureExtensionConnected,
28
28
  stoppedRelayStatus,
29
29
  statusCollections
30
- } from "./main-3qpkwaa7.mjs";
30
+ } from "./main-8a245rm1.mjs";
31
+ import"./main-75rdz6bc.mjs";
31
32
  import {
32
33
  opencodeBrowserVersion2,
33
34
  opencodeBrowserBuildId2
34
- } from "./main-y7c1atbf.mjs";
35
+ } from "./main-ts9jjqs8.mjs";
35
36
  import {
36
- addSecret,
37
- listSecrets,
38
- removeSecret,
37
+ updateJson2,
38
+ formatUpdateNotice2,
39
+ updateNotice2
40
+ } from "./main-2wrztakb.mjs";
41
+ import {
42
+ settingKeys,
43
+ formatSettings,
44
+ ensureSessionExists,
39
45
  readClient,
40
46
  socketPath,
41
47
  connect,
42
48
  pair,
43
49
  unpair,
44
- settingKeys,
45
- formatSettings
46
- } from "./main-e4stqtf2.mjs";
50
+ addSecret,
51
+ listSecrets,
52
+ removeSecret
53
+ } from "./main-90enpn5q.mjs";
47
54
  import {
48
55
  defaultJournalBaseDir,
49
56
  readJournalEntries,
50
57
  formatJournalEntry
51
- } from "./main-kvmxefp1.mjs";
52
- import {
53
- updateJson2,
54
- formatUpdateNotice2,
55
- updateNotice2
56
- } from "./main-4p2p9eey.mjs";
57
- import"./main-4bht27ry.mjs";
58
+ } from "./main-knahxbey.mjs";
58
59
 
59
60
  // src/cli.ts
60
61
  import { NodeRuntime, NodeServices } from "@effect/platform-node";
61
- import { Config, Console, Effect as Effect3, FileSystem as FileSystem3, Layer as Layer2, Option as Option2, Predicate as Predicate2, Result, Schema as Schema3 } from "effect";
62
- import { Argument, CliError, Command, Flag } from "effect/cli";
63
- import path from "node:path";
64
- import process from "node:process";
62
+ import { Console as Console9, Effect as Effect11, Layer as Layer2 } from "effect";
63
+ import { CliError, Command as Command8 } from "effect/cli";
64
+ import path6 from "node:path";
65
+ import process3 from "node:process";
65
66
  import { fileURLToPath, pathToFileURL } from "node:url";
66
67
 
67
- // src/cli-output.ts
68
+ // src/cli/output.ts
68
69
  import { writeSync } from "node:fs";
69
70
  var ExitCode = { ok: 0, failure: 1, usage: 2, auth: 3, host: 4 };
70
71
  var failure = (command, code, message, options = {}) => ({
@@ -102,10 +103,15 @@ var writeStdout = (text, fd = 1) => {
102
103
  var printJson = (value) => writeStdout(`${JSON.stringify(value, null, 2)}
103
104
  `);
104
105
 
106
+ // src/cli/browser.ts
107
+ import { Console as Console2, Effect as Effect4, FileSystem as FileSystem3, Option as Option3, Result } from "effect";
108
+ import { Argument, Command, Flag as Flag2 } from "effect/cli";
109
+ import path from "node:path";
110
+
105
111
  // src/doctor.ts
106
112
  import { Effect as Effect2, FileSystem as FileSystem2, Path as Path2, Schema as Schema2 } from "effect";
107
113
 
108
- // src/session-store.ts
114
+ // src/session-manager/store.ts
109
115
  import { Context, Effect, FileSystem, Layer, Option, Path, Predicate, Schema } from "effect";
110
116
  class SessionStoreError extends Schema.TaggedError()("SessionStore.SessionStoreError", {
111
117
  message: Schema.String,
@@ -211,10 +217,9 @@ var createDoctorReport = Effect2.fn("Doctor.createReport")(function* (options) {
211
217
  }
212
218
  })));
213
219
  const fileExists = (relativePath) => fs.exists(path.join(options.packageRoot, relativePath)).pipe(Effect2.orElseSucceed(() => false));
214
- const [packageResult, distCliExists, distMcpExists, currentResult] = yield* Effect2.all([
220
+ const [packageResult, distCliExists, currentResult] = yield* Effect2.all([
215
221
  probe(readPackageInfo),
216
222
  fileExists("dist/cli.mjs"),
217
- fileExists("dist/cli.mjs"),
218
223
  probe(store.read)
219
224
  ]);
220
225
  const expectedExtensionVersion = packageResult.ok ? { ok: true, value: extensionManifestVersion(packageResult.value.version) } : packageResult;
@@ -248,7 +253,7 @@ var createDoctorReport = Effect2.fn("Doctor.createReport")(function* (options) {
248
253
  return session.connected && session.id !== current;
249
254
  });
250
255
  const artifacts = [
251
- { path: "dist/cli.mjs", exists: distCliExists && distMcpExists }
256
+ { path: "dist/cli.mjs", exists: distCliExists }
252
257
  ];
253
258
  const checks = buildDoctorChecks({
254
259
  packageResult,
@@ -601,8 +606,10 @@ function formatTargetSummary(target, options = {}) {
601
606
  return `${target.type} ${target.id}${tab}${session}${owner}${health} ${target.url || "about:blank"}`;
602
607
  }
603
608
 
604
- // src/cli.ts
605
- var packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
609
+ // src/cli/context.ts
610
+ import { Config, Console, Effect as Effect3, Option as Option2, Predicate as Predicate2 } from "effect";
611
+ import { Flag } from "effect/cli";
612
+ import process from "node:process";
606
613
  var sessionIdConfig = Config.option(Config.String("OPENCODE_BROWSER_SESSION"));
607
614
  var agentSessionIdConfig = Config.option(Config.String("OPENCODE_SESSION_ID"));
608
615
  var targetUrlConfig = Config.option(Config.String("OPENCODE_BROWSER_TARGET_URL"));
@@ -624,7 +631,11 @@ var failExit = Effect3.sync(() => {
624
631
  var exitWith = (code) => Effect3.sync(() => {
625
632
  process.exitCode = code;
626
633
  });
627
- var hostErrorTags = new Set(["RelayClient.RelayUnreachable", "RelayLifecycle.RelayStartFailed", "RelayLifecycle.ExtensionDisconnected"]);
634
+ var hostErrorTags = new Set([
635
+ "RelayClient.RelayUnreachable",
636
+ "RelayLifecycle.RelayStartFailed",
637
+ "RelayLifecycle.ExtensionDisconnected"
638
+ ]);
628
639
  function exitCodeForError(error) {
629
640
  if (error instanceof RelayRejected && error.code === "browser-unavailable")
630
641
  return ExitCode.host;
@@ -685,10 +696,9 @@ function decodeCliOperands(operands) {
685
696
  return decodeURIComponent(operand.slice(encodedCliOperandPrefix.length));
686
697
  });
687
698
  }
688
- var readExecuteFile = Effect3.fnUntraced(function* (filePath) {
689
- const fs = yield* FileSystem3.FileSystem;
690
- return yield* fs.readFileString(path.resolve(filePath)).pipe(Effect3.mapError((cause) => new Error(`read execute file ${filePath}: ${cause.reason.message}`, { cause })));
691
- });
699
+ function formatSessionContinuation(sessionId) {
700
+ return `Session: ${sessionId}. Continue with --session ${sessionId}.`;
701
+ }
692
702
  var ensureCliRelay = Effect3.fnUntraced(function* () {
693
703
  const relay = yield* Service;
694
704
  const readiness = yield* ensureRelay({ relay });
@@ -725,16 +735,10 @@ var resolvePositionalOrFlagSessionId = Effect3.fnUntraced(function* (id, session
725
735
  environment: Option2.getOrUndefined(yield* sessionIdConfig)
726
736
  }));
727
737
  });
728
- var ensureSessionExists = Effect3.fnUntraced(function* (id) {
738
+ var ensureSessionExists2 = Effect3.fnUntraced(function* (id) {
729
739
  const relay = yield* Service;
730
740
  yield* ensureCliRelay();
731
- const sessions = yield* relay.sessions;
732
- const exists = sessions.some((session) => {
733
- return session.id === id;
734
- });
735
- if (!exists) {
736
- return yield* Effect3.fail(new Error(`Session not found: ${id}`));
737
- }
741
+ yield* ensureSessionExists(relay, id);
738
742
  });
739
743
  var recordingTarget = Effect3.fnUntraced(function* (options) {
740
744
  const sessionId = Option2.getOrUndefined(options.session);
@@ -756,6 +760,229 @@ var parseRecordingModeOption = Effect3.fnUntraced(function* (value) {
756
760
  }
757
761
  return yield* Effect3.fail(new Error("Recording mode must be auto, tab-capture, or cdp"));
758
762
  });
763
+ var networkSession = Effect3.fnUntraced(function* (session) {
764
+ return yield* resolveSelectedSessionId(Option2.getOrUndefined(session) ?? Option2.getOrUndefined(yield* sessionIdConfig));
765
+ });
766
+
767
+ // src/cli/registry.ts
768
+ var entries = [];
769
+ function registerCliCommand(order, build) {
770
+ entries.push({
771
+ order,
772
+ build
773
+ });
774
+ }
775
+ function buildRegisteredCliCommands(ctx) {
776
+ return [...entries].sort((left, right) => left.order - right.order).map((entry) => entry.build(ctx));
777
+ }
778
+
779
+ // src/cli/browser.ts
780
+ var tabsCleanup = Command.make("cleanup", {
781
+ 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)")),
782
+ json: jsonFlag
783
+ }, Effect4.fn("Cli.tabsCleanup")(function* ({ idleMinutes, json }) {
784
+ const relay = yield* Service;
785
+ yield* ensureCliRelay();
786
+ const result = yield* relay.tabsCleanup(Option3.getOrUndefined(idleMinutes));
787
+ if (json)
788
+ return yield* emitJson(result);
789
+ if (!result.closed.length)
790
+ return yield* Console2.log("No idle agent tabs to close.");
791
+ for (const tab of result.closed)
792
+ yield* Console2.log(`Closed ${tab.title} (idle ${tab.idleMinutes}m)`);
793
+ })).pipe(Command.withDescription("Close tabs agents opened that nobody has used for a while"));
794
+ var tabs = Command.make("tabs").pipe(Command.withDescription("Manage the tabs agents open"), Command.withSubcommands([tabsCleanup]));
795
+ var printSettings = (settings) => Effect4.forEach(formatSettings(settings), (line) => Console2.log(line), { discard: true });
796
+ var settingsGet = Command.make("get", { json: jsonFlag }, Effect4.fn("Cli.settingsGet")(function* ({ json }) {
797
+ const relay = yield* Service;
798
+ yield* ensureCliRelay();
799
+ const result = yield* relay.settingsGet;
800
+ if (json)
801
+ return yield* emitJson(result);
802
+ yield* printSettings(result.settings);
803
+ yield* Console2.log(result.note);
804
+ })).pipe(Command.withDescription("Show each setting's value, the value in effect, and any relay environment variable overriding it"));
805
+ var settingsSet = Command.make("set", {
806
+ assignments: Argument.String("key=value").pipe(Argument.variadic({ min: 1 }), Argument.withDescription(`Settings to change: ${settingKeys.join(", ")}`)),
807
+ json: jsonFlag
808
+ }, Effect4.fn("Cli.settingsSet")(function* ({ assignments, json }) {
809
+ const changes = {};
810
+ for (const assignment of assignments) {
811
+ const index = assignment.indexOf("=");
812
+ if (index <= 0)
813
+ return yield* Effect4.fail(new Error(`Expected key=value, got "${assignment}". Settings: ${settingKeys.join(", ")}.`));
814
+ changes[assignment.slice(0, index)] = assignment.slice(index + 1);
815
+ }
816
+ const relay = yield* Service;
817
+ yield* ensureCliRelay();
818
+ const result = yield* relay.settingsSet(changes);
819
+ if (json)
820
+ return yield* emitJson(result);
821
+ yield* Console2.log(result.message);
822
+ yield* printSettings(result.settings);
823
+ })).pipe(Command.withDescription("Change settings (validated strictly; the extension's options page updates live)"));
824
+ 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]));
825
+ var status = Command.make("status", {
826
+ json: jsonFlag
827
+ }, Effect4.fn("Cli.status")(function* ({ json }) {
828
+ const relay = yield* Service;
829
+ const store = yield* Service2;
830
+ const [relayResult, update] = yield* Effect4.all([Effect4.result(relay.version), Effect4.promise(() => updateNotice2())], { concurrency: 2 });
831
+ const updateLine = update ? formatUpdateNotice2(update) : undefined;
832
+ if (Result.isFailure(relayResult)) {
833
+ if (!(relayResult.failure instanceof RelayUnreachable)) {
834
+ if (json) {
835
+ yield* emitJson({
836
+ endpoint: relay.endpoint,
837
+ relay: { running: false, error: relayResult.failure.message },
838
+ extension: null,
839
+ sessions: [],
840
+ targets: [],
841
+ update: updateJson2(update)
842
+ });
843
+ }
844
+ yield* stderrLine(`Relay status failed: ${relayResult.failure.message}`);
845
+ yield* exitWith(ExitCode.host);
846
+ return;
847
+ }
848
+ const stopped = stoppedRelayStatus(relay.endpoint);
849
+ if (json) {
850
+ yield* emitJson({ ...stopped, update: updateJson2(update), autoUpdate: autoUpdateStatus() });
851
+ yield* stderrLine(`relay is not running at ${relay.endpoint}`);
852
+ } else {
853
+ yield* Console2.log(`Relay: stopped (${relay.endpoint})`);
854
+ yield* Console2.log(`Run \`${cliCommand("execute")}\` (or any relay-backed command) to start it automatically.`);
855
+ if (updateLine)
856
+ yield* Console2.log(updateLine);
857
+ }
858
+ yield* exitWith(ExitCode.host);
859
+ return;
860
+ }
861
+ const version = relayResult.success;
862
+ const buildProblem = relayBuildProblem(version);
863
+ const [extensionStatus, current] = yield* Effect4.all([relay.extensionStatus, store.read]);
864
+ const collections = statusCollections(extensionStatus);
865
+ const [sessions, targets] = collections ? [collections.sessions, collections.targets] : yield* Effect4.all([relay.sessions, relay.targets]);
866
+ if (json) {
867
+ yield* emitJson({
868
+ endpoint: relay.endpoint,
869
+ relay: {
870
+ running: true,
871
+ version: version.version,
872
+ buildId: version.buildId ?? null,
873
+ stale: buildProblem !== undefined
874
+ },
875
+ extension: extensionStatus,
876
+ currentSession: current ?? null,
877
+ sessions,
878
+ targets,
879
+ update: updateJson2(update),
880
+ autoUpdate: autoUpdateStatus()
881
+ });
882
+ if (buildProblem) {
883
+ yield* stderrLine(buildProblem);
884
+ yield* failExit;
885
+ }
886
+ return;
887
+ }
888
+ if (updateLine)
889
+ yield* Console2.log(updateLine);
890
+ yield* Console2.log(`Relay: ${relay.endpoint} (${version.version})`);
891
+ if (buildProblem) {
892
+ yield* Console2.log(`Warning: ${buildProblem}`);
893
+ }
894
+ yield* Console2.log(`Extension: ${extensionStatus.connected ? "connected" : "disconnected"}${extensionStatus.version ? ` (${extensionStatus.version})` : ""}`);
895
+ if ((extensionStatus.rejectedConnections ?? 0) > 0) {
896
+ 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.`);
897
+ }
898
+ if (extensionStatus.nextStep)
899
+ yield* Console2.log(extensionStatus.nextStep);
900
+ else if ((extensionStatus.pairingFailures ?? 0) > 0 && !extensionStatus.connected) {
901
+ 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.`);
902
+ }
903
+ if ((extensionStatus.processFaults?.suppressed ?? 0) > 0) {
904
+ const faults = extensionStatus.processFaults;
905
+ 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.`);
906
+ }
907
+ if (extensionStatus.protocolVersion !== undefined && extensionStatus.protocolVersion !== null) {
908
+ const compatibility = extensionStatus.protocolCompatible === false ? "incompatible" : "compatible";
909
+ const legacy = extensionStatus.protocolLegacy === true ? ", inferred from legacy hello" : "";
910
+ yield* Console2.log(`Extension protocol: ${extensionStatus.protocolVersion} (${compatibility}${legacy})`);
911
+ if (extensionStatus.protocolCompatible === false && !extensionStatus.nextStep)
912
+ yield* Console2.log(extensionProtocolAdvice(extensionStatus.protocolVersion, { install: installCommand() }));
913
+ }
914
+ yield* Console2.log(`Active targets: ${extensionStatus.activeTargets}`);
915
+ if (extensionStatus.childTargets !== undefined) {
916
+ yield* Console2.log(`Child targets: ${extensionStatus.childTargets}`);
917
+ }
918
+ if (extensionStatus.cdpClients !== undefined) {
919
+ yield* Console2.log(`CDP clients: ${extensionStatus.cdpClients}`);
920
+ }
921
+ yield* Console2.log(`Current session: ${current ?? "none"}`);
922
+ if (sessions.length === 0) {
923
+ yield* Console2.log("Sessions: none");
924
+ } else {
925
+ yield* Console2.log("Sessions:");
926
+ const activeSessions = sessions.filter((item) => item.id === current || item.connected || item.pageUrl !== null || item.stateKeys.length > 0 || item.readOnly);
927
+ const idleSessions = sessions.filter((item) => !activeSessions.includes(item));
928
+ const shownSessions = activeSessions.length > 0 ? activeSessions : idleSessions.slice(0, 5);
929
+ const omittedIdleCount = activeSessions.length > 0 ? idleSessions.length : Math.max(0, idleSessions.length - shownSessions.length);
930
+ yield* Effect4.forEach(shownSessions, (item) => {
931
+ const marker = item.id === current ? "*" : " ";
932
+ return Console2.log(`${marker} ${item.id} ${item.pageUrl ?? "no page yet"}`);
933
+ });
934
+ if (omittedIdleCount > 0) {
935
+ yield* Console2.log(` (${omittedIdleCount} idle session(s) with no page omitted; run ${cliCommand("session list")} to view all)`);
936
+ }
937
+ }
938
+ if (targets.length === 0) {
939
+ yield* Console2.log("Targets: none");
940
+ } else {
941
+ yield* Console2.log("Targets:");
942
+ yield* Effect4.forEach(targets, (target, index) => {
943
+ return Console2.log(`- [${index}] ${formatTargetSummary(target, { includeSession: true })}`);
944
+ });
945
+ }
946
+ if (buildProblem) {
947
+ yield* failExit;
948
+ }
949
+ })).pipe(Command.withDescription("Show relay, extension, and target status"));
950
+ var mcp = Command.make("mcp", {}, Effect4.fn("Cli.mcp")(function* () {
951
+ const { runMcpServer } = yield* Effect4.promise(() => import("./mcp-p0mcjvq9.mjs"));
952
+ yield* runMcpServer;
953
+ })).pipe(Command.withDescription("Run the Browse MCP server over stdio"));
954
+ registerCliCommand(400, () => tabs);
955
+ registerCliCommand(410, () => settings);
956
+ registerCliCommand(500, () => status);
957
+ registerCliCommand(810, ({ packageRoot }) => Command.make("doctor", {
958
+ json: jsonFlag
959
+ }, Effect4.fn("Cli.doctor")(function* ({ json }) {
960
+ const update = yield* Effect4.promise(() => updateNotice2());
961
+ const report = yield* createDoctorReport({ packageRoot, update: updateJson2(update) });
962
+ if (json) {
963
+ yield* emitJson(report);
964
+ } else {
965
+ yield* Console2.log(formatDoctorReport(report));
966
+ }
967
+ if (report.status === "fail") {
968
+ yield* failExit;
969
+ }
970
+ })).pipe(Command.withDescription("Diagnose the local Browse install and runtime")));
971
+ registerCliCommand(820, ({ packageRoot }) => Command.make("skill", {}, Effect4.fn("Cli.skill")(function* () {
972
+ const fs = yield* FileSystem3.FileSystem;
973
+ const text = yield* fs.readFileString(path.join(packageRoot, "skills", "browse", "SKILL.md")).pipe(Effect4.mapError((cause) => new Error("read opencode-browser skill", { cause })));
974
+ yield* Console2.log(text.trimEnd());
975
+ })).pipe(Command.withDescription("Print the Browse agent skill text")));
976
+ registerCliCommand(900, () => mcp);
977
+
978
+ // src/cli/execute.ts
979
+ import { Console as Console3, Effect as Effect5, FileSystem as FileSystem4, Option as Option4, Predicate as Predicate3, Result as Result2, Schema as Schema3 } from "effect";
980
+ import { Argument as Argument2, Command as Command2, Flag as Flag3 } from "effect/cli";
981
+ import path2 from "node:path";
982
+ var readExecuteFile = Effect5.fnUntraced(function* (filePath) {
983
+ const fs = yield* FileSystem4.FileSystem;
984
+ return yield* fs.readFileString(path2.resolve(filePath)).pipe(Effect5.mapError((cause) => new Error(`read execute file ${filePath}: ${cause.reason.message}`, { cause })));
985
+ });
759
986
  function formatExecuteLogs(logs) {
760
987
  return [
761
988
  "Console logs:",
@@ -801,73 +1028,59 @@ function executeJsonEnvelope(result) {
801
1028
  session: result.session
802
1029
  };
803
1030
  }
804
- function formatSessionContinuation(sessionId) {
805
- return `Session: ${sessionId}. Continue with --session ${sessionId}.`;
806
- }
807
1031
  function errorJsonEnvelope(error) {
808
- const tag = Predicate2.isObject(error) && Predicate2.hasProperty(error, "_tag") && Predicate2.isString(error._tag) ? error._tag : "Error";
1032
+ const tag = Predicate3.isObject(error) && Predicate3.hasProperty(error, "_tag") && Predicate3.isString(error._tag) ? error._tag : "Error";
809
1033
  const message = error instanceof Error ? error.message : String(error);
810
1034
  const errorValue = Object.assign({ message }, { _tag: tag });
811
- return { ok: false, isError: true, text: message, value: null, valueUnavailable: true, error: errorValue, logs: [], warnings: [] };
1035
+ return {
1036
+ ok: false,
1037
+ isError: true,
1038
+ text: message,
1039
+ value: null,
1040
+ valueUnavailable: true,
1041
+ error: errorValue,
1042
+ logs: [],
1043
+ warnings: []
1044
+ };
812
1045
  }
813
- var serve = Command.make("serve", {}, Effect3.fn("Cli.serve")(function* () {
814
- const [{ startRelay }, { parseAdditionalExtensionOrigins }] = yield* Effect3.promise(() => Promise.all([import("./relay-ns525pms.mjs"), import("./relay-helpers-pjcbxs7p.mjs")]));
815
- const port = yield* portConfig;
816
- const additionalExtensionOrigins = parseAdditionalExtensionOrigins(yield* extensionOriginsConfig);
817
- yield* Effect3.scoped(Effect3.gen(function* () {
818
- const relay = yield* startRelay({ port, additionalExtensionOrigins });
819
- yield* Console.log(`Browse relay listening at ${relay.url}`);
820
- yield* Console.log(`Browse connects automatically while the browser runs. Run \`${cliCommand("status")}\` to check.`);
821
- yield* Effect3.never;
822
- }));
823
- })).pipe(Command.withDescription("Start the local Browse relay"));
824
- var relayRestart = Command.make("restart", {
825
- force: Flag.Boolean("force").pipe(Flag.withDefault(false), Flag.withDescription("End network captures and recordings (discarding them) and disconnect raw CDP clients instead of refusing to restart"))
826
- }, Effect3.fn("Cli.relayRestart")(function* ({ force }) {
827
- const relay = yield* Service;
828
- yield* Console.error("Restarting the relay leaves browser tabs open but resets in-memory JavaScript state.");
829
- const readiness = yield* restartRelay({ relay, clientKind: "cli", force });
830
- yield* Console.log(`${readiness.started ? "Started" : "Reused concurrent replacement"} Browse relay at ${relay.endpoint} (${readiness.version.buildId})`);
831
- })).pipe(Command.withDescription("Explicitly drain and restart the managed relay using this installation"));
832
- var relay = Command.make("relay").pipe(Command.withDescription("Manage the local relay lifecycle"), Command.withSubcommands([relayRestart]));
833
- var execute = Command.make("execute", {
834
- code: Argument.String("code").pipe(Argument.variadic({ min: 0 })),
835
- file: Flag.String("file").pipe(Flag.optional, Flag.withDescription("Read execute code from a file")),
1046
+ var execute = Command2.make("execute", {
1047
+ code: Argument2.String("code").pipe(Argument2.variadic({ min: 0 })),
1048
+ file: Flag3.String("file").pipe(Flag3.optional, Flag3.withDescription("Read execute code from a file")),
836
1049
  session: sessionFlag("Continue an existing Browse session; defaults to the OpenCode agent's session"),
837
- rm: Flag.Boolean("rm").pipe(Flag.withDefault(false), Flag.withDescription("Use a fresh one-shot session and close its tabs afterward")),
838
- timeoutMs: Flag.Int("timeout-ms").pipe(Flag.optional, Flag.withDescription("Overall script timeout in milliseconds (paused during handoff); defaults to 60000, 0 disables")),
839
- targetUrl: Flag.String("target-url").pipe(Flag.optional, Flag.withDescription("Use the attached page whose URL contains this text")),
840
- targetIndex: Flag.Int("target-index").pipe(Flag.optional, Flag.withDescription("Use the attached page at this zero-based index")),
841
- json: Flag.Boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Print a machine-readable result envelope: { ok, isError, text, value, valueUnavailable, error?, logs, warnings, diagnostic?, aftermath, session }"))
842
- }, Effect3.fn("Cli.execute")(function* ({ code, file, session, rm, timeoutMs, targetUrl, targetIndex, json }) {
843
- const run = Effect3.gen(function* () {
1050
+ rm: Flag3.Boolean("rm").pipe(Flag3.withDefault(false), Flag3.withDescription("Use a fresh one-shot session and close its tabs afterward")),
1051
+ timeoutMs: Flag3.Int("timeout-ms").pipe(Flag3.optional, Flag3.withDescription("Overall script timeout in milliseconds (paused during handoff); defaults to 60000, 0 disables")),
1052
+ targetUrl: Flag3.String("target-url").pipe(Flag3.optional, Flag3.withDescription("Use the attached page whose URL contains this text")),
1053
+ targetIndex: Flag3.Int("target-index").pipe(Flag3.optional, Flag3.withDescription("Use the attached page at this zero-based index")),
1054
+ 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 }"))
1055
+ }, Effect5.fn("Cli.execute")(function* ({ code, file, session, rm, timeoutMs, targetUrl, targetIndex, json }) {
1056
+ const run = Effect5.gen(function* () {
844
1057
  const relay = yield* Service;
845
- const filePath = Option2.getOrUndefined(file);
1058
+ const filePath = Option4.getOrUndefined(file);
846
1059
  if (code.length > 0 && filePath) {
847
- return yield* Effect3.fail(new Error("Use either positional code or --file, not both"));
1060
+ return yield* Effect5.fail(new Error("Use either positional code or --file, not both"));
848
1061
  }
849
1062
  if (code.length === 0 && !filePath) {
850
- return yield* Effect3.fail(new Error("Execute requires positional code or --file <path>"));
1063
+ return yield* Effect5.fail(new Error("Execute requires positional code or --file <path>"));
851
1064
  }
852
- const timeoutMsValue = Option2.getOrUndefined(timeoutMs);
1065
+ const timeoutMsValue = Option4.getOrUndefined(timeoutMs);
853
1066
  if (timeoutMsValue !== undefined && timeoutMsValue < 0) {
854
- return yield* Effect3.fail(new Error("Timeout must be a non-negative integer"));
1067
+ return yield* Effect5.fail(new Error("Timeout must be a non-negative integer"));
855
1068
  }
856
1069
  const executeCode = filePath ? yield* readExecuteFile(filePath) : code.join(" ");
857
- const explicitSessionId = Option2.getOrUndefined(session) ?? Option2.getOrUndefined(yield* sessionIdConfig);
1070
+ const explicitSessionId = Option4.getOrUndefined(session) ?? Option4.getOrUndefined(yield* sessionIdConfig);
858
1071
  if (rm && explicitSessionId) {
859
- return yield* Effect3.fail(new Error("Use --rm without --session or OPENCODE_BROWSER_SESSION"));
1072
+ return yield* Effect5.fail(new Error("Use --rm without --session or OPENCODE_BROWSER_SESSION"));
860
1073
  }
861
- const agentWorkSessionId = agentDefaultSessionId(Option2.getOrUndefined(yield* agentSessionIdConfig));
1074
+ const agentWorkSessionId = agentDefaultSessionId(Option4.getOrUndefined(yield* agentSessionIdConfig));
862
1075
  const workSessionId = explicitSessionId ?? (rm ? undefined : agentWorkSessionId);
863
1076
  yield* ensureCliRelayAndExtension(workSessionId);
864
- const targetUrlValue = Option2.getOrUndefined(targetUrl) ?? Option2.getOrUndefined(yield* targetUrlConfig);
865
- const targetIndexValue = Option2.getOrUndefined(targetIndex) ?? Option2.getOrUndefined(yield* targetIndexConfig);
1077
+ const targetUrlValue = Option4.getOrUndefined(targetUrl) ?? Option4.getOrUndefined(yield* targetUrlConfig);
1078
+ const targetIndexValue = Option4.getOrUndefined(targetIndex) ?? Option4.getOrUndefined(yield* targetIndexConfig);
866
1079
  if (targetIndexValue !== undefined && targetIndexValue < 0) {
867
- return yield* Effect3.fail(new Error("Target index must be a non-negative integer"));
1080
+ return yield* Effect5.fail(new Error("Target index must be a non-negative integer"));
868
1081
  }
869
1082
  if (targetUrlValue && targetIndexValue !== undefined) {
870
- return yield* Effect3.fail(new Error("Use only one target selector: --target-url/OPENCODE_BROWSER_TARGET_URL or --target-index/OPENCODE_BROWSER_TARGET_INDEX"));
1083
+ return yield* Effect5.fail(new Error("Use only one target selector: --target-url/OPENCODE_BROWSER_TARGET_URL or --target-index/OPENCODE_BROWSER_TARGET_INDEX"));
871
1084
  }
872
1085
  const result = yield* relay.execute({
873
1086
  ...workSessionId ? { sessionId: workSessionId } : {},
@@ -883,33 +1096,33 @@ var execute = Command.make("execute", {
883
1096
  } : {}
884
1097
  });
885
1098
  if (!explicitSessionId && !rm) {
886
- yield* Console.error(formatSessionContinuation(result.session.id));
1099
+ yield* Console3.error(formatSessionContinuation(result.session.id));
887
1100
  }
888
1101
  return result;
889
1102
  });
890
1103
  if (json) {
891
- const outcome = yield* Effect3.result(run);
892
- const envelope = Result.isFailure(outcome) ? errorJsonEnvelope(outcome.failure) : executeJsonEnvelope(outcome.success);
1104
+ const outcome = yield* Effect5.result(run);
1105
+ const envelope = Result2.isFailure(outcome) ? errorJsonEnvelope(outcome.failure) : executeJsonEnvelope(outcome.success);
893
1106
  yield* emitJson(envelope);
894
1107
  if (!envelope.ok) {
895
1108
  yield* stderrLine(envelope.error?.message ?? envelope.text);
896
- yield* exitWith(Result.isFailure(outcome) ? exitCodeForError(outcome.failure) : ExitCode.failure);
1109
+ yield* exitWith(Result2.isFailure(outcome) ? exitCodeForError(outcome.failure) : ExitCode.failure);
897
1110
  }
898
1111
  return;
899
1112
  }
900
- const outcome = yield* Effect3.result(run);
901
- if (Result.isFailure(outcome)) {
902
- yield* Console.error(outcome.failure.message);
1113
+ const outcome = yield* Effect5.result(run);
1114
+ if (Result2.isFailure(outcome)) {
1115
+ yield* Console3.error(outcome.failure.message);
903
1116
  yield* exitWith(exitCodeForError(outcome.failure));
904
1117
  return;
905
1118
  }
906
1119
  const result = outcome.success;
907
- const print = result.isError ? Console.error : Console.log;
1120
+ const print = result.isError ? Console3.error : Console3.log;
908
1121
  yield* print(result.text);
909
1122
  if (result.logs.length > 0) {
910
1123
  yield* print(formatExecuteLogs(result.logs));
911
1124
  }
912
- yield* Effect3.forEach(result.warnings ?? [], (warning) => print(`Warning: ${warning}`));
1125
+ yield* Effect5.forEach(result.warnings ?? [], (warning) => print(`Warning: ${warning}`));
913
1126
  if (result.diagnostic) {
914
1127
  yield* print(`Diagnostic: ${result.diagnostic}`);
915
1128
  }
@@ -920,321 +1133,177 @@ var execute = Command.make("execute", {
920
1133
  if (result.isError) {
921
1134
  yield* failExit;
922
1135
  }
923
- })).pipe(Command.withDescription("Execute Playwright code against the attached browser"));
924
- var sessionNew = Command.make("new", {
925
- name: Argument.String("name").pipe(Argument.optional, Argument.withDescription("Optional lowercase session id")),
926
- readOnly: Flag.Boolean("read-only").pipe(Flag.withDefault(false), Flag.withDescription("Create a read-only session: the relay rejects input-dispatching CDP so scripts can inspect but not click or type")),
927
- allowOrigin: Flag.String("allow-origin").pipe(Flag.atLeast(0), Flag.withDescription("Only let this session's tabs load these hosts or origins (repeatable or comma-separated): example.com, *.example.com, https://app.example.com, http://localhost:3000"))
928
- }, Effect3.fn("Cli.sessionNew")(function* ({ name, readOnly, allowOrigin }) {
1136
+ })).pipe(Command2.withDescription("Execute Playwright code against the attached browser"));
1137
+ registerCliCommand(200, () => execute);
1138
+
1139
+ // src/cli/network.ts
1140
+ import { Console as Console4, Effect as Effect6, Option as Option5 } from "effect";
1141
+ import { Command as Command3, Flag as Flag4 } from "effect/cli";
1142
+ import path3 from "node:path";
1143
+ var parseNetworkContent = Effect6.fnUntraced(function* (content) {
1144
+ const value = Option5.getOrUndefined(content);
1145
+ if (value === undefined || value === "embed" || value === "omit")
1146
+ return value;
1147
+ return yield* Effect6.fail(new Error("Network content must be embed or omit"));
1148
+ });
1149
+ function formatNetworkStatus(status) {
1150
+ if (!status.active)
1151
+ return status.ended ? `Network capture: inactive (the last one ended because ${status.ended})` : "Network capture: inactive";
1152
+ 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}` : ""}`;
1153
+ }
1154
+ function formatNetworkResult(result) {
1155
+ const output = result.outputPath ? ` output=${result.outputPath}` : "";
1156
+ const profile = result.authProfile ? ` secrets=${result.authProfile.name}(${result.authProfile.slotCount})` : "";
1157
+ return `Network capture stopped: entries=${result.entryCount} responses=${result.responseCount} failures=${result.failureCount} bodyBytes=${result.capturedBodyBytes} truncated=${result.truncatedBodyCount} dropped=${result.droppedEntryCount}${output}${profile}`;
1158
+ }
1159
+ var networkStart = Command3.make("start", {
1160
+ session: sessionFlag("Capture the default page for this Browse session"),
1161
+ urlFilter: Flag4.String("url").pipe(Flag4.optional, Flag4.withDescription("Capture only requests whose URL contains this text")),
1162
+ resourceTypes: Flag4.String("resource-type").pipe(Flag4.atMost(50), Flag4.withDescription("Capture this Playwright resource type; repeat for multiple types")),
1163
+ content: Flag4.String("content").pipe(Flag4.optional, Flag4.withDescription("Response and request body mode: embed (default) or omit")),
1164
+ maxBodyBytes: Flag4.Int("max-body-bytes").pipe(Flag4.optional, Flag4.withDescription("Maximum captured bytes per body, defaults to 1000000")),
1165
+ maxTotalBodyBytes: Flag4.Int("max-total-body-bytes").pipe(Flag4.optional, Flag4.withDescription("Maximum captured body bytes for the whole capture, defaults to 25000000")),
1166
+ maxEntries: Flag4.Int("max-entries").pipe(Flag4.optional, Flag4.withDescription("Maximum request entries, defaults to 1000")),
1167
+ 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)")),
1168
+ json: jsonFlag
1169
+ }, Effect6.fn("Cli.networkStart")(function* ({
1170
+ session,
1171
+ urlFilter,
1172
+ resourceTypes,
1173
+ content,
1174
+ maxBodyBytes,
1175
+ maxTotalBodyBytes,
1176
+ maxEntries,
1177
+ maxDuration,
1178
+ json
1179
+ }) {
929
1180
  const relay = yield* Service;
930
- yield* ensureCliRelay();
931
- const store = yield* Service2;
932
- const result = yield* relay.sessionNew(Option2.getOrUndefined(name), {
933
- ...readOnly ? { readOnly: true } : {},
934
- ...allowOrigin.length > 0 ? { allowedOrigins: allowOrigin } : {}
1181
+ yield* ensureCliRelayAndExtension();
1182
+ const sessionId = yield* networkSession(session);
1183
+ const contentValue = yield* parseNetworkContent(content);
1184
+ const result = yield* relay.networkStart({
1185
+ sessionId,
1186
+ ...Option5.isSome(urlFilter) ? { urlFilter: urlFilter.value } : {},
1187
+ ...resourceTypes.length === 0 ? {} : { resourceTypes },
1188
+ ...contentValue === undefined ? {} : { content: contentValue },
1189
+ ...Option5.isSome(maxBodyBytes) ? { maxBodyBytes: maxBodyBytes.value } : {},
1190
+ ...Option5.isSome(maxTotalBodyBytes) ? { maxTotalBodyBytes: maxTotalBodyBytes.value } : {},
1191
+ ...Option5.isSome(maxEntries) ? { maxEntries: maxEntries.value } : {},
1192
+ ...Option5.isSome(maxDuration) ? { maxDurationMs: maxDuration.value } : {}
935
1193
  });
936
- yield* store.write(result.id);
937
- yield* Console.log(result.id);
938
- })).pipe(Command.withDescription("Create a Browse session and make it current"));
939
- var sessionList = Command.make("list", {
1194
+ yield* printOr(json, result, formatNetworkStatus(result));
1195
+ })).pipe(Command3.withDescription("Start session-scoped network capture"));
1196
+ var networkStatus = Command3.make("status", {
1197
+ session: sessionFlag(),
940
1198
  json: jsonFlag
941
- }, Effect3.fn("Cli.sessionList")(function* ({ json }) {
1199
+ }, Effect6.fn("Cli.networkStatus")(function* ({ session, json }) {
942
1200
  const relay = yield* Service;
1201
+ const sessionId = yield* networkSession(session);
943
1202
  yield* ensureCliRelay();
944
- const store = yield* Service2;
945
- const sessions = yield* relay.sessions;
946
- const current = yield* store.read;
947
- if (json) {
948
- yield* emitJson({ current: current ?? undefined, sessions });
949
- return;
950
- }
951
- if (sessions.length === 0) {
952
- yield* Console.log("No sessions");
953
- return;
1203
+ const result = yield* relay.networkStatus({ sessionId });
1204
+ yield* printOr(json, result, formatNetworkStatus(result));
1205
+ })).pipe(Command3.withDescription("Show session-scoped network capture status"));
1206
+ var networkStop = Command3.make("stop", {
1207
+ session: sessionFlag(),
1208
+ output: Flag4.String("output").pipe(Flag4.optional, Flag4.withAlias("o"), Flag4.withDescription("Write a credential-redacted HAR artifact to this path")),
1209
+ secrets: Flag4.String("secrets").pipe(Flag4.optional, Flag4.withDescription("Store captured credentials under this reusable profile name")),
1210
+ json: jsonFlag
1211
+ }, Effect6.fn("Cli.networkStop")(function* ({ session, output, secrets, json }) {
1212
+ const relay = yield* Service;
1213
+ const outputValue = Option5.getOrUndefined(output);
1214
+ const secretsValue = Option5.getOrUndefined(secrets);
1215
+ if (!outputValue && !secretsValue) {
1216
+ return yield* Effect6.fail(new Error("network stop requires --output, --secrets, or both"));
954
1217
  }
955
- yield* Effect3.forEach(sessions, (item) => {
956
- const marker = item.id === current ? "*" : " ";
957
- const page = item.pageUrl ?? "no page yet";
958
- const keys = item.stateKeys.length ? ` state=${item.stateKeys.join(",")}` : "";
959
- const readOnly = item.readOnly ? " read-only" : "";
960
- const allowed = item.allowedOrigins ? ` allow=${item.allowedOrigins.join(",")}` : "";
961
- return Console.log(`${marker} ${item.id} ${page}${keys}${readOnly}${allowed}`);
1218
+ const sessionId = yield* networkSession(session);
1219
+ yield* ensureCliRelay();
1220
+ const result = yield* relay.networkStop({
1221
+ sessionId,
1222
+ ...outputValue ? { outputPath: path3.resolve(outputValue) } : {},
1223
+ ...secretsValue ? { secrets: secretsValue } : {}
962
1224
  });
963
- })).pipe(Command.withDescription("List Browse sessions"));
964
- var sessionCurrent = Command.make("current", {}, Effect3.fn("Cli.sessionCurrent")(function* () {
965
- const store = yield* Service2;
966
- const current = yield* store.read;
967
- yield* Console.log(current ?? "none");
968
- })).pipe(Command.withDescription("Print the current default session"));
969
- var sessionUse = Command.make("use", {
970
- id: Argument.String("id")
971
- }, Effect3.fn("Cli.sessionUse")(function* ({ id }) {
972
- const store = yield* Service2;
973
- yield* ensureSessionExists(id);
974
- yield* store.write(id);
975
- yield* Console.log(id);
976
- })).pipe(Command.withDescription("Set the current default session"));
977
- var sessionReset = Command.make("reset", {
978
- id: Argument.String("id").pipe(Argument.optional),
979
- session: sessionFlag("Reset this Browse session id")
980
- }, Effect3.fn("Cli.sessionReset")(function* ({ id, session }) {
1225
+ yield* printOr(json, result, formatNetworkResult(result));
1226
+ })).pipe(Command3.withDescription("Stop capture and write a redacted artifact or reusable secret profile"));
1227
+ var networkCancel = Command3.make("cancel", { session: sessionFlag() }, Effect6.fn("Cli.networkCancel")(function* ({ session }) {
981
1228
  const relay = yield* Service;
982
- const sessionId = yield* resolvePositionalOrFlagSessionId(id, session);
1229
+ const sessionId = yield* networkSession(session);
983
1230
  yield* ensureCliRelay();
984
- const reset = yield* relay.sessionReset(sessionId);
985
- if (reset.stopped)
986
- yield* Console.error(`Stopped first: ${describeStop(reset.stopped)}`);
987
- yield* Console.log(reset.session.id);
988
- })).pipe(Command.withDescription("Reset a Browse session state and page"));
989
- var sessionAdopt = Command.make("adopt", {
990
- session: sessionFlag("Adopt into this Browse session, creating it when it does not exist yet; omit to create a fresh readable id"),
991
- targetUrl: Flag.String("target-url").pipe(Flag.optional, Flag.withDescription("Adopt the attached page whose URL contains this text")),
992
- targetIndex: Flag.Int("target-index").pipe(Flag.optional, Flag.withDescription("Adopt the attached page at this zero-based target index")),
993
- tabId: tabIdFlag("Adopt the user's shared tab with this Chrome tab id (the tab= value in status)")
994
- }, Effect3.fn("Cli.sessionAdopt")(function* ({ session, targetUrl, targetIndex, tabId }) {
995
- const relay = yield* Service;
996
- const explicitSessionId = Option2.getOrUndefined(session) ?? Option2.getOrUndefined(yield* sessionIdConfig);
997
- yield* ensureCliRelayAndExtension(explicitSessionId);
998
- const targetUrlValue = Option2.getOrUndefined(targetUrl);
999
- const targetIndexValue = Option2.getOrUndefined(targetIndex);
1000
- if (targetIndexValue !== undefined && targetIndexValue < 0) {
1001
- return yield* Effect3.fail(new Error("Target index must be a non-negative integer"));
1002
- }
1003
- const tabIdValue = Option2.getOrUndefined(tabId);
1004
- if ([targetUrlValue, targetIndexValue, tabIdValue].filter((value) => value !== undefined).length > 1) {
1005
- return yield* Effect3.fail(new Error("Use only one target selector: --target-url, --target-index, or --tab-id"));
1006
- }
1007
- const targetSelection = targetUrlValue ? { urlIncludes: targetUrlValue } : targetIndexValue !== undefined ? { index: targetIndexValue } : undefined;
1008
- const result = yield* relay.sessionAdopt({
1009
- ...explicitSessionId ? { sessionId: explicitSessionId } : {},
1010
- createIfMissing: true,
1011
- ...targetSelection ? { targetSelection } : {},
1012
- ...tabIdValue === undefined ? {} : { tabId: tabIdValue }
1013
- });
1014
- yield* Console.log(`${result.session.created ? "Created and adopted" : "Adopted"} session '${result.session.id}' default page: ${result.adoptedUrl}`);
1015
- if (result.session.created) {
1016
- yield* Console.error(formatSessionContinuation(result.session.id));
1017
- }
1018
- })).pipe(Command.withDescription("Make an attached tab the session's default page"));
1019
- function describeStop(stopped) {
1020
- return [
1021
- stopped.execute ? "a running execute" : undefined,
1022
- stopped.handoff ? "a pending handoff" : undefined
1023
- ].filter(Boolean).join(" and ") + (stopped.forced ? " (the script did not stop in time and was abandoned with its state)" : "");
1024
- }
1025
- var sessionDelete = Command.make("delete", {
1026
- id: Argument.String("id").pipe(Argument.optional),
1027
- session: sessionFlag("Delete this Browse session id")
1028
- }, Effect3.fn("Cli.sessionDelete")(function* ({ id, session }) {
1029
- const relay = yield* Service;
1030
- const store = yield* Service2;
1031
- const sessionId = yield* resolvePositionalOrFlagSessionId(id, session);
1032
- yield* ensureCliRelay();
1033
- const deleted = yield* relay.sessionDelete(sessionId);
1034
- if (deleted.stopped)
1035
- yield* Console.error(`Stopped first: ${describeStop(deleted.stopped)}`);
1036
- const current = yield* store.read;
1037
- if (current === sessionId) {
1038
- yield* store.clear;
1039
- }
1040
- yield* Console.log(sessionId);
1041
- })).pipe(Command.withDescription("Delete a Browse session"));
1042
- var sessionAllow = Command.make("allow", {
1043
- session: sessionFlag(),
1044
- origins: Argument.String("origins").pipe(Argument.variadic({ min: 0 }), Argument.withDescription("Hosts or origins the session's tabs may load: example.com, *.example.com, https://app.example.com, http://localhost:3000")),
1045
- clear: Flag.Boolean("clear").pipe(Flag.withDefault(false), Flag.withDescription("Remove the allowlist: the session may load any site again")),
1046
- json: jsonFlag
1047
- }, Effect3.fn("Cli.sessionAllow")(function* ({ session, origins, clear, json }) {
1048
- const relay = yield* Service;
1049
- yield* ensureCliRelay();
1050
- if (!clear && origins.length === 0) {
1051
- return yield* Effect3.fail(new Error("session allow needs origins, or --clear to remove the allowlist"));
1052
- }
1053
- const id = yield* resolvePositionalOrFlagSessionId(Option2.none(), session);
1054
- const result = yield* relay.sessionAllowlist(id, clear ? [] : origins);
1055
- if (json) {
1056
- yield* emitJson(result);
1057
- return;
1058
- }
1059
- yield* Console.log(result.allowedOrigins ? `${result.id} may load: ${result.allowedOrigins.join(", ")}` : `${result.id} may load any site`);
1060
- })).pipe(Command.withDescription("Set the domain allowlist of a session (the current one by default)"));
1061
- var session = Command.make("session").pipe(Command.withDescription("Manage Browse sessions"), Command.withSubcommands([sessionNew, sessionList, sessionCurrent, sessionUse, sessionReset, sessionAdopt, sessionDelete, sessionAllow]));
1062
- var status = Command.make("status", {
1063
- json: jsonFlag
1064
- }, Effect3.fn("Cli.status")(function* ({ json }) {
1065
- const relay = yield* Service;
1066
- const store = yield* Service2;
1067
- const [relayResult, update] = yield* Effect3.all([Effect3.result(relay.version), Effect3.promise(() => updateNotice2())], { concurrency: 2 });
1068
- const updateLine = update ? formatUpdateNotice2(update) : undefined;
1069
- if (Result.isFailure(relayResult)) {
1070
- if (!(relayResult.failure instanceof RelayUnreachable)) {
1071
- if (json) {
1072
- yield* emitJson({
1073
- endpoint: relay.endpoint,
1074
- relay: { running: false, error: relayResult.failure.message },
1075
- extension: null,
1076
- sessions: [],
1077
- targets: [],
1078
- update: updateJson2(update)
1079
- });
1080
- }
1081
- yield* stderrLine(`Relay status failed: ${relayResult.failure.message}`);
1082
- yield* exitWith(ExitCode.host);
1083
- return;
1084
- }
1085
- const stopped = stoppedRelayStatus(relay.endpoint);
1086
- if (json) {
1087
- yield* emitJson({ ...stopped, update: updateJson2(update), autoUpdate: autoUpdateStatus() });
1088
- yield* stderrLine(`relay is not running at ${relay.endpoint}`);
1089
- } else {
1090
- yield* Console.log(`Relay: stopped (${relay.endpoint})`);
1091
- yield* Console.log(`Run \`${cliCommand("execute")}\` (or any relay-backed command) to start it automatically.`);
1092
- if (updateLine)
1093
- yield* Console.log(updateLine);
1094
- }
1095
- yield* exitWith(ExitCode.host);
1096
- return;
1097
- }
1098
- const version = relayResult.success;
1099
- const buildProblem = relayBuildProblem(version);
1100
- const [extensionStatus, current] = yield* Effect3.all([relay.extensionStatus, store.read]);
1101
- const collections = statusCollections(extensionStatus);
1102
- const [sessions, targets] = collections ? [collections.sessions, collections.targets] : yield* Effect3.all([relay.sessions, relay.targets]);
1103
- if (json) {
1104
- yield* emitJson({
1105
- endpoint: relay.endpoint,
1106
- relay: { running: true, version: version.version, buildId: version.buildId ?? null, stale: buildProblem !== undefined },
1107
- extension: extensionStatus,
1108
- currentSession: current ?? null,
1109
- sessions,
1110
- targets,
1111
- update: updateJson2(update),
1112
- autoUpdate: autoUpdateStatus()
1113
- });
1114
- if (buildProblem) {
1115
- yield* stderrLine(buildProblem);
1116
- yield* failExit;
1117
- }
1118
- return;
1119
- }
1120
- if (updateLine)
1121
- yield* Console.log(updateLine);
1122
- yield* Console.log(`Relay: ${relay.endpoint} (${version.version})`);
1123
- if (buildProblem) {
1124
- yield* Console.log(`Warning: ${buildProblem}`);
1125
- }
1126
- yield* Console.log(`Extension: ${extensionStatus.connected ? "connected" : "disconnected"}${extensionStatus.version ? ` (${extensionStatus.version})` : ""}`);
1127
- if ((extensionStatus.rejectedConnections ?? 0) > 0) {
1128
- yield* Console.log(`Warning: ${extensionStatus.rejectedConnections} competing browser/profile connection attempt(s) rejected; the active connection was preserved. Use Browse in one browser/profile at a time.`);
1129
- }
1130
- if (extensionStatus.nextStep)
1131
- yield* Console.log(extensionStatus.nextStep);
1132
- else if ((extensionStatus.pairingFailures ?? 0) > 0 && !extensionStatus.connected) {
1133
- yield* Console.log(`Warning: ${extensionStatus.pairingFailures} extension connection(s) failed pairing. Update the extension, or run \`${cliCommand("pair")}\` and enter the code on its status page.`);
1134
- }
1135
- if ((extensionStatus.processFaults?.suppressed ?? 0) > 0) {
1136
- const faults = extensionStatus.processFaults;
1137
- yield* Console.log(`Warning: the relay survived ${faults.suppressed} Playwright fault(s)${faults.lastAt ? `, last at ${faults.lastAt}` : ""}${faults.lastMessage ? `: ${faults.lastMessage}` : ""}. See the relay log.`);
1138
- }
1139
- if (extensionStatus.protocolVersion !== undefined && extensionStatus.protocolVersion !== null) {
1140
- const compatibility = extensionStatus.protocolCompatible === false ? "incompatible" : "compatible";
1141
- const legacy = extensionStatus.protocolLegacy === true ? ", inferred from legacy hello" : "";
1142
- yield* Console.log(`Extension protocol: ${extensionStatus.protocolVersion} (${compatibility}${legacy})`);
1143
- if (extensionStatus.protocolCompatible === false && !extensionStatus.nextStep)
1144
- yield* Console.log(extensionProtocolAdvice(extensionStatus.protocolVersion, { install: installCommand() }));
1145
- }
1146
- yield* Console.log(`Active targets: ${extensionStatus.activeTargets}`);
1147
- if (extensionStatus.childTargets !== undefined) {
1148
- yield* Console.log(`Child targets: ${extensionStatus.childTargets}`);
1149
- }
1150
- if (extensionStatus.cdpClients !== undefined) {
1151
- yield* Console.log(`CDP clients: ${extensionStatus.cdpClients}`);
1152
- }
1153
- yield* Console.log(`Current session: ${current ?? "none"}`);
1154
- if (sessions.length === 0) {
1155
- yield* Console.log("Sessions: none");
1156
- } else {
1157
- yield* Console.log("Sessions:");
1158
- const activeSessions = sessions.filter((item) => item.id === current || item.connected || item.pageUrl !== null || item.stateKeys.length > 0 || item.readOnly);
1159
- const idleSessions = sessions.filter((item) => !activeSessions.includes(item));
1160
- const shownSessions = activeSessions.length > 0 ? activeSessions : idleSessions.slice(0, 5);
1161
- const omittedIdleCount = activeSessions.length > 0 ? idleSessions.length : Math.max(0, idleSessions.length - shownSessions.length);
1162
- yield* Effect3.forEach(shownSessions, (item) => {
1163
- const marker = item.id === current ? "*" : " ";
1164
- return Console.log(`${marker} ${item.id} ${item.pageUrl ?? "no page yet"}`);
1165
- });
1166
- if (omittedIdleCount > 0) {
1167
- yield* Console.log(` (${omittedIdleCount} idle session(s) with no page omitted; run ${cliCommand("session list")} to view all)`);
1168
- }
1169
- }
1170
- if (targets.length === 0) {
1171
- yield* Console.log("Targets: none");
1172
- } else {
1173
- yield* Console.log("Targets:");
1174
- yield* Effect3.forEach(targets, (target, index) => {
1175
- return Console.log(`- [${index}] ${formatTargetSummary(target, { includeSession: true })}`);
1176
- });
1177
- }
1178
- if (buildProblem) {
1179
- yield* failExit;
1180
- }
1181
- })).pipe(Command.withDescription("Show relay, extension, and target status"));
1182
- var recordingStart = Command.make("start", {
1183
- outputPath: Argument.String("output-path").pipe(Argument.withDescription("Path to write the recording artifact; tabCapture requires .webm, CDP accepts .webm or .mp4")),
1231
+ const result = yield* relay.networkCancel({ sessionId });
1232
+ yield* Console4.log(result.cancelled ? "Network capture cancelled" : "Network capture was not active");
1233
+ })).pipe(Command3.withDescription("Cancel capture without writing an artifact"));
1234
+ var network = Command3.make("network").pipe(Command3.withDescription("Capture authenticated network exchanges for direct client derivation"), Command3.withSubcommands([networkStart, networkStatus, networkStop, networkCancel]));
1235
+ registerCliCommand(600, () => network);
1236
+
1237
+ // src/cli/recording.ts
1238
+ import { Console as Console5, Effect as Effect7, Option as Option6 } from "effect";
1239
+ import { Argument as Argument3, Command as Command4, Flag as Flag5 } from "effect/cli";
1240
+ import path4 from "node:path";
1241
+ var recordingStart = Command4.make("start", {
1242
+ outputPath: Argument3.String("output-path").pipe(Argument3.withDescription("Path to write the recording artifact; tabCapture requires .webm, CDP accepts .webm or .mp4")),
1184
1243
  session: sessionFlag("Record the page for this Browse or CDP session id"),
1185
1244
  tabId: tabIdFlag("Record this attached Chrome tab id"),
1186
- mode: Flag.String("mode").pipe(Flag.optional, Flag.withDescription("Recording mode: auto, tab-capture, or cdp. auto uses CDP for relay-owned tabs and tabCapture for user-owned tabs")),
1187
- audio: Flag.Boolean("audio").pipe(Flag.withDefault(false), Flag.withDescription("Include tab audio")),
1188
- frameRate: Flag.Int("frame-rate").pipe(Flag.optional, Flag.withDescription("Output frame rate, integer 1..60; defaults to 30 for tab-capture and 60 for CDP")),
1189
- maxDurationMs: Flag.Int("max-duration-ms").pipe(Flag.optional, Flag.withDescription("Auto-stop guard in milliseconds, defaults to 900000")),
1190
- cursor: Flag.Boolean("cursor").pipe(Flag.withDefault(false), Flag.withDescription("Draw the ghost cursor in the recording (off by default: only the page is recorded)")),
1245
+ 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")),
1246
+ audio: Flag5.Boolean("audio").pipe(Flag5.withDefault(false), Flag5.withDescription("Include tab audio")),
1247
+ 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")),
1248
+ maxDurationMs: Flag5.Int("max-duration-ms").pipe(Flag5.optional, Flag5.withDescription("Auto-stop guard in milliseconds, defaults to 900000")),
1249
+ 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)")),
1191
1250
  json: jsonFlag
1192
- }, Effect3.fn("Cli.recordingStart")(function* ({ outputPath, session, tabId, mode, audio, frameRate, maxDurationMs, cursor, json }) {
1193
- if (Option2.isSome(frameRate) && (frameRate.value < 1 || frameRate.value > 60)) {
1194
- return yield* Effect3.fail(new Error("Recording frameRate must be an integer from 1 to 60"));
1251
+ }, Effect7.fn("Cli.recordingStart")(function* ({
1252
+ outputPath,
1253
+ session,
1254
+ tabId,
1255
+ mode,
1256
+ audio,
1257
+ frameRate,
1258
+ maxDurationMs,
1259
+ cursor,
1260
+ json
1261
+ }) {
1262
+ if (Option6.isSome(frameRate) && (frameRate.value < 1 || frameRate.value > 60)) {
1263
+ return yield* Effect7.fail(new Error("Recording frameRate must be an integer from 1 to 60"));
1195
1264
  }
1196
1265
  const relay = yield* Service;
1197
1266
  yield* ensureCliRelayAndExtension();
1198
1267
  const target = yield* recordingTarget({ session, tabId });
1199
- const modeValue = yield* parseRecordingModeOption(Option2.getOrUndefined(mode));
1200
- const resolvedOutputPath = path.resolve(outputPath);
1268
+ const modeValue = yield* parseRecordingModeOption(Option6.getOrUndefined(mode));
1269
+ const resolvedOutputPath = path4.resolve(outputPath);
1201
1270
  const result = yield* relay.recordingStart({
1202
1271
  ...target,
1203
1272
  outputPath: resolvedOutputPath,
1204
1273
  ...modeValue === undefined ? {} : { mode: modeValue },
1205
1274
  audio,
1206
- ...Option2.isSome(frameRate) ? { frameRate: frameRate.value } : {},
1207
- ...Option2.isSome(maxDurationMs) ? { maxDurationMs: maxDurationMs.value } : {},
1275
+ ...Option6.isSome(frameRate) ? { frameRate: frameRate.value } : {},
1276
+ ...Option6.isSome(maxDurationMs) ? { maxDurationMs: maxDurationMs.value } : {},
1208
1277
  ...cursor ? { cursor } : {}
1209
1278
  });
1210
1279
  if (!result.success) {
1211
- return yield* Effect3.fail(new Error(result.error ?? "Failed to start recording"));
1280
+ return yield* Effect7.fail(new Error(result.error ?? "Failed to start recording"));
1212
1281
  }
1213
1282
  yield* printOr(json, result, `Recording started: ${result.path ?? resolvedOutputPath} tab=${result.tabId ?? "unknown"} mode=${result.mode ?? "tab-capture"} artifact=${result.artifactType ?? "webm"} mime=${result.mimeType ?? "video/webm"} fps=${result.frameRate ?? "unknown"}`);
1214
- })).pipe(Command.withDescription("Start recording an attached tab"));
1215
- var recordingStop = Command.make("stop", {
1283
+ })).pipe(Command4.withDescription("Start recording an attached tab"));
1284
+ var recordingStop = Command4.make("stop", {
1216
1285
  session: sessionFlag("Stop recording for this CDP session id"),
1217
1286
  tabId: tabIdFlag("Stop recording for this Chrome tab id"),
1218
1287
  json: jsonFlag
1219
- }, Effect3.fn("Cli.recordingStop")(function* ({ session, tabId, json }) {
1288
+ }, Effect7.fn("Cli.recordingStop")(function* ({ session, tabId, json }) {
1220
1289
  const relay = yield* Service;
1221
1290
  yield* ensureCliRelay();
1222
1291
  const target = yield* recordingTarget({ session, tabId });
1223
1292
  const result = yield* relay.recordingStop(target);
1224
1293
  if (!result.success) {
1225
- return yield* Effect3.fail(new Error(result.error ?? "Failed to stop recording"));
1294
+ return yield* Effect7.fail(new Error(result.error ?? "Failed to stop recording"));
1226
1295
  }
1227
1296
  if (json)
1228
1297
  return yield* emitJson(result);
1229
1298
  const frames = result.frameCount === undefined ? "" : `, frames=${result.frameCount}`;
1230
- yield* Console.log(`Recording saved: ${result.path ?? "unknown"} (${result.size ?? 0} bytes, ${result.duration ?? 0}ms, mode=${result.mode ?? "tab-capture"}, artifact=${result.artifactType ?? "webm"}${frames})`);
1231
- yield* Console.log(formatRecordingQuality(result.quality));
1232
- })).pipe(Command.withDescription("Stop recording and write the artifact"));
1233
- var recordingStatus = Command.make("status", {
1299
+ 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})`);
1300
+ yield* Console5.log(formatRecordingQuality(result.quality));
1301
+ })).pipe(Command4.withDescription("Stop recording and write the artifact"));
1302
+ var recordingStatus = Command4.make("status", {
1234
1303
  session: sessionFlag("Check recording for this CDP session id"),
1235
1304
  tabId: tabIdFlag("Check recording for this Chrome tab id"),
1236
1305
  json: jsonFlag
1237
- }, Effect3.fn("Cli.recordingStatus")(function* ({ session, tabId, json }) {
1306
+ }, Effect7.fn("Cli.recordingStatus")(function* ({ session, tabId, json }) {
1238
1307
  const relay = yield* Service;
1239
1308
  yield* ensureCliRelay();
1240
1309
  const target = yield* recordingTarget({ session, tabId });
@@ -1244,175 +1313,129 @@ var recordingStatus = Command.make("status", {
1244
1313
  return;
1245
1314
  }
1246
1315
  if (!result.isRecording) {
1247
- yield* Console.log("Recording: inactive");
1316
+ yield* Console5.log("Recording: inactive");
1248
1317
  return;
1249
1318
  }
1250
1319
  const frames = result.frameCount === undefined ? "" : ` frameCount=${result.frameCount}`;
1251
- yield* Console.log(`Recording: active tab=${result.tabId ?? "unknown"} mode=${result.mode ?? "tab-capture"} artifact=${result.artifactType ?? "webm"} path=${result.path ?? "unknown"} size=${result.size ?? 0}${frames} startedAt=${result.startedAt ?? "unknown"}`);
1252
- yield* Console.log(formatRecordingQuality(result.quality));
1253
- })).pipe(Command.withDescription("Check current recording status"));
1254
- var recordingCancel = Command.make("cancel", {
1320
+ 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"}`);
1321
+ yield* Console5.log(formatRecordingQuality(result.quality));
1322
+ })).pipe(Command4.withDescription("Check current recording status"));
1323
+ var recordingCancel = Command4.make("cancel", {
1255
1324
  session: sessionFlag("Cancel recording for this CDP session id"),
1256
1325
  tabId: tabIdFlag("Cancel recording for this Chrome tab id")
1257
- }, Effect3.fn("Cli.recordingCancel")(function* ({ session, tabId }) {
1326
+ }, Effect7.fn("Cli.recordingCancel")(function* ({ session, tabId }) {
1258
1327
  const relay = yield* Service;
1259
1328
  yield* ensureCliRelay();
1260
1329
  const target = yield* recordingTarget({ session, tabId });
1261
1330
  const result = yield* relay.recordingCancel(target);
1262
1331
  if (!result.success) {
1263
- return yield* Effect3.fail(new Error(result.error ?? "Failed to cancel recording"));
1332
+ return yield* Effect7.fail(new Error(result.error ?? "Failed to cancel recording"));
1264
1333
  }
1265
- yield* Console.log("Recording cancelled");
1266
- })).pipe(Command.withDescription("Cancel recording without writing a file"));
1267
- var recording = Command.make("recording").pipe(Command.withDescription("Record an attached tab to WebM or MP4"), Command.withSubcommands([recordingStart, recordingStop, recordingStatus, recordingCancel]));
1268
- var flightRecorderStart = Command.make("start", {
1334
+ yield* Console5.log("Recording cancelled");
1335
+ })).pipe(Command4.withDescription("Cancel recording without writing a file"));
1336
+ var recording = Command4.make("recording").pipe(Command4.withDescription("Record an attached tab to WebM or MP4"), Command4.withSubcommands([recordingStart, recordingStop, recordingStatus, recordingCancel]));
1337
+ var flightRecorderStart = Command4.make("start", {
1269
1338
  session: sessionFlag("Buffer the tab owned by this session"),
1270
1339
  tabId: tabIdFlag("Buffer this attached Chrome tab id"),
1271
- retentionMs: Flag.Int("retention-ms").pipe(Flag.optional, Flag.withDescription("Ring-buffer duration in milliseconds (1000..120000; default 60000)")),
1272
- frameRate: Flag.Int("frame-rate").pipe(Flag.optional, Flag.withDescription("Output frame rate from 1 to 60")),
1273
- cursor: Flag.Boolean("cursor").pipe(Flag.withDefault(false), Flag.withDescription("Draw the ghost cursor in the buffered video (off by default)")),
1340
+ retentionMs: Flag5.Int("retention-ms").pipe(Flag5.optional, Flag5.withDescription("Ring-buffer duration in milliseconds (1000..120000; default 60000)")),
1341
+ frameRate: Flag5.Int("frame-rate").pipe(Flag5.optional, Flag5.withDescription("Output frame rate from 1 to 60")),
1342
+ cursor: Flag5.Boolean("cursor").pipe(Flag5.withDefault(false), Flag5.withDescription("Draw the ghost cursor in the buffered video (off by default)")),
1274
1343
  json: jsonFlag
1275
- }, Effect3.fn("Cli.flightRecorderStart")(function* ({ session, tabId, retentionMs, frameRate, cursor, json }) {
1344
+ }, Effect7.fn("Cli.flightRecorderStart")(function* ({ session, tabId, retentionMs, frameRate, cursor, json }) {
1276
1345
  const relay = yield* Service;
1277
1346
  yield* ensureCliRelayAndExtension();
1278
1347
  const target = yield* recordingTarget({ session, tabId });
1279
1348
  const result = yield* relay.flightRecorderStart({
1280
1349
  ...target,
1281
- ...Option2.isSome(retentionMs) ? { retentionMs: retentionMs.value } : {},
1282
- ...Option2.isSome(frameRate) ? { frameRate: frameRate.value } : {},
1350
+ ...Option6.isSome(retentionMs) ? { retentionMs: retentionMs.value } : {},
1351
+ ...Option6.isSome(frameRate) ? { frameRate: frameRate.value } : {},
1283
1352
  ...cursor ? { cursor } : {}
1284
1353
  });
1285
1354
  yield* printOr(json, result, `Flight recorder buffering tab ${result.tabId}; retention=${result.retentionMs}ms`);
1286
- })).pipe(Command.withDescription("Start a rolling in-memory video buffer"));
1287
- var flightRecorderStatus = Command.make("status", {
1355
+ })).pipe(Command4.withDescription("Start a rolling in-memory video buffer"));
1356
+ var flightRecorderStatus = Command4.make("status", {
1288
1357
  session: sessionFlag(),
1289
1358
  tabId: tabIdFlag(),
1290
1359
  json: jsonFlag
1291
- }, Effect3.fn("Cli.flightRecorderStatus")(function* ({ session, tabId, json }) {
1360
+ }, Effect7.fn("Cli.flightRecorderStatus")(function* ({ session, tabId, json }) {
1292
1361
  const relay = yield* Service;
1293
1362
  yield* ensureCliRelay();
1294
1363
  const result = yield* relay.flightRecorderStatus(yield* recordingTarget({ session, tabId }));
1295
1364
  if (json)
1296
1365
  return yield* emitJson(result);
1297
1366
  if (!result.active)
1298
- return yield* Console.log("Flight recorder inactive");
1299
- yield* Console.log(`Flight recorder tab=${result.tabId} frames=${result.bufferedFrames} retained=${result.retainedDurationMs}ms bytes=${result.bufferedBytes}`);
1300
- })).pipe(Command.withDescription("Show rolling flight-recorder status"));
1301
- var flightRecorderSaveLast = Command.make("save-last", {
1302
- outputPath: Argument.String("output-path").pipe(Argument.withDescription("Fresh .webm or .mp4 artifact path")),
1367
+ return yield* Console5.log("Flight recorder inactive");
1368
+ yield* Console5.log(`Flight recorder tab=${result.tabId} frames=${result.bufferedFrames} retained=${result.retainedDurationMs}ms bytes=${result.bufferedBytes}`);
1369
+ })).pipe(Command4.withDescription("Show rolling flight-recorder status"));
1370
+ var flightRecorderSaveLast = Command4.make("save-last", {
1371
+ outputPath: Argument3.String("output-path").pipe(Argument3.withDescription("Fresh .webm or .mp4 artifact path")),
1303
1372
  session: sessionFlag(),
1304
1373
  tabId: tabIdFlag(),
1305
- durationMs: Flag.Int("duration-ms").pipe(Flag.optional, Flag.withDescription("How much recent history to save; defaults to 30 seconds")),
1374
+ durationMs: Flag5.Int("duration-ms").pipe(Flag5.optional, Flag5.withDescription("How much recent history to save; defaults to 30 seconds")),
1306
1375
  json: jsonFlag
1307
- }, Effect3.fn("Cli.flightRecorderSaveLast")(function* ({ outputPath, session, tabId, durationMs, json }) {
1376
+ }, Effect7.fn("Cli.flightRecorderSaveLast")(function* ({ outputPath, session, tabId, durationMs, json }) {
1308
1377
  const relay = yield* Service;
1309
1378
  yield* ensureCliRelay();
1310
1379
  const result = yield* relay.flightRecorderSaveLast({
1311
1380
  ...yield* recordingTarget({ session, tabId }),
1312
- outputPath: path.resolve(outputPath),
1313
- ...Option2.isSome(durationMs) ? { durationMs: durationMs.value } : {}
1381
+ outputPath: path4.resolve(outputPath),
1382
+ ...Option6.isSome(durationMs) ? { durationMs: durationMs.value } : {}
1314
1383
  });
1315
1384
  yield* printOr(json, result, `Saved ${result.durationMs}ms flight recorder clip (${result.frameCount} frames) to ${result.path}`);
1316
- })).pipe(Command.withDescription("Save the most recent buffered video without stopping the recorder"));
1317
- var flightRecorderCancel = Command.make("cancel", {
1385
+ })).pipe(Command4.withDescription("Save the most recent buffered video without stopping the recorder"));
1386
+ var flightRecorderCancel = Command4.make("cancel", {
1318
1387
  session: sessionFlag(),
1319
1388
  tabId: tabIdFlag()
1320
- }, Effect3.fn("Cli.flightRecorderCancel")(function* ({ session, tabId }) {
1389
+ }, Effect7.fn("Cli.flightRecorderCancel")(function* ({ session, tabId }) {
1321
1390
  const relay = yield* Service;
1322
1391
  yield* ensureCliRelay();
1323
1392
  const result = yield* relay.flightRecorderCancel(yield* recordingTarget({ session, tabId }));
1324
- yield* Console.log(result.cancelled ? "Flight recorder stopped" : "No active flight recorder");
1325
- })).pipe(Command.withDescription("Stop and discard the rolling video buffer"));
1326
- var flightRecorder = Command.make("flight-recorder").pipe(Command.withDescription("Keep and save a rolling buffer of recent browser video"), Command.withSubcommands([flightRecorderStart, flightRecorderStatus, flightRecorderSaveLast, flightRecorderCancel]));
1327
- var networkSession = Effect3.fnUntraced(function* (session) {
1328
- return yield* resolveSelectedSessionId(Option2.getOrUndefined(session) ?? Option2.getOrUndefined(yield* sessionIdConfig));
1329
- });
1330
- var parseNetworkContent = Effect3.fnUntraced(function* (content) {
1331
- const value = Option2.getOrUndefined(content);
1332
- if (value === undefined || value === "embed" || value === "omit")
1333
- return value;
1334
- return yield* Effect3.fail(new Error("Network content must be embed or omit"));
1335
- });
1336
- function formatNetworkStatus(status) {
1337
- if (!status.active)
1338
- return status.ended ? `Network capture: inactive (the last one ended because ${status.ended})` : "Network capture: inactive";
1339
- return `Network capture: active entries=${status.entryCount} responses=${status.responseCount} failures=${status.failureCount} bodyBytes=${status.capturedBodyBytes} truncated=${status.truncatedBodyCount} dropped=${status.droppedEntryCount} startedAt=${status.startedAt ?? "unknown"}${status.endsAt ? ` endsAt=${status.endsAt}` : ""}`;
1340
- }
1341
- function formatNetworkResult(result) {
1342
- const output = result.outputPath ? ` output=${result.outputPath}` : "";
1343
- const profile = result.authProfile ? ` secrets=${result.authProfile.name}(${result.authProfile.slotCount})` : "";
1344
- return `Network capture stopped: entries=${result.entryCount} responses=${result.responseCount} failures=${result.failureCount} bodyBytes=${result.capturedBodyBytes} truncated=${result.truncatedBodyCount} dropped=${result.droppedEntryCount}${output}${profile}`;
1345
- }
1346
- var networkStart = Command.make("start", {
1347
- session: sessionFlag("Capture the default page for this Browse session"),
1348
- urlFilter: Flag.String("url").pipe(Flag.optional, Flag.withDescription("Capture only requests whose URL contains this text")),
1349
- resourceTypes: Flag.String("resource-type").pipe(Flag.atMost(50), Flag.withDescription("Capture this Playwright resource type; repeat for multiple types")),
1350
- content: Flag.String("content").pipe(Flag.optional, Flag.withDescription("Response and request body mode: embed (default) or omit")),
1351
- maxBodyBytes: Flag.Int("max-body-bytes").pipe(Flag.optional, Flag.withDescription("Maximum captured bytes per body, defaults to 1000000")),
1352
- maxTotalBodyBytes: Flag.Int("max-total-body-bytes").pipe(Flag.optional, Flag.withDescription("Maximum captured body bytes for the whole capture, defaults to 25000000")),
1353
- maxEntries: Flag.Int("max-entries").pipe(Flag.optional, Flag.withDescription("Maximum request entries, defaults to 1000")),
1354
- maxDuration: Flag.Int("max-duration-ms").pipe(Flag.optional, Flag.withDescription("End the capture by itself (discarding it) after this long, defaults to 1800000 (30 minutes)")),
1355
- json: jsonFlag
1356
- }, Effect3.fn("Cli.networkStart")(function* ({ session, urlFilter, resourceTypes, content, maxBodyBytes, maxTotalBodyBytes, maxEntries, maxDuration, json }) {
1357
- const relay = yield* Service;
1358
- yield* ensureCliRelayAndExtension();
1359
- const sessionId = yield* networkSession(session);
1360
- const contentValue = yield* parseNetworkContent(content);
1361
- const result = yield* relay.networkStart({
1362
- sessionId,
1363
- ...Option2.isSome(urlFilter) ? { urlFilter: urlFilter.value } : {},
1364
- ...resourceTypes.length === 0 ? {} : { resourceTypes },
1365
- ...contentValue === undefined ? {} : { content: contentValue },
1366
- ...Option2.isSome(maxBodyBytes) ? { maxBodyBytes: maxBodyBytes.value } : {},
1367
- ...Option2.isSome(maxTotalBodyBytes) ? { maxTotalBodyBytes: maxTotalBodyBytes.value } : {},
1368
- ...Option2.isSome(maxEntries) ? { maxEntries: maxEntries.value } : {},
1369
- ...Option2.isSome(maxDuration) ? { maxDurationMs: maxDuration.value } : {}
1370
- });
1371
- yield* printOr(json, result, formatNetworkStatus(result));
1372
- })).pipe(Command.withDescription("Start session-scoped network capture"));
1373
- var networkStatus = Command.make("status", {
1374
- session: sessionFlag(),
1375
- json: jsonFlag
1376
- }, Effect3.fn("Cli.networkStatus")(function* ({ session, json }) {
1377
- const relay = yield* Service;
1378
- const sessionId = yield* networkSession(session);
1379
- yield* ensureCliRelay();
1380
- const result = yield* relay.networkStatus({ sessionId });
1381
- yield* printOr(json, result, formatNetworkStatus(result));
1382
- })).pipe(Command.withDescription("Show session-scoped network capture status"));
1383
- var networkStop = Command.make("stop", {
1384
- session: sessionFlag(),
1385
- output: Flag.String("output").pipe(Flag.optional, Flag.withAlias("o"), Flag.withDescription("Write a credential-redacted HAR artifact to this path")),
1386
- secrets: Flag.String("secrets").pipe(Flag.optional, Flag.withDescription("Store captured credentials under this reusable profile name")),
1387
- json: jsonFlag
1388
- }, Effect3.fn("Cli.networkStop")(function* ({ session, output, secrets, json }) {
1389
- const relay = yield* Service;
1390
- const outputValue = Option2.getOrUndefined(output);
1391
- const secretsValue = Option2.getOrUndefined(secrets);
1392
- if (!outputValue && !secretsValue) {
1393
- return yield* Effect3.fail(new Error("network stop requires --output, --secrets, or both"));
1394
- }
1395
- const sessionId = yield* networkSession(session);
1396
- yield* ensureCliRelay();
1397
- const result = yield* relay.networkStop({
1398
- sessionId,
1399
- ...outputValue ? { outputPath: path.resolve(outputValue) } : {},
1400
- ...secretsValue ? { secrets: secretsValue } : {}
1401
- });
1402
- yield* printOr(json, result, formatNetworkResult(result));
1403
- })).pipe(Command.withDescription("Stop capture and write a redacted artifact or reusable secret profile"));
1404
- var networkCancel = Command.make("cancel", { session: sessionFlag() }, Effect3.fn("Cli.networkCancel")(function* ({ session }) {
1393
+ yield* Console5.log(result.cancelled ? "Flight recorder stopped" : "No active flight recorder");
1394
+ })).pipe(Command4.withDescription("Stop and discard the rolling video buffer"));
1395
+ var flightRecorder = Command4.make("flight-recorder").pipe(Command4.withDescription("Keep and save a rolling buffer of recent browser video"), Command4.withSubcommands([
1396
+ flightRecorderStart,
1397
+ flightRecorderStatus,
1398
+ flightRecorderSaveLast,
1399
+ flightRecorderCancel
1400
+ ]));
1401
+ registerCliCommand(700, () => recording);
1402
+ registerCliCommand(710, () => flightRecorder);
1403
+
1404
+ // src/cli/relay.ts
1405
+ import { Console as Console6, Effect as Effect8 } from "effect";
1406
+ import { Command as Command5, Flag as Flag6 } from "effect/cli";
1407
+ var serve = Command5.make("serve", {}, Effect8.fn("Cli.serve")(function* () {
1408
+ const [{ startRelay }, { parseAdditionalExtensionOrigins }] = yield* Effect8.promise(() => Promise.all([import("./relay-tfvn856e.mjs"), import("./helpers-rta4d2as.mjs")]));
1409
+ const port = yield* portConfig;
1410
+ const additionalExtensionOrigins = parseAdditionalExtensionOrigins(yield* extensionOriginsConfig);
1411
+ yield* Effect8.scoped(Effect8.gen(function* () {
1412
+ const relay = yield* startRelay({ port, additionalExtensionOrigins });
1413
+ yield* Console6.log(`Browse relay listening at ${relay.url}`);
1414
+ yield* Console6.log(`Browse connects automatically while the browser runs. Run \`${cliCommand("status")}\` to check.`);
1415
+ yield* Effect8.never;
1416
+ }));
1417
+ })).pipe(Command5.withDescription("Start the local Browse relay"));
1418
+ var relayRestart = Command5.make("restart", {
1419
+ 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"))
1420
+ }, Effect8.fn("Cli.relayRestart")(function* ({ force }) {
1405
1421
  const relay = yield* Service;
1406
- const sessionId = yield* networkSession(session);
1407
- yield* ensureCliRelay();
1408
- const result = yield* relay.networkCancel({ sessionId });
1409
- yield* Console.log(result.cancelled ? "Network capture cancelled" : "Network capture was not active");
1410
- })).pipe(Command.withDescription("Cancel capture without writing an artifact"));
1411
- var network = Command.make("network").pipe(Command.withDescription("Capture authenticated network exchanges for direct client derivation"), Command.withSubcommands([networkStart, networkStatus, networkStop, networkCancel]));
1412
- var secretsStatus = Command.make("status", {
1413
- name: Argument.String("name"),
1422
+ yield* Console6.error("Restarting the relay leaves browser tabs open but resets in-memory JavaScript state.");
1423
+ const readiness = yield* restartRelay({ relay, clientKind: "cli", force });
1424
+ yield* Console6.log(`${readiness.started ? "Started" : "Reused concurrent replacement"} Browse relay at ${relay.endpoint} (${readiness.version.buildId})`);
1425
+ })).pipe(Command5.withDescription("Explicitly drain and restart the managed relay using this installation"));
1426
+ var relay = Command5.make("relay").pipe(Command5.withDescription("Manage the local relay lifecycle"), Command5.withSubcommands([relayRestart]));
1427
+ registerCliCommand(100, () => serve);
1428
+ registerCliCommand(110, () => relay);
1429
+
1430
+ // src/cli/secrets.ts
1431
+ import { Console as Console7, Effect as Effect9, Option as Option7 } from "effect";
1432
+ import { Argument as Argument4, Command as Command6, Flag as Flag7 } from "effect/cli";
1433
+ import path5 from "node:path";
1434
+ import process2 from "node:process";
1435
+ var secretsStatus = Command6.make("status", {
1436
+ name: Argument4.String("name"),
1414
1437
  json: jsonFlag
1415
- }, Effect3.fn("Cli.secretsStatus")(function* ({ name, json }) {
1438
+ }, Effect9.fn("Cli.secretsStatus")(function* ({ name, json }) {
1416
1439
  const relay = yield* Service;
1417
1440
  yield* ensureCliRelay();
1418
1441
  const result = yield* relay.authStatus({ name });
@@ -1420,60 +1443,60 @@ var secretsStatus = Command.make("status", {
1420
1443
  yield* emitJson(result);
1421
1444
  return;
1422
1445
  }
1423
- yield* Console.log(`Secrets profile ${result.name}: slots=${result.slotCount} updatedAt=${result.updatedAt}`);
1424
- yield* Effect3.forEach(result.slots, (slot) => Console.log(`- ${slot.ref} sources=${slot.sources.join(",")} expired=${slot.expired}${slot.expiresAt ? ` expiresAt=${slot.expiresAt}` : ""}`));
1425
- })).pipe(Command.withDescription("Show secret profile metadata without revealing values"));
1426
- var secretsRefresh = Command.make("refresh", {
1427
- name: Argument.String("name"),
1446
+ yield* Console7.log(`Secrets profile ${result.name}: slots=${result.slotCount} updatedAt=${result.updatedAt}`);
1447
+ yield* Effect9.forEach(result.slots, (slot) => Console7.log(`- ${slot.ref} sources=${slot.sources.join(",")} expired=${slot.expired}${slot.expiresAt ? ` expiresAt=${slot.expiresAt}` : ""}`));
1448
+ })).pipe(Command6.withDescription("Show secret profile metadata without revealing values"));
1449
+ var secretsRefresh = Command6.make("refresh", {
1450
+ name: Argument4.String("name"),
1428
1451
  session: sessionFlag(),
1429
- urlFilter: Flag.String("url").pipe(Flag.optional, Flag.withDescription("Observe credentials only on matching request URLs")),
1430
- timeoutMs: Flag.Int("timeout-ms").pipe(Flag.optional, Flag.withDescription("Page reload timeout, defaults to 30000")),
1452
+ urlFilter: Flag7.String("url").pipe(Flag7.optional, Flag7.withDescription("Observe credentials only on matching request URLs")),
1453
+ timeoutMs: Flag7.Int("timeout-ms").pipe(Flag7.optional, Flag7.withDescription("Page reload timeout, defaults to 30000")),
1431
1454
  json: jsonFlag
1432
- }, Effect3.fn("Cli.secretsRefresh")(function* ({ name, session, urlFilter, timeoutMs, json }) {
1455
+ }, Effect9.fn("Cli.secretsRefresh")(function* ({ name, session, urlFilter, timeoutMs, json }) {
1433
1456
  const relay = yield* Service;
1434
1457
  yield* ensureCliRelayAndExtension();
1435
1458
  const sessionId = yield* networkSession(session);
1436
1459
  const result = yield* relay.authRefresh({
1437
1460
  sessionId,
1438
1461
  name,
1439
- ...Option2.isSome(urlFilter) ? { urlFilter: urlFilter.value } : {},
1440
- ...Option2.isSome(timeoutMs) ? { timeoutMs: timeoutMs.value } : {}
1462
+ ...Option7.isSome(urlFilter) ? { urlFilter: urlFilter.value } : {},
1463
+ ...Option7.isSome(timeoutMs) ? { timeoutMs: timeoutMs.value } : {}
1441
1464
  });
1442
1465
  yield* printOr(json, result, `Secrets profile ${name} refreshed: observed=${result.observedSecretRefs.length} changed=${result.updatedSecretRefs.length}`);
1443
- })).pipe(Command.withDescription("Reload a session page and refresh a profile while preserving stable references"));
1444
- var secretsRun = Command.make("run", {
1445
- name: Argument.String("name"),
1446
- command: Argument.String("command").pipe(Argument.variadic({ min: 1 })),
1447
- cwd: Flag.String("cwd").pipe(Flag.optional, Flag.withDescription("Child process working directory")),
1448
- timeoutMs: Flag.Int("timeout-ms").pipe(Flag.optional, Flag.withDescription("Child timeout in milliseconds, defaults to 120000"))
1449
- }, Effect3.fn("Cli.secretsRun")(function* ({ name, command, cwd, timeoutMs }) {
1466
+ })).pipe(Command6.withDescription("Reload a session page and refresh a profile while preserving stable references"));
1467
+ var secretsRun = Command6.make("run", {
1468
+ name: Argument4.String("name"),
1469
+ command: Argument4.String("command").pipe(Argument4.variadic({ min: 1 })),
1470
+ cwd: Flag7.String("cwd").pipe(Flag7.optional, Flag7.withDescription("Child process working directory")),
1471
+ timeoutMs: Flag7.Int("timeout-ms").pipe(Flag7.optional, Flag7.withDescription("Child timeout in milliseconds, defaults to 120000"))
1472
+ }, Effect9.fn("Cli.secretsRun")(function* ({ name, command, cwd, timeoutMs }) {
1450
1473
  const relay = yield* Service;
1451
1474
  yield* ensureCliRelay();
1452
1475
  const [executable, ...args] = decodeCliOperands(command);
1453
1476
  if (!executable)
1454
- return yield* Effect3.fail(new Error("secrets run requires a command after --"));
1477
+ return yield* Effect9.fail(new Error("secrets run requires a command after --"));
1455
1478
  const result = yield* relay.authRun({
1456
1479
  name,
1457
1480
  command: executable,
1458
1481
  args,
1459
- cwd: path.resolve(Option2.getOrElse(cwd, () => process.cwd())),
1460
- ...Option2.isSome(timeoutMs) ? { timeoutMs: timeoutMs.value } : {}
1482
+ cwd: path5.resolve(Option7.getOrElse(cwd, () => process2.cwd())),
1483
+ ...Option7.isSome(timeoutMs) ? { timeoutMs: timeoutMs.value } : {}
1461
1484
  });
1462
- yield* Effect3.sync(() => {
1485
+ yield* Effect9.sync(() => {
1463
1486
  if (result.stdout)
1464
- process.stdout.write(result.stdout);
1487
+ process2.stdout.write(result.stdout);
1465
1488
  if (result.stderr)
1466
- process.stderr.write(result.stderr);
1489
+ process2.stderr.write(result.stderr);
1467
1490
  if (result.stdoutTruncated || result.stderrTruncated)
1468
- process.stderr.write(`
1491
+ process2.stderr.write(`
1469
1492
  Browse truncated child output.
1470
1493
  `);
1471
1494
  if (result.exitCode !== 0)
1472
- process.exitCode = result.exitCode;
1495
+ process2.exitCode = result.exitCode;
1473
1496
  });
1474
- })).pipe(Command.withDescription("Run a command with profile values injected as BC_SECRET_* environment variables"));
1497
+ })).pipe(Command6.withDescription("Run a command with profile values injected as BC_SECRET_* environment variables"));
1475
1498
  function readSecretValue(prompt) {
1476
- const input = process.stdin;
1499
+ const input = process2.stdin;
1477
1500
  if (!input.isTTY) {
1478
1501
  return new Promise((resolve, reject) => {
1479
1502
  const chunks = [];
@@ -1483,13 +1506,13 @@ function readSecretValue(prompt) {
1483
1506
  });
1484
1507
  }
1485
1508
  return new Promise((resolve, reject) => {
1486
- process.stderr.write(prompt);
1509
+ process2.stderr.write(prompt);
1487
1510
  let value = "";
1488
1511
  const finish = (result) => {
1489
1512
  input.setRawMode(false);
1490
1513
  input.pause();
1491
1514
  input.off("data", onData);
1492
- process.stderr.write(`
1515
+ process2.stderr.write(`
1493
1516
  `);
1494
1517
  if (result instanceof Error)
1495
1518
  reject(result);
@@ -1514,44 +1537,49 @@ function readSecretValue(prompt) {
1514
1537
  input.on("data", onData);
1515
1538
  });
1516
1539
  }
1517
- var secretsAdd = Command.make("add", {
1518
- name: Argument.String("name").pipe(Argument.withDescription('Secret name agents use: secrets.type(locator, "name")')),
1519
- origin: Flag.String("origin").pipe(Flag.atLeast(1), Flag.withDescription("Origin the secret may be typed on (repeatable): https://example.com, *.example.com (https), http://localhost:3000")),
1520
- totp: Flag.Boolean("totp").pipe(Flag.withDefault(false), Flag.withDescription("The value is a TOTP seed (base32 or otpauth:// URI); typing it types the current code. Storing the seed beside the password makes 2FA one factor")),
1540
+ var secretsAdd = Command6.make("add", {
1541
+ name: Argument4.String("name").pipe(Argument4.withDescription('Secret name agents use: secrets.type(locator, "name")')),
1542
+ 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")),
1543
+ 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")),
1521
1544
  json: jsonFlag
1522
- }, Effect3.fn("Cli.secretsAdd")(function* ({ name, origin, totp, json }) {
1523
- const value = yield* Effect3.tryPromise({
1545
+ }, Effect9.fn("Cli.secretsAdd")(function* ({ name, origin, totp, json }) {
1546
+ const value = yield* Effect9.tryPromise({
1524
1547
  try: () => readSecretValue(totp ? `TOTP seed for ${name} (hidden): ` : `Value for ${name} (hidden): `),
1525
1548
  catch: (cause) => cause instanceof Error ? cause : new Error("Could not read the secret value")
1526
1549
  });
1527
- const result = yield* addSecret({ name, value, allowedOrigins: origin, kind: totp ? "totp" : "password" });
1550
+ const result = yield* addSecret({
1551
+ name,
1552
+ value,
1553
+ allowedOrigins: origin,
1554
+ kind: totp ? "totp" : "password"
1555
+ });
1528
1556
  if (json) {
1529
1557
  yield* emitJson(result);
1530
1558
  return;
1531
1559
  }
1532
- yield* Console.log(`Saved ${result.kind} secret ${result.name}; it can be typed on ${result.allowedOrigins.join(", ")}`);
1560
+ yield* Console7.log(`Saved ${result.kind} secret ${result.name}; it can be typed on ${result.allowedOrigins.join(", ")}`);
1533
1561
  if (totp)
1534
- yield* Console.error("Note: with the TOTP seed stored here, anyone who can use these secrets has both factors. Prefer handing 2FA to a person when you can.");
1535
- })).pipe(Command.withDescription("Store a secret agents can type only on its origins (value from stdin or a hidden prompt)"));
1536
- var secretsList = Command.make("list", { json: jsonFlag }, Effect3.fn("Cli.secretsList")(function* ({ json }) {
1562
+ 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.");
1563
+ })).pipe(Command6.withDescription("Store a secret agents can type only on its origins (value from stdin or a hidden prompt)"));
1564
+ var secretsList = Command6.make("list", { json: jsonFlag }, Effect9.fn("Cli.secretsList")(function* ({ json }) {
1537
1565
  const secrets = yield* listSecrets();
1538
1566
  if (json) {
1539
1567
  yield* emitJson({ secrets });
1540
1568
  return;
1541
1569
  }
1542
1570
  if (secrets.length === 0) {
1543
- yield* Console.log("No site secrets");
1571
+ yield* Console7.log("No site secrets");
1544
1572
  return;
1545
1573
  }
1546
- yield* Effect3.forEach(secrets, (secret) => Console.log(`${secret.name} ${secret.kind} ${secret.allowedOrigins.join(",")}`));
1547
- })).pipe(Command.withDescription("List site secrets (names, kinds and origins, never values)"));
1548
- var secretsRemove = Command.make("remove", { name: Argument.String("name") }, Effect3.fn("Cli.secretsRemove")(function* ({ name }) {
1574
+ yield* Effect9.forEach(secrets, (secret) => Console7.log(`${secret.name} ${secret.kind} ${secret.allowedOrigins.join(",")}`));
1575
+ })).pipe(Command6.withDescription("List site secrets (names, kinds and origins, never values)"));
1576
+ var secretsRemove = Command6.make("remove", { name: Argument4.String("name") }, Effect9.fn("Cli.secretsRemove")(function* ({ name }) {
1549
1577
  const removed = yield* removeSecret(name);
1550
- yield* Console.log(removed ? `Removed ${name}` : `No secret named ${name}`);
1551
- })).pipe(Command.withDescription("Delete a site secret"));
1552
- var secrets = Command.make("secrets").pipe(Command.withDescription("Site secrets agents can type without seeing them, and captured credential profiles"), Command.withSubcommands([secretsAdd, secretsList, secretsRemove, secretsStatus, secretsRefresh, secretsRun]));
1553
- var deadboltConnect = Command.make("connect", { json: jsonFlag }, Effect3.fn("Cli.deadboltConnect")(function* ({ json }) {
1554
- const paired = yield* Effect3.tryPromise({
1578
+ yield* Console7.log(removed ? `Removed ${name}` : `No secret named ${name}`);
1579
+ })).pipe(Command6.withDescription("Delete a site secret"));
1580
+ 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]));
1581
+ var deadboltConnect = Command6.make("connect", { json: jsonFlag }, Effect9.fn("Cli.deadboltConnect")(function* ({ json }) {
1582
+ const paired = yield* Effect9.tryPromise({
1555
1583
  try: () => pair(),
1556
1584
  catch: (cause) => cause instanceof Error ? cause : new Error("Could not connect to Deadbolt")
1557
1585
  });
@@ -1559,138 +1587,237 @@ var deadboltConnect = Command.make("connect", { json: jsonFlag }, Effect3.fn("Cl
1559
1587
  yield* emitJson({ connected: true, id: paired.id });
1560
1588
  return;
1561
1589
  }
1562
- yield* Console.log(`Connected Browse to Deadbolt as client ${paired.id}.`);
1563
- yield* Console.log("Agents can use only the logins you allow, in Deadbolt: deadbolt agents allow <site> (narrow with --login, --fields, --expires), or deadbolt agents allow --all for every login");
1564
- })).pipe(Command.withDescription("Pair Browse with Deadbolt (runs `deadbolt agents add-client browse`, which asks you to confirm) and store its client token (0600)"));
1565
- var deadboltDisconnect = Command.make("disconnect", {}, Effect3.fn("Cli.deadboltDisconnect")(function* () {
1566
- const result = yield* Effect3.try({
1590
+ yield* Console7.log(`Connected Browse to Deadbolt as client ${paired.id}.`);
1591
+ 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");
1592
+ })).pipe(Command6.withDescription("Pair Browse with Deadbolt (runs `deadbolt agents add-client browse`, which asks you to confirm) and store its client token (0600)"));
1593
+ var deadboltDisconnect = Command6.make("disconnect", {}, Effect9.fn("Cli.deadboltDisconnect")(function* () {
1594
+ const result = yield* Effect9.try({
1567
1595
  try: () => unpair(),
1568
1596
  catch: (cause) => cause instanceof Error ? cause : new Error("Could not disconnect from Deadbolt")
1569
1597
  });
1570
1598
  if (!result.removed) {
1571
- yield* Console.log("Browse isn't connected to Deadbolt");
1599
+ yield* Console7.log("Browse isn't connected to Deadbolt");
1572
1600
  return;
1573
1601
  }
1574
- yield* Console.log(result.revoked ? "Disconnected: the client is removed in Deadbolt and its token deleted here." : "Deleted the client token here. Deadbolt didn't confirm removing the client: check `deadbolt agents clients`.");
1575
- })).pipe(Command.withDescription("Remove Browse's Deadbolt client (in Deadbolt and here)"));
1576
- var deadboltStatus = Command.make("status", { json: jsonFlag }, Effect3.fn("Cli.deadboltStatus")(function* ({ json }) {
1602
+ 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`.");
1603
+ })).pipe(Command6.withDescription("Remove Browse's Deadbolt client (in Deadbolt and here)"));
1604
+ var deadboltStatus = Command6.make("status", { json: jsonFlag }, Effect9.fn("Cli.deadboltStatus")(function* ({ json }) {
1577
1605
  const client = readClient();
1578
1606
  const socket = socketPath();
1579
- const status = client ? yield* Effect3.promise(() => connect({ client, socket }).status().then((value) => ({ reachable: true, ...value }), (error) => ({ reachable: false, error: error instanceof Error ? error.message : String(error) }))) : undefined;
1580
- const report = { connected: client !== undefined, ...client ? { id: client.id } : {}, socket, ...status ? { deadbolt: status } : {} };
1607
+ const status = client ? yield* Effect9.promise(() => connect({ client, socket }).status().then((value) => ({ reachable: true, ...value }), (error) => ({
1608
+ reachable: false,
1609
+ error: error instanceof Error ? error.message : String(error)
1610
+ }))) : undefined;
1611
+ const report = {
1612
+ connected: client !== undefined,
1613
+ ...client ? { id: client.id } : {},
1614
+ socket,
1615
+ ...status ? { deadbolt: status } : {}
1616
+ };
1581
1617
  if (json) {
1582
1618
  yield* emitJson(report);
1583
1619
  return;
1584
1620
  }
1585
1621
  if (!client) {
1586
- yield* Console.log("Not connected to Deadbolt. Run `opencode-browser deadbolt connect`.");
1622
+ yield* Console7.log("Not connected to Deadbolt. Run `opencode-browser deadbolt connect`.");
1587
1623
  return;
1588
1624
  }
1589
- yield* Console.log(`Connected as Deadbolt client ${client.id}; socket ${socket}`);
1625
+ yield* Console7.log(`Connected as Deadbolt client ${client.id}; socket ${socket}`);
1590
1626
  if (status && status.reachable)
1591
- yield* Console.log(`Deadbolt ${status.version ?? ""} (protocol ${status.protocol}) is ${status.unlocked ? "unlocked" : "locked: agent sign-ins are paused until you unlock it"}`);
1627
+ yield* Console7.log(`Deadbolt ${status.version ?? ""} (protocol ${status.protocol}) is ${status.unlocked ? "unlocked" : "locked: agent sign-ins are paused until you unlock it"}`);
1592
1628
  else if (status)
1593
- yield* Console.log(`Deadbolt isn't reachable: ${status.error}`);
1594
- })).pipe(Command.withDescription("Show whether Browse is paired with Deadbolt and whether Deadbolt is reachable and unlocked"));
1595
- var deadbolt = Command.make("deadbolt").pipe(Command.withDescription("Let agents sign in with the Deadbolt logins you grant them, without seeing any value"), Command.withSubcommands([deadboltConnect, deadboltDisconnect, deadboltStatus]));
1596
- var journal = Command.make("journal", {
1597
- session: sessionFlag("Show the journal for this Browse session id"),
1598
- limit: Flag.Int("limit").pipe(Flag.optional, Flag.withDescription("Number of most recent entries to show, defaults to 20")),
1599
- json: jsonFlag
1600
- }, Effect3.fn("Cli.journal")(function* ({ session, limit, json }) {
1601
- const sessionId = yield* networkSession(session);
1602
- const entries = yield* Effect3.tryPromise({
1603
- try: () => readJournalEntries({ baseDir: defaultJournalBaseDir(), sessionId, limit: Option2.getOrUndefined(limit) ?? 20 }),
1604
- catch: (cause) => new Error(`read session journal for ${sessionId}`, { cause })
1629
+ yield* Console7.log(`Deadbolt isn't reachable: ${status.error}`);
1630
+ })).pipe(Command6.withDescription("Show whether Browse is paired with Deadbolt and whether Deadbolt is reachable and unlocked"));
1631
+ 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]));
1632
+ registerCliCommand(610, () => secrets);
1633
+ registerCliCommand(620, () => deadbolt);
1634
+
1635
+ // src/cli/session.ts
1636
+ import { Console as Console8, Effect as Effect10, Option as Option8 } from "effect";
1637
+ import { Argument as Argument5, Command as Command7, Flag as Flag8 } from "effect/cli";
1638
+ function describeStop(stopped) {
1639
+ 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)" : "");
1640
+ }
1641
+ var sessionNew = Command7.make("new", {
1642
+ name: Argument5.String("name").pipe(Argument5.optional, Argument5.withDescription("Optional lowercase session id")),
1643
+ 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")),
1644
+ 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"))
1645
+ }, Effect10.fn("Cli.sessionNew")(function* ({ name, readOnly, allowOrigin }) {
1646
+ const relay = yield* Service;
1647
+ yield* ensureCliRelay();
1648
+ const store = yield* Service2;
1649
+ const result = yield* relay.sessionNew(Option8.getOrUndefined(name), {
1650
+ ...readOnly ? { readOnly: true } : {},
1651
+ ...allowOrigin.length > 0 ? { allowedOrigins: allowOrigin } : {}
1605
1652
  });
1653
+ yield* store.write(result.id);
1654
+ yield* Console8.log(result.id);
1655
+ })).pipe(Command7.withDescription("Create a Browse session and make it current"));
1656
+ var sessionList = Command7.make("list", {
1657
+ json: jsonFlag
1658
+ }, Effect10.fn("Cli.sessionList")(function* ({ json }) {
1659
+ const relay = yield* Service;
1660
+ yield* ensureCliRelay();
1661
+ const store = yield* Service2;
1662
+ const sessions = yield* relay.sessions;
1663
+ const current = yield* store.read;
1606
1664
  if (json) {
1607
- yield* emitJson({ session: sessionId, entries });
1665
+ yield* emitJson({ current: current ?? undefined, sessions });
1608
1666
  return;
1609
1667
  }
1610
- if (entries.length === 0) {
1611
- yield* Console.log(`No journal entries for session ${sessionId}`);
1668
+ if (sessions.length === 0) {
1669
+ yield* Console8.log("No sessions");
1612
1670
  return;
1613
1671
  }
1614
- yield* Console.log(`Journal for ${sessionId} (last ${entries.length}):`);
1615
- yield* Effect3.forEach(entries, (entry) => {
1616
- return Console.log(formatJournalEntry(entry));
1672
+ yield* Effect10.forEach(sessions, (item) => {
1673
+ const marker = item.id === current ? "*" : " ";
1674
+ const page = item.pageUrl ?? "no page yet";
1675
+ const keys = item.stateKeys.length ? ` state=${item.stateKeys.join(",")}` : "";
1676
+ const readOnly = item.readOnly ? " read-only" : "";
1677
+ const allowed = item.allowedOrigins ? ` allow=${item.allowedOrigins.join(",")}` : "";
1678
+ return Console8.log(`${marker} ${item.id} ${page}${keys}${readOnly}${allowed}`);
1617
1679
  });
1618
- })).pipe(Command.withDescription("Show what agents did in a Browse session"));
1619
- var doctor = Command.make("doctor", {
1620
- json: jsonFlag
1621
- }, Effect3.fn("Cli.doctor")(function* ({ json }) {
1622
- const update = yield* Effect3.promise(() => updateNotice2());
1623
- const report = yield* createDoctorReport({ packageRoot, update: updateJson2(update) });
1624
- if (json) {
1625
- yield* emitJson(report);
1626
- } else {
1627
- yield* Console.log(formatDoctorReport(report));
1680
+ })).pipe(Command7.withDescription("List Browse sessions"));
1681
+ var sessionCurrent = Command7.make("current", {}, Effect10.fn("Cli.sessionCurrent")(function* () {
1682
+ const store = yield* Service2;
1683
+ const current = yield* store.read;
1684
+ yield* Console8.log(current ?? "none");
1685
+ })).pipe(Command7.withDescription("Print the current default session"));
1686
+ var sessionUse = Command7.make("use", {
1687
+ id: Argument5.String("id")
1688
+ }, Effect10.fn("Cli.sessionUse")(function* ({ id }) {
1689
+ const store = yield* Service2;
1690
+ yield* ensureSessionExists2(id);
1691
+ yield* store.write(id);
1692
+ yield* Console8.log(id);
1693
+ })).pipe(Command7.withDescription("Set the current default session"));
1694
+ var sessionReset = Command7.make("reset", {
1695
+ id: Argument5.String("id").pipe(Argument5.optional),
1696
+ session: sessionFlag("Reset this Browse session id")
1697
+ }, Effect10.fn("Cli.sessionReset")(function* ({ id, session }) {
1698
+ const relay = yield* Service;
1699
+ const sessionId = yield* resolvePositionalOrFlagSessionId(id, session);
1700
+ yield* ensureCliRelay();
1701
+ const reset = yield* relay.sessionReset(sessionId);
1702
+ if (reset.stopped)
1703
+ yield* Console8.error(`Stopped first: ${describeStop(reset.stopped)}`);
1704
+ yield* Console8.log(reset.session.id);
1705
+ })).pipe(Command7.withDescription("Reset a Browse session state and page"));
1706
+ var sessionAdopt = Command7.make("adopt", {
1707
+ session: sessionFlag("Adopt into this Browse session, creating it when it does not exist yet; omit to create a fresh readable id"),
1708
+ targetUrl: Flag8.String("target-url").pipe(Flag8.optional, Flag8.withDescription("Adopt the attached page whose URL contains this text")),
1709
+ targetIndex: Flag8.Int("target-index").pipe(Flag8.optional, Flag8.withDescription("Adopt the attached page at this zero-based target index")),
1710
+ tabId: tabIdFlag("Adopt the user's shared tab with this Chrome tab id (the tab= value in status)")
1711
+ }, Effect10.fn("Cli.sessionAdopt")(function* ({ session, targetUrl, targetIndex, tabId }) {
1712
+ const relay = yield* Service;
1713
+ const explicitSessionId = Option8.getOrUndefined(session) ?? Option8.getOrUndefined(yield* sessionIdConfig);
1714
+ yield* ensureCliRelayAndExtension(explicitSessionId);
1715
+ const targetUrlValue = Option8.getOrUndefined(targetUrl);
1716
+ const targetIndexValue = Option8.getOrUndefined(targetIndex);
1717
+ if (targetIndexValue !== undefined && targetIndexValue < 0) {
1718
+ return yield* Effect10.fail(new Error("Target index must be a non-negative integer"));
1628
1719
  }
1629
- if (report.status === "fail") {
1630
- yield* failExit;
1720
+ const tabIdValue = Option8.getOrUndefined(tabId);
1721
+ if ([targetUrlValue, targetIndexValue, tabIdValue].filter((value) => value !== undefined).length > 1) {
1722
+ return yield* Effect10.fail(new Error("Use only one target selector: --target-url, --target-index, or --tab-id"));
1631
1723
  }
1632
- })).pipe(Command.withDescription("Diagnose the local Browse install and runtime"));
1633
- var skill = Command.make("skill", {}, Effect3.fn("Cli.skill")(function* () {
1634
- const fs = yield* FileSystem3.FileSystem;
1635
- const text = yield* fs.readFileString(path.join(packageRoot, "skills", "browse", "SKILL.md")).pipe(Effect3.mapError((cause) => new Error("read opencode-browser skill", { cause })));
1636
- yield* Console.log(text.trimEnd());
1637
- })).pipe(Command.withDescription("Print the Browse agent skill text"));
1638
- var tabsCleanup = Command.make("cleanup", {
1639
- idleMinutes: Flag.Int("idle-minutes").pipe(Flag.optional, Flag.withDescription("Close agent tabs idle at least this long; 0 closes every eligible agent tab (default: the extension's setting, 10; still 10 when it is set to never)")),
1640
- json: jsonFlag
1641
- }, Effect3.fn("Cli.tabsCleanup")(function* ({ idleMinutes, json }) {
1724
+ const targetSelection = targetUrlValue ? { urlIncludes: targetUrlValue } : targetIndexValue !== undefined ? { index: targetIndexValue } : undefined;
1725
+ const result = yield* relay.sessionAdopt({
1726
+ ...explicitSessionId ? { sessionId: explicitSessionId } : {},
1727
+ createIfMissing: true,
1728
+ ...targetSelection ? { targetSelection } : {},
1729
+ ...tabIdValue === undefined ? {} : { tabId: tabIdValue }
1730
+ });
1731
+ yield* Console8.log(`${result.session.created ? "Created and adopted" : "Adopted"} session '${result.session.id}' default page: ${result.adoptedUrl}`);
1732
+ if (result.session.created) {
1733
+ yield* Console8.error(formatSessionContinuation(result.session.id));
1734
+ }
1735
+ })).pipe(Command7.withDescription("Make an attached tab the session's default page"));
1736
+ var sessionDelete = Command7.make("delete", {
1737
+ id: Argument5.String("id").pipe(Argument5.optional),
1738
+ session: sessionFlag("Delete this Browse session id")
1739
+ }, Effect10.fn("Cli.sessionDelete")(function* ({ id, session }) {
1642
1740
  const relay = yield* Service;
1741
+ const store = yield* Service2;
1742
+ const sessionId = yield* resolvePositionalOrFlagSessionId(id, session);
1643
1743
  yield* ensureCliRelay();
1644
- const result = yield* relay.tabsCleanup(Option2.getOrUndefined(idleMinutes));
1645
- if (json)
1646
- return yield* emitJson(result);
1647
- if (!result.closed.length)
1648
- return yield* Console.log("No idle agent tabs to close.");
1649
- for (const tab of result.closed)
1650
- yield* Console.log(`Closed ${tab.title} (idle ${tab.idleMinutes}m)`);
1651
- })).pipe(Command.withDescription("Close tabs agents opened that nobody has used for a while"));
1652
- var tabs = Command.make("tabs").pipe(Command.withDescription("Manage the tabs agents open"), Command.withSubcommands([tabsCleanup]));
1653
- var printSettings = (settings) => Effect3.forEach(formatSettings(settings), (line) => Console.log(line), { discard: true });
1654
- var settingsGet = Command.make("get", { json: jsonFlag }, Effect3.fn("Cli.settingsGet")(function* ({ json }) {
1744
+ const deleted = yield* relay.sessionDelete(sessionId);
1745
+ if (deleted.stopped)
1746
+ yield* Console8.error(`Stopped first: ${describeStop(deleted.stopped)}`);
1747
+ const current = yield* store.read;
1748
+ if (current === sessionId) {
1749
+ yield* store.clear;
1750
+ }
1751
+ yield* Console8.log(sessionId);
1752
+ })).pipe(Command7.withDescription("Delete a Browse session"));
1753
+ var sessionAllow = Command7.make("allow", {
1754
+ session: sessionFlag(),
1755
+ 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")),
1756
+ clear: Flag8.Boolean("clear").pipe(Flag8.withDefault(false), Flag8.withDescription("Remove the allowlist: the session may load any site again")),
1757
+ json: jsonFlag
1758
+ }, Effect10.fn("Cli.sessionAllow")(function* ({ session, origins, clear, json }) {
1655
1759
  const relay = yield* Service;
1656
1760
  yield* ensureCliRelay();
1657
- const result = yield* relay.settingsGet;
1658
- if (json)
1659
- return yield* emitJson(result);
1660
- yield* printSettings(result.settings);
1661
- yield* Console.log(result.note);
1662
- })).pipe(Command.withDescription("Show each setting's value, the value in effect, and any relay environment variable overriding it"));
1663
- var settingsSet = Command.make("set", {
1664
- assignments: Argument.String("key=value").pipe(Argument.variadic({ min: 1 }), Argument.withDescription(`Settings to change: ${settingKeys.join(", ")}`)),
1761
+ if (!clear && origins.length === 0) {
1762
+ return yield* Effect10.fail(new Error("session allow needs origins, or --clear to remove the allowlist"));
1763
+ }
1764
+ const id = yield* resolvePositionalOrFlagSessionId(Option8.none(), session);
1765
+ const result = yield* relay.sessionAllowlist(id, clear ? [] : origins);
1766
+ if (json) {
1767
+ yield* emitJson(result);
1768
+ return;
1769
+ }
1770
+ yield* Console8.log(result.allowedOrigins ? `${result.id} may load: ${result.allowedOrigins.join(", ")}` : `${result.id} may load any site`);
1771
+ })).pipe(Command7.withDescription("Set the domain allowlist of a session (the current one by default)"));
1772
+ var session = Command7.make("session").pipe(Command7.withDescription("Manage Browse sessions"), Command7.withSubcommands([
1773
+ sessionNew,
1774
+ sessionList,
1775
+ sessionCurrent,
1776
+ sessionUse,
1777
+ sessionReset,
1778
+ sessionAdopt,
1779
+ sessionDelete,
1780
+ sessionAllow
1781
+ ]));
1782
+ var journal = Command7.make("journal", {
1783
+ session: sessionFlag("Show the journal for this Browse session id"),
1784
+ limit: Flag8.Int("limit").pipe(Flag8.optional, Flag8.withDescription("Number of most recent entries to show, defaults to 20")),
1665
1785
  json: jsonFlag
1666
- }, Effect3.fn("Cli.settingsSet")(function* ({ assignments, json }) {
1667
- const changes = {};
1668
- for (const assignment of assignments) {
1669
- const index = assignment.indexOf("=");
1670
- if (index <= 0)
1671
- return yield* Effect3.fail(new Error(`Expected key=value, got "${assignment}". Settings: ${settingKeys.join(", ")}.`));
1672
- changes[assignment.slice(0, index)] = assignment.slice(index + 1);
1786
+ }, Effect10.fn("Cli.journal")(function* ({ session, limit, json }) {
1787
+ const sessionId = yield* networkSession(session);
1788
+ const entries = yield* Effect10.tryPromise({
1789
+ try: () => readJournalEntries({
1790
+ baseDir: defaultJournalBaseDir(),
1791
+ sessionId,
1792
+ limit: Option8.getOrUndefined(limit) ?? 20
1793
+ }),
1794
+ catch: (cause) => new Error(`read session journal for ${sessionId}`, { cause })
1795
+ });
1796
+ if (json) {
1797
+ yield* emitJson({ session: sessionId, entries });
1798
+ return;
1673
1799
  }
1674
- const relay = yield* Service;
1675
- yield* ensureCliRelay();
1676
- const result = yield* relay.settingsSet(changes);
1677
- if (json)
1678
- return yield* emitJson(result);
1679
- yield* Console.log(result.message);
1680
- yield* printSettings(result.settings);
1681
- })).pipe(Command.withDescription("Change settings (validated strictly; the extension's options page updates live)"));
1682
- var settings = Command.make("settings").pipe(Command.withDescription("Read or change the user's settings (the extension's options page). Security switches are environment-only"), Command.withSubcommands([settingsGet, settingsSet]));
1683
- var mcp = Command.make("mcp", {}, Effect3.fn("Cli.mcp")(function* () {
1684
- const { runMcpServer } = yield* Effect3.promise(() => import("./mcp-2smdsn8z.mjs"));
1685
- yield* runMcpServer;
1686
- })).pipe(Command.withDescription("Run the Browse MCP server over stdio"));
1687
- var opencodeBrowser = Command.make("opencode-browser").pipe(Command.withDescription("Control the user's existing browser through the Browse extension"), Command.withSubcommands([serve, relay, execute, session, tabs, settings, status, network, secrets, deadbolt, recording, flightRecorder, journal, doctor, skill, mcp]));
1800
+ if (entries.length === 0) {
1801
+ yield* Console8.log(`No journal entries for session ${sessionId}`);
1802
+ return;
1803
+ }
1804
+ yield* Console8.log(`Journal for ${sessionId} (last ${entries.length}):`);
1805
+ yield* Effect10.forEach(entries, (entry) => {
1806
+ return Console8.log(formatJournalEntry(entry));
1807
+ });
1808
+ })).pipe(Command7.withDescription("Show what agents did in a Browse session"));
1809
+ registerCliCommand(300, () => session);
1810
+ registerCliCommand(800, () => journal);
1811
+
1812
+ // src/cli.ts
1813
+ var packageRoot = path6.dirname(path6.dirname(fileURLToPath(import.meta.url)));
1814
+ var opencodeBrowser = Command8.make("opencode-browser").pipe(Command8.withDescription("Control the user's existing browser through the Browse extension"), Command8.withSubcommands(buildRegisteredCliCommands({ packageRoot })));
1688
1815
  var mainLayer = Layer2.mergeAll(layerFetch, layer).pipe(Layer2.provideMerge(NodeServices.layer));
1689
- function runCli(rawArgs = process.argv.slice(2)) {
1816
+ function runCli(rawArgs = process3.argv.slice(2)) {
1690
1817
  const cliArgs = hoistJsonFlag(rawArgs);
1691
1818
  const subcommand = cliArgs.argv.find((arg) => !arg.startsWith("-"));
1692
1819
  const commandName = subcommand ? `opencode-browser ${subcommand}` : "opencode-browser";
1693
- const reportUsageError = (error) => Effect3.gen(function* () {
1820
+ const reportUsageError = (error) => Effect11.gen(function* () {
1694
1821
  const errors = error._tag === "ShowHelp" ? error.errors : [error];
1695
1822
  if (errors.length === 0)
1696
1823
  return;
@@ -1701,23 +1828,23 @@ function runCli(rawArgs = process.argv.slice(2)) {
1701
1828
  }
1702
1829
  yield* exitWith(ExitCode.usage);
1703
1830
  });
1704
- const reportFailure = (error) => Effect3.gen(function* () {
1831
+ const reportFailure = (error) => Effect11.gen(function* () {
1705
1832
  const message = error instanceof Error ? error.message : String(error);
1706
1833
  const exitCode = exitCodeForError(error);
1707
1834
  if (cliArgs.json) {
1708
1835
  yield* emitJson(failure(commandName, exitCode === ExitCode.host ? "relay_unavailable" : "error", message, { exitCode }));
1709
1836
  yield* stderrLine(message);
1710
1837
  } else {
1711
- yield* Console.error(message);
1838
+ yield* Console9.error(message);
1712
1839
  }
1713
1840
  yield* exitWith(exitCode);
1714
1841
  });
1715
1842
  const silent = () => {};
1716
1843
  const askedForHelp = cliArgs.argv.some((arg) => arg === "--help" || arg === "-h");
1717
1844
  const quietJson = cliArgs.json && !askedForHelp;
1718
- Command.runWith(opencodeBrowser, { version: opencodeBrowserVersion2, renderErrors: !quietJson })(normalizeCliArguments(cliArgs.argv)).pipe(quietJson ? Effect3.provideService(Console.Console, { ...globalThis.console, log: silent }) : (effect) => effect, Effect3.catchIf(CliError.isCliError, reportUsageError), Effect3.catch(reportFailure), Effect3.provide(mainLayer), NodeRuntime.runMain);
1845
+ 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);
1719
1846
  }
1720
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
1847
+ if (process3.argv[1] && import.meta.url === pathToFileURL(process3.argv[1]).href) {
1721
1848
  runCli();
1722
1849
  }
1723
1850
  export {