@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 CHANGED
@@ -2388,6 +2388,36 @@ function ensureSchema(db) {
2388
2388
  ensureColumn("projects", "org_id", "TEXT");
2389
2389
  ensureColumn("plans", "task_list_id", "TEXT");
2390
2390
  ensureColumn("plans", "agent_id", "TEXT");
2391
+ ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
2392
+ ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
2393
+ ensureColumn("template_tasks", "condition", "TEXT");
2394
+ ensureColumn("template_tasks", "include_template_id", "TEXT");
2395
+ ensureTable("template_versions", `
2396
+ CREATE TABLE template_versions (
2397
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
2398
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
2399
+ version INTEGER NOT NULL,
2400
+ snapshot TEXT NOT NULL,
2401
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2402
+ )`);
2403
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
2404
+ ensureColumn("webhooks", "project_id", "TEXT");
2405
+ ensureColumn("webhooks", "task_list_id", "TEXT");
2406
+ ensureColumn("webhooks", "agent_id", "TEXT");
2407
+ ensureColumn("webhooks", "task_id", "TEXT");
2408
+ ensureTable("webhook_deliveries", `
2409
+ CREATE TABLE webhook_deliveries (
2410
+ id TEXT PRIMARY KEY,
2411
+ webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
2412
+ event TEXT NOT NULL,
2413
+ payload TEXT NOT NULL,
2414
+ status_code INTEGER,
2415
+ response TEXT,
2416
+ attempt INTEGER NOT NULL DEFAULT 1,
2417
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2418
+ )`);
2419
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
2420
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
2391
2421
  ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
2392
2422
  ensureColumn("task_comments", "progress_pct", "INTEGER");
2393
2423
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
@@ -3076,6 +3106,26 @@ var init_database = __esm(() => {
3076
3106
  CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
3077
3107
 
3078
3108
  INSERT OR IGNORE INTO _migrations (id) VALUES (37);
3109
+ `,
3110
+ `
3111
+ ALTER TABLE task_templates ADD COLUMN variables TEXT DEFAULT '[]';
3112
+ INSERT OR IGNORE INTO _migrations (id) VALUES (38);
3113
+ `,
3114
+ `
3115
+ ALTER TABLE template_tasks ADD COLUMN condition TEXT;
3116
+ ALTER TABLE template_tasks ADD COLUMN include_template_id TEXT;
3117
+ ALTER TABLE task_templates ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
3118
+
3119
+ CREATE TABLE IF NOT EXISTS template_versions (
3120
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
3121
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
3122
+ version INTEGER NOT NULL,
3123
+ snapshot TEXT NOT NULL,
3124
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
3125
+ );
3126
+ CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id);
3127
+
3128
+ INSERT OR IGNORE INTO _migrations (id) VALUES (39);
3079
3129
  `
3080
3130
  ];
3081
3131
  });
@@ -3713,18 +3763,37 @@ var init_recurrence = __esm(() => {
3713
3763
  var exports_webhooks = {};
3714
3764
  __export(exports_webhooks, {
3715
3765
  listWebhooks: () => listWebhooks,
3766
+ listDeliveries: () => listDeliveries,
3716
3767
  getWebhook: () => getWebhook,
3717
3768
  dispatchWebhook: () => dispatchWebhook,
3718
3769
  deleteWebhook: () => deleteWebhook,
3719
3770
  createWebhook: () => createWebhook
3720
3771
  });
3721
3772
  function rowToWebhook(row) {
3722
- return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
3773
+ return {
3774
+ ...row,
3775
+ events: JSON.parse(row.events || "[]"),
3776
+ active: !!row.active,
3777
+ project_id: row.project_id || null,
3778
+ task_list_id: row.task_list_id || null,
3779
+ agent_id: row.agent_id || null,
3780
+ task_id: row.task_id || null
3781
+ };
3723
3782
  }
3724
3783
  function createWebhook(input, db) {
3725
3784
  const d = db || getDatabase();
3726
3785
  const id = uuid();
3727
- d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
3786
+ d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3787
+ id,
3788
+ input.url,
3789
+ JSON.stringify(input.events || []),
3790
+ input.secret || null,
3791
+ input.project_id || null,
3792
+ input.task_list_id || null,
3793
+ input.agent_id || null,
3794
+ input.task_id || null,
3795
+ now()
3796
+ ]);
3728
3797
  return getWebhook(id, d);
3729
3798
  }
3730
3799
  function getWebhook(id, db) {
@@ -3740,22 +3809,69 @@ function deleteWebhook(id, db) {
3740
3809
  const d = db || getDatabase();
3741
3810
  return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
3742
3811
  }
3812
+ function listDeliveries(webhookId, limit = 50, db) {
3813
+ const d = db || getDatabase();
3814
+ if (webhookId) {
3815
+ return d.query("SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?").all(webhookId, limit);
3816
+ }
3817
+ return d.query("SELECT * FROM webhook_deliveries ORDER BY created_at DESC LIMIT ?").all(limit);
3818
+ }
3819
+ function logDelivery(d, webhookId, event, payload, statusCode, response, attempt) {
3820
+ const id = uuid();
3821
+ 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()]);
3822
+ }
3823
+ function matchesScope(wh, payload) {
3824
+ if (wh.project_id && payload.project_id !== wh.project_id)
3825
+ return false;
3826
+ if (wh.task_list_id && payload.task_list_id !== wh.task_list_id)
3827
+ return false;
3828
+ if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
3829
+ return false;
3830
+ if (wh.task_id && payload.id !== wh.task_id)
3831
+ return false;
3832
+ return true;
3833
+ }
3834
+ async function deliverWebhook(wh, event, body, attempt, db) {
3835
+ try {
3836
+ const headers = { "Content-Type": "application/json" };
3837
+ if (wh.secret) {
3838
+ const encoder = new TextEncoder;
3839
+ const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
3840
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
3841
+ headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
3842
+ }
3843
+ const resp = await fetch(wh.url, { method: "POST", headers, body });
3844
+ const respText = await resp.text().catch(() => "");
3845
+ logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
3846
+ if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
3847
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
3848
+ setTimeout(() => {
3849
+ deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
3850
+ }, delay);
3851
+ }
3852
+ } catch (err) {
3853
+ const errorMsg = err instanceof Error ? err.message : String(err);
3854
+ logDelivery(db, wh.id, event, body, null, errorMsg.slice(0, 1000), attempt);
3855
+ if (attempt < MAX_RETRY_ATTEMPTS) {
3856
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
3857
+ setTimeout(() => {
3858
+ deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
3859
+ }, delay);
3860
+ }
3861
+ }
3862
+ }
3743
3863
  async function dispatchWebhook(event, payload, db) {
3744
- const webhooks = listWebhooks(db).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
3864
+ const d = db || getDatabase();
3865
+ const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
3866
+ const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
3745
3867
  for (const wh of webhooks) {
3746
- try {
3747
- const body = JSON.stringify({ event, payload, timestamp: now() });
3748
- const headers = { "Content-Type": "application/json" };
3749
- if (wh.secret) {
3750
- const encoder = new TextEncoder;
3751
- const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
3752
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
3753
- headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
3754
- }
3755
- fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
3756
- } catch {}
3868
+ if (!matchesScope(wh, payloadObj))
3869
+ continue;
3870
+ const body = JSON.stringify({ event, payload, timestamp: now() });
3871
+ deliverWebhook(wh, event, body, 1, d).catch(() => {});
3757
3872
  }
3758
3873
  }
3874
+ var MAX_RETRY_ATTEMPTS = 3, RETRY_BASE_DELAY_MS = 1000;
3759
3875
  var init_webhooks = __esm(() => {
3760
3876
  init_database();
3761
3877
  });
@@ -3766,10 +3882,17 @@ __export(exports_templates, {
3766
3882
  updateTemplate: () => updateTemplate,
3767
3883
  tasksFromTemplate: () => tasksFromTemplate,
3768
3884
  taskFromTemplate: () => taskFromTemplate,
3885
+ resolveVariables: () => resolveVariables,
3886
+ previewTemplate: () => previewTemplate,
3769
3887
  listTemplates: () => listTemplates,
3888
+ listTemplateVersions: () => listTemplateVersions,
3889
+ importTemplate: () => importTemplate,
3770
3890
  getTemplateWithTasks: () => getTemplateWithTasks,
3891
+ getTemplateVersion: () => getTemplateVersion,
3771
3892
  getTemplateTasks: () => getTemplateTasks,
3772
3893
  getTemplate: () => getTemplate,
3894
+ exportTemplate: () => exportTemplate,
3895
+ evaluateCondition: () => evaluateCondition,
3773
3896
  deleteTemplate: () => deleteTemplate,
3774
3897
  createTemplate: () => createTemplate,
3775
3898
  addTemplateTasks: () => addTemplateTasks
@@ -3778,8 +3901,10 @@ function rowToTemplate(row) {
3778
3901
  return {
3779
3902
  ...row,
3780
3903
  tags: JSON.parse(row.tags || "[]"),
3904
+ variables: JSON.parse(row.variables || "[]"),
3781
3905
  metadata: JSON.parse(row.metadata || "{}"),
3782
- priority: row.priority || "medium"
3906
+ priority: row.priority || "medium",
3907
+ version: row.version ?? 1
3783
3908
  };
3784
3909
  }
3785
3910
  function rowToTemplateTask(row) {
@@ -3788,7 +3913,9 @@ function rowToTemplateTask(row) {
3788
3913
  tags: JSON.parse(row.tags || "[]"),
3789
3914
  depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
3790
3915
  metadata: JSON.parse(row.metadata || "{}"),
3791
- priority: row.priority || "medium"
3916
+ priority: row.priority || "medium",
3917
+ condition: row.condition ?? null,
3918
+ include_template_id: row.include_template_id ?? null
3792
3919
  };
3793
3920
  }
3794
3921
  function resolveTemplateId(id, d) {
@@ -3797,14 +3924,15 @@ function resolveTemplateId(id, d) {
3797
3924
  function createTemplate(input, db) {
3798
3925
  const d = db || getDatabase();
3799
3926
  const id = uuid();
3800
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
3801
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3927
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
3928
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3802
3929
  id,
3803
3930
  input.name,
3804
3931
  input.title_pattern,
3805
3932
  input.description || null,
3806
3933
  input.priority || "medium",
3807
3934
  JSON.stringify(input.tags || []),
3935
+ JSON.stringify(input.variables || []),
3808
3936
  input.project_id || null,
3809
3937
  input.plan_id || null,
3810
3938
  JSON.stringify(input.metadata || {}),
@@ -3839,7 +3967,23 @@ function updateTemplate(id, updates, db) {
3839
3967
  const resolved = resolveTemplateId(id, d);
3840
3968
  if (!resolved)
3841
3969
  return null;
3842
- const sets = [];
3970
+ const current = getTemplateWithTasks(resolved, d);
3971
+ if (current) {
3972
+ const snapshot = JSON.stringify({
3973
+ name: current.name,
3974
+ title_pattern: current.title_pattern,
3975
+ description: current.description,
3976
+ priority: current.priority,
3977
+ tags: current.tags,
3978
+ variables: current.variables,
3979
+ project_id: current.project_id,
3980
+ plan_id: current.plan_id,
3981
+ metadata: current.metadata,
3982
+ tasks: current.tasks
3983
+ });
3984
+ d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
3985
+ }
3986
+ const sets = ["version = version + 1"];
3843
3987
  const values = [];
3844
3988
  if (updates.name !== undefined) {
3845
3989
  sets.push("name = ?");
@@ -3861,6 +4005,10 @@ function updateTemplate(id, updates, db) {
3861
4005
  sets.push("tags = ?");
3862
4006
  values.push(JSON.stringify(updates.tags));
3863
4007
  }
4008
+ if (updates.variables !== undefined) {
4009
+ sets.push("variables = ?");
4010
+ values.push(JSON.stringify(updates.variables));
4011
+ }
3864
4012
  if (updates.project_id !== undefined) {
3865
4013
  sets.push("project_id = ?");
3866
4014
  values.push(updates.project_id);
@@ -3873,8 +4021,6 @@ function updateTemplate(id, updates, db) {
3873
4021
  sets.push("metadata = ?");
3874
4022
  values.push(JSON.stringify(updates.metadata));
3875
4023
  }
3876
- if (sets.length === 0)
3877
- return getTemplate(resolved, d);
3878
4024
  values.push(resolved);
3879
4025
  d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
3880
4026
  return getTemplate(resolved, d);
@@ -3904,8 +4050,8 @@ function addTemplateTasks(templateId, tasks, db) {
3904
4050
  for (let i = 0;i < tasks.length; i++) {
3905
4051
  const task = tasks[i];
3906
4052
  const id = uuid();
3907
- d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, depends_on_positions, metadata, created_at)
3908
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
4053
+ 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)
4054
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3909
4055
  id,
3910
4056
  templateId,
3911
4057
  i,
@@ -3914,6 +4060,8 @@ function addTemplateTasks(templateId, tasks, db) {
3914
4060
  task.priority || "medium",
3915
4061
  JSON.stringify(task.tags || []),
3916
4062
  task.task_type || null,
4063
+ task.condition || null,
4064
+ task.include_template_id || null,
3917
4065
  JSON.stringify(task.depends_on || []),
3918
4066
  JSON.stringify(task.metadata || {}),
3919
4067
  now()
@@ -3941,11 +4089,142 @@ function getTemplateTasks(templateId, db) {
3941
4089
  const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
3942
4090
  return rows.map(rowToTemplateTask);
3943
4091
  }
3944
- function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
4092
+ function evaluateCondition(condition, variables) {
4093
+ if (!condition || condition.trim() === "")
4094
+ return true;
4095
+ const trimmed = condition.trim();
4096
+ const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
4097
+ if (eqMatch) {
4098
+ const varName = eqMatch[1];
4099
+ const expected = eqMatch[2].trim();
4100
+ return (variables[varName] ?? "") === expected;
4101
+ }
4102
+ const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
4103
+ if (neqMatch) {
4104
+ const varName = neqMatch[1];
4105
+ const expected = neqMatch[2].trim();
4106
+ return (variables[varName] ?? "") !== expected;
4107
+ }
4108
+ const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
4109
+ if (falsyMatch) {
4110
+ const varName = falsyMatch[1];
4111
+ const val = variables[varName];
4112
+ return !val || val === "" || val === "false";
4113
+ }
4114
+ const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
4115
+ if (truthyMatch) {
4116
+ const varName = truthyMatch[1];
4117
+ const val = variables[varName];
4118
+ return !!val && val !== "" && val !== "false";
4119
+ }
4120
+ return true;
4121
+ }
4122
+ function exportTemplate(id, db) {
4123
+ const d = db || getDatabase();
4124
+ const template = getTemplateWithTasks(id, d);
4125
+ if (!template)
4126
+ throw new Error(`Template not found: ${id}`);
4127
+ return {
4128
+ name: template.name,
4129
+ title_pattern: template.title_pattern,
4130
+ description: template.description,
4131
+ priority: template.priority,
4132
+ tags: template.tags,
4133
+ variables: template.variables,
4134
+ project_id: template.project_id,
4135
+ plan_id: template.plan_id,
4136
+ metadata: template.metadata,
4137
+ tasks: template.tasks.map((t) => ({
4138
+ position: t.position,
4139
+ title_pattern: t.title_pattern,
4140
+ description: t.description,
4141
+ priority: t.priority,
4142
+ tags: t.tags,
4143
+ task_type: t.task_type,
4144
+ condition: t.condition,
4145
+ include_template_id: t.include_template_id,
4146
+ depends_on_positions: t.depends_on_positions,
4147
+ metadata: t.metadata
4148
+ }))
4149
+ };
4150
+ }
4151
+ function importTemplate(json, db) {
4152
+ const d = db || getDatabase();
4153
+ const taskInputs = (json.tasks || []).map((t) => ({
4154
+ title_pattern: t.title_pattern,
4155
+ description: t.description ?? undefined,
4156
+ priority: t.priority,
4157
+ tags: t.tags,
4158
+ task_type: t.task_type ?? undefined,
4159
+ condition: t.condition ?? undefined,
4160
+ include_template_id: t.include_template_id ?? undefined,
4161
+ depends_on: t.depends_on_positions,
4162
+ metadata: t.metadata
4163
+ }));
4164
+ return createTemplate({
4165
+ name: json.name,
4166
+ title_pattern: json.title_pattern,
4167
+ description: json.description ?? undefined,
4168
+ priority: json.priority,
4169
+ tags: json.tags,
4170
+ variables: json.variables,
4171
+ project_id: json.project_id ?? undefined,
4172
+ plan_id: json.plan_id ?? undefined,
4173
+ metadata: json.metadata,
4174
+ tasks: taskInputs
4175
+ }, d);
4176
+ }
4177
+ function getTemplateVersion(id, version, db) {
4178
+ const d = db || getDatabase();
4179
+ const resolved = resolveTemplateId(id, d);
4180
+ if (!resolved)
4181
+ return null;
4182
+ const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
4183
+ return row || null;
4184
+ }
4185
+ function listTemplateVersions(id, db) {
4186
+ const d = db || getDatabase();
4187
+ const resolved = resolveTemplateId(id, d);
4188
+ if (!resolved)
4189
+ return [];
4190
+ return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
4191
+ }
4192
+ function resolveVariables(templateVars, provided) {
4193
+ const merged = { ...provided };
4194
+ for (const v of templateVars) {
4195
+ if (merged[v.name] === undefined && v.default !== undefined) {
4196
+ merged[v.name] = v.default;
4197
+ }
4198
+ }
4199
+ const missing = [];
4200
+ for (const v of templateVars) {
4201
+ if (v.required && merged[v.name] === undefined) {
4202
+ missing.push(v.name);
4203
+ }
4204
+ }
4205
+ if (missing.length > 0) {
4206
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
4207
+ }
4208
+ return merged;
4209
+ }
4210
+ function substituteVars(text, variables) {
4211
+ let result = text;
4212
+ for (const [key, val] of Object.entries(variables)) {
4213
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
4214
+ }
4215
+ return result;
4216
+ }
4217
+ function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
3945
4218
  const d = db || getDatabase();
3946
4219
  const template = getTemplateWithTasks(templateId, d);
3947
4220
  if (!template)
3948
4221
  throw new Error(`Template not found: ${templateId}`);
4222
+ const visited = _visitedTemplateIds || new Set;
4223
+ if (visited.has(template.id)) {
4224
+ throw new Error(`Circular template reference detected: ${template.id}`);
4225
+ }
4226
+ visited.add(template.id);
4227
+ const resolved = resolveVariables(template.variables, variables);
3949
4228
  if (template.tasks.length === 0) {
3950
4229
  const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
3951
4230
  const task = createTask(input, d);
@@ -3953,16 +4232,27 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
3953
4232
  }
3954
4233
  const createdTasks = [];
3955
4234
  const positionToId = new Map;
4235
+ const skippedPositions = new Set;
3956
4236
  for (const tt of template.tasks) {
3957
- let title = tt.title_pattern;
3958
- let desc = tt.description;
3959
- if (variables) {
3960
- for (const [key, val] of Object.entries(variables)) {
3961
- title = title.replace(new RegExp(`\\{${key}\\}`, "g"), val);
3962
- if (desc)
3963
- desc = desc.replace(new RegExp(`\\{${key}\\}`, "g"), val);
4237
+ if (tt.include_template_id) {
4238
+ const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
4239
+ createdTasks.push(...includedTasks);
4240
+ if (includedTasks.length > 0) {
4241
+ positionToId.set(tt.position, includedTasks[0].id);
4242
+ } else {
4243
+ skippedPositions.add(tt.position);
3964
4244
  }
4245
+ continue;
3965
4246
  }
4247
+ if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
4248
+ skippedPositions.add(tt.position);
4249
+ continue;
4250
+ }
4251
+ let title = tt.title_pattern;
4252
+ let desc = tt.description;
4253
+ title = substituteVars(title, resolved);
4254
+ if (desc)
4255
+ desc = substituteVars(desc, resolved);
3966
4256
  const task = createTask({
3967
4257
  title,
3968
4258
  description: desc ?? undefined,
@@ -3977,8 +4267,14 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
3977
4267
  positionToId.set(tt.position, task.id);
3978
4268
  }
3979
4269
  for (const tt of template.tasks) {
4270
+ if (skippedPositions.has(tt.position))
4271
+ continue;
4272
+ if (tt.include_template_id)
4273
+ continue;
3980
4274
  const deps = tt.depends_on_positions;
3981
4275
  for (const depPos of deps) {
4276
+ if (skippedPositions.has(depPos))
4277
+ continue;
3982
4278
  const taskId = positionToId.get(tt.position);
3983
4279
  const depId = positionToId.get(depPos);
3984
4280
  if (taskId && depId) {
@@ -3988,6 +4284,47 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
3988
4284
  }
3989
4285
  return createdTasks;
3990
4286
  }
4287
+ function previewTemplate(templateId, variables, db) {
4288
+ const d = db || getDatabase();
4289
+ const template = getTemplateWithTasks(templateId, d);
4290
+ if (!template)
4291
+ throw new Error(`Template not found: ${templateId}`);
4292
+ const resolved = resolveVariables(template.variables, variables);
4293
+ const tasks = [];
4294
+ if (template.tasks.length === 0) {
4295
+ tasks.push({
4296
+ position: 0,
4297
+ title: substituteVars(template.title_pattern, resolved),
4298
+ description: template.description ? substituteVars(template.description, resolved) : null,
4299
+ priority: template.priority,
4300
+ tags: template.tags,
4301
+ task_type: null,
4302
+ depends_on_positions: []
4303
+ });
4304
+ } else {
4305
+ for (const tt of template.tasks) {
4306
+ if (tt.condition && !evaluateCondition(tt.condition, resolved))
4307
+ continue;
4308
+ tasks.push({
4309
+ position: tt.position,
4310
+ title: substituteVars(tt.title_pattern, resolved),
4311
+ description: tt.description ? substituteVars(tt.description, resolved) : null,
4312
+ priority: tt.priority,
4313
+ tags: tt.tags,
4314
+ task_type: tt.task_type,
4315
+ depends_on_positions: tt.depends_on_positions
4316
+ });
4317
+ }
4318
+ }
4319
+ return {
4320
+ template_id: template.id,
4321
+ template_name: template.name,
4322
+ description: template.description,
4323
+ variables: template.variables,
4324
+ resolved_variables: resolved,
4325
+ tasks
4326
+ };
4327
+ }
3991
4328
  var init_templates = __esm(() => {
3992
4329
  init_database();
3993
4330
  init_tasks();
@@ -6550,6 +6887,118 @@ var init_task_commits = __esm(() => {
6550
6887
  init_database();
6551
6888
  });
6552
6889
 
6890
+ // src/db/builtin-templates.ts
6891
+ var exports_builtin_templates = {};
6892
+ __export(exports_builtin_templates, {
6893
+ initBuiltinTemplates: () => initBuiltinTemplates,
6894
+ BUILTIN_TEMPLATES: () => BUILTIN_TEMPLATES
6895
+ });
6896
+ function initBuiltinTemplates(db) {
6897
+ const d = db || getDatabase();
6898
+ const existing = listTemplates(d);
6899
+ const existingNames = new Set(existing.map((t) => t.name));
6900
+ let created = 0;
6901
+ let skipped = 0;
6902
+ const names = [];
6903
+ for (const bt of BUILTIN_TEMPLATES) {
6904
+ if (existingNames.has(bt.name)) {
6905
+ skipped++;
6906
+ continue;
6907
+ }
6908
+ const tasks = bt.tasks.map((t) => ({
6909
+ title_pattern: t.title_pattern,
6910
+ description: t.description,
6911
+ priority: t.priority,
6912
+ tags: t.tags,
6913
+ task_type: t.task_type,
6914
+ depends_on: t.depends_on_positions || t.depends_on,
6915
+ metadata: t.metadata
6916
+ }));
6917
+ createTemplate({
6918
+ name: bt.name,
6919
+ title_pattern: `${bt.name}: {${bt.variables[0]?.name || "name"}}`,
6920
+ description: bt.description,
6921
+ variables: bt.variables,
6922
+ tasks
6923
+ }, d);
6924
+ created++;
6925
+ names.push(bt.name);
6926
+ }
6927
+ return { created, skipped, names };
6928
+ }
6929
+ var BUILTIN_TEMPLATES;
6930
+ var init_builtin_templates = __esm(() => {
6931
+ init_database();
6932
+ init_templates();
6933
+ BUILTIN_TEMPLATES = [
6934
+ {
6935
+ name: "open-source-project",
6936
+ description: "Full open-source project bootstrap \u2014 scaffold to publish",
6937
+ variables: [
6938
+ { name: "name", required: true, description: "Service name" },
6939
+ { name: "org", required: false, default: "hasna", description: "GitHub org" }
6940
+ ],
6941
+ tasks: [
6942
+ { position: 0, title_pattern: "Scaffold {name} package structure", priority: "critical" },
6943
+ { position: 1, title_pattern: "Create {name} SQLite database + migrations", priority: "critical", depends_on_positions: [0] },
6944
+ { position: 2, title_pattern: "Implement {name} CRUD operations", priority: "high", depends_on_positions: [1] },
6945
+ { position: 3, title_pattern: "Build {name} MCP server with standard tools", priority: "high", depends_on_positions: [2] },
6946
+ { position: 4, title_pattern: "Build {name} CLI with Commander.js", priority: "high", depends_on_positions: [2] },
6947
+ { position: 5, title_pattern: "Build {name} REST API", priority: "medium", depends_on_positions: [2] },
6948
+ { position: 6, title_pattern: "Write unit tests for {name}", priority: "high", depends_on_positions: [2, 3, 4] },
6949
+ { position: 7, title_pattern: "Add Apache 2.0 license and README", priority: "medium", depends_on_positions: [0] },
6950
+ { position: 8, title_pattern: "Create GitHub repo {org}/{name}", priority: "medium", depends_on_positions: [7] },
6951
+ { position: 9, title_pattern: "Add @hasna/cloud adapter", priority: "medium", depends_on_positions: [1] },
6952
+ { position: 10, title_pattern: "Write PostgreSQL migrations for {name}", priority: "medium", depends_on_positions: [1] },
6953
+ { position: 11, title_pattern: "Add feedback system + send_feedback MCP tool", priority: "medium", depends_on_positions: [3] },
6954
+ { position: 12, title_pattern: "Add agent tools (register_agent, heartbeat, set_focus, list_agents)", priority: "medium", depends_on_positions: [3] },
6955
+ { position: 13, title_pattern: "Create RDS database for {name}", priority: "low", depends_on_positions: [10] },
6956
+ { position: 14, title_pattern: "Publish @hasna/{name} to npm", priority: "high", depends_on_positions: [6, 7, 8] },
6957
+ { position: 15, title_pattern: "Install @hasna/{name} globally and verify", priority: "medium", depends_on_positions: [14] }
6958
+ ]
6959
+ },
6960
+ {
6961
+ name: "bug-fix",
6962
+ description: "Standard bug fix workflow",
6963
+ variables: [{ name: "bug", required: true, description: "Bug description" }],
6964
+ tasks: [
6965
+ { position: 0, title_pattern: "Reproduce: {bug}", priority: "critical" },
6966
+ { position: 1, title_pattern: "Diagnose root cause of {bug}", priority: "critical", depends_on_positions: [0] },
6967
+ { position: 2, title_pattern: "Implement fix for {bug}", priority: "critical", depends_on_positions: [1] },
6968
+ { position: 3, title_pattern: "Write regression test for {bug}", priority: "high", depends_on_positions: [2] },
6969
+ { position: 4, title_pattern: "Publish fix and verify in production", priority: "high", depends_on_positions: [3] }
6970
+ ]
6971
+ },
6972
+ {
6973
+ name: "feature",
6974
+ description: "Standard feature development workflow",
6975
+ variables: [{ name: "feature", required: true }, { name: "scope", required: false, default: "medium" }],
6976
+ tasks: [
6977
+ { position: 0, title_pattern: "Write spec for {feature}", priority: "high" },
6978
+ { position: 1, title_pattern: "Design implementation approach for {feature}", priority: "high", depends_on_positions: [0] },
6979
+ { position: 2, title_pattern: "Implement {feature}", priority: "critical", depends_on_positions: [1] },
6980
+ { position: 3, title_pattern: "Write tests for {feature}", priority: "high", depends_on_positions: [2] },
6981
+ { position: 4, title_pattern: "Code review for {feature}", priority: "medium", depends_on_positions: [3] },
6982
+ { position: 5, title_pattern: "Update docs for {feature}", priority: "medium", depends_on_positions: [2] },
6983
+ { position: 6, title_pattern: "Deploy {feature}", priority: "high", depends_on_positions: [4] }
6984
+ ]
6985
+ },
6986
+ {
6987
+ name: "security-audit",
6988
+ description: "Security audit workflow",
6989
+ variables: [{ name: "target", required: true }],
6990
+ tasks: [
6991
+ { position: 0, title_pattern: "Scan {target} for vulnerabilities", priority: "critical" },
6992
+ { position: 1, title_pattern: "Review {target} security findings", priority: "critical", depends_on_positions: [0] },
6993
+ { position: 2, title_pattern: "Fix critical issues in {target}", priority: "critical", depends_on_positions: [1] },
6994
+ { position: 3, title_pattern: "Retest {target} after fixes", priority: "high", depends_on_positions: [2] },
6995
+ { position: 4, title_pattern: "Write security report for {target}", priority: "medium", depends_on_positions: [3] },
6996
+ { position: 5, title_pattern: "Close audit for {target}", priority: "low", depends_on_positions: [4] }
6997
+ ]
6998
+ }
6999
+ ];
7000
+ });
7001
+
6553
7002
  // src/lib/extract.ts
6554
7003
  var exports_extract = {};
6555
7004
  __export(exports_extract, {
@@ -23102,6 +23551,26 @@ var init_pg_migrations = __esm(() => {
23102
23551
  CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
23103
23552
 
23104
23553
  INSERT INTO _migrations (id) VALUES (37) ON CONFLICT DO NOTHING;
23554
+ `,
23555
+ `
23556
+ ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS variables TEXT DEFAULT '[]';
23557
+ INSERT INTO _migrations (id) VALUES (38) ON CONFLICT DO NOTHING;
23558
+ `,
23559
+ `
23560
+ ALTER TABLE template_tasks ADD COLUMN IF NOT EXISTS condition TEXT;
23561
+ ALTER TABLE template_tasks ADD COLUMN IF NOT EXISTS include_template_id TEXT;
23562
+ ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1;
23563
+
23564
+ CREATE TABLE IF NOT EXISTS template_versions (
23565
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
23566
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
23567
+ version INTEGER NOT NULL,
23568
+ snapshot TEXT NOT NULL,
23569
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
23570
+ );
23571
+ CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id);
23572
+
23573
+ INSERT INTO _migrations (id) VALUES (39) ON CONFLICT DO NOTHING;
23105
23574
  `
23106
23575
  ];
23107
23576
  });
