@wenathlan/extension 1.1.30 → 1.1.32
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 -11
- package/dist/cli.js +23 -2
- package/dist/index.js +124 -15
- package/dist/index.js.map +2 -2
- package/dist/memory.d.ts +11 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +7 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +7 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +44 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +330 -38
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +7 -1
- package/extension/dist/pagebridge.js +422 -7
- package/extension/dist/pagebridge.js.map +4 -4
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +36 -1
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +75 -16
- 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
|
@@ -28,12 +28,43 @@ var sessionmemory = class {
|
|
|
28
28
|
async setdiagnostic(value) {
|
|
29
29
|
return this.adapter.set("diagnostic", value);
|
|
30
30
|
}
|
|
31
|
+
async getprogress() {
|
|
32
|
+
return this.adapter.get("progress");
|
|
33
|
+
}
|
|
34
|
+
async setprogress(value) {
|
|
35
|
+
return this.adapter.set("progress", value);
|
|
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
|
+
}
|
|
31
49
|
async getaudit() {
|
|
32
50
|
return await this.adapter.get("audit") ?? [];
|
|
33
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. */
|
|
34
56
|
async addaudi(event) {
|
|
35
57
|
const records = await this.getaudit();
|
|
36
|
-
|
|
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));
|
|
37
68
|
}
|
|
38
69
|
};
|
|
39
70
|
function randomid() {
|
|
@@ -41,8 +72,12 @@ function randomid() {
|
|
|
41
72
|
}
|
|
42
73
|
|
|
43
74
|
// policy.ts
|
|
44
|
-
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate"]);
|
|
45
|
-
var
|
|
75
|
+
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"]);
|
|
76
|
+
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset"]);
|
|
77
|
+
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"]);
|
|
78
|
+
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
79
|
+
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"]);
|
|
80
|
+
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile"]);
|
|
46
81
|
function normalizeendpoint(value) {
|
|
47
82
|
const endpoint = new URL(value.trim());
|
|
48
83
|
if (endpoint.protocol !== "https:") throw new Error("Devthink accepts HTTPS endpoints only.");
|
|
@@ -57,44 +92,141 @@ function hostpattern(origin) {
|
|
|
57
92
|
function actionrisk(kind) {
|
|
58
93
|
if (!allowedactions.has(kind)) throw new Error("Unsupported browser action.");
|
|
59
94
|
if (sensitiveactions.has(kind)) return "sensitive";
|
|
60
|
-
return kind
|
|
95
|
+
return interactionactions.has(kind) ? "interaction" : "read";
|
|
96
|
+
}
|
|
97
|
+
function parseoptions(step) {
|
|
98
|
+
if (step.options === void 0) return {};
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(step.options);
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error("Step options must be a JSON object.");
|
|
104
|
+
}
|
|
105
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Step options must be a JSON object.");
|
|
106
|
+
return parsed;
|
|
107
|
+
}
|
|
108
|
+
function requiredcapability(kind) {
|
|
109
|
+
if (kind === "tablist") return "tabs";
|
|
110
|
+
if (kind === "downloadfile") return "downloads";
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
function waitduration(step) {
|
|
114
|
+
const requested = step.value ? Number.parseInt(step.value, 10) : 250;
|
|
115
|
+
if (!Number.isFinite(requested) || requested < 0) throw new Error("Wait duration must be zero or a positive number of milliseconds.");
|
|
116
|
+
return requested;
|
|
117
|
+
}
|
|
118
|
+
function isnumericid(value) {
|
|
119
|
+
return typeof value === "string" && /^\d+$/.test(value);
|
|
120
|
+
}
|
|
121
|
+
function numericoption(options, key) {
|
|
122
|
+
return options[key] === void 0 || typeof options[key] === "number" && Number.isFinite(options[key]);
|
|
61
123
|
}
|
|
62
124
|
function validatestep(step, origin) {
|
|
63
125
|
if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
|
|
64
126
|
if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
|
|
65
|
-
if ((step.kind
|
|
127
|
+
if (targetactions.has(step.kind) && !step.target?.trim()) return { allowed: false, reason: "A page target is required." };
|
|
128
|
+
if (valueactions.has(step.kind) && !step.value?.trim()) return { allowed: false, reason: "A reviewed value is required." };
|
|
129
|
+
if (step.kind === "select" && !step.value?.trim()) return { allowed: false, reason: "A reviewed option value is required." };
|
|
130
|
+
if (step.kind === "navigate" && !step.value) return { allowed: false, reason: "A navigation URL is required." };
|
|
131
|
+
let options;
|
|
132
|
+
try {
|
|
133
|
+
options = parseoptions(step);
|
|
134
|
+
} catch {
|
|
135
|
+
return { allowed: false, reason: "Step options must be a JSON object." };
|
|
136
|
+
}
|
|
137
|
+
if (step.kind === "wait") {
|
|
138
|
+
try {
|
|
139
|
+
waitduration(step);
|
|
140
|
+
} catch {
|
|
141
|
+
return { allowed: false, reason: "Wait duration must be zero or a positive number of milliseconds." };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
66
144
|
if (step.kind === "navigate") {
|
|
67
|
-
if (!step.value) return { allowed: false, reason: "A navigation URL is required." };
|
|
68
145
|
try {
|
|
69
|
-
if (new URL(step.value).origin !== origin) return { allowed: false, reason: "Navigation must remain within the approved origin." };
|
|
146
|
+
if (new URL(step.value ?? "").origin !== origin) return { allowed: false, reason: "Navigation must remain within the approved origin." };
|
|
70
147
|
} catch {
|
|
71
148
|
return { allowed: false, reason: "Navigation URL is invalid." };
|
|
72
149
|
}
|
|
73
150
|
}
|
|
151
|
+
if (step.kind === "tabcreate" || step.kind === "windowcreate" || step.kind === "downloadfile") {
|
|
152
|
+
try {
|
|
153
|
+
const url = new URL(step.value ?? "");
|
|
154
|
+
if (url.protocol !== "https:") return { allowed: false, reason: "The reviewed URL must use HTTPS." };
|
|
155
|
+
} catch {
|
|
156
|
+
return { allowed: false, reason: "The reviewed URL is invalid." };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (step.kind === "tabactivate" || step.kind === "tabclose" || step.kind === "tabreload" || step.kind === "windowclose" || step.kind === "windowresize") {
|
|
160
|
+
if (!isnumericid(step.value)) return { allowed: false, reason: "A numeric browser id is required." };
|
|
161
|
+
}
|
|
162
|
+
if (step.kind === "zoomset") {
|
|
163
|
+
const zoom = Number(step.value);
|
|
164
|
+
if (!Number.isFinite(zoom) || zoom <= 0) return { allowed: false, reason: "The reviewed zoom must be a positive number." };
|
|
165
|
+
}
|
|
166
|
+
if (step.kind === "setattribute" || step.kind === "writestorage") {
|
|
167
|
+
const keyname = step.kind === "setattribute" ? "name" : "key";
|
|
168
|
+
if (typeof options[keyname] !== "string" || !options[keyname].trim()) return { allowed: false, reason: `A reviewed ${keyname} is required in options.` };
|
|
169
|
+
if (typeof options.value !== "string") return { allowed: false, reason: "A reviewed value is required in options." };
|
|
170
|
+
}
|
|
171
|
+
if (step.kind === "windowresize") {
|
|
172
|
+
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." };
|
|
173
|
+
}
|
|
174
|
+
if ((step.kind === "scrollpage" || step.kind === "scrollby") && (!numericoption(options, "x") || !numericoption(options, "y"))) return { allowed: false, reason: "Scroll amounts must be numbers in options." };
|
|
175
|
+
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." };
|
|
176
|
+
return { allowed: true };
|
|
177
|
+
}
|
|
178
|
+
function sessiongate(input) {
|
|
179
|
+
if (!input.session || input.session.stoppedat) return { allowed: false, reason: "No active browser session exists." };
|
|
180
|
+
if (input.session.expiresat <= input.now) return { allowed: false, reason: "The browser session has expired." };
|
|
181
|
+
if (input.session.pausedat) return { allowed: false, reason: `The browser session is paused and cannot ${input.action}.` };
|
|
182
|
+
if (input.session.tabid !== input.tabid || input.session.origin !== input.origin) return { allowed: false, reason: `The ${input.action} is outside the approved tab or origin.` };
|
|
74
183
|
return { allowed: true };
|
|
75
184
|
}
|
|
76
185
|
function canexecute(input) {
|
|
77
186
|
const now = input.now ?? Date.now();
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
if (input.session.tabid !== input.tabid || input.session.origin !== input.origin) return { allowed: false, reason: "The action is outside the approved tab or origin." };
|
|
187
|
+
const gate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now, action: "execute an action" });
|
|
188
|
+
if (!gate.allowed) return gate;
|
|
81
189
|
if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "The plan has not received explicit approval." };
|
|
82
190
|
if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
|
|
83
191
|
return validatestep(input.step, input.origin);
|
|
84
192
|
}
|
|
85
193
|
function canpreview(input) {
|
|
86
194
|
const now = input.now ?? Date.now();
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
if (input.session.tabid !== input.tabid || input.session.origin !== input.origin) return { allowed: false, reason: "The preview is outside the approved tab or origin." };
|
|
195
|
+
const gate = sessiongate({ session: input.session, tabid: input.tabid, origin: input.origin, now, action: "preview a target" });
|
|
196
|
+
if (!gate.allowed) return gate;
|
|
90
197
|
if (!input.plan || !["pending", "approved"].includes(input.plan.state)) return { allowed: false, reason: "Only a reviewed pending or approved plan can be previewed." };
|
|
91
198
|
if (input.plan.expiresat <= now) return { allowed: false, reason: "The reviewed plan has expired." };
|
|
92
|
-
if (!
|
|
199
|
+
if (!targetactions.has(input.step.kind)) return { allowed: false, reason: "Only a target-based action can be previewed." };
|
|
93
200
|
return validatestep(input.step, input.origin);
|
|
94
201
|
}
|
|
95
202
|
|
|
203
|
+
// progress.ts
|
|
204
|
+
function emptyprogress(planid, now) {
|
|
205
|
+
return { planid, completedsteps: [], updatedat: now };
|
|
206
|
+
}
|
|
207
|
+
function recordstep(progress, planid, stepid, now) {
|
|
208
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
209
|
+
if (base.completedsteps.includes(stepid)) return { ...base, updatedat: now };
|
|
210
|
+
return { planid, completedsteps: [...base.completedsteps, stepid], updatedat: now };
|
|
211
|
+
}
|
|
212
|
+
function recordoutcome(progress, planid, outcome, now) {
|
|
213
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
214
|
+
return { ...base, outcomes: [...base.outcomes ?? [], outcome], updatedat: now };
|
|
215
|
+
}
|
|
216
|
+
function iscomplete(progress, plan) {
|
|
217
|
+
if (!progress || progress.planid !== plan.id) return false;
|
|
218
|
+
const required = plan.steps.map((step) => step.id);
|
|
219
|
+
return required.length > 0 && required.every((id) => progress.completedsteps.includes(id));
|
|
220
|
+
}
|
|
221
|
+
function resetforplan(progress, plan, now) {
|
|
222
|
+
if (progress && progress.planid === plan.id) return progress;
|
|
223
|
+
if (!progress) return emptyprogress(plan.id, now);
|
|
224
|
+
const snapshot2 = { planid: progress.planid, completedsteps: progress.completedsteps, ...progress.outcomes ? { outcomes: progress.outcomes } : {}, updatedat: progress.updatedat };
|
|
225
|
+
return { planid: plan.id, completedsteps: [], outcomes: [], prior: [...progress.prior ?? [], snapshot2], updatedat: now };
|
|
226
|
+
}
|
|
227
|
+
|
|
96
228
|
// version.ts
|
|
97
|
-
var packageversion = "1.1.
|
|
229
|
+
var packageversion = "1.1.32";
|
|
98
230
|
|
|
99
231
|
// types.ts
|
|
100
232
|
var protocolversion = packageversion;
|
|
@@ -113,7 +245,7 @@ function parseproposal(value, origin) {
|
|
|
113
245
|
if (root.version !== protocolversion) throw new Error("Unsupported protocol version.");
|
|
114
246
|
const planinput = record(root.plan);
|
|
115
247
|
const stepsinput = planinput.steps;
|
|
116
|
-
if (!Array.isArray(stepsinput) || stepsinput.length === 0
|
|
248
|
+
if (!Array.isArray(stepsinput) || stepsinput.length === 0) throw new Error("A plan needs at least one step.");
|
|
117
249
|
const steps = stepsinput.map((input, index) => {
|
|
118
250
|
const candidate = record(input);
|
|
119
251
|
const kind = text(candidate.kind, `step ${index + 1} kind`);
|
|
@@ -123,31 +255,117 @@ function parseproposal(value, origin) {
|
|
|
123
255
|
summary: text(candidate.summary, `step ${index + 1} summary`),
|
|
124
256
|
risk: actionrisk(kind),
|
|
125
257
|
...typeof candidate.target === "string" ? { target: candidate.target } : {},
|
|
126
|
-
...typeof candidate.value === "string" ? { value: candidate.value } : {}
|
|
258
|
+
...typeof candidate.value === "string" ? { value: candidate.value } : {},
|
|
259
|
+
...typeof candidate.options === "string" ? { options: candidate.options } : {}
|
|
127
260
|
};
|
|
128
261
|
const evaluation = validatestep(step, origin);
|
|
129
262
|
if (!evaluation.allowed) throw new Error(evaluation.reason);
|
|
130
263
|
return step;
|
|
131
264
|
});
|
|
132
265
|
const createdat = Date.now();
|
|
266
|
+
const expiresat = typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3;
|
|
133
267
|
const plan = {
|
|
134
268
|
id: typeof planinput.id === "string" ? planinput.id : crypto.randomUUID(),
|
|
135
269
|
objective: text(planinput.objective, "objective"),
|
|
136
270
|
origin,
|
|
137
271
|
steps,
|
|
138
272
|
createdat,
|
|
139
|
-
expiresat
|
|
273
|
+
expiresat,
|
|
140
274
|
state: "pending"
|
|
141
275
|
};
|
|
142
276
|
if (plan.expiresat <= createdat) throw new Error("Plan expiry must be in the future.");
|
|
143
277
|
return { version: protocolversion, plan };
|
|
144
278
|
}
|
|
145
279
|
function requestbody(input) {
|
|
146
|
-
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation });
|
|
280
|
+
return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// extension/browsertabs.ts
|
|
284
|
+
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
285
|
+
function isbrowserkind(kind) {
|
|
286
|
+
return browserkinds.has(kind);
|
|
287
|
+
}
|
|
288
|
+
async function readcapabilities() {
|
|
289
|
+
const [tabs, downloads, clipboardread, clipboardwrite] = await Promise.all([
|
|
290
|
+
chrome.permissions.contains({ permissions: ["tabs"] }),
|
|
291
|
+
chrome.permissions.contains({ permissions: ["downloads"] }),
|
|
292
|
+
chrome.permissions.contains({ permissions: ["clipboardRead"] }),
|
|
293
|
+
chrome.permissions.contains({ permissions: ["clipboardWrite"] })
|
|
294
|
+
]);
|
|
295
|
+
return { tabs, downloads, clipboardread, clipboardwrite, reportedat: Date.now() };
|
|
296
|
+
}
|
|
297
|
+
function stepoptions(step) {
|
|
298
|
+
if (!step.options) return {};
|
|
299
|
+
try {
|
|
300
|
+
const parsed = JSON.parse(step.options);
|
|
301
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
302
|
+
} catch {
|
|
303
|
+
return {};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function tabid(step) {
|
|
307
|
+
return Number.parseInt(step.value ?? "", 10);
|
|
308
|
+
}
|
|
309
|
+
async function runbrowseraction(step, sessiontabid, windowid) {
|
|
310
|
+
const options = stepoptions(step);
|
|
311
|
+
switch (step.kind) {
|
|
312
|
+
case "tablist": {
|
|
313
|
+
const tabs = await chrome.tabs.query({});
|
|
314
|
+
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 })) } };
|
|
315
|
+
}
|
|
316
|
+
case "tabcreate": {
|
|
317
|
+
const created = await chrome.tabs.create({ url: step.value, active: options.active !== false, pinned: options.pinned === true });
|
|
318
|
+
return { ok: true, summary: `Opened a new tab for ${step.value}.`, details: { tabid: created?.id ?? 0 } };
|
|
319
|
+
}
|
|
320
|
+
case "tabactivate": {
|
|
321
|
+
await chrome.tabs.update(tabid(step), { active: true });
|
|
322
|
+
return { ok: true, summary: `Activated tab ${tabid(step)}.` };
|
|
323
|
+
}
|
|
324
|
+
case "tabclose": {
|
|
325
|
+
await chrome.tabs.remove(tabid(step));
|
|
326
|
+
return { ok: true, summary: `Closed tab ${tabid(step)}.` };
|
|
327
|
+
}
|
|
328
|
+
case "tabreload": {
|
|
329
|
+
await chrome.tabs.reload(tabid(step), { bypassCache: options.bypasscache === true });
|
|
330
|
+
return { ok: true, summary: `Reloaded tab ${tabid(step)}.` };
|
|
331
|
+
}
|
|
332
|
+
case "tabsnapshot": {
|
|
333
|
+
const shot = await chrome.tabs.captureVisibleTab(windowid, { format: "png" });
|
|
334
|
+
return { ok: true, summary: "Captured the visible area of the active tab.", details: { shot } };
|
|
335
|
+
}
|
|
336
|
+
case "windowlist": {
|
|
337
|
+
const windows = await chrome.windows.getAll();
|
|
338
|
+
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 })) } };
|
|
339
|
+
}
|
|
340
|
+
case "windowcreate": {
|
|
341
|
+
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 } : {} });
|
|
342
|
+
return { ok: true, summary: `Opened a new window for ${step.value}.`, details: { windowid: created?.id ?? 0 } };
|
|
343
|
+
}
|
|
344
|
+
case "windowclose": {
|
|
345
|
+
await chrome.windows.remove(tabid(step));
|
|
346
|
+
return { ok: true, summary: `Closed window ${tabid(step)}.` };
|
|
347
|
+
}
|
|
348
|
+
case "zoomset": {
|
|
349
|
+
const zoom = Number(step.value);
|
|
350
|
+
await chrome.tabs.setZoom(sessiontabid, zoom);
|
|
351
|
+
return { ok: true, summary: `Set the tab zoom to ${zoom}.` };
|
|
352
|
+
}
|
|
353
|
+
case "windowresize": {
|
|
354
|
+
await chrome.windows.update(tabid(step), { width: options.width, height: options.height });
|
|
355
|
+
return { ok: true, summary: `Resized window ${tabid(step)}.` };
|
|
356
|
+
}
|
|
357
|
+
case "downloadfile": {
|
|
358
|
+
const downloadid = await chrome.downloads.download({ url: step.value ?? "" });
|
|
359
|
+
return { ok: true, summary: `Started the download of ${step.value}.`, details: { downloadid } };
|
|
360
|
+
}
|
|
361
|
+
default:
|
|
362
|
+
return { ok: false, summary: "Unsupported browser action." };
|
|
363
|
+
}
|
|
147
364
|
}
|
|
148
365
|
|
|
149
366
|
// extension/background.ts
|
|
150
367
|
var sessionduration = 15 * 60 * 1e3;
|
|
368
|
+
var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
|
|
151
369
|
var chromestorage = {
|
|
152
370
|
async get(key) {
|
|
153
371
|
return (await chrome.storage.local.get(key))[key];
|
|
@@ -163,6 +381,11 @@ function extensionpage(sender) {
|
|
|
163
381
|
async function audit(kind, summary, extra = {}) {
|
|
164
382
|
await memory.addaudi({ id: randomid(), kind, at: Date.now(), summary, ...extra });
|
|
165
383
|
}
|
|
384
|
+
async function refreshcapabilities() {
|
|
385
|
+
const report = await readcapabilities();
|
|
386
|
+
await memory.setcapabilities(report);
|
|
387
|
+
return report;
|
|
388
|
+
}
|
|
166
389
|
async function activecontext() {
|
|
167
390
|
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
168
391
|
if (!tab?.id || !tab.url) throw new Error("No active web tab is available.");
|
|
@@ -170,9 +393,9 @@ async function activecontext() {
|
|
|
170
393
|
if (!origin.startsWith("https://")) throw new Error("Devthink can work with HTTPS pages only.");
|
|
171
394
|
return { tab, origin };
|
|
172
395
|
}
|
|
173
|
-
async function snapshot(
|
|
174
|
-
await chrome.scripting.executeScript({ target: { tabId:
|
|
175
|
-
const result = await chrome.scripting.executeScript({ target: { tabId:
|
|
396
|
+
async function snapshot(tabid2) {
|
|
397
|
+
await chrome.scripting.executeScript({ target: { tabId: tabid2 }, files: ["pagebridge.js"] });
|
|
398
|
+
const result = await chrome.scripting.executeScript({ target: { tabId: tabid2 }, func: () => {
|
|
176
399
|
const bridge = globalThis.devthinkbridge;
|
|
177
400
|
if (!bridge) throw new Error("Devthink page bridge is unavailable.");
|
|
178
401
|
return bridge.capturesnapshot();
|
|
@@ -183,7 +406,7 @@ async function snapshot(tabid) {
|
|
|
183
406
|
}
|
|
184
407
|
async function startsession() {
|
|
185
408
|
const { tab, origin } = await activecontext();
|
|
186
|
-
const session = { id: randomid(), tabid: tab.id, origin, startedat: Date.now(), expiresat: Date.now() + sessionduration };
|
|
409
|
+
const session = { id: randomid(), tabid: tab.id, origin, startedat: Date.now(), expiresat: Date.now() + sessionduration, grants: [origin] };
|
|
187
410
|
await memory.setsession(session);
|
|
188
411
|
await audit("session", `Session started for ${origin}.`, { sessionid: session.id });
|
|
189
412
|
return session;
|
|
@@ -200,7 +423,7 @@ async function diagnostic() {
|
|
|
200
423
|
}
|
|
201
424
|
function localplan(objective, session) {
|
|
202
425
|
const now = Date.now();
|
|
203
|
-
return { id: randomid(), objective, origin: session.origin, steps: [{ id: randomid(), kind: "observe", summary: "Capture a
|
|
426
|
+
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" };
|
|
204
427
|
}
|
|
205
428
|
async function propose(objective, remote) {
|
|
206
429
|
if (!objective.trim()) throw new Error("An objective is required.");
|
|
@@ -209,18 +432,25 @@ async function propose(objective, remote) {
|
|
|
209
432
|
const { tab, origin } = await activecontext();
|
|
210
433
|
if (session.tabid !== tab.id || session.origin !== origin) throw new Error("The selected tab or origin no longer matches the approved session.");
|
|
211
434
|
const observation = await snapshot(session.tabid);
|
|
435
|
+
const capabilities = await refreshcapabilities();
|
|
212
436
|
const config = await memory.getconfig();
|
|
213
437
|
let plan = localplan(objective.trim(), session);
|
|
214
438
|
if (remote) {
|
|
215
439
|
if (!config) throw new Error("Configure an approved HTTPS endpoint before requesting a remote proposal.");
|
|
216
|
-
const response = await fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, credentials: "omit", body: requestbody({ objective: objective.trim(), session, observation }) });
|
|
440
|
+
const response = await fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, credentials: "omit", body: requestbody({ objective: objective.trim(), session, observation, capabilities }) });
|
|
217
441
|
if (!response.ok) throw new Error(`Proposal endpoint returned ${response.status}.`);
|
|
218
442
|
plan = parseproposal(await response.json(), session.origin).plan;
|
|
219
443
|
}
|
|
220
444
|
await memory.setplan(plan);
|
|
445
|
+
await memory.setprogress(resetforplan(await memory.getprogress(), plan, Date.now()));
|
|
221
446
|
await audit("proposal", `Plan proposed with ${plan.steps.length} reviewed step${plan.steps.length === 1 ? "" : "s"}.`, { sessionid: session.id, planid: plan.id });
|
|
222
447
|
return plan;
|
|
223
448
|
}
|
|
449
|
+
function browserauditkind(step) {
|
|
450
|
+
if (step.kind === "downloadfile") return "download";
|
|
451
|
+
if (step.kind.startsWith("window")) return "window";
|
|
452
|
+
return "tab";
|
|
453
|
+
}
|
|
224
454
|
async function executestep(stepid) {
|
|
225
455
|
const session = await memory.getsession();
|
|
226
456
|
const plan = await memory.getplan();
|
|
@@ -229,16 +459,41 @@ async function executestep(stepid) {
|
|
|
229
459
|
if (!step) throw new Error("Reviewed step was not found.");
|
|
230
460
|
const gate = canexecute({ session, plan, step, tabid: tab.id, origin });
|
|
231
461
|
if (!gate.allowed) throw new Error(gate.reason);
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
462
|
+
let output;
|
|
463
|
+
if (isbrowserkind(step.kind)) {
|
|
464
|
+
const capability = requiredcapability(step.kind);
|
|
465
|
+
if (capability) {
|
|
466
|
+
const granted = await chrome.permissions.contains({ permissions: [capability] });
|
|
467
|
+
if (!granted) throw new Error(`The ${capability} capability has not been granted; request it from the review panel.`);
|
|
468
|
+
}
|
|
469
|
+
output = await runbrowseraction(step, tab.id, tab.windowId ?? chrome.windows.WINDOW_ID_CURRENT);
|
|
470
|
+
} else {
|
|
471
|
+
if (step.target && freshcheckkinds.has(step.kind)) {
|
|
472
|
+
const fresh = await snapshot(tab.id);
|
|
473
|
+
if (!fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
|
|
474
|
+
}
|
|
475
|
+
const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (action, expectedorigin) => {
|
|
476
|
+
const bridge = globalThis.devthinkbridge;
|
|
477
|
+
if (!bridge) throw new Error("Devthink page bridge is unavailable.");
|
|
478
|
+
return bridge.performstep(action, expectedorigin);
|
|
479
|
+
}, args: [step, origin] });
|
|
480
|
+
output = result[0]?.result;
|
|
481
|
+
}
|
|
240
482
|
const summary = output?.summary ?? "The page action returned no result.";
|
|
241
|
-
|
|
483
|
+
const outcome = { stepid, ok: Boolean(output?.ok), summary, ...output?.details ? { details: output.details } : {}, at: Date.now() };
|
|
484
|
+
const auditkind = isbrowserkind(step.kind) ? browserauditkind(step) : output?.ok ? "action" : "error";
|
|
485
|
+
await audit(auditkind, summary, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
|
|
486
|
+
await memory.addoutcome(outcome);
|
|
487
|
+
if (output?.ok && plan) {
|
|
488
|
+
const completed = recordstep(await memory.getprogress(), plan.id, stepid, Date.now());
|
|
489
|
+
const tracked = recordoutcome(completed, plan.id, outcome, Date.now());
|
|
490
|
+
await memory.setprogress(tracked);
|
|
491
|
+
if (iscomplete(tracked, plan) && plan.state === "approved") {
|
|
492
|
+
const done = { ...plan, state: "completed", completedat: Date.now() };
|
|
493
|
+
await memory.setplan(done);
|
|
494
|
+
await audit("complete", "Every reviewed step of the approved plan has executed.", { ...session ? { sessionid: session.id } : {}, planid: done.id });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
242
497
|
return output ?? { ok: false, summary };
|
|
243
498
|
}
|
|
244
499
|
async function previewstep(stepid) {
|
|
@@ -249,8 +504,7 @@ async function previewstep(stepid) {
|
|
|
249
504
|
if (!step) throw new Error("Reviewed step was not found.");
|
|
250
505
|
const gate = canpreview({ session, plan, step, tabid: tab.id, origin });
|
|
251
506
|
if (!gate.allowed) throw new Error(gate.reason);
|
|
252
|
-
|
|
253
|
-
if (!step.target || !fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
|
|
507
|
+
if (!step.target) throw new Error("Only a target-based step can be previewed.");
|
|
254
508
|
const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (target, expectedorigin) => {
|
|
255
509
|
const bridge = globalThis.devthinkbridge;
|
|
256
510
|
if (!bridge) throw new Error("Devthink page bridge is unavailable.");
|
|
@@ -261,6 +515,33 @@ async function previewstep(stepid) {
|
|
|
261
515
|
await audit(output?.ok ? "observe" : "error", summary, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
|
|
262
516
|
return output ?? { ok: false, summary };
|
|
263
517
|
}
|
|
518
|
+
async function pausesession() {
|
|
519
|
+
const session = await memory.getsession();
|
|
520
|
+
if (!session || session.stoppedat) throw new Error("No active browser session exists.");
|
|
521
|
+
if (session.expiresat <= Date.now()) throw new Error("The browser session has expired.");
|
|
522
|
+
if (session.pausedat) throw new Error("The browser session is already paused.");
|
|
523
|
+
const paused = { ...session, pausedat: Date.now() };
|
|
524
|
+
await memory.setsession(paused);
|
|
525
|
+
await audit("pause", "The user paused the browser session; no action or preview can run.", { sessionid: session.id });
|
|
526
|
+
return paused;
|
|
527
|
+
}
|
|
528
|
+
async function resumesession() {
|
|
529
|
+
const session = await memory.getsession();
|
|
530
|
+
if (!session || session.stoppedat) throw new Error("No active browser session exists.");
|
|
531
|
+
if (session.expiresat <= Date.now()) throw new Error("The browser session has expired and cannot be resumed.");
|
|
532
|
+
if (!session.pausedat) throw new Error("The browser session is not paused.");
|
|
533
|
+
const resumed = { id: session.id, tabid: session.tabid, origin: session.origin, startedat: session.startedat, expiresat: session.expiresat, ...session.grants ? { grants: session.grants } : {} };
|
|
534
|
+
await memory.setsession(resumed);
|
|
535
|
+
await audit("resume", "The user resumed the browser session; reviewed actions can run again.", { sessionid: session.id });
|
|
536
|
+
return resumed;
|
|
537
|
+
}
|
|
538
|
+
async function grantcapability(permission) {
|
|
539
|
+
if (!["tabs", "downloads", "clipboardRead", "clipboardWrite"].includes(permission)) throw new Error("Unknown capability.");
|
|
540
|
+
const granted = await chrome.permissions.request({ permissions: [permission] });
|
|
541
|
+
if (!granted) throw new Error("The capability grant was declined.");
|
|
542
|
+
await audit("capability", `Capability ${permission} granted by the user.`);
|
|
543
|
+
return refreshcapabilities();
|
|
544
|
+
}
|
|
264
545
|
async function handlerequest(message, sender) {
|
|
265
546
|
if (!extensionpage(sender)) throw new Error("Requests are accepted only from Devthink extension pages.");
|
|
266
547
|
const input = message;
|
|
@@ -275,8 +556,15 @@ async function handlerequest(message, sender) {
|
|
|
275
556
|
}
|
|
276
557
|
case "startsession":
|
|
277
558
|
return startsession();
|
|
278
|
-
case "context":
|
|
279
|
-
|
|
559
|
+
case "context": {
|
|
560
|
+
const plan = await memory.getplan();
|
|
561
|
+
const progress = await memory.getprogress();
|
|
562
|
+
return { config: await memory.getconfig(), session: await memory.getsession(), 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() };
|
|
563
|
+
}
|
|
564
|
+
case "capabilities":
|
|
565
|
+
return refreshcapabilities();
|
|
566
|
+
case "grantcapability":
|
|
567
|
+
return grantcapability(input.permission ?? "");
|
|
280
568
|
case "diagnostic":
|
|
281
569
|
return diagnostic();
|
|
282
570
|
case "proposelocal":
|
|
@@ -305,6 +593,10 @@ async function handlerequest(message, sender) {
|
|
|
305
593
|
return previewstep(input.stepid ?? "");
|
|
306
594
|
case "execute":
|
|
307
595
|
return executestep(input.stepid ?? "");
|
|
596
|
+
case "pausesession":
|
|
597
|
+
return pausesession();
|
|
598
|
+
case "resumesession":
|
|
599
|
+
return resumesession();
|
|
308
600
|
case "stop": {
|
|
309
601
|
const session = await memory.getsession();
|
|
310
602
|
if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
|