kanban-lite 1.0.9 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cli.js +1015 -407
  2. package/dist/extension.js +897 -347
  3. package/dist/mcp-server.js +550 -189
  4. package/dist/sdk/index.cjs +1223 -0
  5. package/dist/sdk/index.mjs +1168 -0
  6. package/dist/sdk/sdk/KanbanSDK.d.ts +39 -21
  7. package/dist/sdk/sdk/index.d.ts +4 -3
  8. package/dist/sdk/sdk/migration.d.ts +6 -0
  9. package/dist/sdk/sdk/types.d.ts +3 -5
  10. package/dist/sdk/shared/config.d.ts +23 -10
  11. package/dist/sdk/shared/types.d.ts +18 -4
  12. package/dist/standalone-webview/index.js +27 -27
  13. package/dist/standalone-webview/index.js.map +1 -1
  14. package/dist/standalone-webview/style.css +1 -1
  15. package/dist/standalone.js +831 -328
  16. package/dist/webview/index.js +45 -45
  17. package/dist/webview/index.js.map +1 -1
  18. package/dist/webview/style.css +1 -1
  19. package/package.json +4 -3
  20. package/src/cli/index.ts +217 -95
  21. package/src/extension/KanbanPanel.ts +49 -22
  22. package/src/mcp-server/index.ts +138 -62
  23. package/src/sdk/KanbanSDK.ts +283 -77
  24. package/src/sdk/__tests__/KanbanSDK.test.ts +5 -5
  25. package/src/sdk/__tests__/migration.test.ts +269 -0
  26. package/src/sdk/__tests__/multi-board.test.ts +449 -0
  27. package/src/sdk/index.ts +4 -3
  28. package/src/sdk/migration.ts +52 -0
  29. package/src/sdk/types.ts +3 -6
  30. package/src/shared/config.ts +144 -22
  31. package/src/shared/types.ts +14 -5
  32. package/src/standalone/__tests__/server.integration.test.ts +38 -37
  33. package/src/standalone/server.ts +239 -21
  34. package/src/webview/App.tsx +17 -7
  35. package/src/webview/components/Toolbar.tsx +99 -3
  36. package/src/webview/store/index.ts +11 -3
  37. package/.kanban/backlog/1-test1.md +0 -30
  38. package/.kanban.json +0 -42
@@ -1,41 +1,214 @@
1
1
  import * as fs from 'fs/promises'
2
2
  import * as path from 'path'
3
3
  import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing'
4
- import type { Comment, Feature, FeatureStatus, KanbanColumn } from '../shared/types'
4
+ import type { Comment, Feature, KanbanColumn, BoardInfo } from '../shared/types'
5
5
  import { getTitleFromContent, generateFeatureFilename, extractNumericId } from '../shared/types'
6
- import { readConfig, writeConfig, configToSettings, settingsToConfig, allocateCardId, syncCardIdCounter } from '../shared/config'
6
+ import { readConfig, writeConfig, configToSettings, settingsToConfig, allocateCardId, syncCardIdCounter, getBoardConfig } from '../shared/config'
7
+ import type { BoardConfig } from '../shared/config'
7
8
  import type { CardDisplaySettings } from '../shared/types'
9
+ import type { Priority } from '../shared/types'
8
10
  import { parseFeatureFile, serializeFeature } from './parser'
9
11
  import { ensureDirectories, ensureStatusSubfolders, getFeatureFilePath, getStatusFromPath, moveFeatureFile, renameFeatureFile } from './fileUtils'
10
12
  import type { CreateCardInput } from './types'
13
+ import { migrateFileSystemToMultiBoard } from './migration'
11
14
 
