@hasna/todos 0.11.18 → 0.11.20

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/index.js CHANGED
@@ -176,6 +176,18 @@ function ensureSchema(db) {
176
176
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
177
177
  UNIQUE(source_id, source_type, target_id, target_type, relation_type)
178
178
  )`);
179
+ ensureTable("project_machine_paths", `
180
+ CREATE TABLE project_machine_paths (
181
+ id TEXT PRIMARY KEY,
182
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
183
+ machine_id TEXT NOT NULL,
184
+ path TEXT NOT NULL,
185
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
186
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
187
+ UNIQUE(project_id, machine_id)
188
+ )`);
189
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
190
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
179
191
  ensureTable("machines", `
180
192
  CREATE TABLE machines (
181
193
  id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
@@ -211,6 +223,7 @@ function ensureSchema(db) {
211
223
  ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
212
224
  ensureColumn("tasks", "retry_after", "TEXT");
213
225
  ensureColumn("tasks", "sla_minutes", "INTEGER");
226
+ ensureColumn("tasks", "archived_at", "TEXT");
214
227
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
215
228
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
216
229
  ensureColumn("agents", "reports_to", "TEXT");
@@ -1049,6 +1062,25 @@ var init_schema = __esm(() => {
1049
1062
  CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id);
1050
1063
 
1051
1064
  INSERT OR IGNORE INTO _migrations (id) VALUES (41);
1065
+ `,
1066
+ `
1067
+ CREATE TABLE IF NOT EXISTS project_machine_paths (
1068
+ id TEXT PRIMARY KEY,
1069
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
1070
+ machine_id TEXT NOT NULL,
1071
+ path TEXT NOT NULL,
1072
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1073
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1074
+ UNIQUE(project_id, machine_id)
1075
+ );
1076
+ CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id);
1077
+ CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id);
1078
+ INSERT OR IGNORE INTO _migrations (id) VALUES (42);
1079
+ `,
1080
+ `
1081
+ ALTER TABLE tasks ADD COLUMN archived_at TEXT;
1082
+ CREATE INDEX IF NOT EXISTS idx_tasks_archived ON tasks(archived_at) WHERE archived_at IS NOT NULL;
1083
+ INSERT OR IGNORE INTO _migrations (id) VALUES (43);
1052
1084
  `
1053
1085
  ];
1054
1086
  });
@@ -1750,6 +1782,7 @@ init_database();
1750
1782
  // src/db/projects.ts
1751
1783
  init_types();
1752
1784
  init_database();
1785
+ init_machines();
1753
1786
  function slugify(name) {
1754
1787
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1755
1788
  }
@@ -1790,8 +1823,15 @@ function getProject(id, db) {
1790
1823
  }
1791
1824
  function getProjectByPath(path, db) {
1792
1825
  const d = db || getDatabase();
1793
- const row = d.query("SELECT * FROM projects WHERE path = ?").get(path);
1794
- return row;
1826
+ try {
1827
+ const machineId = getMachineId(d);
1828
+ const machineRow = d.query(`SELECT p.* FROM projects p
1829
+ JOIN project_machine_paths pmp ON pmp.project_id = p.id
1830
+ WHERE pmp.machine_id = ? AND pmp.path = ?`).get(machineId, path);
1831
+ if (machineRow)
1832
+ return machineRow;
1833
+ } catch {}
1834
+ return d.query("SELECT * FROM projects WHERE path = ?").get(path);
1795
1835
  }
1796
1836
  function listProjects(db) {
1797
1837
  const d = db || getDatabase();
@@ -1816,10 +1856,40 @@ function updateProject(id, input, db) {
1816
1856
  sets.push("task_list_id = ?");
1817
1857
  params.push(input.task_list_id);
1818
1858
  }
1859
+ if (input.path !== undefined) {
1860
+ sets.push("path = ?");
1861
+ params.push(input.path);
1862
+ }
1819
1863
  params.push(id);
1820
1864
  d.run(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`, params);
1821
1865
  return getProject(id, d);
1822
1866
  }
