klypix-mcp 1.40.0 → 1.40.2

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.
@@ -220,7 +220,7 @@ const flatten = (code) => code
220
220
  .replace(/\.\.\/src\/klypix-(core|format)\.mjs/g, './klypix-$1.mjs')
221
221
  // brain-doctor + agent-rules (the server's lazy `import('../src/brain-doctor.mjs')`
222
222
  // for the brain_doctor tool) → flat sibling refs in the runtime layout.
223
- .replace(/\.\.\/src\/(brain-doctor|agent-rules|mcp-presence|mcp-supervisor)\.mjs/g, './$1.mjs')
223
+ .replace(/\.\.\/src\/(brain-doctor|agent-rules|mcp-presence|mcp-supervisor|mcp-auto-update)\.mjs/g, './$1.mjs')
224
224
  .replace(/klypix-worker\.mjs/g, 'klypix-mcp-worker.mjs')
225
225
  .replace(/const PKG_VERSION = \(\(\) => \{[\s\S]*?\}\)\(\);/, `const PKG_VERSION = '${VERSION}'; // baked at install (flat layout has no package.json)`);
226
226
 
@@ -32,6 +32,7 @@ import {
32
32
  } from '../src/klypix-core.mjs';
33
33
  import { mcpServerEntry } from '../src/agent-rules.mjs';
34
34
  import { createMcpPresence, KLYPIX_MCP_INSTRUCTIONS } from '../src/mcp-presence.mjs';
35
+ import { spawnAutoUpdateHelper } from '../src/mcp-auto-update.mjs';
35
36
 
36
37
  // Real package version for the MCP handshake (was hardcoded '1.0.0', which
37
38
  // misled every client/version diagnosis — it could never reflect the true release).
