@hasna/todos 0.10.4 → 0.10.7

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
@@ -6,39 +6,60 @@ var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ function __accessProp(key) {
10
+ return this[key];
11
+ }
12
+ var __toESMCache_node;
13
+ var __toESMCache_esm;
9
14
  var __toESM = (mod, isNodeMode, target) => {
15
+ var canCache = mod != null && typeof mod === "object";
16
+ if (canCache) {
17
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
18
+ var cached = cache.get(mod);
19
+ if (cached)
20
+ return cached;
21
+ }
10
22
  target = mod != null ? __create(__getProtoOf(mod)) : {};
11
23
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
12
24
  for (let key of __getOwnPropNames(mod))
13
25
  if (!__hasOwnProp.call(to, key))
14
26
  __defProp(to, key, {
15
- get: () => mod[key],
27
+ get: __accessProp.bind(mod, key),
16
28
  enumerable: true
17
29
  });
30
+ if (canCache)
31
+ cache.set(mod, to);
18
32
  return to;
19
33
  };
20
- var __moduleCache = /* @__PURE__ */ new WeakMap;
21
34
  var __toCommonJS = (from) => {
22
- var entry = __moduleCache.get(from), desc;
35
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
23
36
  if (entry)
24
37
  return entry;
25
38
  entry = __defProp({}, "__esModule", { value: true });
26
- if (from && typeof from === "object" || typeof from === "function")
27
- __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
28
- get: () => from[key],
29
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
30
- }));
39
+ if (from && typeof from === "object" || typeof from === "function") {
40
+ for (var key of __getOwnPropNames(from))
41
+ if (!__hasOwnProp.call(entry, key))
42
+ __defProp(entry, key, {
43
+ get: __accessProp.bind(from, key),
44
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
45
+ });
46
+ }
31
47
  __moduleCache.set(from, entry);
32
48
  return entry;
33
49
  };
50
+ var __moduleCache;
34
51
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
52
+ var __returnValue = (v) => v;
53
+ function __exportSetter(name, newValue) {
54
+ this[name] = __returnValue.bind(null, newValue);
55
+ }
35
56
  var __export = (target, all) => {
36
57
  for (var name in all)
37
58
  __defProp(target, name, {
38
59
  get: all[name],
39
60
  enumerable: true,
40
61
  configurable: true,
41
- set: (newValue) => all[name] = () => newValue
62
+ set: __exportSetter.bind(all, name)
42
63
  });
43
64
  };
44
65
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -2876,6 +2897,20 @@ var init_database = __esm(() => {
2876
2897
  ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
2877
2898
  CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
2878
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);
2879
2914
  `
2880
2915
  ];
2881
2916
  });
@@ -9774,6 +9809,7 @@ __export(exports_task_files, {
9774
9809
  updateTaskFileStatus: () => updateTaskFileStatus,
9775
9810
  removeTaskFile: () => removeTaskFile,
9776
9811
  listTaskFiles: () => listTaskFiles,
9812
+ listActiveFiles: () => listActiveFiles,
9777
9813
  getTaskFile: () => getTaskFile,
9778
9814
  findTasksByFile: () => findTasksByFile,
9779
9815
  bulkAddTaskFiles: () => bulkAddTaskFiles,
@@ -9816,6 +9852,31 @@ function removeTaskFile(taskId, path, db) {
9816
9852
  const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
9817
9853
  return result.changes > 0;
9818
9854
  }
9855
+ function listActiveFiles(db) {
9856
+ const d = db || getDatabase();
9857
+ return d.query(`
9858
+ SELECT
9859
+ tf.path,
9860
+ tf.status AS file_status,
9861
+ tf.agent_id AS file_agent_id,
9862
+ tf.note,
9863
+ tf.updated_at,
9864
+ t.id AS task_id,
9865
+ t.short_id AS task_short_id,
9866
+ t.title AS task_title,
9867
+ t.status AS task_status,
9868
+ t.locked_by AS task_locked_by,
9869
+ t.locked_at AS task_locked_at,
9870
+ a.id AS agent_id,
9871
+ a.name AS agent_name
9872
+ FROM task_files tf
9873
+ JOIN tasks t ON tf.task_id = t.id
9874
+ LEFT JOIN agents a ON (tf.agent_id = a.id OR (tf.agent_id IS NULL AND t.assigned_to = a.id))
9875
+ WHERE t.status = 'in_progress'
9876
+ AND tf.status != 'removed'
9877
+ ORDER BY tf.updated_at DESC
9878
+ `).all();
9879
+ }
9819
9880
  function bulkAddTaskFiles(taskId, paths, agentId, db) {
9820
9881
  const d = db || getDatabase();
9821
9882
  const results = [];
@@ -9831,6 +9892,74 @@ var init_task_files = __esm(() => {
9831
9892
  init_database();
9832
9893
  });
9833
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
+
9834
9963
  // src/db/handoffs.ts
9835
9964
  var exports_handoffs = {};
9836
9965
  __export(exports_handoffs, {
@@ -13189,6 +13318,107 @@ ${lines.join(`
13189
13318
  }