@@ -23361,6 +23830,11 @@ var init_mcp = __esm(() => {
23361
23830
  "create_task_from_template",
23362
23831
  "delete_template",
23363
23832
  "update_template",
23833
+ "init_templates",
23834
+ "preview_template",
23835
+ "export_template",
23836
+ "import_template",
23837
+ "template_history",
23364
23838
  "approve_task"
23365
23839
  ]);
23366
23840
  agentFocusMap = new Map;
@@ -24954,15 +25428,20 @@ ${data.chart}` }] };
24954
25428
  });
24955
25429
  }
24956
25430
  if (shouldRegisterTool("create_webhook")) {
24957
- server.tool("create_webhook", "Register a webhook for task change events.", {
25431
+ server.tool("create_webhook", "Register a webhook for task change events. Optionally scope to a project, task list, agent, or specific task.", {
24958
25432
  url: exports_external.string(),
24959
- events: exports_external.array(exports_external.string()).optional(),
24960
- secret: exports_external.string().optional()
25433
+ 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"),
25434
+ secret: exports_external.string().optional().describe("HMAC secret for signing webhook payloads"),
25435
+ project_id: exports_external.string().optional().describe("Only fire for events in this project"),
25436
+ task_list_id: exports_external.string().optional().describe("Only fire for events in this task list"),
25437
+ agent_id: exports_external.string().optional().describe("Only fire for events involving this agent"),
25438
+ task_id: exports_external.string().optional().describe("Only fire for events on this specific task")
24961
25439
  }, async (params) => {
24962
25440
  try {
24963
25441
  const { createWebhook: createWebhook2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
24964
25442
  const wh = createWebhook2(params);
24965
- return { content: [{ type: "text", text: `Webhook created: ${wh.id.slice(0, 8)} | ${wh.url} | events: ${wh.events.length === 0 ? "all" : wh.events.join(",")}` }] };
25443
+ 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(", ");
25444
+ 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}` : ""}` }] };
24966
25445
  } catch (e) {
24967
25446
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
24968
25447
  }
@@ -24998,7 +25477,7 @@ ${text}` }] };
24998
25477
  });
