backlog-mcp 0.24.0 → 0.25.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.
Files changed (39) hide show
  1. package/dist/cli/index.mjs +16 -5
  2. package/dist/cli/index.mjs.map +1 -1
  3. package/dist/resources/data-dir.d.mts +23 -0
  4. package/dist/resources/data-dir.mjs +36 -0
  5. package/dist/resources/data-dir.mjs.map +1 -0
  6. package/dist/resources/index-resources.d.mts +12 -0
  7. package/dist/resources/index-resources.mjs +14 -8
  8. package/dist/resources/index-resources.mjs.map +1 -1
  9. package/dist/resources/resource-file.d.mts +14 -0
  10. package/dist/resources/resource-file.mjs +21 -3
  11. package/dist/resources/resource-file.mjs.map +1 -1
  12. package/dist/resources/task-attached.d.mts +14 -0
  13. package/dist/resources/task-attached.mjs +20 -1
  14. package/dist/resources/task-attached.mjs.map +1 -1
  15. package/dist/server/fastify-server.mjs +1 -3
  16. package/dist/server/fastify-server.mjs.map +1 -1
  17. package/dist/server/viewer-routes.mjs +13 -1
  18. package/dist/server/viewer-routes.mjs.map +1 -1
  19. package/dist/storage/backlog.d.mts +0 -2
  20. package/dist/storage/backlog.mjs +3 -6
  21. package/dist/storage/backlog.mjs.map +1 -1
  22. package/dist/utils/paths.d.mts +15 -1
  23. package/dist/utils/paths.mjs +18 -1
  24. package/dist/utils/paths.mjs.map +1 -1
  25. package/dist/utils/uri-resolver.d.mts +1 -2
  26. package/dist/utils/uri-resolver.mjs +4 -7
  27. package/dist/utils/uri-resolver.mjs.map +1 -1
  28. package/dist/viewer/index.html +4 -0
  29. package/dist/viewer/main.css +106 -4
  30. package/dist/viewer/main.js +110 -0
  31. package/dist/viewer/settings-3ELIWNF4.svg +3 -0
  32. package/package.json +1 -1
  33. package/viewer/components/system-info-modal.ts +112 -0
  34. package/viewer/components/task-list.ts +2 -1
  35. package/viewer/icons/index.ts +2 -0
  36. package/viewer/icons/settings.svg +3 -0
  37. package/viewer/index.html +4 -0
  38. package/viewer/main.ts +12 -0
  39. package/viewer/styles.css +118 -6
@@ -16,11 +16,22 @@ if (command === "serve") {
16
16
  console.log("Server is not running");
17
17
  process.exit(1);
18
18
  }
19
- const version = await getServerVersion(port);
20
- console.log(`Server is running on port ${port}`);
21
- console.log(`Version: ${version || "unknown"}`);
22
- console.log(`Viewer: http://localhost:${port}/`);
23
- console.log(`MCP endpoint: http://localhost:${port}/mcp`);
19
+ try {
20
+ const status = await (await fetch(`http://localhost:${port}/api/status`)).json();
21
+ console.log(`Server is running on port ${status.port}`);
22
+ console.log(`Version: ${status.version}`);
23
+ console.log(`Data directory: ${status.dataDir}`);
24
+ console.log(`Task count: ${status.taskCount}`);
25
+ console.log(`Uptime: ${status.uptime}s`);
26
+ console.log(`Viewer: http://localhost:${port}/`);
27
+ console.log(`MCP endpoint: http://localhost:${port}/mcp`);
28
+ } catch (error) {
29
+ const version = await getServerVersion(port);
30
+ console.log(`Server is running on port ${port}`);
31
+ console.log(`Version: ${version || "unknown"}`);
32
+ console.log(`Viewer: http://localhost:${port}/`);
33
+ console.log(`MCP endpoint: http://localhost:${port}/mcp`);
34
+ }
24
35
  process.exit(0);
