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.
- package/README.md +15 -3
- package/bin/cli.cjs +0 -5
- package/dist/assets/{AgentLibrary-QPw9gC_o.js → AgentLibrary-BDtr4oI-.js} +1 -1
- package/dist/assets/{Button-8RAbgh1d.js → Button-CuXBCQDp.js} +1 -1
- package/dist/assets/{DataModel-BEkuOZuS.js → DataModel-DmvRipT5.js} +1 -1
- package/dist/assets/{History-BLrNSazM.js → History-B-v9pwwr.js} +1 -1
- package/dist/assets/{Hooks-C8P6wQq1.js → Hooks-S_0oHHlr.js} +1 -1
- package/dist/assets/{Library-CWFvBZw-.js → Library-BcbGJxQ3.js} +1 -1
- package/dist/assets/{ListDetail-B1jVMgzN.js → ListDetail-DtqHqJHt.js} +1 -1
- package/dist/assets/{MarkdownEditor-JrnUbWjW.js → MarkdownEditor-B56sPp_l.js} +1 -1
- package/dist/assets/{McpServers-BGqTxDBm.js → McpServers-D5VnmxAn.js} +1 -1
- package/dist/assets/{Memory-h8bhRNyI.js → Memory-0yGZLyRP.js} +1 -1
- package/dist/assets/{Panel-srf0FT0m.js → Panel-NoAADJwx.js} +1 -1
- package/dist/assets/{Permissions-BA4oWydI.js → Permissions-D2Dh-ee8.js} +1 -1
- package/dist/assets/{Plugins-DTuTQqUL.js → Plugins-kGoUgS9b.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BW18EquH.js → ProvenanceBadge-CHwDFTX7.js} +1 -1
- package/dist/assets/{SaveBar-Cc6UukFa.js → SaveBar-BtKNMIYK.js} +1 -1
- package/dist/assets/{Scheduler-BctPlNL1.js → Scheduler-B0_kMMDs.js} +1 -1
- package/dist/assets/{ScopeSwitcher-DV1OnSuc.js → ScopeSwitcher-BusohWZ5.js} +1 -1
- package/dist/assets/{Settings-Bpyt1RGa.js → Settings-DuRJntaE.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-B28AYo12.js → SkillReferenceGraph-BHOdIpsy.js} +1 -1
- package/dist/assets/{Skills-zXLI5OB_.js → Skills-DSTLZimI.js} +1 -1
- package/dist/assets/{SystemPrompt-DKoN0UAV.js → SystemPrompt-BG7wFWdb.js} +1 -1
- package/dist/assets/{TagLibrary-BkDOa8IM.js → TagLibrary-Bx27aE3_.js} +1 -1
- package/dist/assets/{TiptapBody-CIY1f6mI.js → TiptapBody-j0O8J7YI.js} +1 -1
- package/dist/assets/{Toggle-C139wuAK.js → Toggle-DzVZ6rC7.js} +1 -1
- package/dist/assets/{ViewTabs-CUEfsMOe.js → ViewTabs-BizjuynT.js} +1 -1
- package/dist/assets/{index-D89EUizB.css → index-BLVzrc4k.css} +1 -1
- package/dist/assets/{index-DrvxIbmR.js → index-CLIaYljt.js} +416 -416
- package/dist/assets/{settingsSchema-CAFckLz6.js → settingsSchema-7MRAxaJk.js} +1 -1
- package/dist/assets/useAgentsDirWatch-BUwfU2Kq.js +1 -0
- package/dist/assets/{useConfigDirWatch-QKxfZx9X.js → useConfigDirWatch-ot0VYwQC.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +10 -12
- package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +12 -0
- package/plugins/session-manager-dev/skills/develop/SKILL.md +4 -2
- package/scripts/scheduler-mcp-server.cjs +7 -17
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +4 -4
- package/src/main/historyAggregator.cjs +7 -1
- package/src/main/index.cjs +34 -1
- package/src/main/lib/agentPersonaSchema.cjs +8 -1
- package/src/main/lib/appRuntime.cjs +57 -0
- package/src/main/lib/childWithLog.cjs +10 -10
- package/src/main/lib/claudeBin.cjs +40 -16
- package/src/main/lib/cleanEnv.cjs +29 -6
- package/src/main/lib/defaultShell.cjs +36 -0
- package/src/main/lib/definitionOfDone.cjs +6 -2
- package/src/main/lib/delegationReadiness.cjs +13 -5
- package/src/main/lib/gitWorktree.cjs +6 -3
- package/src/main/lib/insideHome.cjs +15 -4
- package/src/main/lib/killTree.cjs +80 -0
- package/src/main/lib/loadGate.cjs +16 -0
- package/src/main/lib/loginShellPath.cjs +82 -0
- package/src/main/lib/machineProfile.cjs +4 -2
- package/src/main/lib/mcpStdioServer.cjs +127 -0
- package/src/main/lib/openExternalApp.cjs +62 -11
- package/src/main/lib/otlpTraceExporter.cjs +147 -0
- package/src/main/lib/prereqs.cjs +145 -0
- package/src/main/lib/procIdentity.cjs +37 -2
- package/src/main/lib/rcaReport.cjs +1 -0
- package/src/main/lib/reaperHelpers.cjs +5 -2
- package/src/main/lib/shippedPersonaSeeds.cjs +10 -0
- package/src/main/lib/timeoutShimScript.cjs +14 -2
- package/src/main/lib/updateCheck.cjs +41 -11
- package/src/main/lib/watchdogHelpers.cjs +23 -3
- package/src/main/lib/winSpawn.cjs +67 -0
- package/src/main/otel.cjs +18 -62
- package/src/main/pty.cjs +32 -11
- package/src/main/runVerify.cjs +42 -2
- package/src/main/scheduler.cjs +105 -20
- package/src/main/seedSchedulerMcp.cjs +11 -2
- package/src/preload/api.d.ts +14 -0
- package/src/preload/index.cjs +2 -0
- package/src/seed/agents/validator.md +2 -2
- package/dist/assets/useAgentsDirWatch-XhQEVlaK.js +0 -1
- 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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
56
|
-
|
|
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 };
|