@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/mcp/index.js CHANGED
@@ -200,6 +200,18 @@ function ensureSchema(db) {
200
200
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
201
201
  UNIQUE(source_id, source_type, target_id, target_type, relation_type)
202
202
  )`);
203
+ ensureTable("project_machine_paths", `
204
+ CREATE TABLE project_machine_paths (
205
+ id TEXT PRIMARY KEY,
206
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
207
+ machine_id TEXT NOT NULL,
208
+ path TEXT NOT NULL,
209
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
210
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
211
+ UNIQUE(project_id, machine_id)
212
+ )`);
213
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
214
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
203
215
  ensureTable("machines", `
204
216
  CREATE TABLE machines (
205
217
  id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
@@ -235,6 +247,7 @@ function ensureSchema(db) {
235
247
  ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
236
248
  ensureColumn("tasks", "retry_after", "TEXT");
237
249
  ensureColumn("tasks", "sla_minutes", "INTEGER");
250
+ ensureColumn("tasks", "archived_at", "TEXT");
238
251
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
239
252
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
240
253
  ensureColumn("agents", "reports_to", "TEXT");
@@ -1073,6 +1086,25 @@ var init_schema = __esm(() => {
1073
1086
  CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id);
1074
1087
 
1075
1088
  INSERT OR IGNORE INTO _migrations (id) VALUES (41);
1089
+ `,
1090
+ `
1091
+ CREATE TABLE IF NOT EXISTS project_machine_paths (
1092
+ id TEXT PRIMARY KEY,
1093
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
1094
+ machine_id TEXT NOT NULL,
1095
+ path TEXT NOT NULL,
1096
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1097
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1098
+ UNIQUE(project_id, machine_id)
1099
+ );
1100
+ CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id);
1101
+ CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id);
1102
+ INSERT OR IGNORE INTO _migrations (id) VALUES (42);
1103
+ `,
1104
+ `
1105
+ ALTER TABLE tasks ADD COLUMN archived_at TEXT;
1106
+ CREATE INDEX IF NOT EXISTS idx_tasks_archived ON tasks(archived_at) WHERE archived_at IS NOT NULL;
1107
+ INSERT OR IGNORE INTO _migrations (id) VALUES (43);
1076
1108
  `
1077
1109
  ];
1078
1110
  });
@@ -12544,6 +12576,59 @@ function listProjects(db) {
12544
12576
  const d = db || getDatabase();
12545
12577
  return d.query("SELECT * FROM projects ORDER BY name").all();
12546
12578
  }
12579
+ function updateProject(id, input, db) {
12580
+ const d = db || getDatabase();
12581
+ const project = getProject(id, d);
12582
+ if (!project)
12583
+ throw new ProjectNotFoundError(id);
12584
+ const sets = ["updated_at = ?"];
12585
+ const params = [now()];
12586
+ if (input.name !== undefined) {
12587
+ sets.push("name = ?");
12588
+ params.push(input.name);
12589
+ }
12590
+ if (input.description !== undefined) {
12591
+ sets.push("description = ?");
12592
+ params.push(input.description);
12593
+ }
12594
+ if (input.task_list_id !== undefined) {
12595
+ sets.push("task_list_id = ?");
12596
+ params.push(input.task_list_id);
12597
+ }
12598
+ if (input.path !== undefined) {
12599
+ sets.push("path = ?");
12600
+ params.push(input.path);
12601
+ }
12602
+ params.push(id);
12603
+ d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
12604
+ return getProject(id, d);
12605
+ }
12606
+ function renameProject(id, input, db) {
12607
+ const d = db || getDatabase();
12608
+ const project = getProject(id, d);
12609
+ if (!project)
12610
+ throw new ProjectNotFoundError(id);
12611
+ let taskListsUpdated = 0;
12612
+ const ts = now();
12613
+ if (input.new_slug !== undefined) {
12614
+ const normalised = input.new_slug.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-|-$/g, "");
12615
+ if (!normalised)
12616
+ throw new Error("Invalid slug \u2014 must be non-empty kebab-case");
12617
+ const conflict = d.query("SELECT id FROM projects WHERE task_list_id = ? AND id != ?").get(normalised, id);
12618
+ if (conflict)
12619
+ throw new Error(`Slug "${normalised}" is already used by another project`);
12620
+ const oldSlug = project.task_list_id;
12621
+ d.run("UPDATE projects SET task_list_id = ?, updated_at = ? WHERE id = ?", [normalised, ts, id]);
12622
+ if (oldSlug) {
12623
+ 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]);
12624
+ taskListsUpdated = result.changes;
12625
+ }
12626
+ }
12627
+ if (input.name !== undefined) {
12628
+ d.run("UPDATE projects SET name = ?, updated_at = ? WHERE id = ?", [input.name, ts, id]);
12629
+ }
12630
+ return { project: getProject(id, d), task_lists_updated: taskListsUpdated };
12631
+ }
12547
12632
  function rowToSource(row) {
12548
12633
  return {
12549
12634
  ...row,
@@ -12583,6 +12668,13 @@ function nextTaskShortId(projectId, db) {
12583
12668
  const project = getProject(projectId, d);
12584
12669
  if (!project || !project.task_prefix)
12585
12670
  return null;
12671
+ const prefix = project.task_prefix;
12672
+ const prefixLen = prefix.length + 2;
12673
+ const maxRow = d.query(`SELECT MAX(CAST(SUBSTR(short_id, ?) AS INTEGER)) as max_counter FROM tasks WHERE short_id LIKE ?`).get(prefixLen, `${prefix}-%`);
12674
+ const syncedMax = maxRow?.max_counter ?? 0;
12675
+ if (syncedMax >= (project.task_counter ?? 0)) {
12676
+ d.run("UPDATE projects SET task_counter = ?, updated_at = ? WHERE id = ?", [syncedMax, now(), projectId]);
12677
+ }
12586
12678
  d.run("UPDATE projects SET task_counter = task_counter + 1, updated_at = ? WHERE id = ?", [now(), projectId]);
12587
12679
  const updated = getProject(projectId, d);
12588
12680
  const padded = String(updated.task_counter).padStart(5, "0");
@@ -12591,6 +12683,7 @@ function nextTaskShortId(projectId, db) {
12591
12683
  var init_projects = __esm(() => {
12592
12684
  init_types2();
12593
12685
  init_database();
12686
+ init_machines();
12594
12687
  });
12595
12688
 
12596
12689
  // src/lib/sync-utils.ts
@@ -13553,6 +13646,7 @@ var exports_tasks = {};
13553
13646
  __export(exports_tasks, {
13554
13647
  updateTask: () => updateTask,
13555
13648
  unlockTask: () => unlockTask,
13649
+ unarchiveTask: () => unarchiveTask,
13556
13650
  stealTask: () => stealTask,
13557
13651
  startTask: () => startTask,
13558
13652
  setTaskStatus: () => setTaskStatus,
@@ -13587,6 +13681,7 @@ __export(exports_tasks, {
13587
13681
  claimNextTask: () => claimNextTask,
13588
13682
  bulkUpdateTasks: () => bulkUpdateTasks,
13589
13683
  bulkCreateTasks: () => bulkCreateTasks,
13684
+ archiveTasks: () => archiveTasks,
13590
13685
  addDependency: () => addDependency
13591
13686
  });
13592
13687
  function rowToTask(row) {
@@ -13779,6 +13874,9 @@ function listTasks(filter = {}, db) {
13779
13874
  params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
13780
13875
  } catch {}
13781
13876
  }
13877
+ if (!filter.include_archived) {
13878
+ conditions.push("archived_at IS NULL");
13879
+ }
13782
13880
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
13783
13881
  let limitClause = "";
13784
13882
  if (filter.limit) {
@@ -14692,6 +14790,35 @@ function bulkUpdateTasks(taskIds, updates, db) {
14692
14790
  tx();
14693
14791
  return { updated, failed };
14694
14792
  }
14793
+ function archiveTasks(options, db) {
14794
+ const d = db || getDatabase();
14795
+ const conditions = ["archived_at IS NULL"];
14796
+ const params = [];
14797
+ const statuses = options.status ?? ["completed", "failed", "cancelled"];
14798
+ conditions.push(`status IN (${statuses.map(() => "?").join(",")})`);
14799
+ params.push(...statuses);
14800
+ if (options.project_id) {
14801
+ conditions.push("project_id = ?");
14802
+ params.push(options.project_id);
14803
+ }
14804
+ if (options.task_list_id) {
14805
+ conditions.push("task_list_id = ?");
14806
+ params.push(options.task_list_id);
14807
+ }
14808
+ if (options.older_than_days !== undefined) {
14809
+ const cutoff = new Date(Date.now() - options.older_than_days * 86400000).toISOString();
14810
+ conditions.push("updated_at < ?");
14811
+ params.push(cutoff);
14812
+ }
14813
+ const ts = now();
14814
+ const result = d.run(`UPDATE tasks SET archived_at = ? WHERE ${conditions.join(" AND ")}`, [ts, ...params]);
14815
+ return { archived: result.changes };
14816
+ }
14817
+ function unarchiveTask(id, db) {
14818
+ const d = db || getDatabase();
14819
+ d.run("UPDATE tasks SET archived_at = NULL WHERE id = ?", [id]);
14820
+ return getTask(id, d);
14821
+ }
14695
14822
  function getOverdueTasks(projectId, db) {
14696
14823
  const d = db || getDatabase();
14697
14824
  const nowStr = new Date().toISOString();
@@ -22240,21 +22367,25 @@ function escapeFtsQuery(q) {
22240
22367
  return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
22241
22368
  }
22242
22369
  function searchTasks(options, projectId, taskListId, db) {
22243
- const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
22370
+ const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
22244
22371
  const d = db || getDatabase();
22245
22372
  clearExpiredLocks(d);
22246
22373
  const params = [];
22247
22374
  let sql;
22248
- if (hasFts(d) && opts.query.trim()) {
22249
- const ftsQuery = escapeFtsQuery(opts.query);
22375
+ const raw = opts.query?.trim() ?? "";
22376
+ const q = raw === "*" ? "" : raw;
22377
+ if (hasFts(d) && q) {
22378
+ const ftsQuery = escapeFtsQuery(q);
22250
22379
  sql = `SELECT t.* FROM tasks t
22251
22380
  INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
22252
22381
  WHERE tasks_fts MATCH ?`;
22253
22382
  params.push(ftsQuery);
22254
- } else {
22255
- const pattern = `%${opts.query}%`;
22383
+ } else if (q) {
22384
+ const pattern = `%${q}%`;
22256
22385
  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 ?))`;
22257
22386
  params.push(pattern, pattern, pattern);
22387
+ } else {
22388
+ sql = `SELECT * FROM tasks t WHERE 1=1`;
22258
22389
  }
