@hasna/todos 0.15.7 → 0.15.9

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/mcp.js CHANGED
@@ -41,7 +41,7 @@ var __require = import.meta.require;
41
41
  // package.json
42
42
  var package_default = {
43
43
  name: "@hasna/todos",
44
- version: "0.15.7",
44
+ version: "0.15.9",
45
45
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
46
46
  type: "module",
47
47
  main: "dist/index.js",
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/project-registration/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EACV,+BAA+B,EAC/B,0CAA0C,EAC1C,sCAAsC,EACtC,kCAAkC,EAClC,oCAAoC,EACpC,kCAAkC,EAEnC,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,YAAY,CAAC;AAuWvE,qBAAa,qCACb,YAAW,+BAA+B;IAI5B,OAAO,CAAC,QAAQ,CAAC,EAAE;IAH/B,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4C;gBAEtC,EAAE,EAAE,QAAQ;IAKnC,WAAW,CAAC,CAAC,EACjB,EAAE,EAAE,CAAC,WAAW,EAAE,0CAA0C,KAAK,OAAO,CAAC,CAAC,CAAC,GAC1E,OAAO,CAAC,CAAC,CAAC;IA4Bb,mBAAmB,CACjB,QAAQ,EAAE,oCAAoC,GAC7C,OAAO,CAAC,kCAAkC,GAAG,IAAI,CAAC;IAIrD,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kCAAkC,GAAG,IAAI,CAAC;IAIrF,UAAU,CACR,KAAK,EAAE,sCAAsC,EAC7C,YAAY,EAAE,oCAAoC,EAClD,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,kCAAkC,GAAG,IAAI,CAAC;IAIrD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAI/C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;CAGlD"}
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/project-registration/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAQ3C,OAAO,KAAK,EAGV,OAAO,EACP,QAAQ,EAET,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EACV,+BAA+B,EAC/B,0CAA0C,EAC1C,sCAAsC,EACtC,kCAAkC,EAClC,oCAAoC,EACpC,kCAAkC,EAEnC,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,YAAY,CAAC;AA4/BvE,qBAAa,qCACb,YAAW,+BAA+B;IAI5B,OAAO,CAAC,QAAQ,CAAC,EAAE;IAH/B,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4C;gBAEtC,EAAE,EAAE,QAAQ;IAKnC,WAAW,CAAC,CAAC,EACjB,EAAE,EAAE,CAAC,WAAW,EAAE,0CAA0C,KAAK,OAAO,CAAC,CAAC,CAAC,GAC1E,OAAO,CAAC,CAAC,CAAC;IAmCb,mBAAmB,CACjB,QAAQ,EAAE,oCAAoC,GAC7C,OAAO,CAAC,kCAAkC,GAAG,IAAI,CAAC;IAIrD,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kCAAkC,GAAG,IAAI,CAAC;IAIrF,UAAU,CACR,KAAK,EAAE,sCAAsC,EAC7C,YAAY,EAAE,oCAAoC,EAClD,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,kCAAkC,GAAG,IAAI,CAAC;IAIrD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAI/C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;CAGlD"}
@@ -6755,6 +6755,40 @@ function deleteTaskList(id, db) {
6755
6755
  return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
6756
6756
  })();
6757
6757
  }
