@hasna/todos 0.14.0 → 0.15.2

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/mcp/index.js CHANGED
@@ -3804,6 +3804,41 @@ var init_identity_mapping = __esm(() => {
3804
3804
  });
3805
3805
  });
3806
3806
 
3807
+ // src/lib/sync-utils.ts
3808
+ import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
3809
+ import { homedir } from "os";
3810
+ import { join } from "path";
3811
+ function getHomeDir() {
3812
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
3813
+ }
3814
+ function getTodosGlobalDir() {
3815
+ return join(getHomeDir(), ".hasna", "todos");
3816
+ }
3817
+ function ensureDir(dir) {
3818
+ if (!existsSync2(dir))
3819
+ mkdirSync(dir, { recursive: true });
3820
+ }
3821
+ function readJsonFile(path) {
3822
+ try {
3823
+ return JSON.parse(readFileSync(path, "utf-8"));
3824
+ } catch {
3825
+ return null;
3826
+ }
3827
+ }
3828
+ function writeJsonFile(path, data) {
3829
+ writeFileSync(path, JSON.stringify(data, null, 2) + `
3830
+ `);
3831
+ }
3832
+ function appendSyncConflict(metadata, conflict, limit = 5) {
3833
+ const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
3834
+ const next = [conflict, ...current].slice(0, limit);
3835
+ return { ...metadata, sync_conflicts: next };
3836
+ }
3837
+ var HOME;
3838
+ var init_sync_utils = __esm(() => {
3839
+ HOME = getHomeDir();
3840
+ });
3841
+
3807
3842
  // src/storage/config.ts
3808
3843
  var exports_config = {};
