@hasna/todos 0.9.33 → 0.9.35

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