@cwim/kanban 1.1.20 → 1.2.1

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.
@@ -1,49 +1,52 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
- import { execSync } from 'child_process';
6
- import { listTasks, getTask, createTask, updateTask, moveTask, deleteTask, appendNote, recallTasks, listAllSessions, getCurrentSessionName, setActiveSession, } from '../storage/store.js';
7
- import { VALID_STATUSES } from '../types.js';
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { execSync } from "child_process";
6
+ import { listTasks, getTask, createTask, updateTask, moveTask, deleteTask, appendNote, recallTasks, listAllSessions, getCurrentSessionName, setActiveSession, } from "../storage/store.js";
7
+ import { VALID_STATUSES } from "../types.js";
8
8
  function detectAgent() {
9
9
  if (process.env.CLAUDE_CODE)
10
- return 'claude';
10
+ return "claude";
11
11
  if (process.env.OPENCODE)
12
- return 'opencode';
12
+ return "opencode";
13
13
  // Try to detect parent process name cross-platform
14
14
  if (process.ppid) {
15
15
  try {
16
- let parentProcess = '';
17
- if (process.platform === 'win32') {
16
+ let parentProcess = "";
17
+ if (process.platform === "win32") {
18
18
  // Windows: use wmic or tasklist
19
19
  try {
20
- parentProcess = execSync(`wmic process where "ProcessId=${process.ppid}" get Name /value`, { encoding: 'utf8', timeout: 5000 });
20
+ parentProcess = execSync(`wmic process where "ProcessId=${process.ppid}" get Name /value`, { encoding: "utf8", timeout: 5000 });
21
21
  }
22
22
  catch {
23
23
  // Fallback to tasklist
24
- parentProcess = execSync(`tasklist /FI "PID eq ${process.ppid}" /FO CSV /NH`, { encoding: 'utf8', timeout: 5000 });
24
+ parentProcess = execSync(`tasklist /FI "PID eq ${process.ppid}" /FO CSV /NH`, { encoding: "utf8", timeout: 5000 });
25
25
  }
26
26
  }
27
27
  else {
28
28
  // Unix-like (macOS, Linux)
29
- parentProcess = execSync(`ps -p ${process.ppid} -o comm=`, { encoding: 'utf8', timeout: 5000 }).trim();
29
+ parentProcess = execSync(`ps -p ${process.ppid} -o comm=`, {
30
+ encoding: "utf8",
31
+ timeout: 5000,
32
+ }).trim();
30
33
  }
31
- if (parentProcess.toLowerCase().includes('claude'))
32
- return 'claude';
33
- if (parentProcess.toLowerCase().includes('opencode'))
34
- return 'opencode';
34
+ if (parentProcess.toLowerCase().includes("claude"))
35
+ return "claude";
36
+ if (parentProcess.toLowerCase().includes("opencode"))
37
+ return "opencode";
35
38
  }
36
39
  catch {
37
40
  // Ignore errors from process detection
38
41
  }
39
42
  }
40
43
  // Default fallback
41
- return 'claude';
44
+ return "claude";
42
45
  }
43
46
  const CURRENT_AGENT = detectAgent();
44
47
  const server = new Server({
45
- name: 'cwim-kanban',
46
- version: '1.1.20',
48
+ name: "cwim-kanban",
49
+ version: "1.2.1",
47
50
  }, {
48
51
  capabilities: {
49
52
  tools: {},
@@ -53,161 +56,164 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
53
56
  return {
54
57
  tools: [
55
58
  {
56
- name: 'task_create',
57
- description: 'Create a new task on the Kanban board',
59
+ name: "task_create",
60
+ description: "Create a new task on the Kanban board",
58
61
  inputSchema: {
59
- type: 'object',
62
+ type: "object",
60
63
  properties: {
61
- title: { type: 'string', description: 'Task title (required)' },
64
+ title: { type: "string", description: "Task title (required)" },
62
65
  description: {
63
- type: 'string',
64
- description: 'Optional task description',
66
+ type: "string",
67
+ description: "Optional task description",
65
68
  },
66
69
  status: {
67
- type: 'string',
70
+ type: "string",
68
71
  enum: VALID_STATUSES,
69
- description: 'Initial status (default: todo)',
72
+ description: "Initial status (default: todo)",
70
73
  },
71
74
  tags: {
72
- type: 'array',
73
- items: { type: 'string' },
74
- description: 'Optional tags for categorization',
75
+ type: "array",
76
+ items: { type: "string" },
77
+ description: "Optional tags for categorization",
75
78
  },
76
79
  },
77
- required: ['title'],
80
+ required: ["title"],
78
81
  },
79
82
  },
80
83
  {
81
- name: 'task_update',
82
- description: 'Update an existing task',
84
+ name: "task_update",
85
+ description: "Update an existing task",
83
86
  inputSchema: {
84
- type: 'object',
87
+ type: "object",
85
88
  properties: {
86
- id: { type: 'string', description: 'Task ID (required)' },
87
- title: { type: 'string' },
88
- description: { type: 'string' },
89
+ id: { type: "string", description: "Task ID (required)" },
90
+ title: { type: "string" },
91
+ description: { type: "string" },
89
92
  status: {
90
- type: 'string',
93
+ type: "string",
91
94
  enum: VALID_STATUSES,
92
95
  },
93
96
  tags: {
94
- type: 'array',
95
- items: { type: 'string' },
97
+ type: "array",
98
+ items: { type: "string" },
96
99
  },
97
100
  },
98
- required: ['id'],
101
+ required: ["id"],
99
102
  },
100
103
  },
101
104
  {
102
- name: 'task_move',
103
- description: 'Move a task to a different status column',
105
+ name: "task_move",
106
+ description: "Move a task to a different status column",
104
107
  inputSchema: {
105
- type: 'object',
108
+ type: "object",
106
109
  properties: {
107
- id: { type: 'string', description: 'Task ID (required)' },
110
+ id: { type: "string", description: "Task ID (required)" },
108
111
  status: {
109
- type: 'string',
112
+ type: "string",
110
113
  enum: VALID_STATUSES,
111
- description: 'New status (required)',
114
+ description: "New status (required)",
112
115
  },
113
116
  },
114
- required: ['id', 'status'],
117
+ required: ["id", "status"],
115
118
  },
116
119
  },
117
120
  {
118
- name: 'task_delete',
119
- description: 'Delete a task from the board',
121
+ name: "task_delete",
122
+ description: "Delete a task from the board",
120
123
  inputSchema: {
121
- type: 'object',
124
+ type: "object",
122
125
  properties: {
123
- id: { type: 'string', description: 'Task ID (required)' },
126
+ id: { type: "string", description: "Task ID (required)" },
124
127
  },
125
- required: ['id'],
128
+ required: ["id"],
126
129
  },
127
130
  },
128
131
  {
129
- name: 'task_list',
130
- description: 'List all tasks in the current session, optionally filtered',
132
+ name: "task_list",
133
+ description: "List all tasks in the current session, optionally filtered",
131
134
  inputSchema: {
132
- type: 'object',
135
+ type: "object",
133
136
  properties: {
134
137
  status: {
135
- type: 'string',
138
+ type: "string",
136
139
  enum: VALID_STATUSES,
137
- description: 'Filter by status',
140
+ description: "Filter by status",
138
141
  },
139
142
  tag: {
140
- type: 'string',
141
- description: 'Filter by tag',
143
+ type: "string",
144
+ description: "Filter by tag",
142
145
  },
143
146
  query: {
144
- type: 'string',
145
- description: 'Search query to filter tasks by title or description (case-insensitive)',
147
+ type: "string",
148
+ description: "Search query to filter tasks by title or description (case-insensitive)",
146
149
  },
147
150
  },
148
151
  },
149
152
  },
150
153
  {
151
- name: 'task_append_note',
152
- description: 'Append a timestamped note to a task without overwriting existing content',
154
+ name: "task_append_note",
155
+ description: "Append a timestamped note to a task without overwriting existing content",
153
156
  inputSchema: {
154
- type: 'object',
157
+ type: "object",
155
158
  properties: {
156
- id: { type: 'string', description: 'Task ID (required)' },
157
- note: { type: 'string', description: 'Note text to append (required)' },
159
+ id: { type: "string", description: "Task ID (required)" },
160
+ note: {
161
+ type: "string",
162
+ description: "Note text to append (required)",
163
+ },
158
164
  },
159
- required: ['id', 'note'],
165
+ required: ["id", "note"],
160
166
  },
161
167
  },
162
168
  {
163
- name: 'task_recall',
164
- description: 'Intelligently recall relevant task context based on current work. Call this before starting complex tasks to check for existing context and avoid duplicating work.',
169
+ name: "task_recall",
170
+ description: "Intelligently recall relevant task context based on current work. Call this before starting complex tasks to check for existing context and avoid duplicating work.",
165
171
  inputSchema: {
166
- type: 'object',
172
+ type: "object",
167
173
  properties: {
168
174
  context: {
169
- type: 'string',
175
+ type: "string",
170
176
  description: 'Brief description of what you are about to work on (e.g., "refactoring auth middleware")',
171
177
  },
172
178
  limit: {
173
- type: 'number',
174
- description: 'Max number of relevant tasks to return (default: 5)',
179
+ type: "number",
180
+ description: "Max number of relevant tasks to return (default: 5)",
175
181
  default: 5,
176
182
  },
177
183
  },
178
184
  },
179
185
  },
180
186
  {
181
- name: 'task_get',
182
- description: 'Get details of a single task',
187
+ name: "task_get",
188
+ description: "Get details of a single task",
183
189
  inputSchema: {
184
- type: 'object',
190
+ type: "object",
185
191
  properties: {
186
- id: { type: 'string', description: 'Task ID (required)' },
192
+ id: { type: "string", description: "Task ID (required)" },
187
193
  },
188
- required: ['id'],
194
+ required: ["id"],
189
195
  },
190
196
  },
191
197
  {
192
- name: 'session_list',
193
- description: 'List all available sessions',
198
+ name: "session_list",
199
+ description: "List all available sessions",
194
200
  inputSchema: {
195
- type: 'object',
201
+ type: "object",
196
202
  properties: {},
197
203
  },
198
204
  },
199
205
  {
200
- name: 'session_switch',
201
- description: 'Switch to a different session',
206
+ name: "session_switch",
207
+ description: "Switch to a different session",
202
208
  inputSchema: {
203
- type: 'object',
209
+ type: "object",
204
210
  properties: {
205
211
  name: {
206
- type: 'string',
207
- description: 'Session name to switch to (required)',
212
+ type: "string",
213
+ description: "Session name to switch to (required)",
208
214
  },
209
215
  },
210
- required: ['name'],
216
+ required: ["name"],
211
217
  },
212
218
  },
213
219
  ],
@@ -217,13 +223,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
217
223
  const { name, arguments: args = {} } = request.params;
218
224
  try {
219
225
  switch (name) {
220
- case 'task_create': {
226
+ case "task_create": {
221
227
  const task = await createTask({
222
228
  title: String(args.title),
223
- description: args.description ? String(args.description) : undefined,
229
+ description: args.description
230
+ ? String(args.description)
231
+ : undefined,
224
232
  status: VALID_STATUSES.includes(args.status)
225
233
  ? args.status
226
- : 'todo',
234
+ : "todo",
227
235
  tags: Array.isArray(args.tags) ? args.tags.map(String) : undefined,
228
236
  source: CURRENT_AGENT,
229
237
  });
@@ -231,16 +239,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
231
239
  return {
232
240
  content: [
233
241
  {
234
- type: 'text',
242
+ type: "text",
235
243
  text: `Created task "${task.title}" (${task.id}) in ${task.status} [Session: ${sessionName}]`,
236
244
  },
237
245
  ],
238
246
  };
239
247
  }
240
- case 'task_update': {
248
+ case "task_update": {
241
249
  const task = await updateTask(String(args.id), {
242
250
  title: args.title !== undefined ? String(args.title) : undefined,
243
- description: args.description !== undefined ? String(args.description) : undefined,
251
+ description: args.description !== undefined
252
+ ? String(args.description)
253
+ : undefined,
244
254
  status: VALID_STATUSES.includes(args.status)
245
255
  ? args.status
246
256
  : undefined,
@@ -248,48 +258,50 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
248
258
  });
249
259
  if (!task) {
250
260
  return {
251
- content: [{ type: 'text', text: `Task ${args.id} not found` }],
261
+ content: [{ type: "text", text: `Task ${args.id} not found` }],
252
262
  isError: true,
253
263
  };
254
264
  }
255
265
  return {
256
266
  content: [
257
267
  {
258
- type: 'text',
268
+ type: "text",
259
269
  text: `Updated task "${task.title}" (${task.id})`,
260
270
  },
261
271
  ],
262
272
  };
263
273
  }
264
- case 'task_move': {
274
+ case "task_move": {
265
275
  const task = await moveTask(String(args.id), args.status);
266
276
  if (!task) {
267
277
  return {
268
- content: [{ type: 'text', text: `Task ${args.id} not found` }],
278
+ content: [{ type: "text", text: `Task ${args.id} not found` }],
269
279
  isError: true,
270
280
  };
271
281
  }
272
282
  return {
273
283
  content: [
274
284
  {
275
- type: 'text',
285
+ type: "text",
276
286
  text: `Moved "${task.title}" to ${task.status}`,
277
287
  },
278
288
  ],
279
289
  };
280
290
  }
281
- case 'task_delete': {
291
+ case "task_delete": {
282
292
  const ok = await deleteTask(String(args.id));
283
293
  return {
284
294
  content: [
285
295
  {
286
- type: 'text',
287
- text: ok ? `Deleted task ${args.id}` : `Task ${args.id} not found`,
296
+ type: "text",
297
+ text: ok
298
+ ? `Deleted task ${args.id}`
299
+ : `Task ${args.id} not found`,
288
300
  },
289
301
  ],
290
302
  };
291
303
  }
292
- case 'task_list': {
304
+ case "task_list": {
293
305
  const tasks = await listTasks(undefined, {
294
306
  status: VALID_STATUSES.includes(args.status)
295
307
  ? args.status
@@ -300,19 +312,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
300
312
  const sessionName = await getCurrentSessionName();
301
313
  if (tasks.length === 0) {
302
314
  return {
303
- content: [{ type: 'text', text: `No tasks found in session "${sessionName}"` }],
315
+ content: [
316
+ {
317
+ type: "text",
318
+ text: `No tasks found in session "${sessionName}"`,
319
+ },
320
+ ],
304
321
  };
305
322
  }
306
- const lines = tasks.map((t) => `[${t.status}] ${t.title} (${t.id}) ${t.tags.length > 0 ? '#' + t.tags.join(' #') : ''}`);
323
+ const lines = tasks.map((t) => `[${t.status}] ${t.title} (${t.id}) ${t.tags.length > 0 ? "#" + t.tags.join(" #") : ""}`);
307
324
  return {
308
- content: [{ type: 'text', text: `Session: ${sessionName}\n${lines.join('\n')}` }],
325
+ content: [
326
+ {
327
+ type: "text",
328
+ text: `Session: ${sessionName}\n${lines.join("\n")}`,
329
+ },
330
+ ],
309
331
  };
310
332
  }
311
- case 'task_append_note': {
333
+ case "task_append_note": {
312
334
  const task = await appendNote(String(args.id), String(args.note));
313
335
  if (!task) {
314
336
  return {
315
- content: [{ type: 'text', text: `Task ${args.id} not found` }],
337
+ content: [{ type: "text", text: `Task ${args.id} not found` }],
316
338
  isError: true,
317
339
  };
318
340
  }
@@ -320,22 +342,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
320
342
  return {
321
343
  content: [
322
344
  {
323
- type: 'text',
345
+ type: "text",
324
346
  text: `Appended note to "${task.title}" (${task.id})\nTotal notes: ${noteCount}`,
325
347
  },
326
348
  ],
327
349
  };
328
350
  }
329
- case 'task_recall': {
330
- const context = args.context ? String(args.context) : '';
331
- const limit = typeof args.limit === 'number' ? args.limit : 5;
351
+ case "task_recall": {
352
+ const context = args.context ? String(args.context) : "";
353
+ const limit = typeof args.limit === "number" ? args.limit : 5;
332
354
  const result = await recallTasks(context, limit);
333
355
  const sessionName = await getCurrentSessionName();
334
356
  if (result.relevant.length === 0) {
335
357
  return {
336
358
  content: [
337
359
  {
338
- type: 'text',
360
+ type: "text",
339
361
  text: `No relevant tasks found in session "${sessionName}".\n\nSession Summary: ${result.summary.active} active, ${result.summary.done} done, ${result.summary.blocked} blocked (total: ${result.summary.total})`,
340
362
  },
341
363
  ],
@@ -344,83 +366,95 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
344
366
  const lines = result.relevant.map((t) => {
345
367
  const notesPreview = t.notes && t.notes.length > 0
346
368
  ? `\n Notes: ${t.notes.length} entries`
347
- : '';
369
+ : "";
348
370
  const reasons = result.reasons.get(t.id);
349
371
  const reasonText = reasons && reasons.length > 0
350
- ? `\n Matched: ${reasons.map(r => `${r.field} "${r.matched}"`).join(', ')}`
351
- : '';
372
+ ? `\n Matched: ${reasons.map((r) => `${r.field} "${r.matched}"`).join(", ")}`
373
+ : "";
352
374
  return `• [${t.status}] ${t.title} (${t.id})${notesPreview}${reasonText}`;
353
375
  });
354
376
  return {
355
377
  content: [
356
378
  {
357
- type: 'text',
358
- text: `Relevant Context Found (${result.relevant.length} tasks):\n\n${lines.join('\n')}\n\nSession Summary: ${result.summary.active} active, ${result.summary.done} done, ${result.summary.blocked} blocked (total: ${result.summary.total})`,
379
+ type: "text",
380
+ text: `Relevant Context Found (${result.relevant.length} tasks):\n\n${lines.join("\n")}\n\nSession Summary: ${result.summary.active} active, ${result.summary.done} done, ${result.summary.blocked} blocked (total: ${result.summary.total})`,
359
381
  },
360
382
  ],
361
383
  };
362
384
  }
363
- case 'task_get': {
385
+ case "task_get": {
364
386
  const task = await getTask(String(args.id));
365
387
  if (!task) {
366
388
  return {
367
- content: [{ type: 'text', text: `Task ${args.id} not found` }],
389
+ content: [{ type: "text", text: `Task ${args.id} not found` }],
368
390
  isError: true,
369
391
  };
370
392
  }
371
393
  const notesText = task.notes?.length
372
- ? `\nNotes (${task.notes.length}):\n${task.notes.join('\n')}`
373
- : '';
394
+ ? `\nNotes (${task.notes.length}):\n${task.notes.join("\n")}`
395
+ : "";
374
396
  return {
375
397
  content: [
376
398
  {
377
- type: 'text',
378
- text: `Task: ${task.title}\nID: ${task.id}\nStatus: ${task.status}\nDescription: ${task.description ?? '(none)'}${notesText}\nTags: ${task.tags.join(', ') || '(none)'}\nCreated: ${task.createdAt}\nUpdated: ${task.updatedAt}`,
399
+ type: "text",
400
+ text: `Task: ${task.title}\nID: ${task.id}\nStatus: ${task.status}\nDescription: ${task.description ?? "(none)"}${notesText}\nTags: ${task.tags.join(", ") || "(none)"}\nCreated: ${task.createdAt}\nUpdated: ${task.updatedAt}`,
379
401
  },
380
402
  ],
381
403
  };
382
404
  }
383
- case 'session_list': {
405
+ case "session_list": {
384
406
  const sessions = await listAllSessions();
385
407
  const active = await getCurrentSessionName();
386
408
  if (sessions.length === 0) {
387
409
  return {
388
- content: [{ type: 'text', text: 'No sessions found' }],
410
+ content: [{ type: "text", text: "No sessions found" }],
389
411
  };
390
412
  }
391
413
  const lines = sessions.map((s) => {
392
- const marker = s.name === active ? ' *' : '';
414
+ const marker = s.name === active ? " *" : "";
393
415
  return `- ${s.name}${marker}`;
394
416
  });
395
417
  return {
396
- content: [{ type: 'text', text: `Active: ${active}\n\n${lines.join('\n')}` }],
418
+ content: [
419
+ {
420
+ type: "text",
421
+ text: `Active: ${active}\n\n${lines.join("\n")}`,
422
+ },
423
+ ],
397
424
  };
398
425
  }
399
- case 'session_switch': {
426
+ case "session_switch": {
400
427
  const targetName = String(args.name);
401
428
  const sessions = await listAllSessions();
402
429
  const exists = sessions.some((s) => s.name === targetName);
403
430
  if (!exists) {
404
431
  return {
405
- content: [{ type: 'text', text: `Session "${targetName}" not found. Use session_list to see available sessions.` }],
432
+ content: [
433
+ {
434
+ type: "text",
435
+ text: `Session "${targetName}" not found. Use session_list to see available sessions.`,
436
+ },
437
+ ],
406
438
  isError: true,
407
439
  };
408
440
  }
409
441
  await setActiveSession(targetName);
410
442
  return {
411
- content: [{ type: 'text', text: `Switched to session "${targetName}"` }],
443
+ content: [
444
+ { type: "text", text: `Switched to session "${targetName}"` },
445
+ ],
412
446
  };
413
447
  }
414
448
  default:
415
449
  return {
416
- content: [{ type: 'text', text: `Unknown tool: ${name}` }],
450
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
417
451
  isError: true,
418
452
  };
419
453
  }
420
454
  }
421
455
  catch (err) {
422
456
  return {
423
- content: [{ type: 'text', text: `Error: ${err.message}` }],
457
+ content: [{ type: "text", text: `Error: ${err.message}` }],
424
458
  isError: true,
425
459
  };
426
460
  }