@hasna/todos 0.11.94 → 0.11.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.94",
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();
@@ -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" },
@@ -16200,6 +16316,39 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
16200
16316
  agent_id: { type: "string", minLength: 1 },
16201
16317
  status: { type: "string", enum: ["active", "completed", "archived"] }
16202
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
+ }
16203
16352
  }
16204
16353
  }
16205
16354
  },
@@ -16502,6 +16651,41 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
16502
16651
  responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
16503
16652
  }
16504
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
+ },
16505
16689
  "/v1/task-lists": {
16506
16690
  get: {
16507
16691
  operationId: "listTaskLists",
@@ -16579,6 +16763,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
16579
16763
  agents: { type: "array", items: { type: "object" } },
16580
16764
  taskLists: { type: "array", items: { type: "object" } },
16581
16765
  templates: { type: "array", items: { type: "object" } },
16766
+ templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
16582
16767
  auditHistory: { type: "array", items: { type: "object" } },
16583
16768
  tombstones: { type: "array", items: { type: "object" } }
16584
16769
  }
@@ -16615,7 +16800,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
16615
16800
  }
16616
16801
  };
16617
16802
  }
16618
- var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema;
16803
+ var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
16619
16804
  var init_openapi = __esm(() => {
16620
16805
  init_package_version();
16621
16806
  taskSchema = {
@@ -16692,6 +16877,72 @@ var init_openapi = __esm(() => {
16692
16877
  updated_at: { type: "string", format: "date-time" }
16693
16878
  }
16694
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
+ };
16695
16946
  });
16696
16947
 
16697
16948
  // src/server/v1.ts
@@ -16831,6 +17082,122 @@ function validatePlanCreate(value) {
16831
17082
  }
16832
17083
  };
16833
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
+ }
16834
17201
  async function readJson(req) {
16835
17202
  try {
16836
17203
  const text = await req.text();
@@ -16891,12 +17258,13 @@ function normalizeImportSnapshot(raw) {
16891
17258
  agents: arr(body["agents"]),
16892
17259
  taskLists: arr(body["taskLists"]),
16893
17260
  templates: arr(body["templates"]),
17261
+ templateTasks: arr(body["templateTasks"]),
16894
17262
  auditHistory: arr(body["auditHistory"]),
16895
17263
  tombstones: arr(body["tombstones"])
16896
17264
  };
16897
17265
  }
16898
17266
  function countSnapshotRecords(s) {
16899
- 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);
16900
17268
  }
16901
17269
  async function handleV1Request(req, url, dependencies = {}) {
16902
17270
  const path = url.pathname;
@@ -17466,6 +17834,40 @@ async function handleV1Request(req, url, dependencies = {}) {
17466
17834
  if (id)
17467
17835
  return error(405, `method ${method} not allowed on /v1/plans/:id`);
17468
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
+ }
17469
17871
  if (resource === "agents") {
17470
17872
  if (!id && method === "GET") {
17471
17873
  const agents = await store.agents.list();
@@ -85086,9 +85488,9 @@ ${task2.id.slice(0, 8)} | ${task2.priority} | ${task2.title}` }] };
85086
85488
  if (shouldRegisterTool("delete_template")) {
85087
85489
  server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
85088
85490
  try {
85089
- const { deleteTemplate: deleteTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
85491
+ const { deleteTemplate: deleteTemplate3 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
85090
85492
  const resolvedId = resolveId(id, "task_templates");
85091
- const deleted = deleteTemplate2(resolvedId);
85493
+ const deleted = deleteTemplate3(resolvedId);
85092
85494
  return { content: [{ type: "text", text: deleted ? "Template deleted." : "Template not found." }] };
85093
85495
  } catch (e) {
85094
85496
  return { content: [{ type: "text", text: formatError2(e) }], isError: true };