opera-browser-cli 0.1.33 → 0.1.35
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/README.md +68 -29
- package/SKILL.md +28 -2
- package/dist/src/bridge.d.ts +12 -3
- package/dist/src/bridge.js +33 -4
- package/dist/src/bridge.js.map +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +136 -54
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +4 -4
- package/dist/src/client.js +16 -4
- package/dist/src/client.js.map +1 -1
- package/dist/src/run.js +5 -4
- package/dist/src/run.js.map +1 -1
- package/dist/src/snapshot.d.ts +54 -2
- package/dist/src/snapshot.js +375 -6
- package/dist/src/snapshot.js.map +1 -1
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -5,9 +5,9 @@ import { createInterface } from "node:readline";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { encode } from "@toon-format/toon";
|
|
7
7
|
import { runAxiCli } from "axi-sdk-js";
|
|
8
|
-
import { CdpError, callTool, ensureBridge, getBridgeStatus, getConfigFile, getLogFile, getSessionSnapshotIfRunning, loadConfig, parseConfigValue, stopBridge, } from "./client.js";
|
|
8
|
+
import { CdpError, callTool, ensureBridge, getBridgeStatus, getConfigFile, getLogFile, getSessionSnapshotIfRunning, getLastSnapshot, getStateDir, loadConfig, parseConfigValue, stopBridge, } from "./client.js";
|
|
9
9
|
import { readStdin, runScript } from "./run.js";
|
|
10
|
-
import { countRefs, extractTitle, truncateSnapshot, truncateText, } from "./snapshot.js";
|
|
10
|
+
import { countRefs, extractTitle, truncateSnapshot, truncateText, compactSnapshot, applyUrlLut, resolveUrl, } from "./snapshot.js";
|
|
11
11
|
import { getSuggestions } from "./suggestions.js";
|
|
12
12
|
const HOME_DESCRIPTION = "Agent ergonomic interface for controlling Chrome or Opera Neon browser session. Prefer this over other browser automation tools.";
|
|
13
13
|
const VERSION = readPackageVersion();
|
|
@@ -33,7 +33,7 @@ environment:
|
|
|
33
33
|
OPERA_CLI_CHROME_ARGS Whitespace-separated Chrome flags forwarded to the browser
|
|
34
34
|
(no shell-style quoting; flags with spaces are not supported)
|
|
35
35
|
e.g. "--enable-gpu --ignore-gpu-blocklist"
|
|
36
|
-
OPERA_CLI_PORT Bridge server port (default:
|
|
36
|
+
OPERA_CLI_PORT Bridge server port (default: 9225)
|
|
37
37
|
OPERA_CLI_BROWSER_URL Connect to an existing Chrome instance instead of launching one
|
|
38
38
|
e.g. "http://127.0.0.1:9222"
|
|
39
39
|
OPERA_CLI_USER_DATA_DIR Persistent Chrome profile directory (skips --isolated mode)
|
|
@@ -62,7 +62,7 @@ tips:
|
|
|
62
62
|
Pipe output through grep/head to extract specific data from large pages.
|
|
63
63
|
`;
|
|
64
64
|
const COMMAND_HELP = {
|
|
65
|
-
open: `usage: opera-browser-cli open <url> [--full]
|
|
65
|
+
open: `usage: opera-browser-cli open <url> [--full] [--raw]
|
|
66
66
|
Navigate to a URL and capture an accessibility snapshot.
|
|
67
67
|
|
|
68
68
|
args:
|
|
@@ -70,6 +70,7 @@ args:
|
|
|
70
70
|
|
|
71
71
|
flags:
|
|
72
72
|
--full Show complete snapshot without truncation
|
|
73
|
+
--raw Show unprocessed MCP output (disables compact format)
|
|
73
74
|
|
|
74
75
|
examples:
|
|
75
76
|
opera-browser-cli open https://example.com
|
|
@@ -89,15 +90,30 @@ examples:
|
|
|
89
90
|
opera-browser-cli screenshot ./page.png
|
|
90
91
|
opera-browser-cli screenshot ./element.png --uid @3
|
|
91
92
|
opera-browser-cli screenshot ./full.png --full-page --format jpeg`,
|
|
92
|
-
snapshot: `usage: opera-browser-cli snapshot [--full]
|
|
93
|
+
snapshot: `usage: opera-browser-cli snapshot [--full] [--raw]
|
|
93
94
|
Capture the current page accessibility snapshot.
|
|
94
95
|
|
|
95
96
|
flags:
|
|
96
97
|
--full Show complete snapshot without truncation
|
|
98
|
+
--raw Show unprocessed MCP output (disables compact format)
|
|
97
99
|
|
|
98
100
|
examples:
|
|
99
101
|
opera-browser-cli snapshot
|
|
100
|
-
opera-browser-cli snapshot --full
|
|
102
|
+
opera-browser-cli snapshot --full
|
|
103
|
+
opera-browser-cli snapshot --raw`,
|
|
104
|
+
url: `usage: opera-browser-cli url <$uN | @ref>
|
|
105
|
+
Resolve a URL token or element ref from the last snapshot.
|
|
106
|
+
|
|
107
|
+
args:
|
|
108
|
+
$uN URL token printed in the snapshot's urls: trailer (e.g. $u3)
|
|
109
|
+
@ref Element ref from the snapshot (e.g. @11.57)
|
|
110
|
+
|
|
111
|
+
Tokens ($uN) are scoped to the last snapshot. If no snapshot is cached
|
|
112
|
+
the bridge takes a fresh one automatically.
|
|
113
|
+
|
|
114
|
+
examples:
|
|
115
|
+
opera-browser-cli url \\$u3
|
|
116
|
+
opera-browser-cli url @11.57`,
|
|
101
117
|
click: `usage: opera-browser-cli click @<uid> [--full]
|
|
102
118
|
Click an interactive element by its ref from the snapshot.
|
|
103
119
|
|
|
@@ -760,8 +776,9 @@ function readPackageVersion() {
|
|
|
760
776
|
}
|
|
761
777
|
function splitFullFlag(args) {
|
|
762
778
|
return {
|
|
763
|
-
args: args.filter((arg) => arg !== "--full"),
|
|
779
|
+
args: args.filter((arg) => arg !== "--full" && arg !== "--raw"),
|
|
764
780
|
full: args.includes("--full"),
|
|
781
|
+
raw: args.includes("--raw"),
|
|
765
782
|
};
|
|
766
783
|
}
|
|
767
784
|
function trimSingleTrailingNewline(text) {
|
|
@@ -823,10 +840,11 @@ function parseSnapshotFromResponse(response) {
|
|
|
823
840
|
? trimmed.trimEnd()
|
|
824
841
|
: trimmed.slice(0, nextHeading).trimEnd();
|
|
825
842
|
}
|
|
826
|
-
/** Format page metadata (TOON) +
|
|
827
|
-
function formatPageOutput(snapshot, command, url, full = false) {
|
|
828
|
-
const
|
|
829
|
-
const
|
|
843
|
+
/** Format page metadata (TOON) + snapshot + suggestions. */
|
|
844
|
+
function formatPageOutput(snapshot, command, url, full = false, raw = false) {
|
|
845
|
+
const tree = raw ? snapshot : compactSnapshot(snapshot);
|
|
846
|
+
const title = extractTitle(tree);
|
|
847
|
+
const refs = countRefs(tree);
|
|
830
848
|
const blocks = [];
|
|
831
849
|
// Page metadata as TOON
|
|
832
850
|
const page = {};
|
|
@@ -836,15 +854,28 @@ function formatPageOutput(snapshot, command, url, full = false) {
|
|
|
836
854
|
page.url = url;
|
|
837
855
|
page.refs = refs;
|
|
838
856
|
blocks.push(encode({ page }));
|
|
839
|
-
// Truncate snapshot
|
|
840
|
-
|
|
841
|
-
|
|
857
|
+
// Truncate snapshot, then apply URL LUT to the visible portion only.
|
|
858
|
+
// LUT runs after truncation so the trailer lists only URLs the agent can see.
|
|
859
|
+
const tr = truncateSnapshot(tree, full, raw ? 16000 : 12000);
|
|
860
|
+
const lutResult = raw ? { body: tr.text, trailer: "", urlMap: new Map() } : applyUrlLut(tr.text);
|
|
861
|
+
const { body, trailer } = lutResult;
|
|
862
|
+
// Persist the urlMap so `opera-browser-cli url $uN` resolves against exactly
|
|
863
|
+
// the same token assignments the agent saw (truncated snapshot, not full).
|
|
864
|
+
try {
|
|
865
|
+
writeFileSync(join(getStateDir(), "last-url-map.json"), JSON.stringify(Object.fromEntries(lutResult.urlMap)));
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
// Non-fatal: url command falls back to re-derivation if write fails.
|
|
869
|
+
}
|
|
870
|
+
let snapshotBlock = `snapshot:\n${body.trimEnd()}`;
|
|
871
|
+
if (trailer)
|
|
872
|
+
snapshotBlock += `\n${trailer}`;
|
|
842
873
|
if (tr.truncated) {
|
|
843
874
|
snapshotBlock += `\n ... (truncated, ${tr.totalLength} chars total)`;
|
|
844
875
|
}
|
|
845
876
|
blocks.push(snapshotBlock);
|
|
846
877
|
// Contextual suggestions
|
|
847
|
-
const suggestions = getSuggestions({ command, url, snapshot });
|
|
878
|
+
const suggestions = getSuggestions({ command, url, snapshot: tree });
|
|
848
879
|
if (tr.truncated) {
|
|
849
880
|
suggestions.push(`Run \`opera-browser-cli ${command}${url ? " " + url : ""} --full\` to see complete snapshot`);
|
|
850
881
|
}
|
|
@@ -858,14 +889,15 @@ function stripSnapshotHeader(text) {
|
|
|
858
889
|
// Find the first line that looks like a tree node (uid= or RootWebArea)
|
|
859
890
|
const lines = text.split("\n");
|
|
860
891
|
const treeStart = lines.findIndex((l) => /\bRootWebArea\b|\buid=/.test(l));
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
892
|
+
const result = treeStart > 0
|
|
893
|
+
? lines.slice(treeStart).join("\n")
|
|
894
|
+
: text.replace(/^[\s\S]*?##\s+Latest page snapshot\s*\n/, "");
|
|
895
|
+
// Rewrite MCP-internal tool name to the CLI command users actually run.
|
|
896
|
+
return result.replace(/Call list_pages\b/g, "Run `opera-browser-cli pages`");
|
|
865
897
|
}
|
|
866
|
-
/** Strip leading @
|
|
898
|
+
/** Strip leading @ and normalise dot-form refs to underscore form for MCP ("@2.4" → "2_4"). */
|
|
867
899
|
function parseUid(arg) {
|
|
868
|
-
return arg.
|
|
900
|
+
return arg.replace(/^@/, "").replace(/\./g, "_");
|
|
869
901
|
}
|
|
870
902
|
function isRecoverableOpenError(error) {
|
|
871
903
|
if (!(error instanceof CdpError))
|
|
@@ -897,28 +929,35 @@ function normalizeUrl(raw) {
|
|
|
897
929
|
return raw;
|
|
898
930
|
return `https://${raw}`;
|
|
899
931
|
}
|
|
900
|
-
async function handleOpen(args, full) {
|
|
932
|
+
async function handleOpen(args, full, raw = false) {
|
|
901
933
|
const url = args[0] ? normalizeUrl(args[0]) : undefined;
|
|
902
934
|
if (!url) {
|
|
903
935
|
throw new CdpError("Missing URL", "VALIDATION_ERROR", [
|
|
904
936
|
"Run `opera-browser-cli open https://example.com` to navigate to a page",
|
|
905
937
|
]);
|
|
906
938
|
}
|
|
939
|
+
let needNewPage = false;
|
|
907
940
|
try {
|
|
908
|
-
await callTool("navigate_page", { type: "url", url });
|
|
941
|
+
const navResult = await callTool("navigate_page", { type: "url", url });
|
|
942
|
+
if (/selected page has been closed/i.test(navResult)) {
|
|
943
|
+
needNewPage = true;
|
|
944
|
+
}
|
|
909
945
|
}
|
|
910
946
|
catch (error) {
|
|
911
947
|
if (!isRecoverableOpenError(error)) {
|
|
912
948
|
throw error;
|
|
913
949
|
}
|
|
950
|
+
needNewPage = true;
|
|
951
|
+
}
|
|
952
|
+
if (needNewPage) {
|
|
914
953
|
await callTool("new_page", { url });
|
|
915
954
|
}
|
|
916
955
|
const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
|
|
917
|
-
return formatPageOutput(snapshot, "open", url, full);
|
|
956
|
+
return formatPageOutput(snapshot, "open", url, full, raw);
|
|
918
957
|
}
|
|
919
|
-
async function handleSnapshot(full) {
|
|
958
|
+
async function handleSnapshot(full, raw = false) {
|
|
920
959
|
const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
|
|
921
|
-
return formatPageOutput(snapshot, "snapshot", undefined, full);
|
|
960
|
+
return formatPageOutput(snapshot, "snapshot", undefined, full, raw);
|
|
922
961
|
}
|
|
923
962
|
async function handleScreenshot(args) {
|
|
924
963
|
const parsed = parseScreenshotArgs(args);
|
|
@@ -946,7 +985,7 @@ async function handleScreenshot(args) {
|
|
|
946
985
|
}
|
|
947
986
|
return formatScreenshotOutput(parsed.filePath);
|
|
948
987
|
}
|
|
949
|
-
async function handleClick(args, full) {
|
|
988
|
+
async function handleClick(args, full, raw = false) {
|
|
950
989
|
const uid = args[0];
|
|
951
990
|
if (!uid) {
|
|
952
991
|
throw new CdpError("Missing element ref", "VALIDATION_ERROR", [
|
|
@@ -954,9 +993,9 @@ async function handleClick(args, full) {
|
|
|
954
993
|
]);
|
|
955
994
|
}
|
|
956
995
|
const snapshot = await callWithSnapshot("click", { uid: parseUid(uid) });
|
|
957
|
-
return formatPageOutput(snapshot, "click", undefined, full);
|
|
996
|
+
return formatPageOutput(snapshot, "click", undefined, full, raw);
|
|
958
997
|
}
|
|
959
|
-
async function handleFill(args, full) {
|
|
998
|
+
async function handleFill(args, full, raw = false) {
|
|
960
999
|
const uid = args[0];
|
|
961
1000
|
const value = args.slice(1).join(" ");
|
|
962
1001
|
if (!uid) {
|
|
@@ -973,9 +1012,9 @@ async function handleFill(args, full) {
|
|
|
973
1012
|
uid: parseUid(uid),
|
|
974
1013
|
value,
|
|
975
1014
|
});
|
|
976
|
-
return formatPageOutput(snapshot, "fill", undefined, full);
|
|
1015
|
+
return formatPageOutput(snapshot, "fill", undefined, full, raw);
|
|
977
1016
|
}
|
|
978
|
-
async function handlePress(args, full) {
|
|
1017
|
+
async function handlePress(args, full, raw = false) {
|
|
979
1018
|
const key = args[0];
|
|
980
1019
|
if (!key) {
|
|
981
1020
|
throw new CdpError("Missing key name", "VALIDATION_ERROR", [
|
|
@@ -983,9 +1022,9 @@ async function handlePress(args, full) {
|
|
|
983
1022
|
]);
|
|
984
1023
|
}
|
|
985
1024
|
const snapshot = await callWithSnapshot("press_key", { key });
|
|
986
|
-
return formatPageOutput(snapshot, "press", undefined, full);
|
|
1025
|
+
return formatPageOutput(snapshot, "press", undefined, full, raw);
|
|
987
1026
|
}
|
|
988
|
-
async function handleType(args, full) {
|
|
1027
|
+
async function handleType(args, full, raw = false) {
|
|
989
1028
|
const text = args.join(" ");
|
|
990
1029
|
if (!text) {
|
|
991
1030
|
throw new CdpError("Missing text", "VALIDATION_ERROR", [
|
|
@@ -994,9 +1033,9 @@ async function handleType(args, full) {
|
|
|
994
1033
|
}
|
|
995
1034
|
await callTool("type_text", { text });
|
|
996
1035
|
const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
|
|
997
|
-
return formatPageOutput(snapshot, "type", undefined, full);
|
|
1036
|
+
return formatPageOutput(snapshot, "type", undefined, full, raw);
|
|
998
1037
|
}
|
|
999
|
-
async function handleScroll(args, full) {
|
|
1038
|
+
async function handleScroll(args, full, raw = false) {
|
|
1000
1039
|
const dir = (args[0] ?? "down").toLowerCase();
|
|
1001
1040
|
const fn = SCROLL_FUNCTIONS[dir];
|
|
1002
1041
|
if (!fn) {
|
|
@@ -1006,12 +1045,12 @@ async function handleScroll(args, full) {
|
|
|
1006
1045
|
}
|
|
1007
1046
|
await callTool("evaluate_script", { function: fn });
|
|
1008
1047
|
const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
|
|
1009
|
-
return formatPageOutput(snapshot, "scroll", undefined, full);
|
|
1048
|
+
return formatPageOutput(snapshot, "scroll", undefined, full, raw);
|
|
1010
1049
|
}
|
|
1011
|
-
async function handleBack(full) {
|
|
1050
|
+
async function handleBack(full, raw = false) {
|
|
1012
1051
|
await callTool("navigate_page", { type: "back" });
|
|
1013
1052
|
const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
|
|
1014
|
-
return formatPageOutput(snapshot, "back", undefined, full);
|
|
1053
|
+
return formatPageOutput(snapshot, "back", undefined, full, raw);
|
|
1015
1054
|
}
|
|
1016
1055
|
async function handleWait(args) {
|
|
1017
1056
|
const target = args[0];
|
|
@@ -1112,7 +1151,7 @@ async function handlePages() {
|
|
|
1112
1151
|
]));
|
|
1113
1152
|
return renderOutput(blocks);
|
|
1114
1153
|
}
|
|
1115
|
-
async function handleNewPage(args, full) {
|
|
1154
|
+
async function handleNewPage(args, full, raw = false) {
|
|
1116
1155
|
const url = args.filter((a) => !a.startsWith("--"))[0];
|
|
1117
1156
|
if (!url) {
|
|
1118
1157
|
throw new CdpError("Missing URL", "VALIDATION_ERROR", [
|
|
@@ -1125,9 +1164,9 @@ async function handleNewPage(args, full) {
|
|
|
1125
1164
|
toolArgs.background = true;
|
|
1126
1165
|
await callTool("new_page", toolArgs);
|
|
1127
1166
|
const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
|
|
1128
|
-
return formatPageOutput(snapshot, "newpage", url, full);
|
|
1167
|
+
return formatPageOutput(snapshot, "newpage", url, full, raw);
|
|
1129
1168
|
}
|
|
1130
|
-
async function handleSelectPage(args, full) {
|
|
1169
|
+
async function handleSelectPage(args, full, raw = false) {
|
|
1131
1170
|
const id = args[0];
|
|
1132
1171
|
if (!id) {
|
|
1133
1172
|
throw new CdpError("Missing page ID", "VALIDATION_ERROR", [
|
|
@@ -1142,7 +1181,7 @@ async function handleSelectPage(args, full) {
|
|
|
1142
1181
|
}
|
|
1143
1182
|
await callTool("select_page", { pageId });
|
|
1144
1183
|
const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
|
|
1145
|
-
return formatPageOutput(snapshot, "selectpage", undefined, full);
|
|
1184
|
+
return formatPageOutput(snapshot, "selectpage", undefined, full, raw);
|
|
1146
1185
|
}
|
|
1147
1186
|
async function handleClosePage(args) {
|
|
1148
1187
|
const id = args[0];
|
|
@@ -1194,7 +1233,7 @@ async function handleResize(args) {
|
|
|
1194
1233
|
return encode({ resized: { width, height } });
|
|
1195
1234
|
}
|
|
1196
1235
|
// --- Interaction handlers ---
|
|
1197
|
-
async function handleHover(args, full) {
|
|
1236
|
+
async function handleHover(args, full, raw = false) {
|
|
1198
1237
|
const uid = args[0];
|
|
1199
1238
|
if (!uid) {
|
|
1200
1239
|
throw new CdpError("Missing element ref", "VALIDATION_ERROR", [
|
|
@@ -1202,9 +1241,9 @@ async function handleHover(args, full) {
|
|
|
1202
1241
|
]);
|
|
1203
1242
|
}
|
|
1204
1243
|
const snapshot = await callWithSnapshot("hover", { uid: parseUid(uid) });
|
|
1205
|
-
return formatPageOutput(snapshot, "hover", undefined, full);
|
|
1244
|
+
return formatPageOutput(snapshot, "hover", undefined, full, raw);
|
|
1206
1245
|
}
|
|
1207
|
-
async function handleDrag(args, full) {
|
|
1246
|
+
async function handleDrag(args, full, raw = false) {
|
|
1208
1247
|
const from = args[0];
|
|
1209
1248
|
const to = args[1];
|
|
1210
1249
|
if (!from || !to) {
|
|
@@ -1216,9 +1255,9 @@ async function handleDrag(args, full) {
|
|
|
1216
1255
|
from_uid: parseUid(from),
|
|
1217
1256
|
to_uid: parseUid(to),
|
|
1218
1257
|
});
|
|
1219
|
-
return formatPageOutput(snapshot, "drag", undefined, full);
|
|
1258
|
+
return formatPageOutput(snapshot, "drag", undefined, full, raw);
|
|
1220
1259
|
}
|
|
1221
|
-
async function handleFillForm(args, full) {
|
|
1260
|
+
async function handleFillForm(args, full, raw = false) {
|
|
1222
1261
|
const { entries } = parseFillFormArgs(args);
|
|
1223
1262
|
if (entries.length === 0) {
|
|
1224
1263
|
throw new CdpError("No valid field entries", "VALIDATION_ERROR", [
|
|
@@ -1226,7 +1265,7 @@ async function handleFillForm(args, full) {
|
|
|
1226
1265
|
]);
|
|
1227
1266
|
}
|
|
1228
1267
|
const snapshot = await callWithSnapshot("fill_form", { elements: entries });
|
|
1229
|
-
return formatPageOutput(snapshot, "fillform", undefined, full);
|
|
1268
|
+
return formatPageOutput(snapshot, "fillform", undefined, full, raw);
|
|
1230
1269
|
}
|
|
1231
1270
|
async function handleDialog(args) {
|
|
1232
1271
|
const action = args[0];
|
|
@@ -1242,7 +1281,7 @@ async function handleDialog(args) {
|
|
|
1242
1281
|
await callTool("handle_dialog", params);
|
|
1243
1282
|
return encode({ dialog: action });
|
|
1244
1283
|
}
|
|
1245
|
-
async function handleUpload(args, full) {
|
|
1284
|
+
async function handleUpload(args, full, raw = false) {
|
|
1246
1285
|
const uid = args[0];
|
|
1247
1286
|
const filePath = args[1];
|
|
1248
1287
|
if (!uid) {
|
|
@@ -1259,7 +1298,7 @@ async function handleUpload(args, full) {
|
|
|
1259
1298
|
uid: parseUid(uid),
|
|
1260
1299
|
filePath,
|
|
1261
1300
|
});
|
|
1262
|
-
return formatPageOutput(snapshot, "upload", undefined, full);
|
|
1301
|
+
return formatPageOutput(snapshot, "upload", undefined, full, raw);
|
|
1263
1302
|
}
|
|
1264
1303
|
// --- Emulation handler ---
|
|
1265
1304
|
async function handleEmulate(args) {
|
|
@@ -1968,7 +2007,7 @@ async function handleHome(_full) {
|
|
|
1968
2007
|
renderHelp(help),
|
|
1969
2008
|
]);
|
|
1970
2009
|
}
|
|
1971
|
-
const snapshot = stripSnapshotHeader(result);
|
|
2010
|
+
const snapshot = compactSnapshot(stripSnapshotHeader(result));
|
|
1972
2011
|
const title = extractTitle(snapshot);
|
|
1973
2012
|
const refs = countRefs(snapshot);
|
|
1974
2013
|
const page = {};
|
|
@@ -1982,10 +2021,52 @@ async function handleHome(_full) {
|
|
|
1982
2021
|
];
|
|
1983
2022
|
return renderOutput([encode({ page }), renderHelp(help)]);
|
|
1984
2023
|
}
|
|
2024
|
+
async function handleUrl(args) {
|
|
2025
|
+
const target = args[0];
|
|
2026
|
+
if (!target) {
|
|
2027
|
+
throw new CdpError("Missing argument", "VALIDATION_ERROR", [
|
|
2028
|
+
"Run `opera-browser-cli url \\$u3` to resolve a URL token",
|
|
2029
|
+
"Run `opera-browser-cli url @11.57` to resolve an element ref",
|
|
2030
|
+
]);
|
|
2031
|
+
}
|
|
2032
|
+
// Prefer the bridge's cached snapshot to avoid an extra MCP round-trip.
|
|
2033
|
+
// Fall back to a fresh snapshot if the cache is cold.
|
|
2034
|
+
let raw;
|
|
2035
|
+
const cached = await getLastSnapshot();
|
|
2036
|
+
if (cached) {
|
|
2037
|
+
raw = cached.raw;
|
|
2038
|
+
}
|
|
2039
|
+
else {
|
|
2040
|
+
await ensureBridge();
|
|
2041
|
+
raw = stripSnapshotHeader(await callTool("take_snapshot"));
|
|
2042
|
+
}
|
|
2043
|
+
// Use the urlMap persisted at render time so token IDs match exactly what the
|
|
2044
|
+
// agent saw (derived from the truncated snapshot). Fall back to re-derivation
|
|
2045
|
+
// on the full snapshot only if the sidecar file is missing.
|
|
2046
|
+
// Use compact (no LUT applied) as the body: ref lookups find literal url="..."
|
|
2047
|
+
// values there, so token-index alignment with urlMap is not needed.
|
|
2048
|
+
const compact = compactSnapshot(raw);
|
|
2049
|
+
let urlMap;
|
|
2050
|
+
try {
|
|
2051
|
+
const stored = JSON.parse(readFileSync(join(getStateDir(), "last-url-map.json"), "utf-8"));
|
|
2052
|
+
urlMap = new Map(Object.entries(stored));
|
|
2053
|
+
}
|
|
2054
|
+
catch {
|
|
2055
|
+
({ urlMap } = applyUrlLut(compact));
|
|
2056
|
+
}
|
|
2057
|
+
const body = compact;
|
|
2058
|
+
const resolved = resolveUrl(body, urlMap, target);
|
|
2059
|
+
if (resolved === null) {
|
|
2060
|
+
process.stderr.write(`url: "${target}" not found in last snapshot\n`);
|
|
2061
|
+
process.exitCode = 1;
|
|
2062
|
+
return "";
|
|
2063
|
+
}
|
|
2064
|
+
return resolved;
|
|
2065
|
+
}
|
|
1985
2066
|
function withFullFlag(handler) {
|
|
1986
2067
|
return (args) => {
|
|
1987
2068
|
const parsed = splitFullFlag(args);
|
|
1988
|
-
return handler(parsed.args, parsed.full);
|
|
2069
|
+
return handler(parsed.args, parsed.full, parsed.raw);
|
|
1989
2070
|
};
|
|
1990
2071
|
}
|
|
1991
2072
|
function withoutFullFlag(handler) {
|
|
@@ -1993,14 +2074,15 @@ function withoutFullFlag(handler) {
|
|
|
1993
2074
|
}
|
|
1994
2075
|
const COMMANDS = {
|
|
1995
2076
|
open: withFullFlag(handleOpen),
|
|
1996
|
-
snapshot: async (args) =>
|
|
2077
|
+
snapshot: async (args) => { const f = splitFullFlag(args); return handleSnapshot(f.full, f.raw); },
|
|
2078
|
+
url: withoutFullFlag(handleUrl),
|
|
1997
2079
|
screenshot: withoutFullFlag(handleScreenshot),
|
|
1998
2080
|
click: withFullFlag(handleClick),
|
|
1999
2081
|
fill: withFullFlag(handleFill),
|
|
2000
2082
|
type: withFullFlag(handleType),
|
|
2001
2083
|
press: withFullFlag(handlePress),
|
|
2002
2084
|
scroll: withFullFlag(handleScroll),
|
|
2003
|
-
back: async (args) =>
|
|
2085
|
+
back: async (args) => { const f = splitFullFlag(args); return handleBack(f.full, f.raw); },
|
|
2004
2086
|
wait: withoutFullFlag(handleWait),
|
|
2005
2087
|
eval: withFullFlag(handleEval),
|
|
2006
2088
|
run: async () => handleRun(),
|