@hasna/todos 0.10.5 → 0.10.8

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
@@ -2897,6 +2897,20 @@ var init_database = __esm(() => {
2897
2897
  ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
2898
2898
  CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
2899
2899
  INSERT OR IGNORE INTO _migrations (id) VALUES (30);
2900
+ `,
2901
+ `
2902
+ CREATE TABLE IF NOT EXISTS file_locks (
2903
+ id TEXT PRIMARY KEY,
2904
+ path TEXT NOT NULL UNIQUE,
2905
+ agent_id TEXT NOT NULL,
2906
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
2907
+ expires_at TEXT NOT NULL,
2908
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2909
+ );
2910
+ CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
2911
+ CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
2912
+ CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
2913
+ INSERT OR IGNORE INTO _migrations (id) VALUES (31);
2900
2914
  `
2901
2915
  ];
2902
2916
  });
@@ -4807,7 +4821,7 @@ function registerAgent(input, db) {
4807
4821
  session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
4808
4822
  working_dir: existing.working_dir,
4809
4823
  suggestions: suggestions.slice(0, 5),
4810
- message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(0, 8)}\u2026, dir: ${existing.working_dir ?? "unknown"}). Are you that agent? If so, pass session_id="${existing.session_id}" to reclaim it. Otherwise choose a different name.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
4824
+ message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session \u2026${existing.session_id?.slice(-4)}, dir: ${existing.working_dir ?? "unknown"}). Cannot reclaim an active agent \u2014 choose a different name, or wait for the session to go stale.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
4811
4825
  };
4812
4826
  }
4813
4827
  const updates = ["last_seen_at = ?", "status = 'active'"];
@@ -9878,6 +9892,74 @@ var init_task_files = __esm(() => {
9878
9892
  init_database();
9879
9893
  });
9880
9894
 
9895
+ // src/db/file-locks.ts
9896
+ var exports_file_locks = {};
9897
+ __export(exports_file_locks, {
9898
+ unlockFile: () => unlockFile,
9899
+ lockFile: () => lockFile,
9900
+ listFileLocks: () => listFileLocks,
9901
+ forceUnlockFile: () => forceUnlockFile,
9902
+ cleanExpiredFileLocks: () => cleanExpiredFileLocks,
9903
+ checkFileLock: () => checkFileLock,
9904
+ FILE_LOCK_DEFAULT_TTL_SECONDS: () => FILE_LOCK_DEFAULT_TTL_SECONDS
9905
+ });
9906
+ function expiresAt(ttlSeconds) {
9907
+ return new Date(Date.now() + ttlSeconds * 1000).toISOString();
9908
+ }
9909
+ function cleanExpiredFileLocks(db) {
9910
+ const d = db || getDatabase();
9911
+ const result = d.run("DELETE FROM file_locks WHERE expires_at <= ?", [now()]);
9912
+ return result.changes;
9913
+ }
9914
+ function lockFile(input, db) {
9915
+ const d = db || getDatabase();
9916
+ const ttl = input.ttl_seconds ?? FILE_LOCK_DEFAULT_TTL_SECONDS;
9917
+ const expiry = expiresAt(ttl);
9918
+ const timestamp = now();
9919
+ cleanExpiredFileLocks(d);
9920
+ const existing = d.query("SELECT * FROM file_locks WHERE path = ?").get(input.path);
9921
+ if (existing) {
9922
+ if (existing.agent_id === input.agent_id) {
9923
+ d.run("UPDATE file_locks SET expires_at = ?, task_id = COALESCE(?, task_id) WHERE id = ?", [expiry, input.task_id ?? null, existing.id]);
9924
+ return d.query("SELECT * FROM file_locks WHERE id = ?").get(existing.id);
9925
+ }
9926
+ throw new LockError(input.path, existing.agent_id);
9927
+ }
9928
+ const id = uuid();
9929
+ d.run("INSERT INTO file_locks (id, path, agent_id, task_id, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?)", [id, input.path, input.agent_id, input.task_id ?? null, expiry, timestamp]);
9930
+ return d.query("SELECT * FROM file_locks WHERE id = ?").get(id);
9931
+ }
9932
+ function unlockFile(path, agentId, db) {
9933
+ const d = db || getDatabase();
9934
+ cleanExpiredFileLocks(d);
9935
+ const result = d.run("DELETE FROM file_locks WHERE path = ? AND agent_id = ?", [path, agentId]);
9936
+ return result.changes > 0;
9937
+ }
9938
+ function checkFileLock(path, db) {
9939
+ const d = db || getDatabase();
9940
+ cleanExpiredFileLocks(d);
9941
+ return d.query("SELECT * FROM file_locks WHERE path = ?").get(path);
9942
+ }
9943
+ function listFileLocks(agentId, db) {
9944
+ const d = db || getDatabase();
9945
+ cleanExpiredFileLocks(d);
9946
+ if (agentId) {
9947
+ return d.query("SELECT * FROM file_locks WHERE agent_id = ? ORDER BY created_at DESC").all(agentId);
9948
+ }
9949
+ return d.query("SELECT * FROM file_locks ORDER BY created_at DESC").all();
9950
+ }
9951
+ function forceUnlockFile(path, db) {
9952
+ const d = db || getDatabase();
9953
+ const result = d.run("DELETE FROM file_locks WHERE path = ?", [path]);
9954
+ return result.changes > 0;
9955
+ }
9956
+ var FILE_LOCK_DEFAULT_TTL_SECONDS;
9957
+ var init_file_locks = __esm(() => {
9958
+ init_database();
9959
+ init_types();
9960
+ FILE_LOCK_DEFAULT_TTL_SECONDS = 30 * 60;
9961
+ });
9962
+
9881
9963
  // src/db/handoffs.ts
9882
9964
  var exports_handoffs = {};
9883
9965
  __export(exports_handoffs, {
@@ -13279,6 +13361,64 @@ ${lines.join(`
13279
13361
  }
13280
13362
  });
