@wenathlan/extension 1.1.31 → 1.1.33
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 +17 -12
- package/dist/cli.js +23 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +306 -15
- package/dist/index.js.map +2 -2
- package/dist/memory.d.ts +41 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +14 -2
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +38 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +146 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +796 -44
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +7 -1
- package/extension/dist/pagebridge.js +1238 -61
- package/extension/dist/pagebridge.js.map +4 -4
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +24 -0
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +179 -20
- package/extension/dist/sidepanel.js.map +2 -2
- package/extension/dist/style.css +1 -0
- package/extension/manifest.json +7 -1
- package/package.json +1 -1
|
@@ -34,12 +34,99 @@ var sessionmemory = class {
|
|
|
34
34
|
async setprogress(value) {
|
|
35
35
|
return this.adapter.set("progress", value);
|
|
36
36
|
}
|
|
37
|
+
async getcapabilities() {
|
|
38
|
+
return this.adapter.get("capabilities");
|
|
39
|
+
}
|
|
40
|
+
async setcapabilities(value) {
|
|
41
|
+
return this.adapter.set("capabilities", value);
|
|
42
|
+
}
|
|
43
|
+
async getsettings() {
|
|
44
|
+
return this.adapter.get("settings");
|
|
45
|
+
}
|
|
46
|
+
async setsettings(value) {
|
|
47
|
+
return this.adapter.set("settings", value);
|
|
48
|
+
}
|
|
37
49
|
async getaudit() {
|
|
38
50
|
return await this.adapter.get("audit") ?? [];
|
|
39
51
|
}
|
|
52
|
+
async getoutcomes() {
|
|
53
|
+
return await this.adapter.get("outcomes") ?? [];
|
|
54
|
+
}
|
|
55
|
+
/** Records one audit event; retention is a user setting and an absent setting keeps every event. */
|
|
40
56
|
async addaudi(event) {
|
|
41
57
|
const records = await this.getaudit();
|
|
42
|
-
|
|
58
|
+
const combined = [event, ...records];
|
|
59
|
+
const retention = (await this.getsettings())?.auditretention;
|
|
60
|
+
await this.adapter.set("audit", retention === void 0 ? combined : combined.slice(0, retention));
|
|
61
|
+
}
|
|
62
|
+
/** Records one step outcome; retention is a user setting and an absent setting keeps every outcome. */
|
|
63
|
+
async addoutcome(outcome) {
|
|
64
|
+
const records = await this.getoutcomes();
|
|
65
|
+
const combined = [outcome, ...records];
|
|
66
|
+
const retention = (await this.getsettings())?.outcomeretention;
|
|
67
|
+
await this.adapter.set("outcomes", retention === void 0 ? combined : combined.slice(0, retention));
|
|
68
|
+
}
|
|
69
|
+
/** Stores one clickable map under its observation version so every captured map stays available. */
|
|
70
|
+
async setmap(map) {
|
|
71
|
+
return this.adapter.set(`map${map.version}`, map);
|
|
72
|
+
}
|
|
73
|
+
/** Returns one stored clickable map by its observation version. */
|
|
74
|
+
async getmap(version) {
|
|
75
|
+
return this.adapter.get(`map${version}`);
|
|
76
|
+
}
|
|
77
|
+
/** Advances and persists the observation version counter used to stamp clickable maps. */
|
|
78
|
+
async nextobservationversion() {
|
|
79
|
+
const current = await this.adapter.get("observationversion") ?? 0;
|
|
80
|
+
const next = current + 1;
|
|
81
|
+
await this.adapter.set("observationversion", next);
|
|
82
|
+
return next;
|
|
83
|
+
}
|
|
84
|
+
/** Returns the latest observation version used to stamp a clickable map. */
|
|
85
|
+
async getobservationversion() {
|
|
86
|
+
return this.adapter.get("observationversion");
|
|
87
|
+
}
|
|
88
|
+
/** Returns the key hold registry, persisted so holds survive service worker restarts. */
|
|
89
|
+
async getholds() {
|
|
90
|
+
return await this.adapter.get("holds") ?? [];
|
|
91
|
+
}
|
|
92
|
+
/** Replaces the key hold registry after one press or release transition. */
|
|
93
|
+
async setholds(holds) {
|
|
94
|
+
return this.adapter.set("holds", holds);
|
|
95
|
+
}
|
|
96
|
+
/** Returns every dialog decision recorded for the audit trail. */
|
|
97
|
+
async getdialogs() {
|
|
98
|
+
return await this.adapter.get("dialogs") ?? [];
|
|
99
|
+
}
|
|
100
|
+
/** Records one dialog decision with the reviewed answer and the observed dialog text. */
|
|
101
|
+
async adddialog(decision) {
|
|
102
|
+
const records = await this.getdialogs();
|
|
103
|
+
await this.adapter.set("dialogs", [decision, ...records]);
|
|
104
|
+
}
|
|
105
|
+
/** Returns every retry outcome recorded with attempts and movement deltas. */
|
|
106
|
+
async getretries() {
|
|
107
|
+
return await this.adapter.get("retries") ?? [];
|
|
108
|
+
}
|
|
109
|
+
/** Records one retry outcome with the attempts made and the movement delta observed. */
|
|
110
|
+
async addretry(outcome) {
|
|
111
|
+
const records = await this.getretries();
|
|
112
|
+
await this.adapter.set("retries", [outcome, ...records]);
|
|
113
|
+
}
|
|
114
|
+
/** Returns every resolution summary stored per target mode. */
|
|
115
|
+
async getresolutions() {
|
|
116
|
+
return await this.adapter.get("resolutions") ?? [];
|
|
117
|
+
}
|
|
118
|
+
/** Records one resolution summary for later selector derivation. */
|
|
119
|
+
async addresolution(summary) {
|
|
120
|
+
const records = await this.getresolutions();
|
|
121
|
+
await this.adapter.set("resolutions", [summary, ...records]);
|
|
122
|
+
}
|
|
123
|
+
/** Returns the reviewed default dialog policy kept for the session auto handler. */
|
|
124
|
+
async getdialogpolicy() {
|
|
125
|
+
return this.adapter.get("dialogpolicy");
|
|
126
|
+
}
|
|
127
|
+
/** Stores the reviewed default dialog policy of the latest approved plan. */
|
|
128
|
+
async setdialogpolicy(policy) {
|
|
129
|
+
return this.adapter.set("dialogpolicy", policy);
|
|
43
130
|
}
|
|
44
131
|
};
|
|
45
132
|
function randomid() {
|
|
@@ -47,10 +134,12 @@ function randomid() {
|
|
|
47
134
|
}
|
|
48
135
|
|
|
49
136
|
// policy.ts
|
|
50
|
-
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select"]);
|
|
51
|
-
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover"]);
|
|
52
|
-
var
|
|
53
|
-
var
|
|
137
|
+
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor"]);
|
|
138
|
+
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
|
|
139
|
+
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath"]);
|
|
140
|
+
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
141
|
+
var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow"]);
|
|
142
|
+
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor"]);
|
|
54
143
|
function normalizeendpoint(value) {
|
|
55
144
|
const endpoint = new URL(value.trim());
|
|
56
145
|
if (endpoint.protocol !== "https:") throw new Error("Devthink accepts HTTPS endpoints only.");
|
|
@@ -67,16 +156,111 @@ function actionrisk(kind) {
|
|
|
67
156
|
if (sensitiveactions.has(kind)) return "sensitive";
|
|
68
157
|
return interactionactions.has(kind) ? "interaction" : "read";
|
|
69
158
|
}
|
|
159
|
+
function parseoptions(step) {
|
|
160
|
+
if (step.options === void 0) return {};
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = JSON.parse(step.options);
|
|
164
|
+
} catch {
|
|
165
|
+
throw new Error("Step options must be a JSON object.");
|
|
166
|
+
}
|
|
167
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Step options must be a JSON object.");
|
|
168
|
+
return parsed;
|
|
169
|
+
}
|
|
170
|
+
function requiredcapability(kind) {
|
|
171
|
+
if (kind === "tablist") return "tabs";
|
|
172
|
+
if (kind === "downloadfile") return "downloads";
|
|
173
|
+
return void 0;
|
|
174
|
+
}
|
|
70
175
|
function waitduration(step) {
|
|
71
176
|
const requested = step.value ? Number.parseInt(step.value, 10) : 250;
|
|
72
177
|
if (!Number.isFinite(requested) || requested < 0) throw new Error("Wait duration must be zero or a positive number of milliseconds.");
|
|
73
|
-
return
|
|
178
|
+
return requested;
|
|
179
|
+
}
|
|
180
|
+
function isnumericid(value) {
|
|
181
|
+
return typeof value === "string" && /^\d+$/.test(value);
|
|
182
|
+
}
|
|
183
|
+
function numericoption(options, key) {
|
|
184
|
+
return options[key] === void 0 || typeof options[key] === "number" && Number.isFinite(options[key]);
|
|
185
|
+
}
|
|
186
|
+
function nonnegativeoption(options, key) {
|
|
187
|
+
return numericoption(options, key) && !(typeof options[key] === "number" && options[key] < 0);
|
|
188
|
+
}
|
|
189
|
+
function isnonempty(value) {
|
|
190
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
191
|
+
}
|
|
192
|
+
function ispoint(value) {
|
|
193
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
194
|
+
const point = value;
|
|
195
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
196
|
+
}
|
|
197
|
+
function validatetargetref(reference) {
|
|
198
|
+
if (!reference || typeof reference !== "object" || Array.isArray(reference)) return { allowed: false, reason: "The reviewed target reference must be an object." };
|
|
199
|
+
const ref = reference;
|
|
200
|
+
if (ref.mode === "selector") return isnonempty(ref.selector) ? { allowed: true } : { allowed: false, reason: "The selector target reference needs a non-empty selector." };
|
|
201
|
+
if (ref.mode === "text") return isnonempty(ref.text) ? { allowed: true } : { allowed: false, reason: "The text target reference needs non-empty text." };
|
|
202
|
+
if (ref.mode === "aria") {
|
|
203
|
+
if (!isnonempty(ref.role)) return { allowed: false, reason: "The aria target reference needs a non-empty role." };
|
|
204
|
+
return isnonempty(ref.name) ? { allowed: true } : { allowed: false, reason: "The aria target reference needs a non-empty name." };
|
|
205
|
+
}
|
|
206
|
+
if (ref.mode === "name") return isnonempty(ref.name) ? { allowed: true } : { allowed: false, reason: "The name target reference needs a non-empty name." };
|
|
207
|
+
if (ref.mode === "xpath") return isnonempty(ref.xpath) ? { allowed: true } : { allowed: false, reason: "The xpath target reference needs a non-empty expression." };
|
|
208
|
+
if (ref.mode === "index") {
|
|
209
|
+
const index = ref.index;
|
|
210
|
+
return typeof index === "number" && Number.isInteger(index) && index >= 1 ? { allowed: true } : { allowed: false, reason: "The index target reference needs a positive integer map number." };
|
|
211
|
+
}
|
|
212
|
+
if (ref.mode === "point") {
|
|
213
|
+
const pointok = typeof ref.x === "number" && Number.isFinite(ref.x) && typeof ref.y === "number" && Number.isFinite(ref.y);
|
|
214
|
+
return pointok ? { allowed: true } : { allowed: false, reason: "The point target reference needs numeric x and y coordinates." };
|
|
215
|
+
}
|
|
216
|
+
return { allowed: false, reason: "The target reference mode must be selector, text, aria, name, xpath, index or point." };
|
|
217
|
+
}
|
|
218
|
+
function origingranted(session, origin) {
|
|
219
|
+
if (!session) return false;
|
|
220
|
+
const grants = session.grants ?? [session.origin];
|
|
221
|
+
return grants.includes(origin);
|
|
222
|
+
}
|
|
223
|
+
function validateinnerstep(options, origin) {
|
|
224
|
+
const stepid = options.stepid;
|
|
225
|
+
const kind = options.kind;
|
|
226
|
+
if (isnonempty(stepid)) {
|
|
227
|
+
if (kind !== void 0) return { allowed: false, reason: "The reviewed wrapper must reference a step id or an inline step, not both." };
|
|
228
|
+
return { allowed: true };
|
|
229
|
+
}
|
|
230
|
+
if (typeof kind !== "string" || !kind.trim()) return { allowed: false, reason: "A reviewed step id or inline step kind is required in options." };
|
|
231
|
+
if (kind === "retryaction" || kind === "enterframe") return { allowed: false, reason: "The reviewed inner step cannot be another wrapper kind." };
|
|
232
|
+
if (!allowedactions.has(kind)) return { allowed: false, reason: "The reviewed inner step kind is unsupported." };
|
|
233
|
+
const inneroptions = options.options;
|
|
234
|
+
if (inneroptions !== void 0 && (!inneroptions || typeof inneroptions !== "object" || Array.isArray(inneroptions))) return { allowed: false, reason: "The reviewed inner step options must be an object." };
|
|
235
|
+
const inner = {
|
|
236
|
+
id: "inner",
|
|
237
|
+
kind,
|
|
238
|
+
summary: "Reviewed inner step.",
|
|
239
|
+
risk: actionrisk(kind),
|
|
240
|
+
...isnonempty(options.target) ? { target: options.target } : {},
|
|
241
|
+
...isnonempty(options.value) ? { value: options.value } : {},
|
|
242
|
+
...inneroptions !== void 0 ? { options: JSON.stringify(inneroptions) } : {}
|
|
243
|
+
};
|
|
244
|
+
return validatestep(inner, origin);
|
|
74
245
|
}
|
|
75
246
|
function validatestep(step, origin) {
|
|
76
247
|
if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
|
|
77
248
|
if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
|
|
78
|
-
|
|
249
|
+
let options;
|
|
250
|
+
try {
|
|
251
|
+
options = parseoptions(step);
|
|
252
|
+
} catch {
|
|
253
|
+
return { allowed: false, reason: "Step options must be a JSON object." };
|
|
254
|
+
}
|
|
255
|
+
const hastargetref = options.targetref !== void 0;
|
|
256
|
+
if (targetactions.has(step.kind) && !step.target?.trim() && !hastargetref) return { allowed: false, reason: "A page target is required." };
|
|
257
|
+
if (valueactions.has(step.kind) && !step.value?.trim()) return { allowed: false, reason: "A reviewed value is required." };
|
|
79
258
|
if (step.kind === "select" && !step.value?.trim()) return { allowed: false, reason: "A reviewed option value is required." };
|
|
259
|
+
if (step.kind === "navigate" && !step.value) return { allowed: false, reason: "A navigation URL is required." };
|
|
260
|
+
if (hastargetref) {
|
|
261
|
+
const reference = validatetargetref(options.targetref);
|
|
262
|
+
if (!reference.allowed) return reference;
|
|
263
|
+
}
|
|
80
264
|
if (step.kind === "wait") {
|
|
81
265
|
try {
|
|
82
266
|
waitduration(step);
|
|
@@ -85,13 +269,99 @@ function validatestep(step, origin) {
|
|
|
85
269
|
}
|
|
86
270
|
}
|
|
87
271
|
if (step.kind === "navigate") {
|
|
88
|
-
if (!step.value) return { allowed: false, reason: "A navigation URL is required." };
|
|
89
272
|
try {
|
|
90
|
-
if (new URL(step.value).origin !== origin) return { allowed: false, reason: "Navigation must remain within the approved origin." };
|
|
273
|
+
if (new URL(step.value ?? "").origin !== origin) return { allowed: false, reason: "Navigation must remain within the approved origin." };
|
|
91
274
|
} catch {
|
|
92
275
|
return { allowed: false, reason: "Navigation URL is invalid." };
|
|
93
276
|
}
|
|
94
277
|
}
|
|
278
|
+
if (step.kind === "tabcreate" || step.kind === "windowcreate" || step.kind === "downloadfile") {
|
|
279
|
+
try {
|
|
280
|
+
const url = new URL(step.value ?? "");
|
|
281
|
+
if (url.protocol !== "https:") return { allowed: false, reason: "The reviewed URL must use HTTPS." };
|
|
282
|
+
} catch {
|
|
283
|
+
return { allowed: false, reason: "The reviewed URL is invalid." };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (step.kind === "tabactivate" || step.kind === "tabclose" || step.kind === "tabreload" || step.kind === "windowclose" || step.kind === "windowresize") {
|
|
287
|
+
if (!isnumericid(step.value)) return { allowed: false, reason: "A numeric browser id is required." };
|
|
288
|
+
}
|
|
289
|
+
if (step.kind === "zoomset") {
|
|
290
|
+
const zoom = Number(step.value);
|
|
291
|
+
if (!Number.isFinite(zoom) || zoom <= 0) return { allowed: false, reason: "The reviewed zoom must be a positive number." };
|
|
292
|
+
}
|
|
293
|
+
if (step.kind === "setattribute" || step.kind === "writestorage") {
|
|
294
|
+
const keyname = step.kind === "setattribute" ? "name" : "key";
|
|
295
|
+
if (typeof options[keyname] !== "string" || !options[keyname].trim()) return { allowed: false, reason: `A reviewed ${keyname} is required in options.` };
|
|
296
|
+
if (typeof options.value !== "string") return { allowed: false, reason: "A reviewed value is required in options." };
|
|
297
|
+
}
|
|
298
|
+
if (step.kind === "windowresize") {
|
|
299
|
+
if (typeof options.width !== "number" || typeof options.height !== "number" || !Number.isFinite(options.width) || !Number.isFinite(options.height)) return { allowed: false, reason: "Reviewed width and height numbers are required in options." };
|
|
300
|
+
}
|
|
301
|
+
if ((step.kind === "scrollpage" || step.kind === "scrollby") && (!numericoption(options, "x") || !numericoption(options, "y"))) return { allowed: false, reason: "Scroll amounts must be numbers in options." };
|
|
302
|
+
if (step.kind === "waitfor" && options.timeout !== void 0 && (typeof options.timeout !== "number" || options.timeout < 0)) return { allowed: false, reason: "The waitfor timeout must be zero or a positive number of milliseconds." };
|
|
303
|
+
if (step.kind === "movepointer") {
|
|
304
|
+
const path = options.pointpath;
|
|
305
|
+
if (!path || typeof path !== "object" || Array.isArray(path)) return { allowed: false, reason: "A reviewed pointpath with start and end points is required in options." };
|
|
306
|
+
const points = path;
|
|
307
|
+
if (!ispoint(points.start) || !ispoint(points.end)) return { allowed: false, reason: "The reviewed pointpath needs numeric start and end points." };
|
|
308
|
+
if (points.waypoints !== void 0 && (!Array.isArray(points.waypoints) || !points.waypoints.every((waypoint) => ispoint(waypoint)))) return { allowed: false, reason: "The reviewed pointpath waypoints must be numeric points." };
|
|
309
|
+
if (!nonnegativeoption(points, "duration")) return { allowed: false, reason: "The reviewed pointpath duration must be zero or a positive number of milliseconds." };
|
|
310
|
+
const speed = options.speedprofile;
|
|
311
|
+
if (speed !== void 0) {
|
|
312
|
+
if (!speed || typeof speed !== "object" || Array.isArray(speed)) return { allowed: false, reason: "The reviewed speed profile must be an object." };
|
|
313
|
+
const profile = speed;
|
|
314
|
+
if (profile.easing !== void 0 && profile.easing !== "linear" && profile.easing !== "easeinout") return { allowed: false, reason: "The reviewed easing must be linear or easeinout." };
|
|
315
|
+
if (!nonnegativeoption(profile, "peak")) return { allowed: false, reason: "The reviewed peak velocity must be zero or a positive number." };
|
|
316
|
+
if (!nonnegativeoption(profile, "jitter")) return { allowed: false, reason: "The reviewed jitter window must be zero or a positive number of milliseconds." };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (step.kind === "clickpoint" && (!hastargetref || options.targetref.mode !== "point")) return { allowed: false, reason: "A reviewed point target reference is required in options." };
|
|
320
|
+
if (step.kind === "clicktext" && (!hastargetref || options.targetref.mode !== "text")) return { allowed: false, reason: "A reviewed text target reference is required in options." };
|
|
321
|
+
if (step.kind === "clickaria" && (!hastargetref || options.targetref.mode !== "aria")) return { allowed: false, reason: "A reviewed aria target reference is required in options." };
|
|
322
|
+
if (step.kind === "clickname" && (!hastargetref || options.targetref.mode !== "name")) return { allowed: false, reason: "A reviewed name target reference is required in options." };
|
|
323
|
+
if (step.kind === "resolvexpath" && (!hastargetref || options.targetref.mode !== "xpath")) return { allowed: false, reason: "A reviewed xpath target reference is required in options." };
|
|
324
|
+
if (step.kind === "typetime" && options.delay !== void 0 && (typeof options.delay !== "number" || !Number.isFinite(options.delay) || options.delay < 0)) return { allowed: false, reason: "The reviewed per keystroke delay must be zero or a positive number of milliseconds." };
|
|
325
|
+
if (step.kind === "submitsearch") {
|
|
326
|
+
if (!isnonempty(options.results)) return { allowed: false, reason: "A reviewed results region selector is required in options." };
|
|
327
|
+
if (options.timeout !== void 0 && (typeof options.timeout !== "number" || !Number.isFinite(options.timeout) || options.timeout < 0)) return { allowed: false, reason: "The submitsearch timeout must be zero or a positive number of milliseconds." };
|
|
328
|
+
}
|
|
329
|
+
if (step.kind === "selectmulti") {
|
|
330
|
+
const values = options.values;
|
|
331
|
+
if (!Array.isArray(values) || values.length === 0 || !values.every((value) => isnonempty(value))) return { allowed: false, reason: "A reviewed list of option values is required in options." };
|
|
332
|
+
}
|
|
333
|
+
if (step.kind === "setslider") {
|
|
334
|
+
const slider = Number(step.value);
|
|
335
|
+
if (!Number.isFinite(slider)) return { allowed: false, reason: "The reviewed slider value must be a number." };
|
|
336
|
+
}
|
|
337
|
+
if (step.kind === "setdate" && !/^\d{4}-\d{2}-\d{2}$/.test(step.value ?? "")) return { allowed: false, reason: "The reviewed date must use the yyyy-mm-dd form." };
|
|
338
|
+
if (step.kind === "setcolor" && !/^#[0-9a-fA-F]{6}$/.test(step.value ?? "")) return { allowed: false, reason: "The reviewed color must use the #rrggbb form." };
|
|
339
|
+
if (step.kind === "keyhold" && options.holdid !== void 0 && !isnonempty(options.holdid)) return { allowed: false, reason: "The reviewed hold id must be a non-empty string." };
|
|
340
|
+
if (step.kind === "dismissdialog") {
|
|
341
|
+
const accept = options.accept;
|
|
342
|
+
const answer = options.answer;
|
|
343
|
+
if (accept === void 0 && !isnonempty(answer)) return { allowed: false, reason: "A reviewed accept flag or prompt answer is required in options." };
|
|
344
|
+
if (accept !== void 0 && typeof accept !== "boolean") return { allowed: false, reason: "The reviewed dialog accept flag must be a boolean." };
|
|
345
|
+
if (answer !== void 0 && !isnonempty(answer)) return { allowed: false, reason: "The reviewed prompt answer must be a non-empty string." };
|
|
346
|
+
}
|
|
347
|
+
if (step.kind === "pierceshadow" && options.shadow !== void 0) {
|
|
348
|
+
if (!Array.isArray(options.shadow) || !options.shadow.every((item) => isnonempty(item))) return { allowed: false, reason: "The reviewed shadow path must be a list of non-empty selectors." };
|
|
349
|
+
}
|
|
350
|
+
if (step.kind === "enterframe") {
|
|
351
|
+
const path = options.framepath;
|
|
352
|
+
if (!Array.isArray(path) || path.length === 0 || !path.every((item) => typeof item === "number" && Number.isInteger(item) && item >= 0)) return { allowed: false, reason: "A reviewed frame path of frame indexes is required in options." };
|
|
353
|
+
return validateinnerstep(options, origin);
|
|
354
|
+
}
|
|
355
|
+
if (step.kind === "retryaction") {
|
|
356
|
+
const inner = validateinnerstep(options, origin);
|
|
357
|
+
if (!inner.allowed) return inner;
|
|
358
|
+
const rule = options.retryrule;
|
|
359
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) return { allowed: false, reason: "A reviewed retry rule with attempts is required in options." };
|
|
360
|
+
const retry = rule;
|
|
361
|
+
if (typeof retry.attempts !== "number" || !Number.isInteger(retry.attempts) || retry.attempts < 1) return { allowed: false, reason: "The reviewed retry attempts must be a positive integer with no code ceiling." };
|
|
362
|
+
if (!nonnegativeoption(retry, "settle")) return { allowed: false, reason: "The reviewed retry settle window must be zero or a positive number of milliseconds." };
|
|
363
|
+
if (!nonnegativeoption(retry, "tolerance")) return { allowed: false, reason: "The reviewed retry movement tolerance must be zero or a positive number of pixels." };
|
|
364
|
+
}
|
|
95
365
|
return { allowed: true };
|
|
96
366
|
}
|
|
97
367
|
function sessiongate(input) {
|
|
@@ -107,6 +377,7 @@ function canexecute(input) {
|
|
|
107
377
|
if (!gate.allowed) return gate;
|
|
108
378
|
if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "The plan has not received explicit approval." };
|
|
109
379
|
if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
|
|
380
|
+
if ((input.step.kind === "pierceshadow" || input.step.kind === "enterframe") && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The shadow or frame step is outside the session origin grants." };
|
|
110
381
|
return validatestep(input.step, input.origin);
|
|
111
382
|
}
|
|
112
383
|
function canpreview(input) {
|
|
@@ -115,7 +386,13 @@ function canpreview(input) {
|
|
|
115
386
|
if (!gate.allowed) return gate;
|
|
116
387
|
if (!input.plan || !["pending", "approved"].includes(input.plan.state)) return { allowed: false, reason: "Only a reviewed pending or approved plan can be previewed." };
|
|
117
388
|
if (input.plan.expiresat <= now) return { allowed: false, reason: "The reviewed plan has expired." };
|
|
118
|
-
|
|
389
|
+
let options = {};
|
|
390
|
+
try {
|
|
391
|
+
options = parseoptions(input.step);
|
|
392
|
+
} catch {
|
|
393
|
+
options = {};
|
|
394
|
+
}
|
|
395
|
+
if (!targetactions.has(input.step.kind) && options.targetref === void 0) return { allowed: false, reason: "Only a target-based action can be previewed." };
|
|
119
396
|
return validatestep(input.step, input.origin);
|
|
120
397
|
}
|
|
121
398
|
|
|
@@ -128,6 +405,10 @@ function recordstep(progress, planid, stepid, now) {
|
|
|
128
405
|
if (base.completedsteps.includes(stepid)) return { ...base, updatedat: now };
|
|
129
406
|
return { planid, completedsteps: [...base.completedsteps, stepid], updatedat: now };
|
|
130
407
|
}
|
|
408
|
+
function recordoutcome(progress, planid, outcome, now) {
|
|
409
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
410
|
+
return { ...base, outcomes: [...base.outcomes ?? [], outcome], updatedat: now };
|
|
411
|
+
}
|
|
131
412
|
function iscomplete(progress, plan) {
|
|
132
413
|
if (!progress || progress.planid !== plan.id) return false;
|
|
133
414
|
const required = plan.steps.map((step) => step.id);
|
|
@@ -135,11 +416,13 @@ function iscomplete(progress, plan) {
|
|
|
135
416
|
}
|
|
136
417
|
function resetforplan(progress, plan, now) {
|
|
137
418
|
if (progress && progress.planid === plan.id) return progress;
|
|
138
|
-
return emptyprogress(plan.id, now);
|
|
419
|
+
if (!progress) return emptyprogress(plan.id, now);
|
|
420
|
+
const snapshot2 = { planid: progress.planid, completedsteps: progress.completedsteps, ...progress.outcomes ? { outcomes: progress.outcomes } : {}, updatedat: progress.updatedat };
|
|
421
|
+
return { planid: plan.id, completedsteps: [], outcomes: [], prior: [...progress.prior ?? [], snapshot2], updatedat: now };
|
|
139
422
|
}
|
|
140
423
|
|
|
141
424
|
// version.ts
|
|
142
|
-
var packageversion = "1.1.
|
|
425
|
+
var packageversion = "1.1.33";
|
|
143
426
|
|
|
144
427
|
// types.ts
|
|
145
428
|
var protocolversion = packageversion;
|
|
@@ -158,7 +441,7 @@ function parseproposal(value, origin) {
|
|
|
158
441
|
if (root.version !== protocolversion) throw new Error("Unsupported protocol version.");
|
|
159
442
|
const planinput = record(root.plan);
|
|
160
443
|
const stepsinput = planinput.steps;
|
|
161
|
-
if (!Array.isArray(stepsinput) || stepsinput.length === 0
|
|
444
|
+
if (!Array.isArray(stepsinput) || stepsinput.length === 0) throw new Error("A plan needs at least one step.");
|
|
162
445
|
const steps = stepsinput.map((input, index) => {
|
|
163
446
|
const candidate = record(input);
|
|
164
447
|
const kind = text(candidate.kind, `step ${index + 1} kind`);
|
|
@@ -168,31 +451,265 @@ function parseproposal(value, origin) {
|
|
|
168
451
|
summary: text(candidate.summary, `step ${index + 1} summary`),
|
|
169
452
|
risk: actionrisk(kind),
|
|
170
453
|
...typeof candidate.target === "string" ? { target: candidate.target } : {},
|
|
171
|
-
...typeof candidate.value === "string" ? { value: candidate.value } : {}
|
|
454
|
+
...typeof candidate.value === "string" ? { value: candidate.value } : {},
|
|
455
|
+
...typeof candidate.options === "string" ? { options: candidate.options } : {}
|
|
172
456
|
};
|
|
173
457
|
const evaluation = validatestep(step, origin);
|
|
174
458
|
if (!evaluation.allowed) throw new Error(evaluation.reason);
|
|
175
459
|
return step;
|
|
176
460
|
});
|
|
461
|
+
for (const step of steps) {
|
|
462
|
+
if (step.kind !== "retryaction" && step.kind !== "enterframe") continue;
|
|
463
|
+
const options = parseoptions(step);
|
|
464
|
+
if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry or frame wrapper references an unknown step id.");
|
|
465
|
+
}
|
|
177
466
|
const createdat = Date.now();
|
|
467
|
+
const expiresat = typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3;
|
|
178
468
|
const plan = {
|
|
179
469
|
id: typeof planinput.id === "string" ? planinput.id : crypto.randomUUID(),
|
|
180
470
|
objective: text(planinput.objective, "objective"),
|
|
181
471
|
origin,
|
|
182
472
|
steps,
|
|
183
473
|
createdat,
|
|
184
|
-
expiresat
|
|
474
|
+
expiresat,
|
|
185
475
|
state: "pending"
|
|
186
476
|
};
|
|
187
477
|
if (plan.expiresat <= createdat) throw new Error("Plan expiry must be in the future.");
|
|
188
478
|
return { version: protocolversion, plan };
|
|
189
479
|
}
|
|
190
480
|
function requestbody(input) {
|
|
191
|
-
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation });
|
|
481
|
+
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
482
|
+
}
|
|
483
|
+
function outcomeresponse(input) {
|
|
484
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {} });
|
|
485
|
+
}
|
|
486
|
+
function mapresponse(input) {
|
|
487
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
|
|
488
|
+
}
|
|
489
|
+
function heldkeysreport(input) {
|
|
490
|
+
return { version: protocolversion, tabid: input.tabid, heldkeys: input.holds };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// extension/browsertabs.ts
|
|
494
|
+
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
495
|
+
function isbrowserkind(kind) {
|
|
496
|
+
return browserkinds.has(kind);
|
|
497
|
+
}
|
|
498
|
+
async function readcapabilities() {
|
|
499
|
+
const [tabs, downloads, clipboardread, clipboardwrite] = await Promise.all([
|
|
500
|
+
chrome.permissions.contains({ permissions: ["tabs"] }),
|
|
501
|
+
chrome.permissions.contains({ permissions: ["downloads"] }),
|
|
502
|
+
chrome.permissions.contains({ permissions: ["clipboardRead"] }),
|
|
503
|
+
chrome.permissions.contains({ permissions: ["clipboardWrite"] })
|
|
504
|
+
]);
|
|
505
|
+
return { tabs, downloads, clipboardread, clipboardwrite, reportedat: Date.now() };
|
|
506
|
+
}
|
|
507
|
+
function stepoptions(step) {
|
|
508
|
+
if (!step.options) return {};
|
|
509
|
+
try {
|
|
510
|
+
const parsed = JSON.parse(step.options);
|
|
511
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
512
|
+
} catch {
|
|
513
|
+
return {};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function tabid(step) {
|
|
517
|
+
return Number.parseInt(step.value ?? "", 10);
|
|
518
|
+
}
|
|
519
|
+
async function runbrowseraction(step, sessiontabid, windowid) {
|
|
520
|
+
const options = stepoptions(step);
|
|
521
|
+
switch (step.kind) {
|
|
522
|
+
case "tablist": {
|
|
523
|
+
const tabs = await chrome.tabs.query({});
|
|
524
|
+
return { ok: true, summary: `Listed ${tabs.length} open tab${tabs.length === 1 ? "" : "s"}.`, details: { tabs: tabs.map((tab) => ({ id: tab.id ?? 0, index: tab.index, title: tab.title ?? "", url: tab.url ?? "", active: tab.active, pinned: tab.pinned, audible: tab.audible ?? false })) } };
|
|
525
|
+
}
|
|
526
|
+
case "tabcreate": {
|
|
527
|
+
const created = await chrome.tabs.create({ url: step.value, active: options.active !== false, pinned: options.pinned === true });
|
|
528
|
+
return { ok: true, summary: `Opened a new tab for ${step.value}.`, details: { tabid: created?.id ?? 0 } };
|
|
529
|
+
}
|
|
530
|
+
case "tabactivate": {
|
|
531
|
+
await chrome.tabs.update(tabid(step), { active: true });
|
|
532
|
+
return { ok: true, summary: `Activated tab ${tabid(step)}.` };
|
|
533
|
+
}
|
|
534
|
+
case "tabclose": {
|
|
535
|
+
await chrome.tabs.remove(tabid(step));
|
|
536
|
+
return { ok: true, summary: `Closed tab ${tabid(step)}.` };
|
|
537
|
+
}
|
|
538
|
+
case "tabreload": {
|
|
539
|
+
await chrome.tabs.reload(tabid(step), { bypassCache: options.bypasscache === true });
|
|
540
|
+
return { ok: true, summary: `Reloaded tab ${tabid(step)}.` };
|
|
541
|
+
}
|
|
542
|
+
case "tabsnapshot": {
|
|
543
|
+
const shot = await chrome.tabs.captureVisibleTab(windowid, { format: "png" });
|
|
544
|
+
return { ok: true, summary: "Captured the visible area of the active tab.", details: { shot } };
|
|
545
|
+
}
|
|
546
|
+
case "windowlist": {
|
|
547
|
+
const windows = await chrome.windows.getAll();
|
|
548
|
+
return { ok: true, summary: `Listed ${windows.length} open window${windows.length === 1 ? "" : "s"}.`, details: { windows: windows.map((item) => ({ id: item.id ?? 0, type: item.type, state: item.state ?? "", focused: item.focused })) } };
|
|
549
|
+
}
|
|
550
|
+
case "windowcreate": {
|
|
551
|
+
const created = await chrome.windows.create({ url: step.value ?? "about:blank", ...typeof options.width === "number" ? { width: options.width } : {}, ...typeof options.height === "number" ? { height: options.height } : {} });
|
|
552
|
+
return { ok: true, summary: `Opened a new window for ${step.value}.`, details: { windowid: created?.id ?? 0 } };
|
|
553
|
+
}
|
|
554
|
+
case "windowclose": {
|
|
555
|
+
await chrome.windows.remove(tabid(step));
|
|
556
|
+
return { ok: true, summary: `Closed window ${tabid(step)}.` };
|
|
557
|
+
}
|
|
558
|
+
case "zoomset": {
|
|
559
|
+
const zoom = Number(step.value);
|
|
560
|
+
await chrome.tabs.setZoom(sessiontabid, zoom);
|
|
561
|
+
return { ok: true, summary: `Set the tab zoom to ${zoom}.` };
|
|
562
|
+
}
|
|
563
|
+
case "windowresize": {
|
|
564
|
+
await chrome.windows.update(tabid(step), { width: options.width, height: options.height });
|
|
565
|
+
return { ok: true, summary: `Resized window ${tabid(step)}.` };
|
|
566
|
+
}
|
|
567
|
+
case "downloadfile": {
|
|
568
|
+
const downloadid = await chrome.downloads.download({ url: step.value ?? "" });
|
|
569
|
+
return { ok: true, summary: `Started the download of ${step.value}.`, details: { downloadid } };
|
|
570
|
+
}
|
|
571
|
+
default:
|
|
572
|
+
return { ok: false, summary: "Unsupported browser action." };
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// extension/pagedialogs.ts
|
|
577
|
+
function parsedialogpolicy(step) {
|
|
578
|
+
let options = {};
|
|
579
|
+
try {
|
|
580
|
+
options = parseoptions(step);
|
|
581
|
+
} catch {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
const accept = options.accept;
|
|
585
|
+
const answer = options.answer;
|
|
586
|
+
if (typeof accept !== "boolean" && typeof answer !== "string") return null;
|
|
587
|
+
return { accept: accept === true, ...typeof answer === "string" && answer.trim() ? { answer } : {} };
|
|
588
|
+
}
|
|
589
|
+
function dialoganswer(policy, dialog) {
|
|
590
|
+
if (dialog === "prompt") {
|
|
591
|
+
if (policy.answer === void 0 || policy.answer === "") return { accept: false };
|
|
592
|
+
return { accept: policy.accept !== false, ...policy.answer !== void 0 ? { answer: policy.answer } : {} };
|
|
593
|
+
}
|
|
594
|
+
return { accept: policy.accept };
|
|
595
|
+
}
|
|
596
|
+
function installdialoghandler(accept, answer, persistent) {
|
|
597
|
+
const world = globalThis;
|
|
598
|
+
const originals = world.devthinkoriginaldialogs ?? { confirm: window.confirm.bind(window), alert: window.alert.bind(window), prompt: window.prompt.bind(window) };
|
|
599
|
+
world.devthinkoriginaldialogs = originals;
|
|
600
|
+
const decide = (dialog) => {
|
|
601
|
+
if (dialog === "prompt") return answer ? { accept: true, answer } : { accept: false, answer: null };
|
|
602
|
+
return { accept, answer: null };
|
|
603
|
+
};
|
|
604
|
+
const record2 = (dialog, text2, result) => {
|
|
605
|
+
try {
|
|
606
|
+
const root = document.documentElement;
|
|
607
|
+
const log = JSON.parse(root.dataset.devthinkdialoglog ?? "[]");
|
|
608
|
+
log.push({ dialog, text: text2, result, at: Date.now() });
|
|
609
|
+
root.dataset.devthinkdialoglog = JSON.stringify(log);
|
|
610
|
+
} catch {
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
window.confirm = (text2) => {
|
|
614
|
+
const decision = decide("confirm");
|
|
615
|
+
record2("confirm", text2 ?? "", decision.accept);
|
|
616
|
+
if (!persistent) window.confirm = originals.confirm;
|
|
617
|
+
return decision.accept;
|
|
618
|
+
};
|
|
619
|
+
window.alert = (text2) => {
|
|
620
|
+
record2("alert", text2 ?? "", true);
|
|
621
|
+
if (!persistent) window.alert = originals.alert;
|
|
622
|
+
};
|
|
623
|
+
window.prompt = (text2, defaultvalue) => {
|
|
624
|
+
const decision = decide("prompt");
|
|
625
|
+
const outcome = decision.accept ? decision.answer ?? defaultvalue ?? "" : null;
|
|
626
|
+
record2("prompt", text2 ?? "", outcome);
|
|
627
|
+
if (!persistent) window.prompt = originals.prompt;
|
|
628
|
+
return outcome;
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// extension/pageinteract.ts
|
|
633
|
+
function optionsof(step) {
|
|
634
|
+
try {
|
|
635
|
+
return parseoptions(step);
|
|
636
|
+
} catch {
|
|
637
|
+
return {};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function innerstep(step) {
|
|
641
|
+
const options = optionsof(step);
|
|
642
|
+
const kind = options.kind;
|
|
643
|
+
if (typeof kind !== "string" || !kind.trim()) return null;
|
|
644
|
+
const inneroptions = options.options;
|
|
645
|
+
return {
|
|
646
|
+
id: `${step.id}inner`,
|
|
647
|
+
kind,
|
|
648
|
+
summary: step.summary,
|
|
649
|
+
risk: step.risk,
|
|
650
|
+
...typeof options.target === "string" ? { target: options.target } : {},
|
|
651
|
+
...typeof options.value === "string" ? { value: options.value } : {},
|
|
652
|
+
...inneroptions && typeof inneroptions === "object" && !Array.isArray(inneroptions) ? { options: JSON.stringify(inneroptions) } : {}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function pointdistance(a, b) {
|
|
656
|
+
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
657
|
+
}
|
|
658
|
+
function settle(delay) {
|
|
659
|
+
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
660
|
+
}
|
|
661
|
+
async function runretries(rule, probe, execute) {
|
|
662
|
+
const attempts = Math.max(1, Number.isFinite(rule.attempts) ? Math.floor(rule.attempts) : 1);
|
|
663
|
+
const tolerance = typeof rule.tolerance === "number" && Number.isFinite(rule.tolerance) ? rule.tolerance : 0;
|
|
664
|
+
const settles = typeof rule.settle === "number" && Number.isFinite(rule.settle) ? rule.settle : 0;
|
|
665
|
+
let previous = await probe();
|
|
666
|
+
let movement = 0;
|
|
667
|
+
let made = 0;
|
|
668
|
+
let last = "";
|
|
669
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
670
|
+
made = attempt;
|
|
671
|
+
const result = await execute(attempt);
|
|
672
|
+
last = result.summary;
|
|
673
|
+
if (result.ok) return { ok: true, attempts: made, movement, summary: `Retry interaction succeeded on attempt ${made} after ${movement.toFixed(1)} pixels of observed movement.` };
|
|
674
|
+
if (attempt >= attempts) break;
|
|
675
|
+
if (settles > 0) await settle(settles);
|
|
676
|
+
const current = await probe();
|
|
677
|
+
if (!current) {
|
|
678
|
+
previous = null;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (previous) {
|
|
682
|
+
const delta = pointdistance(previous, current);
|
|
683
|
+
movement = Math.max(movement, delta);
|
|
684
|
+
if (delta <= tolerance) return { ok: false, attempts: made, movement, summary: `The target stayed within the reviewed tolerance of ${tolerance} pixels; retry stopped after attempt ${made}. ${last}` };
|
|
685
|
+
}
|
|
686
|
+
previous = current;
|
|
687
|
+
}
|
|
688
|
+
return { ok: false, attempts: made, movement, summary: `Retry interaction failed after ${made} attempt${made === 1 ? "" : "s"} with ${movement.toFixed(1)} pixels of observed movement. ${last}` };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// extension/pagecontrols.ts
|
|
692
|
+
function presshold(holds, hold) {
|
|
693
|
+
if (holds.some((existing) => existing.holdid === hold.holdid && existing.releasedat === void 0)) return { holds, ok: false };
|
|
694
|
+
return { holds: [...holds, hold], ok: true };
|
|
695
|
+
}
|
|
696
|
+
function releasehold(holds, holdid, releasedat) {
|
|
697
|
+
let released;
|
|
698
|
+
const next = holds.map((hold) => {
|
|
699
|
+
if (hold.holdid !== holdid || hold.releasedat !== void 0) return hold;
|
|
700
|
+
released = { ...hold, releasedat };
|
|
701
|
+
return released;
|
|
702
|
+
});
|
|
703
|
+
return { holds: next, ...released ? { released } : {} };
|
|
704
|
+
}
|
|
705
|
+
function heldkeys(holds, tabid2) {
|
|
706
|
+
return holds.filter((hold) => hold.releasedat === void 0 && (tabid2 === void 0 || hold.tabid === void 0 || hold.tabid === tabid2));
|
|
192
707
|
}
|
|
193
708
|
|
|
194
709
|
// extension/background.ts
|
|
195
710
|
var sessionduration = 15 * 60 * 1e3;
|
|
711
|
+
var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
|
|
712
|
+
var pointerkinds = /* @__PURE__ */ new Set(["movepointer", "clickpoint", "shiftclick", "clicktext", "clickaria", "clickname", "pierceshadow"]);
|
|
196
713
|
var chromestorage = {
|
|
197
714
|
async get(key) {
|
|
198
715
|
return (await chrome.storage.local.get(key))[key];
|
|
@@ -208,6 +725,18 @@ function extensionpage(sender) {
|
|
|
208
725
|
async function audit(kind, summary, extra = {}) {
|
|
209
726
|
await memory.addaudi({ id: randomid(), kind, at: Date.now(), summary, ...extra });
|
|
210
727
|
}
|
|
728
|
+
function stepoptions2(step) {
|
|
729
|
+
try {
|
|
730
|
+
return parseoptions(step);
|
|
731
|
+
} catch {
|
|
732
|
+
return {};
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
async function refreshcapabilities() {
|
|
736
|
+
const report = await readcapabilities();
|
|
737
|
+
await memory.setcapabilities(report);
|
|
738
|
+
return report;
|
|
739
|
+
}
|
|
211
740
|
async function activecontext() {
|
|
212
741
|
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
213
742
|
if (!tab?.id || !tab.url) throw new Error("No active web tab is available.");
|
|
@@ -215,9 +744,9 @@ async function activecontext() {
|
|
|
215
744
|
if (!origin.startsWith("https://")) throw new Error("Devthink can work with HTTPS pages only.");
|
|
216
745
|
return { tab, origin };
|
|
217
746
|
}
|
|
218
|
-
async function snapshot(
|
|
219
|
-
await chrome.scripting.executeScript({ target: { tabId:
|
|
220
|
-
const result = await chrome.scripting.executeScript({ target: { tabId:
|
|
747
|
+
async function snapshot(tabid2) {
|
|
748
|
+
await chrome.scripting.executeScript({ target: { tabId: tabid2 }, files: ["pagebridge.js"] });
|
|
749
|
+
const result = await chrome.scripting.executeScript({ target: { tabId: tabid2 }, func: () => {
|
|
221
750
|
const bridge = globalThis.devthinkbridge;
|
|
222
751
|
if (!bridge) throw new Error("Devthink page bridge is unavailable.");
|
|
223
752
|
return bridge.capturesnapshot();
|
|
@@ -226,11 +755,77 @@ async function snapshot(tabid) {
|
|
|
226
755
|
if (!value) throw new Error("The page did not return an observation.");
|
|
227
756
|
return value;
|
|
228
757
|
}
|
|
758
|
+
function plandialogpolicy(plan) {
|
|
759
|
+
const step = plan?.steps.find((candidate) => candidate.kind === "dismissdialog");
|
|
760
|
+
return step ? parsedialogpolicy(step) : null;
|
|
761
|
+
}
|
|
762
|
+
async function installdialogpolicy(tabid2, policy, persistent) {
|
|
763
|
+
await chrome.scripting.executeScript({
|
|
764
|
+
target: { tabId: tabid2 },
|
|
765
|
+
world: "MAIN",
|
|
766
|
+
func: installdialoghandler,
|
|
767
|
+
args: [policy.accept, policy.answer ?? "", persistent]
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
async function ensuredialoghandler(plan, tabid2) {
|
|
771
|
+
const policy = plandialogpolicy(plan) ?? await memory.getdialogpolicy();
|
|
772
|
+
if (!policy) return;
|
|
773
|
+
try {
|
|
774
|
+
await installdialogpolicy(tabid2, policy, true);
|
|
775
|
+
} catch {
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async function harvestdialogs(session, plan, stepid, tabid2) {
|
|
779
|
+
const policy = plandialogpolicy(plan) ?? await memory.getdialogpolicy();
|
|
780
|
+
let observed = [];
|
|
781
|
+
try {
|
|
782
|
+
const result = await chrome.scripting.executeScript({ target: { tabId: tabid2 }, func: () => {
|
|
783
|
+
const bridge = globalThis.devthinkbridge;
|
|
784
|
+
return bridge?.readdialogs ? bridge.readdialogs() : [];
|
|
785
|
+
} });
|
|
786
|
+
const value = result[0]?.result;
|
|
787
|
+
if (Array.isArray(value)) observed = value;
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
for (const entry of observed) {
|
|
791
|
+
const reviewed = policy ? dialoganswer(policy, ["confirm", "alert", "prompt"].includes(entry.dialog) ? entry.dialog : "confirm") : void 0;
|
|
792
|
+
const decision = {
|
|
793
|
+
id: randomid(),
|
|
794
|
+
dialog: entry.dialog,
|
|
795
|
+
text: entry.text,
|
|
796
|
+
accept: reviewed ? reviewed.accept : entry.result !== null && entry.result !== false,
|
|
797
|
+
...reviewed?.answer !== void 0 ? { answer: reviewed.answer } : {},
|
|
798
|
+
...session ? { sessionid: session.id } : {},
|
|
799
|
+
at: Number.isFinite(entry.at) ? entry.at : Date.now()
|
|
800
|
+
};
|
|
801
|
+
await memory.adddialog(decision);
|
|
802
|
+
await audit("dialog", `Dialog ${decision.dialog} answered ${decision.accept ? "accept" : "dismiss"}${decision.answer ? ` with the reviewed answer "${decision.answer}"` : ""} for dialog text "${decision.text.slice(0, 120)}".`, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
async function dispatchpagestep(step, tabid2, origin, plan) {
|
|
806
|
+
const session = await memory.getsession();
|
|
807
|
+
const activeplan = plan ?? await memory.getplan();
|
|
808
|
+
await ensuredialoghandler(activeplan, tabid2);
|
|
809
|
+
const result = await chrome.scripting.executeScript({ target: { tabId: tabid2 }, func: (action, expectedorigin) => {
|
|
810
|
+
const bridge = globalThis.devthinkbridge;
|
|
811
|
+
if (!bridge) throw new Error("Devthink page bridge is unavailable.");
|
|
812
|
+
return bridge.performstep(action, expectedorigin);
|
|
813
|
+
}, args: [step, origin] });
|
|
814
|
+
await harvestdialogs(session, activeplan, step.id, tabid2);
|
|
815
|
+
return result[0]?.result;
|
|
816
|
+
}
|
|
229
817
|
async function startsession() {
|
|
230
818
|
const { tab, origin } = await activecontext();
|
|
231
|
-
const session = { id: randomid(), tabid: tab.id, origin, startedat: Date.now(), expiresat: Date.now() + sessionduration };
|
|
819
|
+
const session = { id: randomid(), tabid: tab.id, origin, startedat: Date.now(), expiresat: Date.now() + sessionduration, grants: [origin] };
|
|
232
820
|
await memory.setsession(session);
|
|
233
821
|
await audit("session", `Session started for ${origin}.`, { sessionid: session.id });
|
|
822
|
+
const policy = await memory.getdialogpolicy();
|
|
823
|
+
if (policy) {
|
|
824
|
+
try {
|
|
825
|
+
await installdialogpolicy(session.tabid, policy, true);
|
|
826
|
+
} catch {
|
|
827
|
+
}
|
|
828
|
+
}
|
|
234
829
|
return session;
|
|
235
830
|
}
|
|
236
831
|
async function diagnostic() {
|
|
@@ -245,7 +840,7 @@ async function diagnostic() {
|
|
|
245
840
|
}
|
|
246
841
|
function localplan(objective, session) {
|
|
247
842
|
const now = Date.now();
|
|
248
|
-
return { id: randomid(), objective, origin: session.origin, steps: [{ id: randomid(), kind: "observe", summary: "Capture a
|
|
843
|
+
return { id: randomid(), objective, origin: session.origin, steps: [{ id: randomid(), kind: "observe", summary: "Capture a complete semantic snapshot of the approved active tab.", risk: "read" }], createdat: now, expiresat: now + sessionduration, state: "pending" };
|
|
249
844
|
}
|
|
250
845
|
async function propose(objective, remote) {
|
|
251
846
|
if (!objective.trim()) throw new Error("An objective is required.");
|
|
@@ -254,11 +849,12 @@ async function propose(objective, remote) {
|
|
|
254
849
|
const { tab, origin } = await activecontext();
|
|
255
850
|
if (session.tabid !== tab.id || session.origin !== origin) throw new Error("The selected tab or origin no longer matches the approved session.");
|
|
256
851
|
const observation = await snapshot(session.tabid);
|
|
852
|
+
const capabilities = await refreshcapabilities();
|
|
257
853
|
const config = await memory.getconfig();
|
|
258
854
|
let plan = localplan(objective.trim(), session);
|
|
259
855
|
if (remote) {
|
|
260
856
|
if (!config) throw new Error("Configure an approved HTTPS endpoint before requesting a remote proposal.");
|
|
261
|
-
const response = await fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, credentials: "omit", body: requestbody({ objective: objective.trim(), session, observation }) });
|
|
857
|
+
const response = await fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, credentials: "omit", body: requestbody({ objective: objective.trim(), session, observation, capabilities }) });
|
|
262
858
|
if (!response.ok) throw new Error(`Proposal endpoint returned ${response.status}.`);
|
|
263
859
|
plan = parseproposal(await response.json(), session.origin).plan;
|
|
264
860
|
}
|
|
@@ -267,6 +863,103 @@ async function propose(objective, remote) {
|
|
|
267
863
|
await audit("proposal", `Plan proposed with ${plan.steps.length} reviewed step${plan.steps.length === 1 ? "" : "s"}.`, { sessionid: session.id, planid: plan.id });
|
|
268
864
|
return plan;
|
|
269
865
|
}
|
|
866
|
+
function browserauditkind(step) {
|
|
867
|
+
if (step.kind === "downloadfile") return "download";
|
|
868
|
+
if (step.kind.startsWith("window")) return "window";
|
|
869
|
+
return "tab";
|
|
870
|
+
}
|
|
871
|
+
function stepauditkind(step, ok) {
|
|
872
|
+
if (isbrowserkind(step.kind)) return browserauditkind(step);
|
|
873
|
+
if (step.kind === "dismissdialog") return "dialog";
|
|
874
|
+
if (step.kind === "keyhold" || step.kind === "keyrelease") return "hold";
|
|
875
|
+
if (step.kind === "retryaction") return "retry";
|
|
876
|
+
if (pointerkinds.has(step.kind)) return "pointer";
|
|
877
|
+
return ok ? "action" : "error";
|
|
878
|
+
}
|
|
879
|
+
function resolvedinnerstep(step, plan) {
|
|
880
|
+
const options = stepoptions2(step);
|
|
881
|
+
if (typeof options.stepid === "string" && options.stepid.trim()) {
|
|
882
|
+
return plan.steps.find((candidate) => candidate.id === options.stepid) ?? null;
|
|
883
|
+
}
|
|
884
|
+
return innerstep(step);
|
|
885
|
+
}
|
|
886
|
+
async function executekeyhold(step, session, plan, tabid2, origin) {
|
|
887
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan);
|
|
888
|
+
if (!output?.ok) return output ?? { ok: false, summary: "The key hold was not delivered." };
|
|
889
|
+
const options = stepoptions2(step);
|
|
890
|
+
const holdid = typeof options.holdid === "string" && options.holdid.trim() ? options.holdid : randomid();
|
|
891
|
+
const modifiers = Array.isArray(options.modifiers) ? options.modifiers.filter((item) => typeof item === "string") : [];
|
|
892
|
+
const hold = { holdid, key: step.value ?? "", ...modifiers.length > 0 ? { modifiers } : {}, tabid: tabid2, stepid: step.id, pressedat: Date.now() };
|
|
893
|
+
const next = presshold(await memory.getholds(), hold);
|
|
894
|
+
if (!next.ok) return { ok: false, summary: `Hold id ${holdid} is already held.` };
|
|
895
|
+
await memory.setholds(next.holds);
|
|
896
|
+
await audit("hold", `Key ${hold.key} held under hold id ${holdid} pressed at ${new Date(hold.pressedat).toISOString()}.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
|
|
897
|
+
return { ok: true, summary: `Key ${hold.key} held under hold id ${holdid}.`, details: { holdid } };
|
|
898
|
+
}
|
|
899
|
+
async function executekeyrelease(step, session, plan, tabid2, origin) {
|
|
900
|
+
const holdid = step.value ?? "";
|
|
901
|
+
const holds = await memory.getholds();
|
|
902
|
+
const held = holds.find((candidate) => candidate.holdid === holdid && candidate.releasedat === void 0);
|
|
903
|
+
if (!held) throw new Error(`No held key matches hold id ${holdid}.`);
|
|
904
|
+
const modifiers = held.modifiers ?? [];
|
|
905
|
+
const dispatchstep = { ...step, value: held.key, options: JSON.stringify({ ...modifiers.length > 0 ? { modifiers } : {} }) };
|
|
906
|
+
const output = await dispatchpagestep(dispatchstep, tabid2, origin, plan);
|
|
907
|
+
const releasedat = Date.now();
|
|
908
|
+
const transition = releasehold(holds, holdid, releasedat);
|
|
909
|
+
await memory.setholds(transition.holds);
|
|
910
|
+
await audit("hold", `Key ${held.key} released from hold id ${holdid} at ${new Date(releasedat).toISOString()} after ${Math.max(0, releasedat - held.pressedat)} milliseconds.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
|
|
911
|
+
return { ok: output?.ok ?? false, summary: output?.summary ?? "Key release delivered.", details: { holdid } };
|
|
912
|
+
}
|
|
913
|
+
async function executedismissdialog(step, session, plan, tabid2, origin) {
|
|
914
|
+
const policy = parsedialogpolicy(step);
|
|
915
|
+
if (!policy) throw new Error("A reviewed dialog policy is required.");
|
|
916
|
+
await installdialogpolicy(tabid2, policy, false);
|
|
917
|
+
await harvestdialogs(session, plan, step.id, tabid2);
|
|
918
|
+
const answer = policy.answer !== void 0 ? ` with the reviewed answer "${policy.answer}"` : "";
|
|
919
|
+
return { ok: true, summary: `Dialog handler armed${answer} for the next confirm, alert or prompt.` };
|
|
920
|
+
}
|
|
921
|
+
async function executeretryaction(step, session, plan, tabid2, origin) {
|
|
922
|
+
const rule = stepoptions2(step).retryrule;
|
|
923
|
+
const inner = resolvedinnerstep(step, plan);
|
|
924
|
+
if (!inner) throw new Error("The reviewed wrapper step could not be resolved.");
|
|
925
|
+
const innergate = validatestep(inner, origin);
|
|
926
|
+
if (!innergate.allowed) throw new Error(`The wrapped step is not allowed: ${innergate.reason}`);
|
|
927
|
+
const probe = async () => {
|
|
928
|
+
const probestep = { id: `${step.id}probe`, kind: "readgeometry", summary: step.summary, risk: "read", ...inner.target ? { target: inner.target } : {}, ...inner.options ? { options: inner.options } : {} };
|
|
929
|
+
const output = await dispatchpagestep(probestep, tabid2, origin, plan);
|
|
930
|
+
const geometry = output?.details?.geometry;
|
|
931
|
+
if (!geometry || typeof geometry.x !== "number" || typeof geometry.y !== "number" || !Number.isFinite(geometry.x) || !Number.isFinite(geometry.y)) return null;
|
|
932
|
+
return { x: geometry.x, y: geometry.y };
|
|
933
|
+
};
|
|
934
|
+
const execute = async () => {
|
|
935
|
+
const output = await dispatchpagestep(inner, tabid2, origin, plan);
|
|
936
|
+
return { ok: Boolean(output?.ok), summary: output?.summary ?? "The wrapped step returned no result." };
|
|
937
|
+
};
|
|
938
|
+
const outcome = await runretries(rule ?? { attempts: 1 }, probe, execute);
|
|
939
|
+
const record2 = { stepid: step.id, attempts: outcome.attempts, movement: outcome.movement, ok: outcome.ok, at: Date.now() };
|
|
940
|
+
await memory.addretry(record2);
|
|
941
|
+
await audit("retry", outcome.summary, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
|
|
942
|
+
return { ok: outcome.ok, summary: outcome.summary, details: { attempts: outcome.attempts, movement: outcome.movement } };
|
|
943
|
+
}
|
|
944
|
+
async function executemapclicks(step, plan, tabid2, origin) {
|
|
945
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan);
|
|
946
|
+
if (!output?.ok) return output ?? { ok: false, summary: "The clickable map was not captured." };
|
|
947
|
+
const entries = Array.isArray(output.details?.entries) ? output.details?.entries : [];
|
|
948
|
+
const version = await memory.nextobservationversion();
|
|
949
|
+
const map = { version, entries, builtat: Date.now() };
|
|
950
|
+
await memory.setmap(map);
|
|
951
|
+
return { ok: true, summary: output.summary, details: { entries, mapversion: version } };
|
|
952
|
+
}
|
|
953
|
+
async function executeenterframe(step, plan, tabid2, origin) {
|
|
954
|
+
const inner = resolvedinnerstep(step, plan);
|
|
955
|
+
if (!inner) throw new Error("The reviewed frame wrapper step could not be resolved.");
|
|
956
|
+
const innergate = validatestep(inner, origin);
|
|
957
|
+
if (!innergate.allowed) throw new Error(`The wrapped step is not allowed: ${innergate.reason}`);
|
|
958
|
+
const options = stepoptions2(step);
|
|
959
|
+
const inneroptions = inner.options ? stepoptions2(inner) : void 0;
|
|
960
|
+
const derived = { ...step, options: JSON.stringify({ ...options, kind: inner.kind, ...inner.target ? { target: inner.target } : {}, ...inner.value ? { value: inner.value } : {}, ...inneroptions ? { options: inneroptions } : {} }) };
|
|
961
|
+
return dispatchpagestep(derived, tabid2, origin, plan);
|
|
962
|
+
}
|
|
270
963
|
async function executestep(stepid) {
|
|
271
964
|
const session = await memory.getsession();
|
|
272
965
|
const plan = await memory.getplan();
|
|
@@ -275,23 +968,50 @@ async function executestep(stepid) {
|
|
|
275
968
|
if (!step) throw new Error("Reviewed step was not found.");
|
|
276
969
|
const gate = canexecute({ session, plan, step, tabid: tab.id, origin });
|
|
277
970
|
if (!gate.allowed) throw new Error(gate.reason);
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
971
|
+
let output;
|
|
972
|
+
if (isbrowserkind(step.kind)) {
|
|
973
|
+
const capability = requiredcapability(step.kind);
|
|
974
|
+
if (capability) {
|
|
975
|
+
const granted = await chrome.permissions.contains({ permissions: [capability] });
|
|
976
|
+
if (!granted) throw new Error(`The ${capability} capability has not been granted; request it from the review panel.`);
|
|
977
|
+
}
|
|
978
|
+
output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
|
|
979
|
+
} else if (step.kind === "keyhold") {
|
|
980
|
+
output = await executekeyhold(step, session, plan, tab.id, origin);
|
|
981
|
+
} else if (step.kind === "keyrelease") {
|
|
982
|
+
output = await executekeyrelease(step, session, plan, tab.id, origin);
|
|
983
|
+
} else if (step.kind === "dismissdialog") {
|
|
984
|
+
output = await executedismissdialog(step, session, plan, tab.id, origin);
|
|
985
|
+
} else if (step.kind === "retryaction") {
|
|
986
|
+
output = await executeretryaction(step, session, plan, tab.id, origin);
|
|
987
|
+
} else if (step.kind === "mapclicks") {
|
|
988
|
+
output = await executemapclicks(step, plan, tab.id, origin);
|
|
989
|
+
} else if (step.kind === "enterframe") {
|
|
990
|
+
output = await executeenterframe(step, plan, tab.id, origin);
|
|
991
|
+
} else {
|
|
992
|
+
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
993
|
+
const fresh = await snapshot(tab.id);
|
|
994
|
+
if (!fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
|
|
995
|
+
}
|
|
996
|
+
output = await dispatchpagestep(step, tab.id, origin, plan);
|
|
997
|
+
}
|
|
286
998
|
const summary = output?.summary ?? "The page action returned no result.";
|
|
287
|
-
|
|
999
|
+
const resolved = output?.details?.resolvedtarget;
|
|
1000
|
+
if (resolved) {
|
|
1001
|
+
await memory.addresolution({ stepid, mode: resolved.mode, selector: resolved.selector, label: resolved.label, at: Date.now() });
|
|
1002
|
+
}
|
|
1003
|
+
const outcome = { stepid, ok: Boolean(output?.ok), summary, ...output?.details ? { details: output.details } : {}, at: Date.now() };
|
|
1004
|
+
const auditkind = stepauditkind(step, Boolean(output?.ok));
|
|
1005
|
+
await audit(auditkind, summary, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
|
|
1006
|
+
await memory.addoutcome(outcome);
|
|
288
1007
|
if (output?.ok && plan) {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
await
|
|
1008
|
+
const completed = recordstep(await memory.getprogress(), plan.id, stepid, Date.now());
|
|
1009
|
+
const tracked = recordoutcome(completed, plan.id, outcome, Date.now());
|
|
1010
|
+
await memory.setprogress(tracked);
|
|
1011
|
+
if (iscomplete(tracked, plan) && plan.state === "approved") {
|
|
1012
|
+
const done = { ...plan, state: "completed", completedat: Date.now() };
|
|
1013
|
+
await memory.setplan(done);
|
|
1014
|
+
await audit("complete", "Every reviewed step of the approved plan has executed.", { ...session ? { sessionid: session.id } : {}, planid: done.id });
|
|
295
1015
|
}
|
|
296
1016
|
}
|
|
297
1017
|
return output ?? { ok: false, summary };
|
|
@@ -304,13 +1024,12 @@ async function previewstep(stepid) {
|
|
|
304
1024
|
if (!step) throw new Error("Reviewed step was not found.");
|
|
305
1025
|
const gate = canpreview({ session, plan, step, tabid: tab.id, origin });
|
|
306
1026
|
if (!gate.allowed) throw new Error(gate.reason);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (target, expectedorigin) => {
|
|
1027
|
+
if (!step.target && !stepoptions2(step).targetref) throw new Error("Only a target-based step can be previewed.");
|
|
1028
|
+
const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (action, expectedorigin) => {
|
|
310
1029
|
const bridge = globalThis.devthinkbridge;
|
|
311
1030
|
if (!bridge) throw new Error("Devthink page bridge is unavailable.");
|
|
312
|
-
return bridge.previewtarget(
|
|
313
|
-
}, args: [step
|
|
1031
|
+
return bridge.previewtarget(action, expectedorigin);
|
|
1032
|
+
}, args: [step, origin] });
|
|
314
1033
|
const output = result[0]?.result;
|
|
315
1034
|
const summary = output?.summary ?? "The target preview returned no result.";
|
|
316
1035
|
await audit(output?.ok ? "observe" : "error", summary, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
|
|
@@ -331,11 +1050,18 @@ async function resumesession() {
|
|
|
331
1050
|
if (!session || session.stoppedat) throw new Error("No active browser session exists.");
|
|
332
1051
|
if (session.expiresat <= Date.now()) throw new Error("The browser session has expired and cannot be resumed.");
|
|
333
1052
|
if (!session.pausedat) throw new Error("The browser session is not paused.");
|
|
334
|
-
const resumed = { id: session.id, tabid: session.tabid, origin: session.origin, startedat: session.startedat, expiresat: session.expiresat };
|
|
1053
|
+
const resumed = { id: session.id, tabid: session.tabid, origin: session.origin, startedat: session.startedat, expiresat: session.expiresat, ...session.grants ? { grants: session.grants } : {} };
|
|
335
1054
|
await memory.setsession(resumed);
|
|
336
1055
|
await audit("resume", "The user resumed the browser session; reviewed actions can run again.", { sessionid: session.id });
|
|
337
1056
|
return resumed;
|
|
338
1057
|
}
|
|
1058
|
+
async function grantcapability(permission) {
|
|
1059
|
+
if (!["tabs", "downloads", "clipboardRead", "clipboardWrite"].includes(permission)) throw new Error("Unknown capability.");
|
|
1060
|
+
const granted = await chrome.permissions.request({ permissions: [permission] });
|
|
1061
|
+
if (!granted) throw new Error("The capability grant was declined.");
|
|
1062
|
+
await audit("capability", `Capability ${permission} granted by the user.`);
|
|
1063
|
+
return refreshcapabilities();
|
|
1064
|
+
}
|
|
339
1065
|
async function handlerequest(message, sender) {
|
|
340
1066
|
if (!extensionpage(sender)) throw new Error("Requests are accepted only from Devthink extension pages.");
|
|
341
1067
|
const input = message;
|
|
@@ -353,8 +1079,16 @@ async function handlerequest(message, sender) {
|
|
|
353
1079
|
case "context": {
|
|
354
1080
|
const plan = await memory.getplan();
|
|
355
1081
|
const progress = await memory.getprogress();
|
|
356
|
-
|
|
1082
|
+
const session = await memory.getsession();
|
|
1083
|
+
const holds = heldkeys(await memory.getholds());
|
|
1084
|
+
const observationversion = await memory.getobservationversion();
|
|
1085
|
+
const map = observationversion !== void 0 && observationversion > 0 ? await memory.getmap(observationversion) : void 0;
|
|
1086
|
+
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...map ? { map } : {} };
|
|
357
1087
|
}
|
|
1088
|
+
case "capabilities":
|
|
1089
|
+
return refreshcapabilities();
|
|
1090
|
+
case "grantcapability":
|
|
1091
|
+
return grantcapability(input.permission ?? "");
|
|
358
1092
|
case "diagnostic":
|
|
359
1093
|
return diagnostic();
|
|
360
1094
|
case "proposelocal":
|
|
@@ -366,6 +1100,8 @@ async function handlerequest(message, sender) {
|
|
|
366
1100
|
if (!plan || plan.state !== "pending") throw new Error("Only a pending plan can be approved.");
|
|
367
1101
|
const approved = { ...plan, state: "approved", approvedat: Date.now() };
|
|
368
1102
|
await memory.setplan(approved);
|
|
1103
|
+
const policy = plandialogpolicy(approved);
|
|
1104
|
+
if (policy) await memory.setdialogpolicy(policy);
|
|
369
1105
|
const current = await memory.getsession();
|
|
370
1106
|
await audit("approval", "The user approved the reviewed plan.", { ...current ? { sessionid: current.id } : {}, planid: approved.id });
|
|
371
1107
|
return approved;
|
|
@@ -383,6 +1119,22 @@ async function handlerequest(message, sender) {
|
|
|
383
1119
|
return previewstep(input.stepid ?? "");
|
|
384
1120
|
case "execute":
|
|
385
1121
|
return executestep(input.stepid ?? "");
|
|
1122
|
+
case "outcome": {
|
|
1123
|
+
const plan = await memory.getplan();
|
|
1124
|
+
if (!plan) throw new Error("No plan is available for an outcome envelope.");
|
|
1125
|
+
const outcome = (await memory.getoutcomes()).find((candidate) => candidate.stepid === (input.stepid ?? ""));
|
|
1126
|
+
if (!outcome) throw new Error("No outcome exists for the reviewed step.");
|
|
1127
|
+
const resolved = outcome.details?.resolvedtarget;
|
|
1128
|
+
return JSON.parse(outcomeresponse({ outcome, plan, ...resolved ? { resolvedtarget: resolved } : {} }));
|
|
1129
|
+
}
|
|
1130
|
+
case "map": {
|
|
1131
|
+
const plan = await memory.getplan();
|
|
1132
|
+
if (!plan) throw new Error("No plan is available for a map envelope.");
|
|
1133
|
+
const version = await memory.getobservationversion();
|
|
1134
|
+
const map = version !== void 0 ? await memory.getmap(version) : void 0;
|
|
1135
|
+
if (!map) throw new Error("No clickable map has been captured yet.");
|
|
1136
|
+
return JSON.parse(mapresponse({ map, plan }));
|
|
1137
|
+
}
|
|
386
1138
|
case "pausesession":
|
|
387
1139
|
return pausesession();
|
|
388
1140
|
case "resumesession":
|