@we-scrum/cli 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +99 -47
- package/package.json +18 -14
- package/src/cli.ts +73 -38
- package/src/hbs.d.ts +4 -0
- package/src/helpers/auth-helper.ts +1 -1
- package/src/helpers/index.ts +1 -0
- package/src/helpers/template.helper.ts +23 -0
- package/src/helpers/we-scrum.helper.ts +77 -93
- package/src/mcp-server/mcp-server.ts +26 -54
- package/src/mcp-server/tools/get-next-task.ts +19 -143
- package/src/mcp-server/tools/index.ts +0 -3
- package/templates/develop-story-skill.hbs +40 -0
- package/templates/get-next-task-description.hbs +3 -0
- package/templates/get-next-task.hbs +21 -0
- package/tsconfig.json +2 -1
- package/src/mcp-server/tools/complete-task.ts +0 -6
- package/src/mcp-server/tools/get-unit-test-guidelines.ts +0 -15
- package/src/mcp-server/tools/start-task.ts +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@we-scrum/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Cli tool for we-scrum application",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,27 +20,31 @@
|
|
|
20
20
|
"noExternal": [
|
|
21
21
|
"@we-scrum/*",
|
|
22
22
|
"@my-devkit/*"
|
|
23
|
-
]
|
|
23
|
+
],
|
|
24
|
+
"loader": {
|
|
25
|
+
".hbs": "text"
|
|
26
|
+
}
|
|
24
27
|
},
|
|
25
28
|
"dependencies": {
|
|
26
|
-
"@inquirer/prompts": "8.
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"open": "^11.0.0"
|
|
29
|
+
"@inquirer/prompts": "^8.4.2",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
31
|
+
"commander": "^14.0.3",
|
|
32
|
+
"firebase": "^12.12.1",
|
|
33
|
+
"google-auth-library": "^10.6.2",
|
|
34
|
+
"handlebars": "^4.7.9",
|
|
35
|
+
"open": "^11.0.0",
|
|
36
|
+
"zod": "^4.4.1"
|
|
33
37
|
},
|
|
34
38
|
"devDependencies": {
|
|
35
|
-
"@
|
|
36
|
-
"@
|
|
37
|
-
"tsup": "8.5.1",
|
|
38
|
-
"typescript": "5.9.3",
|
|
39
|
+
"@modelcontextprotocol/inspector": "^0.21.2",
|
|
40
|
+
"@types/node": "^22.19.17",
|
|
41
|
+
"tsup": "^8.5.1",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
39
43
|
"@my-devkit/cli": "2.1.0",
|
|
40
44
|
"@my-devkit/core": "1.0.0",
|
|
41
45
|
"@we-scrum/commands": "1.0.0",
|
|
42
|
-
"@we-scrum/enums": "1.0.0",
|
|
43
46
|
"@we-scrum/models": "1.0.0",
|
|
47
|
+
"@we-scrum/enums": "1.0.0",
|
|
44
48
|
"@we-scrum/utils": "1.0.0"
|
|
45
49
|
},
|
|
46
50
|
"scripts": {
|
package/src/cli.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { select } from '@inquirer/prompts';
|
|
3
|
-
import { Logger } from '@my-devkit/core';
|
|
3
|
+
import { assert, Logger } from '@my-devkit/core';
|
|
4
4
|
import { UserProjectModel } from '@we-scrum/models';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { version } from '../package.json';
|
|
7
7
|
|
|
8
|
-
import { AuthHelper, FirebaseHelper, SettingsHelper } from './helpers';
|
|
8
|
+
import { AuthHelper, FirebaseHelper, SettingsHelper, WeScrumHelper } from './helpers';
|
|
9
9
|
import { startMcpServer } from './mcp-server';
|
|
10
10
|
|
|
11
11
|
Logger.registerLogger(new Logger.ConsoleImplementation());
|
|
@@ -14,54 +14,59 @@ const program = new Command();
|
|
|
14
14
|
|
|
15
15
|
program.name('we-scrum').description('CLI for we-scrum application').version(version);
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
async function requireAuth(): Promise<void> {
|
|
18
|
+
const initialized = await AuthHelper.initializeFromStoredCredentials();
|
|
19
|
+
assert(initialized, 'Not authenticated. Please run "we-scrum login" first.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runCommand<T = void>(fn: (options: T) => Promise<void>) {
|
|
23
|
+
return async (options: T) => {
|
|
21
24
|
try {
|
|
22
|
-
await
|
|
25
|
+
await fn(options);
|
|
23
26
|
process.exit(0);
|
|
24
27
|
} catch (error) {
|
|
25
|
-
|
|
28
|
+
if (error?.name === 'ExitPromptError') process.exit(0);
|
|
29
|
+
Logger.error(error?.message ?? 'Command failed');
|
|
26
30
|
process.exit(1);
|
|
27
31
|
}
|
|
28
|
-
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command('login')
|
|
37
|
+
.description('Authenticate with your Google account')
|
|
38
|
+
.action(
|
|
39
|
+
runCommand(async () => {
|
|
40
|
+
await AuthHelper.loginWithGoogle();
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
29
43
|
|
|
30
44
|
program
|
|
31
45
|
.command('logout')
|
|
32
46
|
.description('Log out and remove stored credentials')
|
|
33
|
-
.action(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
.action(
|
|
48
|
+
runCommand(async () => {
|
|
49
|
+
AuthHelper.logout();
|
|
50
|
+
Logger.info('Logged out successfully.');
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
38
53
|
|
|
39
54
|
program
|
|
40
55
|
.command('use-project')
|
|
41
56
|
.description('Select the active project')
|
|
42
57
|
.option('--projectId <id>', 'Set the active project directly without the interactive prompt')
|
|
43
|
-
.action(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!initialized) {
|
|
47
|
-
Logger.error('Not authenticated. Please run "we-scrum login" first.');
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
58
|
+
.action(
|
|
59
|
+
runCommand<{ projectId?: string }>(async (options) => {
|
|
60
|
+
await requireAuth();
|
|
50
61
|
|
|
51
62
|
const projects = await FirebaseHelper.getCollection<UserProjectModel>(`users/${AuthHelper.userId}/projects`);
|
|
52
|
-
|
|
53
|
-
Logger.error('No projects found for your account.');
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
63
|
+
assert(projects.length > 0, 'No projects found for your account. Please create a project in the we-scrum web app first.');
|
|
56
64
|
|
|
57
65
|
let selectedProjectId: string;
|
|
58
66
|
|
|
59
67
|
if (options.projectId) {
|
|
60
68
|
const match = projects.find((p) => p.projectId === options.projectId);
|
|
61
|
-
|
|
62
|
-
Logger.error(`Project "${options.projectId}" not found in your account.`);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
69
|
+
assert(!!match, `Project "${options.projectId}" not found in your account.`);
|
|
65
70
|
selectedProjectId = match.projectId;
|
|
66
71
|
} else {
|
|
67
72
|
selectedProjectId = await select({
|
|
@@ -77,15 +82,45 @@ program
|
|
|
77
82
|
|
|
78
83
|
SettingsHelper.set({ selectedProjectId });
|
|
79
84
|
Logger.info(`Active project set to: ${projects.find((p) => p.projectId === selectedProjectId)?.name}`);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
program
|
|
89
|
+
.command('start-task')
|
|
90
|
+
.description('Mark a specific task as in progress')
|
|
91
|
+
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
92
|
+
.requiredOption('--taskId <id>', 'Unique identifier of the change to act on')
|
|
93
|
+
.action(
|
|
94
|
+
runCommand<{ identificationNumber: string; taskId: string }>(async (options) => {
|
|
95
|
+
await requireAuth();
|
|
96
|
+
await WeScrumHelper.startTask(options.identificationNumber, options.taskId);
|
|
97
|
+
Logger.info('Task started successfully. You can now make changes to the codebase.');
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
program
|
|
102
|
+
.command('complete-task')
|
|
103
|
+
.description('Mark the current in-progress task as done')
|
|
104
|
+
.requiredOption('--identificationNumber <id>', 'Story identification number')
|
|
105
|
+
.requiredOption('--taskId <id>', 'Unique identifier of the change to act on')
|
|
106
|
+
.action(
|
|
107
|
+
runCommand<{ identificationNumber: string; taskId: string }>(async (options) => {
|
|
108
|
+
await requireAuth();
|
|
109
|
+
await WeScrumHelper.completeTask(options.identificationNumber, options.taskId);
|
|
110
|
+
Logger.info('Task completed successfully.');
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
program
|
|
115
|
+
.command('synchronize-development-policies')
|
|
116
|
+
.description('Sync development guidelines from we-scrum into .claude/')
|
|
117
|
+
.action(
|
|
118
|
+
runCommand(async () => {
|
|
119
|
+
await requireAuth();
|
|
120
|
+
const count = await WeScrumHelper.synchronizeDevelopmentPolicies(process.cwd());
|
|
121
|
+
Logger.info(`Synchronized ${count} development ${count === 1 ? 'policy' : 'policies'}.`);
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
89
124
|
|
|
90
125
|
program
|
|
91
126
|
.command('mcp')
|
package/src/hbs.d.ts
ADDED
package/src/helpers/index.ts
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Handlebars from 'handlebars';
|
|
2
|
+
import getNextTaskDescription from '../../templates/get-next-task-description.hbs';
|
|
3
|
+
import getNextTask from '../../templates/get-next-task.hbs';
|
|
4
|
+
|
|
5
|
+
const sources = {
|
|
6
|
+
'get-next-task': getNextTask,
|
|
7
|
+
'get-next-task-description': getNextTaskDescription,
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export type TemplateName = keyof typeof sources;
|
|
11
|
+
|
|
12
|
+
const compiled = new Map<TemplateName, Handlebars.TemplateDelegate>();
|
|
13
|
+
|
|
14
|
+
function getTemplate(name: TemplateName): Handlebars.TemplateDelegate {
|
|
15
|
+
if (!compiled.has(name)) {
|
|
16
|
+
compiled.set(name, Handlebars.compile(sources[name]));
|
|
17
|
+
}
|
|
18
|
+
return compiled.get(name)!;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function renderTemplate(name: TemplateName, context?: Record<string, unknown>): string {
|
|
22
|
+
return getTemplate(name)(context ?? {}).trim();
|
|
23
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _sortBy, Command, serialize, TypeHelper } from '@my-devkit/core';
|
|
1
|
+
import { _sortBy, assert, Command, serialize, TypeHelper } from '@my-devkit/core';
|
|
2
2
|
import {
|
|
3
3
|
MarkEnumerationChangeAsDoneCommand,
|
|
4
4
|
MarkObjectChangeAsDoneCommand,
|
|
@@ -23,13 +23,16 @@ import {
|
|
|
23
23
|
ProjectStorySectionModelTask,
|
|
24
24
|
} from '@we-scrum/models';
|
|
25
25
|
import { DevelopmentPolicyHelper } from '@we-scrum/utils';
|
|
26
|
-
import {
|
|
26
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
27
28
|
import { AuthHelper } from './auth-helper';
|
|
28
29
|
import { FirebaseHelper } from './firebase-helper';
|
|
29
30
|
import { SettingsHelper } from './settings-helper';
|
|
30
31
|
|
|
31
32
|
export class WeScrumHelper {
|
|
32
|
-
private static backendUrl = 'https://europe-west1-we-scrum-prod.cloudfunctions.net';
|
|
33
|
+
private static readonly backendUrl = 'https://europe-west1-we-scrum-prod.cloudfunctions.net';
|
|
34
|
+
private static readonly claudeSubdir = '.claude';
|
|
35
|
+
private static readonly relPoliciesDir = `${WeScrumHelper.claudeSubdir}/development-policies`;
|
|
33
36
|
constructor(private userId: string) {}
|
|
34
37
|
|
|
35
38
|
public static get projectId(): string {
|
|
@@ -56,11 +59,33 @@ export class WeScrumHelper {
|
|
|
56
59
|
return task;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
public static async
|
|
60
|
-
|
|
62
|
+
public static async getChangeById(identificationNumber: string, changeId: string) {
|
|
63
|
+
this.assertProjectIsSelected();
|
|
64
|
+
|
|
65
|
+
const story = await this.findStoryByIterationNumber(identificationNumber);
|
|
66
|
+
assert(!!story, `Story "${identificationNumber}" not found in the active project.`);
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
const sections = await this.getStorySections(story.storyId);
|
|
69
|
+
|
|
70
|
+
const change = _sortBy(sections, (s) => s.name)
|
|
71
|
+
.flatMap((s) => [...s.enumerationChanges, ...s.objectChanges, ...s.routeChanges, ...s.relationChanges, ...s.tasks])
|
|
72
|
+
.find((t) => {
|
|
73
|
+
if (t instanceof ProjectStorySectionModelEnumerationChange) return t.enumerationChangeId === changeId;
|
|
74
|
+
if (t instanceof ProjectStorySectionModelObjectChange) return t.objectChangeId === changeId;
|
|
75
|
+
if (t instanceof ProjectStorySectionModelRouteChange) return t.routeChangeId === changeId;
|
|
76
|
+
if (t instanceof ProjectStorySectionModelRelationChange) return t.relationChangeId === changeId;
|
|
77
|
+
if (t instanceof ProjectStorySectionModelTask) return t.taskId === changeId;
|
|
78
|
+
return false;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert(!!change, `Change "${changeId}" not found in story "${identificationNumber}".`);
|
|
82
|
+
return change;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public static async startTask(identificationNumber: string, taskId: string): Promise<void> {
|
|
86
|
+
const change = await this.getChangeById(identificationNumber, taskId);
|
|
87
|
+
|
|
88
|
+
assert(change.progress.status === ProgressStatus.ToDo, `Task is not in ToDo status (current status: ${change.progress.status}).`);
|
|
64
89
|
|
|
65
90
|
if (change instanceof ProjectStorySectionModelEnumerationChange) {
|
|
66
91
|
await this.post(
|
|
@@ -87,10 +112,9 @@ export class WeScrumHelper {
|
|
|
87
112
|
}
|
|
88
113
|
}
|
|
89
114
|
|
|
90
|
-
public static async completeTask(identificationNumber: string): Promise<void> {
|
|
91
|
-
const change = await this.
|
|
115
|
+
public static async completeTask(identificationNumber: string, taskId: string): Promise<void> {
|
|
116
|
+
const change = await this.getChangeById(identificationNumber, taskId);
|
|
92
117
|
|
|
93
|
-
assert(!!change, `All tasks for story "${identificationNumber}" are complete.`);
|
|
94
118
|
assert(change.progress.status === ProgressStatus.Doing, `Task is not in progress (current status: ${change.progress.status}).`);
|
|
95
119
|
|
|
96
120
|
if (change instanceof ProjectStorySectionModelEnumerationChange) {
|
|
@@ -124,6 +148,20 @@ export class WeScrumHelper {
|
|
|
124
148
|
return DevelopmentPolicyHelper.getMatchingDevelopmentPolicy(change, developmentPolicies);
|
|
125
149
|
}
|
|
126
150
|
|
|
151
|
+
public static getDevelopmentPolicyPaths(
|
|
152
|
+
policy: DevelopmentPolicyModel | null,
|
|
153
|
+
projectDir = '.',
|
|
154
|
+
): {
|
|
155
|
+
guidelinesPath: string | null;
|
|
156
|
+
unitTestsPath: string | null;
|
|
157
|
+
} {
|
|
158
|
+
const policiesDir = join(projectDir, this.relPoliciesDir);
|
|
159
|
+
return {
|
|
160
|
+
guidelinesPath: policy?.slug ? join(policiesDir, `develop-${policy.slug}.md`) : null,
|
|
161
|
+
unitTestsPath: policy?.slug ? join(policiesDir, `test-${policy.slug}.md`) : null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
127
165
|
private static async findStoryByIterationNumber(identificationNumber: string) {
|
|
128
166
|
return FirebaseHelper.findDocument<ProjectStoryModel>(`/projects/${this.projectId}/stories`, [
|
|
129
167
|
['identificationNumber', '==', identificationNumber],
|
|
@@ -134,92 +172,38 @@ export class WeScrumHelper {
|
|
|
134
172
|
return FirebaseHelper.getCollection<ProjectStorySectionModel>(`projects/${this.projectId}/stories/${storyId}/sections`);
|
|
135
173
|
}
|
|
136
174
|
|
|
137
|
-
|
|
175
|
+
public static async getDevelopmentPolicies() {
|
|
138
176
|
return FirebaseHelper.getCollection<DevelopmentPolicyModel>(`/projects/${this.projectId}/development-policies`);
|
|
139
177
|
}
|
|
140
178
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// const createCommand = TypeHelper.transform(CreateObjectCommand, {
|
|
170
|
-
// projectId: this.projectId,
|
|
171
|
-
// name: object.name,
|
|
172
|
-
// });
|
|
173
|
-
// const objectId = await this.post<CreateObjectCommand, string>('object-management/create-object', createCommand);
|
|
174
|
-
|
|
175
|
-
// const propertyMap = new Map<string, string>();
|
|
176
|
-
// for (const property of object.properties) {
|
|
177
|
-
// const objectPropertyId = guid();
|
|
178
|
-
// const propertyCommand = TypeHelper.transform(CreateObjectPropertyCommand, {
|
|
179
|
-
// objectId,
|
|
180
|
-
// objectPropertyId,
|
|
181
|
-
// objectPropertyParentId: propertyMap.get(property.parent) || null,
|
|
182
|
-
// name: property.name,
|
|
183
|
-
// objectPropertyType: property.type,
|
|
184
|
-
// isArray: property.isArray,
|
|
185
|
-
// enumerationId: property.enumerationName ? getEnumerationId(property.enumerationName) : null,
|
|
186
|
-
// });
|
|
187
|
-
// await this.post<CreateObjectPropertyCommand, string>('object-management/create-object-property', propertyCommand);
|
|
188
|
-
|
|
189
|
-
// propertyMap.set(property.path, objectPropertyId);
|
|
190
|
-
// }
|
|
191
|
-
|
|
192
|
-
// return objectId;
|
|
193
|
-
// }
|
|
194
|
-
|
|
195
|
-
// public async createHandler(handler: Handler, getObjectId: (name: string) => string): Promise<void> {
|
|
196
|
-
// Logger.info(`Creating handler ${handler.name}`);
|
|
197
|
-
|
|
198
|
-
// for (const relation of handler.relations) {
|
|
199
|
-
// const createCommand = TypeHelper.transform(CreateRelationCommand, {
|
|
200
|
-
// projectId: this.projectId,
|
|
201
|
-
// boundedContext: handler.boundedContext,
|
|
202
|
-
// handlerName: handler.name,
|
|
203
|
-
// objectIds: relation.map((o) => getObjectId(o)),
|
|
204
|
-
// subscriptionName: null,
|
|
205
|
-
// });
|
|
206
|
-
// await this.post<CreateRelationCommand, string>('relation-management/create-relation', createCommand);
|
|
207
|
-
// }
|
|
208
|
-
// }
|
|
209
|
-
|
|
210
|
-
// public async createRoute(route: Route, getObjectId: (name: string) => string): Promise<string> {
|
|
211
|
-
// Logger.info(`Creating route ${route.method} ${route.path}`);
|
|
212
|
-
|
|
213
|
-
// const createCommand = TypeHelper.transform(CreateRouteCommand, {
|
|
214
|
-
// projectId: this.projectId,
|
|
215
|
-
// method: route.method,
|
|
216
|
-
// path: route.path,
|
|
217
|
-
// objectId: getObjectId(route.object),
|
|
218
|
-
// permission: null,
|
|
219
|
-
// });
|
|
220
|
-
|
|
221
|
-
// return this.post<CreateRouteCommand, string>('route-management/create-route', createCommand);
|
|
222
|
-
// }
|
|
179
|
+
public static async synchronizeDevelopmentPolicies(projectDir: string): Promise<number> {
|
|
180
|
+
this.assertProjectIsSelected();
|
|
181
|
+
|
|
182
|
+
const claudeDir = join(projectDir, this.claudeSubdir);
|
|
183
|
+
assert(
|
|
184
|
+
existsSync(claudeDir),
|
|
185
|
+
`No ${this.claudeSubdir} directory found in ${projectDir}. Please run this command from the root of your project.`,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const policiesDir = join(projectDir, this.relPoliciesDir);
|
|
189
|
+
|
|
190
|
+
rmSync(policiesDir, { recursive: true, force: true });
|
|
191
|
+
mkdirSync(policiesDir, { recursive: true });
|
|
192
|
+
|
|
193
|
+
const policies = await this.getDevelopmentPolicies();
|
|
194
|
+
|
|
195
|
+
for (const policy of policies) {
|
|
196
|
+
const { guidelinesPath, unitTestsPath } = this.getDevelopmentPolicyPaths(policy, projectDir);
|
|
197
|
+
if (policy.codeGuidelines && guidelinesPath) {
|
|
198
|
+
writeFileSync(guidelinesPath, policy.codeGuidelines);
|
|
199
|
+
}
|
|
200
|
+
if (policy.unitTestsGuidelines && unitTestsPath) {
|
|
201
|
+
writeFileSync(unitTestsPath, policy.unitTestsGuidelines);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return policies.length;
|
|
206
|
+
}
|
|
223
207
|
|
|
224
208
|
private static async post<C extends Command, R>(route: string, command: C): Promise<R> {
|
|
225
209
|
const userIdToken = await AuthHelper.getUserIdToken();
|
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
3
5
|
import { z } from 'zod';
|
|
4
6
|
|
|
5
|
-
import { AuthHelper } from '@helpers';
|
|
6
|
-
import
|
|
7
|
+
import { AuthHelper, renderTemplate } from '@helpers';
|
|
8
|
+
import developStorySkill from '../../templates/develop-story-skill.hbs';
|
|
9
|
+
import { getNextTask } from './tools';
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return async ({ identificationNumber }: { identificationNumber: string }) => {
|
|
14
|
-
try {
|
|
15
|
-
const result = await fn(identificationNumber);
|
|
16
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
17
|
-
} catch (error) {
|
|
18
|
-
return {
|
|
19
|
-
content: [{ type: 'text' as const, text: JSON.stringify({ error: error.message }) }],
|
|
20
|
-
isError: true,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
};
|
|
11
|
+
function installSkills(): void {
|
|
12
|
+
const skillDir = join(process.cwd(), '.claude', 'skills', 'develop-story');
|
|
13
|
+
if (!existsSync(join(process.cwd(), '.claude'))) return;
|
|
14
|
+
mkdirSync(skillDir, { recursive: true });
|
|
15
|
+
writeFileSync(join(skillDir, 'SKILL.md'), developStorySkill);
|
|
24
16
|
}
|
|
25
17
|
|
|
26
18
|
export async function startMcpServer(): Promise<void> {
|
|
19
|
+
installSkills();
|
|
20
|
+
|
|
27
21
|
const initialized = await AuthHelper.initializeFromStoredCredentials();
|
|
28
22
|
if (!initialized) {
|
|
29
23
|
process.stderr.write('Error: Not authenticated. Please run "we-scrum login" first.\n');
|
|
@@ -35,44 +29,22 @@ export async function startMcpServer(): Promise<void> {
|
|
|
35
29
|
server.registerTool(
|
|
36
30
|
'get_next_task',
|
|
37
31
|
{
|
|
38
|
-
description:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Also returns storyId, sectionId and taskId needed to call start_task and complete_task.`,
|
|
43
|
-
inputSchema: identificationNumberSchema,
|
|
44
|
-
},
|
|
45
|
-
createToolCallback(getNextTask),
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
server.registerTool(
|
|
49
|
-
'start_task',
|
|
50
|
-
{
|
|
51
|
-
description: `Mark the next pending task of a story as in progress.
|
|
52
|
-
Call this before making any changes to the codebase, as instructed by get_next_task.`,
|
|
53
|
-
inputSchema: identificationNumberSchema,
|
|
54
|
-
},
|
|
55
|
-
createToolCallback(startTask),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
server.registerTool(
|
|
59
|
-
'get_unit_test_guidelines',
|
|
60
|
-
{
|
|
61
|
-
description: `Get unit test writing guidelines for the current pending task of a story.
|
|
62
|
-
Call this tool when get_next_task returns unitTests instructions to run it.`,
|
|
63
|
-
inputSchema: identificationNumberSchema,
|
|
32
|
+
description: renderTemplate('get-next-task-description'),
|
|
33
|
+
inputSchema: z.object({
|
|
34
|
+
identificationNumber: z.string().describe('The story identification number, e.g. "144154"'),
|
|
35
|
+
}),
|
|
64
36
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
37
|
+
async ({ identificationNumber }) => {
|
|
38
|
+
try {
|
|
39
|
+
const markdown = await getNextTask(identificationNumber);
|
|
40
|
+
return { content: [{ type: 'text' as const, text: markdown }] };
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: 'text' as const, text: JSON.stringify({ error: error.message }) }],
|
|
44
|
+
isError: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
74
47
|
},
|
|
75
|
-
createToolCallback(completeTask),
|
|
76
48
|
);
|
|
77
49
|
|
|
78
50
|
const transport = new StdioServerTransport();
|