6758
+ function deleteTaskListIfUnchangedAndUnused(id, expected, db) {
6759
+ const d = db || getDatabase();
6760
+ return d.transaction(() => {
6761
+ const current = getTaskList(id, d);
6762
+ if (!current) {
6763
+ return { status: "not_found", task_dependents: 0, plan_dependents: 0 };
6764
+ }
6765
+ const changed = current.project_id !== expected.project_id || current.slug !== expected.slug || current.name !== expected.name || current.description !== expected.description || current.updated_at !== expected.updated_at || JSON.stringify(current.metadata) !== JSON.stringify(expected.metadata);
6766
+ if (changed) {
6767
+ return { status: "changed", task_dependents: 0, plan_dependents: 0 };
6768
+ }
6769
+ const taskDependents = Number(d.query("SELECT COUNT(*) AS count FROM tasks WHERE task_list_id = ?").get(id).count);
6770
+ const planDependents = Number(d.query("SELECT COUNT(*) AS count FROM plans WHERE task_list_id = ?").get(id).count);
6771
+ if (taskDependents > 0 || planDependents > 0) {
6772
+ return {
6773
+ status: "has_dependents",
6774
+ task_dependents: taskDependents,
6775
+ plan_dependents: planDependents
6776
+ };
6777
+ }
6778
+ recordStorageTombstone({
6779
+ object_type: "task_lists",
6780
+ object_id: id,
6781
+ payload: current
6782
+ }, d);
6783
+ releaseCanonicalSlugClaims("task_list", id, d);
6784
+ const deleted = d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
6785
+ return {
6786
+ status: deleted ? "deleted" : "not_found",
6787
+ task_dependents: 0,
6788
+ plan_dependents: 0
6789
+ };
6790
+ })();
6791
+ }
6758
6792
  function ensureTaskList(name, slug, projectId, db) {
6759
6793
  const d = db || getDatabase();
6760
6794
  const existing = getTaskListBySlug(slug, projectId, d);
@@ -12253,7 +12287,7 @@ import { createHash as createHash4 } from "crypto";
12253
12287
  // package.json
12254
12288
  var package_default = {
12255
12289
  name: "@hasna/todos",
12256
- version: "0.15.7",
12290
+ version: "0.15.9",
12257
12291
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12258
12292
  type: "module",
12259
12293
  main: "dist/index.js",
@@ -14975,6 +15009,10 @@ class PostgresTodosProjectRegistrationBackend {
14975
15009
  }
14976
15010
  }
14977
15011
 
15012
+ // src/project-registration/sqlite.ts
15013
+ init_database();
15014
+ init_storage_tombstones();
15015
+
14978
15016
  // src/storage/local-sqlite.ts
14979
15017
  init_types();
14980
15018
 
@@ -16328,7 +16366,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
16328
16366
  getBySlug: (slug, projectId) => getTaskListBySlug(slug, projectId, database()),
16329
16367
  list: (projectId) => listTaskLists(projectId, database()),
16330
16368
  update: (id, input) => updateTaskList2(id, input, database()),
16331
- delete: (id) => deleteTaskList(id, database())
16369
+ delete: (id) => deleteTaskList(id, database()),
16370
+ deleteIfUnchangedAndUnused: (id, expected) => deleteTaskListIfUnchangedAndUnused(id, expected, database())
16332
16371
  },
