@hasna/todos 0.11.57 → 0.11.58

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/storage.js CHANGED
@@ -4418,7 +4418,7 @@ async function testLocalEventHook(name, input) {
4418
4418
  return emitLocalEventHooks({ ...input, hooks: [hook] });
4419
4419
  }
4420
4420
 
4421
- // node_modules/.bun/@hasna+events@0.1.7/node_modules/@hasna/events/dist/index.js
4421
+ // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
4422
4422
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
4423
4423
  import { existsSync as existsSync5 } from "fs";
4424
4424
  import { homedir } from "os";
@@ -4435,17 +4435,30 @@ function getPathValue(input, path) {
4435
4435
  return;
4436
4436
  }, input);
4437
4437
  }
4438
- function wildcardToRegExp(pattern) {
4439
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
4440
- return new RegExp(`^${escaped}$`);
4438
+ function wildcardToRegExp(pattern, options = {}) {
4439
+ let body = "";
4440
+ for (let index = 0;index < pattern.length; index += 1) {
4441
+ const char = pattern[index];
4442
+ if (char === "*") {
4443
+ if (pattern[index + 1] === "*") {
4444
+ body += ".*";
4445
+ index += 1;
4446
+ } else {
4447
+ body += options.segmentSafe ? "[^/]*" : ".*";
4448
+ }
4449
+ } else {
4450
+ body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
4451
+ }
4452
+ }
4453
+ return new RegExp(`^${body}$`);
4441
4454
  }
4442
- function matchString(value, matcher) {
4455
+ function matchString(value, matcher, options = {}) {
4443
4456
  if (matcher === undefined)
4444
4457
  return true;
4445
4458
  if (value === undefined)
4446
4459
  return false;
4447
4460
  const matchers = Array.isArray(matcher) ? matcher : [matcher];
4448
- return matchers.some((item) => wildcardToRegExp(item).test(value));
4461
+ return matchers.some((item) => wildcardToRegExp(item, options).test(value));
4449
4462
  }
4450
4463
  function matchRecord(input, matcher) {
4451
4464
  if (!matcher)
@@ -4453,7 +4466,9 @@ function matchRecord(input, matcher) {
4453
4466
  return Object.entries(matcher).every(([path, expected]) => {
4454
4467
  const actual = getPathValue(input, path);
4455
4468
  if (typeof expected === "string" || Array.isArray(expected)) {
4456
- return matchString(actual === undefined ? undefined : String(actual), expected);
4469
+ return matchString(actual === undefined ? undefined : String(actual), expected, {
4470
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
4471
+ });
4457
4472
  }
4458
4473
  return actual === expected;
4459
4474
  });
@@ -4473,7 +4488,6 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
4473
4488
  function getEventsDataDir(override) {
4474
4489
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir(), ".hasna", "events");
4475
4490
  }
4476
-
4477
4491
  class JsonEventsStore {
4478
4492
  dataDir;
4479
4493
  channelsPath;
@@ -4810,7 +4824,7 @@ class EventsClient {
4810
4824
  }
4811
4825
  return deliveries;
4812
4826
  }
4813
- async testChannel(id, input = {}) {
4827
+ async matchChannel(id, input = {}) {
4814
4828
  const channel = await this.store.getChannel(id);
4815
4829
  if (!channel)
4816
4830
  throw new Error(`Channel not found: ${id}`);
@@ -4827,6 +4841,34 @@ class EventsClient {
4827
4841
  time: input.time,
4828
4842
  id: input.id
4829
4843
  });
4844
+ const matched = channelMatchesEvent(channel, event);
4845
+ return {
4846
+ channelId: channel.id,
4847
+ matched,
4848
+ event,
4849
+ filters: channel.filters,
4850
+ reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
4851
+ };
4852
+ }
4853
+ async testChannel(id, input = {}, options = {}) {
4854
+ const channel = await this.store.getChannel(id);
4855
+ if (!channel)
4856
+ throw new Error(`Channel not found: ${id}`);
4857
+ const match = await this.matchChannel(id, input);
4858
+ const event = match.event;
4859
+ if (options.honorFilters && !match.matched) {
4860
+ const timestamp = new Date().toISOString();
4861
+ const result2 = createDeliveryResult(event, channel, [{
4862
+ attempt: 1,
4863
+ status: "skipped",
4864
+ startedAt: timestamp,
4865
+ completedAt: timestamp,
4866
+ error: match.reason
4867
+ }]);
4868
+ result2.metadata = { reason: "filter_mismatch" };
4869
+ await this.store.appendDelivery(result2);
4870
+ return result2;
4871
+ }
4830
4872
  const eventForChannel = await this.applyRedaction(event, channel);
4831
4873
  const result = await this.deliverWithRetry(eventForChannel, channel);
4832
4874
  await this.store.appendDelivery(result);
@@ -4926,6 +4968,90 @@ function normalizeRetryPolicy(policy) {
4926
4968
  };
4927
4969
  }
4928
4970
 
4971
+ // src/lib/shared-events.ts
4972
+ init_database();
4973
+
4974
+ // src/db/task-lists.ts
4975
+ init_types();
4976
+ init_database();
4977
+ function rowToTaskList(row) {
4978
+ return {
4979
+ ...row,
4980
+ metadata: JSON.parse(row.metadata || "{}")
4981
+ };
4982
+ }
4983
+ function createTaskList(input, db) {
4984
+ const d = db || getDatabase();
4985
+ const id = uuid();
4986
+ const timestamp = now();
4987
+ const slug = input.slug || slugify(input.name);
4988
+ if (!input.project_id) {
4989
+ const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
4990
+ if (existing) {
4991
+ throw new Error(`Standalone task list with slug "${slug}" already exists`);
4992
+ }
4993
+ }
4994
+ d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
4995
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
4996
+ return getTaskList(id, d);
4997
+ }
4998
+ function getTaskList(id, db) {
4999
+ const d = db || getDatabase();
5000
+ const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
5001
+ return row ? rowToTaskList(row) : null;
5002
+ }
5003
+ function getTaskListBySlug(slug, projectId, db) {
5004
+ const d = db || getDatabase();
5005
+ let row;
5006
+ if (projectId) {
5007
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
5008
+ } else {
5009
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
5010
+ }
5011
+ return row ? rowToTaskList(row) : null;
5012
+ }
5013
+ function listTaskLists(projectId, db) {
5014
+ const d = db || getDatabase();
5015
+ if (projectId) {
5016
+ return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
5017
+ }
5018
+ return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
5019
+ }
5020
+ function updateTaskList(id, input, db) {
5021
+ const d = db || getDatabase();
5022
+ const existing = getTaskList(id, d);
5023
+ if (!existing)
5024
+ throw new TaskListNotFoundError(id);
5025
+ const sets = ["updated_at = ?"];
5026
+ const params = [now()];
5027
+ if (input.name !== undefined) {
5028
+ sets.push("name = ?");
5029
+ params.push(input.name);
5030
+ }
5031
+ if (input.description !== undefined) {
5032
+ sets.push("description = ?");
5033
+ params.push(input.description);
5034
+ }
5035
+ if (input.metadata !== undefined) {
5036
+ sets.push("metadata = ?");
5037
+ params.push(JSON.stringify(input.metadata));
5038
+ }
5039
+ params.push(id);
5040
+ d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
5041
+ return getTaskList(id, d);
5042
+ }
5043
+ function deleteTaskList(id, db) {
5044
+ const d = db || getDatabase();
5045
+ return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
5046
+ }
5047
+ function ensureTaskList(name, slug, projectId, db) {
5048
+ const d = db || getDatabase();
5049
+ const existing = getTaskListBySlug(slug, projectId, d);
5050
+ if (existing)
5051
+ return existing;
5052
+ return createTaskList({ name, slug, project_id: projectId }, d);
5053
+ }
5054
+
4929
5055
  // src/lib/shared-events.ts
4930
5056
  var SOURCE = "todos";
4931
5057
  function taskEventData(task, extra = {}) {
@@ -4956,6 +5082,69 @@ function taskEventData(task, extra = {}) {
4956
5082
  ...extra
4957
5083
  };
4958
5084
  }
5085
+ function taskEventMetadata(task) {
5086
+ const metadata = {
5087
+ package: "@hasna/todos",
5088
+ todos_event_schema_version: 1,
5089
+ task_id: task.id,
5090
+ task_short_id: task.short_id,
5091
+ project_id: task.project_id,
5092
+ task_list_id: task.task_list_id,
5093
+ working_dir: task.working_dir
5094
+ };
5095
+ try {
5096
+ const project = task.project_id ? getProject(task.project_id) : null;
5097
+ const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
5098
+ if (project) {
5099
+ metadata.project_id = project.id;
5100
+ metadata.project_name = project.name;
5101
+ metadata.project_path = projectPath;
5102
+ metadata.project_canonical_path = project.path;
5103
+ metadata.project_default_task_list_slug = project.task_list_id;
5104
+ metadata.root_project_id = inferRootProjectId(project);
5105
+ } else if (projectPath) {
5106
+ metadata.project_path = projectPath;
5107
+ metadata.project_canonical_path = projectPath;
5108
+ }
5109
+ if (projectPath) {
5110
+ metadata.project_kind = classifyProjectKind(projectPath);
5111
+ metadata.project_is_worktree = isWorktreePath(projectPath);
5112
+ if (typeof task.metadata.route_enabled === "boolean") {
5113
+ metadata.route_enabled = task.metadata.route_enabled;
5114
+ }
5115
+ metadata.working_dir = task.working_dir ?? projectPath;
5116
+ }
5117
+ const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
5118
+ if (taskList) {
5119
+ metadata.task_list_id = taskList.id;
5120
+ metadata.task_list_slug = taskList.slug;
5121
+ metadata.task_list_name = taskList.name;
5122
+ metadata.task_list_project_id = taskList.project_id;
5123
+ metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
5124
+ }
5125
+ } catch {}
5126
+ return metadata;
5127
+ }
5128
+ function classifyProjectKind(path) {
5129
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
5130
+ }
5131
+ function isWorktreePath(path) {
5132
+ return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
5133
+ }
5134
+ function inferRootProjectId(project) {
5135
+ return isWorktreePath(project.path) ? null : project.id;
5136
+ }
5137
+ function readMachineLocalPath(project) {
5138
+ const machineId = process.env["TODOS_MACHINE_ID"];
5139
+ if (!machineId)
5140
+ return null;
5141
+ try {
5142
+ const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
5143
+ return row?.path ?? null;
5144
+ } catch {
5145
+ return null;
5146
+ }
5147
+ }
4959
5148
  async function emitSharedTaskEvent(input) {
4960
5149
  const data = taskEventData(input.task, input.data);
4961
5150
  await new EventsClient().emit({
@@ -4966,12 +5155,7 @@ async function emitSharedTaskEvent(input) {
4966
5155
  message: input.message ?? `${input.type}: ${input.task.title}`,
4967
5156
  data,
4968
5157
  dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
4969
- metadata: {
4970
- package: "@hasna/todos",
4971
- task_id: input.task.id,
4972
- project_id: input.task.project_id,
4973
- task_list_id: input.task.task_list_id
4974
- }
5158
+ metadata: taskEventMetadata(input.task)
4975
5159
  }, { deliver: true, dedupe: true });
4976
5160
  }
4977
5161
  function emitSharedTaskEventQuiet(input) {
@@ -9741,87 +9925,6 @@ function getCapableAgents(capabilities, opts, db) {
9741
9925
  return opts?.limit ? scored.slice(0, opts.limit) : scored;
9742
9926
  }
9743
9927
 
9744
- // src/db/task-lists.ts
9745
- init_types();
9746
- init_database();
9747
- function rowToTaskList(row) {
9748
- return {
9749
- ...row,
9750
- metadata: JSON.parse(row.metadata || "{}")
9751
- };
9752
- }
9753
- function createTaskList(input, db) {
9754
- const d = db || getDatabase();
9755
- const id = uuid();
9756
- const timestamp = now();
9757
- const slug = input.slug || slugify(input.name);
9758
- if (!input.project_id) {
9759
- const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
9760
- if (existing) {
9761
- throw new Error(`Standalone task list with slug "${slug}" already exists`);
9762
- }
9763
- }
9764
- d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
9765
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
9766
- return getTaskList(id, d);
9767
- }
9768
- function getTaskList(id, db) {
9769
- const d = db || getDatabase();
9770
- const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
9771
- return row ? rowToTaskList(row) : null;
9772
- }
9773
- function getTaskListBySlug(slug, projectId, db) {
9774
- const d = db || getDatabase();
9775
- let row;
9776
- if (projectId) {
9777
- row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
9778
- } else {
9779
- row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
9780
- }
9781
- return row ? rowToTaskList(row) : null;
9782
- }
9783
- function listTaskLists(projectId, db) {
9784
- const d = db || getDatabase();
9785
- if (projectId) {
9786
- return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
9787
- }
9788
- return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
9789
- }
9790
- function updateTaskList(id, input, db) {
9791
- const d = db || getDatabase();
9792
- const existing = getTaskList(id, d);
9793
- if (!existing)
9794
- throw new TaskListNotFoundError(id);
9795
- const sets = ["updated_at = ?"];
9796
- const params = [now()];
9797
- if (input.name !== undefined) {
9798
- sets.push("name = ?");
9799
- params.push(input.name);
9800
- }
9801
- if (input.description !== undefined) {
9802
- sets.push("description = ?");
9803
- params.push(input.description);
9804
- }
9805
- if (input.metadata !== undefined) {
9806
- sets.push("metadata = ?");
9807
- params.push(JSON.stringify(input.metadata));
9808
- }
9809
- params.push(id);
9810
- d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
9811
- return getTaskList(id, d);
9812
- }
9813
- function deleteTaskList(id, db) {
9814
- const d = db || getDatabase();
9815
- return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
9816
- }
9817
- function ensureTaskList(name, slug, projectId, db) {
9818
- const d = db || getDatabase();
9819
- const existing = getTaskListBySlug(slug, projectId, d);
9820
- if (existing)
9821
- return existing;
9822
- return createTaskList({ name, slug, project_id: projectId }, d);
9823
- }
9824
-
9825
9928
  // src/storage/local-sqlite.ts
9826
9929
  init_database();
9827
9930
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.11.57",
3
+ "version": "0.11.58",
4
4
  "description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -89,7 +89,7 @@
89
89
  "author": "Andrei Hasna <andrei@hasna.com>",
90
90
  "license": "Apache-2.0",
91
91
  "dependencies": {
92
- "@hasna/events": "^0.1.7",
92
+ "@hasna/events": "^0.1.9",
93
93
  "@modelcontextprotocol/sdk": "^1.12.1",
94
94
  "chalk": "^5.4.1",
95
95
  "commander": "^13.1.0",