backlog-mcp 0.27.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bridge.mjs +43 -8
- package/dist/cli/bridge.mjs.map +1 -1
- package/dist/cli/supervisor.d.mts +38 -0
- package/dist/cli/supervisor.mjs +54 -0
- package/dist/cli/supervisor.mjs.map +1 -0
- package/dist/resources/manager.mjs +34 -31
- package/dist/resources/manager.mjs.map +1 -1
- package/dist/resources/operations.mjs +7 -7
- package/dist/resources/operations.mjs.map +1 -1
- package/dist/resources/types.d.mts +12 -16
- package/dist/server/fastify-server.mjs +7 -0
- package/dist/server/fastify-server.mjs.map +1 -1
- package/dist/storage/backlog.mjs +9 -1
- package/dist/storage/backlog.mjs.map +1 -1
- package/dist/tools/backlog-create.mjs +1 -1
- package/dist/tools/backlog-create.mjs.map +1 -1
- package/dist/tools/backlog-update.mjs +2 -2
- package/dist/tools/backlog-update.mjs.map +1 -1
- package/dist/utils/logger.d.mts +10 -0
- package/dist/utils/logger.mjs +49 -0
- package/dist/utils/logger.mjs.map +1 -0
- package/package.json +1 -1
package/dist/cli/bridge.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { paths } from "../utils/paths.mjs";
|
|
3
|
+
import { logger } from "../utils/logger.mjs";
|
|
3
4
|
import { ensureServer } from "./server-manager.mjs";
|
|
5
|
+
import { DEFAULT_CONFIG, Supervisor } from "./supervisor.mjs";
|
|
4
6
|
import { existsSync } from "node:fs";
|
|
5
7
|
import { spawn } from "node:child_process";
|
|
6
8
|
|
|
@@ -10,18 +12,51 @@ async function runBridge(port) {
|
|
|
10
12
|
const serverUrl = `http://localhost:${port}/mcp`;
|
|
11
13
|
const mcpRemotePath = paths.getBinPath("mcp-remote");
|
|
12
14
|
if (!existsSync(mcpRemotePath)) {
|
|
13
|
-
|
|
15
|
+
logger.error("mcp-remote not found", { path: mcpRemotePath });
|
|
14
16
|
process.exit(1);
|
|
15
17
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const supervisor = new Supervisor(DEFAULT_CONFIG);
|
|
19
|
+
const spawnBridge = () => {
|
|
20
|
+
supervisor.onStart();
|
|
21
|
+
const bridge = spawn(mcpRemotePath, [
|
|
22
|
+
serverUrl,
|
|
23
|
+
"--allow-http",
|
|
24
|
+
"--transport",
|
|
25
|
+
"http-only"
|
|
26
|
+
], { stdio: [
|
|
27
|
+
"inherit",
|
|
28
|
+
"inherit",
|
|
29
|
+
"pipe"
|
|
30
|
+
] });
|
|
31
|
+
let connectionLost = false;
|
|
32
|
+
bridge.stderr?.on("data", (data) => {
|
|
33
|
+
const msg = data.toString();
|
|
34
|
+
process.stderr.write(msg);
|
|
35
|
+
if (!connectionLost && (msg.includes("ECONNREFUSED") || msg.includes("fetch failed"))) {
|
|
36
|
+
connectionLost = true;
|
|
37
|
+
logger.warn("mcp-remote lost connection, restarting");
|
|
38
|
+
bridge.kill();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
bridge.on("exit", async (code) => {
|
|
42
|
+
const result = supervisor.onExit(connectionLost ? 1 : code);
|
|
43
|
+
if (result.action === "stop") process.exit(0);
|
|
44
|
+
if (result.action === "give-up") {
|
|
45
|
+
logger.error("mcp-remote crashed too many times", { restarts: result.restartCount });
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
logger.warn("mcp-remote restarting", {
|
|
49
|
+
delay: result.delay,
|
|
50
|
+
attempt: result.restartCount
|
|
51
|
+
});
|
|
52
|
+
await ensureServer(port).catch(() => {});
|
|
53
|
+
setTimeout(spawnBridge, result.delay);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
spawnBridge();
|
|
22
57
|
}
|
|
23
58
|
runBridge(parseInt(process.env.BACKLOG_VIEWER_PORT || "3030")).catch((error) => {
|
|
24
|
-
|
|
59
|
+
logger.error("Bridge error", { error: String(error) });
|
|
25
60
|
process.exit(1);
|
|
26
61
|
});
|
|
27
62
|
|
package/dist/cli/bridge.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.mjs","names":[],"sources":["../../src/cli/bridge.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { ensureServer } from './server-manager.js';\nimport { paths } from '@/utils/paths.js';\n\nasync function runBridge(port: number): Promise<void> {\n await ensureServer(port);\n \n const serverUrl = `http://localhost:${port}/mcp`;\n const mcpRemotePath = paths.getBinPath('mcp-remote');\n \n if (!existsSync(mcpRemotePath)) {\n
|
|
1
|
+
{"version":3,"file":"bridge.mjs","names":[],"sources":["../../src/cli/bridge.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { spawn, ChildProcess } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { ensureServer } from './server-manager.js';\nimport { Supervisor, DEFAULT_CONFIG } from './supervisor.js';\nimport { paths } from '@/utils/paths.js';\nimport { logger } from '@/utils/logger.js';\n\nasync function runBridge(port: number): Promise<void> {\n await ensureServer(port);\n \n const serverUrl = `http://localhost:${port}/mcp`;\n const mcpRemotePath = paths.getBinPath('mcp-remote');\n \n if (!existsSync(mcpRemotePath)) {\n logger.error('mcp-remote not found', { path: mcpRemotePath });\n process.exit(1);\n }\n \n const supervisor = new Supervisor(DEFAULT_CONFIG);\n \n const spawnBridge = () => {\n supervisor.onStart();\n \n const bridge = spawn(mcpRemotePath, [serverUrl, '--allow-http', '--transport', 'http-only'], {\n stdio: ['inherit', 'inherit', 'pipe']\n });\n \n let connectionLost = false;\n \n bridge.stderr?.on('data', (data: Buffer) => {\n const msg = data.toString();\n process.stderr.write(msg);\n \n // mcp-remote hangs on connection errors instead of exiting - detect and kill\n if (!connectionLost && (msg.includes('ECONNREFUSED') || msg.includes('fetch failed'))) {\n connectionLost = true;\n logger.warn('mcp-remote lost connection, restarting');\n bridge.kill();\n }\n });\n \n bridge.on('exit', async (code) => {\n const result = supervisor.onExit(connectionLost ? 1 : code);\n \n if (result.action === 'stop') {\n process.exit(0);\n }\n \n if (result.action === 'give-up') {\n logger.error('mcp-remote crashed too many times', { restarts: result.restartCount });\n process.exit(1);\n }\n \n logger.warn('mcp-remote restarting', { delay: result.delay, attempt: result.restartCount });\n \n await ensureServer(port).catch(() => {});\n setTimeout(spawnBridge, result.delay);\n });\n };\n \n spawnBridge();\n}\n\nconst port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\nrunBridge(port).catch((error) => {\n logger.error('Bridge error', { error: String(error) });\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;AASA,eAAe,UAAU,MAA6B;AACpD,OAAM,aAAa,KAAK;CAExB,MAAM,YAAY,oBAAoB,KAAK;CAC3C,MAAM,gBAAgB,MAAM,WAAW,aAAa;AAEpD,KAAI,CAAC,WAAW,cAAc,EAAE;AAC9B,SAAO,MAAM,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAC7D,UAAQ,KAAK,EAAE;;CAGjB,MAAM,aAAa,IAAI,WAAW,eAAe;CAEjD,MAAM,oBAAoB;AACxB,aAAW,SAAS;EAEpB,MAAM,SAAS,MAAM,eAAe;GAAC;GAAW;GAAgB;GAAe;GAAY,EAAE,EAC3F,OAAO;GAAC;GAAW;GAAW;GAAO,EACtC,CAAC;EAEF,IAAI,iBAAiB;AAErB,SAAO,QAAQ,GAAG,SAAS,SAAiB;GAC1C,MAAM,MAAM,KAAK,UAAU;AAC3B,WAAQ,OAAO,MAAM,IAAI;AAGzB,OAAI,CAAC,mBAAmB,IAAI,SAAS,eAAe,IAAI,IAAI,SAAS,eAAe,GAAG;AACrF,qBAAiB;AACjB,WAAO,KAAK,yCAAyC;AACrD,WAAO,MAAM;;IAEf;AAEF,SAAO,GAAG,QAAQ,OAAO,SAAS;GAChC,MAAM,SAAS,WAAW,OAAO,iBAAiB,IAAI,KAAK;AAE3D,OAAI,OAAO,WAAW,OACpB,SAAQ,KAAK,EAAE;AAGjB,OAAI,OAAO,WAAW,WAAW;AAC/B,WAAO,MAAM,qCAAqC,EAAE,UAAU,OAAO,cAAc,CAAC;AACpF,YAAQ,KAAK,EAAE;;AAGjB,UAAO,KAAK,yBAAyB;IAAE,OAAO,OAAO;IAAO,SAAS,OAAO;IAAc,CAAC;AAE3F,SAAM,aAAa,KAAK,CAAC,YAAY,GAAG;AACxC,cAAW,aAAa,OAAO,MAAM;IACrC;;AAGJ,cAAa;;AAIf,UADa,SAAS,QAAQ,IAAI,uBAAuB,OAAO,CACjD,CAAC,OAAO,UAAU;AAC/B,QAAO,MAAM,gBAAgB,EAAE,OAAO,OAAO,MAAM,EAAE,CAAC;AACtD,SAAQ,KAAK,EAAE;EACf"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/cli/supervisor.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Supervisor manages restart logic with exponential backoff.
|
|
4
|
+
* Extracted for testability - no child process or I/O dependencies.
|
|
5
|
+
*/
|
|
6
|
+
interface SupervisorConfig {
|
|
7
|
+
maxRestarts: number;
|
|
8
|
+
initialDelayMs: number;
|
|
9
|
+
maxDelayMs: number;
|
|
10
|
+
successThresholdMs: number;
|
|
11
|
+
}
|
|
12
|
+
declare const DEFAULT_CONFIG: SupervisorConfig;
|
|
13
|
+
declare class Supervisor {
|
|
14
|
+
private config;
|
|
15
|
+
private restartCount;
|
|
16
|
+
private delay;
|
|
17
|
+
private startTime;
|
|
18
|
+
constructor(config?: SupervisorConfig);
|
|
19
|
+
/** Call when process starts */
|
|
20
|
+
onStart(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Call when process exits. Returns action to take.
|
|
23
|
+
* @param code - exit code (0 = normal, null = signal, other = crash)
|
|
24
|
+
*/
|
|
25
|
+
onExit(code: number | null): {
|
|
26
|
+
action: 'stop' | 'restart' | 'give-up';
|
|
27
|
+
delay?: number;
|
|
28
|
+
restartCount?: number;
|
|
29
|
+
};
|
|
30
|
+
/** Get current state for testing/logging */
|
|
31
|
+
getState(): {
|
|
32
|
+
restartCount: number;
|
|
33
|
+
delay: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { DEFAULT_CONFIG, Supervisor, SupervisorConfig };
|
|
38
|
+
//# sourceMappingURL=supervisor.d.mts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/cli/supervisor.ts
|
|
2
|
+
const DEFAULT_CONFIG = {
|
|
3
|
+
maxRestarts: 10,
|
|
4
|
+
initialDelayMs: 1e3,
|
|
5
|
+
maxDelayMs: 3e4,
|
|
6
|
+
successThresholdMs: 3e4
|
|
7
|
+
};
|
|
8
|
+
var Supervisor = class {
|
|
9
|
+
restartCount = 0;
|
|
10
|
+
delay;
|
|
11
|
+
startTime = 0;
|
|
12
|
+
constructor(config = DEFAULT_CONFIG) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.delay = config.initialDelayMs;
|
|
15
|
+
}
|
|
16
|
+
/** Call when process starts */
|
|
17
|
+
onStart() {
|
|
18
|
+
this.startTime = Date.now();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Call when process exits. Returns action to take.
|
|
22
|
+
* @param code - exit code (0 = normal, null = signal, other = crash)
|
|
23
|
+
*/
|
|
24
|
+
onExit(code) {
|
|
25
|
+
if (code === 0 || code === null) return { action: "stop" };
|
|
26
|
+
if (Date.now() - this.startTime > this.config.successThresholdMs) {
|
|
27
|
+
this.restartCount = 0;
|
|
28
|
+
this.delay = this.config.initialDelayMs;
|
|
29
|
+
}
|
|
30
|
+
this.restartCount++;
|
|
31
|
+
if (this.restartCount > this.config.maxRestarts) return {
|
|
32
|
+
action: "give-up",
|
|
33
|
+
restartCount: this.restartCount
|
|
34
|
+
};
|
|
35
|
+
const currentDelay = this.delay;
|
|
36
|
+
this.delay = Math.min(this.delay * 2, this.config.maxDelayMs);
|
|
37
|
+
return {
|
|
38
|
+
action: "restart",
|
|
39
|
+
delay: currentDelay,
|
|
40
|
+
restartCount: this.restartCount
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Get current state for testing/logging */
|
|
44
|
+
getState() {
|
|
45
|
+
return {
|
|
46
|
+
restartCount: this.restartCount,
|
|
47
|
+
delay: this.delay
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
export { DEFAULT_CONFIG, Supervisor };
|
|
54
|
+
//# sourceMappingURL=supervisor.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.mjs","names":[],"sources":["../../src/cli/supervisor.ts"],"sourcesContent":["/**\n * Supervisor manages restart logic with exponential backoff.\n * Extracted for testability - no child process or I/O dependencies.\n */\nexport interface SupervisorConfig {\n maxRestarts: number;\n initialDelayMs: number;\n maxDelayMs: number;\n successThresholdMs: number;\n}\n\nexport const DEFAULT_CONFIG: SupervisorConfig = {\n maxRestarts: 10,\n initialDelayMs: 1000,\n maxDelayMs: 30000,\n successThresholdMs: 30000,\n};\n\nexport class Supervisor {\n private restartCount = 0;\n private delay: number;\n private startTime = 0;\n \n constructor(private config: SupervisorConfig = DEFAULT_CONFIG) {\n this.delay = config.initialDelayMs;\n }\n \n /** Call when process starts */\n onStart(): void {\n this.startTime = Date.now();\n }\n \n /** \n * Call when process exits. Returns action to take.\n * @param code - exit code (0 = normal, null = signal, other = crash)\n */\n onExit(code: number | null): { action: 'stop' | 'restart' | 'give-up'; delay?: number; restartCount?: number } {\n // Normal exit or signal - stop\n if (code === 0 || code === null) {\n return { action: 'stop' };\n }\n \n // Reset if ran successfully for a while\n const runDuration = Date.now() - this.startTime;\n if (runDuration > this.config.successThresholdMs) {\n this.restartCount = 0;\n this.delay = this.config.initialDelayMs;\n }\n \n this.restartCount++;\n \n if (this.restartCount > this.config.maxRestarts) {\n return { action: 'give-up', restartCount: this.restartCount };\n }\n \n const currentDelay = this.delay;\n this.delay = Math.min(this.delay * 2, this.config.maxDelayMs);\n \n return { action: 'restart', delay: currentDelay, restartCount: this.restartCount };\n }\n \n /** Get current state for testing/logging */\n getState(): { restartCount: number; delay: number } {\n return { restartCount: this.restartCount, delay: this.delay };\n }\n}\n"],"mappings":";AAWA,MAAa,iBAAmC;CAC9C,aAAa;CACb,gBAAgB;CAChB,YAAY;CACZ,oBAAoB;CACrB;AAED,IAAa,aAAb,MAAwB;CACtB,AAAQ,eAAe;CACvB,AAAQ;CACR,AAAQ,YAAY;CAEpB,YAAY,AAAQ,SAA2B,gBAAgB;EAA3C;AAClB,OAAK,QAAQ,OAAO;;;CAItB,UAAgB;AACd,OAAK,YAAY,KAAK,KAAK;;;;;;CAO7B,OAAO,MAAwG;AAE7G,MAAI,SAAS,KAAK,SAAS,KACzB,QAAO,EAAE,QAAQ,QAAQ;AAK3B,MADoB,KAAK,KAAK,GAAG,KAAK,YACpB,KAAK,OAAO,oBAAoB;AAChD,QAAK,eAAe;AACpB,QAAK,QAAQ,KAAK,OAAO;;AAG3B,OAAK;AAEL,MAAI,KAAK,eAAe,KAAK,OAAO,YAClC,QAAO;GAAE,QAAQ;GAAW,cAAc,KAAK;GAAc;EAG/D,MAAM,eAAe,KAAK;AAC1B,OAAK,QAAQ,KAAK,IAAI,KAAK,QAAQ,GAAG,KAAK,OAAO,WAAW;AAE7D,SAAO;GAAE,QAAQ;GAAW,OAAO;GAAc,cAAc,KAAK;GAAc;;;CAIpF,WAAoD;AAClD,SAAO;GAAE,cAAc,KAAK;GAAc,OAAO,KAAK;GAAO"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { paths } from "../utils/paths.mjs";
|
|
2
2
|
import { applyOperation } from "./operations.mjs";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
import matter from "gray-matter";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -74,18 +74,19 @@ var ResourceManager = class {
|
|
|
74
74
|
write(uri, operation) {
|
|
75
75
|
try {
|
|
76
76
|
const filePath = this.resolve(uri);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
const canCreate = [
|
|
78
|
+
"create",
|
|
79
|
+
"append",
|
|
80
|
+
"insert"
|
|
81
|
+
].includes(operation.type);
|
|
82
|
+
if (!existsSync(filePath)) if (canCreate) {
|
|
83
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
84
|
+
writeFileSync(filePath, "", "utf-8");
|
|
85
|
+
} else return {
|
|
86
|
+
success: false,
|
|
87
|
+
message: "File not found",
|
|
88
|
+
error: `Resource not found: ${uri} (${operation.type} requires existing file)`
|
|
89
|
+
};
|
|
89
90
|
writeFileSync(filePath, applyOperation(readFileSync(filePath, "utf-8"), operation), "utf-8");
|
|
90
91
|
return {
|
|
91
92
|
success: true,
|
|
@@ -129,33 +130,35 @@ var ResourceManager = class {
|
|
|
129
130
|
*/
|
|
130
131
|
registerWriteTool(server) {
|
|
131
132
|
server.registerTool("write_resource", {
|
|
132
|
-
description:
|
|
133
|
+
description: `A tool for creating and editing files on the MCP server
|
|
134
|
+
* The \`create\` command will override the file at \`uri\` if it already exists as a file, and otherwise create a new file
|
|
135
|
+
* The \`append\` command will add content to the end of a file, automatically adding a newline if the file doesn't end with one. Creates the file if it doesn't exist.
|
|
136
|
+
Notes for using the \`str_replace\` command:
|
|
137
|
+
* The \`old_str\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
|
|
138
|
+
* If the \`old_str\` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in \`old_str\` to make it unique
|
|
139
|
+
* The \`new_str\` parameter should contain the edited lines that should replace the \`old_str\``,
|
|
133
140
|
inputSchema: z.object({
|
|
134
|
-
uri: z.string().describe("MCP URI
|
|
135
|
-
operation: z.discriminatedUnion("type", [
|
|
136
|
-
z.object({
|
|
137
|
-
type: z.literal("str_replace"),
|
|
138
|
-
old_str: z.string().describe("String to find and replace"),
|
|
139
|
-
new_str: z.string().describe("Replacement string")
|
|
140
|
-
}),
|
|
141
|
+
uri: z.string().describe("MCP resource URI, e.g. mcp://backlog/path/to/file.md"),
|
|
142
|
+
operation: z.preprocess((val) => typeof val === "string" ? JSON.parse(val) : val, z.discriminatedUnion("type", [
|
|
141
143
|
z.object({
|
|
142
|
-
type: z.literal("
|
|
143
|
-
|
|
144
|
+
type: z.literal("create"),
|
|
145
|
+
file_text: z.string().describe("Content of the file to be created")
|
|
144
146
|
}),
|
|
145
147
|
z.object({
|
|
146
|
-
type: z.literal("
|
|
147
|
-
|
|
148
|
+
type: z.literal("str_replace"),
|
|
149
|
+
old_str: z.string().describe("String in file to replace (must match exactly)"),
|
|
150
|
+
new_str: z.string().describe("New string to replace old_str with")
|
|
148
151
|
}),
|
|
149
152
|
z.object({
|
|
150
153
|
type: z.literal("insert"),
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
insert_line: z.number().describe("Line number after which new_str will be inserted"),
|
|
155
|
+
new_str: z.string().describe("String to insert")
|
|
153
156
|
}),
|
|
154
157
|
z.object({
|
|
155
|
-
type: z.literal("
|
|
156
|
-
|
|
158
|
+
type: z.literal("append"),
|
|
159
|
+
new_str: z.string().describe("Content to append to the file")
|
|
157
160
|
})
|
|
158
|
-
]).describe("Operation to apply")
|
|
161
|
+
])).describe("Operation to apply")
|
|
159
162
|
})
|
|
160
163
|
}, async ({ uri, operation }) => {
|
|
161
164
|
const result = this.write(uri, operation);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.mjs","names":[],"sources":["../../src/resources/manager.ts"],"sourcesContent":["import { readFileSync, existsSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport matter from 'gray-matter';\nimport { z } from 'zod';\nimport { paths } from '@/utils/paths.js';\nimport { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Operation, WriteResourceResult } from './types.js';\nimport { applyOperation } from './operations.js';\n\nexport interface ResourceContent {\n content: string;\n frontmatter?: Record<string, any>;\n mimeType: string;\n}\n\n/**\n * ResourceManager - Single point of responsibility for MCP resource operations.\n * \n * Pure catch-all design: mcp://backlog/{+path} → {dataDir}/{path}\n * No special cases, no magic behavior.\n */\nexport class ResourceManager {\n constructor(private readonly dataDir: string) {}\n\n /**\n * Resolve MCP URI to absolute file path.\n * Pure catch-all: mcp://backlog/path/file.md → {dataDir}/path/file.md\n * \n * @param uri MCP URI (must start with mcp://backlog/)\n * @returns Absolute file path\n * @throws Error if URI is invalid or contains path traversal\n */\n resolve(uri: string): string {\n if (!uri.startsWith('mcp://')) {\n throw new Error(`Not an MCP URI: ${uri}`);\n }\n\n // Check for path traversal BEFORE URL parsing (URL normalizes ..)\n if (uri.includes('..')) {\n throw new Error(`Path traversal not allowed: ${uri}`);\n }\n\n const url = new URL(uri);\n \n if (url.hostname !== 'backlog') {\n throw new Error(`Invalid hostname: ${url.hostname}. Expected 'backlog'`);\n }\n \n const path = url.pathname.substring(1); // Remove leading /\n \n return join(this.dataDir, path);\n }\n\n /**\n * Read resource content from MCP URI.\n * Parses frontmatter for markdown files and detects MIME type.\n * \n * @param uri MCP URI\n * @returns Resource content with frontmatter and MIME type\n * @throws Error if file not found\n */\n read(uri: string): ResourceContent {\n const filePath = this.resolve(uri);\n \n if (!existsSync(filePath)) {\n // Helpful error for common mistake: extension-less task URIs\n if (/^mcp:\\/\\/backlog\\/tasks\\/(TASK|EPIC)-\\d+$/.test(uri)) {\n throw new Error(\n `Task URIs must include .md extension. Did you mean: ${uri}.md?`\n );\n }\n throw new Error(`Resource not found: ${uri} (resolved to ${filePath})`);\n }\n \n const content = readFileSync(filePath, 'utf-8');\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n const mimeType = this.getMimeType(ext);\n \n // Parse frontmatter for markdown files\n if (ext === 'md') {\n const parsed = matter(content);\n return {\n content: parsed.content,\n frontmatter: Object.keys(parsed.data).length > 0 ? parsed.data : undefined,\n mimeType,\n };\n }\n \n return {\n content,\n mimeType,\n };\n }\n\n /**\n * Write/modify resource content.\n * Applies operations like str_replace, append, insert, etc.\n * \n * @param uri MCP URI\n * @param operation Operation to apply\n * @returns Result with success status and message\n */\n write(uri: string, operation: Operation): WriteResourceResult {\n try {\n const filePath = this.resolve(uri);\n \n if (!existsSync(filePath)) {\n // Helpful error for common mistake: extension-less task URIs\n if (/^mcp:\\/\\/backlog\\/tasks\\/(TASK|EPIC)-\\d+$/.test(uri)) {\n return {\n success: false,\n message: 'Task URIs must include .md extension',\n error: `Did you mean: ${uri}.md?`,\n };\n }\n return {\n success: false,\n message: 'File not found',\n error: `Resource not found: ${uri}`,\n };\n }\n\n const fileContent = readFileSync(filePath, 'utf-8');\n const newContent = applyOperation(fileContent, operation);\n writeFileSync(filePath, newContent, 'utf-8');\n\n return {\n success: true,\n message: `Successfully applied ${operation.type} to ${uri}`,\n };\n } catch (error) {\n return {\n success: false,\n message: 'Operation failed',\n error: error instanceof Error ? error.message : String(error),\n };\n }\n }\n\n /**\n * Convert file path to MCP URI.\n * Pure mapping: {dataDir}/path/file.md → mcp://backlog/path/file.md\n * \n * @param filePath Absolute file path\n * @returns MCP URI or null if file is outside data directory\n */\n toUri(filePath: string): string | null {\n if (!filePath.startsWith(this.dataDir)) {\n return null;\n }\n \n const relativePath = filePath.substring(this.dataDir.length + 1);\n return `mcp://backlog/${relativePath}`;\n }\n\n /**\n * Register MCP resource handler (catch-all pattern).\n */\n registerResource(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) => {\n const resource = this.read(uri.toString());\n return { \n contents: [{ \n uri: uri.toString(), \n mimeType: resource.mimeType, \n text: resource.content \n }] \n };\n }\n );\n }\n\n /**\n * Register write_resource MCP tool.\n */\n registerWriteTool(server: McpServer) {\n server.registerTool(\n 'write_resource',\n {\n description: 'Write/modify resource content with operations like str_replace, append, insert',\n inputSchema: z.object({\n uri: z.string().describe('MCP URI (mcp://backlog/path/file.md)'),\n operation: z.discriminatedUnion('type', [\n z.object({\n type: z.literal('str_replace'),\n old_str: z.string().describe('String to find and replace'),\n new_str: z.string().describe('Replacement string'),\n }),\n z.object({\n type: z.literal('append'),\n content: z.string().describe('Content to append'),\n }),\n z.object({\n type: z.literal('prepend'),\n content: z.string().describe('Content to prepend'),\n }),\n z.object({\n type: z.literal('insert'),\n line: z.number().describe('Line number to insert at (0-based)'),\n content: z.string().describe('Content to insert'),\n }),\n z.object({\n type: z.literal('delete'),\n content: z.string().describe('Content to delete'),\n }),\n ]).describe('Operation to apply'),\n }),\n },\n async ({ uri, operation }) => {\n const result = this.write(uri, operation);\n return {\n content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],\n };\n }\n );\n }\n\n private getMimeType(ext: string): string {\n const mimeMap: Record<string, string> = {\n md: 'text/markdown',\n json: 'application/json',\n ts: 'text/typescript',\n js: 'application/javascript',\n txt: 'text/plain',\n };\n \n return mimeMap[ext] || 'text/plain';\n }\n}\n\n/**\n * Singleton instance for dependency injection.\n * Uses the configured backlog data directory.\n */\nexport const resourceManager = new ResourceManager(paths.backlogDataDir);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"manager.mjs","names":[],"sources":["../../src/resources/manager.ts"],"sourcesContent":["import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport matter from 'gray-matter';\nimport { z } from 'zod';\nimport { paths } from '@/utils/paths.js';\nimport { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Operation, WriteResourceResult } from './types.js';\nimport { applyOperation } from './operations.js';\n\nexport interface ResourceContent {\n content: string;\n frontmatter?: Record<string, any>;\n mimeType: string;\n}\n\n/**\n * ResourceManager - Single point of responsibility for MCP resource operations.\n * \n * Pure catch-all design: mcp://backlog/{+path} → {dataDir}/{path}\n * No special cases, no magic behavior.\n */\nexport class ResourceManager {\n constructor(private readonly dataDir: string) {}\n\n /**\n * Resolve MCP URI to absolute file path.\n * Pure catch-all: mcp://backlog/path/file.md → {dataDir}/path/file.md\n * \n * @param uri MCP URI (must start with mcp://backlog/)\n * @returns Absolute file path\n * @throws Error if URI is invalid or contains path traversal\n */\n resolve(uri: string): string {\n if (!uri.startsWith('mcp://')) {\n throw new Error(`Not an MCP URI: ${uri}`);\n }\n\n // Check for path traversal BEFORE URL parsing (URL normalizes ..)\n if (uri.includes('..')) {\n throw new Error(`Path traversal not allowed: ${uri}`);\n }\n\n const url = new URL(uri);\n \n if (url.hostname !== 'backlog') {\n throw new Error(`Invalid hostname: ${url.hostname}. Expected 'backlog'`);\n }\n \n const path = url.pathname.substring(1); // Remove leading /\n \n return join(this.dataDir, path);\n }\n\n /**\n * Read resource content from MCP URI.\n * Parses frontmatter for markdown files and detects MIME type.\n * \n * @param uri MCP URI\n * @returns Resource content with frontmatter and MIME type\n * @throws Error if file not found\n */\n read(uri: string): ResourceContent {\n const filePath = this.resolve(uri);\n \n if (!existsSync(filePath)) {\n // Helpful error for common mistake: extension-less task URIs\n if (/^mcp:\\/\\/backlog\\/tasks\\/(TASK|EPIC)-\\d+$/.test(uri)) {\n throw new Error(\n `Task URIs must include .md extension. Did you mean: ${uri}.md?`\n );\n }\n throw new Error(`Resource not found: ${uri} (resolved to ${filePath})`);\n }\n \n const content = readFileSync(filePath, 'utf-8');\n const ext = filePath.split('.').pop()?.toLowerCase() || 'txt';\n const mimeType = this.getMimeType(ext);\n \n // Parse frontmatter for markdown files\n if (ext === 'md') {\n const parsed = matter(content);\n return {\n content: parsed.content,\n frontmatter: Object.keys(parsed.data).length > 0 ? parsed.data : undefined,\n mimeType,\n };\n }\n \n return {\n content,\n mimeType,\n };\n }\n\n /**\n * Write/modify resource content.\n * Applies operations like str_replace, append, insert, etc.\n * \n * @param uri MCP URI\n * @param operation Operation to apply\n * @returns Result with success status and message\n */\n write(uri: string, operation: Operation): WriteResourceResult {\n try {\n const filePath = this.resolve(uri);\n const canCreate = ['create', 'append', 'insert'].includes(operation.type);\n \n if (!existsSync(filePath)) {\n if (canCreate) {\n // Auto-create file and parent directories\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, '', 'utf-8');\n } else {\n // str_replace/delete need existing content\n return {\n success: false,\n message: 'File not found',\n error: `Resource not found: ${uri} (${operation.type} requires existing file)`,\n };\n }\n }\n\n const fileContent = readFileSync(filePath, 'utf-8');\n const newContent = applyOperation(fileContent, operation);\n writeFileSync(filePath, newContent, 'utf-8');\n\n return {\n success: true,\n message: `Successfully applied ${operation.type} to ${uri}`,\n };\n } catch (error) {\n return {\n success: false,\n message: 'Operation failed',\n error: error instanceof Error ? error.message : String(error),\n };\n }\n }\n\n /**\n * Convert file path to MCP URI.\n * Pure mapping: {dataDir}/path/file.md → mcp://backlog/path/file.md\n * \n * @param filePath Absolute file path\n * @returns MCP URI or null if file is outside data directory\n */\n toUri(filePath: string): string | null {\n if (!filePath.startsWith(this.dataDir)) {\n return null;\n }\n \n const relativePath = filePath.substring(this.dataDir.length + 1);\n return `mcp://backlog/${relativePath}`;\n }\n\n /**\n * Register MCP resource handler (catch-all pattern).\n */\n registerResource(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) => {\n const resource = this.read(uri.toString());\n return { \n contents: [{ \n uri: uri.toString(), \n mimeType: resource.mimeType, \n text: resource.content \n }] \n };\n }\n );\n }\n\n /**\n * Register write_resource MCP tool.\n */\n registerWriteTool(server: McpServer) {\n server.registerTool(\n 'write_resource',\n {\n description: `A tool for creating and editing files on the MCP server\n * The \\`create\\` command will override the file at \\`uri\\` if it already exists as a file, and otherwise create a new file\n * The \\`append\\` command will add content to the end of a file, automatically adding a newline if the file doesn't end with one. Creates the file if it doesn't exist.\n Notes for using the \\`str_replace\\` command:\n * The \\`old_str\\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n * If the \\`old_str\\` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in \\`old_str\\` to make it unique\n * The \\`new_str\\` parameter should contain the edited lines that should replace the \\`old_str\\``,\n inputSchema: z.object({\n uri: z.string().describe('MCP resource URI, e.g. mcp://backlog/path/to/file.md'),\n operation: z.preprocess(\n // Workaround: MCP clients stringify object params with $ref/oneOf schemas\n // https://github.com/anthropics/claude-code/issues/18260\n (val) => typeof val === 'string' ? JSON.parse(val) : val,\n z.discriminatedUnion('type', [\n z.object({\n type: z.literal('create'),\n file_text: z.string().describe('Content of the file to be created'),\n }),\n z.object({\n type: z.literal('str_replace'),\n old_str: z.string().describe('String in file to replace (must match exactly)'),\n new_str: z.string().describe('New string to replace old_str with'),\n }),\n z.object({\n type: z.literal('insert'),\n insert_line: z.number().describe('Line number after which new_str will be inserted'),\n new_str: z.string().describe('String to insert'),\n }),\n z.object({\n type: z.literal('append'),\n new_str: z.string().describe('Content to append to the file'),\n }),\n ])).describe('Operation to apply'),\n }),\n },\n async ({ uri, operation }) => {\n const result = this.write(uri, operation);\n return {\n content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],\n };\n }\n );\n }\n\n private getMimeType(ext: string): string {\n const mimeMap: Record<string, string> = {\n md: 'text/markdown',\n json: 'application/json',\n ts: 'text/typescript',\n js: 'application/javascript',\n txt: 'text/plain',\n };\n \n return mimeMap[ext] || 'text/plain';\n }\n}\n\n/**\n * Singleton instance for dependency injection.\n * Uses the configured backlog data directory.\n */\nexport const resourceManager = new ResourceManager(paths.backlogDataDir);\n"],"mappings":"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"}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
//#region src/resources/operations.ts
|
|
2
2
|
function applyOperation(content, operation) {
|
|
3
3
|
switch (operation.type) {
|
|
4
|
+
case "create": return operation.file_text;
|
|
4
5
|
case "str_replace": {
|
|
5
6
|
const { old_str, new_str } = operation;
|
|
6
7
|
if (!content.includes(old_str)) throw new Error(`str_replace failed: old_str not found in content`);
|
|
8
|
+
const firstIndex = content.indexOf(old_str);
|
|
9
|
+
if (content.indexOf(old_str, firstIndex + 1) !== -1) throw new Error(`str_replace failed: old_str is not unique in file. Include more context to make it unique.`);
|
|
7
10
|
return content.replace(old_str, new_str);
|
|
8
11
|
}
|
|
9
|
-
case "append": return content + operation.content;
|
|
10
|
-
case "prepend": return operation.content + content;
|
|
11
12
|
case "insert": {
|
|
12
13
|
const lines = content.split("\n");
|
|
13
|
-
|
|
14
|
-
lines.
|
|
14
|
+
const lineNum = operation.insert_line;
|
|
15
|
+
if (lineNum < 0 || lineNum > lines.length) throw new Error(`insert failed: line ${lineNum} out of range (0-${lines.length})`);
|
|
16
|
+
lines.splice(lineNum, 0, operation.new_str);
|
|
15
17
|
return lines.join("\n");
|
|
16
18
|
}
|
|
17
|
-
case "
|
|
18
|
-
if (!content.includes(operation.content)) throw new Error(`delete failed: content not found`);
|
|
19
|
-
return content.replace(operation.content, "");
|
|
19
|
+
case "append": return content + (content.length > 0 && !content.endsWith("\n") ? "\n" : "") + operation.new_str;
|
|
20
20
|
default: throw new Error(`Unknown operation type: ${operation.type}`);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operations.mjs","names":[],"sources":["../../src/resources/operations.ts"],"sourcesContent":["// Apply operations to text content\n\nimport type { Operation } from './types.js';\n\nexport function applyOperation(content: string, operation: Operation): string {\n switch (operation.type) {\n case 'str_replace': {\n const { old_str, new_str } = operation;\n if (!content.includes(old_str)) {\n throw new Error(`str_replace failed: old_str not found in content`);\n }\n
|
|
1
|
+
{"version":3,"file":"operations.mjs","names":[],"sources":["../../src/resources/operations.ts"],"sourcesContent":["// Apply operations to text content (mirrors fs_write semantics)\n\nimport type { Operation } from './types.js';\n\nexport function applyOperation(content: string, operation: Operation): string {\n switch (operation.type) {\n case 'create': {\n return operation.file_text;\n }\n\n case 'str_replace': {\n const { old_str, new_str } = operation;\n if (!content.includes(old_str)) {\n throw new Error(`str_replace failed: old_str not found in content`);\n }\n // Check uniqueness - fail if old_str appears more than once\n const firstIndex = content.indexOf(old_str);\n const secondIndex = content.indexOf(old_str, firstIndex + 1);\n if (secondIndex !== -1) {\n throw new Error(`str_replace failed: old_str is not unique in file. Include more context to make it unique.`);\n }\n return content.replace(old_str, new_str);\n }\n\n case 'insert': {\n // insert_line: insert AFTER this line (1-based, like fs_write)\n const lines = content.split('\\n');\n const lineNum = operation.insert_line;\n if (lineNum < 0 || lineNum > lines.length) {\n throw new Error(`insert failed: line ${lineNum} out of range (0-${lines.length})`);\n }\n lines.splice(lineNum, 0, operation.new_str);\n return lines.join('\\n');\n }\n\n case 'append': {\n // Add newline if file doesn't end with one (like fs_write)\n const needsNewline = content.length > 0 && !content.endsWith('\\n');\n return content + (needsNewline ? '\\n' : '') + operation.new_str;\n }\n\n default:\n throw new Error(`Unknown operation type: ${(operation as any).type}`);\n }\n}\n"],"mappings":";AAIA,SAAgB,eAAe,SAAiB,WAA8B;AAC5E,SAAQ,UAAU,MAAlB;EACE,KAAK,SACH,QAAO,UAAU;EAGnB,KAAK,eAAe;GAClB,MAAM,EAAE,SAAS,YAAY;AAC7B,OAAI,CAAC,QAAQ,SAAS,QAAQ,CAC5B,OAAM,IAAI,MAAM,mDAAmD;GAGrE,MAAM,aAAa,QAAQ,QAAQ,QAAQ;AAE3C,OADoB,QAAQ,QAAQ,SAAS,aAAa,EAAE,KACxC,GAClB,OAAM,IAAI,MAAM,6FAA6F;AAE/G,UAAO,QAAQ,QAAQ,SAAS,QAAQ;;EAG1C,KAAK,UAAU;GAEb,MAAM,QAAQ,QAAQ,MAAM,KAAK;GACjC,MAAM,UAAU,UAAU;AAC1B,OAAI,UAAU,KAAK,UAAU,MAAM,OACjC,OAAM,IAAI,MAAM,uBAAuB,QAAQ,mBAAmB,MAAM,OAAO,GAAG;AAEpF,SAAM,OAAO,SAAS,GAAG,UAAU,QAAQ;AAC3C,UAAO,MAAM,KAAK,KAAK;;EAGzB,KAAK,SAGH,QAAO,WADc,QAAQ,SAAS,KAAK,CAAC,QAAQ,SAAS,KAAK,GACjC,OAAO,MAAM,UAAU;EAG1D,QACE,OAAM,IAAI,MAAM,2BAA4B,UAAkB,OAAO"}
|
|
@@ -1,33 +1,29 @@
|
|
|
1
1
|
//#region src/resources/types.d.ts
|
|
2
|
-
type OperationType = '
|
|
2
|
+
type OperationType = 'create' | 'str_replace' | 'insert' | 'append';
|
|
3
|
+
interface CreateOperation {
|
|
4
|
+
type: 'create';
|
|
5
|
+
file_text: string;
|
|
6
|
+
}
|
|
3
7
|
interface StrReplaceOperation {
|
|
4
8
|
type: 'str_replace';
|
|
5
9
|
old_str: string;
|
|
6
10
|
new_str: string;
|
|
7
11
|
}
|
|
8
|
-
interface AppendOperation {
|
|
9
|
-
type: 'append';
|
|
10
|
-
content: string;
|
|
11
|
-
}
|
|
12
|
-
interface PrependOperation {
|
|
13
|
-
type: 'prepend';
|
|
14
|
-
content: string;
|
|
15
|
-
}
|
|
16
12
|
interface InsertOperation {
|
|
17
13
|
type: 'insert';
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
insert_line: number;
|
|
15
|
+
new_str: string;
|
|
20
16
|
}
|
|
21
|
-
interface
|
|
22
|
-
type: '
|
|
23
|
-
|
|
17
|
+
interface AppendOperation {
|
|
18
|
+
type: 'append';
|
|
19
|
+
new_str: string;
|
|
24
20
|
}
|
|
25
|
-
type Operation =
|
|
21
|
+
type Operation = CreateOperation | StrReplaceOperation | InsertOperation | AppendOperation;
|
|
26
22
|
interface WriteResourceResult {
|
|
27
23
|
success: boolean;
|
|
28
24
|
message: string;
|
|
29
25
|
error?: string;
|
|
30
26
|
}
|
|
31
27
|
//#endregion
|
|
32
|
-
export { AppendOperation,
|
|
28
|
+
export { AppendOperation, CreateOperation, InsertOperation, Operation, OperationType, StrReplaceOperation, WriteResourceResult };
|
|
33
29
|
//# sourceMappingURL=types.d.mts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { paths } from "../utils/paths.mjs";
|
|
3
|
+
import { logger } from "../utils/logger.mjs";
|
|
3
4
|
import { registerViewerRoutes } from "./viewer-routes.mjs";
|
|
4
5
|
import { registerMcpHandler } from "./mcp-handler.mjs";
|
|
5
6
|
import { authMiddleware } from "../middleware/auth.mjs";
|
|
@@ -26,12 +27,18 @@ async function startHttpServer(port = 3030) {
|
|
|
26
27
|
port,
|
|
27
28
|
host: "0.0.0.0"
|
|
28
29
|
});
|
|
30
|
+
logger.info("Server started", {
|
|
31
|
+
port,
|
|
32
|
+
dataDir: paths.backlogDataDir,
|
|
33
|
+
version: paths.getVersion()
|
|
34
|
+
});
|
|
29
35
|
console.log(`Backlog MCP server running on http://localhost:${port}`);
|
|
30
36
|
console.log(`- Viewer: http://localhost:${port}/`);
|
|
31
37
|
console.log(`- MCP endpoint: http://localhost:${port}/mcp`);
|
|
32
38
|
console.log(`- Data directory: ${paths.backlogDataDir}`);
|
|
33
39
|
}
|
|
34
40
|
const shutdown = async () => {
|
|
41
|
+
logger.info("Server shutting down");
|
|
35
42
|
console.log("Shutting down gracefully...");
|
|
36
43
|
await app.close();
|
|
37
44
|
setTimeout(() => process.exit(0), 500);
|
|
@@ -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 { 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":"
|
|
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';\nimport { logger } from '@/utils/logger.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 logger.info('Server started', { port, dataDir: paths.backlogDataDir, version: paths.getVersion() });\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 logger.info('Server shutting down');\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,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,QAAO,KAAK,kBAAkB;EAAE;EAAM,SAAS,MAAM;EAAgB,SAAS,MAAM,YAAY;EAAE,CAAC;AACnG,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,QAAO,KAAK,uBAAuB;AACnC,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"}
|
package/dist/storage/backlog.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { paths } from "../utils/paths.mjs";
|
|
2
|
+
import { logger } from "../utils/logger.mjs";
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import matter from "gray-matter";
|
|
@@ -43,7 +44,14 @@ var BacklogStorage = class BacklogStorage {
|
|
|
43
44
|
if (!task.id) continue;
|
|
44
45
|
yield task;
|
|
45
46
|
} catch (error) {
|
|
46
|
-
if (error.code !== "ENOENT")
|
|
47
|
+
if (error.code !== "ENOENT") {
|
|
48
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
49
|
+
logger.warn("Malformed task file", {
|
|
50
|
+
file,
|
|
51
|
+
error: errorMessage
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
}
|
|
@@ -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';\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 const task = this.markdownToTask(readFileSync(filePath, 'utf-8'));\n // Skip malformed tasks without valid ID\n if (!task.id) continue;\n yield task;\n } catch (error) {\n // Skip files that
|
|
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';\nimport { logger } from '@/utils/logger.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 const task = this.markdownToTask(readFileSync(filePath, 'utf-8'));\n // Skip malformed tasks without valid ID\n if (!task.id) continue;\n yield task;\n } catch (error) {\n // Skip files that fail to parse (deleted, malformed YAML, etc.)\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.warn('Malformed task file', { file, error: errorMessage });\n }\n continue;\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":"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"}
|
|
@@ -14,7 +14,7 @@ function registerBacklogCreateTool(server) {
|
|
|
14
14
|
references: z.array(z.object({
|
|
15
15
|
url: z.string(),
|
|
16
16
|
title: z.string().optional()
|
|
17
|
-
})).optional().describe("Reference links
|
|
17
|
+
})).optional().describe("Reference links. Formats: external URLs (https://...), task refs (mcp://backlog/tasks/TASK-XXXX.md), resources (mcp://backlog/resources/{path}). Local files must include extension (file:///path/to/file.md)")
|
|
18
18
|
})
|
|
19
19
|
}, async ({ title, description, type, epic_id, references }) => {
|
|
20
20
|
const task = createTask({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backlog-create.mjs","names":[],"sources":["../../src/tools/backlog-create.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport { storage } from '../storage/backlog.js';\nimport { nextTaskId, createTask, TASK_TYPES } from '../storage/schema.js';\n\nexport function registerBacklogCreateTool(server: McpServer) {\n server.registerTool(\n 'backlog_create',\n {\n description: 'Create a new task in the backlog.',\n inputSchema: z.object({\n title: z.string().describe('Task title'),\n description: z.string().optional().describe('Task description in markdown'),\n type: z.enum(TASK_TYPES).optional().describe('Type: task (default) or epic'),\n epic_id: z.string().optional().describe('Parent epic ID to link this task to'),\n references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links
|
|
1
|
+
{"version":3,"file":"backlog-create.mjs","names":[],"sources":["../../src/tools/backlog-create.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport { storage } from '../storage/backlog.js';\nimport { nextTaskId, createTask, TASK_TYPES } from '../storage/schema.js';\n\nexport function registerBacklogCreateTool(server: McpServer) {\n server.registerTool(\n 'backlog_create',\n {\n description: 'Create a new task in the backlog.',\n inputSchema: z.object({\n title: z.string().describe('Task title'),\n description: z.string().optional().describe('Task description in markdown'),\n type: z.enum(TASK_TYPES).optional().describe('Type: task (default) or epic'),\n epic_id: z.string().optional().describe('Parent epic ID to link this task to'),\n references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links. Formats: external URLs (https://...), task refs (mcp://backlog/tasks/TASK-XXXX.md), resources (mcp://backlog/resources/{path}). Local files must include extension (file:///path/to/file.md)'),\n }),\n },\n async ({ title, description, type, epic_id, references }) => {\n const id = nextTaskId(storage.getMaxId(type), type);\n const task = createTask({ id, title, description, type, epic_id, references });\n storage.add(task);\n return { content: [{ type: 'text', text: `Created ${task.id}` }] };\n }\n );\n}\n"],"mappings":";;;;;AAKA,SAAgB,0BAA0B,QAAmB;AAC3D,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa,EAAE,OAAO;GACpB,OAAO,EAAE,QAAQ,CAAC,SAAS,aAAa;GACxC,aAAa,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,+BAA+B;GAC3E,MAAM,EAAE,KAAK,WAAW,CAAC,UAAU,CAAC,SAAS,+BAA+B;GAC5E,SAAS,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,sCAAsC;GAC9E,YAAY,EAAE,MAAM,EAAE,OAAO;IAAE,KAAK,EAAE,QAAQ;IAAE,OAAO,EAAE,QAAQ,CAAC,UAAU;IAAE,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,gNAAgN;GACtT,CAAC;EACH,EACD,OAAO,EAAE,OAAO,aAAa,MAAM,SAAS,iBAAiB;EAE3D,MAAM,OAAO,WAAW;GAAE,IADf,WAAW,QAAQ,SAAS,KAAK,EAAE,KAAK;GACrB;GAAO;GAAa;GAAM;GAAS;GAAY,CAAC;AAC9E,UAAQ,IAAI,KAAK;AACjB,SAAO,EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,WAAW,KAAK;GAAM,CAAC,EAAE;GAErE"}
|
|
@@ -9,7 +9,7 @@ function registerBacklogUpdateTool(server) {
|
|
|
9
9
|
inputSchema: z.object({
|
|
10
10
|
id: z.string().describe("Task ID to update"),
|
|
11
11
|
title: z.string().optional().describe("New title"),
|
|
12
|
-
description: z.string().optional().describe("New description"),
|
|
12
|
+
description: z.string().optional().describe("New description (replaces entire content). For appending/editing sections, use write_resource tool instead"),
|
|
13
13
|
status: z.enum(STATUSES).optional().describe("New status"),
|
|
14
14
|
epic_id: z.union([z.string(), z.null()]).optional().describe("Parent epic ID (null to unlink)"),
|
|
15
15
|
blocked_reason: z.array(z.string()).optional().describe("Reason if status is blocked"),
|
|
@@ -17,7 +17,7 @@ function registerBacklogUpdateTool(server) {
|
|
|
17
17
|
references: z.array(z.object({
|
|
18
18
|
url: z.string(),
|
|
19
19
|
title: z.string().optional()
|
|
20
|
-
})).optional().describe("Reference links
|
|
20
|
+
})).optional().describe("Reference links. Formats: external URLs (https://...), task refs (mcp://backlog/tasks/TASK-XXXX.md), resources (mcp://backlog/resources/{path}). Local files must include extension (file:///path/to/file.md)")
|
|
21
21
|
})
|
|
22
22
|
}, async ({ id, ...updates }) => {
|
|
23
23
|
const task = storage.get(id);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backlog-update.mjs","names":[],"sources":["../../src/tools/backlog-update.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport { storage } from '../storage/backlog.js';\nimport { STATUSES } from '../storage/schema.js';\n\nexport function registerBacklogUpdateTool(server: McpServer) {\n server.registerTool(\n 'backlog_update',\n {\n description: 'Update an existing task.',\n inputSchema: z.object({\n id: z.string().describe('Task ID to update'),\n title: z.string().optional().describe('New title'),\n description: z.string().optional().describe('New description'),\n status: z.enum(STATUSES).optional().describe('New status'),\n epic_id: z.union([z.string(), z.null()]).optional().describe('Parent epic ID (null to unlink)'),\n blocked_reason: z.array(z.string()).optional().describe('Reason if status is blocked'),\n evidence: z.array(z.string()).optional().describe('Proof of completion when marking done - links to PRs, docs, or notes'),\n references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links
|
|
1
|
+
{"version":3,"file":"backlog-update.mjs","names":[],"sources":["../../src/tools/backlog-update.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport { storage } from '../storage/backlog.js';\nimport { STATUSES } from '../storage/schema.js';\n\nexport function registerBacklogUpdateTool(server: McpServer) {\n server.registerTool(\n 'backlog_update',\n {\n description: 'Update an existing task.',\n inputSchema: z.object({\n id: z.string().describe('Task ID to update'),\n title: z.string().optional().describe('New title'),\n description: z.string().optional().describe('New description (replaces entire content). For appending/editing sections, use write_resource tool instead'),\n status: z.enum(STATUSES).optional().describe('New status'),\n epic_id: z.union([z.string(), z.null()]).optional().describe('Parent epic ID (null to unlink)'),\n blocked_reason: z.array(z.string()).optional().describe('Reason if status is blocked'),\n evidence: z.array(z.string()).optional().describe('Proof of completion when marking done - links to PRs, docs, or notes'),\n references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links. Formats: external URLs (https://...), task refs (mcp://backlog/tasks/TASK-XXXX.md), resources (mcp://backlog/resources/{path}). Local files must include extension (file:///path/to/file.md)'),\n }),\n },\n async ({ id, ...updates }) => {\n const task = storage.get(id);\n if (!task) return { content: [{ type: 'text', text: `Task ${id} not found` }], isError: true };\n Object.assign(task, updates, { updated_at: new Date().toISOString() });\n storage.save(task);\n return { content: [{ type: 'text', text: `Updated ${id}` }] };\n }\n );\n}\n"],"mappings":";;;;;AAKA,SAAgB,0BAA0B,QAAmB;AAC3D,QAAO,aACL,kBACA;EACE,aAAa;EACb,aAAa,EAAE,OAAO;GACpB,IAAI,EAAE,QAAQ,CAAC,SAAS,oBAAoB;GAC5C,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,YAAY;GAClD,aAAa,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,6GAA6G;GACzJ,QAAQ,EAAE,KAAK,SAAS,CAAC,UAAU,CAAC,SAAS,aAAa;GAC1D,SAAS,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,kCAAkC;GAC/F,gBAAgB,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,SAAS,8BAA8B;GACtF,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,SAAS,uEAAuE;GACzH,YAAY,EAAE,MAAM,EAAE,OAAO;IAAE,KAAK,EAAE,QAAQ;IAAE,OAAO,EAAE,QAAQ,CAAC,UAAU;IAAE,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,gNAAgN;GACtT,CAAC;EACH,EACD,OAAO,EAAE,IAAI,GAAG,cAAc;EAC5B,MAAM,OAAO,QAAQ,IAAI,GAAG;AAC5B,MAAI,CAAC,KAAM,QAAO;GAAE,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,QAAQ,GAAG;IAAa,CAAC;GAAE,SAAS;GAAM;AAC9F,SAAO,OAAO,MAAM,SAAS,EAAE,6BAAY,IAAI,MAAM,EAAC,aAAa,EAAE,CAAC;AACtE,UAAQ,KAAK,KAAK;AAClB,SAAO,EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,WAAW;GAAM,CAAC,EAAE;GAEhE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/utils/logger.d.ts
|
|
2
|
+
declare const logger: {
|
|
3
|
+
debug: (message: string, data?: Record<string, unknown>) => void;
|
|
4
|
+
info: (message: string, data?: Record<string, unknown>) => void;
|
|
5
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
6
|
+
error: (message: string, data?: Record<string, unknown>) => void;
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { logger };
|
|
10
|
+
//# sourceMappingURL=logger.d.mts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { paths } from "./paths.mjs";
|
|
2
|
+
import { appendFile, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region src/utils/logger.ts
|
|
6
|
+
const LEVELS = [
|
|
7
|
+
"debug",
|
|
8
|
+
"info",
|
|
9
|
+
"warn",
|
|
10
|
+
"error"
|
|
11
|
+
];
|
|
12
|
+
const levelPriority = {
|
|
13
|
+
debug: 0,
|
|
14
|
+
info: 1,
|
|
15
|
+
warn: 2,
|
|
16
|
+
error: 3
|
|
17
|
+
};
|
|
18
|
+
function getLogLevel() {
|
|
19
|
+
const env = process.env.LOG_LEVEL?.toLowerCase();
|
|
20
|
+
return LEVELS.includes(env) ? env : "info";
|
|
21
|
+
}
|
|
22
|
+
function getLogFile() {
|
|
23
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
24
|
+
return join(paths.backlogDataDir, "logs", `backlog-${date}.log`);
|
|
25
|
+
}
|
|
26
|
+
function write(level, message, data) {
|
|
27
|
+
if (levelPriority[level] < levelPriority[getLogLevel()]) return;
|
|
28
|
+
const logDir = join(paths.backlogDataDir, "logs");
|
|
29
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
30
|
+
const entry = JSON.stringify({
|
|
31
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32
|
+
level,
|
|
33
|
+
message,
|
|
34
|
+
...data
|
|
35
|
+
});
|
|
36
|
+
appendFile(getLogFile(), entry + "\n", (err) => {
|
|
37
|
+
if (err) process.stderr.write(`Logger error: ${err.message}\n`);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const logger = {
|
|
41
|
+
debug: (message, data) => write("debug", message, data),
|
|
42
|
+
info: (message, data) => write("info", message, data),
|
|
43
|
+
warn: (message, data) => write("warn", message, data),
|
|
44
|
+
error: (message, data) => write("error", message, data)
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { logger };
|
|
49
|
+
//# sourceMappingURL=logger.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.mjs","names":[],"sources":["../../src/utils/logger.ts"],"sourcesContent":["import { appendFile, mkdirSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { paths } from './paths.js';\n\nconst LEVELS = ['debug', 'info', 'warn', 'error'] as const;\ntype Level = (typeof LEVELS)[number];\n\nconst levelPriority: Record<Level, number> = { debug: 0, info: 1, warn: 2, error: 3 };\n\nfunction getLogLevel(): Level {\n const env = process.env.LOG_LEVEL?.toLowerCase();\n return LEVELS.includes(env as Level) ? (env as Level) : 'info';\n}\n\nfunction getLogFile(): string {\n const date = new Date().toISOString().split('T')[0];\n return join(paths.backlogDataDir, 'logs', `backlog-${date}.log`);\n}\n\nfunction write(level: Level, message: string, data?: Record<string, unknown>): void {\n if (levelPriority[level] < levelPriority[getLogLevel()]) return;\n\n const logDir = join(paths.backlogDataDir, 'logs');\n if (!existsSync(logDir)) {\n mkdirSync(logDir, { recursive: true });\n }\n\n const entry = JSON.stringify({\n timestamp: new Date().toISOString(),\n level,\n message,\n ...data,\n });\n\n appendFile(getLogFile(), entry + '\\n', (err) => {\n if (err) {\n process.stderr.write(`Logger error: ${err.message}\\n`);\n }\n });\n}\n\nexport const logger = {\n debug: (message: string, data?: Record<string, unknown>) => write('debug', message, data),\n info: (message: string, data?: Record<string, unknown>) => write('info', message, data),\n warn: (message: string, data?: Record<string, unknown>) => write('warn', message, data),\n error: (message: string, data?: Record<string, unknown>) => write('error', message, data),\n};\n"],"mappings":";;;;;AAIA,MAAM,SAAS;CAAC;CAAS;CAAQ;CAAQ;CAAQ;AAGjD,MAAM,gBAAuC;CAAE,OAAO;CAAG,MAAM;CAAG,MAAM;CAAG,OAAO;CAAG;AAErF,SAAS,cAAqB;CAC5B,MAAM,MAAM,QAAQ,IAAI,WAAW,aAAa;AAChD,QAAO,OAAO,SAAS,IAAa,GAAI,MAAgB;;AAG1D,SAAS,aAAqB;CAC5B,MAAM,wBAAO,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,IAAI,CAAC;AACjD,QAAO,KAAK,MAAM,gBAAgB,QAAQ,WAAW,KAAK,MAAM;;AAGlE,SAAS,MAAM,OAAc,SAAiB,MAAsC;AAClF,KAAI,cAAc,SAAS,cAAc,aAAa,EAAG;CAEzD,MAAM,SAAS,KAAK,MAAM,gBAAgB,OAAO;AACjD,KAAI,CAAC,WAAW,OAAO,CACrB,WAAU,QAAQ,EAAE,WAAW,MAAM,CAAC;CAGxC,MAAM,QAAQ,KAAK,UAAU;EAC3B,4BAAW,IAAI,MAAM,EAAC,aAAa;EACnC;EACA;EACA,GAAG;EACJ,CAAC;AAEF,YAAW,YAAY,EAAE,QAAQ,OAAO,QAAQ;AAC9C,MAAI,IACF,SAAQ,OAAO,MAAM,iBAAiB,IAAI,QAAQ,IAAI;GAExD;;AAGJ,MAAa,SAAS;CACpB,QAAQ,SAAiB,SAAmC,MAAM,SAAS,SAAS,KAAK;CACzF,OAAO,SAAiB,SAAmC,MAAM,QAAQ,SAAS,KAAK;CACvF,OAAO,SAAiB,SAAmC,MAAM,QAAQ,SAAS,KAAK;CACvF,QAAQ,SAAiB,SAAmC,MAAM,SAAS,SAAS,KAAK;CAC1F"}
|