13190
13319
  });
13191
13320
  }
13321
+ if (shouldRegisterTool("list_active_files")) {
13322
+ server.tool("list_active_files", "Return all files linked to in-progress tasks across all agents \u2014 the bird's-eye view of what's being worked on right now.", {
13323
+ project_id: exports_external.string().optional().describe("Filter by project")
13324
+ }, async ({ project_id }) => {
13325
+ try {
13326
+ const { listActiveFiles: listActiveFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
13327
+ let files = listActiveFiles2();
13328
+ if (project_id) {
13329
+ const pid = resolveId(project_id, "projects");
13330
+ const db = (init_database(), __toCommonJS(exports_database)).getDatabase();
13331
+ files = db.query(`
13332
+ SELECT
13333
+ tf.path,
13334
+ tf.status AS file_status,
13335
+ tf.agent_id AS file_agent_id,
13336
+ tf.note,
13337
+ tf.updated_at,
13338
+ t.id AS task_id,
13339
+ t.short_id AS task_short_id,
13340
+ t.title AS task_title,
13341
+ t.status AS task_status,
13342
+ t.locked_by AS task_locked_by,
13343
+ t.locked_at AS task_locked_at,
13344
+ a.id AS agent_id,
13345
+ a.name AS agent_name
13346
+ FROM task_files tf
13347
+ JOIN tasks t ON tf.task_id = t.id
13348
+ LEFT JOIN agents a ON (tf.agent_id = a.id OR (tf.agent_id IS NULL AND t.assigned_to = a.id))
13349
+ WHERE t.status = 'in_progress'
13350
+ AND tf.status != 'removed'
13351
+ AND t.project_id = ?
13352
+ ORDER BY tf.updated_at DESC
13353
+ `).all(pid);
13354
+ }
13355
+ if (files.length === 0) {
13356
+ return { content: [{ type: "text", text: "No active files \u2014 no in-progress tasks have linked files." }] };
13357
+ }
13358
+ return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
13359
+ } catch (e) {
13360
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13361
+ }
13362
+ });
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
+ }
13192
13422
  if (shouldRegisterTool("create_handoff")) {
13193
13423
  server.tool("create_handoff", "Create a session handoff note for agent coordination.", {
13194
13424
  agent_id: exports_external.string().optional().describe("Agent creating the handoff"),
@@ -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"}
@@ -22,5 +22,21 @@ export declare function listTaskFiles(taskId: string, db?: Database): TaskFile[]
22
22
  export declare function findTasksByFile(path: string, db?: Database): TaskFile[];
23
23
  export declare function updateTaskFileStatus(taskId: string, path: string, status: TaskFile["status"], agentId?: string, db?: Database): TaskFile | null;
24
24
  export declare function removeTaskFile(taskId: string, path: string, db?: Database): boolean;
25
+ export interface ActiveFileInfo {
26
+ path: string;
27
+ file_status: TaskFile["status"];
28
+ file_agent_id: string | null;
29
+ note: string | null;
30
+ updated_at: string;
31
+ task_id: string;
32
+ task_short_id: string | null;
33
+ task_title: string;
34
+ task_status: string;
35
+ task_locked_by: string | null;
36
+ task_locked_at: string | null;
37
+ agent_id: string | null;
38
+ agent_name: string | null;
39
+ }
40
+ export declare function listActiveFiles(db?: Database): ActiveFileInfo[];
25
41
  export declare function bulkAddTaskFiles(taskId: string, paths: string[], agentId?: string, db?: Database): TaskFile[];
26
42
  //# sourceMappingURL=task-files.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-files.d.ts","sourceRoot":"","sources":["../../src/db/task-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;IACnE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAwB5E;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAGtE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAKvE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAKvE;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAC1B,OAAO,CAAC,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,QAAQ,GACZ,QAAQ,GAAG,IAAI,CAWjB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAOnF;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,CAAC,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,QAAQ,GACZ,QAAQ,EAAE,CAUZ"}
1
+ {"version":3,"file":"task-files.d.ts","sourceRoot":"","sources":["../../src/db/task-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;IACnE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAwB5E;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAGtE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAKvE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAKvE;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAC1B,OAAO,CAAC,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,QAAQ,GACZ,QAAQ,GAAG,IAAI,CAWjB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAOnF;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAChC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAgB,eAAe,CAAC,EAAE,CAAC,EAAE,QAAQ,GAAG,cAAc,EAAE,CAwB/D;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,CAAC,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,QAAQ,GACZ,QAAQ,EAAE,CAUZ"}
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;
package/dist/mcp/index.js CHANGED
@@ -4,32 +4,160 @@ var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __moduleCache = /* @__PURE__ */ new WeakMap;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
8
10
  var __toCommonJS = (from) => {
9
- var entry = __moduleCache.get(from), desc;
11
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
12
  if (entry)
11
13
  return entry;
12
14
  entry = __defProp({}, "__esModule", { value: true });
13
- if (from && typeof from === "object" || typeof from === "function")
14
- __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
15
- get: () => from[key],
16
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
- }));
15
+ if (from && typeof from === "object" || typeof from === "function") {
16
+ for (var key of __getOwnPropNames(from))
17
+ if (!__hasOwnProp.call(entry, key))
18
+ __defProp(entry, key, {
19
+ get: __accessProp.bind(from, key),
20
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
21
+ });
22
+ }
18
23
  __moduleCache.set(from, entry);
19
24
  return entry;
20
25
  };
26
+ var __moduleCache;
27
+ var __returnValue = (v) => v;
28
+ function __exportSetter(name, newValue) {
29
+ this[name] = __returnValue.bind(null, newValue);
30
+ }
21
31
  var __export = (target, all) => {
22
32
  for (var name in all)
23
33
  __defProp(target, name, {
24
34
  get: all[name],
25
35
  enumerable: true,
26
36
  configurable: true,
27
- set: (newValue) => all[name] = () => newValue
37
+ set: __exportSetter.bind(all, name)
28
38
  });
29
39
  };
30
40
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
31
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
+
32
147
  // src/db/database.ts
148
+ var exports_database = {};
149
+ __export(exports_database, {
150
+ uuid: () => uuid,
151
+ resolvePartialId: () => resolvePartialId,
152
+ resetDatabase: () => resetDatabase,
153
+ now: () => now,
154
+ lockExpiryCutoff: () => lockExpiryCutoff,
155
+ isLockExpired: () => isLockExpired,
156
+ getDatabase: () => getDatabase,
157
+ closeDatabase: () => closeDatabase,
158
+ clearExpiredLocks: () => clearExpiredLocks,
159
+ LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
160
+ });
33
161
  import { Database } from "bun:sqlite";
34
162
  import { existsSync, mkdirSync } from "fs";
35
163
  import { dirname, join, resolve } from "path";
@@ -332,6 +460,15 @@ function backfillTaskTags(db) {
332
460
  }
333
461
  } catch {}
334
462
  }
