@projitive/mcp 1.0.2 → 1.0.4

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.
Files changed (40) hide show
  1. package/README.md +47 -23
  2. package/output/hooks.js +1 -14
  3. package/output/hooks.test.js +7 -18
  4. package/output/index.js +23 -5
  5. package/output/package.json +36 -0
  6. package/output/projitive.js +21 -2
  7. package/output/projitive.test.js +1 -0
  8. package/output/source/designs.js +38 -0
  9. package/output/source/helpers/artifacts/artifacts.js +10 -0
  10. package/output/source/helpers/artifacts/artifacts.test.js +18 -0
  11. package/output/source/helpers/artifacts/index.js +1 -0
  12. package/output/source/helpers/catch/catch.js +48 -0
  13. package/output/source/helpers/catch/catch.test.js +43 -0
  14. package/output/source/helpers/catch/index.js +1 -0
  15. package/output/source/helpers/files/files.js +62 -0
  16. package/output/source/helpers/files/files.test.js +32 -0
  17. package/output/source/helpers/files/index.js +1 -0
  18. package/output/source/helpers/index.js +6 -0
  19. package/output/source/helpers/linter/codes.js +25 -0
  20. package/output/source/helpers/linter/index.js +2 -0
  21. package/output/source/helpers/linter/linter.js +6 -0
  22. package/output/source/helpers/linter/linter.test.js +16 -0
  23. package/output/source/helpers/markdown/index.js +1 -0
  24. package/output/source/helpers/markdown/markdown.js +33 -0
  25. package/output/source/helpers/markdown/markdown.test.js +36 -0
  26. package/output/source/helpers/response/index.js +1 -0
  27. package/output/source/helpers/response/response.js +73 -0
  28. package/output/source/helpers/response/response.test.js +50 -0
  29. package/output/source/index.js +215 -0
  30. package/output/source/projitive.js +497 -0
  31. package/output/source/projitive.test.js +75 -0
  32. package/output/source/readme.js +26 -0
  33. package/output/source/reports.js +36 -0
  34. package/output/source/roadmap.js +165 -0
  35. package/output/source/roadmap.test.js +11 -0
  36. package/output/source/tasks.js +762 -0
  37. package/output/source/tasks.test.js +152 -0
  38. package/output/tasks.js +100 -80
  39. package/output/tasks.test.js +32 -8
  40. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Language: English | [简体中文](README_CN.md)
4
4
 
5
- **Current Spec Version: projitive-spec v1.0.0 | MCP Version: 1.0.2**
5
+ **Current Spec Version: projitive-spec v1.0.0 | MCP Version: 1.0.4**
6
6
 
7
7
  Projitive MCP server (semantic interface edition) helps agents discover projects, select tasks, locate evidence, and execute under governance workflows.
8
8
 
@@ -50,6 +50,17 @@ taskNext
50
50
  -> taskNext (next cycle)
51
51
  ```
52
52
 
53
+ When no actionable task exists (`actionableTasks: 0`), use bootstrap path:
54
+
55
+ ```text
56
+ taskNext
57
+ -> projectContext
58
+ -> create 1-3 TODO tasks in tasks.md marker block (from roadmap/readme/report gaps)
59
+ -> taskNext
60
+ ```
61
+
62
+ Optional customization: add `hooks/task_no_actionable.md` in governance root to override the default no-task discovery checklist.
63
+
53
64
  When the agent starts inside a project:
54
65
 
55
66
  ```text
@@ -58,17 +69,43 @@ projectLocate -> projectContext -> taskList -> taskContext
58
69
 
59
70
  ## Quick Start
60
71
 
