gm-plugkit 2.0.1155 → 2.0.1157
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/bootstrap.js +29 -12
- package/package.json +1 -1
- package/plugkit-wasm-wrapper.js +166 -9
package/bootstrap.js
CHANGED
|
@@ -777,14 +777,6 @@ function startSpoolDaemon() {
|
|
|
777
777
|
return { ok: false, error: `wrapper not at ${wrapper} — ensureReady() must run first` };
|
|
778
778
|
}
|
|
779
779
|
const runtime = process.platform === 'win32' ? 'bun.exe' : 'bun';
|
|
780
|
-
let cmd = runtime;
|
|
781
|
-
let args = [wrapper, 'spool'];
|
|
782
|
-
try {
|
|
783
|
-
require('child_process').execFileSync(runtime, ['--version'], { stdio: 'ignore' });
|
|
784
|
-
} catch (_) {
|
|
785
|
-
cmd = process.execPath;
|
|
786
|
-
args = [wrapper, 'spool'];
|
|
787
|
-
}
|
|
788
780
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
789
781
|
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
790
782
|
fs.mkdirSync(spoolDir, { recursive: true });
|
|
@@ -796,18 +788,43 @@ function startSpoolDaemon() {
|
|
|
796
788
|
fs.renameSync(logPath, path.join(spoolDir, '.watcher.log.1'));
|
|
797
789
|
}
|
|
798
790
|
} catch (_) {}
|
|
791
|
+
|
|
792
|
+
const supervisor = path.join(__dirname, 'supervisor.js');
|
|
793
|
+
if (process.env.PLUGKIT_SKIP_SUPERVISOR === '1' || !fs.existsSync(supervisor)) {
|
|
794
|
+
let cmd = runtime;
|
|
795
|
+
let args = [wrapper, 'spool'];
|
|
796
|
+
try {
|
|
797
|
+
require('child_process').execFileSync(runtime, ['--version'], { stdio: 'ignore' });
|
|
798
|
+
} catch (_) {
|
|
799
|
+
cmd = process.execPath;
|
|
800
|
+
args = [wrapper, 'spool'];
|
|
801
|
+
}
|
|
802
|
+
const logFd = fs.openSync(logPath, 'a');
|
|
803
|
+
try { fs.writeSync(logFd, `\n--- daemon spawn ${new Date().toISOString()} parent=${process.pid} (no supervisor) ---\n`); } catch (_) {}
|
|
804
|
+
const child = require('child_process').spawn(cmd, args, {
|
|
805
|
+
detached: true,
|
|
806
|
+
stdio: ['ignore', logFd, logFd],
|
|
807
|
+
windowsHide: true,
|
|
808
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir, PLUGKIT_BOOT_REASON: 'direct-no-supervisor' },
|
|
809
|
+
});
|
|
810
|
+
try { fs.closeSync(logFd); } catch (_) {}
|
|
811
|
+
const pid = child.pid;
|
|
812
|
+
child.unref();
|
|
813
|
+
return { ok: true, pid, wrapper, runtime: cmd, logPath, supervised: false };
|
|
814
|
+
}
|
|
815
|
+
|
|
799
816
|
const logFd = fs.openSync(logPath, 'a');
|
|
800
|
-
try { fs.writeSync(logFd, `\n---
|
|
801
|
-
const child = require('child_process').spawn(
|
|
817
|
+
try { fs.writeSync(logFd, `\n--- supervisor spawn ${new Date().toISOString()} parent=${process.pid} ---\n`); } catch (_) {}
|
|
818
|
+
const child = require('child_process').spawn(process.execPath, [supervisor], {
|
|
802
819
|
detached: true,
|
|
803
820
|
stdio: ['ignore', logFd, logFd],
|
|
804
821
|
windowsHide: true,
|
|
805
|
-
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
|
|
822
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir, PLUGKIT_RUNTIME: runtime },
|
|
806
823
|
});
|
|
807
824
|
try { fs.closeSync(logFd); } catch (_) {}
|
|
808
825
|
const pid = child.pid;
|
|
809
826
|
child.unref();
|
|
810
|
-
return { ok: true, pid, wrapper, runtime
|
|
827
|
+
return { ok: true, pid, wrapper, supervisor, runtime, logPath, supervised: true };
|
|
811
828
|
} catch (e) {
|
|
812
829
|
return { ok: false, error: e.message };
|
|
813
830
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1157",
|
|
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": {
|
package/plugkit-wasm-wrapper.js
CHANGED
|
@@ -4,10 +4,17 @@ import os from 'os';
|
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import https from 'https';
|
|
6
6
|
import { watch } from 'fs';
|
|
7
|
-
import { spawn, spawnSync } from 'child_process';
|
|
7
|
+
import { spawn as _rawSpawn, spawnSync as _rawSpawnSync } from 'child_process';
|
|
8
8
|
import net from 'net';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
|
|
11
|
+
function spawnSync(cmd, args, opts) {
|
|
12
|
+
return _rawSpawnSync(cmd, args, { windowsHide: true, ...(opts || {}) });
|
|
13
|
+
}
|
|
14
|
+
function spawn(cmd, args, opts) {
|
|
15
|
+
return _rawSpawn(cmd, args, { windowsHide: true, ...(opts || {}) });
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
19
|
const __dirname = path.dirname(__filename);
|
|
13
20
|
|
|
@@ -116,8 +123,33 @@ function emitOrchestratorEvents(verb, taskBase, resultStr) {
|
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
const TMP_DIR = os.tmpdir();
|
|
119
|
-
const
|
|
120
|
-
const
|
|
126
|
+
const LEGACY_BROWSER_PORTS_FILE = path.join(TMP_DIR, 'plugkit-browser-ports.json');
|
|
127
|
+
const LEGACY_BROWSER_SESSIONS_FILE = path.join(TMP_DIR, 'plugkit-browser-sessions.json');
|
|
128
|
+
|
|
129
|
+
function browserStateDir(cwd) {
|
|
130
|
+
const dir = path.join(cwd || process.cwd(), '.gm', 'exec-spool');
|
|
131
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
|
|
132
|
+
return dir;
|
|
133
|
+
}
|
|
134
|
+
function browserPortsFile(cwd) { return path.join(browserStateDir(cwd), 'browser-ports.json'); }
|
|
135
|
+
function browserSessionsFile(cwd) { return path.join(browserStateDir(cwd), 'browser-sessions.json'); }
|
|
136
|
+
|
|
137
|
+
function migrateLegacyBrowserState(cwd) {
|
|
138
|
+
const dst1 = browserPortsFile(cwd);
|
|
139
|
+
const dst2 = browserSessionsFile(cwd);
|
|
140
|
+
try {
|
|
141
|
+
if (!fs.existsSync(dst1) && fs.existsSync(LEGACY_BROWSER_PORTS_FILE)) {
|
|
142
|
+
const legacy = JSON.parse(fs.readFileSync(LEGACY_BROWSER_PORTS_FILE, 'utf-8'));
|
|
143
|
+
if (legacy && typeof legacy === 'object') fs.writeFileSync(dst1, JSON.stringify(legacy, null, 2));
|
|
144
|
+
}
|
|
145
|
+
} catch (_) {}
|
|
146
|
+
try {
|
|
147
|
+
if (!fs.existsSync(dst2) && fs.existsSync(LEGACY_BROWSER_SESSIONS_FILE)) {
|
|
148
|
+
const legacy = JSON.parse(fs.readFileSync(LEGACY_BROWSER_SESSIONS_FILE, 'utf-8'));
|
|
149
|
+
if (legacy && typeof legacy === 'object') fs.writeFileSync(dst2, JSON.stringify(legacy, null, 2));
|
|
150
|
+
}
|
|
151
|
+
} catch (_) {}
|
|
152
|
+
}
|
|
121
153
|
|
|
122
154
|
function readJsonFile(fp, fallback) {
|
|
123
155
|
try { return JSON.parse(fs.readFileSync(fp, 'utf-8')); } catch (_) { return fallback; }
|
|
@@ -239,8 +271,11 @@ function runPlaywriter(pw, args, timeoutMs) {
|
|
|
239
271
|
}
|
|
240
272
|
|
|
241
273
|
function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
242
|
-
|
|
243
|
-
const
|
|
274
|
+
migrateLegacyBrowserState(cwd);
|
|
275
|
+
const portsFile = browserPortsFile(cwd);
|
|
276
|
+
const sessionsFile = browserSessionsFile(cwd);
|
|
277
|
+
const ports = readJsonFile(portsFile, {});
|
|
278
|
+
const sessions = readJsonFile(sessionsFile, {});
|
|
244
279
|
const existing = ports[claudeSessionId];
|
|
245
280
|
if (existing && existing.port && isPortAliveSync(existing.port)) {
|
|
246
281
|
const pwIds = sessions[claudeSessionId] || [];
|
|
@@ -258,6 +293,7 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
258
293
|
'--disable-features=Translate',
|
|
259
294
|
];
|
|
260
295
|
const child = spawn(chrome, chromeArgs, { detached: true, stdio: 'ignore' });
|
|
296
|
+
const chromePid = child.pid;
|
|
261
297
|
child.unref();
|
|
262
298
|
const deadline = Date.now() + 10000;
|
|
263
299
|
let alive = false;
|
|
@@ -281,10 +317,10 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
281
317
|
try { const j = JSON.parse(out); pwSessionId = j.id || j.session_id || j.session; } catch (_) {}
|
|
282
318
|
}
|
|
283
319
|
if (!pwSessionId) throw new Error(`could not parse playwriter session id from: ${out}`);
|
|
284
|
-
ports[claudeSessionId] = { port, profileDir };
|
|
320
|
+
ports[claudeSessionId] = { port, profileDir, pid: chromePid };
|
|
285
321
|
sessions[claudeSessionId] = [pwSessionId];
|
|
286
|
-
writeJsonFile(
|
|
287
|
-
writeJsonFile(
|
|
322
|
+
writeJsonFile(portsFile, ports);
|
|
323
|
+
writeJsonFile(sessionsFile, sessions);
|
|
288
324
|
return pwSessionId;
|
|
289
325
|
}
|
|
290
326
|
|
|
@@ -735,6 +771,83 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
735
771
|
}
|
|
736
772
|
acquireLock();
|
|
737
773
|
setInterval(refreshLock, 5000);
|
|
774
|
+
|
|
775
|
+
const IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_IDLE_LIMIT_MS, 10) || 15 * 60 * 1000;
|
|
776
|
+
const IDLE_CHECK_MS = 60_000;
|
|
777
|
+
const SHUTDOWN_REASON_PATH = path.join(spoolDir, '.shutdown-reason.json');
|
|
778
|
+
const STATUS_PATH_FOR_TEARDOWN = path.join(spoolDir, '.status.json');
|
|
779
|
+
const ACPTOAPI_STATUS_PATH = path.join(process.cwd(), '.gm', 'acptoapi-status.json');
|
|
780
|
+
let lastActivityMs = Date.now();
|
|
781
|
+
function markActivity(source) {
|
|
782
|
+
lastActivityMs = Date.now();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function killPidQuiet(pid) {
|
|
786
|
+
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return false;
|
|
787
|
+
try { process.kill(pid, 'SIGTERM'); } catch (_) {}
|
|
788
|
+
if (process.platform === 'win32') {
|
|
789
|
+
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', timeout: 3000 }); } catch (_) {}
|
|
790
|
+
}
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function teardownAll(reason) {
|
|
795
|
+
try {
|
|
796
|
+
logEvent('plugkit', 'watcher.teardown', { reason, idle_ms: Date.now() - lastActivityMs });
|
|
797
|
+
console.log(`[plugkit-wasm] teardown reason=${reason}`);
|
|
798
|
+
} catch (_) {}
|
|
799
|
+
|
|
800
|
+
try {
|
|
801
|
+
if (fs.existsSync(ACPTOAPI_STATUS_PATH)) {
|
|
802
|
+
const status = JSON.parse(fs.readFileSync(ACPTOAPI_STATUS_PATH, 'utf-8'));
|
|
803
|
+
if (status && Number.isFinite(status.pid)) killPidQuiet(status.pid);
|
|
804
|
+
try { fs.unlinkSync(ACPTOAPI_STATUS_PATH); } catch (_) {}
|
|
805
|
+
}
|
|
806
|
+
} catch (_) {}
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
const portsFile = browserPortsFile(process.cwd());
|
|
810
|
+
const sessionsFile = browserSessionsFile(process.cwd());
|
|
811
|
+
const ports = readJsonFile(portsFile, {});
|
|
812
|
+
for (const [sid, entry] of Object.entries(ports)) {
|
|
813
|
+
if (entry && Number.isFinite(entry.pid)) killPidQuiet(entry.pid);
|
|
814
|
+
}
|
|
815
|
+
try { fs.unlinkSync(portsFile); } catch (_) {}
|
|
816
|
+
try { fs.unlinkSync(sessionsFile); } catch (_) {}
|
|
817
|
+
} catch (_) {}
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
fs.writeFileSync(SHUTDOWN_REASON_PATH, JSON.stringify({
|
|
821
|
+
reason,
|
|
822
|
+
ts: Date.now(),
|
|
823
|
+
pid: process.pid,
|
|
824
|
+
idle_ms: Date.now() - lastActivityMs,
|
|
825
|
+
}));
|
|
826
|
+
} catch (_) {}
|
|
827
|
+
|
|
828
|
+
try { fs.unlinkSync(STATUS_PATH_FOR_TEARDOWN); } catch (_) {}
|
|
829
|
+
try { releaseLock(); } catch (_) {}
|
|
830
|
+
process.exit(0);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
setInterval(() => {
|
|
834
|
+
try {
|
|
835
|
+
const idleMs = Date.now() - lastActivityMs;
|
|
836
|
+
if (idleMs < IDLE_LIMIT_MS) return;
|
|
837
|
+
try {
|
|
838
|
+
const ports = readJsonFile(browserPortsFile(process.cwd()), {});
|
|
839
|
+
let browserAlive = false;
|
|
840
|
+
for (const entry of Object.values(ports)) {
|
|
841
|
+
if (entry && Number.isFinite(entry.port) && isPortAliveSync(entry.port)) { browserAlive = true; break; }
|
|
842
|
+
}
|
|
843
|
+
if (browserAlive) { markActivity('browser-port-alive'); return; }
|
|
844
|
+
} catch (_) {}
|
|
845
|
+
teardownAll('idle');
|
|
846
|
+
} catch (e) {
|
|
847
|
+
console.error(`[idle-check] error: ${e.message}`);
|
|
848
|
+
}
|
|
849
|
+
}, IDLE_CHECK_MS);
|
|
850
|
+
|
|
738
851
|
process.on('SIGINT', () => { releaseLock(); process.exit(0); });
|
|
739
852
|
process.on('SIGTERM', () => { releaseLock(); process.exit(0); });
|
|
740
853
|
process.on('exit', releaseLock);
|
|
@@ -760,7 +873,49 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
760
873
|
const _bootVersion = resolveVersion(instance);
|
|
761
874
|
console.log(`[plugkit-wasm] plugkit v${_bootVersion} (wasm)`);
|
|
762
875
|
console.log(`[plugkit-wasm] watching ${inDir}`);
|
|
763
|
-
|
|
876
|
+
|
|
877
|
+
let _priorShutdown = null;
|
|
878
|
+
let _priorStatus = null;
|
|
879
|
+
try { _priorShutdown = JSON.parse(fs.readFileSync(SHUTDOWN_REASON_PATH, 'utf-8')); } catch (_) {}
|
|
880
|
+
try { _priorStatus = JSON.parse(fs.readFileSync(STATUS_PATH_FOR_TEARDOWN, 'utf-8')); } catch (_) {}
|
|
881
|
+
const _bootReason = process.env.PLUGKIT_BOOT_REASON || 'unknown';
|
|
882
|
+
const _supervisorPid = parseInt(process.env.PLUGKIT_SUPERVISOR_PID, 10) || null;
|
|
883
|
+
const restartContext = {
|
|
884
|
+
boot_reason: _bootReason,
|
|
885
|
+
supervisor_pid: _supervisorPid,
|
|
886
|
+
prior_shutdown: _priorShutdown,
|
|
887
|
+
prior_status: _priorStatus,
|
|
888
|
+
prior_status_age_ms: _priorStatus && Number.isFinite(_priorStatus.ts) ? Date.now() - _priorStatus.ts : null,
|
|
889
|
+
};
|
|
890
|
+
const _isPlannedBoot = _priorShutdown && (_priorShutdown.reason === 'idle' || _priorShutdown.reason === 'sigterm' || _priorShutdown.reason === 'version-change');
|
|
891
|
+
const _isFirstBoot = !_priorShutdown && !_priorStatus;
|
|
892
|
+
const UNPLANNED_RESTART_MARKER = path.join(spoolDir, '.unplanned-restart.json');
|
|
893
|
+
if (!_isPlannedBoot && !_isFirstBoot) {
|
|
894
|
+
const incidentPayload = {
|
|
895
|
+
ts: Date.now(),
|
|
896
|
+
version: _bootVersion,
|
|
897
|
+
severity: 'critical',
|
|
898
|
+
...restartContext,
|
|
899
|
+
log_tail_path: path.join(spoolDir, '.watcher.log'),
|
|
900
|
+
gm_log_dir: GM_LOG_ROOT,
|
|
901
|
+
instruction: 'Prior watcher died without a planned shutdown. This is treated as a critical failure. Inspect .watcher.log and gm-log/<day>/plugkit.jsonl events supervisor.watcher-exited-unexpectedly + supervisor.heartbeat-stale around the prior_status.ts timestamp to diagnose root cause.',
|
|
902
|
+
};
|
|
903
|
+
logEvent('plugkit', 'watcher.unplanned-restart', incidentPayload);
|
|
904
|
+
try {
|
|
905
|
+
let history = [];
|
|
906
|
+
try { history = JSON.parse(fs.readFileSync(UNPLANNED_RESTART_MARKER, 'utf-8')).history || []; } catch (_) {}
|
|
907
|
+
history.push(incidentPayload);
|
|
908
|
+
if (history.length > 20) history = history.slice(-20);
|
|
909
|
+
fs.writeFileSync(UNPLANNED_RESTART_MARKER, JSON.stringify({
|
|
910
|
+
latest: incidentPayload,
|
|
911
|
+
count: history.length,
|
|
912
|
+
history,
|
|
913
|
+
}, null, 2));
|
|
914
|
+
} catch (_) {}
|
|
915
|
+
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}`);
|
|
916
|
+
}
|
|
917
|
+
try { fs.unlinkSync(SHUTDOWN_REASON_PATH); } catch (_) {}
|
|
918
|
+
logEvent('plugkit', 'watcher.boot', { version: _bootVersion, in_dir: inDir, out_dir: outDir, spool_dir: spoolDir, ...restartContext });
|
|
764
919
|
|
|
765
920
|
const PROCESSED_MAX = 10000;
|
|
766
921
|
const processed = new Map();
|
|
@@ -927,6 +1082,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
927
1082
|
|
|
928
1083
|
const pollInterval = setInterval(async () => {
|
|
929
1084
|
const existing = walkDir(inDir);
|
|
1085
|
+
if (existing.length > 0) markActivity('poll');
|
|
930
1086
|
for (const fullPath of existing) {
|
|
931
1087
|
await processFile(fullPath);
|
|
932
1088
|
}
|
|
@@ -997,6 +1153,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
997
1153
|
watch(inDir, { recursive: true }, (eventType, filename) => {
|
|
998
1154
|
if (!filename) return;
|
|
999
1155
|
const fullPath = path.join(inDir, filename);
|
|
1156
|
+
markActivity('watch');
|
|
1000
1157
|
|
|
1001
1158
|
clearTimeout(debounce[fullPath]);
|
|
1002
1159
|
debounce[fullPath] = setTimeout(async () => {
|