@hasna/todos 0.11.18 → 0.11.20

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]);
@@ -3665,13 +3739,53 @@ function ensureProject(name, path, db) {
3665
3739
  d.run("UPDATE projects SET task_prefix = ?, updated_at = ? WHERE id = ?", [prefix, now(), existing.id]);
3666
3740
  return getProject(existing.id, d);
3667
3741
  }
3742
+ setMachineLocalPath(existing.id, path, d);
3668
3743
  return existing;
3669
3744
  }
3670
- return createProject({ name, path }, d);
3745
+ const project = createProject({ name, path }, d);
3746
+ setMachineLocalPath(project.id, path, d);
3747
+ return project;
3748
+ }
3749
+ function setMachineLocalPath(projectId, path, db) {
3750
+ const d = db || getDatabase();
3751
+ const machineId = getMachineId(d);
3752
+ const ts = now();
3753
+ const existing = d.query("SELECT * FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(projectId, machineId);
3754
+ if (existing) {
3755
+ if (existing.path !== path) {
3756
+ d.run("UPDATE project_machine_paths SET path = ?, updated_at = ? WHERE id = ?", [path, ts, existing.id]);
3757
+ }
3758
+ return d.query("SELECT * FROM project_machine_paths WHERE id = ?").get(existing.id);
3759
+ }
3760
+ const id = uuid();
3761
+ d.run("INSERT INTO project_machine_paths (id, project_id, machine_id, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", [id, projectId, machineId, path, ts, ts]);
3762
+ return d.query("SELECT * FROM project_machine_paths WHERE id = ?").get(id);
3763
+ }
3764
+ function getMachineLocalPath(projectId, db) {
3765
+ const d = db || getDatabase();
3766
+ try {
3767
+ const machineId = getMachineId(d);
3768
+ const row = d.query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(projectId, machineId);
3769
+ if (row)
3770
+ return row.path;
3771
+ } catch {}
3772
+ const project = getProject(projectId, d);
3773
+ return project?.path ?? null;
3774
+ }
3775
+ function listMachineLocalPaths(projectId, db) {
3776
+ const d = db || getDatabase();
3777
+ return d.query("SELECT * FROM project_machine_paths WHERE project_id = ? ORDER BY machine_id").all(projectId);
3778
+ }
3779
+ function removeMachineLocalPath(projectId, machineId, db) {
3780
+ const d = db || getDatabase();
3781
+ const mid = machineId ?? getMachineId(d);
3782
+ const result = d.run("DELETE FROM project_machine_paths WHERE project_id = ? AND machine_id = ?", [projectId, mid]);
3783
+ return result.changes > 0;
3671
3784
  }
3672
3785
  var init_projects = __esm(() => {
3673
3786
  init_types();
3674
3787
  init_database();
3788
+ init_machines();
3675
3789
  });
3676
3790
 
3677
3791
  // src/lib/sync-utils.ts
@@ -4658,6 +4772,7 @@ var exports_tasks = {};
4658
4772
  __export(exports_tasks, {
4659
4773
  updateTask: () => updateTask,
4660
4774
  unlockTask: () => unlockTask,
4775
+ unarchiveTask: () => unarchiveTask,
4661
4776
  stealTask: () => stealTask,
4662
4777
  startTask: () => startTask,
4663
4778
  setTaskStatus: () => setTaskStatus,
@@ -4692,6 +4807,7 @@ __export(exports_tasks, {
4692
4807
  claimNextTask: () => claimNextTask,
4693
4808
  bulkUpdateTasks: () => bulkUpdateTasks,
4694
4809
  bulkCreateTasks: () => bulkCreateTasks,
4810
+ archiveTasks: () => archiveTasks,
4695
4811
  addDependency: () => addDependency
4696
4812
  });
4697
4813
  function rowToTask(row) {
@@ -4884,6 +5000,9 @@ function listTasks(filter = {}, db) {
4884
5000
  params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
4885
5001
  } catch {}
4886
5002
  }
5003
+ if (!filter.include_archived) {
5004
+ conditions.push("archived_at IS NULL");
5005
+ }
4887
5006
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4888
5007
  let limitClause = "";
4889
5008
  if (filter.limit) {
@@ -5797,6 +5916,35 @@ function bulkUpdateTasks(taskIds, updates, db) {
5797
5916
  tx();
5798
5917
  return { updated, failed };
5799
5918
  }
5919
+ function archiveTasks(options, db) {
5920
+ const d = db || getDatabase();
5921
+ const conditions = ["archived_at IS NULL"];
5922
+ const params = [];
5923
+ const statuses = options.status ?? ["completed", "failed", "cancelled"];
5924
+ conditions.push(`status IN (${statuses.map(() => "?").join(",")})`);
5925
+ params.push(...statuses);
5926
+ if (options.project_id) {
5927
+ conditions.push("project_id = ?");
5928
+ params.push(options.project_id);
5929
+ }
5930
+ if (options.task_list_id) {
5931
+ conditions.push("task_list_id = ?");
5932
+ params.push(options.task_list_id);
5933
+ }
5934
+ if (options.older_than_days !== undefined) {
5935
+ const cutoff = new Date(Date.now() - options.older_than_days * 86400000).toISOString();
5936
+ conditions.push("updated_at < ?");
5937
+ params.push(cutoff);
5938
+ }
5939
+ const ts = now();
5940
+ const result = d.run(`UPDATE tasks SET archived_at = ? WHERE ${conditions.join(" AND ")}`, [ts, ...params]);
5941
+ return { archived: result.changes };
5942
+ }
5943
+ function unarchiveTask(id, db) {
5944
+ const d = db || getDatabase();
5945
+ d.run("UPDATE tasks SET archived_at = NULL WHERE id = ?", [id]);
5946
+ return getTask(id, d);
5947
+ }
5800
5948
  function getOverdueTasks(projectId, db) {
5801
5949
  const d = db || getDatabase();
5802
5950
  const nowStr = new Date().toISOString();
@@ -6331,21 +6479,25 @@ function escapeFtsQuery(q) {
6331
6479
  return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
6332
6480
  }
6333
6481
  function searchTasks(options, projectId, taskListId, db) {
6334
- const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
6482
+ const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
6335
6483
  const d = db || getDatabase();
6336
6484
  clearExpiredLocks(d);
6337
6485
  const params = [];
6338
6486
  let sql;
6339
- if (hasFts(d) && opts.query.trim()) {
6340
- const ftsQuery = escapeFtsQuery(opts.query);
6487
+ const raw = opts.query?.trim() ?? "";
6488
+ const q = raw === "*" ? "" : raw;
6489
+ if (hasFts(d) && q) {
6490
+ const ftsQuery = escapeFtsQuery(q);
6341
6491
  sql = `SELECT t.* FROM tasks t
6342
6492
  INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
6343
6493
  WHERE tasks_fts MATCH ?`;
6344
6494
  params.push(ftsQuery);
6345
- } else {
6346
- const pattern = `%${opts.query}%`;
6495
+ } else if (q) {
6496
+ const pattern = `%${q}%`;
6347
6497
  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
6498
  params.push(pattern, pattern, pattern);
6499
+ } else {
6500
+ sql = `SELECT * FROM tasks t WHERE 1=1`;
6349
6501
  }
6350
6502
  if (opts.project_id) {
6351
6503
  sql += " AND t.project_id = ?";
@@ -6399,7 +6551,7 @@ function searchTasks(options, projectId, taskListId, db) {
6399
6551
  } else if (opts.is_blocked === false) {
6400
6552
  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
6553
  }
6402
- if (hasFts(d) && opts.query.trim()) {
6554
+ if (hasFts(d) && q) {
6403
6555
  sql += ` ORDER BY bm25(tasks_fts),
6404
6556
  CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
6405
6557
  t.created_at DESC`;
@@ -23771,12 +23923,17 @@ ${text}` }] };
23771
23923
  if (!agent) {
23772
23924
  return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
23773
23925
  }
23926
+ const oldName = agent.name;
23774
23927
  const updated = updateAgent(agent.id, { name: new_name });
23928
+ const db = getDatabase();
23929
+ const tasksResult = db.run("UPDATE tasks SET assigned_to = ? WHERE assigned_to = ?", [new_name, oldName]);
23930
+ const taskNote = tasksResult.changes > 0 ? `
23931
+ Updated assigned_to on ${tasksResult.changes} task(s).` : "";
23775
23932
  return {
23776
23933
  content: [{
23777
23934
  type: "text",
23778
- text: `Agent renamed: ${agent.name} -> ${updated.name}
23779
- ID: ${updated.id}`
23935
+ text: `Agent renamed: ${oldName} -> ${updated.name}
23936
+ ID: ${updated.id}${taskNote}`
23780
23937
  }]
23781
23938
  };
23782
23939
  } catch (e) {
@@ -25996,6 +26153,8 @@ function formatTaskDetail(task, maxDescriptionChars) {
25996
26153
  parts.push(`Project: ${task.project_id}`);
25997
26154
  if (task.plan_id)
25998
26155
  parts.push(`Plan: ${task.plan_id}`);
26156
+ if (task.due_at)
26157
+ parts.push(`Due: ${task.due_at.slice(0, 10)}`);
25999
26158
  if (task.tags.length > 0)
26000
26159
  parts.push(`Tags: ${task.tags.join(", ")}`);
26001
26160
  if (task.recurrence_rule)
@@ -26097,7 +26256,8 @@ var init_mcp = __esm(() => {
26097
26256
  reason: exports_external.string().optional().describe("Why this task exists \u2014 context for agents picking it up"),
26098
26257
  spawned_from_session: exports_external.string().optional().describe("Session ID that created this task (for tracing task lineage)"),
26099
26258
  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")
26259
+ task_type: exports_external.string().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or any custom string"),
26260
+ 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
26261
  }, async (params) => {
26102
26262
  try {
26103
26263
  if (!params.agent_id) {
@@ -26112,6 +26272,10 @@ var init_mcp = __esm(() => {
26112
26272
  resolved["plan_id"] = resolveId(resolved["plan_id"], "plans");
26113
26273
  if (resolved["task_list_id"])
26114
26274
  resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
26275
+ if (resolved["due_date"]) {
26276
+ resolved["due_at"] = `${resolved["due_date"]}T23:59:59.000Z`;
26277
+ delete resolved["due_date"];
26278
+ }
26115
26279
  if (!resolved["assigned_from_project"]) {
26116
26280
  const focus = getAgentFocus(params.agent_id);
26117
26281
  if (focus?.project_id) {
@@ -26147,7 +26311,8 @@ var init_mcp = __esm(() => {
26147
26311
  limit: exports_external.number().optional(),
26148
26312
  offset: exports_external.number().optional(),
26149
26313
  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.")
26314
+ 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."),
26315
+ include_archived: exports_external.boolean().optional().describe("When true, include archived tasks. Default: false.")
26151
26316
  }, async (params) => {
26152
26317
  try {
26153
26318
  const { due_today, overdue, summary_only, ...rest } = params;
@@ -26182,7 +26347,8 @@ var init_mcp = __esm(() => {
26182
26347
  const assigned = t.assigned_to ? ` -> ${t.assigned_to}` : "";
26183
26348
  const due = t.due_at ? ` due:${t.due_at.slice(0, 10)}` : "";
26184
26349
  const recur = t.recurrence_rule ? " [\u21BB]" : "";
26185
- return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${lock}${due}${recur}`;
26350
+ const list = t.task_list_id ? ` list:${t.task_list_id.slice(0, 8)}` : "";
26351
+ return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${list}${lock}${due}${recur}`;
26186
26352
  }).join(`
26187
26353
  `);
26188
26354
  const currentOffset = resolved.offset || 0;
@@ -26269,7 +26435,7 @@ Checklist (${done}/${task.checklist.length}):`);
26269
26435
  if (shouldRegisterTool("update_task")) {
26270
26436
  server.tool("update_task", "Update task fields. Version required for optimistic locking.", {
26271
26437
  id: exports_external.string(),
26272
- version: exports_external.number(),
26438
+ version: exports_external.union([exports_external.number(), exports_external.string()]).transform((v) => Number(v)),
26273
26439
  title: exports_external.string().optional(),
26274
26440
  description: exports_external.string().optional(),
26275
26441
  status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
@@ -26279,7 +26445,8 @@ Checklist (${done}/${task.checklist.length}):`);
26279
26445
  metadata: exports_external.record(exports_external.unknown()).optional(),
26280
26446
  plan_id: exports_external.string().optional(),
26281
26447
  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.")
26448
+ task_type: exports_external.string().nullable().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or custom. null to clear."),
26449
+ 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
26450
  }, async ({ id, ...rest }) => {
26284
26451
  try {
26285
26452
  const resolvedId = resolveId(id);
@@ -26288,6 +26455,10 @@ Checklist (${done}/${task.checklist.length}):`);
26288
26455
  resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
26289
26456
  if (resolved.plan_id)
26290
26457
  resolved.plan_id = resolveId(resolved.plan_id, "plans");
26458
+ if ("due_date" in resolved) {
26459
+ resolved["due_at"] = resolved["due_date"] ? `${resolved["due_date"]}T23:59:59.000Z` : null;
26460
+ delete resolved["due_date"];
26461
+ }
26291
26462
  const task = updateTask(resolvedId, resolved);
26292
26463
  return { content: [{ type: "text", text: `updated: ${formatTask(task)}` }] };
26293
26464
  } catch (e) {
@@ -26523,6 +26694,29 @@ ${text}` }] };
26523
26694
  }
26524
26695
  });
26525
26696
  }
26697
+ if (shouldRegisterTool("update_project")) {
26698
+ 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).", {
26699
+ id: exports_external.string().describe("Project ID"),
26700
+ name: exports_external.string().optional(),
26701
+ description: exports_external.string().optional(),
26702
+ path: exports_external.string().optional().describe("Update global path (use projects-path set for machine-local overrides)"),
26703
+ task_list_id: exports_external.string().optional(),
26704
+ new_slug: exports_external.string().optional().describe("Rename the project slug (kebab-case). Cascades to matching task_list slugs.")
26705
+ }, async ({ id, name, description, path, task_list_id, new_slug }) => {
26706
+ try {
26707
+ const resolvedId = resolveId(id, "projects");
26708
+ if (new_slug || name && !description && !path && !task_list_id) {
26709
+ const result = renameProject(resolvedId, { name, new_slug });
26710
+ const note = result.task_lists_updated > 0 ? ` (${result.task_lists_updated} task list(s) slug updated)` : "";
26711
+ return { content: [{ type: "text", text: `Project updated: ${result.project.id.slice(0, 8)} | ${result.project.name}${note}` }] };
26712
+ }
26713
+ const updated = updateProject(resolvedId, { name, description, path, task_list_id });
26714
+ return { content: [{ type: "text", text: `Project updated: ${updated.id.slice(0, 8)} | ${updated.name}${updated.path ? ` (${updated.path})` : ""}` }] };
26715
+ } catch (e) {
26716
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
26717
+ }
26718
+ });
26719
+ }
26526
26720
  if (shouldRegisterTool("remove_project_source")) {
26527
26721
  server.tool("remove_project_source", "Remove a data source from a project by source ID.", {
26528
26722
  source_id: exports_external.string().describe("Source ID to remove")
@@ -26786,8 +26980,8 @@ ${text}` }] };
26786
26980
  });
26787
26981
  }
26788
26982
  if (shouldRegisterTool("search_tasks")) {
26789
- server.tool("search_tasks", "Full-text search across tasks with filters.", {
26790
- query: exports_external.string(),
26983
+ server.tool("search_tasks", "Full-text search across tasks with filters. query is optional \u2014 if omitted, returns all tasks matching the given filters.", {
26984
+ query: exports_external.string().optional(),
26791
26985
  project_id: exports_external.string().optional(),
26792
26986
  task_list_id: exports_external.string().optional(),
26793
26987
  status: exports_external.union([
@@ -26815,11 +27009,12 @@ ${text}` }] };
26815
27009
  ...filters
26816
27010
  });
26817
27011
  if (tasks.length === 0) {
26818
- return { content: [{ type: "text", text: `No tasks matching "${query}".` }] };
27012
+ return { content: [{ type: "text", text: query ? `No tasks matching "${query}".` : "No tasks found." }] };
26819
27013
  }
26820
27014
  const text = tasks.map((t) => `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}`).join(`
26821
27015
  `);
26822
- return { content: [{ type: "text", text: `${tasks.length} result(s) for "${query}":
27016
+ const label = query ? `${tasks.length} result(s) for "${query}"` : `${tasks.length} task(s)`;
27017
+ return { content: [{ type: "text", text: `${label}:
26823
27018
  ${text}` }] };
26824
27019
  } catch (e) {
26825
27020
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -26915,9 +27110,15 @@ Description: ${list.description}` : ""}`
26915
27110
  if (lists.length === 0) {
26916
27111
  return { content: [{ type: "text", text: "No task lists found." }] };
26917
27112
  }
27113
+ const db = getDatabase();
26918
27114
  const text = lists.map((l) => {
26919
27115
  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}` : ""}`;
27116
+ const counts = db.query(`SELECT COUNT(*) as total,
27117
+ SUM(CASE WHEN status IN ('pending','in_progress') THEN 1 ELSE 0 END) as active,
27118
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as done
27119
+ FROM tasks WHERE task_list_id = ? AND archived_at IS NULL`).get(l.id);
27120
+ const taskNote = ` [${counts.active} active, ${counts.done} done / ${counts.total} total]`;
27121
+ return `${l.id.slice(0, 8)} | ${l.name} [${l.slug}]${taskNote}${project}${l.description ? ` - ${l.description}` : ""}`;
26921
27122
  }).join(`
26922
27123
  `);
26923
27124
  return { content: [{ type: "text", text: `${lists.length} task list(s):
@@ -27505,6 +27706,52 @@ In Progress:`);
27505
27706
  }
27506
27707
  });
27507
27708
  }
27709
+ if (shouldRegisterTool("archive_completed")) {
27710
+ server.tool("archive_completed", "Archive completed/failed/cancelled tasks to reduce clutter. Archived tasks are hidden from list_tasks and search_tasks by default.", {
27711
+ project_id: exports_external.string().optional().describe("Scope to a project"),
27712
+ task_list_id: exports_external.string().optional().describe("Scope to a task list"),
27713
+ older_than_days: exports_external.number().optional().describe("Only archive tasks last updated more than N days ago. Default: all matching tasks."),
27714
+ status: exports_external.array(exports_external.enum(["completed", "failed", "cancelled"])).optional().describe("Statuses to archive. Default: completed, failed, cancelled."),
27715
+ dry_run: exports_external.boolean().optional().describe("Preview count without archiving. Default: false.")
27716
+ }, async ({ project_id, task_list_id, older_than_days, status, dry_run }) => {
27717
+ try {
27718
+ const filters = {};
27719
+ if (project_id)
27720
+ filters.project_id = resolveId(project_id, "projects");
27721
+ if (task_list_id)
27722
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
27723
+ if (older_than_days !== undefined)
27724
+ filters.older_than_days = older_than_days;
27725
+ if (status)
27726
+ filters.status = status;
27727
+ if (dry_run) {
27728
+ const db = getDatabase();
27729
+ const statuses = status ?? ["completed", "failed", "cancelled"];
27730
+ const conditions = ["archived_at IS NULL", `status IN (${statuses.map(() => "?").join(",")})`];
27731
+ const params = [...statuses];
27732
+ if (filters.project_id) {
27733
+ conditions.push("project_id = ?");
27734
+ params.push(filters.project_id);
27735
+ }
27736
+ if (filters.task_list_id) {
27737
+ conditions.push("task_list_id = ?");
27738
+ params.push(filters.task_list_id);
27739
+ }
27740
+ if (older_than_days !== undefined) {
27741
+ const cutoff = new Date(Date.now() - older_than_days * 86400000).toISOString();
27742
+ conditions.push("updated_at < ?");
27743
+ params.push(cutoff);
27744
+ }
27745
+ const count = db.query(`SELECT COUNT(*) as c FROM tasks WHERE ${conditions.join(" AND ")}`).get(...params).c;
27746
+ return { content: [{ type: "text", text: `Dry run: would archive ${count} task(s).` }] };
27747
+ }
27748
+ const result = archiveTasks(filters);
27749
+ return { content: [{ type: "text", text: `Archived ${result.archived} task(s).` }] };
27750
+ } catch (e) {
27751
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
27752
+ }
27753
+ });
27754
+ }
27508
27755
  if (shouldRegisterTool("bulk_update_tasks")) {
27509
27756
  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
27757
  task_ids: exports_external.array(exports_external.string()).optional().describe("Task IDs to update with the same fields (mode 1)"),
@@ -27617,6 +27864,26 @@ In Progress:`);
27617
27864
  if (agent_id)
27618
27865
  filters.agent_id = agent_id;
27619
27866
  const stats = getTaskStats(Object.keys(filters).length > 0 ? filters : undefined);
27867
+ if (!task_list_id) {
27868
+ const db = getDatabase();
27869
+ const listRows = db.query(`SELECT tl.id, tl.name, tl.slug,
27870
+ COUNT(t.id) as total,
27871
+ SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as completed,
27872
+ SUM(CASE WHEN t.status IN ('pending','in_progress') THEN 1 ELSE 0 END) as active
27873
+ FROM task_lists tl
27874
+ LEFT JOIN tasks t ON t.task_list_id = tl.id ${filters.project_id ? "AND t.project_id = ?" : ""}
27875
+ ${filters.project_id ? "WHERE tl.project_id = ?" : ""}
27876
+ GROUP BY tl.id ORDER BY tl.name`).all(...filters.project_id ? [filters.project_id, filters.project_id] : []);
27877
+ stats.by_task_list = listRows.map((r) => ({
27878
+ id: r.id.slice(0, 8),
27879
+ name: r.name,
27880
+ slug: r.slug,
27881
+ total: r.total,
27882
+ completed: r.completed,
27883
+ active: r.active,
27884
+ completion_rate: r.total > 0 ? Math.round(r.completed / r.total * 100) : 0
27885
+ }));
27886
+ }
27620
27887
  return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
27621
27888
  } catch (e) {
27622
27889
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -33513,6 +33780,10 @@ program2.command("projects").description("List and manage projects").option("--a
33513
33780
  } else {
33514
33781
  project = createProject({ name, path: projectPath, task_list_id: opts.taskListId });
33515
33782
  }
33783
+ try {
33784
+ const { setMachineLocalPath: setMachineLocalPath2 } = (init_projects(), __toCommonJS(exports_projects));
33785
+ setMachineLocalPath2(project.id, projectPath);
33786
+ } catch {}
33516
33787
  if (globalOpts.json) {
33517
33788
  output(project, true);
33518
33789
  } else {
@@ -33538,6 +33809,106 @@ program2.command("projects").description("List and manage projects").option("--a
33538
33809
  console.log(`${chalk3.dim(p.id.slice(0, 8))} ${chalk3.bold(p.name)} ${chalk3.dim(p.path)}${taskList}${p.description ? ` - ${p.description}` : ""}`);
33539
33810
  }
33540
33811
  });
33812
+ 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) => {
33813
+ const globalOpts = program2.opts();
33814
+ const useJson = opts.json || globalOpts.json;
33815
+ try {
33816
+ const { renameProject: renameProject2 } = (init_projects(), __toCommonJS(exports_projects));
33817
+ const db = getDatabase();
33818
+ let resolvedId = resolvePartialId(db, "projects", idOrSlug);
33819
+ if (!resolvedId) {
33820
+ const bySlug = db.query("SELECT id FROM projects WHERE task_list_id = ?").get(idOrSlug);
33821
+ resolvedId = bySlug?.id ?? null;
33822
+ }
33823
+ if (!resolvedId) {
33824
+ console.error(chalk3.red(`Project not found: ${idOrSlug}`));
33825
+ process.exit(1);
33826
+ }
33827
+ const result = renameProject2(resolvedId, { name: opts.name, new_slug: newSlug });
33828
+ if (useJson) {
33829
+ output({ project: result.project, task_lists_updated: result.task_lists_updated }, true);
33830
+ } else {
33831
+ console.log(chalk3.green(`Project renamed: ${result.project.name} (slug: ${result.project.task_list_id})`));
33832
+ if (result.task_lists_updated > 0) {
33833
+ console.log(chalk3.dim(` Updated ${result.task_lists_updated} task list slug(s).`));
33834
+ }
33835
+ }
33836
+ } catch (e) {
33837
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
33838
+ process.exit(1);
33839
+ }
33840
+ });
33841
+ var projectsPathCmd = program2.command("projects-path").description("Manage machine-local path overrides for projects");
33842
+ 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) => {
33843
+ const globalOpts = program2.opts();
33844
+ const useJson = opts.json || globalOpts.json;
33845
+ try {
33846
+ const { setMachineLocalPath: setMachineLocalPath2 } = (init_projects(), __toCommonJS(exports_projects));
33847
+ const db = getDatabase();
33848
+ const resolved = resolvePartialId(db, "projects", projectId);
33849
+ if (!resolved) {
33850
+ console.error(chalk3.red(`Project not found: ${projectId}`));
33851
+ process.exit(1);
33852
+ }
33853
+ const entry = setMachineLocalPath2(resolved, resolve4(projectPath));
33854
+ if (useJson) {
33855
+ output(entry, true);
33856
+ } else {
33857
+ console.log(chalk3.green(`Local path set: ${entry.path} (machine: ${entry.machine_id.slice(0, 8)})`));
33858
+ }
33859
+ } catch (e) {
33860
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
33861
+ process.exit(1);
33862
+ }
33863
+ });
33864
+ projectsPathCmd.command("list <project-id>").description("List all machine path overrides for a project").option("--json", "Output as JSON").action((projectId, opts) => {
33865
+ const globalOpts = program2.opts();
33866
+ const useJson = opts.json || globalOpts.json;
33867
+ try {
33868
+ const { listMachineLocalPaths: listMachineLocalPaths2 } = (init_projects(), __toCommonJS(exports_projects));
33869
+ const db = getDatabase();
33870
+ const resolved = resolvePartialId(db, "projects", projectId);
33871
+ if (!resolved) {
33872
+ console.error(chalk3.red(`Project not found: ${projectId}`));
33873
+ process.exit(1);
33874
+ }
33875
+ const paths = listMachineLocalPaths2(resolved);
33876
+ if (useJson) {
33877
+ output(paths, true);
33878
+ return;
33879
+ }
33880
+ if (paths.length === 0) {
33881
+ console.log(chalk3.dim("No machine path overrides."));
33882
+ return;
33883
+ }
33884
+ for (const p of paths) {
33885
+ console.log(`${chalk3.dim(p.machine_id.slice(0, 8))} ${p.path} ${chalk3.dim(p.updated_at)}`);
33886
+ }
33887
+ } catch (e) {
33888
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
33889
+ process.exit(1);
33890
+ }
33891
+ });
33892
+ 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) => {
33893
+ try {
33894
+ const { removeMachineLocalPath: removeMachineLocalPath2 } = (init_projects(), __toCommonJS(exports_projects));
33895
+ const db = getDatabase();
33896
+ const resolved = resolvePartialId(db, "projects", projectId);
33897
+ if (!resolved) {
33898
+ console.error(chalk3.red(`Project not found: ${projectId}`));
33899
+ process.exit(1);
33900
+ }
33901
+ const removed = removeMachineLocalPath2(resolved, opts.machine);
33902
+ if (removed) {
33903
+ console.log(chalk3.green("Machine path override removed."));
33904
+ } else {
33905
+ console.log(chalk3.dim("No override found to remove."));
33906
+ }
33907
+ } catch (e) {
33908
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
33909
+ process.exit(1);
33910
+ }
33911
+ });
33541
33912
  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
33913
  try {
33543
33914
  const globalOpts = program2.opts();