@hasna/todos 0.11.10 → 0.11.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -10954,6 +10954,36 @@ function ensureSchema(db) {
10954
10954
  ensureColumn("projects", "org_id", "TEXT");
10955
10955
  ensureColumn("plans", "task_list_id", "TEXT");
10956
10956
  ensureColumn("plans", "agent_id", "TEXT");
10957
+ ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
10958
+ ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
10959
+ ensureColumn("template_tasks", "condition", "TEXT");
10960
+ ensureColumn("template_tasks", "include_template_id", "TEXT");
10961
+ ensureTable("template_versions", `
10962
+ CREATE TABLE template_versions (
10963
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
10964
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
10965
+ version INTEGER NOT NULL,
10966
+ snapshot TEXT NOT NULL,
10967
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
10968
+ )`);
10969
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
10970
+ ensureColumn("webhooks", "project_id", "TEXT");
10971
+ ensureColumn("webhooks", "task_list_id", "TEXT");
10972
+ ensureColumn("webhooks", "agent_id", "TEXT");
10973
+ ensureColumn("webhooks", "task_id", "TEXT");
10974
+ ensureTable("webhook_deliveries", `
10975
+ CREATE TABLE webhook_deliveries (
10976
+ id TEXT PRIMARY KEY,
10977
+ webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
10978
+ event TEXT NOT NULL,
10979
+ payload TEXT NOT NULL,
10980
+ status_code INTEGER,
10981
+ response TEXT,
10982
+ attempt INTEGER NOT NULL DEFAULT 1,
10983
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
10984
+ )`);
10985
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
10986
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
10957
10987
  ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
10958
10988
  ensureColumn("task_comments", "progress_pct", "INTEGER");
10959
10989
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
@@ -11642,6 +11672,26 @@ var init_database = __esm(() => {
11642
11672
  CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
11643
11673
 
11644
11674
  INSERT OR IGNORE INTO _migrations (id) VALUES (37);
11675
+ `,
11676
+ `
11677
+ ALTER TABLE task_templates ADD COLUMN variables TEXT DEFAULT '[]';
11678
+ INSERT OR IGNORE INTO _migrations (id) VALUES (38);
11679
+ `,
11680
+ `
11681
+ ALTER TABLE template_tasks ADD COLUMN condition TEXT;
11682
+ ALTER TABLE template_tasks ADD COLUMN include_template_id TEXT;
11683
+ ALTER TABLE task_templates ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
11684
+
11685
+ CREATE TABLE IF NOT EXISTS template_versions (
11686
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
11687
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
11688
+ version INTEGER NOT NULL,
11689
+ snapshot TEXT NOT NULL,
11690
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
11691
+ );
11692
+ CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id);
11693
+
11694
+ INSERT OR IGNORE INTO _migrations (id) VALUES (39);
11645
11695
  `
11646
11696
  ];
11647
11697
  });
@@ -12080,18 +12130,37 @@ var init_recurrence = __esm(() => {
12080
12130
  var exports_webhooks = {};
12081
12131
  __export(exports_webhooks, {
12082
12132
  listWebhooks: () => listWebhooks,
12133
+ listDeliveries: () => listDeliveries,
12083
12134
  getWebhook: () => getWebhook,
12084
12135
  dispatchWebhook: () => dispatchWebhook,
12085
12136
  deleteWebhook: () => deleteWebhook,
12086
12137
  createWebhook: () => createWebhook
12087
12138
  });
12088
12139
  function rowToWebhook(row) {
12089
- return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
12140
+ return {
12141
+ ...row,
12142
+ events: JSON.parse(row.events || "[]"),
12143
+ active: !!row.active,
12144
+ project_id: row.project_id || null,
12145
+ task_list_id: row.task_list_id || null,
12146
+ agent_id: row.agent_id || null,
12147
+ task_id: row.task_id || null
12148
+ };
12090
12149
  }
12091
12150
  function createWebhook(input, db) {
12092
12151
  const d = db || getDatabase();
12093
12152
  const id = uuid();
12094
- d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
12153
+ d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
12154
+ id,
12155
+ input.url,
12156
+ JSON.stringify(input.events || []),
12157
+ input.secret || null,
12158
+ input.project_id || null,
12159
+ input.task_list_id || null,
12160
+ input.agent_id || null,
12161
+ input.task_id || null,
12162
+ now()
12163
+ ]);
12095
12164
  return getWebhook(id, d);
12096
12165
  }
12097
12166
  function getWebhook(id, db) {
@@ -12107,22 +12176,69 @@ function deleteWebhook(id, db) {
12107
12176
  const d = db || getDatabase();
12108
12177
  return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
12109
12178
  }
12179
+ function listDeliveries(webhookId, limit = 50, db) {
12180
+ const d = db || getDatabase();
12181
+ if (webhookId) {
12182
+ return d.query("SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?").all(webhookId, limit);
12183
+ }
12184
+ return d.query("SELECT * FROM webhook_deliveries ORDER BY created_at DESC LIMIT ?").all(limit);
12185
+ }
12186
+ function logDelivery(d, webhookId, event, payload, statusCode, response, attempt) {
12187
+ const id = uuid();
12188
+ d.run(`INSERT INTO webhook_deliveries (id, webhook_id, event, payload, status_code, response, attempt, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, webhookId, event, payload, statusCode, response, attempt, now()]);
12189
+ }
12190
+ function matchesScope(wh, payload) {
12191
+ if (wh.project_id && payload.project_id !== wh.project_id)
12192
+ return false;
12193
+ if (wh.task_list_id && payload.task_list_id !== wh.task_list_id)
12194
+ return false;
12195
+ if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
12196
+ return false;
12197
+ if (wh.task_id && payload.id !== wh.task_id)
12198
+ return false;
12199
+ return true;
12200
+ }
12201
+ async function deliverWebhook(wh, event, body, attempt, db) {
12202
+ try {
12203
+ const headers = { "Content-Type": "application/json" };
12204
+ if (wh.secret) {
12205
+ const encoder = new TextEncoder;
12206
+ const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
12207
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
12208
+ headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
12209
+ }
12210
+ const resp = await fetch(wh.url, { method: "POST", headers, body });
12211
+ const respText = await resp.text().catch(() => "");
12212
+ logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
12213
+ if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
12214
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
12215
+ setTimeout(() => {
12216
+ deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
12217
+ }, delay);
12218
+ }
12219
+ } catch (err) {
12220
+ const errorMsg = err instanceof Error ? err.message : String(err);
12221
+ logDelivery(db, wh.id, event, body, null, errorMsg.slice(0, 1000), attempt);
12222
+ if (attempt < MAX_RETRY_ATTEMPTS) {
12223
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
12224
+ setTimeout(() => {
12225
+ deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
12226
+ }, delay);
12227
+ }
12228
+ }
12229
+ }
12110
12230
  async function dispatchWebhook(event, payload, db) {
12111
- const webhooks = listWebhooks(db).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
12231
+ const d = db || getDatabase();
12232
+ const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
12233
+ const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
12112
12234
  for (const wh of webhooks) {
12113
- try {
12114
- const body = JSON.stringify({ event, payload, timestamp: now() });
12115
- const headers = { "Content-Type": "application/json" };
12116
- if (wh.secret) {
12117
- const encoder = new TextEncoder;
12118
- const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
12119
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
12120
- headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
12121
- }
12122
- fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
12123
- } catch {}
12235
+ if (!matchesScope(wh, payloadObj))
12236
+ continue;
12237
+ const body = JSON.stringify({ event, payload, timestamp: now() });
12238
+ deliverWebhook(wh, event, body, 1, d).catch(() => {});
12124
12239
  }
12125
12240
  }
