@hasna/todos 0.15.17 → 0.15.19
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 +7 -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 +478 -88
- package/dist/contracts.js +57 -2
- package/dist/db/plans.d.ts +4 -0
- package/dist/db/plans.d.ts.map +1 -1
- package/dist/index.js +122 -8
- package/dist/mcp/index.js +268 -21
- package/dist/mcp.js +1 -1
- package/dist/project-registration.js +113 -8
- package/dist/registry.js +57 -2
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/v1.generated.d.ts +13 -1
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +268 -21
- package/dist/server/openapi.d.ts +50 -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 +10 -0
- 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/shadow.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +121 -7
- package/dist/task-manifest.js +14 -1
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -47,7 +47,7 @@ function isBlockingDependencyStatus(status) {
|
|
|
47
47
|
function isTerminalStatus(status) {
|
|
48
48
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
49
49
|
}
|
|
50
|
-
var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
50
|
+
var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
51
51
|
var init_types = __esm(() => {
|
|
52
52
|
TASK_STATUSES = [
|
|
53
53
|
"pending",
|
|
@@ -134,6 +134,19 @@ var init_types = __esm(() => {
|
|
|
134
134
|
this.name = "ResourceConflictError";
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
|
+
PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
|
|
138
|
+
planId;
|
|
139
|
+
expectedUpdatedAt;
|
|
140
|
+
currentUpdatedAt;
|
|
141
|
+
static code = "PLAN_REVISION_CONFLICT";
|
|
142
|
+
constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
|
|
143
|
+
super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
|
|
144
|
+
this.planId = planId;
|
|
145
|
+
this.expectedUpdatedAt = expectedUpdatedAt;
|
|
146
|
+
this.currentUpdatedAt = currentUpdatedAt;
|
|
147
|
+
this.name = "PlanRevisionConflictError";
|
|
148
|
+
}
|
|
149
|
+
};
|
|
137
150
|
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
138
151
|
planId;
|
|
139
152
|
static code = "PLAN_NOT_FOUND";
|
|
@@ -15309,6 +15322,48 @@ function updatePlan(id, input, db) {
|
|
|
15309
15322
|
return updatePlanStored(id, input, d);
|
|
15310
15323
|
})();
|
|
15311
15324
|
}
|
|
15325
|
+
function nextPlanCompletionTimestamp(expectedUpdatedAt) {
|
|
15326
|
+
const expected = Date.parse(expectedUpdatedAt);
|
|
15327
|
+
const minimum = Number.isNaN(expected) ? Date.now() : expected + 2;
|
|
15328
|
+
return new Date(Math.max(Date.now(), minimum)).toISOString();
|
|
15329
|
+
}
|
|
15330
|
+
function completePlanAtRevision(id, expectedUpdatedAt, db) {
|
|
15331
|
+
const d = db || getDatabase();
|
|
15332
|
+
return d.transaction(() => {
|
|
15333
|
+
guardPlanRowsSqlite([id], d);
|
|
15334
|
+
const plan = getPlan(id, d);
|
|
15335
|
+
if (!plan)
|
|
15336
|
+
throw new PlanNotFoundError(id);
|
|
15337
|
+
if (plan.updated_at !== expectedUpdatedAt) {
|
|
15338
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, plan.updated_at);
|
|
15339
|
+
}
|
|
15340
|
+
if (plan.status === "completed")
|
|
15341
|
+
return { plan, applied: false };
|
|
15342
|
+
const updatedAt = nextPlanCompletionTimestamp(expectedUpdatedAt);
|
|
15343
|
+
const result = d.run(`UPDATE plans
|
|
15344
|
+
SET status = 'completed', updated_at = ?
|
|
15345
|
+
WHERE id = ? AND updated_at = ? AND status <> 'completed'`, [updatedAt, id, expectedUpdatedAt]);
|
|
15346
|
+
if (result.changes !== 1) {
|
|
15347
|
+
const current = getPlan(id, d);
|
|
15348
|
+
if (!current)
|
|
15349
|
+
throw new PlanNotFoundError(id);
|
|
15350
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
15351
|
+
}
|
|
15352
|
+
const completed = getPlan(id, d);
|
|
15353
|
+
emitLocalEventHooksQuiet({
|
|
15354
|
+
type: "plan.updated",
|
|
15355
|
+
payload: {
|
|
15356
|
+
id,
|
|
15357
|
+
old_status: plan.status,
|
|
15358
|
+
new_status: completed.status,
|
|
15359
|
+
name: completed.name,
|
|
15360
|
+
project_id: completed.project_id
|
|
15361
|
+
},
|
|
15362
|
+
databasePath: databasePathFromDatabase(d)
|
|
15363
|
+
});
|
|
15364
|
+
return { plan: completed, applied: true };
|
|
15365
|
+
})();
|
|
15366
|
+
}
|
|
15312
15367
|
function deletePlan(id, db) {
|
|
15313
15368
|
const d = db || getDatabase();
|
|
15314
15369
|
const plan = getPlan(id, d);
|
|
@@ -21322,7 +21377,7 @@ function resolveTaskListFromCandidates(lists, input) {
|
|
|
21322
21377
|
];
|
|
21323
21378
|
for (const matches of matchGroups) {
|
|
21324
21379
|
if (matches.length === 1)
|
|
21325
|
-
return matches[0]
|
|
21380
|
+
return matches[0];
|
|
21326
21381
|
if (matches.length > 1) {
|
|
21327
21382
|
throw new Error(`Task list reference is ambiguous: "${input}"`);
|
|
21328
21383
|
}
|
|
@@ -21335,27 +21390,26 @@ async function legacyProjectTaskLists(client, projectId) {
|
|
|
21335
21390
|
return [];
|
|
21336
21391
|
return (await cloudListTaskLists(client)).filter((list) => list.project_id == null && list.slug === project.task_list_id);
|
|
21337
21392
|
}
|
|
21338
|
-
async function
|
|
21393
|
+
async function cloudResolveTaskList(client, ref, projectId) {
|
|
21339
21394
|
const input = ref.trim();
|
|
21340
21395
|
const normalizedIdRef = input.toLowerCase();
|
|
21341
|
-
if (UUID_RE.test(input)
|
|
21342
|
-
return normalizedIdRef;
|
|
21343
|
-
if (UUID_RE.test(input) && projectId) {
|
|
21396
|
+
if (UUID_RE.test(input)) {
|
|
21344
21397
|
const direct = await cloudGetTaskList(client, normalizedIdRef);
|
|
21345
21398
|
if (direct?.id?.toLowerCase() === normalizedIdRef) {
|
|
21346
|
-
if (direct.project_id === projectId)
|
|
21347
|
-
return direct
|
|
21399
|
+
if (!projectId || direct.project_id === projectId)
|
|
21400
|
+
return direct;
|
|
21348
21401
|
if (direct.project_id == null) {
|
|
21349
21402
|
const project = await cloudGetProjectById(client, projectId);
|
|
21350
21403
|
if (project?.task_list_id === direct.slug)
|
|
21351
|
-
return direct
|
|
21404
|
+
return direct;
|
|
21352
21405
|
}
|
|
21353
21406
|
throw new Error(`Task list not found: "${input}"`);
|
|
21354
21407
|
}
|
|
21355
21408
|
}
|
|
21356
21409
|
const scopedMatch = resolveTaskListFromCandidates(await cloudListTaskLists(client, projectId), input);
|
|
21357
|
-
if (scopedMatch)
|
|
21410
|
+
if (scopedMatch && (!projectId || scopedMatch.project_id === projectId)) {
|
|
21358
21411
|
return scopedMatch;
|
|
21412
|
+
}
|
|
21359
21413
|
if (projectId) {
|
|
21360
21414
|
const legacyMatch = resolveTaskListFromCandidates(await legacyProjectTaskLists(client, projectId), input);
|
|
21361
21415
|
if (legacyMatch)
|
|
@@ -21363,7 +21417,13 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
21363
21417
|
}
|
|
21364
21418
|
throw new Error(`Task list not found: "${input}"`);
|
|
21365
21419
|
}
|
|
21366
|
-
|
|
21420
|
+
async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
21421
|
+
const input = ref.trim();
|
|
21422
|
+
if (UUID_RE.test(input) && !projectId)
|
|
21423
|
+
return input.toLowerCase();
|
|
21424
|
+
return (await cloudResolveTaskList(client, ref, projectId)).id;
|
|
21425
|
+
}
|
|
21426
|
+
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, retryCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache;
|
|
21367
21427
|
var init_cloud_router = __esm(() => {
|
|
21368
21428
|
init_types();
|
|
21369
21429
|
init_redaction();
|
|
@@ -21380,6 +21440,7 @@ var init_cloud_router = __esm(() => {
|
|
|
21380
21440
|
hybrid: "http"
|
|
21381
21441
|
};
|
|
21382
21442
|
completionCapabilityCache = new Map;
|
|
21443
|
+
retryCapabilityCache = new Map;
|
|
21383
21444
|
gitRefCapabilityCache = new Map;
|
|
21384
21445
|
SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
|
|
21385
21446
|
PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
@@ -35476,7 +35537,7 @@ var package_default;
|
|
|
35476
35537
|
var init_package = __esm(() => {
|
|
35477
35538
|
package_default = {
|
|
35478
35539
|
name: "@hasna/todos",
|
|
35479
|
-
version: "0.15.
|
|
35540
|
+
version: "0.15.19",
|
|
35480
35541
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
35481
35542
|
type: "module",
|
|
35482
35543
|
main: "dist/index.js",
|
|
@@ -45072,7 +45133,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45072
45133
|
}
|
|
45073
45134
|
if (result.errors.length > 0)
|
|
45074
45135
|
return result;
|
|
45075
|
-
const applyRows = (objectType2, table, columns, rows, updateClockColumn, afterUpsert) => {
|
|
45136
|
+
const applyRows = (objectType2, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
|
|
45076
45137
|
for (const row of rows) {
|
|
45077
45138
|
try {
|
|
45078
45139
|
const record = asRecord2(row);
|
|
@@ -45081,7 +45142,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45081
45142
|
result.skipped += 1;
|
|
45082
45143
|
continue;
|
|
45083
45144
|
}
|
|
45084
|
-
const state = upsertById(d, table, columns, record, updateClockColumn);
|
|
45145
|
+
const state = upsertById(d, table, columns, record, updateClockColumn, acceptEqualClock);
|
|
45085
45146
|
if (state === "inserted")
|
|
45086
45147
|
result.inserted += 1;
|
|
45087
45148
|
else if (state === "updated")
|
|
@@ -45098,10 +45159,10 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45098
45159
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
45099
45160
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
45100
45161
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
45101
|
-
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
45162
|
+
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at", false);
|
|
45102
45163
|
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
45103
45164
|
applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
|
|
45104
|
-
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
|
|
45165
|
+
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", true, (row, changed) => {
|
|
45105
45166
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
45106
45167
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
45107
45168
|
}
|
|
@@ -45110,7 +45171,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45110
45171
|
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
45111
45172
|
return result;
|
|
45112
45173
|
}
|
|
45113
|
-
function upsertById(db, table, columns, row, updateClockColumn) {
|
|
45174
|
+
function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
|
|
45114
45175
|
const id = row["id"];
|
|
45115
45176
|
if (typeof id !== "string" || !id)
|
|
45116
45177
|
throw new Error(`${table} row is missing id`);
|
|
@@ -45122,7 +45183,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
|
|
|
45122
45183
|
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
45123
45184
|
const updateColumns = presentColumns.filter((column) => column !== "id");
|
|
45124
45185
|
const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
|
|
45125
|
-
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
|
|
45186
|
+
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} ${acceptEqualClock ? "<=" : "<"} excluded.${updateClockColumn}` : "";
|
|
45126
45187
|
const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders2})
|
|
45127
45188
|
ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders2})`;
|
|
45128
45189
|
const changes = db.run(sql, values).changes;
|
|
@@ -45541,6 +45602,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
45541
45602
|
get: (id) => getPlan(id, database()),
|
|
45542
45603
|
list: (projectId) => listPlans(projectId, database()),
|
|
45543
45604
|
update: (id, input) => updatePlan(id, input, database()),
|
|
45605
|
+
completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
|
|
45544
45606
|
delete: (id) => deletePlan(id, database())
|
|
45545
45607
|
},
|
|
45546
45608
|
planProjectLinks: {
|
|
@@ -46501,6 +46563,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
46501
46563
|
get: (id) => store.get("plans", id),
|
|
46502
46564
|
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
46503
46565
|
update: (id, input) => updatePlan2(id, input, store),
|
|
46566
|
+
completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
|
|
46504
46567
|
delete: (id, context) => store.deletePlan(id, context)
|
|
46505
46568
|
},
|
|
46506
46569
|
planProjectLinks: {
|
|
@@ -46988,6 +47051,54 @@ class PostgresJsonRecordStore {
|
|
|
46988
47051
|
throw new PlanNotFoundError(value.id);
|
|
46989
47052
|
return payloadRecord2(row.payload);
|
|
46990
47053
|
}
|
|
47054
|
+
async completePlanAtRevision(id, expectedUpdatedAt, context = {}) {
|
|
47055
|
+
await this.ensureSchema();
|
|
47056
|
+
const result = await this.options.client.query(`/* todos:complete-plan-revision-cas */ WITH next_clock AS (
|
|
47057
|
+
SELECT date_trunc(
|
|
47058
|
+
'milliseconds',
|
|
47059
|
+
GREATEST(clock_timestamp(), ($3::text)::timestamptz + interval '2 milliseconds')
|
|
47060
|
+
) AS completed_at
|
|
47061
|
+
), stored AS (
|
|
47062
|
+
UPDATE ${this.tableName} AS record SET
|
|
47063
|
+
payload = record.payload || jsonb_build_object(
|
|
47064
|
+
'status', 'completed',
|
|
47065
|
+
'updated_at', to_char(
|
|
47066
|
+
next_clock.completed_at AT TIME ZONE 'UTC',
|
|
47067
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
47068
|
+
)
|
|
47069
|
+
),
|
|
47070
|
+
updated_at = next_clock.completed_at,
|
|
47071
|
+
deleted_at = NULL,
|
|
47072
|
+
source_machine_id = COALESCE($4, record.source_machine_id),
|
|
47073
|
+
version = COALESCE(record.version, 0) + 1
|
|
47074
|
+
FROM next_clock
|
|
47075
|
+
WHERE record.service = $1
|
|
47076
|
+
AND record.object_type = 'plans'
|
|
47077
|
+
AND record.object_id = $2
|
|
47078
|
+
AND record.deleted_at IS NULL
|
|
47079
|
+
AND record.payload->>'updated_at' = $3::text
|
|
47080
|
+
AND record.payload->>'status' IS DISTINCT FROM 'completed'
|
|
47081
|
+
RETURNING record.payload
|
|
47082
|
+
)
|
|
47083
|
+
SELECT payload FROM stored`, [
|
|
47084
|
+
this.service,
|
|
47085
|
+
id,
|
|
47086
|
+
expectedUpdatedAt,
|
|
47087
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
47088
|
+
]);
|
|
47089
|
+
const payload = result.rows[0]?.payload;
|
|
47090
|
+
if (payload)
|
|
47091
|
+
return { plan: payloadRecord2(payload), applied: true };
|
|
47092
|
+
const current = await this.get("plans", id);
|
|
47093
|
+
if (!current)
|
|
47094
|
+
throw new PlanNotFoundError(id);
|
|
47095
|
+
if (current.updated_at !== expectedUpdatedAt) {
|
|
47096
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
47097
|
+
}
|
|
47098
|
+
if (current.status === "completed")
|
|
47099
|
+
return { plan: current, applied: false };
|
|
47100
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
47101
|
+
}
|
|
46991
47102
|
async createTemplateWithTasks(template, tasks, context = {}) {
|
|
46992
47103
|
await this.ensureSchema();
|
|
46993
47104
|
const records = [
|
|
@@ -55529,8 +55640,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55529
55640
|
"/v1/import": {
|
|
55530
55641
|
post: {
|
|
55531
55642
|
operationId: "importSnapshot",
|
|
55532
|
-
summary: "Bulk-ingest a
|
|
55533
|
-
description: "Upserts every record
|
|
55643
|
+
summary: "Bulk-ingest a snapshot or atomically complete one observed plan",
|
|
55644
|
+
description: "Upserts every snapshot record by primary key, or accepts exactly one planCompletions operation that changes only plan status under an expected_updated_at CAS. Snapshot records and planCompletions are mutually exclusive. Requires the todos:write scope.",
|
|
55534
55645
|
requestBody: {
|
|
55535
55646
|
required: true,
|
|
55536
55647
|
content: {
|
|
@@ -55549,7 +55660,22 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55549
55660
|
templates: { type: "array", items: { type: "object" } },
|
|
55550
55661
|
templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
|
|
55551
55662
|
auditHistory: { type: "array", items: { type: "object" } },
|
|
55552
|
-
tombstones: { type: "array", items: { type: "object" } }
|
|
55663
|
+
tombstones: { type: "array", items: { type: "object" } },
|
|
55664
|
+
planCompletions: {
|
|
55665
|
+
type: "array",
|
|
55666
|
+
minItems: 1,
|
|
55667
|
+
maxItems: 1,
|
|
55668
|
+
items: {
|
|
55669
|
+
type: "object",
|
|
55670
|
+
additionalProperties: false,
|
|
55671
|
+
required: ["id", "expected_updated_at", "status"],
|
|
55672
|
+
properties: {
|
|
55673
|
+
id: { type: "string" },
|
|
55674
|
+
expected_updated_at: { type: "string", format: "date-time" },
|
|
55675
|
+
status: { type: "string", enum: ["completed"] }
|
|
55676
|
+
}
|
|
55677
|
+
}
|
|
55678
|
+
}
|
|
55553
55679
|
}
|
|
55554
55680
|
}
|
|
55555
55681
|
}
|
|
@@ -55572,6 +55698,26 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55572
55698
|
skipped: { type: "number" },
|
|
55573
55699
|
errors: { type: "array", items: { type: "string" } }
|
|
55574
55700
|
}
|
|
55701
|
+
},
|
|
55702
|
+
planCompletions: {
|
|
55703
|
+
type: "array",
|
|
55704
|
+
items: {
|
|
55705
|
+
type: "object",
|
|
55706
|
+
required: [
|
|
55707
|
+
"id",
|
|
55708
|
+
"status",
|
|
55709
|
+
"expected_updated_at",
|
|
55710
|
+
"result_updated_at",
|
|
55711
|
+
"applied"
|
|
55712
|
+
],
|
|
55713
|
+
properties: {
|
|
55714
|
+
id: { type: "string" },
|
|
55715
|
+
status: { type: "string", enum: ["completed"] },
|
|
55716
|
+
expected_updated_at: { type: "string", format: "date-time" },
|
|
55717
|
+
result_updated_at: { type: "string", format: "date-time" },
|
|
55718
|
+
applied: { type: "boolean" }
|
|
55719
|
+
}
|
|
55720
|
+
}
|
|
55575
55721
|
}
|
|
55576
55722
|
}
|
|
55577
55723
|
}
|
|
@@ -56768,6 +56914,65 @@ function normalizeImportSnapshot(raw) {
|
|
|
56768
56914
|
function countSnapshotRecords(s) {
|
|
56769
56915
|
return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.templateTasks.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
|
|
56770
56916
|
}
|
|
56917
|
+
function validatePlanCompletionImports(raw) {
|
|
56918
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
56919
|
+
return { present: false, operations: [] };
|
|
56920
|
+
}
|
|
56921
|
+
const body2 = raw;
|
|
56922
|
+
if (!Object.prototype.hasOwnProperty.call(body2, "planCompletions")) {
|
|
56923
|
+
return { present: false, operations: [] };
|
|
56924
|
+
}
|
|
56925
|
+
if (!Array.isArray(body2["planCompletions"]) || body2["planCompletions"].length !== 1) {
|
|
56926
|
+
return {
|
|
56927
|
+
present: true,
|
|
56928
|
+
operations: [],
|
|
56929
|
+
error: "planCompletions must contain exactly one completion operation"
|
|
56930
|
+
};
|
|
56931
|
+
}
|
|
56932
|
+
const operation = body2["planCompletions"][0];
|
|
56933
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
|
|
56934
|
+
return { present: true, operations: [], error: "plan completion must be an object" };
|
|
56935
|
+
}
|
|
56936
|
+
const record = operation;
|
|
56937
|
+
const allowed = new Set(["id", "expected_updated_at", "status"]);
|
|
56938
|
+
const unknown = Object.keys(record).find((key2) => !allowed.has(key2));
|
|
56939
|
+
if (unknown) {
|
|
56940
|
+
return { present: true, operations: [], error: `unknown plan completion field: ${unknown}` };
|
|
56941
|
+
}
|
|
56942
|
+
if (typeof record["id"] !== "string" || !record["id"].trim()) {
|
|
56943
|
+
return { present: true, operations: [], error: "plan completion id must be a non-empty string" };
|
|
56944
|
+
}
|
|
56945
|
+
if (record["status"] !== "completed") {
|
|
56946
|
+
return { present: true, operations: [], error: "plan completion status must be completed" };
|
|
56947
|
+
}
|
|
56948
|
+
const expectedUpdatedAt = typeof record["expected_updated_at"] === "string" ? record["expected_updated_at"] : "";
|
|
56949
|
+
const timestampMatch = RFC3339_DATE_TIME.exec(expectedUpdatedAt);
|
|
56950
|
+
const parsedTimestamp = Date.parse(expectedUpdatedAt);
|
|
56951
|
+
if (!timestampMatch || Number.isNaN(parsedTimestamp)) {
|
|
56952
|
+
return {
|
|
56953
|
+
present: true,
|
|
56954
|
+
operations: [],
|
|
56955
|
+
error: "plan completion expected_updated_at must be an RFC 3339 date-time with an explicit offset"
|
|
56956
|
+
};
|
|
56957
|
+
}
|
|
56958
|
+
const [, year, month, day] = timestampMatch;
|
|
56959
|
+
const calendarProbe = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
56960
|
+
if (calendarProbe.getUTCFullYear() !== Number(year) || calendarProbe.getUTCMonth() !== Number(month) - 1 || calendarProbe.getUTCDate() !== Number(day)) {
|
|
56961
|
+
return {
|
|
56962
|
+
present: true,
|
|
56963
|
+
operations: [],
|
|
56964
|
+
error: "plan completion expected_updated_at names a date that does not exist"
|
|
56965
|
+
};
|
|
56966
|
+
}
|
|
56967
|
+
return {
|
|
56968
|
+
present: true,
|
|
56969
|
+
operations: [{
|
|
56970
|
+
id: record["id"],
|
|
56971
|
+
expected_updated_at: expectedUpdatedAt,
|
|
56972
|
+
status: "completed"
|
|
56973
|
+
}]
|
|
56974
|
+
};
|
|
56975
|
+
}
|
|
56771
56976
|
async function handleV1Request(req, url, dependencies = {}) {
|
|
56772
56977
|
const path = url.pathname;
|
|
56773
56978
|
if (path !== "/v1" && !path.startsWith("/v1/"))
|
|
@@ -57704,6 +57909,36 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57704
57909
|
return error(400, "invalid JSON body");
|
|
57705
57910
|
const snapshot = normalizeImportSnapshot(raw);
|
|
57706
57911
|
const received = countSnapshotRecords(snapshot);
|
|
57912
|
+
const completionImports = validatePlanCompletionImports(raw);
|
|
57913
|
+
if (completionImports.present) {
|
|
57914
|
+
if (completionImports.error)
|
|
57915
|
+
return error(400, completionImports.error);
|
|
57916
|
+
if (received !== 0) {
|
|
57917
|
+
return error(400, "planCompletions cannot be combined with snapshot record arrays");
|
|
57918
|
+
}
|
|
57919
|
+
if (typeof store.plans.completeAtRevision !== "function") {
|
|
57920
|
+
return error(501, "atomic plan completion is not supported by this storage backend");
|
|
57921
|
+
}
|
|
57922
|
+
const operation = completionImports.operations[0];
|
|
57923
|
+
const completed = await store.plans.completeAtRevision(operation.id, operation.expected_updated_at, contextFromPrincipal(principal));
|
|
57924
|
+
return json5({
|
|
57925
|
+
result: {
|
|
57926
|
+
inserted: 0,
|
|
57927
|
+
updated: completed.applied ? 1 : 0,
|
|
57928
|
+
deleted: 0,
|
|
57929
|
+
skipped: completed.applied ? 0 : 1,
|
|
57930
|
+
errors: []
|
|
57931
|
+
},
|
|
57932
|
+
received: 1,
|
|
57933
|
+
planCompletions: [{
|
|
57934
|
+
id: operation.id,
|
|
57935
|
+
status: "completed",
|
|
57936
|
+
expected_updated_at: operation.expected_updated_at,
|
|
57937
|
+
result_updated_at: completed.plan.updated_at,
|
|
57938
|
+
applied: completed.applied
|
|
57939
|
+
}]
|
|
57940
|
+
});
|
|
57941
|
+
}
|
|
57707
57942
|
if (received === 0) {
|
|
57708
57943
|
return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
|
|
57709
57944
|
}
|
|
@@ -57730,6 +57965,18 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57730
57965
|
if (e instanceof TaskNotFoundError) {
|
|
57731
57966
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
57732
57967
|
}
|
|
57968
|
+
if (e instanceof PlanNotFoundError) {
|
|
57969
|
+
return error(404, e.message, { code: PlanNotFoundError.code });
|
|
57970
|
+
}
|
|
57971
|
+
if (e instanceof PlanRevisionConflictError) {
|
|
57972
|
+
return error(409, e.message, {
|
|
57973
|
+
code: PlanRevisionConflictError.code,
|
|
57974
|
+
conflict: true,
|
|
57975
|
+
plan_id: e.planId,
|
|
57976
|
+
expected_updated_at: e.expectedUpdatedAt,
|
|
57977
|
+
current_updated_at: e.currentUpdatedAt
|
|
57978
|
+
});
|
|
57979
|
+
}
|
|
57733
57980
|
if (e instanceof LockError)
|
|
57734
57981
|
return error(409, e.message, { code: LockError.code });
|
|
57735
57982
|
if (e instanceof TaskNotStartableError) {
|
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.19",
|
|
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",
|
|
@@ -117,7 +117,7 @@ function isBlockingDependencyStatus(status) {
|
|
|
117
117
|
function isTerminalStatus(status) {
|
|
118
118
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
119
119
|
}
|
|
120
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
120
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
121
121
|
var init_types = __esm(() => {
|
|
122
122
|
TASK_STATUSES = [
|
|
123
123
|
"pending",
|
|
@@ -205,6 +205,19 @@ var init_types = __esm(() => {
|
|
|
205
205
|
this.name = "ResourceConflictError";
|
|
206
206
|
}
|
|
207
207
|
};
|
|
208
|
+
PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
|
|
209
|
+
planId;
|
|
210
|
+
expectedUpdatedAt;
|
|
211
|
+
currentUpdatedAt;
|
|
212
|
+
static code = "PLAN_REVISION_CONFLICT";
|
|
213
|
+
constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
|
|
214
|
+
super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
|
|
215
|
+
this.planId = planId;
|
|
216
|
+
this.expectedUpdatedAt = expectedUpdatedAt;
|
|
217
|
+
this.currentUpdatedAt = currentUpdatedAt;
|
|
218
|
+
this.name = "PlanRevisionConflictError";
|
|
219
|
+
}
|
|
220
|
+
};
|
|
208
221
|
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
209
222
|
planId;
|
|
210
223
|
static code = "PLAN_NOT_FOUND";
|
|
@@ -10521,6 +10534,48 @@ function updatePlan2(id, input, db) {
|
|
|
10521
10534
|
return updatePlanStored(id, input, d);
|
|
10522
10535
|
})();
|
|
10523
10536
|
}
|
|
10537
|
+
function nextPlanCompletionTimestamp(expectedUpdatedAt) {
|
|
10538
|
+
const expected = Date.parse(expectedUpdatedAt);
|
|
10539
|
+
const minimum = Number.isNaN(expected) ? Date.now() : expected + 2;
|
|
10540
|
+
return new Date(Math.max(Date.now(), minimum)).toISOString();
|
|
10541
|
+
}
|
|
10542
|
+
function completePlanAtRevision(id, expectedUpdatedAt, db) {
|
|
10543
|
+
const d = db || getDatabase();
|
|
10544
|
+
return d.transaction(() => {
|
|
10545
|
+
guardPlanRowsSqlite([id], d);
|
|
10546
|
+
const plan = getPlan(id, d);
|
|
10547
|
+
if (!plan)
|
|
10548
|
+
throw new PlanNotFoundError(id);
|
|
10549
|
+
if (plan.updated_at !== expectedUpdatedAt) {
|
|
10550
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, plan.updated_at);
|
|
10551
|
+
}
|
|
10552
|
+
if (plan.status === "completed")
|
|
10553
|
+
return { plan, applied: false };
|
|
10554
|
+
const updatedAt = nextPlanCompletionTimestamp(expectedUpdatedAt);
|
|
10555
|
+
const result = d.run(`UPDATE plans
|
|
10556
|
+
SET status = 'completed', updated_at = ?
|
|
10557
|
+
WHERE id = ? AND updated_at = ? AND status <> 'completed'`, [updatedAt, id, expectedUpdatedAt]);
|
|
10558
|
+
if (result.changes !== 1) {
|
|
10559
|
+
const current = getPlan(id, d);
|
|
10560
|
+
if (!current)
|
|
10561
|
+
throw new PlanNotFoundError(id);
|
|
10562
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
10563
|
+
}
|
|
10564
|
+
const completed = getPlan(id, d);
|
|
10565
|
+
emitLocalEventHooksQuiet({
|
|
10566
|
+
type: "plan.updated",
|
|
10567
|
+
payload: {
|
|
10568
|
+
id,
|
|
10569
|
+
old_status: plan.status,
|
|
10570
|
+
new_status: completed.status,
|
|
10571
|
+
name: completed.name,
|
|
10572
|
+
project_id: completed.project_id
|
|
10573
|
+
},
|
|
10574
|
+
databasePath: databasePathFromDatabase(d)
|
|
10575
|
+
});
|
|
10576
|
+
return { plan: completed, applied: true };
|
|
10577
|
+
})();
|
|
10578
|
+
}
|
|
10524
10579
|
function deletePlan(id, db) {
|
|
10525
10580
|
const d = db || getDatabase();
|
|
10526
10581
|
const plan = getPlan(id, d);
|
|
@@ -12383,7 +12438,7 @@ import { createHash as createHash5 } from "crypto";
|
|
|
12383
12438
|
// package.json
|
|
12384
12439
|
var package_default = {
|
|
12385
12440
|
name: "@hasna/todos",
|
|
12386
|
-
version: "0.15.
|
|
12441
|
+
version: "0.15.19",
|
|
12387
12442
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12388
12443
|
type: "module",
|
|
12389
12444
|
main: "dist/index.js",
|
|
@@ -13288,6 +13343,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
13288
13343
|
get: (id) => store.get("plans", id),
|
|
13289
13344
|
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
13290
13345
|
update: (id, input) => updatePlan(id, input, store),
|
|
13346
|
+
completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
|
|
13291
13347
|
delete: (id, context) => store.deletePlan(id, context)
|
|
13292
13348
|
},
|
|
13293
13349
|
planProjectLinks: {
|
|
@@ -13775,6 +13831,54 @@ class PostgresJsonRecordStore {
|
|
|
13775
13831
|
throw new PlanNotFoundError(value.id);
|
|
13776
13832
|
return payloadRecord2(row.payload);
|
|
13777
13833
|
}
|
|
13834
|
+
async completePlanAtRevision(id, expectedUpdatedAt, context = {}) {
|
|
13835
|
+
await this.ensureSchema();
|
|
13836
|
+
const result = await this.options.client.query(`/* todos:complete-plan-revision-cas */ WITH next_clock AS (
|
|
13837
|
+
SELECT date_trunc(
|
|
13838
|
+
'milliseconds',
|
|
13839
|
+
GREATEST(clock_timestamp(), ($3::text)::timestamptz + interval '2 milliseconds')
|
|
13840
|
+
) AS completed_at
|
|
13841
|
+
), stored AS (
|
|
13842
|
+
UPDATE ${this.tableName} AS record SET
|
|
13843
|
+
payload = record.payload || jsonb_build_object(
|
|
13844
|
+
'status', 'completed',
|
|
13845
|
+
'updated_at', to_char(
|
|
13846
|
+
next_clock.completed_at AT TIME ZONE 'UTC',
|
|
13847
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
13848
|
+
)
|
|
13849
|
+
),
|
|
13850
|
+
updated_at = next_clock.completed_at,
|
|
13851
|
+
deleted_at = NULL,
|
|
13852
|
+
source_machine_id = COALESCE($4, record.source_machine_id),
|
|
13853
|
+
version = COALESCE(record.version, 0) + 1
|
|
13854
|
+
FROM next_clock
|
|
13855
|
+
WHERE record.service = $1
|
|
13856
|
+
AND record.object_type = 'plans'
|
|
13857
|
+
AND record.object_id = $2
|
|
13858
|
+
AND record.deleted_at IS NULL
|
|
13859
|
+
AND record.payload->>'updated_at' = $3::text
|
|
13860
|
+
AND record.payload->>'status' IS DISTINCT FROM 'completed'
|
|
13861
|
+
RETURNING record.payload
|
|
13862
|
+
)
|
|
13863
|
+
SELECT payload FROM stored`, [
|
|
13864
|
+
this.service,
|
|
13865
|
+
id,
|
|
13866
|
+
expectedUpdatedAt,
|
|
13867
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
13868
|
+
]);
|
|
13869
|
+
const payload = result.rows[0]?.payload;
|
|
13870
|
+
if (payload)
|
|
13871
|
+
return { plan: payloadRecord2(payload), applied: true };
|
|
13872
|
+
const current = await this.get("plans", id);
|
|
13873
|
+
if (!current)
|
|
13874
|
+
throw new PlanNotFoundError(id);
|
|
13875
|
+
if (current.updated_at !== expectedUpdatedAt) {
|
|
13876
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
13877
|
+
}
|
|
13878
|
+
if (current.status === "completed")
|
|
13879
|
+
return { plan: current, applied: false };
|
|
13880
|
+
throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
|
|
13881
|
+
}
|
|
13778
13882
|
async createTemplateWithTasks(template, tasks, context = {}) {
|
|
13779
13883
|
await this.ensureSchema();
|
|
13780
13884
|
const records = [
|
|
@@ -16879,7 +16983,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
16879
16983
|
}
|
|
16880
16984
|
if (result.errors.length > 0)
|
|
16881
16985
|
return result;
|
|
16882
|
-
const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
|
|
16986
|
+
const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
|
|
16883
16987
|
for (const row of rows) {
|
|
16884
16988
|
try {
|
|
16885
16989
|
const record = asRecord(row);
|
|
@@ -16888,7 +16992,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
16888
16992
|
result.skipped += 1;
|
|
16889
16993
|
continue;
|
|
16890
16994
|
}
|
|
16891
|
-
const state = upsertById(d, table, columns, record, updateClockColumn);
|
|
16995
|
+
const state = upsertById(d, table, columns, record, updateClockColumn, acceptEqualClock);
|
|
16892
16996
|
if (state === "inserted")
|
|
16893
16997
|
result.inserted += 1;
|
|
16894
16998
|
else if (state === "updated")
|
|
@@ -16905,10 +17009,10 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
16905
17009
|
applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
|
|
16906
17010
|
applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
16907
17011
|
applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
16908
|
-
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
17012
|
+
applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at", false);
|
|
16909
17013
|
applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
16910
17014
|
applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
|
|
16911
|
-
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
|
|
17015
|
+
applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", true, (row, changed) => {
|
|
16912
17016
|
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
16913
17017
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
16914
17018
|
}
|
|
@@ -16917,7 +17021,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
16917
17021
|
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
16918
17022
|
return result;
|
|
16919
17023
|
}
|
|
16920
|
-
function upsertById(db, table, columns, row, updateClockColumn) {
|
|
17024
|
+
function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
|
|
16921
17025
|
const id = row["id"];
|
|
16922
17026
|
if (typeof id !== "string" || !id)
|
|
16923
17027
|
throw new Error(`${table} row is missing id`);
|
|
@@ -16929,7 +17033,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
|
|
|
16929
17033
|
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
16930
17034
|
const updateColumns = presentColumns.filter((column) => column !== "id");
|
|
16931
17035
|
const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
|
|
16932
|
-
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
|
|
17036
|
+
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} ${acceptEqualClock ? "<=" : "<"} excluded.${updateClockColumn}` : "";
|
|
16933
17037
|
const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
|
|
16934
17038
|
ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
|
|
16935
17039
|
const changes = db.run(sql, values).changes;
|
|
@@ -17169,6 +17273,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
17169
17273
|
get: (id) => getPlan(id, database()),
|
|
17170
17274
|
list: (projectId) => listPlans(projectId, database()),
|
|
17171
17275
|
update: (id, input) => updatePlan2(id, input, database()),
|
|
17276
|
+
completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
|
|
17172
17277
|
delete: (id) => deletePlan(id, database())
|
|
17173
17278
|
},
|
|
17174
17279
|
planProjectLinks: {
|