@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/cloud-router.d.ts +11 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +289 -210
- package/dist/contracts.js +1 -1
- package/dist/index.js +56 -3
- package/dist/mcp/index.js +231 -4
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/registry.js +1 -1
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/v1.generated.d.ts +2 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +10 -0
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +237 -4
- package/dist/server/openapi.d.ts +8 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +10 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +22 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.js +55 -2
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
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(`
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
15978
|
-
|
|
15979
|
-
|
|
15980
|
-
|
|
15981
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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",
|