@hasna/todos 0.9.6 → 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.
package/dist/cli/index.js CHANGED
@@ -2374,12 +2374,17 @@ var init_database = __esm(() => {
2374
2374
  CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL;
2375
2375
 
2376
2376
  INSERT OR IGNORE INTO _migrations (id) VALUES (6);
2377
+ `,
2378
+ `
2379
+ ALTER TABLE tasks ADD COLUMN due_at TEXT;
2380
+ CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at);
2381
+ INSERT OR IGNORE INTO _migrations (id) VALUES (7);
2377
2382
  `
2378
2383
  ];
2379
2384
  });
2380
2385
 
2381
2386
  // src/types/index.ts
2382
- var VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, TaskListNotFoundError, DependencyCycleError;
2387
+ var VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError;
2383
2388
  var init_types = __esm(() => {
2384
2389
  VersionConflictError = class VersionConflictError extends Error {
2385
2390
  taskId;
@@ -2445,9 +2450,31 @@ var init_types = __esm(() => {
2445
2450
  this.name = "DependencyCycleError";
2446
2451
  }
2447
2452
  };
2453
+ CompletionGuardError = class CompletionGuardError extends Error {
2454
+ reason;
2455
+ retryAfterSeconds;
2456
+ constructor(reason, retryAfterSeconds) {
2457
+ super(reason);
2458
+ this.reason = reason;
2459
+ this.retryAfterSeconds = retryAfterSeconds;
2460
+ this.name = "CompletionGuardError";
2461
+ }
2462
+ };
2448
2463
  });
2449
2464
 
2450
2465
  // src/db/projects.ts
2466
+ var exports_projects = {};
2467
+ __export(exports_projects, {
2468
+ updateProject: () => updateProject,
2469
+ slugify: () => slugify,
2470
+ nextTaskShortId: () => nextTaskShortId,
2471
+ listProjects: () => listProjects,
2472
+ getProjectByPath: () => getProjectByPath,
2473
+ getProject: () => getProject,
2474
+ ensureProject: () => ensureProject,
2475
+ deleteProject: () => deleteProject,
2476
+ createProject: () => createProject
2477
+ });
2451
2478
  function slugify(name) {
2452
2479
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2453
2480
  }
@@ -2518,6 +2545,11 @@ function updateProject(id, input, db) {
2518
2545
  d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
2519
2546
  return getProject(id, d);
2520
2547
  }
2548
+ function deleteProject(id, db) {
2549
+ const d = db || getDatabase();
2550
+ const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
2551
+ return result.changes > 0;
2552
+ }
2521
2553
  function nextTaskShortId(projectId, db) {
2522
2554
  const d = db || getDatabase();
2523
2555
  const project = getProject(projectId, d);
@@ -2546,6 +2578,173 @@ var init_projects = __esm(() => {
2546
2578
  init_database();
2547
2579
  });
2548
2580
 
2581
+ // src/lib/sync-utils.ts
2582
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
2583
+ import { join as join2 } from "path";
2584
+ function ensureDir2(dir) {
2585
+ if (!existsSync2(dir))
2586
+ mkdirSync2(dir, { recursive: true });
2587
+ }
2588
+ function listJsonFiles(dir) {
2589
+ if (!existsSync2(dir))
2590
+ return [];
2591
+ return readdirSync(dir).filter((f) => f.endsWith(".json"));
2592
+ }
2593
+ function readJsonFile(path) {
2594
+ try {
2595
+ return JSON.parse(readFileSync(path, "utf-8"));
2596
+ } catch {
2597
+ return null;
2598
+ }
2599
+ }
2600
+ function writeJsonFile(path, data) {
2601
+ writeFileSync(path, JSON.stringify(data, null, 2) + `
2602
+ `);
2603
+ }
2604
+ function readHighWaterMark(dir) {
2605
+ const path = join2(dir, ".highwatermark");
2606
+ if (!existsSync2(path))
2607
+ return 1;
2608
+ const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
2609
+ return isNaN(val) ? 1 : val;
2610
+ }
2611
+ function writeHighWaterMark(dir, value) {
2612
+ writeFileSync(join2(dir, ".highwatermark"), String(value));
2613
+ }
2614
+ function getFileMtimeMs(path) {
2615
+ try {
2616
+ return statSync(path).mtimeMs;
2617
+ } catch {
2618
+ return null;
2619
+ }
2620
+ }
2621
+ function parseTimestamp(value) {
2622
+ if (typeof value !== "string")
2623
+ return null;
2624
+ const parsed = Date.parse(value);
2625
+ return Number.isNaN(parsed) ? null : parsed;
2626
+ }
2627
+ function appendSyncConflict(metadata, conflict, limit = 5) {
2628
+ const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
2629
+ const next = [conflict, ...current].slice(0, limit);
2630
+ return { ...metadata, sync_conflicts: next };
2631
+ }
2632
+ var HOME;
2633
+ var init_sync_utils = __esm(() => {
2634
+ HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2635
+ });
2636
+
2637
+ // src/lib/config.ts
2638
+ import { existsSync as existsSync3 } from "fs";
2639
+ import { join as join3 } from "path";
2640
+ function normalizeAgent(agent) {
2641
+ return agent.trim().toLowerCase();
2642
+ }
2643
+ function loadConfig() {
2644
+ if (cached)
2645
+ return cached;
2646
+ if (!existsSync3(CONFIG_PATH)) {
2647
+ cached = {};
2648
+ return cached;
2649
+ }
2650
+ const config = readJsonFile(CONFIG_PATH) || {};
2651
+ if (typeof config.sync_agents === "string") {
2652
+ config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
2653
+ }
2654
+ cached = config;
2655
+ return cached;
2656
+ }
2657
+ function getSyncAgentsFromConfig() {
2658
+ const config = loadConfig();
2659
+ const agents = config.sync_agents;
2660
+ if (Array.isArray(agents) && agents.length > 0)
2661
+ return agents.map(normalizeAgent);
2662
+ return null;
2663
+ }
2664
+ function getAgentTaskListId(agent) {
2665
+ const config = loadConfig();
2666
+ const key = normalizeAgent(agent);
2667
+ return config.agents?.[key]?.task_list_id || config.task_list_id || null;
2668
+ }
2669
+ function getAgentTasksDir(agent) {
2670
+ const config = loadConfig();
2671
+ const key = normalizeAgent(agent);
2672
+ return config.agents?.[key]?.tasks_dir || config.agent_tasks_dir || null;
2673
+ }
2674
+ function getTaskPrefixConfig() {
2675
+ const config = loadConfig();
2676
+ return config.task_prefix || null;
2677
+ }
2678
+ function getCompletionGuardConfig(projectPath) {
2679
+ const config = loadConfig();
2680
+ const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
2681
+ if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
2682
+ return { ...global, ...config.project_overrides[projectPath].completion_guard };
2683
+ }
2684
+ return global;
2685
+ }
2686
+ var CONFIG_PATH, cached = null, GUARD_DEFAULTS;
2687
+ var init_config = __esm(() => {
2688
+ init_sync_utils();
2689
+ CONFIG_PATH = join3(HOME, ".todos", "config.json");
2690
+ GUARD_DEFAULTS = {
2691
+ enabled: false,
2692
+ min_work_seconds: 30,
2693
+ max_completions_per_window: 5,
2694
+ window_minutes: 10,
2695
+ cooldown_seconds: 60
2696
+ };
2697
+ });
2698
+
2699
+ // src/lib/completion-guard.ts
2700
+ function checkCompletionGuard(task, agentId, db, configOverride) {
2701
+ let config;
2702
+ if (configOverride) {
2703
+ config = configOverride;
2704
+ } else {
2705
+ const projectPath = task.project_id ? getProject(task.project_id, db)?.path : null;
2706
+ config = getCompletionGuardConfig(projectPath);
2707
+ }
2708
+ if (!config.enabled)
2709
+ return;
2710
+ if (task.status !== "in_progress") {
2711
+ throw new CompletionGuardError(`Task must be in 'in_progress' status before completing (current: '${task.status}'). Use start_task first.`);
2712
+ }
2713
+ const agent = agentId || task.assigned_to || task.agent_id;
2714
+ if (config.min_work_seconds && task.locked_at) {
2715
+ const startedAt = new Date(task.locked_at).getTime();
2716
+ const elapsedSeconds = (Date.now() - startedAt) / 1000;
2717
+ if (elapsedSeconds < config.min_work_seconds) {
2718
+ const remaining = Math.ceil(config.min_work_seconds - elapsedSeconds);
2719
+ 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);
2720
+ }
2721
+ }
2722
+ if (agent && config.max_completions_per_window && config.window_minutes) {
2723
+ const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
2724
+ const result = db.query(`SELECT COUNT(*) as count FROM tasks
2725
+ WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
2726
+ if (result.count >= config.max_completions_per_window) {
2727
+ throw new CompletionGuardError(`Rate limit: ${result.count} tasks completed in the last ${config.window_minutes} minutes (max ${config.max_completions_per_window}). Slow down.`);
2728
+ }
2729
+ }
2730
+ if (agent && config.cooldown_seconds) {
2731
+ const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
2732
+ WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
2733
+ if (result.last_completed) {
2734
+ const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
2735
+ if (elapsedSeconds < config.cooldown_seconds) {
2736
+ const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
2737
+ throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
2738
+ }
2739
+ }
2740
+ }
2741
+ }
2742
+ var init_completion_guard = __esm(() => {
2743
+ init_types();
2744
+ init_config();
2745
+ init_projects();
2746
+ });
2747
+
2549
2748
  // src/db/tasks.ts
