@hasna/todos 0.3.6 → 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 -482
- package/dist/index.d.ts +42 -0
- package/dist/index.js +845 -31
- package/dist/mcp/index.js +853 -75
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -4010,6 +4010,15 @@ class TaskNotFoundError extends Error {
|
|
|
4010
4010
|
this.name = "TaskNotFoundError";
|
|
4011
4011
|
}
|
|
4012
4012
|
}
|
|
4013
|
+
class PlanNotFoundError extends Error {
|
|
4014
|
+
planId;
|
|
4015
|
+
constructor(planId) {
|
|
4016
|
+
super(`Plan not found: ${planId}`);
|
|
4017
|
+
this.planId = planId;
|
|
4018
|
+
this.name = "PlanNotFoundError";
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4013
4022
|
class LockError extends Error {
|
|
4014
4023
|
taskId;
|
|
4015
4024
|
lockedBy;
|
|
@@ -4037,19 +4046,54 @@ import { Database } from "bun:sqlite";
|
|
|
4037
4046
|
import { existsSync, mkdirSync } from "fs";
|
|
4038
4047
|
import { dirname, join, resolve } from "path";
|
|
4039
4048
|
var LOCK_EXPIRY_MINUTES = 30;
|
|
4049
|
+
function isInMemoryDb(path) {
|
|
4050
|
+
return path === ":memory:" || path.startsWith("file::memory:");
|
|
4051
|
+
}
|
|
4052
|
+
function findNearestTodosDb(startDir) {
|
|
4053
|
+
let dir = resolve(startDir);
|
|
4054
|
+
while (true) {
|
|
4055
|
+
const candidate = join(dir, ".todos", "todos.db");
|
|
4056
|
+
if (existsSync(candidate))
|
|
4057
|
+
return candidate;
|
|
4058
|
+
const parent = dirname(dir);
|
|
4059
|
+
if (parent === dir)
|
|
4060
|
+
break;
|
|
4061
|
+
dir = parent;
|
|
4062
|
+
}
|
|
4063
|
+
return null;
|
|
4064
|
+
}
|
|
4065
|
+
function findGitRoot(startDir) {
|
|
4066
|
+
let dir = resolve(startDir);
|
|
4067
|
+
while (true) {
|
|
4068
|
+
if (existsSync(join(dir, ".git")))
|
|
4069
|
+
return dir;
|
|
4070
|
+
const parent = dirname(dir);
|
|
4071
|
+
if (parent === dir)
|
|
4072
|
+
break;
|
|
4073
|
+
dir = parent;
|
|
4074
|
+
}
|
|
4075
|
+
return null;
|
|
4076
|
+
}
|
|
4040
4077
|
function getDbPath() {
|
|
4041
4078
|
if (process.env["TODOS_DB_PATH"]) {
|
|
4042
4079
|
return process.env["TODOS_DB_PATH"];
|
|
4043
4080
|
}
|
|
4044
4081
|
const cwd = process.cwd();
|
|
4045
|
-
const
|
|
4046
|
-
if (
|
|
4047
|
-
return
|
|
4082
|
+
const nearest = findNearestTodosDb(cwd);
|
|
4083
|
+
if (nearest)
|
|
4084
|
+
return nearest;
|
|
4085
|
+
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4086
|
+
const gitRoot = findGitRoot(cwd);
|
|
4087
|
+
if (gitRoot) {
|
|
4088
|
+
return join(gitRoot, ".todos", "todos.db");
|
|
4089
|
+
}
|
|
4048
4090
|
}
|
|
4049
4091
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4050
4092
|
return join(home, ".todos", "todos.db");
|
|
4051
4093
|
}
|
|
4052
4094
|
function ensureDir(filePath) {
|
|
4095
|
+
if (isInMemoryDb(filePath))
|
|
4096
|
+
return;
|
|
4053
4097
|
const dir = dirname(resolve(filePath));
|
|
4054
4098
|
if (!existsSync(dir)) {
|
|
4055
4099
|
mkdirSync(dir, { recursive: true });
|
|
@@ -4131,6 +4175,37 @@ var MIGRATIONS = [
|
|
|
4131
4175
|
);
|
|
4132
4176
|
|
|
4133
4177
|
INSERT OR IGNORE INTO _migrations (id) VALUES (1);
|
|
4178
|
+
`,
|
|
4179
|
+
`
|
|
4180
|
+
ALTER TABLE projects ADD COLUMN task_list_id TEXT;
|
|
4181
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (2);
|
|
4182
|
+
`,
|
|
4183
|
+
`
|
|
4184
|
+
CREATE TABLE IF NOT EXISTS task_tags (
|
|
4185
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
4186
|
+
tag TEXT NOT NULL,
|
|
4187
|
+
PRIMARY KEY (task_id, tag)
|
|
4188
|
+
);
|
|
4189
|
+
CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag);
|
|
4190
|
+
CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id);
|
|
4191
|
+
|
|
4192
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (3);
|
|
4193
|
+
`,
|
|
4194
|
+
`
|
|
4195
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
4196
|
+
id TEXT PRIMARY KEY,
|
|
4197
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
4198
|
+
name TEXT NOT NULL,
|
|
4199
|
+
description TEXT,
|
|
4200
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
4201
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4202
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4203
|
+
);
|
|
4204
|
+
CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id);
|
|
4205
|
+
CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status);
|
|
4206
|
+
ALTER TABLE tasks ADD COLUMN plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL;
|
|
4207
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id);
|
|
4208
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (4);
|
|
4134
4209
|
`
|
|
4135
4210
|
];
|
|
4136
4211
|
var _db = null;
|
|
@@ -4144,6 +4219,7 @@ function getDatabase(dbPath) {
|
|
|
4144
4219
|
_db.run("PRAGMA busy_timeout = 5000");
|
|
4145
4220
|
_db.run("PRAGMA foreign_keys = ON");
|
|
4146
4221
|
runMigrations(_db);
|
|
4222
|
+
backfillTaskTags(_db);
|
|
4147
4223
|
return _db;
|
|
4148
4224
|
}
|
|
4149
4225
|
function runMigrations(db) {
|
|
@@ -4159,6 +4235,35 @@ function runMigrations(db) {
|
|
|
4159
4235
|
}
|
|
4160
4236
|
}
|
|
4161
4237
|
}
|
|
4238
|
+
function backfillTaskTags(db) {
|
|
4239
|
+
try {
|
|
4240
|
+
const count = db.query("SELECT COUNT(*) as count FROM task_tags").get();
|
|
4241
|
+
if (count && count.count > 0)
|
|
4242
|
+
return;
|
|
4243
|
+
} catch {
|
|
4244
|
+
return;
|
|
4245
|
+
}
|
|
4246
|
+
try {
|
|
4247
|
+
const rows = db.query("SELECT id, tags FROM tasks WHERE tags IS NOT NULL AND tags != '[]'").all();
|
|
4248
|
+
if (rows.length === 0)
|
|
4249
|
+
return;
|
|
4250
|
+
const insert = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
4251
|
+
for (const row of rows) {
|
|
4252
|
+
if (!row.tags)
|
|
4253
|
+
continue;
|
|
4254
|
+
let tags = [];
|
|
4255
|
+
try {
|
|
4256
|
+
tags = JSON.parse(row.tags);
|
|
4257
|
+
} catch {
|
|
4258
|
+
continue;
|
|
4259
|
+
}
|
|
4260
|
+
for (const tag of tags) {
|
|
4261
|
+
if (tag)
|
|
4262
|
+
insert.run(row.id, tag);
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
} catch {}
|
|
4266
|
+
}
|
|
4162
4267
|
function now() {
|
|
4163
4268
|
return new Date().toISOString();
|
|
4164
4269
|
}
|
|
@@ -4172,6 +4277,14 @@ function isLockExpired(lockedAt) {
|
|
|
4172
4277
|
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
4173
4278
|
return Date.now() - lockTime > expiryMs;
|
|
4174
4279
|
}
|
|
4280
|
+
function lockExpiryCutoff(nowMs = Date.now()) {
|
|
4281
|
+
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
4282
|
+
return new Date(nowMs - expiryMs).toISOString();
|
|
4283
|
+
}
|
|
4284
|
+
function clearExpiredLocks(db) {
|
|
4285
|
+
const cutoff = lockExpiryCutoff();
|
|
4286
|
+
db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
|
|
4287
|
+
}
|
|
4175
4288
|
function resolvePartialId(db, table, partialId) {
|
|
4176
4289
|
if (partialId.length >= 36) {
|
|
4177
4290
|
const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
|
|
@@ -4197,15 +4310,30 @@ function rowToTask(row) {
|
|
|
4197
4310
|
priority: row.priority
|
|
4198
4311
|
};
|
|
4199
4312
|
}
|
|
4313
|
+
function insertTaskTags(taskId, tags, db) {
|
|
4314
|
+
if (tags.length === 0)
|
|
4315
|
+
return;
|
|
4316
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
4317
|
+
for (const tag of tags) {
|
|
4318
|
+
if (tag)
|
|
4319
|
+
stmt.run(taskId, tag);
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
function replaceTaskTags(taskId, tags, db) {
|
|
4323
|
+
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
4324
|
+
insertTaskTags(taskId, tags, db);
|
|
4325
|
+
}
|
|
4200
4326
|
function createTask(input, db) {
|
|
4201
4327
|
const d = db || getDatabase();
|
|
4202
4328
|
const id = uuid();
|
|
4203
4329
|
const timestamp = now();
|
|
4204
|
-
|
|
4205
|
-
|
|
4330
|
+
const tags = input.tags || [];
|
|
4331
|
+
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)
|
|
4332
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)`, [
|
|
4206
4333
|
id,
|
|
4207
4334
|
input.project_id || null,
|
|
4208
4335
|
input.parent_id || null,
|
|
4336
|
+
input.plan_id || null,
|
|
4209
4337
|
input.title,
|
|
4210
4338
|
input.description || null,
|
|
4211
4339
|
input.status || "pending",
|
|
@@ -4214,11 +4342,14 @@ function createTask(input, db) {
|
|
|
4214
4342
|
input.assigned_to || null,
|
|
4215
4343
|
input.session_id || null,
|
|
4216
4344
|
input.working_dir || null,
|
|
4217
|
-
JSON.stringify(
|
|
4345
|
+
JSON.stringify(tags),
|
|
4218
4346
|
JSON.stringify(input.metadata || {}),
|
|
4219
4347
|
timestamp,
|
|
4220
4348
|
timestamp
|
|
4221
4349
|
]);
|
|
4350
|
+
if (tags.length > 0) {
|
|
4351
|
+
insertTaskTags(id, tags, d);
|
|
4352
|
+
}
|
|
4222
4353
|
return getTask(id, d);
|
|
4223
4354
|
}
|
|
4224
4355
|
function getTask(id, db) {
|
|
@@ -4256,6 +4387,7 @@ function getTaskWithRelations(id, db) {
|
|
|
4256
4387
|
}
|
|
4257
4388
|
function listTasks(filter = {}, db) {
|
|
4258
4389
|
const d = db || getDatabase();
|
|
4390
|
+
clearExpiredLocks(d);
|
|
4259
4391
|
const conditions = [];
|
|
4260
4392
|
const params = [];
|
|
4261
4393
|
if (filter.project_id) {
|
|
@@ -4301,9 +4433,13 @@ function listTasks(filter = {}, db) {
|
|
|
4301
4433
|
params.push(filter.session_id);
|
|
4302
4434
|
}
|
|
4303
4435
|
if (filter.tags && filter.tags.length > 0) {
|
|
4304
|
-
const
|
|
4305
|
-
conditions.push(`(${
|
|
4306
|
-
params.push(...filter.tags
|
|
4436
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
4437
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
4438
|
+
params.push(...filter.tags);
|
|
4439
|
+
}
|
|
4440
|
+
if (filter.plan_id) {
|
|
4441
|
+
conditions.push("plan_id = ?");
|
|
4442
|
+
params.push(filter.plan_id);
|
|
4307
4443
|
}
|
|
4308
4444
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4309
4445
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
|
|
@@ -4353,12 +4489,19 @@ function updateTask(id, input, db) {
|
|
|
4353
4489
|
sets.push("metadata = ?");
|
|
4354
4490
|
params.push(JSON.stringify(input.metadata));
|
|
4355
4491
|
}
|
|
4492
|
+
if (input.plan_id !== undefined) {
|
|
4493
|
+
sets.push("plan_id = ?");
|
|
4494
|
+
params.push(input.plan_id);
|
|
4495
|
+
}
|
|
4356
4496
|
params.push(id, input.version);
|
|
4357
4497
|
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
4358
4498
|
if (result.changes === 0) {
|
|
4359
4499
|
const current = getTask(id, d);
|
|
4360
4500
|
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
4361
4501
|
}
|
|
4502
|
+
if (input.tags !== undefined) {
|
|
4503
|
+
replaceTaskTags(id, input.tags, d);
|
|
4504
|
+
}
|
|
4362
4505
|
return getTask(id, d);
|
|
4363
4506
|
}
|
|
4364
4507
|
function deleteTask(id, db) {
|
|
@@ -4368,15 +4511,18 @@ function deleteTask(id, db) {
|
|
|
4368
4511
|
}
|
|
4369
4512
|
function startTask(id, agentId, db) {
|
|
4370
4513
|
const d = db || getDatabase();
|
|
4371
|
-
const
|
|
4372
|
-
if (!task)
|
|
4373
|
-
throw new TaskNotFoundError(id);
|
|
4374
|
-
if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
4375
|
-
throw new LockError(id, task.locked_by);
|
|
4376
|
-
}
|
|
4514
|
+
const cutoff = lockExpiryCutoff();
|
|
4377
4515
|
const timestamp = now();
|
|
4378
|
-
d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
4379
|
-
WHERE id =
|
|
4516
|
+
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
4517
|
+
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
4518
|
+
if (result.changes === 0) {
|
|
4519
|
+
const current = getTask(id, d);
|
|
4520
|
+
if (!current)
|
|
4521
|
+
throw new TaskNotFoundError(id);
|
|
4522
|
+
if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
|
|
4523
|
+
throw new LockError(id, current.locked_by);
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4380
4526
|
return getTask(id, d);
|
|
4381
4527
|
}
|
|
4382
4528
|
function completeTask(id, agentId, db) {
|
|
@@ -4397,20 +4543,26 @@ function lockTask(id, agentId, db) {
|
|
|
4397
4543
|
const task = getTask(id, d);
|
|
4398
4544
|
if (!task)
|
|
4399
4545
|
throw new TaskNotFoundError(id);
|
|
4400
|
-
if (task.locked_by === agentId) {
|
|
4546
|
+
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
4401
4547
|
return { success: true, locked_by: agentId, locked_at: task.locked_at };
|
|
4402
4548
|
}
|
|
4403
|
-
|
|
4404
|
-
return {
|
|
4405
|
-
success: false,
|
|
4406
|
-
locked_by: task.locked_by,
|
|
4407
|
-
locked_at: task.locked_at,
|
|
4408
|
-
error: `Task is locked by ${task.locked_by}`
|
|
4409
|
-
};
|
|
4410
|
-
}
|
|
4549
|
+
const cutoff = lockExpiryCutoff();
|
|
4411
4550
|
const timestamp = now();
|
|
4412
|
-
d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
4413
|
-
WHERE id =
|
|
4551
|
+
const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
4552
|
+
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
4553
|
+
if (result.changes === 0) {
|
|
4554
|
+
const current = getTask(id, d);
|
|
4555
|
+
if (!current)
|
|
4556
|
+
throw new TaskNotFoundError(id);
|
|
4557
|
+
if (current.locked_by && !isLockExpired(current.locked_at)) {
|
|
4558
|
+
return {
|
|
4559
|
+
success: false,
|
|
4560
|
+
locked_by: current.locked_by,
|
|
4561
|
+
locked_at: current.locked_at,
|
|
4562
|
+
error: `Task is locked by ${current.locked_by}`
|
|
4563
|
+
};
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4414
4566
|
return { success: true, locked_by: agentId, locked_at: timestamp };
|
|
4415
4567
|
}
|
|
4416
4568
|
function unlockTask(id, agentId, db) {
|
|
@@ -4485,12 +4637,16 @@ function getComment(id, db) {
|
|
|
4485
4637
|
}
|
|
4486
4638
|
|
|
4487
4639
|
// src/db/projects.ts
|
|
4640
|
+
function slugify(name) {
|
|
4641
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4642
|
+
}
|
|
4488
4643
|
function createProject(input, db) {
|
|
4489
4644
|
const d = db || getDatabase();
|
|
4490
4645
|
const id = uuid();
|
|
4491
4646
|
const timestamp = now();
|
|
4492
|
-
|
|
4493
|
-
|
|
4647
|
+
const taskListId = input.task_list_id ?? `todos-${slugify(input.name)}`;
|
|
4648
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, created_at, updated_at)
|
|
4649
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, timestamp, timestamp]);
|
|
4494
4650
|
return getProject(id, d);
|
|
4495
4651
|
}
|
|
4496
4652
|
function getProject(id, db) {
|
|
@@ -4503,6 +4659,64 @@ function listProjects(db) {
|
|
|
4503
4659
|
return d.query("SELECT * FROM projects ORDER BY name").all();
|
|
4504
4660
|
}
|
|
4505
4661
|
|
|
4662
|
+
// src/db/plans.ts
|
|
4663
|
+
function createPlan(input, db) {
|
|
4664
|
+
const d = db || getDatabase();
|
|
4665
|
+
const id = uuid();
|
|
4666
|
+
const timestamp = now();
|
|
4667
|
+
d.run(`INSERT INTO plans (id, project_id, name, description, status, created_at, updated_at)
|
|
4668
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
4669
|
+
id,
|
|
4670
|
+
input.project_id || null,
|
|
4671
|
+
input.name,
|
|
4672
|
+
input.description || null,
|
|
4673
|
+
input.status || "active",
|
|
4674
|
+
timestamp,
|
|
4675
|
+
timestamp
|
|
4676
|
+
]);
|
|
4677
|
+
return getPlan(id, d);
|
|
4678
|
+
}
|
|
4679
|
+
function getPlan(id, db) {
|
|
4680
|
+
const d = db || getDatabase();
|
|
4681
|
+
const row = d.query("SELECT * FROM plans WHERE id = ?").get(id);
|
|
4682
|
+
return row;
|
|
4683
|
+
}
|
|
4684
|
+
function listPlans(projectId, db) {
|
|
4685
|
+
const d = db || getDatabase();
|
|
4686
|
+
if (projectId) {
|
|
4687
|
+
return d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
|
|
4688
|
+
}
|
|
4689
|
+
return d.query("SELECT * FROM plans ORDER BY created_at DESC").all();
|
|
4690
|
+
}
|
|
4691
|
+
function updatePlan(id, input, db) {
|
|
4692
|
+
const d = db || getDatabase();
|
|
4693
|
+
const plan = getPlan(id, d);
|
|
4694
|
+
if (!plan)
|
|
4695
|
+
throw new PlanNotFoundError(id);
|
|
4696
|
+
const sets = ["updated_at = ?"];
|
|
4697
|
+
const params = [now()];
|
|
4698
|
+
if (input.name !== undefined) {
|
|
4699
|
+
sets.push("name = ?");
|
|
4700
|
+
params.push(input.name);
|
|
4701
|
+
}
|
|
4702
|
+
if (input.description !== undefined) {
|
|
4703
|
+
sets.push("description = ?");
|
|
4704
|
+
params.push(input.description);
|
|
4705
|
+
}
|
|
4706
|
+
if (input.status !== undefined) {
|
|
4707
|
+
sets.push("status = ?");
|
|
4708
|
+
params.push(input.status);
|
|
4709
|
+
}
|
|
4710
|
+
params.push(id);
|
|
4711
|
+
d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
4712
|
+
return getPlan(id, d);
|
|
4713
|
+
}
|
|
4714
|
+
function deletePlan(id, db) {
|
|
4715
|
+
const d = db || getDatabase();
|
|
4716
|
+
const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
|
|
4717
|
+
return result.changes > 0;
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4506
4720
|
// src/lib/search.ts
|
|
4507
4721
|
function rowToTask2(row) {
|
|
4508
4722
|
return {
|
|
@@ -4515,8 +4729,9 @@ function rowToTask2(row) {
|
|
|
4515
4729
|
}
|
|
4516
4730
|
function searchTasks(query, projectId, db) {
|
|
4517
4731
|
const d = db || getDatabase();
|
|
4732
|
+
clearExpiredLocks(d);
|
|
4518
4733
|
const pattern = `%${query}%`;
|
|
4519
|
-
let sql = `SELECT * FROM tasks WHERE (title LIKE ? OR description LIKE ? OR
|
|
4734
|
+
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 ?))`;
|
|
4520
4735
|
const params = [pattern, pattern, pattern];
|
|
4521
4736
|
if (projectId) {
|
|
4522
4737
|
sql += " AND project_id = ?";
|
|
@@ -4530,11 +4745,32 @@ function searchTasks(query, projectId, db) {
|
|
|
4530
4745
|
}
|
|
4531
4746
|
|
|
4532
4747
|
// src/lib/claude-tasks.ts
|
|
4533
|
-
import { existsSync as
|
|
4748
|
+
import { existsSync as existsSync3 } from "fs";
|
|
4749
|
+
import { join as join3 } from "path";
|
|
4750
|
+
|
|
4751
|
+
// src/lib/sync-utils.ts
|
|
4752
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync as readdirSync2, statSync, writeFileSync } from "fs";
|
|
4534
4753
|
import { join as join2 } from "path";
|
|
4535
4754
|
var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4536
|
-
function
|
|
4537
|
-
|
|
4755
|
+
function ensureDir2(dir) {
|
|
4756
|
+
if (!existsSync2(dir))
|
|
4757
|
+
mkdirSync2(dir, { recursive: true });
|
|
4758
|
+
}
|
|
4759
|
+
function listJsonFiles(dir) {
|
|
4760
|
+
if (!existsSync2(dir))
|
|
4761
|
+
return [];
|
|
4762
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".json"));
|
|
4763
|
+
}
|
|
4764
|
+
function readJsonFile(path) {
|
|
4765
|
+
try {
|
|
4766
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
4767
|
+
} catch {
|
|
4768
|
+
return null;
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
function writeJsonFile(path, data) {
|
|
4772
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + `
|
|
4773
|
+
`);
|
|
4538
4774
|
}
|
|
4539
4775
|
function readHighWaterMark(dir) {
|
|
4540
4776
|
const path = join2(dir, ".highwatermark");
|
|
@@ -4546,17 +4782,34 @@ function readHighWaterMark(dir) {
|
|
|
4546
4782
|
function writeHighWaterMark(dir, value) {
|
|
4547
4783
|
writeFileSync(join2(dir, ".highwatermark"), String(value));
|
|
4548
4784
|
}
|
|
4549
|
-
function
|
|
4785
|
+
function getFileMtimeMs(path) {
|
|
4550
4786
|
try {
|
|
4551
|
-
|
|
4552
|
-
return JSON.parse(content);
|
|
4787
|
+
return statSync(path).mtimeMs;
|
|
4553
4788
|
} catch {
|
|
4554
4789
|
return null;
|
|
4555
4790
|
}
|
|
4556
4791
|
}
|
|
4792
|
+
function parseTimestamp(value) {
|
|
4793
|
+
if (typeof value !== "string")
|
|
4794
|
+
return null;
|
|
4795
|
+
const parsed = Date.parse(value);
|
|
4796
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
4797
|
+
}
|
|
4798
|
+
function appendSyncConflict(metadata, conflict, limit = 5) {
|
|
4799
|
+
const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
|
|
4800
|
+
const next = [conflict, ...current].slice(0, limit);
|
|
4801
|
+
return { ...metadata, sync_conflicts: next };
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4804
|
+
// src/lib/claude-tasks.ts
|
|
4805
|
+
function getTaskListDir(taskListId) {
|
|
4806
|
+
return join3(HOME, ".claude", "tasks", taskListId);
|
|
4807
|
+
}
|
|
4808
|
+
function readClaudeTask(dir, filename) {
|
|
4809
|
+
return readJsonFile(join3(dir, filename));
|
|
4810
|
+
}
|
|
4557
4811
|
function writeClaudeTask(dir, task) {
|
|
4558
|
-
|
|
4559
|
-
`);
|
|
4812
|
+
writeJsonFile(join3(dir, `${task.id}.json`), task);
|
|
4560
4813
|
}
|
|
4561
4814
|
function toClaudeStatus(status) {
|
|
4562
4815
|
if (status === "pending" || status === "in_progress" || status === "completed") {
|
|
@@ -4567,7 +4820,7 @@ function toClaudeStatus(status) {
|
|
|
4567
4820
|
function toSqliteStatus(status) {
|
|
4568
4821
|
return status;
|
|
4569
4822
|
}
|
|
4570
|
-
function taskToClaudeTask(task, claudeTaskId) {
|
|
4823
|
+
function taskToClaudeTask(task, claudeTaskId, existingMeta) {
|
|
4571
4824
|
return {
|
|
4572
4825
|
id: claudeTaskId,
|
|
4573
4826
|
subject: task.title,
|
|
@@ -4578,39 +4831,80 @@ function taskToClaudeTask(task, claudeTaskId) {
|
|
|
4578
4831
|
blocks: [],
|
|
4579
4832
|
blockedBy: [],
|
|
4580
4833
|
metadata: {
|
|
4834
|
+
...existingMeta || {},
|
|
4581
4835
|
todos_id: task.id,
|
|
4582
|
-
priority: task.priority
|
|
4836
|
+
priority: task.priority,
|
|
4837
|
+
todos_updated_at: task.updated_at,
|
|
4838
|
+
todos_version: task.version
|
|
4583
4839
|
}
|
|
4584
4840
|
};
|
|
4585
4841
|
}
|
|
4586
|
-
function pushToClaudeTaskList(taskListId, projectId) {
|
|
4842
|
+
function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
4587
4843
|
const dir = getTaskListDir(taskListId);
|
|
4588
|
-
if (!
|
|
4589
|
-
|
|
4844
|
+
if (!existsSync3(dir))
|
|
4845
|
+
ensureDir2(dir);
|
|
4590
4846
|
const filter = {};
|
|
4591
4847
|
if (projectId)
|
|
4592
4848
|
filter["project_id"] = projectId;
|
|
4593
4849
|
const tasks = listTasks(filter);
|
|
4594
4850
|
const existingByTodosId = new Map;
|
|
4595
|
-
const files =
|
|
4851
|
+
const files = listJsonFiles(dir);
|
|
4596
4852
|
for (const f of files) {
|
|
4853
|
+
const path = join3(dir, f);
|
|
4597
4854
|
const ct = readClaudeTask(dir, f);
|
|
4598
4855
|
if (ct?.metadata?.["todos_id"]) {
|
|
4599
|
-
existingByTodosId.set(ct.metadata["todos_id"], ct);
|
|
4856
|
+
existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
|
|
4600
4857
|
}
|
|
4601
4858
|
}
|
|
4602
4859
|
let hwm = readHighWaterMark(dir);
|
|
4603
4860
|
let pushed = 0;
|
|
4604
4861
|
const errors2 = [];
|
|
4862
|
+
const prefer = options.prefer || "remote";
|
|
4605
4863
|
for (const task of tasks) {
|
|
4606
4864
|
try {
|
|
4607
4865
|
const existing = existingByTodosId.get(task.id);
|
|
4608
4866
|
if (existing) {
|
|
4609
|
-
const
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4867
|
+
const lastSyncedAt = parseTimestamp(existing.task.metadata?.["todos_updated_at"]);
|
|
4868
|
+
const localUpdatedAt = parseTimestamp(task.updated_at);
|
|
4869
|
+
const remoteUpdatedAt = existing.mtimeMs;
|
|
4870
|
+
let recordConflict = false;
|
|
4871
|
+
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
4872
|
+
if (prefer === "remote") {
|
|
4873
|
+
const conflict = {
|
|
4874
|
+
agent: "claude",
|
|
4875
|
+
direction: "push",
|
|
4876
|
+
prefer,
|
|
4877
|
+
local_updated_at: task.updated_at,
|
|
4878
|
+
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
4879
|
+
detected_at: new Date().toISOString()
|
|
4880
|
+
};
|
|
4881
|
+
const newMeta = appendSyncConflict(task.metadata, conflict);
|
|
4882
|
+
updateTask(task.id, { version: task.version, metadata: newMeta });
|
|
4883
|
+
errors2.push(`conflict push ${task.id}: remote newer`);
|
|
4884
|
+
continue;
|
|
4885
|
+
}
|
|
4886
|
+
recordConflict = true;
|
|
4887
|
+
}
|
|
4888
|
+
const updated = taskToClaudeTask(task, existing.task.id, existing.task.metadata);
|
|
4889
|
+
updated.blocks = existing.task.blocks;
|
|
4890
|
+
updated.blockedBy = existing.task.blockedBy;
|
|
4891
|
+
updated.activeForm = existing.task.activeForm;
|
|
4613
4892
|
writeClaudeTask(dir, updated);
|
|
4893
|
+
if (recordConflict) {
|
|
4894
|
+
const latest = getTask(task.id);
|
|
4895
|
+
if (latest) {
|
|
4896
|
+
const conflict = {
|
|
4897
|
+
agent: "claude",
|
|
4898
|
+
direction: "push",
|
|
4899
|
+
prefer,
|
|
4900
|
+
local_updated_at: latest.updated_at,
|
|
4901
|
+
remote_updated_at: remoteUpdatedAt ? new Date(remoteUpdatedAt).toISOString() : undefined,
|
|
4902
|
+
detected_at: new Date().toISOString()
|
|
4903
|
+
};
|
|
4904
|
+
const newMeta = appendSyncConflict(latest.metadata, conflict);
|
|
4905
|
+
updateTask(latest.id, { version: latest.version, metadata: newMeta });
|
|
4906
|
+
}
|
|
4907
|
+
}
|
|
4614
4908
|
} else {
|
|
4615
4909
|
const claudeId = String(hwm);
|
|
4616
4910
|
hwm++;
|
|
@@ -4630,14 +4924,15 @@ function pushToClaudeTaskList(taskListId, projectId) {
|
|
|
4630
4924
|
writeHighWaterMark(dir, hwm);
|
|
4631
4925
|
return { pushed, pulled: 0, errors: errors2 };
|
|
4632
4926
|
}
|
|
4633
|
-
function pullFromClaudeTaskList(taskListId, projectId) {
|
|
4927
|
+
function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
|
|
4634
4928
|
const dir = getTaskListDir(taskListId);
|
|
4635
|
-
if (!
|
|
4929
|
+
if (!existsSync3(dir)) {
|
|
4636
4930
|
return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
|
|
4637
4931
|
}
|
|
4638
4932
|
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
4639
4933
|
let pulled = 0;
|
|
4640
4934
|
const errors2 = [];
|
|
4935
|
+
const prefer = options.prefer || "remote";
|
|
4641
4936
|
const allTasks = listTasks({});
|
|
4642
4937
|
const byClaudeId = new Map;
|
|
4643
4938
|
for (const t of allTasks) {
|
|
@@ -4651,6 +4946,7 @@ function pullFromClaudeTaskList(taskListId, projectId) {
|
|
|
4651
4946
|
}
|
|
4652
4947
|
for (const f of files) {
|
|
4653
4948
|
try {
|
|
4949
|
+
const filePath = join3(dir, f);
|
|
4654
4950
|
const ct = readClaudeTask(dir, f);
|
|
4655
4951
|
if (!ct)
|
|
4656
4952
|
continue;
|
|
@@ -4661,13 +4957,33 @@ function pullFromClaudeTaskList(taskListId, projectId) {
|
|
|
4661
4957
|
const existingByTodos = todosId ? byTodosId.get(todosId) : undefined;
|
|
4662
4958
|
const existing = existingByMapping || existingByTodos;
|
|
4663
4959
|
if (existing) {
|
|
4960
|
+
const lastSyncedAt = parseTimestamp(ct.metadata?.["todos_updated_at"]);
|
|
4961
|
+
const localUpdatedAt = parseTimestamp(existing.updated_at);
|
|
4962
|
+
const remoteUpdatedAt = getFileMtimeMs(filePath);
|
|
4963
|
+
let conflictMeta = null;
|
|
4964
|
+
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
4965
|
+
const conflict = {
|
|
4966
|
+
agent: "claude",
|
|
4967
|
+
direction: "pull",
|
|
4968
|
+
prefer,
|
|
4969
|
+
local_updated_at: existing.updated_at,
|
|
4970
|
+
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
4971
|
+
detected_at: new Date().toISOString()
|
|
4972
|
+
};
|
|
4973
|
+
conflictMeta = appendSyncConflict(existing.metadata, conflict);
|
|
4974
|
+
if (prefer === "local") {
|
|
4975
|
+
updateTask(existing.id, { version: existing.version, metadata: conflictMeta });
|
|
4976
|
+
errors2.push(`conflict pull ${existing.id}: local newer`);
|
|
4977
|
+
continue;
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4664
4980
|
updateTask(existing.id, {
|
|
4665
4981
|
version: existing.version,
|
|
4666
4982
|
title: ct.subject,
|
|
4667
4983
|
description: ct.description || undefined,
|
|
4668
4984
|
status: toSqliteStatus(ct.status),
|
|
4669
4985
|
assigned_to: ct.owner || undefined,
|
|
4670
|
-
metadata: { ...existing.metadata, claude_task_id: ct.id }
|
|
4986
|
+
metadata: { ...conflictMeta || existing.metadata, claude_task_id: ct.id, ...ct.metadata }
|
|
4671
4987
|
});
|
|
4672
4988
|
} else {
|
|
4673
4989
|
createTask({
|
|
@@ -4676,7 +4992,7 @@ function pullFromClaudeTaskList(taskListId, projectId) {
|
|
|
4676
4992
|
status: toSqliteStatus(ct.status),
|
|
4677
4993
|
assigned_to: ct.owner || undefined,
|
|
4678
4994
|
project_id: projectId,
|
|
4679
|
-
metadata: { claude_task_id: ct.id },
|
|
4995
|
+
metadata: { ...ct.metadata, claude_task_id: ct.id },
|
|
4680
4996
|
priority: ct.metadata?.["priority"] || "medium"
|
|
4681
4997
|
});
|
|
4682
4998
|
}
|
|
@@ -4687,9 +5003,9 @@ function pullFromClaudeTaskList(taskListId, projectId) {
|
|
|
4687
5003
|
}
|
|
4688
5004
|
return { pushed: 0, pulled, errors: errors2 };
|
|
4689
5005
|
}
|
|
4690
|
-
function syncClaudeTaskList(taskListId, projectId) {
|
|
4691
|
-
const pullResult = pullFromClaudeTaskList(taskListId, projectId);
|
|
4692
|
-
const pushResult = pushToClaudeTaskList(taskListId, projectId);
|
|
5006
|
+
function syncClaudeTaskList(taskListId, projectId, options = {}) {
|
|
5007
|
+
const pullResult = pullFromClaudeTaskList(taskListId, projectId, options);
|
|
5008
|
+
const pushResult = pushToClaudeTaskList(taskListId, projectId, options);
|
|
4693
5009
|
return {
|
|
4694
5010
|
pushed: pushResult.pushed,
|
|
4695
5011
|
pulled: pullResult.pulled,
|
|
@@ -4697,6 +5013,326 @@ function syncClaudeTaskList(taskListId, projectId) {
|
|
|
4697
5013
|
};
|
|
4698
5014
|
}
|
|
4699
5015
|
|
|
5016
|
+
// src/lib/agent-tasks.ts
|
|
5017
|
+
import { existsSync as existsSync5 } from "fs";
|
|
5018
|
+
import { join as join5 } from "path";
|
|
5019
|
+
|
|
5020
|
+
// src/lib/config.ts
|
|
5021
|
+
import { existsSync as existsSync4 } from "fs";
|
|
5022
|
+
import { join as join4 } from "path";
|
|
5023
|
+
var CONFIG_PATH = join4(HOME, ".todos", "config.json");
|
|
5024
|
+
var cached = null;
|
|
5025
|
+
function normalizeAgent(agent) {
|
|
5026
|
+
return agent.trim().toLowerCase();
|
|
5027
|
+
}
|
|
5028
|
+
function loadConfig() {
|
|
5029
|
+
if (cached)
|
|
5030
|
+
return cached;
|
|
5031
|
+
if (!existsSync4(CONFIG_PATH)) {
|
|
5032
|
+
cached = {};
|
|
5033
|
+
return cached;
|
|
5034
|
+
}
|
|
5035
|
+
const config = readJsonFile(CONFIG_PATH) || {};
|
|
5036
|
+
if (typeof config.sync_agents === "string") {
|
|
5037
|
+
config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
|
|
5038
|
+
}
|
|
5039
|
+
cached = config;
|
|
5040
|
+
return cached;
|
|
5041
|
+
}
|
|
5042
|
+
function getSyncAgentsFromConfig() {
|
|
5043
|
+
const config = loadConfig();
|
|
5044
|
+
const agents = config.sync_agents;
|
|
5045
|
+
if (Array.isArray(agents) && agents.length > 0)
|
|
5046
|
+
return agents.map(normalizeAgent);
|
|
5047
|
+
return null;
|
|
5048
|
+
}
|
|
5049
|
+
function getAgentTaskListId(agent) {
|
|
5050
|
+
const config = loadConfig();
|
|
5051
|
+
const key = normalizeAgent(agent);
|
|
5052
|
+
return config.agents?.[key]?.task_list_id || config.task_list_id || null;
|
|
5053
|
+
}
|
|
5054
|
+
function getAgentTasksDir(agent) {
|
|
5055
|
+
const config = loadConfig();
|
|
5056
|
+
const key = normalizeAgent(agent);
|
|
5057
|
+
return config.agents?.[key]?.tasks_dir || config.agent_tasks_dir || null;
|
|
5058
|
+
}
|
|
5059
|
+
|
|
5060
|
+
// src/lib/agent-tasks.ts
|
|
5061
|
+
function agentBaseDir(agent) {
|
|
5062
|
+
const key = `TODOS_${agent.toUpperCase()}_TASKS_DIR`;
|
|
5063
|
+
return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join5(HOME, ".todos", "agents");
|
|
5064
|
+
}
|
|
5065
|
+
function getTaskListDir2(agent, taskListId) {
|
|
5066
|
+
return join5(agentBaseDir(agent), agent, taskListId);
|
|
5067
|
+
}
|
|
5068
|
+
function readAgentTask(dir, filename) {
|
|
5069
|
+
return readJsonFile(join5(dir, filename));
|
|
5070
|
+
}
|
|
5071
|
+
function writeAgentTask(dir, task) {
|
|
5072
|
+
writeJsonFile(join5(dir, `${task.id}.json`), task);
|
|
5073
|
+
}
|
|
5074
|
+
function taskToAgentTask(task, externalId, existingMeta) {
|
|
5075
|
+
return {
|
|
5076
|
+
id: externalId,
|
|
5077
|
+
title: task.title,
|
|
5078
|
+
description: task.description || "",
|
|
5079
|
+
status: task.status,
|
|
5080
|
+
priority: task.priority,
|
|
5081
|
+
assigned_to: task.assigned_to || task.agent_id || "",
|
|
5082
|
+
tags: task.tags || [],
|
|
5083
|
+
metadata: {
|
|
5084
|
+
...existingMeta || {},
|
|
5085
|
+
...task.metadata,
|
|
5086
|
+
todos_id: task.id,
|
|
5087
|
+
todos_updated_at: task.updated_at,
|
|
5088
|
+
todos_version: task.version
|
|
5089
|
+
}
|
|
5090
|
+
};
|
|
5091
|
+
}
|
|
5092
|
+
function metadataKey(agent) {
|
|
5093
|
+
return `${agent}_task_id`;
|
|
5094
|
+
}
|
|
5095
|
+
function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
5096
|
+
const dir = getTaskListDir2(agent, taskListId);
|
|
5097
|
+
if (!existsSync5(dir))
|
|
5098
|
+
ensureDir2(dir);
|
|
5099
|
+
const filter = {};
|
|
5100
|
+
if (projectId)
|
|
5101
|
+
filter["project_id"] = projectId;
|
|
5102
|
+
const tasks = listTasks(filter);
|
|
5103
|
+
const existingByTodosId = new Map;
|
|
5104
|
+
const files = listJsonFiles(dir);
|
|
5105
|
+
for (const f of files) {
|
|
5106
|
+
const path = join5(dir, f);
|
|
5107
|
+
const at = readAgentTask(dir, f);
|
|
5108
|
+
if (at?.metadata?.["todos_id"]) {
|
|
5109
|
+
existingByTodosId.set(at.metadata["todos_id"], { task: at, mtimeMs: getFileMtimeMs(path) });
|
|
5110
|
+
}
|
|
5111
|
+
}
|
|
5112
|
+
let hwm = readHighWaterMark(dir);
|
|
5113
|
+
let pushed = 0;
|
|
5114
|
+
const errors2 = [];
|
|
5115
|
+
const metaKey = metadataKey(agent);
|
|
5116
|
+
const prefer = options.prefer || "remote";
|
|
5117
|
+
for (const task of tasks) {
|
|
5118
|
+
try {
|
|
5119
|
+
const existing = existingByTodosId.get(task.id);
|
|
5120
|
+
if (existing) {
|
|
5121
|
+
const lastSyncedAt = parseTimestamp(existing.task.metadata?.["todos_updated_at"]);
|
|
5122
|
+
const localUpdatedAt = parseTimestamp(task.updated_at);
|
|
5123
|
+
const remoteUpdatedAt = existing.mtimeMs;
|
|
5124
|
+
let recordConflict = false;
|
|
5125
|
+
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
5126
|
+
if (prefer === "remote") {
|
|
5127
|
+
const conflict = {
|
|
5128
|
+
agent,
|
|
5129
|
+
direction: "push",
|
|
5130
|
+
prefer,
|
|
5131
|
+
local_updated_at: task.updated_at,
|
|
5132
|
+
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
5133
|
+
detected_at: new Date().toISOString()
|
|
5134
|
+
};
|
|
5135
|
+
const newMeta = appendSyncConflict(task.metadata, conflict);
|
|
5136
|
+
updateTask(task.id, { version: task.version, metadata: newMeta });
|
|
5137
|
+
errors2.push(`conflict push ${task.id}: remote newer`);
|
|
5138
|
+
continue;
|
|
5139
|
+
}
|
|
5140
|
+
recordConflict = true;
|
|
5141
|
+
}
|
|
5142
|
+
const updated = taskToAgentTask(task, existing.task.id, existing.task.metadata);
|
|
5143
|
+
writeAgentTask(dir, updated);
|
|
5144
|
+
if (recordConflict) {
|
|
5145
|
+
const latest = getTask(task.id);
|
|
5146
|
+
if (latest) {
|
|
5147
|
+
const conflict = {
|
|
5148
|
+
agent,
|
|
5149
|
+
direction: "push",
|
|
5150
|
+
prefer,
|
|
5151
|
+
local_updated_at: latest.updated_at,
|
|
5152
|
+
remote_updated_at: remoteUpdatedAt ? new Date(remoteUpdatedAt).toISOString() : undefined,
|
|
5153
|
+
detected_at: new Date().toISOString()
|
|
5154
|
+
};
|
|
5155
|
+
const newMeta = appendSyncConflict(latest.metadata, conflict);
|
|
5156
|
+
updateTask(latest.id, { version: latest.version, metadata: newMeta });
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
} else {
|
|
5160
|
+
const externalId = String(hwm);
|
|
5161
|
+
hwm++;
|
|
5162
|
+
const at = taskToAgentTask(task, externalId);
|
|
5163
|
+
writeAgentTask(dir, at);
|
|
5164
|
+
const current = getTask(task.id);
|
|
5165
|
+
if (current) {
|
|
5166
|
+
const newMeta = { ...current.metadata, [metaKey]: externalId };
|
|
5167
|
+
updateTask(task.id, { version: current.version, metadata: newMeta });
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
pushed++;
|
|
5171
|
+
} catch (e) {
|
|
5172
|
+
errors2.push(`push ${task.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5175
|
+
writeHighWaterMark(dir, hwm);
|
|
5176
|
+
return { pushed, pulled: 0, errors: errors2 };
|
|
5177
|
+
}
|
|
5178
|
+
function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
5179
|
+
const dir = getTaskListDir2(agent, taskListId);
|
|
5180
|
+
if (!existsSync5(dir)) {
|
|
5181
|
+
return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
|
|
5182
|
+
}
|
|
5183
|
+
const files = listJsonFiles(dir);
|
|
5184
|
+
let pulled = 0;
|
|
5185
|
+
const errors2 = [];
|
|
5186
|
+
const metaKey = metadataKey(agent);
|
|
5187
|
+
const prefer = options.prefer || "remote";
|
|
5188
|
+
const allTasks = listTasks({});
|
|
5189
|
+
const byExternalId = new Map;
|
|
5190
|
+
const byTodosId = new Map;
|
|
5191
|
+
for (const t of allTasks) {
|
|
5192
|
+
const extId = t.metadata[metaKey];
|
|
5193
|
+
if (extId)
|
|
5194
|
+
byExternalId.set(String(extId), t);
|
|
5195
|
+
byTodosId.set(t.id, t);
|
|
5196
|
+
}
|
|
5197
|
+
for (const f of files) {
|
|
5198
|
+
try {
|
|
5199
|
+
const filePath = join5(dir, f);
|
|
5200
|
+
const at = readAgentTask(dir, f);
|
|
5201
|
+
if (!at)
|
|
5202
|
+
continue;
|
|
5203
|
+
if (at.metadata?.["_internal"])
|
|
5204
|
+
continue;
|
|
5205
|
+
const todosId = at.metadata?.["todos_id"];
|
|
5206
|
+
const existingByMapping = byExternalId.get(at.id);
|
|
5207
|
+
const existingByTodos = todosId ? byTodosId.get(todosId) : undefined;
|
|
5208
|
+
const existing = existingByMapping || existingByTodos;
|
|
5209
|
+
if (existing) {
|
|
5210
|
+
const lastSyncedAt = parseTimestamp(at.metadata?.["todos_updated_at"]);
|
|
5211
|
+
const localUpdatedAt = parseTimestamp(existing.updated_at);
|
|
5212
|
+
const remoteUpdatedAt = getFileMtimeMs(filePath);
|
|
5213
|
+
let conflictMeta = null;
|
|
5214
|
+
if (lastSyncedAt && localUpdatedAt && remoteUpdatedAt && localUpdatedAt > lastSyncedAt && remoteUpdatedAt > lastSyncedAt) {
|
|
5215
|
+
const conflict = {
|
|
5216
|
+
agent,
|
|
5217
|
+
direction: "pull",
|
|
5218
|
+
prefer,
|
|
5219
|
+
local_updated_at: existing.updated_at,
|
|
5220
|
+
remote_updated_at: new Date(remoteUpdatedAt).toISOString(),
|
|
5221
|
+
detected_at: new Date().toISOString()
|
|
5222
|
+
};
|
|
5223
|
+
conflictMeta = appendSyncConflict(existing.metadata, conflict);
|
|
5224
|
+
if (prefer === "local") {
|
|
5225
|
+
updateTask(existing.id, { version: existing.version, metadata: conflictMeta });
|
|
5226
|
+
errors2.push(`conflict pull ${existing.id}: local newer`);
|
|
5227
|
+
continue;
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
updateTask(existing.id, {
|
|
5231
|
+
version: existing.version,
|
|
5232
|
+
title: at.title,
|
|
5233
|
+
description: at.description || undefined,
|
|
5234
|
+
status: at.status,
|
|
5235
|
+
priority: at.priority,
|
|
5236
|
+
assigned_to: at.assigned_to || undefined,
|
|
5237
|
+
tags: at.tags || [],
|
|
5238
|
+
metadata: { ...conflictMeta || existing.metadata, ...at.metadata, [metaKey]: at.id }
|
|
5239
|
+
});
|
|
5240
|
+
} else {
|
|
5241
|
+
createTask({
|
|
5242
|
+
title: at.title,
|
|
5243
|
+
description: at.description || undefined,
|
|
5244
|
+
status: at.status,
|
|
5245
|
+
priority: at.priority || "medium",
|
|
5246
|
+
assigned_to: at.assigned_to || undefined,
|
|
5247
|
+
tags: at.tags || [],
|
|
5248
|
+
project_id: projectId,
|
|
5249
|
+
metadata: { ...at.metadata, [metaKey]: at.id }
|
|
5250
|
+
});
|
|
5251
|
+
}
|
|
5252
|
+
pulled++;
|
|
5253
|
+
} catch (e) {
|
|
5254
|
+
errors2.push(`pull ${f}: ${e instanceof Error ? e.message : String(e)}`);
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
return { pushed: 0, pulled, errors: errors2 };
|
|
5258
|
+
}
|
|
5259
|
+
function syncAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
5260
|
+
const pullResult = pullFromAgentTaskList(agent, taskListId, projectId, options);
|
|
5261
|
+
const pushResult = pushToAgentTaskList(agent, taskListId, projectId, options);
|
|
5262
|
+
return {
|
|
5263
|
+
pushed: pushResult.pushed,
|
|
5264
|
+
pulled: pullResult.pulled,
|
|
5265
|
+
errors: [...pullResult.errors, ...pushResult.errors]
|
|
5266
|
+
};
|
|
5267
|
+
}
|
|
5268
|
+
|
|
5269
|
+
// src/lib/sync.ts
|
|
5270
|
+
function normalizeAgent2(agent) {
|
|
5271
|
+
return agent.trim().toLowerCase();
|
|
5272
|
+
}
|
|
5273
|
+
function isClaudeAgent(agent) {
|
|
5274
|
+
const a = normalizeAgent2(agent);
|
|
5275
|
+
return a === "claude" || a === "claude-code" || a === "claude_code";
|
|
5276
|
+
}
|
|
5277
|
+
function defaultSyncAgents() {
|
|
5278
|
+
const env = process.env["TODOS_SYNC_AGENTS"];
|
|
5279
|
+
if (env) {
|
|
5280
|
+
return env.split(",").map((a) => a.trim()).filter(Boolean);
|
|
5281
|
+
}
|
|
5282
|
+
const fromConfig = getSyncAgentsFromConfig();
|
|
5283
|
+
if (fromConfig && fromConfig.length > 0)
|
|
5284
|
+
return fromConfig;
|
|
5285
|
+
return ["claude", "codex", "gemini"];
|
|
5286
|
+
}
|
|
5287
|
+
function syncWithAgent(agent, taskListId, projectId, direction = "both", options = {}) {
|
|
5288
|
+
const normalized = normalizeAgent2(agent);
|
|
5289
|
+
if (isClaudeAgent(normalized)) {
|
|
5290
|
+
if (direction === "push")
|
|
5291
|
+
return pushToClaudeTaskList(taskListId, projectId, options);
|
|
5292
|
+
if (direction === "pull")
|
|
5293
|
+
return pullFromClaudeTaskList(taskListId, projectId, options);
|
|
5294
|
+
return syncClaudeTaskList(taskListId, projectId, options);
|
|
5295
|
+
}
|
|
5296
|
+
if (direction === "push")
|
|
5297
|
+
return pushToAgentTaskList(normalized, taskListId, projectId, options);
|
|
5298
|
+
if (direction === "pull")
|
|
5299
|
+
return pullFromAgentTaskList(normalized, taskListId, projectId, options);
|
|
5300
|
+
return syncAgentTaskList(normalized, taskListId, projectId, options);
|
|
5301
|
+
}
|
|
5302
|
+
function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both", options = {}) {
|
|
5303
|
+
let pushed = 0;
|
|
5304
|
+
let pulled = 0;
|
|
5305
|
+
const errors2 = [];
|
|
5306
|
+
const normalized = agents.map(normalizeAgent2);
|
|
5307
|
+
if (direction === "pull" || direction === "both") {
|
|
5308
|
+
for (const agent of normalized) {
|
|
5309
|
+
const listId = taskListIdByAgent(agent);
|
|
5310
|
+
if (!listId) {
|
|
5311
|
+
errors2.push(`sync ${agent}: missing task list id`);
|
|
5312
|
+
continue;
|
|
5313
|
+
}
|
|
5314
|
+
const result = syncWithAgent(agent, listId, projectId, "pull", options);
|
|
5315
|
+
pushed += result.pushed;
|
|
5316
|
+
pulled += result.pulled;
|
|
5317
|
+
errors2.push(...result.errors.map((e) => `${agent}: ${e}`));
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
if (direction === "push" || direction === "both") {
|
|
5321
|
+
for (const agent of normalized) {
|
|
5322
|
+
const listId = taskListIdByAgent(agent);
|
|
5323
|
+
if (!listId) {
|
|
5324
|
+
errors2.push(`sync ${agent}: missing task list id`);
|
|
5325
|
+
continue;
|
|
5326
|
+
}
|
|
5327
|
+
const result = syncWithAgent(agent, listId, projectId, "push", options);
|
|
5328
|
+
pushed += result.pushed;
|
|
5329
|
+
pulled += result.pulled;
|
|
5330
|
+
errors2.push(...result.errors.map((e) => `${agent}: ${e}`));
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
5333
|
+
return { pushed, pulled, errors: errors2 };
|
|
5334
|
+
}
|
|
5335
|
+
|
|
4700
5336
|
// src/mcp/index.ts
|
|
4701
5337
|
var server = new McpServer({
|
|
4702
5338
|
name: "todos",
|
|
@@ -4707,6 +5343,8 @@ function formatError(error) {
|
|
|
4707
5343
|
return `Version conflict: ${error.message}`;
|
|
4708
5344
|
if (error instanceof TaskNotFoundError)
|
|
4709
5345
|
return `Not found: ${error.message}`;
|
|
5346
|
+
if (error instanceof PlanNotFoundError)
|
|
5347
|
+
return `Not found: ${error.message}`;
|
|
4710
5348
|
if (error instanceof LockError)
|
|
4711
5349
|
return `Lock error: ${error.message}`;
|
|
4712
5350
|
if (error instanceof DependencyCycleError)
|
|
@@ -4722,6 +5360,16 @@ function resolveId(partialId, table = "tasks") {
|
|
|
4722
5360
|
throw new Error(`Could not resolve ID: ${partialId}`);
|
|
4723
5361
|
return id;
|
|
4724
5362
|
}
|
|
5363
|
+
function resolveTaskListId(agent, explicit) {
|
|
5364
|
+
if (explicit)
|
|
5365
|
+
return explicit;
|
|
5366
|
+
const normalized = agent.trim().toLowerCase();
|
|
5367
|
+
if (normalized === "claude" || normalized === "claude-code" || normalized === "claude_code") {
|
|
5368
|
+
return process.env["TODOS_CLAUDE_TASK_LIST"] || process.env["CLAUDE_CODE_TASK_LIST_ID"] || process.env["CLAUDE_CODE_SESSION_ID"] || getAgentTaskListId(normalized) || null;
|
|
5369
|
+
}
|
|
5370
|
+
const key = `TODOS_${normalized.toUpperCase()}_TASK_LIST`;
|
|
5371
|
+
return process.env[key] || process.env["TODOS_TASK_LIST_ID"] || getAgentTaskListId(normalized) || "default";
|
|
5372
|
+
}
|
|
4725
5373
|
function formatTask(task) {
|
|
4726
5374
|
const parts = [
|
|
4727
5375
|
`ID: ${task.id}`,
|
|
@@ -4741,6 +5389,8 @@ function formatTask(task) {
|
|
|
4741
5389
|
parts.push(`Parent: ${task.parent_id}`);
|
|
4742
5390
|
if (task.project_id)
|
|
4743
5391
|
parts.push(`Project: ${task.project_id}`);
|
|
5392
|
+
if (task.plan_id)
|
|
5393
|
+
parts.push(`Plan: ${task.plan_id}`);
|
|
4744
5394
|
if (task.tags.length > 0)
|
|
4745
5395
|
parts.push(`Tags: ${task.tags.join(", ")}`);
|
|
4746
5396
|
parts.push(`Version: ${task.version}`);
|
|
@@ -4761,6 +5411,7 @@ server.tool("create_task", "Create a new task", {
|
|
|
4761
5411
|
assigned_to: exports_external.string().optional().describe("Assigned agent ID"),
|
|
4762
5412
|
session_id: exports_external.string().optional().describe("Session ID"),
|
|
4763
5413
|
working_dir: exports_external.string().optional().describe("Working directory context"),
|
|
5414
|
+
plan_id: exports_external.string().optional().describe("Plan ID to assign task to"),
|
|
4764
5415
|
tags: exports_external.array(exports_external.string()).optional().describe("Task tags"),
|
|
4765
5416
|
metadata: exports_external.record(exports_external.unknown()).optional().describe("Arbitrary metadata")
|
|
4766
5417
|
}, async (params) => {
|
|
@@ -4770,6 +5421,8 @@ server.tool("create_task", "Create a new task", {
|
|
|
4770
5421
|
resolved.project_id = resolveId(resolved.project_id, "projects");
|
|
4771
5422
|
if (resolved.parent_id)
|
|
4772
5423
|
resolved.parent_id = resolveId(resolved.parent_id);
|
|
5424
|
+
if (resolved.plan_id)
|
|
5425
|
+
resolved.plan_id = resolveId(resolved.plan_id, "plans");
|
|
4773
5426
|
const task = createTask(resolved);
|
|
4774
5427
|
return { content: [{ type: "text", text: `Task created:
|
|
4775
5428
|
${formatTask(task)}` }] };
|
|
@@ -4788,12 +5441,15 @@ server.tool("list_tasks", "List tasks with optional filters", {
|
|
|
4788
5441
|
exports_external.array(exports_external.enum(["low", "medium", "high", "critical"]))
|
|
4789
5442
|
]).optional().describe("Filter by priority"),
|
|
4790
5443
|
assigned_to: exports_external.string().optional().describe("Filter by assigned agent"),
|
|
4791
|
-
tags: exports_external.array(exports_external.string()).optional().describe("Filter by tags (any match)")
|
|
5444
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Filter by tags (any match)"),
|
|
5445
|
+
plan_id: exports_external.string().optional().describe("Filter by plan")
|
|
4792
5446
|
}, async (params) => {
|
|
4793
5447
|
try {
|
|
4794
5448
|
const resolved = { ...params };
|
|
4795
5449
|
if (resolved.project_id)
|
|
4796
5450
|
resolved.project_id = resolveId(resolved.project_id, "projects");
|
|
5451
|
+
if (resolved.plan_id)
|
|
5452
|
+
resolved.plan_id = resolveId(resolved.plan_id, "plans");
|
|
4797
5453
|
const tasks = listTasks(resolved);
|
|
4798
5454
|
if (tasks.length === 0) {
|
|
4799
5455
|
return { content: [{ type: "text", text: "No tasks found." }] };
|
|
@@ -4867,7 +5523,8 @@ server.tool("update_task", "Update task fields (requires version for optimistic
|
|
|
4867
5523
|
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("New priority"),
|
|
4868
5524
|
assigned_to: exports_external.string().optional().describe("Assign to agent"),
|
|
4869
5525
|
tags: exports_external.array(exports_external.string()).optional().describe("New tags"),
|
|
4870
|
-
metadata: exports_external.record(exports_external.unknown()).optional().describe("New metadata")
|
|
5526
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("New metadata"),
|
|
5527
|
+
plan_id: exports_external.string().optional().describe("Plan ID to assign task to")
|
|
4871
5528
|
}, async ({ id, ...rest }) => {
|
|
4872
5529
|
try {
|
|
4873
5530
|
const resolvedId = resolveId(id);
|
|
@@ -4998,7 +5655,10 @@ server.tool("list_projects", "List all registered projects", {}, async () => {
|
|
|
4998
5655
|
if (projects.length === 0) {
|
|
4999
5656
|
return { content: [{ type: "text", text: "No projects registered." }] };
|
|
5000
5657
|
}
|
|
5001
|
-
const text = projects.map((p) =>
|
|
5658
|
+
const text = projects.map((p) => {
|
|
5659
|
+
const taskList = p.task_list_id ? ` [${p.task_list_id}]` : "";
|
|
5660
|
+
return `${p.id.slice(0, 8)} | ${p.name} | ${p.path}${taskList}${p.description ? ` - ${p.description}` : ""}`;
|
|
5661
|
+
}).join(`
|
|
5002
5662
|
`);
|
|
5003
5663
|
return { content: [{ type: "text", text: `${projects.length} project(s):
|
|
5004
5664
|
${text}` }] };
|
|
@@ -5009,14 +5669,117 @@ ${text}` }] };
|
|
|
5009
5669
|
server.tool("create_project", "Register a new project", {
|
|
5010
5670
|
name: exports_external.string().describe("Project name"),
|
|
5011
5671
|
path: exports_external.string().describe("Absolute path to project"),
|
|
5012
|
-
description: exports_external.string().optional().describe("Project description")
|
|
5672
|
+
description: exports_external.string().optional().describe("Project description"),
|
|
5673
|
+
task_list_id: exports_external.string().optional().describe("Custom task list ID for Claude Code sync (defaults to todos-<slugified-name>)")
|
|
5013
5674
|
}, async (params) => {
|
|
5014
5675
|
try {
|
|
5015
5676
|
const project = createProject(params);
|
|
5677
|
+
const taskList = project.task_list_id ? ` [${project.task_list_id}]` : "";
|
|
5016
5678
|
return {
|
|
5017
5679
|
content: [{
|
|
5018
5680
|
type: "text",
|
|
5019
|
-
text: `Project created: ${project.id.slice(0, 8)} | ${project.name} | ${project.path}`
|
|
5681
|
+
text: `Project created: ${project.id.slice(0, 8)} | ${project.name} | ${project.path}${taskList}`
|
|
5682
|
+
}]
|
|
5683
|
+
};
|
|
5684
|
+
} catch (e) {
|
|
5685
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
5686
|
+
}
|
|
5687
|
+
});
|
|
5688
|
+
server.tool("create_plan", "Create a new plan", {
|
|
5689
|
+
name: exports_external.string().describe("Plan name"),
|
|
5690
|
+
project_id: exports_external.string().optional().describe("Project ID"),
|
|
5691
|
+
description: exports_external.string().optional().describe("Plan description"),
|
|
5692
|
+
status: exports_external.enum(["active", "completed", "archived"]).optional().describe("Plan status")
|
|
5693
|
+
}, async (params) => {
|
|
5694
|
+
try {
|
|
5695
|
+
const resolved = { ...params };
|
|
5696
|
+
if (resolved.project_id)
|
|
5697
|
+
resolved.project_id = resolveId(resolved.project_id, "projects");
|
|
5698
|
+
const plan = createPlan(resolved);
|
|
5699
|
+
return {
|
|
5700
|
+
content: [{
|
|
5701
|
+
type: "text",
|
|
5702
|
+
text: `Plan created: ${plan.id.slice(0, 8)} | ${plan.name} | ${plan.status}`
|
|
5703
|
+
}]
|
|
5704
|
+
};
|
|
5705
|
+
} catch (e) {
|
|
5706
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
5707
|
+
}
|
|
5708
|
+
});
|
|
5709
|
+
server.tool("list_plans", "List plans with optional project filter", {
|
|
5710
|
+
project_id: exports_external.string().optional().describe("Filter by project")
|
|
5711
|
+
}, async ({ project_id }) => {
|
|
5712
|
+
try {
|
|
5713
|
+
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
5714
|
+
const plans = listPlans(resolvedProjectId);
|
|
5715
|
+
if (plans.length === 0) {
|
|
5716
|
+
return { content: [{ type: "text", text: "No plans found." }] };
|
|
5717
|
+
}
|
|
5718
|
+
const text = plans.map((p) => {
|
|
5719
|
+
const project = p.project_id ? ` (project: ${p.project_id.slice(0, 8)})` : "";
|
|
5720
|
+
return `[${p.status}] ${p.id.slice(0, 8)} | ${p.name}${project}`;
|
|
5721
|
+
}).join(`
|
|
5722
|
+
`);
|
|
5723
|
+
return { content: [{ type: "text", text: `${plans.length} plan(s):
|
|
5724
|
+
${text}` }] };
|
|
5725
|
+
} catch (e) {
|
|
5726
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
5727
|
+
}
|
|
5728
|
+
});
|
|
5729
|
+
server.tool("get_plan", "Get plan details", {
|
|
5730
|
+
id: exports_external.string().describe("Plan ID (full or partial)")
|
|
5731
|
+
}, async ({ id }) => {
|
|
5732
|
+
try {
|
|
5733
|
+
const resolvedId = resolveId(id, "plans");
|
|
5734
|
+
const plan = getPlan(resolvedId);
|
|
5735
|
+
if (!plan)
|
|
5736
|
+
return { content: [{ type: "text", text: `Plan not found: ${id}` }], isError: true };
|
|
5737
|
+
const parts = [
|
|
5738
|
+
`ID: ${plan.id}`,
|
|
5739
|
+
`Name: ${plan.name}`,
|
|
5740
|
+
`Status: ${plan.status}`
|
|
5741
|
+
];
|
|
5742
|
+
if (plan.description)
|
|
5743
|
+
parts.push(`Description: ${plan.description}`);
|
|
5744
|
+
if (plan.project_id)
|
|
5745
|
+
parts.push(`Project: ${plan.project_id}`);
|
|
5746
|
+
parts.push(`Created: ${plan.created_at}`);
|
|
5747
|
+
parts.push(`Updated: ${plan.updated_at}`);
|
|
5748
|
+
return { content: [{ type: "text", text: parts.join(`
|
|
5749
|
+
`) }] };
|
|
5750
|
+
} catch (e) {
|
|
5751
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
5752
|
+
}
|
|
5753
|
+
});
|
|
5754
|
+
server.tool("update_plan", "Update a plan", {
|
|
5755
|
+
id: exports_external.string().describe("Plan ID (full or partial)"),
|
|
5756
|
+
name: exports_external.string().optional().describe("New name"),
|
|
5757
|
+
description: exports_external.string().optional().describe("New description"),
|
|
5758
|
+
status: exports_external.enum(["active", "completed", "archived"]).optional().describe("New status")
|
|
5759
|
+
}, async ({ id, ...rest }) => {
|
|
5760
|
+
try {
|
|
5761
|
+
const resolvedId = resolveId(id, "plans");
|
|
5762
|
+
const plan = updatePlan(resolvedId, rest);
|
|
5763
|
+
return {
|
|
5764
|
+
content: [{
|
|
5765
|
+
type: "text",
|
|
5766
|
+
text: `Plan updated: ${plan.id.slice(0, 8)} | ${plan.name} | ${plan.status}`
|
|
5767
|
+
}]
|
|
5768
|
+
};
|
|
5769
|
+
} catch (e) {
|
|
5770
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
5771
|
+
}
|
|
5772
|
+
});
|
|
5773
|
+
server.tool("delete_plan", "Delete a plan", {
|
|
5774
|
+
id: exports_external.string().describe("Plan ID (full or partial)")
|
|
5775
|
+
}, async ({ id }) => {
|
|
5776
|
+
try {
|
|
5777
|
+
const resolvedId = resolveId(id, "plans");
|
|
5778
|
+
const deleted = deletePlan(resolvedId);
|
|
5779
|
+
return {
|
|
5780
|
+
content: [{
|
|
5781
|
+
type: "text",
|
|
5782
|
+
text: deleted ? `Plan ${id} deleted.` : `Plan ${id} not found.`
|
|
5020
5783
|
}]
|
|
5021
5784
|
};
|
|
5022
5785
|
} catch (e) {
|
|
@@ -5041,27 +5804,42 @@ ${text}` }] };
|
|
|
5041
5804
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
5042
5805
|
}
|
|
5043
5806
|
});
|
|
5044
|
-
server.tool("sync", "Sync tasks with
|
|
5045
|
-
task_list_id: exports_external.string().describe("
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5807
|
+
server.tool("sync", "Sync tasks with an agent task list (Claude uses native task list; others use JSON lists).", {
|
|
5808
|
+
task_list_id: exports_external.string().optional().describe("Task list ID (required for Claude)"),
|
|
5809
|
+
agent: exports_external.string().optional().describe("Agent/provider name (default: claude)"),
|
|
5810
|
+
all_agents: exports_external.boolean().optional().describe("Sync across all configured agents"),
|
|
5811
|
+
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"),
|
|
5812
|
+
direction: exports_external.enum(["push", "pull", "both"]).optional().describe("Sync direction: push (SQLite->agent), pull (agent->SQLite), or both (default)"),
|
|
5813
|
+
prefer: exports_external.enum(["local", "remote"]).optional().describe("Conflict strategy")
|
|
5814
|
+
}, async ({ task_list_id, agent, all_agents, project_id, direction, prefer }) => {
|
|
5049
5815
|
try {
|
|
5050
5816
|
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
5051
|
-
const
|
|
5817
|
+
const project = resolvedProjectId ? getProject(resolvedProjectId) : undefined;
|
|
5818
|
+
const dir = direction ?? "both";
|
|
5819
|
+
const options = { prefer: prefer ?? "remote" };
|
|
5052
5820
|
let result;
|
|
5053
|
-
if (
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
result = pullFromClaudeTaskList(taskListId, resolvedProjectId);
|
|
5821
|
+
if (all_agents) {
|
|
5822
|
+
const agents = defaultSyncAgents();
|
|
5823
|
+
result = syncWithAgents(agents, (a) => resolveTaskListId(a, task_list_id || project?.task_list_id || undefined), resolvedProjectId, dir, options);
|
|
5057
5824
|
} else {
|
|
5058
|
-
|
|
5825
|
+
const resolvedAgent = agent || "claude";
|
|
5826
|
+
const taskListId = resolveTaskListId(resolvedAgent, task_list_id || project?.task_list_id || undefined);
|
|
5827
|
+
if (!taskListId) {
|
|
5828
|
+
return {
|
|
5829
|
+
content: [{
|
|
5830
|
+
type: "text",
|
|
5831
|
+
text: `Could not determine task list ID for ${resolvedAgent}. Provide task_list_id or set task_list_id on the project.`
|
|
5832
|
+
}],
|
|
5833
|
+
isError: true
|
|
5834
|
+
};
|
|
5835
|
+
}
|
|
5836
|
+
result = syncWithAgent(resolvedAgent, taskListId, resolvedProjectId, dir, options);
|
|
5059
5837
|
}
|
|
5060
5838
|
const parts = [];
|
|
5061
5839
|
if (result.pulled > 0)
|
|
5062
|
-
parts.push(`Pulled ${result.pulled} task(s)
|
|
5840
|
+
parts.push(`Pulled ${result.pulled} task(s).`);
|
|
5063
5841
|
if (result.pushed > 0)
|
|
5064
|
-
parts.push(`Pushed ${result.pushed} task(s)
|
|
5842
|
+
parts.push(`Pushed ${result.pushed} task(s).`);
|
|
5065
5843
|
if (result.pulled === 0 && result.pushed === 0 && result.errors.length === 0) {
|
|
5066
5844
|
parts.push("Nothing to sync.");
|
|
5067
5845
|
}
|