@hasna/todos 0.10.9 → 0.10.10

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
@@ -9812,6 +9812,7 @@ __export(exports_task_files, {
9812
9812
  listActiveFiles: () => listActiveFiles,
9813
9813
  getTaskFile: () => getTaskFile,
9814
9814
  findTasksByFile: () => findTasksByFile,
9815
+ detectFileConflicts: () => detectFileConflicts,
9815
9816
  bulkFindTasksByFiles: () => bulkFindTasksByFiles,
9816
9817
  bulkAddTaskFiles: () => bulkAddTaskFiles,
9817
9818
  addTaskFile: () => addTaskFile
@@ -9853,6 +9854,24 @@ function removeTaskFile(taskId, path, db) {
9853
9854
  const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
9854
9855
  return result.changes > 0;
9855
9856
  }
9857
+ function detectFileConflicts(taskId, paths, db) {
9858
+ const d = db || getDatabase();
9859
+ if (paths.length === 0)
9860
+ return [];
9861
+ const placeholders = paths.map(() => "?").join(", ");
9862
+ const rows = d.query(`
9863
+ SELECT tf.path, tf.agent_id AS conflicting_agent_id, t.id AS conflicting_task_id,
9864
+ t.title AS conflicting_task_title, t.status AS conflicting_task_status
9865
+ FROM task_files tf
9866
+ JOIN tasks t ON tf.task_id = t.id
9867
+ WHERE tf.path IN (${placeholders})
9868
+ AND tf.task_id != ?
9869
+ AND tf.status != 'removed'
9870
+ AND t.status = 'in_progress'
9871
+ ORDER BY tf.updated_at DESC
9872
+ `).all(...paths, taskId);
9873
+ return rows;
9874
+ }
9856
9875
  function bulkFindTasksByFiles(paths, db) {
9857
9876
  const d = db || getDatabase();
9858
9877
  if (paths.length === 0)
@@ -13289,7 +13308,7 @@ Claimed: ${formatTask(result.claimed)}`);
13289
13308
  return { contents: [{ uri: "todos://agents", text: JSON.stringify(agents, null, 2), mimeType: "application/json" }] };
13290
13309
  });
13291
13310
  if (shouldRegisterTool("add_task_file")) {
13292
- server.tool("add_task_file", "Link a file path to a task. Tracks which files an agent is working on. Upserts if same task+path exists.", {
13311
+ server.tool("add_task_file", "Link a file path to a task. Tracks which files an agent is working on. Upserts if same task+path exists. Auto-detects conflicts with other in-progress tasks.", {
13293
13312
  task_id: exports_external.string().describe("Task ID"),
13294
13313
  path: exports_external.string().describe("File path (relative or absolute)"),
13295
13314
  paths: exports_external.array(exports_external.string()).optional().describe("Multiple file paths to add at once"),
@@ -13298,14 +13317,41 @@ Claimed: ${formatTask(result.claimed)}`);
13298
13317
  note: exports_external.string().optional().describe("Note about why this file is linked")
13299
13318
  }, async ({ task_id, path, paths: multiplePaths, status, agent_id, note }) => {
13300
13319
  try {
13301
- const { addTaskFile: addTaskFile2, bulkAddTaskFiles: bulkAddTaskFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
13320
+ const { addTaskFile: addTaskFile2, bulkAddTaskFiles: bulkAddTaskFiles2, detectFileConflicts: detectFileConflicts2 } = (init_task_files(), __toCommonJS(exports_task_files));
13302
13321
  const resolvedId = resolveId(task_id);
13322
+ let addedFiles;
13303
13323
  if (multiplePaths && multiplePaths.length > 0) {
13304
13324
  const allPaths = path ? [path, ...multiplePaths] : multiplePaths;
13305
- const files = bulkAddTaskFiles2(resolvedId, allPaths, agent_id);
13306
- return { content: [{ type: "text", text: `${files.length} file(s) linked to task ${resolvedId.slice(0, 8)}` }] };
13325
+ addedFiles = bulkAddTaskFiles2(resolvedId, allPaths, agent_id);
13326
+ const conflicts2 = detectFileConflicts2(resolvedId, allPaths);
13327
+ if (conflicts2.length > 0) {
13328
+ return {
13329
+ content: [{
13330
+ type: "text",
13331
+ text: JSON.stringify({
13332
+ added: addedFiles.length,
13333
+ conflicts: conflicts2,
13334
+ warning: `${conflicts2.length} file(s) already claimed by other in-progress tasks`
13335
+ }, null, 2)
13336
+ }]
13337
+ };
13338
+ }
13339
+ return { content: [{ type: "text", text: `${addedFiles.length} file(s) linked to task ${resolvedId.slice(0, 8)}` }] };
13307
13340
  }
13308
13341
  const file = addTaskFile2({ task_id: resolvedId, path, status, agent_id, note });
13342
+ const conflicts = detectFileConflicts2(resolvedId, [path]);
13343
+ if (conflicts.length > 0) {
13344
+ return {
13345
+ content: [{
13346
+ type: "text",
13347
+ text: JSON.stringify({
13348
+ file,
13349
+ conflicts,
13350
+ warning: `${path} is already claimed by another in-progress task`
13351
+ }, null, 2)
13352
+ }]
13353
+ };
13354
+ }
13309
13355
  return { content: [{ type: "text", text: `${file.status} ${file.path} \u2192 task ${resolvedId.slice(0, 8)}` }] };
13310
13356
  } catch (e) {
13311
13357
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -22,6 +22,18 @@ 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 FileConflict {
26
+ path: string;
27
+ conflicting_task_id: string;
28
+ conflicting_agent_id: string | null;
29
+ conflicting_task_title: string;
30
+ conflicting_task_status: string;
31
+ }
32
+ /**
33
+ * Check if adding a file to a task would conflict with another in-progress task.
34
+ * Returns conflicts (not a hard block — caller decides what to do).
35
+ */
36
+ export declare function detectFileConflicts(taskId: string, paths: string[], db?: Database): FileConflict[];
25
37
  export interface BulkFileResult {
26
38
  path: string;
27
39
  tasks: TaskFile[];
@@ -1 +1 @@
1
- {"version":3,"file":"task-files.d.ts","sourceRoot":"","sources":["../../src/db/task-files.ts"],"names":[],"mappings":"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"}
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,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,YAAY,EAAE,CAkBlG;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,cAAc,EAAE,CA6BrF;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/mcp/index.js CHANGED
@@ -1357,6 +1357,7 @@ __export(exports_task_files, {
1357
1357
  listActiveFiles: () => listActiveFiles,
1358
1358
  getTaskFile: () => getTaskFile,
1359
1359
  findTasksByFile: () => findTasksByFile,
1360
+ detectFileConflicts: () => detectFileConflicts,
1360
1361
  bulkFindTasksByFiles: () => bulkFindTasksByFiles,
1361
1362
  bulkAddTaskFiles: () => bulkAddTaskFiles,
1362
1363
  addTaskFile: () => addTaskFile
@@ -1398,6 +1399,24 @@ function removeTaskFile(taskId, path, db) {
1398
1399
  const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
1399
1400
  return result.changes > 0;
1400
1401
  }
1402
+ function detectFileConflicts(taskId, paths, db) {
1403
+ const d = db || getDatabase();
1404
+ if (paths.length === 0)
1405
+ return [];
1406
+ const placeholders = paths.map(() => "?").join(", ");
1407
+ const rows = d.query(`
1408
+ SELECT tf.path, tf.agent_id AS conflicting_agent_id, t.id AS conflicting_task_id,
1409
+ t.title AS conflicting_task_title, t.status AS conflicting_task_status
1410
+ FROM task_files tf
1411
+ JOIN tasks t ON tf.task_id = t.id
1412
+ WHERE tf.path IN (${placeholders})
1413
+ AND tf.task_id != ?
1414
+ AND tf.status != 'removed'
1415
+ AND t.status = 'in_progress'
1416
+ ORDER BY tf.updated_at DESC
1417
+ `).all(...paths, taskId);
1418
+ return rows;
1419
+ }
1401
1420
  function bulkFindTasksByFiles(paths, db) {
1402
1421
  const d = db || getDatabase();
1403
1422
  if (paths.length === 0)
@@ -11017,7 +11036,7 @@ server.resource("agents", "todos://agents", { description: "All registered agent
11017
11036
  return { contents: [{ uri: "todos://agents", text: JSON.stringify(agents, null, 2), mimeType: "application/json" }] };
11018
11037
  });
11019
11038
  if (shouldRegisterTool("add_task_file")) {
11020
- server.tool("add_task_file", "Link a file path to a task. Tracks which files an agent is working on. Upserts if same task+path exists.", {
11039
+ server.tool("add_task_file", "Link a file path to a task. Tracks which files an agent is working on. Upserts if same task+path exists. Auto-detects conflicts with other in-progress tasks.", {
11021
11040
  task_id: exports_external.string().describe("Task ID"),
11022
11041
  path: exports_external.string().describe("File path (relative or absolute)"),
11023
11042
  paths: exports_external.array(exports_external.string()).optional().describe("Multiple file paths to add at once"),
@@ -11026,14 +11045,41 @@ if (shouldRegisterTool("add_task_file")) {
11026
11045
  note: exports_external.string().optional().describe("Note about why this file is linked")
11027
11046
  }, async ({ task_id, path, paths: multiplePaths, status, agent_id, note }) => {
11028
11047
  try {
11029
- const { addTaskFile: addTaskFile2, bulkAddTaskFiles: bulkAddTaskFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
11048
+ const { addTaskFile: addTaskFile2, bulkAddTaskFiles: bulkAddTaskFiles2, detectFileConflicts: detectFileConflicts2 } = (init_task_files(), __toCommonJS(exports_task_files));
11030
11049
  const resolvedId = resolveId(task_id);
11050
+ let addedFiles;
11031
11051
  if (multiplePaths && multiplePaths.length > 0) {
11032
11052
  const allPaths = path ? [path, ...multiplePaths] : multiplePaths;
11033
- const files = bulkAddTaskFiles2(resolvedId, allPaths, agent_id);
11034
- return { content: [{ type: "text", text: `${files.length} file(s) linked to task ${resolvedId.slice(0, 8)}` }] };
11053
+ addedFiles = bulkAddTaskFiles2(resolvedId, allPaths, agent_id);
11054
+ const conflicts2 = detectFileConflicts2(resolvedId, allPaths);
11055
+ if (conflicts2.length > 0) {
11056
+ return {
11057
+ content: [{
11058
+ type: "text",
11059
+ text: JSON.stringify({
11060
+ added: addedFiles.length,
11061
+ conflicts: conflicts2,
11062
+ warning: `${conflicts2.length} file(s) already claimed by other in-progress tasks`
11063
+ }, null, 2)
11064
+ }]
11065
+ };
11066
+ }
11067
+ return { content: [{ type: "text", text: `${addedFiles.length} file(s) linked to task ${resolvedId.slice(0, 8)}` }] };
11035
11068
  }
11036
11069
  const file = addTaskFile2({ task_id: resolvedId, path, status, agent_id, note });
11070
+ const conflicts = detectFileConflicts2(resolvedId, [path]);
11071
+ if (conflicts.length > 0) {
11072
+ return {
11073
+ content: [{
11074
+ type: "text",
11075
+ text: JSON.stringify({
11076
+ file,
11077
+ conflicts,
11078
+ warning: `${path} is already claimed by another in-progress task`
11079
+ }, null, 2)
11080
+ }]
11081
+ };
11082
+ }
11037
11083
  return { content: [{ type: "text", text: `${file.status} ${file.path} \u2192 task ${resolvedId.slice(0, 8)}` }] };
11038
11084
  } catch (e) {
11039
11085
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.10.9",
3
+ "version": "0.10.10",
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",