@vibecodr/cli 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/legacy/cli/run.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AA+CtD,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAoED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8CzF"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/legacy/cli/run.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAgDtD,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAoED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8CzF"}
@@ -1,5 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { ConfigStore, DEFAULT_API_URL, resolveConfigDir } from "../config/store.js";
@@ -18,6 +19,7 @@ const DEFAULT_AUTH_API_URL = "https://api.vibecodr.space";
18
19
  const GRANT_REFRESH_SKEW_SECONDS = 60;
19
20
  const ARTIFACT_OUTPUT_WORKSPACE_MESSAGE = "Artifact output is workspace-bounded so downloaded bytes can only be written to files you intentionally target inside this workspace. Use --local for ./vibecodr-proof, --out ./artifacts, --out ./artifacts/report.pdf, or cd to the intended workspace and use --out .";
20
21
  const ARTIFACT_INPUT_WORKSPACE_MESSAGE = "Artifact upload sources are workspace-bounded so the CLI only reads files you intentionally target inside this workspace. Move the file into this workspace, or cd to the workspace that contains it.";
22
+ const WORKSPACE_SCRATCH_PROOF_DIR = ".vibecodr/browser-artifacts";
21
23
  export async function runCli(argv, options = {}) {
22
24
  const stdout = options.stdout ?? process.stdout;
23
25
  const stderr = options.stderr ?? process.stderr;
@@ -190,11 +192,19 @@ async function commandTry(context, parsed) {
190
192
  const { profile } = await context.store.getProfile(context.globals.profile);
191
193
  const client = createClient(context, profile, await resolveToken(context, true));
192
194
  const proofDir = getStringFlag(parsed.flags, "out") ?? "vibecodr-proof";
195
+ const proofOutput = await prepareAutoProofOutput(context, {
196
+ positionals: [],
197
+ flags: {
198
+ ...parsed.flags,
199
+ out: proofDir
200
+ }
201
+ });
202
+ const resolvedProofDir = getStringFlag(proofOutput.parsed.flags, "out") ?? proofDir;
193
203
  const browserParsed = {
194
204
  positionals: ["https://example.com"],
195
205
  flags: {
196
206
  ...parsed.flags,
197
- out: proofDir,
207
+ out: resolvedProofDir,
198
208
  filename: "browser-read.md",
199
209
  pollIntervalMs: getStringFlag(parsed.flags, "pollIntervalMs") ?? "250"
200
210
  }
@@ -204,7 +214,7 @@ async function commandTry(context, parsed) {
204
214
  flags: {
205
215
  ...parsed.flags,
206
216
  command: "node -e \"console.log('vibecodr computer ok')\"",
207
- out: proofDir,
217
+ out: resolvedProofDir,
208
218
  filename: "computer-run.json",
209
219
  pollIntervalMs: getStringFlag(parsed.flags, "pollIntervalMs") ?? "250"
210
220
  }
@@ -217,7 +227,7 @@ async function commandTry(context, parsed) {
217
227
  proof: "failed",
218
228
  usage: "failed"
219
229
  };
220
- const warnings = [];
230
+ const warnings = [...proofOutput.warnings];
221
231
  const browserPayload = buildToolTestPayload("browser.extract_markdown", browserParsed.positionals[0], browserParsed);
222
232
  const browserWork = await client.request("POST", "tools/test", { body: browserPayload });
223
233
  const browserResult = await followSubmittedWork(context, client, "browser.extract_markdown", browserWork, browserParsed);
@@ -253,12 +263,12 @@ async function commandTry(context, parsed) {
253
263
  const ready = Object.values(checks).every((status) => status === "ok");
254
264
  return {
255
265
  message: ready
256
- ? `Vibecodr Agent Computer check passed.\nProof saved: ${path.resolve(context.cwd, proofDir)}`
257
- : `Vibecodr Agent Computer check finished with attention needed.\nProof path: ${path.resolve(context.cwd, proofDir)}`,
266
+ ? `Vibecodr Agent Computer check passed.\nProof saved: ${path.resolve(context.cwd, resolvedProofDir)}`
267
+ : `Vibecodr Agent Computer check finished with attention needed.\nProof path: ${path.resolve(context.cwd, resolvedProofDir)}`,
258
268
  data: {
259
269
  ready,
260
270
  checks,
261
- proofPath: path.resolve(context.cwd, proofDir)
271
+ proofPath: path.resolve(context.cwd, resolvedProofDir)
262
272
  },
263
273
  warnings,
264
274
  humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
@@ -650,12 +660,145 @@ async function commandBrowser(context, subcommand, rest) {
650
660
  const normalized = normalizeBrowserSnapshotOptions(parsed);
651
661
  return submitHostedCapability(context, "browser.snapshot", normalized, "Captured a hosted Browser snapshot.", { autoFollow: true });
652
662
  }
663
+ case "notes":
653
664
  case "ask": {
654
- const normalized = normalizeBrowserAskOptions(parsed);
655
- return submitHostedCapability(context, "browser.ask", normalized, "Captured a hosted Browser snapshot with your note attached.", { autoFollow: true });
665
+ const normalized = normalizeBrowserNotesOptions(parsed);
666
+ return submitHostedCapability(context, "browser.notes", normalized, "Captured a hosted Browser snapshot with your note attached.", { autoFollow: true });
667
+ }
668
+ case "session":
669
+ return commandBrowserSession(context, rest[0], rest.slice(1));
670
+ default:
671
+ throw unknownSubcommandError("browser", subcommand, ["render", "screenshot", "read", "markdown", "pdf", "crawl", "snapshot", "notes", "session"], "Use vibecodr browser screenshot <https-url>, browser read <https-url>, browser snapshot <https-url> --local, or browser session open <https-url>.");
672
+ }
673
+ }
674
+ async function commandBrowserSession(context, subcommand, rest) {
675
+ const parsed = parseCommandOptions(rest);
676
+ const { profile } = await context.store.getProfile(context.globals.profile);
677
+ const client = createClient(context, profile, await resolveToken(context, true));
678
+ switch (subcommand) {
679
+ case "open": {
680
+ const target = parsed.positionals[0];
681
+ if (!target) {
682
+ throw new CliError("input.url_required", "browser session open requires an HTTPS URL target.", 2);
683
+ }
684
+ const body = { url: validateBrowserUrl(target) };
685
+ const timeoutMs = validatePositiveInt(getStringFlag(parsed.flags, "timeoutMs"), "--timeout-ms", 1000, 3_600_000);
686
+ const idleTimeoutMs = validatePositiveInt(getStringFlag(parsed.flags, "idleTimeoutMs"), "--idle-timeout-ms", 1000, 600_000);
687
+ if (timeoutMs !== undefined)
688
+ body.timeoutMs = timeoutMs;
689
+ if (idleTimeoutMs !== undefined)
690
+ body.idleTimeoutMs = idleTimeoutMs;
691
+ const response = await client.request("POST", "browser/sessions", { body });
692
+ return {
693
+ message: browserSessionMessage(response, "Opened Agent Browser session."),
694
+ data: response,
695
+ humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
696
+ };
697
+ }
698
+ case "observe": {
699
+ const sessionId = validateBrowserSessionId(parsed.positionals[0]);
700
+ const response = await client.request("GET", `browser/sessions/${encodePathSegment(sessionId)}`);
701
+ return {
702
+ message: browserSessionMessage(response, "Observed Agent Browser session."),
703
+ data: response,
704
+ humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
705
+ };
706
+ }
707
+ case "goto":
708
+ case "navigate":
709
+ case "click":
710
+ case "type":
711
+ case "scroll":
712
+ case "wait": {
713
+ const sessionId = validateBrowserSessionId(parsed.positionals[0]);
714
+ const body = browserSessionActionBody(subcommand, parsed);
715
+ const response = await client.request("POST", `browser/sessions/${encodePathSegment(sessionId)}/actions`, { body });
716
+ return {
717
+ message: browserSessionMessage(response, `Ran Agent Browser ${subcommand}.`),
718
+ data: response,
719
+ humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
720
+ };
721
+ }
722
+ case "auth":
723
+ case "auth-request": {
724
+ const sessionId = validateBrowserSessionId(parsed.positionals[0]);
725
+ const response = await client.request("POST", `browser/sessions/${encodePathSegment(sessionId)}/auth${browserSessionLiveViewQuery(parsed, context.globals.debug)}`);
726
+ const handoffUrl = browserSessionHandoffUrlFromResponse(response);
727
+ const skipOpen = getBooleanFlag(parsed.flags, "noOpen") ||
728
+ parsed.flags.open === false ||
729
+ context.globals.json ||
730
+ context.globals.quiet ||
731
+ context.globals.noInput;
732
+ let opened = false;
733
+ if (handoffUrl && !skipOpen) {
734
+ opened = await maybeOpenBrowser(context, parsed, handoffUrl);
735
+ }
736
+ return {
737
+ message: browserSessionMessage(response, opened ? "Opened Agent Browser for you to control." : "Agent Browser control link ready."),
738
+ data: response,
739
+ humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
740
+ };
741
+ }
742
+ case "live":
743
+ case "watch":
744
+ case "view": {
745
+ const sessionId = validateBrowserSessionId(parsed.positionals[0]);
746
+ const response = await client.request("POST", `browser/sessions/${encodePathSegment(sessionId)}/live${browserSessionLiveViewQuery(parsed, context.globals.debug)}`);
747
+ const handoffUrl = browserSessionHandoffUrlFromResponse(response);
748
+ const skipOpen = getBooleanFlag(parsed.flags, "noOpen") ||
749
+ parsed.flags.open === false ||
750
+ context.globals.json ||
751
+ context.globals.quiet ||
752
+ context.globals.noInput;
753
+ let opened = false;
754
+ if (handoffUrl && !skipOpen) {
755
+ opened = await maybeOpenBrowser(context, parsed, handoffUrl);
756
+ }
757
+ return {
758
+ message: browserSessionMessage(response, opened ? "Opened Agent Browser live view." : "Agent Browser live view ready."),
759
+ data: response,
760
+ humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
761
+ };
762
+ }
763
+ case "auth-status": {
764
+ const sessionId = validateBrowserSessionId(parsed.positionals[0]);
765
+ const response = await client.request("GET", `browser/sessions/${encodePathSegment(sessionId)}/auth`);
766
+ return {
767
+ message: browserSessionMessage(response, "Read Agent Browser live-control status."),
768
+ data: response,
769
+ humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
770
+ };
771
+ }
772
+ case "auth-complete":
773
+ case "auth-done": {
774
+ const sessionId = validateBrowserSessionId(parsed.positionals[0]);
775
+ const response = await client.request("POST", `browser/sessions/${encodePathSegment(sessionId)}/auth/complete`);
776
+ return {
777
+ message: browserSessionMessage(response, "Gave Agent Browser control back to the agent."),
778
+ data: response,
779
+ humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
780
+ };
781
+ }
782
+ case "auth-revoke": {
783
+ const sessionId = validateBrowserSessionId(parsed.positionals[0]);
784
+ const response = await client.request("POST", `browser/sessions/${encodePathSegment(sessionId)}/auth/revoke`);
785
+ return {
786
+ message: browserSessionMessage(response, "Ended Agent Browser live link."),
787
+ data: response,
788
+ humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
789
+ };
790
+ }
791
+ case "close": {
792
+ const sessionId = validateBrowserSessionId(parsed.positionals[0]);
793
+ const response = await client.request("DELETE", `browser/sessions/${encodePathSegment(sessionId)}`);
794
+ return {
795
+ message: "Closed Agent Browser session.",
796
+ data: response,
797
+ humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
798
+ };
656
799
  }
657
800
  default:
658
- throw unknownSubcommandError("browser", subcommand, ["render", "screenshot", "read", "markdown", "pdf", "crawl", "snapshot", "ask"], "Use vibecodr browser screenshot <https-url>, browser read <https-url>, or browser snapshot <https-url> --local.");
801
+ throw unknownSubcommandError("browser session", subcommand, ["open", "observe", "goto", "click", "type", "scroll", "wait", "live", "auth", "auth-status", "auth-complete", "auth-revoke", "close"], "Use vibecodr browser session open <https-url>, live <sessionId>, observe <sessionId>, click <sessionId> --selector <css>, or close <sessionId>.");
659
802
  }
660
803
  }
661
804
  async function commandComputer(context, subcommand, rest) {
@@ -735,13 +878,16 @@ async function submitHostedCapability(context, capabilityInput, parsed, successM
735
878
  throw new CliError("input.local_requires_wait", "--local saves the completed output, so it cannot be combined with --no-wait.", 2);
736
879
  }
737
880
  const payload = buildToolTestPayload(capability, parsed.positionals[0], parsed, context.globals.timeoutMs === 30_000 ? undefined : context.globals.timeoutMs);
881
+ const proofOutput = options.autoFollow === true && !shouldSkipWait(parsed)
882
+ ? await prepareAutoProofOutput(context, parsed)
883
+ : { parsed, warnings: [] };
738
884
  const { profile } = await context.store.getProfile(context.globals.profile);
739
885
  const client = createClient(context, profile, await resolveToken(context, true));
740
886
  const response = await client.request("POST", "tools/test", {
741
887
  body: payload
742
888
  });
743
889
  if (options.autoFollow === true && !shouldSkipWait(parsed)) {
744
- return followSubmittedWork(context, client, capability, response, parsed);
890
+ return followSubmittedWork(context, client, capability, response, proofOutput.parsed, proofOutput.warnings);
745
891
  }
746
892
  return {
747
893
  message: successMessage ?? (capability === "usage.read" ? "Read usage and limits from hosted Vibecodr." : `Submitted ${capability} test to hosted Vibecodr.`),
@@ -751,12 +897,13 @@ async function submitHostedCapability(context, capabilityInput, parsed, successM
751
897
  function shouldSkipWait(parsed) {
752
898
  return getBooleanFlag(parsed.flags, "noWait") || parsed.flags.wait === false;
753
899
  }
754
- async function followSubmittedWork(context, client, capability, submitted, parsed) {
900
+ async function followSubmittedWork(context, client, capability, submitted, parsed, warnings = []) {
755
901
  const jobId = jobIdFromWork(submitted);
756
902
  if (!jobId) {
757
903
  return {
758
904
  message: completedCapabilityMessage(capability),
759
905
  data: publicWorkResult(capability, submitted, parsed),
906
+ warnings,
760
907
  humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
761
908
  };
762
909
  }
@@ -770,21 +917,24 @@ async function followSubmittedWork(context, client, capability, submitted, parse
770
917
  return {
771
918
  message: formatCompletedWorkMessage(capability, terminal, proof),
772
919
  data: publicWorkResult(capability, terminal, parsed, proof),
920
+ warnings,
773
921
  humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
774
922
  };
775
923
  }
776
924
  async function commandWorkFollow(context, parsed) {
777
925
  const jobId = validateEntityId(requiredPositional(parsed, 0, "work follow requires a job id."), "job id");
926
+ const proofOutput = await prepareAutoProofOutput(context, parsed);
778
927
  const { profile } = await context.store.getProfile(context.globals.profile);
779
928
  const client = createClient(context, profile, await resolveToken(context, true));
780
- const job = await pollWorkUntilTerminal(client, jobId, parsed);
929
+ const job = await pollWorkUntilTerminal(client, jobId, proofOutput.parsed);
781
930
  const artifactId = artifactIdFromWork(job);
782
- const proof = artifactId && shouldSaveArtifact(parsed)
783
- ? await saveArtifact(context, client, artifactId, parsedWithLocalOutput(parsed))
931
+ const proof = artifactId && shouldSaveArtifact(proofOutput.parsed)
932
+ ? await saveArtifact(context, client, artifactId, proofOutput.parsed)
784
933
  : undefined;
785
934
  return {
786
935
  message: formatCompletedWorkMessage(undefined, job, proof),
787
- data: publicWorkResult(undefined, job, parsed, proof),
936
+ data: publicWorkResult(undefined, job, proofOutput.parsed, proof),
937
+ warnings: proofOutput.warnings,
788
938
  humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
789
939
  };
790
940
  }
@@ -880,6 +1030,60 @@ function parsedWithLocalOutput(parsed) {
880
1030
  }
881
1031
  };
882
1032
  }
1033
+ async function prepareAutoProofOutput(context, parsed) {
1034
+ if (!shouldSaveArtifact(parsed)) {
1035
+ return { parsed, warnings: [] };
1036
+ }
1037
+ const withLocalOutput = parsedWithLocalOutput(parsed);
1038
+ const out = getStringFlag(withLocalOutput.flags, "out") ?? ".";
1039
+ const outPath = path.resolve(context.cwd, out);
1040
+ try {
1041
+ await ensureOutputPathAllowed(context.cwd, outPath);
1042
+ return { parsed: withLocalOutput, warnings: [] };
1043
+ }
1044
+ catch (error) {
1045
+ const cliError = toCliError(error);
1046
+ if (cliError.code !== "file.outside_workspace" || !isPathOutside(path.resolve(context.cwd), outPath)) {
1047
+ throw error;
1048
+ }
1049
+ }
1050
+ const fallbackOut = await allocateScratchProofOutputDir(context.cwd);
1051
+ return {
1052
+ parsed: {
1053
+ ...withLocalOutput,
1054
+ flags: {
1055
+ ...withLocalOutput.flags,
1056
+ out: fallbackOut
1057
+ }
1058
+ },
1059
+ warnings: [
1060
+ `Output path is outside this workspace, so hosted artifacts cannot be written there. Writing to ${formatCliPath(fallbackOut)} instead. This scratch folder is gitignored; use --out ./path-inside-workspace to choose another workspace path.`
1061
+ ]
1062
+ };
1063
+ }
1064
+ async function allocateScratchProofOutputDir(cwd) {
1065
+ const root = path.join(cwd, WORKSPACE_SCRATCH_PROOF_DIR);
1066
+ await fs.mkdir(root, { recursive: true });
1067
+ for (let attempt = 0; attempt < 5; attempt += 1) {
1068
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1069
+ const relative = `${WORKSPACE_SCRATCH_PROOF_DIR}/${timestamp}-${randomUUID().slice(0, 8)}`;
1070
+ const absolute = path.resolve(cwd, relative);
1071
+ try {
1072
+ await fs.mkdir(absolute, { recursive: false });
1073
+ return relative;
1074
+ }
1075
+ catch (error) {
1076
+ if (error.code !== "EEXIST") {
1077
+ throw error;
1078
+ }
1079
+ }
1080
+ }
1081
+ throw new CliError("file.scratch_output_failed", "Could not create a workspace scratch output directory for hosted artifacts.", 5);
1082
+ }
1083
+ function formatCliPath(input) {
1084
+ const normalized = input.split(path.sep).join("/");
1085
+ return normalized.startsWith("./") || normalized.startsWith("../") ? normalized : `./${normalized}`;
1086
+ }
883
1087
  function formatCompletedWorkMessage(capability, work, proof) {
884
1088
  const status = workStatus(work) ?? "completed";
885
1089
  if (status === "completed") {
@@ -999,14 +1203,14 @@ function userToolName(capability) {
999
1203
  function normalizeBrowserSnapshotOptions(parsed) {
1000
1204
  const [url, ...extraParts] = parsed.positionals;
1001
1205
  if (extraParts.length > 0 || parsed.flags.instructions !== undefined || parsed.flags.note !== undefined) {
1002
- throw new CliError("input.snapshot_is_not_prompted", "browser snapshot captures the page state; it does not prompt an agent or model. Remove the note/instructions, or use `vibecodr browser ask <url> --note \"...\"` for the advanced compatibility lane.", 2);
1206
+ throw new CliError("input.snapshot_is_not_prompted", "browser snapshot captures the page state; it does not prompt an agent or model. Remove the note/instructions, or use `vibecodr browser notes <url> --note \"...\"` to save a note with the snapshot.", 2);
1003
1207
  }
1004
1208
  return {
1005
1209
  positionals: url === undefined ? [] : [url],
1006
1210
  flags: parsed.flags
1007
1211
  };
1008
1212
  }
1009
- function normalizeBrowserAskOptions(parsed) {
1213
+ function normalizeBrowserNotesOptions(parsed) {
1010
1214
  const [url, ...instructionParts] = parsed.positionals;
1011
1215
  const flags = { ...parsed.flags };
1012
1216
  const note = getStringFlag(flags, "note");
@@ -1019,13 +1223,132 @@ function normalizeBrowserAskOptions(parsed) {
1019
1223
  flags.instructions = instructionParts.join(" ");
1020
1224
  }
1021
1225
  if (getStringFlag(flags, "instructions") === undefined) {
1022
- throw new CliError("input.ask_note_required", "browser ask is an advanced compatibility alias and needs a note. For a normal capture, use `vibecodr browser snapshot <url> --local`.", 2);
1226
+ throw new CliError("input.notes_note_required", "browser notes needs a note. For a normal capture, use `vibecodr browser snapshot <url> --local`.", 2);
1023
1227
  }
1024
1228
  return {
1025
1229
  positionals: url === undefined ? [] : [url],
1026
1230
  flags
1027
1231
  };
1028
1232
  }
1233
+ function validateBrowserSessionId(input) {
1234
+ return validateEntityId(input ?? "", "Agent Browser session id");
1235
+ }
1236
+ function browserSessionActionBody(subcommand, parsed) {
1237
+ if (subcommand === "goto" || subcommand === "navigate") {
1238
+ const target = getStringFlag(parsed.flags, "url") ?? parsed.positionals[1];
1239
+ if (!target) {
1240
+ throw new CliError("input.url_required", `browser session ${subcommand} requires an HTTPS URL target.`, 2);
1241
+ }
1242
+ return { action: "navigate", url: validateBrowserUrl(target) };
1243
+ }
1244
+ if (subcommand === "click") {
1245
+ return { action: "click", selector: requiredBrowserSessionSelector(parsed, "browser session click requires --selector <css>.") };
1246
+ }
1247
+ if (subcommand === "type") {
1248
+ const text = getStringFlag(parsed.flags, "text") ?? parsed.positionals.slice(2).join(" ").trim();
1249
+ if (!text) {
1250
+ throw new CliError("input.text_required", "browser session type requires --text <text>.", 2);
1251
+ }
1252
+ return {
1253
+ action: "type",
1254
+ selector: requiredBrowserSessionSelector(parsed, "browser session type requires --selector <css>."),
1255
+ text: text.slice(0, 2_000)
1256
+ };
1257
+ }
1258
+ if (subcommand === "scroll") {
1259
+ const deltaY = validateIntegerRange(getStringFlag(parsed.flags, "deltaY") ?? parsed.positionals[1], "--delta-y", -10_000, 10_000);
1260
+ return deltaY === undefined ? { action: "scroll" } : { action: "scroll", deltaY };
1261
+ }
1262
+ if (subcommand === "wait") {
1263
+ const ms = validateIntegerRange(getStringFlag(parsed.flags, "ms") ?? parsed.positionals[1], "--ms", 1, 30_000);
1264
+ return ms === undefined ? { action: "wait" } : { action: "wait", ms };
1265
+ }
1266
+ throw new CliError("input.invalid_browser_session_action", "Unknown Agent Browser session action.", 2);
1267
+ }
1268
+ function browserSessionLiveViewQuery(parsed, globalDebug = false) {
1269
+ const mode = browserSessionLiveViewMode(parsed, globalDebug);
1270
+ return mode === "devtools" ? "?view=devtools" : "";
1271
+ }
1272
+ function browserSessionLiveViewMode(parsed, globalDebug = false) {
1273
+ const raw = (getStringFlag(parsed.flags, "view") ?? getStringFlag(parsed.flags, "mode") ?? "").trim().toLowerCase();
1274
+ const debug = globalDebug || getBooleanFlag(parsed.flags, "debug") || getBooleanFlag(parsed.flags, "devtools");
1275
+ if (debug && (raw === "tab" || raw === "browser")) {
1276
+ throw new CliError("input.invalid_live_view_mode", "Use either --debug/--devtools or --view tab, not both.", 2);
1277
+ }
1278
+ if (debug || raw === "devtools" || raw === "debug" || raw === "inspector") {
1279
+ return "devtools";
1280
+ }
1281
+ if (!raw || raw === "tab" || raw === "browser") {
1282
+ return "tab";
1283
+ }
1284
+ throw new CliError("input.invalid_live_view_mode", "Agent Browser live view mode must be tab or devtools.", 2);
1285
+ }
1286
+ function requiredBrowserSessionSelector(parsed, message) {
1287
+ const selector = getStringFlag(parsed.flags, "selector") ?? parsed.positionals[1];
1288
+ if (!selector || selector.trim().length === 0) {
1289
+ throw new CliError("input.selector_required", message, 2);
1290
+ }
1291
+ return selector.trim().slice(0, 500);
1292
+ }
1293
+ function validateIntegerRange(input, label, min, max) {
1294
+ if (input === undefined) {
1295
+ return undefined;
1296
+ }
1297
+ const value = Number(input);
1298
+ if (!Number.isInteger(value) || value < min || value > max) {
1299
+ throw new CliError("input.invalid_number", `${label} must be an integer from ${min} to ${max}.`, 2);
1300
+ }
1301
+ return value;
1302
+ }
1303
+ function browserSessionMessage(response, fallback) {
1304
+ if (!isRecord(response)) {
1305
+ return fallback;
1306
+ }
1307
+ const session = isRecord(response.session) ? response.session : undefined;
1308
+ const id = typeof session?.id === "string" ? session.id : typeof response.id === "string" ? response.id : undefined;
1309
+ const observation = isRecord(response.observation) ? response.observation : undefined;
1310
+ const screenshot = isRecord(observation?.screenshot) ? observation?.screenshot : undefined;
1311
+ const auth = isRecord(response.auth) ? response.auth : isRecord(session?.auth) ? session?.auth : undefined;
1312
+ const lines = [fallback];
1313
+ if (id) {
1314
+ lines.push(`Session: ${id}`);
1315
+ lines.push(`Observe: vibecodr browser session observe ${id}`);
1316
+ lines.push(`Watch live: vibecodr browser session live ${id}`);
1317
+ lines.push(`Need to sign in or steer? vibecodr browser session auth ${id}`);
1318
+ lines.push(`Close: vibecodr browser session close ${id}`);
1319
+ }
1320
+ if (auth && typeof auth.status === "string") {
1321
+ lines.push(`Control: ${browserSessionControlLabel(auth.status)}`);
1322
+ }
1323
+ const handoffUrl = browserSessionHandoffUrlFromResponse(response);
1324
+ if (handoffUrl) {
1325
+ lines.push(`Open live page: ${handoffUrl}`);
1326
+ }
1327
+ if (typeof screenshot?.id === "string") {
1328
+ lines.push(`Screenshot proof: vibecodr proof show ${screenshot.id}`);
1329
+ }
1330
+ return lines.join("\n");
1331
+ }
1332
+ function browserSessionControlLabel(status) {
1333
+ if (status === "human_control")
1334
+ return "you are controlling the browser";
1335
+ if (status === "auth_ready")
1336
+ return "agent can continue";
1337
+ if (status === "revoked")
1338
+ return "live link ended";
1339
+ if (status === "handoff_expired")
1340
+ return "watch link expired";
1341
+ if (status === "public")
1342
+ return "agent is browsing";
1343
+ return status;
1344
+ }
1345
+ function browserSessionHandoffUrlFromResponse(response) {
1346
+ if (!isRecord(response)) {
1347
+ return undefined;
1348
+ }
1349
+ const auth = isRecord(response.auth) ? response.auth : undefined;
1350
+ return typeof auth?.handoffUrl === "string" ? auth.handoffUrl : undefined;
1351
+ }
1029
1352
  function normalizeComputerCommandOptions(parsed, missingMessage) {
1030
1353
  const command = getStringFlag(parsed.flags, "command") ?? parsed.positionals.join(" ").trim();
1031
1354
  if (!command) {
@@ -1894,6 +2217,26 @@ function usageBar(percent) {
1894
2217
  return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
1895
2218
  }
1896
2219
  function buildToolTestPayload(capability, target, parsed, globalToolTimeoutMs) {
2220
+ if (capability.startsWith("browser.session_")) {
2221
+ const input = {};
2222
+ if (capability === "browser.session_open") {
2223
+ if (!target) {
2224
+ throw new CliError("input.url_required", "browser.session_open requires an HTTPS URL target.", 2);
2225
+ }
2226
+ input.url = validateBrowserUrl(target);
2227
+ const timeoutMs = validatePositiveInt(getStringFlag(parsed.flags, "timeoutMs"), "--timeout-ms", 1000, 3_600_000);
2228
+ const idleTimeoutMs = validatePositiveInt(getStringFlag(parsed.flags, "idleTimeoutMs"), "--idle-timeout-ms", 1000, 600_000);
2229
+ if (timeoutMs !== undefined)
2230
+ input.timeoutMs = timeoutMs;
2231
+ if (idleTimeoutMs !== undefined)
2232
+ input.idleTimeoutMs = idleTimeoutMs;
2233
+ }
2234
+ else {
2235
+ input.sessionId = validateBrowserSessionId(target);
2236
+ Object.assign(input, browserSessionToolTestActionInput(capability, parsed));
2237
+ }
2238
+ return { capability, input };
2239
+ }
1897
2240
  if (capability.startsWith("browser.")) {
1898
2241
  if (!target) {
1899
2242
  throw new CliError("input.url_required", `${capability} requires an HTTPS URL target.`, 2);
@@ -1975,6 +2318,37 @@ function buildToolTestPayload(capability, target, parsed, globalToolTimeoutMs) {
1975
2318
  }
1976
2319
  return { capability, input: {} };
1977
2320
  }
2321
+ function browserSessionToolTestActionInput(capability, parsed) {
2322
+ if (capability === "browser.session_observe" || capability === "browser.session_close") {
2323
+ return {};
2324
+ }
2325
+ if (capability === "browser.session_navigate") {
2326
+ const target = getStringFlag(parsed.flags, "url") ?? parsed.positionals[1];
2327
+ if (!target) {
2328
+ throw new CliError("input.url_required", "browser.session_navigate requires an HTTPS URL target.", 2);
2329
+ }
2330
+ return { url: validateBrowserUrl(target) };
2331
+ }
2332
+ if (capability === "browser.session_click") {
2333
+ return { selector: requiredBrowserSessionSelector(parsed, "browser.session_click requires --selector <css>.") };
2334
+ }
2335
+ if (capability === "browser.session_type") {
2336
+ const text = getStringFlag(parsed.flags, "text") ?? parsed.positionals.slice(2).join(" ").trim();
2337
+ if (!text) {
2338
+ throw new CliError("input.text_required", "browser.session_type requires --text <text>.", 2);
2339
+ }
2340
+ return { selector: requiredBrowserSessionSelector(parsed, "browser.session_type requires --selector <css>."), text };
2341
+ }
2342
+ if (capability === "browser.session_scroll") {
2343
+ const deltaY = validateIntegerRange(getStringFlag(parsed.flags, "deltaY") ?? parsed.positionals[1], "--delta-y", -10_000, 10_000);
2344
+ return deltaY === undefined ? {} : { deltaY };
2345
+ }
2346
+ if (capability === "browser.session_wait") {
2347
+ const ms = validateIntegerRange(getStringFlag(parsed.flags, "ms") ?? parsed.positionals[1], "--ms", 1, 30_000);
2348
+ return ms === undefined ? {} : { ms };
2349
+ }
2350
+ return {};
2351
+ }
1978
2352
  function buildScheduledQaPayload(target, parsed, globalToolTimeoutMs) {
1979
2353
  const capability = normalizeScheduledQaCliCapability(getStringFlag(parsed.flags, "capability") ?? getStringFlag(parsed.flags, "tool") ?? "browser.render_url");
1980
2354
  const timeoutInput = getStringFlag(parsed.flags, "timeoutMs") ?? (globalToolTimeoutMs === undefined ? undefined : String(globalToolTimeoutMs));
@@ -2796,6 +3170,9 @@ Usage:
2796
3170
  vibecodr computer status
2797
3171
  vibecodr computer run "<command>" [--timeout-ms <ms>] [--network public|off] [--local|--out ./proof] [--no-wait] [--details]
2798
3172
  vibecodr computer test "<command>" [--timeout-ms <ms>] [--network public|off] [--local|--out ./proof] [--no-wait] [--details]
3173
+
3174
+ Notes:
3175
+ Automatic output saves are workspace-bounded. If --out points outside this workspace, vibecodr writes to ./.vibecodr/browser-artifacts/<run> instead.
2799
3176
  `;
2800
3177
  case "browser":
2801
3178
  return `vibecodr browser
@@ -2809,14 +3186,32 @@ Usage:
2809
3186
  vibecodr browser pdf <https-url> [--local|--out ./proof] [--no-wait] [--details]
2810
3187
  vibecodr browser crawl <https-url> [--max-pages n] [--max-depth n] [--local|--out ./proof]
2811
3188
  vibecodr browser snapshot <https-url> [--local|--out ./proof]
2812
-
2813
- Advanced compatibility:
2814
- vibecodr browser ask <https-url> --note <text> [--local|--out ./proof]
3189
+ vibecodr browser session open <https-url> [--timeout-ms <ms>] [--idle-timeout-ms <ms>]
3190
+ vibecodr browser session observe <sessionId>
3191
+ vibecodr browser session goto <sessionId> <https-url>
3192
+ vibecodr browser session click <sessionId> --selector <css>
3193
+ vibecodr browser session type <sessionId> --selector <css> --text <text>
3194
+ vibecodr browser session scroll <sessionId> [--delta-y 800]
3195
+ vibecodr browser session wait <sessionId> [--ms 1000]
3196
+ vibecodr browser session live <sessionId> [--no-open] [--debug|--view devtools]
3197
+ vibecodr browser session auth <sessionId> [--no-open] [--debug|--view devtools]
3198
+ vibecodr browser session auth-status <sessionId>
3199
+ vibecodr browser session auth-complete <sessionId>
3200
+ vibecodr browser session auth-revoke <sessionId>
3201
+ vibecodr browser session close <sessionId>
3202
+
3203
+ Attach a note:
3204
+ vibecodr browser notes <https-url> --note <text> [--local|--out ./proof]
2815
3205
 
2816
3206
  Notes:
2817
3207
  Add --local to save completed output into ./vibecodr-proof automatically.
3208
+ Automatic output saves are workspace-bounded. If --out points outside this workspace, vibecodr writes to ./.vibecodr/browser-artifacts/<run> instead.
2818
3209
  browser snapshot captures page state; it does not prompt an agent or model.
2819
- browser ask saves your note with the snapshot; it is not a chat answerer.
3210
+ browser notes saves your note with the snapshot.
3211
+ browser session opens a real hosted Agent Browser the agent can observe and control until it is closed or idle.
3212
+ browser session live opens the watch-and-intercede page without pausing the agent.
3213
+ browser session auth opens the same live page with human control already active for login, MFA, CAPTCHA, or other human-only steps.
3214
+ Add --debug or --view devtools only when an agent/developer needs the inspector panel instead of the plain browser tab.
2820
3215
  `;
2821
3216
  case "work":
2822
3217
  return `vibecodr work
@@ -2828,6 +3223,9 @@ Usage:
2828
3223
  vibecodr work show <jobId>
2829
3224
  vibecodr work follow <jobId> [--local|--out ./proof] [--details]
2830
3225
  vibecodr work cancel <jobId> --yes
3226
+
3227
+ Notes:
3228
+ Automatic output saves are workspace-bounded. If --out points outside this workspace, vibecodr writes to ./.vibecodr/browser-artifacts/<run> instead.
2831
3229
  `;
2832
3230
  case "proof":
2833
3231
  return `vibecodr proof