@hasna/todos 0.11.42 → 0.11.44

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`;
@@ -26403,6 +26613,8 @@ var init_token_utils = __esm(() => {
26403
26613
  "check_file_lock",
26404
26614
  "create_comment",
26405
26615
  "create_handoff",
26616
+ "capture_environment_snapshot",
26617
+ "compare_environment_snapshots",
26406
26618
  "create_inbox_item",
26407
26619
  "delete_comment",
26408
26620
  "detect_file_relationships",
@@ -26525,10 +26737,12 @@ var init_token_utils = __esm(() => {
26525
26737
  "export_template",
26526
26738
  "import_template",
26527
26739
  "init_templates",
26740
+ "list_template_library",
26528
26741
  "list_templates",
26529
26742
  "preview_template",
26530
26743
  "template_history",
26531
- "update_template"
26744
+ "update_template",
26745
+ "write_template_library"
26532
26746
  ],
26533
26747
  webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
26534
26748
  machines: [
@@ -32670,6 +32884,587 @@ var init_agents2 = __esm(() => {
32670
32884
  init_database();
32671
32885
  });
32672
32886
 
32887
+ // src/mcp/tools/templates.ts
32888
+ function registerTemplateTools(server, { shouldRegisterTool, resolveId, formatError }) {
32889
+ if (shouldRegisterTool("create_template")) {
32890
+ 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.", {
32891
+ name: exports_external.string(),
32892
+ title_pattern: exports_external.string(),
32893
+ description: exports_external.string().optional(),
32894
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
32895
+ tags: exports_external.array(exports_external.string()).optional(),
32896
+ project_id: exports_external.string().optional(),
32897
+ plan_id: exports_external.string().optional(),
32898
+ variables: exports_external.array(exports_external.object({
32899
+ name: exports_external.string().describe("Variable name (used as {name} in patterns)"),
32900
+ required: exports_external.boolean().describe("Whether this variable must be provided"),
32901
+ default: exports_external.string().optional().describe("Default value if not provided"),
32902
+ description: exports_external.string().optional().describe("Help text for the variable")
32903
+ })).optional().describe("Typed variable definitions with defaults and required flags"),
32904
+ tasks: exports_external.array(exports_external.object({
32905
+ title_pattern: exports_external.string().describe("Title pattern with optional {variable} placeholders"),
32906
+ description: exports_external.string().optional(),
32907
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
32908
+ tags: exports_external.array(exports_external.string()).optional(),
32909
+ task_type: exports_external.string().optional(),
32910
+ depends_on: exports_external.array(exports_external.number()).optional().describe("Position indices (0-based) of tasks this task depends on"),
32911
+ metadata: exports_external.record(exports_external.unknown()).optional()
32912
+ })).optional().describe("Multi-task template: ordered list of tasks to create together with dependencies")
32913
+ }, async (params) => {
32914
+ try {
32915
+ const { createTemplate: createTemplate2, getTemplateWithTasks: getTemplateWithTasks2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
32916
+ const t = createTemplate2(params);
32917
+ const withTasks = getTemplateWithTasks2(t.id);
32918
+ const taskCount = withTasks?.tasks.length ?? 0;
32919
+ const taskInfo = taskCount > 0 ? ` | ${taskCount} task(s)` : "";
32920
+ return { content: [{ type: "text", text: `Template created: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}"${taskInfo}` }] };
32921
+ } catch (e) {
32922
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
32923
+ }
32924
+ });
32925
+ }
32926
+ if (shouldRegisterTool("list_templates")) {
32927
+ server.tool("list_templates", "List all task templates", {}, async () => {
32928
+ try {
32929
+ const { listTemplates: listTemplates2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
32930
+ const templates = listTemplates2();
32931
+ if (templates.length === 0)
32932
+ return { content: [{ type: "text", text: "No templates." }] };
32933
+ const text = templates.map((t) => {
32934
+ const vars = t.variables.length > 0 ? ` | vars: ${t.variables.map((v) => `${v.name}${v.required ? "*" : ""}${v.default ? `=${v.default}` : ""}`).join(", ")}` : "";
32935
+ return `${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}" | ${t.priority}${vars}`;
32936
+ }).join(`
32937
+ `);
32938
+ return { content: [{ type: "text", text: `${templates.length} template(s):
32939
+ ${text}` }] };
32940
+ } catch (e) {
32941
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
32942
+ }
32943
+ });
32944
+ }
32945
+ if (shouldRegisterTool("create_task_from_template")) {
32946
+ 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.", {
32947
+ template_id: exports_external.string(),
32948
+ title: exports_external.string().optional().describe("Override title (single-task templates only)"),
32949
+ description: exports_external.string().optional().describe("Override description (single-task templates only)"),
32950
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
32951
+ assigned_to: exports_external.string().optional(),
32952
+ project_id: exports_external.string().optional(),
32953
+ task_list_id: exports_external.string().optional(),
32954
+ variables: exports_external.record(exports_external.string()).optional().describe("Variable substitution map for {name} placeholders in multi-task templates")
32955
+ }, async (params) => {
32956
+ try {
32957
+ const { taskFromTemplate: taskFromTemplate2, getTemplateWithTasks: getTemplateWithTasks2, tasksFromTemplate: tasksFromTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
32958
+ const resolvedTemplateId = resolveId(params.template_id, "task_templates");
32959
+ const resolvedProjectId = params.project_id ? resolveId(params.project_id, "projects") : undefined;
32960
+ const resolvedTaskListId = params.task_list_id ? resolveId(params.task_list_id, "task_lists") : undefined;
32961
+ const templateWithTasks = getTemplateWithTasks2(resolvedTemplateId);
32962
+ if (templateWithTasks && templateWithTasks.tasks.length > 0) {
32963
+ const effectiveProjectId = resolvedProjectId || templateWithTasks.project_id || undefined;
32964
+ const tasks = tasksFromTemplate2(resolvedTemplateId, effectiveProjectId, params.variables, resolvedTaskListId);
32965
+ const text = tasks.map((t) => `${t.id.slice(0, 8)} | ${t.priority} | ${t.title}`).join(`
32966
+ `);
32967
+ return { content: [{ type: "text", text: `${tasks.length} task(s) created from template:
32968
+ ${text}` }] };
32969
+ }
32970
+ const input = taskFromTemplate2(resolvedTemplateId, {
32971
+ title: params.title,
32972
+ description: params.description,
32973
+ priority: params.priority,
32974
+ assigned_to: params.assigned_to,
32975
+ project_id: resolvedProjectId,
32976
+ task_list_id: resolvedTaskListId
32977
+ });
32978
+ const task = createTask(input);
32979
+ return { content: [{ type: "text", text: `Task created from template:
32980
+ ${task.id.slice(0, 8)} | ${task.priority} | ${task.title}` }] };
32981
+ } catch (e) {
32982
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
32983
+ }
32984
+ });
32985
+ }
32986
+ if (shouldRegisterTool("delete_template")) {
32987
+ server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
32988
+ try {
32989
+ const { deleteTemplate: deleteTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
32990
+ const resolvedId = resolveId(id, "task_templates");
32991
+ const deleted = deleteTemplate2(resolvedId);
32992
+ return { content: [{ type: "text", text: deleted ? "Template deleted." : "Template not found." }] };
32993
+ } catch (e) {
32994
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
32995
+ }
32996
+ });
32997
+ }
32998
+ if (shouldRegisterTool("update_template")) {
32999
+ server.tool("update_template", "Update a task template's name, title pattern, description, priority, tags, or other fields.", {
33000
+ id: exports_external.string(),
33001
+ name: exports_external.string().optional(),
33002
+ title_pattern: exports_external.string().optional(),
33003
+ description: exports_external.string().optional(),
33004
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
33005
+ tags: exports_external.array(exports_external.string()).optional(),
33006
+ project_id: exports_external.string().optional(),
33007
+ plan_id: exports_external.string().optional()
33008
+ }, async ({ id, ...updates }) => {
33009
+ try {
33010
+ const { updateTemplate: updateTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
33011
+ const resolvedId = resolveId(id, "task_templates");
33012
+ const t = updateTemplate2(resolvedId, updates);
33013
+ if (!t)
33014
+ return { content: [{ type: "text", text: `Template not found: ${id}` }], isError: true };
33015
+ return { content: [{ type: "text", text: `Template updated: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}" | ${t.priority}` }] };
33016
+ } catch (e) {
33017
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33018
+ }
33019
+ });
33020
+ }
33021
+ if (shouldRegisterTool("init_templates")) {
33022
+ server.tool("init_templates", "Initialize the bundled local template library. Skips templates that already exist by name.", {}, async () => {
33023
+ try {
33024
+ const { initBuiltinTemplates: initBuiltinTemplates2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
33025
+ const result = initBuiltinTemplates2();
33026
+ if (result.created === 0)
33027
+ return { content: [{ type: "text", text: `All ${result.skipped} built-in template(s) already exist.` }] };
33028
+ return { content: [{ type: "text", text: `Created ${result.created} template(s): ${result.names.join(", ")}. Skipped ${result.skipped} existing.` }] };
33029
+ } catch (e) {
33030
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33031
+ }
33032
+ });
33033
+ }
33034
+ if (shouldRegisterTool("list_template_library")) {
33035
+ server.tool("list_template_library", "List the bundled marketplace-free local template library without mutating the database.", {}, async () => {
33036
+ try {
33037
+ const { listBuiltinTemplates: listBuiltinTemplates2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
33038
+ const templates = listBuiltinTemplates2().map((template) => ({
33039
+ name: template.name,
33040
+ description: template.description,
33041
+ category: template.category,
33042
+ version: template.version,
33043
+ variables: template.variables,
33044
+ task_count: template.tasks.length
33045
+ }));
33046
+ return { content: [{ type: "text", text: JSON.stringify(templates, null, 2) }] };
33047
+ } catch (e) {
33048
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33049
+ }
33050
+ });
33051
+ }
33052
+ if (shouldRegisterTool("write_template_library")) {
33053
+ 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 }) => {
33054
+ try {
33055
+ const { writeBuiltinTemplateFiles: writeBuiltinTemplateFiles2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
33056
+ return { content: [{ type: "text", text: JSON.stringify(writeBuiltinTemplateFiles2(directory), null, 2) }] };
33057
+ } catch (e) {
33058
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33059
+ }
33060
+ });
33061
+ }
33062
+ if (shouldRegisterTool("preview_template")) {
33063
+ server.tool("preview_template", "Preview a template without creating tasks. Shows resolved titles (variables substituted), dependencies, and priorities.", {
33064
+ template_id: exports_external.string(),
33065
+ variables: exports_external.record(exports_external.string()).optional().describe("Variable substitution map for {name} placeholders")
33066
+ }, async (params) => {
33067
+ try {
33068
+ const { previewTemplate: previewTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
33069
+ const resolvedId = resolveId(params.template_id, "task_templates");
33070
+ const preview = previewTemplate2(resolvedId, params.variables);
33071
+ const lines = preview.tasks.map((t) => {
33072
+ const deps = t.depends_on_positions.length > 0 ? ` (after: ${t.depends_on_positions.join(", ")})` : "";
33073
+ return ` [${t.position}] ${t.priority} | ${t.title}${deps}`;
33074
+ });
33075
+ const varsInfo = preview.variables.length > 0 ? `
33076
+ Variables: ${preview.variables.map((v) => `${v.name}${v.required ? "*" : ""}${v.default ? `=${v.default}` : ""}`).join(", ")}` : "";
33077
+ const resolvedVars = Object.keys(preview.resolved_variables).length > 0 ? `
33078
+ Resolved: ${Object.entries(preview.resolved_variables).map(([k, v]) => `${k}=${v}`).join(", ")}` : "";
33079
+ return { content: [{ type: "text", text: `Preview: ${preview.template_name} (${preview.tasks.length} tasks)${varsInfo}${resolvedVars}
33080
+ ${lines.join(`
33081
+ `)}` }] };
33082
+ } catch (e) {
33083
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33084
+ }
33085
+ });
33086
+ }
33087
+ if (shouldRegisterTool("export_template")) {
33088
+ 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 }) => {
33089
+ try {
33090
+ const { exportTemplate: exportTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
33091
+ const resolvedId = resolveId(template_id, "task_templates");
33092
+ const json2 = exportTemplate2(resolvedId);
33093
+ return { content: [{ type: "text", text: JSON.stringify(json2, null, 2) }] };
33094
+ } catch (e) {
33095
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33096
+ }
33097
+ });
33098
+ }
33099
+ if (shouldRegisterTool("import_template")) {
33100
+ 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 }) => {
33101
+ try {
33102
+ const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
33103
+ const parsed = JSON.parse(json2);
33104
+ const t = importTemplate2(parsed);
33105
+ return { content: [{ type: "text", text: `Template imported: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}"` }] };
33106
+ } catch (e) {
33107
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33108
+ }
33109
+ });
33110
+ }
33111
+ if (shouldRegisterTool("template_history")) {
33112
+ 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 }) => {
33113
+ try {
33114
+ const { listTemplateVersions: listTemplateVersions2, getTemplate: getTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
33115
+ const resolvedId = resolveId(template_id, "task_templates");
33116
+ const template = getTemplate2(resolvedId);
33117
+ if (!template)
33118
+ return { content: [{ type: "text", text: `Template not found: ${template_id}` }], isError: true };
33119
+ const versions = listTemplateVersions2(resolvedId);
33120
+ if (versions.length === 0)
33121
+ return { content: [{ type: "text", text: `${template.name} v${template.version} \u2014 no previous versions.` }] };
33122
+ const lines = versions.map((v) => {
33123
+ const snap = JSON.parse(v.snapshot);
33124
+ return `v${v.version} | ${v.created_at} | ${snap.name} | "${snap.title_pattern}"`;
33125
+ });
33126
+ return { content: [{ type: "text", text: `${template.name} \u2014 current: v${template.version}
33127
+ ${lines.join(`
33128
+ `)}` }] };
33129
+ } catch (e) {
33130
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33131
+ }
33132
+ });
33133
+ }
33134
+ }
33135
+ var init_templates2 = __esm(() => {
33136
+ init_zod();
33137
+ init_tasks();
33138
+ });
33139
+
33140
+ // src/lib/environment-snapshots.ts
33141
+ var exports_environment_snapshots = {};
33142
+ __export(exports_environment_snapshots, {
33143
+ writeEnvironmentSnapshot: () => writeEnvironmentSnapshot,
33144
+ recordEnvironmentSnapshot: () => recordEnvironmentSnapshot,
33145
+ readEnvironmentSnapshot: () => readEnvironmentSnapshot,
33146
+ compareEnvironmentSnapshots: () => compareEnvironmentSnapshots,
33147
+ compareEnvironmentSnapshotFiles: () => compareEnvironmentSnapshotFiles,
33148
+ captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
33149
+ });
33150
+ import { createHash as createHash6 } from "crypto";
33151
+ import { existsSync as existsSync13, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
33152
+ import { hostname, platform, arch } from "os";
33153
+ import { dirname as dirname10, join as join14, resolve as resolve12 } from "path";
33154
+ import { tmpdir as tmpdir2 } from "os";
33155
+ function sha2563(value) {
33156
+ return createHash6("sha256").update(value).digest("hex");
33157
+ }
33158
+ function fileRecord(root, relativePath) {
33159
+ const path = join14(root, relativePath);
33160
+ if (!existsSync13(path))
33161
+ return null;
33162
+ const stat = statSync6(path);
33163
+ if (!stat.isFile())
33164
+ return null;
33165
+ const content = readFileSync9(path);
33166
+ return { path: relativePath, sha256: sha2563(content), size_bytes: content.length };
33167
+ }
33168
+ function manifestRecord(root, relativePath) {
33169
+ const base = fileRecord(root, relativePath);
33170
+ if (!base)
33171
+ return null;
33172
+ const parsed = readJsonFile(join14(root, relativePath));
33173
+ if (!parsed)
33174
+ return { ...base, redacted: {} };
33175
+ const redacted = redactValue({
33176
+ name: parsed["name"] ?? null,
33177
+ version: parsed["version"] ?? null,
33178
+ packageManager: parsed["packageManager"] ?? null,
33179
+ scripts: parsed["scripts"] ?? {},
33180
+ dependencies: parsed["dependencies"] ?? {},
33181
+ devDependencies: parsed["devDependencies"] ?? {},
33182
+ peerDependencies: parsed["peerDependencies"] ?? {},
33183
+ optionalDependencies: parsed["optionalDependencies"] ?? {}
33184
+ });
33185
+ return { ...base, redacted };
33186
+ }
33187
+ function runLocalCommand(root, args) {
33188
+ const result = Bun.spawnSync({
33189
+ cmd: args,
33190
+ cwd: root,
33191
+ stdout: "pipe",
33192
+ stderr: "pipe",
33193
+ env: { PATH: process.env["PATH"] || "" }
33194
+ });
33195
+ return {
33196
+ exitCode: result.exitCode,
33197
+ stdout: redactEvidenceText(result.stdout.toString("utf8").trim()),
33198
+ stderr: redactEvidenceText(result.stderr.toString("utf8").trim())
33199
+ };
33200
+ }
33201
+ function summarizeGitStatus(lines) {
33202
+ const summary = { added: 0, modified: 0, deleted: 0, renamed: 0, untracked: 0 };
33203
+ for (const line of lines) {
33204
+ if (line.startsWith("??"))
33205
+ summary.untracked += 1;
33206
+ else if (line.includes("R"))
33207
+ summary.renamed += 1;
33208
+ else if (line.includes("D"))
33209
+ summary.deleted += 1;
33210
+ else if (line.includes("A"))
33211
+ summary.added += 1;
33212
+ else if (line.includes("M"))
33213
+ summary.modified += 1;
33214
+ }
33215
+ return summary;
33216
+ }
33217
+ function captureGit(root, warnings) {
33218
+ const inside = runLocalCommand(root, ["git", "rev-parse", "--is-inside-work-tree"]);
33219
+ if (inside.exitCode !== 0 || inside.stdout !== "true") {
33220
+ return { present: false, branch: null, commit: null, is_dirty: false, status_porcelain: [], status_summary: summarizeGitStatus([]) };
33221
+ }
33222
+ const branch = runLocalCommand(root, ["git", "branch", "--show-current"]);
33223
+ const commit = runLocalCommand(root, ["git", "rev-parse", "HEAD"]);
33224
+ const status = runLocalCommand(root, ["git", "status", "--porcelain=v1"]);
33225
+ if (commit.exitCode !== 0)
33226
+ warnings.push(`git commit unavailable: ${commit.stderr || commit.stdout || "unknown error"}`);
33227
+ if (status.exitCode !== 0)
33228
+ warnings.push(`git status unavailable: ${status.stderr || status.stdout || "unknown error"}`);
33229
+ const lines = status.stdout ? status.stdout.split(/\r?\n/).filter(Boolean) : [];
33230
+ return {
33231
+ present: true,
33232
+ branch: branch.stdout || null,
33233
+ commit: commit.stdout || null,
33234
+ is_dirty: lines.length > 0,
33235
+ status_porcelain: lines,
33236
+ status_summary: summarizeGitStatus(lines)
33237
+ };
33238
+ }
33239
+ function packageManager(env, lockfiles) {
33240
+ const userAgent = (env["npm_config_user_agent"] || "").toLowerCase();
33241
+ if (userAgent.includes("bun"))
33242
+ return "bun";
33243
+ if (lockfiles.some((file) => file.path.startsWith("bun.lock")))
33244
+ return "bun";
33245
+ if (userAgent.includes("npm") || lockfiles.some((file) => file.path.includes("package-lock")))
33246
+ return "npm";
33247
+ return "unknown";
33248
+ }
33249
+ function isSecretEnvKey(key) {
33250
+ return /api[_-]?key|token|secret|password|credential|private|session|cookie/i.test(key);
33251
+ }
33252
+ function commandEnv(env, includeValues) {
33253
+ const keys = Object.keys(env).sort();
33254
+ const interesting = keys.filter((key) => isSecretEnvKey(key) || ["CI", "NODE_ENV", "BUN_ENV", "SHELL", "TERM", "PATH", "PWD", "USER", "npm_config_user_agent"].includes(key) || key.startsWith("TODOS_") || key.startsWith("BUN_"));
33255
+ const redactedKeys = interesting.filter(isSecretEnvKey);
33256
+ const values = includeValues ? Object.fromEntries(interesting.map((key) => [key, isSecretEnvKey(key) ? "[REDACTED]" : redactEvidenceText(String(env[key] ?? ""))])) : null;
33257
+ return {
33258
+ command: null,
33259
+ env_keys: interesting,
33260
+ env: values,
33261
+ redacted_keys: redactedKeys
33262
+ };
33263
+ }
33264
+ function defaultSnapshotDir() {
33265
+ const dbPath = getDatabasePath();
33266
+ if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
33267
+ return join14(tmpdir2(), "hasna-todos", "environment-snapshots");
33268
+ return join14(dirname10(resolve12(dbPath)), "environment-snapshots");
33269
+ }
33270
+ function snapshotWithId(snapshot) {
33271
+ const digest = sha2563(JSON.stringify(snapshot)).slice(0, 24);
33272
+ return { id: `env_${digest}`, ...snapshot };
33273
+ }
33274
+ function captureEnvironmentSnapshot(input = {}) {
33275
+ const root = resolve12(input.root || process.cwd());
33276
+ const env = input.env || process.env;
33277
+ const warnings = [];
33278
+ const manifests = MANIFEST_FILES.map((file) => manifestRecord(root, file)).filter((file) => Boolean(file));
33279
+ const lockfiles = LOCKFILES.map((file) => fileRecord(root, file)).filter((file) => Boolean(file));
33280
+ const configHashes = CONFIG_FILES.map((file) => fileRecord(root, file)).filter((file) => Boolean(file));
33281
+ const commandMetadata = commandEnv(env, Boolean(input.include_env_values));
33282
+ commandMetadata.command = input.command ? redactEvidenceText(input.command) : null;
33283
+ if (manifests.length === 0)
33284
+ warnings.push("no package manifest found");
33285
+ if (lockfiles.length === 0)
33286
+ warnings.push("no package lockfile found");
33287
+ return snapshotWithId({
33288
+ schema_version: 1,
33289
+ captured_at: input.now ? new Date(input.now).toISOString() : new Date().toISOString(),
33290
+ root,
33291
+ machine: { hostname: hostname(), platform: platform(), arch: arch() },
33292
+ target: {
33293
+ task_id: input.task_id ?? null,
33294
+ run_id: input.run_id ?? null,
33295
+ agent_id: input.agent_id ?? null
33296
+ },
33297
+ runtime: {
33298
+ bun: Bun.version || null,
33299
+ node: process.version,
33300
+ executable: process.execPath
33301
+ },
33302
+ package_manager: {
33303
+ manager: packageManager(env, lockfiles),
33304
+ user_agent: env["npm_config_user_agent"] ? redactEvidenceText(env["npm_config_user_agent"]) : null,
33305
+ manifests,
33306
+ lockfiles
33307
+ },
33308
+ git: captureGit(root, warnings),
33309
+ config_hashes: configHashes,
33310
+ command_env: commandMetadata,
33311
+ warnings
33312
+ });
33313
+ }
33314
+ function writeEnvironmentSnapshot(snapshot, outputPath) {
33315
+ const path = outputPath ? resolve12(outputPath) : join14(defaultSnapshotDir(), `${snapshot.id}.json`);
33316
+ ensureDir2(dirname10(path));
33317
+ writeJsonFile(path, snapshot);
33318
+ return path;
33319
+ }
33320
+ function readEnvironmentSnapshot(path) {
33321
+ const snapshot = readJsonFile(resolve12(path));
33322
+ if (!snapshot || snapshot.schema_version !== 1 || typeof snapshot.id !== "string") {
33323
+ throw new Error(`Invalid environment snapshot: ${path}`);
33324
+ }
33325
+ return snapshot;
33326
+ }
33327
+ function recordEnvironmentSnapshot(input = {}, db) {
33328
+ let taskId = input.task_id;
33329
+ let runId = input.run_id;
33330
+ const needsDatabase = Boolean(taskId || runId);
33331
+ const d = needsDatabase ? db || getDatabase() : null;
33332
+ if (runId) {
33333
+ runId = resolveTaskRunId(runId, d);
33334
+ const run = getTaskRun(runId, d);
33335
+ if (!run)
33336
+ throw new Error(`Run not found: ${input.run_id}`);
33337
+ taskId = taskId || run.task_id;
33338
+ }
33339
+ if (taskId && d) {
33340
+ taskId = resolvePartialId(d, "tasks", taskId) || taskId;
33341
+ if (!getTask(taskId, d))
33342
+ throw new Error(`Task not found: ${taskId}`);
33343
+ }
33344
+ const snapshot = captureEnvironmentSnapshot({ ...input, task_id: taskId, run_id: runId });
33345
+ const outputPath = writeEnvironmentSnapshot(snapshot, input.output_path);
33346
+ let taskVerificationId = null;
33347
+ let runArtifactId = null;
33348
+ if (runId) {
33349
+ const artifact = addTaskRunArtifact({
33350
+ run_id: runId,
33351
+ path: outputPath,
33352
+ artifact_type: "environment_snapshot",
33353
+ description: "Reproducible local environment snapshot",
33354
+ metadata: { environment_snapshot_id: snapshot.id, schema_version: snapshot.schema_version },
33355
+ store_content: input.store_content ?? true,
33356
+ agent_id: input.agent_id
33357
+ }, d);
33358
+ runArtifactId = artifact.id;
33359
+ } else if (taskId) {
33360
+ const verification = addTaskVerification({
33361
+ task_id: taskId,
33362
+ command: input.command || "capture environment snapshot",
33363
+ status: "unknown",
33364
+ output_summary: `environment snapshot ${snapshot.id}`,
33365
+ artifact_path: outputPath,
33366
+ agent_id: input.agent_id,
33367
+ run_at: snapshot.captured_at
33368
+ }, d);
33369
+ taskVerificationId = verification.id;
33370
+ }
33371
+ return { snapshot, output_path: outputPath, task_verification_id: taskVerificationId, run_artifact_id: runArtifactId };
33372
+ }
33373
+ function keyed(files) {
33374
+ return new Map(files.map((file) => [file.path, file]));
33375
+ }
33376
+ function diffFiles(left, right) {
33377
+ const leftMap = keyed(left);
33378
+ const rightMap = keyed(right);
33379
+ const paths = [...new Set([...leftMap.keys(), ...rightMap.keys()])].sort((a, b) => a.localeCompare(b));
33380
+ return paths.map((path) => ({ path, left_sha256: leftMap.get(path)?.sha256 ?? null, right_sha256: rightMap.get(path)?.sha256 ?? null })).filter((entry) => entry.left_sha256 !== entry.right_sha256);
33381
+ }
33382
+ function compareEnvironmentSnapshots(left, right) {
33383
+ const warnings = [];
33384
+ if (left.schema_version !== right.schema_version)
33385
+ warnings.push("snapshot schema versions differ");
33386
+ return {
33387
+ schema_version: 1,
33388
+ left_id: left.id,
33389
+ right_id: right.id,
33390
+ same_root: left.root === right.root,
33391
+ same_machine: left.machine.hostname === right.machine.hostname && left.machine.platform === right.machine.platform && left.machine.arch === right.machine.arch,
33392
+ same_runtime: left.runtime.bun === right.runtime.bun && left.runtime.node === right.runtime.node,
33393
+ same_git_commit: left.git.commit === right.git.commit,
33394
+ dirty_state_changed: left.git.is_dirty !== right.git.is_dirty,
33395
+ changed_config_hashes: diffFiles(left.config_hashes, right.config_hashes),
33396
+ changed_lockfiles: diffFiles(left.package_manager.lockfiles, right.package_manager.lockfiles),
33397
+ changed_manifests: diffFiles(left.package_manager.manifests, right.package_manager.manifests),
33398
+ warnings
33399
+ };
33400
+ }
33401
+ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
33402
+ return compareEnvironmentSnapshots(readEnvironmentSnapshot(leftPath), readEnvironmentSnapshot(rightPath));
33403
+ }
33404
+ var MANIFEST_FILES, LOCKFILES, CONFIG_FILES;
33405
+ var init_environment_snapshots = __esm(() => {
33406
+ init_task_runs();
33407
+ init_task_commits();
33408
+ init_database();
33409
+ init_tasks();
33410
+ init_sync_utils();
33411
+ MANIFEST_FILES = ["package.json", "dashboard/package.json", "sdk/package.json"];
33412
+ LOCKFILES = ["bun.lock", "bun.lockb", "package-lock.json", "npm-shrinkwrap.json"];
33413
+ CONFIG_FILES = [
33414
+ "AGENTS.md",
33415
+ "CLAUDE.md",
33416
+ "README.md",
33417
+ "SECURITY.md",
33418
+ "bunfig.toml",
33419
+ "tsconfig.json",
33420
+ "components.json",
33421
+ "next.config.js",
33422
+ "next.config.mjs",
33423
+ "next.config.ts",
33424
+ "vite.config.ts",
33425
+ "dashboard/vite.config.ts"
33426
+ ];
33427
+ });
33428
+
33429
+ // src/mcp/tools/environment-snapshots.ts
33430
+ function registerEnvironmentSnapshotTools(server, { shouldRegisterTool, formatError }) {
33431
+ if (shouldRegisterTool("capture_environment_snapshot")) {
33432
+ server.tool("capture_environment_snapshot", "Capture a local reproducible environment snapshot with Bun/node versions, package-manager state, git status, config hashes, command metadata, and redacted manifests. Optionally attach it to a local task or run.", {
33433
+ root: exports_external.string().optional(),
33434
+ task_id: exports_external.string().optional(),
33435
+ run_id: exports_external.string().optional(),
33436
+ agent_id: exports_external.string().optional(),
33437
+ command: exports_external.string().optional(),
33438
+ output_path: exports_external.string().optional(),
33439
+ include_env_values: exports_external.boolean().optional()
33440
+ }, async (params) => {
33441
+ try {
33442
+ const { recordEnvironmentSnapshot: recordEnvironmentSnapshot2 } = await Promise.resolve().then(() => (init_environment_snapshots(), exports_environment_snapshots));
33443
+ const result = recordEnvironmentSnapshot2(params);
33444
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
33445
+ } catch (e) {
33446
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33447
+ }
33448
+ });
33449
+ }
33450
+ if (shouldRegisterTool("compare_environment_snapshots")) {
33451
+ server.tool("compare_environment_snapshots", "Compare two local environment snapshot JSON files and report runtime, git, manifest, lockfile, and config hash drift.", {
33452
+ left_path: exports_external.string(),
33453
+ right_path: exports_external.string()
33454
+ }, async ({ left_path, right_path }) => {
33455
+ try {
33456
+ const { compareEnvironmentSnapshotFiles: compareEnvironmentSnapshotFiles2 } = await Promise.resolve().then(() => (init_environment_snapshots(), exports_environment_snapshots));
33457
+ return { content: [{ type: "text", text: JSON.stringify(compareEnvironmentSnapshotFiles2(left_path, right_path), null, 2) }] };
33458
+ } catch (e) {
33459
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33460
+ }
33461
+ });
33462
+ }
33463
+ }
33464
+ var init_environment_snapshots2 = __esm(() => {
33465
+ init_zod();
33466
+ });
33467
+
32673
33468
  // src/mcp/index.ts
