@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/dashboard/dist/assets/index-BiBRhfMn.js +311 -0
- package/dashboard/dist/assets/index-gYTrU_vV.css +1 -0
- package/dashboard/dist/index.html +13 -0
- package/dashboard/dist/logo.jpg +0 -0
- package/dist/cli/index.js +655 -133
- package/dist/index.js +194 -103
- package/dist/mcp/index.js +200 -108
- package/dist/server/index.js +1285 -0
- package/package.json +11 -5
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
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
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
|
|
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
|
|
9551
|
-
import { basename, dirname as
|
|
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 =
|
|
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 =
|
|
10208
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
10285
|
-
if (
|
|
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 (!
|
|
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 =
|
|
10300
|
-
if (!
|
|
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 (!
|
|
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 =
|
|
10312
|
-
if (!
|
|
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 ?
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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();
|