gm-plugkit 2.0.1221 → 2.0.1223

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.1221",
3
+ "version": "2.0.1223",
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": {
@@ -78,6 +78,17 @@ function isBrowserRunningFileLocal(rel) {
78
78
  return false;
79
79
  }
80
80
 
81
+ function hashFileShortLocal(cwd, rel) {
82
+ try {
83
+ const fs = require('fs');
84
+ const path = require('path');
85
+ const crypto = require('crypto');
86
+ const abs = path.isAbsolute(rel) ? rel : path.join(cwd, rel);
87
+ const buf = fs.readFileSync(abs);
88
+ return crypto.createHash('sha256').update(buf).digest('hex').slice(0, 12);
89
+ } catch (_) { return ''; }
90
+ }
91
+
81
92
  function recordBrowserEditLocal(cwd, filePath) {
82
93
  try {
83
94
  const fs = require('fs');
@@ -89,8 +100,11 @@ function recordBrowserEditLocal(cwd, filePath) {
89
100
  fs.mkdirSync(path.dirname(editsFile), { recursive: true });
90
101
  let list = [];
91
102
  try { list = JSON.parse(fs.readFileSync(editsFile, 'utf8')); if (!Array.isArray(list)) list = []; } catch (_) {}
92
- const entry = { file: rel.replace(/\\\\/g, '/'), ts: Date.now() };
93
- if (!list.some(e => e && e.file === entry.file)) list.push(entry);
103
+ const relPath = rel.replace(/\\\\/g, '/');
104
+ const hash = hashFileShortLocal(cwd, relPath);
105
+ const idx = list.findIndex(e => e && e.file === relPath);
106
+ const entry = { file: relPath, ts: Date.now(), hash };
107
+ if (idx === -1) list.push(entry); else list[idx] = entry;
94
108
  fs.writeFileSync(editsFile, JSON.stringify(list));
95
109
  return true;
96
110
  } catch (_) { return false; }
@@ -322,9 +336,24 @@ function dispatchAutoRecall(instance, queryPrompt) {
322
336
  function tryAutoRecallForTurnEntry(instance, sess, cwd) {
323
337
  try {
324
338
  const prompt = readUserPromptForRecall(cwd);
325
- if (!prompt) return null;
326
- const primary = dispatchAutoRecall(instance, prompt);
327
- const fallbackQuery = deriveFallbackQuery(prompt);
339
+ let emptyPromptFallback = false;
340
+ let effectivePrompt = prompt;
341
+ if (!prompt || !String(prompt).trim()) {
342
+ emptyPromptFallback = true;
343
+ const key = sess || '(no-session)';
344
+ const t = _turns.get(key);
345
+ const phase = (t && t.lastPhase) || 'PLAN';
346
+ const phaseQueryMap = {
347
+ PLAN: 'PLAN orient',
348
+ EXECUTE: 'EXECUTE work',
349
+ EMIT: 'EMIT closure',
350
+ VERIFY: 'VERIFY trajectory',
351
+ COMPLETE: 'COMPLETE residual',
352
+ };
353
+ effectivePrompt = phaseQueryMap[phase] || 'PLAN orient';
354
+ }
355
+ const primary = dispatchAutoRecall(instance, effectivePrompt);
356
+ const fallbackQuery = deriveFallbackQuery(effectivePrompt);
328
357
  let fallback = null;
329
358
  if (fallbackQuery && fallbackQuery !== (primary && primary.query)) {
330
359
  fallback = dispatchAutoRecall(instance, fallbackQuery);
@@ -344,12 +373,16 @@ function tryAutoRecallForTurnEntry(instance, sess, cwd) {
344
373
  if (primary && primary.query) queries.push(primary.query);
345
374
  if (fallback && fallback.query && !queries.includes(fallback.query)) queries.push(fallback.query);
346
375
  const payload = {
347
- query: (primary && primary.query) || '',
376
+ query: (primary && primary.query) || effectivePrompt || '',
348
377
  queries,
349
378
  hits: merged.slice(0, 20),
350
379
  fired_at: new Date().toISOString(),
351
380
  turn_entry: true,
352
381
  };
382
+ if (emptyPromptFallback) {
383
+ payload.fallback_reason = 'empty-prompt';
384
+ if (!payload.query) payload.query = effectivePrompt;
385
+ }
353
386
  logEvent('plugkit', 'auto_recall.turn-entry', { sess, queries, count: merged.length });
354
387
  return payload;
355
388
  } catch (e) {
@@ -618,9 +651,48 @@ function ensureGitignored(cwd, entry) {
618
651
  } catch (_) {}
619
652
  }
620
653
 
654
+ function isProcessAliveSync(pid) {
655
+ if (!pid || typeof pid !== 'number' || pid <= 0) return false;
656
+ try {
657
+ process.kill(pid, 0);
658
+ return true;
659
+ } catch (e) {
660
+ return e && e.code === 'EPERM';
661
+ }
662
+ }
663
+
664
+ function readSingletonLockPid(profileDir) {
665
+ const lock = path.join(profileDir, 'SingletonLock');
666
+ try {
667
+ let target;
668
+ try {
669
+ target = fs.readlinkSync(lock);
670
+ } catch (_) {
671
+ try { target = fs.readFileSync(lock, 'utf-8'); } catch (__) { return null; }
672
+ }
673
+ if (!target) return null;
674
+ const m = String(target).match(/-(\d+)\s*$/);
675
+ if (m) return parseInt(m[1], 10);
676
+ const m2 = String(target).match(/(\d+)/);
677
+ if (m2) return parseInt(m2[1], 10);
678
+ } catch (_) {}
679
+ return null;
680
+ }
681
+
621
682
  function isProfileLocked(profileDir) {
622
683
  const lock = path.join(profileDir, 'SingletonLock');
623
- return fs.existsSync(lock);
684
+ if (!fs.existsSync(lock)) return false;
685
+ const holderPid = readSingletonLockPid(profileDir);
686
+ if (holderPid != null && !isProcessAliveSync(holderPid)) {
687
+ try { fs.unlinkSync(lock); } catch (_) {}
688
+ try { fs.unlinkSync(path.join(profileDir, 'SingletonCookie')); } catch (_) {}
689
+ try { fs.unlinkSync(path.join(profileDir, 'SingletonSocket')); } catch (_) {}
690
+ logEvent('bootstrap', 'browser-profile.lock-cleared', {
691
+ profileDir, dead_pid: holderPid,
692
+ });
693
+ return false;
694
+ }
695
+ return true;
624
696
  }
625
697
 
626
698
  function acquireProfileDir(cwd) {
@@ -636,6 +708,31 @@ function acquireProfileDir(cwd) {
636
708
  return fallback;
637
709
  }
638
710
 
711
+ function cleanDeadProfileFragments(cwd) {
712
+ try {
713
+ const gmDir = path.join(cwd, '.gm');
714
+ if (!fs.existsSync(gmDir)) return { cleaned: 0 };
715
+ let cleaned = 0;
716
+ for (const name of fs.readdirSync(gmDir)) {
717
+ const m = name.match(/^browser-profile-(\d+)$/);
718
+ if (!m) continue;
719
+ const pid = parseInt(m[1], 10);
720
+ if (!isProcessAliveSync(pid)) {
721
+ try {
722
+ fs.rmSync(path.join(gmDir, name), { recursive: true, force: true });
723
+ cleaned++;
724
+ } catch (_) {}
725
+ }
726
+ }
727
+ if (cleaned > 0) {
728
+ logEvent('bootstrap', 'browser-profile.hygiene', { cwd, cleaned });
729
+ }
730
+ return { cleaned };
731
+ } catch (_) {
732
+ return { cleaned: 0 };
733
+ }
734
+ }
735
+
639
736
  function findFreePortSync() {
640
737
  const r = spawnSync(process.execPath, ['-e', `
641
738
  const net = require('net');
@@ -689,10 +786,31 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
689
786
  const ports = readJsonFile(portsFile, {});
690
787
  const sessions = readJsonFile(sessionsFile, {});
691
788
  const existing = ports[claudeSessionId];
692
- if (existing && existing.port && isPortAliveSync(existing.port)) {
693
- const pwIds = sessions[claudeSessionId] || [];
694
- if (pwIds.length > 0) return pwIds[0];
789
+ if (existing && existing.port) {
790
+ const wantProfile = path.join(cwd, '.gm', 'browser-profile');
791
+ const pidOk = existing.pid && isProcessAliveSync(existing.pid);
792
+ const profileOk = !existing.profileDir || existing.profileDir === wantProfile || existing.profileDir.startsWith(path.join(cwd, '.gm', 'browser-profile'));
793
+ const portOk = isPortAliveSync(existing.port);
794
+ if (pidOk && profileOk && portOk) {
795
+ const pwIds = sessions[claudeSessionId] || [];
796
+ if (pwIds.length > 0) return pwIds[0];
797
+ } else {
798
+ const reason = !pidOk ? 'pid-dead' : !profileOk ? 'profile-drift' : 'port-dead';
799
+ logEvent('hook', 'deviation.browser-profile-collision', {
800
+ sid: claudeSessionId,
801
+ stale_pid: existing.pid || null,
802
+ stale_port: existing.port || null,
803
+ stale_profile: existing.profileDir || null,
804
+ want_profile: wantProfile,
805
+ reason,
806
+ });
807
+ delete ports[claudeSessionId];
808
+ delete sessions[claudeSessionId];
809
+ try { writeJsonFile(portsFile, ports); } catch (_) {}
810
+ try { writeJsonFile(sessionsFile, sessions); } catch (_) {}
811
+ }
695
812
  }
813
+ cleanDeadProfileFragments(cwd);
696
814
  const chrome = findChrome();
697
815
  if (!chrome) throw new Error('Chrome not found. Please install Google Chrome.');
698
816
  const profileDir = acquireProfileDir(cwd);
@@ -1276,6 +1394,7 @@ function makeHostFunctions(instanceRef) {
1276
1394
  const lang = opts.lang || 'nodejs';
1277
1395
  const cwd = opts.cwd || process.cwd();
1278
1396
  const rawTimeout = opts.timeoutMs;
1397
+ const MIN_TIMEOUT_MS = 100;
1279
1398
  if (rawTimeout === undefined || rawTimeout === null || typeof rawTimeout !== 'number' || !Number.isFinite(rawTimeout) || rawTimeout <= 0 || !Number.isInteger(rawTimeout)) {
1280
1399
  return writeWasmJson(instanceRef.value, {
1281
1400
  ok: false,
@@ -1285,6 +1404,15 @@ function makeHostFunctions(instanceRef) {
1285
1404
  received: rawTimeout === undefined ? null : rawTimeout,
1286
1405
  });
1287
1406
  }
1407
+ if (rawTimeout < MIN_TIMEOUT_MS) {
1408
+ return writeWasmJson(instanceRef.value, {
1409
+ ok: false,
1410
+ error: 'timeoutMs below floor',
1411
+ min: MIN_TIMEOUT_MS,
1412
+ received: rawTimeout,
1413
+ paper_ref: '§20',
1414
+ });
1415
+ }
1288
1416
  const timeoutMs = rawTimeout;
1289
1417
  let cmd, args;
1290
1418
  if (lang === 'nodejs' || lang === 'js') { cmd = process.execPath; args = ['-e', code]; }
@@ -1342,7 +1470,12 @@ function makeHostFunctions(instanceRef) {
1342
1470
  });
1343
1471
  }
1344
1472
  const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
1345
- const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
1473
+ const portsAfter = readJsonFile(browserPortsFile(cwd), {});
1474
+ const livePort = portsAfter[sessionId] && portsAfter[sessionId].port;
1475
+ const directArgs = (livePort && isPortAliveSync(livePort))
1476
+ ? [`--direct=localhost:${livePort}`]
1477
+ : [];
1478
+ const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', ...directArgs, '-e', body], 60000);
1346
1479
  return writeWasmJson(instanceRef.value, {
1347
1480
  ok: r.status === 0,
1348
1481
  stdout: scrubBrowserRunnerText(r.stdout || ''),
@@ -1852,10 +1985,23 @@ async function runSpoolWatcher(instance, spoolDir) {
1852
1985
 
1853
1986
  if (verb === 'browser') {
1854
1987
  try {
1855
- const witnessFile = path.join(process.cwd(), '.gm', 'exec-spool', '.turn-browser-witnessed');
1988
+ const cwd_ = process.cwd();
1989
+ const editsFile = path.join(cwd_, '.gm', 'exec-spool', '.turn-browser-edits.json');
1990
+ const witnessFile = path.join(cwd_, '.gm', 'exec-spool', '.turn-browser-witnessed');
1856
1991
  fs.mkdirSync(path.dirname(witnessFile), { recursive: true });
1857
- fs.writeFileSync(witnessFile, JSON.stringify({ ts: Date.now(), task: taskBase, dur_ms }));
1858
- logEvent('plugkit', 'browser.witness-marked', { task: taskBase });
1992
+ let edits = [];
1993
+ try { edits = JSON.parse(fs.readFileSync(editsFile, 'utf8')); if (!Array.isArray(edits)) edits = []; } catch (_) {}
1994
+ const witnessed_hashes = {};
1995
+ for (const e of edits) {
1996
+ if (!e || !e.file) continue;
1997
+ try {
1998
+ const abs = path.isAbsolute(e.file) ? e.file : path.join(cwd_, e.file);
1999
+ const buf = fs.readFileSync(abs);
2000
+ witnessed_hashes[e.file] = crypto.createHash('sha256').update(buf).digest('hex').slice(0, 12);
2001
+ } catch (_) { witnessed_hashes[e.file] = ''; }
2002
+ }
2003
+ fs.writeFileSync(witnessFile, JSON.stringify({ ts: Date.now(), task: taskBase, dur_ms, witnessed_hashes }));
2004
+ logEvent('plugkit', 'browser.witness-marked', { task: taskBase, files: Object.keys(witnessed_hashes) });
1859
2005
  } catch (_) {}
1860
2006
  }
1861
2007
 
package/plugkit.sha256 CHANGED
@@ -1 +1 @@
1
- {"plugkit.wasm":"9bafe43d985cebd33c63433b532a7f396599905fa1d90c9f411ea292f10fd7c1"}
1
+ {"plugkit.wasm":"0cac0a48e7346d2f75779c9972efe47c3f4ab38063a077660906a53028d2e70a"}
package/plugkit.version CHANGED
@@ -1 +1 @@
1
- 0.1.448
1
+ 0.1.449