@hasna/todos 0.11.19 → 0.11.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2260,6 +2260,18 @@ function ensureSchema(db) {
2260
2260
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
2261
2261
  UNIQUE(source_id, source_type, target_id, target_type, relation_type)
2262
2262
  )`);
2263
+ ensureTable("project_machine_paths", `
2264
+ CREATE TABLE project_machine_paths (
2265
+ id TEXT PRIMARY KEY,
2266
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
2267
+ machine_id TEXT NOT NULL,
2268
+ path TEXT NOT NULL,
2269
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2270
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
2271
+ UNIQUE(project_id, machine_id)
2272
+ )`);
2273
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
2274
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
2263
2275
  ensureTable("machines", `
2264
2276
  CREATE TABLE machines (
2265
2277
  id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
@@ -2295,6 +2307,7 @@ function ensureSchema(db) {
2295
2307
  ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
2296
2308
  ensureColumn("tasks", "retry_after", "TEXT");
2297
2309
  ensureColumn("tasks", "sla_minutes", "INTEGER");
2310
+ ensureColumn("tasks", "archived_at", "TEXT");
2298
2311
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
2299
2312
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
2300
2313
  ensureColumn("agents", "reports_to", "TEXT");
@@ -3133,6 +3146,25 @@ var init_schema = __esm(() => {
3133
3146
  CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id);
3134
3147
 
3135
3148
  INSERT OR IGNORE INTO _migrations (id) VALUES (41);
3149
+ `,
3150
+ `
3151
+ CREATE TABLE IF NOT EXISTS project_machine_paths (
3152
+ id TEXT PRIMARY KEY,
3153
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3154
+ machine_id TEXT NOT NULL,
3155
+ path TEXT NOT NULL,
3156
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3157
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
3158
+ UNIQUE(project_id, machine_id)
3159
+ );
3160
+ CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id);
3161
+ CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id);
3162
+ INSERT OR IGNORE INTO _migrations (id) VALUES (42);
3163
+ `,
3164
+ `
3165
+ ALTER TABLE tasks ADD COLUMN archived_at TEXT;
3166
+ CREATE INDEX IF NOT EXISTS idx_tasks_archived ON tasks(archived_at) WHERE archived_at IS NOT NULL;
3167
+ INSERT OR IGNORE INTO _migrations (id) VALUES (43);
3136
3168
  `
3137
3169
  ];
3138
3170
  });
@@ -3517,13 +3549,18 @@ var exports_projects = {};
3517
3549
  __export(exports_projects, {
3518
3550
  updateProject: () => updateProject,
3519
3551
  slugify: () => slugify,
3552
+ setMachineLocalPath: () => setMachineLocalPath,
3553
+ renameProject: () => renameProject,
3520
3554
  removeProjectSource: () => removeProjectSource,
3555
+ removeMachineLocalPath: () => removeMachineLocalPath,
3521
3556
  nextTaskShortId: () => nextTaskShortId,
3522
3557
  listProjects: () => listProjects,
3523
3558
  listProjectSources: () => listProjectSources,
3559
+ listMachineLocalPaths: () => listMachineLocalPaths,
3524
3560
  getProjectWithSources: () => getProjectWithSources,
3525
3561
  getProjectByPath: () => getProjectByPath,
3526
3562
  getProject: () => getProject,
3563
+ getMachineLocalPath: () => getMachineLocalPath,
3527
3564
  ensureProject: () => ensureProject,
3528
3565
  deleteProject: () => deleteProject,
3529
3566
  createProject: () => createProject,
@@ -3569,8 +3606,15 @@ function getProject(id, db) {
3569
3606
  }
3570
3607
  function getProjectByPath(path, db) {
3571
3608
  const d = db || getDatabase();
3572
- const row = d.query("SELECT * FROM projects WHERE path = ?").get(path);
3573
- return row;
3609
+ try {
3610
+ const machineId = getMachineId(d);
3611
+ const machineRow = d.query(`SELECT p.* FROM projects p
3612
+ JOIN project_machine_paths pmp ON pmp.project_id = p.id
3613
+ WHERE pmp.machine_id = ? AND pmp.path = ?`).get(machineId, path);
3614
+ if (machineRow)
3615
+ return machineRow;
3616
+ } catch {}
3617
+ return d.query("SELECT * FROM projects WHERE path = ?").get(path);
3574
3618
  }
3575
3619
  function listProjects(db) {
3576
3620
  const d = db || getDatabase();
@@ -3595,10 +3639,40 @@ function updateProject(id, input, db) {
3595
3639
  sets.push("task_list_id = ?");
3596
3640
  params.push(input.task_list_id);
3597
3641
  }
3642
+ if (input.path !== undefined) {
3643
+ sets.push("path = ?");
3644
+ params.push(input.path);
3645
+ }
3598
3646
  params.push(id);
3599
3647
  d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
3600
3648
  return getProject(id, d);
3601
3649
  }
3650
+ function renameProject(id, input, db) {
3651
+ const d = db || getDatabase();
3652
+ const project = getProject(id, d);
3653
+ if (!project)
3654
+ throw new ProjectNotFoundError(id);
3655
+ let taskListsUpdated = 0;
3656
+ const ts = now();
3657
+ if (input.new_slug !== undefined) {
3658
+ const normalised = input.new_slug.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-|-$/g, "");
3659
+ if (!normalised)
3660
+ throw new Error("Invalid slug \u2014 must be non-empty kebab-case");
3661
+ const conflict = d.query("SELECT id FROM projects WHERE task_list_id = ? AND id != ?").get(normalised, id);
3662
+ if (conflict)
3663
+ throw new Error(`Slug "${normalised}" is already used by another project`);
3664
+ const oldSlug = project.task_list_id;
3665
+ d.run("UPDATE projects SET task_list_id = ?, updated_at = ? WHERE id = ?", [normalised, ts, id]);
3666
+ if (oldSlug) {
3667
+ const result = d.run("UPDATE task_lists SET slug = ?, name = COALESCE(?, name), updated_at = ? WHERE project_id = ? AND slug = ?", [normalised, input.name ?? null, ts, id, oldSlug]);
3668
+ taskListsUpdated = result.changes;
3669
+ }
3670
+ }
3671
+ if (input.name !== undefined) {
3672
+ d.run("UPDATE projects SET name = ?, updated_at = ? WHERE id = ?", [input.name, ts, id]);
3673
+ }
3674
+ return { project: getProject(id, d), task_lists_updated: taskListsUpdated };
3675
+ }
3602
3676
  function deleteProject(id, db) {
3603
3677
  const d = db || getDatabase();
3604
3678
  const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
@@ -3651,6 +3725,13 @@ function nextTaskShortId(projectId, db) {
3651
3725
  const project = getProject(projectId, d);
3652
3726
  if (!project || !project.task_prefix)
3653
3727
  return null;
3728
+ const prefix = project.task_prefix;
3729
+ const prefixLen = prefix.length + 2;
3730
+ const maxRow = d.query(`SELECT MAX(CAST(SUBSTR(short_id, ?) AS INTEGER)) as max_counter FROM tasks WHERE short_id LIKE ?`).get(prefixLen, `${prefix}-%`);
3731
+ const syncedMax = maxRow?.max_counter ?? 0;
3732
+ if (syncedMax >= (project.task_counter ?? 0)) {
3733
+ d.run("UPDATE projects SET task_counter = ?, updated_at = ? WHERE id = ?", [syncedMax, now(), projectId]);
3734
+ }
3654
3735
  d.run("UPDATE projects SET task_counter = task_counter + 1, updated_at = ? WHERE id = ?", [now(), projectId]);
3655
3736
  const updated = getProject(projectId, d);
3656
3737
  const padded = String(updated.task_counter).padStart(5, "0");
@@ -3665,13 +3746,53 @@ function ensureProject(name, path, db) {
3665
3746
  d.run("UPDATE projects SET task_prefix = ?, updated_at = ? WHERE id = ?", [prefix, now(), existing.id]);
3666
3747
  return getProject(existing.id, d);
3667
3748
  }
3749
+ setMachineLocalPath(existing.id, path, d);
3668
3750
  return existing;
3669
3751
  }
3670
- return createProject({ name, path }, d);
3752
+ const project = createProject({ name, path }, d);
3753
+ setMachineLocalPath(project.id, path, d);
3754
+ return project;
3755
+ }
3756
+ function setMachineLocalPath(projectId, path, db) {
3757
+ const d = db || getDatabase();
3758
+ const machineId = getMachineId(d);
3759
+ const ts = now();
3760
+ const existing = d.query("SELECT * FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(projectId, machineId);
3761
+ if (existing) {
3762
+ if (existing.path !== path) {
3763
+ d.run("UPDATE project_machine_paths SET path = ?, updated_at = ? WHERE id = ?", [path, ts, existing.id]);
3764
+ }
3765
+ return d.query("SELECT * FROM project_machine_paths WHERE id = ?").get(existing.id);
3766
+ }
3767
+ const id = uuid();
3768
+ d.run("INSERT INTO project_machine_paths (id, project_id, machine_id, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", [id, projectId, machineId, path, ts, ts]);
3769
+ return d.query("SELECT * FROM project_machine_paths WHERE id = ?").get(id);
3770
+ }
3771
+ function getMachineLocalPath(projectId, db) {
3772
+ const d = db || getDatabase();
3773
+ try {
3774
+ const machineId = getMachineId(d);
3775
+ const row = d.query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(projectId, machineId);
3776
+ if (row)
3777
+ return row.path;
3778
+ } catch {}
3779
+ const project = getProject(projectId, d);
3780
+ return project?.path ?? null;
3781
+ }
3782
+ function listMachineLocalPaths(projectId, db) {
3783
+ const d = db || getDatabase();
3784
+ return d.query("SELECT * FROM project_machine_paths WHERE project_id = ? ORDER BY machine_id").all(projectId);
3785
+ }
3786
+ function removeMachineLocalPath(projectId, machineId, db) {
3787
+ const d = db || getDatabase();
3788
+ const mid = machineId ?? getMachineId(d);
3789
+ const result = d.run("DELETE FROM project_machine_paths WHERE project_id = ? AND machine_id = ?", [projectId, mid]);
3790
+ return result.changes > 0;
3671
3791
  }
3672
3792
  var init_projects = __esm(() => {
3673
3793
  init_types();
3674
3794
  init_database();
3795
+ init_machines();
3675
3796
  });
3676
3797
 
3677
3798
  // src/lib/sync-utils.ts
@@ -4658,6 +4779,7 @@ var exports_tasks = {};
4658
4779
  __export(exports_tasks, {
4659
4780
  updateTask: () => updateTask,
4660
4781
  unlockTask: () => unlockTask,
4782
+ unarchiveTask: () => unarchiveTask,
4661
4783
  stealTask: () => stealTask,
4662
4784
  startTask: () => startTask,
4663
4785
  setTaskStatus: () => setTaskStatus,
@@ -4692,6 +4814,7 @@ __export(exports_tasks, {
4692
4814
  claimNextTask: () => claimNextTask,
4693
4815
  bulkUpdateTasks: () => bulkUpdateTasks,
4694
4816
  bulkCreateTasks: () => bulkCreateTasks,
4817
+ archiveTasks: () => archiveTasks,
4695
4818
  addDependency: () => addDependency
4696
4819
  });
4697
4820
  function rowToTask(row) {
@@ -4884,6 +5007,9 @@ function listTasks(filter = {}, db) {
4884
5007
  params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
4885
5008
  } catch {}
4886
5009
  }
5010
+ if (!filter.include_archived) {
5011
+ conditions.push("archived_at IS NULL");
5012
+ }
4887
5013
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4888
5014
  let limitClause = "";
4889
5015
  if (filter.limit) {
@@ -5797,6 +5923,35 @@ function bulkUpdateTasks(taskIds, updates, db) {
5797
5923
  tx();
5798
5924
  return { updated, failed };
5799
5925
  }
5926
+ function archiveTasks(options, db) {
5927
+ const d = db || getDatabase();
5928
+ const conditions = ["archived_at IS NULL"];
5929
+ const params = [];
5930
+ const statuses = options.status ?? ["completed", "failed", "cancelled"];
5931
+ conditions.push(`status IN (${statuses.map(() => "?").join(",")})`);
5932
+ params.push(...statuses);
5933
+ if (options.project_id) {
5934
+ conditions.push("project_id = ?");
5935
+ params.push(options.project_id);
5936
+ }
5937
+ if (options.task_list_id) {
5938
+ conditions.push("task_list_id = ?");
5939
+ params.push(options.task_list_id);
5940
+ }
5941
+ if (options.older_than_days !== undefined) {
5942
+ const cutoff = new Date(Date.now() - options.older_than_days * 86400000).toISOString();
5943
+ conditions.push("updated_at < ?");
5944
+ params.push(cutoff);
5945
+ }
5946
+ const ts = now();
5947
+ const result = d.run(`UPDATE tasks SET archived_at = ? WHERE ${conditions.join(" AND ")}`, [ts, ...params]);
5948
+ return { archived: result.changes };
5949
+ }
5950
+ function unarchiveTask(id, db) {
5951
+ const d = db || getDatabase();
5952
+ d.run("UPDATE tasks SET archived_at = NULL WHERE id = ?", [id]);
5953
+ return getTask(id, d);
5954
+ }
5800
5955
  function getOverdueTasks(projectId, db) {
5801
5956
  const d = db || getDatabase();
5802
5957
  const nowStr = new Date().toISOString();
@@ -6331,21 +6486,25 @@ function escapeFtsQuery(q) {
6331
6486
  return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
6332
6487
  }
6333
6488
  function searchTasks(options, projectId, taskListId, db) {
6334
- const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
6489
+ const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
6335
6490
  const d = db || getDatabase();
6336
6491
  clearExpiredLocks(d);
6337
6492
  const params = [];
6338
6493
  let sql;
6339
- if (hasFts(d) && opts.query.trim()) {
6340
- const ftsQuery = escapeFtsQuery(opts.query);
6494
+ const raw = opts.query?.trim() ?? "";
6495
+ const q = raw === "*" ? "" : raw;
6496
+ if (hasFts(d) && q) {
6497
+ const ftsQuery = escapeFtsQuery(q);
6341
6498
  sql = `SELECT t.* FROM tasks t
6342
6499
  INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
6343
6500
  WHERE tasks_fts MATCH ?`;
6344
6501
  params.push(ftsQuery);
6345
- } else {
6346
- const pattern = `%${opts.query}%`;
6502
+ } else if (q) {
6503
+ const pattern = `%${q}%`;
6347
6504
  sql = `SELECT * FROM tasks t WHERE (t.title LIKE ? OR t.description LIKE ? OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?))`;
6348
6505
  params.push(pattern, pattern, pattern);
6506
+ } else {
6507
+ sql = `SELECT * FROM tasks t WHERE 1=1`;
6349
6508
  }
6350
6509
  if (opts.project_id) {
6351
6510
  sql += " AND t.project_id = ?";
@@ -6399,7 +6558,7 @@ function searchTasks(options, projectId, taskListId, db) {
6399
6558
  } else if (opts.is_blocked === false) {
6400
6559
  sql += " AND t.id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
6401
6560
  }
6402
- if (hasFts(d) && opts.query.trim()) {
6561
+ if (hasFts(d) && q) {
6403
6562
  sql += ` ORDER BY bm25(tasks_fts),