2550
2749
  function rowToTask(row) {
2551
2750
  return {
@@ -2576,8 +2775,8 @@ function createTask(input, db) {
2576
2775
  const tags = input.tags || [];
2577
2776
  const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
2578
2777
  const title = shortId ? `${shortId}: ${input.title}` : input.title;
2579
- 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)
2580
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)`, [
2778
+ 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)
2779
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)`, [
2581
2780
  id,
2582
2781
  shortId,
2583
2782
  input.project_id || null,
@@ -2595,7 +2794,8 @@ function createTask(input, db) {
2595
2794
  JSON.stringify(tags),
2596
2795
  JSON.stringify(input.metadata || {}),
2597
2796
  timestamp,
2598
- timestamp
2797
+ timestamp,
2798
+ input.due_at || null
2599
2799
  ]);
2600
2800
  if (tags.length > 0) {
2601
2801
  insertTaskTags(id, tags, d);
@@ -2696,12 +2896,18 @@ function listTasks(filter = {}, db) {
2696
2896
  params.push(filter.task_list_id);
2697
2897
  }
2698
2898
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2699
- const limitVal = filter.limit || 100;
2700
- const offsetVal = filter.offset || 0;
2701
- params.push(limitVal, offsetVal);
2899
+ let limitClause = "";
2900
+ if (filter.limit) {
2901
+ limitClause = " LIMIT ?";
2902
+ params.push(filter.limit);
2903
+ if (filter.offset) {
2904
+ limitClause += " OFFSET ?";
2905
+ params.push(filter.offset);
2906
+ }
2907
+ }
2702
2908
  const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
2703
2909
  CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
2704
- created_at DESC LIMIT ? OFFSET ?`).all(...params);
2910
+ created_at DESC${limitClause}`).all(...params);
2705
2911
  return rows.map(rowToTask);
2706
2912
  }
