@opencode/browse 0.0.6-dev.18 → 0.0.6-dev.20
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.
- package/dist/{cli-z00ttket.mjs → cli-anqhds1e.mjs} +771 -643
- package/dist/cli.mjs +8 -8
- package/dist/{relay-helpers-6a2vvzx9.mjs → helpers-n4k4pdag.mjs} +9 -9
- package/dist/{main-jjny7bmm.mjs → main-0nta0a2z.mjs} +200 -191
- package/dist/{main-9y22cffn.mjs → main-20ahaz2q.mjs} +3 -3
- package/dist/{main-w5sta7hq.mjs → main-3rsfghef.mjs} +4 -4
- package/dist/{main-d7rbre73.mjs → main-42qfr69q.mjs} +4 -4
- package/dist/main-arx3pfyn.mjs +4 -0
- package/dist/{main-0egy8naa.mjs → main-hywb9qbv.mjs} +5 -5
- package/dist/{main-ha5bnd77.mjs → main-ma25hhqh.mjs} +120 -118
- package/dist/{main-fkhwazsm.mjs → main-yd1d7qr1.mjs} +1708 -1532
- package/dist/{mcp-qh25n9s0.mjs → mcp-j9e2da4y.mjs} +584 -481
- package/dist/{relay-zs96tkfm.mjs → relay-cvhz2f0a.mjs} +16299 -15709
- package/dist/{runtime-update-8wte78jj.mjs → runtime-update-baranjc5.mjs} +5 -5
- package/dist/{setup-cqjdfcy0.mjs → setup-7shrgdza.mjs} +811 -705
- package/dist/{update-st5c8grg.mjs → update-vkspb2rz.mjs} +2 -2
- package/dist/{version-6g6gtnra.mjs → version-gfpqxbzm.mjs} +1 -1
- package/package.json +1 -2
- package/dist/main-ppz7sv9s.mjs +0 -4
|
@@ -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
|
-
|
|
8
|
+
autoUpdateStatus,
|
|
9
|
+
extensionProtocolVersion,
|
|
10
|
+
extensionProtocolAdvice
|
|
11
|
+
} from "./main-ma25hhqh.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-
|
|
31
|
-
import"./main-
|
|
30
|
+
} from "./main-0nta0a2z.mjs";
|
|
31
|
+
import"./main-20ahaz2q.mjs";
|
|
32
32
|
import {
|
|
33
33
|
opencodeBrowserVersion2,
|
|
34
34
|
opencodeBrowserBuildId2
|
|
35
|
-
} from "./main-
|
|
35
|
+
} from "./main-arx3pfyn.mjs";
|
|
36
36
|
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} from "./main-
|
|
37
|
+
updateJson2,
|
|
38
|
+
formatUpdateNotice2,
|
|
39
|
+
updateNotice2
|
|
40
|
+
} from "./main-3rsfghef.mjs";
|
|
41
41
|
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
settingKeys,
|
|
43
|
+
formatSettings,
|
|
44
|
+
ensureSessionExists,
|
|
45
45
|
readClient,
|
|
46
46
|
socketPath,
|
|
47
47
|
connect,
|
|
48
48
|
pair,
|
|
49
49
|
unpair,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
addSecret,
|
|
51
|
+
listSecrets,
|
|
52
|
+
removeSecret
|
|
53
|
+
} from "./main-yd1d7qr1.mjs";
|
|
53
54
|
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
} from "./main-
|
|
55
|
+
defaultJournalBaseDir,
|
|
56
|
+
readJournalEntries,
|
|
57
|
+
formatJournalEntry
|
|
58
|
+
} from "./main-42qfr69q.mjs";
|
|
58
59
|
|
|
59
60
|
// src/cli.ts
|
|
60
61
|
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
63
|
-
import
|
|
64
|
-
import
|
|
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
|
|
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,
|
|
@@ -600,8 +606,10 @@ function formatTargetSummary(target, options = {}) {
|
|
|
600
606
|
return `${target.type} ${target.id}${tab}${session}${owner}${health} ${target.url || "about:blank"}`;
|
|
601
607
|
}
|
|
602
608
|
|
|
603
|
-
// src/cli.ts
|
|
604
|
-
|
|
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";
|
|
605
613
|
var sessionIdConfig = Config.option(Config.String("OPENCODE_BROWSER_SESSION"));
|
|
606
614
|
var agentSessionIdConfig = Config.option(Config.String("OPENCODE_SESSION_ID"));
|
|
607
615
|
var targetUrlConfig = Config.option(Config.String("OPENCODE_BROWSER_TARGET_URL"));
|
|
@@ -623,7 +631,11 @@ var failExit = Effect3.sync(() => {
|
|
|
623
631
|
var exitWith = (code) => Effect3.sync(() => {
|
|
624
632
|
process.exitCode = code;
|
|
625
633
|
});
|
|
626
|
-
var hostErrorTags = new Set([
|
|
634
|
+
var hostErrorTags = new Set([
|
|
635
|
+
"RelayClient.RelayUnreachable",
|
|
636
|
+
"RelayLifecycle.RelayStartFailed",
|
|
637
|
+
"RelayLifecycle.ExtensionDisconnected"
|
|
638
|
+
]);
|
|
627
639
|
function exitCodeForError(error) {
|
|
628
640
|
if (error instanceof RelayRejected && error.code === "browser-unavailable")
|
|
629
641
|
return ExitCode.host;
|
|
@@ -684,10 +696,9 @@ function decodeCliOperands(operands) {
|
|
|
684
696
|
return decodeURIComponent(operand.slice(encodedCliOperandPrefix.length));
|
|
685
697
|
});
|
|
686
698
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
});
|
|
699
|
+
function formatSessionContinuation(sessionId) {
|
|
700
|
+
return `Session: ${sessionId}. Continue with --session ${sessionId}.`;
|
|
701
|
+
}
|
|
691
702
|
var ensureCliRelay = Effect3.fnUntraced(function* () {
|
|
692
703
|
const relay = yield* Service;
|
|
693
704
|
const readiness = yield* ensureRelay({ relay });
|
|
@@ -724,16 +735,10 @@ var resolvePositionalOrFlagSessionId = Effect3.fnUntraced(function* (id, session
|
|
|
724
735
|
environment: Option2.getOrUndefined(yield* sessionIdConfig)
|
|
725
736
|
}));
|
|
726
737
|
});
|
|
727
|
-
var
|
|
738
|
+
var ensureSessionExists2 = Effect3.fnUntraced(function* (id) {
|
|
728
739
|
const relay = yield* Service;
|
|
729
740
|
yield* ensureCliRelay();
|
|
730
|
-
|
|
731
|
-
const exists = sessions.some((session) => {
|
|
732
|
-
return session.id === id;
|
|
733
|
-
});
|
|
734
|
-
if (!exists) {
|
|
735
|
-
return yield* Effect3.fail(new Error(`Session not found: ${id}`));
|
|
736
|
-
}
|
|
741
|
+
yield* ensureSessionExists(relay, id);
|
|
737
742
|
});
|
|
738
743
|
var recordingTarget = Effect3.fnUntraced(function* (options) {
|
|
739
744
|
const sessionId = Option2.getOrUndefined(options.session);
|
|
@@ -755,6 +760,229 @@ var parseRecordingModeOption = Effect3.fnUntraced(function* (value) {
|
|
|
755
760
|
}
|
|
756
761
|
return yield* Effect3.fail(new Error("Recording mode must be auto, tab-capture, or cdp"));
|
|
757
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-j9e2da4y.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
|
+
});
|
|
758
986
|
function formatExecuteLogs(logs) {
|
|
759
987
|
return [
|
|
760
988
|
"Console logs:",
|
|
@@ -800,73 +1028,59 @@ function executeJsonEnvelope(result) {
|
|
|
800
1028
|
session: result.session
|
|
801
1029
|
};
|
|
802
1030
|
}
|
|
803
|
-
function formatSessionContinuation(sessionId) {
|
|
804
|
-
return `Session: ${sessionId}. Continue with --session ${sessionId}.`;
|
|
805
|
-
}
|
|
806
1031
|
function errorJsonEnvelope(error) {
|
|
807
|
-
const tag =
|
|
1032
|
+
const tag = Predicate3.isObject(error) && Predicate3.hasProperty(error, "_tag") && Predicate3.isString(error._tag) ? error._tag : "Error";
|
|
808
1033
|
const message = error instanceof Error ? error.message : String(error);
|
|
809
1034
|
const errorValue = Object.assign({ message }, { _tag: tag });
|
|
810
|
-
return {
|
|
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
|
+
};
|
|
811
1045
|
}
|
|
812
|
-
var
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const additionalExtensionOrigins = parseAdditionalExtensionOrigins(yield* extensionOriginsConfig);
|
|
816
|
-
yield* Effect3.scoped(Effect3.gen(function* () {
|
|
817
|
-
const relay = yield* startRelay({ port, additionalExtensionOrigins });
|
|
818
|
-
yield* Console.log(`Browse relay listening at ${relay.url}`);
|
|
819
|
-
yield* Console.log(`Browse connects automatically while the browser runs. Run \`${cliCommand("status")}\` to check.`);
|
|
820
|
-
yield* Effect3.never;
|
|
821
|
-
}));
|
|
822
|
-
})).pipe(Command.withDescription("Start the local Browse relay"));
|
|
823
|
-
var relayRestart = Command.make("restart", {
|
|
824
|
-
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"))
|
|
825
|
-
}, Effect3.fn("Cli.relayRestart")(function* ({ force }) {
|
|
826
|
-
const relay = yield* Service;
|
|
827
|
-
yield* Console.error("Restarting the relay leaves browser tabs open but resets in-memory JavaScript state.");
|
|
828
|
-
const readiness = yield* restartRelay({ relay, clientKind: "cli", force });
|
|
829
|
-
yield* Console.log(`${readiness.started ? "Started" : "Reused concurrent replacement"} Browse relay at ${relay.endpoint} (${readiness.version.buildId})`);
|
|
830
|
-
})).pipe(Command.withDescription("Explicitly drain and restart the managed relay using this installation"));
|
|
831
|
-
var relay = Command.make("relay").pipe(Command.withDescription("Manage the local relay lifecycle"), Command.withSubcommands([relayRestart]));
|
|
832
|
-
var execute = Command.make("execute", {
|
|
833
|
-
code: Argument.String("code").pipe(Argument.variadic({ min: 0 })),
|
|
834
|
-
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")),
|
|
835
1049
|
session: sessionFlag("Continue an existing Browse session; defaults to the OpenCode agent's session"),
|
|
836
|
-
rm:
|
|
837
|
-
timeoutMs:
|
|
838
|
-
targetUrl:
|
|
839
|
-
targetIndex:
|
|
840
|
-
json:
|
|
841
|
-
},
|
|
842
|
-
const run =
|
|
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* () {
|
|
843
1057
|
const relay = yield* Service;
|
|
844
|
-
const filePath =
|
|
1058
|
+
const filePath = Option4.getOrUndefined(file);
|
|
845
1059
|
if (code.length > 0 && filePath) {
|
|
846
|
-
return yield*
|
|
1060
|
+
return yield* Effect5.fail(new Error("Use either positional code or --file, not both"));
|
|
847
1061
|
}
|
|
848
1062
|
if (code.length === 0 && !filePath) {
|
|
849
|
-
return yield*
|
|
1063
|
+
return yield* Effect5.fail(new Error("Execute requires positional code or --file <path>"));
|
|
850
1064
|
}
|
|
851
|
-
const timeoutMsValue =
|
|
1065
|
+
const timeoutMsValue = Option4.getOrUndefined(timeoutMs);
|
|
852
1066
|
if (timeoutMsValue !== undefined && timeoutMsValue < 0) {
|
|
853
|
-
return yield*
|
|
1067
|
+
return yield* Effect5.fail(new Error("Timeout must be a non-negative integer"));
|
|
854
1068
|
}
|
|
855
1069
|
const executeCode = filePath ? yield* readExecuteFile(filePath) : code.join(" ");
|
|
856
|
-
const explicitSessionId =
|
|
1070
|
+
const explicitSessionId = Option4.getOrUndefined(session) ?? Option4.getOrUndefined(yield* sessionIdConfig);
|
|
857
1071
|
if (rm && explicitSessionId) {
|
|
858
|
-
return yield*
|
|
1072
|
+
return yield* Effect5.fail(new Error("Use --rm without --session or OPENCODE_BROWSER_SESSION"));
|
|
859
1073
|
}
|
|
860
|
-
const agentWorkSessionId = agentDefaultSessionId(
|
|
1074
|
+
const agentWorkSessionId = agentDefaultSessionId(Option4.getOrUndefined(yield* agentSessionIdConfig));
|
|
861
1075
|
const workSessionId = explicitSessionId ?? (rm ? undefined : agentWorkSessionId);
|
|
862
1076
|
yield* ensureCliRelayAndExtension(workSessionId);
|
|
863
|
-
const targetUrlValue =
|
|
864
|
-
const targetIndexValue =
|
|
1077
|
+
const targetUrlValue = Option4.getOrUndefined(targetUrl) ?? Option4.getOrUndefined(yield* targetUrlConfig);
|
|
1078
|
+
const targetIndexValue = Option4.getOrUndefined(targetIndex) ?? Option4.getOrUndefined(yield* targetIndexConfig);
|
|
865
1079
|
if (targetIndexValue !== undefined && targetIndexValue < 0) {
|
|
866
|
-
return yield*
|
|
1080
|
+
return yield* Effect5.fail(new Error("Target index must be a non-negative integer"));
|
|
867
1081
|
}
|
|
868
1082
|
if (targetUrlValue && targetIndexValue !== undefined) {
|
|
869
|
-
return yield*
|
|
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"));
|
|
870
1084
|
}
|
|
871
1085
|
const result = yield* relay.execute({
|
|
872
1086
|
...workSessionId ? { sessionId: workSessionId } : {},
|
|
@@ -882,33 +1096,33 @@ var execute = Command.make("execute", {
|
|
|
882
1096
|
} : {}
|
|
883
1097
|
});
|
|
884
1098
|
if (!explicitSessionId && !rm) {
|
|
885
|
-
yield*
|
|
1099
|
+
yield* Console3.error(formatSessionContinuation(result.session.id));
|
|
886
1100
|
}
|
|
887
1101
|
return result;
|
|
888
1102
|
});
|
|
889
1103
|
if (json) {
|
|
890
|
-
const outcome = yield*
|
|
891
|
-
const envelope =
|
|
1104
|
+
const outcome = yield* Effect5.result(run);
|
|
1105
|
+
const envelope = Result2.isFailure(outcome) ? errorJsonEnvelope(outcome.failure) : executeJsonEnvelope(outcome.success);
|
|
892
1106
|
yield* emitJson(envelope);
|
|
893
1107
|
if (!envelope.ok) {
|
|
894
1108
|
yield* stderrLine(envelope.error?.message ?? envelope.text);
|
|
895
|
-
yield* exitWith(
|
|
1109
|
+
yield* exitWith(Result2.isFailure(outcome) ? exitCodeForError(outcome.failure) : ExitCode.failure);
|
|
896
1110
|
}
|
|
897
1111
|
return;
|
|
898
1112
|
}
|
|
899
|
-
const outcome = yield*
|
|
900
|
-
if (
|
|
901
|
-
yield*
|
|
1113
|
+
const outcome = yield* Effect5.result(run);
|
|
1114
|
+
if (Result2.isFailure(outcome)) {
|
|
1115
|
+
yield* Console3.error(outcome.failure.message);
|
|
902
1116
|
yield* exitWith(exitCodeForError(outcome.failure));
|
|
903
1117
|
return;
|
|
904
1118
|
}
|
|
905
1119
|
const result = outcome.success;
|
|
906
|
-
const print = result.isError ?
|
|
1120
|
+
const print = result.isError ? Console3.error : Console3.log;
|
|
907
1121
|
yield* print(result.text);
|
|
908
1122
|
if (result.logs.length > 0) {
|
|
909
1123
|
yield* print(formatExecuteLogs(result.logs));
|
|
910
1124
|
}
|
|
911
|
-
yield*
|
|
1125
|
+
yield* Effect5.forEach(result.warnings ?? [], (warning) => print(`Warning: ${warning}`));
|
|
912
1126
|
if (result.diagnostic) {
|
|
913
1127
|
yield* print(`Diagnostic: ${result.diagnostic}`);
|
|
914
1128
|
}
|
|
@@ -919,321 +1133,177 @@ var execute = Command.make("execute", {
|
|
|
919
1133
|
if (result.isError) {
|
|
920
1134
|
yield* failExit;
|
|
921
1135
|
}
|
|
922
|
-
})).pipe(
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
+
}) {
|
|
928
1180
|
const relay = yield* Service;
|
|
929
|
-
yield*
|
|
930
|
-
const
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
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 } : {}
|
|
934
1193
|
});
|
|
935
|
-
yield*
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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(),
|
|
939
1198
|
json: jsonFlag
|
|
940
|
-
},
|
|
1199
|
+
}, Effect6.fn("Cli.networkStatus")(function* ({ session, json }) {
|
|
941
1200
|
const relay = yield* Service;
|
|
1201
|
+
const sessionId = yield* networkSession(session);
|
|
942
1202
|
yield* ensureCliRelay();
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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"));
|
|
953
1217
|
}
|
|
954
|
-
yield*
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
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 } : {}
|
|
961
1224
|
});
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
const current = yield* store.read;
|
|
966
|
-
yield* Console.log(current ?? "none");
|
|
967
|
-
})).pipe(Command.withDescription("Print the current default session"));
|
|
968
|
-
var sessionUse = Command.make("use", {
|
|
969
|
-
id: Argument.String("id")
|
|
970
|
-
}, Effect3.fn("Cli.sessionUse")(function* ({ id }) {
|
|
971
|
-
const store = yield* Service2;
|
|
972
|
-
yield* ensureSessionExists(id);
|
|
973
|
-
yield* store.write(id);
|
|
974
|
-
yield* Console.log(id);
|
|
975
|
-
})).pipe(Command.withDescription("Set the current default session"));
|
|
976
|
-
var sessionReset = Command.make("reset", {
|
|
977
|
-
id: Argument.String("id").pipe(Argument.optional),
|
|
978
|
-
session: sessionFlag("Reset this Browse session id")
|
|
979
|
-
}, 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 }) {
|
|
980
1228
|
const relay = yield* Service;
|
|
981
|
-
const sessionId = yield*
|
|
1229
|
+
const sessionId = yield* networkSession(session);
|
|
982
1230
|
yield* ensureCliRelay();
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
const explicitSessionId = Option2.getOrUndefined(session) ?? Option2.getOrUndefined(yield* sessionIdConfig);
|
|
996
|
-
yield* ensureCliRelayAndExtension(explicitSessionId);
|
|
997
|
-
const targetUrlValue = Option2.getOrUndefined(targetUrl);
|
|
998
|
-
const targetIndexValue = Option2.getOrUndefined(targetIndex);
|
|
999
|
-
if (targetIndexValue !== undefined && targetIndexValue < 0) {
|
|
1000
|
-
return yield* Effect3.fail(new Error("Target index must be a non-negative integer"));
|
|
1001
|
-
}
|
|
1002
|
-
const tabIdValue = Option2.getOrUndefined(tabId);
|
|
1003
|
-
if ([targetUrlValue, targetIndexValue, tabIdValue].filter((value) => value !== undefined).length > 1) {
|
|
1004
|
-
return yield* Effect3.fail(new Error("Use only one target selector: --target-url, --target-index, or --tab-id"));
|
|
1005
|
-
}
|
|
1006
|
-
const targetSelection = targetUrlValue ? { urlIncludes: targetUrlValue } : targetIndexValue !== undefined ? { index: targetIndexValue } : undefined;
|
|
1007
|
-
const result = yield* relay.sessionAdopt({
|
|
1008
|
-
...explicitSessionId ? { sessionId: explicitSessionId } : {},
|
|
1009
|
-
createIfMissing: true,
|
|
1010
|
-
...targetSelection ? { targetSelection } : {},
|
|
1011
|
-
...tabIdValue === undefined ? {} : { tabId: tabIdValue }
|
|
1012
|
-
});
|
|
1013
|
-
yield* Console.log(`${result.session.created ? "Created and adopted" : "Adopted"} session '${result.session.id}' default page: ${result.adoptedUrl}`);
|
|
1014
|
-
if (result.session.created) {
|
|
1015
|
-
yield* Console.error(formatSessionContinuation(result.session.id));
|
|
1016
|
-
}
|
|
1017
|
-
})).pipe(Command.withDescription("Make an attached tab the session's default page"));
|
|
1018
|
-
function describeStop(stopped) {
|
|
1019
|
-
return [
|
|
1020
|
-
stopped.execute ? "a running execute" : undefined,
|
|
1021
|
-
stopped.handoff ? "a pending handoff" : undefined
|
|
1022
|
-
].filter(Boolean).join(" and ") + (stopped.forced ? " (the script did not stop in time and was abandoned with its state)" : "");
|
|
1023
|
-
}
|
|
1024
|
-
var sessionDelete = Command.make("delete", {
|
|
1025
|
-
id: Argument.String("id").pipe(Argument.optional),
|
|
1026
|
-
session: sessionFlag("Delete this Browse session id")
|
|
1027
|
-
}, Effect3.fn("Cli.sessionDelete")(function* ({ id, session }) {
|
|
1028
|
-
const relay = yield* Service;
|
|
1029
|
-
const store = yield* Service2;
|
|
1030
|
-
const sessionId = yield* resolvePositionalOrFlagSessionId(id, session);
|
|
1031
|
-
yield* ensureCliRelay();
|
|
1032
|
-
const deleted = yield* relay.sessionDelete(sessionId);
|
|
1033
|
-
if (deleted.stopped)
|
|
1034
|
-
yield* Console.error(`Stopped first: ${describeStop(deleted.stopped)}`);
|
|
1035
|
-
const current = yield* store.read;
|
|
1036
|
-
if (current === sessionId) {
|
|
1037
|
-
yield* store.clear;
|
|
1038
|
-
}
|
|
1039
|
-
yield* Console.log(sessionId);
|
|
1040
|
-
})).pipe(Command.withDescription("Delete a Browse session"));
|
|
1041
|
-
var sessionAllow = Command.make("allow", {
|
|
1042
|
-
session: sessionFlag(),
|
|
1043
|
-
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")),
|
|
1044
|
-
clear: Flag.Boolean("clear").pipe(Flag.withDefault(false), Flag.withDescription("Remove the allowlist: the session may load any site again")),
|
|
1045
|
-
json: jsonFlag
|
|
1046
|
-
}, Effect3.fn("Cli.sessionAllow")(function* ({ session, origins, clear, json }) {
|
|
1047
|
-
const relay = yield* Service;
|
|
1048
|
-
yield* ensureCliRelay();
|
|
1049
|
-
if (!clear && origins.length === 0) {
|
|
1050
|
-
return yield* Effect3.fail(new Error("session allow needs origins, or --clear to remove the allowlist"));
|
|
1051
|
-
}
|
|
1052
|
-
const id = yield* resolvePositionalOrFlagSessionId(Option2.none(), session);
|
|
1053
|
-
const result = yield* relay.sessionAllowlist(id, clear ? [] : origins);
|
|
1054
|
-
if (json) {
|
|
1055
|
-
yield* emitJson(result);
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
yield* Console.log(result.allowedOrigins ? `${result.id} may load: ${result.allowedOrigins.join(", ")}` : `${result.id} may load any site`);
|
|
1059
|
-
})).pipe(Command.withDescription("Set the domain allowlist of a session (the current one by default)"));
|
|
1060
|
-
var session = Command.make("session").pipe(Command.withDescription("Manage Browse sessions"), Command.withSubcommands([sessionNew, sessionList, sessionCurrent, sessionUse, sessionReset, sessionAdopt, sessionDelete, sessionAllow]));
|
|
1061
|
-
var status = Command.make("status", {
|
|
1062
|
-
json: jsonFlag
|
|
1063
|
-
}, Effect3.fn("Cli.status")(function* ({ json }) {
|
|
1064
|
-
const relay = yield* Service;
|
|
1065
|
-
const store = yield* Service2;
|
|
1066
|
-
const [relayResult, update] = yield* Effect3.all([Effect3.result(relay.version), Effect3.promise(() => updateNotice2())], { concurrency: 2 });
|
|
1067
|
-
const updateLine = update ? formatUpdateNotice2(update) : undefined;
|
|
1068
|
-
if (Result.isFailure(relayResult)) {
|
|
1069
|
-
if (!(relayResult.failure instanceof RelayUnreachable)) {
|
|
1070
|
-
if (json) {
|
|
1071
|
-
yield* emitJson({
|
|
1072
|
-
endpoint: relay.endpoint,
|
|
1073
|
-
relay: { running: false, error: relayResult.failure.message },
|
|
1074
|
-
extension: null,
|
|
1075
|
-
sessions: [],
|
|
1076
|
-
targets: [],
|
|
1077
|
-
update: updateJson2(update)
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
yield* stderrLine(`Relay status failed: ${relayResult.failure.message}`);
|
|
1081
|
-
yield* exitWith(ExitCode.host);
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
const stopped = stoppedRelayStatus(relay.endpoint);
|
|
1085
|
-
if (json) {
|
|
1086
|
-
yield* emitJson({ ...stopped, update: updateJson2(update), autoUpdate: autoUpdateStatus() });
|
|
1087
|
-
yield* stderrLine(`relay is not running at ${relay.endpoint}`);
|
|
1088
|
-
} else {
|
|
1089
|
-
yield* Console.log(`Relay: stopped (${relay.endpoint})`);
|
|
1090
|
-
yield* Console.log(`Run \`${cliCommand("execute")}\` (or any relay-backed command) to start it automatically.`);
|
|
1091
|
-
if (updateLine)
|
|
1092
|
-
yield* Console.log(updateLine);
|
|
1093
|
-
}
|
|
1094
|
-
yield* exitWith(ExitCode.host);
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
const version = relayResult.success;
|
|
1098
|
-
const buildProblem = relayBuildProblem(version);
|
|
1099
|
-
const [extensionStatus, current] = yield* Effect3.all([relay.extensionStatus, store.read]);
|
|
1100
|
-
const collections = statusCollections(extensionStatus);
|
|
1101
|
-
const [sessions, targets] = collections ? [collections.sessions, collections.targets] : yield* Effect3.all([relay.sessions, relay.targets]);
|
|
1102
|
-
if (json) {
|
|
1103
|
-
yield* emitJson({
|
|
1104
|
-
endpoint: relay.endpoint,
|
|
1105
|
-
relay: { running: true, version: version.version, buildId: version.buildId ?? null, stale: buildProblem !== undefined },
|
|
1106
|
-
extension: extensionStatus,
|
|
1107
|
-
currentSession: current ?? null,
|
|
1108
|
-
sessions,
|
|
1109
|
-
targets,
|
|
1110
|
-
update: updateJson2(update),
|
|
1111
|
-
autoUpdate: autoUpdateStatus()
|
|
1112
|
-
});
|
|
1113
|
-
if (buildProblem) {
|
|
1114
|
-
yield* stderrLine(buildProblem);
|
|
1115
|
-
yield* failExit;
|
|
1116
|
-
}
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
if (updateLine)
|
|
1120
|
-
yield* Console.log(updateLine);
|
|
1121
|
-
yield* Console.log(`Relay: ${relay.endpoint} (${version.version})`);
|
|
1122
|
-
if (buildProblem) {
|
|
1123
|
-
yield* Console.log(`Warning: ${buildProblem}`);
|
|
1124
|
-
}
|
|
1125
|
-
yield* Console.log(`Extension: ${extensionStatus.connected ? "connected" : "disconnected"}${extensionStatus.version ? ` (${extensionStatus.version})` : ""}`);
|
|
1126
|
-
if ((extensionStatus.rejectedConnections ?? 0) > 0) {
|
|
1127
|
-
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.`);
|
|
1128
|
-
}
|
|
1129
|
-
if (extensionStatus.nextStep)
|
|
1130
|
-
yield* Console.log(extensionStatus.nextStep);
|
|
1131
|
-
else if ((extensionStatus.pairingFailures ?? 0) > 0 && !extensionStatus.connected) {
|
|
1132
|
-
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.`);
|
|
1133
|
-
}
|
|
1134
|
-
if ((extensionStatus.processFaults?.suppressed ?? 0) > 0) {
|
|
1135
|
-
const faults = extensionStatus.processFaults;
|
|
1136
|
-
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.`);
|
|
1137
|
-
}
|
|
1138
|
-
if (extensionStatus.protocolVersion !== undefined && extensionStatus.protocolVersion !== null) {
|
|
1139
|
-
const compatibility = extensionStatus.protocolCompatible === false ? "incompatible" : "compatible";
|
|
1140
|
-
const legacy = extensionStatus.protocolLegacy === true ? ", inferred from legacy hello" : "";
|
|
1141
|
-
yield* Console.log(`Extension protocol: ${extensionStatus.protocolVersion} (${compatibility}${legacy})`);
|
|
1142
|
-
if (extensionStatus.protocolCompatible === false && !extensionStatus.nextStep)
|
|
1143
|
-
yield* Console.log(extensionProtocolAdvice(extensionStatus.protocolVersion, { install: installCommand() }));
|
|
1144
|
-
}
|
|
1145
|
-
yield* Console.log(`Active targets: ${extensionStatus.activeTargets}`);
|
|
1146
|
-
if (extensionStatus.childTargets !== undefined) {
|
|
1147
|
-
yield* Console.log(`Child targets: ${extensionStatus.childTargets}`);
|
|
1148
|
-
}
|
|
1149
|
-
if (extensionStatus.cdpClients !== undefined) {
|
|
1150
|
-
yield* Console.log(`CDP clients: ${extensionStatus.cdpClients}`);
|
|
1151
|
-
}
|
|
1152
|
-
yield* Console.log(`Current session: ${current ?? "none"}`);
|
|
1153
|
-
if (sessions.length === 0) {
|
|
1154
|
-
yield* Console.log("Sessions: none");
|
|
1155
|
-
} else {
|
|
1156
|
-
yield* Console.log("Sessions:");
|
|
1157
|
-
const activeSessions = sessions.filter((item) => item.id === current || item.connected || item.pageUrl !== null || item.stateKeys.length > 0 || item.readOnly);
|
|
1158
|
-
const idleSessions = sessions.filter((item) => !activeSessions.includes(item));
|
|
1159
|
-
const shownSessions = activeSessions.length > 0 ? activeSessions : idleSessions.slice(0, 5);
|
|
1160
|
-
const omittedIdleCount = activeSessions.length > 0 ? idleSessions.length : Math.max(0, idleSessions.length - shownSessions.length);
|
|
1161
|
-
yield* Effect3.forEach(shownSessions, (item) => {
|
|
1162
|
-
const marker = item.id === current ? "*" : " ";
|
|
1163
|
-
return Console.log(`${marker} ${item.id} ${item.pageUrl ?? "no page yet"}`);
|
|
1164
|
-
});
|
|
1165
|
-
if (omittedIdleCount > 0) {
|
|
1166
|
-
yield* Console.log(` (${omittedIdleCount} idle session(s) with no page omitted; run ${cliCommand("session list")} to view all)`);
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
if (targets.length === 0) {
|
|
1170
|
-
yield* Console.log("Targets: none");
|
|
1171
|
-
} else {
|
|
1172
|
-
yield* Console.log("Targets:");
|
|
1173
|
-
yield* Effect3.forEach(targets, (target, index) => {
|
|
1174
|
-
return Console.log(`- [${index}] ${formatTargetSummary(target, { includeSession: true })}`);
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
if (buildProblem) {
|
|
1178
|
-
yield* failExit;
|
|
1179
|
-
}
|
|
1180
|
-
})).pipe(Command.withDescription("Show relay, extension, and target status"));
|
|
1181
|
-
var recordingStart = Command.make("start", {
|
|
1182
|
-
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")),
|
|
1183
1243
|
session: sessionFlag("Record the page for this Browse or CDP session id"),
|
|
1184
1244
|
tabId: tabIdFlag("Record this attached Chrome tab id"),
|
|
1185
|
-
mode:
|
|
1186
|
-
audio:
|
|
1187
|
-
frameRate:
|
|
1188
|
-
maxDurationMs:
|
|
1189
|
-
cursor:
|
|
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)")),
|
|
1190
1250
|
json: jsonFlag
|
|
1191
|
-
},
|
|
1192
|
-
|
|
1193
|
-
|
|
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"));
|
|
1194
1264
|
}
|
|
1195
1265
|
const relay = yield* Service;
|
|
1196
1266
|
yield* ensureCliRelayAndExtension();
|
|
1197
1267
|
const target = yield* recordingTarget({ session, tabId });
|
|
1198
|
-
const modeValue = yield* parseRecordingModeOption(
|
|
1199
|
-
const resolvedOutputPath =
|
|
1268
|
+
const modeValue = yield* parseRecordingModeOption(Option6.getOrUndefined(mode));
|
|
1269
|
+
const resolvedOutputPath = path4.resolve(outputPath);
|
|
1200
1270
|
const result = yield* relay.recordingStart({
|
|
1201
1271
|
...target,
|
|
1202
1272
|
outputPath: resolvedOutputPath,
|
|
1203
1273
|
...modeValue === undefined ? {} : { mode: modeValue },
|
|
1204
1274
|
audio,
|
|
1205
|
-
...
|
|
1206
|
-
...
|
|
1275
|
+
...Option6.isSome(frameRate) ? { frameRate: frameRate.value } : {},
|
|
1276
|
+
...Option6.isSome(maxDurationMs) ? { maxDurationMs: maxDurationMs.value } : {},
|
|
1207
1277
|
...cursor ? { cursor } : {}
|
|
1208
1278
|
});
|
|
1209
1279
|
if (!result.success) {
|
|
1210
|
-
return yield*
|
|
1280
|
+
return yield* Effect7.fail(new Error(result.error ?? "Failed to start recording"));
|
|
1211
1281
|
}
|
|
1212
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"}`);
|
|
1213
|
-
})).pipe(
|
|
1214
|
-
var recordingStop =
|
|
1283
|
+
})).pipe(Command4.withDescription("Start recording an attached tab"));
|
|
1284
|
+
var recordingStop = Command4.make("stop", {
|
|
1215
1285
|
session: sessionFlag("Stop recording for this CDP session id"),
|
|
1216
1286
|
tabId: tabIdFlag("Stop recording for this Chrome tab id"),
|
|
1217
1287
|
json: jsonFlag
|
|
1218
|
-
},
|
|
1288
|
+
}, Effect7.fn("Cli.recordingStop")(function* ({ session, tabId, json }) {
|
|
1219
1289
|
const relay = yield* Service;
|
|
1220
1290
|
yield* ensureCliRelay();
|
|
1221
1291
|
const target = yield* recordingTarget({ session, tabId });
|
|
1222
1292
|
const result = yield* relay.recordingStop(target);
|
|
1223
1293
|
if (!result.success) {
|
|
1224
|
-
return yield*
|
|
1294
|
+
return yield* Effect7.fail(new Error(result.error ?? "Failed to stop recording"));
|
|
1225
1295
|
}
|
|
1226
1296
|
if (json)
|
|
1227
1297
|
return yield* emitJson(result);
|
|
1228
1298
|
const frames = result.frameCount === undefined ? "" : `, frames=${result.frameCount}`;
|
|
1229
|
-
yield*
|
|
1230
|
-
yield*
|
|
1231
|
-
})).pipe(
|
|
1232
|
-
var recordingStatus =
|
|
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", {
|
|
1233
1303
|
session: sessionFlag("Check recording for this CDP session id"),
|
|
1234
1304
|
tabId: tabIdFlag("Check recording for this Chrome tab id"),
|
|
1235
1305
|
json: jsonFlag
|
|
1236
|
-
},
|
|
1306
|
+
}, Effect7.fn("Cli.recordingStatus")(function* ({ session, tabId, json }) {
|
|
1237
1307
|
const relay = yield* Service;
|
|
1238
1308
|
yield* ensureCliRelay();
|
|
1239
1309
|
const target = yield* recordingTarget({ session, tabId });
|
|
@@ -1243,175 +1313,129 @@ var recordingStatus = Command.make("status", {
|
|
|
1243
1313
|
return;
|
|
1244
1314
|
}
|
|
1245
1315
|
if (!result.isRecording) {
|
|
1246
|
-
yield*
|
|
1316
|
+
yield* Console5.log("Recording: inactive");
|
|
1247
1317
|
return;
|
|
1248
1318
|
}
|
|
1249
1319
|
const frames = result.frameCount === undefined ? "" : ` frameCount=${result.frameCount}`;
|
|
1250
|
-
yield*
|
|
1251
|
-
yield*
|
|
1252
|
-
})).pipe(
|
|
1253
|
-
var recordingCancel =
|
|
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", {
|
|
1254
1324
|
session: sessionFlag("Cancel recording for this CDP session id"),
|
|
1255
1325
|
tabId: tabIdFlag("Cancel recording for this Chrome tab id")
|
|
1256
|
-
},
|
|
1326
|
+
}, Effect7.fn("Cli.recordingCancel")(function* ({ session, tabId }) {
|
|
1257
1327
|
const relay = yield* Service;
|
|
1258
1328
|
yield* ensureCliRelay();
|
|
1259
1329
|
const target = yield* recordingTarget({ session, tabId });
|
|
1260
1330
|
const result = yield* relay.recordingCancel(target);
|
|
1261
1331
|
if (!result.success) {
|
|
1262
|
-
return yield*
|
|
1332
|
+
return yield* Effect7.fail(new Error(result.error ?? "Failed to cancel recording"));
|
|
1263
1333
|
}
|
|
1264
|
-
yield*
|
|
1265
|
-
})).pipe(
|
|
1266
|
-
var recording =
|
|
1267
|
-
var flightRecorderStart =
|
|
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", {
|
|
1268
1338
|
session: sessionFlag("Buffer the tab owned by this session"),
|
|
1269
1339
|
tabId: tabIdFlag("Buffer this attached Chrome tab id"),
|
|
1270
|
-
retentionMs:
|
|
1271
|
-
frameRate:
|
|
1272
|
-
cursor:
|
|
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)")),
|
|
1273
1343
|
json: jsonFlag
|
|
1274
|
-
},
|
|
1344
|
+
}, Effect7.fn("Cli.flightRecorderStart")(function* ({ session, tabId, retentionMs, frameRate, cursor, json }) {
|
|
1275
1345
|
const relay = yield* Service;
|
|
1276
1346
|
yield* ensureCliRelayAndExtension();
|
|
1277
1347
|
const target = yield* recordingTarget({ session, tabId });
|
|
1278
1348
|
const result = yield* relay.flightRecorderStart({
|
|
1279
1349
|
...target,
|
|
1280
|
-
...
|
|
1281
|
-
...
|
|
1350
|
+
...Option6.isSome(retentionMs) ? { retentionMs: retentionMs.value } : {},
|
|
1351
|
+
...Option6.isSome(frameRate) ? { frameRate: frameRate.value } : {},
|
|
1282
1352
|
...cursor ? { cursor } : {}
|
|
1283
1353
|
});
|
|
1284
1354
|
yield* printOr(json, result, `Flight recorder buffering tab ${result.tabId}; retention=${result.retentionMs}ms`);
|
|
1285
|
-
})).pipe(
|
|
1286
|
-
var flightRecorderStatus =
|
|
1355
|
+
})).pipe(Command4.withDescription("Start a rolling in-memory video buffer"));
|
|
1356
|
+
var flightRecorderStatus = Command4.make("status", {
|
|
1287
1357
|
session: sessionFlag(),
|
|
1288
1358
|
tabId: tabIdFlag(),
|
|
1289
1359
|
json: jsonFlag
|
|
1290
|
-
},
|
|
1360
|
+
}, Effect7.fn("Cli.flightRecorderStatus")(function* ({ session, tabId, json }) {
|
|
1291
1361
|
const relay = yield* Service;
|
|
1292
1362
|
yield* ensureCliRelay();
|
|
1293
1363
|
const result = yield* relay.flightRecorderStatus(yield* recordingTarget({ session, tabId }));
|
|
1294
1364
|
if (json)
|
|
1295
1365
|
return yield* emitJson(result);
|
|
1296
1366
|
if (!result.active)
|
|
1297
|
-
return yield*
|
|
1298
|
-
yield*
|
|
1299
|
-
})).pipe(
|
|
1300
|
-
var flightRecorderSaveLast =
|
|
1301
|
-
outputPath:
|
|
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")),
|
|
1302
1372
|
session: sessionFlag(),
|
|
1303
1373
|
tabId: tabIdFlag(),
|
|
1304
|
-
durationMs:
|
|
1374
|
+
durationMs: Flag5.Int("duration-ms").pipe(Flag5.optional, Flag5.withDescription("How much recent history to save; defaults to 30 seconds")),
|
|
1305
1375
|
json: jsonFlag
|
|
1306
|
-
},
|
|
1376
|
+
}, Effect7.fn("Cli.flightRecorderSaveLast")(function* ({ outputPath, session, tabId, durationMs, json }) {
|
|
1307
1377
|
const relay = yield* Service;
|
|
1308
1378
|
yield* ensureCliRelay();
|
|
1309
1379
|
const result = yield* relay.flightRecorderSaveLast({
|
|
1310
1380
|
...yield* recordingTarget({ session, tabId }),
|
|
1311
|
-
outputPath:
|
|
1312
|
-
...
|
|
1381
|
+
outputPath: path4.resolve(outputPath),
|
|
1382
|
+
...Option6.isSome(durationMs) ? { durationMs: durationMs.value } : {}
|
|
1313
1383
|
});
|
|
1314
1384
|
yield* printOr(json, result, `Saved ${result.durationMs}ms flight recorder clip (${result.frameCount} frames) to ${result.path}`);
|
|
1315
|
-
})).pipe(
|
|
1316
|
-
var flightRecorderCancel =
|
|
1385
|
+
})).pipe(Command4.withDescription("Save the most recent buffered video without stopping the recorder"));
|
|
1386
|
+
var flightRecorderCancel = Command4.make("cancel", {
|
|
1317
1387
|
session: sessionFlag(),
|
|
1318
1388
|
tabId: tabIdFlag()
|
|
1319
|
-
},
|
|
1389
|
+
}, Effect7.fn("Cli.flightRecorderCancel")(function* ({ session, tabId }) {
|
|
1320
1390
|
const relay = yield* Service;
|
|
1321
1391
|
yield* ensureCliRelay();
|
|
1322
1392
|
const result = yield* relay.flightRecorderCancel(yield* recordingTarget({ session, tabId }));
|
|
1323
|
-
yield*
|
|
1324
|
-
})).pipe(
|
|
1325
|
-
var flightRecorder =
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
maxTotalBodyBytes: Flag.Int("max-total-body-bytes").pipe(Flag.optional, Flag.withDescription("Maximum captured body bytes for the whole capture, defaults to 25000000")),
|
|
1352
|
-
maxEntries: Flag.Int("max-entries").pipe(Flag.optional, Flag.withDescription("Maximum request entries, defaults to 1000")),
|
|
1353
|
-
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)")),
|
|
1354
|
-
json: jsonFlag
|
|
1355
|
-
}, Effect3.fn("Cli.networkStart")(function* ({ session, urlFilter, resourceTypes, content, maxBodyBytes, maxTotalBodyBytes, maxEntries, maxDuration, json }) {
|
|
1356
|
-
const relay = yield* Service;
|
|
1357
|
-
yield* ensureCliRelayAndExtension();
|
|
1358
|
-
const sessionId = yield* networkSession(session);
|
|
1359
|
-
const contentValue = yield* parseNetworkContent(content);
|
|
1360
|
-
const result = yield* relay.networkStart({
|
|
1361
|
-
sessionId,
|
|
1362
|
-
...Option2.isSome(urlFilter) ? { urlFilter: urlFilter.value } : {},
|
|
1363
|
-
...resourceTypes.length === 0 ? {} : { resourceTypes },
|
|
1364
|
-
...contentValue === undefined ? {} : { content: contentValue },
|
|
1365
|
-
...Option2.isSome(maxBodyBytes) ? { maxBodyBytes: maxBodyBytes.value } : {},
|
|
1366
|
-
...Option2.isSome(maxTotalBodyBytes) ? { maxTotalBodyBytes: maxTotalBodyBytes.value } : {},
|
|
1367
|
-
...Option2.isSome(maxEntries) ? { maxEntries: maxEntries.value } : {},
|
|
1368
|
-
...Option2.isSome(maxDuration) ? { maxDurationMs: maxDuration.value } : {}
|
|
1369
|
-
});
|
|
1370
|
-
yield* printOr(json, result, formatNetworkStatus(result));
|
|
1371
|
-
})).pipe(Command.withDescription("Start session-scoped network capture"));
|
|
1372
|
-
var networkStatus = Command.make("status", {
|
|
1373
|
-
session: sessionFlag(),
|
|
1374
|
-
json: jsonFlag
|
|
1375
|
-
}, Effect3.fn("Cli.networkStatus")(function* ({ session, json }) {
|
|
1376
|
-
const relay = yield* Service;
|
|
1377
|
-
const sessionId = yield* networkSession(session);
|
|
1378
|
-
yield* ensureCliRelay();
|
|
1379
|
-
const result = yield* relay.networkStatus({ sessionId });
|
|
1380
|
-
yield* printOr(json, result, formatNetworkStatus(result));
|
|
1381
|
-
})).pipe(Command.withDescription("Show session-scoped network capture status"));
|
|
1382
|
-
var networkStop = Command.make("stop", {
|
|
1383
|
-
session: sessionFlag(),
|
|
1384
|
-
output: Flag.String("output").pipe(Flag.optional, Flag.withAlias("o"), Flag.withDescription("Write a credential-redacted HAR artifact to this path")),
|
|
1385
|
-
secrets: Flag.String("secrets").pipe(Flag.optional, Flag.withDescription("Store captured credentials under this reusable profile name")),
|
|
1386
|
-
json: jsonFlag
|
|
1387
|
-
}, Effect3.fn("Cli.networkStop")(function* ({ session, output, secrets, json }) {
|
|
1388
|
-
const relay = yield* Service;
|
|
1389
|
-
const outputValue = Option2.getOrUndefined(output);
|
|
1390
|
-
const secretsValue = Option2.getOrUndefined(secrets);
|
|
1391
|
-
if (!outputValue && !secretsValue) {
|
|
1392
|
-
return yield* Effect3.fail(new Error("network stop requires --output, --secrets, or both"));
|
|
1393
|
-
}
|
|
1394
|
-
const sessionId = yield* networkSession(session);
|
|
1395
|
-
yield* ensureCliRelay();
|
|
1396
|
-
const result = yield* relay.networkStop({
|
|
1397
|
-
sessionId,
|
|
1398
|
-
...outputValue ? { outputPath: path.resolve(outputValue) } : {},
|
|
1399
|
-
...secretsValue ? { secrets: secretsValue } : {}
|
|
1400
|
-
});
|
|
1401
|
-
yield* printOr(json, result, formatNetworkResult(result));
|
|
1402
|
-
})).pipe(Command.withDescription("Stop capture and write a redacted artifact or reusable secret profile"));
|
|
1403
|
-
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-cvhz2f0a.mjs"), import("./helpers-n4k4pdag.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 }) {
|
|
1404
1421
|
const relay = yield* Service;
|
|
1405
|
-
|
|
1406
|
-
yield*
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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"),
|
|
1413
1437
|
json: jsonFlag
|
|
1414
|
-
},
|
|
1438
|
+
}, Effect9.fn("Cli.secretsStatus")(function* ({ name, json }) {
|
|
1415
1439
|
const relay = yield* Service;
|
|
1416
1440
|
yield* ensureCliRelay();
|
|
1417
1441
|
const result = yield* relay.authStatus({ name });
|
|
@@ -1419,60 +1443,60 @@ var secretsStatus = Command.make("status", {
|
|
|
1419
1443
|
yield* emitJson(result);
|
|
1420
1444
|
return;
|
|
1421
1445
|
}
|
|
1422
|
-
yield*
|
|
1423
|
-
yield*
|
|
1424
|
-
})).pipe(
|
|
1425
|
-
var secretsRefresh =
|
|
1426
|
-
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"),
|
|
1427
1451
|
session: sessionFlag(),
|
|
1428
|
-
urlFilter:
|
|
1429
|
-
timeoutMs:
|
|
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")),
|
|
1430
1454
|
json: jsonFlag
|
|
1431
|
-
},
|
|
1455
|
+
}, Effect9.fn("Cli.secretsRefresh")(function* ({ name, session, urlFilter, timeoutMs, json }) {
|
|
1432
1456
|
const relay = yield* Service;
|
|
1433
1457
|
yield* ensureCliRelayAndExtension();
|
|
1434
1458
|
const sessionId = yield* networkSession(session);
|
|
1435
1459
|
const result = yield* relay.authRefresh({
|
|
1436
1460
|
sessionId,
|
|
1437
1461
|
name,
|
|
1438
|
-
...
|
|
1439
|
-
...
|
|
1462
|
+
...Option7.isSome(urlFilter) ? { urlFilter: urlFilter.value } : {},
|
|
1463
|
+
...Option7.isSome(timeoutMs) ? { timeoutMs: timeoutMs.value } : {}
|
|
1440
1464
|
});
|
|
1441
1465
|
yield* printOr(json, result, `Secrets profile ${name} refreshed: observed=${result.observedSecretRefs.length} changed=${result.updatedSecretRefs.length}`);
|
|
1442
|
-
})).pipe(
|
|
1443
|
-
var secretsRun =
|
|
1444
|
-
name:
|
|
1445
|
-
command:
|
|
1446
|
-
cwd:
|
|
1447
|
-
timeoutMs:
|
|
1448
|
-
},
|
|
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 }) {
|
|
1449
1473
|
const relay = yield* Service;
|
|
1450
1474
|
yield* ensureCliRelay();
|
|
1451
1475
|
const [executable, ...args] = decodeCliOperands(command);
|
|
1452
1476
|
if (!executable)
|
|
1453
|
-
return yield*
|
|
1477
|
+
return yield* Effect9.fail(new Error("secrets run requires a command after --"));
|
|
1454
1478
|
const result = yield* relay.authRun({
|
|
1455
1479
|
name,
|
|
1456
1480
|
command: executable,
|
|
1457
1481
|
args,
|
|
1458
|
-
cwd:
|
|
1459
|
-
...
|
|
1482
|
+
cwd: path5.resolve(Option7.getOrElse(cwd, () => process2.cwd())),
|
|
1483
|
+
...Option7.isSome(timeoutMs) ? { timeoutMs: timeoutMs.value } : {}
|
|
1460
1484
|
});
|
|
1461
|
-
yield*
|
|
1485
|
+
yield* Effect9.sync(() => {
|
|
1462
1486
|
if (result.stdout)
|
|
1463
|
-
|
|
1487
|
+
process2.stdout.write(result.stdout);
|
|
1464
1488
|
if (result.stderr)
|
|
1465
|
-
|
|
1489
|
+
process2.stderr.write(result.stderr);
|
|
1466
1490
|
if (result.stdoutTruncated || result.stderrTruncated)
|
|
1467
|
-
|
|
1491
|
+
process2.stderr.write(`
|
|
1468
1492
|
Browse truncated child output.
|
|
1469
1493
|
`);
|
|
1470
1494
|
if (result.exitCode !== 0)
|
|
1471
|
-
|
|
1495
|
+
process2.exitCode = result.exitCode;
|
|
1472
1496
|
});
|
|
1473
|
-
})).pipe(
|
|
1497
|
+
})).pipe(Command6.withDescription("Run a command with profile values injected as BC_SECRET_* environment variables"));
|
|
1474
1498
|
function readSecretValue(prompt) {
|
|
1475
|
-
const input =
|
|
1499
|
+
const input = process2.stdin;
|
|
1476
1500
|
if (!input.isTTY) {
|
|
1477
1501
|
return new Promise((resolve, reject) => {
|
|
1478
1502
|
const chunks = [];
|
|
@@ -1482,13 +1506,13 @@ function readSecretValue(prompt) {
|
|
|
1482
1506
|
});
|
|
1483
1507
|
}
|
|
1484
1508
|
return new Promise((resolve, reject) => {
|
|
1485
|
-
|
|
1509
|
+
process2.stderr.write(prompt);
|
|
1486
1510
|
let value = "";
|
|
1487
1511
|
const finish = (result) => {
|
|
1488
1512
|
input.setRawMode(false);
|
|
1489
1513
|
input.pause();
|
|
1490
1514
|
input.off("data", onData);
|
|
1491
|
-
|
|
1515
|
+
process2.stderr.write(`
|
|
1492
1516
|
`);
|
|
1493
1517
|
if (result instanceof Error)
|
|
1494
1518
|
reject(result);
|
|
@@ -1513,44 +1537,49 @@ function readSecretValue(prompt) {
|
|
|
1513
1537
|
input.on("data", onData);
|
|
1514
1538
|
});
|
|
1515
1539
|
}
|
|
1516
|
-
var secretsAdd =
|
|
1517
|
-
name:
|
|
1518
|
-
origin:
|
|
1519
|
-
totp:
|
|
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")),
|
|
1520
1544
|
json: jsonFlag
|
|
1521
|
-
},
|
|
1522
|
-
const value = yield*
|
|
1545
|
+
}, Effect9.fn("Cli.secretsAdd")(function* ({ name, origin, totp, json }) {
|
|
1546
|
+
const value = yield* Effect9.tryPromise({
|
|
1523
1547
|
try: () => readSecretValue(totp ? `TOTP seed for ${name} (hidden): ` : `Value for ${name} (hidden): `),
|
|
1524
1548
|
catch: (cause) => cause instanceof Error ? cause : new Error("Could not read the secret value")
|
|
1525
1549
|
});
|
|
1526
|
-
const result = yield* addSecret({
|
|
1550
|
+
const result = yield* addSecret({
|
|
1551
|
+
name,
|
|
1552
|
+
value,
|
|
1553
|
+
allowedOrigins: origin,
|
|
1554
|
+
kind: totp ? "totp" : "password"
|
|
1555
|
+
});
|
|
1527
1556
|
if (json) {
|
|
1528
1557
|
yield* emitJson(result);
|
|
1529
1558
|
return;
|
|
1530
1559
|
}
|
|
1531
|
-
yield*
|
|
1560
|
+
yield* Console7.log(`Saved ${result.kind} secret ${result.name}; it can be typed on ${result.allowedOrigins.join(", ")}`);
|
|
1532
1561
|
if (totp)
|
|
1533
|
-
yield*
|
|
1534
|
-
})).pipe(
|
|
1535
|
-
var secretsList =
|
|
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 }) {
|
|
1536
1565
|
const secrets = yield* listSecrets();
|
|
1537
1566
|
if (json) {
|
|
1538
1567
|
yield* emitJson({ secrets });
|
|
1539
1568
|
return;
|
|
1540
1569
|
}
|
|
1541
1570
|
if (secrets.length === 0) {
|
|
1542
|
-
yield*
|
|
1571
|
+
yield* Console7.log("No site secrets");
|
|
1543
1572
|
return;
|
|
1544
1573
|
}
|
|
1545
|
-
yield*
|
|
1546
|
-
})).pipe(
|
|
1547
|
-
var secretsRemove =
|
|
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 }) {
|
|
1548
1577
|
const removed = yield* removeSecret(name);
|
|
1549
|
-
yield*
|
|
1550
|
-
})).pipe(
|
|
1551
|
-
var secrets =
|
|
1552
|
-
var deadboltConnect =
|
|
1553
|
-
const paired = yield*
|
|
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({
|
|
1554
1583
|
try: () => pair(),
|
|
1555
1584
|
catch: (cause) => cause instanceof Error ? cause : new Error("Could not connect to Deadbolt")
|
|
1556
1585
|
});
|
|
@@ -1558,138 +1587,237 @@ var deadboltConnect = Command.make("connect", { json: jsonFlag }, Effect3.fn("Cl
|
|
|
1558
1587
|
yield* emitJson({ connected: true, id: paired.id });
|
|
1559
1588
|
return;
|
|
1560
1589
|
}
|
|
1561
|
-
yield*
|
|
1562
|
-
yield*
|
|
1563
|
-
})).pipe(
|
|
1564
|
-
var deadboltDisconnect =
|
|
1565
|
-
const result = yield*
|
|
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({
|
|
1566
1595
|
try: () => unpair(),
|
|
1567
1596
|
catch: (cause) => cause instanceof Error ? cause : new Error("Could not disconnect from Deadbolt")
|
|
1568
1597
|
});
|
|
1569
1598
|
if (!result.removed) {
|
|
1570
|
-
yield*
|
|
1599
|
+
yield* Console7.log("Browse isn't connected to Deadbolt");
|
|
1571
1600
|
return;
|
|
1572
1601
|
}
|
|
1573
|
-
yield*
|
|
1574
|
-
})).pipe(
|
|
1575
|
-
var deadboltStatus =
|
|
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 }) {
|
|
1576
1605
|
const client = readClient();
|
|
1577
1606
|
const socket = socketPath();
|
|
1578
|
-
const status = client ? yield*
|
|
1579
|
-
|
|
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
|
+
};
|
|
1580
1617
|
if (json) {
|
|
1581
1618
|
yield* emitJson(report);
|
|
1582
1619
|
return;
|
|
1583
1620
|
}
|
|
1584
1621
|
if (!client) {
|
|
1585
|
-
yield*
|
|
1622
|
+
yield* Console7.log("Not connected to Deadbolt. Run `opencode-browser deadbolt connect`.");
|
|
1586
1623
|
return;
|
|
1587
1624
|
}
|
|
1588
|
-
yield*
|
|
1625
|
+
yield* Console7.log(`Connected as Deadbolt client ${client.id}; socket ${socket}`);
|
|
1589
1626
|
if (status && status.reachable)
|
|
1590
|
-
yield*
|
|
1627
|
+
yield* Console7.log(`Deadbolt ${status.version ?? ""} (protocol ${status.protocol}) is ${status.unlocked ? "unlocked" : "locked: agent sign-ins are paused until you unlock it"}`);
|
|
1591
1628
|
else if (status)
|
|
1592
|
-
yield*
|
|
1593
|
-
})).pipe(
|
|
1594
|
-
var deadbolt =
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
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 } : {}
|
|
1604
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;
|
|
1605
1664
|
if (json) {
|
|
1606
|
-
yield* emitJson({
|
|
1665
|
+
yield* emitJson({ current: current ?? undefined, sessions });
|
|
1607
1666
|
return;
|
|
1608
1667
|
}
|
|
1609
|
-
if (
|
|
1610
|
-
yield*
|
|
1668
|
+
if (sessions.length === 0) {
|
|
1669
|
+
yield* Console8.log("No sessions");
|
|
1611
1670
|
return;
|
|
1612
1671
|
}
|
|
1613
|
-
yield*
|
|
1614
|
-
|
|
1615
|
-
|
|
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}`);
|
|
1616
1679
|
});
|
|
1617
|
-
})).pipe(
|
|
1618
|
-
var
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
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"));
|
|
1627
1719
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
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"));
|
|
1630
1723
|
}
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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 }) {
|
|
1641
1740
|
const relay = yield* Service;
|
|
1741
|
+
const store = yield* Service2;
|
|
1742
|
+
const sessionId = yield* resolvePositionalOrFlagSessionId(id, session);
|
|
1642
1743
|
yield* ensureCliRelay();
|
|
1643
|
-
const
|
|
1644
|
-
if (
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
var
|
|
1653
|
-
|
|
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 }) {
|
|
1654
1759
|
const relay = yield* Service;
|
|
1655
1760
|
yield* ensureCliRelay();
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
yield*
|
|
1660
|
-
yield*
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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")),
|
|
1664
1785
|
json: jsonFlag
|
|
1665
|
-
},
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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;
|
|
1672
1799
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
yield*
|
|
1679
|
-
|
|
1680
|
-
})
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
var
|
|
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 })));
|
|
1687
1815
|
var mainLayer = Layer2.mergeAll(layerFetch, layer).pipe(Layer2.provideMerge(NodeServices.layer));
|
|
1688
|
-
function runCli(rawArgs =
|
|
1816
|
+
function runCli(rawArgs = process3.argv.slice(2)) {
|
|
1689
1817
|
const cliArgs = hoistJsonFlag(rawArgs);
|
|
1690
1818
|
const subcommand = cliArgs.argv.find((arg) => !arg.startsWith("-"));
|
|
1691
1819
|
const commandName = subcommand ? `opencode-browser ${subcommand}` : "opencode-browser";
|
|
1692
|
-
const reportUsageError = (error) =>
|
|
1820
|
+
const reportUsageError = (error) => Effect11.gen(function* () {
|
|
1693
1821
|
const errors = error._tag === "ShowHelp" ? error.errors : [error];
|
|
1694
1822
|
if (errors.length === 0)
|
|
1695
1823
|
return;
|
|
@@ -1700,23 +1828,23 @@ function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
1700
1828
|
}
|
|
1701
1829
|
yield* exitWith(ExitCode.usage);
|
|
1702
1830
|
});
|
|
1703
|
-
const reportFailure = (error) =>
|
|
1831
|
+
const reportFailure = (error) => Effect11.gen(function* () {
|
|
1704
1832
|
const message = error instanceof Error ? error.message : String(error);
|
|
1705
1833
|
const exitCode = exitCodeForError(error);
|
|
1706
1834
|
if (cliArgs.json) {
|
|
1707
1835
|
yield* emitJson(failure(commandName, exitCode === ExitCode.host ? "relay_unavailable" : "error", message, { exitCode }));
|
|
1708
1836
|
yield* stderrLine(message);
|
|
1709
1837
|
} else {
|
|
1710
|
-
yield*
|
|
1838
|
+
yield* Console9.error(message);
|
|
1711
1839
|
}
|
|
1712
1840
|
yield* exitWith(exitCode);
|
|
1713
1841
|
});
|
|
1714
1842
|
const silent = () => {};
|
|
1715
1843
|
const askedForHelp = cliArgs.argv.some((arg) => arg === "--help" || arg === "-h");
|
|
1716
1844
|
const quietJson = cliArgs.json && !askedForHelp;
|
|
1717
|
-
|
|
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);
|
|
1718
1846
|
}
|
|
1719
|
-
if (
|
|
1847
|
+
if (process3.argv[1] && import.meta.url === pathToFileURL(process3.argv[1]).href) {
|
|
1720
1848
|
runCli();
|
|
1721
1849
|
}
|
|
1722
1850
|
export {
|