kanban-lite 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +9 -0
- package/.github/workflows/ci.yml +59 -0
- package/.github/workflows/release.yml +75 -0
- package/.prettierignore +6 -0
- package/.prettierrc.yaml +4 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/launch.json +17 -0
- package/.vscode/settings.json +21 -0
- package/.vscode/tasks.json +22 -0
- package/.vscodeignore +11 -0
- package/CHANGELOG.md +184 -0
- package/CLAUDE.md +58 -0
- package/CONTRIBUTING.md +114 -0
- package/LICENSE +22 -0
- package/README.md +482 -0
- package/SKILL.md +237 -0
- package/dist/cli.js +8716 -0
- package/dist/extension.js +8463 -0
- package/dist/mcp-server.js +1327 -0
- package/dist/standalone-webview/icons-Dx9MGYqN.js +180 -0
- package/dist/standalone-webview/icons-Dx9MGYqN.js.map +1 -0
- package/dist/standalone-webview/index.js +85 -0
- package/dist/standalone-webview/index.js.map +1 -0
- package/dist/standalone-webview/react-vendor-DkYdDBET.js +25 -0
- package/dist/standalone-webview/react-vendor-DkYdDBET.js.map +1 -0
- package/dist/standalone-webview/style.css +1 -0
- package/dist/standalone.js +7513 -0
- package/dist/webview/icons-Dx9MGYqN.js +180 -0
- package/dist/webview/icons-Dx9MGYqN.js.map +1 -0
- package/dist/webview/index.js +85 -0
- package/dist/webview/index.js.map +1 -0
- package/dist/webview/react-vendor-DkYdDBET.js +25 -0
- package/dist/webview/react-vendor-DkYdDBET.js.map +1 -0
- package/dist/webview/style.css +1 -0
- package/docs/images/board-overview.png +0 -0
- package/docs/images/editor-view.png +0 -0
- package/docs/plans/2026-02-20-kanban-json-config-design.md +74 -0
- package/docs/plans/2026-02-20-kanban-json-config.md +690 -0
- package/eslint.config.mjs +31 -0
- package/package.json +161 -0
- package/postcss.config.js +6 -0
- package/resources/icon-light.png +0 -0
- package/resources/icon-light.svg +105 -0
- package/resources/icon.png +0 -0
- package/resources/icon.svg +105 -0
- package/resources/kanban-dark.svg +21 -0
- package/resources/kanban-light.svg +21 -0
- package/resources/kanban.svg +21 -0
- package/src/cli/index.ts +846 -0
- package/src/extension/FeatureHeaderProvider.ts +370 -0
- package/src/extension/KanbanPanel.ts +973 -0
- package/src/extension/SidebarViewProvider.ts +507 -0
- package/src/extension/featureFileUtils.ts +82 -0
- package/src/extension/index.ts +234 -0
- package/src/mcp-server/index.ts +632 -0
- package/src/sdk/KanbanSDK.ts +349 -0
- package/src/sdk/__tests__/KanbanSDK.test.ts +468 -0
- package/src/sdk/__tests__/parser.test.ts +170 -0
- package/src/sdk/fileUtils.ts +76 -0
- package/src/sdk/index.ts +6 -0
- package/src/sdk/parser.ts +70 -0
- package/src/sdk/types.ts +15 -0
- package/src/shared/config.ts +113 -0
- package/src/shared/editorTypes.ts +14 -0
- package/src/shared/types.ts +120 -0
- package/src/standalone/__tests__/server.integration.test.ts +1916 -0
- package/src/standalone/__tests__/webhooks.test.ts +357 -0
- package/src/standalone/fileUtils.ts +70 -0
- package/src/standalone/index.ts +71 -0
- package/src/standalone/server.ts +1046 -0
- package/src/standalone/webhooks.ts +135 -0
- package/src/webview/App.tsx +469 -0
- package/src/webview/assets/main.css +329 -0
- package/src/webview/assets/standalone-theme.css +130 -0
- package/src/webview/components/ColumnDialog.tsx +119 -0
- package/src/webview/components/CreateFeatureDialog.tsx +524 -0
- package/src/webview/components/DatePicker.tsx +185 -0
- package/src/webview/components/FeatureCard.tsx +186 -0
- package/src/webview/components/FeatureEditor.tsx +623 -0
- package/src/webview/components/KanbanBoard.tsx +144 -0
- package/src/webview/components/KanbanColumn.tsx +159 -0
- package/src/webview/components/MarkdownEditor.tsx +291 -0
- package/src/webview/components/PrioritySelect.tsx +39 -0
- package/src/webview/components/QuickAddInput.tsx +72 -0
- package/src/webview/components/SettingsPanel.tsx +284 -0
- package/src/webview/components/Toolbar.tsx +175 -0
- package/src/webview/components/UndoToast.tsx +70 -0
- package/src/webview/index.html +12 -0
- package/src/webview/lib/utils.ts +6 -0
- package/src/webview/main.tsx +11 -0
- package/src/webview/standalone-main.tsx +13 -0
- package/src/webview/standalone-shim.ts +132 -0
- package/src/webview/standalone.html +12 -0
- package/src/webview/store/index.ts +241 -0
- package/tailwind.config.js +53 -0
- package/tsconfig.json +22 -0
- package/vite.config.ts +36 -0
- package/vite.standalone.config.ts +62 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,1327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/mcp-server/index.ts
|
|
27
|
+
var path6 = __toESM(require("path"));
|
|
28
|
+
var fs5 = __toESM(require("fs/promises"));
|
|
29
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
30
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
31
|
+
var import_zod = require("zod");
|
|
32
|
+
|
|
33
|
+
// src/sdk/KanbanSDK.ts
|
|
34
|
+
var fs3 = __toESM(require("fs/promises"));
|
|
35
|
+
var path4 = __toESM(require("path"));
|
|
36
|
+
|
|
37
|
+
// node_modules/fractional-indexing/src/index.js
|
|
38
|
+
var BASE_62_DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
39
|
+
function midpoint(a, b, digits) {
|
|
40
|
+
const zero = digits[0];
|
|
41
|
+
if (b != null && a >= b) {
|
|
42
|
+
throw new Error(a + " >= " + b);
|
|
43
|
+
}
|
|
44
|
+
if (a.slice(-1) === zero || b && b.slice(-1) === zero) {
|
|
45
|
+
throw new Error("trailing zero");
|
|
46
|
+
}
|
|
47
|
+
if (b) {
|
|
48
|
+
let n = 0;
|
|
49
|
+
while ((a[n] || zero) === b[n]) {
|
|
50
|
+
n++;
|
|
51
|
+
}
|
|
52
|
+
if (n > 0) {
|
|
53
|
+
return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const digitA = a ? digits.indexOf(a[0]) : 0;
|
|
57
|
+
const digitB = b != null ? digits.indexOf(b[0]) : digits.length;
|
|
58
|
+
if (digitB - digitA > 1) {
|
|
59
|
+
const midDigit = Math.round(0.5 * (digitA + digitB));
|
|
60
|
+
return digits[midDigit];
|
|
61
|
+
} else {
|
|
62
|
+
if (b && b.length > 1) {
|
|
63
|
+
return b.slice(0, 1);
|
|
64
|
+
} else {
|
|
65
|
+
return digits[digitA] + midpoint(a.slice(1), null, digits);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function validateInteger(int) {
|
|
70
|
+
if (int.length !== getIntegerLength(int[0])) {
|
|
71
|
+
throw new Error("invalid integer part of order key: " + int);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function getIntegerLength(head) {
|
|
75
|
+
if (head >= "a" && head <= "z") {
|
|
76
|
+
return head.charCodeAt(0) - "a".charCodeAt(0) + 2;
|
|
77
|
+
} else if (head >= "A" && head <= "Z") {
|
|
78
|
+
return "Z".charCodeAt(0) - head.charCodeAt(0) + 2;
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error("invalid order key head: " + head);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function getIntegerPart(key) {
|
|
84
|
+
const integerPartLength = getIntegerLength(key[0]);
|
|
85
|
+
if (integerPartLength > key.length) {
|
|
86
|
+
throw new Error("invalid order key: " + key);
|
|
87
|
+
}
|
|
88
|
+
return key.slice(0, integerPartLength);
|
|
89
|
+
}
|
|
90
|
+
function validateOrderKey(key, digits) {
|
|
91
|
+
if (key === "A" + digits[0].repeat(26)) {
|
|
92
|
+
throw new Error("invalid order key: " + key);
|
|
93
|
+
}
|
|
94
|
+
const i = getIntegerPart(key);
|
|
95
|
+
const f = key.slice(i.length);
|
|
96
|
+
if (f.slice(-1) === digits[0]) {
|
|
97
|
+
throw new Error("invalid order key: " + key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function incrementInteger(x, digits) {
|
|
101
|
+
validateInteger(x);
|
|
102
|
+
const [head, ...digs] = x.split("");
|
|
103
|
+
let carry = true;
|
|
104
|
+
for (let i = digs.length - 1; carry && i >= 0; i--) {
|
|
105
|
+
const d = digits.indexOf(digs[i]) + 1;
|
|
106
|
+
if (d === digits.length) {
|
|
107
|
+
digs[i] = digits[0];
|
|
108
|
+
} else {
|
|
109
|
+
digs[i] = digits[d];
|
|
110
|
+
carry = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (carry) {
|
|
114
|
+
if (head === "Z") {
|
|
115
|
+
return "a" + digits[0];
|
|
116
|
+
}
|
|
117
|
+
if (head === "z") {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const h = String.fromCharCode(head.charCodeAt(0) + 1);
|
|
121
|
+
if (h > "a") {
|
|
122
|
+
digs.push(digits[0]);
|
|
123
|
+
} else {
|
|
124
|
+
digs.pop();
|
|
125
|
+
}
|
|
126
|
+
return h + digs.join("");
|
|
127
|
+
} else {
|
|
128
|
+
return head + digs.join("");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function decrementInteger(x, digits) {
|
|
132
|
+
validateInteger(x);
|
|
133
|
+
const [head, ...digs] = x.split("");
|
|
134
|
+
let borrow = true;
|
|
135
|
+
for (let i = digs.length - 1; borrow && i >= 0; i--) {
|
|
136
|
+
const d = digits.indexOf(digs[i]) - 1;
|
|
137
|
+
if (d === -1) {
|
|
138
|
+
digs[i] = digits.slice(-1);
|
|
139
|
+
} else {
|
|
140
|
+
digs[i] = digits[d];
|
|
141
|
+
borrow = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (borrow) {
|
|
145
|
+
if (head === "a") {
|
|
146
|
+
return "Z" + digits.slice(-1);
|
|
147
|
+
}
|
|
148
|
+
if (head === "A") {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const h = String.fromCharCode(head.charCodeAt(0) - 1);
|
|
152
|
+
if (h < "Z") {
|
|
153
|
+
digs.push(digits.slice(-1));
|
|
154
|
+
} else {
|
|
155
|
+
digs.pop();
|
|
156
|
+
}
|
|
157
|
+
return h + digs.join("");
|
|
158
|
+
} else {
|
|
159
|
+
return head + digs.join("");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function generateKeyBetween(a, b, digits = BASE_62_DIGITS) {
|
|
163
|
+
if (a != null) {
|
|
164
|
+
validateOrderKey(a, digits);
|
|
165
|
+
}
|
|
166
|
+
if (b != null) {
|
|
167
|
+
validateOrderKey(b, digits);
|
|
168
|
+
}
|
|
169
|
+
if (a != null && b != null && a >= b) {
|
|
170
|
+
throw new Error(a + " >= " + b);
|
|
171
|
+
}
|
|
172
|
+
if (a == null) {
|
|
173
|
+
if (b == null) {
|
|
174
|
+
return "a" + digits[0];
|
|
175
|
+
}
|
|
176
|
+
const ib2 = getIntegerPart(b);
|
|
177
|
+
const fb2 = b.slice(ib2.length);
|
|
178
|
+
if (ib2 === "A" + digits[0].repeat(26)) {
|
|
179
|
+
return ib2 + midpoint("", fb2, digits);
|
|
180
|
+
}
|
|
181
|
+
if (ib2 < b) {
|
|
182
|
+
return ib2;
|
|
183
|
+
}
|
|
184
|
+
const res = decrementInteger(ib2, digits);
|
|
185
|
+
if (res == null) {
|
|
186
|
+
throw new Error("cannot decrement any more");
|
|
187
|
+
}
|
|
188
|
+
return res;
|
|
189
|
+
}
|
|
190
|
+
if (b == null) {
|
|
191
|
+
const ia2 = getIntegerPart(a);
|
|
192
|
+
const fa2 = a.slice(ia2.length);
|
|
193
|
+
const i2 = incrementInteger(ia2, digits);
|
|
194
|
+
return i2 == null ? ia2 + midpoint(fa2, null, digits) : i2;
|
|
195
|
+
}
|
|
196
|
+
const ia = getIntegerPart(a);
|
|
197
|
+
const fa = a.slice(ia.length);
|
|
198
|
+
const ib = getIntegerPart(b);
|
|
199
|
+
const fb = b.slice(ib.length);
|
|
200
|
+
if (ia === ib) {
|
|
201
|
+
return ia + midpoint(fa, fb, digits);
|
|
202
|
+
}
|
|
203
|
+
const i = incrementInteger(ia, digits);
|
|
204
|
+
if (i == null) {
|
|
205
|
+
throw new Error("cannot increment any more");
|
|
206
|
+
}
|
|
207
|
+
if (i < b) {
|
|
208
|
+
return i;
|
|
209
|
+
}
|
|
210
|
+
return ia + midpoint(fa, null, digits);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/shared/types.ts
|
|
214
|
+
function getTitleFromContent(content) {
|
|
215
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
216
|
+
if (match)
|
|
217
|
+
return match[1].trim();
|
|
218
|
+
const firstLine = content.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
219
|
+
return firstLine || "Untitled";
|
|
220
|
+
}
|
|
221
|
+
function generateSlug(title) {
|
|
222
|
+
return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50) || "feature";
|
|
223
|
+
}
|
|
224
|
+
function generateFeatureFilename(id, title) {
|
|
225
|
+
const slug = generateSlug(title);
|
|
226
|
+
return `${id}-${slug}`;
|
|
227
|
+
}
|
|
228
|
+
function extractNumericId(filenameOrId) {
|
|
229
|
+
const match = filenameOrId.match(/^(\d+)(?:-|$)/);
|
|
230
|
+
return match ? parseInt(match[1], 10) : null;
|
|
231
|
+
}
|
|
232
|
+
var DEFAULT_COLUMNS = [
|
|
233
|
+
{ id: "backlog", name: "Backlog", color: "#6b7280" },
|
|
234
|
+
{ id: "todo", name: "To Do", color: "#3b82f6" },
|
|
235
|
+
{ id: "in-progress", name: "In Progress", color: "#f59e0b" },
|
|
236
|
+
{ id: "review", name: "Review", color: "#8b5cf6" },
|
|
237
|
+
{ id: "done", name: "Done", color: "#22c55e" }
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
// src/shared/config.ts
|
|
241
|
+
var fs = __toESM(require("fs"));
|
|
242
|
+
var path = __toESM(require("path"));
|
|
243
|
+
var DEFAULT_CONFIG = {
|
|
244
|
+
featuresDirectory: ".kanban",
|
|
245
|
+
defaultPriority: "medium",
|
|
246
|
+
defaultStatus: "backlog",
|
|
247
|
+
columns: [
|
|
248
|
+
{ id: "backlog", name: "Backlog", color: "#6b7280" },
|
|
249
|
+
{ id: "todo", name: "To Do", color: "#3b82f6" },
|
|
250
|
+
{ id: "in-progress", name: "In Progress", color: "#f59e0b" },
|
|
251
|
+
{ id: "review", name: "Review", color: "#8b5cf6" },
|
|
252
|
+
{ id: "done", name: "Done", color: "#22c55e" }
|
|
253
|
+
],
|
|
254
|
+
aiAgent: "claude",
|
|
255
|
+
nextCardId: 1,
|
|
256
|
+
showPriorityBadges: true,
|
|
257
|
+
showAssignee: true,
|
|
258
|
+
showDueDate: true,
|
|
259
|
+
showLabels: true,
|
|
260
|
+
showBuildWithAI: true,
|
|
261
|
+
showFileName: false,
|
|
262
|
+
compactMode: false,
|
|
263
|
+
markdownEditorMode: false
|
|
264
|
+
};
|
|
265
|
+
var CONFIG_FILENAME = ".kanban.json";
|
|
266
|
+
function configPath(workspaceRoot) {
|
|
267
|
+
return path.join(workspaceRoot, CONFIG_FILENAME);
|
|
268
|
+
}
|
|
269
|
+
function readConfig(workspaceRoot) {
|
|
270
|
+
const filePath = configPath(workspaceRoot);
|
|
271
|
+
try {
|
|
272
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
273
|
+
return { ...DEFAULT_CONFIG, ...raw };
|
|
274
|
+
} catch {
|
|
275
|
+
return { ...DEFAULT_CONFIG };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function writeConfig(workspaceRoot, config) {
|
|
279
|
+
const filePath = configPath(workspaceRoot);
|
|
280
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
281
|
+
}
|
|
282
|
+
function allocateCardId(workspaceRoot) {
|
|
283
|
+
const config = readConfig(workspaceRoot);
|
|
284
|
+
const id = config.nextCardId;
|
|
285
|
+
writeConfig(workspaceRoot, { ...config, nextCardId: id + 1 });
|
|
286
|
+
return id;
|
|
287
|
+
}
|
|
288
|
+
function syncCardIdCounter(workspaceRoot, existingIds) {
|
|
289
|
+
if (existingIds.length === 0)
|
|
290
|
+
return;
|
|
291
|
+
const maxId = Math.max(...existingIds);
|
|
292
|
+
const config = readConfig(workspaceRoot);
|
|
293
|
+
if (config.nextCardId <= maxId) {
|
|
294
|
+
writeConfig(workspaceRoot, { ...config, nextCardId: maxId + 1 });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function configToSettings(config) {
|
|
298
|
+
return {
|
|
299
|
+
showPriorityBadges: config.showPriorityBadges,
|
|
300
|
+
showAssignee: config.showAssignee,
|
|
301
|
+
showDueDate: config.showDueDate,
|
|
302
|
+
showLabels: config.showLabels,
|
|
303
|
+
showBuildWithAI: config.showBuildWithAI,
|
|
304
|
+
showFileName: config.showFileName,
|
|
305
|
+
compactMode: config.compactMode,
|
|
306
|
+
markdownEditorMode: config.markdownEditorMode,
|
|
307
|
+
defaultPriority: config.defaultPriority,
|
|
308
|
+
defaultStatus: config.defaultStatus
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function settingsToConfig(config, settings) {
|
|
312
|
+
return {
|
|
313
|
+
...config,
|
|
314
|
+
showPriorityBadges: settings.showPriorityBadges,
|
|
315
|
+
showAssignee: settings.showAssignee,
|
|
316
|
+
showDueDate: settings.showDueDate,
|
|
317
|
+
showLabels: settings.showLabels,
|
|
318
|
+
showFileName: settings.showFileName,
|
|
319
|
+
compactMode: settings.compactMode,
|
|
320
|
+
defaultPriority: settings.defaultPriority,
|
|
321
|
+
defaultStatus: settings.defaultStatus
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/sdk/parser.ts
|
|
326
|
+
var path2 = __toESM(require("path"));
|
|
327
|
+
function extractIdFromFilename(filePath) {
|
|
328
|
+
const basename4 = path2.basename(filePath, ".md");
|
|
329
|
+
const numericMatch = basename4.match(/^(\d+)-/);
|
|
330
|
+
if (numericMatch)
|
|
331
|
+
return numericMatch[1];
|
|
332
|
+
return basename4;
|
|
333
|
+
}
|
|
334
|
+
function parseFeatureFile(content, filePath) {
|
|
335
|
+
content = content.replace(/\r\n/g, "\n");
|
|
336
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
337
|
+
if (!frontmatterMatch)
|
|
338
|
+
return null;
|
|
339
|
+
const frontmatter = frontmatterMatch[1];
|
|
340
|
+
const body = frontmatterMatch[2] || "";
|
|
341
|
+
const getValue = (key) => {
|
|
342
|
+
const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
|
|
343
|
+
if (!match)
|
|
344
|
+
return "";
|
|
345
|
+
const value = match[1].trim().replace(/^["']|["']$/g, "");
|
|
346
|
+
return value === "null" ? "" : value;
|
|
347
|
+
};
|
|
348
|
+
const getArrayValue = (key) => {
|
|
349
|
+
const match = frontmatter.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m"));
|
|
350
|
+
if (!match)
|
|
351
|
+
return [];
|
|
352
|
+
return match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
353
|
+
};
|
|
354
|
+
return {
|
|
355
|
+
id: getValue("id") || extractIdFromFilename(filePath),
|
|
356
|
+
status: getValue("status") || "backlog",
|
|
357
|
+
priority: getValue("priority") || "medium",
|
|
358
|
+
assignee: getValue("assignee") || null,
|
|
359
|
+
dueDate: getValue("dueDate") || null,
|
|
360
|
+
created: getValue("created") || (/* @__PURE__ */ new Date()).toISOString(),
|
|
361
|
+
modified: getValue("modified") || (/* @__PURE__ */ new Date()).toISOString(),
|
|
362
|
+
completedAt: getValue("completedAt") || null,
|
|
363
|
+
labels: getArrayValue("labels"),
|
|
364
|
+
attachments: getArrayValue("attachments"),
|
|
365
|
+
order: getValue("order") || "a0",
|
|
366
|
+
content: body.trim(),
|
|
367
|
+
filePath
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function serializeFeature(feature) {
|
|
371
|
+
const frontmatter = [
|
|
372
|
+
"---",
|
|
373
|
+
`id: "${feature.id}"`,
|
|
374
|
+
`status: "${feature.status}"`,
|
|
375
|
+
`priority: "${feature.priority}"`,
|
|
376
|
+
`assignee: ${feature.assignee ? `"${feature.assignee}"` : "null"}`,
|
|
377
|
+
`dueDate: ${feature.dueDate ? `"${feature.dueDate}"` : "null"}`,
|
|
378
|
+
`created: "${feature.created}"`,
|
|
379
|
+
`modified: "${feature.modified}"`,
|
|
380
|
+
`completedAt: ${feature.completedAt ? `"${feature.completedAt}"` : "null"}`,
|
|
381
|
+
`labels: [${feature.labels.map((l) => `"${l}"`).join(", ")}]`,
|
|
382
|
+
`attachments: [${(feature.attachments || []).map((a) => `"${a}"`).join(", ")}]`,
|
|
383
|
+
`order: "${feature.order}"`,
|
|
384
|
+
"---",
|
|
385
|
+
""
|
|
386
|
+
].join("\n");
|
|
387
|
+
return frontmatter + feature.content;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/sdk/fileUtils.ts
|
|
391
|
+
var path3 = __toESM(require("path"));
|
|
392
|
+
var fs2 = __toESM(require("fs/promises"));
|
|
393
|
+
function getFeatureFilePath(featuresDir, status, filename) {
|
|
394
|
+
return path3.join(featuresDir, status, `${filename}.md`);
|
|
395
|
+
}
|
|
396
|
+
async function ensureDirectories(featuresDir) {
|
|
397
|
+
await fs2.mkdir(featuresDir, { recursive: true });
|
|
398
|
+
}
|
|
399
|
+
async function moveFeatureFile(currentPath, featuresDir, newStatus, attachments) {
|
|
400
|
+
const filename = path3.basename(currentPath);
|
|
401
|
+
const targetDir = path3.join(featuresDir, newStatus);
|
|
402
|
+
let targetPath = path3.join(targetDir, filename);
|
|
403
|
+
if (currentPath === targetPath)
|
|
404
|
+
return currentPath;
|
|
405
|
+
const ext = path3.extname(filename);
|
|
406
|
+
const base = path3.basename(filename, ext);
|
|
407
|
+
let counter = 1;
|
|
408
|
+
while (await fileExists(targetPath)) {
|
|
409
|
+
targetPath = path3.join(targetDir, `${base}-${counter}${ext}`);
|
|
410
|
+
counter++;
|
|
411
|
+
}
|
|
412
|
+
await fs2.mkdir(targetDir, { recursive: true });
|
|
413
|
+
await fs2.rename(currentPath, targetPath);
|
|
414
|
+
if (attachments && attachments.length > 0) {
|
|
415
|
+
const sourceDir = path3.dirname(currentPath);
|
|
416
|
+
for (const attachment of attachments) {
|
|
417
|
+
const srcAttach = path3.join(sourceDir, attachment);
|
|
418
|
+
const destAttach = path3.join(targetDir, attachment);
|
|
419
|
+
try {
|
|
420
|
+
await fs2.access(srcAttach);
|
|
421
|
+
await fs2.rename(srcAttach, destAttach);
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return targetPath;
|
|
427
|
+
}
|
|
428
|
+
async function renameFeatureFile(currentPath, newFilename) {
|
|
429
|
+
const dir = path3.dirname(currentPath);
|
|
430
|
+
const newPath = path3.join(dir, `${newFilename}.md`);
|
|
431
|
+
if (currentPath === newPath)
|
|
432
|
+
return currentPath;
|
|
433
|
+
await fs2.rename(currentPath, newPath);
|
|
434
|
+
return newPath;
|
|
435
|
+
}
|
|
436
|
+
async function fileExists(filePath) {
|
|
437
|
+
try {
|
|
438
|
+
await fs2.access(filePath);
|
|
439
|
+
return true;
|
|
440
|
+
} catch {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/sdk/KanbanSDK.ts
|
|
446
|
+
var BOARD_CONFIG_FILE = "board.json";
|
|
447
|
+
var KanbanSDK = class {
|
|
448
|
+
constructor(featuresDir) {
|
|
449
|
+
this.featuresDir = featuresDir;
|
|
450
|
+
}
|
|
451
|
+
async init() {
|
|
452
|
+
await ensureDirectories(this.featuresDir);
|
|
453
|
+
}
|
|
454
|
+
// --- Card CRUD ---
|
|
455
|
+
async listCards() {
|
|
456
|
+
await ensureDirectories(this.featuresDir);
|
|
457
|
+
const cards = [];
|
|
458
|
+
const entries = await fs3.readdir(this.featuresDir, { withFileTypes: true });
|
|
459
|
+
for (const entry of entries) {
|
|
460
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
461
|
+
continue;
|
|
462
|
+
const subdir = path4.join(this.featuresDir, entry.name);
|
|
463
|
+
try {
|
|
464
|
+
const mdFiles = await this._readMdFiles(subdir);
|
|
465
|
+
for (const filePath of mdFiles) {
|
|
466
|
+
const card = await this._loadCard(filePath);
|
|
467
|
+
if (card)
|
|
468
|
+
cards.push(card);
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const rootFiles = await this._readMdFiles(this.featuresDir);
|
|
475
|
+
for (const filePath of rootFiles) {
|
|
476
|
+
const card = await this._loadCard(filePath);
|
|
477
|
+
if (card)
|
|
478
|
+
cards.push(card);
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
const numericIds = cards.map((c) => parseInt(c.id, 10)).filter((n) => !Number.isNaN(n));
|
|
483
|
+
if (numericIds.length > 0) {
|
|
484
|
+
syncCardIdCounter(path4.dirname(this.featuresDir), numericIds);
|
|
485
|
+
}
|
|
486
|
+
return cards.sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
|
|
487
|
+
}
|
|
488
|
+
async getCard(cardId) {
|
|
489
|
+
const cards = await this.listCards();
|
|
490
|
+
return cards.find((c) => c.id === cardId) || null;
|
|
491
|
+
}
|
|
492
|
+
async createCard(data) {
|
|
493
|
+
await ensureDirectories(this.featuresDir);
|
|
494
|
+
const status = data.status || "backlog";
|
|
495
|
+
const priority = data.priority || "medium";
|
|
496
|
+
const title = getTitleFromContent(data.content);
|
|
497
|
+
const workspaceRoot = path4.dirname(this.featuresDir);
|
|
498
|
+
const numericId = allocateCardId(workspaceRoot);
|
|
499
|
+
const filename = generateFeatureFilename(numericId, title);
|
|
500
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
501
|
+
const cards = await this.listCards();
|
|
502
|
+
const cardsInStatus = cards.filter((c) => c.status === status).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
|
|
503
|
+
const lastOrder = cardsInStatus.length > 0 ? cardsInStatus[cardsInStatus.length - 1].order : null;
|
|
504
|
+
const card = {
|
|
505
|
+
id: String(numericId),
|
|
506
|
+
status,
|
|
507
|
+
priority,
|
|
508
|
+
assignee: data.assignee ?? null,
|
|
509
|
+
dueDate: data.dueDate ?? null,
|
|
510
|
+
created: now,
|
|
511
|
+
modified: now,
|
|
512
|
+
completedAt: status === "done" ? now : null,
|
|
513
|
+
labels: data.labels || [],
|
|
514
|
+
attachments: data.attachments || [],
|
|
515
|
+
order: generateKeyBetween(lastOrder, null),
|
|
516
|
+
content: data.content,
|
|
517
|
+
filePath: getFeatureFilePath(this.featuresDir, status, filename)
|
|
518
|
+
};
|
|
519
|
+
await fs3.mkdir(path4.dirname(card.filePath), { recursive: true });
|
|
520
|
+
await fs3.writeFile(card.filePath, serializeFeature(card), "utf-8");
|
|
521
|
+
return card;
|
|
522
|
+
}
|
|
523
|
+
async updateCard(cardId, updates) {
|
|
524
|
+
const card = await this.getCard(cardId);
|
|
525
|
+
if (!card)
|
|
526
|
+
throw new Error(`Card not found: ${cardId}`);
|
|
527
|
+
const oldStatus = card.status;
|
|
528
|
+
const oldTitle = getTitleFromContent(card.content);
|
|
529
|
+
const { filePath: _fp, id: _id, ...safeUpdates } = updates;
|
|
530
|
+
Object.assign(card, safeUpdates);
|
|
531
|
+
card.modified = (/* @__PURE__ */ new Date()).toISOString();
|
|
532
|
+
if (oldStatus !== card.status) {
|
|
533
|
+
card.completedAt = card.status === "done" ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
534
|
+
}
|
|
535
|
+
await fs3.writeFile(card.filePath, serializeFeature(card), "utf-8");
|
|
536
|
+
const newTitle = getTitleFromContent(card.content);
|
|
537
|
+
const numericId = extractNumericId(card.id);
|
|
538
|
+
if (numericId !== null && newTitle !== oldTitle) {
|
|
539
|
+
const newFilename = generateFeatureFilename(numericId, newTitle);
|
|
540
|
+
card.filePath = await renameFeatureFile(card.filePath, newFilename);
|
|
541
|
+
}
|
|
542
|
+
if (oldStatus !== card.status) {
|
|
543
|
+
const newPath = await moveFeatureFile(card.filePath, this.featuresDir, card.status, card.attachments);
|
|
544
|
+
card.filePath = newPath;
|
|
545
|
+
}
|
|
546
|
+
return card;
|
|
547
|
+
}
|
|
548
|
+
async moveCard(cardId, newStatus, position) {
|
|
549
|
+
const cards = await this.listCards();
|
|
550
|
+
const card = cards.find((c) => c.id === cardId);
|
|
551
|
+
if (!card)
|
|
552
|
+
throw new Error(`Card not found: ${cardId}`);
|
|
553
|
+
const oldStatus = card.status;
|
|
554
|
+
card.status = newStatus;
|
|
555
|
+
card.modified = (/* @__PURE__ */ new Date()).toISOString();
|
|
556
|
+
if (oldStatus !== newStatus) {
|
|
557
|
+
card.completedAt = newStatus === "done" ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
558
|
+
}
|
|
559
|
+
const targetColumnCards = cards.filter((c) => c.status === newStatus && c.id !== cardId).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
|
|
560
|
+
const pos = position !== void 0 ? Math.max(0, Math.min(position, targetColumnCards.length)) : targetColumnCards.length;
|
|
561
|
+
const before = pos > 0 ? targetColumnCards[pos - 1].order : null;
|
|
562
|
+
const after = pos < targetColumnCards.length ? targetColumnCards[pos].order : null;
|
|
563
|
+
card.order = generateKeyBetween(before, after);
|
|
564
|
+
await fs3.writeFile(card.filePath, serializeFeature(card), "utf-8");
|
|
565
|
+
if (oldStatus !== newStatus) {
|
|
566
|
+
const newPath = await moveFeatureFile(card.filePath, this.featuresDir, newStatus, card.attachments);
|
|
567
|
+
card.filePath = newPath;
|
|
568
|
+
}
|
|
569
|
+
return card;
|
|
570
|
+
}
|
|
571
|
+
async deleteCard(cardId) {
|
|
572
|
+
const card = await this.getCard(cardId);
|
|
573
|
+
if (!card)
|
|
574
|
+
throw new Error(`Card not found: ${cardId}`);
|
|
575
|
+
await fs3.unlink(card.filePath);
|
|
576
|
+
}
|
|
577
|
+
async getCardsByStatus(status) {
|
|
578
|
+
const cards = await this.listCards();
|
|
579
|
+
return cards.filter((c) => c.status === status);
|
|
580
|
+
}
|
|
581
|
+
async getUniqueAssignees() {
|
|
582
|
+
const cards = await this.listCards();
|
|
583
|
+
const assignees = /* @__PURE__ */ new Set();
|
|
584
|
+
for (const c of cards) {
|
|
585
|
+
if (c.assignee)
|
|
586
|
+
assignees.add(c.assignee);
|
|
587
|
+
}
|
|
588
|
+
return [...assignees].sort();
|
|
589
|
+
}
|
|
590
|
+
async getUniqueLabels() {
|
|
591
|
+
const cards = await this.listCards();
|
|
592
|
+
const labels = /* @__PURE__ */ new Set();
|
|
593
|
+
for (const c of cards) {
|
|
594
|
+
for (const l of c.labels)
|
|
595
|
+
labels.add(l);
|
|
596
|
+
}
|
|
597
|
+
return [...labels].sort();
|
|
598
|
+
}
|
|
599
|
+
// --- Attachment management ---
|
|
600
|
+
async addAttachment(cardId, sourcePath) {
|
|
601
|
+
const card = await this.getCard(cardId);
|
|
602
|
+
if (!card)
|
|
603
|
+
throw new Error(`Card not found: ${cardId}`);
|
|
604
|
+
const fileName = path4.basename(sourcePath);
|
|
605
|
+
const cardDir = path4.dirname(card.filePath);
|
|
606
|
+
const destPath = path4.join(cardDir, fileName);
|
|
607
|
+
const sourceDir = path4.dirname(path4.resolve(sourcePath));
|
|
608
|
+
if (sourceDir !== cardDir) {
|
|
609
|
+
await fs3.copyFile(path4.resolve(sourcePath), destPath);
|
|
610
|
+
}
|
|
611
|
+
if (!card.attachments.includes(fileName)) {
|
|
612
|
+
card.attachments.push(fileName);
|
|
613
|
+
}
|
|
614
|
+
card.modified = (/* @__PURE__ */ new Date()).toISOString();
|
|
615
|
+
await fs3.writeFile(card.filePath, serializeFeature(card), "utf-8");
|
|
616
|
+
return card;
|
|
617
|
+
}
|
|
618
|
+
async removeAttachment(cardId, attachment) {
|
|
619
|
+
const card = await this.getCard(cardId);
|
|
620
|
+
if (!card)
|
|
621
|
+
throw new Error(`Card not found: ${cardId}`);
|
|
622
|
+
card.attachments = card.attachments.filter((a) => a !== attachment);
|
|
623
|
+
card.modified = (/* @__PURE__ */ new Date()).toISOString();
|
|
624
|
+
await fs3.writeFile(card.filePath, serializeFeature(card), "utf-8");
|
|
625
|
+
return card;
|
|
626
|
+
}
|
|
627
|
+
async listAttachments(cardId) {
|
|
628
|
+
const card = await this.getCard(cardId);
|
|
629
|
+
if (!card)
|
|
630
|
+
throw new Error(`Card not found: ${cardId}`);
|
|
631
|
+
return card.attachments;
|
|
632
|
+
}
|
|
633
|
+
// --- Column management ---
|
|
634
|
+
async listColumns() {
|
|
635
|
+
const config = await this._readBoardConfig();
|
|
636
|
+
return config.columns;
|
|
637
|
+
}
|
|
638
|
+
async addColumn(column) {
|
|
639
|
+
const config = await this._readBoardConfig();
|
|
640
|
+
if (config.columns.some((c) => c.id === column.id)) {
|
|
641
|
+
throw new Error(`Column already exists: ${column.id}`);
|
|
642
|
+
}
|
|
643
|
+
config.columns.push(column);
|
|
644
|
+
await this._writeBoardConfig(config);
|
|
645
|
+
return config.columns;
|
|
646
|
+
}
|
|
647
|
+
async updateColumn(columnId, updates) {
|
|
648
|
+
const config = await this._readBoardConfig();
|
|
649
|
+
const col = config.columns.find((c) => c.id === columnId);
|
|
650
|
+
if (!col)
|
|
651
|
+
throw new Error(`Column not found: ${columnId}`);
|
|
652
|
+
if (updates.name !== void 0)
|
|
653
|
+
col.name = updates.name;
|
|
654
|
+
if (updates.color !== void 0)
|
|
655
|
+
col.color = updates.color;
|
|
656
|
+
await this._writeBoardConfig(config);
|
|
657
|
+
return config.columns;
|
|
658
|
+
}
|
|
659
|
+
async removeColumn(columnId) {
|
|
660
|
+
const config = await this._readBoardConfig();
|
|
661
|
+
const idx = config.columns.findIndex((c) => c.id === columnId);
|
|
662
|
+
if (idx === -1)
|
|
663
|
+
throw new Error(`Column not found: ${columnId}`);
|
|
664
|
+
const cards = await this.listCards();
|
|
665
|
+
const cardsInColumn = cards.filter((c) => c.status === columnId);
|
|
666
|
+
if (cardsInColumn.length > 0) {
|
|
667
|
+
throw new Error(`Cannot remove column "${columnId}": ${cardsInColumn.length} card(s) still in this column`);
|
|
668
|
+
}
|
|
669
|
+
config.columns.splice(idx, 1);
|
|
670
|
+
await this._writeBoardConfig(config);
|
|
671
|
+
return config.columns;
|
|
672
|
+
}
|
|
673
|
+
async reorderColumns(columnIds) {
|
|
674
|
+
const config = await this._readBoardConfig();
|
|
675
|
+
const colMap = new Map(config.columns.map((c) => [c.id, c]));
|
|
676
|
+
for (const id of columnIds) {
|
|
677
|
+
if (!colMap.has(id))
|
|
678
|
+
throw new Error(`Column not found: ${id}`);
|
|
679
|
+
}
|
|
680
|
+
if (columnIds.length !== config.columns.length) {
|
|
681
|
+
throw new Error("Must include all column IDs when reordering");
|
|
682
|
+
}
|
|
683
|
+
config.columns = columnIds.map((id) => colMap.get(id));
|
|
684
|
+
await this._writeBoardConfig(config);
|
|
685
|
+
return config.columns;
|
|
686
|
+
}
|
|
687
|
+
// --- Private helpers ---
|
|
688
|
+
async _readMdFiles(dir) {
|
|
689
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
690
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => path4.join(dir, e.name));
|
|
691
|
+
}
|
|
692
|
+
async _loadCard(filePath) {
|
|
693
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
694
|
+
return parseFeatureFile(content, filePath);
|
|
695
|
+
}
|
|
696
|
+
_boardConfigPath() {
|
|
697
|
+
return path4.join(this.featuresDir, BOARD_CONFIG_FILE);
|
|
698
|
+
}
|
|
699
|
+
async _readBoardConfig() {
|
|
700
|
+
try {
|
|
701
|
+
const raw = await fs3.readFile(this._boardConfigPath(), "utf-8");
|
|
702
|
+
return JSON.parse(raw);
|
|
703
|
+
} catch {
|
|
704
|
+
return { columns: [...DEFAULT_COLUMNS] };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async _writeBoardConfig(config) {
|
|
708
|
+
await ensureDirectories(this.featuresDir);
|
|
709
|
+
await fs3.writeFile(this._boardConfigPath(), JSON.stringify(config, null, 2), "utf-8");
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
// src/standalone/webhooks.ts
|
|
714
|
+
var fs4 = __toESM(require("fs"));
|
|
715
|
+
var path5 = __toESM(require("path"));
|
|
716
|
+
var crypto = __toESM(require("crypto"));
|
|
717
|
+
var WEBHOOKS_FILENAME = ".kanban-webhooks.json";
|
|
718
|
+
function webhooksPath(workspaceRoot) {
|
|
719
|
+
return path5.join(workspaceRoot, WEBHOOKS_FILENAME);
|
|
720
|
+
}
|
|
721
|
+
function loadWebhooks(workspaceRoot) {
|
|
722
|
+
try {
|
|
723
|
+
const raw = fs4.readFileSync(webhooksPath(workspaceRoot), "utf-8");
|
|
724
|
+
const data = JSON.parse(raw);
|
|
725
|
+
return Array.isArray(data) ? data : [];
|
|
726
|
+
} catch {
|
|
727
|
+
return [];
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function saveWebhooks(workspaceRoot, webhooks) {
|
|
731
|
+
fs4.writeFileSync(webhooksPath(workspaceRoot), JSON.stringify(webhooks, null, 2) + "\n", "utf-8");
|
|
732
|
+
}
|
|
733
|
+
function createWebhook(workspaceRoot, config) {
|
|
734
|
+
const webhooks = loadWebhooks(workspaceRoot);
|
|
735
|
+
const webhook = {
|
|
736
|
+
id: "wh_" + crypto.randomBytes(8).toString("hex"),
|
|
737
|
+
url: config.url,
|
|
738
|
+
events: config.events,
|
|
739
|
+
secret: config.secret,
|
|
740
|
+
active: true
|
|
741
|
+
};
|
|
742
|
+
webhooks.push(webhook);
|
|
743
|
+
saveWebhooks(workspaceRoot, webhooks);
|
|
744
|
+
return webhook;
|
|
745
|
+
}
|
|
746
|
+
function deleteWebhook(workspaceRoot, id) {
|
|
747
|
+
const webhooks = loadWebhooks(workspaceRoot);
|
|
748
|
+
const filtered = webhooks.filter((w) => w.id !== id);
|
|
749
|
+
if (filtered.length === webhooks.length)
|
|
750
|
+
return false;
|
|
751
|
+
saveWebhooks(workspaceRoot, filtered);
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/mcp-server/index.ts
|
|
756
|
+
async function findWorkspaceRoot(startDir) {
|
|
757
|
+
let dir = startDir;
|
|
758
|
+
while (true) {
|
|
759
|
+
try {
|
|
760
|
+
await fs5.access(path6.join(dir, ".git"));
|
|
761
|
+
return dir;
|
|
762
|
+
} catch {
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
await fs5.access(path6.join(dir, "package.json"));
|
|
766
|
+
return dir;
|
|
767
|
+
} catch {
|
|
768
|
+
}
|
|
769
|
+
const parent = path6.dirname(dir);
|
|
770
|
+
if (parent === dir)
|
|
771
|
+
return startDir;
|
|
772
|
+
dir = parent;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async function resolveFeaturesDir() {
|
|
776
|
+
const dirIndex = process.argv.indexOf("--dir");
|
|
777
|
+
if (dirIndex !== -1 && process.argv[dirIndex + 1]) {
|
|
778
|
+
return path6.resolve(process.argv[dirIndex + 1]);
|
|
779
|
+
}
|
|
780
|
+
if (process.env.KANBAN_FEATURES_DIR) {
|
|
781
|
+
return path6.resolve(process.env.KANBAN_FEATURES_DIR);
|
|
782
|
+
}
|
|
783
|
+
const root = await findWorkspaceRoot(process.cwd());
|
|
784
|
+
return path6.join(root, ".devtool", "features");
|
|
785
|
+
}
|
|
786
|
+
function getTitleFromContent2(content) {
|
|
787
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
788
|
+
if (match)
|
|
789
|
+
return match[1].trim();
|
|
790
|
+
const firstLine = content.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
791
|
+
return firstLine || "Untitled";
|
|
792
|
+
}
|
|
793
|
+
async function main() {
|
|
794
|
+
const featuresDir = await resolveFeaturesDir();
|
|
795
|
+
const sdk = new KanbanSDK(featuresDir);
|
|
796
|
+
const server = new import_mcp.McpServer({
|
|
797
|
+
name: "kanban-lite",
|
|
798
|
+
version: "1.0.0"
|
|
799
|
+
});
|
|
800
|
+
server.tool(
|
|
801
|
+
"list_cards",
|
|
802
|
+
"List all kanban cards. Optionally filter by status, priority, assignee, or label.",
|
|
803
|
+
{
|
|
804
|
+
status: import_zod.z.enum(["backlog", "todo", "in-progress", "review", "done"]).optional().describe("Filter by status"),
|
|
805
|
+
priority: import_zod.z.enum(["critical", "high", "medium", "low"]).optional().describe("Filter by priority"),
|
|
806
|
+
assignee: import_zod.z.string().optional().describe("Filter by assignee name"),
|
|
807
|
+
label: import_zod.z.string().optional().describe("Filter by label")
|
|
808
|
+
},
|
|
809
|
+
async ({ status, priority, assignee, label }) => {
|
|
810
|
+
let cards = await sdk.listCards();
|
|
811
|
+
if (status)
|
|
812
|
+
cards = cards.filter((c) => c.status === status);
|
|
813
|
+
if (priority)
|
|
814
|
+
cards = cards.filter((c) => c.priority === priority);
|
|
815
|
+
if (assignee)
|
|
816
|
+
cards = cards.filter((c) => c.assignee === assignee);
|
|
817
|
+
if (label)
|
|
818
|
+
cards = cards.filter((c) => c.labels.includes(label));
|
|
819
|
+
const summary = cards.map((c) => ({
|
|
820
|
+
id: c.id,
|
|
821
|
+
title: getTitleFromContent2(c.content),
|
|
822
|
+
status: c.status,
|
|
823
|
+
priority: c.priority,
|
|
824
|
+
assignee: c.assignee,
|
|
825
|
+
labels: c.labels,
|
|
826
|
+
dueDate: c.dueDate
|
|
827
|
+
}));
|
|
828
|
+
return {
|
|
829
|
+
content: [{
|
|
830
|
+
type: "text",
|
|
831
|
+
text: JSON.stringify(summary, null, 2)
|
|
832
|
+
}]
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
);
|
|
836
|
+
server.tool(
|
|
837
|
+
"get_card",
|
|
838
|
+
"Get full details of a specific kanban card by ID. Supports partial ID matching.",
|
|
839
|
+
{
|
|
840
|
+
cardId: import_zod.z.string().describe("Card ID (or partial ID)")
|
|
841
|
+
},
|
|
842
|
+
async ({ cardId }) => {
|
|
843
|
+
let card = await sdk.getCard(cardId);
|
|
844
|
+
if (!card) {
|
|
845
|
+
const all = await sdk.listCards();
|
|
846
|
+
const matches = all.filter((c) => c.id.includes(cardId));
|
|
847
|
+
if (matches.length === 1) {
|
|
848
|
+
card = matches[0];
|
|
849
|
+
} else if (matches.length > 1) {
|
|
850
|
+
return {
|
|
851
|
+
content: [{ type: "text", text: `Multiple cards match "${cardId}": ${matches.map((m) => m.id).join(", ")}` }],
|
|
852
|
+
isError: true
|
|
853
|
+
};
|
|
854
|
+
} else {
|
|
855
|
+
return {
|
|
856
|
+
content: [{ type: "text", text: `Card not found: ${cardId}` }],
|
|
857
|
+
isError: true
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
content: [{
|
|
863
|
+
type: "text",
|
|
864
|
+
text: JSON.stringify(card, null, 2)
|
|
865
|
+
}]
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
);
|
|
869
|
+
server.tool(
|
|
870
|
+
"create_card",
|
|
871
|
+
"Create a new kanban card. Returns the created card.",
|
|
872
|
+
{
|
|
873
|
+
title: import_zod.z.string().describe("Card title"),
|
|
874
|
+
body: import_zod.z.string().optional().describe("Card body/description (markdown)"),
|
|
875
|
+
status: import_zod.z.enum(["backlog", "todo", "in-progress", "review", "done"]).optional().describe("Initial status (default: backlog)"),
|
|
876
|
+
priority: import_zod.z.enum(["critical", "high", "medium", "low"]).optional().describe("Priority level (default: medium)"),
|
|
877
|
+
assignee: import_zod.z.string().optional().describe("Assignee name"),
|
|
878
|
+
dueDate: import_zod.z.string().optional().describe("Due date (ISO format or YYYY-MM-DD)"),
|
|
879
|
+
labels: import_zod.z.array(import_zod.z.string()).optional().describe("Labels/tags")
|
|
880
|
+
},
|
|
881
|
+
async ({ title, body, status, priority, assignee, dueDate, labels }) => {
|
|
882
|
+
const content = `# ${title}${body ? "\n\n" + body : ""}`;
|
|
883
|
+
const card = await sdk.createCard({
|
|
884
|
+
content,
|
|
885
|
+
status,
|
|
886
|
+
priority,
|
|
887
|
+
assignee: assignee || null,
|
|
888
|
+
dueDate: dueDate || null,
|
|
889
|
+
labels: labels || []
|
|
890
|
+
});
|
|
891
|
+
return {
|
|
892
|
+
content: [{
|
|
893
|
+
type: "text",
|
|
894
|
+
text: JSON.stringify(card, null, 2)
|
|
895
|
+
}]
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
);
|
|
899
|
+
server.tool(
|
|
900
|
+
"update_card",
|
|
901
|
+
"Update fields of an existing kanban card. Only specified fields are changed.",
|
|
902
|
+
{
|
|
903
|
+
cardId: import_zod.z.string().describe("Card ID (or partial ID)"),
|
|
904
|
+
status: import_zod.z.enum(["backlog", "todo", "in-progress", "review", "done"]).optional().describe("New status"),
|
|
905
|
+
priority: import_zod.z.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
|
|
906
|
+
assignee: import_zod.z.string().optional().describe("New assignee"),
|
|
907
|
+
dueDate: import_zod.z.string().optional().describe("New due date"),
|
|
908
|
+
labels: import_zod.z.array(import_zod.z.string()).optional().describe("New labels (replaces existing)"),
|
|
909
|
+
content: import_zod.z.string().optional().describe("New markdown content (replaces existing body)")
|
|
910
|
+
},
|
|
911
|
+
async ({ cardId, status, priority, assignee, dueDate, labels, content }) => {
|
|
912
|
+
let resolvedId = cardId;
|
|
913
|
+
const card = await sdk.getCard(cardId);
|
|
914
|
+
if (!card) {
|
|
915
|
+
const all = await sdk.listCards();
|
|
916
|
+
const matches = all.filter((c) => c.id.includes(cardId));
|
|
917
|
+
if (matches.length === 1) {
|
|
918
|
+
resolvedId = matches[0].id;
|
|
919
|
+
} else if (matches.length > 1) {
|
|
920
|
+
return {
|
|
921
|
+
content: [{ type: "text", text: `Multiple cards match "${cardId}": ${matches.map((m) => m.id).join(", ")}` }],
|
|
922
|
+
isError: true
|
|
923
|
+
};
|
|
924
|
+
} else {
|
|
925
|
+
return {
|
|
926
|
+
content: [{ type: "text", text: `Card not found: ${cardId}` }],
|
|
927
|
+
isError: true
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const updates = {};
|
|
932
|
+
if (status)
|
|
933
|
+
updates.status = status;
|
|
934
|
+
if (priority)
|
|
935
|
+
updates.priority = priority;
|
|
936
|
+
if (assignee !== void 0)
|
|
937
|
+
updates.assignee = assignee || null;
|
|
938
|
+
if (dueDate !== void 0)
|
|
939
|
+
updates.dueDate = dueDate || null;
|
|
940
|
+
if (labels)
|
|
941
|
+
updates.labels = labels;
|
|
942
|
+
if (content !== void 0)
|
|
943
|
+
updates.content = content;
|
|
944
|
+
const updated = await sdk.updateCard(resolvedId, updates);
|
|
945
|
+
return {
|
|
946
|
+
content: [{
|
|
947
|
+
type: "text",
|
|
948
|
+
text: JSON.stringify(updated, null, 2)
|
|
949
|
+
}]
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
);
|
|
953
|
+
server.tool(
|
|
954
|
+
"move_card",
|
|
955
|
+
"Move a kanban card to a different status column.",
|
|
956
|
+
{
|
|
957
|
+
cardId: import_zod.z.string().describe("Card ID (or partial ID)"),
|
|
958
|
+
status: import_zod.z.enum(["backlog", "todo", "in-progress", "review", "done"]).describe("Target status column")
|
|
959
|
+
},
|
|
960
|
+
async ({ cardId, status }) => {
|
|
961
|
+
let resolvedId = cardId;
|
|
962
|
+
const card = await sdk.getCard(cardId);
|
|
963
|
+
if (!card) {
|
|
964
|
+
const all = await sdk.listCards();
|
|
965
|
+
const matches = all.filter((c) => c.id.includes(cardId));
|
|
966
|
+
if (matches.length === 1) {
|
|
967
|
+
resolvedId = matches[0].id;
|
|
968
|
+
} else if (matches.length > 1) {
|
|
969
|
+
return {
|
|
970
|
+
content: [{ type: "text", text: `Multiple cards match "${cardId}": ${matches.map((m) => m.id).join(", ")}` }],
|
|
971
|
+
isError: true
|
|
972
|
+
};
|
|
973
|
+
} else {
|
|
974
|
+
return {
|
|
975
|
+
content: [{ type: "text", text: `Card not found: ${cardId}` }],
|
|
976
|
+
isError: true
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const updated = await sdk.moveCard(resolvedId, status);
|
|
981
|
+
return {
|
|
982
|
+
content: [{
|
|
983
|
+
type: "text",
|
|
984
|
+
text: JSON.stringify({ id: updated.id, status: updated.status, order: updated.order }, null, 2)
|
|
985
|
+
}]
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
);
|
|
989
|
+
server.tool(
|
|
990
|
+
"delete_card",
|
|
991
|
+
"Permanently delete a kanban card.",
|
|
992
|
+
{
|
|
993
|
+
cardId: import_zod.z.string().describe("Card ID (or partial ID)")
|
|
994
|
+
},
|
|
995
|
+
async ({ cardId }) => {
|
|
996
|
+
let resolvedId = cardId;
|
|
997
|
+
const card = await sdk.getCard(cardId);
|
|
998
|
+
if (!card) {
|
|
999
|
+
const all = await sdk.listCards();
|
|
1000
|
+
const matches = all.filter((c) => c.id.includes(cardId));
|
|
1001
|
+
if (matches.length === 1) {
|
|
1002
|
+
resolvedId = matches[0].id;
|
|
1003
|
+
} else if (matches.length > 1) {
|
|
1004
|
+
return {
|
|
1005
|
+
content: [{ type: "text", text: `Multiple cards match "${cardId}": ${matches.map((m) => m.id).join(", ")}` }],
|
|
1006
|
+
isError: true
|
|
1007
|
+
};
|
|
1008
|
+
} else {
|
|
1009
|
+
return {
|
|
1010
|
+
content: [{ type: "text", text: `Card not found: ${cardId}` }],
|
|
1011
|
+
isError: true
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
await sdk.deleteCard(resolvedId);
|
|
1016
|
+
return {
|
|
1017
|
+
content: [{
|
|
1018
|
+
type: "text",
|
|
1019
|
+
text: `Deleted card: ${resolvedId}`
|
|
1020
|
+
}]
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
);
|
|
1024
|
+
server.tool(
|
|
1025
|
+
"list_attachments",
|
|
1026
|
+
"List all attachments on a kanban card.",
|
|
1027
|
+
{
|
|
1028
|
+
cardId: import_zod.z.string().describe("Card ID (or partial ID)")
|
|
1029
|
+
},
|
|
1030
|
+
async ({ cardId }) => {
|
|
1031
|
+
let resolvedId = cardId;
|
|
1032
|
+
const card = await sdk.getCard(cardId);
|
|
1033
|
+
if (!card) {
|
|
1034
|
+
const all = await sdk.listCards();
|
|
1035
|
+
const matches = all.filter((c) => c.id.includes(cardId));
|
|
1036
|
+
if (matches.length === 1) {
|
|
1037
|
+
resolvedId = matches[0].id;
|
|
1038
|
+
} else if (matches.length > 1) {
|
|
1039
|
+
return {
|
|
1040
|
+
content: [{ type: "text", text: `Multiple cards match "${cardId}": ${matches.map((m) => m.id).join(", ")}` }],
|
|
1041
|
+
isError: true
|
|
1042
|
+
};
|
|
1043
|
+
} else {
|
|
1044
|
+
return {
|
|
1045
|
+
content: [{ type: "text", text: `Card not found: ${cardId}` }],
|
|
1046
|
+
isError: true
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const attachments = await sdk.listAttachments(resolvedId);
|
|
1051
|
+
return {
|
|
1052
|
+
content: [{
|
|
1053
|
+
type: "text",
|
|
1054
|
+
text: JSON.stringify(attachments, null, 2)
|
|
1055
|
+
}]
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
);
|
|
1059
|
+
server.tool(
|
|
1060
|
+
"add_attachment",
|
|
1061
|
+
"Add a file attachment to a kanban card. Copies the file to the card directory.",
|
|
1062
|
+
{
|
|
1063
|
+
cardId: import_zod.z.string().describe("Card ID (or partial ID)"),
|
|
1064
|
+
filePath: import_zod.z.string().describe("Absolute path to the file to attach")
|
|
1065
|
+
},
|
|
1066
|
+
async ({ cardId, filePath }) => {
|
|
1067
|
+
let resolvedId = cardId;
|
|
1068
|
+
const card = await sdk.getCard(cardId);
|
|
1069
|
+
if (!card) {
|
|
1070
|
+
const all = await sdk.listCards();
|
|
1071
|
+
const matches = all.filter((c) => c.id.includes(cardId));
|
|
1072
|
+
if (matches.length === 1) {
|
|
1073
|
+
resolvedId = matches[0].id;
|
|
1074
|
+
} else if (matches.length > 1) {
|
|
1075
|
+
return {
|
|
1076
|
+
content: [{ type: "text", text: `Multiple cards match "${cardId}": ${matches.map((m) => m.id).join(", ")}` }],
|
|
1077
|
+
isError: true
|
|
1078
|
+
};
|
|
1079
|
+
} else {
|
|
1080
|
+
return {
|
|
1081
|
+
content: [{ type: "text", text: `Card not found: ${cardId}` }],
|
|
1082
|
+
isError: true
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
const updated = await sdk.addAttachment(resolvedId, filePath);
|
|
1087
|
+
return {
|
|
1088
|
+
content: [{
|
|
1089
|
+
type: "text",
|
|
1090
|
+
text: JSON.stringify({ id: updated.id, attachments: updated.attachments }, null, 2)
|
|
1091
|
+
}]
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
);
|
|
1095
|
+
server.tool(
|
|
1096
|
+
"remove_attachment",
|
|
1097
|
+
"Remove an attachment from a kanban card. Only removes the reference, not the file.",
|
|
1098
|
+
{
|
|
1099
|
+
cardId: import_zod.z.string().describe("Card ID (or partial ID)"),
|
|
1100
|
+
attachment: import_zod.z.string().describe("Attachment filename to remove")
|
|
1101
|
+
},
|
|
1102
|
+
async ({ cardId, attachment }) => {
|
|
1103
|
+
let resolvedId = cardId;
|
|
1104
|
+
const card = await sdk.getCard(cardId);
|
|
1105
|
+
if (!card) {
|
|
1106
|
+
const all = await sdk.listCards();
|
|
1107
|
+
const matches = all.filter((c) => c.id.includes(cardId));
|
|
1108
|
+
if (matches.length === 1) {
|
|
1109
|
+
resolvedId = matches[0].id;
|
|
1110
|
+
} else if (matches.length > 1) {
|
|
1111
|
+
return {
|
|
1112
|
+
content: [{ type: "text", text: `Multiple cards match "${cardId}": ${matches.map((m) => m.id).join(", ")}` }],
|
|
1113
|
+
isError: true
|
|
1114
|
+
};
|
|
1115
|
+
} else {
|
|
1116
|
+
return {
|
|
1117
|
+
content: [{ type: "text", text: `Card not found: ${cardId}` }],
|
|
1118
|
+
isError: true
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
const updated = await sdk.removeAttachment(resolvedId, attachment);
|
|
1123
|
+
return {
|
|
1124
|
+
content: [{
|
|
1125
|
+
type: "text",
|
|
1126
|
+
text: JSON.stringify({ id: updated.id, attachments: updated.attachments }, null, 2)
|
|
1127
|
+
}]
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
);
|
|
1131
|
+
server.tool(
|
|
1132
|
+
"list_columns",
|
|
1133
|
+
"List all kanban board columns.",
|
|
1134
|
+
{},
|
|
1135
|
+
async () => {
|
|
1136
|
+
const columns = await sdk.listColumns();
|
|
1137
|
+
return {
|
|
1138
|
+
content: [{
|
|
1139
|
+
type: "text",
|
|
1140
|
+
text: JSON.stringify(columns, null, 2)
|
|
1141
|
+
}]
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
);
|
|
1145
|
+
server.tool(
|
|
1146
|
+
"add_column",
|
|
1147
|
+
"Add a new column to the kanban board.",
|
|
1148
|
+
{
|
|
1149
|
+
id: import_zod.z.string().describe("Unique column ID (used in card status field)"),
|
|
1150
|
+
name: import_zod.z.string().describe("Display name for the column"),
|
|
1151
|
+
color: import_zod.z.string().describe('Column color (hex format, e.g. "#3b82f6")')
|
|
1152
|
+
},
|
|
1153
|
+
async ({ id, name, color }) => {
|
|
1154
|
+
const columns = await sdk.addColumn({ id, name, color });
|
|
1155
|
+
return {
|
|
1156
|
+
content: [{
|
|
1157
|
+
type: "text",
|
|
1158
|
+
text: JSON.stringify(columns, null, 2)
|
|
1159
|
+
}]
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
);
|
|
1163
|
+
server.tool(
|
|
1164
|
+
"update_column",
|
|
1165
|
+
"Update an existing kanban board column.",
|
|
1166
|
+
{
|
|
1167
|
+
columnId: import_zod.z.string().describe("Column ID to update"),
|
|
1168
|
+
name: import_zod.z.string().optional().describe("New display name"),
|
|
1169
|
+
color: import_zod.z.string().optional().describe("New color (hex format)")
|
|
1170
|
+
},
|
|
1171
|
+
async ({ columnId, name, color }) => {
|
|
1172
|
+
const updates = {};
|
|
1173
|
+
if (name)
|
|
1174
|
+
updates.name = name;
|
|
1175
|
+
if (color)
|
|
1176
|
+
updates.color = color;
|
|
1177
|
+
const columns = await sdk.updateColumn(columnId, updates);
|
|
1178
|
+
return {
|
|
1179
|
+
content: [{
|
|
1180
|
+
type: "text",
|
|
1181
|
+
text: JSON.stringify(columns, null, 2)
|
|
1182
|
+
}]
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
);
|
|
1186
|
+
server.tool(
|
|
1187
|
+
"remove_column",
|
|
1188
|
+
"Remove a column from the kanban board. Fails if any cards are in the column.",
|
|
1189
|
+
{
|
|
1190
|
+
columnId: import_zod.z.string().describe("Column ID to remove")
|
|
1191
|
+
},
|
|
1192
|
+
async ({ columnId }) => {
|
|
1193
|
+
const columns = await sdk.removeColumn(columnId);
|
|
1194
|
+
return {
|
|
1195
|
+
content: [{
|
|
1196
|
+
type: "text",
|
|
1197
|
+
text: JSON.stringify(columns, null, 2)
|
|
1198
|
+
}]
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
);
|
|
1202
|
+
const workspaceRoot = path6.dirname(featuresDir);
|
|
1203
|
+
server.tool(
|
|
1204
|
+
"get_settings",
|
|
1205
|
+
"Get the current kanban board display settings.",
|
|
1206
|
+
{},
|
|
1207
|
+
async () => {
|
|
1208
|
+
const config = readConfig(workspaceRoot);
|
|
1209
|
+
const settings = configToSettings(config);
|
|
1210
|
+
return {
|
|
1211
|
+
content: [{
|
|
1212
|
+
type: "text",
|
|
1213
|
+
text: JSON.stringify(settings, null, 2)
|
|
1214
|
+
}]
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
);
|
|
1218
|
+
server.tool(
|
|
1219
|
+
"update_settings",
|
|
1220
|
+
"Update kanban board display settings. Only specified fields are changed.",
|
|
1221
|
+
{
|
|
1222
|
+
showPriorityBadges: import_zod.z.boolean().optional().describe("Show priority badges on cards"),
|
|
1223
|
+
showAssignee: import_zod.z.boolean().optional().describe("Show assignee on cards"),
|
|
1224
|
+
showDueDate: import_zod.z.boolean().optional().describe("Show due date on cards"),
|
|
1225
|
+
showLabels: import_zod.z.boolean().optional().describe("Show labels on cards"),
|
|
1226
|
+
showFileName: import_zod.z.boolean().optional().describe("Show file name on cards"),
|
|
1227
|
+
compactMode: import_zod.z.boolean().optional().describe("Enable compact card display"),
|
|
1228
|
+
defaultPriority: import_zod.z.enum(["critical", "high", "medium", "low"]).optional().describe("Default priority for new cards"),
|
|
1229
|
+
defaultStatus: import_zod.z.enum(["backlog", "todo", "in-progress", "review", "done"]).optional().describe("Default status for new cards")
|
|
1230
|
+
},
|
|
1231
|
+
async (updates) => {
|
|
1232
|
+
const config = readConfig(workspaceRoot);
|
|
1233
|
+
const settings = configToSettings(config);
|
|
1234
|
+
const merged = { ...settings };
|
|
1235
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
1236
|
+
if (value !== void 0) {
|
|
1237
|
+
merged[key] = value;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
writeConfig(workspaceRoot, settingsToConfig(config, merged));
|
|
1241
|
+
const updated = configToSettings(readConfig(workspaceRoot));
|
|
1242
|
+
return {
|
|
1243
|
+
content: [{
|
|
1244
|
+
type: "text",
|
|
1245
|
+
text: JSON.stringify(updated, null, 2)
|
|
1246
|
+
}]
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
);
|
|
1250
|
+
server.tool(
|
|
1251
|
+
"list_webhooks",
|
|
1252
|
+
"List all registered webhooks.",
|
|
1253
|
+
{},
|
|
1254
|
+
async () => {
|
|
1255
|
+
const webhooks = loadWebhooks(workspaceRoot);
|
|
1256
|
+
return {
|
|
1257
|
+
content: [{
|
|
1258
|
+
type: "text",
|
|
1259
|
+
text: JSON.stringify(webhooks, null, 2)
|
|
1260
|
+
}]
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
);
|
|
1264
|
+
server.tool(
|
|
1265
|
+
"add_webhook",
|
|
1266
|
+
"Register a new webhook to receive event notifications.",
|
|
1267
|
+
{
|
|
1268
|
+
url: import_zod.z.string().describe("Webhook target URL (HTTP/HTTPS)"),
|
|
1269
|
+
events: import_zod.z.array(import_zod.z.string()).optional().describe('Events to subscribe to (e.g. ["task.created", "task.updated"]). Default: ["*"] for all.'),
|
|
1270
|
+
secret: import_zod.z.string().optional().describe("Optional HMAC-SHA256 signing secret")
|
|
1271
|
+
},
|
|
1272
|
+
async ({ url, events, secret }) => {
|
|
1273
|
+
const webhook = createWebhook(workspaceRoot, {
|
|
1274
|
+
url,
|
|
1275
|
+
events: events || ["*"],
|
|
1276
|
+
secret
|
|
1277
|
+
});
|
|
1278
|
+
return {
|
|
1279
|
+
content: [{
|
|
1280
|
+
type: "text",
|
|
1281
|
+
text: JSON.stringify(webhook, null, 2)
|
|
1282
|
+
}]
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
);
|
|
1286
|
+
server.tool(
|
|
1287
|
+
"remove_webhook",
|
|
1288
|
+
"Remove a registered webhook by ID.",
|
|
1289
|
+
{
|
|
1290
|
+
webhookId: import_zod.z.string().describe('Webhook ID (e.g. "wh_abc123")')
|
|
1291
|
+
},
|
|
1292
|
+
async ({ webhookId }) => {
|
|
1293
|
+
const removed = deleteWebhook(workspaceRoot, webhookId);
|
|
1294
|
+
if (!removed) {
|
|
1295
|
+
return {
|
|
1296
|
+
content: [{ type: "text", text: `Webhook not found: ${webhookId}` }],
|
|
1297
|
+
isError: true
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
content: [{
|
|
1302
|
+
type: "text",
|
|
1303
|
+
text: `Deleted webhook: ${webhookId}`
|
|
1304
|
+
}]
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
);
|
|
1308
|
+
server.tool(
|
|
1309
|
+
"get_workspace_info",
|
|
1310
|
+
"Get the workspace root path and features directory.",
|
|
1311
|
+
{},
|
|
1312
|
+
async () => {
|
|
1313
|
+
return {
|
|
1314
|
+
content: [{
|
|
1315
|
+
type: "text",
|
|
1316
|
+
text: JSON.stringify({ workspaceRoot, featuresDir }, null, 2)
|
|
1317
|
+
}]
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
);
|
|
1321
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
1322
|
+
await server.connect(transport);
|
|
1323
|
+
}
|
|
1324
|
+
main().catch((err) => {
|
|
1325
|
+
console.error(`MCP Server error: ${err.message}`);
|
|
1326
|
+
process.exit(1);
|
|
1327
|
+
});
|