2707
2913
  function updateTask(id, input, db) {
@@ -2723,6 +2929,9 @@ function updateTask(id, input, db) {
2723
2929
  params.push(input.description);
2724
2930
  }
2725
2931
  if (input.status !== undefined) {
2932
+ if (input.status === "completed") {
2933
+ checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
2934
+ }
2726
2935
  sets.push("status = ?");
2727
2936
  params.push(input.status);
2728
2937
  if (input.status === "completed") {
@@ -2754,6 +2963,10 @@ function updateTask(id, input, db) {
2754
2963
  sets.push("task_list_id = ?");
2755
2964
  params.push(input.task_list_id);
2756
2965
  }
2966
+ if (input.due_at !== undefined) {
2967
+ sets.push("due_at = ?");
2968
+ params.push(input.due_at);
2969
+ }
2757
2970
  params.push(id, input.version);
2758
2971
  const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
2759
2972
  if (result.changes === 0) {
@@ -2794,6 +3007,7 @@ function completeTask(id, agentId, db) {
2794
3007
  if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
2795
3008
  throw new LockError(id, task.locked_by);
2796
3009
  }
3010
+ checkCompletionGuard(task, agentId || null, d);
2797
3011
  const timestamp = now();
2798
3012
  d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
2799
3013
  WHERE id = ?`, [timestamp, timestamp, id]);
@@ -2876,9 +3090,19 @@ var init_tasks = __esm(() => {
2876
3090
  init_types();
2877
3091
  init_database();
2878
3092
  init_projects();
3093
+ init_completion_guard();
2879
3094
  });
2880
3095
 
2881
3096
  // src/db/agents.ts
3097
+ var exports_agents = {};
3098
+ __export(exports_agents, {
3099
+ updateAgentActivity: () => updateAgentActivity,
3100
+ registerAgent: () => registerAgent,
3101
+ listAgents: () => listAgents,
3102
+ getAgentByName: () => getAgentByName,
3103
+ getAgent: () => getAgent,
3104
+ deleteAgent: () => deleteAgent
3105
+ });
2882
3106
  function shortUuid() {
2883
3107
  return crypto.randomUUID().slice(0, 8);
2884
3108
  }
@@ -2915,6 +3139,14 @@ function listAgents(db) {
2915
3139
  const d = db || getDatabase();
2916
3140
  return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
2917
3141
  }
3142
+ function updateAgentActivity(id, db) {
3143
+ const d = db || getDatabase();
3144
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
3145
+ }
3146
+ function deleteAgent(id, db) {
3147
+ const d = db || getDatabase();
3148
+ return d.run("DELETE FROM agents WHERE id = ?", [id]).changes > 0;
3149
+ }
2918
3150
  var init_agents = __esm(() => {
2919
3151
  init_database();
2920
3152
  });
@@ -3111,109 +3343,6 @@ var init_search = __esm(() => {
3111
3343
  init_database();
3112
3344
  });
3113
3345
 
3114
- // src/lib/sync-utils.ts
3115
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
3116
- import { join as join2 } from "path";
3117
- function ensureDir2(dir) {
3118
- if (!existsSync2(dir))
3119
- mkdirSync2(dir, { recursive: true });
3120
- }
3121
- function listJsonFiles(dir) {
3122
- if (!existsSync2(dir))
3123
- return [];
3124
- return readdirSync(dir).filter((f) => f.endsWith(".json"));
3125
- }
3126
- function readJsonFile(path) {
3127
- try {
3128
- return JSON.parse(readFileSync(path, "utf-8"));
3129
- } catch {
3130
- return null;
3131
- }
3132
- }
3133
- function writeJsonFile(path, data) {
3134
- writeFileSync(path, JSON.stringify(data, null, 2) + `
3135
- `);
3136
- }
3137
- function readHighWaterMark(dir) {
3138
- const path = join2(dir, ".highwatermark");
3139
- if (!existsSync2(path))
3140
- return 1;
3141
- const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
3142
- return isNaN(val) ? 1 : val;
3143
- }
3144
- function writeHighWaterMark(dir, value) {
3145
- writeFileSync(join2(dir, ".highwatermark"), String(value));
3146
- }
3147
- function getFileMtimeMs(path) {
3148
- try {
3149
- return statSync(path).mtimeMs;
3150
- } catch {
3151
- return null;
3152
- }
3153
- }
3154
- function parseTimestamp(value) {
3155
- if (typeof value !== "string")
3156
- return null;
3157
- const parsed = Date.parse(value);
3158
- return Number.isNaN(parsed) ? null : parsed;
3159
- }
3160
- function appendSyncConflict(metadata, conflict, limit = 5) {
3161
- const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
3162
- const next = [conflict, ...current].slice(0, limit);
3163
- return { ...metadata, sync_conflicts: next };
3164
- }
3165
- var HOME;
3166
- var init_sync_utils = __esm(() => {
3167
- HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
3168
- });
3169
-
3170
- // src/lib/config.ts
3171
- import { existsSync as existsSync3 } from "fs";
3172
- import { join as join3 } from "path";
3173
- function normalizeAgent(agent) {
3174
- return agent.trim().toLowerCase();
3175
- }
3176
- function loadConfig() {
3177
- if (cached)
3178
- return cached;
3179
- if (!existsSync3(CONFIG_PATH)) {
3180
- cached = {};
3181
- return cached;
3182
- }
3183
- const config = readJsonFile(CONFIG_PATH) || {};
3184
- if (typeof config.sync_agents === "string") {
3185
- config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
3186
- }
3187
- cached = config;
3188
- return cached;
3189
- }
3190
- function getSyncAgentsFromConfig() {
3191
- const config = loadConfig();
3192
- const agents = config.sync_agents;
3193
- if (Array.isArray(agents) && agents.length > 0)
3194
- return agents.map(normalizeAgent);
3195
- return null;
3196
- }
3197
- function getAgentTaskListId(agent) {
3198
- const config = loadConfig();
3199
- const key = normalizeAgent(agent);
3200
- return config.agents?.[key]?.task_list_id || config.task_list_id || null;
3201
- }
3202
- function getAgentTasksDir(agent) {
3203
- const config = loadConfig();
3204
- const key = normalizeAgent(agent);
3205
- return config.agents?.[key]?.tasks_dir || config.agent_tasks_dir || null;
3206
- }
3207
- function getTaskPrefixConfig() {
3208
- const config = loadConfig();
3209
- return config.task_prefix || null;
3210
- }
3211
- var CONFIG_PATH, cached = null;
3212
- var init_config = __esm(() => {
3213
- init_sync_utils();
3214
- CONFIG_PATH = join3(HOME, ".todos", "config.json");
3215
- });
3216
-
3217
3346
  // src/lib/claude-tasks.ts
3218
3347
  import { existsSync as existsSync4, readFileSync as readFileSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
3219
3348
  import { join as join4 } from "path";
@@ -7729,6 +7858,10 @@ function formatError(error) {
7729
7858
  return `Lock error: ${error.message}`;
7730
7859
  if (error instanceof DependencyCycleError)
7731
7860
  return `Dependency cycle: ${error.message}`;
7861
+ if (error instanceof CompletionGuardError) {
7862
+ const retry = error.retryAfterSeconds ? ` (retry after ${error.retryAfterSeconds}s)` : "";
7863
+ return `Completion blocked: ${error.reason}${retry}`;
7864
+ }
7732
7865
  if (error instanceof Error)
7733
7866
  return error.message;
7734
7867
  return String(error);
@@ -8461,6 +8594,391 @@ Slug: ${list.slug}`
8461
8594
  });
8462
8595
  });
