@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.
Files changed (37) hide show
  1. package/dist/cli/cloud-router.d.ts +12 -1
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  5. package/dist/cli/index.js +885 -76
  6. package/dist/contracts.js +56 -50
  7. package/dist/db/storage-tombstones.d.ts +1 -1
  8. package/dist/db/storage-tombstones.d.ts.map +1 -1
  9. package/dist/db/templates.d.ts.map +1 -1
  10. package/dist/index.js +170 -56
  11. package/dist/lib/template-semantics.d.ts +8 -0
  12. package/dist/lib/template-semantics.d.ts.map +1 -0
  13. package/dist/mcp/index.js +605 -61
  14. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  15. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
  16. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  17. package/dist/mcp.js +1 -1
  18. package/dist/registry.js +56 -50
  19. package/dist/release-provenance.json +5 -5
  20. package/dist/sdk/index.js +35 -0
  21. package/dist/sdk/v1.generated.d.ts +105 -1
  22. package/dist/sdk/v1.generated.d.ts.map +1 -1
  23. package/dist/server/index.js +611 -67
  24. package/dist/server/openapi.d.ts +496 -0
  25. package/dist/server/openapi.d.ts.map +1 -1
  26. package/dist/server/v1.d.ts.map +1 -1
  27. package/dist/storage/interfaces.d.ts +3 -2
  28. package/dist/storage/interfaces.d.ts.map +1 -1
  29. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  30. package/dist/storage/postgres-sync.d.ts +1 -1
  31. package/dist/storage/postgres-sync.d.ts.map +1 -1
  32. package/dist/storage/shadow-outbox.d.ts.map +1 -1
  33. package/dist/storage/shadow.d.ts +1 -1
  34. package/dist/storage/shadow.d.ts.map +1 -1
  35. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  36. package/dist/storage.js +169 -55
  37. package/package.json +1 -1
@@ -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.93",
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) => store.delete("templates", 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
- return template ? { ...template, tasks: [] } : null;
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 ?? existing.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
- return store.upsert("templates", {
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
- }, context);
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: () => 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 deleteTemplate(id, db) {
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
- if (!condition || condition.trim() === "")
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
- const merged = { ...provided };
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
- let result = text;
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 = deleteTemplate(id);
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) => deleteTemplate(id, database()),
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
- var CLOUD_MODES, VALID_STORAGE_MODES, completionCapabilityCache;
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: deleteTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
85491
+ const { deleteTemplate: deleteTemplate3 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
84948
85492
  const resolvedId = resolveId(id, "task_templates");
84949
- const deleted = deleteTemplate2(resolvedId);
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 };