24999
25478
  }
25000
25479
  if (shouldRegisterTool("create_template")) {
25001
- 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).", {
25480
+ 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.", {
25002
25481
  name: exports_external.string(),
25003
25482
  title_pattern: exports_external.string(),
25004
25483
  description: exports_external.string().optional(),
@@ -25006,6 +25485,12 @@ ${text}` }] };
25006
25485
  tags: exports_external.array(exports_external.string()).optional(),
25007
25486
  project_id: exports_external.string().optional(),
25008
25487
  plan_id: exports_external.string().optional(),
25488
+ variables: exports_external.array(exports_external.object({
25489
+ name: exports_external.string().describe("Variable name (used as {name} in patterns)"),
25490
+ required: exports_external.boolean().describe("Whether this variable must be provided"),
25491
+ default: exports_external.string().optional().describe("Default value if not provided"),
25492
+ description: exports_external.string().optional().describe("Help text for the variable")
25493
+ })).optional().describe("Typed variable definitions with defaults and required flags"),
25009
25494
  tasks: exports_external.array(exports_external.object({
25010
25495
  title_pattern: exports_external.string().describe("Title pattern with optional {variable} placeholders"),
25011
25496
  description: exports_external.string().optional(),
@@ -25035,7 +25520,10 @@ ${text}` }] };
25035
25520
  const templates = listTemplates2();