12
15
  export class KanbanSDK {
16
+ private _migrated = false
17
+
13
18
  constructor(public readonly featuresDir: string) {}
14
19
 
15
20
  get workspaceRoot(): string {
16
21
  return path.dirname(this.featuresDir)
17
22
  }
18
23
 
24
+ // --- Board resolution helpers ---
25
+
26
+ private _resolveBoardId(boardId?: string): string {
27
+ const config = readConfig(this.workspaceRoot)
28
+ return boardId || config.defaultBoard
29
+ }
30
+
31
+ private _boardDir(boardId?: string): string {
32
+ const resolvedId = this._resolveBoardId(boardId)
33
+ return path.join(this.featuresDir, 'boards', resolvedId)
34
+ }
35
+
36
+ private _isCompletedStatus(status: string, boardId?: string): boolean {
37
+ const config = readConfig(this.workspaceRoot)
38
+ const resolvedId = boardId || config.defaultBoard
39
+ const board = config.boards[resolvedId]
40
+ if (!board || board.columns.length === 0) return status === 'done'
41
+ return board.columns[board.columns.length - 1].id === status
42
+ }
43
+
44
+ private async _ensureMigrated(): Promise<void> {
45
+ if (this._migrated) return
46
+ await migrateFileSystemToMultiBoard(this.featuresDir)
47
+ this._migrated = true
48
+ }
49
+
19
50
  async init(): Promise<void> {
20
- await ensureDirectories(this.featuresDir)
51
+ await this._ensureMigrated()
52
+ const boardDir = this._boardDir()
53
+ await ensureDirectories(boardDir)
54
+ }
55
+
56
+ // --- Board management ---
57
+
58
+ listBoards(): BoardInfo[] {
59
+ const config = readConfig(this.workspaceRoot)
60
+ return Object.entries(config.boards).map(([id, board]) => ({
61
+ id,
62
+ name: board.name,
63
+ description: board.description
64
+ }))
65
+ }
66
+
67
+ createBoard(id: string, name: string, options?: {
68
+ description?: string
69
+ columns?: KanbanColumn[]
70
+ defaultStatus?: string
71
+ defaultPriority?: Priority
72
+ }): BoardInfo {
73
+ const config = readConfig(this.workspaceRoot)
74
+ if (config.boards[id]) {
75
+ throw new Error(`Board already exists: ${id}`)
76
+ }
77
+
78
+ const columns = options?.columns || [...config.boards[config.defaultBoard]?.columns || [
79
+ { id: 'backlog', name: 'Backlog', color: '#6b7280' },
80
+ { id: 'todo', name: 'To Do', color: '#3b82f6' },
81
+ { id: 'in-progress', name: 'In Progress', color: '#f59e0b' },
82
+ { id: 'review', name: 'Review', color: '#8b5cf6' },
83
+ { id: 'done', name: 'Done', color: '#22c55e' }
84
+ ]]
85
+
86
+ config.boards[id] = {
87
+ name,
88
+ description: options?.description,
89
+ columns,
90
+ nextCardId: 1,
91
+ defaultStatus: options?.defaultStatus || columns[0]?.id || 'backlog',
92
+ defaultPriority: options?.defaultPriority || config.defaultPriority
93
+ }
94
+ writeConfig(this.workspaceRoot, config)
95
+
96
+ return { id, name, description: options?.description }
97
+ }
98
+
99
+ async deleteBoard(boardId: string): Promise<void> {
100
+ const config = readConfig(this.workspaceRoot)
101
+ if (!config.boards[boardId]) {
102
+ throw new Error(`Board not found: ${boardId}`)
103
+ }
104
+ if (config.defaultBoard === boardId) {
105
+ throw new Error(`Cannot delete the default board: ${boardId}`)
106
+ }
107
+
108
+ // Check if board has cards
109
+ const cards = await this.listCards(undefined, boardId)
110
+ if (cards.length > 0) {
111
+ throw new Error(`Cannot delete board "${boardId}": ${cards.length} card(s) still exist`)
112
+ }
113
+
114
+ // Remove board directory
115
+ const boardDir = this._boardDir(boardId)
116
+ try {
117
+ await fs.rm(boardDir, { recursive: true })
118
+ } catch {
119
+ // Directory might not exist
120
+ }
121
+
122
+ delete config.boards[boardId]
123
+ writeConfig(this.workspaceRoot, config)
124
+ }
125
+
126
+ getBoard(boardId: string): BoardConfig {
127
+ return getBoardConfig(this.workspaceRoot, boardId)
128
+ }
129
+
130
+ updateBoard(boardId: string, updates: Partial<Omit<BoardConfig, 'nextCardId'>>): BoardConfig {
131
+ const config = readConfig(this.workspaceRoot)
132
+ const board = config.boards[boardId]
133
+ if (!board) {
134
+ throw new Error(`Board not found: ${boardId}`)
135
+ }
136
+
137
+ if (updates.name !== undefined) board.name = updates.name
138
+ if (updates.description !== undefined) board.description = updates.description
139
+ if (updates.columns !== undefined) board.columns = updates.columns
140
+ if (updates.defaultStatus !== undefined) board.defaultStatus = updates.defaultStatus
141
+ if (updates.defaultPriority !== undefined) board.defaultPriority = updates.defaultPriority
142
+
143
+ writeConfig(this.workspaceRoot, config)
144
+ return board
145
+ }
146
+
147
+ async transferCard(cardId: string, fromBoardId: string, toBoardId: string, targetStatus?: string): Promise<Feature> {
148
+ const toBoardDir = this._boardDir(toBoardId)
149
+
150
+ const config = readConfig(this.workspaceRoot)
151
+ if (!config.boards[fromBoardId]) throw new Error(`Board not found: ${fromBoardId}`)
152
+ if (!config.boards[toBoardId]) throw new Error(`Board not found: ${toBoardId}`)
153
+
154
+ // Find card in source board
155
+ const card = await this.getCard(cardId, fromBoardId)
156
+ if (!card) throw new Error(`Card not found: ${cardId} in board ${fromBoardId}`)
157
+
158
+ // Determine target status
159
+ const toBoard = config.boards[toBoardId]
160
+ const newStatus = targetStatus || toBoard.defaultStatus || toBoard.columns[0]?.id || 'backlog'
161
+
162
+ // Ensure target directory exists
163
+ const targetDir = path.join(toBoardDir, newStatus)
164
+ await fs.mkdir(targetDir, { recursive: true })
165
+
166
+ // Move file
167
+ const oldPath = card.filePath
168
+ const filename = path.basename(oldPath)
169
+ const newPath = path.join(targetDir, filename)
170
+ await fs.rename(oldPath, newPath)
171
+
172
+ // Update card metadata
173
+ card.status = newStatus
174
+ card.boardId = toBoardId
175
+ card.filePath = newPath
176
+ card.modified = new Date().toISOString()
177
+ card.completedAt = this._isCompletedStatus(newStatus, toBoardId) ? new Date().toISOString() : null
178
+
179
+ // Recompute order for target column
180
+ const targetCards = await this.listCards(undefined, toBoardId)
181
+ const cardsInStatus = targetCards
182
+ .filter(c => c.status === newStatus && c.id !== cardId)
183
+ .sort((a, b) => (a.order < b.order ? -1 : a.order > b.order ? 1 : 0))
184
+ const lastOrder = cardsInStatus.length > 0 ? cardsInStatus[cardsInStatus.length - 1].order : null
185
+ card.order = generateKeyBetween(lastOrder, null)
186
+
187
+ await fs.writeFile(card.filePath, serializeFeature(card), 'utf-8')
188
+
189
+ return card
21
190
  }
22
191
 
23
192
  // --- Card CRUD ---
24
193
 
25
- async listCards(columns?: string[]): Promise<Feature[]> {
26
- await ensureDirectories(this.featuresDir)
194
+ async listCards(columns?: string[], boardId?: string): Promise<Feature[]> {
195
+ await this._ensureMigrated()
196
+ const boardDir = this._boardDir(boardId)
197
+ const resolvedBoardId = this._resolveBoardId(boardId)
198
+
199
+ await ensureDirectories(boardDir)
27
200
  if (columns) {
28
- await ensureStatusSubfolders(this.featuresDir, columns)
201
+ await ensureStatusSubfolders(boardDir, columns)
29
202
  }
30
203
 
31
204
  // Phase 1: Migrate flat root .md files into their status subfolder
32
205
  try {
33
- const rootFiles = await this._readMdFiles(this.featuresDir)
206
+ const rootFiles = await this._readMdFiles(boardDir)
34
207
  for (const filePath of rootFiles) {
35
208
  try {
36
209
  const card = await this._loadCard(filePath)
37
210
  if (card) {
38
- await moveFeatureFile(filePath, this.featuresDir, card.status, card.attachments)
211
+ await moveFeatureFile(filePath, boardDir, card.status, card.attachments)
39
212
  }
40
213
  } catch {
41
214
  // Skip files that fail to migrate
@@ -47,15 +220,23 @@ export class KanbanSDK {
47
220
 
48
221
  // Phase 2: Load .md files from ALL subdirectories
49
222
  const cards: Feature[] = []
50
- const entries = await fs.readdir(this.featuresDir, { withFileTypes: true })
223
+ let entries: import('fs').Dirent[]
224
+ try {
225
+ entries = await fs.readdir(boardDir, { withFileTypes: true }) as import('fs').Dirent[]
226
+ } catch {
227
+ return []
228
+ }
51
229
  for (const entry of entries) {
52
230
  if (!entry.isDirectory() || entry.name.startsWith('.')) continue
53
- const subdir = path.join(this.featuresDir, entry.name)
231
+ const subdir = path.join(boardDir, entry.name)
54
232
  try {
55
233
  const mdFiles = await this._readMdFiles(subdir)
56
234
  for (const filePath of mdFiles) {
57
235
  const card = await this._loadCard(filePath)
58
- if (card) cards.push(card)
236
+ if (card) {
237
+ card.boardId = resolvedBoardId
238
+ cards.push(card)
239
+ }
59
240
  }
60
241
  } catch {
61
242
  // Skip unreadable directories
@@ -64,10 +245,10 @@ export class KanbanSDK {
64
245
 
65
246
  // Phase 3: Reconcile status ↔ folder mismatches
66
247
  for (const card of cards) {
67
- const pathStatus = getStatusFromPath(card.filePath, this.featuresDir)
248
+ const pathStatus = getStatusFromPath(card.filePath, boardDir)
68
249
  if (pathStatus !== null && pathStatus !== card.status) {
69
250
  try {
70
- card.filePath = await moveFeatureFile(card.filePath, this.featuresDir, card.status, card.attachments)
251
+ card.filePath = await moveFeatureFile(card.filePath, boardDir, card.status, card.attachments)
71
252
  } catch {
72
253
  // Will retry on next load
73
254
  }
@@ -98,30 +279,35 @@ export class KanbanSDK {
98
279
  .map(c => parseInt(c.id, 10))
99
280
  .filter(n => !Number.isNaN(n))
100
281
  if (numericIds.length > 0) {
101
- syncCardIdCounter(path.dirname(this.featuresDir), numericIds)
282
+ syncCardIdCounter(this.workspaceRoot, resolvedBoardId, numericIds)
102
283
  }
103
284
 
104
285
  return cards.sort((a, b) => (a.order < b.order ? -1 : a.order > b.order ? 1 : 0))
105
286
  }
106
287
 
107
- async getCard(cardId: string): Promise<Feature | null> {
108
- const cards = await this.listCards()
288
+ async getCard(cardId: string, boardId?: string): Promise<Feature | null> {
289
+ const cards = await this.listCards(undefined, boardId)
109
290
  return cards.find(c => c.id === cardId) || null
110
291
  }
111
292
 
112
293
  async createCard(data: CreateCardInput): Promise<Feature> {
113
- await ensureDirectories(this.featuresDir)
294
+ await this._ensureMigrated()
295
+ const resolvedBoardId = this._resolveBoardId(data.boardId)
296
+ const boardDir = this._boardDir(resolvedBoardId)
297
+ await ensureDirectories(boardDir)
298
+
299
+ const config = readConfig(this.workspaceRoot)
300
+ const board = config.boards[resolvedBoardId]
114
301
 
115
- const status = data.status || 'backlog'
116
- const priority = data.priority || 'medium'
302
+ const status = data.status || board?.defaultStatus || config.defaultStatus || 'backlog'
303
+ const priority = data.priority || board?.defaultPriority || config.defaultPriority || 'medium'
117
304
  const title = getTitleFromContent(data.content)
118
- const workspaceRoot = path.dirname(this.featuresDir)
119
- const numericId = allocateCardId(workspaceRoot)
305
+ const numericId = allocateCardId(this.workspaceRoot, resolvedBoardId)
120
306
  const filename = generateFeatureFilename(numericId, title)
121
307
  const now = new Date().toISOString()
122
308
 
123
309
  // Compute order: place at end of target column
124
- const cards = await this.listCards()
310
+ const cards = await this.listCards(undefined, resolvedBoardId)
125
311
  const cardsInStatus = cards
126
312
  .filter(c => c.status === status)
127
313
  .sort((a, b) => (a.order < b.order ? -1 : a.order > b.order ? 1 : 0))
@@ -131,19 +317,20 @@ export class KanbanSDK {
131
317
 
132
318
  const card: Feature = {
133
319
  id: String(numericId),
320
+ boardId: resolvedBoardId,
134
321
  status,
135
322
  priority,
136
323
  assignee: data.assignee ?? null,
137
324
  dueDate: data.dueDate ?? null,
138
325
  created: now,
139
326
  modified: now,
140
- completedAt: status === 'done' ? now : null,
327
+ completedAt: this._isCompletedStatus(status, resolvedBoardId) ? now : null,
141
328
  labels: data.labels || [],
142
329
  attachments: data.attachments || [],
143
330
  comments: [],
144
331
  order: generateKeyBetween(lastOrder, null),
145
332
  content: data.content,
146
- filePath: getFeatureFilePath(this.featuresDir, status, filename)
333
+ filePath: getFeatureFilePath(boardDir, status, filename)
147
334
  }
148
335
 
149
336
  await fs.mkdir(path.dirname(card.filePath), { recursive: true })
@@ -152,20 +339,22 @@ export class KanbanSDK {
152
339
  return card
153
340
  }
154
341
 
155
- async updateCard(cardId: string, updates: Partial<Feature>): Promise<Feature> {
156
- const card = await this.getCard(cardId)
342
+ async updateCard(cardId: string, updates: Partial<Feature>, boardId?: string): Promise<Feature> {
343
+ const card = await this.getCard(cardId, boardId)
157
344
  if (!card) throw new Error(`Card not found: ${cardId}`)
158
345
 
346
+ const resolvedBoardId = card.boardId || this._resolveBoardId(boardId)
347
+ const boardDir = this._boardDir(resolvedBoardId)
159
348
  const oldStatus = card.status
160
349
  const oldTitle = getTitleFromContent(card.content)
161
350
 
162
- // Merge updates (exclude filePath/id from being overwritten)
163
- const { filePath: _fp, id: _id, ...safeUpdates } = updates
351
+ // Merge updates (exclude filePath/id/boardId from being overwritten)
352
+ const { filePath: _fp, id: _id, boardId: _bid, ...safeUpdates } = updates
164
353
  Object.assign(card, safeUpdates)
165
354
  card.modified = new Date().toISOString()
166
355
 
167
356
  if (oldStatus !== card.status) {
168
- card.completedAt = card.status === 'done' ? new Date().toISOString() : null
357
+ card.completedAt = this._isCompletedStatus(card.status, resolvedBoardId) ? new Date().toISOString() : null
169
358
  }
170
359
 
171
360
  // Write updated content
@@ -181,24 +370,26 @@ export class KanbanSDK {
181
370
 
182
371
  // Move file if status changed
183
372
  if (oldStatus !== card.status) {
184
- const newPath = await moveFeatureFile(card.filePath, this.featuresDir, card.status, card.attachments)
373
+ const newPath = await moveFeatureFile(card.filePath, boardDir, card.status, card.attachments)
185
374
  card.filePath = newPath
186
375
  }
187
376
 
188
377
  return card
189
378
  }
190
379
 
191
- async moveCard(cardId: string, newStatus: FeatureStatus, position?: number): Promise<Feature> {
192
- const cards = await this.listCards()
380
+ async moveCard(cardId: string, newStatus: string, position?: number, boardId?: string): Promise<Feature> {
381
+ const cards = await this.listCards(undefined, boardId)
193
382
  const card = cards.find(c => c.id === cardId)
194
383
  if (!card) throw new Error(`Card not found: ${cardId}`)
195
384
 
385
+ const resolvedBoardId = card.boardId || this._resolveBoardId(boardId)
386
+ const boardDir = this._boardDir(resolvedBoardId)
196
387
  const oldStatus = card.status
197
388
  card.status = newStatus
198
389
  card.modified = new Date().toISOString()
199
390
 
200
391
  if (oldStatus !== newStatus) {
201
- card.completedAt = newStatus === 'done' ? new Date().toISOString() : null
392
+ card.completedAt = this._isCompletedStatus(newStatus, resolvedBoardId) ? new Date().toISOString() : null
202
393
  }
203
394
 
204
395
  // Compute new fractional order
@@ -218,26 +409,26 @@ export class KanbanSDK {
218
409
 
219
410
  // Move file if status changed
220
411
  if (oldStatus !== newStatus) {
221
- const newPath = await moveFeatureFile(card.filePath, this.featuresDir, newStatus, card.attachments)
412
+ const newPath = await moveFeatureFile(card.filePath, boardDir, newStatus, card.attachments)
222
413
  card.filePath = newPath
223
414
  }
224
415
 
225
416
  return card
226
417
  }
227
418
 
228
- async deleteCard(cardId: string): Promise<void> {
229
- const card = await this.getCard(cardId)
419
+ async deleteCard(cardId: string, boardId?: string): Promise<void> {
420
+ const card = await this.getCard(cardId, boardId)
230
421
  if (!card) throw new Error(`Card not found: ${cardId}`)
231
422
  await fs.unlink(card.filePath)
232
423
  }
233
424
 
234
- async getCardsByStatus(status: FeatureStatus): Promise<Feature[]> {
235
- const cards = await this.listCards()
425
+ async getCardsByStatus(status: string, boardId?: string): Promise<Feature[]> {
426
+ const cards = await this.listCards(undefined, boardId)
236
427
  return cards.filter(c => c.status === status)
237
428
  }
238
429
 
239
- async getUniqueAssignees(): Promise<string[]> {
240
- const cards = await this.listCards()
430
+ async getUniqueAssignees(boardId?: string): Promise<string[]> {
431
+ const cards = await this.listCards(undefined, boardId)
241
432
  const assignees = new Set<string>()
242
433
  for (const c of cards) {
243
434
  if (c.assignee) assignees.add(c.assignee)
@@ -245,8 +436,8 @@ export class KanbanSDK {
245
436
  return [...assignees].sort()
246
437
  }
247
438
 
248
- async getUniqueLabels(): Promise<string[]> {
249
- const cards = await this.listCards()
439
+ async getUniqueLabels(boardId?: string): Promise<string[]> {
440
+ const cards = await this.listCards(undefined, boardId)
250
441
  const labels = new Set<string>()
251
442
  for (const c of cards) {
252
443
  for (const l of c.labels) labels.add(l)
@@ -256,8 +447,8 @@ export class KanbanSDK {
256
447
 
257
448
  // --- Attachment management ---
258
449
 
259
- async addAttachment(cardId: string, sourcePath: string): Promise<Feature> {
260
- const card = await this.getCard(cardId)
450
+ async addAttachment(cardId: string, sourcePath: string, boardId?: string): Promise<Feature> {
451
+ const card = await this.getCard(cardId, boardId)
261
452
  if (!card) throw new Error(`Card not found: ${cardId}`)
262
453
 
263
454
  const fileName = path.basename(sourcePath)
@@ -281,8 +472,8 @@ export class KanbanSDK {
281
472
  return card
282
473
  }
283
474
 
284
- async removeAttachment(cardId: string, attachment: string): Promise<Feature> {
285
- const card = await this.getCard(cardId)
475
+ async removeAttachment(cardId: string, attachment: string, boardId?: string): Promise<Feature> {
476
+ const card = await this.getCard(cardId, boardId)
286
477
  if (!card) throw new Error(`Card not found: ${cardId}`)
287
478
 
288
479
  card.attachments = card.attachments.filter(a => a !== attachment)
@@ -292,22 +483,22 @@ export class KanbanSDK {
292
483
  return card
293
484
  }
294
485
 
295
- async listAttachments(cardId: string): Promise<string[]> {
296
- const card = await this.getCard(cardId)
486
+ async listAttachments(cardId: string, boardId?: string): Promise<string[]> {
487
+ const card = await this.getCard(cardId, boardId)
297
488
  if (!card) throw new Error(`Card not found: ${cardId}`)
298
489
  return card.attachments
299
490
  }
300
491
 
301
492
  // --- Comment management ---
302
493
 
303
- async listComments(cardId: string): Promise<Comment[]> {
304
- const card = await this.getCard(cardId)
494
+ async listComments(cardId: string, boardId?: string): Promise<Comment[]> {
495
+ const card = await this.getCard(cardId, boardId)
305
496
  if (!card) throw new Error(`Card not found: ${cardId}`)
306
497
  return card.comments || []
307
498
  }
308
499
 
309
- async addComment(cardId: string, author: string, content: string): Promise<Feature> {
310
- const card = await this.getCard(cardId)
500
+ async addComment(cardId: string, author: string, content: string, boardId?: string): Promise<Feature> {
501
+ const card = await this.getCard(cardId, boardId)
311
502
  if (!card) throw new Error(`Card not found: ${cardId}`)
312
503
 
313
504
  if (!card.comments) card.comments = []
@@ -332,8 +523,8 @@ export class KanbanSDK {
332
523
  return card
333
524
  }
334
525
 
335
- async updateComment(cardId: string, commentId: string, content: string): Promise<Feature> {
336
- const card = await this.getCard(cardId)
526
+ async updateComment(cardId: string, commentId: string, content: string, boardId?: string): Promise<Feature> {
527
+ const card = await this.getCard(cardId, boardId)
337
528
  if (!card) throw new Error(`Card not found: ${cardId}`)
338
529
 
339
530
  const comment = (card.comments || []).find(c => c.id === commentId)
@@ -346,8 +537,8 @@ export class KanbanSDK {
346
537
  return card
347
538
  }
348
539
 
349
- async deleteComment(cardId: string, commentId: string): Promise<Feature> {
350
- const card = await this.getCard(cardId)
540
+ async deleteComment(cardId: string, commentId: string, boardId?: string): Promise<Feature> {
541
+ const card = await this.getCard(cardId, boardId)
351
542
  if (!card) throw new Error(`Card not found: ${cardId}`)
352
543
 
353
544
  card.comments = (card.comments || []).filter(c => c.id !== commentId)
@@ -357,67 +548,82 @@ export class KanbanSDK {
357
548
  return card
358
549
  }
359
550
 
360
- // --- Column management ---
551
+ // --- Column management (board-scoped) ---
361
552
 
362
- listColumns(): KanbanColumn[] {
363
- return readConfig(this.workspaceRoot).columns
553
+ listColumns(boardId?: string): KanbanColumn[] {
554
+ const config = readConfig(this.workspaceRoot)
555
+ const resolvedId = boardId || config.defaultBoard
556
+ const board = config.boards[resolvedId]
557
+ return board?.columns || []
364
558
  }
365
559
 
366
- addColumn(column: KanbanColumn): KanbanColumn[] {
560
+ addColumn(column: KanbanColumn, boardId?: string): KanbanColumn[] {
367
561
  const config = readConfig(this.workspaceRoot)
368
- if (config.columns.some(c => c.id === column.id)) {
562
+ const resolvedId = boardId || config.defaultBoard
563
+ const board = config.boards[resolvedId]
564
+ if (!board) throw new Error(`Board not found: ${resolvedId}`)
565
+ if (board.columns.some(c => c.id === column.id)) {
369
566
  throw new Error(`Column already exists: ${column.id}`)
370
567
  }
371
- config.columns.push(column)
568
+ board.columns.push(column)
372
569
  writeConfig(this.workspaceRoot, config)
373
- return config.columns
570
+ return board.columns
374
571
  }
375
572
 
376
- updateColumn(columnId: string, updates: Partial<Omit<KanbanColumn, 'id'>>): KanbanColumn[] {
573
+ updateColumn(columnId: string, updates: Partial<Omit<KanbanColumn, 'id'>>, boardId?: string): KanbanColumn[] {
377
574
  const config = readConfig(this.workspaceRoot)
378
- const col = config.columns.find(c => c.id === columnId)
575
+ const resolvedId = boardId || config.defaultBoard
576
+ const board = config.boards[resolvedId]
577
+ if (!board) throw new Error(`Board not found: ${resolvedId}`)
578
+ const col = board.columns.find(c => c.id === columnId)
379
579
  if (!col) throw new Error(`Column not found: ${columnId}`)
380
580
  if (updates.name !== undefined) col.name = updates.name
381
581
  if (updates.color !== undefined) col.color = updates.color
382
582
  writeConfig(this.workspaceRoot, config)
383
- return config.columns
583
+ return board.columns
384
584
  }
385
585
 
386
- async removeColumn(columnId: string): Promise<KanbanColumn[]> {
586
+ async removeColumn(columnId: string, boardId?: string): Promise<KanbanColumn[]> {
387
587
  const config = readConfig(this.workspaceRoot)
388
- const idx = config.columns.findIndex(c => c.id === columnId)
588
+ const resolvedId = boardId || config.defaultBoard
589
+ const board = config.boards[resolvedId]
590
+ if (!board) throw new Error(`Board not found: ${resolvedId}`)
591
+ const idx = board.columns.findIndex(c => c.id === columnId)
389
592
  if (idx === -1) throw new Error(`Column not found: ${columnId}`)
390
593
 
391
594
  // Check if any cards use this column
392
- const cards = await this.listCards()
595
+ const cards = await this.listCards(undefined, resolvedId)
393
596
  const cardsInColumn = cards.filter(c => c.status === columnId)
394
597
  if (cardsInColumn.length > 0) {
395
598
  throw new Error(`Cannot remove column "${columnId}": ${cardsInColumn.length} card(s) still in this column`)
396
599
  }
397
600
 
398
- config.columns.splice(idx, 1)
601
+ board.columns.splice(idx, 1)
399
602
  writeConfig(this.workspaceRoot, config)
400
- return config.columns
603
+ return board.columns
401
604
  }
402
605
 
403
- reorderColumns(columnIds: string[]): KanbanColumn[] {
606
+ reorderColumns(columnIds: string[], boardId?: string): KanbanColumn[] {
404
607
  const config = readConfig(this.workspaceRoot)
405
- const colMap = new Map(config.columns.map(c => [c.id, c]))
608
+ const resolvedId = boardId || config.defaultBoard
609
+ const board = config.boards[resolvedId]
610
+ if (!board) throw new Error(`Board not found: ${resolvedId}`)
611
+ const colMap = new Map(board.columns.map(c => [c.id, c]))
406
612
 
407
613
  // Validate all IDs exist
408
614
  for (const id of columnIds) {
409
615
  if (!colMap.has(id)) throw new Error(`Column not found: ${id}`)
410
616
  }
411
- if (columnIds.length !== config.columns.length) {
617
+ if (columnIds.length !== board.columns.length) {
412
618
  throw new Error('Must include all column IDs when reordering')
413
619
  }
414
620
 
415
- config.columns = columnIds.map(id => colMap.get(id) as KanbanColumn)
621
+ board.columns = columnIds.map(id => colMap.get(id) as KanbanColumn)
416
622
  writeConfig(this.workspaceRoot, config)
417
- return config.columns
623
+ return board.columns
418
624
  }
419
625
 
420
- // --- Settings management ---
626
+ // --- Settings management (global) ---
421
627
 
422
628
  getSettings(): CardDisplaySettings {
423
629
  return configToSettings(readConfig(this.workspaceRoot))
@@ -9,7 +9,7 @@ function createTempDir(): string {
9
9
  }
10
10
 
11
11
  function writeCardFile(dir: string, filename: string, content: string, subfolder?: string): void {
12
- const targetDir = subfolder ? path.join(dir, subfolder) : dir
12
+ const targetDir = subfolder ? path.join(dir, 'boards', 'default', subfolder) : dir
13
13
  fs.mkdirSync(targetDir, { recursive: true })
14
14
  fs.writeFileSync(path.join(targetDir, filename), content, 'utf-8')
15
15
  }
@@ -217,7 +217,7 @@ describe('KanbanSDK', () => {
217
217
  const updated = await sdk.updateCard('move-status', { status: 'in-progress' })
218
218
  expect(updated.filePath).toContain('/in-progress/')
219
219
  expect(fs.existsSync(updated.filePath)).toBe(true)
220
- expect(fs.existsSync(path.join(tempDir, 'backlog', 'move-status.md'))).toBe(false)
220
+ expect(fs.existsSync(path.join(tempDir, 'boards', 'default', 'backlog', 'move-status.md'))).toBe(false)
221
221
  })
222
222
 
223
223
  it('should throw for non-existent card', async () => {
@@ -261,7 +261,7 @@ describe('KanbanSDK', () => {
261
261
  describe('deleteCard', () => {
262
262
  it('should remove the file from disk', async () => {
263
263
  writeCardFile(tempDir, 'delete-me.md', makeCardContent({ id: 'delete-me' }), 'backlog')
264
- const filePath = path.join(tempDir, 'backlog', 'delete-me.md')
264
+ const filePath = path.join(tempDir, 'boards', 'default', 'backlog', 'delete-me.md')
265
265
  expect(fs.existsSync(filePath)).toBe(true)
266
266
 
267
267
  await sdk.deleteCard('delete-me')
@@ -318,7 +318,7 @@ describe('KanbanSDK', () => {
318
318
  expect(updated.attachments).toContain('test-attach.txt')
319
319
 
320
320
  // Verify file was copied to the status subfolder
321
- const destPath = path.join(tempDir, 'backlog', 'test-attach.txt')
321
+ const destPath = path.join(tempDir, 'boards', 'default', 'backlog', 'test-attach.txt')
322
322
  expect(fs.existsSync(destPath)).toBe(true)
323
323
 
324
324
  fs.unlinkSync(srcFile)
@@ -412,7 +412,7 @@ describe('KanbanSDK', () => {
412
412
  // Verify persisted
413
413
  const raw = fs.readFileSync(path.join(workspaceDir, '.kanban.json'), 'utf-8')
414
414
  const config = JSON.parse(raw)
415
- expect(config.columns.length).toBe(6)
415
+ expect(config.boards.default.columns.length).toBe(6)
416
416
  })
417
417
 
418
418
  it('should throw if column ID already exists', () => {