@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.
@@ -378,6 +378,36 @@ function ensureSchema(db) {
378
378
  ensureColumn("projects", "org_id", "TEXT");
379
379
  ensureColumn("plans", "task_list_id", "TEXT");
380
380
  ensureColumn("plans", "agent_id", "TEXT");
381
+ ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
382
+ ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
383
+ ensureColumn("template_tasks", "condition", "TEXT");
384
+ ensureColumn("template_tasks", "include_template_id", "TEXT");
385
+ ensureTable("template_versions", `
386
+ CREATE TABLE template_versions (
387
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
388
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
389
+ version INTEGER NOT NULL,
390
+ snapshot TEXT NOT NULL,
391
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
392
+ )`);
393
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
394
+ ensureColumn("webhooks", "project_id", "TEXT");
395
+ ensureColumn("webhooks", "task_list_id", "TEXT");
396
+ ensureColumn("webhooks", "agent_id", "TEXT");
397
+ ensureColumn("webhooks", "task_id", "TEXT");
398
+ ensureTable("webhook_deliveries", `
399
+ CREATE TABLE webhook_deliveries (
400
+ id TEXT PRIMARY KEY,
401
+ webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
402
+ event TEXT NOT NULL,
403
+ payload TEXT NOT NULL,
404
+ status_code INTEGER,
405
+ response TEXT,
406
+ attempt INTEGER NOT NULL DEFAULT 1,
407
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
408
+ )`);
409
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
410
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
381
411
  ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
382
412
  ensureColumn("task_comments", "progress_pct", "INTEGER");
383
413
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
@@ -1057,6 +1087,26 @@ var init_database = __esm(() => {
1057
1087
  CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
1058
1088
 
1059
1089
  INSERT OR IGNORE INTO _migrations (id) VALUES (37);
1090
+ `,
1091
+ `
1092
+ ALTER TABLE task_templates ADD COLUMN variables TEXT DEFAULT '[]';
1093
+ INSERT OR IGNORE INTO _migrations (id) VALUES (38);
1094
+ `,
1095
+ `
1096
+ ALTER TABLE template_tasks ADD COLUMN condition TEXT;
1097
+ ALTER TABLE template_tasks ADD COLUMN include_template_id TEXT;
1098
+ ALTER TABLE task_templates ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
1099
+
1100
+ CREATE TABLE IF NOT EXISTS template_versions (
1101
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
1102
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
1103
+ version INTEGER NOT NULL,
1104
+ snapshot TEXT NOT NULL,
1105
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1106
+ );
1107
+ CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id);
1108
+
1109
+ INSERT OR IGNORE INTO _migrations (id) VALUES (39);
1060
1110
  `
1061
1111
  ];
1062
1112
  });
@@ -1482,18 +1532,37 @@ var init_recurrence = __esm(() => {
1482
1532
  var exports_webhooks = {};
1483
1533
  __export(exports_webhooks, {
1484
1534
  listWebhooks: () => listWebhooks,
1535
+ listDeliveries: () => listDeliveries,
1485
1536
  getWebhook: () => getWebhook,
1486
1537
  dispatchWebhook: () => dispatchWebhook,
1487
1538
  deleteWebhook: () => deleteWebhook,
1488
1539
  createWebhook: () => createWebhook
1489
1540
  });
1490
1541
  function rowToWebhook(row) {
1491
- return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
1542
+ return {
1543
+ ...row,
1544
+ events: JSON.parse(row.events || "[]"),
1545
+ active: !!row.active,
1546
+ project_id: row.project_id || null,
1547
+ task_list_id: row.task_list_id || null,
1548
+ agent_id: row.agent_id || null,
1549
+ task_id: row.task_id || null
1550
+ };
1492
1551
  }
1493
1552
  function createWebhook(input, db) {
1494
1553
  const d = db || getDatabase();
1495
1554
  const id = uuid();
1496
- d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
1555
+ d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1556
+ id,
1557
+ input.url,
1558
+ JSON.stringify(input.events || []),
1559
+ input.secret || null,
1560
+ input.project_id || null,
1561
+ input.task_list_id || null,
1562
+ input.agent_id || null,
1563
+ input.task_id || null,
1564
+ now()
1565
+ ]);
1497
1566
  return getWebhook(id, d);
1498
1567
  }
1499
1568
  function getWebhook(id, db) {
@@ -1509,22 +1578,69 @@ function deleteWebhook(id, db) {
1509
1578
  const d = db || getDatabase();
1510
1579
  return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
1511
1580
  }
1581
+ function listDeliveries(webhookId, limit = 50, db) {
1582
+ const d = db || getDatabase();
1583
+ if (webhookId) {
1584
+ return d.query("SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?").all(webhookId, limit);
1585
+ }
1586
+ return d.query("SELECT * FROM webhook_deliveries ORDER BY created_at DESC LIMIT ?").all(limit);
1587
+ }
1588
+ function logDelivery(d, webhookId, event, payload, statusCode, response, attempt) {
1589
+ const id = uuid();
1590
+ 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()]);
1591
+ }
1592
+ function matchesScope(wh, payload) {
1593
+ if (wh.project_id && payload.project_id !== wh.project_id)
1594
+ return false;
1595
+ if (wh.task_list_id && payload.task_list_id !== wh.task_list_id)
1596
+ return false;
1597
+ if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
1598
+ return false;
1599
+ if (wh.task_id && payload.id !== wh.task_id)
1600
+ return false;
1601
+ return true;
1602
+ }
1603
+ async function deliverWebhook(wh, event, body, attempt, db) {
1604
+ try {
1605
+ const headers = { "Content-Type": "application/json" };
1606
+ if (wh.secret) {
1607
+ const encoder = new TextEncoder;
1608
+ const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1609
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
1610
+ headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
1611
+ }
1612
+ const resp = await fetch(wh.url, { method: "POST", headers, body });
1613
+ const respText = await resp.text().catch(() => "");
1614
+ logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
1615
+ if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
1616
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
1617
+ setTimeout(() => {
1618
+ deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
1619
+ }, delay);
1620
+ }
1621
+ } catch (err) {
1622
+ const errorMsg = err instanceof Error ? err.message : String(err);
1623
+ logDelivery(db, wh.id, event, body, null, errorMsg.slice(0, 1000), attempt);
1624
+ if (attempt < MAX_RETRY_ATTEMPTS) {
1625
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
1626
+ setTimeout(() => {
1627
+ deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
1628
+ }, delay);
1629
+ }
1630
+ }
1631
+ }
1512
1632
  async function dispatchWebhook(event, payload, db) {
1513
- const webhooks = listWebhooks(db).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
1633
+ const d = db || getDatabase();
1634
+ const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
1635
+ const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
1514
1636
  for (const wh of webhooks) {
1515
- try {
1516
- const body = JSON.stringify({ event, payload, timestamp: now() });
1517
- const headers = { "Content-Type": "application/json" };
1518
- if (wh.secret) {
1519
- const encoder = new TextEncoder;
1520
- const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1521
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
1522
- headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
1523
- }
1524
- fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
1525
- } catch {}
1637
+ if (!matchesScope(wh, payloadObj))
1638
+ continue;
1639
+ const body = JSON.stringify({ event, payload, timestamp: now() });
1640
+ deliverWebhook(wh, event, body, 1, d).catch(() => {});
1526
1641
  }
1527
1642
  }
1643
+ var MAX_RETRY_ATTEMPTS = 3, RETRY_BASE_DELAY_MS = 1000;
1528
1644
  var init_webhooks = __esm(() => {
1529
1645
  init_database();
1530
1646
  });
@@ -1535,10 +1651,17 @@ __export(exports_templates, {
1535
1651
  updateTemplate: () => updateTemplate,
1536
1652
  tasksFromTemplate: () => tasksFromTemplate,
1537
1653
  taskFromTemplate: () => taskFromTemplate,
1654
+ resolveVariables: () => resolveVariables,
1655
+ previewTemplate: () => previewTemplate,
1538
1656
  listTemplates: () => listTemplates,
1657
+ listTemplateVersions: () => listTemplateVersions,
1658
+ importTemplate: () => importTemplate,
1539
1659
  getTemplateWithTasks: () => getTemplateWithTasks,
1660
+ getTemplateVersion: () => getTemplateVersion,
1540
1661
  getTemplateTasks: () => getTemplateTasks,
1541
1662
  getTemplate: () => getTemplate,
1663
+ exportTemplate: () => exportTemplate,
1664
+ evaluateCondition: () => evaluateCondition,
1542
1665
  deleteTemplate: () => deleteTemplate,
1543
1666
  createTemplate: () => createTemplate,
1544
1667
  addTemplateTasks: () => addTemplateTasks
@@ -1547,8 +1670,10 @@ function rowToTemplate(row) {
1547
1670
  return {
1548
1671
  ...row,
1549
1672
  tags: JSON.parse(row.tags || "[]"),
1673
+ variables: JSON.parse(row.variables || "[]"),
1550
1674
  metadata: JSON.parse(row.metadata || "{}"),
1551
- priority: row.priority || "medium"
1675
+ priority: row.priority || "medium",
1676
+ version: row.version ?? 1
1552
1677
  };
1553
1678
  }
1554
1679
  function rowToTemplateTask(row) {
@@ -1557,7 +1682,9 @@ function rowToTemplateTask(row) {
1557
1682
  tags: JSON.parse(row.tags || "[]"),
1558
1683
  depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
1559
1684
  metadata: JSON.parse(row.metadata || "{}"),
1560
- priority: row.priority || "medium"
1685
+ priority: row.priority || "medium",
1686
+ condition: row.condition ?? null,
1687
+ include_template_id: row.include_template_id ?? null
1561
1688
  };
1562
1689
  }
1563
1690
  function resolveTemplateId(id, d) {
@@ -1566,14 +1693,15 @@ function resolveTemplateId(id, d) {
1566
1693
  function createTemplate(input, db) {
1567
1694
  const d = db || getDatabase();
1568
1695
  const id = uuid();
1569
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
1570
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1696
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
1697
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1571
1698
  id,
1572
1699
  input.name,
1573
1700
  input.title_pattern,
1574
1701
  input.description || null,
1575
1702
  input.priority || "medium",
1576
1703
  JSON.stringify(input.tags || []),
1704
+ JSON.stringify(input.variables || []),
1577
1705
  input.project_id || null,
1578
1706
  input.plan_id || null,
1579
1707
  JSON.stringify(input.metadata || {}),
@@ -1608,7 +1736,23 @@ function updateTemplate(id, updates, db) {
1608
1736
  const resolved = resolveTemplateId(id, d);
1609
1737
  if (!resolved)
1610
1738
  return null;
1611
- const sets = [];
1739
+ const current = getTemplateWithTasks(resolved, d);
1740
+ if (current) {
1741
+ const snapshot = JSON.stringify({
1742
+ name: current.name,
1743
+ title_pattern: current.title_pattern,
1744
+ description: current.description,
1745
+ priority: current.priority,
1746
+ tags: current.tags,
1747
+ variables: current.variables,
1748
+ project_id: current.project_id,
1749
+ plan_id: current.plan_id,
1750
+ metadata: current.metadata,
1751
+ tasks: current.tasks
1752
+ });
1753
+ d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
1754
+ }
1755
+ const sets = ["version = version + 1"];
1612
1756
  const values = [];
1613
1757
  if (updates.name !== undefined) {
1614
1758
  sets.push("name = ?");
@@ -1630,6 +1774,10 @@ function updateTemplate(id, updates, db) {
1630
1774
  sets.push("tags = ?");
1631
1775
  values.push(JSON.stringify(updates.tags));
1632
1776
  }
1777
+ if (updates.variables !== undefined) {
1778
+ sets.push("variables = ?");
1779
+ values.push(JSON.stringify(updates.variables));
1780
+ }
1633
1781
  if (updates.project_id !== undefined) {
1634
1782
  sets.push("project_id = ?");
1635
1783
  values.push(updates.project_id);
@@ -1642,8 +1790,6 @@ function updateTemplate(id, updates, db) {
1642
1790
  sets.push("metadata = ?");
1643
1791
  values.push(JSON.stringify(updates.metadata));
1644
1792
  }
1645
- if (sets.length === 0)
1646
- return getTemplate(resolved, d);
1647
1793
  values.push(resolved);
1648
1794
  d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
1649
1795
  return getTemplate(resolved, d);
@@ -1673,8 +1819,8 @@ function addTemplateTasks(templateId, tasks, db) {
1673
1819
  for (let i = 0;i < tasks.length; i++) {
1674
1820
  const task = tasks[i];
1675
1821
  const id = uuid();
1676
- d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, depends_on_positions, metadata, created_at)
1677
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1822
+ 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)
1823
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1678
1824
  id,
1679
1825
  templateId,
1680
1826
  i,
@@ -1683,6 +1829,8 @@ function addTemplateTasks(templateId, tasks, db) {
1683
1829
  task.priority || "medium",
1684
1830
  JSON.stringify(task.tags || []),
1685
1831
  task.task_type || null,
1832
+ task.condition || null,
1833
+ task.include_template_id || null,
1686
1834
  JSON.stringify(task.depends_on || []),
1687
1835
  JSON.stringify(task.metadata || {}),
1688
1836
  now()
@@ -1710,11 +1858,142 @@ function getTemplateTasks(templateId, db) {
1710
1858
  const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
1711
1859
  return rows.map(rowToTemplateTask);
1712
1860
  }
1713
- function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
1861
+ function evaluateCondition(condition, variables) {
1862
+ if (!condition || condition.trim() === "")
1863
+ return true;
1864
+ const trimmed = condition.trim();
1865
+ const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
1866
+ if (eqMatch) {
1867
+ const varName = eqMatch[1];
1868
+ const expected = eqMatch[2].trim();
1869
+ return (variables[varName] ?? "") === expected;
1870
+ }
1871
+ const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
1872
+ if (neqMatch) {
1873
+ const varName = neqMatch[1];
1874
+ const expected = neqMatch[2].trim();
1875
+ return (variables[varName] ?? "") !== expected;
1876
+ }
1877
+ const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
1878
+ if (falsyMatch) {
1879
+ const varName = falsyMatch[1];
1880
+ const val = variables[varName];
1881
+ return !val || val === "" || val === "false";
1882
+ }
1883
+ const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
1884
+ if (truthyMatch) {
1885
+ const varName = truthyMatch[1];
1886
+ const val = variables[varName];
1887
+ return !!val && val !== "" && val !== "false";
1888
+ }
1889
+ return true;
1890
+ }
1891
+ function exportTemplate(id, db) {
1892
+ const d = db || getDatabase();
1893
+ const template = getTemplateWithTasks(id, d);
1894
+ if (!template)
1895
+ throw new Error(`Template not found: ${id}`);
1896
+ return {
1897
+ name: template.name,
1898
+ title_pattern: template.title_pattern,
1899
+ description: template.description,
1900
+ priority: template.priority,
1901
+ tags: template.tags,
1902
+ variables: template.variables,
1903
+ project_id: template.project_id,
1904
+ plan_id: template.plan_id,
1905
+ metadata: template.metadata,
1906
+ tasks: template.tasks.map((t) => ({
1907
+ position: t.position,
1908
+ title_pattern: t.title_pattern,
1909
+ description: t.description,
1910
+ priority: t.priority,
1911
+ tags: t.tags,
1912
+ task_type: t.task_type,
1913
+ condition: t.condition,
1914
+ include_template_id: t.include_template_id,
1915
+ depends_on_positions: t.depends_on_positions,
1916
+ metadata: t.metadata
1917
+ }))
1918
+ };
1919
+ }
1920
+ function importTemplate(json, db) {
1921
+ const d = db || getDatabase();
1922
+ const taskInputs = (json.tasks || []).map((t) => ({
1923
+ title_pattern: t.title_pattern,
1924
+ description: t.description ?? undefined,
1925
+ priority: t.priority,
1926
+ tags: t.tags,
1927
+ task_type: t.task_type ?? undefined,
1928
+ condition: t.condition ?? undefined,
1929
+ include_template_id: t.include_template_id ?? undefined,
1930
+ depends_on: t.depends_on_positions,
1931
+ metadata: t.metadata
1932
+ }));
1933
+ return createTemplate({
1934
+ name: json.name,
1935
+ title_pattern: json.title_pattern,
1936
+ description: json.description ?? undefined,
1937
+ priority: json.priority,
1938
+ tags: json.tags,
1939
+ variables: json.variables,
1940
+ project_id: json.project_id ?? undefined,
1941
+ plan_id: json.plan_id ?? undefined,
1942
+ metadata: json.metadata,
1943
+ tasks: taskInputs
1944
+ }, d);
1945
+ }
1946
+ function getTemplateVersion(id, version, db) {
1947
+ const d = db || getDatabase();
1948
+ const resolved = resolveTemplateId(id, d);
1949
+ if (!resolved)
1950
+ return null;
1951
+ const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
1952
+ return row || null;
1953
+ }
1954
+ function listTemplateVersions(id, db) {
1955
+ const d = db || getDatabase();
1956
+ const resolved = resolveTemplateId(id, d);
1957
+ if (!resolved)
1958
+ return [];
1959
+ return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
1960
+ }
1961
+ function resolveVariables(templateVars, provided) {
1962
+ const merged = { ...provided };
1963
+ for (const v of templateVars) {
1964
+ if (merged[v.name] === undefined && v.default !== undefined) {
1965
+ merged[v.name] = v.default;
1966
+ }
1967
+ }
1968
+ const missing = [];
1969
+ for (const v of templateVars) {
1970
+ if (v.required && merged[v.name] === undefined) {
1971
+ missing.push(v.name);
1972
+ }
1973
+ }
1974
+ if (missing.length > 0) {
1975
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
1976
+ }
1977
+ return merged;
1978
+ }
1979
+ function substituteVars(text, variables) {
1980
+ let result = text;
1981
+ for (const [key, val] of Object.entries(variables)) {
1982
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
1983
+ }
1984
+ return result;
1985
+ }
1986
+ function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
1714
1987
  const d = db || getDatabase();
1715
1988
  const template = getTemplateWithTasks(templateId, d);
1716
1989
  if (!template)
1717
1990
  throw new Error(`Template not found: ${templateId}`);
1991
+ const visited = _visitedTemplateIds || new Set;
1992
+ if (visited.has(template.id)) {
1993
+ throw new Error(`Circular template reference detected: ${template.id}`);
1994
+ }
1995
+ visited.add(template.id);
1996
+ const resolved = resolveVariables(template.variables, variables);
1718
1997
  if (template.tasks.length === 0) {
1719
1998
  const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
1720
1999
  const task = createTask(input, d);
@@ -1722,16 +2001,27 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
1722
2001
  }
1723
2002
  const createdTasks = [];
1724
2003
  const positionToId = new Map;
2004
+ const skippedPositions = new Set;
1725
2005
  for (const tt of template.tasks) {
1726
- let title = tt.title_pattern;
1727
- let desc = tt.description;
1728
- if (variables) {
1729
- for (const [key, val] of Object.entries(variables)) {
1730
- title = title.replace(new RegExp(`\\{${key}\\}`, "g"), val);
1731
- if (desc)
1732
- desc = desc.replace(new RegExp(`\\{${key}\\}`, "g"), val);
2006
+ if (tt.include_template_id) {
2007
+ const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
2008
+ createdTasks.push(...includedTasks);
2009
+ if (includedTasks.length > 0) {
2010
+ positionToId.set(tt.position, includedTasks[0].id);
2011
+ } else {
2012
+ skippedPositions.add(tt.position);
1733
2013
  }
2014
+ continue;
1734
2015
  }
2016
+ if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
2017
+ skippedPositions.add(tt.position);
2018
+ continue;
2019
+ }
2020
+ let title = tt.title_pattern;
2021
+ let desc = tt.description;
2022
+ title = substituteVars(title, resolved);
2023
+ if (desc)
2024
+ desc = substituteVars(desc, resolved);
1735
2025
  const task = createTask({
1736
2026
  title,
1737
2027
  description: desc ?? undefined,
@@ -1746,8 +2036,14 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
1746
2036
  positionToId.set(tt.position, task.id);
1747
2037
  }
1748
2038
  for (const tt of template.tasks) {
2039
+ if (skippedPositions.has(tt.position))
2040
+ continue;
2041
+ if (tt.include_template_id)
2042
+ continue;
1749
2043
  const deps = tt.depends_on_positions;
1750
2044
  for (const depPos of deps) {
2045
+ if (skippedPositions.has(depPos))
2046
+ continue;
1751
2047
  const taskId = positionToId.get(tt.position);
1752
2048
  const depId = positionToId.get(depPos);
1753
2049
  if (taskId && depId) {
@@ -1757,6 +2053,47 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
1757
2053
  }
1758
2054
  return createdTasks;
1759
2055
  }
2056
+ function previewTemplate(templateId, variables, db) {
2057
+ const d = db || getDatabase();
2058
+ const template = getTemplateWithTasks(templateId, d);
2059
+ if (!template)
2060
+ throw new Error(`Template not found: ${templateId}`);
2061
+ const resolved = resolveVariables(template.variables, variables);
2062
+ const tasks = [];
2063
+ if (template.tasks.length === 0) {
2064
+ tasks.push({
2065
+ position: 0,
2066
+ title: substituteVars(template.title_pattern, resolved),
2067
+ description: template.description ? substituteVars(template.description, resolved) : null,
2068
+ priority: template.priority,
2069
+ tags: template.tags,
2070
+ task_type: null,
2071
+ depends_on_positions: []
2072
+ });
2073
+ } else {
2074
+ for (const tt of template.tasks) {
2075
+ if (tt.condition && !evaluateCondition(tt.condition, resolved))
2076
+ continue;
2077
+ tasks.push({
2078
+ position: tt.position,
2079
+ title: substituteVars(tt.title_pattern, resolved),
2080
+ description: tt.description ? substituteVars(tt.description, resolved) : null,
2081
+ priority: tt.priority,
2082
+ tags: tt.tags,
2083
+ task_type: tt.task_type,
2084
+ depends_on_positions: tt.depends_on_positions
2085
+ });
2086
+ }
2087
+ }
2088
+ return {
2089
+ template_id: template.id,
2090
+ template_name: template.name,
2091
+ description: template.description,
2092
+ variables: template.variables,
2093
+ resolved_variables: resolved,
2094
+ tasks
2095
+ };
2096
+ }
1760
2097
  var init_templates = __esm(() => {
1761
2098
  init_database();
1762
2099
  init_tasks();
@@ -3538,6 +3875,8 @@ async function startServer(port, options) {
3538
3875
  continue;
3539
3876
  if (client.agentId && event.agent_id !== client.agentId)
3540
3877
  continue;
3878
+ if (client.projectId && event.project_id !== client.projectId)
3879
+ continue;
3541
3880
  try {
3542
3881
  client.controller.enqueue(`event: ${eventName}
3543
3882
  data: ${data}
@@ -3576,6 +3915,31 @@ Dashboard not found at: ${dashboardDir}`);
3576
3915
  });
3577
3916
  }
3578
3917
  if (path === "/api/events" && method === "GET") {
3918
+ const agentId = url.searchParams.get("agent_id") || undefined;
3919
+ const projectId = url.searchParams.get("project_id") || undefined;
3920
+ if (agentId || projectId) {
3921
+ const client = { controller: null, agentId, projectId, events: undefined };
3922
+ const stream2 = new ReadableStream({
3923
+ start(controller) {
3924
+ client.controller = controller;
3925
+ filteredSseClients.add(client);
3926
+ controller.enqueue(`data: ${JSON.stringify({ type: "connected", agent_id: agentId, project_id: projectId, timestamp: new Date().toISOString() })}
3927
+
3928
+ `);
3929
+ },
3930
+ cancel() {
3931
+ filteredSseClients.delete(client);
3932
+ }
3933
+ });
3934
+ return new Response(stream2, {
3935
+ headers: {
3936
+ "Content-Type": "text/event-stream",
3937
+ "Cache-Control": "no-cache",
3938
+ Connection: "keep-alive",
3939
+ "Access-Control-Allow-Origin": "*"
3940
+ }
3941
+ });
3942
+ }
3579
3943
  const stream = new ReadableStream({
3580
3944
  start(controller) {
3581
3945
  sseClients.add(controller);
@@ -3684,7 +4048,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
3684
4048
  priority: body.priority,
3685
4049
  project_id: body.project_id
3686
4050
  });
3687
- broadcastEvent({ type: "task", task_id: task.id, action: "created", agent_id: task.agent_id });
4051
+ broadcastEvent({ type: "task", task_id: task.id, action: "created", agent_id: task.agent_id, project_id: task.project_id });
3688
4052
  return json(taskToSummary(task), 201, port);
3689
4053
  } catch (e) {
3690
4054
  return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
@@ -3905,7 +4269,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
3905
4269
  const id = startMatch[1];
3906
4270
  try {
3907
4271
  const task = startTask(id, "dashboard");
3908
- broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard" });
4272
+ broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard", project_id: task.project_id });
3909
4273
  return json(taskToSummary(task), 200, port);
3910
4274
  } catch (e) {
3911
4275
  return json({ error: e instanceof Error ? e.message : "Failed to start task" }, 500, port);
@@ -3918,7 +4282,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
3918
4282
  const body = await req.json().catch(() => ({}));
3919
4283
  const { failTask: failTask2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
3920
4284
  const result = failTask2(id, body.agent_id, body.reason, { retry: body.retry, error_code: body.error_code });
3921
- broadcastEvent({ type: "task", task_id: id, action: "failed", agent_id: body.agent_id || null });
4285
+ broadcastEvent({ type: "task", task_id: id, action: "failed", agent_id: body.agent_id || null, project_id: result.task.project_id });
3922
4286
  return json({ task: taskToSummary(result.task), retry_task: result.retryTask ? taskToSummary(result.retryTask) : null }, 200, port);
3923
4287
  } catch (e) {
3924
4288
  return json({ error: e instanceof Error ? e.message : "Failed to fail task" }, 500, port);
@@ -3929,7 +4293,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
3929
4293
  const id = completeMatch[1];
3930
4294
  try {
3931
4295
  const task = completeTask(id, "dashboard");
3932
- broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard" });
4296
+ broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard", project_id: task.project_id });
3933
4297
  return json(taskToSummary(task), 200, port);
3934
4298
  } catch (e) {
3935
4299
  return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500, port);
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiHH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA01B1G"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiHH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAk3B1G"}