@compilr-dev/agents 0.2.0 → 0.3.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.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Backlog Tools - File-based project backlog management
3
+ *
4
+ * Provides backlog_read and backlog_write tools for managing project backlogs.
5
+ * Uses a JSON file stored in .compilr/backlog.json (or backlog.json in project root).
6
+ *
7
+ * Features:
8
+ * - Read with filters (status, type, search, limit)
9
+ * - Write/update individual items or full backlog
10
+ * - Support for common item types (feature, bug, chore, spike)
11
+ * - Priority levels (critical, high, medium, low)
12
+ */
13
+ import type { Tool } from '../types.js';
14
+ /**
15
+ * Status of a backlog item
16
+ */
17
+ export type BacklogStatus = 'backlog' | 'in-progress' | 'done' | 'blocked';
18
+ /**
19
+ * Type of backlog item
20
+ */
21
+ export type BacklogItemType = 'feature' | 'bug' | 'chore' | 'spike';
22
+ /**
23
+ * Priority level
24
+ */
25
+ export type BacklogPriority = 'critical' | 'high' | 'medium' | 'low';
26
+ /**
27
+ * A backlog item
28
+ */
29
+ export interface BacklogItem {
30
+ /** Unique identifier (e.g., "FEAT-001", "BUG-002") */
31
+ id: string;
32
+ /** Type of item */
33
+ type: BacklogItemType;
34
+ /** Short title */
35
+ title: string;
36
+ /** Detailed description (optional) */
37
+ description?: string;
38
+ /** Current status */
39
+ status: BacklogStatus;
40
+ /** Priority level (optional, defaults to medium) */
41
+ priority?: BacklogPriority;
42
+ /** Acceptance criteria (optional) */
43
+ acceptanceCriteria?: string[];
44
+ /** Dependencies on other items (optional) */
45
+ dependencies?: string[];
46
+ /** Labels/tags (optional) */
47
+ labels?: string[];
48
+ /** Created timestamp */
49
+ createdAt?: string;
50
+ /** Last updated timestamp */
51
+ updatedAt?: string;
52
+ }
53
+ /**
54
+ * Input parameters for backlog_read tool
55
+ */
56
+ export interface BacklogReadInput {
57
+ /** Get a specific item by ID */
58
+ id?: string;
59
+ /** Filter by status */
60
+ status?: BacklogStatus;
61
+ /** Filter by type */
62
+ type?: BacklogItemType;
63
+ /** Search in title and description */
64
+ search?: string;
65
+ /** Filter by priority */
66
+ priority?: BacklogPriority;
67
+ /** Maximum items to return (default: 20) */
68
+ limit?: number;
69
+ }
70
+ /**
71
+ * Result from backlog_read
72
+ */
73
+ export interface BacklogReadResult {
74
+ items: BacklogItem[];
75
+ total: number;
76
+ filtered: number;
77
+ }
78
+ /**
79
+ * Input parameters for backlog_write tool
80
+ */
81
+ export interface BacklogWriteInput {
82
+ /** Action to perform */
83
+ action: 'add' | 'update' | 'delete' | 'replace';
84
+ /** Item to add or update (required for add/update) */
85
+ item?: Partial<BacklogItem>;
86
+ /** Item ID to delete (required for delete) */
87
+ deleteId?: string;
88
+ /** Full list of items (required for replace action) */
89
+ items?: BacklogItem[];
90
+ }
91
+ /**
92
+ * Result from backlog_write
93
+ */
94
+ export interface BacklogWriteResult {
95
+ success: boolean;
96
+ action: string;
97
+ itemId?: string;
98
+ itemCount?: number;
99
+ }
100
+ /**
101
+ * Read backlog items with optional filters
102
+ */
103
+ export declare const backlogReadTool: Tool<BacklogReadInput>;
104
+ /**
105
+ * Write/update backlog items
106
+ */
107
+ export declare const backlogWriteTool: Tool<BacklogWriteInput>;
108
+ /**
109
+ * Options for creating custom backlog tools
110
+ */
111
+ export interface BacklogToolOptions {
112
+ /** Custom base path for backlog file */
113
+ basePath?: string;
114
+ }
115
+ /**
116
+ * Create backlog tools with custom options
117
+ */
118
+ export declare function createBacklogTools(_options?: BacklogToolOptions): {
119
+ backlogRead: Tool<BacklogReadInput>;
120
+ backlogWrite: Tool<BacklogWriteInput>;
121
+ };
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Backlog Tools - File-based project backlog management
3
+ *
4
+ * Provides backlog_read and backlog_write tools for managing project backlogs.
5
+ * Uses a JSON file stored in .compilr/backlog.json (or backlog.json in project root).
6
+ *
7
+ * Features:
8
+ * - Read with filters (status, type, search, limit)
9
+ * - Write/update individual items or full backlog
10
+ * - Support for common item types (feature, bug, chore, spike)
11
+ * - Priority levels (critical, high, medium, low)
12
+ */
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+ import { defineTool, createSuccessResult, createErrorResult } from '../define.js';
16
+ // =============================================================================
17
+ // File Operations
18
+ // =============================================================================
19
+ const BACKLOG_PATHS = ['.compilr/backlog.json', 'backlog.json', '.claude/backlog.json'];
20
+ function findBacklogFile(cwd = process.cwd()) {
21
+ for (const relPath of BACKLOG_PATHS) {
22
+ const fullPath = path.join(cwd, relPath);
23
+ if (fs.existsSync(fullPath)) {
24
+ return fullPath;
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+ function getDefaultBacklogPath(cwd = process.cwd()) {
30
+ // Prefer .compilr directory if it exists
31
+ const compilrDir = path.join(cwd, '.compilr');
32
+ if (fs.existsSync(compilrDir)) {
33
+ return path.join(compilrDir, 'backlog.json');
34
+ }
35
+ // Otherwise use project root
36
+ return path.join(cwd, 'backlog.json');
37
+ }
38
+ function loadBacklog(filePath) {
39
+ try {
40
+ const content = fs.readFileSync(filePath, 'utf-8');
41
+ const data = JSON.parse(content);
42
+ return {
43
+ version: data.version || '1.0',
44
+ items: Array.isArray(data.items) ? data.items : [],
45
+ };
46
+ }
47
+ catch {
48
+ return { version: '1.0', items: [] };
49
+ }
50
+ }
51
+ function saveBacklog(filePath, backlog) {
52
+ // Ensure directory exists
53
+ const dir = path.dirname(filePath);
54
+ if (!fs.existsSync(dir)) {
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ }
57
+ // Update timestamps
58
+ const now = new Date().toISOString();
59
+ backlog.items = backlog.items.map((item) => ({
60
+ ...item,
61
+ updatedAt: now,
62
+ createdAt: item.createdAt || now,
63
+ }));
64
+ fs.writeFileSync(filePath, JSON.stringify(backlog, null, 2), 'utf-8');
65
+ }
66
+ function generateItemId(type, items) {
67
+ const prefix = {
68
+ feature: 'FEAT',
69
+ bug: 'BUG',
70
+ chore: 'CHORE',
71
+ spike: 'SPIKE',
72
+ }[type];
73
+ // Find highest number for this type
74
+ const existingIds = items
75
+ .filter((i) => i.id.startsWith(prefix))
76
+ .map((i) => parseInt(i.id.split('-')[1], 10))
77
+ .filter((n) => !isNaN(n));
78
+ const nextNum = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
79
+ return `${prefix}-${String(nextNum).padStart(3, '0')}`;
80
+ }
81
+ // =============================================================================
82
+ // Backlog Read Tool
83
+ // =============================================================================
84
+ /**
85
+ * Read backlog items with optional filters
86
+ */
87
+ export const backlogReadTool = defineTool({
88
+ name: 'backlog_read',
89
+ description: 'Read backlog items from the project. ' +
90
+ 'Use filters to narrow results. Use id parameter to get a specific item. ' +
91
+ 'Returns items sorted by priority (critical first) then by creation date.',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ id: {
96
+ type: 'string',
97
+ description: 'Get a specific item by ID (e.g., "FEAT-001")',
98
+ },
99
+ status: {
100
+ type: 'string',
101
+ enum: ['backlog', 'in-progress', 'done', 'blocked'],
102
+ description: 'Filter by status',
103
+ },
104
+ type: {
105
+ type: 'string',
106
+ enum: ['feature', 'bug', 'chore', 'spike'],
107
+ description: 'Filter by item type',
108
+ },
109
+ search: {
110
+ type: 'string',
111
+ description: 'Search in title and description',
112
+ },
113
+ priority: {
114
+ type: 'string',
115
+ enum: ['critical', 'high', 'medium', 'low'],
116
+ description: 'Filter by priority',
117
+ },
118
+ limit: {
119
+ type: 'number',
120
+ description: 'Maximum items to return (default: 20)',
121
+ },
122
+ },
123
+ },
124
+ // eslint-disable-next-line @typescript-eslint/require-await
125
+ execute: async (input) => {
126
+ try {
127
+ const filePath = findBacklogFile();
128
+ if (!filePath) {
129
+ return createSuccessResult({
130
+ items: [],
131
+ total: 0,
132
+ filtered: 0,
133
+ message: 'No backlog file found. Use backlog_write to create one.',
134
+ });
135
+ }
136
+ const backlog = loadBacklog(filePath);
137
+ let items = [...backlog.items];
138
+ const total = items.length;
139
+ // Filter by ID (exact match)
140
+ if (input.id) {
141
+ items = items.filter((i) => i.id === input.id);
142
+ }
143
+ // Filter by status
144
+ if (input.status) {
145
+ items = items.filter((i) => i.status === input.status);
146
+ }
147
+ // Filter by type
148
+ if (input.type) {
149
+ items = items.filter((i) => i.type === input.type);
150
+ }
151
+ // Filter by priority
152
+ if (input.priority) {
153
+ items = items.filter((i) => i.priority === input.priority);
154
+ }
155
+ // Search in title and description
156
+ if (input.search) {
157
+ const query = input.search.toLowerCase();
158
+ items = items.filter((i) => i.title.toLowerCase().includes(query) || i.description?.toLowerCase().includes(query));
159
+ }
160
+ // Sort by priority then by creation date
161
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
162
+ items.sort((a, b) => {
163
+ const pa = priorityOrder[a.priority || 'medium'];
164
+ const pb = priorityOrder[b.priority || 'medium'];
165
+ if (pa !== pb)
166
+ return pa - pb;
167
+ return (a.createdAt || '').localeCompare(b.createdAt || '');
168
+ });
169
+ // Apply limit
170
+ const limit = input.limit ?? 20;
171
+ const filtered = items.length;
172
+ items = items.slice(0, limit);
173
+ return createSuccessResult({
174
+ items,
175
+ total,
176
+ filtered,
177
+ });
178
+ }
179
+ catch (error) {
180
+ return createErrorResult(error instanceof Error ? error.message : String(error));
181
+ }
182
+ },
183
+ });
184
+ // =============================================================================
185
+ // Backlog Write Tool
186
+ // =============================================================================
187
+ /**
188
+ * Write/update backlog items
189
+ */
190
+ export const backlogWriteTool = defineTool({
191
+ name: 'backlog_write',
192
+ description: 'Create, update, or delete backlog items. ' +
193
+ 'Actions: add (new item), update (modify existing), delete (remove), replace (full list). ' +
194
+ 'IDs are auto-generated for new items based on type (e.g., FEAT-001, BUG-002).',
195
+ inputSchema: {
196
+ type: 'object',
197
+ properties: {
198
+ action: {
199
+ type: 'string',
200
+ enum: ['add', 'update', 'delete', 'replace'],
201
+ description: 'Action to perform',
202
+ },
203
+ item: {
204
+ type: 'object',
205
+ description: 'Item to add or update (for add/update actions)',
206
+ properties: {
207
+ id: { type: 'string', description: 'Item ID (required for update)' },
208
+ type: {
209
+ type: 'string',
210
+ enum: ['feature', 'bug', 'chore', 'spike'],
211
+ description: 'Item type (required for add)',
212
+ },
213
+ title: { type: 'string', description: 'Item title' },
214
+ description: { type: 'string', description: 'Detailed description' },
215
+ status: {
216
+ type: 'string',
217
+ enum: ['backlog', 'in-progress', 'done', 'blocked'],
218
+ description: 'Item status',
219
+ },
220
+ priority: {
221
+ type: 'string',
222
+ enum: ['critical', 'high', 'medium', 'low'],
223
+ description: 'Priority level',
224
+ },
225
+ acceptanceCriteria: {
226
+ type: 'array',
227
+ items: { type: 'string' },
228
+ description: 'Acceptance criteria list',
229
+ },
230
+ dependencies: {
231
+ type: 'array',
232
+ items: { type: 'string' },
233
+ description: 'IDs of items this depends on',
234
+ },
235
+ labels: {
236
+ type: 'array',
237
+ items: { type: 'string' },
238
+ description: 'Labels/tags',
239
+ },
240
+ },
241
+ },
242
+ deleteId: {
243
+ type: 'string',
244
+ description: 'Item ID to delete (for delete action)',
245
+ },
246
+ items: {
247
+ type: 'array',
248
+ description: 'Full list of items (for replace action)',
249
+ items: {
250
+ type: 'object',
251
+ },
252
+ },
253
+ },
254
+ required: ['action'],
255
+ },
256
+ // eslint-disable-next-line @typescript-eslint/require-await
257
+ execute: async (input) => {
258
+ try {
259
+ const existingPath = findBacklogFile();
260
+ const filePath = existingPath || getDefaultBacklogPath();
261
+ const backlog = existingPath ? loadBacklog(existingPath) : { version: '1.0', items: [] };
262
+ switch (input.action) {
263
+ case 'add': {
264
+ if (!input.item) {
265
+ return createErrorResult('Item is required for add action');
266
+ }
267
+ const itemType = input.item.type;
268
+ const itemTitle = input.item.title;
269
+ if (!itemType) {
270
+ return createErrorResult('Item type is required for add action');
271
+ }
272
+ if (!itemTitle) {
273
+ return createErrorResult('Item title is required for add action');
274
+ }
275
+ const newItem = {
276
+ id: generateItemId(itemType, backlog.items),
277
+ type: itemType,
278
+ title: itemTitle,
279
+ description: input.item.description,
280
+ status: input.item.status ?? 'backlog',
281
+ priority: input.item.priority,
282
+ acceptanceCriteria: input.item.acceptanceCriteria,
283
+ dependencies: input.item.dependencies,
284
+ labels: input.item.labels,
285
+ };
286
+ backlog.items.push(newItem);
287
+ saveBacklog(filePath, backlog);
288
+ return createSuccessResult({
289
+ success: true,
290
+ action: 'add',
291
+ itemId: newItem.id,
292
+ itemCount: backlog.items.length,
293
+ });
294
+ }
295
+ case 'update': {
296
+ const updateItem = input.item;
297
+ const updateId = updateItem?.id;
298
+ if (!updateItem || !updateId) {
299
+ return createErrorResult('Item ID is required for update action');
300
+ }
301
+ const idx = backlog.items.findIndex((i) => i.id === updateId);
302
+ if (idx === -1) {
303
+ return createErrorResult(`Item not found: ${updateId}`);
304
+ }
305
+ // Merge updates
306
+ backlog.items[idx] = {
307
+ ...backlog.items[idx],
308
+ ...updateItem,
309
+ id: backlog.items[idx].id, // Preserve original ID
310
+ };
311
+ saveBacklog(filePath, backlog);
312
+ return createSuccessResult({
313
+ success: true,
314
+ action: 'update',
315
+ itemId: updateId,
316
+ itemCount: backlog.items.length,
317
+ });
318
+ }
319
+ case 'delete': {
320
+ const deleteId = input.deleteId || input.item?.id;
321
+ if (!deleteId) {
322
+ return createErrorResult('deleteId or item.id is required for delete action');
323
+ }
324
+ const initialCount = backlog.items.length;
325
+ backlog.items = backlog.items.filter((i) => i.id !== deleteId);
326
+ if (backlog.items.length === initialCount) {
327
+ return createErrorResult(`Item not found: ${deleteId}`);
328
+ }
329
+ saveBacklog(filePath, backlog);
330
+ return createSuccessResult({
331
+ success: true,
332
+ action: 'delete',
333
+ itemId: deleteId,
334
+ itemCount: backlog.items.length,
335
+ });
336
+ }
337
+ case 'replace': {
338
+ if (!input.items) {
339
+ return createErrorResult('Items array is required for replace action');
340
+ }
341
+ backlog.items = input.items;
342
+ saveBacklog(filePath, backlog);
343
+ return createSuccessResult({
344
+ success: true,
345
+ action: 'replace',
346
+ itemCount: backlog.items.length,
347
+ });
348
+ }
349
+ default:
350
+ return createErrorResult(`Unknown action: ${input.action}`);
351
+ }
352
+ }
353
+ catch (error) {
354
+ return createErrorResult(error instanceof Error ? error.message : String(error));
355
+ }
356
+ },
357
+ });
358
+ /**
359
+ * Create backlog tools with custom options
360
+ */
361
+ export function createBacklogTools(_options = {}) {
362
+ // For now, just return the default tools
363
+ // Future: could customize file paths, storage backend, etc.
364
+ return {
365
+ backlogRead: backlogReadTool,
366
+ backlogWrite: backlogWriteTool,
367
+ };
368
+ }