@projitive/mcp 2.0.3 → 2.1.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/output/package.json +8 -2
- package/output/source/common/artifacts.js +1 -1
- package/output/source/common/artifacts.test.js +11 -11
- package/output/source/common/errors.js +19 -19
- package/output/source/common/errors.test.js +59 -0
- package/output/source/common/files.js +30 -19
- package/output/source/common/files.test.js +14 -14
- package/output/source/common/index.js +11 -10
- package/output/source/common/linter.js +29 -27
- package/output/source/common/linter.test.js +9 -9
- package/output/source/common/markdown.js +3 -3
- package/output/source/common/markdown.test.js +15 -15
- package/output/source/common/response.js +91 -107
- package/output/source/common/response.test.js +30 -30
- package/output/source/common/store.js +40 -40
- package/output/source/common/store.test.js +72 -72
- package/output/source/common/tool.js +43 -0
- package/output/source/common/types.js +3 -3
- package/output/source/common/utils.js +8 -8
- package/output/source/common/utils.test.js +48 -0
- package/output/source/index.js +16 -16
- package/output/source/index.runtime.test.js +57 -0
- package/output/source/index.test.js +64 -64
- package/output/source/prompts/index.js +3 -3
- package/output/source/prompts/index.test.js +23 -0
- package/output/source/prompts/quickStart.js +96 -96
- package/output/source/prompts/quickStart.test.js +24 -0
- package/output/source/prompts/taskDiscovery.js +184 -184
- package/output/source/prompts/taskDiscovery.test.js +24 -0
- package/output/source/prompts/taskExecution.js +164 -148
- package/output/source/prompts/taskExecution.test.js +27 -0
- package/output/source/resources/designs.js +26 -26
- package/output/source/resources/designs.resources.test.js +52 -0
- package/output/source/resources/designs.test.js +88 -88
- package/output/source/resources/governance.js +19 -19
- package/output/source/resources/governance.test.js +35 -0
- package/output/source/resources/index.js +2 -2
- package/output/source/resources/index.test.js +18 -0
- package/output/source/resources/readme.js +7 -7
- package/output/source/resources/readme.test.js +113 -113
- package/output/source/resources/reports.js +10 -10
- package/output/source/resources/reports.test.js +83 -83
- package/output/source/tools/index.js +3 -3
- package/output/source/tools/index.test.js +23 -0
- package/output/source/tools/project.js +330 -377
- package/output/source/tools/project.test.js +308 -175
- package/output/source/tools/roadmap.js +236 -255
- package/output/source/tools/roadmap.test.js +241 -46
- package/output/source/tools/task.js +770 -652
- package/output/source/tools/task.test.js +433 -105
- package/output/source/types.js +28 -22
- package/package.json +8 -2
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { afterEach, describe, expect, it } from
|
|
4
|
-
import { ensureStore, getMarkdownViewState, getStoreVersion, loadActionableTasksFromStore, loadRoadmapIdsFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, markMarkdownViewBuilt, markMarkdownViewDirty, replaceRoadmapsInStore, replaceTasksInStore, upsertRoadmapInStore, upsertTaskInStore, } from
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { ensureStore, getMarkdownViewState, getStoreVersion, loadActionableTasksFromStore, loadRoadmapIdsFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, markMarkdownViewBuilt, markMarkdownViewDirty, replaceRoadmapsInStore, replaceTasksInStore, upsertRoadmapInStore, upsertTaskInStore, } from './store.js';
|
|
5
5
|
const tempPaths = [];
|
|
6
6
|
async function createTempDbPath() {
|
|
7
|
-
const sandboxRoot = path.join(process.cwd(),
|
|
7
|
+
const sandboxRoot = path.join(process.cwd(), '.tmp', 'store-tests');
|
|
8
8
|
await fs.mkdir(sandboxRoot, { recursive: true });
|
|
9
|
-
const dir = await fs.mkdtemp(path.join(sandboxRoot,
|
|
9
|
+
const dir = await fs.mkdtemp(path.join(sandboxRoot, 'case-'));
|
|
10
10
|
tempPaths.push(dir);
|
|
11
|
-
return path.join(dir,
|
|
11
|
+
return path.join(dir, '.projitive');
|
|
12
12
|
}
|
|
13
13
|
async function readRawStore(dbPath) {
|
|
14
|
-
const content = await fs.readFile(dbPath,
|
|
14
|
+
const content = await fs.readFile(dbPath, 'utf8');
|
|
15
15
|
return JSON.parse(content);
|
|
16
16
|
}
|
|
17
17
|
function task(input) {
|
|
18
18
|
return {
|
|
19
19
|
id: input.id,
|
|
20
20
|
title: input.title,
|
|
21
|
-
status: input.status ??
|
|
22
|
-
owner: input.owner ??
|
|
23
|
-
summary: input.summary ??
|
|
21
|
+
status: input.status ?? 'TODO',
|
|
22
|
+
owner: input.owner ?? '',
|
|
23
|
+
summary: input.summary ?? '',
|
|
24
24
|
updatedAt: input.updatedAt ?? new Date().toISOString(),
|
|
25
25
|
links: input.links ?? [],
|
|
26
26
|
roadmapRefs: input.roadmapRefs ?? [],
|
|
@@ -32,7 +32,7 @@ function milestone(input) {
|
|
|
32
32
|
return {
|
|
33
33
|
id: input.id,
|
|
34
34
|
title: input.title,
|
|
35
|
-
status: input.status ??
|
|
35
|
+
status: input.status ?? 'active',
|
|
36
36
|
time: input.time,
|
|
37
37
|
updatedAt: input.updatedAt ?? new Date().toISOString(),
|
|
38
38
|
};
|
|
@@ -40,12 +40,12 @@ function milestone(input) {
|
|
|
40
40
|
afterEach(async () => {
|
|
41
41
|
await Promise.all(tempPaths.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
|
42
42
|
});
|
|
43
|
-
describe(
|
|
44
|
-
it(
|
|
43
|
+
describe('store', () => {
|
|
44
|
+
it('initializes JSON store with default meta/view state', async () => {
|
|
45
45
|
const dbPath = await createTempDbPath();
|
|
46
46
|
await ensureStore(dbPath);
|
|
47
47
|
const store = await readRawStore(dbPath);
|
|
48
|
-
expect(store.schema).toBe(
|
|
48
|
+
expect(store.schema).toBe('projitive-json-store');
|
|
49
49
|
expect(store.meta.store_schema_version).toBe(3);
|
|
50
50
|
expect(store.meta.tasks_version).toBe(0);
|
|
51
51
|
expect(store.meta.roadmaps_version).toBe(0);
|
|
@@ -55,65 +55,65 @@ describe("store", () => {
|
|
|
55
55
|
expect(store.view_state.roadmaps_markdown.lastSourceVersion).toBe(0);
|
|
56
56
|
expect(Array.isArray(store.migration_history)).toBe(true);
|
|
57
57
|
});
|
|
58
|
-
it(
|
|
58
|
+
it('tracks view state dirty/build transitions', async () => {
|
|
59
59
|
const dbPath = await createTempDbPath();
|
|
60
60
|
await ensureStore(dbPath);
|
|
61
|
-
const initial = await getMarkdownViewState(dbPath,
|
|
61
|
+
const initial = await getMarkdownViewState(dbPath, 'tasks_markdown');
|
|
62
62
|
expect(initial.dirty).toBe(true);
|
|
63
|
-
await markMarkdownViewBuilt(dbPath,
|
|
64
|
-
const built = await getMarkdownViewState(dbPath,
|
|
63
|
+
await markMarkdownViewBuilt(dbPath, 'tasks_markdown', 7, '2026-03-13T00:00:00.000Z');
|
|
64
|
+
const built = await getMarkdownViewState(dbPath, 'tasks_markdown');
|
|
65
65
|
expect(built.dirty).toBe(false);
|
|
66
66
|
expect(built.lastSourceVersion).toBe(7);
|
|
67
|
-
expect(built.lastBuiltAt).toBe(
|
|
68
|
-
await markMarkdownViewDirty(dbPath,
|
|
69
|
-
const dirtyAgain = await getMarkdownViewState(dbPath,
|
|
67
|
+
expect(built.lastBuiltAt).toBe('2026-03-13T00:00:00.000Z');
|
|
68
|
+
await markMarkdownViewDirty(dbPath, 'tasks_markdown');
|
|
69
|
+
const dirtyAgain = await getMarkdownViewState(dbPath, 'tasks_markdown');
|
|
70
70
|
expect(dirtyAgain.dirty).toBe(true);
|
|
71
71
|
expect(dirtyAgain.lastSourceVersion).toBe(7);
|
|
72
72
|
});
|
|
73
|
-
it(
|
|
73
|
+
it('upserts tasks and updates source/view versions', async () => {
|
|
74
74
|
const dbPath = await createTempDbPath();
|
|
75
75
|
await ensureStore(dbPath);
|
|
76
76
|
await upsertTaskInStore(dbPath, task({
|
|
77
|
-
id:
|
|
78
|
-
title:
|
|
79
|
-
status:
|
|
80
|
-
owner:
|
|
81
|
-
summary:
|
|
82
|
-
updatedAt:
|
|
83
|
-
roadmapRefs: [
|
|
77
|
+
id: 'TASK-0001',
|
|
78
|
+
title: 'First',
|
|
79
|
+
status: 'TODO',
|
|
80
|
+
owner: 'alice',
|
|
81
|
+
summary: 'first',
|
|
82
|
+
updatedAt: '2026-03-13T00:00:00.000Z',
|
|
83
|
+
roadmapRefs: ['ROADMAP-0001'],
|
|
84
84
|
}));
|
|
85
85
|
await upsertTaskInStore(dbPath, task({
|
|
86
|
-
id:
|
|
87
|
-
title:
|
|
88
|
-
status:
|
|
89
|
-
owner:
|
|
90
|
-
summary:
|
|
91
|
-
updatedAt:
|
|
92
|
-
roadmapRefs: [
|
|
93
|
-
links: [
|
|
86
|
+
id: 'TASK-0001',
|
|
87
|
+
title: 'First Updated',
|
|
88
|
+
status: 'IN_PROGRESS',
|
|
89
|
+
owner: 'alice',
|
|
90
|
+
summary: 'updated',
|
|
91
|
+
updatedAt: '2026-03-13T01:00:00.000Z',
|
|
92
|
+
roadmapRefs: ['ROADMAP-0001'],
|
|
93
|
+
links: ['reports/r1.md'],
|
|
94
94
|
}));
|
|
95
95
|
const tasks = await loadTasksFromStore(dbPath);
|
|
96
96
|
expect(tasks).toHaveLength(1);
|
|
97
|
-
expect(tasks[0].title).toBe(
|
|
98
|
-
expect(tasks[0].status).toBe(
|
|
99
|
-
expect(tasks[0].links).toEqual([
|
|
100
|
-
const tasksVersion = await getStoreVersion(dbPath,
|
|
97
|
+
expect(tasks[0].title).toBe('First Updated');
|
|
98
|
+
expect(tasks[0].status).toBe('IN_PROGRESS');
|
|
99
|
+
expect(tasks[0].links).toEqual(['reports/r1.md']);
|
|
100
|
+
const tasksVersion = await getStoreVersion(dbPath, 'tasks');
|
|
101
101
|
expect(tasksVersion).toBe(2);
|
|
102
|
-
const viewState = await getMarkdownViewState(dbPath,
|
|
102
|
+
const viewState = await getMarkdownViewState(dbPath, 'tasks_markdown');
|
|
103
103
|
expect(viewState.dirty).toBe(true);
|
|
104
104
|
const store = await readRawStore(dbPath);
|
|
105
|
-
const updated = store.tasks.find((item) => item.id ===
|
|
105
|
+
const updated = store.tasks.find((item) => item.id === 'TASK-0001');
|
|
106
106
|
expect(updated?.recordVersion).toBe(2);
|
|
107
107
|
});
|
|
108
|
-
it(
|
|
108
|
+
it('replaces task set and computes actionable ranking/stats', async () => {
|
|
109
109
|
const dbPath = await createTempDbPath();
|
|
110
110
|
await ensureStore(dbPath);
|
|
111
111
|
await replaceTasksInStore(dbPath, [
|
|
112
|
-
task({ id:
|
|
113
|
-
task({ id:
|
|
114
|
-
task({ id:
|
|
115
|
-
task({ id:
|
|
116
|
-
task({ id:
|
|
112
|
+
task({ id: 'TASK-0001', title: 'Todo older', status: 'TODO', updatedAt: '2026-03-10T00:00:00.000Z' }),
|
|
113
|
+
task({ id: 'TASK-0002', title: 'In progress', status: 'IN_PROGRESS', updatedAt: '2026-03-12T00:00:00.000Z' }),
|
|
114
|
+
task({ id: 'TASK-0003', title: 'Todo newer', status: 'TODO', updatedAt: '2026-03-13T00:00:00.000Z' }),
|
|
115
|
+
task({ id: 'TASK-0004', title: 'Blocked', status: 'BLOCKED', updatedAt: '2026-03-11T00:00:00.000Z' }),
|
|
116
|
+
task({ id: 'TASK-0005', title: 'Done', status: 'DONE', updatedAt: '2026-03-09T00:00:00.000Z' }),
|
|
117
117
|
]);
|
|
118
118
|
const stats = await loadTaskStatusStatsFromStore(dbPath);
|
|
119
119
|
expect(stats.todo).toBe(2);
|
|
@@ -121,44 +121,44 @@ describe("store", () => {
|
|
|
121
121
|
expect(stats.blocked).toBe(1);
|
|
122
122
|
expect(stats.done).toBe(1);
|
|
123
123
|
expect(stats.total).toBe(5);
|
|
124
|
-
expect(stats.latestUpdatedAt).toBe(
|
|
124
|
+
expect(stats.latestUpdatedAt).toBe('2026-03-13T00:00:00.000Z');
|
|
125
125
|
const actionable = await loadActionableTasksFromStore(dbPath, 2);
|
|
126
126
|
expect(actionable).toHaveLength(2);
|
|
127
|
-
expect(actionable[0].id).toBe(
|
|
128
|
-
expect(actionable[1].id).toBe(
|
|
129
|
-
const tasksVersion = await getStoreVersion(dbPath,
|
|
127
|
+
expect(actionable[0].id).toBe('TASK-0002');
|
|
128
|
+
expect(actionable[1].id).toBe('TASK-0003');
|
|
129
|
+
const tasksVersion = await getStoreVersion(dbPath, 'tasks');
|
|
130
130
|
expect(tasksVersion).toBe(1);
|
|
131
131
|
});
|
|
132
|
-
it(
|
|
132
|
+
it('upserts/replaces roadmaps and updates versions', async () => {
|
|
133
133
|
const dbPath = await createTempDbPath();
|
|
134
134
|
await ensureStore(dbPath);
|
|
135
135
|
await upsertRoadmapInStore(dbPath, milestone({
|
|
136
|
-
id:
|
|
137
|
-
title:
|
|
138
|
-
status:
|
|
139
|
-
updatedAt:
|
|
136
|
+
id: 'ROADMAP-0001',
|
|
137
|
+
title: 'Phase 1',
|
|
138
|
+
status: 'active',
|
|
139
|
+
updatedAt: '2026-03-10T00:00:00.000Z',
|
|
140
140
|
}));
|
|
141
141
|
await upsertRoadmapInStore(dbPath, milestone({
|
|
142
|
-
id:
|
|
143
|
-
title:
|
|
144
|
-
status:
|
|
145
|
-
time:
|
|
146
|
-
updatedAt:
|
|
142
|
+
id: 'ROADMAP-0001',
|
|
143
|
+
title: 'Phase 1 done',
|
|
144
|
+
status: 'done',
|
|
145
|
+
time: '2026-Q1',
|
|
146
|
+
updatedAt: '2026-03-11T00:00:00.000Z',
|
|
147
147
|
}));
|
|
148
148
|
const list1 = await loadRoadmapsFromStore(dbPath);
|
|
149
149
|
expect(list1).toHaveLength(1);
|
|
150
|
-
expect(list1[0].title).toBe(
|
|
151
|
-
expect(list1[0].status).toBe(
|
|
152
|
-
expect(list1[0].time).toBe(
|
|
150
|
+
expect(list1[0].title).toBe('Phase 1 done');
|
|
151
|
+
expect(list1[0].status).toBe('done');
|
|
152
|
+
expect(list1[0].time).toBe('2026-Q1');
|
|
153
153
|
await replaceRoadmapsInStore(dbPath, [
|
|
154
|
-
milestone({ id:
|
|
155
|
-
milestone({ id:
|
|
154
|
+
milestone({ id: 'ROADMAP-0002', title: 'Phase 2', status: 'active', updatedAt: '2026-03-12T00:00:00.000Z' }),
|
|
155
|
+
milestone({ id: 'ROADMAP-0003', title: 'Phase 3', status: 'done', updatedAt: '2026-03-13T00:00:00.000Z' }),
|
|
156
156
|
]);
|
|
157
157
|
const ids = await loadRoadmapIdsFromStore(dbPath);
|
|
158
|
-
expect(ids).toEqual([
|
|
159
|
-
const roadmapsVersion = await getStoreVersion(dbPath,
|
|
158
|
+
expect(ids).toEqual(['ROADMAP-0003', 'ROADMAP-0002']);
|
|
159
|
+
const roadmapsVersion = await getStoreVersion(dbPath, 'roadmaps');
|
|
160
160
|
expect(roadmapsVersion).toBe(3);
|
|
161
|
-
const viewState = await getMarkdownViewState(dbPath,
|
|
161
|
+
const viewState = await getMarkdownViewState(dbPath, 'roadmaps_markdown');
|
|
162
162
|
expect(viewState.dirty).toBe(true);
|
|
163
163
|
});
|
|
164
164
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { asText, renderToolResponseMarkdown, renderErrorMarkdown, evidenceSection, guidanceSection, lintSection, nextCallSection, summarySection, } from './response.js';
|
|
2
|
+
export class ToolExecutionError extends Error {
|
|
3
|
+
nextSteps;
|
|
4
|
+
retryExample;
|
|
5
|
+
constructor(message, nextSteps = [], retryExample) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.nextSteps = nextSteps;
|
|
8
|
+
this.retryExample = retryExample;
|
|
9
|
+
this.name = 'ToolExecutionError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function createGovernedTool(spec) {
|
|
13
|
+
const cb = async (input) => {
|
|
14
|
+
try {
|
|
15
|
+
const ctx = { now: new Date().toISOString() };
|
|
16
|
+
const data = await spec.execute(input, ctx);
|
|
17
|
+
const markdown = renderToolResponseMarkdown({
|
|
18
|
+
toolName: spec.name,
|
|
19
|
+
sections: [
|
|
20
|
+
summarySection(await spec.summary(data, ctx)),
|
|
21
|
+
evidenceSection(await (spec.evidence?.(data, ctx) ?? [])),
|
|
22
|
+
guidanceSection(await spec.guidance(data, ctx)),
|
|
23
|
+
lintSection(await (spec.suggestions?.(data, ctx) ?? [])),
|
|
24
|
+
nextCallSection(await spec.nextCall(data, ctx)),
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
return asText(markdown);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof ToolExecutionError) {
|
|
31
|
+
return {
|
|
32
|
+
...asText(renderErrorMarkdown(spec.name, error.message, error.nextSteps, error.retryExample)),
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
...asText(renderErrorMarkdown(spec.name, String(error), [])),
|
|
38
|
+
isError: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
return [spec.name, { title: spec.title, description: spec.description, inputSchema: spec.inputSchema }, cb];
|
|
43
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Common type definitions (simplified)
|
|
2
2
|
export const VALID_STATUS_TRANSITIONS = {
|
|
3
|
-
TODO: [
|
|
4
|
-
IN_PROGRESS: [
|
|
5
|
-
BLOCKED: [
|
|
3
|
+
TODO: ['IN_PROGRESS'],
|
|
4
|
+
IN_PROGRESS: ['TODO', 'BLOCKED', 'DONE'],
|
|
5
|
+
BLOCKED: ['IN_PROGRESS'],
|
|
6
6
|
DONE: [],
|
|
7
7
|
};
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
// Common utility functions
|
|
2
|
-
import fs from
|
|
3
|
-
import path from
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
4
|
/**
|
|
5
5
|
* Safely read Markdown file content, return fallback if file doesn't exist or is empty
|
|
6
6
|
*/
|
|
7
7
|
export async function readMarkdownOrFallback(relativePath, fallbackTitle, repoRoot = process.cwd()) {
|
|
8
8
|
const absolutePath = path.resolve(repoRoot, relativePath);
|
|
9
9
|
try {
|
|
10
|
-
const content = await fs.readFile(absolutePath,
|
|
10
|
+
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
11
11
|
if (content.trim().length > 0) {
|
|
12
12
|
return content;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
catch (error) {
|
|
16
|
-
if (error.code
|
|
16
|
+
if (!(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')) {
|
|
17
17
|
console.error(`Failed to read file: ${absolutePath}`, error);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
return [
|
|
21
21
|
`# ${fallbackTitle}`,
|
|
22
|
-
|
|
22
|
+
'',
|
|
23
23
|
`- file: ${relativePath}`,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
].join(
|
|
24
|
+
'- status: missing-or-empty',
|
|
25
|
+
'- next: create this file or ensure it has readable markdown content',
|
|
26
|
+
].join('\n');
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Capitalize first letter
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { capitalizeFirstLetter, formatTitle, readMarkdownOrFallback } from './utils.js';
|
|
6
|
+
const tempPaths = [];
|
|
7
|
+
async function createTempDir() {
|
|
8
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-utils-test-'));
|
|
9
|
+
tempPaths.push(dir);
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
await Promise.all(tempPaths.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
|
15
|
+
});
|
|
16
|
+
describe('utils module', () => {
|
|
17
|
+
it('reads markdown content when file exists and is non-empty', async () => {
|
|
18
|
+
const root = await createTempDir();
|
|
19
|
+
const filePath = path.join(root, 'README.md');
|
|
20
|
+
await fs.writeFile(filePath, '# Hello\n', 'utf-8');
|
|
21
|
+
const content = await readMarkdownOrFallback('README.md', 'Fallback', root);
|
|
22
|
+
expect(content).toBe('# Hello\n');
|
|
23
|
+
});
|
|
24
|
+
it('returns fallback markdown for missing or empty files', async () => {
|
|
25
|
+
const root = await createTempDir();
|
|
26
|
+
await fs.writeFile(path.join(root, 'empty.md'), ' ', 'utf-8');
|
|
27
|
+
const missing = await readMarkdownOrFallback('missing.md', 'Missing Title', root);
|
|
28
|
+
const empty = await readMarkdownOrFallback('empty.md', 'Empty Title', root);
|
|
29
|
+
expect(missing).toContain('# Missing Title');
|
|
30
|
+
expect(missing).toContain('- status: missing-or-empty');
|
|
31
|
+
expect(empty).toContain('# Empty Title');
|
|
32
|
+
expect(empty).toContain('- file: empty.md');
|
|
33
|
+
});
|
|
34
|
+
it('logs non-ENOENT read errors before falling back', async () => {
|
|
35
|
+
const root = await createTempDir();
|
|
36
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
37
|
+
vi.spyOn(fs, 'readFile').mockRejectedValueOnce({ code: 'EACCES' });
|
|
38
|
+
const result = await readMarkdownOrFallback('secret.md', 'Secret', root);
|
|
39
|
+
expect(result).toContain('# Secret');
|
|
40
|
+
expect(errorSpy).toHaveBeenCalledOnce();
|
|
41
|
+
});
|
|
42
|
+
it('formats title helpers consistently', () => {
|
|
43
|
+
expect(capitalizeFirstLetter('task_execution')).toBe('TaskExecution');
|
|
44
|
+
expect(capitalizeFirstLetter('roadmap-next')).toBe('RoadmapNext');
|
|
45
|
+
expect(formatTitle('task_execution')).toBe('Task Execution');
|
|
46
|
+
expect(formatTitle('roadmap-next')).toBe('Roadmap Next');
|
|
47
|
+
});
|
|
48
|
+
});
|
package/output/source/index.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import path from
|
|
3
|
-
import process from
|
|
4
|
-
import { fileURLToPath } from
|
|
5
|
-
import { McpServer } from
|
|
6
|
-
import { StdioServerTransport } from
|
|
7
|
-
import packageJson from
|
|
8
|
-
import { registerTools } from
|
|
9
|
-
import { registerPrompts } from
|
|
10
|
-
import { registerResources } from
|
|
11
|
-
const PROJITIVE_SPEC_VERSION =
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
8
|
+
import { registerTools } from './tools/index.js';
|
|
9
|
+
import { registerPrompts } from './prompts/index.js';
|
|
10
|
+
import { registerResources } from './resources/index.js';
|
|
11
|
+
const PROJITIVE_SPEC_VERSION = '1.1.0';
|
|
12
12
|
const currentFilePath = fileURLToPath(import.meta.url);
|
|
13
13
|
const sourceDir = path.dirname(currentFilePath);
|
|
14
|
-
const repoRoot = path.resolve(sourceDir,
|
|
15
|
-
const MCP_RUNTIME_VERSION = typeof packageJson.version ===
|
|
14
|
+
const repoRoot = path.resolve(sourceDir, '..', '..', '..');
|
|
15
|
+
const MCP_RUNTIME_VERSION = typeof packageJson.version === 'string' && packageJson.version.trim().length > 0
|
|
16
16
|
? packageJson.version.trim()
|
|
17
17
|
: PROJITIVE_SPEC_VERSION;
|
|
18
18
|
const server = new McpServer({
|
|
19
|
-
name:
|
|
19
|
+
name: 'projitive',
|
|
20
20
|
version: MCP_RUNTIME_VERSION,
|
|
21
|
-
description:
|
|
21
|
+
description: 'Semantic Projitive MCP for project/task discovery and agent guidance with governance-store-first outputs',
|
|
22
22
|
});
|
|
23
23
|
// 注册所有模块
|
|
24
24
|
registerTools(server);
|
|
25
25
|
registerPrompts(server);
|
|
26
26
|
registerResources(server, repoRoot);
|
|
27
27
|
async function main() {
|
|
28
|
-
console.error(
|
|
28
|
+
console.error('[projitive-mcp] starting server');
|
|
29
29
|
console.error(`[projitive-mcp] version=${MCP_RUNTIME_VERSION} spec=${PROJITIVE_SPEC_VERSION} transport=stdio pid=${process.pid}`);
|
|
30
30
|
const transport = new StdioServerTransport();
|
|
31
31
|
await server.connect(transport);
|
|
32
32
|
}
|
|
33
33
|
void main().catch((error) => {
|
|
34
|
-
console.error(
|
|
34
|
+
console.error('Server error:', error);
|
|
35
35
|
process.exit(1);
|
|
36
36
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const hoisted = vi.hoisted(() => {
|
|
3
|
+
const connectMock = vi.fn().mockResolvedValue(undefined);
|
|
4
|
+
const serverInstance = { connect: connectMock };
|
|
5
|
+
return {
|
|
6
|
+
connectMock,
|
|
7
|
+
serverInstance,
|
|
8
|
+
registerToolsMock: vi.fn(),
|
|
9
|
+
registerPromptsMock: vi.fn(),
|
|
10
|
+
registerResourcesMock: vi.fn(),
|
|
11
|
+
mcpServerMock: vi.fn().mockImplementation(() => serverInstance),
|
|
12
|
+
stdioTransportMock: vi.fn().mockImplementation(() => ({ kind: 'stdio' })),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
|
|
16
|
+
McpServer: hoisted.mcpServerMock,
|
|
17
|
+
}));
|
|
18
|
+
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
|
|
19
|
+
StdioServerTransport: hoisted.stdioTransportMock,
|
|
20
|
+
}));
|
|
21
|
+
vi.mock('./tools/index.js', () => ({
|
|
22
|
+
registerTools: hoisted.registerToolsMock,
|
|
23
|
+
}));
|
|
24
|
+
vi.mock('./prompts/index.js', () => ({
|
|
25
|
+
registerPrompts: hoisted.registerPromptsMock,
|
|
26
|
+
}));
|
|
27
|
+
vi.mock('./resources/index.js', () => ({
|
|
28
|
+
registerResources: hoisted.registerResourcesMock,
|
|
29
|
+
}));
|
|
30
|
+
describe('index runtime module', () => {
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.restoreAllMocks();
|
|
33
|
+
vi.resetModules();
|
|
34
|
+
hoisted.connectMock.mockClear();
|
|
35
|
+
hoisted.registerToolsMock.mockClear();
|
|
36
|
+
hoisted.registerPromptsMock.mockClear();
|
|
37
|
+
hoisted.registerResourcesMock.mockClear();
|
|
38
|
+
hoisted.mcpServerMock.mockClear();
|
|
39
|
+
hoisted.stdioTransportMock.mockClear();
|
|
40
|
+
});
|
|
41
|
+
it('boots the MCP server, registers modules, and connects stdio transport', async () => {
|
|
42
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
43
|
+
await import('./index.js');
|
|
44
|
+
await Promise.resolve();
|
|
45
|
+
expect(hoisted.mcpServerMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
46
|
+
name: 'projitive',
|
|
47
|
+
description: expect.stringContaining('governance-store-first outputs'),
|
|
48
|
+
}));
|
|
49
|
+
expect(hoisted.registerToolsMock).toHaveBeenCalledWith(hoisted.serverInstance);
|
|
50
|
+
expect(hoisted.registerPromptsMock).toHaveBeenCalledWith(hoisted.serverInstance);
|
|
51
|
+
expect(hoisted.registerResourcesMock).toHaveBeenCalledWith(hoisted.serverInstance, expect.any(String));
|
|
52
|
+
expect(hoisted.stdioTransportMock).toHaveBeenCalledOnce();
|
|
53
|
+
expect(hoisted.connectMock).toHaveBeenCalledOnce();
|
|
54
|
+
expect(errorSpy).toHaveBeenCalledWith('[projitive-mcp] starting server');
|
|
55
|
+
expect(errorSpy.mock.calls.some((call) => String(call[0]).includes('transport=stdio'))).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|