@hasna/todos 0.11.9 → 0.11.12

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
@@ -2290,6 +2290,20 @@ function ensureSchema(db) {
2290
2290
  metadata TEXT DEFAULT '{}',
2291
2291
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
2292
2292
  )`);
2293
+ ensureTable("template_tasks", `
2294
+ CREATE TABLE template_tasks (
2295
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
2296
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
2297
+ position INTEGER NOT NULL,
2298
+ title_pattern TEXT NOT NULL,
2299
+ description TEXT,
2300
+ priority TEXT DEFAULT 'medium',
2301
+ tags TEXT DEFAULT '[]',
2302
+ task_type TEXT,
2303
+ depends_on_positions TEXT DEFAULT '[]',
2304
+ metadata TEXT DEFAULT '{}',
2305
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2306
+ )`);
2293
2307
  ensureTable("task_checklists", `
2294
2308
  CREATE TABLE task_checklists (
2295
2309
  id TEXT PRIMARY KEY,
@@ -2374,6 +2388,36 @@ function ensureSchema(db) {
2374
2388
  ensureColumn("projects", "org_id", "TEXT");
2375
2389
  ensureColumn("plans", "task_list_id", "TEXT");
2376
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)");
2377
2421
  ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
2378
2422
  ensureColumn("task_comments", "progress_pct", "INTEGER");
2379
2423
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
@@ -2400,6 +2444,7 @@ function ensureSchema(db) {
2400
2444
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id)");
2401
2445
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id)");
2402
2446
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type)");
2447
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id)");
2403
2448
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
2404
2449
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
2405
2450
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
@@ -3043,6 +3088,44 @@ var init_database = __esm(() => {
3043
3088
  );
3044
3089
 
3045
3090
  INSERT OR IGNORE INTO _migrations (id) VALUES (36);
3091
+ `,
3092
+ `
3093
+ CREATE TABLE IF NOT EXISTS template_tasks (
3094
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
3095
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
3096
+ position INTEGER NOT NULL,
3097
+ title_pattern TEXT NOT NULL,
3098
+ description TEXT,
3099
+ priority TEXT DEFAULT 'medium',
3100
+ tags TEXT DEFAULT '[]',
3101
+ task_type TEXT,
3102
+ depends_on_positions TEXT DEFAULT '[]',
3103
+ metadata TEXT DEFAULT '{}',
3104
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
3105
+ );
3106
+ CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
3107
+
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);
3046
3129
  `
3047
3130
  ];
3048
3131
  });
@@ -3680,18 +3763,37 @@ var init_recurrence = __esm(() => {
3680
3763
  var exports_webhooks = {};
3681
3764
  __export(exports_webhooks, {
3682
3765
  listWebhooks: () => listWebhooks,
3766
+ listDeliveries: () => listDeliveries,
3683
3767
  getWebhook: () => getWebhook,
3684
3768
  dispatchWebhook: () => dispatchWebhook,
3685
3769
  deleteWebhook: () => deleteWebhook,
3686
3770
  createWebhook: () => createWebhook
3687
3771
  });
3688
3772
  function rowToWebhook(row) {
3689
- 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
+ };
3690
3782
  }
3691
3783
  function createWebhook(input, db) {
3692
3784
  const d = db || getDatabase();
3693
3785
  const id = uuid();
3694
- 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
+ ]);
3695
3797
  return getWebhook(id, d);
3696
3798
  }
3697
3799
  function getWebhook(id, db) {
@@ -3707,22 +3809,69 @@ function deleteWebhook(id, db) {
3707
3809
  const d = db || getDatabase();
3708
3810
  return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
3709
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
+ }
3710
3863
  async function dispatchWebhook(event, payload, db) {
3711
- 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 : {};
3712
3867
  for (const wh of webhooks) {
3713
- try {
3714
- const body = JSON.stringify({ event, payload, timestamp: now() });
3715
- const headers = { "Content-Type": "application/json" };
3716
- if (wh.secret) {
3717
- const encoder = new TextEncoder;
3718
- const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
3719
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
3720
- headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
3721
- }
3722
- fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
3723
- } 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(() => {});
3724
3872
  }
3725
3873
  }
3874
+ var MAX_RETRY_ATTEMPTS = 3, RETRY_BASE_DELAY_MS = 1000;
3726
3875
  var init_webhooks = __esm(() => {
3727
3876
  init_database();
3728
3877
  });
@@ -3731,18 +3880,42 @@ var init_webhooks = __esm(() => {
3731
3880
  var exports_templates = {};
3732
3881
  __export(exports_templates, {
3733
3882
  updateTemplate: () => updateTemplate,
3883
+ tasksFromTemplate: () => tasksFromTemplate,
3734
3884
  taskFromTemplate: () => taskFromTemplate,
3885
+ resolveVariables: () => resolveVariables,
3886
+ previewTemplate: () => previewTemplate,
3735
3887
  listTemplates: () => listTemplates,
3888
+ listTemplateVersions: () => listTemplateVersions,
3889
+ importTemplate: () => importTemplate,
3890
+ getTemplateWithTasks: () => getTemplateWithTasks,
3891
+ getTemplateVersion: () => getTemplateVersion,
3892
+ getTemplateTasks: () => getTemplateTasks,
3736
3893
  getTemplate: () => getTemplate,
3894
+ exportTemplate: () => exportTemplate,
3895
+ evaluateCondition: () => evaluateCondition,
3737
3896
  deleteTemplate: () => deleteTemplate,
3738
- createTemplate: () => createTemplate
3897
+ createTemplate: () => createTemplate,
3898
+ addTemplateTasks: () => addTemplateTasks
3739
3899
  });
3740
3900
  function rowToTemplate(row) {
3741
3901
  return {
3742
3902
  ...row,
3743
3903
  tags: JSON.parse(row.tags || "[]"),
3904
+ variables: JSON.parse(row.variables || "[]"),
3905
+ metadata: JSON.parse(row.metadata || "{}"),
3906
+ priority: row.priority || "medium",
3907
+ version: row.version ?? 1
3908
+ };
3909
+ }
3910
+ function rowToTemplateTask(row) {
3911
+ return {
3912
+ ...row,
3913
+ tags: JSON.parse(row.tags || "[]"),
3914
+ depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
3744
3915
  metadata: JSON.parse(row.metadata || "{}"),
3745
- priority: row.priority || "medium"
3916
+ priority: row.priority || "medium",
3917
+ condition: row.condition ?? null,
3918
+ include_template_id: row.include_template_id ?? null
3746
3919
  };
3747
3920
  }
3748
3921
  function resolveTemplateId(id, d) {
@@ -3751,19 +3924,23 @@ function resolveTemplateId(id, d) {
3751
3924
  function createTemplate(input, db) {
3752
3925
  const d = db || getDatabase();
3753
3926
  const id = uuid();
3754
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
3755
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3756
3929
  id,
3757
3930
  input.name,
3758
3931
  input.title_pattern,
3759
3932
  input.description || null,
3760
3933
  input.priority || "medium",
3761
3934
  JSON.stringify(input.tags || []),
3935
+ JSON.stringify(input.variables || []),
3762
3936
  input.project_id || null,
3763
3937
  input.plan_id || null,
3764
3938
  JSON.stringify(input.metadata || {}),
3765
3939
  now()
3766
3940
  ]);
3941
+ if (input.tasks && input.tasks.length > 0) {
3942
+ addTemplateTasks(id, input.tasks, d);
3943
+ }
3767
3944
  return getTemplate(id, d);
3768
3945
  }
3769
3946
  function getTemplate(id, db) {
@@ -3790,7 +3967,23 @@ function updateTemplate(id, updates, db) {
3790
3967
  const resolved = resolveTemplateId(id, d);
3791
3968
  if (!resolved)
3792
3969
  return null;
3793
- 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"];
3794
3987
  const values = [];
3795
3988
  if (updates.name !== undefined) {
3796
3989
  sets.push("name = ?");
@@ -3812,6 +4005,10 @@ function updateTemplate(id, updates, db) {
3812
4005
  sets.push("tags = ?");
3813
4006
  values.push(JSON.stringify(updates.tags));
3814
4007
  }
4008
+ if (updates.variables !== undefined) {
4009
+ sets.push("variables = ?");
4010
+ values.push(JSON.stringify(updates.variables));
4011
+ }
3815
4012
  if (updates.project_id !== undefined) {
3816
4013
  sets.push("project_id = ?");
3817
4014
  values.push(updates.project_id);
@@ -3824,8 +4021,6 @@ function updateTemplate(id, updates, db) {
3824
4021
  sets.push("metadata = ?");
3825
4022
  values.push(JSON.stringify(updates.metadata));
3826
4023
  }
3827
- if (sets.length === 0)
3828
- return getTemplate(resolved, d);
3829
4024
  values.push(resolved);
3830
4025
  d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
3831
4026
  return getTemplate(resolved, d);
@@ -3845,8 +4040,294 @@ function taskFromTemplate(templateId, overrides = {}, db) {
3845
4040
  ...overrides
3846
4041
  };
3847
4042
  }
4043
+ function addTemplateTasks(templateId, tasks, db) {
4044
+ const d = db || getDatabase();
4045
+ const template = getTemplate(templateId, d);
4046
+ if (!template)
4047
+ throw new Error(`Template not found: ${templateId}`);
4048
+ d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
4049
+ const results = [];
4050
+ for (let i = 0;i < tasks.length; i++) {
4051
+ const task = tasks[i];
4052
+ const id = uuid();
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
4055
+ id,
4056
+ templateId,
4057
+ i,
4058
+ task.title_pattern,
4059
+ task.description || null,
4060
+ task.priority || "medium",
4061
+ JSON.stringify(task.tags || []),
4062
+ task.task_type || null,
4063
+ task.condition || null,
4064
+ task.include_template_id || null,
4065
+ JSON.stringify(task.depends_on || []),
4066
+ JSON.stringify(task.metadata || {}),
4067
+ now()
4068
+ ]);
4069
+ const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
4070
+ if (row)
4071
+ results.push(rowToTemplateTask(row));
4072
+ }
4073
+ return results;
4074
+ }
4075
+ function getTemplateWithTasks(id, db) {
4076
+ const d = db || getDatabase();
4077
+ const template = getTemplate(id, d);
4078
+ if (!template)
4079
+ return null;
4080
+ const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
4081
+ const tasks = rows.map(rowToTemplateTask);
4082
+ return { ...template, tasks };
4083
+ }
4084
+ function getTemplateTasks(templateId, db) {
4085
+ const d = db || getDatabase();
4086
+ const resolved = resolveTemplateId(templateId, d);
4087
+ if (!resolved)
4088
+ return [];
4089
+ const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
4090
+ return rows.map(rowToTemplateTask);
4091
+ }
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) {
4218
+ const d = db || getDatabase();
4219
+ const template = getTemplateWithTasks(templateId, d);
4220
+ if (!template)
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);
4228
+ if (template.tasks.length === 0) {
4229
+ const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
4230
+ const task = createTask(input, d);
4231
+ return [task];
4232
+ }
4233
+ const createdTasks = [];
4234
+ const positionToId = new Map;
4235
+ const skippedPositions = new Set;
4236
+ for (const tt of template.tasks) {
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);
4244
+ }
4245
+ continue;
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);
4256
+ const task = createTask({
4257
+ title,
4258
+ description: desc ?? undefined,
4259
+ priority: tt.priority,
4260
+ tags: tt.tags,
4261
+ task_type: tt.task_type ?? undefined,
4262
+ project_id: projectId,
4263
+ task_list_id: taskListId,
4264
+ metadata: tt.metadata
4265
+ }, d);
4266
+ createdTasks.push(task);
4267
+ positionToId.set(tt.position, task.id);
4268
+ }
4269
+ for (const tt of template.tasks) {
4270
+ if (skippedPositions.has(tt.position))
4271
+ continue;
4272
+ if (tt.include_template_id)
4273
+ continue;
4274
+ const deps = tt.depends_on_positions;
4275
+ for (const depPos of deps) {
4276
+ if (skippedPositions.has(depPos))
4277
+ continue;
4278
+ const taskId = positionToId.get(tt.position);
4279
+ const depId = positionToId.get(depPos);
4280
+ if (taskId && depId) {
4281
+ addDependency(taskId, depId, d);
4282
+ }
4283
+ }
4284
+ }
4285
+ return createdTasks;
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
+ }
3848
4328
  var init_templates = __esm(() => {
3849
4329
  init_database();
4330
+ init_tasks();
3850
4331
  });
3851
4332
 
3852
4333
  // src/db/checklists.ts
@@ -6406,6 +6887,118 @@ var init_task_commits = __esm(() => {
6406
6887
  init_database();
6407
6888
  });
6408
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
+
6409
7002
  // src/lib/extract.ts
6410
7003
  var exports_extract = {};
6411
7004
  __export(exports_extract, {
@@ -22940,6 +23533,44 @@ var init_pg_migrations = __esm(() => {
22940
23533
  );
22941
23534
 
22942
23535
  INSERT INTO _migrations (id) VALUES (36) ON CONFLICT DO NOTHING;
23536
+ `,
23537
+ `
23538
+ CREATE TABLE IF NOT EXISTS template_tasks (
23539
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
23540
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
23541
+ position INTEGER NOT NULL,
23542
+ title_pattern TEXT NOT NULL,
23543
+ description TEXT,
23544
+ priority TEXT DEFAULT 'medium',
23545
+ tags TEXT DEFAULT '[]',
23546
+ task_type TEXT,
23547
+ depends_on_positions TEXT DEFAULT '[]',
23548
+ metadata TEXT DEFAULT '{}',
23549
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
23550
+ );
23551
+ CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
23552
+
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;
22943
23574
  `
