claude-code-session-manager 0.104.2 → 0.105.0

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.
Files changed (77) hide show
  1. package/README.md +15 -3
  2. package/bin/cli.cjs +0 -5
  3. package/dist/assets/{AgentLibrary-QPw9gC_o.js → AgentLibrary-BDtr4oI-.js} +1 -1
  4. package/dist/assets/{Button-8RAbgh1d.js → Button-CuXBCQDp.js} +1 -1
  5. package/dist/assets/{DataModel-BEkuOZuS.js → DataModel-DmvRipT5.js} +1 -1
  6. package/dist/assets/{History-BLrNSazM.js → History-B-v9pwwr.js} +1 -1
  7. package/dist/assets/{Hooks-C8P6wQq1.js → Hooks-S_0oHHlr.js} +1 -1
  8. package/dist/assets/{Library-CWFvBZw-.js → Library-BcbGJxQ3.js} +1 -1
  9. package/dist/assets/{ListDetail-B1jVMgzN.js → ListDetail-DtqHqJHt.js} +1 -1
  10. package/dist/assets/{MarkdownEditor-JrnUbWjW.js → MarkdownEditor-B56sPp_l.js} +1 -1
  11. package/dist/assets/{McpServers-BGqTxDBm.js → McpServers-D5VnmxAn.js} +1 -1
  12. package/dist/assets/{Memory-h8bhRNyI.js → Memory-0yGZLyRP.js} +1 -1
  13. package/dist/assets/{Panel-srf0FT0m.js → Panel-NoAADJwx.js} +1 -1
  14. package/dist/assets/{Permissions-BA4oWydI.js → Permissions-D2Dh-ee8.js} +1 -1
  15. package/dist/assets/{Plugins-DTuTQqUL.js → Plugins-kGoUgS9b.js} +2 -2
  16. package/dist/assets/{ProvenanceBadge-BW18EquH.js → ProvenanceBadge-CHwDFTX7.js} +1 -1
  17. package/dist/assets/{SaveBar-Cc6UukFa.js → SaveBar-BtKNMIYK.js} +1 -1
  18. package/dist/assets/{Scheduler-BctPlNL1.js → Scheduler-B0_kMMDs.js} +1 -1
  19. package/dist/assets/{ScopeSwitcher-DV1OnSuc.js → ScopeSwitcher-BusohWZ5.js} +1 -1
  20. package/dist/assets/{Settings-Bpyt1RGa.js → Settings-DuRJntaE.js} +1 -1
  21. package/dist/assets/{SkillReferenceGraph-B28AYo12.js → SkillReferenceGraph-BHOdIpsy.js} +1 -1
  22. package/dist/assets/{Skills-zXLI5OB_.js → Skills-DSTLZimI.js} +1 -1
  23. package/dist/assets/{SystemPrompt-DKoN0UAV.js → SystemPrompt-BG7wFWdb.js} +1 -1
  24. package/dist/assets/{TagLibrary-BkDOa8IM.js → TagLibrary-Bx27aE3_.js} +1 -1
  25. package/dist/assets/{TiptapBody-CIY1f6mI.js → TiptapBody-j0O8J7YI.js} +1 -1
  26. package/dist/assets/{Toggle-C139wuAK.js → Toggle-DzVZ6rC7.js} +1 -1
  27. package/dist/assets/{ViewTabs-CUEfsMOe.js → ViewTabs-BizjuynT.js} +1 -1
  28. package/dist/assets/{index-D89EUizB.css → index-BLVzrc4k.css} +1 -1
  29. package/dist/assets/{index-DrvxIbmR.js → index-CLIaYljt.js} +416 -416
  30. package/dist/assets/{settingsSchema-CAFckLz6.js → settingsSchema-7MRAxaJk.js} +1 -1
  31. package/dist/assets/useAgentsDirWatch-BUwfU2Kq.js +1 -0
  32. package/dist/assets/{useConfigDirWatch-QKxfZx9X.js → useConfigDirWatch-ot0VYwQC.js} +1 -1
  33. package/dist/index.html +2 -2
  34. package/package.json +10 -12
  35. package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +12 -0
  36. package/plugins/session-manager-dev/skills/develop/SKILL.md +4 -2
  37. package/scripts/scheduler-mcp-server.cjs +7 -17
  38. package/src/main/build-info.json +4 -4
  39. package/src/main/chatRunner.cjs +4 -4
  40. package/src/main/historyAggregator.cjs +7 -1
  41. package/src/main/index.cjs +34 -1
  42. package/src/main/lib/agentPersonaSchema.cjs +8 -1
  43. package/src/main/lib/appRuntime.cjs +57 -0
  44. package/src/main/lib/childWithLog.cjs +10 -10
  45. package/src/main/lib/claudeBin.cjs +40 -16
  46. package/src/main/lib/cleanEnv.cjs +29 -6
  47. package/src/main/lib/defaultShell.cjs +36 -0
  48. package/src/main/lib/definitionOfDone.cjs +6 -2
  49. package/src/main/lib/delegationReadiness.cjs +13 -5
  50. package/src/main/lib/gitWorktree.cjs +6 -3
  51. package/src/main/lib/insideHome.cjs +15 -4
  52. package/src/main/lib/killTree.cjs +80 -0
  53. package/src/main/lib/loadGate.cjs +16 -0
  54. package/src/main/lib/loginShellPath.cjs +82 -0
  55. package/src/main/lib/machineProfile.cjs +4 -2
  56. package/src/main/lib/mcpStdioServer.cjs +127 -0
  57. package/src/main/lib/openExternalApp.cjs +62 -11
  58. package/src/main/lib/otlpTraceExporter.cjs +147 -0
  59. package/src/main/lib/prereqs.cjs +145 -0
  60. package/src/main/lib/procIdentity.cjs +37 -2
  61. package/src/main/lib/rcaReport.cjs +1 -0
  62. package/src/main/lib/reaperHelpers.cjs +5 -2
  63. package/src/main/lib/shippedPersonaSeeds.cjs +10 -0
  64. package/src/main/lib/timeoutShimScript.cjs +14 -2
  65. package/src/main/lib/updateCheck.cjs +41 -11
  66. package/src/main/lib/watchdogHelpers.cjs +23 -3
  67. package/src/main/lib/winSpawn.cjs +67 -0
  68. package/src/main/otel.cjs +18 -62
  69. package/src/main/pty.cjs +32 -11
  70. package/src/main/runVerify.cjs +42 -2
  71. package/src/main/scheduler.cjs +105 -20
  72. package/src/main/seedSchedulerMcp.cjs +11 -2
  73. package/src/preload/api.d.ts +14 -0
  74. package/src/preload/index.cjs +2 -0
  75. package/src/seed/agents/validator.md +2 -2
  76. package/dist/assets/useAgentsDirWatch-XhQEVlaK.js +0 -1
  77. package/scripts/postinstall.cjs +0 -52
