@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/server/index.js
CHANGED
|
@@ -70,7 +70,7 @@ var package_default;
|
|
|
70
70
|
var init_package = __esm(() => {
|
|
71
71
|
package_default = {
|
|
72
72
|
name: "@hasna/todos",
|
|
73
|
-
version: "0.11.
|
|
73
|
+
version: "0.11.95",
|
|
74
74
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
75
75
|
type: "module",
|
|
76
76
|
main: "dist/index.js",
|
|
@@ -898,6 +898,7 @@ function snapshotEntries(snapshot) {
|
|
|
898
898
|
...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
|
|
899
899
|
...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
|
|
900
900
|
...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
|
|
901
|
+
...(snapshot.templateTasks ?? []).map((payload) => entry("template_tasks", payload, snapshot.exportedAt)),
|
|
901
902
|
...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
|
|
902
903
|
...(snapshot.tombstones ?? []).map((tombstone) => ({
|
|
903
904
|
type: tombstone.object_type,
|
|
@@ -949,6 +950,7 @@ function rowsToSnapshot(rows) {
|
|
|
949
950
|
agents: [],
|
|
950
951
|
taskLists: [],
|
|
951
952
|
templates: [],
|
|
953
|
+
templateTasks: [],
|
|
952
954
|
auditHistory: [],
|
|
953
955
|
tombstones: []
|
|
954
956
|
};
|
|
@@ -983,6 +985,8 @@ function rowsToSnapshot(rows) {
|
|
|
983
985
|
snapshot.taskLists.push(payload);
|
|
984
986
|
else if (row.object_type === "templates")
|
|
985
987
|
snapshot.templates.push(payload);
|
|
988
|
+
else if (row.object_type === "template_tasks")
|
|
989
|
+
snapshot.templateTasks.push(payload);
|
|
986
990
|
else if (row.object_type === "audit_history")
|
|
987
991
|
snapshot.auditHistory.push(payload);
|
|
988
992
|
}
|
|
@@ -1316,10 +1320,13 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
1316
1320
|
get: (id) => store.get("templates", id),
|
|
1317
1321
|
list: async () => (await store.list("templates")).sort((a, b) => a.name.localeCompare(b.name)),
|
|
1318
1322
|
update: (id, input) => updateTemplate(id, input, store),
|
|
1319
|
-
delete: (id, context) =>
|
|
1323
|
+
delete: (id, context) => deleteTemplate(id, store, context),
|
|
1320
1324
|
getWithTasks: async (id) => {
|
|
1321
1325
|
const template = await store.get("templates", id);
|
|
1322
|
-
|
|
1326
|
+
if (!template)
|
|
1327
|
+
return null;
|
|
1328
|
+
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));
|
|
1329
|
+
return { ...template, tasks };
|
|
1323
1330
|
}
|
|
1324
1331
|
},
|
|
1325
1332
|
audit: {
|
|
@@ -1607,6 +1614,57 @@ class PostgresJsonRecordStore {
|
|
|
1607
1614
|
}
|
|
1608
1615
|
return value;
|
|
1609
1616
|
}
|
|
1617
|
+
async createTemplateWithTasks(template, tasks, context = {}) {
|
|
1618
|
+
await this.ensureSchema();
|
|
1619
|
+
const records = [
|
|
1620
|
+
{ object_type: "templates", object_id: template.id, payload: template, updated_at: template.created_at, version: template.version },
|
|
1621
|
+
...tasks.map((task) => ({ object_type: "template_tasks", object_id: task.id, payload: task, updated_at: task.created_at, version: 1 }))
|
|
1622
|
+
];
|
|
1623
|
+
const result = await this.options.client.query(`/* todos:create-template-with-tasks-atomic */ WITH input AS (
|
|
1624
|
+
SELECT value->>'object_type' AS object_type,
|
|
1625
|
+
value->>'object_id' AS object_id,
|
|
1626
|
+
value->'payload' AS payload,
|
|
1627
|
+
value->>'updated_at' AS updated_at,
|
|
1628
|
+
COALESCE((value->>'version')::integer, 1) AS version
|
|
1629
|
+
FROM jsonb_array_elements($2::jsonb) AS value
|
|
1630
|
+
) INSERT INTO ${this.tableName} (
|
|
1631
|
+
service, object_type, object_id, payload, updated_at,
|
|
1632
|
+
deleted_at, source_machine_id, version
|
|
1633
|
+
) SELECT $1, object_type, object_id, payload, updated_at::timestamptz,
|
|
1634
|
+
NULL, $3, version
|
|
1635
|
+
FROM input
|
|
1636
|
+
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
1637
|
+
payload = EXCLUDED.payload,
|
|
1638
|
+
updated_at = EXCLUDED.updated_at,
|
|
1639
|
+
deleted_at = NULL,
|
|
1640
|
+
source_machine_id = EXCLUDED.source_machine_id,
|
|
1641
|
+
version = EXCLUDED.version
|
|
1642
|
+
WHERE ${this.tableName}.updated_at IS NULL
|
|
1643
|
+
OR ${this.tableName}.updated_at < EXCLUDED.updated_at
|
|
1644
|
+
OR (${this.tableName}.updated_at = EXCLUDED.updated_at
|
|
1645
|
+
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
|
|
1646
|
+
RETURNING object_type, object_id`, [this.service, jsonbParam(records), this.machineId(context)]);
|
|
1647
|
+
if (result.rows.length !== records.length) {
|
|
1648
|
+
throw new Error("Template checklist write was rejected before completion; no partial template was committed");
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
async deleteTemplateWithTasks(id, context = {}) {
|
|
1652
|
+
await this.ensureSchema();
|
|
1653
|
+
const timestamp = new Date().toISOString();
|
|
1654
|
+
const result = await this.options.client.query(`/* todos:delete-template-with-tasks-atomic */ WITH target AS (
|
|
1655
|
+
SELECT 1 FROM ${this.tableName}
|
|
1656
|
+
WHERE service = $1 AND object_type = 'templates' AND object_id = $2 AND deleted_at IS NULL
|
|
1657
|
+
) UPDATE ${this.tableName} AS record SET
|
|
1658
|
+
deleted_at = $3::timestamptz,
|
|
1659
|
+
updated_at = $3::timestamptz,
|
|
1660
|
+
source_machine_id = COALESCE($4, record.source_machine_id),
|
|
1661
|
+
version = COALESCE(record.version, 0) + 1
|
|
1662
|
+
WHERE record.service = $1 AND record.deleted_at IS NULL AND EXISTS (SELECT 1 FROM target)
|
|
1663
|
+
AND (record.object_type = 'templates' AND record.object_id = $2
|
|
1664
|
+
OR record.object_type = 'template_tasks' AND record.payload->>'template_id' = $2)
|
|
1665
|
+
RETURNING record.object_type`, [this.service, id, timestamp, this.machineId(context)]);
|
|
1666
|
+
return result.rows.some((row) => row.object_type === "templates");
|
|
1667
|
+
}
|
|
1610
1668
|
async completeTask(id, agentId, options) {
|
|
1611
1669
|
await this.ensureSchema();
|
|
1612
1670
|
const operationTimestamp = new Date().toISOString();
|
|
@@ -1947,7 +2005,7 @@ async function updateTask(id, input, store) {
|
|
|
1947
2005
|
tags: input.tags ?? existing.tags,
|
|
1948
2006
|
metadata: input.metadata ?? existing.metadata,
|
|
1949
2007
|
requires_approval: input.requires_approval ?? existing.requires_approval,
|
|
1950
|
-
task_list_id: input.task_list_id
|
|
2008
|
+
task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id
|
|
1951
2009
|
};
|
|
1952
2010
|
await store.upsert("tasks", task);
|
|
1953
2011
|
return task;
|
|
@@ -2369,7 +2427,7 @@ async function updateTaskList(id, input, store) {
|
|
|
2369
2427
|
}
|
|
2370
2428
|
async function createTemplate(input, store, context) {
|
|
2371
2429
|
const timestamp = new Date().toISOString();
|
|
2372
|
-
|
|
2430
|
+
const template = {
|
|
2373
2431
|
id: randomUUID(),
|
|
2374
2432
|
name: input.name,
|
|
2375
2433
|
title_pattern: input.title_pattern,
|
|
@@ -2384,7 +2442,30 @@ async function createTemplate(input, store, context) {
|
|
|
2384
2442
|
created_at: timestamp,
|
|
2385
2443
|
machine_id: store.machineId(context),
|
|
2386
2444
|
synced_at: null
|
|
2387
|
-
}
|
|
2445
|
+
};
|
|
2446
|
+
const tasks = buildTemplateTasks(template.id, input.tasks ?? [], timestamp);
|
|
2447
|
+
await store.createTemplateWithTasks(template, tasks, context);
|
|
2448
|
+
return template;
|
|
2449
|
+
}
|
|
2450
|
+
function buildTemplateTasks(templateId, inputs, timestamp) {
|
|
2451
|
+
return inputs.map((input, position) => ({
|
|
2452
|
+
id: randomUUID(),
|
|
2453
|
+
template_id: templateId,
|
|
2454
|
+
position,
|
|
2455
|
+
title_pattern: input.title_pattern,
|
|
2456
|
+
description: input.description ?? null,
|
|
2457
|
+
priority: input.priority ?? "medium",
|
|
2458
|
+
tags: input.tags ?? [],
|
|
2459
|
+
task_type: input.task_type ?? null,
|
|
2460
|
+
condition: input.condition ?? null,
|
|
2461
|
+
include_template_id: input.include_template_id ?? null,
|
|
2462
|
+
depends_on_positions: input.depends_on ?? [],
|
|
2463
|
+
metadata: input.metadata ?? {},
|
|
2464
|
+
created_at: timestamp
|
|
2465
|
+
}));
|
|
2466
|
+
}
|
|
2467
|
+
async function deleteTemplate(id, store, context) {
|
|
2468
|
+
return store.deleteTemplateWithTasks(id, context);
|
|
2388
2469
|
}
|
|
2389
2470
|
async function updateTemplate(id, input, store) {
|
|
2390
2471
|
const template = await store.get("templates", id);
|
|
@@ -2440,6 +2521,7 @@ async function exportSnapshot(store) {
|
|
|
2440
2521
|
agents: await store.list("agents"),
|
|
2441
2522
|
taskLists: await store.list("task_lists"),
|
|
2442
2523
|
templates: await store.list("templates"),
|
|
2524
|
+
templateTasks: await store.list("template_tasks"),
|
|
2443
2525
|
auditHistory: await store.list("audit_history"),
|
|
2444
2526
|
tombstones: await store.listTombstones()
|
|
2445
2527
|
};
|
|
@@ -2464,6 +2546,7 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
2464
2546
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
2465
2547
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
2466
2548
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
2549
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row]),
|
|
2467
2550
|
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
2468
2551
|
];
|
|
2469
2552
|
for (const [type, row] of entries) {
|
|
@@ -8562,6 +8645,50 @@ var init_recurrence = __esm(() => {
|
|
|
8562
8645
|
};
|
|
8563
8646
|
});
|
|
8564
8647
|
|
|
8648
|
+
// src/lib/template-semantics.ts
|
|
8649
|
+
function resolveTemplateVariables(templateVars, provided) {
|
|
8650
|
+
const merged = { ...provided };
|
|
8651
|
+
for (const variable of templateVars) {
|
|
8652
|
+
if (merged[variable.name] === undefined && variable.default !== undefined) {
|
|
8653
|
+
merged[variable.name] = variable.default;
|
|
8654
|
+
}
|
|
8655
|
+
}
|
|
8656
|
+
const missing = templateVars.filter((variable) => variable.required && merged[variable.name] === undefined).map((variable) => variable.name);
|
|
8657
|
+
if (missing.length > 0) {
|
|
8658
|
+
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
8659
|
+
}
|
|
8660
|
+
return merged;
|
|
8661
|
+
}
|
|
8662
|
+
function substituteTemplateVariables(text, variables) {
|
|
8663
|
+
let result = text;
|
|
8664
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
8665
|
+
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
8666
|
+
}
|
|
8667
|
+
return result;
|
|
8668
|
+
}
|
|
8669
|
+
function evaluateTemplateCondition(condition, variables) {
|
|
8670
|
+
if (!condition || condition.trim() === "")
|
|
8671
|
+
return true;
|
|
8672
|
+
const trimmed = condition.trim();
|
|
8673
|
+
const equal = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
8674
|
+
if (equal)
|
|
8675
|
+
return (variables[equal[1]] ?? "") === equal[2].trim();
|
|
8676
|
+
const unequal = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
8677
|
+
if (unequal)
|
|
8678
|
+
return (variables[unequal[1]] ?? "") !== unequal[2].trim();
|
|
8679
|
+
const falsy = trimmed.match(/^!\{([^}]+)\}$/);
|
|
8680
|
+
if (falsy) {
|
|
8681
|
+
const value = variables[falsy[1]];
|
|
8682
|
+
return !value || value === "" || value === "false";
|
|
8683
|
+
}
|
|
8684
|
+
const truthy = trimmed.match(/^\{([^}]+)\}$/);
|
|
8685
|
+
if (truthy) {
|
|
8686
|
+
const value = variables[truthy[1]];
|
|
8687
|
+
return !!value && value !== "" && value !== "false";
|
|
8688
|
+
}
|
|
8689
|
+
return true;
|
|
8690
|
+
}
|
|
8691
|
+
|
|
8565
8692
|
// src/db/templates.ts
|
|
8566
8693
|
var exports_templates = {};
|
|
8567
8694
|
__export(exports_templates, {
|
|
@@ -8579,7 +8706,7 @@ __export(exports_templates, {
|
|
|
8579
8706
|
getTemplate: () => getTemplate,
|
|
8580
8707
|
exportTemplate: () => exportTemplate,
|
|
8581
8708
|
evaluateCondition: () => evaluateCondition,
|
|
8582
|
-
deleteTemplate: () =>
|
|
8709
|
+
deleteTemplate: () => deleteTemplate2,
|
|
8583
8710
|
createTemplate: () => createTemplate2,
|
|
8584
8711
|
addTemplateTasks: () => addTemplateTasks
|
|
8585
8712
|
});
|
|
@@ -8643,7 +8770,7 @@ function listTemplates(db) {
|
|
|
8643
8770
|
const d = db || getDatabase();
|
|
8644
8771
|
return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
|
|
8645
8772
|
}
|
|
8646
|
-
function
|
|
8773
|
+
function deleteTemplate2(id, db) {
|
|
8647
8774
|
const d = db || getDatabase();
|
|
8648
8775
|
const resolved = resolveTemplateId(id, d);
|
|
8649
8776
|
if (!resolved)
|
|
@@ -8657,6 +8784,14 @@ function deleteTemplate(id, db) {
|
|
|
8657
8784
|
payload: template,
|
|
8658
8785
|
version: template.version
|
|
8659
8786
|
}, d);
|
|
8787
|
+
for (const task of getTemplateTasks(resolved, d)) {
|
|
8788
|
+
recordStorageTombstone({
|
|
8789
|
+
object_type: "template_tasks",
|
|
8790
|
+
object_id: task.id,
|
|
8791
|
+
payload: task,
|
|
8792
|
+
version: 1
|
|
8793
|
+
}, d);
|
|
8794
|
+
}
|
|
8660
8795
|
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
8661
8796
|
}
|
|
8662
8797
|
function updateTemplate2(id, updates, db) {
|
|
@@ -8788,34 +8923,7 @@ function getTemplateTasks(templateId, db) {
|
|
|
8788
8923
|
return rows.map(rowToTemplateTask);
|
|
8789
8924
|
}
|
|
8790
8925
|
function evaluateCondition(condition, variables) {
|
|
8791
|
-
|
|
8792
|
-
return true;
|
|
8793
|
-
const trimmed = condition.trim();
|
|
8794
|
-
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
8795
|
-
if (eqMatch) {
|
|
8796
|
-
const varName = eqMatch[1];
|
|
8797
|
-
const expected = eqMatch[2].trim();
|
|
8798
|
-
return (variables[varName] ?? "") === expected;
|
|
8799
|
-
}
|
|
8800
|
-
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
8801
|
-
if (neqMatch) {
|
|
8802
|
-
const varName = neqMatch[1];
|
|
8803
|
-
const expected = neqMatch[2].trim();
|
|
8804
|
-
return (variables[varName] ?? "") !== expected;
|
|
8805
|
-
}
|
|
8806
|
-
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
8807
|
-
if (falsyMatch) {
|
|
8808
|
-
const varName = falsyMatch[1];
|
|
8809
|
-
const val = variables[varName];
|
|
8810
|
-
return !val || val === "" || val === "false";
|
|
8811
|
-
}
|
|
8812
|
-
const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
|
|
8813
|
-
if (truthyMatch) {
|
|
8814
|
-
const varName = truthyMatch[1];
|
|
8815
|
-
const val = variables[varName];
|
|
8816
|
-
return !!val && val !== "" && val !== "false";
|
|
8817
|
-
}
|
|
8818
|
-
return true;
|
|
8926
|
+
return evaluateTemplateCondition(condition, variables);
|
|
8819
8927
|
}
|
|
8820
8928
|
function exportTemplate(id, db) {
|
|
8821
8929
|
const d = db || getDatabase();
|
|
@@ -8888,29 +8996,10 @@ function listTemplateVersions(id, db) {
|
|
|
8888
8996
|
return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
|
|
8889
8997
|
}
|
|
8890
8998
|
function resolveVariables(templateVars, provided) {
|
|
8891
|
-
|
|
8892
|
-
for (const v of templateVars) {
|
|
8893
|
-
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
8894
|
-
merged[v.name] = v.default;
|
|
8895
|
-
}
|
|
8896
|
-
}
|
|
8897
|
-
const missing = [];
|
|
8898
|
-
for (const v of templateVars) {
|
|
8899
|
-
if (v.required && merged[v.name] === undefined) {
|
|
8900
|
-
missing.push(v.name);
|
|
8901
|
-
}
|
|
8902
|
-
}
|
|
8903
|
-
if (missing.length > 0) {
|
|
8904
|
-
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
8905
|
-
}
|
|
8906
|
-
return merged;
|
|
8999
|
+
return resolveTemplateVariables(templateVars, provided);
|
|
8907
9000
|
}
|
|
8908
9001
|
function substituteVars(text, variables) {
|
|
8909
|
-
|
|
8910
|
-
for (const [key, val] of Object.entries(variables)) {
|
|
8911
|
-
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
|
|
8912
|
-
}
|
|
8913
|
-
return result;
|
|
9002
|
+
return substituteTemplateVariables(text, variables);
|
|
8914
9003
|
}
|
|
8915
9004
|
function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
|
|
8916
9005
|
const d = db || getDatabase();
|
|
@@ -15090,7 +15179,7 @@ async function handleCreateTemplate(req, _ctx, json2) {
|
|
|
15090
15179
|
}
|
|
15091
15180
|
}
|
|
15092
15181
|
function handleDeleteTemplate(id, _ctx, json2) {
|
|
15093
|
-
const deleted =
|
|
15182
|
+
const deleted = deleteTemplate2(id);
|
|
15094
15183
|
return json2(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
|
|
15095
15184
|
}
|
|
15096
15185
|
function handleListPlans(url, _ctx, json2) {
|
|
@@ -15202,6 +15291,7 @@ function exportSqliteTodosStorageSnapshot(db) {
|
|
|
15202
15291
|
agents: listAgents({ include_archived: true }, d),
|
|
15203
15292
|
taskLists: listTaskLists(undefined, d),
|
|
15204
15293
|
templates: listTemplates(d),
|
|
15294
|
+
templateTasks: listTemplates(d).flatMap((template) => getTemplateTasks(template.id, d)),
|
|
15205
15295
|
auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
|
|
15206
15296
|
tombstones: listStorageTombstones(d)
|
|
15207
15297
|
};
|
|
@@ -15251,6 +15341,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
15251
15341
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
15252
15342
|
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
15253
15343
|
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
15344
|
+
applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
|
|
15254
15345
|
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
|
|
15255
15346
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
15256
15347
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
@@ -15355,6 +15446,8 @@ function tableForTombstone(objectType) {
|
|
|
15355
15446
|
return "task_lists";
|
|
15356
15447
|
if (objectType === "templates")
|
|
15357
15448
|
return "task_templates";
|
|
15449
|
+
if (objectType === "template_tasks")
|
|
15450
|
+
return "template_tasks";
|
|
15358
15451
|
return "task_history";
|
|
15359
15452
|
}
|
|
15360
15453
|
function listRows(db, table, columns) {
|
|
@@ -15391,7 +15484,7 @@ function clockColumnsForTable(table) {
|
|
|
15391
15484
|
return ["created_at"];
|
|
15392
15485
|
return ["updated_at", "created_at"];
|
|
15393
15486
|
}
|
|
15394
|
-
var PROJECT_COLUMNS, PROJECT_MACHINE_PATH_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
|
|
15487
|
+
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;
|
|
15395
15488
|
var init_sqlite_snapshot = __esm(() => {
|
|
15396
15489
|
init_database();
|
|
15397
15490
|
init_agents();
|
|
@@ -15485,6 +15578,21 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
15485
15578
|
"machine_id",
|
|
15486
15579
|
"synced_at"
|
|
15487
15580
|
];
|
|
15581
|
+
TEMPLATE_TASK_COLUMNS = [
|
|
15582
|
+
"id",
|
|
15583
|
+
"template_id",
|
|
15584
|
+
"position",
|
|
15585
|
+
"title_pattern",
|
|
15586
|
+
"description",
|
|
15587
|
+
"priority",
|
|
15588
|
+
"tags",
|
|
15589
|
+
"task_type",
|
|
15590
|
+
"condition",
|
|
15591
|
+
"include_template_id",
|
|
15592
|
+
"depends_on_positions",
|
|
15593
|
+
"metadata",
|
|
15594
|
+
"created_at"
|
|
15595
|
+
];
|
|
15488
15596
|
TASK_COLUMNS = [
|
|
15489
15597
|
"id",
|
|
15490
15598
|
"short_id",
|
|
@@ -15553,7 +15661,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
15553
15661
|
"created_at",
|
|
15554
15662
|
"machine_id"
|
|
15555
15663
|
];
|
|
15556
|
-
JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables"]);
|
|
15664
|
+
JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
|
|
15557
15665
|
BOOLEAN_COLUMNS = new Set(["requires_approval"]);
|
|
15558
15666
|
});
|
|
15559
15667
|
|
|
@@ -15646,7 +15754,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
15646
15754
|
get: (id) => getTemplate(id, database()),
|
|
15647
15755
|
list: () => listTemplates(database()),
|
|
15648
15756
|
update: (id, input) => updateTemplate2(id, input, database()),
|
|
15649
|
-
delete: (id) =>
|
|
15757
|
+
delete: (id) => deleteTemplate2(id, database()),
|
|
15650
15758
|
getWithTasks: (id) => getTemplateWithTasks(id, database())
|
|
15651
15759
|
},
|
|
15652
15760
|
audit: {
|
|
@@ -15869,6 +15977,9 @@ class TodosShadowOutbox {
|
|
|
15869
15977
|
case "templates":
|
|
15870
15978
|
snapshot.templates.push(record);
|
|
15871
15979
|
break;
|
|
15980
|
+
case "template_tasks":
|
|
15981
|
+
snapshot.templateTasks.push(record);
|
|
15982
|
+
break;
|
|
15872
15983
|
case "audit_history":
|
|
15873
15984
|
snapshot.auditHistory.push(record);
|
|
15874
15985
|
break;
|
|
@@ -15935,6 +16046,7 @@ function emptySnapshot() {
|
|
|
15935
16046
|
agents: [],
|
|
15936
16047
|
taskLists: [],
|
|
15937
16048
|
templates: [],
|
|
16049
|
+
templateTasks: [],
|
|
15938
16050
|
auditHistory: [],
|
|
15939
16051
|
tombstones: []
|
|
15940
16052
|
};
|
|
@@ -16060,12 +16172,16 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
16060
16172
|
TaskList: taskListSchema,
|
|
16061
16173
|
TaskComment: taskCommentSchema,
|
|
16062
16174
|
Plan: planSchema,
|
|
16175
|
+
Template: templateSchema,
|
|
16176
|
+
TemplateTask: templateTaskSchema,
|
|
16177
|
+
TemplateVariable: templateVariableSchema,
|
|
16178
|
+
CreateTemplateTaskInput: createTemplateTaskInputSchema,
|
|
16063
16179
|
CreateTaskInput: {
|
|
16064
16180
|
type: "object",
|
|
16065
16181
|
required: ["title"],
|
|
16066
16182
|
properties: {
|
|
16067
16183
|
title: { type: "string" },
|
|
16068
|
-
description: { type: "string" },
|
|
16184
|
+
description: { type: "string", nullable: true },
|
|
16069
16185
|
status: { type: "string" },
|
|
16070
16186
|
priority: { type: "string" },
|
|
16071
16187
|
project_id: { type: "string" },
|
|
@@ -16082,6 +16198,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
16082
16198
|
status: { type: "string" },
|
|
16083
16199
|
priority: { type: "string" },
|
|
16084
16200
|
assigned_to: { type: "string" },
|
|
16201
|
+
project_id: { type: "string", nullable: true },
|
|
16202
|
+
task_list_id: { type: "string", nullable: true },
|
|
16085
16203
|
version: { type: "number" }
|
|
16086
16204
|
}
|
|
16087
16205
|
},
|
|
@@ -16198,6 +16316,39 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
16198
16316
|
agent_id: { type: "string", minLength: 1 },
|
|
16199
16317
|
status: { type: "string", enum: ["active", "completed", "archived"] }
|
|
16200
16318
|
}
|
|
16319
|
+
},
|
|
16320
|
+
CreateTemplateInput: {
|
|
16321
|
+
type: "object",
|
|
16322
|
+
additionalProperties: false,
|
|
16323
|
+
required: ["name", "title_pattern"],
|
|
16324
|
+
properties: {
|
|
16325
|
+
name: { type: "string", minLength: 1 },
|
|
16326
|
+
title_pattern: { type: "string", minLength: 1 },
|
|
16327
|
+
description: { type: "string", nullable: true },
|
|
16328
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
16329
|
+
tags: { type: "array", items: { type: "string", minLength: 1 } },
|
|
16330
|
+
variables: { type: "array", items: { $ref: "#/components/schemas/TemplateVariable" } },
|
|
16331
|
+
project_id: { type: "string", minLength: 1, nullable: true },
|
|
16332
|
+
plan_id: { type: "string", minLength: 1, nullable: true },
|
|
16333
|
+
metadata: { type: "object", additionalProperties: true },
|
|
16334
|
+
tasks: { type: "array", items: { $ref: "#/components/schemas/CreateTemplateTaskInput" } }
|
|
16335
|
+
}
|
|
16336
|
+
},
|
|
16337
|
+
UpdateTemplateInput: {
|
|
16338
|
+
type: "object",
|
|
16339
|
+
additionalProperties: false,
|
|
16340
|
+
minProperties: 1,
|
|
16341
|
+
properties: {
|
|
16342
|
+
name: { type: "string", minLength: 1 },
|
|
16343
|
+
title_pattern: { type: "string", minLength: 1 },
|
|
16344
|
+
description: { type: "string", nullable: true },
|
|
16345
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
16346
|
+
tags: { type: "array", items: { type: "string", minLength: 1 } },
|
|
16347
|
+
variables: { type: "array", items: { type: "object" } },
|
|
16348
|
+
project_id: { type: "string", nullable: true },
|
|
16349
|
+
plan_id: { type: "string", nullable: true },
|
|
16350
|
+
metadata: { type: "object", additionalProperties: true }
|
|
16351
|
+
}
|
|
16201
16352
|
}
|
|
16202
16353
|
}
|
|
16203
16354
|
},
|
|
@@ -16500,6 +16651,41 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
16500
16651
|
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
|
|
16501
16652
|
}
|
|
16502
16653
|
},
|
|
16654
|
+
"/v1/templates": {
|
|
16655
|
+
get: {
|
|
16656
|
+
operationId: "listTemplates",
|
|
16657
|
+
summary: "List reusable task templates",
|
|
16658
|
+
parameters: [{ name: "project_id", in: "query", schema: { type: "string" } }],
|
|
16659
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { templates: { type: "array", items: { $ref: "#/components/schemas/Template" } }, count: { type: "number" } } } } } } }
|
|
16660
|
+
},
|
|
16661
|
+
post: {
|
|
16662
|
+
operationId: "createTemplate",
|
|
16663
|
+
summary: "Create a reusable task template",
|
|
16664
|
+
requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateTemplateInput" } } } },
|
|
16665
|
+
responses: { "201": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
|
|
16666
|
+
}
|
|
16667
|
+
},
|
|
16668
|
+
"/v1/templates/{id}": {
|
|
16669
|
+
get: {
|
|
16670
|
+
operationId: "getTemplate",
|
|
16671
|
+
summary: "Get one reusable task template with its checklist steps",
|
|
16672
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
16673
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
|
|
16674
|
+
},
|
|
16675
|
+
patch: {
|
|
16676
|
+
operationId: "updateTemplate",
|
|
16677
|
+
summary: "Update reusable template metadata and defaults",
|
|
16678
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
16679
|
+
requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateTemplateInput" } } } },
|
|
16680
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
|
|
16681
|
+
},
|
|
16682
|
+
delete: {
|
|
16683
|
+
operationId: "deleteTemplate",
|
|
16684
|
+
summary: "Delete a reusable task template and its checklist steps",
|
|
16685
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
16686
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
|
|
16687
|
+
}
|
|
16688
|
+
},
|
|
16503
16689
|
"/v1/task-lists": {
|
|
16504
16690
|
get: {
|
|
16505
16691
|
operationId: "listTaskLists",
|
|
@@ -16577,6 +16763,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
16577
16763
|
agents: { type: "array", items: { type: "object" } },
|
|
16578
16764
|
taskLists: { type: "array", items: { type: "object" } },
|
|
16579
16765
|
templates: { type: "array", items: { type: "object" } },
|
|
16766
|
+
templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
|
|
16580
16767
|
auditHistory: { type: "array", items: { type: "object" } },
|
|
16581
16768
|
tombstones: { type: "array", items: { type: "object" } }
|
|
16582
16769
|
}
|
|
@@ -16613,7 +16800,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
16613
16800
|
}
|
|
16614
16801
|
};
|
|
16615
16802
|
}
|
|
16616
|
-
var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema;
|
|
16803
|
+
var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
16617
16804
|
var init_openapi = __esm(() => {
|
|
16618
16805
|
init_package_version();
|
|
16619
16806
|
taskSchema = {
|
|
@@ -16690,6 +16877,72 @@ var init_openapi = __esm(() => {
|
|
|
16690
16877
|
updated_at: { type: "string", format: "date-time" }
|
|
16691
16878
|
}
|
|
16692
16879
|
};
|
|
16880
|
+
templateTaskSchema = {
|
|
16881
|
+
type: "object",
|
|
16882
|
+
required: ["id", "template_id", "position", "title_pattern", "priority", "tags", "depends_on_positions", "metadata", "created_at"],
|
|
16883
|
+
properties: {
|
|
16884
|
+
id: { type: "string" },
|
|
16885
|
+
template_id: { type: "string" },
|
|
16886
|
+
position: { type: "integer", minimum: 0 },
|
|
16887
|
+
title_pattern: { type: "string" },
|
|
16888
|
+
description: { type: "string", nullable: true },
|
|
16889
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
16890
|
+
tags: { type: "array", items: { type: "string" } },
|
|
16891
|
+
task_type: { type: "string", nullable: true },
|
|
16892
|
+
condition: { type: "string", nullable: true },
|
|
16893
|
+
include_template_id: { type: "string", nullable: true },
|
|
16894
|
+
depends_on_positions: { type: "array", items: { type: "integer", minimum: 0 } },
|
|
16895
|
+
metadata: { type: "object", additionalProperties: true },
|
|
16896
|
+
created_at: { type: "string", format: "date-time" }
|
|
16897
|
+
}
|
|
16898
|
+
};
|
|
16899
|
+
templateSchema = {
|
|
16900
|
+
type: "object",
|
|
16901
|
+
required: ["id", "name", "title_pattern", "priority", "tags", "variables", "version", "metadata", "created_at"],
|
|
16902
|
+
properties: {
|
|
16903
|
+
id: { type: "string" },
|
|
16904
|
+
name: { type: "string" },
|
|
16905
|
+
title_pattern: { type: "string" },
|
|
16906
|
+
description: { type: "string", nullable: true },
|
|
16907
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
16908
|
+
tags: { type: "array", items: { type: "string" } },
|
|
16909
|
+
variables: { type: "array", items: { type: "object", properties: { name: { type: "string" }, required: { type: "boolean" }, default: { type: "string" }, description: { type: "string" } } } },
|
|
16910
|
+
version: { type: "integer", minimum: 1 },
|
|
16911
|
+
project_id: { type: "string", nullable: true },
|
|
16912
|
+
plan_id: { type: "string", nullable: true },
|
|
16913
|
+
metadata: { type: "object", additionalProperties: true },
|
|
16914
|
+
created_at: { type: "string", format: "date-time" },
|
|
16915
|
+
tasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } }
|
|
16916
|
+
}
|
|
16917
|
+
};
|
|
16918
|
+
templateVariableSchema = {
|
|
16919
|
+
type: "object",
|
|
16920
|
+
required: ["name", "required"],
|
|
16921
|
+
properties: {
|
|
16922
|
+
name: { type: "string" },
|
|
16923
|
+
required: { type: "boolean" },
|
|
16924
|
+
default: { type: "string" },
|
|
16925
|
+
description: { type: "string" }
|
|
16926
|
+
}
|
|
16927
|
+
};
|
|
16928
|
+
createTemplateTaskInputSchema = {
|
|
16929
|
+
type: "object",
|
|
16930
|
+
additionalProperties: false,
|
|
16931
|
+
required: ["title_pattern"],
|
|
16932
|
+
properties: {
|
|
16933
|
+
position: { type: "integer", minimum: 0 },
|
|
16934
|
+
title_pattern: { type: "string", minLength: 1 },
|
|
16935
|
+
description: { type: "string", nullable: true },
|
|
16936
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
16937
|
+
tags: { type: "array", items: { type: "string", minLength: 1 } },
|
|
16938
|
+
task_type: { type: "string", nullable: true },
|
|
16939
|
+
condition: { type: "string", nullable: true },
|
|
16940
|
+
include_template_id: { type: "string", nullable: true },
|
|
16941
|
+
depends_on: { type: "array", items: { type: "integer", minimum: 0 } },
|
|
16942
|
+
depends_on_positions: { type: "array", items: { type: "integer", minimum: 0 } },
|
|
16943
|
+
metadata: { type: "object", additionalProperties: true }
|
|
16944
|
+
}
|
|
16945
|
+
};
|
|
16693
16946
|
});
|
|
16694
16947
|
|
|
16695
16948
|
// src/server/v1.ts
|
|
@@ -16829,6 +17082,122 @@ function validatePlanCreate(value) {
|
|
|
16829
17082
|
}
|
|
16830
17083
|
};
|
|
16831
17084
|
}
|
|
17085
|
+
function validateTemplateTask(value) {
|
|
17086
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
17087
|
+
return null;
|
|
17088
|
+
const body = value;
|
|
17089
|
+
const allowed = new Set(["position", "title_pattern", "description", "priority", "tags", "task_type", "condition", "include_template_id", "depends_on", "depends_on_positions", "metadata"]);
|
|
17090
|
+
if (Object.keys(body).some((key) => !allowed.has(key)))
|
|
17091
|
+
return null;
|
|
17092
|
+
if (typeof body.title_pattern !== "string" || !body.title_pattern.trim())
|
|
17093
|
+
return null;
|
|
17094
|
+
if (body.position !== undefined && (typeof body.position !== "number" || !Number.isSafeInteger(body.position) || body.position < 0))
|
|
17095
|
+
return null;
|
|
17096
|
+
if (body.description !== undefined && body.description !== null && typeof body.description !== "string")
|
|
17097
|
+
return null;
|
|
17098
|
+
if (body.priority !== undefined && (typeof body.priority !== "string" || !["low", "medium", "high", "critical"].includes(body.priority)))
|
|
17099
|
+
return null;
|
|
17100
|
+
if (body.tags !== undefined && (!Array.isArray(body.tags) || body.tags.some((tag) => typeof tag !== "string" || !tag.trim())))
|
|
17101
|
+
return null;
|
|
17102
|
+
for (const field of ["task_type", "condition", "include_template_id"]) {
|
|
17103
|
+
if (body[field] !== undefined && body[field] !== null && (typeof body[field] !== "string" || !body[field].trim()))
|
|
17104
|
+
return null;
|
|
17105
|
+
}
|
|
17106
|
+
if (body.depends_on !== undefined && body.depends_on_positions !== undefined)
|
|
17107
|
+
return null;
|
|
17108
|
+
const dependencies = body.depends_on ?? body.depends_on_positions;
|
|
17109
|
+
if (dependencies !== undefined && (!Array.isArray(dependencies) || dependencies.some((position) => !Number.isSafeInteger(position) || position < 0)))
|
|
17110
|
+
return null;
|
|
17111
|
+
if (body.metadata !== undefined && (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata)))
|
|
17112
|
+
return null;
|
|
17113
|
+
return {
|
|
17114
|
+
title_pattern: body.title_pattern,
|
|
17115
|
+
...typeof body.description === "string" ? { description: body.description } : {},
|
|
17116
|
+
...typeof body.priority === "string" ? { priority: body.priority } : {},
|
|
17117
|
+
...Array.isArray(body.tags) ? { tags: body.tags } : {},
|
|
17118
|
+
...typeof body.task_type === "string" ? { task_type: body.task_type } : {},
|
|
17119
|
+
...typeof body.condition === "string" ? { condition: body.condition } : {},
|
|
17120
|
+
...typeof body.include_template_id === "string" ? { include_template_id: body.include_template_id } : {},
|
|
17121
|
+
...Array.isArray(dependencies) ? { depends_on: dependencies } : {},
|
|
17122
|
+
...body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? { metadata: body.metadata } : {}
|
|
17123
|
+
};
|
|
17124
|
+
}
|
|
17125
|
+
function validateTemplateCreate(value) {
|
|
17126
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
17127
|
+
return { ok: false, message: "template body must be an object" };
|
|
17128
|
+
const body = value;
|
|
17129
|
+
const allowed = new Set(["name", "title_pattern", "description", "priority", "tags", "variables", "project_id", "plan_id", "metadata", "tasks"]);
|
|
17130
|
+
const unknown = Object.keys(body).find((key) => !allowed.has(key));
|
|
17131
|
+
if (unknown)
|
|
17132
|
+
return { ok: false, message: `unknown template field: ${unknown}` };
|
|
17133
|
+
if (typeof body.name !== "string" || !body.name.trim())
|
|
17134
|
+
return { ok: false, message: "name must be a non-empty string" };
|
|
17135
|
+
if (typeof body.title_pattern !== "string" || !body.title_pattern.trim())
|
|
17136
|
+
return { ok: false, message: "title_pattern must be a non-empty string" };
|
|
17137
|
+
if (body.description !== undefined && body.description !== null && typeof body.description !== "string")
|
|
17138
|
+
return { ok: false, message: "description must be a string or null" };
|
|
17139
|
+
if (body.priority !== undefined && (typeof body.priority !== "string" || !["low", "medium", "high", "critical"].includes(body.priority)))
|
|
17140
|
+
return { ok: false, message: "priority must be low, medium, high, or critical" };
|
|
17141
|
+
if (body.tags !== undefined && (!Array.isArray(body.tags) || body.tags.some((tag) => typeof tag !== "string" || !tag.trim())))
|
|
17142
|
+
return { ok: false, message: "tags must be an array of non-empty strings" };
|
|
17143
|
+
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"))) {
|
|
17144
|
+
return { ok: false, message: "variables must be valid template variable objects" };
|
|
17145
|
+
}
|
|
17146
|
+
for (const field of ["project_id", "plan_id"]) {
|
|
17147
|
+
if (body[field] !== undefined && body[field] !== null && (typeof body[field] !== "string" || !body[field].trim()))
|
|
17148
|
+
return { ok: false, message: `${field} must be a non-empty string or null` };
|
|
17149
|
+
}
|
|
17150
|
+
if (body.metadata !== undefined && (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata)))
|
|
17151
|
+
return { ok: false, message: "metadata must be an object" };
|
|
17152
|
+
const tasks = body.tasks === undefined ? [] : Array.isArray(body.tasks) ? body.tasks.map(validateTemplateTask) : null;
|
|
17153
|
+
if (tasks === null || tasks.some((task) => task === null))
|
|
17154
|
+
return { ok: false, message: "tasks must be valid template task objects" };
|
|
17155
|
+
const taskInputs = tasks;
|
|
17156
|
+
for (const [position, task] of taskInputs.entries()) {
|
|
17157
|
+
if ((task.depends_on ?? []).some((dependency) => dependency >= position)) {
|
|
17158
|
+
return { ok: false, message: "template task dependencies must reference earlier task positions" };
|
|
17159
|
+
}
|
|
17160
|
+
}
|
|
17161
|
+
return {
|
|
17162
|
+
ok: true,
|
|
17163
|
+
input: {
|
|
17164
|
+
name: body.name,
|
|
17165
|
+
title_pattern: body.title_pattern,
|
|
17166
|
+
...typeof body.description === "string" ? { description: body.description } : {},
|
|
17167
|
+
...typeof body.priority === "string" ? { priority: body.priority } : {},
|
|
17168
|
+
...Array.isArray(body.tags) ? { tags: body.tags } : {},
|
|
17169
|
+
...Array.isArray(body.variables) ? { variables: body.variables } : {},
|
|
17170
|
+
...typeof body.project_id === "string" ? { project_id: body.project_id } : {},
|
|
17171
|
+
...typeof body.plan_id === "string" ? { plan_id: body.plan_id } : {},
|
|
17172
|
+
...body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? { metadata: body.metadata } : {},
|
|
17173
|
+
tasks: taskInputs
|
|
17174
|
+
}
|
|
17175
|
+
};
|
|
17176
|
+
}
|
|
17177
|
+
function validateTemplatePatch(value) {
|
|
17178
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
17179
|
+
return { ok: false, message: "template patch must be an object" };
|
|
17180
|
+
const body = value;
|
|
17181
|
+
const allowed = new Set(["name", "title_pattern", "description", "priority", "tags", "variables", "project_id", "plan_id", "metadata"]);
|
|
17182
|
+
const unknown = Object.keys(body).find((key) => !allowed.has(key));
|
|
17183
|
+
if (unknown)
|
|
17184
|
+
return { ok: false, message: `unknown template field: ${unknown}` };
|
|
17185
|
+
if (Object.keys(body).length === 0)
|
|
17186
|
+
return { ok: false, message: "template patch must not be empty" };
|
|
17187
|
+
const templateLike = { name: body.name ?? "template", title_pattern: body.title_pattern ?? "template", ...body };
|
|
17188
|
+
const validated = validateTemplateCreate(templateLike);
|
|
17189
|
+
if (!validated.ok)
|
|
17190
|
+
return validated;
|
|
17191
|
+
const { name: _name, title_pattern: _title, tasks: _tasks, ...patch } = validated.input;
|
|
17192
|
+
return { ok: true, patch: {
|
|
17193
|
+
...body.name !== undefined ? { name: validated.input.name } : {},
|
|
17194
|
+
...body.title_pattern !== undefined ? { title_pattern: validated.input.title_pattern } : {},
|
|
17195
|
+
...patch,
|
|
17196
|
+
...body.description === null ? { description: null } : {},
|
|
17197
|
+
...body.project_id === null ? { project_id: null } : {},
|
|
17198
|
+
...body.plan_id === null ? { plan_id: null } : {}
|
|
17199
|
+
} };
|
|
17200
|
+
}
|
|
16832
17201
|
async function readJson(req) {
|
|
16833
17202
|
try {
|
|
16834
17203
|
const text = await req.text();
|
|
@@ -16889,12 +17258,13 @@ function normalizeImportSnapshot(raw) {
|
|
|
16889
17258
|
agents: arr(body["agents"]),
|
|
16890
17259
|
taskLists: arr(body["taskLists"]),
|
|
16891
17260
|
templates: arr(body["templates"]),
|
|
17261
|
+
templateTasks: arr(body["templateTasks"]),
|
|
16892
17262
|
auditHistory: arr(body["auditHistory"]),
|
|
16893
17263
|
tombstones: arr(body["tombstones"])
|
|
16894
17264
|
};
|
|
16895
17265
|
}
|
|
16896
17266
|
function countSnapshotRecords(s) {
|
|
16897
|
-
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);
|
|
17267
|
+
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);
|
|
16898
17268
|
}
|
|
16899
17269
|
async function handleV1Request(req, url, dependencies = {}) {
|
|
16900
17270
|
const path = url.pathname;
|
|
@@ -17464,6 +17834,40 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
17464
17834
|
if (id)
|
|
17465
17835
|
return error(405, `method ${method} not allowed on /v1/plans/:id`);
|
|
17466
17836
|
}
|
|
17837
|
+
if (resource === "templates") {
|
|
17838
|
+
if (!id && method === "GET") {
|
|
17839
|
+
const projectId = url.searchParams.get("project_id");
|
|
17840
|
+
const templates = (await store.templates.list()).filter((template) => projectId === null || template.project_id === projectId);
|
|
17841
|
+
return json2({ templates, count: templates.length });
|
|
17842
|
+
}
|
|
17843
|
+
if (!id && method === "POST") {
|
|
17844
|
+
const body = await readJson(req);
|
|
17845
|
+
const validated = validateTemplateCreate(body);
|
|
17846
|
+
if (!validated.ok)
|
|
17847
|
+
return error(400, validated.message);
|
|
17848
|
+
const template = await store.templates.create(validated.input, contextFromPrincipal(principal));
|
|
17849
|
+
return json2({ template: await store.templates.getWithTasks(template.id) }, 201);
|
|
17850
|
+
}
|
|
17851
|
+
if (!id)
|
|
17852
|
+
return error(405, `method ${method} not allowed on /v1/templates`);
|
|
17853
|
+
if (method === "GET") {
|
|
17854
|
+
const template = await store.templates.getWithTasks(id);
|
|
17855
|
+
return template ? json2({ template }) : error(404, "template not found");
|
|
17856
|
+
}
|
|
17857
|
+
if (method === "PATCH" || method === "PUT") {
|
|
17858
|
+
const body = await readJson(req);
|
|
17859
|
+
const validated = validateTemplatePatch(body);
|
|
17860
|
+
if (!validated.ok)
|
|
17861
|
+
return error(400, validated.message);
|
|
17862
|
+
const template = await store.templates.update(id, validated.patch, contextFromPrincipal(principal));
|
|
17863
|
+
return template ? json2({ template: await store.templates.getWithTasks(id) }) : error(404, "template not found");
|
|
17864
|
+
}
|
|
17865
|
+
if (method === "DELETE") {
|
|
17866
|
+
const deleted = await store.templates.delete(id, contextFromPrincipal(principal));
|
|
17867
|
+
return deleted ? json2({ deleted: true, id }) : error(404, "template not found");
|
|
17868
|
+
}
|
|
17869
|
+
return error(405, `method ${method} not allowed on /v1/templates/:id`);
|
|
17870
|
+
}
|
|
17467
17871
|
if (resource === "agents") {
|
|
17468
17872
|
if (!id && method === "GET") {
|
|
17469
17873
|
const agents = await store.agents.list();
|
|
@@ -39120,6 +39524,7 @@ var init_token_utils = __esm(() => {
|
|
|
39120
39524
|
|
|
39121
39525
|
// src/cli/cloud-router.ts
|
|
39122
39526
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
39527
|
+
import { resolve as resolvePath } from "path";
|
|
39123
39528
|
function cleanMode(value) {
|
|
39124
39529
|
const normalized = value?.trim().toLowerCase();
|
|
39125
39530
|
return normalized || null;
|
|
@@ -39402,6 +39807,39 @@ async function cloudListProjects(client) {
|
|
|
39402
39807
|
const envelope = res.raw;
|
|
39403
39808
|
return Array.isArray(envelope?.projects) ? envelope.projects : res.items;
|
|
39404
39809
|
}
|
|
39810
|
+
function cloudProjectSlug(value) {
|
|
39811
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
39812
|
+
}
|
|
39813
|
+
function cloudProjectPathBasename(value) {
|
|
39814
|
+
return value.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? value;
|
|
39815
|
+
}
|
|
39816
|
+
function uniqueProjectMatches(projects, predicate) {
|
|
39817
|
+
return [...new Map(projects.filter(predicate).map((project) => [project.id, project])).values()];
|
|
39818
|
+
}
|
|
39819
|
+
function resolveCloudProjectRef(projects, ref) {
|
|
39820
|
+
const input = ref.trim();
|
|
39821
|
+
const normalizedRef = input.toLowerCase();
|
|
39822
|
+
const pathLike = input.startsWith(".") || input.includes("/") || input.includes("\\");
|
|
39823
|
+
const normalizedPath = pathLike ? resolvePath(input) : undefined;
|
|
39824
|
+
const slug = cloudProjectSlug(pathLike ? cloudProjectPathBasename(input) : input);
|
|
39825
|
+
const matchGroups = [
|
|
39826
|
+
uniqueProjectMatches(projects, (project) => project.id.toLowerCase() === normalizedRef),
|
|
39827
|
+
uniqueProjectMatches(projects, (project) => project.path === input || normalizedPath !== undefined && resolvePath(project.path) === normalizedPath),
|
|
39828
|
+
uniqueProjectMatches(projects, (project) => project.name.toLowerCase() === normalizedRef),
|
|
39829
|
+
uniqueProjectMatches(projects, (project) => project.task_list_id === input || cloudProjectSlug(project.name) === slug || cloudProjectSlug(cloudProjectPathBasename(project.path)) === slug),
|
|
39830
|
+
uniqueProjectMatches(projects, (project) => project.id.toLowerCase().startsWith(normalizedRef))
|
|
39831
|
+
];
|
|
39832
|
+
for (const matches of matchGroups) {
|
|
39833
|
+
if (matches.length === 1)
|
|
39834
|
+
return matches[0].id;
|
|
39835
|
+
if (matches.length > 1)
|
|
39836
|
+
throw new Error(`Project reference is ambiguous: "${input}"`);
|
|
39837
|
+
}
|
|
39838
|
+
throw new Error(`Project not found: "${input}"`);
|
|
39839
|
+
}
|
|
39840
|
+
async function cloudResolveProjectRef(client, ref) {
|
|
39841
|
+
return resolveCloudProjectRef(await cloudListProjects(client), ref);
|
|
39842
|
+
}
|
|
39405
39843
|
async function cloudAddComment(client, taskId, input) {
|
|
39406
39844
|
const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/comments`, input);
|
|
39407
39845
|
const comment = raw && typeof raw === "object" && "comment" in raw ? raw.comment : raw;
|
|
@@ -39446,9 +39884,46 @@ async function cloudReleaseAgent(client, idOrName, sessionId) {
|
|
|
39446
39884
|
const env = raw ?? {};
|
|
39447
39885
|
return { agent: env.agent ?? null, released: env.released !== false };
|
|
39448
39886
|
}
|
|
39449
|
-
|
|
39887
|
+
async function cloudListTaskLists(client, projectId) {
|
|
39888
|
+
const query = projectId ? { project_id: projectId } : {};
|
|
39889
|
+
const raw = await requiredRemoteRoute(client, "/v1/task-lists", () => client.transport.get("/task-lists", { query }));
|
|
39890
|
+
const envelope = raw ?? {};
|
|
39891
|
+
if (Array.isArray(envelope.task_lists))
|
|
39892
|
+
return envelope.task_lists;
|
|
39893
|
+
if (Array.isArray(envelope.taskLists))
|
|
39894
|
+
return envelope.taskLists;
|
|
39895
|
+
return Array.isArray(raw) ? raw : [];
|
|
39896
|
+
}
|
|
39897
|
+
async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
39898
|
+
const input = ref.trim();
|
|
39899
|
+
const normalizedIdRef = input.toLowerCase();
|
|
39900
|
+
if (UUID_RE.test(input) && !projectId)
|
|
39901
|
+
return normalizedIdRef;
|
|
39902
|
+
const lists = await cloudListTaskLists(client, projectId);
|
|
39903
|
+
const exactIds = lists.filter((list) => list.id.toLowerCase() === normalizedIdRef);
|
|
39904
|
+
if (exactIds.length === 1)
|
|
39905
|
+
return exactIds[0].id;
|
|
39906
|
+
if (exactIds.length > 1) {
|
|
39907
|
+
throw new Error(`Task list reference is ambiguous: "${input}"`);
|
|
39908
|
+
}
|
|
39909
|
+
const slugs = lists.filter((list) => list.slug === input);
|
|
39910
|
+
if (slugs.length === 1)
|
|
39911
|
+
return slugs[0].id;
|
|
39912
|
+
if (slugs.length > 1) {
|
|
39913
|
+
throw new Error(`Task list reference is ambiguous: "${input}"`);
|
|
39914
|
+
}
|
|
39915
|
+
const prefixes = lists.filter((list) => list.id.toLowerCase().startsWith(normalizedIdRef));
|
|
39916
|
+
if (prefixes.length === 1)
|
|
39917
|
+
return prefixes[0].id;
|
|
39918
|
+
if (prefixes.length > 1) {
|
|
39919
|
+
throw new Error(`Task list reference is ambiguous: "${input}"`);
|
|
39920
|
+
}
|
|
39921
|
+
throw new Error(`Task list not found: "${input}"`);
|
|
39922
|
+
}
|
|
39923
|
+
var UUID_RE, CLOUD_MODES, VALID_STORAGE_MODES, completionCapabilityCache;
|
|
39450
39924
|
var init_cloud_router = __esm(() => {
|
|
39451
39925
|
init_redaction();
|
|
39926
|
+
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
39452
39927
|
CLOUD_MODES = new Set(["self_hosted", "cloud", "remote", "hybrid"]);
|
|
39453
39928
|
VALID_STORAGE_MODES = new Set(["local", ...CLOUD_MODES]);
|
|
39454
39929
|
completionCapabilityCache = new Map;
|
|
@@ -39735,6 +40210,17 @@ ${task.description}` : null
|
|
|
39735
40210
|
patch.estimated_minutes = estimate;
|
|
39736
40211
|
if (deadline !== undefined)
|
|
39737
40212
|
patch.due_at = deadline;
|
|
40213
|
+
if (typeof patch.project_id === "string" && patch.project_id) {
|
|
40214
|
+
patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
|
|
40215
|
+
}
|
|
40216
|
+
if (typeof patch.task_list_id === "string" && patch.task_list_id) {
|
|
40217
|
+
let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
|
|
40218
|
+
if (!scope) {
|
|
40219
|
+
const current = await cloudGetTask(cloud, task_id2);
|
|
40220
|
+
scope = current?.project_id ?? undefined;
|
|
40221
|
+
}
|
|
40222
|
+
patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
|
|
40223
|
+
}
|
|
39738
40224
|
if (version3 !== undefined)
|
|
39739
40225
|
patch.version = version3;
|
|
39740
40226
|
const updated = await cloudUpdateTask(cloud, task_id2, patch);
|
|
@@ -45748,6 +46234,63 @@ function registerTaskProjectTools(server, ctx) {
|
|
|
45748
46234
|
}
|
|
45749
46235
|
});
|
|
45750
46236
|
}
|
|
46237
|
+
if (shouldRegisterTool("move_task")) {
|
|
46238
|
+
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.", {
|
|
46239
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
46240
|
+
to_project: exports_external.string().optional().describe("Destination project ID, slug, or path"),
|
|
46241
|
+
to_list: exports_external.string().optional().describe("Destination task list (UUID, or slug resolved in the destination project)"),
|
|
46242
|
+
clear_list: exports_external.boolean().optional().describe("Detach from its task list (set task_list_id to null)"),
|
|
46243
|
+
version: exports_external.number().optional().describe("Expected version for optimistic locking")
|
|
46244
|
+
}, async ({ task_id, to_project, to_list, clear_list, version: version2 }) => {
|
|
46245
|
+
try {
|
|
46246
|
+
if (!to_project && !to_list && !clear_list) {
|
|
46247
|
+
throw new Error("Nothing to move: pass to_project, to_list, or clear_list.");
|
|
46248
|
+
}
|
|
46249
|
+
if (to_list && clear_list) {
|
|
46250
|
+
throw new Error("Use either to_list or clear_list, not both.");
|
|
46251
|
+
}
|
|
46252
|
+
const cloud = getTodosCloudClient();
|
|
46253
|
+
if (cloud) {
|
|
46254
|
+
const current2 = await cloudGetTask(cloud, task_id);
|
|
46255
|
+
if (!current2)
|
|
46256
|
+
throw new Error(`Task not found: ${task_id}`);
|
|
46257
|
+
const targetProjectId2 = to_project ? await cloudResolveProjectRef(cloud, to_project) : undefined;
|
|
46258
|
+
const scope = targetProjectId2 ?? current2.project_id ?? undefined;
|
|
46259
|
+
const patch = {};
|
|
46260
|
+
if (targetProjectId2 !== undefined)
|
|
46261
|
+
patch.project_id = targetProjectId2;
|
|
46262
|
+
if (to_list)
|
|
46263
|
+
patch.task_list_id = await cloudResolveTaskListRef(cloud, to_list, scope);
|
|
46264
|
+
else if (clear_list)
|
|
46265
|
+
patch.task_list_id = null;
|
|
46266
|
+
else if (targetProjectId2 && targetProjectId2 !== current2.project_id)
|
|
46267
|
+
patch.task_list_id = null;
|
|
46268
|
+
if (version2 !== undefined)
|
|
46269
|
+
patch.version = version2;
|
|
46270
|
+
const task2 = await cloudUpdateTask(cloud, task_id, patch);
|
|
46271
|
+
return { content: [{ type: "text", text: formatTask(task2) }] };
|
|
46272
|
+
}
|
|
46273
|
+
const resolvedId = resolveId(task_id);
|
|
46274
|
+
const current = getTask(resolvedId);
|
|
46275
|
+
if (!current)
|
|
46276
|
+
throw new Error(`Task not found: ${task_id}`);
|
|
46277
|
+
const targetProjectId = to_project ? resolveId(to_project, "projects") : undefined;
|
|
46278
|
+
const updates = {};
|
|
46279
|
+
if (targetProjectId !== undefined)
|
|
46280
|
+
updates.project_id = targetProjectId;
|
|
46281
|
+
if (to_list)
|
|
46282
|
+
updates.task_list_id = resolveId(to_list, "task_lists");
|
|
46283
|
+
else if (clear_list)
|
|
46284
|
+
updates.task_list_id = null;
|
|
46285
|
+
else if (targetProjectId && targetProjectId !== current.project_id)
|
|
46286
|
+
updates.task_list_id = null;
|
|
46287
|
+
const task = updateWithOptionalVersion(resolvedId, updates, version2);
|
|
46288
|
+
return { content: [{ type: "text", text: formatTask(task) }] };
|
|
46289
|
+
} catch (e) {
|
|
46290
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
46291
|
+
}
|
|
46292
|
+
});
|
|
46293
|
+
}
|
|
45751
46294
|
if (shouldRegisterTool("reschedule_task")) {
|
|
45752
46295
|
server.tool("reschedule_task", "Update a task's deadline.", {
|
|
45753
46296
|
task_id: exports_external.string().describe("Task ID"),
|
|
@@ -73450,6 +73993,7 @@ function registerTaskMetaTools(server, ctx) {
|
|
|
73450
73993
|
complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
|
|
73451
73994
|
cancel_task: "cancel_task \u2014 Cancel a task. Params: task_id, version",
|
|
73452
73995
|
reassign_task: "reassign_task \u2014 Change task assignee. Params: task_id, new_assignee, version",
|
|
73996
|
+
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",
|
|
73453
73997
|
reschedule_task: "reschedule_task \u2014 Update deadline. Params: task_id, deadline, version",
|
|
73454
73998
|
prioritize_task: "prioritize_task \u2014 Set priority. Params: task_id, priority, version",
|
|
73455
73999
|
search_tasks: "search_tasks \u2014 Full-text search. Params: query, project_id, status, limit",
|
|
@@ -84944,9 +85488,9 @@ ${task2.id.slice(0, 8)} | ${task2.priority} | ${task2.title}` }] };
|
|
|
84944
85488
|
if (shouldRegisterTool("delete_template")) {
|
|
84945
85489
|
server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
|
|
84946
85490
|
try {
|
|
84947
|
-
const { deleteTemplate:
|
|
85491
|
+
const { deleteTemplate: deleteTemplate3 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
84948
85492
|
const resolvedId = resolveId(id, "task_templates");
|
|
84949
|
-
const deleted =
|
|
85493
|
+
const deleted = deleteTemplate3(resolvedId);
|
|
84950
85494
|
return { content: [{ type: "text", text: deleted ? "Template deleted." : "Template not found." }] };
|
|
84951
85495
|
} catch (e) {
|
|
84952
85496
|
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|