22944
23575
  ];
22945
23576
  });
@@ -23199,6 +23830,11 @@ var init_mcp = __esm(() => {
23199
23830
  "create_task_from_template",
23200
23831
  "delete_template",
23201
23832
  "update_template",
23833
+ "init_templates",
23834
+ "preview_template",
23835
+ "export_template",
23836
+ "import_template",
23837
+ "template_history",
23202
23838
  "approve_task"
23203
23839
  ]);
23204
23840
  agentFocusMap = new Map;
@@ -24792,15 +25428,20 @@ ${data.chart}` }] };
24792
25428
  });
24793
25429
  }
24794
25430
  if (shouldRegisterTool("create_webhook")) {
24795
- 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.", {
24796
25432
  url: exports_external.string(),
24797
- events: exports_external.array(exports_external.string()).optional(),
24798
- 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")
24799
25439
  }, async (params) => {
24800
25440
  try {
24801
25441
  const { createWebhook: createWebhook2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
24802
25442
  const wh = createWebhook2(params);
24803
- 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}` : ""}` }] };
24804
25445
  } catch (e) {
24805
25446
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
24806
25447
  }
@@ -24836,19 +25477,37 @@ ${text}` }] };
24836
25477
  });
24837
25478
  }
24838
25479
  if (shouldRegisterTool("create_template")) {
24839
- server.tool("create_template", "Create a reusable task template.", {
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.", {
24840
25481
  name: exports_external.string(),
24841
25482
  title_pattern: exports_external.string(),
24842
25483
  description: exports_external.string().optional(),
24843
25484
  priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
24844
25485
  tags: exports_external.array(exports_external.string()).optional(),
24845
25486
  project_id: exports_external.string().optional(),
24846
- plan_id: exports_external.string().optional()
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"),
25494
+ tasks: exports_external.array(exports_external.object({
25495
+ title_pattern: exports_external.string().describe("Title pattern with optional {variable} placeholders"),
25496
+ description: exports_external.string().optional(),
25497
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
25498
+ tags: exports_external.array(exports_external.string()).optional(),
25499
+ task_type: exports_external.string().optional(),
25500
+ depends_on: exports_external.array(exports_external.number()).optional().describe("Position indices (0-based) of tasks this task depends on"),
25501
+ metadata: exports_external.record(exports_external.unknown()).optional()
25502
+ })).optional().describe("Multi-task template: ordered list of tasks to create together with dependencies")
24847
25503
  }, async (params) => {
24848
25504
  try {
24849
- const { createTemplate: createTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
25505
+ const { createTemplate: createTemplate2, getTemplateWithTasks: getTemplateWithTasks2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
24850
25506
  const t = createTemplate2(params);
24851
- return { content: [{ type: "text", text: `Template created: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}"` }] };
25507
+ const withTasks = getTemplateWithTasks2(t.id);
25508
+ const taskCount = withTasks?.tasks.length ?? 0;
25509
+ const taskInfo = taskCount > 0 ? ` | ${taskCount} task(s)` : "";
25510
+ return { content: [{ type: "text", text: `Template created: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}"${taskInfo}` }] };
24852
25511
  } catch (e) {
24853
25512
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
24854
25513
  }
@@ -24861,7 +25520,10 @@ ${text}` }] };
24861
25520
  const templates = listTemplates2();
24862
25521
  if (templates.length === 0)
24863
25522
  return { content: [{ type: "text", text: "No templates." }] };
24864
- 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(`
24865
25527
  `);
24866
25528
  return { content: [{ type: "text", text: `${templates.length} template(s):
24867
25529
  ${text}` }] };
@@ -24871,23 +25533,35 @@ ${text}` }] };
24871
25533
  });