@@ -11,8 +11,21 @@ const SECRET_KEY_RE = /^(?:.*_)?(TOKEN|API_?KEY|SECRET|PASSWORD|AUTHORIZATION|CO
11
11
  * so pty.cjs and pluginInstall.cjs can't drift (one of them was missing
12
12
  * /opt/homebrew and broke plugin install on Apple Silicon).
13
13
  */
14
- function userBinDirs() {
15
- const home = os.homedir();
14
+ function userBinDirs(opts) {
15
+ const platform = (opts && opts.platform) || process.platform;
16
+ if (platform === 'win32') {
17
+ const env = (opts && opts.env) || process.env;
18
+ const w = path.win32;
19
+ const dirs = [];
20
+ if (env.USERPROFILE) dirs.push(w.join(env.USERPROFILE, '.local', 'bin'));
21
+ if (env.APPDATA) dirs.push(w.join(env.APPDATA, 'npm'));
22
+ if (env.ProgramFiles) {
23
+ dirs.push(w.join(env.ProgramFiles, 'Git', 'cmd'));
24
+ dirs.push(w.join(env.ProgramFiles, 'nodejs'));
25
+ }
26
+ return dirs;
27
+ }
28
+ const home = (opts && opts.homedir) || os.homedir();
16
29
  return [
17
30
  path.join(home, '.claude', 'local'),
18
31
  path.join(home, '.local', 'bin'),
@@ -29,10 +42,20 @@ function userBinDirs() {
29
42
  * value to put in a spawned child's PATH so `claude`, node, git, etc. resolve
30
43
  * on macOS even under a stripped Finder/Dock PATH. Appended (not prepended) so
31
44
  * a user's version manager (nvm/asdf/volta/mise) earlier in PATH still wins and
32
- * isn't shadowed by /opt/homebrew. */
33
- function pathWithUserBins() {
34
- const base = process.env.PATH || '';
35
- return base ? `${base}:${userBinDirs().join(':')}` : userBinDirs().join(':');
45
+ * isn't shadowed by /opt/homebrew. Windows: ';' delimiter, and PATH is read
46
+ * case-insensitively (the variable is usually `Path`). */
47
+ function pathWithUserBins(opts) {
48
+ const platform = (opts && opts.platform) || process.platform;
49
+ const env = (opts && opts.env) || process.env;
50
+ const win = platform === 'win32';
51
+ const delim = win ? ';' : ':';
52
+ let base = env.PATH || '';
53
+ if (win && !base) {
54
+ const key = Object.keys(env).find((k) => k.toUpperCase() === 'PATH');
55
+ base = (key && env[key]) || '';
56
+ }
57
+ const dirs = userBinDirs({ ...(opts || {}), platform, env }).join(delim);
58
+ return base && dirs ? `${base}${delim}${dirs}` : base || dirs;
36
59
  }
37
60
 
38
61
  /**
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ /**
6
+ * Pick the interactive shell for a terminal tab. Pure: every host fact comes in
7
+ * through `platform` / `env` / `exists` so it is testable for any OS.
8
+ *
9
+ * win32: PowerShell 7 (`pwsh.exe`) when found on PATH or under
10
+ * %ProgramFiles%\PowerShell\7, else the Windows PowerShell 5.1 that ships with
11
+ * every install. POSIX: `$SHELL` (or /bin/bash) as an interactive login shell.
12
+ *
13
+ * @param {{ platform: string, env: Record<string, string | undefined>, exists: (p: string) => boolean }} opts
14
+ * @returns {{ file: string, args: string[] }}
15
+ */
16
+ function defaultShell({ platform, env, exists }) {
17
+ if (platform !== 'win32') {
18
+ return { file: env.SHELL || '/bin/bash', args: ['-il'] };
19
+ }
20
+ const w = path.win32;
21
+ const pathDirs = (env.PATH || env.Path || '').split(';').filter(Boolean);
22
+ const programFiles = env.ProgramFiles || 'C:\\Program Files';
23
+ const candidates = [
24
+ ...pathDirs.map((d) => w.join(d, 'pwsh.exe')),
25
+ w.join(programFiles, 'PowerShell', '7', 'pwsh.exe'),
26
+ ];
27
+ const pwsh = candidates.find((p) => exists(p));
28
+ if (pwsh) return { file: pwsh, args: ['-NoLogo'] };
29
+ const systemRoot = env.SystemRoot || env.windir || 'C:\\Windows';
30
+ return {
31
+ file: w.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
32
+ args: ['-NoLogo'],
33
+ };
34
+ }
35
+
36
+ module.exports = { defaultShell };
@@ -16,6 +16,7 @@ const { splitFrontmatter } = require('./prdFrontmatter.cjs');
16
16
  const { resolvePrdWriteDir } = require('./prdLocations.cjs');
17
17
  const schedulerPaths = require('./schedulerPaths.cjs');
18
18
  const atomicFs = require('./atomicFs.cjs');
19
+ const { resolveSpawn } = require('./winSpawn.cjs');
19
20
 
20
21
  // Regex identifying meta/dod slugs that must NOT influence the batchKey.
21
22
  // This is the load-bearing loop-avoidance filter: when the gate job itself
@@ -771,10 +772,13 @@ function runOneGateCommand(cmd, { cwd, env }) {
771
772
  const label = cmd.argv.join(' ');
772
773
  let child;
773
774
  try {
774
- child = spawn(cmd.argv[0], cmd.argv.slice(1), {
775
+ const childEnv = { ...env, ...cmd.env };
776
+ const sp = resolveSpawn(cmd.argv[0], cmd.argv.slice(1), { env: childEnv });
777
+ child = spawn(sp.command, sp.args, {
775
778
  cwd,
776
- env: { ...env, ...cmd.env },
779
+ env: childEnv,
777
780
  stdio: ['ignore', 'pipe', 'pipe'],
781
+ ...(sp.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
778
782
  // No shell:true — the sequence was tokenized by tokenizeNoShell.
779
783
  });
780
784
  } catch {
@@ -24,6 +24,7 @@ const { writeJson, addAllowedRoot } = require('../config.cjs');
24
24
  const { cleanChildEnv, pathWithUserBins } = require('./cleanEnv.cjs');
25
25
  const { shimPath: guardShimPath, resolveShimTarget, ensureGuardShimsOrError } = require('./guardShims.cjs');
26
26
  const { resolveProjectRoot } = require('./opsOwnership.cjs');
27
+ const { nodeShellCommand } = require('./appRuntime.cjs');
27
28
 
28
29
  const SCHEDULER_MCP_NAME = 'session-manager-scheduler';
29
30
  const DEV_PLUGIN_ENABLED_KEY = 'session-manager-dev@session-manager';
@@ -49,7 +50,14 @@ function readJsonSafe(absPath, fallback) {
49
50
  }
50
51
  }
51
52
 
52
- const SCHEDULER_MCP_FIX_CMD = `claude mcp add ${SCHEDULER_MCP_NAME} --scope user -- node ${SCHEDULER_MCP_SERVER_SCRIPT}`;
53
+ /** Lazy (not a module-load constant) so tests can toggle packaged mode. */
54
+ function schedulerMcpFixCmd() {
55
+ const cmd = nodeShellCommand(SCHEDULER_MCP_SERVER_SCRIPT);
56
+ const m = /^ELECTRON_RUN_AS_NODE=1 (.*)$/.exec(cmd);
57
+ return m
58
+ ? `claude mcp add ${SCHEDULER_MCP_NAME} --scope user -e ELECTRON_RUN_AS_NODE=1 -- ${m[1]}`
59
+ : `claude mcp add ${SCHEDULER_MCP_NAME} --scope user -- ${cmd}`;
60
+ }
53
61
 
54
62
  /**
55
63
  * `ok` must mean "an agent in this project can actually reach a working
@@ -79,7 +87,7 @@ function checkSchedulerMcp({ cwd, homeDir }) {
79
87
  detail: !isAbsolute
80
88
  ? `user-scope ${SCHEDULER_MCP_NAME} registration points at a non-absolute script path (${scriptArg})`
81
89
  : `user-scope ${SCHEDULER_MCP_NAME} registration points at a script that no longer exists (${scriptArg})`,
82
- fix: SCHEDULER_MCP_FIX_CMD,
90
+ fix: schedulerMcpFixCmd(),
83
91
  };
84
92
  }
85
93
  return {
@@ -106,7 +114,7 @@ function checkSchedulerMcp({ cwd, homeDir }) {
106
114
  label: 'Scheduler MCP server registered',
107
115
  ok: false,
108
116
  detail: `no ${SCHEDULER_MCP_NAME} entry in ~/.claude.json or ${cwd}/.mcp.json`,
109
- fix: SCHEDULER_MCP_FIX_CMD,
117
+ fix: schedulerMcpFixCmd(),
110
118
  };
111
119
  }
112
120
 
@@ -476,7 +484,7 @@ function checkGuard(def, { cwd, homeDir = os.homedir() }) {
476
484
  detail,
477
485
  fix: ok
478
486
  ? null
479
- : `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching ${def.matcher} that runs node ${guardShimPath(def.scriptBasename, homeDir)}${def.nudge ? ' (a nudge, not a hard gate)' : ''}`,
487
+ : `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching ${def.matcher} that runs ${nodeShellCommand(guardShimPath(def.scriptBasename, homeDir))}${def.nudge ? ' (a nudge, not a hard gate)' : ''}`,
480
488
  fixAction: ok ? null : def.fixActionId,
481
489
  };
482
490
  }
@@ -549,7 +557,7 @@ async function installGuard(def, { cwd, homeDir = os.homedir(), skipShimEnsure =
549
557
  }
550
558
  const settings = (existing && typeof existing === 'object' && !Array.isArray(existing)) ? existing : {};
551
559
 
552
- const command = `node ${guardShimPath(def.scriptBasename, homeDir)}`;
560
+ const command = nodeShellCommand(guardShimPath(def.scriptBasename, homeDir));
553
561
  const hooks = (settings.hooks && typeof settings.hooks === 'object') ? settings.hooks : {};
554
562
  const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse.slice() : [];
555
563
 
@@ -1422,14 +1422,16 @@ async function integrateBranch({ cwd, branch, key, kind, carriedPaths, baseBranc
1422
1422
 
1423
1423
  try {
1424
1424
  await execGit(['merge', '--ff-only', branch], { cwd, timeout: 30_000 });
1425
- return { ok: true, integrated: true, fastForward: true };
1425
+ // The branch tip we fast-forwarded to, read from the branch ref (not HEAD,
1426
+ // which a concurrent writer may already have moved).
1427
+ return { ok: true, integrated: true, fastForward: true, sha: await tryResolveCommit(cwd, `${branch}^{commit}`) };
1426
1428
  } catch {
1427
1429
  // Main tree advanced since the worktree branched (a sibling job/Epic
1428
1430
  // merged first) — a real merge commit still lands the branch's commit(s).
1429
1431
  }
1430
1432
  try {
1431
1433
  await execGit(['merge', '--no-ff', '--no-edit', '-m', mergeMessage, branch], { cwd, timeout: 30_000 });
1432
- return { ok: true, integrated: true, mergeCommit: true };
1434
+ return { ok: true, integrated: true, mergeCommit: true, sha: await readHeadSha(cwd) };
1433
1435
  } catch (e) {
1434
1436
  const stderrText = (e && (e.stderrText || e.message)) || String(e);
1435
1437
  // git refuses this class of merge before ever touching MERGE_HEAD, so
@@ -1452,6 +1454,7 @@ async function integrateBranch({ cwd, branch, key, kind, carriedPaths, baseBranc
1452
1454
  ok: true,
1453
1455
  integrated: true,
1454
1456
  mergeCommit: true,
1457
+ sha: await readHeadSha(cwd),
1455
1458
  autoResolved: 'identical_working_tree_duplicates',
1456
1459
  resolvedPaths: allPaths,
1457
1460
  };
@@ -1479,7 +1482,7 @@ async function integrateBranch({ cwd, branch, key, kind, carriedPaths, baseBranc
1479
1482
  await resolveByConcatenation({ cwd, paths });
1480
1483
  await execGit(['add', ...paths], { cwd, timeout: 30_000 });
1481
1484
  await execGit(['commit', '-m', mergeMessage], { cwd, timeout: 30_000 });
1482
- return { ok: true, integrated: true, mergeCommit: true, autoResolved: 'pure_addition_concat', resolvedPaths: paths };
1485
+ return { ok: true, integrated: true, mergeCommit: true, sha: await readHeadSha(cwd), autoResolved: 'pure_addition_concat', resolvedPaths: paths };
1483
1486
  }
1484
1487
  } catch { /* fall through to the abort */ }
1485
1488
  }
@@ -19,7 +19,8 @@
19
19
  * resolved at startup we keep going with the literal value — boot-time
20
20
  * self-check in index.cjs surfaces that to the user.
21
21
  *
22
- * Linux + darwin only. No Windows path handling.
22
+ * win32: containment compares lower-cased, path.win32-normalized paths; UNC
23
+ * (`\\server\share`) and `\\?\` paths are always outside home.
23
24
  */
24
25
  'use strict';
25
26
 
@@ -52,8 +53,18 @@ function resolveTarget(p) {
52
53
  }
53
54
  }
54
55
 
55
- function isContained(realPath) {
56
- return realPath === HOME_REAL || realPath.startsWith(HOME_REAL + path.sep);
56
+ function isContainedWin32(target, home) {
57
+ // UNC and \\?\ / \\.\ prefixes can alias any location — never inside home.
58
+ if (/^[\\/]{2}/.test(target)) return false;
59
+ const strip = (v) => path.win32.normalize(v).toLowerCase().replace(/[\\/]+$/, '');
60
+ const t = strip(target);
61
+ const h = strip(home);
62
+ return t === h || t.startsWith(h + '\\');
63
+ }
64
+
65
+ function isContained(realPath, home = HOME_REAL, platform = process.platform) {
66
+ if (platform === 'win32') return isContainedWin32(realPath, home);
67
+ return realPath === home || realPath.startsWith(home + path.sep);
57
68
  }
58
69
 
59
70
  function assertInsideHome(p) {
@@ -88,4 +99,4 @@ function checkInsideHome(p) {
88
99
  return { ok: true, realPath };
89
100
  }
90
101
 
91
- module.exports = { assertInsideHome, checkInsideHome };
102
+ module.exports = { assertInsideHome, checkInsideHome, isContained };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * killTree.cjs — cross-platform "kill this child and its descendants" plus the
5
+ * matching spawn options for children we intend to kill that way.
6
+ *
7
+ * POSIX: children are spawned `detached: true` so they lead their own process
8
+ * group, and `process.kill(-pid, sig)` signals the whole group.
9
+ * win32: no process groups; `taskkill /PID <pid> /T /F` kills the tree.
10
+ */
11
+
12
+ const { execFile: nodeExecFile } = require('node:child_process');
13
+
14
+ /**
15
+ * @typedef {object} KillTreeOpts
16
+ * @property {string} [platform] defaults to process.platform
17
+ * @property {(pid: number, signal?: string | number) => unknown} [kill] defaults to process.kill
18
+ * @property {(file: string, args: string[], options: object, cb: (err: Error | null) => void) => unknown} [execFile]
19
+ * @property {(msg: string) => void} [log] receives taskkill failure messages
20
+ */
21
+
22
+ /**
23
+ * Kill `pid` and its descendants. Never throws.
24
+ *
25
+ * Return contract (deliberately synchronous on every platform so POSIX callers
26
+ * keep their existing control flow):
27
+ * - POSIX: true when a signal was delivered (group first, then the bare pid if
28
+ * the group kill threw ESRCH/EPERM), false otherwise.
29
+ * - win32: taskkill is started fire-and-forget and true is returned
30
+ * immediately when it was launched; its eventual non-zero exit is reported
31
+ * through `opts.log`, not the return value. The signal is ignored — taskkill
32
+ * /F is always forceful.
33
+ *
34
+ * @param {number} pid
35
+ * @param {string | number} [signal]
36
+ * @param {KillTreeOpts} [opts]
37
+ * @returns {boolean}
38
+ */
39
+ function killTree(pid, signal, opts = {}) {
40
+ const platform = opts.platform || process.platform;
41
+ const log = typeof opts.log === 'function' ? opts.log : () => {};
42
+ if (!Number.isInteger(pid) || pid <= 0) return false;
43
+
44
+ if (platform === 'win32') {
45
+ const execFile = opts.execFile || nodeExecFile;
46
+ try {
47
+ execFile('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true }, (err) => {
48
+ if (err) {
49
+ try { log(`[killTree] taskkill /PID ${pid} failed: ${err.message}\n`); } catch { /* log must not throw */ }
50
+ }
51
+ });
52
+ return true;
53
+ } catch (e) {
54
+ try { log(`[killTree] taskkill spawn failed: ${e && e.message}\n`); } catch { /* log must not throw */ }
55
+ return false;
56
+ }
57
+ }
58
+
59
+ const kill = opts.kill || process.kill.bind(process);
60
+ try { kill(-pid, signal); return true; } catch (e) {
61
+ const code = e && e.code;
62
+ if (code !== 'ESRCH' && code !== 'EPERM') return false;
63
+ }
64
+ try { kill(pid, signal); return true; } catch { return false; }
65
+ }
66
+
67
+ /**
68
+ * spawn() options for a child that killTree() will later kill.
69
+ * `detached` makes it a process-group leader on POSIX; on win32 it would open a
70
+ * console window unless `windowsHide` is set.
71
+ *
72
+ * @param {{ platform?: string }} [opts]
73
+ * @returns {{ detached: true, windowsHide?: true }}
74
+ */
75
+ function detachedSpawnOpts(opts = {}) {
76
+ const platform = opts.platform || process.platform;
77
+ return platform === 'win32' ? { detached: true, windowsHide: true } : { detached: true };
78
+ }
79
+
80
+ module.exports = { killTree, detachedSpawnOpts };
@@ -116,6 +116,7 @@ function createLoadGate({
116
116
  auditIntervalMs = AUDIT_INTERVAL_MS,
117
117
  escalateAfterMs = JOB_OVERRUN_FLOOR_MS,
118
118
  releaseWindowMs = RELEASE_WINDOW_MS,
119
+ platform = process.platform,
119
120
  } = {}) {
120
121
  let lastAuditAt = null; // null = never audited; the first gated tick always audits
121
122
  let gatedSince = null;
@@ -124,6 +125,21 @@ function createLoadGate({
124
125
 
125
126
  function evaluate({ bypass = false } = {}) {
126
127
  const t = now();
128
+ if (platform === 'win32') {
129
+ // os.loadavg() is always [0,0,0] on Windows — the gate is open, not computed.
130
+ last = {
131
+ gated: false,
132
+ bypassed: false,
133
+ ratio: 0,
134
+ threshold: typeof threshold === 'function' ? threshold() : threshold,
135
+ loadavg1: null,
136
+ cores: cores(),
137
+ gatedSinceMs: 0,
138
+ reason: 'loadavg-unavailable-win32',
139
+ at: new Date(t).toISOString(),
140
+ };
141
+ return { ...last, shouldAudit: false, escalate: false };
142
+ }
127
143
  const [l1] = loadavg();
128
144
  const c = cores();
129
145
  const th = typeof threshold === 'function' ? threshold() : threshold;
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const { execFile } = require('node:child_process');
4
+ const path = require('node:path');
5
+
6
+ const START = '__SM_PATH_START__';
7
+ const END = '__SM_PATH_END__';
8
+ const DEFAULT_TIMEOUT_MS = 3000;
9
+
10
+ /**
11
+ * A packaged macOS app launched from Finder/Dock inherits only
12
+ * /usr/bin:/bin:/usr/sbin:/sbin, so nvm/asdf/volta/fnm/Homebrew installs of
13
+ * node, git and claude are invisible. Ask the user's login shell for its PATH.
14
+ * Complements cleanEnv.pathWithUserBins (fixed dirs, appended).
15
+ */
16
+
17
+ /** @param {{shell?: string, platform?: string, timeoutMs?: number, env?: NodeJS.ProcessEnv}} [opts] */
18
+ function resolveShell(opts) {
19
+ const platform = opts.platform || process.platform;
20
+ const env = opts.env || process.env;
21
+ return opts.shell || env.SHELL || (platform === 'darwin' ? '/bin/zsh' : '/bin/bash');
22
+ }
23
+
24
+ /**
25
+ * Login-shell PATH, or null on timeout / non-zero exit / missing markers /
26
+ * win32 (fail-open: callers keep their current PATH).
27
+ * @param {{shell?: string, platform?: string, timeoutMs?: number, env?: NodeJS.ProcessEnv}} [opts]
28
+ * @returns {Promise<string|null>}
29
+ */
30
+ function readLoginShellPath(opts = {}) {
31
+ const platform = opts.platform || process.platform;
32
+ if (platform === 'win32') return Promise.resolve(null);
33
+ const shell = resolveShell(opts);
34
+ const cmd = `printf '%s' '${START}'; printf '%s' "$PATH"; printf '%s' '${END}'`;
35
+ return new Promise((resolve) => {
36
+ try {
37
+ execFile(
38
+ shell,
39
+ ['-ilc', cmd],
40
+ { timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, env: opts.env || process.env, encoding: 'utf8' },
41
+ (err, stdout) => {
42
+ if (err) return resolve(null);
43
+ const out = String(stdout || '');
44
+ const s = out.indexOf(START);
45
+ if (s < 0) return resolve(null);
46
+ const from = s + START.length;
47
+ const e = out.indexOf(END, from);
48
+ if (e < 0) return resolve(null);
49
+ resolve(out.slice(from, e));
50
+ },
51
+ );
52
+ } catch {
53
+ resolve(null);
54
+ }
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Prepend login-shell PATH entries not already present to env.PATH
60
+ * (order preserved, deduped, empties dropped).
61
+ * @param {{shell?: string, platform?: string, timeoutMs?: number, env?: NodeJS.ProcessEnv}} [opts]
62
+ * @returns {Promise<{applied: boolean, added: number}>}
63
+ */
64
+ async function applyLoginShellPath(opts = {}) {
65
+ const platform = opts.platform || process.platform;
66
+ if (platform === 'win32') return { applied: false, added: 0 };
67
+ const loginPath = await readLoginShellPath(opts);
68
+ if (loginPath === null) return { applied: false, added: 0 };
69
+ const env = opts.env || process.env;
70
+ const current = (env.PATH || '').split(path.delimiter).filter(Boolean);
71
+ const seen = new Set(current);
72
+ const fresh = [];
73
+ for (const entry of loginPath.split(path.delimiter)) {
74
+ if (!entry || seen.has(entry)) continue;
75
+ seen.add(entry);
76
+ fresh.push(entry);
77
+ }
78
+ if (fresh.length) env.PATH = [...fresh, ...current].join(path.delimiter);
79
+ return { applied: true, added: fresh.length };
80
+ }
81
+
82
+ module.exports = { readLoginShellPath, applyLoginShellPath };
@@ -16,6 +16,7 @@ const os = require('node:os');
16
16
  const crypto = require('node:crypto');
17
17
  const path = require('node:path');
18
18
  const claudeBin = require('./claudeBin.cjs');
19
+ const { isPackagedApp } = require('./appRuntime.cjs');
19
20
 
20
21
  function resolveElectronApp() {
21
22
  try {
@@ -39,12 +40,13 @@ function resolvePackageVersion() {
39
40
  }
40
41
 
41
42
  /**
42
- * 'npx' when the app path lives inside an npm/npx cache dir, 'dev' when
43
+ * 'installer' for a packaged app build, 'npx' when the app path lives inside an npm/npx cache dir, 'dev' when
43
44
  * SM_DEV is set, else 'unknown'. Injectable so tests never depend on real
44
45
  * process state.
45
46
  */
46
- function resolveInstallChannel({ appPath = null, devFlag = !!process.env.SM_DEV } = {}) {
47
+ function resolveInstallChannel({ appPath = null, devFlag = !!process.env.SM_DEV, packaged } = {}) {
47
48
  if (devFlag) return 'dev';
49
+ if (isPackagedApp(typeof packaged === 'boolean' ? { packaged } : undefined)) return 'installer';
48
50
  if (typeof appPath === 'string' && /[\\/](\.npm|_npx|npm-cache|npx-cache)[\\/]/i.test(appPath)) {
49
51
  return 'npx';
50
52
  }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * mcpStdioServer.cjs — zero-dependency MCP server over newline-delimited
3
+ * JSON-RPC 2.0 on stdio. Implements only what the scheduler MCP server uses:
4
+ * initialize, ping, tools/list, tools/call. Stdout carries protocol messages
5
+ * only; diagnostics go to stderr.
6
+ */
7
+ 'use strict';
8
+
9
+ // Copied verbatim from @modelcontextprotocol/sdk dist/cjs/types.js
10
+ // (LATEST_PROTOCOL_VERSION first).
11
+ const SUPPORTED_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07'];
12
+
13
+ const PARSE_ERROR = -32700;
14
+ const METHOD_NOT_FOUND = -32601;
15
+ const INTERNAL_ERROR = -32603;
16
+
17
+ /**
18
+ * @param {{
19
+ * name: string,
20
+ * version: string,
21
+ * listTools: () => any[] | Promise<any[]>,
22
+ * callTool: (params: any) => Promise<any>,
23
+ * input?: NodeJS.ReadableStream,
24
+ * output?: NodeJS.WritableStream,
25
+ * }} opts
26
+ */
27
+ function createStdioServer({ name, version, listTools, callTool, input = process.stdin, output = process.stdout }) {
28
+ let buffer = '';
29
+ let started = false;
30
+ let closed = false;
31
+
32
+ function send(msg) {
33
+ if (closed) return;
34
+ try {
35
+ output.write(JSON.stringify(msg) + '\n');
36
+ } catch (err) {
37
+ process.stderr.write(`[mcpStdioServer] write failed: ${err && err.message}\n`);
38
+ }
39
+ }
40
+
41
+ function sendError(id, code, message) {
42
+ send({ jsonrpc: '2.0', id, error: { code, message } });
43
+ }
44
+
45
+ async function dispatch(msg) {
46
+ const hasId = msg && typeof msg === 'object' && msg.id !== undefined && msg.id !== null;
47
+ if (!hasId) return; // notification (or unusable message): never reply
48
+ const { id, method, params } = msg;
49
+ try {
50
+ let result;
51
+ switch (method) {
52
+ case 'initialize': {
53
+ const requested = params && params.protocolVersion;
54
+ result = {
55
+ protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : SUPPORTED_PROTOCOL_VERSIONS[0],
56
+ capabilities: { tools: {} },
57
+ serverInfo: { name, version },
58
+ };
59
+ break;
60
+ }
61
+ case 'ping':
62
+ result = {};
63
+ break;
64
+ case 'tools/list':
65
+ result = { tools: await listTools() };
66
+ break;
67
+ case 'tools/call':
68
+ result = await callTool(params);
69
+ break;
70
+ default:
71
+ sendError(id, METHOD_NOT_FOUND, `Method not found: ${method}`);
72
+ return;
73
+ }
74
+ send({ jsonrpc: '2.0', id, result });
75
+ } catch (err) {
76
+ sendError(id, INTERNAL_ERROR, err && err.message ? err.message : String(err));
77
+ }
78
+ }
79
+
80
+ function handleLine(line) {
81
+ if (line.endsWith('\r')) line = line.slice(0, -1);
82
+ if (!line.trim()) return;
83
+ let msg;
84
+ try {
85
+ msg = JSON.parse(line);
86
+ } catch {
87
+ sendError(null, PARSE_ERROR, 'Parse error');
88
+ return;
89
+ }
90
+ dispatch(msg).catch((err) => {
91
+ process.stderr.write(`[mcpStdioServer] dispatch failed: ${err && err.message}\n`);
92
+ });
93
+ }
94
+
95
+ function onData(chunk) {
96
+ buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
97
+ let idx;
98
+ while ((idx = buffer.indexOf('\n')) !== -1) {
99
+ const line = buffer.slice(0, idx);
100
+ buffer = buffer.slice(idx + 1);
101
+ handleLine(line);
102
+ }
103
+ }
104
+
105
+ function onEnd() {
106
+ close();
107
+ }
108
+
109
+ function start() {
110
+ if (started || closed) return;
111
+ started = true;
112
+ input.on('data', onData);
113
+ input.on('end', onEnd);
114
+ }
115
+
116
+ function close() {
117
+ if (closed) return;
118
+ closed = true;
119
+ input.removeListener('data', onData);
120
+ input.removeListener('end', onEnd);
121
+ buffer = '';
122
+ }
123
+
124
+ return { start, close };
125
+ }
126
+
127
+ module.exports = { createStdioServer, SUPPORTED_PROTOCOL_VERSIONS };