1867
+ function renameProject(id, input, db) {
1868
+ const d = db || getDatabase();
1869
+ const project = getProject(id, d);
1870
+ if (!project)
1871
+ throw new ProjectNotFoundError(id);
1872
+ let taskListsUpdated = 0;
1873
+ const ts = now();
1874
+ if (input.new_slug !== undefined) {
1875
+ const normalised = input.new_slug.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-|-$/g, "");
1876
+ if (!normalised)
1877
+ throw new Error("Invalid slug \u2014 must be non-empty kebab-case");
1878
+ const conflict = d.query("SELECT id FROM projects WHERE task_list_id = ? AND id != ?").get(normalised, id);
1879
+ if (conflict)
1880
+ throw new Error(`Slug "${normalised}" is already used by another project`);
1881
+ const oldSlug = project.task_list_id;
1882
+ d.run("UPDATE projects SET task_list_id = ?, updated_at = ? WHERE id = ?", [normalised, ts, id]);
1883
+ if (oldSlug) {
1884
+ const result = d.run("UPDATE task_lists SET slug = ?, name = COALESCE(?, name), updated_at = ? WHERE project_id = ? AND slug = ?", [normalised, input.name ?? null, ts, id, oldSlug]);
1885
+ taskListsUpdated = result.changes;
1886
+ }
1887
+ }
1888
+ if (input.name !== undefined) {
1889
+ d.run("UPDATE projects SET name = ?, updated_at = ? WHERE id = ?", [input.name, ts, id]);
1890
+ }
1891
+ return { project: getProject(id, d), task_lists_updated: taskListsUpdated };
1892
+ }
1823
1893
  function deleteProject(id, db) {
1824
1894
  const d = db || getDatabase();
1825
1895
  const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
@@ -1886,9 +1956,48 @@ function ensureProject(name, path, db) {
1886
1956
  d.run("UPDATE projects SET task_prefix = ?, updated_at = ? WHERE id = ?", [prefix, now(), existing.id]);
1887
1957
  return getProject(existing.id, d);
1888
1958
  }
1959
+ setMachineLocalPath(existing.id, path, d);
1889
1960
  return existing;
1890
1961
  }
1891
- return createProject({ name, path }, d);
1962
+ const project = createProject({ name, path }, d);
1963
+ setMachineLocalPath(project.id, path, d);
1964
+ return project;
1965
+ }
1966
+ function setMachineLocalPath(projectId, path, db) {
1967
+ const d = db || getDatabase();
1968
+ const machineId = getMachineId(d);
1969
+ const ts = now();
1970
+ const existing = d.query("SELECT * FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(projectId, machineId);
1971
+ if (existing) {
1972
+ if (existing.path !== path) {
1973
+ d.run("UPDATE project_machine_paths SET path = ?, updated_at = ? WHERE id = ?", [path, ts, existing.id]);
1974
+ }
1975
+ return d.query("SELECT * FROM project_machine_paths WHERE id = ?").get(existing.id);
1976
+ }
1977
+ const id = uuid();
1978
+ d.run("INSERT INTO project_machine_paths (id, project_id, machine_id, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", [id, projectId, machineId, path, ts, ts]);
1979
+ return d.query("SELECT * FROM project_machine_paths WHERE id = ?").get(id);
1980
+ }
1981
+ function getMachineLocalPath(projectId, db) {
1982
+ const d = db || getDatabase();
1983
+ try {
1984
+ const machineId = getMachineId(d);
1985
+ const row = d.query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(projectId, machineId);
1986
+ if (row)
1987
+ return row.path;
1988
+ } catch {}
1989
+ const project = getProject(projectId, d);
1990
+ return project?.path ?? null;
1991
+ }
1992
+ function listMachineLocalPaths(projectId, db) {
1993
+ const d = db || getDatabase();
1994
+ return d.query("SELECT * FROM project_machine_paths WHERE project_id = ? ORDER BY machine_id").all(projectId);
1995
+ }
1996
+ function removeMachineLocalPath(projectId, machineId, db) {
1997
+ const d = db || getDatabase();
1998
+ const mid = machineId ?? getMachineId(d);
1999
+ const result = d.run("DELETE FROM project_machine_paths WHERE project_id = ? AND machine_id = ?", [projectId, mid]);
2000
+ return result.changes > 0;
1892
2001
  }
1893
2002
 
1894
2003
  // src/lib/completion-guard.ts
@@ -2980,6 +3089,9 @@ function listTasks(filter = {}, db) {
2980
3089
  params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
2981
3090
  } catch {}
2982
3091
  }
3092
+ if (!filter.include_archived) {
3093
+ conditions.push("archived_at IS NULL");
3094
+ }
2983
3095
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2984
3096
  let limitClause = "";
