@hasna/todos 0.3.5 → 0.4.0
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/README.md +33 -1
- package/dist/cli/index.js +1649 -485
- package/dist/index.d.ts +42 -0
- package/dist/index.js +845 -31
- package/dist/mcp/index.js +853 -78
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2067,19 +2067,54 @@ var require_commander = __commonJS((exports) => {
|
|
|
2067
2067
|
import { Database } from "bun:sqlite";
|
|
2068
2068
|
import { existsSync, mkdirSync } from "fs";
|
|
2069
2069
|
import { dirname, join, resolve } from "path";
|
|
2070
|
+
function isInMemoryDb(path) {
|
|
2071
|
+
return path === ":memory:" || path.startsWith("file::memory:");
|
|
2072
|
+
}
|
|
2073
|
+
function findNearestTodosDb(startDir) {
|
|
2074
|
+
let dir = resolve(startDir);
|
|
2075
|
+
while (true) {
|
|
2076
|
+
const candidate = join(dir, ".todos", "todos.db");
|
|
2077
|
+
if (existsSync(candidate))
|
|
2078
|
+
return candidate;
|
|
2079
|
+
const parent = dirname(dir);
|
|
2080
|
+
if (parent === dir)
|
|
2081
|
+
break;
|
|
2082
|
+
dir = parent;
|
|
2083
|
+
}
|
|
2084
|
+
return null;
|
|
2085
|
+
}
|
|
2086
|
+
function findGitRoot(startDir) {
|
|
2087
|
+
let dir = resolve(startDir);
|
|
2088
|
+
while (true) {
|
|
2089
|
+
if (existsSync(join(dir, ".git")))
|
|
2090
|
+
return dir;
|
|
2091
|
+
const parent = dirname(dir);
|
|
2092
|
+
if (parent === dir)
|
|
2093
|
+
break;
|
|
2094
|
+
dir = parent;
|
|
2095
|
+
}
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2070
2098
|
function getDbPath() {
|
|
2071
2099
|
if (process.env["TODOS_DB_PATH"]) {
|
|
2072
2100
|
return process.env["TODOS_DB_PATH"];
|
|
2073
2101
|
}
|
|
2074
2102
|
const cwd = process.cwd();
|
|
2075
|
-
const
|
|
2076
|
-
if (
|
|
2077
|
-
return
|
|
2103
|
+
const nearest = findNearestTodosDb(cwd);
|
|
2104
|
+
if (nearest)
|
|
2105
|
+
return nearest;
|
|
2106
|
+
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
2107
|
+
const gitRoot = findGitRoot(cwd);
|
|
2108
|
+
if (gitRoot) {
|
|
2109
|
+
return join(gitRoot, ".todos", "todos.db");
|
|
2110
|
+
}
|
|
2078
2111
|
}
|
|
2079
2112
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2080
2113
|
return join(home, ".todos", "todos.db");
|
|
2081
2114
|
}
|
|
2082
2115
|
function ensureDir(filePath) {
|
|
2116
|
+
if (isInMemoryDb(filePath))
|
|
2117
|
+
return;
|
|
2083
2118
|
const dir = dirname(resolve(filePath));
|
|
2084
2119
|
if (!existsSync(dir)) {
|
|
2085
2120
|
mkdirSync(dir, { recursive: true });
|
|
@@ -2095,6 +2130,7 @@ function getDatabase(dbPath) {
|
|
|
2095
2130
|
_db.run("PRAGMA busy_timeout = 5000");
|
|
2096
2131
|
_db.run("PRAGMA foreign_keys = ON");
|
|
2097
2132
|
runMigrations(_db);
|
|
2133
|
+
backfillTaskTags(_db);
|
|
2098
2134
|
return _db;
|
|
2099
2135
|
}
|
|
2100
2136
|
function runMigrations(db) {
|
|
@@ -2110,6 +2146,35 @@ function runMigrations(db) {
|
|
|
2110
2146
|
}
|
|
2111
2147
|
}
|
|
2112
2148
|
}
|
|
2149
|
+
function backfillTaskTags(db) {
|
|
2150
|
+
try {
|
|
2151
|
+
const count = db.query("SELECT COUNT(*) as count FROM task_tags").get();
|
|
2152
|
+
if (count && count.count > 0)
|
|
2153
|
+
return;
|
|
2154
|
+
} catch {
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
try {
|
|
2158
|
+
const rows = db.query("SELECT id, tags FROM tasks WHERE tags IS NOT NULL AND tags != '[]'").all();
|
|
2159
|
+
if (rows.length === 0)
|
|
2160
|
+
return;
|
|
2161
|
+
const insert = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
2162
|
+
for (const row of rows) {
|
|
2163
|
+
if (!row.tags)
|
|
2164
|
+
continue;
|
|
2165
|
+
let tags = [];
|
|
2166
|
+
try {
|
|
2167
|
+
tags = JSON.parse(row.tags);
|
|
2168
|
+
} catch {
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
for (const tag of tags) {
|
|
2172
|
+
if (tag)
|
|
2173
|
+
insert.run(row.id, tag);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
} catch {}
|
|
2177
|
+
}
|
|
2113
2178
|
function now() {
|
|
2114
2179
|
return new Date().toISOString();
|
|
2115
2180
|
}
|
|
@@ -2123,6 +2188,14 @@ function isLockExpired(lockedAt) {
|
|
|
2123
2188
|
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
2124
2189
|
return Date.now() - lockTime > expiryMs;
|
|
2125
2190
|
}
|
|
2191
|
+
function lockExpiryCutoff(nowMs = Date.now()) {
|
|
2192
|
+
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
2193
|
+
return new Date(nowMs - expiryMs).toISOString();
|
|
2194
|
+
}
|
|
2195
|
+
function clearExpiredLocks(db) {
|
|
2196
|
+
const cutoff = lockExpiryCutoff();
|
|
2197
|
+
db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
|
|
2198
|
+
}
|
|
2126
2199
|
function resolvePartialId(db, table, partialId) {
|
|
2127
2200
|
if (partialId.length >= 36) {
|
|
2128
2201
|
const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
|
|
@@ -2215,12 +2288,43 @@ var init_database = __esm(() => {
|
|
|
2215
2288
|
);
|
|
2216
2289
|
|
|
2217
2290
|
INSERT OR IGNORE INTO _migrations (id) VALUES (1);
|
|
2291
|
+
`,
|
|
2292
|
+
`
|
|
2293
|
+
ALTER TABLE projects ADD COLUMN task_list_id TEXT;
|
|
2294
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (2);
|
|
2295
|
+
`,
|
|
2296
|
+
`
|
|
2297
|
+
CREATE TABLE IF NOT EXISTS task_tags (
|
|
2298
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
2299
|
+
tag TEXT NOT NULL,
|
|
2300
|
+
PRIMARY KEY (task_id, tag)
|
|
2301
|
+
);
|
|
2302
|
+
CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag);
|
|
2303
|
+
CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id);
|
|
2304
|
+
|
|
2305
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (3);
|
|
2306
|
+
`,
|
|
2307
|
+
`
|
|
2308
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
2309
|
+
id TEXT PRIMARY KEY,
|
|
2310
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
2311
|
+
name TEXT NOT NULL,
|
|
2312
|
+
description TEXT,
|
|
2313
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
2314
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2315
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2316
|
+
);
|
|
2317
|
+
CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id);
|
|
2318
|
+
CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status);
|
|
2319
|
+
ALTER TABLE tasks ADD COLUMN plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL;
|
|
2320
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id);
|
|
2321
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (4);
|
|
2218
2322
|
`
|
|
2219
2323
|
];
|
|
2220
2324
|
});
|
|
2221
2325
|
|
|
2222
2326
|
// src/types/index.ts
|
|
2223
|
-
var VersionConflictError, TaskNotFoundError, LockError, DependencyCycleError;
|
|
2327
|
+
var VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, DependencyCycleError;
|
|
2224
2328
|
var init_types = __esm(() => {
|
|
2225
2329
|
VersionConflictError = class VersionConflictError extends Error {
|
|
2226
2330
|
taskId;
|
|
@@ -2242,6 +2346,22 @@ var init_types = __esm(() => {
|
|
|
2242
2346
|
this.name = "TaskNotFoundError";
|
|
2243
2347
|
}
|
|
2244
2348
|
};
|
|
2349
|
+
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
2350
|
+
projectId;
|
|
2351
|
+
constructor(projectId) {
|
|
2352
|
+
super(`Project not found: ${projectId}`);
|
|
2353
|
+
this.projectId = projectId;
|
|
2354
|
+
this.name = "ProjectNotFoundError";
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
2358
|
+
planId;
|
|
2359
|
+
constructor(planId) {
|
|
2360
|
+
super(`Plan not found: ${planId}`);
|
|
2361
|
+
this.planId = planId;
|
|
2362
|
+
this.name = "PlanNotFoundError";
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2245
2365
|
LockError = class LockError extends Error {
|
|
2246
2366
|
taskId;
|
|
2247
2367
|
lockedBy;
|
|
@@ -2274,15 +2394,30 @@ function rowToTask(row) {
|
|
|
2274
2394
|
priority: row.priority
|
|
2275
2395
|
};
|
|
2276
2396
|
}
|
|
2397
|
+
function insertTaskTags(taskId, tags, db) {
|
|
2398
|
+
if (tags.length === 0)
|
|
2399
|
+
return;
|
|
2400
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
2401
|
+
for (const tag of tags) {
|
|
2402
|
+
if (tag)
|
|
2403
|
+
stmt.run(taskId, tag);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
function replaceTaskTags(taskId, tags, db) {
|
|
2407
|
+
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
2408
|
+
insertTaskTags(taskId, tags, db);
|
|
2409
|
+
}
|
|
2277
2410
|
function createTask(input, db) {
|
|
2278
2411
|
const d = db || getDatabase();
|
|
2279
2412
|
const id = uuid();
|
|
2280
2413
|
const timestamp = now();
|
|
2281
|
-
|
|
2282
|
-
|
|
2414
|
+
const tags = input.tags || [];
|
|
2415
|
+
d.run(`INSERT INTO tasks (id, project_id, parent_id, plan_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at)
|
|
2416
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)`, [
|
|
2283
2417
|
id,
|
|
2284
2418
|
input.project_id || null,
|
|
2285
2419
|
input.parent_id || null,
|
|
2420
|
+
input.plan_id || null,
|
|
2286
2421
|
input.title,
|
|
2287
2422
|
input.description || null,
|
|
2288
2423
|
input.status || "pending",
|
|
@@ -2291,11 +2426,14 @@ function createTask(input, db) {
|
|
|
2291
2426
|
input.assigned_to || null,
|
|
2292
2427
|
input.session_id || null,
|
|
2293
2428
|
input.working_dir || null,
|
|
2294
|
-
JSON.stringify(
|
|
2429
|
+
JSON.stringify(tags),
|
|
2295
2430
|
JSON.stringify(input.metadata || {}),
|
|
2296
2431
|
timestamp,
|
|
2297
2432
|
timestamp
|
|
2298
2433
|
]);
|
|
2434
|
+
if (tags.length > 0) {
|
|
2435
|
+
insertTaskTags(id, tags, d);
|
|
2436
|
+
}
|
|
2299
2437
|
return getTask(id, d);
|
|
2300
2438
|
}
|
|
2301
2439
|
function getTask(id, db) {
|
|
@@ -2333,6 +2471,7 @@ function getTaskWithRelations(id, db) {
|
|
|
2333
2471
|
}
|
|
2334
2472
|
function listTasks(filter = {}, db) {
|
|
2335
2473
|
const d = db || getDatabase();
|
|
2474
|
+
clearExpiredLocks(d);
|
|
2336
2475
|
const conditions = [];
|
|
2337
2476
|
const params = [];
|
|
2338
2477
|
if (filter.project_id) {
|
|
@@ -2378,9 +2517,13 @@ function listTasks(filter = {}, db) {
|
|
|
2378
2517
|
params.push(filter.session_id);
|
|
2379
2518
|
}
|
|
2380
2519
|
if (filter.tags && filter.tags.length > 0) {
|
|
2381
|
-
const
|
|
2382
|
-
conditions.push(`(${
|
|
2383
|
-
params.push(...filter.tags
|
|
2520
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
2521
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
2522
|
+
params.push(...filter.tags);
|
|
2523
|
+
}
|
|
2524
|
+
if (filter.plan_id) {
|
|
2525
|
+
conditions.push("plan_id = ?");
|
|
2526
|
+
params.push(filter.plan_id);
|
|
2384
2527
|
}
|
|
2385
2528
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2386
2529
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
|
|
@@ -2430,12 +2573,19 @@ function updateTask(id, input, db) {
|
|
|
2430
2573
|
sets.push("metadata = ?");
|
|
2431
2574
|
params.push(JSON.stringify(input.metadata));
|
|
2432
2575
|
}
|
|
2576
|
+
if (input.plan_id !== undefined) {
|
|
2577
|
+
sets.push("plan_id = ?");
|
|
2578
|
+
params.push(input.plan_id);
|
|
2579
|
+
}
|
|
2433
2580
|
params.push(id, input.version);
|
|
2434
2581
|
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
2435
2582
|
if (result.changes === 0) {
|
|
2436
2583
|
const current = getTask(id, d);
|
|
2437
2584
|
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
2438
2585
|
}
|
|
2586
|
+
if (input.tags !== undefined) {
|
|
2587
|
+
replaceTaskTags(id, input.tags, d);
|
|
2588
|
+
}
|
|
2439
2589
|
return getTask(id, d);
|
|
2440
2590
|
}
|
|
2441
2591
|
function deleteTask(id, db) {
|
|
@@ -2445,15 +2595,18 @@ function deleteTask(id, db) {
|
|
|
2445
2595
|
}
|
|
2446
2596
|
function startTask(id, agentId, db) {
|
|
2447
2597
|
const d = db || getDatabase();
|
|
2448
|
-
const
|
|
2449
|
-
if (!task)
|
|
2450
|
-
throw new TaskNotFoundError(id);
|
|
2451
|
-
if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
2452
|
-
throw new LockError(id, task.locked_by);
|
|
2453
|
-
}
|
|
2598
|
+
const cutoff = lockExpiryCutoff();
|
|
2454
2599
|
const timestamp = now();
|
|
2455
|
-
d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
2456
|
-
WHERE id =
|
|
2600
|
+
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
2601
|
+
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
2602
|
+
if (result.changes === 0) {
|
|
2603
|
+
const current = getTask(id, d);
|
|
2604
|
+
if (!current)
|
|
2605
|
+
throw new TaskNotFoundError(id);
|
|
2606
|
+
if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
|
|
2607
|
+
throw new LockError(id, current.locked_by);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2457
2610
|
return getTask(id, d);
|
|
2458
2611
|
}
|
|
2459
2612
|
function completeTask(id, agentId, db) {
|
|
@@ -2474,20 +2627,26 @@ function lockTask(id, agentId, db) {
|
|
|
2474
2627
|
const task = getTask(id, d);
|
|
2475
2628
|
if (!task)
|
|
2476
2629
|
throw new TaskNotFoundError(id);
|
|
2477
|
-
if (task.locked_by === agentId) {
|
|
2630
|
+
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
2478
2631
|
return { success: true, locked_by: agentId, locked_at: task.locked_at };
|
|
2479
2632
|
}
|
|
2480
|
-
|
|
2481
|
-
return {
|
|
2482
|
-
success: false,
|
|
2483
|
-
locked_by: task.locked_by,
|
|
2484
|
-
locked_at: task.locked_at,
|
|
2485
|
-
error: `Task is locked by ${task.locked_by}`
|
|
2486
|
-
};
|
|
2487
|
-
}
|
|
2633
|
+
const cutoff = lockExpiryCutoff();
|
|
2488
2634
|
const timestamp = now();
|
|
2489
|
-
d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
2490
|
-
WHERE id =
|
|
2635
|
+
const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
2636
|
+
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
2637
|
+
if (result.changes === 0) {
|
|
2638
|
+
const current = getTask(id, d);
|
|
2639
|
+
if (!current)
|
|
2640
|
+
throw new TaskNotFoundError(id);
|
|
2641
|
+
if (current.locked_by && !isLockExpired(current.locked_at)) {
|
|
2642
|
+
return {
|
|
2643
|
+
success: false,
|
|
2644
|
+
locked_by: current.locked_by,
|
|
2645
|
+
locked_at: current.locked_at,
|
|
2646
|
+
error: `Task is locked by ${current.locked_by}`
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2491
2650
|
return { success: true, locked_by: agentId, locked_at: timestamp };
|
|
2492
2651
|
}
|
|
2493
2652
|
function unlockTask(id, agentId, db) {
|
|
@@ -2542,12 +2701,16 @@ var init_tasks = __esm(() => {
|
|
|
2542
2701
|
});
|
|
2543
2702
|
|
|
2544
2703
|
// src/db/projects.ts
|
|
2704
|
+
function slugify(name) {
|
|
2705
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
2706
|
+
}
|
|
2545
2707
|
function createProject(input, db) {
|
|
2546
2708
|
const d = db || getDatabase();
|
|
2547
2709
|
const id = uuid();
|
|
2548
2710
|
const timestamp = now();
|
|
2549
|
-
|
|
2550
|
-
|
|
2711
|
+
const taskListId = input.task_list_id ?? `todos-${slugify(input.name)}`;
|
|
2712
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, created_at, updated_at)
|
|
2713
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, timestamp, timestamp]);
|
|
2551
2714
|
return getProject(id, d);
|
|
2552
2715
|
}
|
|
2553
2716
|
function getProject(id, db) {
|
|
@@ -2564,6 +2727,34 @@ function listProjects(db) {
|
|
|
2564
2727
|
const d = db || getDatabase();
|
|
2565
2728
|
return d.query("SELECT * FROM projects ORDER BY name").all();
|
|
2566
2729
|
}
|
|
2730
|
+
function updateProject(id, input, db) {
|
|
2731
|
+
const d = db || getDatabase();
|
|
2732
|
+
const project = getProject(id, d);
|
|
2733
|
+
if (!project)
|
|
2734
|
+
throw new ProjectNotFoundError(id);
|
|
2735
|
+
const sets = ["updated_at = ?"];
|
|
2736
|
+
const params = [now()];
|
|
2737
|
+
if (input.name !== undefined) {
|
|
2738
|
+
sets.push("name = ?");
|
|
2739
|
+
params.push(input.name);
|
|
2740
|
+
}
|
|
2741
|
+
if (input.description !== undefined) {
|
|
2742
|
+
sets.push("description = ?");
|
|
2743
|
+
params.push(input.description);
|
|
2744
|
+
}
|
|
2745
|
+
if (input.task_list_id !== undefined) {
|
|
2746
|
+
sets.push("task_list_id = ?");
|
|
2747
|
+
params.push(input.task_list_id);
|
|
2748
|
+
}
|
|
2749
|
+
params.push(id);
|
|
2750
|
+
d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
2751
|
+
return getProject(id, d);
|
|
2752
|
+
}
|
|
2753
|
+
function deleteProject(id, db) {
|
|
2754
|
+
const d = db || getDatabase();
|
|
2755
|
+
const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
|
|
2756
|
+
return result.changes > 0;
|
|
2757
|
+
}
|
|
2567
2758
|
function ensureProject(name, path, db) {
|
|
2568
2759
|
const d = db || getDatabase();
|
|
2569
2760
|
const existing = getProjectByPath(path, d);
|
|
@@ -2576,6 +2767,68 @@ var init_projects = __esm(() => {
|
|
|
2576
2767
|
init_database();
|
|
2577
2768
|
});
|
|
2578
2769
|
|
|
2770
|
+
// src/db/plans.ts
|
|
2771
|
+
function createPlan(input, db) {
|
|
2772
|
+
const d = db || getDatabase();
|
|
2773
|
+
const id = uuid();
|
|
2774
|
+
const timestamp = now();
|
|
2775
|
+
d.run(`INSERT INTO plans (id, project_id, name, description, status, created_at, updated_at)
|
|
2776
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
2777
|
+
id,
|
|
2778
|
+
input.project_id || null,
|
|
2779
|
+
input.name,
|
|
2780
|
+
input.description || null,
|
|
2781
|
+
input.status || "active",
|
|
2782
|
+
timestamp,
|
|
2783
|
+
timestamp
|
|
2784
|
+
]);
|
|
2785
|
+
return getPlan(id, d);
|
|
2786
|
+
}
|
|
2787
|
+
function getPlan(id, db) {
|
|
2788
|
+
const d = db || getDatabase();
|
|
2789
|
+
const row = d.query("SELECT * FROM plans WHERE id = ?").get(id);
|
|
2790
|
+
return row;
|
|
2791
|
+
}
|
|
2792
|
+
function listPlans(projectId, db) {
|
|
2793
|
+
const d = db || getDatabase();
|
|
2794
|
+
if (projectId) {
|
|
2795
|
+
return d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
|
|
2796
|
+
}
|
|
2797
|
+
return d.query("SELECT * FROM plans ORDER BY created_at DESC").all();
|
|
2798
|
+
}
|
|
2799
|
+
function updatePlan(id, input, db) {
|
|
2800
|
+
const d = db || getDatabase();
|
|
2801
|
+
const plan = getPlan(id, d);
|
|
2802
|
+
if (!plan)
|
|
2803
|
+
throw new PlanNotFoundError(id);
|
|
2804
|
+
const sets = ["updated_at = ?"];
|
|
2805
|
+
const params = [now()];
|
|
2806
|
+
if (input.name !== undefined) {
|
|
2807
|
+
sets.push("name = ?");
|
|
2808
|
+
params.push(input.name);
|
|
2809
|
+
}
|
|
2810
|
+
if (input.description !== undefined) {
|
|
2811
|
+
sets.push("description = ?");
|
|
2812
|
+
params.push(input.description);
|
|
2813
|
+
}
|
|
2814
|
+
if (input.status !== undefined) {
|
|
2815
|
+
sets.push("status = ?");
|
|
2816
|
+
params.push(input.status);
|
|
2817
|
+
}
|
|
2818
|
+
params.push(id);
|
|
2819
|
+
d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
2820
|
+
return getPlan(id, d);
|
|
2821
|
+
}
|
|
2822
|
+
function deletePlan(id, db) {
|
|
2823
|
+
const d = db || getDatabase();
|
|
2824
|
+
const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
|
|
2825
|
+
return result.changes > 0;
|
|
2826
|
+
}
|
|
2827
|
+
var init_plans = __esm(() => {
|
|
2828
|
+
init_types();
|
|
2829
|
+
init_database();
|
|
2830
|
+
});
|
|
2831
|
+
|
|
2579
2832
|
// src/db/comments.ts
|
|
2580
2833
|
function addComment(input, db) {
|
|
2581
2834
|
const d = db || getDatabase();
|
|
@@ -2621,8 +2874,9 @@ function rowToTask2(row) {
|
|
|
2621
2874
|
}
|
|
2622
2875
|
function searchTasks(query, projectId, db) {
|
|
2623
2876
|
const d = db || getDatabase();
|
|
2877
|
+
clearExpiredLocks(d);
|
|
2624
2878
|
const pattern = `%${query}%`;
|
|
2625
|
-
let sql = `SELECT * FROM tasks WHERE (title LIKE ? OR description LIKE ? OR
|
|
2879
|
+
let sql = `SELECT * FROM tasks WHERE (title LIKE ? OR description LIKE ? OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = tasks.id AND tag LIKE ?))`;
|
|
2626
2880
|
const params = [pattern, pattern, pattern];
|
|
2627
2881
|
if (projectId) {
|
|
2628
2882
|
sql += " AND project_id = ?";
|
|
@@ -2638,11 +2892,28 @@ var init_search = __esm(() => {
|
|
|
2638
2892
|
init_database();
|
|
2639
2893
|
});
|
|
2640
2894
|
|
|
2641
|
-
// src/lib/
|
|
2642
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, writeFileSync } from "fs";
|
|
2895
|
+
// src/lib/sync-utils.ts
|
|
2896
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync as readdirSync2, statSync, writeFileSync } from "fs";
|
|
2643
2897
|
import { join as join2 } from "path";
|
|
2644
|
-
function
|
|
2645
|
-
|
|
2898
|
+
function ensureDir2(dir) {
|
|
2899
|
+
if (!existsSync2(dir))
|
|
2900
|
+
mkdirSync2(dir, { recursive: true });
|
|
2901
|
+
}
|
|
2902
|
+
function listJsonFiles(dir) {
|
|
2903
|
+
if (!existsSync2(dir))
|
|
2904
|
+
return [];
|
|
2905
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".json"));
|
|
2906
|
+
}
|
|
2907
|
+
function readJsonFile(path) {
|
|
2908
|
+
try {
|
|
2909
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
2910
|
+
} catch {
|
|
2911
|
+
return null;
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
function writeJsonFile(path, data) {
|
|
2915
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + `
|
|
2916
|
+
`);
|
|
2646
2917
|
}
|
|
2647
2918
|
function readHighWaterMark(dir) {
|
|
2648
2919
|
const path = join2(dir, ".highwatermark");
|
|
@@ -2654,17 +2925,40 @@ function readHighWaterMark(dir) {
|
|
|
2654
2925
|
function writeHighWaterMark(dir, value) {
|
|
2655
2926
|
writeFileSync(join2(dir, ".highwatermark"), String(value));
|
|
2656
2927
|
}
|
|
2657
|
-
function
|
|
2928
|
+
function getFileMtimeMs(path) {
|
|
2658
2929
|
try {
|
|
2659
|
-
|
|
2660
|
-
return JSON.parse(content);
|
|
2930
|
+
return statSync(path).mtimeMs;
|
|
2661
2931
|
} catch {
|
|
2662
2932
|
return null;
|
|
2663
2933
|
}
|
|
2664
2934
|
}
|
|
2935
|
+
function parseTimestamp(value) {
|
|
2936
|
+
if (typeof value !== "string")
|
|
2937
|
+
return null;
|
|
2938
|
+
const parsed = Date.parse(value);
|
|
2939
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
2940
|
+
}
|
|
2941
|
+
function appendSyncConflict(metadata, conflict, limit = 5) {
|
|
2942
|
+
const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
|
|
2943
|
+
const next = [conflict, ...current].slice(0, limit);
|
|
2944
|
+
return { ...metadata, sync_conflicts: next };
|
|
2945
|
+
}
|
|
2946
|
+
var HOME;
|
|
2947
|
+
var init_sync_utils = __esm(() => {
|
|
2948
|
+
HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2949
|
+
});
|
|
2950
|
+
|
|
2951
|
+
// src/lib/claude-tasks.ts
|
|
2952
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2953
|
+
import { join as join3 } from "path";
|
|
2954
|
+
function getTaskListDir(taskListId) {
|
|
2955
|
+
return join3(HOME, ".claude", "tasks", taskListId);
|
|
2956
|
+
}
|
|
2957
|
+
function readClaudeTask(dir, filename) {
|
|
2958
|
+
return readJsonFile(join3(dir, filename));
|
|
2959
|
+
}
|
|
2665
2960
|
function writeClaudeTask(dir, task) {
|
|
2666
|
-
|
|
2667
|
-
`);
|
|
2961
|
+
writeJsonFile(join3(dir, `${task.id}.json`), task);
|
|
2668
2962
|
}
|
|
2669
2963
|
function toClaudeStatus(status) {
|
|
2670
2964
|
if (status === "pending" || status === "in_progress" || status === "completed") {
|
|
@@ -2675,7 +2969,7 @@ function toClaudeStatus(status) {
|
|
|
2675
2969
|
function toSqliteStatus(status) {
|
|
2676
2970
|
return status;
|
|
2677
2971
|
}
|
|
2678
|
-
function taskToClaudeTask(task, claudeTaskId) {
|
|
2972
|
+
function taskToClaudeTask(task, claudeTaskId, existingMeta) {
|
|
2679
2973
|
return {
|
|
2680
2974
|
id: claudeTaskId,
|
|
2681
2975
|
subject: task.title,
|
|
@@ -2686,39 +2980,80 @@ function taskToClaudeTask(task, claudeTaskId) {
|
|
|
2686
2980
|
blocks: [],
|
|
2687
2981
|
blockedBy: [],
|
|
2688
2982
|
metadata: {
|
|
2983
|
+
...existingMeta || {},
|
|
2689
2984
|
todos_id: task.id,
|
|
2690
|
-
priority: task.priority
|
|
2985
|
+
priority: task.priority,
|
|
2986
|
+
todos_updated_at: task.updated_at,
|
|
2987
|
+
todos_version: task.version
|
|
2691
2988
|
}
|
|
2692
2989
|
};
|
|
2693
2990
|
}
|
|
2694
|
-
function pushToClaudeTaskList(taskListId, projectId) {
|
|
2991
|
+
function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
2695
2992
|
const dir = getTaskListDir(taskListId);
|
|
2696
|
-
if (!
|
|
2697
|
-
|
|
2993
|
+
if (!existsSync3(dir))
|
|
2994
|
+
ensureDir2(dir);
|
|
2698
2995
|
const filter = {};
|
|
2699
2996
|
if (projectId)
|
|
2700
2997
|
filter["project_id"] = projectId;
|
|
2701
2998
|
const tasks = listTasks(filter);
|
|
2702
2999
|
const existingByTodosId = new Map;
|
|
2703
|
-
const files =
|
|
3000
|
+
const files = listJsonFiles(dir);
|
|
2704
3001
|
for (const f of files) {
|
|
3002
|
+
const path = join3(dir, f);
|
|
2705
3003
|
const ct = readClaudeTask(dir, f);
|
|
2706
3004
|
if (ct?.metadata?.["todos_id"]) {
|
|
2707
|
-
existingByTodosId.set(ct.metadata["todos_id"], ct);
|
|
3005
|
+
existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
|
|
2708
3006
|
}
|
|
2709
3007
|
}
|
|
2710
3008
|
let hwm = readHighWaterMark(dir);
|
|
2711
3009
|
let pushed = 0;
|
|
2712
3010
|
const errors = [];
|
|
3011
|
+
const prefer = options.prefer || "remote";
|
|
2713
3012
|
for (const task of tasks) {
|
|
2714
3013
|
try {
|
|
2715
3014
|
const existing = existingByTodosId.get(task.id);
|
|
2716
3015
|
if (existing) {
|
|
2717
|
-
const
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
3016
|
+
const lastSyncedAt = parseTimestamp(existing.task.metadata?.["todos_updated_at"]);
|
|
3017
|
+
const localUpdatedAt = parseTimestamp(task.updated_at);
|
|
3018
|
+
const remoteUpdatedAt = existing.mtimeMs;
|
|
3019
|
+
let recordConflict = false;
|
|
3020
|
+
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
3021
|
+
if (prefer === "remote") {
|
|
3022
|
+
const conflict = {
|
|
3023
|
+
agent: "claude",
|
|
3024
|
+
direction: "push",
|
|
3025
|
+
prefer,
|
|
3026
|
+
local_updated_at: task.updated_at,
|
|
3027
|
+
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
3028
|
+
detected_at: new Date().toISOString()
|
|
3029
|
+
};
|
|
3030
|
+
const newMeta = appendSyncConflict(task.metadata, conflict);
|
|
3031
|
+
updateTask(task.id, { version: task.version, metadata: newMeta });
|
|
3032
|
+
errors.push(`conflict push ${task.id}: remote newer`);
|
|
3033
|
+
continue;
|
|
3034
|
+
}
|
|
3035
|
+
recordConflict = true;
|
|
3036
|
+
}
|
|
3037
|
+
const updated = taskToClaudeTask(task, existing.task.id, existing.task.metadata);
|
|
3038
|
+
updated.blocks = existing.task.blocks;
|
|
3039
|
+
updated.blockedBy = existing.task.blockedBy;
|
|
3040
|
+
updated.activeForm = existing.task.activeForm;
|
|
2721
3041
|
writeClaudeTask(dir, updated);
|
|
3042
|
+
if (recordConflict) {
|
|
3043
|
+
const latest = getTask(task.id);
|
|
3044
|
+
if (latest) {
|
|
3045
|
+
const conflict = {
|
|
3046
|
+
agent: "claude",
|
|
3047
|
+
direction: "push",
|
|
3048
|
+
prefer,
|
|
3049
|
+
local_updated_at: latest.updated_at,
|
|
3050
|
+
remote_updated_at: remoteUpdatedAt ? new Date(remoteUpdatedAt).toISOString() : undefined,
|
|
3051
|
+
detected_at: new Date().toISOString()
|
|
3052
|
+
};
|
|
3053
|
+
const newMeta = appendSyncConflict(latest.metadata, conflict);
|
|
3054
|
+
updateTask(latest.id, { version: latest.version, metadata: newMeta });
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
2722
3057
|
} else {
|
|
2723
3058
|
const claudeId = String(hwm);
|
|
2724
3059
|
hwm++;
|
|
@@ -2738,14 +3073,15 @@ function pushToClaudeTaskList(taskListId, projectId) {
|
|
|
2738
3073
|
writeHighWaterMark(dir, hwm);
|
|
2739
3074
|
return { pushed, pulled: 0, errors };
|
|
2740
3075
|
}
|
|
2741
|
-
function pullFromClaudeTaskList(taskListId, projectId) {
|
|
3076
|
+
function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
|
|
2742
3077
|
const dir = getTaskListDir(taskListId);
|
|
2743
|
-
if (!
|
|
3078
|
+
if (!existsSync3(dir)) {
|
|
2744
3079
|
return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
|
|
2745
3080
|
}
|
|
2746
3081
|
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
2747
3082
|
let pulled = 0;
|
|
2748
3083
|
const errors = [];
|
|
3084
|
+
const prefer = options.prefer || "remote";
|
|
2749
3085
|
const allTasks = listTasks({});
|
|
2750
3086
|
const byClaudeId = new Map;
|
|
2751
3087
|
for (const t of allTasks) {
|
|
@@ -2759,6 +3095,7 @@ function pullFromClaudeTaskList(taskListId, projectId) {
|
|
|
2759
3095
|
}
|
|
2760
3096
|
for (const f of files) {
|
|
2761
3097
|
try {
|
|
3098
|
+
const filePath = join3(dir, f);
|
|
2762
3099
|
const ct = readClaudeTask(dir, f);
|
|
2763
3100
|
if (!ct)
|
|
2764
3101
|
continue;
|
|
@@ -2769,13 +3106,33 @@ function pullFromClaudeTaskList(taskListId, projectId) {
|
|
|
2769
3106
|
const existingByTodos = todosId ? byTodosId.get(todosId) : undefined;
|
|
2770
3107
|
const existing = existingByMapping || existingByTodos;
|
|
2771
3108
|
if (existing) {
|
|
3109
|
+
const lastSyncedAt = parseTimestamp(ct.metadata?.["todos_updated_at"]);
|
|
3110
|
+
const localUpdatedAt = parseTimestamp(existing.updated_at);
|
|
3111
|
+
const remoteUpdatedAt = getFileMtimeMs(filePath);
|
|
3112
|
+
let conflictMeta = null;
|
|
3113
|
+
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
3114
|
+
const conflict = {
|
|
3115
|
+
agent: "claude",
|
|
3116
|
+
direction: "pull",
|
|
3117
|
+
prefer,
|
|
3118
|
+
local_updated_at: existing.updated_at,
|
|
3119
|
+
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
3120
|
+
detected_at: new Date().toISOString()
|
|
3121
|
+
};
|
|
3122
|
+
conflictMeta = appendSyncConflict(existing.metadata, conflict);
|
|
3123
|
+
if (prefer === "local") {
|
|
3124
|
+
updateTask(existing.id, { version: existing.version, metadata: conflictMeta });
|
|
3125
|
+
errors.push(`conflict pull ${existing.id}: local newer`);
|
|
3126
|
+
continue;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
2772
3129
|
updateTask(existing.id, {
|
|
2773
3130
|
version: existing.version,
|
|
2774
3131
|
title: ct.subject,
|
|
2775
3132
|
description: ct.description || undefined,
|
|
2776
3133
|
status: toSqliteStatus(ct.status),
|
|
2777
3134
|
assigned_to: ct.owner || undefined,
|
|
2778
|
-
metadata: { ...existing.metadata, claude_task_id: ct.id }
|
|
3135
|
+
metadata: { ...conflictMeta || existing.metadata, claude_task_id: ct.id, ...ct.metadata }
|
|
2779
3136
|
});
|
|
2780
3137
|
} else {
|
|
2781
3138
|
createTask({
|
|
@@ -2784,7 +3141,7 @@ function pullFromClaudeTaskList(taskListId, projectId) {
|
|
|
2784
3141
|
status: toSqliteStatus(ct.status),
|
|
2785
3142
|
assigned_to: ct.owner || undefined,
|
|
2786
3143
|
project_id: projectId,
|
|
2787
|
-
metadata: { claude_task_id: ct.id },
|
|
3144
|
+
metadata: { ...ct.metadata, claude_task_id: ct.id },
|
|
2788
3145
|
priority: ct.metadata?.["priority"] || "medium"
|
|
2789
3146
|
});
|
|
2790
3147
|
}
|
|
@@ -2795,19 +3152,349 @@ function pullFromClaudeTaskList(taskListId, projectId) {
|
|
|
2795
3152
|
}
|
|
2796
3153
|
return { pushed: 0, pulled, errors };
|
|
2797
3154
|
}
|
|
2798
|
-
function syncClaudeTaskList(taskListId, projectId) {
|
|
2799
|
-
const pullResult = pullFromClaudeTaskList(taskListId, projectId);
|
|
2800
|
-
const pushResult = pushToClaudeTaskList(taskListId, projectId);
|
|
3155
|
+
function syncClaudeTaskList(taskListId, projectId, options = {}) {
|
|
3156
|
+
const pullResult = pullFromClaudeTaskList(taskListId, projectId, options);
|
|
3157
|
+
const pushResult = pushToClaudeTaskList(taskListId, projectId, options);
|
|
2801
3158
|
return {
|
|
2802
3159
|
pushed: pushResult.pushed,
|
|
2803
3160
|
pulled: pullResult.pulled,
|
|
2804
3161
|
errors: [...pullResult.errors, ...pushResult.errors]
|
|
2805
3162
|
};
|
|
2806
3163
|
}
|
|
2807
|
-
var HOME;
|
|
2808
3164
|
var init_claude_tasks = __esm(() => {
|
|
2809
3165
|
init_tasks();
|
|
2810
|
-
|
|
3166
|
+
init_sync_utils();
|
|
3167
|
+
});
|
|
3168
|
+
|
|
3169
|
+
// src/lib/config.ts
|
|
3170
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3171
|
+
import { join as join4 } from "path";
|
|
3172
|
+
function normalizeAgent(agent) {
|
|
3173
|
+
return agent.trim().toLowerCase();
|
|
3174
|
+
}
|
|
3175
|
+
function loadConfig() {
|
|
3176
|
+
if (cached)
|
|
3177
|
+
return cached;
|
|
3178
|
+
if (!existsSync4(CONFIG_PATH)) {
|
|
3179
|
+
cached = {};
|
|
3180
|
+
return cached;
|
|
3181
|
+
}
|
|
3182
|
+
const config = readJsonFile(CONFIG_PATH) || {};
|
|
3183
|
+
if (typeof config.sync_agents === "string") {
|
|
3184
|
+
config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
|
|
3185
|
+
}
|
|
3186
|
+
cached = config;
|
|
3187
|
+
return cached;
|
|
3188
|
+
}
|
|
3189
|
+
function getSyncAgentsFromConfig() {
|
|
3190
|
+
const config = loadConfig();
|
|
3191
|
+
const agents = config.sync_agents;
|
|
3192
|
+
if (Array.isArray(agents) && agents.length > 0)
|
|
3193
|
+
return agents.map(normalizeAgent);
|
|
3194
|
+
return null;
|
|
3195
|
+
}
|
|
3196
|
+
function getAgentTaskListId(agent) {
|
|
3197
|
+
const config = loadConfig();
|
|
3198
|
+
const key = normalizeAgent(agent);
|
|
3199
|
+
return config.agents?.[key]?.task_list_id || config.task_list_id || null;
|
|
3200
|
+
}
|
|
3201
|
+
function getAgentTasksDir(agent) {
|
|
3202
|
+
const config = loadConfig();
|
|
3203
|
+
const key = normalizeAgent(agent);
|
|
3204
|
+
return config.agents?.[key]?.tasks_dir || config.agent_tasks_dir || null;
|
|
3205
|
+
}
|
|
3206
|
+
var CONFIG_PATH, cached = null;
|
|
3207
|
+
var init_config = __esm(() => {
|
|
3208
|
+
init_sync_utils();
|
|
3209
|
+
CONFIG_PATH = join4(HOME, ".todos", "config.json");
|
|
3210
|
+
});
|
|
3211
|
+
|
|
3212
|
+
// src/lib/agent-tasks.ts
|
|
3213
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3214
|
+
import { join as join5 } from "path";
|
|
3215
|
+
function agentBaseDir(agent) {
|
|
3216
|
+
const key = `TODOS_${agent.toUpperCase()}_TASKS_DIR`;
|
|
3217
|
+
return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join5(HOME, ".todos", "agents");
|
|
3218
|
+
}
|
|
3219
|
+
function getTaskListDir2(agent, taskListId) {
|
|
3220
|
+
return join5(agentBaseDir(agent), agent, taskListId);
|
|
3221
|
+
}
|
|
3222
|
+
function readAgentTask(dir, filename) {
|
|
3223
|
+
return readJsonFile(join5(dir, filename));
|
|
3224
|
+
}
|
|
3225
|
+
function writeAgentTask(dir, task) {
|
|
3226
|
+
writeJsonFile(join5(dir, `${task.id}.json`), task);
|
|
3227
|
+
}
|
|
3228
|
+
function taskToAgentTask(task, externalId, existingMeta) {
|
|
3229
|
+
return {
|
|
3230
|
+
id: externalId,
|
|
3231
|
+
title: task.title,
|
|
3232
|
+
description: task.description || "",
|
|
3233
|
+
status: task.status,
|
|
3234
|
+
priority: task.priority,
|
|
3235
|
+
assigned_to: task.assigned_to || task.agent_id || "",
|
|
3236
|
+
tags: task.tags || [],
|
|
3237
|
+
metadata: {
|
|
3238
|
+
...existingMeta || {},
|
|
3239
|
+
...task.metadata,
|
|
3240
|
+
todos_id: task.id,
|
|
3241
|
+
todos_updated_at: task.updated_at,
|
|
3242
|
+
todos_version: task.version
|
|
3243
|
+
}
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
function metadataKey(agent) {
|
|
3247
|
+
return `${agent}_task_id`;
|
|
3248
|
+
}
|
|
3249
|
+
function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
3250
|
+
const dir = getTaskListDir2(agent, taskListId);
|
|
3251
|
+
if (!existsSync5(dir))
|
|
3252
|
+
ensureDir2(dir);
|
|
3253
|
+
const filter = {};
|
|
3254
|
+
if (projectId)
|
|
3255
|
+
filter["project_id"] = projectId;
|
|
3256
|
+
const tasks = listTasks(filter);
|
|
3257
|
+
const existingByTodosId = new Map;
|
|
3258
|
+
const files = listJsonFiles(dir);
|
|
3259
|
+
for (const f of files) {
|
|
3260
|
+
const path = join5(dir, f);
|
|
3261
|
+
const at = readAgentTask(dir, f);
|
|
3262
|
+
if (at?.metadata?.["todos_id"]) {
|
|
3263
|
+
existingByTodosId.set(at.metadata["todos_id"], { task: at, mtimeMs: getFileMtimeMs(path) });
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
let hwm = readHighWaterMark(dir);
|
|
3267
|
+
let pushed = 0;
|
|
3268
|
+
const errors = [];
|
|
3269
|
+
const metaKey = metadataKey(agent);
|
|
3270
|
+
const prefer = options.prefer || "remote";
|
|
3271
|
+
for (const task of tasks) {
|
|
3272
|
+
try {
|
|
3273
|
+
const existing = existingByTodosId.get(task.id);
|
|
3274
|
+
if (existing) {
|
|
3275
|
+
const lastSyncedAt = parseTimestamp(existing.task.metadata?.["todos_updated_at"]);
|
|
3276
|
+
const localUpdatedAt = parseTimestamp(task.updated_at);
|
|
3277
|
+
const remoteUpdatedAt = existing.mtimeMs;
|
|
3278
|
+
let recordConflict = false;
|
|
3279
|
+
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
3280
|
+
if (prefer === "remote") {
|
|
3281
|
+
const conflict = {
|
|
3282
|
+
agent,
|
|
3283
|
+
direction: "push",
|
|
3284
|
+
prefer,
|
|
3285
|
+
local_updated_at: task.updated_at,
|
|
3286
|
+
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
3287
|
+
detected_at: new Date().toISOString()
|
|
3288
|
+
};
|
|
3289
|
+
const newMeta = appendSyncConflict(task.metadata, conflict);
|
|
3290
|
+
updateTask(task.id, { version: task.version, metadata: newMeta });
|
|
3291
|
+
errors.push(`conflict push ${task.id}: remote newer`);
|
|
3292
|
+
continue;
|
|
3293
|
+
}
|
|
3294
|
+
recordConflict = true;
|
|
3295
|
+
}
|
|
3296
|
+
const updated = taskToAgentTask(task, existing.task.id, existing.task.metadata);
|
|
3297
|
+
writeAgentTask(dir, updated);
|
|
3298
|
+
if (recordConflict) {
|
|
3299
|
+
const latest = getTask(task.id);
|
|
3300
|
+
if (latest) {
|
|
3301
|
+
const conflict = {
|
|
3302
|
+
agent,
|
|
3303
|
+
direction: "push",
|
|
3304
|
+
prefer,
|
|
3305
|
+
local_updated_at: latest.updated_at,
|
|
3306
|
+
remote_updated_at: remoteUpdatedAt ? new Date(remoteUpdatedAt).toISOString() : undefined,
|
|
3307
|
+
detected_at: new Date().toISOString()
|
|
3308
|
+
};
|
|
3309
|
+
const newMeta = appendSyncConflict(latest.metadata, conflict);
|
|
3310
|
+
updateTask(latest.id, { version: latest.version, metadata: newMeta });
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
} else {
|
|
3314
|
+
const externalId = String(hwm);
|
|
3315
|
+
hwm++;
|
|
3316
|
+
const at = taskToAgentTask(task, externalId);
|
|
3317
|
+
writeAgentTask(dir, at);
|
|
3318
|
+
const current = getTask(task.id);
|
|
3319
|
+
if (current) {
|
|
3320
|
+
const newMeta = { ...current.metadata, [metaKey]: externalId };
|
|
3321
|
+
updateTask(task.id, { version: current.version, metadata: newMeta });
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
pushed++;
|
|
3325
|
+
} catch (e) {
|
|
3326
|
+
errors.push(`push ${task.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
writeHighWaterMark(dir, hwm);
|
|
3330
|
+
return { pushed, pulled: 0, errors };
|
|
3331
|
+
}
|
|
3332
|
+
function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
3333
|
+
const dir = getTaskListDir2(agent, taskListId);
|
|
3334
|
+
if (!existsSync5(dir)) {
|
|
3335
|
+
return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
|
|
3336
|
+
}
|
|
3337
|
+
const files = listJsonFiles(dir);
|
|
3338
|
+
let pulled = 0;
|
|
3339
|
+
const errors = [];
|
|
3340
|
+
const metaKey = metadataKey(agent);
|
|
3341
|
+
const prefer = options.prefer || "remote";
|
|
3342
|
+
const allTasks = listTasks({});
|
|
3343
|
+
const byExternalId = new Map;
|
|
3344
|
+
const byTodosId = new Map;
|
|
3345
|
+
for (const t of allTasks) {
|
|
3346
|
+
const extId = t.metadata[metaKey];
|
|
3347
|
+
if (extId)
|
|
3348
|
+
byExternalId.set(String(extId), t);
|
|
3349
|
+
byTodosId.set(t.id, t);
|
|
3350
|
+
}
|
|
3351
|
+
for (const f of files) {
|
|
3352
|
+
try {
|
|
3353
|
+
const filePath = join5(dir, f);
|
|
3354
|
+
const at = readAgentTask(dir, f);
|
|
3355
|
+
if (!at)
|
|
3356
|
+
continue;
|
|
3357
|
+
if (at.metadata?.["_internal"])
|
|
3358
|
+
continue;
|
|
3359
|
+
const todosId = at.metadata?.["todos_id"];
|
|
3360
|
+
const existingByMapping = byExternalId.get(at.id);
|
|
3361
|
+
const existingByTodos = todosId ? byTodosId.get(todosId) : undefined;
|
|
3362
|
+
const existing = existingByMapping || existingByTodos;
|
|
3363
|
+
if (existing) {
|
|
3364
|
+
const lastSyncedAt = parseTimestamp(at.metadata?.["todos_updated_at"]);
|
|
3365
|
+
const localUpdatedAt = parseTimestamp(existing.updated_at);
|
|
3366
|
+
const remoteUpdatedAt = getFileMtimeMs(filePath);
|
|
3367
|
+
let conflictMeta = null;
|
|
3368
|
+
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
3369
|
+
const conflict = {
|
|
3370
|
+
agent,
|
|
3371
|
+
direction: "pull",
|
|
3372
|
+
prefer,
|
|
3373
|
+
local_updated_at: existing.updated_at,
|
|
3374
|
+
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
3375
|
+
detected_at: new Date().toISOString()
|
|
3376
|
+
};
|
|
3377
|
+
conflictMeta = appendSyncConflict(existing.metadata, conflict);
|
|
3378
|
+
if (prefer === "local") {
|
|
3379
|
+
updateTask(existing.id, { version: existing.version, metadata: conflictMeta });
|
|
3380
|
+
errors.push(`conflict pull ${existing.id}: local newer`);
|
|
3381
|
+
continue;
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
updateTask(existing.id, {
|
|
3385
|
+
version: existing.version,
|
|
3386
|
+
title: at.title,
|
|
3387
|
+
description: at.description || undefined,
|
|
3388
|
+
status: at.status,
|
|
3389
|
+
priority: at.priority,
|
|
3390
|
+
assigned_to: at.assigned_to || undefined,
|
|
3391
|
+
tags: at.tags || [],
|
|
3392
|
+
metadata: { ...conflictMeta || existing.metadata, ...at.metadata, [metaKey]: at.id }
|
|
3393
|
+
});
|
|
3394
|
+
} else {
|
|
3395
|
+
createTask({
|
|
3396
|
+
title: at.title,
|
|
3397
|
+
description: at.description || undefined,
|
|
3398
|
+
status: at.status,
|
|
3399
|
+
priority: at.priority || "medium",
|
|
3400
|
+
assigned_to: at.assigned_to || undefined,
|
|
3401
|
+
tags: at.tags || [],
|
|
3402
|
+
project_id: projectId,
|
|
3403
|
+
metadata: { ...at.metadata, [metaKey]: at.id }
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
pulled++;
|
|
3407
|
+
} catch (e) {
|
|
3408
|
+
errors.push(`pull ${f}: ${e instanceof Error ? e.message : String(e)}`);
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
return { pushed: 0, pulled, errors };
|
|
3412
|
+
}
|
|
3413
|
+
function syncAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
3414
|
+
const pullResult = pullFromAgentTaskList(agent, taskListId, projectId, options);
|
|
3415
|
+
const pushResult = pushToAgentTaskList(agent, taskListId, projectId, options);
|
|
3416
|
+
return {
|
|
3417
|
+
pushed: pushResult.pushed,
|
|
3418
|
+
pulled: pullResult.pulled,
|
|
3419
|
+
errors: [...pullResult.errors, ...pushResult.errors]
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
var init_agent_tasks = __esm(() => {
|
|
3423
|
+
init_tasks();
|
|
3424
|
+
init_sync_utils();
|
|
3425
|
+
init_config();
|
|
3426
|
+
});
|
|
3427
|
+
|
|
3428
|
+
// src/lib/sync.ts
|
|
3429
|
+
function normalizeAgent2(agent) {
|
|
3430
|
+
return agent.trim().toLowerCase();
|
|
3431
|
+
}
|
|
3432
|
+
function isClaudeAgent(agent) {
|
|
3433
|
+
const a = normalizeAgent2(agent);
|
|
3434
|
+
return a === "claude" || a === "claude-code" || a === "claude_code";
|
|
3435
|
+
}
|
|
3436
|
+
function defaultSyncAgents() {
|
|
3437
|
+
const env = process.env["TODOS_SYNC_AGENTS"];
|
|
3438
|
+
if (env) {
|
|
3439
|
+
return env.split(",").map((a) => a.trim()).filter(Boolean);
|
|
3440
|
+
}
|
|
3441
|
+
const fromConfig = getSyncAgentsFromConfig();
|
|
3442
|
+
if (fromConfig && fromConfig.length > 0)
|
|
3443
|
+
return fromConfig;
|
|
3444
|
+
return ["claude", "codex", "gemini"];
|
|
3445
|
+
}
|
|
3446
|
+
function syncWithAgent(agent, taskListId, projectId, direction = "both", options = {}) {
|
|
3447
|
+
const normalized = normalizeAgent2(agent);
|
|
3448
|
+
if (isClaudeAgent(normalized)) {
|
|
3449
|
+
if (direction === "push")
|
|
3450
|
+
return pushToClaudeTaskList(taskListId, projectId, options);
|
|
3451
|
+
if (direction === "pull")
|
|
3452
|
+
return pullFromClaudeTaskList(taskListId, projectId, options);
|
|
3453
|
+
return syncClaudeTaskList(taskListId, projectId, options);
|
|
3454
|
+
}
|
|
3455
|
+
if (direction === "push")
|
|
3456
|
+
return pushToAgentTaskList(normalized, taskListId, projectId, options);
|
|
3457
|
+
if (direction === "pull")
|
|
3458
|
+
return pullFromAgentTaskList(normalized, taskListId, projectId, options);
|
|
3459
|
+
return syncAgentTaskList(normalized, taskListId, projectId, options);
|
|
3460
|
+
}
|
|
3461
|
+
function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both", options = {}) {
|
|
3462
|
+
let pushed = 0;
|
|
3463
|
+
let pulled = 0;
|
|
3464
|
+
const errors = [];
|
|
3465
|
+
const normalized = agents.map(normalizeAgent2);
|
|
3466
|
+
if (direction === "pull" || direction === "both") {
|
|
3467
|
+
for (const agent of normalized) {
|
|
3468
|
+
const listId = taskListIdByAgent(agent);
|
|
3469
|
+
if (!listId) {
|
|
3470
|
+
errors.push(`sync ${agent}: missing task list id`);
|
|
3471
|
+
continue;
|
|
3472
|
+
}
|
|
3473
|
+
const result = syncWithAgent(agent, listId, projectId, "pull", options);
|
|
3474
|
+
pushed += result.pushed;
|
|
3475
|
+
pulled += result.pulled;
|
|
3476
|
+
errors.push(...result.errors.map((e) => `${agent}: ${e}`));
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
if (direction === "push" || direction === "both") {
|
|
3480
|
+
for (const agent of normalized) {
|
|
3481
|
+
const listId = taskListIdByAgent(agent);
|
|
3482
|
+
if (!listId) {
|
|
3483
|
+
errors.push(`sync ${agent}: missing task list id`);
|
|
3484
|
+
continue;
|
|
3485
|
+
}
|
|
3486
|
+
const result = syncWithAgent(agent, listId, projectId, "push", options);
|
|
3487
|
+
pushed += result.pushed;
|
|
3488
|
+
pulled += result.pulled;
|
|
3489
|
+
errors.push(...result.errors.map((e) => `${agent}: ${e}`));
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
return { pushed, pulled, errors };
|
|
3493
|
+
}
|
|
3494
|
+
var init_sync = __esm(() => {
|
|
3495
|
+
init_claude_tasks();
|
|
3496
|
+
init_agent_tasks();
|
|
3497
|
+
init_config();
|
|
2811
3498
|
});
|
|
2812
3499
|
|
|
2813
3500
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -6785,6 +7472,8 @@ function formatError(error) {
|
|
|
6785
7472
|
return `Version conflict: ${error.message}`;
|
|
6786
7473
|
if (error instanceof TaskNotFoundError)
|
|
6787
7474
|
return `Not found: ${error.message}`;
|
|
7475
|
+
if (error instanceof PlanNotFoundError)
|
|
7476
|
+
return `Not found: ${error.message}`;
|
|
6788
7477
|
if (error instanceof LockError)
|
|
6789
7478
|
return `Lock error: ${error.message}`;
|
|
6790
7479
|
if (error instanceof DependencyCycleError)
|
|
@@ -6800,6 +7489,16 @@ function resolveId(partialId, table = "tasks") {
|
|
|
6800
7489
|
throw new Error(`Could not resolve ID: ${partialId}`);
|
|
6801
7490
|
return id;
|
|
6802
7491
|
}
|
|
7492
|
+
function resolveTaskListId(agent, explicit) {
|
|
7493
|
+
if (explicit)
|
|
7494
|
+
return explicit;
|
|
7495
|
+
const normalized = agent.trim().toLowerCase();
|
|
7496
|
+
if (normalized === "claude" || normalized === "claude-code" || normalized === "claude_code") {
|
|
7497
|
+
return process.env["TODOS_CLAUDE_TASK_LIST"] || process.env["CLAUDE_CODE_TASK_LIST_ID"] || process.env["CLAUDE_CODE_SESSION_ID"] || getAgentTaskListId(normalized) || null;
|
|
7498
|
+
}
|
|
7499
|
+
const key = `TODOS_${normalized.toUpperCase()}_TASK_LIST`;
|
|
7500
|
+
return process.env[key] || process.env["TODOS_TASK_LIST_ID"] || getAgentTaskListId(normalized) || "default";
|
|
7501
|
+
}
|
|
6803
7502
|
function formatTask(task) {
|
|
6804
7503
|
const parts = [
|
|
6805
7504
|
`ID: ${task.id}`,
|
|
@@ -6819,6 +7518,8 @@ function formatTask(task) {
|
|
|
6819
7518
|
parts.push(`Parent: ${task.parent_id}`);
|
|
6820
7519
|
if (task.project_id)
|
|
6821
7520
|
parts.push(`Project: ${task.project_id}`);
|
|
7521
|
+
if (task.plan_id)
|
|
7522
|
+
parts.push(`Plan: ${task.plan_id}`);
|
|
6822
7523
|
if (task.tags.length > 0)
|
|
6823
7524
|
parts.push(`Tags: ${task.tags.join(", ")}`);
|
|
6824
7525
|
parts.push(`Version: ${task.version}`);
|
|
@@ -6838,8 +7539,10 @@ var init_mcp = __esm(() => {
|
|
|
6838
7539
|
init_tasks();
|
|
6839
7540
|
init_comments();
|
|
6840
7541
|
init_projects();
|
|
7542
|
+
init_plans();
|
|
6841
7543
|
init_search();
|
|
6842
|
-
|
|
7544
|
+
init_sync();
|
|
7545
|
+
init_config();
|
|
6843
7546
|
init_database();
|
|
6844
7547
|
init_types();
|
|
6845
7548
|
server = new McpServer({
|
|
@@ -6857,6 +7560,7 @@ var init_mcp = __esm(() => {
|
|
|
6857
7560
|
assigned_to: exports_external.string().optional().describe("Assigned agent ID"),
|
|
6858
7561
|
session_id: exports_external.string().optional().describe("Session ID"),
|
|
6859
7562
|
working_dir: exports_external.string().optional().describe("Working directory context"),
|
|
7563
|
+
plan_id: exports_external.string().optional().describe("Plan ID to assign task to"),
|
|
6860
7564
|
tags: exports_external.array(exports_external.string()).optional().describe("Task tags"),
|
|
6861
7565
|
metadata: exports_external.record(exports_external.unknown()).optional().describe("Arbitrary metadata")
|
|
6862
7566
|
}, async (params) => {
|
|
@@ -6866,6 +7570,8 @@ var init_mcp = __esm(() => {
|
|
|
6866
7570
|
resolved.project_id = resolveId(resolved.project_id, "projects");
|
|
6867
7571
|
if (resolved.parent_id)
|
|
6868
7572
|
resolved.parent_id = resolveId(resolved.parent_id);
|
|
7573
|
+
if (resolved.plan_id)
|
|
7574
|
+
resolved.plan_id = resolveId(resolved.plan_id, "plans");
|
|
6869
7575
|
const task = createTask(resolved);
|
|
6870
7576
|
return { content: [{ type: "text", text: `Task created:
|
|
6871
7577
|
${formatTask(task)}` }] };
|
|
@@ -6884,12 +7590,15 @@ ${formatTask(task)}` }] };
|
|
|
6884
7590
|
exports_external.array(exports_external.enum(["low", "medium", "high", "critical"]))
|
|
6885
7591
|
]).optional().describe("Filter by priority"),
|
|
6886
7592
|
assigned_to: exports_external.string().optional().describe("Filter by assigned agent"),
|
|
6887
|
-
tags: exports_external.array(exports_external.string()).optional().describe("Filter by tags (any match)")
|
|
7593
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Filter by tags (any match)"),
|
|
7594
|
+
plan_id: exports_external.string().optional().describe("Filter by plan")
|
|
6888
7595
|
}, async (params) => {
|
|
6889
7596
|
try {
|
|
6890
7597
|
const resolved = { ...params };
|
|
6891
7598
|
if (resolved.project_id)
|
|
6892
7599
|
resolved.project_id = resolveId(resolved.project_id, "projects");
|
|
7600
|
+
if (resolved.plan_id)
|
|
7601
|
+
resolved.plan_id = resolveId(resolved.plan_id, "plans");
|
|
6893
7602
|
const tasks = listTasks(resolved);
|
|
6894
7603
|
if (tasks.length === 0) {
|
|
6895
7604
|
return { content: [{ type: "text", text: "No tasks found." }] };
|
|
@@ -6963,7 +7672,8 @@ Parent: ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
|
|
|
6963
7672
|
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("New priority"),
|
|
6964
7673
|
assigned_to: exports_external.string().optional().describe("Assign to agent"),
|
|
6965
7674
|
tags: exports_external.array(exports_external.string()).optional().describe("New tags"),
|
|
6966
|
-
metadata: exports_external.record(exports_external.unknown()).optional().describe("New metadata")
|
|
7675
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("New metadata"),
|
|
7676
|
+
plan_id: exports_external.string().optional().describe("Plan ID to assign task to")
|
|
6967
7677
|
}, async ({ id, ...rest }) => {
|
|
6968
7678
|
try {
|
|
6969
7679
|
const resolvedId = resolveId(id);
|
|
@@ -7074,45 +7784,151 @@ ${formatTask(task)}` }] };
|
|
|
7074
7784
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
7075
7785
|
}
|
|
7076
7786
|
});
|
|
7077
|
-
server.tool("add_comment", "Add a comment/note to a task", {
|
|
7078
|
-
task_id: exports_external.string().describe("Task ID (full or partial)"),
|
|
7079
|
-
content: exports_external.string().describe("Comment content"),
|
|
7080
|
-
agent_id: exports_external.string().optional().describe("Agent adding comment"),
|
|
7081
|
-
session_id: exports_external.string().optional().describe("Session ID")
|
|
7082
|
-
}, async ({ task_id, ...rest }) => {
|
|
7787
|
+
server.tool("add_comment", "Add a comment/note to a task", {
|
|
7788
|
+
task_id: exports_external.string().describe("Task ID (full or partial)"),
|
|
7789
|
+
content: exports_external.string().describe("Comment content"),
|
|
7790
|
+
agent_id: exports_external.string().optional().describe("Agent adding comment"),
|
|
7791
|
+
session_id: exports_external.string().optional().describe("Session ID")
|
|
7792
|
+
}, async ({ task_id, ...rest }) => {
|
|
7793
|
+
try {
|
|
7794
|
+
const resolvedId = resolveId(task_id);
|
|
7795
|
+
const comment = addComment({ task_id: resolvedId, ...rest });
|
|
7796
|
+
return { content: [{ type: "text", text: `Comment added (${comment.id.slice(0, 8)}) at ${comment.created_at}` }] };
|
|
7797
|
+
} catch (e) {
|
|
7798
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
7799
|
+
}
|
|
7800
|
+
});
|
|
7801
|
+
server.tool("list_projects", "List all registered projects", {}, async () => {
|
|
7802
|
+
try {
|
|
7803
|
+
const projects = listProjects();
|
|
7804
|
+
if (projects.length === 0) {
|
|
7805
|
+
return { content: [{ type: "text", text: "No projects registered." }] };
|
|
7806
|
+
}
|
|
7807
|
+
const text = projects.map((p) => {
|
|
7808
|
+
const taskList = p.task_list_id ? ` [${p.task_list_id}]` : "";
|
|
7809
|
+
return `${p.id.slice(0, 8)} | ${p.name} | ${p.path}${taskList}${p.description ? ` - ${p.description}` : ""}`;
|
|
7810
|
+
}).join(`
|
|
7811
|
+
`);
|
|
7812
|
+
return { content: [{ type: "text", text: `${projects.length} project(s):
|
|
7813
|
+
${text}` }] };
|
|
7814
|
+
} catch (e) {
|
|
7815
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
7816
|
+
}
|
|
7817
|
+
});
|
|
7818
|
+
server.tool("create_project", "Register a new project", {
|
|
7819
|
+
name: exports_external.string().describe("Project name"),
|
|
7820
|
+
path: exports_external.string().describe("Absolute path to project"),
|
|
7821
|
+
description: exports_external.string().optional().describe("Project description"),
|
|
7822
|
+
task_list_id: exports_external.string().optional().describe("Custom task list ID for Claude Code sync (defaults to todos-<slugified-name>)")
|
|
7823
|
+
}, async (params) => {
|
|
7824
|
+
try {
|
|
7825
|
+
const project = createProject(params);
|
|
7826
|
+
const taskList = project.task_list_id ? ` [${project.task_list_id}]` : "";
|
|
7827
|
+
return {
|
|
7828
|
+
content: [{
|
|
7829
|
+
type: "text",
|
|
7830
|
+
text: `Project created: ${project.id.slice(0, 8)} | ${project.name} | ${project.path}${taskList}`
|
|
7831
|
+
}]
|
|
7832
|
+
};
|
|
7833
|
+
} catch (e) {
|
|
7834
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
7835
|
+
}
|
|
7836
|
+
});
|
|
7837
|
+
server.tool("create_plan", "Create a new plan", {
|
|
7838
|
+
name: exports_external.string().describe("Plan name"),
|
|
7839
|
+
project_id: exports_external.string().optional().describe("Project ID"),
|
|
7840
|
+
description: exports_external.string().optional().describe("Plan description"),
|
|
7841
|
+
status: exports_external.enum(["active", "completed", "archived"]).optional().describe("Plan status")
|
|
7842
|
+
}, async (params) => {
|
|
7843
|
+
try {
|
|
7844
|
+
const resolved = { ...params };
|
|
7845
|
+
if (resolved.project_id)
|
|
7846
|
+
resolved.project_id = resolveId(resolved.project_id, "projects");
|
|
7847
|
+
const plan = createPlan(resolved);
|
|
7848
|
+
return {
|
|
7849
|
+
content: [{
|
|
7850
|
+
type: "text",
|
|
7851
|
+
text: `Plan created: ${plan.id.slice(0, 8)} | ${plan.name} | ${plan.status}`
|
|
7852
|
+
}]
|
|
7853
|
+
};
|
|
7854
|
+
} catch (e) {
|
|
7855
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
7856
|
+
}
|
|
7857
|
+
});
|
|
7858
|
+
server.tool("list_plans", "List plans with optional project filter", {
|
|
7859
|
+
project_id: exports_external.string().optional().describe("Filter by project")
|
|
7860
|
+
}, async ({ project_id }) => {
|
|
7861
|
+
try {
|
|
7862
|
+
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
7863
|
+
const plans = listPlans(resolvedProjectId);
|
|
7864
|
+
if (plans.length === 0) {
|
|
7865
|
+
return { content: [{ type: "text", text: "No plans found." }] };
|
|
7866
|
+
}
|
|
7867
|
+
const text = plans.map((p) => {
|
|
7868
|
+
const project = p.project_id ? ` (project: ${p.project_id.slice(0, 8)})` : "";
|
|
7869
|
+
return `[${p.status}] ${p.id.slice(0, 8)} | ${p.name}${project}`;
|
|
7870
|
+
}).join(`
|
|
7871
|
+
`);
|
|
7872
|
+
return { content: [{ type: "text", text: `${plans.length} plan(s):
|
|
7873
|
+
${text}` }] };
|
|
7874
|
+
} catch (e) {
|
|
7875
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
7876
|
+
}
|
|
7877
|
+
});
|
|
7878
|
+
server.tool("get_plan", "Get plan details", {
|
|
7879
|
+
id: exports_external.string().describe("Plan ID (full or partial)")
|
|
7880
|
+
}, async ({ id }) => {
|
|
7083
7881
|
try {
|
|
7084
|
-
const resolvedId = resolveId(
|
|
7085
|
-
const
|
|
7086
|
-
|
|
7882
|
+
const resolvedId = resolveId(id, "plans");
|
|
7883
|
+
const plan = getPlan(resolvedId);
|
|
7884
|
+
if (!plan)
|
|
7885
|
+
return { content: [{ type: "text", text: `Plan not found: ${id}` }], isError: true };
|
|
7886
|
+
const parts = [
|
|
7887
|
+
`ID: ${plan.id}`,
|
|
7888
|
+
`Name: ${plan.name}`,
|
|
7889
|
+
`Status: ${plan.status}`
|
|
7890
|
+
];
|
|
7891
|
+
if (plan.description)
|
|
7892
|
+
parts.push(`Description: ${plan.description}`);
|
|
7893
|
+
if (plan.project_id)
|
|
7894
|
+
parts.push(`Project: ${plan.project_id}`);
|
|
7895
|
+
parts.push(`Created: ${plan.created_at}`);
|
|
7896
|
+
parts.push(`Updated: ${plan.updated_at}`);
|
|
7897
|
+
return { content: [{ type: "text", text: parts.join(`
|
|
7898
|
+
`) }] };
|
|
7087
7899
|
} catch (e) {
|
|
7088
7900
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
7089
7901
|
}
|
|
7090
7902
|
});
|
|
7091
|
-
server.tool("
|
|
7903
|
+
server.tool("update_plan", "Update a plan", {
|
|
7904
|
+
id: exports_external.string().describe("Plan ID (full or partial)"),
|
|
7905
|
+
name: exports_external.string().optional().describe("New name"),
|
|
7906
|
+
description: exports_external.string().optional().describe("New description"),
|
|
7907
|
+
status: exports_external.enum(["active", "completed", "archived"]).optional().describe("New status")
|
|
7908
|
+
}, async ({ id, ...rest }) => {
|
|
7092
7909
|
try {
|
|
7093
|
-
const
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
`)
|
|
7099
|
-
|
|
7100
|
-
|
|
7910
|
+
const resolvedId = resolveId(id, "plans");
|
|
7911
|
+
const plan = updatePlan(resolvedId, rest);
|
|
7912
|
+
return {
|
|
7913
|
+
content: [{
|
|
7914
|
+
type: "text",
|
|
7915
|
+
text: `Plan updated: ${plan.id.slice(0, 8)} | ${plan.name} | ${plan.status}`
|
|
7916
|
+
}]
|
|
7917
|
+
};
|
|
7101
7918
|
} catch (e) {
|
|
7102
7919
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
7103
7920
|
}
|
|
7104
7921
|
});
|
|
7105
|
-
server.tool("
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
description: exports_external.string().optional().describe("Project description")
|
|
7109
|
-
}, async (params) => {
|
|
7922
|
+
server.tool("delete_plan", "Delete a plan", {
|
|
7923
|
+
id: exports_external.string().describe("Plan ID (full or partial)")
|
|
7924
|
+
}, async ({ id }) => {
|
|
7110
7925
|
try {
|
|
7111
|
-
const
|
|
7926
|
+
const resolvedId = resolveId(id, "plans");
|
|
7927
|
+
const deleted = deletePlan(resolvedId);
|
|
7112
7928
|
return {
|
|
7113
7929
|
content: [{
|
|
7114
7930
|
type: "text",
|
|
7115
|
-
text: `
|
|
7931
|
+
text: deleted ? `Plan ${id} deleted.` : `Plan ${id} not found.`
|
|
7116
7932
|
}]
|
|
7117
7933
|
};
|
|
7118
7934
|
} catch (e) {
|
|
@@ -7137,30 +7953,42 @@ ${text}` }] };
|
|
|
7137
7953
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
7138
7954
|
}
|
|
7139
7955
|
});
|
|
7140
|
-
server.tool("sync", "Sync tasks with
|
|
7141
|
-
task_list_id: exports_external.string().optional().describe("
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7956
|
+
server.tool("sync", "Sync tasks with an agent task list (Claude uses native task list; others use JSON lists).", {
|
|
7957
|
+
task_list_id: exports_external.string().optional().describe("Task list ID (required for Claude)"),
|
|
7958
|
+
agent: exports_external.string().optional().describe("Agent/provider name (default: claude)"),
|
|
7959
|
+
all_agents: exports_external.boolean().optional().describe("Sync across all configured agents"),
|
|
7960
|
+
project_id: exports_external.string().optional().describe("Project ID \u2014 its task_list_id will be used for Claude if task_list_id is not provided"),
|
|
7961
|
+
direction: exports_external.enum(["push", "pull", "both"]).optional().describe("Sync direction: push (SQLite->agent), pull (agent->SQLite), or both (default)"),
|
|
7962
|
+
prefer: exports_external.enum(["local", "remote"]).optional().describe("Conflict strategy")
|
|
7963
|
+
}, async ({ task_list_id, agent, all_agents, project_id, direction, prefer }) => {
|
|
7145
7964
|
try {
|
|
7146
7965
|
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
7147
|
-
const
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
}
|
|
7966
|
+
const project = resolvedProjectId ? getProject(resolvedProjectId) : undefined;
|
|
7967
|
+
const dir = direction ?? "both";
|
|
7968
|
+
const options = { prefer: prefer ?? "remote" };
|
|
7151
7969
|
let result;
|
|
7152
|
-
if (
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
result = pullFromClaudeTaskList(taskListId, resolvedProjectId);
|
|
7970
|
+
if (all_agents) {
|
|
7971
|
+
const agents = defaultSyncAgents();
|
|
7972
|
+
result = syncWithAgents(agents, (a) => resolveTaskListId(a, task_list_id || project?.task_list_id || undefined), resolvedProjectId, dir, options);
|
|
7156
7973
|
} else {
|
|
7157
|
-
|
|
7974
|
+
const resolvedAgent = agent || "claude";
|
|
7975
|
+
const taskListId = resolveTaskListId(resolvedAgent, task_list_id || project?.task_list_id || undefined);
|
|
7976
|
+
if (!taskListId) {
|
|
7977
|
+
return {
|
|
7978
|
+
content: [{
|
|
7979
|
+
type: "text",
|
|
7980
|
+
text: `Could not determine task list ID for ${resolvedAgent}. Provide task_list_id or set task_list_id on the project.`
|
|
7981
|
+
}],
|
|
7982
|
+
isError: true
|
|
7983
|
+
};
|
|
7984
|
+
}
|
|
7985
|
+
result = syncWithAgent(resolvedAgent, taskListId, resolvedProjectId, dir, options);
|
|
7158
7986
|
}
|
|
7159
7987
|
const parts = [];
|
|
7160
7988
|
if (result.pulled > 0)
|
|
7161
|
-
parts.push(`Pulled ${result.pulled} task(s)
|
|
7989
|
+
parts.push(`Pulled ${result.pulled} task(s).`);
|
|
7162
7990
|
if (result.pushed > 0)
|
|
7163
|
-
parts.push(`Pushed ${result.pushed} task(s)
|
|
7991
|
+
parts.push(`Pushed ${result.pushed} task(s).`);
|
|
7164
7992
|
if (result.pulled === 0 && result.pushed === 0 && result.errors.length === 0) {
|
|
7165
7993
|
parts.push("Nothing to sync.");
|
|
7166
7994
|
}
|
|
@@ -7190,30 +8018,34 @@ ${text}` }] };
|
|
|
7190
8018
|
// src/server/serve.ts
|
|
7191
8019
|
var exports_serve = {};
|
|
7192
8020
|
__export(exports_serve, {
|
|
7193
|
-
startServer: () => startServer
|
|
8021
|
+
startServer: () => startServer,
|
|
8022
|
+
createFetchHandler: () => createFetchHandler
|
|
7194
8023
|
});
|
|
7195
8024
|
import { execSync } from "child_process";
|
|
7196
|
-
import { existsSync as
|
|
7197
|
-
import { join as
|
|
8025
|
+
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
|
|
8026
|
+
import { join as join6, dirname as dirname2, extname } from "path";
|
|
7198
8027
|
import { fileURLToPath } from "url";
|
|
7199
8028
|
function resolveDashboardDir() {
|
|
7200
8029
|
const candidates = [];
|
|
7201
8030
|
try {
|
|
7202
8031
|
const scriptDir = dirname2(fileURLToPath(import.meta.url));
|
|
7203
|
-
candidates.push(
|
|
7204
|
-
candidates.push(
|
|
8032
|
+
candidates.push(join6(scriptDir, "..", "dashboard", "dist"));
|
|
8033
|
+
candidates.push(join6(scriptDir, "..", "..", "dashboard", "dist"));
|
|
7205
8034
|
} catch {}
|
|
7206
8035
|
if (process.argv[1]) {
|
|
7207
8036
|
const mainDir = dirname2(process.argv[1]);
|
|
7208
|
-
candidates.push(
|
|
7209
|
-
candidates.push(
|
|
8037
|
+
candidates.push(join6(mainDir, "..", "dashboard", "dist"));
|
|
8038
|
+
candidates.push(join6(mainDir, "..", "..", "dashboard", "dist"));
|
|
7210
8039
|
}
|
|
7211
|
-
candidates.push(
|
|
8040
|
+
candidates.push(join6(process.cwd(), "dashboard", "dist"));
|
|
7212
8041
|
for (const candidate of candidates) {
|
|
7213
|
-
if (
|
|
8042
|
+
if (existsSync6(candidate))
|
|
7214
8043
|
return candidate;
|
|
7215
8044
|
}
|
|
7216
|
-
return
|
|
8045
|
+
return join6(process.cwd(), "dashboard", "dist");
|
|
8046
|
+
}
|
|
8047
|
+
function randomPort() {
|
|
8048
|
+
return 20000 + Math.floor(Math.random() * 20000);
|
|
7217
8049
|
}
|
|
7218
8050
|
function json(data, status = 200, port) {
|
|
7219
8051
|
return new Response(JSON.stringify(data), {
|
|
@@ -7227,14 +8059,21 @@ function json(data, status = 200, port) {
|
|
|
7227
8059
|
}
|
|
7228
8060
|
function getPackageVersion() {
|
|
7229
8061
|
try {
|
|
7230
|
-
const pkgPath =
|
|
8062
|
+
const pkgPath = join6(dirname2(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
7231
8063
|
return JSON.parse(readFileSync2(pkgPath, "utf-8")).version || "0.0.0";
|
|
7232
8064
|
} catch {
|
|
7233
8065
|
return "0.0.0";
|
|
7234
8066
|
}
|
|
7235
8067
|
}
|
|
8068
|
+
async function parseJsonBody(req) {
|
|
8069
|
+
try {
|
|
8070
|
+
return await req.json();
|
|
8071
|
+
} catch {
|
|
8072
|
+
return null;
|
|
8073
|
+
}
|
|
8074
|
+
}
|
|
7236
8075
|
function serveStaticFile(filePath) {
|
|
7237
|
-
if (!
|
|
8076
|
+
if (!existsSync6(filePath))
|
|
7238
8077
|
return null;
|
|
7239
8078
|
const ext = extname(filePath);
|
|
7240
8079
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
@@ -7242,299 +8081,474 @@ function serveStaticFile(filePath) {
|
|
|
7242
8081
|
headers: { "Content-Type": contentType }
|
|
7243
8082
|
});
|
|
7244
8083
|
}
|
|
7245
|
-
|
|
7246
|
-
const
|
|
7247
|
-
const
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
const projectId = url.searchParams.get("project_id");
|
|
7273
|
-
if (status)
|
|
7274
|
-
filter.status = status;
|
|
7275
|
-
if (priority)
|
|
7276
|
-
filter.priority = priority;
|
|
7277
|
-
if (projectId)
|
|
7278
|
-
filter.project_id = projectId;
|
|
7279
|
-
const tasks = listTasks(filter);
|
|
7280
|
-
const projectCache = new Map;
|
|
7281
|
-
const enriched = tasks.map((t) => {
|
|
7282
|
-
let projectName;
|
|
7283
|
-
if (t.project_id) {
|
|
7284
|
-
if (projectCache.has(t.project_id)) {
|
|
7285
|
-
projectName = projectCache.get(t.project_id);
|
|
7286
|
-
} else {
|
|
7287
|
-
const p = getProject(t.project_id);
|
|
7288
|
-
projectName = p?.name;
|
|
7289
|
-
if (projectName)
|
|
7290
|
-
projectCache.set(t.project_id, projectName);
|
|
7291
|
-
}
|
|
7292
|
-
}
|
|
7293
|
-
return { ...t, project_name: projectName };
|
|
7294
|
-
});
|
|
7295
|
-
return json(enriched, 200, port);
|
|
7296
|
-
} catch (e) {
|
|
7297
|
-
return json({ error: e instanceof Error ? e.message : "Failed to list tasks" }, 500, port);
|
|
7298
|
-
}
|
|
7299
|
-
}
|
|
7300
|
-
const taskGetMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
|
|
7301
|
-
if (taskGetMatch && method === "GET") {
|
|
7302
|
-
try {
|
|
7303
|
-
const id = taskGetMatch[1];
|
|
7304
|
-
const task = getTaskWithRelations(id);
|
|
7305
|
-
if (!task)
|
|
7306
|
-
return json({ error: "Task not found" }, 404, port);
|
|
8084
|
+
function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
8085
|
+
const dir = dashboardDir || resolveDashboardDir();
|
|
8086
|
+
const hasDashboard = dashboardExists ?? existsSync6(dir);
|
|
8087
|
+
return async (req) => {
|
|
8088
|
+
const url = new URL(req.url);
|
|
8089
|
+
let path = url.pathname;
|
|
8090
|
+
if (path.startsWith("/api/v1/")) {
|
|
8091
|
+
path = path.replace("/api/v1", "/api");
|
|
8092
|
+
}
|
|
8093
|
+
const method = req.method;
|
|
8094
|
+
const port = getPort();
|
|
8095
|
+
if (path === "/api/tasks" && method === "GET") {
|
|
8096
|
+
try {
|
|
8097
|
+
const filter = {};
|
|
8098
|
+
const status = url.searchParams.get("status");
|
|
8099
|
+
const priority = url.searchParams.get("priority");
|
|
8100
|
+
const projectId = url.searchParams.get("project_id");
|
|
8101
|
+
if (status)
|
|
8102
|
+
filter.status = status;
|
|
8103
|
+
if (priority)
|
|
8104
|
+
filter.priority = priority;
|
|
8105
|
+
if (projectId)
|
|
8106
|
+
filter.project_id = projectId;
|
|
8107
|
+
const tasks = listTasks(filter);
|
|
8108
|
+
const projectCache = new Map;
|
|
8109
|
+
const planCache = new Map;
|
|
8110
|
+
const enriched = tasks.map((t) => {
|
|
7307
8111
|
let projectName;
|
|
7308
|
-
if (
|
|
7309
|
-
|
|
7310
|
-
|
|
8112
|
+
if (t.project_id) {
|
|
8113
|
+
if (projectCache.has(t.project_id)) {
|
|
8114
|
+
projectName = projectCache.get(t.project_id);
|
|
8115
|
+
} else {
|
|
8116
|
+
const p = getProject(t.project_id);
|
|
8117
|
+
projectName = p?.name;
|
|
8118
|
+
if (projectName)
|
|
8119
|
+
projectCache.set(t.project_id, projectName);
|
|
8120
|
+
}
|
|
7311
8121
|
}
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
const body = await req.json();
|
|
7323
|
-
if (!body.title || typeof body.title !== "string") {
|
|
7324
|
-
return json({ error: "Missing required field: title" }, 400, port);
|
|
8122
|
+
let planName;
|
|
8123
|
+
if (t.plan_id) {
|
|
8124
|
+
if (planCache.has(t.plan_id)) {
|
|
8125
|
+
planName = planCache.get(t.plan_id);
|
|
8126
|
+
} else {
|
|
8127
|
+
const pl = getPlan(t.plan_id);
|
|
8128
|
+
planName = pl?.name;
|
|
8129
|
+
if (planName)
|
|
8130
|
+
planCache.set(t.plan_id, planName);
|
|
8131
|
+
}
|
|
7325
8132
|
}
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
parent_id: body.parent_id,
|
|
7332
|
-
tags: body.tags,
|
|
7333
|
-
assigned_to: body.assigned_to,
|
|
7334
|
-
agent_id: body.agent_id,
|
|
7335
|
-
status: body.status
|
|
7336
|
-
});
|
|
7337
|
-
return json(task, 201, port);
|
|
7338
|
-
} catch (e) {
|
|
7339
|
-
return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
|
|
7340
|
-
}
|
|
8133
|
+
return { ...t, project_name: projectName, plan_name: planName };
|
|
8134
|
+
});
|
|
8135
|
+
return json(enriched, 200, port);
|
|
8136
|
+
} catch (e) {
|
|
8137
|
+
return json({ error: e instanceof Error ? e.message : "Failed to list tasks" }, 500, port);
|
|
7341
8138
|
}
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
version: body.version,
|
|
7355
|
-
title: body.title,
|
|
7356
|
-
description: body.description,
|
|
7357
|
-
status: body.status,
|
|
7358
|
-
priority: body.priority,
|
|
7359
|
-
assigned_to: body.assigned_to,
|
|
7360
|
-
tags: body.tags,
|
|
7361
|
-
metadata: body.metadata
|
|
7362
|
-
});
|
|
7363
|
-
return json(task, 200, port);
|
|
7364
|
-
} catch (e) {
|
|
7365
|
-
const status = e instanceof Error && e.name === "VersionConflictError" ? 409 : 500;
|
|
7366
|
-
return json({ error: e instanceof Error ? e.message : "Failed to update task" }, status, port);
|
|
8139
|
+
}
|
|
8140
|
+
const taskGetMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
|
|
8141
|
+
if (taskGetMatch && method === "GET") {
|
|
8142
|
+
try {
|
|
8143
|
+
const id = taskGetMatch[1];
|
|
8144
|
+
const task = getTaskWithRelations(id);
|
|
8145
|
+
if (!task)
|
|
8146
|
+
return json({ error: "Task not found" }, 404, port);
|
|
8147
|
+
let projectName;
|
|
8148
|
+
if (task.project_id) {
|
|
8149
|
+
const p = getProject(task.project_id);
|
|
8150
|
+
projectName = p?.name;
|
|
7367
8151
|
}
|
|
8152
|
+
return json({ ...task, project_name: projectName }, 200, port);
|
|
8153
|
+
} catch (e) {
|
|
8154
|
+
return json({ error: e instanceof Error ? e.message : "Failed to get task" }, 500, port);
|
|
7368
8155
|
}
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
8156
|
+
}
|
|
8157
|
+
if (path === "/api/tasks" && method === "POST") {
|
|
8158
|
+
try {
|
|
8159
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
8160
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
8161
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
8162
|
+
const body = await parseJsonBody(req);
|
|
8163
|
+
if (!body)
|
|
8164
|
+
return json({ error: "Invalid JSON" }, 400, port);
|
|
8165
|
+
if (!body.title || typeof body.title !== "string") {
|
|
8166
|
+
return json({ error: "Missing required field: title" }, 400, port);
|
|
8167
|
+
}
|
|
8168
|
+
const parsed = createTaskSchema.safeParse(body);
|
|
8169
|
+
if (!parsed.success) {
|
|
8170
|
+
return json({ error: "Invalid request body" }, 400, port);
|
|
8171
|
+
}
|
|
8172
|
+
const task = createTask({
|
|
8173
|
+
title: parsed.data.title,
|
|
8174
|
+
description: parsed.data.description,
|
|
8175
|
+
priority: parsed.data.priority,
|
|
8176
|
+
project_id: parsed.data.project_id,
|
|
8177
|
+
parent_id: parsed.data.parent_id,
|
|
8178
|
+
plan_id: parsed.data.plan_id,
|
|
8179
|
+
tags: parsed.data.tags,
|
|
8180
|
+
assigned_to: parsed.data.assigned_to,
|
|
8181
|
+
agent_id: parsed.data.agent_id,
|
|
8182
|
+
status: parsed.data.status
|
|
8183
|
+
});
|
|
8184
|
+
return json(task, 201, port);
|
|
8185
|
+
} catch (e) {
|
|
8186
|
+
return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
|
|
7380
8187
|
}
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
return json(
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
return json({ error:
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
}
|
|
7414
|
-
return json({ error: e instanceof Error ? e.message : "Failed to list comments" }, 500, port);
|
|
7415
|
-
}
|
|
8188
|
+
}
|
|
8189
|
+
const taskPatchMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
|
|
8190
|
+
if (taskPatchMatch && method === "PATCH") {
|
|
8191
|
+
try {
|
|
8192
|
+
const id = taskPatchMatch[1];
|
|
8193
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
8194
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
8195
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
8196
|
+
const body = await parseJsonBody(req);
|
|
8197
|
+
if (!body)
|
|
8198
|
+
return json({ error: "Invalid JSON" }, 400, port);
|
|
8199
|
+
if (typeof body.version !== "number") {
|
|
8200
|
+
return json({ error: "Missing required field: version" }, 400, port);
|
|
8201
|
+
}
|
|
8202
|
+
const parsed = updateTaskSchema.safeParse(body);
|
|
8203
|
+
if (!parsed.success) {
|
|
8204
|
+
return json({ error: "Invalid request body" }, 400, port);
|
|
8205
|
+
}
|
|
8206
|
+
const task = updateTask(id, {
|
|
8207
|
+
version: parsed.data.version,
|
|
8208
|
+
title: parsed.data.title,
|
|
8209
|
+
description: parsed.data.description,
|
|
8210
|
+
status: parsed.data.status,
|
|
8211
|
+
priority: parsed.data.priority,
|
|
8212
|
+
assigned_to: parsed.data.assigned_to,
|
|
8213
|
+
plan_id: parsed.data.plan_id,
|
|
8214
|
+
tags: parsed.data.tags,
|
|
8215
|
+
metadata: parsed.data.metadata
|
|
8216
|
+
});
|
|
8217
|
+
return json(task, 200, port);
|
|
8218
|
+
} catch (e) {
|
|
8219
|
+
const status = e instanceof Error && e.name === "VersionConflictError" ? 409 : 500;
|
|
8220
|
+
return json({ error: e instanceof Error ? e.message : "Failed to update task" }, status, port);
|
|
7416
8221
|
}
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
const comment = addComment({
|
|
7429
|
-
task_id: taskId,
|
|
7430
|
-
content: body.content,
|
|
7431
|
-
agent_id: body.agent_id,
|
|
7432
|
-
session_id: body.session_id
|
|
7433
|
-
});
|
|
7434
|
-
return json(comment, 201, port);
|
|
7435
|
-
} catch (e) {
|
|
7436
|
-
return json({ error: e instanceof Error ? e.message : "Failed to add comment" }, 500, port);
|
|
7437
|
-
}
|
|
8222
|
+
}
|
|
8223
|
+
const taskDeleteMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
|
|
8224
|
+
if (taskDeleteMatch && method === "DELETE") {
|
|
8225
|
+
try {
|
|
8226
|
+
const id = taskDeleteMatch[1];
|
|
8227
|
+
const deleted = deleteTask(id);
|
|
8228
|
+
if (!deleted)
|
|
8229
|
+
return json({ error: "Task not found" }, 404, port);
|
|
8230
|
+
return json({ deleted: true }, 200, port);
|
|
8231
|
+
} catch (e) {
|
|
8232
|
+
return json({ error: e instanceof Error ? e.message : "Failed to delete task" }, 500, port);
|
|
7438
8233
|
}
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
8234
|
+
}
|
|
8235
|
+
const taskStartMatch = path.match(/^\/api\/tasks\/([^/]+)\/start$/);
|
|
8236
|
+
if (taskStartMatch && method === "POST") {
|
|
8237
|
+
try {
|
|
8238
|
+
const id = taskStartMatch[1];
|
|
8239
|
+
const body = await parseJsonBody(req);
|
|
8240
|
+
if (!body)
|
|
8241
|
+
return json({ error: "Invalid JSON" }, 400, port);
|
|
8242
|
+
const parsed = agentSchema.safeParse(body);
|
|
8243
|
+
if (!parsed.success)
|
|
8244
|
+
return json({ error: "Invalid request body" }, 400, port);
|
|
8245
|
+
const agentId = parsed.data.agent_id || "dashboard";
|
|
8246
|
+
const task = startTask(id, agentId);
|
|
8247
|
+
return json(task, 200, port);
|
|
8248
|
+
} catch (e) {
|
|
8249
|
+
const status = e instanceof Error && e.name === "TaskNotFoundError" ? 404 : e instanceof Error && e.name === "LockError" ? 409 : 500;
|
|
8250
|
+
return json({ error: e instanceof Error ? e.message : "Failed to start task" }, status, port);
|
|
8251
|
+
}
|
|
8252
|
+
}
|
|
8253
|
+
const taskCompleteMatch = path.match(/^\/api\/tasks\/([^/]+)\/complete$/);
|
|
8254
|
+
if (taskCompleteMatch && method === "POST") {
|
|
8255
|
+
try {
|
|
8256
|
+
const id = taskCompleteMatch[1];
|
|
8257
|
+
const body = await parseJsonBody(req);
|
|
8258
|
+
if (!body)
|
|
8259
|
+
return json({ error: "Invalid JSON" }, 400, port);
|
|
8260
|
+
const parsed = agentSchema.safeParse(body);
|
|
8261
|
+
if (!parsed.success)
|
|
8262
|
+
return json({ error: "Invalid request body" }, 400, port);
|
|
8263
|
+
const agentId = parsed.data.agent_id;
|
|
8264
|
+
const task = completeTask(id, agentId);
|
|
8265
|
+
return json(task, 200, port);
|
|
8266
|
+
} catch (e) {
|
|
8267
|
+
const status = e instanceof Error && e.name === "TaskNotFoundError" ? 404 : e instanceof Error && e.name === "LockError" ? 409 : 500;
|
|
8268
|
+
return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, status, port);
|
|
8269
|
+
}
|
|
8270
|
+
}
|
|
8271
|
+
const commentsGetMatch = path.match(/^\/api\/tasks\/([^/]+)\/comments$/);
|
|
8272
|
+
if (commentsGetMatch && method === "GET") {
|
|
8273
|
+
try {
|
|
8274
|
+
const taskId = commentsGetMatch[1];
|
|
8275
|
+
const comments = listComments(taskId);
|
|
8276
|
+
return json(comments, 200, port);
|
|
8277
|
+
} catch (e) {
|
|
8278
|
+
return json({ error: e instanceof Error ? e.message : "Failed to list comments" }, 500, port);
|
|
7446
8279
|
}
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
return json({ error:
|
|
7464
|
-
}
|
|
8280
|
+
}
|
|
8281
|
+
const commentsPostMatch = path.match(/^\/api\/tasks\/([^/]+)\/comments$/);
|
|
8282
|
+
if (commentsPostMatch && method === "POST") {
|
|
8283
|
+
try {
|
|
8284
|
+
const taskId = commentsPostMatch[1];
|
|
8285
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
8286
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
8287
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
8288
|
+
const body = await parseJsonBody(req);
|
|
8289
|
+
if (!body)
|
|
8290
|
+
return json({ error: "Invalid JSON" }, 400, port);
|
|
8291
|
+
if (!body.content || typeof body.content !== "string") {
|
|
8292
|
+
return json({ error: "Missing required field: content" }, 400, port);
|
|
8293
|
+
}
|
|
8294
|
+
const parsed = createCommentSchema.safeParse(body);
|
|
8295
|
+
if (!parsed.success) {
|
|
8296
|
+
return json({ error: "Invalid request body" }, 400, port);
|
|
8297
|
+
}
|
|
8298
|
+
const comment = addComment({
|
|
8299
|
+
task_id: taskId,
|
|
8300
|
+
content: parsed.data.content,
|
|
8301
|
+
agent_id: parsed.data.agent_id,
|
|
8302
|
+
session_id: parsed.data.session_id
|
|
8303
|
+
});
|
|
8304
|
+
return json(comment, 201, port);
|
|
8305
|
+
} catch (e) {
|
|
8306
|
+
return json({ error: e instanceof Error ? e.message : "Failed to add comment" }, 500, port);
|
|
7465
8307
|
}
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
return json(results, 200, port);
|
|
7474
|
-
} catch (e) {
|
|
7475
|
-
return json({ error: e instanceof Error ? e.message : "Search failed" }, 500, port);
|
|
7476
|
-
}
|
|
8308
|
+
}
|
|
8309
|
+
if (path === "/api/projects" && method === "GET") {
|
|
8310
|
+
try {
|
|
8311
|
+
const projects = listProjects();
|
|
8312
|
+
return json(projects, 200, port);
|
|
8313
|
+
} catch (e) {
|
|
8314
|
+
return json({ error: e instanceof Error ? e.message : "Failed to list projects" }, 500, port);
|
|
7477
8315
|
}
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
8316
|
+
}
|
|
8317
|
+
if (path === "/api/projects" && method === "POST") {
|
|
8318
|
+
try {
|
|
8319
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
8320
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
8321
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
8322
|
+
const body = await parseJsonBody(req);
|
|
8323
|
+
if (!body)
|
|
8324
|
+
return json({ error: "Invalid JSON" }, 400, port);
|
|
8325
|
+
if (!body.name || typeof body.name !== "string") {
|
|
8326
|
+
return json({ error: "Missing required field: name" }, 400, port);
|
|
8327
|
+
}
|
|
8328
|
+
const parsed = createProjectSchema.safeParse(body);
|
|
8329
|
+
if (!parsed.success) {
|
|
8330
|
+
return json({ error: "Invalid request body" }, 400, port);
|
|
8331
|
+
}
|
|
8332
|
+
const project = createProject({
|
|
8333
|
+
name: parsed.data.name,
|
|
8334
|
+
path: parsed.data.path || process.cwd(),
|
|
8335
|
+
description: parsed.data.description,
|
|
8336
|
+
task_list_id: parsed.data.task_list_id
|
|
8337
|
+
});
|
|
8338
|
+
return json(project, 201, port);
|
|
8339
|
+
} catch (e) {
|
|
8340
|
+
return json({ error: e instanceof Error ? e.message : "Failed to create project" }, 500, port);
|
|
8341
|
+
}
|
|
8342
|
+
}
|
|
8343
|
+
if (path === "/api/search" && method === "GET") {
|
|
8344
|
+
try {
|
|
8345
|
+
const q = url.searchParams.get("q");
|
|
8346
|
+
if (!q)
|
|
8347
|
+
return json({ error: "Missing query parameter: q" }, 400, port);
|
|
8348
|
+
const projectId = url.searchParams.get("project_id") || undefined;
|
|
8349
|
+
const results = searchTasks(q, projectId);
|
|
8350
|
+
return json(results, 200, port);
|
|
8351
|
+
} catch (e) {
|
|
8352
|
+
return json({ error: e instanceof Error ? e.message : "Search failed" }, 500, port);
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
8355
|
+
if (path === "/api/system/version" && method === "GET") {
|
|
8356
|
+
try {
|
|
8357
|
+
const current = getPackageVersion();
|
|
8358
|
+
const npmRes = await fetch("https://registry.npmjs.org/@hasna/todos/latest");
|
|
8359
|
+
if (!npmRes.ok) {
|
|
7490
8360
|
return json({ current, latest: current, updateAvailable: false }, 200, port);
|
|
7491
8361
|
}
|
|
8362
|
+
const data = await npmRes.json();
|
|
8363
|
+
const latest = data.version;
|
|
8364
|
+
return json({ current, latest, updateAvailable: current !== latest }, 200, port);
|
|
8365
|
+
} catch {
|
|
8366
|
+
const current = getPackageVersion();
|
|
8367
|
+
return json({ current, latest: current, updateAvailable: false }, 200, port);
|
|
7492
8368
|
}
|
|
7493
|
-
|
|
8369
|
+
}
|
|
8370
|
+
if (path === "/api/system/update" && method === "POST") {
|
|
8371
|
+
try {
|
|
8372
|
+
let useBun = false;
|
|
7494
8373
|
try {
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
8374
|
+
execSync("which bun", { stdio: "ignore" });
|
|
8375
|
+
useBun = true;
|
|
8376
|
+
} catch {}
|
|
8377
|
+
const cmd = useBun ? "bun add -g @hasna/todos@latest" : "npm install -g @hasna/todos@latest";
|
|
8378
|
+
execSync(cmd, { stdio: "ignore", timeout: 60000 });
|
|
8379
|
+
return json({ success: true, message: "Updated! Restart the server to use the new version." }, 200, port);
|
|
8380
|
+
} catch (e) {
|
|
8381
|
+
return json({ success: false, message: e instanceof Error ? e.message : "Update failed" }, 500, port);
|
|
8382
|
+
}
|
|
8383
|
+
}
|
|
8384
|
+
if (path === "/api/plans" && method === "GET") {
|
|
8385
|
+
try {
|
|
8386
|
+
const projectId = url.searchParams.get("project_id") || undefined;
|
|
8387
|
+
const plans = listPlans(projectId);
|
|
8388
|
+
const enriched = plans.map((p) => {
|
|
8389
|
+
const db = getDatabase();
|
|
8390
|
+
const row = db.query("SELECT COUNT(*) as count FROM tasks WHERE plan_id = ?").get(p.id);
|
|
8391
|
+
let projectName;
|
|
8392
|
+
if (p.project_id) {
|
|
8393
|
+
const proj = getProject(p.project_id);
|
|
8394
|
+
projectName = proj?.name;
|
|
7513
8395
|
}
|
|
8396
|
+
return { ...p, task_count: row?.count ?? 0, project_name: projectName };
|
|
7514
8397
|
});
|
|
8398
|
+
return json(enriched, 200, port);
|
|
8399
|
+
} catch (e) {
|
|
8400
|
+
return json({ error: e instanceof Error ? e.message : "Failed to list plans" }, 500, port);
|
|
8401
|
+
}
|
|
8402
|
+
}
|
|
8403
|
+
if (path === "/api/plans" && method === "POST") {
|
|
8404
|
+
try {
|
|
8405
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
8406
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
8407
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
8408
|
+
const body = await parseJsonBody(req);
|
|
8409
|
+
if (!body)
|
|
8410
|
+
return json({ error: "Invalid JSON" }, 400, port);
|
|
8411
|
+
if (!body.name || typeof body.name !== "string") {
|
|
8412
|
+
return json({ error: "Missing required field: name" }, 400, port);
|
|
8413
|
+
}
|
|
8414
|
+
const parsed = createPlanSchema.safeParse(body);
|
|
8415
|
+
if (!parsed.success) {
|
|
8416
|
+
return json({ error: "Invalid request body" }, 400, port);
|
|
8417
|
+
}
|
|
8418
|
+
const plan = createPlan(parsed.data);
|
|
8419
|
+
return json(plan, 201, port);
|
|
8420
|
+
} catch (e) {
|
|
8421
|
+
return json({ error: e instanceof Error ? e.message : "Failed to create plan" }, 500, port);
|
|
8422
|
+
}
|
|
8423
|
+
}
|
|
8424
|
+
const planGetMatch = path.match(/^\/api\/plans\/([^/]+)$/);
|
|
8425
|
+
if (planGetMatch && method === "GET") {
|
|
8426
|
+
try {
|
|
8427
|
+
const id = planGetMatch[1];
|
|
8428
|
+
const plan = getPlan(id);
|
|
8429
|
+
if (!plan)
|
|
8430
|
+
return json({ error: "Plan not found" }, 404, port);
|
|
8431
|
+
return json(plan, 200, port);
|
|
8432
|
+
} catch (e) {
|
|
8433
|
+
return json({ error: e instanceof Error ? e.message : "Failed to get plan" }, 500, port);
|
|
8434
|
+
}
|
|
8435
|
+
}
|
|
8436
|
+
const planPatchMatch = path.match(/^\/api\/plans\/([^/]+)$/);
|
|
8437
|
+
if (planPatchMatch && method === "PATCH") {
|
|
8438
|
+
try {
|
|
8439
|
+
const id = planPatchMatch[1];
|
|
8440
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
8441
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
8442
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
8443
|
+
const body = await parseJsonBody(req);
|
|
8444
|
+
if (!body)
|
|
8445
|
+
return json({ error: "Invalid JSON" }, 400, port);
|
|
8446
|
+
const parsed = updatePlanSchema.safeParse(body);
|
|
8447
|
+
if (!parsed.success) {
|
|
8448
|
+
return json({ error: "Invalid request body" }, 400, port);
|
|
8449
|
+
}
|
|
8450
|
+
const plan = updatePlan(id, parsed.data);
|
|
8451
|
+
return json(plan, 200, port);
|
|
8452
|
+
} catch (e) {
|
|
8453
|
+
const status = e instanceof Error && e.name === "PlanNotFoundError" ? 404 : 500;
|
|
8454
|
+
return json({ error: e instanceof Error ? e.message : "Failed to update plan" }, status, port);
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
8457
|
+
const planDeleteMatch = path.match(/^\/api\/plans\/([^/]+)$/);
|
|
8458
|
+
if (planDeleteMatch && method === "DELETE") {
|
|
8459
|
+
try {
|
|
8460
|
+
const id = planDeleteMatch[1];
|
|
8461
|
+
const deleted = deletePlan(id);
|
|
8462
|
+
if (!deleted)
|
|
8463
|
+
return json({ error: "Plan not found" }, 404, port);
|
|
8464
|
+
return json({ deleted: true }, 200, port);
|
|
8465
|
+
} catch (e) {
|
|
8466
|
+
return json({ error: e instanceof Error ? e.message : "Failed to delete plan" }, 500, port);
|
|
7515
8467
|
}
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
8468
|
+
}
|
|
8469
|
+
const projectDeleteMatch = path.match(/^\/api\/projects\/([^/]+)$/);
|
|
8470
|
+
if (projectDeleteMatch && method === "DELETE") {
|
|
8471
|
+
try {
|
|
8472
|
+
const id = projectDeleteMatch[1];
|
|
8473
|
+
const deleted = deleteProject(id);
|
|
8474
|
+
if (!deleted)
|
|
8475
|
+
return json({ error: "Project not found" }, 404, port);
|
|
8476
|
+
return json({ deleted: true }, 200, port);
|
|
8477
|
+
} catch (e) {
|
|
8478
|
+
return json({ error: e instanceof Error ? e.message : "Failed to delete project" }, 500, port);
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
if (method === "OPTIONS") {
|
|
8482
|
+
return new Response(null, {
|
|
8483
|
+
headers: {
|
|
8484
|
+
"Access-Control-Allow-Origin": `http://localhost:${port}`,
|
|
8485
|
+
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
8486
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
7522
8487
|
}
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
8488
|
+
});
|
|
8489
|
+
}
|
|
8490
|
+
if (hasDashboard && (method === "GET" || method === "HEAD")) {
|
|
8491
|
+
if (path !== "/") {
|
|
8492
|
+
const filePath = join6(dir, path);
|
|
8493
|
+
const res2 = serveStaticFile(filePath);
|
|
8494
|
+
if (res2)
|
|
8495
|
+
return res2;
|
|
7527
8496
|
}
|
|
7528
|
-
|
|
8497
|
+
const indexPath = join6(dir, "index.html");
|
|
8498
|
+
const res = serveStaticFile(indexPath);
|
|
8499
|
+
if (res)
|
|
8500
|
+
return res;
|
|
7529
8501
|
}
|
|
7530
|
-
|
|
8502
|
+
return json({ error: "Not found" }, 404, port);
|
|
8503
|
+
};
|
|
8504
|
+
}
|
|
8505
|
+
async function startServer(port, options) {
|
|
8506
|
+
const shouldOpen = options?.open ?? true;
|
|
8507
|
+
const dashboardDir = resolveDashboardDir();
|
|
8508
|
+
const dashboardExists = existsSync6(dashboardDir);
|
|
8509
|
+
if (!dashboardExists) {
|
|
8510
|
+
console.error(`
|
|
8511
|
+
Dashboard not found at: ${dashboardDir}`);
|
|
8512
|
+
console.error(`Run this to build it:
|
|
8513
|
+
`);
|
|
8514
|
+
console.error(` cd dashboard && bun install && bun run build
|
|
8515
|
+
`);
|
|
8516
|
+
console.error(`Or from the project root:
|
|
8517
|
+
`);
|
|
8518
|
+
console.error(` bun run build:dashboard
|
|
8519
|
+
`);
|
|
8520
|
+
}
|
|
8521
|
+
let actualPort = port;
|
|
8522
|
+
const fetchHandler = createFetchHandler(() => actualPort, dashboardDir, dashboardExists);
|
|
8523
|
+
const attempts = port === 0 ? 20 : 1;
|
|
8524
|
+
let server2 = null;
|
|
8525
|
+
let lastError;
|
|
8526
|
+
for (let i = 0;i < attempts; i++) {
|
|
8527
|
+
const candidate = port === 0 ? randomPort() : port;
|
|
8528
|
+
try {
|
|
8529
|
+
server2 = Bun.serve({
|
|
8530
|
+
port: candidate,
|
|
8531
|
+
fetch: fetchHandler
|
|
8532
|
+
});
|
|
8533
|
+
actualPort = server2.port;
|
|
8534
|
+
break;
|
|
8535
|
+
} catch (e) {
|
|
8536
|
+
lastError = e;
|
|
8537
|
+
if (port !== 0) {
|
|
8538
|
+
throw e;
|
|
8539
|
+
}
|
|
8540
|
+
}
|
|
8541
|
+
}
|
|
8542
|
+
if (!server2) {
|
|
8543
|
+
throw lastError;
|
|
8544
|
+
}
|
|
7531
8545
|
const shutdown = () => {
|
|
7532
8546
|
server2.stop();
|
|
7533
8547
|
process.exit(0);
|
|
7534
8548
|
};
|
|
7535
8549
|
process.on("SIGINT", shutdown);
|
|
7536
8550
|
process.on("SIGTERM", shutdown);
|
|
7537
|
-
const serverUrl = `http://localhost:${
|
|
8551
|
+
const serverUrl = `http://localhost:${actualPort}`;
|
|
7538
8552
|
console.log(`Todos Dashboard running at ${serverUrl}`);
|
|
7539
8553
|
if (shouldOpen) {
|
|
7540
8554
|
try {
|
|
@@ -7545,10 +8559,13 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
7545
8559
|
}
|
|
7546
8560
|
return server2;
|
|
7547
8561
|
}
|
|
7548
|
-
var MIME_TYPES, SECURITY_HEADERS, MAX_BODY_SIZE;
|
|
8562
|
+
var MIME_TYPES, SECURITY_HEADERS, MAX_BODY_SIZE, createTaskSchema, updateTaskSchema, createProjectSchema, createCommentSchema, createPlanSchema, updatePlanSchema, agentSchema;
|
|
7549
8563
|
var init_serve = __esm(() => {
|
|
8564
|
+
init_zod();
|
|
7550
8565
|
init_tasks();
|
|
7551
8566
|
init_projects();
|
|
8567
|
+
init_plans();
|
|
8568
|
+
init_database();
|
|
7552
8569
|
init_comments();
|
|
7553
8570
|
init_search();
|
|
7554
8571
|
MIME_TYPES = {
|
|
@@ -7568,6 +8585,54 @@ var init_serve = __esm(() => {
|
|
|
7568
8585
|
"X-Frame-Options": "DENY"
|
|
7569
8586
|
};
|
|
7570
8587
|
MAX_BODY_SIZE = 1024 * 1024;
|
|
8588
|
+
createTaskSchema = exports_external.object({
|
|
8589
|
+
title: exports_external.string(),
|
|
8590
|
+
description: exports_external.string().optional(),
|
|
8591
|
+
status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
|
|
8592
|
+
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
8593
|
+
project_id: exports_external.string().optional(),
|
|
8594
|
+
parent_id: exports_external.string().optional(),
|
|
8595
|
+
plan_id: exports_external.string().optional(),
|
|
8596
|
+
tags: exports_external.array(exports_external.string()).optional(),
|
|
8597
|
+
assigned_to: exports_external.string().optional(),
|
|
8598
|
+
agent_id: exports_external.string().optional()
|
|
8599
|
+
});
|
|
8600
|
+
updateTaskSchema = exports_external.object({
|
|
8601
|
+
version: exports_external.number(),
|
|
8602
|
+
title: exports_external.string().optional(),
|
|
8603
|
+
description: exports_external.string().optional(),
|
|
8604
|
+
status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
|
|
8605
|
+
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
8606
|
+
assigned_to: exports_external.string().optional(),
|
|
8607
|
+
plan_id: exports_external.string().optional(),
|
|
8608
|
+
tags: exports_external.array(exports_external.string()).optional(),
|
|
8609
|
+
metadata: exports_external.record(exports_external.unknown()).optional()
|
|
8610
|
+
});
|
|
8611
|
+
createProjectSchema = exports_external.object({
|
|
8612
|
+
name: exports_external.string(),
|
|
8613
|
+
path: exports_external.string().optional(),
|
|
8614
|
+
description: exports_external.string().optional(),
|
|
8615
|
+
task_list_id: exports_external.string().optional()
|
|
8616
|
+
});
|
|
8617
|
+
createCommentSchema = exports_external.object({
|
|
8618
|
+
content: exports_external.string(),
|
|
8619
|
+
agent_id: exports_external.string().optional(),
|
|
8620
|
+
session_id: exports_external.string().optional()
|
|
8621
|
+
});
|
|
8622
|
+
createPlanSchema = exports_external.object({
|
|
8623
|
+
name: exports_external.string(),
|
|
8624
|
+
project_id: exports_external.string().optional(),
|
|
8625
|
+
description: exports_external.string().optional(),
|
|
8626
|
+
status: exports_external.enum(["active", "completed", "archived"]).optional()
|
|
8627
|
+
});
|
|
8628
|
+
updatePlanSchema = exports_external.object({
|
|
8629
|
+
name: exports_external.string().optional(),
|
|
8630
|
+
description: exports_external.string().optional(),
|
|
8631
|
+
status: exports_external.enum(["active", "completed", "archived"]).optional()
|
|
8632
|
+
});
|
|
8633
|
+
agentSchema = exports_external.object({
|
|
8634
|
+
agent_id: exports_external.string().optional()
|
|
8635
|
+
});
|
|
7571
8636
|
});
|
|
7572
8637
|
|
|
7573
8638
|
// src/cli/components/Header.tsx
|
|
@@ -8647,17 +9712,19 @@ var {
|
|
|
8647
9712
|
init_database();
|
|
8648
9713
|
init_tasks();
|
|
8649
9714
|
init_projects();
|
|
9715
|
+
init_plans();
|
|
8650
9716
|
init_comments();
|
|
8651
9717
|
init_search();
|
|
8652
|
-
|
|
9718
|
+
init_sync();
|
|
9719
|
+
init_config();
|
|
8653
9720
|
import chalk from "chalk";
|
|
8654
9721
|
import { execSync as execSync2 } from "child_process";
|
|
8655
|
-
import { existsSync as
|
|
8656
|
-
import { basename, dirname as dirname3, join as
|
|
9722
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
9723
|
+
import { basename, dirname as dirname3, join as join7, resolve as resolve2 } from "path";
|
|
8657
9724
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8658
9725
|
function getPackageVersion2() {
|
|
8659
9726
|
try {
|
|
8660
|
-
const pkgPath =
|
|
9727
|
+
const pkgPath = join7(dirname3(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
8661
9728
|
return JSON.parse(readFileSync3(pkgPath, "utf-8")).version || "0.0.0";
|
|
8662
9729
|
} catch {
|
|
8663
9730
|
return "0.0.0";
|
|
@@ -8684,20 +9751,21 @@ function detectGitRoot() {
|
|
|
8684
9751
|
return null;
|
|
8685
9752
|
}
|
|
8686
9753
|
}
|
|
8687
|
-
function
|
|
9754
|
+
function autoDetectProject(opts) {
|
|
8688
9755
|
if (opts.project) {
|
|
8689
|
-
|
|
8690
|
-
return p?.id;
|
|
9756
|
+
return getProjectByPath(resolve2(opts.project)) ?? undefined;
|
|
8691
9757
|
}
|
|
8692
9758
|
if (process.env["TODOS_AUTO_PROJECT"] === "false")
|
|
8693
9759
|
return;
|
|
8694
9760
|
const gitRoot = detectGitRoot();
|
|
8695
9761
|
if (gitRoot) {
|
|
8696
|
-
|
|
8697
|
-
return p.id;
|
|
9762
|
+
return ensureProject(basename(gitRoot), gitRoot);
|
|
8698
9763
|
}
|
|
8699
9764
|
return;
|
|
8700
9765
|
}
|
|
9766
|
+
function autoProject(opts) {
|
|
9767
|
+
return autoDetectProject(opts)?.id;
|
|
9768
|
+
}
|
|
8701
9769
|
function output(data, jsonMode) {
|
|
8702
9770
|
if (jsonMode) {
|
|
8703
9771
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -8722,10 +9790,11 @@ function formatTaskLine(t) {
|
|
|
8722
9790
|
const lock = t.locked_by ? chalk.magenta(` [locked:${t.locked_by}]`) : "";
|
|
8723
9791
|
const assigned = t.assigned_to ? chalk.cyan(` -> ${t.assigned_to}`) : "";
|
|
8724
9792
|
const tags = t.tags.length > 0 ? chalk.dim(` [${t.tags.join(",")}]`) : "";
|
|
8725
|
-
|
|
9793
|
+
const plan = t.plan_id ? chalk.magenta(` [plan:${t.plan_id.slice(0, 8)}]`) : "";
|
|
9794
|
+
return `${chalk.dim(t.id.slice(0, 8))} ${statusFn(t.status.padEnd(11))} ${priorityFn(t.priority.padEnd(8))} ${t.title}${assigned}${lock}${tags}${plan}`;
|
|
8726
9795
|
}
|
|
8727
9796
|
program2.name("todos").description("Universal task management for AI coding agents").version(getPackageVersion2()).option("--project <path>", "Project path").option("--json", "Output as JSON").option("--agent <name>", "Agent name").option("--session <id>", "Session ID");
|
|
8728
|
-
program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("--tags <tags>", "Comma-separated tags").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").action((title, opts) => {
|
|
9797
|
+
program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("--tags <tags>", "Comma-separated tags").option("--plan <id>", "Assign to a plan").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").action((title, opts) => {
|
|
8729
9798
|
const globalOpts = program2.opts();
|
|
8730
9799
|
const projectId = autoProject(globalOpts);
|
|
8731
9800
|
const task = createTask({
|
|
@@ -8734,6 +9803,15 @@ program2.command("add <title>").description("Create a new task").option("-d, --d
|
|
|
8734
9803
|
priority: opts.priority,
|
|
8735
9804
|
parent_id: opts.parent ? resolveTaskId(opts.parent) : undefined,
|
|
8736
9805
|
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
|
|
9806
|
+
plan_id: opts.plan ? (() => {
|
|
9807
|
+
const db = getDatabase();
|
|
9808
|
+
const id = resolvePartialId(db, "plans", opts.plan);
|
|
9809
|
+
if (!id) {
|
|
9810
|
+
console.error(chalk.red(`Could not resolve plan ID: ${opts.plan}`));
|
|
9811
|
+
process.exit(1);
|
|
9812
|
+
}
|
|
9813
|
+
return id;
|
|
9814
|
+
})() : undefined,
|
|
8737
9815
|
assigned_to: opts.assign,
|
|
8738
9816
|
status: opts.status,
|
|
8739
9817
|
agent_id: globalOpts.agent,
|
|
@@ -8810,6 +9888,8 @@ program2.command("show <id>").description("Show full task details").action((id)
|
|
|
8810
9888
|
console.log(` ${chalk.dim("Locked:")} ${task.locked_by} (at ${task.locked_at})`);
|
|
8811
9889
|
if (task.project_id)
|
|
8812
9890
|
console.log(` ${chalk.dim("Project:")} ${task.project_id}`);
|
|
9891
|
+
if (task.plan_id)
|
|
9892
|
+
console.log(` ${chalk.dim("Plan:")} ${task.plan_id}`);
|
|
8813
9893
|
if (task.working_dir)
|
|
8814
9894
|
console.log(` ${chalk.dim("WorkDir:")} ${task.working_dir}`);
|
|
8815
9895
|
if (task.parent)
|
|
@@ -8958,45 +10038,114 @@ program2.command("delete <id>").description("Delete a task").action((id) => {
|
|
|
8958
10038
|
process.exit(1);
|
|
8959
10039
|
}
|
|
8960
10040
|
});
|
|
8961
|
-
program2.command("
|
|
10041
|
+
program2.command("plans").description("List and manage plans").option("--add <name>", "Create a plan").option("-d, --description <text>", "Plan description (with --add)").option("--show <id>", "Show plan details with its tasks").option("--delete <id>", "Delete a plan").option("--complete <id>", "Mark a plan as completed").action((opts) => {
|
|
8962
10042
|
const globalOpts = program2.opts();
|
|
8963
10043
|
const projectId = autoProject(globalOpts);
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
const taskTitles = opts.tasks.split(",").map((t) => t.trim());
|
|
8976
|
-
for (const st of taskTitles) {
|
|
8977
|
-
subtasks.push(createTask({
|
|
8978
|
-
title: st,
|
|
8979
|
-
parent_id: parent.id,
|
|
8980
|
-
priority: opts.priority,
|
|
8981
|
-
agent_id: globalOpts.agent,
|
|
8982
|
-
session_id: globalOpts.session,
|
|
8983
|
-
project_id: projectId,
|
|
8984
|
-
working_dir: process.cwd()
|
|
8985
|
-
}));
|
|
10044
|
+
if (opts.add) {
|
|
10045
|
+
const plan = createPlan({
|
|
10046
|
+
name: opts.add,
|
|
10047
|
+
description: opts.description,
|
|
10048
|
+
project_id: projectId
|
|
10049
|
+
});
|
|
10050
|
+
if (globalOpts.json) {
|
|
10051
|
+
output(plan, true);
|
|
10052
|
+
} else {
|
|
10053
|
+
console.log(chalk.green("Plan created:"));
|
|
10054
|
+
console.log(`${chalk.dim(plan.id.slice(0, 8))} ${chalk.bold(plan.name)} ${chalk.cyan(`[${plan.status}]`)}`);
|
|
8986
10055
|
}
|
|
10056
|
+
return;
|
|
8987
10057
|
}
|
|
8988
|
-
if (
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
10058
|
+
if (opts.show) {
|
|
10059
|
+
const db = getDatabase();
|
|
10060
|
+
const resolvedId = resolvePartialId(db, "plans", opts.show);
|
|
10061
|
+
if (!resolvedId) {
|
|
10062
|
+
console.error(chalk.red(`Could not resolve plan ID: ${opts.show}`));
|
|
10063
|
+
process.exit(1);
|
|
10064
|
+
}
|
|
10065
|
+
const plan = getPlan(resolvedId);
|
|
10066
|
+
if (!plan) {
|
|
10067
|
+
console.error(chalk.red(`Plan not found: ${opts.show}`));
|
|
10068
|
+
process.exit(1);
|
|
10069
|
+
}
|
|
10070
|
+
const tasks = listTasks({ plan_id: resolvedId });
|
|
10071
|
+
if (globalOpts.json) {
|
|
10072
|
+
output({ plan, tasks }, true);
|
|
10073
|
+
return;
|
|
10074
|
+
}
|
|
10075
|
+
console.log(chalk.bold(`Plan Details:
|
|
10076
|
+
`));
|
|
10077
|
+
console.log(` ${chalk.dim("ID:")} ${plan.id}`);
|
|
10078
|
+
console.log(` ${chalk.dim("Name:")} ${plan.name}`);
|
|
10079
|
+
console.log(` ${chalk.dim("Status:")} ${chalk.cyan(plan.status)}`);
|
|
10080
|
+
if (plan.description)
|
|
10081
|
+
console.log(` ${chalk.dim("Desc:")} ${plan.description}`);
|
|
10082
|
+
if (plan.project_id)
|
|
10083
|
+
console.log(` ${chalk.dim("Project:")} ${plan.project_id}`);
|
|
10084
|
+
console.log(` ${chalk.dim("Created:")} ${plan.created_at}`);
|
|
10085
|
+
if (tasks.length > 0) {
|
|
8994
10086
|
console.log(chalk.bold(`
|
|
8995
|
-
|
|
8996
|
-
for (const
|
|
8997
|
-
console.log(`
|
|
10087
|
+
Tasks (${tasks.length}):`));
|
|
10088
|
+
for (const t of tasks) {
|
|
10089
|
+
console.log(` ${formatTaskLine(t)}`);
|
|
10090
|
+
}
|
|
10091
|
+
} else {
|
|
10092
|
+
console.log(chalk.dim(`
|
|
10093
|
+
No tasks in this plan.`));
|
|
10094
|
+
}
|
|
10095
|
+
return;
|
|
10096
|
+
}
|
|
10097
|
+
if (opts.delete) {
|
|
10098
|
+
const db = getDatabase();
|
|
10099
|
+
const resolvedId = resolvePartialId(db, "plans", opts.delete);
|
|
10100
|
+
if (!resolvedId) {
|
|
10101
|
+
console.error(chalk.red(`Could not resolve plan ID: ${opts.delete}`));
|
|
10102
|
+
process.exit(1);
|
|
10103
|
+
}
|
|
10104
|
+
const deleted = deletePlan(resolvedId);
|
|
10105
|
+
if (globalOpts.json) {
|
|
10106
|
+
output({ deleted }, true);
|
|
10107
|
+
} else if (deleted) {
|
|
10108
|
+
console.log(chalk.green("Plan deleted."));
|
|
10109
|
+
} else {
|
|
10110
|
+
console.error(chalk.red("Plan not found."));
|
|
10111
|
+
process.exit(1);
|
|
10112
|
+
}
|
|
10113
|
+
return;
|
|
10114
|
+
}
|
|
10115
|
+
if (opts.complete) {
|
|
10116
|
+
const db = getDatabase();
|
|
10117
|
+
const resolvedId = resolvePartialId(db, "plans", opts.complete);
|
|
10118
|
+
if (!resolvedId) {
|
|
10119
|
+
console.error(chalk.red(`Could not resolve plan ID: ${opts.complete}`));
|
|
10120
|
+
process.exit(1);
|
|
10121
|
+
}
|
|
10122
|
+
try {
|
|
10123
|
+
const plan = updatePlan(resolvedId, { status: "completed" });
|
|
10124
|
+
if (globalOpts.json) {
|
|
10125
|
+
output(plan, true);
|
|
10126
|
+
} else {
|
|
10127
|
+
console.log(chalk.green("Plan completed:"));
|
|
10128
|
+
console.log(`${chalk.dim(plan.id.slice(0, 8))} ${chalk.bold(plan.name)} ${chalk.cyan(`[${plan.status}]`)}`);
|
|
8998
10129
|
}
|
|
10130
|
+
} catch (e) {
|
|
10131
|
+
handleError(e);
|
|
8999
10132
|
}
|
|
10133
|
+
return;
|
|
10134
|
+
}
|
|
10135
|
+
const plans = listPlans(projectId);
|
|
10136
|
+
if (globalOpts.json) {
|
|
10137
|
+
output(plans, true);
|
|
10138
|
+
return;
|
|
10139
|
+
}
|
|
10140
|
+
if (plans.length === 0) {
|
|
10141
|
+
console.log(chalk.dim("No plans found."));
|
|
10142
|
+
return;
|
|
10143
|
+
}
|
|
10144
|
+
console.log(chalk.bold(`${plans.length} plan(s):
|
|
10145
|
+
`));
|
|
10146
|
+
for (const p of plans) {
|
|
10147
|
+
const desc = p.description ? chalk.dim(` - ${p.description}`) : "";
|
|
10148
|
+
console.log(`${chalk.dim(p.id.slice(0, 8))} ${chalk.bold(p.name)} ${chalk.cyan(`[${p.status}]`)}${desc}`);
|
|
9000
10149
|
}
|
|
9001
10150
|
});
|
|
9002
10151
|
program2.command("comment <id> <text>").description("Add a comment to a task").action((id, text) => {
|
|
@@ -9083,16 +10232,27 @@ program2.command("deps <id>").description("Manage task dependencies").option("--
|
|
|
9083
10232
|
}
|
|
9084
10233
|
}
|
|
9085
10234
|
});
|
|
9086
|
-
program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--name <name>", "Project name (with --add)").action((opts) => {
|
|
10235
|
+
program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--name <name>", "Project name (with --add)").option("--task-list-id <id>", "Custom task list ID (with --add)").action((opts) => {
|
|
9087
10236
|
const globalOpts = program2.opts();
|
|
9088
10237
|
if (opts.add) {
|
|
9089
10238
|
const projectPath = resolve2(opts.add);
|
|
9090
10239
|
const name = opts.name || basename(projectPath);
|
|
9091
|
-
const
|
|
10240
|
+
const existing = getProjectByPath(projectPath);
|
|
10241
|
+
let project;
|
|
10242
|
+
if (existing) {
|
|
10243
|
+
project = existing;
|
|
10244
|
+
if (opts.taskListId) {
|
|
10245
|
+
project = updateProject(existing.id, { task_list_id: opts.taskListId });
|
|
10246
|
+
}
|
|
10247
|
+
} else {
|
|
10248
|
+
project = createProject({ name, path: projectPath, task_list_id: opts.taskListId });
|
|
10249
|
+
}
|
|
9092
10250
|
if (globalOpts.json) {
|
|
9093
10251
|
output(project, true);
|
|
9094
10252
|
} else {
|
|
9095
10253
|
console.log(chalk.green(`Project registered: ${project.name} (${project.path})`));
|
|
10254
|
+
if (project.task_list_id)
|
|
10255
|
+
console.log(chalk.dim(` Task list: ${project.task_list_id}`));
|
|
9096
10256
|
}
|
|
9097
10257
|
return;
|
|
9098
10258
|
}
|
|
@@ -9108,7 +10268,8 @@ program2.command("projects").description("List and manage projects").option("--a
|
|
|
9108
10268
|
console.log(chalk.bold(`${projects.length} project(s):
|
|
9109
10269
|
`));
|
|
9110
10270
|
for (const p of projects) {
|
|
9111
|
-
|
|
10271
|
+
const taskList = p.task_list_id ? chalk.cyan(` [${p.task_list_id}]`) : "";
|
|
10272
|
+
console.log(`${chalk.dim(p.id.slice(0, 8))} ${chalk.bold(p.name)} ${chalk.dim(p.path)}${taskList}${p.description ? ` - ${p.description}` : ""}`);
|
|
9112
10273
|
}
|
|
9113
10274
|
});
|
|
9114
10275
|
program2.command("export").description("Export tasks").option("-f, --format <format>", "Format: json or md", "json").action((opts) => {
|
|
@@ -9128,33 +10289,43 @@ program2.command("export").description("Export tasks").option("-f, --format <for
|
|
|
9128
10289
|
console.log(JSON.stringify(tasks, null, 2));
|
|
9129
10290
|
}
|
|
9130
10291
|
});
|
|
9131
|
-
function
|
|
9132
|
-
|
|
10292
|
+
function resolveTaskListId2(agent, explicit, projectTaskListId) {
|
|
10293
|
+
if (explicit)
|
|
10294
|
+
return explicit;
|
|
10295
|
+
const normalized = agent.trim().toLowerCase();
|
|
10296
|
+
if (normalized === "claude" || normalized === "claude-code" || normalized === "claude_code") {
|
|
10297
|
+
return process.env["TODOS_CLAUDE_TASK_LIST"] || process.env["CLAUDE_CODE_TASK_LIST_ID"] || process.env["CLAUDE_CODE_SESSION_ID"] || getAgentTaskListId(normalized) || projectTaskListId || null;
|
|
10298
|
+
}
|
|
10299
|
+
const key = `TODOS_${normalized.toUpperCase()}_TASK_LIST`;
|
|
10300
|
+
return process.env[key] || process.env["TODOS_TASK_LIST_ID"] || getAgentTaskListId(normalized) || "default";
|
|
9133
10301
|
}
|
|
9134
|
-
program2.command("sync").description("Sync tasks with
|
|
10302
|
+
program2.command("sync").description("Sync tasks with an agent task list (Claude uses native task list; others use JSON lists)").option("--task-list <id>", "Task list ID (Claude auto-detects from CLAUDE_CODE_TASK_LIST_ID or CLAUDE_CODE_SESSION_ID)").option("--agent <name>", "Agent/provider to sync (default: claude)").option("--all", "Sync across all configured agents (TODOS_SYNC_AGENTS or default: claude,codex,gemini)").option("--push", "One-way: push SQLite tasks to agent task list").option("--pull", "One-way: pull agent task list into SQLite").option("--prefer <side>", "Conflict strategy: local or remote", "remote").action((opts) => {
|
|
9135
10303
|
const globalOpts = program2.opts();
|
|
9136
|
-
const
|
|
9137
|
-
const
|
|
9138
|
-
|
|
9139
|
-
console.error(chalk.red("Could not detect task list ID. Use --task-list <id>, or run inside a Claude Code session."));
|
|
9140
|
-
process.exit(1);
|
|
9141
|
-
}
|
|
10304
|
+
const project = autoDetectProject(globalOpts);
|
|
10305
|
+
const projectId = project?.id;
|
|
10306
|
+
const direction = opts.push && !opts.pull ? "push" : opts.pull && !opts.push ? "pull" : "both";
|
|
9142
10307
|
let result;
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
result =
|
|
10308
|
+
const prefer = opts.prefer === "local" ? "local" : "remote";
|
|
10309
|
+
if (opts.all) {
|
|
10310
|
+
const agents = defaultSyncAgents();
|
|
10311
|
+
result = syncWithAgents(agents, (agent) => resolveTaskListId2(agent, opts.taskList, project?.task_list_id), projectId, direction, { prefer });
|
|
9147
10312
|
} else {
|
|
9148
|
-
|
|
10313
|
+
const agent = opts.agent || "claude";
|
|
10314
|
+
const taskListId = resolveTaskListId2(agent, opts.taskList, project?.task_list_id);
|
|
10315
|
+
if (!taskListId) {
|
|
10316
|
+
console.error(chalk.red(`Could not detect task list ID for ${agent}. Use --task-list <id> or set appropriate env vars.`));
|
|
10317
|
+
process.exit(1);
|
|
10318
|
+
}
|
|
10319
|
+
result = syncWithAgent(agent, taskListId, projectId, direction, { prefer });
|
|
9149
10320
|
}
|
|
9150
10321
|
if (globalOpts.json) {
|
|
9151
10322
|
output(result, true);
|
|
9152
10323
|
return;
|
|
9153
10324
|
}
|
|
9154
10325
|
if (result.pulled > 0)
|
|
9155
|
-
console.log(chalk.green(`Pulled ${result.pulled} task(s)
|
|
10326
|
+
console.log(chalk.green(`Pulled ${result.pulled} task(s).`));
|
|
9156
10327
|
if (result.pushed > 0)
|
|
9157
|
-
console.log(chalk.green(`Pushed ${result.pushed} task(s)
|
|
10328
|
+
console.log(chalk.green(`Pushed ${result.pushed} task(s).`));
|
|
9158
10329
|
if (result.pulled === 0 && result.pushed === 0 && result.errors.length === 0) {
|
|
9159
10330
|
console.log(chalk.dim("Nothing to sync."));
|
|
9160
10331
|
}
|
|
@@ -9170,45 +10341,38 @@ hooks.command("install").description("Install Claude Code hooks for auto-sync").
|
|
|
9170
10341
|
if (p)
|
|
9171
10342
|
todosBin = p;
|
|
9172
10343
|
} catch {}
|
|
9173
|
-
const hooksDir =
|
|
9174
|
-
if (!
|
|
10344
|
+
const hooksDir = join7(process.cwd(), ".claude", "hooks");
|
|
10345
|
+
if (!existsSync7(hooksDir))
|
|
9175
10346
|
mkdirSync3(hooksDir, { recursive: true });
|
|
9176
10347
|
const hookScript = `#!/usr/bin/env bash
|
|
9177
10348
|
# Auto-generated by: todos hooks install
|
|
9178
10349
|
# Syncs todos with Claude Code task list on tool use events.
|
|
9179
|
-
#
|
|
10350
|
+
# Uses session_id when available; falls back to project-based task_list_id.
|
|
9180
10351
|
|
|
9181
10352
|
INPUT=$(cat)
|
|
9182
10353
|
|
|
9183
|
-
# Extract session_id from stdin JSON (hooks always receive this)
|
|
9184
10354
|
SESSION_ID=$(echo "$INPUT" | grep -o '"session_id":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || true)
|
|
9185
|
-
|
|
9186
|
-
# Task list priority: env override > session ID from hook input
|
|
9187
|
-
TASK_LIST="\${TODOS_CLAUDE_TASK_LIST:-\${CLAUDE_CODE_TASK_LIST_ID:-$SESSION_ID}}"
|
|
9188
|
-
|
|
9189
|
-
if [ -z "$TASK_LIST" ]; then
|
|
9190
|
-
exit 0
|
|
9191
|
-
fi
|
|
10355
|
+
TASK_LIST="\${TODOS_CLAUDE_TASK_LIST:-\${SESSION_ID}}"
|
|
9192
10356
|
|
|
9193
10357
|
TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || true)
|
|
9194
10358
|
|
|
9195
10359
|
case "$TOOL_NAME" in
|
|
9196
10360
|
TaskCreate|TaskUpdate)
|
|
9197
|
-
${todosBin} sync --
|
|
10361
|
+
TODOS_CLAUDE_TASK_LIST="$TASK_LIST" ${todosBin} sync --all --pull 2>/dev/null || true
|
|
9198
10362
|
;;
|
|
9199
10363
|
mcp__todos__*)
|
|
9200
|
-
${todosBin} sync --
|
|
10364
|
+
TODOS_CLAUDE_TASK_LIST="$TASK_LIST" ${todosBin} sync --all --push 2>/dev/null || true
|
|
9201
10365
|
;;
|
|
9202
10366
|
esac
|
|
9203
10367
|
|
|
9204
10368
|
exit 0
|
|
9205
10369
|
`;
|
|
9206
|
-
const hookPath =
|
|
10370
|
+
const hookPath = join7(hooksDir, "todos-sync.sh");
|
|
9207
10371
|
writeFileSync2(hookPath, hookScript);
|
|
9208
10372
|
execSync2(`chmod +x "${hookPath}"`);
|
|
9209
10373
|
console.log(chalk.green(`Hook script created: ${hookPath}`));
|
|
9210
|
-
const settingsPath =
|
|
9211
|
-
const settings =
|
|
10374
|
+
const settingsPath = join7(process.cwd(), ".claude", "settings.json");
|
|
10375
|
+
const settings = readJsonFile2(settingsPath);
|
|
9212
10376
|
if (!settings["hooks"]) {
|
|
9213
10377
|
settings["hooks"] = {};
|
|
9214
10378
|
}
|
|
@@ -9232,9 +10396,9 @@ exit 0
|
|
|
9232
10396
|
hooks: [{ type: "command", command: hookPath }]
|
|
9233
10397
|
});
|
|
9234
10398
|
hooksConfig["PostToolUse"] = filtered;
|
|
9235
|
-
|
|
10399
|
+
writeJsonFile2(settingsPath, settings);
|
|
9236
10400
|
console.log(chalk.green(`Claude Code hooks configured in: ${settingsPath}`));
|
|
9237
|
-
console.log(chalk.dim("Task list ID auto-detected from
|
|
10401
|
+
console.log(chalk.dim("Task list ID auto-detected from project."));
|
|
9238
10402
|
});
|
|
9239
10403
|
program2.command("mcp").description("Start MCP server (stdio)").option("--register <agent>", "Register MCP server with an agent (claude, codex, gemini, all)").option("--unregister <agent>", "Unregister MCP server from an agent (claude, codex, gemini, all)").option("-g, --global", "Register/unregister globally (user-level) instead of project-level").action(async (opts) => {
|
|
9240
10404
|
if (opts.register) {
|
|
@@ -9254,13 +10418,13 @@ function getMcpBinaryPath() {
|
|
|
9254
10418
|
if (p)
|
|
9255
10419
|
return p;
|
|
9256
10420
|
} catch {}
|
|
9257
|
-
const bunBin =
|
|
9258
|
-
if (
|
|
10421
|
+
const bunBin = join7(HOME2, ".bun", "bin", "todos-mcp");
|
|
10422
|
+
if (existsSync7(bunBin))
|
|
9259
10423
|
return bunBin;
|
|
9260
10424
|
return "todos-mcp";
|
|
9261
10425
|
}
|
|
9262
|
-
function
|
|
9263
|
-
if (!
|
|
10426
|
+
function readJsonFile2(path) {
|
|
10427
|
+
if (!existsSync7(path))
|
|
9264
10428
|
return {};
|
|
9265
10429
|
try {
|
|
9266
10430
|
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
@@ -9268,27 +10432,27 @@ function readJsonFile(path) {
|
|
|
9268
10432
|
return {};
|
|
9269
10433
|
}
|
|
9270
10434
|
}
|
|
9271
|
-
function
|
|
10435
|
+
function writeJsonFile2(path, data) {
|
|
9272
10436
|
const dir = dirname3(path);
|
|
9273
|
-
if (!
|
|
10437
|
+
if (!existsSync7(dir))
|
|
9274
10438
|
mkdirSync3(dir, { recursive: true });
|
|
9275
10439
|
writeFileSync2(path, JSON.stringify(data, null, 2) + `
|
|
9276
10440
|
`);
|
|
9277
10441
|
}
|
|
9278
10442
|
function readTomlFile(path) {
|
|
9279
|
-
if (!
|
|
10443
|
+
if (!existsSync7(path))
|
|
9280
10444
|
return "";
|
|
9281
10445
|
return readFileSync3(path, "utf-8");
|
|
9282
10446
|
}
|
|
9283
10447
|
function writeTomlFile(path, content) {
|
|
9284
10448
|
const dir = dirname3(path);
|
|
9285
|
-
if (!
|
|
10449
|
+
if (!existsSync7(dir))
|
|
9286
10450
|
mkdirSync3(dir, { recursive: true });
|
|
9287
10451
|
writeFileSync2(path, content);
|
|
9288
10452
|
}
|
|
9289
10453
|
function registerClaude(binPath, global) {
|
|
9290
|
-
const configPath = global ?
|
|
9291
|
-
const config =
|
|
10454
|
+
const configPath = global ? join7(HOME2, ".claude", ".mcp.json") : join7(process.cwd(), ".mcp.json");
|
|
10455
|
+
const config = readJsonFile2(configPath);
|
|
9292
10456
|
if (!config["mcpServers"]) {
|
|
9293
10457
|
config["mcpServers"] = {};
|
|
9294
10458
|
}
|
|
@@ -9297,25 +10461,25 @@ function registerClaude(binPath, global) {
|
|
|
9297
10461
|
command: binPath,
|
|
9298
10462
|
args: []
|
|
9299
10463
|
};
|
|
9300
|
-
|
|
10464
|
+
writeJsonFile2(configPath, config);
|
|
9301
10465
|
const scope = global ? "global" : "project";
|
|
9302
10466
|
console.log(chalk.green(`Claude Code (${scope}): registered in ${configPath}`));
|
|
9303
10467
|
}
|
|
9304
10468
|
function unregisterClaude(global) {
|
|
9305
|
-
const configPath = global ?
|
|
9306
|
-
const config =
|
|
10469
|
+
const configPath = global ? join7(HOME2, ".claude", ".mcp.json") : join7(process.cwd(), ".mcp.json");
|
|
10470
|
+
const config = readJsonFile2(configPath);
|
|
9307
10471
|
const servers = config["mcpServers"];
|
|
9308
10472
|
if (!servers || !("todos" in servers)) {
|
|
9309
10473
|
console.log(chalk.dim(`Claude Code: todos not found in ${configPath}`));
|
|
9310
10474
|
return;
|
|
9311
10475
|
}
|
|
9312
10476
|
delete servers["todos"];
|
|
9313
|
-
|
|
10477
|
+
writeJsonFile2(configPath, config);
|
|
9314
10478
|
const scope = global ? "global" : "project";
|
|
9315
10479
|
console.log(chalk.green(`Claude Code (${scope}): unregistered from ${configPath}`));
|
|
9316
10480
|
}
|
|
9317
10481
|
function registerCodex(binPath) {
|
|
9318
|
-
const configPath =
|
|
10482
|
+
const configPath = join7(HOME2, ".codex", "config.toml");
|
|
9319
10483
|
let content = readTomlFile(configPath);
|
|
9320
10484
|
content = removeTomlBlock(content, "mcp_servers.todos");
|
|
9321
10485
|
const block = `
|
|
@@ -9329,7 +10493,7 @@ args = []
|
|
|
9329
10493
|
console.log(chalk.green(`Codex CLI: registered in ${configPath}`));
|
|
9330
10494
|
}
|
|
9331
10495
|
function unregisterCodex() {
|
|
9332
|
-
const configPath =
|
|
10496
|
+
const configPath = join7(HOME2, ".codex", "config.toml");
|
|
9333
10497
|
let content = readTomlFile(configPath);
|
|
9334
10498
|
if (!content.includes("[mcp_servers.todos]")) {
|
|
9335
10499
|
console.log(chalk.dim(`Codex CLI: todos not found in ${configPath}`));
|
|
@@ -9362,8 +10526,8 @@ function removeTomlBlock(content, blockName) {
|
|
|
9362
10526
|
`);
|
|
9363
10527
|
}
|
|
9364
10528
|
function registerGemini(binPath) {
|
|
9365
|
-
const configPath =
|
|
9366
|
-
const config =
|
|
10529
|
+
const configPath = join7(HOME2, ".gemini", "settings.json");
|
|
10530
|
+
const config = readJsonFile2(configPath);
|
|
9367
10531
|
if (!config["mcpServers"]) {
|
|
9368
10532
|
config["mcpServers"] = {};
|
|
9369
10533
|
}
|
|
@@ -9372,19 +10536,19 @@ function registerGemini(binPath) {
|
|
|
9372
10536
|
command: binPath,
|
|
9373
10537
|
args: []
|
|
9374
10538
|
};
|
|
9375
|
-
|
|
10539
|
+
writeJsonFile2(configPath, config);
|
|
9376
10540
|
console.log(chalk.green(`Gemini CLI: registered in ${configPath}`));
|
|
9377
10541
|
}
|
|
9378
10542
|
function unregisterGemini() {
|
|
9379
|
-
const configPath =
|
|
9380
|
-
const config =
|
|
10543
|
+
const configPath = join7(HOME2, ".gemini", "settings.json");
|
|
10544
|
+
const config = readJsonFile2(configPath);
|
|
9381
10545
|
const servers = config["mcpServers"];
|
|
9382
10546
|
if (!servers || !("todos" in servers)) {
|
|
9383
10547
|
console.log(chalk.dim(`Gemini CLI: todos not found in ${configPath}`));
|
|
9384
10548
|
return;
|
|
9385
10549
|
}
|
|
9386
10550
|
delete servers["todos"];
|
|
9387
|
-
|
|
10551
|
+
writeJsonFile2(configPath, config);
|
|
9388
10552
|
console.log(chalk.green(`Gemini CLI: unregistered from ${configPath}`));
|
|
9389
10553
|
}
|
|
9390
10554
|
function registerMcp(agent, global) {
|