8463
8596
 
8597
+ // src/server/serve.ts
8598
+ var exports_serve = {};
8599
+ __export(exports_serve, {
8600
+ startServer: () => startServer
8601
+ });
8602
+ import { existsSync as existsSync6 } from "fs";
8603
+ import { join as join6, dirname as dirname2, extname } from "path";
8604
+ import { fileURLToPath } from "url";
8605
+ function resolveDashboardDir() {
8606
+ const candidates = [];
8607
+ try {
8608
+ const scriptDir = dirname2(fileURLToPath(import.meta.url));
8609
+ candidates.push(join6(scriptDir, "..", "dashboard", "dist"));
8610
+ candidates.push(join6(scriptDir, "..", "..", "dashboard", "dist"));
8611
+ } catch {}
8612
+ if (process.argv[1]) {
8613
+ const mainDir = dirname2(process.argv[1]);
8614
+ candidates.push(join6(mainDir, "..", "dashboard", "dist"));
8615
+ candidates.push(join6(mainDir, "..", "..", "dashboard", "dist"));
8616
+ }
8617
+ candidates.push(join6(process.cwd(), "dashboard", "dist"));
8618
+ for (const candidate of candidates) {
8619
+ if (existsSync6(candidate))
8620
+ return candidate;
8621
+ }
8622
+ return join6(process.cwd(), "dashboard", "dist");
8623
+ }
8624
+ function json(data, status = 200, port) {
8625
+ return new Response(JSON.stringify(data), {
8626
+ status,
8627
+ headers: {
8628
+ "Content-Type": "application/json",
8629
+ "Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
8630
+ ...SECURITY_HEADERS
8631
+ }
8632
+ });
8633
+ }
8634
+ function serveStaticFile(filePath) {
8635
+ if (!existsSync6(filePath))
8636
+ return null;
8637
+ const ext = extname(filePath);
8638
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
8639
+ return new Response(Bun.file(filePath), {
8640
+ headers: { "Content-Type": contentType }
8641
+ });
8642
+ }
8643
+ function taskToSummary(task) {
8644
+ return {
8645
+ id: task.id,
8646
+ short_id: task.short_id,
8647
+ title: task.title,
8648
+ description: task.description,
8649
+ status: task.status,
8650
+ priority: task.priority,
8651
+ project_id: task.project_id,
8652
+ plan_id: task.plan_id,
8653
+ task_list_id: task.task_list_id,
8654
+ agent_id: task.agent_id,
8655
+ assigned_to: task.assigned_to,
8656
+ locked_by: task.locked_by,
8657
+ tags: task.tags,
8658
+ version: task.version,
8659
+ created_at: task.created_at,
8660
+ updated_at: task.updated_at,
8661
+ completed_at: task.completed_at,
8662
+ due_at: task.due_at
8663
+ };
8664
+ }
8665
+ async function startServer(port, options) {
8666
+ const shouldOpen = options?.open ?? true;
8667
+ getDatabase();
8668
+ const dashboardDir = resolveDashboardDir();
8669
+ const dashboardExists = existsSync6(dashboardDir);
8670
+ if (!dashboardExists) {
8671
+ console.error(`
8672
+ Dashboard not found at: ${dashboardDir}`);
8673
+ console.error(`Run this to build it:
8674
+ `);
8675
+ console.error(` cd dashboard && bun install && bun run build
8676
+ `);
8677
+ }
8678
+ const server2 = Bun.serve({
8679
+ port,
8680
+ async fetch(req) {
8681
+ const url = new URL(req.url);
8682
+ const path = url.pathname;
8683
+ const method = req.method;
8684
+ if (method === "OPTIONS") {
8685
+ return new Response(null, {
8686
+ headers: {
8687
+ "Access-Control-Allow-Origin": `http://localhost:${port}`,
8688
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
8689
+ "Access-Control-Allow-Headers": "Content-Type"
8690
+ }
8691
+ });
8692
+ }
8693
+ if (path === "/api/stats" && method === "GET") {
8694
+ const all = listTasks({ limit: 1e4 });
8695
+ const projects = listProjects();
8696
+ const agents = listAgents();
8697
+ return json({
8698
+ total_tasks: all.length,
8699
+ pending: all.filter((t) => t.status === "pending").length,
8700
+ in_progress: all.filter((t) => t.status === "in_progress").length,
8701
+ completed: all.filter((t) => t.status === "completed").length,
8702
+ failed: all.filter((t) => t.status === "failed").length,
8703
+ cancelled: all.filter((t) => t.status === "cancelled").length,
8704
+ projects: projects.length,
8705
+ agents: agents.length
8706
+ }, 200, port);
8707
+ }
8708
+ if (path === "/api/tasks" && method === "GET") {
8709
+ const status = url.searchParams.get("status") || undefined;
8710
+ const projectId = url.searchParams.get("project_id") || undefined;
8711
+ const limitParam = url.searchParams.get("limit");
8712
+ const tasks = listTasks({
8713
+ status,
8714
+ project_id: projectId,
8715
+ limit: limitParam ? parseInt(limitParam, 10) : undefined
8716
+ });
8717
+ return json(tasks.map(taskToSummary), 200, port);
8718
+ }
8719
+ if (path === "/api/tasks" && method === "POST") {
8720
+ try {
8721
+ const body = await req.json();
8722
+ if (!body.title)
8723
+ return json({ error: "Missing 'title'" }, 400, port);
8724
+ const task = createTask({
8725
+ title: body.title,
8726
+ description: body.description,
8727
+ priority: body.priority,
8728
+ project_id: body.project_id
8729
+ });
8730
+ return json(taskToSummary(task), 201, port);
8731
+ } catch (e) {
8732
+ return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
8733
+ }
8734
+ }
8735
+ if (path === "/api/tasks/export" && method === "GET") {
8736
+ const format = url.searchParams.get("format") || "json";
8737
+ const status = url.searchParams.get("status") || undefined;
8738
+ const projectId = url.searchParams.get("project_id") || undefined;
8739
+ const tasks = listTasks({ status, project_id: projectId, limit: 1e4 });
8740
+ const summaries = tasks.map(taskToSummary);
8741
+ if (format === "csv") {
8742
+ const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
8743
+ const rows = summaries.map((t) => headers.map((h) => {
8744
+ const val = t[h];
8745
+ if (val === null || val === undefined)
8746
+ return "";
8747
+ const str = String(val);
8748
+ return str.includes(",") || str.includes('"') || str.includes(`
8749
+ `) ? `"${str.replace(/"/g, '""')}"` : str;
8750
+ }).join(","));
8751
+ const csv = [headers.join(","), ...rows].join(`
8752
+ `);
8753
+ return new Response(csv, {
8754
+ headers: {
8755
+ "Content-Type": "text/csv",
8756
+ "Content-Disposition": "attachment; filename=tasks.csv",
8757
+ ...SECURITY_HEADERS
8758
+ }
8759
+ });
8760
+ }
8761
+ return new Response(JSON.stringify(summaries, null, 2), {
8762
+ headers: {
8763
+ "Content-Type": "application/json",
8764
+ "Content-Disposition": "attachment; filename=tasks.json",
8765
+ ...SECURITY_HEADERS
8766
+ }
8767
+ });
8768
+ }
8769
+ if (path === "/api/tasks/bulk" && method === "POST") {
8770
+ try {
8771
+ const body = await req.json();
8772
+ if (!body.ids?.length || !body.action)
8773
+ return json({ error: "Missing ids or action" }, 400, port);
8774
+ const results = [];
8775
+ for (const id of body.ids) {
8776
+ try {
8777
+ if (body.action === "delete") {
8778
+ deleteTask(id);
8779
+ results.push({ id, success: true });
8780
+ } else if (body.action === "start") {
8781
+ startTask(id, "dashboard");
8782
+ results.push({ id, success: true });
8783
+ } else if (body.action === "complete") {
8784
+ completeTask(id, "dashboard");
8785
+ results.push({ id, success: true });
8786
+ }
8787
+ } catch (e) {
8788
+ results.push({ id, success: false, error: e instanceof Error ? e.message : "Failed" });
8789
+ }
8790
+ }
8791
+ return json({ results, succeeded: results.filter((r) => r.success).length, failed: results.filter((r) => !r.success).length }, 200, port);
8792
+ } catch (e) {
8793
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
8794
+ }
8795
+ }
8796
+ const taskMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
8797
+ if (taskMatch) {
8798
+ const id = taskMatch[1];
8799
+ if (method === "GET") {
8800
+ const task = getTask(id);
8801
+ if (!task)
8802
+ return json({ error: "Task not found" }, 404, port);
8803
+ return json(taskToSummary(task), 200, port);
8804
+ }
8805
+ if (method === "PATCH") {
8806
+ try {
8807
+ const body = await req.json();
8808
+ const task = getTask(id);
8809
+ if (!task)
8810
+ return json({ error: "Task not found" }, 404, port);
8811
+ const updated = updateTask(id, {
8812
+ ...body,
8813
+ version: task.version
8814
+ });
8815
+ return json(taskToSummary(updated), 200, port);
8816
+ } catch (e) {
8817
+ return json({ error: e instanceof Error ? e.message : "Failed to update task" }, 500, port);
8818
+ }
8819
+ }
8820
+ if (method === "DELETE") {
8821
+ const deleted = deleteTask(id);
8822
+ if (!deleted)
8823
+ return json({ error: "Task not found" }, 404, port);
8824
+ return json({ success: true }, 200, port);
8825
+ }
8826
+ }
8827
+ const startMatch = path.match(/^\/api\/tasks\/([^/]+)\/start$/);
8828
+ if (startMatch && method === "POST") {
8829
+ const id = startMatch[1];
8830
+ try {
8831
+ const task = startTask(id, "dashboard");
8832
+ return json(taskToSummary(task), 200, port);
8833
+ } catch (e) {
8834
+ return json({ error: e instanceof Error ? e.message : "Failed to start task" }, 500, port);
8835
+ }
8836
+ }
8837
+ const completeMatch = path.match(/^\/api\/tasks\/([^/]+)\/complete$/);
8838
+ if (completeMatch && method === "POST") {
8839
+ const id = completeMatch[1];
8840
+ try {
8841
+ const task = completeTask(id, "dashboard");
8842
+ return json(taskToSummary(task), 200, port);
8843
+ } catch (e) {
8844
+ return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500, port);
8845
+ }
8846
+ }
8847
+ if (path === "/api/projects" && method === "GET") {
8848
+ return json(listProjects(), 200, port);
8849
+ }
8850
+ if (path === "/api/agents" && method === "GET") {
8851
+ return json(listAgents(), 200, port);
8852
+ }
8853
+ if (path === "/api/projects" && method === "POST") {
8854
+ try {
8855
+ const body = await req.json();
8856
+ if (!body.name || !body.path)
8857
+ return json({ error: "Missing name or path" }, 400, port);
8858
+ const { createProject: createProject2 } = await Promise.resolve().then(() => (init_projects(), exports_projects));
8859
+ const project = createProject2({ name: body.name, path: body.path, description: body.description });
8860
+ return json(project, 201, port);
8861
+ } catch (e) {
8862
+ return json({ error: e instanceof Error ? e.message : "Failed to create project" }, 500, port);
8863
+ }
8864
+ }
8865
+ const projectDeleteMatch = path.match(/^\/api\/projects\/([^/]+)$/);
8866
+ if (projectDeleteMatch && method === "DELETE") {
8867
+ const id = projectDeleteMatch[1];
8868
+ const { deleteProject: deleteProject2 } = await Promise.resolve().then(() => (init_projects(), exports_projects));
8869
+ const deleted = deleteProject2(id);
8870
+ if (!deleted)
8871
+ return json({ error: "Project not found" }, 404, port);
8872
+ return json({ success: true }, 200, port);
8873
+ }
8874
+ if (path === "/api/agents" && method === "POST") {
8875
+ try {
8876
+ const body = await req.json();
8877
+ if (!body.name)
8878
+ return json({ error: "Missing name" }, 400, port);
8879
+ const { registerAgent: registerAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
8880
+ const agent = registerAgent2({ name: body.name, description: body.description });
8881
+ return json(agent, 201, port);
8882
+ } catch (e) {
8883
+ return json({ error: e instanceof Error ? e.message : "Failed to register agent" }, 500, port);
8884
+ }
8885
+ }
8886
+ const agentDeleteMatch = path.match(/^\/api\/agents\/([^/]+)$/);
8887
+ if (agentDeleteMatch && method === "DELETE") {
8888
+ const id = agentDeleteMatch[1];
8889
+ const { deleteAgent: deleteAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
8890
+ const deleted = deleteAgent2(id);
8891
+ if (!deleted)
8892
+ return json({ error: "Agent not found" }, 404, port);
8893
+ return json({ success: true }, 200, port);
8894
+ }
8895
+ if (path === "/api/agents/bulk" && method === "POST") {
8896
+ try {
8897
+ const body = await req.json();
8898
+ if (!body.ids?.length || body.action !== "delete")
8899
+ return json({ error: "Missing ids or invalid action" }, 400, port);
8900
+ const { deleteAgent: deleteAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
8901
+ let succeeded = 0;
8902
+ for (const id of body.ids) {
8903
+ if (deleteAgent2(id))
8904
+ succeeded++;
8905
+ }
8906
+ return json({ succeeded, failed: body.ids.length - succeeded }, 200, port);
8907
+ } catch (e) {
8908
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
8909
+ }
8910
+ }
8911
+ if (path === "/api/projects/bulk" && method === "POST") {
8912
+ try {
8913
+ const body = await req.json();
8914
+ if (!body.ids?.length || body.action !== "delete")
8915
+ return json({ error: "Missing ids or invalid action" }, 400, port);
8916
+ const { deleteProject: deleteProject2 } = await Promise.resolve().then(() => (init_projects(), exports_projects));
8917
+ let succeeded = 0;
8918
+ for (const id of body.ids) {
8919
+ if (deleteProject2(id))
8920
+ succeeded++;
8921
+ }
8922
+ return json({ succeeded, failed: body.ids.length - succeeded }, 200, port);
8923
+ } catch (e) {
8924
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
8925
+ }
8926
+ }
8927
+ if (dashboardExists && (method === "GET" || method === "HEAD")) {
8928
+ if (path !== "/") {
8929
+ const filePath = join6(dashboardDir, path);
8930
+ const res2 = serveStaticFile(filePath);
8931
+ if (res2)
8932
+ return res2;
8933
+ }
8934
+ const indexPath = join6(dashboardDir, "index.html");
8935
+ const res = serveStaticFile(indexPath);
8936
+ if (res)
8937
+ return res;
8938
+ }
8939
+ return json({ error: "Not found" }, 404, port);
8940
+ }
8941
+ });
8942
+ const shutdown = () => {
8943
+ server2.stop();
8944
+ process.exit(0);
8945
+ };
8946
+ process.on("SIGINT", shutdown);
8947
+ process.on("SIGTERM", shutdown);
8948
+ const serverUrl = `http://localhost:${port}`;
8949
+ console.log(`Todos Dashboard running at ${serverUrl}`);
8950
+ if (shouldOpen) {
8951
+ try {
8952
+ const { exec } = await import("child_process");
8953
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
8954
+ exec(`${openCmd} ${serverUrl}`);
8955
+ } catch {}
8956
+ }
8957
+ }
8958
+ var MIME_TYPES, SECURITY_HEADERS;
8959
+ var init_serve = __esm(() => {
8960
+ init_tasks();
8961
+ init_projects();
8962
+ init_agents();
8963
+ init_database();
8964
+ MIME_TYPES = {
8965
+ ".html": "text/html; charset=utf-8",
8966
+ ".js": "application/javascript",
8967
+ ".css": "text/css",
8968
+ ".json": "application/json",
8969
+ ".png": "image/png",
8970
+ ".jpg": "image/jpeg",
8971
+ ".svg": "image/svg+xml",
8972
+ ".ico": "image/x-icon",
8973
+ ".woff": "font/woff",
8974
+ ".woff2": "font/woff2"
8975
+ };
8976
+ SECURITY_HEADERS = {
8977
+ "X-Content-Type-Options": "nosniff",
8978
+ "X-Frame-Options": "DENY"
8979
+ };
8980
+ });
8981
+
8464
8982
  // src/cli/components/Header.tsx
8465
8983
  import { Box, Text } from "ink";
8466
8984
  import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
@@ -9547,12 +10065,12 @@ init_sync();
9547
10065
  init_config();
9548
10066
  import chalk from "chalk";
9549
10067
  import { execSync } from "child_process";
9550
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
9551
- import { basename, dirname as dirname2, join as join6, resolve as resolve2 } from "path";
9552
- import { fileURLToPath } from "url";
10068
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
10069
+ import { basename, dirname as dirname3, join as join7, resolve as resolve2 } from "path";
10070
+ import { fileURLToPath as fileURLToPath2 } from "url";
9553
10071
  function getPackageVersion() {
9554
10072
  try {
9555
- const pkgPath = join6(dirname2(fileURLToPath(import.meta.url)), "..", "..", "package.json");
10073
+ const pkgPath = join7(dirname3(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
9556
10074
  return JSON.parse(readFileSync3(pkgPath, "utf-8")).version || "0.0.0";
9557
10075
  } catch {
9558
10076
  return "0.0.0";
@@ -10204,8 +10722,8 @@ hooks.command("install").description("Install Claude Code hooks for auto-sync").
10204
10722
  if (p)
10205
10723
  todosBin = p;
10206
10724
  } catch {}
10207
- const hooksDir = join6(process.cwd(), ".claude", "hooks");
10208
- if (!existsSync6(hooksDir))
10725
+ const hooksDir = join7(process.cwd(), ".claude", "hooks");
10726
+ if (!existsSync7(hooksDir))
10209
10727
  mkdirSync3(hooksDir, { recursive: true });
10210
10728
  const hookScript = `#!/usr/bin/env bash
10211
10729
  # Auto-generated by: todos hooks install
@@ -10230,11 +10748,11 @@ esac
10230
10748
 
10231
10749
  exit 0
10232
10750
  `;
10233
- const hookPath = join6(hooksDir, "todos-sync.sh");
10751
+ const hookPath = join7(hooksDir, "todos-sync.sh");
10234
10752
  writeFileSync3(hookPath, hookScript);
10235
10753
  execSync(`chmod +x "${hookPath}"`);
10236
10754
  console.log(chalk.green(`Hook script created: ${hookPath}`));
10237
- const settingsPath = join6(process.cwd(), ".claude", "settings.json");
10755
+ const settingsPath = join7(process.cwd(), ".claude", "settings.json");
10238
10756
  const settings = readJsonFile2(settingsPath);
10239
10757
  if (!settings["hooks"]) {
10240
10758
  settings["hooks"] = {};
@@ -10281,13 +10799,13 @@ function getMcpBinaryPath() {
10281
10799
  if (p)
10282
10800
  return p;
10283
10801
  } catch {}
10284
- const bunBin = join6(HOME2, ".bun", "bin", "todos-mcp");
10285
- if (existsSync6(bunBin))
10802
+ const bunBin = join7(HOME2, ".bun", "bin", "todos-mcp");
10803
+ if (existsSync7(bunBin))
10286
10804
  return bunBin;
10287
10805
  return "todos-mcp";
10288
10806
  }
10289
10807
  function readJsonFile2(path) {
10290
- if (!existsSync6(path))
10808
+ if (!existsSync7(path))
10291
10809
  return {};
10292
10810
  try {
10293
10811
  return JSON.parse(readFileSync3(path, "utf-8"));
@@ -10296,25 +10814,25 @@ function readJsonFile2(path) {
10296
10814
  }
10297
10815
  }
10298
10816
  function writeJsonFile2(path, data) {
10299
- const dir = dirname2(path);
10300
- if (!existsSync6(dir))
10817
+ const dir = dirname3(path);
10818
+ if (!existsSync7(dir))
10301
10819
  mkdirSync3(dir, { recursive: true });
10302
10820
  writeFileSync3(path, JSON.stringify(data, null, 2) + `
10303
10821
  `);
10304
10822
  }
10305
10823
  function readTomlFile(path) {
10306
- if (!existsSync6(path))
10824
+ if (!existsSync7(path))
10307
10825
  return "";
10308
10826
  return readFileSync3(path, "utf-8");
10309
10827
  }
10310
10828
  function writeTomlFile(path, content) {
10311
- const dir = dirname2(path);
10312
- if (!existsSync6(dir))
10829
+ const dir = dirname3(path);
10830
+ if (!existsSync7(dir))
10313
10831
  mkdirSync3(dir, { recursive: true });
10314
10832
  writeFileSync3(path, content);
10315
10833
  }
10316
10834
  function registerClaude(binPath, global) {
10317
- const configPath = global ? join6(HOME2, ".claude", ".mcp.json") : join6(process.cwd(), ".mcp.json");
10835
+ const configPath = global ? join7(HOME2, ".claude", ".mcp.json") : join7(process.cwd(), ".mcp.json");
10318
10836
  const config = readJsonFile2(configPath);
10319
10837
  if (!config["mcpServers"]) {
10320
10838
  config["mcpServers"] = {};
@@ -10329,7 +10847,7 @@ function registerClaude(binPath, global) {
10329
10847
  console.log(chalk.green(`Claude Code (${scope}): registered in ${configPath}`));
10330
10848
  }
10331
10849
  function unregisterClaude(global) {
10332
- const configPath = global ? join6(HOME2, ".claude", ".mcp.json") : join6(process.cwd(), ".mcp.json");
10850
+ const configPath = global ? join7(HOME2, ".claude", ".mcp.json") : join7(process.cwd(), ".mcp.json");
10333
10851
  const config = readJsonFile2(configPath);
10334
10852
  const servers = config["mcpServers"];
10335
10853
  if (!servers || !("todos" in servers)) {
@@ -10342,7 +10860,7 @@ function unregisterClaude(global) {
10342
10860
  console.log(chalk.green(`Claude Code (${scope}): unregistered from ${configPath}`));
10343
10861
  }
10344
10862
  function registerCodex(binPath) {
10345
- const configPath = join6(HOME2, ".codex", "config.toml");
10863
+ const configPath = join7(HOME2, ".codex", "config.toml");
10346
10864
  let content = readTomlFile(configPath);
10347
10865
  content = removeTomlBlock(content, "mcp_servers.todos");
10348
10866
  const block = `
@@ -10356,7 +10874,7 @@ args = []
10356
10874
  console.log(chalk.green(`Codex CLI: registered in ${configPath}`));
10357
10875
  }
10358
10876
  function unregisterCodex() {
10359
- const configPath = join6(HOME2, ".codex", "config.toml");
10877
+ const configPath = join7(HOME2, ".codex", "config.toml");
10360
10878
  let content = readTomlFile(configPath);
10361
10879
  if (!content.includes("[mcp_servers.todos]")) {
10362
10880
  console.log(chalk.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -10389,7 +10907,7 @@ function removeTomlBlock(content, blockName) {
10389
10907
  `);
10390
10908
  }
10391
10909
  function registerGemini(binPath) {
10392
- const configPath = join6(HOME2, ".gemini", "settings.json");
10910
+ const configPath = join7(HOME2, ".gemini", "settings.json");
10393
10911
  const config = readJsonFile2(configPath);
10394
10912
  if (!config["mcpServers"]) {
10395
10913
  config["mcpServers"] = {};
@@ -10403,7 +10921,7 @@ function registerGemini(binPath) {
10403
10921
  console.log(chalk.green(`Gemini CLI: registered in ${configPath}`));
10404
10922
  }
10405
10923
  function unregisterGemini() {
10406
- const configPath = join6(HOME2, ".gemini", "settings.json");
10924
+ const configPath = join7(HOME2, ".gemini", "settings.json");
10407
10925
  const config = readJsonFile2(configPath);
10408
10926
  const servers = config["mcpServers"];
10409
10927
  if (!servers || !("todos" in servers)) {
@@ -10567,6 +11085,10 @@ Updated to ${latestVersion}!`));
10567
11085
  handleError(e);
10568
11086
  }
10569
11087
  });
11088
+ program2.command("serve").description("Start the web dashboard").option("--port <port>", "Port number", "19427").option("--no-open", "Don't open browser automatically").action(async (opts) => {
11089
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
11090
+ await startServer2(parseInt(opts.port, 10), { open: opts.open !== false });
11091
+ });
10570
11092
  program2.command("interactive").description("Launch interactive TUI").action(async () => {
10571
11093
  const { renderApp: renderApp2 } = await Promise.resolve().then(() => (init_App(), exports_App));
10572
11094
  const globalOpts = program2.opts();