@hasna/todos 0.9.31 → 0.9.33
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/package.json +1 -1
- package/dashboard/dist/assets/index-DVzieYOj.js +0 -346
- package/dashboard/dist/assets/index-DWpVlvWb.css +0 -1
- package/dashboard/dist/index.html +0 -13
- package/dashboard/dist/logo.jpg +0 -0
- package/dist/cli/components/App.d.ts +0 -2
- package/dist/cli/components/App.d.ts.map +0 -1
- package/dist/cli/components/Header.d.ts +0 -8
- package/dist/cli/components/Header.d.ts.map +0 -1
- package/dist/cli/components/ProjectList.d.ts +0 -8
- package/dist/cli/components/ProjectList.d.ts.map +0 -1
- package/dist/cli/components/SearchView.d.ts +0 -10
- package/dist/cli/components/SearchView.d.ts.map +0 -1
- package/dist/cli/components/TaskDetail.d.ts +0 -7
- package/dist/cli/components/TaskDetail.d.ts.map +0 -1
- package/dist/cli/components/TaskForm.d.ts +0 -15
- package/dist/cli/components/TaskForm.d.ts.map +0 -1
- package/dist/cli/components/TaskList.d.ts +0 -8
- package/dist/cli/components/TaskList.d.ts.map +0 -1
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -12559
- package/dist/db/agents.d.ts +0 -19
- package/dist/db/agents.d.ts.map +0 -1
- package/dist/db/comments.d.ts +0 -7
- package/dist/db/comments.d.ts.map +0 -1
- package/dist/db/database.d.ts +0 -12
- package/dist/db/database.d.ts.map +0 -1
- package/dist/db/plans.d.ts +0 -8
- package/dist/db/plans.d.ts.map +0 -1
- package/dist/db/projects.d.ts +0 -12
- package/dist/db/projects.d.ts.map +0 -1
- package/dist/db/sessions.d.ts +0 -8
- package/dist/db/sessions.d.ts.map +0 -1
- package/dist/db/task-lists.d.ts +0 -10
- package/dist/db/task-lists.d.ts.map +0 -1
- package/dist/db/tasks.d.ts +0 -17
- package/dist/db/tasks.d.ts.map +0 -1
- package/dist/index.d.ts +0 -17
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2355
- package/dist/lib/agent-tasks.d.ts +0 -11
- package/dist/lib/agent-tasks.d.ts.map +0 -1
- package/dist/lib/claude-tasks.d.ts +0 -20
- package/dist/lib/claude-tasks.d.ts.map +0 -1
- package/dist/lib/completion-guard.d.ts +0 -17
- package/dist/lib/completion-guard.d.ts.map +0 -1
- package/dist/lib/config.d.ts +0 -34
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/search.d.ts +0 -4
- package/dist/lib/search.d.ts.map +0 -1
- package/dist/lib/sync-types.d.ts +0 -16
- package/dist/lib/sync-types.d.ts.map +0 -1
- package/dist/lib/sync-utils.d.ts +0 -12
- package/dist/lib/sync-utils.d.ts.map +0 -1
- package/dist/lib/sync.d.ts +0 -9
- package/dist/lib/sync.d.ts.map +0 -1
- package/dist/mcp/index.d.ts +0 -3
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js +0 -7214
- package/dist/server/index.d.ts +0 -9
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -2077
- package/dist/server/serve.d.ts +0 -9
- package/dist/server/serve.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -359
- package/dist/types/index.d.ts.map +0 -1
package/dist/index.js
DELETED
|
@@ -1,2355 +0,0 @@
|
|
|
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 isInMemoryDb(path) {
|
|
8
|
-
return path === ":memory:" || path.startsWith("file::memory:");
|
|
9
|
-
}
|
|
10
|
-
function findNearestTodosDb(startDir) {
|
|
11
|
-
let dir = resolve(startDir);
|
|
12
|
-
while (true) {
|
|
13
|
-
const candidate = join(dir, ".todos", "todos.db");
|
|
14
|
-
if (existsSync(candidate))
|
|
15
|
-
return candidate;
|
|
16
|
-
const parent = dirname(dir);
|
|
17
|
-
if (parent === dir)
|
|
18
|
-
break;
|
|
19
|
-
dir = parent;
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
function findGitRoot(startDir) {
|
|
24
|
-
let dir = resolve(startDir);
|
|
25
|
-
while (true) {
|
|
26
|
-
if (existsSync(join(dir, ".git")))
|
|
27
|
-
return dir;
|
|
28
|
-
const parent = dirname(dir);
|
|
29
|
-
if (parent === dir)
|
|
30
|
-
break;
|
|
31
|
-
dir = parent;
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
function getDbPath() {
|
|
36
|
-
if (process.env["TODOS_DB_PATH"]) {
|
|
37
|
-
return process.env["TODOS_DB_PATH"];
|
|
38
|
-
}
|
|
39
|
-
const cwd = process.cwd();
|
|
40
|
-
const nearest = findNearestTodosDb(cwd);
|
|
41
|
-
if (nearest)
|
|
42
|
-
return nearest;
|
|
43
|
-
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
44
|
-
const gitRoot = findGitRoot(cwd);
|
|
45
|
-
if (gitRoot) {
|
|
46
|
-
return join(gitRoot, ".todos", "todos.db");
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
50
|
-
return join(home, ".todos", "todos.db");
|
|
51
|
-
}
|
|
52
|
-
function ensureDir(filePath) {
|
|
53
|
-
if (isInMemoryDb(filePath))
|
|
54
|
-
return;
|
|
55
|
-
const dir = dirname(resolve(filePath));
|
|
56
|
-
if (!existsSync(dir)) {
|
|
57
|
-
mkdirSync(dir, { recursive: true });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
var MIGRATIONS = [
|
|
61
|
-
`
|
|
62
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
63
|
-
id TEXT PRIMARY KEY,
|
|
64
|
-
name TEXT NOT NULL,
|
|
65
|
-
path TEXT UNIQUE NOT NULL,
|
|
66
|
-
description TEXT,
|
|
67
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
68
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
72
|
-
id TEXT PRIMARY KEY,
|
|
73
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
74
|
-
parent_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
|
|
75
|
-
title TEXT NOT NULL,
|
|
76
|
-
description TEXT,
|
|
77
|
-
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'failed', 'cancelled')),
|
|
78
|
-
priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high', 'critical')),
|
|
79
|
-
agent_id TEXT,
|
|
80
|
-
assigned_to TEXT,
|
|
81
|
-
session_id TEXT,
|
|
82
|
-
working_dir TEXT,
|
|
83
|
-
tags TEXT DEFAULT '[]',
|
|
84
|
-
metadata TEXT DEFAULT '{}',
|
|
85
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
86
|
-
locked_by TEXT,
|
|
87
|
-
locked_at TEXT,
|
|
88
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
89
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
90
|
-
completed_at TEXT
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
94
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
95
|
-
depends_on TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
96
|
-
PRIMARY KEY (task_id, depends_on),
|
|
97
|
-
CHECK (task_id != depends_on)
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
CREATE TABLE IF NOT EXISTS task_comments (
|
|
101
|
-
id TEXT PRIMARY KEY,
|
|
102
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
103
|
-
agent_id TEXT,
|
|
104
|
-
session_id TEXT,
|
|
105
|
-
content TEXT NOT NULL,
|
|
106
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
110
|
-
id TEXT PRIMARY KEY,
|
|
111
|
-
agent_id TEXT,
|
|
112
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
113
|
-
working_dir TEXT,
|
|
114
|
-
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
115
|
-
last_activity TEXT NOT NULL DEFAULT (datetime('now')),
|
|
116
|
-
metadata TEXT DEFAULT '{}'
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
|
120
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id);
|
|
121
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
122
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
|
|
123
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to);
|
|
124
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_agent ON tasks(agent_id);
|
|
125
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
126
|
-
CREATE INDEX IF NOT EXISTS idx_comments_task ON task_comments(task_id);
|
|
127
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
|
|
128
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
129
|
-
|
|
130
|
-
CREATE TABLE IF NOT EXISTS _migrations (
|
|
131
|
-
id INTEGER PRIMARY KEY,
|
|
132
|
-
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (1);
|
|
136
|
-
`,
|
|
137
|
-
`
|
|
138
|
-
ALTER TABLE projects ADD COLUMN task_list_id TEXT;
|
|
139
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (2);
|
|
140
|
-
`,
|
|
141
|
-
`
|
|
142
|
-
CREATE TABLE IF NOT EXISTS task_tags (
|
|
143
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
144
|
-
tag TEXT NOT NULL,
|
|
145
|
-
PRIMARY KEY (task_id, tag)
|
|
146
|
-
);
|
|
147
|
-
CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag);
|
|
148
|
-
CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id);
|
|
149
|
-
|
|
150
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (3);
|
|
151
|
-
`,
|
|
152
|
-
`
|
|
153
|
-
CREATE TABLE IF NOT EXISTS plans (
|
|
154
|
-
id TEXT PRIMARY KEY,
|
|
155
|
-
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
156
|
-
name TEXT NOT NULL,
|
|
157
|
-
description TEXT,
|
|
158
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
159
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
160
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
161
|
-
);
|
|
162
|
-
CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id);
|
|
163
|
-
CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status);
|
|
164
|
-
ALTER TABLE tasks ADD COLUMN plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL;
|
|
165
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id);
|
|
166
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (4);
|
|
167
|
-
`,
|
|
168
|
-
`
|
|
169
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
170
|
-
id TEXT PRIMARY KEY,
|
|
171
|
-
name TEXT NOT NULL UNIQUE,
|
|
172
|
-
description TEXT,
|
|
173
|
-
metadata TEXT DEFAULT '{}',
|
|
174
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
175
|
-
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
176
|
-
);
|
|
177
|
-
CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name);
|
|
178
|
-
|
|
179
|
-
CREATE TABLE IF NOT EXISTS task_lists (
|
|
180
|
-
id TEXT PRIMARY KEY,
|
|
181
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
182
|
-
slug TEXT NOT NULL,
|
|
183
|
-
name TEXT NOT NULL,
|
|
184
|
-
description TEXT,
|
|
185
|
-
metadata TEXT DEFAULT '{}',
|
|
186
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
187
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
188
|
-
UNIQUE(project_id, slug)
|
|
189
|
-
);
|
|
190
|
-
CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id);
|
|
191
|
-
CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug);
|
|
192
|
-
|
|
193
|
-
ALTER TABLE tasks ADD COLUMN task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL;
|
|
194
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id);
|
|
195
|
-
|
|
196
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (5);
|
|
197
|
-
`,
|
|
198
|
-
`
|
|
199
|
-
ALTER TABLE projects ADD COLUMN task_prefix TEXT;
|
|
200
|
-
ALTER TABLE projects ADD COLUMN task_counter INTEGER NOT NULL DEFAULT 0;
|
|
201
|
-
|
|
202
|
-
ALTER TABLE tasks ADD COLUMN short_id TEXT;
|
|
203
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL;
|
|
204
|
-
|
|
205
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (6);
|
|
206
|
-
`,
|
|
207
|
-
`
|
|
208
|
-
ALTER TABLE tasks ADD COLUMN due_at TEXT;
|
|
209
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at);
|
|
210
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (7);
|
|
211
|
-
`,
|
|
212
|
-
`
|
|
213
|
-
ALTER TABLE agents ADD COLUMN role TEXT DEFAULT 'agent';
|
|
214
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (8);
|
|
215
|
-
`,
|
|
216
|
-
`
|
|
217
|
-
ALTER TABLE plans ADD COLUMN task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL;
|
|
218
|
-
ALTER TABLE plans ADD COLUMN agent_id TEXT;
|
|
219
|
-
CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id);
|
|
220
|
-
CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id);
|
|
221
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (9);
|
|
222
|
-
`,
|
|
223
|
-
`
|
|
224
|
-
CREATE TABLE IF NOT EXISTS task_history (
|
|
225
|
-
id TEXT PRIMARY KEY,
|
|
226
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
227
|
-
action TEXT NOT NULL,
|
|
228
|
-
field TEXT,
|
|
229
|
-
old_value TEXT,
|
|
230
|
-
new_value TEXT,
|
|
231
|
-
agent_id TEXT,
|
|
232
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
233
|
-
);
|
|
234
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id);
|
|
235
|
-
CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id);
|
|
236
|
-
|
|
237
|
-
CREATE TABLE IF NOT EXISTS webhooks (
|
|
238
|
-
id TEXT PRIMARY KEY,
|
|
239
|
-
url TEXT NOT NULL,
|
|
240
|
-
events TEXT NOT NULL DEFAULT '[]',
|
|
241
|
-
secret TEXT,
|
|
242
|
-
active INTEGER NOT NULL DEFAULT 1,
|
|
243
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
CREATE TABLE IF NOT EXISTS task_templates (
|
|
247
|
-
id TEXT PRIMARY KEY,
|
|
248
|
-
name TEXT NOT NULL,
|
|
249
|
-
title_pattern TEXT NOT NULL,
|
|
250
|
-
description TEXT,
|
|
251
|
-
priority TEXT DEFAULT 'medium',
|
|
252
|
-
tags TEXT DEFAULT '[]',
|
|
253
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
254
|
-
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
255
|
-
metadata TEXT DEFAULT '{}',
|
|
256
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
ALTER TABLE tasks ADD COLUMN estimated_minutes INTEGER;
|
|
260
|
-
ALTER TABLE tasks ADD COLUMN requires_approval INTEGER NOT NULL DEFAULT 0;
|
|
261
|
-
ALTER TABLE tasks ADD COLUMN approved_by TEXT;
|
|
262
|
-
ALTER TABLE tasks ADD COLUMN approved_at TEXT;
|
|
263
|
-
|
|
264
|
-
ALTER TABLE agents ADD COLUMN permissions TEXT DEFAULT '["*"]';
|
|
265
|
-
|
|
266
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (10);
|
|
267
|
-
`,
|
|
268
|
-
`
|
|
269
|
-
ALTER TABLE agents ADD COLUMN reports_to TEXT;
|
|
270
|
-
ALTER TABLE agents ADD COLUMN title TEXT;
|
|
271
|
-
ALTER TABLE agents ADD COLUMN level TEXT;
|
|
272
|
-
INSERT OR IGNORE INTO _migrations (id) VALUES (11);
|
|
273
|
-
`
|
|
274
|
-
];
|
|
275
|
-
var _db = null;
|
|
276
|
-
function getDatabase(dbPath) {
|
|
277
|
-
if (_db)
|
|
278
|
-
return _db;
|
|
279
|
-
const path = dbPath || getDbPath();
|
|
280
|
-
ensureDir(path);
|
|
281
|
-
_db = new Database(path, { create: true });
|
|
282
|
-
_db.run("PRAGMA journal_mode = WAL");
|
|
283
|
-
_db.run("PRAGMA busy_timeout = 5000");
|
|
284
|
-
_db.run("PRAGMA foreign_keys = ON");
|
|
285
|
-
runMigrations(_db);
|
|
286
|
-
backfillTaskTags(_db);
|
|
287
|
-
return _db;
|
|
288
|
-
}
|
|
289
|
-
function runMigrations(db) {
|
|
290
|
-
try {
|
|
291
|
-
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
292
|
-
const currentLevel = result?.max_id ?? 0;
|
|
293
|
-
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
294
|
-
try {
|
|
295
|
-
db.exec(MIGRATIONS[i]);
|
|
296
|
-
} catch {}
|
|
297
|
-
}
|
|
298
|
-
} catch {
|
|
299
|
-
for (const migration of MIGRATIONS) {
|
|
300
|
-
try {
|
|
301
|
-
db.exec(migration);
|
|
302
|
-
} catch {}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
ensureSchema(db);
|
|
306
|
-
}
|
|
307
|
-
function ensureSchema(db) {
|
|
308
|
-
const ensureColumn = (table, column, type) => {
|
|
309
|
-
try {
|
|
310
|
-
db.query(`SELECT ${column} FROM ${table} LIMIT 0`).get();
|
|
311
|
-
} catch {
|
|
312
|
-
try {
|
|
313
|
-
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
314
|
-
} catch {}
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
const ensureTable = (name, sql) => {
|
|
318
|
-
try {
|
|
319
|
-
const exists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(name);
|
|
320
|
-
if (!exists)
|
|
321
|
-
db.exec(sql);
|
|
322
|
-
} catch {}
|
|
323
|
-
};
|
|
324
|
-
const ensureIndex = (sql) => {
|
|
325
|
-
try {
|
|
326
|
-
db.exec(sql);
|
|
327
|
-
} catch {}
|
|
328
|
-
};
|
|
329
|
-
ensureTable("agents", `
|
|
330
|
-
CREATE TABLE agents (
|
|
331
|
-
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
|
|
332
|
-
role TEXT DEFAULT 'agent', permissions TEXT DEFAULT '["*"]',
|
|
333
|
-
metadata TEXT DEFAULT '{}',
|
|
334
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
335
|
-
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
336
|
-
)`);
|
|
337
|
-
ensureTable("task_lists", `
|
|
338
|
-
CREATE TABLE task_lists (
|
|
339
|
-
id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
340
|
-
slug TEXT NOT NULL, name TEXT NOT NULL, description TEXT,
|
|
341
|
-
metadata TEXT DEFAULT '{}',
|
|
342
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
343
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
344
|
-
UNIQUE(project_id, slug)
|
|
345
|
-
)`);
|
|
346
|
-
ensureTable("plans", `
|
|
347
|
-
CREATE TABLE plans (
|
|
348
|
-
id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
349
|
-
task_list_id TEXT, agent_id TEXT,
|
|
350
|
-
name TEXT NOT NULL, description TEXT,
|
|
351
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
352
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
353
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
354
|
-
)`);
|
|
355
|
-
ensureTable("task_tags", `
|
|
356
|
-
CREATE TABLE task_tags (
|
|
357
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
358
|
-
tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
|
|
359
|
-
)`);
|
|
360
|
-
ensureTable("task_history", `
|
|
361
|
-
CREATE TABLE task_history (
|
|
362
|
-
id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
363
|
-
action TEXT NOT NULL, field TEXT, old_value TEXT, new_value TEXT, agent_id TEXT,
|
|
364
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
365
|
-
)`);
|
|
366
|
-
ensureTable("webhooks", `
|
|
367
|
-
CREATE TABLE webhooks (
|
|
368
|
-
id TEXT PRIMARY KEY, url TEXT NOT NULL, events TEXT NOT NULL DEFAULT '[]',
|
|
369
|
-
secret TEXT, active INTEGER NOT NULL DEFAULT 1,
|
|
370
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
371
|
-
)`);
|
|
372
|
-
ensureTable("task_templates", `
|
|
373
|
-
CREATE TABLE task_templates (
|
|
374
|
-
id TEXT PRIMARY KEY, name TEXT NOT NULL, title_pattern TEXT NOT NULL,
|
|
375
|
-
description TEXT, priority TEXT DEFAULT 'medium', tags TEXT DEFAULT '[]',
|
|
376
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
377
|
-
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
378
|
-
metadata TEXT DEFAULT '{}',
|
|
379
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
380
|
-
)`);
|
|
381
|
-
ensureColumn("projects", "task_list_id", "TEXT");
|
|
382
|
-
ensureColumn("projects", "task_prefix", "TEXT");
|
|
383
|
-
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
384
|
-
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
385
|
-
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
386
|
-
ensureColumn("tasks", "short_id", "TEXT");
|
|
387
|
-
ensureColumn("tasks", "due_at", "TEXT");
|
|
388
|
-
ensureColumn("tasks", "estimated_minutes", "INTEGER");
|
|
389
|
-
ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
|
|
390
|
-
ensureColumn("tasks", "approved_by", "TEXT");
|
|
391
|
-
ensureColumn("tasks", "approved_at", "TEXT");
|
|
392
|
-
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
393
|
-
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
394
|
-
ensureColumn("agents", "reports_to", "TEXT");
|
|
395
|
-
ensureColumn("agents", "title", "TEXT");
|
|
396
|
-
ensureColumn("agents", "level", "TEXT");
|
|
397
|
-
ensureColumn("plans", "task_list_id", "TEXT");
|
|
398
|
-
ensureColumn("plans", "agent_id", "TEXT");
|
|
399
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
|
|
400
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
|
|
401
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
|
|
402
|
-
ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
|
|
403
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)");
|
|
404
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id)");
|
|
405
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
|
|
406
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
|
|
407
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
|
|
408
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
|
|
409
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
|
|
410
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
|
|
411
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
|
|
412
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
|
|
413
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id)");
|
|
414
|
-
}
|
|
415
|
-
function backfillTaskTags(db) {
|
|
416
|
-
try {
|
|
417
|
-
const count = db.query("SELECT COUNT(*) as count FROM task_tags").get();
|
|
418
|
-
if (count && count.count > 0)
|
|
419
|
-
return;
|
|
420
|
-
} catch {
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
try {
|
|
424
|
-
const rows = db.query("SELECT id, tags FROM tasks WHERE tags IS NOT NULL AND tags != '[]'").all();
|
|
425
|
-
if (rows.length === 0)
|
|
426
|
-
return;
|
|
427
|
-
const insert = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
428
|
-
for (const row of rows) {
|
|
429
|
-
if (!row.tags)
|
|
430
|
-
continue;
|
|
431
|
-
let tags = [];
|
|
432
|
-
try {
|
|
433
|
-
tags = JSON.parse(row.tags);
|
|
434
|
-
} catch {
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
for (const tag of tags) {
|
|
438
|
-
if (tag)
|
|
439
|
-
insert.run(row.id, tag);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
} catch {}
|
|
443
|
-
}
|
|
444
|
-
function closeDatabase() {
|
|
445
|
-
if (_db) {
|
|
446
|
-
_db.close();
|
|
447
|
-
_db = null;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
function resetDatabase() {
|
|
451
|
-
_db = null;
|
|
452
|
-
}
|
|
453
|
-
function now() {
|
|
454
|
-
return new Date().toISOString();
|
|
455
|
-
}
|
|
456
|
-
function uuid() {
|
|
457
|
-
return crypto.randomUUID();
|
|
458
|
-
}
|
|
459
|
-
function isLockExpired(lockedAt) {
|
|
460
|
-
if (!lockedAt)
|
|
461
|
-
return true;
|
|
462
|
-
const lockTime = new Date(lockedAt).getTime();
|
|
463
|
-
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
464
|
-
return Date.now() - lockTime > expiryMs;
|
|
465
|
-
}
|
|
466
|
-
function lockExpiryCutoff(nowMs = Date.now()) {
|
|
467
|
-
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
468
|
-
return new Date(nowMs - expiryMs).toISOString();
|
|
469
|
-
}
|
|
470
|
-
function clearExpiredLocks(db) {
|
|
471
|
-
const cutoff = lockExpiryCutoff();
|
|
472
|
-
db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
|
|
473
|
-
}
|
|
474
|
-
function resolvePartialId(db, table, partialId) {
|
|
475
|
-
if (partialId.length >= 36) {
|
|
476
|
-
const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
|
|
477
|
-
return row?.id ?? null;
|
|
478
|
-
}
|
|
479
|
-
const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
|
|
480
|
-
if (rows.length === 1) {
|
|
481
|
-
return rows[0].id;
|
|
482
|
-
}
|
|
483
|
-
if (rows.length > 1) {
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
// src/types/index.ts
|
|
489
|
-
var TASK_STATUSES = [
|
|
490
|
-
"pending",
|
|
491
|
-
"in_progress",
|
|
492
|
-
"completed",
|
|
493
|
-
"failed",
|
|
494
|
-
"cancelled"
|
|
495
|
-
];
|
|
496
|
-
var TASK_PRIORITIES = [
|
|
497
|
-
"low",
|
|
498
|
-
"medium",
|
|
499
|
-
"high",
|
|
500
|
-
"critical"
|
|
501
|
-
];
|
|
502
|
-
var PLAN_STATUSES = ["active", "completed", "archived"];
|
|
503
|
-
|
|
504
|
-
class VersionConflictError extends Error {
|
|
505
|
-
taskId;
|
|
506
|
-
expectedVersion;
|
|
507
|
-
actualVersion;
|
|
508
|
-
constructor(taskId, expectedVersion, actualVersion) {
|
|
509
|
-
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
510
|
-
this.taskId = taskId;
|
|
511
|
-
this.expectedVersion = expectedVersion;
|
|
512
|
-
this.actualVersion = actualVersion;
|
|
513
|
-
this.name = "VersionConflictError";
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
class TaskNotFoundError extends Error {
|
|
518
|
-
taskId;
|
|
519
|
-
constructor(taskId) {
|
|
520
|
-
super(`Task not found: ${taskId}`);
|
|
521
|
-
this.taskId = taskId;
|
|
522
|
-
this.name = "TaskNotFoundError";
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
class ProjectNotFoundError extends Error {
|
|
527
|
-
projectId;
|
|
528
|
-
constructor(projectId) {
|
|
529
|
-
super(`Project not found: ${projectId}`);
|
|
530
|
-
this.projectId = projectId;
|
|
531
|
-
this.name = "ProjectNotFoundError";
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
class PlanNotFoundError extends Error {
|
|
536
|
-
planId;
|
|
537
|
-
constructor(planId) {
|
|
538
|
-
super(`Plan not found: ${planId}`);
|
|
539
|
-
this.planId = planId;
|
|
540
|
-
this.name = "PlanNotFoundError";
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
class LockError extends Error {
|
|
545
|
-
taskId;
|
|
546
|
-
lockedBy;
|
|
547
|
-
constructor(taskId, lockedBy) {
|
|
548
|
-
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
549
|
-
this.taskId = taskId;
|
|
550
|
-
this.lockedBy = lockedBy;
|
|
551
|
-
this.name = "LockError";
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
class AgentNotFoundError extends Error {
|
|
556
|
-
agentId;
|
|
557
|
-
constructor(agentId) {
|
|
558
|
-
super(`Agent not found: ${agentId}`);
|
|
559
|
-
this.agentId = agentId;
|
|
560
|
-
this.name = "AgentNotFoundError";
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
class TaskListNotFoundError extends Error {
|
|
565
|
-
taskListId;
|
|
566
|
-
constructor(taskListId) {
|
|
567
|
-
super(`Task list not found: ${taskListId}`);
|
|
568
|
-
this.taskListId = taskListId;
|
|
569
|
-
this.name = "TaskListNotFoundError";
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
class DependencyCycleError extends Error {
|
|
574
|
-
taskId;
|
|
575
|
-
dependsOn;
|
|
576
|
-
constructor(taskId, dependsOn) {
|
|
577
|
-
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
578
|
-
this.taskId = taskId;
|
|
579
|
-
this.dependsOn = dependsOn;
|
|
580
|
-
this.name = "DependencyCycleError";
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
class CompletionGuardError extends Error {
|
|
585
|
-
reason;
|
|
586
|
-
retryAfterSeconds;
|
|
587
|
-
constructor(reason, retryAfterSeconds) {
|
|
588
|
-
super(reason);
|
|
589
|
-
this.reason = reason;
|
|
590
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
591
|
-
this.name = "CompletionGuardError";
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// src/db/projects.ts
|
|
596
|
-
function slugify(name) {
|
|
597
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
598
|
-
}
|
|
599
|
-
function generatePrefix(name, db) {
|
|
600
|
-
const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
|
|
601
|
-
let prefix;
|
|
602
|
-
if (words.length >= 3) {
|
|
603
|
-
prefix = words.slice(0, 3).map((w) => w[0].toUpperCase()).join("");
|
|
604
|
-
} else if (words.length === 2) {
|
|
605
|
-
prefix = (words[0].slice(0, 2) + words[1][0]).toUpperCase();
|
|
606
|
-
} else {
|
|
607
|
-
prefix = words[0].slice(0, 3).toUpperCase();
|
|
608
|
-
}
|
|
609
|
-
let candidate = prefix;
|
|
610
|
-
let suffix = 1;
|
|
611
|
-
while (true) {
|
|
612
|
-
const existing = db.query("SELECT id FROM projects WHERE task_prefix = ?").get(candidate);
|
|
613
|
-
if (!existing)
|
|
614
|
-
return candidate;
|
|
615
|
-
suffix++;
|
|
616
|
-
candidate = `${prefix}${suffix}`;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
function createProject(input, db) {
|
|
620
|
-
const d = db || getDatabase();
|
|
621
|
-
const id = uuid();
|
|
622
|
-
const timestamp = now();
|
|
623
|
-
const taskListId = input.task_list_id ?? `todos-${slugify(input.name)}`;
|
|
624
|
-
const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
|
|
625
|
-
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at)
|
|
626
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp]);
|
|
627
|
-
return getProject(id, d);
|
|
628
|
-
}
|
|
629
|
-
function getProject(id, db) {
|
|
630
|
-
const d = db || getDatabase();
|
|
631
|
-
const row = d.query("SELECT * FROM projects WHERE id = ?").get(id);
|
|
632
|
-
return row;
|
|
633
|
-
}
|
|
634
|
-
function getProjectByPath(path, db) {
|
|
635
|
-
const d = db || getDatabase();
|
|
636
|
-
const row = d.query("SELECT * FROM projects WHERE path = ?").get(path);
|
|
637
|
-
return row;
|
|
638
|
-
}
|
|
639
|
-
function listProjects(db) {
|
|
640
|
-
const d = db || getDatabase();
|
|
641
|
-
return d.query("SELECT * FROM projects ORDER BY name").all();
|
|
642
|
-
}
|
|
643
|
-
function updateProject(id, input, db) {
|
|
644
|
-
const d = db || getDatabase();
|
|
645
|
-
const project = getProject(id, d);
|
|
646
|
-
if (!project)
|
|
647
|
-
throw new ProjectNotFoundError(id);
|
|
648
|
-
const sets = ["updated_at = ?"];
|
|
649
|
-
const params = [now()];
|
|
650
|
-
if (input.name !== undefined) {
|
|
651
|
-
sets.push("name = ?");
|
|
652
|
-
params.push(input.name);
|
|
653
|
-
}
|
|
654
|
-
if (input.description !== undefined) {
|
|
655
|
-
sets.push("description = ?");
|
|
656
|
-
params.push(input.description);
|
|
657
|
-
}
|
|
658
|
-
if (input.task_list_id !== undefined) {
|
|
659
|
-
sets.push("task_list_id = ?");
|
|
660
|
-
params.push(input.task_list_id);
|
|
661
|
-
}
|
|
662
|
-
params.push(id);
|
|
663
|
-
d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
664
|
-
return getProject(id, d);
|
|
665
|
-
}
|
|
666
|
-
function deleteProject(id, db) {
|
|
667
|
-
const d = db || getDatabase();
|
|
668
|
-
const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
|
|
669
|
-
return result.changes > 0;
|
|
670
|
-
}
|
|
671
|
-
function nextTaskShortId(projectId, db) {
|
|
672
|
-
const d = db || getDatabase();
|
|
673
|
-
const project = getProject(projectId, d);
|
|
674
|
-
if (!project || !project.task_prefix)
|
|
675
|
-
return null;
|
|
676
|
-
d.run("UPDATE projects SET task_counter = task_counter + 1, updated_at = ? WHERE id = ?", [now(), projectId]);
|
|
677
|
-
const updated = getProject(projectId, d);
|
|
678
|
-
const padded = String(updated.task_counter).padStart(5, "0");
|
|
679
|
-
return `${updated.task_prefix}-${padded}`;
|
|
680
|
-
}
|
|
681
|
-
function ensureProject(name, path, db) {
|
|
682
|
-
const d = db || getDatabase();
|
|
683
|
-
const existing = getProjectByPath(path, d);
|
|
684
|
-
if (existing) {
|
|
685
|
-
if (!existing.task_prefix) {
|
|
686
|
-
const prefix = generatePrefix(existing.name, d);
|
|
687
|
-
d.run("UPDATE projects SET task_prefix = ?, updated_at = ? WHERE id = ?", [prefix, now(), existing.id]);
|
|
688
|
-
return getProject(existing.id, d);
|
|
689
|
-
}
|
|
690
|
-
return existing;
|
|
691
|
-
}
|
|
692
|
-
return createProject({ name, path }, d);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// src/lib/config.ts
|
|
696
|
-
import { existsSync as existsSync3 } from "fs";
|
|
697
|
-
import { join as join3 } from "path";
|
|
698
|
-
|
|
699
|
-
// src/lib/sync-utils.ts
|
|
700
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
701
|
-
import { join as join2 } from "path";
|
|
702
|
-
var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
703
|
-
function ensureDir2(dir) {
|
|
704
|
-
if (!existsSync2(dir))
|
|
705
|
-
mkdirSync2(dir, { recursive: true });
|
|
706
|
-
}
|
|
707
|
-
function listJsonFiles(dir) {
|
|
708
|
-
if (!existsSync2(dir))
|
|
709
|
-
return [];
|
|
710
|
-
return readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
711
|
-
}
|
|
712
|
-
function readJsonFile(path) {
|
|
713
|
-
try {
|
|
714
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
715
|
-
} catch {
|
|
716
|
-
return null;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
function writeJsonFile(path, data) {
|
|
720
|
-
writeFileSync(path, JSON.stringify(data, null, 2) + `
|
|
721
|
-
`);
|
|
722
|
-
}
|
|
723
|
-
function readHighWaterMark(dir) {
|
|
724
|
-
const path = join2(dir, ".highwatermark");
|
|
725
|
-
if (!existsSync2(path))
|
|
726
|
-
return 1;
|
|
727
|
-
const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
|
|
728
|
-
return isNaN(val) ? 1 : val;
|
|
729
|
-
}
|
|
730
|
-
function writeHighWaterMark(dir, value) {
|
|
731
|
-
writeFileSync(join2(dir, ".highwatermark"), String(value));
|
|
732
|
-
}
|
|
733
|
-
function getFileMtimeMs(path) {
|
|
734
|
-
try {
|
|
735
|
-
return statSync(path).mtimeMs;
|
|
736
|
-
} catch {
|
|
737
|
-
return null;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
function parseTimestamp(value) {
|
|
741
|
-
if (typeof value !== "string")
|
|
742
|
-
return null;
|
|
743
|
-
const parsed = Date.parse(value);
|
|
744
|
-
return Number.isNaN(parsed) ? null : parsed;
|
|
745
|
-
}
|
|
746
|
-
function appendSyncConflict(metadata, conflict, limit = 5) {
|
|
747
|
-
const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
|
|
748
|
-
const next = [conflict, ...current].slice(0, limit);
|
|
749
|
-
return { ...metadata, sync_conflicts: next };
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// src/lib/config.ts
|
|
753
|
-
var CONFIG_PATH = join3(HOME, ".todos", "config.json");
|
|
754
|
-
var cached = null;
|
|
755
|
-
function normalizeAgent(agent) {
|
|
756
|
-
return agent.trim().toLowerCase();
|
|
757
|
-
}
|
|
758
|
-
function loadConfig() {
|
|
759
|
-
if (cached)
|
|
760
|
-
return cached;
|
|
761
|
-
if (!existsSync3(CONFIG_PATH)) {
|
|
762
|
-
cached = {};
|
|
763
|
-
return cached;
|
|
764
|
-
}
|
|
765
|
-
const config = readJsonFile(CONFIG_PATH) || {};
|
|
766
|
-
if (typeof config.sync_agents === "string") {
|
|
767
|
-
config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
|
|
768
|
-
}
|
|
769
|
-
cached = config;
|
|
770
|
-
return cached;
|
|
771
|
-
}
|
|
772
|
-
function getSyncAgentsFromConfig() {
|
|
773
|
-
const config = loadConfig();
|
|
774
|
-
const agents = config.sync_agents;
|
|
775
|
-
if (Array.isArray(agents) && agents.length > 0)
|
|
776
|
-
return agents.map(normalizeAgent);
|
|
777
|
-
return null;
|
|
778
|
-
}
|
|
779
|
-
function getAgentTasksDir(agent) {
|
|
780
|
-
const config = loadConfig();
|
|
781
|
-
const key = normalizeAgent(agent);
|
|
782
|
-
return config.agents?.[key]?.tasks_dir || config.agent_tasks_dir || null;
|
|
783
|
-
}
|
|
784
|
-
function getTaskPrefixConfig() {
|
|
785
|
-
const config = loadConfig();
|
|
786
|
-
return config.task_prefix || null;
|
|
787
|
-
}
|
|
788
|
-
var GUARD_DEFAULTS = {
|
|
789
|
-
enabled: false,
|
|
790
|
-
min_work_seconds: 30,
|
|
791
|
-
max_completions_per_window: 5,
|
|
792
|
-
window_minutes: 10,
|
|
793
|
-
cooldown_seconds: 60
|
|
794
|
-
};
|
|
795
|
-
function getCompletionGuardConfig(projectPath) {
|
|
796
|
-
const config = loadConfig();
|
|
797
|
-
const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
|
|
798
|
-
if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
|
|
799
|
-
return { ...global, ...config.project_overrides[projectPath].completion_guard };
|
|
800
|
-
}
|
|
801
|
-
return global;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// src/lib/completion-guard.ts
|
|
805
|
-
function checkCompletionGuard(task, agentId, db, configOverride) {
|
|
806
|
-
let config;
|
|
807
|
-
if (configOverride) {
|
|
808
|
-
config = configOverride;
|
|
809
|
-
} else {
|
|
810
|
-
const projectPath = task.project_id ? getProject(task.project_id, db)?.path : null;
|
|
811
|
-
config = getCompletionGuardConfig(projectPath);
|
|
812
|
-
}
|
|
813
|
-
if (!config.enabled)
|
|
814
|
-
return;
|
|
815
|
-
if (task.status !== "in_progress") {
|
|
816
|
-
throw new CompletionGuardError(`Task must be in 'in_progress' status before completing (current: '${task.status}'). Use start_task first.`);
|
|
817
|
-
}
|
|
818
|
-
const agent = agentId || task.assigned_to || task.agent_id;
|
|
819
|
-
if (config.min_work_seconds && task.locked_at) {
|
|
820
|
-
const startedAt = new Date(task.locked_at).getTime();
|
|
821
|
-
const elapsedSeconds = (Date.now() - startedAt) / 1000;
|
|
822
|
-
if (elapsedSeconds < config.min_work_seconds) {
|
|
823
|
-
const remaining = Math.ceil(config.min_work_seconds - elapsedSeconds);
|
|
824
|
-
throw new CompletionGuardError(`Too fast: task was started ${Math.floor(elapsedSeconds)}s ago. Minimum work duration is ${config.min_work_seconds}s. Wait ${remaining}s.`, remaining);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
if (agent && config.max_completions_per_window && config.window_minutes) {
|
|
828
|
-
const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
|
|
829
|
-
const result = db.query(`SELECT COUNT(*) as count FROM tasks
|
|
830
|
-
WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
|
|
831
|
-
if (result.count >= config.max_completions_per_window) {
|
|
832
|
-
throw new CompletionGuardError(`Rate limit: ${result.count} tasks completed in the last ${config.window_minutes} minutes (max ${config.max_completions_per_window}). Slow down.`);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
if (agent && config.cooldown_seconds) {
|
|
836
|
-
const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
|
|
837
|
-
WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
|
|
838
|
-
if (result.last_completed) {
|
|
839
|
-
const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
|
|
840
|
-
if (elapsedSeconds < config.cooldown_seconds) {
|
|
841
|
-
const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
|
|
842
|
-
throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// src/db/audit.ts
|
|
849
|
-
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
850
|
-
const d = db || getDatabase();
|
|
851
|
-
const id = uuid();
|
|
852
|
-
const timestamp = now();
|
|
853
|
-
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
|
|
854
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
|
|
855
|
-
return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
|
|
856
|
-
}
|
|
857
|
-
function getTaskHistory(taskId, db) {
|
|
858
|
-
const d = db || getDatabase();
|
|
859
|
-
return d.query("SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
|
|
860
|
-
}
|
|
861
|
-
function getRecentActivity(limit = 50, db) {
|
|
862
|
-
const d = db || getDatabase();
|
|
863
|
-
return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// src/db/tasks.ts
|
|
867
|
-
function rowToTask(row) {
|
|
868
|
-
return {
|
|
869
|
-
...row,
|
|
870
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
871
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
872
|
-
status: row.status,
|
|
873
|
-
priority: row.priority,
|
|
874
|
-
requires_approval: !!row.requires_approval
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
function insertTaskTags(taskId, tags, db) {
|
|
878
|
-
if (tags.length === 0)
|
|
879
|
-
return;
|
|
880
|
-
const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
881
|
-
for (const tag of tags) {
|
|
882
|
-
if (tag)
|
|
883
|
-
stmt.run(taskId, tag);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
function replaceTaskTags(taskId, tags, db) {
|
|
887
|
-
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
888
|
-
insertTaskTags(taskId, tags, db);
|
|
889
|
-
}
|
|
890
|
-
function createTask(input, db) {
|
|
891
|
-
const d = db || getDatabase();
|
|
892
|
-
const id = uuid();
|
|
893
|
-
const timestamp = now();
|
|
894
|
-
const tags = input.tags || [];
|
|
895
|
-
const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
|
|
896
|
-
const title = shortId ? `${shortId}: ${input.title}` : input.title;
|
|
897
|
-
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at)
|
|
898
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
899
|
-
id,
|
|
900
|
-
shortId,
|
|
901
|
-
input.project_id || null,
|
|
902
|
-
input.parent_id || null,
|
|
903
|
-
input.plan_id || null,
|
|
904
|
-
input.task_list_id || null,
|
|
905
|
-
title,
|
|
906
|
-
input.description || null,
|
|
907
|
-
input.status || "pending",
|
|
908
|
-
input.priority || "medium",
|
|
909
|
-
input.agent_id || null,
|
|
910
|
-
input.assigned_to || null,
|
|
911
|
-
input.session_id || null,
|
|
912
|
-
input.working_dir || null,
|
|
913
|
-
JSON.stringify(tags),
|
|
914
|
-
JSON.stringify(input.metadata || {}),
|
|
915
|
-
timestamp,
|
|
916
|
-
timestamp,
|
|
917
|
-
input.due_at || null,
|
|
918
|
-
input.estimated_minutes || null,
|
|
919
|
-
input.requires_approval ? 1 : 0,
|
|
920
|
-
null,
|
|
921
|
-
null
|
|
922
|
-
]);
|
|
923
|
-
if (tags.length > 0) {
|
|
924
|
-
insertTaskTags(id, tags, d);
|
|
925
|
-
}
|
|
926
|
-
return getTask(id, d);
|
|
927
|
-
}
|
|
928
|
-
function getTask(id, db) {
|
|
929
|
-
const d = db || getDatabase();
|
|
930
|
-
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
931
|
-
if (!row)
|
|
932
|
-
return null;
|
|
933
|
-
return rowToTask(row);
|
|
934
|
-
}
|
|
935
|
-
function getTaskWithRelations(id, db) {
|
|
936
|
-
const d = db || getDatabase();
|
|
937
|
-
const task = getTask(id, d);
|
|
938
|
-
if (!task)
|
|
939
|
-
return null;
|
|
940
|
-
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
941
|
-
const subtasks = subtaskRows.map(rowToTask);
|
|
942
|
-
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
943
|
-
JOIN task_dependencies td ON td.depends_on = t.id
|
|
944
|
-
WHERE td.task_id = ?`).all(id);
|
|
945
|
-
const dependencies = depRows.map(rowToTask);
|
|
946
|
-
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
947
|
-
JOIN task_dependencies td ON td.task_id = t.id
|
|
948
|
-
WHERE td.depends_on = ?`).all(id);
|
|
949
|
-
const blocked_by = blockedByRows.map(rowToTask);
|
|
950
|
-
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
951
|
-
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
952
|
-
return {
|
|
953
|
-
...task,
|
|
954
|
-
subtasks,
|
|
955
|
-
dependencies,
|
|
956
|
-
blocked_by,
|
|
957
|
-
comments,
|
|
958
|
-
parent
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
function listTasks(filter = {}, db) {
|
|
962
|
-
const d = db || getDatabase();
|
|
963
|
-
clearExpiredLocks(d);
|
|
964
|
-
const conditions = [];
|
|
965
|
-
const params = [];
|
|
966
|
-
if (filter.project_id) {
|
|
967
|
-
conditions.push("project_id = ?");
|
|
968
|
-
params.push(filter.project_id);
|
|
969
|
-
}
|
|
970
|
-
if (filter.parent_id !== undefined) {
|
|
971
|
-
if (filter.parent_id === null) {
|
|
972
|
-
conditions.push("parent_id IS NULL");
|
|
973
|
-
} else {
|
|
974
|
-
conditions.push("parent_id = ?");
|
|
975
|
-
params.push(filter.parent_id);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
if (filter.status) {
|
|
979
|
-
if (Array.isArray(filter.status)) {
|
|
980
|
-
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
981
|
-
params.push(...filter.status);
|
|
982
|
-
} else {
|
|
983
|
-
conditions.push("status = ?");
|
|
984
|
-
params.push(filter.status);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
if (filter.priority) {
|
|
988
|
-
if (Array.isArray(filter.priority)) {
|
|
989
|
-
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
990
|
-
params.push(...filter.priority);
|
|
991
|
-
} else {
|
|
992
|
-
conditions.push("priority = ?");
|
|
993
|
-
params.push(filter.priority);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
if (filter.assigned_to) {
|
|
997
|
-
conditions.push("assigned_to = ?");
|
|
998
|
-
params.push(filter.assigned_to);
|
|
999
|
-
}
|
|
1000
|
-
if (filter.agent_id) {
|
|
1001
|
-
conditions.push("agent_id = ?");
|
|
1002
|
-
params.push(filter.agent_id);
|
|
1003
|
-
}
|
|
1004
|
-
if (filter.session_id) {
|
|
1005
|
-
conditions.push("session_id = ?");
|
|
1006
|
-
params.push(filter.session_id);
|
|
1007
|
-
}
|
|
1008
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
1009
|
-
const placeholders = filter.tags.map(() => "?").join(",");
|
|
1010
|
-
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
1011
|
-
params.push(...filter.tags);
|
|
1012
|
-
}
|
|
1013
|
-
if (filter.plan_id) {
|
|
1014
|
-
conditions.push("plan_id = ?");
|
|
1015
|
-
params.push(filter.plan_id);
|
|
1016
|
-
}
|
|
1017
|
-
if (filter.task_list_id) {
|
|
1018
|
-
conditions.push("task_list_id = ?");
|
|
1019
|
-
params.push(filter.task_list_id);
|
|
1020
|
-
}
|
|
1021
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1022
|
-
let limitClause = "";
|
|
1023
|
-
if (filter.limit) {
|
|
1024
|
-
limitClause = " LIMIT ?";
|
|
1025
|
-
params.push(filter.limit);
|
|
1026
|
-
if (filter.offset) {
|
|
1027
|
-
limitClause += " OFFSET ?";
|
|
1028
|
-
params.push(filter.offset);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
|
|
1032
|
-
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
1033
|
-
created_at DESC${limitClause}`).all(...params);
|
|
1034
|
-
return rows.map(rowToTask);
|
|
1035
|
-
}
|
|
1036
|
-
function updateTask(id, input, db) {
|
|
1037
|
-
const d = db || getDatabase();
|
|
1038
|
-
const task = getTask(id, d);
|
|
1039
|
-
if (!task)
|
|
1040
|
-
throw new TaskNotFoundError(id);
|
|
1041
|
-
if (task.version !== input.version) {
|
|
1042
|
-
throw new VersionConflictError(id, input.version, task.version);
|
|
1043
|
-
}
|
|
1044
|
-
const sets = ["version = version + 1", "updated_at = ?"];
|
|
1045
|
-
const params = [now()];
|
|
1046
|
-
if (input.title !== undefined) {
|
|
1047
|
-
sets.push("title = ?");
|
|
1048
|
-
params.push(input.title);
|
|
1049
|
-
}
|
|
1050
|
-
if (input.description !== undefined) {
|
|
1051
|
-
sets.push("description = ?");
|
|
1052
|
-
params.push(input.description);
|
|
1053
|
-
}
|
|
1054
|
-
if (input.status !== undefined) {
|
|
1055
|
-
if (input.status === "completed") {
|
|
1056
|
-
checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
|
|
1057
|
-
}
|
|
1058
|
-
sets.push("status = ?");
|
|
1059
|
-
params.push(input.status);
|
|
1060
|
-
if (input.status === "completed") {
|
|
1061
|
-
sets.push("completed_at = ?");
|
|
1062
|
-
params.push(now());
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
if (input.priority !== undefined) {
|
|
1066
|
-
sets.push("priority = ?");
|
|
1067
|
-
params.push(input.priority);
|
|
1068
|
-
}
|
|
1069
|
-
if (input.assigned_to !== undefined) {
|
|
1070
|
-
sets.push("assigned_to = ?");
|
|
1071
|
-
params.push(input.assigned_to);
|
|
1072
|
-
}
|
|
1073
|
-
if (input.tags !== undefined) {
|
|
1074
|
-
sets.push("tags = ?");
|
|
1075
|
-
params.push(JSON.stringify(input.tags));
|
|
1076
|
-
}
|
|
1077
|
-
if (input.metadata !== undefined) {
|
|
1078
|
-
sets.push("metadata = ?");
|
|
1079
|
-
params.push(JSON.stringify(input.metadata));
|
|
1080
|
-
}
|
|
1081
|
-
if (input.plan_id !== undefined) {
|
|
1082
|
-
sets.push("plan_id = ?");
|
|
1083
|
-
params.push(input.plan_id);
|
|
1084
|
-
}
|
|
1085
|
-
if (input.task_list_id !== undefined) {
|
|
1086
|
-
sets.push("task_list_id = ?");
|
|
1087
|
-
params.push(input.task_list_id);
|
|
1088
|
-
}
|
|
1089
|
-
if (input.due_at !== undefined) {
|
|
1090
|
-
sets.push("due_at = ?");
|
|
1091
|
-
params.push(input.due_at);
|
|
1092
|
-
}
|
|
1093
|
-
if (input.estimated_minutes !== undefined) {
|
|
1094
|
-
sets.push("estimated_minutes = ?");
|
|
1095
|
-
params.push(input.estimated_minutes);
|
|
1096
|
-
}
|
|
1097
|
-
if (input.requires_approval !== undefined) {
|
|
1098
|
-
sets.push("requires_approval = ?");
|
|
1099
|
-
params.push(input.requires_approval ? 1 : 0);
|
|
1100
|
-
}
|
|
1101
|
-
if (input.approved_by !== undefined) {
|
|
1102
|
-
sets.push("approved_by = ?");
|
|
1103
|
-
params.push(input.approved_by);
|
|
1104
|
-
sets.push("approved_at = ?");
|
|
1105
|
-
params.push(now());
|
|
1106
|
-
}
|
|
1107
|
-
params.push(id, input.version);
|
|
1108
|
-
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
1109
|
-
if (result.changes === 0) {
|
|
1110
|
-
const current = getTask(id, d);
|
|
1111
|
-
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
1112
|
-
}
|
|
1113
|
-
if (input.tags !== undefined) {
|
|
1114
|
-
replaceTaskTags(id, input.tags, d);
|
|
1115
|
-
}
|
|
1116
|
-
const agentId = task.assigned_to || task.agent_id || null;
|
|
1117
|
-
if (input.status !== undefined && input.status !== task.status)
|
|
1118
|
-
logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
|
|
1119
|
-
if (input.priority !== undefined && input.priority !== task.priority)
|
|
1120
|
-
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
1121
|
-
if (input.title !== undefined && input.title !== task.title)
|
|
1122
|
-
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
1123
|
-
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
1124
|
-
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
1125
|
-
if (input.approved_by !== undefined)
|
|
1126
|
-
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
1127
|
-
return {
|
|
1128
|
-
...task,
|
|
1129
|
-
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
1130
|
-
tags: input.tags ?? task.tags,
|
|
1131
|
-
metadata: input.metadata ?? task.metadata,
|
|
1132
|
-
version: task.version + 1,
|
|
1133
|
-
updated_at: now(),
|
|
1134
|
-
completed_at: input.status === "completed" ? now() : task.completed_at,
|
|
1135
|
-
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
1136
|
-
approved_by: input.approved_by ?? task.approved_by,
|
|
1137
|
-
approved_at: input.approved_by ? now() : task.approved_at
|
|
1138
|
-
};
|
|
1139
|
-
}
|
|
1140
|
-
function deleteTask(id, db) {
|
|
1141
|
-
const d = db || getDatabase();
|
|
1142
|
-
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
1143
|
-
return result.changes > 0;
|
|
1144
|
-
}
|
|
1145
|
-
function getBlockingDeps(id, db) {
|
|
1146
|
-
const d = db || getDatabase();
|
|
1147
|
-
const deps = getTaskDependencies(id, d);
|
|
1148
|
-
if (deps.length === 0)
|
|
1149
|
-
return [];
|
|
1150
|
-
const blocking = [];
|
|
1151
|
-
for (const dep of deps) {
|
|
1152
|
-
const task = getTask(dep.depends_on, d);
|
|
1153
|
-
if (task && task.status !== "completed")
|
|
1154
|
-
blocking.push(task);
|
|
1155
|
-
}
|
|
1156
|
-
return blocking;
|
|
1157
|
-
}
|
|
1158
|
-
function startTask(id, agentId, db) {
|
|
1159
|
-
const d = db || getDatabase();
|
|
1160
|
-
const task = getTask(id, d);
|
|
1161
|
-
if (!task)
|
|
1162
|
-
throw new TaskNotFoundError(id);
|
|
1163
|
-
const blocking = getBlockingDeps(id, d);
|
|
1164
|
-
if (blocking.length > 0) {
|
|
1165
|
-
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
1166
|
-
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
1167
|
-
}
|
|
1168
|
-
const cutoff = lockExpiryCutoff();
|
|
1169
|
-
const timestamp = now();
|
|
1170
|
-
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
1171
|
-
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
1172
|
-
if (result.changes === 0) {
|
|
1173
|
-
if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
1174
|
-
throw new LockError(id, task.locked_by);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
1178
|
-
return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
|
|
1179
|
-
}
|
|
1180
|
-
function completeTask(id, agentId, db, evidence) {
|
|
1181
|
-
const d = db || getDatabase();
|
|
1182
|
-
const task = getTask(id, d);
|
|
1183
|
-
if (!task)
|
|
1184
|
-
throw new TaskNotFoundError(id);
|
|
1185
|
-
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
1186
|
-
throw new LockError(id, task.locked_by);
|
|
1187
|
-
}
|
|
1188
|
-
checkCompletionGuard(task, agentId || null, d);
|
|
1189
|
-
if (evidence) {
|
|
1190
|
-
const meta2 = { ...task.metadata, _evidence: evidence };
|
|
1191
|
-
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
|
|
1192
|
-
}
|
|
1193
|
-
const timestamp = now();
|
|
1194
|
-
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
1195
|
-
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
1196
|
-
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
1197
|
-
const meta = evidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
|
|
1198
|
-
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
1199
|
-
}
|
|
1200
|
-
function lockTask(id, agentId, db) {
|
|
1201
|
-
const d = db || getDatabase();
|
|
1202
|
-
const task = getTask(id, d);
|
|
1203
|
-
if (!task)
|
|
1204
|
-
throw new TaskNotFoundError(id);
|
|
1205
|
-
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
1206
|
-
return { success: true, locked_by: agentId, locked_at: task.locked_at };
|
|
1207
|
-
}
|
|
1208
|
-
const cutoff = lockExpiryCutoff();
|
|
1209
|
-
const timestamp = now();
|
|
1210
|
-
const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
1211
|
-
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
1212
|
-
if (result.changes === 0) {
|
|
1213
|
-
const current = getTask(id, d);
|
|
1214
|
-
if (!current)
|
|
1215
|
-
throw new TaskNotFoundError(id);
|
|
1216
|
-
if (current.locked_by && !isLockExpired(current.locked_at)) {
|
|
1217
|
-
return {
|
|
1218
|
-
success: false,
|
|
1219
|
-
locked_by: current.locked_by,
|
|
1220
|
-
locked_at: current.locked_at,
|
|
1221
|
-
error: `Task is locked by ${current.locked_by}`
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
return { success: true, locked_by: agentId, locked_at: timestamp };
|
|
1226
|
-
}
|
|
1227
|
-
function unlockTask(id, agentId, db) {
|
|
1228
|
-
const d = db || getDatabase();
|
|
1229
|
-
const task = getTask(id, d);
|
|
1230
|
-
if (!task)
|
|
1231
|
-
throw new TaskNotFoundError(id);
|
|
1232
|
-
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
1233
|
-
throw new LockError(id, task.locked_by);
|
|
1234
|
-
}
|
|
1235
|
-
const timestamp = now();
|
|
1236
|
-
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
|
|
1237
|
-
WHERE id = ?`, [timestamp, id]);
|
|
1238
|
-
return true;
|
|
1239
|
-
}
|
|
1240
|
-
function addDependency(taskId, dependsOn, db) {
|
|
1241
|
-
const d = db || getDatabase();
|
|
1242
|
-
if (!getTask(taskId, d))
|
|
1243
|
-
throw new TaskNotFoundError(taskId);
|
|
1244
|
-
if (!getTask(dependsOn, d))
|
|
1245
|
-
throw new TaskNotFoundError(dependsOn);
|
|
1246
|
-
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
1247
|
-
throw new DependencyCycleError(taskId, dependsOn);
|
|
1248
|
-
}
|
|
1249
|
-
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
|
|
1250
|
-
}
|
|
1251
|
-
function removeDependency(taskId, dependsOn, db) {
|
|
1252
|
-
const d = db || getDatabase();
|
|
1253
|
-
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
1254
|
-
return result.changes > 0;
|
|
1255
|
-
}
|
|
1256
|
-
function getTaskDependencies(taskId, db) {
|
|
1257
|
-
const d = db || getDatabase();
|
|
1258
|
-
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
1259
|
-
}
|
|
1260
|
-
function getTaskDependents(taskId, db) {
|
|
1261
|
-
const d = db || getDatabase();
|
|
1262
|
-
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
1263
|
-
}
|
|
1264
|
-
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
1265
|
-
const visited = new Set;
|
|
1266
|
-
const queue = [dependsOn];
|
|
1267
|
-
while (queue.length > 0) {
|
|
1268
|
-
const current = queue.shift();
|
|
1269
|
-
if (current === taskId)
|
|
1270
|
-
return true;
|
|
1271
|
-
if (visited.has(current))
|
|
1272
|
-
continue;
|
|
1273
|
-
visited.add(current);
|
|
1274
|
-
const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
|
|
1275
|
-
for (const dep of deps) {
|
|
1276
|
-
queue.push(dep.depends_on);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
return false;
|
|
1280
|
-
}
|
|
1281
|
-
// src/db/plans.ts
|
|
1282
|
-
function createPlan(input, db) {
|
|
1283
|
-
const d = db || getDatabase();
|
|
1284
|
-
const id = uuid();
|
|
1285
|
-
const timestamp = now();
|
|
1286
|
-
d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
|
|
1287
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1288
|
-
id,
|
|
1289
|
-
input.project_id || null,
|
|
1290
|
-
input.task_list_id || null,
|
|
1291
|
-
input.agent_id || null,
|
|
1292
|
-
input.name,
|
|
1293
|
-
input.description || null,
|
|
1294
|
-
input.status || "active",
|
|
1295
|
-
timestamp,
|
|
1296
|
-
timestamp
|
|
1297
|
-
]);
|
|
1298
|
-
return getPlan(id, d);
|
|
1299
|
-
}
|
|
1300
|
-
function getPlan(id, db) {
|
|
1301
|
-
const d = db || getDatabase();
|
|
1302
|
-
const row = d.query("SELECT * FROM plans WHERE id = ?").get(id);
|
|
1303
|
-
return row;
|
|
1304
|
-
}
|
|
1305
|
-
function listPlans(projectId, db) {
|
|
1306
|
-
const d = db || getDatabase();
|
|
1307
|
-
if (projectId) {
|
|
1308
|
-
return d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
|
|
1309
|
-
}
|
|
1310
|
-
return d.query("SELECT * FROM plans ORDER BY created_at DESC").all();
|
|
1311
|
-
}
|
|
1312
|
-
function updatePlan(id, input, db) {
|
|
1313
|
-
const d = db || getDatabase();
|
|
1314
|
-
const plan = getPlan(id, d);
|
|
1315
|
-
if (!plan)
|
|
1316
|
-
throw new PlanNotFoundError(id);
|
|
1317
|
-
const sets = ["updated_at = ?"];
|
|
1318
|
-
const params = [now()];
|
|
1319
|
-
if (input.name !== undefined) {
|
|
1320
|
-
sets.push("name = ?");
|
|
1321
|
-
params.push(input.name);
|
|
1322
|
-
}
|
|
1323
|
-
if (input.description !== undefined) {
|
|
1324
|
-
sets.push("description = ?");
|
|
1325
|
-
params.push(input.description);
|
|
1326
|
-
}
|
|
1327
|
-
if (input.status !== undefined) {
|
|
1328
|
-
sets.push("status = ?");
|
|
1329
|
-
params.push(input.status);
|
|
1330
|
-
}
|
|
1331
|
-
if (input.task_list_id !== undefined) {
|
|
1332
|
-
sets.push("task_list_id = ?");
|
|
1333
|
-
params.push(input.task_list_id);
|
|
1334
|
-
}
|
|
1335
|
-
if (input.agent_id !== undefined) {
|
|
1336
|
-
sets.push("agent_id = ?");
|
|
1337
|
-
params.push(input.agent_id);
|
|
1338
|
-
}
|
|
1339
|
-
params.push(id);
|
|
1340
|
-
d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1341
|
-
return getPlan(id, d);
|
|
1342
|
-
}
|
|
1343
|
-
function deletePlan(id, db) {
|
|
1344
|
-
const d = db || getDatabase();
|
|
1345
|
-
const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
|
|
1346
|
-
return result.changes > 0;
|
|
1347
|
-
}
|
|
1348
|
-
// src/db/comments.ts
|
|
1349
|
-
function addComment(input, db) {
|
|
1350
|
-
const d = db || getDatabase();
|
|
1351
|
-
if (!getTask(input.task_id, d)) {
|
|
1352
|
-
throw new TaskNotFoundError(input.task_id);
|
|
1353
|
-
}
|
|
1354
|
-
const id = uuid();
|
|
1355
|
-
const timestamp = now();
|
|
1356
|
-
d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, created_at)
|
|
1357
|
-
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
1358
|
-
id,
|
|
1359
|
-
input.task_id,
|
|
1360
|
-
input.agent_id || null,
|
|
1361
|
-
input.session_id || null,
|
|
1362
|
-
input.content,
|
|
1363
|
-
timestamp
|
|
1364
|
-
]);
|
|
1365
|
-
return getComment(id, d);
|
|
1366
|
-
}
|
|
1367
|
-
function getComment(id, db) {
|
|
1368
|
-
const d = db || getDatabase();
|
|
1369
|
-
return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
|
|
1370
|
-
}
|
|
1371
|
-
function listComments(taskId, db) {
|
|
1372
|
-
const d = db || getDatabase();
|
|
1373
|
-
return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
|
|
1374
|
-
}
|
|
1375
|
-
function deleteComment(id, db) {
|
|
1376
|
-
const d = db || getDatabase();
|
|
1377
|
-
const result = d.run("DELETE FROM task_comments WHERE id = ?", [id]);
|
|
1378
|
-
return result.changes > 0;
|
|
1379
|
-
}
|
|
1380
|
-
// src/db/agents.ts
|
|
1381
|
-
function shortUuid() {
|
|
1382
|
-
return crypto.randomUUID().slice(0, 8);
|
|
1383
|
-
}
|
|
1384
|
-
function rowToAgent(row) {
|
|
1385
|
-
return {
|
|
1386
|
-
...row,
|
|
1387
|
-
permissions: JSON.parse(row.permissions || '["*"]'),
|
|
1388
|
-
metadata: JSON.parse(row.metadata || "{}")
|
|
1389
|
-
};
|
|
1390
|
-
}
|
|
1391
|
-
function registerAgent(input, db) {
|
|
1392
|
-
const d = db || getDatabase();
|
|
1393
|
-
const existing = getAgentByName(input.name, d);
|
|
1394
|
-
if (existing) {
|
|
1395
|
-
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), existing.id]);
|
|
1396
|
-
return getAgent(existing.id, d);
|
|
1397
|
-
}
|
|
1398
|
-
const id = shortUuid();
|
|
1399
|
-
const timestamp = now();
|
|
1400
|
-
d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, reports_to, metadata, created_at, last_seen_at)
|
|
1401
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1402
|
-
id,
|
|
1403
|
-
input.name,
|
|
1404
|
-
input.description || null,
|
|
1405
|
-
input.role || "agent",
|
|
1406
|
-
input.title || null,
|
|
1407
|
-
input.level || null,
|
|
1408
|
-
JSON.stringify(input.permissions || ["*"]),
|
|
1409
|
-
input.reports_to || null,
|
|
1410
|
-
JSON.stringify(input.metadata || {}),
|
|
1411
|
-
timestamp,
|
|
1412
|
-
timestamp
|
|
1413
|
-
]);
|
|
1414
|
-
return getAgent(id, d);
|
|
1415
|
-
}
|
|
1416
|
-
function getAgent(id, db) {
|
|
1417
|
-
const d = db || getDatabase();
|
|
1418
|
-
const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
|
|
1419
|
-
return row ? rowToAgent(row) : null;
|
|
1420
|
-
}
|
|
1421
|
-
function getAgentByName(name, db) {
|
|
1422
|
-
const d = db || getDatabase();
|
|
1423
|
-
const row = d.query("SELECT * FROM agents WHERE name = ?").get(name);
|
|
1424
|
-
return row ? rowToAgent(row) : null;
|
|
1425
|
-
}
|
|
1426
|
-
function listAgents(db) {
|
|
1427
|
-
const d = db || getDatabase();
|
|
1428
|
-
return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
|
|
1429
|
-
}
|
|
1430
|
-
function updateAgentActivity(id, db) {
|
|
1431
|
-
const d = db || getDatabase();
|
|
1432
|
-
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
|
|
1433
|
-
}
|
|
1434
|
-
function updateAgent(id, input, db) {
|
|
1435
|
-
const d = db || getDatabase();
|
|
1436
|
-
const agent = getAgent(id, d);
|
|
1437
|
-
if (!agent)
|
|
1438
|
-
throw new Error(`Agent not found: ${id}`);
|
|
1439
|
-
const sets = ["last_seen_at = ?"];
|
|
1440
|
-
const params = [now()];
|
|
1441
|
-
if (input.name !== undefined) {
|
|
1442
|
-
sets.push("name = ?");
|
|
1443
|
-
params.push(input.name);
|
|
1444
|
-
}
|
|
1445
|
-
if (input.description !== undefined) {
|
|
1446
|
-
sets.push("description = ?");
|
|
1447
|
-
params.push(input.description);
|
|
1448
|
-
}
|
|
1449
|
-
if (input.role !== undefined) {
|
|
1450
|
-
sets.push("role = ?");
|
|
1451
|
-
params.push(input.role);
|
|
1452
|
-
}
|
|
1453
|
-
if (input.permissions !== undefined) {
|
|
1454
|
-
sets.push("permissions = ?");
|
|
1455
|
-
params.push(JSON.stringify(input.permissions));
|
|
1456
|
-
}
|
|
1457
|
-
if (input.title !== undefined) {
|
|
1458
|
-
sets.push("title = ?");
|
|
1459
|
-
params.push(input.title);
|
|
1460
|
-
}
|
|
1461
|
-
if (input.level !== undefined) {
|
|
1462
|
-
sets.push("level = ?");
|
|
1463
|
-
params.push(input.level);
|
|
1464
|
-
}
|
|
1465
|
-
if (input.reports_to !== undefined) {
|
|
1466
|
-
sets.push("reports_to = ?");
|
|
1467
|
-
params.push(input.reports_to);
|
|
1468
|
-
}
|
|
1469
|
-
if (input.metadata !== undefined) {
|
|
1470
|
-
sets.push("metadata = ?");
|
|
1471
|
-
params.push(JSON.stringify(input.metadata));
|
|
1472
|
-
}
|
|
1473
|
-
params.push(id);
|
|
1474
|
-
d.run(`UPDATE agents SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1475
|
-
return getAgent(id, d);
|
|
1476
|
-
}
|
|
1477
|
-
function deleteAgent(id, db) {
|
|
1478
|
-
const d = db || getDatabase();
|
|
1479
|
-
return d.run("DELETE FROM agents WHERE id = ?", [id]).changes > 0;
|
|
1480
|
-
}
|
|
1481
|
-
function getDirectReports(agentId, db) {
|
|
1482
|
-
const d = db || getDatabase();
|
|
1483
|
-
return d.query("SELECT * FROM agents WHERE reports_to = ? ORDER BY name").all(agentId).map(rowToAgent);
|
|
1484
|
-
}
|
|
1485
|
-
function getOrgChart(db) {
|
|
1486
|
-
const agents = listAgents(db);
|
|
1487
|
-
const byManager = new Map;
|
|
1488
|
-
for (const a of agents) {
|
|
1489
|
-
const key = a.reports_to;
|
|
1490
|
-
if (!byManager.has(key))
|
|
1491
|
-
byManager.set(key, []);
|
|
1492
|
-
byManager.get(key).push(a);
|
|
1493
|
-
}
|
|
1494
|
-
function buildTree(parentId) {
|
|
1495
|
-
const children = byManager.get(parentId) || [];
|
|
1496
|
-
return children.map((a) => ({ agent: a, reports: buildTree(a.id) }));
|
|
1497
|
-
}
|
|
1498
|
-
return buildTree(null);
|
|
1499
|
-
}
|
|
1500
|
-
// src/db/task-lists.ts
|
|
1501
|
-
function rowToTaskList(row) {
|
|
1502
|
-
return {
|
|
1503
|
-
...row,
|
|
1504
|
-
metadata: JSON.parse(row.metadata || "{}")
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
function createTaskList(input, db) {
|
|
1508
|
-
const d = db || getDatabase();
|
|
1509
|
-
const id = uuid();
|
|
1510
|
-
const timestamp = now();
|
|
1511
|
-
const slug = input.slug || slugify(input.name);
|
|
1512
|
-
if (!input.project_id) {
|
|
1513
|
-
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
1514
|
-
if (existing) {
|
|
1515
|
-
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
1519
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
1520
|
-
return getTaskList(id, d);
|
|
1521
|
-
}
|
|
1522
|
-
function getTaskList(id, db) {
|
|
1523
|
-
const d = db || getDatabase();
|
|
1524
|
-
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
1525
|
-
return row ? rowToTaskList(row) : null;
|
|
1526
|
-
}
|
|
1527
|
-
function getTaskListBySlug(slug, projectId, db) {
|
|
1528
|
-
const d = db || getDatabase();
|
|
1529
|
-
let row;
|
|
1530
|
-
if (projectId) {
|
|
1531
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
1532
|
-
} else {
|
|
1533
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
1534
|
-
}
|
|
1535
|
-
return row ? rowToTaskList(row) : null;
|
|
1536
|
-
}
|
|
1537
|
-
function listTaskLists(projectId, db) {
|
|
1538
|
-
const d = db || getDatabase();
|
|
1539
|
-
if (projectId) {
|
|
1540
|
-
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
1541
|
-
}
|
|
1542
|
-
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
1543
|
-
}
|
|
1544
|
-
function updateTaskList(id, input, db) {
|
|
1545
|
-
const d = db || getDatabase();
|
|
1546
|
-
const existing = getTaskList(id, d);
|
|
1547
|
-
if (!existing)
|
|
1548
|
-
throw new TaskListNotFoundError(id);
|
|
1549
|
-
const sets = ["updated_at = ?"];
|
|
1550
|
-
const params = [now()];
|
|
1551
|
-
if (input.name !== undefined) {
|
|
1552
|
-
sets.push("name = ?");
|
|
1553
|
-
params.push(input.name);
|
|
1554
|
-
}
|
|
1555
|
-
if (input.description !== undefined) {
|
|
1556
|
-
sets.push("description = ?");
|
|
1557
|
-
params.push(input.description);
|
|
1558
|
-
}
|
|
1559
|
-
if (input.metadata !== undefined) {
|
|
1560
|
-
sets.push("metadata = ?");
|
|
1561
|
-
params.push(JSON.stringify(input.metadata));
|
|
1562
|
-
}
|
|
1563
|
-
params.push(id);
|
|
1564
|
-
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1565
|
-
return getTaskList(id, d);
|
|
1566
|
-
}
|
|
1567
|
-
function deleteTaskList(id, db) {
|
|
1568
|
-
const d = db || getDatabase();
|
|
1569
|
-
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
1570
|
-
}
|
|
1571
|
-
function ensureTaskList(name, slug, projectId, db) {
|
|
1572
|
-
const d = db || getDatabase();
|
|
1573
|
-
const existing = getTaskListBySlug(slug, projectId, d);
|
|
1574
|
-
if (existing)
|
|
1575
|
-
return existing;
|
|
1576
|
-
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
1577
|
-
}
|
|
1578
|
-
// src/db/sessions.ts
|
|
1579
|
-
function rowToSession(row) {
|
|
1580
|
-
return {
|
|
1581
|
-
...row,
|
|
1582
|
-
metadata: JSON.parse(row.metadata || "{}")
|
|
1583
|
-
};
|
|
1584
|
-
}
|
|
1585
|
-
function createSession(input, db) {
|
|
1586
|
-
const d = db || getDatabase();
|
|
1587
|
-
const id = uuid();
|
|
1588
|
-
const timestamp = now();
|
|
1589
|
-
d.run(`INSERT INTO sessions (id, agent_id, project_id, working_dir, started_at, last_activity, metadata)
|
|
1590
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
1591
|
-
id,
|
|
1592
|
-
input.agent_id || null,
|
|
1593
|
-
input.project_id || null,
|
|
1594
|
-
input.working_dir || null,
|
|
1595
|
-
timestamp,
|
|
1596
|
-
timestamp,
|
|
1597
|
-
JSON.stringify(input.metadata || {})
|
|
1598
|
-
]);
|
|
1599
|
-
return getSession(id, d);
|
|
1600
|
-
}
|
|
1601
|
-
function getSession(id, db) {
|
|
1602
|
-
const d = db || getDatabase();
|
|
1603
|
-
const row = d.query("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
1604
|
-
if (!row)
|
|
1605
|
-
return null;
|
|
1606
|
-
return rowToSession(row);
|
|
1607
|
-
}
|
|
1608
|
-
function listSessions(db) {
|
|
1609
|
-
const d = db || getDatabase();
|
|
1610
|
-
const rows = d.query("SELECT * FROM sessions ORDER BY last_activity DESC").all();
|
|
1611
|
-
return rows.map(rowToSession);
|
|
1612
|
-
}
|
|
1613
|
-
function updateSessionActivity(id, db) {
|
|
1614
|
-
const d = db || getDatabase();
|
|
1615
|
-
d.run("UPDATE sessions SET last_activity = ? WHERE id = ?", [now(), id]);
|
|
1616
|
-
}
|
|
1617
|
-
function deleteSession(id, db) {
|
|
1618
|
-
const d = db || getDatabase();
|
|
1619
|
-
const result = d.run("DELETE FROM sessions WHERE id = ?", [id]);
|
|
1620
|
-
return result.changes > 0;
|
|
1621
|
-
}
|
|
1622
|
-
// src/db/webhooks.ts
|
|
1623
|
-
function rowToWebhook(row) {
|
|
1624
|
-
return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
|
|
1625
|
-
}
|
|
1626
|
-
function createWebhook(input, db) {
|
|
1627
|
-
const d = db || getDatabase();
|
|
1628
|
-
const id = uuid();
|
|
1629
|
-
d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
|
|
1630
|
-
return getWebhook(id, d);
|
|
1631
|
-
}
|
|
1632
|
-
function getWebhook(id, db) {
|
|
1633
|
-
const d = db || getDatabase();
|
|
1634
|
-
const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
|
|
1635
|
-
return row ? rowToWebhook(row) : null;
|
|
1636
|
-
}
|
|
1637
|
-
function listWebhooks(db) {
|
|
1638
|
-
const d = db || getDatabase();
|
|
1639
|
-
return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
|
|
1640
|
-
}
|
|
1641
|
-
function deleteWebhook(id, db) {
|
|
1642
|
-
const d = db || getDatabase();
|
|
1643
|
-
return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
|
|
1644
|
-
}
|
|
1645
|
-
async function dispatchWebhook(event, payload, db) {
|
|
1646
|
-
const webhooks = listWebhooks(db).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
|
|
1647
|
-
for (const wh of webhooks) {
|
|
1648
|
-
try {
|
|
1649
|
-
const body = JSON.stringify({ event, payload, timestamp: now() });
|
|
1650
|
-
const headers = { "Content-Type": "application/json" };
|
|
1651
|
-
if (wh.secret) {
|
|
1652
|
-
const encoder = new TextEncoder;
|
|
1653
|
-
const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1654
|
-
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
1655
|
-
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1656
|
-
}
|
|
1657
|
-
fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
|
|
1658
|
-
} catch {}
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
// src/db/templates.ts
|
|
1662
|
-
function rowToTemplate(row) {
|
|
1663
|
-
return {
|
|
1664
|
-
...row,
|
|
1665
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
1666
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
1667
|
-
priority: row.priority || "medium"
|
|
1668
|
-
};
|
|
1669
|
-
}
|
|
1670
|
-
function createTemplate(input, db) {
|
|
1671
|
-
const d = db || getDatabase();
|
|
1672
|
-
const id = uuid();
|
|
1673
|
-
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
|
|
1674
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1675
|
-
id,
|
|
1676
|
-
input.name,
|
|
1677
|
-
input.title_pattern,
|
|
1678
|
-
input.description || null,
|
|
1679
|
-
input.priority || "medium",
|
|
1680
|
-
JSON.stringify(input.tags || []),
|
|
1681
|
-
input.project_id || null,
|
|
1682
|
-
input.plan_id || null,
|
|
1683
|
-
JSON.stringify(input.metadata || {}),
|
|
1684
|
-
now()
|
|
1685
|
-
]);
|
|
1686
|
-
return getTemplate(id, d);
|
|
1687
|
-
}
|
|
1688
|
-
function getTemplate(id, db) {
|
|
1689
|
-
const d = db || getDatabase();
|
|
1690
|
-
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(id);
|
|
1691
|
-
return row ? rowToTemplate(row) : null;
|
|
1692
|
-
}
|
|
1693
|
-
function listTemplates(db) {
|
|
1694
|
-
const d = db || getDatabase();
|
|
1695
|
-
return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
|
|
1696
|
-
}
|
|
1697
|
-
function deleteTemplate(id, db) {
|
|
1698
|
-
const d = db || getDatabase();
|
|
1699
|
-
return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
|
|
1700
|
-
}
|
|
1701
|
-
function taskFromTemplate(templateId, overrides = {}, db) {
|
|
1702
|
-
const t = getTemplate(templateId, db);
|
|
1703
|
-
if (!t)
|
|
1704
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
1705
|
-
return {
|
|
1706
|
-
title: overrides.title || t.title_pattern,
|
|
1707
|
-
description: overrides.description ?? t.description ?? undefined,
|
|
1708
|
-
priority: overrides.priority ?? t.priority,
|
|
1709
|
-
tags: overrides.tags ?? t.tags,
|
|
1710
|
-
project_id: overrides.project_id ?? t.project_id ?? undefined,
|
|
1711
|
-
plan_id: overrides.plan_id ?? t.plan_id ?? undefined,
|
|
1712
|
-
metadata: overrides.metadata ?? t.metadata,
|
|
1713
|
-
...overrides
|
|
1714
|
-
};
|
|
1715
|
-
}
|
|
1716
|
-
// src/lib/search.ts
|
|
1717
|
-
function rowToTask2(row) {
|
|
1718
|
-
return {
|
|
1719
|
-
...row,
|
|
1720
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
1721
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
1722
|
-
status: row.status,
|
|
1723
|
-
priority: row.priority
|
|
1724
|
-
};
|
|
1725
|
-
}
|
|
1726
|
-
function searchTasks(query, projectId, taskListId, db) {
|
|
1727
|
-
const d = db || getDatabase();
|
|
1728
|
-
clearExpiredLocks(d);
|
|
1729
|
-
const pattern = `%${query}%`;
|
|
1730
|
-
let sql = `SELECT * FROM tasks WHERE (title LIKE ? OR description LIKE ? OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = tasks.id AND tag LIKE ?))`;
|
|
1731
|
-
const params = [pattern, pattern, pattern];
|
|
1732
|
-
if (projectId) {
|
|
1733
|
-
sql += " AND project_id = ?";
|
|
1734
|
-
params.push(projectId);
|
|
1735
|
-
}
|
|
1736
|
-
if (taskListId) {
|
|
1737
|
-
sql += " AND task_list_id = ?";
|
|
1738
|
-
params.push(taskListId);
|
|
1739
|
-
}
|
|
1740
|
-
sql += ` ORDER BY
|
|
1741
|
-
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
1742
|
-
created_at DESC`;
|
|
1743
|
-
const rows = d.query(sql).all(...params);
|
|
1744
|
-
return rows.map(rowToTask2);
|
|
1745
|
-
}
|
|
1746
|
-
// src/lib/claude-tasks.ts
|
|
1747
|
-
import { existsSync as existsSync4, readFileSync as readFileSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1748
|
-
import { join as join4 } from "path";
|
|
1749
|
-
function getTaskListDir(taskListId) {
|
|
1750
|
-
return join4(HOME, ".claude", "tasks", taskListId);
|
|
1751
|
-
}
|
|
1752
|
-
function readClaudeTask(dir, filename) {
|
|
1753
|
-
return readJsonFile(join4(dir, filename));
|
|
1754
|
-
}
|
|
1755
|
-
function writeClaudeTask(dir, task) {
|
|
1756
|
-
writeJsonFile(join4(dir, `${task.id}.json`), task);
|
|
1757
|
-
}
|
|
1758
|
-
function toClaudeStatus(status) {
|
|
1759
|
-
if (status === "pending" || status === "in_progress" || status === "completed") {
|
|
1760
|
-
return status;
|
|
1761
|
-
}
|
|
1762
|
-
return "completed";
|
|
1763
|
-
}
|
|
1764
|
-
function toSqliteStatus(status) {
|
|
1765
|
-
return status;
|
|
1766
|
-
}
|
|
1767
|
-
function readPrefixCounter(dir) {
|
|
1768
|
-
const path = join4(dir, ".prefix-counter");
|
|
1769
|
-
if (!existsSync4(path))
|
|
1770
|
-
return 0;
|
|
1771
|
-
const val = parseInt(readFileSync2(path, "utf-8").trim(), 10);
|
|
1772
|
-
return isNaN(val) ? 0 : val;
|
|
1773
|
-
}
|
|
1774
|
-
function writePrefixCounter(dir, value) {
|
|
1775
|
-
writeFileSync2(join4(dir, ".prefix-counter"), String(value));
|
|
1776
|
-
}
|
|
1777
|
-
function formatPrefixedSubject(title, prefix, counter) {
|
|
1778
|
-
const padded = String(counter).padStart(5, "0");
|
|
1779
|
-
return `${prefix}-${padded}: ${title}`;
|
|
1780
|
-
}
|
|
1781
|
-
function taskToClaudeTask(task, claudeTaskId, existingMeta) {
|
|
1782
|
-
return {
|
|
1783
|
-
id: claudeTaskId,
|
|
1784
|
-
subject: task.title,
|
|
1785
|
-
description: task.description || "",
|
|
1786
|
-
activeForm: "",
|
|
1787
|
-
status: toClaudeStatus(task.status),
|
|
1788
|
-
owner: task.assigned_to || task.agent_id || "",
|
|
1789
|
-
blocks: [],
|
|
1790
|
-
blockedBy: [],
|
|
1791
|
-
metadata: {
|
|
1792
|
-
...existingMeta || {},
|
|
1793
|
-
todos_id: task.id,
|
|
1794
|
-
priority: task.priority,
|
|
1795
|
-
todos_updated_at: task.updated_at,
|
|
1796
|
-
todos_version: task.version
|
|
1797
|
-
}
|
|
1798
|
-
};
|
|
1799
|
-
}
|
|
1800
|
-
function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
1801
|
-
const dir = getTaskListDir(taskListId);
|
|
1802
|
-
if (!existsSync4(dir))
|
|
1803
|
-
ensureDir2(dir);
|
|
1804
|
-
const filter = {};
|
|
1805
|
-
if (projectId)
|
|
1806
|
-
filter["project_id"] = projectId;
|
|
1807
|
-
const tasks = listTasks(filter);
|
|
1808
|
-
const existingByTodosId = new Map;
|
|
1809
|
-
const files = listJsonFiles(dir);
|
|
1810
|
-
for (const f of files) {
|
|
1811
|
-
const path = join4(dir, f);
|
|
1812
|
-
const ct = readClaudeTask(dir, f);
|
|
1813
|
-
if (ct?.metadata?.["todos_id"]) {
|
|
1814
|
-
existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
let hwm = readHighWaterMark(dir);
|
|
1818
|
-
let pushed = 0;
|
|
1819
|
-
const errors = [];
|
|
1820
|
-
const prefer = options.prefer || "remote";
|
|
1821
|
-
const prefixConfig = getTaskPrefixConfig();
|
|
1822
|
-
let prefixCounter = prefixConfig ? readPrefixCounter(dir) : 0;
|
|
1823
|
-
if (prefixConfig?.start_from && prefixCounter < prefixConfig.start_from) {
|
|
1824
|
-
prefixCounter = prefixConfig.start_from - 1;
|
|
1825
|
-
}
|
|
1826
|
-
for (const task of tasks) {
|
|
1827
|
-
try {
|
|
1828
|
-
const existing = existingByTodosId.get(task.id);
|
|
1829
|
-
if (existing) {
|
|
1830
|
-
const lastSyncedAt = parseTimestamp(existing.task.metadata?.["todos_updated_at"]);
|
|
1831
|
-
const localUpdatedAt = parseTimestamp(task.updated_at);
|
|
1832
|
-
const remoteUpdatedAt = existing.mtimeMs;
|
|
1833
|
-
let recordConflict = false;
|
|
1834
|
-
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
1835
|
-
if (prefer === "remote") {
|
|
1836
|
-
const conflict = {
|
|
1837
|
-
agent: "claude",
|
|
1838
|
-
direction: "push",
|
|
1839
|
-
prefer,
|
|
1840
|
-
local_updated_at: task.updated_at,
|
|
1841
|
-
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
1842
|
-
detected_at: new Date().toISOString()
|
|
1843
|
-
};
|
|
1844
|
-
const newMeta = appendSyncConflict(task.metadata, conflict);
|
|
1845
|
-
updateTask(task.id, { version: task.version, metadata: newMeta });
|
|
1846
|
-
errors.push(`conflict push ${task.id}: remote newer`);
|
|
1847
|
-
continue;
|
|
1848
|
-
}
|
|
1849
|
-
recordConflict = true;
|
|
1850
|
-
}
|
|
1851
|
-
const updated = taskToClaudeTask(task, existing.task.id, existing.task.metadata);
|
|
1852
|
-
updated.blocks = existing.task.blocks;
|
|
1853
|
-
updated.blockedBy = existing.task.blockedBy;
|
|
1854
|
-
updated.activeForm = existing.task.activeForm;
|
|
1855
|
-
writeClaudeTask(dir, updated);
|
|
1856
|
-
if (recordConflict) {
|
|
1857
|
-
const latest = getTask(task.id);
|
|
1858
|
-
if (latest) {
|
|
1859
|
-
const conflict = {
|
|
1860
|
-
agent: "claude",
|
|
1861
|
-
direction: "push",
|
|
1862
|
-
prefer,
|
|
1863
|
-
local_updated_at: latest.updated_at,
|
|
1864
|
-
remote_updated_at: remoteUpdatedAt ? new Date(remoteUpdatedAt).toISOString() : undefined,
|
|
1865
|
-
detected_at: new Date().toISOString()
|
|
1866
|
-
};
|
|
1867
|
-
const newMeta = appendSyncConflict(latest.metadata, conflict);
|
|
1868
|
-
updateTask(latest.id, { version: latest.version, metadata: newMeta });
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
} else {
|
|
1872
|
-
const claudeId = String(hwm);
|
|
1873
|
-
hwm++;
|
|
1874
|
-
const ct = taskToClaudeTask(task, claudeId);
|
|
1875
|
-
if (prefixConfig) {
|
|
1876
|
-
prefixCounter++;
|
|
1877
|
-
ct.subject = formatPrefixedSubject(task.title, prefixConfig.prefix, prefixCounter);
|
|
1878
|
-
}
|
|
1879
|
-
writeClaudeTask(dir, ct);
|
|
1880
|
-
const current = getTask(task.id);
|
|
1881
|
-
if (current) {
|
|
1882
|
-
const newMeta = { ...current.metadata, claude_task_id: claudeId };
|
|
1883
|
-
updateTask(task.id, { version: current.version, metadata: newMeta });
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
pushed++;
|
|
1887
|
-
} catch (e) {
|
|
1888
|
-
errors.push(`push ${task.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
writeHighWaterMark(dir, hwm);
|
|
1892
|
-
if (prefixConfig)
|
|
1893
|
-
writePrefixCounter(dir, prefixCounter);
|
|
1894
|
-
return { pushed, pulled: 0, errors };
|
|
1895
|
-
}
|
|
1896
|
-
function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
|
|
1897
|
-
const dir = getTaskListDir(taskListId);
|
|
1898
|
-
if (!existsSync4(dir)) {
|
|
1899
|
-
return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
|
|
1900
|
-
}
|
|
1901
|
-
const files = readdirSync2(dir).filter((f) => f.endsWith(".json"));
|
|
1902
|
-
let pulled = 0;
|
|
1903
|
-
const errors = [];
|
|
1904
|
-
const prefer = options.prefer || "remote";
|
|
1905
|
-
const allTasks = listTasks({});
|
|
1906
|
-
const byClaudeId = new Map;
|
|
1907
|
-
for (const t of allTasks) {
|
|
1908
|
-
const cid = t.metadata["claude_task_id"];
|
|
1909
|
-
if (cid)
|
|
1910
|
-
byClaudeId.set(String(cid), t);
|
|
1911
|
-
}
|
|
1912
|
-
const byTodosId = new Map;
|
|
1913
|
-
for (const t of allTasks) {
|
|
1914
|
-
byTodosId.set(t.id, t);
|
|
1915
|
-
}
|
|
1916
|
-
for (const f of files) {
|
|
1917
|
-
try {
|
|
1918
|
-
const filePath = join4(dir, f);
|
|
1919
|
-
const ct = readClaudeTask(dir, f);
|
|
1920
|
-
if (!ct)
|
|
1921
|
-
continue;
|
|
1922
|
-
if (ct.metadata?.["_internal"])
|
|
1923
|
-
continue;
|
|
1924
|
-
const todosId = ct.metadata?.["todos_id"];
|
|
1925
|
-
const existingByMapping = byClaudeId.get(ct.id);
|
|
1926
|
-
const existingByTodos = todosId ? byTodosId.get(todosId) : undefined;
|
|
1927
|
-
const existing = existingByMapping || existingByTodos;
|
|
1928
|
-
if (existing) {
|
|
1929
|
-
const lastSyncedAt = parseTimestamp(ct.metadata?.["todos_updated_at"]);
|
|
1930
|
-
const localUpdatedAt = parseTimestamp(existing.updated_at);
|
|
1931
|
-
const remoteUpdatedAt = getFileMtimeMs(filePath);
|
|
1932
|
-
let conflictMeta = null;
|
|
1933
|
-
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
1934
|
-
const conflict = {
|
|
1935
|
-
agent: "claude",
|
|
1936
|
-
direction: "pull",
|
|
1937
|
-
prefer,
|
|
1938
|
-
local_updated_at: existing.updated_at,
|
|
1939
|
-
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
1940
|
-
detected_at: new Date().toISOString()
|
|
1941
|
-
};
|
|
1942
|
-
conflictMeta = appendSyncConflict(existing.metadata, conflict);
|
|
1943
|
-
if (prefer === "local") {
|
|
1944
|
-
updateTask(existing.id, { version: existing.version, metadata: conflictMeta });
|
|
1945
|
-
errors.push(`conflict pull ${existing.id}: local newer`);
|
|
1946
|
-
continue;
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
updateTask(existing.id, {
|
|
1950
|
-
version: existing.version,
|
|
1951
|
-
title: ct.subject,
|
|
1952
|
-
description: ct.description || undefined,
|
|
1953
|
-
status: toSqliteStatus(ct.status),
|
|
1954
|
-
assigned_to: ct.owner || undefined,
|
|
1955
|
-
metadata: { ...conflictMeta || existing.metadata, claude_task_id: ct.id, ...ct.metadata }
|
|
1956
|
-
});
|
|
1957
|
-
} else {
|
|
1958
|
-
createTask({
|
|
1959
|
-
title: ct.subject,
|
|
1960
|
-
description: ct.description || undefined,
|
|
1961
|
-
status: toSqliteStatus(ct.status),
|
|
1962
|
-
assigned_to: ct.owner || undefined,
|
|
1963
|
-
project_id: projectId,
|
|
1964
|
-
metadata: { ...ct.metadata, claude_task_id: ct.id },
|
|
1965
|
-
priority: ct.metadata?.["priority"] || "medium"
|
|
1966
|
-
});
|
|
1967
|
-
}
|
|
1968
|
-
pulled++;
|
|
1969
|
-
} catch (e) {
|
|
1970
|
-
errors.push(`pull ${f}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
return { pushed: 0, pulled, errors };
|
|
1974
|
-
}
|
|
1975
|
-
function syncClaudeTaskList(taskListId, projectId, options = {}) {
|
|
1976
|
-
const pullResult = pullFromClaudeTaskList(taskListId, projectId, options);
|
|
1977
|
-
const pushResult = pushToClaudeTaskList(taskListId, projectId, options);
|
|
1978
|
-
return {
|
|
1979
|
-
pushed: pushResult.pushed,
|
|
1980
|
-
pulled: pullResult.pulled,
|
|
1981
|
-
errors: [...pullResult.errors, ...pushResult.errors]
|
|
1982
|
-
};
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
// src/lib/agent-tasks.ts
|
|
1986
|
-
import { existsSync as existsSync5 } from "fs";
|
|
1987
|
-
import { join as join5 } from "path";
|
|
1988
|
-
function agentBaseDir(agent) {
|
|
1989
|
-
const key = `TODOS_${agent.toUpperCase()}_TASKS_DIR`;
|
|
1990
|
-
return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join5(HOME, ".todos", "agents");
|
|
1991
|
-
}
|
|
1992
|
-
function getTaskListDir2(agent, taskListId) {
|
|
1993
|
-
return join5(agentBaseDir(agent), agent, taskListId);
|
|
1994
|
-
}
|
|
1995
|
-
function readAgentTask(dir, filename) {
|
|
1996
|
-
return readJsonFile(join5(dir, filename));
|
|
1997
|
-
}
|
|
1998
|
-
function writeAgentTask(dir, task) {
|
|
1999
|
-
writeJsonFile(join5(dir, `${task.id}.json`), task);
|
|
2000
|
-
}
|
|
2001
|
-
function taskToAgentTask(task, externalId, existingMeta) {
|
|
2002
|
-
return {
|
|
2003
|
-
id: externalId,
|
|
2004
|
-
title: task.title,
|
|
2005
|
-
description: task.description || "",
|
|
2006
|
-
status: task.status,
|
|
2007
|
-
priority: task.priority,
|
|
2008
|
-
assigned_to: task.assigned_to || task.agent_id || "",
|
|
2009
|
-
tags: task.tags || [],
|
|
2010
|
-
metadata: {
|
|
2011
|
-
...existingMeta || {},
|
|
2012
|
-
...task.metadata,
|
|
2013
|
-
todos_id: task.id,
|
|
2014
|
-
todos_updated_at: task.updated_at,
|
|
2015
|
-
todos_version: task.version
|
|
2016
|
-
}
|
|
2017
|
-
};
|
|
2018
|
-
}
|
|
2019
|
-
function metadataKey(agent) {
|
|
2020
|
-
return `${agent}_task_id`;
|
|
2021
|
-
}
|
|
2022
|
-
function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
2023
|
-
const dir = getTaskListDir2(agent, taskListId);
|
|
2024
|
-
if (!existsSync5(dir))
|
|
2025
|
-
ensureDir2(dir);
|
|
2026
|
-
const filter = {};
|
|
2027
|
-
if (projectId)
|
|
2028
|
-
filter["project_id"] = projectId;
|
|
2029
|
-
const tasks = listTasks(filter);
|
|
2030
|
-
const existingByTodosId = new Map;
|
|
2031
|
-
const files = listJsonFiles(dir);
|
|
2032
|
-
for (const f of files) {
|
|
2033
|
-
const path = join5(dir, f);
|
|
2034
|
-
const at = readAgentTask(dir, f);
|
|
2035
|
-
if (at?.metadata?.["todos_id"]) {
|
|
2036
|
-
existingByTodosId.set(at.metadata["todos_id"], { task: at, mtimeMs: getFileMtimeMs(path) });
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
let hwm = readHighWaterMark(dir);
|
|
2040
|
-
let pushed = 0;
|
|
2041
|
-
const errors = [];
|
|
2042
|
-
const metaKey = metadataKey(agent);
|
|
2043
|
-
const prefer = options.prefer || "remote";
|
|
2044
|
-
for (const task of tasks) {
|
|
2045
|
-
try {
|
|
2046
|
-
const existing = existingByTodosId.get(task.id);
|
|
2047
|
-
if (existing) {
|
|
2048
|
-
const lastSyncedAt = parseTimestamp(existing.task.metadata?.["todos_updated_at"]);
|
|
2049
|
-
const localUpdatedAt = parseTimestamp(task.updated_at);
|
|
2050
|
-
const remoteUpdatedAt = existing.mtimeMs;
|
|
2051
|
-
let recordConflict = false;
|
|
2052
|
-
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
2053
|
-
if (prefer === "remote") {
|
|
2054
|
-
const conflict = {
|
|
2055
|
-
agent,
|
|
2056
|
-
direction: "push",
|
|
2057
|
-
prefer,
|
|
2058
|
-
local_updated_at: task.updated_at,
|
|
2059
|
-
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
2060
|
-
detected_at: new Date().toISOString()
|
|
2061
|
-
};
|
|
2062
|
-
const newMeta = appendSyncConflict(task.metadata, conflict);
|
|
2063
|
-
updateTask(task.id, { version: task.version, metadata: newMeta });
|
|
2064
|
-
errors.push(`conflict push ${task.id}: remote newer`);
|
|
2065
|
-
continue;
|
|
2066
|
-
}
|
|
2067
|
-
recordConflict = true;
|
|
2068
|
-
}
|
|
2069
|
-
const updated = taskToAgentTask(task, existing.task.id, existing.task.metadata);
|
|
2070
|
-
writeAgentTask(dir, updated);
|
|
2071
|
-
if (recordConflict) {
|
|
2072
|
-
const latest = getTask(task.id);
|
|
2073
|
-
if (latest) {
|
|
2074
|
-
const conflict = {
|
|
2075
|
-
agent,
|
|
2076
|
-
direction: "push",
|
|
2077
|
-
prefer,
|
|
2078
|
-
local_updated_at: latest.updated_at,
|
|
2079
|
-
remote_updated_at: remoteUpdatedAt ? new Date(remoteUpdatedAt).toISOString() : undefined,
|
|
2080
|
-
detected_at: new Date().toISOString()
|
|
2081
|
-
};
|
|
2082
|
-
const newMeta = appendSyncConflict(latest.metadata, conflict);
|
|
2083
|
-
updateTask(latest.id, { version: latest.version, metadata: newMeta });
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
} else {
|
|
2087
|
-
const externalId = String(hwm);
|
|
2088
|
-
hwm++;
|
|
2089
|
-
const at = taskToAgentTask(task, externalId);
|
|
2090
|
-
writeAgentTask(dir, at);
|
|
2091
|
-
const current = getTask(task.id);
|
|
2092
|
-
if (current) {
|
|
2093
|
-
const newMeta = { ...current.metadata, [metaKey]: externalId };
|
|
2094
|
-
updateTask(task.id, { version: current.version, metadata: newMeta });
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
pushed++;
|
|
2098
|
-
} catch (e) {
|
|
2099
|
-
errors.push(`push ${task.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
writeHighWaterMark(dir, hwm);
|
|
2103
|
-
return { pushed, pulled: 0, errors };
|
|
2104
|
-
}
|
|
2105
|
-
function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
2106
|
-
const dir = getTaskListDir2(agent, taskListId);
|
|
2107
|
-
if (!existsSync5(dir)) {
|
|
2108
|
-
return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
|
|
2109
|
-
}
|
|
2110
|
-
const files = listJsonFiles(dir);
|
|
2111
|
-
let pulled = 0;
|
|
2112
|
-
const errors = [];
|
|
2113
|
-
const metaKey = metadataKey(agent);
|
|
2114
|
-
const prefer = options.prefer || "remote";
|
|
2115
|
-
const allTasks = listTasks({});
|
|
2116
|
-
const byExternalId = new Map;
|
|
2117
|
-
const byTodosId = new Map;
|
|
2118
|
-
for (const t of allTasks) {
|
|
2119
|
-
const extId = t.metadata[metaKey];
|
|
2120
|
-
if (extId)
|
|
2121
|
-
byExternalId.set(String(extId), t);
|
|
2122
|
-
byTodosId.set(t.id, t);
|
|
2123
|
-
}
|
|
2124
|
-
for (const f of files) {
|
|
2125
|
-
try {
|
|
2126
|
-
const filePath = join5(dir, f);
|
|
2127
|
-
const at = readAgentTask(dir, f);
|
|
2128
|
-
if (!at)
|
|
2129
|
-
continue;
|
|
2130
|
-
if (at.metadata?.["_internal"])
|
|
2131
|
-
continue;
|
|
2132
|
-
const todosId = at.metadata?.["todos_id"];
|
|
2133
|
-
const existingByMapping = byExternalId.get(at.id);
|
|
2134
|
-
const existingByTodos = todosId ? byTodosId.get(todosId) : undefined;
|
|
2135
|
-
const existing = existingByMapping || existingByTodos;
|
|
2136
|
-
if (existing) {
|
|
2137
|
-
const lastSyncedAt = parseTimestamp(at.metadata?.["todos_updated_at"]);
|
|
2138
|
-
const localUpdatedAt = parseTimestamp(existing.updated_at);
|
|
2139
|
-
const remoteUpdatedAt = getFileMtimeMs(filePath);
|
|
2140
|
-
let conflictMeta = null;
|
|
2141
|
-
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
2142
|
-
const conflict = {
|
|
2143
|
-
agent,
|
|
2144
|
-
direction: "pull",
|
|
2145
|
-
prefer,
|
|
2146
|
-
local_updated_at: existing.updated_at,
|
|
2147
|
-
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
2148
|
-
detected_at: new Date().toISOString()
|
|
2149
|
-
};
|
|
2150
|
-
conflictMeta = appendSyncConflict(existing.metadata, conflict);
|
|
2151
|
-
if (prefer === "local") {
|
|
2152
|
-
updateTask(existing.id, { version: existing.version, metadata: conflictMeta });
|
|
2153
|
-
errors.push(`conflict pull ${existing.id}: local newer`);
|
|
2154
|
-
continue;
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
updateTask(existing.id, {
|
|
2158
|
-
version: existing.version,
|
|
2159
|
-
title: at.title,
|
|
2160
|
-
description: at.description || undefined,
|
|
2161
|
-
status: at.status,
|
|
2162
|
-
priority: at.priority,
|
|
2163
|
-
assigned_to: at.assigned_to || undefined,
|
|
2164
|
-
tags: at.tags || [],
|
|
2165
|
-
metadata: { ...conflictMeta || existing.metadata, ...at.metadata, [metaKey]: at.id }
|
|
2166
|
-
});
|
|
2167
|
-
} else {
|
|
2168
|
-
createTask({
|
|
2169
|
-
title: at.title,
|
|
2170
|
-
description: at.description || undefined,
|
|
2171
|
-
status: at.status,
|
|
2172
|
-
priority: at.priority || "medium",
|
|
2173
|
-
assigned_to: at.assigned_to || undefined,
|
|
2174
|
-
tags: at.tags || [],
|
|
2175
|
-
project_id: projectId,
|
|
2176
|
-
metadata: { ...at.metadata, [metaKey]: at.id }
|
|
2177
|
-
});
|
|
2178
|
-
}
|
|
2179
|
-
pulled++;
|
|
2180
|
-
} catch (e) {
|
|
2181
|
-
errors.push(`pull ${f}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
return { pushed: 0, pulled, errors };
|
|
2185
|
-
}
|
|
2186
|
-
function syncAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
2187
|
-
const pullResult = pullFromAgentTaskList(agent, taskListId, projectId, options);
|
|
2188
|
-
const pushResult = pushToAgentTaskList(agent, taskListId, projectId, options);
|
|
2189
|
-
return {
|
|
2190
|
-
pushed: pushResult.pushed,
|
|
2191
|
-
pulled: pullResult.pulled,
|
|
2192
|
-
errors: [...pullResult.errors, ...pushResult.errors]
|
|
2193
|
-
};
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
// src/lib/sync.ts
|
|
2197
|
-
function normalizeAgent2(agent) {
|
|
2198
|
-
return agent.trim().toLowerCase();
|
|
2199
|
-
}
|
|
2200
|
-
function isClaudeAgent(agent) {
|
|
2201
|
-
const a = normalizeAgent2(agent);
|
|
2202
|
-
return a === "claude" || a === "claude-code" || a === "claude_code";
|
|
2203
|
-
}
|
|
2204
|
-
function defaultSyncAgents() {
|
|
2205
|
-
const env = process.env["TODOS_SYNC_AGENTS"];
|
|
2206
|
-
if (env) {
|
|
2207
|
-
return env.split(",").map((a) => a.trim()).filter(Boolean);
|
|
2208
|
-
}
|
|
2209
|
-
const fromConfig = getSyncAgentsFromConfig();
|
|
2210
|
-
if (fromConfig && fromConfig.length > 0)
|
|
2211
|
-
return fromConfig;
|
|
2212
|
-
return ["claude", "codex", "gemini"];
|
|
2213
|
-
}
|
|
2214
|
-
function syncWithAgent(agent, taskListId, projectId, direction = "both", options = {}) {
|
|
2215
|
-
const normalized = normalizeAgent2(agent);
|
|
2216
|
-
if (isClaudeAgent(normalized)) {
|
|
2217
|
-
if (direction === "push")
|
|
2218
|
-
return pushToClaudeTaskList(taskListId, projectId, options);
|
|
2219
|
-
if (direction === "pull")
|
|
2220
|
-
return pullFromClaudeTaskList(taskListId, projectId, options);
|
|
2221
|
-
return syncClaudeTaskList(taskListId, projectId, options);
|
|
2222
|
-
}
|
|
2223
|
-
if (direction === "push")
|
|
2224
|
-
return pushToAgentTaskList(normalized, taskListId, projectId, options);
|
|
2225
|
-
if (direction === "pull")
|
|
2226
|
-
return pullFromAgentTaskList(normalized, taskListId, projectId, options);
|
|
2227
|
-
return syncAgentTaskList(normalized, taskListId, projectId, options);
|
|
2228
|
-
}
|
|
2229
|
-
function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both", options = {}) {
|
|
2230
|
-
let pushed = 0;
|
|
2231
|
-
let pulled = 0;
|
|
2232
|
-
const errors = [];
|
|
2233
|
-
const normalized = agents.map(normalizeAgent2);
|
|
2234
|
-
if (direction === "pull" || direction === "both") {
|
|
2235
|
-
for (const agent of normalized) {
|
|
2236
|
-
const listId = taskListIdByAgent(agent);
|
|
2237
|
-
if (!listId) {
|
|
2238
|
-
errors.push(`sync ${agent}: missing task list id`);
|
|
2239
|
-
continue;
|
|
2240
|
-
}
|
|
2241
|
-
const result = syncWithAgent(agent, listId, projectId, "pull", options);
|
|
2242
|
-
pushed += result.pushed;
|
|
2243
|
-
pulled += result.pulled;
|
|
2244
|
-
errors.push(...result.errors.map((e) => `${agent}: ${e}`));
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
if (direction === "push" || direction === "both") {
|
|
2248
|
-
for (const agent of normalized) {
|
|
2249
|
-
const listId = taskListIdByAgent(agent);
|
|
2250
|
-
if (!listId) {
|
|
2251
|
-
errors.push(`sync ${agent}: missing task list id`);
|
|
2252
|
-
continue;
|
|
2253
|
-
}
|
|
2254
|
-
const result = syncWithAgent(agent, listId, projectId, "push", options);
|
|
2255
|
-
pushed += result.pushed;
|
|
2256
|
-
pulled += result.pulled;
|
|
2257
|
-
errors.push(...result.errors.map((e) => `${agent}: ${e}`));
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
return { pushed, pulled, errors };
|
|
2261
|
-
}
|
|
2262
|
-
export {
|
|
2263
|
-
uuid,
|
|
2264
|
-
updateTaskList,
|
|
2265
|
-
updateTask,
|
|
2266
|
-
updateSessionActivity,
|
|
2267
|
-
updateProject,
|
|
2268
|
-
updatePlan,
|
|
2269
|
-
updateAgentActivity,
|
|
2270
|
-
updateAgent,
|
|
2271
|
-
unlockTask,
|
|
2272
|
-
taskFromTemplate,
|
|
2273
|
-
syncWithAgents,
|
|
2274
|
-
syncWithAgent,
|
|
2275
|
-
startTask,
|
|
2276
|
-
slugify,
|
|
2277
|
-
searchTasks,
|
|
2278
|
-
resolvePartialId,
|
|
2279
|
-
resetDatabase,
|
|
2280
|
-
removeDependency,
|
|
2281
|
-
registerAgent,
|
|
2282
|
-
now,
|
|
2283
|
-
nextTaskShortId,
|
|
2284
|
-
logTaskChange,
|
|
2285
|
-
lockTask,
|
|
2286
|
-
loadConfig,
|
|
2287
|
-
listWebhooks,
|
|
2288
|
-
listTemplates,
|
|
2289
|
-
listTasks,
|
|
2290
|
-
listTaskLists,
|
|
2291
|
-
listSessions,
|
|
2292
|
-
listProjects,
|
|
2293
|
-
listPlans,
|
|
2294
|
-
listComments,
|
|
2295
|
-
listAgents,
|
|
2296
|
-
getWebhook,
|
|
2297
|
-
getTemplate,
|
|
2298
|
-
getTaskWithRelations,
|
|
2299
|
-
getTaskListBySlug,
|
|
2300
|
-
getTaskList,
|
|
2301
|
-
getTaskHistory,
|
|
2302
|
-
getTaskDependents,
|
|
2303
|
-
getTaskDependencies,
|
|
2304
|
-
getTask,
|
|
2305
|
-
getSession,
|
|
2306
|
-
getRecentActivity,
|
|
2307
|
-
getProjectByPath,
|
|
2308
|
-
getProject,
|
|
2309
|
-
getPlan,
|
|
2310
|
-
getOrgChart,
|
|
2311
|
-
getDirectReports,
|
|
2312
|
-
getDatabase,
|
|
2313
|
-
getCompletionGuardConfig,
|
|
2314
|
-
getComment,
|
|
2315
|
-
getBlockingDeps,
|
|
2316
|
-
getAgentByName,
|
|
2317
|
-
getAgent,
|
|
2318
|
-
ensureTaskList,
|
|
2319
|
-
ensureProject,
|
|
2320
|
-
dispatchWebhook,
|
|
2321
|
-
deleteWebhook,
|
|
2322
|
-
deleteTemplate,
|
|
2323
|
-
deleteTaskList,
|
|
2324
|
-
deleteTask,
|
|
2325
|
-
deleteSession,
|
|
2326
|
-
deleteProject,
|
|
2327
|
-
deletePlan,
|
|
2328
|
-
deleteComment,
|
|
2329
|
-
deleteAgent,
|
|
2330
|
-
defaultSyncAgents,
|
|
2331
|
-
createWebhook,
|
|
2332
|
-
createTemplate,
|
|
2333
|
-
createTaskList,
|
|
2334
|
-
createTask,
|
|
2335
|
-
createSession,
|
|
2336
|
-
createProject,
|
|
2337
|
-
createPlan,
|
|
2338
|
-
completeTask,
|
|
2339
|
-
closeDatabase,
|
|
2340
|
-
checkCompletionGuard,
|
|
2341
|
-
addDependency,
|
|
2342
|
-
addComment,
|
|
2343
|
-
VersionConflictError,
|
|
2344
|
-
TaskNotFoundError,
|
|
2345
|
-
TaskListNotFoundError,
|
|
2346
|
-
TASK_STATUSES,
|
|
2347
|
-
TASK_PRIORITIES,
|
|
2348
|
-
ProjectNotFoundError,
|
|
2349
|
-
PlanNotFoundError,
|
|
2350
|
-
PLAN_STATUSES,
|
|
2351
|
-
LockError,
|
|
2352
|
-
DependencyCycleError,
|
|
2353
|
-
CompletionGuardError,
|
|
2354
|
-
AgentNotFoundError
|
|
2355
|
-
};
|