@juancr11/sibu 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -0
- package/bin/entrypoints/cli/create-program.js +13 -0
- package/bin/entrypoints/cli/execute-command.js +10 -0
- package/bin/modules/interactive-guidance/index.js +1 -1
- package/bin/modules/interactive-guidance/prompts.js +18 -1
- package/bin/modules/mcp-server-selection-management/index.js +3 -0
- package/bin/modules/mcp-server-selection-management/list-mcp-servers/command.js +1 -0
- package/bin/modules/mcp-server-selection-management/list-mcp-servers/handler.js +46 -0
- package/bin/modules/mcp-server-selection-management/stop-mcp-server/command.js +1 -0
- package/bin/modules/mcp-server-selection-management/stop-mcp-server/handler.js +194 -0
- package/bin/modules/mcp-server-selection-management/use-mcp-server/command.js +1 -0
- package/bin/modules/mcp-server-selection-management/use-mcp-server/handler.js +127 -0
- package/bin/modules/project-adoption/handler.js +36 -12
- package/bin/modules/sync-review/apply-action.js +3 -1
- package/bin/modules/sync-review/sync-preview.js +16 -6
- package/bin/modules/template-catalog-rendering/index.js +1 -1
- package/bin/modules/template-catalog-rendering/templates.js +56 -1
- package/bin/modules/workflow-health-diagnosis/handler.js +8 -3
- package/bin/modules/workflow-state-registry/state.js +2 -0
- package/bin/modules/workflow-target-planning/catalog.js +15 -0
- package/bin/modules/workflow-target-planning/index.js +2 -2
- package/bin/modules/workflow-target-planning/workflow-targets.js +74 -6
- package/package.json +1 -1
- package/templates/manifest.json +20 -6
- package/templates/mcp/claude/.mcp.json +3 -0
- package/templates/mcp/gemini/settings.json +3 -0
- package/templates/skills/scrum-master-planner/SKILL.md +22 -0
package/README.md
CHANGED
|
@@ -42,6 +42,95 @@ Use `sibu skills list` to list available workflow skills and see which selectabl
|
|
|
42
42
|
|
|
43
43
|
Use `sibu skills stop <file>` to stop managing an Sibu-tracked workflow file. The file is marked as `unmanaged` in `.sibu/state.json`, removed from the selected skill state when applicable, and the CLI asks whether to keep or delete the local file.
|
|
44
44
|
|
|
45
|
+
## MCP server setup
|
|
46
|
+
|
|
47
|
+
Sibu can generate MCP server configuration for supported agents. The first supported server is GitHub's official MCP server.
|
|
48
|
+
|
|
49
|
+
Sibu only writes and tracks MCP config files. Runtime prerequisites, credentials, and provider authentication remain user-owned.
|
|
50
|
+
|
|
51
|
+
### 1. Confirm available MCP servers
|
|
52
|
+
|
|
53
|
+
From a Sibu-managed project, run:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
sibu mcp list
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This shows each available MCP server and whether it is already selected for the project.
|
|
60
|
+
|
|
61
|
+
### 2. Configure the GitHub MCP server
|
|
62
|
+
|
|
63
|
+
You can select GitHub during `sibu init`, or add it later:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
sibu mcp use github
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Sibu writes the relevant config for the agents selected in the project:
|
|
70
|
+
|
|
71
|
+
- Codex: `.codex/config.toml`
|
|
72
|
+
- Claude: `.mcp.json`
|
|
73
|
+
- Gemini: `.gemini/settings.json`
|
|
74
|
+
|
|
75
|
+
All supported agents use GitHub's hosted MCP endpoint instead of requiring Docker. Codex uses Codex's native bearer-token environment variable setting:
|
|
76
|
+
|
|
77
|
+
```toml
|
|
78
|
+
[mcp_servers.github]
|
|
79
|
+
url = "https://api.githubcopilot.com/mcp/"
|
|
80
|
+
bearer_token_env_var = "GITHUB_PERSONAL_ACCESS_TOKEN"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Claude uses HTTP MCP config with an authorization header:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"github": {
|
|
89
|
+
"type": "http",
|
|
90
|
+
"url": "https://api.githubcopilot.com/mcp/",
|
|
91
|
+
"headers": {
|
|
92
|
+
"Authorization": "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Gemini uses streamable HTTP config with an authorization header:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"mcpServers": {
|
|
104
|
+
"github": {
|
|
105
|
+
"httpUrl": "https://api.githubcopilot.com/mcp/",
|
|
106
|
+
"headers": {
|
|
107
|
+
"Authorization": "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 3. Provide GitHub credentials
|
|
115
|
+
|
|
116
|
+
Create a GitHub personal access token with the repository permissions your agent should have, then expose it as an environment variable before launching the agent:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
export GITHUB_PERSONAL_ACCESS_TOKEN=your_token_here
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Do not commit tokens or write them into Sibu-managed config files. The generated configs read `GITHUB_PERSONAL_ACCESS_TOKEN` from the environment so credentials can stay outside the repository.
|
|
123
|
+
|
|
124
|
+
### 4. Stop managing the GitHub MCP server
|
|
125
|
+
|
|
126
|
+
To remove GitHub from Sibu's selected MCP server state, run:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
sibu mcp stop github
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Sibu updates generated agent config where possible and asks whether to keep or delete MCP-only config files.
|
|
133
|
+
|
|
45
134
|
## Release notes and changelog
|
|
46
135
|
|
|
47
136
|
For every Sibu release, update both release-note locations:
|
|
@@ -29,5 +29,18 @@ export function createProgram() {
|
|
|
29
29
|
.command('stop <skill_name>')
|
|
30
30
|
.description('Stop managing one selected Sibu skill')
|
|
31
31
|
.action((skillName) => executeCliCommand({ type: 'skills:stop', skillName }));
|
|
32
|
+
const mcp = cli.command('mcp').description('Manage Sibu MCP server configuration');
|
|
33
|
+
mcp
|
|
34
|
+
.command('list')
|
|
35
|
+
.description('List available MCP servers')
|
|
36
|
+
.action(() => executeCliCommand({ type: 'mcp:list' }));
|
|
37
|
+
mcp
|
|
38
|
+
.command('use <mcp_server_id>')
|
|
39
|
+
.description('Add one available MCP server to a clean Sibu workflow')
|
|
40
|
+
.action((serverId) => executeCliCommand({ type: 'mcp:use', serverId }));
|
|
41
|
+
mcp
|
|
42
|
+
.command('stop <mcp_server_id>')
|
|
43
|
+
.description('Stop managing one selected MCP server')
|
|
44
|
+
.action((serverId) => executeCliCommand({ type: 'mcp:stop', serverId }));
|
|
32
45
|
return cli;
|
|
33
46
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { handleDoctorProject } from '../../modules/workflow-health-diagnosis/index.js';
|
|
2
2
|
import { handleInitProject } from '../../modules/project-adoption/index.js';
|
|
3
|
+
import { handleListMcpServers, handleStopMcpServer, handleUseMcpServer } from '../../modules/mcp-server-selection-management/index.js';
|
|
3
4
|
import { handleListSkills } from '../../modules/skill-selection-management/index.js';
|
|
4
5
|
import { handleStopManagingFile } from '../../modules/skill-selection-management/index.js';
|
|
5
6
|
import { handleSyncProject } from '../../modules/sync-review/index.js';
|
|
@@ -24,5 +25,14 @@ export async function executeCliCommand(command) {
|
|
|
24
25
|
case 'skills:use':
|
|
25
26
|
await handleUseSkill(command);
|
|
26
27
|
return;
|
|
28
|
+
case 'mcp:list':
|
|
29
|
+
await handleListMcpServers(command);
|
|
30
|
+
return;
|
|
31
|
+
case 'mcp:use':
|
|
32
|
+
await handleUseMcpServer(command);
|
|
33
|
+
return;
|
|
34
|
+
case 'mcp:stop':
|
|
35
|
+
await handleStopMcpServer(command);
|
|
36
|
+
return;
|
|
27
37
|
}
|
|
28
38
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { askForArchitectureSkill, askForDatabaseSkills, askForFrameworkSkills, askForLanguageSkills, askForMissingFrameworkSkills, askForNewArchitectureSkill, askForNewLanguageSkills, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, renderIntro, shouldAskForNewLanguageSkills, } from './prompts.js';
|
|
1
|
+
export { askForArchitectureSkill, askForDatabaseSkills, askForFrameworkSkills, askForLanguageSkills, askForMcpServers, askForMissingFrameworkSkills, askForNewArchitectureSkill, askForNewLanguageSkills, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, MCP_SERVER_SELECTION_MESSAGE, renderIntro, shouldAskForNewLanguageSkills, } from './prompts.js';
|
|
@@ -3,8 +3,9 @@ import { cancel, isCancel, multiselect, select, text } from '@clack/prompts';
|
|
|
3
3
|
import gradient from 'gradient-string';
|
|
4
4
|
import { Box, Text, render, useApp } from 'ink';
|
|
5
5
|
import { useEffect } from 'react';
|
|
6
|
-
import { SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_DATABASE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SELECTABLE_WORKFLOW_SKILLS, SUPPORTED_AGENTS } from '../workflow-target-planning/index.js';
|
|
6
|
+
import { SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_DATABASE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SELECTABLE_MCP_SERVERS, SELECTABLE_WORKFLOW_SKILLS, SUPPORTED_AGENTS, } from '../workflow-target-planning/index.js';
|
|
7
7
|
const NONE_OPTION_ID = 'none';
|
|
8
|
+
export const MCP_SERVER_SELECTION_MESSAGE = 'Select optional MCP servers to configure. Sibu writes config files only; you own prerequisites and authentication.';
|
|
8
9
|
export async function renderIntro() {
|
|
9
10
|
console.log(gradient(['#39ff14', '#00e5ff', '#9b5de5']).multiline('⧖ S I B U ⧖'));
|
|
10
11
|
const app = render(_jsx(IntroPanel, {}));
|
|
@@ -72,6 +73,22 @@ export async function askForDatabaseSkills() {
|
|
|
72
73
|
}
|
|
73
74
|
return SELECTABLE_DATABASE_SKILLS.filter((skill) => selectedDatabaseSkillIds.includes(skill.id));
|
|
74
75
|
}
|
|
76
|
+
export async function askForMcpServers() {
|
|
77
|
+
const selectedMcpServerIds = await multiselect({
|
|
78
|
+
message: MCP_SERVER_SELECTION_MESSAGE,
|
|
79
|
+
required: false,
|
|
80
|
+
options: SELECTABLE_MCP_SERVERS.map((server) => ({
|
|
81
|
+
value: server.id,
|
|
82
|
+
label: server.name,
|
|
83
|
+
hint: server.description,
|
|
84
|
+
})),
|
|
85
|
+
});
|
|
86
|
+
if (isCancel(selectedMcpServerIds)) {
|
|
87
|
+
cancel('Initialization cancelled.');
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
return SELECTABLE_MCP_SERVERS.filter((server) => selectedMcpServerIds.includes(server.id));
|
|
91
|
+
}
|
|
75
92
|
async function askForFrameworkSkillSelection({ message, cancelMessage, }) {
|
|
76
93
|
const selectedFrameworkSkillIds = await multiselect({
|
|
77
94
|
message,
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { getMcpServerListItems, handleListMcpServers } from './list-mcp-servers/handler.js';
|
|
2
|
+
export { getNextMcpSelection, handleUseMcpServer } from './use-mcp-server/handler.js';
|
|
3
|
+
export { applyStoppedMcpFileDeleteDecision, getNextStoppedMcpSelection, handleStopMcpServer, stopSelectedMcpServer } from './stop-mcp-server/handler.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { intro, log, outro } from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { SELECTABLE_MCP_SERVERS } from '../../workflow-target-planning/index.js';
|
|
4
|
+
import { getProjectContext } from '../../../shared/paths.js';
|
|
5
|
+
import { renderIntro } from '../../interactive-guidance/index.js';
|
|
6
|
+
import { readStateForDoctor } from '../../workflow-state-registry/index.js';
|
|
7
|
+
const defaultDependencies = {
|
|
8
|
+
renderIntro,
|
|
9
|
+
intro,
|
|
10
|
+
logMessage: log.message,
|
|
11
|
+
logWarn: log.warn,
|
|
12
|
+
logInfo: log.info,
|
|
13
|
+
outro,
|
|
14
|
+
getStatePath: () => getProjectContext().statePath,
|
|
15
|
+
readState: readStateForDoctor,
|
|
16
|
+
};
|
|
17
|
+
export async function handleListMcpServers(_command, dependencies = defaultDependencies) {
|
|
18
|
+
await dependencies.renderIntro();
|
|
19
|
+
dependencies.intro(chalk.cyan('Available MCP servers'));
|
|
20
|
+
const statePath = dependencies.getStatePath();
|
|
21
|
+
const stateResult = dependencies.readState(statePath);
|
|
22
|
+
const state = stateResult.ok ? stateResult.state : undefined;
|
|
23
|
+
if (!stateResult.ok) {
|
|
24
|
+
dependencies.logWarn(`${stateResult.message} Showing available MCP servers without project state.`);
|
|
25
|
+
dependencies.logInfo('Run `sibu init` before selecting project MCP servers.');
|
|
26
|
+
}
|
|
27
|
+
dependencies.logInfo('Sibu configures MCP files only; prerequisites, credentials, and provider authentication remain user-owned.');
|
|
28
|
+
dependencies.logMessage(chalk.bold('MCP servers'));
|
|
29
|
+
for (const server of getMcpServerListItems(state)) {
|
|
30
|
+
const marker = server.selected ? chalk.green('● selected') : chalk.dim('○ available');
|
|
31
|
+
console.log(` ${marker} ${chalk.bold(server.name)} ${chalk.dim(`(${server.id})`)}`);
|
|
32
|
+
console.log(` ${chalk.dim(server.description)}`);
|
|
33
|
+
console.log(` ${chalk.dim(`Source: ${server.source}`)}`);
|
|
34
|
+
}
|
|
35
|
+
dependencies.outro(chalk.green('MCP server list ready.'));
|
|
36
|
+
}
|
|
37
|
+
export function getMcpServerListItems(state) {
|
|
38
|
+
const selectedServerIds = new Set(state?.selectedMcpServers ?? []);
|
|
39
|
+
return SELECTABLE_MCP_SERVERS.map((server) => ({
|
|
40
|
+
name: server.name,
|
|
41
|
+
id: server.id,
|
|
42
|
+
description: server.description,
|
|
43
|
+
source: server.source,
|
|
44
|
+
selected: selectedServerIds.has(server.id),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
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 '../../../shared/catalog.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 '../../workflow-target-planning/index.js';
|
|
9
|
+
import { getWorkflowMutationReadiness } from '../../workflow-mutation-readiness/index.js';
|
|
10
|
+
import { getTemplateVersion, readTemplateManifest } from '../../template-catalog-rendering/index.js';
|
|
11
|
+
import { cloneState, writeStateFile } from '../../workflow-state-registry/index.js';
|
|
12
|
+
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowTargets, } from '../../workflow-target-planning/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
|
+
nextState.templateVersion = manifest.templateVersion;
|
|
83
|
+
nextState.updatedAt = new Date().toISOString();
|
|
84
|
+
for (const previousTarget of previousTargets.filter((target) => target.mcpConfigAgentId)) {
|
|
85
|
+
const relativePath = path.relative(rootPath, previousTarget.targetPath);
|
|
86
|
+
const managedFile = nextState.managedFiles[relativePath];
|
|
87
|
+
if (!managedFile) {
|
|
88
|
+
return {
|
|
89
|
+
status: 'blocked',
|
|
90
|
+
message: `${relativePath} is not recorded in ${STATE_RELATIVE_PATH}.`,
|
|
91
|
+
hint: 'Run `sibu sync` to review workflow state before stopping this MCP server.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const nextTarget = nextTargetsByPath.get(previousTarget.targetPath);
|
|
95
|
+
if (nextTarget) {
|
|
96
|
+
const [file] = renderMissingWorkflowFiles({
|
|
97
|
+
missingTargets: [nextTarget],
|
|
98
|
+
selectedLanguageSkills,
|
|
99
|
+
selectedFrameworkSkills,
|
|
100
|
+
selectedArchitectureSkill,
|
|
101
|
+
selectedWorkflowSkills,
|
|
102
|
+
selectedDatabaseSkills,
|
|
103
|
+
selectedMcpServers: selectionResult.remainingMcpServers,
|
|
104
|
+
});
|
|
105
|
+
fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
|
|
106
|
+
fs.writeFileSync(file.targetPath, file.contents, 'utf8');
|
|
107
|
+
nextState.managedFiles[relativePath] = removeUndefinedFields({
|
|
108
|
+
...managedFile,
|
|
109
|
+
templateVersion: getTemplateVersion(manifest, managedFile.template),
|
|
110
|
+
sha256: sha256(file.contents),
|
|
111
|
+
status: managedFile.status ?? 'managed',
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const stoppedFile = removeUndefinedFields({
|
|
116
|
+
...managedFile,
|
|
117
|
+
templateVersion: getTemplateVersion(manifest, managedFile.template),
|
|
118
|
+
sha256: readFileHashIfPresent(previousTarget.targetPath) ?? managedFile.sha256,
|
|
119
|
+
status: 'unmanaged',
|
|
120
|
+
reason: 'Stopped by `sibu mcp stop`.',
|
|
121
|
+
});
|
|
122
|
+
nextState.managedFiles[relativePath] = stoppedFile;
|
|
123
|
+
stoppedPaths.push({ relativePath, absolutePath: previousTarget.targetPath });
|
|
124
|
+
stoppedFiles.push(stoppedFile);
|
|
125
|
+
}
|
|
126
|
+
return { status: 'stopped', state: nextState, stoppedPaths, stoppedFiles, serverName: selectionResult.serverName };
|
|
127
|
+
}
|
|
128
|
+
export async function askToDeleteStoppedMcpFile(managedPath, managedFile) {
|
|
129
|
+
if (!fs.existsSync(managedPath.absolutePath)) {
|
|
130
|
+
log.info(`${managedPath.relativePath} does not exist locally, so there is nothing to delete.`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!fs.lstatSync(managedPath.absolutePath).isFile()) {
|
|
134
|
+
log.warn(`${managedPath.relativePath} is not a regular file. I will not delete it.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const deleteAction = await select({
|
|
138
|
+
message: `Do you also want to delete ${managedPath.relativePath} for good?`,
|
|
139
|
+
options: [
|
|
140
|
+
{ value: 'keep', label: 'Keep file', hint: 'Recommended. Leave the local MCP config file unchanged.' },
|
|
141
|
+
{ value: 'delete', label: 'Delete file', hint: 'Permanently remove this file from the project.' },
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
if (isCancel(deleteAction)) {
|
|
145
|
+
cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
const confirmDelete = deleteAction === 'delete' && hasLocalEdits(managedPath.absolutePath, managedFile) ? await askToConfirmEditedDelete(managedPath) : undefined;
|
|
149
|
+
applyStoppedMcpFileDeleteDecision({ managedPath, managedFile, deleteAction, confirmDelete });
|
|
150
|
+
}
|
|
151
|
+
export function applyStoppedMcpFileDeleteDecision({ managedPath, managedFile, deleteAction, confirmDelete, }) {
|
|
152
|
+
if (!fs.existsSync(managedPath.absolutePath)) {
|
|
153
|
+
log.info(`${managedPath.relativePath} does not exist locally, so there is nothing to delete.`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!fs.lstatSync(managedPath.absolutePath).isFile()) {
|
|
157
|
+
log.warn(`${managedPath.relativePath} is not a regular file. I will not delete it.`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (deleteAction === 'keep') {
|
|
161
|
+
log.info(`Kept ${managedPath.relativePath}.`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (hasLocalEdits(managedPath.absolutePath, managedFile) && confirmDelete !== 'delete') {
|
|
165
|
+
log.info(`Kept ${managedPath.relativePath}.`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
fs.unlinkSync(managedPath.absolutePath);
|
|
169
|
+
log.success(`Deleted ${managedPath.relativePath}.`);
|
|
170
|
+
}
|
|
171
|
+
async function askToConfirmEditedDelete(managedPath) {
|
|
172
|
+
const confirmDelete = await select({
|
|
173
|
+
message: `${managedPath.relativePath} differs from the last Sibu-recorded hash. Delete it anyway?`,
|
|
174
|
+
options: [
|
|
175
|
+
{ value: 'keep', label: 'Keep file', hint: 'Recommended. Preserve local edits.' },
|
|
176
|
+
{ value: 'delete', label: 'Delete anyway', hint: 'Permanently remove the edited file.' },
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
if (isCancel(confirmDelete)) {
|
|
180
|
+
cancel('Delete prompt cancelled. The file is no longer managed, but it was not deleted.');
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
return confirmDelete;
|
|
184
|
+
}
|
|
185
|
+
function hasLocalEdits(filePath, managedFile) {
|
|
186
|
+
return readFileHashIfPresent(filePath) !== managedFile.sha256;
|
|
187
|
+
}
|
|
188
|
+
function getMcpServerById(serverId) {
|
|
189
|
+
const resolution = resolveSelectableMcpServerById(serverId);
|
|
190
|
+
if (!resolution.ok) {
|
|
191
|
+
throw new Error(`Unsupported MCP server in state: ${serverId}`);
|
|
192
|
+
}
|
|
193
|
+
return resolution.resolved.server;
|
|
194
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { log } from '@clack/prompts';
|
|
4
|
+
import { STATE_RELATIVE_PATH } from '../../../shared/catalog.js';
|
|
5
|
+
import { getProjectContext } from '../../../shared/paths.js';
|
|
6
|
+
import { getWorkflowMutationReadiness } from '../../workflow-mutation-readiness/index.js';
|
|
7
|
+
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowTargets, renderMissingWorkflowFiles, resolveSelectableMcpServerById, writeSibuState, } from '../../workflow-target-planning/index.js';
|
|
8
|
+
export async function handleUseMcpServer(command) {
|
|
9
|
+
const { rootPath, statePath } = getProjectContext();
|
|
10
|
+
const readiness = getWorkflowMutationReadiness({ rootPath, statePath });
|
|
11
|
+
if (!readiness.ok) {
|
|
12
|
+
log.error(readiness.message);
|
|
13
|
+
log.info(readiness.hint);
|
|
14
|
+
for (const preview of readiness.actionablePreviews?.slice(0, 3) ?? []) {
|
|
15
|
+
log.info(`${preview.relativePath}: ${preview.status}`);
|
|
16
|
+
}
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const selectionResult = getNextMcpSelection(readiness.state, command.serverId);
|
|
21
|
+
switch (selectionResult.status) {
|
|
22
|
+
case 'noop':
|
|
23
|
+
log.success(selectionResult.message);
|
|
24
|
+
log.info('No files changed.');
|
|
25
|
+
return;
|
|
26
|
+
case 'blocked':
|
|
27
|
+
log.error(selectionResult.message);
|
|
28
|
+
if (selectionResult.hint) {
|
|
29
|
+
log.info(selectionResult.hint);
|
|
30
|
+
}
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
case 'selected':
|
|
34
|
+
applySelectedMcpServer({ rootPath, statePath, state: readiness.state, selectionResult });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function getNextMcpSelection(state, serverId) {
|
|
39
|
+
const resolution = resolveSelectableMcpServerById(serverId);
|
|
40
|
+
if (!resolution.ok) {
|
|
41
|
+
return { status: 'blocked', message: resolution.message };
|
|
42
|
+
}
|
|
43
|
+
const selectedMcpServerIds = [...(state.selectedMcpServers ?? [])];
|
|
44
|
+
if (selectedMcpServerIds.includes(resolution.resolved.server.id)) {
|
|
45
|
+
return { status: 'noop', message: `${resolution.resolved.server.name} is already selected.` };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
status: 'selected',
|
|
49
|
+
serverName: resolution.resolved.server.name,
|
|
50
|
+
selectedMcpServers: [...selectedMcpServerIds, resolution.resolved.server.id].map(getMcpServerById),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function applySelectedMcpServer({ rootPath, statePath, state, selectionResult, }) {
|
|
54
|
+
const selectedAgents = getSelectedAgentsFromState(state);
|
|
55
|
+
const selectedLanguageSkills = getSelectedLanguageSkillsFromState(state);
|
|
56
|
+
const selectedFrameworkSkills = getSelectedFrameworkSkillsFromState(state);
|
|
57
|
+
const selectedArchitectureSkill = getSelectedArchitectureSkillFromState(state);
|
|
58
|
+
const selectedWorkflowSkills = getSelectedWorkflowSkillsFromState(state);
|
|
59
|
+
const selectedDatabaseSkills = getSelectedDatabaseSkillsFromState(state);
|
|
60
|
+
const previousTargets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, getSelectedMcpServersFromState(state));
|
|
61
|
+
const targets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectionResult.selectedMcpServers);
|
|
62
|
+
const affectedTargets = getAffectedMcpTargets(previousTargets, targets);
|
|
63
|
+
const preflightError = getMcpUsePreflightError({ rootPath, state, affectedTargets, previousTargets });
|
|
64
|
+
if (preflightError) {
|
|
65
|
+
log.error(preflightError);
|
|
66
|
+
log.info('Run `sibu sync` to review workflow state before selecting an MCP server.');
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const files = renderMissingWorkflowFiles({
|
|
71
|
+
missingTargets: affectedTargets,
|
|
72
|
+
selectedLanguageSkills,
|
|
73
|
+
selectedFrameworkSkills,
|
|
74
|
+
selectedArchitectureSkill,
|
|
75
|
+
selectedWorkflowSkills,
|
|
76
|
+
selectedDatabaseSkills,
|
|
77
|
+
selectedMcpServers: selectionResult.selectedMcpServers,
|
|
78
|
+
});
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const fileAlreadyExists = fs.existsSync(file.targetPath);
|
|
81
|
+
fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
|
|
82
|
+
fs.writeFileSync(file.targetPath, file.contents, 'utf8');
|
|
83
|
+
log.success(`${fileAlreadyExists ? 'Updated' : 'Created'} ${file.label}`);
|
|
84
|
+
}
|
|
85
|
+
writeSibuState({
|
|
86
|
+
rootPath,
|
|
87
|
+
statePath,
|
|
88
|
+
selectedAgents,
|
|
89
|
+
selectedLanguageSkills,
|
|
90
|
+
selectedFrameworkSkills,
|
|
91
|
+
selectedArchitectureSkill,
|
|
92
|
+
selectedWorkflowSkills,
|
|
93
|
+
selectedDatabaseSkills,
|
|
94
|
+
selectedMcpServers: selectionResult.selectedMcpServers,
|
|
95
|
+
targets,
|
|
96
|
+
});
|
|
97
|
+
log.success(`Updated ${STATE_RELATIVE_PATH}`);
|
|
98
|
+
log.success(`Added ${selectionResult.serverName}.`);
|
|
99
|
+
log.info('Sibu configured MCP files only; prerequisites, credentials, and provider authentication remain user-owned.');
|
|
100
|
+
}
|
|
101
|
+
function getAffectedMcpTargets(previousTargets, targets) {
|
|
102
|
+
const previousTargetPaths = new Set(previousTargets.map((target) => target.targetPath));
|
|
103
|
+
return targets.filter((target) => target.mcpConfigAgentId || (!previousTargetPaths.has(target.targetPath) && target.targetKind === 'mcp-config'));
|
|
104
|
+
}
|
|
105
|
+
function getMcpUsePreflightError({ rootPath, state, affectedTargets, previousTargets, }) {
|
|
106
|
+
const previousTargetPaths = new Set(previousTargets.map((target) => target.targetPath));
|
|
107
|
+
for (const target of affectedTargets) {
|
|
108
|
+
const relativePath = path.relative(rootPath, target.targetPath);
|
|
109
|
+
if (previousTargetPaths.has(target.targetPath)) {
|
|
110
|
+
if (!state.managedFiles[relativePath]) {
|
|
111
|
+
return `${relativePath} is not recorded in ${STATE_RELATIVE_PATH}.`;
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (fs.existsSync(target.targetPath)) {
|
|
116
|
+
return `${relativePath} already exists but is not recorded for this MCP selection.`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
function getMcpServerById(serverId) {
|
|
122
|
+
const resolution = resolveSelectableMcpServerById(serverId);
|
|
123
|
+
if (!resolution.ok) {
|
|
124
|
+
throw new Error(`Unsupported MCP server in state: ${serverId}`);
|
|
125
|
+
}
|
|
126
|
+
return resolution.resolved.server;
|
|
127
|
+
}
|
|
@@ -4,11 +4,22 @@ import { intro, log, outro } from '@clack/prompts';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { STATE_RELATIVE_PATH } from '../../shared/catalog.js';
|
|
6
6
|
import { getProjectContext } from '../../shared/paths.js';
|
|
7
|
-
import { askForArchitectureSkill, askForDatabaseSkills, askForFrameworkSkills, askForLanguageSkills, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, renderIntro } from '../interactive-guidance/index.js';
|
|
7
|
+
import { askForArchitectureSkill, askForDatabaseSkills, askForFrameworkSkills, askForLanguageSkills, askForMcpServers, askForProjectOverview, askForSupportedAgents, askForWorkflowSkills, renderIntro, } from '../interactive-guidance/index.js';
|
|
8
8
|
import { readStateForDoctor } from '../workflow-state-registry/index.js';
|
|
9
9
|
import { getWorkflowTargets, renderMissingWorkflowFiles, writeSibuState } from '../workflow-target-planning/index.js';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const defaultDependencies = {
|
|
11
|
+
renderIntro,
|
|
12
|
+
askForSupportedAgents,
|
|
13
|
+
askForMcpServers,
|
|
14
|
+
askForLanguageSkills,
|
|
15
|
+
askForFrameworkSkills,
|
|
16
|
+
askForDatabaseSkills,
|
|
17
|
+
askForArchitectureSkill,
|
|
18
|
+
askForWorkflowSkills,
|
|
19
|
+
askForProjectOverview,
|
|
20
|
+
};
|
|
21
|
+
export async function handleInitProject(_command, dependencies = defaultDependencies) {
|
|
22
|
+
await dependencies.renderIntro();
|
|
12
23
|
intro(chalk.cyan('Setting up Sibu'));
|
|
13
24
|
const { rootPath, statePath } = getProjectContext();
|
|
14
25
|
if (fs.existsSync(statePath)) {
|
|
@@ -27,13 +38,14 @@ export async function handleInitProject(_command) {
|
|
|
27
38
|
process.exitCode = 1;
|
|
28
39
|
return;
|
|
29
40
|
}
|
|
30
|
-
const selectedAgents = await askForSupportedAgents();
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
41
|
+
const selectedAgents = await dependencies.askForSupportedAgents();
|
|
42
|
+
const selectedMcpServers = await dependencies.askForMcpServers();
|
|
43
|
+
const selectedLanguageSkills = await dependencies.askForLanguageSkills();
|
|
44
|
+
const selectedFrameworkSkills = await dependencies.askForFrameworkSkills();
|
|
45
|
+
const selectedDatabaseSkills = await dependencies.askForDatabaseSkills();
|
|
46
|
+
const selectedArchitectureSkill = await dependencies.askForArchitectureSkill();
|
|
47
|
+
const selectedWorkflowSkills = await dependencies.askForWorkflowSkills();
|
|
48
|
+
const targets = getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectedMcpServers);
|
|
37
49
|
const missingTargets = targets.filter((target) => !fs.existsSync(target.targetPath));
|
|
38
50
|
log.message('I will create this project’s initial AI workflow files.');
|
|
39
51
|
for (const target of targets) {
|
|
@@ -46,7 +58,7 @@ export async function handleInitProject(_command) {
|
|
|
46
58
|
}
|
|
47
59
|
log.info(`${STATE_RELATIVE_PATH} is missing. I will create it.`);
|
|
48
60
|
const shouldAskForOverview = missingTargets.some((target) => target.requiresProjectOverview);
|
|
49
|
-
const overview = shouldAskForOverview ? await askForProjectOverview() : undefined;
|
|
61
|
+
const overview = shouldAskForOverview ? await dependencies.askForProjectOverview() : undefined;
|
|
50
62
|
const files = renderMissingWorkflowFiles({
|
|
51
63
|
missingTargets,
|
|
52
64
|
overview,
|
|
@@ -55,13 +67,25 @@ export async function handleInitProject(_command) {
|
|
|
55
67
|
selectedArchitectureSkill,
|
|
56
68
|
selectedWorkflowSkills,
|
|
57
69
|
selectedDatabaseSkills,
|
|
70
|
+
selectedMcpServers,
|
|
58
71
|
});
|
|
59
72
|
for (const file of files) {
|
|
60
73
|
fs.mkdirSync(path.dirname(file.targetPath), { recursive: true });
|
|
61
74
|
fs.writeFileSync(file.targetPath, file.contents, { encoding: 'utf8', flag: 'wx' });
|
|
62
75
|
log.success(`Created ${file.label}`);
|
|
63
76
|
}
|
|
64
|
-
writeSibuState({
|
|
77
|
+
writeSibuState({
|
|
78
|
+
rootPath,
|
|
79
|
+
statePath,
|
|
80
|
+
selectedAgents,
|
|
81
|
+
selectedLanguageSkills,
|
|
82
|
+
selectedFrameworkSkills,
|
|
83
|
+
selectedArchitectureSkill,
|
|
84
|
+
selectedWorkflowSkills,
|
|
85
|
+
selectedDatabaseSkills,
|
|
86
|
+
selectedMcpServers,
|
|
87
|
+
targets,
|
|
88
|
+
});
|
|
65
89
|
log.success(`Created ${STATE_RELATIVE_PATH}`);
|
|
66
90
|
outro(chalk.green('Setup complete.'));
|
|
67
91
|
}
|
|
@@ -5,7 +5,7 @@ import { sha256 } from '../../shared/hash.js';
|
|
|
5
5
|
import { getSideTemplatePath } from '../../shared/paths.js';
|
|
6
6
|
import { cloneState } from '../workflow-state-registry/index.js';
|
|
7
7
|
import { getTemplateVersion, renderTemplateForSync } from '../template-catalog-rendering/index.js';
|
|
8
|
-
import { getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedWorkflowSkillsFromState, } from '../workflow-target-planning/index.js';
|
|
8
|
+
import { getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, } from '../workflow-target-planning/index.js';
|
|
9
9
|
export function applySyncAction({ rootPath, state, manifest, preview, action, }) {
|
|
10
10
|
const nextState = cloneState(state);
|
|
11
11
|
const managedFile = nextState.managedFiles[preview.relativePath] ?? preview.managedFile;
|
|
@@ -21,6 +21,7 @@ export function applySyncAction({ rootPath, state, manifest, preview, action, })
|
|
|
21
21
|
selectedArchitectureSkill: getSelectedArchitectureSkillFromState(nextState),
|
|
22
22
|
selectedWorkflowSkills: getSelectedWorkflowSkillsFromState(nextState),
|
|
23
23
|
selectedDatabaseSkills: getSelectedDatabaseSkillsFromState(nextState),
|
|
24
|
+
selectedMcpServers: getSelectedMcpServersFromState(nextState),
|
|
24
25
|
});
|
|
25
26
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
26
27
|
fs.writeFileSync(targetPath, contents, 'utf8');
|
|
@@ -58,6 +59,7 @@ export function applySyncAction({ rootPath, state, manifest, preview, action, })
|
|
|
58
59
|
selectedArchitectureSkill: getSelectedArchitectureSkillFromState(state),
|
|
59
60
|
selectedWorkflowSkills: getSelectedWorkflowSkillsFromState(state),
|
|
60
61
|
selectedDatabaseSkills: getSelectedDatabaseSkillsFromState(state),
|
|
62
|
+
selectedMcpServers: getSelectedMcpServersFromState(state),
|
|
61
63
|
});
|
|
62
64
|
fs.mkdirSync(path.dirname(sideTemplatePath), { recursive: true });
|
|
63
65
|
fs.writeFileSync(sideTemplatePath, contents, 'utf8');
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { sha256 } from '../../shared/hash.js';
|
|
4
4
|
import { hasReviewedTemplateVersion } from '../workflow-state-registry/index.js';
|
|
5
5
|
import { renderTemplateForSync } from '../template-catalog-rendering/index.js';
|
|
6
|
-
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedWorkflowSkillsFromState, getWorkflowTargets, } from '../workflow-target-planning/index.js';
|
|
6
|
+
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowTargets, } from '../workflow-target-planning/index.js';
|
|
7
7
|
const ACTIONABLE_SYNC_PREVIEW_STATUSES = new Set([
|
|
8
8
|
'new-template',
|
|
9
9
|
'missing',
|
|
@@ -31,6 +31,7 @@ export function getSyncPreviews({ rootPath, state, manifest }) {
|
|
|
31
31
|
const selectedArchitectureSkill = getSelectedArchitectureSkillFromState(state);
|
|
32
32
|
const selectedWorkflowSkills = getSelectedWorkflowSkillsFromState(state);
|
|
33
33
|
const selectedDatabaseSkills = getSelectedDatabaseSkillsFromState(state);
|
|
34
|
+
const selectedMcpServers = getSelectedMcpServersFromState(state);
|
|
34
35
|
const previews = Object.entries(state.managedFiles).map(([relativePath, managedFile]) => getManagedFileSyncPreview({
|
|
35
36
|
rootPath,
|
|
36
37
|
manifest,
|
|
@@ -41,8 +42,9 @@ export function getSyncPreviews({ rootPath, state, manifest }) {
|
|
|
41
42
|
selectedArchitectureSkill,
|
|
42
43
|
selectedWorkflowSkills,
|
|
43
44
|
selectedDatabaseSkills,
|
|
45
|
+
selectedMcpServers,
|
|
44
46
|
}));
|
|
45
|
-
const expectedTargets = getWorkflowTargets(rootPath, getSelectedAgentsFromState(state), selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills);
|
|
47
|
+
const expectedTargets = getWorkflowTargets(rootPath, getSelectedAgentsFromState(state), selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectedMcpServers);
|
|
46
48
|
for (const target of expectedTargets) {
|
|
47
49
|
const relativePath = path.relative(rootPath, target.targetPath);
|
|
48
50
|
if (state.managedFiles[relativePath]) {
|
|
@@ -79,7 +81,7 @@ export function getSyncPreviews({ rootPath, state, manifest }) {
|
|
|
79
81
|
}
|
|
80
82
|
return previews;
|
|
81
83
|
}
|
|
82
|
-
function getManagedFileSyncPreview({ rootPath, manifest, relativePath, managedFile, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, }) {
|
|
84
|
+
function getManagedFileSyncPreview({ rootPath, manifest, relativePath, managedFile, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectedMcpServers, }) {
|
|
83
85
|
if (managedFile.status === 'unmanaged') {
|
|
84
86
|
return { relativePath, managedFile, status: 'unmanaged', recordedTemplateVersion: managedFile.templateVersion, changes: [] };
|
|
85
87
|
}
|
|
@@ -100,9 +102,10 @@ function getManagedFileSyncPreview({ rootPath, manifest, relativePath, managedFi
|
|
|
100
102
|
selectedArchitectureSkill,
|
|
101
103
|
selectedWorkflowSkills,
|
|
102
104
|
selectedDatabaseSkills,
|
|
105
|
+
selectedMcpServers,
|
|
103
106
|
});
|
|
104
107
|
const hasUpdate = hasTemplateUpdate || hasSelectionUpdate;
|
|
105
|
-
const changes = hasTemplateUpdate ? template.changes : [
|
|
108
|
+
const changes = hasTemplateUpdate ? template.changes : [getSelectionRefreshChange(managedFile.template)];
|
|
106
109
|
if (!hasLocalFile) {
|
|
107
110
|
return {
|
|
108
111
|
relativePath,
|
|
@@ -157,8 +160,14 @@ function getManagedFileSyncPreview({ rootPath, manifest, relativePath, managedFi
|
|
|
157
160
|
hasLocalFile,
|
|
158
161
|
};
|
|
159
162
|
}
|
|
160
|
-
function
|
|
161
|
-
if (
|
|
163
|
+
function getSelectionRefreshChange(templateRelativePath) {
|
|
164
|
+
if (templateRelativePath === '.codex/config.toml' || templateRelativePath === 'mcp/claude/.mcp.json' || templateRelativePath === 'mcp/gemini/settings.json') {
|
|
165
|
+
return 'Refreshes generated MCP configuration for the current selected MCP servers.';
|
|
166
|
+
}
|
|
167
|
+
return 'Refreshes generated skill routing for the current selected skills.';
|
|
168
|
+
}
|
|
169
|
+
function hasRenderedSelectionUpdate({ relativePath, currentPath, managedFile, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills, selectedDatabaseSkills, selectedMcpServers, }) {
|
|
170
|
+
if (relativePath !== 'AGENTS.md' && selectedMcpServers.length === 0) {
|
|
162
171
|
return false;
|
|
163
172
|
}
|
|
164
173
|
const renderedContents = renderTemplateForSync({
|
|
@@ -169,6 +178,7 @@ function hasRenderedSelectionUpdate({ relativePath, currentPath, managedFile, se
|
|
|
169
178
|
selectedArchitectureSkill,
|
|
170
179
|
selectedWorkflowSkills,
|
|
171
180
|
selectedDatabaseSkills,
|
|
181
|
+
selectedMcpServers,
|
|
172
182
|
});
|
|
173
183
|
return sha256(renderedContents) !== managedFile.sha256;
|
|
174
184
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { extractProjectOverview, getTemplateVersion, readTemplate, readTemplateManifest, renderSkillRouting, renderTemplateForSync, } from './templates.js';
|
|
1
|
+
export { extractProjectOverview, getTemplateVersion, readTemplate, readTemplateManifest, renderMcpConfig, renderSkillRouting, renderTemplateForSync, } from './templates.js';
|
|
@@ -18,8 +18,12 @@ export function getTemplateVersion(manifest, templateRelativePath) {
|
|
|
18
18
|
}
|
|
19
19
|
return template.version;
|
|
20
20
|
}
|
|
21
|
-
export function renderTemplateForSync({ templateRelativePath, currentPath, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills = [], selectedDatabaseSkills = [], }) {
|
|
21
|
+
export function renderTemplateForSync({ templateRelativePath, currentPath, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills = [], selectedDatabaseSkills = [], selectedMcpServers = [], }) {
|
|
22
22
|
let contents = readTemplate(templateRelativePath);
|
|
23
|
+
const mcpConfigAgentId = getMcpConfigAgentId(templateRelativePath);
|
|
24
|
+
if (mcpConfigAgentId) {
|
|
25
|
+
return renderMcpConfig({ agentId: mcpConfigAgentId, baseContents: contents, selectedMcpServers });
|
|
26
|
+
}
|
|
23
27
|
if (contents.includes('{{PROJECT_OVERVIEW}}')) {
|
|
24
28
|
contents = contents.replace('{{PROJECT_OVERVIEW}}', extractProjectOverview(currentPath) ?? 'Describe this project.');
|
|
25
29
|
}
|
|
@@ -34,6 +38,18 @@ export function renderSkillRouting(contents, selectedLanguageSkills, selectedFra
|
|
|
34
38
|
.join('\n');
|
|
35
39
|
return contents.replace('{{OPTIONAL_SKILL_ROUTING}}', optionalRouting);
|
|
36
40
|
}
|
|
41
|
+
export function renderMcpConfig({ agentId, baseContents, selectedMcpServers, }) {
|
|
42
|
+
const githubServer = selectedMcpServers.find((server) => server.id === 'github');
|
|
43
|
+
if (!githubServer) {
|
|
44
|
+
return baseContents ?? renderJsonMcpConfig({});
|
|
45
|
+
}
|
|
46
|
+
if (agentId === 'codex') {
|
|
47
|
+
return renderCodexGithubMcpConfig(baseContents ?? '');
|
|
48
|
+
}
|
|
49
|
+
return renderJsonMcpConfig({
|
|
50
|
+
github: buildGithubMcpServerConfig(agentId),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
37
53
|
export function extractProjectOverview(filePath) {
|
|
38
54
|
if (!fs.existsSync(filePath)) {
|
|
39
55
|
return undefined;
|
|
@@ -43,6 +59,45 @@ export function extractProjectOverview(filePath) {
|
|
|
43
59
|
const overview = match?.[1]?.trim();
|
|
44
60
|
return overview || undefined;
|
|
45
61
|
}
|
|
62
|
+
function renderCodexGithubMcpConfig(baseContents) {
|
|
63
|
+
const trimmedBaseContents = baseContents.trimEnd();
|
|
64
|
+
const separator = trimmedBaseContents ? '\n\n' : '';
|
|
65
|
+
return `${trimmedBaseContents}${separator}[mcp_servers.github]
|
|
66
|
+
url = "https://api.githubcopilot.com/mcp/"
|
|
67
|
+
bearer_token_env_var = "GITHUB_PERSONAL_ACCESS_TOKEN"
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
function getMcpConfigAgentId(templateRelativePath) {
|
|
71
|
+
if (templateRelativePath === '.codex/config.toml') {
|
|
72
|
+
return 'codex';
|
|
73
|
+
}
|
|
74
|
+
if (templateRelativePath === 'mcp/claude/.mcp.json') {
|
|
75
|
+
return 'claude';
|
|
76
|
+
}
|
|
77
|
+
if (templateRelativePath === 'mcp/gemini/settings.json') {
|
|
78
|
+
return 'gemini';
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
function buildGithubMcpServerConfig(agentId) {
|
|
83
|
+
const headers = {
|
|
84
|
+
Authorization: 'Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}',
|
|
85
|
+
};
|
|
86
|
+
if (agentId === 'gemini') {
|
|
87
|
+
return {
|
|
88
|
+
httpUrl: 'https://api.githubcopilot.com/mcp/',
|
|
89
|
+
headers,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
type: 'http',
|
|
94
|
+
url: 'https://api.githubcopilot.com/mcp/',
|
|
95
|
+
headers,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function renderJsonMcpConfig(mcpServers) {
|
|
99
|
+
return `${JSON.stringify({ mcpServers }, null, 2)}\n`;
|
|
100
|
+
}
|
|
46
101
|
function isTemplateManifest(value) {
|
|
47
102
|
if (!value || typeof value !== 'object') {
|
|
48
103
|
return false;
|
|
@@ -3,14 +3,14 @@ import path from 'node:path';
|
|
|
3
3
|
import { intro, log, outro } from '@clack/prompts';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { STATE_RELATIVE_PATH } from '../../shared/catalog.js';
|
|
6
|
-
import { SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_DATABASE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SUPPORTED_AGENTS } from '../workflow-target-planning/index.js';
|
|
6
|
+
import { SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_DATABASE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SELECTABLE_MCP_SERVERS, SUPPORTED_AGENTS } from '../workflow-target-planning/index.js';
|
|
7
7
|
import { sha256 } from '../../shared/hash.js';
|
|
8
8
|
import { checkForLatestSibuVersion } from '../version-advisory/index.js';
|
|
9
9
|
import { getProjectContext } from '../../shared/paths.js';
|
|
10
10
|
import { renderIntro } from '../interactive-guidance/index.js';
|
|
11
11
|
import { hasReviewedTemplateVersion, readStateForDoctor } from '../workflow-state-registry/index.js';
|
|
12
12
|
import { getTemplateVersion, readTemplateManifest } from '../template-catalog-rendering/index.js';
|
|
13
|
-
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedWorkflowSkillsFromState, getWorkflowTargets, } from '../workflow-target-planning/index.js';
|
|
13
|
+
import { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedWorkflowSkillsFromState, getWorkflowTargets, } from '../workflow-target-planning/index.js';
|
|
14
14
|
export async function handleDoctorProject(_command) {
|
|
15
15
|
await renderIntro();
|
|
16
16
|
intro(chalk.cyan('Checking workflow state'));
|
|
@@ -122,6 +122,11 @@ function addUnsupportedSelectionIssues(state, issues) {
|
|
|
122
122
|
issues.push({ severity: 'warning', message: `State references unsupported database skill: ${selectedDatabaseSkill}.` });
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
for (const selectedMcpServer of state.selectedMcpServers ?? []) {
|
|
126
|
+
if (!SELECTABLE_MCP_SERVERS.some((server) => server.id === selectedMcpServer)) {
|
|
127
|
+
issues.push({ severity: 'warning', message: `State references unsupported MCP server: ${selectedMcpServer}.` });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
125
130
|
for (const reviewedArchitectureSkill of state.reviewedArchitectureSkills ?? []) {
|
|
126
131
|
if (!SELECTABLE_ARCHITECTURE_SKILLS.some((skill) => skill.id === reviewedArchitectureSkill)) {
|
|
127
132
|
issues.push({ severity: 'warning', message: `State references unsupported reviewed architecture skill: ${reviewedArchitectureSkill}.` });
|
|
@@ -130,7 +135,7 @@ function addUnsupportedSelectionIssues(state, issues) {
|
|
|
130
135
|
}
|
|
131
136
|
function addExpectedTargetIssues(rootPath, state, issues) {
|
|
132
137
|
const reportedMissingPaths = new Set();
|
|
133
|
-
const expectedTargets = getWorkflowTargets(rootPath, getSelectedAgentsFromState(state), getSelectedLanguageSkillsFromState(state), getSelectedFrameworkSkillsFromState(state), getSelectedArchitectureSkillFromState(state), getSelectedWorkflowSkillsFromState(state), getSelectedDatabaseSkillsFromState(state));
|
|
138
|
+
const expectedTargets = getWorkflowTargets(rootPath, getSelectedAgentsFromState(state), getSelectedLanguageSkillsFromState(state), getSelectedFrameworkSkillsFromState(state), getSelectedArchitectureSkillFromState(state), getSelectedWorkflowSkillsFromState(state), getSelectedDatabaseSkillsFromState(state), getSelectedMcpServersFromState(state));
|
|
134
139
|
for (const target of expectedTargets) {
|
|
135
140
|
const relativePath = path.relative(rootPath, target.targetPath);
|
|
136
141
|
const managedFile = state.managedFiles[relativePath];
|
|
@@ -59,6 +59,8 @@ function isSibuState(value) {
|
|
|
59
59
|
(Array.isArray(state.selectedWorkflowSkills) && state.selectedWorkflowSkills.every((skill) => typeof skill === 'string'))) &&
|
|
60
60
|
(state.selectedDatabaseSkills === undefined ||
|
|
61
61
|
(Array.isArray(state.selectedDatabaseSkills) && state.selectedDatabaseSkills.every((skill) => typeof skill === 'string'))) &&
|
|
62
|
+
(state.selectedMcpServers === undefined ||
|
|
63
|
+
(Array.isArray(state.selectedMcpServers) && state.selectedMcpServers.every((server) => typeof server === 'string'))) &&
|
|
62
64
|
(state.reviewedArchitectureSkills === undefined ||
|
|
63
65
|
(Array.isArray(state.reviewedArchitectureSkills) && state.reviewedArchitectureSkills.every((skill) => typeof skill === 'string'))) &&
|
|
64
66
|
!!state.managedFiles &&
|
|
@@ -199,6 +199,14 @@ export const SELECTABLE_WORKFLOW_SKILLS = [
|
|
|
199
199
|
},
|
|
200
200
|
},
|
|
201
201
|
];
|
|
202
|
+
export const SELECTABLE_MCP_SERVERS = [
|
|
203
|
+
{
|
|
204
|
+
id: 'github',
|
|
205
|
+
name: 'GitHub MCP Server',
|
|
206
|
+
description: "Configure GitHub's official MCP server; Sibu writes config only, while prerequisites, runtime availability, credentials, and authentication remain user-owned",
|
|
207
|
+
source: 'github/github-mcp-server',
|
|
208
|
+
},
|
|
209
|
+
];
|
|
202
210
|
export const SUPPORTED_AGENTS = [
|
|
203
211
|
{
|
|
204
212
|
id: 'codex',
|
|
@@ -227,6 +235,13 @@ export const SUPPORTED_AGENTS = [
|
|
|
227
235
|
description: 'Use root AGENTS.md and shared .agents/skills/ discovery',
|
|
228
236
|
},
|
|
229
237
|
];
|
|
238
|
+
export function resolveSelectableMcpServerById(serverId) {
|
|
239
|
+
const server = SELECTABLE_MCP_SERVERS.find((selectableServer) => selectableServer.id === serverId);
|
|
240
|
+
if (server) {
|
|
241
|
+
return { ok: true, resolved: { server } };
|
|
242
|
+
}
|
|
243
|
+
return { ok: false, message: `Unknown MCP server \`${serverId}\`. Run \`sibu mcp list\` to see available MCP servers.` };
|
|
244
|
+
}
|
|
230
245
|
export function resolveSelectableSkillById(skillId) {
|
|
231
246
|
const languageSkill = SELECTABLE_LANGUAGE_SKILLS.find((skill) => skill.id === skillId);
|
|
232
247
|
if (languageSkill) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { MANDATORY_SKILLS, SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_DATABASE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SELECTABLE_WORKFLOW_SKILLS, SUPPORTED_AGENTS, resolveSelectableSkillById, } from './catalog.js';
|
|
2
|
-
export { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedSkillTargetsForAgents, getSelectedWorkflowSkillsFromState, getWorkflowTargets, renderMissingWorkflowFiles, writeSibuState, } from './workflow-targets.js';
|
|
1
|
+
export { MANDATORY_SKILLS, SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_DATABASE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SELECTABLE_MCP_SERVERS, SELECTABLE_WORKFLOW_SKILLS, SUPPORTED_AGENTS, resolveSelectableMcpServerById, resolveSelectableSkillById, } from './catalog.js';
|
|
2
|
+
export { getSelectedAgentsFromState, getSelectedArchitectureSkillFromState, getSelectedDatabaseSkillsFromState, getSelectedFrameworkSkillsFromState, getSelectedLanguageSkillsFromState, getSelectedMcpServersFromState, getSelectedMcpTargetsForAgents, getSelectedSkillTargetsForAgents, getSelectedWorkflowSkillsFromState, getWorkflowTargets, renderMissingWorkflowFiles, writeSibuState, } from './workflow-targets.js';
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { SIBU_VERSION } from '../version-advisory/index.js';
|
|
4
|
-
import { MANDATORY_SKILLS, SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_DATABASE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SELECTABLE_WORKFLOW_SKILLS, SUPPORTED_AGENTS, } from './index.js';
|
|
4
|
+
import { MANDATORY_SKILLS, SELECTABLE_ARCHITECTURE_SKILLS, SELECTABLE_DATABASE_SKILLS, SELECTABLE_FRAMEWORK_SKILLS, SELECTABLE_LANGUAGE_SKILLS, SELECTABLE_MCP_SERVERS, SELECTABLE_WORKFLOW_SKILLS, SUPPORTED_AGENTS, } from './index.js';
|
|
5
5
|
import { sha256 } from '../../shared/hash.js';
|
|
6
6
|
import { removeUndefinedFields } from '../../shared/object.js';
|
|
7
7
|
import { readExistingState } from '../workflow-state-registry/index.js';
|
|
8
|
-
import { getTemplateVersion, readTemplate, readTemplateManifest, renderSkillRouting } from '../template-catalog-rendering/index.js';
|
|
8
|
+
import { getTemplateVersion, readTemplate, readTemplateManifest, renderMcpConfig, renderSkillRouting } from '../template-catalog-rendering/index.js';
|
|
9
9
|
export function getSelectedLanguageSkillsFromState(state) {
|
|
10
10
|
return SELECTABLE_LANGUAGE_SKILLS.filter((skill) => state.selectedLanguageSkills?.includes(skill.id));
|
|
11
11
|
}
|
|
@@ -21,6 +21,9 @@ export function getSelectedWorkflowSkillsFromState(state) {
|
|
|
21
21
|
export function getSelectedDatabaseSkillsFromState(state) {
|
|
22
22
|
return SELECTABLE_DATABASE_SKILLS.filter((skill) => state.selectedDatabaseSkills?.includes(skill.id));
|
|
23
23
|
}
|
|
24
|
+
export function getSelectedMcpServersFromState(state) {
|
|
25
|
+
return SELECTABLE_MCP_SERVERS.filter((server) => state.selectedMcpServers?.includes(server.id));
|
|
26
|
+
}
|
|
24
27
|
export function getSelectedSkillTargetsForAgents(selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills = [], selectedDatabaseSkills = []) {
|
|
25
28
|
const skillTargets = new Map();
|
|
26
29
|
const selectedSkills = [
|
|
@@ -45,13 +48,49 @@ export function getSelectedSkillTargetsForAgents(selectedAgents, selectedLanguag
|
|
|
45
48
|
}
|
|
46
49
|
return [...skillTargets.values()];
|
|
47
50
|
}
|
|
48
|
-
export function
|
|
49
|
-
|
|
51
|
+
export function getSelectedMcpTargetsForAgents(selectedAgents, selectedMcpServers) {
|
|
52
|
+
if (!selectedMcpServers.some((server) => server.id === 'github')) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
return selectedAgents.flatMap((agent) => {
|
|
56
|
+
if (agent.id === 'codex') {
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
targetRelativePath: '.codex/config.toml',
|
|
60
|
+
templateRelativePath: '.codex/config.toml',
|
|
61
|
+
agentId: 'codex',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
if (agent.id === 'claude') {
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
targetRelativePath: '.mcp.json',
|
|
69
|
+
templateRelativePath: 'mcp/claude/.mcp.json',
|
|
70
|
+
agentId: 'claude',
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
if (agent.id === 'gemini') {
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
targetRelativePath: '.gemini/settings.json',
|
|
78
|
+
templateRelativePath: 'mcp/gemini/settings.json',
|
|
79
|
+
agentId: 'gemini',
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export function getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSkills = [], selectedFrameworkSkills = [], selectedArchitectureSkill, selectedWorkflowSkills = [], selectedDatabaseSkills = [], selectedMcpServers = []) {
|
|
87
|
+
const targets = [
|
|
50
88
|
{
|
|
51
89
|
label: 'AGENTS.md',
|
|
52
90
|
targetPath: path.join(rootPath, 'AGENTS.md'),
|
|
53
91
|
templateRelativePath: 'AGENTS.md',
|
|
54
92
|
requiresProjectOverview: true,
|
|
93
|
+
targetKind: 'agent-support',
|
|
55
94
|
},
|
|
56
95
|
...selectedAgents.flatMap((agent) => {
|
|
57
96
|
if (!agent.targetRelativePath || !agent.templateRelativePath) {
|
|
@@ -63,6 +102,7 @@ export function getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSki
|
|
|
63
102
|
targetPath: path.join(rootPath, agent.targetRelativePath),
|
|
64
103
|
templateRelativePath: agent.templateRelativePath,
|
|
65
104
|
requiresProjectOverview: false,
|
|
105
|
+
targetKind: 'agent-support',
|
|
66
106
|
},
|
|
67
107
|
];
|
|
68
108
|
}),
|
|
@@ -71,12 +111,39 @@ export function getWorkflowTargets(rootPath, selectedAgents, selectedLanguageSki
|
|
|
71
111
|
targetPath: path.join(rootPath, skillTarget.targetRelativePath),
|
|
72
112
|
templateRelativePath: skillTarget.templateRelativePath,
|
|
73
113
|
requiresProjectOverview: false,
|
|
114
|
+
targetKind: 'skill',
|
|
74
115
|
})),
|
|
75
116
|
];
|
|
117
|
+
for (const mcpTarget of getSelectedMcpTargetsForAgents(selectedAgents, selectedMcpServers)) {
|
|
118
|
+
const targetPath = path.join(rootPath, mcpTarget.targetRelativePath);
|
|
119
|
+
const existingTarget = targets.find((target) => target.targetPath === targetPath);
|
|
120
|
+
if (existingTarget) {
|
|
121
|
+
existingTarget.selectedMcpServers = selectedMcpServers;
|
|
122
|
+
existingTarget.mcpConfigAgentId = mcpTarget.agentId;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
targets.push({
|
|
126
|
+
label: mcpTarget.targetRelativePath,
|
|
127
|
+
targetPath,
|
|
128
|
+
templateRelativePath: mcpTarget.templateRelativePath,
|
|
129
|
+
requiresProjectOverview: false,
|
|
130
|
+
targetKind: 'mcp-config',
|
|
131
|
+
mcpConfigAgentId: mcpTarget.agentId,
|
|
132
|
+
selectedMcpServers,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return targets;
|
|
76
136
|
}
|
|
77
|
-
export function renderMissingWorkflowFiles({ missingTargets, overview, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills = [], selectedDatabaseSkills = [], }) {
|
|
137
|
+
export function renderMissingWorkflowFiles({ missingTargets, overview, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills = [], selectedDatabaseSkills = [], selectedMcpServers = [], }) {
|
|
78
138
|
return missingTargets.map((target) => {
|
|
79
139
|
let contents = readTemplate(target.templateRelativePath);
|
|
140
|
+
if (target.mcpConfigAgentId && (target.selectedMcpServers?.length || selectedMcpServers.length)) {
|
|
141
|
+
contents = renderMcpConfig({
|
|
142
|
+
agentId: target.mcpConfigAgentId,
|
|
143
|
+
baseContents: contents,
|
|
144
|
+
selectedMcpServers: target.selectedMcpServers ?? selectedMcpServers,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
80
147
|
if (target.requiresProjectOverview) {
|
|
81
148
|
if (!overview?.trim()) {
|
|
82
149
|
throw new Error('Project overview is required to create AGENTS.md.');
|
|
@@ -91,7 +158,7 @@ export function renderMissingWorkflowFiles({ missingTargets, overview, selectedL
|
|
|
91
158
|
};
|
|
92
159
|
});
|
|
93
160
|
}
|
|
94
|
-
export function writeSibuState({ rootPath, statePath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills = [], selectedDatabaseSkills = [], targets, }) {
|
|
161
|
+
export function writeSibuState({ rootPath, statePath, selectedAgents, selectedLanguageSkills, selectedFrameworkSkills, selectedArchitectureSkill, selectedWorkflowSkills = [], selectedDatabaseSkills = [], selectedMcpServers, targets, }) {
|
|
95
162
|
const previousState = readExistingState(statePath);
|
|
96
163
|
const now = new Date().toISOString();
|
|
97
164
|
const manifest = readTemplateManifest();
|
|
@@ -106,6 +173,7 @@ export function writeSibuState({ rootPath, statePath, selectedAgents, selectedLa
|
|
|
106
173
|
selectedArchitectureSkill: selectedArchitectureSkill?.id,
|
|
107
174
|
selectedWorkflowSkills: selectedWorkflowSkills.map((skill) => skill.id),
|
|
108
175
|
selectedDatabaseSkills: selectedDatabaseSkills.map((skill) => skill.id),
|
|
176
|
+
...(selectedMcpServers !== undefined ? { selectedMcpServers: selectedMcpServers.map((server) => server.id) } : {}),
|
|
109
177
|
managedFiles: Object.fromEntries(targets
|
|
110
178
|
.filter((target) => fs.existsSync(target.targetPath))
|
|
111
179
|
.map((target) => {
|
package/package.json
CHANGED
package/templates/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"templateVersion": "
|
|
2
|
+
"templateVersion": "74",
|
|
3
3
|
"templates": {
|
|
4
4
|
"AGENTS.md": {
|
|
5
5
|
"version": "25",
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
]
|
|
10
10
|
},
|
|
11
11
|
".codex/config.toml": {
|
|
12
|
-
"version": "
|
|
13
|
-
"description": "Codex configuration pointing to AGENTS.md.",
|
|
12
|
+
"version": "2",
|
|
13
|
+
"description": "Codex configuration pointing to AGENTS.md and rendering selected MCP servers with Codex-compatible settings.",
|
|
14
14
|
"changes": [
|
|
15
|
-
"
|
|
15
|
+
"Updates Codex GitHub MCP rendering to use the hosted HTTPS endpoint with bearer_token_env_var instead of Docker command arguments."
|
|
16
16
|
]
|
|
17
17
|
},
|
|
18
18
|
"GEMINI.md": {
|
|
@@ -29,6 +29,20 @@
|
|
|
29
29
|
"Delegates Claude project instructions to AGENTS.md as the source of truth."
|
|
30
30
|
]
|
|
31
31
|
},
|
|
32
|
+
"mcp/claude/.mcp.json": {
|
|
33
|
+
"version": "2",
|
|
34
|
+
"description": "Claude MCP configuration template managed when selectable MCP servers are selected.",
|
|
35
|
+
"changes": [
|
|
36
|
+
"Updates Claude GitHub MCP rendering to use the hosted HTTPS endpoint with an Authorization header instead of Docker."
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"mcp/gemini/settings.json": {
|
|
40
|
+
"version": "2",
|
|
41
|
+
"description": "Gemini MCP configuration template managed when selectable MCP servers are selected.",
|
|
42
|
+
"changes": [
|
|
43
|
+
"Updates Gemini GitHub MCP rendering to use the hosted streamable HTTP endpoint with an Authorization header instead of Docker."
|
|
44
|
+
]
|
|
45
|
+
},
|
|
32
46
|
"skills/clean-code/SKILL.md": {
|
|
33
47
|
"version": "4",
|
|
34
48
|
"description": "Mandatory clean-code skill installed once at the shared .agents/skills workspace path.",
|
|
@@ -116,10 +130,10 @@
|
|
|
116
130
|
]
|
|
117
131
|
},
|
|
118
132
|
"skills/scrum-master-planner/SKILL.md": {
|
|
119
|
-
"version": "
|
|
133
|
+
"version": "8",
|
|
120
134
|
"description": "Mandatory Scrum planner skill for creating pragmatic Epics and User Stories from approved feature and technical design docs.",
|
|
121
135
|
"changes": [
|
|
122
|
-
"
|
|
136
|
+
"Adds optional GitHub issue export guidance after Scrum planning when GitHub MCP tools are available."
|
|
123
137
|
]
|
|
124
138
|
},
|
|
125
139
|
"skills/ai-implementation-planner/SKILL.md": {
|
|
@@ -210,6 +210,28 @@ Before finishing, verify:
|
|
|
210
210
|
- no Story adds scope that is absent from the feature brief or technical design
|
|
211
211
|
- acceptance criteria are testable enough for a reviewer
|
|
212
212
|
|
|
213
|
+
### 6. Optionally export planning artifacts to GitHub Issues
|
|
214
|
+
|
|
215
|
+
After all local Epic and User Story files are written, check whether GitHub MCP tools are available for issue creation, label creation, and native sub-issue mutation.
|
|
216
|
+
|
|
217
|
+
If any required GitHub MCP capability is unavailable, do not offer GitHub export. Finish with the normal local planning response.
|
|
218
|
+
|
|
219
|
+
If the required GitHub MCP capabilities are available:
|
|
220
|
+
|
|
221
|
+
1. Resolve the target repository from the current local repo's GitHub `origin` remote only. Do not ask for or use another repository.
|
|
222
|
+
2. Ask one explicit opt-in question before any GitHub mutation: "GitHub MCP is available. Create GitHub Issues for these Epics and User Stories in the current repo?"
|
|
223
|
+
3. If the user declines, perform no GitHub mutation.
|
|
224
|
+
4. If the user accepts, create a fresh issue set every time. Do not search for duplicates or update existing issues.
|
|
225
|
+
5. Create missing labels before creating issues: `epic`, `user-story`, and `sibu-generated`.
|
|
226
|
+
6. Create one issue per Epic with labels `epic` and `sibu-generated`.
|
|
227
|
+
7. Create one issue per User Story with labels `user-story` and `sibu-generated`.
|
|
228
|
+
8. Attach each User Story issue as a native GitHub sub-issue of its parent Epic issue.
|
|
229
|
+
9. Report created issue numbers or URLs.
|
|
230
|
+
|
|
231
|
+
Keep issue bodies concise and source-grounded. Include the source local doc path and relevant summary, scope, acceptance criteria, or validation notes. Do not modify local Markdown files with GitHub URLs.
|
|
232
|
+
|
|
233
|
+
Native sub-issues are required for this export. Preserve each created User Story issue's GitHub issue `id` because native sub-issue APIs need the child issue ID, not only its issue number. If label creation, issue creation, issue ID access, or native sub-issue attachment fails or is unavailable, fail clearly and do not fall back to Markdown checklists, loose links, GitHub Projects, milestones, assignees, or status tracking.
|
|
234
|
+
|
|
213
235
|
## Final response behavior
|
|
214
236
|
|
|
215
237
|
After writing files, final-answer with only:
|