ast-mcp-server 0.3.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.
@@ -0,0 +1,233 @@
1
+ import path from "node:path";
2
+ import { Node, } from "ts-morph";
3
+ function joinPresent(parts) {
4
+ return parts.filter((part) => Boolean(part)).join(" ");
5
+ }
6
+ function modifiersOf(node) {
7
+ return node
8
+ .getModifiers()
9
+ .map((modifier) => modifier.getText())
10
+ .join(" ");
11
+ }
12
+ function typeParametersOf(node) {
13
+ const parameters = node.getTypeParameters().map((parameter) => parameter.getText());
14
+ return parameters.length > 0 ? `<${parameters.join(", ")}>` : "";
15
+ }
16
+ function parametersOf(node) {
17
+ return node
18
+ .getParameters()
19
+ .map((parameter) => parameter.getText())
20
+ .join(", ");
21
+ }
22
+ function returnTypeOf(node) {
23
+ const returnType = node.getReturnTypeNode()?.getText();
24
+ return returnType ? `: ${returnType}` : "";
25
+ }
26
+ function optionalMarker(node) {
27
+ return node.hasQuestionToken() ? "?" : "";
28
+ }
29
+ function functionSignature(node) {
30
+ const name = node.getName() ?? "<anonymous>";
31
+ const generator = node.getAsteriskToken() ? "*" : "";
32
+ const declaration = `function${generator} ${name}${typeParametersOf(node)}(${parametersOf(node)})${returnTypeOf(node)};`;
33
+ return joinPresent([modifiersOf(node), declaration]);
34
+ }
35
+ function methodSignature(node) {
36
+ const generator = node.getAsteriskToken() ? "*" : "";
37
+ const declaration = `${generator}${node.getName()}${optionalMarker(node)}${typeParametersOf(node)}(${parametersOf(node)})${returnTypeOf(node)};`;
38
+ return joinPresent([modifiersOf(node), declaration]);
39
+ }
40
+ function callableExpressionSignature(node) {
41
+ const generic = typeParametersOf(node);
42
+ const returnType = node.getReturnTypeNode()?.getText() ?? "unknown";
43
+ return `${generic}(${parametersOf(node)}) => ${returnType}`;
44
+ }
45
+ function memberSignature(member) {
46
+ if (Node.isMethodDeclaration(member))
47
+ return methodSignature(member);
48
+ if (Node.isConstructorDeclaration(member)) {
49
+ return joinPresent([modifiersOf(member), `constructor(${parametersOf(member)});`]);
50
+ }
51
+ if (Node.isGetAccessorDeclaration(member)) {
52
+ return joinPresent([modifiersOf(member), `get ${member.getName()}()${returnTypeOf(member)};`]);
53
+ }
54
+ if (Node.isSetAccessorDeclaration(member)) {
55
+ return joinPresent([modifiersOf(member), `set ${member.getName()}(${parametersOf(member)});`]);
56
+ }
57
+ if (Node.isPropertyDeclaration(member)) {
58
+ const question = member.hasQuestionToken() ? "?" : "";
59
+ const definite = member.hasExclamationToken() ? "!" : "";
60
+ const explicitType = member.getTypeNode()?.getText();
61
+ const initializer = member.getInitializer();
62
+ const callableType = initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))
63
+ ? callableExpressionSignature(initializer)
64
+ : undefined;
65
+ const type = explicitType ?? callableType;
66
+ return joinPresent([
67
+ modifiersOf(member),
68
+ `${member.getName()}${question}${definite}${type ? `: ${type}` : ""};`,
69
+ ]);
70
+ }
71
+ return undefined;
72
+ }
73
+ function symbolOf(node, symbolPath, name, signature) {
74
+ return {
75
+ symbolPath,
76
+ name,
77
+ kind: node.getKindName(),
78
+ signature,
79
+ startLine: node.getStartLineNumber(),
80
+ endLine: node.getEndLineNumber(),
81
+ };
82
+ }
83
+ function formatClass(statement, prefix = "") {
84
+ const name = statement.getName() ?? "<anonymous>";
85
+ const symbolPath = prefix ? `${prefix}.${name}` : name;
86
+ const heritage = statement.getExtends()?.getText();
87
+ const implementations = statement.getImplements().map((item) => item.getText());
88
+ const declaration = joinPresent([
89
+ modifiersOf(statement),
90
+ `class ${name}${typeParametersOf(statement)}`,
91
+ heritage ? `extends ${heritage}` : undefined,
92
+ implementations.length > 0 ? `implements ${implementations.join(", ")}` : undefined,
93
+ ]);
94
+ const lines = [`${declaration} {`];
95
+ const symbols = [symbolOf(statement, symbolPath, name, declaration)];
96
+ for (const member of statement.getMembers()) {
97
+ const signature = memberSignature(member);
98
+ if (!signature)
99
+ continue;
100
+ const memberName = Node.isConstructorDeclaration(member) ? "constructor" : member.getName();
101
+ lines.push(` ${signature}`);
102
+ symbols.push(symbolOf(member, `${symbolPath}.${memberName}`, memberName, signature));
103
+ }
104
+ lines.push("}");
105
+ return { lines, symbols };
106
+ }
107
+ function formatInterface(statement, prefix = "") {
108
+ const name = statement.getName();
109
+ const symbolPath = prefix ? `${prefix}.${name}` : name;
110
+ const extensions = statement.getExtends().map((item) => item.getText());
111
+ const declaration = joinPresent([
112
+ modifiersOf(statement),
113
+ `interface ${name}${typeParametersOf(statement)}`,
114
+ extensions.length > 0 ? `extends ${extensions.join(", ")}` : undefined,
115
+ ]);
116
+ const lines = [`${declaration} {`];
117
+ const symbols = [symbolOf(statement, symbolPath, name, declaration)];
118
+ for (const member of statement.getMembers()) {
119
+ const signature = member.getText().trim();
120
+ lines.push(` ${signature}`);
121
+ const memberName = "getName" in member ? member.getName() : member.getKindName();
122
+ symbols.push(symbolOf(member, `${symbolPath}.${memberName}`, memberName, signature));
123
+ }
124
+ lines.push("}");
125
+ return { lines, symbols };
126
+ }
127
+ function formatVariable(statement) {
128
+ const lines = [];
129
+ const symbols = [];
130
+ const keyword = statement.getDeclarationKind();
131
+ const modifiers = modifiersOf(statement);
132
+ for (const declaration of statement.getDeclarations()) {
133
+ const name = declaration.getName();
134
+ const explicitType = declaration.getTypeNode()?.getText();
135
+ const initializer = declaration.getInitializer();
136
+ const callableType = initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))
137
+ ? callableExpressionSignature(initializer)
138
+ : undefined;
139
+ const signature = joinPresent([
140
+ modifiers,
141
+ `${keyword} ${name}${explicitType || callableType ? `: ${explicitType ?? callableType}` : ""};`,
142
+ ]);
143
+ lines.push(signature);
144
+ symbols.push(symbolOf(declaration, name, name, signature));
145
+ }
146
+ return { lines, symbols };
147
+ }
148
+ function moduleStatements(module) {
149
+ const body = module.getBody();
150
+ if (!body)
151
+ return [];
152
+ if (Node.isModuleBlock(body))
153
+ return body.getStatements();
154
+ if (Node.isModuleDeclaration(body))
155
+ return [body];
156
+ return [];
157
+ }
158
+ function formatStatement(statement, prefix = "") {
159
+ if (Node.isClassDeclaration(statement))
160
+ return formatClass(statement, prefix);
161
+ if (Node.isInterfaceDeclaration(statement))
162
+ return formatInterface(statement, prefix);
163
+ if (Node.isFunctionDeclaration(statement)) {
164
+ const name = statement.getName() ?? "<anonymous>";
165
+ const symbolPath = prefix ? `${prefix}.${name}` : name;
166
+ const signature = functionSignature(statement);
167
+ return { lines: [signature], symbols: [symbolOf(statement, symbolPath, name, signature)] };
168
+ }
169
+ if (Node.isTypeAliasDeclaration(statement)) {
170
+ const name = statement.getName();
171
+ const symbolPath = prefix ? `${prefix}.${name}` : name;
172
+ const signature = joinPresent([
173
+ modifiersOf(statement),
174
+ `type ${name}${typeParametersOf(statement)} = ${statement.getTypeNode()?.getText() ?? "unknown"};`,
175
+ ]);
176
+ return { lines: [signature], symbols: [symbolOf(statement, symbolPath, name, signature)] };
177
+ }
178
+ if (Node.isEnumDeclaration(statement)) {
179
+ const name = statement.getName();
180
+ const symbolPath = prefix ? `${prefix}.${name}` : name;
181
+ const members = statement
182
+ .getMembers()
183
+ .map((member) => member.getName())
184
+ .join(", ");
185
+ const signature = joinPresent([modifiersOf(statement), `enum ${name} { ${members} }`]);
186
+ return { lines: [signature], symbols: [symbolOf(statement, symbolPath, name, signature)] };
187
+ }
188
+ if (Node.isVariableStatement(statement))
189
+ return formatVariable(statement);
190
+ if (Node.isModuleDeclaration(statement)) {
191
+ const name = statement.getName();
192
+ const symbolPath = prefix ? `${prefix}.${name}` : name;
193
+ const declaration = joinPresent([modifiersOf(statement), `namespace ${name}`]);
194
+ const lines = [`${declaration} {`];
195
+ const symbols = [symbolOf(statement, symbolPath, name, declaration)];
196
+ for (const child of moduleStatements(statement)) {
197
+ const formatted = formatStatement(child, symbolPath);
198
+ if (!formatted)
199
+ continue;
200
+ lines.push(...formatted.lines.map((line) => ` ${line}`));
201
+ symbols.push(...formatted.symbols);
202
+ }
203
+ lines.push("}");
204
+ return { lines, symbols };
205
+ }
206
+ return undefined;
207
+ }
208
+ export function buildFileOutline(sourceFile) {
209
+ const lines = [];
210
+ const symbols = [];
211
+ for (const statement of sourceFile.getStatements()) {
212
+ const formatted = formatStatement(statement);
213
+ if (!formatted)
214
+ continue;
215
+ lines.push(...formatted.lines);
216
+ symbols.push(...formatted.symbols);
217
+ }
218
+ return { text: lines.join("\n"), symbols };
219
+ }
220
+ export function fileOutline(sourceFile) {
221
+ return buildFileOutline(sourceFile).text;
222
+ }
223
+ export function nodeSourceWithLocation(node, projectRoot) {
224
+ const sourceFile = node.getSourceFile();
225
+ return {
226
+ file: projectRoot
227
+ ? path.relative(projectRoot, sourceFile.getFilePath())
228
+ : sourceFile.getFilePath(),
229
+ startLine: node.getStartLineNumber(),
230
+ endLine: node.getEndLineNumber(),
231
+ text: node.getText(),
232
+ };
233
+ }
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ export const DEFAULT_PAGE_LIMIT = 100;
3
+ export const MAX_PAGE_LIMIT = 500;
4
+ export const PaginationInputSchema = {
5
+ offset: z.number().int().min(0).default(0).describe("Zero-based result offset."),
6
+ limit: z
7
+ .number()
8
+ .int()
9
+ .min(1)
10
+ .max(MAX_PAGE_LIMIT)
11
+ .default(DEFAULT_PAGE_LIMIT)
12
+ .describe(`Maximum results to return (1-${MAX_PAGE_LIMIT}).`),
13
+ };
14
+ export const PaginationOutputSchema = {
15
+ offset: z.number().int().min(0),
16
+ limit: z.number().int().min(1),
17
+ total: z.number().int().min(0),
18
+ has_more: z.boolean(),
19
+ next_offset: z.number().int().min(0).nullable(),
20
+ };
21
+ export function paginate(items, offset, limit) {
22
+ const pageItems = items.slice(offset, offset + limit);
23
+ const nextOffset = offset + pageItems.length;
24
+ const hasMore = nextOffset < items.length;
25
+ return {
26
+ items: pageItems,
27
+ offset,
28
+ limit,
29
+ total: items.length,
30
+ has_more: hasMore,
31
+ next_offset: hasMore ? nextOffset : null,
32
+ };
33
+ }
@@ -0,0 +1,123 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { Project } from "ts-morph";
4
+ import { findDeclaration } from "./symbols.js";
5
+ import { createConfigSnapshot } from "./workspace.js";
6
+ const MAX_PROJECT_SESSIONS = 8;
7
+ const projectSessions = new Map();
8
+ const projectRoots = new WeakMap();
9
+ export function resolveTsConfigPath(projectRoot) {
10
+ const abs = path.resolve(projectRoot);
11
+ const stat = fs.existsSync(abs) ? fs.statSync(abs) : null;
12
+ if (stat?.isFile() && abs.endsWith(".json")) {
13
+ return fs.realpathSync(abs);
14
+ }
15
+ const candidate = path.join(abs, "tsconfig.json");
16
+ if (fs.existsSync(candidate)) {
17
+ return fs.realpathSync(candidate);
18
+ }
19
+ throw new Error(`No tsconfig.json found at "${abs}". Pass a project directory containing tsconfig.json or the config file itself.`);
20
+ }
21
+ function buildProjectContext(tsConfigFilePath) {
22
+ const project = new Project({
23
+ tsConfigFilePath,
24
+ skipAddingFilesFromTsConfig: false,
25
+ });
26
+ const projectRoot = path.dirname(tsConfigFilePath);
27
+ projectRoots.set(project, projectRoot);
28
+ return { project, projectRoot, tsConfigFilePath };
29
+ }
30
+ export function createFreshProject(projectRoot) {
31
+ return buildProjectContext(resolveTsConfigPath(projectRoot));
32
+ }
33
+ function evictSessions() {
34
+ if (projectSessions.size < MAX_PROJECT_SESSIONS)
35
+ return;
36
+ const candidate = [...projectSessions.entries()]
37
+ .filter(([, session]) => session.activeOperations === 0)
38
+ .sort((left, right) => left[1].lastAccessedAt - right[1].lastAccessedAt)[0];
39
+ if (candidate)
40
+ projectSessions.delete(candidate[0]);
41
+ }
42
+ function getOrCreateSession(projectRoot) {
43
+ const tsConfigFilePath = resolveTsConfigPath(projectRoot);
44
+ const existing = projectSessions.get(tsConfigFilePath);
45
+ if (existing)
46
+ return existing;
47
+ evictSessions();
48
+ const session = {
49
+ context: buildProjectContext(tsConfigFilePath),
50
+ configDigest: createConfigSnapshot(tsConfigFilePath).digest,
51
+ queue: Promise.resolve(),
52
+ activeOperations: 0,
53
+ lastAccessedAt: Date.now(),
54
+ };
55
+ projectSessions.set(tsConfigFilePath, session);
56
+ return session;
57
+ }
58
+ async function synchronizeSession(session) {
59
+ const currentConfigDigest = createConfigSnapshot(session.context.tsConfigFilePath).digest;
60
+ if (currentConfigDigest !== session.configDigest) {
61
+ session.context = buildProjectContext(session.context.tsConfigFilePath);
62
+ session.configDigest = currentConfigDigest;
63
+ return;
64
+ }
65
+ const { project, tsConfigFilePath } = session.context;
66
+ project.addSourceFilesFromTsConfig(tsConfigFilePath);
67
+ await Promise.all(project.getSourceFiles().map((sourceFile) => sourceFile.refreshFromFileSystem()));
68
+ }
69
+ export async function withProject(projectRoot, operation) {
70
+ const session = getOrCreateSession(projectRoot);
71
+ session.activeOperations += 1;
72
+ let release;
73
+ const previous = session.queue;
74
+ session.queue = new Promise((resolve) => {
75
+ release = resolve;
76
+ });
77
+ await previous;
78
+ try {
79
+ await synchronizeSession(session);
80
+ session.lastAccessedAt = Date.now();
81
+ return await operation(session.context);
82
+ }
83
+ finally {
84
+ session.activeOperations -= 1;
85
+ release?.();
86
+ }
87
+ }
88
+ // Compatibility API for internal scripts. MCP tools should use withProject().
89
+ export function getProject(projectRoot) {
90
+ return createFreshProject(projectRoot).project;
91
+ }
92
+ export function invalidateProject(projectRoot) {
93
+ const tsConfigFilePath = resolveTsConfigPath(projectRoot);
94
+ projectSessions.delete(tsConfigFilePath);
95
+ }
96
+ export function clearProjectSessions() {
97
+ projectSessions.clear();
98
+ }
99
+ export function getSourceFileOrThrow(project, filePath) {
100
+ const projectRoot = projectRoots.get(project) ?? process.cwd();
101
+ const normalizedInput = path.normalize(filePath);
102
+ const exactPath = path.isAbsolute(normalizedInput)
103
+ ? normalizedInput
104
+ : path.resolve(projectRoot, normalizedInput);
105
+ const sourceFiles = project.getSourceFiles();
106
+ const exact = sourceFiles.find((sourceFile) => path.normalize(sourceFile.getFilePath()) === exactPath);
107
+ if (exact)
108
+ return exact;
109
+ const suffix = normalizedInput.split(path.sep).join("/");
110
+ const candidates = sourceFiles.filter((sourceFile) => sourceFile.getFilePath().split(path.sep).join("/").endsWith(`/${suffix}`));
111
+ if (candidates.length === 1)
112
+ return candidates[0];
113
+ if (candidates.length > 1) {
114
+ const relativeCandidates = candidates
115
+ .map((sourceFile) => path.relative(projectRoot, sourceFile.getFilePath()))
116
+ .sort();
117
+ throw new Error(`Ambiguous source file "${filePath}". Use one of: ${relativeCandidates.join(", ")}.`);
118
+ }
119
+ throw new Error(`Source file "${filePath}" was not found in the project. Use a project-relative or absolute path returned by ast_list_files.`);
120
+ }
121
+ export function findDeclarationByName(sourceFile, symbolPath) {
122
+ return findDeclaration(sourceFile, symbolPath);
123
+ }
@@ -0,0 +1,75 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmod, lstat, mkdir, open, unlink } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ export function resolveRuntimeStateDirectory(options = {}) {
6
+ if (options.stateDirectory)
7
+ return path.resolve(options.stateDirectory);
8
+ if (process.env.AST_TOOL_STATE_DIR)
9
+ return path.resolve(process.env.AST_TOOL_STATE_DIR);
10
+ const base = process.env.XDG_STATE_HOME
11
+ ? path.resolve(process.env.XDG_STATE_HOME)
12
+ : path.join(os.homedir(), ".local", "state");
13
+ return path.join(base, "ast-tool");
14
+ }
15
+ export async function ensurePrivateDirectory(directory) {
16
+ await mkdir(directory, { recursive: true, mode: 0o700 });
17
+ if (process.platform !== "win32") {
18
+ const root = path.parse(directory).root;
19
+ let current = root;
20
+ for (const segment of path.relative(root, directory).split(path.sep).filter(Boolean)) {
21
+ current = path.join(current, segment);
22
+ const currentStat = await lstat(current);
23
+ if (currentStat.isSymbolicLink()) {
24
+ throw new Error(`Runtime state path must not traverse symbolic links: ${current}`);
25
+ }
26
+ }
27
+ const directoryStat = await lstat(directory);
28
+ if (!directoryStat.isDirectory()) {
29
+ throw new Error(`Runtime state path is not a directory: ${directory}`);
30
+ }
31
+ const uid = process.getuid?.();
32
+ if (uid !== undefined && directoryStat.uid !== uid) {
33
+ throw new Error(`Runtime state directory is not owned by the current user: ${directory}`);
34
+ }
35
+ await chmod(directory, 0o700);
36
+ }
37
+ }
38
+ export async function withWorkspaceFileLock(workspaceKey, options, callback) {
39
+ const locksDirectory = path.join(resolveRuntimeStateDirectory(options), "locks");
40
+ await ensurePrivateDirectory(locksDirectory);
41
+ const key = createHash("sha256").update(workspaceKey).digest("hex");
42
+ const lockPath = path.join(locksDirectory, `${key}.lock`);
43
+ let handle;
44
+ try {
45
+ handle = await open(lockPath, "wx", 0o600);
46
+ await handle.writeFile(JSON.stringify({
47
+ pid: process.pid,
48
+ workspace_key: workspaceKey,
49
+ phase: "applying",
50
+ created_at: new Date().toISOString(),
51
+ }), "utf8");
52
+ await handle.sync();
53
+ }
54
+ catch (error) {
55
+ if (handle) {
56
+ await handle.close().catch(() => undefined);
57
+ await unlink(lockPath).catch(() => undefined);
58
+ }
59
+ if (error.code === "EEXIST") {
60
+ throw new Error(`Another AST apply holds the workspace lock. Inspect and remove a stale lock only if no apply is running: ${lockPath}`, { cause: error });
61
+ }
62
+ throw error;
63
+ }
64
+ try {
65
+ return await callback();
66
+ }
67
+ finally {
68
+ try {
69
+ await handle.close();
70
+ }
71
+ finally {
72
+ await unlink(lockPath);
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,113 @@
1
+ export const AGENT_IDS = ["claude", "hermes"];
2
+ const AGENT_LABELS = {
3
+ claude: "Claude Code",
4
+ hermes: "Hermes",
5
+ };
6
+ function isAgentId(value) {
7
+ return AGENT_IDS.includes(value);
8
+ }
9
+ export function parseAgentsArgument(value) {
10
+ const normalized = value.trim().toLowerCase();
11
+ if (normalized === "") {
12
+ throw new Error("The --agents value cannot be empty.");
13
+ }
14
+ if (normalized === "all") {
15
+ return [...AGENT_IDS];
16
+ }
17
+ const requested = normalized
18
+ .split(",")
19
+ .map((item) => item.trim())
20
+ .filter(Boolean);
21
+ if (requested.length === 0) {
22
+ throw new Error("The --agents value cannot be empty.");
23
+ }
24
+ const unsupported = requested.find((item) => !isAgentId(item));
25
+ if (unsupported !== undefined) {
26
+ throw new Error(`Unsupported agent: ${unsupported}. Expected claude, hermes, or all.`);
27
+ }
28
+ const selected = new Set(requested);
29
+ return AGENT_IDS.filter((agent) => selected.has(agent));
30
+ }
31
+ export function applyAgentDeselections(answer, detections) {
32
+ const installed = detections.filter((item) => item.installed);
33
+ const selected = new Set(installed.map((item) => item.id));
34
+ const normalized = answer.trim().toLowerCase();
35
+ if (normalized === "") {
36
+ return AGENT_IDS.filter((agent) => selected.has(agent));
37
+ }
38
+ if (normalized === "none") {
39
+ return [];
40
+ }
41
+ const tokens = normalized
42
+ .split(/[\s,]+/)
43
+ .map((item) => item.trim())
44
+ .filter(Boolean);
45
+ for (const token of tokens) {
46
+ let detection;
47
+ if (/^\d+$/.test(token)) {
48
+ detection = detections[Number(token) - 1];
49
+ }
50
+ else if (isAgentId(token)) {
51
+ detection = detections.find((item) => item.id === token);
52
+ }
53
+ if (detection === undefined) {
54
+ throw new Error(`Invalid selection: ${token}.`);
55
+ }
56
+ if (!detection.installed) {
57
+ throw new Error(`${detection.label} is not installed.`);
58
+ }
59
+ selected.delete(detection.id);
60
+ }
61
+ return AGENT_IDS.filter((agent) => selected.has(agent));
62
+ }
63
+ function renderSelectionQuestion(detections, validationMessage) {
64
+ const choices = detections
65
+ .map((item, index) => {
66
+ const marker = item.installed ? "x" : " ";
67
+ const detail = item.installed
68
+ ? [item.version, item.executable].filter(Boolean).join(" · ")
69
+ : "not installed";
70
+ return ` [${marker}] ${index + 1}. ${AGENT_LABELS[item.id]} (${detail})`;
71
+ })
72
+ .join("\n");
73
+ const validation = validationMessage === undefined ? "" : `\n${validationMessage}\n`;
74
+ return `\nDetected agents (all installed agents are selected):\n${choices}\n${validation}\nEnter agents to deselect by number or name, 'none' to deselect all, or press Enter to keep all:\n> `;
75
+ }
76
+ function renderConfirmation(agents, validationMessage) {
77
+ const labels = agents.map((agent) => AGENT_LABELS[agent]).join(", ");
78
+ const validation = validationMessage === undefined ? "" : `\n${validationMessage}\n`;
79
+ return `${validation}\nSetup will install the MCP server and skill for: ${labels}.\nContinue? [Y/n] `;
80
+ }
81
+ export async function promptForAgentSelection(detections, ask) {
82
+ if (!detections.some((item) => item.installed)) {
83
+ throw new Error("No supported agents were detected in PATH.");
84
+ }
85
+ let selected;
86
+ let selectionError;
87
+ while (true) {
88
+ const answer = await ask(renderSelectionQuestion(detections, selectionError));
89
+ try {
90
+ selected = applyAgentDeselections(answer, detections);
91
+ break;
92
+ }
93
+ catch (error) {
94
+ selectionError = error instanceof Error ? error.message : String(error);
95
+ }
96
+ }
97
+ if (selected.length === 0) {
98
+ return [];
99
+ }
100
+ let confirmationError;
101
+ while (true) {
102
+ const answer = (await ask(renderConfirmation(selected, confirmationError)))
103
+ .trim()
104
+ .toLowerCase();
105
+ if (answer === "" || answer === "y" || answer === "yes") {
106
+ return selected;
107
+ }
108
+ if (answer === "n" || answer === "no") {
109
+ return [];
110
+ }
111
+ confirmationError = "Enter y or n.";
112
+ }
113
+ }