@hasna/todos 0.11.41 → 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
@@ -3083,6 +3083,19 @@ var init_migrations = __esm(() => {
3083
3083
  );
3084
3084
  CREATE INDEX IF NOT EXISTS idx_handoff_acks_agent ON handoff_acknowledgements(agent_id, acknowledged_at);
3085
3085
  INSERT OR IGNORE INTO _migrations (id) VALUES (54);
3086
+ `,
3087
+ `
3088
+ CREATE TABLE IF NOT EXISTS saved_search_views (
3089
+ id TEXT PRIMARY KEY,
3090
+ name TEXT NOT NULL UNIQUE,
3091
+ description TEXT,
3092
+ scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('all', 'tasks', 'projects', 'plans', 'runs', 'comments')),
3093
+ filters TEXT NOT NULL DEFAULT '{}',
3094
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3095
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
3096
+ );
3097
+ CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope);
3098
+ INSERT OR IGNORE INTO _migrations (id) VALUES (55);
3086
3099
  `
3087
3100
  ];
3088
3101
  });
@@ -3259,6 +3272,17 @@ function ensureSchema(db) {
3259
3272
  PRIMARY KEY (handoff_id, agent_id)
3260
3273
  )`);
3261
3274
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_handoff_acks_agent ON handoff_acknowledgements(agent_id, acknowledged_at)");
3275
+ ensureTable("saved_search_views", `
3276
+ CREATE TABLE saved_search_views (
3277
+ id TEXT PRIMARY KEY,
3278
+ name TEXT NOT NULL UNIQUE,
3279
+ description TEXT,
3280
+ scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('all', 'tasks', 'projects', 'plans', 'runs', 'comments')),
3281
+ filters TEXT NOT NULL DEFAULT '{}',
3282
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3283
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
3284
+ )`);
3285
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope)");
3262
3286
  ensureTable("task_relationships", `
3263
3287
  CREATE TABLE task_relationships (
3264
3288
  id TEXT PRIMARY KEY,
@@ -8884,9 +8908,83 @@ var init_plans = __esm(() => {
8884
8908
  // src/db/builtin-templates.ts
8885
8909
  var exports_builtin_templates = {};
8886
8910
  __export(exports_builtin_templates, {
8911
+ writeBuiltinTemplateFiles: () => writeBuiltinTemplateFiles,
8912
+ listBuiltinTemplates: () => listBuiltinTemplates,
8887
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,
8888
8919
  BUILTIN_TEMPLATES: () => BUILTIN_TEMPLATES
8889
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
+ }
8890
8988
  function initBuiltinTemplates(db) {
8891
8989
  const d = db || getDatabase();
8892
8990
  const existing = listTemplates(d);
@@ -8912,7 +9010,9 @@ function initBuiltinTemplates(db) {
8912
9010
  name: bt.name,
8913
9011
  title_pattern: `${bt.name}: {${bt.variables[0]?.name || "name"}}`,
8914
9012
  description: bt.description,
9013
+ tags: [bt.category, "local-template"],
8915
9014
  variables: bt.variables,
9015
+ metadata: templateMetadata(bt),
8916
9016
  tasks
8917
9017
  }, d);
8918
9018
  created++;
@@ -8920,14 +9020,141 @@ function initBuiltinTemplates(db) {
8920
9020
  }
8921
9021
  return { created, skipped, names };
8922
9022
  }
8923
- var BUILTIN_TEMPLATES;
9023
+ var BUILTIN_TEMPLATE_LIBRARY_VERSION = "2026-05-21", BUILTIN_TEMPLATE_LIBRARY_SOURCE = "bundled-local-template-library", BUILTIN_TEMPLATES;
8924
9024
  var init_builtin_templates = __esm(() => {
8925
9025
  init_database();
8926
9026
  init_templates();
8927
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
+ },
8928
9153
  {
8929
9154
  name: "open-source-project",
8930
9155
  description: "Full open-source project bootstrap \u2014 scaffold to publish",
9156
+ category: "open-source",
9157
+ version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
8931
9158
  variables: [
8932
9159
  { name: "name", required: true, description: "Service name" },
8933
9160
  { name: "org", required: false, default: "hasna", description: "GitHub org" }
@@ -8947,45 +9174,6 @@ var init_builtin_templates = __esm(() => {
8947
9174
  { position: 11, title_pattern: "Publish @hasna/{name} to npm", priority: "high", depends_on_positions: [6, 7, 8] },
8948
9175
  { position: 12, title_pattern: "Install @hasna/{name} globally and verify", priority: "medium", depends_on_positions: [11] }
8949
9176
  ]
8950
- },
8951
- {
8952
- name: "bug-fix",
8953
- description: "Standard bug fix workflow",
8954
- variables: [{ name: "bug", required: true, description: "Bug description" }],
8955
- tasks: [
8956
- { position: 0, title_pattern: "Reproduce: {bug}", priority: "critical" },
8957
- { position: 1, title_pattern: "Diagnose root cause of {bug}", priority: "critical", depends_on_positions: [0] },
8958
- { position: 2, title_pattern: "Implement fix for {bug}", priority: "critical", depends_on_positions: [1] },
8959
- { position: 3, title_pattern: "Write regression test for {bug}", priority: "high", depends_on_positions: [2] },
8960
- { position: 4, title_pattern: "Publish fix and verify in production", priority: "high", depends_on_positions: [3] }
8961
- ]
8962
- },
8963
- {
8964
- name: "feature",
8965
- description: "Standard feature development workflow",
8966
- variables: [{ name: "feature", required: true }, { name: "scope", required: false, default: "medium" }],
8967
- tasks: [
8968
- { position: 0, title_pattern: "Write spec for {feature}", priority: "high" },
8969
- { position: 1, title_pattern: "Design implementation approach for {feature}", priority: "high", depends_on_positions: [0] },
8970
- { position: 2, title_pattern: "Implement {feature}", priority: "critical", depends_on_positions: [1] },
8971
- { position: 3, title_pattern: "Write tests for {feature}", priority: "high", depends_on_positions: [2] },
8972
- { position: 4, title_pattern: "Code review for {feature}", priority: "medium", depends_on_positions: [3] },
8973
- { position: 5, title_pattern: "Update docs for {feature}", priority: "medium", depends_on_positions: [2] },
8974
- { position: 6, title_pattern: "Deploy {feature}", priority: "high", depends_on_positions: [4] }
8975
- ]
8976
- },
8977
- {
8978
- name: "security-audit",
8979
- description: "Security audit workflow",
8980
- variables: [{ name: "target", required: true }],
8981
- tasks: [
8982
- { position: 0, title_pattern: "Scan {target} for vulnerabilities", priority: "critical" },
8983
- { position: 1, title_pattern: "Review {target} security findings", priority: "critical", depends_on_positions: [0] },
8984
- { position: 2, title_pattern: "Fix critical issues in {target}", priority: "critical", depends_on_positions: [1] },
8985
- { position: 3, title_pattern: "Retest {target} after fixes", priority: "high", depends_on_positions: [2] },
8986
- { position: 4, title_pattern: "Write security report for {target}", priority: "medium", depends_on_positions: [3] },
8987
- { position: 5, title_pattern: "Close audit for {target}", priority: "low", depends_on_positions: [4] }
8988
- ]
8989
9177
  }
8990
9178
  ];
8991
9179
  });
@@ -9253,7 +9441,7 @@ function registerPlanTemplateCommands(program2) {
9253
9441
  console.log(` ${chalk3.dim(t.id.slice(0, 8))} ${chalk3.bold(t.name)} ${chalk3.cyan(`"${t.title_pattern}"`)} ${chalk3.yellow(t.priority)}${vars}`);
9254
9442
  }
9255
9443
  });
9256
- 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 () => {
9257
9445
  const globalOpts = program2.opts();
9258
9446
  const { initBuiltinTemplates: initBuiltinTemplates2 } = await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates));
9259
9447
  const result = initBuiltinTemplates2();
@@ -9267,6 +9455,52 @@ function registerPlanTemplateCommands(program2) {
9267
9455
  console.log(chalk3.green(`Created ${result.created} template(s): ${result.names.join(", ")}. Skipped ${result.skipped} existing.`));
9268
9456
  }
9269
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
+ });
9270
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) => {
9271
9505
  const globalOpts = program2.opts();
9272
9506
  const { previewTemplate: previewTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
@@ -9432,6 +9666,101 @@ var init_comments = __esm(() => {
9432
9666
  init_tasks();
9433
9667
  });
9434
9668
 
9669
+ // src/lib/local-fields.ts
9670
+ function normalizeList(values) {
9671
+ return [...new Set((values || []).map((value) => value.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b));
9672
+ }
9673
+ function metadataFields(task) {
9674
+ const value = task.metadata[LOCAL_FIELDS_KEY];
9675
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
9676
+ }
9677
+ function sameCustomValue(actual, expected) {
9678
+ return JSON.stringify(actual) === JSON.stringify(expected);
9679
+ }
9680
+ function hasOwnField(fields, key) {
9681
+ return Object.prototype.hasOwnProperty.call(fields, key);
9682
+ }
9683
+ function getTaskLocalFields(taskId, db) {
9684
+ const d = db || getDatabase();
9685
+ const task = getTask(taskId, d);
9686
+ if (!task)
9687
+ throw new TaskNotFoundError(taskId);
9688
+ const fields = metadataFields(task);
9689
+ return {
9690
+ labels: normalizeList(fields.labels),
9691
+ priority: task.priority,
9692
+ severity: typeof fields.severity === "string" ? fields.severity : null,
9693
+ owner: hasOwnField(fields, "owner") ? typeof fields.owner === "string" ? fields.owner : null : task.assigned_to,
9694
+ area: typeof fields.area === "string" ? fields.area : null,
9695
+ custom: fields.custom && typeof fields.custom === "object" && !Array.isArray(fields.custom) ? fields.custom : {}
9696
+ };
9697
+ }
9698
+ function setTaskLocalFields(taskId, input, db) {
9699
+ const d = db || getDatabase();
9700
+ const task = getTask(taskId, d);
9701
+ if (!task)
9702
+ throw new TaskNotFoundError(taskId);
9703
+ const currentFields = getTaskLocalFields(taskId, d);
9704
+ const labels = input.labels !== undefined ? normalizeList(input.labels) : currentFields.labels;
9705
+ const custom = input.custom !== undefined ? redactValue(input.merge_custom === false ? input.custom : { ...currentFields.custom, ...input.custom }) : currentFields.custom;
9706
+ const nextFields = {
9707
+ labels,
9708
+ priority: input.priority || task.priority,
9709
+ severity: input.severity !== undefined ? input.severity : currentFields.severity,
9710
+ owner: input.owner !== undefined ? input.owner : currentFields.owner,
9711
+ area: input.area !== undefined ? input.area : currentFields.area,
9712
+ custom
9713
+ };
9714
+ const nextMetadata = {
9715
+ ...task.metadata,
9716
+ [LOCAL_FIELDS_KEY]: nextFields
9717
+ };
9718
+ const previousLabels = new Set(currentFields.labels);
9719
+ const nextTags = normalizeList([...task.tags.filter((tag) => !previousLabels.has(tag)), ...labels]);
9720
+ const updates = {
9721
+ version: task.version,
9722
+ priority: input.priority,
9723
+ tags: nextTags,
9724
+ metadata: nextMetadata
9725
+ };
9726
+ if (input.owner !== undefined)
9727
+ updates.assigned_to = nextFields.owner;
9728
+ return updateTask(taskId, updates, d);
9729
+ }
9730
+ function queryTasksByLocalFields(query, db) {
9731
+ const d = db || getDatabase();
9732
+ const tasks = listTasks({
9733
+ priority: query.priority,
9734
+ tags: query.labels,
9735
+ limit: 1e4
9736
+ }, d);
9737
+ const matches = tasks.filter((task) => {
9738
+ const fields = getTaskLocalFields(task.id, d);
9739
+ if (query.labels && !query.labels.every((label) => fields.labels.includes(label)))
9740
+ return false;
9741
+ if (query.severity && fields.severity !== query.severity)
9742
+ return false;
9743
+ if (query.owner && fields.owner !== query.owner)
9744
+ return false;
9745
+ if (query.area && fields.area !== query.area)
9746
+ return false;
9747
+ if (query.custom) {
9748
+ for (const [key, expected] of Object.entries(query.custom)) {
9749
+ if (!sameCustomValue(fields.custom[key], expected))
9750
+ return false;
9751
+ }
9752
+ }
9753
+ return true;
9754
+ });
9755
+ return matches.slice(0, query.limit || 100);
9756
+ }
9757
+ var LOCAL_FIELDS_KEY = "local_fields";
9758
+ var init_local_fields = __esm(() => {
9759
+ init_database();
9760
+ init_tasks();
9761
+ init_types();
9762
+ });
9763
+
9435
9764
  // src/lib/search.ts
9436
9765
  var exports_search = {};
9437
9766
  __export(exports_search, {
@@ -9547,17 +9876,358 @@ var init_search = __esm(() => {
9547
9876
  init_database();
9548
9877
  });
9549
9878
 
9879
+ // src/lib/saved-search-views.ts
9880
+ var exports_saved_search_views = {};
9881
+ __export(exports_saved_search_views, {
9882
+ saveSearchView: () => saveSearchView,
9883
+ runSearchView: () => runSearchView,
9884
+ runSavedSearch: () => runSavedSearch,
9885
+ normalizeScope: () => normalizeScope,
9886
+ listSearchViews: () => listSearchViews,
9887
+ getSearchView: () => getSearchView,
9888
+ deleteSearchView: () => deleteSearchView
9889
+ });
9890
+ function parseFilters(value) {
9891
+ if (!value)
9892
+ return {};
9893
+ try {
9894
+ const parsed = JSON.parse(value);
9895
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
9896
+ } catch {
9897
+ return {};
9898
+ }
9899
+ }
9900
+ function rowToSavedSearchView(row) {
9901
+ return {
9902
+ ...row,
9903
+ scope: normalizeScope(row.scope),
9904
+ filters: parseFilters(row.filters)
9905
+ };
9906
+ }
9907
+ function normalizeScope(scope) {
9908
+ if (scope === "all" || scope === "tasks" || scope === "projects" || scope === "plans" || scope === "runs" || scope === "comments") {
9909
+ return scope;
9910
+ }
9911
+ return "tasks";
9912
+ }
9913
+ function normalizeName(name) {
9914
+ const normalized = name.trim();
9915
+ if (!normalized)
9916
+ throw new Error("Saved view name is required");
9917
+ return normalized;
9918
+ }
9919
+ function normalizeLimit(limit) {
9920
+ if (!Number.isFinite(limit) || !limit || limit <= 0)
9921
+ return 100;
9922
+ return Math.min(Math.trunc(limit), 1000);
9923
+ }
9924
+ function valuesList(value) {
9925
+ if (value === undefined)
9926
+ return [];
9927
+ return (Array.isArray(value) ? value : [value]).map((item) => item.trim()).filter(Boolean);
9928
+ }
9929
+ function addStatusFilter(sql, params, column, value) {
9930
+ const values = valuesList(value);
9931
+ if (values.length === 0)
9932
+ return sql;
9933
+ params.push(...values);
9934
+ return `${sql} AND ${column} IN (${values.map(() => "?").join(",")})`;
9935
+ }
9936
+ function addDateFilter(sql, params, column, value) {
9937
+ if (!value)
9938
+ return sql;
9939
+ params.push(value);
9940
+ return `${sql} AND ${column} > ?`;
9941
+ }
9942
+ function likePattern(query) {
9943
+ const trimmed = query?.trim();
9944
+ if (!trimmed || trimmed === "*")
9945
+ return null;
9946
+ return `%${trimmed}%`;
9947
+ }
9948
+ function parseJsonObject(value) {
9949
+ if (!value)
9950
+ return {};
9951
+ if (typeof value === "object" && !Array.isArray(value))
9952
+ return value;
9953
+ if (typeof value !== "string")
9954
+ return {};
9955
+ try {
9956
+ const parsed = JSON.parse(value);
9957
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
9958
+ } catch {
9959
+ return {};
9960
+ }
9961
+ }
9962
+ function rowToTaskRun(row) {
9963
+ return { ...row, metadata: parseJsonObject(row.metadata) };
9964
+ }
9965
+ function taskMatchesSavedFilters(task, filters, db) {
9966
+ if (filters.plan_id && task.plan_id !== filters.plan_id)
9967
+ return false;
9968
+ if (filters.tags && !filters.tags.every((tag) => task.tags.includes(tag)))
9969
+ return false;
9970
+ if (filters.depends_on) {
9971
+ const row = db.query("SELECT 1 FROM task_dependencies WHERE task_id = ? AND depends_on = ?").get(task.id, filters.depends_on);
9972
+ if (!row)
9973
+ return false;
9974
+ }
9975
+ if (filters.blocks) {
9976
+ const row = db.query("SELECT 1 FROM task_dependencies WHERE task_id = ? AND depends_on = ?").get(filters.blocks, task.id);
9977
+ if (!row)
9978
+ return false;
9979
+ }
9980
+ return true;
9981
+ }
9982
+ function searchTaskEntities(filters, db) {
9983
+ let tasks;
9984
+ if (filters.local_fields) {
9985
+ const localMatches = queryTasksByLocalFields({ ...filters.local_fields, limit: 1e4 }, db);
9986
+ const allowedIds = new Set(localMatches.map((task) => task.id));
9987
+ tasks = searchTasks({
9988
+ query: filters.query,
9989
+ project_id: filters.project_id,
9990
+ task_list_id: filters.task_list_id,
9991
+ status: filters.status,
9992
+ priority: filters.priority,
9993
+ assigned_to: filters.assigned_to,
9994
+ agent_id: filters.agent_id,
9995
+ created_after: filters.created_after,
9996
+ updated_after: filters.updated_after,
9997
+ has_dependencies: filters.has_dependencies,
9998
+ is_blocked: filters.is_blocked
9999
+ }, undefined, undefined, db).filter((task) => allowedIds.has(task.id));
10000
+ } else {
10001
+ tasks = searchTasks({
10002
+ query: filters.query,
10003
+ project_id: filters.project_id,
10004
+ task_list_id: filters.task_list_id,
10005
+ status: filters.status,
10006
+ priority: filters.priority,
10007
+ assigned_to: filters.assigned_to,
10008
+ agent_id: filters.agent_id,
10009
+ created_after: filters.created_after,
10010
+ updated_after: filters.updated_after,
10011
+ has_dependencies: filters.has_dependencies,
10012
+ is_blocked: filters.is_blocked
10013
+ }, undefined, undefined, db);
10014
+ }
10015
+ return tasks.filter((task) => taskMatchesSavedFilters(task, filters, db)).slice(0, normalizeLimit(filters.limit));
10016
+ }
10017
+ function searchProjects(filters, db) {
10018
+ const params = [];
10019
+ let sql = "SELECT * FROM projects WHERE 1=1";
10020
+ if (filters.project_id) {
10021
+ sql += " AND id = ?";
10022
+ params.push(filters.project_id);
10023
+ }
10024
+ const pattern = likePattern(filters.query);
10025
+ if (pattern) {
10026
+ sql += " AND (name LIKE ? OR description LIKE ? OR path LIKE ?)";
10027
+ params.push(pattern, pattern, pattern);
10028
+ }
10029
+ sql = addDateFilter(sql, params, "created_at", filters.created_after);
10030
+ sql = addDateFilter(sql, params, "updated_at", filters.updated_after);
10031
+ sql += " ORDER BY name LIMIT ?";
10032
+ params.push(normalizeLimit(filters.limit));
10033
+ return db.query(sql).all(...params);
10034
+ }
10035
+ function searchPlans(filters, db) {
10036
+ const params = [];
10037
+ let sql = "SELECT * FROM plans WHERE 1=1";
10038
+ if (filters.project_id) {
10039
+ sql += " AND project_id = ?";
10040
+ params.push(filters.project_id);
10041
+ }
10042
+ if (filters.task_list_id) {
10043
+ sql += " AND task_list_id = ?";
10044
+ params.push(filters.task_list_id);
10045
+ }
10046
+ if (filters.agent_id) {
10047
+ sql += " AND agent_id = ?";
10048
+ params.push(filters.agent_id);
10049
+ }
10050
+ sql = addStatusFilter(sql, params, "status", filters.status);
10051
+ const pattern = likePattern(filters.query);
10052
+ if (pattern) {
10053
+ sql += " AND (name LIKE ? OR description LIKE ?)";
10054
+ params.push(pattern, pattern);
10055
+ }
10056
+ sql = addDateFilter(sql, params, "created_at", filters.created_after);
10057
+ sql = addDateFilter(sql, params, "updated_at", filters.updated_after);
10058
+ sql += " ORDER BY updated_at DESC, created_at DESC LIMIT ?";
10059
+ params.push(normalizeLimit(filters.limit));
10060
+ return db.query(sql).all(...params);
10061
+ }
10062
+ function searchRuns(filters, db) {
10063
+ const params = [];
10064
+ let sql = `SELECT r.* FROM task_runs r
10065
+ JOIN tasks t ON t.id = r.task_id
10066
+ WHERE 1=1`;
10067
+ if (filters.project_id) {
10068
+ sql += " AND t.project_id = ?";
10069
+ params.push(filters.project_id);
10070
+ }
10071
+ if (filters.task_list_id) {
10072
+ sql += " AND t.task_list_id = ?";
10073
+ params.push(filters.task_list_id);
10074
+ }
10075
+ if (filters.plan_id) {
10076
+ sql += " AND t.plan_id = ?";
10077
+ params.push(filters.plan_id);
10078
+ }
10079
+ if (filters.task_id) {
10080
+ sql += " AND r.task_id = ?";
10081
+ params.push(filters.task_id);
10082
+ }
10083
+ if (filters.agent_id) {
10084
+ sql += " AND r.agent_id = ?";
10085
+ params.push(filters.agent_id);
10086
+ }
10087
+ sql = addStatusFilter(sql, params, "r.status", filters.status);
10088
+ const pattern = likePattern(filters.query);
10089
+ if (pattern) {
10090
+ sql += " AND (r.title LIKE ? OR r.summary LIKE ? OR t.title LIKE ?)";
10091
+ params.push(pattern, pattern, pattern);
10092
+ }
10093
+ sql = addDateFilter(sql, params, "r.created_at", filters.created_after);
10094
+ sql = addDateFilter(sql, params, "r.updated_at", filters.updated_after);
10095
+ sql += " ORDER BY r.started_at DESC, r.created_at DESC LIMIT ?";
10096
+ params.push(normalizeLimit(filters.limit));
10097
+ return db.query(sql).all(...params).map(rowToTaskRun);
10098
+ }
10099
+ function searchComments(filters, db) {
10100
+ const params = [];
10101
+ let sql = `SELECT c.* FROM task_comments c
10102
+ JOIN tasks t ON t.id = c.task_id
10103
+ WHERE 1=1`;
10104
+ if (filters.project_id) {
10105
+ sql += " AND t.project_id = ?";
10106
+ params.push(filters.project_id);
10107
+ }
10108
+ if (filters.task_list_id) {
10109
+ sql += " AND t.task_list_id = ?";
10110
+ params.push(filters.task_list_id);
10111
+ }
10112
+ if (filters.plan_id) {
10113
+ sql += " AND t.plan_id = ?";
10114
+ params.push(filters.plan_id);
10115
+ }
10116
+ if (filters.task_id) {
10117
+ sql += " AND c.task_id = ?";
10118
+ params.push(filters.task_id);
10119
+ }
10120
+ if (filters.agent_id) {
10121
+ sql += " AND c.agent_id = ?";
10122
+ params.push(filters.agent_id);
10123
+ }
10124
+ const pattern = likePattern(filters.query);
10125
+ if (pattern) {
10126
+ sql += " AND (c.content LIKE ? OR t.title LIKE ?)";
10127
+ params.push(pattern, pattern);
10128
+ }
10129
+ sql = addDateFilter(sql, params, "c.created_at", filters.created_after);
10130
+ sql += " ORDER BY c.created_at DESC, c.id LIMIT ?";
10131
+ params.push(normalizeLimit(filters.limit));
10132
+ return db.query(sql).all(...params);
10133
+ }
10134
+ function toResults(entityType, rows) {
10135
+ return rows.map((entity) => ({ entity_type: entityType, entity }));
10136
+ }
10137
+ function runSavedSearch(filters = {}, scope = "tasks", db) {
10138
+ const d = db || getDatabase();
10139
+ const normalizedScope = normalizeScope(scope);
10140
+ const scopes = normalizedScope === "all" ? ["tasks", "projects", "plans", "runs", "comments"] : [normalizedScope];
10141
+ const results = [];
10142
+ for (const item of scopes) {
10143
+ if (item === "tasks")
10144
+ results.push(...toResults("tasks", searchTaskEntities(filters, d)));
10145
+ if (item === "projects")
10146
+ results.push(...toResults("projects", searchProjects(filters, d)));
10147
+ if (item === "plans")
10148
+ results.push(...toResults("plans", searchPlans(filters, d)));
10149
+ if (item === "runs")
10150
+ results.push(...toResults("runs", searchRuns(filters, d)));
10151
+ if (item === "comments")
10152
+ results.push(...toResults("comments", searchComments(filters, d)));
10153
+ }
10154
+ return {
10155
+ scope: normalizedScope,
10156
+ filters,
10157
+ count: results.length,
10158
+ results: results.slice(0, normalizeLimit(filters.limit))
10159
+ };
10160
+ }
10161
+ function saveSearchView(input, db) {
10162
+ const d = db || getDatabase();
10163
+ const name = normalizeName(input.name);
10164
+ const timestamp = now();
10165
+ const existing = getSearchView(name, d);
10166
+ if (existing) {
10167
+ d.run(`UPDATE saved_search_views
10168
+ SET description = ?, scope = ?, filters = ?, updated_at = ?
10169
+ WHERE id = ?`, [
10170
+ input.description ?? existing.description,
10171
+ normalizeScope(input.scope ?? existing.scope),
10172
+ JSON.stringify(input.filters ?? existing.filters),
10173
+ timestamp,
10174
+ existing.id
10175
+ ]);
10176
+ return getSearchView(existing.id, d);
10177
+ }
10178
+ const id = uuid();
10179
+ d.run(`INSERT INTO saved_search_views (id, name, description, scope, filters, created_at, updated_at)
10180
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
10181
+ id,
10182
+ name,
10183
+ input.description ?? null,
10184
+ normalizeScope(input.scope),
10185
+ JSON.stringify(input.filters ?? {}),
10186
+ timestamp,
10187
+ timestamp
10188
+ ]);
10189
+ return getSearchView(id, d);
10190
+ }
10191
+ function getSearchView(idOrName, db) {
10192
+ const d = db || getDatabase();
10193
+ const row = d.query("SELECT * FROM saved_search_views WHERE id = ? OR name = ?").get(idOrName, idOrName);
10194
+ return row ? rowToSavedSearchView(row) : null;
10195
+ }
10196
+ function listSearchViews(scope, db) {
10197
+ const d = db || getDatabase();
10198
+ const normalizedScope = scope ? normalizeScope(scope) : null;
10199
+ const rows = normalizedScope ? d.query("SELECT * FROM saved_search_views WHERE scope = ? ORDER BY name").all(normalizedScope) : d.query("SELECT * FROM saved_search_views ORDER BY name").all();
10200
+ return rows.map(rowToSavedSearchView);
10201
+ }
10202
+ function deleteSearchView(idOrName, db) {
10203
+ const d = db || getDatabase();
10204
+ const result = d.run("DELETE FROM saved_search_views WHERE id = ? OR name = ?", [idOrName, idOrName]);
10205
+ return result.changes > 0;
10206
+ }
10207
+ function runSearchView(idOrName, db) {
10208
+ const d = db || getDatabase();
10209
+ const view = getSearchView(idOrName, d);
10210
+ if (!view)
10211
+ throw new Error(`Saved search view not found: ${idOrName}`);
10212
+ return { ...runSavedSearch(view.filters, view.scope, d), view };
10213
+ }
10214
+ var init_saved_search_views = __esm(() => {
10215
+ init_database();
10216
+ init_local_fields();
10217
+ init_search();
10218
+ });
10219
+
9550
10220
  // src/lib/claude-tasks.ts
