gm-plugkit 2.0.1304 → 2.0.1306

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": "gm-plugkit",
3
- "version": "2.0.1304",
3
+ "version": "2.0.1306",
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": {
@@ -381,7 +381,18 @@ function emitOrchestratorEvents(verb, taskBase, resultStr) {
381
381
  if (verb === 'prd-resolve' && errData && errData.deviation_kind === 'prd-resolve-unknown-id') {
382
382
  logEvent('hook', 'deviation.prd-resolve-unknown-id', { task: taskBase, prd_id: errData.prd_id, reason: errData.error });
383
383
  }
384
- logEvent('plugkit', 'orchestrator.error', { verb, task: taskBase, error: parsed && parsed.error ? String(parsed.error) : 'unknown' });
384
+ const reason = (parsed && (parsed.reason || parsed.error)) ||
385
+ (parsed && parsed.data && (parsed.data.reason || parsed.data.error)) ||
386
+ (errData && (errData.reason || errData.error)) ||
387
+ (parsed && parsed.stderr) ||
388
+ 'unknown';
389
+ logEvent('plugkit', 'orchestrator.error', {
390
+ verb,
391
+ task: taskBase,
392
+ error: String(reason).slice(0, 500),
393
+ gate_denied: !!(parsed && parsed.gate_denied),
394
+ next_dispatch: parsed && parsed.next_dispatch || null,
395
+ });
385
396
  return;
386
397
  }
387
398
  const data = parsed.data || {};
@@ -722,105 +733,84 @@ function findInstalledChromiumBinary() {
722
733
  }
723
734
  }
724
735
 
