@hasna/todos 0.11.21 → 0.11.22

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
@@ -1081,6 +1081,12 @@ var init_schema = __esm(() => {
1081
1081
  ALTER TABLE tasks ADD COLUMN archived_at TEXT;
1082
1082
  CREATE INDEX IF NOT EXISTS idx_tasks_archived ON tasks(archived_at) WHERE archived_at IS NOT NULL;
1083
1083
  INSERT OR IGNORE INTO _migrations (id) VALUES (43);
1084
+ `,
1085
+ `
1086
+ DROP INDEX IF EXISTS idx_tasks_short_id;
1087
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id, machine_id) WHERE short_id IS NOT NULL AND machine_id IS NOT NULL;
1088
+ CREATE INDEX IF NOT EXISTS idx_tasks_short_id_lookup ON tasks(short_id) WHERE short_id IS NOT NULL;
1089
+ INSERT OR IGNORE INTO _migrations (id) VALUES (44);
1084
1090
  `
1085
1091
  ];
1086
1092
  });
@@ -1779,6 +1785,128 @@ init_database();
1779
1785
  init_types();
1780
1786
  init_database();
1781
1787
 
1788
+ // src/lib/completion-guard.ts
1789
+ init_types();
1790
+
1791
+ // src/lib/config.ts
1792
+ import { existsSync as existsSync3 } from "fs";
1793
+ import { join as join3 } from "path";
1794
+
1795
+ // src/lib/sync-utils.ts
1796
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
1797
+ import { join as join2 } from "path";
1798
+ var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
1799
+ function ensureDir2(dir) {
1800
+ if (!existsSync2(dir))
1801
+ mkdirSync2(dir, { recursive: true });
1802
+ }
1803
+ function listJsonFiles(dir) {
1804
+ if (!existsSync2(dir))
1805
+ return [];
1806
+ return readdirSync(dir).filter((f) => f.endsWith(".json"));
1807
+ }
1808
+ function readJsonFile(path) {
1809
+ try {
1810
+ return JSON.parse(readFileSync(path, "utf-8"));
1811
+ } catch {
1812
+ return null;
1813
+ }
1814
+ }
1815
+ function writeJsonFile(path, data) {
1816
+ writeFileSync(path, JSON.stringify(data, null, 2) + `
1817
+ `);
1818
+ }
1819
+ function readHighWaterMark(dir) {
1820
+ const path = join2(dir, ".highwatermark");
1821
+ if (!existsSync2(path))
1822
+ return 1;
1823
+ const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
1824
+ return isNaN(val) ? 1 : val;
1825
+ }
1826
+ function writeHighWaterMark(dir, value) {
1827
+ writeFileSync(join2(dir, ".highwatermark"), String(value));
1828
+ }
1829
+ function getFileMtimeMs(path) {
1830
+ try {
1831
+ return statSync(path).mtimeMs;
1832
+ } catch {
1833
+ return null;
1834
+ }
1835
+ }
1836
+ function parseTimestamp(value) {
1837
+ if (typeof value !== "string")
1838
+ return null;
1839
+ const parsed = Date.parse(value);
1840
+ return Number.isNaN(parsed) ? null : parsed;
1841
+ }
1842
+ function appendSyncConflict(metadata, conflict, limit = 5) {
1843
+ const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
1844
+ const next = [conflict, ...current].slice(0, limit);
1845
+ return { ...metadata, sync_conflicts: next };
1846
+ }
1847
+
1848
+ // src/lib/config.ts
1849
+ function getTodosGlobalDir() {
1850
+ const home = process.env["HOME"] || HOME;
1851
+ const newDir = join3(home, ".hasna", "todos");
1852
+ const legacyDir = join3(home, ".todos");
1853
+ if (!existsSync3(newDir) && existsSync3(legacyDir))
1854
+ return legacyDir;
1855
+ return newDir;
1856
+ }
1857
+ function getConfigPath() {
1858
+ return join3(getTodosGlobalDir(), "config.json");
1859
+ }
1860
+ var cached = null;
1861
+ function normalizeAgent(agent) {
1862
+ return agent.trim().toLowerCase();
1863
+ }
1864
+ function loadConfig() {
1865
+ if (cached)
1866
+ return cached;
1867
+ if (!existsSync3(getConfigPath())) {
1868
+ cached = {};
1869
+ return cached;
1870
+ }
1871
+ const config = readJsonFile(getConfigPath()) || {};
1872
+ if (typeof config.sync_agents === "string") {
1873
+ config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
1874
+ }
1875
+ cached = config;
1876
+ return cached;
1877
+ }
1878
+ function getSyncAgentsFromConfig() {
1879
+ const config = loadConfig();
1880
+ const agents = config.sync_agents;
1881
+ if (Array.isArray(agents) && agents.length > 0)
1882
+ return agents.map(normalizeAgent);
1883
+ return null;
1884
+ }
1885
+ function getAgentTasksDir(agent) {
1886
+ const config = loadConfig();
1887
+ const key = normalizeAgent(agent);
1888
+ return config.agents?.[key]?.tasks_dir || config.agent_tasks_dir || null;
1889
+ }
1890
+ function getTaskPrefixConfig() {
1891
+ const config = loadConfig();
1892
+ return config.task_prefix || null;
1893
+ }
1894
+ var GUARD_DEFAULTS = {
1895
+ enabled: false,
1896
+ min_work_seconds: 30,
1897
+ max_completions_per_window: 5,
1898
+ window_minutes: 10,
1899
+ cooldown_seconds: 60
1900
+ };
1901
+ function getCompletionGuardConfig(projectPath) {
1902
+ const config = loadConfig();
1903
+ const global2 = { ...GUARD_DEFAULTS, ...config.completion_guard };
1904
+ if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
1905
+ return { ...global2, ...config.project_overrides[projectPath].completion_guard };
1906
+ }
1907
+ return global2;
1908
+ }
1909
+
1782
1910
  // src/db/projects.ts
1783
1911
  init_types();
1784
1912
  init_database();
@@ -1942,13 +2070,6 @@ function nextTaskShortId(projectId, db) {
1942
2070
  const project = getProject(projectId, d);
1943
2071
  if (!project || !project.task_prefix)
1944
2072
  return null;
1945
- const prefix = project.task_prefix;
1946
- const prefixLen = prefix.length + 2;
1947
- const maxRow = d.query(`SELECT MAX(CAST(SUBSTR(short_id, ?) AS INTEGER)) as max_counter FROM tasks WHERE short_id LIKE ?`).get(prefixLen, `${prefix}-%`);
1948
- const syncedMax = maxRow?.max_counter ?? 0;
1949
- if (syncedMax >= (project.task_counter ?? 0)) {
1950
- d.run("UPDATE projects SET task_counter = ?, updated_at = ? WHERE id = ?", [syncedMax, now(), projectId]);
1951
- }
1952
2073
  d.run("UPDATE projects SET task_counter = task_counter + 1, updated_at = ? WHERE id = ?", [now(), projectId]);
1953
2074
  const updated = getProject(projectId, d);
1954
2075
  const padded = String(updated.task_counter).padStart(5, "0");
@@ -2007,128 +2128,6 @@ function removeMachineLocalPath(projectId, machineId, db) {
2007
2128
  return result.changes > 0;
2008
2129
  }
2009
2130
 
2010
- // src/lib/completion-guard.ts
2011
- init_types();
2012
-
2013
- // src/lib/config.ts
2014
- import { existsSync as existsSync3 } from "fs";
2015
- import { join as join3 } from "path";
2016
-
2017
- // src/lib/sync-utils.ts
2018
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
2019
- import { join as join2 } from "path";
2020
- var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2021
- function ensureDir2(dir) {
2022
- if (!existsSync2(dir))
2023
- mkdirSync2(dir, { recursive: true });
2024
- }
2025
- function listJsonFiles(dir) {
2026
- if (!existsSync2(dir))
2027
- return [];
2028
- return readdirSync(dir).filter((f) => f.endsWith(".json"));
2029
- }
2030
- function readJsonFile(path) {
2031
- try {
2032
- return JSON.parse(readFileSync(path, "utf-8"));
2033
- } catch {
2034
- return null;
2035
- }
2036
- }
2037
- function writeJsonFile(path, data) {
2038
- writeFileSync(path, JSON.stringify(data, null, 2) + `
2039
- `);
2040
- }
2041
- function readHighWaterMark(dir) {
2042
- const path = join2(dir, ".highwatermark");
2043
- if (!existsSync2(path))
2044
- return 1;
2045
- const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
2046
- return isNaN(val) ? 1 : val;
2047
- }
2048
- function writeHighWaterMark(dir, value) {
2049
- writeFileSync(join2(dir, ".highwatermark"), String(value));
2050
- }
2051
- function getFileMtimeMs(path) {
2052
- try {
2053
- return statSync(path).mtimeMs;
2054
- } catch {
2055
- return null;
2056
- }
2057
- }
2058
- function parseTimestamp(value) {
2059
- if (typeof value !== "string")
2060
- return null;
2061
- const parsed = Date.parse(value);
2062
- return Number.isNaN(parsed) ? null : parsed;
2063
- }
2064
- function appendSyncConflict(metadata, conflict, limit = 5) {
2065
- const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
2066
- const next = [conflict, ...current].slice(0, limit);
2067
- return { ...metadata, sync_conflicts: next };
2068
- }
2069
-
2070
- // src/lib/config.ts
2071
- function getTodosGlobalDir() {
2072
- const home = process.env["HOME"] || HOME;
2073
- const newDir = join3(home, ".hasna", "todos");
2074
- const legacyDir = join3(home, ".todos");
2075
- if (!existsSync3(newDir) && existsSync3(legacyDir))
2076
- return legacyDir;
2077
- return newDir;
2078
- }
2079
- function getConfigPath() {
2080
- return join3(getTodosGlobalDir(), "config.json");
2081
- }
2082
- var cached = null;
2083
- function normalizeAgent(agent) {
2084
- return agent.trim().toLowerCase();
2085
- }
2086
- function loadConfig() {
2087
- if (cached)
2088
- return cached;
2089
- if (!existsSync3(getConfigPath())) {
2090
- cached = {};
2091
- return cached;
2092
- }
2093
- const config = readJsonFile(getConfigPath()) || {};
2094
- if (typeof config.sync_agents === "string") {
2095
- config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
2096
- }
2097
- cached = config;
2098
- return cached;
2099
- }
2100
- function getSyncAgentsFromConfig() {
2101
- const config = loadConfig();
2102
- const agents = config.sync_agents;
2103
- if (Array.isArray(agents) && agents.length > 0)
2104
- return agents.map(normalizeAgent);
2105
- return null;
2106
- }
2107
- function getAgentTasksDir(agent) {
2108
- const config = loadConfig();
2109
- const key = normalizeAgent(agent);
2110
- return config.agents?.[key]?.tasks_dir || config.agent_tasks_dir || null;
2111
- }
2112
- function getTaskPrefixConfig() {
2113
- const config = loadConfig();
2114
- return config.task_prefix || null;
2115
- }
2116
- var GUARD_DEFAULTS = {
2117
- enabled: false,
2118
- min_work_seconds: 30,
2119
- max_completions_per_window: 5,
2120
- window_minutes: 10,
2121
- cooldown_seconds: 60
2122
- };
2123
- function getCompletionGuardConfig(projectPath) {
2124
- const config = loadConfig();
2125
- const global2 = { ...GUARD_DEFAULTS, ...config.completion_guard };
2126
- if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
2127
- return { ...global2, ...config.project_overrides[projectPath].completion_guard };
2128
- }
2129
- return global2;
2130
- }
2131
-
2132
2131
  // src/lib/completion-guard.ts
2133
2132
  function checkCompletionGuard(task, agentId, db, configOverride) {
2134
2133
  let config;
@@ -2931,47 +2930,56 @@ function replaceTaskTags(taskId, tags, db) {
2931
2930
  }
2932
2931
  function createTask(input, db) {
2933
2932
  const d = db || getDatabase();
2934
- const id = uuid();
2935
2933
  const timestamp = now();
2936
2934
  const tags = input.tags || [];
2937
- const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
2938
- const title = shortId ? `${shortId}: ${input.title}` : input.title;
2939
2935
  const assignedBy = input.assigned_by || input.agent_id;
2940
2936
  const assignedFromProject = input.assigned_from_project || null;
2941
- d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
2942
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2943
- id,
2944
- shortId,
2945
- input.project_id || null,
2946
- input.parent_id || null,
2947
- input.plan_id || null,
2948
- input.task_list_id || null,
2949
- title,
2950
- input.description || null,
2951
- input.status || "pending",
2952
- input.priority || "medium",
2953
- input.agent_id || null,
2954
- input.assigned_to || null,
2955
- input.session_id || null,
2956
- input.working_dir || null,
2957
- JSON.stringify(tags),
2958
- JSON.stringify(input.metadata || {}),
2959
- timestamp,
2960
- timestamp,
2961
- input.due_at || null,
2962
- input.estimated_minutes || null,
2963
- input.requires_approval ? 1 : 0,
2964
- null,
2965
- null,
2966
- input.recurrence_rule || null,
2967
- input.recurrence_parent_id || null,
2968
- input.spawns_template_id || null,
2969
- input.reason || null,
2970
- input.spawned_from_session || null,
2971
- assignedBy || null,
2972
- assignedFromProject || null,
2973
- input.task_type || null
2974
- ]);
2937
+ let id = uuid();
2938
+ for (let attempt = 0;attempt < 3; attempt++) {
2939
+ try {
2940
+ d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
2941
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2942
+ id,
2943
+ null,
2944
+ input.project_id || null,
2945
+ input.parent_id || null,
2946
+ input.plan_id || null,
2947
+ input.task_list_id || null,
2948
+ input.title,
2949
+ input.description || null,
2950
+ input.status || "pending",
2951
+ input.priority || "medium",
2952
+ input.agent_id || null,
2953
+ input.assigned_to || null,
2954
+ input.session_id || null,
2955
+ input.working_dir || null,
2956
+ JSON.stringify(tags),
2957
+ JSON.stringify(input.metadata || {}),
2958
+ timestamp,
2959
+ timestamp,
2960
+ input.due_at || null,
2961
+ input.estimated_minutes || null,
2962
+ input.requires_approval ? 1 : 0,
2963
+ null,
2964
+ null,
2965
+ input.recurrence_rule || null,
2966
+ input.recurrence_parent_id || null,
2967
+ input.spawns_template_id || null,
2968
+ input.reason || null,
2969
+ input.spawned_from_session || null,
2970
+ assignedBy || null,
2971
+ assignedFromProject || null,
2972
+ input.task_type || null
2973
+ ]);
2974
+ break;
2975
+ } catch (e) {
2976
+ if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
2977
+ id = uuid();
2978
+ continue;
2979
+ }
2980
+ throw e;
2981
+ }
2982
+ }
2975
2983
  if (tags.length > 0) {
2976
2984
  insertTaskTags(id, tags, d);
2977
2985
  }
@@ -16311,6 +16319,12 @@ var PG_MIGRATIONS = [
16311
16319
  CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id);
16312
16320
 
16313
16321
  INSERT INTO _migrations (id) VALUES (39) ON CONFLICT DO NOTHING;
16322
+ `,
16323
+ `
16324
+ DROP INDEX IF EXISTS idx_tasks_short_id;
16325
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id, machine_id) WHERE short_id IS NOT NULL AND machine_id IS NOT NULL;
16326
+ CREATE INDEX IF NOT EXISTS idx_tasks_short_id_lookup ON tasks(short_id) WHERE short_id IS NOT NULL;
16327
+ INSERT INTO _migrations (id) VALUES (40) ON CONFLICT DO NOTHING;
16314
16328
  `
16315
16329
  ];
16316
16330