@juancr11/sibu 0.16.0 → 0.17.1
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 +66 -29
- package/bin/entrypoints/cli/create-program.js +1 -1
- package/bin/entrypoints/cli/execute-command.js +4 -7
- package/bin/modules/agent-tool-configuration/index.js +1 -0
- package/bin/modules/agent-tool-configuration/mcp-config.js +134 -0
- package/bin/modules/interactive-guidance/prompts.js +1 -1
- package/bin/modules/mcp-server-selection-management/list-mcp-servers/handler.js +2 -2
- package/bin/modules/mcp-server-selection-management/stop-mcp-server/handler.js +5 -5
- package/bin/modules/mcp-server-selection-management/use-mcp-server/handler.js +4 -3
- package/bin/modules/skill-selection-management/list-skills/handler.js +2 -2
- package/bin/modules/skill-selection-management/stop-managing-file/handler.js +5 -4
- package/bin/modules/skill-selection-management/use-skill/handler.js +5 -4
- package/bin/modules/{sync-review → sync-review-orchestrator}/action-prompt.js +14 -1
- package/bin/modules/{sync-review → sync-review-orchestrator}/apply-action.js +4 -3
- package/bin/modules/sync-review-orchestrator/handler.js +124 -0
- package/bin/modules/{sync-review → sync-review-orchestrator}/index.js +2 -0
- package/bin/modules/{sync-review → sync-review-orchestrator}/log-preview.js +1 -1
- package/bin/modules/{sync-review → sync-review-orchestrator}/sync-preview.js +3 -3
- package/bin/modules/sync-review-orchestrator/unsupported-agent-cleanup.js +57 -0
- package/bin/modules/{workflow-mutation-readiness → sync-review-orchestrator}/workflow-mutation-readiness.js +12 -3
- package/bin/modules/{workflow-target-planning → template-catalog}/catalog.js +83 -26
- package/bin/modules/{workflow-target-planning → template-catalog}/index.js +2 -1
- package/bin/modules/template-catalog/templates.js +154 -0
- package/bin/modules/workflow-configuration-manager/index.js +6 -0
- package/bin/modules/workflow-configuration-manager/list-mcp-servers/handler.js +46 -0
- package/bin/modules/workflow-configuration-manager/list-skills/command.js +1 -0
- package/bin/modules/workflow-configuration-manager/list-skills/handler.js +75 -0
- package/bin/modules/workflow-configuration-manager/stop-managing-file/command.js +1 -0
- package/bin/modules/workflow-configuration-manager/stop-managing-file/handler.js +221 -0
- package/bin/modules/workflow-configuration-manager/stop-mcp-server/command.js +1 -0
- package/bin/modules/workflow-configuration-manager/stop-mcp-server/handler.js +198 -0
- package/bin/modules/workflow-configuration-manager/use-mcp-server/command.js +1 -0
- package/bin/modules/workflow-configuration-manager/use-mcp-server/handler.js +193 -0
- package/bin/modules/workflow-configuration-manager/use-skill/command.js +1 -0
- package/bin/modules/workflow-configuration-manager/use-skill/handler.js +314 -0
- package/bin/modules/workflow-health-inspector/command.js +1 -0
- package/bin/modules/{workflow-health-diagnosis → workflow-health-inspector}/handler.js +7 -7
- package/bin/modules/workflow-installer/command.js +1 -0
- package/bin/modules/{project-adoption → workflow-installer}/handler.js +5 -4
- package/bin/modules/workflow-state-ledger/index.js +2 -0
- package/bin/modules/workflow-state-ledger/state-path.js +1 -0
- package/bin/modules/{workflow-state-registry → workflow-state-ledger}/state.js +40 -1
- package/bin/{modules/workflow-target-planning/workflow-targets.js → shared/expected-workflow-targets.js} +3 -103
- package/bin/shared/paths.js +1 -1
- package/bin/support/expected-workflow-targets.js +131 -0
- package/bin/support/interactive-guidance/index.js +1 -0
- package/bin/support/interactive-guidance/prompts.js +275 -0
- package/bin/support/version-advisory/index.js +1 -0
- package/bin/support/version-advisory/npm-version.js +205 -0
- package/package.json +2 -1
- package/templates/.claude/agents/sibu-implementation-executor.md +30 -0
- package/templates/.claude/agents/sibu-implementation-planner.md +30 -0
- package/templates/.codex/agents/sibu-implementation-executor.toml +30 -0
- package/templates/.codex/agents/sibu-implementation-planner.toml +30 -0
- package/templates/.gemini/agents/sibu-implementation-executor.md +30 -0
- package/templates/.gemini/agents/sibu-implementation-planner.md +30 -0
- package/templates/AGENTS.md +4 -2
- package/templates/manifest.json +94 -17
- package/templates/skills/ai-implementation-executor-toolbox/SKILL.md +78 -0
- package/templates/skills/ai-implementation-plan-executor/SKILL.md +64 -108
- package/templates/skills/ai-implementation-planner/SKILL.md +55 -138
- package/templates/skills/ai-implementation-planner-toolbox/SKILL.md +78 -0
- package/templates/skills/business-domain-model-writer/SKILL.md +313 -0
- package/templates/skills/capabilities-map-writer/SKILL.md +271 -0
- package/templates/skills/deep-module-map-writer/SKILL.md +50 -14
- package/templates/skills/feature-brief-writer/SKILL.md +72 -32
- package/templates/skills/technical-design-writer/SKILL.md +10 -10
- package/templates/skills/ux-expert/SKILL.md +62 -17
- package/bin/modules/sync-review/handler.js +0 -77
- package/bin/modules/template-catalog-rendering/index.js +0 -1
- package/bin/modules/template-catalog-rendering/templates.js +0 -163
- package/bin/modules/workflow-mutation-readiness/index.js +0 -1
- package/bin/modules/workflow-state-registry/index.js +0 -1
- /package/bin/{modules/project-adoption/command.js → entrypoints/cli-command-surface/index.js} +0 -0
- /package/bin/modules/{sync-review → sync-review-orchestrator}/command.js +0 -0
- /package/bin/modules/{workflow-health-diagnosis → workflow-configuration-manager/list-mcp-servers}/command.js +0 -0
- /package/bin/modules/{workflow-health-diagnosis → workflow-health-inspector}/index.js +0 -0
- /package/bin/modules/{project-adoption → workflow-installer}/index.js +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cancel, intro, isCancel, log, outro, select } from '@clack/prompts';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { STATE_RELATIVE_PATH } from '../../workflow-state-ledger/state-path.js';
|
|
6
|
+
import { resolveSelectableSkillById } from '../../template-catalog/index.js';
|
|
7
|
+
import { readFileHashIfPresent, sha256 } from '../../../shared/hash.js';
|
|
8
|
+
import { getProjectContext } from '../../../shared/paths.js';
|
|
9
|
+
import { renderIntro } from '../../../support/interactive-guidance/index.js';
|
|
10
|
+
import { cloneState, readStateForDoctor, writeStateFile } from '../../workflow-state-ledger/index.js';
|
|
11
|
+
import { getTemplateVersion, readTemplateManifest } from '../../template-catalog/index.js';
|
|
12
|
+
import { renderTemplateForSync } from '../../template-catalog/index.js';
|
|
13
|
+
import { removeUndefinedFields } from '../../../shared/object.js';
|
|
14
|
+
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedWorkflowSkillsFromState, getSkillTargetsForAgents, } from '../../template-catalog/index.js';
|
|
15
|
+
export async function handleStopManagingFile({ skillName }) {
|
|
16
|
+
await renderIntro();
|
|
17
|
+
intro(chalk.cyan('Updating skill management'));
|
|
18
|
+
const { rootPath, statePath } = getProjectContext();
|
|
19
|
+
const stateResult = readStateForDoctor(statePath);
|
|
20
|
+
if (!stateResult.ok) {
|
|
21
|
+
log.error(stateResult.message);
|
|
22
|
+
log.info('Run `sibu init` before managing workflow skill state.');
|
|
23
|
+
outro(chalk.yellow('Skill update unavailable.'));
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const result = stopSelectedSkill({ rootPath, state: stateResult.state, skillName });
|
|
28
|
+
switch (result.status) {
|
|
29
|
+
case 'blocked':
|
|
30
|
+
log.error(result.message);
|
|
31
|
+
if (result.hint) {
|
|
32
|
+
log.info(result.hint);
|
|
33
|
+
}
|
|
34
|
+
outro(chalk.yellow('Nothing changed.'));
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
return;
|
|
37
|
+
case 'noop':
|
|
38
|
+
log.success(result.message);
|
|
39
|
+
log.info('No files changed.');
|
|
40
|
+
outro(chalk.green('Skill selection is already up to date.'));
|
|
41
|
+
return;
|
|
42
|
+
case 'stopped':
|
|
43
|
+
writeStateFile(statePath, result.state);
|
|
44
|
+
for (const stoppedPath of result.stoppedPaths) {
|
|
45
|
+
log.success(`Stopped managing ${stoppedPath.relativePath}.`);
|
|
46
|
+
}
|
|
47
|
+
log.success('Updated AGENTS.md skill routing.');
|
|
48
|
+
log.success(`Updated ${STATE_RELATIVE_PATH}.`);
|
|
49
|
+
for (const [index, stoppedPath] of result.stoppedPaths.entries()) {
|
|
50
|
+
await askToDeleteStoppedFile(stoppedPath, result.stoppedFiles[index]);
|
|
51
|
+
}
|
|
52
|
+
outro(chalk.green(`Updated ${result.skillName}.`));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function stopSelectedSkill({ rootPath, state, skillName }) {
|
|
57
|
+
const resolution = resolveSelectableSkillById(skillName);
|
|
58
|
+
if (!resolution.ok) {
|
|
59
|
+
return { status: 'blocked', message: resolution.message, hint: 'Use `sibu skills stop <skill_name>` with a selectable skill id from `sibu skills list`.' };
|
|
60
|
+
}
|
|
61
|
+
if (!isSkillSelected(state, resolution.resolved)) {
|
|
62
|
+
return { status: 'noop', message: `${resolution.resolved.skill.name} is not selected.` };
|
|
63
|
+
}
|
|
64
|
+
const stoppedPaths = getSkillManagedPaths(rootPath, state, resolution.resolved);
|
|
65
|
+
if (stoppedPaths.length === 0) {
|
|
66
|
+
return { status: 'blocked', message: `${resolution.resolved.skill.name} does not have a managed target for the selected agents.` };
|
|
67
|
+
}
|
|
68
|
+
const nextState = cloneState(state);
|
|
69
|
+
const stoppedFiles = [];
|
|
70
|
+
for (const stoppedPath of stoppedPaths) {
|
|
71
|
+
const managedFile = nextState.managedFiles[stoppedPath.relativePath];
|
|
72
|
+
if (!managedFile) {
|
|
73
|
+
return {
|
|
74
|
+
status: 'blocked',
|
|
75
|
+
message: `${stoppedPath.relativePath} is not recorded in ${STATE_RELATIVE_PATH}.`,
|
|
76
|
+
hint: 'Run `sibu sync` to review workflow state before stopping this skill.',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const currentHash = readFileHashIfPresent(stoppedPath.absolutePath);
|
|
80
|
+
const stoppedFile = removeUndefinedFields({
|
|
81
|
+
...managedFile,
|
|
82
|
+
sha256: currentHash ?? managedFile.sha256,
|
|
83
|
+
status: 'unmanaged',
|
|
84
|
+
reason: 'Stopped by `sibu skills stop`.',
|
|
85
|
+
});
|
|
86
|
+
nextState.managedFiles[stoppedPath.relativePath] = stoppedFile;
|
|
87
|
+
stoppedFiles.push(stoppedFile);
|
|
88
|
+
}
|
|
89
|
+
removeSelectedSkill(nextState, resolution.resolved);
|
|
90
|
+
const agentsUpdate = getAgentsUpdate(rootPath, nextState);
|
|
91
|
+
if (!agentsUpdate.ok) {
|
|
92
|
+
return { status: 'blocked', message: agentsUpdate.message, hint: 'Run `sibu sync` to review workflow state before stopping this skill.' };
|
|
93
|
+
}
|
|
94
|
+
fs.writeFileSync(agentsUpdate.path, agentsUpdate.contents, 'utf8');
|
|
95
|
+
const manifest = readTemplateManifest();
|
|
96
|
+
nextState.templateVersion = manifest.templateVersion;
|
|
97
|
+
nextState.updatedAt = new Date().toISOString();
|
|
98
|
+
nextState.managedFiles['AGENTS.md'] = removeUndefinedFields({
|
|
99
|
+
...agentsUpdate.managedFile,
|
|
100
|
+
templateVersion: getTemplateVersion(manifest, agentsUpdate.managedFile.template),
|
|
101
|
+
sha256: sha256(agentsUpdate.contents),
|
|
102
|
+
status: agentsUpdate.managedFile.status ?? 'managed',
|
|
103
|
+
});
|
|
104
|
+
return { status: 'stopped', state: nextState, stoppedPaths, stoppedFiles, skillName: resolution.resolved.skill.name };
|
|
105
|
+
}
|
|
106
|
+
function isSkillSelected(state, resolved) {
|
|
107
|
+
switch (resolved.kind) {
|
|
108
|
+
case 'language':
|
|
109
|
+
return state.selectedLanguageSkills?.includes(resolved.skill.id) ?? false;
|
|
110
|
+
case 'framework':
|
|
111
|
+
return state.selectedFrameworkSkills?.includes(resolved.skill.id) ?? false;
|
|
112
|
+
case 'architecture':
|
|
113
|
+
return state.selectedArchitectureSkill === resolved.skill.id;
|
|
114
|
+
case 'database':
|
|
115
|
+
return state.selectedDatabaseSkills?.includes(resolved.skill.id) ?? false;
|
|
116
|
+
case 'workflow':
|
|
117
|
+
return state.selectedWorkflowSkills?.includes(resolved.skill.id) ?? false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function removeSelectedSkill(state, resolved) {
|
|
121
|
+
switch (resolved.kind) {
|
|
122
|
+
case 'language':
|
|
123
|
+
state.selectedLanguageSkills = (state.selectedLanguageSkills ?? []).filter((skillId) => skillId !== resolved.skill.id);
|
|
124
|
+
return;
|
|
125
|
+
case 'framework':
|
|
126
|
+
state.selectedFrameworkSkills = (state.selectedFrameworkSkills ?? []).filter((skillId) => skillId !== resolved.skill.id);
|
|
127
|
+
return;
|
|
128
|
+
case 'architecture':
|
|
129
|
+
delete state.selectedArchitectureSkill;
|
|
130
|
+
return;
|
|
131
|
+
case 'database':
|
|
132
|
+
state.selectedDatabaseSkills = (state.selectedDatabaseSkills ?? []).filter((skillId) => skillId !== resolved.skill.id);
|
|
133
|
+
return;
|
|
134
|
+
case 'workflow':
|
|
135
|
+
state.selectedWorkflowSkills = (state.selectedWorkflowSkills ?? []).filter((skillId) => skillId !== resolved.skill.id);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function getSkillManagedPaths(rootPath, state, resolved) {
|
|
140
|
+
const relativePaths = new Set(getSkillTargetsForAgents(resolved.skill, getSelectedAgentsFromState(state)).map((target) => target.targetRelativePath));
|
|
141
|
+
return [...relativePaths].map((relativePath) => ({
|
|
142
|
+
relativePath,
|
|
143
|
+
absolutePath: path.join(rootPath, relativePath),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
function getAgentsUpdate(rootPath, state) {
|
|
147
|
+
const agentsRelativePath = 'AGENTS.md';
|
|
148
|
+
const agentsPath = path.join(rootPath, agentsRelativePath);
|
|
149
|
+
const managedFile = state.managedFiles[agentsRelativePath];
|
|
150
|
+
if (!managedFile) {
|
|
151
|
+
return { ok: false, message: 'AGENTS.md is not recorded in `.sibu/state.json`.' };
|
|
152
|
+
}
|
|
153
|
+
if (!fs.existsSync(agentsPath)) {
|
|
154
|
+
return { ok: false, message: 'AGENTS.md is missing.' };
|
|
155
|
+
}
|
|
156
|
+
const currentHash = sha256(fs.readFileSync(agentsPath, 'utf8'));
|
|
157
|
+
if (currentHash !== managedFile.sha256) {
|
|
158
|
+
return { ok: false, message: 'AGENTS.md has changed since Sibu last recorded it.' };
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
path: agentsPath,
|
|
163
|
+
managedFile,
|
|
164
|
+
contents: renderTemplateForSync({
|
|
165
|
+
templateRelativePath: managedFile.template,
|
|
166
|
+
currentPath: agentsPath,
|
|
167
|
+
selectedLanguageSkills: getSelectedLanguageSkillsFromState(state),
|
|
168
|
+
selectedFrameworkSkills: getSelectedFrameworkSkillsFromState(state),
|
|
169
|
+
selectedArchitectureSkill: getSelectedArchitectureSkillFromState(state),
|
|
170
|
+
selectedWorkflowSkills: getSelectedWorkflowSkillsFromState(state),
|
|
171
|
+
selectedDatabaseSkills: getSelectedDatabaseSkillsFromState(state),
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async function askToDeleteStoppedFile(managedPath, managedFile) {
|
|
176
|
+
if (!fs.existsSync(managedPath.absolutePath)) {
|
|
177
|
+
log.info(`${managedPath.relativePath} does not exist locally, so there is nothing to delete.`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!fs.lstatSync(managedPath.absolutePath).isFile()) {
|
|
181
|
+
log.warn(`${managedPath.relativePath} is not a regular file. I will not delete it.`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const deleteAction = await select({
|
|
185
|
+
message: `Do you also want to delete ${managedPath.relativePath} for good?`,
|
|
186
|
+
options: [
|
|
187
|
+
{ value: 'keep', label: 'Keep file', hint: 'Recommended. Leave the local file unchanged.' },
|
|
188
|
+
{ value: 'delete', label: 'Delete file', hint: 'Permanently remove this file from the project.' },
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
if (isCancel(deleteAction)) {
|
|
192
|
+
cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
if (deleteAction === 'keep') {
|
|
196
|
+
log.info(`Kept ${managedPath.relativePath}.`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (hasLocalEdits(managedPath.absolutePath, managedFile)) {
|
|
200
|
+
const confirmDelete = await select({
|
|
201
|
+
message: `${managedPath.relativePath} differs from the last Sibu-recorded hash. Delete it anyway?`,
|
|
202
|
+
options: [
|
|
203
|
+
{ value: 'keep', label: 'Keep file', hint: 'Recommended. Preserve local edits.' },
|
|
204
|
+
{ value: 'delete', label: 'Delete anyway', hint: 'Permanently remove the edited file.' },
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
if (isCancel(confirmDelete)) {
|
|
208
|
+
cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
if (confirmDelete === 'keep') {
|
|
212
|
+
log.info(`Kept ${managedPath.relativePath}.`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
fs.unlinkSync(managedPath.absolutePath);
|
|
217
|
+
log.success(`Deleted ${managedPath.relativePath}.`);
|
|
218
|
+
}
|
|
219
|
+
function hasLocalEdits(filePath, managedFile) {
|
|
220
|
+
return readFileHashIfPresent(filePath) !== managedFile.sha256;
|
|
221
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cancel, isCancel, log, select } from '@clack/prompts';
|
|
4
|
+
import { STATE_RELATIVE_PATH } from '../../workflow-state-ledger/state-path.js';
|
|
5
|
+
import { readFileHashIfPresent, sha256 } from '../../../shared/hash.js';
|
|
6
|
+
import { removeUndefinedFields } from '../../../shared/object.js';
|
|
7
|
+
import { getProjectContext } from '../../../shared/paths.js';
|
|
8
|
+
import { renderMissingWorkflowFiles, resolveSelectableMcpServerById } from '../../template-catalog/index.js';
|
|
9
|
+
import { getWorkflowMutationReadiness } from '../../sync-review-orchestrator/index.js';
|
|
10
|
+
import { getTemplateVersion, readTemplateManifest } from '../../template-catalog/index.js';
|
|
11
|
+
import { cloneState, writeStateFile } from '../../workflow-state-ledger/index.js';
|
|
12
|
+
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowTargets, } from '../../template-catalog/index.js';
|
|
13
|
+
export async function handleStopMcpServer(command) {
|
|
14
|
+
const { rootPath, statePath } = getProjectContext();
|
|
15
|
+
const readiness = getWorkflowMutationReadiness({ rootPath, statePath });
|
|
16
|
+
if (!readiness.ok) {
|
|
17
|
+
log.error(readiness.message);
|
|
18
|
+
log.info(readiness.hint);
|
|
19
|
+
for (const preview of readiness.actionablePreviews?.slice(0, 3) ?? []) {
|
|
20
|
+
log.info(`${preview.relativePath}: ${preview.status}`);
|
|
21
|
+
}
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const result = stopSelectedMcpServer({ rootPath, state: readiness.state, serverId: command.serverId });
|
|
26
|
+
switch (result.status) {
|
|
27
|
+
case 'blocked':
|
|
28
|
+
log.error(result.message);
|
|
29
|
+
if (result.hint) {
|
|
30
|
+
log.info(result.hint);
|
|
31
|
+
}
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
case 'noop':
|
|
35
|
+
log.success(result.message);
|
|
36
|
+
log.info('No files changed.');
|
|
37
|
+
return;
|
|
38
|
+
case 'stopped':
|
|
39
|
+
writeStateFile(statePath, result.state);
|
|
40
|
+
log.success(`Updated ${STATE_RELATIVE_PATH}`);
|
|
41
|
+
log.success(`Stopped ${result.serverName}.`);
|
|
42
|
+
for (const [index, stoppedPath] of result.stoppedPaths.entries()) {
|
|
43
|
+
await askToDeleteStoppedMcpFile(stoppedPath, result.stoppedFiles[index]);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function getNextStoppedMcpSelection(state, serverId) {
|
|
49
|
+
const resolution = resolveSelectableMcpServerById(serverId);
|
|
50
|
+
if (!resolution.ok) {
|
|
51
|
+
return { status: 'blocked', message: resolution.message };
|
|
52
|
+
}
|
|
53
|
+
const selectedMcpServerIds = [...(state.selectedMcpServers ?? [])];
|
|
54
|
+
if (!selectedMcpServerIds.includes(resolution.resolved.server.id)) {
|
|
55
|
+
return { status: 'noop', message: `${resolution.resolved.server.name} is not selected.` };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
status: 'selected',
|
|
59
|
+
serverName: resolution.resolved.server.name,
|
|
60
|
+
remainingMcpServers: selectedMcpServerIds.filter((selectedServerId) => selectedServerId !== resolution.resolved.server.id).map(getMcpServerById),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function stopSelectedMcpServer({ rootPath, state, serverId }) {
|
|
64
|
+
const selectionResult = getNextStoppedMcpSelection(state, serverId);
|
|
65
|
+
if (selectionResult.status !== 'selected') {
|
|
66
|
+
return selectionResult;
|
|
67
|
+
}
|
|
68
|
+
const selectedAgents = getSelectedAgentsFromState(state);
|
|
69
|
+
const selectedLanguageSkills = getSelectedLanguageSkillsFromState(state);
|
|
70
|
+
const selectedFrameworkSkills = getSelectedFrameworkSkillsFromState(state);
|
|
71
|
+
const selectedArchitectureSkill = getSelectedArchitectureSkillFromState(state);
|
|
72
|
+
const selectedWorkflowSkills = getSelectedWorkflowSkillsFromState(state);
|
|
73
|
+
const selectedDatabaseSkills = getSelectedDatabaseSkillsFromState(state);
|
|
74
|
+
const previousTargets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, getSelectedMcpServersFromState(state));
|
|
75
|
+
const nextTargets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectionResult.remainingMcpServers);
|
|
76
|
+
const manifest = readTemplateManifest();
|
|
77
|
+
const nextState = cloneState(state);
|
|
78
|
+
const nextTargetsByPath = new Map(nextTargets.map((target) => [target.targetPath, target]));
|
|
79
|
+
const stoppedPaths = [];
|
|
80
|
+
const stoppedFiles = [];
|
|
81
|
+
nextState.selectedMcpServers = selectionResult.remainingMcpServers.map((server) => server.id);
|
|
82
|
+
if (serverId === 'notion' && nextState.mcpServerConfigs?.notion) {
|
|
83
|
+
const { notion: _notion, ...remainingMcpServerConfigs } = nextState.mcpServerConfigs;
|
|
84
|
+
nextState.mcpServerConfigs = Object.keys(remainingMcpServerConfigs).length > 0 ? remainingMcpServerConfigs : undefined;
|
|
85
|
+
}
|
|
86
|
+
nextState.templateVersion = manifest.templateVersion;
|
|
87
|
+
nextState.updatedAt = new Date().toISOString();
|
|
88
|
+
for (const previousTarget of previousTargets.filter((target) => target.mcpConfigAgentId)) {
|
|
89
|
+
const relativePath = path.relative(rootPath, previousTarget.targetPath);
|
|
90
|
+
const managedFile = nextState.managedFiles[relativePath];
|
|
91
|
+
if (!managedFile) {
|
|
92
|
+
return {
|
|
93
|
+
status: 'blocked',
|
|
94
|
+
message: `${relativePath} is not recorded in ${STATE_RELATIVE_PATH}.`,
|
|
95
|
+
hint: 'Run `sibu sync` to review workflow state before stopping this MCP server.',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const nextTarget = nextTargetsByPath.get(previousTarget.targetPath);
|
|
99
|
+
if (nextTarget) {
|
|
100
|
+
const [file] = renderMissingWorkflowFiles({
|
|
101
|
+
missingTargets: [nextTarget],
|
|
102
|
+
selectedLanguageSkills,
|
|
103
|
+
selectedFrameworkSkills,
|
|
104
|
+
selectedArchitectureSkill,
|
|
105
|
+
selectedWorkflowSkills,
|
|
106
|
+
selectedDatabaseSkills,
|
|
107
|
+
selectedMcpServers: selectionResult.remainingMcpServers,
|
|
108
|
+
});
|
|
109
|
+
fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
|
|
110
|
+
fs.writeFileSync(file.targetPath, file.contents, 'utf8');
|
|
111
|
+
nextState.managedFiles[relativePath] = removeUndefinedFields({
|
|
112
|
+
...managedFile,
|
|
113
|
+
templateVersion: getTemplateVersion(manifest, managedFile.template),
|
|
114
|
+
sha256: sha256(file.contents),
|
|
115
|
+
status: managedFile.status ?? 'managed',
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const stoppedFile = removeUndefinedFields({
|
|
120
|
+
...managedFile,
|
|
121
|
+
templateVersion: getTemplateVersion(manifest, managedFile.template),
|
|
122
|
+
sha256: readFileHashIfPresent(previousTarget.targetPath) ?? managedFile.sha256,
|
|
123
|
+
status: 'unmanaged',
|
|
124
|
+
reason: 'Stopped by `sibu mcp stop`.',
|
|
125
|
+
});
|
|
126
|
+
nextState.managedFiles[relativePath] = stoppedFile;
|
|
127
|
+
stoppedPaths.push({ relativePath, absolutePath: previousTarget.targetPath });
|
|
128
|
+
stoppedFiles.push(stoppedFile);
|
|
129
|
+
}
|
|
130
|
+
return { status: 'stopped', state: nextState, stoppedPaths, stoppedFiles, serverName: selectionResult.serverName };
|
|
131
|
+
}
|
|
132
|
+
export async function askToDeleteStoppedMcpFile(managedPath, managedFile) {
|
|
133
|
+
if (!fs.existsSync(managedPath.absolutePath)) {
|
|
134
|
+
log.info(`${managedPath.relativePath} does not exist locally, so there is nothing to delete.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!fs.lstatSync(managedPath.absolutePath).isFile()) {
|
|
138
|
+
log.warn(`${managedPath.relativePath} is not a regular file. I will not delete it.`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const deleteAction = await select({
|
|
142
|
+
message: `Do you also want to delete ${managedPath.relativePath} for good?`,
|
|
143
|
+
options: [
|
|
144
|
+
{ value: 'keep', label: 'Keep file', hint: 'Recommended. Leave the local MCP config file unchanged.' },
|
|
145
|
+
{ value: 'delete', label: 'Delete file', hint: 'Permanently remove this file from the project.' },
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
if (isCancel(deleteAction)) {
|
|
149
|
+
cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
const confirmDelete = deleteAction === 'delete' && hasLocalEdits(managedPath.absolutePath, managedFile) ? await askToConfirmEditedDelete(managedPath) : undefined;
|
|
153
|
+
applyStoppedMcpFileDeleteDecision({ managedPath, managedFile, deleteAction, confirmDelete });
|
|
154
|
+
}
|
|
155
|
+
export function applyStoppedMcpFileDeleteDecision({ managedPath, managedFile, deleteAction, confirmDelete, }) {
|
|
156
|
+
if (!fs.existsSync(managedPath.absolutePath)) {
|
|
157
|
+
log.info(`${managedPath.relativePath} does not exist locally, so there is nothing to delete.`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!fs.lstatSync(managedPath.absolutePath).isFile()) {
|
|
161
|
+
log.warn(`${managedPath.relativePath} is not a regular file. I will not delete it.`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (deleteAction === 'keep') {
|
|
165
|
+
log.info(`Kept ${managedPath.relativePath}.`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (hasLocalEdits(managedPath.absolutePath, managedFile) && confirmDelete !== 'delete') {
|
|
169
|
+
log.info(`Kept ${managedPath.relativePath}.`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
fs.unlinkSync(managedPath.absolutePath);
|
|
173
|
+
log.success(`Deleted ${managedPath.relativePath}.`);
|
|
174
|
+
}
|
|
175
|
+
async function askToConfirmEditedDelete(managedPath) {
|
|
176
|
+
const confirmDelete = await select({
|
|
177
|
+
message: `${managedPath.relativePath} differs from the last Sibu-recorded hash. Delete it anyway?`,
|
|
178
|
+
options: [
|
|
179
|
+
{ value: 'keep', label: 'Keep file', hint: 'Recommended. Preserve local edits.' },
|
|
180
|
+
{ value: 'delete', label: 'Delete anyway', hint: 'Permanently remove the edited file.' },
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
if (isCancel(confirmDelete)) {
|
|
184
|
+
cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
return confirmDelete;
|
|
188
|
+
}
|
|
189
|
+
function hasLocalEdits(filePath, managedFile) {
|
|
190
|
+
return readFileHashIfPresent(filePath) !== managedFile.sha256;
|
|
191
|
+
}
|
|
192
|
+
function getMcpServerById(serverId) {
|
|
193
|
+
const resolution = resolveSelectableMcpServerById(serverId);
|
|
194
|
+
if (!resolution.ok) {
|
|
195
|
+
throw new Error(`Unsupported MCP server in state: ${serverId}`);
|
|
196
|
+
}
|
|
197
|
+
return resolution.resolved.server;
|
|
198
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { writeSibuState } from '../../workflow-state-ledger/index.js';
|
|
4
|
+
import { log } from '@clack/prompts';
|
|
5
|
+
import { STATE_RELATIVE_PATH } from '../../workflow-state-ledger/state-path.js';
|
|
6
|
+
import { sha256 } from '../../../shared/hash.js';
|
|
7
|
+
import { getProjectContext } from '../../../shared/paths.js';
|
|
8
|
+
import { askForNotionDocsParentPage } from '../../../support/interactive-guidance/index.js';
|
|
9
|
+
import { renderTemplateForSync } from '../../template-catalog/index.js';
|
|
10
|
+
import { getWorkflowMutationReadiness } from '../../sync-review-orchestrator/index.js';
|
|
11
|
+
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowSkillsImpliedByMcpServers, getWorkflowTargets, renderMissingWorkflowFiles, resolveSelectableMcpServerById, } from '../../template-catalog/index.js';
|
|
12
|
+
const defaultDependencies = {
|
|
13
|
+
askForNotionDocsParentPage,
|
|
14
|
+
};
|
|
15
|
+
export async function handleUseMcpServer(command, dependencies = defaultDependencies) {
|
|
16
|
+
const { rootPath, statePath } = getProjectContext();
|
|
17
|
+
const readiness = getWorkflowMutationReadiness({ rootPath, statePath });
|
|
18
|
+
if (!readiness.ok) {
|
|
19
|
+
log.error(readiness.message);
|
|
20
|
+
log.info(readiness.hint);
|
|
21
|
+
for (const preview of readiness.actionablePreviews?.slice(0, 3) ?? []) {
|
|
22
|
+
log.info(`${preview.relativePath}: ${preview.status}`);
|
|
23
|
+
}
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const selectionResult = getNextMcpSelection(readiness.state, command.serverId);
|
|
28
|
+
switch (selectionResult.status) {
|
|
29
|
+
case 'noop':
|
|
30
|
+
log.success(selectionResult.message);
|
|
31
|
+
log.info('No files changed.');
|
|
32
|
+
return;
|
|
33
|
+
case 'blocked':
|
|
34
|
+
log.error(selectionResult.message);
|
|
35
|
+
if (selectionResult.hint) {
|
|
36
|
+
log.info(selectionResult.hint);
|
|
37
|
+
}
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
case 'selected': {
|
|
41
|
+
const mcpServerConfigs = await getNextMcpServerConfigs({ state: readiness.state, selectionResult, dependencies });
|
|
42
|
+
applySelectedMcpServer({ rootPath, statePath, state: readiness.state, selectionResult, mcpServerConfigs });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function getNextMcpSelection(state, serverId) {
|
|
48
|
+
const resolution = resolveSelectableMcpServerById(serverId);
|
|
49
|
+
if (!resolution.ok) {
|
|
50
|
+
return { status: 'blocked', message: resolution.message };
|
|
51
|
+
}
|
|
52
|
+
const selectedMcpServerIds = [...(state.selectedMcpServers ?? [])];
|
|
53
|
+
if (selectedMcpServerIds.includes(resolution.resolved.server.id)) {
|
|
54
|
+
return { status: 'noop', message: `${resolution.resolved.server.name} is already selected.` };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
status: 'selected',
|
|
58
|
+
serverName: resolution.resolved.server.name,
|
|
59
|
+
selectedMcpServers: [...selectedMcpServerIds, resolution.resolved.server.id].map(getMcpServerById),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function getNextMcpServerConfigs({ state, selectionResult, dependencies, }) {
|
|
63
|
+
if (!selectionResult.selectedMcpServers.some((server) => server.id === 'notion') || state.selectedMcpServers?.includes('notion')) {
|
|
64
|
+
return state.mcpServerConfigs;
|
|
65
|
+
}
|
|
66
|
+
const docsParentPage = await dependencies.askForNotionDocsParentPage();
|
|
67
|
+
return {
|
|
68
|
+
...state.mcpServerConfigs,
|
|
69
|
+
notion: { docsParentPage },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function applySelectedMcpServer({ rootPath, statePath, state, selectionResult, mcpServerConfigs, }) {
|
|
73
|
+
const selectedAgents = getSelectedAgentsFromState(state);
|
|
74
|
+
const selectedLanguageSkills = getSelectedLanguageSkillsFromState(state);
|
|
75
|
+
const selectedFrameworkSkills = getSelectedFrameworkSkillsFromState(state);
|
|
76
|
+
const selectedArchitectureSkill = getSelectedArchitectureSkillFromState(state);
|
|
77
|
+
const currentWorkflowSkills = getSelectedWorkflowSkillsFromState(state);
|
|
78
|
+
const impliedWorkflowSkills = getWorkflowSkillsImpliedByMcpServers(selectionResult.selectedMcpServers.map((server) => server.id));
|
|
79
|
+
const selectedWorkflowSkills = mergeWorkflowSkills(currentWorkflowSkills, impliedWorkflowSkills);
|
|
80
|
+
const selectedDatabaseSkills = getSelectedDatabaseSkillsFromState(state);
|
|
81
|
+
const previousTargets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, currentWorkflowSkills, selectedDatabaseSkills, getSelectedMcpServersFromState(state));
|
|
82
|
+
const targets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectionResult.selectedMcpServers);
|
|
83
|
+
const affectedTargets = getAffectedTargets(previousTargets, targets);
|
|
84
|
+
const preflightError = getMcpUsePreflightError({ rootPath, state, affectedTargets, previousTargets });
|
|
85
|
+
if (preflightError) {
|
|
86
|
+
log.error(preflightError);
|
|
87
|
+
log.info('Run `sibu sync` to review workflow state before selecting an MCP server.');
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const agentsTarget = targets.find((target) => target.label === 'AGENTS.md');
|
|
92
|
+
if (!agentsTarget) {
|
|
93
|
+
throw new Error('AGENTS.md target is missing from workflow targets.');
|
|
94
|
+
}
|
|
95
|
+
const nonAgentTargets = affectedTargets.filter((target) => target.label !== 'AGENTS.md');
|
|
96
|
+
const files = renderMissingWorkflowFiles({
|
|
97
|
+
missingTargets: nonAgentTargets,
|
|
98
|
+
selectedLanguageSkills,
|
|
99
|
+
selectedFrameworkSkills,
|
|
100
|
+
selectedArchitectureSkill,
|
|
101
|
+
selectedWorkflowSkills,
|
|
102
|
+
selectedDatabaseSkills,
|
|
103
|
+
selectedMcpServers: selectionResult.selectedMcpServers,
|
|
104
|
+
});
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
const fileAlreadyExists = fs.existsSync(file.targetPath);
|
|
107
|
+
fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
|
|
108
|
+
fs.writeFileSync(file.targetPath, file.contents, fileAlreadyExists ? 'utf8' : { encoding: 'utf8', flag: 'wx' });
|
|
109
|
+
log.success(`${fileAlreadyExists ? 'Updated' : 'Created'} ${file.label}`);
|
|
110
|
+
}
|
|
111
|
+
if (affectedTargets.some((target) => target.label === 'AGENTS.md')) {
|
|
112
|
+
const agentsContents = renderTemplateForSync({
|
|
113
|
+
templateRelativePath: agentsTarget.templateRelativePath,
|
|
114
|
+
currentPath: agentsTarget.targetPath,
|
|
115
|
+
selectedLanguageSkills,
|
|
116
|
+
selectedFrameworkSkills,
|
|
117
|
+
selectedArchitectureSkill,
|
|
118
|
+
selectedWorkflowSkills,
|
|
119
|
+
selectedDatabaseSkills,
|
|
120
|
+
selectedMcpServers: selectionResult.selectedMcpServers,
|
|
121
|
+
});
|
|
122
|
+
fs.writeFileSync(agentsTarget.targetPath, agentsContents, 'utf8');
|
|
123
|
+
log.success('Updated AGENTS.md skill routing');
|
|
124
|
+
}
|
|
125
|
+
writeSibuState({
|
|
126
|
+
rootPath,
|
|
127
|
+
statePath,
|
|
128
|
+
selectedAgents,
|
|
129
|
+
selectedLanguageSkills,
|
|
130
|
+
selectedFrameworkSkills,
|
|
131
|
+
selectedArchitectureSkill,
|
|
132
|
+
selectedWorkflowSkills,
|
|
133
|
+
selectedDatabaseSkills,
|
|
134
|
+
selectedMcpServers: selectionResult.selectedMcpServers,
|
|
135
|
+
mcpServerConfigs,
|
|
136
|
+
targets,
|
|
137
|
+
});
|
|
138
|
+
log.success(`Updated ${STATE_RELATIVE_PATH}`);
|
|
139
|
+
log.success(`Added ${selectionResult.serverName}.`);
|
|
140
|
+
for (const skill of impliedWorkflowSkills.filter((skill) => !currentWorkflowSkills.some((currentSkill) => currentSkill.id === skill.id))) {
|
|
141
|
+
log.success(`Added ${skill.name}.`);
|
|
142
|
+
}
|
|
143
|
+
log.info('Sibu configured MCP files only; prerequisites, credentials, and provider authentication remain user-owned.');
|
|
144
|
+
}
|
|
145
|
+
function getAffectedTargets(previousTargets, targets) {
|
|
146
|
+
const previousTargetPaths = new Set(previousTargets.map((target) => target.targetPath));
|
|
147
|
+
const newTargets = targets.filter((target) => !previousTargetPaths.has(target.targetPath) && (target.targetKind === 'skill' || target.targetKind === 'mcp-config'));
|
|
148
|
+
const mcpConfigTargets = targets.filter((target) => target.mcpConfigAgentId);
|
|
149
|
+
const agentsTarget = targets.find((target) => target.label === 'AGENTS.md');
|
|
150
|
+
const affectedTargets = new Map();
|
|
151
|
+
for (const target of [...mcpConfigTargets, ...newTargets, ...(newTargets.some((target) => target.targetKind === 'skill') && agentsTarget ? [agentsTarget] : [])]) {
|
|
152
|
+
affectedTargets.set(target.targetPath, target);
|
|
153
|
+
}
|
|
154
|
+
return [...affectedTargets.values()];
|
|
155
|
+
}
|
|
156
|
+
function mergeWorkflowSkills(...skillGroups) {
|
|
157
|
+
const skillsById = new Map();
|
|
158
|
+
for (const skill of skillGroups.flat()) {
|
|
159
|
+
skillsById.set(skill.id, skill);
|
|
160
|
+
}
|
|
161
|
+
return [...skillsById.values()];
|
|
162
|
+
}
|
|
163
|
+
function getMcpUsePreflightError({ rootPath, state, affectedTargets, previousTargets, }) {
|
|
164
|
+
const previousTargetPaths = new Set(previousTargets.map((target) => target.targetPath));
|
|
165
|
+
for (const target of affectedTargets) {
|
|
166
|
+
const relativePath = path.relative(rootPath, target.targetPath);
|
|
167
|
+
if (previousTargetPaths.has(target.targetPath)) {
|
|
168
|
+
const managedFile = state.managedFiles[relativePath];
|
|
169
|
+
if (!managedFile) {
|
|
170
|
+
return `${relativePath} is not recorded in ${STATE_RELATIVE_PATH}.`;
|
|
171
|
+
}
|
|
172
|
+
if (!fs.existsSync(target.targetPath)) {
|
|
173
|
+
return `${relativePath} is missing.`;
|
|
174
|
+
}
|
|
175
|
+
const currentHash = sha256(fs.readFileSync(target.targetPath, 'utf8'));
|
|
176
|
+
if (currentHash !== managedFile.sha256) {
|
|
177
|
+
return `${relativePath} has changed since Sibu last recorded it.`;
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (fs.existsSync(target.targetPath)) {
|
|
182
|
+
return `${relativePath} already exists but is not recorded for this MCP selection.`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
function getMcpServerById(serverId) {
|
|
188
|
+
const resolution = resolveSelectableMcpServerById(serverId);
|
|
189
|
+
if (!resolution.ok) {
|
|
190
|
+
throw new Error(`Unsupported MCP server in state: ${serverId}`);
|
|
191
|
+
}
|
|
192
|
+
return resolution.resolved.server;
|
|
193
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|