@hasna/todos 0.11.42 → 0.11.43

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
@@ -8908,9 +8908,83 @@ var init_plans = __esm(() => {
8908
8908
  // src/db/builtin-templates.ts
8909
8909
  var exports_builtin_templates = {};
8910
8910
  __export(exports_builtin_templates, {
8911
+ writeBuiltinTemplateFiles: () => writeBuiltinTemplateFiles,
8912
+ listBuiltinTemplates: () => listBuiltinTemplates,
8911
8913
  initBuiltinTemplates: () => initBuiltinTemplates,
8914
+ getBuiltinTemplate: () => getBuiltinTemplate,
8915
+ exportBuiltinTemplateFiles: () => exportBuiltinTemplateFiles,
8916
+ exportBuiltinTemplate: () => exportBuiltinTemplate,
8917
+ BUILTIN_TEMPLATE_LIBRARY_VERSION: () => BUILTIN_TEMPLATE_LIBRARY_VERSION,
8918
+ BUILTIN_TEMPLATE_LIBRARY_SOURCE: () => BUILTIN_TEMPLATE_LIBRARY_SOURCE,
8912
8919
  BUILTIN_TEMPLATES: () => BUILTIN_TEMPLATES
8913
8920
  });
8921
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
8922
+ import { join as join5 } from "path";
8923
+ function templateMetadata(template) {
8924
+ return {
8925
+ source: BUILTIN_TEMPLATE_LIBRARY_SOURCE,
8926
+ library_version: template.version,
8927
+ category: template.category,
8928
+ template_file: `${template.name}.json`,
8929
+ local_only: true,
8930
+ marketplace_free: true
8931
+ };
8932
+ }
8933
+ function listBuiltinTemplates() {
8934
+ return BUILTIN_TEMPLATES.map((template) => ({
8935
+ ...template,
8936
+ variables: template.variables.map((variable) => ({ ...variable })),
8937
+ tasks: template.tasks.map((task) => ({ ...task, tags: task.tags ? [...task.tags] : undefined }))
8938
+ }));
8939
+ }
8940
+ function getBuiltinTemplate(name) {
8941
+ return listBuiltinTemplates().find((template) => template.name === name) ?? null;
8942
+ }
8943
+ function exportBuiltinTemplate(name) {
8944
+ const template = getBuiltinTemplate(name);
8945
+ if (!template)
8946
+ throw new Error(`Built-in template not found: ${name}`);
8947
+ return {
8948
+ name: template.name,
8949
+ title_pattern: `${template.name}: {${template.variables[0]?.name ?? "name"}}`,
8950
+ description: template.description,
8951
+ priority: "medium",
8952
+ tags: [template.category, "local-template"],
8953
+ variables: template.variables,
8954
+ project_id: null,
8955
+ plan_id: null,
8956
+ metadata: templateMetadata(template),
8957
+ tasks: template.tasks.map((task) => ({
8958
+ position: task.position,
8959
+ title_pattern: task.title_pattern,
8960
+ description: task.description ?? null,
8961
+ priority: task.priority ?? "medium",
8962
+ tags: task.tags ?? [template.category],
8963
+ task_type: task.task_type ?? null,
8964
+ condition: task.condition ?? null,
8965
+ include_template_id: task.include_template_id ?? null,
8966
+ depends_on_positions: task.depends_on_positions ?? task.depends_on ?? [],
8967
+ metadata: task.metadata ?? { category: template.category }
8968
+ }))
8969
+ };
8970
+ }
8971
+ function exportBuiltinTemplateFiles() {
8972
+ return BUILTIN_TEMPLATES.map((template) => ({
8973
+ filename: `${template.name}.json`,
8974
+ template: exportBuiltinTemplate(template.name)
8975
+ }));
8976
+ }
8977
+ function writeBuiltinTemplateFiles(directory) {
8978
+ mkdirSync4(directory, { recursive: true });
8979
+ const files = [];
8980
+ for (const entry of exportBuiltinTemplateFiles()) {
8981
+ const path = join5(directory, entry.filename);
8982
+ writeFileSync2(path, `${JSON.stringify(entry.template, null, 2)}
8983
+ `, "utf-8");
8984
+ files.push(path);
8985
+ }
8986
+ return { directory, written: files.length, files };
8987
+ }
8914
8988
  function initBuiltinTemplates(db) {
8915
8989
  const d = db || getDatabase();
8916
8990
  const existing = listTemplates(d);
@@ -8936,7 +9010,9 @@ function initBuiltinTemplates(db) {
8936
9010
  name: bt.name,
8937
9011
  title_pattern: `${bt.name}: {${bt.variables[0]?.name || "name"}}`,
8938
9012
  description: bt.description,
9013
+ tags: [bt.category, "local-template"],
8939
9014
  variables: bt.variables,
9015
+ metadata: templateMetadata(bt),
8940
9016
  tasks
8941
9017
  }, d);
8942
9018
  created++;
@@ -8944,14 +9020,141 @@ function initBuiltinTemplates(db) {
8944
9020
  }
8945
9021
  return { created, skipped, names };
8946
9022
  }
8947
- var BUILTIN_TEMPLATES;
9023
+ var BUILTIN_TEMPLATE_LIBRARY_VERSION = "2026-05-21", BUILTIN_TEMPLATE_LIBRARY_SOURCE = "bundled-local-template-library", BUILTIN_TEMPLATES;
8948
9024
  var init_builtin_templates = __esm(() => {
8949
9025
  init_database();
8950
9026
  init_templates();
8951
9027
  BUILTIN_TEMPLATES = [
9028
+ {
9029
+ name: "bug-fix",
9030
+ description: "Reproduce, diagnose, fix, test, and release a defect.",
9031
+ category: "bug-fix",
9032
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
9033
+ variables: [{ name: "bug", required: true, description: "Bug description" }],
9034
+ tasks: [
9035
+ { position: 0, title_pattern: "Reproduce: {bug}", priority: "critical", tags: ["bug", "repro"] },
9036
+ { position: 1, title_pattern: "Diagnose root cause of {bug}", priority: "critical", tags: ["bug", "diagnosis"], depends_on_positions: [0] },
9037
+ { position: 2, title_pattern: "Write regression test for {bug}", priority: "high", tags: ["bug", "test"], depends_on_positions: [1] },
9038
+ { position: 3, title_pattern: "Implement fix for {bug}", priority: "critical", tags: ["bug", "implementation"], depends_on_positions: [2] },
9039
+ { position: 4, title_pattern: "Run full verification for {bug}", priority: "high", tags: ["bug", "verification"], depends_on_positions: [3] },
9040
+ { position: 5, title_pattern: "Publish and smoke test fix for {bug}", priority: "high", tags: ["bug", "release"], depends_on_positions: [4] }
9041
+ ]
9042
+ },
9043
+ {
9044
+ name: "feature-implementation",
9045
+ description: "Plan, build, test, document, and release a product feature.",
9046
+ category: "feature",
9047
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
9048
+ variables: [
9049
+ { name: "feature", required: true, description: "Feature name" },
9050
+ { name: "scope", required: false, default: "medium", description: "Implementation size or risk" }
9051
+ ],
9052
+ tasks: [
9053
+ { position: 0, title_pattern: "Define acceptance criteria for {feature}", priority: "high", tags: ["feature", "spec"] },
9054
+ { position: 1, title_pattern: "Design {scope} implementation plan for {feature}", priority: "high", tags: ["feature", "design"], depends_on_positions: [0] },
9055
+ { position: 2, title_pattern: "Implement {feature}", priority: "critical", tags: ["feature", "implementation"], depends_on_positions: [1] },
9056
+ { position: 3, title_pattern: "Add tests for {feature}", priority: "high", tags: ["feature", "test"], depends_on_positions: [2] },
9057
+ { position: 4, title_pattern: "Update docs for {feature}", priority: "medium", tags: ["feature", "docs"], depends_on_positions: [2] },
9058
+ { position: 5, title_pattern: "Run release checks for {feature}", priority: "high", tags: ["feature", "verification"], depends_on_positions: [3, 4] }
9059
+ ]
9060
+ },
9061
+ {
9062
+ name: "security-review",
9063
+ description: "Threat model, test, remediate, and report on security posture.",
9064
+ category: "security",
9065
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
9066
+ variables: [{ name: "target", required: true, description: "System, package, or change under review" }],
9067
+ tasks: [
9068
+ { position: 0, title_pattern: "Map trust boundaries for {target}", priority: "critical", tags: ["security", "threat-model"] },
9069
+ { position: 1, title_pattern: "Scan {target} for vulnerabilities and secret exposure", priority: "critical", tags: ["security", "scan"], depends_on_positions: [0] },
9070
+ { position: 2, title_pattern: "Review authz, data access, and dependency risks in {target}", priority: "critical", tags: ["security", "review"], depends_on_positions: [0] },
9071
+ { position: 3, title_pattern: "Fix critical security findings in {target}", priority: "critical", tags: ["security", "fix"], depends_on_positions: [1, 2] },
9072
+ { position: 4, title_pattern: "Retest {target} security fixes", priority: "high", tags: ["security", "verification"], depends_on_positions: [3] },
9073
+ { position: 5, title_pattern: "Write local security review report for {target}", priority: "medium", tags: ["security", "report"], depends_on_positions: [4] }
9074
+ ]
9075
+ },
9076
+ {
9077
+ name: "release",
9078
+ description: "Prepare, verify, publish, install, and smoke test a package release.",
9079
+ category: "release",
9080
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
9081
+ variables: [
9082
+ { name: "package", required: true, description: "Package name" },
9083
+ { name: "version", required: false, default: "patch", description: "Release version or bump type" }
9084
+ ],
9085
+ tasks: [
9086
+ { position: 0, title_pattern: "Prepare {package} {version} release notes", priority: "medium", tags: ["release", "docs"] },
9087
+ { position: 1, title_pattern: "Run full tests for {package}", priority: "critical", tags: ["release", "test"] },
9088
+ { position: 2, title_pattern: "Run build and release verification for {package}", priority: "critical", tags: ["release", "verification"], depends_on_positions: [1] },
9089
+ { position: 3, title_pattern: "Scan {package} release diff for secrets", priority: "critical", tags: ["release", "security"], depends_on_positions: [2] },
9090
+ { position: 4, title_pattern: "Publish {package} {version}", priority: "high", tags: ["release", "publish"], depends_on_positions: [3] },
9091
+ { position: 5, title_pattern: "Install and smoke test published {package}", priority: "high", tags: ["release", "smoke"], depends_on_positions: [4] }
9092
+ ]
9093
+ },
9094
+ {
9095
+ name: "migration",
9096
+ description: "Plan, test, apply, and verify a schema or data migration.",
9097
+ category: "migration",
9098
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
9099
+ variables: [{ name: "migration", required: true, description: "Migration name or objective" }],
9100
+ tasks: [
9101
+ { position: 0, title_pattern: "Design migration plan for {migration}", priority: "critical", tags: ["migration", "plan"] },
9102
+ { position: 1, title_pattern: "Write rollback plan for {migration}", priority: "critical", tags: ["migration", "rollback"], depends_on_positions: [0] },
9103
+ { position: 2, title_pattern: "Implement migration {migration}", priority: "critical", tags: ["migration", "implementation"], depends_on_positions: [0] },
9104
+ { position: 3, title_pattern: "Test migration {migration} on fixture data", priority: "high", tags: ["migration", "test"], depends_on_positions: [2] },
9105
+ { position: 4, title_pattern: "Run migration {migration} verification and drift checks", priority: "high", tags: ["migration", "verification"], depends_on_positions: [1, 3] },
9106
+ { position: 5, title_pattern: "Document migration {migration} evidence", priority: "medium", tags: ["migration", "docs"], depends_on_positions: [4] }
9107
+ ]
9108
+ },
9109
+ {
9110
+ name: "incident",
9111
+ description: "Triage, mitigate, repair, verify, and retrospect an incident.",
9112
+ category: "incident",
9113
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
9114
+ variables: [{ name: "incident", required: true, description: "Incident summary" }],
9115
+ tasks: [
9116
+ { position: 0, title_pattern: "Triage incident: {incident}", priority: "critical", tags: ["incident", "triage"] },
9117
+ { position: 1, title_pattern: "Mitigate customer impact for {incident}", priority: "critical", tags: ["incident", "mitigation"], depends_on_positions: [0] },
9118
+ { position: 2, title_pattern: "Diagnose root cause for {incident}", priority: "critical", tags: ["incident", "diagnosis"], depends_on_positions: [0] },
9119
+ { position: 3, title_pattern: "Implement durable repair for {incident}", priority: "critical", tags: ["incident", "repair"], depends_on_positions: [2] },
9120
+ { position: 4, title_pattern: "Verify recovery from {incident}", priority: "high", tags: ["incident", "verification"], depends_on_positions: [1, 3] },
9121
+ { position: 5, title_pattern: "Write retrospective for {incident}", priority: "medium", tags: ["incident", "retro"], depends_on_positions: [4] }
9122
+ ]
9123
+ },
9124
+ {
9125
+ name: "docs-refresh",
9126
+ description: "Audit, update, validate, and publish documentation changes.",
9127
+ category: "docs",
9128
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
9129
+ variables: [{ name: "area", required: true, description: "Documentation area or product surface" }],
9130
+ tasks: [
9131
+ { position: 0, title_pattern: "Audit current docs for {area}", priority: "medium", tags: ["docs", "audit"] },
9132
+ { position: 1, title_pattern: "Update examples and commands for {area}", priority: "medium", tags: ["docs", "examples"], depends_on_positions: [0] },
9133
+ { position: 2, title_pattern: "Validate docs snippets for {area}", priority: "high", tags: ["docs", "verification"], depends_on_positions: [1] },
9134
+ { position: 3, title_pattern: "Refresh screenshots or generated artifacts for {area}", priority: "medium", tags: ["docs", "assets"], depends_on_positions: [1] },
9135
+ { position: 4, title_pattern: "Publish docs refresh for {area}", priority: "medium", tags: ["docs", "release"], depends_on_positions: [2, 3] }
9136
+ ]
9137
+ },
9138
+ {
9139
+ name: "qa",
9140
+ description: "Build a focused QA plan, execute checks, file defects, and sign off.",
9141
+ category: "qa",
9142
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
9143
+ variables: [{ name: "target", required: true, description: "Feature, release, or workflow under QA" }],
9144
+ tasks: [
9145
+ { position: 0, title_pattern: "Create QA matrix for {target}", priority: "high", tags: ["qa", "plan"] },
9146
+ { position: 1, title_pattern: "Run happy-path QA for {target}", priority: "high", tags: ["qa", "manual"], depends_on_positions: [0] },
9147
+ { position: 2, title_pattern: "Run edge-case QA for {target}", priority: "high", tags: ["qa", "edge-cases"], depends_on_positions: [0] },
9148
+ { position: 3, title_pattern: "File and dedupe QA defects for {target}", priority: "medium", tags: ["qa", "bugs"], depends_on_positions: [1, 2] },
9149
+ { position: 4, title_pattern: "Verify QA fixes for {target}", priority: "high", tags: ["qa", "verification"], depends_on_positions: [3] },
9150
+ { position: 5, title_pattern: "Record QA signoff for {target}", priority: "medium", tags: ["qa", "signoff"], depends_on_positions: [4] }
9151
+ ]
9152
+ },
8952
9153
  {
8953
9154
  name: "open-source-project",
8954
9155
  description: "Full open-source project bootstrap \u2014 scaffold to publish",
9156
+ category: "open-source",
9157
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
8955
9158
  variables: [
8956
9159
  { name: "name", required: true, description: "Service name" },
8957
9160
  { name: "org", required: false, default: "hasna", description: "GitHub org" }
@@ -8971,45 +9174,6 @@ var init_builtin_templates = __esm(() => {
8971
9174
  { position: 11, title_pattern: "Publish @hasna/{name} to npm", priority: "high", depends_on_positions: [6, 7, 8] },
8972
9175
  { position: 12, title_pattern: "Install @hasna/{name} globally and verify", priority: "medium", depends_on_positions: [11] }
8973
9176
  ]
8974
- },
8975
- {
8976
- name: "bug-fix",
8977
- description: "Standard bug fix workflow",
8978
- variables: [{ name: "bug", required: true, description: "Bug description" }],
8979
- tasks: [
8980
- { position: 0, title_pattern: "Reproduce: {bug}", priority: "critical" },
8981
- { position: 1, title_pattern: "Diagnose root cause of {bug}", priority: "critical", depends_on_positions: [0] },
8982
- { position: 2, title_pattern: "Implement fix for {bug}", priority: "critical", depends_on_positions: [1] },
8983
- { position: 3, title_pattern: "Write regression test for {bug}", priority: "high", depends_on_positions: [2] },
8984
- { position: 4, title_pattern: "Publish fix and verify in production", priority: "high", depends_on_positions: [3] }
8985
- ]
8986
- },
8987
- {
8988
- name: "feature",
8989
- description: "Standard feature development workflow",
8990
- variables: [{ name: "feature", required: true }, { name: "scope", required: false, default: "medium" }],
8991
- tasks: [
8992
- { position: 0, title_pattern: "Write spec for {feature}", priority: "high" },
8993
- { position: 1, title_pattern: "Design implementation approach for {feature}", priority: "high", depends_on_positions: [0] },
8994
- { position: 2, title_pattern: "Implement {feature}", priority: "critical", depends_on_positions: [1] },
8995
- { position: 3, title_pattern: "Write tests for {feature}", priority: "high", depends_on_positions: [2] },
8996
- { position: 4, title_pattern: "Code review for {feature}", priority: "medium", depends_on_positions: [3] },
8997
- { position: 5, title_pattern: "Update docs for {feature}", priority: "medium", depends_on_positions: [2] },
8998
- { position: 6, title_pattern: "Deploy {feature}", priority: "high", depends_on_positions: [4] }
8999
- ]
9000
- },
9001
- {
9002
- name: "security-audit",
9003
- description: "Security audit workflow",
9004
- variables: [{ name: "target", required: true }],
9005
- tasks: [
9006
- { position: 0, title_pattern: "Scan {target} for vulnerabilities", priority: "critical" },
9007
- { position: 1, title_pattern: "Review {target} security findings", priority: "critical", depends_on_positions: [0] },
9008
- { position: 2, title_pattern: "Fix critical issues in {target}", priority: "critical", depends_on_positions: [1] },
9009
- { position: 3, title_pattern: "Retest {target} after fixes", priority: "high", depends_on_positions: [2] },
9010
- { position: 4, title_pattern: "Write security report for {target}", priority: "medium", depends_on_positions: [3] },
9011
- { position: 5, title_pattern: "Close audit for {target}", priority: "low", depends_on_positions: [4] }
9012
- ]
9013
9177
  }
9014
9178
  ];
9015
9179
  });
@@ -9277,7 +9441,7 @@ function registerPlanTemplateCommands(program2) {
9277
9441
  console.log(` ${chalk3.dim(t.id.slice(0, 8))} ${chalk3.bold(t.name)} ${chalk3.cyan(`"${t.title_pattern}"`)} ${chalk3.yellow(t.priority)}${vars}`);
9278
9442
  }
9279
9443
  });
9280
- program2.command("template-init").alias("templates-init").description("Initialize built-in starter templates (open-source-project, bug-fix, feature, security-audit)").action(async () => {
9444
+ program2.command("template-init").alias("templates-init").description("Initialize the bundled local template library").action(async () => {
9281
9445
  const globalOpts = program2.opts();
9282
9446
  const { initBuiltinTemplates: initBuiltinTemplates2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
9283
9447
  const result = initBuiltinTemplates2();
@@ -9291,6 +9455,52 @@ function registerPlanTemplateCommands(program2) {
9291
9455
  console.log(chalk3.green(`Created ${result.created} template(s): ${result.names.join(", ")}. Skipped ${result.skipped} existing.`));
9292
9456
  }
9293
9457
  });
9458
+ program2.command("template-library").alias("templates-library").description("List, show, or write the bundled local template library as editable JSON files").option("--show <name>", "Show one bundled template as JSON").option("--write <dir>", "Write all bundled templates to editable JSON files").action(async (opts) => {
9459
+ const globalOpts = program2.opts();
9460
+ const {
9461
+ exportBuiltinTemplate: exportBuiltinTemplate2,
9462
+ listBuiltinTemplates: listBuiltinTemplates2,
9463
+ writeBuiltinTemplateFiles: writeBuiltinTemplateFiles2
9464
+ } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
9465
+ try {
9466
+ if (opts.show) {
9467
+ const template = exportBuiltinTemplate2(opts.show);
9468
+ output(template, true);
9469
+ return;
9470
+ }
9471
+ if (opts.write) {
9472
+ const result = writeBuiltinTemplateFiles2(opts.write);
9473
+ if (globalOpts.json) {
9474
+ output(result, true);
9475
+ return;
9476
+ }
9477
+ console.log(chalk3.green(`Wrote ${result.written} editable template file(s) to ${result.directory}`));
9478
+ for (const file of result.files)
9479
+ console.log(chalk3.dim(` ${file}`));
9480
+ return;
9481
+ }
9482
+ const templates = listBuiltinTemplates2().map((template) => ({
9483
+ name: template.name,
9484
+ description: template.description,
9485
+ category: template.category,
9486
+ version: template.version,
9487
+ variables: template.variables,
9488
+ task_count: template.tasks.length
9489
+ }));
9490
+ if (globalOpts.json) {
9491
+ output(templates, true);
9492
+ return;
9493
+ }
9494
+ console.log(chalk3.bold(`${templates.length} bundled local template(s):
9495
+ `));
9496
+ for (const template of templates) {
9497
+ console.log(` ${chalk3.bold(template.name)} ${chalk3.dim(`[${template.category}]`)} ${chalk3.yellow(`${template.task_count} tasks`)}`);
9498
+ console.log(chalk3.dim(` ${template.description}`));
9499
+ }
9500
+ } catch (e) {
9501
+ handleError(e);
9502
+ }
9503
+ });
9294
9504
  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(async (id, opts) => {
9295
9505
  const globalOpts = program2.opts();
9296
9506
  const { previewTemplate: previewTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
@@ -10008,16 +10218,16 @@ var init_saved_search_views = __esm(() => {
10008
10218
  });
10009
10219
 
10010
10220
  // src/lib/claude-tasks.ts
10011
- import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
10012
- import { join as join5 } from "path";
10221
+ import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync3 } from "fs";
10222
+ import { join as join6 } from "path";
10013
10223
  function getTaskListDir(taskListId) {
10014
- return join5(HOME, ".claude", "tasks", taskListId);
10224
+ return join6(HOME, ".claude", "tasks", taskListId);
10015
10225
  }
10016
10226
  function readClaudeTask(dir, filename) {
10017
- return readJsonFile(join5(dir, filename));
10227
+ return readJsonFile(join6(dir, filename));
10018
10228
  }
10019
10229
  function writeClaudeTask(dir, task) {
10020
- writeJsonFile(join5(dir, `${task.id}.json`), task);
10230
+ writeJsonFile(join6(dir, `${task.id}.json`), task);
10021
10231
  }
10022
10232
  function toClaudeStatus(status) {
10023
10233
  if (status === "pending" || status === "in_progress" || status === "completed") {
@@ -10029,14 +10239,14 @@ function toSqliteStatus(status) {
10029
10239
  return status;
10030
10240
  }
10031
10241
  function readPrefixCounter(dir) {
10032
- const path = join5(dir, ".prefix-counter");
10242
+ const path = join6(dir, ".prefix-counter");
10033
10243
  if (!existsSync5(path))
10034
10244
  return 0;
10035
10245
  const val = parseInt(readFileSync3(path, "utf-8").trim(), 10);
10036
10246
  return isNaN(val) ? 0 : val;
10037
10247
  }
10038
10248
  function writePrefixCounter(dir, value) {
10039
- writeFileSync2(join5(dir, ".prefix-counter"), String(value));
10249
+ writeFileSync3(join6(dir, ".prefix-counter"), String(value));
10040
10250
  }
10041
10251
  function formatPrefixedSubject(title, prefix, counter) {
10042
10252
  const padded = String(counter).padStart(5, "0");
@@ -10072,7 +10282,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
10072
10282
  const existingByTodosId = new Map;
10073
10283
  const files = listJsonFiles(dir);
10074
10284
  for (const f of files) {
10075
- const path = join5(dir, f);
10285
+ const path = join6(dir, f);
10076
10286
  const ct = readClaudeTask(dir, f);
10077
10287
  if (ct?.metadata?.["todos_id"]) {
10078
10288
  existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
@@ -10179,7 +10389,7 @@ function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
10179
10389
  }
10180
10390
  for (const f of files) {
10181
10391
  try {
10182
- const filePath = join5(dir, f);
10392
+ const filePath = join6(dir, f);
10183
10393
  const ct = readClaudeTask(dir, f);
10184
10394
  if (!ct)
10185
10395
  continue;
@@ -10253,26 +10463,26 @@ var init_claude_tasks = __esm(() => {
10253
10463
 
10254
10464
  // src/lib/agent-tasks.ts
10255
10465
  import { existsSync as existsSync6 } from "fs";
10256
- import { join as join6 } from "path";
10466
+ import { join as join7 } from "path";
10257
10467
  function getTodosGlobalDir2() {
10258
- const newDir = join6(HOME, ".hasna", "todos");
10259
- const legacyDir = join6(HOME, ".todos");
10468
+ const newDir = join7(HOME, ".hasna", "todos");
10469
+ const legacyDir = join7(HOME, ".todos");
10260
10470
  if (!existsSync6(newDir) && existsSync6(legacyDir))
10261
10471
  return legacyDir;
10262
10472
  return newDir;
10263
10473
  }
10264
10474
  function agentBaseDir(agent) {
10265
10475
  const key = `TODOS_${agent.toUpperCase()}_TASKS_DIR`;
10266
- return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join6(getTodosGlobalDir2(), "agents");
10476
+ return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join7(getTodosGlobalDir2(), "agents");
10267
10477
  }
10268
10478
  function getTaskListDir2(agent, taskListId) {
10269
- return join6(agentBaseDir(agent), agent, taskListId);
10479
+ return join7(agentBaseDir(agent), agent, taskListId);
10270
10480
  }
10271
10481
  function readAgentTask(dir, filename) {
10272
- return readJsonFile(join6(dir, filename));
10482
+ return readJsonFile(join7(dir, filename));
10273
10483
  }
10274
10484
  function writeAgentTask(dir, task) {
10275
- writeJsonFile(join6(dir, `${task.id}.json`), task);
10485
+ writeJsonFile(join7(dir, `${task.id}.json`), task);
10276
10486
  }
10277
10487
  function taskToAgentTask(task, externalId, existingMeta) {
10278
10488
  return {
@@ -10306,7 +10516,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
10306
10516
  const existingByTodosId = new Map;
10307
10517
  const files = listJsonFiles(dir);
10308
10518
  for (const f of files) {
10309
- const path = join6(dir, f);
10519
+ const path = join7(dir, f);
10310
10520
  const at = readAgentTask(dir, f);
10311
10521
  if (at?.metadata?.["todos_id"]) {
10312
10522
  existingByTodosId.set(at.metadata["todos_id"], { task: at, mtimeMs: getFileMtimeMs(path) });
@@ -10399,7 +10609,7 @@ function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
10399
10609
  }
10400
10610
  for (const f of files) {
10401
10611
  try {
10402
- const filePath = join6(dir, f);
10612
+ const filePath = join7(dir, f);
10403
10613
  const at = readAgentTask(dir, f);
10404
10614
  if (!at)
10405
10615
  continue;
@@ -10810,7 +11020,7 @@ __export(exports_extract, {
10810
11020
  EXTRACT_TAGS: () => EXTRACT_TAGS
10811
11021
  });
10812
11022
  import { readFileSync as readFileSync5, statSync as statSync3 } from "fs";
10813
- import { relative as relative3, resolve as resolve7, join as join7 } from "path";
11023
+ import { relative as relative3, resolve as resolve7, join as join8 } from "path";
10814
11024
  function tagToPriority(tag) {
10815
11025
  switch (tag) {
10816
11026
  case "BUG":
@@ -10882,7 +11092,7 @@ function extractTodos(options, db) {
10882
11092
  const files = collectFiles(basePath, extensions);
10883
11093
  const allComments = [];
10884
11094
  for (const file of files) {
10885
- const fullPath = statSync3(basePath).isFile() ? basePath : join7(basePath, file);
11095
+ const fullPath = statSync3(basePath).isFile() ? basePath : join8(basePath, file);
10886
11096
  try {
10887
11097
  const source = readFileSync5(fullPath, "utf-8");
10888
11098
  const relPath = statSync3(basePath).isFile() ? relative3(resolve7(basePath, ".."), fullPath) : file;
@@ -11009,8 +11219,8 @@ var init_extract = __esm(() => {
11009
11219
 
11010
11220
  // src/lib/artifact-store.ts
11011
11221
  import { createHash as createHash2 } from "crypto";
11012
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
11013
- import { dirname as dirname6, join as join8, resolve as resolve8 } from "path";
11222
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
11223
+ import { dirname as dirname6, join as join9, resolve as resolve8 } from "path";
11014
11224
  import { tmpdir } from "os";
11015
11225
  function isInMemoryDb2(path) {
11016
11226
  return path === ":memory:" || path.startsWith("file::memory:");
@@ -11022,15 +11232,15 @@ function artifactStoreRoot() {
11022
11232
  return resolve8(process.env["TODOS_ARTIFACTS_DIR"]);
11023
11233
  const dbPath = getDatabasePath();
11024
11234
  if (isInMemoryDb2(dbPath))
11025
- return join8(tmpdir(), "hasna-todos-artifacts");
11026
- return join8(dirname6(resolve8(dbPath)), "artifacts");
11235
+ return join9(tmpdir(), "hasna-todos-artifacts");
11236
+ return join9(dirname6(resolve8(dbPath)), "artifacts");
11027
11237
  }
11028
11238
  function artifactStorePath(relativePath) {
11029
11239
  const normalized = relativePath.replace(/\\/g, "/");
11030
11240
  if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
11031
11241
  throw new Error("Invalid artifact store path");
11032
11242
  }
11033
- return join8(artifactStoreRoot(), normalized);
11243
+ return join9(artifactStoreRoot(), normalized);
11034
11244
  }
11035
11245
  function sha256(buffer) {
11036
11246
  return createHash2("sha256").update(buffer).digest("hex");
@@ -11088,11 +11298,11 @@ function storeArtifactContent(input) {
11088
11298
  redactionStatus = "redacted";
11089
11299
  }
11090
11300
  const storedSha = sha256(storedBuffer);
11091
- const relativePath = join8("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
11301
+ const relativePath = join9("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
11092
11302
  const destination = artifactStorePath(relativePath);
11093
11303
  if (!existsSync8(destination)) {
11094
- mkdirSync4(dirname6(destination), { recursive: true });
11095
- writeFileSync3(destination, storedBuffer);
11304
+ mkdirSync5(dirname6(destination), { recursive: true });
11305
+ writeFileSync4(destination, storedBuffer);
11096
11306
  }
11097
11307
  const createdAt = input.created_at || new Date().toISOString();
11098
11308
  const retentionDays = input.retention_days ?? null;
@@ -11209,8 +11419,8 @@ function importStoredArtifactContent(content) {
11209
11419
  };
11210
11420
  }
11211
11421
  const destination = artifactStorePath(content.relative_path);
11212
- mkdirSync4(dirname6(destination), { recursive: true });
11213
- writeFileSync3(destination, buffer);
11422
+ mkdirSync5(dirname6(destination), { recursive: true });
11423
+ writeFileSync4(destination, buffer);
11214
11424
  return {
11215
11425
  id: content.artifact_id,
11216
11426
  path: content.relative_path,
@@ -13032,8 +13242,8 @@ function registerProjectCommands(program2) {
13032
13242
  const projectId = autoProject(globalOpts);
13033
13243
  const writeOutput = async (content) => {
13034
13244
  if (opts.output) {
13035
- const { writeFileSync: writeFileSync4 } = await import("fs");
13036
- writeFileSync4(resolve9(opts.output), content.endsWith(`
13245
+ const { writeFileSync: writeFileSync5 } = await import("fs");
13246
+ writeFileSync5(resolve9(opts.output), content.endsWith(`
13037
13247
  `) ? content : `${content}
13038
13248
  `);
13039
13249
  } else {
@@ -14822,8 +15032,8 @@ var exports_doctor = {};
14822
15032
  __export(exports_doctor, {
14823
15033
  runTodosDoctor: () => runTodosDoctor
14824
15034
  });
14825
- import { chmodSync, copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync5, statSync as statSync5 } from "fs";
14826
- import { basename as basename4, dirname as dirname7, join as join9 } from "path";
15035
+ import { chmodSync, copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync6, statSync as statSync5 } from "fs";
15036
+ import { basename as basename4, dirname as dirname7, join as join10 } from "path";
14827
15037
  function tableExists(db, table) {
14828
15038
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
14829
15039
  }
@@ -14980,13 +15190,13 @@ function createBackup(dbPath) {
14980
15190
  if (!existsSync9(dbPath))
14981
15191
  return;
14982
15192
  const stamp = now().replace(/[:.]/g, "-");
14983
- const backupDir = join9(dirname7(dbPath), `${basename4(dbPath)}.backup-${stamp}`);
15193
+ const backupDir = join10(dirname7(dbPath), `${basename4(dbPath)}.backup-${stamp}`);
14984
15194
  const files = [];
14985
- mkdirSync5(backupDir, { recursive: true });
15195
+ mkdirSync6(backupDir, { recursive: true });
14986
15196
  for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
14987
15197
  if (!existsSync9(source))
14988
15198
  continue;
14989
- const target = join9(backupDir, basename4(source));
15199
+ const target = join10(backupDir, basename4(source));
14990
15200
  copyFileSync(source, target);
14991
15201
  files.push(target);
14992
15202
  }
@@ -15212,7 +15422,7 @@ var init_doctor = __esm(() => {
15212
15422
  });
15213
15423
 
15214
15424
  // src/server/routes.ts
15215
- import { join as join10, resolve as resolve11, sep } from "path";
15425
+ import { join as join11, resolve as resolve11, sep } from "path";
15216
15426
  function parseFieldsParam(url) {
15217
15427
  const fieldsParam = url.searchParams.get("fields");
15218
15428
  return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
@@ -15912,7 +16122,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
15912
16122
  if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
15913
16123
  return null;
15914
16124
  if (path !== "/") {
15915
- const filePath = join10(ctx.dashboardDir, path);
16125
+ const filePath = join11(ctx.dashboardDir, path);
15916
16126
  const resolvedFile = resolve11(filePath);
15917
16127
  const resolvedBase = resolve11(ctx.dashboardDir);
15918
16128
  if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
@@ -15922,7 +16132,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
15922
16132
  if (res2)
15923
16133
  return res2;
15924
16134
  }
15925
- const indexPath = join10(ctx.dashboardDir, "index.html");
16135
+ const indexPath = join11(ctx.dashboardDir, "index.html");
15926
16136
  const res = serveStaticFile2(indexPath);
15927
16137
  if (res)
15928
16138
  return res;
@@ -15952,26 +16162,26 @@ __export(exports_serve, {
15952
16162
  MIME_TYPES: () => MIME_TYPES
15953
16163
  });
15954
16164
  import { existsSync as existsSync10 } from "fs";
15955
- import { join as join11, dirname as dirname8, extname } from "path";
16165
+ import { join as join12, dirname as dirname8, extname } from "path";
15956
16166
  import { fileURLToPath as fileURLToPath2 } from "url";
15957
16167
  function resolveDashboardDir() {
15958
16168
  const candidates = [];
15959
16169
  try {
15960
16170
  const scriptDir = dirname8(fileURLToPath2(import.meta.url));
15961
- candidates.push(join11(scriptDir, "..", "dashboard", "dist"));
15962
- candidates.push(join11(scriptDir, "..", "..", "dashboard", "dist"));
16171
+ candidates.push(join12(scriptDir, "..", "dashboard", "dist"));
16172
+ candidates.push(join12(scriptDir, "..", "..", "dashboard", "dist"));
15963
16173
  } catch {}
15964
16174
  if (process.argv[1]) {
15965
16175
  const mainDir = dirname8(process.argv[1]);
15966
- candidates.push(join11(mainDir, "..", "dashboard", "dist"));
15967
- candidates.push(join11(mainDir, "..", "..", "dashboard", "dist"));
16176
+ candidates.push(join12(mainDir, "..", "dashboard", "dist"));
16177
+ candidates.push(join12(mainDir, "..", "..", "dashboard", "dist"));
15968
16178
  }
15969
- candidates.push(join11(process.cwd(), "dashboard", "dist"));
16179
+ candidates.push(join12(process.cwd(), "dashboard", "dist"));
15970
16180
  for (const candidate of candidates) {
15971
16181
  if (existsSync10(candidate))
15972
16182
  return candidate;
15973
16183
  }
15974
- return join11(process.cwd(), "dashboard", "dist");
16184
+ return join12(process.cwd(), "dashboard", "dist");
15975
16185
  }
15976
16186
  function getProvidedApiKey(req) {
15977
16187
  const headerKey = req.headers.get("x-api-key");
@@ -17747,14 +17957,14 @@ __export(exports_config_serve_commands, {
17747
17957
  registerConfigServeCommands: () => registerConfigServeCommands
17748
17958
  });
17749
17959
  import chalk6 from "chalk";
17750
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
17751
- import { dirname as dirname9, join as join12 } from "path";
17960
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
17961
+ import { dirname as dirname9, join as join13 } from "path";
17752
17962
  function registerConfigServeCommands(program2) {
17753
17963
  program2.command("config").description("View or update configuration").option("--get <key>", "Get a config value").option("--set <key=value>", "Set a config value (e.g. completion_guard.enabled=true)").action((opts) => {
17754
17964
  const globalOpts = program2.opts();
17755
17965
  const home = process.env["HOME"] || "~";
17756
- const newPath = join12(home, ".hasna", "todos", "config.json");
17757
- const legacyPath = join12(home, ".todos", "config.json");
17966
+ const newPath = join13(home, ".hasna", "todos", "config.json");
17967
+ const legacyPath = join13(home, ".todos", "config.json");
17758
17968
  const configPath = !existsSync11(newPath) && existsSync11(legacyPath) ? legacyPath : newPath;
17759
17969
  if (opts.get) {
17760
17970
  const config2 = loadConfig();
@@ -17793,8 +18003,8 @@ function registerConfigServeCommands(program2) {
17793
18003
  obj[keys[keys.length - 1]] = parsedValue;
17794
18004
  const dir = dirname9(configPath);
17795
18005
  if (!existsSync11(dir))
17796
- mkdirSync6(dir, { recursive: true });
17797
- writeFileSync4(configPath, JSON.stringify(config2, null, 2));
18006
+ mkdirSync7(dir, { recursive: true });
18007
+ writeFileSync5(configPath, JSON.stringify(config2, null, 2));
17798
18008
  if (globalOpts.json) {
17799
18009
  output({ key, value: parsedValue }, true);
17800
18010
  } else {
@@ -20605,8 +20815,8 @@ Repairs`));
20605
20815
  const db = getDatabase();
20606
20816
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
20607
20817
  const { statSync: statSync6 } = await import("fs");
20608
- const { join: join13 } = await import("path");
20609
- const dbPath = process.env["TODOS_DB_PATH"] || join13(process.env["HOME"] || "~", ".todos", "todos.db");
20818
+ const { join: join14 } = await import("path");
20819
+ const dbPath = process.env["TODOS_DB_PATH"] || join14(process.env["HOME"] || "~", ".todos", "todos.db");
20610
20820
  let size = "unknown";
20611
20821
  try {
20612
20822
  size = `${(statSync6(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
@@ -26525,10 +26735,12 @@ var init_token_utils = __esm(() => {
26525
26735
  "export_template",
26526
26736
  "import_template",
26527
26737
  "init_templates",
26738
+ "list_template_library",
26528
26739
  "list_templates",
26529
26740
  "preview_template",
26530
26741
  "template_history",
26531
- "update_template"
26742
+ "update_template",
26743
+ "write_template_library"
26532
26744
  ],
26533
26745
  webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
26534
26746
  machines: [
@@ -32670,6 +32882,259 @@ var init_agents2 = __esm(() => {
32670
32882
  init_database();
32671
32883
  });
32672
32884
 
32885
+ // src/mcp/tools/templates.ts
32886
+ function registerTemplateTools(server, { shouldRegisterTool, resolveId, formatError }) {
32887
+ if (shouldRegisterTool("create_template")) {
32888
+ 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.", {
32889
+ name: exports_external.string(),
32890
+ title_pattern: exports_external.string(),
32891
+ description: exports_external.string().optional(),
32892
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
32893
+ tags: exports_external.array(exports_external.string()).optional(),
32894
+ project_id: exports_external.string().optional(),
32895
+ plan_id: exports_external.string().optional(),
32896
+ variables: exports_external.array(exports_external.object({
32897
+ name: exports_external.string().describe("Variable name (used as {name} in patterns)"),
32898
+ required: exports_external.boolean().describe("Whether this variable must be provided"),
32899
+ default: exports_external.string().optional().describe("Default value if not provided"),
32900
+ description: exports_external.string().optional().describe("Help text for the variable")
32901
+ })).optional().describe("Typed variable definitions with defaults and required flags"),
32902
+ tasks: exports_external.array(exports_external.object({
32903
+ title_pattern: exports_external.string().describe("Title pattern with optional {variable} placeholders"),
32904
+ description: exports_external.string().optional(),
32905
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
32906
+ tags: exports_external.array(exports_external.string()).optional(),
32907
+ task_type: exports_external.string().optional(),
32908
+ depends_on: exports_external.array(exports_external.number()).optional().describe("Position indices (0-based) of tasks this task depends on"),
32909
+ metadata: exports_external.record(exports_external.unknown()).optional()
32910
+ })).optional().describe("Multi-task template: ordered list of tasks to create together with dependencies")
32911
+ }, async (params) => {
32912
+ try {
32913
+ const { createTemplate: createTemplate2, getTemplateWithTasks: getTemplateWithTasks2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
32914
+ const t = createTemplate2(params);
32915
+ const withTasks = getTemplateWithTasks2(t.id);
32916
+ const taskCount = withTasks?.tasks.length ?? 0;
32917
+ const taskInfo = taskCount > 0 ? ` | ${taskCount} task(s)` : "";
32918
+ return { content: [{ type: "text", text: `Template created: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}"${taskInfo}` }] };
32919
+ } catch (e) {
32920
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
32921
+ }
32922
+ });
32923
+ }
32924
+ if (shouldRegisterTool("list_templates")) {
32925
+ server.tool("list_templates", "List all task templates", {}, async () => {
32926
+ try {
32927
+ const { listTemplates: listTemplates2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
32928
+ const templates = listTemplates2();
32929
+ if (templates.length === 0)
32930
+ return { content: [{ type: "text", text: "No templates." }] };
32931
+ const text = templates.map((t) => {
32932
+ const vars = t.variables.length > 0 ? ` | vars: ${t.variables.map((v) => `${v.name}${v.required ? "*" : ""}${v.default ? `=${v.default}` : ""}`).join(", ")}` : "";
32933
+ return `${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}" | ${t.priority}${vars}`;
32934
+ }).join(`
32935
+ `);
32936
+ return { content: [{ type: "text", text: `${templates.length} template(s):
32937
+ ${text}` }] };
32938
+ } catch (e) {
32939
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
32940
+ }
32941
+ });
32942
+ }
32943
+ if (shouldRegisterTool("create_task_from_template")) {
32944
+ 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.", {
32945
+ template_id: exports_external.string(),
32946
+ title: exports_external.string().optional().describe("Override title (single-task templates only)"),
32947
+ description: exports_external.string().optional().describe("Override description (single-task templates only)"),
32948
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
32949
+ assigned_to: exports_external.string().optional(),
32950
+ project_id: exports_external.string().optional(),
32951
+ task_list_id: exports_external.string().optional(),
32952
+ variables: exports_external.record(exports_external.string()).optional().describe("Variable substitution map for {name} placeholders in multi-task templates")
32953
+ }, async (params) => {
32954
+ try {
32955
+ const { taskFromTemplate: taskFromTemplate2, getTemplateWithTasks: getTemplateWithTasks2, tasksFromTemplate: tasksFromTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
32956
+ const resolvedTemplateId = resolveId(params.template_id, "task_templates");
32957
+ const resolvedProjectId = params.project_id ? resolveId(params.project_id, "projects") : undefined;
32958
+ const resolvedTaskListId = params.task_list_id ? resolveId(params.task_list_id, "task_lists") : undefined;
32959
+ const templateWithTasks = getTemplateWithTasks2(resolvedTemplateId);
32960
+ if (templateWithTasks && templateWithTasks.tasks.length > 0) {
32961
+ const effectiveProjectId = resolvedProjectId || templateWithTasks.project_id || undefined;
32962
+ const tasks = tasksFromTemplate2(resolvedTemplateId, effectiveProjectId, params.variables, resolvedTaskListId);
32963
+ const text = tasks.map((t) => `${t.id.slice(0, 8)} | ${t.priority} | ${t.title}`).join(`
32964
+ `);
32965
+ return { content: [{ type: "text", text: `${tasks.length} task(s) created from template:
32966
+ ${text}` }] };
32967
+ }
32968
+ const input = taskFromTemplate2(resolvedTemplateId, {
32969
+ title: params.title,
32970
+ description: params.description,
32971
+ priority: params.priority,
32972
+ assigned_to: params.assigned_to,
32973
+ project_id: resolvedProjectId,
32974
+ task_list_id: resolvedTaskListId
32975
+ });
32976
+ const task = createTask(input);
32977
+ return { content: [{ type: "text", text: `Task created from template:
32978
+ ${task.id.slice(0, 8)} | ${task.priority} | ${task.title}` }] };
32979
+ } catch (e) {
32980
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
32981
+ }
32982
+ });
32983
+ }
32984
+ if (shouldRegisterTool("delete_template")) {
32985
+ server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
32986
+ try {
32987
+ const { deleteTemplate: deleteTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
32988
+ const resolvedId = resolveId(id, "task_templates");
32989
+ const deleted = deleteTemplate2(resolvedId);
32990
+ return { content: [{ type: "text", text: deleted ? "Template deleted." : "Template not found." }] };
32991
+ } catch (e) {
32992
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
32993
+ }
32994
+ });
32995
+ }
32996
+ if (shouldRegisterTool("update_template")) {
32997
+ server.tool("update_template", "Update a task template's name, title pattern, description, priority, tags, or other fields.", {
32998
+ id: exports_external.string(),
32999
+ name: exports_external.string().optional(),
33000
+ title_pattern: exports_external.string().optional(),
33001
+ description: exports_external.string().optional(),
33002
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
33003
+ tags: exports_external.array(exports_external.string()).optional(),
33004
+ project_id: exports_external.string().optional(),
33005
+ plan_id: exports_external.string().optional()
33006
+ }, async ({ id, ...updates }) => {
33007
+ try {
33008
+ const { updateTemplate: updateTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
33009
+ const resolvedId = resolveId(id, "task_templates");
33010
+ const t = updateTemplate2(resolvedId, updates);
33011
+ if (!t)
33012
+ return { content: [{ type: "text", text: `Template not found: ${id}` }], isError: true };
33013
+ return { content: [{ type: "text", text: `Template updated: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}" | ${t.priority}` }] };
33014
+ } catch (e) {
33015
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33016
+ }
33017
+ });
33018
+ }
33019
+ if (shouldRegisterTool("init_templates")) {
33020
+ server.tool("init_templates", "Initialize the bundled local template library. Skips templates that already exist by name.", {}, async () => {
33021
+ try {
33022
+ const { initBuiltinTemplates: initBuiltinTemplates2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
33023
+ const result = initBuiltinTemplates2();
33024
+ if (result.created === 0)
33025
+ return { content: [{ type: "text", text: `All ${result.skipped} built-in template(s) already exist.` }] };
33026
+ return { content: [{ type: "text", text: `Created ${result.created} template(s): ${result.names.join(", ")}. Skipped ${result.skipped} existing.` }] };
33027
+ } catch (e) {
33028
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33029
+ }
33030
+ });
33031
+ }
33032
+ if (shouldRegisterTool("list_template_library")) {
33033
+ server.tool("list_template_library", "List the bundled marketplace-free local template library without mutating the database.", {}, async () => {
33034
+ try {
33035
+ const { listBuiltinTemplates: listBuiltinTemplates2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
33036
+ const templates = listBuiltinTemplates2().map((template) => ({
33037
+ name: template.name,
33038
+ description: template.description,
33039
+ category: template.category,
33040
+ version: template.version,
33041
+ variables: template.variables,
33042
+ task_count: template.tasks.length
33043
+ }));
33044
+ return { content: [{ type: "text", text: JSON.stringify(templates, null, 2) }] };
33045
+ } catch (e) {
33046
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33047
+ }
33048
+ });
33049
+ }
33050
+ if (shouldRegisterTool("write_template_library")) {
33051
+ server.tool("write_template_library", "Write the bundled local template library to editable JSON files for review or import.", { directory: exports_external.string() }, async ({ directory }) => {
33052
+ try {
33053
+ const { writeBuiltinTemplateFiles: writeBuiltinTemplateFiles2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
33054
+ return { content: [{ type: "text", text: JSON.stringify(writeBuiltinTemplateFiles2(directory), null, 2) }] };
33055
+ } catch (e) {
33056
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33057
+ }
33058
+ });
33059
+ }
33060
+ if (shouldRegisterTool("preview_template")) {
33061
+ server.tool("preview_template", "Preview a template without creating tasks. Shows resolved titles (variables substituted), dependencies, and priorities.", {
33062
+ template_id: exports_external.string(),
33063
+ variables: exports_external.record(exports_external.string()).optional().describe("Variable substitution map for {name} placeholders")
33064
+ }, async (params) => {
33065
+ try {
33066
+ const { previewTemplate: previewTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
33067
+ const resolvedId = resolveId(params.template_id, "task_templates");
33068
+ const preview = previewTemplate2(resolvedId, params.variables);
33069
+ const lines = preview.tasks.map((t) => {
33070
+ const deps = t.depends_on_positions.length > 0 ? ` (after: ${t.depends_on_positions.join(", ")})` : "";
33071
+ return ` [${t.position}] ${t.priority} | ${t.title}${deps}`;
33072
+ });
33073
+ const varsInfo = preview.variables.length > 0 ? `
33074
+ Variables: ${preview.variables.map((v) => `${v.name}${v.required ? "*" : ""}${v.default ? `=${v.default}` : ""}`).join(", ")}` : "";
33075
+ const resolvedVars = Object.keys(preview.resolved_variables).length > 0 ? `
33076
+ Resolved: ${Object.entries(preview.resolved_variables).map(([k, v]) => `${k}=${v}`).join(", ")}` : "";
33077
+ return { content: [{ type: "text", text: `Preview: ${preview.template_name} (${preview.tasks.length} tasks)${varsInfo}${resolvedVars}
33078
+ ${lines.join(`
33079
+ `)}` }] };
33080
+ } catch (e) {
33081
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33082
+ }
33083
+ });
33084
+ }
33085
+ if (shouldRegisterTool("export_template")) {
33086
+ 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 }) => {
33087
+ try {
33088
+ const { exportTemplate: exportTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
33089
+ const resolvedId = resolveId(template_id, "task_templates");
33090
+ const json2 = exportTemplate2(resolvedId);
33091
+ return { content: [{ type: "text", text: JSON.stringify(json2, null, 2) }] };
33092
+ } catch (e) {
33093
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33094
+ }
33095
+ });
33096
+ }
33097
+ if (shouldRegisterTool("import_template")) {
33098
+ 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: json2 }) => {
33099
+ try {
33100
+ const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
33101
+ const parsed = JSON.parse(json2);
33102
+ const t = importTemplate2(parsed);
33103
+ return { content: [{ type: "text", text: `Template imported: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}"` }] };
33104
+ } catch (e) {
33105
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33106
+ }
33107
+ });
33108
+ }
33109
+ if (shouldRegisterTool("template_history")) {
33110
+ 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 }) => {
33111
+ try {
33112
+ const { listTemplateVersions: listTemplateVersions2, getTemplate: getTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
33113
+ const resolvedId = resolveId(template_id, "task_templates");
33114
+ const template = getTemplate2(resolvedId);
33115
+ if (!template)
33116
+ return { content: [{ type: "text", text: `Template not found: ${template_id}` }], isError: true };
33117
+ const versions = listTemplateVersions2(resolvedId);
33118
+ if (versions.length === 0)
33119
+ return { content: [{ type: "text", text: `${template.name} v${template.version} \u2014 no previous versions.` }] };
33120
+ const lines = versions.map((v) => {
33121
+ const snap = JSON.parse(v.snapshot);
33122
+ return `v${v.version} | ${v.created_at} | ${snap.name} | "${snap.title_pattern}"`;
33123
+ });
33124
+ return { content: [{ type: "text", text: `${template.name} \u2014 current: v${template.version}
33125
+ ${lines.join(`
33126
+ `)}` }] };
33127
+ } catch (e) {
33128
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33129
+ }
33130
+ });
33131
+ }
33132
+ }
33133
+ var init_templates2 = __esm(() => {
33134
+ init_zod();
33135
+ init_tasks();
33136
+ });
33137
+
32673
33138
  // src/mcp/index.ts
32674
33139
  var exports_mcp = {};
32675
33140
  __export(exports_mcp, {
@@ -32838,6 +33303,7 @@ var init_mcp = __esm(() => {
32838
33303
  init_code_tools();
32839
33304
  init_machines2();
32840
33305
  init_agents2();
33306
+ init_templates2();
32841
33307
  init_package_version();
32842
33308
  init_token_utils();
32843
33309
  if (hasVersionFlag()) {
@@ -32868,6 +33334,7 @@ var init_mcp = __esm(() => {
32868
33334
  registerTaskRelTools(server, toolContext);
32869
33335
  registerCodeTools(server, toolContext);
32870
33336
  registerAgentTools(server, { ...toolContext, agentFocusMap });
33337
+ registerTemplateTools(server, toolContext);
32871
33338
  registerMachineTools(server, { shouldRegisterTool, formatError });
32872
33339
  registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError });
32873
33340
  main().catch(async (err) => {
@@ -32887,15 +33354,15 @@ __export(exports_mcp_hooks_commands, {
32887
33354
  });
32888
33355
  import chalk8 from "chalk";
32889
33356
  import { execSync as execSync3 } from "child_process";
32890
- import { existsSync as existsSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, chmodSync as chmodSync2 } from "fs";
32891
- import { dirname as dirname10, join as join13 } from "path";
33357
+ import { existsSync as existsSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8, chmodSync as chmodSync2 } from "fs";
33358
+ import { dirname as dirname10, join as join14 } from "path";
32892
33359
  function getMcpBinaryPath() {
32893
33360
  try {
32894
33361
  const p = execSync3("which todos-mcp", { encoding: "utf-8" }).trim();
32895
33362
  if (p)
32896
33363
  return p;
32897
33364
  } catch {}
32898
- const bunBin = join13(HOME2, ".bun", "bin", "todos-mcp");
33365
+ const bunBin = join14(HOME2, ".bun", "bin", "todos-mcp");
32899
33366
  if (existsSync13(bunBin))
32900
33367
  return bunBin;
32901
33368
  return "todos-mcp";
@@ -32912,8 +33379,8 @@ function readJsonFile2(path) {
32912
33379
  function writeJsonFile2(path, data) {
32913
33380
  const dir = dirname10(path);
32914
33381
  if (!existsSync13(dir))
32915
- mkdirSync7(dir, { recursive: true });
32916
- writeFileSync5(path, JSON.stringify(data, null, 2) + `
33382
+ mkdirSync8(dir, { recursive: true });
33383
+ writeFileSync6(path, JSON.stringify(data, null, 2) + `
32917
33384
  `);
32918
33385
  }
32919
33386
  function readTomlFile(path) {
@@ -32924,8 +33391,8 @@ function readTomlFile(path) {
32924
33391
  function writeTomlFile(path, content) {
32925
33392
  const dir = dirname10(path);
32926
33393
  if (!existsSync13(dir))
32927
- mkdirSync7(dir, { recursive: true });
32928
- writeFileSync5(path, content);
33394
+ mkdirSync8(dir, { recursive: true });
33395
+ writeFileSync6(path, content);
32929
33396
  }
32930
33397
  function removeTomlBlock(content, blockName) {
32931
33398
  const lines = content.split(`
@@ -32989,7 +33456,7 @@ function unregisterClaude(_global) {
32989
33456
  }
32990
33457
  }
32991
33458
  function registerCodex(binPath) {
32992
- const configPath = join13(HOME2, ".codex", "config.toml");
33459
+ const configPath = join14(HOME2, ".codex", "config.toml");
32993
33460
  let content = readTomlFile(configPath);
32994
33461
  content = removeTomlBlock(content, "mcp_servers.todos");
32995
33462
  const block = `
@@ -33003,7 +33470,7 @@ args = []
33003
33470
  console.log(chalk8.green(`Codex CLI: registered in ${configPath}`));
33004
33471
  }
33005
33472
  function unregisterCodex() {
33006
- const configPath = join13(HOME2, ".codex", "config.toml");
33473
+ const configPath = join14(HOME2, ".codex", "config.toml");
33007
33474
  let content = readTomlFile(configPath);
33008
33475
  if (!content.includes("[mcp_servers.todos]")) {
33009
33476
  console.log(chalk8.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -33015,7 +33482,7 @@ function unregisterCodex() {
33015
33482
  console.log(chalk8.green(`Codex CLI: unregistered from ${configPath}`));
33016
33483
  }
33017
33484
  function registerGemini(binPath) {
33018
- const configPath = join13(HOME2, ".gemini", "settings.json");
33485
+ const configPath = join14(HOME2, ".gemini", "settings.json");
33019
33486
  const config = readJsonFile2(configPath);
33020
33487
  if (!config["mcpServers"]) {
33021
33488
  config["mcpServers"] = {};
@@ -33029,7 +33496,7 @@ function registerGemini(binPath) {
33029
33496
  console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
33030
33497
  }
33031
33498
  function unregisterGemini() {
33032
- const configPath = join13(HOME2, ".gemini", "settings.json");
33499
+ const configPath = join14(HOME2, ".gemini", "settings.json");
33033
33500
  const config = readJsonFile2(configPath);
33034
33501
  const servers = config["mcpServers"];
33035
33502
  if (!servers || !("todos" in servers)) {
@@ -33086,9 +33553,9 @@ function registerMcpHooksCommands(program2) {
33086
33553
  if (p)
33087
33554
  todosBin = p;
33088
33555
  } catch {}
33089
- const hooksDir = join13(process.cwd(), ".claude", "hooks");
33556
+ const hooksDir = join14(process.cwd(), ".claude", "hooks");
33090
33557
  if (!existsSync13(hooksDir))
33091
- mkdirSync7(hooksDir, { recursive: true });
33558
+ mkdirSync8(hooksDir, { recursive: true });
33092
33559
  const hookScript = `#!/usr/bin/env bash
33093
33560
  # Auto-generated by: todos hooks install
33094
33561
  # Syncs todos with Claude Code task list on tool use events.
@@ -33112,11 +33579,11 @@ esac
33112
33579
 
33113
33580
  exit 0
33114
33581
  `;
33115
- const hookPath = join13(hooksDir, "todos-sync.sh");
33116
- writeFileSync5(hookPath, hookScript);
33582
+ const hookPath = join14(hooksDir, "todos-sync.sh");
33583
+ writeFileSync6(hookPath, hookScript);
33117
33584
  execSync3(`chmod +x "${hookPath}"`);
33118
33585
  console.log(chalk8.green(`Hook script created: ${hookPath}`));
33119
- const settingsPath = join13(process.cwd(), ".claude", "settings.json");
33586
+ const settingsPath = join14(process.cwd(), ".claude", "settings.json");
33120
33587
  const settings = readJsonFile2(settingsPath);
33121
33588
  if (!settings["hooks"]) {
33122
33589
  settings["hooks"] = {};
@@ -33865,12 +34332,12 @@ Artifacts:`));
33865
34332
  console.log(chalk8.yellow("Hook already installed."));
33866
34333
  return;
33867
34334
  }
33868
- writeFileSync5(hookPath, existing + `
34335
+ writeFileSync6(hookPath, existing + `
33869
34336
  ${marker}
33870
34337
  $(dirname "$0")/../../scripts/post-commit-hook.sh
33871
34338
  `);
33872
34339
  } else {
33873
- writeFileSync5(hookPath, `#!/usr/bin/env bash
34340
+ writeFileSync6(hookPath, `#!/usr/bin/env bash
33874
34341
  ${marker}
33875
34342
  $(dirname "$0")/../../scripts/post-commit-hook.sh
33876
34343
  `);
@@ -33902,7 +34369,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
33902
34369
  if (cleaned === "#!/usr/bin/env bash" || cleaned === "") {
33903
34370
  (await import("fs")).unlinkSync(hookPath);
33904
34371
  } else {
33905
- writeFileSync5(hookPath, cleaned + `
34372
+ writeFileSync6(hookPath, cleaned + `
33906
34373
  `);
33907
34374
  }
33908
34375
  console.log(chalk8.green("Post-commit hook removed."));
@@ -34067,9 +34534,9 @@ __export(exports_machines2, {
34067
34534
  });
34068
34535
  import chalk10 from "chalk";
34069
34536
  import { execSync as execSync4 } from "child_process";
34070
- import { writeFileSync as writeFileSync6 } from "fs";
34537
+ import { writeFileSync as writeFileSync7 } from "fs";
34071
34538
  import { tmpdir as tmpdir2 } from "os";
34072
- import { join as join14 } from "path";
34539
+ import { join as join15 } from "path";
34073
34540
  function getOrCreateLocalMachineName() {
34074
34541
  return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
34075
34542
  }
@@ -34263,8 +34730,8 @@ Warning: No primary machine set.`));
34263
34730
  if (opts.push) {
34264
34731
  try {
34265
34732
  const localTasks = listTasks3();
34266
- const tmpFile = join14(tmpdir2(), `todos-export-${uuid()}.json`);
34267
- writeFileSync6(tmpFile, JSON.stringify(localTasks, null, 2));
34733
+ const tmpFile = join15(tmpdir2(), `todos-export-${uuid()}.json`);
34734
+ writeFileSync7(tmpFile, JSON.stringify(localTasks, null, 2));
34268
34735
  execSync4(`scp ${tmpFile} ${ssh}:/tmp/todos-import.json`, { timeout: 15000 });
34269
34736
  const importCmd = `ssh ${ssh} 'node -e "const fs=require(\\'fs\\');const tasks=JSON.parse(fs.readFileSync(\\'/tmp/todos-import.json\\',\\'utf-8\\'));console.log(JSON.stringify(tasks.length))"'`;
34270
34737
  const count = execSync4(importCmd, { encoding: "utf-8", timeout: 1e4 }).trim();