463
+ function closeDatabase() {
464
+ if (_db) {
465
+ _db.close();
466
+ _db = null;
467
+ }
468
+ }
469
+ function resetDatabase() {
470
+ _db = null;
471
+ }
335
472
  function now() {
336
473
  return new Date().toISOString();
337
474
  }
@@ -805,6 +942,20 @@ var init_database = __esm(() => {
805
942
  ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
806
943
  CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
807
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);
808
959
  `
809
960
  ];
810
961
  });
@@ -1203,6 +1354,7 @@ __export(exports_task_files, {
1203
1354
  updateTaskFileStatus: () => updateTaskFileStatus,
1204
1355
  removeTaskFile: () => removeTaskFile,
1205
1356
  listTaskFiles: () => listTaskFiles,
1357
+ listActiveFiles: () => listActiveFiles,
1206
1358
  getTaskFile: () => getTaskFile,
1207
1359
  findTasksByFile: () => findTasksByFile,
1208
1360
  bulkAddTaskFiles: () => bulkAddTaskFiles,
@@ -1245,6 +1397,31 @@ function removeTaskFile(taskId, path, db) {
1245
1397
  const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
1246
1398
  return result.changes > 0;
1247
1399
  }
1400
+ function listActiveFiles(db) {
1401
+ const d = db || getDatabase();
1402
+ return d.query(`
1403
+ SELECT
1404
+ tf.path,
1405
+ tf.status AS file_status,
1406
+ tf.agent_id AS file_agent_id,
1407
+ tf.note,
1408
+ tf.updated_at,
1409
+ t.id AS task_id,
1410
+ t.short_id AS task_short_id,
1411
+ t.title AS task_title,
1412
+ t.status AS task_status,
1413
+ t.locked_by AS task_locked_by,
1414
+ t.locked_at AS task_locked_at,
1415
+ a.id AS agent_id,
1416
+ a.name AS agent_name
1417
+ FROM task_files tf
1418
+ JOIN tasks t ON tf.task_id = t.id
1419
+ LEFT JOIN agents a ON (tf.agent_id = a.id OR (tf.agent_id IS NULL AND t.assigned_to = a.id))
1420
+ WHERE t.status = 'in_progress'
1421
+ AND tf.status != 'removed'
1422
+ ORDER BY tf.updated_at DESC
1423
+ `).all();
1424
+ }
1248
1425
  function bulkAddTaskFiles(taskId, paths, agentId, db) {
1249
1426
  const d = db || getDatabase();
1250
1427
  const results = [];
@@ -1260,6 +1437,74 @@ var init_task_files = __esm(() => {
1260
1437
  init_database();
1261
1438
  });
1262
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
+
1263
1508
  // src/db/handoffs.ts
1264
1509
  var exports_handoffs = {};
1265
1510
  __export(exports_handoffs, {
@@ -5886,120 +6131,12 @@ var coerce = {
5886
6131
  date: (arg) => ZodDate.create({ ...arg, coerce: true })
5887
6132
  };
5888
6133
  var NEVER = INVALID;
5889
- // src/types/index.ts
5890
- class VersionConflictError extends Error {
5891
- taskId;
5892
- expectedVersion;
5893
- actualVersion;
5894
- static code = "VERSION_CONFLICT";
5895
- static suggestion = "Fetch the task with get_task to get the current version before updating.";
5896
- constructor(taskId, expectedVersion, actualVersion) {
5897
- super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
5898
- this.taskId = taskId;
5899
- this.expectedVersion = expectedVersion;
5900
- this.actualVersion = actualVersion;
5901
- this.name = "VersionConflictError";
5902
- }
5903
- }
5904
-
5905
- class TaskNotFoundError extends Error {
5906
- taskId;
5907
- static code = "TASK_NOT_FOUND";
5908
- static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
5909
- constructor(taskId) {
5910
- super(`Task not found: ${taskId}`);
5911
- this.taskId = taskId;
5912
- this.name = "TaskNotFoundError";
5913
- }
5914
- }
5915
-
5916
- class ProjectNotFoundError extends Error {
5917
- projectId;
5918
- static code = "PROJECT_NOT_FOUND";
5919
- static suggestion = "Use list_projects to see available projects.";
5920
- constructor(projectId) {
5921
- super(`Project not found: ${projectId}`);
5922
- this.projectId = projectId;
5923
- this.name = "ProjectNotFoundError";
5924
- }
5925
- }
5926
-
5927
- class PlanNotFoundError extends Error {
5928
- planId;
5929
- static code = "PLAN_NOT_FOUND";
5930
- static suggestion = "Use list_plans to see available plans.";
5931
- constructor(planId) {
5932
- super(`Plan not found: ${planId}`);
5933
- this.planId = planId;
5934
- this.name = "PlanNotFoundError";
5935
- }
5936
- }
5937
-
5938
- class LockError extends Error {
5939
- taskId;
5940
- lockedBy;
5941
- static code = "LOCK_ERROR";
5942
- static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
5943
- constructor(taskId, lockedBy) {
5944
- super(`Task ${taskId} is locked by ${lockedBy}`);
5945
- this.taskId = taskId;
5946
- this.lockedBy = lockedBy;
5947
- this.name = "LockError";
5948
- }
5949
- }
5950
-
5951
- class AgentNotFoundError extends Error {
5952
- agentId;
5953
- static code = "AGENT_NOT_FOUND";
5954
- static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
5955
- constructor(agentId) {
5956
- super(`Agent not found: ${agentId}`);
5957
- this.agentId = agentId;
5958
- this.name = "AgentNotFoundError";
5959
- }
5960
- }
5961
-
5962
- class TaskListNotFoundError extends Error {
5963
- taskListId;
5964
- static code = "TASK_LIST_NOT_FOUND";
5965
- static suggestion = "Use list_task_lists to see available lists.";
5966
- constructor(taskListId) {
5967
- super(`Task list not found: ${taskListId}`);
5968
- this.taskListId = taskListId;
5969
- this.name = "TaskListNotFoundError";
5970
- }
5971
- }
5972
-
5973
- class DependencyCycleError extends Error {
5974
- taskId;
5975
- dependsOn;
5976
- static code = "DEPENDENCY_CYCLE";
5977
- static suggestion = "Check the dependency chain with get_task to avoid circular references.";
5978
- constructor(taskId, dependsOn) {
5979
- super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
5980
- this.taskId = taskId;
5981
- this.dependsOn = dependsOn;
5982
- this.name = "DependencyCycleError";
5983
- }
5984
- }
5985
-
5986
- class CompletionGuardError extends Error {
5987
- reason;
5988
- retryAfterSeconds;
5989
- static code = "COMPLETION_BLOCKED";
5990
- static suggestion = "Wait for the cooldown period, then retry.";
5991
- constructor(reason, retryAfterSeconds) {
5992
- super(reason);
5993
- this.reason = reason;
5994
- this.retryAfterSeconds = retryAfterSeconds;
5995
- this.name = "CompletionGuardError";
5996
- }
5997
- }
5998
-
5999
6134
  // src/db/tasks.ts
6135
+ init_types();
6000
6136
  init_database();
6001
6137
 
6002
6138
  // src/db/projects.ts
6139
+ init_types();
6003
6140
  init_database();
6004
6141
  function slugify(name) {
6005
6142
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -6088,6 +6225,9 @@ function nextTaskShortId(projectId, db) {
6088
6225
  return `${updated.task_prefix}-${padded}`;
6089
6226
  }
6090
6227
 
6228
+ // src/lib/completion-guard.ts
6229
+ init_types();
6230
+
6091
6231
  // src/lib/config.ts
6092
6232
  import { existsSync as existsSync3 } from "fs";
6093
6233
  import { join as join3 } from "path";
@@ -7431,6 +7571,7 @@ function bulkUpdateTasks(taskIds, updates, db) {
7431
7571
  }
7432
7572
 
7433
7573
  // src/db/comments.ts
7574
+ init_types();
7434
7575
  init_database();
7435
7576
  function addComment(input, db) {
7436
7577
  const d = db || getDatabase();
@@ -7461,6 +7602,7 @@ function getComment(id, db) {
7461
7602
  }
7462
7603
 
7463
7604
  // src/db/plans.ts
7605
+ init_types();
7464
7606
  init_database();
7465
7607
  function createPlan(input, db) {
7466
7608
  const d = db || getDatabase();
@@ -7533,6 +7675,7 @@ function deletePlan(id, db) {
7533
7675
  init_agents();
7534
7676
 
7535
7677
  // src/db/task-lists.ts
7678
+ init_types();
7536
7679
  init_database();
7537
7680
  function rowToTaskList(row) {
7538
7681
  return {
@@ -8219,6 +8362,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
8219
8362
 
8220
8363
  // src/mcp/index.ts
8221
8364
  init_database();
8365
+ init_types();
8222
8366
  import { readFileSync as readFileSync3 } from "fs";
8223
8367
  import { join as join6, dirname as dirname2 } from "path";
8224
8368
  import { fileURLToPath } from "url";
@@ -10902,6 +11046,107 @@ ${lines.join(`
10902
11046
  }