@@ -494,12 +495,18 @@ if (!canvasViewAsApp) {
494
495
 
495
496
  const transport = new StdioServerTransport();
496
497
  let runningHeartbeat = null;
498
+ let autoUpdateStarter = null;
499
+ let autoUpdatePoller = null;
497
500
  let runtimeStopped = false;
498
501
  const stopRuntimePresence = () => {
499
502
  if (runtimeStopped) return;
500
503
  runtimeStopped = true;
501
504
  if (runningHeartbeat) clearInterval(runningHeartbeat);
502
505
  runningHeartbeat = null;
506
+ if (autoUpdateStarter) clearTimeout(autoUpdateStarter);
507
+ if (autoUpdatePoller) clearInterval(autoUpdatePoller);
508
+ autoUpdateStarter = null;
509
+ autoUpdatePoller = null;
503
510
  recordRunningServer({ remove: true });
504
511
  mcpPresence.stop();
505
512
  };
@@ -508,6 +515,22 @@ server.server.oninitialized = () => {
508
515
  recordRunningServer();
509
516
  runningHeartbeat = setInterval(() => recordRunningServer(), 30_000);
510
517
  runningHeartbeat.unref?.();
518
+ // The worker mirrors the supervisor's host-neutral scheduler. This lets an
519
+ // older stable supervisor acquire the updater immediately after hot-swapping
520
+ // to a compatible new worker; no extra host reconnect is needed for the
521
+ // scheduler itself. Stamp + lock make the duplicate trigger effectively free.
522
+ const autoUpdateDir = path.dirname(
523
+ process.env.KLYPIX_MCP_RUNTIME_MANIFEST
524
+ || path.join(os.homedir(), '.claude', 'project-brain', '.mcp-runtime.json'),
525
+ );
526
+ const checkForCoreUpdate = () => spawnAutoUpdateHelper({
527
+ brainDir: autoUpdateDir,
528
+ currentVersion: PKG_VERSION,
529
+ });
530
+ autoUpdateStarter = setTimeout(checkForCoreUpdate, 2000);
531
+ autoUpdateStarter.unref?.();
532
+ autoUpdatePoller = setInterval(checkForCoreUpdate, 60 * 60 * 1000);
533
+ autoUpdatePoller.unref?.();
511
534
  log(`ready · vault=${VAULT} · presence=mcp`);
512
535
  };
513
536
  server.server.onclose = stopRuntimePresence;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.40.0",
3
+ "version": "1.40.2",
4
4
  "description": "Every project gets a brain — one open .klypix file your AI agents read, write, and argue from, over MCP. Works with Claude, Codex, Cursor, Cline, any model.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -21,6 +21,7 @@ import path from 'path';
21
21
  import crypto from 'crypto';
22
22
  import https from 'https';
23
23
  import { spawn } from 'child_process';
24
+ import { fileURLToPath } from 'url';
24
25
 
25
26
  export const AUTO_UPDATE_TTL_MS = 24 * 60 * 60 * 1000;
26
27
  export const AUTO_UPDATE_LOCK_STALE_MS = 30 * 60 * 1000;
@@ -106,6 +107,44 @@ export function inspectAutoUpdate(brainDir, { now = Date.now(), env = process.en
106
107
  };
107
108
  }
108
109
 
110
+ /**
111
+ * Start the detached updater when this machine is due.
112
+ *
113
+ * Safe to call from both the stable supervisor and the replaceable worker:
114
+ * the shared stamp prevents unnecessary children and the helper lock collapses
115
+ * the remaining cross-process race.
116
+ */
117
+ export function spawnAutoUpdateHelper({
118
+ brainDir = path.join(os.homedir(), '.claude', 'project-brain'),
119
+ currentVersion = null,
120
+ env = process.env,
121
+ spawnProcess = spawn,
122
+ } = {}) {
123
+ if (!autoUpdateEnabled(env) || env.KLYPIX_MCP_AUTO_UPDATE_CHILD === '1') return null;
124
+ if (!inspectAutoUpdate(brainDir, { env }).due) return null;
125
+ try {
126
+ const helper = fileURLToPath(import.meta.url);
127
+ const child = spawnProcess(process.execPath, [helper, AUTO_UPDATE_WORKER_ARG], {
128
+ // Do not hold the managed directory as this detached process's cwd.
129
+ // This matters for ephemeral/test homes on Windows and is cleaner for
130
+ // uninstallers; the installer receives its exact target through env.
131
+ cwd: os.tmpdir(),
132
+ env: {
133
+ ...env,
134
+ KLYPIX_MCP_AUTO_UPDATE_DIR: path.resolve(brainDir),
135
+ KLYPIX_MCP_AUTO_UPDATE_CURRENT: String(currentVersion || ''),
136
+ KLYPIX_MCP_AUTO_UPDATE_CHILD: '1',
137
+ },
138
+ detached: true,
139
+ stdio: 'ignore',
140
+ windowsHide: true,
141
+ });
142
+ child.on('error', () => { /* fail-open: the MCP transport remains healthy */ });
143
+ child.unref();
144
+ return child;
145
+ } catch { return null; }
146
+ }
147
+
109
148
  function acquireLock(lockFile, now, staleMs = AUTO_UPDATE_LOCK_STALE_MS) {
110
149
  fs.mkdirSync(path.dirname(lockFile), { recursive: true });
111
150
  const token = `${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
@@ -190,7 +229,25 @@ export function installExactRuntime(version, {
190
229
  };
191
230
  let child;
192
231
  try {
193
- child = spawnProcess('npx', ['-y', `klypix-mcp@${version}`, 'install', '--runtime-only'], {
232
+ let command = 'npx';
233
+ let args = ['-y', `klypix-mcp@${version}`, 'install', '--runtime-only'];
234
+ if (process.platform === 'win32') {
235
+ // .cmd files require a shell on Windows, but Node 24 correctly warns
236
+ // that shell:true concatenates arguments. Invoke npm's JS entry with
237
+ // this exact Node binary instead: no quoting ambiguity, no shell, and
238
+ // the strict-semver gate above leaves no command-injection surface.
239
+ const npxCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npx-cli.js');
240
+ if (fs.existsSync(npxCli)) {
241
+ command = process.execPath;
242
+ args = [npxCli, ...args];
243
+ } else {
244
+ // Portable fallback for unusual Windows Node layouts. The only
245
+ // interpolated value is strict x.y.z semver.
246
+ command = process.env.ComSpec || 'cmd.exe';
247
+ args = ['/d', '/s', '/c', `npx -y klypix-mcp@${version} install --runtime-only`];
248
+ }
249
+ }
250
+ child = spawnProcess(command, args, {
194
251
  cwd: brainDir,
195
252
  env: {
196
253
  ...process.env,
@@ -198,7 +255,7 @@ export function installExactRuntime(version, {
198
255
  KLYPIX_MCP_AUTO_UPDATE_CHILD: '1',
199
256
  },
200
257
  stdio: 'ignore',
201
- shell: process.platform === 'win32',
258
+ shell: false,
202
259
  windowsHide: true,
203
260
  });
204
261
  } catch (error) {
@@ -21,11 +21,10 @@ import os from 'os';
21
21
  import path from 'path';
22
22
  import crypto from 'crypto';
23
23
  import { spawn } from 'child_process';
24
- import { fileURLToPath } from 'url';
25
24
  import {
26
- AUTO_UPDATE_WORKER_ARG,
27
25
  autoUpdateEnabled,
28
26
  inspectAutoUpdate,
27
+ spawnAutoUpdateHelper,
29
28
  } from './mcp-auto-update.mjs';
30
29
 
31
30
  const INTERNAL_PREFIX = '__klypix_supervisor__';
@@ -705,29 +704,10 @@ class Supervisor {
705
704
 
706
705
  scheduleAutoUpdate() {
707
706
  if (this.closed || !this.autoUpdate || process.env.KLYPIX_MCP_AUTO_UPDATE_CHILD === '1') return;
708
- const brainDir = path.dirname(this.runtimeManifest);
709
- const status = inspectAutoUpdate(brainDir);
710
- if (!status.due) return;
711
- try {
712
- const helper = fileURLToPath(new URL('./mcp-auto-update.mjs', import.meta.url));
713
- const child = spawn(process.execPath, [helper, AUTO_UPDATE_WORKER_ARG], {
714
- // Do not hold the managed directory as this detached process's cwd.
715
- // This matters for ephemeral/test homes on Windows and is cleaner for
716
- // uninstallers; the installer receives its exact target through env.
717
- cwd: os.tmpdir(),
718
- env: {
719
- ...process.env,
720
- KLYPIX_MCP_AUTO_UPDATE_DIR: brainDir,
721
- KLYPIX_MCP_AUTO_UPDATE_CURRENT: String(this.active?.version || this.fallbackTarget.version || ''),
722
- KLYPIX_MCP_AUTO_UPDATE_CHILD: '1',
723
- },
724
- detached: true,
725
- stdio: 'ignore',
726
- windowsHide: true,
727
- });
728
- child.on('error', () => { /* fail-open: the MCP transport remains healthy */ });
729
- child.unref();
730
- } catch { /* update discovery must never affect MCP */ }
707
+ spawnAutoUpdateHelper({
708
+ brainDir: path.dirname(this.runtimeManifest),
709
+ currentVersion: this.active?.version || this.fallbackTarget.version,
710
+ });
731
711
  }
732
712
 
733
713
  flushHostQueue() {