16333
16372
  templates: {
16334
16373
  create: (input) => createTemplate2(input, database()),
@@ -16384,9 +16423,75 @@ var PROJECT_REFERENCE_COLUMNS = new Set([
16384
16423
  "external_project_id"
16385
16424
  ]);
16386
16425
  var TASK_LIST_REFERENCE_COLUMNS = new Set(["task_list_id"]);
16426
+ var SQLITE_TRANSACTION_RETRY_LIMIT = 8;
16427
+
16428
+ class SqliteRegistrationOptimisticConflict extends Error {
16429
+ constructor(message, options = {}) {
16430
+ super(message, options);
16431
+ this.name = "SqliteRegistrationOptimisticConflict";
16432
+ }
16433
+ }
16434
+ function sameSqliteValue(left, right) {
16435
+ return JSON.stringify(left) === JSON.stringify(right);
16436
+ }
16437
+ function taskListFromRow(row) {
16438
+ return {
16439
+ ...row,
16440
+ metadata: JSON.parse(row.metadata || "{}")
16441
+ };
16442
+ }
16443
+ function selectProject(db, id) {
16444
+ return db.query("SELECT * FROM projects WHERE id = ? LIMIT 1").get(id);
16445
+ }
16446
+ function selectTaskList(db, id) {
16447
+ const row = db.query("SELECT * FROM task_lists WHERE id = ? LIMIT 1").get(id);
16448
+ return row ? taskListFromRow(row) : null;
16449
+ }
16450
+ function selectProjectConflict(db, path, taskListSlug) {
16451
+ return db.query(`
16452
+ SELECT * FROM projects
16453
+ WHERE path = ? OR task_list_id = ?
16454
+ ORDER BY created_at ASC, id ASC
16455
+ LIMIT 1
16456
+ `).get(path, taskListSlug);
16457
+ }
16458
+ function selectTaskListConflict(db, projectId, slug) {
16459
+ const row = db.query(`
16460
+ SELECT * FROM task_lists
16461
+ WHERE project_id = ? AND slug = ?
16462
+ LIMIT 1
16463
+ `).get(projectId, slug);
16464
+ return row ? taskListFromRow(row) : null;
16465
+ }
16387
16466
  function quoteSqliteIdentifier(value) {
16388
16467
  return `"${value.replaceAll('"', '""')}"`;
16389
16468
  }
16469
+ function hasSqliteDependents(db, resourceKind, targetId) {
16470
+ const targetTable = resourceKind === "project" ? "projects" : "task_lists";
16471
+ const semanticColumns = resourceKind === "project" ? PROJECT_REFERENCE_COLUMNS : TASK_LIST_REFERENCE_COLUMNS;
16472
+ const tables = db.query(`
16473
+ SELECT name FROM sqlite_schema
16474
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
16475
+ ORDER BY name
16476
+ `).all();
16477
+ for (const { name: tableName } of tables) {
16478
+ const quotedTable = quoteSqliteIdentifier(tableName);
16479
+ const columns = db.query(`PRAGMA table_info(${quotedTable})`).all();
16480
+ const foreignKeys = db.query(`PRAGMA foreign_key_list(${quotedTable})`).all();
16481
+ const referenceColumns = columns.map((column) => column.name).filter((columnName) => semanticColumns.has(columnName) || foreignKeys.some((foreignKey) => foreignKey.from === columnName && foreignKey.table === targetTable));
16482
+ for (const columnName of referenceColumns) {
16483
+ const row = db.query(`
16484
+ SELECT 1 AS found
16485
+ FROM ${quotedTable}
16486
+ WHERE ${quoteSqliteIdentifier(columnName)} = ?
16487
+ LIMIT 1
16488
+ `).get(targetId);
16489
+ if (row)
16490
+ return true;
16491
+ }
16492
+ }
16493
+ return false;
16494
+ }
16390
16495
  function receiptFromRow2(row) {
16391
16496
  return {
16392
16497
  ...row,
@@ -16529,30 +16634,7 @@ class SqliteTodosProjectRegistrationTransaction {
16529
16634
  }
16530
16635
  async lockCompensationWrites() {}
16531
16636
  async hasDependents(resourceKind, targetId) {
16532
- const targetTable = resourceKind === "project" ? "projects" : "task_lists";
16533
- const semanticColumns = resourceKind === "project" ? PROJECT_REFERENCE_COLUMNS : TASK_LIST_REFERENCE_COLUMNS;
16534
- const tables = this.db.query(`
16535
- SELECT name FROM sqlite_schema
16536
- WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
16537
- ORDER BY name
16538
- `).all();
16539
- for (const { name: tableName } of tables) {
16540
- const quotedTable = quoteSqliteIdentifier(tableName);
16541
- const columns = this.db.query(`PRAGMA table_info(${quotedTable})`).all();
16542
- const foreignKeys = this.db.query(`PRAGMA foreign_key_list(${quotedTable})`).all();
16543
- const referenceColumns = columns.map((column) => column.name).filter((columnName) => semanticColumns.has(columnName) || foreignKeys.some((foreignKey) => foreignKey.from === columnName && foreignKey.table === targetTable));
16544
- for (const columnName of referenceColumns) {
16545
- const row = this.db.query(`
16546
- SELECT 1 AS found
16547
- FROM ${quotedTable}
16548
- WHERE ${quoteSqliteIdentifier(columnName)} = ?
16549
- LIMIT 1
16550
- `).get(targetId);
16551
- if (row)
16552
- return true;
16553
- }
16554
- }
16555
- return false;
16637
+ return hasSqliteDependents(this.db, resourceKind, targetId);
16556
16638
  }
16557
16639
  async deleteProject(id) {
16558
16640
  return await this.storage.projects.delete(id);
@@ -16562,6 +16644,376 @@ class SqliteTodosProjectRegistrationTransaction {
16562
16644
  }
16563
16645
  }
16564
16646
 
16647
+ class StagedSqliteTodosProjectRegistrationTransaction {
16648
+ db;
16649
+ direct;
16650
+ validators = [];
16651
+ mutations = [];
16652
+ receipts = new Map;
16653
+ bindings = new Map;
16654
+ projects = new Map;
16655
+ taskLists = new Map;
16656
+ constructor(db) {
16657
+ this.db = db;
16658
+ this.direct = new SqliteTodosProjectRegistrationTransaction(db);
16659
+ }
16660
+ commit() {
16661
+ this.db.exec("BEGIN IMMEDIATE");
16662
+ try {
16663
+ for (const validate of this.validators) {
16664
+ if (!validate()) {
16665
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration input changed before SQLite commit");
16666
+ }
16667
+ }
16668
+ for (const mutate of this.mutations)
16669
+ mutate();
16670
+ this.db.exec("COMMIT");
16671
+ } catch (error) {
16672
+ try {
16673
+ this.db.exec("ROLLBACK");
16674
+ } catch {}
16675
+ throw error;
16676
+ }
16677
+ }
16678
+ async lockStep(_identity) {}
16679
+ async getReceiptForLookup(identity) {
16680
+ const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
16681
+ const stored = await this.direct.getReceiptForLookup(identity);
16682
+ if (stored)
16683
+ staged.push(stored);
16684
+ const outcomeRank = (receipt) => receipt.outcome === "terminal_nonacceptance" ? 0 : receipt.outcome === "duplicate_of_accepted" ? 1 : 2;
16685
+ return staged.sort((left, right) => outcomeRank(left) - outcomeRank(right) || right.created_at.localeCompare(left.created_at) || right.receipt_id.localeCompare(left.receipt_id))[0] ?? null;
16686
+ }
16687
+ async getReceiptById(receiptId) {
16688
+ return this.receipts.get(receiptId) ?? this.direct.getReceiptById(receiptId);
16689
+ }
16690
+ async getAcceptedReceiptForStep(identity) {
16691
+ const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.outcome === "accepted");
16692
+ const stored = await this.direct.getAcceptedReceiptForStep(identity);
16693
+ if (stored)
16694
+ staged.push(stored);
16695
+ return staged.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.receipt_id.localeCompare(right.receipt_id))[0] ?? null;
16696
+ }
16697
+ async insertReceipt(receipt) {
16698
+ if (this.receipts.has(receipt.receipt_id))
16699
+ return false;
16700
+ if (await this.direct.getReceiptById(receipt.receipt_id))
16701
+ return false;
16702
+ const planned = { ...receipt };
16703
+ this.receipts.set(planned.receipt_id, planned);
16704
+ this.mutations.push(() => {
16705
+ try {
16706
+ const result = this.db.query(`
16707
+ INSERT OR IGNORE INTO todos_project_registration_receipts (
16708
+ receipt_id, authority, route, package_version, authority_id, tenant_id,
16709
+ corpus_id, operation_id, step_id, resource_kind, direction,
16710
+ target_selector, idempotency_key, request_digest, precondition_digest,
16711
+ normalized_call_digest, outcome, reason, target_id, result_revision,
16712
+ result_digest, duplicate_of_receipt_id, accepted_receipt_id,
16713
+ created_by_operation, created_at
16714
+ ) VALUES (
16715
+ ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
16716
+ )
16717
+ `).run(planned.receipt_id, planned.authority, planned.route, planned.package_version, planned.authority_id, planned.tenant_id, planned.corpus_id, planned.operation_id, planned.step_id, planned.resource_kind, planned.direction, planned.target_selector, planned.idempotency_key, planned.request_digest, planned.precondition_digest, planned.normalized_call_digest, planned.outcome, planned.reason, planned.target_id, planned.result_revision, planned.result_digest, planned.duplicate_of_receipt_id, planned.accepted_receipt_id, planned.created_by_operation ? 1 : 0, planned.created_at);
16718
+ if (result.changes !== 1) {
16719
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt changed before SQLite commit");
16720
+ }
16721
+ } catch (error) {
16722
+ if (error instanceof SqliteRegistrationOptimisticConflict)
16723
+ throw error;
16724
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt conflicted at SQLite commit", { cause: error });
16725
+ }
16726
+ });
16727
+ return true;
16728
+ }
16729
+ async getBinding(scope, resourceKind, targetSelector) {
16730
+ const key = this.bindingKey(scope, resourceKind, targetSelector);
16731
+ return this.bindings.get(key) ?? this.direct.getBinding(scope, resourceKind, targetSelector);
16732
+ }
16733
+ async claimBinding(binding) {
16734
+ const key = this.bindingKey(binding, binding.resource_kind, binding.target_selector);
16735
+ if (this.bindings.has(key))
16736
+ return false;
16737
+ if (await this.direct.getBinding(binding, binding.resource_kind, binding.target_selector)) {
16738
+ return false;
16739
+ }
16740
+ const planned = { ...binding };
16741
+ this.bindings.set(key, planned);
16742
+ this.mutations.push(() => {
16743
+ try {
16744
+ const result = this.db.query(`
16745
+ INSERT OR IGNORE INTO todos_project_registration_bindings (
16746
+ authority_id, tenant_id, corpus_id, resource_kind, target_selector,
16747
+ operation_id, step_id, direction, idempotency_key, request_digest,
16748
+ precondition_digest, normalized_call_digest, state, target_id,
16749
+ accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
16750
+ created_at, updated_at
16751
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
16752
+ `).run(planned.authority_id, planned.tenant_id, planned.corpus_id, planned.resource_kind, planned.target_selector, planned.operation_id, planned.step_id, planned.direction, planned.idempotency_key, planned.request_digest, planned.precondition_digest, planned.normalized_call_digest, planned.state, planned.target_id, planned.accepted_receipt_id, planned.result_revision, planned.result_digest, planned.removed_receipt_id, planned.created_at, planned.updated_at);
16753
+ if (result.changes !== 1) {
16754
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding changed before SQLite commit");
16755
+ }
16756
+ } catch (error) {
16757
+ if (error instanceof SqliteRegistrationOptimisticConflict)
16758
+ throw error;
16759
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding conflicted at SQLite commit", { cause: error });
16760
+ }
16761
+ });
16762
+ return true;
16763
+ }
16764
+ async setBindingAccepted(scope, resourceKind, targetSelector, update) {
16765
+ const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
16766
+ this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
16767
+ ...binding,
16768
+ state: "accepted",
16769
+ target_id: update.target_id,
16770
+ accepted_receipt_id: update.accepted_receipt_id,
16771
+ result_revision: update.result_revision,
16772
+ result_digest: update.result_digest,
16773
+ updated_at: update.updated_at
16774
+ });
16775
+ this.mutations.push(() => {
16776
+ const result = this.db.query(`
16777
+ UPDATE todos_project_registration_bindings
16778
+ SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
16779
+ result_revision = ?, result_digest = ?, updated_at = ?
16780
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
16781
+ AND resource_kind = ? AND target_selector = ? AND state = 'pending'
16782
+ `).run(update.target_id, update.accepted_receipt_id, update.result_revision, update.result_digest, update.updated_at, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
16783
+ if (result.changes !== 1) {
16784
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
16785
+ }
16786
+ });
16787
+ }
16788
+ async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
16789
+ const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
16790
+ this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
16791
+ ...binding,
16792
+ state: "terminal_nonacceptance",
16793
+ updated_at: updatedAt
16794
+ });
16795
+ this.mutations.push(() => {
16796
+ const result = this.db.query(`
16797
+ UPDATE todos_project_registration_bindings
16798
+ SET state = 'terminal_nonacceptance', updated_at = ?
16799
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
16800
+ AND resource_kind = ? AND target_selector = ? AND state = 'pending'
16801
+ `).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
16802
+ if (result.changes !== 1) {
16803
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
16804
+ }
16805
+ });
16806
+ }
16807
+ async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
16808
+ const binding = await this.requireBinding(scope, resourceKind, targetSelector, "accepted");
16809
+ this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
16810
+ ...binding,
16811
+ state: "removed",
16812
+ removed_receipt_id: removedReceiptId,
16813
+ updated_at: updatedAt
16814
+ });
16815
+ this.mutations.push(() => {
16816
+ const result = this.db.query(`
16817
+ UPDATE todos_project_registration_bindings
16818
+ SET state = 'removed', removed_receipt_id = ?, updated_at = ?
16819
+ WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
16820
+ AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
16821
+ `).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
16822
+ if (result.changes !== 1) {
16823
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer accepted at SQLite commit");
16824
+ }
16825
+ });
16826
+ }
16827
+ async findProjectConflict(path, taskListSlug) {
16828
+ const planned = [...this.projects.values()].find((project) => project?.path === path || project?.task_list_id === taskListSlug);
16829
+ if (planned)
16830
+ return planned;
16831
+ const observed = selectProjectConflict(this.db, path, taskListSlug);
16832
+ this.validators.push(() => sameSqliteValue(selectProjectConflict(this.db, path, taskListSlug), observed));
16833
+ return observed;
16834
+ }
16835
+ async findTaskListConflict(projectId, slug) {
16836
+ const planned = [...this.taskLists.values()].find((taskList) => taskList?.project_id === projectId && taskList.slug === slug);
16837
+ if (planned)
16838
+ return planned;
16839
+ const observed = selectTaskListConflict(this.db, projectId, slug);
16840
+ this.validators.push(() => sameSqliteValue(selectTaskListConflict(this.db, projectId, slug), observed));
16841
+ return observed;
16842
+ }
16843
+ async createProject(input) {
16844
+ const derivedSlug = normalizeSlug(input.name);
16845
+ const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : normalizeSlug(input.task_list_id);
16846
+ if (!derivedSlug || !taskListId) {
16847
+ throw new Error("Project name and task-list slug must be non-empty");
16848
+ }
16849
+ const project = {
16850
+ id: uuid(),
16851
+ name: input.name,
16852
+ path: input.path,
16853
+ description: input.description || null,
16854
+ task_list_id: taskListId,
16855
+ task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
16856
+ task_counter: 0,
16857
+ created_at: now(),
16858
+ updated_at: now(),
16859
+ machine_id: currentStorageMachineId(this.db)
16860
+ };
16861
+ project.updated_at = project.created_at;
16862
+ this.projects.set(project.id, project);
16863
+ this.mutations.push(() => {
16864
+ try {
16865
+ const result = this.db.run(`INSERT INTO projects (
16866
+ id, name, path, description, task_list_id, task_prefix,
16867
+ task_counter, created_at, updated_at, machine_id
16868
+ ) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
16869
+ project.id,
16870
+ project.name,
16871
+ project.path,
16872
+ project.description,
16873
+ project.task_list_id,
16874
+ project.task_prefix,
16875
+ project.created_at,
16876
+ project.updated_at,
16877
+ project.machine_id ?? null
16878
+ ]);
16879
+ if (result.changes < 1) {
16880
+ throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite registration commit");
16881
+ }
16882
+ } catch (error) {
16883
+ if (error instanceof SqliteRegistrationOptimisticConflict)
16884
+ throw error;
16885
+ throw new SqliteRegistrationOptimisticConflict("Todos project conflicted at SQLite registration commit", { cause: error });
16886
+ }
16887
+ });
16888
+ return project;
16889
+ }
16890
+ async createTaskList(input) {
16891
+ const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
16892
+ if (!slug)
16893
+ throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
16894
+ const taskList = {
16895
+ id: uuid(),
16896
+ project_id: input.project_id || null,
16897
+ slug,
16898
+ name: input.name,
16899
+ description: input.description || null,
16900
+ metadata: input.metadata ?? {},
16901
+ created_at: now(),
16902
+ updated_at: now(),
16903
+ machine_id: currentStorageMachineId(this.db)
16904
+ };
16905
+ taskList.updated_at = taskList.created_at;
16906
+ this.taskLists.set(taskList.id, taskList);
16907
+ this.mutations.push(() => {
16908
+ try {
16909
+ const result = this.db.run(`INSERT INTO task_lists (
16910
+ id, project_id, slug, name, description, metadata,
16911
+ created_at, updated_at, machine_id
16912
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
16913
+ taskList.id,
16914
+ taskList.project_id,
16915
+ taskList.slug,
16916
+ taskList.name,
16917
+ taskList.description,
16918
+ JSON.stringify(taskList.metadata),
16919
+ taskList.created_at,
16920
+ taskList.updated_at,
16921
+ taskList.machine_id ?? null
16922
+ ]);
16923
+ if (result.changes < 1) {
16924
+ throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite registration commit");
16925
+ }
16926
+ } catch (error) {
16927
+ if (error instanceof SqliteRegistrationOptimisticConflict)
16928
+ throw error;
16929
+ throw new SqliteRegistrationOptimisticConflict("Todos task list conflicted at SQLite registration commit", { cause: error });
16930
+ }
16931
+ });
16932
+ return taskList;
16933
+ }
16934
+ async getProject(id) {
16935
+ if (this.projects.has(id))
16936
+ return this.projects.get(id) ?? null;
16937
+ const observed = selectProject(this.db, id);
16938
+ this.validators.push(() => sameSqliteValue(selectProject(this.db, id), observed));
16939
+ return observed;
16940
+ }
16941
+ async getTaskList(id) {
16942
+ if (this.taskLists.has(id))
16943
+ return this.taskLists.get(id) ?? null;
16944
+ const observed = selectTaskList(this.db, id);
16945
+ this.validators.push(() => sameSqliteValue(selectTaskList(this.db, id), observed));
16946
+ return observed;
16947
+ }
16948
+ async lockCompensationWrites() {}
16949
+ async hasDependents(resourceKind, targetId) {
16950
+ const observed = hasSqliteDependents(this.db, resourceKind, targetId);
16951
+ this.validators.push(() => hasSqliteDependents(this.db, resourceKind, targetId) === observed);
16952
+ return observed;
16953
+ }
16954
+ async deleteProject(id) {
16955
+ const project = await this.getProject(id);
16956
+ if (!project)
16957
+ return false;
16958
+ this.projects.set(id, null);
16959
+ this.mutations.push(() => {
16960
+ recordStorageTombstone({
16961
+ object_type: "projects",
16962
+ object_id: id,
16963
+ payload: project
16964
+ }, this.db);
16965
+ if (this.db.run("DELETE FROM projects WHERE id = ?", [id]).changes < 1) {
16966
+ throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite compensation commit");
16967
+ }
16968
+ });
16969
+ return true;
16970
+ }
16971
+ async deleteTaskList(id) {
16972
+ const taskList = await this.getTaskList(id);
16973
+ if (!taskList)
16974
+ return false;
16975
+ this.taskLists.set(id, null);
16976
+ this.mutations.push(() => {
16977
+ recordStorageTombstone({
16978
+ object_type: "task_lists",
16979
+ object_id: id,
16980
+ payload: taskList
16981
+ }, this.db);
16982
+ if (this.db.run("DELETE FROM task_lists WHERE id = ?", [id]).changes < 1) {
16983
+ throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite compensation commit");
16984
+ }
16985
+ });
16986
+ return true;
16987
+ }
16988
+ bindingKey(scope, resourceKind, targetSelector) {
16989
+ return JSON.stringify([
16990
+ scope.authority_id,
16991
+ scope.tenant_id,
16992
+ scope.corpus_id,
16993
+ resourceKind,
16994
+ targetSelector
16995
+ ]);
16996
+ }
16997
+ async requireBinding(scope, resourceKind, targetSelector, state) {
16998
+ const binding = await this.getBinding(scope, resourceKind, targetSelector);
16999
+ if (!binding || binding.state !== state) {
17000
+ throw new Error(`Todos project registration binding was not ${state}`);
17001
+ }
17002
+ return binding;
17003
+ }
17004
+ availableProjectPrefix(name) {
17005
+ const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
17006
+ const prefix = words.length >= 3 ? words.slice(0, 3).map((word) => word[0].toUpperCase()).join("") : words.length === 2 ? (words[0].slice(0, 2) + words[1][0]).toUpperCase() : words[0].slice(0, 3).toUpperCase();
17007
+ let candidate = prefix;
17008
+ let suffix = 1;
17009
+ while (this.db.query("SELECT id FROM projects WHERE task_prefix = ? LIMIT 1").get(candidate) || [...this.projects.values()].some((project) => project?.task_prefix === candidate)) {
17010
+ suffix += 1;
17011
+ candidate = `${prefix}${suffix}`;
17012
+ }
17013
+ return candidate;
17014
+ }
17015
+ }
17016
+
16565
17017
  class SqliteTodosProjectRegistrationBackend {
16566
17018
  db;
16567
17019
  kind = "sqlite";
@@ -16580,15 +17032,19 @@ class SqliteTodosProjectRegistrationBackend {
16580
17032
  sqliteTransactionTails.set(this.db, current);
16581
17033
  await previous;
16582
17034
  try {
16583
- this.db.exec("BEGIN IMMEDIATE");
16584
- const result = await fn(new SqliteTodosProjectRegistrationTransaction(this.db));
16585
- this.db.exec("COMMIT");
16586
- return result;
16587
- } catch (error) {
16588
- try {
16589
- this.db.exec("ROLLBACK");
16590
- } catch {}
16591
- throw error;
17035
+ for (let attempt = 0;attempt < SQLITE_TRANSACTION_RETRY_LIMIT; attempt += 1) {
17036
+ const transaction = new StagedSqliteTodosProjectRegistrationTransaction(this.db);
17037
+ const result = await fn(transaction);
17038
+ try {
17039
+ transaction.commit();
17040
+ return result;
17041
+ } catch (error) {
17042
+ if (!(error instanceof SqliteRegistrationOptimisticConflict) || attempt === SQLITE_TRANSACTION_RETRY_LIMIT - 1) {
17043
+ throw error;
17044
+ }
17045
+ }
17046
+ }
17047
+ throw new SqliteRegistrationOptimisticConflict("Todos project registration exhausted SQLite optimistic retries");
16592
17048
  } finally {
16593
17049
  release();
16594
17050
  if (sqliteTransactionTails.get(this.db) === current) {
package/dist/registry.js CHANGED
@@ -6747,6 +6747,40 @@ function deleteTaskList(id, db) {
6747
6747
  return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
6748
6748
  })();
6749
6749
  }
6750
+ function deleteTaskListIfUnchangedAndUnused(id, expected, db) {
6751
+ const d = db || getDatabase();
6752
+ return d.transaction(() => {
6753
+ const current = getTaskList(id, d);
6754
+ if (!current) {
6755
+ return { status: "not_found", task_dependents: 0, plan_dependents: 0 };
6756
+ }
6757
+ const changed = current.project_id !== expected.project_id || current.slug !== expected.slug || current.name !== expected.name || current.description !== expected.description || current.updated_at !== expected.updated_at || JSON.stringify(current.metadata) !== JSON.stringify(expected.metadata);
6758
+ if (changed) {
6759
+ return { status: "changed", task_dependents: 0, plan_dependents: 0 };
6760
+ }
6761
+ const taskDependents = Number(d.query("SELECT COUNT(*) AS count FROM tasks WHERE task_list_id = ?").get(id).count);
6762
+ const planDependents = Number(d.query("SELECT COUNT(*) AS count FROM plans WHERE task_list_id = ?").get(id).count);
6763
+ if (taskDependents > 0 || planDependents > 0) {
6764
+ return {
6765
+ status: "has_dependents",
6766
+ task_dependents: taskDependents,
6767
+ plan_dependents: planDependents
6768
+ };
6769
+ }
6770
+ recordStorageTombstone({
6771
+ object_type: "task_lists",
6772
+ object_id: id,
6773
+ payload: current
6774
+ }, d);
6775
+ releaseCanonicalSlugClaims("task_list", id, d);
6776
+ const deleted = d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
6777
+ return {
6778
+ status: deleted ? "deleted" : "not_found",
6779
+ task_dependents: 0,
6780
+ plan_dependents: 0
6781
+ };
6782
+ })();
6783
+ }
6750
6784
  function ensureTaskList(name, slug, projectId, db) {
6751
6785
  const d = db || getDatabase();
6752
6786
  const existing = getTaskListBySlug(slug, projectId, d);
@@ -12250,7 +12284,7 @@ var init_tasks = __esm(() => {
12250
12284
  // package.json
12251
12285
  var package_default = {
12252
12286
  name: "@hasna/todos",
12253
- version: "0.15.7",
12287
+ version: "0.15.9",
12254
12288
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12255
12289
  type: "module",
12256
12290
  main: "dist/index.js",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "packageName": "@hasna/todos",
3
- "packageVersion": "0.15.7",
3
+ "packageVersion": "0.15.9",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "6ef5eb2227291a359af981c83ef65de91095c0fb",
6
- "gitTree": "bd69c321f9e9bad133ceb8651951b6249275f0ed",
7
- "sourceTreeSha256": "66e3e6e675918a3270ba06abecb416fe9c028316f2c53d61e753f29d38855318",
8
- "generatedAt": "2026-08-07T15:16:11.000Z"
5
+ "gitCommit": "331ab0710345c56ca1ca7ca7b67dc1777a8d4dc1",
6
+ "gitTree": "81c21adf316b981a2f3ab0f9b6383f5543d44eca",
7
+ "sourceTreeSha256": "bc48c5fe8df470608a6ede9df9e22b8366fbe0e9422bdc046e1612e6a0fcf551",
8
+ "generatedAt": "2026-08-07T17:58:30.000Z"
9
9
  }