2985
3097
  if (filter.limit) {
@@ -3893,6 +4005,35 @@ function bulkUpdateTasks(taskIds, updates, db) {
3893
4005
  tx();
3894
4006
  return { updated, failed };
3895
4007
  }
4008
+ function archiveTasks(options, db) {
4009
+ const d = db || getDatabase();
4010
+ const conditions = ["archived_at IS NULL"];
4011
+ const params = [];
4012
+ const statuses = options.status ?? ["completed", "failed", "cancelled"];
4013
+ conditions.push(`status IN (${statuses.map(() => "?").join(",")})`);
4014
+ params.push(...statuses);
4015
+ if (options.project_id) {
4016
+ conditions.push("project_id = ?");
4017
+ params.push(options.project_id);
4018
+ }
4019
+ if (options.task_list_id) {
4020
+ conditions.push("task_list_id = ?");
4021
+ params.push(options.task_list_id);
4022
+ }
4023
+ if (options.older_than_days !== undefined) {
4024
+ const cutoff = new Date(Date.now() - options.older_than_days * 86400000).toISOString();
4025
+ conditions.push("updated_at < ?");
4026
+ params.push(cutoff);
4027
+ }
4028
+ const ts = now();
4029
+ const result = d.run(`UPDATE tasks SET archived_at = ? WHERE ${conditions.join(" AND ")}`, [ts, ...params]);
4030
+ return { archived: result.changes };
4031
+ }
4032
+ function unarchiveTask(id, db) {
4033
+ const d = db || getDatabase();
4034
+ d.run("UPDATE tasks SET archived_at = NULL WHERE id = ?", [id]);
4035
+ return getTask(id, d);
4036
+ }
3896
4037
  function getOverdueTasks(projectId, db) {
3897
4038
  const d = db || getDatabase();
3898
4039
  const nowStr = new Date().toISOString();
@@ -5402,21 +5543,25 @@ function escapeFtsQuery(q) {
5402
5543
  return q.replace(/["*^()]/g, " ").trim().split(/\s+/).filter(Boolean).map((token) => `"${token}"*`).join(" ");
5403
5544
  }
5404
5545
  function searchTasks(options, projectId, taskListId, db) {
5405
- const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
5546
+ const opts = typeof options === "string" ? { query: options || undefined, project_id: projectId, task_list_id: taskListId } : options;
5406
5547
  const d = db || getDatabase();
5407
5548
  clearExpiredLocks(d);
5408
5549
  const params = [];
5409
5550
  let sql;
5410
- if (hasFts(d) && opts.query.trim()) {
5411
- const ftsQuery = escapeFtsQuery(opts.query);
5551
+ const raw = opts.query?.trim() ?? "";
5552
+ const q = raw === "*" ? "" : raw;
5553
+ if (hasFts(d) && q) {
5554
+ const ftsQuery = escapeFtsQuery(q);
5412
5555
  sql = `SELECT t.* FROM tasks t
5413
5556
  INNER JOIN tasks_fts fts ON fts.rowid = t.rowid
5414
5557
  WHERE tasks_fts MATCH ?`;
5415
5558
  params.push(ftsQuery);
5416
- } else {
5417
- const pattern = `%${opts.query}%`;
5559
+ } else if (q) {
5560
+ const pattern = `%${q}%`;
5418
5561
  sql = `SELECT * FROM tasks t WHERE (t.title LIKE ? OR t.description LIKE ? OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = t.id AND tag LIKE ?))`;
5419
5562
  params.push(pattern, pattern, pattern);
5563
+ } else {
5564
+ sql = `SELECT * FROM tasks t WHERE 1=1`;
5420
5565
  }
5421
5566
  if (opts.project_id) {
5422
5567
  sql += " AND t.project_id = ?";
@@ -5470,7 +5615,7 @@ function searchTasks(options, projectId, taskListId, db) {
5470
5615
  } else if (opts.is_blocked === false) {
5471
5616
  sql += " AND t.id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
5472
5617
  }
5473
- if (hasFts(d) && opts.query.trim()) {
5618
+ if (hasFts(d) && q) {
5474
5619
  sql += ` ORDER BY bm25(tasks_fts),
5475
5620
  CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
5476
5621
  t.created_at DESC`;
@@ -16940,6 +17085,7 @@ export {
16940
17085
  updateAgentActivity,
16941
17086
  updateAgent,
16942
17087
  unlockTask,
17088
+ unarchiveTask,
16943
17089
  unarchiveAgent,
16944
17090
  tasksFromTemplate,
16945
17091
  taskFromTemplate,
@@ -16952,6 +17098,7 @@ export {
16952
17098
  slugify,
16953
17099
  setTaskStatus,
16954
17100
  setTaskPriority,
17101
+ setMachineLocalPath,
16955
17102
  setBudget,
16956
17103
  setActiveModel,
16957
17104
  sendToTmux,
@@ -16963,10 +17110,12 @@ export {
16963
17110
  resolvePartialId,
16964
17111
  resetMachineId,
16965
17112
  resetDatabase,
17113
+ renameProject,
16966
17114
  removeTaskRelationshipByPair,
16967
17115
  removeTaskRelationship,
16968
17116
  removeTaskFile,
16969
17117
  removeProjectSource,
17118
+ removeMachineLocalPath,
16970
17119
  removeKgEdges,
16971
17120
  removeDependency,
16972
17121
  removeChecklistItem,
@@ -17003,6 +17152,7 @@ export {
17003
17152
  listPlans,
17004
17153
  listOrgs,
17005
17154
  listMachines,
17155
+ listMachineLocalPaths,
17006
17156
  listHandoffs,
17007
17157
  listDispatches,
17008
17158
  listDispatchLogs,
@@ -17051,6 +17201,7 @@ export {
17051
17201
  getOrg,
17052
17202
  getOrCreateLocalMachine,
17053
17203
  getNextTask,
17204
+ getMachineLocalPath,
17054
17205
  getMachineId,
17055
17206
  getMachineByName,
17056
17207
  getMachine,
@@ -17139,6 +17290,7 @@ export {
17139
17290
  bulkAddTaskFiles,
17140
17291
  autoReleaseStaleAgents,
17141
17292
  autoDetectFileRelationships,
17293
+ archiveTasks,
17142
17294
  archiveAgent,
17143
17295
  applyPgMigrations,
17144
17296
  addTemplateTasks,