gm-skill 2.0.1686 → 2.0.1688

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1686",
3
+ "version": "2.0.1688",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1164,26 +1164,41 @@ function fetchJsonSync(url, timeoutMs) {
1164
1164
  try { return JSON.parse(r.stdout); } catch (_) { return null; }
1165
1165
  }
1166
1166
 
1167
- function startManagedBrowser(pw, profileDir) {
1168
- const headless = process.env.GM_BROWSER_HEADLESS === '1';
1169
- let browserBin = findInstalledChromiumBinary();
1170
- if (!browserBin) {
1171
- logEvent('plugkit', 'browser.chromium-installing', {});
1172
- spawnSync(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['--yes', 'playwright', 'install', 'chromium'], {
1173
- encoding: 'utf-8',
1174
- timeout: 300000,
1175
- windowsHide: true,
1176
- shell: process.platform === 'win32',
1177
- stdio: 'ignore',
1178
- });
1179
- browserBin = findInstalledChromiumBinary();
1167
+ function closeExtraBlankTabs(port, keepWsEndpoint) {
1168
+ try {
1169
+ const targets = fetchJsonSync(`http://127.0.0.1:${port}/json/list`, 1500);
1170
+ if (!Array.isArray(targets)) return { closed: 0 };
1171
+ const pages = targets.filter(t => t && t.type === 'page');
1172
+ const blank = pages.filter(t => t.url === 'about:blank' || t.url === '');
1173
+ const nonBlankCount = pages.length - blank.length;
1174
+ const keepCount = nonBlankCount > 0 ? 0 : 1;
1175
+ const toClose = blank.slice(0, Math.max(0, blank.length - keepCount));
1176
+ let closed = 0;
1177
+ for (const t of toClose) {
1178
+ if (t.webSocketDebuggerUrl === keepWsEndpoint) continue;
1179
+ const r = spawnSync(process.execPath, ['-e', `
1180
+ const http = require('http');
1181
+ const req = http.get(${JSON.stringify(`http://127.0.0.1:${port}/json/close/${t.id}`)}, res => { res.resume(); res.on('end', () => process.exit(0)); });
1182
+ req.on('error', () => process.exit(1));
1183
+ req.setTimeout(1500, () => { req.destroy(); process.exit(1); });
1184
+ `], { timeout: 3000, windowsHide: true });
1185
+ if (r.status === 0) closed++;
1186
+ }
1187
+ return { closed };
1188
+ } catch (_) {
1189
+ return { closed: 0 };
1180
1190
  }
1181
- if (!browserBin) {
1182
- const err = new Error('chromium binary not found after install attempt');
1183
- logEvent('plugkit', 'browser.launch-failed', { reason: 'chromium-missing' });
1184
- throw err;
1191
+ }
1192
+
1193
+ function chromeLogHasSandboxDenied(chromeLogPath) {
1194
+ try {
1195
+ return /Sandbox cannot access executable/.test(fs.readFileSync(chromeLogPath, 'utf-8'));
1196
+ } catch (_) {
1197
+ return false;
1185
1198
  }
1186
- const port = findFreePortSync();
1199
+ }
1200
+
1201
+ function spawnChromiumOnce(browserBin, profileDir, port, headless, noSandbox) {
1187
1202
  const args = [
1188
1203
  '--user-data-dir=' + profileDir,
1189
1204
  '--remote-debugging-port=' + port,
@@ -1193,7 +1208,7 @@ function startManagedBrowser(pw, profileDir) {
1193
1208
  '--disable-default-apps',
1194
1209
  '--disable-gpu-process-crash-limit',
1195
1210
  ];
1196
- if (process.env.GM_BROWSER_NO_SANDBOX === '1') {
1211
+ if (noSandbox) {
1197
1212
  args.push('--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage');
1198
1213
  }
1199
1214
  if (headless) {
@@ -1211,27 +1226,70 @@ function startManagedBrowser(pw, profileDir) {
1211
1226
  env: process.env,
1212
1227
  });
1213
1228
  try { if (typeof logFd === 'number') fs.closeSync(logFd); } catch (_) {}
1214
- const pid = child.pid;
1215
1229
  child.unref();
1216
- logEvent('plugkit', 'browser.chromium-launched', { pid, port, profileDir, headless, binary: browserBin, chromeLogPath });
1230
+ return { pid: child.pid, chromeLogPath };
1231
+ }
1232
+
1233
+ function waitForCdpReady(port, deadlineMs) {
1217
1234
  const start = Date.now();
1218
- const deadline = start + 30000;
1219
- let wsEndpoint = null;
1220
- let lastErr = null;
1235
+ const deadline = start + deadlineMs;
1221
1236
  while (Date.now() < deadline) {
1222
1237
  const info = fetchJsonSync(`http://127.0.0.1:${port}/json/version`, 1500);
1223
- if (info && info.webSocketDebuggerUrl) {
1224
- wsEndpoint = info.webSocketDebuggerUrl;
1225
- break;
1226
- }
1238
+ if (info && info.webSocketDebuggerUrl) return { wsEndpoint: info.webSocketDebuggerUrl, ms: Date.now() - start };
1227
1239
  sleepSync(500);
1228
1240
  }
1229
- if (!wsEndpoint) {
1230
- logEvent('plugkit', 'browser.launch-failed', { reason: 'cdp-not-ready', pid, port, elapsed_ms: Date.now() - start });
1231
- throw new Error(`chromium launched (pid=${pid}) but CDP at 127.0.0.1:${port} did not become ready within 30s${lastErr ? ' :: ' + lastErr : ''}`);
1241
+ return null;
1242
+ }
1243
+
1244
+ function startManagedBrowser(pw, profileDir) {
1245
+ const headless = process.env.GM_BROWSER_HEADLESS === '1';
1246
+ logEvent('plugkit', 'browser.headless-mode-resolved', { headless, source: headless ? 'GM_BROWSER_HEADLESS=1' : 'default-headful' });
1247
+ let browserBin = findInstalledChromiumBinary();
1248
+ if (!browserBin) {
1249
+ logEvent('plugkit', 'browser.chromium-installing', {});
1250
+ spawnSync(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['--yes', 'playwright', 'install', 'chromium'], {
1251
+ encoding: 'utf-8',
1252
+ timeout: 300000,
1253
+ windowsHide: true,
1254
+ shell: process.platform === 'win32',
1255
+ stdio: 'ignore',
1256
+ });
1257
+ browserBin = findInstalledChromiumBinary();
1258
+ }
1259
+ if (!browserBin) {
1260
+ const err = new Error('chromium binary not found after install attempt');
1261
+ logEvent('plugkit', 'browser.launch-failed', { reason: 'chromium-missing' });
1262
+ throw err;
1263
+ }
1264
+ const port = findFreePortSync();
1265
+ let noSandbox = process.env.GM_BROWSER_NO_SANDBOX === '1';
1266
+ let { pid, chromeLogPath } = spawnChromiumOnce(browserBin, profileDir, port, headless, noSandbox);
1267
+ logEvent('plugkit', 'browser.chromium-launched', { pid, port, profileDir, headless, noSandbox, binary: browserBin, chromeLogPath });
1268
+ let ready = waitForCdpReady(port, 30000);
1269
+ if (!ready) {
1270
+ logEvent('plugkit', 'browser.launch-failed', { reason: 'cdp-not-ready', pid, port });
1271
+ throw new Error(`chromium launched (pid=${pid}) but CDP at 127.0.0.1:${port} did not become ready within 30s`);
1272
+ }
1273
+ if (!noSandbox && chromeLogHasSandboxDenied(chromeLogPath)) {
1274
+ logEvent('plugkit', 'browser.sandbox-fallback-engaged', { pid, port, profileDir, reason: 'sandbox-access-denied-detected-in-chrome-launch-log' });
1275
+ try { process.kill(pid, 'SIGTERM'); } catch (_) {}
1276
+ sleepSyncMs(500);
1277
+ try { killPidQuiet(pid); } catch (_) {}
1278
+ purgeProfileLockFiles(profileDir);
1279
+ noSandbox = true;
1280
+ const port2 = findFreePortSync();
1281
+ ({ pid, chromeLogPath } = spawnChromiumOnce(browserBin, profileDir, port2, headless, noSandbox));
1282
+ logEvent('plugkit', 'browser.chromium-launched', { pid, port: port2, profileDir, headless, noSandbox, binary: browserBin, chromeLogPath, retry: true });
1283
+ ready = waitForCdpReady(port2, 30000);
1284
+ if (!ready) {
1285
+ logEvent('plugkit', 'browser.launch-failed', { reason: 'cdp-not-ready-after-sandbox-fallback', pid, port: port2 });
1286
+ throw new Error(`chromium sandbox-fallback relaunch (pid=${pid}) but CDP at 127.0.0.1:${port2} did not become ready within 30s`);
1287
+ }
1288
+ logEvent('plugkit', 'browser.cdp-ready', { pid, port: port2, ms: ready.ms, wsEndpoint: ready.wsEndpoint, noSandbox: true });
1289
+ return { pid, port: port2, wsEndpoint: ready.wsEndpoint };
1232
1290
  }
1233
- logEvent('plugkit', 'browser.cdp-ready', { pid, port, ms: Date.now() - start, wsEndpoint });
1234
- return { pid, port, wsEndpoint };
1291
+ logEvent('plugkit', 'browser.cdp-ready', { pid, port, ms: ready.ms, wsEndpoint: ready.wsEndpoint });
1292
+ return { pid, port, wsEndpoint: ready.wsEndpoint };
1235
1293
  }
1236
1294
 
1237
1295
  function killPidQuiet(pid) {
@@ -1283,6 +1341,18 @@ function gracefulCloseBrowser(entry, reason) {
1283
1341
  try { logEvent('plugkit', 'browser.closed', { reason: reason || 'closed', pid, port, profileDir }); } catch (_) {}
1284
1342
  }
1285
1343
 
1344
+ function checkSessionNavigatedAway(port, claudeSessionId) {
1345
+ try {
1346
+ const list = fetchJsonSync(`http://127.0.0.1:${port}/json/list`, 1000);
1347
+ if (!Array.isArray(list)) return;
1348
+ const pages = list.filter(t => t && t.type === 'page');
1349
+ const stray = pages.filter(t => /^(chrome:\/\/new-tab-page|about:blank|chrome:\/\/newtab)/i.test(String(t.url || '')));
1350
+ if (pages.length > 0 && stray.length === pages.length) {
1351
+ logEvent('plugkit', 'browser.session-navigated-away', { sid: claudeSessionId, port, urls: pages.map(p => p.url) });
1352
+ }
1353
+ } catch (_) {}
1354
+ }
1355
+
1286
1356
  function resolveExistingBrowserEntry(cwd, claudeSessionId, pw, portsFile, sessionsFile, ports, sessions) {
1287
1357
  const existing = ports[claudeSessionId];
1288
1358
  if (!(existing && existing.pid && existing.wsEndpoint)) return null;
@@ -1292,7 +1362,10 @@ function resolveExistingBrowserEntry(cwd, claudeSessionId, pw, portsFile, sessio
1292
1362
  const cdpOk = pidOk && !!fetchJsonSync(`http://127.0.0.1:${existing.port}/json/version`, 1000);
1293
1363
  if (pidOk && profileOk && cdpOk) {
1294
1364
  const pwIds = sessions[claudeSessionId] || [];
1295
- if (pwIds.length > 0 && existing.pwSessionId) return existing.pwSessionId;
1365
+ if (pwIds.length > 0 && existing.pwSessionId) {
1366
+ checkSessionNavigatedAway(existing.port, claudeSessionId);
1367
+ return existing.pwSessionId;
1368
+ }
1296
1369
  const r = runBrowserRunner(pw, ['session', 'new', '--direct', existing.wsEndpoint], 30000, cwd, claudeSessionId);
1297
1370
  if (r && r.status === 0) {
1298
1371
  const sid = parseSessionId(r.stdout || '');
@@ -1362,7 +1435,11 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
1362
1435
  && fetchJsonSync(`http://127.0.0.1:${winner.port}/json/version`, 1000)) {
1363
1436
  const a = runBrowserRunner(pw, ['session', 'new', '--direct', winner.wsEndpoint], 30000, cwd, claudeSessionId);
1364
1437
  const sid = a && a.status === 0 ? parseSessionId(a.stdout || '') : null;
1365
- if (sid) { logEvent('plugkit', 'browser.attached', { pwSessionId: sid, reused: true, via: 'spawn-lock-wait' }); return sid; }
1438
+ if (sid) {
1439
+ checkSessionNavigatedAway(winner.port, claudeSessionId);
1440
+ logEvent('plugkit', 'browser.attached', { pwSessionId: sid, reused: true, via: 'spawn-lock-wait' });
1441
+ return sid;
1442
+ }
1366
1443
  }
1367
1444
  if (Date.now() > spawnDeadline) break;
1368
1445
  sleepSyncMs(300);
@@ -1391,13 +1468,15 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
1391
1468
  }
1392
1469
  return null;
1393
1470
  })();
1394
- let browserPid, port, wsEndpoint;
1471
+ let browserPid, port, wsEndpoint, freshLaunch;
1395
1472
  if (aliveCdpForProfile) {
1396
1473
  ({ pid: browserPid, port, wsEndpoint } = aliveCdpForProfile);
1474
+ freshLaunch = false;
1397
1475
  logEvent('plugkit', 'browser.reused-existing-chromium', { pid: browserPid, port, profileDir });
1398
1476
  } else {
1399
1477
  logEvent('plugkit', 'browser.start', { profileDir });
1400
1478
  ({ pid: browserPid, port, wsEndpoint } = startManagedBrowser(pw, profileDir));
1479
+ freshLaunch = true;
1401
1480
  }
1402
1481
  markLaunching(browserPid);
1403
1482
  const r = runBrowserRunner(pw, ['session', 'new', '--direct', wsEndpoint], 30000, cwd, claudeSessionId);
@@ -1406,6 +1485,10 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
1406
1485
  logEvent('plugkit', 'browser.launch-failed', { reason: 'session-attach-failed', pid: browserPid, port, error: errTxt });
1407
1486
  throw new Error(`playwriter session new --direct failed: ${errTxt}`);
1408
1487
  }
1488
+ if (freshLaunch) {
1489
+ const { closed } = closeExtraBlankTabs(port, wsEndpoint);
1490
+ if (closed > 0) logEvent('plugkit', 'browser.extra-blank-tabs-closed', { pid: browserPid, port, closed });
1491
+ }
1409
1492
  const pwSessionId = parseSessionId(r.stdout || '');
1410
1493
  if (!pwSessionId) {
1411
1494
  logEvent('plugkit', 'browser.launch-failed', { reason: 'session-id-unparseable', stdout: r.stdout });
@@ -2299,7 +2382,7 @@ function makeHostFunctions(instanceRef) {
2299
2382
  stampBrowserLastUse(cwd, sessionId);
2300
2383
  return writeWasmJson(instanceRef.value, {
2301
2384
  ok: true,
2302
- stdout: `Session ${pwSessionId} attached to locally-profiled chromium at ${path.join(cwd, '.gm', 'browser-profile')}`,
2385
+ stdout: `Session ${pwSessionId} attached to locally-profiled chromium at ${sessionProfileDir(cwd, sessionId)}`,
2303
2386
  stderr: '',
2304
2387
  exit_code: 0,
2305
2388
  session_id: pwSessionId,
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1686",
3
+ "version": "2.0.1688",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1686",
3
+ "version": "2.0.1688",
4
4
  "description": "Canonical universal harness — AI-native software engineering via skill-driven orchestration; bootstraps plugkit for task execution and session isolation. Install in any AI coding agent host.",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",