@compilr-dev/cli 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +110 -0
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agent.js +317 -0
  4. package/dist/agents/registry.d.ts +66 -0
  5. package/dist/agents/registry.js +238 -0
  6. package/dist/agents/types.d.ts +40 -0
  7. package/dist/agents/types.js +94 -0
  8. package/dist/commands/custom-registry.d.ts +69 -0
  9. package/dist/commands/custom-registry.js +246 -0
  10. package/dist/commands/index.d.ts +7 -0
  11. package/dist/commands/index.js +7 -0
  12. package/dist/commands/types.d.ts +31 -0
  13. package/dist/commands/types.js +26 -0
  14. package/dist/commands.d.ts +63 -0
  15. package/dist/commands.js +324 -0
  16. package/dist/db/index.d.ts +42 -0
  17. package/dist/db/index.js +146 -0
  18. package/dist/db/repositories/document-repository.d.ts +63 -0
  19. package/dist/db/repositories/document-repository.js +184 -0
  20. package/dist/db/repositories/index.d.ts +9 -0
  21. package/dist/db/repositories/index.js +6 -0
  22. package/dist/db/repositories/project-repository.d.ts +132 -0
  23. package/dist/db/repositories/project-repository.js +337 -0
  24. package/dist/db/repositories/work-item-repository.d.ts +115 -0
  25. package/dist/db/repositories/work-item-repository.js +389 -0
  26. package/dist/db/schema.d.ts +83 -0
  27. package/dist/db/schema.js +143 -0
  28. package/dist/debug.d.ts +8 -0
  29. package/dist/debug.js +48 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +348 -0
  32. package/dist/index.old.d.ts +7 -0
  33. package/dist/index.old.js +1014 -0
  34. package/dist/repl.d.ts +121 -0
  35. package/dist/repl.js +1878 -0
  36. package/dist/settings/index.d.ts +80 -0
  37. package/dist/settings/index.js +195 -0
  38. package/dist/shared-handlers.d.ts +63 -0
  39. package/dist/shared-handlers.js +57 -0
  40. package/dist/slash-autocomplete.d.ts +41 -0
  41. package/dist/slash-autocomplete.js +638 -0
  42. package/dist/state.d.ts +75 -0
  43. package/dist/state.js +130 -0
  44. package/dist/tabbed-menu.d.ts +11 -0
  45. package/dist/tabbed-menu.js +328 -0
  46. package/dist/templates/backlog-md.d.ts +7 -0
  47. package/dist/templates/backlog-md.js +94 -0
  48. package/dist/templates/claude-md.d.ts +7 -0
  49. package/dist/templates/claude-md.js +189 -0
  50. package/dist/templates/coding-standards.d.ts +7 -0
  51. package/dist/templates/coding-standards.js +299 -0
  52. package/dist/templates/compilr-md.d.ts +7 -0
  53. package/dist/templates/compilr-md.js +189 -0
  54. package/dist/templates/config-json.d.ts +38 -0
  55. package/dist/templates/config-json.js +39 -0
  56. package/dist/templates/gitignore.d.ts +7 -0
  57. package/dist/templates/gitignore.js +85 -0
  58. package/dist/templates/index.d.ts +19 -0
  59. package/dist/templates/index.js +302 -0
  60. package/dist/templates/package-json.d.ts +7 -0
  61. package/dist/templates/package-json.js +111 -0
  62. package/dist/templates/readme-md.d.ts +7 -0
  63. package/dist/templates/readme-md.js +161 -0
  64. package/dist/templates/tsconfig.d.ts +7 -0
  65. package/dist/templates/tsconfig.js +61 -0
  66. package/dist/templates/types.d.ts +33 -0
  67. package/dist/templates/types.js +24 -0
  68. package/dist/test-autocomplete.d.ts +7 -0
  69. package/dist/test-autocomplete.js +85 -0
  70. package/dist/test-tabbed-menu.d.ts +7 -0
  71. package/dist/test-tabbed-menu.js +25 -0
  72. package/dist/themes/colors.d.ts +49 -0
  73. package/dist/themes/colors.js +135 -0
  74. package/dist/themes/index.d.ts +23 -0
  75. package/dist/themes/index.js +24 -0
  76. package/dist/themes/registry.d.ts +60 -0
  77. package/dist/themes/registry.js +195 -0
  78. package/dist/themes/types.d.ts +82 -0
  79. package/dist/themes/types.js +7 -0
  80. package/dist/tool-selector.d.ts +71 -0
  81. package/dist/tool-selector.js +184 -0
  82. package/dist/tools/ask-user-simple.d.ts +19 -0
  83. package/dist/tools/ask-user-simple.js +86 -0
  84. package/dist/tools/ask-user.d.ts +32 -0
  85. package/dist/tools/ask-user.js +113 -0
  86. package/dist/tools/backlog.d.ts +53 -0
  87. package/dist/tools/backlog.js +709 -0
  88. package/dist/tools.d.ts +15 -0
  89. package/dist/tools.js +121 -0
  90. package/dist/ui/agents-overlay.d.ts +12 -0
  91. package/dist/ui/agents-overlay.js +501 -0
  92. package/dist/ui/arch-type-overlay.d.ts +20 -0
  93. package/dist/ui/arch-type-overlay.js +229 -0
  94. package/dist/ui/ask-user-overlay.d.ts +26 -0
  95. package/dist/ui/ask-user-overlay.js +647 -0
  96. package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
  97. package/dist/ui/ask-user-simple-overlay.js +242 -0
  98. package/dist/ui/backlog-overlay.d.ts +17 -0
  99. package/dist/ui/backlog-overlay.js +786 -0
  100. package/dist/ui/commands-overlay.d.ts +11 -0
  101. package/dist/ui/commands-overlay.js +410 -0
  102. package/dist/ui/config-overlay.d.ts +34 -0
  103. package/dist/ui/config-overlay.js +977 -0
  104. package/dist/ui/conversation.d.ts +82 -0
  105. package/dist/ui/conversation.js +508 -0
  106. package/dist/ui/diff.d.ts +38 -0
  107. package/dist/ui/diff.js +182 -0
  108. package/dist/ui/ephemeral.d.ts +111 -0
  109. package/dist/ui/ephemeral.js +413 -0
  110. package/dist/ui/file-autocomplete.d.ts +45 -0
  111. package/dist/ui/file-autocomplete.js +237 -0
  112. package/dist/ui/footer.d.ts +153 -0
  113. package/dist/ui/footer.js +422 -0
  114. package/dist/ui/index.d.ts +12 -0
  115. package/dist/ui/index.js +15 -0
  116. package/dist/ui/init-overlay.d.ts +24 -0
  117. package/dist/ui/init-overlay.js +525 -0
  118. package/dist/ui/input-prompt-v2.d.ts +179 -0
  119. package/dist/ui/input-prompt-v2.js +991 -0
  120. package/dist/ui/input-prompt.d.ts +97 -0
  121. package/dist/ui/input-prompt.js +800 -0
  122. package/dist/ui/iteration-limit-overlay.d.ts +21 -0
  123. package/dist/ui/iteration-limit-overlay.js +150 -0
  124. package/dist/ui/keys-overlay.d.ts +14 -0
  125. package/dist/ui/keys-overlay.js +181 -0
  126. package/dist/ui/model-warning-overlay.d.ts +30 -0
  127. package/dist/ui/model-warning-overlay.js +171 -0
  128. package/dist/ui/overlay-controller.d.ts +25 -0
  129. package/dist/ui/overlay-controller.js +35 -0
  130. package/dist/ui/overlays.d.ts +47 -0
  131. package/dist/ui/overlays.js +627 -0
  132. package/dist/ui/permission-overlay.d.ts +16 -0
  133. package/dist/ui/permission-overlay.js +494 -0
  134. package/dist/ui/terminal.d.ts +117 -0
  135. package/dist/ui/terminal.js +237 -0
  136. package/dist/ui/todo-zone.d.ts +112 -0
  137. package/dist/ui/todo-zone.js +353 -0
  138. package/dist/ui/tools-overlay.d.ts +26 -0
  139. package/dist/ui/tools-overlay.js +278 -0
  140. package/dist/ui/tutorial-overlay.d.ts +10 -0
  141. package/dist/ui/tutorial-overlay.js +936 -0
  142. package/dist/ui/types.d.ts +103 -0
  143. package/dist/ui/types.js +33 -0
  144. package/dist/utils/credentials.d.ts +55 -0
  145. package/dist/utils/credentials.js +268 -0
  146. package/dist/utils/model-tiers.d.ts +37 -0
  147. package/dist/utils/model-tiers.js +118 -0
  148. package/dist/utils/project-memory.d.ts +47 -0
  149. package/dist/utils/project-memory.js +117 -0
  150. package/dist/utils/project-status.d.ts +56 -0
  151. package/dist/utils/project-status.js +237 -0
  152. package/package.json +66 -0
