claude-code-session-manager 0.75.2 → 0.76.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/dist/assets/{AgentLibrary-s1jRGOji.js → AgentLibrary-CBx9l4zN.js} +1 -1
- package/dist/assets/{DataModel-DNefcWbk.js → DataModel-Bf0EIE_t.js} +1 -1
- package/dist/assets/{History-4sMk80yI.js → History-CpdtWhC8.js} +1 -1
- package/dist/assets/{Hooks-CNUVwH5P.js → Hooks-DyUbMDmg.js} +1 -1
- package/dist/assets/{HostBilko-BhkyvIhw.js → HostBilko-By-wIpry.js} +1 -1
- package/dist/assets/{Library-BpockanA.js → Library-CQmo4QVC.js} +1 -1
- package/dist/assets/{ListDetail-Cre7h3U0.js → ListDetail-BQMd6NOm.js} +1 -1
- package/dist/assets/{MarkdownEditor-x-FzvaXm.js → MarkdownEditor-DEp43FXX.js} +1 -1
- package/dist/assets/{McpServers-CXAcQLMB.js → McpServers-CLarzwqA.js} +1 -1
- package/dist/assets/{Memory-CGBv1Rco.js → Memory-B0sCdIy1.js} +1 -1
- package/dist/assets/{Panel-czEs21N2.js → Panel-BhWPVOCD.js} +1 -1
- package/dist/assets/{Permissions-rGkLOpqI.js → Permissions-Ddlq8T_O.js} +1 -1
- package/dist/assets/{Plugins-BTmB_vgU.js → Plugins-D2oA_2Jl.js} +2 -2
- package/dist/assets/{ProvenanceBadge-dmCivDq5.js → ProvenanceBadge-DgAgavUM.js} +1 -1
- package/dist/assets/{SaveBar-D80TNkAx.js → SaveBar-Qvc4Ek-H.js} +1 -1
- package/dist/assets/{Scheduler-C6wHSZP7.js → Scheduler-BmYJvNzK.js} +1 -1
- package/dist/assets/{ScopeSwitcher-DsaiYTgl.js → ScopeSwitcher-C_zWEtIl.js} +1 -1
- package/dist/assets/{Settings-m2fyU8Q5.js → Settings-2Vx3X5SI.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-yS-lgBFU.js → SkillReferenceGraph-BDEUjlTQ.js} +1 -1
- package/dist/assets/{Skills-DtQgCs9-.js → Skills-Cmrz_LeN.js} +1 -1
- package/dist/assets/{SystemPrompt-CLuVdoMA.js → SystemPrompt-DVA1eYDP.js} +1 -1
- package/dist/assets/{TagLibrary-DQXoI752.js → TagLibrary-DYJGAKZu.js} +1 -1
- package/dist/assets/{TiptapBody-BGKVXrvq.js → TiptapBody-DmPc3amD.js} +1 -1
- package/dist/assets/{Toggle-RFnEV71m.js → Toggle-zfd5LJkK.js} +1 -1
- package/dist/assets/{index-Ddrw7J7O.js → index-B_4PNh9T.js} +676 -676
- package/dist/assets/{index-CMLnzdZC.css → index-DIjnPkRN.css} +1 -1
- package/dist/assets/{settingsSchema-LwuJXKD1.js → settingsSchema-B9es6fdA.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/lib/activeSessions.cjs +146 -4
- package/scripts/scheduler-mcp-server.cjs +154 -95
- package/src/main/__tests__/epicStatusMirror.test.cjs +110 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +105 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +295 -0
- package/src/main/__tests__/prdCreate.test.cjs +109 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +15 -3
- package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +41 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +60 -1
- package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +185 -0
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +66 -0
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +14 -5
- package/src/main/bilkoHost.cjs +4 -3
- package/src/main/chatRunner.cjs +6 -1
- package/src/main/config.cjs +22 -33
- package/src/main/health.cjs +153 -2
- package/src/main/index.cjs +56 -4
- package/src/main/ipcSchemas.cjs +18 -1
- package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +179 -0
- package/src/main/lib/__tests__/childWithLog.test.cjs +63 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +241 -42
- package/src/main/lib/__tests__/ephemeralCwd.test.cjs +91 -0
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +1 -1
- package/src/main/lib/__tests__/gitWorktree.test.cjs +14 -2
- package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +107 -0
- package/src/main/lib/__tests__/jobWorktree.test.cjs +2 -2
- package/src/main/lib/__tests__/loadGate.test.cjs +159 -0
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +101 -0
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +202 -0
- package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +51 -0
- package/src/main/lib/__tests__/opsRootResolve.test.cjs +149 -0
- package/src/main/lib/__tests__/projectRootResolve.test.cjs +148 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +112 -0
- package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +19 -9
- package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +213 -0
- package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +127 -0
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +217 -0
- package/src/main/lib/activeIndexMerge.cjs +15 -0
- package/src/main/lib/activeIndexRebuild.cjs +133 -0
- package/src/main/lib/buildTarget.cjs +3 -2
- package/src/main/lib/childWithLog.cjs +33 -1
- package/src/main/lib/crossProjectFeedback.cjs +8 -1
- package/src/main/lib/delegationReadiness.cjs +408 -26
- package/src/main/lib/ephemeralCwd.cjs +78 -0
- package/src/main/lib/epicDelegationStats.cjs +2 -1
- package/src/main/lib/epicMint.cjs +17 -1
- package/src/main/lib/epicStatusMirror.cjs +95 -0
- package/src/main/lib/epicValidationHook.cjs +2 -1
- package/src/main/lib/gitWorktree.cjs +56 -2
- package/src/main/lib/jobWorktree.cjs +1 -0
- package/src/main/lib/loadGate.cjs +134 -0
- package/src/main/lib/mcpToolCatalog.cjs +285 -0
- package/src/main/lib/opsErrorLog.cjs +12 -1
- package/src/main/lib/opsOwnership.cjs +94 -0
- package/src/main/lib/prdAdminRoutes.cjs +43 -3
- package/src/main/lib/prdCreate.cjs +46 -14
- package/src/main/lib/prdLocations.cjs +13 -6
- package/src/main/lib/projectRootResolve.cjs +134 -0
- package/src/main/lib/promptSessionSchema.cjs +7 -0
- package/src/main/lib/queueStore.cjs +70 -8
- package/src/main/lib/rcaReport.cjs +1 -1
- package/src/main/lib/reaperHelpers.cjs +47 -1
- package/src/main/lib/schedulerBatch.cjs +171 -29
- package/src/main/lib/schedulerConfig.cjs +80 -0
- package/src/main/projectBrief.cjs +3 -2
- package/src/main/projectPages.cjs +2 -1
- package/src/main/promptSessionTranscript.cjs +0 -0
- package/src/main/pty.cjs +5 -0
- package/src/main/queueOps.cjs +15 -8
- package/src/main/scheduler.cjs +346 -49
- package/src/main/seedSchedulerMcp.cjs +58 -4
- package/src/preload/api.d.ts +69 -1
- package/src/preload/index.cjs +2 -0
|
@@ -11,8 +11,28 @@
|
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const os = require('node:os');
|
|
13
13
|
const path = require('node:path');
|
|
14
|
+
const { KIND_CONFIG: WORKTREE_KIND_CONFIG } = require('../../src/main/lib/gitWorktree.cjs');
|
|
14
15
|
|
|
15
16
|
const HOME = os.homedir();
|
|
17
|
+
const TMPDIR = os.tmpdir();
|
|
18
|
+
|
|
19
|
+
// The last-resort drop targets for addCwd's tmp guard.
|
|
20
|
+
// - exactDropCwds: dropped only on an EXACT match — os.tmpdir() itself
|
|
21
|
+
// (e.g. `/tmp`). NOT a prefix match: this codebase's own test suite
|
|
22
|
+
// routinely stubs HOME to a tmp dir and nests a fake project cwd
|
|
23
|
+
// underneath it, a legitimate, unrelated use of os.tmpdir() that a
|
|
24
|
+
// blanket prefix match would wrongly drop.
|
|
25
|
+
// - prefixDropRoots: dropped on an exact match OR any nested path — the two
|
|
26
|
+
// managed worktree roots (both live under os.tmpdir() per gitWorktree
|
|
27
|
+
// .cjs's KIND_CONFIG). Nothing but managed worktree checkouts is ever
|
|
28
|
+
// created under these roots, so a prefix match here is safe. A resolvable
|
|
29
|
+
// worktree (managed or user-made, anywhere on disk) is already handled by
|
|
30
|
+
// worktreeMainRootOf above; this guard only catches what that resolution
|
|
31
|
+
// could not — e.g. a bare, .git-less worktree-root directory.
|
|
32
|
+
const TMP_DROP_ROOTS = {
|
|
33
|
+
exactDropCwds: [TMPDIR],
|
|
34
|
+
prefixDropRoots: [WORKTREE_KIND_CONFIG.job.root, WORKTREE_KIND_CONFIG.epic.root],
|
|
35
|
+
};
|
|
16
36
|
|
|
17
37
|
// Transcript reads only need the last line with a `cwd` field — a few dozen
|
|
18
38
|
// lines is always enough. 64 KB keeps peak RSS proportionate.
|
|
@@ -21,6 +41,111 @@ const TAIL_BYTES = 64 * 1024;
|
|
|
21
41
|
// Safety cap on distinct cwds to bound result set size. O(1) extra space.
|
|
22
42
|
const MAX_CWDS = 50;
|
|
23
43
|
|
|
44
|
+
// The ops-root folder name. A transcript's `cwd` can point INSIDE it whenever
|
|
45
|
+
// an agent `cd`s into an artifact directory (a PRD folder, prompt-sessions,
|
|
46
|
+
// scheduler/state) — Claude Code records the new absolute cwd on every
|
|
47
|
+
// subsequent row, and this scan reads the LAST such row. Left unnormalized,
|
|
48
|
+
// that subdirectory is handed to consumers as if it were a project, and
|
|
49
|
+
// queueStore.writeSplit materializes a whole second ops root beneath it:
|
|
50
|
+
// `<project>/session-manager-operations/scheduler/epics/<id>/prds/session-
|
|
51
|
+
// manager-operations/scheduler/state/queue.json`, holding `{"jobs": []}`.
|
|
52
|
+
// Observed live on 2026-08-30 in starry-night-ships: 14 such stubs, mtimes
|
|
53
|
+
// spanning 8 days, one per directory an agent had cd'd into.
|
|
54
|
+
//
|
|
55
|
+
// Truncating at the segment is strictly better than dropping the row: the
|
|
56
|
+
// ancestor IS the project the agent was working in, so the active-project
|
|
57
|
+
// signal survives instead of being silently lost.
|
|
58
|
+
const OPS_DIRNAME = 'session-manager-operations';
|
|
59
|
+
|
|
60
|
+
// Bound on the ancestor walk in worktreeMainRootOf — well past any real
|
|
61
|
+
// filesystem depth, purely to guarantee termination without relying on
|
|
62
|
+
// reaching '/' (e.g. a symlink loop or an unusually deep path).
|
|
63
|
+
const MAX_WORKTREE_WALK = 40;
|
|
64
|
+
|
|
65
|
+
const WORKTREES_MARKER = `${path.sep}.git${path.sep}worktrees${path.sep}`;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* worktreeMainRootOf(cwd) → the main tree's root when cwd sits inside a
|
|
69
|
+
* linked `git worktree` (job/epic worktrees under os.tmpdir(), or a
|
|
70
|
+
* user-made one anywhere). Walks UP from cwd looking for a `.git` entry:
|
|
71
|
+
* - `.git` is a FILE (linked worktree) whose first line is
|
|
72
|
+
* `gitdir: <main>/.git/worktrees/<name>` → returns <main>.
|
|
73
|
+
* - `.git` is a DIRECTORY (already the main tree) → returns that ancestor.
|
|
74
|
+
* - nothing found within MAX_WORKTREE_WALK levels → returns null.
|
|
75
|
+
* Pure fs, synchronous, never throws — any unexpected shape (garbled file,
|
|
76
|
+
* unreadable entry, gitdir path without the worktrees marker) yields null so
|
|
77
|
+
* the caller leaves the cwd unchanged rather than guessing.
|
|
78
|
+
*/
|
|
79
|
+
function worktreeMainRootOf(cwd) {
|
|
80
|
+
let dir = cwd;
|
|
81
|
+
for (let i = 0; i < MAX_WORKTREE_WALK; i++) {
|
|
82
|
+
const gitPath = path.join(dir, '.git');
|
|
83
|
+
let stat;
|
|
84
|
+
try {
|
|
85
|
+
stat = fs.statSync(gitPath);
|
|
86
|
+
} catch {
|
|
87
|
+
stat = null;
|
|
88
|
+
}
|
|
89
|
+
if (stat) {
|
|
90
|
+
if (stat.isDirectory()) return dir;
|
|
91
|
+
if (stat.isFile()) {
|
|
92
|
+
let body;
|
|
93
|
+
try {
|
|
94
|
+
body = fs.readFileSync(gitPath, 'utf8');
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const firstLine = body.split('\n', 1)[0].trim();
|
|
99
|
+
const prefix = 'gitdir:';
|
|
100
|
+
if (!firstLine.startsWith(prefix)) return null;
|
|
101
|
+
const gitdir = firstLine.slice(prefix.length).trim();
|
|
102
|
+
const markerIdx = gitdir.indexOf(WORKTREES_MARKER);
|
|
103
|
+
if (markerIdx <= 0) return null;
|
|
104
|
+
const candidateMain = gitdir.slice(0, markerIdx);
|
|
105
|
+
const worktreeName = gitdir.slice(markerIdx + WORKTREES_MARKER.length).split(path.sep)[0];
|
|
106
|
+
// Round-trip verification: gitdir's content is a plain file that
|
|
107
|
+
// anything on disk could have written (e.g. a nested `.git` file
|
|
108
|
+
// tracked inside an untrusted repo), so it must not be trusted to
|
|
109
|
+
// redirect callers to an arbitrary attacker-chosen absolute path.
|
|
110
|
+
// A genuine linked worktree's admin dir back-references the exact
|
|
111
|
+
// `.git` FILE we are resolving via its own `gitdir` pointer file —
|
|
112
|
+
// require that round trip before accepting candidateMain.
|
|
113
|
+
if (!worktreeName) return null;
|
|
114
|
+
const adminGitdirFile = path.join(candidateMain, '.git', 'worktrees', worktreeName, 'gitdir');
|
|
115
|
+
let backRef;
|
|
116
|
+
try {
|
|
117
|
+
backRef = fs.readFileSync(adminGitdirFile, 'utf8').trim();
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
if (path.resolve(backRef) !== path.resolve(gitPath)) return null;
|
|
122
|
+
return candidateMain;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const parent = path.dirname(dir);
|
|
127
|
+
if (parent === dir) return null;
|
|
128
|
+
dir = parent;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* projectRootOf(cwd) → the project root for a cwd that may sit inside an ops
|
|
135
|
+
* tree and/or a linked git worktree. Returns cwd unchanged when neither
|
|
136
|
+
* applies. Only the FIRST ops-dir occurrence matters — a nested stray ops
|
|
137
|
+
* root is itself the bug, never a project. The ops-dir truncation runs
|
|
138
|
+
* first, then the worktree resolution, so a cwd deep inside a worktree's OWN
|
|
139
|
+
* ops tree still lands on the real project's main root.
|
|
140
|
+
*/
|
|
141
|
+
function projectRootOf(cwd) {
|
|
142
|
+
const parts = cwd.split(path.sep);
|
|
143
|
+
const i = parts.indexOf(OPS_DIRNAME);
|
|
144
|
+
const truncated = i > 0 ? (parts.slice(0, i).join(path.sep) || path.sep) : cwd;
|
|
145
|
+
const mainRoot = worktreeMainRootOf(truncated);
|
|
146
|
+
return mainRoot || truncated;
|
|
147
|
+
}
|
|
148
|
+
|
|
24
149
|
/**
|
|
25
150
|
* readTailLines(filePath, maxBytes) → string[]
|
|
26
151
|
* Reads at most maxBytes from the END of filePath and splits into non-empty lines.
|
|
@@ -57,11 +182,13 @@ function readTailLines(filePath, maxBytes) {
|
|
|
57
182
|
* Complexity: O(P × L) over P project dirs, each bounded-tail read (64 KB).
|
|
58
183
|
*
|
|
59
184
|
* opts (for testing):
|
|
60
|
-
* projectsDir
|
|
185
|
+
* projectsDir — override the default ~/.claude/projects path
|
|
186
|
+
* tmpDropRoots — override the tmp guard's drop roots (default TMP_DROP_ROOTS)
|
|
61
187
|
*/
|
|
62
188
|
function activeProjectCwds(maxAgeMin = 90, {
|
|
63
189
|
projectsDir = path.join(HOME, '.claude', 'projects'),
|
|
64
190
|
maxCwds = MAX_CWDS,
|
|
191
|
+
tmpDropRoots = TMP_DROP_ROOTS,
|
|
65
192
|
} = {}) {
|
|
66
193
|
// maxAgeMin === Infinity means "every project ever seen, no recency filter"
|
|
67
194
|
// (allProjectCwds below). Date.now() - Infinity is -Infinity, which every
|
|
@@ -70,8 +197,12 @@ function activeProjectCwds(maxAgeMin = 90, {
|
|
|
70
197
|
const seen = new Set();
|
|
71
198
|
const result = [];
|
|
72
199
|
|
|
73
|
-
function addCwd(
|
|
74
|
-
if (!
|
|
200
|
+
function addCwd(rawCwd) {
|
|
201
|
+
if (!rawCwd || typeof rawCwd !== 'string') return;
|
|
202
|
+
// Normalize an ops-internal cwd up to its project root BEFORE any of the
|
|
203
|
+
// checks below — the stray-ops-root incident (see OPS_DIRNAME above) got
|
|
204
|
+
// through precisely because such a path is absolute and does exist.
|
|
205
|
+
const cwd = projectRootOf(rawCwd);
|
|
75
206
|
// Must be ABSOLUTE. A relative fragment would pass the statSync below
|
|
76
207
|
// whenever it happens to resolve against THIS process's own cwd, and
|
|
77
208
|
// every consumer (queueStore.projectStateDir, prdLocations) then joins
|
|
@@ -79,6 +210,17 @@ function activeProjectCwds(maxAgeMin = 90, {
|
|
|
79
210
|
// whole per-project state off these strings — a project is a cwd, and a
|
|
80
211
|
// cwd is an absolute path.
|
|
81
212
|
if (!path.isAbsolute(cwd)) return;
|
|
213
|
+
// Drop-guard of last resort: a cwd that is (or sits inside) a known
|
|
214
|
+
// worktree-scratch root after projectRootOf's normalization is one
|
|
215
|
+
// worktreeMainRootOf could not resolve to a main tree — never a real
|
|
216
|
+
// project.
|
|
217
|
+
if (tmpDropRoots.exactDropCwds.includes(cwd)) return;
|
|
218
|
+
const isUnderPrefixDropRoot = tmpDropRoots.prefixDropRoots.some((root) => {
|
|
219
|
+
if (cwd === root) return true;
|
|
220
|
+
const rel = path.relative(root, cwd);
|
|
221
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
222
|
+
});
|
|
223
|
+
if (isUnderPrefixDropRoot) return;
|
|
82
224
|
if (seen.has(cwd) || result.length >= maxCwds) return;
|
|
83
225
|
// Must exist AND be a directory — a transcript naming a since-deleted
|
|
84
226
|
// path, or a file, is not a project.
|
|
@@ -146,4 +288,4 @@ function allProjectCwds(opts = {}) {
|
|
|
146
288
|
return activeProjectCwds(Infinity, { maxCwds: 500, ...opts });
|
|
147
289
|
}
|
|
148
290
|
|
|
149
|
-
module.exports = { activeProjectCwds, allProjectCwds };
|
|
291
|
+
module.exports = { activeProjectCwds, allProjectCwds, projectRootOf, worktreeMainRootOf };
|
|
@@ -29,11 +29,33 @@ const {
|
|
|
29
29
|
CallToolRequestSchema,
|
|
30
30
|
} = require('@modelcontextprotocol/sdk/types.js');
|
|
31
31
|
const { PRD_WORK_TYPES } = require('../src/main/lib/workTypeLibrary.cjs');
|
|
32
|
+
const { MCP_TOOL_CATALOG, MCP_RECIPES, composeDescription } = require('../src/main/lib/mcpToolCatalog.cjs');
|
|
33
|
+
|
|
34
|
+
const CATALOG_BY_NAME = new Map(MCP_TOOL_CATALOG.map((entry) => [entry.name, entry]));
|
|
35
|
+
|
|
36
|
+
function descriptionFor(toolName) {
|
|
37
|
+
const entry = CATALOG_BY_NAME.get(toolName);
|
|
38
|
+
if (!entry) throw new Error(`mcpToolCatalog.cjs has no entry for tool "${toolName}"`);
|
|
39
|
+
return composeDescription(entry);
|
|
40
|
+
}
|
|
32
41
|
|
|
33
42
|
const TOKEN_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'admin-api.json');
|
|
34
43
|
|
|
44
|
+
// Defined once so every failure path points to the same next call, verbatim
|
|
45
|
+
// — never paste this sentence at each return site (PRD: session_manager_help).
|
|
46
|
+
const HELP_POINTER = ' — call session_manager_help for the correct usage';
|
|
47
|
+
|
|
48
|
+
function withPointer(text) {
|
|
49
|
+
const str = String(text);
|
|
50
|
+
return str.endsWith(HELP_POINTER) ? str : str + HELP_POINTER;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function errorResult(text) {
|
|
54
|
+
return { content: [{ type: 'text', text: withPointer(text) }], isError: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
const NOT_RUNNING_ERROR =
|
|
36
|
-
|
|
58
|
+
`session-manager app is not running (admin API unreachable) — start it first${HELP_POINTER}`;
|
|
37
59
|
|
|
38
60
|
async function readAdminConfig() {
|
|
39
61
|
const raw = await fsp.readFile(TOKEN_PATH, 'utf8');
|
|
@@ -68,56 +90,56 @@ async function adminRequest(method, urlPath, body) {
|
|
|
68
90
|
return json;
|
|
69
91
|
}
|
|
70
92
|
|
|
93
|
+
// session_manager_help's live half. The catalog/recipes below are static and
|
|
94
|
+
// answerable purely in-process (no admin API needed), but "is this MCP
|
|
95
|
+
// server actually wired up for THIS project" needs the admin API's
|
|
96
|
+
// checkDelegationReadiness result — that's genuinely unavailable when the
|
|
97
|
+
// app is down, so this must degrade to a reported-unavailable state rather
|
|
98
|
+
// than throwing NOT_RUNNING_ERROR and failing the whole help call.
|
|
99
|
+
async function fetchReadiness() {
|
|
100
|
+
// SM_PROJECT_ROOT (when present) is a trusted hint for the real project
|
|
101
|
+
// root — forwarded so a worktree/ops-internal process.cwd() still reports
|
|
102
|
+
// the real project's readiness. See projectRootResolve.cjs.
|
|
103
|
+
const cwd = process.env.SM_PROJECT_ROOT || process.cwd();
|
|
104
|
+
try {
|
|
105
|
+
const qs = new URLSearchParams({ cwd });
|
|
106
|
+
const result = await adminRequest('GET', `/admin/mcp/readiness?${qs.toString()}`);
|
|
107
|
+
if (result?.ok === false) {
|
|
108
|
+
return { available: false, cwd, reason: result.error ?? 'readiness check failed' };
|
|
109
|
+
}
|
|
110
|
+
return { available: true, cwd, ok: result.ready, checks: result.checks };
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return { available: false, cwd, reason: e?.message ?? String(e) };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
71
116
|
const TOOLS = [
|
|
72
117
|
{
|
|
73
118
|
name: 'scheduler_reset_job',
|
|
74
|
-
description:
|
|
75
|
-
+ 'Refuses a job whose status is already "completed" unless force:true is passed — resetting '
|
|
76
|
-
+ 'a completed job re-executes already-shipped work.',
|
|
119
|
+
description: descriptionFor('scheduler_reset_job'),
|
|
77
120
|
inputSchema: {
|
|
78
121
|
type: 'object',
|
|
79
122
|
properties: {
|
|
80
123
|
slug: { type: 'string', description: 'PRD slug of the job to reset' },
|
|
81
124
|
force: { type: 'boolean', description: 'Required to reset a job whose status is already "completed"' },
|
|
125
|
+
cwd: { type: 'string', description: 'Optional: the PRD project cwd, narrows/speeds the search' },
|
|
82
126
|
},
|
|
83
127
|
required: ['slug'],
|
|
84
128
|
},
|
|
85
129
|
},
|
|
86
130
|
{
|
|
87
131
|
name: 'scheduler_list_jobs',
|
|
88
|
-
description:
|
|
132
|
+
description: descriptionFor('scheduler_list_jobs'),
|
|
89
133
|
inputSchema: { type: 'object', properties: {} },
|
|
90
134
|
},
|
|
91
135
|
{
|
|
92
136
|
name: 'scheduler_create_prd',
|
|
93
|
-
description:
|
|
94
|
-
"THE ONLY SANCTIONED WAY to author a PRD. Write a new PRD file via the session-manager "
|
|
95
|
-
+ "app's admin API. Server-side validates the frontmatter, atomically allocates the NN "
|
|
96
|
-
+ 'parallel-group number, appends the engineering standards, and writes the PRD file to '
|
|
97
|
-
+ "disk. This tool ONLY writes the file — it does not create a scheduler queue row. The "
|
|
98
|
-
+ 'queue row is derived automatically by the scheduler\'s next reconcile pass (typically '
|
|
99
|
-
+ 'within ~1 minute); the response has `enqueued: false` for exactly this reason. Every '
|
|
100
|
-
+ 'PRD must join an EXISTING, already-human-approved Epic (pass sourcePromptId) — this '
|
|
101
|
-
+ 'tool never mints a new one, and refuses the write if no Epic can be resolved. '
|
|
102
|
-
+ 'TWO DISTINCT FAILURE MODES if this tool is not usable — do not conflate them: '
|
|
103
|
-
+ '(a) this tool call is PRESENT in your tool list but ERRORS as app-not-running / admin '
|
|
104
|
-
+ 'API unreachable — that is the ONLY case where hand-authoring the PRD file directly on '
|
|
105
|
-
+ 'disk is an acceptable DEGRADED, LAST-RESORT fallback; the caller MUST say so explicitly '
|
|
106
|
-
+ 'and visibly in its report (which file, why the tool was unreachable, that it needs '
|
|
107
|
-
+ 'verification) since the server-side validation, atomic NN allocation, and '
|
|
108
|
-
+ 'Epic-existence check this tool performs did not run for that file. '
|
|
109
|
-
+ '(b) this tool is ABSENT from your tool list entirely — you were never offered it, so '
|
|
110
|
-
+ 'there is no error to catch. That means the session-manager-scheduler MCP server is not '
|
|
111
|
-
+ 'registered for this project: a MISCONFIGURATION, not an offline app. In that case DO '
|
|
112
|
-
+ 'NOT hand-write any PRD file — stop and tell the human the MCP server is not registered '
|
|
113
|
-
+ '(fix: `claude mcp add session-manager-scheduler --scope user -- node '
|
|
114
|
-
+ '<session-manager-repo>/scripts/scheduler-mcp-server.cjs`, once at user scope covers '
|
|
115
|
-
+ 'every project). See /develop.',
|
|
137
|
+
description: descriptionFor('scheduler_create_prd'),
|
|
116
138
|
inputSchema: {
|
|
117
139
|
type: 'object',
|
|
118
140
|
properties: {
|
|
119
141
|
title: { type: 'string', description: 'One-line human-readable title' },
|
|
120
|
-
cwd: { type: 'string', description: 'Absolute path to the target project (where claude -p will run)' },
|
|
142
|
+
cwd: { type: 'string', description: 'Absolute path to the target project (where claude -p will run). Optional inside an Epic session — the server resolves the real project from the calling session (originClaudeSessionId/sourcePromptId) when omitted.' },
|
|
121
143
|
estimateMinutes: { type: 'number', description: 'Integer wall-clock estimate in minutes' },
|
|
122
144
|
goal: { type: 'string', description: '2-4 sentences: what the executor will build and why' },
|
|
123
145
|
acceptanceCriteria: {
|
|
@@ -137,17 +159,12 @@ const TOOLS = [
|
|
|
137
159
|
description: 'Optional: the work type of THIS PRD — independent of the parent Epic\'s own tag. An Epic is the plan; a PRD is one unit of work inside it, and a single plan may legitimately contain several different work types. Never derived or inherited from the Epic.',
|
|
138
160
|
},
|
|
139
161
|
},
|
|
140
|
-
required: ['title', '
|
|
162
|
+
required: ['title', 'estimateMinutes', 'goal', 'acceptanceCriteria', 'implementationNotes'],
|
|
141
163
|
},
|
|
142
164
|
},
|
|
143
165
|
{
|
|
144
166
|
name: 'scheduler_list_prds',
|
|
145
|
-
description:
|
|
146
|
-
+ 'Each entry includes its real job status (pending/running/completed/failed/needs_review, or null if not yet '
|
|
147
|
-
+ 'queued/reconciled). Optionally filter by project cwd, Epic id, and/or status. Results are paginated (default '
|
|
148
|
-
+ 'limit 100, max 500) sorted by slug ascending — check `hasMore`/`total` in the response before assuming you '
|
|
149
|
-
+ "received every PRD; page further with `offset`. Default fields are compact (no parallelGroup/estimateMinutes/"
|
|
150
|
-
+ 'sourcePromptId/epicId/archivedStatus) — pass fields:"full" to restore them.',
|
|
167
|
+
description: descriptionFor('scheduler_list_prds'),
|
|
151
168
|
inputSchema: {
|
|
152
169
|
type: 'object',
|
|
153
170
|
properties: {
|
|
@@ -162,8 +179,7 @@ const TOOLS = [
|
|
|
162
179
|
},
|
|
163
180
|
{
|
|
164
181
|
name: 'scheduler_get_prd',
|
|
165
|
-
description:
|
|
166
|
-
+ "session-manager app's admin API.",
|
|
182
|
+
description: descriptionFor('scheduler_get_prd'),
|
|
167
183
|
inputSchema: {
|
|
168
184
|
type: 'object',
|
|
169
185
|
properties: {
|
|
@@ -175,11 +191,7 @@ const TOOLS = [
|
|
|
175
191
|
},
|
|
176
192
|
{
|
|
177
193
|
name: 'scheduler_update_prd',
|
|
178
|
-
description:
|
|
179
|
-
+ 'app\'s admin API. Refuses once a queue row exists for the slug and its status is anything but "pending" '
|
|
180
|
-
+ '(running/completed/failed/needs_review) — editing the spec under a live or already-finished executor is refused, '
|
|
181
|
-
+ 'not silently applied. Only recognized frontmatter keys (title, cwd, estimateMinutes, parallelGroup, '
|
|
182
|
-
+ 'sourcePromptId, sourceTabId, tag) may be patched; unrecognized keys (e.g. dependsOn) round-trip unchanged.',
|
|
194
|
+
description: descriptionFor('scheduler_update_prd'),
|
|
183
195
|
inputSchema: {
|
|
184
196
|
type: 'object',
|
|
185
197
|
properties: {
|
|
@@ -205,34 +217,31 @@ const TOOLS = [
|
|
|
205
217
|
},
|
|
206
218
|
{
|
|
207
219
|
name: 'scheduler_archive_prd',
|
|
208
|
-
description:
|
|
209
|
-
+ "app's admin API.",
|
|
220
|
+
description: descriptionFor('scheduler_archive_prd'),
|
|
210
221
|
inputSchema: {
|
|
211
222
|
type: 'object',
|
|
212
223
|
properties: {
|
|
213
224
|
slugs: { type: 'array', items: { type: 'string' }, description: 'Slugs to archive' },
|
|
225
|
+
cwd: { type: 'string', description: 'Optional: the PRDs\' project cwd, narrows/speeds the search for every slug in this batch' },
|
|
214
226
|
},
|
|
215
227
|
required: ['slugs'],
|
|
216
228
|
},
|
|
217
229
|
},
|
|
218
230
|
{
|
|
219
231
|
name: 'scheduler_cancel_job',
|
|
220
|
-
description:
|
|
221
|
-
+ 'API. A running job is SIGTERM\'d; a pending job is simply retired. There is no "cancelled" job status, so a '
|
|
222
|
-
+ 'cancelled job lands as "failed" with an error naming the cause. Refuses a slug whose job is already terminal '
|
|
223
|
-
+ '(completed/failed/needs_review) — nothing to cancel.',
|
|
232
|
+
description: descriptionFor('scheduler_cancel_job'),
|
|
224
233
|
inputSchema: {
|
|
225
234
|
type: 'object',
|
|
226
235
|
properties: {
|
|
227
236
|
slug: { type: 'string', description: 'PRD slug of the job to cancel' },
|
|
237
|
+
cwd: { type: 'string', description: 'Optional: the PRD project cwd, narrows/speeds the search' },
|
|
228
238
|
},
|
|
229
239
|
required: ['slug'],
|
|
230
240
|
},
|
|
231
241
|
},
|
|
232
242
|
{
|
|
233
243
|
name: 'scheduler_retag_prd',
|
|
234
|
-
description:
|
|
235
|
-
+ "parallelGroup changes, its NN- filename prefix) via the session-manager app's admin API.",
|
|
244
|
+
description: descriptionFor('scheduler_retag_prd'),
|
|
236
245
|
inputSchema: {
|
|
237
246
|
type: 'object',
|
|
238
247
|
properties: {
|
|
@@ -255,10 +264,7 @@ const TOOLS = [
|
|
|
255
264
|
},
|
|
256
265
|
{
|
|
257
266
|
name: 'chat_send_prompt',
|
|
258
|
-
description:
|
|
259
|
-
"Push a prompt into an already-open tab's chat queue via the session-manager app's admin "
|
|
260
|
-
+ 'API. The renderer resolves the tab (must currently be open) and runs the prompt through '
|
|
261
|
-
+ 'the same queued-vs-immediate path as a manual send. No-ops if the tab is unknown/closed.',
|
|
267
|
+
description: descriptionFor('chat_send_prompt'),
|
|
262
268
|
inputSchema: {
|
|
263
269
|
type: 'object',
|
|
264
270
|
properties: {
|
|
@@ -270,31 +276,12 @@ const TOOLS = [
|
|
|
270
276
|
},
|
|
271
277
|
{
|
|
272
278
|
name: 'feedback_list_projects',
|
|
273
|
-
description:
|
|
274
|
-
'List the OTHER projects on this machine that can receive feedback (i.e. that Session Manager '
|
|
275
|
-
+ 'already manages — they have a session-manager-operations/ directory). Call this FIRST when you '
|
|
276
|
-
+ 'need the exact `toCwd` for feedback_open_session and are not certain of it — never guess a path. '
|
|
277
|
-
+ 'A project missing from this list has simply never been opened in Session Manager; ask the human '
|
|
278
|
-
+ 'to open it once rather than inventing a path.',
|
|
279
|
+
description: descriptionFor('feedback_list_projects'),
|
|
279
280
|
inputSchema: { type: 'object', properties: {} },
|
|
280
281
|
},
|
|
281
282
|
{
|
|
282
283
|
name: 'feedback_open_session',
|
|
283
|
-
description:
|
|
284
|
-
'THE ONLY SANCTIONED WAY to hand a finding from THIS project to a DIFFERENT project. Opens a new '
|
|
285
|
-
+ "PROPOSED session in the receiving project's own Sessions queue, carrying your report as its "
|
|
286
|
-
+ 'opening prompt and stamped with where it came from. Session Manager performs the cross-folder '
|
|
287
|
-
+ 'write; you never write another project\'s files yourself. '
|
|
288
|
-
+ 'WHAT THIS DOES NOT DO: it does not start anything, queue a PRD, or spend a token. The session '
|
|
289
|
-
+ 'lands as `proposed` and runs only if a human in the RECEIVING project presses "Approve & start". '
|
|
290
|
-
+ 'There is no callback and no reply channel — do not wait for an answer, and do not tell the user '
|
|
291
|
-
+ 'the other project has "been fixed" or "is working on it". Report only that the proposal was '
|
|
292
|
-
+ 'delivered. '
|
|
293
|
-
+ 'WHEN NOT TO USE IT: for work in the project you are ALREADY in, run /develop inside the Epic you '
|
|
294
|
-
+ 'are already in — this tool refuses toCwd === fromCwd outright. '
|
|
295
|
-
+ 'Write the report for a reader who has never seen your project: state the symptom, where you '
|
|
296
|
-
+ 'observed it, what you expected, and (if you know) the file in THEIR repo that looks responsible. '
|
|
297
|
-
+ 'Never assume they can see your code.',
|
|
284
|
+
description: descriptionFor('feedback_open_session'),
|
|
298
285
|
inputSchema: {
|
|
299
286
|
type: 'object',
|
|
300
287
|
properties: {
|
|
@@ -329,6 +316,17 @@ const TOOLS = [
|
|
|
329
316
|
required: ['toCwd', 'fromCwd', 'title', 'body'],
|
|
330
317
|
},
|
|
331
318
|
},
|
|
319
|
+
{
|
|
320
|
+
name: 'session_manager_help',
|
|
321
|
+
description: descriptionFor('session_manager_help'),
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
properties: {
|
|
325
|
+
tool: { type: 'string', description: 'Optional: a tool name — returns that one catalog entry, including exampleArgs' },
|
|
326
|
+
topic: { type: 'string', description: 'Optional: a recipe id — returns that recipe\'s step-by-step instructions' },
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
332
330
|
];
|
|
333
331
|
|
|
334
332
|
const server = new Server(
|
|
@@ -338,16 +336,21 @@ const server = new Server(
|
|
|
338
336
|
|
|
339
337
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
340
338
|
|
|
341
|
-
|
|
339
|
+
// Named + exported (see module.exports below) so mcpToolCatalog.test.cjs's
|
|
340
|
+
// sibling can exercise session_manager_help's argument handling directly —
|
|
341
|
+
// mocking node:fs/promises + global.fetch to drive adminRequest — without
|
|
342
|
+
// booting a stdio transport.
|
|
343
|
+
async function handleCallTool(request) {
|
|
342
344
|
const { name, arguments: args } = request.params;
|
|
343
345
|
try {
|
|
344
346
|
if (name === 'scheduler_reset_job') {
|
|
345
347
|
const slug = args && typeof args.slug === 'string' ? args.slug : null;
|
|
346
348
|
if (!slug) {
|
|
347
|
-
return
|
|
349
|
+
return errorResult('missing required argument: slug');
|
|
348
350
|
}
|
|
349
351
|
const force = args && args.force === true;
|
|
350
|
-
const
|
|
352
|
+
const cwd = args && typeof args.cwd === 'string' ? args.cwd : undefined;
|
|
353
|
+
const result = await adminRequest('POST', '/admin/scheduler/reset-job', { slug, force, cwd });
|
|
351
354
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
352
355
|
}
|
|
353
356
|
if (name === 'scheduler_list_jobs') {
|
|
@@ -375,7 +378,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
375
378
|
if (name === 'scheduler_get_prd') {
|
|
376
379
|
const slug = args && typeof args.slug === 'string' ? args.slug : null;
|
|
377
380
|
if (!slug) {
|
|
378
|
-
return
|
|
381
|
+
return errorResult('missing required argument: slug');
|
|
379
382
|
}
|
|
380
383
|
const qs = new URLSearchParams({ slug });
|
|
381
384
|
if (args?.cwd) qs.set('cwd', args.cwd);
|
|
@@ -385,7 +388,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
385
388
|
if (name === 'scheduler_update_prd') {
|
|
386
389
|
const slug = args && typeof args.slug === 'string' ? args.slug : null;
|
|
387
390
|
if (!slug) {
|
|
388
|
-
return
|
|
391
|
+
return errorResult('missing required argument: slug');
|
|
389
392
|
}
|
|
390
393
|
const result = await adminRequest('POST', '/admin/scheduler/update-prd', args);
|
|
391
394
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
@@ -393,23 +396,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
393
396
|
if (name === 'scheduler_archive_prd') {
|
|
394
397
|
const slugs = Array.isArray(args?.slugs) ? args.slugs : null;
|
|
395
398
|
if (!slugs || slugs.length === 0) {
|
|
396
|
-
return
|
|
399
|
+
return errorResult('missing required argument: slugs');
|
|
397
400
|
}
|
|
398
|
-
const
|
|
401
|
+
const cwd = args && typeof args.cwd === 'string' ? args.cwd : undefined;
|
|
402
|
+
const result = await adminRequest('POST', '/admin/scheduler/archive-prd', { slugs, cwd });
|
|
399
403
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
400
404
|
}
|
|
401
405
|
if (name === 'scheduler_cancel_job') {
|
|
402
406
|
const slug = args && typeof args.slug === 'string' ? args.slug : null;
|
|
403
407
|
if (!slug) {
|
|
404
|
-
return
|
|
408
|
+
return errorResult('missing required argument: slug');
|
|
405
409
|
}
|
|
406
|
-
const
|
|
410
|
+
const cwd = args && typeof args.cwd === 'string' ? args.cwd : undefined;
|
|
411
|
+
const result = await adminRequest('POST', '/admin/scheduler/cancel-job', { slug, cwd });
|
|
407
412
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
408
413
|
}
|
|
409
414
|
if (name === 'scheduler_retag_prd') {
|
|
410
415
|
const items = Array.isArray(args?.items) ? args.items : null;
|
|
411
416
|
if (!items || items.length === 0) {
|
|
412
|
-
return
|
|
417
|
+
return errorResult('missing required argument: items');
|
|
413
418
|
}
|
|
414
419
|
const result = await adminRequest('POST', '/admin/scheduler/retag-prd', { items });
|
|
415
420
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
@@ -425,6 +430,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
425
430
|
if (!payload.sourcePromptId && process.env.SM_CHAT_SESSION_ID) {
|
|
426
431
|
payload.originClaudeSessionId = process.env.SM_CHAT_SESSION_ID;
|
|
427
432
|
}
|
|
433
|
+
// SM_PROJECT_ROOT (chatRunner.cjs/pty.cjs/scheduler.cjs's job spawn) —
|
|
434
|
+
// a trusted hint for the real project cwd, forwarded so the admin route
|
|
435
|
+
// can resolve it even when the caller omitted cwd or passed a worktree
|
|
436
|
+
// pwd. See projectRootResolve.cjs's resolveProjectContext.
|
|
437
|
+
if (process.env.SM_PROJECT_ROOT) {
|
|
438
|
+
payload.originProjectRoot = process.env.SM_PROJECT_ROOT;
|
|
439
|
+
}
|
|
428
440
|
const result = await adminRequest('POST', '/admin/scheduler/create-prd', payload);
|
|
429
441
|
// Never say "queued" — this tool only writes the PRD file; the queue
|
|
430
442
|
// row is derived by the scheduler's next reconcile pass, not by this
|
|
@@ -441,7 +453,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
441
453
|
if (name === 'feedback_open_session') {
|
|
442
454
|
for (const key of ['toCwd', 'fromCwd', 'title', 'body']) {
|
|
443
455
|
if (!args || typeof args[key] !== 'string' || !args[key].trim()) {
|
|
444
|
-
return
|
|
456
|
+
return errorResult(`missing required argument: ${key}`);
|
|
445
457
|
}
|
|
446
458
|
}
|
|
447
459
|
// Same fallback shape as scheduler_create_prd: forward this process's
|
|
@@ -452,6 +464,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
452
464
|
if (!payload.fromEpicId && process.env.SM_CHAT_SESSION_ID) {
|
|
453
465
|
payload.originClaudeSessionId = process.env.SM_CHAT_SESSION_ID;
|
|
454
466
|
}
|
|
467
|
+
// See scheduler_create_prd's own SM_PROJECT_ROOT forwarding above.
|
|
468
|
+
if (process.env.SM_PROJECT_ROOT) {
|
|
469
|
+
payload.originProjectRoot = process.env.SM_PROJECT_ROOT;
|
|
470
|
+
}
|
|
455
471
|
const result = await adminRequest('POST', '/admin/feedback/open-session', payload);
|
|
456
472
|
// Never say "sent", "filed" or "fixed" — this call delivers a PROPOSAL
|
|
457
473
|
// that a human in the other project must still approve.
|
|
@@ -464,23 +480,66 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
464
480
|
const tabId = args && typeof args.tabId === 'string' ? args.tabId : null;
|
|
465
481
|
const prompt = args && typeof args.prompt === 'string' ? args.prompt : null;
|
|
466
482
|
if (!tabId || !prompt) {
|
|
467
|
-
return
|
|
483
|
+
return errorResult('missing required arguments: tabId, prompt');
|
|
468
484
|
}
|
|
469
485
|
const result = await adminRequest('POST', '/admin/chat/send-prompt', { tabId, prompt });
|
|
470
486
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
471
487
|
}
|
|
472
|
-
|
|
488
|
+
if (name === 'session_manager_help') {
|
|
489
|
+
const toolName = args && typeof args.tool === 'string' ? args.tool : null;
|
|
490
|
+
const topic = args && typeof args.topic === 'string' ? args.topic : null;
|
|
491
|
+
|
|
492
|
+
const response = {};
|
|
493
|
+
|
|
494
|
+
if (toolName) {
|
|
495
|
+
const entry = CATALOG_BY_NAME.get(toolName);
|
|
496
|
+
if (!entry) {
|
|
497
|
+
const valid = MCP_TOOL_CATALOG.map((e) => e.name);
|
|
498
|
+
return errorResult(`unknown tool "${toolName}" for session_manager_help — valid tool names: ${valid.join(', ')}`);
|
|
499
|
+
}
|
|
500
|
+
response.tool = entry;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (topic) {
|
|
504
|
+
const recipe = MCP_RECIPES.find((r) => r.id === topic);
|
|
505
|
+
if (!recipe) {
|
|
506
|
+
const valid = MCP_RECIPES.map((r) => r.id);
|
|
507
|
+
return errorResult(`unknown topic "${topic}" for session_manager_help — valid topic ids: ${valid.join(', ')}`);
|
|
508
|
+
}
|
|
509
|
+
response.recipe = recipe;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!toolName && !topic) {
|
|
513
|
+
response.tools = MCP_TOOL_CATALOG.map((e) => ({ name: e.name, group: e.group, purpose: e.purpose }));
|
|
514
|
+
response.recipes = MCP_RECIPES.map((r) => ({ id: r.id, title: r.title }));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
response.readiness = await fetchReadiness();
|
|
518
|
+
|
|
519
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
520
|
+
}
|
|
521
|
+
return errorResult(`unknown tool: ${name}`);
|
|
473
522
|
} catch (e) {
|
|
474
|
-
return
|
|
523
|
+
return errorResult(e?.message ?? String(e));
|
|
475
524
|
}
|
|
476
|
-
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
server.setRequestHandler(CallToolRequestSchema, handleCallTool);
|
|
477
528
|
|
|
478
529
|
async function main() {
|
|
479
530
|
const transport = new StdioServerTransport();
|
|
480
531
|
await server.connect(transport);
|
|
481
532
|
}
|
|
482
533
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
534
|
+
// Guarded so mcpToolCatalog.test.cjs can `require()` this file to read TOOLS
|
|
535
|
+
// (for both-directions name-parity) without opening a stdio transport as a
|
|
536
|
+
// require-time side effect — only the real `node scripts/scheduler-mcp-server.cjs`
|
|
537
|
+
// invocation satisfies require.main === module.
|
|
538
|
+
if (require.main === module) {
|
|
539
|
+
main().catch((e) => {
|
|
540
|
+
process.stderr.write(`scheduler-mcp-server fatal: ${e?.stack ?? e}\n`);
|
|
541
|
+
process.exit(1);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
module.exports = { TOOLS, handleCallTool, HELP_POINTER, NOT_RUNNING_ERROR };
|