gm-skill 2.0.2208 → 2.0.2210

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 CHANGED
@@ -30,7 +30,7 @@ Plugkit-core = the wasm cdylib guest (the gm brain), published as `plugkit.wasm`
30
30
 
31
31
  **`agentplug-runner`** (repo `AnEntrypoint/agentplug`, published to `AnEntrypoint/agentplug-bin`) is a real native wasmtime binary that loads gm.wasm as one plugin among siblings (`bert`/`libsql`/`treesitter`), routing gm.wasm's `host_plugin_call`/`host_vec_embed` to those shared plugins -- browser and task are both native in agentplug-host, needing nothing from the retired JS wrapper. Full mechanics: the recall store (`recall: agentplug-runner wasmtime plugin loading mechanics`).
32
32
 
33
- The `gm-plugkit` npm identity stays load-bearing as the thin launcher edge only: `bun x gm-plugkit@latest spool` (`cli.js::tryDelegateToRunner`) re-execs the `agentplug-runner` executable staged at `~/.gm-tools/agentplug-runner` (gm-plugkit's own launcher-edge install location) and exits. **The executable's own runtime state lives elsewhere**: `agentplug-host::install_dir()` resolves to `~/.agentplug`, not `~/.gm-tools` -- every plugin wasm/version marker (`~/.agentplug/plugins/<name>.wasm`, `<name>.version`), `daemon-status.json`, `daemon-registry.txt`, and `daemon-config.json` live under `~/.agentplug`, fully independent of gm-plugkit's own launcher-edge files under `~/.gm-tools` (which still holds gm-plugkit's own `plugkit.wasm`/`plugkit-slim.wasm`/`plugkit.version` for the pre-agentplug JS-wrapper code path, now dormant once delegation succeeds). Inspecting the wrong directory for the live-served gm.wasm version is the single most likely false-alarm source when diagnosing update-path issues -- always check `~/.agentplug/plugins/gm.version`, never `~/.gm-tools/plugins/gm.version` or `~/.gm-tools/plugkit.version`, to see what agentplug-runner is actually serving. `bootstrap.js` still seeds per-project wiring and downloads `plugkit.wasm`/`plugkit-slim.wasm` (sha256-pinned) from `plugkit-bin` into `~/.gm-tools`, but this path is bypassed entirely once `tryDelegateToRunner` succeeds (the common case whenever `~/.gm-tools/agentplug-runner.exe` exists) -- `plugkit-slim.wasm` at the `~/.gm-tools` top level is therefore normally ABSENT by design on a machine where agentplug-runner is doing the serving; that is not a bootstrap failure. `startSpoolDaemon()` launches agentplug-runner or, when it is absent, fails loudly with an actionable install message -- there is no silent no-loader state. Size + embedded-model mechanics: the recall store (`recall: WASM-only plugkit size mechanics`).
33
+ The `gm-plugkit` npm identity stays load-bearing as the thin launcher edge only: `bun x gm-plugkit@latest spool` (`cli.js::tryDelegateToRunner`) re-execs the `agentplug-runner` executable staged at `~/.gm-tools/agentplug-runner` and exits -- there is no silent no-loader state. The launcher-edge install path (`~/.gm-tools`) and the runner's own runtime state (`~/.agentplug`) are two different directories; always check `~/.agentplug/plugins/gm.version` for what is actually being served, never `~/.gm-tools`. Full path-split mechanics: the recall store (`recall: agentplug-runner runtime-state path split`). Size + embedded-model mechanics: the recall store (`recall: WASM-only plugkit size mechanics`).
34
34
 
35
35
  **`bin/install.js` hard-requires agentplug-runner.** It downloads the sha256-verified native runner from `AnEntrypoint/agentplug-bin` for the host platform, and if none is published (or the download/verify fails) it fails the install loudly with a clear message rather than leaving the user with no loader. There is no JS-host fallback to silently fall through to anymore.
36
36
 
package/bin/bootstrap.js CHANGED
@@ -5,42 +5,40 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
7
  const crypto = require('crypto');
8
- const { spawnSync } = require('child_process');
9
- const { pidAlive, sha256OfFile, sha256OfFileSync } = require('../gm-plugkit/gm-process');
10
8
  const shared = require('../gm-plugkit/bootstrap-shared');
9
+ const core = require('../gm-plugkit/bootstrap-wasm-core');
11
10
  const {
12
11
  obsEvent,
13
12
  cacheRoot,
14
13
  fallbackCacheRoot,
15
- gmToolsDir,
16
14
  ensureDir,
17
15
  acquireLock,
18
16
  releaseLock,
19
- isLockStale,
20
- pruneOldVersions,
21
17
  healIfShaMatches,
22
- daemonVersionSentinel,
18
+ pruneOldVersions,
23
19
  readDaemonVersion,
24
20
  writeDaemonVersion,
25
- pidCommandLineForKillGuard,
26
- pidIsPlugkitProcess,
27
- writeKillAttribution,
28
- killPid,
29
21
  killSpoolWatcherInCwd,
30
22
  proactiveKillForNewInstall,
31
23
  ensureNextStepWiring,
32
- resolveWindowsExe,
33
- resolveNpmCliJs,
34
24
  } = shared;
25
+ const {
26
+ readVersionFile,
27
+ readShaManifest,
28
+ resolveCacheWasmPath,
29
+ extractNpmPackageWithRetry: extractNpmPackageWithRetryCore,
30
+ writeBootstrapError,
31
+ clearBootstrapError,
32
+ } = core;
35
33
 
36
- const NPM_PACKAGE = 'plugkit-wasm';
37
- const ATTEMPT_TIMEOUT_MS = 10 * 60 * 1000;
38
- const MAX_ATTEMPTS = 3;
39
- const BACKOFF_MS = [5000, 15000];
40
- const LOCK_STALE_MS = 30 * 60 * 1000;
34
+ const log = core.makeLogger('plugkit-bootstrap');
35
+
36
+ function copyWasmToGmTools(wasmPath, wrapperDir, version) {
37
+ return core.copyWasmToGmTools(wasmPath, version, { wrapperDir });
38
+ }
41
39
 
