@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/cli/index.js +775 -44
- package/dist/db/builtin-templates.d.ts +22 -0
- package/dist/db/builtin-templates.d.ts.map +1 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/templates.d.ts +71 -5
- package/dist/db/templates.d.ts.map +1 -1
- package/dist/db/webhooks.d.ts +11 -0
- package/dist/db/webhooks.d.ts.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +492 -31
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +633 -39
- package/dist/server/index.js +399 -35
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +28 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -846,6 +846,26 @@ var MIGRATIONS = [
|
|
|
846
846
|
CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
|
|
847
847
|
|
|
848
848
|
INSERT OR IGNORE INTO _migrations (id) VALUES (37);
|
|
849
|
+
`,
|
|
850
|
+
`
|
|
851
|
+
ALTER TABLE task_templates ADD COLUMN variables TEXT DEFAULT '[]';
|
|
852
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (38);
|
|
853
|
+
`,
|
|
854
|
+
`
|
|
855
|
+
ALTER TABLE template_tasks ADD COLUMN condition TEXT;
|
|
856
|
+
ALTER TABLE template_tasks ADD COLUMN include_template_id TEXT;
|
|
857
|
+
ALTER TABLE task_templates ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
|
|
858
|
+
|
|
859
|
+
CREATE TABLE IF NOT EXISTS template_versions (
|
|
860
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
861
|
+
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
862
|
+
version INTEGER NOT NULL,
|
|
863
|
+
snapshot TEXT NOT NULL,
|
|
864
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
865
|
+
);
|
|
866
|
+
CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id);
|
|
867
|
+
|
|
868
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (39);
|
|
849
869
|
`
|
|
850
870
|
];
|
|
851
871
|
var _db = null;
|
|
@@ -1060,6 +1080,36 @@ function ensureSchema(db) {
|
|
|
1060
1080
|
ensureColumn("projects", "org_id", "TEXT");
|
|
1061
1081
|
ensureColumn("plans", "task_list_id", "TEXT");
|
|
1062
1082
|
ensureColumn("plans", "agent_id", "TEXT");
|
|
1083
|
+
ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
|
|
1084
|
+
ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
|
|
1085
|
+
ensureColumn("template_tasks", "condition", "TEXT");
|
|
1086
|
+
ensureColumn("template_tasks", "include_template_id", "TEXT");
|
|
1087
|
+
ensureTable("template_versions", `
|
|
1088
|
+
CREATE TABLE template_versions (
|
|
1089
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
1090
|
+
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
1091
|
+
version INTEGER NOT NULL,
|
|
1092
|
+
snapshot TEXT NOT NULL,
|
|
1093
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1094
|
+
)`);
|
|
1095
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
|
|
1096
|
+
ensureColumn("webhooks", "project_id", "TEXT");
|
|
1097
|
+
ensureColumn("webhooks", "task_list_id", "TEXT");
|
|
1098
|
+
ensureColumn("webhooks", "agent_id", "TEXT");
|
|
1099
|
+
ensureColumn("webhooks", "task_id", "TEXT");
|
|
1100
|
+
ensureTable("webhook_deliveries", `
|
|
1101
|
+
CREATE TABLE webhook_deliveries (
|
|
1102
|
+
id TEXT PRIMARY KEY,
|
|
1103
|
+
webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
|
|
1104
|
+
event TEXT NOT NULL,
|
|
1105
|
+
payload TEXT NOT NULL,
|
|
1106
|
+
status_code INTEGER,
|
|
1107
|
+
response TEXT,
|
|
1108
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
1109
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1110
|
+
)`);
|
|
1111
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
|
|
1112
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
|
|
1063
1113
|
ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
|
|
1064
1114
|
ensureColumn("task_comments", "progress_pct", "INTEGER");
|
|
1065
1115
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
|
|
@@ -1750,13 +1800,33 @@ function nextOccurrence(rule, from) {
|
|
|
1750
1800
|
}
|
|
1751
1801
|
|
|
1752
1802
|
// src/db/webhooks.ts
|
|
1803
|
+
var MAX_RETRY_ATTEMPTS = 3;
|
|
1804
|
+
var RETRY_BASE_DELAY_MS = 1000;
|
|
1753
1805
|
function rowToWebhook(row) {
|
|
1754
|
-
return {
|
|
1806
|
+
return {
|
|
1807
|
+
...row,
|
|
1808
|
+
events: JSON.parse(row.events || "[]"),
|
|
1809
|
+
active: !!row.active,
|
|
1810
|
+
project_id: row.project_id || null,
|
|
1811
|
+
task_list_id: row.task_list_id || null,
|
|
1812
|
+
agent_id: row.agent_id || null,
|
|
1813
|
+
task_id: row.task_id || null
|
|
1814
|
+
};
|
|
1755
1815
|
}
|
|
1756
1816
|
function createWebhook(input, db) {
|
|
1757
1817
|
const d = db || getDatabase();
|
|
1758
1818
|
const id = uuid();
|
|
1759
|
-
d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?,
|
|
1819
|
+
d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1820
|
+
id,
|
|
1821
|
+
input.url,
|
|
1822
|
+
JSON.stringify(input.events || []),
|
|
1823
|
+
input.secret || null,
|
|
1824
|
+
input.project_id || null,
|
|
1825
|
+
input.task_list_id || null,
|
|
1826
|
+
input.agent_id || null,
|
|
1827
|
+
input.task_id || null,
|
|
1828
|
+
now()
|
|
1829
|
+
]);
|
|
1760
1830
|
return getWebhook(id, d);
|
|
1761
1831
|
}
|
|
1762
1832
|
function getWebhook(id, db) {
|
|
@@ -1772,20 +1842,66 @@ function deleteWebhook(id, db) {
|
|
|
1772
1842
|
const d = db || getDatabase();
|
|
1773
1843
|
return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
|
|
1774
1844
|
}
|
|
1845
|
+
function listDeliveries(webhookId, limit = 50, db) {
|
|
1846
|
+
const d = db || getDatabase();
|
|
1847
|
+
if (webhookId) {
|
|
1848
|
+
return d.query("SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?").all(webhookId, limit);
|
|
1849
|
+
}
|
|
1850
|
+
return d.query("SELECT * FROM webhook_deliveries ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
1851
|
+
}
|
|
1852
|
+
function logDelivery(d, webhookId, event, payload, statusCode, response, attempt) {
|
|
1853
|
+
const id = uuid();
|
|
1854
|
+
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()]);
|
|
1855
|
+
}
|
|
1856
|
+
function matchesScope(wh, payload) {
|
|
1857
|
+
if (wh.project_id && payload.project_id !== wh.project_id)
|
|
1858
|
+
return false;
|
|
1859
|
+
if (wh.task_list_id && payload.task_list_id !== wh.task_list_id)
|
|
1860
|
+
return false;
|
|
1861
|
+
if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
|
|
1862
|
+
return false;
|
|
1863
|
+
if (wh.task_id && payload.id !== wh.task_id)
|
|
1864
|
+
return false;
|
|
1865
|
+
return true;
|
|
1866
|
+
}
|
|
1867
|
+
async function deliverWebhook(wh, event, body, attempt, db) {
|
|
1868
|
+
try {
|
|
1869
|
+
const headers = { "Content-Type": "application/json" };
|
|
1870
|
+
if (wh.secret) {
|
|
1871
|
+
const encoder = new TextEncoder;
|
|
1872
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1873
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
1874
|
+
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1875
|
+
}
|
|
1876
|
+
const resp = await fetch(wh.url, { method: "POST", headers, body });
|
|
1877
|
+
const respText = await resp.text().catch(() => "");
|
|
1878
|
+
logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
|
|
1879
|
+
if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
|
|
1880
|
+
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
1881
|
+
setTimeout(() => {
|
|
1882
|
+
deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
|
|
1883
|
+
}, delay);
|
|
1884
|
+
}
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1887
|
+
logDelivery(db, wh.id, event, body, null, errorMsg.slice(0, 1000), attempt);
|
|
1888
|
+
if (attempt < MAX_RETRY_ATTEMPTS) {
|
|
1889
|
+
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
1890
|
+
setTimeout(() => {
|
|
1891
|
+
deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
|
|
1892
|
+
}, delay);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1775
1896
|
async function dispatchWebhook(event, payload, db) {
|
|
1776
|
-
const
|
|
1897
|
+
const d = db || getDatabase();
|
|
1898
|
+
const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
|
|
1899
|
+
const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
|
|
1777
1900
|
for (const wh of webhooks) {
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
const encoder = new TextEncoder;
|
|
1783
|
-
const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1784
|
-
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
1785
|
-
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1786
|
-
}
|
|
1787
|
-
fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
|
|
1788
|
-
} catch {}
|
|
1901
|
+
if (!matchesScope(wh, payloadObj))
|
|
1902
|
+
continue;
|
|
1903
|
+
const body = JSON.stringify({ event, payload, timestamp: now() });
|
|
1904
|
+
deliverWebhook(wh, event, body, 1, d).catch(() => {});
|
|
1789
1905
|
}
|
|
1790
1906
|
}
|
|
1791
1907
|
|
|
@@ -1794,8 +1910,10 @@ function rowToTemplate(row) {
|
|
|
1794
1910
|
return {
|
|
1795
1911
|
...row,
|
|
1796
1912
|
tags: JSON.parse(row.tags || "[]"),
|
|
1913
|
+
variables: JSON.parse(row.variables || "[]"),
|
|
1797
1914
|
metadata: JSON.parse(row.metadata || "{}"),
|
|
1798
|
-
priority: row.priority || "medium"
|
|
1915
|
+
priority: row.priority || "medium",
|
|
1916
|
+
version: row.version ?? 1
|
|
1799
1917
|
};
|
|
1800
1918
|
}
|
|
1801
1919
|
function rowToTemplateTask(row) {
|
|
@@ -1804,7 +1922,9 @@ function rowToTemplateTask(row) {
|
|
|
1804
1922
|
tags: JSON.parse(row.tags || "[]"),
|
|
1805
1923
|
depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
|
|
1806
1924
|
metadata: JSON.parse(row.metadata || "{}"),
|
|
1807
|
-
priority: row.priority || "medium"
|
|
1925
|
+
priority: row.priority || "medium",
|
|
1926
|
+
condition: row.condition ?? null,
|
|
1927
|
+
include_template_id: row.include_template_id ?? null
|
|
1808
1928
|
};
|
|
1809
1929
|
}
|
|
1810
1930
|
function resolveTemplateId(id, d) {
|
|
@@ -1813,14 +1933,15 @@ function resolveTemplateId(id, d) {
|
|
|
1813
1933
|
function createTemplate(input, db) {
|
|
1814
1934
|
const d = db || getDatabase();
|
|
1815
1935
|
const id = uuid();
|
|
1816
|
-
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
|
|
1817
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1936
|
+
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
|
|
1937
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1818
1938
|
id,
|
|
1819
1939
|
input.name,
|
|
1820
1940
|
input.title_pattern,
|
|
1821
1941
|
input.description || null,
|
|
1822
1942
|
input.priority || "medium",
|
|
1823
1943
|
JSON.stringify(input.tags || []),
|
|
1944
|
+
JSON.stringify(input.variables || []),
|
|
1824
1945
|
input.project_id || null,
|
|
1825
1946
|
input.plan_id || null,
|
|
1826
1947
|
JSON.stringify(input.metadata || {}),
|
|
@@ -1855,7 +1976,23 @@ function updateTemplate(id, updates, db) {
|
|
|
1855
1976
|
const resolved = resolveTemplateId(id, d);
|
|
1856
1977
|
if (!resolved)
|
|
1857
1978
|
return null;
|
|
1858
|
-
const
|
|
1979
|
+
const current = getTemplateWithTasks(resolved, d);
|
|
1980
|
+
if (current) {
|
|
1981
|
+
const snapshot = JSON.stringify({
|
|
1982
|
+
name: current.name,
|
|
1983
|
+
title_pattern: current.title_pattern,
|
|
1984
|
+
description: current.description,
|
|
1985
|
+
priority: current.priority,
|
|
1986
|
+
tags: current.tags,
|
|
1987
|
+
variables: current.variables,
|
|
1988
|
+
project_id: current.project_id,
|
|
1989
|
+
plan_id: current.plan_id,
|
|
1990
|
+
metadata: current.metadata,
|
|
1991
|
+
tasks: current.tasks
|
|
1992
|
+
});
|
|
1993
|
+
d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
|
|
1994
|
+
}
|
|
1995
|
+
const sets = ["version = version + 1"];
|
|
1859
1996
|
const values = [];
|
|
1860
1997
|
if (updates.name !== undefined) {
|
|
1861
1998
|
sets.push("name = ?");
|
|
@@ -1877,6 +2014,10 @@ function updateTemplate(id, updates, db) {
|
|
|
1877
2014
|
sets.push("tags = ?");
|
|
1878
2015
|
values.push(JSON.stringify(updates.tags));
|
|
1879
2016
|
}
|
|
2017
|
+
if (updates.variables !== undefined) {
|
|
2018
|
+
sets.push("variables = ?");
|
|
2019
|
+
values.push(JSON.stringify(updates.variables));
|
|
2020
|
+
}
|
|
1880
2021
|
if (updates.project_id !== undefined) {
|
|
1881
2022
|
sets.push("project_id = ?");
|
|
1882
2023
|
values.push(updates.project_id);
|
|
@@ -1889,8 +2030,6 @@ function updateTemplate(id, updates, db) {
|
|
|
1889
2030
|
sets.push("metadata = ?");
|
|
1890
2031
|
values.push(JSON.stringify(updates.metadata));
|
|
1891
2032
|
}
|
|
1892
|
-
if (sets.length === 0)
|
|
1893
|
-
return getTemplate(resolved, d);
|
|
1894
2033
|
values.push(resolved);
|
|
1895
2034
|
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
1896
2035
|
return getTemplate(resolved, d);
|
|
@@ -1920,8 +2059,8 @@ function addTemplateTasks(templateId, tasks, db) {
|
|
|
1920
2059
|
for (let i = 0;i < tasks.length; i++) {
|
|
1921
2060
|
const task = tasks[i];
|
|
1922
2061
|
const id = uuid();
|
|
1923
|
-
d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, depends_on_positions, metadata, created_at)
|
|
1924
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2062
|
+
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)
|
|
2063
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1925
2064
|
id,
|
|
1926
2065
|
templateId,
|
|
1927
2066
|
i,
|
|
@@ -1930,6 +2069,8 @@ function addTemplateTasks(templateId, tasks, db) {
|
|
|
1930
2069
|
task.priority || "medium",
|
|
1931
2070
|
JSON.stringify(task.tags || []),
|
|
1932
2071
|
task.task_type || null,
|
|
2072
|
+
task.condition || null,
|
|
2073
|
+
task.include_template_id || null,
|
|
1933
2074
|
JSON.stringify(task.depends_on || []),
|
|
1934
2075
|
JSON.stringify(task.metadata || {}),
|
|
1935
2076
|
now()
|
|
@@ -1957,11 +2098,142 @@ function getTemplateTasks(templateId, db) {
|
|
|
1957
2098
|
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
|
|
1958
2099
|
return rows.map(rowToTemplateTask);
|
|
1959
2100
|
}
|
|
1960
|
-
function
|
|
2101
|
+
function evaluateCondition(condition, variables) {
|
|
2102
|
+
if (!condition || condition.trim() === "")
|
|
2103
|
+
return true;
|
|
2104
|
+
const trimmed = condition.trim();
|
|
2105
|
+
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
2106
|
+
if (eqMatch) {
|
|
2107
|
+
const varName = eqMatch[1];
|
|
2108
|
+
const expected = eqMatch[2].trim();
|
|
2109
|
+
return (variables[varName] ?? "") === expected;
|
|
2110
|
+
}
|
|
2111
|
+
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
2112
|
+
if (neqMatch) {
|
|
2113
|
+
const varName = neqMatch[1];
|
|
2114
|
+
const expected = neqMatch[2].trim();
|
|
2115
|
+
return (variables[varName] ?? "") !== expected;
|
|
2116
|
+
}
|
|
2117
|
+
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
2118
|
+
if (falsyMatch) {
|
|
2119
|
+
const varName = falsyMatch[1];
|
|
2120
|
+
const val = variables[varName];
|
|
2121
|
+
return !val || val === "" || val === "false";
|
|
2122
|
+
}
|
|
2123
|
+
const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
|
|
2124
|
+
if (truthyMatch) {
|
|
2125
|
+
const varName = truthyMatch[1];
|
|
2126
|
+
const val = variables[varName];
|
|
2127
|
+
return !!val && val !== "" && val !== "false";
|
|
2128
|
+
}
|
|
2129
|
+
return true;
|
|
2130
|
+
}
|
|
2131
|
+
function exportTemplate(id, db) {
|
|
2132
|
+
const d = db || getDatabase();
|
|
2133
|
+
const template = getTemplateWithTasks(id, d);
|
|
2134
|
+
if (!template)
|
|
2135
|
+
throw new Error(`Template not found: ${id}`);
|
|
2136
|
+
return {
|
|
2137
|
+
name: template.name,
|
|
2138
|
+
title_pattern: template.title_pattern,
|
|
2139
|
+
description: template.description,
|
|
2140
|
+
priority: template.priority,
|
|
2141
|
+
tags: template.tags,
|
|
2142
|
+
variables: template.variables,
|
|
2143
|
+
project_id: template.project_id,
|
|
2144
|
+
plan_id: template.plan_id,
|
|
2145
|
+
metadata: template.metadata,
|
|
2146
|
+
tasks: template.tasks.map((t) => ({
|
|
2147
|
+
position: t.position,
|
|
2148
|
+
title_pattern: t.title_pattern,
|
|
2149
|
+
description: t.description,
|
|
2150
|
+
priority: t.priority,
|
|
2151
|
+
tags: t.tags,
|
|
2152
|
+
task_type: t.task_type,
|
|
2153
|
+
condition: t.condition,
|
|
2154
|
+
include_template_id: t.include_template_id,
|
|
2155
|
+
depends_on_positions: t.depends_on_positions,
|
|
2156
|
+
metadata: t.metadata
|
|
2157
|
+
}))
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
function importTemplate(json, db) {
|
|
2161
|
+
const d = db || getDatabase();
|
|
2162
|
+
const taskInputs = (json.tasks || []).map((t) => ({
|
|
2163
|
+
title_pattern: t.title_pattern,
|
|
2164
|
+
description: t.description ?? undefined,
|
|
2165
|
+
priority: t.priority,
|
|
2166
|
+
tags: t.tags,
|
|
2167
|
+
task_type: t.task_type ?? undefined,
|
|
2168
|
+
condition: t.condition ?? undefined,
|
|
2169
|
+
include_template_id: t.include_template_id ?? undefined,
|
|
2170
|
+
depends_on: t.depends_on_positions,
|
|
2171
|
+
metadata: t.metadata
|
|
2172
|
+
}));
|
|
2173
|
+
return createTemplate({
|
|
2174
|
+
name: json.name,
|
|
2175
|
+
title_pattern: json.title_pattern,
|
|
2176
|
+
description: json.description ?? undefined,
|
|
2177
|
+
priority: json.priority,
|
|
2178
|
+
tags: json.tags,
|
|
2179
|
+
variables: json.variables,
|
|
2180
|
+
project_id: json.project_id ?? undefined,
|
|
2181
|
+
plan_id: json.plan_id ?? undefined,
|
|
2182
|
+
metadata: json.metadata,
|
|
2183
|
+
tasks: taskInputs
|
|
2184
|
+
}, d);
|
|
2185
|
+
}
|
|
2186
|
+
function getTemplateVersion(id, version, db) {
|
|
2187
|
+
const d = db || getDatabase();
|
|
2188
|
+
const resolved = resolveTemplateId(id, d);
|
|
2189
|
+
if (!resolved)
|
|
2190
|
+
return null;
|
|
2191
|
+
const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
|
|
2192
|
+
return row || null;
|
|
2193
|
+
}
|
|
2194
|
+
function listTemplateVersions(id, db) {
|
|
2195
|
+
const d = db || getDatabase();
|
|
2196
|
+
const resolved = resolveTemplateId(id, d);
|
|
2197
|
+
if (!resolved)
|
|
2198
|
+
return [];
|
|
2199
|
+
return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
|
|
2200
|
+
}
|
|
2201
|
+
function resolveVariables(templateVars, provided) {
|
|
2202
|
+
const merged = { ...provided };
|
|
2203
|
+
for (const v of templateVars) {
|
|
2204
|
+
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
2205
|
+
merged[v.name] = v.default;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
const missing = [];
|
|
2209
|
+
for (const v of templateVars) {
|
|
2210
|
+
if (v.required && merged[v.name] === undefined) {
|
|
2211
|
+
missing.push(v.name);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
if (missing.length > 0) {
|
|
2215
|
+
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
2216
|
+
}
|
|
2217
|
+
return merged;
|
|
2218
|
+
}
|
|
2219
|
+
function substituteVars(text, variables) {
|
|
2220
|
+
let result = text;
|
|
2221
|
+
for (const [key, val] of Object.entries(variables)) {
|
|
2222
|
+
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
|
|
2223
|
+
}
|
|
2224
|
+
return result;
|
|
2225
|
+
}
|
|
2226
|
+
function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
|
|
1961
2227
|
const d = db || getDatabase();
|
|
1962
2228
|
const template = getTemplateWithTasks(templateId, d);
|
|
1963
2229
|
if (!template)
|
|
1964
2230
|
throw new Error(`Template not found: ${templateId}`);
|
|
2231
|
+
const visited = _visitedTemplateIds || new Set;
|
|
2232
|
+
if (visited.has(template.id)) {
|
|
2233
|
+
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
2234
|
+
}
|
|
2235
|
+
visited.add(template.id);
|
|
2236
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
1965
2237
|
if (template.tasks.length === 0) {
|
|
1966
2238
|
const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
|
|
1967
2239
|
const task = createTask(input, d);
|
|
@@ -1969,16 +2241,27 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
|
|
|
1969
2241
|
}
|
|
1970
2242
|
const createdTasks = [];
|
|
1971
2243
|
const positionToId = new Map;
|
|
2244
|
+
const skippedPositions = new Set;
|
|
1972
2245
|
for (const tt of template.tasks) {
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2246
|
+
if (tt.include_template_id) {
|
|
2247
|
+
const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
|
|
2248
|
+
createdTasks.push(...includedTasks);
|
|
2249
|
+
if (includedTasks.length > 0) {
|
|
2250
|
+
positionToId.set(tt.position, includedTasks[0].id);
|
|
2251
|
+
} else {
|
|
2252
|
+
skippedPositions.add(tt.position);
|
|
1980
2253
|
}
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
2256
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
|
|
2257
|
+
skippedPositions.add(tt.position);
|
|
2258
|
+
continue;
|
|
1981
2259
|
}
|
|
2260
|
+
let title = tt.title_pattern;
|
|
2261
|
+
let desc = tt.description;
|
|
2262
|
+
title = substituteVars(title, resolved);
|
|
2263
|
+
if (desc)
|
|
2264
|
+
desc = substituteVars(desc, resolved);
|
|
1982
2265
|
const task = createTask({
|
|
1983
2266
|
title,
|
|
1984
2267
|
description: desc ?? undefined,
|
|
@@ -1993,8 +2276,14 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
|
|
|
1993
2276
|
positionToId.set(tt.position, task.id);
|
|
1994
2277
|
}
|
|
1995
2278
|
for (const tt of template.tasks) {
|
|
2279
|
+
if (skippedPositions.has(tt.position))
|
|
2280
|
+
continue;
|
|
2281
|
+
if (tt.include_template_id)
|
|
2282
|
+
continue;
|
|
1996
2283
|
const deps = tt.depends_on_positions;
|
|
1997
2284
|
for (const depPos of deps) {
|
|
2285
|
+
if (skippedPositions.has(depPos))
|
|
2286
|
+
continue;
|
|
1998
2287
|
const taskId = positionToId.get(tt.position);
|
|
1999
2288
|
const depId = positionToId.get(depPos);
|
|
2000
2289
|
if (taskId && depId) {
|
|
@@ -2004,6 +2293,47 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
|
|
|
2004
2293
|
}
|
|
2005
2294
|
return createdTasks;
|
|
2006
2295
|
}
|
|
2296
|
+
function previewTemplate(templateId, variables, db) {
|
|
2297
|
+
const d = db || getDatabase();
|
|
2298
|
+
const template = getTemplateWithTasks(templateId, d);
|
|
2299
|
+
if (!template)
|
|
2300
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
2301
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
2302
|
+
const tasks = [];
|
|
2303
|
+
if (template.tasks.length === 0) {
|
|
2304
|
+
tasks.push({
|
|
2305
|
+
position: 0,
|
|
2306
|
+
title: substituteVars(template.title_pattern, resolved),
|
|
2307
|
+
description: template.description ? substituteVars(template.description, resolved) : null,
|
|
2308
|
+
priority: template.priority,
|
|
2309
|
+
tags: template.tags,
|
|
2310
|
+
task_type: null,
|
|
2311
|
+
depends_on_positions: []
|
|
2312
|
+
});
|
|
2313
|
+
} else {
|
|
2314
|
+
for (const tt of template.tasks) {
|
|
2315
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved))
|
|
2316
|
+
continue;
|
|
2317
|
+
tasks.push({
|
|
2318
|
+
position: tt.position,
|
|
2319
|
+
title: substituteVars(tt.title_pattern, resolved),
|
|
2320
|
+
description: tt.description ? substituteVars(tt.description, resolved) : null,
|
|
2321
|
+
priority: tt.priority,
|
|
2322
|
+
tags: tt.tags,
|
|
2323
|
+
task_type: tt.task_type,
|
|
2324
|
+
depends_on_positions: tt.depends_on_positions
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
return {
|
|
2329
|
+
template_id: template.id,
|
|
2330
|
+
template_name: template.name,
|
|
2331
|
+
description: template.description,
|
|
2332
|
+
variables: template.variables,
|
|
2333
|
+
resolved_variables: resolved,
|
|
2334
|
+
tasks
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2007
2337
|
|
|
2008
2338
|
// src/db/checklists.ts
|
|
2009
2339
|
function rowToItem(row) {
|
|
@@ -3798,6 +4128,107 @@ function clearActiveModel() {
|
|
|
3798
4128
|
delete config.activeModel;
|
|
3799
4129
|
writeConfig(config);
|
|
3800
4130
|
}
|
|
4131
|
+
// src/db/builtin-templates.ts
|
|
4132
|
+
var BUILTIN_TEMPLATES = [
|
|
4133
|
+
{
|
|
4134
|
+
name: "open-source-project",
|
|
4135
|
+
description: "Full open-source project bootstrap \u2014 scaffold to publish",
|
|
4136
|
+
variables: [
|
|
4137
|
+
{ name: "name", required: true, description: "Service name" },
|
|
4138
|
+
{ name: "org", required: false, default: "hasna", description: "GitHub org" }
|
|
4139
|
+
],
|
|
4140
|
+
tasks: [
|
|
4141
|
+
{ position: 0, title_pattern: "Scaffold {name} package structure", priority: "critical" },
|
|
4142
|
+
{ position: 1, title_pattern: "Create {name} SQLite database + migrations", priority: "critical", depends_on_positions: [0] },
|
|
4143
|
+
{ position: 2, title_pattern: "Implement {name} CRUD operations", priority: "high", depends_on_positions: [1] },
|
|
4144
|
+
{ position: 3, title_pattern: "Build {name} MCP server with standard tools", priority: "high", depends_on_positions: [2] },
|
|
4145
|
+
{ position: 4, title_pattern: "Build {name} CLI with Commander.js", priority: "high", depends_on_positions: [2] },
|
|
4146
|
+
{ position: 5, title_pattern: "Build {name} REST API", priority: "medium", depends_on_positions: [2] },
|
|
4147
|
+
{ position: 6, title_pattern: "Write unit tests for {name}", priority: "high", depends_on_positions: [2, 3, 4] },
|
|
4148
|
+
{ position: 7, title_pattern: "Add Apache 2.0 license and README", priority: "medium", depends_on_positions: [0] },
|
|
4149
|
+
{ position: 8, title_pattern: "Create GitHub repo {org}/{name}", priority: "medium", depends_on_positions: [7] },
|
|
4150
|
+
{ position: 9, title_pattern: "Add @hasna/cloud adapter", priority: "medium", depends_on_positions: [1] },
|
|
4151
|
+
{ position: 10, title_pattern: "Write PostgreSQL migrations for {name}", priority: "medium", depends_on_positions: [1] },
|
|
4152
|
+
{ position: 11, title_pattern: "Add feedback system + send_feedback MCP tool", priority: "medium", depends_on_positions: [3] },
|
|
4153
|
+
{ position: 12, title_pattern: "Add agent tools (register_agent, heartbeat, set_focus, list_agents)", priority: "medium", depends_on_positions: [3] },
|
|
4154
|
+
{ position: 13, title_pattern: "Create RDS database for {name}", priority: "low", depends_on_positions: [10] },
|
|
4155
|
+
{ position: 14, title_pattern: "Publish @hasna/{name} to npm", priority: "high", depends_on_positions: [6, 7, 8] },
|
|
4156
|
+
{ position: 15, title_pattern: "Install @hasna/{name} globally and verify", priority: "medium", depends_on_positions: [14] }
|
|
4157
|
+
]
|
|
4158
|
+
},
|
|
4159
|
+
{
|
|
4160
|
+
name: "bug-fix",
|
|
4161
|
+
description: "Standard bug fix workflow",
|
|
4162
|
+
variables: [{ name: "bug", required: true, description: "Bug description" }],
|
|
4163
|
+
tasks: [
|
|
4164
|
+
{ position: 0, title_pattern: "Reproduce: {bug}", priority: "critical" },
|
|
4165
|
+
{ position: 1, title_pattern: "Diagnose root cause of {bug}", priority: "critical", depends_on_positions: [0] },
|
|
4166
|
+
{ position: 2, title_pattern: "Implement fix for {bug}", priority: "critical", depends_on_positions: [1] },
|
|
4167
|
+
{ position: 3, title_pattern: "Write regression test for {bug}", priority: "high", depends_on_positions: [2] },
|
|
4168
|
+
{ position: 4, title_pattern: "Publish fix and verify in production", priority: "high", depends_on_positions: [3] }
|
|
4169
|
+
]
|
|
4170
|
+
},
|
|
4171
|
+
{
|
|
4172
|
+
name: "feature",
|
|
4173
|
+
description: "Standard feature development workflow",
|
|
4174
|
+
variables: [{ name: "feature", required: true }, { name: "scope", required: false, default: "medium" }],
|
|
4175
|
+
tasks: [
|
|
4176
|
+
{ position: 0, title_pattern: "Write spec for {feature}", priority: "high" },
|
|
4177
|
+
{ position: 1, title_pattern: "Design implementation approach for {feature}", priority: "high", depends_on_positions: [0] },
|
|
4178
|
+
{ position: 2, title_pattern: "Implement {feature}", priority: "critical", depends_on_positions: [1] },
|
|
4179
|
+
{ position: 3, title_pattern: "Write tests for {feature}", priority: "high", depends_on_positions: [2] },
|
|
4180
|
+
{ position: 4, title_pattern: "Code review for {feature}", priority: "medium", depends_on_positions: [3] },
|
|
4181
|
+
{ position: 5, title_pattern: "Update docs for {feature}", priority: "medium", depends_on_positions: [2] },
|
|
4182
|
+
{ position: 6, title_pattern: "Deploy {feature}", priority: "high", depends_on_positions: [4] }
|
|
4183
|
+
]
|
|
4184
|
+
},
|
|
4185
|
+
{
|
|
4186
|
+
name: "security-audit",
|
|
4187
|
+
description: "Security audit workflow",
|
|
4188
|
+
variables: [{ name: "target", required: true }],
|
|
4189
|
+
tasks: [
|
|
4190
|
+
{ position: 0, title_pattern: "Scan {target} for vulnerabilities", priority: "critical" },
|
|
4191
|
+
{ position: 1, title_pattern: "Review {target} security findings", priority: "critical", depends_on_positions: [0] },
|
|
4192
|
+
{ position: 2, title_pattern: "Fix critical issues in {target}", priority: "critical", depends_on_positions: [1] },
|
|
4193
|
+
{ position: 3, title_pattern: "Retest {target} after fixes", priority: "high", depends_on_positions: [2] },
|
|
4194
|
+
{ position: 4, title_pattern: "Write security report for {target}", priority: "medium", depends_on_positions: [3] },
|
|
4195
|
+
{ position: 5, title_pattern: "Close audit for {target}", priority: "low", depends_on_positions: [4] }
|
|
4196
|
+
]
|
|
4197
|
+
}
|
|
4198
|
+
];
|
|
4199
|
+
function initBuiltinTemplates(db) {
|
|
4200
|
+
const d = db || getDatabase();
|
|
4201
|
+
const existing = listTemplates(d);
|
|
4202
|
+
const existingNames = new Set(existing.map((t) => t.name));
|
|
4203
|
+
let created = 0;
|
|
4204
|
+
let skipped = 0;
|
|
4205
|
+
const names = [];
|
|
4206
|
+
for (const bt of BUILTIN_TEMPLATES) {
|
|
4207
|
+
if (existingNames.has(bt.name)) {
|
|
4208
|
+
skipped++;
|
|
4209
|
+
continue;
|
|
4210
|
+
}
|
|
4211
|
+
const tasks = bt.tasks.map((t) => ({
|
|
4212
|
+
title_pattern: t.title_pattern,
|
|
4213
|
+
description: t.description,
|
|
4214
|
+
priority: t.priority,
|
|
4215
|
+
tags: t.tags,
|
|
4216
|
+
task_type: t.task_type,
|
|
4217
|
+
depends_on: t.depends_on_positions || t.depends_on,
|
|
4218
|
+
metadata: t.metadata
|
|
4219
|
+
}));
|
|
4220
|
+
createTemplate({
|
|
4221
|
+
name: bt.name,
|
|
4222
|
+
title_pattern: `${bt.name}: {${bt.variables[0]?.name || "name"}}`,
|
|
4223
|
+
description: bt.description,
|
|
4224
|
+
variables: bt.variables,
|
|
4225
|
+
tasks
|
|
4226
|
+
}, d);
|
|
4227
|
+
created++;
|
|
4228
|
+
names.push(bt.name);
|
|
4229
|
+
}
|
|
4230
|
+
return { created, skipped, names };
|
|
4231
|
+
}
|
|
3801
4232
|
// src/db/handoffs.ts
|
|
3802
4233
|
function createHandoff(input, db) {
|
|
3803
4234
|
const d = db || getDatabase();
|
|
@@ -14751,6 +15182,26 @@ var PG_MIGRATIONS = [
|
|
|
14751
15182
|
CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
|
|
14752
15183
|
|
|
14753
15184
|
INSERT INTO _migrations (id) VALUES (37) ON CONFLICT DO NOTHING;
|
|
15185
|
+
`,
|
|
15186
|
+
`
|
|
15187
|
+
ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS variables TEXT DEFAULT '[]';
|
|
15188
|
+
INSERT INTO _migrations (id) VALUES (38) ON CONFLICT DO NOTHING;
|
|
15189
|
+
`,
|
|
15190
|
+
`
|
|
15191
|
+
ALTER TABLE template_tasks ADD COLUMN IF NOT EXISTS condition TEXT;
|
|
15192
|
+
ALTER TABLE template_tasks ADD COLUMN IF NOT EXISTS include_template_id TEXT;
|
|
15193
|
+
ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1;
|
|
15194
|
+
|
|
15195
|
+
CREATE TABLE IF NOT EXISTS template_versions (
|
|
15196
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
15197
|
+
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
15198
|
+
version INTEGER NOT NULL,
|
|
15199
|
+
snapshot TEXT NOT NULL,
|
|
15200
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15201
|
+
);
|
|
15202
|
+
CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id);
|
|
15203
|
+
|
|
15204
|
+
INSERT INTO _migrations (id) VALUES (39) ON CONFLICT DO NOTHING;
|
|
14754
15205
|
`
|
|
14755
15206
|
];
|
|
14756
15207
|
|
|
@@ -15325,6 +15776,7 @@ export {
|
|
|
15325
15776
|
searchTasks,
|
|
15326
15777
|
scoreTask,
|
|
15327
15778
|
saveSnapshot,
|
|
15779
|
+
resolveVariables,
|
|
15328
15780
|
resolvePartialId,
|
|
15329
15781
|
resetDatabase,
|
|
15330
15782
|
removeTaskRelationshipByPair,
|
|
@@ -15338,6 +15790,7 @@ export {
|
|
|
15338
15790
|
releaseAgent,
|
|
15339
15791
|
registerAgent,
|
|
15340
15792
|
redistributeStaleTasks,
|
|
15793
|
+
previewTemplate,
|
|
15341
15794
|
patrolTasks,
|
|
15342
15795
|
parseRecurrenceRule,
|
|
15343
15796
|
parseGitHubUrl,
|
|
@@ -15354,6 +15807,7 @@ export {
|
|
|
15354
15807
|
loadConfig,
|
|
15355
15808
|
listWebhooks,
|
|
15356
15809
|
listTemplates,
|
|
15810
|
+
listTemplateVersions,
|
|
15357
15811
|
listTasks,
|
|
15358
15812
|
listTaskLists,
|
|
15359
15813
|
listTaskFiles,
|
|
@@ -15364,14 +15818,18 @@ export {
|
|
|
15364
15818
|
listPlans,
|
|
15365
15819
|
listOrgs,
|
|
15366
15820
|
listHandoffs,
|
|
15821
|
+
listDeliveries,
|
|
15367
15822
|
listComments,
|
|
15368
15823
|
listAgents,
|
|
15369
15824
|
issueToTask,
|
|
15370
15825
|
isValidRecurrenceRule,
|
|
15371
15826
|
isAgentConflict,
|
|
15827
|
+
initBuiltinTemplates,
|
|
15828
|
+
importTemplate,
|
|
15372
15829
|
getWebhook,
|
|
15373
15830
|
getTraceStats,
|
|
15374
15831
|
getTemplateWithTasks,
|
|
15832
|
+
getTemplateVersion,
|
|
15375
15833
|
getTemplateTasks,
|
|
15376
15834
|
getTemplate,
|
|
15377
15835
|
getTasksChangedSince,
|
|
@@ -15432,6 +15890,8 @@ export {
|
|
|
15432
15890
|
failTask,
|
|
15433
15891
|
extractTodos,
|
|
15434
15892
|
extractFromSource,
|
|
15893
|
+
exportTemplate,
|
|
15894
|
+
evaluateCondition,
|
|
15435
15895
|
ensureTaskList,
|
|
15436
15896
|
ensureProject,
|
|
15437
15897
|
dispatchWebhook,
|
|
@@ -15501,5 +15961,6 @@ export {
|
|
|
15501
15961
|
DependencyCycleError,
|
|
15502
15962
|
DEFAULT_MODEL,
|
|
15503
15963
|
CompletionGuardError,
|
|
15964
|
+
BUILTIN_TEMPLATES,
|
|
15504
15965
|
AgentNotFoundError
|
|
15505
15966
|
};
|