gm-skill 2.0.1303 → 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 +347 -152
- 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();
|
|
739
767
|
}
|
|
740
|
-
|
|
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;
|
|
772
|
+
}
|
|
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;
|
|
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 (_) {}
|
|
772
829
|
}
|
|
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
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
|
-
attempt,
|
|
809
|
-
});
|
|
810
|
-
lastProgressAt = now;
|
|
811
|
-
}
|
|
812
|
-
const sleepMs = backoff[Math.min(attempt, backoff.length - 1)];
|
|
813
|
-
attempt++;
|
|
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 (_) {}
|
|
816
854
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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);
|
|
860
|
+
}
|
|
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));
|
|
870
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}`);
|
|
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,
|
|
@@ -1822,14 +1964,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1822
1964
|
lastActivityMs = Date.now();
|
|
1823
1965
|
}
|
|
1824
1966
|
|
|
1825
|
-
|
|
1826
|
-
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return false;
|
|
1827
|
-
try { process.kill(pid, 'SIGTERM'); } catch (_) {}
|
|
1828
|
-
if (process.platform === 'win32') {
|
|
1829
|
-
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', timeout: 3000 }); } catch (_) {}
|
|
1830
|
-
}
|
|
1831
|
-
return true;
|
|
1832
|
-
}
|
|
1967
|
+
/* killPidQuiet, purgeProfileLockFiles, gracefulCloseBrowser are module-scope (defined above spool()). */
|
|
1833
1968
|
|
|
1834
1969
|
function teardownAll(reason) {
|
|
1835
1970
|
try {
|
|
@@ -1844,7 +1979,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1844
1979
|
const sessionsFile = browserSessionsFile(process.cwd());
|
|
1845
1980
|
const ports = readJsonFile(portsFile, {});
|
|
1846
1981
|
for (const [sid, entry] of Object.entries(ports)) {
|
|
1847
|
-
|
|
1982
|
+
gracefulCloseBrowser(entry, `teardown:${reason}`);
|
|
1848
1983
|
}
|
|
1849
1984
|
try { fs.unlinkSync(portsFile); } catch (_) {}
|
|
1850
1985
|
try { fs.unlinkSync(sessionsFile); } catch (_) {}
|
|
@@ -1857,6 +1992,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1857
1992
|
pid: process.pid,
|
|
1858
1993
|
idle_ms: Date.now() - lastActivityMs,
|
|
1859
1994
|
}));
|
|
1995
|
+
__shutdownReasonWritten = true;
|
|
1860
1996
|
} catch (_) {}
|
|
1861
1997
|
|
|
1862
1998
|
try { fs.unlinkSync(STATUS_PATH_FOR_TEARDOWN); } catch (_) {}
|
|
@@ -1936,6 +2072,32 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1936
2072
|
}
|
|
1937
2073
|
}, 60_000);
|
|
1938
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
|
+
|
|
1939
2101
|
setInterval(() => {
|
|
1940
2102
|
try {
|
|
1941
2103
|
const idleMs = Date.now() - lastActivityMs;
|
|
@@ -1961,20 +2123,48 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1961
2123
|
}
|
|
1962
2124
|
}, IDLE_CHECK_MS);
|
|
1963
2125
|
|
|
1964
|
-
|
|
2126
|
+
const SHUTDOWN_REQUEST_PATH = path.join(spoolDir, '.shutdown-requested');
|
|
2127
|
+
setInterval(() => {
|
|
1965
2128
|
try {
|
|
1966
|
-
fs.
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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
|
+
}
|
|
1975
2163
|
}
|
|
1976
2164
|
process.on('SIGINT', () => handleSignalShutdown('SIGINT'));
|
|
1977
2165
|
process.on('SIGTERM', () => handleSignalShutdown('SIGTERM'));
|
|
2166
|
+
process.on('SIGBREAK', () => handleSignalShutdown('SIGBREAK'));
|
|
2167
|
+
process.on('SIGHUP', () => handleSignalShutdown('SIGHUP'));
|
|
1978
2168
|
process.on('exit', () => { try { clearBootActive(); } catch (_) {} releaseLock(); });
|
|
1979
2169
|
|
|
1980
2170
|
try {
|
|
@@ -2012,8 +2202,9 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2012
2202
|
prior_status: _priorStatus,
|
|
2013
2203
|
prior_status_age_ms: _priorStatus && Number.isFinite(_priorStatus.ts) ? Date.now() - _priorStatus.ts : null,
|
|
2014
2204
|
};
|
|
2015
|
-
const
|
|
2016
|
-
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);
|
|
2017
2208
|
const _isFirstBoot = !_priorShutdown && !_priorStatus;
|
|
2018
2209
|
const UNPLANNED_RESTART_MARKER = path.join(spoolDir, '.unplanned-restart.json');
|
|
2019
2210
|
const HEARTBEAT_RECENT_MS = 60_000;
|
|
@@ -2054,7 +2245,11 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2054
2245
|
history,
|
|
2055
2246
|
}, null, 2));
|
|
2056
2247
|
} catch (_) {}
|
|
2057
|
-
|
|
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
|
+
}
|
|
2058
2253
|
}
|
|
2059
2254
|
try { fs.unlinkSync(SHUTDOWN_REASON_PATH); } catch (_) {}
|
|
2060
2255
|
logEvent('plugkit', 'watcher.boot', { version: _bootVersion, in_dir: inDir, out_dir: outDir, spool_dir: spoolDir, ...restartContext });
|