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