24872
25534
  }
24873
25535
  if (shouldRegisterTool("create_task_from_template")) {
24874
- server.tool("create_task_from_template", "Create a task from a template with optional overrides.", {
25536
+ server.tool("create_task_from_template", "Create task(s) from a template. For multi-task templates, creates all tasks with dependencies wired. Supports {variable} substitution in titles/descriptions.", {
24875
25537
  template_id: exports_external.string(),
24876
- title: exports_external.string().optional(),
24877
- description: exports_external.string().optional(),
25538
+ title: exports_external.string().optional().describe("Override title (single-task templates only)"),
25539
+ description: exports_external.string().optional().describe("Override description (single-task templates only)"),
24878
25540
  priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
24879
25541
  assigned_to: exports_external.string().optional(),
24880
- project_id: exports_external.string().optional()
25542
+ project_id: exports_external.string().optional(),
25543
+ task_list_id: exports_external.string().optional(),
25544
+ variables: exports_external.record(exports_external.string()).optional().describe("Variable substitution map for {name} placeholders in multi-task templates")
24881
25545
  }, async (params) => {
24882
25546
  try {
24883
- const { taskFromTemplate: taskFromTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
25547
+ const { taskFromTemplate: taskFromTemplate2, getTemplateWithTasks: getTemplateWithTasks2, tasksFromTemplate: tasksFromTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
24884
25548
  const resolvedTemplateId = resolveId(params.template_id, "task_templates");
25549
+ const templateWithTasks = getTemplateWithTasks2(resolvedTemplateId);
25550
+ if (templateWithTasks && templateWithTasks.tasks.length > 0) {
25551
+ const effectiveProjectId = params.project_id || templateWithTasks.project_id || undefined;
25552
+ const tasks = tasksFromTemplate2(resolvedTemplateId, effectiveProjectId, params.variables, params.task_list_id);
25553
+ const text = tasks.map((t) => `${t.id.slice(0, 8)} | ${t.priority} | ${t.title}`).join(`
25554
+ `);
25555
+ return { content: [{ type: "text", text: `${tasks.length} task(s) created from template:
25556
+ ${text}` }] };
25557
+ }
24885
25558
  const input = taskFromTemplate2(resolvedTemplateId, {
24886
25559
  title: params.title,
24887
25560
  description: params.description,
24888
25561
  priority: params.priority,
24889
25562
  assigned_to: params.assigned_to,
24890
- project_id: params.project_id
25563
+ project_id: params.project_id,
25564
+ task_list_id: params.task_list_id
24891
25565
  });
24892
25566
  const task = createTask(input);
24893
25567
  return { content: [{ type: "text", text: `Task created from template:
@@ -24932,6 +25606,93 @@ ${task.id.slice(0, 8)} | ${task.priority} | ${task.title}` }] };
24932
25606
  }
24933
25607
  });
24934
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
+ }
24935
25696
  if (shouldRegisterTool("approve_task")) {
24936
25697
  server.tool("approve_task", "Approve a task with requires_approval=true.", {
24937
25698
  id: exports_external.string(),
@@ -26049,6 +26810,11 @@ ${stack_trace.slice(0, 1500)}
26049
26810
  "create_task_from_template",
26050
26811
  "delete_template",
26051
26812
  "update_template",
26813
+ "init_templates",
26814
+ "preview_template",
26815
+ "export_template",
26816
+ "import_template",
26817
+ "template_history",
26052
26818
  "bulk_update_tasks",
26053
26819
  "bulk_create_tasks",
26054
26820
  "get_task_stats",
@@ -26275,19 +27041,34 @@ ${stack_trace.slice(0, 1500)}
26275
27041
  delete_webhook: `Delete a webhook by ID.
26276
27042
  Params: id(string, req)
26277
27043
  Example: {id: 'a1b2c3d4'}`,
26278
- create_template: `Create a reusable task template.
26279
- Params: name(string, req), title_pattern(string, req \u2014 e.g. 'Fix: {description}'), description(string), priority(low|medium|high|critical), tags(string[]), project_id(string), plan_id(string)
26280
- Example: {name: 'Bug Report', title_pattern: 'Bug: {description}', priority: 'high', tags: ['bug']}`,
27044
+ create_template: `Create a reusable task template. Supports multi-task templates with dependencies.
27045
+ Params: name(string, req), title_pattern(string, req), description(string), priority(low|medium|high|critical), tags(string[]), project_id(string), plan_id(string), tasks(array of {title_pattern, description, priority, tags, task_type, depends_on(number[]), metadata})
27046
+ Example single: {name: 'Bug Report', title_pattern: 'Bug: {description}', priority: 'high', tags: ['bug']}
27047
+ Example multi: {name: 'Feature', title_pattern: 'Feature: {name}', tasks: [{title_pattern: 'Design {name}'}, {title_pattern: 'Implement {name}', depends_on: [0]}, {title_pattern: 'Test {name}', depends_on: [1]}]}`,
26281
27048
  list_templates: "List all task templates. No params.",
26282
- create_task_from_template: `Create a task from a template with optional overrides.
26283
- Params: template_id(string, req), title(string), description(string), priority(low|medium|high|critical), assigned_to(string), project_id(string)
26284
- Example: {template_id: 'a1b2c3d4', assigned_to: 'maximus'}`,
27049
+ create_task_from_template: `Create task(s) from a template. Multi-task templates create all tasks with dependencies wired. Supports {variable} substitution.
27050
+ Params: template_id(string, req), title(string \u2014 single-task override), description(string), priority(low|medium|high|critical), assigned_to(string), project_id(string), task_list_id(string), variables(Record<string,string> \u2014 {name} substitution)
27051
+ Example single: {template_id: 'a1b2c3d4', assigned_to: 'maximus'}
27052
+ Example multi: {template_id: 'a1b2c3d4', project_id: 'proj1', variables: {name: 'OAuth login'}}`,
26285
27053
  delete_template: `Delete a task template.
26286
27054
  Params: id(string, req)
26287
27055
  Example: {id: 'a1b2c3d4'}`,
26288
27056
  update_template: `Update a task template's name, title pattern, or other fields.
26289
- 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)
26290
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'}`,
26291
27072
  get_active_work: `See all in-progress tasks and who is working on them.
26292
27073
  Params: project_id(string, optional), task_list_id(string, optional)
26293
27074
  Example: {project_id: 'a1b2c3d4'}`,
@@ -27307,6 +28088,8 @@ async function startServer(port, options) {
27307
28088
  continue;
27308
28089
  if (client.agentId && event.agent_id !== client.agentId)
27309
28090
  continue;
28091
+ if (client.projectId && event.project_id !== client.projectId)
28092
+ continue;
27310
28093
  try {
27311
28094
  client.controller.enqueue(`event: ${eventName}
27312
28095
  data: ${data}
@@ -27345,6 +28128,31 @@ Dashboard not found at: ${dashboardDir}`);
27345
28128
  });
27346
28129
  }
27347
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
+ }
27348
28156
  const stream = new ReadableStream({
27349
28157
  start(controller) {
27350
28158
  sseClients.add(controller);
@@ -27453,7 +28261,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
27453
28261
  priority: body.priority,
27454
28262
  project_id: body.project_id
27455
28263
  });
27456
- 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 });
27457
28265
  return json(taskToSummary(task), 201, port);
27458
28266
  } catch (e) {
27459
28267
  return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
@@ -27674,7 +28482,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
27674
28482
  const id = startMatch[1];
27675
28483
  try {
27676
28484
  const task = startTask(id, "dashboard");
27677
- 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 });
27678
28486
  return json(taskToSummary(task), 200, port);
27679
28487
  } catch (e) {
27680
28488
  return json({ error: e instanceof Error ? e.message : "Failed to start task" }, 500, port);
@@ -27687,7 +28495,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
27687
28495
  const body = await req.json().catch(() => ({}));
27688
28496
  const { failTask: failTask2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
27689
28497
  const result = failTask2(id, body.agent_id, body.reason, { retry: body.retry, error_code: body.error_code });
27690
- 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 });
27691
28499
  return json({ task: taskToSummary(result.task), retry_task: result.retryTask ? taskToSummary(result.retryTask) : null }, 200, port);
27692
28500
  } catch (e) {
27693
28501
  return json({ error: e instanceof Error ? e.message : "Failed to fail task" }, 500, port);
@@ -27698,7 +28506,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
27698
28506
  const id = completeMatch[1];
27699
28507
  try {
27700
28508
  const task = completeTask(id, "dashboard");
27701
- 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 });
27702
28510
  return json(taskToSummary(task), 200, port);
27703
28511
  } catch (e) {
27704
28512
  return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500, port);
@@ -30699,7 +31507,117 @@ program2.command("templates").description("List and manage task templates").opti
30699
31507
  console.log(chalk2.bold(`${templates.length} template(s):
30700
31508
  `));
30701
31509
  for (const t of templates) {
30702
- 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);
30703
31621
  }
30704
31622
  });
30705
31623
  program2.command("comment <id> <text>").description("Add a comment to a task").action((id, text) => {