@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/cli/cloud-router.d.ts +13 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2300 -1123
- package/dist/contracts.js +35 -1
- package/dist/db/task-lists.d.ts +5 -0
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/index.js +489 -35
- package/dist/lib/assignee-validation.d.ts +44 -0
- package/dist/lib/assignee-validation.d.ts.map +1 -1
- package/dist/lib/project-task-list-ensure.d.ts +19 -0
- package/dist/lib/project-task-list-ensure.d.ts.map +1 -0
- package/dist/mcp/index.js +1085 -5
- package/dist/mcp.js +1 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration.js +491 -35
- package/dist/registry.js +35 -1
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +21 -0
- package/dist/sdk/v1.generated.d.ts +43 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1179 -99
- package/dist/server/openapi.d.ts +311 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +11 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage.js +36 -1
- package/dist/types/index.d.ts +29 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
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.
|
|
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;
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
16584
|
-
|
|
16585
|
-
|
|
16586
|
-
|
|
16587
|
-
|
|
16588
|
-
|
|
16589
|
-
|
|
16590
|
-
|
|
16591
|
-
|
|
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.
|
|
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.
|
|
3
|
+
"packageVersion": "0.15.9",
|
|
4
4
|
"repository": "https://github.com/hasna/todos.git",
|
|
5
|
-
"gitCommit": "
|
|
6
|
-
"gitTree": "
|
|
7
|
-
"sourceTreeSha256": "
|
|
8
|
-
"generatedAt": "2026-08-
|
|
5
|
+
"gitCommit": "331ab0710345c56ca1ca7ca7b67dc1777a8d4dc1",
|
|
6
|
+
"gitTree": "81c21adf316b981a2f3ab0f9b6383f5543d44eca",
|
|
7
|
+
"sourceTreeSha256": "bc48c5fe8df470608a6ede9df9e22b8366fbe0e9422bdc046e1612e6a0fcf551",
|
|
8
|
+
"generatedAt": "2026-08-07T17:58:30.000Z"
|
|
9
9
|
}
|