@hasna/todos 0.1.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/LICENSE +190 -0
- package/dist/cli/index.js +8670 -0
- package/dist/index.d.ts +236 -0
- package/dist/index.js +711 -0
- package/dist/mcp/index.js +4882 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/db/database.ts
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import { existsSync, mkdirSync } from "fs";
|
|
5
|
+
import { dirname, join, resolve } from "path";
|
|
6
|
+
var LOCK_EXPIRY_MINUTES = 30;
|
|
7
|
+
function getDbPath() {
|
|
8
|
+
if (process.env["TODOS_DB_PATH"]) {
|
|
9
|
+
return process.env["TODOS_DB_PATH"];
|
|
10
|
+
}
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const localDb = join(cwd, ".todos", "todos.db");
|
|
13
|
+
if (existsSync(localDb)) {
|
|
14
|
+
return localDb;
|
|
15
|
+
}
|
|
16
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
17
|
+
return join(home, ".todos", "todos.db");
|
|
18
|
+
}
|
|
19
|
+
function ensureDir(filePath) {
|
|
20
|
+
const dir = dirname(resolve(filePath));
|
|
21
|
+
if (!existsSync(dir)) {
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
var MIGRATIONS = [
|
|
26
|
+
`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
name TEXT NOT NULL,
|
|
30
|
+
path TEXT UNIQUE NOT NULL,
|
|
31
|
+
description TEXT,
|
|
32
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
33
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
39
|
+
parent_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
|
|
40
|
+
title TEXT NOT NULL,
|
|
41
|
+
description TEXT,
|
|
42
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'failed', 'cancelled')),
|
|
43
|
+
priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high', 'critical')),
|
|
44
|
+
agent_id TEXT,
|
|
45
|
+
assigned_to TEXT,
|
|
46
|
+
session_id TEXT,
|
|
47
|
+
working_dir TEXT,
|
|
48
|
+
tags TEXT DEFAULT '[]',
|
|
49
|
+
metadata TEXT DEFAULT '{}',
|
|
50
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
51
|
+
locked_by TEXT,
|
|
52
|
+
locked_at TEXT,
|
|
53
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
54
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
55
|
+
completed_at TEXT
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
59
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
60
|
+
depends_on TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
61
|
+
PRIMARY KEY (task_id, depends_on),
|
|
62
|
+
CHECK (task_id != depends_on)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS task_comments (
|
|
66
|
+
id TEXT PRIMARY KEY,
|
|
67
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
68
|
+
agent_id TEXT,
|
|
69
|
+
session_id TEXT,
|
|
70
|
+
content TEXT NOT NULL,
|
|
71
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
75
|
+
id TEXT PRIMARY KEY,
|
|
76
|
+
agent_id TEXT,
|
|
77
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
78
|
+
working_dir TEXT,
|
|
79
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
80
|
+
last_activity TEXT NOT NULL DEFAULT (datetime('now')),
|
|
81
|
+
metadata TEXT DEFAULT '{}'
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_agent ON tasks(agent_id);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_comments_task ON task_comments(task_id);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
94
|
+
|
|
95
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
96
|
+
id INTEGER PRIMARY KEY,
|
|
97
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (1);
|
|
101
|
+
`
|
|
102
|
+
];
|
|
103
|
+
var _db = null;
|
|
104
|
+
function getDatabase(dbPath) {
|
|
105
|
+
if (_db)
|
|
106
|
+
return _db;
|
|
107
|
+
const path = dbPath || getDbPath();
|
|
108
|
+
ensureDir(path);
|
|
109
|
+
_db = new Database(path, { create: true });
|
|
110
|
+
_db.run("PRAGMA journal_mode = WAL");
|
|
111
|
+
_db.run("PRAGMA busy_timeout = 5000");
|
|
112
|
+
_db.run("PRAGMA foreign_keys = ON");
|
|
113
|
+
runMigrations(_db);
|
|
114
|
+
return _db;
|
|
115
|
+
}
|
|
116
|
+
function runMigrations(db) {
|
|
117
|
+
try {
|
|
118
|
+
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
119
|
+
const currentLevel = result?.max_id ?? 0;
|
|
120
|
+
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
121
|
+
db.run(MIGRATIONS[i]);
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
for (const migration of MIGRATIONS) {
|
|
125
|
+
db.exec(migration);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function closeDatabase() {
|
|
130
|
+
if (_db) {
|
|
131
|
+
_db.close();
|
|
132
|
+
_db = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function resetDatabase() {
|
|
136
|
+
_db = null;
|
|
137
|
+
}
|
|
138
|
+
function now() {
|
|
139
|
+
return new Date().toISOString();
|
|
140
|
+
}
|
|
141
|
+
function uuid() {
|
|
142
|
+
return crypto.randomUUID();
|
|
143
|
+
}
|
|
144
|
+
function isLockExpired(lockedAt) {
|
|
145
|
+
if (!lockedAt)
|
|
146
|
+
return true;
|
|
147
|
+
const lockTime = new Date(lockedAt).getTime();
|
|
148
|
+
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
149
|
+
return Date.now() - lockTime > expiryMs;
|
|
150
|
+
}
|
|
151
|
+
function resolvePartialId(db, table, partialId) {
|
|
152
|
+
if (partialId.length >= 36) {
|
|
153
|
+
const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
|
|
154
|
+
return row?.id ?? null;
|
|
155
|
+
}
|
|
156
|
+
const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
|
|
157
|
+
if (rows.length === 1) {
|
|
158
|
+
return rows[0].id;
|
|
159
|
+
}
|
|
160
|
+
if (rows.length > 1) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
// src/types/index.ts
|
|
166
|
+
var TASK_STATUSES = [
|
|
167
|
+
"pending",
|
|
168
|
+
"in_progress",
|
|
169
|
+
"completed",
|
|
170
|
+
"failed",
|
|
171
|
+
"cancelled"
|
|
172
|
+
];
|
|
173
|
+
var TASK_PRIORITIES = [
|
|
174
|
+
"low",
|
|
175
|
+
"medium",
|
|
176
|
+
"high",
|
|
177
|
+
"critical"
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
class VersionConflictError extends Error {
|
|
181
|
+
taskId;
|
|
182
|
+
expectedVersion;
|
|
183
|
+
actualVersion;
|
|
184
|
+
constructor(taskId, expectedVersion, actualVersion) {
|
|
185
|
+
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
186
|
+
this.taskId = taskId;
|
|
187
|
+
this.expectedVersion = expectedVersion;
|
|
188
|
+
this.actualVersion = actualVersion;
|
|
189
|
+
this.name = "VersionConflictError";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
class TaskNotFoundError extends Error {
|
|
194
|
+
taskId;
|
|
195
|
+
constructor(taskId) {
|
|
196
|
+
super(`Task not found: ${taskId}`);
|
|
197
|
+
this.taskId = taskId;
|
|
198
|
+
this.name = "TaskNotFoundError";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
class ProjectNotFoundError extends Error {
|
|
203
|
+
projectId;
|
|
204
|
+
constructor(projectId) {
|
|
205
|
+
super(`Project not found: ${projectId}`);
|
|
206
|
+
this.projectId = projectId;
|
|
207
|
+
this.name = "ProjectNotFoundError";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
class LockError extends Error {
|
|
212
|
+
taskId;
|
|
213
|
+
lockedBy;
|
|
214
|
+
constructor(taskId, lockedBy) {
|
|
215
|
+
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
216
|
+
this.taskId = taskId;
|
|
217
|
+
this.lockedBy = lockedBy;
|
|
218
|
+
this.name = "LockError";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
class DependencyCycleError extends Error {
|
|
223
|
+
taskId;
|
|
224
|
+
dependsOn;
|
|
225
|
+
constructor(taskId, dependsOn) {
|
|
226
|
+
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
227
|
+
this.taskId = taskId;
|
|
228
|
+
this.dependsOn = dependsOn;
|
|
229
|
+
this.name = "DependencyCycleError";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/db/tasks.ts
|
|
234
|
+
function rowToTask(row) {
|
|
235
|
+
return {
|
|
236
|
+
...row,
|
|
237
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
238
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
239
|
+
status: row.status,
|
|
240
|
+
priority: row.priority
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function createTask(input, db) {
|
|
244
|
+
const d = db || getDatabase();
|
|
245
|
+
const id = uuid();
|
|
246
|
+
const timestamp = now();
|
|
247
|
+
d.run(`INSERT INTO tasks (id, project_id, parent_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at)
|
|
248
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)`, [
|
|
249
|
+
id,
|
|
250
|
+
input.project_id || null,
|
|
251
|
+
input.parent_id || null,
|
|
252
|
+
input.title,
|
|
253
|
+
input.description || null,
|
|
254
|
+
input.status || "pending",
|
|
255
|
+
input.priority || "medium",
|
|
256
|
+
input.agent_id || null,
|
|
257
|
+
input.assigned_to || null,
|
|
258
|
+
input.session_id || null,
|
|
259
|
+
input.working_dir || null,
|
|
260
|
+
JSON.stringify(input.tags || []),
|
|
261
|
+
JSON.stringify(input.metadata || {}),
|
|
262
|
+
timestamp,
|
|
263
|
+
timestamp
|
|
264
|
+
]);
|
|
265
|
+
return getTask(id, d);
|
|
266
|
+
}
|
|
267
|
+
function getTask(id, db) {
|
|
268
|
+
const d = db || getDatabase();
|
|
269
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
270
|
+
if (!row)
|
|
271
|
+
return null;
|
|
272
|
+
return rowToTask(row);
|
|
273
|
+
}
|
|
274
|
+
function getTaskWithRelations(id, db) {
|
|
275
|
+
const d = db || getDatabase();
|
|
276
|
+
const task = getTask(id, d);
|
|
277
|
+
if (!task)
|
|
278
|
+
return null;
|
|
279
|
+
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
280
|
+
const subtasks = subtaskRows.map(rowToTask);
|
|
281
|
+
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
282
|
+
JOIN task_dependencies td ON td.depends_on = t.id
|
|
283
|
+
WHERE td.task_id = ?`).all(id);
|
|
284
|
+
const dependencies = depRows.map(rowToTask);
|
|
285
|
+
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
286
|
+
JOIN task_dependencies td ON td.task_id = t.id
|
|
287
|
+
WHERE td.depends_on = ?`).all(id);
|
|
288
|
+
const blocked_by = blockedByRows.map(rowToTask);
|
|
289
|
+
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
290
|
+
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
291
|
+
return {
|
|
292
|
+
...task,
|
|
293
|
+
subtasks,
|
|
294
|
+
dependencies,
|
|
295
|
+
blocked_by,
|
|
296
|
+
comments,
|
|
297
|
+
parent
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function listTasks(filter = {}, db) {
|
|
301
|
+
const d = db || getDatabase();
|
|
302
|
+
const conditions = [];
|
|
303
|
+
const params = [];
|
|
304
|
+
if (filter.project_id) {
|
|
305
|
+
conditions.push("project_id = ?");
|
|
306
|
+
params.push(filter.project_id);
|
|
307
|
+
}
|
|
308
|
+
if (filter.parent_id !== undefined) {
|
|
309
|
+
if (filter.parent_id === null) {
|
|
310
|
+
conditions.push("parent_id IS NULL");
|
|
311
|
+
} else {
|
|
312
|
+
conditions.push("parent_id = ?");
|
|
313
|
+
params.push(filter.parent_id);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (filter.status) {
|
|
317
|
+
if (Array.isArray(filter.status)) {
|
|
318
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
319
|
+
params.push(...filter.status);
|
|
320
|
+
} else {
|
|
321
|
+
conditions.push("status = ?");
|
|
322
|
+
params.push(filter.status);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (filter.priority) {
|
|
326
|
+
if (Array.isArray(filter.priority)) {
|
|
327
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
328
|
+
params.push(...filter.priority);
|
|
329
|
+
} else {
|
|
330
|
+
conditions.push("priority = ?");
|
|
331
|
+
params.push(filter.priority);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (filter.assigned_to) {
|
|
335
|
+
conditions.push("assigned_to = ?");
|
|
336
|
+
params.push(filter.assigned_to);
|
|
337
|
+
}
|
|
338
|
+
if (filter.agent_id) {
|
|
339
|
+
conditions.push("agent_id = ?");
|
|
340
|
+
params.push(filter.agent_id);
|
|
341
|
+
}
|
|
342
|
+
if (filter.session_id) {
|
|
343
|
+
conditions.push("session_id = ?");
|
|
344
|
+
params.push(filter.session_id);
|
|
345
|
+
}
|
|
346
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
347
|
+
const tagConditions = filter.tags.map(() => "tags LIKE ?");
|
|
348
|
+
conditions.push(`(${tagConditions.join(" OR ")})`);
|
|
349
|
+
params.push(...filter.tags.map((t) => `%"${t}"%`));
|
|
350
|
+
}
|
|
351
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
352
|
+
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
|
|
353
|
+
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
354
|
+
created_at DESC`).all(...params);
|
|
355
|
+
return rows.map(rowToTask);
|
|
356
|
+
}
|
|
357
|
+
function updateTask(id, input, db) {
|
|
358
|
+
const d = db || getDatabase();
|
|
359
|
+
const task = getTask(id, d);
|
|
360
|
+
if (!task)
|
|
361
|
+
throw new TaskNotFoundError(id);
|
|
362
|
+
if (task.version !== input.version) {
|
|
363
|
+
throw new VersionConflictError(id, input.version, task.version);
|
|
364
|
+
}
|
|
365
|
+
const sets = ["version = version + 1", "updated_at = ?"];
|
|
366
|
+
const params = [now()];
|
|
367
|
+
if (input.title !== undefined) {
|
|
368
|
+
sets.push("title = ?");
|
|
369
|
+
params.push(input.title);
|
|
370
|
+
}
|
|
371
|
+
if (input.description !== undefined) {
|
|
372
|
+
sets.push("description = ?");
|
|
373
|
+
params.push(input.description);
|
|
374
|
+
}
|
|
375
|
+
if (input.status !== undefined) {
|
|
376
|
+
sets.push("status = ?");
|
|
377
|
+
params.push(input.status);
|
|
378
|
+
if (input.status === "completed") {
|
|
379
|
+
sets.push("completed_at = ?");
|
|
380
|
+
params.push(now());
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (input.priority !== undefined) {
|
|
384
|
+
sets.push("priority = ?");
|
|
385
|
+
params.push(input.priority);
|
|
386
|
+
}
|
|
387
|
+
if (input.assigned_to !== undefined) {
|
|
388
|
+
sets.push("assigned_to = ?");
|
|
389
|
+
params.push(input.assigned_to);
|
|
390
|
+
}
|
|
391
|
+
if (input.tags !== undefined) {
|
|
392
|
+
sets.push("tags = ?");
|
|
393
|
+
params.push(JSON.stringify(input.tags));
|
|
394
|
+
}
|
|
395
|
+
if (input.metadata !== undefined) {
|
|
396
|
+
sets.push("metadata = ?");
|
|
397
|
+
params.push(JSON.stringify(input.metadata));
|
|
398
|
+
}
|
|
399
|
+
params.push(id, input.version);
|
|
400
|
+
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
401
|
+
if (result.changes === 0) {
|
|
402
|
+
const current = getTask(id, d);
|
|
403
|
+
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
404
|
+
}
|
|
405
|
+
return getTask(id, d);
|
|
406
|
+
}
|
|
407
|
+
function deleteTask(id, db) {
|
|
408
|
+
const d = db || getDatabase();
|
|
409
|
+
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
410
|
+
return result.changes > 0;
|
|
411
|
+
}
|
|
412
|
+
function startTask(id, agentId, db) {
|
|
413
|
+
const d = db || getDatabase();
|
|
414
|
+
const task = getTask(id, d);
|
|
415
|
+
if (!task)
|
|
416
|
+
throw new TaskNotFoundError(id);
|
|
417
|
+
if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
418
|
+
throw new LockError(id, task.locked_by);
|
|
419
|
+
}
|
|
420
|
+
const timestamp = now();
|
|
421
|
+
d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
422
|
+
WHERE id = ?`, [agentId, agentId, timestamp, timestamp, id]);
|
|
423
|
+
return getTask(id, d);
|
|
424
|
+
}
|
|
425
|
+
function completeTask(id, agentId, db) {
|
|
426
|
+
const d = db || getDatabase();
|
|
427
|
+
const task = getTask(id, d);
|
|
428
|
+
if (!task)
|
|
429
|
+
throw new TaskNotFoundError(id);
|
|
430
|
+
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
431
|
+
throw new LockError(id, task.locked_by);
|
|
432
|
+
}
|
|
433
|
+
const timestamp = now();
|
|
434
|
+
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
435
|
+
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
436
|
+
return getTask(id, d);
|
|
437
|
+
}
|
|
438
|
+
function lockTask(id, agentId, db) {
|
|
439
|
+
const d = db || getDatabase();
|
|
440
|
+
const task = getTask(id, d);
|
|
441
|
+
if (!task)
|
|
442
|
+
throw new TaskNotFoundError(id);
|
|
443
|
+
if (task.locked_by === agentId) {
|
|
444
|
+
return { success: true, locked_by: agentId, locked_at: task.locked_at };
|
|
445
|
+
}
|
|
446
|
+
if (task.locked_by && !isLockExpired(task.locked_at)) {
|
|
447
|
+
return {
|
|
448
|
+
success: false,
|
|
449
|
+
locked_by: task.locked_by,
|
|
450
|
+
locked_at: task.locked_at,
|
|
451
|
+
error: `Task is locked by ${task.locked_by}`
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const timestamp = now();
|
|
455
|
+
d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
456
|
+
WHERE id = ?`, [agentId, timestamp, timestamp, id]);
|
|
457
|
+
return { success: true, locked_by: agentId, locked_at: timestamp };
|
|
458
|
+
}
|
|
459
|
+
function unlockTask(id, agentId, db) {
|
|
460
|
+
const d = db || getDatabase();
|
|
461
|
+
const task = getTask(id, d);
|
|
462
|
+
if (!task)
|
|
463
|
+
throw new TaskNotFoundError(id);
|
|
464
|
+
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
465
|
+
throw new LockError(id, task.locked_by);
|
|
466
|
+
}
|
|
467
|
+
const timestamp = now();
|
|
468
|
+
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
|
|
469
|
+
WHERE id = ?`, [timestamp, id]);
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
function addDependency(taskId, dependsOn, db) {
|
|
473
|
+
const d = db || getDatabase();
|
|
474
|
+
if (!getTask(taskId, d))
|
|
475
|
+
throw new TaskNotFoundError(taskId);
|
|
476
|
+
if (!getTask(dependsOn, d))
|
|
477
|
+
throw new TaskNotFoundError(dependsOn);
|
|
478
|
+
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
479
|
+
throw new DependencyCycleError(taskId, dependsOn);
|
|
480
|
+
}
|
|
481
|
+
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
|
|
482
|
+
}
|
|
483
|
+
function removeDependency(taskId, dependsOn, db) {
|
|
484
|
+
const d = db || getDatabase();
|
|
485
|
+
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
486
|
+
return result.changes > 0;
|
|
487
|
+
}
|
|
488
|
+
function getTaskDependencies(taskId, db) {
|
|
489
|
+
const d = db || getDatabase();
|
|
490
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
491
|
+
}
|
|
492
|
+
function getTaskDependents(taskId, db) {
|
|
493
|
+
const d = db || getDatabase();
|
|
494
|
+
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
495
|
+
}
|
|
496
|
+
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
497
|
+
const visited = new Set;
|
|
498
|
+
const queue = [dependsOn];
|
|
499
|
+
while (queue.length > 0) {
|
|
500
|
+
const current = queue.shift();
|
|
501
|
+
if (current === taskId)
|
|
502
|
+
return true;
|
|
503
|
+
if (visited.has(current))
|
|
504
|
+
continue;
|
|
505
|
+
visited.add(current);
|
|
506
|
+
const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
|
|
507
|
+
for (const dep of deps) {
|
|
508
|
+
queue.push(dep.depends_on);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
// src/db/projects.ts
|
|
514
|
+
function createProject(input, db) {
|
|
515
|
+
const d = db || getDatabase();
|
|
516
|
+
const id = uuid();
|
|
517
|
+
const timestamp = now();
|
|
518
|
+
d.run(`INSERT INTO projects (id, name, path, description, created_at, updated_at)
|
|
519
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, timestamp, timestamp]);
|
|
520
|
+
return getProject(id, d);
|
|
521
|
+
}
|
|
522
|
+
function getProject(id, db) {
|
|
523
|
+
const d = db || getDatabase();
|
|
524
|
+
const row = d.query("SELECT * FROM projects WHERE id = ?").get(id);
|
|
525
|
+
return row;
|
|
526
|
+
}
|
|
527
|
+
function getProjectByPath(path, db) {
|
|
528
|
+
const d = db || getDatabase();
|
|
529
|
+
const row = d.query("SELECT * FROM projects WHERE path = ?").get(path);
|
|
530
|
+
return row;
|
|
531
|
+
}
|
|
532
|
+
function listProjects(db) {
|
|
533
|
+
const d = db || getDatabase();
|
|
534
|
+
return d.query("SELECT * FROM projects ORDER BY name").all();
|
|
535
|
+
}
|
|
536
|
+
function updateProject(id, input, db) {
|
|
537
|
+
const d = db || getDatabase();
|
|
538
|
+
const project = getProject(id, d);
|
|
539
|
+
if (!project)
|
|
540
|
+
throw new ProjectNotFoundError(id);
|
|
541
|
+
const sets = ["updated_at = ?"];
|
|
542
|
+
const params = [now()];
|
|
543
|
+
if (input.name !== undefined) {
|
|
544
|
+
sets.push("name = ?");
|
|
545
|
+
params.push(input.name);
|
|
546
|
+
}
|
|
547
|
+
if (input.description !== undefined) {
|
|
548
|
+
sets.push("description = ?");
|
|
549
|
+
params.push(input.description);
|
|
550
|
+
}
|
|
551
|
+
params.push(id);
|
|
552
|
+
d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
553
|
+
return getProject(id, d);
|
|
554
|
+
}
|
|
555
|
+
function deleteProject(id, db) {
|
|
556
|
+
const d = db || getDatabase();
|
|
557
|
+
const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
|
|
558
|
+
return result.changes > 0;
|
|
559
|
+
}
|
|
560
|
+
function ensureProject(name, path, db) {
|
|
561
|
+
const d = db || getDatabase();
|
|
562
|
+
const existing = getProjectByPath(path, d);
|
|
563
|
+
if (existing)
|
|
564
|
+
return existing;
|
|
565
|
+
return createProject({ name, path }, d);
|
|
566
|
+
}
|
|
567
|
+
// src/db/comments.ts
|
|
568
|
+
function addComment(input, db) {
|
|
569
|
+
const d = db || getDatabase();
|
|
570
|
+
if (!getTask(input.task_id, d)) {
|
|
571
|
+
throw new TaskNotFoundError(input.task_id);
|
|
572
|
+
}
|
|
573
|
+
const id = uuid();
|
|
574
|
+
const timestamp = now();
|
|
575
|
+
d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, created_at)
|
|
576
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
577
|
+
id,
|
|
578
|
+
input.task_id,
|
|
579
|
+
input.agent_id || null,
|
|
580
|
+
input.session_id || null,
|
|
581
|
+
input.content,
|
|
582
|
+
timestamp
|
|
583
|
+
]);
|
|
584
|
+
return getComment(id, d);
|
|
585
|
+
}
|
|
586
|
+
function getComment(id, db) {
|
|
587
|
+
const d = db || getDatabase();
|
|
588
|
+
return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
|
|
589
|
+
}
|
|
590
|
+
function listComments(taskId, db) {
|
|
591
|
+
const d = db || getDatabase();
|
|
592
|
+
return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
|
|
593
|
+
}
|
|
594
|
+
function deleteComment(id, db) {
|
|
595
|
+
const d = db || getDatabase();
|
|
596
|
+
const result = d.run("DELETE FROM task_comments WHERE id = ?", [id]);
|
|
597
|
+
return result.changes > 0;
|
|
598
|
+
}
|
|
599
|
+
// src/db/sessions.ts
|
|
600
|
+
function rowToSession(row) {
|
|
601
|
+
return {
|
|
602
|
+
...row,
|
|
603
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function createSession(input, db) {
|
|
607
|
+
const d = db || getDatabase();
|
|
608
|
+
const id = uuid();
|
|
609
|
+
const timestamp = now();
|
|
610
|
+
d.run(`INSERT INTO sessions (id, agent_id, project_id, working_dir, started_at, last_activity, metadata)
|
|
611
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
612
|
+
id,
|
|
613
|
+
input.agent_id || null,
|
|
614
|
+
input.project_id || null,
|
|
615
|
+
input.working_dir || null,
|
|
616
|
+
timestamp,
|
|
617
|
+
timestamp,
|
|
618
|
+
JSON.stringify(input.metadata || {})
|
|
619
|
+
]);
|
|
620
|
+
return getSession(id, d);
|
|
621
|
+
}
|
|
622
|
+
function getSession(id, db) {
|
|
623
|
+
const d = db || getDatabase();
|
|
624
|
+
const row = d.query("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
625
|
+
if (!row)
|
|
626
|
+
return null;
|
|
627
|
+
return rowToSession(row);
|
|
628
|
+
}
|
|
629
|
+
function listSessions(db) {
|
|
630
|
+
const d = db || getDatabase();
|
|
631
|
+
const rows = d.query("SELECT * FROM sessions ORDER BY last_activity DESC").all();
|
|
632
|
+
return rows.map(rowToSession);
|
|
633
|
+
}
|
|
634
|
+
function updateSessionActivity(id, db) {
|
|
635
|
+
const d = db || getDatabase();
|
|
636
|
+
d.run("UPDATE sessions SET last_activity = ? WHERE id = ?", [now(), id]);
|
|
637
|
+
}
|
|
638
|
+
function deleteSession(id, db) {
|
|
639
|
+
const d = db || getDatabase();
|
|
640
|
+
const result = d.run("DELETE FROM sessions WHERE id = ?", [id]);
|
|
641
|
+
return result.changes > 0;
|
|
642
|
+
}
|
|
643
|
+
// src/lib/search.ts
|
|
644
|
+
function rowToTask2(row) {
|
|
645
|
+
return {
|
|
646
|
+
...row,
|
|
647
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
648
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
649
|
+
status: row.status,
|
|
650
|
+
priority: row.priority
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function searchTasks(query, projectId, db) {
|
|
654
|
+
const d = db || getDatabase();
|
|
655
|
+
const pattern = `%${query}%`;
|
|
656
|
+
let sql = `SELECT * FROM tasks WHERE (title LIKE ? OR description LIKE ? OR tags LIKE ?)`;
|
|
657
|
+
const params = [pattern, pattern, pattern];
|
|
658
|
+
if (projectId) {
|
|
659
|
+
sql += " AND project_id = ?";
|
|
660
|
+
params.push(projectId);
|
|
661
|
+
}
|
|
662
|
+
sql += ` ORDER BY
|
|
663
|
+
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
664
|
+
created_at DESC`;
|
|
665
|
+
const rows = d.query(sql).all(...params);
|
|
666
|
+
return rows.map(rowToTask2);
|
|
667
|
+
}
|
|
668
|
+
export {
|
|
669
|
+
updateTask,
|
|
670
|
+
updateSessionActivity,
|
|
671
|
+
updateProject,
|
|
672
|
+
unlockTask,
|
|
673
|
+
startTask,
|
|
674
|
+
searchTasks,
|
|
675
|
+
resolvePartialId,
|
|
676
|
+
resetDatabase,
|
|
677
|
+
removeDependency,
|
|
678
|
+
lockTask,
|
|
679
|
+
listTasks,
|
|
680
|
+
listSessions,
|
|
681
|
+
listProjects,
|
|
682
|
+
listComments,
|
|
683
|
+
getTaskWithRelations,
|
|
684
|
+
getTaskDependents,
|
|
685
|
+
getTaskDependencies,
|
|
686
|
+
getTask,
|
|
687
|
+
getSession,
|
|
688
|
+
getProjectByPath,
|
|
689
|
+
getProject,
|
|
690
|
+
getDatabase,
|
|
691
|
+
getComment,
|
|
692
|
+
ensureProject,
|
|
693
|
+
deleteTask,
|
|
694
|
+
deleteSession,
|
|
695
|
+
deleteProject,
|
|
696
|
+
deleteComment,
|
|
697
|
+
createTask,
|
|
698
|
+
createSession,
|
|
699
|
+
createProject,
|
|
700
|
+
completeTask,
|
|
701
|
+
closeDatabase,
|
|
702
|
+
addDependency,
|
|
703
|
+
addComment,
|
|
704
|
+
VersionConflictError,
|
|
705
|
+
TaskNotFoundError,
|
|
706
|
+
TASK_STATUSES,
|
|
707
|
+
TASK_PRIORITIES,
|
|
708
|
+
ProjectNotFoundError,
|
|
709
|
+
LockError,
|
|
710
|
+
DependencyCycleError
|
|
711
|
+
};
|