@compilr-dev/sdk 0.2.3 → 0.2.4
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/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/platform/index.d.ts +2 -0
- package/dist/platform/index.js +2 -0
- package/dist/platform/sqlite/db.d.ts +24 -0
- package/dist/platform/sqlite/db.js +140 -0
- package/dist/platform/sqlite/document-repository.d.ts +19 -0
- package/dist/platform/sqlite/document-repository.js +126 -0
- package/dist/platform/sqlite/index.d.ts +40 -0
- package/dist/platform/sqlite/index.js +41 -0
- package/dist/platform/sqlite/plan-repository.d.ts +24 -0
- package/dist/platform/sqlite/plan-repository.js +205 -0
- package/dist/platform/sqlite/project-repository.d.ts +34 -0
- package/dist/platform/sqlite/project-repository.js +282 -0
- package/dist/platform/sqlite/schema.d.ts +65 -0
- package/dist/platform/sqlite/schema.js +159 -0
- package/dist/platform/sqlite/work-item-repository.d.ts +23 -0
- package/dist/platform/sqlite/work-item-repository.js +350 -0
- package/package.json +11 -4
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Work Item Repository — Concrete implementation of IWorkItemRepository.
|
|
3
|
+
*/
|
|
4
|
+
function recordToWorkItem(record) {
|
|
5
|
+
return {
|
|
6
|
+
id: record.id,
|
|
7
|
+
projectId: record.project_id,
|
|
8
|
+
itemNumber: record.item_number,
|
|
9
|
+
itemId: record.item_id,
|
|
10
|
+
type: record.type,
|
|
11
|
+
status: record.status,
|
|
12
|
+
priority: record.priority,
|
|
13
|
+
guidedStep: record.guided_step,
|
|
14
|
+
owner: record.owner,
|
|
15
|
+
title: record.title,
|
|
16
|
+
description: record.description,
|
|
17
|
+
estimatedEffort: record.estimated_effort,
|
|
18
|
+
actualMinutes: record.actual_minutes,
|
|
19
|
+
completedAt: record.completed_at ? new Date(record.completed_at) : null,
|
|
20
|
+
completedBy: record.completed_by,
|
|
21
|
+
commitHash: record.commit_hash,
|
|
22
|
+
createdAt: new Date(record.created_at),
|
|
23
|
+
updatedAt: new Date(record.updated_at),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function getTypePrefix(type) {
|
|
27
|
+
const prefixes = {
|
|
28
|
+
feature: 'REQ',
|
|
29
|
+
bug: 'BUG',
|
|
30
|
+
'tech-debt': 'TEC',
|
|
31
|
+
chore: 'CHR',
|
|
32
|
+
};
|
|
33
|
+
return prefixes[type];
|
|
34
|
+
}
|
|
35
|
+
const PRIORITY_ORDER = `
|
|
36
|
+
CASE priority
|
|
37
|
+
WHEN 'critical' THEN 1
|
|
38
|
+
WHEN 'high' THEN 2
|
|
39
|
+
WHEN 'medium' THEN 3
|
|
40
|
+
WHEN 'low' THEN 4
|
|
41
|
+
ELSE 5
|
|
42
|
+
END
|
|
43
|
+
`;
|
|
44
|
+
export class SQLiteWorkItemRepository {
|
|
45
|
+
db;
|
|
46
|
+
constructor(db) {
|
|
47
|
+
this.db = db;
|
|
48
|
+
}
|
|
49
|
+
create(input) {
|
|
50
|
+
const now = new Date().toISOString();
|
|
51
|
+
const maxResult = this.db
|
|
52
|
+
.prepare('SELECT MAX(item_number) as max_num FROM work_items WHERE project_id = ?')
|
|
53
|
+
.get(input.project_id);
|
|
54
|
+
const itemNumber = (maxResult.max_num ?? 0) + 1;
|
|
55
|
+
const itemId = `${getTypePrefix(input.type)}-${String(itemNumber).padStart(3, '0')}`;
|
|
56
|
+
const result = this.db
|
|
57
|
+
.prepare(`INSERT INTO work_items (
|
|
58
|
+
project_id, item_number, item_id, type, status, priority,
|
|
59
|
+
owner, title, description, estimated_effort,
|
|
60
|
+
created_at, updated_at
|
|
61
|
+
) VALUES (
|
|
62
|
+
@project_id, @item_number, @item_id, @type, @status, @priority,
|
|
63
|
+
@owner, @title, @description, @estimated_effort,
|
|
64
|
+
@created_at, @updated_at
|
|
65
|
+
)`)
|
|
66
|
+
.run({
|
|
67
|
+
project_id: input.project_id,
|
|
68
|
+
item_number: itemNumber,
|
|
69
|
+
item_id: itemId,
|
|
70
|
+
type: input.type,
|
|
71
|
+
status: 'backlog',
|
|
72
|
+
priority: input.priority ?? 'medium',
|
|
73
|
+
owner: input.owner ?? null,
|
|
74
|
+
title: input.title,
|
|
75
|
+
description: input.description ?? null,
|
|
76
|
+
estimated_effort: input.estimated_effort ?? null,
|
|
77
|
+
created_at: now,
|
|
78
|
+
updated_at: now,
|
|
79
|
+
});
|
|
80
|
+
const record = this.db
|
|
81
|
+
.prepare('SELECT * FROM work_items WHERE id = ?')
|
|
82
|
+
.get(Number(result.lastInsertRowid));
|
|
83
|
+
if (!record)
|
|
84
|
+
throw new Error('Failed to create work item');
|
|
85
|
+
return Promise.resolve(recordToWorkItem(record));
|
|
86
|
+
}
|
|
87
|
+
getById(id) {
|
|
88
|
+
const record = this.db.prepare('SELECT * FROM work_items WHERE id = ?').get(id);
|
|
89
|
+
return Promise.resolve(record ? recordToWorkItem(record) : null);
|
|
90
|
+
}
|
|
91
|
+
getByItemId(projectId, itemId) {
|
|
92
|
+
const record = this.db
|
|
93
|
+
.prepare('SELECT * FROM work_items WHERE project_id = ? AND item_id = ?')
|
|
94
|
+
.get(projectId, itemId);
|
|
95
|
+
return Promise.resolve(record ? recordToWorkItem(record) : null);
|
|
96
|
+
}
|
|
97
|
+
query(input) {
|
|
98
|
+
const conditions = [];
|
|
99
|
+
const params = {};
|
|
100
|
+
if (input.project_id) {
|
|
101
|
+
conditions.push('project_id = @project_id');
|
|
102
|
+
params.project_id = input.project_id;
|
|
103
|
+
}
|
|
104
|
+
if (input.status && input.status !== 'all') {
|
|
105
|
+
conditions.push('status = @status');
|
|
106
|
+
params.status = input.status;
|
|
107
|
+
}
|
|
108
|
+
if (input.type && input.type !== 'all') {
|
|
109
|
+
conditions.push('type = @type');
|
|
110
|
+
params.type = input.type;
|
|
111
|
+
}
|
|
112
|
+
if (input.priority && input.priority !== 'all') {
|
|
113
|
+
conditions.push('priority = @priority');
|
|
114
|
+
params.priority = input.priority;
|
|
115
|
+
}
|
|
116
|
+
if (input.owner && input.owner !== 'all') {
|
|
117
|
+
if (input.owner === 'unassigned') {
|
|
118
|
+
conditions.push('owner IS NULL');
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
conditions.push('owner = @owner');
|
|
122
|
+
params.owner = input.owner;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (input.search) {
|
|
126
|
+
conditions.push('(title LIKE @search OR description LIKE @search)');
|
|
127
|
+
params.search = `%${input.search}%`;
|
|
128
|
+
}
|
|
129
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
130
|
+
const countResult = this.db
|
|
131
|
+
.prepare(`SELECT COUNT(*) as count FROM work_items ${whereClause}`)
|
|
132
|
+
.get(params);
|
|
133
|
+
const limit = input.limit ?? 50;
|
|
134
|
+
const offset = input.offset ?? 0;
|
|
135
|
+
const query = `
|
|
136
|
+
SELECT * FROM work_items ${whereClause}
|
|
137
|
+
ORDER BY
|
|
138
|
+
CASE status WHEN 'in_progress' THEN 0 ELSE 1 END,
|
|
139
|
+
${PRIORITY_ORDER},
|
|
140
|
+
created_at ASC
|
|
141
|
+
LIMIT @limit OFFSET @offset
|
|
142
|
+
`;
|
|
143
|
+
params.limit = limit;
|
|
144
|
+
params.offset = offset;
|
|
145
|
+
const records = this.db.prepare(query).all(params);
|
|
146
|
+
return Promise.resolve({
|
|
147
|
+
items: records.map(recordToWorkItem),
|
|
148
|
+
total: countResult.count,
|
|
149
|
+
hasMore: offset + records.length < countResult.count,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
getNext(projectId, type) {
|
|
153
|
+
let query = `SELECT * FROM work_items WHERE project_id = ? AND status = 'backlog'`;
|
|
154
|
+
const params = [projectId];
|
|
155
|
+
if (type) {
|
|
156
|
+
query += ' AND type = ?';
|
|
157
|
+
params.push(type);
|
|
158
|
+
}
|
|
159
|
+
query += ` ORDER BY ${PRIORITY_ORDER}, created_at ASC LIMIT 1`;
|
|
160
|
+
const record = this.db.prepare(query).get(...params);
|
|
161
|
+
return Promise.resolve(record ? recordToWorkItem(record) : null);
|
|
162
|
+
}
|
|
163
|
+
update(id, input) {
|
|
164
|
+
const oldRecord = this.db.prepare('SELECT * FROM work_items WHERE id = ?').get(id);
|
|
165
|
+
if (!oldRecord)
|
|
166
|
+
return Promise.resolve(null);
|
|
167
|
+
const oldItem = recordToWorkItem(oldRecord);
|
|
168
|
+
const updates = [];
|
|
169
|
+
const params = { id };
|
|
170
|
+
if (input.type !== undefined) {
|
|
171
|
+
updates.push('type = @type');
|
|
172
|
+
params.type = input.type;
|
|
173
|
+
}
|
|
174
|
+
if (input.status !== undefined) {
|
|
175
|
+
updates.push('status = @status');
|
|
176
|
+
params.status = input.status;
|
|
177
|
+
if (input.status === 'completed' && oldItem.status !== 'completed') {
|
|
178
|
+
updates.push('completed_at = @completed_at');
|
|
179
|
+
params.completed_at = new Date().toISOString();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (input.priority !== undefined) {
|
|
183
|
+
updates.push('priority = @priority');
|
|
184
|
+
params.priority = input.priority;
|
|
185
|
+
}
|
|
186
|
+
if (input.guided_step !== undefined) {
|
|
187
|
+
updates.push('guided_step = @guided_step');
|
|
188
|
+
params.guided_step = input.guided_step;
|
|
189
|
+
}
|
|
190
|
+
if (input.owner !== undefined) {
|
|
191
|
+
updates.push('owner = @owner');
|
|
192
|
+
params.owner = input.owner;
|
|
193
|
+
}
|
|
194
|
+
if (input.title !== undefined) {
|
|
195
|
+
updates.push('title = @title');
|
|
196
|
+
params.title = input.title;
|
|
197
|
+
}
|
|
198
|
+
if (input.description !== undefined) {
|
|
199
|
+
updates.push('description = @description');
|
|
200
|
+
params.description = input.description;
|
|
201
|
+
}
|
|
202
|
+
if (input.estimated_effort !== undefined) {
|
|
203
|
+
updates.push('estimated_effort = @estimated_effort');
|
|
204
|
+
params.estimated_effort = input.estimated_effort;
|
|
205
|
+
}
|
|
206
|
+
if (input.actual_minutes !== undefined) {
|
|
207
|
+
updates.push('actual_minutes = @actual_minutes');
|
|
208
|
+
params.actual_minutes = input.actual_minutes;
|
|
209
|
+
}
|
|
210
|
+
if (input.commit_hash !== undefined) {
|
|
211
|
+
updates.push('commit_hash = @commit_hash');
|
|
212
|
+
params.commit_hash = input.commit_hash;
|
|
213
|
+
}
|
|
214
|
+
if (updates.length === 0)
|
|
215
|
+
return Promise.resolve(oldItem);
|
|
216
|
+
updates.push('updated_at = @updated_at');
|
|
217
|
+
params.updated_at = new Date().toISOString();
|
|
218
|
+
this.db.prepare(`UPDATE work_items SET ${updates.join(', ')} WHERE id = @id`).run(params);
|
|
219
|
+
// Record history
|
|
220
|
+
this.recordHistory(id, oldItem.projectId, input, oldItem);
|
|
221
|
+
const record = this.db.prepare('SELECT * FROM work_items WHERE id = ?').get(id);
|
|
222
|
+
return Promise.resolve(record ? recordToWorkItem(record) : null);
|
|
223
|
+
}
|
|
224
|
+
delete(id) {
|
|
225
|
+
const result = this.db.prepare('DELETE FROM work_items WHERE id = ?').run(id);
|
|
226
|
+
return Promise.resolve(result.changes > 0);
|
|
227
|
+
}
|
|
228
|
+
getByOwner(projectId, owner, status) {
|
|
229
|
+
let query = 'SELECT * FROM work_items WHERE project_id = ?';
|
|
230
|
+
const params = [projectId];
|
|
231
|
+
if (owner === 'unassigned') {
|
|
232
|
+
query += ' AND owner IS NULL';
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
query += ' AND owner = ?';
|
|
236
|
+
params.push(owner);
|
|
237
|
+
}
|
|
238
|
+
if (status) {
|
|
239
|
+
query += ' AND status = ?';
|
|
240
|
+
params.push(status);
|
|
241
|
+
}
|
|
242
|
+
query += ' ORDER BY priority, created_at ASC';
|
|
243
|
+
const records = this.db.prepare(query).all(...params);
|
|
244
|
+
return Promise.resolve(records.map(recordToWorkItem));
|
|
245
|
+
}
|
|
246
|
+
getOwnerCounts(projectId) {
|
|
247
|
+
const results = this.db
|
|
248
|
+
.prepare(`SELECT COALESCE(owner, 'unassigned') as owner, COUNT(*) as count
|
|
249
|
+
FROM work_items
|
|
250
|
+
WHERE project_id = ? AND status != 'completed' AND status != 'skipped'
|
|
251
|
+
GROUP BY owner`)
|
|
252
|
+
.all(projectId);
|
|
253
|
+
const counts = {};
|
|
254
|
+
for (const row of results) {
|
|
255
|
+
counts[row.owner] = row.count;
|
|
256
|
+
}
|
|
257
|
+
return Promise.resolve(counts);
|
|
258
|
+
}
|
|
259
|
+
getStatusCounts(projectId) {
|
|
260
|
+
const results = this.db
|
|
261
|
+
.prepare('SELECT status, COUNT(*) as count FROM work_items WHERE project_id = ? GROUP BY status')
|
|
262
|
+
.all(projectId);
|
|
263
|
+
const counts = {
|
|
264
|
+
backlog: 0,
|
|
265
|
+
in_progress: 0,
|
|
266
|
+
completed: 0,
|
|
267
|
+
skipped: 0,
|
|
268
|
+
};
|
|
269
|
+
for (const row of results) {
|
|
270
|
+
counts[row.status] = row.count;
|
|
271
|
+
}
|
|
272
|
+
return Promise.resolve(counts);
|
|
273
|
+
}
|
|
274
|
+
getHistory(workItemId) {
|
|
275
|
+
const records = this.db
|
|
276
|
+
.prepare('SELECT * FROM work_item_history WHERE work_item_id = ? ORDER BY changed_at DESC')
|
|
277
|
+
.all(workItemId);
|
|
278
|
+
return Promise.resolve(records.map((r) => ({
|
|
279
|
+
action: r.action,
|
|
280
|
+
oldValue: r.old_value,
|
|
281
|
+
newValue: r.new_value,
|
|
282
|
+
notes: r.notes,
|
|
283
|
+
changedBy: r.changed_by,
|
|
284
|
+
changedAt: new Date(r.changed_at),
|
|
285
|
+
})));
|
|
286
|
+
}
|
|
287
|
+
bulkCreate(projectId, items) {
|
|
288
|
+
const now = new Date().toISOString();
|
|
289
|
+
const maxResult = this.db
|
|
290
|
+
.prepare('SELECT MAX(item_number) as max_num FROM work_items WHERE project_id = ?')
|
|
291
|
+
.get(projectId);
|
|
292
|
+
let itemNumber = (maxResult.max_num ?? 0) + 1;
|
|
293
|
+
const createdIds = [];
|
|
294
|
+
const insertStmt = this.db.prepare(`
|
|
295
|
+
INSERT INTO work_items (
|
|
296
|
+
project_id, item_number, item_id, type, status, priority,
|
|
297
|
+
owner, title, description, created_at, updated_at
|
|
298
|
+
) VALUES (
|
|
299
|
+
@project_id, @item_number, @item_id, @type, @status, @priority,
|
|
300
|
+
@owner, @title, @description, @created_at, @updated_at
|
|
301
|
+
)
|
|
302
|
+
`);
|
|
303
|
+
const insertAll = this.db.transaction(() => {
|
|
304
|
+
for (const item of items) {
|
|
305
|
+
const itemId = `${getTypePrefix(item.type)}-${String(itemNumber).padStart(3, '0')}`;
|
|
306
|
+
const result = insertStmt.run({
|
|
307
|
+
project_id: projectId,
|
|
308
|
+
item_number: itemNumber,
|
|
309
|
+
item_id: itemId,
|
|
310
|
+
type: item.type,
|
|
311
|
+
status: 'backlog',
|
|
312
|
+
priority: item.priority ?? 'medium',
|
|
313
|
+
owner: item.owner ?? null,
|
|
314
|
+
title: item.title,
|
|
315
|
+
description: item.description ?? null,
|
|
316
|
+
created_at: now,
|
|
317
|
+
updated_at: now,
|
|
318
|
+
});
|
|
319
|
+
createdIds.push(Number(result.lastInsertRowid));
|
|
320
|
+
itemNumber++;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
insertAll();
|
|
324
|
+
const results = createdIds
|
|
325
|
+
.map((rowId) => {
|
|
326
|
+
const record = this.db.prepare('SELECT * FROM work_items WHERE id = ?').get(rowId);
|
|
327
|
+
return record ? recordToWorkItem(record) : null;
|
|
328
|
+
})
|
|
329
|
+
.filter((item) => item !== null);
|
|
330
|
+
return Promise.resolve(results);
|
|
331
|
+
}
|
|
332
|
+
// Internal: record history entries
|
|
333
|
+
recordHistory(workItemId, projectId, changes, oldItem) {
|
|
334
|
+
const now = new Date().toISOString();
|
|
335
|
+
const stmt = this.db.prepare(`INSERT INTO work_item_history (work_item_id, project_id, action, old_value, new_value, changed_by, changed_at)
|
|
336
|
+
VALUES (?, ?, ?, ?, ?, 'agent', ?)`);
|
|
337
|
+
if (changes.status && changes.status !== oldItem.status) {
|
|
338
|
+
stmt.run(workItemId, projectId, 'status_change', oldItem.status, changes.status, now);
|
|
339
|
+
}
|
|
340
|
+
if (changes.priority && changes.priority !== oldItem.priority) {
|
|
341
|
+
stmt.run(workItemId, projectId, 'priority_change', oldItem.priority, changes.priority, now);
|
|
342
|
+
}
|
|
343
|
+
if (changes.guided_step !== undefined && changes.guided_step !== oldItem.guidedStep) {
|
|
344
|
+
stmt.run(workItemId, projectId, 'step_advance', oldItem.guidedStep ?? 'none', changes.guided_step ?? 'none', now);
|
|
345
|
+
}
|
|
346
|
+
if (changes.owner !== undefined && changes.owner !== oldItem.owner) {
|
|
347
|
+
stmt.run(workItemId, projectId, 'owner_change', oldItem.owner ?? 'unassigned', changes.owner ?? 'unassigned', now);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compilr-dev/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Universal agent runtime for building AI-powered applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -56,15 +56,22 @@
|
|
|
56
56
|
"ajv": "^6.14.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@compilr-dev/agents": "^0.3.
|
|
60
|
-
"@compilr-dev/agents-coding": "^1.0.2"
|
|
59
|
+
"@compilr-dev/agents": "^0.3.25",
|
|
60
|
+
"@compilr-dev/agents-coding": "^1.0.2",
|
|
61
|
+
"better-sqlite3": "^11.0.0 || ^12.0.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"better-sqlite3": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
61
67
|
},
|
|
62
68
|
"devDependencies": {
|
|
63
69
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
64
|
-
"@compilr-dev/agents": "^0.3.
|
|
70
|
+
"@compilr-dev/agents": "^0.3.25",
|
|
65
71
|
"@compilr-dev/agents-coding": "^1.0.2",
|
|
66
72
|
"@eslint/js": "^9.39.1",
|
|
67
73
|
"@opentelemetry/api": "^1.9.0",
|
|
74
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
68
75
|
"@types/node": "^25.2.3",
|
|
69
76
|
"@vitest/coverage-v8": "^4.0.18",
|
|
70
77
|
"eslint": "^9.39.1",
|