72
+ Use npm package directly in MCP client configuration:
73
+
61
74
  ```bash
62
- cd packages/mcp
63
- npm ci
64
- npm run build
65
- npm run test
75
+ npx -y @projitive/mcp
76
+ ```
77
+
78
+ MCP client config example (`mcp.json`):
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "projitive": {
84
+ "command": "npx",
85
+ "args": ["-y", "@projitive/mcp"],
86
+ "env": {
87
+ "PROJITIVE_SCAN_ROOT_PATH": "/absolute/path/to/your/workspace",
88
+ "PROJITIVE_SCAN_MAX_DEPTH": "3"
89
+ }
90
+ }
91
+ }
92
+ }
66
93
  ```
67
94
 
68
- Then configure your MCP client to run:
95
+ Environment variables (required):
96
+
97
+ - `PROJITIVE_SCAN_ROOT_PATH`: required scan root for discovery methods.
98
+ - `PROJITIVE_SCAN_MAX_DEPTH`: required scan depth for discovery methods (integer `0-8`).
99
+
100
+ Local path startup is not the recommended usage mode in this README.
101
+
102
+ For maintainers/contributors only:
69
103
 
70
104
  ```bash
71
- node /absolute/path/to/packages/mcp/output/index.js
105
+ cd packages/mcp
106
+ npm ci
107
+ npm run build
108
+ npm run test
72
109
  ```
73
110
 
74
111
  ## Spec Version
@@ -112,14 +149,14 @@ node /absolute/path/to/packages/mcp/output/index.js
112
149
  #### `projectInit`
113
150
 
114
151
  - **Purpose**: manually initialize governance directory structure for a project (default `.projitive`).
115
- - **Input**: `rootPath?`, `governanceDir?`, `force?`
152
+ - **Input**: `projectPath?`, `governanceDir?`, `force?`
116
153
  - **Output Example (Markdown)**:
117
154
 
118
155
  ```markdown
119
156
  # projectInit
120
157
 
121
158
  ## Summary
122
- - rootPath: /workspace/proj-a
159
+ - projectPath: /workspace/proj-a
123
160
  - governanceDir: /workspace/proj-a/.projitive
124
161
  - markerPath: /workspace/proj-a/.projitive/.projitive
125
162
  - force: false
@@ -169,7 +206,7 @@ node /absolute/path/to/packages/mcp/output/index.js
169
206
  #### `projectScan`
170
207
 
171
208
  - **Purpose**: scan directories and discover governable projects.
172
- - **Input**: `rootPath?`, `maxDepth?`
209
+ - **Input**: `(none)`
173
210
  - **Output Example (Markdown)**:
174
211
 
175
212
  ```markdown
@@ -332,17 +369,9 @@ node /absolute/path/to/packages/mcp/output/index.js
332
369
 
333
370
  - **Purpose**: return task detail + related evidence locations in one call (replacing `trace.references`).
334
371
  - **Input**: `projectPath`, `taskId`
335
- - **HOOK Injection**:
336
- - If `hooks/task_get_head.md` exists, its content is prepended to result.
337
- - If `hooks/task_get_footer.md` exists, its content is appended to result.
338
- - Used for project-level custom guidance without changing core `taskContext` shape.
339
372
  - **Output Example (Markdown)**:
340
373
 
341
374
  ```markdown
342
- [hooks/task_get_head.md content (if present)]
343
-
344
- ---
345
-
346
375
  # taskContext
347
376
 
348
377
  ## Summary
@@ -354,7 +383,6 @@ node /absolute/path/to/packages/mcp/output/index.js
354
383
  - updatedAt: 2026-02-17T12:00:00.000Z
355
384
  - roadmapRefs: ROADMAP-0001
356
385
  - taskLocation: /workspace/proj-a/tasks.md#L42
357
- - hookStatus: head=loaded, footer=missing
358
386
 
359
387
  ## Evidence
360
388
  ### Related Artifacts
@@ -379,10 +407,6 @@ node /absolute/path/to/packages/mcp/output/index.js
379
407
 
380
408
  ## Next Call
381
409
  - taskContext(projectPath="/workspace/proj-a", taskId="TASK-0003")