25
36
  } else if (command === "stop") {
26
37
  const port = parseInt(process.env.BACKLOG_VIEWER_PORT || "3030");
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { paths } from '@/utils/paths.js';\nimport { isServerRunning, getServerVersion, shutdownServer } from './server-manager.js';\n\nconst args = process.argv.slice(2);\nconst command = args[0];\n\nif (command === 'serve') {\n // HTTP server mode\n const { startHttpServer } = await import('../server/fastify-server.js');\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n await startHttpServer(port);\n} else if (command === 'version') {\n // Show version\n console.log(paths.getVersion());\n process.exit(0);\n} else if (command === 'status') {\n // Check server status\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n const running = await isServerRunning(port);\n \n if (!running) {\n console.log('Server is not running');\n process.exit(1);\n }\n \n const version = await getServerVersion(port);\n console.log(`Server is running on port ${port}`);\n console.log(`Version: ${version || 'unknown'}`);\n console.log(`Viewer: http://localhost:${port}/`);\n console.log(`MCP endpoint: http://localhost:${port}/mcp`);\n process.exit(0);\n} else if (command === 'stop') {\n // Stop server\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n const running = await isServerRunning(port);\n \n if (!running) {\n console.log('Server is not running');\n process.exit(0);\n }\n \n console.log(`Stopping server on port ${port}...`);\n await shutdownServer(port);\n console.log('Server stopped');\n process.exit(0);\n} else if (command === '--help' || command === '-h') {\n console.log(`\nbacklog-mcp - Task management MCP server\n\nUsage:\n backlog-mcp Run as stdio MCP server (auto-bridges to HTTP server)\n backlog-mcp serve Run as HTTP MCP server with viewer\n backlog-mcp version Show version\n backlog-mcp status Check if server is running\n backlog-mcp stop Stop the server\n backlog-mcp --help Show this help\n\nEnvironment variables:\n BACKLOG_DATA_DIR Data directory path (default: ./data)\n BACKLOG_VIEWER_PORT HTTP server port (default: 3030)\n\nHow it works:\n - Default mode auto-spawns HTTP server and bridges stdio to it\n - HTTP server persists across sessions (shared by multiple clients)\n - Automatic version upgrades on server restart\n `);\n process.exit(0);\n} else {\n // Default: bridge mode (auto-spawn HTTP server)\n await import('./bridge.js');\n}\n"],"mappings":";;;;;AAMA,MAAM,UADO,QAAQ,KAAK,MAAM,EAAE,CACb;AAErB,IAAI,YAAY,SAAS;CAEvB,MAAM,EAAE,oBAAoB,MAAM,OAAO;AAEzC,OAAM,gBADO,SAAS,QAAQ,IAAI,uBAAuB,OAAO,CACrC;WAClB,YAAY,WAAW;AAEhC,SAAQ,IAAI,MAAM,YAAY,CAAC;AAC/B,SAAQ,KAAK,EAAE;WACN,YAAY,UAAU;CAE/B,MAAM,OAAO,SAAS,QAAQ,IAAI,uBAAuB,OAAO;AAGhE,KAAI,CAFY,MAAM,gBAAgB,KAAK,EAE7B;AACZ,UAAQ,IAAI,wBAAwB;AACpC,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,MAAM,iBAAiB,KAAK;AAC5C,SAAQ,IAAI,6BAA6B,OAAO;AAChD,SAAQ,IAAI,YAAY,WAAW,YAAY;AAC/C,SAAQ,IAAI,4BAA4B,KAAK,GAAG;AAChD,SAAQ,IAAI,kCAAkC,KAAK,MAAM;AACzD,SAAQ,KAAK,EAAE;WACN,YAAY,QAAQ;CAE7B,MAAM,OAAO,SAAS,QAAQ,IAAI,uBAAuB,OAAO;AAGhE,KAAI,CAFY,MAAM,gBAAgB,KAAK,EAE7B;AACZ,UAAQ,IAAI,wBAAwB;AACpC,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,2BAA2B,KAAK,KAAK;AACjD,OAAM,eAAe,KAAK;AAC1B,SAAQ,IAAI,iBAAiB;AAC7B,SAAQ,KAAK,EAAE;WACN,YAAY,YAAY,YAAY,MAAM;AACnD,SAAQ,IAAI;;;;;;;;;;;;;;;;;;;IAmBV;AACF,SAAQ,KAAK,EAAE;MAGf,OAAM,OAAO"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { paths } from '@/utils/paths.js';\nimport { isServerRunning, getServerVersion, shutdownServer } from './server-manager.js';\n\nconst args = process.argv.slice(2);\nconst command = args[0];\n\nif (command === 'serve') {\n // HTTP server mode\n const { startHttpServer } = await import('../server/fastify-server.js');\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n await startHttpServer(port);\n} else if (command === 'version') {\n // Show version\n console.log(paths.getVersion());\n process.exit(0);\n} else if (command === 'status') {\n // Check server status\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n const running = await isServerRunning(port);\n \n if (!running) {\n console.log('Server is not running');\n process.exit(1);\n }\n \n try {\n const response = await fetch(`http://localhost:${port}/api/status`);\n const status = await response.json() as any;\n \n console.log(`Server is running on port ${status.port}`);\n console.log(`Version: ${status.version}`);\n console.log(`Data directory: ${status.dataDir}`);\n console.log(`Task count: ${status.taskCount}`);\n console.log(`Uptime: ${status.uptime}s`);\n console.log(`Viewer: http://localhost:${port}/`);\n console.log(`MCP endpoint: http://localhost:${port}/mcp`);\n } catch (error) {\n // Fallback to old behavior if /api/status doesn't exist\n const version = await getServerVersion(port);\n console.log(`Server is running on port ${port}`);\n console.log(`Version: ${version || 'unknown'}`);\n console.log(`Viewer: http://localhost:${port}/`);\n console.log(`MCP endpoint: http://localhost:${port}/mcp`);\n }\n process.exit(0);\n} else if (command === 'stop') {\n // Stop server\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n const running = await isServerRunning(port);\n \n if (!running) {\n console.log('Server is not running');\n process.exit(0);\n }\n \n console.log(`Stopping server on port ${port}...`);\n await shutdownServer(port);\n console.log('Server stopped');\n process.exit(0);\n} else if (command === '--help' || command === '-h') {\n console.log(`\nbacklog-mcp - Task management MCP server\n\nUsage:\n backlog-mcp Run as stdio MCP server (auto-bridges to HTTP server)\n backlog-mcp serve Run as HTTP MCP server with viewer\n backlog-mcp version Show version\n backlog-mcp status Check if server is running\n backlog-mcp stop Stop the server\n backlog-mcp --help Show this help\n\nEnvironment variables:\n BACKLOG_DATA_DIR Data directory path (default: ./data)\n BACKLOG_VIEWER_PORT HTTP server port (default: 3030)\n\nHow it works:\n - Default mode auto-spawns HTTP server and bridges stdio to it\n - HTTP server persists across sessions (shared by multiple clients)\n - Automatic version upgrades on server restart\n `);\n process.exit(0);\n} else {\n // Default: bridge mode (auto-spawn HTTP server)\n await import('./bridge.js');\n}\n"],"mappings":";;;;;AAMA,MAAM,UADO,QAAQ,KAAK,MAAM,EAAE,CACb;AAErB,IAAI,YAAY,SAAS;CAEvB,MAAM,EAAE,oBAAoB,MAAM,OAAO;AAEzC,OAAM,gBADO,SAAS,QAAQ,IAAI,uBAAuB,OAAO,CACrC;WAClB,YAAY,WAAW;AAEhC,SAAQ,IAAI,MAAM,YAAY,CAAC;AAC/B,SAAQ,KAAK,EAAE;WACN,YAAY,UAAU;CAE/B,MAAM,OAAO,SAAS,QAAQ,IAAI,uBAAuB,OAAO;AAGhE,KAAI,CAFY,MAAM,gBAAgB,KAAK,EAE7B;AACZ,UAAQ,IAAI,wBAAwB;AACpC,UAAQ,KAAK,EAAE;;AAGjB,KAAI;EAEF,MAAM,SAAS,OADE,MAAM,MAAM,oBAAoB,KAAK,aAAa,EACrC,MAAM;AAEpC,UAAQ,IAAI,6BAA6B,OAAO,OAAO;AACvD,UAAQ,IAAI,YAAY,OAAO,UAAU;AACzC,UAAQ,IAAI,mBAAmB,OAAO,UAAU;AAChD,UAAQ,IAAI,eAAe,OAAO,YAAY;AAC9C,UAAQ,IAAI,WAAW,OAAO,OAAO,GAAG;AACxC,UAAQ,IAAI,4BAA4B,KAAK,GAAG;AAChD,UAAQ,IAAI,kCAAkC,KAAK,MAAM;UAClD,OAAO;EAEd,MAAM,UAAU,MAAM,iBAAiB,KAAK;AAC5C,UAAQ,IAAI,6BAA6B,OAAO;AAChD,UAAQ,IAAI,YAAY,WAAW,YAAY;AAC/C,UAAQ,IAAI,4BAA4B,KAAK,GAAG;AAChD,UAAQ,IAAI,kCAAkC,KAAK,MAAM;;AAE3D,SAAQ,KAAK,EAAE;WACN,YAAY,QAAQ;CAE7B,MAAM,OAAO,SAAS,QAAQ,IAAI,uBAAuB,OAAO;AAGhE,KAAI,CAFY,MAAM,gBAAgB,KAAK,EAE7B;AACZ,UAAQ,IAAI,wBAAwB;AACpC,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,2BAA2B,KAAK,KAAK;AACjD,OAAM,eAAe,KAAK;AAC1B,SAAQ,IAAI,iBAAiB;AAC7B,SAAQ,KAAK,EAAE;WACN,YAAY,YAAY,YAAY,MAAM;AACnD,SAAQ,IAAI;;;;;;;;;;;;;;;;;;;IAmBV;AACF,SAAQ,KAAK,EAAE;MAGf,OAAM,OAAO"}
@@ -0,0 +1,23 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+
3
+ //#region src/resources/data-dir.d.ts
4
+ /**
5
+ * Catch-all handler for any path in the backlog data directory.
6
+ *
7
+ * URI Template (RFC 6570): mcp://backlog/{+path}
8
+ * - {+path}: Greedy match - captures everything including slashes
9
+ *
10
+ * Examples:
11
+ * ✅ mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md
12
+ * ✅ mcp://backlog/artifacts/some-file.md
13
+ * ✅ mcp://backlog/any/nested/path/file.md
14
+ *
15
+ * Storage location: {BACKLOG_DATA_DIR}/{path}
16
+ *
17
+ * Note: This is the lowest priority handler (registered last).
18
+ * More specific handlers (tasks, task-attached resources) take precedence.
19
+ */
20
+ declare function registerDataDirResource(server: McpServer): void;
21
+ //#endregion
22
+ export { registerDataDirResource };
23
+ //# sourceMappingURL=data-dir.d.mts.map
@@ -0,0 +1,36 @@
1
+ import { readMcpResource } from "./resource-reader.mjs";
2
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+
4
+ //#region src/resources/data-dir.ts
5
+ /**
6
+ * Catch-all handler for any path in the backlog data directory.
7
+ *
8
+ * URI Template (RFC 6570): mcp://backlog/{+path}
9
+ * - {+path}: Greedy match - captures everything including slashes
10
+ *
11
+ * Examples:
12
+ * ✅ mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md
13
+ * ✅ mcp://backlog/artifacts/some-file.md
14
+ * ✅ mcp://backlog/any/nested/path/file.md
15
+ *
16
+ * Storage location: {BACKLOG_DATA_DIR}/{path}
17
+ *
18
+ * Note: This is the lowest priority handler (registered last).
19
+ * More specific handlers (tasks, task-attached resources) take precedence.
20
+ */
21
+ function registerDataDirResource(server) {
22
+ const template = new ResourceTemplate("mcp://backlog/{+path}", { list: void 0 });
23
+ server.registerResource("Data Directory Resource", template, { description: "Any file in the backlog data directory" }, async (uri, variables) => {
24
+ String(variables.path);
25
+ const resource = await readMcpResource(uri.toString());
26
+ return { contents: [{
27
+ uri: uri.toString(),
28
+ mimeType: resource.mimeType,
29
+ text: resource.content
30
+ }] };
31
+ });
32
+ }
33
+
34
+ //#endregion
35
+ export { registerDataDirResource };
36
+ //# sourceMappingURL=data-dir.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-dir.mjs","names":[],"sources":["../../src/resources/data-dir.ts"],"sourcesContent":["import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { readMcpResource } from './resource-reader.js';\n\n/**\n * Catch-all handler for any path in the backlog data directory.\n * \n * URI Template (RFC 6570): mcp://backlog/{+path}\n * - {+path}: Greedy match - captures everything including slashes\n * \n * Examples:\n * ✅ mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md\n * ✅ mcp://backlog/artifacts/some-file.md\n * ✅ mcp://backlog/any/nested/path/file.md\n * \n * Storage location: {BACKLOG_DATA_DIR}/{path}\n * \n * Note: This is the lowest priority handler (registered last).\n * More specific handlers (tasks, task-attached resources) take precedence.\n */\nexport function registerDataDirResource(server: McpServer) {\n const template = new ResourceTemplate(\n 'mcp://backlog/{+path}',\n { list: undefined }\n );\n \n server.registerResource(\n 'Data Directory Resource',\n template,\n { description: 'Any file in the backlog data directory' },\n async (uri, variables) => {\n const path = String(variables.path);\n \n const resource = await readMcpResource(uri.toString());\n return { contents: [{ uri: uri.toString(), mimeType: resource.mimeType, text: resource.content }] };\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,wBAAwB,QAAmB;CACzD,MAAM,WAAW,IAAI,iBACnB,yBACA,EAAE,MAAM,QAAW,CACpB;AAED,QAAO,iBACL,2BACA,UACA,EAAE,aAAa,0CAA0C,EACzD,OAAO,KAAK,cAAc;AACX,SAAO,UAAU,KAAK;EAEnC,MAAM,WAAW,MAAM,gBAAgB,IAAI,UAAU,CAAC;AACtD,SAAO,EAAE,UAAU,CAAC;GAAE,KAAK,IAAI,UAAU;GAAE,UAAU,SAAS;GAAU,MAAM,SAAS;GAAS,CAAC,EAAE;GAEtG"}
@@ -1,6 +1,18 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
 
3
3
  //#region src/resources/index-resources.d.ts
4
+ /**
5
+ * Register MCP resources.
6
+ *
7
+ * Single handler: mcp://backlog/{+path} → {BACKLOG_DATA_DIR}/{path}
8
+ *
9
+ * Examples:
10
+ * mcp://backlog/tasks/TASK-0092.md
11
+ * mcp://backlog/resources/TASK-0092/strategic-improvements.md
12
+ * mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md
13
+ *
14
+ * For git repo files (ADRs, source code), use file:// URIs.
15
+ */
4
16
  declare function registerResources(server: McpServer): void;
5
17
  //#endregion
6
18
  export { registerResources };
@@ -1,14 +1,20 @@
1
- import { registerTasksResource } from "./tasks.mjs";
2
- import { registerTaskByIdResource } from "./task-by-id.mjs";
3
- import { registerTaskAttachedResource } from "./task-attached.mjs";
4
- import { registerResourceFileResource } from "./resource-file.mjs";
1
+ import { registerDataDirResource } from "./data-dir.mjs";
5
2
 
6
3
  //#region src/resources/index-resources.ts
4
+ /**
5
+ * Register MCP resources.
6
+ *
7
+ * Single handler: mcp://backlog/{+path} → {BACKLOG_DATA_DIR}/{path}
8
+ *
9
+ * Examples:
10
+ * mcp://backlog/tasks/TASK-0092.md
11
+ * mcp://backlog/resources/TASK-0092/strategic-improvements.md
12
+ * mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md
13
+ *
14
+ * For git repo files (ADRs, source code), use file:// URIs.
15
+ */
7
16
  function registerResources(server) {
8
- registerTasksResource(server);
9
- registerTaskByIdResource(server);
10
- registerTaskAttachedResource(server);
11
- registerResourceFileResource(server);
17
+ registerDataDirResource(server);
12
18
  }
13
19
 
14
20
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index-resources.mjs","names":[],"sources":["../../src/resources/index-resources.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { registerTasksResource } from './tasks.js';\nimport { registerTaskByIdResource } from './task-by-id.js';\nimport { registerTaskAttachedResource } from './task-attached.js';\nimport { registerResourceFileResource } from './resource-file.js';\n\nexport function registerResources(server: McpServer) {\n registerTasksResource(server);\n registerTaskByIdResource(server);\n registerTaskAttachedResource(server);\n registerResourceFileResource(server);\n}\n"],"mappings":";;;;;;AAMA,SAAgB,kBAAkB,QAAmB;AACnD,uBAAsB,OAAO;AAC7B,0BAAyB,OAAO;AAChC,8BAA6B,OAAO;AACpC,8BAA6B,OAAO"}
1
+ {"version":3,"file":"index-resources.mjs","names":[],"sources":["../../src/resources/index-resources.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { registerDataDirResource } from './data-dir.js';\n\n/**\n * Register MCP resources.\n * \n * Single handler: mcp://backlog/{+path} {BACKLOG_DATA_DIR}/{path}\n * \n * Examples:\n * mcp://backlog/tasks/TASK-0092.md\n * mcp://backlog/resources/TASK-0092/strategic-improvements.md\n * mcp://backlog/backlog-mcp-engineer/system-config-visibility-2026-01-26/artifact.md\n * \n * For git repo files (ADRs, source code), use file:// URIs.\n */\nexport function registerResources(server: McpServer) {\n registerDataDirResource(server);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,kBAAkB,QAAmB;AACnD,yBAAwB,OAAO"}
@@ -1,6 +1,20 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
 
3
3
  //#region src/resources/resource-file.d.ts
4
+ /**
5
+ * Repository resources (docs, ADRs, etc.) - NOT task-attached resources.
6
+ *
7
+ * URI Template (RFC 6570): mcp://backlog/resources/{+path}
8
+ * - {+path}: Greedy match - captures everything including slashes
9
+ * - Excludes TASK-/EPIC- prefixed paths (handled by task-attached.ts)
10
+ *
11
+ * Examples:
12
+ * ✅ mcp://backlog/resources/docs/adr/0001-decision.md
13
+ * ✅ mcp://backlog/resources/README.md
14
+ * ❌ mcp://backlog/resources/TASK-0092/file.md (handled by task-attached.ts)
15
+ *
16
+ * Storage location: {REPO_ROOT}/{path}
17
+ */
4
18
  declare function registerResourceFileResource(server: McpServer): void;
5
19
  //#endregion
6
20
  export { registerResourceFileResource };
@@ -1,11 +1,29 @@
1
1
  import { readMcpResource } from "./resource-reader.mjs";
2
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
3
 
3
4
  //#region src/resources/resource-file.ts
5
+ /**
6
+ * Repository resources (docs, ADRs, etc.) - NOT task-attached resources.
7
+ *
8
+ * URI Template (RFC 6570): mcp://backlog/resources/{+path}
9
+ * - {+path}: Greedy match - captures everything including slashes
10
+ * - Excludes TASK-/EPIC- prefixed paths (handled by task-attached.ts)
11
+ *
12
+ * Examples:
13
+ * ✅ mcp://backlog/resources/docs/adr/0001-decision.md
14
+ * ✅ mcp://backlog/resources/README.md
15
+ * ❌ mcp://backlog/resources/TASK-0092/file.md (handled by task-attached.ts)
16
+ *
17
+ * Storage location: {REPO_ROOT}/{path}
18
+ */
4
19
  function registerResourceFileResource(server) {
5
- server.registerResource("Resource File", "mcp://backlog/resources/{path}", {
6
- description: "Read a resource file",
20
+ const template = new ResourceTemplate("mcp://backlog/resources/{+path}", { list: void 0 });
21
+ server.registerResource("Resource File", template, {
22
+ description: "Repository resource files (docs, ADRs, etc.) - excludes task-attached resources",
7
23
  mimeType: "text/plain"
8
- }, async (uri) => {
24
+ }, async (uri, variables) => {
25
+ const path = String(variables.path);
26
+ if (/^(TASK-\d+|EPIC-\d+)\//.test(path)) throw new Error(`Task-attached resources must use the Task-Attached Resource handler. Path: ${path}`);
9
27
  const resource = await readMcpResource(uri.toString());
10
28
  return { contents: [{
11
29
  uri: uri.toString(),
@@ -1 +1 @@
1
- {"version":3,"file":"resource-file.mjs","names":[],"sources":["../../src/resources/resource-file.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { readMcpResource } from './resource-reader.js';\n\nexport function registerResourceFileResource(server: McpServer) {\n server.registerResource(\n 'Resource File',\n 'mcp://backlog/resources/{path}',\n { description: 'Read a resource file', mimeType: 'text/plain' },\n async (uri: URL) => {\n const resource = await readMcpResource(uri.toString());\n return { contents: [{ uri: uri.toString(), mimeType: resource.mimeType, text: resource.content }] };\n }\n );\n}\n"],"mappings":";;;AAGA,SAAgB,6BAA6B,QAAmB;AAC9D,QAAO,iBACL,iBACA,kCACA;EAAE,aAAa;EAAwB,UAAU;EAAc,EAC/D,OAAO,QAAa;EAClB,MAAM,WAAW,MAAM,gBAAgB,IAAI,UAAU,CAAC;AACtD,SAAO,EAAE,UAAU,CAAC;GAAE,KAAK,IAAI,UAAU;GAAE,UAAU,SAAS;GAAU,MAAM,SAAS;GAAS,CAAC,EAAE;GAEtG"}
1
+ {"version":3,"file":"resource-file.mjs","names":[],"sources":["../../src/resources/resource-file.ts"],"sourcesContent":["import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { readMcpResource } from './resource-reader.js';\n\n/**\n * Repository resources (docs, ADRs, etc.) - NOT task-attached resources.\n * \n * URI Template (RFC 6570): mcp://backlog/resources/{+path}\n * - {+path}: Greedy match - captures everything including slashes\n * - Excludes TASK-/EPIC- prefixed paths (handled by task-attached.ts)\n * \n * Examples:\n * ✅ mcp://backlog/resources/docs/adr/0001-decision.md\n * ✅ mcp://backlog/resources/README.md\n * ❌ mcp://backlog/resources/TASK-0092/file.md (handled by task-attached.ts)\n * \n * Storage location: {REPO_ROOT}/{path}\n */\nexport function registerResourceFileResource(server: McpServer) {\n const template = new ResourceTemplate(\n 'mcp://backlog/resources/{+path}',\n { list: undefined } // No listing callback needed\n );\n \n server.registerResource(\n 'Resource File',\n template,\n { description: 'Repository resource files (docs, ADRs, etc.) - excludes task-attached resources', mimeType: 'text/plain' },\n async (uri, variables) => {\n const path = String(variables.path);\n \n // Reject task-attached resources - they have their own handler\n if (/^(TASK-\\d+|EPIC-\\d+)\\//.test(path)) {\n throw new Error(`Task-attached resources must use the Task-Attached Resource handler. Path: ${path}`);\n }\n \n const resource = await readMcpResource(uri.toString());\n return { contents: [{ uri: uri.toString(), mimeType: resource.mimeType, text: resource.content }] };\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,SAAgB,6BAA6B,QAAmB;CAC9D,MAAM,WAAW,IAAI,iBACnB,mCACA,EAAE,MAAM,QAAW,CACpB;AAED,QAAO,iBACL,iBACA,UACA;EAAE,aAAa;EAAmF,UAAU;EAAc,EAC1H,OAAO,KAAK,cAAc;EACxB,MAAM,OAAO,OAAO,UAAU,KAAK;AAGnC,MAAI,yBAAyB,KAAK,KAAK,CACrC,OAAM,IAAI,MAAM,8EAA8E,OAAO;EAGvG,MAAM,WAAW,MAAM,gBAAgB,IAAI,UAAU,CAAC;AACtD,SAAO,EAAE,UAAU,CAAC;GAAE,KAAK,IAAI,UAAU;GAAE,UAAU,SAAS;GAAU,MAAM,SAAS;GAAS,CAAC,EAAE;GAEtG"}
@@ -1,6 +1,20 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
 
3
3
  //#region src/resources/task-attached.d.ts
4
+ /**
5
+ * Task-attached resources (ADRs, design docs, artifacts specific to a task).
6
+ *
7
+ * URI Template (RFC 6570): mcp://backlog/resources/{taskId}/{filename}
8
+ * - {taskId}: TASK-NNNN or EPIC-NNNN
9
+ * - {filename}: Any filename (no slashes)
10
+ *
11
+ * Examples:
12
+ * ✅ mcp://backlog/resources/TASK-0092/strategic-improvements.md
13
+ * ✅ mcp://backlog/resources/EPIC-0002/roadmap.md
14
+ * ❌ mcp://backlog/resources/docs/adr/0001.md (handled by resource-file.ts)
15
+ *
16
+ * Storage location: {BACKLOG_DATA_DIR}/resources/{taskId}/{filename}
17
+ */
4
18
  declare function registerTaskAttachedResource(server: McpServer): void;
5
19
  //#endregion
6
20
  export { registerTaskAttachedResource };
@@ -1,8 +1,27 @@
1
1
  import { readMcpResource } from "./resource-reader.mjs";
2
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
3
 
3
4
  //#region src/resources/task-attached.ts
5
+ /**
6
+ * Task-attached resources (ADRs, design docs, artifacts specific to a task).
7
+ *
8
+ * URI Template (RFC 6570): mcp://backlog/resources/{taskId}/{filename}
9
+ * - {taskId}: TASK-NNNN or EPIC-NNNN
10
+ * - {filename}: Any filename (no slashes)
11
+ *
12
+ * Examples:
13
+ * ✅ mcp://backlog/resources/TASK-0092/strategic-improvements.md
14
+ * ✅ mcp://backlog/resources/EPIC-0002/roadmap.md
15
+ * ❌ mcp://backlog/resources/docs/adr/0001.md (handled by resource-file.ts)
16
+ *
17
+ * Storage location: {BACKLOG_DATA_DIR}/resources/{taskId}/{filename}
18
+ */
4
19
  function registerTaskAttachedResource(server) {
5
- server.registerResource("Task-Attached Resource", "mcp://backlog/resources/{taskId}/{filename}", { description: "Task-attached resources (ADRs, design docs, etc.)" }, async (uri) => {
20
+ const template = new ResourceTemplate("mcp://backlog/resources/{taskId}/{filename}", { list: void 0 });
21
+ server.registerResource("Task-Attached Resource", template, { description: "Task-attached resources (ADRs, design docs, etc.)" }, async (uri, variables) => {
22
+ const taskId = String(variables.taskId);
23
+ String(variables.filename);
24
+ if (!/^(TASK-\d+|EPIC-\d+)$/.test(taskId)) throw new Error(`Invalid task ID format. Expected TASK-NNNN or EPIC-NNNN, got: ${taskId}`);
6
25
  const { content, mimeType } = await readMcpResource(uri.toString());
7
26
  return { contents: [{
8
27
  uri: uri.toString(),
@@ -1 +1 @@
1
- {"version":3,"file":"task-attached.mjs","names":[],"sources":["../../src/resources/task-attached.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { readMcpResource } from './resource-reader.js';\n\nexport function registerTaskAttachedResource(server: McpServer) {\n server.registerResource(\n 'Task-Attached Resource',\n 'mcp://backlog/resources/{taskId}/{filename}',\n { description: 'Task-attached resources (ADRs, design docs, etc.)' },\n async (uri: URL) => {\n const { content, mimeType } = await readMcpResource(uri.toString());\n return { contents: [{ uri: uri.toString(), mimeType, text: content }] };\n }\n );\n}\n"],"mappings":";;;AAGA,SAAgB,6BAA6B,QAAmB;AAC9D,QAAO,iBACL,0BACA,+CACA,EAAE,aAAa,qDAAqD,EACpE,OAAO,QAAa;EAClB,MAAM,EAAE,SAAS,aAAa,MAAM,gBAAgB,IAAI,UAAU,CAAC;AACnE,SAAO,EAAE,UAAU,CAAC;GAAE,KAAK,IAAI,UAAU;GAAE;GAAU,MAAM;GAAS,CAAC,EAAE;GAE1E"}
1
+ {"version":3,"file":"task-attached.mjs","names":[],"sources":["../../src/resources/task-attached.ts"],"sourcesContent":["import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { readMcpResource } from './resource-reader.js';\n\n/**\n * Task-attached resources (ADRs, design docs, artifacts specific to a task).\n * \n * URI Template (RFC 6570): mcp://backlog/resources/{taskId}/{filename}\n * - {taskId}: TASK-NNNN or EPIC-NNNN\n * - {filename}: Any filename (no slashes)\n * \n * Examples:\n * ✅ mcp://backlog/resources/TASK-0092/strategic-improvements.md\n * ✅ mcp://backlog/resources/EPIC-0002/roadmap.md\n * ❌ mcp://backlog/resources/docs/adr/0001.md (handled by resource-file.ts)\n * \n * Storage location: {BACKLOG_DATA_DIR}/resources/{taskId}/{filename}\n */\nexport function registerTaskAttachedResource(server: McpServer) {\n const template = new ResourceTemplate(\n 'mcp://backlog/resources/{taskId}/{filename}',\n { list: undefined } // No listing callback needed\n );\n \n server.registerResource(\n 'Task-Attached Resource',\n template,\n { description: 'Task-attached resources (ADRs, design docs, etc.)' },\n async (uri, variables) => {\n const taskId = String(variables.taskId);\n const filename = String(variables.filename);\n \n // Validate task ID format\n if (!/^(TASK-\\d+|EPIC-\\d+)$/.test(taskId)) {\n throw new Error(`Invalid task ID format. Expected TASK-NNNN or EPIC-NNNN, got: ${taskId}`);\n }\n \n const { content, mimeType } = await readMcpResource(uri.toString());\n return { contents: [{ uri: uri.toString(), mimeType, text: content }] };\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,SAAgB,6BAA6B,QAAmB;CAC9D,MAAM,WAAW,IAAI,iBACnB,+CACA,EAAE,MAAM,QAAW,CACpB;AAED,QAAO,iBACL,0BACA,UACA,EAAE,aAAa,qDAAqD,EACpE,OAAO,KAAK,cAAc;EACxB,MAAM,SAAS,OAAO,UAAU,OAAO;AACtB,SAAO,UAAU,SAAS;AAG3C,MAAI,CAAC,wBAAwB,KAAK,OAAO,CACvC,OAAM,IAAI,MAAM,iEAAiE,SAAS;EAG5F,MAAM,EAAE,SAAS,aAAa,MAAM,gBAAgB,IAAI,UAAU,CAAC;AACnE,SAAO,EAAE,UAAU,CAAC;GAAE,KAAK,IAAI,UAAU;GAAE;GAAU,MAAM;GAAS,CAAC,EAAE;GAE1E"}
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { storage } from "../storage/backlog.mjs";
3
2
  import { paths } from "../utils/paths.mjs";
4
3
  import { registerViewerRoutes } from "./viewer-routes.mjs";
5
4
  import { registerMcpHandler } from "./mcp-handler.mjs";
@@ -12,8 +11,6 @@ const app = Fastify({
12
11
  logger: false,
13
12
  bodyLimit: 10 * 1024 * 1024
14
13
  });
15
- const dataDir = process.env.BACKLOG_DATA_DIR ?? "data";
16
- storage.init(dataDir);
17
14
  await app.register(cors, { origin: "*" });
18
15
  app.addHook("preHandler", authMiddleware);
19
16
  registerViewerRoutes(app);
@@ -32,6 +29,7 @@ async function startHttpServer(port = 3030) {
32
29
  console.log(`Backlog MCP server running on http://localhost:${port}`);
33
30
  console.log(`- Viewer: http://localhost:${port}/`);
34
31
  console.log(`- MCP endpoint: http://localhost:${port}/mcp`);
32
+ console.log(`- Data directory: ${paths.backlogDataDir}`);
35
33
  }
36
34
  const shutdown = async () => {
37
35
  console.log("Shutting down gracefully...");
@@ -1 +1 @@
1
- {"version":3,"file":"fastify-server.mjs","names":[],"sources":["../../src/server/fastify-server.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport Fastify from 'fastify';\nimport cors from '@fastify/cors';\nimport { registerViewerRoutes } from './viewer-routes.js';\nimport { registerMcpHandler } from './mcp-handler.js';\nimport { authMiddleware } from '@/middleware/auth.js';\nimport { storage } from '@/storage/backlog.js';\nimport { paths } from '@/utils/paths.js';\n\nconst app = Fastify({ logger: false, bodyLimit: 10 * 1024 * 1024 });\n\n// Initialize storage\nconst dataDir = process.env.BACKLOG_DATA_DIR ?? 'data';\nstorage.init(dataDir);\n\n// CORS\nawait app.register(cors, { origin: '*' });\n\n// Auth middleware\napp.addHook('preHandler', authMiddleware);\n\n// Register routes\nregisterViewerRoutes(app);\nregisterMcpHandler(app);\n\n// Health check\napp.get('/health', async () => ({ status: 'ok' }));\n\n// Version endpoint\napp.get('/version', async () => paths.getVersion());\n\n// Shutdown endpoint\napp.post('/shutdown', async (request, reply) => {\n reply.send('Shutting down...');\n setTimeout(() => process.exit(0), 500);\n});\n\nexport async function startHttpServer(port: number = 3030): Promise<void> {\n await app.listen({ port, host: '0.0.0.0' });\n console.log(`Backlog MCP server running on http://localhost:${port}`);\n console.log(`- Viewer: http://localhost:${port}/`);\n console.log(`- MCP endpoint: http://localhost:${port}/mcp`);\n}\n\n// Graceful shutdown\nconst shutdown = async () => {\n console.log('Shutting down gracefully...');\n await app.close();\n setTimeout(() => process.exit(0), 500);\n};\n\nprocess.on('SIGTERM', shutdown);\nprocess.on('SIGINT', shutdown);\n\n// Run if executed directly\nif (import.meta.url === `file://${process.argv[1]}`) {\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n startHttpServer(port).catch((error) => {\n console.error('Failed to start server:', error);\n process.exit(1);\n });\n}\n"],"mappings":";;;;;;;;;;AAUA,MAAM,MAAM,QAAQ;CAAE,QAAQ;CAAO,WAAW,KAAK,OAAO;CAAM,CAAC;AAGnE,MAAM,UAAU,QAAQ,IAAI,oBAAoB;AAChD,QAAQ,KAAK,QAAQ;AAGrB,MAAM,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,CAAC;AAGzC,IAAI,QAAQ,cAAc,eAAe;AAGzC,qBAAqB,IAAI;AACzB,mBAAmB,IAAI;AAGvB,IAAI,IAAI,WAAW,aAAa,EAAE,QAAQ,MAAM,EAAE;AAGlD,IAAI,IAAI,YAAY,YAAY,MAAM,YAAY,CAAC;AAGnD,IAAI,KAAK,aAAa,OAAO,SAAS,UAAU;AAC9C,OAAM,KAAK,mBAAmB;AAC9B,kBAAiB,QAAQ,KAAK,EAAE,EAAE,IAAI;EACtC;AAEF,eAAsB,gBAAgB,OAAe,MAAqB;AACxE,OAAM,IAAI,OAAO;EAAE;EAAM,MAAM;EAAW,CAAC;AAC3C,SAAQ,IAAI,kDAAkD,OAAO;AACrE,SAAQ,IAAI,8BAA8B,KAAK,GAAG;AAClD,SAAQ,IAAI,oCAAoC,KAAK,MAAM;;AAI7D,MAAM,WAAW,YAAY;AAC3B,SAAQ,IAAI,8BAA8B;AAC1C,OAAM,IAAI,OAAO;AACjB,kBAAiB,QAAQ,KAAK,EAAE,EAAE,IAAI;;AAGxC,QAAQ,GAAG,WAAW,SAAS;AAC/B,QAAQ,GAAG,UAAU,SAAS;AAG9B,IAAI,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,KAE7C,iBADa,SAAS,QAAQ,IAAI,uBAAuB,OAAO,CAC3C,CAAC,OAAO,UAAU;AACrC,SAAQ,MAAM,2BAA2B,MAAM;AAC/C,SAAQ,KAAK,EAAE;EACf"}
1
+ {"version":3,"file":"fastify-server.mjs","names":[],"sources":["../../src/server/fastify-server.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport Fastify from 'fastify';\nimport cors from '@fastify/cors';\nimport { registerViewerRoutes } from './viewer-routes.js';\nimport { registerMcpHandler } from './mcp-handler.js';\nimport { authMiddleware } from '@/middleware/auth.js';\nimport { paths } from '@/utils/paths.js';\n\nconst app = Fastify({ logger: false, bodyLimit: 10 * 1024 * 1024 });\n\n// CORS\nawait app.register(cors, { origin: '*' });\n\n// Auth middleware\napp.addHook('preHandler', authMiddleware);\n\n// Register routes\nregisterViewerRoutes(app);\nregisterMcpHandler(app);\n\n// Health check\napp.get('/health', async () => ({ status: 'ok' }));\n\n// Version endpoint\napp.get('/version', async () => paths.getVersion());\n\n// Shutdown endpoint\napp.post('/shutdown', async (request, reply) => {\n reply.send('Shutting down...');\n setTimeout(() => process.exit(0), 500);\n});\n\nexport async function startHttpServer(port: number = 3030): Promise<void> {\n await app.listen({ port, host: '0.0.0.0' });\n console.log(`Backlog MCP server running on http://localhost:${port}`);\n console.log(`- Viewer: http://localhost:${port}/`);\n console.log(`- MCP endpoint: http://localhost:${port}/mcp`);\n console.log(`- Data directory: ${paths.backlogDataDir}`);\n}\n\n// Graceful shutdown\nconst shutdown = async () => {\n console.log('Shutting down gracefully...');\n await app.close();\n setTimeout(() => process.exit(0), 500);\n};\n\nprocess.on('SIGTERM', shutdown);\nprocess.on('SIGINT', shutdown);\n\n// Run if executed directly\nif (import.meta.url === `file://${process.argv[1]}`) {\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n startHttpServer(port).catch((error) => {\n console.error('Failed to start server:', error);\n process.exit(1);\n });\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,MAAM,QAAQ;CAAE,QAAQ;CAAO,WAAW,KAAK,OAAO;CAAM,CAAC;AAGnE,MAAM,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,CAAC;AAGzC,IAAI,QAAQ,cAAc,eAAe;AAGzC,qBAAqB,IAAI;AACzB,mBAAmB,IAAI;AAGvB,IAAI,IAAI,WAAW,aAAa,EAAE,QAAQ,MAAM,EAAE;AAGlD,IAAI,IAAI,YAAY,YAAY,MAAM,YAAY,CAAC;AAGnD,IAAI,KAAK,aAAa,OAAO,SAAS,UAAU;AAC9C,OAAM,KAAK,mBAAmB;AAC9B,kBAAiB,QAAQ,KAAK,EAAE,EAAE,IAAI;EACtC;AAEF,eAAsB,gBAAgB,OAAe,MAAqB;AACxE,OAAM,IAAI,OAAO;EAAE;EAAM,MAAM;EAAW,CAAC;AAC3C,SAAQ,IAAI,kDAAkD,OAAO;AACrE,SAAQ,IAAI,8BAA8B,KAAK,GAAG;AAClD,SAAQ,IAAI,oCAAoC,KAAK,MAAM;AAC3D,SAAQ,IAAI,qBAAqB,MAAM,iBAAiB;;AAI1D,MAAM,WAAW,YAAY;AAC3B,SAAQ,IAAI,8BAA8B;AAC1C,OAAM,IAAI,OAAO;AACjB,kBAAiB,QAAQ,KAAK,EAAE,EAAE,IAAI;;AAGxC,QAAQ,GAAG,WAAW,SAAS;AAC/B,QAAQ,GAAG,UAAU,SAAS;AAG9B,IAAI,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,KAE7C,iBADa,SAAS,QAAQ,IAAI,uBAAuB,OAAO,CAC3C,CAAC,OAAO,UAAU;AACrC,SAAQ,MAAM,2BAA2B,MAAM;AAC/C,SAAQ,KAAK,EAAE;EACf"}
@@ -1,5 +1,5 @@
1
- import { storage } from "../storage/backlog.mjs";
2
1
  import { paths } from "../utils/paths.mjs";
2
+ import { storage } from "../storage/backlog.mjs";
3
3
  import { filePathToMcpUri, resolveMcpUri } from "../utils/uri-resolver.mjs";
4
4
  import { readMcpResource } from "../resources/resource-reader.mjs";
5
5
  import { existsSync, readFileSync } from "node:fs";
@@ -37,6 +37,18 @@ function registerViewerRoutes(app) {
37
37
  if (!task) return reply.code(404).send({ error: "Task not found" });
38
38
  return task;
39
39
  });
40
+ app.get("/api/status", async () => {
41
+ const tasks = storage.list({ limit: 1e4 });
42
+ const address = app.server.address();
43
+ const port = typeof address === "object" && address ? address.port : 3030;
44
+ return {
45
+ version: paths.getVersion(),
46
+ port,
47
+ dataDir: paths.backlogDataDir,
48
+ taskCount: tasks.length,
49
+ uptime: Math.floor(process.uptime())
50
+ };
51
+ });
40
52
  app.get("/open/:id", async (request, reply) => {
41
53
  const { id } = request.params;
42
54
  const filePath = storage.getFilePath(id);
@@ -1 +1 @@
1
- {"version":3,"file":"viewer-routes.mjs","names":[],"sources":["../../src/server/viewer-routes.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport fastifyStatic from '@fastify/static';\nimport { exec } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport matter from 'gray-matter';\nimport { storage } from '../storage/backlog.js';\nimport { readMcpResource } from '../resources/resource-reader.js';\nimport { resolveMcpUri, filePathToMcpUri } from '../utils/uri-resolver.js';\nimport { paths } from '../utils/paths.js';\n\nexport function registerViewerRoutes(app: FastifyInstance) {\n // Static files - serve from dist/viewer (built assets)\n app.register(fastifyStatic, {\n root: paths.viewerDist,\n prefix: '/',\n });\n\n // List tasks\n app.get('/tasks', async (request) => {\n const { filter, limit } = request.query as { filter?: string; limit?: string };\n \n const statusMap: Record<string, any> = {\n active: { status: ['open', 'in_progress', 'blocked'] },\n done: { status: ['done'] },\n cancelled: { status: ['cancelled'] },\n all: {},\n };\n \n const filterConfig = statusMap[filter || 'active'] || statusMap.active;\n const tasks = storage.list({ ...filterConfig, limit: limit ? parseInt(limit) : 100 });\n \n return tasks;\n });\n\n // Get single task\n app.get('/tasks/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n const task = storage.get(id);\n \n if (!task) {\n return reply.code(404).send({ error: 'Task not found' });\n }\n \n return task;\n });\n\n // Open task in editor\n app.get('/open/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n const filePath = storage.getFilePath(id);\n \n if (!filePath) {\n return reply.code(404).send({ error: 'Task not found' });\n }\n \n exec(`open \"${filePath}\"`);\n return { status: 'Opening...' };\n });\n\n // Resource proxy\n app.get('/resource', async (request, reply) => {\n const { path: filePath } = request.query as { path?: string };\n \n if (!filePath) {\n return reply.code(400).send({ error: 'Missing path parameter' });\n }\n \n if (!existsSync(filePath)) {\n return reply.code(404).send({ error: 'File not found', path: filePath });\n }\n \n try {\n const content = readFileSync(filePath, 'utf-8');\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n const mimeMap: Record<string, string> = {\n md: 'text/markdown',\n ts: 'text/typescript',\n js: 'text/javascript',\n json: 'application/json',\n txt: 'text/plain',\n };\n \n let frontmatter = {};\n let bodyContent = content;\n \n // Parse frontmatter for markdown files\n if (ext === 'md') {\n const parsed = matter(content);\n frontmatter = parsed.data;\n bodyContent = parsed.content;\n }\n \n return {\n content: bodyContent,\n frontmatter,\n type: mimeMap[ext] || 'text/plain',\n path: filePath,\n fileUri: `file://${filePath}`,\n mcpUri: filePathToMcpUri(filePath),\n ext\n };\n } catch (error: any) {\n return reply.code(500).send({ error: 'Failed to read file', message: error.message });\n }\n });\n\n // MCP resource proxy\n app.get('/mcp/resource', async (request, reply) => {\n const { uri } = request.query as { uri?: string };\n \n if (!uri || !uri.startsWith('mcp://backlog/')) {\n return reply.code(400).send({ error: 'Invalid MCP URI' });\n }\n \n try {\n const resource = await readMcpResource(uri);\n const filePath = resolveMcpUri(uri);\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n \n return {\n content: resource.content,\n frontmatter: resource.frontmatter || {},\n type: resource.mimeType,\n path: filePath,\n fileUri: `file://${filePath}`,\n mcpUri: uri,\n ext\n };\n } catch (error: any) {\n return reply.code(404).send({ error: 'Resource not found', uri, message: error.message });\n }\n });\n\n // Open resource in viewer\n app.get('/open', async (request, reply) => {\n const { uri } = request.query as { uri?: string };\n \n if (!uri) {\n return reply.code(400).send({ error: 'Missing uri parameter' });\n }\n \n return reply.redirect(`/?resource=${encodeURIComponent(uri)}`);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"viewer-routes.mjs","names":[],"sources":["../../src/server/viewer-routes.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport fastifyStatic from '@fastify/static';\nimport { exec } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport matter from 'gray-matter';\nimport { storage } from '../storage/backlog.js';\nimport { readMcpResource } from '../resources/resource-reader.js';\nimport { resolveMcpUri, filePathToMcpUri } from '../utils/uri-resolver.js';\nimport { paths } from '../utils/paths.js';\n\nexport function registerViewerRoutes(app: FastifyInstance) {\n // Static files - serve from dist/viewer (built assets)\n app.register(fastifyStatic, {\n root: paths.viewerDist,\n prefix: '/',\n });\n\n // List tasks\n app.get('/tasks', async (request) => {\n const { filter, limit } = request.query as { filter?: string; limit?: string };\n \n const statusMap: Record<string, any> = {\n active: { status: ['open', 'in_progress', 'blocked'] },\n done: { status: ['done'] },\n cancelled: { status: ['cancelled'] },\n all: {},\n };\n \n const filterConfig = statusMap[filter || 'active'] || statusMap.active;\n const tasks = storage.list({ ...filterConfig, limit: limit ? parseInt(limit) : 100 });\n \n return tasks;\n });\n\n // Get single task\n app.get('/tasks/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n const task = storage.get(id);\n \n if (!task) {\n return reply.code(404).send({ error: 'Task not found' });\n }\n \n return task;\n });\n\n // System status\n app.get('/api/status', async () => {\n const tasks = storage.list({ limit: 10000 });\n const address = app.server.address();\n const port = typeof address === 'object' && address ? address.port : 3030;\n \n return {\n version: paths.getVersion(),\n port,\n dataDir: paths.backlogDataDir,\n taskCount: tasks.length,\n uptime: Math.floor(process.uptime())\n };\n });\n\n // Open task in editor\n app.get('/open/:id', async (request, reply) => {\n const { id } = request.params as { id: string };\n const filePath = storage.getFilePath(id);\n \n if (!filePath) {\n return reply.code(404).send({ error: 'Task not found' });\n }\n \n exec(`open \"${filePath}\"`);\n return { status: 'Opening...' };\n });\n\n // Resource proxy\n app.get('/resource', async (request, reply) => {\n const { path: filePath } = request.query as { path?: string };\n \n if (!filePath) {\n return reply.code(400).send({ error: 'Missing path parameter' });\n }\n \n if (!existsSync(filePath)) {\n return reply.code(404).send({ error: 'File not found', path: filePath });\n }\n \n try {\n const content = readFileSync(filePath, 'utf-8');\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n const mimeMap: Record<string, string> = {\n md: 'text/markdown',\n ts: 'text/typescript',\n js: 'text/javascript',\n json: 'application/json',\n txt: 'text/plain',\n };\n \n let frontmatter = {};\n let bodyContent = content;\n \n // Parse frontmatter for markdown files\n if (ext === 'md') {\n const parsed = matter(content);\n frontmatter = parsed.data;\n bodyContent = parsed.content;\n }\n \n return {\n content: bodyContent,\n frontmatter,\n type: mimeMap[ext] || 'text/plain',\n path: filePath,\n fileUri: `file://${filePath}`,\n mcpUri: filePathToMcpUri(filePath),\n ext\n };\n } catch (error: any) {\n return reply.code(500).send({ error: 'Failed to read file', message: error.message });\n }\n });\n\n // MCP resource proxy\n app.get('/mcp/resource', async (request, reply) => {\n const { uri } = request.query as { uri?: string };\n \n if (!uri || !uri.startsWith('mcp://backlog/')) {\n return reply.code(400).send({ error: 'Invalid MCP URI' });\n }\n \n try {\n const resource = await readMcpResource(uri);\n const filePath = resolveMcpUri(uri);\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n \n return {\n content: resource.content,\n frontmatter: resource.frontmatter || {},\n type: resource.mimeType,\n path: filePath,\n fileUri: `file://${filePath}`,\n mcpUri: uri,\n ext\n };\n } catch (error: any) {\n return reply.code(404).send({ error: 'Resource not found', uri, message: error.message });\n }\n });\n\n // Open resource in viewer\n app.get('/open', async (request, reply) => {\n const { uri } = request.query as { uri?: string };\n \n if (!uri) {\n return reply.code(400).send({ error: 'Missing uri parameter' });\n }\n \n return reply.redirect(`/?resource=${encodeURIComponent(uri)}`);\n });\n}\n"],"mappings":"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"}
@@ -2,10 +2,8 @@ import { Status, Task, TaskType } from "./schema.mjs";
2
2
 
3
3
  //#region src/storage/backlog.d.ts
4
4
  declare class BacklogStorage {
5
- private dataDir;
6
5
  private static instance;
7
6
  static getInstance(): BacklogStorage;
8
- init(dataDir: string): void;
9
7
  private get tasksPath();
10
8
  private ensureDir;
11
9
  private taskFilePath;
@@ -1,3 +1,4 @@
1
+ import { paths } from "../utils/paths.mjs";
1
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import matter from "gray-matter";
@@ -5,17 +6,13 @@ import matter from "gray-matter";
5
6
  //#region src/storage/backlog.ts
6
7
  const TASKS_DIR = "tasks";
7
8
  var BacklogStorage = class BacklogStorage {
8
- dataDir = "data";
9
9
  static instance;
10
10
  static getInstance() {
11
11
  if (!BacklogStorage.instance) BacklogStorage.instance = new BacklogStorage();
12
12
  return BacklogStorage.instance;
13
13
  }
14
- init(dataDir) {
15
- this.dataDir = dataDir;
16
- }
17
14
  get tasksPath() {
18
- return join(this.dataDir, TASKS_DIR);
15
+ return join(paths.backlogDataDir, TASKS_DIR);
19
16
  }
20
17
  ensureDir(dir) {
21
18
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
@@ -77,7 +74,7 @@ var BacklogStorage = class BacklogStorage {
77
74
  const path = this.taskFilePath(id);
78
75
  if (existsSync(path)) {
79
76
  unlinkSync(path);
80
- const resourcesPath = join(this.dataDir, "resources", id);
77
+ const resourcesPath = join(paths.backlogDataDir, "resources", id);
81
78
  if (existsSync(resourcesPath)) rmSync(resourcesPath, {
82
79
  recursive: true,
83
80
  force: true
@@ -1 +1 @@
1
- {"version":3,"file":"backlog.mjs","names":[],"sources":["../../src/storage/backlog.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport matter from 'gray-matter';\nimport type { Task, Status, TaskType } from './schema.js';\n\nconst TASKS_DIR = 'tasks';\n\nclass BacklogStorage {\n private dataDir: string = 'data';\n private static instance: BacklogStorage;\n\n static getInstance(): BacklogStorage {\n if (!BacklogStorage.instance) {\n BacklogStorage.instance = new BacklogStorage();\n }\n return BacklogStorage.instance;\n }\n\n init(dataDir: string): void {\n this.dataDir = dataDir;\n }\n\n private get tasksPath(): string {\n return join(this.dataDir, TASKS_DIR);\n }\n\n private ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n }\n\n private taskFilePath(id: string): string {\n return join(this.tasksPath, `${id}.md`);\n }\n\n private taskToMarkdown(task: Task): string {\n const { description, ...frontmatter } = task;\n return matter.stringify(description || '', frontmatter);\n }\n\n private markdownToTask(content: string): Task {\n const { data, content: description } = matter(content);\n return { ...data, description: description.trim() } as Task;\n }\n\n getFilePath(id: string): string | null {\n const path = this.taskFilePath(id);\n return existsSync(path) ? path : null;\n }\n\n private *iterateTasks(): Generator<Task> {\n if (existsSync(this.tasksPath)) {\n for (const file of readdirSync(this.tasksPath).filter(f => f.endsWith('.md'))) {\n const filePath = join(this.tasksPath, file);\n try {\n yield this.markdownToTask(readFileSync(filePath, 'utf-8'));\n } catch (error) {\n // Skip files that were deleted between listing and reading\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw error;\n }\n }\n }\n }\n }\n\n get(id: string): Task | undefined {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n return this.markdownToTask(readFileSync(path, 'utf-8'));\n }\n return undefined;\n }\n\n getMarkdown(id: string): string | null {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n return readFileSync(path, 'utf-8');\n }\n return null;\n }\n\n list(filter?: { status?: Status[]; type?: TaskType; epic_id?: string; limit?: number }): Task[] {\n const { status, type, epic_id, limit = 20 } = filter ?? {};\n\n let tasks = Array.from(this.iterateTasks());\n \n if (status) tasks = tasks.filter(t => status.includes(t.status));\n if (type) tasks = tasks.filter(t => (t.type ?? 'task') === type);\n if (epic_id) tasks = tasks.filter(t => t.epic_id === epic_id);\n\n return tasks\n .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())\n .slice(0, limit);\n }\n\n add(task: Task): void {\n this.ensureDir(this.tasksPath);\n const filePath = this.taskFilePath(task.id);\n writeFileSync(filePath, this.taskToMarkdown(task));\n }\n\n save(task: Task): void {\n this.ensureDir(this.tasksPath);\n const filePath = this.taskFilePath(task.id);\n writeFileSync(filePath, this.taskToMarkdown(task));\n }\n\n delete(id: string): boolean {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n unlinkSync(path);\n \n // Delete associated resources if they exist\n const resourcesPath = join(this.dataDir, 'resources', id);\n if (existsSync(resourcesPath)) {\n rmSync(resourcesPath, { recursive: true, force: true });\n }\n \n return true;\n }\n return false;\n }\n\n counts(): { total_tasks: number; total_epics: number; by_status: Record<Status, number> } {\n const by_status: Record<Status, number> = {\n open: 0,\n in_progress: 0,\n blocked: 0,\n done: 0,\n cancelled: 0,\n };\n\n let total_tasks = 0;\n let total_epics = 0;\n\n for (const task of this.iterateTasks()) {\n by_status[task.status]++;\n if ((task.type ?? 'task') === 'epic') {\n total_epics++;\n } else {\n total_tasks++;\n }\n }\n\n return { total_tasks, total_epics, by_status };\n }\n\n getMaxId(type?: 'task' | 'epic'): number {\n const pattern = type === 'epic' ? /^EPIC-(\\d{4,})\\.md$/ : /^TASK-(\\d{4,})\\.md$/;\n let maxNum = 0;\n\n if (existsSync(this.tasksPath)) {\n for (const file of readdirSync(this.tasksPath)) {\n const match = pattern.exec(file);\n if (match?.[1]) {\n const num = parseInt(match[1], 10);\n if (num > maxNum) maxNum = num;\n }\n }\n }\n\n return maxNum;\n }\n}\n\nexport const storage = BacklogStorage.getInstance();\n"],"mappings":"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"}
1
+ {"version":3,"file":"backlog.mjs","names":[],"sources":["../../src/storage/backlog.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport matter from 'gray-matter';\nimport type { Task, Status, TaskType } from './schema.js';\nimport { paths } from '@/utils/paths.js';\n\nconst TASKS_DIR = 'tasks';\n\nclass BacklogStorage {\n private static instance: BacklogStorage;\n\n static getInstance(): BacklogStorage {\n if (!BacklogStorage.instance) {\n BacklogStorage.instance = new BacklogStorage();\n }\n return BacklogStorage.instance;\n }\n\n private get tasksPath(): string {\n return join(paths.backlogDataDir, TASKS_DIR);\n }\n\n private ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n }\n\n private taskFilePath(id: string): string {\n return join(this.tasksPath, `${id}.md`);\n }\n\n private taskToMarkdown(task: Task): string {\n const { description, ...frontmatter } = task;\n return matter.stringify(description || '', frontmatter);\n }\n\n private markdownToTask(content: string): Task {\n const { data, content: description } = matter(content);\n return { ...data, description: description.trim() } as Task;\n }\n\n getFilePath(id: string): string | null {\n const path = this.taskFilePath(id);\n return existsSync(path) ? path : null;\n }\n\n private *iterateTasks(): Generator<Task> {\n if (existsSync(this.tasksPath)) {\n for (const file of readdirSync(this.tasksPath).filter(f => f.endsWith('.md'))) {\n const filePath = join(this.tasksPath, file);\n try {\n yield this.markdownToTask(readFileSync(filePath, 'utf-8'));\n } catch (error) {\n // Skip files that were deleted between listing and reading\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw error;\n }\n }\n }\n }\n }\n\n get(id: string): Task | undefined {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n return this.markdownToTask(readFileSync(path, 'utf-8'));\n }\n return undefined;\n }\n\n getMarkdown(id: string): string | null {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n return readFileSync(path, 'utf-8');\n }\n return null;\n }\n\n list(filter?: { status?: Status[]; type?: TaskType; epic_id?: string; limit?: number }): Task[] {\n const { status, type, epic_id, limit = 20 } = filter ?? {};\n\n let tasks = Array.from(this.iterateTasks());\n \n if (status) tasks = tasks.filter(t => status.includes(t.status));\n if (type) tasks = tasks.filter(t => (t.type ?? 'task') === type);\n if (epic_id) tasks = tasks.filter(t => t.epic_id === epic_id);\n\n return tasks\n .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())\n .slice(0, limit);\n }\n\n add(task: Task): void {\n this.ensureDir(this.tasksPath);\n const filePath = this.taskFilePath(task.id);\n writeFileSync(filePath, this.taskToMarkdown(task));\n }\n\n save(task: Task): void {\n this.ensureDir(this.tasksPath);\n const filePath = this.taskFilePath(task.id);\n writeFileSync(filePath, this.taskToMarkdown(task));\n }\n\n delete(id: string): boolean {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n unlinkSync(path);\n \n // Delete associated resources if they exist\n const resourcesPath = join(paths.backlogDataDir, 'resources', id);\n if (existsSync(resourcesPath)) {\n rmSync(resourcesPath, { recursive: true, force: true });\n }\n \n return true;\n }\n return false;\n }\n\n counts(): { total_tasks: number; total_epics: number; by_status: Record<Status, number> } {\n const by_status: Record<Status, number> = {\n open: 0,\n in_progress: 0,\n blocked: 0,\n done: 0,\n cancelled: 0,\n };\n\n let total_tasks = 0;\n let total_epics = 0;\n\n for (const task of this.iterateTasks()) {\n by_status[task.status]++;\n if ((task.type ?? 'task') === 'epic') {\n total_epics++;\n } else {\n total_tasks++;\n }\n }\n\n return { total_tasks, total_epics, by_status };\n }\n\n getMaxId(type?: 'task' | 'epic'): number {\n const pattern = type === 'epic' ? /^EPIC-(\\d{4,})\\.md$/ : /^TASK-(\\d{4,})\\.md$/;\n let maxNum = 0;\n\n if (existsSync(this.tasksPath)) {\n for (const file of readdirSync(this.tasksPath)) {\n const match = pattern.exec(file);\n if (match?.[1]) {\n const num = parseInt(match[1], 10);\n if (num > maxNum) maxNum = num;\n }\n }\n }\n\n return maxNum;\n }\n}\n\nexport const storage = BacklogStorage.getInstance();\n"],"mappings":";;;;;;AAMA,MAAM,YAAY;AAElB,IAAM,iBAAN,MAAM,eAAe;CACnB,OAAe;CAEf,OAAO,cAA8B;AACnC,MAAI,CAAC,eAAe,SAClB,gBAAe,WAAW,IAAI,gBAAgB;AAEhD,SAAO,eAAe;;CAGxB,IAAY,YAAoB;AAC9B,SAAO,KAAK,MAAM,gBAAgB,UAAU;;CAG9C,AAAQ,UAAU,KAAmB;AACnC,MAAI,CAAC,WAAW,IAAI,CAClB,WAAU,KAAK,EAAE,WAAW,MAAM,CAAC;;CAIvC,AAAQ,aAAa,IAAoB;AACvC,SAAO,KAAK,KAAK,WAAW,GAAG,GAAG,KAAK;;CAGzC,AAAQ,eAAe,MAAoB;EACzC,MAAM,EAAE,aAAa,GAAG,gBAAgB;AACxC,SAAO,OAAO,UAAU,eAAe,IAAI,YAAY;;CAGzD,AAAQ,eAAe,SAAuB;EAC5C,MAAM,EAAE,MAAM,SAAS,gBAAgB,OAAO,QAAQ;AACtD,SAAO;GAAE,GAAG;GAAM,aAAa,YAAY,MAAM;GAAE;;CAGrD,YAAY,IAA2B;EACrC,MAAM,OAAO,KAAK,aAAa,GAAG;AAClC,SAAO,WAAW,KAAK,GAAG,OAAO;;CAGnC,CAAS,eAAgC;AACvC,MAAI,WAAW,KAAK,UAAU,CAC5B,MAAK,MAAM,QAAQ,YAAY,KAAK,UAAU,CAAC,QAAO,MAAK,EAAE,SAAS,MAAM,CAAC,EAAE;GAC7E,MAAM,WAAW,KAAK,KAAK,WAAW,KAAK;AAC3C,OAAI;AACF,UAAM,KAAK,eAAe,aAAa,UAAU,QAAQ,CAAC;YACnD,OAAO;AAEd,QAAK,MAAgC,SAAS,SAC5C,OAAM;;;;CAOhB,IAAI,IAA8B;EAChC,MAAM,OAAO,KAAK,aAAa,GAAG;AAClC,MAAI,WAAW,KAAK,CAClB,QAAO,KAAK,eAAe,aAAa,MAAM,QAAQ,CAAC;;CAK3D,YAAY,IAA2B;EACrC,MAAM,OAAO,KAAK,aAAa,GAAG;AAClC,MAAI,WAAW,KAAK,CAClB,QAAO,aAAa,MAAM,QAAQ;AAEpC,SAAO;;CAGT,KAAK,QAA2F;EAC9F,MAAM,EAAE,QAAQ,MAAM,SAAS,QAAQ,OAAO,UAAU,EAAE;EAE1D,IAAI,QAAQ,MAAM,KAAK,KAAK,cAAc,CAAC;AAE3C,MAAI,OAAQ,SAAQ,MAAM,QAAO,MAAK,OAAO,SAAS,EAAE,OAAO,CAAC;AAChE,MAAI,KAAM,SAAQ,MAAM,QAAO,OAAM,EAAE,QAAQ,YAAY,KAAK;AAChE,MAAI,QAAS,SAAQ,MAAM,QAAO,MAAK,EAAE,YAAY,QAAQ;AAE7D,SAAO,MACJ,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,WAAW,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,WAAW,CAAC,SAAS,CAAC,CACnF,MAAM,GAAG,MAAM;;CAGpB,IAAI,MAAkB;AACpB,OAAK,UAAU,KAAK,UAAU;AAE9B,gBADiB,KAAK,aAAa,KAAK,GAAG,EACnB,KAAK,eAAe,KAAK,CAAC;;CAGpD,KAAK,MAAkB;AACrB,OAAK,UAAU,KAAK,UAAU;AAE9B,gBADiB,KAAK,aAAa,KAAK,GAAG,EACnB,KAAK,eAAe,KAAK,CAAC;;CAGpD,OAAO,IAAqB;EAC1B,MAAM,OAAO,KAAK,aAAa,GAAG;AAClC,MAAI,WAAW,KAAK,EAAE;AACpB,cAAW,KAAK;GAGhB,MAAM,gBAAgB,KAAK,MAAM,gBAAgB,aAAa,GAAG;AACjE,OAAI,WAAW,cAAc,CAC3B,QAAO,eAAe;IAAE,WAAW;IAAM,OAAO;IAAM,CAAC;AAGzD,UAAO;;AAET,SAAO;;CAGT,SAA0F;EACxF,MAAM,YAAoC;GACxC,MAAM;GACN,aAAa;GACb,SAAS;GACT,MAAM;GACN,WAAW;GACZ;EAED,IAAI,cAAc;EAClB,IAAI,cAAc;AAElB,OAAK,MAAM,QAAQ,KAAK,cAAc,EAAE;AACtC,aAAU,KAAK;AACf,QAAK,KAAK,QAAQ,YAAY,OAC5B;OAEA;;AAIJ,SAAO;GAAE;GAAa;GAAa;GAAW;;CAGhD,SAAS,MAAgC;EACvC,MAAM,UAAU,SAAS,SAAS,wBAAwB;EAC1D,IAAI,SAAS;AAEb,MAAI,WAAW,KAAK,UAAU,CAC5B,MAAK,MAAM,QAAQ,YAAY,KAAK,UAAU,EAAE;GAC9C,MAAM,QAAQ,QAAQ,KAAK,KAAK;AAChC,OAAI,QAAQ,IAAI;IACd,MAAM,MAAM,SAAS,MAAM,IAAI,GAAG;AAClC,QAAI,MAAM,OAAQ,UAAS;;;AAKjC,SAAO;;;AAIX,MAAa,UAAU,eAAe,aAAa"}
@@ -32,6 +32,20 @@ declare class PathResolver {
32
32
  * Get the application version from package.json
33
33
  */
34
34
  getVersion(): string;
35
+ /**
36
+ * Get backlog data directory path.
37
+ *
38
+ * Reads from BACKLOG_DATA_DIR environment variable, defaults to './data'.
39
+ * Relative paths are resolved against project root.
40
+ * Absolute paths (starting with / or ~) are returned as-is.
41
+ *
42
+ * @example
43
+ * // BACKLOG_DATA_DIR not set → '/path/to/project/data'
44
+ * // BACKLOG_DATA_DIR='./my-data' → '/path/to/project/my-data'
45
+ * // BACKLOG_DATA_DIR='/absolute/path' → '/absolute/path'
46
+ * // BACKLOG_DATA_DIR='~/Documents/data' → '~/Documents/data'
47
+ */
48
+ get backlogDataDir(): string;
35
49
  /**
36
50
  * Resolve a path relative to project root
37
51
  * @example paths.fromRoot('data', 'tasks') → '/path/to/package/data/tasks'
@@ -68,5 +82,5 @@ declare class PathResolver {
68
82
  }
69
83
  declare const paths: PathResolver;
70
84
  //#endregion
71
- export { RuntimeEnvironment, paths };
85
+ export { PathResolver, RuntimeEnvironment, paths };
72
86
  //# sourceMappingURL=paths.d.mts.map
@@ -48,6 +48,23 @@ var PathResolver = class PathResolver {
48
48
  return this.packageJson.version;
49
49
  }
50
50
  /**
51
+ * Get backlog data directory path.
52
+ *
53
+ * Reads from BACKLOG_DATA_DIR environment variable, defaults to './data'.
54
+ * Relative paths are resolved against project root.
55
+ * Absolute paths (starting with / or ~) are returned as-is.
56
+ *
57
+ * @example
58
+ * // BACKLOG_DATA_DIR not set → '/path/to/project/data'
59
+ * // BACKLOG_DATA_DIR='./my-data' → '/path/to/project/my-data'
60
+ * // BACKLOG_DATA_DIR='/absolute/path' → '/absolute/path'
61
+ * // BACKLOG_DATA_DIR='~/Documents/data' → '~/Documents/data'
62
+ */
63
+ get backlogDataDir() {
64
+ const dataDir = process.env.BACKLOG_DATA_DIR ?? "data";
65
+ return dataDir.startsWith("/") || dataDir.startsWith("~") ? dataDir : join(this.projectRoot, dataDir);
66
+ }
67
+ /**
51
68
  * Resolve a path relative to project root
52
69
  * @example paths.fromRoot('data', 'tasks') → '/path/to/package/data/tasks'
53
70
  */
@@ -113,5 +130,5 @@ var PathResolver = class PathResolver {
113
130
  const paths = PathResolver.getInstance();
114
131
 
115
132
  //#endregion
116
- export { RuntimeEnvironment, paths };
133
+ export { PathResolver, RuntimeEnvironment, paths };
117
134
  //# sourceMappingURL=paths.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.mjs","names":[],"sources":["../../src/utils/paths.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\nimport { readFileSync } from 'node:fs';\n\n/**\n * Runtime environment modes\n */\nexport enum RuntimeEnvironment {\n Development = 'development',\n Production = 'production',\n}\n\n/**\n * Centralized path resolution for the entire application.\n * All file paths and directory references should go through this singleton.\n */\nclass PathResolver {\n private static instance: PathResolver;\n \n /** Current runtime environment */\n public readonly environment: RuntimeEnvironment;\n \n /** Root directory of the npm package (where package.json lives) */\n public readonly projectRoot: string;\n \n /** Root directory of compiled output (dist/) */\n public readonly distRoot: string;\n \n /** Directory containing built viewer assets (dist/viewer/) */\n public readonly viewerDist: string;\n \n /** Parsed package.json metadata */\n public readonly packageJson: { name: string; version: string; [key: string]: any };\n \n private constructor() {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n \n this.environment = this.detectEnvironment();\n const paths = this.resolvePaths(currentDir, this.environment);\n \n this.projectRoot = paths.projectRoot;\n this.distRoot = paths.distRoot;\n this.viewerDist = paths.viewerDist;\n \n // Load package.json once\n const pkgPath = join(this.projectRoot, 'package.json');\n this.packageJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n }\n \n public static getInstance(): PathResolver {\n if (!PathResolver.instance) {\n PathResolver.instance = new PathResolver();\n }\n return PathResolver.instance;\n }\n \n /**\n * Get the application version from package.json\n */\n public getVersion(): string {\n return this.packageJson.version;\n }\n \n /**\n * Resolve a path relative to project root\n * @example paths.fromRoot('data', 'tasks') → '/path/to/package/data/tasks'\n */\n public fromRoot(...paths: string[]): string {\n return join(this.projectRoot, ...paths);\n }\n \n /**\n * Resolve a path relative to dist/\n * @example paths.fromDist('server', 'index.mjs') → '/path/to/package/dist/server/index.mjs'\n */\n public fromDist(...paths: string[]): string {\n return join(this.distRoot, ...paths);\n }\n \n /**\n * Get path to a binary in node_modules/.bin\n * @example paths.getBinPath('mcp-remote') → '/path/to/package/node_modules/.bin/mcp-remote'\n */\n public getBinPath(binName: string): string {\n return join(this.projectRoot, 'node_modules', '.bin', binName);\n }\n \n /**\n * Detect runtime environment based on NODE_ENV\n * Defaults to production if not set\n */\n private detectEnvironment(): RuntimeEnvironment {\n const env = process.env.NODE_ENV;\n return env === 'development' ? RuntimeEnvironment.Development : RuntimeEnvironment.Production;\n }\n \n /**\n * Resolve all paths based on current directory and environment\n * @param currentDir - Directory where this file is located\n * @param environment - Current runtime environment\n * @returns Object containing all resolved paths\n */\n /**\n * Resolve all directory paths based on current location and environment\n * @param currentDir - Directory containing this file (src/utils or dist/utils)\n * @param environment - Current runtime environment\n * @returns Resolved paths for project root, dist, and viewer\n */\n private resolvePaths(currentDir: string, environment: RuntimeEnvironment): {\n projectRoot: string;\n distRoot: string;\n viewerDist: string;\n } {\n switch (environment) {\n case RuntimeEnvironment.Development: {\n // Dev mode: this file is at src/utils/paths.ts\n // Extract project root from the path\n const srcIndex = currentDir.indexOf('/src/');\n const projectRoot = currentDir.substring(0, srcIndex);\n const distRoot = join(projectRoot, 'dist');\n const viewerDist = join(distRoot, 'viewer');\n return { projectRoot, distRoot, viewerDist };\n }\n \n case RuntimeEnvironment.Production: {\n // Production: this file is at dist/utils/paths.mjs\n // Go up two levels to reach project root\n const distRoot = dirname(currentDir);\n const projectRoot = dirname(distRoot);\n const viewerDist = join(distRoot, 'viewer');\n return { projectRoot, distRoot, viewerDist };\n }\n }\n }\n}\n\n// Export singleton instance\nexport const paths = PathResolver.getInstance();\n"],"mappings":";;;;;;;;AAOA,IAAY,kEAAL;AACL;AACA;;;;;;;AAOF,IAAM,eAAN,MAAM,aAAa;CACjB,OAAe;;CAGf,AAAgB;;CAGhB,AAAgB;;CAGhB,AAAgB;;CAGhB,AAAgB;;CAGhB,AAAgB;CAEhB,AAAQ,cAAc;EAEpB,MAAM,aAAa,QADC,cAAc,OAAO,KAAK,IAAI,CACX;AAEvC,OAAK,cAAc,KAAK,mBAAmB;EAC3C,MAAM,QAAQ,KAAK,aAAa,YAAY,KAAK,YAAY;AAE7D,OAAK,cAAc,MAAM;AACzB,OAAK,WAAW,MAAM;AACtB,OAAK,aAAa,MAAM;EAGxB,MAAM,UAAU,KAAK,KAAK,aAAa,eAAe;AACtD,OAAK,cAAc,KAAK,MAAM,aAAa,SAAS,QAAQ,CAAC;;CAG/D,OAAc,cAA4B;AACxC,MAAI,CAAC,aAAa,SAChB,cAAa,WAAW,IAAI,cAAc;AAE5C,SAAO,aAAa;;;;;CAMtB,AAAO,aAAqB;AAC1B,SAAO,KAAK,YAAY;;;;;;CAO1B,AAAO,SAAS,GAAG,OAAyB;AAC1C,SAAO,KAAK,KAAK,aAAa,GAAG,MAAM;;;;;;CAOzC,AAAO,SAAS,GAAG,OAAyB;AAC1C,SAAO,KAAK,KAAK,UAAU,GAAG,MAAM;;;;;;CAOtC,AAAO,WAAW,SAAyB;AACzC,SAAO,KAAK,KAAK,aAAa,gBAAgB,QAAQ,QAAQ;;;;;;CAOhE,AAAQ,oBAAwC;AAE9C,SADY,QAAQ,IAAI,aACT,gBAAgB,mBAAmB,cAAc,mBAAmB;;;;;;;;;;;;;;CAerF,AAAQ,aAAa,YAAoB,aAIvC;AACA,UAAQ,aAAR;GACE,KAAK,mBAAmB,aAAa;IAGnC,MAAM,WAAW,WAAW,QAAQ,QAAQ;IAC5C,MAAM,cAAc,WAAW,UAAU,GAAG,SAAS;IACrD,MAAM,WAAW,KAAK,aAAa,OAAO;AAE1C,WAAO;KAAE;KAAa;KAAU,YADb,KAAK,UAAU,SAAS;KACC;;GAG9C,KAAK,mBAAmB,YAAY;IAGlC,MAAM,WAAW,QAAQ,WAAW;AAGpC,WAAO;KAAE,aAFW,QAAQ,SAAS;KAEf;KAAU,YADb,KAAK,UAAU,SAAS;KACC;;;;;AAOpD,MAAa,QAAQ,aAAa,aAAa"}
1
+ {"version":3,"file":"paths.mjs","names":[],"sources":["../../src/utils/paths.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\nimport { readFileSync } from 'node:fs';\n\n/**\n * Runtime environment modes\n */\nexport enum RuntimeEnvironment {\n Development = 'development',\n Production = 'production',\n}\n\n/**\n * Centralized path resolution for the entire application.\n * All file paths and directory references should go through this singleton.\n */\nexport class PathResolver {\n private static instance: PathResolver;\n \n /** Current runtime environment */\n public readonly environment: RuntimeEnvironment;\n \n /** Root directory of the npm package (where package.json lives) */\n public readonly projectRoot: string;\n \n /** Root directory of compiled output (dist/) */\n public readonly distRoot: string;\n \n /** Directory containing built viewer assets (dist/viewer/) */\n public readonly viewerDist: string;\n \n /** Parsed package.json metadata */\n public readonly packageJson: { name: string; version: string; [key: string]: any };\n \n private constructor() {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n \n this.environment = this.detectEnvironment();\n const paths = this.resolvePaths(currentDir, this.environment);\n \n this.projectRoot = paths.projectRoot;\n this.distRoot = paths.distRoot;\n this.viewerDist = paths.viewerDist;\n \n // Load package.json once\n const pkgPath = join(this.projectRoot, 'package.json');\n this.packageJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n }\n \n public static getInstance(): PathResolver {\n if (!PathResolver.instance) {\n PathResolver.instance = new PathResolver();\n }\n return PathResolver.instance;\n }\n \n /**\n * Get the application version from package.json\n */\n public getVersion(): string {\n return this.packageJson.version;\n }\n \n /**\n * Get backlog data directory path.\n * \n * Reads from BACKLOG_DATA_DIR environment variable, defaults to './data'.\n * Relative paths are resolved against project root.\n * Absolute paths (starting with / or ~) are returned as-is.\n * \n * @example\n * // BACKLOG_DATA_DIR not set → '/path/to/project/data'\n * // BACKLOG_DATA_DIR='./my-data' → '/path/to/project/my-data'\n * // BACKLOG_DATA_DIR='/absolute/path' → '/absolute/path'\n * // BACKLOG_DATA_DIR='~/Documents/data' → '~/Documents/data'\n */\n public get backlogDataDir(): string {\n const dataDir = process.env.BACKLOG_DATA_DIR ?? 'data';\n const isAbsolutePath = dataDir.startsWith('/') || dataDir.startsWith('~');\n \n return isAbsolutePath ? dataDir : join(this.projectRoot, dataDir);\n }\n \n /**\n * Resolve a path relative to project root\n * @example paths.fromRoot('data', 'tasks') → '/path/to/package/data/tasks'\n */\n public fromRoot(...paths: string[]): string {\n return join(this.projectRoot, ...paths);\n }\n \n /**\n * Resolve a path relative to dist/\n * @example paths.fromDist('server', 'index.mjs') → '/path/to/package/dist/server/index.mjs'\n */\n public fromDist(...paths: string[]): string {\n return join(this.distRoot, ...paths);\n }\n \n /**\n * Get path to a binary in node_modules/.bin\n * @example paths.getBinPath('mcp-remote') → '/path/to/package/node_modules/.bin/mcp-remote'\n */\n public getBinPath(binName: string): string {\n return join(this.projectRoot, 'node_modules', '.bin', binName);\n }\n \n /**\n * Detect runtime environment based on NODE_ENV\n * Defaults to production if not set\n */\n private detectEnvironment(): RuntimeEnvironment {\n const env = process.env.NODE_ENV;\n return env === 'development' ? RuntimeEnvironment.Development : RuntimeEnvironment.Production;\n }\n \n /**\n * Resolve all paths based on current directory and environment\n * @param currentDir - Directory where this file is located\n * @param environment - Current runtime environment\n * @returns Object containing all resolved paths\n */\n /**\n * Resolve all directory paths based on current location and environment\n * @param currentDir - Directory containing this file (src/utils or dist/utils)\n * @param environment - Current runtime environment\n * @returns Resolved paths for project root, dist, and viewer\n */\n private resolvePaths(currentDir: string, environment: RuntimeEnvironment): {\n projectRoot: string;\n distRoot: string;\n viewerDist: string;\n } {\n switch (environment) {\n case RuntimeEnvironment.Development: {\n // Dev mode: this file is at src/utils/paths.ts\n // Extract project root from the path\n const srcIndex = currentDir.indexOf('/src/');\n const projectRoot = currentDir.substring(0, srcIndex);\n const distRoot = join(projectRoot, 'dist');\n const viewerDist = join(distRoot, 'viewer');\n return { projectRoot, distRoot, viewerDist };\n }\n \n case RuntimeEnvironment.Production: {\n // Production: this file is at dist/utils/paths.mjs\n // Go up two levels to reach project root\n const distRoot = dirname(currentDir);\n const projectRoot = dirname(distRoot);\n const viewerDist = join(distRoot, 'viewer');\n return { projectRoot, distRoot, viewerDist };\n }\n }\n }\n}\n\n// Export singleton instance\nexport const paths = PathResolver.getInstance();\n"],"mappings":";;;;;;;;AAOA,IAAY,kEAAL;AACL;AACA;;;;;;;AAOF,IAAa,eAAb,MAAa,aAAa;CACxB,OAAe;;CAGf,AAAgB;;CAGhB,AAAgB;;CAGhB,AAAgB;;CAGhB,AAAgB;;CAGhB,AAAgB;CAEhB,AAAQ,cAAc;EAEpB,MAAM,aAAa,QADC,cAAc,OAAO,KAAK,IAAI,CACX;AAEvC,OAAK,cAAc,KAAK,mBAAmB;EAC3C,MAAM,QAAQ,KAAK,aAAa,YAAY,KAAK,YAAY;AAE7D,OAAK,cAAc,MAAM;AACzB,OAAK,WAAW,MAAM;AACtB,OAAK,aAAa,MAAM;EAGxB,MAAM,UAAU,KAAK,KAAK,aAAa,eAAe;AACtD,OAAK,cAAc,KAAK,MAAM,aAAa,SAAS,QAAQ,CAAC;;CAG/D,OAAc,cAA4B;AACxC,MAAI,CAAC,aAAa,SAChB,cAAa,WAAW,IAAI,cAAc;AAE5C,SAAO,aAAa;;;;;CAMtB,AAAO,aAAqB;AAC1B,SAAO,KAAK,YAAY;;;;;;;;;;;;;;;CAgB1B,IAAW,iBAAyB;EAClC,MAAM,UAAU,QAAQ,IAAI,oBAAoB;AAGhD,SAFuB,QAAQ,WAAW,IAAI,IAAI,QAAQ,WAAW,IAAI,GAEjD,UAAU,KAAK,KAAK,aAAa,QAAQ;;;;;;CAOnE,AAAO,SAAS,GAAG,OAAyB;AAC1C,SAAO,KAAK,KAAK,aAAa,GAAG,MAAM;;;;;;CAOzC,AAAO,SAAS,GAAG,OAAyB;AAC1C,SAAO,KAAK,KAAK,UAAU,GAAG,MAAM;;;;;;CAOtC,AAAO,WAAW,SAAyB;AACzC,SAAO,KAAK,KAAK,aAAa,gBAAgB,QAAQ,QAAQ;;;;;;CAOhE,AAAQ,oBAAwC;AAE9C,SADY,QAAQ,IAAI,aACT,gBAAgB,mBAAmB,cAAc,mBAAmB;;;;;;;;;;;;;;CAerF,AAAQ,aAAa,YAAoB,aAIvC;AACA,UAAQ,aAAR;GACE,KAAK,mBAAmB,aAAa;IAGnC,MAAM,WAAW,WAAW,QAAQ,QAAQ;IAC5C,MAAM,cAAc,WAAW,UAAU,GAAG,SAAS;IACrD,MAAM,WAAW,KAAK,aAAa,OAAO;AAE1C,WAAO;KAAE;KAAa;KAAU,YADb,KAAK,UAAU,SAAS;KACC;;GAG9C,KAAK,mBAAmB,YAAY;IAGlC,MAAM,WAAW,QAAQ,WAAW;AAGpC,WAAO;KAAE,aAFW,QAAQ,SAAS;KAEf;KAAU,YADb,KAAK,UAAU,SAAS;KACC;;;;;AAOpD,MAAa,QAAQ,aAAa,aAAa"}
@@ -1,8 +1,7 @@
1
1
  //#region src/utils/uri-resolver.d.ts
2
2
  declare function getRepoRoot(): string;
3
- declare function getBacklogDataDir(): string;
4
3
  declare function resolveMcpUri(uri: string): string;
5
4
  declare function filePathToMcpUri(filePath: string): string | null;
6
5
  //#endregion
7
- export { filePathToMcpUri, getBacklogDataDir, getRepoRoot, resolveMcpUri };
6
+ export { filePathToMcpUri, getRepoRoot, resolveMcpUri };
8
7
  //# sourceMappingURL=uri-resolver.d.mts.map