@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.
@@ -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
- await this.adapter.set("audit", [event, ...records].slice(0, 100));
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 allowedactions = /* @__PURE__ */ new Set(["observe", "inspect", "focus", "click", "type", "navigate"]);
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 === "focus" ? "interaction" : "read";
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 === "click" || step.kind === "focus" || step.kind === "inspect" || step.kind === "type") && !step.target?.trim()) return { allowed: false, reason: "A page target is required." };
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
- if (!input.session || input.session.stoppedat) return { allowed: false, reason: "No active browser session exists." };
79
- if (input.session.expiresat <= now) return { allowed: false, reason: "The browser session has expired." };
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
- if (!input.session || input.session.stoppedat) return { allowed: false, reason: "No active browser session exists." };
88
- if (input.session.expiresat <= now) return { allowed: false, reason: "The browser session has expired." };
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 (!["focus", "inspect", "click", "type"].includes(input.step.kind)) return { allowed: false, reason: "Only a target-based action can be previewed." };
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.30";
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 || stepsinput.length > 20) throw new Error("A plan needs between one and twenty steps.");
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: Math.min(typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3, createdat + 30 * 60 * 1e3),
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(tabid) {
174
- await chrome.scripting.executeScript({ target: { tabId: tabid }, files: ["pagebridge.js"] });
175
- const result = await chrome.scripting.executeScript({ target: { tabId: tabid }, func: () => {
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 bounded semantic snapshot of the approved active tab.", risk: "read" }], createdat: now, expiresat: now + sessionduration, state: "pending" };
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
- const fresh = await snapshot(tab.id);
233
- if (step.target && !fresh.interactive.some((item) => item.selector === step.target)) throw new Error("The page changed and the target must be reviewed again.");
234
- const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (action, expectedorigin) => {
235
- const bridge = globalThis.devthinkbridge;
236
- if (!bridge) throw new Error("Devthink page bridge is unavailable.");
237
- return bridge.performstep(action, expectedorigin);
238
- }, args: [step, origin] });
239
- const output = result[0]?.result;
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
- await audit(output?.ok ? "action" : "error", summary, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
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
- const fresh = await snapshot(tab.id);
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
- return { config: await memory.getconfig(), session: await memory.getsession(), plan: await memory.getplan(), diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit() };
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() });