@hasna/todos 0.9.18 → 0.9.20
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/cli/index.js +570 -25
- package/dist/index.js +124 -34
- package/dist/mcp/index.js +1950 -1538
- package/dist/server/index.js +117 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -285,29 +285,20 @@ function runMigrations(db) {
|
|
|
285
285
|
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
286
286
|
const currentLevel = result?.max_id ?? 0;
|
|
287
287
|
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
288
|
-
|
|
288
|
+
try {
|
|
289
|
+
db.exec(MIGRATIONS[i]);
|
|
290
|
+
} catch {}
|
|
289
291
|
}
|
|
290
292
|
} catch {
|
|
291
293
|
for (const migration of MIGRATIONS) {
|
|
292
|
-
|
|
294
|
+
try {
|
|
295
|
+
db.exec(migration);
|
|
296
|
+
} catch {}
|
|
293
297
|
}
|
|
294
298
|
}
|
|
295
|
-
|
|
299
|
+
ensureSchema(db);
|
|
296
300
|
}
|
|
297
|
-
function
|
|
298
|
-
try {
|
|
299
|
-
const hasAgents = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='agents'").get();
|
|
300
|
-
if (!hasAgents) {
|
|
301
|
-
db.exec(MIGRATIONS[4]);
|
|
302
|
-
}
|
|
303
|
-
} catch {}
|
|
304
|
-
try {
|
|
305
|
-
db.query("SELECT task_prefix FROM projects LIMIT 0").get();
|
|
306
|
-
} catch {
|
|
307
|
-
try {
|
|
308
|
-
db.exec(MIGRATIONS[5]);
|
|
309
|
-
} catch {}
|
|
310
|
-
}
|
|
301
|
+
function ensureSchema(db) {
|
|
311
302
|
const ensureColumn = (table, column, type) => {
|
|
312
303
|
try {
|
|
313
304
|
db.query(`SELECT ${column} FROM ${table} LIMIT 0`).get();
|
|
@@ -317,6 +308,76 @@ function ensureTableMigrations(db) {
|
|
|
317
308
|
} catch {}
|
|
318
309
|
}
|
|
319
310
|
};
|
|
311
|
+
const ensureTable = (name, sql) => {
|
|
312
|
+
try {
|
|
313
|
+
const exists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(name);
|
|
314
|
+
if (!exists)
|
|
315
|
+
db.exec(sql);
|
|
316
|
+
} catch {}
|
|
317
|
+
};
|
|
318
|
+
const ensureIndex = (sql) => {
|
|
319
|
+
try {
|
|
320
|
+
db.exec(sql);
|
|
321
|
+
} catch {}
|
|
322
|
+
};
|
|
323
|
+
ensureTable("agents", `
|
|
324
|
+
CREATE TABLE agents (
|
|
325
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
|
|
326
|
+
role TEXT DEFAULT 'agent', permissions TEXT DEFAULT '["*"]',
|
|
327
|
+
metadata TEXT DEFAULT '{}',
|
|
328
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
329
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
330
|
+
)`);
|
|
331
|
+
ensureTable("task_lists", `
|
|
332
|
+
CREATE TABLE task_lists (
|
|
333
|
+
id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
334
|
+
slug TEXT NOT NULL, name TEXT NOT NULL, description TEXT,
|
|
335
|
+
metadata TEXT DEFAULT '{}',
|
|
336
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
337
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
338
|
+
UNIQUE(project_id, slug)
|
|
339
|
+
)`);
|
|
340
|
+
ensureTable("plans", `
|
|
341
|
+
CREATE TABLE plans (
|
|
342
|
+
id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
343
|
+
task_list_id TEXT, agent_id TEXT,
|
|
344
|
+
name TEXT NOT NULL, description TEXT,
|
|
345
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
346
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
347
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
348
|
+
)`);
|
|
349
|
+
ensureTable("task_tags", `
|
|
350
|
+
CREATE TABLE task_tags (
|
|
351
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
352
|
+
tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
|
|
353
|
+
)`);
|
|
354
|
+
ensureTable("task_history", `
|
|
355
|
+
CREATE TABLE task_history (
|
|
356
|
+
id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
357
|
+
action TEXT NOT NULL, field TEXT, old_value TEXT, new_value TEXT, agent_id TEXT,
|
|
358
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
359
|
+
)`);
|
|
360
|
+
ensureTable("webhooks", `
|
|
361
|
+
CREATE TABLE webhooks (
|
|
362
|
+
id TEXT PRIMARY KEY, url TEXT NOT NULL, events TEXT NOT NULL DEFAULT '[]',
|
|
363
|
+
secret TEXT, active INTEGER NOT NULL DEFAULT 1,
|
|
364
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
365
|
+
)`);
|
|
366
|
+
ensureTable("task_templates", `
|
|
367
|
+
CREATE TABLE task_templates (
|
|
368
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL, title_pattern TEXT NOT NULL,
|
|
369
|
+
description TEXT, priority TEXT DEFAULT 'medium', tags TEXT DEFAULT '[]',
|
|
370
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
371
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
372
|
+
metadata TEXT DEFAULT '{}',
|
|
373
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
374
|
+
)`);
|
|
375
|
+
ensureColumn("projects", "task_list_id", "TEXT");
|
|
376
|
+
ensureColumn("projects", "task_prefix", "TEXT");
|
|
377
|
+
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
378
|
+
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
379
|
+
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
380
|
+
ensureColumn("tasks", "short_id", "TEXT");
|
|
320
381
|
ensureColumn("tasks", "due_at", "TEXT");
|
|
321
382
|
ensureColumn("tasks", "estimated_minutes", "INTEGER");
|
|
322
383
|
ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
|
|
@@ -326,6 +387,21 @@ function ensureTableMigrations(db) {
|
|
|
326
387
|
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
327
388
|
ensureColumn("plans", "task_list_id", "TEXT");
|
|
328
389
|
ensureColumn("plans", "agent_id", "TEXT");
|
|
390
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
|
|
391
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
|
|
392
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
|
|
393
|
+
ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
|
|
394
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)");
|
|
395
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id)");
|
|
396
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
|
|
397
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
|
|
398
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
|
|
399
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
|
|
400
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
|
|
401
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
|
|
402
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
|
|
403
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
|
|
404
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id)");
|
|
329
405
|
}
|
|
330
406
|
function backfillTaskTags(db) {
|
|
331
407
|
try {
|
|
@@ -760,6 +836,24 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
|
|
|
760
836
|
}
|
|
761
837
|
}
|
|
762
838
|
|
|
839
|
+
// src/db/audit.ts
|
|
840
|
+
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
841
|
+
const d = db || getDatabase();
|
|
842
|
+
const id = uuid();
|
|
843
|
+
const timestamp = now();
|
|
844
|
+
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
|
|
845
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
|
|
846
|
+
return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
|
|
847
|
+
}
|
|
848
|
+
function getTaskHistory(taskId, db) {
|
|
849
|
+
const d = db || getDatabase();
|
|
850
|
+
return d.query("SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
|
|
851
|
+
}
|
|
852
|
+
function getRecentActivity(limit = 50, db) {
|
|
853
|
+
const d = db || getDatabase();
|
|
854
|
+
return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
855
|
+
}
|
|
856
|
+
|
|
763
857
|
// src/db/tasks.ts
|
|
764
858
|
function rowToTask(row) {
|
|
765
859
|
return {
|
|
@@ -1010,6 +1104,17 @@ function updateTask(id, input, db) {
|
|
|
1010
1104
|
if (input.tags !== undefined) {
|
|
1011
1105
|
replaceTaskTags(id, input.tags, d);
|
|
1012
1106
|
}
|
|
1107
|
+
const agentId = task.assigned_to || task.agent_id || null;
|
|
1108
|
+
if (input.status !== undefined && input.status !== task.status)
|
|
1109
|
+
logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
|
|
1110
|
+
if (input.priority !== undefined && input.priority !== task.priority)
|
|
1111
|
+
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
1112
|
+
if (input.title !== undefined && input.title !== task.title)
|
|
1113
|
+
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
1114
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
1115
|
+
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
1116
|
+
if (input.approved_by !== undefined)
|
|
1117
|
+
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
1013
1118
|
return getTask(id, d);
|
|
1014
1119
|
}
|
|
1015
1120
|
function deleteTask(id, db) {
|
|
@@ -1031,6 +1136,7 @@ function startTask(id, agentId, db) {
|
|
|
1031
1136
|
throw new LockError(id, current.locked_by);
|
|
1032
1137
|
}
|
|
1033
1138
|
}
|
|
1139
|
+
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
1034
1140
|
return getTask(id, d);
|
|
1035
1141
|
}
|
|
1036
1142
|
function completeTask(id, agentId, db) {
|
|
@@ -1045,6 +1151,7 @@ function completeTask(id, agentId, db) {
|
|
|
1045
1151
|
const timestamp = now();
|
|
1046
1152
|
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
1047
1153
|
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
1154
|
+
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
1048
1155
|
return getTask(id, d);
|
|
1049
1156
|
}
|
|
1050
1157
|
function lockTask(id, agentId, db) {
|
|
@@ -1435,23 +1542,6 @@ function deleteSession(id, db) {
|
|
|
1435
1542
|
const result = d.run("DELETE FROM sessions WHERE id = ?", [id]);
|
|
1436
1543
|
return result.changes > 0;
|
|
1437
1544
|
}
|
|
1438
|
-
// src/db/audit.ts
|
|
1439
|
-
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
1440
|
-
const d = db || getDatabase();
|
|
1441
|
-
const id = uuid();
|
|
1442
|
-
const timestamp = now();
|
|
1443
|
-
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
|
|
1444
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
|
|
1445
|
-
return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
|
|
1446
|
-
}
|
|
1447
|
-
function getTaskHistory(taskId, db) {
|
|
1448
|
-
const d = db || getDatabase();
|
|
1449
|
-
return d.query("SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
|
|
1450
|
-
}
|
|
1451
|
-
function getRecentActivity(limit = 50, db) {
|
|
1452
|
-
const d = db || getDatabase();
|
|
1453
|
-
return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
1454
|
-
}
|
|
1455
1545
|
// src/db/webhooks.ts
|
|
1456
1546
|
function rowToWebhook(row) {
|
|
1457
1547
|
return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
|