@hasna/todos 0.11.93 → 0.11.95
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 +12 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +885 -76
- package/dist/contracts.js +56 -50
- package/dist/db/storage-tombstones.d.ts +1 -1
- package/dist/db/storage-tombstones.d.ts.map +1 -1
- package/dist/db/templates.d.ts.map +1 -1
- package/dist/index.js +170 -56
- package/dist/lib/template-semantics.d.ts +8 -0
- package/dist/lib/template-semantics.d.ts.map +1 -0
- package/dist/mcp/index.js +605 -61
- 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 +56 -50
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +35 -0
- package/dist/sdk/v1.generated.d.ts +105 -1
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +611 -67
- package/dist/server/openapi.d.ts +496 -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 +3 -2
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +1 -1
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/shadow-outbox.d.ts.map +1 -1
- package/dist/storage/shadow.d.ts +1 -1
- package/dist/storage/shadow.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +169 -55
- 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.95",
|
|
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",
|
|
@@ -2826,6 +2826,48 @@ async function cloudDeleteTask(client, id) {
|
|
|
2826
2826
|
throw error;
|
|
2827
2827
|
}
|
|
2828
2828
|
}
|
|
2829
|
+
function unwrapTemplate(raw) {
|
|
2830
|
+
if (raw && typeof raw === "object" && "template" in raw) {
|
|
2831
|
+
return raw.template;
|
|
2832
|
+
}
|
|
2833
|
+
return raw;
|
|
2834
|
+
}
|
|
2835
|
+
async function cloudListTemplates(client, projectId) {
|
|
2836
|
+
const result = await requiredRemoteRoute(client, "/v1/templates", () => client.list("templates", { query: projectId ? { project_id: projectId } : undefined }));
|
|
2837
|
+
const envelope = result.raw;
|
|
2838
|
+
return Array.isArray(envelope?.templates) ? envelope.templates : result.items;
|
|
2839
|
+
}
|
|
2840
|
+
async function cloudCreateTemplate(client, input) {
|
|
2841
|
+
return unwrapTemplate(await requiredRemoteRoute(client, "/v1/templates", () => client.create("templates", input)));
|
|
2842
|
+
}
|
|
2843
|
+
async function cloudGetTemplate(client, id) {
|
|
2844
|
+
try {
|
|
2845
|
+
return unwrapTemplate(await client.get("templates", id));
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
if (error && typeof error === "object" && error.status === 404)
|
|
2848
|
+
return null;
|
|
2849
|
+
throw error;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
async function cloudUpdateTemplate(client, id, patch) {
|
|
2853
|
+
try {
|
|
2854
|
+
return unwrapTemplate(await client.update("templates", id, patch));
|
|
2855
|
+
} catch (error) {
|
|
2856
|
+
if (error && typeof error === "object" && error.status === 404)
|
|
2857
|
+
return null;
|
|
2858
|
+
throw error;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
async function cloudDeleteTemplate(client, id) {
|
|
2862
|
+
try {
|
|
2863
|
+
await client.transport.del(`/templates/${encodeURIComponent(id)}`);
|
|
2864
|
+
return true;
|
|
2865
|
+
} catch (error) {
|
|
2866
|
+
if (error && typeof error === "object" && error.status === 404)
|
|
2867
|
+
return false;
|
|
2868
|
+
throw error;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2829
2871
|
async function cloudTaskAction(client, id, action, body = {}) {
|
|
2830
2872
|
const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/${action}`, body);
|
|
2831
2873
|
return unwrapTask(raw);
|
|
@@ -9928,6 +9970,50 @@ var init_recurrence = __esm(() => {
|
|
|
9928
9970
|
};
|
|
9929
9971
|
});
|
|
9930
9972
|
|
|
9973
|
+
// src/lib/template-semantics.ts
|
|
9974
|
+
function resolveTemplateVariables(templateVars, provided) {
|
|
9975
|
+
const merged = { ...provided };
|
|
9976
|
+
for (const variable of templateVars) {
|
|
9977
|
+
if (merged[variable.name] === undefined && variable.default !== undefined) {
|
|
9978
|
+
merged[variable.name] = variable.default;
|
|
9979
|
+
}
|
|
9980
|
+
}
|
|
9981
|
+
const missing = templateVars.filter((variable) => variable.required && merged[variable.name] === undefined).map((variable) => variable.name);
|
|
9982
|
+
if (missing.length > 0) {
|
|
9983
|
+
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
9984
|
+
}
|
|
9985
|
+
return merged;
|
|
9986
|
+
}
|
|
9987
|
+
function substituteTemplateVariables(text, variables) {
|
|
9988
|
+
let result = text;
|
|
9989
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
9990
|
+
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
9991
|
+
}
|
|
9992
|
+
return result;
|
|
9993
|
+
}
|
|
9994
|
+
function evaluateTemplateCondition(condition, variables) {
|
|
9995
|
+
if (!condition || condition.trim() === "")
|
|
9996
|
+
return true;
|
|
9997
|
+
const trimmed = condition.trim();
|
|
9998
|
+
const equal = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
9999
|
+
if (equal)
|
|
10000
|
+
return (variables[equal[1]] ?? "") === equal[2].trim();
|
|
10001
|
+
const unequal = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
10002
|
+
if (unequal)
|
|
10003
|
+
return (variables[unequal[1]] ?? "") !== unequal[2].trim();
|
|
10004
|
+
const falsy = trimmed.match(/^!\{([^}]+)\}$/);
|
|
10005
|
+
if (falsy) {
|
|
10006
|
+
const value = variables[falsy[1]];
|
|
10007
|
+
return !value || value === "" || value === "false";
|
|
10008
|
+
}
|
|
10009
|
+
const truthy = trimmed.match(/^\{([^}]+)\}$/);
|
|
10010
|
+
if (truthy) {
|
|
10011
|
+
const value = variables[truthy[1]];
|
|
10012
|
+
return !!value && value !== "" && value !== "false";
|
|
10013
|
+
}
|
|
10014
|
+
return true;
|
|
10015
|
+
}
|
|
10016
|
+
|
|
9931
10017
|
// src/db/templates.ts
|
|
9932
10018
|
var exports_templates = {};
|
|
9933
10019
|
__export(exports_templates, {
|
|
@@ -10023,6 +10109,14 @@ function deleteTemplate(id, db) {
|
|
|
10023
10109
|
payload: template,
|
|
10024
10110
|
version: template.version
|
|
10025
10111
|
}, d);
|
|
10112
|
+
for (const task of getTemplateTasks(resolved, d)) {
|
|
10113
|
+
recordStorageTombstone({
|
|
10114
|
+
object_type: "template_tasks",
|
|
10115
|
+
object_id: task.id,
|
|
10116
|
+
payload: task,
|
|
10117
|
+
version: 1
|
|
10118
|
+
}, d);
|
|
10119
|
+
}
|
|
10026
10120
|
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
10027
10121
|
}
|
|
10028
10122
|
function updateTemplate(id, updates, db) {
|
|
@@ -10154,34 +10248,7 @@ function getTemplateTasks(templateId, db) {
|
|
|
10154
10248
|
return rows.map(rowToTemplateTask);
|
|
10155
10249
|
}
|
|
10156
10250
|
function evaluateCondition(condition, variables) {
|
|
10157
|
-
|
|
10158
|
-
return true;
|
|
10159
|
-
const trimmed = condition.trim();
|
|
10160
|
-
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
10161
|
-
if (eqMatch) {
|
|
10162
|
-
const varName = eqMatch[1];
|
|
10163
|
-
const expected = eqMatch[2].trim();
|
|
10164
|
-
return (variables[varName] ?? "") === expected;
|
|
10165
|
-
}
|
|
10166
|
-
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
10167
|
-
if (neqMatch) {
|
|
10168
|
-
const varName = neqMatch[1];
|
|
10169
|
-
const expected = neqMatch[2].trim();
|
|
10170
|
-
return (variables[varName] ?? "") !== expected;
|
|
10171
|
-
}
|
|
10172
|
-
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
10173
|
-
if (falsyMatch) {
|
|
10174
|
-
const varName = falsyMatch[1];
|
|
10175
|
-
const val = variables[varName];
|
|
10176
|
-
return !val || val === "" || val === "false";
|
|
10177
|
-
}
|
|
10178
|
-
const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
|
|
10179
|
-
if (truthyMatch) {
|
|
10180
|
-
const varName = truthyMatch[1];
|
|
10181
|
-
const val = variables[varName];
|
|
10182
|
-
return !!val && val !== "" && val !== "false";
|
|
10183
|
-
}
|
|
10184
|
-
return true;
|
|
10251
|
+
return evaluateTemplateCondition(condition, variables);
|
|
10185
10252
|
}
|
|
10186
10253
|
function exportTemplate(id, db) {
|
|
10187
10254
|
const d = db || getDatabase();
|
|
@@ -10254,29 +10321,10 @@ function listTemplateVersions(id, db) {
|
|
|
10254
10321
|
return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
|
|
10255
10322
|
}
|
|
10256
10323
|
function resolveVariables(templateVars, provided) {
|
|
10257
|
-
|
|
10258
|
-
for (const v of templateVars) {
|
|
10259
|
-
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
10260
|
-
merged[v.name] = v.default;
|
|
10261
|
-
}
|
|
10262
|
-
}
|
|
10263
|
-
const missing = [];
|
|
10264
|
-
for (const v of templateVars) {
|
|
10265
|
-
if (v.required && merged[v.name] === undefined) {
|
|
10266
|
-
missing.push(v.name);
|
|
10267
|
-
}
|
|
10268
|
-
}
|
|
10269
|
-
if (missing.length > 0) {
|
|
10270
|
-
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
10271
|
-
}
|
|
10272
|
-
return merged;
|
|
10324
|
+
return resolveTemplateVariables(templateVars, provided);
|
|
10273
10325
|
}
|
|
10274
10326
|
function substituteVars(text, variables) {
|
|
10275
|
-
|
|
10276
|
-
for (const [key, val] of Object.entries(variables)) {
|
|
10277
|
-
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
|
|
10278
|
-
}
|
|
10279
|
-
return result;
|
|
10327
|
+
return substituteTemplateVariables(text, variables);
|
|
10280
10328
|
}
|
|
10281
10329
|
function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
|
|
10282
10330
|
const d = db || getDatabase();
|
|
@@ -15027,6 +15075,46 @@ function resolveTaskListRef(ref, projectId) {
|
|
|
15027
15075
|
return { id: bySlug.id };
|
|
15028
15076
|
return { error: `Could not resolve task list "${ref}" to a UUID${projectId ? " within the task's project" : ""}. Pass an exact task-list UUID.` };
|
|
15029
15077
|
}
|
|
15078
|
+
async function computeCloudReparent(cloud, current, opts) {
|
|
15079
|
+
const targetProjectId = opts.projectRef ? await cloudResolveProjectRef(cloud, opts.projectRef) : undefined;
|
|
15080
|
+
const scope = targetProjectId ?? current.project_id ?? undefined;
|
|
15081
|
+
let taskListId;
|
|
15082
|
+
if (opts.listRef)
|
|
15083
|
+
taskListId = await cloudResolveTaskListRef(cloud, opts.listRef, scope);
|
|
15084
|
+
else if (opts.clearList)
|
|
15085
|
+
taskListId = null;
|
|
15086
|
+
else if (targetProjectId && targetProjectId !== current.project_id)
|
|
15087
|
+
taskListId = null;
|
|
15088
|
+
const patch = {};
|
|
15089
|
+
if (targetProjectId !== undefined)
|
|
15090
|
+
patch.project_id = targetProjectId;
|
|
15091
|
+
if (taskListId !== undefined)
|
|
15092
|
+
patch.task_list_id = taskListId;
|
|
15093
|
+
return patch;
|
|
15094
|
+
}
|
|
15095
|
+
function computeLocalReparent(current, opts) {
|
|
15096
|
+
const targetProjectId = opts.projectRef ? resolveProjectIdOrSlug(opts.projectRef) : undefined;
|
|
15097
|
+
const scope = targetProjectId ?? current.project_id ?? null;
|
|
15098
|
+
let taskListId;
|
|
15099
|
+
if (opts.listRef) {
|
|
15100
|
+
const resolved = resolveTaskListRef(opts.listRef, scope);
|
|
15101
|
+
if ("error" in resolved) {
|
|
15102
|
+
console.error(chalk2.red(resolved.error));
|
|
15103
|
+
process.exit(1);
|
|
15104
|
+
}
|
|
15105
|
+
taskListId = resolved.id;
|
|
15106
|
+
} else if (opts.clearList) {
|
|
15107
|
+
taskListId = null;
|
|
15108
|
+
} else if (targetProjectId && targetProjectId !== current.project_id) {
|
|
15109
|
+
taskListId = null;
|
|
15110
|
+
}
|
|
15111
|
+
const patch = {};
|
|
15112
|
+
if (targetProjectId !== undefined)
|
|
15113
|
+
patch.project_id = targetProjectId;
|
|
15114
|
+
if (taskListId !== undefined)
|
|
15115
|
+
patch.task_list_id = taskListId;
|
|
15116
|
+
return patch;
|
|
15117
|
+
}
|
|
15030
15118
|
function registerTaskCommands(program2) {
|
|
15031
15119
|
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) => {
|
|
15032
15120
|
const globalOpts = program2.opts();
|
|
@@ -15731,7 +15819,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
15731
15819
|
console.log(` ${chalk2.dim(h.created_at)} ${chalk2.bold(h.action)}${field}${change}${agent}`);
|
|
15732
15820
|
}
|
|
15733
15821
|
});
|
|
15734
|
-
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) => {
|
|
15822
|
+
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) => {
|
|
15735
15823
|
const globalOpts = program2.opts();
|
|
15736
15824
|
opts.tags = opts.tags || opts.tag;
|
|
15737
15825
|
opts.list = opts.list || opts.taskList;
|
|
@@ -15758,7 +15846,11 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
15758
15846
|
const plan = opts.plan ? await cloudResolvePlan(cloud, opts.plan, current2.project_id ?? undefined) : null;
|
|
15759
15847
|
if (opts.plan && !plan)
|
|
15760
15848
|
throw new Error(`Plan not found: ${opts.plan}`);
|
|
15761
|
-
const
|
|
15849
|
+
const reparent2 = await computeCloudReparent(cloud, current2, {
|
|
15850
|
+
projectRef: opts.project || globalOpts.project,
|
|
15851
|
+
listRef: opts.list,
|
|
15852
|
+
clearList: opts.clearList
|
|
15853
|
+
});
|
|
15762
15854
|
task3 = await cloudUpdateTask(cloud, currentId, {
|
|
15763
15855
|
title: opts.title,
|
|
15764
15856
|
description: opts.description,
|
|
@@ -15767,7 +15859,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
15767
15859
|
assigned_to: opts.assign,
|
|
15768
15860
|
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
|
|
15769
15861
|
plan_id: plan?.id ?? (opts.clearPlan ? null : undefined),
|
|
15770
|
-
|
|
15862
|
+
...reparent2,
|
|
15771
15863
|
working_dir: opts.workingDir ? resolve9(opts.workingDir) : opts.clearWorkingDir ? null : undefined,
|
|
15772
15864
|
estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
|
|
15773
15865
|
sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
|
|
@@ -15792,14 +15884,11 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
15792
15884
|
console.error(chalk2.red(`Task not found: ${id}`));
|
|
15793
15885
|
process.exit(1);
|
|
15794
15886
|
}
|
|
15795
|
-
const
|
|
15796
|
-
|
|
15797
|
-
|
|
15798
|
-
|
|
15799
|
-
|
|
15800
|
-
}
|
|
15801
|
-
return resolved.id;
|
|
15802
|
-
})() : opts.clearList ? null : undefined;
|
|
15887
|
+
const reparent = computeLocalReparent(current, {
|
|
15888
|
+
projectRef: opts.project || globalOpts.project,
|
|
15889
|
+
listRef: opts.list,
|
|
15890
|
+
clearList: opts.clearList
|
|
15891
|
+
});
|
|
15803
15892
|
const planId = opts.plan ? resolvePlanId(opts.plan) : opts.clearPlan ? null : undefined;
|
|
15804
15893
|
let task2;
|
|
15805
15894
|
try {
|
|
@@ -15812,7 +15901,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
15812
15901
|
assigned_to: opts.assign,
|
|
15813
15902
|
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
|
|
15814
15903
|
plan_id: planId,
|
|
15815
|
-
|
|
15904
|
+
...reparent,
|
|
15816
15905
|
working_dir: opts.workingDir ? resolve9(opts.workingDir) : opts.clearWorkingDir ? null : undefined,
|
|
15817
15906
|
estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
|
|
15818
15907
|
sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
|
|
@@ -15830,6 +15919,68 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
15830
15919
|
console.log(formatTaskLine(task2));
|
|
15831
15920
|
}
|
|
15832
15921
|
});
|
|
15922
|
+
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) => {
|
|
15923
|
+
const globalOpts = program2.opts();
|
|
15924
|
+
const projectRef = opts.toProject ?? globalOpts.project;
|
|
15925
|
+
const listRef = opts.toList;
|
|
15926
|
+
if (!projectRef && !listRef && !opts.clearList) {
|
|
15927
|
+
handleError(new Error("Nothing to move: pass --to-project, --to-list, or --clear-list."));
|
|
15928
|
+
}
|
|
15929
|
+
if (listRef && opts.clearList) {
|
|
15930
|
+
handleError(new Error("Use either --to-list or --clear-list, not both."));
|
|
15931
|
+
}
|
|
15932
|
+
const cloud = getTodosCloudClient();
|
|
15933
|
+
if (cloud) {
|
|
15934
|
+
let task3;
|
|
15935
|
+
try {
|
|
15936
|
+
const currentId = await resolveTaskIdForCommand(id, cloud);
|
|
15937
|
+
const current2 = await cloudGetTask(cloud, currentId);
|
|
15938
|
+
if (!current2)
|
|
15939
|
+
throw new Error(`Task not found: ${id}`);
|
|
15940
|
+
const reparent2 = await computeCloudReparent(cloud, current2, {
|
|
15941
|
+
projectRef,
|
|
15942
|
+
listRef,
|
|
15943
|
+
clearList: opts.clearList
|
|
15944
|
+
});
|
|
15945
|
+
if (reparent2.project_id === undefined && reparent2.task_list_id === undefined) {
|
|
15946
|
+
throw new Error("Nothing to move: the task is already in the requested project/list.");
|
|
15947
|
+
}
|
|
15948
|
+
task3 = await cloudUpdateTask(cloud, currentId, reparent2);
|
|
15949
|
+
} catch (e) {
|
|
15950
|
+
handleError(e);
|
|
15951
|
+
}
|
|
15952
|
+
if (globalOpts.json) {
|
|
15953
|
+
output(task3, true);
|
|
15954
|
+
} else {
|
|
15955
|
+
console.log(chalk2.green("Task moved:"));
|
|
15956
|
+
console.log(formatTaskLine(task3));
|
|
15957
|
+
}
|
|
15958
|
+
return;
|
|
15959
|
+
}
|
|
15960
|
+
const resolvedId = resolveTaskId(id);
|
|
15961
|
+
const current = getTask(resolvedId);
|
|
15962
|
+
if (!current) {
|
|
15963
|
+
console.error(chalk2.red(`Task not found: ${id}`));
|
|
15964
|
+
process.exit(1);
|
|
15965
|
+
}
|
|
15966
|
+
const reparent = computeLocalReparent(current, { projectRef, listRef, clearList: opts.clearList });
|
|
15967
|
+
if (reparent.project_id === undefined && reparent.task_list_id === undefined) {
|
|
15968
|
+
console.error(chalk2.red("Nothing to move: the task is already in the requested project/list."));
|
|
15969
|
+
process.exit(1);
|
|
15970
|
+
}
|
|
15971
|
+
let task2;
|
|
15972
|
+
try {
|
|
15973
|
+
task2 = updateTask(resolvedId, { version: current.version, ...reparent });
|
|
15974
|
+
} catch (e) {
|
|
15975
|
+
handleError(e);
|
|
15976
|
+
}
|
|
15977
|
+
if (globalOpts.json) {
|
|
15978
|
+
output(task2, true);
|
|
15979
|
+
} else {
|
|
15980
|
+
console.log(chalk2.green("Task moved:"));
|
|
15981
|
+
console.log(formatTaskLine(task2));
|
|
15982
|
+
}
|
|
15983
|
+
});
|
|
15833
15984
|
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) => {
|
|
15834
15985
|
const globalOpts = program2.opts();
|
|
15835
15986
|
const attachmentIds = opts.attachIds ? opts.attachIds.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
@@ -16807,6 +16958,80 @@ __export(exports_plan_template_commands, {
|
|
|
16807
16958
|
registerPlanTemplateCommands: () => registerPlanTemplateCommands
|
|
16808
16959
|
});
|
|
16809
16960
|
import chalk3 from "chalk";
|
|
16961
|
+
async function createRemoteTemplateTasks(cloud, template, projectId, variables, agentId, overrides, visited = new Set) {
|
|
16962
|
+
if (visited.has(template.id)) {
|
|
16963
|
+
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
16964
|
+
}
|
|
16965
|
+
visited.add(template.id);
|
|
16966
|
+
try {
|
|
16967
|
+
const resolved = resolveTemplateVariables(template.variables ?? [], variables);
|
|
16968
|
+
const render = (value) => value === null || value === undefined ? value : substituteTemplateVariables(value, resolved);
|
|
16969
|
+
if (template.tasks.length === 0) {
|
|
16970
|
+
const task = await cloudCreateTask(cloud, {
|
|
16971
|
+
title: render(overrides?.title || template.title_pattern),
|
|
16972
|
+
...render(overrides?.description ?? template.description) ? { description: render(overrides?.description ?? template.description) } : {},
|
|
16973
|
+
priority: overrides?.priority ?? template.priority,
|
|
16974
|
+
tags: template.tags,
|
|
16975
|
+
...projectId ? { project_id: projectId } : {},
|
|
16976
|
+
...template.plan_id ? { plan_id: template.plan_id } : {},
|
|
16977
|
+
...agentId ? { agent_id: agentId } : {},
|
|
16978
|
+
...Object.keys(template.metadata ?? {}).length > 0 ? { metadata: template.metadata } : {}
|
|
16979
|
+
});
|
|
16980
|
+
return { tasks: [task] };
|
|
16981
|
+
}
|
|
16982
|
+
const created = [];
|
|
16983
|
+
const positionToTaskId = new Map;
|
|
16984
|
+
const skippedPositions = new Set;
|
|
16985
|
+
for (const step of template.tasks) {
|
|
16986
|
+
if (step.include_template_id) {
|
|
16987
|
+
const included = await cloudGetTemplate(cloud, step.include_template_id);
|
|
16988
|
+
if (!included)
|
|
16989
|
+
throw new Error(`Included template not found: ${step.include_template_id}`);
|
|
16990
|
+
const result = await createRemoteTemplateTasks(cloud, included, projectId, resolved, agentId, undefined, visited);
|
|
16991
|
+
created.push(...result.tasks);
|
|
16992
|
+
if (result.tasks.length > 0)
|
|
16993
|
+
positionToTaskId.set(step.position, result.tasks[0].id);
|
|
16994
|
+
else
|
|
16995
|
+
skippedPositions.add(step.position);
|
|
16996
|
+
continue;
|
|
16997
|
+
}
|
|
16998
|
+
if (step.condition && !evaluateTemplateCondition(step.condition, resolved)) {
|
|
16999
|
+
skippedPositions.add(step.position);
|
|
17000
|
+
continue;
|
|
17001
|
+
}
|
|
17002
|
+
const task = await cloudCreateTask(cloud, {
|
|
17003
|
+
title: render(step.title_pattern),
|
|
17004
|
+
...render(step.description) ? { description: render(step.description) } : {},
|
|
17005
|
+
priority: step.priority,
|
|
17006
|
+
tags: step.tags,
|
|
17007
|
+
...step.task_type ? { task_type: step.task_type } : {},
|
|
17008
|
+
...projectId ? { project_id: projectId } : {},
|
|
17009
|
+
...template.plan_id ? { plan_id: template.plan_id } : {},
|
|
17010
|
+
...agentId ? { agent_id: agentId } : {},
|
|
17011
|
+
...Object.keys(step.metadata ?? {}).length > 0 ? { metadata: step.metadata } : {}
|
|
17012
|
+
});
|
|
17013
|
+
created.push(task);
|
|
17014
|
+
positionToTaskId.set(step.position, task.id);
|
|
17015
|
+
}
|
|
17016
|
+
for (const step of template.tasks) {
|
|
17017
|
+
if (skippedPositions.has(step.position) || step.include_template_id)
|
|
17018
|
+
continue;
|
|
17019
|
+
const taskId = positionToTaskId.get(step.position);
|
|
17020
|
+
if (!taskId)
|
|
17021
|
+
continue;
|
|
17022
|
+
for (const dependencyPosition of step.depends_on_positions) {
|
|
17023
|
+
if (skippedPositions.has(dependencyPosition))
|
|
17024
|
+
continue;
|
|
17025
|
+
const dependencyId = positionToTaskId.get(dependencyPosition);
|
|
17026
|
+
if (dependencyId)
|
|
17027
|
+
await cloudAddDependency(cloud, taskId, dependencyId);
|
|
17028
|
+
}
|
|
17029
|
+
}
|
|
17030
|
+
return { tasks: created };
|
|
17031
|
+
} finally {
|
|
17032
|
+
visited.delete(template.id);
|
|
17033
|
+
}
|
|
17034
|
+
}
|
|
16810
17035
|
function resolvePlanCliRef(ref, projectId) {
|
|
16811
17036
|
const db = getDatabase();
|
|
16812
17037
|
const resolved = resolvePlanRefDetailed(ref, db, projectId);
|
|
@@ -17051,6 +17276,117 @@ function registerPlanTemplateCommands(program2) {
|
|
|
17051
17276
|
});
|
|
17052
17277
|
program2.command("templates").description("List and manage task templates").option("--add <name>", "Create a template").option("--title <pattern>", "Title pattern (with --add)").option("-d, --description <text>", "Default description").option("-p, --priority <level>", "Default priority").option("-t, --tags <tags>", "Default tags (comma-separated)").option("--delete <id>", "Delete a template").option("--update <id>", "Update a template").option("--use <id>", "Create a task from a template").option("--var <vars...>", "Variable substitutions: key=value (e.g. --var feature=login)").action(async (opts) => {
|
|
17053
17278
|
const globalOpts = program2.opts();
|
|
17279
|
+
const cloud = getTodosCloudClient();
|
|
17280
|
+
if (cloud) {
|
|
17281
|
+
try {
|
|
17282
|
+
const projectId = globalOpts.project ? await cloudResolveProjectRef(cloud, globalOpts.project) : undefined;
|
|
17283
|
+
if (opts.add) {
|
|
17284
|
+
if (!opts.title) {
|
|
17285
|
+
console.error(chalk3.red("--title is required with --add"));
|
|
17286
|
+
process.exit(1);
|
|
17287
|
+
}
|
|
17288
|
+
const template = await cloudCreateTemplate(cloud, {
|
|
17289
|
+
name: opts.add,
|
|
17290
|
+
title_pattern: opts.title,
|
|
17291
|
+
description: opts.description,
|
|
17292
|
+
priority: opts.priority || "medium",
|
|
17293
|
+
tags: opts.tags ? opts.tags.split(",").map((tag) => tag.trim()).filter(Boolean) : [],
|
|
17294
|
+
project_id: projectId
|
|
17295
|
+
});
|
|
17296
|
+
if (globalOpts.json) {
|
|
17297
|
+
output(template, true);
|
|
17298
|
+
} else {
|
|
17299
|
+
console.log(chalk3.green(`Template created: ${template.id.slice(0, 8)} | ${template.name} | "${template.title_pattern}"`));
|
|
17300
|
+
}
|
|
17301
|
+
return;
|
|
17302
|
+
}
|
|
17303
|
+
if (opts.delete) {
|
|
17304
|
+
const deleted = await cloudDeleteTemplate(cloud, opts.delete);
|
|
17305
|
+
if (globalOpts.json) {
|
|
17306
|
+
output({ deleted }, true);
|
|
17307
|
+
} else if (deleted) {
|
|
17308
|
+
console.log(chalk3.green("Template deleted."));
|
|
17309
|
+
} else {
|
|
17310
|
+
console.error(chalk3.red("Template not found."));
|
|
17311
|
+
process.exit(1);
|
|
17312
|
+
}
|
|
17313
|
+
return;
|
|
17314
|
+
}
|
|
17315
|
+
if (opts.update) {
|
|
17316
|
+
const updates = {};
|
|
17317
|
+
if (opts.title)
|
|
17318
|
+
updates.title_pattern = opts.title;
|
|
17319
|
+
if (opts.description)
|
|
17320
|
+
updates.description = opts.description;
|
|
17321
|
+
if (opts.priority)
|
|
17322
|
+
updates.priority = opts.priority;
|
|
17323
|
+
if (opts.tags)
|
|
17324
|
+
updates.tags = opts.tags.split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
17325
|
+
if (Object.keys(updates).length === 0) {
|
|
17326
|
+
console.error(chalk3.red("Provide --title, --description, --priority, or --tags with --update"));
|
|
17327
|
+
process.exit(1);
|
|
17328
|
+
}
|
|
17329
|
+
const updated = await cloudUpdateTemplate(cloud, opts.update, updates);
|
|
17330
|
+
if (!updated) {
|
|
17331
|
+
console.error(chalk3.red("Template not found."));
|
|
17332
|
+
process.exit(1);
|
|
17333
|
+
}
|
|
17334
|
+
if (globalOpts.json) {
|
|
17335
|
+
output(updated, true);
|
|
17336
|
+
} else {
|
|
17337
|
+
console.log(chalk3.green(`Template updated: ${updated.id.slice(0, 8)} | ${updated.name} | "${updated.title_pattern}"`));
|
|
17338
|
+
}
|
|
17339
|
+
return;
|
|
17340
|
+
}
|
|
17341
|
+
if (opts.use) {
|
|
17342
|
+
const variables = {};
|
|
17343
|
+
for (const value of opts.var ?? []) {
|
|
17344
|
+
const separator = value.indexOf("=");
|
|
17345
|
+
if (separator === -1) {
|
|
17346
|
+
console.error(chalk3.red(`Invalid variable format: ${value} (expected key=value)`));
|
|
17347
|
+
process.exit(1);
|
|
17348
|
+
}
|
|
17349
|
+
variables[value.slice(0, separator)] = value.slice(separator + 1);
|
|
17350
|
+
}
|
|
17351
|
+
const template = await cloudGetTemplate(cloud, opts.use);
|
|
17352
|
+
if (!template) {
|
|
17353
|
+
console.error(chalk3.red("Template not found."));
|
|
17354
|
+
process.exit(1);
|
|
17355
|
+
}
|
|
17356
|
+
const targetProjectId = template.project_id ?? projectId;
|
|
17357
|
+
const { tasks: created } = await createRemoteTemplateTasks(cloud, template, targetProjectId, variables, globalOpts.agent, {
|
|
17358
|
+
title: opts.title,
|
|
17359
|
+
description: opts.description,
|
|
17360
|
+
priority: opts.priority
|
|
17361
|
+
});
|
|
17362
|
+
if (globalOpts.json) {
|
|
17363
|
+
output(created, true);
|
|
17364
|
+
} else {
|
|
17365
|
+
console.log(chalk3.green(`${created.length} task(s) created from template:`));
|
|
17366
|
+
for (const task of created)
|
|
17367
|
+
console.log(formatTaskLine(task));
|
|
17368
|
+
}
|
|
17369
|
+
return;
|
|
17370
|
+
}
|
|
17371
|
+
const templates2 = await cloudListTemplates(cloud, projectId);
|
|
17372
|
+
if (globalOpts.json) {
|
|
17373
|
+
output(templates2, true);
|
|
17374
|
+
return;
|
|
17375
|
+
}
|
|
17376
|
+
if (templates2.length === 0) {
|
|
17377
|
+
console.log(chalk3.dim("No templates."));
|
|
17378
|
+
return;
|
|
17379
|
+
}
|
|
17380
|
+
console.log(chalk3.bold(`${templates2.length} template(s):
|
|
17381
|
+
`));
|
|
17382
|
+
for (const template of templates2) {
|
|
17383
|
+
console.log(` ${chalk3.dim(template.id.slice(0, 8))} ${chalk3.bold(template.name)} ${chalk3.cyan(`"${template.title_pattern}"`)} ${chalk3.yellow(template.priority)}`);
|
|
17384
|
+
}
|
|
17385
|
+
} catch (error) {
|
|
17386
|
+
handleError(error);
|
|
17387
|
+
}
|
|
17388
|
+
return;
|
|
17389
|
+
}
|
|
17054
17390
|
const {
|
|
17055
17391
|
createTemplate: createTemplate2,
|
|
17056
17392
|
getTemplateWithTasks: getTemplateWithTasks2,
|
|
@@ -17303,7 +17639,6 @@ function registerPlanTemplateCommands(program2) {
|
|
|
17303
17639
|
});
|
|
17304
17640
|
program2.command("template-import [file]").alias("templates-import").description("Import a template from a JSON file").option("--file <path>", "Path to template JSON file (alternative to positional arg)").action(async (file, opts) => {
|
|
17305
17641
|
const globalOpts = program2.opts();
|
|
17306
|
-
const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
17307
17642
|
const { readFileSync: readFileSync5 } = await import("fs");
|
|
17308
17643
|
try {
|
|
17309
17644
|
const filePath = file || opts.file;
|
|
@@ -17313,7 +17648,8 @@ function registerPlanTemplateCommands(program2) {
|
|
|
17313
17648
|
}
|
|
17314
17649
|
const content = readFileSync5(filePath, "utf-8");
|
|
17315
17650
|
const json = JSON.parse(content);
|
|
17316
|
-
const
|
|
17651
|
+
const cloud = getTodosCloudClient();
|
|
17652
|
+
const template = cloud ? await cloudCreateTemplate(cloud, json) : (await Promise.resolve().then(() => (init_templates(), exports_templates))).importTemplate(json);
|
|
17317
17653
|
if (globalOpts.json) {
|
|
17318
17654
|
output(template, true);
|
|
17319
17655
|
} else {
|
|
@@ -26208,6 +26544,7 @@ function exportSqliteTodosStorageSnapshot(db) {
|
|
|
26208
26544
|
agents: listAgents({ include_archived: true }, d),
|
|
26209
26545
|
taskLists: listTaskLists(undefined, d),
|
|
26210
26546
|
templates: listTemplates(d),
|
|
26547
|
+
templateTasks: listTemplates(d).flatMap((template) => getTemplateTasks(template.id, d)),
|
|
26211
26548
|
auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
|
|
26212
26549
|
tombstones: listStorageTombstones(d)
|
|
26213
26550
|
};
|
|
@@ -26257,6 +26594,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
26257
26594
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
26258
26595
|
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
26259
26596
|
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
26597
|
+
applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
|
|
26260
26598
|
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
|
|
26261
26599
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
26262
26600
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
@@ -26361,6 +26699,8 @@ function tableForTombstone(objectType) {
|
|
|
26361
26699
|
return "task_lists";
|
|
26362
26700
|
if (objectType === "templates")
|
|
26363
26701
|
return "task_templates";
|
|
26702
|
+
if (objectType === "template_tasks")
|
|
26703
|
+
return "template_tasks";
|
|
26364
26704
|
return "task_history";
|
|
26365
26705
|
}
|
|
26366
26706
|
function listRows(db, table, columns) {
|
|
@@ -26397,7 +26737,7 @@ function clockColumnsForTable(table) {
|
|
|
26397
26737
|
return ["created_at"];
|
|
26398
26738
|
return ["updated_at", "created_at"];
|
|
26399
26739
|
}
|
|
26400
|
-
var PROJECT_COLUMNS, PROJECT_MACHINE_PATH_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
|
|
26740
|
+
var PROJECT_COLUMNS, PROJECT_MACHINE_PATH_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TEMPLATE_TASK_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
|
|
26401
26741
|
var init_sqlite_snapshot = __esm(() => {
|
|
26402
26742
|
init_database();
|
|
26403
26743
|
init_agents();
|
|
@@ -26491,6 +26831,21 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
26491
26831
|
"machine_id",
|
|
26492
26832
|
"synced_at"
|
|
26493
26833
|
];
|
|
26834
|
+
TEMPLATE_TASK_COLUMNS = [
|
|
26835
|
+
"id",
|
|
26836
|
+
"template_id",
|
|
26837
|
+
"position",
|
|
26838
|
+
"title_pattern",
|
|
26839
|
+
"description",
|
|
26840
|
+
"priority",
|
|
26841
|
+
"tags",
|
|
26842
|
+
"task_type",
|
|
26843
|
+
"condition",
|
|
26844
|
+
"include_template_id",
|
|
26845
|
+
"depends_on_positions",
|
|
26846
|
+
"metadata",
|
|
26847
|
+
"created_at"
|
|
26848
|
+
];
|
|
26494
26849
|
TASK_COLUMNS = [
|
|
26495
26850
|
"id",
|
|
26496
26851
|
"short_id",
|
|
@@ -26559,7 +26914,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
26559
26914
|
"created_at",
|
|
26560
26915
|
"machine_id"
|
|
26561
26916
|
];
|
|
26562
|
-
JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables"]);
|
|
26917
|
+
JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
|
|
26563
26918
|
BOOLEAN_COLUMNS = new Set(["requires_approval"]);
|
|
26564
26919
|
});
|
|
26565
26920
|
|
|
@@ -26949,6 +27304,7 @@ function snapshotEntries(snapshot) {
|
|
|
26949
27304
|
...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
|
|
26950
27305
|
...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
|
|
26951
27306
|
...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
|
|
27307
|
+
...(snapshot.templateTasks ?? []).map((payload) => entry("template_tasks", payload, snapshot.exportedAt)),
|
|
26952
27308
|
...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
|
|
26953
27309
|
...(snapshot.tombstones ?? []).map((tombstone) => ({
|
|
26954
27310
|
type: tombstone.object_type,
|
|
@@ -27000,6 +27356,7 @@ function rowsToSnapshot(rows) {
|
|
|
27000
27356
|
agents: [],
|
|
27001
27357
|
taskLists: [],
|
|
27002
27358
|
templates: [],
|
|
27359
|
+
templateTasks: [],
|
|
27003
27360
|
auditHistory: [],
|
|
27004
27361
|
tombstones: []
|
|
27005
27362
|
};
|
|
@@ -27034,6 +27391,8 @@ function rowsToSnapshot(rows) {
|
|
|
27034
27391
|
snapshot.taskLists.push(payload);
|
|
27035
27392
|
else if (row.object_type === "templates")
|
|
27036
27393
|
snapshot.templates.push(payload);
|
|
27394
|
+
else if (row.object_type === "template_tasks")
|
|
27395
|
+
snapshot.templateTasks.push(payload);
|
|
27037
27396
|
else if (row.object_type === "audit_history")
|
|
27038
27397
|
snapshot.auditHistory.push(payload);
|
|
27039
27398
|
}
|
|
@@ -27251,6 +27610,9 @@ class TodosShadowOutbox {
|
|
|
27251
27610
|
case "templates":
|
|
27252
27611
|
snapshot.templates.push(record);
|
|
27253
27612
|
break;
|
|
27613
|
+
case "template_tasks":
|
|
27614
|
+
snapshot.templateTasks.push(record);
|
|
27615
|
+
break;
|
|
27254
27616
|
case "audit_history":
|
|
27255
27617
|
snapshot.auditHistory.push(record);
|
|
27256
27618
|
break;
|
|
@@ -27317,6 +27679,7 @@ function emptySnapshot() {
|
|
|
27317
27679
|
agents: [],
|
|
27318
27680
|
taskLists: [],
|
|
27319
27681
|
templates: [],
|
|
27682
|
+
templateTasks: [],
|
|
27320
27683
|
auditHistory: [],
|
|
27321
27684
|
tombstones: []
|
|
27322
27685
|
};
|
|
@@ -27513,10 +27876,13 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
27513
27876
|
get: (id) => store.get("templates", id),
|
|
27514
27877
|
list: async () => (await store.list("templates")).sort((a, b) => a.name.localeCompare(b.name)),
|
|
27515
27878
|
update: (id, input) => updateTemplate2(id, input, store),
|
|
27516
|
-
delete: (id, context) =>
|
|
27879
|
+
delete: (id, context) => deleteTemplate2(id, store, context),
|
|
27517
27880
|
getWithTasks: async (id) => {
|
|
27518
27881
|
const template = await store.get("templates", id);
|
|
27519
|
-
|
|
27882
|
+
if (!template)
|
|
27883
|
+
return null;
|
|
27884
|
+
const tasks = (await store.list("template_tasks")).filter((task) => task.template_id === id).sort((left, right) => left.position - right.position || left.id.localeCompare(right.id));
|
|
27885
|
+
return { ...template, tasks };
|
|
27520
27886
|
}
|
|
27521
27887
|
},
|
|
27522
27888
|
audit: {
|
|
@@ -27804,6 +28170,57 @@ class PostgresJsonRecordStore {
|
|
|
27804
28170
|
}
|
|
27805
28171
|
return value;
|
|
27806
28172
|
}
|
|
28173
|
+
async createTemplateWithTasks(template, tasks, context = {}) {
|
|
28174
|
+
await this.ensureSchema();
|
|
28175
|
+
const records = [
|
|
28176
|
+
{ object_type: "templates", object_id: template.id, payload: template, updated_at: template.created_at, version: template.version },
|
|
28177
|
+
...tasks.map((task) => ({ object_type: "template_tasks", object_id: task.id, payload: task, updated_at: task.created_at, version: 1 }))
|
|
28178
|
+
];
|
|
28179
|
+
const result = await this.options.client.query(`/* todos:create-template-with-tasks-atomic */ WITH input AS (
|
|
28180
|
+
SELECT value->>'object_type' AS object_type,
|
|
28181
|
+
value->>'object_id' AS object_id,
|
|
28182
|
+
value->'payload' AS payload,
|
|
28183
|
+
value->>'updated_at' AS updated_at,
|
|
28184
|
+
COALESCE((value->>'version')::integer, 1) AS version
|
|
28185
|
+
FROM jsonb_array_elements($2::jsonb) AS value
|
|
28186
|
+
) INSERT INTO ${this.tableName} (
|
|
28187
|
+
service, object_type, object_id, payload, updated_at,
|
|
28188
|
+
deleted_at, source_machine_id, version
|
|
28189
|
+
) SELECT $1, object_type, object_id, payload, updated_at::timestamptz,
|
|
28190
|
+
NULL, $3, version
|
|
28191
|
+
FROM input
|
|
28192
|
+
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
28193
|
+
payload = EXCLUDED.payload,
|
|
28194
|
+
updated_at = EXCLUDED.updated_at,
|
|
28195
|
+
deleted_at = NULL,
|
|
28196
|
+
source_machine_id = EXCLUDED.source_machine_id,
|
|
28197
|
+
version = EXCLUDED.version
|
|
28198
|
+
WHERE ${this.tableName}.updated_at IS NULL
|
|
28199
|
+
OR ${this.tableName}.updated_at < EXCLUDED.updated_at
|
|
28200
|
+
OR (${this.tableName}.updated_at = EXCLUDED.updated_at
|
|
28201
|
+
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
|
|
28202
|
+
RETURNING object_type, object_id`, [this.service, jsonbParam(records), this.machineId(context)]);
|
|
28203
|
+
if (result.rows.length !== records.length) {
|
|
28204
|
+
throw new Error("Template checklist write was rejected before completion; no partial template was committed");
|
|
28205
|
+
}
|
|
28206
|
+
}
|
|
28207
|
+
async deleteTemplateWithTasks(id, context = {}) {
|
|
28208
|
+
await this.ensureSchema();
|
|
28209
|
+
const timestamp = new Date().toISOString();
|
|
28210
|
+
const result = await this.options.client.query(`/* todos:delete-template-with-tasks-atomic */ WITH target AS (
|
|
28211
|
+
SELECT 1 FROM ${this.tableName}
|
|
28212
|
+
WHERE service = $1 AND object_type = 'templates' AND object_id = $2 AND deleted_at IS NULL
|
|
28213
|
+
) UPDATE ${this.tableName} AS record SET
|
|
28214
|
+
deleted_at = $3::timestamptz,
|
|
28215
|
+
updated_at = $3::timestamptz,
|
|
28216
|
+
source_machine_id = COALESCE($4, record.source_machine_id),
|
|
28217
|
+
version = COALESCE(record.version, 0) + 1
|
|
28218
|
+
WHERE record.service = $1 AND record.deleted_at IS NULL AND EXISTS (SELECT 1 FROM target)
|
|
28219
|
+
AND (record.object_type = 'templates' AND record.object_id = $2
|
|
28220
|
+
OR record.object_type = 'template_tasks' AND record.payload->>'template_id' = $2)
|
|
28221
|
+
RETURNING record.object_type`, [this.service, id, timestamp, this.machineId(context)]);
|
|
28222
|
+
return result.rows.some((row) => row.object_type === "templates");
|
|
28223
|
+
}
|
|
27807
28224
|
async completeTask(id, agentId, options) {
|
|
27808
28225
|
await this.ensureSchema();
|
|
27809
28226
|
const operationTimestamp = new Date().toISOString();
|
|
@@ -28144,7 +28561,7 @@ async function updateTask2(id, input, store) {
|
|
|
28144
28561
|
tags: input.tags ?? existing.tags,
|
|
28145
28562
|
metadata: input.metadata ?? existing.metadata,
|
|
28146
28563
|
requires_approval: input.requires_approval ?? existing.requires_approval,
|
|
28147
|
-
task_list_id: input.task_list_id
|
|
28564
|
+
task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id
|
|
28148
28565
|
};
|
|
28149
28566
|
await store.upsert("tasks", task);
|
|
28150
28567
|
return task;
|
|
@@ -28566,7 +28983,7 @@ async function updateTaskList2(id, input, store) {
|
|
|
28566
28983
|
}
|
|
28567
28984
|
async function createTemplate2(input, store, context) {
|
|
28568
28985
|
const timestamp = new Date().toISOString();
|
|
28569
|
-
|
|
28986
|
+
const template = {
|
|
28570
28987
|
id: randomUUID3(),
|
|
28571
28988
|
name: input.name,
|
|
28572
28989
|
title_pattern: input.title_pattern,
|
|
@@ -28581,7 +28998,30 @@ async function createTemplate2(input, store, context) {
|
|
|
28581
28998
|
created_at: timestamp,
|
|
28582
28999
|
machine_id: store.machineId(context),
|
|
28583
29000
|
synced_at: null
|
|
28584
|
-
}
|
|
29001
|
+
};
|
|
29002
|
+
const tasks = buildTemplateTasks(template.id, input.tasks ?? [], timestamp);
|
|
29003
|
+
await store.createTemplateWithTasks(template, tasks, context);
|
|
29004
|
+
return template;
|
|
29005
|
+
}
|
|
29006
|
+
function buildTemplateTasks(templateId, inputs, timestamp) {
|
|
29007
|
+
return inputs.map((input, position) => ({
|
|
29008
|
+
id: randomUUID3(),
|
|
29009
|
+
template_id: templateId,
|
|
29010
|
+
position,
|
|
29011
|
+
title_pattern: input.title_pattern,
|
|
29012
|
+
description: input.description ?? null,
|
|
29013
|
+
priority: input.priority ?? "medium",
|
|
29014
|
+
tags: input.tags ?? [],
|
|
29015
|
+
task_type: input.task_type ?? null,
|
|
29016
|
+
condition: input.condition ?? null,
|
|
29017
|
+
include_template_id: input.include_template_id ?? null,
|
|
29018
|
+
depends_on_positions: input.depends_on ?? [],
|
|
29019
|
+
metadata: input.metadata ?? {},
|
|
29020
|
+
created_at: timestamp
|
|
29021
|
+
}));
|
|
29022
|
+
}
|
|
29023
|
+
async function deleteTemplate2(id, store, context) {
|
|
29024
|
+
return store.deleteTemplateWithTasks(id, context);
|
|
28585
29025
|
}
|
|
28586
29026
|
async function updateTemplate2(id, input, store) {
|
|
28587
29027
|
const template = await store.get("templates", id);
|
|
@@ -28637,6 +29077,7 @@ async function exportSnapshot(store) {
|
|
|
28637
29077
|
agents: await store.list("agents"),
|
|
28638
29078
|
taskLists: await store.list("task_lists"),
|
|
28639
29079
|
templates: await store.list("templates"),
|
|
29080
|
+
templateTasks: await store.list("template_tasks"),
|
|
28640
29081
|
auditHistory: await store.list("audit_history"),
|
|
28641
29082
|
tombstones: await store.listTombstones()
|
|
28642
29083
|
};
|
|
@@ -28661,6 +29102,7 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
28661
29102
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
28662
29103
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
28663
29104
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
29105
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row]),
|
|
28664
29106
|
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
28665
29107
|
];
|
|
28666
29108
|
for (const [type, row] of entries) {
|
|
@@ -29070,12 +29512,16 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29070
29512
|
TaskList: taskListSchema,
|
|
29071
29513
|
TaskComment: taskCommentSchema,
|
|
29072
29514
|
Plan: planSchema,
|
|
29515
|
+
Template: templateSchema,
|
|
29516
|
+
TemplateTask: templateTaskSchema,
|
|
29517
|
+
TemplateVariable: templateVariableSchema,
|
|
29518
|
+
CreateTemplateTaskInput: createTemplateTaskInputSchema,
|
|
29073
29519
|
CreateTaskInput: {
|
|
29074
29520
|
type: "object",
|
|
29075
29521
|
required: ["title"],
|
|
29076
29522
|
properties: {
|
|
29077
29523
|
title: { type: "string" },
|
|
29078
|
-
description: { type: "string" },
|
|
29524
|
+
description: { type: "string", nullable: true },
|
|
29079
29525
|
status: { type: "string" },
|
|
29080
29526
|
priority: { type: "string" },
|
|
29081
29527
|
project_id: { type: "string" },
|
|
@@ -29092,6 +29538,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29092
29538
|
status: { type: "string" },
|
|
29093
29539
|
priority: { type: "string" },
|
|
29094
29540
|
assigned_to: { type: "string" },
|
|
29541
|
+
project_id: { type: "string", nullable: true },
|
|
29542
|
+
task_list_id: { type: "string", nullable: true },
|
|
29095
29543
|
version: { type: "number" }
|
|
29096
29544
|
}
|
|
29097
29545
|
},
|
|
@@ -29208,6 +29656,39 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29208
29656
|
agent_id: { type: "string", minLength: 1 },
|
|
29209
29657
|
status: { type: "string", enum: ["active", "completed", "archived"] }
|
|
29210
29658
|
}
|
|
29659
|
+
},
|
|
29660
|
+
CreateTemplateInput: {
|
|
29661
|
+
type: "object",
|
|
29662
|
+
additionalProperties: false,
|
|
29663
|
+
required: ["name", "title_pattern"],
|
|
29664
|
+
properties: {
|
|
29665
|
+
name: { type: "string", minLength: 1 },
|
|
29666
|
+
title_pattern: { type: "string", minLength: 1 },
|
|
29667
|
+
description: { type: "string", nullable: true },
|
|
29668
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
29669
|
+
tags: { type: "array", items: { type: "string", minLength: 1 } },
|
|
29670
|
+
variables: { type: "array", items: { $ref: "#/components/schemas/TemplateVariable" } },
|
|
29671
|
+
project_id: { type: "string", minLength: 1, nullable: true },
|
|
29672
|
+
plan_id: { type: "string", minLength: 1, nullable: true },
|
|
29673
|
+
metadata: { type: "object", additionalProperties: true },
|
|
29674
|
+
tasks: { type: "array", items: { $ref: "#/components/schemas/CreateTemplateTaskInput" } }
|
|
29675
|
+
}
|
|
29676
|
+
},
|
|
29677
|
+
UpdateTemplateInput: {
|
|
29678
|
+
type: "object",
|
|
29679
|
+
additionalProperties: false,
|
|
29680
|
+
minProperties: 1,
|
|
29681
|
+
properties: {
|
|
29682
|
+
name: { type: "string", minLength: 1 },
|
|
29683
|
+
title_pattern: { type: "string", minLength: 1 },
|
|
29684
|
+
description: { type: "string", nullable: true },
|
|
29685
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
29686
|
+
tags: { type: "array", items: { type: "string", minLength: 1 } },
|
|
29687
|
+
variables: { type: "array", items: { type: "object" } },
|
|
29688
|
+
project_id: { type: "string", nullable: true },
|
|
29689
|
+
plan_id: { type: "string", nullable: true },
|
|
29690
|
+
metadata: { type: "object", additionalProperties: true }
|
|
29691
|
+
}
|
|
29211
29692
|
}
|
|
29212
29693
|
}
|
|
29213
29694
|
},
|
|
@@ -29510,6 +29991,41 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29510
29991
|
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
|
|
29511
29992
|
}
|
|
29512
29993
|
},
|
|
29994
|
+
"/v1/templates": {
|
|
29995
|
+
get: {
|
|
29996
|
+
operationId: "listTemplates",
|
|
29997
|
+
summary: "List reusable task templates",
|
|
29998
|
+
parameters: [{ name: "project_id", in: "query", schema: { type: "string" } }],
|
|
29999
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { templates: { type: "array", items: { $ref: "#/components/schemas/Template" } }, count: { type: "number" } } } } } } }
|
|
30000
|
+
},
|
|
30001
|
+
post: {
|
|
30002
|
+
operationId: "createTemplate",
|
|
30003
|
+
summary: "Create a reusable task template",
|
|
30004
|
+
requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateTemplateInput" } } } },
|
|
30005
|
+
responses: { "201": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
|
|
30006
|
+
}
|
|
30007
|
+
},
|
|
30008
|
+
"/v1/templates/{id}": {
|
|
30009
|
+
get: {
|
|
30010
|
+
operationId: "getTemplate",
|
|
30011
|
+
summary: "Get one reusable task template with its checklist steps",
|
|
30012
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
30013
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
|
|
30014
|
+
},
|
|
30015
|
+
patch: {
|
|
30016
|
+
operationId: "updateTemplate",
|
|
30017
|
+
summary: "Update reusable template metadata and defaults",
|
|
30018
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
30019
|
+
requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateTemplateInput" } } } },
|
|
30020
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
|
|
30021
|
+
},
|
|
30022
|
+
delete: {
|
|
30023
|
+
operationId: "deleteTemplate",
|
|
30024
|
+
summary: "Delete a reusable task template and its checklist steps",
|
|
30025
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
30026
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
|
|
30027
|
+
}
|
|
30028
|
+
},
|
|
29513
30029
|
"/v1/task-lists": {
|
|
29514
30030
|
get: {
|
|
29515
30031
|
operationId: "listTaskLists",
|
|
@@ -29587,6 +30103,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29587
30103
|
agents: { type: "array", items: { type: "object" } },
|
|
29588
30104
|
taskLists: { type: "array", items: { type: "object" } },
|
|
29589
30105
|
templates: { type: "array", items: { type: "object" } },
|
|
30106
|
+
templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
|
|
29590
30107
|
auditHistory: { type: "array", items: { type: "object" } },
|
|
29591
30108
|
tombstones: { type: "array", items: { type: "object" } }
|
|
29592
30109
|
}
|
|
@@ -29623,7 +30140,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29623
30140
|
}
|
|
29624
30141
|
};
|
|
29625
30142
|
}
|
|
29626
|
-
var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema;
|
|
30143
|
+
var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
29627
30144
|
var init_openapi = __esm(() => {
|
|
29628
30145
|
init_package_version();
|
|
29629
30146
|
taskSchema = {
|
|
@@ -29700,6 +30217,72 @@ var init_openapi = __esm(() => {
|
|
|
29700
30217
|
updated_at: { type: "string", format: "date-time" }
|
|
29701
30218
|
}
|
|
29702
30219
|
};
|
|
30220
|
+
templateTaskSchema = {
|
|
30221
|
+
type: "object",
|
|
30222
|
+
required: ["id", "template_id", "position", "title_pattern", "priority", "tags", "depends_on_positions", "metadata", "created_at"],
|
|
30223
|
+
properties: {
|
|
30224
|
+
id: { type: "string" },
|
|
30225
|
+
template_id: { type: "string" },
|
|
30226
|
+
position: { type: "integer", minimum: 0 },
|
|
30227
|
+
title_pattern: { type: "string" },
|
|
30228
|
+
description: { type: "string", nullable: true },
|
|
30229
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
30230
|
+
tags: { type: "array", items: { type: "string" } },
|
|
30231
|
+
task_type: { type: "string", nullable: true },
|
|
30232
|
+
condition: { type: "string", nullable: true },
|
|
30233
|
+
include_template_id: { type: "string", nullable: true },
|
|
30234
|
+
depends_on_positions: { type: "array", items: { type: "integer", minimum: 0 } },
|
|
30235
|
+
metadata: { type: "object", additionalProperties: true },
|
|
30236
|
+
created_at: { type: "string", format: "date-time" }
|
|
30237
|
+
}
|
|
30238
|
+
};
|
|
30239
|
+
templateSchema = {
|
|
30240
|
+
type: "object",
|
|
30241
|
+
required: ["id", "name", "title_pattern", "priority", "tags", "variables", "version", "metadata", "created_at"],
|
|
30242
|
+
properties: {
|
|
30243
|
+
id: { type: "string" },
|
|
30244
|
+
name: { type: "string" },
|
|
30245
|
+
title_pattern: { type: "string" },
|
|
30246
|
+
description: { type: "string", nullable: true },
|
|
30247
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
30248
|
+
tags: { type: "array", items: { type: "string" } },
|
|
30249
|
+
variables: { type: "array", items: { type: "object", properties: { name: { type: "string" }, required: { type: "boolean" }, default: { type: "string" }, description: { type: "string" } } } },
|
|
30250
|
+
version: { type: "integer", minimum: 1 },
|
|
30251
|
+
project_id: { type: "string", nullable: true },
|
|
30252
|
+
plan_id: { type: "string", nullable: true },
|
|
30253
|
+
metadata: { type: "object", additionalProperties: true },
|
|
30254
|
+
created_at: { type: "string", format: "date-time" },
|
|
30255
|
+
tasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } }
|
|
30256
|
+
}
|
|
30257
|
+
};
|
|
30258
|
+
templateVariableSchema = {
|
|
30259
|
+
type: "object",
|
|
30260
|
+
required: ["name", "required"],
|
|
30261
|
+
properties: {
|
|
30262
|
+
name: { type: "string" },
|
|
30263
|
+
required: { type: "boolean" },
|
|
30264
|
+
default: { type: "string" },
|
|
30265
|
+
description: { type: "string" }
|
|
30266
|
+
}
|
|
30267
|
+
};
|
|
30268
|
+
createTemplateTaskInputSchema = {
|
|
30269
|
+
type: "object",
|
|
30270
|
+
additionalProperties: false,
|
|
30271
|
+
required: ["title_pattern"],
|
|
30272
|
+
properties: {
|
|
30273
|
+
position: { type: "integer", minimum: 0 },
|
|
30274
|
+
title_pattern: { type: "string", minLength: 1 },
|
|
30275
|
+
description: { type: "string", nullable: true },
|
|
30276
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
30277
|
+
tags: { type: "array", items: { type: "string", minLength: 1 } },
|
|
30278
|
+
task_type: { type: "string", nullable: true },
|
|
30279
|
+
condition: { type: "string", nullable: true },
|
|
30280
|
+
include_template_id: { type: "string", nullable: true },
|
|
30281
|
+
depends_on: { type: "array", items: { type: "integer", minimum: 0 } },
|
|
30282
|
+
depends_on_positions: { type: "array", items: { type: "integer", minimum: 0 } },
|
|
30283
|
+
metadata: { type: "object", additionalProperties: true }
|
|
30284
|
+
}
|
|
30285
|
+
};
|
|
29703
30286
|
});
|
|
29704
30287
|
|
|
29705
30288
|
// src/server/v1.ts
|
|
@@ -29839,6 +30422,122 @@ function validatePlanCreate(value) {
|
|
|
29839
30422
|
}
|
|
29840
30423
|
};
|
|
29841
30424
|
}
|
|
30425
|
+
function validateTemplateTask(value) {
|
|
30426
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
30427
|
+
return null;
|
|
30428
|
+
const body = value;
|
|
30429
|
+
const allowed = new Set(["position", "title_pattern", "description", "priority", "tags", "task_type", "condition", "include_template_id", "depends_on", "depends_on_positions", "metadata"]);
|
|
30430
|
+
if (Object.keys(body).some((key) => !allowed.has(key)))
|
|
30431
|
+
return null;
|
|
30432
|
+
if (typeof body.title_pattern !== "string" || !body.title_pattern.trim())
|
|
30433
|
+
return null;
|
|
30434
|
+
if (body.position !== undefined && (typeof body.position !== "number" || !Number.isSafeInteger(body.position) || body.position < 0))
|
|
30435
|
+
return null;
|
|
30436
|
+
if (body.description !== undefined && body.description !== null && typeof body.description !== "string")
|
|
30437
|
+
return null;
|
|
30438
|
+
if (body.priority !== undefined && (typeof body.priority !== "string" || !["low", "medium", "high", "critical"].includes(body.priority)))
|
|
30439
|
+
return null;
|
|
30440
|
+
if (body.tags !== undefined && (!Array.isArray(body.tags) || body.tags.some((tag) => typeof tag !== "string" || !tag.trim())))
|
|
30441
|
+
return null;
|
|
30442
|
+
for (const field of ["task_type", "condition", "include_template_id"]) {
|
|
30443
|
+
if (body[field] !== undefined && body[field] !== null && (typeof body[field] !== "string" || !body[field].trim()))
|
|
30444
|
+
return null;
|
|
30445
|
+
}
|
|
30446
|
+
if (body.depends_on !== undefined && body.depends_on_positions !== undefined)
|
|
30447
|
+
return null;
|
|
30448
|
+
const dependencies = body.depends_on ?? body.depends_on_positions;
|
|
30449
|
+
if (dependencies !== undefined && (!Array.isArray(dependencies) || dependencies.some((position) => !Number.isSafeInteger(position) || position < 0)))
|
|
30450
|
+
return null;
|
|
30451
|
+
if (body.metadata !== undefined && (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata)))
|
|
30452
|
+
return null;
|
|
30453
|
+
return {
|
|
30454
|
+
title_pattern: body.title_pattern,
|
|
30455
|
+
...typeof body.description === "string" ? { description: body.description } : {},
|
|
30456
|
+
...typeof body.priority === "string" ? { priority: body.priority } : {},
|
|
30457
|
+
...Array.isArray(body.tags) ? { tags: body.tags } : {},
|
|
30458
|
+
...typeof body.task_type === "string" ? { task_type: body.task_type } : {},
|
|
30459
|
+
...typeof body.condition === "string" ? { condition: body.condition } : {},
|
|
30460
|
+
...typeof body.include_template_id === "string" ? { include_template_id: body.include_template_id } : {},
|
|
30461
|
+
...Array.isArray(dependencies) ? { depends_on: dependencies } : {},
|
|
30462
|
+
...body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? { metadata: body.metadata } : {}
|
|
30463
|
+
};
|
|
30464
|
+
}
|
|
30465
|
+
function validateTemplateCreate(value) {
|
|
30466
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
30467
|
+
return { ok: false, message: "template body must be an object" };
|
|
30468
|
+
const body = value;
|
|
30469
|
+
const allowed = new Set(["name", "title_pattern", "description", "priority", "tags", "variables", "project_id", "plan_id", "metadata", "tasks"]);
|
|
30470
|
+
const unknown = Object.keys(body).find((key) => !allowed.has(key));
|
|
30471
|
+
if (unknown)
|
|
30472
|
+
return { ok: false, message: `unknown template field: ${unknown}` };
|
|
30473
|
+
if (typeof body.name !== "string" || !body.name.trim())
|
|
30474
|
+
return { ok: false, message: "name must be a non-empty string" };
|
|
30475
|
+
if (typeof body.title_pattern !== "string" || !body.title_pattern.trim())
|
|
30476
|
+
return { ok: false, message: "title_pattern must be a non-empty string" };
|
|
30477
|
+
if (body.description !== undefined && body.description !== null && typeof body.description !== "string")
|
|
30478
|
+
return { ok: false, message: "description must be a string or null" };
|
|
30479
|
+
if (body.priority !== undefined && (typeof body.priority !== "string" || !["low", "medium", "high", "critical"].includes(body.priority)))
|
|
30480
|
+
return { ok: false, message: "priority must be low, medium, high, or critical" };
|
|
30481
|
+
if (body.tags !== undefined && (!Array.isArray(body.tags) || body.tags.some((tag) => typeof tag !== "string" || !tag.trim())))
|
|
30482
|
+
return { ok: false, message: "tags must be an array of non-empty strings" };
|
|
30483
|
+
if (body.variables !== undefined && (!Array.isArray(body.variables) || body.variables.some((variable) => !variable || typeof variable !== "object" || Array.isArray(variable) || typeof variable.name !== "string" || !variable.name || typeof variable.required !== "boolean" || variable.default !== undefined && typeof variable.default !== "string" || variable.description !== undefined && typeof variable.description !== "string"))) {
|
|
30484
|
+
return { ok: false, message: "variables must be valid template variable objects" };
|
|
30485
|
+
}
|
|
30486
|
+
for (const field of ["project_id", "plan_id"]) {
|
|
30487
|
+
if (body[field] !== undefined && body[field] !== null && (typeof body[field] !== "string" || !body[field].trim()))
|
|
30488
|
+
return { ok: false, message: `${field} must be a non-empty string or null` };
|
|
30489
|
+
}
|
|
30490
|
+
if (body.metadata !== undefined && (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata)))
|
|
30491
|
+
return { ok: false, message: "metadata must be an object" };
|
|
30492
|
+
const tasks = body.tasks === undefined ? [] : Array.isArray(body.tasks) ? body.tasks.map(validateTemplateTask) : null;
|
|
30493
|
+
if (tasks === null || tasks.some((task) => task === null))
|
|
30494
|
+
return { ok: false, message: "tasks must be valid template task objects" };
|
|
30495
|
+
const taskInputs = tasks;
|
|
30496
|
+
for (const [position, task] of taskInputs.entries()) {
|
|
30497
|
+
if ((task.depends_on ?? []).some((dependency) => dependency >= position)) {
|
|
30498
|
+
return { ok: false, message: "template task dependencies must reference earlier task positions" };
|
|
30499
|
+
}
|
|
30500
|
+
}
|
|
30501
|
+
return {
|
|
30502
|
+
ok: true,
|
|
30503
|
+
input: {
|
|
30504
|
+
name: body.name,
|
|
30505
|
+
title_pattern: body.title_pattern,
|
|
30506
|
+
...typeof body.description === "string" ? { description: body.description } : {},
|
|
30507
|
+
...typeof body.priority === "string" ? { priority: body.priority } : {},
|
|
30508
|
+
...Array.isArray(body.tags) ? { tags: body.tags } : {},
|
|
30509
|
+
...Array.isArray(body.variables) ? { variables: body.variables } : {},
|
|
30510
|
+
...typeof body.project_id === "string" ? { project_id: body.project_id } : {},
|
|
30511
|
+
...typeof body.plan_id === "string" ? { plan_id: body.plan_id } : {},
|
|
30512
|
+
...body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? { metadata: body.metadata } : {},
|
|
30513
|
+
tasks: taskInputs
|
|
30514
|
+
}
|
|
30515
|
+
};
|
|
30516
|
+
}
|
|
30517
|
+
function validateTemplatePatch(value) {
|
|
30518
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
30519
|
+
return { ok: false, message: "template patch must be an object" };
|
|
30520
|
+
const body = value;
|
|
30521
|
+
const allowed = new Set(["name", "title_pattern", "description", "priority", "tags", "variables", "project_id", "plan_id", "metadata"]);
|
|
30522
|
+
const unknown = Object.keys(body).find((key) => !allowed.has(key));
|
|
30523
|
+
if (unknown)
|
|
30524
|
+
return { ok: false, message: `unknown template field: ${unknown}` };
|
|
30525
|
+
if (Object.keys(body).length === 0)
|
|
30526
|
+
return { ok: false, message: "template patch must not be empty" };
|
|
30527
|
+
const templateLike = { name: body.name ?? "template", title_pattern: body.title_pattern ?? "template", ...body };
|
|
30528
|
+
const validated = validateTemplateCreate(templateLike);
|
|
30529
|
+
if (!validated.ok)
|
|
30530
|
+
return validated;
|
|
30531
|
+
const { name: _name, title_pattern: _title, tasks: _tasks, ...patch } = validated.input;
|
|
30532
|
+
return { ok: true, patch: {
|
|
30533
|
+
...body.name !== undefined ? { name: validated.input.name } : {},
|
|
30534
|
+
...body.title_pattern !== undefined ? { title_pattern: validated.input.title_pattern } : {},
|
|
30535
|
+
...patch,
|
|
30536
|
+
...body.description === null ? { description: null } : {},
|
|
30537
|
+
...body.project_id === null ? { project_id: null } : {},
|
|
30538
|
+
...body.plan_id === null ? { plan_id: null } : {}
|
|
30539
|
+
} };
|
|
30540
|
+
}
|
|
29842
30541
|
async function readJson(req) {
|
|
29843
30542
|
try {
|
|
29844
30543
|
const text = await req.text();
|
|
@@ -29899,12 +30598,13 @@ function normalizeImportSnapshot(raw) {
|
|
|
29899
30598
|
agents: arr(body["agents"]),
|
|
29900
30599
|
taskLists: arr(body["taskLists"]),
|
|
29901
30600
|
templates: arr(body["templates"]),
|
|
30601
|
+
templateTasks: arr(body["templateTasks"]),
|
|
29902
30602
|
auditHistory: arr(body["auditHistory"]),
|
|
29903
30603
|
tombstones: arr(body["tombstones"])
|
|
29904
30604
|
};
|
|
29905
30605
|
}
|
|
29906
30606
|
function countSnapshotRecords(s) {
|
|
29907
|
-
return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
|
|
30607
|
+
return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.templateTasks.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
|
|
29908
30608
|
}
|
|
29909
30609
|
async function handleV1Request(req, url, dependencies = {}) {
|
|
29910
30610
|
const path = url.pathname;
|
|
@@ -30474,6 +31174,40 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
30474
31174
|
if (id)
|
|
30475
31175
|
return error(405, `method ${method} not allowed on /v1/plans/:id`);
|
|
30476
31176
|
}
|
|
31177
|
+
if (resource === "templates") {
|
|
31178
|
+
if (!id && method === "GET") {
|
|
31179
|
+
const projectId = url.searchParams.get("project_id");
|
|
31180
|
+
const templates = (await store.templates.list()).filter((template) => projectId === null || template.project_id === projectId);
|
|
31181
|
+
return json2({ templates, count: templates.length });
|
|
31182
|
+
}
|
|
31183
|
+
if (!id && method === "POST") {
|
|
31184
|
+
const body = await readJson(req);
|
|
31185
|
+
const validated = validateTemplateCreate(body);
|
|
31186
|
+
if (!validated.ok)
|
|
31187
|
+
return error(400, validated.message);
|
|
31188
|
+
const template = await store.templates.create(validated.input, contextFromPrincipal(principal));
|
|
31189
|
+
return json2({ template: await store.templates.getWithTasks(template.id) }, 201);
|
|
31190
|
+
}
|
|
31191
|
+
if (!id)
|
|
31192
|
+
return error(405, `method ${method} not allowed on /v1/templates`);
|
|
31193
|
+
if (method === "GET") {
|
|
31194
|
+
const template = await store.templates.getWithTasks(id);
|
|
31195
|
+
return template ? json2({ template }) : error(404, "template not found");
|
|
31196
|
+
}
|
|
31197
|
+
if (method === "PATCH" || method === "PUT") {
|
|
31198
|
+
const body = await readJson(req);
|
|
31199
|
+
const validated = validateTemplatePatch(body);
|
|
31200
|
+
if (!validated.ok)
|
|
31201
|
+
return error(400, validated.message);
|
|
31202
|
+
const template = await store.templates.update(id, validated.patch, contextFromPrincipal(principal));
|
|
31203
|
+
return template ? json2({ template: await store.templates.getWithTasks(id) }) : error(404, "template not found");
|
|
31204
|
+
}
|
|
31205
|
+
if (method === "DELETE") {
|
|
31206
|
+
const deleted = await store.templates.delete(id, contextFromPrincipal(principal));
|
|
31207
|
+
return deleted ? json2({ deleted: true, id }) : error(404, "template not found");
|
|
31208
|
+
}
|
|
31209
|
+
return error(405, `method ${method} not allowed on /v1/templates/:id`);
|
|
31210
|
+
}
|
|
30477
31211
|
if (resource === "agents") {
|
|
30478
31212
|
if (!id && method === "GET") {
|
|
30479
31213
|
const agents = await store.agents.list();
|
|
@@ -35450,6 +36184,17 @@ ${task.description}` : null
|
|
|
35450
36184
|
patch.estimated_minutes = estimate;
|
|
35451
36185
|
if (deadline !== undefined)
|
|
35452
36186
|
patch.due_at = deadline;
|
|
36187
|
+
if (typeof patch.project_id === "string" && patch.project_id) {
|
|
36188
|
+
patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
|
|
36189
|
+
}
|
|
36190
|
+
if (typeof patch.task_list_id === "string" && patch.task_list_id) {
|
|
36191
|
+
let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
|
|
36192
|
+
if (!scope) {
|
|
36193
|
+
const current = await cloudGetTask(cloud, task_id2);
|
|
36194
|
+
scope = current?.project_id ?? undefined;
|
|
36195
|
+
}
|
|
36196
|
+
patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
|
|
36197
|
+
}
|
|
35453
36198
|
if (version2 !== undefined)
|
|
35454
36199
|
patch.version = version2;
|
|
35455
36200
|
const updated = await cloudUpdateTask(cloud, task_id2, patch);
|
|
@@ -39667,6 +40412,63 @@ function registerTaskProjectTools(server, ctx) {
|
|
|
39667
40412
|
}
|
|
39668
40413
|
});
|
|
39669
40414
|
}
|
|
40415
|
+
if (shouldRegisterTool("move_task")) {
|
|
40416
|
+
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.", {
|
|
40417
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
40418
|
+
to_project: exports_external.string().optional().describe("Destination project ID, slug, or path"),
|
|
40419
|
+
to_list: exports_external.string().optional().describe("Destination task list (UUID, or slug resolved in the destination project)"),
|
|
40420
|
+
clear_list: exports_external.boolean().optional().describe("Detach from its task list (set task_list_id to null)"),
|
|
40421
|
+
version: exports_external.number().optional().describe("Expected version for optimistic locking")
|
|
40422
|
+
}, async ({ task_id, to_project, to_list, clear_list, version }) => {
|
|
40423
|
+
try {
|
|
40424
|
+
if (!to_project && !to_list && !clear_list) {
|
|
40425
|
+
throw new Error("Nothing to move: pass to_project, to_list, or clear_list.");
|
|
40426
|
+
}
|
|
40427
|
+
if (to_list && clear_list) {
|
|
40428
|
+
throw new Error("Use either to_list or clear_list, not both.");
|
|
40429
|
+
}
|
|
40430
|
+
const cloud = getTodosCloudClient();
|
|
40431
|
+
if (cloud) {
|
|
40432
|
+
const current2 = await cloudGetTask(cloud, task_id);
|
|
40433
|
+
if (!current2)
|
|
40434
|
+
throw new Error(`Task not found: ${task_id}`);
|
|
40435
|
+
const targetProjectId2 = to_project ? await cloudResolveProjectRef(cloud, to_project) : undefined;
|
|
40436
|
+
const scope = targetProjectId2 ?? current2.project_id ?? undefined;
|
|
40437
|
+
const patch = {};
|
|
40438
|
+
if (targetProjectId2 !== undefined)
|
|
40439
|
+
patch.project_id = targetProjectId2;
|
|
40440
|
+
if (to_list)
|
|
40441
|
+
patch.task_list_id = await cloudResolveTaskListRef(cloud, to_list, scope);
|
|
40442
|
+
else if (clear_list)
|
|
40443
|
+
patch.task_list_id = null;
|
|
40444
|
+
else if (targetProjectId2 && targetProjectId2 !== current2.project_id)
|
|
40445
|
+
patch.task_list_id = null;
|
|
40446
|
+
if (version !== undefined)
|
|
40447
|
+
patch.version = version;
|
|
40448
|
+
const task2 = await cloudUpdateTask(cloud, task_id, patch);
|
|
40449
|
+
return { content: [{ type: "text", text: formatTask(task2) }] };
|
|
40450
|
+
}
|
|
40451
|
+
const resolvedId = resolveId(task_id);
|
|
40452
|
+
const current = getTask(resolvedId);
|
|
40453
|
+
if (!current)
|
|
40454
|
+
throw new Error(`Task not found: ${task_id}`);
|
|
40455
|
+
const targetProjectId = to_project ? resolveId(to_project, "projects") : undefined;
|
|
40456
|
+
const updates = {};
|
|
40457
|
+
if (targetProjectId !== undefined)
|
|
40458
|
+
updates.project_id = targetProjectId;
|
|
40459
|
+
if (to_list)
|
|
40460
|
+
updates.task_list_id = resolveId(to_list, "task_lists");
|
|
40461
|
+
else if (clear_list)
|
|
40462
|
+
updates.task_list_id = null;
|
|
40463
|
+
else if (targetProjectId && targetProjectId !== current.project_id)
|
|
40464
|
+
updates.task_list_id = null;
|
|
40465
|
+
const task = updateWithOptionalVersion(resolvedId, updates, version);
|
|
40466
|
+
return { content: [{ type: "text", text: formatTask(task) }] };
|
|
40467
|
+
} catch (e) {
|
|
40468
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
40469
|
+
}
|
|
40470
|
+
});
|
|
40471
|
+
}
|
|
39670
40472
|
if (shouldRegisterTool("reschedule_task")) {
|
|
39671
40473
|
server.tool("reschedule_task", "Update a task's deadline.", {
|
|
39672
40474
|
task_id: exports_external.string().describe("Task ID"),
|
|
@@ -43595,6 +44397,7 @@ function registerTaskMetaTools(server, ctx) {
|
|
|
43595
44397
|
complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
|
|
43596
44398
|
cancel_task: "cancel_task \u2014 Cancel a task. Params: task_id, version",
|
|
43597
44399
|
reassign_task: "reassign_task \u2014 Change task assignee. Params: task_id, new_assignee, version",
|
|
44400
|
+
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",
|
|
43598
44401
|
reschedule_task: "reschedule_task \u2014 Update deadline. Params: task_id, deadline, version",
|
|
43599
44402
|
prioritize_task: "prioritize_task \u2014 Set priority. Params: task_id, priority, version",
|
|
43600
44403
|
search_tasks: "search_tasks \u2014 Full-text search. Params: query, project_id, status, limit",
|
|
@@ -53634,9 +54437,9 @@ ${task2.id.slice(0, 8)} | ${task2.priority} | ${task2.title}` }] };
|
|
|
53634
54437
|
if (shouldRegisterTool("delete_template")) {
|
|
53635
54438
|
server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
|
|
53636
54439
|
try {
|
|
53637
|
-
const { deleteTemplate:
|
|
54440
|
+
const { deleteTemplate: deleteTemplate3 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
53638
54441
|
const resolvedId = resolveId(id, "task_templates");
|
|
53639
|
-
const deleted =
|
|
54442
|
+
const deleted = deleteTemplate3(resolvedId);
|
|
53640
54443
|
return { content: [{ type: "text", text: deleted ? "Template deleted." : "Template not found." }] };
|
|
53641
54444
|
} catch (e) {
|
|
53642
54445
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
@@ -69084,6 +69887,7 @@ function emptySnapshot2() {
|
|
|
69084
69887
|
agents: [],
|
|
69085
69888
|
taskLists: [],
|
|
69086
69889
|
templates: [],
|
|
69890
|
+
templateTasks: [],
|
|
69087
69891
|
auditHistory: [],
|
|
69088
69892
|
tombstones: []
|
|
69089
69893
|
};
|
|
@@ -69272,6 +70076,7 @@ var init_shadow = __esm(() => {
|
|
|
69272
70076
|
agents: "agents",
|
|
69273
70077
|
taskLists: "task_lists",
|
|
69274
70078
|
templates: "templates",
|
|
70079
|
+
templateTasks: "template_tasks",
|
|
69275
70080
|
auditHistory: "audit_history"
|
|
69276
70081
|
};
|
|
69277
70082
|
});
|
|
@@ -71150,6 +71955,7 @@ var REGISTERED_CANONICAL_COMMANDS = [
|
|
|
71150
71955
|
"manual",
|
|
71151
71956
|
"mcp",
|
|
71152
71957
|
"mine",
|
|
71958
|
+
"move",
|
|
71153
71959
|
"next",
|
|
71154
71960
|
"notifications",
|
|
71155
71961
|
"onboarding",
|
|
@@ -71285,6 +72091,7 @@ var REMOTE_COMMANDS = new Set([
|
|
|
71285
72091
|
"lists",
|
|
71286
72092
|
"lock",
|
|
71287
72093
|
"log-progress",
|
|
72094
|
+
"move",
|
|
71288
72095
|
"next",
|
|
71289
72096
|
"plans",
|
|
71290
72097
|
"project-rename",
|
|
@@ -71300,6 +72107,8 @@ var REMOTE_COMMANDS = new Set([
|
|
|
71300
72107
|
"task",
|
|
71301
72108
|
"task-lists",
|
|
71302
72109
|
"timeline",
|
|
72110
|
+
"template-import",
|
|
72111
|
+
"templates",
|
|
71303
72112
|
"tl",
|
|
71304
72113
|
"unlock",
|
|
71305
72114
|
"update"
|