13281
13363
  }
13364
+ if (shouldRegisterTool("lock_file")) {
13365
+ server.tool("lock_file", "Acquire an exclusive lock on a file path. Throws if another agent holds an active lock. Same agent re-locks refreshes the TTL.", {
13366
+ path: exports_external.string().describe("File path to lock"),
13367
+ agent_id: exports_external.string().describe("Agent acquiring the lock"),
13368
+ task_id: exports_external.string().optional().describe("Task this lock is associated with"),
13369
+ ttl_seconds: exports_external.number().optional().describe("Lock TTL in seconds (default: 1800 = 30 min)")
13370
+ }, async ({ path, agent_id, task_id, ttl_seconds }) => {
13371
+ try {
13372
+ const { lockFile: lockFile2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
13373
+ const lock = lockFile2({ path, agent_id, task_id, ttl_seconds });
13374
+ return { content: [{ type: "text", text: JSON.stringify(lock, null, 2) }] };
13375
+ } catch (e) {
13376
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13377
+ }
13378
+ });
13379
+ }
13380
+ if (shouldRegisterTool("unlock_file")) {
13381
+ server.tool("unlock_file", "Release a file lock. Only the lock holder can release it. Returns true if released.", {
13382
+ path: exports_external.string().describe("File path to unlock"),
13383
+ agent_id: exports_external.string().describe("Agent releasing the lock (must be the lock holder)")
13384
+ }, async ({ path, agent_id }) => {
13385
+ try {
13386
+ const { unlockFile: unlockFile2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
13387
+ const released = unlockFile2(path, agent_id);
13388
+ return { content: [{ type: "text", text: JSON.stringify({ released, path }) }] };
13389
+ } catch (e) {
13390
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13391
+ }
13392
+ });
13393
+ }
13394
+ if (shouldRegisterTool("check_file_lock")) {
13395
+ server.tool("check_file_lock", "Check who holds a lock on a file path. Returns null if unlocked or expired.", {
13396
+ path: exports_external.string().describe("File path to check")
13397
+ }, async ({ path }) => {
13398
+ try {
13399
+ const { checkFileLock: checkFileLock2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
13400
+ const lock = checkFileLock2(path);
13401
+ if (!lock)
13402
+ return { content: [{ type: "text", text: JSON.stringify({ path, locked: false }) }] };
13403
+ return { content: [{ type: "text", text: JSON.stringify({ path, locked: true, ...lock }) }] };
13404
+ } catch (e) {
13405
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13406
+ }
13407
+ });
13408
+ }
13409
+ if (shouldRegisterTool("list_file_locks")) {
13410
+ server.tool("list_file_locks", "List all active file locks. Optionally filter by agent_id.", {
13411
+ agent_id: exports_external.string().optional().describe("Filter locks by agent")
13412
+ }, async ({ agent_id }) => {
13413
+ try {
13414
+ const { listFileLocks: listFileLocks2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
13415
+ const locks = listFileLocks2(agent_id);
13416
+ return { content: [{ type: "text", text: JSON.stringify(locks, null, 2) }] };
13417
+ } catch (e) {
13418
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13419
+ }
13420
+ });
13421
+ }
13282
13422
  if (shouldRegisterTool("create_handoff")) {
13283
13423
  server.tool("create_handoff", "Create a session handoff note for agent coordination.", {
13284
13424
  agent_id: exports_external.string().optional().describe("Agent creating the handoff"),
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/db/agents.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/db/agents.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AA8gBtC,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAkBrD;AAyRD,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,IAAI,IAAI,MAAM,CAE7B;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAK9D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,SAAa,GAAG,MAAM,CAG3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAGpD;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0B9F"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AA6hBtC,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAkBrD;AAyRD,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,IAAI,IAAI,MAAM,CAE7B;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAK9D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,SAAa,GAAG,MAAM,CAG3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAGpD;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0B9F"}
@@ -0,0 +1,43 @@
1
+ import type { Database } from "bun:sqlite";
2
+ export declare const FILE_LOCK_DEFAULT_TTL_SECONDS: number;
3
+ export interface FileLock {
4
+ id: string;
5
+ path: string;
6
+ agent_id: string;
7
+ task_id: string | null;
8
+ expires_at: string;
9
+ created_at: string;
10
+ }
11
+ export interface LockFileInput {
12
+ path: string;
13
+ agent_id: string;
14
+ task_id?: string;
15
+ /** TTL in seconds (default: 1800 = 30 min) */
16
+ ttl_seconds?: number;
17
+ }
18
+ /** Clean up expired locks. Called automatically on read operations. */
19
+ export declare function cleanExpiredFileLocks(db?: Database): number;
20
+ /**
21
+ * Acquire an exclusive lock on a file path.
22
+ * - If no lock exists (or existing lock is expired), lock is granted.
23
+ * - If same agent already holds the lock, the TTL is refreshed.
24
+ * - If another agent holds an active lock, throws LockError.
25
+ */
26
+ export declare function lockFile(input: LockFileInput, db?: Database): FileLock;
27
+ /**
28
+ * Release a file lock. Only the lock holder can release it.
29
+ * Returns true if released, false if not found or wrong agent.
30
+ */
31
+ export declare function unlockFile(path: string, agentId: string, db?: Database): boolean;
32
+ /**
33
+ * Check who holds a lock on a file path.
34
+ * Returns null if unlocked or expired.
35
+ */
36
+ export declare function checkFileLock(path: string, db?: Database): FileLock | null;
37
+ /**
38
+ * List all active (non-expired) file locks, optionally filtered by agent.
39
+ */
40
+ export declare function listFileLocks(agentId?: string, db?: Database): FileLock[];
41
+ /** Force-release a lock regardless of which agent holds it (admin operation). */
42
+ export declare function forceUnlockFile(path: string, db?: Database): boolean;
43
+ //# sourceMappingURL=file-locks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-locks.d.ts","sourceRoot":"","sources":["../../src/db/file-locks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3C,eAAO,MAAM,6BAA6B,QAAU,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAMD,uEAAuE;AACvE,wBAAgB,qBAAqB,CAAC,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAI3D;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CA8BtE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAQhF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAI1E;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAOzE;AAED,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAIpE"}
package/dist/index.js CHANGED
@@ -702,6 +702,20 @@ var MIGRATIONS = [
702
702
  ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
703
703
  CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
704
704
  INSERT OR IGNORE INTO _migrations (id) VALUES (30);
705
+ `,
706
+ `
707
+ CREATE TABLE IF NOT EXISTS file_locks (
708
+ id TEXT PRIMARY KEY,
709
+ path TEXT NOT NULL UNIQUE,
710
+ agent_id TEXT NOT NULL,
711
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
712
+ expires_at TEXT NOT NULL,
713
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
714
+ );
715
+ CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
716
+ CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
717
+ CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
718
+ INSERT OR IGNORE INTO _migrations (id) VALUES (31);
705
719
  `
706
720
  ];
707
721
  var _db = null;
@@ -2871,7 +2885,7 @@ function registerAgent(input, db) {
2871
2885
  session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
2872
2886
  working_dir: existing.working_dir,
2873
2887
  suggestions: suggestions.slice(0, 5),
2874
- message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(0, 8)}\u2026, dir: ${existing.working_dir ?? "unknown"}). Are you that agent? If so, pass session_id="${existing.session_id}" to reclaim it. Otherwise choose a different name.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
2888
+ message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session \u2026${existing.session_id?.slice(-4)}, dir: ${existing.working_dir ?? "unknown"}). Cannot reclaim an active agent \u2014 choose a different name, or wait for the session to go stale.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
2875
2889
  };
2876
2890
  }
2877
2891
  const updates = ["last_seen_at = ?", "status = 'active'"];
package/dist/mcp/index.js CHANGED
@@ -39,6 +39,111 @@ var __export = (target, all) => {
39
39
  };
40
40
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
41
41
 
42
+ // src/types/index.ts
43
+ var VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError;
44
+ var init_types = __esm(() => {
45
+ VersionConflictError = class VersionConflictError extends Error {
46
+ taskId;
47
+ expectedVersion;
48
+ actualVersion;
49
+ static code = "VERSION_CONFLICT";
50
+ static suggestion = "Fetch the task with get_task to get the current version before updating.";
51
+ constructor(taskId, expectedVersion, actualVersion) {
52
+ super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
53
+ this.taskId = taskId;
54
+ this.expectedVersion = expectedVersion;
55
+ this.actualVersion = actualVersion;
56
+ this.name = "VersionConflictError";
57
+ }
58
+ };
59
+ TaskNotFoundError = class TaskNotFoundError extends Error {
60
+ taskId;
61
+ static code = "TASK_NOT_FOUND";
62
+ static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
63
+ constructor(taskId) {
64
+ super(`Task not found: ${taskId}`);
65
+ this.taskId = taskId;
66
+ this.name = "TaskNotFoundError";
67
+ }
68
+ };
69
+ ProjectNotFoundError = class ProjectNotFoundError extends Error {
70
+ projectId;
71
+ static code = "PROJECT_NOT_FOUND";
72
+ static suggestion = "Use list_projects to see available projects.";
73
+ constructor(projectId) {
74
+ super(`Project not found: ${projectId}`);
75
+ this.projectId = projectId;
76
+ this.name = "ProjectNotFoundError";
77
+ }
78
+ };
79
+ PlanNotFoundError = class PlanNotFoundError extends Error {
80
+ planId;
81
+ static code = "PLAN_NOT_FOUND";
82
+ static suggestion = "Use list_plans to see available plans.";
83
+ constructor(planId) {
84
+ super(`Plan not found: ${planId}`);
85
+ this.planId = planId;
86
+ this.name = "PlanNotFoundError";
87
+ }
88
+ };
89
+ LockError = class LockError extends Error {
90
+ taskId;
91
+ lockedBy;
92
+ static code = "LOCK_ERROR";
93
+ static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
94
+ constructor(taskId, lockedBy) {
95
+ super(`Task ${taskId} is locked by ${lockedBy}`);
96
+ this.taskId = taskId;
97
+ this.lockedBy = lockedBy;
98
+ this.name = "LockError";
99
+ }
100
+ };
101
+ AgentNotFoundError = class AgentNotFoundError extends Error {
102
+ agentId;
103
+ static code = "AGENT_NOT_FOUND";
104
+ static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
105
+ constructor(agentId) {
106
+ super(`Agent not found: ${agentId}`);
107
+ this.agentId = agentId;
108
+ this.name = "AgentNotFoundError";
109
+ }
110
+ };
111
+ TaskListNotFoundError = class TaskListNotFoundError extends Error {
112
+ taskListId;
113
+ static code = "TASK_LIST_NOT_FOUND";
114
+ static suggestion = "Use list_task_lists to see available lists.";
115
+ constructor(taskListId) {
116
+ super(`Task list not found: ${taskListId}`);
117
+ this.taskListId = taskListId;
118
+ this.name = "TaskListNotFoundError";
119
+ }
120
+ };
121
+ DependencyCycleError = class DependencyCycleError extends Error {
122
+ taskId;
123
+ dependsOn;
124
+ static code = "DEPENDENCY_CYCLE";
125
+ static suggestion = "Check the dependency chain with get_task to avoid circular references.";
126
+ constructor(taskId, dependsOn) {
127
+ super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
128
+ this.taskId = taskId;
129
+ this.dependsOn = dependsOn;
130
+ this.name = "DependencyCycleError";
131
+ }
132
+ };
133
+ CompletionGuardError = class CompletionGuardError extends Error {
134
+ reason;
135
+ retryAfterSeconds;
136
+ static code = "COMPLETION_BLOCKED";
137
+ static suggestion = "Wait for the cooldown period, then retry.";
138
+ constructor(reason, retryAfterSeconds) {
139
+ super(reason);
140
+ this.reason = reason;
141
+ this.retryAfterSeconds = retryAfterSeconds;
142
+ this.name = "CompletionGuardError";
143
+ }
144
+ };
145
+ });
146
+
42
147
  // src/db/database.ts
43
148
  var exports_database = {};
44
149
  __export(exports_database, {
@@ -837,6 +942,20 @@ var init_database = __esm(() => {
837
942
  ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
838
943
  CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
839
944
  INSERT OR IGNORE INTO _migrations (id) VALUES (30);
945
+ `,
946
+ `
947
+ CREATE TABLE IF NOT EXISTS file_locks (
948
+ id TEXT PRIMARY KEY,
949
+ path TEXT NOT NULL UNIQUE,
950
+ agent_id TEXT NOT NULL,
951
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
952
+ expires_at TEXT NOT NULL,
953
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
954
+ );
955
+ CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
956
+ CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
957
+ CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
958
+ INSERT OR IGNORE INTO _migrations (id) VALUES (31);
840
959
  `
841
960
  ];
842
961
  });
@@ -1042,7 +1161,7 @@ function registerAgent(input, db) {
1042
1161
  session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
1043
1162
  working_dir: existing.working_dir,
1044
1163
  suggestions: suggestions.slice(0, 5),
1045
- message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(0, 8)}\u2026, dir: ${existing.working_dir ?? "unknown"}). Are you that agent? If so, pass session_id="${existing.session_id}" to reclaim it. Otherwise choose a different name.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
1164
+ message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session \u2026${existing.session_id?.slice(-4)}, dir: ${existing.working_dir ?? "unknown"}). Cannot reclaim an active agent \u2014 choose a different name, or wait for the session to go stale.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
1046
1165
  };
1047
1166
  }