25036
25521
  if (templates.length === 0)
25037
25522
  return { content: [{ type: "text", text: "No templates." }] };
25038
- const text = templates.map((t) => `${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}" | ${t.priority}`).join(`
25523
+ const text = templates.map((t) => {
25524
+ const vars = t.variables.length > 0 ? ` | vars: ${t.variables.map((v) => `${v.name}${v.required ? "*" : ""}${v.default ? `=${v.default}` : ""}`).join(", ")}` : "";
25525
+ return `${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}" | ${t.priority}${vars}`;
25526
+ }).join(`
25039
25527
  `);
25040
25528
  return { content: [{ type: "text", text: `${templates.length} template(s):
25041
25529
  ${text}` }] };
@@ -25060,7 +25548,8 @@ ${text}` }] };
25060
25548
  const resolvedTemplateId = resolveId(params.template_id, "task_templates");
25061
25549
  const templateWithTasks = getTemplateWithTasks2(resolvedTemplateId);
25062
25550
  if (templateWithTasks && templateWithTasks.tasks.length > 0) {
25063
- const tasks = tasksFromTemplate2(resolvedTemplateId, params.project_id || templateWithTasks.project_id || "", params.variables, params.task_list_id);
25551
+ const effectiveProjectId = params.project_id || templateWithTasks.project_id || undefined;
25552
+ const tasks = tasksFromTemplate2(resolvedTemplateId, effectiveProjectId, params.variables, params.task_list_id);
25064
25553
  const text = tasks.map((t) => `${t.id.slice(0, 8)} | ${t.priority} | ${t.title}`).join(`