42
- function log(msg) {
43
- try { process.stderr.write(`[plugkit-bootstrap] ${msg}\n`); } catch (_) {}
40
+ function extractNpmPackageWithRetry(destPath, version) {
41
+ return extractNpmPackageWithRetryCore(destPath, version, { log });
44
42
  }
45
43
 
46
44
  function discoverBundledSkills(wrapperDir) {
@@ -126,139 +124,6 @@ function probeBinaryVersion(binPath) {
126
124
  } catch (_) { return null; }
127
125
  }
128
126
 
129
- function writeBootstrapError(spec) {
130
- try {
131
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
132
- const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
133
- fs.mkdirSync(spoolDir, { recursive: true });
134
- const out = path.join(spoolDir, '.bootstrap-error.json');
135
- fs.writeFileSync(out, JSON.stringify({ ts: new Date().toISOString(), ...spec }, null, 2));
136
- } catch (_) {}
137
- }
138
-
139
- function clearBootstrapError() {
140
- try {
141
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
142
- const out = path.join(projectDir, '.gm', 'exec-spool', '.bootstrap-error.json');
143
- fs.unlinkSync(out);
144
- } catch (_) {}
145
- }
146
-
147
- function copyWasmToGmTools(wasmPath, wrapperDir, version) {
148
- const dst = gmToolsDir();
149
- fs.mkdirSync(dst, { recursive: true });
150
- const target = path.join(dst, 'plugkit.wasm');
151
- if (fs.existsSync(target)) {
152
- try {
153
- const cur = sha256OfFileSync(target);
154
- const src = sha256OfFileSync(wasmPath);
155
- if (cur === src) {
156
- try { fs.writeFileSync(path.join(dst, 'plugkit.version'), version); } catch (_) {}
157
- return;
158
- }
159
- } catch (_) {}
160
- }
161
- fs.copyFileSync(wasmPath, target);
162
- fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
163
- try {
164
- const srcSha = path.join(wrapperDir, 'plugkit.sha256');
165
- if (fs.existsSync(srcSha)) fs.copyFileSync(srcSha, path.join(dst, 'plugkit.sha256'));
166
- } catch (_) {}
167
- }
168
-
169
- function readVersionFile(wrapperDir) {
170
- const p = path.join(wrapperDir, 'plugkit.version');
171
- if (!fs.existsSync(p)) throw new Error(`plugkit.version not found at ${p}`);
172
- return fs.readFileSync(p, 'utf8').trim();
173
- }
174
-
175
-
176
- function readShaManifest(wrapperDir, manifestName) {
177
- const p = path.join(wrapperDir, manifestName || 'plugkit.sha256');
178
- if (!fs.existsSync(p)) return null;
179
- const out = {};
180
- for (const line of fs.readFileSync(p, 'utf8').split(/\r?\n/)) {
181
- const m = line.match(/^([0-9a-f]{64})\s+(\S+)\s*$/i);
182
- if (m) out[m[2]] = m[1].toLowerCase();
183
- }
184
- return out;
185
- }
186
-
187
- async function extractNpmPackageWasm(destPath, version) {
188
- const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
189
- try {
190
- ensureDir(tempDir);
191
- const startMs = Date.now();
192
- log(`extracting npm package ${NPM_PACKAGE}@${version} to ${tempDir}`);
193
- obsEvent('bootstrap', 'npm.extract.start', { package: NPM_PACKAGE, version });
194
-
195
- const npmResolved = resolveWindowsExe('npm');
196
- const isCmdShim = process.platform === 'win32' && /\.(cmd|bat)$/i.test(npmResolved);
197
- const npmCliJs = isCmdShim ? resolveNpmCliJs(npmResolved) : null;
198
- const installArgs = ['install', '--no-audit', '--no-fund', '--no-save', '--prefix', tempDir, NPM_PACKAGE + '@' + version];
199
-
200
- const spawnCmd = npmCliJs ? process.execPath : (isCmdShim && /\s/.test(npmResolved) ? `"${npmResolved}"` : npmResolved);
201
- const rawArgs = npmCliJs ? [npmCliJs, ...installArgs] : installArgs;
202
- const spawnArgs = (isCmdShim && !npmCliJs) ? rawArgs.map(a => /[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a) : rawArgs;
203
- const result = spawnSync(
204
- spawnCmd,
205
- spawnArgs,
206
- {
207
- stdio: ['ignore', 'pipe', 'pipe'],
208
- timeout: ATTEMPT_TIMEOUT_MS,
209
- encoding: 'utf8',
210
- windowsHide: true,
211
- ...((isCmdShim && !npmCliJs) ? { shell: true } : {}),
212
- }
213
- );
214
-
215
- if (result.error) throw result.error;
216
- if (result.status !== 0) {
217
- throw new Error(`npm install extraction failed: ${result.stderr || result.stdout || 'unknown error'}`);
218
- }
219
-
220
- const nodeModulesPath = path.join(tempDir, 'node_modules', NPM_PACKAGE, 'plugkit.wasm');
221
- if (!fs.existsSync(nodeModulesPath)) {
222
- throw new Error(`plugkit.wasm not found in extracted npm package at ${nodeModulesPath}`);
223
- }
224
-
225
- fs.copyFileSync(nodeModulesPath, destPath);
226
- log(`extracted ${nodeModulesPath} -> ${destPath}`);
227
- obsEvent('bootstrap', 'npm.extract.end', { dur_ms: Date.now() - startMs, ok: true });
228
- } finally {
229
- try { fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 }); } catch (_) {}
230
- }
231
- }
232
-
233
- async function extractNpmPackageWithRetry(destPath, version) {
234
- let lastErr;
235
- for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
236
- try {
237
- log(`npm extract attempt ${attempt}/${MAX_ATTEMPTS}: ${NPM_PACKAGE}@${version}`);
238
- await extractNpmPackageWasm(destPath, version);
239
- return;
240
- } catch (err) {
241
- lastErr = err;
242
- log(`attempt ${attempt} failed: ${err.message}`);
243
- obsEvent('bootstrap', 'npm.extract.attempt_failed', { package: NPM_PACKAGE, attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
244
- if (err && (err.code === 'ENOENT' || /ENOENT/.test(String(err.message || '')))) {
245
- log(`npx binary unresolvable (ENOENT); skipping retries, falling back`);
246
- throw err;
247
- }
248
- if (err && (err.code === 'EINVAL' || /EINVAL/.test(String(err.message || '')))) {
249
- log(`spawn EINVAL on npx shim; skipping retries, falling back`);
250
- throw err;
251
- }
252
- if (attempt < MAX_ATTEMPTS) {
253
- const wait = BACKOFF_MS[attempt - 1] || 120000;
254
- log(`backing off ${wait}ms`);
255
- await new Promise(r => setTimeout(r, wait));
256
- }
257
- }
258
- }
259
- throw lastErr;
260
- }
261
-
262
127
  async function bootstrap(opts) {
263
128
  opts = opts || {};
264
129
  const wrapperDir = opts.wrapperDir || __dirname;
@@ -282,7 +147,7 @@ async function bootstrap(opts) {
282
147
 
283
148
  if (fs.existsSync(wasmFinalPath) && fs.existsSync(wasmOkSentinel)) {
284
149
  if (wasmExpectedSha) {
285
- const actualSha = sha256OfFileSync(wasmFinalPath);
150
+ const actualSha = require('../gm-plugkit/gm-process').sha256OfFileSync(wasmFinalPath);
286
151
  if (actualSha === wasmExpectedSha) {
287
152
  obsEvent('bootstrap', 'decision.hit', { reason: 'sha-match', version, path: wasmFinalPath });
288
153
  copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
@@ -332,7 +197,7 @@ async function bootstrap(opts) {
332
197
  if (fs.existsSync(wasmPartialPath)) {
333
198
  try {
334
199
  const st = fs.statSync(wasmPartialPath);
335
- if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
200
+ if (Date.now() - st.mtimeMs > 30 * 60 * 1000) {
336
201
  fs.unlinkSync(wasmPartialPath);
337
202
  log(`cleared stale partial: ${wasmPartialPath}`);
338
203
  }
@@ -351,7 +216,7 @@ async function bootstrap(opts) {
351
216
  }
352
217
 
353
218
  if (wasmExpectedSha) {
354
- const got = await sha256OfFile(wasmPartialPath);
219
+ const got = await require('../gm-plugkit/gm-process').sha256OfFile(wasmPartialPath);
355
220
  if (got !== wasmExpectedSha) {
356
221
  try { fs.unlinkSync(wasmPartialPath); } catch (_) {}
357
222
  writeBootstrapError({
@@ -396,11 +261,7 @@ function getWasmPath(opts) {
396
261
  try { const r = cacheRoot(); ensureDir(r); return r; }
397
262
  catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
398
263
  })();
399
- const verDir = path.join(root, `v${version}`);
400
- const wasmPath = path.join(verDir, 'plugkit.wasm');
401
- const okSentinel = path.join(verDir, '.wasm-ok');
402
- if (fs.existsSync(wasmPath) && fs.existsSync(okSentinel)) return wasmPath;
403
- return null;
264
+ return resolveCacheWasmPath(root, version, 'plugkit.wasm');
404
265
  }
405
266
 
406
267
  function killStaleDaemonIfVersionChanged(wrapperDir) {
@@ -1 +1 @@
1
- 4785c38fbedab482430d5925eebf3eb687b5cc4607dd2d8442de89feb20c532a plugkit-slim.wasm
1
+ ffa850254a06a6a16e190b88a64dd960c369aae0a2ba2e985edc7a72ff11e779 plugkit-slim.wasm
@@ -1 +1 @@
1
- 0.1.1048
1
+ 0.1.1049
@@ -1 +1 @@
1
- 81a01efb3f14df1df3a1891a34920fdfabde276519fc1186cfd4e325a3efe907 plugkit.wasm
1
+ d0008048fd103533948b5d91e48890ba3ff9b7d4544d00260bfa443d99077d8d plugkit.wasm
@@ -0,0 +1,320 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { spawnSync } = require('child_process');
7
+ const { pidAlive, sha256OfFileSync } = require('./gm-process');
8
+
9
+
10
+ const LOCK_STALE_MS = 30 * 60 * 1000;
11
+ const ATTEMPT_TIMEOUT_MS = 10 * 60 * 1000;
12
+
13
+ function obsEvent(subsystem, event, fields) {
14
+ if (process.env.GM_LOG_DISABLE) return;
15
+ try {
16
+ const root = process.env.GM_LOG_DIR || path.join(os.homedir(), '.claude', 'gm-log');
17
+ const day = new Date().toISOString().slice(0, 10);
18
+ const dir = path.join(root, day);
19
+ fs.mkdirSync(dir, { recursive: true });
20
+ const line = JSON.stringify({
21
+ ts: new Date().toISOString(),
22
+ sub: subsystem,
23
+ event,
24
+ pid: process.pid,
25
+ sess: process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
26
+ ...fields,
27
+ });
28
+ fs.appendFileSync(path.join(dir, `${subsystem}.jsonl`), line + '\n');
29
+ } catch (_) {}
30
+ }
31
+
32
+ function cacheRoot() {
33
+ const home = os.homedir();
34
+ if (process.env.PLUGKIT_CACHE_DIR) return process.env.PLUGKIT_CACHE_DIR;
35
+ if (os.platform() === 'win32') {
36
+ const base = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
37
+ return path.join(base, 'plugkit', 'bin');
38
+ }
39
+ if (os.platform() === 'darwin') return path.join(home, 'Library', 'Caches', 'plugkit', 'bin');
40
+ const xdg = process.env.XDG_CACHE_HOME || path.join(home, '.cache');
41
+ return path.join(xdg, 'plugkit', 'bin');
42
+ }
43
+
44
+ function fallbackCacheRoot() {
45
+ return path.join(os.tmpdir(), 'plugkit-cache', 'bin');
46
+ }
47
+
48
+ function gmToolsDir() {
49
+ const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
50
+ const primary = path.join(home, '.gm-tools');
51
+ const fallback = path.join(home, '.claude', 'gm-tools');
52
+ if (fs.existsSync(primary)) return primary;
53
+ if (fs.existsSync(fallback)) return fallback;
54
+ return primary;
55
+ }
56
+
57
+ function ensureDir(dir) {
58
+ fs.mkdirSync(dir, { recursive: true });
59
+ }
60
+
61
+ function acquireLock(lockPath) {
62
+ const start = Date.now();
63
+ for (;;) {
64
+ try {
65
+ const fd = fs.openSync(lockPath, 'wx');
66
+ fs.writeSync(fd, String(process.pid));
67
+ fs.closeSync(fd);
68
+ return true;
69
+ } catch (err) {
70
+ if (err.code !== 'EEXIST') throw err;
71
+ let stale = false;
72
+ try {
73
+ const st = fs.statSync(lockPath);
74
+ if (Date.now() - st.mtimeMs > LOCK_STALE_MS) stale = true;
75
+ const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
76
+ if (Number.isFinite(owner) && owner !== process.pid && !pidAlive(owner)) stale = true;
77
+ } catch (_) { stale = true; }
78
+ if (stale) {
79
+ try { fs.unlinkSync(lockPath); } catch (_) {}
80
+ continue;
81
+ }
82
+ if (Date.now() - start > ATTEMPT_TIMEOUT_MS) throw new Error(`lock wait timeout: ${lockPath}`);
83
+ try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000); }
84
+ catch (e) { obsEvent('bootstrap', 'acquire-lock.atomics-wait-failed', { error: e.message, lockPath }); }
85
+ }
86
+ }
87
+ }
88
+
89
+ function releaseLock(lockPath) {
90
+ try { fs.unlinkSync(lockPath); } catch (_) {}
91
+ }
92
+
93
+ function isLockStale(lockPath) {
94
+ try {
95
+ const st = fs.statSync(lockPath);
96
+ if (Date.now() - st.mtimeMs > LOCK_STALE_MS) return true;
97
+ const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
98
+ if (Number.isFinite(owner) && !pidAlive(owner)) return true;
99
+ } catch (_) { return true; }
100
+ return false;
101
+ }
102
+
103
+ function pruneOldVersions(root, keepVersion) {
104
+ try {
105
+ const entries = fs.readdirSync(root);
106
+ for (const e of entries) {
107
+ if (!e.startsWith('v')) continue;
108
+ if (e === `v${keepVersion}`) continue;
109
+ const dir = path.join(root, e);
110
+ const lock = path.join(dir, '.lock');
111
+ if (fs.existsSync(lock) && !isLockStale(lock)) continue;
112
+ if (fs.existsSync(lock)) { try { fs.unlinkSync(lock); } catch (_) {} }
113
+ try {
114
+ fs.rmSync(dir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 });
115
+ } catch (_) {}
116
+ }
117
+ } catch (_) {}
118
+ }
119
+
120
+ function healIfShaMatches(binPath, expectedSha, sentinelPath, partialPath, kind) {
121
+ if (!fs.existsSync(binPath)) return false;
122
+ if (partialPath) { try { if (fs.existsSync(partialPath)) fs.unlinkSync(partialPath); } catch (_) {} }
123
+ if (!expectedSha) return false;
124
+ let got;
125
+ try { got = sha256OfFileSync(binPath); }
126
+ catch (_) { return false; }
127
+ if (got !== expectedSha) {
128
+ try { fs.unlinkSync(binPath); } catch (_) {}
129
+ return false;
130
+ }
131
+ try { fs.writeFileSync(sentinelPath, new Date().toISOString()); } catch (_) { return false; }
132
+ obsEvent('bootstrap', 'cache.heal', { path: binPath, kind });
133
+ return true;
134
+ }
135
+
136
+ function daemonVersionSentinel() {
137
+ const root = (() => {
138
+ try { const r = cacheRoot(); ensureDir(r); return r; }
139
+ catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
140
+ })();
141
+ return path.join(root, '.daemon-version');
142
+ }
143
+
144
+ function readDaemonVersion() {
145
+ try { return fs.readFileSync(daemonVersionSentinel(), 'utf8').trim(); }
146
+ catch (_) { return null; }
147
+ }
148
+
149
+ function writeDaemonVersion(v) {
150
+ try { fs.writeFileSync(daemonVersionSentinel(), String(v)); } catch (_) {}
151
+ }
152
+
153
+ function pidCommandLineForKillGuard(pid) {
154
+ try {
155
+ if (process.platform === 'win32') {
156
+ const r = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId=${Number(pid)}").CommandLine`], { encoding: 'utf8', windowsHide: true, timeout: 5000 });
157
+ return String((r && r.stdout) || '');
158
+ }
159
+ const r = spawnSync('ps', ['-p', String(pid), '-o', 'args='], { encoding: 'utf8', timeout: 5000 });
160
+ return String((r && r.stdout) || '');
161
+ } catch (_) { return ''; }
162
+ }
163
+
164
+ function pidIsPlugkitProcess(pid) {
165
+ return /agentplug-runner(\.exe)?/i.test(pidCommandLineForKillGuard(pid));
166
+ }
167
+
168
+ function writeKillAttribution(targetSpoolDir, info) {
169
+ try {
170
+ fs.mkdirSync(targetSpoolDir, { recursive: true });
171
+ fs.writeFileSync(path.join(targetSpoolDir, '.kill-attribution.json'), JSON.stringify({ killer_pid: process.pid, killer_cwd: process.cwd(), killer_script: __filename, ts: Date.now(), ...info }, null, 2));
172
+ } catch (_) {}
173
+ }
174
+
175
+ function killPid(pid) {
176
+ if (!Number.isFinite(pid) || pid === process.pid || !pidAlive(pid)) return false;
177
+ try { process.kill(pid, 'SIGTERM'); }
178
+ catch (_) { try { process.kill(pid); } catch (_) {} }
179
+ if (os.platform() === 'win32' && pidAlive(pid)) {
180
+ try { spawnSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true, timeout: 3000, killSignal: 'SIGKILL' }); } catch (_) {}
181
+ }
182
+ return true;
183
+ }
184
+
185
+ function killSpoolWatcherInCwd(reason) {
186
+ try {
187
+ const pidPath = path.join(process.cwd(), '.gm', 'exec-spool', '.watcher.pid');
188
+ if (!fs.existsSync(pidPath)) return null;
189
+ const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
190
+ if (pidAlive(pid) && !pidIsPlugkitProcess(pid)) {
191
+ obsEvent('bootstrap', 'watcher.kill-skipped-pid-reused', { pid, reason });
192
+ try { fs.unlinkSync(pidPath); } catch (_) {}
193
+ return null;
194
+ }
195
+ writeKillAttribution(path.join(process.cwd(), '.gm', 'exec-spool'), { reason, target_pid: pid, via: 'killSpoolWatcherInCwd' });
196
+ if (killPid(pid)) {
197
+ obsEvent('bootstrap', 'watcher.killed', { pid, reason });
198
+ try { fs.unlinkSync(pidPath); } catch (_) {}
199
+ return pid;
200
+ }
201
+ try { fs.unlinkSync(pidPath); } catch (_) {}
202
+ } catch (_) {}
203
+ return null;
204
+ }
205
+
206
+ function proactiveKillForNewInstall(installedVersion) {
207
+ try {
208
+ const reason = `install:v${installedVersion}`;
209
+ killSpoolWatcherInCwd(reason);
210
+ writeDaemonVersion(installedVersion);
211
+ } catch (_) {}
212
+ }
213
+
214
+ function resolveWindowsExe(cmd) {
215
+ if (process.platform !== 'win32') return cmd;
216
+ try {
217
+ const r = spawnSync('where', [cmd], {
218
+ encoding: 'utf-8',
219
+ stdio: ['ignore', 'pipe', 'ignore'],
220
+ windowsHide: true,
221
+ timeout: 800,
222
+ });
223
+ if (r.status !== 0) return cmd;
224
+ const lines = (r.stdout || '').split(/\r?\n/).map(l => l.trim()).filter(Boolean);
225
+ const exe = lines.find(l => /\.exe$/i.test(l));
226
+ const shim = lines.find(l => /\.(cmd|bat)$/i.test(l));
227
+ return exe || shim || cmd;
228
+ } catch {
229
+ return cmd;
230
+ }
231
+ }
232
+
233
+ function resolveNpmCliJs(shimPath) {
234
+ const candidates = [];
235
+ try {
236
+ const execDir = path.dirname(process.execPath);
237
+ candidates.push(path.join(execDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'));
238
+ } catch (_) {}
239
+ try {
240
+ const shimDir = path.dirname(shimPath);
241
+ candidates.push(path.join(shimDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'));
242
+ } catch (_) {}
243
+ candidates.push(
244
+ path.join('C:', 'Program Files', 'nodejs', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
245
+ path.join(process.env.APPDATA || '', 'npm', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
246
+ path.join(process.env.APPDATA || '', 'nvm', process.version.replace(/^v/, ''), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
247
+ );
248
+ return candidates.find(p => { try { return fs.existsSync(p); } catch (_) { return false; } }) || null;
249
+ }
250
+
251
+ function ensureNextStepWiring(cwd) {
252
+ const changes = [];
253
+ const gmDir = path.join(cwd, '.gm');
254
+ try { fs.mkdirSync(gmDir, { recursive: true }); }
255
+ catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: gmDir, error: e.message }); }
256
+
257
+ const nextStepPath = path.join(gmDir, 'next-step.md');
258
+ const nextStepBody = '# Next step\n\nDispatch the `instruction` verb to plugkit by writing `.gm/exec-spool/in/instruction/<N>.txt` (any unique N) with body `{}` (or `{"prompt":"<user request>"}` on the first dispatch of the turn). Read the response from `.gm/exec-spool/out/<N>.json` and follow the imperative in the `instruction` field.\n\nThis file is auto-rewritten by plugkit on every instruction dispatch.\n';
259
+ try {
260
+ if (!fs.existsSync(nextStepPath)) {
261
+ fs.writeFileSync(nextStepPath, nextStepBody);
262
+ changes.push('seeded .gm/next-step.md');
263
+ }
264
+ } catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: nextStepPath, error: e.message }); }
265
+
266
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
267
+ try {
268
+ if (!fs.existsSync(claudeMdPath)) {
269
+ fs.writeFileSync(claudeMdPath, '@AGENTS.md\n');
270
+ changes.push('created CLAUDE.md');
271
+ } else {
272
+ const cur = fs.readFileSync(claudeMdPath, 'utf8');
273
+ const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@AGENTS.md');
274
+ if (!hasLine) {
275
+ fs.writeFileSync(claudeMdPath, '@AGENTS.md\n' + cur);
276
+ changes.push('prepended @AGENTS.md to CLAUDE.md');
277
+ }
278
+ }
279
+ } catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: claudeMdPath, error: e.message }); }
280
+
281
+ const agentsMdPath = path.join(cwd, 'AGENTS.md');
282
+ try {
283
+ if (fs.existsSync(agentsMdPath)) {
284
+ const cur = fs.readFileSync(agentsMdPath, 'utf8');
285
+ const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@.gm/next-step.md');
286
+ if (!hasLine) {
287
+ const sep = cur.endsWith('\n') ? '' : '\n';
288
+ fs.writeFileSync(agentsMdPath, cur + sep + '\n@.gm/next-step.md\n');
289
+ changes.push('appended @.gm/next-step.md to AGENTS.md');
290
+ }
291
+ }
292
+ } catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: agentsMdPath, error: e.message }); }
293
+
294
+ return changes;
295
+ }
296
+
297
+ module.exports = {
298
+ obsEvent,
299
+ cacheRoot,
300
+ fallbackCacheRoot,
301
+ gmToolsDir,
302
+ ensureDir,
303
+ acquireLock,
304
+ releaseLock,
305
+ isLockStale,
306
+ pruneOldVersions,
307
+ healIfShaMatches,
308
+ daemonVersionSentinel,
309
+ readDaemonVersion,
310
+ writeDaemonVersion,
311
+ pidCommandLineForKillGuard,
312
+ pidIsPlugkitProcess,
313
+ writeKillAttribution,
314
+ killPid,
315
+ killSpoolWatcherInCwd,
316
+ proactiveKillForNewInstall,
317
+ ensureNextStepWiring,
318
+ resolveWindowsExe,
319
+ resolveNpmCliJs,
320
+ };
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { spawnSync } = require('child_process');
7
+ const { sha256OfFileSync } = require('./gm-process');
8
+ const {
9
+ obsEvent,
10
+ gmToolsDir,
11
+ resolveWindowsExe,
12
+ resolveNpmCliJs,
13
+ } = require('./bootstrap-shared');
14
+
15
+ const NPM_PACKAGE = 'plugkit-wasm';
16
+ const ATTEMPT_TIMEOUT_MS = 10 * 60 * 1000;
17
+ const MAX_ATTEMPTS = 3;
18
+ const BACKOFF_MS = [5000, 15000];
19
+
20
+ function makeLogger(prefix) {
21
+ return function log(msg) {
22
+ try { process.stderr.write(`[${prefix}] ${msg}\n`); } catch (_) {}
23
+ };
24
+ }
25
+
26
+ function resolveProjectRoot(start) {
27
+ const resolved = path.resolve(start);
28
+ try {
29
+ const r = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: resolved, encoding: 'utf-8', windowsHide: true, timeout: 1500 });
30
+ if (r.status === 0 && r.stdout && r.stdout.trim()) {
31
+ let commonDir = r.stdout.trim();
32
+ if (!path.isAbsolute(commonDir)) commonDir = path.resolve(resolved, commonDir);
33
+ if (/(^|[\\/])\.git$/.test(commonDir)) return path.dirname(commonDir);
34
+ }
35
+ } catch (_) {}
36
+ return resolved;
37
+ }
38
+
39
+ function writeBootstrapError(spec) {
40
+ try {
41
+ const projectDir = resolveProjectRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
42
+ const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
43
+ fs.mkdirSync(spoolDir, { recursive: true });
44
+ fs.writeFileSync(path.join(spoolDir, '.bootstrap-error.json'), JSON.stringify({ ts: new Date().toISOString(), ...spec }, null, 2));
45
+ } catch (_) {}
46
+ }
47
+
48
+ function clearBootstrapError() {
49
+ try {
50
+ const projectDir = resolveProjectRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
51
+ fs.unlinkSync(path.join(projectDir, '.gm', 'exec-spool', '.bootstrap-error.json'));
52
+ } catch (_) {}
53
+ }
54
+
55
+ function readVersionFile(wrapperDir) {
56
+ const p = path.join(wrapperDir, 'plugkit.version');
57
+ if (!fs.existsSync(p)) throw new Error(`plugkit.version not found at ${p}`);
58
+ return fs.readFileSync(p, 'utf8').trim();
59
+ }
60
+
61
+ function readShaManifest(wrapperDir, manifestName) {
62
+ const p = path.join(wrapperDir, manifestName || 'plugkit.sha256');
63
+ if (!fs.existsSync(p)) return null;
64
+ const raw = fs.readFileSync(p, 'utf8');
65
+ try {
66
+ const parsed = JSON.parse(raw);
67
+ if (parsed && typeof parsed === 'object') {
68
+ const out = {};
69
+ for (const [name, sha] of Object.entries(parsed)) {
70
+ if (typeof sha === 'string') out[name] = sha.toLowerCase();
71
+ }
72
+ return out;
73
+ }
74
+ } catch (_) {}
75
+ const out = {};
76
+ for (const line of raw.split(/\r?\n/)) {
77
+ const m = line.match(/^([0-9a-f]{64})\s+(\S+)\s*$/i);
78
+ if (m) out[m[2]] = m[1].toLowerCase();
79
+ }
80
+ return out;
81
+ }
82
+
83
+ function copyWasmToGmTools(wasmPath, version, opts) {
84
+ opts = opts || {};
85
+ const dst = gmToolsDir();
86
+ fs.mkdirSync(dst, { recursive: true });
87
+ const target = path.join(dst, 'plugkit.wasm');
88
+
89
+ let wasmFresh = false;
90
+ if (fs.existsSync(target)) {
91
+ try {
92
+ const cur = sha256OfFileSync(target);
93
+ const src = sha256OfFileSync(wasmPath);
94
+ if (cur === src) wasmFresh = true;
95
+ } catch (_) {}
96
+ }
97
+ if (!wasmFresh) {
98
+ const tmp = `${target}.partial-${process.pid}`;
99
+ fs.copyFileSync(wasmPath, tmp);
100
+ try { fs.renameSync(tmp, target); }
101
+ catch (err) {
102
+ if (err.code === 'EEXIST' || err.code === 'EPERM') {
103
+ try { fs.unlinkSync(target); } catch (_) {}
104
+ fs.renameSync(tmp, target);
105
+ } else {
106
+ try { fs.unlinkSync(tmp); } catch (_) {}
107
+ throw err;
108
+ }
109
+ }
110
+ }
111
+ fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
112
+
113
+ if (opts.wrapperDir) {
114
+ try {
115
+ const srcSha = path.join(opts.wrapperDir, 'plugkit.sha256');
116
+ if (fs.existsSync(srcSha)) fs.copyFileSync(srcSha, path.join(dst, 'plugkit.sha256'));
117
+ } catch (_) {}
118
+ }
119
+ }
120
+
121
+ function resolveCacheWasmPath(root, version, wasmName) {
122
+ const verDir = path.join(root, `v${version}`);
123
+ const wasmPath = path.join(verDir, wasmName || 'plugkit.wasm');
124
+ const okSentinel = path.join(verDir, '.wasm-ok');
125
+ if (fs.existsSync(wasmPath) && fs.existsSync(okSentinel)) return wasmPath;
126
+ return null;
127
+ }
128
+
129
+ function resolveInstalledWasmPath() {
130
+ const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
131
+ const primary = path.join(home, '.gm-tools', 'plugkit.wasm');
132
+ const fallback = path.join(home, '.claude', 'gm-tools', 'plugkit.wasm');
133
+ if (fs.existsSync(primary)) return primary;
134
+ if (fs.existsSync(fallback)) return fallback;
135
+ return primary;
136
+ }
137
+
138
+ async function extractNpmPackageWasm(destPath, version, opts) {
139
+ opts = opts || {};
140
+ const log = opts.log || makeLogger('plugkit-bootstrap');
141
+ const { ensureDir } = require('./bootstrap-shared');
142
+ const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
143
+ try {
144
+ ensureDir(tempDir);
145
+ const startMs = Date.now();
146
+ log(`extracting npm package ${NPM_PACKAGE}@${version} to ${tempDir}`);
147
+ obsEvent('bootstrap', 'npm.extract.start', { package: NPM_PACKAGE, version });
148
+
149
+ fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'plugkit-extract', version: '0.0.0', private: true }));
150
+
151
+ const cmd = resolveWindowsExe('npm');
152
+ const installArgs = ['install', '--no-audit', '--no-fund', '--no-save', NPM_PACKAGE + '@' + version];
153
+ const isCmdShim = process.platform === 'win32' && /\.(cmd|bat)$/i.test(cmd);
154
+ const npmCliJs = isCmdShim ? resolveNpmCliJs(cmd) : null;
155
+
156
+ const spawnCmd = npmCliJs ? process.execPath : (isCmdShim ? `"${cmd}"` : cmd);
157
+ const rawArgs = npmCliJs ? [npmCliJs, ...installArgs] : installArgs;
158
+ const spawnArgs = (isCmdShim && !npmCliJs) ? rawArgs.map(a => /[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a) : rawArgs;
159
+
160
+ const result = spawnSync(spawnCmd, spawnArgs, {
161
+ cwd: tempDir,
162
+ stdio: ['ignore', 'pipe', 'pipe'],
163
+ timeout: ATTEMPT_TIMEOUT_MS,
164
+ encoding: 'utf8',
165
+ windowsHide: true,
166
+ ...((isCmdShim && !npmCliJs) ? { shell: true } : {}),
167
+ });
168
+
169
+ if (result.error) throw result.error;
170
+ if (result.status !== 0) {
171
+ const detail = (result.stderr || result.stdout || '').trim().split(/\r?\n/).slice(-5).join(' | ');
172
+ const sig = result.signal ? ` signal=${result.signal}` : '';
173
+ throw new Error(`npm install failed status=${result.status}${sig}: ${detail || 'no stderr/stdout captured'}`);
174
+ }
175
+
176
+ const nodeModulesPath = path.join(tempDir, 'node_modules', NPM_PACKAGE, 'plugkit.wasm');
177
+ if (!fs.existsSync(nodeModulesPath)) {
178
+ throw new Error(`plugkit.wasm not found in extracted npm package at ${nodeModulesPath}`);
179
+ }
180
+
181
+ fs.copyFileSync(nodeModulesPath, destPath);
182
+ log(`extracted ${nodeModulesPath} -> ${destPath}`);
183
+ obsEvent('bootstrap', 'npm.extract.end', { dur_ms: Date.now() - startMs, ok: true });
184
+ } finally {
185
+ try { fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 }); } catch (_) {}
186
+ }
187
+ }
188
+
189
+ async function extractNpmPackageWithRetry(destPath, version, opts) {
190
+ opts = opts || {};
191
+ const log = opts.log || makeLogger('plugkit-bootstrap');
192
+ let lastErr;
193
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
194
+ try {
195
+ log(`npm extract attempt ${attempt}/${MAX_ATTEMPTS}: ${NPM_PACKAGE}@${version}`);
196
+ await extractNpmPackageWasm(destPath, version, opts);
197
+ return;
198
+ } catch (err) {
199
+ lastErr = err;
200
+ log(`attempt ${attempt} failed: ${err.message}`);
201
+ obsEvent('bootstrap', 'npm.extract.attempt_failed', { package: NPM_PACKAGE, attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
202
+ if (err && (err.code === 'ENOENT' || /ENOENT/.test(String(err.message || '')))) {
203
+ log(`npm binary unresolvable (ENOENT); skipping retries, falling back`);
204
+ throw err;
205
+ }
206
+ if (err && (err.code === 'EINVAL' || /EINVAL/.test(String(err.message || '')))) {
207
+ log(`spawn EINVAL on npm shim; skipping retries, falling back`);
208
+ throw err;
209
+ }
210
+ if (attempt < MAX_ATTEMPTS) {
211
+ const wait = BACKOFF_MS[attempt - 1] || 120000;
212
+ log(`backing off ${wait}ms`);
213
+ await new Promise(r => setTimeout(r, wait));
214
+ }
215
+ }
216
+ }
217
+ throw lastErr;
218
+ }
219
+
220
+ module.exports = {
221
+ NPM_PACKAGE,
222
+ ATTEMPT_TIMEOUT_MS,
223
+ MAX_ATTEMPTS,
224
+ BACKOFF_MS,
225
+ makeLogger,
226
+ resolveProjectRoot,
227
+ writeBootstrapError,
228
+ clearBootstrapError,
229
+ readVersionFile,
230
+ readShaManifest,
231
+ copyWasmToGmTools,
232
+ resolveCacheWasmPath,
233
+ resolveInstalledWasmPath,
234
+ extractNpmPackageWasm,
235
+ extractNpmPackageWithRetry,
236
+ };
@@ -6,8 +6,9 @@ const path = require('path');
6
6
  const os = require('os');
7
7
  const crypto = require('crypto');
8
8
  const { spawn, spawnSync } = require('child_process');
9
- const { pidAlive, sha256OfFile, sha256OfFileSync } = require('./gm-process');
9
+ const { sha256OfFile, sha256OfFileSync } = require('./gm-process');
10
10
  const shared = require('./bootstrap-shared');
11
+ const core = require('./bootstrap-wasm-core');
11
12
  const {
12
13
  obsEvent,
13
14
  cacheRoot,
@@ -16,49 +17,40 @@ const {
16
17
  ensureDir,
17
18
  acquireLock,
18
19
  releaseLock,
19
- isLockStale,
20
20
  pruneOldVersions,
21
21
  healIfShaMatches,
22
22
  daemonVersionSentinel,
23
23
  readDaemonVersion,
24
24
  writeDaemonVersion,
25
- pidCommandLineForKillGuard,
26
- pidIsPlugkitProcess,
27
- writeKillAttribution,
28
- killPid,
29
25
  killSpoolWatcherInCwd,
30
26
  proactiveKillForNewInstall,
31
27
  ensureNextStepWiring: ensureNextStepWiringShared,
32
- resolveWindowsExe,
33
- resolveNpmCliJs,
34
28
  } = shared;
29
+ const {
30
+ resolveProjectRoot,
31
+ writeBootstrapError,
32
+ clearBootstrapError,
33
+ resolveInstalledWasmPath,
34
+ } = core;
35
35
 
36
- const NPM_PACKAGE = 'plugkit-wasm';
37
- const ATTEMPT_TIMEOUT_MS = 10 * 60 * 1000;
38
- const MAX_ATTEMPTS = 3;
39
- const BACKOFF_MS = [5000, 15000];
40
36
  const LOCK_STALE_MS = 30 * 60 * 1000;
41
37
 
42
38
  const wrapperDir = __dirname;
43
39
 
44
- function log(msg) {
45
- try { process.stderr.write(`[gm-plugkit] ${msg}\n`); } catch (_) {}
46
- }
40
+ const log = core.makeLogger('gm-plugkit');
47
41
 
48
- function writeBootstrapError(spec) {
42
+ function copyWasmToGmTools(wasmPath, version) {
43
+ core.copyWasmToGmTools(wasmPath, version);
49
44
  try {
50
- const projectDir = resolveProjectRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
51
- const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
52
- fs.mkdirSync(spoolDir, { recursive: true });
53
- fs.writeFileSync(path.join(spoolDir, '.bootstrap-error.json'), JSON.stringify({ ts: new Date().toISOString(), ...spec }, null, 2));
45
+ const ownPkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
46
+ if (ownPkg && ownPkg.version) {
47
+ fs.writeFileSync(path.join(gmToolsDir(), 'gm-plugkit.version'), ownPkg.version);
48
+ }
54
49
  } catch (_) {}
55
50
  }
56
51
 
57
- function clearBootstrapError() {
58
- try {
59
- const projectDir = resolveProjectRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
60
- fs.unlinkSync(path.join(projectDir, '.gm', 'exec-spool', '.bootstrap-error.json'));
61
- } catch (_) {}
52
+ function extractNpmPackageWithRetry(destPath, version) {
53
+ return core.extractNpmPackageWithRetry(destPath, version, { log });
62
54
  }
63
55
 
64
56
  function sha256Hex(buf) {
@@ -179,45 +171,12 @@ function hasNativeEmbedRunner() {
179
171
  return names.some(n => { try { return fs.existsSync(path.join(dir, n)); } catch (_) { return false; } });
180
172
  }
181
173
 
182
- function resolveProjectRoot(start) {
183
- const resolved = path.resolve(start);
184
- try {
185
- const r = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: resolved, encoding: 'utf-8', windowsHide: true, timeout: 1500 });
186
- if (r.status === 0 && r.stdout && r.stdout.trim()) {
187
- let commonDir = r.stdout.trim();
188
- if (!path.isAbsolute(commonDir)) commonDir = path.resolve(resolved, commonDir);
189
- if (/(^|[\\/])\.git$/.test(commonDir)) return path.dirname(commonDir);
190
- }
191
- } catch (_) {}
192
- return resolved;
193
- }
194
-
195
174
  function readVersionFile() {
196
- const p = path.join(wrapperDir, 'plugkit.version');
197
- if (!fs.existsSync(p)) throw new Error(`plugkit.version not found at ${p}`);
198
- return fs.readFileSync(p, 'utf8').trim();
175
+ return core.readVersionFile(wrapperDir);
199
176
  }
200
177
 
201
178
  function readShaManifest() {
202
- const p = path.join(wrapperDir, 'plugkit.sha256');
203
- if (!fs.existsSync(p)) return null;
204
- const raw = fs.readFileSync(p, 'utf8');
205
- try {
206
- const parsed = JSON.parse(raw);
207
- if (parsed && typeof parsed === 'object') {
208
- const out = {};
209
- for (const [name, sha] of Object.entries(parsed)) {
210
- if (typeof sha === 'string') out[name] = sha.toLowerCase();
211
- }
212
- return out;
213
- }
214
- } catch (_) {}
215
- const out = {};
216
- for (const line of raw.split(/\r?\n/)) {
217
- const m = line.match(/^([0-9a-f]{64})\s+(\S+)\s*$/i);
218
- if (m) out[m[2]] = m[1].toLowerCase();
219
- }
220
- return out;
179
+ return core.readShaManifest(wrapperDir);
221
180
  }
222
181
 
223
182
  async function fetchRemoteSha(version, artifactName) {
@@ -231,54 +190,6 @@ async function fetchRemoteSha(version, artifactName) {
231
190
  }
232
191
  }
233
192
 
234
- async function extractNpmPackageWasm(destPath, version) {
235
- const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
236
- try {
237
- ensureDir(tempDir);
238
- const startMs = Date.now();
239
- log(`extracting npm package ${NPM_PACKAGE}@${version} to ${tempDir}`);
240
- obsEvent('bootstrap', 'npm.extract.start', { package: NPM_PACKAGE, version });
241
-
242
- fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'plugkit-extract', version: '0.0.0', private: true }));
243
-
244
- const cmd = resolveWindowsExe('npm');
245
- const installArgs = ['install', '--no-audit', '--no-fund', '--no-save', NPM_PACKAGE + '@' + version];
246
- const isCmdShim = process.platform === 'win32' && /\.(cmd|bat)$/i.test(cmd);
247
- const npmCliJs = isCmdShim ? resolveNpmCliJs(cmd) : null;
248
-
249
- const spawnCmd = npmCliJs ? process.execPath : (isCmdShim ? `"${cmd}"` : cmd);
250
- const rawArgs = npmCliJs ? [npmCliJs, ...installArgs] : installArgs;
251
- const spawnArgs = (isCmdShim && !npmCliJs) ? rawArgs.map(a => /[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a) : rawArgs;
252
-
253
- const result = spawnSync(spawnCmd, spawnArgs, {
254
- cwd: tempDir,
255
- stdio: ['ignore', 'pipe', 'pipe'],
256
- timeout: ATTEMPT_TIMEOUT_MS,
257
- encoding: 'utf8',
258
- windowsHide: true,
259
- ...((isCmdShim && !npmCliJs) ? { shell: true } : {}),
260
- });
261
-
262
- if (result.error) throw result.error;
263
- if (result.status !== 0) {
264
- const detail = (result.stderr || result.stdout || '').trim().split(/\r?\n/).slice(-5).join(' | ');
265
- const sig = result.signal ? ` signal=${result.signal}` : '';
266
- throw new Error(`npm install failed status=${result.status}${sig}: ${detail || 'no stderr/stdout captured'}`);
267
- }
268
-
269
- const nodeModulesPath = path.join(tempDir, 'node_modules', NPM_PACKAGE, 'plugkit.wasm');
270
- if (!fs.existsSync(nodeModulesPath)) {
271
- throw new Error(`plugkit.wasm not found in extracted npm package at ${nodeModulesPath}`);
272
- }
273
-
274
- fs.copyFileSync(nodeModulesPath, destPath);
275
- log(`extracted ${nodeModulesPath} -> ${destPath}`);
276
- obsEvent('bootstrap', 'npm.extract.end', { dur_ms: Date.now() - startMs, ok: true });
277
- } finally {
278
- try { fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 }); } catch (_) {}
279
- }
280
- }
281
-
282
193
  function httpGetBuffer(url, timeoutMs) {
283
194
  const https = require('https');
284
195
  const idleTimeoutMs = timeoutMs || 30000;
@@ -355,35 +266,6 @@ async function downloadFromGithubReleases(destPath, version, artifactName) {
355
266
  log(`gh-releases wrote ${buf.length} bytes to ${destPath} (artifact=${name})`);
356
267
  }
357
268
 
358
- async function extractNpmPackageWithRetry(destPath, version) {
359
- let lastErr;
360
- for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
361
- try {
362
- log(`npm extract attempt ${attempt}/${MAX_ATTEMPTS}: ${NPM_PACKAGE}@${version}`);
363
- await extractNpmPackageWasm(destPath, version);
364
- return;
365
- } catch (err) {
366
- lastErr = err;
367
- log(`attempt ${attempt} failed: ${err.message}`);
368
- obsEvent('bootstrap', 'npm.extract.attempt_failed', { package: NPM_PACKAGE, attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
369
- if (err && (err.code === 'ENOENT' || /ENOENT/.test(String(err.message || '')))) {
370
- log(`npm binary unresolvable (ENOENT); skipping retries, falling back`);
371
- throw err;
372
- }
373
- if (err && (err.code === 'EINVAL' || /EINVAL/.test(String(err.message || '')))) {
374
- log(`spawn EINVAL on npm shim; skipping retries, falling back`);
375
- throw err;
376
- }
377
- if (attempt < MAX_ATTEMPTS) {
378
- const wait = BACKOFF_MS[attempt - 1] || 120000;
379
- log(`backing off ${wait}ms`);
380
- await new Promise(r => setTimeout(r, wait));
381
- }
382
- }
383
- }
384
- throw lastErr;
385
- }
386
-
387
269
  function killStaleDaemonIfVersionChanged() {
388
270
  let currentVersion;
389
271
  try { currentVersion = readVersionFile(); }
@@ -391,9 +273,9 @@ function killStaleDaemonIfVersionChanged() {
391
273
  obsEvent('bootstrap', 'kill-stale-daemon.version-read-failed', { error: e.message });
392
274
  return;
393
275
  }
394
- const cached = resolveCachedBinary({ version: currentVersion });
395
- if (cached) {
396
- proactiveKillForNewInstall(currentVersion, cached);
276
+ const cached = resolveInstalledWasmPath();
277
+ if (cached && fs.existsSync(cached)) {
278
+ proactiveKillForNewInstall(currentVersion);
397
279
  return;
398
280
  }
399
281
  const recorded = readDaemonVersion();
@@ -553,51 +435,8 @@ async function bootstrap(opts) {
553
435
  }
554
436
  }
555
437
 
556
- function copyWasmToGmTools(wasmPath, version) {
557
- const dst = gmToolsDir();
558
- fs.mkdirSync(dst, { recursive: true });
559
- const target = path.join(dst, 'plugkit.wasm');
560
-
561
- let wasmFresh = false;
562
- if (fs.existsSync(target)) {
563
- try {
564
- const cur = sha256OfFileSync(target);
565
- const src = sha256OfFileSync(wasmPath);
566
- if (cur === src) wasmFresh = true;
567
- } catch (_) {}
568
- }
569
- if (!wasmFresh) {
570
- const tmp = `${target}.partial-${process.pid}`;
571
- fs.copyFileSync(wasmPath, tmp);
572
- try { fs.renameSync(tmp, target); }
573
- catch (err) {
574
- if (err.code === 'EEXIST' || err.code === 'EPERM') {
575
- try { fs.unlinkSync(target); } catch (_) {}
576
- fs.renameSync(tmp, target);
577
- } else {
578
- try { fs.unlinkSync(tmp); } catch (_) {}
579
- throw err;
580
- }
581
- }
582
- }
583
- fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
584
-
585
- try {
586
- const ownPkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
587
- if (ownPkg && ownPkg.version) {
588
- fs.writeFileSync(path.join(dst, 'gm-plugkit.version'), ownPkg.version);
589
- }
590
- } catch (_) {}
591
-
592
- }
593
-
594
438
  function getWasmPath() {
595
- const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
596
- const primary = path.join(home, '.gm-tools', 'plugkit.wasm');
597
- const fallback = path.join(home, '.claude', 'gm-tools', 'plugkit.wasm');
598
- if (fs.existsSync(primary)) return primary;
599
- if (fs.existsSync(fallback)) return fallback;
600
- return primary;
439
+ return resolveInstalledWasmPath();
601
440
  }
602
441
 
603
442
  function isReady() {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.2208",
3
+ "version": "2.0.2210",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform wasm, verifies SHA256, and launches agentplug-runner (the native wasm host) as the spool watcher daemon.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1 +1 @@
1
- 4785c38fbedab482430d5925eebf3eb687b5cc4607dd2d8442de89feb20c532a plugkit-slim.wasm
1
+ ffa850254a06a6a16e190b88a64dd960c369aae0a2ba2e985edc7a72ff11e779 plugkit-slim.wasm
@@ -1 +1 @@
1
- 0.1.1048
1
+ 0.1.1049
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.2208",
3
+ "version": "2.0.2210",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -17,5 +17,5 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
- "plugkitVersion": "0.1.1048"
20
+ "plugkitVersion": "0.1.1049"
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.2208",
3
+ "version": "2.0.2210",
4
4
  "description": "Canonical universal harness — AI-native software engineering via skill-driven orchestration; bootstraps plugkit for task execution and session isolation. Install in any AI coding agent host.",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -38,6 +38,8 @@
38
38
  "bin/plugkit.wasm.sha256",
39
39
  "bin/plugkit-slim.wasm.sha256",
40
40
  "gm-plugkit/bootstrap.js",
41
+ "gm-plugkit/bootstrap-wasm-core.js",
42
+ "gm-plugkit/bootstrap-shared.js",
41
43
  "gm-plugkit/cli.js",
42
44
  "gm-plugkit/index.js",
43
45
  "gm-plugkit/package.json",