1048
1167
  const updates = ["last_seen_at = ?", "status = 'active'"];
@@ -1318,6 +1437,74 @@ var init_task_files = __esm(() => {
1318
1437
  init_database();
1319
1438
  });
1320
1439
 
1440
+ // src/db/file-locks.ts
1441
+ var exports_file_locks = {};
1442
+ __export(exports_file_locks, {
1443
+ unlockFile: () => unlockFile,
1444
+ lockFile: () => lockFile,
1445
+ listFileLocks: () => listFileLocks,
1446
+ forceUnlockFile: () => forceUnlockFile,
1447
+ cleanExpiredFileLocks: () => cleanExpiredFileLocks,
1448
+ checkFileLock: () => checkFileLock,
1449
+ FILE_LOCK_DEFAULT_TTL_SECONDS: () => FILE_LOCK_DEFAULT_TTL_SECONDS
1450
+ });
1451
+ function expiresAt(ttlSeconds) {
1452
+ return new Date(Date.now() + ttlSeconds * 1000).toISOString();
1453
+ }
1454
+ function cleanExpiredFileLocks(db) {
1455
+ const d = db || getDatabase();
1456
+ const result = d.run("DELETE FROM file_locks WHERE expires_at <= ?", [now()]);
1457
+ return result.changes;
1458
+ }
1459
+ function lockFile(input, db) {
1460
+ const d = db || getDatabase();
1461
+ const ttl = input.ttl_seconds ?? FILE_LOCK_DEFAULT_TTL_SECONDS;
1462
+ const expiry = expiresAt(ttl);
1463
+ const timestamp = now();
1464
+ cleanExpiredFileLocks(d);
1465
+ const existing = d.query("SELECT * FROM file_locks WHERE path = ?").get(input.path);
1466
+ if (existing) {
1467
+ if (existing.agent_id === input.agent_id) {
1468
+ d.run("UPDATE file_locks SET expires_at = ?, task_id = COALESCE(?, task_id) WHERE id = ?", [expiry, input.task_id ?? null, existing.id]);
1469
+ return d.query("SELECT * FROM file_locks WHERE id = ?").get(existing.id);
1470
+ }
1471
+ throw new LockError(input.path, existing.agent_id);
1472
+ }
1473
+ const id = uuid();
1474
+ d.run("INSERT INTO file_locks (id, path, agent_id, task_id, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?)", [id, input.path, input.agent_id, input.task_id ?? null, expiry, timestamp]);
1475
+ return d.query("SELECT * FROM file_locks WHERE id = ?").get(id);
1476
+ }
1477
+ function unlockFile(path, agentId, db) {
1478
+ const d = db || getDatabase();
1479
+ cleanExpiredFileLocks(d);
1480
+ const result = d.run("DELETE FROM file_locks WHERE path = ? AND agent_id = ?", [path, agentId]);
1481
+ return result.changes > 0;
1482
+ }
1483
+ function checkFileLock(path, db) {
1484
+ const d = db || getDatabase();
1485
+ cleanExpiredFileLocks(d);
1486
+ return d.query("SELECT * FROM file_locks WHERE path = ?").get(path);
1487
+ }
1488
+ function listFileLocks(agentId, db) {
1489
+ const d = db || getDatabase();
1490
+ cleanExpiredFileLocks(d);
1491
+ if (agentId) {
1492
+ return d.query("SELECT * FROM file_locks WHERE agent_id = ? ORDER BY created_at DESC").all(agentId);
1493
+ }
1494
+ return d.query("SELECT * FROM file_locks ORDER BY created_at DESC").all();
1495
+ }
1496
+ function forceUnlockFile(path, db) {
1497
+ const d = db || getDatabase();
1498
+ const result = d.run("DELETE FROM file_locks WHERE path = ?", [path]);
1499
+ return result.changes > 0;
1500
+ }
1501
+ var FILE_LOCK_DEFAULT_TTL_SECONDS;
1502
+ var init_file_locks = __esm(() => {
1503
+ init_database();
1504
+ init_types();
1505
+ FILE_LOCK_DEFAULT_TTL_SECONDS = 30 * 60;
1506
+ });
1507
+
1321
1508
  // src/db/handoffs.ts