12241
+ var MAX_RETRY_ATTEMPTS = 3, RETRY_BASE_DELAY_MS = 1000;
12126
12242
  var init_webhooks = __esm(() => {
12127
12243
  init_database();
12128
12244
  });
@@ -12133,10 +12249,17 @@ __export(exports_templates, {
12133
12249
  updateTemplate: () => updateTemplate,
12134
12250
  tasksFromTemplate: () => tasksFromTemplate,
12135
12251
  taskFromTemplate: () => taskFromTemplate,
12252
+ resolveVariables: () => resolveVariables,
12253
+ previewTemplate: () => previewTemplate,
12136
12254
  listTemplates: () => listTemplates,
12255
+ listTemplateVersions: () => listTemplateVersions,
12256
+ importTemplate: () => importTemplate,
12137
12257
  getTemplateWithTasks: () => getTemplateWithTasks,
12258
+ getTemplateVersion: () => getTemplateVersion,
12138
12259
  getTemplateTasks: () => getTemplateTasks,
12139
12260
  getTemplate: () => getTemplate,
12261
+ exportTemplate: () => exportTemplate,
12262
+ evaluateCondition: () => evaluateCondition,
12140
12263
  deleteTemplate: () => deleteTemplate,
12141
12264
  createTemplate: () => createTemplate,
12142
12265
  addTemplateTasks: () => addTemplateTasks
@@ -12145,8 +12268,10 @@ function rowToTemplate(row) {
12145
12268
  return {
12146
12269
  ...row,
12147
12270
  tags: JSON.parse(row.tags || "[]"),
12271
+ variables: JSON.parse(row.variables || "[]"),
12148
12272
  metadata: JSON.parse(row.metadata || "{}"),
12149
- priority: row.priority || "medium"
12273
+ priority: row.priority || "medium",
12274
+ version: row.version ?? 1
12150
12275
  };
12151
12276
  }
12152
12277
  function rowToTemplateTask(row) {
@@ -12155,7 +12280,9 @@ function rowToTemplateTask(row) {
12155
12280
  tags: JSON.parse(row.tags || "[]"),
12156
12281
  depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
12157
12282
  metadata: JSON.parse(row.metadata || "{}"),
12158
- priority: row.priority || "medium"
12283
+ priority: row.priority || "medium",
12284
+ condition: row.condition ?? null,
12285
+ include_template_id: row.include_template_id ?? null
12159
12286
  };
12160
12287
  }
12161
12288
  function resolveTemplateId(id, d) {
@@ -12164,14 +12291,15 @@ function resolveTemplateId(id, d) {
12164
12291
  function createTemplate(input, db) {
12165
12292
  const d = db || getDatabase();
12166
12293
  const id = uuid();
12167
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
12168
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
12294
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
12295
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
12169
12296
  id,
12170
12297
  input.name,
12171
12298
  input.title_pattern,
12172
12299
  input.description || null,
12173
12300
  input.priority || "medium",
12174
12301
  JSON.stringify(input.tags || []),
12302
+ JSON.stringify(input.variables || []),
12175
12303
  input.project_id || null,
12176
12304
  input.plan_id || null,
12177
12305
  JSON.stringify(input.metadata || {}),
@@ -12206,7 +12334,23 @@ function updateTemplate(id, updates, db) {
12206
12334
  const resolved = resolveTemplateId(id, d);
12207
12335
  if (!resolved)
12208
12336
  return null;
12209
- const sets = [];
12337
+ const current = getTemplateWithTasks(resolved, d);
12338
+ if (current) {
12339
+ const snapshot = JSON.stringify({
12340
+ name: current.name,
12341
+ title_pattern: current.title_pattern,
12342
+ description: current.description,
12343
+ priority: current.priority,
12344
+ tags: current.tags,
12345
+ variables: current.variables,
12346
+ project_id: current.project_id,
12347
+ plan_id: current.plan_id,
12348
+ metadata: current.metadata,
12349
+ tasks: current.tasks
12350
+ });
12351
+ d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
12352
+ }
12353
+ const sets = ["version = version + 1"];
12210
12354
  const values = [];
12211
12355
  if (updates.name !== undefined) {
12212
12356
  sets.push("name = ?");
@@ -12228,6 +12372,10 @@ function updateTemplate(id, updates, db) {
12228
12372
  sets.push("tags = ?");
12229
12373
  values.push(JSON.stringify(updates.tags));
12230
12374
  }
12375
+ if (updates.variables !== undefined) {
12376
+ sets.push("variables = ?");
12377
+ values.push(JSON.stringify(updates.variables));
12378
+ }
12231
12379
  if (updates.project_id !== undefined) {
12232
12380
  sets.push("project_id = ?");
12233
12381
  values.push(updates.project_id);
@@ -12240,8 +12388,6 @@ function updateTemplate(id, updates, db) {
12240
12388
  sets.push("metadata = ?");
12241
12389
  values.push(JSON.stringify(updates.metadata));
12242
12390
  }
12243
- if (sets.length === 0)
12244
- return getTemplate(resolved, d);
12245
12391
  values.push(resolved);
12246
12392
  d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
12247
12393
  return getTemplate(resolved, d);
@@ -12271,8 +12417,8 @@ function addTemplateTasks(templateId, tasks, db) {
12271
12417
  for (let i = 0;i < tasks.length; i++) {
12272
12418
  const task = tasks[i];
12273
12419
  const id = uuid();
12274
- d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, depends_on_positions, metadata, created_at)
12275
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
12420
+ d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, condition, include_template_id, depends_on_positions, metadata, created_at)
12421
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
12276
12422
  id,
12277
12423
  templateId,
12278
12424
  i,
@@ -12281,6 +12427,8 @@ function addTemplateTasks(templateId, tasks, db) {
12281
12427
  task.priority || "medium",
12282
12428
  JSON.stringify(task.tags || []),
12283
12429
  task.task_type || null,
12430
+ task.condition || null,
12431
+ task.include_template_id || null,
12284
12432
  JSON.stringify(task.depends_on || []),
12285
12433
  JSON.stringify(task.metadata || {}),
12286
12434
  now()
@@ -12308,11 +12456,142 @@ function getTemplateTasks(templateId, db) {
12308
12456
  const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
12309
12457
  return rows.map(rowToTemplateTask);
12310
12458
  }
12311
- function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
12459
+ function evaluateCondition(condition, variables) {
12460
+ if (!condition || condition.trim() === "")
12461
+ return true;
12462
+ const trimmed = condition.trim();
12463
+ const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
12464
+ if (eqMatch) {
12465
+ const varName = eqMatch[1];
12466
+ const expected = eqMatch[2].trim();
12467
+ return (variables[varName] ?? "") === expected;
12468
+ }
12469
+ const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
12470
+ if (neqMatch) {
12471
+ const varName = neqMatch[1];
12472
+ const expected = neqMatch[2].trim();
12473
+ return (variables[varName] ?? "") !== expected;
12474
+ }
12475
+ const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
12476
+ if (falsyMatch) {
12477
+ const varName = falsyMatch[1];
12478
+ const val = variables[varName];
12479
+ return !val || val === "" || val === "false";
12480
+ }
12481
+ const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
12482
+ if (truthyMatch) {
12483
+ const varName = truthyMatch[1];
12484
+ const val = variables[varName];
12485
+ return !!val && val !== "" && val !== "false";
12486
+ }
12487
+ return true;
12488
+ }
12489
+ function exportTemplate(id, db) {
12490
+ const d = db || getDatabase();
12491
+ const template = getTemplateWithTasks(id, d);
12492
+ if (!template)
12493
+ throw new Error(`Template not found: ${id}`);
12494
+ return {
12495
+ name: template.name,
12496
+ title_pattern: template.title_pattern,
12497
+ description: template.description,
12498
+ priority: template.priority,
12499
+ tags: template.tags,
12500
+ variables: template.variables,
12501
+ project_id: template.project_id,
12502
+ plan_id: template.plan_id,
12503
+ metadata: template.metadata,
12504
+ tasks: template.tasks.map((t) => ({
12505
+ position: t.position,
12506
+ title_pattern: t.title_pattern,
12507
+ description: t.description,
12508
+ priority: t.priority,
12509
+ tags: t.tags,
12510
+ task_type: t.task_type,
12511
+ condition: t.condition,
12512
+ include_template_id: t.include_template_id,
12513
+ depends_on_positions: t.depends_on_positions,
12514
+ metadata: t.metadata
12515
+ }))
12516
+ };
12517
+ }
12518
+ function importTemplate(json, db) {
12519
+ const d = db || getDatabase();
12520
+ const taskInputs = (json.tasks || []).map((t) => ({
12521
+ title_pattern: t.title_pattern,
12522
+ description: t.description ?? undefined,
12523
+ priority: t.priority,
12524
+ tags: t.tags,
12525
+ task_type: t.task_type ?? undefined,
12526
+ condition: t.condition ?? undefined,
12527
+ include_template_id: t.include_template_id ?? undefined,
12528
+ depends_on: t.depends_on_positions,
12529
+ metadata: t.metadata
12530
+ }));
12531
+ return createTemplate({
12532
+ name: json.name,
12533
+ title_pattern: json.title_pattern,
12534
+ description: json.description ?? undefined,
12535
+ priority: json.priority,
12536
+ tags: json.tags,
12537
+ variables: json.variables,
12538
+ project_id: json.project_id ?? undefined,
12539
+ plan_id: json.plan_id ?? undefined,
12540
+ metadata: json.metadata,
12541
+ tasks: taskInputs
12542
+ }, d);
12543
+ }
12544
+ function getTemplateVersion(id, version, db) {
12545
+ const d = db || getDatabase();
12546
+ const resolved = resolveTemplateId(id, d);
12547
+ if (!resolved)
12548
+ return null;
12549
+ const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
12550
+ return row || null;
12551
+ }
12552
+ function listTemplateVersions(id, db) {
12553
+ const d = db || getDatabase();
12554
+ const resolved = resolveTemplateId(id, d);
12555
+ if (!resolved)
12556
+ return [];
12557
+ return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
12558
+ }
12559
+ function resolveVariables(templateVars, provided) {
12560
+ const merged = { ...provided };
12561
+ for (const v of templateVars) {
12562
+ if (merged[v.name] === undefined && v.default !== undefined) {
12563
+ merged[v.name] = v.default;
12564
+ }
12565
+ }
12566
+ const missing = [];
12567
+ for (const v of templateVars) {
12568
+ if (v.required && merged[v.name] === undefined) {
12569
+ missing.push(v.name);
12570
+ }
12571
+ }
12572
+ if (missing.length > 0) {
12573
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
12574
+ }
12575
+ return merged;
12576
+ }
12577
+ function substituteVars(text, variables) {
12578
+ let result = text;
12579
+ for (const [key, val] of Object.entries(variables)) {
12580
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
12581
+ }
12582
+ return result;
12583
+ }
12584
+ function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
12312
12585
  const d = db || getDatabase();
12313
12586
  const template = getTemplateWithTasks(templateId, d);
12314
12587
  if (!template)
12315
12588
  throw new Error(`Template not found: ${templateId}`);
12589
+ const visited = _visitedTemplateIds || new Set;
12590
+ if (visited.has(template.id)) {
12591
+ throw new Error(`Circular template reference detected: ${template.id}`);
12592
+ }
12593
+ visited.add(template.id);
12594
+ const resolved = resolveVariables(template.variables, variables);
12316
12595
  if (template.tasks.length === 0) {
12317
12596
  const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
12318
12597
  const task = createTask(input, d);
@@ -12320,16 +12599,27 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
12320
12599
  }
12321
12600
  const createdTasks = [];
12322
12601
  const positionToId = new Map;
12602
+ const skippedPositions = new Set;
12323
12603
  for (const tt of template.tasks) {
12324
- let title = tt.title_pattern;
12325
- let desc = tt.description;
12326
- if (variables) {
12327
- for (const [key, val] of Object.entries(variables)) {
12328
- title = title.replace(new RegExp(`\\{${key}\\}`, "g"), val);
12329
- if (desc)
12330
- desc = desc.replace(new RegExp(`\\{${key}\\}`, "g"), val);
12604
+ if (tt.include_template_id) {
12605
+ const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
12606
+ createdTasks.push(...includedTasks);
12607
+ if (includedTasks.length > 0) {
12608
+ positionToId.set(tt.position, includedTasks[0].id);
12609
+ } else {
12610
+ skippedPositions.add(tt.position);
12331
12611
  }
12612
+ continue;
12332
12613
  }
12614
+ if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
12615
+ skippedPositions.add(tt.position);
12616
+ continue;
12617
+ }
12618
+ let title = tt.title_pattern;
12619
+ let desc = tt.description;
12620
+ title = substituteVars(title, resolved);
12621
+ if (desc)
12622
+ desc = substituteVars(desc, resolved);
12333
12623
  const task = createTask({
12334
12624
  title,
12335
12625
  description: desc ?? undefined,
@@ -12344,8 +12634,14 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
12344
12634
  positionToId.set(tt.position, task.id);
12345
12635
  }
12346
12636
  for (const tt of template.tasks) {
12637
+ if (skippedPositions.has(tt.position))
12638
+ continue;
12639
+ if (tt.include_template_id)
12640
+ continue;
12347
12641
  const deps = tt.depends_on_positions;
12348
12642
  for (const depPos of deps) {
12643
+ if (skippedPositions.has(depPos))
12644
+ continue;
12349
12645
  const taskId = positionToId.get(tt.position);
12350
12646
  const depId = positionToId.get(depPos);
12351
12647
  if (taskId && depId) {
@@ -12355,6 +12651,47 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
12355
12651
  }
12356
12652
  return createdTasks;
12357
12653
  }
12654
+ function previewTemplate(templateId, variables, db) {
12655
+ const d = db || getDatabase();
12656
+ const template = getTemplateWithTasks(templateId, d);
12657
+ if (!template)
12658
+ throw new Error(`Template not found: ${templateId}`);
12659
+ const resolved = resolveVariables(template.variables, variables);
12660
+ const tasks = [];
12661
+ if (template.tasks.length === 0) {
12662
+ tasks.push({
12663
+ position: 0,
12664
+ title: substituteVars(template.title_pattern, resolved),
12665
+ description: template.description ? substituteVars(template.description, resolved) : null,
12666
+ priority: template.priority,
12667
+ tags: template.tags,
12668
+ task_type: null,
12669
+ depends_on_positions: []
12670
+ });
12671
+ } else {
12672
+ for (const tt of template.tasks) {
12673
+ if (tt.condition && !evaluateCondition(tt.condition, resolved))
12674
+ continue;
12675
+ tasks.push({
12676
+ position: tt.position,
12677
+ title: substituteVars(tt.title_pattern, resolved),
12678
+ description: tt.description ? substituteVars(tt.description, resolved) : null,
12679
+ priority: tt.priority,
12680
+ tags: tt.tags,
12681
+ task_type: tt.task_type,
12682
+ depends_on_positions: tt.depends_on_positions
12683
+ });
12684
+ }
12685
+ }
12686
+ return {
12687
+ template_id: template.id,
12688
+ template_name: template.name,
12689
+ description: template.description,
12690
+ variables: template.variables,
12691
+ resolved_variables: resolved,
12692
+ tasks
12693
+ };
12694
+ }
12358
12695
  var init_templates = __esm(() => {
12359
12696
  init_database();
12360
12697
  init_tasks();
@@ -14439,6 +14776,118 @@ var init_burndown = __esm(() => {
14439
14776
  init_database();
14440
14777
  });
14441
14778
 
14779
+ // src/db/builtin-templates.ts
14780
+ var exports_builtin_templates = {};
14781
+ __export(exports_builtin_templates, {
14782
+ initBuiltinTemplates: () => initBuiltinTemplates,
14783
+ BUILTIN_TEMPLATES: () => BUILTIN_TEMPLATES
14784
+ });
14785
+ function initBuiltinTemplates(db) {
14786
+ const d = db || getDatabase();
14787
+ const existing = listTemplates(d);
14788
+ const existingNames = new Set(existing.map((t) => t.name));
14789
+ let created = 0;
14790
+ let skipped = 0;
14791
+ const names = [];
14792
+ for (const bt of BUILTIN_TEMPLATES) {
14793
+ if (existingNames.has(bt.name)) {
14794
+ skipped++;
14795
+ continue;
14796
+ }
14797
+ const tasks = bt.tasks.map((t) => ({
14798
+ title_pattern: t.title_pattern,
14799
+ description: t.description,
14800
+ priority: t.priority,
14801
+ tags: t.tags,
14802
+ task_type: t.task_type,
14803
+ depends_on: t.depends_on_positions || t.depends_on,
14804
+ metadata: t.metadata
14805
+ }));
14806
+ createTemplate({
14807
+ name: bt.name,
14808
+ title_pattern: `${bt.name}: {${bt.variables[0]?.name || "name"}}`,
14809
+ description: bt.description,
14810
+ variables: bt.variables,
14811
+ tasks
14812
+ }, d);
14813
+ created++;
14814
+ names.push(bt.name);
14815
+ }
14816
+ return { created, skipped, names };
14817
+ }
14818
+ var BUILTIN_TEMPLATES;
14819
+ var init_builtin_templates = __esm(() => {
14820
+ init_database();
14821
+ init_templates();
14822
+ BUILTIN_TEMPLATES = [
14823
+ {
14824
+ name: "open-source-project",
14825
+ description: "Full open-source project bootstrap \u2014 scaffold to publish",
14826
+ variables: [
14827
+ { name: "name", required: true, description: "Service name" },
14828
+ { name: "org", required: false, default: "hasna", description: "GitHub org" }
14829
+ ],
14830
+ tasks: [
14831
+ { position: 0, title_pattern: "Scaffold {name} package structure", priority: "critical" },
14832
+ { position: 1, title_pattern: "Create {name} SQLite database + migrations", priority: "critical", depends_on_positions: [0] },
14833
+ { position: 2, title_pattern: "Implement {name} CRUD operations", priority: "high", depends_on_positions: [1] },
14834
+ { position: 3, title_pattern: "Build {name} MCP server with standard tools", priority: "high", depends_on_positions: [2] },
14835
+ { position: 4, title_pattern: "Build {name} CLI with Commander.js", priority: "high", depends_on_positions: [2] },
14836
+ { position: 5, title_pattern: "Build {name} REST API", priority: "medium", depends_on_positions: [2] },
14837
+ { position: 6, title_pattern: "Write unit tests for {name}", priority: "high", depends_on_positions: [2, 3, 4] },
14838
+ { position: 7, title_pattern: "Add Apache 2.0 license and README", priority: "medium", depends_on_positions: [0] },
14839
+ { position: 8, title_pattern: "Create GitHub repo {org}/{name}", priority: "medium", depends_on_positions: [7] },
14840
+ { position: 9, title_pattern: "Add @hasna/cloud adapter", priority: "medium", depends_on_positions: [1] },
14841
+ { position: 10, title_pattern: "Write PostgreSQL migrations for {name}", priority: "medium", depends_on_positions: [1] },
14842
+ { position: 11, title_pattern: "Add feedback system + send_feedback MCP tool", priority: "medium", depends_on_positions: [3] },
14843
+ { position: 12, title_pattern: "Add agent tools (register_agent, heartbeat, set_focus, list_agents)", priority: "medium", depends_on_positions: [3] },
14844
+ { position: 13, title_pattern: "Create RDS database for {name}", priority: "low", depends_on_positions: [10] },
14845
+ { position: 14, title_pattern: "Publish @hasna/{name} to npm", priority: "high", depends_on_positions: [6, 7, 8] },
14846
+ { position: 15, title_pattern: "Install @hasna/{name} globally and verify", priority: "medium", depends_on_positions: [14] }
14847
+ ]
14848
+ },
14849
+ {
14850
+ name: "bug-fix",
14851
+ description: "Standard bug fix workflow",
14852
+ variables: [{ name: "bug", required: true, description: "Bug description" }],
14853
+ tasks: [
14854
+ { position: 0, title_pattern: "Reproduce: {bug}", priority: "critical" },
14855
+ { position: 1, title_pattern: "Diagnose root cause of {bug}", priority: "critical", depends_on_positions: [0] },
14856
+ { position: 2, title_pattern: "Implement fix for {bug}", priority: "critical", depends_on_positions: [1] },
14857
+ { position: 3, title_pattern: "Write regression test for {bug}", priority: "high", depends_on_positions: [2] },
14858
+ { position: 4, title_pattern: "Publish fix and verify in production", priority: "high", depends_on_positions: [3] }
14859
+ ]
14860
+ },
14861
+ {
14862
+ name: "feature",
14863
+ description: "Standard feature development workflow",
14864
+ variables: [{ name: "feature", required: true }, { name: "scope", required: false, default: "medium" }],
14865
+ tasks: [
14866
+ { position: 0, title_pattern: "Write spec for {feature}", priority: "high" },
14867
+ { position: 1, title_pattern: "Design implementation approach for {feature}", priority: "high", depends_on_positions: [0] },
14868
+ { position: 2, title_pattern: "Implement {feature}", priority: "critical", depends_on_positions: [1] },
14869
+ { position: 3, title_pattern: "Write tests for {feature}", priority: "high", depends_on_positions: [2] },
14870
+ { position: 4, title_pattern: "Code review for {feature}", priority: "medium", depends_on_positions: [3] },
14871
+ { position: 5, title_pattern: "Update docs for {feature}", priority: "medium", depends_on_positions: [2] },
14872
+ { position: 6, title_pattern: "Deploy {feature}", priority: "high", depends_on_positions: [4] }
14873
+ ]
14874
+ },
14875
+ {
14876
+ name: "security-audit",
14877
+ description: "Security audit workflow",
14878
+ variables: [{ name: "target", required: true }],
14879
+ tasks: [
14880
+ { position: 0, title_pattern: "Scan {target} for vulnerabilities", priority: "critical" },
14881
+ { position: 1, title_pattern: "Review {target} security findings", priority: "critical", depends_on_positions: [0] },
14882
+ { position: 2, title_pattern: "Fix critical issues in {target}", priority: "critical", depends_on_positions: [1] },
14883
+ { position: 3, title_pattern: "Retest {target} after fixes", priority: "high", depends_on_positions: [2] },
14884
+ { position: 4, title_pattern: "Write security report for {target}", priority: "medium", depends_on_positions: [3] },
14885
+ { position: 5, title_pattern: "Close audit for {target}", priority: "low", depends_on_positions: [4] }
14886
+ ]
14887
+ }
14888
+ ];
14889
+ });
14890
+
14442
14891
  // src/lib/auto-assign.ts
14443
14892
  var exports_auto_assign = {};
14444
14893
  __export(exports_auto_assign, {
@@ -16146,6 +16595,26 @@ var init_pg_migrations = __esm(() => {
16146
16595
  CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
16147
16596
 
16148
16597
  INSERT INTO _migrations (id) VALUES (37) ON CONFLICT DO NOTHING;
16598
+ `,
16599
+ `
16600
+ ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS variables TEXT DEFAULT '[]';
16601
+ INSERT INTO _migrations (id) VALUES (38) ON CONFLICT DO NOTHING;
16602
+ `,
16603
+ `
16604
+ ALTER TABLE template_tasks ADD COLUMN IF NOT EXISTS condition TEXT;
16605
+ ALTER TABLE template_tasks ADD COLUMN IF NOT EXISTS include_template_id TEXT;
16606
+ ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1;
16607
+
16608
+ CREATE TABLE IF NOT EXISTS template_versions (
16609
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
16610
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
16611
+ version INTEGER NOT NULL,
16612
+ snapshot TEXT NOT NULL,
16613
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
16614
+ );
16615
+ CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id);
16616
+
16617
+ INSERT INTO _migrations (id) VALUES (39) ON CONFLICT DO NOTHING;
16149
16618
  `
16150
16619
  ];
16151
16620
  });
@@ -21034,6 +21503,11 @@ var STANDARD_EXCLUDED = new Set([
21034
21503
  "create_task_from_template",
21035
21504
  "delete_template",
21036
21505
  "update_template",
21506
+ "init_templates",
21507
+ "preview_template",
21508
+ "export_template",
21509
+ "import_template",
21510
+ "template_history",
21037
21511
  "approve_task"
21038
21512
  ]);
21039
21513
  function shouldRegisterTool(name) {
@@ -22766,15 +23240,20 @@ ${data.chart}` }] };
22766
23240
  });
22767
23241
  }
22768
23242
  if (shouldRegisterTool("create_webhook")) {
22769
- server.tool("create_webhook", "Register a webhook for task change events.", {
23243
+ server.tool("create_webhook", "Register a webhook for task change events. Optionally scope to a project, task list, agent, or specific task.", {
22770
23244
  url: exports_external.string(),
22771
- events: exports_external.array(exports_external.string()).optional(),
22772
- secret: exports_external.string().optional()
23245
+ events: exports_external.array(exports_external.string()).optional().describe("Event types to subscribe to (empty = all). E.g. task.created, task.completed, task.failed, task.started, task.assigned, task.status_changed"),
23246
+ secret: exports_external.string().optional().describe("HMAC secret for signing webhook payloads"),
23247
+ project_id: exports_external.string().optional().describe("Only fire for events in this project"),
23248
+ task_list_id: exports_external.string().optional().describe("Only fire for events in this task list"),
23249
+ agent_id: exports_external.string().optional().describe("Only fire for events involving this agent"),
23250
+ task_id: exports_external.string().optional().describe("Only fire for events on this specific task")
22773
23251
  }, async (params) => {
22774
23252
  try {
22775
23253
  const { createWebhook: createWebhook2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
22776
23254
  const wh = createWebhook2(params);
22777
- return { content: [{ type: "text", text: `Webhook created: ${wh.id.slice(0, 8)} | ${wh.url} | events: ${wh.events.length === 0 ? "all" : wh.events.join(",")}` }] };
23255
+ const scope = [wh.project_id && `project:${wh.project_id}`, wh.task_list_id && `list:${wh.task_list_id}`, wh.agent_id && `agent:${wh.agent_id}`, wh.task_id && `task:${wh.task_id}`].filter(Boolean).join(", ");
23256
+ return { content: [{ type: "text", text: `Webhook created: ${wh.id.slice(0, 8)} | ${wh.url} | events: ${wh.events.length === 0 ? "all" : wh.events.join(",")}${scope ? ` | scope: ${scope}` : ""}` }] };
22778
23257
  } catch (e) {
22779
23258
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
22780
23259
  }
@@ -22810,7 +23289,7 @@ if (shouldRegisterTool("delete_webhook")) {
22810
23289
  });
22811
23290
  }
22812
23291
  if (shouldRegisterTool("create_template")) {
22813
- server.tool("create_template", "Create a reusable task template. Optionally include a tasks array to define a multi-task template with dependencies and variable placeholders ({name} syntax).", {
23292
+ server.tool("create_template", "Create a reusable task template. Optionally include a tasks array to define a multi-task template with dependencies and variable placeholders ({name} syntax). Use variables to define typed variable definitions with defaults and required flags.", {
22814
23293
  name: exports_external.string(),
22815
23294
  title_pattern: exports_external.string(),
22816
23295
  description: exports_external.string().optional(),
@@ -22818,6 +23297,12 @@ if (shouldRegisterTool("create_template")) {
22818
23297
  tags: exports_external.array(exports_external.string()).optional(),
22819
23298
  project_id: exports_external.string().optional(),
22820
23299
  plan_id: exports_external.string().optional(),
23300
+ variables: exports_external.array(exports_external.object({
23301
+ name: exports_external.string().describe("Variable name (used as {name} in patterns)"),
23302
+ required: exports_external.boolean().describe("Whether this variable must be provided"),
23303
+ default: exports_external.string().optional().describe("Default value if not provided"),
23304
+ description: exports_external.string().optional().describe("Help text for the variable")
23305
+ })).optional().describe("Typed variable definitions with defaults and required flags"),
22821
23306
  tasks: exports_external.array(exports_external.object({
22822
23307
  title_pattern: exports_external.string().describe("Title pattern with optional {variable} placeholders"),
22823
23308
  description: exports_external.string().optional(),
@@ -22847,7 +23332,10 @@ if (shouldRegisterTool("list_templates")) {
22847
23332
  const templates = listTemplates2();
22848
23333
  if (templates.length === 0)
22849
23334
  return { content: [{ type: "text", text: "No templates." }] };
22850
- const text = templates.map((t) => `${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}" | ${t.priority}`).join(`
23335
+ const text = templates.map((t) => {
23336
+ const vars = t.variables.length > 0 ? ` | vars: ${t.variables.map((v) => `${v.name}${v.required ? "*" : ""}${v.default ? `=${v.default}` : ""}`).join(", ")}` : "";
23337
+ return `${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}" | ${t.priority}${vars}`;
23338
+ }).join(`
22851
23339
  `);
22852
23340
  return { content: [{ type: "text", text: `${templates.length} template(s):
22853
23341
  ${text}` }] };
@@ -22872,7 +23360,8 @@ if (shouldRegisterTool("create_task_from_template")) {
22872
23360
  const resolvedTemplateId = resolveId(params.template_id, "task_templates");
22873
23361
  const templateWithTasks = getTemplateWithTasks2(resolvedTemplateId);
22874
23362
  if (templateWithTasks && templateWithTasks.tasks.length > 0) {
22875
- const tasks = tasksFromTemplate2(resolvedTemplateId, params.project_id || templateWithTasks.project_id || "", params.variables, params.task_list_id);
23363
+ const effectiveProjectId = params.project_id || templateWithTasks.project_id || undefined;
23364
+ const tasks = tasksFromTemplate2(resolvedTemplateId, effectiveProjectId, params.variables, params.task_list_id);
22876
23365
  const text = tasks.map((t) => `${t.id.slice(0, 8)} | ${t.priority} | ${t.title}`).join(`
22877
23366
  `);
22878
23367
  return { content: [{ type: "text", text: `${tasks.length} task(s) created from template:
@@ -22929,6 +23418,93 @@ if (shouldRegisterTool("update_template")) {
22929
23418
  }
22930
23419
  });
22931
23420
  }
23421
+ if (shouldRegisterTool("init_templates")) {
23422
+ server.tool("init_templates", "Initialize built-in starter templates (open-source-project, bug-fix, feature, security-audit). Skips templates that already exist by name.", {}, async () => {
23423
+ try {
23424
+ const { initBuiltinTemplates: initBuiltinTemplates2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
23425
+ const result = initBuiltinTemplates2();
23426
+ if (result.created === 0) {
23427
+ return { content: [{ type: "text", text: `All ${result.skipped} built-in template(s) already exist.` }] };
23428
+ }
23429
+ return { content: [{ type: "text", text: `Created ${result.created} template(s): ${result.names.join(", ")}. Skipped ${result.skipped} existing.` }] };
23430
+ } catch (e) {
23431
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
23432
+ }
23433
+ });
23434
+ }
23435
+ if (shouldRegisterTool("preview_template")) {
23436
+ server.tool("preview_template", "Preview a template without creating tasks. Shows resolved titles (variables substituted), dependencies, and priorities.", {
23437
+ template_id: exports_external.string(),
23438
+ variables: exports_external.record(exports_external.string()).optional().describe("Variable substitution map for {name} placeholders")
23439
+ }, async (params) => {
23440
+ try {
23441
+ const { previewTemplate: previewTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
23442
+ const resolvedId = resolveId(params.template_id, "task_templates");
23443
+ const preview = previewTemplate2(resolvedId, params.variables);
23444
+ const lines = preview.tasks.map((t) => {
23445
+ const deps = t.depends_on_positions.length > 0 ? ` (after: ${t.depends_on_positions.join(", ")})` : "";
23446
+ return ` [${t.position}] ${t.priority} | ${t.title}${deps}`;
23447
+ });
23448
+ const varsInfo = preview.variables.length > 0 ? `
23449
+ Variables: ${preview.variables.map((v) => `${v.name}${v.required ? "*" : ""}${v.default ? `=${v.default}` : ""}`).join(", ")}` : "";
23450
+ const resolvedVars = Object.keys(preview.resolved_variables).length > 0 ? `
23451
+ Resolved: ${Object.entries(preview.resolved_variables).map(([k, v]) => `${k}=${v}`).join(", ")}` : "";
23452
+ return { content: [{ type: "text", text: `Preview: ${preview.template_name} (${preview.tasks.length} tasks)${varsInfo}${resolvedVars}
23453
+ ${lines.join(`
23454
+ `)}` }] };
23455
+ } catch (e) {
23456
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
23457
+ }
23458
+ });
23459
+ }
23460
+ if (shouldRegisterTool("export_template")) {
23461
+ server.tool("export_template", "Export a template as a full JSON object (template + tasks + variables). Useful for sharing or backup.", { template_id: exports_external.string() }, async ({ template_id }) => {
23462
+ try {
23463
+ const { exportTemplate: exportTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
23464
+ const resolvedId = resolveId(template_id, "task_templates");
23465
+ const json = exportTemplate2(resolvedId);
23466
+ return { content: [{ type: "text", text: JSON.stringify(json, null, 2) }] };
23467
+ } catch (e) {
23468
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
23469
+ }
23470
+ });
23471
+ }
23472
+ if (shouldRegisterTool("import_template")) {
23473
+ server.tool("import_template", "Import a template from a JSON string (as returned by export_template). Creates new template with new IDs.", { json: exports_external.string().describe("JSON string of the template export") }, async ({ json }) => {
23474
+ try {
23475
+ const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
23476
+ const parsed = JSON.parse(json);
23477
+ const t = importTemplate2(parsed);
23478
+ return { content: [{ type: "text", text: `Template imported: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}"` }] };
23479
+ } catch (e) {
23480
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
23481
+ }
23482
+ });
23483
+ }
23484
+ if (shouldRegisterTool("template_history")) {
23485
+ server.tool("template_history", "Show version history of a template. Each update creates a snapshot of the previous state.", { template_id: exports_external.string() }, async ({ template_id }) => {
23486
+ try {
23487
+ const { listTemplateVersions: listTemplateVersions2, getTemplate: getTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
23488
+ const resolvedId = resolveId(template_id, "task_templates");
23489
+ const template = getTemplate2(resolvedId);
23490
+ if (!template)
23491
+ return { content: [{ type: "text", text: `Template not found: ${template_id}` }], isError: true };
23492
+ const versions = listTemplateVersions2(resolvedId);
23493
+ if (versions.length === 0) {
23494
+ return { content: [{ type: "text", text: `${template.name} v${template.version} \u2014 no previous versions.` }] };
23495
+ }
23496
+ const lines = versions.map((v) => {
23497
+ const snap = JSON.parse(v.snapshot);
23498
+ return `v${v.version} | ${v.created_at} | ${snap.name} | "${snap.title_pattern}"`;
23499
+ });
23500
+ return { content: [{ type: "text", text: `${template.name} \u2014 current: v${template.version}
23501
+ ${lines.join(`
23502
+ `)}` }] };
23503
+ } catch (e) {
23504
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
23505
+ }
23506
+ });
23507
+ }
22932
23508
  if (shouldRegisterTool("approve_task")) {
22933
23509
  server.tool("approve_task", "Approve a task with requires_approval=true.", {
22934
23510
  id: exports_external.string(),
@@ -24046,6 +24622,11 @@ if (shouldRegisterTool("search_tools")) {
24046
24622
  "create_task_from_template",
24047
24623
  "delete_template",
24048
24624
  "update_template",
24625
+ "init_templates",
24626
+ "preview_template",
24627
+ "export_template",
24628
+ "import_template",
24629
+ "template_history",
24049
24630
  "bulk_update_tasks",
24050
24631
  "bulk_create_tasks",
24051
24632
  "get_task_stats",
@@ -24285,8 +24866,21 @@ if (shouldRegisterTool("describe_tools")) {
24285
24866
  Params: id(string, req)
24286
24867
  Example: {id: 'a1b2c3d4'}`,
24287
24868
  update_template: `Update a task template's name, title pattern, or other fields.
24288
- Params: id(string, req), name(string), title_pattern(string), description(string), priority(low|medium|high|critical), tags(string[]), project_id(string), plan_id(string)
24869
+ Params: id(string, req), name(string), title_pattern(string), description(string), priority(low|medium|high|critical), tags(string[]), variables(TemplateVariable[]), project_id(string), plan_id(string)
24289
24870
  Example: {id: 'a1b2c3d4', name: 'Renamed Template', priority: 'critical'}`,
24871
+ init_templates: "Initialize built-in starter templates (open-source-project, bug-fix, feature, security-audit). Skips already existing. No params.",
24872
+ preview_template: `Preview a template without creating tasks. Shows resolved titles, deps, priorities.
24873
+ Params: template_id(string, req), variables(Record<string,string>)
24874
+ Example: {template_id: 'a1b2c3d4', variables: {name: 'invoices'}}`,
24875
+ export_template: `Export a template as JSON (template + tasks + variables). Use for sharing or backup.
24876
+ Params: template_id(string, req)
24877
+ Example: {template_id: 'a1b2c3d4'}`,
24878
+ import_template: `Import a template from a JSON string (as returned by export_template). Creates new template with new IDs.
24879
+ Params: json(string, req)
24880
+ Example: {json: '{"name":"My Template",...}'}`,
24881
+ template_history: `Show version history of a template. Each update creates a snapshot of the previous state.
24882
+ Params: template_id(string, req)
24883
+ Example: {template_id: 'a1b2c3d4'}`,
24290
24884
  get_active_work: `See all in-progress tasks and who is working on them.
24291
24885
  Params: project_id(string, optional), task_list_id(string, optional)
24292
24886
  Example: {project_id: 'a1b2c3d4'}`,