webhelm 1.0.14 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webhelm",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Agent browser automation in a visible managed Chrome with persistent JavaScript sessions",
5
5
  "type": "module",
6
6
  "bin": {
package/runtime/cli.mjs CHANGED
@@ -75,6 +75,7 @@ async function runPlatform(args) {
75
75
  const config = loadConfig(args);
76
76
  const mode = validateMode(platform, action, args, config);
77
77
  const sid = arg(args, '--session-id');
78
+ if (sid) await requireCliSession(sid);
78
79
  const auto = has(args, '--auto-session');
79
80
  validateConfig(platform, action, config);
80
81
  if (sid && auto) throw Error('--session-id and --auto-session are mutually exclusive');
@@ -94,7 +95,6 @@ async function runPlatform(args) {
94
95
  };
95
96
  }
96
97
  if (mode === 'sync' && (!sid || auto)) throw Error('sync new-turn requires a caller-owned --session-id');
97
- if (sid) await requireCliSession(sid);
98
98
  if (!sid && !auto) throw Error('platform run requires --session-id; use --auto-session only for one-shot actions');
99
99
  return request({op:'platform-run', platform, action, config: {...config, ...(mode ? {mode} : {})},
100
100
  session_id:sid, auto_session:auto, url:arg(args, '--url'), workspace:CLI_WORKSPACE});
@@ -119,6 +119,7 @@ async function requireCliSession(sessionId) {
119
119
  async function main(args = process.argv.slice(2)) {
120
120
  const [group, command] = args;
121
121
  if (!group || ['--help', '-h', 'help'].includes(group) || has(args, '--help') || has(args, '-h')) return help();
122
+ rejectWorkspaceOverride(args);
122
123
  if (group === 'migrate') {
123
124
  await ensureActions(); // ~/.local/webhelm is the Actions Git checkout, never create an empty replacement.
124
125
  const legacy=await migrateConfig();
@@ -152,7 +153,6 @@ async function main(args = process.argv.slice(2)) {
152
153
  return emit(await request(payload));
153
154
  }
154
155
  if (group === 'session') {
155
- rejectWorkspaceOverride(args);
156
156
  if (command === 'start') return emit(await request({op:'start', workspace:CLI_WORKSPACE, url:arg(args,'--url')}));
157
157
  if (command === 'list') {
158
158
  const result=await request({op:'list'});
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "WebHelm",
4
- "version": "0.4.6",
4
+ "version": "0.4.7",
5
5
  "description": "Own named Chrome tab-group workspaces and lazy reusable tab pools for browser agents.",
6
6
  "permissions": [
7
7
  "storage",
@@ -102,6 +102,8 @@ globalThis.browserWorkspaceManagerRpc = async (request) => {
102
102
  const APPLICATION_ONLY_METHODS = new Set([
103
103
  "workspace.create", "workspace.resize", "workspace.delete",
104
104
  "workspace.ensure", "workspace.migrateDefault",
105
+ "workspace.acquire", "workspace.acquireIdentity",
106
+ "workspace.release", "workspace.releaseAll",
105
107
  ]);
106
108
  chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
107
109
  if (APPLICATION_ONLY_METHODS.has(request?.method)) {
@@ -552,9 +552,12 @@ export class WorkspaceManager {
552
552
  const normalizedName = normalizeWorkspaceName(name);
553
553
  // Leasing may allocate a Tab inside an existing Workspace, but must never
554
554
  // provision a new Workspace as a side effect. Only app-owned creation can.
555
- const ready = await this.status(normalizedName);
556
- if (!ready.initialized)
555
+ const { entry } = await this.configuredWorkspace(normalizedName);
556
+ if (!entry)
557
557
  throw new Error(`Workspace ${normalizedName} does not exist; an application must provision it first`);
558
+ // Previously provisioned workspaces may restore a missing physical group
559
+ // from their durable configuration; this never creates a new name.
560
+ const ready = await this.ensureWorkspace(normalizedName);
558
561
  const tabs = await this.groupTabs(ready.groupId);
559
562
  const { idle, leased } = this.classify(normalizedName, tabs);
560
563
  if (!idle.length && leased.length >= ready.poolSize) {
@@ -597,9 +600,12 @@ export class WorkspaceManager {
597
600
 
598
601
  // Leasing may allocate a Tab inside an existing Workspace, but must never
599
602
  // provision a new Workspace as a side effect. Only app-owned creation can.
600
- const ready = await this.status(normalizedName);
601
- if (!ready.initialized)
603
+ const { entry } = await this.configuredWorkspace(normalizedName);
604
+ if (!entry)
602
605
  throw new Error(`Workspace ${normalizedName} does not exist; an application must provision it first`);
606
+ // Previously provisioned workspaces may restore a missing physical group
607
+ // from their durable configuration; this never creates a new name.
608
+ const ready = await this.ensureWorkspace(normalizedName);
603
609
  const tabs = await this.groupTabs(ready.groupId);
604
610
  const { idle, leased } = this.classify(normalizedName, tabs);
605
611
  if (!idle.length && leased.length >= ready.poolSize) {
@@ -312,36 +312,48 @@ async function extensionCallSerialized(_session,method,args={}) {
312
312
  `globalThis.browserWorkspaceManagerRpc(${request})`);
313
313
  }catch(error){debuggerSession=null;throw error;}
314
314
  }
315
- const page=targetInfos.find(t=>t.type==='page'&&t.url.startsWith(EXTENSION_PREFIX)&&(t.url.includes('/workspace.html')||t.url.includes('/helm.html')));
316
- if(page){
317
- const {sessionId}=await browser.send('Target.attachToTarget',{targetId:page.targetId,flatten:true});
318
- try{return await evaluateExtension(browser,sessionId,`chrome.runtime.sendMessage(${request})`);}
319
- finally{await browser.send('Target.detachFromTarget',{sessionId}).catch(()=>{});}
320
- }
321
- if(!extensionConfigured())throw Error('WebHelm workspace extension is not installed or enabled');
322
- // Wake a suspended MV3 worker from a temporary background extension page.
323
- // Only this temporary target is closed; no user tab is reused or modified.
324
- const {targetId}=await browser.send('Target.createTarget',{
325
- url:`${EXTENSION_PREFIX}helm.html?webhelm_wakeup=1`,background:true});
326
- let sessionId;
327
- try{
328
- ({sessionId}=await browser.send('Target.attachToTarget',{targetId,flatten:true}));
329
- let ready=false;
315
+ // Pages (including an agent-opened extension page) cannot mutate groups.
316
+ // Wake the MV3 worker using a read-only message, then invoke the actual
317
+ // operation directly in the worker over daemon-owned CDP.
318
+ const page=targetInfos.find(t=>t.type==='page'&&t.url.startsWith(EXTENSION_PREFIX)&&
319
+ (t.url.includes('/workspace.html')||t.url.includes('/helm.html')));
320
+ if(!page && !extensionConfigured())
321
+ throw Error('WebHelm workspace extension is not installed or enabled');
322
+ const wakeTarget=page?.targetId||(await browser.send('Target.createTarget',{
323
+ url:`${EXTENSION_PREFIX}helm.html?webhelm_wakeup=1`,background:true})).targetId;
324
+ const temporary=!page;
325
+ let wakeSession;
326
+ try {
327
+ ({sessionId:wakeSession}=await browser.send('Target.attachToTarget',{targetId:wakeTarget,flatten:true}));
328
+ let awake=false;
330
329
  for(let attempt=0;attempt<20;attempt++){
331
330
  try{
332
- const reply=await browser.send('Runtime.evaluate',{
333
- expression:`typeof chrome!=='undefined'&&chrome.runtime?.id===${JSON.stringify(EXTENSION_ID)}`,
334
- returnByValue:true},sessionId);
335
- if(reply.result?.value===true){ready=true;break;}
336
- }catch{}
337
- await delay(100);
331
+ await evaluateExtension(browser,wakeSession,
332
+ 'chrome.runtime.sendMessage({method:"workspace.list"})');
333
+ awake=true;break;
334
+ }catch(error){
335
+ if(attempt===19)throw error;
336
+ await delay(100);
337
+ }
338
+ }
339
+ if(!awake)throw Error('WebHelm Extension did not awaken');
340
+ } finally {
341
+ if(wakeSession)await browser.send('Target.detachFromTarget',{sessionId:wakeSession}).catch(()=>{});
342
+ if(temporary)await browser.send('Target.closeTarget',{targetId:wakeTarget}).catch(()=>{});
343
+ }
344
+ for(let attempt=0;attempt<30;attempt++){
345
+ const {targetInfos:updated=[]}=await browser.send('Target.getTargets');
346
+ const active=updated.find(t=>t.type==='service_worker'&&t.url.startsWith(EXTENSION_PREFIX));
347
+ if(active){
348
+ const {sessionId}=await browser.send('Target.attachToTarget',{targetId:active.targetId,flatten:true});
349
+ debuggerSession={targetId:active.targetId,sessionId};
350
+ try{return await evaluateExtension(browser,sessionId,
351
+ `globalThis.browserWorkspaceManagerRpc(${request})`);}
352
+ catch(error){debuggerSession=null;throw error;}
338
353
  }
339
- if(!ready)throw Error('WebHelm installed extension did not initialize its background page');
340
- return await evaluateExtension(browser,sessionId,`chrome.runtime.sendMessage(${request})`);
341
- }finally{
342
- if(sessionId)await browser.send('Target.detachFromTarget',{sessionId}).catch(()=>{});
343
- await browser.send('Target.closeTarget',{targetId}).catch(()=>{});
354
+ await delay(100);
344
355
  }
356
+ throw Error('WebHelm Extension Service Worker did not become available after wake');
345
357
  }
346
358
 
347
359
  function jsonSafe(value){try{JSON.stringify(value);return {value}}catch{return {value_repr:String(value)}}}