@hasna/todos 0.11.88 → 0.11.89
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 +10 -2
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.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 +950 -118
- package/dist/contracts.js +301 -72
- package/dist/db/projects.d.ts +2 -2
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/slug-claims.d.ts +11 -0
- package/dist/db/slug-claims.d.ts.map +1 -0
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/index.js +641 -100
- package/dist/lib/slugs.d.ts +19 -0
- package/dist/lib/slugs.d.ts.map +1 -0
- package/dist/mcp/index.js +897 -102
- package/dist/registry.js +301 -72
- package/dist/release-provenance.json +3 -3
- package/dist/sdk/index.js +56 -0
- package/dist/sdk/v1.generated.d.ts +79 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +2 -0
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +906 -108
- package/dist/server/openapi.d.ts +464 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +3 -2
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +30 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/shadow.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +635 -88
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/registry.js
CHANGED
|
@@ -1748,6 +1748,95 @@ function ensureSchema(db) {
|
|
|
1748
1748
|
)`);
|
|
1749
1749
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
|
|
1750
1750
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
|
|
1751
|
+
ensureTable("canonical_slug_claims", `
|
|
1752
|
+
CREATE TABLE canonical_slug_claims (
|
|
1753
|
+
kind TEXT NOT NULL CHECK(kind IN ('project', 'task_list')),
|
|
1754
|
+
scope_key TEXT NOT NULL,
|
|
1755
|
+
slug TEXT NOT NULL,
|
|
1756
|
+
object_id TEXT NOT NULL,
|
|
1757
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1758
|
+
PRIMARY KEY (kind, scope_key, slug)
|
|
1759
|
+
)`);
|
|
1760
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_canonical_slug_claims_object ON canonical_slug_claims(kind, object_id)");
|
|
1761
|
+
ensureColumn("projects", "task_list_id", "TEXT");
|
|
1762
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS claim_project_canonical_slug_insert
|
|
1763
|
+
BEFORE INSERT ON projects
|
|
1764
|
+
WHEN NEW.task_list_id IS NOT NULL AND NEW.task_list_id <> ''
|
|
1765
|
+
BEGIN
|
|
1766
|
+
SELECT CASE WHEN EXISTS (
|
|
1767
|
+
SELECT 1 FROM projects WHERE id <> NEW.id AND task_list_id = NEW.task_list_id
|
|
1768
|
+
) THEN RAISE(ABORT, 'PROJECT_SLUG_CONFLICT') END;
|
|
1769
|
+
INSERT OR IGNORE INTO canonical_slug_claims(kind, scope_key, slug, object_id)
|
|
1770
|
+
VALUES ('project', 'global', NEW.task_list_id, NEW.id);
|
|
1771
|
+
SELECT CASE WHEN (
|
|
1772
|
+
SELECT object_id FROM canonical_slug_claims
|
|
1773
|
+
WHERE kind = 'project' AND scope_key = 'global' AND slug = NEW.task_list_id
|
|
1774
|
+
) <> NEW.id THEN RAISE(ABORT, 'PROJECT_SLUG_CONFLICT') END;
|
|
1775
|
+
END`);
|
|
1776
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS claim_project_canonical_slug_update
|
|
1777
|
+
BEFORE UPDATE OF task_list_id ON projects
|
|
1778
|
+
WHEN NEW.task_list_id IS NOT OLD.task_list_id
|
|
1779
|
+
BEGIN
|
|
1780
|
+
DELETE FROM canonical_slug_claims WHERE kind = 'project' AND object_id = NEW.id;
|
|
1781
|
+
SELECT CASE WHEN EXISTS (
|
|
1782
|
+
SELECT 1 FROM projects WHERE id <> NEW.id AND task_list_id = NEW.task_list_id
|
|
1783
|
+
) AND NEW.task_list_id IS NOT NULL AND NEW.task_list_id <> ''
|
|
1784
|
+
THEN RAISE(ABORT, 'PROJECT_SLUG_CONFLICT') END;
|
|
1785
|
+
INSERT OR IGNORE INTO canonical_slug_claims(kind, scope_key, slug, object_id)
|
|
1786
|
+
SELECT 'project', 'global', NEW.task_list_id, NEW.id
|
|
1787
|
+
WHERE NEW.task_list_id IS NOT NULL AND NEW.task_list_id <> '';
|
|
1788
|
+
SELECT CASE WHEN NEW.task_list_id IS NOT NULL AND NEW.task_list_id <> '' AND (
|
|
1789
|
+
SELECT object_id FROM canonical_slug_claims
|
|
1790
|
+
WHERE kind = 'project' AND scope_key = 'global' AND slug = NEW.task_list_id
|
|
1791
|
+
) <> NEW.id THEN RAISE(ABORT, 'PROJECT_SLUG_CONFLICT') END;
|
|
1792
|
+
END`);
|
|
1793
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS release_project_canonical_slug_delete
|
|
1794
|
+
AFTER DELETE ON projects
|
|
1795
|
+
BEGIN
|
|
1796
|
+
DELETE FROM canonical_slug_claims WHERE kind = 'project' AND object_id = OLD.id;
|
|
1797
|
+
END`);
|
|
1798
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS claim_task_list_canonical_slug_insert
|
|
1799
|
+
BEFORE INSERT ON task_lists
|
|
1800
|
+
WHEN NEW.slug IS NOT NULL AND NEW.slug <> ''
|
|
1801
|
+
BEGIN
|
|
1802
|
+
SELECT CASE WHEN EXISTS (
|
|
1803
|
+
SELECT 1 FROM task_lists
|
|
1804
|
+
WHERE id <> NEW.id AND project_id IS NEW.project_id AND slug = NEW.slug
|
|
1805
|
+
) THEN RAISE(ABORT, 'TASK_LIST_SLUG_CONFLICT') END;
|
|
1806
|
+
INSERT OR IGNORE INTO canonical_slug_claims(kind, scope_key, slug, object_id)
|
|
1807
|
+
VALUES ('task_list', CASE WHEN NEW.project_id IS NULL THEN 'standalone:' ELSE 'project:' || NEW.project_id END, NEW.slug, NEW.id);
|
|
1808
|
+
SELECT CASE WHEN (
|
|
1809
|
+
SELECT object_id FROM canonical_slug_claims
|
|
1810
|
+
WHERE kind = 'task_list'
|
|
1811
|
+
AND scope_key = CASE WHEN NEW.project_id IS NULL THEN 'standalone:' ELSE 'project:' || NEW.project_id END
|
|
1812
|
+
AND slug = NEW.slug
|
|
1813
|
+
) <> NEW.id THEN RAISE(ABORT, 'TASK_LIST_SLUG_CONFLICT') END;
|
|
1814
|
+
END`);
|
|
1815
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS claim_task_list_canonical_slug_update
|
|
1816
|
+
BEFORE UPDATE OF slug, project_id ON task_lists
|
|
1817
|
+
WHEN NEW.slug IS NOT OLD.slug OR NEW.project_id IS NOT OLD.project_id
|
|
1818
|
+
BEGIN
|
|
1819
|
+
DELETE FROM canonical_slug_claims WHERE kind = 'task_list' AND object_id = NEW.id;
|
|
1820
|
+
SELECT CASE WHEN EXISTS (
|
|
1821
|
+
SELECT 1 FROM task_lists
|
|
1822
|
+
WHERE id <> NEW.id AND project_id IS NEW.project_id AND slug = NEW.slug
|
|
1823
|
+
) AND NEW.slug IS NOT NULL AND NEW.slug <> ''
|
|
1824
|
+
THEN RAISE(ABORT, 'TASK_LIST_SLUG_CONFLICT') END;
|
|
1825
|
+
INSERT OR IGNORE INTO canonical_slug_claims(kind, scope_key, slug, object_id)
|
|
1826
|
+
SELECT 'task_list', CASE WHEN NEW.project_id IS NULL THEN 'standalone:' ELSE 'project:' || NEW.project_id END, NEW.slug, NEW.id
|
|
1827
|
+
WHERE NEW.slug IS NOT NULL AND NEW.slug <> '';
|
|
1828
|
+
SELECT CASE WHEN NEW.slug IS NOT NULL AND NEW.slug <> '' AND (
|
|
1829
|
+
SELECT object_id FROM canonical_slug_claims
|
|
1830
|
+
WHERE kind = 'task_list'
|
|
1831
|
+
AND scope_key = CASE WHEN NEW.project_id IS NULL THEN 'standalone:' ELSE 'project:' || NEW.project_id END
|
|
1832
|
+
AND slug = NEW.slug
|
|
1833
|
+
) <> NEW.id THEN RAISE(ABORT, 'TASK_LIST_SLUG_CONFLICT') END;
|
|
1834
|
+
END`);
|
|
1835
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS release_task_list_canonical_slug_delete
|
|
1836
|
+
AFTER DELETE ON task_lists
|
|
1837
|
+
BEGIN
|
|
1838
|
+
DELETE FROM canonical_slug_claims WHERE kind = 'task_list' AND object_id = OLD.id;
|
|
1839
|
+
END`);
|
|
1751
1840
|
ensureTable("storage_tombstones", `
|
|
1752
1841
|
CREATE TABLE storage_tombstones (
|
|
1753
1842
|
id TEXT PRIMARY KEY,
|
|
@@ -3141,7 +3230,7 @@ var init_database = __esm(() => {
|
|
|
3141
3230
|
});
|
|
3142
3231
|
|
|
3143
3232
|
// src/types/index.ts
|
|
3144
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3233
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3145
3234
|
var init_types = __esm(() => {
|
|
3146
3235
|
TASK_STATUSES = [
|
|
3147
3236
|
"pending",
|
|
@@ -3191,6 +3280,14 @@ var init_types = __esm(() => {
|
|
|
3191
3280
|
this.name = "ProjectNotFoundError";
|
|
3192
3281
|
}
|
|
3193
3282
|
};
|
|
3283
|
+
ResourceConflictError = class ResourceConflictError extends Error {
|
|
3284
|
+
code;
|
|
3285
|
+
constructor(code, message) {
|
|
3286
|
+
super(message);
|
|
3287
|
+
this.code = code;
|
|
3288
|
+
this.name = "ResourceConflictError";
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3194
3291
|
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
3195
3292
|
planId;
|
|
3196
3293
|
static code = "PLAN_NOT_FOUND";
|
|
@@ -3496,9 +3593,94 @@ var init_storage_tombstones = __esm(() => {
|
|
|
3496
3593
|
init_machines();
|
|
3497
3594
|
});
|
|
3498
3595
|
|
|
3596
|
+
// src/lib/slugs.ts
|
|
3597
|
+
function normalizeSlug(value) {
|
|
3598
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
3599
|
+
}
|
|
3600
|
+
function isCanonicalSlug(value) {
|
|
3601
|
+
return typeof value === "string" && value.length > 0 && normalizeSlug(value) === value;
|
|
3602
|
+
}
|
|
3603
|
+
function isValidTaskListProjectScope(value) {
|
|
3604
|
+
return value === undefined || value === null || typeof value === "string" && value.trim().length > 0;
|
|
3605
|
+
}
|
|
3606
|
+
function validateSnapshotRoutingRecords(projects, taskLists) {
|
|
3607
|
+
const errors = [];
|
|
3608
|
+
const projectSlugs = new Map;
|
|
3609
|
+
const taskListSlugs = new Map;
|
|
3610
|
+
for (const project of projects) {
|
|
3611
|
+
if (!isCanonicalSlug(project.task_list_id)) {
|
|
3612
|
+
errors.push(`project ${project.id}: task_list_id must be non-empty canonical kebab-case`);
|
|
3613
|
+
continue;
|
|
3614
|
+
}
|
|
3615
|
+
const existing = projectSlugs.get(project.task_list_id);
|
|
3616
|
+
if (projectSlugs.has(project.task_list_id)) {
|
|
3617
|
+
errors.push(`project ${project.id}: task_list_id duplicates project ${existing}: ${project.task_list_id}`);
|
|
3618
|
+
} else {
|
|
3619
|
+
projectSlugs.set(project.task_list_id, project.id);
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
for (const taskList of taskLists) {
|
|
3623
|
+
if (!isCanonicalSlug(taskList.slug)) {
|
|
3624
|
+
errors.push(`task list ${taskList.id}: slug must be non-empty canonical kebab-case`);
|
|
3625
|
+
continue;
|
|
3626
|
+
}
|
|
3627
|
+
if (!isValidTaskListProjectScope(taskList.project_id)) {
|
|
3628
|
+
errors.push(`task list ${taskList.id}: project_id must be null, missing, or a non-empty string`);
|
|
3629
|
+
continue;
|
|
3630
|
+
}
|
|
3631
|
+
const scope = taskList.project_id ?? null;
|
|
3632
|
+
const scopedSlugs = taskListSlugs.get(scope) ?? new Map;
|
|
3633
|
+
const existing = scopedSlugs.get(taskList.slug);
|
|
3634
|
+
if (scopedSlugs.has(taskList.slug)) {
|
|
3635
|
+
errors.push(`task list ${taskList.id}: slug duplicates task list ${existing} in the same scope: ${taskList.slug}`);
|
|
3636
|
+
} else {
|
|
3637
|
+
scopedSlugs.set(taskList.slug, taskList.id);
|
|
3638
|
+
taskListSlugs.set(scope, scopedSlugs);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
return errors;
|
|
3642
|
+
}
|
|
3643
|
+
function validateSnapshotRoutingDestinationConflicts(projects, taskLists, existingProjects, existingTaskLists) {
|
|
3644
|
+
const errors = [];
|
|
3645
|
+
for (const project of projects) {
|
|
3646
|
+
const current = existingProjects.find((candidate) => candidate.id === project.id);
|
|
3647
|
+
if (current?.task_list_id === project.task_list_id)
|
|
3648
|
+
continue;
|
|
3649
|
+
const conflict = existingProjects.find((candidate) => candidate.id !== project.id && candidate.task_list_id === project.task_list_id);
|
|
3650
|
+
if (conflict) {
|
|
3651
|
+
errors.push(`project ${project.id}: task_list_id conflicts with existing project ${conflict.id}: ${String(project.task_list_id)}`);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
for (const taskList of taskLists) {
|
|
3655
|
+
const projectId = taskList.project_id ?? null;
|
|
3656
|
+
const current = existingTaskLists.find((candidate) => candidate.id === taskList.id);
|
|
3657
|
+
if ((current?.project_id ?? null) === projectId && current?.slug === taskList.slug)
|
|
3658
|
+
continue;
|
|
3659
|
+
const conflict = existingTaskLists.find((candidate) => candidate.id !== taskList.id && (candidate.project_id ?? null) === projectId && candidate.slug === taskList.slug);
|
|
3660
|
+
if (conflict) {
|
|
3661
|
+
errors.push(`task list ${taskList.id}: slug conflicts with existing task list ${conflict.id} in the same scope: ${String(taskList.slug)}`);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
return errors;
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
// src/db/slug-claims.ts
|
|
3668
|
+
function taskListSlugScopeKey(projectId) {
|
|
3669
|
+
return projectId ? `project:${projectId}` : "standalone:";
|
|
3670
|
+
}
|
|
3671
|
+
function claimCanonicalSlug(kind, scopeKey, slug, objectId, db) {
|
|
3672
|
+
db.run(`INSERT OR IGNORE INTO canonical_slug_claims (kind, scope_key, slug, object_id)
|
|
3673
|
+
VALUES (?, ?, ?, ?)`, [kind, scopeKey, slug, objectId]);
|
|
3674
|
+
const claim = db.query("SELECT object_id FROM canonical_slug_claims WHERE kind = ? AND scope_key = ? AND slug = ?").get(kind, scopeKey, slug);
|
|
3675
|
+
return claim?.object_id === objectId;
|
|
3676
|
+
}
|
|
3677
|
+
function releaseCanonicalSlugClaims(kind, objectId, db) {
|
|
3678
|
+
db.run("DELETE FROM canonical_slug_claims WHERE kind = ? AND object_id = ?", [kind, objectId]);
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3499
3681
|
// src/db/projects.ts
|
|
3500
3682
|
function slugify(name) {
|
|
3501
|
-
return name
|
|
3683
|
+
return normalizeSlug(name);
|
|
3502
3684
|
}
|
|
3503
3685
|
function generatePrefix(name, db) {
|
|
3504
3686
|
const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
|
|
@@ -3522,14 +3704,23 @@ function generatePrefix(name, db) {
|
|
|
3522
3704
|
}
|
|
3523
3705
|
function createProject(input, db) {
|
|
3524
3706
|
const d = db || getDatabase();
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3707
|
+
return d.transaction(() => {
|
|
3708
|
+
const id = uuid();
|
|
3709
|
+
const timestamp = now();
|
|
3710
|
+
const derivedSlug = slugify(input.name);
|
|
3711
|
+
const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugify(input.task_list_id);
|
|
3712
|
+
if (!derivedSlug || !taskListId)
|
|
3713
|
+
throw new Error("Project name and task-list slug must be non-empty");
|
|
3714
|
+
const slugConflict = d.query("SELECT id FROM projects WHERE task_list_id = ? LIMIT 1").get(taskListId);
|
|
3715
|
+
if (slugConflict || !claimCanonicalSlug("project", "global", taskListId, id, d)) {
|
|
3716
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${taskListId}" already exists`);
|
|
3717
|
+
}
|
|
3718
|
+
const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
|
|
3719
|
+
const machineId = currentStorageMachineId(d);
|
|
3720
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
|
|
3721
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp, machineId]);
|
|
3722
|
+
return getProject(id, d);
|
|
3723
|
+
})();
|
|
3533
3724
|
}
|
|
3534
3725
|
function getProject(id, db) {
|
|
3535
3726
|
const d = db || getDatabase();
|
|
@@ -3557,6 +3748,9 @@ function updateProject(id, input, db) {
|
|
|
3557
3748
|
const project = getProject(id, d);
|
|
3558
3749
|
if (!project)
|
|
3559
3750
|
throw new ProjectNotFoundError(id);
|
|
3751
|
+
if ("task_list_id" in input) {
|
|
3752
|
+
throw new Error("task_list_id cannot be changed by updateProject; use renameProject for an atomic canonical rename");
|
|
3753
|
+
}
|
|
3560
3754
|
const sets = ["updated_at = ?"];
|
|
3561
3755
|
const params = [now()];
|
|
3562
3756
|
if (input.name !== undefined) {
|
|
@@ -3567,10 +3761,6 @@ function updateProject(id, input, db) {
|
|
|
3567
3761
|
sets.push("description = ?");
|
|
3568
3762
|
params.push(input.description);
|
|
3569
3763
|
}
|
|
3570
|
-
if (input.task_list_id !== undefined) {
|
|
3571
|
-
sets.push("task_list_id = ?");
|
|
3572
|
-
params.push(input.task_list_id);
|
|
3573
|
-
}
|
|
3574
3764
|
if (input.path !== undefined) {
|
|
3575
3765
|
sets.push("path = ?");
|
|
3576
3766
|
params.push(input.path);
|
|
@@ -3581,29 +3771,41 @@ function updateProject(id, input, db) {
|
|
|
3581
3771
|
}
|
|
3582
3772
|
function renameProject(id, input, db) {
|
|
3583
3773
|
const d = db || getDatabase();
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3774
|
+
return d.transaction(() => {
|
|
3775
|
+
const project = getProject(id, d);
|
|
3776
|
+
if (!project)
|
|
3777
|
+
throw new ProjectNotFoundError(id);
|
|
3778
|
+
let taskListsUpdated = 0;
|
|
3779
|
+
const ts = now();
|
|
3780
|
+
if (input.new_slug !== undefined) {
|
|
3781
|
+
const normalised = normalizeSlug(input.new_slug);
|
|
3782
|
+
if (!normalised)
|
|
3783
|
+
throw new Error("Invalid slug \u2014 must be non-empty kebab-case");
|
|
3784
|
+
const oldSlug = project.task_list_id;
|
|
3785
|
+
if (normalised !== oldSlug) {
|
|
3786
|
+
const conflict = d.query("SELECT id FROM projects WHERE task_list_id = ? AND id != ?").get(normalised, id);
|
|
3787
|
+
if (conflict) {
|
|
3788
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Slug "${normalised}" is already used by another project`);
|
|
3789
|
+
}
|
|
3790
|
+
const taskListConflict = d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND slug != COALESCE(?, '') LIMIT 1").get(id, normalised, oldSlug);
|
|
3791
|
+
if (taskListConflict) {
|
|
3792
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task-list slug "${normalised}" is already used in project "${project.name}"`);
|
|
3793
|
+
}
|
|
3794
|
+
releaseCanonicalSlugClaims("project", id, d);
|
|
3795
|
+
if (!claimCanonicalSlug("project", "global", normalised, id, d)) {
|
|
3796
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Slug "${normalised}" is already used by another project`);
|
|
3797
|
+
}
|
|
3798
|
+
d.run("UPDATE projects SET task_list_id = ?, updated_at = ? WHERE id = ?", [normalised, ts, id]);
|
|
3799
|
+
}
|
|
3800
|
+
if (oldSlug && (normalised !== oldSlug || input.name !== undefined && input.name !== project.name)) {
|
|
3801
|
+
taskListsUpdated = d.query("UPDATE task_lists SET slug = ?, name = COALESCE(?, name), updated_at = ? WHERE project_id = ? AND slug = ? RETURNING id").all(normalised, input.name ?? null, ts, id, oldSlug).length;
|
|
3802
|
+
}
|
|
3601
3803
|
}
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3804
|
+
if (input.name !== undefined && input.name !== project.name) {
|
|
3805
|
+
d.run("UPDATE projects SET name = ?, updated_at = ? WHERE id = ?", [input.name, ts, id]);
|
|
3806
|
+
}
|
|
3807
|
+
return { project: getProject(id, d), task_lists_updated: taskListsUpdated };
|
|
3808
|
+
})();
|
|
3607
3809
|
}
|
|
3608
3810
|
function deleteProject(id, db) {
|
|
3609
3811
|
const d = db || getDatabase();
|
|
@@ -3615,8 +3817,10 @@ function deleteProject(id, db) {
|
|
|
3615
3817
|
object_id: id,
|
|
3616
3818
|
payload: project
|
|
3617
3819
|
}, d);
|
|
3618
|
-
|
|
3619
|
-
|
|
3820
|
+
return d.transaction(() => {
|
|
3821
|
+
releaseCanonicalSlugClaims("project", id, d);
|
|
3822
|
+
return d.run("DELETE FROM projects WHERE id = ?", [id]).changes > 0;
|
|
3823
|
+
})();
|
|
3620
3824
|
}
|
|
3621
3825
|
function rowToSource(row) {
|
|
3622
3826
|
return {
|
|
@@ -5175,19 +5379,22 @@ function rowToTaskList(row) {
|
|
|
5175
5379
|
}
|
|
5176
5380
|
function createTaskList(input, db) {
|
|
5177
5381
|
const d = db || getDatabase();
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5382
|
+
return d.transaction(() => {
|
|
5383
|
+
const id = uuid();
|
|
5384
|
+
const timestamp = now();
|
|
5385
|
+
const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
|
|
5386
|
+
if (!slug)
|
|
5387
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
5388
|
+
const machineId = currentStorageMachineId(d);
|
|
5389
|
+
const scopeKey = taskListSlugScopeKey(input.project_id);
|
|
5390
|
+
const existing = input.project_id ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(input.project_id, slug) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
5391
|
+
if (existing || !claimCanonicalSlug("task_list", scopeKey, slug, id, d)) {
|
|
5392
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
|
|
5393
|
+
}
|
|
5394
|
+
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at, machine_id)
|
|
5395
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp, machineId]);
|
|
5396
|
+
return getTaskList(id, d);
|
|
5397
|
+
})();
|
|
5191
5398
|
}
|
|
5192
5399
|
function getTaskList(id, db) {
|
|
5193
5400
|
const d = db || getDatabase();
|
|
@@ -5213,26 +5420,45 @@ function listTaskLists(projectId, db) {
|
|
|
5213
5420
|
}
|
|
5214
5421
|
function updateTaskList(id, input, db) {
|
|
5215
5422
|
const d = db || getDatabase();
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5423
|
+
return d.transaction(() => {
|
|
5424
|
+
const existing = getTaskList(id, d);
|
|
5425
|
+
if (!existing)
|
|
5426
|
+
throw new TaskListNotFoundError(id);
|
|
5427
|
+
const sets = ["updated_at = ?"];
|
|
5428
|
+
const params = [now()];
|
|
5429
|
+
if (input.slug !== undefined) {
|
|
5430
|
+
const slug = slugify(input.slug);
|
|
5431
|
+
if (!slug)
|
|
5432
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
5433
|
+
const duplicate = existing.project_id ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND id != ?").get(existing.project_id, slug, id) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ? AND id != ?").get(slug, id);
|
|
5434
|
+
if (duplicate) {
|
|
5435
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
|
|
5436
|
+
}
|
|
5437
|
+
if (slug !== existing.slug) {
|
|
5438
|
+
releaseCanonicalSlugClaims("task_list", id, d);
|
|
5439
|
+
if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(existing.project_id), slug, id, d)) {
|
|
5440
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
sets.push("slug = ?");
|
|
5444
|
+
params.push(slug);
|
|
5445
|
+
}
|
|
5446
|
+
if (input.name !== undefined) {
|
|
5447
|
+
sets.push("name = ?");
|
|
5448
|
+
params.push(input.name);
|
|
5449
|
+
}
|
|
5450
|
+
if (input.description !== undefined) {
|
|
5451
|
+
sets.push("description = ?");
|
|
5452
|
+
params.push(input.description);
|
|
5453
|
+
}
|
|
5454
|
+
if (input.metadata !== undefined) {
|
|
5455
|
+
sets.push("metadata = ?");
|
|
5456
|
+
params.push(JSON.stringify(input.metadata));
|
|
5457
|
+
}
|
|
5458
|
+
params.push(id);
|
|
5459
|
+
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
5460
|
+
return getTaskList(id, d);
|
|
5461
|
+
})();
|
|
5236
5462
|
}
|
|
5237
5463
|
function deleteTaskList(id, db) {
|
|
5238
5464
|
const d = db || getDatabase();
|
|
@@ -5244,7 +5470,10 @@ function deleteTaskList(id, db) {
|
|
|
5244
5470
|
object_id: id,
|
|
5245
5471
|
payload: list
|
|
5246
5472
|
}, d);
|
|
5247
|
-
return d.
|
|
5473
|
+
return d.transaction(() => {
|
|
5474
|
+
releaseCanonicalSlugClaims("task_list", id, d);
|
|
5475
|
+
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
5476
|
+
})();
|
|
5248
5477
|
}
|
|
5249
5478
|
function ensureTaskList(name, slug, projectId, db) {
|
|
5250
5479
|
const d = db || getDatabase();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"packageName": "@hasna/todos",
|
|
3
|
-
"packageVersion": "0.11.
|
|
3
|
+
"packageVersion": "0.11.89",
|
|
4
4
|
"repository": "https://github.com/hasna/todos.git",
|
|
5
|
-
"gitCommit": "
|
|
6
|
-
"generatedAt": "2026-07-
|
|
5
|
+
"gitCommit": "f4e825829729657f285e1f75b8981697dede1c4a",
|
|
6
|
+
"generatedAt": "2026-07-15T12:01:08.998Z"
|
|
7
7
|
}
|
package/dist/sdk/index.js
CHANGED
|
@@ -705,6 +705,27 @@ class TodosV1Client {
|
|
|
705
705
|
init
|
|
706
706
|
});
|
|
707
707
|
}
|
|
708
|
+
async deleteProject(id, init) {
|
|
709
|
+
return this.request("DELETE", `/v1/projects/${encodeURIComponent(String(id))}`, {
|
|
710
|
+
body: undefined,
|
|
711
|
+
query: undefined,
|
|
712
|
+
init
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
async updateProject(id, body, init) {
|
|
716
|
+
return this.request("PATCH", `/v1/projects/${encodeURIComponent(String(id))}`, {
|
|
717
|
+
body,
|
|
718
|
+
query: undefined,
|
|
719
|
+
init
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
async renameProject(id, body, init) {
|
|
723
|
+
return this.request("POST", `/v1/projects/${encodeURIComponent(String(id))}/rename`, {
|
|
724
|
+
body,
|
|
725
|
+
query: undefined,
|
|
726
|
+
init
|
|
727
|
+
});
|
|
728
|
+
}
|
|
708
729
|
async getStats(init) {
|
|
709
730
|
return this.request("GET", `/v1/stats`, {
|
|
710
731
|
body: undefined,
|
|
@@ -712,6 +733,41 @@ class TodosV1Client {
|
|
|
712
733
|
init
|
|
713
734
|
});
|
|
714
735
|
}
|
|
736
|
+
async listTaskLists(query, init) {
|
|
737
|
+
return this.request("GET", `/v1/task-lists`, {
|
|
738
|
+
body: undefined,
|
|
739
|
+
query,
|
|
740
|
+
init
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
async createTaskList(body, init) {
|
|
744
|
+
return this.request("POST", `/v1/task-lists`, {
|
|
745
|
+
body,
|
|
746
|
+
query: undefined,
|
|
747
|
+
init
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async getTaskList(id, init) {
|
|
751
|
+
return this.request("GET", `/v1/task-lists/${encodeURIComponent(String(id))}`, {
|
|
752
|
+
body: undefined,
|
|
753
|
+
query: undefined,
|
|
754
|
+
init
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
async deleteTaskList(id, init) {
|
|
758
|
+
return this.request("DELETE", `/v1/task-lists/${encodeURIComponent(String(id))}`, {
|
|
759
|
+
body: undefined,
|
|
760
|
+
query: undefined,
|
|
761
|
+
init
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
async updateTaskList(id, body, init) {
|
|
765
|
+
return this.request("PATCH", `/v1/task-lists/${encodeURIComponent(String(id))}`, {
|
|
766
|
+
body,
|
|
767
|
+
query: undefined,
|
|
768
|
+
init
|
|
769
|
+
});
|
|
770
|
+
}
|
|
715
771
|
async listTasks(query, init) {
|
|
716
772
|
return this.request("GET", `/v1/tasks`, {
|
|
717
773
|
body: undefined,
|
|
@@ -17,6 +17,19 @@ export interface Project {
|
|
|
17
17
|
"name"?: string;
|
|
18
18
|
"path"?: string;
|
|
19
19
|
"description"?: string | null;
|
|
20
|
+
"task_list_id"?: string | null;
|
|
21
|
+
"task_prefix"?: string | null;
|
|
22
|
+
"task_counter"?: number;
|
|
23
|
+
"created_at"?: string;
|
|
24
|
+
"updated_at"?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface TaskList {
|
|
27
|
+
"id"?: string;
|
|
28
|
+
"project_id"?: string | null;
|
|
29
|
+
"slug"?: string;
|
|
30
|
+
"name"?: string;
|
|
31
|
+
"description"?: string | null;
|
|
32
|
+
"metadata"?: Record<string, unknown>;
|
|
20
33
|
"created_at"?: string;
|
|
21
34
|
"updated_at"?: string;
|
|
22
35
|
}
|
|
@@ -52,8 +65,36 @@ export interface CreateProjectInput {
|
|
|
52
65
|
"name": string;
|
|
53
66
|
"path": string;
|
|
54
67
|
"description"?: string;
|
|
68
|
+
"task_list_id"?: string;
|
|
55
69
|
"task_prefix"?: string;
|
|
56
70
|
}
|
|
71
|
+
export interface UpdateProjectInput {
|
|
72
|
+
"name"?: string;
|
|
73
|
+
"path"?: string;
|
|
74
|
+
"description"?: string | null;
|
|
75
|
+
}
|
|
76
|
+
export interface RenameProjectInput {
|
|
77
|
+
"new_slug": string;
|
|
78
|
+
"name"?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface ErrorResponse {
|
|
81
|
+
"error": string;
|
|
82
|
+
"code"?: string;
|
|
83
|
+
"conflict"?: boolean;
|
|
84
|
+
}
|
|
85
|
+
export interface CreateTaskListInput {
|
|
86
|
+
"name": string;
|
|
87
|
+
"slug"?: string;
|
|
88
|
+
"project_id"?: string;
|
|
89
|
+
"description"?: string;
|
|
90
|
+
"metadata"?: Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
export interface UpdateTaskListInput {
|
|
93
|
+
"slug"?: string;
|
|
94
|
+
"name"?: string;
|
|
95
|
+
"description"?: string;
|
|
96
|
+
"metadata"?: Record<string, unknown>;
|
|
97
|
+
}
|
|
57
98
|
export interface CreateTaskCommentInput {
|
|
58
99
|
"content": string;
|
|
59
100
|
"agent_id"?: string;
|
|
@@ -119,11 +160,49 @@ export declare class TodosV1Client {
|
|
|
119
160
|
getProject(id: string, init?: RequestInit): Promise<{
|
|
120
161
|
"project"?: Project;
|
|
121
162
|
}>;
|
|
163
|
+
/** Delete a project */
|
|
164
|
+
deleteProject(id: string, init?: RequestInit): Promise<{
|
|
165
|
+
"deleted"?: boolean;
|
|
166
|
+
"id"?: string;
|
|
167
|
+
}>;
|
|
168
|
+
/** Update a project */
|
|
169
|
+
updateProject(id: string, body: UpdateProjectInput, init?: RequestInit): Promise<{
|
|
170
|
+
"project"?: Project;
|
|
171
|
+
}>;
|
|
172
|
+
/** Atomically rename a project and its canonical task list */
|
|
173
|
+
renameProject(id: string, body: RenameProjectInput, init?: RequestInit): Promise<{
|
|
174
|
+
"project"?: Project;
|
|
175
|
+
"task_lists_updated"?: number;
|
|
176
|
+
}>;
|
|
122
177
|
/** Aggregate counts */
|
|
123
178
|
getStats(init?: RequestInit): Promise<{
|
|
124
179
|
"tasks"?: number;
|
|
125
180
|
"projects"?: number;
|
|
126
181
|
}>;
|
|
182
|
+
/** List task lists */
|
|
183
|
+
listTaskLists(query?: {
|
|
184
|
+
"project_id"?: string;
|
|
185
|
+
}, init?: RequestInit): Promise<{
|
|
186
|
+
"task_lists"?: Array<TaskList>;
|
|
187
|
+
"count"?: number;
|
|
188
|
+
}>;
|
|
189
|
+
/** Create a task list */
|
|
190
|
+
createTaskList(body: CreateTaskListInput, init?: RequestInit): Promise<{
|
|
191
|
+
"task_list"?: TaskList;
|
|
192
|
+
}>;
|
|
193
|
+
/** Get a task list by id */
|
|
194
|
+
getTaskList(id: string, init?: RequestInit): Promise<{
|
|
195
|
+
"task_list"?: TaskList;
|
|
196
|
+
}>;
|
|
197
|
+
/** Delete a task list */
|
|
198
|
+
deleteTaskList(id: string, init?: RequestInit): Promise<{
|
|
199
|
+
"deleted"?: boolean;
|
|
200
|
+
"id"?: string;
|
|
201
|
+
}>;
|
|
202
|
+
/** Update a task list */
|
|
203
|
+
updateTaskList(id: string, body: UpdateTaskListInput, init?: RequestInit): Promise<{
|
|
204
|
+
"task_list"?: TaskList;
|
|
205
|
+
}>;
|
|
127
206
|
/** List tasks */
|
|
128
207
|
listTasks(query?: {
|
|
129
208
|
"status"?: string;
|