@vibecodr/cli 1.0.14 → 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"
@@ -655,8 +665,140 @@ async function commandBrowser(context, subcommand, rest) {
655
665
  const normalized = normalizeBrowserNotesOptions(parsed);
656
666
  return submitHostedCapability(context, "browser.notes", normalized, "Captured a hosted Browser snapshot with your note attached.", { autoFollow: true });
657
667
  }
668
+ case "session":
669
+ return commandBrowserSession(context, rest[0], rest.slice(1));
658
670
  default:
659
- throw unknownSubcommandError("browser", subcommand, ["render", "screenshot", "read", "markdown", "pdf", "crawl", "snapshot", "notes"], "Use vibecodr browser screenshot <https-url>, browser read <https-url>, or browser snapshot <https-url> --local.");
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
+ };
799
+ }
800
+ default:
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>.");
660
802
  }
661
803
  }
662
804
  async function commandComputer(context, subcommand, rest) {
@@ -736,13 +878,16 @@ async function submitHostedCapability(context, capabilityInput, parsed, successM
736
878
  throw new CliError("input.local_requires_wait", "--local saves the completed output, so it cannot be combined with --no-wait.", 2);
737
879
  }
738
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: [] };
739
884
  const { profile } = await context.store.getProfile(context.globals.profile);
740
885
  const client = createClient(context, profile, await resolveToken(context, true));
741
886
  const response = await client.request("POST", "tools/test", {
742
887
  body: payload
743
888
  });
744
889
  if (options.autoFollow === true && !shouldSkipWait(parsed)) {
745
- return followSubmittedWork(context, client, capability, response, parsed);
890
+ return followSubmittedWork(context, client, capability, response, proofOutput.parsed, proofOutput.warnings);
746
891
  }
747
892
  return {
748
893
  message: successMessage ?? (capability === "usage.read" ? "Read usage and limits from hosted Vibecodr." : `Submitted ${capability} test to hosted Vibecodr.`),
@@ -752,12 +897,13 @@ async function submitHostedCapability(context, capabilityInput, parsed, successM
752
897
  function shouldSkipWait(parsed) {
753
898
  return getBooleanFlag(parsed.flags, "noWait") || parsed.flags.wait === false;
754
899
  }
755
- async function followSubmittedWork(context, client, capability, submitted, parsed) {
900
+ async function followSubmittedWork(context, client, capability, submitted, parsed, warnings = []) {
756
901
  const jobId = jobIdFromWork(submitted);
757
902
  if (!jobId) {
758
903
  return {
759
904
  message: completedCapabilityMessage(capability),
760
905
  data: publicWorkResult(capability, submitted, parsed),
906
+ warnings,
761
907
  humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
762
908
  };
763
909
  }
@@ -771,21 +917,24 @@ async function followSubmittedWork(context, client, capability, submitted, parse
771
917
  return {
772
918
  message: formatCompletedWorkMessage(capability, terminal, proof),
773
919
  data: publicWorkResult(capability, terminal, parsed, proof),
920
+ warnings,
774
921
  humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
775
922
  };
776
923
  }
777
924
  async function commandWorkFollow(context, parsed) {
778
925
  const jobId = validateEntityId(requiredPositional(parsed, 0, "work follow requires a job id."), "job id");
926
+ const proofOutput = await prepareAutoProofOutput(context, parsed);
779
927
  const { profile } = await context.store.getProfile(context.globals.profile);
780
928
  const client = createClient(context, profile, await resolveToken(context, true));
781
- const job = await pollWorkUntilTerminal(client, jobId, parsed);
929
+ const job = await pollWorkUntilTerminal(client, jobId, proofOutput.parsed);
782
930
  const artifactId = artifactIdFromWork(job);
783
- const proof = artifactId && shouldSaveArtifact(parsed)
784
- ? await saveArtifact(context, client, artifactId, parsedWithLocalOutput(parsed))
931
+ const proof = artifactId && shouldSaveArtifact(proofOutput.parsed)
932
+ ? await saveArtifact(context, client, artifactId, proofOutput.parsed)
785
933
  : undefined;
786
934
  return {
787
935
  message: formatCompletedWorkMessage(undefined, job, proof),
788
- data: publicWorkResult(undefined, job, parsed, proof),
936
+ data: publicWorkResult(undefined, job, proofOutput.parsed, proof),
937
+ warnings: proofOutput.warnings,
789
938
  humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
790
939
  };
791
940
  }
@@ -881,6 +1030,60 @@ function parsedWithLocalOutput(parsed) {
881
1030
  }
882
1031
  };
883
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
+ }
884
1087
  function formatCompletedWorkMessage(capability, work, proof) {
885
1088
  const status = workStatus(work) ?? "completed";
886
1089
  if (status === "completed") {
@@ -1027,6 +1230,125 @@ function normalizeBrowserNotesOptions(parsed) {
1027
1230
  flags
1028
1231
  };
1029
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
+ }
1030
1352
  function normalizeComputerCommandOptions(parsed, missingMessage) {
1031
1353
  const command = getStringFlag(parsed.flags, "command") ?? parsed.positionals.join(" ").trim();
1032
1354
  if (!command) {
@@ -1895,6 +2217,26 @@ function usageBar(percent) {
1895
2217
  return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
1896
2218
  }
1897
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
+ }
1898
2240
  if (capability.startsWith("browser.")) {
1899
2241
  if (!target) {
1900
2242
  throw new CliError("input.url_required", `${capability} requires an HTTPS URL target.`, 2);
@@ -1976,6 +2318,37 @@ function buildToolTestPayload(capability, target, parsed, globalToolTimeoutMs) {
1976
2318
  }
1977
2319
  return { capability, input: {} };
1978
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
+ }
1979
2352
  function buildScheduledQaPayload(target, parsed, globalToolTimeoutMs) {
1980
2353
  const capability = normalizeScheduledQaCliCapability(getStringFlag(parsed.flags, "capability") ?? getStringFlag(parsed.flags, "tool") ?? "browser.render_url");
1981
2354
  const timeoutInput = getStringFlag(parsed.flags, "timeoutMs") ?? (globalToolTimeoutMs === undefined ? undefined : String(globalToolTimeoutMs));
@@ -2797,6 +3170,9 @@ Usage:
2797
3170
  vibecodr computer status
2798
3171
  vibecodr computer run "<command>" [--timeout-ms <ms>] [--network public|off] [--local|--out ./proof] [--no-wait] [--details]
2799
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.
2800
3176
  `;
2801
3177
  case "browser":
2802
3178
  return `vibecodr browser
@@ -2810,14 +3186,32 @@ Usage:
2810
3186
  vibecodr browser pdf <https-url> [--local|--out ./proof] [--no-wait] [--details]
2811
3187
  vibecodr browser crawl <https-url> [--max-pages n] [--max-depth n] [--local|--out ./proof]
2812
3188
  vibecodr browser snapshot <https-url> [--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>
2813
3202
 
2814
3203
  Attach a note:
2815
3204
  vibecodr browser notes <https-url> --note <text> [--local|--out ./proof]
2816
3205
 
2817
3206
  Notes:
2818
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.
2819
3209
  browser snapshot captures page state; it does not prompt an agent or model.
2820
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.
2821
3215
  `;
2822
3216
  case "work":
2823
3217
  return `vibecodr work
@@ -2829,6 +3223,9 @@ Usage:
2829
3223
  vibecodr work show <jobId>
2830
3224
  vibecodr work follow <jobId> [--local|--out ./proof] [--details]
2831
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.
2832
3229
  `;
2833
3230
  case "proof":
2834
3231
  return `vibecodr proof