9551
- import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
9552
- 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";
9553
10223
  function getTaskListDir(taskListId) {
9554
- return join5(HOME, ".claude", "tasks", taskListId);
10224
+ return join6(HOME, ".claude", "tasks", taskListId);
9555
10225
  }
9556
10226
  function readClaudeTask(dir, filename) {
9557
- return readJsonFile(join5(dir, filename));
10227
+ return readJsonFile(join6(dir, filename));
9558
10228
  }
9559
10229
  function writeClaudeTask(dir, task) {
9560
- writeJsonFile(join5(dir, `${task.id}.json`), task);
10230
+ writeJsonFile(join6(dir, `${task.id}.json`), task);
9561
10231
  }
9562
10232
  function toClaudeStatus(status) {
9563
10233
  if (status === "pending" || status === "in_progress" || status === "completed") {
@@ -9569,14 +10239,14 @@ function toSqliteStatus(status) {
9569
10239
  return status;
9570
10240
  }
9571
10241
  function readPrefixCounter(dir) {
9572
- const path = join5(dir, ".prefix-counter");
10242
+ const path = join6(dir, ".prefix-counter");
9573
10243
  if (!existsSync5(path))
9574
10244
  return 0;
9575
10245
  const val = parseInt(readFileSync3(path, "utf-8").trim(), 10);
9576
10246
  return isNaN(val) ? 0 : val;
9577
10247
  }
9578
10248
  function writePrefixCounter(dir, value) {
9579
- writeFileSync2(join5(dir, ".prefix-counter"), String(value));
10249
+ writeFileSync3(join6(dir, ".prefix-counter"), String(value));
9580
10250
  }
9581
10251
  function formatPrefixedSubject(title, prefix, counter) {
9582
10252
  const padded = String(counter).padStart(5, "0");
@@ -9612,7 +10282,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
9612
10282
  const existingByTodosId = new Map;
9613
10283
  const files = listJsonFiles(dir);
9614
10284
  for (const f of files) {
9615
- const path = join5(dir, f);
10285
+ const path = join6(dir, f);
9616
10286
  const ct = readClaudeTask(dir, f);
9617
10287
  if (ct?.metadata?.["todos_id"]) {
9618
10288
  existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
@@ -9719,7 +10389,7 @@ function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
9719
10389
  }
9720
10390
  for (const f of files) {
9721
10391
  try {
9722
- const filePath = join5(dir, f);
10392
+ const filePath = join6(dir, f);
9723
10393
  const ct = readClaudeTask(dir, f);
9724
10394
  if (!ct)
9725
10395
  continue;
@@ -9793,26 +10463,26 @@ var init_claude_tasks = __esm(() => {
9793
10463
 
9794
10464
  // src/lib/agent-tasks.ts
9795
10465
  import { existsSync as existsSync6 } from "fs";
9796
- import { join as join6 } from "path";
10466
+ import { join as join7 } from "path";
9797
10467
  function getTodosGlobalDir2() {
9798
- const newDir = join6(HOME, ".hasna", "todos");
9799
- const legacyDir = join6(HOME, ".todos");
10468
+ const newDir = join7(HOME, ".hasna", "todos");
10469
+ const legacyDir = join7(HOME, ".todos");
9800
10470
  if (!existsSync6(newDir) && existsSync6(legacyDir))
9801
10471
  return legacyDir;
9802
10472
  return newDir;
9803
10473
  }
9804
10474
  function agentBaseDir(agent) {
9805
10475
  const key = `TODOS_${agent.toUpperCase()}_TASKS_DIR`;
9806
- 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");
9807
10477
  }
9808
10478
  function getTaskListDir2(agent, taskListId) {
9809
- return join6(agentBaseDir(agent), agent, taskListId);
10479
+ return join7(agentBaseDir(agent), agent, taskListId);
9810
10480
  }
9811
10481
  function readAgentTask(dir, filename) {
9812
- return readJsonFile(join6(dir, filename));
10482
+ return readJsonFile(join7(dir, filename));
9813
10483
  }
9814
10484
  function writeAgentTask(dir, task) {
9815
- writeJsonFile(join6(dir, `${task.id}.json`), task);
10485
+ writeJsonFile(join7(dir, `${task.id}.json`), task);
9816
10486
  }
9817
10487
  function taskToAgentTask(task, externalId, existingMeta) {
9818
10488
  return {
@@ -9846,7 +10516,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
9846
10516
  const existingByTodosId = new Map;
9847
10517
  const files = listJsonFiles(dir);
9848
10518
  for (const f of files) {
9849
- const path = join6(dir, f);
10519
+ const path = join7(dir, f);
9850
10520
  const at = readAgentTask(dir, f);
9851
10521
  if (at?.metadata?.["todos_id"]) {
9852
10522
  existingByTodosId.set(at.metadata["todos_id"], { task: at, mtimeMs: getFileMtimeMs(path) });
@@ -9939,7 +10609,7 @@ function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
9939
10609
  }
9940
10610
  for (const f of files) {
9941
10611
  try {
9942
- const filePath = join6(dir, f);
10612
+ const filePath = join7(dir, f);
9943
10613
  const at = readAgentTask(dir, f);
9944
10614
  if (!at)
9945
10615
  continue;
@@ -10350,7 +11020,7 @@ __export(exports_extract, {
10350
11020
  EXTRACT_TAGS: () => EXTRACT_TAGS
10351
11021
  });
10352
11022
  import { readFileSync as readFileSync5, statSync as statSync3 } from "fs";
10353
- 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";
10354
11024
  function tagToPriority(tag) {
10355
11025
  switch (tag) {
10356
11026
  case "BUG":
@@ -10422,7 +11092,7 @@ function extractTodos(options, db) {
10422
11092
  const files = collectFiles(basePath, extensions);
10423
11093
  const allComments = [];
10424
11094
  for (const file of files) {
10425
- const fullPath = statSync3(basePath).isFile() ? basePath : join7(basePath, file);
11095
+ const fullPath = statSync3(basePath).isFile() ? basePath : join8(basePath, file);
10426
11096
  try {
10427
11097
  const source = readFileSync5(fullPath, "utf-8");
10428
11098
  const relPath = statSync3(basePath).isFile() ? relative3(resolve7(basePath, ".."), fullPath) : file;
@@ -10549,8 +11219,8 @@ var init_extract = __esm(() => {
10549
11219
 
10550
11220
  // src/lib/artifact-store.ts
10551
11221
  import { createHash as createHash2 } from "crypto";
10552
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
10553
- 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";
10554
11224
  import { tmpdir } from "os";
10555
11225
  function isInMemoryDb2(path) {
10556
11226
  return path === ":memory:" || path.startsWith("file::memory:");
@@ -10562,15 +11232,15 @@ function artifactStoreRoot() {
10562
11232
  return resolve8(process.env["TODOS_ARTIFACTS_DIR"]);
10563
11233
  const dbPath = getDatabasePath();
10564
11234
  if (isInMemoryDb2(dbPath))
10565
- return join8(tmpdir(), "hasna-todos-artifacts");
10566
- return join8(dirname6(resolve8(dbPath)), "artifacts");
11235
+ return join9(tmpdir(), "hasna-todos-artifacts");
11236
+ return join9(dirname6(resolve8(dbPath)), "artifacts");
10567
11237
  }
10568
11238
  function artifactStorePath(relativePath) {
10569
11239
  const normalized = relativePath.replace(/\\/g, "/");
10570
11240
  if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
10571
11241
  throw new Error("Invalid artifact store path");
10572
11242
  }
10573
- return join8(artifactStoreRoot(), normalized);
11243
+ return join9(artifactStoreRoot(), normalized);
10574
11244
  }
10575
11245
  function sha256(buffer) {
10576
11246
  return createHash2("sha256").update(buffer).digest("hex");
@@ -10628,11 +11298,11 @@ function storeArtifactContent(input) {
10628
11298
  redactionStatus = "redacted";
10629
11299
  }
10630
11300
  const storedSha = sha256(storedBuffer);
10631
- const relativePath = join8("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
11301
+ const relativePath = join9("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
10632
11302
  const destination = artifactStorePath(relativePath);
10633
11303
  if (!existsSync8(destination)) {
10634
- mkdirSync4(dirname6(destination), { recursive: true });
10635
- writeFileSync3(destination, storedBuffer);
11304
+ mkdirSync5(dirname6(destination), { recursive: true });
11305
+ writeFileSync4(destination, storedBuffer);
10636
11306
  }
10637
11307
  const createdAt = input.created_at || new Date().toISOString();
10638
11308
  const retentionDays = input.retention_days ?? null;
@@ -10749,8 +11419,8 @@ function importStoredArtifactContent(content) {
10749
11419
  };
10750
11420
  }
10751
11421
  const destination = artifactStorePath(content.relative_path);
10752
- mkdirSync4(dirname6(destination), { recursive: true });
10753
- writeFileSync3(destination, buffer);
11422
+ mkdirSync5(dirname6(destination), { recursive: true });
11423
+ writeFileSync4(destination, buffer);
10754
11424
  return {
10755
11425
  id: content.artifact_id,
10756
11426
  path: content.relative_path,
@@ -10786,7 +11456,7 @@ function packageSource(version) {
10786
11456
  function emptyCounts() {
10787
11457
  return Object.fromEntries(dataKeys.map((key) => [key, 0]));
10788
11458
  }
10789
- function parseJsonObject(value) {
11459
+ function parseJsonObject2(value) {
10790
11460
  if (!value)
10791
11461
  return {};
10792
11462
  if (typeof value === "object" && !Array.isArray(value))
@@ -10832,22 +11502,25 @@ function rowToTask3(row) {
10832
11502
  return {
10833
11503
  ...row,
10834
11504
  tags: parseJsonArray(row.tags),
10835
- metadata: parseJsonObject(row.metadata),
11505
+ metadata: parseJsonObject2(row.metadata),
10836
11506
  requires_approval: Boolean(row.requires_approval)
10837
11507
  };
10838
11508
  }
10839
11509
  function rowToTaskList2(row) {
10840
- return { ...row, metadata: parseJsonObject(row.metadata) };
11510
+ return { ...row, metadata: parseJsonObject2(row.metadata) };
10841
11511
  }
10842
11512
  function rowWithMetadata(row) {
10843
- return { ...row, metadata: parseJsonObject(row.metadata) };
11513
+ return { ...row, metadata: parseJsonObject2(row.metadata) };
10844
11514
  }
10845
11515
  function rowToRunEvent(row) {
10846
- return { ...row, data: parseJsonObject(row.data) };
11516
+ return { ...row, data: parseJsonObject2(row.data) };
10847
11517
  }
10848
11518
  function rowToCommit2(row) {
10849
11519
  return { ...row, files_changed: row.files_changed ? parseJsonArray(row.files_changed) : null };
10850
11520
  }
11521
+ function rowToSavedView(row) {
11522
+ return { ...row, filters: parseJsonObject2(row.filters) };
11523
+ }
10851
11524
  function bridgeStats(data) {
10852
11525
  return Object.fromEntries(dataKeys.map((key) => [key, data[key].length]));
10853
11526
  }
@@ -10871,7 +11544,8 @@ function createLocalBridgeBundle(options = {}, db) {
10871
11544
  task_files: queryByTaskIds(d, "SELECT * FROM task_files WHERE task_id IN (__TASK_IDS__) ORDER BY path, id", taskIds),
10872
11545
  task_commits: queryByTaskIds(d, "SELECT * FROM task_commits WHERE task_id IN (__TASK_IDS__) ORDER BY created_at, id", taskIds).map(rowToCommit2),
10873
11546
  task_git_refs: queryByTaskIds(d, "SELECT * FROM task_git_refs WHERE task_id IN (__TASK_IDS__) ORDER BY created_at, id", taskIds).map(rowWithMetadata),
10874
- task_verifications: queryByTaskIds(d, "SELECT * FROM task_verifications WHERE task_id IN (__TASK_IDS__) ORDER BY run_at, id", taskIds)
11547
+ task_verifications: queryByTaskIds(d, "SELECT * FROM task_verifications WHERE task_id IN (__TASK_IDS__) ORDER BY run_at, id", taskIds),
11548
+ saved_views: (options.project_id ? d.query("SELECT * FROM saved_search_views WHERE json_extract(filters, '$.project_id') = ? ORDER BY name").all(options.project_id) : d.query("SELECT * FROM saved_search_views ORDER BY name").all()).map(rowToSavedView)
10875
11549
  };
10876
11550
  const artifactContents = data.run_artifacts.map((artifact) => exportStoredArtifactContent({
10877
11551
  id: artifact.id,
@@ -10910,6 +11584,8 @@ function validateLocalBridgeBundle(value) {
10910
11584
  issues.push("data must be an object");
10911
11585
  } else {
10912
11586
  for (const key of dataKeys) {
11587
+ if (key === "saved_views" && data[key] === undefined)
11588
+ continue;
10913
11589
  if (!Array.isArray(data[key]))
10914
11590
  issues.push(`data.${key} must be an array`);
10915
11591
  }
@@ -11082,7 +11758,8 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
11082
11758
  const conflictStrategy = options.conflictStrategy ?? "skip";
11083
11759
  const data = {
11084
11760
  ...bundle.data,
11085
- tasks: sortedTasks(bundle.data.tasks)
11761
+ tasks: sortedTasks(bundle.data.tasks),
11762
+ saved_views: bundle.data.saved_views ?? []
11086
11763
  };
11087
11764
  for (const key of dataKeys) {
11088
11765
  for (const row of data[key]) {
@@ -11153,7 +11830,8 @@ var init_local_bridge = __esm(() => {
11153
11830
  "task_files",
11154
11831
  "task_commits",
11155
11832
  "task_git_refs",
11156
- "task_verifications"
11833
+ "task_verifications",
11834
+ "saved_views"
11157
11835
  ];
11158
11836
  insertColumns = {
11159
11837
  projects: ["id", "name", "path", "description", "task_list_id", "task_prefix", "task_counter", "created_at", "updated_at", "machine_id", "synced_at"],
@@ -11225,7 +11903,8 @@ var init_local_bridge = __esm(() => {
11225
11903
  task_files: ["id", "task_id", "path", "status", "agent_id", "note", "created_at", "updated_at", "machine_id"],
11226
11904
  task_commits: ["id", "task_id", "sha", "message", "author", "files_changed", "committed_at", "created_at"],
11227
11905
  task_git_refs: ["id", "task_id", "ref_type", "name", "url", "provider", "metadata", "created_at", "updated_at"],
11228
- task_verifications: ["id", "task_id", "command", "status", "output_summary", "artifact_path", "agent_id", "run_at", "created_at"]
11906
+ task_verifications: ["id", "task_id", "command", "status", "output_summary", "artifact_path", "agent_id", "run_at", "created_at"],
11907
+ saved_views: ["id", "name", "description", "scope", "filters", "created_at", "updated_at"]
11229
11908
  };
11230
11909
  tableByKey = {
11231
11910
  projects: "projects",
@@ -11241,9 +11920,10 @@ var init_local_bridge = __esm(() => {
11241
11920
  task_files: "task_files",
11242
11921
  task_commits: "task_commits",
11243
11922
  task_git_refs: "task_git_refs",
11244
- task_verifications: "task_verifications"
11923
+ task_verifications: "task_verifications",
11924
+ saved_views: "saved_search_views"
11245
11925
  };
11246
- jsonColumns = new Set(["metadata", "tags", "data", "files_changed"]);
11926
+ jsonColumns = new Set(["metadata", "tags", "data", "files_changed", "filters"]);
11247
11927
  });
11248
11928
 
11249
11929
  // src/lib/local-encryption.ts
@@ -11818,7 +12498,8 @@ function emptyCounts2() {
11818
12498
  task_files: 0,
11819
12499
  task_commits: 0,
11820
12500
  task_git_refs: 0,
11821
- task_verifications: 0
12501
+ task_verifications: 0,
12502
+ saved_views: 0
11822
12503
  };
11823
12504
  }
11824
12505
  function bridgeToMarkdownPayload(bundle) {
@@ -12069,6 +12750,61 @@ __export(exports_project_commands, {
12069
12750
  });
12070
12751
  import chalk4 from "chalk";
12071
12752
  import { basename as basename3, resolve as resolve9 } from "path";
12753
+ function collectOption(value, previous = []) {
12754
+ return [...previous, value];
12755
+ }
12756
+ function splitList(value) {
12757
+ if (!value)
12758
+ return;
12759
+ const values = Array.isArray(value) ? value : [value];
12760
+ const items = values.flatMap((item) => item.split(",").map((part) => part.trim()).filter(Boolean));
12761
+ return items.length > 0 ? items : undefined;
12762
+ }
12763
+ function parseJsonObjectOption(value, label) {
12764
+ if (!value)
12765
+ return;
12766
+ try {
12767
+ const parsed = JSON.parse(value);
12768
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
12769
+ return parsed;
12770
+ } catch {}
12771
+ throw new Error(`${label} must be a JSON object`);
12772
+ }
12773
+ function buildSearchFilters(query, opts, projectId) {
12774
+ const filterPatch = parseJsonObjectOption(opts.filter, "--filter");
12775
+ const customFields = parseJsonObjectOption(opts.fieldCustom, "--field-custom");
12776
+ const labels = splitList(opts.fieldLabel);
12777
+ const tags = splitList(opts.tag);
12778
+ const statuses = splitList(opts.status)?.map(normalizeStatus);
12779
+ const filters = {
12780
+ query: query || opts.query,
12781
+ project_id: opts.allProjects ? undefined : projectId,
12782
+ status: statuses,
12783
+ priority: splitList(opts.priority),
12784
+ assigned_to: opts.assigned,
12785
+ agent_id: opts.agentId,
12786
+ task_list_id: opts.taskList,
12787
+ plan_id: opts.plan,
12788
+ task_id: opts.task,
12789
+ tags,
12790
+ created_after: opts.createdAfter,
12791
+ updated_after: opts.since || opts.updatedAfter,
12792
+ has_dependencies: opts.hasDeps ? true : undefined,
12793
+ is_blocked: opts.blocked ? true : undefined,
12794
+ depends_on: opts.dependsOn,
12795
+ blocks: opts.blocks,
12796
+ limit: opts.limit ? Number(opts.limit) : undefined,
12797
+ local_fields: labels || opts.fieldOwner || opts.fieldArea || opts.fieldSeverity || customFields ? {
12798
+ labels,
12799
+ owner: opts.fieldOwner,
12800
+ area: opts.fieldArea,
12801
+ severity: opts.fieldSeverity,
12802
+ custom: customFields
12803
+ } : undefined,
12804
+ ...filterPatch
12805
+ };
12806
+ return Object.fromEntries(Object.entries(filters).filter(([, value]) => value !== undefined));
12807
+ }
12072
12808
  function registerProjectCommands(program2) {
12073
12809
  program2.command("project-bootstrap [path]").description("Discover a local workspace and initialize project task state").option("--name <name>", "Project display name").option("--task-list <slug>", "Default task list slug").option("--dry-run", "Show discovery without writing local state").action(async (inputPath, opts) => {
12074
12810
  const globalOpts = program2.opts();
@@ -12121,35 +12857,126 @@ function registerProjectCommands(program2) {
12121
12857
  console.log(chalk4.green("Comment added."));
12122
12858
  }
12123
12859
  });
12124
- program2.command("search <query>").description("Search tasks").option("--status <status>", "Filter by status").option("--priority <p>", "Filter by priority").option("--assigned <agent>", "Filter by assigned agent").option("--since <date>", "Only tasks updated after this date (ISO)").option("--blocked", "Only blocked tasks (incomplete dependencies)").option("--has-deps", "Only tasks with dependencies").action((query, opts) => {
12860
+ program2.command("search <query>").description("Search local tasks, or run/save a cross-entity search view").option("--status <status>", "Filter by status").option("--priority <p>", "Filter by priority").option("--assigned <agent>", "Filter by assigned agent").option("--agent-id <agent>", "Filter by creator/run/comment agent").option("--task-list <id>", "Filter by task list").option("--plan <id>", "Filter by plan").option("--task <id>", "Filter runs/comments by task").option("--tag <tag>", "Filter by task tag (repeatable or comma-separated)", collectOption, []).option("--field-label <label>", "Filter by local field label (repeatable or comma-separated)", collectOption, []).option("--field-owner <owner>", "Filter by local field owner").option("--field-area <area>", "Filter by local field area").option("--field-severity <severity>", "Filter by local field severity").option("--field-custom <json>", "Filter by local custom fields as JSON").option("--since <date>", "Only tasks updated after this date (ISO)").option("--created-after <date>", "Only records created after this date (ISO)").option("--blocked", "Only blocked tasks (incomplete dependencies)").option("--has-deps", "Only tasks with dependencies").option("--depends-on <id>", "Only tasks that depend on a task").option("--blocks <id>", "Only tasks that block a task").option("--scope <scope>", "Search scope: tasks, projects, plans, runs, comments, all", "tasks").option("--limit <n>", "Maximum results", "100").option("--filter <json>", "Merge an advanced saved-search filter JSON object").option("--save-as <name>", "Save this search as a named view").option("--description <text>", "Saved view description").option("--all-projects", "Do not auto-scope the search to the current project").action((query, opts) => {
12861
+ const globalOpts = program2.opts();
12862
+ try {
12863
+ const projectId = opts.allProjects ? undefined : autoProject(globalOpts);
12864
+ const scope = normalizeScope(opts.scope);
12865
+ const searchOpts = buildSearchFilters(query, opts, projectId);
12866
+ if (opts.saveAs) {
12867
+ const view = saveSearchView({
12868
+ name: opts.saveAs,
12869
+ description: opts.description,
12870
+ scope,
12871
+ filters: searchOpts
12872
+ });
12873
+ output(view, Boolean(globalOpts.json));
12874
+ if (!globalOpts.json)
12875
+ console.log(chalk4.green(`Saved view ${view.name}.`));
12876
+ return;
12877
+ }
12878
+ if (scope !== "tasks") {
12879
+ const result = runSavedSearch(searchOpts, scope);
12880
+ if (globalOpts.json) {
12881
+ output(result, true);
12882
+ return;
12883
+ }
12884
+ if (result.count === 0) {
12885
+ console.log(chalk4.dim(`No ${scope} results matching "${query}".`));
12886
+ return;
12887
+ }
12888
+ console.log(chalk4.bold(`${result.count} ${scope} result(s) for "${query}":
12889
+ `));
12890
+ for (const item of result.results) {
12891
+ const entity = item.entity;
12892
+ console.log(`${chalk4.cyan(item.entity_type)} ${entity.id?.slice?.(0, 8) || ""} ${entity.name || entity.title || entity.content || entity.summary || ""}`);
12893
+ }
12894
+ return;
12895
+ }
12896
+ const tasks = runSavedSearch(searchOpts, "tasks").results.map((item) => item.entity);
12897
+ if (globalOpts.json) {
12898
+ output(tasks, true);
12899
+ return;
12900
+ }
12901
+ if (tasks.length === 0) {
12902
+ console.log(chalk4.dim(`No tasks matching "${query}".`));
12903
+ return;
12904
+ }
12905
+ console.log(chalk4.bold(`${tasks.length} result(s) for "${query}":
12906
+ `));
12907
+ for (const t of tasks) {
12908
+ console.log(formatTaskLine(t));
12909
+ }
12910
+ } catch (e) {
12911
+ handleError(e);
12912
+ }
12913
+ });
12914
+ const views = program2.command("views").description("Manage local saved search views");
12915
+ views.command("save <name>").description("Save a local search view").option("--query <query>", "Search query").option("--scope <scope>", "Search scope: tasks, projects, plans, runs, comments, all", "tasks").option("--description <text>", "Description").option("--status <status>", "Filter by status").option("--priority <p>", "Filter by priority").option("--assigned <agent>", "Filter by assigned agent").option("--agent-id <agent>", "Filter by creator/run/comment agent").option("--task-list <id>", "Filter by task list").option("--plan <id>", "Filter by plan").option("--task <id>", "Filter runs/comments by task").option("--tag <tag>", "Filter by task tag (repeatable or comma-separated)", collectOption, []).option("--field-label <label>", "Filter by local field label (repeatable or comma-separated)", collectOption, []).option("--field-owner <owner>", "Filter by local field owner").option("--field-area <area>", "Filter by local field area").option("--field-severity <severity>", "Filter by local field severity").option("--field-custom <json>", "Filter by local custom fields as JSON").option("--since <date>", "Only records updated after this date (ISO)").option("--created-after <date>", "Only records created after this date (ISO)").option("--blocked", "Only blocked tasks").option("--has-deps", "Only tasks with dependencies").option("--depends-on <id>", "Only tasks that depend on a task").option("--blocks <id>", "Only tasks that block a task").option("--limit <n>", "Maximum results", "100").option("--filter <json>", "Merge an advanced saved-search filter JSON object").option("--all-projects", "Do not auto-scope the view to the current project").action((name, opts) => {
12916
+ const globalOpts = program2.opts();
12917
+ try {
12918
+ const projectId = opts.allProjects ? undefined : autoProject(globalOpts);
12919
+ const view = saveSearchView({
12920
+ name,
12921
+ description: opts.description,
12922
+ scope: normalizeScope(opts.scope),
12923
+ filters: buildSearchFilters(opts.query, opts, projectId)
12924
+ });
12925
+ output(view, Boolean(globalOpts.json));
12926
+ if (!globalOpts.json)
12927
+ console.log(chalk4.green(`Saved view ${view.name}.`));
12928
+ } catch (e) {
12929
+ handleError(e);
12930
+ }
12931
+ });
12932
+ views.command("list").description("List local saved search views").option("--scope <scope>", "Filter by scope").action((opts) => {
12933
+ const globalOpts = program2.opts();
12934
+ try {
12935
+ const rows = listSearchViews(opts.scope ? normalizeScope(opts.scope) : undefined);
12936
+ output(rows, Boolean(globalOpts.json));
12937
+ if (!globalOpts.json) {
12938
+ if (rows.length === 0) {
12939
+ console.log(chalk4.dim("No saved search views."));
12940
+ return;
12941
+ }
12942
+ for (const row of rows)
12943
+ console.log(`${chalk4.cyan(row.name)} ${chalk4.dim(`[${row.scope}]`)} ${JSON.stringify(row.filters)}`);
12944
+ }
12945
+ } catch (e) {
12946
+ handleError(e);
12947
+ }
12948
+ });
12949
+ views.command("run <name>").description("Run a local saved search view").action((name) => {
12950
+ const globalOpts = program2.opts();
12951
+ try {
12952
+ const result = runSearchView(name);
12953
+ if (globalOpts.json) {
12954
+ output(result, true);
12955
+ return;
12956
+ }
12957
+ console.log(chalk4.bold(`${result.count} result(s) for view "${result.view?.name || name}":
12958
+ `));
12959
+ for (const item of result.results) {
12960
+ if (item.entity_type === "tasks") {
12961
+ console.log(formatTaskLine(item.entity));
12962
+ continue;
12963
+ }
12964
+ const entity = item.entity;
12965
+ console.log(`${chalk4.cyan(item.entity_type)} ${entity.id?.slice?.(0, 8) || ""} ${entity.name || entity.title || entity.content || entity.summary || ""}`);
12966
+ }
12967
+ } catch (e) {
12968
+ handleError(e);
12969
+ }
12970
+ });
12971
+ views.command("delete <name>").description("Delete a local saved search view").action((name) => {
12125
12972
  const globalOpts = program2.opts();
12126
- const projectId = autoProject(globalOpts);
12127
- const searchOpts = { query, project_id: projectId };
12128
- if (opts.status)
12129
- searchOpts.status = normalizeStatus(opts.status);
12130
- if (opts.priority)
12131
- searchOpts.priority = opts.priority;
12132
- if (opts.assigned)
12133
- searchOpts.assigned_to = opts.assigned;
12134
- if (opts.since)
12135
- searchOpts.updated_after = opts.since;
12136
- if (opts.blocked)
12137
- searchOpts.is_blocked = true;
12138
- if (opts.hasDeps)
12139
- searchOpts.has_dependencies = true;
12140
- const tasks = searchTasks(searchOpts);
12141
- if (globalOpts.json) {
12142
- output(tasks, true);
12143
- return;
12144
- }
12145
- if (tasks.length === 0) {
12146
- console.log(chalk4.dim(`No tasks matching "${query}".`));
12147
- return;
12148
- }
12149
- console.log(chalk4.bold(`${tasks.length} result(s) for "${query}":
12150
- `));
12151
- for (const t of tasks) {
12152
- console.log(formatTaskLine(t));
12973
+ try {
12974
+ const deleted = deleteSearchView(name);
12975
+ output({ deleted }, Boolean(globalOpts.json));
12976
+ if (!globalOpts.json)
12977
+ console.log(deleted ? chalk4.green(`Deleted view ${name}.`) : chalk4.dim(`View not found: ${name}`));
12978
+ } catch (e) {
12979
+ handleError(e);
12153
12980
  }
12154
12981
  });
12155
12982
  program2.command("deps <id>").description("Manage task dependencies").option("--needs <dep-id>", "Add dependency (this task needs dep-id)").option("--remove <dep-id>", "Remove dependency").option("--graph", "Show the dependency graph instead of direct edges").option("--direction <direction>", "Graph direction: up, down, or both", "both").action(async (id, opts) => {
@@ -12415,8 +13242,8 @@ function registerProjectCommands(program2) {
12415
13242
  const projectId = autoProject(globalOpts);
12416
13243
  const writeOutput = async (content) => {
12417
13244
  if (opts.output) {
12418
- const { writeFileSync: writeFileSync4 } = await import("fs");
12419
- writeFileSync4(resolve9(opts.output), content.endsWith(`
13245
+ const { writeFileSync: writeFileSync5 } = await import("fs");
13246
+ writeFileSync5(resolve9(opts.output), content.endsWith(`
12420
13247
  `) ? content : `${content}
12421
13248
  `);
12422
13249
  } else {
@@ -12576,7 +13403,7 @@ var init_project_commands = __esm(() => {
12576
13403
  init_database();
12577
13404
  init_projects();
12578
13405
  init_comments();
12579
- init_search();
13406
+ init_saved_search_views();
12580
13407
  init_sync();
12581
13408
  init_config();
12582
13409
  init_helpers();
@@ -14205,8 +15032,8 @@ var exports_doctor = {};
14205
15032
  __export(exports_doctor, {
14206
15033
  runTodosDoctor: () => runTodosDoctor
14207
15034
  });
14208
- import { chmodSync, copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync5, statSync as statSync5 } from "fs";
14209
- 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";
14210
15037
  function tableExists(db, table) {
14211
15038
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
14212
15039
  }
@@ -14363,13 +15190,13 @@ function createBackup(dbPath) {
14363
15190
  if (!existsSync9(dbPath))
14364
15191
  return;
14365
15192
  const stamp = now().replace(/[:.]/g, "-");
14366
- const backupDir = join9(dirname7(dbPath), `${basename4(dbPath)}.backup-${stamp}`);
15193
+ const backupDir = join10(dirname7(dbPath), `${basename4(dbPath)}.backup-${stamp}`);
14367
15194
  const files = [];
14368
- mkdirSync5(backupDir, { recursive: true });
15195
+ mkdirSync6(backupDir, { recursive: true });
14369
15196
  for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
14370
15197
  if (!existsSync9(source))
14371
15198
  continue;
14372
- const target = join9(backupDir, basename4(source));
15199
+ const target = join10(backupDir, basename4(source));
14373
15200
  copyFileSync(source, target);
14374
15201
  files.push(target);
14375
15202
  }
@@ -14595,7 +15422,7 @@ var init_doctor = __esm(() => {
14595
15422
  });
14596
15423
 
14597
15424
  // src/server/routes.ts
14598
- import { join as join10, resolve as resolve11, sep } from "path";
15425
+ import { join as join11, resolve as resolve11, sep } from "path";
14599
15426
  function parseFieldsParam(url) {
14600
15427
  const fieldsParam = url.searchParams.get("fields");
14601
15428
  return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
@@ -15295,7 +16122,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
15295
16122
  if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
15296
16123
  return null;
15297
16124
  if (path !== "/") {
15298
- const filePath = join10(ctx.dashboardDir, path);
16125
+ const filePath = join11(ctx.dashboardDir, path);
15299
16126
  const resolvedFile = resolve11(filePath);
15300
16127
  const resolvedBase = resolve11(ctx.dashboardDir);
15301
16128
  if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
@@ -15305,7 +16132,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
15305
16132
  if (res2)
15306
16133
  return res2;
15307
16134
  }
15308
- const indexPath = join10(ctx.dashboardDir, "index.html");
16135
+ const indexPath = join11(ctx.dashboardDir, "index.html");
15309
16136
  const res = serveStaticFile2(indexPath);
15310
16137
  if (res)
15311
16138
  return res;
@@ -15335,26 +16162,26 @@ __export(exports_serve, {
15335
16162
  MIME_TYPES: () => MIME_TYPES
15336
16163
  });
15337
16164
  import { existsSync as existsSync10 } from "fs";
15338
- import { join as join11, dirname as dirname8, extname } from "path";
16165
+ import { join as join12, dirname as dirname8, extname } from "path";
15339
16166
  import { fileURLToPath as fileURLToPath2 } from "url";
15340
16167
  function resolveDashboardDir() {
15341
16168
  const candidates = [];
15342
16169
  try {
15343
16170
  const scriptDir = dirname8(fileURLToPath2(import.meta.url));
15344
- candidates.push(join11(scriptDir, "..", "dashboard", "dist"));
15345
- candidates.push(join11(scriptDir, "..", "..", "dashboard", "dist"));
16171
+ candidates.push(join12(scriptDir, "..", "dashboard", "dist"));
16172
+ candidates.push(join12(scriptDir, "..", "..", "dashboard", "dist"));
15346
16173
  } catch {}
15347
16174
  if (process.argv[1]) {
15348
16175
  const mainDir = dirname8(process.argv[1]);
15349
- candidates.push(join11(mainDir, "..", "dashboard", "dist"));
15350
- candidates.push(join11(mainDir, "..", "..", "dashboard", "dist"));
16176
+ candidates.push(join12(mainDir, "..", "dashboard", "dist"));
16177
+ candidates.push(join12(mainDir, "..", "..", "dashboard", "dist"));
15351
16178
  }
15352
- candidates.push(join11(process.cwd(), "dashboard", "dist"));
16179
+ candidates.push(join12(process.cwd(), "dashboard", "dist"));
15353
16180
  for (const candidate of candidates) {
15354
16181
  if (existsSync10(candidate))
15355
16182
  return candidate;
15356
16183
  }
15357
- return join11(process.cwd(), "dashboard", "dist");
16184
+ return join12(process.cwd(), "dashboard", "dist");
15358
16185
  }
15359
16186
  function getProvidedApiKey(req) {
15360
16187
  const headerKey = req.headers.get("x-api-key");
@@ -17130,14 +17957,14 @@ __export(exports_config_serve_commands, {
17130
17957
  registerConfigServeCommands: () => registerConfigServeCommands
17131
17958
  });
17132
17959
  import chalk6 from "chalk";
17133
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
17134
- 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";
17135
17962
  function registerConfigServeCommands(program2) {
17136
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) => {
17137
17964
  const globalOpts = program2.opts();
17138
17965
  const home = process.env["HOME"] || "~";
17139
- const newPath = join12(home, ".hasna", "todos", "config.json");
17140
- const legacyPath = join12(home, ".todos", "config.json");
17966
+ const newPath = join13(home, ".hasna", "todos", "config.json");
17967
+ const legacyPath = join13(home, ".todos", "config.json");
17141
17968
  const configPath = !existsSync11(newPath) && existsSync11(legacyPath) ? legacyPath : newPath;
17142
17969
  if (opts.get) {
17143
17970
  const config2 = loadConfig();
@@ -17176,8 +18003,8 @@ function registerConfigServeCommands(program2) {
17176
18003
  obj[keys[keys.length - 1]] = parsedValue;
17177
18004
  const dir = dirname9(configPath);
17178
18005
  if (!existsSync11(dir))
17179
- mkdirSync6(dir, { recursive: true });
17180
- writeFileSync4(configPath, JSON.stringify(config2, null, 2));
18006
+ mkdirSync7(dir, { recursive: true });
18007
+ writeFileSync5(configPath, JSON.stringify(config2, null, 2));
17181
18008
  if (globalOpts.json) {
17182
18009
  output({ key, value: parsedValue }, true);
17183
18010
  } else {
@@ -18614,101 +19441,6 @@ var init_task_dedupe = __esm(() => {
18614
19441
  PY_STACK_RE = /File\s+"([^"]+)",\s+line\s+\d+,\s+in\s+([^\s]+)/i;
18615
19442
  });
18616
19443
 
18617
- // src/lib/local-fields.ts
18618
- function normalizeList(values) {
18619
- return [...new Set((values || []).map((value) => value.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b));
18620
- }
18621
- function metadataFields(task) {
18622
- const value = task.metadata[LOCAL_FIELDS_KEY];
18623
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
18624
- }
18625
- function sameCustomValue(actual, expected) {
18626
- return JSON.stringify(actual) === JSON.stringify(expected);
18627
- }
18628
- function hasOwnField(fields, key) {
18629
- return Object.prototype.hasOwnProperty.call(fields, key);
18630
- }
18631
- function getTaskLocalFields(taskId, db) {
18632
- const d = db || getDatabase();
18633
- const task = getTask(taskId, d);
18634
- if (!task)
18635
- throw new TaskNotFoundError(taskId);
18636
- const fields = metadataFields(task);
18637
- return {
18638
- labels: normalizeList(fields.labels),
18639
- priority: task.priority,
18640
- severity: typeof fields.severity === "string" ? fields.severity : null,
18641
- owner: hasOwnField(fields, "owner") ? typeof fields.owner === "string" ? fields.owner : null : task.assigned_to,
18642
- area: typeof fields.area === "string" ? fields.area : null,
18643
- custom: fields.custom && typeof fields.custom === "object" && !Array.isArray(fields.custom) ? fields.custom : {}
18644
- };
18645
- }
18646
- function setTaskLocalFields(taskId, input, db) {
18647
- const d = db || getDatabase();
18648
- const task = getTask(taskId, d);
18649
- if (!task)
18650
- throw new TaskNotFoundError(taskId);
18651
- const currentFields = getTaskLocalFields(taskId, d);
18652
- const labels = input.labels !== undefined ? normalizeList(input.labels) : currentFields.labels;
18653
- const custom = input.custom !== undefined ? redactValue(input.merge_custom === false ? input.custom : { ...currentFields.custom, ...input.custom }) : currentFields.custom;
18654
- const nextFields = {
18655
- labels,
18656
- priority: input.priority || task.priority,
18657
- severity: input.severity !== undefined ? input.severity : currentFields.severity,
18658
- owner: input.owner !== undefined ? input.owner : currentFields.owner,
18659
- area: input.area !== undefined ? input.area : currentFields.area,
18660
- custom
18661
- };
18662
- const nextMetadata = {
18663
- ...task.metadata,
18664
- [LOCAL_FIELDS_KEY]: nextFields
18665
- };
18666
- const previousLabels = new Set(currentFields.labels);
18667
- const nextTags = normalizeList([...task.tags.filter((tag) => !previousLabels.has(tag)), ...labels]);
18668
- const updates = {
18669
- version: task.version,
18670
- priority: input.priority,
18671
- tags: nextTags,
18672
- metadata: nextMetadata
18673
- };
18674
- if (input.owner !== undefined)
18675
- updates.assigned_to = nextFields.owner;
18676
- return updateTask(taskId, updates, d);
18677
- }
18678
- function queryTasksByLocalFields(query, db) {
18679
- const d = db || getDatabase();
18680
- const tasks = listTasks({
18681
- priority: query.priority,
18682
- tags: query.labels,
18683
- limit: 1e4
18684
- }, d);
18685
- const matches = tasks.filter((task) => {
18686
- const fields = getTaskLocalFields(task.id, d);
18687
- if (query.labels && !query.labels.every((label) => fields.labels.includes(label)))
18688
- return false;
18689
- if (query.severity && fields.severity !== query.severity)
18690
- return false;
18691
- if (query.owner && fields.owner !== query.owner)
18692
- return false;
18693
- if (query.area && fields.area !== query.area)
18694
- return false;
18695
- if (query.custom) {
18696
- for (const [key, expected] of Object.entries(query.custom)) {
18697
- if (!sameCustomValue(fields.custom[key], expected))
18698
- return false;
18699
- }
18700
- }
18701
- return true;
18702
- });
18703
- return matches.slice(0, query.limit || 100);
18704
- }
18705
- var LOCAL_FIELDS_KEY = "local_fields";
18706
- var init_local_fields = __esm(() => {
18707
- init_database();
18708
- init_tasks();
18709
- init_types();
18710
- });
18711
-
18712
19444
  // src/lib/activity-timeline.ts
18713
19445
  var exports_activity_timeline = {};
18714
19446
  __export(exports_activity_timeline, {
@@ -19552,7 +20284,7 @@ __export(exports_query_commands, {
19552
20284
  registerQueryCommands: () => registerQueryCommands
19553
20285
  });
19554
20286
  import chalk7 from "chalk";
19555
- function parseJsonObjectOption(value, label) {
20287
+ function parseJsonObjectOption2(value, label) {
19556
20288
  if (!value)
19557
20289
  return;
19558
20290
  try {
@@ -20083,8 +20815,8 @@ Repairs`));
20083
20815
  const db = getDatabase();
20084
20816
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
20085
20817
  const { statSync: statSync6 } = await import("fs");
20086
- const { join: join13 } = await import("path");
20087
- 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");
20088
20820
  let size = "unknown";
20089
20821
  try {
20090
20822
  size = `${(statSync6(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
@@ -20904,7 +21636,7 @@ Repairs`));
20904
21636
  fields.command("set <task-id>").description("Set local fields for a task").option("--labels <labels>", "Comma-separated labels").option("--priority <priority>", "Priority: low, medium, high, critical").option("--severity <severity>", "Local severity, for example s0, s1, s2").option("--owner <owner>", "Local owner or responsible agent").option("--area <area>", "Local area or component").option("--custom <json>", "Custom fields as a JSON object").option("--field <pairs...>", "Custom key=value pairs").option("--replace-custom", "Replace custom fields instead of merging").option("-j, --json", "Output as JSON").action((taskId, opts) => {
20905
21637
  const globalOpts = program2.opts();
20906
21638
  try {
20907
- const custom = mergeCustomFields(parseJsonObjectOption(opts.custom, "--custom"), parseFieldPairs(opts.field));
21639
+ const custom = mergeCustomFields(parseJsonObjectOption2(opts.custom, "--custom"), parseFieldPairs(opts.field));
20908
21640
  const input = {
20909
21641
  labels: parseCsvOption(opts.labels),
20910
21642
  priority: parsePriority(opts.priority),
@@ -20934,7 +21666,7 @@ Repairs`));
20934
21666
  severity: opts.severity,
20935
21667
  owner: opts.owner,
20936
21668
  area: opts.area,
20937
- custom: mergeCustomFields(parseJsonObjectOption(opts.custom, "--custom"), parseFieldPairs(opts.field)),
21669
+ custom: mergeCustomFields(parseJsonObjectOption2(opts.custom, "--custom"), parseFieldPairs(opts.field)),
20938
21670
  limit: Number(opts.limit)
20939
21671
  };
20940
21672
  const tasks = queryTasksByLocalFields(query);
@@ -21018,7 +21750,7 @@ Repairs`));
21018
21750
  source_type: opts.sourceType,
21019
21751
  source_name: opts.sourceName || opts.file,
21020
21752
  source_url: opts.sourceUrl,
21021
- metadata: parseJsonObjectOption(opts.metadata, "--metadata"),
21753
+ metadata: parseJsonObjectOption2(opts.metadata, "--metadata"),
21022
21754
  project_id: autoProject(globalOpts) || undefined,
21023
21755
  priority: opts.priority,
21024
21756
  tags: opts.tags ? String(opts.tags).split(",").map((tag) => tag.trim()).filter(Boolean) : undefined,
@@ -25810,6 +26542,10 @@ var init_token_utils = __esm(() => {
25810
26542
  "request_task_review",
25811
26543
  "reschedule_task",
25812
26544
  "search_tasks",
26545
+ "save_search_view",
26546
+ "list_search_views",
26547
+ "run_search_view",
26548
+ "delete_search_view",
25813
26549
  "standup",
25814
26550
  "set_task_contract",
25815
26551
  "task_context",
@@ -25999,10 +26735,12 @@ var init_token_utils = __esm(() => {
25999
26735
  "export_template",
26000
26736
  "import_template",
26001
26737
  "init_templates",
26738
+ "list_template_library",
26002
26739
  "list_templates",
26003
26740
  "preview_template",
26004
26741
  "template_history",
26005
- "update_template"
26742
+ "update_template",
26743
+ "write_template_library"
26006
26744
  ],
26007
26745
  webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
26008
26746
  machines: [
@@ -27601,6 +28339,76 @@ ${lines.join(`
27601
28339
  }
27602
28340
  });
27603
28341
  }
28342
+ if (shouldRegisterTool("save_search_view")) {
28343
+ server.tool("save_search_view", "Save a local search view for tasks, projects, plans, runs, comments, or all records.", {
28344
+ name: exports_external.string().describe("Saved view name"),
28345
+ query: exports_external.string().optional().describe("Search query"),
28346
+ scope: exports_external.enum(["all", "tasks", "projects", "plans", "runs", "comments"]).optional(),
28347
+ description: exports_external.string().optional(),
28348
+ project_id: exports_external.string().optional(),
28349
+ status: exports_external.union([exports_external.string(), exports_external.array(exports_external.string())]).optional(),
28350
+ priority: exports_external.union([exports_external.string(), exports_external.array(exports_external.string())]).optional(),
28351
+ assigned_to: exports_external.string().optional(),
28352
+ agent_id: exports_external.string().optional(),
28353
+ tags: exports_external.array(exports_external.string()).optional(),
28354
+ limit: exports_external.number().optional()
28355
+ }, async ({ name, scope, description, ...filters }) => {
28356
+ try {
28357
+ const { saveSearchView: saveSearchView2 } = (init_saved_search_views(), __toCommonJS(exports_saved_search_views));
28358
+ const view = saveSearchView2({
28359
+ name,
28360
+ description,
28361
+ scope,
28362
+ filters: {
28363
+ ...filters,
28364
+ project_id: filters.project_id ? resolveId(filters.project_id, "projects") : undefined
28365
+ }
28366
+ });
28367
+ return { content: [{ type: "text", text: JSON.stringify(view, null, 2) }] };
28368
+ } catch (e) {
28369
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
28370
+ }
28371
+ });
28372
+ }
28373
+ if (shouldRegisterTool("list_search_views")) {
28374
+ server.tool("list_search_views", "List local saved search views.", {
28375
+ scope: exports_external.enum(["all", "tasks", "projects", "plans", "runs", "comments"]).optional()
28376
+ }, async ({ scope }) => {
28377
+ try {
28378
+ const { listSearchViews: listSearchViews2 } = (init_saved_search_views(), __toCommonJS(exports_saved_search_views));
28379
+ const views = listSearchViews2(scope);
28380
+ return { content: [{ type: "text", text: JSON.stringify(views, null, 2) }] };
28381
+ } catch (e) {
28382
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
28383
+ }
28384
+ });
28385
+ }
28386
+ if (shouldRegisterTool("run_search_view")) {
28387
+ server.tool("run_search_view", "Run a local saved search view and return stable JSON results.", {
28388
+ name: exports_external.string().describe("Saved view name or id")
28389
+ }, async ({ name }) => {
28390
+ try {
28391
+ const { runSearchView: runSearchView2 } = (init_saved_search_views(), __toCommonJS(exports_saved_search_views));
28392
+ const result = runSearchView2(name);
28393
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
28394
+ } catch (e) {
28395
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
28396
+ }
28397
+ });
28398
+ }
28399
+ if (shouldRegisterTool("delete_search_view")) {
28400
+ server.tool("delete_search_view", "Delete a local saved search view.", {
28401
+ name: exports_external.string().describe("Saved view name or id")
28402
+ }, async ({ name }) => {
28403
+ try {
28404
+ const { deleteSearchView: deleteSearchView2 } = (init_saved_search_views(), __toCommonJS(exports_saved_search_views));
28405
+ const deleted = deleteSearchView2(name);
28406
+ return { content: [{ type: "text", text: JSON.stringify({ deleted }, null, 2) }] };
28407
+ } catch (e) {
28408
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
28409
+ }
28410
+ });
28411
+ }
27604
28412
  }
27605
28413
  var init_task_project_tools = __esm(() => {
27606
28414
  init_zod();
@@ -28949,6 +29757,10 @@ function registerTaskMetaTools(server, ctx) {
28949
29757
  reschedule_task: "reschedule_task \u2014 Update deadline. Params: task_id, deadline, version",
28950
29758
  prioritize_task: "prioritize_task \u2014 Set priority. Params: task_id, priority, version",
28951
29759
  search_tasks: "search_tasks \u2014 Full-text search. Params: query, project_id, status, limit",
29760
+ save_search_view: "save_search_view \u2014 Save a local search view across tasks, projects, plans, runs, comments, or all records. Params: name, query, scope, description, project_id, status, priority, assigned_to, agent_id, tags, limit",
29761
+ list_search_views: "list_search_views \u2014 List local saved search views. Params: scope",
29762
+ run_search_view: "run_search_view \u2014 Run a local saved search view and return stable JSON results. Params: name",
29763
+ delete_search_view: "delete_search_view \u2014 Delete a local saved search view. Params: name",
28952
29764
  get_my_tasks: "get_my_tasks \u2014 Get tasks for calling agent. Params: agent_id, status, project_id, limit",
28953
29765
  get_next_task: "get_next_task \u2014 Get the next available task without claiming it. Params: agent_id, project_id, task_list_id, plan_id, tags",
28954
29766
  claim_next_task: "claim_next_task \u2014 Atomically claim and start the next available task. Params: agent_id, project_id, task_list_id, plan_id, tags, steal_stale, stale_minutes",
@@ -29281,7 +30093,7 @@ __export(exports_verification_providers, {
29281
30093
  discoverVerificationProviderCapabilities: () => discoverVerificationProviderCapabilities
29282
30094
  });
29283
30095
  import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
29284
- function normalizeName(name) {
30096
+ function normalizeName2(name) {
29285
30097
  const normalized = name.trim().toLowerCase();
29286
30098
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
29287
30099
  throw new Error("verification provider name must use lowercase letters, numbers, dashes, or underscores");
@@ -29300,10 +30112,10 @@ function timeoutMs(value) {
29300
30112
  return Math.max(1, Math.min(24 * 60 * 60000, Math.floor(value)));
29301
30113
  }
29302
30114
  function getProvider(name) {
29303
- return loadConfig().verification_providers?.[normalizeName(name)] || null;
30115
+ return loadConfig().verification_providers?.[normalizeName2(name)] || null;
29304
30116
  }
29305
30117
  function upsertVerificationProvider(input) {
29306
- const name = normalizeName(input.name);
30118
+ const name = normalizeName2(input.name);
29307
30119
  const config = loadConfig();
29308
30120
  const existing = config.verification_providers?.[name];
29309
30121
  const timestamp = new Date().toISOString();
@@ -29333,7 +30145,7 @@ function listVerificationProviders() {
29333
30145
  return Object.values(loadConfig().verification_providers || {}).sort((a, b) => a.name.localeCompare(b.name));
29334
30146
  }
29335
30147
  function removeVerificationProvider(name) {
29336
- const normalized = normalizeName(name);
30148
+ const normalized = normalizeName2(name);
29337
30149
  const config = loadConfig();
29338
30150
  if (!config.verification_providers?.[normalized])
29339
30151
  return false;
@@ -32070,6 +32882,259 @@ var init_agents2 = __esm(() => {
32070
32882
  init_database();
32071
32883
  });
32072
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
+
32073
33138
  // src/mcp/index.ts
32074
33139
  var exports_mcp = {};
32075
33140
  __export(exports_mcp, {
@@ -32238,6 +33303,7 @@ var init_mcp = __esm(() => {
32238
33303
  init_code_tools();
32239
33304
  init_machines2();
32240
33305
  init_agents2();
33306
+ init_templates2();
32241
33307
  init_package_version();
32242
33308
  init_token_utils();
32243
33309
  if (hasVersionFlag()) {
@@ -32268,6 +33334,7 @@ var init_mcp = __esm(() => {
32268
33334
  registerTaskRelTools(server, toolContext);
32269
33335
  registerCodeTools(server, toolContext);
32270
33336
  registerAgentTools(server, { ...toolContext, agentFocusMap });
33337
+ registerTemplateTools(server, toolContext);
32271
33338
  registerMachineTools(server, { shouldRegisterTool, formatError });
32272
33339
  registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError });
32273
33340
  main().catch(async (err) => {
@@ -32287,15 +33354,15 @@ __export(exports_mcp_hooks_commands, {
32287
33354
  });
32288
33355
  import chalk8 from "chalk";
32289
33356
  import { execSync as execSync3 } from "child_process";
32290
- import { existsSync as existsSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, chmodSync as chmodSync2 } from "fs";
32291
- 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";
32292
33359
  function getMcpBinaryPath() {
32293
33360
  try {
32294
33361
  const p = execSync3("which todos-mcp", { encoding: "utf-8" }).trim();
32295
33362
  if (p)
32296
33363
  return p;
32297
33364
  } catch {}
32298
- const bunBin = join13(HOME2, ".bun", "bin", "todos-mcp");
33365
+ const bunBin = join14(HOME2, ".bun", "bin", "todos-mcp");
32299
33366
  if (existsSync13(bunBin))
32300
33367
  return bunBin;
32301
33368
  return "todos-mcp";
@@ -32312,8 +33379,8 @@ function readJsonFile2(path) {
32312
33379
  function writeJsonFile2(path, data) {
32313
33380
  const dir = dirname10(path);
32314
33381
  if (!existsSync13(dir))
32315
- mkdirSync7(dir, { recursive: true });
32316
- writeFileSync5(path, JSON.stringify(data, null, 2) + `
33382
+ mkdirSync8(dir, { recursive: true });
33383
+ writeFileSync6(path, JSON.stringify(data, null, 2) + `
32317
33384
  `);
32318
33385
  }
32319
33386
  function readTomlFile(path) {
@@ -32324,8 +33391,8 @@ function readTomlFile(path) {
32324
33391
  function writeTomlFile(path, content) {
32325
33392
  const dir = dirname10(path);
32326
33393
  if (!existsSync13(dir))
32327
- mkdirSync7(dir, { recursive: true });
32328
- writeFileSync5(path, content);
33394
+ mkdirSync8(dir, { recursive: true });
33395
+ writeFileSync6(path, content);
32329
33396
  }
32330
33397
  function removeTomlBlock(content, blockName) {
32331
33398
  const lines = content.split(`
@@ -32389,7 +33456,7 @@ function unregisterClaude(_global) {
32389
33456
  }
32390
33457
  }
32391
33458
  function registerCodex(binPath) {
32392
- const configPath = join13(HOME2, ".codex", "config.toml");
33459
+ const configPath = join14(HOME2, ".codex", "config.toml");
32393
33460
  let content = readTomlFile(configPath);
32394
33461
  content = removeTomlBlock(content, "mcp_servers.todos");
32395
33462
  const block = `
@@ -32403,7 +33470,7 @@ args = []
32403
33470
  console.log(chalk8.green(`Codex CLI: registered in ${configPath}`));
32404
33471
  }
32405
33472
  function unregisterCodex() {
32406
- const configPath = join13(HOME2, ".codex", "config.toml");
33473
+ const configPath = join14(HOME2, ".codex", "config.toml");
32407
33474
  let content = readTomlFile(configPath);
32408
33475
  if (!content.includes("[mcp_servers.todos]")) {
32409
33476
  console.log(chalk8.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -32415,7 +33482,7 @@ function unregisterCodex() {
32415
33482
  console.log(chalk8.green(`Codex CLI: unregistered from ${configPath}`));
32416
33483
  }
32417
33484
  function registerGemini(binPath) {
32418
- const configPath = join13(HOME2, ".gemini", "settings.json");
33485
+ const configPath = join14(HOME2, ".gemini", "settings.json");
32419
33486
  const config = readJsonFile2(configPath);
32420
33487
  if (!config["mcpServers"]) {
32421
33488
  config["mcpServers"] = {};
@@ -32429,7 +33496,7 @@ function registerGemini(binPath) {
32429
33496
  console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
32430
33497
  }
32431
33498
  function unregisterGemini() {
32432
- const configPath = join13(HOME2, ".gemini", "settings.json");
33499
+ const configPath = join14(HOME2, ".gemini", "settings.json");
32433
33500
  const config = readJsonFile2(configPath);
32434
33501
  const servers = config["mcpServers"];
32435
33502
  if (!servers || !("todos" in servers)) {
@@ -32486,9 +33553,9 @@ function registerMcpHooksCommands(program2) {
32486
33553
  if (p)
32487
33554
  todosBin = p;
32488
33555
  } catch {}
32489
- const hooksDir = join13(process.cwd(), ".claude", "hooks");
33556
+ const hooksDir = join14(process.cwd(), ".claude", "hooks");
32490
33557
  if (!existsSync13(hooksDir))
32491
- mkdirSync7(hooksDir, { recursive: true });
33558
+ mkdirSync8(hooksDir, { recursive: true });
32492
33559
  const hookScript = `#!/usr/bin/env bash
32493
33560
  # Auto-generated by: todos hooks install
32494
33561
  # Syncs todos with Claude Code task list on tool use events.
@@ -32512,11 +33579,11 @@ esac
32512
33579
 
32513
33580
  exit 0
32514
33581
  `;
32515
- const hookPath = join13(hooksDir, "todos-sync.sh");
32516
- writeFileSync5(hookPath, hookScript);
33582
+ const hookPath = join14(hooksDir, "todos-sync.sh");
33583
+ writeFileSync6(hookPath, hookScript);
32517
33584
  execSync3(`chmod +x "${hookPath}"`);
32518
33585
  console.log(chalk8.green(`Hook script created: ${hookPath}`));
32519
- const settingsPath = join13(process.cwd(), ".claude", "settings.json");
33586
+ const settingsPath = join14(process.cwd(), ".claude", "settings.json");
32520
33587
  const settings = readJsonFile2(settingsPath);
32521
33588
  if (!settings["hooks"]) {
32522
33589
  settings["hooks"] = {};
@@ -33265,12 +34332,12 @@ Artifacts:`));
33265
34332
  console.log(chalk8.yellow("Hook already installed."));
33266
34333
  return;
33267
34334
  }
33268
- writeFileSync5(hookPath, existing + `
34335
+ writeFileSync6(hookPath, existing + `
33269
34336
  ${marker}
33270
34337
  $(dirname "$0")/../../scripts/post-commit-hook.sh
33271
34338
  `);
33272
34339
  } else {
33273
- writeFileSync5(hookPath, `#!/usr/bin/env bash
34340
+ writeFileSync6(hookPath, `#!/usr/bin/env bash
33274
34341
  ${marker}
33275
34342
  $(dirname "$0")/../../scripts/post-commit-hook.sh
33276
34343
  `);
@@ -33302,7 +34369,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
33302
34369
  if (cleaned === "#!/usr/bin/env bash" || cleaned === "") {
33303
34370
  (await import("fs")).unlinkSync(hookPath);
33304
34371
  } else {
33305
- writeFileSync5(hookPath, cleaned + `
34372
+ writeFileSync6(hookPath, cleaned + `
33306
34373
  `);
33307
34374
  }
33308
34375
  console.log(chalk8.green("Post-commit hook removed."));
@@ -33467,9 +34534,9 @@ __export(exports_machines2, {
33467
34534
  });
33468
34535
  import chalk10 from "chalk";
33469
34536
  import { execSync as execSync4 } from "child_process";
33470
- import { writeFileSync as writeFileSync6 } from "fs";
34537
+ import { writeFileSync as writeFileSync7 } from "fs";
33471
34538
  import { tmpdir as tmpdir2 } from "os";
33472
- import { join as join14 } from "path";
34539
+ import { join as join15 } from "path";
33473
34540
  function getOrCreateLocalMachineName() {
33474
34541
  return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
33475
34542
  }
@@ -33663,8 +34730,8 @@ Warning: No primary machine set.`));
33663
34730
  if (opts.push) {
33664
34731
  try {
33665
34732
  const localTasks = listTasks3();
33666
- const tmpFile = join14(tmpdir2(), `todos-export-${uuid()}.json`);
33667
- writeFileSync6(tmpFile, JSON.stringify(localTasks, null, 2));
34733
+ const tmpFile = join15(tmpdir2(), `todos-export-${uuid()}.json`);
34734
+ writeFileSync7(tmpFile, JSON.stringify(localTasks, null, 2));
33668
34735
  execSync4(`scp ${tmpFile} ${ssh}:/tmp/todos-import.json`, { timeout: 15000 });
33669
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))"'`;
33670
34737
  const count = execSync4(importCmd, { encoding: "utf-8", timeout: 1e4 }).trim();