32674
33469
  var exports_mcp = {};
32675
33470
  __export(exports_mcp, {
@@ -32838,6 +33633,8 @@ var init_mcp = __esm(() => {
32838
33633
  init_code_tools();
32839
33634
  init_machines2();
32840
33635
  init_agents2();
33636
+ init_templates2();
33637
+ init_environment_snapshots2();
32841
33638
  init_package_version();
32842
33639
  init_token_utils();
32843
33640
  if (hasVersionFlag()) {
@@ -32868,6 +33665,8 @@ var init_mcp = __esm(() => {
32868
33665
  registerTaskRelTools(server, toolContext);
32869
33666
  registerCodeTools(server, toolContext);
32870
33667
  registerAgentTools(server, { ...toolContext, agentFocusMap });
33668
+ registerTemplateTools(server, toolContext);
33669
+ registerEnvironmentSnapshotTools(server, toolContext);
32871
33670
  registerMachineTools(server, { shouldRegisterTool, formatError });
32872
33671
  registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError });
32873
33672
  main().catch(async (err) => {
@@ -32887,45 +33686,45 @@ __export(exports_mcp_hooks_commands, {
32887
33686
  });
32888
33687
  import chalk8 from "chalk";
32889
33688
  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";
33689
+ import { existsSync as existsSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8, chmodSync as chmodSync2 } from "fs";
33690
+ import { dirname as dirname11, join as join15 } from "path";
32892
33691
  function getMcpBinaryPath() {
32893
33692
  try {
32894
33693
  const p = execSync3("which todos-mcp", { encoding: "utf-8" }).trim();
32895
33694
  if (p)
32896
33695
  return p;
32897
33696
  } catch {}
32898
- const bunBin = join13(HOME2, ".bun", "bin", "todos-mcp");
32899
- if (existsSync13(bunBin))
33697
+ const bunBin = join15(HOME2, ".bun", "bin", "todos-mcp");
33698
+ if (existsSync14(bunBin))
32900
33699
  return bunBin;
32901
33700
  return "todos-mcp";
32902
33701
  }
32903
33702
  function readJsonFile2(path) {
32904
- if (!existsSync13(path))
33703
+ if (!existsSync14(path))
32905
33704
  return {};
32906
33705
  try {
32907
- return JSON.parse(readFileSync9(path, "utf-8"));
33706
+ return JSON.parse(readFileSync10(path, "utf-8"));
32908
33707
  } catch {
32909
33708
  return {};
32910
33709
  }
32911
33710
  }
32912
33711
  function writeJsonFile2(path, data) {
32913
- const dir = dirname10(path);
32914
- if (!existsSync13(dir))
32915
- mkdirSync7(dir, { recursive: true });
32916
- writeFileSync5(path, JSON.stringify(data, null, 2) + `
33712
+ const dir = dirname11(path);
33713
+ if (!existsSync14(dir))
33714
+ mkdirSync8(dir, { recursive: true });
33715
+ writeFileSync6(path, JSON.stringify(data, null, 2) + `
32917
33716
  `);
32918
33717
  }
32919
33718
  function readTomlFile(path) {
32920
- if (!existsSync13(path))
33719
+ if (!existsSync14(path))
32921
33720
  return "";
32922
- return readFileSync9(path, "utf-8");
33721
+ return readFileSync10(path, "utf-8");
32923
33722
  }
32924
33723
  function writeTomlFile(path, content) {
32925
- const dir = dirname10(path);
32926
- if (!existsSync13(dir))
32927
- mkdirSync7(dir, { recursive: true });
32928
- writeFileSync5(path, content);
33724
+ const dir = dirname11(path);
33725
+ if (!existsSync14(dir))
33726
+ mkdirSync8(dir, { recursive: true });
33727
+ writeFileSync6(path, content);
32929
33728
  }
32930
33729
  function removeTomlBlock(content, blockName) {
32931
33730
  const lines = content.split(`
@@ -32989,7 +33788,7 @@ function unregisterClaude(_global) {
32989
33788
  }
32990
33789
  }
32991
33790
  function registerCodex(binPath) {
32992
- const configPath = join13(HOME2, ".codex", "config.toml");
33791
+ const configPath = join15(HOME2, ".codex", "config.toml");
32993
33792
  let content = readTomlFile(configPath);
32994
33793
  content = removeTomlBlock(content, "mcp_servers.todos");
32995
33794
  const block = `
@@ -33003,7 +33802,7 @@ args = []
33003
33802
  console.log(chalk8.green(`Codex CLI: registered in ${configPath}`));
33004
33803
  }
33005
33804
  function unregisterCodex() {
33006
- const configPath = join13(HOME2, ".codex", "config.toml");
33805
+ const configPath = join15(HOME2, ".codex", "config.toml");
33007
33806
  let content = readTomlFile(configPath);
33008
33807
  if (!content.includes("[mcp_servers.todos]")) {
33009
33808
  console.log(chalk8.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -33015,7 +33814,7 @@ function unregisterCodex() {
33015
33814
  console.log(chalk8.green(`Codex CLI: unregistered from ${configPath}`));
33016
33815
  }
33017
33816
  function registerGemini(binPath) {
33018
- const configPath = join13(HOME2, ".gemini", "settings.json");
33817
+ const configPath = join15(HOME2, ".gemini", "settings.json");
33019
33818
  const config = readJsonFile2(configPath);
33020
33819
  if (!config["mcpServers"]) {
33021
33820
  config["mcpServers"] = {};
@@ -33029,7 +33828,7 @@ function registerGemini(binPath) {
33029
33828
  console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
33030
33829
  }
33031
33830
  function unregisterGemini() {
33032
- const configPath = join13(HOME2, ".gemini", "settings.json");
33831
+ const configPath = join15(HOME2, ".gemini", "settings.json");
33033
33832
  const config = readJsonFile2(configPath);
33034
33833
  const servers = config["mcpServers"];
33035
33834
  if (!servers || !("todos" in servers)) {
@@ -33086,9 +33885,9 @@ function registerMcpHooksCommands(program2) {
33086
33885
  if (p)
33087
33886
  todosBin = p;
33088
33887
  } catch {}
33089
- const hooksDir = join13(process.cwd(), ".claude", "hooks");
33090
- if (!existsSync13(hooksDir))
33091
- mkdirSync7(hooksDir, { recursive: true });
33888
+ const hooksDir = join15(process.cwd(), ".claude", "hooks");
33889
+ if (!existsSync14(hooksDir))
33890
+ mkdirSync8(hooksDir, { recursive: true });
33092
33891
  const hookScript = `#!/usr/bin/env bash
33093
33892
  # Auto-generated by: todos hooks install
33094
33893
  # Syncs todos with Claude Code task list on tool use events.
@@ -33112,11 +33911,11 @@ esac
33112
33911
 
33113
33912
  exit 0
33114
33913
  `;
33115
- const hookPath = join13(hooksDir, "todos-sync.sh");
33116
- writeFileSync5(hookPath, hookScript);
33914
+ const hookPath = join15(hooksDir, "todos-sync.sh");
33915
+ writeFileSync6(hookPath, hookScript);
33117
33916
  execSync3(`chmod +x "${hookPath}"`);
33118
33917
  console.log(chalk8.green(`Hook script created: ${hookPath}`));
33119
- const settingsPath = join13(process.cwd(), ".claude", "settings.json");
33918
+ const settingsPath = join15(process.cwd(), ".claude", "settings.json");
33120
33919
  const settings = readJsonFile2(settingsPath);
33121
33920
  if (!settings["hooks"]) {
33122
33921
  settings["hooks"] = {};
@@ -33859,18 +34658,18 @@ Artifacts:`));
33859
34658
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
33860
34659
  const hookPath = `${gitDir}/hooks/post-commit`;
33861
34660
  const marker = "# todos-auto-link";
33862
- if (existsSync13(hookPath)) {
33863
- const existing = readFileSync9(hookPath, "utf-8");
34661
+ if (existsSync14(hookPath)) {
34662
+ const existing = readFileSync10(hookPath, "utf-8");
33864
34663
  if (existing.includes(marker)) {
33865
34664
  console.log(chalk8.yellow("Hook already installed."));
33866
34665
  return;
33867
34666
  }
33868
- writeFileSync5(hookPath, existing + `
34667
+ writeFileSync6(hookPath, existing + `
33869
34668
  ${marker}
33870
34669
  $(dirname "$0")/../../scripts/post-commit-hook.sh
33871
34670
  `);
33872
34671
  } else {
33873
- writeFileSync5(hookPath, `#!/usr/bin/env bash
34672
+ writeFileSync6(hookPath, `#!/usr/bin/env bash
33874
34673
  ${marker}
33875
34674
  $(dirname "$0")/../../scripts/post-commit-hook.sh
33876
34675
  `);
@@ -33887,11 +34686,11 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
33887
34686
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
33888
34687
  const hookPath = `${gitDir}/hooks/post-commit`;
33889
34688
  const marker = "# todos-auto-link";
33890
- if (!existsSync13(hookPath)) {
34689
+ if (!existsSync14(hookPath)) {
33891
34690
  console.log(chalk8.dim("No post-commit hook found."));
33892
34691
  return;
33893
34692
  }
33894
- const content = readFileSync9(hookPath, "utf-8");
34693
+ const content = readFileSync10(hookPath, "utf-8");
33895
34694
  if (!content.includes(marker)) {
33896
34695
  console.log(chalk8.dim("Hook not managed by todos."));
33897
34696
  return;
@@ -33902,7 +34701,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
33902
34701
  if (cleaned === "#!/usr/bin/env bash" || cleaned === "") {
33903
34702
  (await import("fs")).unlinkSync(hookPath);
33904
34703
  } else {
33905
- writeFileSync5(hookPath, cleaned + `
34704
+ writeFileSync6(hookPath, cleaned + `
33906
34705
  `);
33907
34706
  }
33908
34707
  console.log(chalk8.green("Post-commit hook removed."));
@@ -34067,9 +34866,9 @@ __export(exports_machines2, {
34067
34866
  });
34068
34867
  import chalk10 from "chalk";
34069
34868
  import { execSync as execSync4 } from "child_process";
34070
- import { writeFileSync as writeFileSync6 } from "fs";
34071
- import { tmpdir as tmpdir2 } from "os";
34072
- import { join as join14 } from "path";
34869
+ import { writeFileSync as writeFileSync7 } from "fs";
34870
+ import { tmpdir as tmpdir3 } from "os";
34871
+ import { join as join16 } from "path";
34073
34872
  function getOrCreateLocalMachineName() {
34074
34873
  return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
34075
34874
  }
@@ -34263,8 +35062,8 @@ Warning: No primary machine set.`));
34263
35062
  if (opts.push) {
34264
35063
  try {
34265
35064
  const localTasks = listTasks3();
34266
- const tmpFile = join14(tmpdir2(), `todos-export-${uuid()}.json`);
34267
- writeFileSync6(tmpFile, JSON.stringify(localTasks, null, 2));
35065
+ const tmpFile = join16(tmpdir3(), `todos-export-${uuid()}.json`);
35066
+ writeFileSync7(tmpFile, JSON.stringify(localTasks, null, 2));
34268
35067
  execSync4(`scp ${tmpFile} ${ssh}:/tmp/todos-import.json`, { timeout: 15000 });
34269
35068
  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
35069
  const count = execSync4(importCmd, { encoding: "utf-8", timeout: 1e4 }).trim();
@@ -34413,6 +35212,84 @@ var init_api_key_commands = __esm(() => {
34413
35212
  init_helpers();
34414
35213
  });
34415
35214
 
35215
+ // src/cli/commands/environment-snapshots.ts
35216
+ var exports_environment_snapshots2 = {};
35217
+ __export(exports_environment_snapshots2, {
35218
+ registerEnvironmentSnapshotCommands: () => registerEnvironmentSnapshotCommands
35219
+ });
35220
+ import chalk12 from "chalk";
35221
+ function printJson(value) {
35222
+ console.log(JSON.stringify(value, null, 2));
35223
+ }
35224
+ function registerEnvironmentSnapshotCommands(program2) {
35225
+ const envCmd = program2.command("env-snapshot").alias("environment-snapshot").description("Capture and compare local reproducible environment snapshots");
35226
+ envCmd.command("capture").description("Capture runtime, package-manager, git, config hash, and redacted environment metadata").option("--root <path>", "Project root to inspect").option("--task <id>", "Attach snapshot evidence to a task").option("--run <id>", "Attach snapshot artifact to a task run").option("--agent <name>", "Agent name for attached evidence").option("--command <command>", "Command or verification step this snapshot explains").option("--output <path>", "Write snapshot JSON to a specific path").option("--include-env-values", "Include nonsecret environment values; secret-like keys are still redacted").action((opts) => {
35227
+ const globalOpts = program2.opts();
35228
+ try {
35229
+ const result = recordEnvironmentSnapshot({
35230
+ root: opts.root,
35231
+ task_id: opts.task,
35232
+ run_id: opts.run,
35233
+ agent_id: opts.agent || globalOpts.agent,
35234
+ command: opts.command,
35235
+ output_path: opts.output,
35236
+ include_env_values: Boolean(opts.includeEnvValues)
35237
+ });
35238
+ if (globalOpts.json) {
35239
+ printJson(result);
35240
+ return;
35241
+ }
35242
+ console.log(chalk12.green("Captured") + ` ${result.snapshot.id}`);
35243
+ console.log(`path: ${result.output_path}`);
35244
+ console.log(`root: ${result.snapshot.root}`);
35245
+ console.log(`git: ${result.snapshot.git.commit || "none"}${result.snapshot.git.is_dirty ? " dirty" : ""}`);
35246
+ console.log(`runtime: bun ${result.snapshot.runtime.bun || "unknown"} / node ${result.snapshot.runtime.node}`);
35247
+ if (result.run_artifact_id)
35248
+ console.log(`run artifact: ${result.run_artifact_id}`);
35249
+ if (result.task_verification_id)
35250
+ console.log(`task verification: ${result.task_verification_id}`);
35251
+ for (const warning of result.snapshot.warnings)
35252
+ console.log(chalk12.yellow(`warning: ${warning}`));
35253
+ } catch (error) {
35254
+ const message = error instanceof Error ? error.message : String(error);
35255
+ if (globalOpts.json)
35256
+ printJson({ error: message });
35257
+ else
35258
+ console.error(chalk12.red(`Error: ${message}`));
35259
+ process.exit(1);
35260
+ }
35261
+ });
35262
+ envCmd.command("compare").description("Compare two environment snapshot JSON files").argument("<left>", "Left snapshot JSON path").argument("<right>", "Right snapshot JSON path").action((left, right) => {
35263
+ const globalOpts = program2.opts();
35264
+ try {
35265
+ const comparison = compareEnvironmentSnapshotFiles(left, right);
35266
+ if (globalOpts.json) {
35267
+ printJson(comparison);
35268
+ return;
35269
+ }
35270
+ console.log(`left: ${comparison.left_id}`);
35271
+ console.log(`right: ${comparison.right_id}`);
35272
+ console.log(`same root: ${comparison.same_root}`);
35273
+ console.log(`same machine: ${comparison.same_machine}`);
35274
+ console.log(`same runtime: ${comparison.same_runtime}`);
35275
+ console.log(`same git commit: ${comparison.same_git_commit}`);
35276
+ console.log(`dirty state changed: ${comparison.dirty_state_changed}`);
35277
+ const changed = comparison.changed_config_hashes.length + comparison.changed_lockfiles.length + comparison.changed_manifests.length;
35278
+ console.log(`changed files: ${changed}`);
35279
+ } catch (error) {
35280
+ const message = error instanceof Error ? error.message : String(error);
35281
+ if (globalOpts.json)
35282
+ printJson({ error: message });
35283
+ else
35284
+ console.error(chalk12.red(`Error: ${message}`));
35285
+ process.exit(1);
35286
+ }
35287
+ });
35288
+ }
35289
+ var init_environment_snapshots3 = __esm(() => {
35290
+ init_environment_snapshots();
35291
+ });
35292
+
34416
35293
  // src/cli/index.tsx
34417
35294
  init_esm();
34418
35295
  init_package_version();
@@ -34428,7 +35305,8 @@ var [
34428
35305
  { registerMcpHooksCommands: registerMcpHooksCommands2 },
34429
35306
  { registerDispatchCommands: registerDispatchCommands2 },
34430
35307
  { registerMachineCommands: registerMachineCommands2 },
34431
- { registerApiKeyCommands: registerApiKeyCommands2 }
35308
+ { registerApiKeyCommands: registerApiKeyCommands2 },
35309
+ { registerEnvironmentSnapshotCommands: registerEnvironmentSnapshotCommands2 }
34432
35310
  ] = await Promise.all([
34433
35311
  Promise.resolve().then(() => (init_task_commands(), exports_task_commands)),
34434
35312
  Promise.resolve().then(() => (init_plan_template_commands(), exports_plan_template_commands)),
@@ -34439,7 +35317,8 @@ var [
34439
35317
  Promise.resolve().then(() => (init_mcp_hooks_commands(), exports_mcp_hooks_commands)),
34440
35318
  Promise.resolve().then(() => (init_dispatch3(), exports_dispatch2)),
34441
35319
  Promise.resolve().then(() => (init_machines3(), exports_machines2)),
34442
- Promise.resolve().then(() => (init_api_key_commands(), exports_api_key_commands))
35320
+ Promise.resolve().then(() => (init_api_key_commands(), exports_api_key_commands)),
35321
+ Promise.resolve().then(() => (init_environment_snapshots3(), exports_environment_snapshots2))
34443
35322
  ]);
34444
35323
  registerTaskCommands2(program2);
34445
35324
  registerPlanTemplateCommands2(program2);
@@ -34451,4 +35330,5 @@ registerMcpHooksCommands2(program2);
34451
35330
  registerDispatchCommands2(program2);
34452
35331
  registerMachineCommands2(program2);
34453
35332
  registerApiKeyCommands2(program2);
35333
+ registerEnvironmentSnapshotCommands2(program2);
34454
35334
  program2.parse();