382
-
383
- ---
384
-
385
- [hooks/task_get_footer.md content (if present)]
386
410
  ```
387
411
 
388
412
  ### Roadmap Layer
package/output/hooks.js CHANGED
@@ -30,20 +30,7 @@ async function readHookFile(filePath) {
30
30
  }
31
31
  return { ok: true, content };
32
32
  }
33
- export async function resolveHookForEvent(governanceDir, task, event) {
34
- const taskHookPath = task.hooks[event];
35
- if (taskHookPath) {
36
- const resolvedTaskPath = path.resolve(governanceDir, taskHookPath);
37
- const taskFile = await readHookFile(resolvedTaskPath);
38
- if (taskFile.ok) {
39
- return {
40
- event,
41
- source: "task",
42
- path: resolvedTaskPath,
43
- content: taskFile.content,
44
- };
45
- }
46
- }
33
+ export async function resolveHookForEvent(governanceDir, event) {
47
34
  const globalPath = path.join(governanceDir, "hooks", GLOBAL_EVENT_FILES[event]);
48
35
  const globalFile = await readHookFile(globalPath);
49
36
  if (globalFile.ok) {
@@ -3,7 +3,6 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { afterEach, describe, expect, it } from "vitest";
5
5
  import { detectHookEvent, resolveHookForEvent } from "./hooks.js";
6
- import { normalizeTask } from "./tasks.js";
7
6
  const tempPaths = [];
8
7
  async function createTempDir() {
9
8
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
@@ -22,30 +21,20 @@ describe("hooks module", () => {
22
21
  expect(detectHookEvent("IN_PROGRESS", "BLOCKED")).toBe("onBlocked");
23
22
  expect(detectHookEvent("BLOCKED", "TODO")).toBe(null);
24
23
  });
25
- it("prefers task-level hook over global hook", async () => {
24
+ it("loads global hook when global file exists", async () => {
26
25
  const root = await createTempDir();
27
26
  const governanceDir = path.join(root, "gov");
28
27
  await fs.mkdir(path.join(governanceDir, "hooks"), { recursive: true });
29
28
  await fs.writeFile(path.join(governanceDir, "hooks", "on_task_assigned.md"), "global assigned", "utf-8");
30
- await fs.writeFile(path.join(governanceDir, "hooks", "custom-assigned.md"), "task assigned", "utf-8");
31
- const task = normalizeTask({
32
- id: "TASK-0001",
33
- title: "Task",
34
- status: "TODO",
35
- hooks: { onAssigned: "./hooks/custom-assigned.md" },
36
- });
37
- const hook = await resolveHookForEvent(governanceDir, task, "onAssigned");
38
- expect(hook.source).toBe("task");
39
- expect(hook.content).toContain("task assigned");
29
+ const hook = await resolveHookForEvent(governanceDir, "onAssigned");
30
+ expect(hook.source).toBe("global");
31
+ expect(hook.content).toContain("global assigned");
40
32
  });
41
- it("falls back to global hook when task-level hook is missing", async () => {
33
+ it("returns none when global hook is missing", async () => {
42
34
  const root = await createTempDir();
43
35
  const governanceDir = path.join(root, "gov");
44
36
  await fs.mkdir(path.join(governanceDir, "hooks"), { recursive: true });
45
- await fs.writeFile(path.join(governanceDir, "hooks", "on_task_completed.md"), "global completed", "utf-8");
46
- const task = normalizeTask({ id: "TASK-0001", title: "Task", status: "IN_PROGRESS" });
47
- const hook = await resolveHookForEvent(governanceDir, task, "onCompleted");
48
- expect(hook.source).toBe("global");
49
- expect(hook.content).toContain("global completed");
37
+ const hook = await resolveHookForEvent(governanceDir, "onCompleted");
38
+ expect(hook.source).toBe("none");
50
39
  });
51
40
  });
package/output/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import process from "node:process";
@@ -10,14 +11,29 @@ import { registerProjectTools } from "./projitive.js";
10
11
  import { registerTaskTools } from "./tasks.js";
11
12
  import { registerRoadmapTools } from "./roadmap.js";
12
13
  const PROJITIVE_SPEC_VERSION = "1.0.0";
14
+ const currentFilePath = fileURLToPath(import.meta.url);
15
+ const sourceDir = path.dirname(currentFilePath);
16
+ const repoRoot = path.resolve(sourceDir, "..", "..", "..");
17
+ function resolveRuntimeVersion() {
18
+ const packageJsonPath = path.resolve(sourceDir, "..", "package.json");
19
+ try {
20
+ const raw = readFileSync(packageJsonPath, "utf-8");
21
+ const parsed = JSON.parse(raw);
22
+ if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
23
+ return parsed.version.trim();
24
+ }
25
+ }
26
+ catch {
27
+ // fallback handled below
28
+ }
29
+ return PROJITIVE_SPEC_VERSION;
30
+ }
31
+ const MCP_RUNTIME_VERSION = resolveRuntimeVersion();
13
32
  const server = new McpServer({
14
33
  name: "projitive",
15
- version: PROJITIVE_SPEC_VERSION,
34
+ version: MCP_RUNTIME_VERSION,
16
35
  description: "Semantic Projitive MCP for project/task discovery and agent guidance with markdown-first outputs",
17
36
  });
18
- const currentFilePath = fileURLToPath(import.meta.url);
19
- const sourceDir = path.dirname(currentFilePath);
20
- const repoRoot = path.resolve(sourceDir, "..", "..", "..");
21
37
  function resolveRepoFile(relativePath) {
22
38
  return path.join(repoRoot, relativePath);
23
39
  }
@@ -170,7 +186,7 @@ function registerGovernancePrompts() {
170
186
  "Checklist:",
171
187
  "- Transition is valid per status machine.",
172
188
  "- links/roadmapRefs remain parseable and consistent.",
173
- "- Hook paths (if any) still resolve.",
189
+ "- Only `hooks/task_no_actionable.md` is used as global background hook for no-task discovery.",
174
190
  ].join("\n");
175
191
  return asUserPrompt(text);
176
192
  });
@@ -200,6 +216,8 @@ registerRoadmapTools(server);
200
216
  registerGovernanceResources();
201
217
  registerGovernancePrompts();
202
218
  async function main() {
219
+ console.error(`[projitive-mcp] starting server`);
220
+ console.error(`[projitive-mcp] version=${MCP_RUNTIME_VERSION} spec=${PROJITIVE_SPEC_VERSION} transport=stdio pid=${process.pid}`);
203
221
  const transport = new StdioServerTransport();
204
222
  await server.connect(transport);
205
223
  }
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@projitive/mcp",
3
+ "version": "1.0.4",
4
+ "description": "Projitive MCP Server for project and task discovery/update",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "bin": {
9
+ "mcp": "output/index.js"
10
+ },
11
+ "main": "./output/index.js",
12
+ "types": "./output/index.d.ts",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "test": "vitest run",
18
+ "lint": "tsc -p tsconfig.json --noEmit",
19
+ "build": "tsc -p tsconfig.json",
20
+ "prepublishOnly": "npm run build",
21
+ "dev": "tsc -p tsconfig.json --watch"
22
+ },
23
+ "files": [
24
+ "output"
25
+ ],
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.17.5",
28
+ "zod": "^3.23.8"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^24.3.0",
32
+ "tsx": "^4.20.5",
33
+ "typescript": "^5.9.2",
34
+ "vitest": "^3.2.4"
35
+ }
36
+ }
@@ -201,12 +201,30 @@ function defaultTasksMarkdown() {
201
201
  "- owner: unassigned",
202
202
  "- summary: Create initial governance artifacts and confirm task execution loop.",
203
203
  `- updatedAt: ${updatedAt}`,
204
- "- links:",
205
204
  "- roadmapRefs: ROADMAP-0001",
206
- "- hooks:",
205
+ "- links:",
206
+ " - (none)",
207
207
  "<!-- PROJITIVE:TASKS:END -->",
208
208
  ].join("\n");
209
209
  }
210
+ function defaultNoTaskDiscoveryHookMarkdown() {
211
+ return [
212
+ "Objective:",
213
+ "- When no actionable task exists, proactively discover meaningful work and convert it into TODO tasks.",
214
+ "",
215
+ "Checklist:",
216
+ "- Check whether code violates project guides/specs; create tasks for each actionable gap.",
217
+ "- Check test coverage improvement opportunities; create tasks for high-value missing tests.",
218
+ "- Check development/testing workflow bottlenecks; create tasks for reliability and speed improvements.",
219
+ "- Check TODO/FIXME/HACK comments; turn feasible items into governed tasks.",
220
+ "- Check dependency/security hygiene and stale tooling; create tasks where upgrades are justified.",
221
+ "",
222
+ "Output Format:",
223
+ "- Candidate findings (3-10)",
224
+ "- Proposed tasks (TASK-xxxx style)",
225
+ "- Priority rationale",
226
+ ].join("\n");
227
+ }
210
228
  export async function initializeProjectStructure(inputPath, governanceDir, force = false) {
211
229
  const rootPath = normalizePath(inputPath);
212
230
  const governanceDirName = normalizeGovernanceDirName(governanceDir);
@@ -231,6 +249,7 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
231
249
  writeTextFile(path.join(governancePath, "README.md"), defaultReadmeMarkdown(governanceDirName), force),
232
250
  writeTextFile(path.join(governancePath, "roadmap.md"), defaultRoadmapMarkdown(), force),
233
251
  writeTextFile(path.join(governancePath, "tasks.md"), defaultTasksMarkdown(), force),
252
+ writeTextFile(path.join(governancePath, "hooks", "task_no_actionable.md"), defaultNoTaskDiscoveryHookMarkdown(), force),
234
253
  ]);
235
254
  return {
236
255
  rootPath,
@@ -52,6 +52,7 @@ describe("projitive module", () => {
52
52
  path.join(root, ".projitive", "README.md"),
53
53
  path.join(root, ".projitive", "roadmap.md"),
54
54
  path.join(root, ".projitive", "tasks.md"),
55
+ path.join(root, ".projitive", "hooks", "task_no_actionable.md"),
55
56
  path.join(root, ".projitive", "designs"),
56
57
  path.join(root, ".projitive", "reports"),
57
58
  path.join(root, ".projitive", "hooks"),
@@ -0,0 +1,38 @@
1
+ import { isValidRoadmapId } from "./roadmap.js";
2
+ import { isValidTaskId } from "./tasks.js";
3
+ export function parseDesignMetadata(markdown) {
4
+ const lines = markdown.split(/\r?\n/);
5
+ const metadata = {};
6
+ for (const line of lines) {
7
+ const [rawKey, ...rawValue] = line.split(":");
8
+ if (!rawKey || rawValue.length === 0) {
9
+ continue;
10
+ }
11
+ const key = rawKey.trim().toLowerCase();
12
+ const value = rawValue.join(":").trim();
13
+ if (key === "task")
14
+ metadata.task = value;
15
+ if (key === "roadmap")
16
+ metadata.roadmap = value;
17
+ if (key === "owner")
18
+ metadata.owner = value;
19
+ if (key === "status")
20
+ metadata.status = value;
21
+ if (key === "last updated")
22
+ metadata.lastUpdated = value;
23
+ }
24
+ return metadata;
25
+ }
26
+ export function validateDesignMetadata(metadata) {
27
+ const errors = [];
28
+ if (!metadata.task) {
29
+ errors.push("Missing Task metadata");
30
+ }
31
+ else if (!isValidTaskId(metadata.task)) {
32
+ errors.push(`Invalid Task metadata format: ${metadata.task}`);
33
+ }
34
+ if (metadata.roadmap && !isValidRoadmapId(metadata.roadmap)) {
35
+ errors.push(`Invalid Roadmap metadata format: ${metadata.roadmap}`);
36
+ }
37
+ return { ok: errors.length === 0, errors };
38
+ }
@@ -0,0 +1,10 @@
1
+ export function candidateFilesFromArtifacts(artifacts) {
2
+ return artifacts
3
+ .filter((item) => item.exists)
4
+ .flatMap((item) => {
5
+ if (item.kind === "file") {
6
+ return [item.path];
7
+ }
8
+ return (item.markdownFiles ?? []).map((entry) => entry.path);
9
+ });
10
+ }
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { candidateFilesFromArtifacts } from "./artifacts.js";
3
+ describe("candidateFilesFromArtifacts", () => {
4
+ it("collects existing file artifacts and markdown files from existing directories", () => {
5
+ const candidates = candidateFilesFromArtifacts([
6
+ { name: "README.md", kind: "file", path: "/a/README.md", exists: true, lineCount: 3 },
7
+ { name: "tasks.md", kind: "file", path: "/a/tasks.md", exists: false },
8
+ {
9
+ name: "designs",
10
+ kind: "directory",
11
+ path: "/a/designs",
12
+ exists: true,
13
+ markdownFiles: [{ path: "/a/designs/d1.md", lineCount: 10 }],
14
+ },
15
+ ]);
16
+ expect(candidates).toEqual(["/a/README.md", "/a/designs/d1.md"]);
17
+ });
18
+ });
@@ -0,0 +1 @@
1
+ export * from "./artifacts.js";
@@ -0,0 +1,48 @@
1
+ // 辅助函数:检查是否为 PromiseLike
2
+ function isPromiseLike(value) {
3
+ return value != null && typeof value === 'object' && 'then' in value && typeof value.then === 'function';
4
+ }
5
+ /**
6
+ * 构造成功结果对象
7
+ * isError 始终返回 false
8
+ */
9
+ function createSuccess(value) {
10
+ return {
11
+ value,
12
+ error: undefined,
13
+ isError() { return false; }
14
+ };
15
+ }
16
+ /**
17
+ * 构造失败结果对象
18
+ * isError 始终返回 true
19
+ */
20
+ function createFailure(error) {
21
+ return {
22
+ error,
23
+ value: undefined,
24
+ isError() { return true; }
25
+ };
26
+ }
27
+ export async function catchIt(input) {
28
+ try {
29
+ if (isPromiseLike(input)) {
30
+ const result = await Promise.resolve(input);
31
+ return createSuccess(result);
32
+ }
33
+ else if (typeof input === 'function') {
34
+ const result = input();
35
+ if (isPromiseLike(result)) {
36
+ return catchIt(result);
37
+ }
38
+ else {
39
+ return createSuccess(result);
40
+ }
41
+ }
42
+ }
43
+ catch (error) {
44
+ return createFailure(error);
45
+ }
46
+ // 理论上不会到达这里,兜底类型安全
47
+ return createFailure(new Error('Unexpected input type'));
48
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { catchIt } from './catch.js';
3
+ describe('catchIt', () => {
4
+ it('同步函数返回值应为 value,error 为 undefined,isError 为 false', async () => {
5
+ const result = await catchIt(() => 123);
6
+ expect(result.value).toBe(123);
7
+ expect(result.error).toBeUndefined();
8
+ expect(result.isError()).toBe(false);
9
+ });
10
+ it('异步函数返回值应为 value,error 为 undefined,isError 为 false', async () => {
11
+ const result = await catchIt(async () => 456);
12
+ expect(result.value).toBe(456);
13
+ expect(result.error).toBeUndefined();
14
+ expect(result.isError()).toBe(false);
15
+ });
16
+ it('同步抛出异常时应返回 error,value 为 undefined,isError 为 true', async () => {
17
+ const error = new Error('fail');
18
+ const result = await catchIt(() => { throw error; });
19
+ expect(result.value).toBeUndefined();
20
+ expect(result.error).toBe(error);
21
+ expect(result.isError()).toBe(true);
22
+ });
23
+ it('异步抛出异常时应返回 error,value 为 undefined,isError 为 true', async () => {
24
+ const error = new Error('fail-async');
25
+ const result = await catchIt(() => Promise.reject(error));
26
+ expect(result.value).toBeUndefined();
27
+ expect(result.error).toBe(error);
28
+ expect(result.isError()).toBe(true);
29
+ });
30
+ it('PromiseLike resolve 时应返回 value,error 为 undefined,isError 为 false', async () => {
31
+ const result = await catchIt(Promise.resolve('ok'));
32
+ expect(result.value).toBe('ok');
33
+ expect(result.error).toBeUndefined();
34
+ expect(result.isError()).toBe(false);
35
+ });
36
+ it('PromiseLike reject 时应返回 error,value 为 undefined,isError 为 true', async () => {
37
+ const error = new Error('promise-fail');
38
+ const result = await catchIt(Promise.reject(error));
39
+ expect(result.value).toBeUndefined();
40
+ expect(result.error).toBe(error);
41
+ expect(result.isError()).toBe(true);
42
+ });
43
+ });
@@ -0,0 +1 @@
1
+ export * from './catch.js';
@@ -0,0 +1,62 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { catchIt } from "../catch/index.js";
4
+ const FILE_ARTIFACTS = ["README.md", "roadmap.md", "tasks.md"];
5
+ const DIRECTORY_ARTIFACTS = ["designs", "reports", "hooks"];
6
+ async function fileLineCount(filePath) {
7
+ const content = await fs.readFile(filePath, "utf-8");
8
+ if (!content) {
9
+ return 0;
10
+ }
11
+ return content.split(/\r?\n/).length;
12
+ }
13
+ async function listMarkdownFiles(dirPath) {
14
+ const entriesResult = await catchIt(fs.readdir(dirPath, { withFileTypes: true }));
15
+ if (entriesResult.isError()) {
16
+ return [];
17
+ }
18
+ const entries = entriesResult.value;
19
+ const files = entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"));
20
+ const result = [];
21
+ for (const file of files) {
22
+ const fullPath = path.join(dirPath, file.name);
23
+ result.push({ path: fullPath, lineCount: await fileLineCount(fullPath) });
24
+ }
25
+ return result.sort((a, b) => a.path.localeCompare(b.path));
26
+ }
27
+ export async function discoverGovernanceArtifacts(governanceDir) {
28
+ const result = [];
29
+ for (const artifact of FILE_ARTIFACTS) {
30
+ const artifactPath = path.join(governanceDir, artifact);
31
+ const accessResult = await catchIt(fs.access(artifactPath));
32
+ if (!accessResult.isError()) {
33
+ result.push({
34
+ name: artifact,
35
+ kind: "file",
36
+ path: artifactPath,
37
+ exists: true,
38
+ lineCount: await fileLineCount(artifactPath),
39
+ });
40
+ }
41
+ else {
42
+ result.push({ name: artifact, kind: "file", path: artifactPath, exists: false });
43
+ }
44
+ }
45
+ for (const artifact of DIRECTORY_ARTIFACTS) {
46
+ const artifactPath = path.join(governanceDir, artifact);
47
+ const accessResult = await catchIt(fs.access(artifactPath));
48
+ if (!accessResult.isError()) {
49
+ result.push({
50
+ name: artifact,
51
+ kind: "directory",
52
+ path: artifactPath,
53
+ exists: true,
54
+ markdownFiles: await listMarkdownFiles(artifactPath),
55
+ });
56
+ }
57
+ else {
58
+ result.push({ name: artifact, kind: "directory", path: artifactPath, exists: false, markdownFiles: [] });
59
+ }
60
+ }
61
+ return result;
62
+ }
@@ -0,0 +1,32 @@
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 } from "vitest";
5
+ import { discoverGovernanceArtifacts } from "./files.js";
6
+ const tempPaths = [];
7
+ async function createTempDir() {
8
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
9
+ tempPaths.push(dir);
10
+ return dir;
11
+ }
12
+ afterEach(async () => {
13
+ await Promise.all(tempPaths.splice(0).map(async (dir) => {
14
+ await fs.rm(dir, { recursive: true, force: true });
15
+ }));
16
+ });
17
+ describe("files module", () => {
18
+ it("discovers governance artifacts with paths and line counts", async () => {
19
+ const root = await createTempDir();
20
+ await fs.writeFile(path.join(root, "README.md"), "# Readme\n", "utf-8");
21
+ await fs.writeFile(path.join(root, "tasks.md"), "# Tasks\n## TODO\n", "utf-8");
22
+ await fs.mkdir(path.join(root, "designs"), { recursive: true });
23
+ await fs.writeFile(path.join(root, "designs", "feature-design.md"), "# Design\n", "utf-8");
24
+ const artifacts = await discoverGovernanceArtifacts(root);
25
+ const readme = artifacts.find((item) => item.name === "README.md");
26
+ const designs = artifacts.find((item) => item.name === "designs");
27
+ expect(readme?.exists).toBe(true);
28
+ expect(readme?.lineCount).toBe(2);
29
+ expect(designs?.exists).toBe(true);
30
+ expect(designs?.markdownFiles?.[0].path.endsWith("feature-design.md")).toBe(true);
31
+ });
32
+ });
@@ -0,0 +1 @@
1
+ export * from './files.js';
@@ -0,0 +1,6 @@
1
+ export * from './artifacts/index.js';
2
+ export * from './catch/index.js';
3
+ export * from './files/index.js';
4
+ export * from './linter/index.js';
5
+ export * from './markdown/index.js';
6
+ export * from './response/index.js';
@@ -0,0 +1,25 @@
1
+ export const TASK_LINT_CODES = {
2
+ DUPLICATE_ID: "TASK_DUPLICATE_ID",
3
+ IN_PROGRESS_OWNER_EMPTY: "TASK_IN_PROGRESS_OWNER_EMPTY",
4
+ DONE_LINKS_MISSING: "TASK_DONE_LINKS_MISSING",
5
+ BLOCKED_SUMMARY_EMPTY: "TASK_BLOCKED_SUMMARY_EMPTY",
6
+ UPDATED_AT_INVALID: "TASK_UPDATED_AT_INVALID",
7
+ ROADMAP_REFS_EMPTY: "TASK_ROADMAP_REFS_EMPTY",
8
+ OUTSIDE_MARKER: "TASK_OUTSIDE_MARKER",
9
+ LINK_TARGET_MISSING: "TASK_LINK_TARGET_MISSING",
10
+ HOOK_FILE_MISSING: "TASK_HOOK_FILE_MISSING",
11
+ FILTER_EMPTY: "TASK_FILTER_EMPTY",
12
+ CONTEXT_HOOK_HEAD_MISSING: "TASK_CONTEXT_HOOK_HEAD_MISSING",
13
+ CONTEXT_HOOK_FOOTER_MISSING: "TASK_CONTEXT_HOOK_FOOTER_MISSING",
14
+ };
15
+ export const ROADMAP_LINT_CODES = {
16
+ IDS_EMPTY: "ROADMAP_IDS_EMPTY",
17
+ TASKS_EMPTY: "ROADMAP_TASKS_EMPTY",
18
+ TASK_REFS_EMPTY: "ROADMAP_TASK_REFS_EMPTY",
19
+ UNKNOWN_REFS: "ROADMAP_UNKNOWN_REFS",
20
+ ZERO_LINKED_TASKS: "ROADMAP_ZERO_LINKED_TASKS",
21
+ CONTEXT_RELATED_TASKS_EMPTY: "ROADMAP_CONTEXT_RELATED_TASKS_EMPTY",
22
+ };
23
+ export const PROJECT_LINT_CODES = {
24
+ TASKS_FILE_MISSING: "PROJECT_TASKS_FILE_MISSING",
25
+ };
@@ -0,0 +1,2 @@
1
+ export * from './linter.js';
2
+ export * from './codes.js';
@@ -0,0 +1,6 @@
1
+ export function renderLintSuggestions(suggestions) {
2
+ return suggestions.map((item) => {
3
+ const suffix = item.fixHint ? ` ${item.fixHint}` : "";
4
+ return `- [${item.code}] ${item.message}${suffix}`;
5
+ });
6
+ }