@@ -0,0 +1,709 @@
1
+ /**
2
+ * Backlog Tools - Read and write project backlog items
3
+ *
4
+ * These tools allow the agent to programmatically manage the project backlog,
5
+ * supporting the /design and /refine workflow.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { defineTool } from '@compilr-dev/agents';
10
+ // =============================================================================
11
+ // Backlog File Detection
12
+ // =============================================================================
13
+ /**
14
+ * Find the backlog file path for the current project.
15
+ * Checks both single-repo and two-repo patterns.
16
+ */
17
+ export function findBacklogPath(basePath = process.cwd()) {
18
+ // Single repo pattern: .compilr/backlog.md
19
+ const singleRepoPath = path.join(basePath, '.compilr', 'backlog.md');
20
+ if (fs.existsSync(singleRepoPath)) {
21
+ return singleRepoPath;
22
+ }
23
+ // Two repo pattern: look for -docs sibling folder
24
+ const parentDir = path.dirname(basePath);
25
+ const projectName = path.basename(basePath);
26
+ const docsRepoPath = path.join(parentDir, `${projectName}-docs`, '01-planning', 'backlog.md');
27
+ if (fs.existsSync(docsRepoPath)) {
28
+ return docsRepoPath;
29
+ }
30
+ // Also check if we're already in the docs repo
31
+ const inDocsPath = path.join(basePath, '01-planning', 'backlog.md');
32
+ if (fs.existsSync(inDocsPath)) {
33
+ return inDocsPath;
34
+ }
35
+ // Check for project subfolders with -docs pattern
36
+ // This handles when running from a parent folder (e.g., /workspace/test-folder/)
37
+ // where projects are in subfolders (e.g., test-project-03, test-project-03-docs)
38
+ try {
39
+ const entries = fs.readdirSync(basePath, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ if (entry.isDirectory() && entry.name.endsWith('-docs')) {
42
+ const docsBacklog = path.join(basePath, entry.name, '01-planning', 'backlog.md');
43
+ if (fs.existsSync(docsBacklog)) {
44
+ return docsBacklog;
45
+ }
46
+ }
47
+ }
48
+ }
49
+ catch {
50
+ // Ignore read errors
51
+ }
52
+ return null;
53
+ }
54
+ /**
55
+ * Get or create the backlog path.
56
+ * If no backlog exists, determines the best location based on project structure.
57
+ */
58
+ function getOrCreateBacklogPath(basePath = process.cwd()) {
59
+ const existingPath = findBacklogPath(basePath);
60
+ if (existingPath) {
61
+ return existingPath;
62
+ }
63
+ // Check for -docs subfolder pattern (two-repo setup in parent folder)
64
+ try {
65
+ const entries = fs.readdirSync(basePath, { withFileTypes: true });
66
+ for (const entry of entries) {
67
+ if (entry.isDirectory() && entry.name.endsWith('-docs')) {
68
+ const docsDir = path.join(basePath, entry.name, '01-planning');
69
+ // If the 01-planning directory exists, use it
70
+ if (fs.existsSync(docsDir)) {
71
+ return path.join(docsDir, 'backlog.md');
72
+ }
73
+ }
74
+ }
75
+ }
76
+ catch {
77
+ // Ignore read errors
78
+ }
79
+ // Check for sibling -docs folder (two-repo setup in project folder)
80
+ const parentDir = path.dirname(basePath);
81
+ const projectName = path.basename(basePath);
82
+ const siblingDocsDir = path.join(parentDir, `${projectName}-docs`, '01-planning');
83
+ if (fs.existsSync(siblingDocsDir)) {
84
+ return path.join(siblingDocsDir, 'backlog.md');
85
+ }
86
+ // Default to single repo pattern
87
+ const defaultPath = path.join(basePath, '.compilr', 'backlog.md');
88
+ return defaultPath;
89
+ }
90
+ // =============================================================================
91
+ // Markdown Table Parsing & Generation
92
+ // =============================================================================
93
+ const TABLE_HEADER = '| ID | Type | Status | Priority | Title | Description | Commit |';
94
+ const TABLE_SEPARATOR = '|----|------|--------|----------|-------|-------------|--------|';
95
+ /**
96
+ * Parse a markdown table row into a BacklogItem.
97
+ */
98
+ function parseTableRow(row) {
99
+ // Remove leading/trailing pipes and split by |
100
+ const cells = row.split('|').map(c => c.trim()).filter(c => c !== '');
101
+ if (cells.length < 6) {
102
+ return null;
103
+ }
104
+ const [id, type, status, priority, title, description, commit] = cells;
105
+ // Validate ID format (e.g., REQ-001, BUG-001)
106
+ if (!id || !/^[A-Z]+-\d{3}$/.test(id)) {
107
+ return null;
108
+ }
109
+ return {
110
+ id,
111
+ type: type,
112
+ status: status,
113
+ priority: priority,
114
+ title,
115
+ description,
116
+ commit: commit || undefined,
117
+ };
118
+ }
119
+ /**
120
+ * Generate a markdown table row from a BacklogItem.
121
+ */
122
+ function generateTableRow(item) {
123
+ return `| ${item.id} | ${item.type} | ${item.status} | ${item.priority} | ${item.title} | ${item.description} | ${item.commit || ''} |`;
124
+ }
125
+ /**
126
+ * Parse backlog items from markdown content.
127
+ */
128
+ export function parseBacklogItems(content) {
129
+ const items = [];
130
+ const lines = content.split('\n');
131
+ let inTable = false;
132
+ for (const line of lines) {
133
+ const trimmed = line.trim();
134
+ // Detect table header
135
+ if (trimmed.startsWith('| ID |')) {
136
+ inTable = true;
137
+ continue;
138
+ }
139
+ // Skip separator row
140
+ if (trimmed.startsWith('|---')) {
141
+ continue;
142
+ }
143
+ // Parse table rows
144
+ if (inTable && trimmed.startsWith('|')) {
145
+ const item = parseTableRow(trimmed);
146
+ if (item) {
147
+ items.push(item);
148
+ }
149
+ else {
150
+ // End of table if row doesn't parse
151
+ inTable = false;
152
+ }
153
+ }
154
+ else if (inTable && !trimmed.startsWith('|')) {
155
+ // End of table
156
+ inTable = false;
157
+ }
158
+ }
159
+ return items;
160
+ }
161
+ /**
162
+ * Generate markdown content for backlog items.
163
+ */
164
+ function generateBacklogMarkdown(items, existingContent) {
165
+ // If there's existing content with our table format, replace just the table
166
+ if (existingContent && existingContent.includes(TABLE_HEADER)) {
167
+ const lines = existingContent.split('\n');
168
+ const result = [];
169
+ let inTable = false;
170
+ let tableReplaced = false;
171
+ for (const line of lines) {
172
+ const trimmed = line.trim();
173
+ if (trimmed.startsWith('| ID |')) {
174
+ // Start of our table - replace it
175
+ inTable = true;
176
+ if (!tableReplaced) {
177
+ result.push(TABLE_HEADER);
178
+ result.push(TABLE_SEPARATOR);
179
+ for (const item of items) {
180
+ result.push(generateTableRow(item));
181
+ }
182
+ tableReplaced = true;
183
+ }
184
+ continue;
185
+ }
186
+ if (inTable) {
187
+ if (trimmed.startsWith('|---') || (trimmed.startsWith('|') && trimmed.includes('|'))) {
188
+ // Skip existing table rows
189
+ continue;
190
+ }
191
+ // End of table - include this line and stop skipping
192
+ inTable = false;
193
+ }
194
+ // Include non-table lines
195
+ result.push(line);
196
+ }
197
+ return result.join('\n');
198
+ }
199
+ // Generate new content
200
+ const date = new Date().toISOString().split('T')[0];
201
+ let content = `# Backlog
202
+
203
+ ${TABLE_HEADER}
204
+ ${TABLE_SEPARATOR}
205
+ `;
206
+ for (const item of items) {
207
+ content += generateTableRow(item) + '\n';
208
+ }
209
+ content += `
210
+ ---
211
+
212
+ *Last updated: ${date} by agent*
213
+ `;
214
+ return content;
215
+ }
216
+ // =============================================================================
217
+ // Input Sanitization
218
+ // =============================================================================
219
+ /**
220
+ * Sanitize a string for use in a markdown table cell.
221
+ * - Removes newlines (replace with space)
222
+ * - Removes pipe characters (breaks table)
223
+ * - Trims whitespace
224
+ * - Limits length
225
+ */
226
+ function sanitizeTableCell(value, maxLength = 500) {
227
+ if (!value || typeof value !== 'string') {
228
+ return '';
229
+ }
230
+ return value
231
+ // Replace newlines with spaces
232
+ .replace(/[\r\n]+/g, ' ')
233
+ // Remove pipe characters (break markdown tables)
234
+ .replace(/\|/g, '-')
235
+ // Collapse multiple spaces
236
+ .replace(/\s+/g, ' ')
237
+ // Trim whitespace
238
+ .trim()
239
+ // Limit length
240
+ .slice(0, maxLength);
241
+ }
242
+ /**
243
+ * Sanitize a backlog item's text fields.
244
+ */
245
+ function sanitizeBacklogItem(item) {
246
+ const warnings = [];
247
+ // Sanitize title (max 100 chars)
248
+ const originalTitle = item.title || '';
249
+ const sanitizedTitle = sanitizeTableCell(originalTitle, 100);
250
+ if (sanitizedTitle !== originalTitle.trim()) {
251
+ warnings.push(`Title was sanitized (removed newlines/pipes or truncated)`);
252
+ }
253
+ // Sanitize description (max 500 chars)
254
+ const originalDesc = item.description || '';
255
+ const sanitizedDesc = sanitizeTableCell(originalDesc, 500);
256
+ if (sanitizedDesc !== originalDesc.trim()) {
257
+ warnings.push(`Description was sanitized (removed newlines/pipes or truncated)`);
258
+ }
259
+ // Validate type
260
+ const validTypes = ['feature', 'bug', 'tech-debt', 'chore'];
261
+ const type = validTypes.includes(item.type) ? item.type : 'feature';
262
+ if (type !== item.type) {
263
+ warnings.push(`Invalid type "${item.type}" defaulted to "feature"`);
264
+ }
265
+ // Validate status
266
+ const validStatuses = ['📋', '🚧', '✅'];
267
+ const status = validStatuses.includes(item.status) ? item.status : '📋';
268
+ if (item.status && status !== item.status) {
269
+ warnings.push(`Invalid status "${item.status}" defaulted to "📋"`);
270
+ }
271
+ // Validate priority
272
+ const validPriorities = ['critical', 'high', 'medium', 'low'];
273
+ const priority = validPriorities.includes(item.priority) ? item.priority : 'medium';
274
+ if (priority !== item.priority) {
275
+ warnings.push(`Invalid priority "${item.priority}" defaulted to "medium"`);
276
+ }
277
+ return {
278
+ title: sanitizedTitle || 'Untitled',
279
+ description: sanitizedDesc || 'No description',
280
+ type,
281
+ status,
282
+ priority,
283
+ warnings,
284
+ };
285
+ }
286
+ // =============================================================================
287
+ // ID Generation
288
+ // =============================================================================
289
+ const TYPE_PREFIX_MAP = {
290
+ 'feature': 'REQ',
291
+ 'bug': 'BUG',
292
+ 'tech-debt': 'TECH',
293
+ 'chore': 'CHORE',
294
+ };
295
+ /**
296
+ * Generate a new ID for a backlog item based on its type and existing items.
297
+ */
298
+ function generateId(type, existingItems) {
299
+ const prefix = TYPE_PREFIX_MAP[type];
300
+ // Find highest number for this prefix
301
+ let maxNum = 0;
302
+ for (const item of existingItems) {
303
+ if (item.id.startsWith(prefix + '-')) {
304
+ const num = parseInt(item.id.split('-')[1], 10);
305
+ if (!isNaN(num) && num > maxNum) {
306
+ maxNum = num;
307
+ }
308
+ }
309
+ }
310
+ return `${prefix}-${String(maxNum + 1).padStart(3, '0')}`;
311
+ }
312
+ export const backlogReadTool = defineTool({
313
+ name: 'backlog_read',
314
+ description: 'Read backlog items from the project. ' +
315
+ 'Use "id" to get a specific item, or use filters to query multiple items. ' +
316
+ 'Default limit is 10 items - use offset for pagination. ' +
317
+ 'Use "search" to find items by title/description text.',
318
+ inputSchema: {
319
+ type: 'object',
320
+ properties: {
321
+ id: {
322
+ type: 'string',
323
+ description: 'Get a specific item by ID (e.g., "REQ-001", "BUG-002")',
324
+ },
325
+ search: {
326
+ type: 'string',
327
+ description: 'Search text in title and description (case-insensitive)',
328
+ },
329
+ status: {
330
+ type: 'string',
331
+ enum: ['📋', '🚧', '✅', 'all'],
332
+ description: 'Filter by status (📋=backlog, 🚧=in-progress, ✅=done, all=no filter)',
333
+ },
334
+ type: {
335
+ type: 'string',
336
+ enum: ['feature', 'bug', 'tech-debt', 'chore', 'all'],
337
+ description: 'Filter by item type',
338
+ },
339
+ priority: {
340
+ type: 'string',
341
+ enum: ['critical', 'high', 'medium', 'low', 'all'],
342
+ description: 'Filter by priority',
343
+ },
344
+ limit: {
345
+ type: 'number',
346
+ description: 'Maximum items to return (default: 10, max: 50)',
347
+ },
348
+ offset: {
349
+ type: 'number',
350
+ description: 'Skip first N items for pagination (default: 0)',
351
+ },
352
+ },
353
+ required: [],
354
+ },
355
+ execute: (input) => {
356
+ const backlogPath = findBacklogPath();
357
+ if (!backlogPath) {
358
+ return Promise.resolve({
359
+ success: false,
360
+ error: 'No backlog file found. Run /init to create a project first, or /design to create the backlog.',
361
+ });
362
+ }
363
+ try {
364
+ const content = fs.readFileSync(backlogPath, 'utf-8');
365
+ let items = parseBacklogItems(content);
366
+ // If ID specified, return just that item
367
+ if (input.id) {
368
+ const searchId = input.id.toLowerCase();
369
+ const item = items.find(i => i.id.toLowerCase() === searchId);
370
+ if (!item) {
371
+ return Promise.resolve({
372
+ success: false,
373
+ error: `Item "${input.id}" not found in backlog.`,
374
+ });
375
+ }
376
+ return Promise.resolve({
377
+ success: true,
378
+ result: {
379
+ items: [item],
380
+ total: 1,
381
+ returned: 1,
382
+ hasMore: false,
383
+ path: backlogPath,
384
+ },
385
+ });
386
+ }
387
+ // Apply search filter
388
+ if (input.search) {
389
+ const searchLower = input.search.toLowerCase();
390
+ items = items.filter(i => i.title.toLowerCase().includes(searchLower) ||
391
+ i.description.toLowerCase().includes(searchLower));
392
+ }
393
+ // Apply status filter
394
+ if (input.status && input.status !== 'all') {
395
+ items = items.filter(i => i.status === input.status);
396
+ }
397
+ // Apply type filter
398
+ if (input.type && input.type !== 'all') {
399
+ items = items.filter(i => i.type === input.type);
400
+ }
401
+ // Apply priority filter
402
+ if (input.priority && input.priority !== 'all') {
403
+ items = items.filter(i => i.priority === input.priority);
404
+ }
405
+ const total = items.length;
406
+ // Apply pagination
407
+ const offset = Math.max(0, input.offset || 0);
408
+ const limit = Math.min(50, Math.max(1, input.limit || 10)); // Default 10, max 50
409
+ items = items.slice(offset, offset + limit);
410
+ const readResult = {
411
+ items,
412
+ total,
413
+ returned: items.length,
414
+ hasMore: offset + items.length < total,
415
+ path: backlogPath,
416
+ };
417
+ return Promise.resolve({
418
+ success: true,
419
+ result: readResult,
420
+ });
421
+ }
422
+ catch (err) {
423
+ return Promise.resolve({
424
+ success: false,
425
+ error: `Failed to read backlog: ${err instanceof Error ? err.message : String(err)}`,
426
+ });
427
+ }
428
+ },
429
+ });
430
+ export const backlogWriteTool = defineTool({
431
+ name: 'backlog_write',
432
+ description: 'Modify the project backlog. ' +
433
+ 'Actions: add (create new items), update (modify existing item), ' +
434
+ 'remove (delete items), reorder (change order). ' +
435
+ 'IDs are auto-generated based on type (REQ-001, BUG-001, TECH-001, CHORE-001).',
436
+ inputSchema: {
437
+ type: 'object',
438
+ properties: {
439
+ action: {
440
+ type: 'string',
441
+ enum: ['add', 'update', 'remove', 'reorder'],
442
+ description: 'The action to perform',
443
+ },
444
+ items: {
445
+ type: 'array',
446
+ description: 'For "add" action: array of new items to add (ID auto-generated)',
447
+ items: {
448
+ type: 'object',
449
+ properties: {
450
+ type: {
451
+ type: 'string',
452
+ enum: ['feature', 'bug', 'tech-debt', 'chore'],
453
+ description: 'Item type',
454
+ },
455
+ status: {
456
+ type: 'string',
457
+ enum: ['📋', '🚧', '✅'],
458
+ description: 'Status (default: 📋)',
459
+ },
460
+ priority: {
461
+ type: 'string',
462
+ enum: ['critical', 'high', 'medium', 'low'],
463
+ description: 'Priority level',
464
+ },
465
+ title: {
466
+ type: 'string',
467
+ description: 'Short title',
468
+ },
469
+ description: {
470
+ type: 'string',
471
+ description: 'Detailed description',
472
+ },
473
+ },
474
+ required: ['type', 'priority', 'title', 'description'],
475
+ },
476
+ },
477
+ id: {
478
+ type: 'string',
479
+ description: 'For "update" action: ID of item to update',
480
+ },
481
+ updates: {
482
+ type: 'object',
483
+ description: 'For "update" action: fields to update',
484
+ properties: {
485
+ type: { type: 'string', enum: ['feature', 'bug', 'tech-debt', 'chore'] },
486
+ status: { type: 'string', enum: ['📋', '🚧', '✅'] },
487
+ priority: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },
488
+ title: { type: 'string' },
489
+ description: { type: 'string' },
490
+ commit: { type: 'string' },
491
+ },
492
+ },
493
+ ids: {
494
+ type: 'array',
495
+ description: 'For "remove" action: IDs of items to remove',
496
+ items: { type: 'string' },
497
+ },
498
+ order: {
499
+ type: 'array',
500
+ description: 'For "reorder" action: IDs in desired order',
501
+ items: { type: 'string' },
502
+ },
503
+ },
504
+ required: ['action'],
505
+ },
506
+ execute: (input) => {
507
+ const backlogPath = getOrCreateBacklogPath();
508
+ try {
509
+ // Read existing content if it exists
510
+ let existingContent = '';
511
+ let existingItems = [];
512
+ if (fs.existsSync(backlogPath)) {
513
+ existingContent = fs.readFileSync(backlogPath, 'utf-8');
514
+ existingItems = parseBacklogItems(existingContent);
515
+ }
516
+ else {
517
+ // Ensure directory exists
518
+ const dir = path.dirname(backlogPath);
519
+ if (!fs.existsSync(dir)) {
520
+ fs.mkdirSync(dir, { recursive: true });
521
+ }
522
+ }
523
+ let itemsAffected = 0;
524
+ const newIds = [];
525
+ let updatedItems = [...existingItems];
526
+ const allWarnings = [];
527
+ switch (input.action) {
528
+ case 'add': {
529
+ if (!input.items || input.items.length === 0) {
530
+ return Promise.resolve({
531
+ success: false,
532
+ error: 'No items provided for "add" action',
533
+ });
534
+ }
535
+ for (const newItem of input.items) {
536
+ // Sanitize input to prevent markdown table corruption
537
+ const sanitized = sanitizeBacklogItem({
538
+ title: newItem.title,
539
+ description: newItem.description,
540
+ type: newItem.type,
541
+ status: newItem.status,
542
+ priority: newItem.priority,
543
+ });
544
+ if (sanitized.warnings.length > 0) {
545
+ allWarnings.push(...sanitized.warnings.map(w => `Item "${sanitized.title.slice(0, 30)}...": ${w}`));
546
+ }
547
+ const id = generateId(sanitized.type, updatedItems);
548
+ const item = {
549
+ id,
550
+ type: sanitized.type,
551
+ status: sanitized.status,
552
+ priority: sanitized.priority,
553
+ title: sanitized.title,
554
+ description: sanitized.description,
555
+ };
556
+ updatedItems.push(item);
557
+ newIds.push(id);
558
+ itemsAffected++;
559
+ }
560
+ break;
561
+ }
562
+ case 'update': {
563
+ if (!input.id || !input.updates) {
564
+ return Promise.resolve({
565
+ success: false,
566
+ error: 'Both "id" and "updates" are required for "update" action',
567
+ });
568
+ }
569
+ const index = updatedItems.findIndex(i => i.id === input.id);
570
+ if (index === -1) {
571
+ return Promise.resolve({
572
+ success: false,
573
+ error: `Item not found: ${input.id}`,
574
+ });
575
+ }
576
+ // Build sanitized updates
577
+ const currentItem = updatedItems[index];
578
+ const sanitizedUpdates = {};
579
+ const updateWarnings = [];
580
+ // Sanitize title if provided
581
+ if (input.updates.title !== undefined) {
582
+ const sanitizedTitle = sanitizeTableCell(input.updates.title, 100);
583
+ if (sanitizedTitle !== input.updates.title.trim()) {
584
+ updateWarnings.push('Title was sanitized');
585
+ }
586
+ sanitizedUpdates.title = sanitizedTitle || currentItem.title;
587
+ }
588
+ // Sanitize description if provided
589
+ if (input.updates.description !== undefined) {
590
+ const sanitizedDesc = sanitizeTableCell(input.updates.description, 500);
591
+ if (sanitizedDesc !== input.updates.description.trim()) {
592
+ updateWarnings.push('Description was sanitized');
593
+ }
594
+ sanitizedUpdates.description = sanitizedDesc || currentItem.description;
595
+ }
596
+ // Validate type if provided
597
+ if (input.updates.type !== undefined) {
598
+ const validTypes = ['feature', 'bug', 'tech-debt', 'chore'];
599
+ if (validTypes.includes(input.updates.type)) {
600
+ sanitizedUpdates.type = input.updates.type;
601
+ }
602
+ else {
603
+ updateWarnings.push(`Invalid type "${input.updates.type}" ignored`);
604
+ }
605
+ }
606
+ // Validate status if provided
607
+ if (input.updates.status !== undefined) {
608
+ const validStatuses = ['📋', '🚧', '✅'];
609
+ if (validStatuses.includes(input.updates.status)) {
610
+ sanitizedUpdates.status = input.updates.status;
611
+ }
612
+ else {
613
+ updateWarnings.push(`Invalid status "${input.updates.status}" ignored`);
614
+ }
615
+ }
616
+ // Validate priority if provided
617
+ if (input.updates.priority !== undefined) {
618
+ const validPriorities = ['critical', 'high', 'medium', 'low'];
619
+ if (validPriorities.includes(input.updates.priority)) {
620
+ sanitizedUpdates.priority = input.updates.priority;
621
+ }
622
+ else {
623
+ updateWarnings.push(`Invalid priority "${input.updates.priority}" ignored`);
624
+ }
625
+ }
626
+ // Sanitize commit if provided (no newlines, limited length)
627
+ if (input.updates.commit !== undefined) {
628
+ sanitizedUpdates.commit = sanitizeTableCell(input.updates.commit, 50);
629
+ }
630
+ if (updateWarnings.length > 0) {
631
+ const itemId = input.id ?? 'unknown';
632
+ allWarnings.push(...updateWarnings.map(w => `${itemId}: ${w}`));
633
+ }
634
+ updatedItems[index] = {
635
+ ...currentItem,
636
+ ...sanitizedUpdates,
637
+ };
638
+ itemsAffected = 1;
639
+ break;
640
+ }
641
+ case 'remove': {
642
+ if (!input.ids || input.ids.length === 0) {
643
+ return Promise.resolve({
644
+ success: false,
645
+ error: 'No IDs provided for "remove" action',
646
+ });
647
+ }
648
+ const idsToRemove = new Set(input.ids);
649
+ const originalLength = updatedItems.length;
650
+ updatedItems = updatedItems.filter(i => !idsToRemove.has(i.id));
651
+ itemsAffected = originalLength - updatedItems.length;
652
+ break;
653
+ }
654
+ case 'reorder': {
655
+ if (!input.order || input.order.length === 0) {
656
+ return Promise.resolve({
657
+ success: false,
658
+ error: 'No order provided for "reorder" action',
659
+ });
660
+ }
661
+ const itemMap = new Map(updatedItems.map(i => [i.id, i]));
662
+ const reordered = [];
663
+ for (const id of input.order) {
664
+ const item = itemMap.get(id);
665
+ if (item) {
666
+ reordered.push(item);
667
+ itemMap.delete(id);
668
+ }
669
+ }
670
+ // Append any items not in the order list
671
+ for (const item of itemMap.values()) {
672
+ reordered.push(item);
673
+ }
674
+ updatedItems = reordered;
675
+ itemsAffected = input.order.length;
676
+ break;
677
+ }
678
+ default: {
679
+ const _exhaustiveCheck = input.action;
680
+ return Promise.resolve({
681
+ success: false,
682
+ error: `Unknown action: ${String(_exhaustiveCheck)}`,
683
+ });
684
+ }
685
+ }
686
+ // Generate new markdown content
687
+ const newContent = generateBacklogMarkdown(updatedItems, existingContent);
688
+ // Write to file
689
+ fs.writeFileSync(backlogPath, newContent, 'utf-8');
690
+ const writeResult = {
691
+ success: true,
692
+ itemsAffected,
693
+ newIds: newIds.length > 0 ? newIds : undefined,
694
+ warnings: allWarnings.length > 0 ? allWarnings : undefined,
695
+ path: backlogPath,
696
+ };
697
+ return Promise.resolve({
698
+ success: true,
699
+ result: writeResult,
700
+ });
701
+ }
702
+ catch (err) {
703
+ return Promise.resolve({
704
+ success: false,
705
+ error: `Failed to write backlog: ${err instanceof Error ? err.message : String(err)}`,
706
+ });
707
+ }
708
+ },
709
+ });