3809
3844
  __export(exports_config, {
@@ -4134,8 +4169,8 @@ __export(exports_database, {
4134
4169
  LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
4135
4170
  });
4136
4171
  import { Database } from "bun:sqlite";
4137
- import { existsSync as existsSync2, mkdirSync } from "fs";
4138
- import { dirname, join, resolve as resolve2 } from "path";
4172
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
4173
+ import { dirname, join as join2, resolve as resolve2 } from "path";
4139
4174
  function isInMemoryDb(path) {
4140
4175
  return path === ":memory:" || path.startsWith("file::memory:");
4141
4176
  }
@@ -4144,8 +4179,8 @@ function findNearestProjectDb(startDir) {
4144
4179
  const stopAt = gitRoot ? resolve2(gitRoot) : resolve2(startDir);
4145
4180
  let dir = resolve2(startDir);
4146
4181
  while (true) {
4147
- const candidate = join(dir, ".hasna", "todos", "todos.db");
4148
- if (existsSync2(candidate))
4182
+ const candidate = join2(dir, ".hasna", "todos", "todos.db");
4183
+ if (existsSync3(candidate))
4149
4184
  return candidate;
4150
4185
  if (dir === stopAt)
4151
4186
  break;
@@ -4159,7 +4194,7 @@ function findNearestProjectDb(startDir) {
4159
4194
  function findGitRoot(startDir) {
4160
4195
  let dir = resolve2(startDir);
4161
4196
  while (true) {
4162
- if (existsSync2(join(dir, ".git")))
4197
+ if (existsSync3(join2(dir, ".git")))
4163
4198
  return dir;
4164
4199
  const parent = dirname(dir);
4165
4200
  if (parent === dir)
@@ -4169,8 +4204,7 @@ function findGitRoot(startDir) {
4169
4204
  return null;
4170
4205
  }
4171
4206
  function getGlobalDbPath() {
4172
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4173
- return join(home, ".hasna", "todos", "todos.db");
4207
+ return join2(getHomeDir(), ".hasna", "todos", "todos.db");
4174
4208
  }
4175
4209
  function hasExplicitProjectArg(args = process.argv.slice(2)) {
4176
4210
  return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
@@ -4208,7 +4242,7 @@ function getDbPath() {
4208
4242
  if (process.env["TODOS_DB_SCOPE"] === "project") {
4209
4243
  const gitRoot = findGitRoot(cwd);
4210
4244
  if (gitRoot && canCreateScopedProjectDb()) {
4211
- return join(gitRoot, ".hasna", "todos", "todos.db");
4245
+ return join2(gitRoot, ".hasna", "todos", "todos.db");
4212
4246
  }
4213
4247
  }
4214
4248
  return getGlobalDbPath();
@@ -4216,16 +4250,16 @@ function getDbPath() {
4216
4250
  function getDatabasePath() {
4217
4251
  return getDbPath();
4218
4252
  }
4219
- function ensureDir(filePath) {
4253
+ function ensureDir2(filePath) {
4220
4254
  if (isInMemoryDb(filePath))
4221
4255
  return;
4222
4256
  const dir = dirname(resolve2(filePath));
4223
- if (!existsSync2(dir)) {
4224
- mkdirSync(dir, { recursive: true });
4257
+ if (!existsSync3(dir)) {
4258
+ mkdirSync2(dir, { recursive: true });
4225
4259
  }
4226
4260
  }
4227
4261
  function openDatabase(path) {
4228
- ensureDir(path);
4262
+ ensureDir2(path);
4229
4263
  const db = new Database(path);
4230
4264
  db.run("PRAGMA busy_timeout = 5000");
4231
4265
  db.run("PRAGMA journal_mode = WAL");
@@ -4392,6 +4426,7 @@ var init_database = __esm(() => {
4392
4426
  init_machines();
4393
4427
  init_identity_mapping();
4394
4428
  init_types();
4429
+ init_sync_utils();
4395
4430
  ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
4396
4431
  });
4397
4432
 
@@ -9135,40 +9170,6 @@ var init_zod = __esm(() => {
9135
9170
  init_external();
9136
9171
  });
9137
9172
 
9138
- // src/lib/sync-utils.ts
9139
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
9140
- import { join as join2 } from "path";
9141
- function getHomeDir() {
9142
- return process.env["HOME"] || process.env["USERPROFILE"] || "~";
9143
- }
9144
- function getTodosGlobalDir() {
9145
- return join2(getHomeDir(), ".hasna", "todos");
9146
- }
9147
- function ensureDir2(dir) {
9148
- if (!existsSync3(dir))
9149
- mkdirSync2(dir, { recursive: true });
9150
- }
9151
- function readJsonFile(path) {
9152
- try {
9153
- return JSON.parse(readFileSync(path, "utf-8"));
9154
- } catch {
9155
- return null;
9156
- }
9157
- }
9158
- function writeJsonFile(path, data) {
9159
- writeFileSync(path, JSON.stringify(data, null, 2) + `
9160
- `);
9161
- }
9162
- function appendSyncConflict(metadata, conflict, limit = 5) {
9163
- const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
9164
- const next = [conflict, ...current].slice(0, limit);
9165
- return { ...metadata, sync_conflicts: next };
9166
- }
9167
- var HOME;
9168
- var init_sync_utils = __esm(() => {
9169
- HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
9170
- });
9171
-
9172
9173
  // src/lib/config.ts
9173
9174
  import { existsSync as existsSync4 } from "fs";
9174
9175
  import { dirname as dirname2, join as join3 } from "path";
@@ -9191,7 +9192,7 @@ function loadConfig() {
9191
9192
  }
9192
9193
  function saveConfig(config) {
9193
9194
  const configPath = getConfigPath();
9194
- ensureDir2(dirname2(configPath));
9195
+ ensureDir(dirname2(configPath));
9195
9196
  writeJsonFile(configPath, config);
9196
9197
  cached = config;
9197
9198
  return config;
@@ -10746,7 +10747,7 @@ var init_event_hooks = __esm(() => {
10746
10747
  // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
10747
10748
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
10748
10749
  import { existsSync as existsSync5 } from "fs";
10749
- import { homedir } from "os";
10750
+ import { homedir as homedir2 } from "os";
10750
10751
  import { join as join4 } from "path";
10751
10752
  import { createHmac, timingSafeEqual } from "crypto";
10752
10753
  import { randomUUID as randomUUID2 } from "crypto";
@@ -10848,7 +10849,7 @@ function channelMatchesEvent(channel, event) {
10848
10849
  return channel.filters.some((filter) => eventMatchesFilter(event, filter));
10849
10850
  }
10850
10851
  function getEventsDataDir(override) {
10851
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir(), ".hasna", "events");
10852
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir2(), ".hasna", "events");
10852
10853
  }
10853
10854
 
10854
10855
  class JsonEventsStore {
@@ -18002,10 +18003,10 @@ var init_token_utils = __esm(() => {
18002
18003
 
18003
18004
  // src/lib/assignee-validation.ts
18004
18005
  import { readFileSync as readFileSync3 } from "fs";
18005
- import { homedir as homedir2 } from "os";
18006
+ import { homedir as homedir3 } from "os";
18006
18007
  import { join as join7 } from "path";
18007
18008
  function defaultSeatRosterPath() {
18008
- return process.env["TODOS_SEAT_ROSTER_PATH"] || join7(homedir2(), ".hasna", "identities", "hasna-seats.roster.json");
18009
+ return process.env["TODOS_SEAT_ROSTER_PATH"] || join7(homedir3(), ".hasna", "identities", "hasna-seats.roster.json");
18009
18010
  }
18010
18011
  function loadSeatSlugs(path = defaultSeatRosterPath()) {
18011
18012
  try {
@@ -20833,7 +20834,7 @@ async function validateMcpAssignee(value, allowSeat) {
20833
20834
  return verdict.assignee;
20834
20835
  }
20835
20836
  function registerTaskCrudTools(server, ctx) {
20836
- const { shouldRegisterTool, resolveId, formatError, formatTask } = ctx;
20837
+ const { shouldRegisterTool, resolveId, formatError, formatTask, applyFocus } = ctx;
20837
20838
  function mutationTaskResponse(task) {
20838
20839
  const compact = compactTask(task, 240);
20839
20840
  compact["version"] = task.version;
@@ -20866,7 +20867,7 @@ function registerTaskCrudTools(server, ctx) {
20866
20867
  project_id: exports_external.string().optional().describe("Project ID"),
20867
20868
  task_list_id: exports_external.string().optional().describe("Task list ID"),
20868
20869
  assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
20869
- created_by: exports_external.string().optional().describe("Agent who FILED this task. Defaults to the ambient agent identity (todos init / TODOS_AGENT_ID)."),
20870
+ created_by: exports_external.string().optional().describe("Agent who FILED this task. Defaults to the PROCESS-BOUND ambient identity (--agent / TODOS_AGENT_ID) if set; the machine-shared identity `todos init` persists to disk is never used here, because it names the station, not the caller."),
20870
20871
  unassigned: exports_external.boolean().optional().describe("Deliberately file with no assignee. Without it, the task defaults to the filer."),
20871
20872
  allow_seat: exports_external.boolean().optional().describe("Allow assigned_to to name a durable seat. A seat queue has no session watching it, so this must be deliberate."),
20872
20873
  depends_on: exports_external.array(exports_external.string()).optional().describe("Array of task IDs this task depends on"),
@@ -20881,14 +20882,13 @@ function registerTaskCrudTools(server, ctx) {
20881
20882
  try {
20882
20883
  const { depends_on, assigned_to, project_id, task_list_id, tags, estimate, confidence, retry_count, deadline, created_by, unassigned, allow_seat, ...rest } = params;
20883
20884
  const requestedAssignee = assigned_to ? await validateMcpAssignee(assigned_to, Boolean(allow_seat)) : undefined;
20884
- const creator = resolveCreatorIdentity(created_by);
20885
20885
  const router = resolveWritableIdentity(created_by);
20886
20886
  const assignee = requestedAssignee || (unassigned ? undefined : router.agent_id || undefined);
20887
20887
  const cloud = getTodosCloudClient();
20888
20888
  if (cloud) {
20889
20889
  const payload = { ...rest };
20890
- if (creator.agent_id)
20891
- payload.created_by = creator.agent_id;
20890
+ if (router.agent_id)
20891
+ payload.created_by = router.agent_id;
20892
20892
  if (router.agent_id)
20893
20893
  payload.agent_id = payload.agent_id ?? router.agent_id;
20894
20894
  if (assignee)
@@ -20909,12 +20909,13 @@ function registerTaskCrudTools(server, ctx) {
20909
20909
  payload.max_retries = retry_count;
20910
20910
  if (deadline)
20911
20911
  payload.due_at = deadline;
20912
+ applyFocus(payload, router.agent_id || undefined);
20912
20913
  const created = await cloudCreateTask(cloud, payload);
20913
20914
  return { content: [{ type: "text", text: mutationTaskResponse(created) }] };
20914
20915
  }
20915
20916
  const resolved = { ...rest };
20916
- if (creator.agent_id)
20917
- resolved.created_by = creator.agent_id;
20917
+ if (router.agent_id)
20918
+ resolved.created_by = router.agent_id;
20918
20919
  if (router.agent_id)
20919
20920
  resolved.agent_id = resolved.agent_id ?? router.agent_id;
20920
20921
  if (assignee)
@@ -20935,6 +20936,7 @@ function registerTaskCrudTools(server, ctx) {
20935
20936
  resolved.max_retries = retry_count;
20936
20937
  if (deadline)
20937
20938
  resolved.due_at = deadline;
20939
+ applyFocus(resolved, router.agent_id || undefined);
20938
20940
  const task = createTask(resolved);
20939
20941
  return { content: [{ type: "text", text: mutationTaskResponse(task) }] };
20940
20942
  } catch (e) {
@@ -34912,7 +34914,7 @@ var package_default;
34912
34914
  var init_package = __esm(() => {
34913
34915
  package_default = {
34914
34916
  name: "@hasna/todos",
34915
- version: "0.14.0",
34917
+ version: "0.15.2",
34916
34918
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
34917
34919
  type: "module",
34918
34920
  main: "dist/index.js",
@@ -43853,7 +43855,7 @@ function captureEnvironmentSnapshot(input = {}) {
43853
43855
  }
43854
43856
  function writeEnvironmentSnapshot(snapshot, outputPath) {
43855
43857
  const path = outputPath ? resolve15(outputPath) : join14(defaultSnapshotDir(), `${snapshot.id}.json`);
43856
- ensureDir2(dirname8(path));
43858
+ ensureDir(dirname8(path));
43857
43859
  writeJsonFile(path, snapshot);
43858
43860
  return path;
43859
43861
  }
@@ -48112,14 +48114,21 @@ function suggestVocabularyMatches(value, vocabulary, limit = 3) {
48112
48114
  }
48113
48115
  function resolveEnumVocabulary(raw, spec) {
48114
48116
  const allowList = spec.allowList !== false;
48115
- const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim()).filter((element) => element.length > 0);
48116
- if (rawElements.length === 0) {
48117
+ const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim());
48118
+ if (rawElements.every((element) => element.length === 0)) {
48117
48119
  return {
48118
48120
  ok: false,
48119
48121
  message: `${spec.name} requires a value. Allowed values: ${spec.vocabulary.join(", ")}.`,
48120
48122
  invalid: []
48121
48123
  };
48122
48124
  }
48125
+ if (rawElements.some((element) => element.length === 0)) {
48126
+ return {
48127
+ ok: false,
48128
+ message: `${spec.name} has an empty value in "${raw}" \u2014 remove the stray comma. ` + `Allowed values: ${spec.vocabulary.join(", ")}.`,
48129
+ invalid: []
48130
+ };
48131
+ }
48123
48132
  const normalized = rawElements.map((element) => spec.normalize ? spec.normalize(element) : element);
48124
48133
  const allowed = new Set(spec.vocabulary);
48125
48134
  const invalid2 = [];
@@ -51976,7 +51985,13 @@ async function handleV1Request(req, url, dependencies = {}) {
51976
51985
  return error(405, `method ${method} not allowed on /v1/refs/:ref`);
51977
51986
  if (!store.gitRefs)
51978
51987
  return error(501, "git ref links are not supported by this storage backend");
51979
- const refs = await store.gitRefs.find(id);
51988
+ let decodedRef;
51989
+ try {
51990
+ decodedRef = decodeURIComponent(id);
51991
+ } catch {
51992
+ return error(400, "ref path segment has invalid percent encoding");
51993
+ }
51994
+ const refs = await store.gitRefs.find(decodedRef);
51980
51995
  return json3({ refs, count: refs.length });
51981
51996
  }
51982
51997
  if (resource === "next" && !id) {
@@ -53082,7 +53097,8 @@ function buildServer() {
53082
53097
  formatError,
53083
53098
  formatTask,
53084
53099
  formatTaskDetail,
53085
- getAgentFocus
53100
+ getAgentFocus,
53101
+ applyFocus
53086
53102
  };
53087
53103
  registerTaskCrudTools(server, toolContext);
53088
53104
  registerTaskProjectTools(server, toolContext);
@@ -14,6 +14,7 @@ interface TaskCrudContext {
14
14
  agent_id: string;
15
15
  project_id?: string;
16
16
  } | undefined;
17
+ applyFocus: (params: Record<string, any>, agentId?: string) => void;
17
18
  }
18
19
  export declare function registerTaskCrudTools(server: McpServer, ctx: TaskCrudContext): void;
19
20
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"task-crud.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/task-crud.ts"],"names":[],"mappings":"AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAoBjD,UAAU,eAAe;IACvB,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACzD,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;IACxC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IACnC,gBAAgB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACvE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CAC3F;AAuBD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,eAAe,QAkY5E"}
1
+ {"version":3,"file":"task-crud.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/task-crud.ts"],"names":[],"mappings":"AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAoBjD,UAAU,eAAe;IACvB,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACzD,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;IACxC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IACnC,gBAAgB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACvE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAC1F,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACrE;AAuBD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,eAAe,QA2Y5E"}
package/dist/mcp.js CHANGED
@@ -41,7 +41,7 @@ var __require = import.meta.require;
41
41
  // package.json
42
42
  var package_default = {
43
43
  name: "@hasna/todos",
44
- version: "0.14.0",
44
+ version: "0.15.2",
45
45
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
46
46
  type: "module",
47
47
  main: "dist/index.js",
package/dist/registry.js CHANGED
@@ -3790,6 +3790,103 @@ var init_identity_mapping = __esm(() => {
3790
3790
  });
3791
3791
  });
3792
3792
 
3793
+ // src/lib/sync-utils.ts
3794
+ import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
3795
+ import { createHash } from "crypto";
3796
+ import { homedir } from "os";
3797
+ import { join } from "path";
3798
+ function getHomeDir() {
3799
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
3800
+ }
3801
+ function getTodosGlobalDir() {
3802
+ return join(getHomeDir(), ".hasna", "todos");
3803
+ }
3804
+ function ensureDir(dir) {
3805
+ if (!existsSync2(dir))
3806
+ mkdirSync(dir, { recursive: true });
3807
+ }
3808
+ function listJsonFiles(dir) {
3809
+ if (!existsSync2(dir))
3810
+ return [];
3811
+ return readdirSync(dir).filter((f) => f.endsWith(".json"));
3812
+ }
3813
+ function readJsonFile(path) {
3814
+ try {
3815
+ return JSON.parse(readFileSync(path, "utf-8"));
3816
+ } catch {
3817
+ return null;
3818
+ }
3819
+ }
3820
+ function writeJsonFile(path, data) {
3821
+ writeFileSync(path, JSON.stringify(data, null, 2) + `
3822
+ `);
3823
+ }
3824
+ function readHighWaterMark(dir) {
3825
+ const path = join(dir, ".highwatermark");
3826
+ if (!existsSync2(path))
3827
+ return 1;
3828
+ const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
3829
+ return isNaN(val) ? 1 : val;
3830
+ }
3831
+ function writeHighWaterMark(dir, value) {
3832
+ writeFileSync(join(dir, ".highwatermark"), String(value));
3833
+ }
3834
+ function getFileMtimeMs(path) {
3835
+ try {
3836
+ return statSync(path).mtimeMs;
3837
+ } catch {
3838
+ return null;
3839
+ }
3840
+ }
3841
+ function parseTimestamp(value) {
3842
+ if (typeof value !== "string")
3843
+ return null;
3844
+ const parsed = Date.parse(value);
3845
+ return Number.isNaN(parsed) ? null : parsed;
3846
+ }
3847
+ function appendSyncConflict(metadata, conflict, limit = 5) {
3848
+ const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
3849
+ const next = [conflict, ...current].slice(0, limit);
3850
+ return { ...metadata, sync_conflicts: next };
3851
+ }
3852
+ function canonicalize(value) {
3853
+ if (Array.isArray(value))
3854
+ return value.map(canonicalize);
3855
+ if (!value || typeof value !== "object")
3856
+ return value;
3857
+ const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).sort(([a], [b]) => a.localeCompare(b));
3858
+ return Object.fromEntries(entries.map(([key, entryValue]) => [key, canonicalize(entryValue)]));
3859
+ }
3860
+ function withoutSyncFingerprintMetadata(metadata) {
3861
+ const { [TODO_SYNC_FINGERPRINT_KEY]: _fingerprint, ...rest } = metadata;
3862
+ return rest;
3863
+ }
3864
+ function syncFingerprint(record) {
3865
+ const metadata = withoutSyncFingerprintMetadata(record.metadata || {});
3866
+ const canonical = canonicalize({ ...record, metadata });
3867
+ return `sha256:${createHash("sha256").update(JSON.stringify(canonical)).digest("hex")}`;
3868
+ }
3869
+ function withSyncFingerprint(record) {
3870
+ const metadata = withoutSyncFingerprintMetadata(record.metadata);
3871
+ return {
3872
+ ...record,
3873
+ metadata: {
3874
+ ...metadata,
3875
+ [TODO_SYNC_FINGERPRINT_KEY]: syncFingerprint({ ...record, metadata })
3876
+ }
3877
+ };
3878
+ }
3879
+ function hasSyncFingerprintChanged(record) {
3880
+ const stored = record.metadata?.[TODO_SYNC_FINGERPRINT_KEY];
3881
+ if (typeof stored !== "string" || stored.length === 0)
3882
+ return null;
3883
+ return stored !== syncFingerprint(record);
3884
+ }
3885
+ var TODO_SYNC_FINGERPRINT_KEY = "todos_sync_fingerprint", HOME;
3886
+ var init_sync_utils = __esm(() => {
3887
+ HOME = getHomeDir();
3888
+ });
3889
+
3793
3890
  // src/storage/config.ts
3794
3891
  var exports_config = {};
3795
3892
  __export(exports_config, {
@@ -4120,8 +4217,8 @@ __export(exports_database, {
4120
4217
  LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
4121
4218
  });
4122
4219
  import { Database } from "bun:sqlite";
4123
- import { existsSync as existsSync2, mkdirSync } from "fs";
4124
- import { dirname, join, resolve as resolve2 } from "path";
4220
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
4221
+ import { dirname, join as join2, resolve as resolve2 } from "path";
4125
4222
  function isInMemoryDb(path) {
4126
4223
  return path === ":memory:" || path.startsWith("file::memory:");
4127
4224
  }
@@ -4130,8 +4227,8 @@ function findNearestProjectDb(startDir) {
4130
4227
  const stopAt = gitRoot ? resolve2(gitRoot) : resolve2(startDir);
4131
4228
  let dir = resolve2(startDir);
4132
4229
  while (true) {
4133
- const candidate = join(dir, ".hasna", "todos", "todos.db");
4134
- if (existsSync2(candidate))
4230
+ const candidate = join2(dir, ".hasna", "todos", "todos.db");
4231
+ if (existsSync3(candidate))
4135
4232
  return candidate;
4136
4233
  if (dir === stopAt)
4137
4234
  break;
@@ -4145,7 +4242,7 @@ function findNearestProjectDb(startDir) {
4145
4242
  function findGitRoot(startDir) {
4146
4243
  let dir = resolve2(startDir);
4147
4244
  while (true) {
4148
- if (existsSync2(join(dir, ".git")))
4245
+ if (existsSync3(join2(dir, ".git")))
4149
4246
  return dir;
4150
4247
  const parent = dirname(dir);
4151
4248
  if (parent === dir)
@@ -4155,8 +4252,7 @@ function findGitRoot(startDir) {
4155
4252
  return null;
4156
4253
  }
4157
4254
  function getGlobalDbPath() {
4158
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4159
- return join(home, ".hasna", "todos", "todos.db");
4255
+ return join2(getHomeDir(), ".hasna", "todos", "todos.db");
4160
4256
  }
4161
4257
  function hasExplicitProjectArg(args = process.argv.slice(2)) {
4162
4258
  return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
@@ -4194,7 +4290,7 @@ function getDbPath() {
4194
4290
  if (process.env["TODOS_DB_SCOPE"] === "project") {
4195
4291
  const gitRoot = findGitRoot(cwd);
4196
4292
  if (gitRoot && canCreateScopedProjectDb()) {
4197
- return join(gitRoot, ".hasna", "todos", "todos.db");
4293
+ return join2(gitRoot, ".hasna", "todos", "todos.db");
4198
4294
  }
4199
4295
  }
4200
4296
  return getGlobalDbPath();
@@ -4202,16 +4298,16 @@ function getDbPath() {
4202
4298
  function getDatabasePath() {
4203
4299
  return getDbPath();
4204
4300
  }
4205
- function ensureDir(filePath) {
4301
+ function ensureDir2(filePath) {
4206
4302
  if (isInMemoryDb(filePath))
4207
4303
  return;
4208
4304
  const dir = dirname(resolve2(filePath));
4209
- if (!existsSync2(dir)) {
4210
- mkdirSync(dir, { recursive: true });
4305
+ if (!existsSync3(dir)) {
4306
+ mkdirSync2(dir, { recursive: true });
4211
4307
  }
4212
4308
  }
4213
4309
  function openDatabase(path) {
4214
- ensureDir(path);
4310
+ ensureDir2(path);
4215
4311
  const db = new Database(path);
4216
4312
  db.run("PRAGMA busy_timeout = 5000");
4217
4313
  db.run("PRAGMA journal_mode = WAL");
@@ -4378,105 +4474,10 @@ var init_database = __esm(() => {
4378
4474
  init_machines();
4379
4475
  init_identity_mapping();
4380
4476
  init_types();
4477
+ init_sync_utils();
4381
4478
  ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
4382
4479
  });
4383
4480
 
4384
- // src/lib/sync-utils.ts
4385
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
4386
- import { createHash } from "crypto";
4387
- import { join as join2 } from "path";
4388
- function getHomeDir() {
4389
- return process.env["HOME"] || process.env["USERPROFILE"] || "~";
4390
- }
4391
- function getTodosGlobalDir() {
4392
- return join2(getHomeDir(), ".hasna", "todos");
4393
- }
4394
- function ensureDir2(dir) {
4395
- if (!existsSync3(dir))
4396
- mkdirSync2(dir, { recursive: true });
4397
- }
4398
- function listJsonFiles(dir) {
4399
- if (!existsSync3(dir))
4400
- return [];
4401
- return readdirSync(dir).filter((f) => f.endsWith(".json"));
4402
- }
4403
- function readJsonFile(path) {
4404
- try {
4405
- return JSON.parse(readFileSync(path, "utf-8"));
4406
- } catch {
4407
- return null;
4408
- }
4409
- }
4410
- function writeJsonFile(path, data) {
4411
- writeFileSync(path, JSON.stringify(data, null, 2) + `
4412
- `);
4413
- }
4414
- function readHighWaterMark(dir) {
4415
- const path = join2(dir, ".highwatermark");
4416
- if (!existsSync3(path))
4417
- return 1;
4418
- const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
4419
- return isNaN(val) ? 1 : val;
4420
- }
4421
- function writeHighWaterMark(dir, value) {
4422
- writeFileSync(join2(dir, ".highwatermark"), String(value));
4423
- }
4424
- function getFileMtimeMs(path) {
4425
- try {
4426
- return statSync(path).mtimeMs;
4427
- } catch {
4428
- return null;
4429
- }
4430
- }
4431
- function parseTimestamp(value) {
4432
- if (typeof value !== "string")
4433
- return null;
4434
- const parsed = Date.parse(value);
4435
- return Number.isNaN(parsed) ? null : parsed;
4436
- }
4437
- function appendSyncConflict(metadata, conflict, limit = 5) {
4438
- const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
4439
- const next = [conflict, ...current].slice(0, limit);
4440
- return { ...metadata, sync_conflicts: next };
4441
- }
4442
- function canonicalize(value) {
4443
- if (Array.isArray(value))
4444
- return value.map(canonicalize);
4445
- if (!value || typeof value !== "object")
4446
- return value;
4447
- const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).sort(([a], [b]) => a.localeCompare(b));
4448
- return Object.fromEntries(entries.map(([key, entryValue]) => [key, canonicalize(entryValue)]));
4449
- }
4450
- function withoutSyncFingerprintMetadata(metadata) {
4451
- const { [TODO_SYNC_FINGERPRINT_KEY]: _fingerprint, ...rest } = metadata;
4452
- return rest;
4453
- }
4454
- function syncFingerprint(record) {
4455
- const metadata = withoutSyncFingerprintMetadata(record.metadata || {});
4456
- const canonical = canonicalize({ ...record, metadata });
4457
- return `sha256:${createHash("sha256").update(JSON.stringify(canonical)).digest("hex")}`;
4458
- }
4459
- function withSyncFingerprint(record) {
4460
- const metadata = withoutSyncFingerprintMetadata(record.metadata);
4461
- return {
4462
- ...record,
4463
- metadata: {
4464
- ...metadata,
4465
- [TODO_SYNC_FINGERPRINT_KEY]: syncFingerprint({ ...record, metadata })
4466
- }
4467
- };
4468
- }
4469
- function hasSyncFingerprintChanged(record) {
4470
- const stored = record.metadata?.[TODO_SYNC_FINGERPRINT_KEY];
4471
- if (typeof stored !== "string" || stored.length === 0)
4472
- return null;
4473
- return stored !== syncFingerprint(record);
4474
- }
4475
- var TODO_SYNC_FINGERPRINT_KEY = "todos_sync_fingerprint", HOME;
4476
- var init_sync_utils = __esm(() => {
4477
- HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
4478
- });
4479
-
4480
4481
  // src/lib/config.ts
4481
4482
  import { existsSync as existsSync4 } from "fs";
4482
4483
  import { dirname as dirname2, join as join3 } from "path";
@@ -4502,7 +4503,7 @@ function loadConfig() {
4502
4503
  }
4503
4504
  function saveConfig(config) {
4504
4505
  const configPath = getConfigPath();
4505
- ensureDir2(dirname2(configPath));
4506
+ ensureDir(dirname2(configPath));
4506
4507
  writeJsonFile(configPath, config);
4507
4508
  cached = config;
4508
4509
  return config;
@@ -5847,7 +5848,7 @@ var init_event_hooks = __esm(() => {
5847
5848
  // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
5848
5849
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
5849
5850
  import { existsSync as existsSync5 } from "fs";
5850
- import { homedir } from "os";
5851
+ import { homedir as homedir2 } from "os";
5851
5852
  import { join as join4 } from "path";
5852
5853
  import { createHmac, timingSafeEqual } from "crypto";
5853
5854
  import { randomUUID as randomUUID2 } from "crypto";
@@ -5949,7 +5950,7 @@ function channelMatchesEvent(channel, event) {
5949
5950
  return channel.filters.some((filter) => eventMatchesFilter(event, filter));
5950
5951
  }
5951
5952
  function getEventsDataDir(override) {
5952
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir(), ".hasna", "events");
5953
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir2(), ".hasna", "events");
5953
5954
  }
5954
5955
 
5955
5956
  class JsonEventsStore {
@@ -12031,7 +12032,7 @@ var init_tasks = __esm(() => {
12031
12032
  // package.json
12032
12033
  var package_default = {
12033
12034
  name: "@hasna/todos",
12034
- version: "0.14.0",
12035
+ version: "0.15.2",
12035
12036
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12036
12037
  type: "module",
12037
12038
  main: "dist/index.js",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "packageName": "@hasna/todos",
3
- "packageVersion": "0.14.0",
3
+ "packageVersion": "0.15.2",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "d70f16ca424626adf5eb519e69effdcb1aef5d19",
6
- "gitTree": "e00228c9804fbf411b6abb43988ce8adf336b152",
7
- "sourceTreeSha256": "85edd5453eda0f9688079d7a6b7ff49cf3786f08c6b9c615afb68c7eb4604e42",
8
- "generatedAt": "2026-08-02T17:22:28.000Z"
5
+ "gitCommit": "feac5775161b453688e8f5bef0af3e72d5210106",
6
+ "gitTree": "debb675e5132e0058518024cad510f47590c112c",
7
+ "sourceTreeSha256": "27d6cb7b111d064938d5c1c2c4bd5da9db935cc325fcab47361ff0a6774df8d2",
8
+ "generatedAt": "2026-08-04T01:11:58.000Z"
9
9
  }