@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/sdk/index.js CHANGED
@@ -58,11 +58,12 @@ import { dirname, join as join2 } from "path";
58
58
 
59
59
  // src/lib/sync-utils.ts
60
60
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
61
+ import { homedir } from "os";
61
62
  import { join } from "path";
62
- var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
63
63
  function getHomeDir() {
64
- return process.env["HOME"] || process.env["USERPROFILE"] || "~";
64
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
65
65
  }
66
+ var HOME = getHomeDir();
66
67
  function getTodosGlobalDir() {
67
68
  return join(getHomeDir(), ".hasna", "todos");
68
69
  }
@@ -70,7 +70,7 @@ var package_default;
70
70
  var init_package = __esm(() => {
71
71
  package_default = {
72
72
  name: "@hasna/todos",
73
- version: "0.14.0",
73
+ version: "0.15.2",
74
74
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
75
75
  type: "module",
76
76
  main: "dist/index.js",
@@ -1041,9 +1041,10 @@ function normalizeAgentNameInput(name) {
1041
1041
 
1042
1042
  // src/lib/sync-utils.ts
1043
1043
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
1044
+ import { homedir } from "os";
1044
1045
  import { join } from "path";
1045
1046
  function getHomeDir() {
1046
- return process.env["HOME"] || process.env["USERPROFILE"] || "~";
1047
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
1047
1048
  }
1048
1049
  function getTodosGlobalDir() {
1049
1050
  return join(getHomeDir(), ".hasna", "todos");
@@ -1070,7 +1071,7 @@ function appendSyncConflict(metadata, conflict, limit = 5) {
1070
1071
  }
1071
1072
  var HOME;
1072
1073
  var init_sync_utils = __esm(() => {
1073
- HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
1074
+ HOME = getHomeDir();
1074
1075
  });
1075
1076
 
1076
1077
  // src/lib/creator-identity.ts
@@ -9372,8 +9373,7 @@ function findGitRoot(startDir) {
9372
9373
  return null;
9373
9374
  }
9374
9375
  function getGlobalDbPath() {
9375
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
9376
- return join4(home, ".hasna", "todos", "todos.db");
9376
+ return join4(getHomeDir(), ".hasna", "todos", "todos.db");
9377
9377
  }
9378
9378
  function hasExplicitProjectArg(args = process.argv.slice(2)) {
9379
9379
  return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
@@ -9595,6 +9595,7 @@ var init_database = __esm(() => {
9595
9595
  init_machines();
9596
9596
  init_identity_mapping();
9597
9597
  init_types();
9598
+ init_sync_utils();
9598
9599
  ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
9599
9600
  });
9600
9601
 
@@ -10874,7 +10875,7 @@ var init_event_hooks = __esm(() => {
10874
10875
  // node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
10875
10876
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
10876
10877
  import { existsSync as existsSync6 } from "fs";
10877
- import { homedir } from "os";
10878
+ import { homedir as homedir2 } from "os";
10878
10879
  import { join as join5 } from "path";
10879
10880
  import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual3 } from "crypto";
10880
10881
  import { randomUUID as randomUUID3 } from "crypto";
@@ -10976,7 +10977,7 @@ function channelMatchesEvent(channel, event) {
10976
10977
  return channel.filters.some((filter) => eventMatchesFilter(event, filter));
10977
10978
  }
10978
10979
  function getEventsDataDir(override) {
10979
- return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir(), ".hasna", "events");
10980
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir2(), ".hasna", "events");
10980
10981
  }
10981
10982
 
10982
10983
  class JsonEventsStore {
@@ -17301,14 +17302,21 @@ function suggestVocabularyMatches(value, vocabulary, limit = 3) {
17301
17302
  }
17302
17303
  function resolveEnumVocabulary(raw, spec) {
17303
17304
  const allowList = spec.allowList !== false;
17304
- const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim()).filter((element) => element.length > 0);
17305
- if (rawElements.length === 0) {
17305
+ const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim());
17306
+ if (rawElements.every((element) => element.length === 0)) {
17306
17307
  return {
17307
17308
  ok: false,
17308
17309
  message: `${spec.name} requires a value. Allowed values: ${spec.vocabulary.join(", ")}.`,
17309
17310
  invalid: []
17310
17311
  };
17311
17312
  }
17313
+ if (rawElements.some((element) => element.length === 0)) {
17314
+ return {
17315
+ ok: false,
17316
+ message: `${spec.name} has an empty value in "${raw}" \u2014 remove the stray comma. ` + `Allowed values: ${spec.vocabulary.join(", ")}.`,
17317
+ invalid: []
17318
+ };
17319
+ }
17312
17320
  const normalized = rawElements.map((element) => spec.normalize ? spec.normalize(element) : element);
17313
17321
  const allowed = new Set(spec.vocabulary);
17314
17322
  const invalid = [];
@@ -23377,7 +23385,13 @@ async function handleV1Request(req, url, dependencies = {}) {
23377
23385
  return error(405, `method ${method} not allowed on /v1/refs/:ref`);
23378
23386
  if (!store.gitRefs)
23379
23387
  return error(501, "git ref links are not supported by this storage backend");
23380
- const refs = await store.gitRefs.find(id);
23388
+ let decodedRef;
23389
+ try {
23390
+ decodedRef = decodeURIComponent(id);
23391
+ } catch {
23392
+ return error(400, "ref path segment has invalid percent encoding");
23393
+ }
23394
+ const refs = await store.gitRefs.find(decodedRef);
23381
23395
  return json3({ refs, count: refs.length });
23382
23396
  }
23383
23397
  if (resource === "next" && !id) {
@@ -44944,10 +44958,10 @@ var init_token_utils = __esm(() => {
44944
44958
 
44945
44959
  // src/lib/assignee-validation.ts
44946
44960
  import { readFileSync as readFileSync3 } from "fs";
44947
- import { homedir as homedir2 } from "os";
44961
+ import { homedir as homedir3 } from "os";
44948
44962
  import { join as join9 } from "path";
44949
44963
  function defaultSeatRosterPath() {
44950
- return process.env["TODOS_SEAT_ROSTER_PATH"] || join9(homedir2(), ".hasna", "identities", "hasna-seats.roster.json");
44964
+ return process.env["TODOS_SEAT_ROSTER_PATH"] || join9(homedir3(), ".hasna", "identities", "hasna-seats.roster.json");
44951
44965
  }
44952
44966
  function loadSeatSlugs(path = defaultSeatRosterPath()) {
44953
44967
  try {
@@ -58680,7 +58694,7 @@ async function validateMcpAssignee(value, allowSeat) {
58680
58694
  return verdict.assignee;
58681
58695
  }
58682
58696
  function registerTaskCrudTools(server, ctx) {
58683
- const { shouldRegisterTool, resolveId, formatError: formatError2, formatTask } = ctx;
58697
+ const { shouldRegisterTool, resolveId, formatError: formatError2, formatTask, applyFocus } = ctx;
58684
58698
  function mutationTaskResponse(task) {
58685
58699
  const compact = compactTask(task, 240);
58686
58700
  compact["version"] = task.version;
@@ -58713,7 +58727,7 @@ function registerTaskCrudTools(server, ctx) {
58713
58727
  project_id: exports_external.string().optional().describe("Project ID"),
58714
58728
  task_list_id: exports_external.string().optional().describe("Task list ID"),
58715
58729
  assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
58716
- created_by: exports_external.string().optional().describe("Agent who FILED this task. Defaults to the ambient agent identity (todos init / TODOS_AGENT_ID)."),
58730
+ 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."),
58717
58731
  unassigned: exports_external.boolean().optional().describe("Deliberately file with no assignee. Without it, the task defaults to the filer."),
58718
58732
  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."),
58719
58733
  depends_on: exports_external.array(exports_external.string()).optional().describe("Array of task IDs this task depends on"),
@@ -58728,14 +58742,13 @@ function registerTaskCrudTools(server, ctx) {
58728
58742
  try {
58729
58743
  const { depends_on, assigned_to, project_id, task_list_id, tags, estimate, confidence, retry_count, deadline, created_by, unassigned, allow_seat, ...rest } = params;
58730
58744
  const requestedAssignee = assigned_to ? await validateMcpAssignee(assigned_to, Boolean(allow_seat)) : undefined;
58731
- const creator = resolveCreatorIdentity(created_by);
58732
58745
  const router = resolveWritableIdentity(created_by);
58733
58746
  const assignee = requestedAssignee || (unassigned ? undefined : router.agent_id || undefined);
58734
58747
  const cloud = getTodosCloudClient();
58735
58748
  if (cloud) {
58736
58749
  const payload = { ...rest };
58737
- if (creator.agent_id)
58738
- payload.created_by = creator.agent_id;
58750
+ if (router.agent_id)
58751
+ payload.created_by = router.agent_id;
58739
58752
  if (router.agent_id)
58740
58753
  payload.agent_id = payload.agent_id ?? router.agent_id;
58741
58754
  if (assignee)
@@ -58756,12 +58769,13 @@ function registerTaskCrudTools(server, ctx) {
58756
58769
  payload.max_retries = retry_count;
58757
58770
  if (deadline)
58758
58771
  payload.due_at = deadline;
58772
+ applyFocus(payload, router.agent_id || undefined);
58759
58773
  const created = await cloudCreateTask(cloud, payload);
58760
58774
  return { content: [{ type: "text", text: mutationTaskResponse(created) }] };
58761
58775
  }
58762
58776
  const resolved = { ...rest };
58763
- if (creator.agent_id)
58764
- resolved.created_by = creator.agent_id;
58777
+ if (router.agent_id)
58778
+ resolved.created_by = router.agent_id;
58765
58779
  if (router.agent_id)
58766
58780
  resolved.agent_id = resolved.agent_id ?? router.agent_id;
58767
58781
  if (assignee)
@@ -58782,6 +58796,7 @@ function registerTaskCrudTools(server, ctx) {
58782
58796
  resolved.max_retries = retry_count;
58783
58797
  if (deadline)
58784
58798
  resolved.due_at = deadline;
58799
+ applyFocus(resolved, router.agent_id || undefined);
58785
58800
  const task = createTask2(resolved);
58786
58801
  return { content: [{ type: "text", text: mutationTaskResponse(task) }] };
58787
58802
  } catch (e) {
@@ -105060,7 +105075,8 @@ function buildServer() {
105060
105075
  formatError: formatError2,
105061
105076
  formatTask,
105062
105077
  formatTaskDetail,
105063
- getAgentFocus
105078
+ getAgentFocus,
105079
+ applyFocus
105064
105080
  };
105065
105081
  registerTaskCrudTools(server, toolContext);
105066
105082
  registerTaskProjectTools(server, toolContext);
@@ -1 +1 @@
1
- {"version":3,"file":"v1.d.ts","sourceRoot":"","sources":["../../src/server/v1.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAuB,oBAAoB,EAAmD,MAAM,0BAA0B,CAAC;AAC3I,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKhH,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,OAAO,gBAAgB,CAAC;IACtC,YAAY,CAAC,EAAE,OAAO,iBAAiB,CAAC;IACxC,iBAAiB,CAAC,EAAE,OAAO,sBAAsB,CAAC;IAClD,gBAAgB,CAAC,EAAE,OAAO,qBAAqB,CAAC;CACjD;AA0TD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,CAiB1E;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAapE;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,GAAG,EACR,YAAY,GAAE,qBAA0B,GACvC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA65B1B"}
1
+ {"version":3,"file":"v1.d.ts","sourceRoot":"","sources":["../../src/server/v1.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAuB,oBAAoB,EAAmD,MAAM,0BAA0B,CAAC;AAC3I,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKhH,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,OAAO,gBAAgB,CAAC;IACtC,YAAY,CAAC,EAAE,OAAO,iBAAiB,CAAC;IACxC,iBAAiB,CAAC,EAAE,OAAO,sBAAsB,CAAC;IAClD,gBAAgB,CAAC,EAAE,OAAO,qBAAqB,CAAC;CACjD;AA0TD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,CAiB1E;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAapE;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,GAAG,EACR,YAAY,GAAE,qBAA0B,GACvC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAu6B1B"}
package/dist/storage.js CHANGED
@@ -4020,6 +4020,103 @@ var init_identity_mapping = __esm(() => {
4020
4020
  });
4021
4021
  });
4022
4022
 
4023
+ // src/lib/sync-utils.ts
4024
+ import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
4025
+ import { createHash } from "crypto";
4026
+ import { homedir } from "os";
4027
+ import { join } from "path";
4028
+ function getHomeDir() {
4029
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
4030
+ }
4031
+ function getTodosGlobalDir() {
4032
+ return join(getHomeDir(), ".hasna", "todos");
4033
+ }
4034
+ function ensureDir(dir) {
4035
+ if (!existsSync2(dir))
4036
+ mkdirSync(dir, { recursive: true });
4037
+ }
4038
+ function listJsonFiles(dir) {
4039
+ if (!existsSync2(dir))
4040
+ return [];
4041
+ return readdirSync(dir).filter((f) => f.endsWith(".json"));
4042
+ }
4043
+ function readJsonFile(path) {
4044
+ try {
4045
+ return JSON.parse(readFileSync(path, "utf-8"));
4046
+ } catch {
4047
+ return null;
4048
+ }
4049
+ }
4050
+ function writeJsonFile(path, data) {
4051
+ writeFileSync(path, JSON.stringify(data, null, 2) + `
4052
+ `);
4053
+ }
4054
+ function readHighWaterMark(dir) {
4055
+ const path = join(dir, ".highwatermark");
4056
+ if (!existsSync2(path))
4057
+ return 1;
4058
+ const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
4059
+ return isNaN(val) ? 1 : val;
4060
+ }
4061
+ function writeHighWaterMark(dir, value) {
4062
+ writeFileSync(join(dir, ".highwatermark"), String(value));
4063
+ }
4064
+ function getFileMtimeMs(path) {
4065
+ try {
4066
+ return statSync(path).mtimeMs;
4067
+ } catch {
4068
+ return null;
4069
+ }
4070
+ }
4071
+ function parseTimestamp(value) {
4072
+ if (typeof value !== "string")
4073
+ return null;
4074
+ const parsed = Date.parse(value);
4075
+ return Number.isNaN(parsed) ? null : parsed;
4076
+ }
4077
+ function appendSyncConflict(metadata, conflict, limit = 5) {
4078
+ const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
4079
+ const next = [conflict, ...current].slice(0, limit);
4080
+ return { ...metadata, sync_conflicts: next };
4081
+ }
4082
+ function canonicalize(value) {
4083
+ if (Array.isArray(value))
4084
+ return value.map(canonicalize);
4085
+ if (!value || typeof value !== "object")
4086
+ return value;
4087
+ const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).sort(([a], [b]) => a.localeCompare(b));
4088
+ return Object.fromEntries(entries.map(([key, entryValue]) => [key, canonicalize(entryValue)]));
4089
+ }
4090
+ function withoutSyncFingerprintMetadata(metadata) {
4091
+ const { [TODO_SYNC_FINGERPRINT_KEY]: _fingerprint, ...rest } = metadata;
4092
+ return rest;
4093
+ }
4094
+ function syncFingerprint(record) {
4095
+ const metadata = withoutSyncFingerprintMetadata(record.metadata || {});
4096
+ const canonical = canonicalize({ ...record, metadata });
4097
+ return `sha256:${createHash("sha256").update(JSON.stringify(canonical)).digest("hex")}`;
4098
+ }
4099
+ function withSyncFingerprint(record) {
4100
+ const metadata = withoutSyncFingerprintMetadata(record.metadata);
4101
+ return {
4102
+ ...record,
4103
+ metadata: {
4104
+ ...metadata,
4105
+ [TODO_SYNC_FINGERPRINT_KEY]: syncFingerprint({ ...record, metadata })
4106
+ }
4107
+ };
4108
+ }
4109
+ function hasSyncFingerprintChanged(record) {
4110
+ const stored = record.metadata?.[TODO_SYNC_FINGERPRINT_KEY];
4111
+ if (typeof stored !== "string" || stored.length === 0)
4112
+ return null;
4113
+ return stored !== syncFingerprint(record);
4114
+ }
4115
+ var TODO_SYNC_FINGERPRINT_KEY = "todos_sync_fingerprint", HOME;
4116
+ var init_sync_utils = __esm(() => {
4117
+ HOME = getHomeDir();
4118
+ });
4119
+
4023
4120
  // src/storage/shadow-outbox-schema.ts
4024
4121
  var exports_shadow_outbox_schema = {};
4025
4122
  __export(exports_shadow_outbox_schema, {
@@ -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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.14.0",
3
+ "version": "0.15.2",
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",