@hasna/todos 0.11.92 → 0.11.94

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
@@ -2104,7 +2104,7 @@ var package_default;
2104
2104
  var init_package = __esm(() => {
2105
2105
  package_default = {
2106
2106
  name: "@hasna/todos",
2107
- version: "0.11.92",
2107
+ version: "0.11.94",
2108
2108
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
2109
2109
  type: "module",
2110
2110
  main: "dist/index.js",
@@ -2786,205 +2786,25 @@ async function cloudListTasks(client, filter = {}) {
2786
2786
  const envelope = res.raw;
2787
2787
  return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
2788
2788
  }
2789
- async function cloudListTaskPage(client, filter) {
2790
- const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
2791
- const envelope = res.raw;
2792
- if (envelope && Object.prototype.hasOwnProperty.call(envelope, "tasks") && !Array.isArray(envelope.tasks)) {
2793
- throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks returned a malformed tasks page; local SQLite fallback is disabled");
2794
- }
2795
- const tasks = Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
2796
- if (!Array.isArray(tasks) || tasks.some((task) => !task || typeof task !== "object" || typeof task.id !== "string" || !task.id)) {
2797
- throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks returned a malformed tasks page; local SQLite fallback is disabled");
2798
- }
2799
- const count = typeof envelope?.count === "number" ? envelope.count : tasks.length;
2800
- if (!Number.isSafeInteger(count) || count < 0 || count !== tasks.length) {
2801
- throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks returned an invalid count; local SQLite fallback is disabled");
2802
- }
2803
- if (envelope?.total !== undefined && (!Number.isSafeInteger(envelope.total) || envelope.total < 0 || envelope.total < count)) {
2804
- throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks returned an invalid total; local SQLite fallback is disabled");
2805
- }
2806
- return { tasks, count, total: envelope?.total };
2807
- }
2808
- async function cloudTasksAllCount(client) {
2809
- const stats = await cloudGetStats(client);
2810
- if (!Number.isSafeInteger(stats.tasks_all) || stats.tasks_all < 0) {
2811
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/stats must return a non-negative integer tasks_all for short task references");
2812
- }
2813
- return stats.tasks_all;
2814
- }
2815
- async function cloudPaginateTaskScope(client, filter, maximumTotal) {
2816
- const pageSize = 500;
2817
- const tasks = [];
2818
- const ids = new Set;
2819
- let reportedTotal;
2820
- let totalPresence;
2821
- for (let offset = 0;; ) {
2822
- let page;
2823
- try {
2824
- page = await cloudListTaskPage(client, { ...filter, limit: pageSize, offset });
2825
- } catch (error) {
2826
- const message = error instanceof Error ? error.message : String(error);
2827
- if (!message.startsWith("REMOTE_API_INCOMPATIBLE") && !message.startsWith("REMOTE_API_CHANGED_DURING_PAGINATION")) {
2828
- throw error;
2829
- }
2830
- throw new RemoteTaskReferenceSnapshotError(message, { cause: error });
2831
- }
2832
- const pageHasTotal = page.total !== undefined;
2833
- if (totalPresence !== undefined && totalPresence !== pageHasTotal) {
2834
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/tasks changed total support during pagination");
2835
- }
2836
- totalPresence = pageHasTotal;
2837
- if (page.total !== undefined && reportedTotal !== undefined && page.total !== reportedTotal) {
2838
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_CHANGED_DURING_PAGINATION: /v1/tasks total changed during pagination");
2839
- }
2840
- if (page.total !== undefined)
2841
- reportedTotal = page.total;
2842
- if (tasks.length + page.tasks.length > maximumTotal || reportedTotal !== undefined && tasks.length + page.tasks.length > reportedTotal) {
2843
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/tasks pagination exceeded total; local SQLite fallback is disabled");
2844
- }
2845
- for (const task of page.tasks) {
2846
- if (ids.has(task.id)) {
2847
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/tasks pagination returned a duplicate task id");
2848
- }
2849
- ids.add(task.id);
2850
- tasks.push(task);
2851
- }
2852
- if (reportedTotal !== undefined && tasks.length === reportedTotal)
2853
- break;
2854
- if (reportedTotal !== undefined && page.tasks.length === 0) {
2855
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/tasks pagination ended before total; local SQLite fallback is disabled");
2856
- }
2857
- if (reportedTotal === undefined && page.tasks.length < pageSize)
2858
- break;
2859
- offset += page.tasks.length;
2860
- }
2861
- return { tasks, total: reportedTotal };
2862
- }
2863
- function taskParentId(task) {
2864
- return typeof task.parent_id === "string" && task.parent_id ? task.parent_id : null;
2865
- }
2866
- function assertCompleteTaskHierarchy(tasks) {
2867
- const byId = new Map(tasks.map((task) => [task.id, task]));
2868
- for (const task of tasks) {
2869
- const visited = new Set([task.id]);
2870
- let parentId = taskParentId(task);
2871
- while (parentId) {
2872
- if (visited.has(parentId)) {
2873
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/tasks returned a cyclic task hierarchy");
2874
- }
2875
- visited.add(parentId);
2876
- const parent = byId.get(parentId);
2877
- if (!parent) {
2878
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/tasks returned an orphaned task hierarchy");
2879
- }
2880
- parentId = taskParentId(parent);
2881
- }
2882
- }
2883
- }
2884
- async function cloudListTaskHierarchyForResolution(client, expectedTotal) {
2885
- const roots = await cloudPaginateTaskScope(client, {}, expectedTotal);
2886
- for (const root of roots.tasks) {
2887
- if (taskParentId(root) !== null) {
2888
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: default /v1/tasks returned a non-root task");
2889
- }
2890
- }
2891
- const tasks = [...roots.tasks];
2892
- const seen = new Set(tasks.map((task) => task.id));
2893
- const queue = roots.tasks.map((task) => ({ task, ancestors: new Set([task.id]) }));
2894
- for (let index = 0;index < queue.length; index += 1) {
2895
- const current = queue[index];
2896
- const children = await cloudPaginateTaskScope(client, { parent_id: current.task.id }, expectedTotal);
2897
- for (const child of children.tasks) {
2898
- if (taskParentId(child) !== current.task.id) {
2899
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/tasks parent filter returned an orphaned task");
2900
- }
2901
- if (current.ancestors.has(child.id)) {
2902
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/tasks returned a cyclic task hierarchy");
2903
- }
2904
- if (seen.has(child.id)) {
2905
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/tasks hierarchy returned a duplicate task id");
2906
- }
2907
- seen.add(child.id);
2908
- tasks.push(child);
2909
- if (tasks.length > expectedTotal) {
2910
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/tasks hierarchy exceeded /v1/stats tasks_all");
2911
- }
2912
- queue.push({ task: child, ancestors: new Set([...current.ancestors, child.id]) });
2913
- }
2914
- }
2915
- if (tasks.length !== expectedTotal) {
2916
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_INCOMPATIBLE: /v1/tasks hierarchy did not account for every /v1/stats tasks_all row");
2917
- }
2918
- assertCompleteTaskHierarchy(tasks);
2919
- return tasks;
2920
- }
2921
- async function cloudListAllTasksForResolution(client, expectedTotal) {
2922
- const inclusive = await cloudPaginateTaskScope(client, { include_subtasks: true }, expectedTotal);
2923
- if (inclusive.tasks.length === expectedTotal && (inclusive.total === undefined || inclusive.total === expectedTotal)) {
2924
- assertCompleteTaskHierarchy(inclusive.tasks);
2925
- return inclusive.tasks;
2926
- }
2927
- return cloudListTaskHierarchyForResolution(client, expectedTotal);
2928
- }
2929
- async function resolveTaskRefSnapshot(client, ref, input) {
2930
- const beforeTotal = await cloudTasksAllCount(client);
2931
- let tasks;
2932
- try {
2933
- tasks = await cloudListAllTasksForResolution(client, beforeTotal);
2934
- } catch (error) {
2935
- const afterFailureTotal = await cloudTasksAllCount(client);
2936
- if (afterFailureTotal !== beforeTotal) {
2937
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_CHANGED_DURING_PAGINATION: tasks_all changed during task resolution", { cause: error });
2938
- }
2939
- throw error;
2940
- }
2941
- const exactShort = tasks.filter((task) => task.short_id?.toLowerCase() === input);
2942
- const prefixes = exactShort.length === 0 ? tasks.filter((task) => task.id.toLowerCase().startsWith(input)) : [];
2943
- const matches = exactShort.length > 0 ? exactShort : prefixes;
2944
- if (matches.length > 1) {
2945
- const afterTotal2 = await cloudTasksAllCount(client);
2946
- if (afterTotal2 !== beforeTotal) {
2947
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_CHANGED_DURING_PAGINATION: tasks_all changed during task resolution");
2948
- }
2949
- throw new Error(`Task reference is ambiguous: "${ref}"`);
2950
- }
2951
- if (matches.length === 0) {
2952
- const afterTotal2 = await cloudTasksAllCount(client);
2953
- if (afterTotal2 !== beforeTotal) {
2954
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_CHANGED_DURING_PAGINATION: tasks_all changed during task resolution");
2955
- }
2956
- throw new Error(`Task not found: ${ref}`);
2957
- }
2958
- const candidate = matches[0];
2959
- const finalTask = await cloudGetTask(client, candidate.id);
2960
- const finalMatches = finalTask?.id === candidate.id && (exactShort.length === 1 ? finalTask.short_id?.toLowerCase() === input : finalTask.id.toLowerCase().startsWith(input));
2961
- if (!finalMatches) {
2962
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_CHANGED_DURING_PAGINATION: resolved task no longer matches the short reference");
2963
- }
2964
- const afterTotal = await cloudTasksAllCount(client);
2965
- if (afterTotal !== beforeTotal) {
2966
- throw new RemoteTaskReferenceSnapshotError("REMOTE_API_CHANGED_DURING_PAGINATION: tasks_all changed during task resolution");
2967
- }
2968
- return candidate.id;
2969
- }
2970
2789
  async function cloudResolveTaskRef(client, ref) {
2971
2790
  const input = ref.trim().toLowerCase();
2972
2791
  if (!input)
2973
2792
  throw new Error("Task reference must not be empty");
2974
2793
  if (UUID_RE.test(input))
2975
2794
  return input;
2976
- for (let attempt = 0;attempt < 2; attempt += 1) {
2977
- try {
2978
- return await resolveTaskRefSnapshot(client, ref, input);
2979
- } catch (error) {
2980
- if (!(error instanceof RemoteTaskReferenceSnapshotError))
2981
- throw error;
2982
- if (attempt === 1) {
2983
- throw new Error(`REMOTE_TASK_REFERENCE_UNSAFE: could not prove a stable complete task set for "${ref}"; ` + "retry with the full task UUID; no local fallback was attempted", { cause: error });
2984
- }
2985
- }
2795
+ let task;
2796
+ try {
2797
+ task = await cloudGetTask(client, input);
2798
+ } catch (error) {
2799
+ const status = error && typeof error === "object" ? error.status : undefined;
2800
+ if (status === 409)
2801
+ throw new Error(`Task reference is ambiguous: "${ref}"`);
2802
+ throw error;
2803
+ }
2804
+ if (task && typeof task.id === "string" && (task.short_id?.toLowerCase() === input || task.id.toLowerCase().startsWith(input))) {
2805
+ return task.id;
2986
2806
  }
2987
- throw new Error(`REMOTE_TASK_REFERENCE_UNSAFE: use the full task UUID for "${ref}"`);
2807
+ throw new Error(`Task not found: ${ref}`);
2988
2808
  }
2989
2809
  async function cloudGetTask(client, id) {
2990
2810
  const raw = await client.get("tasks", id);
@@ -3660,7 +3480,7 @@ async function cloudTimeline(client, options = {}) {
3660
3480
  const limit = options.limit ?? 50;
3661
3481
  return { entries: entries.slice(offset, offset + limit), total, limit, offset };
3662
3482
  }
3663
- var UUID_RE, CLOUD_MODES, VALID_STORAGE_MODES, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, RemoteTaskReferenceSnapshotError, PRIORITY_RANK;
3483
+ var UUID_RE, CLOUD_MODES, VALID_STORAGE_MODES, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, PRIORITY_RANK;
3664
3484
  var init_cloud_router = __esm(() => {
3665
3485
  init_redaction();
3666
3486
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -3675,8 +3495,6 @@ var init_cloud_router = __esm(() => {
3675
3495
  "confidence"
3676
3496
  ];
3677
3497
  completionCapabilityCache = new Map;
3678
- RemoteTaskReferenceSnapshotError = class RemoteTaskReferenceSnapshotError extends Error {
3679
- };
3680
3498
  PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
3681
3499
  });
3682
3500
 
@@ -15209,6 +15027,46 @@ function resolveTaskListRef(ref, projectId) {
15209
15027
  return { id: bySlug.id };
15210
15028
  return { error: `Could not resolve task list "${ref}" to a UUID${projectId ? " within the task's project" : ""}. Pass an exact task-list UUID.` };
15211
15029
  }
15030
+ async function computeCloudReparent(cloud, current, opts) {
15031
+ const targetProjectId = opts.projectRef ? await cloudResolveProjectRef(cloud, opts.projectRef) : undefined;
15032
+ const scope = targetProjectId ?? current.project_id ?? undefined;
15033
+ let taskListId;
15034
+ if (opts.listRef)
15035
+ taskListId = await cloudResolveTaskListRef(cloud, opts.listRef, scope);
15036
+ else if (opts.clearList)
15037
+ taskListId = null;
15038
+ else if (targetProjectId && targetProjectId !== current.project_id)
15039
+ taskListId = null;
15040
+ const patch = {};
15041
+ if (targetProjectId !== undefined)
15042
+ patch.project_id = targetProjectId;
15043
+ if (taskListId !== undefined)
15044
+ patch.task_list_id = taskListId;
15045
+ return patch;
15046
+ }
15047
+ function computeLocalReparent(current, opts) {
15048
+ const targetProjectId = opts.projectRef ? resolveProjectIdOrSlug(opts.projectRef) : undefined;
15049
+ const scope = targetProjectId ?? current.project_id ?? null;
15050
+ let taskListId;
15051
+ if (opts.listRef) {
15052
+ const resolved = resolveTaskListRef(opts.listRef, scope);
15053
+ if ("error" in resolved) {
15054
+ console.error(chalk2.red(resolved.error));
15055
+ process.exit(1);
15056
+ }
15057
+ taskListId = resolved.id;
15058
+ } else if (opts.clearList) {
15059
+ taskListId = null;
15060
+ } else if (targetProjectId && targetProjectId !== current.project_id) {
15061
+ taskListId = null;
15062
+ }
15063
+ const patch = {};
15064
+ if (targetProjectId !== undefined)
15065
+ patch.project_id = targetProjectId;
15066
+ if (taskListId !== undefined)
15067
+ patch.task_list_id = taskListId;
15068
+ return patch;
15069
+ }
15212
15070
  function registerTaskCommands(program2) {
15213
15071
  program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--plan <id>", "Assign to a plan").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--approval", "Require approval before completion").option("--recurrence <rule>", "Recurrence rule, e.g. 'every day', 'every weekday', 'every 2 weeks'").option("--due <date>", "Due date (ISO string or YYYY-MM-DD)").option("--reason <text>", "Why this task exists").option("--project <id>", "Assign to project by ID or slug (overrides auto-detect)").action(async (title, opts) => {
15214
15072
  const globalOpts = program2.opts();
@@ -15913,7 +15771,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
15913
15771
  console.log(` ${chalk2.dim(h.created_at)} ${chalk2.bold(h.action)}${field}${change}${agent}`);
15914
15772
  }
15915
15773
  });
15916
- program2.command("update <id>").description("Update a task").option("--title <text>", "New title").option("-d, --description <text>", "New description").option("-s, --status <status>", "New status").option("-p, --priority <priority>", "New priority").option("--assign <agent>", "Assign to agent").option("--tags <tags>", "New tags (comma-separated)").option("--tag <tags>", "New tags (alias for --tags)").option("--list <id>", "Move to a task list (UUID authoritative; project-scoped slug accepted)").option("--task-list <id>", "Move to a task list (alias for --list)").option("--clear-list", "Detach from its task list (reset task_list_id to null)").option("--working-dir <path>", "Repair the task's working_dir to a specific path (routing metadata)").option("--clear-working-dir", "Reset the task's working_dir to null (undo path for routing repairs)").option("--plan <id>", "Move to a plan").option("--clear-plan", "Remove from its current plan").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--due <date>", "Due date (ISO string or YYYY-MM-DD), empty to clear").option("--recurrence <rule>", "Recurrence rule, empty to clear").option("--approval", "Require approval before completion").option("--clear-approval", "Remove the approval requirement").action(async (id, opts) => {
15774
+ program2.command("update <id>").description("Update a task").option("--title <text>", "New title").option("-d, --description <text>", "New description").option("-s, --status <status>", "New status").option("-p, --priority <priority>", "New priority").option("--assign <agent>", "Assign to agent").option("--tags <tags>", "New tags (comma-separated)").option("--tag <tags>", "New tags (alias for --tags)").option("--list <id>", "Move to a task list (UUID authoritative; project-scoped slug accepted)").option("--task-list <id>", "Move to a task list (alias for --list)").option("--clear-list", "Detach from its task list (reset task_list_id to null)").option("--project <id>", "Re-parent the task to another project (by ID, slug, or path); see also `todos move`").option("--working-dir <path>", "Repair the task's working_dir to a specific path (routing metadata)").option("--clear-working-dir", "Reset the task's working_dir to null (undo path for routing repairs)").option("--plan <id>", "Move to a plan").option("--clear-plan", "Remove from its current plan").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--due <date>", "Due date (ISO string or YYYY-MM-DD), empty to clear").option("--recurrence <rule>", "Recurrence rule, empty to clear").option("--approval", "Require approval before completion").option("--clear-approval", "Remove the approval requirement").action(async (id, opts) => {
15917
15775
  const globalOpts = program2.opts();
15918
15776
  opts.tags = opts.tags || opts.tag;
15919
15777
  opts.list = opts.list || opts.taskList;
@@ -15940,7 +15798,11 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
15940
15798
  const plan = opts.plan ? await cloudResolvePlan(cloud, opts.plan, current2.project_id ?? undefined) : null;
15941
15799
  if (opts.plan && !plan)
15942
15800
  throw new Error(`Plan not found: ${opts.plan}`);
15943
- const taskListId2 = opts.list ? await cloudResolveTaskListRef(cloud, opts.list, current2.project_id ?? undefined) : opts.clearList ? null : undefined;
15801
+ const reparent2 = await computeCloudReparent(cloud, current2, {
15802
+ projectRef: opts.project || globalOpts.project,
15803
+ listRef: opts.list,
15804
+ clearList: opts.clearList
15805
+ });
15944
15806
  task3 = await cloudUpdateTask(cloud, currentId, {
15945
15807
  title: opts.title,
15946
15808
  description: opts.description,
@@ -15949,7 +15811,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
15949
15811
  assigned_to: opts.assign,
15950
15812
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
15951
15813
  plan_id: plan?.id ?? (opts.clearPlan ? null : undefined),
15952
- task_list_id: taskListId2,
15814
+ ...reparent2,
15953
15815
  working_dir: opts.workingDir ? resolve9(opts.workingDir) : opts.clearWorkingDir ? null : undefined,
15954
15816
  estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
15955
15817
  sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
@@ -15974,14 +15836,11 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
15974
15836
  console.error(chalk2.red(`Task not found: ${id}`));
15975
15837
  process.exit(1);
15976
15838
  }
15977
- const taskListId = opts.list ? (() => {
15978
- const resolved = resolveTaskListRef(opts.list, current.project_id);
15979
- if ("error" in resolved) {
15980
- console.error(chalk2.red(resolved.error));
15981
- process.exit(1);
15982
- }
15983
- return resolved.id;
15984
- })() : opts.clearList ? null : undefined;
15839
+ const reparent = computeLocalReparent(current, {
15840
+ projectRef: opts.project || globalOpts.project,
15841
+ listRef: opts.list,
15842
+ clearList: opts.clearList
15843
+ });
15985
15844
  const planId = opts.plan ? resolvePlanId(opts.plan) : opts.clearPlan ? null : undefined;
15986
15845
  let task2;
15987
15846
  try {
@@ -15994,7 +15853,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
15994
15853
  assigned_to: opts.assign,
15995
15854
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
15996
15855
  plan_id: planId,
15997
- task_list_id: taskListId,
15856
+ ...reparent,
15998
15857
  working_dir: opts.workingDir ? resolve9(opts.workingDir) : opts.clearWorkingDir ? null : undefined,
15999
15858
  estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
16000
15859
  sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
@@ -16012,6 +15871,68 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
16012
15871
  console.log(formatTaskLine(task2));
16013
15872
  }
16014
15873
  });
15874
+ program2.command("move <id>").description("Move a task to another project and/or task list (keeps its id and history)").option("--to-project <id>", "Destination project (by ID, slug, or path)").option("--to-list <id>", "Destination task list (UUID authoritative; slug resolved in the destination project)").option("--clear-list", "Detach from its task list (reset task_list_id to null)").action(async (id, opts) => {
15875
+ const globalOpts = program2.opts();
15876
+ const projectRef = opts.toProject ?? globalOpts.project;
15877
+ const listRef = opts.toList;
15878
+ if (!projectRef && !listRef && !opts.clearList) {
15879
+ handleError(new Error("Nothing to move: pass --to-project, --to-list, or --clear-list."));
15880
+ }
15881
+ if (listRef && opts.clearList) {
15882
+ handleError(new Error("Use either --to-list or --clear-list, not both."));
15883
+ }
15884
+ const cloud = getTodosCloudClient();
15885
+ if (cloud) {
15886
+ let task3;
15887
+ try {
15888
+ const currentId = await resolveTaskIdForCommand(id, cloud);
15889
+ const current2 = await cloudGetTask(cloud, currentId);
15890
+ if (!current2)
15891
+ throw new Error(`Task not found: ${id}`);
15892
+ const reparent2 = await computeCloudReparent(cloud, current2, {
15893
+ projectRef,
15894
+ listRef,
15895
+ clearList: opts.clearList
15896
+ });
15897
+ if (reparent2.project_id === undefined && reparent2.task_list_id === undefined) {
15898
+ throw new Error("Nothing to move: the task is already in the requested project/list.");
15899
+ }
15900
+ task3 = await cloudUpdateTask(cloud, currentId, reparent2);
15901
+ } catch (e) {
15902
+ handleError(e);
15903
+ }
15904
+ if (globalOpts.json) {
15905
+ output(task3, true);
15906
+ } else {
15907
+ console.log(chalk2.green("Task moved:"));
15908
+ console.log(formatTaskLine(task3));
15909
+ }
15910
+ return;
15911
+ }
15912
+ const resolvedId = resolveTaskId(id);
15913
+ const current = getTask(resolvedId);
15914
+ if (!current) {
15915
+ console.error(chalk2.red(`Task not found: ${id}`));
15916
+ process.exit(1);
15917
+ }
15918
+ const reparent = computeLocalReparent(current, { projectRef, listRef, clearList: opts.clearList });
15919
+ if (reparent.project_id === undefined && reparent.task_list_id === undefined) {
15920
+ console.error(chalk2.red("Nothing to move: the task is already in the requested project/list."));
15921
+ process.exit(1);
15922
+ }
15923
+ let task2;
15924
+ try {
15925
+ task2 = updateTask(resolvedId, { version: current.version, ...reparent });
15926
+ } catch (e) {
15927
+ handleError(e);
15928
+ }
15929
+ if (globalOpts.json) {
15930
+ output(task2, true);
15931
+ } else {
15932
+ console.log(chalk2.green("Task moved:"));
15933
+ console.log(formatTaskLine(task2));
15934
+ }
15935
+ });
16015
15936
  program2.command("done <id>").description("Mark a task as completed").option("--attach-ids <ids>", "Comma-separated @hasna/attachments IDs to link as evidence").option("--files-changed <files>", "Comma-separated list of files changed").option("--test-results <results>", "Test results summary").option("--commit-hash <hash>", "Git commit hash").option("--notes <notes>", "Completion notes").option("--confidence <0-1>", "Agent's confidence 0.0-1.0 that the task is fully complete (default: 1.0, <0.7 flagged for review)").action(async (id, opts) => {
16016
15937
  const globalOpts = program2.opts();
16017
15938
  const attachmentIds = opts.attachIds ? opts.attachIds.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
@@ -26746,6 +26667,26 @@ var init_sqlite_snapshot = __esm(() => {
26746
26667
  });
26747
26668
 
26748
26669
  // src/storage/local-sqlite.ts
26670
+ function resolveTaskRefLocal(db, ref) {
26671
+ const raw = ref.trim().toLowerCase();
26672
+ if (!raw)
26673
+ return null;
26674
+ if (TASK_UUID_RE2.test(raw))
26675
+ return getTask(raw, db);
26676
+ const prefixRows = db.query("SELECT id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
26677
+ if (prefixRows.length > 1) {
26678
+ throw new Error(`Task reference is ambiguous: "${ref}"`);
26679
+ }
26680
+ if (prefixRows.length === 1)
26681
+ return getTask(prefixRows[0].id, db);
26682
+ const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
26683
+ if (shortIdRows.length > 1) {
26684
+ throw new Error(`Task reference is ambiguous: "${ref}"`);
26685
+ }
26686
+ if (shortIdRows.length === 1)
26687
+ return getTask(shortIdRows[0].id, db);
26688
+ return null;
26689
+ }
26749
26690
  function createLocalSqliteTodosStorageAdapter(options = {}) {
26750
26691
  const database = () => options.db ?? getDatabase();
26751
26692
  let adapter;
@@ -26761,6 +26702,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
26761
26702
  tasks: {
26762
26703
  create: (input) => createTask(input, database()),
26763
26704
  get: (id) => getTask(id, database()),
26705
+ resolveRef: (ref) => resolveTaskRefLocal(database(), ref),
26764
26706
  list: (filter = {}) => listTasks(filter, database()),
26765
26707
  count: (filter = {}) => countTasks(filter, database()),
26766
26708
  update: (id, input) => updateTask(id, input, database()),
@@ -26849,6 +26791,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
26849
26791
  };
26850
26792
  return adapter;
26851
26793
  }
26794
+ var TASK_UUID_RE2;
26852
26795
  var init_local_sqlite = __esm(() => {
26853
26796
  init_tasks();
26854
26797
  init_projects();
@@ -26860,6 +26803,7 @@ var init_local_sqlite = __esm(() => {
26860
26803
  init_comments();
26861
26804
  init_database();
26862
26805
  init_sqlite_snapshot();
26806
+ TASK_UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
26863
26807
  });
26864
26808
 
26865
26809
  // src/storage/postgres-sync.ts
@@ -26982,6 +26926,18 @@ function postgresTodosCommentCursorIndexSql(tableName = DEFAULT_TODOS_POSTGRES_S
26982
26926
  ON ${tableName} (service, (payload->>'task_id'), (payload->>'created_at'), object_id)
26983
26927
  WHERE object_type = 'comments' AND deleted_at IS NULL`;
26984
26928
  }
26929
+ function postgresTodosTaskShortIdIndexSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
26930
+ assertSafeIdentifier(tableName);
26931
+ return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${tableName}_task_short_id_idx
26932
+ ON ${tableName} ((LOWER(payload->>'short_id')))
26933
+ WHERE object_type = 'tasks' AND deleted_at IS NULL`;
26934
+ }
26935
+ function postgresTodosTaskObjectIdIndexSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
26936
+ assertSafeIdentifier(tableName);
26937
+ return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${tableName}_task_object_id_c_idx
26938
+ ON ${tableName} (service, (object_id COLLATE "C"))
26939
+ WHERE object_type = 'tasks' AND deleted_at IS NULL`;
26940
+ }
26985
26941
 
26986
26942
  class PostgresTodosSyncStore {
26987
26943
  client;
@@ -27586,6 +27542,7 @@ function createPostgresTodosStorageAdapter(options) {
27586
27542
  tasks: {
27587
27543
  create: (input, context) => createTask2(input, store, context),
27588
27544
  get: (id) => store.get("tasks", id),
27545
+ resolveRef: (ref) => store.resolveTaskRef(ref),
27589
27546
  list: (filter = {}) => store.listTasks(filter),
27590
27547
  count: (filter = {}) => store.countTasks(filter),
27591
27548
  update: (id, input) => updateTask2(id, input, store),
@@ -27838,6 +27795,37 @@ class PostgresJsonRecordStore {
27838
27795
  const row = result.rows[0];
27839
27796
  return row ? payloadRecord2(row.payload) : null;
27840
27797
  }
27798
+ async resolveTaskRef(ref) {
27799
+ await this.ensureSchema();
27800
+ const raw = ref.trim().toLowerCase();
27801
+ if (!raw)
27802
+ return null;
27803
+ const lastCode = raw.charCodeAt(raw.length - 1);
27804
+ if (lastCode < 65535) {
27805
+ const upper = raw.slice(0, -1) + String.fromCharCode(lastCode + 1);
27806
+ const prefixResult = await this.options.client.query(`/* todos:resolve-task-ref-prefix */ SELECT payload FROM ${this.tableName}
27807
+ WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL
27808
+ AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
27809
+ LIMIT 2`, [this.service, raw, upper]);
27810
+ if (prefixResult.rows.length > 1) {
27811
+ throw new Error(`Task reference is ambiguous: "${ref}"`);
27812
+ }
27813
+ if (prefixResult.rows.length === 1) {
27814
+ return payloadRecord2(prefixResult.rows[0].payload);
27815
+ }
27816
+ }
27817
+ const shortIdResult = await this.options.client.query(`/* todos:resolve-task-ref-short-id */ SELECT payload FROM ${this.tableName}
27818
+ WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL
27819
+ AND LOWER(payload->>'short_id') = $2
27820
+ LIMIT 2`, [this.service, raw]);
27821
+ if (shortIdResult.rows.length > 1) {
27822
+ throw new Error(`Task reference is ambiguous: "${ref}"`);
27823
+ }
27824
+ if (shortIdResult.rows.length === 1) {
27825
+ return payloadRecord2(shortIdResult.rows[0].payload);
27826
+ }
27827
+ return null;
27828
+ }
27841
27829
  async countTasks(filter) {
27842
27830
  await this.ensureSchema();
27843
27831
  const { where, params } = this.buildTaskFilterSql(filter);
@@ -28259,7 +28247,7 @@ async function updateTask2(id, input, store) {
28259
28247
  tags: input.tags ?? existing.tags,
28260
28248
  metadata: input.metadata ?? existing.metadata,
28261
28249
  requires_approval: input.requires_approval ?? existing.requires_approval,
28262
- task_list_id: input.task_list_id ?? existing.task_list_id
28250
+ task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id
28263
28251
  };
28264
28252
  await store.upsert("tasks", task);
28265
28253
  return task;
@@ -29030,6 +29018,8 @@ __export(exports_cloud, {
29030
29018
  getCloudVerifier: () => getCloudVerifier,
29031
29019
  getCloudStorageAdapter: () => getCloudStorageAdapter,
29032
29020
  getApiKeyStore: () => getApiKeyStore,
29021
+ ensureCloudTaskShortIdIndex: () => ensureCloudTaskShortIdIndex,
29022
+ ensureCloudTaskObjectIdIndex: () => ensureCloudTaskObjectIdIndex,
29033
29023
  ensureCloudScopedSlugUniqueIndexes: () => ensureCloudScopedSlugUniqueIndexes,
29034
29024
  ensureCloudSchema: () => ensureCloudSchema,
29035
29025
  ensureCloudCommentCursorIndex: () => ensureCloudCommentCursorIndex,
@@ -29117,6 +29107,12 @@ async function ensureCloudSchema() {
29117
29107
  async function ensureCloudCommentCursorIndex() {
29118
29108
  await getClient().query(postgresTodosCommentCursorIndexSql());
29119
29109
  }
29110
+ async function ensureCloudTaskShortIdIndex() {
29111
+ await getClient().query(postgresTodosTaskShortIdIndexSql());
29112
+ }
29113
+ async function ensureCloudTaskObjectIdIndex() {
29114
+ await getClient().query(postgresTodosTaskObjectIdIndexSql());
29115
+ }
29120
29116
  async function ensureCloudScopedSlugUniqueIndexes() {
29121
29117
  await ensurePostgresScopedSlugUniqueIndexes(getClient());
29122
29118
  }
@@ -29199,6 +29195,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29199
29195
  status: { type: "string" },
29200
29196
  priority: { type: "string" },
29201
29197
  assigned_to: { type: "string" },
29198
+ project_id: { type: "string", nullable: true },
29199
+ task_list_id: { type: "string", nullable: true },
29202
29200
  version: { type: "number" }
29203
29201
  }
29204
29202
  },
@@ -30411,7 +30409,17 @@ async function handleV1Request(req, url, dependencies = {}) {
30411
30409
  return error(404, `unknown task action: ${action}`);
30412
30410
  }
30413
30411
  if (method === "GET") {
30414
- const task = await store.tasks.get(id);
30412
+ let task = await store.tasks.get(id);
30413
+ if (!task && typeof store.tasks.resolveRef === "function") {
30414
+ try {
30415
+ task = await store.tasks.resolveRef(id);
30416
+ } catch (e) {
30417
+ const msg = e.message || "";
30418
+ if (/ambiguous/i.test(msg))
30419
+ return error(409, msg);
30420
+ throw e;
30421
+ }
30422
+ }
30415
30423
  return task ? json2({ task }) : error(404, "task not found");
30416
30424
  }
30417
30425
  if (method === "PATCH" || method === "PUT") {
@@ -35547,6 +35555,17 @@ ${task.description}` : null
35547
35555
  patch.estimated_minutes = estimate;
35548
35556
  if (deadline !== undefined)
35549
35557
  patch.due_at = deadline;
35558
+ if (typeof patch.project_id === "string" && patch.project_id) {
35559
+ patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
35560
+ }
35561
+ if (typeof patch.task_list_id === "string" && patch.task_list_id) {
35562
+ let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
35563
+ if (!scope) {
35564
+ const current = await cloudGetTask(cloud, task_id2);
35565
+ scope = current?.project_id ?? undefined;
35566
+ }
35567
+ patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
35568
+ }
35550
35569
  if (version2 !== undefined)
35551
35570
  patch.version = version2;
35552
35571
  const updated = await cloudUpdateTask(cloud, task_id2, patch);
@@ -39764,6 +39783,63 @@ function registerTaskProjectTools(server, ctx) {
39764
39783
  }
39765
39784
  });
39766
39785
  }
39786
+ if (shouldRegisterTool("move_task")) {
39787
+ server.tool("move_task", "Re-parent a task to another project and/or task list, preserving its id and history. " + "A task list is project-scoped, so moving to a new project detaches the old list unless to_list is given.", {
39788
+ task_id: exports_external.string().describe("Task ID"),
39789
+ to_project: exports_external.string().optional().describe("Destination project ID, slug, or path"),
39790
+ to_list: exports_external.string().optional().describe("Destination task list (UUID, or slug resolved in the destination project)"),
39791
+ clear_list: exports_external.boolean().optional().describe("Detach from its task list (set task_list_id to null)"),
39792
+ version: exports_external.number().optional().describe("Expected version for optimistic locking")
39793
+ }, async ({ task_id, to_project, to_list, clear_list, version }) => {
39794
+ try {
39795
+ if (!to_project && !to_list && !clear_list) {
39796
+ throw new Error("Nothing to move: pass to_project, to_list, or clear_list.");
39797
+ }
39798
+ if (to_list && clear_list) {
39799
+ throw new Error("Use either to_list or clear_list, not both.");
39800
+ }
39801
+ const cloud = getTodosCloudClient();
39802
+ if (cloud) {
39803
+ const current2 = await cloudGetTask(cloud, task_id);
39804
+ if (!current2)
39805
+ throw new Error(`Task not found: ${task_id}`);
39806
+ const targetProjectId2 = to_project ? await cloudResolveProjectRef(cloud, to_project) : undefined;
39807
+ const scope = targetProjectId2 ?? current2.project_id ?? undefined;
39808
+ const patch = {};
39809
+ if (targetProjectId2 !== undefined)
39810
+ patch.project_id = targetProjectId2;
39811
+ if (to_list)
39812
+ patch.task_list_id = await cloudResolveTaskListRef(cloud, to_list, scope);
39813
+ else if (clear_list)
39814
+ patch.task_list_id = null;
39815
+ else if (targetProjectId2 && targetProjectId2 !== current2.project_id)
39816
+ patch.task_list_id = null;
39817
+ if (version !== undefined)
39818
+ patch.version = version;
39819
+ const task2 = await cloudUpdateTask(cloud, task_id, patch);
39820
+ return { content: [{ type: "text", text: formatTask(task2) }] };
39821
+ }
39822
+ const resolvedId = resolveId(task_id);
39823
+ const current = getTask(resolvedId);
39824
+ if (!current)
39825
+ throw new Error(`Task not found: ${task_id}`);
39826
+ const targetProjectId = to_project ? resolveId(to_project, "projects") : undefined;
39827
+ const updates = {};
39828
+ if (targetProjectId !== undefined)
39829
+ updates.project_id = targetProjectId;
39830
+ if (to_list)
39831
+ updates.task_list_id = resolveId(to_list, "task_lists");
39832
+ else if (clear_list)
39833
+ updates.task_list_id = null;
39834
+ else if (targetProjectId && targetProjectId !== current.project_id)
39835
+ updates.task_list_id = null;
39836
+ const task = updateWithOptionalVersion(resolvedId, updates, version);
39837
+ return { content: [{ type: "text", text: formatTask(task) }] };
39838
+ } catch (e) {
39839
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
39840
+ }
39841
+ });
39842
+ }
39767
39843
  if (shouldRegisterTool("reschedule_task")) {
39768
39844
  server.tool("reschedule_task", "Update a task's deadline.", {
39769
39845
  task_id: exports_external.string().describe("Task ID"),
@@ -43692,6 +43768,7 @@ function registerTaskMetaTools(server, ctx) {
43692
43768
  complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
43693
43769
  cancel_task: "cancel_task \u2014 Cancel a task. Params: task_id, version",
43694
43770
  reassign_task: "reassign_task \u2014 Change task assignee. Params: task_id, new_assignee, version",
43771
+ move_task: "move_task \u2014 Re-parent a task to another project and/or task list (keeps its id and history). Params: task_id (required), to_project, to_list, clear_list, version",
43695
43772
  reschedule_task: "reschedule_task \u2014 Update deadline. Params: task_id, deadline, version",
43696
43773
  prioritize_task: "prioritize_task \u2014 Set priority. Params: task_id, priority, version",
43697
43774
  search_tasks: "search_tasks \u2014 Full-text search. Params: query, project_id, status, limit",
@@ -71247,6 +71324,7 @@ var REGISTERED_CANONICAL_COMMANDS = [
71247
71324
  "manual",
71248
71325
  "mcp",
71249
71326
  "mine",
71327
+ "move",
71250
71328
  "next",
71251
71329
  "notifications",
71252
71330
  "onboarding",
@@ -71382,6 +71460,7 @@ var REMOTE_COMMANDS = new Set([
71382
71460
  "lists",
71383
71461
  "lock",
71384
71462
  "log-progress",
71463
+ "move",
71385
71464
  "next",
71386
71465
  "plans",
71387
71466
  "project-rename",