22259
22390
  if (opts.project_id) {
22260
22391
  sql += " AND t.project_id = ?";
@@ -22308,7 +22439,7 @@ function searchTasks(options, projectId, taskListId, db) {
22308
22439
  } else if (opts.is_blocked === false) {
22309
22440
  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')";
22310
22441
  }
22311
- if (hasFts(d) && opts.query.trim()) {
22442
+ if (hasFts(d) && q) {
22312
22443
  sql += ` ORDER BY bm25(tasks_fts),
22313
22444
  CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
22314
22445
  t.created_at DESC`;
@@ -23497,12 +23628,17 @@ ${text}` }] };
23497
23628
  if (!agent) {
23498
23629
  return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
23499
23630
  }
23631
+ const oldName = agent.name;
23500
23632
  const updated = updateAgent(agent.id, { name: new_name });
23633
+ const db = getDatabase();
23634
+ const tasksResult = db.run("UPDATE tasks SET assigned_to = ? WHERE assigned_to = ?", [new_name, oldName]);
23635
+ const taskNote = tasksResult.changes > 0 ? `
23636
+ Updated assigned_to on ${tasksResult.changes} task(s).` : "";
23501
23637
  return {
23502
23638
  content: [{
23503
23639
  type: "text",
23504
- text: `Agent renamed: ${agent.name} -> ${updated.name}
23505
- ID: ${updated.id}`
23640
+ text: `Agent renamed: ${oldName} -> ${updated.name}
23641
+ ID: ${updated.id}${taskNote}`
23506
23642
  }]
23507
23643
  };
23508
23644
  } catch (e) {
@@ -23819,6 +23955,8 @@ function formatTaskDetail(task, maxDescriptionChars) {
23819
23955
  parts.push(`Project: ${task.project_id}`);
23820
23956
  if (task.plan_id)
23821
23957
  parts.push(`Plan: ${task.plan_id}`);
23958
+ if (task.due_at)
23959
+ parts.push(`Due: ${task.due_at.slice(0, 10)}`);
23822
23960
  if (task.tags.length > 0)
23823
23961
  parts.push(`Tags: ${task.tags.join(", ")}`);
23824
23962
  if (task.recurrence_rule)
@@ -23855,7 +23993,8 @@ if (shouldRegisterTool("create_task")) {
23855
23993
  reason: exports_external.string().optional().describe("Why this task exists \u2014 context for agents picking it up"),
23856
23994
  spawned_from_session: exports_external.string().optional().describe("Session ID that created this task (for tracing task lineage)"),
23857
23995
  assigned_from_project: exports_external.string().optional().describe("Override: project ID the assigning agent is working from. Auto-detected from agent focus if omitted."),
23858
- task_type: exports_external.string().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or any custom string")
23996
+ task_type: exports_external.string().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or any custom string"),
23997
+ 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.")
23859
23998
  }, async (params) => {
23860
23999
  try {
23861
24000
  if (!params.agent_id) {
@@ -23870,6 +24009,10 @@ if (shouldRegisterTool("create_task")) {
23870
24009
  resolved["plan_id"] = resolveId(resolved["plan_id"], "plans");
23871
24010
  if (resolved["task_list_id"])
23872
24011
  resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
24012
+ if (resolved["due_date"]) {
24013
+ resolved["due_at"] = `${resolved["due_date"]}T23:59:59.000Z`;
24014
+ delete resolved["due_date"];
24015
+ }
23873
24016
  if (!resolved["assigned_from_project"]) {
23874
24017
  const focus = getAgentFocus(params.agent_id);
23875
24018
  if (focus?.project_id) {
@@ -23905,7 +24048,8 @@ if (shouldRegisterTool("list_tasks")) {
23905
24048
  limit: exports_external.number().optional(),
23906
24049
  offset: exports_external.number().optional(),
23907
24050
  summary_only: exports_external.boolean().optional().describe("When true, return only id, short_id, title, status, priority \u2014 minimal tokens for navigation"),
23908
- 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.")
24051
+ 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."),
24052
+ include_archived: exports_external.boolean().optional().describe("When true, include archived tasks. Default: false.")
23909
24053
  }, async (params) => {
23910
24054
  try {
23911
24055
  const { due_today, overdue, summary_only, ...rest } = params;
@@ -23940,7 +24084,8 @@ if (shouldRegisterTool("list_tasks")) {
23940
24084
  const assigned = t.assigned_to ? ` -> ${t.assigned_to}` : "";
23941
24085
  const due = t.due_at ? ` due:${t.due_at.slice(0, 10)}` : "";
23942
24086
  const recur = t.recurrence_rule ? " [\u21BB]" : "";
23943
- return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${lock}${due}${recur}`;
24087
+ const list = t.task_list_id ? ` list:${t.task_list_id.slice(0, 8)}` : "";
24088
+ return `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}${assigned}${list}${lock}${due}${recur}`;
23944
24089
  }).join(`
23945
24090
  `);
23946
24091
  const currentOffset = resolved.offset || 0;
@@ -24027,7 +24172,7 @@ Checklist (${done}/${task.checklist.length}):`);
24027
24172
  if (shouldRegisterTool("update_task")) {
24028
24173
  server.tool("update_task", "Update task fields. Version required for optimistic locking.", {
24029
24174
  id: exports_external.string(),
24030
- version: exports_external.number(),
24175
+ version: exports_external.union([exports_external.number(), exports_external.string()]).transform((v) => Number(v)),
24031
24176
  title: exports_external.string().optional(),
24032
24177
  description: exports_external.string().optional(),
24033
24178
  status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
@@ -24037,7 +24182,8 @@ if (shouldRegisterTool("update_task")) {
24037
24182
  metadata: exports_external.record(exports_external.unknown()).optional(),
24038
24183
  plan_id: exports_external.string().optional(),
24039
24184
  task_list_id: exports_external.string().optional(),
24040
- task_type: exports_external.string().nullable().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or custom. null to clear.")
24185
+ task_type: exports_external.string().nullable().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or custom. null to clear."),
24186
+ due_date: exports_external.string().nullable().optional().describe("Due date as YYYY-MM-DD. null to clear. Stored as end-of-day ISO timestamp.")
24041
24187
  }, async ({ id, ...rest }) => {
24042
24188
  try {
24043
24189
  const resolvedId = resolveId(id);
@@ -24046,6 +24192,10 @@ if (shouldRegisterTool("update_task")) {
24046
24192
  resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
24047
24193
  if (resolved.plan_id)
24048
24194
  resolved.plan_id = resolveId(resolved.plan_id, "plans");
24195
+ if ("due_date" in resolved) {
24196
+ resolved["due_at"] = resolved["due_date"] ? `${resolved["due_date"]}T23:59:59.000Z` : null;
24197
+ delete resolved["due_date"];
24198
+ }
24049
24199
  const task = updateTask(resolvedId, resolved);
24050
24200
  return { content: [{ type: "text", text: `updated: ${formatTask(task)}` }] };
24051
24201
  } catch (e) {
@@ -24281,6 +24431,29 @@ if (shouldRegisterTool("add_project_source")) {
24281
24431
  }
24282
24432
  });
24283
24433
  }
24434
+ if (shouldRegisterTool("update_project")) {
24435
+ 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).", {
24436
+ id: exports_external.string().describe("Project ID"),
24437
+ name: exports_external.string().optional(),
24438
+ description: exports_external.string().optional(),
24439
+ path: exports_external.string().optional().describe("Update global path (use projects-path set for machine-local overrides)"),
24440
+ task_list_id: exports_external.string().optional(),
24441
+ new_slug: exports_external.string().optional().describe("Rename the project slug (kebab-case). Cascades to matching task_list slugs.")
24442
+ }, async ({ id, name, description, path, task_list_id, new_slug }) => {
24443
+ try {
24444
+ const resolvedId = resolveId(id, "projects");
24445
+ if (new_slug || name && !description && !path && !task_list_id) {
24446
+ const result = renameProject(resolvedId, { name, new_slug });
24447
+ const note = result.task_lists_updated > 0 ? ` (${result.task_lists_updated} task list(s) slug updated)` : "";
24448
+ return { content: [{ type: "text", text: `Project updated: ${result.project.id.slice(0, 8)} | ${result.project.name}${note}` }] };
24449
+ }
24450
+ const updated = updateProject(resolvedId, { name, description, path, task_list_id });
24451
+ return { content: [{ type: "text", text: `Project updated: ${updated.id.slice(0, 8)} | ${updated.name}${updated.path ? ` (${updated.path})` : ""}` }] };
24452
+ } catch (e) {
24453
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
24454
+ }
24455
+ });
24456
+ }
24284
24457
  if (shouldRegisterTool("remove_project_source")) {
24285
24458
  server.tool("remove_project_source", "Remove a data source from a project by source ID.", {
24286
24459
  source_id: exports_external.string().describe("Source ID to remove")
@@ -24544,8 +24717,8 @@ if (shouldRegisterTool("delete_plan")) {
24544
24717
  });
24545
24718
  }
24546
24719
  if (shouldRegisterTool("search_tasks")) {
24547
- server.tool("search_tasks", "Full-text search across tasks with filters.", {
24548
- query: exports_external.string(),
24720
+ server.tool("search_tasks", "Full-text search across tasks with filters. query is optional \u2014 if omitted, returns all tasks matching the given filters.", {
24721
+ query: exports_external.string().optional(),
24549
24722
  project_id: exports_external.string().optional(),
24550
24723
  task_list_id: exports_external.string().optional(),
24551
24724
  status: exports_external.union([
@@ -24573,11 +24746,12 @@ if (shouldRegisterTool("search_tasks")) {
24573
24746
  ...filters
24574
24747
  });
24575
24748
  if (tasks.length === 0) {
24576
- return { content: [{ type: "text", text: `No tasks matching "${query}".` }] };
24749
+ return { content: [{ type: "text", text: query ? `No tasks matching "${query}".` : "No tasks found." }] };
24577
24750
  }
24578
24751
  const text = tasks.map((t) => `[${t.status}] ${t.id.slice(0, 8)} | ${t.priority} | ${t.title}`).join(`
24579
24752
  `);
24580
- return { content: [{ type: "text", text: `${tasks.length} result(s) for "${query}":
24753
+ const label = query ? `${tasks.length} result(s) for "${query}"` : `${tasks.length} task(s)`;
24754
+ return { content: [{ type: "text", text: `${label}:
24581
24755
  ${text}` }] };
24582
24756
  } catch (e) {
24583
24757
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -24673,9 +24847,15 @@ if (shouldRegisterTool("list_task_lists")) {
24673
24847
  if (lists.length === 0) {
24674
24848
  return { content: [{ type: "text", text: "No task lists found." }] };
24675
24849
  }
24850
+ const db = getDatabase();
24676
24851
  const text = lists.map((l) => {
24677
24852
  const project = l.project_id ? ` (project: ${l.project_id.slice(0, 8)})` : "";
24678
- return `${l.id.slice(0, 8)} | ${l.name} [${l.slug}]${project}${l.description ? ` - ${l.description}` : ""}`;
24853
+ const counts = db.query(`SELECT COUNT(*) as total,
24854
+ SUM(CASE WHEN status IN ('pending','in_progress') THEN 1 ELSE 0 END) as active,
24855
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as done
24856
+ FROM tasks WHERE task_list_id = ? AND archived_at IS NULL`).get(l.id);
24857
+ const taskNote = ` [${counts.active} active, ${counts.done} done / ${counts.total} total]`;
24858
+ return `${l.id.slice(0, 8)} | ${l.name} [${l.slug}]${taskNote}${project}${l.description ? ` - ${l.description}` : ""}`;
24679
24859
  }).join(`
24680
24860
  `);
24681
24861
  return { content: [{ type: "text", text: `${lists.length} task list(s):
@@ -25263,6 +25443,52 @@ if (shouldRegisterTool("set_reports_to")) {
25263
25443
  }
25264
25444
  });
25265
25445
  }
25446
+ if (shouldRegisterTool("archive_completed")) {
25447
+ server.tool("archive_completed", "Archive completed/failed/cancelled tasks to reduce clutter. Archived tasks are hidden from list_tasks and search_tasks by default.", {
25448
+ project_id: exports_external.string().optional().describe("Scope to a project"),
25449
+ task_list_id: exports_external.string().optional().describe("Scope to a task list"),
25450
+ older_than_days: exports_external.number().optional().describe("Only archive tasks last updated more than N days ago. Default: all matching tasks."),
25451
+ status: exports_external.array(exports_external.enum(["completed", "failed", "cancelled"])).optional().describe("Statuses to archive. Default: completed, failed, cancelled."),
25452
+ dry_run: exports_external.boolean().optional().describe("Preview count without archiving. Default: false.")
25453
+ }, async ({ project_id, task_list_id, older_than_days, status, dry_run }) => {
25454
+ try {
25455
+ const filters = {};
25456
+ if (project_id)
25457
+ filters.project_id = resolveId(project_id, "projects");
25458
+ if (task_list_id)
25459
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
25460
+ if (older_than_days !== undefined)
25461
+ filters.older_than_days = older_than_days;
25462
+ if (status)
25463
+ filters.status = status;
25464
+ if (dry_run) {
25465
+ const db = getDatabase();
25466
+ const statuses = status ?? ["completed", "failed", "cancelled"];
25467
+ const conditions = ["archived_at IS NULL", `status IN (${statuses.map(() => "?").join(",")})`];
25468
+ const params = [...statuses];
25469
+ if (filters.project_id) {
25470
+ conditions.push("project_id = ?");
25471
+ params.push(filters.project_id);
25472
+ }
25473
+ if (filters.task_list_id) {
25474
+ conditions.push("task_list_id = ?");
25475
+ params.push(filters.task_list_id);
25476
+ }
25477
+ if (older_than_days !== undefined) {
25478
+ const cutoff = new Date(Date.now() - older_than_days * 86400000).toISOString();
25479
+ conditions.push("updated_at < ?");
25480
+ params.push(cutoff);
25481
+ }
25482
+ const count = db.query(`SELECT COUNT(*) as c FROM tasks WHERE ${conditions.join(" AND ")}`).get(...params).c;
25483
+ return { content: [{ type: "text", text: `Dry run: would archive ${count} task(s).` }] };
25484
+ }
25485
+ const result = archiveTasks(filters);
25486
+ return { content: [{ type: "text", text: `Archived ${result.archived} task(s).` }] };
25487
+ } catch (e) {
25488
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
25489
+ }
25490
+ });
25491
+ }
25266
25492
  if (shouldRegisterTool("bulk_update_tasks")) {
25267
25493
  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.", {
25268
25494
  task_ids: exports_external.array(exports_external.string()).optional().describe("Task IDs to update with the same fields (mode 1)"),
@@ -25375,6 +25601,26 @@ if (shouldRegisterTool("get_task_stats")) {
25375
25601
  if (agent_id)
25376
25602
  filters.agent_id = agent_id;
25377
25603
  const stats = getTaskStats(Object.keys(filters).length > 0 ? filters : undefined);
25604
+ if (!task_list_id) {
25605
+ const db = getDatabase();
25606
+ const listRows = db.query(`SELECT tl.id, tl.name, tl.slug,
25607
+ COUNT(t.id) as total,
25608
+ SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as completed,
25609
+ SUM(CASE WHEN t.status IN ('pending','in_progress') THEN 1 ELSE 0 END) as active
25610
+ FROM task_lists tl
25611
+ LEFT JOIN tasks t ON t.task_list_id = tl.id ${filters.project_id ? "AND t.project_id = ?" : ""}
25612
+ ${filters.project_id ? "WHERE tl.project_id = ?" : ""}
25613
+ GROUP BY tl.id ORDER BY tl.name`).all(...filters.project_id ? [filters.project_id, filters.project_id] : []);
25614
+ stats.by_task_list = listRows.map((r) => ({
25615
+ id: r.id.slice(0, 8),
25616
+ name: r.name,
25617
+ slug: r.slug,
25618
+ total: r.total,
25619
+ completed: r.completed,
25620
+ active: r.active,
25621
+ completion_rate: r.total > 0 ? Math.round(r.completed / r.total * 100) : 0
25622
+ }));
25623
+ }
25378
25624
  return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
25379
25625
  } catch (e) {
25380
25626
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/agents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAMzE,UAAU,UAAU;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,KAAK,OAAO,GAAG;IACb,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;IACpC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;CAC5D,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,EAAE,OAAO,GAAG,IAAI,CA0ZjJ"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/agents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAMzE,UAAU,UAAU;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,KAAK,OAAO,GAAG;IACb,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;IACpC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;CAC5D,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,EAAE,OAAO,GAAG,IAAI,CAuajJ"}