@hasna/todos 0.11.94 → 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/index.js +696 -63
- 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 +169 -55
- package/dist/lib/template-semantics.d.ts +8 -0
- package/dist/lib/template-semantics.d.ts.map +1 -0
- package/dist/mcp/index.js +461 -59
- 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 +103 -1
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +467 -65
- package/dist/server/openapi.d.ts +488 -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 +168 -54
- 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();
|
|
@@ -16910,6 +16958,80 @@ __export(exports_plan_template_commands, {
|
|
|
16910
16958
|
registerPlanTemplateCommands: () => registerPlanTemplateCommands
|
|
16911
16959
|
});
|
|
16912
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
|
+
}
|
|
16913
17035
|
function resolvePlanCliRef(ref, projectId) {
|
|
16914
17036
|
const db = getDatabase();
|
|
16915
17037
|
const resolved = resolvePlanRefDetailed(ref, db, projectId);
|
|
@@ -17154,6 +17276,117 @@ function registerPlanTemplateCommands(program2) {
|
|
|
17154
17276
|
});
|
|
17155
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) => {
|
|
17156
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
|
+
}
|
|
17157
17390
|
const {
|
|
17158
17391
|
createTemplate: createTemplate2,
|
|
17159
17392
|
getTemplateWithTasks: getTemplateWithTasks2,
|
|
@@ -17406,7 +17639,6 @@ function registerPlanTemplateCommands(program2) {
|
|
|
17406
17639
|
});
|
|
17407
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) => {
|
|
17408
17641
|
const globalOpts = program2.opts();
|
|
17409
|
-
const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
17410
17642
|
const { readFileSync: readFileSync5 } = await import("fs");
|
|
17411
17643
|
try {
|
|
17412
17644
|
const filePath = file || opts.file;
|
|
@@ -17416,7 +17648,8 @@ function registerPlanTemplateCommands(program2) {
|
|
|
17416
17648
|
}
|
|
17417
17649
|
const content = readFileSync5(filePath, "utf-8");
|
|
17418
17650
|
const json = JSON.parse(content);
|
|
17419
|
-
const
|
|
17651
|
+
const cloud = getTodosCloudClient();
|
|
17652
|
+
const template = cloud ? await cloudCreateTemplate(cloud, json) : (await Promise.resolve().then(() => (init_templates(), exports_templates))).importTemplate(json);
|
|
17420
17653
|
if (globalOpts.json) {
|
|
17421
17654
|
output(template, true);
|
|
17422
17655
|
} else {
|
|
@@ -26311,6 +26544,7 @@ function exportSqliteTodosStorageSnapshot(db) {
|
|
|
26311
26544
|
agents: listAgents({ include_archived: true }, d),
|
|
26312
26545
|
taskLists: listTaskLists(undefined, d),
|
|
26313
26546
|
templates: listTemplates(d),
|
|
26547
|
+
templateTasks: listTemplates(d).flatMap((template) => getTemplateTasks(template.id, d)),
|
|
26314
26548
|
auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
|
|
26315
26549
|
tombstones: listStorageTombstones(d)
|
|
26316
26550
|
};
|
|
@@ -26360,6 +26594,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
26360
26594
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
26361
26595
|
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
26362
26596
|
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
26597
|
+
applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
|
|
26363
26598
|
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
|
|
26364
26599
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
26365
26600
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
@@ -26464,6 +26699,8 @@ function tableForTombstone(objectType) {
|
|
|
26464
26699
|
return "task_lists";
|
|
26465
26700
|
if (objectType === "templates")
|
|
26466
26701
|
return "task_templates";
|
|
26702
|
+
if (objectType === "template_tasks")
|
|
26703
|
+
return "template_tasks";
|
|
26467
26704
|
return "task_history";
|
|
26468
26705
|
}
|
|
26469
26706
|
function listRows(db, table, columns) {
|
|
@@ -26500,7 +26737,7 @@ function clockColumnsForTable(table) {
|
|
|
26500
26737
|
return ["created_at"];
|
|
26501
26738
|
return ["updated_at", "created_at"];
|
|
26502
26739
|
}
|
|
26503
|
-
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;
|
|
26504
26741
|
var init_sqlite_snapshot = __esm(() => {
|
|
26505
26742
|
init_database();
|
|
26506
26743
|
init_agents();
|
|
@@ -26594,6 +26831,21 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
26594
26831
|
"machine_id",
|
|
26595
26832
|
"synced_at"
|
|
26596
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
|
+
];
|
|
26597
26849
|
TASK_COLUMNS = [
|
|
26598
26850
|
"id",
|
|
26599
26851
|
"short_id",
|
|
@@ -26662,7 +26914,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
26662
26914
|
"created_at",
|
|
26663
26915
|
"machine_id"
|
|
26664
26916
|
];
|
|
26665
|
-
JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables"]);
|
|
26917
|
+
JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
|
|
26666
26918
|
BOOLEAN_COLUMNS = new Set(["requires_approval"]);
|
|
26667
26919
|
});
|
|
26668
26920
|
|
|
@@ -27052,6 +27304,7 @@ function snapshotEntries(snapshot) {
|
|
|
27052
27304
|
...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
|
|
27053
27305
|
...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
|
|
27054
27306
|
...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
|
|
27307
|
+
...(snapshot.templateTasks ?? []).map((payload) => entry("template_tasks", payload, snapshot.exportedAt)),
|
|
27055
27308
|
...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
|
|
27056
27309
|
...(snapshot.tombstones ?? []).map((tombstone) => ({
|
|
27057
27310
|
type: tombstone.object_type,
|
|
@@ -27103,6 +27356,7 @@ function rowsToSnapshot(rows) {
|
|
|
27103
27356
|
agents: [],
|
|
27104
27357
|
taskLists: [],
|
|
27105
27358
|
templates: [],
|
|
27359
|
+
templateTasks: [],
|
|
27106
27360
|
auditHistory: [],
|
|
27107
27361
|
tombstones: []
|
|
27108
27362
|
};
|
|
@@ -27137,6 +27391,8 @@ function rowsToSnapshot(rows) {
|
|
|
27137
27391
|
snapshot.taskLists.push(payload);
|
|
27138
27392
|
else if (row.object_type === "templates")
|
|
27139
27393
|
snapshot.templates.push(payload);
|
|
27394
|
+
else if (row.object_type === "template_tasks")
|
|
27395
|
+
snapshot.templateTasks.push(payload);
|
|
27140
27396
|
else if (row.object_type === "audit_history")
|
|
27141
27397
|
snapshot.auditHistory.push(payload);
|
|
27142
27398
|
}
|
|
@@ -27354,6 +27610,9 @@ class TodosShadowOutbox {
|
|
|
27354
27610
|
case "templates":
|
|
27355
27611
|
snapshot.templates.push(record);
|
|
27356
27612
|
break;
|
|
27613
|
+
case "template_tasks":
|
|
27614
|
+
snapshot.templateTasks.push(record);
|
|
27615
|
+
break;
|
|
27357
27616
|
case "audit_history":
|
|
27358
27617
|
snapshot.auditHistory.push(record);
|
|
27359
27618
|
break;
|
|
@@ -27420,6 +27679,7 @@ function emptySnapshot() {
|
|
|
27420
27679
|
agents: [],
|
|
27421
27680
|
taskLists: [],
|
|
27422
27681
|
templates: [],
|
|
27682
|
+
templateTasks: [],
|
|
27423
27683
|
auditHistory: [],
|
|
27424
27684
|
tombstones: []
|
|
27425
27685
|
};
|
|
@@ -27616,10 +27876,13 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
27616
27876
|
get: (id) => store.get("templates", id),
|
|
27617
27877
|
list: async () => (await store.list("templates")).sort((a, b) => a.name.localeCompare(b.name)),
|
|
27618
27878
|
update: (id, input) => updateTemplate2(id, input, store),
|
|
27619
|
-
delete: (id, context) =>
|
|
27879
|
+
delete: (id, context) => deleteTemplate2(id, store, context),
|
|
27620
27880
|
getWithTasks: async (id) => {
|
|
27621
27881
|
const template = await store.get("templates", id);
|
|
27622
|
-
|
|
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 };
|
|
27623
27886
|
}
|
|
27624
27887
|
},
|
|
27625
27888
|
audit: {
|
|
@@ -27907,6 +28170,57 @@ class PostgresJsonRecordStore {
|
|
|
27907
28170
|
}
|
|
27908
28171
|
return value;
|
|
27909
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
|
+
}
|
|
27910
28224
|
async completeTask(id, agentId, options) {
|
|
27911
28225
|
await this.ensureSchema();
|
|
27912
28226
|
const operationTimestamp = new Date().toISOString();
|
|
@@ -28669,7 +28983,7 @@ async function updateTaskList2(id, input, store) {
|
|
|
28669
28983
|
}
|
|
28670
28984
|
async function createTemplate2(input, store, context) {
|
|
28671
28985
|
const timestamp = new Date().toISOString();
|
|
28672
|
-
|
|
28986
|
+
const template = {
|
|
28673
28987
|
id: randomUUID3(),
|
|
28674
28988
|
name: input.name,
|
|
28675
28989
|
title_pattern: input.title_pattern,
|
|
@@ -28684,7 +28998,30 @@ async function createTemplate2(input, store, context) {
|
|
|
28684
28998
|
created_at: timestamp,
|
|
28685
28999
|
machine_id: store.machineId(context),
|
|
28686
29000
|
synced_at: null
|
|
28687
|
-
}
|
|
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);
|
|
28688
29025
|
}
|
|
28689
29026
|
async function updateTemplate2(id, input, store) {
|
|
28690
29027
|
const template = await store.get("templates", id);
|
|
@@ -28740,6 +29077,7 @@ async function exportSnapshot(store) {
|
|
|
28740
29077
|
agents: await store.list("agents"),
|
|
28741
29078
|
taskLists: await store.list("task_lists"),
|
|
28742
29079
|
templates: await store.list("templates"),
|
|
29080
|
+
templateTasks: await store.list("template_tasks"),
|
|
28743
29081
|
auditHistory: await store.list("audit_history"),
|
|
28744
29082
|
tombstones: await store.listTombstones()
|
|
28745
29083
|
};
|
|
@@ -28764,6 +29102,7 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
28764
29102
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
28765
29103
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
28766
29104
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
29105
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row]),
|
|
28767
29106
|
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
28768
29107
|
];
|
|
28769
29108
|
for (const [type, row] of entries) {
|
|
@@ -29173,12 +29512,16 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29173
29512
|
TaskList: taskListSchema,
|
|
29174
29513
|
TaskComment: taskCommentSchema,
|
|
29175
29514
|
Plan: planSchema,
|
|
29515
|
+
Template: templateSchema,
|
|
29516
|
+
TemplateTask: templateTaskSchema,
|
|
29517
|
+
TemplateVariable: templateVariableSchema,
|
|
29518
|
+
CreateTemplateTaskInput: createTemplateTaskInputSchema,
|
|
29176
29519
|
CreateTaskInput: {
|
|
29177
29520
|
type: "object",
|
|
29178
29521
|
required: ["title"],
|
|
29179
29522
|
properties: {
|
|
29180
29523
|
title: { type: "string" },
|
|
29181
|
-
description: { type: "string" },
|
|
29524
|
+
description: { type: "string", nullable: true },
|
|
29182
29525
|
status: { type: "string" },
|
|
29183
29526
|
priority: { type: "string" },
|
|
29184
29527
|
project_id: { type: "string" },
|
|
@@ -29313,6 +29656,39 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29313
29656
|
agent_id: { type: "string", minLength: 1 },
|
|
29314
29657
|
status: { type: "string", enum: ["active", "completed", "archived"] }
|
|
29315
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
|
+
}
|
|
29316
29692
|
}
|
|
29317
29693
|
}
|
|
29318
29694
|
},
|
|
@@ -29615,6 +29991,41 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29615
29991
|
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
|
|
29616
29992
|
}
|
|
29617
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
|
+
},
|
|
29618
30029
|
"/v1/task-lists": {
|
|
29619
30030
|
get: {
|
|
29620
30031
|
operationId: "listTaskLists",
|
|
@@ -29692,6 +30103,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29692
30103
|
agents: { type: "array", items: { type: "object" } },
|
|
29693
30104
|
taskLists: { type: "array", items: { type: "object" } },
|
|
29694
30105
|
templates: { type: "array", items: { type: "object" } },
|
|
30106
|
+
templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
|
|
29695
30107
|
auditHistory: { type: "array", items: { type: "object" } },
|
|
29696
30108
|
tombstones: { type: "array", items: { type: "object" } }
|
|
29697
30109
|
}
|
|
@@ -29728,7 +30140,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
29728
30140
|
}
|
|
29729
30141
|
};
|
|
29730
30142
|
}
|
|
29731
|
-
var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema;
|
|
30143
|
+
var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
29732
30144
|
var init_openapi = __esm(() => {
|
|
29733
30145
|
init_package_version();
|
|
29734
30146
|
taskSchema = {
|
|
@@ -29805,6 +30217,72 @@ var init_openapi = __esm(() => {
|
|
|
29805
30217
|
updated_at: { type: "string", format: "date-time" }
|
|
29806
30218
|
}
|
|
29807
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
|
+
};
|
|
29808
30286
|
});
|
|
29809
30287
|
|
|
29810
30288
|
// src/server/v1.ts
|
|
@@ -29944,6 +30422,122 @@ function validatePlanCreate(value) {
|
|
|
29944
30422
|
}
|
|
29945
30423
|
};
|
|
29946
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
|
+
}
|
|
29947
30541
|
async function readJson(req) {
|
|
29948
30542
|
try {
|
|
29949
30543
|
const text = await req.text();
|
|
@@ -30004,12 +30598,13 @@ function normalizeImportSnapshot(raw) {
|
|
|
30004
30598
|
agents: arr(body["agents"]),
|
|
30005
30599
|
taskLists: arr(body["taskLists"]),
|
|
30006
30600
|
templates: arr(body["templates"]),
|
|
30601
|
+
templateTasks: arr(body["templateTasks"]),
|
|
30007
30602
|
auditHistory: arr(body["auditHistory"]),
|
|
30008
30603
|
tombstones: arr(body["tombstones"])
|
|
30009
30604
|
};
|
|
30010
30605
|
}
|
|
30011
30606
|
function countSnapshotRecords(s) {
|
|
30012
|
-
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);
|
|
30013
30608
|
}
|
|
30014
30609
|
async function handleV1Request(req, url, dependencies = {}) {
|
|
30015
30610
|
const path = url.pathname;
|
|
@@ -30579,6 +31174,40 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
30579
31174
|
if (id)
|
|
30580
31175
|
return error(405, `method ${method} not allowed on /v1/plans/:id`);
|
|
30581
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
|
+
}
|
|
30582
31211
|
if (resource === "agents") {
|
|
30583
31212
|
if (!id && method === "GET") {
|
|
30584
31213
|
const agents = await store.agents.list();
|
|
@@ -53808,9 +54437,9 @@ ${task2.id.slice(0, 8)} | ${task2.priority} | ${task2.title}` }] };
|
|
|
53808
54437
|
if (shouldRegisterTool("delete_template")) {
|
|
53809
54438
|
server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
|
|
53810
54439
|
try {
|
|
53811
|
-
const { deleteTemplate:
|
|
54440
|
+
const { deleteTemplate: deleteTemplate3 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
53812
54441
|
const resolvedId = resolveId(id, "task_templates");
|
|
53813
|
-
const deleted =
|
|
54442
|
+
const deleted = deleteTemplate3(resolvedId);
|
|
53814
54443
|
return { content: [{ type: "text", text: deleted ? "Template deleted." : "Template not found." }] };
|
|
53815
54444
|
} catch (e) {
|
|
53816
54445
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
@@ -69258,6 +69887,7 @@ function emptySnapshot2() {
|
|
|
69258
69887
|
agents: [],
|
|
69259
69888
|
taskLists: [],
|
|
69260
69889
|
templates: [],
|
|
69890
|
+
templateTasks: [],
|
|
69261
69891
|
auditHistory: [],
|
|
69262
69892
|
tombstones: []
|
|
69263
69893
|
};
|
|
@@ -69446,6 +70076,7 @@ var init_shadow = __esm(() => {
|
|
|
69446
70076
|
agents: "agents",
|
|
69447
70077
|
taskLists: "task_lists",
|
|
69448
70078
|
templates: "templates",
|
|
70079
|
+
templateTasks: "template_tasks",
|
|
69449
70080
|
auditHistory: "audit_history"
|
|
69450
70081
|
};
|
|
69451
70082
|
});
|
|
@@ -71476,6 +72107,8 @@ var REMOTE_COMMANDS = new Set([
|
|
|
71476
72107
|
"task",
|
|
71477
72108
|
"task-lists",
|
|
71478
72109
|
"timeline",
|
|
72110
|
+
"template-import",
|
|
72111
|
+
"templates",
|
|
71479
72112
|
"tl",
|
|
71480
72113
|
"unlock",
|
|
71481
72114
|
"update"
|