10903
11047
  });
10904
11048
  }
11049
+ if (shouldRegisterTool("list_active_files")) {
11050
+ server.tool("list_active_files", "Return all files linked to in-progress tasks across all agents \u2014 the bird's-eye view of what's being worked on right now.", {
11051
+ project_id: exports_external.string().optional().describe("Filter by project")
11052
+ }, async ({ project_id }) => {
11053
+ try {
11054
+ const { listActiveFiles: listActiveFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
11055
+ let files = listActiveFiles2();
11056
+ if (project_id) {
11057
+ const pid = resolveId(project_id, "projects");
11058
+ const db = (init_database(), __toCommonJS(exports_database)).getDatabase();
11059
+ files = db.query(`
11060
+ SELECT
11061
+ tf.path,
11062
+ tf.status AS file_status,
11063
+ tf.agent_id AS file_agent_id,
11064
+ tf.note,
11065
+ tf.updated_at,
11066
+ t.id AS task_id,
11067
+ t.short_id AS task_short_id,
11068
+ t.title AS task_title,
11069
+ t.status AS task_status,
11070
+ t.locked_by AS task_locked_by,
11071
+ t.locked_at AS task_locked_at,
11072
+ a.id AS agent_id,
11073
+ a.name AS agent_name
11074
+ FROM task_files tf
11075
+ JOIN tasks t ON tf.task_id = t.id
11076
+ LEFT JOIN agents a ON (tf.agent_id = a.id OR (tf.agent_id IS NULL AND t.assigned_to = a.id))
11077
+ WHERE t.status = 'in_progress'
11078
+ AND tf.status != 'removed'
11079
+ AND t.project_id = ?
11080
+ ORDER BY tf.updated_at DESC
11081
+ `).all(pid);
11082
+ }
11083
+ if (files.length === 0) {
11084
+ return { content: [{ type: "text", text: "No active files \u2014 no in-progress tasks have linked files." }] };
11085
+ }
11086
+ return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
11087
+ } catch (e) {
11088
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11089
+ }
11090
+ });
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
+ }
10905
11150
  if (shouldRegisterTool("create_handoff")) {
10906
11151
  server.tool("create_handoff", "Create a session handoff note for agent coordination.", {
10907
11152
  agent_id: exports_external.string().optional().describe("Agent creating the handoff"),
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
4
8
  var __export = (target, all) => {
5
9
  for (var name in all)
6
10
  __defProp(target, name, {
7
11
  get: all[name],
8
12
  enumerable: true,
9
13
  configurable: true,
10
- set: (newValue) => all[name] = () => newValue
14
+ set: __exportSetter.bind(all, name)
11
15
  });
12
16
  };
13
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -854,6 +858,20 @@ var init_database = __esm(() => {
854
858
  ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
855
859
  CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
856
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);
857
875
  `
858
876
  ];
859
877
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.10.4",
3
+ "version": "0.10.7",
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",