gm-skill 2.0.1302 → 2.0.1319
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/AGENTS.md +172 -1
- package/README.md +80 -21
- package/bin/gm-validate.js +329 -0
- package/bin/gmsniff.js +19 -6
- package/bin/plugkit.version +1 -1
- package/bin/plugkit.wasm.sha256 +1 -1
- package/gm-plugkit/package.json +10 -3
- package/gm-plugkit/plugkit-wasm-wrapper.js +354 -162
- package/gm.json +2 -2
- package/package.json +5 -5
- package/skills/gm-skill/SKILL.md +16 -2
- package/LICENSE +0 -21
- package/bin/plugkit.wasm +0 -0
|
@@ -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
|
-
|
|
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,133 @@ 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
|
-
|
|
728
|
-
if (
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
|
|
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
|
|
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
|
-
|
|
744
|
-
|
|
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
|
-
|
|
795
|
+
logEvent('plugkit', 'browser.chromium-launched', { pid, port, profileDir, headless, binary: browserBin, chromeLogPath });
|
|
796
|
+
const start = Date.now();
|
|
797
|
+
const deadline = start + 30000;
|
|
798
|
+
let wsEndpoint = null;
|
|
799
|
+
let lastErr = null;
|
|
800
|
+
while (Date.now() < deadline) {
|
|
801
|
+
const info = fetchJsonSync(`http://127.0.0.1:${port}/json/version`, 1500);
|
|
802
|
+
if (info && info.webSocketDebuggerUrl) {
|
|
803
|
+
wsEndpoint = info.webSocketDebuggerUrl;
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
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 : ''}`);
|
|
811
|
+
}
|
|
812
|
+
logEvent('plugkit', 'browser.cdp-ready', { pid, port, ms: Date.now() - start, wsEndpoint });
|
|
813
|
+
return { pid, port, wsEndpoint };
|
|
751
814
|
}
|
|
752
815
|
|
|
753
|
-
function
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
if (!entries.some(n => n === 'Default' || n === 'Local State')) return true;
|
|
759
|
-
return false;
|
|
760
|
-
} catch (_) {
|
|
761
|
-
return true;
|
|
816
|
+
function killPidQuiet(pid) {
|
|
817
|
+
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return false;
|
|
818
|
+
try { process.kill(pid, 'SIGTERM'); } catch (_) {}
|
|
819
|
+
if (process.platform === 'win32') {
|
|
820
|
+
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', timeout: 3000 }); } catch (_) {}
|
|
762
821
|
}
|
|
822
|
+
return true;
|
|
763
823
|
}
|
|
764
824
|
|
|
765
|
-
function
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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}`);
|
|
825
|
+
function purgeProfileLockFiles(profileDir) {
|
|
826
|
+
if (!profileDir) return;
|
|
827
|
+
for (const name of ['SingletonLock', 'SingletonCookie', 'SingletonSocket']) {
|
|
828
|
+
try { fs.unlinkSync(path.join(profileDir, name)); } catch (_) {}
|
|
779
829
|
}
|
|
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
830
|
}
|
|
785
831
|
|
|
786
|
-
function
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (Date.now() + sleepMs >= deadline) break;
|
|
815
|
-
sleepSync(sleepMs);
|
|
832
|
+
function sleepSyncMs(ms) {
|
|
833
|
+
if (ms <= 0) return;
|
|
834
|
+
spawnSync(process.execPath, ['-e', `setTimeout(()=>process.exit(0),${ms})`], { timeout: ms + 500, windowsHide: true });
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function gracefulCloseBrowser(entry, reason) {
|
|
838
|
+
if (!entry) return;
|
|
839
|
+
const { pid, port, profileDir } = entry;
|
|
840
|
+
if (Number.isFinite(port) && port > 0) {
|
|
841
|
+
try {
|
|
842
|
+
const info = fetchJsonSync(`http://127.0.0.1:${port}/json/version`, 600);
|
|
843
|
+
if (info && info.webSocketDebuggerUrl) {
|
|
844
|
+
spawnSync(process.execPath, ['-e', `
|
|
845
|
+
const http = require('http');
|
|
846
|
+
const req = http.request({host:'127.0.0.1',port:${port},path:'/json/close/browser',method:'GET',timeout:1500},
|
|
847
|
+
res => { res.resume(); res.on('end', () => process.exit(0)); });
|
|
848
|
+
req.on('error', () => process.exit(1));
|
|
849
|
+
req.on('timeout', () => { req.destroy(); process.exit(1); });
|
|
850
|
+
req.end();
|
|
851
|
+
`], { timeout: 3000, windowsHide: true });
|
|
852
|
+
}
|
|
853
|
+
} catch (_) {}
|
|
854
|
+
}
|
|
855
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
856
|
+
const deadline = Date.now() + 1500;
|
|
857
|
+
try { process.kill(pid, 'SIGTERM'); } catch (_) {}
|
|
858
|
+
while (Date.now() < deadline && isProcessAliveSync(pid)) sleepSyncMs(Math.min(150, deadline - Date.now()));
|
|
859
|
+
if (isProcessAliveSync(pid)) killPidQuiet(pid);
|
|
816
860
|
}
|
|
817
|
-
|
|
818
|
-
|
|
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;
|
|
861
|
+
purgeProfileLockFiles(profileDir);
|
|
862
|
+
try { logEvent('plugkit', 'browser.closed', { reason: reason || 'closed', pid, port, profileDir }); } catch (_) {}
|
|
824
863
|
}
|
|
825
864
|
|
|
826
865
|
function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
@@ -830,15 +869,29 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
830
869
|
const ports = readJsonFile(portsFile, {});
|
|
831
870
|
const sessions = readJsonFile(sessionsFile, {});
|
|
832
871
|
const existing = ports[claudeSessionId];
|
|
833
|
-
if (existing && existing.pid) {
|
|
872
|
+
if (existing && existing.pid && existing.wsEndpoint) {
|
|
834
873
|
const wantProfile = path.join(cwd, '.gm', 'browser-profile');
|
|
835
874
|
const pidOk = isProcessAliveSync(existing.pid);
|
|
836
875
|
const profileOk = !existing.profileDir || existing.profileDir === wantProfile || existing.profileDir.startsWith(path.join(cwd, '.gm', 'browser-profile'));
|
|
837
|
-
|
|
876
|
+
const cdpOk = pidOk && !!fetchJsonSync(`http://127.0.0.1:${existing.port}/json/version`, 1000);
|
|
877
|
+
if (pidOk && profileOk && cdpOk) {
|
|
838
878
|
const pwIds = sessions[claudeSessionId] || [];
|
|
839
|
-
if (pwIds.length > 0) return
|
|
879
|
+
if (pwIds.length > 0 && existing.pwSessionId) return existing.pwSessionId;
|
|
880
|
+
const r = runBrowserRunner(pw, ['session', 'new', '--direct', existing.wsEndpoint], 30000);
|
|
881
|
+
if (r && r.status === 0) {
|
|
882
|
+
const sid = parseSessionId(r.stdout || '');
|
|
883
|
+
if (sid) {
|
|
884
|
+
existing.pwSessionId = sid;
|
|
885
|
+
ports[claudeSessionId] = existing;
|
|
886
|
+
sessions[claudeSessionId] = [sid];
|
|
887
|
+
writeJsonFile(portsFile, ports);
|
|
888
|
+
writeJsonFile(sessionsFile, sessions);
|
|
889
|
+
logEvent('plugkit', 'browser.attached', { pwSessionId: sid, reused: true });
|
|
890
|
+
return sid;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
840
893
|
} else {
|
|
841
|
-
const reason = !pidOk ? 'pid-dead' : 'profile-drift';
|
|
894
|
+
const reason = !pidOk ? 'pid-dead' : (!cdpOk ? 'cdp-dead' : 'profile-drift');
|
|
842
895
|
logEvent('hook', 'deviation.browser-profile-collision', {
|
|
843
896
|
sid: claudeSessionId,
|
|
844
897
|
stale_pid: existing.pid || null,
|
|
@@ -846,6 +899,12 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
846
899
|
want_profile: wantProfile,
|
|
847
900
|
reason,
|
|
848
901
|
});
|
|
902
|
+
if (typeof gracefulCloseBrowser === 'function') {
|
|
903
|
+
try { gracefulCloseBrowser(existing, `collision:${reason}`); } catch (_) {}
|
|
904
|
+
} else if (pidOk && Number.isFinite(existing.pid)) {
|
|
905
|
+
try { killPidQuiet(existing.pid); } catch (_) {}
|
|
906
|
+
}
|
|
907
|
+
purgeProfileLockFiles(existing.profileDir);
|
|
849
908
|
delete ports[claudeSessionId];
|
|
850
909
|
delete sessions[claudeSessionId];
|
|
851
910
|
try { writeJsonFile(portsFile, ports); } catch (_) {}
|
|
@@ -853,32 +912,58 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
853
912
|
}
|
|
854
913
|
}
|
|
855
914
|
cleanDeadProfileFragments(cwd);
|
|
856
|
-
const probedProfile = path.join(cwd, '.gm', 'browser-profile');
|
|
857
|
-
const coldRun = isColdRunProfile(probedProfile);
|
|
858
915
|
const profileDir = acquireProfileDir(cwd);
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
916
|
+
const aliveCdpForProfile = (() => {
|
|
917
|
+
for (const key of Object.keys(ports)) {
|
|
918
|
+
const ent = ports[key];
|
|
919
|
+
if (!ent || !ent.pid || !ent.port || !ent.wsEndpoint) continue;
|
|
920
|
+
if (ent.profileDir !== profileDir && !(ent.profileDir || '').startsWith(profileDir)) continue;
|
|
921
|
+
if (!isProcessAliveSync(ent.pid)) continue;
|
|
922
|
+
const info = fetchJsonSync(`http://127.0.0.1:${ent.port}/json/version`, 1000);
|
|
923
|
+
if (info && info.webSocketDebuggerUrl) {
|
|
924
|
+
return { pid: ent.pid, port: ent.port, wsEndpoint: ent.wsEndpoint };
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
})();
|
|
929
|
+
let browserPid, port, wsEndpoint;
|
|
930
|
+
if (aliveCdpForProfile) {
|
|
931
|
+
({ pid: browserPid, port, wsEndpoint } = aliveCdpForProfile);
|
|
932
|
+
logEvent('plugkit', 'browser.reused-existing-chromium', { pid: browserPid, port, profileDir });
|
|
933
|
+
} else {
|
|
934
|
+
logEvent('plugkit', 'browser.start', { profileDir });
|
|
935
|
+
({ pid: browserPid, port, wsEndpoint } = startManagedBrowser(pw, profileDir));
|
|
936
|
+
}
|
|
937
|
+
const r = runBrowserRunner(pw, ['session', 'new', '--direct', wsEndpoint], 30000);
|
|
938
|
+
if (!r || r.status !== 0) {
|
|
939
|
+
const errTxt = scrubBrowserRunnerText((r && (r.stderr || r.stdout)) || 'unknown');
|
|
940
|
+
logEvent('plugkit', 'browser.launch-failed', { reason: 'session-attach-failed', pid: browserPid, port, error: errTxt });
|
|
941
|
+
throw new Error(`playwriter session new --direct failed: ${errTxt}`);
|
|
870
942
|
}
|
|
943
|
+
const pwSessionId = parseSessionId(r.stdout || '');
|
|
871
944
|
if (!pwSessionId) {
|
|
872
|
-
|
|
945
|
+
logEvent('plugkit', 'browser.launch-failed', { reason: 'session-id-unparseable', stdout: r.stdout });
|
|
946
|
+
throw new Error(`could not parse managed browser session id from: ${scrubBrowserRunnerText(r.stdout || '')}`);
|
|
873
947
|
}
|
|
874
|
-
|
|
875
|
-
ports[claudeSessionId] = { profileDir, pid: browserPid };
|
|
948
|
+
ports[claudeSessionId] = { profileDir, pid: browserPid, port, wsEndpoint, pwSessionId };
|
|
876
949
|
sessions[claudeSessionId] = [pwSessionId];
|
|
877
950
|
writeJsonFile(portsFile, ports);
|
|
878
951
|
writeJsonFile(sessionsFile, sessions);
|
|
952
|
+
logEvent('plugkit', 'browser.attached', { pwSessionId, pid: browserPid, port });
|
|
879
953
|
return pwSessionId;
|
|
880
954
|
}
|
|
881
955
|
|
|
956
|
+
function parseSessionId(rawOut) {
|
|
957
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
958
|
+
const out = stripAnsi(rawOut || '').trim();
|
|
959
|
+
const created = out.match(/Session\s+(\S+)\s+created/i);
|
|
960
|
+
if (created) return created[1];
|
|
961
|
+
const hex = out.match(/\b([a-f0-9-]{8,})\b/i);
|
|
962
|
+
if (hex) return hex[1];
|
|
963
|
+
try { const j = JSON.parse(out); return j.id || j.session_id || j.session || null; } catch (_) {}
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
|
|
882
967
|
const VEC_K_DEFAULT = 10;
|
|
883
968
|
const EMBED_MODEL_DEFAULT = process.env.EMBED_MODEL || 'mistral/mistral-embed';
|
|
884
969
|
const INFERENCE_MODEL_DEFAULT = process.env.INFERENCE_MODEL || 'groq/llama-3.3-70b-versatile';
|
|
@@ -1048,6 +1133,32 @@ function kvNamespaceDirs(ns) {
|
|
|
1048
1133
|
return out;
|
|
1049
1134
|
}
|
|
1050
1135
|
|
|
1136
|
+
function enabledDisciplineNamespaces(baseNs) {
|
|
1137
|
+
const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
1138
|
+
const set = new Set([baseNs]);
|
|
1139
|
+
try {
|
|
1140
|
+
const enabledPath = path.join(projectRoot, '.gm', 'disciplines', 'enabled.txt');
|
|
1141
|
+
if (fs.existsSync(enabledPath)) {
|
|
1142
|
+
const lines = fs.readFileSync(enabledPath, 'utf-8').split(/\r?\n/);
|
|
1143
|
+
for (const ln of lines) {
|
|
1144
|
+
const name = ln.trim();
|
|
1145
|
+
if (name && !name.startsWith('#')) set.add(name);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
} catch (_) {}
|
|
1149
|
+
return Array.from(set);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function jaccardOverlap(a, b) {
|
|
1153
|
+
if (!a || !b) return 0;
|
|
1154
|
+
const tokenize = (s) => new Set(String(s).toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length >= 3));
|
|
1155
|
+
const A = tokenize(a), B = tokenize(b);
|
|
1156
|
+
if (A.size === 0 || B.size === 0) return 0;
|
|
1157
|
+
let inter = 0;
|
|
1158
|
+
for (const t of A) if (B.has(t)) inter++;
|
|
1159
|
+
return inter / (A.size + B.size - inter);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1051
1162
|
const __tasks = new Map();
|
|
1052
1163
|
|
|
1053
1164
|
function tasksDir(cwd) {
|
|
@@ -1379,8 +1490,8 @@ function makeHostFunctions(instanceRef) {
|
|
|
1379
1490
|
if (!raw) return writeWasmJson(instanceRef.value, []);
|
|
1380
1491
|
let parsedQ;
|
|
1381
1492
|
try { parsedQ = JSON.parse(raw); } catch (_) { parsedQ = { query: raw }; }
|
|
1382
|
-
const q = parsedQ.query || raw;
|
|
1383
1493
|
const namespace = parsedQ.namespace || 'default';
|
|
1494
|
+
const sigil = parsedQ.sigil || parsedQ.discipline_sigil || null;
|
|
1384
1495
|
const extractVec = (e) => {
|
|
1385
1496
|
if (Array.isArray(e)) return e;
|
|
1386
1497
|
if (Array.isArray(e?.data?.[0]?.embedding)) return e.data[0].embedding;
|
|
@@ -1393,37 +1504,58 @@ function makeHostFunctions(instanceRef) {
|
|
|
1393
1504
|
if (process.env.PLUGKIT_DEBUG) console.error('[plugkit-wasm] host_vec_search: no embedding in query, raw=', raw.slice(0, 200));
|
|
1394
1505
|
return writeWasmJson(instanceRef.value, []);
|
|
1395
1506
|
}
|
|
1396
|
-
const
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1507
|
+
const namespaces = sigil ? [namespace] : enabledDisciplineNamespaces(namespace);
|
|
1508
|
+
const HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
1509
|
+
const DEDUP_JACCARD = 0.7;
|
|
1510
|
+
const RECENCY_FLOOR = 0.4;
|
|
1511
|
+
const nowMs = Date.now();
|
|
1401
1512
|
const scored = [];
|
|
1402
1513
|
const seen = new Set();
|
|
1403
|
-
for (const
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1514
|
+
for (const ns of namespaces) {
|
|
1515
|
+
const vecDirs = kvNamespaceDirs(`${ns}-vec`);
|
|
1516
|
+
const dataDirs = kvNamespaceDirs(ns);
|
|
1517
|
+
if (vecDirs.length === 0 || dataDirs.length === 0) continue;
|
|
1518
|
+
for (const vecDir of vecDirs) {
|
|
1519
|
+
for (const f of fs.readdirSync(vecDir)) {
|
|
1520
|
+
if (!f.endsWith('.json')) continue;
|
|
1521
|
+
const key = f.replace(/\.json$/, '');
|
|
1522
|
+
const seenKey = `${ns}::${key}`;
|
|
1523
|
+
if (seen.has(seenKey)) continue;
|
|
1524
|
+
seen.add(seenKey);
|
|
1525
|
+
const vecPath = path.join(vecDir, f);
|
|
1526
|
+
let emb, mtimeMs;
|
|
1527
|
+
try {
|
|
1528
|
+
emb = JSON.parse(fs.readFileSync(vecPath, 'utf-8'));
|
|
1529
|
+
mtimeMs = fs.statSync(vecPath).mtimeMs;
|
|
1530
|
+
} catch (_) { continue; }
|
|
1531
|
+
const vector = Array.isArray(emb?.data?.[0]?.embedding) ? emb.data[0].embedding
|
|
1532
|
+
: Array.isArray(emb?.embedding) ? emb.embedding
|
|
1533
|
+
: Array.isArray(emb) ? emb : null;
|
|
1534
|
+
if (!vector) continue;
|
|
1535
|
+
const cos = cosineSim(queryEmbedding, vector);
|
|
1536
|
+
const ageMs = Math.max(0, nowMs - mtimeMs);
|
|
1537
|
+
const recency = RECENCY_FLOOR + (1 - RECENCY_FLOOR) * Math.exp(-ageMs / HALF_LIFE_MS);
|
|
1538
|
+
const score = cos * recency;
|
|
1539
|
+
let text = '';
|
|
1540
|
+
for (const dataDir of dataDirs) {
|
|
1541
|
+
const valuePath = path.join(dataDir, `${key}.json`);
|
|
1542
|
+
if (fs.existsSync(valuePath)) { text = fs.readFileSync(valuePath, 'utf-8'); break; }
|
|
1543
|
+
}
|
|
1544
|
+
scored.push({ key, text, score, cos, recency, namespace: ns });
|
|
1421
1545
|
}
|
|
1422
|
-
scored.push({ key, text, score });
|
|
1423
1546
|
}
|
|
1424
1547
|
}
|
|
1425
1548
|
scored.sort((a, b) => b.score - a.score);
|
|
1426
|
-
|
|
1549
|
+
const out = [];
|
|
1550
|
+
for (const hit of scored) {
|
|
1551
|
+
let dup = false;
|
|
1552
|
+
for (const kept of out) {
|
|
1553
|
+
if (jaccardOverlap(hit.text, kept.text) >= DEDUP_JACCARD) { dup = true; break; }
|
|
1554
|
+
}
|
|
1555
|
+
if (!dup) out.push(hit);
|
|
1556
|
+
if (out.length >= k_) break;
|
|
1557
|
+
}
|
|
1558
|
+
return writeWasmJson(instanceRef.value, out);
|
|
1427
1559
|
} catch (e) {
|
|
1428
1560
|
console.error('[plugkit-wasm] host_vec_search error:', e.message);
|
|
1429
1561
|
return writeWasmJson(instanceRef.value, []);
|
|
@@ -1483,6 +1615,15 @@ function makeHostFunctions(instanceRef) {
|
|
|
1483
1615
|
const prefix = level >= 3 ? '[plugkit-wasm:err]' : level >= 2 ? '[plugkit-wasm:warn]' : '[plugkit-wasm]';
|
|
1484
1616
|
if (level >= 2) console.error(`${prefix} ${msg}`);
|
|
1485
1617
|
else console.log(`${prefix} ${msg}`);
|
|
1618
|
+
const evtMatch = msg.match(/^evt:\s*(\{.*\})\s*$/);
|
|
1619
|
+
if (evtMatch) {
|
|
1620
|
+
try {
|
|
1621
|
+
const ev = JSON.parse(evtMatch[1]);
|
|
1622
|
+
const eventName = ev.event || 'wasm.event';
|
|
1623
|
+
const { event: _e, ts: _ts, sess: _s, sub: _sub, ...fields } = ev;
|
|
1624
|
+
logEvent(ev.sub || 'plugkit', eventName, fields);
|
|
1625
|
+
} catch (_) {}
|
|
1626
|
+
}
|
|
1486
1627
|
return 0;
|
|
1487
1628
|
} catch (e) {
|
|
1488
1629
|
return 0;
|
|
@@ -1519,6 +1660,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
1519
1660
|
});
|
|
1520
1661
|
}
|
|
1521
1662
|
const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
|
|
1663
|
+
try { lastBrowserActivityMs = Date.now(); } catch (_) {}
|
|
1522
1664
|
const r = runBrowserRunner(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
|
|
1523
1665
|
return writeWasmJson(instanceRef.value, {
|
|
1524
1666
|
ok: r.status === 0,
|
|
@@ -1562,16 +1704,13 @@ function makeHostFunctions(instanceRef) {
|
|
|
1562
1704
|
const args = readWasmStr(instanceRef.value, argsPtr, argsLen);
|
|
1563
1705
|
const cwdStr = readWasmStr(instanceRef.value, cwdPtr, cwdLen);
|
|
1564
1706
|
const cwd = cwdStr || process.cwd();
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
stdout
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
exit_code = (e && typeof e.status === 'number') ? e.status : 1;
|
|
1573
|
-
}
|
|
1574
|
-
return writeWasmJson(instanceRef.value, { stdout, stderr, exit_code });
|
|
1707
|
+
const argv = args.trim().split(/\s+/);
|
|
1708
|
+
const result = _rawSpawnSync('git', argv, { encoding: 'utf-8', timeout: 30000, cwd, windowsHide: true });
|
|
1709
|
+
return writeWasmJson(instanceRef.value, {
|
|
1710
|
+
stdout: result.stdout || '',
|
|
1711
|
+
stderr: result.stderr || '',
|
|
1712
|
+
exit_code: result.status === null ? -1 : result.status,
|
|
1713
|
+
});
|
|
1575
1714
|
} catch (e) {
|
|
1576
1715
|
return writeWasmJson(instanceRef.value, { stdout: '', stderr: String(e && e.message || e), exit_code: 1 });
|
|
1577
1716
|
}
|
|
@@ -1825,14 +1964,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1825
1964
|
lastActivityMs = Date.now();
|
|
1826
1965
|
}
|
|
1827
1966
|
|
|
1828
|
-
|
|
1829
|
-
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return false;
|
|
1830
|
-
try { process.kill(pid, 'SIGTERM'); } catch (_) {}
|
|
1831
|
-
if (process.platform === 'win32') {
|
|
1832
|
-
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', timeout: 3000 }); } catch (_) {}
|
|
1833
|
-
}
|
|
1834
|
-
return true;
|
|
1835
|
-
}
|
|
1967
|
+
/* killPidQuiet, purgeProfileLockFiles, gracefulCloseBrowser are module-scope (defined above spool()). */
|
|
1836
1968
|
|
|
1837
1969
|
function teardownAll(reason) {
|
|
1838
1970
|
try {
|
|
@@ -1847,7 +1979,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1847
1979
|
const sessionsFile = browserSessionsFile(process.cwd());
|
|
1848
1980
|
const ports = readJsonFile(portsFile, {});
|
|
1849
1981
|
for (const [sid, entry] of Object.entries(ports)) {
|
|
1850
|
-
|
|
1982
|
+
gracefulCloseBrowser(entry, `teardown:${reason}`);
|
|
1851
1983
|
}
|
|
1852
1984
|
try { fs.unlinkSync(portsFile); } catch (_) {}
|
|
1853
1985
|
try { fs.unlinkSync(sessionsFile); } catch (_) {}
|
|
@@ -1860,6 +1992,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1860
1992
|
pid: process.pid,
|
|
1861
1993
|
idle_ms: Date.now() - lastActivityMs,
|
|
1862
1994
|
}));
|
|
1995
|
+
__shutdownReasonWritten = true;
|
|
1863
1996
|
} catch (_) {}
|
|
1864
1997
|
|
|
1865
1998
|
try { fs.unlinkSync(STATUS_PATH_FOR_TEARDOWN); } catch (_) {}
|
|
@@ -1939,6 +2072,32 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1939
2072
|
}
|
|
1940
2073
|
}, 60_000);
|
|
1941
2074
|
|
|
2075
|
+
const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 10 * 60 * 1000;
|
|
2076
|
+
let lastBrowserActivityMs = Date.now();
|
|
2077
|
+
setInterval(() => {
|
|
2078
|
+
try {
|
|
2079
|
+
const browserIdleMs = Date.now() - lastBrowserActivityMs;
|
|
2080
|
+
if (browserIdleMs < BROWSER_IDLE_LIMIT_MS) return;
|
|
2081
|
+
const portsFile = browserPortsFile(process.cwd());
|
|
2082
|
+
const sessionsFile = browserSessionsFile(process.cwd());
|
|
2083
|
+
const ports = readJsonFile(portsFile, {});
|
|
2084
|
+
let closed = 0;
|
|
2085
|
+
for (const [sid, entry] of Object.entries(ports)) {
|
|
2086
|
+
if (entry && Number.isFinite(entry.pid) && isProcessAliveSync(entry.pid)) {
|
|
2087
|
+
try { gracefulCloseBrowser(entry, 'browser-idle'); closed++; } catch (_) {}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
if (closed > 0) {
|
|
2091
|
+
try { fs.unlinkSync(portsFile); } catch (_) {}
|
|
2092
|
+
try { fs.unlinkSync(sessionsFile); } catch (_) {}
|
|
2093
|
+
logEvent('plugkit', 'browser.idle-closed', { count: closed, idle_ms: browserIdleMs });
|
|
2094
|
+
}
|
|
2095
|
+
lastBrowserActivityMs = Date.now();
|
|
2096
|
+
} catch (e) {
|
|
2097
|
+
console.error(`[browser-idle] error: ${e.message}`);
|
|
2098
|
+
}
|
|
2099
|
+
}, 60_000);
|
|
2100
|
+
|
|
1942
2101
|
setInterval(() => {
|
|
1943
2102
|
try {
|
|
1944
2103
|
const idleMs = Date.now() - lastActivityMs;
|
|
@@ -1964,20 +2123,48 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1964
2123
|
}
|
|
1965
2124
|
}, IDLE_CHECK_MS);
|
|
1966
2125
|
|
|
1967
|
-
|
|
2126
|
+
const SHUTDOWN_REQUEST_PATH = path.join(spoolDir, '.shutdown-requested');
|
|
2127
|
+
setInterval(() => {
|
|
1968
2128
|
try {
|
|
1969
|
-
fs.
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2129
|
+
if (!fs.existsSync(SHUTDOWN_REQUEST_PATH)) return;
|
|
2130
|
+
let reqReason = 'shutdown-requested';
|
|
2131
|
+
try {
|
|
2132
|
+
const raw = fs.readFileSync(SHUTDOWN_REQUEST_PATH, 'utf-8').trim();
|
|
2133
|
+
if (raw) {
|
|
2134
|
+
try { const j = JSON.parse(raw); if (j && j.reason) reqReason = String(j.reason); }
|
|
2135
|
+
catch (_) { reqReason = raw.slice(0, 64); }
|
|
2136
|
+
}
|
|
2137
|
+
} catch (_) {}
|
|
2138
|
+
try { fs.unlinkSync(SHUTDOWN_REQUEST_PATH); } catch (_) {}
|
|
2139
|
+
handleSignalShutdown(reqReason.toUpperCase());
|
|
2140
|
+
} catch (e) {
|
|
2141
|
+
console.error(`[shutdown-request] error: ${e.message}`);
|
|
2142
|
+
}
|
|
2143
|
+
}, 2000);
|
|
2144
|
+
|
|
2145
|
+
let _signalShutdownInFlight = false;
|
|
2146
|
+
function handleSignalShutdown(sig) {
|
|
2147
|
+
if (_signalShutdownInFlight) return;
|
|
2148
|
+
_signalShutdownInFlight = true;
|
|
2149
|
+
try { teardownAll(sig.toLowerCase()); } catch (_) {
|
|
2150
|
+
try {
|
|
2151
|
+
fs.writeFileSync(SHUTDOWN_REASON_PATH, JSON.stringify({
|
|
2152
|
+
reason: sig.toLowerCase(),
|
|
2153
|
+
ts: Date.now(),
|
|
2154
|
+
pid: process.pid,
|
|
2155
|
+
teardown_failed: true,
|
|
2156
|
+
}));
|
|
2157
|
+
__shutdownReasonWritten = true;
|
|
2158
|
+
} catch (__) {}
|
|
2159
|
+
try { clearBootActive(); } catch (__) {}
|
|
2160
|
+
try { releaseLock(); } catch (__) {}
|
|
2161
|
+
process.exit(0);
|
|
2162
|
+
}
|
|
1978
2163
|
}
|
|
1979
2164
|
process.on('SIGINT', () => handleSignalShutdown('SIGINT'));
|
|
1980
2165
|
process.on('SIGTERM', () => handleSignalShutdown('SIGTERM'));
|
|
2166
|
+
process.on('SIGBREAK', () => handleSignalShutdown('SIGBREAK'));
|
|
2167
|
+
process.on('SIGHUP', () => handleSignalShutdown('SIGHUP'));
|
|
1981
2168
|
process.on('exit', () => { try { clearBootActive(); } catch (_) {} releaseLock(); });
|
|
1982
2169
|
|
|
1983
2170
|
try {
|
|
@@ -2015,8 +2202,9 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2015
2202
|
prior_status: _priorStatus,
|
|
2016
2203
|
prior_status_age_ms: _priorStatus && Number.isFinite(_priorStatus.ts) ? Date.now() - _priorStatus.ts : null,
|
|
2017
2204
|
};
|
|
2018
|
-
const
|
|
2019
|
-
const
|
|
2205
|
+
const _UNPLANNED_REASONS = new Set(['uncaughtexception', 'unhandledrejection', 'wasm-abort', 'wasm-abort-graceful']);
|
|
2206
|
+
const _normalizedShutdownReason = _priorShutdown && _priorShutdown.reason ? String(_priorShutdown.reason).toLowerCase() : null;
|
|
2207
|
+
const _isPlannedBoot = !!_normalizedShutdownReason && !_UNPLANNED_REASONS.has(_normalizedShutdownReason);
|
|
2020
2208
|
const _isFirstBoot = !_priorShutdown && !_priorStatus;
|
|
2021
2209
|
const UNPLANNED_RESTART_MARKER = path.join(spoolDir, '.unplanned-restart.json');
|
|
2022
2210
|
const HEARTBEAT_RECENT_MS = 60_000;
|
|
@@ -2057,7 +2245,11 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2057
2245
|
history,
|
|
2058
2246
|
}, null, 2));
|
|
2059
2247
|
} catch (_) {}
|
|
2060
|
-
|
|
2248
|
+
if (_isPlannedBoot) {
|
|
2249
|
+
console.log(`[plugkit-wasm] planned restart: prior reason="${_priorShutdown.reason}" boot_reason=${_bootReason}`);
|
|
2250
|
+
} else {
|
|
2251
|
+
console.error(`[plugkit-wasm] UNPLANNED RESTART detected — prior watcher died without writing .shutdown-reason.json. prior_status_age_ms=${restartContext.prior_status_age_ms} boot_reason=${_bootReason}`);
|
|
2252
|
+
}
|
|
2061
2253
|
}
|
|
2062
2254
|
try { fs.unlinkSync(SHUTDOWN_REASON_PATH); } catch (_) {}
|
|
2063
2255
|
logEvent('plugkit', 'watcher.boot', { version: _bootVersion, in_dir: inDir, out_dir: outDir, spool_dir: spoolDir, ...restartContext });
|