@hasna/todos 0.15.47 → 0.15.49
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/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/index.js +296 -81
- package/dist/contracts.js +56 -5
- package/dist/db/task-crud.d.ts +6 -0
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +1 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +187 -70
- package/dist/lib/dedupe-projection.d.ts +48 -0
- package/dist/lib/dedupe-projection.d.ts.map +1 -0
- package/dist/lib/redaction.d.ts.map +1 -1
- package/dist/lib/task-dedupe.d.ts +5 -9
- package/dist/lib/task-dedupe.d.ts.map +1 -1
- package/dist/mcp/index.js +246 -80
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +3 -2
- package/dist/project-registration.js +128 -68
- package/dist/registry.js +56 -5
- package/dist/release-provenance.json +5 -5
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +248 -82
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-errors.d.ts +15 -0
- package/dist/storage/postgres-errors.d.ts.map +1 -0
- package/dist/storage/postgres-sync.d.ts +7 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/shadow-outbox.d.ts.map +1 -1
- package/dist/storage/shadow.d.ts.map +1 -1
- package/dist/storage.js +139 -66
- package/dist/task-manifest.js +118 -47
- package/dist/task-subtree-transfer.js +326 -44
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/query-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"query-commands.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/query-commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmPzC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,QAkwGrD"}
|
package/dist/cli/index.js
CHANGED
|
@@ -2123,7 +2123,7 @@ var package_default;
|
|
|
2123
2123
|
var init_package = __esm(() => {
|
|
2124
2124
|
package_default = {
|
|
2125
2125
|
name: "@hasna/todos",
|
|
2126
|
-
version: "0.15.
|
|
2126
|
+
version: "0.15.49",
|
|
2127
2127
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
2128
2128
|
type: "module",
|
|
2129
2129
|
main: "dist/index.js",
|
|
@@ -2187,6 +2187,7 @@ var init_package = __esm(() => {
|
|
|
2187
2187
|
],
|
|
2188
2188
|
scripts: {
|
|
2189
2189
|
build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
2190
|
+
"build:js": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
2190
2191
|
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
2191
2192
|
migrate: "bun run src/server/index.ts migrate",
|
|
2192
2193
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
@@ -2241,7 +2242,7 @@ var init_package = __esm(() => {
|
|
|
2241
2242
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
2242
2243
|
license: "Apache-2.0",
|
|
2243
2244
|
dependencies: {
|
|
2244
|
-
"@hasna/contracts": "0.
|
|
2245
|
+
"@hasna/contracts": "0.14.0",
|
|
2245
2246
|
"@hasna/events": "^0.1.11",
|
|
2246
2247
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
2247
2248
|
chalk: "^5.4.1",
|
|
@@ -2851,13 +2852,15 @@ function upsertSecretSafetyConfig(input) {
|
|
|
2851
2852
|
saveConfig({ ...config, secret_safety: next });
|
|
2852
2853
|
return next;
|
|
2853
2854
|
}
|
|
2854
|
-
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
2855
|
+
var XAI_PREFIX, DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
2855
2856
|
var init_redaction = __esm(() => {
|
|
2856
2857
|
init_config();
|
|
2858
|
+
XAI_PREFIX = ["x", "ai", "-"].join("");
|
|
2857
2859
|
DEFAULT_SECRET_PATTERNS = [
|
|
2858
2860
|
{ name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
|
|
2859
2861
|
{ name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
|
|
2860
2862
|
{ name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
2863
|
+
{ name: `${XAI_PREFIX}token`, regex: /\bxai[-][A-Za-z0-9]{20,80}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
2861
2864
|
{ name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
|
|
2862
2865
|
{ name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
2863
2866
|
{ name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
@@ -18314,6 +18317,12 @@ function listTasks(filter = {}, db) {
|
|
|
18314
18317
|
const d = db || getDatabase();
|
|
18315
18318
|
const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
|
|
18316
18319
|
clearExpiredLocks2(d);
|
|
18320
|
+
if (filter.limit !== undefined) {
|
|
18321
|
+
const limit = filter.limit;
|
|
18322
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > MAX_TASK_LIST_LIMIT) {
|
|
18323
|
+
throw new TypeError(`listTasks limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT}; got ${String(limit)}`);
|
|
18324
|
+
}
|
|
18325
|
+
}
|
|
18317
18326
|
const conditions = [];
|
|
18318
18327
|
const params = [];
|
|
18319
18328
|
if (filter.project_id) {
|
|
@@ -18847,6 +18856,7 @@ function deleteTask(id, db) {
|
|
|
18847
18856
|
}
|
|
18848
18857
|
return result.changes > 0;
|
|
18849
18858
|
}
|
|
18859
|
+
var MAX_TASK_LIST_LIMIT = 1e5;
|
|
18850
18860
|
var init_task_crud = __esm(() => {
|
|
18851
18861
|
init_types();
|
|
18852
18862
|
init_database();
|
|
@@ -21798,7 +21808,8 @@ __export(exports_tasks, {
|
|
|
21798
21808
|
buildTaskBoardSnapshot: () => buildTaskBoardSnapshot,
|
|
21799
21809
|
archiveTasks: () => archiveTasks,
|
|
21800
21810
|
archiveCompletedTasks: () => archiveCompletedTasks,
|
|
21801
|
-
addDependency: () => addDependency
|
|
21811
|
+
addDependency: () => addDependency,
|
|
21812
|
+
MAX_TASK_LIST_LIMIT: () => MAX_TASK_LIST_LIMIT
|
|
21802
21813
|
});
|
|
21803
21814
|
var init_tasks = __esm(() => {
|
|
21804
21815
|
init_task_crud();
|
|
@@ -32238,6 +32249,27 @@ var init_project_commands = __esm(() => {
|
|
|
32238
32249
|
};
|
|
32239
32250
|
});
|
|
32240
32251
|
|
|
32252
|
+
// src/storage/postgres-errors.ts
|
|
32253
|
+
function isPostgresUniqueViolation(error) {
|
|
32254
|
+
if (typeof error !== "object" || error === null)
|
|
32255
|
+
return false;
|
|
32256
|
+
const candidate = error;
|
|
32257
|
+
const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
|
|
32258
|
+
if (typeof candidate.cause === "object" && candidate.cause !== null) {
|
|
32259
|
+
const cause = candidate.cause;
|
|
32260
|
+
states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
|
|
32261
|
+
}
|
|
32262
|
+
return states.some((state) => String(state) === "23505");
|
|
32263
|
+
}
|
|
32264
|
+
function postgresConstraintName(error) {
|
|
32265
|
+
if (typeof error !== "object" || error === null)
|
|
32266
|
+
return "";
|
|
32267
|
+
const candidate = error;
|
|
32268
|
+
const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
|
|
32269
|
+
const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
|
|
32270
|
+
return typeof constraint === "string" ? constraint : "";
|
|
32271
|
+
}
|
|
32272
|
+
|
|
32241
32273
|
// src/storage/postgres-sync.ts
|
|
32242
32274
|
function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE, cursorTableName = DEFAULT_TODOS_POSTGRES_CURSOR_TABLE) {
|
|
32243
32275
|
assertSafeIdentifier(tableName);
|
|
@@ -32432,53 +32464,98 @@ class PostgresTodosSyncStore {
|
|
|
32432
32464
|
if (routingErrors.length > 0) {
|
|
32433
32465
|
throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
|
|
32434
32466
|
}
|
|
32435
|
-
const
|
|
32436
|
-
|
|
32437
|
-
|
|
32438
|
-
|
|
32439
|
-
|
|
32440
|
-
|
|
32441
|
-
const
|
|
32442
|
-
|
|
32443
|
-
|
|
32444
|
-
|
|
32445
|
-
|
|
32446
|
-
|
|
32447
|
-
|
|
32448
|
-
|
|
32449
|
-
|
|
32450
|
-
|
|
32451
|
-
|
|
32452
|
-
|
|
32453
|
-
|
|
32454
|
-
|
|
32455
|
-
|
|
32456
|
-
|
|
32457
|
-
|
|
32458
|
-
|
|
32459
|
-
|
|
32460
|
-
|
|
32461
|
-
|
|
32462
|
-
|
|
32463
|
-
|
|
32464
|
-
|
|
32465
|
-
|
|
32466
|
-
|
|
32467
|
-
|
|
32468
|
-
|
|
32469
|
-
|
|
32470
|
-
|
|
32471
|
-
|
|
32472
|
-
|
|
32473
|
-
|
|
32474
|
-
|
|
32475
|
-
|
|
32476
|
-
|
|
32477
|
-
|
|
32478
|
-
|
|
32479
|
-
|
|
32467
|
+
const push = async (client) => {
|
|
32468
|
+
const existing = await client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
32469
|
+
FROM ${this.tableName}
|
|
32470
|
+
WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
|
|
32471
|
+
const existingProjects = [];
|
|
32472
|
+
const existingTaskLists = [];
|
|
32473
|
+
for (const row of existing.rows) {
|
|
32474
|
+
const payload = payloadRecord(row.payload);
|
|
32475
|
+
if (row.object_type === "projects")
|
|
32476
|
+
existingProjects.push(payload);
|
|
32477
|
+
if (row.object_type === "task_lists")
|
|
32478
|
+
existingTaskLists.push(payload);
|
|
32479
|
+
}
|
|
32480
|
+
const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
|
|
32481
|
+
if (destinationErrors.length > 0) {
|
|
32482
|
+
throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
|
|
32483
|
+
}
|
|
32484
|
+
const result = { records: 0, objectTypes: {} };
|
|
32485
|
+
const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
|
|
32486
|
+
for (const entry of snapshotEntries(snapshot)) {
|
|
32487
|
+
if (entry.deletedAt === null)
|
|
32488
|
+
assertCanonicalScopedSlugEntry(entry);
|
|
32489
|
+
try {
|
|
32490
|
+
await client.query(`INSERT INTO ${this.tableName} (
|
|
32491
|
+
service, object_type, object_id, payload, updated_at,
|
|
32492
|
+
deleted_at, source_machine_id, version
|
|
32493
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
|
|
32494
|
+
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
32495
|
+
payload = EXCLUDED.payload,
|
|
32496
|
+
updated_at = EXCLUDED.updated_at,
|
|
32497
|
+
deleted_at = EXCLUDED.deleted_at,
|
|
32498
|
+
source_machine_id = EXCLUDED.source_machine_id,
|
|
32499
|
+
version = EXCLUDED.version
|
|
32500
|
+
WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
|
|
32501
|
+
OR (${this.tableName}.updated_at = EXCLUDED.updated_at
|
|
32502
|
+
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
|
|
32503
|
+
this.service,
|
|
32504
|
+
entry.type,
|
|
32505
|
+
entry.id,
|
|
32506
|
+
entry.payload,
|
|
32507
|
+
entry.updatedAt,
|
|
32508
|
+
entry.deletedAt,
|
|
32509
|
+
sourceMachineId,
|
|
32510
|
+
entry.version
|
|
32511
|
+
]);
|
|
32512
|
+
} catch (error) {
|
|
32513
|
+
await this.classifySyncInsertConflict(error, entry);
|
|
32514
|
+
}
|
|
32515
|
+
result.records += 1;
|
|
32516
|
+
result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
|
|
32517
|
+
}
|
|
32518
|
+
return result;
|
|
32519
|
+
};
|
|
32520
|
+
if (typeof this.client.transaction === "function") {
|
|
32521
|
+
return this.client.transaction((client) => push(client));
|
|
32480
32522
|
}
|
|
32481
|
-
return
|
|
32523
|
+
return push(this.client);
|
|
32524
|
+
}
|
|
32525
|
+
async classifySyncInsertConflict(error, entry) {
|
|
32526
|
+
if (!isPostgresUniqueViolation(error))
|
|
32527
|
+
throw error;
|
|
32528
|
+
const payload = entry.payload;
|
|
32529
|
+
const taskListSlug = String(payload["slug"] ?? "");
|
|
32530
|
+
const projectSlug = String(payload["task_list_id"] ?? "");
|
|
32531
|
+
const constraintName = postgresConstraintName(error);
|
|
32532
|
+
if (entry.type === "projects" && constraintName.includes("project_task_list_slug_uidx")) {
|
|
32533
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
|
|
32534
|
+
}
|
|
32535
|
+
if (entry.type === "task_lists" && constraintName.includes("task_list_scope_slug_uidx")) {
|
|
32536
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
|
|
32537
|
+
}
|
|
32538
|
+
if (entry.type === "task_lists") {
|
|
32539
|
+
const scope = typeof payload["project_id"] === "string" ? payload["project_id"] : "";
|
|
32540
|
+
const conflict = await this.client.query(`/* todos:classify-sync-task-list-conflict */ SELECT EXISTS (
|
|
32541
|
+
SELECT 1 FROM ${this.tableName}
|
|
32542
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id <> $2
|
|
32543
|
+
AND deleted_at IS NULL AND COALESCE(payload->>'project_id','') = $3 AND payload->>'slug' = $4
|
|
32544
|
+
) AS conflict`, [this.service, entry.id, scope, taskListSlug]);
|
|
32545
|
+
if (conflict.rows[0]?.conflict) {
|
|
32546
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
|
|
32547
|
+
}
|
|
32548
|
+
} else if (entry.type === "projects") {
|
|
32549
|
+
const conflict = await this.client.query(`/* todos:classify-sync-project-conflict */ SELECT EXISTS (
|
|
32550
|
+
SELECT 1 FROM ${this.tableName}
|
|
32551
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id <> $2
|
|
32552
|
+
AND deleted_at IS NULL AND payload->>'task_list_id' = $3
|
|
32553
|
+
) AS conflict`, [this.service, entry.id, projectSlug]);
|
|
32554
|
+
if (conflict.rows[0]?.conflict) {
|
|
32555
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
|
|
32556
|
+
}
|
|
32557
|
+
}
|
|
32558
|
+
throw error;
|
|
32482
32559
|
}
|
|
32483
32560
|
async pullSnapshot(options = {}) {
|
|
32484
32561
|
const params = [this.service];
|
|
@@ -32635,6 +32712,7 @@ function assertSafeIdentifier(value) {
|
|
|
32635
32712
|
}
|
|
32636
32713
|
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records", DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors", PostgresScopedSlugMigrationConflictError, PostgresScopedSlugIndexBuildError;
|
|
32637
32714
|
var init_postgres_sync = __esm(() => {
|
|
32715
|
+
init_types();
|
|
32638
32716
|
PostgresScopedSlugMigrationConflictError = class PostgresScopedSlugMigrationConflictError extends Error {
|
|
32639
32717
|
conflicts;
|
|
32640
32718
|
constructor(conflicts) {
|
|
@@ -35199,25 +35277,6 @@ async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
|
|
|
35199
35277
|
}
|
|
35200
35278
|
throw lastError;
|
|
35201
35279
|
}
|
|
35202
|
-
function isPostgresUniqueViolation(error) {
|
|
35203
|
-
if (typeof error !== "object" || error === null)
|
|
35204
|
-
return false;
|
|
35205
|
-
const candidate = error;
|
|
35206
|
-
const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
|
|
35207
|
-
if (typeof candidate.cause === "object" && candidate.cause !== null) {
|
|
35208
|
-
const cause = candidate.cause;
|
|
35209
|
-
states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
|
|
35210
|
-
}
|
|
35211
|
-
return states.some((state) => String(state) === "23505");
|
|
35212
|
-
}
|
|
35213
|
-
function postgresConstraintName(error) {
|
|
35214
|
-
if (typeof error !== "object" || error === null)
|
|
35215
|
-
return "";
|
|
35216
|
-
const candidate = error;
|
|
35217
|
-
const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
|
|
35218
|
-
const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
|
|
35219
|
-
return typeof constraint === "string" ? constraint : "";
|
|
35220
|
-
}
|
|
35221
35280
|
var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC", TASK_ORDER_BY, TRANSIENT_POSTGRES_SQLSTATES, TRANSIENT_POSTGRES_MESSAGE_MARKERS;
|
|
35222
35281
|
var init_postgres_adapter = __esm(() => {
|
|
35223
35282
|
init_types();
|
|
@@ -52194,6 +52253,13 @@ function resolveSigningSecret(env = process.env) {
|
|
|
52194
52253
|
function isPostgresBackendConfigured(env = process.env) {
|
|
52195
52254
|
return Boolean(resolveCloudDatabaseUrl(env));
|
|
52196
52255
|
}
|
|
52256
|
+
function schemaRetryMinIntervalMs(env = process.env) {
|
|
52257
|
+
const raw = env.HASNA_TODOS_SCHEMA_RETRY_MIN_MS;
|
|
52258
|
+
if (!raw)
|
|
52259
|
+
return DEFAULT_SCHEMA_RETRY_MIN_MS;
|
|
52260
|
+
const parsed = Number(raw);
|
|
52261
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_SCHEMA_RETRY_MIN_MS;
|
|
52262
|
+
}
|
|
52197
52263
|
function getCloudTenantId() {
|
|
52198
52264
|
return process.env.HASNA_TODOS_TENANT_ID ?? "default";
|
|
52199
52265
|
}
|
|
@@ -52289,6 +52355,10 @@ function getCloudVerifier() {
|
|
|
52289
52355
|
async function ensureCloudSchema() {
|
|
52290
52356
|
if (schemaEnsured)
|
|
52291
52357
|
return schemaEnsured;
|
|
52358
|
+
if (lastSchemaFailure !== null && Date.now() - lastSchemaAttemptAtMs < schemaRetryMinIntervalMs()) {
|
|
52359
|
+
throw lastSchemaFailure;
|
|
52360
|
+
}
|
|
52361
|
+
lastSchemaAttemptAtMs = Date.now();
|
|
52292
52362
|
schemaEnsured = (async () => {
|
|
52293
52363
|
const client = getClient();
|
|
52294
52364
|
for (const sql of postgresTodosSyncSchemaSql()) {
|
|
@@ -52309,6 +52379,7 @@ async function ensureCloudSchema() {
|
|
|
52309
52379
|
await getApiKeyStore().ensureSchema();
|
|
52310
52380
|
})().catch((error) => {
|
|
52311
52381
|
schemaEnsured = null;
|
|
52382
|
+
lastSchemaFailure = error;
|
|
52312
52383
|
throw error;
|
|
52313
52384
|
});
|
|
52314
52385
|
return schemaEnsured;
|
|
@@ -52356,8 +52427,10 @@ async function closeCloud() {
|
|
|
52356
52427
|
cachedTaskManifestAuthority = null;
|
|
52357
52428
|
cachedTaskSubtreeTransferAuthority = null;
|
|
52358
52429
|
schemaEnsured = null;
|
|
52430
|
+
lastSchemaAttemptAtMs = 0;
|
|
52431
|
+
lastSchemaFailure = null;
|
|
52359
52432
|
}
|
|
52360
|
-
var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, cachedTaskManifestAuthority = null, cachedTaskSubtreeTransferAuthority = null, schemaEnsured = null;
|
|
52433
|
+
var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, cachedTaskManifestAuthority = null, cachedTaskSubtreeTransferAuthority = null, schemaEnsured = null, lastSchemaAttemptAtMs = 0, lastSchemaFailure = null, DEFAULT_SCHEMA_RETRY_MIN_MS = 1e4;
|
|
52361
52434
|
var init_cloud = __esm(() => {
|
|
52362
52435
|
init_cloud_client();
|
|
52363
52436
|
init_postgres_adapter();
|
|
@@ -53223,6 +53296,19 @@ function taskStatusQueryParam(url) {
|
|
|
53223
53296
|
return { ok: false, message: result.message };
|
|
53224
53297
|
return { ok: true, value: collapseEnumValues(result.values) };
|
|
53225
53298
|
}
|
|
53299
|
+
function parsePaginationQueryParam(url, name) {
|
|
53300
|
+
const raw = url.searchParams.get(name);
|
|
53301
|
+
if (raw === null)
|
|
53302
|
+
return { ok: true, value: undefined };
|
|
53303
|
+
const min = name === "limit" ? 1 : 0;
|
|
53304
|
+
const message = name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer";
|
|
53305
|
+
if (!/^\d+$/.test(raw))
|
|
53306
|
+
return { ok: false, message };
|
|
53307
|
+
const value = Number(raw);
|
|
53308
|
+
if (!Number.isSafeInteger(value) || value < min)
|
|
53309
|
+
return { ok: false, message };
|
|
53310
|
+
return { ok: true, value };
|
|
53311
|
+
}
|
|
53226
53312
|
async function handleListTasks(_req, url, _ctx, json5, taskToSummary2) {
|
|
53227
53313
|
const statusParam = taskStatusQueryParam(url);
|
|
53228
53314
|
if (!statusParam.ok)
|
|
@@ -53230,16 +53316,20 @@ async function handleListTasks(_req, url, _ctx, json5, taskToSummary2) {
|
|
|
53230
53316
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
53231
53317
|
const sessionId = url.searchParams.get("session_id") || undefined;
|
|
53232
53318
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
53233
|
-
const limitParam = url
|
|
53234
|
-
|
|
53319
|
+
const limitParam = parsePaginationQueryParam(url, "limit");
|
|
53320
|
+
if (!limitParam.ok)
|
|
53321
|
+
return json5({ error: limitParam.message }, 400);
|
|
53322
|
+
const offsetParam = parsePaginationQueryParam(url, "offset");
|
|
53323
|
+
if (!offsetParam.ok)
|
|
53324
|
+
return json5({ error: offsetParam.message }, 400);
|
|
53235
53325
|
const fields = parseFieldsParam(url);
|
|
53236
53326
|
const tasks = listTasks({
|
|
53237
53327
|
status: statusParam.value,
|
|
53238
53328
|
project_id: projectId,
|
|
53239
53329
|
session_id: sessionId,
|
|
53240
53330
|
agent_id: agentId,
|
|
53241
|
-
limit: limitParam
|
|
53242
|
-
offset: offsetParam
|
|
53331
|
+
limit: limitParam.value,
|
|
53332
|
+
offset: offsetParam.value
|
|
53243
53333
|
});
|
|
53244
53334
|
return json5(tasks.map((t) => taskToSummary2(t, fields)));
|
|
53245
53335
|
}
|
|
@@ -54031,6 +54121,12 @@ class TodosShadowOutbox {
|
|
|
54031
54121
|
this.onEvent?.({ type: "mirrored", objectType: row.object_type, id: row.object_id, lagMs });
|
|
54032
54122
|
} catch (error) {
|
|
54033
54123
|
const message = error instanceof Error ? error.message : String(error);
|
|
54124
|
+
if (error instanceof ResourceConflictError) {
|
|
54125
|
+
this.metrics.lastError = message;
|
|
54126
|
+
this.db.run(`UPDATE shadow_outbox SET attempts=?, last_error=?, status='failed' WHERE seq=? AND revision=?`, [row.attempts + 1, message, row.seq, row.revision]);
|
|
54127
|
+
this.onEvent?.({ type: "parked", objectType: row.object_type, id: row.object_id, error: message });
|
|
54128
|
+
return;
|
|
54129
|
+
}
|
|
54034
54130
|
this.metrics.retries += 1;
|
|
54035
54131
|
this.metrics.lastError = message;
|
|
54036
54132
|
const attempts = row.attempts + 1;
|
|
@@ -54158,6 +54254,7 @@ function createTodosShadowOutbox(options) {
|
|
|
54158
54254
|
}
|
|
54159
54255
|
var MAX_BACKOFF_MS;
|
|
54160
54256
|
var init_shadow_outbox = __esm(() => {
|
|
54257
|
+
init_types();
|
|
54161
54258
|
init_local_sqlite();
|
|
54162
54259
|
init_postgres_sync();
|
|
54163
54260
|
init_shadow_outbox_schema();
|
|
@@ -58132,6 +58229,26 @@ function parseSinceCursor(raw) {
|
|
|
58132
58229
|
}
|
|
58133
58230
|
return { ok: true, value: new Date(parsed).toISOString() };
|
|
58134
58231
|
}
|
|
58232
|
+
function paginationQueryParam(url, name) {
|
|
58233
|
+
const raw = url.searchParams.get(name);
|
|
58234
|
+
if (raw === null)
|
|
58235
|
+
return { ok: true, value: undefined };
|
|
58236
|
+
const min = name === "limit" ? 1 : 0;
|
|
58237
|
+
if (!/^\d+$/.test(raw)) {
|
|
58238
|
+
return {
|
|
58239
|
+
ok: false,
|
|
58240
|
+
response: error(400, name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer")
|
|
58241
|
+
};
|
|
58242
|
+
}
|
|
58243
|
+
const value = Number(raw);
|
|
58244
|
+
if (!Number.isSafeInteger(value) || value < min) {
|
|
58245
|
+
return {
|
|
58246
|
+
ok: false,
|
|
58247
|
+
response: error(400, name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer")
|
|
58248
|
+
};
|
|
58249
|
+
}
|
|
58250
|
+
return { ok: true, value };
|
|
58251
|
+
}
|
|
58135
58252
|
function validateTaskCompletion(value) {
|
|
58136
58253
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
58137
58254
|
return { ok: false, message: "completion body must be an object" };
|
|
@@ -58660,6 +58777,12 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
58660
58777
|
if (updatedAfter !== null && !updatedAfter.ok) {
|
|
58661
58778
|
return error(400, updatedAfter.message);
|
|
58662
58779
|
}
|
|
58780
|
+
const limitParam = paginationQueryParam(url, "limit");
|
|
58781
|
+
if (!limitParam.ok)
|
|
58782
|
+
return limitParam.response;
|
|
58783
|
+
const offsetParam = paginationQueryParam(url, "offset");
|
|
58784
|
+
if (!offsetParam.ok)
|
|
58785
|
+
return offsetParam.response;
|
|
58663
58786
|
const filter = {
|
|
58664
58787
|
...updatedAfter !== null && updatedAfter.ok ? { updated_after: updatedAfter.value } : {},
|
|
58665
58788
|
...url.searchParams.get("q") ? { query: url.searchParams.get("q") } : {},
|
|
@@ -58676,8 +58799,8 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
58676
58799
|
...url.searchParams.get("tags") ? {
|
|
58677
58800
|
tags: url.searchParams.get("tags").split(",").map((tag) => tag.trim()).filter(Boolean)
|
|
58678
58801
|
} : {},
|
|
58679
|
-
...
|
|
58680
|
-
...
|
|
58802
|
+
...limitParam.value !== undefined ? { limit: limitParam.value } : {},
|
|
58803
|
+
...offsetParam.value !== undefined ? { offset: offsetParam.value } : {}
|
|
58681
58804
|
};
|
|
58682
58805
|
const tasks = await store.tasks.list(filter);
|
|
58683
58806
|
const { limit: _l, offset: _o, ...countFilter } = filter;
|
|
@@ -60437,8 +60560,8 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
60437
60560
|
tags: exports_external.array(exports_external.string()).optional().describe("Filter by tags (AND logic)"),
|
|
60438
60561
|
created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
|
|
60439
60562
|
created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
|
|
60440
|
-
limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
|
|
60441
|
-
offset: exports_external.number().optional().describe("Pagination offset"),
|
|
60563
|
+
limit: exports_external.number().int().min(1).optional().describe("Max results (default: 50, max 500)"),
|
|
60564
|
+
offset: exports_external.number().int().min(0).optional().describe("Pagination offset"),
|
|
60442
60565
|
metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
|
|
60443
60566
|
}, async (params) => {
|
|
60444
60567
|
try {
|
|
@@ -62400,6 +62523,48 @@ var init_task_relationships = __esm(() => {
|
|
|
62400
62523
|
];
|
|
62401
62524
|
});
|
|
62402
62525
|
|
|
62526
|
+
// src/lib/dedupe-projection.ts
|
|
62527
|
+
function projectTasksForDedupe(tasks) {
|
|
62528
|
+
return tasks.map((task2) => {
|
|
62529
|
+
const metadata = {};
|
|
62530
|
+
for (const key2 of DEDUPE_SOURCE_KEY_ALLOWLIST) {
|
|
62531
|
+
if (key2 in task2.metadata) {
|
|
62532
|
+
metadata[key2] = redactValue(task2.metadata[key2]);
|
|
62533
|
+
}
|
|
62534
|
+
}
|
|
62535
|
+
return {
|
|
62536
|
+
id: task2.id,
|
|
62537
|
+
short_id: task2.short_id,
|
|
62538
|
+
title: redactEvidenceText(task2.title),
|
|
62539
|
+
description: task2.description === null || task2.description === undefined ? null : redactEvidenceText(task2.description),
|
|
62540
|
+
status: task2.status,
|
|
62541
|
+
created_at: task2.created_at,
|
|
62542
|
+
updated_at: task2.updated_at,
|
|
62543
|
+
project_id: task2.project_id,
|
|
62544
|
+
task_list_id: task2.task_list_id,
|
|
62545
|
+
assigned_to: task2.assigned_to,
|
|
62546
|
+
priority: task2.priority,
|
|
62547
|
+
metadata
|
|
62548
|
+
};
|
|
62549
|
+
});
|
|
62550
|
+
}
|
|
62551
|
+
var DEDUPE_SOURCE_KEY_ALLOWLIST;
|
|
62552
|
+
var init_dedupe_projection = __esm(() => {
|
|
62553
|
+
init_redaction();
|
|
62554
|
+
DEDUPE_SOURCE_KEY_ALLOWLIST = [
|
|
62555
|
+
"github_url",
|
|
62556
|
+
"github_issue_url",
|
|
62557
|
+
"github_pr_url",
|
|
62558
|
+
"source_url",
|
|
62559
|
+
"url",
|
|
62560
|
+
"external_url",
|
|
62561
|
+
"issue_url",
|
|
62562
|
+
"github_owner",
|
|
62563
|
+
"github_repo",
|
|
62564
|
+
"github_number"
|
|
62565
|
+
];
|
|
62566
|
+
});
|
|
62567
|
+
|
|
62403
62568
|
// src/lib/task-dedupe.ts
|
|
62404
62569
|
function asObject(value) {
|
|
62405
62570
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -62564,10 +62729,10 @@ function olderFirst(left, right) {
|
|
|
62564
62729
|
function findDuplicateTasks(options = {}, db) {
|
|
62565
62730
|
const d = db || getDatabase();
|
|
62566
62731
|
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
62567
|
-
const fingerprints = listTasks({
|
|
62732
|
+
const fingerprints = projectTasksForDedupe(listTasks({
|
|
62568
62733
|
include_archived: Boolean(options.include_archived),
|
|
62569
62734
|
limit: options.limit ?? 1000
|
|
62570
|
-
}, d).map(fingerprint);
|
|
62735
|
+
}, d)).map(fingerprint);
|
|
62571
62736
|
const candidates = [];
|
|
62572
62737
|
for (let i = 0;i < fingerprints.length; i++) {
|
|
62573
62738
|
for (let j = i + 1;j < fingerprints.length; j++) {
|
|
@@ -62802,6 +62967,7 @@ var init_task_dedupe = __esm(() => {
|
|
|
62802
62967
|
init_database();
|
|
62803
62968
|
init_task_relationships();
|
|
62804
62969
|
init_tasks();
|
|
62970
|
+
init_dedupe_projection();
|
|
62805
62971
|
STOP_WORDS = new Set([
|
|
62806
62972
|
"a",
|
|
62807
62973
|
"an",
|
|
@@ -87273,13 +87439,25 @@ Findings`));
|
|
|
87273
87439
|
handleError(e);
|
|
87274
87440
|
}
|
|
87275
87441
|
});
|
|
87442
|
+
function parseDedupeLimit(raw) {
|
|
87443
|
+
const value = Number(raw);
|
|
87444
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_TASK_LIST_LIMIT) {
|
|
87445
|
+
return null;
|
|
87446
|
+
}
|
|
87447
|
+
return value;
|
|
87448
|
+
}
|
|
87276
87449
|
const dedupe2 = program2.command("dedupe").description("Find and merge likely duplicate local tasks");
|
|
87277
87450
|
dedupe2.command("scan").description("Scan local tasks for likely duplicates").option("--threshold <n>", "Minimum duplicate score from 0 to 1", "0.74").option("--limit <n>", "Maximum tasks to compare", "1000").option("--include-archived", "Include archived tasks").option("-j, --json", "Output as JSON").action((opts) => {
|
|
87278
87451
|
const globalOpts = program2.opts();
|
|
87279
87452
|
try {
|
|
87453
|
+
const limit = parseDedupeLimit(opts.limit);
|
|
87454
|
+
if (limit === null) {
|
|
87455
|
+
handleError(new Error(`--limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT} (got: ${opts.limit})`));
|
|
87456
|
+
return;
|
|
87457
|
+
}
|
|
87280
87458
|
const candidates = findDuplicateTasks({
|
|
87281
87459
|
threshold: Number(opts.threshold),
|
|
87282
|
-
limit
|
|
87460
|
+
limit,
|
|
87283
87461
|
include_archived: Boolean(opts.includeArchived)
|
|
87284
87462
|
});
|
|
87285
87463
|
if (opts.json || globalOpts.json) {
|
|
@@ -87299,6 +87477,35 @@ Findings`));
|
|
|
87299
87477
|
handleError(e);
|
|
87300
87478
|
}
|
|
87301
87479
|
});
|
|
87480
|
+
dedupe2.command("project <ref>").description("Project a project's tasks into the bounded dedup projection (safe to emit: no free-form metadata, tags, comments, or run fields)").option("--limit <n>", "Maximum tasks to project", "1000").option("--include-archived", "Include archived tasks").option("-j, --json", "Output as JSON").action((ref, opts) => {
|
|
87481
|
+
const globalOpts = program2.opts();
|
|
87482
|
+
try {
|
|
87483
|
+
const d = getDatabase();
|
|
87484
|
+
const projectId = resolvePartialId(d, "projects", ref) || d.query("SELECT id FROM projects WHERE path = ? OR name = ? OR task_list_id = ?").get(ref, ref, ref)?.id;
|
|
87485
|
+
if (!projectId) {
|
|
87486
|
+
handleError(new Error(`Could not resolve project: ${ref}`));
|
|
87487
|
+
return;
|
|
87488
|
+
}
|
|
87489
|
+
const limit = parseDedupeLimit(opts.limit);
|
|
87490
|
+
if (limit === null) {
|
|
87491
|
+
handleError(new Error(`--limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT} (got: ${opts.limit})`));
|
|
87492
|
+
return;
|
|
87493
|
+
}
|
|
87494
|
+
const tasks = listTasks({
|
|
87495
|
+
project_id: projectId,
|
|
87496
|
+
include_archived: Boolean(opts.includeArchived),
|
|
87497
|
+
limit
|
|
87498
|
+
}, d);
|
|
87499
|
+
const projected = projectTasksForDedupe(tasks);
|
|
87500
|
+
if (opts.json || globalOpts.json) {
|
|
87501
|
+
output({ project_id: projectId, count: projected.length, tasks: projected }, true);
|
|
87502
|
+
return;
|
|
87503
|
+
}
|
|
87504
|
+
console.log(chalk9.dim(`${projected.length} task(s) projected for dedupe (bounded \u2014 no free-form metadata, tags, comments, or run fields).`));
|
|
87505
|
+
} catch (e) {
|
|
87506
|
+
handleError(e);
|
|
87507
|
+
}
|
|
87508
|
+
});
|
|
87302
87509
|
dedupe2.command("merge <primary-task-id> <duplicate-task-id>").description("Merge a duplicate task into a primary task and archive the duplicate").option("--agent <agent>", "Agent ID recording the merge").option("--reason <reason>", "Human-readable merge reason").option("-j, --json", "Output as JSON").action((primaryTaskId, duplicateTaskId, opts) => {
|
|
87303
87510
|
const globalOpts = program2.opts();
|
|
87304
87511
|
try {
|
|
@@ -87603,6 +87810,7 @@ var init_query_commands = __esm(() => {
|
|
|
87603
87810
|
init_audit();
|
|
87604
87811
|
init_handoffs();
|
|
87605
87812
|
init_task_dedupe();
|
|
87813
|
+
init_dedupe_projection();
|
|
87606
87814
|
init_local_fields();
|
|
87607
87815
|
init_workflow_states();
|
|
87608
87816
|
init_local_reports();
|
|
@@ -95508,6 +95716,12 @@ class TodosShadowMirror {
|
|
|
95508
95716
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
95509
95717
|
const objectType2 = op.kind === "upsert" ? SNAPSHOT_TO_OBJECT_TYPE[op.key] : op.objectType;
|
|
95510
95718
|
const id = op.kind === "upsert" ? String(op.record["id"]) : op.id;
|
|
95719
|
+
if (error2 instanceof ResourceConflictError) {
|
|
95720
|
+
this.metrics.failed += 1;
|
|
95721
|
+
this.metrics.lastError = message;
|
|
95722
|
+
this.onEvent?.({ type: "dropped", objectType: objectType2, id, error: message });
|
|
95723
|
+
return;
|
|
95724
|
+
}
|
|
95511
95725
|
this.metrics.retries += 1;
|
|
95512
95726
|
this.metrics.lastError = message;
|
|
95513
95727
|
op.attempts += 1;
|
|
@@ -95755,6 +95969,7 @@ function isAgent(value) {
|
|
|
95755
95969
|
}
|
|
95756
95970
|
var SNAPSHOT_TO_OBJECT_TYPE;
|
|
95757
95971
|
var init_shadow = __esm(() => {
|
|
95972
|
+
init_types();
|
|
95758
95973
|
init_local_sqlite();
|
|
95759
95974
|
init_postgres_sync();
|
|
95760
95975
|
SNAPSHOT_TO_OBJECT_TYPE = {
|