@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.
- package/README.md +110 -0
- package/dist/agent.d.ts +62 -0
- package/dist/agent.js +317 -0
- package/dist/agents/registry.d.ts +66 -0
- package/dist/agents/registry.js +238 -0
- package/dist/agents/types.d.ts +40 -0
- package/dist/agents/types.js +94 -0
- package/dist/commands/custom-registry.d.ts +69 -0
- package/dist/commands/custom-registry.js +246 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/types.d.ts +31 -0
- package/dist/commands/types.js +26 -0
- package/dist/commands.d.ts +63 -0
- package/dist/commands.js +324 -0
- package/dist/db/index.d.ts +42 -0
- package/dist/db/index.js +146 -0
- package/dist/db/repositories/document-repository.d.ts +63 -0
- package/dist/db/repositories/document-repository.js +184 -0
- package/dist/db/repositories/index.d.ts +9 -0
- package/dist/db/repositories/index.js +6 -0
- package/dist/db/repositories/project-repository.d.ts +132 -0
- package/dist/db/repositories/project-repository.js +337 -0
- package/dist/db/repositories/work-item-repository.d.ts +115 -0
- package/dist/db/repositories/work-item-repository.js +389 -0
- package/dist/db/schema.d.ts +83 -0
- package/dist/db/schema.js +143 -0
- package/dist/debug.d.ts +8 -0
- package/dist/debug.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +348 -0
- package/dist/index.old.d.ts +7 -0
- package/dist/index.old.js +1014 -0
- package/dist/repl.d.ts +121 -0
- package/dist/repl.js +1878 -0
- package/dist/settings/index.d.ts +80 -0
- package/dist/settings/index.js +195 -0
- package/dist/shared-handlers.d.ts +63 -0
- package/dist/shared-handlers.js +57 -0
- package/dist/slash-autocomplete.d.ts +41 -0
- package/dist/slash-autocomplete.js +638 -0
- package/dist/state.d.ts +75 -0
- package/dist/state.js +130 -0
- package/dist/tabbed-menu.d.ts +11 -0
- package/dist/tabbed-menu.js +328 -0
- package/dist/templates/backlog-md.d.ts +7 -0
- package/dist/templates/backlog-md.js +94 -0
- package/dist/templates/claude-md.d.ts +7 -0
- package/dist/templates/claude-md.js +189 -0
- package/dist/templates/coding-standards.d.ts +7 -0
- package/dist/templates/coding-standards.js +299 -0
- package/dist/templates/compilr-md.d.ts +7 -0
- package/dist/templates/compilr-md.js +189 -0
- package/dist/templates/config-json.d.ts +38 -0
- package/dist/templates/config-json.js +39 -0
- package/dist/templates/gitignore.d.ts +7 -0
- package/dist/templates/gitignore.js +85 -0
- package/dist/templates/index.d.ts +19 -0
- package/dist/templates/index.js +302 -0
- package/dist/templates/package-json.d.ts +7 -0
- package/dist/templates/package-json.js +111 -0
- package/dist/templates/readme-md.d.ts +7 -0
- package/dist/templates/readme-md.js +161 -0
- package/dist/templates/tsconfig.d.ts +7 -0
- package/dist/templates/tsconfig.js +61 -0
- package/dist/templates/types.d.ts +33 -0
- package/dist/templates/types.js +24 -0
- package/dist/test-autocomplete.d.ts +7 -0
- package/dist/test-autocomplete.js +85 -0
- package/dist/test-tabbed-menu.d.ts +7 -0
- package/dist/test-tabbed-menu.js +25 -0
- package/dist/themes/colors.d.ts +49 -0
- package/dist/themes/colors.js +135 -0
- package/dist/themes/index.d.ts +23 -0
- package/dist/themes/index.js +24 -0
- package/dist/themes/registry.d.ts +60 -0
- package/dist/themes/registry.js +195 -0
- package/dist/themes/types.d.ts +82 -0
- package/dist/themes/types.js +7 -0
- package/dist/tool-selector.d.ts +71 -0
- package/dist/tool-selector.js +184 -0
- package/dist/tools/ask-user-simple.d.ts +19 -0
- package/dist/tools/ask-user-simple.js +86 -0
- package/dist/tools/ask-user.d.ts +32 -0
- package/dist/tools/ask-user.js +113 -0
- package/dist/tools/backlog.d.ts +53 -0
- package/dist/tools/backlog.js +709 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +121 -0
- package/dist/ui/agents-overlay.d.ts +12 -0
- package/dist/ui/agents-overlay.js +501 -0
- package/dist/ui/arch-type-overlay.d.ts +20 -0
- package/dist/ui/arch-type-overlay.js +229 -0
- package/dist/ui/ask-user-overlay.d.ts +26 -0
- package/dist/ui/ask-user-overlay.js +647 -0
- package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
- package/dist/ui/ask-user-simple-overlay.js +242 -0
- package/dist/ui/backlog-overlay.d.ts +17 -0
- package/dist/ui/backlog-overlay.js +786 -0
- package/dist/ui/commands-overlay.d.ts +11 -0
- package/dist/ui/commands-overlay.js +410 -0
- package/dist/ui/config-overlay.d.ts +34 -0
- package/dist/ui/config-overlay.js +977 -0
- package/dist/ui/conversation.d.ts +82 -0
- package/dist/ui/conversation.js +508 -0
- package/dist/ui/diff.d.ts +38 -0
- package/dist/ui/diff.js +182 -0
- package/dist/ui/ephemeral.d.ts +111 -0
- package/dist/ui/ephemeral.js +413 -0
- package/dist/ui/file-autocomplete.d.ts +45 -0
- package/dist/ui/file-autocomplete.js +237 -0
- package/dist/ui/footer.d.ts +153 -0
- package/dist/ui/footer.js +422 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/init-overlay.d.ts +24 -0
- package/dist/ui/init-overlay.js +525 -0
- package/dist/ui/input-prompt-v2.d.ts +179 -0
- package/dist/ui/input-prompt-v2.js +991 -0
- package/dist/ui/input-prompt.d.ts +97 -0
- package/dist/ui/input-prompt.js +800 -0
- package/dist/ui/iteration-limit-overlay.d.ts +21 -0
- package/dist/ui/iteration-limit-overlay.js +150 -0
- package/dist/ui/keys-overlay.d.ts +14 -0
- package/dist/ui/keys-overlay.js +181 -0
- package/dist/ui/model-warning-overlay.d.ts +30 -0
- package/dist/ui/model-warning-overlay.js +171 -0
- package/dist/ui/overlay-controller.d.ts +25 -0
- package/dist/ui/overlay-controller.js +35 -0
- package/dist/ui/overlays.d.ts +47 -0
- package/dist/ui/overlays.js +627 -0
- package/dist/ui/permission-overlay.d.ts +16 -0
- package/dist/ui/permission-overlay.js +494 -0
- package/dist/ui/terminal.d.ts +117 -0
- package/dist/ui/terminal.js +237 -0
- package/dist/ui/todo-zone.d.ts +112 -0
- package/dist/ui/todo-zone.js +353 -0
- package/dist/ui/tools-overlay.d.ts +26 -0
- package/dist/ui/tools-overlay.js +278 -0
- package/dist/ui/tutorial-overlay.d.ts +10 -0
- package/dist/ui/tutorial-overlay.js +936 -0
- package/dist/ui/types.d.ts +103 -0
- package/dist/ui/types.js +33 -0
- package/dist/utils/credentials.d.ts +55 -0
- package/dist/utils/credentials.js +268 -0
- package/dist/utils/model-tiers.d.ts +37 -0
- package/dist/utils/model-tiers.js +118 -0
- package/dist/utils/project-memory.d.ts +47 -0
- package/dist/utils/project-memory.js +117 -0
- package/dist/utils/project-status.d.ts +56 -0
- package/dist/utils/project-status.js +237 -0
- 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
|
+
});
|