736
+ function fetchJsonSync(url, timeoutMs) {
737
+ const r = spawnSync(process.execPath, ['-e', `
738
+ const http = require('http');
739
+ const req = http.get(${JSON.stringify(url)}, (res) => {
740
+ let buf = '';
741
+ res.on('data', d => buf += d);
742
+ res.on('end', () => {
743
+ if (res.statusCode !== 200) { process.stderr.write('status ' + res.statusCode); process.exit(2); }
744
+ process.stdout.write(buf);
745
+ });
746
+ });
747
+ req.on('error', e => { process.stderr.write(e.message); process.exit(1); });
748
+ req.setTimeout(${timeoutMs || 1500}, () => { req.destroy(new Error('timeout')); });
749
+ `], { encoding: 'utf-8', timeout: (timeoutMs || 1500) + 1500, windowsHide: true });
750
+ if (r.status !== 0) return null;
751
+ try { return JSON.parse(r.stdout); } catch (_) { return null; }
752
+ }
753
+
725
754
  function startManagedBrowser(pw, profileDir) {
726
755
  const headless = process.env.GM_BROWSER_HEADLESS === '1';
727
- const args = [...pw.baseArgs, 'browser', 'start', '--user-data-dir', profileDir];
728
- if (headless) args.push('--headless');
729
- const env = { ...process.env };
730
- if (!env.GM_BROWSER_RUNNER_PATH && !env.PLAYWRITER_BROWSER_PATH) {
731
- const browserBin = findInstalledChromiumBinary();
732
- if (browserBin) {
733
- env.GM_BROWSER_RUNNER_PATH = browserBin;
734
- env.PLAYWRITER_BROWSER_PATH = browserBin;
735
- logEvent('plugkit', 'browser.binary-resolved', { path: browserBin });
736
- } else {
737
- logEvent('plugkit', 'browser.binary-missing', {});
738
- }
756
+ let browserBin = findInstalledChromiumBinary();
757
+ if (!browserBin) {
758
+ logEvent('plugkit', 'browser.chromium-installing', {});
759
+ spawnSync(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['--yes', 'playwright', 'install', 'chromium'], {
760
+ encoding: 'utf-8',
761
+ timeout: 300000,
762
+ windowsHide: true,
763
+ shell: process.platform === 'win32',
764
+ stdio: 'ignore',
765
+ });
766
+ browserBin = findInstalledChromiumBinary();
767
+ }
768
+ if (!browserBin) {
769
+ const err = new Error('chromium binary not found after install attempt');
770
+ logEvent('plugkit', 'browser.launch-failed', { reason: 'chromium-missing' });
771
+ throw err;
739
772
  }
740
- const child = spawn(pw.cmd, args, {
773
+ const port = findFreePortSync();
774
+ const args = [
775
+ '--user-data-dir=' + profileDir,
776
+ '--remote-debugging-port=' + port,
777
+ '--remote-debugging-address=127.0.0.1',
778
+ '--no-first-run',
779
+ '--no-default-browser-check',
780
+ '--disable-default-apps',
781
+ ];
782
+ if (headless) args.push('--headless=new');
783
+ const chromeLogPath = path.join(profileDir, '.chrome-launch.log');
784
+ let logFd;
785
+ try { logFd = fs.openSync(chromeLogPath, 'a'); } catch (_) { logFd = null; }
786
+ const child = spawn(browserBin, args, {
741
787
  detached: true,
742
- stdio: 'ignore',
743
- shell: pw.shell,
744
- windowsHide: true,
745
- env,
746
- ...(process.platform === 'win32' ? { creationFlags: 0x08000000 | 0x00000008 } : {}),
788
+ stdio: ['ignore', logFd != null ? logFd : 'ignore', logFd != null ? logFd : 'ignore'],
789
+ windowsHide: false,
790
+ env: process.env,
747
791
  });
792
+ try { if (typeof logFd === 'number') fs.closeSync(logFd); } catch (_) {}
748
793
  const pid = child.pid;
749
794
  child.unref();
750
- return pid;
751
- }
752
-
753
- function isColdRunProfile(profileDir) {
754
- try {
755
- if (!fs.existsSync(profileDir)) return true;
756
- const entries = fs.readdirSync(profileDir);
757
- if (entries.length === 0) return true;
758
- if (!entries.some(n => n === 'Default' || n === 'Local State')) return true;
759
- return false;
760
- } catch (_) {
761
- return true;
762
- }
763
- }
764
-
765
- function resolveManagedBrowserKey(pw) {
766
- const explicitKey = process.env.GM_BROWSER_RUNNER_KEY || process.env.PLAYWRITER_BROWSER_KEY;
767
- if (explicitKey) {
768
- if (/^profile:/.test(explicitKey)) {
769
- throw new Error(`GM_BROWSER_RUNNER_KEY=${explicitKey} points at a user OS browser profile; refusing — managed sessions must use install:Chromium:*`);
770
- }
771
- return explicitKey;
772
- }
773
- let text = '';
774
- try {
775
- const r = runBrowserRunner(pw, ['browser', 'list'], 8000);
776
- text = (r && (r.stdout || r.stderr)) || '';
777
- } catch (e) {
778
- throw new Error(`managed browser session list failed: ${e.message}`);
779
- }
780
- const lines = text.split(/\r?\n/);
781
- const installChromium = lines.map(l => (l.match(/\b(install:Chromium:[A-Za-z0-9_-]+)\b/) || [])[1]).find(Boolean);
782
- if (installChromium) return installChromium;
783
- throw new Error(`no managed Chromium install detected; browser runner returned: ${scrubBrowserRunnerText(text).trim()}`);
784
- }
785
-
786
- function waitForExtensionReady(pw, profileDir, opts) {
787
- const cold = (opts && typeof opts.cold === 'boolean') ? opts.cold : isColdRunProfile(profileDir);
788
- const timeoutMs = (opts && opts.timeoutMs) || (cold ? 180000 : 30000);
795
+ logEvent('plugkit', 'browser.chromium-launched', { pid, port, profileDir, headless, binary: browserBin, chromeLogPath });
789
796
  const start = Date.now();
790
- const deadline = start + timeoutMs;
791
- const backoff = [2000, 4000, 8000];
792
- let attempt = 0;
793
- let lastErr = '';
794
- let lastProgressAt = start;
795
- const browserKey = resolveManagedBrowserKey(pw);
797
+ const deadline = start + 30000;
798
+ let wsEndpoint = null;
799
+ let lastErr = null;
796
800
  while (Date.now() < deadline) {
797
- const remaining = deadline - Date.now();
798
- const innerTimeout = Math.max(28000, Math.min(remaining, 30000));
799
- const r = runBrowserRunner(pw, ['session', 'new', '--browser', browserKey], innerTimeout);
800
- if (r && r.status === 0) return r;
801
- lastErr = scrubBrowserRunnerText((r && (r.stderr || r.stdout)) || '');
802
- const now = Date.now();
803
- if (now - lastProgressAt >= 10000) {
804
- logEvent('plugkit', 'browser.extension-wait', {
805
- elapsed_ms: now - start,
806
- cold_run: cold,
807
- profileDir,
808
- attempt,
809
- });
810
- lastProgressAt = now;
801
+ const info = fetchJsonSync(`http://127.0.0.1:${port}/json/version`, 1500);
802
+ if (info && info.webSocketDebuggerUrl) {
803
+ wsEndpoint = info.webSocketDebuggerUrl;
804
+ break;
811
805
  }
812
- const sleepMs = backoff[Math.min(attempt, backoff.length - 1)];
813
- attempt++;
814
- if (Date.now() + sleepMs >= deadline) break;
815
- sleepSync(sleepMs);
806
+ sleepSync(500);
807
+ }
808
+ if (!wsEndpoint) {
809
+ logEvent('plugkit', 'browser.launch-failed', { reason: 'cdp-not-ready', pid, port, elapsed_ms: Date.now() - start });
810
+ throw new Error(`chromium launched (pid=${pid}) but CDP at 127.0.0.1:${port} did not become ready within 30s${lastErr ? ' :: ' + lastErr : ''}`);
816
811
  }
817
- const flavor = cold
818
- ? `cold-run timeout after ${Math.round((Date.now() - start) / 1000)}s waiting for managed browser extension to connect (first run downloads chromium ~150MB and installs the extension; if this persists the extension never registered with the relay server)`
819
- : `warm-run timeout after ${Math.round((Date.now() - start) / 1000)}s waiting for managed browser extension to reconnect (profile exists but extension is not registering; relay server may be wedged)`;
820
- const err = new Error(`managed browser session start failed: ${flavor}${lastErr ? ` :: ${lastErr}` : ''}`);
821
- err._lastErr = lastErr;
822
- err._coldRun = cold;
823
- throw err;
812
+ logEvent('plugkit', 'browser.cdp-ready', { pid, port, ms: Date.now() - start, wsEndpoint });
813
+ return { pid, port, wsEndpoint };
824
814
  }
825
815
 
826
816
  function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
@@ -830,15 +820,29 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
830
820
  const ports = readJsonFile(portsFile, {});
831
821
  const sessions = readJsonFile(sessionsFile, {});
832
822
  const existing = ports[claudeSessionId];
833
- if (existing && existing.pid) {
823
+ if (existing && existing.pid && existing.wsEndpoint) {
834
824
  const wantProfile = path.join(cwd, '.gm', 'browser-profile');
835
825
  const pidOk = isProcessAliveSync(existing.pid);
836
826
  const profileOk = !existing.profileDir || existing.profileDir === wantProfile || existing.profileDir.startsWith(path.join(cwd, '.gm', 'browser-profile'));
837
- if (pidOk && profileOk) {
827
+ const cdpOk = pidOk && !!fetchJsonSync(`http://127.0.0.1:${existing.port}/json/version`, 1000);
828
+ if (pidOk && profileOk && cdpOk) {
838
829
  const pwIds = sessions[claudeSessionId] || [];
839
- if (pwIds.length > 0) return pwIds[0];
830
+ if (pwIds.length > 0 && existing.pwSessionId) return existing.pwSessionId;
831
+ const r = runBrowserRunner(pw, ['session', 'new', '--direct', existing.wsEndpoint], 30000);
832
+ if (r && r.status === 0) {
833
+ const sid = parseSessionId(r.stdout || '');
834
+ if (sid) {
835
+ existing.pwSessionId = sid;
836
+ ports[claudeSessionId] = existing;
837
+ sessions[claudeSessionId] = [sid];
838
+ writeJsonFile(portsFile, ports);
839
+ writeJsonFile(sessionsFile, sessions);
840
+ logEvent('plugkit', 'browser.attached', { pwSessionId: sid, reused: true });
841
+ return sid;
842
+ }
843
+ }
840
844
  } else {
841
- const reason = !pidOk ? 'pid-dead' : 'profile-drift';
845
+ const reason = !pidOk ? 'pid-dead' : (!cdpOk ? 'cdp-dead' : 'profile-drift');
842
846
  logEvent('hook', 'deviation.browser-profile-collision', {
843
847
  sid: claudeSessionId,
844
848
  stale_pid: existing.pid || null,
@@ -853,32 +857,58 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
853
857
  }
854
858
  }
855
859
  cleanDeadProfileFragments(cwd);
856
- const probedProfile = path.join(cwd, '.gm', 'browser-profile');
857
- const coldRun = isColdRunProfile(probedProfile);
858
860
  const profileDir = acquireProfileDir(cwd);
859
- logEvent('plugkit', 'browser.start', { profileDir, cold_run: coldRun });
860
- const browserPid = startManagedBrowser(pw, profileDir);
861
- const newR = waitForExtensionReady(pw, profileDir, { cold: coldRun });
862
- const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
863
- const out = stripAnsi(newR.stdout || '').trim();
864
- let pwSessionId = null;
865
- const created = out.match(/Session\s+(\S+)\s+created/i);
866
- if (created) pwSessionId = created[1];
867
- if (!pwSessionId) {
868
- const hex = out.match(/\b([a-f0-9-]{8,})\b/i);
869
- if (hex) pwSessionId = hex[1];
861
+ const aliveCdpForProfile = (() => {
862
+ for (const key of Object.keys(ports)) {
863
+ const ent = ports[key];
864
+ if (!ent || !ent.pid || !ent.port || !ent.wsEndpoint) continue;
865
+ if (ent.profileDir !== profileDir && !(ent.profileDir || '').startsWith(profileDir)) continue;
866
+ if (!isProcessAliveSync(ent.pid)) continue;
867
+ const info = fetchJsonSync(`http://127.0.0.1:${ent.port}/json/version`, 1000);
868
+ if (info && info.webSocketDebuggerUrl) {
869
+ return { pid: ent.pid, port: ent.port, wsEndpoint: ent.wsEndpoint };
870
+ }
871
+ }
872
+ return null;
873
+ })();
874
+ let browserPid, port, wsEndpoint;
875
+ if (aliveCdpForProfile) {
876
+ ({ pid: browserPid, port, wsEndpoint } = aliveCdpForProfile);
877
+ logEvent('plugkit', 'browser.reused-existing-chromium', { pid: browserPid, port, profileDir });
878
+ } else {
879
+ logEvent('plugkit', 'browser.start', { profileDir });
880
+ ({ pid: browserPid, port, wsEndpoint } = startManagedBrowser(pw, profileDir));
870
881
  }
882
+ const r = runBrowserRunner(pw, ['session', 'new', '--direct', wsEndpoint], 30000);
883
+ if (!r || r.status !== 0) {
884
+ const errTxt = scrubBrowserRunnerText((r && (r.stderr || r.stdout)) || 'unknown');
885
+ logEvent('plugkit', 'browser.launch-failed', { reason: 'session-attach-failed', pid: browserPid, port, error: errTxt });
886
+ throw new Error(`playwriter session new --direct failed: ${errTxt}`);
887
+ }
888
+ const pwSessionId = parseSessionId(r.stdout || '');
871
889
  if (!pwSessionId) {
872
- try { const j = JSON.parse(out); pwSessionId = j.id || j.session_id || j.session; } catch (_) {}
890
+ logEvent('plugkit', 'browser.launch-failed', { reason: 'session-id-unparseable', stdout: r.stdout });
891
+ throw new Error(`could not parse managed browser session id from: ${scrubBrowserRunnerText(r.stdout || '')}`);
873
892
  }
874
- if (!pwSessionId) throw new Error(`could not parse managed browser session id from: ${scrubBrowserRunnerText(out)}`);
875
- ports[claudeSessionId] = { profileDir, pid: browserPid };
893
+ ports[claudeSessionId] = { profileDir, pid: browserPid, port, wsEndpoint, pwSessionId };
876
894
  sessions[claudeSessionId] = [pwSessionId];
877
895
  writeJsonFile(portsFile, ports);
878
896
  writeJsonFile(sessionsFile, sessions);
897
+ logEvent('plugkit', 'browser.attached', { pwSessionId, pid: browserPid, port });
879
898
  return pwSessionId;
880
899
  }
881
900
 
901
+ function parseSessionId(rawOut) {
902
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
903
+ const out = stripAnsi(rawOut || '').trim();
904
+ const created = out.match(/Session\s+(\S+)\s+created/i);
905
+ if (created) return created[1];
906
+ const hex = out.match(/\b([a-f0-9-]{8,})\b/i);
907
+ if (hex) return hex[1];
908
+ try { const j = JSON.parse(out); return j.id || j.session_id || j.session || null; } catch (_) {}
909
+ return null;
910
+ }
911
+
882
912
  const VEC_K_DEFAULT = 10;
883
913
  const EMBED_MODEL_DEFAULT = process.env.EMBED_MODEL || 'mistral/mistral-embed';
884
914
  const INFERENCE_MODEL_DEFAULT = process.env.INFERENCE_MODEL || 'groq/llama-3.3-70b-versatile';