@hasna/todos 0.9.7 → 0.9.9

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.
@@ -0,0 +1,1285 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true,
9
+ configurable: true,
10
+ set: (newValue) => all[name] = () => newValue
11
+ });
12
+ };
13
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
+ var __require = import.meta.require;
15
+
16
+ // src/types/index.ts
17
+ var VersionConflictError, TaskNotFoundError, ProjectNotFoundError, LockError, CompletionGuardError;
18
+ var init_types = __esm(() => {
19
+ VersionConflictError = class VersionConflictError extends Error {
20
+ taskId;
21
+ expectedVersion;
22
+ actualVersion;
23
+ constructor(taskId, expectedVersion, actualVersion) {
24
+ super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
25
+ this.taskId = taskId;
26
+ this.expectedVersion = expectedVersion;
27
+ this.actualVersion = actualVersion;
28
+ this.name = "VersionConflictError";
29
+ }
30
+ };
31
+ TaskNotFoundError = class TaskNotFoundError extends Error {
32
+ taskId;
33
+ constructor(taskId) {
34
+ super(`Task not found: ${taskId}`);
35
+ this.taskId = taskId;
36
+ this.name = "TaskNotFoundError";
37
+ }
38
+ };
39
+ ProjectNotFoundError = class ProjectNotFoundError extends Error {
40
+ projectId;
41
+ constructor(projectId) {
42
+ super(`Project not found: ${projectId}`);
43
+ this.projectId = projectId;
44
+ this.name = "ProjectNotFoundError";
45
+ }
46
+ };
47
+ LockError = class LockError extends Error {
48
+ taskId;
49
+ lockedBy;
50
+ constructor(taskId, lockedBy) {
51
+ super(`Task ${taskId} is locked by ${lockedBy}`);
52
+ this.taskId = taskId;
53
+ this.lockedBy = lockedBy;
54
+ this.name = "LockError";
55
+ }
56
+ };
57
+ CompletionGuardError = class CompletionGuardError extends Error {
58
+ reason;
59
+ retryAfterSeconds;
60
+ constructor(reason, retryAfterSeconds) {
61
+ super(reason);
62
+ this.reason = reason;
63
+ this.retryAfterSeconds = retryAfterSeconds;
64
+ this.name = "CompletionGuardError";
65
+ }
66
+ };
67
+ });
68
+
69
+ // src/db/database.ts
70
+ import { Database } from "bun:sqlite";
71
+ import { existsSync, mkdirSync } from "fs";
72
+ import { dirname, join, resolve } from "path";
73
+ function isInMemoryDb(path) {
74
+ return path === ":memory:" || path.startsWith("file::memory:");
75
+ }
76
+ function findNearestTodosDb(startDir) {
77
+ let dir = resolve(startDir);
78
+ while (true) {
79
+ const candidate = join(dir, ".todos", "todos.db");
80
+ if (existsSync(candidate))
81
+ return candidate;
82
+ const parent = dirname(dir);
83
+ if (parent === dir)
84
+ break;
85
+ dir = parent;
86
+ }
87
+ return null;
88
+ }
89
+ function findGitRoot(startDir) {
90
+ let dir = resolve(startDir);
91
+ while (true) {
92
+ if (existsSync(join(dir, ".git")))
93
+ return dir;
94
+ const parent = dirname(dir);
95
+ if (parent === dir)
96
+ break;
97
+ dir = parent;
98
+ }
99
+ return null;
100
+ }
101
+ function getDbPath() {
102
+ if (process.env["TODOS_DB_PATH"]) {
103
+ return process.env["TODOS_DB_PATH"];
104
+ }
105
+ const cwd = process.cwd();
106
+ const nearest = findNearestTodosDb(cwd);
107
+ if (nearest)
108
+ return nearest;
109
+ if (process.env["TODOS_DB_SCOPE"] === "project") {
110
+ const gitRoot = findGitRoot(cwd);
111
+ if (gitRoot) {
112
+ return join(gitRoot, ".todos", "todos.db");
113
+ }
114
+ }
115
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
116
+ return join(home, ".todos", "todos.db");
117
+ }
118
+ function ensureDir(filePath) {
119
+ if (isInMemoryDb(filePath))
120
+ return;
121
+ const dir = dirname(resolve(filePath));
122
+ if (!existsSync(dir)) {
123
+ mkdirSync(dir, { recursive: true });
124
+ }
125
+ }
126
+ function getDatabase(dbPath) {
127
+ if (_db)
128
+ return _db;
129
+ const path = dbPath || getDbPath();
130
+ ensureDir(path);
131
+ _db = new Database(path, { create: true });
132
+ _db.run("PRAGMA journal_mode = WAL");
133
+ _db.run("PRAGMA busy_timeout = 5000");
134
+ _db.run("PRAGMA foreign_keys = ON");
135
+ runMigrations(_db);
136
+ backfillTaskTags(_db);
137
+ return _db;
138
+ }
139
+ function runMigrations(db) {
140
+ try {
141
+ const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
142
+ const currentLevel = result?.max_id ?? 0;
143
+ for (let i = currentLevel;i < MIGRATIONS.length; i++) {
144
+ db.exec(MIGRATIONS[i]);
145
+ }
146
+ } catch {
147
+ for (const migration of MIGRATIONS) {
148
+ db.exec(migration);
149
+ }
150
+ }
151
+ ensureTableMigrations(db);
152
+ }
153
+ function ensureTableMigrations(db) {
154
+ try {
155
+ const hasAgents = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='agents'").get();
156
+ if (!hasAgents) {
157
+ db.exec(MIGRATIONS[4]);
158
+ }
159
+ } catch {}
160
+ try {
161
+ db.query("SELECT task_prefix FROM projects LIMIT 0").get();
162
+ } catch {
163
+ try {
164
+ db.exec(MIGRATIONS[5]);
165
+ } catch {}
166
+ }
167
+ }
168
+ function backfillTaskTags(db) {
169
+ try {
170
+ const count = db.query("SELECT COUNT(*) as count FROM task_tags").get();
171
+ if (count && count.count > 0)
172
+ return;
173
+ } catch {
174
+ return;
175
+ }
176
+ try {
177
+ const rows = db.query("SELECT id, tags FROM tasks WHERE tags IS NOT NULL AND tags != '[]'").all();
178
+ if (rows.length === 0)
179
+ return;
180
+ const insert = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
181
+ for (const row of rows) {
182
+ if (!row.tags)
183
+ continue;
184
+ let tags = [];
185
+ try {
186
+ tags = JSON.parse(row.tags);
187
+ } catch {
188
+ continue;
189
+ }
190
+ for (const tag of tags) {
191
+ if (tag)
192
+ insert.run(row.id, tag);
193
+ }
194
+ }
195
+ } catch {}
196
+ }
197
+ function now() {
198
+ return new Date().toISOString();
199
+ }
200
+ function uuid() {
201
+ return crypto.randomUUID();
202
+ }
203
+ function isLockExpired(lockedAt) {
204
+ if (!lockedAt)
205
+ return true;
206
+ const lockTime = new Date(lockedAt).getTime();
207
+ const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
208
+ return Date.now() - lockTime > expiryMs;
209
+ }
210
+ function lockExpiryCutoff(nowMs = Date.now()) {
211
+ const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
212
+ return new Date(nowMs - expiryMs).toISOString();
213
+ }
214
+ function clearExpiredLocks(db) {
215
+ const cutoff = lockExpiryCutoff();
216
+ db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
217
+ }
218
+ var LOCK_EXPIRY_MINUTES = 30, MIGRATIONS, _db = null;
219
+ var init_database = __esm(() => {
220
+ MIGRATIONS = [
221
+ `
222
+ CREATE TABLE IF NOT EXISTS projects (
223
+ id TEXT PRIMARY KEY,
224
+ name TEXT NOT NULL,
225
+ path TEXT UNIQUE NOT NULL,
226
+ description TEXT,
227
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
228
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
229
+ );
230
+
231
+ CREATE TABLE IF NOT EXISTS tasks (
232
+ id TEXT PRIMARY KEY,
233
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
234
+ parent_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
235
+ title TEXT NOT NULL,
236
+ description TEXT,
237
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'failed', 'cancelled')),
238
+ priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high', 'critical')),
239
+ agent_id TEXT,
240
+ assigned_to TEXT,
241
+ session_id TEXT,
242
+ working_dir TEXT,
243
+ tags TEXT DEFAULT '[]',
244
+ metadata TEXT DEFAULT '{}',
245
+ version INTEGER NOT NULL DEFAULT 1,
246
+ locked_by TEXT,
247
+ locked_at TEXT,
248
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
249
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
250
+ completed_at TEXT
251
+ );
252
+
253
+ CREATE TABLE IF NOT EXISTS task_dependencies (
254
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
255
+ depends_on TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
256
+ PRIMARY KEY (task_id, depends_on),
257
+ CHECK (task_id != depends_on)
258
+ );
259
+
260
+ CREATE TABLE IF NOT EXISTS task_comments (
261
+ id TEXT PRIMARY KEY,
262
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
263
+ agent_id TEXT,
264
+ session_id TEXT,
265
+ content TEXT NOT NULL,
266
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
267
+ );
268
+
269
+ CREATE TABLE IF NOT EXISTS sessions (
270
+ id TEXT PRIMARY KEY,
271
+ agent_id TEXT,
272
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
273
+ working_dir TEXT,
274
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
275
+ last_activity TEXT NOT NULL DEFAULT (datetime('now')),
276
+ metadata TEXT DEFAULT '{}'
277
+ );
278
+
279
+ CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
280
+ CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id);
281
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
282
+ CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
283
+ CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to);
284
+ CREATE INDEX IF NOT EXISTS idx_tasks_agent ON tasks(agent_id);
285
+ CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
286
+ CREATE INDEX IF NOT EXISTS idx_comments_task ON task_comments(task_id);
287
+ CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
288
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
289
+
290
+ CREATE TABLE IF NOT EXISTS _migrations (
291
+ id INTEGER PRIMARY KEY,
292
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
293
+ );
294
+
295
+ INSERT OR IGNORE INTO _migrations (id) VALUES (1);
296
+ `,
297
+ `
298
+ ALTER TABLE projects ADD COLUMN task_list_id TEXT;
299
+ INSERT OR IGNORE INTO _migrations (id) VALUES (2);
300
+ `,
301
+ `
302
+ CREATE TABLE IF NOT EXISTS task_tags (
303
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
304
+ tag TEXT NOT NULL,
305
+ PRIMARY KEY (task_id, tag)
306
+ );
307
+ CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag);
308
+ CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id);
309
+
310
+ INSERT OR IGNORE INTO _migrations (id) VALUES (3);
311
+ `,
312
+ `
313
+ CREATE TABLE IF NOT EXISTS plans (
314
+ id TEXT PRIMARY KEY,
315
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
316
+ name TEXT NOT NULL,
317
+ description TEXT,
318
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
319
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
320
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
321
+ );
322
+ CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id);
323
+ CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status);
324
+ ALTER TABLE tasks ADD COLUMN plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL;
325
+ CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id);
326
+ INSERT OR IGNORE INTO _migrations (id) VALUES (4);
327
+ `,
328
+ `
329
+ CREATE TABLE IF NOT EXISTS agents (
330
+ id TEXT PRIMARY KEY,
331
+ name TEXT NOT NULL UNIQUE,
332
+ description TEXT,
333
+ metadata TEXT DEFAULT '{}',
334
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
335
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
336
+ );
337
+ CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name);
338
+
339
+ CREATE TABLE IF NOT EXISTS task_lists (
340
+ id TEXT PRIMARY KEY,
341
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
342
+ slug TEXT NOT NULL,
343
+ name TEXT NOT NULL,
344
+ description TEXT,
345
+ metadata TEXT DEFAULT '{}',
346
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
347
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
348
+ UNIQUE(project_id, slug)
349
+ );
350
+ CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id);
351
+ CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug);
352
+
353
+ ALTER TABLE tasks ADD COLUMN task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL;
354
+ CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id);
355
+
356
+ INSERT OR IGNORE INTO _migrations (id) VALUES (5);
357
+ `,
358
+ `
359
+ ALTER TABLE projects ADD COLUMN task_prefix TEXT;
360
+ ALTER TABLE projects ADD COLUMN task_counter INTEGER NOT NULL DEFAULT 0;
361
+
362
+ ALTER TABLE tasks ADD COLUMN short_id TEXT;
363
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL;
364
+
365
+ INSERT OR IGNORE INTO _migrations (id) VALUES (6);
366
+ `,
367
+ `
368
+ ALTER TABLE tasks ADD COLUMN due_at TEXT;
369
+ CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at);
370
+ INSERT OR IGNORE INTO _migrations (id) VALUES (7);
371
+ `
372
+ ];
373
+ });
374
+
375
+ // src/db/projects.ts
376
+ var exports_projects = {};
377
+ __export(exports_projects, {
378
+ updateProject: () => updateProject,
379
+ slugify: () => slugify,
380
+ nextTaskShortId: () => nextTaskShortId,
381
+ listProjects: () => listProjects,
382
+ getProjectByPath: () => getProjectByPath,
383
+ getProject: () => getProject,
384
+ ensureProject: () => ensureProject,
385
+ deleteProject: () => deleteProject,
386
+ createProject: () => createProject
387
+ });
388
+ function slugify(name) {
389
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
390
+ }
391
+ function generatePrefix(name, db) {
392
+ const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
393
+ let prefix;
394
+ if (words.length >= 3) {
395
+ prefix = words.slice(0, 3).map((w) => w[0].toUpperCase()).join("");
396
+ } else if (words.length === 2) {
397
+ prefix = (words[0].slice(0, 2) + words[1][0]).toUpperCase();
398
+ } else {
399
+ prefix = words[0].slice(0, 3).toUpperCase();
400
+ }
401
+ let candidate = prefix;
402
+ let suffix = 1;
403
+ while (true) {
404
+ const existing = db.query("SELECT id FROM projects WHERE task_prefix = ?").get(candidate);
405
+ if (!existing)
406
+ return candidate;
407
+ suffix++;
408
+ candidate = `${prefix}${suffix}`;
409
+ }
410
+ }
411
+ function createProject(input, db) {
412
+ const d = db || getDatabase();
413
+ const id = uuid();
414
+ const timestamp = now();
415
+ const taskListId = input.task_list_id ?? `todos-${slugify(input.name)}`;
416
+ const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
417
+ d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at)
418
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp]);
419
+ return getProject(id, d);
420
+ }
421
+ function getProject(id, db) {
422
+ const d = db || getDatabase();
423
+ const row = d.query("SELECT * FROM projects WHERE id = ?").get(id);
424
+ return row;
425
+ }
426
+ function getProjectByPath(path, db) {
427
+ const d = db || getDatabase();
428
+ const row = d.query("SELECT * FROM projects WHERE path = ?").get(path);
429
+ return row;
430
+ }
431
+ function listProjects(db) {
432
+ const d = db || getDatabase();
433
+ return d.query("SELECT * FROM projects ORDER BY name").all();
434
+ }
435
+ function updateProject(id, input, db) {
436
+ const d = db || getDatabase();
437
+ const project = getProject(id, d);
438
+ if (!project)
439
+ throw new ProjectNotFoundError(id);
440
+ const sets = ["updated_at = ?"];
441
+ const params = [now()];
442
+ if (input.name !== undefined) {
443
+ sets.push("name = ?");
444
+ params.push(input.name);
445
+ }
446
+ if (input.description !== undefined) {
447
+ sets.push("description = ?");
448
+ params.push(input.description);
449
+ }
450
+ if (input.task_list_id !== undefined) {
451
+ sets.push("task_list_id = ?");
452
+ params.push(input.task_list_id);
453
+ }
454
+ params.push(id);
455
+ d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
456
+ return getProject(id, d);
457
+ }
458
+ function deleteProject(id, db) {
459
+ const d = db || getDatabase();
460
+ const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
461
+ return result.changes > 0;
462
+ }
463
+ function nextTaskShortId(projectId, db) {
464
+ const d = db || getDatabase();
465
+ const project = getProject(projectId, d);
466
+ if (!project || !project.task_prefix)
467
+ return null;
468
+ d.run("UPDATE projects SET task_counter = task_counter + 1, updated_at = ? WHERE id = ?", [now(), projectId]);
469
+ const updated = getProject(projectId, d);
470
+ const padded = String(updated.task_counter).padStart(5, "0");
471
+ return `${updated.task_prefix}-${padded}`;
472
+ }
473
+ function ensureProject(name, path, db) {
474
+ const d = db || getDatabase();
475
+ const existing = getProjectByPath(path, d);
476
+ if (existing) {
477
+ if (!existing.task_prefix) {
478
+ const prefix = generatePrefix(existing.name, d);
479
+ d.run("UPDATE projects SET task_prefix = ?, updated_at = ? WHERE id = ?", [prefix, now(), existing.id]);
480
+ return getProject(existing.id, d);
481
+ }
482
+ return existing;
483
+ }
484
+ return createProject({ name, path }, d);
485
+ }
486
+ var init_projects = __esm(() => {
487
+ init_types();
488
+ init_database();
489
+ });
490
+
491
+ // src/db/agents.ts
492
+ var exports_agents = {};
493
+ __export(exports_agents, {
494
+ updateAgentActivity: () => updateAgentActivity,
495
+ registerAgent: () => registerAgent,
496
+ listAgents: () => listAgents,
497
+ getAgentByName: () => getAgentByName,
498
+ getAgent: () => getAgent,
499
+ deleteAgent: () => deleteAgent
500
+ });
501
+ function shortUuid() {
502
+ return crypto.randomUUID().slice(0, 8);
503
+ }
504
+ function rowToAgent(row) {
505
+ return {
506
+ ...row,
507
+ metadata: JSON.parse(row.metadata || "{}")
508
+ };
509
+ }
510
+ function registerAgent(input, db) {
511
+ const d = db || getDatabase();
512
+ const existing = getAgentByName(input.name, d);
513
+ if (existing) {
514
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), existing.id]);
515
+ return getAgent(existing.id, d);
516
+ }
517
+ const id = shortUuid();
518
+ const timestamp = now();
519
+ d.run(`INSERT INTO agents (id, name, description, metadata, created_at, last_seen_at)
520
+ VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
521
+ return getAgent(id, d);
522
+ }
523
+ function getAgent(id, db) {
524
+ const d = db || getDatabase();
525
+ const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
526
+ return row ? rowToAgent(row) : null;
527
+ }
528
+ function getAgentByName(name, db) {
529
+ const d = db || getDatabase();
530
+ const row = d.query("SELECT * FROM agents WHERE name = ?").get(name);
531
+ return row ? rowToAgent(row) : null;
532
+ }
533
+ function listAgents(db) {
534
+ const d = db || getDatabase();
535
+ return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
536
+ }
537
+ function updateAgentActivity(id, db) {
538
+ const d = db || getDatabase();
539
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
540
+ }
541
+ function deleteAgent(id, db) {
542
+ const d = db || getDatabase();
543
+ return d.run("DELETE FROM agents WHERE id = ?", [id]).changes > 0;
544
+ }
545
+ var init_agents = __esm(() => {
546
+ init_database();
547
+ });
548
+
549
+ // src/server/serve.ts
550
+ import { existsSync as existsSync4 } from "fs";
551
+ import { join as join3, dirname as dirname2, extname } from "path";
552
+ import { fileURLToPath } from "url";
553
+
554
+ // src/db/tasks.ts
555
+ init_types();
556
+ init_database();
557
+ init_projects();
558
+
559
+ // src/lib/completion-guard.ts
560
+ init_types();
561
+
562
+ // src/lib/config.ts
563
+ import { existsSync as existsSync3 } from "fs";
564
+ import { join as join2 } from "path";
565
+
566
+ // src/lib/sync-utils.ts
567
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
568
+ var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
569
+ function readJsonFile(path) {
570
+ try {
571
+ return JSON.parse(readFileSync(path, "utf-8"));
572
+ } catch {
573
+ return null;
574
+ }
575
+ }
576
+
577
+ // src/lib/config.ts
578
+ var CONFIG_PATH = join2(HOME, ".todos", "config.json");
579
+ var cached = null;
580
+ function loadConfig() {
581
+ if (cached)
582
+ return cached;
583
+ if (!existsSync3(CONFIG_PATH)) {
584
+ cached = {};
585
+ return cached;
586
+ }
587
+ const config = readJsonFile(CONFIG_PATH) || {};
588
+ if (typeof config.sync_agents === "string") {
589
+ config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
590
+ }
591
+ cached = config;
592
+ return cached;
593
+ }
594
+ var GUARD_DEFAULTS = {
595
+ enabled: false,
596
+ min_work_seconds: 30,
597
+ max_completions_per_window: 5,
598
+ window_minutes: 10,
599
+ cooldown_seconds: 60
600
+ };
601
+ function getCompletionGuardConfig(projectPath) {
602
+ const config = loadConfig();
603
+ const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
604
+ if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
605
+ return { ...global, ...config.project_overrides[projectPath].completion_guard };
606
+ }
607
+ return global;
608
+ }
609
+
610
+ // src/lib/completion-guard.ts
611
+ init_projects();
612
+ function checkCompletionGuard(task, agentId, db, configOverride) {
613
+ let config;
614
+ if (configOverride) {
615
+ config = configOverride;
616
+ } else {
617
+ const projectPath = task.project_id ? getProject(task.project_id, db)?.path : null;
618
+ config = getCompletionGuardConfig(projectPath);
619
+ }
620
+ if (!config.enabled)
621
+ return;
622
+ if (task.status !== "in_progress") {
623
+ throw new CompletionGuardError(`Task must be in 'in_progress' status before completing (current: '${task.status}'). Use start_task first.`);
624
+ }
625
+ const agent = agentId || task.assigned_to || task.agent_id;
626
+ if (config.min_work_seconds && task.locked_at) {
627
+ const startedAt = new Date(task.locked_at).getTime();
628
+ const elapsedSeconds = (Date.now() - startedAt) / 1000;
629
+ if (elapsedSeconds < config.min_work_seconds) {
630
+ const remaining = Math.ceil(config.min_work_seconds - elapsedSeconds);
631
+ 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);
632
+ }
633
+ }
634
+ if (agent && config.max_completions_per_window && config.window_minutes) {
635
+ const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
636
+ const result = db.query(`SELECT COUNT(*) as count FROM tasks
637
+ WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
638
+ if (result.count >= config.max_completions_per_window) {
639
+ throw new CompletionGuardError(`Rate limit: ${result.count} tasks completed in the last ${config.window_minutes} minutes (max ${config.max_completions_per_window}). Slow down.`);
640
+ }
641
+ }
642
+ if (agent && config.cooldown_seconds) {
643
+ const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
644
+ WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
645
+ if (result.last_completed) {
646
+ const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
647
+ if (elapsedSeconds < config.cooldown_seconds) {
648
+ const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
649
+ throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
650
+ }
651
+ }
652
+ }
653
+ }
654
+
655
+ // src/db/tasks.ts
656
+ function rowToTask(row) {
657
+ return {
658
+ ...row,
659
+ tags: JSON.parse(row.tags || "[]"),
660
+ metadata: JSON.parse(row.metadata || "{}"),
661
+ status: row.status,
662
+ priority: row.priority
663
+ };
664
+ }
665
+ function insertTaskTags(taskId, tags, db) {
666
+ if (tags.length === 0)
667
+ return;
668
+ const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
669
+ for (const tag of tags) {
670
+ if (tag)
671
+ stmt.run(taskId, tag);
672
+ }
673
+ }
674
+ function replaceTaskTags(taskId, tags, db) {
675
+ db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
676
+ insertTaskTags(taskId, tags, db);
677
+ }
678
+ function createTask(input, db) {
679
+ const d = db || getDatabase();
680
+ const id = uuid();
681
+ const timestamp = now();
682
+ const tags = input.tags || [];
683
+ const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
684
+ const title = shortId ? `${shortId}: ${input.title}` : input.title;
685
+ 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)
686
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)`, [
687
+ id,
688
+ shortId,
689
+ input.project_id || null,
690
+ input.parent_id || null,
691
+ input.plan_id || null,
692
+ input.task_list_id || null,
693
+ title,
694
+ input.description || null,
695
+ input.status || "pending",
696
+ input.priority || "medium",
697
+ input.agent_id || null,
698
+ input.assigned_to || null,
699
+ input.session_id || null,
700
+ input.working_dir || null,
701
+ JSON.stringify(tags),
702
+ JSON.stringify(input.metadata || {}),
703
+ timestamp,
704
+ timestamp,
705
+ input.due_at || null
706
+ ]);
707
+ if (tags.length > 0) {
708
+ insertTaskTags(id, tags, d);
709
+ }
710
+ return getTask(id, d);
711
+ }
712
+ function getTask(id, db) {
713
+ const d = db || getDatabase();
714
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
715
+ if (!row)
716
+ return null;
717
+ return rowToTask(row);
718
+ }
719
+ function listTasks(filter = {}, db) {
720
+ const d = db || getDatabase();
721
+ clearExpiredLocks(d);
722
+ const conditions = [];
723
+ const params = [];
724
+ if (filter.project_id) {
725
+ conditions.push("project_id = ?");
726
+ params.push(filter.project_id);
727
+ }
728
+ if (filter.parent_id !== undefined) {
729
+ if (filter.parent_id === null) {
730
+ conditions.push("parent_id IS NULL");
731
+ } else {
732
+ conditions.push("parent_id = ?");
733
+ params.push(filter.parent_id);
734
+ }
735
+ }
736
+ if (filter.status) {
737
+ if (Array.isArray(filter.status)) {
738
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
739
+ params.push(...filter.status);
740
+ } else {
741
+ conditions.push("status = ?");
742
+ params.push(filter.status);
743
+ }
744
+ }
745
+ if (filter.priority) {
746
+ if (Array.isArray(filter.priority)) {
747
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
748
+ params.push(...filter.priority);
749
+ } else {
750
+ conditions.push("priority = ?");
751
+ params.push(filter.priority);
752
+ }
753
+ }
754
+ if (filter.assigned_to) {
755
+ conditions.push("assigned_to = ?");
756
+ params.push(filter.assigned_to);
757
+ }
758
+ if (filter.agent_id) {
759
+ conditions.push("agent_id = ?");
760
+ params.push(filter.agent_id);
761
+ }
762
+ if (filter.session_id) {
763
+ conditions.push("session_id = ?");
764
+ params.push(filter.session_id);
765
+ }
766
+ if (filter.tags && filter.tags.length > 0) {
767
+ const placeholders = filter.tags.map(() => "?").join(",");
768
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
769
+ params.push(...filter.tags);
770
+ }
771
+ if (filter.plan_id) {
772
+ conditions.push("plan_id = ?");
773
+ params.push(filter.plan_id);
774
+ }
775
+ if (filter.task_list_id) {
776
+ conditions.push("task_list_id = ?");
777
+ params.push(filter.task_list_id);
778
+ }
779
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
780
+ let limitClause = "";
781
+ if (filter.limit) {
782
+ limitClause = " LIMIT ?";
783
+ params.push(filter.limit);
784
+ if (filter.offset) {
785
+ limitClause += " OFFSET ?";
786
+ params.push(filter.offset);
787
+ }
788
+ }
789
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
790
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
791
+ created_at DESC${limitClause}`).all(...params);
792
+ return rows.map(rowToTask);
793
+ }
794
+ function updateTask(id, input, db) {
795
+ const d = db || getDatabase();
796
+ const task = getTask(id, d);
797
+ if (!task)
798
+ throw new TaskNotFoundError(id);
799
+ if (task.version !== input.version) {
800
+ throw new VersionConflictError(id, input.version, task.version);
801
+ }
802
+ const sets = ["version = version + 1", "updated_at = ?"];
803
+ const params = [now()];
804
+ if (input.title !== undefined) {
805
+ sets.push("title = ?");
806
+ params.push(input.title);
807
+ }
808
+ if (input.description !== undefined) {
809
+ sets.push("description = ?");
810
+ params.push(input.description);
811
+ }
812
+ if (input.status !== undefined) {
813
+ if (input.status === "completed") {
814
+ checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
815
+ }
816
+ sets.push("status = ?");
817
+ params.push(input.status);
818
+ if (input.status === "completed") {
819
+ sets.push("completed_at = ?");
820
+ params.push(now());
821
+ }
822
+ }
823
+ if (input.priority !== undefined) {
824
+ sets.push("priority = ?");
825
+ params.push(input.priority);
826
+ }
827
+ if (input.assigned_to !== undefined) {
828
+ sets.push("assigned_to = ?");
829
+ params.push(input.assigned_to);
830
+ }
831
+ if (input.tags !== undefined) {
832
+ sets.push("tags = ?");
833
+ params.push(JSON.stringify(input.tags));
834
+ }
835
+ if (input.metadata !== undefined) {
836
+ sets.push("metadata = ?");
837
+ params.push(JSON.stringify(input.metadata));
838
+ }
839
+ if (input.plan_id !== undefined) {
840
+ sets.push("plan_id = ?");
841
+ params.push(input.plan_id);
842
+ }
843
+ if (input.task_list_id !== undefined) {
844
+ sets.push("task_list_id = ?");
845
+ params.push(input.task_list_id);
846
+ }
847
+ if (input.due_at !== undefined) {
848
+ sets.push("due_at = ?");
849
+ params.push(input.due_at);
850
+ }
851
+ params.push(id, input.version);
852
+ const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
853
+ if (result.changes === 0) {
854
+ const current = getTask(id, d);
855
+ throw new VersionConflictError(id, input.version, current?.version ?? -1);
856
+ }
857
+ if (input.tags !== undefined) {
858
+ replaceTaskTags(id, input.tags, d);
859
+ }
860
+ return getTask(id, d);
861
+ }
862
+ function deleteTask(id, db) {
863
+ const d = db || getDatabase();
864
+ const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
865
+ return result.changes > 0;
866
+ }
867
+ function startTask(id, agentId, db) {
868
+ const d = db || getDatabase();
869
+ const cutoff = lockExpiryCutoff();
870
+ const timestamp = now();
871
+ const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
872
+ WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
873
+ if (result.changes === 0) {
874
+ const current = getTask(id, d);
875
+ if (!current)
876
+ throw new TaskNotFoundError(id);
877
+ if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
878
+ throw new LockError(id, current.locked_by);
879
+ }
880
+ }
881
+ return getTask(id, d);
882
+ }
883
+ function completeTask(id, agentId, db) {
884
+ const d = db || getDatabase();
885
+ const task = getTask(id, d);
886
+ if (!task)
887
+ throw new TaskNotFoundError(id);
888
+ if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
889
+ throw new LockError(id, task.locked_by);
890
+ }
891
+ checkCompletionGuard(task, agentId || null, d);
892
+ const timestamp = now();
893
+ d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
894
+ WHERE id = ?`, [timestamp, timestamp, id]);
895
+ return getTask(id, d);
896
+ }
897
+
898
+ // src/server/serve.ts
899
+ init_projects();
900
+ init_agents();
901
+ init_database();
902
+ function resolveDashboardDir() {
903
+ const candidates = [];
904
+ try {
905
+ const scriptDir = dirname2(fileURLToPath(import.meta.url));
906
+ candidates.push(join3(scriptDir, "..", "dashboard", "dist"));
907
+ candidates.push(join3(scriptDir, "..", "..", "dashboard", "dist"));
908
+ } catch {}
909
+ if (process.argv[1]) {
910
+ const mainDir = dirname2(process.argv[1]);
911
+ candidates.push(join3(mainDir, "..", "dashboard", "dist"));
912
+ candidates.push(join3(mainDir, "..", "..", "dashboard", "dist"));
913
+ }
914
+ candidates.push(join3(process.cwd(), "dashboard", "dist"));
915
+ for (const candidate of candidates) {
916
+ if (existsSync4(candidate))
917
+ return candidate;
918
+ }
919
+ return join3(process.cwd(), "dashboard", "dist");
920
+ }
921
+ var MIME_TYPES = {
922
+ ".html": "text/html; charset=utf-8",
923
+ ".js": "application/javascript",
924
+ ".css": "text/css",
925
+ ".json": "application/json",
926
+ ".png": "image/png",
927
+ ".jpg": "image/jpeg",
928
+ ".svg": "image/svg+xml",
929
+ ".ico": "image/x-icon",
930
+ ".woff": "font/woff",
931
+ ".woff2": "font/woff2"
932
+ };
933
+ var SECURITY_HEADERS = {
934
+ "X-Content-Type-Options": "nosniff",
935
+ "X-Frame-Options": "DENY"
936
+ };
937
+ function json(data, status = 200, port) {
938
+ return new Response(JSON.stringify(data), {
939
+ status,
940
+ headers: {
941
+ "Content-Type": "application/json",
942
+ "Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
943
+ ...SECURITY_HEADERS
944
+ }
945
+ });
946
+ }
947
+ function serveStaticFile(filePath) {
948
+ if (!existsSync4(filePath))
949
+ return null;
950
+ const ext = extname(filePath);
951
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
952
+ return new Response(Bun.file(filePath), {
953
+ headers: { "Content-Type": contentType }
954
+ });
955
+ }
956
+ function taskToSummary(task) {
957
+ return {
958
+ id: task.id,
959
+ short_id: task.short_id,
960
+ title: task.title,
961
+ description: task.description,
962
+ status: task.status,
963
+ priority: task.priority,
964
+ project_id: task.project_id,
965
+ plan_id: task.plan_id,
966
+ task_list_id: task.task_list_id,
967
+ agent_id: task.agent_id,
968
+ assigned_to: task.assigned_to,
969
+ locked_by: task.locked_by,
970
+ tags: task.tags,
971
+ version: task.version,
972
+ created_at: task.created_at,
973
+ updated_at: task.updated_at,
974
+ completed_at: task.completed_at,
975
+ due_at: task.due_at
976
+ };
977
+ }
978
+ async function startServer(port, options) {
979
+ const shouldOpen = options?.open ?? true;
980
+ getDatabase();
981
+ const dashboardDir = resolveDashboardDir();
982
+ const dashboardExists = existsSync4(dashboardDir);
983
+ if (!dashboardExists) {
984
+ console.error(`
985
+ Dashboard not found at: ${dashboardDir}`);
986
+ console.error(`Run this to build it:
987
+ `);
988
+ console.error(` cd dashboard && bun install && bun run build
989
+ `);
990
+ }
991
+ const server = Bun.serve({
992
+ port,
993
+ async fetch(req) {
994
+ const url = new URL(req.url);
995
+ const path = url.pathname;
996
+ const method = req.method;
997
+ if (method === "OPTIONS") {
998
+ return new Response(null, {
999
+ headers: {
1000
+ "Access-Control-Allow-Origin": `http://localhost:${port}`,
1001
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
1002
+ "Access-Control-Allow-Headers": "Content-Type"
1003
+ }
1004
+ });
1005
+ }
1006
+ if (path === "/api/stats" && method === "GET") {
1007
+ const all = listTasks({ limit: 1e4 });
1008
+ const projects = listProjects();
1009
+ const agents = listAgents();
1010
+ return json({
1011
+ total_tasks: all.length,
1012
+ pending: all.filter((t) => t.status === "pending").length,
1013
+ in_progress: all.filter((t) => t.status === "in_progress").length,
1014
+ completed: all.filter((t) => t.status === "completed").length,
1015
+ failed: all.filter((t) => t.status === "failed").length,
1016
+ cancelled: all.filter((t) => t.status === "cancelled").length,
1017
+ projects: projects.length,
1018
+ agents: agents.length
1019
+ }, 200, port);
1020
+ }
1021
+ if (path === "/api/tasks" && method === "GET") {
1022
+ const status = url.searchParams.get("status") || undefined;
1023
+ const projectId = url.searchParams.get("project_id") || undefined;
1024
+ const limitParam = url.searchParams.get("limit");
1025
+ const tasks = listTasks({
1026
+ status,
1027
+ project_id: projectId,
1028
+ limit: limitParam ? parseInt(limitParam, 10) : undefined
1029
+ });
1030
+ return json(tasks.map(taskToSummary), 200, port);
1031
+ }
1032
+ if (path === "/api/tasks" && method === "POST") {
1033
+ try {
1034
+ const body = await req.json();
1035
+ if (!body.title)
1036
+ return json({ error: "Missing 'title'" }, 400, port);
1037
+ const task = createTask({
1038
+ title: body.title,
1039
+ description: body.description,
1040
+ priority: body.priority,
1041
+ project_id: body.project_id
1042
+ });
1043
+ return json(taskToSummary(task), 201, port);
1044
+ } catch (e) {
1045
+ return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
1046
+ }
1047
+ }
1048
+ if (path === "/api/tasks/export" && method === "GET") {
1049
+ const format = url.searchParams.get("format") || "json";
1050
+ const status = url.searchParams.get("status") || undefined;
1051
+ const projectId = url.searchParams.get("project_id") || undefined;
1052
+ const tasks = listTasks({ status, project_id: projectId, limit: 1e4 });
1053
+ const summaries = tasks.map(taskToSummary);
1054
+ if (format === "csv") {
1055
+ const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
1056
+ const rows = summaries.map((t) => headers.map((h) => {
1057
+ const val = t[h];
1058
+ if (val === null || val === undefined)
1059
+ return "";
1060
+ const str = String(val);
1061
+ return str.includes(",") || str.includes('"') || str.includes(`
1062
+ `) ? `"${str.replace(/"/g, '""')}"` : str;
1063
+ }).join(","));
1064
+ const csv = [headers.join(","), ...rows].join(`
1065
+ `);
1066
+ return new Response(csv, {
1067
+ headers: {
1068
+ "Content-Type": "text/csv",
1069
+ "Content-Disposition": "attachment; filename=tasks.csv",
1070
+ ...SECURITY_HEADERS
1071
+ }
1072
+ });
1073
+ }
1074
+ return new Response(JSON.stringify(summaries, null, 2), {
1075
+ headers: {
1076
+ "Content-Type": "application/json",
1077
+ "Content-Disposition": "attachment; filename=tasks.json",
1078
+ ...SECURITY_HEADERS
1079
+ }
1080
+ });
1081
+ }
1082
+ if (path === "/api/tasks/bulk" && method === "POST") {
1083
+ try {
1084
+ const body = await req.json();
1085
+ if (!body.ids?.length || !body.action)
1086
+ return json({ error: "Missing ids or action" }, 400, port);
1087
+ const results = [];
1088
+ for (const id of body.ids) {
1089
+ try {
1090
+ if (body.action === "delete") {
1091
+ deleteTask(id);
1092
+ results.push({ id, success: true });
1093
+ } else if (body.action === "start") {
1094
+ startTask(id, "dashboard");
1095
+ results.push({ id, success: true });
1096
+ } else if (body.action === "complete") {
1097
+ completeTask(id, "dashboard");
1098
+ results.push({ id, success: true });
1099
+ }
1100
+ } catch (e) {
1101
+ results.push({ id, success: false, error: e instanceof Error ? e.message : "Failed" });
1102
+ }
1103
+ }
1104
+ return json({ results, succeeded: results.filter((r) => r.success).length, failed: results.filter((r) => !r.success).length }, 200, port);
1105
+ } catch (e) {
1106
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
1107
+ }
1108
+ }
1109
+ const taskMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
1110
+ if (taskMatch) {
1111
+ const id = taskMatch[1];
1112
+ if (method === "GET") {
1113
+ const task = getTask(id);
1114
+ if (!task)
1115
+ return json({ error: "Task not found" }, 404, port);
1116
+ return json(taskToSummary(task), 200, port);
1117
+ }
1118
+ if (method === "PATCH") {
1119
+ try {
1120
+ const body = await req.json();
1121
+ const task = getTask(id);
1122
+ if (!task)
1123
+ return json({ error: "Task not found" }, 404, port);
1124
+ const updated = updateTask(id, {
1125
+ ...body,
1126
+ version: task.version
1127
+ });
1128
+ return json(taskToSummary(updated), 200, port);
1129
+ } catch (e) {
1130
+ return json({ error: e instanceof Error ? e.message : "Failed to update task" }, 500, port);
1131
+ }
1132
+ }
1133
+ if (method === "DELETE") {
1134
+ const deleted = deleteTask(id);
1135
+ if (!deleted)
1136
+ return json({ error: "Task not found" }, 404, port);
1137
+ return json({ success: true }, 200, port);
1138
+ }
1139
+ }
1140
+ const startMatch = path.match(/^\/api\/tasks\/([^/]+)\/start$/);
1141
+ if (startMatch && method === "POST") {
1142
+ const id = startMatch[1];
1143
+ try {
1144
+ const task = startTask(id, "dashboard");
1145
+ return json(taskToSummary(task), 200, port);
1146
+ } catch (e) {
1147
+ return json({ error: e instanceof Error ? e.message : "Failed to start task" }, 500, port);
1148
+ }
1149
+ }
1150
+ const completeMatch = path.match(/^\/api\/tasks\/([^/]+)\/complete$/);
1151
+ if (completeMatch && method === "POST") {
1152
+ const id = completeMatch[1];
1153
+ try {
1154
+ const task = completeTask(id, "dashboard");
1155
+ return json(taskToSummary(task), 200, port);
1156
+ } catch (e) {
1157
+ return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500, port);
1158
+ }
1159
+ }
1160
+ if (path === "/api/projects" && method === "GET") {
1161
+ return json(listProjects(), 200, port);
1162
+ }
1163
+ if (path === "/api/agents" && method === "GET") {
1164
+ return json(listAgents(), 200, port);
1165
+ }
1166
+ if (path === "/api/projects" && method === "POST") {
1167
+ try {
1168
+ const body = await req.json();
1169
+ if (!body.name || !body.path)
1170
+ return json({ error: "Missing name or path" }, 400, port);
1171
+ const { createProject: createProject2 } = await Promise.resolve().then(() => (init_projects(), exports_projects));
1172
+ const project = createProject2({ name: body.name, path: body.path, description: body.description });
1173
+ return json(project, 201, port);
1174
+ } catch (e) {
1175
+ return json({ error: e instanceof Error ? e.message : "Failed to create project" }, 500, port);
1176
+ }
1177
+ }
1178
+ const projectDeleteMatch = path.match(/^\/api\/projects\/([^/]+)$/);
1179
+ if (projectDeleteMatch && method === "DELETE") {
1180
+ const id = projectDeleteMatch[1];
1181
+ const { deleteProject: deleteProject2 } = await Promise.resolve().then(() => (init_projects(), exports_projects));
1182
+ const deleted = deleteProject2(id);
1183
+ if (!deleted)
1184
+ return json({ error: "Project not found" }, 404, port);
1185
+ return json({ success: true }, 200, port);
1186
+ }
1187
+ if (path === "/api/agents" && method === "POST") {
1188
+ try {
1189
+ const body = await req.json();
1190
+ if (!body.name)
1191
+ return json({ error: "Missing name" }, 400, port);
1192
+ const { registerAgent: registerAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
1193
+ const agent = registerAgent2({ name: body.name, description: body.description });
1194
+ return json(agent, 201, port);
1195
+ } catch (e) {
1196
+ return json({ error: e instanceof Error ? e.message : "Failed to register agent" }, 500, port);
1197
+ }
1198
+ }
1199
+ const agentDeleteMatch = path.match(/^\/api\/agents\/([^/]+)$/);
1200
+ if (agentDeleteMatch && method === "DELETE") {
1201
+ const id = agentDeleteMatch[1];
1202
+ const { deleteAgent: deleteAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
1203
+ const deleted = deleteAgent2(id);
1204
+ if (!deleted)
1205
+ return json({ error: "Agent not found" }, 404, port);
1206
+ return json({ success: true }, 200, port);
1207
+ }
1208
+ if (path === "/api/agents/bulk" && method === "POST") {
1209
+ try {
1210
+ const body = await req.json();
1211
+ if (!body.ids?.length || body.action !== "delete")
1212
+ return json({ error: "Missing ids or invalid action" }, 400, port);
1213
+ const { deleteAgent: deleteAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
1214
+ let succeeded = 0;
1215
+ for (const id of body.ids) {
1216
+ if (deleteAgent2(id))
1217
+ succeeded++;
1218
+ }
1219
+ return json({ succeeded, failed: body.ids.length - succeeded }, 200, port);
1220
+ } catch (e) {
1221
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
1222
+ }
1223
+ }
1224
+ if (path === "/api/projects/bulk" && method === "POST") {
1225
+ try {
1226
+ const body = await req.json();
1227
+ if (!body.ids?.length || body.action !== "delete")
1228
+ return json({ error: "Missing ids or invalid action" }, 400, port);
1229
+ const { deleteProject: deleteProject2 } = await Promise.resolve().then(() => (init_projects(), exports_projects));
1230
+ let succeeded = 0;
1231
+ for (const id of body.ids) {
1232
+ if (deleteProject2(id))
1233
+ succeeded++;
1234
+ }
1235
+ return json({ succeeded, failed: body.ids.length - succeeded }, 200, port);
1236
+ } catch (e) {
1237
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
1238
+ }
1239
+ }
1240
+ if (dashboardExists && (method === "GET" || method === "HEAD")) {
1241
+ if (path !== "/") {
1242
+ const filePath = join3(dashboardDir, path);
1243
+ const res2 = serveStaticFile(filePath);
1244
+ if (res2)
1245
+ return res2;
1246
+ }
1247
+ const indexPath = join3(dashboardDir, "index.html");
1248
+ const res = serveStaticFile(indexPath);
1249
+ if (res)
1250
+ return res;
1251
+ }
1252
+ return json({ error: "Not found" }, 404, port);
1253
+ }
1254
+ });
1255
+ const shutdown = () => {
1256
+ server.stop();
1257
+ process.exit(0);
1258
+ };
1259
+ process.on("SIGINT", shutdown);
1260
+ process.on("SIGTERM", shutdown);
1261
+ const serverUrl = `http://localhost:${port}`;
1262
+ console.log(`Todos Dashboard running at ${serverUrl}`);
1263
+ if (shouldOpen) {
1264
+ try {
1265
+ const { exec } = await import("child_process");
1266
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1267
+ exec(`${openCmd} ${serverUrl}`);
1268
+ } catch {}
1269
+ }
1270
+ }
1271
+
1272
+ // src/server/index.ts
1273
+ var DEFAULT_PORT = 19427;
1274
+ function parsePort() {
1275
+ const portArg = process.argv.find((a) => a === "--port" || a.startsWith("--port="));
1276
+ if (portArg) {
1277
+ if (portArg.includes("=")) {
1278
+ return parseInt(portArg.split("=")[1], 10) || DEFAULT_PORT;
1279
+ }
1280
+ const idx = process.argv.indexOf(portArg);
1281
+ return parseInt(process.argv[idx + 1], 10) || DEFAULT_PORT;
1282
+ }
1283
+ return DEFAULT_PORT;
1284
+ }
1285
+ startServer(parsePort());