gm-plugkit 2.0.1441 → 2.0.1443

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 CHANGED
@@ -879,13 +879,32 @@ function probeUnsupervisedWatcher(spoolDir) {
879
879
  } catch (_) {}
880
880
  }
881
881
 
882
+ function resolveNodeRuntime() {
883
+ const isNodeExe = (p) => /(^|[\\/])node(\.exe)?$/i.test(String(p || ''));
884
+ const candidates = [];
885
+ if (isNodeExe(process.env.GM_NODE_PATH)) candidates.push(process.env.GM_NODE_PATH);
886
+ if (isNodeExe(process.execPath)) candidates.push(process.execPath);
887
+ try {
888
+ const which = process.platform === 'win32' ? 'where' : 'which';
889
+ const out = require('child_process').spawnSync(which, ['node'], { encoding: 'utf8', windowsHide: true });
890
+ if (out && out.stdout) {
891
+ const first = out.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
892
+ if (first) candidates.push(first);
893
+ }
894
+ } catch (_) {}
895
+ for (const c of candidates) {
896
+ try { const r = require('child_process').spawnSync(c, ['--version'], { stdio: 'ignore', windowsHide: true }); if (r && r.status === 0) return c; } catch (_) {}
897
+ }
898
+ return process.execPath;
899
+ }
900
+
882
901
  function startSpoolDaemon() {
883
902
  try {
884
903
  const wrapper = path.join(gmToolsDir(), 'plugkit-wasm-wrapper.js');
885
904
  if (!fs.existsSync(wrapper)) {
886
905
  return { ok: false, error: `wrapper not at ${wrapper} — ensureReady() must run first` };
887
906
  }
888
- const runtime = process.execPath;
907
+ const runtime = resolveNodeRuntime();
889
908
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
890
909
  const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
891
910
  fs.mkdirSync(spoolDir, { recursive: true });
@@ -919,7 +938,7 @@ function startSpoolDaemon() {
919
938
 
920
939
  const logFd = fs.openSync(logPath, 'a');
921
940
  try { fs.writeSync(logFd, `\n--- supervisor spawn ${new Date().toISOString()} parent=${process.pid} ---\n`); } catch (_) {}
922
- const child = require('child_process').spawn(process.execPath, [supervisor], {
941
+ const child = require('child_process').spawn(runtime, [supervisor], {
923
942
  detached: true,
924
943
  stdio: ['ignore', logFd, logFd],
925
944
  windowsHide: true,
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ // Legacy fallback. The canonical surface for lang/*.js plugins is the wasm
3
+ // `lang` verb in rs-plugkit, dispatched via .gm/exec-spool/in/lang/<N>.txt.
4
+ // This standalone runner is kept for direct CLI debug + pre-cascade situations.
5
+ 'use strict';
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ async function main() {
10
+ const [, , projectDir, command, codeB64] = process.argv;
11
+ if (!projectDir || !command || codeB64 === undefined) {
12
+ console.log(JSON.stringify({ ok: false, error: 'usage: lang-host-runner <projectDir> <command> <code-base64>' }));
13
+ process.exit(2);
14
+ }
15
+ const code = Buffer.from(codeB64, 'base64').toString('utf8');
16
+ const langDir = path.join(projectDir, 'lang');
17
+ if (!fs.existsSync(langDir)) {
18
+ console.log(JSON.stringify({ ok: false, error: 'no-lang-dir', langDir }));
19
+ return;
20
+ }
21
+ const files = fs.readdirSync(langDir).filter(f => f.endsWith('.js') && f !== 'loader.js');
22
+ const plugins = files.reduce((acc, f) => {
23
+ try {
24
+ const p = require(path.join(langDir, f));
25
+ if (p && typeof p.id === 'string' && p.exec && p.exec.match instanceof RegExp && typeof p.exec.run === 'function') {
26
+ acc.push(p);
27
+ }
28
+ } catch (_) {}
29
+ return acc;
30
+ }, []);
31
+ const plugin = plugins.find(p => p.exec.match.test(command));
32
+ if (!plugin) {
33
+ console.log(JSON.stringify({ ok: false, error: 'no-plugin-matched', command, available: plugins.map(p => p.id) }));
34
+ return;
35
+ }
36
+ const t0 = Date.now();
37
+ const timer = setTimeout(() => {
38
+ console.log(JSON.stringify({ ok: false, error: 'timeout', plugin_id: plugin.id, ms: Date.now() - t0 }));
39
+ process.exit(0);
40
+ }, 30000);
41
+ try {
42
+ const out = await plugin.exec.run(code, projectDir);
43
+ clearTimeout(timer);
44
+ console.log(JSON.stringify({ ok: true, plugin_id: plugin.id, output: String(out), ms: Date.now() - t0 }));
45
+ } catch (e) {
46
+ clearTimeout(timer);
47
+ console.log(JSON.stringify({ ok: false, error: String(e && e.message || e), plugin_id: plugin.id, ms: Date.now() - t0 }));
48
+ }
49
+ }
50
+
51
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1441",
3
+ "version": "2.0.1443",
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": {
@@ -11,6 +11,8 @@
11
11
  "cli.js",
12
12
  "index.js",
13
13
  "bootstrap.js",
14
+ "supervisor.js",
15
+ "lang-host-runner.js",
14
16
  "plugkit-wasm-wrapper.js",
15
17
  "plugkit.version",
16
18
  "plugkit.sha256",
package/supervisor.js ADDED
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { spawn, spawnSync } = require('child_process');
8
+
9
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
10
+ const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
11
+ fs.mkdirSync(spoolDir, { recursive: true });
12
+
13
+ const STATUS_PATH = path.join(spoolDir, '.status.json');
14
+ const SHUTDOWN_REASON_PATH = path.join(spoolDir, '.shutdown-reason.json');
15
+ const SUPERVISOR_PATH = path.join(spoolDir, '.supervisor.json');
16
+ const LOG_PATH = path.join(spoolDir, '.watcher.log');
17
+ const GM_LOG_ROOT = process.env.GM_LOG_DIR || path.join(os.homedir(), '.claude', 'gm-log');
18
+
19
+ const POLL_INTERVAL_MS = 10_000;
20
+ const STATUS_STALE_MS = 30_000;
21
+ const MAX_RESTART_BURST = 5;
22
+ const RESTART_WINDOW_MS = 60_000;
23
+
24
+ function logEvent(event, fields) {
25
+ try {
26
+ const day = new Date().toISOString().slice(0, 10);
27
+ const dir = path.join(GM_LOG_ROOT, day);
28
+ fs.mkdirSync(dir, { recursive: true });
29
+ const line = JSON.stringify({
30
+ ts: new Date().toISOString(),
31
+ sub: 'plugkit',
32
+ event,
33
+ pid: process.pid,
34
+ sess: process.env.CLAUDE_SESSION_ID || '',
35
+ cwd: process.cwd(),
36
+ role: 'supervisor',
37
+ ...fields,
38
+ }) + '\n';
39
+ fs.appendFileSync(path.join(dir, 'plugkit.jsonl'), line);
40
+ } catch (_) {}
41
+ }
42
+
43
+ function writeSupervisorStatus(state, extra) {
44
+ try {
45
+ fs.writeFileSync(SUPERVISOR_PATH, JSON.stringify({
46
+ pid: process.pid,
47
+ ts: Date.now(),
48
+ state,
49
+ ...(extra || {}),
50
+ }));
51
+ } catch (_) {}
52
+ }
53
+
54
+ function pidAlive(pid) {
55
+ if (!Number.isFinite(pid) || pid <= 0) return false;
56
+ try { process.kill(pid, 0); return true; } catch (_) { return false; }
57
+ }
58
+
59
+ function readStatus() {
60
+ try { return JSON.parse(fs.readFileSync(STATUS_PATH, 'utf-8')); } catch (_) { return null; }
61
+ }
62
+
63
+ function readShutdownReason() {
64
+ try { return JSON.parse(fs.readFileSync(SHUTDOWN_REASON_PATH, 'utf-8')); } catch (_) { return null; }
65
+ }
66
+
67
+ let lastSpawnedAt = 0;
68
+ let restartTimestamps = [];
69
+ let currentChildPid = null;
70
+ let currentBootReason = 'initial';
71
+
72
+ function spawnWatcher(bootReason) {
73
+ lastSpawnedAt = Date.now();
74
+ restartTimestamps.push(Date.now());
75
+ restartTimestamps = restartTimestamps.filter(t => Date.now() - t < RESTART_WINDOW_MS);
76
+ if (restartTimestamps.length > MAX_RESTART_BURST) {
77
+ logEvent('supervisor.giving-up', {
78
+ reason: 'restart-burst-exceeded',
79
+ restarts_in_window: restartTimestamps.length,
80
+ window_ms: RESTART_WINDOW_MS,
81
+ max: MAX_RESTART_BURST,
82
+ severity: 'critical',
83
+ });
84
+ writeSupervisorStatus('giving-up', { reason: 'restart-burst-exceeded' });
85
+ process.exit(2);
86
+ }
87
+
88
+ const primaryWrapper = path.join(os.homedir(), '.gm-tools', 'plugkit-wasm-wrapper.js');
89
+ const fallbackWrapper = path.join(os.homedir(), '.claude', 'gm-tools', 'plugkit-wasm-wrapper.js');
90
+ const wrapper = fs.existsSync(primaryWrapper) ? primaryWrapper : fallbackWrapper;
91
+ if (!fs.existsSync(wrapper)) {
92
+ logEvent('supervisor.wrapper-missing', { wrapper, severity: 'critical' });
93
+ writeSupervisorStatus('error', { error: 'wrapper-missing' });
94
+ process.exit(3);
95
+ }
96
+
97
+ const isNodeExe = (p) => /(^|[\\/])node(\.exe)?$/i.test(String(p || ''));
98
+ const resolveNode = () => {
99
+ const candidates = [];
100
+ if (isNodeExe(process.env.PLUGKIT_RUNTIME)) candidates.push(process.env.PLUGKIT_RUNTIME);
101
+ if (isNodeExe(process.execPath)) candidates.push(process.execPath);
102
+ if (process.env.GM_NODE_PATH) candidates.push(process.env.GM_NODE_PATH);
103
+ try {
104
+ const which = process.platform === 'win32' ? 'where' : 'which';
105
+ const out = spawnSync(which, ['node'], { encoding: 'utf8', windowsHide: true });
106
+ if (out && out.stdout) {
107
+ const first = out.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
108
+ if (first) candidates.push(first);
109
+ }
110
+ } catch (_) {}
111
+ for (const c of candidates) {
112
+ try { const r = spawnSync(c, ['--version'], { stdio: 'ignore', windowsHide: true }); if (r && r.status === 0) return c; } catch (_) {}
113
+ }
114
+ return process.execPath;
115
+ };
116
+ let cmd = resolveNode();
117
+ let args = [wrapper, 'spool'];
118
+
119
+ let logFd = null;
120
+ try { logFd = fs.openSync(LOG_PATH, 'a'); } catch (_) {}
121
+ try {
122
+ if (logFd !== null) fs.writeSync(logFd, `\n--- watcher spawn ${new Date().toISOString()} supervisor=${process.pid} reason=${bootReason} ---\n`);
123
+ } catch (_) {}
124
+
125
+ const child = spawn(cmd, args, {
126
+ detached: false,
127
+ stdio: ['ignore', logFd || 'ignore', logFd || 'ignore'],
128
+ windowsHide: true,
129
+ env: {
130
+ ...process.env,
131
+ CLAUDE_PROJECT_DIR: projectDir,
132
+ PLUGKIT_BOOT_REASON: bootReason,
133
+ PLUGKIT_SUPERVISOR_PID: String(process.pid),
134
+ },
135
+ });
136
+
137
+ try { if (logFd !== null) fs.closeSync(logFd); } catch (_) {}
138
+ currentChildPid = child.pid;
139
+ currentBootReason = bootReason;
140
+ writeSupervisorStatus('watching', { watcher_pid: child.pid, boot_reason: bootReason });
141
+ logEvent('supervisor.spawned-watcher', { watcher_pid: child.pid, boot_reason: bootReason, runtime: cmd });
142
+
143
+ child.on('exit', (code, signal) => {
144
+ const shutdownReason = readShutdownReason();
145
+ const reason = shutdownReason && shutdownReason.reason;
146
+ const idleClean = reason === 'idle';
147
+ const plannedReasons = new Set(['idle', 'sigterm', 'version-change', 'wrapper-change', 'peer-stale-takeover', 'external-planned']);
148
+ const isPlanned = plannedReasons.has(reason);
149
+ const eventName = idleClean
150
+ ? 'supervisor.watcher-exited-idle'
151
+ : reason === 'version-change'
152
+ ? 'supervisor.watcher-exited-for-update'
153
+ : 'supervisor.watcher-exited-unexpectedly';
154
+ logEvent(eventName, {
155
+ watcher_pid: currentChildPid,
156
+ exit_code: code,
157
+ signal,
158
+ shutdown_reason: reason || null,
159
+ had_shutdown_reason_file: shutdownReason !== null,
160
+ severity: isPlanned ? 'info' : 'critical',
161
+ uptime_ms: Date.now() - lastSpawnedAt,
162
+ ...(shutdownReason || {}),
163
+ });
164
+ if (idleClean) {
165
+ writeSupervisorStatus('exited-idle', { watcher_pid: currentChildPid });
166
+ try { fs.unlinkSync(SUPERVISOR_PATH); } catch (_) {}
167
+ process.exit(0);
168
+ }
169
+ const respawnReason = reason === 'version-change' ? 'planned-restart-version-change' : 'unplanned-restart-after-exit';
170
+ writeSupervisorStatus('restarting', {
171
+ prior_watcher_pid: currentChildPid,
172
+ prior_exit_code: code,
173
+ prior_signal: signal,
174
+ prior_shutdown_reason: reason || null,
175
+ respawn_reason: respawnReason,
176
+ });
177
+ setTimeout(() => spawnWatcher(respawnReason), 1500);
178
+ });
179
+
180
+ child.on('error', (err) => {
181
+ logEvent('supervisor.spawn-error', { error: err.message, severity: 'critical' });
182
+ });
183
+ }
184
+
185
+ function checkWatcherHealth() {
186
+ if (!currentChildPid) return;
187
+ if (!pidAlive(currentChildPid)) {
188
+ return;
189
+ }
190
+ const status = readStatus();
191
+ if (!status) {
192
+ logEvent('supervisor.status-missing', {
193
+ watcher_pid: currentChildPid,
194
+ severity: 'warn',
195
+ });
196
+ return;
197
+ }
198
+ const age = Date.now() - (status.ts || 0);
199
+ if (age > STATUS_STALE_MS) {
200
+ logEvent('supervisor.heartbeat-stale', {
201
+ watcher_pid: currentChildPid,
202
+ status_pid: status.pid,
203
+ status_age_ms: age,
204
+ stale_limit_ms: STATUS_STALE_MS,
205
+ severity: 'critical',
206
+ });
207
+ try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
208
+ if (process.platform === 'win32') {
209
+ try { spawnSync('taskkill', ['/F', '/T', '/PID', String(currentChildPid)], { stdio: 'ignore', windowsHide: true, timeout: 3000 }); } catch (_) {}
210
+ }
211
+ }
212
+ }
213
+
214
+ process.on('SIGINT', () => {
215
+ logEvent('supervisor.shutdown', { reason: 'sigint' });
216
+ writeSupervisorStatus('shutdown', { reason: 'sigint' });
217
+ if (currentChildPid && pidAlive(currentChildPid)) {
218
+ try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
219
+ }
220
+ process.exit(0);
221
+ });
222
+ process.on('SIGTERM', () => {
223
+ logEvent('supervisor.shutdown', { reason: 'sigterm' });
224
+ writeSupervisorStatus('shutdown', { reason: 'sigterm' });
225
+ if (currentChildPid && pidAlive(currentChildPid)) {
226
+ try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
227
+ }
228
+ process.exit(0);
229
+ });
230
+
231
+ writeSupervisorStatus('starting', {});
232
+ logEvent('supervisor.starting', { spool_dir: spoolDir });
233
+ try { fs.unlinkSync(path.join(spoolDir, '.pre-supervised-watcher.json')); } catch (_) {}
234
+ spawnWatcher('initial');
235
+ setInterval(checkWatcherHealth, POLL_INTERVAL_MS);
236
+ setInterval(() => writeSupervisorStatus('watching', { watcher_pid: currentChildPid, boot_reason: currentBootReason }), 10_000);