gm-qwen 2.0.981 → 2.0.982

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/bin/bootstrap.js CHANGED
@@ -99,6 +99,42 @@ function fallbackCacheRoot() {
99
99
  return path.join(os.tmpdir(), 'plugkit-cache', 'bin');
100
100
  }
101
101
 
102
+ function gmToolsDir() {
103
+ const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
104
+ return path.join(home, '.claude', 'gm-tools');
105
+ }
106
+
107
+ // Copy the freshly-resolved plugkit binary + its version+sha manifests to
108
+ // ~/.claude/gm-tools so hooks.json can invoke plugkit directly without going
109
+ // through node. Self-update inside the Rust binary keeps gm-tools fresh from
110
+ // here on. Skipped silently on any error — the next session-start hook will
111
+ // retry via ensure_tools_current.
112
+ function copyToGmTools(finalPath, wrapperDir, version) {
113
+ try {
114
+ const dst = gmToolsDir();
115
+ fs.mkdirSync(dst, { recursive: true });
116
+ const exeName = process.platform === 'win32' ? 'plugkit.exe' : 'plugkit';
117
+ const target = path.join(dst, exeName);
118
+ const targetTmp = target + '.new';
119
+ fs.copyFileSync(finalPath, targetTmp);
120
+ try { fs.renameSync(targetTmp, target); }
121
+ catch (err) {
122
+ if (err.code === 'EEXIST' || err.code === 'EPERM' || err.code === 'EBUSY') {
123
+ // target may be locked by a running plugkit; the .new file persists
124
+ // and the in-Rust self-update will eventually swap it. Leave it.
125
+ } else { throw err; }
126
+ }
127
+ if (process.platform !== 'win32') {
128
+ try { fs.chmodSync(target, 0o755); } catch (_) {}
129
+ }
130
+ fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
131
+ try {
132
+ const srcSha = path.join(wrapperDir, 'plugkit.sha256');
133
+ if (fs.existsSync(srcSha)) fs.copyFileSync(srcSha, path.join(dst, 'plugkit.sha256'));
134
+ } catch (_) {}
135
+ } catch (_) {}
136
+ }
137
+
102
138
  function ensureDir(dir) {
103
139
  fs.mkdirSync(dir, { recursive: true });
104
140
  }
@@ -349,6 +385,7 @@ async function bootstrap(opts) {
349
385
  const actualSha = sha256OfFileSync(finalPath);
350
386
  if (actualSha === expectedSha) {
351
387
  if (!opts.silent) log(`decision: hit reason: sha-match v${version} (${finalPath})`);
388
+ copyToGmTools(finalPath, wrapperDir, version);
352
389
  return finalPath;
353
390
  }
354
391
  log(`decision: fetch reason: cache-hit-sha-mismatch (dir=v${version} expected ${expectedSha.slice(0,12)}… got ${(actualSha||'').slice(0,12)}…)`);
@@ -362,6 +399,7 @@ async function bootstrap(opts) {
362
399
  try { fs.unlinkSync(okSentinel); } catch (_) {}
363
400
  } else {
364
401
  if (!opts.silent) log(`decision: hit reason: sentinel+no-sha-manifest (${finalPath})`);
402
+ copyToGmTools(finalPath, wrapperDir, version);
365
403
  return finalPath;
366
404
  }
367
405
  }