6404
6563
  CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
6405
6564
  t.created_at DESC`;
@@ -23771,12 +23930,17 @@ ${text}` }] };
23771
23930
  if (!agent) {
23772
23931
  return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
23773
23932
  }
23933
+ const oldName = agent.name;
23774
23934
  const updated = updateAgent(agent.id, { name: new_name });
23935
+ const db = getDatabase();
23936
+ const tasksResult = db.run("UPDATE tasks SET assigned_to = ? WHERE assigned_to = ?", [new_name, oldName]);
23937
+ const taskNote = tasksResult.changes > 0 ? `
23938
+ Updated assigned_to on ${tasksResult.changes} task(s).` : "";
23775
23939
  return {
23776
23940
  content: [{
23777
23941
  type: "text",
23778
- text: `Agent renamed: ${agent.name} -> ${updated.name}
23779
- ID: ${updated.id}`
23942
+ text: `Agent renamed: ${oldName} -> ${updated.name}
23943
+ ID: ${updated.id}${taskNote}`
23780
23944
  }]
23781
23945
  };
23782
23946
  } catch (e) {
@@ -25996,6 +26160,8 @@ function formatTaskDetail(task, maxDescriptionChars) {
25996
26160
  parts.push(`Project: ${task.project_id}`);
25997
26161
  if (task.plan_id)
25998
26162
  parts.push(`Plan: ${task.plan_id}`);
26163
+ if (task.due_at)
26164
+ parts.push(`Due: ${task.due_at.slice(0, 10)}`);
25999
26165
  if (task.tags.length > 0)
26000
26166
  parts.push(`Tags: ${task.tags.join(", ")}`);
26001
26167
  if (task.recurrence_rule)
@@ -26097,7 +26263,8 @@ var init_mcp = __esm(() => {
26097
26263
  reason: exports_external.string().optional().describe("Why this task exists \u2014 context for agents picking it up"),
26098
26264
  spawned_from_session: exports_external.string().optional().describe("Session ID that created this task (for tracing task lineage)"),
26099
26265
  assigned_from_project: exports_external.string().optional().describe("Override: project ID the assigning agent is working from. Auto-detected from agent focus if omitted."),
26100
- task_type: exports_external.string().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or any custom string")
26266
+ task_type: exports_external.string().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or any custom string"),
26267
+ due_date: exports_external.string().optional().describe("Due date as YYYY-MM-DD (e.g. '2026-04-15'). Stored as end-of-day ISO timestamp.")
26101
26268
  }, async (params) => {
26102
26269
  try {
26103
26270
  if (!params.agent_id) {
@@ -26112,6 +26279,10 @@ var init_mcp = __esm(() => {
26112
26279
  resolved["plan_id"] = resolveId(resolved["plan_id"], "plans");
26113
26280
  if (resolved["task_list_id"])
26114
26281
  resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
26282
+ if (resolved["due_date"]) {
26283
+ resolved["due_at"] = `${resolved["due_date"]}T23:59:59.000Z`;
26284
+ delete resolved["due_date"];
26285
+ }
26115
26286
  if (!resolved["assigned_from_project"]) {
26116
26287
  const focus = getAgentFocus(params.agent_id);
26117
26288
  if (focus?.project_id) {
@@ -26147,7 +26318,8 @@ var init_mcp = __esm(() => {
26147
26318
  limit: exports_external.number().optional(),
26148
26319
  offset: exports_external.number().optional(),
26149
26320
  summary_only: exports_external.boolean().optional().describe("When true, return only id, short_id, title, status, priority \u2014 minimal tokens for navigation"),
26150
- cursor: exports_external.string().optional().describe("Opaque cursor from a prior response for stable pagination. Use next_cursor from the previous page. Mutually exclusive with offset.")
26321
+ cursor: exports_external.string().optional().describe("Opaque cursor from a prior response for stable pagination. Use next_cursor from the previous page. Mutually exclusive with offset."),
26322
+ include_archived: exports_external.boolean().optional().describe("When true, include archived tasks. Default: false.")
26151
26323
  }, async (params) => {
26152
26324
  try {
26153
26325
  const { due_today, overdue, summary_only, ...rest } = params;
@@ -26182,7 +26354,8 @@ var init_mcp = __esm(() => {
26182
26354
  const assigned = t.assigned_to ? ` -> ${t.assigned_to}` : "";
26183
26355
  const due = t.due_at ? ` due:${t.due_at.slice(0, 10)}` : "";
26184
26356
  const recur = t.recurrence_rule ? " [\u21BB]" : "";
26185
- return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${lock}${due}${recur}`;
26357
+ const list = t.task_list_id ? ` list:${t.task_list_id.slice(0, 8)}` : "";
26358
+ return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${list}${lock}${due}${recur}`;
26186
26359
  }).join(`
26187
26360
  `);
26188
26361
  const currentOffset = resolved.offset || 0;
@@ -26269,7 +26442,7 @@ Checklist (${done}/${task.checklist.length}):`);
26269
26442
  if (shouldRegisterTool("update_task")) {
26270
26443
  server.tool("update_task", "Update task fields. Version required for optimistic locking.", {
26271
26444
  id: exports_external.string(),
26272
- version: exports_external.number(),
26445
+ version: exports_external.union([exports_external.number(), exports_external.string()]).transform((v) => Number(v)),
26273
26446
  title: exports_external.string().optional(),
26274
26447
  description: exports_external.string().optional(),
26275
26448
  status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
@@ -26279,7 +26452,8 @@ Checklist (${done}/${task.checklist.length}):`);
26279
26452
  metadata: exports_external.record(exports_external.unknown()).optional(),
26280
26453
  plan_id: exports_external.string().optional(),
26281
26454
  task_list_id: exports_external.string().optional(),
26282
- task_type: exports_external.string().nullable().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or custom. null to clear.")
26455
+ task_type: exports_external.string().nullable().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or custom. null to clear."),
26456
+ due_date: exports_external.string().nullable().optional().describe("Due date as YYYY-MM-DD. null to clear. Stored as end-of-day ISO timestamp.")
26283
26457
  }, async ({ id, ...rest }) => {
26284
26458
  try {
26285
26459
  const resolvedId = resolveId(id);
@@ -26288,6 +26462,10 @@ Checklist (${done}/${task.checklist.length}):`);
26288
26462
  resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
26289
26463
  if (resolved.plan_id)
26290
26464
  resolved.plan_id = resolveId(resolved.plan_id, "plans");
26465
+ if ("due_date" in resolved) {
26466
+ resolved["due_at"] = resolved["due_date"] ? `${resolved["due_date"]}T23:59:59.000Z` : null;
26467
+ delete resolved["due_date"];
26468
+ }
26291
26469
  const task = updateTask(resolvedId, resolved);
26292
26470
  return { content: [{ type: "text", text: `updated: ${formatTask(task)}` }] };
26293
26471
  } catch (e) {
@@ -26523,6 +26701,29 @@ ${text}` }] };
26523
26701
  }
26524
26702
  });
26525
26703
  }
26704
+ if (shouldRegisterTool("update_project")) {
26705
+ server.tool("update_project", "Update project fields: name, description, path, task_list_id. Use new_slug to rename the project slug (cascades to task_lists).", {
26706
+ id: exports_external.string().describe("Project ID"),
26707
+ name: exports_external.string().optional(),
26708
+ description: exports_external.string().optional(),
26709
+ path: exports_external.string().optional().describe("Update global path (use projects-path set for machine-local overrides)"),
26710
+ task_list_id: exports_external.string().optional(),
26711
+ new_slug: exports_external.string().optional().describe("Rename the project slug (kebab-case). Cascades to matching task_list slugs.")
26712
+ }, async ({ id, name, description, path, task_list_id, new_slug }) => {
26713
+ try {
26714
+ const resolvedId = resolveId(id, "projects");
26715
+ if (new_slug || name && !description && !path && !task_list_id) {
26716
+ const result = renameProject(resolvedId, { name, new_slug });
26717
+ const note = result.task_lists_updated > 0 ? ` (${result.task_lists_updated} task list(s) slug updated)` : "";
26718
+ return { content: [{ type: "text", text: `Project updated: ${result.project.id.slice(0, 8)} | ${result.project.name}${note}` }] };
26719
+ }
26720
+ const updated = updateProject(resolvedId, { name, description, path, task_list_id });
26721
+ return { content: [{ type: "text", text: `Project updated: ${updated.id.slice(0, 8)} | ${updated.name}${updated.path ? ` (${updated.path})` : ""}` }] };
26722
+ } catch (e) {
26723
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
26724
+ }
26725
+ });
26726
+ }
26526
26727
  if (shouldRegisterTool("remove_project_source")) {
26527
26728
  server.tool("remove_project_source", "Remove a data source from a project by source ID.", {
26528
26729
  source_id: exports_external.string().describe("Source ID to remove")
@@ -26786,8 +26987,8 @@ ${text}` }] };
26786
26987
  });
26787
26988
  }
26788
26989
  if (shouldRegisterTool("search_tasks")) {
26789
- server.tool("search_tasks", "Full-text search across tasks with filters.", {
26790
- query: exports_external.string(),
26990
+ server.tool("search_tasks", "Full-text search across tasks with filters. query is optional \u2014 if omitted, returns all tasks matching the given filters.", {
26991
+ query: exports_external.string().optional(),
26791
26992
  project_id: exports_external.string().optional(),
26792
26993
  task_list_id: exports_external.string().optional(),
26793
26994
  status: exports_external.union([
@@ -26815,11 +27016,12 @@ ${text}` }] };
26815
27016
  ...filters
26816
27017
  });
26817
27018
  if (tasks.length === 0) {
26818
- return { content: [{ type: "text", text: `No tasks matching "${query}".` }] };
27019
+ return { content: [{ type: "text", text: query ? `No tasks matching "${query}".` : "No tasks found." }] };
26819
27020
  }
26820
27021
  const text = tasks.map((t) => `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}`).join(`
26821
27022
  `);
26822
- return { content: [{ type: "text", text: `${tasks.length} result(s) for "${query}":
27023
+ const label = query ? `${tasks.length} result(s) for "${query}"` : `${tasks.length} task(s)`;
27024
+ return { content: [{ type: "text", text: `${label}:
26823
27025
  ${text}` }] };
26824
27026
  } catch (e) {
26825
27027
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -26915,9 +27117,15 @@ Description: ${list.description}` : ""}`
26915
27117
  if (lists.length === 0) {
26916
27118
  return { content: [{ type: "text", text: "No task lists found." }] };
26917
27119
  }
27120
+ const db = getDatabase();
26918
27121
  const text = lists.map((l) => {
26919
27122
  const project = l.project_id ? ` (project: ${l.project_id.slice(0, 8)})` : "";
26920
- return `${l.id.slice(0, 8)} | ${l.name} [${l.slug}]${project}${l.description ? ` - ${l.description}` : ""}`;
27123
+ const counts = db.query(`SELECT COUNT(*) as total,
27124
+ SUM(CASE WHEN status IN ('pending','in_progress') THEN 1 ELSE 0 END) as active,
27125
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as done
27126
+ FROM tasks WHERE task_list_id = ? AND archived_at IS NULL`).get(l.id);
27127
+ const taskNote = ` [${counts.active} active, ${counts.done} done / ${counts.total} total]`;
27128
+ return `${l.id.slice(0, 8)} | ${l.name} [${l.slug}]${taskNote}${project}${l.description ? ` - ${l.description}` : ""}`;
26921
27129
  }).join(`
26922
27130
  `);
26923
27131
  return { content: [{ type: "text", text: `${lists.length} task list(s):
@@ -27505,6 +27713,52 @@ In Progress:`);
27505
27713
  }
27506
27714
  });
27507
27715
  }
27716
+ if (shouldRegisterTool("archive_completed")) {
27717
+ server.tool("archive_completed", "Archive completed/failed/cancelled tasks to reduce clutter. Archived tasks are hidden from list_tasks and search_tasks by default.", {
27718
+ project_id: exports_external.string().optional().describe("Scope to a project"),
27719
+ task_list_id: exports_external.string().optional().describe("Scope to a task list"),
27720
+ older_than_days: exports_external.number().optional().describe("Only archive tasks last updated more than N days ago. Default: all matching tasks."),
27721
+ status: exports_external.array(exports_external.enum(["completed", "failed", "cancelled"])).optional().describe("Statuses to archive. Default: completed, failed, cancelled."),
27722
+ dry_run: exports_external.boolean().optional().describe("Preview count without archiving. Default: false.")
27723
+ }, async ({ project_id, task_list_id, older_than_days, status, dry_run }) => {
27724
+ try {
27725
+ const filters = {};
27726
+ if (project_id)
27727
+ filters.project_id = resolveId(project_id, "projects");
27728
+ if (task_list_id)
27729
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
27730
+ if (older_than_days !== undefined)
27731
+ filters.older_than_days = older_than_days;
27732
+ if (status)
27733
+ filters.status = status;
27734
+ if (dry_run) {
27735
+ const db = getDatabase();
27736
+ const statuses = status ?? ["completed", "failed", "cancelled"];
27737
+ const conditions = ["archived_at IS NULL", `status IN (${statuses.map(() => "?").join(",")})`];
27738
+ const params = [...statuses];
27739
+ if (filters.project_id) {
27740
+ conditions.push("project_id = ?");
27741
+ params.push(filters.project_id);
27742
+ }
27743
+ if (filters.task_list_id) {
27744
+ conditions.push("task_list_id = ?");
27745
+ params.push(filters.task_list_id);
27746
+ }
27747
+ if (older_than_days !== undefined) {
27748
+ const cutoff = new Date(Date.now() - older_than_days * 86400000).toISOString();
27749
+ conditions.push("updated_at < ?");
27750
+ params.push(cutoff);
27751
+ }
27752
+ const count = db.query(`SELECT COUNT(*) as c FROM tasks WHERE ${conditions.join(" AND ")}`).get(...params).c;
27753
+ return { content: [{ type: "text", text: `Dry run: would archive ${count} task(s).` }] };
27754
+ }
27755
+ const result = archiveTasks(filters);
27756
+ return { content: [{ type: "text", text: `Archived ${result.archived} task(s).` }] };
27757
+ } catch (e) {
27758
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
27759
+ }
27760
+ });
27761
+ }
27508
27762
  if (shouldRegisterTool("bulk_update_tasks")) {
27509
27763
  server.tool("bulk_update_tasks", "Update multiple tasks at once. Two modes: (1) task_ids + shared fields \u2014 apply the same changes to all; (2) updates array \u2014 per-task fields, each entry has id plus any fields to update.", {
27510
27764
  task_ids: exports_external.array(exports_external.string()).optional().describe("Task IDs to update with the same fields (mode 1)"),
@@ -27617,6 +27871,26 @@ In Progress:`);
27617
27871
  if (agent_id)
27618
27872
  filters.agent_id = agent_id;
27619
27873
  const stats = getTaskStats(Object.keys(filters).length > 0 ? filters : undefined);
27874
+ if (!task_list_id) {
27875
+ const db = getDatabase();
27876
+ const listRows = db.query(`SELECT tl.id, tl.name, tl.slug,
27877
+ COUNT(t.id) as total,
27878
+ SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as completed,
27879
+ SUM(CASE WHEN t.status IN ('pending','in_progress') THEN 1 ELSE 0 END) as active
27880
+ FROM task_lists tl
27881
+ LEFT JOIN tasks t ON t.task_list_id = tl.id ${filters.project_id ? "AND t.project_id = ?" : ""}
27882
+ ${filters.project_id ? "WHERE tl.project_id = ?" : ""}
27883
+ GROUP BY tl.id ORDER BY tl.name`).all(...filters.project_id ? [filters.project_id, filters.project_id] : []);
27884
+ stats.by_task_list = listRows.map((r) => ({
27885
+ id: r.id.slice(0, 8),
27886
+ name: r.name,
27887
+ slug: r.slug,
27888
+ total: r.total,
27889
+ completed: r.completed,
27890
+ active: r.active,
27891
+ completion_rate: r.total > 0 ? Math.round(r.completed / r.total * 100) : 0
27892
+ }));
27893
+ }
27620
27894
  return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
27621
27895
  } catch (e) {
27622
27896
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -33513,6 +33787,10 @@ program2.command("projects").description("List and manage projects").option("--a
33513
33787
  } else {
33514
33788
  project = createProject({ name, path: projectPath, task_list_id: opts.taskListId });
33515
33789
  }
33790
+ try {
33791
+ const { setMachineLocalPath: setMachineLocalPath2 } = (init_projects(), __toCommonJS(exports_projects));
33792
+ setMachineLocalPath2(project.id, projectPath);
33793
+ } catch {}
33516
33794
  if (globalOpts.json) {
33517
33795
  output(project, true);
33518
33796
  } else {
@@ -33538,6 +33816,106 @@ program2.command("projects").description("List and manage projects").option("--a
33538
33816
  console.log(`${chalk3.dim(p.id.slice(0, 8))} ${chalk3.bold(p.name)} ${chalk3.dim(p.path)}${taskList}${p.description ? ` - ${p.description}` : ""}`);
33539
33817
  }
33540
33818
  });
33819
+ program2.command("project-rename <id-or-slug> <new-slug>").description("Rename a project slug. Cascades to matching task lists. Task prefixes (e.g. APP-00001) are unchanged.").option("--name <name>", "Also update the project display name").option("--json", "Output as JSON").action((idOrSlug, newSlug, opts) => {
33820
+ const globalOpts = program2.opts();
33821
+ const useJson = opts.json || globalOpts.json;
33822
+ try {
33823
+ const { renameProject: renameProject2 } = (init_projects(), __toCommonJS(exports_projects));
33824
+ const db = getDatabase();
33825
+ let resolvedId = resolvePartialId(db, "projects", idOrSlug);
33826
+ if (!resolvedId) {
33827
+ const bySlug = db.query("SELECT id FROM projects WHERE task_list_id = ?").get(idOrSlug);
33828
+ resolvedId = bySlug?.id ?? null;
33829
+ }
33830
+ if (!resolvedId) {
33831
+ console.error(chalk3.red(`Project not found: ${idOrSlug}`));
33832
+ process.exit(1);
33833
+ }
33834
+ const result = renameProject2(resolvedId, { name: opts.name, new_slug: newSlug });
33835
+ if (useJson) {
33836
+ output({ project: result.project, task_lists_updated: result.task_lists_updated }, true);
33837
+ } else {
33838
+ console.log(chalk3.green(`Project renamed: ${result.project.name} (slug: ${result.project.task_list_id})`));
33839
+ if (result.task_lists_updated > 0) {
33840
+ console.log(chalk3.dim(` Updated ${result.task_lists_updated} task list slug(s).`));
33841
+ }
33842
+ }
33843
+ } catch (e) {
33844
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
33845
+ process.exit(1);
33846
+ }
33847
+ });
33848
+ var projectsPathCmd = program2.command("projects-path").description("Manage machine-local path overrides for projects");
33849
+ projectsPathCmd.command("set <project-id> <path>").description("Set the local path for a project on this machine").option("--json", "Output as JSON").action((projectId, projectPath, opts) => {
33850
+ const globalOpts = program2.opts();
33851
+ const useJson = opts.json || globalOpts.json;
33852
+ try {
33853
+ const { setMachineLocalPath: setMachineLocalPath2 } = (init_projects(), __toCommonJS(exports_projects));
33854
+ const db = getDatabase();
33855
+ const resolved = resolvePartialId(db, "projects", projectId);
33856
+ if (!resolved) {
33857
+ console.error(chalk3.red(`Project not found: ${projectId}`));
33858
+ process.exit(1);
33859
+ }
33860
+ const entry = setMachineLocalPath2(resolved, resolve4(projectPath));
33861
+ if (useJson) {
33862
+ output(entry, true);
33863
+ } else {
33864
+ console.log(chalk3.green(`Local path set: ${entry.path} (machine: ${entry.machine_id.slice(0, 8)})`));
33865
+ }
33866
+ } catch (e) {
33867
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
33868
+ process.exit(1);
33869
+ }
33870
+ });
33871
+ projectsPathCmd.command("list <project-id>").description("List all machine path overrides for a project").option("--json", "Output as JSON").action((projectId, opts) => {
33872
+ const globalOpts = program2.opts();
33873
+ const useJson = opts.json || globalOpts.json;
33874
+ try {
33875
+ const { listMachineLocalPaths: listMachineLocalPaths2 } = (init_projects(), __toCommonJS(exports_projects));
33876
+ const db = getDatabase();
33877
+ const resolved = resolvePartialId(db, "projects", projectId);
33878
+ if (!resolved) {
33879
+ console.error(chalk3.red(`Project not found: ${projectId}`));
33880
+ process.exit(1);
33881
+ }
33882
+ const paths = listMachineLocalPaths2(resolved);
33883
+ if (useJson) {
33884
+ output(paths, true);
33885
+ return;
33886
+ }
33887
+ if (paths.length === 0) {
33888
+ console.log(chalk3.dim("No machine path overrides."));
33889
+ return;
33890
+ }
33891
+ for (const p of paths) {
33892
+ console.log(`${chalk3.dim(p.machine_id.slice(0, 8))} ${p.path} ${chalk3.dim(p.updated_at)}`);
33893
+ }
33894
+ } catch (e) {
33895
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
33896
+ process.exit(1);
33897
+ }
33898
+ });
33899
+ projectsPathCmd.command("remove <project-id>").description("Remove the local path override for a project on this machine").option("--machine <id>", "Machine ID to remove override for (default: this machine)").action((projectId, opts) => {
33900
+ try {
33901
+ const { removeMachineLocalPath: removeMachineLocalPath2 } = (init_projects(), __toCommonJS(exports_projects));
33902
+ const db = getDatabase();
33903
+ const resolved = resolvePartialId(db, "projects", projectId);
33904
+ if (!resolved) {
33905
+ console.error(chalk3.red(`Project not found: ${projectId}`));
33906
+ process.exit(1);
33907
+ }
33908
+ const removed = removeMachineLocalPath2(resolved, opts.machine);
33909
+ if (removed) {
33910
+ console.log(chalk3.green("Machine path override removed."));
33911
+ } else {
33912
+ console.log(chalk3.dim("No override found to remove."));
33913
+ }
33914
+ } catch (e) {
33915
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
33916
+ process.exit(1);
33917
+ }
33918
+ });
33541
33919
  program2.command("extract <path>").description("Extract TODO/FIXME/HACK/BUG/XXX/NOTE comments from source files and create tasks").option("--dry-run", "Show extracted comments without creating tasks").option("--pattern <tags>", "Comma-separated tags to look for (default: TODO,FIXME,HACK,XXX,BUG,NOTE)").option("-t, --tags <tags>", "Extra comma-separated tags to add to created tasks").option("--assign <agent>", "Assign extracted tasks to an agent").option("--list <id>", "Task list ID").option("--ext <extensions>", "Comma-separated file extensions to scan (e.g. ts,py,go)").action((scanPath, opts) => {
33542
33920
  try {
33543
33921
  const globalOpts = program2.opts();