1322
1509
  var exports_handoffs = {};
1323
1510
  __export(exports_handoffs, {
@@ -5944,120 +6131,12 @@ var coerce = {
5944
6131
  date: (arg) => ZodDate.create({ ...arg, coerce: true })
5945
6132
  };
5946
6133
  var NEVER = INVALID;
5947
- // src/types/index.ts
5948
- class VersionConflictError extends Error {
5949
- taskId;
5950
- expectedVersion;
5951
- actualVersion;
5952
- static code = "VERSION_CONFLICT";
5953
- static suggestion = "Fetch the task with get_task to get the current version before updating.";
5954
- constructor(taskId, expectedVersion, actualVersion) {
5955
- super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
5956
- this.taskId = taskId;
5957
- this.expectedVersion = expectedVersion;
5958
- this.actualVersion = actualVersion;
5959
- this.name = "VersionConflictError";
5960
- }
5961
- }
5962
-
5963
- class TaskNotFoundError extends Error {
5964
- taskId;
5965
- static code = "TASK_NOT_FOUND";
5966
- static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
5967
- constructor(taskId) {
5968
- super(`Task not found: ${taskId}`);
5969
- this.taskId = taskId;
5970
- this.name = "TaskNotFoundError";
5971
- }
5972
- }
5973
-
5974
- class ProjectNotFoundError extends Error {
5975
- projectId;
5976
- static code = "PROJECT_NOT_FOUND";
5977
- static suggestion = "Use list_projects to see available projects.";
5978
- constructor(projectId) {
5979
- super(`Project not found: ${projectId}`);
5980
- this.projectId = projectId;
5981
- this.name = "ProjectNotFoundError";
5982
- }
5983
- }
5984
-
5985
- class PlanNotFoundError extends Error {
5986
- planId;
5987
- static code = "PLAN_NOT_FOUND";
5988
- static suggestion = "Use list_plans to see available plans.";
5989
- constructor(planId) {
5990
- super(`Plan not found: ${planId}`);
5991
- this.planId = planId;
5992
- this.name = "PlanNotFoundError";
5993
- }
5994
- }
5995
-
5996
- class LockError extends Error {
5997
- taskId;
5998
- lockedBy;
5999
- static code = "LOCK_ERROR";
6000
- static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
6001
- constructor(taskId, lockedBy) {
6002
- super(`Task ${taskId} is locked by ${lockedBy}`);
6003
- this.taskId = taskId;
6004
- this.lockedBy = lockedBy;
6005
- this.name = "LockError";
6006
- }
6007
- }
6008
-
6009
- class AgentNotFoundError extends Error {
6010
- agentId;
6011
- static code = "AGENT_NOT_FOUND";
6012
- static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
6013
- constructor(agentId) {
6014
- super(`Agent not found: ${agentId}`);
6015
- this.agentId = agentId;
6016
- this.name = "AgentNotFoundError";
6017
- }
6018
- }
6019
-
6020
- class TaskListNotFoundError extends Error {
6021
- taskListId;
6022
- static code = "TASK_LIST_NOT_FOUND";
6023
- static suggestion = "Use list_task_lists to see available lists.";
6024
- constructor(taskListId) {
6025
- super(`Task list not found: ${taskListId}`);
6026
- this.taskListId = taskListId;
6027
- this.name = "TaskListNotFoundError";
6028
- }
6029
- }
6030
-
6031
- class DependencyCycleError extends Error {
6032
- taskId;
6033
- dependsOn;
6034
- static code = "DEPENDENCY_CYCLE";
6035
- static suggestion = "Check the dependency chain with get_task to avoid circular references.";
6036
- constructor(taskId, dependsOn) {
6037
- super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
6038
- this.taskId = taskId;
6039
- this.dependsOn = dependsOn;
6040
- this.name = "DependencyCycleError";
6041
- }
6042
- }
6043
-
6044
- class CompletionGuardError extends Error {
6045
- reason;
6046
- retryAfterSeconds;
6047
- static code = "COMPLETION_BLOCKED";
6048
- static suggestion = "Wait for the cooldown period, then retry.";
6049
- constructor(reason, retryAfterSeconds) {
6050
- super(reason);
6051
- this.reason = reason;
6052
- this.retryAfterSeconds = retryAfterSeconds;
6053
- this.name = "CompletionGuardError";
6054
- }
6055
- }
6056
-
6057
6134
  // src/db/tasks.ts
6135
+ init_types();
6058
6136
  init_database();
6059
6137
 
6060
6138
  // src/db/projects.ts
6139
+ init_types();
6061
6140
  init_database();
6062
6141
  function slugify(name) {
6063
6142
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -6146,6 +6225,9 @@ function nextTaskShortId(projectId, db) {
6146
6225
  return `${updated.task_prefix}-${padded}`;
6147
6226
  }
6148
6227
 
6228
+ // src/lib/completion-guard.ts
6229
+ init_types();
6230
+
6149
6231
  // src/lib/config.ts
6150
6232
  import { existsSync as existsSync3 } from "fs";
6151
6233
  import { join as join3 } from "path";
@@ -7489,6 +7571,7 @@ function bulkUpdateTasks(taskIds, updates, db) {
7489
7571
  }
7490
7572
 
7491
7573
  // src/db/comments.ts
7574
+ init_types();
7492
7575
  init_database();
7493
7576
  function addComment(input, db) {
7494
7577
  const d = db || getDatabase();
@@ -7519,6 +7602,7 @@ function getComment(id, db) {
7519
7602
  }
7520
7603
 
7521
7604
  // src/db/plans.ts
7605
+ init_types();
7522
7606
  init_database();
7523
7607
  function createPlan(input, db) {
7524
7608
  const d = db || getDatabase();
@@ -7591,6 +7675,7 @@ function deletePlan(id, db) {
7591
7675
  init_agents();
7592
7676
 
7593
7677
  // src/db/task-lists.ts
7678
+ init_types();
7594
7679
  init_database();
7595
7680
  function rowToTaskList(row) {
7596
7681
  return {
@@ -8277,6 +8362,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
8277
8362
 
8278
8363
  // src/mcp/index.ts
8279
8364
  init_database();
8365
+ init_types();
8280
8366
  import { readFileSync as readFileSync3 } from "fs";
8281
8367
  import { join as join6, dirname as dirname2 } from "path";
8282
8368
  import { fileURLToPath } from "url";
@@ -11003,6 +11089,64 @@ if (shouldRegisterTool("list_active_files")) {
11003
11089
  }
11004
11090
  });
11005
11091
  }
11092
+ if (shouldRegisterTool("lock_file")) {
11093
+ server.tool("lock_file", "Acquire an exclusive lock on a file path. Throws if another agent holds an active lock. Same agent re-locks refreshes the TTL.", {
11094
+ path: exports_external.string().describe("File path to lock"),
11095
+ agent_id: exports_external.string().describe("Agent acquiring the lock"),
11096
+ task_id: exports_external.string().optional().describe("Task this lock is associated with"),
11097
+ ttl_seconds: exports_external.number().optional().describe("Lock TTL in seconds (default: 1800 = 30 min)")
11098
+ }, async ({ path, agent_id, task_id, ttl_seconds }) => {
11099
+ try {
11100
+ const { lockFile: lockFile2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
11101
+ const lock = lockFile2({ path, agent_id, task_id, ttl_seconds });
11102
+ return { content: [{ type: "text", text: JSON.stringify(lock, null, 2) }] };
11103
+ } catch (e) {
11104
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11105
+ }
11106
+ });
11107
+ }
11108
+ if (shouldRegisterTool("unlock_file")) {
11109
+ server.tool("unlock_file", "Release a file lock. Only the lock holder can release it. Returns true if released.", {
11110
+ path: exports_external.string().describe("File path to unlock"),
11111
+ agent_id: exports_external.string().describe("Agent releasing the lock (must be the lock holder)")
11112
+ }, async ({ path, agent_id }) => {
11113
+ try {
11114
+ const { unlockFile: unlockFile2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
11115
+ const released = unlockFile2(path, agent_id);
11116
+ return { content: [{ type: "text", text: JSON.stringify({ released, path }) }] };
11117
+ } catch (e) {
11118
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11119
+ }
11120
+ });
11121
+ }
11122
+ if (shouldRegisterTool("check_file_lock")) {
11123
+ server.tool("check_file_lock", "Check who holds a lock on a file path. Returns null if unlocked or expired.", {
11124
+ path: exports_external.string().describe("File path to check")
11125
+ }, async ({ path }) => {
11126
+ try {
11127
+ const { checkFileLock: checkFileLock2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
11128
+ const lock = checkFileLock2(path);
11129
+ if (!lock)
11130
+ return { content: [{ type: "text", text: JSON.stringify({ path, locked: false }) }] };
11131
+ return { content: [{ type: "text", text: JSON.stringify({ path, locked: true, ...lock }) }] };
11132
+ } catch (e) {
11133
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11134
+ }
11135
+ });
11136
+ }
11137
+ if (shouldRegisterTool("list_file_locks")) {
11138
+ server.tool("list_file_locks", "List all active file locks. Optionally filter by agent_id.", {
11139
+ agent_id: exports_external.string().optional().describe("Filter locks by agent")
11140
+ }, async ({ agent_id }) => {
11141
+ try {
11142
+ const { listFileLocks: listFileLocks2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
11143
+ const locks = listFileLocks2(agent_id);
11144
+ return { content: [{ type: "text", text: JSON.stringify(locks, null, 2) }] };
11145
+ } catch (e) {
11146
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11147
+ }
11148
+ });
11149
+ }
11006
11150
  if (shouldRegisterTool("create_handoff")) {
11007
11151
  server.tool("create_handoff", "Create a session handoff note for agent coordination.", {
11008
11152
  agent_id: exports_external.string().optional().describe("Agent creating the handoff"),
@@ -858,6 +858,20 @@ var init_database = __esm(() => {
858
858
  ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
859
859
  CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
860
860
  INSERT OR IGNORE INTO _migrations (id) VALUES (30);
861
+ `,
862
+ `
863
+ CREATE TABLE IF NOT EXISTS file_locks (
864
+ id TEXT PRIMARY KEY,
865
+ path TEXT NOT NULL UNIQUE,
866
+ agent_id TEXT NOT NULL,
867
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
868
+ expires_at TEXT NOT NULL,
869
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
870
+ );
871
+ CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
872
+ CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
873
+ CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
874
+ INSERT OR IGNORE INTO _migrations (id) VALUES (31);
861
875
  `
862
876
  ];
863
877
  });
@@ -2523,7 +2537,7 @@ function registerAgent(input, db) {
2523
2537
  session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
2524
2538
  working_dir: existing.working_dir,
2525
2539
  suggestions: suggestions.slice(0, 5),
2526
- message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(0, 8)}\u2026, dir: ${existing.working_dir ?? "unknown"}). Are you that agent? If so, pass session_id="${existing.session_id}" to reclaim it. Otherwise choose a different name.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
2540
+ message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session \u2026${existing.session_id?.slice(-4)}, dir: ${existing.working_dir ?? "unknown"}). Cannot reclaim an active agent \u2014 choose a different name, or wait for the session to go stale.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
2527
2541
  };
2528
2542
  }
2529
2543
  const updates = ["last_seen_at = ?", "status = 'active'"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.10.5",
3
+ "version": "0.10.8",
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",