@@ -369,6 +407,7 @@ async function bootstrap(opts) {
369
407
  if (healIfShaMatches(finalPath, expectedSha, okSentinel, partialPath, 'plugkit')) {
370
408
  if (!opts.silent) log(`decision: heal reason: sha-match (${finalPath})`);
371
409
  spawnDetachedRtkFetch(wrapperDir);
410
+ copyToGmTools(finalPath, wrapperDir, version);
372
411
  return finalPath;
373
412
  }
374
413
 
@@ -377,11 +416,13 @@ async function bootstrap(opts) {
377
416
  try {
378
417
  if (fs.existsSync(finalPath) && fs.existsSync(okSentinel)) {
379
418
  log(`decision: hit reason: lock-race-resolved (${finalPath})`);
419
+ copyToGmTools(finalPath, wrapperDir, version);
380
420
  return finalPath;
381
421
  }
382
422
  if (healIfShaMatches(finalPath, expectedSha, okSentinel, partialPath, 'plugkit')) {
383
423
  log(`decision: heal reason: sha-match-under-lock (${finalPath})`);
384
424
  spawnDetachedRtkFetch(wrapperDir);
425
+ copyToGmTools(finalPath, wrapperDir, version);
385
426
  return finalPath;
386
427
  }
387
428
 
@@ -442,6 +483,7 @@ async function bootstrap(opts) {
442
483
  proactiveKillForNewInstall(version, finalPath);
443
484
  pruneOldVersions(root, version, readRtkVersion(wrapperDir));
444
485
  spawnDetachedRtkFetch(wrapperDir);
486
+ copyToGmTools(finalPath, wrapperDir, version);
445
487
  return finalPath;
446
488
  } finally {
447
489
  releaseLock(lockPath);
package/bin/plugkit.js CHANGED
@@ -1,159 +1,50 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- const { spawn, spawnSync } = require('child_process');
3
+ // Minimal exec wrapper. ZERO bootstrap, ZERO version-probe, ZERO async work.
4
+ // Just shell out to ~/.claude/gm-tools/plugkit{.exe} with inherited stdio.
5
+ // The Rust binary handles its own self-update at startup (detached); first-time
6
+ // bootstrap is done by gm-cc postinstall.js. Hot path is one spawnSync, ~150ms
7
+ // of node startup overhead.
8
+
9
+ const { spawnSync } = require('child_process');
4
10
  const path = require('path');
5
11
  const fs = require('fs');
6
- const { bootstrap, resolveCachedBinary, resolveCachedRtk, obsEvent, killStaleDaemonIfVersionChanged } = require('./bootstrap');
7
-
8
- const dir = __dirname;
9
-
10
- function readPinnedVersion() {
11
- try { return fs.readFileSync(path.join(dir, 'plugkit.version'), 'utf8').trim(); } catch (_) { return null; }
12
- }
12
+ const os = require('os');
13
13
 
14
- function probeCachedVersion(binPath) {
15
- try {
16
- const r = spawnSync(binPath, ['--version'], { timeout: 3000, encoding: 'utf8', windowsHide: true });
17
- if (r.error) return null;
18
- const text = `${r.stdout || ''} ${r.stderr || ''}`.trim();
19
- const m = text.match(/(\d+\.\d+\.\d+)/);
20
- return m ? m[1] : null;
21
- } catch (_) { return null; }
14
+ function toolsBin() {
15
+ const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
16
+ const exe = process.platform === 'win32' ? 'plugkit.exe' : 'plugkit';
17
+ return path.join(home, '.claude', 'gm-tools', exe);
22
18
  }
23
19
 
24
- async function resolveBinaryWithPinCheck() {
25
- const cached = resolveCachedBinary({ wrapperDir: dir });
26
- if (!cached) return null;
27
- const pin = readPinnedVersion();
28
- if (!pin) return cached;
29
- const got = probeCachedVersion(cached);
30
- if (got && got === pin) return cached;
31
- try { return await bootstrap({ wrapperDir: dir, silent: true }); } catch (_) { return cached; }
32
- }
33
-
34
- function envWithRtkOnPath() {
35
- const rtkPath = resolveCachedRtk({ wrapperDir: dir });
36
- if (!rtkPath) return process.env;
37
- const rtkDir = path.dirname(rtkPath);
38
- const sep = process.platform === 'win32' ? ';' : ':';
39
- return { ...process.env, PATH: `${rtkDir}${sep}${process.env.PATH || ''}` };
40
- }
41
-
42
- async function resolveBinary() {
43
- const cached = resolveCachedBinary({ wrapperDir: dir });
44
- if (cached) return cached;
45
- return await bootstrap({ wrapperDir: dir });
20
+ function legacyBesideWrapper() {
21
+ const dir = __dirname;
22
+ const p = os.platform();
23
+ const a = os.arch();
24
+ let candidates = [];
25
+ if (p === 'win32') candidates = [path.join(dir, a === 'arm64' ? 'plugkit-win32-arm64.exe' : 'plugkit-win32-x64.exe')];
26
+ else if (p === 'darwin') candidates = [path.join(dir, a === 'arm64' ? 'plugkit-darwin-arm64' : 'plugkit-darwin-x64')];
27
+ else candidates = [path.join(dir, (a === 'arm64' || a === 'aarch64') ? 'plugkit-linux-arm64' : 'plugkit-linux-x64')];
28
+ for (const c of candidates) if (fs.existsSync(c)) return c;
29
+ return null;
46
30
  }
47
31
 
48
- async function main() {
32
+ function main() {
49
33
  const args = process.argv.slice(2);
50
- // Detached bootstrap entry: just run bootstrap() and exit. Used by session-start
51
- // to avoid blocking CC startup on a slow GitHub download.
52
- if (args[0] === '__rtk_only__') {
53
- try { await bootstrap({ wrapperDir: dir, silent: false }); }
54
- catch (e) { try { process.stderr.write(`[plugkit-bootstrap-detached] ${e.message}\n`); } catch (_) {} }
55
- process.exit(0);
56
- }
57
34
  const isHook = args[0] === 'hook';
58
- const startedAt = Date.now();
59
- obsEvent('plugkit_wrapper', 'invoke', { argv: args.slice(0, 4), is_hook: isHook });
60
- // If the plugin tarball updated `plugkit.version` since the runner daemon
61
- // was last started, kill the daemon so the next `runner start` picks up
62
- // the freshly-installed binary instead of serving stale RPCs.
63
- try { killStaleDaemonIfVersionChanged(dir); } catch (_) {}
64
- let bin;
65
- try {
66
- const hookSubcmd = isHook ? (args[1] || '') : '';
67
- // session-start ALWAYS bootstraps: this is the once-per-session moment
68
- // where we guarantee the cached binary matches the wrapper-pinned version.
69
- // If the bootstrap fails (offline) we fall through to whatever the cache
70
- // currently has — the hook itself isn't blocking, just refreshing.
71
- if (isHook && hookSubcmd === 'session-start') {
72
- obsEvent('plugkit_wrapper', 'hook_bootstrap_session_start', { argv: args.slice(0, 4) });
73
- // Bootstrap can stall 60s+ on a slow GitHub mirror — never block CC startup
74
- // on it. Detach into a background child; this hook returns immediately
75
- // with whatever cached binary exists. Subsequent hooks pick up the new
76
- // binary once the detached bootstrap completes.
77
- bin = resolveCachedBinary({ wrapperDir: dir }) || legacyFallback();
78
- try {
79
- const child = spawn(process.execPath, [__filename, '__rtk_only__'], {
80
- detached: true,
81
- stdio: 'ignore',
82
- windowsHide: true,
83
- env: { ...process.env, PLUGKIT_BOOTSTRAP_DETACHED: '1' },
84
- });
85
- child.unref();
86
- obsEvent('plugkit_wrapper', 'session_start_bootstrap_detached', { pid: child.pid });
87
- } catch (e) {
88
- process.stderr.write(`[plugkit] detached bootstrap spawn failed: ${e.message}\n`);
89
- }
90
- // If no cached binary yet (fresh install with bootstrap still downloading),
91
- // skip running the session-start handler this turn — it will fire next
92
- // session-start once binary is in place.
93
- if (!bin) {
94
- process.stderr.write(`[plugkit] session-start skipped: binary not yet installed (bootstrap running in background).\n`);
95
- process.exit(0);
96
- }
97
- } else if (isHook) {
98
- bin = (await resolveBinaryWithPinCheck()) || legacyFallback();
99
- if (!bin) {
100
- process.stderr.write(`[plugkit] hook ${hookSubcmd} skipped: binary not yet installed. Bootstrap will run on session-start.\n`);
101
- obsEvent('plugkit_wrapper', 'hook_skip_uncached', { argv: args.slice(0, 4), dur_ms: Date.now() - startedAt });
102
- process.exit(0);
103
- }
104
- } else {
105
- bin = await resolveBinary();
35
+ let bin = toolsBin();
36
+ if (!fs.existsSync(bin)) {
37
+ bin = legacyBesideWrapper();
38
+ if (!bin) {
39
+ // Binary not yet installed. If this is a hook, exit cleanly so CC doesn't
40
+ // see an error; postinstall will populate gm-tools on /plugin install.
41
+ if (isHook) process.exit(0);
42
+ process.stderr.write('[plugkit] binary not found at ~/.claude/gm-tools/plugkit — run postinstall\n');
43
+ process.exit(1);
106
44
  }
107
- } catch (err) {
108
- process.stderr.write(`[plugkit] bootstrap failed: ${err.message}\n`);
109
- obsEvent('plugkit_wrapper', 'bootstrap_failed', { err: err.message, dur_ms: Date.now() - startedAt, argv: args.slice(0, 4), is_hook: isHook });
110
- const legacy = legacyFallback();
111
- if (legacy) { bin = legacy; }
112
- else if (isHook) { process.exit(0); }
113
- else process.exit(1);
114
- }
115
-
116
- const env = envWithRtkOnPath();
117
-
118
- if (isHook && !process.stdin.isTTY) {
119
- const chunks = [];
120
- process.stdin.on('data', c => chunks.push(c));
121
- process.stdin.on('end', () => {
122
- const child = spawn(bin, args, { stdio: ['pipe', 'inherit', 'inherit'], windowsHide: true, env });
123
- child.stdin.end(Buffer.concat(chunks));
124
- child.on('close', code => process.exit(code ?? 1));
125
- child.on('error', () => process.exit(1));
126
- });
127
- process.stdin.on('error', () => process.exit(1));
128
- } else {
129
- const result = spawnSync(bin, args, { stdio: 'inherit', windowsHide: true, env });
130
- obsEvent('plugkit_wrapper', 'exit', { dur_ms: Date.now() - startedAt, code: result.status ?? -1 });
131
- process.exit(result.status ?? 1);
132
- }
133
- }
134
-
135
- // legacyFallback only returns a binary that lives next to the wrapper. We
136
- // never reach across to ~/.claude/gm-tools/plugkit.exe or other ambient
137
- // install dirs — those have proven to mask bootstrap failures by serving a
138
- // stale version whose hooks silently mismatch the active wrapper code (see
139
- // the v0.1.292-vs-v0.1.294 incident).
140
- function legacyFallback() {
141
- const os = require('os');
142
- const p = os.platform();
143
- const a = os.arch();
144
- let candidates = [];
145
- if (p === 'win32') {
146
- candidates = [path.join(dir, a === 'arm64' ? 'plugkit-win32-arm64.exe' : 'plugkit-win32-x64.exe')];
147
- } else if (p === 'darwin') {
148
- candidates = [path.join(dir, a === 'arm64' ? 'plugkit-darwin-arm64' : 'plugkit-darwin-x64')];
149
- } else {
150
- candidates = [path.join(dir, (a === 'arm64' || a === 'aarch64') ? 'plugkit-linux-arm64' : 'plugkit-linux-x64')];
151
45
  }
152
- for (const c of candidates) if (fs.existsSync(c)) return c;
153
- return null;
46
+ const r = spawnSync(bin, args, { stdio: 'inherit', windowsHide: true });
47
+ process.exit(r.status ?? 1);
154
48
  }
155
49
 
156
- main().catch(err => {
157
- process.stderr.write(`[plugkit] fatal: ${err.message}\n`);
158
- process.exit(1);
159
- });
50
+ main();
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.981",
3
+ "version": "2.0.982",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-qwen",
3
- "version": "2.0.981",
3
+ "version": "2.0.982",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",