25065
25554
  `);
25066
25555
  return { content: [{ type: "text", text: `${tasks.length} task(s) created from template:
@@ -25117,6 +25606,93 @@ ${task.id.slice(0, 8)} | ${task.priority} | ${task.title}` }] };
25117
25606
  }
25118
25607
  });
25119
25608
  }
25609
+ if (shouldRegisterTool("init_templates")) {
25610
+ 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 () => {
25611
+ try {
25612
+ const { initBuiltinTemplates: initBuiltinTemplates2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
25613
+ const result = initBuiltinTemplates2();
25614
+ if (result.created === 0) {
25615
+ return { content: [{ type: "text", text: `All ${result.skipped} built-in template(s) already exist.` }] };
25616
+ }
25617
+ return { content: [{ type: "text", text: `Created ${result.created} template(s): ${result.names.join(", ")}. Skipped ${result.skipped} existing.` }] };
25618
+ } catch (e) {
25619
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
25620
+ }
25621
+ });
25622
+ }
25623
+ if (shouldRegisterTool("preview_template")) {
25624
+ server.tool("preview_template", "Preview a template without creating tasks. Shows resolved titles (variables substituted), dependencies, and priorities.", {
25625
+ template_id: exports_external.string(),
25626
+ variables: exports_external.record(exports_external.string()).optional().describe("Variable substitution map for {name} placeholders")
25627
+ }, async (params) => {
25628
+ try {
25629
+ const { previewTemplate: previewTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
25630
+ const resolvedId = resolveId(params.template_id, "task_templates");
25631
+ const preview = previewTemplate2(resolvedId, params.variables);
25632
+ const lines = preview.tasks.map((t) => {
25633
+ const deps = t.depends_on_positions.length > 0 ? ` (after: ${t.depends_on_positions.join(", ")})` : "";
25634
+ return ` [${t.position}] ${t.priority} | ${t.title}${deps}`;
25635
+ });
25636
+ const varsInfo = preview.variables.length > 0 ? `
25637
+ Variables: ${preview.variables.map((v) => `${v.name}${v.required ? "*" : ""}${v.default ? `=${v.default}` : ""}`).join(", ")}` : "";
25638
+ const resolvedVars = Object.keys(preview.resolved_variables).length > 0 ? `
25639
+ Resolved: ${Object.entries(preview.resolved_variables).map(([k, v]) => `${k}=${v}`).join(", ")}` : "";
25640
+ return { content: [{ type: "text", text: `Preview: ${preview.template_name} (${preview.tasks.length} tasks)${varsInfo}${resolvedVars}
25641
+ ${lines.join(`
25642
+ `)}` }] };
25643
+ } catch (e) {
25644
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
25645
+ }
25646
+ });
25647
+ }
25648
+ if (shouldRegisterTool("export_template")) {
25649
+ 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 }) => {
25650
+ try {
25651
+ const { exportTemplate: exportTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
25652
+ const resolvedId = resolveId(template_id, "task_templates");
25653
+ const json = exportTemplate2(resolvedId);
25654
+ return { content: [{ type: "text", text: JSON.stringify(json, null, 2) }] };
25655
+ } catch (e) {
25656
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
25657
+ }
25658
+ });
25659
+ }
25660
+ if (shouldRegisterTool("import_template")) {
25661
+ 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 }) => {
25662
+ try {
25663
+ const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
25664
+ const parsed = JSON.parse(json);
25665
+ const t = importTemplate2(parsed);
25666
+ return { content: [{ type: "text", text: `Template imported: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}"` }] };
25667
+ } catch (e) {
25668
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
25669
+ }
25670
+ });
25671
+ }
25672
+ if (shouldRegisterTool("template_history")) {
25673
+ 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 }) => {
25674
+ try {
25675
+ const { listTemplateVersions: listTemplateVersions2, getTemplate: getTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
25676
+ const resolvedId = resolveId(template_id, "task_templates");
25677
+ const template = getTemplate2(resolvedId);
25678
+ if (!template)
25679
+ return { content: [{ type: "text", text: `Template not found: ${template_id}` }], isError: true };
25680
+ const versions = listTemplateVersions2(resolvedId);
25681
+ if (versions.length === 0) {
25682
+ return { content: [{ type: "text", text: `${template.name} v${template.version} \u2014 no previous versions.` }] };
25683
+ }
25684
+ const lines = versions.map((v) => {
25685
+ const snap = JSON.parse(v.snapshot);
25686
+ return `v${v.version} | ${v.created_at} | ${snap.name} | "${snap.title_pattern}"`;
25687
+ });
25688
+ return { content: [{ type: "text", text: `${template.name} \u2014 current: v${template.version}
25689
+ ${lines.join(`
25690
+ `)}` }] };
25691
+ } catch (e) {
25692
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
25693
+ }
25694
+ });
25695
+ }
25120
25696
  if (shouldRegisterTool("approve_task")) {
25121
25697
  server.tool("approve_task", "Approve a task with requires_approval=true.", {
25122
25698
  id: exports_external.string(),
@@ -26234,6 +26810,11 @@ ${stack_trace.slice(0, 1500)}
26234
26810
  "create_task_from_template",
26235
26811
  "delete_template",
26236
26812
  "update_template",
26813
+ "init_templates",
26814
+ "preview_template",
26815
+ "export_template",
26816
+ "import_template",
26817
+ "template_history",
26237
26818
  "bulk_update_tasks",
26238
26819
  "bulk_create_tasks",
26239
26820
  "get_task_stats",
@@ -26473,8 +27054,21 @@ ${stack_trace.slice(0, 1500)}
26473
27054
  Params: id(string, req)
26474
27055
  Example: {id: 'a1b2c3d4'}`,
26475
27056
  update_template: `Update a task template's name, title pattern, or other fields.
26476
- Params: id(string, req), name(string), title_pattern(string), description(string), priority(low|medium|high|critical), tags(string[]), project_id(string), plan_id(string)
27057
+ 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)
26477
27058
  Example: {id: 'a1b2c3d4', name: 'Renamed Template', priority: 'critical'}`,
27059
+ init_templates: "Initialize built-in starter templates (open-source-project, bug-fix, feature, security-audit). Skips already existing. No params.",
27060
+ preview_template: `Preview a template without creating tasks. Shows resolved titles, deps, priorities.
27061
+ Params: template_id(string, req), variables(Record<string,string>)
27062
+ Example: {template_id: 'a1b2c3d4', variables: {name: 'invoices'}}`,
27063
+ export_template: `Export a template as JSON (template + tasks + variables). Use for sharing or backup.
27064
+ Params: template_id(string, req)
27065
+ Example: {template_id: 'a1b2c3d4'}`,
27066
+ import_template: `Import a template from a JSON string (as returned by export_template). Creates new template with new IDs.
27067
+ Params: json(string, req)
27068
+ Example: {json: '{"name":"My Template",...}'}`,
27069
+ template_history: `Show version history of a template. Each update creates a snapshot of the previous state.
27070
+ Params: template_id(string, req)
27071
+ Example: {template_id: 'a1b2c3d4'}`,
26478
27072
  get_active_work: `See all in-progress tasks and who is working on them.
26479
27073
  Params: project_id(string, optional), task_list_id(string, optional)
26480
27074
  Example: {project_id: 'a1b2c3d4'}`,
@@ -27494,6 +28088,8 @@ async function startServer(port, options) {
27494
28088
  continue;
27495
28089
  if (client.agentId && event.agent_id !== client.agentId)
27496
28090
  continue;
28091
+ if (client.projectId && event.project_id !== client.projectId)
28092
+ continue;
27497
28093
  try {
27498
28094
  client.controller.enqueue(`event: ${eventName}
27499
28095
  data: ${data}
@@ -27532,6 +28128,31 @@ Dashboard not found at: ${dashboardDir}`);
27532
28128
  });
27533
28129
  }
27534
28130
  if (path === "/api/events" && method === "GET") {
28131
+ const agentId = url.searchParams.get("agent_id") || undefined;
28132
+ const projectId = url.searchParams.get("project_id") || undefined;
28133
+ if (agentId || projectId) {
28134
+ const client = { controller: null, agentId, projectId, events: undefined };
28135
+ const stream2 = new ReadableStream({
28136
+ start(controller) {
28137
+ client.controller = controller;
28138
+ filteredSseClients.add(client);
28139
+ controller.enqueue(`data: ${JSON.stringify({ type: "connected", agent_id: agentId, project_id: projectId, timestamp: new Date().toISOString() })}
28140
+
28141
+ `);
28142
+ },
28143
+ cancel() {
28144
+ filteredSseClients.delete(client);
28145
+ }
28146
+ });
28147
+ return new Response(stream2, {
28148
+ headers: {
28149
+ "Content-Type": "text/event-stream",
28150
+ "Cache-Control": "no-cache",
28151
+ Connection: "keep-alive",
28152
+ "Access-Control-Allow-Origin": "*"
28153
+ }
28154
+ });
28155
+ }
27535
28156
  const stream = new ReadableStream({
27536
28157
  start(controller) {
27537
28158
  sseClients.add(controller);
@@ -27640,7 +28261,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
27640
28261
  priority: body.priority,
27641
28262
  project_id: body.project_id
27642
28263
  });
27643
- broadcastEvent({ type: "task", task_id: task.id, action: "created", agent_id: task.agent_id });
28264
+ broadcastEvent({ type: "task", task_id: task.id, action: "created", agent_id: task.agent_id, project_id: task.project_id });
27644
28265
  return json(taskToSummary(task), 201, port);
27645
28266
  } catch (e) {
27646
28267
  return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
@@ -27861,7 +28482,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
27861
28482
  const id = startMatch[1];
27862
28483
  try {
27863
28484
  const task = startTask(id, "dashboard");
27864
- broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard" });
28485
+ broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard", project_id: task.project_id });
27865
28486
  return json(taskToSummary(task), 200, port);
27866
28487
  } catch (e) {
27867
28488
  return json({ error: e instanceof Error ? e.message : "Failed to start task" }, 500, port);
@@ -27874,7 +28495,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
27874
28495
  const body = await req.json().catch(() => ({}));
27875
28496
  const { failTask: failTask2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
27876
28497
  const result = failTask2(id, body.agent_id, body.reason, { retry: body.retry, error_code: body.error_code });
27877
- broadcastEvent({ type: "task", task_id: id, action: "failed", agent_id: body.agent_id || null });
28498
+ broadcastEvent({ type: "task", task_id: id, action: "failed", agent_id: body.agent_id || null, project_id: result.task.project_id });
27878
28499
  return json({ task: taskToSummary(result.task), retry_task: result.retryTask ? taskToSummary(result.retryTask) : null }, 200, port);
27879
28500
  } catch (e) {
27880
28501
  return json({ error: e instanceof Error ? e.message : "Failed to fail task" }, 500, port);
@@ -27885,7 +28506,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
27885
28506
  const id = completeMatch[1];
27886
28507
  try {
27887
28508
  const task = completeTask(id, "dashboard");
27888
- broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard" });
28509
+ broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard", project_id: task.project_id });
27889
28510
  return json(taskToSummary(task), 200, port);
27890
28511
  } catch (e) {
27891
28512
  return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500, port);
@@ -30886,7 +31507,117 @@ program2.command("templates").description("List and manage task templates").opti
30886
31507
  console.log(chalk2.bold(`${templates.length} template(s):
30887
31508
  `));
30888
31509
  for (const t of templates) {
30889
- console.log(` ${chalk2.dim(t.id.slice(0, 8))} ${chalk2.bold(t.name)} ${chalk2.cyan(`"${t.title_pattern}"`)} ${chalk2.yellow(t.priority)}`);
31510
+ const vars = t.variables && t.variables.length > 0 ? ` ${chalk2.dim(`(${t.variables.map((v) => `${v.name}${v.required ? "*" : ""}${v.default ? `=${v.default}` : ""}`).join(", ")})`)}` : "";
31511
+ console.log(` ${chalk2.dim(t.id.slice(0, 8))} ${chalk2.bold(t.name)} ${chalk2.cyan(`"${t.title_pattern}"`)} ${chalk2.yellow(t.priority)}${vars}`);
31512
+ }
31513
+ });
31514
+ program2.command("template-init").alias("templates-init").description("Initialize built-in starter templates (open-source-project, bug-fix, feature, security-audit)").action(() => {
31515
+ const globalOpts = program2.opts();
31516
+ const { initBuiltinTemplates: initBuiltinTemplates2 } = (init_builtin_templates(), __toCommonJS(exports_builtin_templates));
31517
+ const result = initBuiltinTemplates2();
31518
+ if (globalOpts.json) {
31519
+ output(result, true);
31520
+ return;
31521
+ }
31522
+ if (result.created === 0) {
31523
+ console.log(chalk2.dim(`All ${result.skipped} built-in template(s) already exist.`));
31524
+ } else {
31525
+ console.log(chalk2.green(`Created ${result.created} template(s): ${result.names.join(", ")}. Skipped ${result.skipped} existing.`));
31526
+ }
31527
+ });
31528
+ program2.command("template-preview <id>").alias("templates-preview").description("Preview a template without creating tasks \u2014 shows resolved titles, deps, and priorities").option("--var <vars...>", "Variable substitution in key=value format (e.g. --var name=invoices)").action((id, opts) => {
31529
+ const globalOpts = program2.opts();
31530
+ const { previewTemplate: previewTemplate2 } = (init_templates(), __toCommonJS(exports_templates));
31531
+ const variables = {};
31532
+ if (opts.var) {
31533
+ for (const v of opts.var) {
31534
+ const eq = v.indexOf("=");
31535
+ if (eq === -1) {
31536
+ console.error(chalk2.red(`Invalid variable format: ${v} (expected key=value)`));
31537
+ process.exit(1);
31538
+ }
31539
+ variables[v.slice(0, eq)] = v.slice(eq + 1);
31540
+ }
31541
+ }
31542
+ try {
31543
+ const preview = previewTemplate2(id, Object.keys(variables).length > 0 ? variables : undefined);
31544
+ if (globalOpts.json) {
31545
+ output(preview, true);
31546
+ return;
31547
+ }
31548
+ console.log(chalk2.bold(`Preview: ${preview.template_name} (${preview.tasks.length} tasks)`));
31549
+ if (preview.description)
31550
+ console.log(chalk2.dim(` ${preview.description}`));
31551
+ if (preview.variables.length > 0) {
31552
+ console.log(chalk2.dim(` Variables: ${preview.variables.map((v) => `${v.name}${v.required ? "*" : ""}${v.default ? `=${v.default}` : ""}`).join(", ")}`));
31553
+ }
31554
+ if (Object.keys(preview.resolved_variables).length > 0) {
31555
+ console.log(chalk2.dim(` Resolved: ${Object.entries(preview.resolved_variables).map(([k, v]) => `${k}=${v}`).join(", ")}`));
31556
+ }
31557
+ console.log();
31558
+ for (const t of preview.tasks) {
31559
+ const deps = t.depends_on_positions.length > 0 ? chalk2.dim(` (after: ${t.depends_on_positions.join(", ")})`) : "";
31560
+ console.log(` ${chalk2.dim(`[${t.position}]`)} ${chalk2.yellow(t.priority)} | ${t.title}${deps}`);
31561
+ }
31562
+ } catch (e) {
31563
+ handleError(e);
31564
+ }
31565
+ });
31566
+ program2.command("template-export <id>").alias("templates-export").description("Export a template as JSON to stdout").action((id) => {
31567
+ const { exportTemplate: exportTemplate2 } = (init_templates(), __toCommonJS(exports_templates));
31568
+ try {
31569
+ const json2 = exportTemplate2(id);
31570
+ console.log(JSON.stringify(json2, null, 2));
31571
+ } catch (e) {
31572
+ handleError(e);
31573
+ }
31574
+ });
31575
+ program2.command("template-import").alias("templates-import").description("Import a template from a JSON file").option("--file <path>", "Path to template JSON file").action((opts) => {
31576
+ const globalOpts = program2.opts();
31577
+ const { importTemplate: importTemplate2 } = (init_templates(), __toCommonJS(exports_templates));
31578
+ const { readFileSync: readFileSync8 } = __require("fs");
31579
+ try {
31580
+ if (!opts.file) {
31581
+ console.error(chalk2.red("--file is required"));
31582
+ process.exit(1);
31583
+ }
31584
+ const content = readFileSync8(opts.file, "utf-8");
31585
+ const json2 = JSON.parse(content);
31586
+ const template = importTemplate2(json2);
31587
+ if (globalOpts.json) {
31588
+ output(template, true);
31589
+ } else {
31590
+ console.log(chalk2.green(`Template imported: ${template.id.slice(0, 8)} | ${template.name} | "${template.title_pattern}"`));
31591
+ }
31592
+ } catch (e) {
31593
+ handleError(e);
31594
+ }
31595
+ });
31596
+ program2.command("template-history <id>").alias("templates-history").description("Show version history of a template").action((id) => {
31597
+ const globalOpts = program2.opts();
31598
+ const { listTemplateVersions: listTemplateVersions2, getTemplate: getTemplate2 } = (init_templates(), __toCommonJS(exports_templates));
31599
+ try {
31600
+ const template = getTemplate2(id);
31601
+ if (!template) {
31602
+ console.error(chalk2.red("Template not found."));
31603
+ process.exit(1);
31604
+ }
31605
+ const versions = listTemplateVersions2(id);
31606
+ if (globalOpts.json) {
31607
+ output({ current_version: template.version, versions }, true);
31608
+ return;
31609
+ }
31610
+ console.log(chalk2.bold(`${template.name} \u2014 current version: ${template.version}`));
31611
+ if (versions.length === 0) {
31612
+ console.log(chalk2.dim(" No previous versions."));
31613
+ } else {
31614
+ for (const v of versions) {
31615
+ const snap = JSON.parse(v.snapshot);
31616
+ console.log(` ${chalk2.dim(`v${v.version}`)} | ${v.created_at} | ${snap.name} | "${snap.title_pattern}"`);
31617
+ }
31618
+ }
31619
+ } catch (e) {
31620
+ handleError(e);
30890
31621
  }
30891
31622
  });
30892
31623
  program2.command("comment <id> <text>").description("Add a comment to a task").action((id, text) => {